Merge branch 'develop' into issue/1023

This commit is contained in:
Maksim Pechnikov 2020-07-16 08:53:21 +03:00
commit 6c59fe259d
218 changed files with 1736 additions and 2586 deletions

View file

@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- MFR policy to set global expiration for all local Create activities - MFR policy to set global expiration for all local Create activities
- OGP rich media parser merged with TwitterCard - OGP rich media parser merged with TwitterCard
- Configuration: `:instance, rewrite_policy` moved to `:mrf, policies`, `:instance, :mrf_transparency` moved to `:mrf, :transparency`, `:instance, :mrf_transparency_exclusions` moved to `:mrf, :transparency_exclusions`. Old config namespace is deprecated. - Configuration: `:instance, rewrite_policy` moved to `:mrf, policies`, `:instance, :mrf_transparency` moved to `:mrf, :transparency`, `:instance, :mrf_transparency_exclusions` moved to `:mrf, :transparency_exclusions`. Old config namespace is deprecated.
- Configuration: `:media_proxy, whitelist` format changed to host with scheme (e.g. `http://example.com` instead of `example.com`). Domain format is deprecated.
<details> <details>
<summary>API Changes</summary> <summary>API Changes</summary>
@ -24,6 +25,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mastodon API: Added `pleroma.metadata.fields_limits` to /api/v1/instance - Mastodon API: Added `pleroma.metadata.fields_limits` to /api/v1/instance
- Mastodon API: On deletion, returns the original post text. - Mastodon API: On deletion, returns the original post text.
- Mastodon API: Add `pleroma.unread_count` to the Marker entity. - Mastodon API: Add `pleroma.unread_count` to the Marker entity.
- **Breaking:** Notification Settings API for suppressing notifications
has been simplified down to `block_from_strangers`.
- **Breaking:** Notification Settings API option for hiding push notification
contents has been renamed to `hide_notification_contents`
</details> </details>
<details> <details>

View file

@ -172,7 +172,7 @@
"application/ld+json" => ["activity+json"] "application/ld+json" => ["activity+json"]
} }
config :tesla, adapter: Tesla.Adapter.Hackney config :tesla, adapter: Tesla.Adapter.Gun
# Configures http settings, upstream proxy etc. # Configures http settings, upstream proxy etc.
config :pleroma, :http, config :pleroma, :http,
@ -512,6 +512,7 @@
attachments_cleanup: 5, attachments_cleanup: 5,
new_users_digest: 1 new_users_digest: 1
], ],
plugins: [Oban.Plugins.Pruner],
crontab: [ crontab: [
{"0 0 * * *", Pleroma.Workers.Cron.ClearOauthTokenWorker}, {"0 0 * * *", Pleroma.Workers.Cron.ClearOauthTokenWorker},
{"0 * * * *", Pleroma.Workers.Cron.StatsWorker}, {"0 * * * *", Pleroma.Workers.Cron.StatsWorker},
@ -647,32 +648,30 @@
prepare: :unnamed prepare: :unnamed
config :pleroma, :connections_pool, config :pleroma, :connections_pool,
checkin_timeout: 250, reclaim_multiplier: 0.1,
connection_acquisition_wait: 250,
connection_acquisition_retries: 5,
max_connections: 250, max_connections: 250,
retry: 1, max_idle_time: 30_000,
retry_timeout: 1000, retry: 0,
await_up_timeout: 5_000 await_up_timeout: 5_000
config :pleroma, :pools, config :pleroma, :pools,
federation: [ federation: [
size: 50, size: 50,
max_overflow: 10, max_waiting: 10
timeout: 150_000
], ],
media: [ media: [
size: 50, size: 50,
max_overflow: 10, max_waiting: 10
timeout: 150_000
], ],
upload: [ upload: [
size: 25, size: 25,
max_overflow: 5, max_waiting: 5
timeout: 300_000
], ],
default: [ default: [
size: 10, size: 10,
max_overflow: 2, max_waiting: 2
timeout: 10_000
] ]
config :pleroma, :hackney_pools, config :pleroma, :hackney_pools,

View file

@ -1768,8 +1768,8 @@
%{ %{
key: :whitelist, key: :whitelist,
type: {:list, :string}, type: {:list, :string},
description: "List of domains to bypass the mediaproxy", description: "List of hosts with scheme to bypass the mediaproxy",
suggestions: ["example.com"] suggestions: ["http://example.com"]
} }
] ]
}, },
@ -2008,13 +2008,15 @@
label: "Pleroma Admin Token", label: "Pleroma Admin Token",
type: :group, type: :group,
description: description:
"Allows to set a token that can be used to authenticate with the admin api without using an actual user by giving it as the `admin_token` parameter", "Allows setting a token that can be used to authenticate requests with admin privileges without a normal user account token. Append the `admin_token` parameter to requests to utilize it. (Please reconsider using HTTP Basic Auth or OAuth-based authentication if possible)",
children: [ children: [
%{ %{
key: :admin_token, key: :admin_token,
type: :string, type: :string,
description: "Admin token", description: "Admin token",
suggestions: ["We recommend a secure random string or UUID"] suggestions: [
"Please use a high entropy string or UUID"
]
} }
] ]
}, },
@ -3159,36 +3161,37 @@
description: "Advanced settings for `gun` connections pool", description: "Advanced settings for `gun` connections pool",
children: [ children: [
%{ %{
key: :checkin_timeout, key: :connection_acquisition_wait,
type: :integer, type: :integer,
description: "Timeout to checkin connection from pool. Default: 250ms.", description:
"Timeout to acquire a connection from pool.The total max time is this value multiplied by the number of retries. Default: 250ms.",
suggestions: [250] suggestions: [250]
}, },
%{
key: :connection_acquisition_retries,
type: :integer,
description:
"Number of attempts to acquire the connection from the pool if it is overloaded. Default: 5",
suggestions: [5]
},
%{ %{
key: :max_connections, key: :max_connections,
type: :integer, type: :integer,
description: "Maximum number of connections in the pool. Default: 250 connections.", description: "Maximum number of connections in the pool. Default: 250 connections.",
suggestions: [250] suggestions: [250]
}, },
%{
key: :retry,
type: :integer,
description:
"Number of retries, while `gun` will try to reconnect if connection goes down. Default: 1.",
suggestions: [1]
},
%{
key: :retry_timeout,
type: :integer,
description:
"Time between retries when `gun` will try to reconnect in milliseconds. Default: 1000ms.",
suggestions: [1000]
},
%{ %{
key: :await_up_timeout, key: :await_up_timeout,
type: :integer, type: :integer,
description: "Timeout while `gun` will wait until connection is up. Default: 5000ms.", description: "Timeout while `gun` will wait until connection is up. Default: 5000ms.",
suggestions: [5000] suggestions: [5000]
},
%{
key: :reclaim_multiplier,
type: :integer,
description:
"Multiplier for the number of idle connection to be reclaimed if the pool is full. For example if the pool maxes out at 250 connections and this setting is set to 0.3, the pool will reclaim at most 75 idle connections if it's overloaded. Default: 0.1",
suggestions: [0.1]
} }
] ]
}, },
@ -3197,108 +3200,29 @@
key: :pools, key: :pools,
type: :group, type: :group,
description: "Advanced settings for `gun` workers pools", description: "Advanced settings for `gun` workers pools",
children: [ children:
Enum.map([:federation, :media, :upload, :default], fn pool_name ->
%{ %{
key: :federation, key: pool_name,
type: :keyword, type: :keyword,
description: "Settings for federation pool.", description: "Settings for #{pool_name} pool.",
children: [ children: [
%{ %{
key: :size, key: :size,
type: :integer, type: :integer,
description: "Number workers in the pool.", description: "Maximum number of concurrent requests in the pool.",
suggestions: [50] suggestions: [50]
}, },
%{ %{
key: :max_overflow, key: :max_waiting,
type: :integer, type: :integer,
description: "Number of additional workers if pool is under load.", description:
"Maximum number of requests waiting for other requests to finish. After this number is reached, the pool will start returning errrors when a new request is made",
suggestions: [10] suggestions: [10]
},
%{
key: :timeout,
type: :integer,
description: "Timeout while `gun` will wait for response.",
suggestions: [150_000]
}
]
},
%{
key: :media,
type: :keyword,
description: "Settings for media pool.",
children: [
%{
key: :size,
type: :integer,
description: "Number workers in the pool.",
suggestions: [50]
},
%{
key: :max_overflow,
type: :integer,
description: "Number of additional workers if pool is under load.",
suggestions: [10]
},
%{
key: :timeout,
type: :integer,
description: "Timeout while `gun` will wait for response.",
suggestions: [150_000]
}
]
},
%{
key: :upload,
type: :keyword,
description: "Settings for upload pool.",
children: [
%{
key: :size,
type: :integer,
description: "Number workers in the pool.",
suggestions: [25]
},
%{
key: :max_overflow,
type: :integer,
description: "Number of additional workers if pool is under load.",
suggestions: [5]
},
%{
key: :timeout,
type: :integer,
description: "Timeout while `gun` will wait for response.",
suggestions: [300_000]
}
]
},
%{
key: :default,
type: :keyword,
description: "Settings for default pool.",
children: [
%{
key: :size,
type: :integer,
description: "Number workers in the pool.",
suggestions: [10]
},
%{
key: :max_overflow,
type: :integer,
description: "Number of additional workers if pool is under load.",
suggestions: [2]
},
%{
key: :timeout,
type: :integer,
description: "Timeout while `gun` will wait for response.",
suggestions: [10_000]
} }
] ]
} }
] end)
}, },
%{ %{
group: :pleroma, group: :pleroma,

View file

@ -113,6 +113,11 @@
config :pleroma, :instances_favicons, enabled: true config :pleroma, :instances_favicons, enabled: true
config :pleroma, Pleroma.Uploaders.S3,
bucket: nil,
streaming_enabled: true,
public_endpoint: nil
if File.exists?("./config/test.secret.exs") do if File.exists?("./config/test.secret.exs") do
import_config "test.secret.exs" import_config "test.secret.exs"
else else

View file

@ -287,11 +287,8 @@ See [Admin-API](admin_api.md)
* Method `PUT` * Method `PUT`
* Authentication: required * Authentication: required
* Params: * Params:
* `followers`: BOOLEAN field, receives notifications from followers * `block_from_strangers`: BOOLEAN field, blocks notifications from accounts you do not follow
* `follows`: BOOLEAN field, receives notifications from people the user follows * `hide_notification_contents`: BOOLEAN field. When set to true, it removes the contents of a message from the push notification.
* `remote`: BOOLEAN field, receives notifications from people on remote instances
* `local`: BOOLEAN field, receives notifications from people on the local instance
* `privacy_option`: BOOLEAN field. When set to true, it removes the contents of a message from the push notification.
* Response: JSON. Returns `{"status": "success"}` if the update was successful, otherwise returns `{"error": "error_msg"}` * Response: JSON. Returns `{"status": "success"}` if the update was successful, otherwise returns `{"error": "error_msg"}`
## `/api/pleroma/healthcheck` ## `/api/pleroma/healthcheck`

View file

@ -252,6 +252,7 @@ This section describe PWA manifest instance-specific values. Currently this opti
* `background_color`: Describe the background color of the app. (Example: `"#191b22"`, `"aliceblue"`). * `background_color`: Describe the background color of the app. (Example: `"#191b22"`, `"aliceblue"`).
## :emoji ## :emoji
* `shortcode_globs`: Location of custom emoji files. `*` can be used as a wildcard. Example `["/emoji/custom/**/*.png"]` * `shortcode_globs`: Location of custom emoji files. `*` can be used as a wildcard. Example `["/emoji/custom/**/*.png"]`
* `pack_extensions`: A list of file extensions for emojis, when no emoji.txt for a pack is present. Example `[".png", ".gif"]` * `pack_extensions`: A list of file extensions for emojis, when no emoji.txt for a pack is present. Example `[".png", ".gif"]`
* `groups`: Emojis are ordered in groups (tags). This is an array of key-value pairs where the key is the groupname and the value the location or array of locations. `*` can be used as a wildcard. Example `[Custom: ["/emoji/*.png", "/emoji/custom/*.png"]]` * `groups`: Emojis are ordered in groups (tags). This is an array of key-value pairs where the key is the groupname and the value the location or array of locations. `*` can be used as a wildcard. Example `[Custom: ["/emoji/*.png", "/emoji/custom/*.png"]]`
@ -260,10 +261,11 @@ This section describe PWA manifest instance-specific values. Currently this opti
memory for this amount of seconds multiplied by the number of files. memory for this amount of seconds multiplied by the number of files.
## :media_proxy ## :media_proxy
* `enabled`: Enables proxying of remote media to the instances proxy * `enabled`: Enables proxying of remote media to the instances proxy
* `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host/CDN fronts. * `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host/CDN fronts.
* `proxy_opts`: All options defined in `Pleroma.ReverseProxy` documentation, defaults to `[max_body_length: (25*1_048_576)]`. * `proxy_opts`: All options defined in `Pleroma.ReverseProxy` documentation, defaults to `[max_body_length: (25*1_048_576)]`.
* `whitelist`: List of domains to bypass the mediaproxy * `whitelist`: List of hosts with scheme to bypass the mediaproxy (e.g. `https://example.com`)
* `invalidation`: options for remove media from cache after delete object: * `invalidation`: options for remove media from cache after delete object:
* `enabled`: Enables purge cache * `enabled`: Enables purge cache
* `provider`: Which one of the [purge cache strategy](#purge-cache-strategy) to use. * `provider`: Which one of the [purge cache strategy](#purge-cache-strategy) to use.
@ -278,6 +280,7 @@ Urls of attachments pass to script as arguments.
* `script_path`: path to external script. * `script_path`: path to external script.
Example: Example:
```elixir ```elixir
config :pleroma, Pleroma.Web.MediaProxy.Invalidation.Script, config :pleroma, Pleroma.Web.MediaProxy.Invalidation.Script,
script_path: "./installation/nginx-cache-purge.example" script_path: "./installation/nginx-cache-purge.example"
@ -445,36 +448,32 @@ For each pool, the options are:
*For `gun` adapter* *For `gun` adapter*
Advanced settings for connections pool. Pool with opened connections. These connections can be reused in worker pools. Settings for HTTP connection pool.
For big instances it's recommended to increase `config :pleroma, :connections_pool, max_connections: 500` up to 500-1000. * `:connection_acquisition_wait` - Timeout to acquire a connection from pool.The total max time is this value multiplied by the number of retries.
It will increase memory usage, but federation would work faster. * `connection_acquisition_retries` - Number of attempts to acquire the connection from the pool if it is overloaded. Each attempt is timed `:connection_acquisition_wait` apart.
* `:max_connections` - Maximum number of connections in the pool.
* `:checkin_timeout` - timeout to checkin connection from pool. Default: 250ms. * `:await_up_timeout` - Timeout to connect to the host.
* `:max_connections` - maximum number of connections in the pool. Default: 250 connections. * `:reclaim_multiplier` - Multiplied by `:max_connections` this will be the maximum number of idle connections that will be reclaimed in case the pool is overloaded.
* `:retry` - number of retries, while `gun` will try to reconnect if connection goes down. Default: 1.
* `:retry_timeout` - time between retries when `gun` will try to reconnect in milliseconds. Default: 1000ms.
* `:await_up_timeout` - timeout while `gun` will wait until connection is up. Default: 5000ms.
### :pools ### :pools
*For `gun` adapter* *For `gun` adapter*
Advanced settings for workers pools. Settings for request pools. These pools are limited on top of `:connections_pool`.
There are four pools used: There are four pools used:
* `:federation` for the federation jobs. * `:federation` for the federation jobs. You may want this pool's max_connections to be at least equal to the number of federator jobs + retry queue jobs.
You may want this pool max_connections to be at least equal to the number of federator jobs + retry queue jobs. * `:media` - for rich media, media proxy.
* `:media` for rich media, media proxy * `:upload` - for proxying media when a remote uploader is used and `proxy_remote: true`.
* `:upload` for uploaded media (if using a remote uploader and `proxy_remote: true`) * `:default` - for other requests.
* `:default` for other requests
For each pool, the options are: For each pool, the options are:
* `:size` - how much workers the pool can hold * `:size` - limit to how much requests can be concurrently executed.
* `:timeout` - timeout while `gun` will wait for response * `:timeout` - timeout while `gun` will wait for response
* `:max_overflow` - additional workers if pool is under load * `:max_waiting` - limit to how much requests can be waiting for others to finish, after this is reached, subsequent requests will be dropped.
## Captcha ## Captcha
@ -629,8 +628,7 @@ Email notifications settings.
Configuration options described in [Oban readme](https://github.com/sorentwo/oban#usage): Configuration options described in [Oban readme](https://github.com/sorentwo/oban#usage):
* `repo` - app's Ecto repo (`Pleroma.Repo`) * `repo` - app's Ecto repo (`Pleroma.Repo`)
* `verbose` - logs verbosity * `log` - logs verbosity
* `prune` - non-retryable jobs [pruning settings](https://github.com/sorentwo/oban#pruning) (`:disabled` / `{:maxlen, value}` / `{:maxage, value}`)
* `queues` - job queues (see below) * `queues` - job queues (see below)
* `crontab` - periodic jobs, see [`Oban.Cron`](#obancron) * `crontab` - periodic jobs, see [`Oban.Cron`](#obancron)
@ -815,6 +813,8 @@ or
curl -H "X-Admin-Token: somerandomtoken" "http://localhost:4000/api/pleroma/admin/users/invites" curl -H "X-Admin-Token: somerandomtoken" "http://localhost:4000/api/pleroma/admin/users/invites"
``` ```
Warning: it's discouraged to use this feature because of the associated security risk: static / rarely changed instance-wide token is much weaker compared to email-password pair of a real admin user; consider using HTTP Basic Auth or OAuth-based authentication instead.
### :auth ### :auth
* `Pleroma.Web.Auth.PleromaAuthenticator`: default database authenticator. * `Pleroma.Web.Auth.PleromaAuthenticator`: default database authenticator.

View file

@ -3,8 +3,8 @@ defmodule Mix.Tasks.Pleroma.NotificationSettings do
@moduledoc """ @moduledoc """
Example: Example:
> mix pleroma.notification_settings --privacy-option=false --nickname-users="parallel588" # set false only for parallel588 user > mix pleroma.notification_settings --hide-notification-contents=false --nickname-users="parallel588" # set false only for parallel588 user
> mix pleroma.notification_settings --privacy-option=true # set true for all users > mix pleroma.notification_settings --hide-notification-contents=true # set true for all users
""" """
@ -19,16 +19,16 @@ def run(args) do
OptionParser.parse( OptionParser.parse(
args, args,
strict: [ strict: [
privacy_option: :boolean, hide_notification_contents: :boolean,
email_users: :string, email_users: :string,
nickname_users: :string nickname_users: :string
] ]
) )
privacy_option = Keyword.get(options, :privacy_option) hide_notification_contents = Keyword.get(options, :hide_notification_contents)
if not is_nil(privacy_option) do if not is_nil(hide_notification_contents) do
privacy_option hide_notification_contents
|> build_query(options) |> build_query(options)
|> Pleroma.Repo.update_all([]) |> Pleroma.Repo.update_all([])
end end
@ -36,15 +36,15 @@ def run(args) do
shell_info("Done") shell_info("Done")
end end
defp build_query(privacy_option, options) do defp build_query(hide_notification_contents, options) do
query = query =
from(u in Pleroma.User, from(u in Pleroma.User,
update: [ update: [
set: [ set: [
notification_settings: notification_settings:
fragment( fragment(
"jsonb_set(notification_settings, '{privacy_option}', ?)", "jsonb_set(notification_settings, '{hide_notification_contents}', ?)",
^privacy_option ^hide_notification_contents
) )
] ]
] ]

View file

@ -35,6 +35,11 @@ def user_agent do
# See http://elixir-lang.org/docs/stable/elixir/Application.html # See http://elixir-lang.org/docs/stable/elixir/Application.html
# for more information on OTP Applications # for more information on OTP Applications
def start(_type, _args) do def start(_type, _args) do
# Scrubbers are compiled at runtime and therefore will cause a conflict
# every time the application is restarted, so we disable module
# conflicts at runtime
Code.compiler_options(ignore_module_conflict: true)
Pleroma.Telemetry.Logger.attach()
Config.Holder.save_default() Config.Holder.save_default()
Pleroma.HTML.compile_scrubbers() Pleroma.HTML.compile_scrubbers()
Config.DeprecationWarnings.warn() Config.DeprecationWarnings.warn()
@ -219,9 +224,7 @@ defp task_children(_) do
# start hackney and gun pools in tests # start hackney and gun pools in tests
defp http_children(_, :test) do defp http_children(_, :test) do
hackney_options = Config.get([:hackney_pools, :federation]) http_children(Tesla.Adapter.Hackney, nil) ++ http_children(Tesla.Adapter.Gun, nil)
hackney_pool = :hackney_pool.child_spec(:federation, hackney_options)
[hackney_pool, Pleroma.Pool.Supervisor]
end end
defp http_children(Tesla.Adapter.Hackney, _) do defp http_children(Tesla.Adapter.Hackney, _) do
@ -240,7 +243,10 @@ defp http_children(Tesla.Adapter.Hackney, _) do
end end
end end
defp http_children(Tesla.Adapter.Gun, _), do: [Pleroma.Pool.Supervisor] defp http_children(Tesla.Adapter.Gun, _) do
Pleroma.Gun.ConnectionPool.children() ++
[{Task, &Pleroma.HTTP.AdapterHelper.Gun.limiter_setup/0}]
end
defp http_children(_, _), do: [] defp http_children(_, _), do: []
end end

View file

@ -54,6 +54,7 @@ def warn do
check_hellthread_threshold() check_hellthread_threshold()
mrf_user_allowlist() mrf_user_allowlist()
check_old_mrf_config() check_old_mrf_config()
check_media_proxy_whitelist_config()
end end
def check_old_mrf_config do def check_old_mrf_config do
@ -65,7 +66,7 @@ def check_old_mrf_config do
move_namespace_and_warn(@mrf_config_map, warning_preface) move_namespace_and_warn(@mrf_config_map, warning_preface)
end end
@spec move_namespace_and_warn([config_map()], String.t()) :: :ok @spec move_namespace_and_warn([config_map()], String.t()) :: :ok | nil
def move_namespace_and_warn(config_map, warning_preface) do def move_namespace_and_warn(config_map, warning_preface) do
warning = warning =
Enum.reduce(config_map, "", fn Enum.reduce(config_map, "", fn
@ -84,4 +85,16 @@ def move_namespace_and_warn(config_map, warning_preface) do
Logger.warn(warning_preface <> warning) Logger.warn(warning_preface <> warning)
end end
end end
@spec check_media_proxy_whitelist_config() :: :ok | nil
def check_media_proxy_whitelist_config do
whitelist = Config.get([:media_proxy, :whitelist])
if Enum.any?(whitelist, &(not String.starts_with?(&1, "http"))) do
Logger.warn("""
!!!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.
""")
end
end
end end

View file

@ -19,7 +19,8 @@ defmodule Pleroma.Gun.API do
:tls_opts, :tls_opts,
:tcp_opts, :tcp_opts,
:socks_opts, :socks_opts,
:ws_opts :ws_opts,
:supervise
] ]
@impl Gun @impl Gun

View file

@ -3,85 +3,33 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Gun.Conn do defmodule Pleroma.Gun.Conn do
@moduledoc """
Struct for gun connection data
"""
alias Pleroma.Gun alias Pleroma.Gun
alias Pleroma.Pool.Connections
require Logger require Logger
@type gun_state :: :up | :down def open(%URI{} = uri, opts) do
@type conn_state :: :active | :idle
@type t :: %__MODULE__{
conn: pid(),
gun_state: gun_state(),
conn_state: conn_state(),
used_by: [pid()],
last_reference: pos_integer(),
crf: float(),
retries: pos_integer()
}
defstruct conn: nil,
gun_state: :open,
conn_state: :init,
used_by: [],
last_reference: 0,
crf: 1,
retries: 0
@spec open(String.t() | URI.t(), atom(), keyword()) :: :ok | nil
def open(url, name, opts \\ [])
def open(url, name, opts) when is_binary(url), do: open(URI.parse(url), name, opts)
def open(%URI{} = uri, name, opts) do
pool_opts = Pleroma.Config.get([:connections_pool], []) pool_opts = Pleroma.Config.get([:connections_pool], [])
opts = opts =
opts opts
|> Enum.into(%{}) |> Enum.into(%{})
|> Map.put_new(:retry, pool_opts[:retry] || 1)
|> Map.put_new(:retry_timeout, pool_opts[:retry_timeout] || 1000)
|> Map.put_new(:await_up_timeout, pool_opts[:await_up_timeout] || 5_000) |> Map.put_new(:await_up_timeout, pool_opts[:await_up_timeout] || 5_000)
|> Map.put_new(:supervise, false)
|> maybe_add_tls_opts(uri) |> maybe_add_tls_opts(uri)
key = "#{uri.scheme}:#{uri.host}:#{uri.port}"
max_connections = pool_opts[:max_connections] || 250
conn_pid =
if Connections.count(name) < max_connections do
do_open(uri, opts) do_open(uri, opts)
else
close_least_used_and_do_open(name, uri, opts)
end
if is_pid(conn_pid) do
conn = %Pleroma.Gun.Conn{
conn: conn_pid,
gun_state: :up,
conn_state: :active,
last_reference: :os.system_time(:second)
}
:ok = Gun.set_owner(conn_pid, Process.whereis(name))
Connections.add_conn(name, key, conn)
end
end end
defp maybe_add_tls_opts(opts, %URI{scheme: "http"}), do: opts defp maybe_add_tls_opts(opts, %URI{scheme: "http"}), do: opts
defp maybe_add_tls_opts(opts, %URI{scheme: "https", host: host}) do defp maybe_add_tls_opts(opts, %URI{scheme: "https"}) do
tls_opts = [ tls_opts = [
verify: :verify_peer, verify: :verify_peer,
cacertfile: CAStore.file_path(), cacertfile: CAStore.file_path(),
depth: 20, depth: 20,
reuse_sessions: false, reuse_sessions: false,
verify_fun: log_level: :warning,
{&:ssl_verify_hostname.verify_fun/3, customize_hostname_check: [match_fun: :public_key.pkix_verify_hostname_match_fun(:https)]
[check_hostname: Pleroma.HTTP.Connection.format_host(host)]}
] ]
tls_opts = tls_opts =
@ -105,7 +53,7 @@ defp do_open(uri, %{proxy: {proxy_host, proxy_port}} = opts) do
{:ok, _} <- Gun.await_up(conn, opts[:await_up_timeout]), {:ok, _} <- Gun.await_up(conn, opts[:await_up_timeout]),
stream <- Gun.connect(conn, connect_opts), stream <- Gun.connect(conn, connect_opts),
{:response, :fin, 200, _} <- Gun.await(conn, stream) do {:response, :fin, 200, _} <- Gun.await(conn, stream) do
conn {:ok, conn}
else else
error -> error ->
Logger.warn( Logger.warn(
@ -141,7 +89,7 @@ defp do_open(uri, %{proxy: {proxy_type, proxy_host, proxy_port}} = opts) do
with {:ok, conn} <- Gun.open(proxy_host, proxy_port, opts), with {:ok, conn} <- Gun.open(proxy_host, proxy_port, opts),
{:ok, _} <- Gun.await_up(conn, opts[:await_up_timeout]) do {:ok, _} <- Gun.await_up(conn, opts[:await_up_timeout]) do
conn {:ok, conn}
else else
error -> error ->
Logger.warn( Logger.warn(
@ -155,11 +103,11 @@ defp do_open(uri, %{proxy: {proxy_type, proxy_host, proxy_port}} = opts) do
end end
defp do_open(%URI{host: host, port: port} = uri, opts) do defp do_open(%URI{host: host, port: port} = uri, opts) do
host = Pleroma.HTTP.Connection.parse_host(host) host = Pleroma.HTTP.AdapterHelper.parse_host(host)
with {:ok, conn} <- Gun.open(host, port, opts), with {:ok, conn} <- Gun.open(host, port, opts),
{:ok, _} <- Gun.await_up(conn, opts[:await_up_timeout]) do {:ok, _} <- Gun.await_up(conn, opts[:await_up_timeout]) do
conn {:ok, conn}
else else
error -> error ->
Logger.warn( Logger.warn(
@ -171,7 +119,7 @@ defp do_open(%URI{host: host, port: port} = uri, opts) do
end end
defp destination_opts(%URI{host: host, port: port}) do defp destination_opts(%URI{host: host, port: port}) do
host = Pleroma.HTTP.Connection.parse_host(host) host = Pleroma.HTTP.AdapterHelper.parse_host(host)
%{host: host, port: port} %{host: host, port: port}
end end
@ -181,17 +129,6 @@ defp add_http2_opts(opts, "https", tls_opts) do
defp add_http2_opts(opts, _, _), do: opts defp add_http2_opts(opts, _, _), do: opts
defp close_least_used_and_do_open(name, uri, opts) do
with [{key, conn} | _conns] <- Connections.get_unused_conns(name),
:ok <- Gun.close(conn.conn) do
Connections.remove_conn(name, key)
do_open(uri, opts)
else
[] -> {:error, :pool_overflowed}
end
end
def compose_uri_log(%URI{scheme: scheme, host: host, path: path}) do def compose_uri_log(%URI{scheme: scheme, host: host, path: path}) do
"#{scheme}://#{host}#{path}" "#{scheme}://#{host}#{path}"
end end

View file

@ -0,0 +1,79 @@
defmodule Pleroma.Gun.ConnectionPool do
@registry __MODULE__
alias Pleroma.Gun.ConnectionPool.WorkerSupervisor
def children do
[
{Registry, keys: :unique, name: @registry},
Pleroma.Gun.ConnectionPool.WorkerSupervisor
]
end
def get_conn(uri, opts) do
key = "#{uri.scheme}:#{uri.host}:#{uri.port}"
case Registry.lookup(@registry, key) do
# The key has already been registered, but connection is not up yet
[{worker_pid, nil}] ->
get_gun_pid_from_worker(worker_pid, true)
[{worker_pid, {gun_pid, _used_by, _crf, _last_reference}}] ->
GenServer.cast(worker_pid, {:add_client, self(), false})
{:ok, gun_pid}
[] ->
# :gun.set_owner fails in :connected state for whatevever reason,
# so we open the connection in the process directly and send it's pid back
# We trust gun to handle timeouts by itself
case WorkerSupervisor.start_worker([key, uri, opts, self()]) do
{:ok, worker_pid} ->
get_gun_pid_from_worker(worker_pid, false)
{:error, {:already_started, worker_pid}} ->
get_gun_pid_from_worker(worker_pid, true)
err ->
err
end
end
end
defp get_gun_pid_from_worker(worker_pid, register) do
# GenServer.call will block the process for timeout length if
# the server crashes on startup (which will happen if gun fails to connect)
# so instead we use cast + monitor
ref = Process.monitor(worker_pid)
if register, do: GenServer.cast(worker_pid, {:add_client, self(), true})
receive do
{:conn_pid, pid} ->
Process.demonitor(ref)
{:ok, pid}
{:DOWN, ^ref, :process, ^worker_pid, reason} ->
case reason do
{:shutdown, error} -> error
_ -> {:error, reason}
end
end
end
def release_conn(conn_pid) do
# :ets.fun2ms(fn {_, {worker_pid, {gun_pid, _, _, _}}} when gun_pid == conn_pid ->
# worker_pid end)
query_result =
Registry.select(@registry, [
{{:_, :"$1", {:"$2", :_, :_, :_}}, [{:==, :"$2", conn_pid}], [:"$1"]}
])
case query_result do
[worker_pid] ->
GenServer.cast(worker_pid, {:remove_client, self()})
[] ->
:ok
end
end
end

View file

@ -0,0 +1,85 @@
defmodule Pleroma.Gun.ConnectionPool.Reclaimer do
use GenServer, restart: :temporary
@registry Pleroma.Gun.ConnectionPool
def start_monitor do
pid =
case :gen_server.start(__MODULE__, [], name: {:via, Registry, {@registry, "reclaimer"}}) do
{:ok, pid} ->
pid
{:error, {:already_registered, pid}} ->
pid
end
{pid, Process.monitor(pid)}
end
@impl true
def init(_) do
{:ok, nil, {:continue, :reclaim}}
end
@impl true
def handle_continue(:reclaim, _) do
max_connections = Pleroma.Config.get([:connections_pool, :max_connections])
reclaim_max =
[:connections_pool, :reclaim_multiplier]
|> Pleroma.Config.get()
|> Kernel.*(max_connections)
|> round
|> max(1)
:telemetry.execute([:pleroma, :connection_pool, :reclaim, :start], %{}, %{
max_connections: max_connections,
reclaim_max: reclaim_max
})
# :ets.fun2ms(
# fn {_, {worker_pid, {_, used_by, crf, last_reference}}} when used_by == [] ->
# {worker_pid, crf, last_reference} end)
unused_conns =
Registry.select(
@registry,
[
{{:_, :"$1", {:_, :"$2", :"$3", :"$4"}}, [{:==, :"$2", []}], [{{:"$1", :"$3", :"$4"}}]}
]
)
case unused_conns do
[] ->
:telemetry.execute(
[:pleroma, :connection_pool, :reclaim, :stop],
%{reclaimed_count: 0},
%{
max_connections: max_connections
}
)
{:stop, :no_unused_conns, nil}
unused_conns ->
reclaimed =
unused_conns
|> Enum.sort(fn {_pid1, crf1, last_reference1}, {_pid2, crf2, last_reference2} ->
crf1 <= crf2 and last_reference1 <= last_reference2
end)
|> Enum.take(reclaim_max)
reclaimed
|> Enum.each(fn {pid, _, _} ->
DynamicSupervisor.terminate_child(Pleroma.Gun.ConnectionPool.WorkerSupervisor, pid)
end)
:telemetry.execute(
[:pleroma, :connection_pool, :reclaim, :stop],
%{reclaimed_count: Enum.count(reclaimed)},
%{max_connections: max_connections}
)
{:stop, :normal, nil}
end
end
end

View file

@ -0,0 +1,127 @@
defmodule Pleroma.Gun.ConnectionPool.Worker do
alias Pleroma.Gun
use GenServer, restart: :temporary
@registry Pleroma.Gun.ConnectionPool
def start_link([key | _] = opts) do
GenServer.start_link(__MODULE__, opts, name: {:via, Registry, {@registry, key}})
end
@impl true
def init([_key, _uri, _opts, _client_pid] = opts) do
{:ok, nil, {:continue, {:connect, opts}}}
end
@impl true
def handle_continue({:connect, [key, uri, opts, client_pid]}, _) do
with {:ok, conn_pid} <- Gun.Conn.open(uri, opts),
Process.link(conn_pid) do
time = :erlang.monotonic_time(:millisecond)
{_, _} =
Registry.update_value(@registry, key, fn _ ->
{conn_pid, [client_pid], 1, time}
end)
send(client_pid, {:conn_pid, conn_pid})
{:noreply,
%{key: key, timer: nil, client_monitors: %{client_pid => Process.monitor(client_pid)}},
:hibernate}
else
err ->
{:stop, {:shutdown, err}, nil}
end
end
@impl true
def handle_cast({:add_client, client_pid, send_pid_back}, %{key: key} = state) do
time = :erlang.monotonic_time(:millisecond)
{{conn_pid, _, _, _}, _} =
Registry.update_value(@registry, key, fn {conn_pid, used_by, crf, last_reference} ->
{conn_pid, [client_pid | used_by], crf(time - last_reference, crf), time}
end)
if send_pid_back, do: send(client_pid, {:conn_pid, conn_pid})
state =
if state.timer != nil do
Process.cancel_timer(state[:timer])
%{state | timer: nil}
else
state
end
ref = Process.monitor(client_pid)
state = put_in(state.client_monitors[client_pid], ref)
{:noreply, state, :hibernate}
end
@impl true
def handle_cast({:remove_client, client_pid}, %{key: key} = state) do
{{_conn_pid, used_by, _crf, _last_reference}, _} =
Registry.update_value(@registry, key, fn {conn_pid, used_by, crf, last_reference} ->
{conn_pid, List.delete(used_by, client_pid), crf, last_reference}
end)
{ref, state} = pop_in(state.client_monitors[client_pid])
Process.demonitor(ref)
timer =
if used_by == [] do
max_idle = Pleroma.Config.get([:connections_pool, :max_idle_time], 30_000)
Process.send_after(self(), :idle_close, max_idle)
else
nil
end
{:noreply, %{state | timer: timer}, :hibernate}
end
@impl true
def handle_info(:idle_close, state) do
# Gun monitors the owner process, and will close the connection automatically
# when it's terminated
{:stop, :normal, state}
end
# Gracefully shutdown if the connection got closed without any streams left
@impl true
def handle_info({:gun_down, _pid, _protocol, _reason, []}, state) do
{:stop, :normal, state}
end
# Otherwise, shutdown with an error
@impl true
def handle_info({:gun_down, _pid, _protocol, _reason, _killed_streams} = down_message, state) do
{:stop, {:error, down_message}, state}
end
@impl true
def handle_info({:DOWN, _ref, :process, pid, reason}, state) do
# Sometimes the client is dead before we demonitor it in :remove_client, so the message
# arrives anyway
case state.client_monitors[pid] do
nil ->
{:noreply, state, :hibernate}
_ref ->
:telemetry.execute(
[:pleroma, :connection_pool, :client_death],
%{client_pid: pid, reason: reason},
%{key: state.key}
)
handle_cast({:remove_client, pid}, state)
end
end
# LRFU policy: https://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.55.1478
defp crf(time_delta, prev_crf) do
1 + :math.pow(0.5, 0.0001 * time_delta) * prev_crf
end
end

View file

@ -0,0 +1,45 @@
defmodule Pleroma.Gun.ConnectionPool.WorkerSupervisor do
@moduledoc "Supervisor for pool workers. Does not do anything except enforce max connection limit"
use DynamicSupervisor
def start_link(opts) do
DynamicSupervisor.start_link(__MODULE__, opts, name: __MODULE__)
end
def init(_opts) do
DynamicSupervisor.init(
strategy: :one_for_one,
max_children: Pleroma.Config.get([:connections_pool, :max_connections])
)
end
def start_worker(opts, retry \\ false) do
case DynamicSupervisor.start_child(__MODULE__, {Pleroma.Gun.ConnectionPool.Worker, opts}) do
{:error, :max_children} ->
if retry or free_pool() == :error do
:telemetry.execute([:pleroma, :connection_pool, :provision_failure], %{opts: opts})
{:error, :pool_full}
else
start_worker(opts, true)
end
res ->
res
end
end
defp free_pool do
wait_for_reclaimer_finish(Pleroma.Gun.ConnectionPool.Reclaimer.start_monitor())
end
defp wait_for_reclaimer_finish({pid, mon}) do
receive do
{:DOWN, ^mon, :process, ^pid, :no_unused_conns} ->
:error
{:DOWN, ^mon, :process, ^pid, :normal} ->
:ok
end
end
end

View file

@ -3,32 +3,30 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.HTTP.AdapterHelper do defmodule Pleroma.HTTP.AdapterHelper do
alias Pleroma.HTTP.Connection @moduledoc """
Configure Tesla.Client with default and customized adapter options.
"""
@defaults [pool: :federation]
@type proxy_type() :: :socks4 | :socks5
@type host() :: charlist() | :inet.ip_address()
alias Pleroma.Config
alias Pleroma.HTTP.AdapterHelper
require Logger
@type proxy :: @type proxy ::
{Connection.host(), pos_integer()} {Connection.host(), pos_integer()}
| {Connection.proxy_type(), Connection.host(), pos_integer()} | {Connection.proxy_type(), Connection.host(), pos_integer()}
@callback options(keyword(), URI.t()) :: keyword() @callback options(keyword(), URI.t()) :: keyword()
@callback after_request(keyword()) :: :ok @callback get_conn(URI.t(), keyword()) :: {:ok, term()} | {:error, term()}
@spec options(keyword(), URI.t()) :: keyword()
def options(opts, _uri) do
proxy = Pleroma.Config.get([:http, :proxy_url], nil)
maybe_add_proxy(opts, format_proxy(proxy))
end
@spec maybe_get_conn(URI.t(), keyword()) :: keyword()
def maybe_get_conn(_uri, opts), do: opts
@spec after_request(keyword()) :: :ok
def after_request(_opts), do: :ok
@spec format_proxy(String.t() | tuple() | nil) :: proxy() | nil @spec format_proxy(String.t() | tuple() | nil) :: proxy() | nil
def format_proxy(nil), do: nil def format_proxy(nil), do: nil
def format_proxy(proxy_url) do def format_proxy(proxy_url) do
case Connection.parse_proxy(proxy_url) do case parse_proxy(proxy_url) do
{:ok, host, port} -> {host, port} {:ok, host, port} -> {host, port}
{:ok, type, host, port} -> {type, host, port} {:ok, type, host, port} -> {type, host, port}
_ -> nil _ -> nil
@ -38,4 +36,105 @@ def format_proxy(proxy_url) do
@spec maybe_add_proxy(keyword(), proxy() | nil) :: keyword() @spec maybe_add_proxy(keyword(), proxy() | nil) :: keyword()
def maybe_add_proxy(opts, nil), do: opts def maybe_add_proxy(opts, nil), do: opts
def maybe_add_proxy(opts, proxy), do: Keyword.put_new(opts, :proxy, proxy) def maybe_add_proxy(opts, proxy), do: Keyword.put_new(opts, :proxy, proxy)
@doc """
Merge default connection & adapter options with received ones.
"""
@spec options(URI.t(), keyword()) :: keyword()
def options(%URI{} = uri, opts \\ []) do
@defaults
|> put_timeout()
|> Keyword.merge(opts)
|> adapter_helper().options(uri)
end
# For Hackney, this is the time a connection can stay idle in the pool.
# For Gun, this is the timeout to receive a message from Gun.
defp put_timeout(opts) do
{config_key, default} =
if adapter() == Tesla.Adapter.Gun do
{:pools, Config.get([:pools, :default, :timeout], 5_000)}
else
{:hackney_pools, 10_000}
end
timeout = Config.get([config_key, opts[:pool], :timeout], default)
Keyword.merge(opts, timeout: timeout)
end
def get_conn(uri, opts), do: adapter_helper().get_conn(uri, opts)
defp adapter, do: Application.get_env(:tesla, :adapter)
defp adapter_helper do
case adapter() do
Tesla.Adapter.Gun -> AdapterHelper.Gun
Tesla.Adapter.Hackney -> AdapterHelper.Hackney
_ -> AdapterHelper.Default
end
end
@spec parse_proxy(String.t() | tuple() | nil) ::
{:ok, host(), pos_integer()}
| {:ok, proxy_type(), host(), pos_integer()}
| {:error, atom()}
| nil
def parse_proxy(nil), do: nil
def parse_proxy(proxy) when is_binary(proxy) do
with [host, port] <- String.split(proxy, ":"),
{port, ""} <- Integer.parse(port) do
{:ok, parse_host(host), port}
else
{_, _} ->
Logger.warn("Parsing port failed #{inspect(proxy)}")
{:error, :invalid_proxy_port}
:error ->
Logger.warn("Parsing port failed #{inspect(proxy)}")
{:error, :invalid_proxy_port}
_ ->
Logger.warn("Parsing proxy failed #{inspect(proxy)}")
{:error, :invalid_proxy}
end
end
def parse_proxy(proxy) when is_tuple(proxy) do
with {type, host, port} <- proxy do
{:ok, type, parse_host(host), port}
else
_ ->
Logger.warn("Parsing proxy failed #{inspect(proxy)}")
{:error, :invalid_proxy}
end
end
@spec parse_host(String.t() | atom() | charlist()) :: charlist() | :inet.ip_address()
def parse_host(host) when is_list(host), do: host
def parse_host(host) when is_atom(host), do: to_charlist(host)
def parse_host(host) when is_binary(host) do
host = to_charlist(host)
case :inet.parse_address(host) do
{:error, :einval} -> host
{:ok, ip} -> ip
end
end
@spec format_host(String.t()) :: charlist()
def format_host(host) do
host_charlist = to_charlist(host)
case :inet.parse_address(host_charlist) do
{:error, :einval} ->
:idna.encode(host_charlist)
{:ok, _ip} ->
host_charlist
end
end
end end

View file

@ -0,0 +1,14 @@
defmodule Pleroma.HTTP.AdapterHelper.Default do
alias Pleroma.HTTP.AdapterHelper
@behaviour Pleroma.HTTP.AdapterHelper
@spec options(keyword(), URI.t()) :: keyword()
def options(opts, _uri) do
proxy = Pleroma.Config.get([:http, :proxy_url], nil)
AdapterHelper.maybe_add_proxy(opts, AdapterHelper.format_proxy(proxy))
end
@spec get_conn(URI.t(), keyword()) :: {:ok, keyword()}
def get_conn(_uri, opts), do: {:ok, opts}
end

View file

@ -5,8 +5,8 @@
defmodule Pleroma.HTTP.AdapterHelper.Gun do defmodule Pleroma.HTTP.AdapterHelper.Gun do
@behaviour Pleroma.HTTP.AdapterHelper @behaviour Pleroma.HTTP.AdapterHelper
alias Pleroma.Gun.ConnectionPool
alias Pleroma.HTTP.AdapterHelper alias Pleroma.HTTP.AdapterHelper
alias Pleroma.Pool.Connections
require Logger require Logger
@ -14,7 +14,7 @@ defmodule Pleroma.HTTP.AdapterHelper.Gun do
connect_timeout: 5_000, connect_timeout: 5_000,
domain_lookup_timeout: 5_000, domain_lookup_timeout: 5_000,
tls_handshake_timeout: 5_000, tls_handshake_timeout: 5_000,
retry: 1, retry: 0,
retry_timeout: 1000, retry_timeout: 1000,
await_up_timeout: 5_000 await_up_timeout: 5_000
] ]
@ -31,16 +31,7 @@ def options(incoming_opts \\ [], %URI{} = uri) do
|> Keyword.merge(config_opts) |> Keyword.merge(config_opts)
|> add_scheme_opts(uri) |> add_scheme_opts(uri)
|> AdapterHelper.maybe_add_proxy(proxy) |> AdapterHelper.maybe_add_proxy(proxy)
|> maybe_get_conn(uri, incoming_opts) |> Keyword.merge(incoming_opts)
end
@spec after_request(keyword()) :: :ok
def after_request(opts) do
if opts[:conn] && opts[:body_as] != :chunks do
Connections.checkout(opts[:conn], self(), :gun_connections)
end
:ok
end end
defp add_scheme_opts(opts, %{scheme: "http"}), do: opts defp add_scheme_opts(opts, %{scheme: "http"}), do: opts
@ -48,30 +39,40 @@ defp add_scheme_opts(opts, %{scheme: "http"}), do: opts
defp add_scheme_opts(opts, %{scheme: "https"}) do defp add_scheme_opts(opts, %{scheme: "https"}) do
opts opts
|> Keyword.put(:certificates_verification, true) |> Keyword.put(:certificates_verification, true)
|> Keyword.put(:tls_opts, log_level: :warning)
end end
defp maybe_get_conn(adapter_opts, uri, incoming_opts) do @spec get_conn(URI.t(), keyword()) :: {:ok, keyword()} | {:error, atom()}
{receive_conn?, opts} = def get_conn(uri, opts) do
adapter_opts case ConnectionPool.get_conn(uri, opts) do
|> Keyword.merge(incoming_opts) {:ok, conn_pid} -> {:ok, Keyword.merge(opts, conn: conn_pid, close_conn: false)}
|> Keyword.pop(:receive_conn, true) err -> err
if Connections.alive?(:gun_connections) and receive_conn? do
checkin_conn(uri, opts)
else
opts
end end
end end
defp checkin_conn(uri, opts) do @prefix Pleroma.Gun.ConnectionPool
case Connections.checkin(uri, :gun_connections) do def limiter_setup do
nil -> wait = Pleroma.Config.get([:connections_pool, :connection_acquisition_wait])
Task.start(Pleroma.Gun.Conn, :open, [uri, :gun_connections, opts]) retries = Pleroma.Config.get([:connections_pool, :connection_acquisition_retries])
opts
conn when is_pid(conn) -> :pools
Keyword.merge(opts, conn: conn, close_conn: false) |> Pleroma.Config.get([])
|> Enum.each(fn {name, opts} ->
max_running = Keyword.get(opts, :size, 50)
max_waiting = Keyword.get(opts, :max_waiting, 10)
result =
ConcurrentLimiter.new(:"#{@prefix}.#{name}", max_running, max_waiting,
wait: wait,
max_retries: retries
)
case result do
:ok -> :ok
{:error, :existing} -> :ok
e -> raise e
end end
end)
:ok
end end
end end

View file

@ -24,5 +24,6 @@ def options(connection_opts \\ [], %URI{} = uri) do
defp add_scheme_opts(opts, _), do: opts defp add_scheme_opts(opts, _), do: opts
def after_request(_), do: :ok @spec get_conn(URI.t(), keyword()) :: {:ok, keyword()}
def get_conn(_uri, opts), do: {:ok, opts}
end end

View file

@ -1,124 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.HTTP.Connection do
@moduledoc """
Configure Tesla.Client with default and customized adapter options.
"""
alias Pleroma.Config
alias Pleroma.HTTP.AdapterHelper
require Logger
@defaults [pool: :federation]
@type ip_address :: ipv4_address() | ipv6_address()
@type ipv4_address :: {0..255, 0..255, 0..255, 0..255}
@type ipv6_address ::
{0..65_535, 0..65_535, 0..65_535, 0..65_535, 0..65_535, 0..65_535, 0..65_535, 0..65_535}
@type proxy_type() :: :socks4 | :socks5
@type host() :: charlist() | ip_address()
@doc """
Merge default connection & adapter options with received ones.
"""
@spec options(URI.t(), keyword()) :: keyword()
def options(%URI{} = uri, opts \\ []) do
@defaults
|> pool_timeout()
|> Keyword.merge(opts)
|> adapter_helper().options(uri)
end
defp pool_timeout(opts) do
{config_key, default} =
if adapter() == Tesla.Adapter.Gun do
{:pools, Config.get([:pools, :default, :timeout])}
else
{:hackney_pools, 10_000}
end
timeout = Config.get([config_key, opts[:pool], :timeout], default)
Keyword.merge(opts, timeout: timeout)
end
@spec after_request(keyword()) :: :ok
def after_request(opts), do: adapter_helper().after_request(opts)
defp adapter, do: Application.get_env(:tesla, :adapter)
defp adapter_helper do
case adapter() do
Tesla.Adapter.Gun -> AdapterHelper.Gun
Tesla.Adapter.Hackney -> AdapterHelper.Hackney
_ -> AdapterHelper
end
end
@spec parse_proxy(String.t() | tuple() | nil) ::
{:ok, host(), pos_integer()}
| {:ok, proxy_type(), host(), pos_integer()}
| {:error, atom()}
| nil
def parse_proxy(nil), do: nil
def parse_proxy(proxy) when is_binary(proxy) do
with [host, port] <- String.split(proxy, ":"),
{port, ""} <- Integer.parse(port) do
{:ok, parse_host(host), port}
else
{_, _} ->
Logger.warn("Parsing port failed #{inspect(proxy)}")
{:error, :invalid_proxy_port}
:error ->
Logger.warn("Parsing port failed #{inspect(proxy)}")
{:error, :invalid_proxy_port}
_ ->
Logger.warn("Parsing proxy failed #{inspect(proxy)}")
{:error, :invalid_proxy}
end
end
def parse_proxy(proxy) when is_tuple(proxy) do
with {type, host, port} <- proxy do
{:ok, type, parse_host(host), port}
else
_ ->
Logger.warn("Parsing proxy failed #{inspect(proxy)}")
{:error, :invalid_proxy}
end
end
@spec parse_host(String.t() | atom() | charlist()) :: charlist() | ip_address()
def parse_host(host) when is_list(host), do: host
def parse_host(host) when is_atom(host), do: to_charlist(host)
def parse_host(host) when is_binary(host) do
host = to_charlist(host)
case :inet.parse_address(host) do
{:error, :einval} -> host
{:ok, ip} -> ip
end
end
@spec format_host(String.t()) :: charlist()
def format_host(host) do
host_charlist = to_charlist(host)
case :inet.parse_address(host_charlist) do
{:error, :einval} ->
:idna.encode(host_charlist)
{:ok, _ip} ->
host_charlist
end
end
end

View file

@ -7,7 +7,7 @@ defmodule Pleroma.HTTP do
Wrapper for `Tesla.request/2`. Wrapper for `Tesla.request/2`.
""" """
alias Pleroma.HTTP.Connection alias Pleroma.HTTP.AdapterHelper
alias Pleroma.HTTP.Request alias Pleroma.HTTP.Request
alias Pleroma.HTTP.RequestBuilder, as: Builder alias Pleroma.HTTP.RequestBuilder, as: Builder
alias Tesla.Client alias Tesla.Client
@ -60,49 +60,29 @@ def post(url, body, headers \\ [], options \\ []),
{:ok, Env.t()} | {:error, any()} {:ok, Env.t()} | {:error, any()}
def request(method, url, body, headers, options) when is_binary(url) do def request(method, url, body, headers, options) when is_binary(url) do
uri = URI.parse(url) uri = URI.parse(url)
adapter_opts = Connection.options(uri, options[:adapter] || []) adapter_opts = AdapterHelper.options(uri, options[:adapter] || [])
case AdapterHelper.get_conn(uri, adapter_opts) do
{:ok, adapter_opts} ->
options = put_in(options[:adapter], adapter_opts) options = put_in(options[:adapter], adapter_opts)
params = options[:params] || [] params = options[:params] || []
request = build_request(method, headers, options, url, body, params) request = build_request(method, headers, options, url, body, params)
adapter = Application.get_env(:tesla, :adapter) adapter = Application.get_env(:tesla, :adapter)
client = Tesla.client([Tesla.Middleware.FollowRedirects], adapter) client = Tesla.client([Pleroma.HTTP.Middleware.FollowRedirects], adapter)
pid = Process.whereis(adapter_opts[:pool]) maybe_limit(
fn ->
pool_alive? = request(client, request)
if adapter == Tesla.Adapter.Gun && pid do end,
Process.alive?(pid) adapter,
else
false
end
request_opts =
adapter_opts adapter_opts
|> Enum.into(%{})
|> Map.put(:env, Pleroma.Config.get([:env]))
|> Map.put(:pool_alive?, pool_alive?)
response = request(client, request, request_opts)
Connection.after_request(adapter_opts)
response
end
@spec request(Client.t(), keyword(), map()) :: {:ok, Env.t()} | {:error, any()}
def request(%Client{} = client, request, %{env: :test}), do: request(client, request)
def request(%Client{} = client, request, %{body_as: :chunks}), do: request(client, request)
def request(%Client{} = client, request, %{pool_alive?: false}), do: request(client, request)
def request(%Client{} = client, request, %{pool: pool, timeout: timeout}) do
:poolboy.transaction(
pool,
&Pleroma.Pool.Request.execute(&1, client, request, timeout),
timeout
) )
# Connection release is handled in a custom FollowRedirects middleware
err ->
err
end
end end
@spec request(Client.t(), keyword()) :: {:ok, Env.t()} | {:error, any()} @spec request(Client.t(), keyword()) :: {:ok, Env.t()} | {:error, any()}
@ -118,4 +98,13 @@ defp build_request(method, headers, options, url, body, params) do
|> Builder.add_param(:query, :query, params) |> Builder.add_param(:query, :query, params)
|> Builder.convert_to_keyword() |> Builder.convert_to_keyword()
end end
@prefix Pleroma.Gun.ConnectionPool
defp maybe_limit(fun, Tesla.Adapter.Gun, opts) do
ConcurrentLimiter.limit(:"#{@prefix}.#{opts[:pool] || :default}", fun)
end
defp maybe_limit(fun, _, _) do
fun.()
end
end end

View file

@ -571,10 +571,7 @@ def skip?(%Activity{} = activity, %User{} = user) do
[ [
:self, :self,
:invisible, :invisible,
:followers, :block_from_strangers,
:follows,
:non_followers,
:non_follows,
:recently_followed, :recently_followed,
:filtered :filtered
] ]
@ -595,45 +592,15 @@ def skip?(:invisible, %Activity{} = activity, _) do
end end
def skip?( def skip?(
:followers, :block_from_strangers,
%Activity{} = activity, %Activity{} = activity,
%User{notification_settings: %{followers: false}} = user %User{notification_settings: %{block_from_strangers: true}} = user
) do
actor = activity.data["actor"]
follower = User.get_cached_by_ap_id(actor)
User.following?(follower, user)
end
def skip?(
:non_followers,
%Activity{} = activity,
%User{notification_settings: %{non_followers: false}} = user
) do ) do
actor = activity.data["actor"] actor = activity.data["actor"]
follower = User.get_cached_by_ap_id(actor) follower = User.get_cached_by_ap_id(actor)
!User.following?(follower, user) !User.following?(follower, user)
end end
def skip?(
:follows,
%Activity{} = activity,
%User{notification_settings: %{follows: false}} = user
) do
actor = activity.data["actor"]
followed = User.get_cached_by_ap_id(actor)
User.following?(user, followed)
end
def skip?(
:non_follows,
%Activity{} = activity,
%User{notification_settings: %{non_follows: false}} = user
) do
actor = activity.data["actor"]
followed = User.get_cached_by_ap_id(actor)
!User.following?(user, followed)
end
# To do: consider defining recency in hours and checking FollowingRelationship with a single SQL # To do: consider defining recency in hours and checking FollowingRelationship with a single SQL
def skip?(:recently_followed, %Activity{data: %{"type" => "Follow"}} = activity, %User{} = user) do def skip?(:recently_followed, %Activity{data: %{"type" => "Follow"}} = activity, %User{} = user) do
actor = activity.data["actor"] actor = activity.data["actor"]

View file

@ -4,6 +4,9 @@
defmodule Pleroma.Plugs.AdminSecretAuthenticationPlug do defmodule Pleroma.Plugs.AdminSecretAuthenticationPlug do
import Plug.Conn import Plug.Conn
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Plugs.RateLimiter
alias Pleroma.User alias Pleroma.User
def init(options) do def init(options) do
@ -11,7 +14,10 @@ def init(options) do
end end
def secret_token do def secret_token do
Pleroma.Config.get(:admin_token) case Pleroma.Config.get(:admin_token) do
blank when blank in [nil, ""] -> nil
token -> token
end
end end
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
@ -26,9 +32,9 @@ def call(conn, _) do
def authenticate(%{params: %{"admin_token" => admin_token}} = conn) do def authenticate(%{params: %{"admin_token" => admin_token}} = conn) do
if admin_token == secret_token() do if admin_token == secret_token() do
assign(conn, :user, %User{is_admin: true}) assign_admin_user(conn)
else else
conn handle_bad_token(conn)
end end
end end
@ -36,8 +42,19 @@ def authenticate(conn) do
token = secret_token() token = secret_token()
case get_req_header(conn, "x-admin-token") do case get_req_header(conn, "x-admin-token") do
[^token] -> assign(conn, :user, %User{is_admin: true}) blank when blank in [[], [""]] -> conn
_ -> conn [^token] -> assign_admin_user(conn)
_ -> handle_bad_token(conn)
end end
end end
defp assign_admin_user(conn) do
conn
|> assign(:user, %User{is_admin: true})
|> OAuthScopesPlug.skip_plug()
end
defp handle_bad_token(conn) do
RateLimiter.call(conn, name: :authentication)
end
end end

View file

@ -108,31 +108,48 @@ defp csp_string do
|> :erlang.iolist_to_binary() |> :erlang.iolist_to_binary()
end end
defp build_csp_from_whitelist([], acc), do: acc
defp build_csp_from_whitelist([last], acc) do
[build_csp_param_from_whitelist(last) | acc]
end
defp build_csp_from_whitelist([head | tail], acc) do
build_csp_from_whitelist(tail, [[?\s, build_csp_param_from_whitelist(head)] | acc])
end
# TODO: use `build_csp_param/1` after removing support bare domains for media proxy whitelist
defp build_csp_param_from_whitelist("http" <> _ = url) do
build_csp_param(url)
end
defp build_csp_param_from_whitelist(url), do: url
defp build_csp_multimedia_source_list do defp build_csp_multimedia_source_list do
media_proxy_whitelist = media_proxy_whitelist =
Enum.reduce(Config.get([:media_proxy, :whitelist]), [], fn host, acc -> [:media_proxy, :whitelist]
add_source(acc, host) |> Config.get()
end) |> build_csp_from_whitelist([])
media_proxy_base_url = build_csp_param(Config.get([:media_proxy, :base_url]))
upload_base_url = build_csp_param(Config.get([Pleroma.Upload, :base_url]))
s3_endpoint = build_csp_param(Config.get([Pleroma.Uploaders.S3, :public_endpoint]))
captcha_method = Config.get([Pleroma.Captcha, :method]) captcha_method = Config.get([Pleroma.Captcha, :method])
captcha_endpoint = Config.get([captcha_method, :endpoint])
captcha_endpoint = build_csp_param(Config.get([captcha_method, :endpoint])) base_endpoints =
[
[:media_proxy, :base_url],
[Pleroma.Upload, :base_url],
[Pleroma.Uploaders.S3, :public_endpoint]
]
|> Enum.map(&Config.get/1)
[] [captcha_endpoint | base_endpoints]
|> add_source(media_proxy_base_url) |> Enum.map(&build_csp_param/1)
|> add_source(upload_base_url) |> Enum.reduce([], &add_source(&2, &1))
|> add_source(s3_endpoint)
|> add_source(media_proxy_whitelist) |> add_source(media_proxy_whitelist)
|> add_source(captcha_endpoint)
end end
defp add_source(iodata, nil), do: iodata defp add_source(iodata, nil), do: iodata
defp add_source(iodata, []), do: iodata
defp add_source(iodata, source), do: [[?\s, source] | iodata] defp add_source(iodata, source), do: [[?\s, source] | iodata]
defp add_csp_param(csp_iodata, nil), do: csp_iodata defp add_csp_param(csp_iodata, nil), do: csp_iodata

View file

@ -7,37 +7,18 @@ defmodule Pleroma.Plugs.UserIsAdminPlug do
import Plug.Conn import Plug.Conn
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.OAuth
def init(options) do def init(options) do
options options
end end
def call(%{assigns: %{user: %User{is_admin: true}} = assigns} = conn, _) do def call(%{assigns: %{user: %User{is_admin: true}}} = conn, _) do
token = assigns[:token]
cond do
not Pleroma.Config.enforce_oauth_admin_scope_usage?() ->
conn conn
token && OAuth.Scopes.contains_admin_scopes?(token.scopes) ->
# Note: checking for _any_ admin scope presence, not necessarily fitting requested action.
# Thus, controller must explicitly invoke OAuthScopesPlug to verify scope requirements.
# Admin might opt out of admin scope for some apps to block any admin actions from them.
conn
true ->
fail(conn)
end
end end
def call(conn, _) do def call(conn, _) do
fail(conn)
end
defp fail(conn) do
conn conn
|> render_error(:forbidden, "User is not an admin or OAuth admin scope is not granted.") |> render_error(:forbidden, "User is not an admin.")
|> halt() |> halt()
end end
end end

View file

@ -1,283 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Pool.Connections do
use GenServer
alias Pleroma.Config
alias Pleroma.Gun
require Logger
@type domain :: String.t()
@type conn :: Pleroma.Gun.Conn.t()
@type t :: %__MODULE__{
conns: %{domain() => conn()},
opts: keyword()
}
defstruct conns: %{}, opts: []
@spec start_link({atom(), keyword()}) :: {:ok, pid()}
def start_link({name, opts}) do
GenServer.start_link(__MODULE__, opts, name: name)
end
@impl true
def init(opts), do: {:ok, %__MODULE__{conns: %{}, opts: opts}}
@spec checkin(String.t() | URI.t(), atom()) :: pid() | nil
def checkin(url, name)
def checkin(url, name) when is_binary(url), do: checkin(URI.parse(url), name)
def checkin(%URI{} = uri, name) do
timeout = Config.get([:connections_pool, :checkin_timeout], 250)
GenServer.call(name, {:checkin, uri}, timeout)
end
@spec alive?(atom()) :: boolean()
def alive?(name) do
if pid = Process.whereis(name) do
Process.alive?(pid)
else
false
end
end
@spec get_state(atom()) :: t()
def get_state(name) do
GenServer.call(name, :state)
end
@spec count(atom()) :: pos_integer()
def count(name) do
GenServer.call(name, :count)
end
@spec get_unused_conns(atom()) :: [{domain(), conn()}]
def get_unused_conns(name) do
GenServer.call(name, :unused_conns)
end
@spec checkout(pid(), pid(), atom()) :: :ok
def checkout(conn, pid, name) do
GenServer.cast(name, {:checkout, conn, pid})
end
@spec add_conn(atom(), String.t(), Pleroma.Gun.Conn.t()) :: :ok
def add_conn(name, key, conn) do
GenServer.cast(name, {:add_conn, key, conn})
end
@spec remove_conn(atom(), String.t()) :: :ok
def remove_conn(name, key) do
GenServer.cast(name, {:remove_conn, key})
end
@impl true
def handle_cast({:add_conn, key, conn}, state) do
state = put_in(state.conns[key], conn)
Process.monitor(conn.conn)
{:noreply, state}
end
@impl true
def handle_cast({:checkout, conn_pid, pid}, state) do
state =
with true <- Process.alive?(conn_pid),
{key, conn} <- find_conn(state.conns, conn_pid),
used_by <- List.keydelete(conn.used_by, pid, 0) do
conn_state = if used_by == [], do: :idle, else: conn.conn_state
put_in(state.conns[key], %{conn | conn_state: conn_state, used_by: used_by})
else
false ->
Logger.debug("checkout for closed conn #{inspect(conn_pid)}")
state
nil ->
Logger.debug("checkout for alive conn #{inspect(conn_pid)}, but is not in state")
state
end
{:noreply, state}
end
@impl true
def handle_cast({:remove_conn, key}, state) do
state = put_in(state.conns, Map.delete(state.conns, key))
{:noreply, state}
end
@impl true
def handle_call({:checkin, uri}, from, state) do
key = "#{uri.scheme}:#{uri.host}:#{uri.port}"
case state.conns[key] do
%{conn: pid, gun_state: :up} = conn ->
time = :os.system_time(:second)
last_reference = time - conn.last_reference
crf = crf(last_reference, 100, conn.crf)
state =
put_in(state.conns[key], %{
conn
| last_reference: time,
crf: crf,
conn_state: :active,
used_by: [from | conn.used_by]
})
{:reply, pid, state}
%{gun_state: :down} ->
{:reply, nil, state}
nil ->
{:reply, nil, state}
end
end
@impl true
def handle_call(:state, _from, state), do: {:reply, state, state}
@impl true
def handle_call(:count, _from, state) do
{:reply, Enum.count(state.conns), state}
end
@impl true
def handle_call(:unused_conns, _from, state) do
unused_conns =
state.conns
|> Enum.filter(&filter_conns/1)
|> Enum.sort(&sort_conns/2)
{:reply, unused_conns, state}
end
defp filter_conns({_, %{conn_state: :idle, used_by: []}}), do: true
defp filter_conns(_), do: false
defp sort_conns({_, c1}, {_, c2}) do
c1.crf <= c2.crf and c1.last_reference <= c2.last_reference
end
@impl true
def handle_info({:gun_up, conn_pid, _protocol}, state) do
%{origin_host: host, origin_scheme: scheme, origin_port: port} = Gun.info(conn_pid)
host =
case :inet.ntoa(host) do
{:error, :einval} -> host
ip -> ip
end
key = "#{scheme}:#{host}:#{port}"
state =
with {key, conn} <- find_conn(state.conns, conn_pid, key),
{true, key} <- {Process.alive?(conn_pid), key} do
put_in(state.conns[key], %{
conn
| gun_state: :up,
conn_state: :active,
retries: 0
})
else
{false, key} ->
put_in(
state.conns,
Map.delete(state.conns, key)
)
nil ->
:ok = Gun.close(conn_pid)
state
end
{:noreply, state}
end
@impl true
def handle_info({:gun_down, conn_pid, _protocol, _reason, _killed}, state) do
retries = Config.get([:connections_pool, :retry], 1)
# we can't get info on this pid, because pid is dead
state =
with {key, conn} <- find_conn(state.conns, conn_pid),
{true, key} <- {Process.alive?(conn_pid), key} do
if conn.retries == retries do
:ok = Gun.close(conn.conn)
put_in(
state.conns,
Map.delete(state.conns, key)
)
else
put_in(state.conns[key], %{
conn
| gun_state: :down,
retries: conn.retries + 1
})
end
else
{false, key} ->
put_in(
state.conns,
Map.delete(state.conns, key)
)
nil ->
Logger.debug(":gun_down for conn which isn't found in state")
state
end
{:noreply, state}
end
@impl true
def handle_info({:DOWN, _ref, :process, conn_pid, reason}, state) do
Logger.debug("received DOWN message for #{inspect(conn_pid)} reason -> #{inspect(reason)}")
state =
with {key, conn} <- find_conn(state.conns, conn_pid) do
Enum.each(conn.used_by, fn {pid, _ref} ->
Process.exit(pid, reason)
end)
put_in(
state.conns,
Map.delete(state.conns, key)
)
else
nil ->
Logger.debug(":DOWN for conn which isn't found in state")
state
end
{:noreply, state}
end
defp find_conn(conns, conn_pid) do
Enum.find(conns, fn {_key, conn} ->
conn.conn == conn_pid
end)
end
defp find_conn(conns, conn_pid, conn_key) do
Enum.find(conns, fn {key, conn} ->
key == conn_key and conn.conn == conn_pid
end)
end
def crf(current, steps, crf) do
1 + :math.pow(0.5, current / steps) * crf
end
end

View file

@ -1,22 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Pool do
def child_spec(opts) do
poolboy_opts =
opts
|> Keyword.put(:worker_module, Pleroma.Pool.Request)
|> Keyword.put(:name, {:local, opts[:name]})
|> Keyword.put(:size, opts[:size])
|> Keyword.put(:max_overflow, opts[:max_overflow])
%{
id: opts[:id] || {__MODULE__, make_ref()},
start: {:poolboy, :start_link, [poolboy_opts, [name: opts[:name]]]},
restart: :permanent,
shutdown: 5000,
type: :worker
}
end
end

View file

@ -1,65 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Pool.Request do
use GenServer
require Logger
def start_link(args) do
GenServer.start_link(__MODULE__, args)
end
@impl true
def init(_), do: {:ok, []}
@spec execute(pid() | atom(), Tesla.Client.t(), keyword(), pos_integer()) ::
{:ok, Tesla.Env.t()} | {:error, any()}
def execute(pid, client, request, timeout) do
GenServer.call(pid, {:execute, client, request}, timeout)
end
@impl true
def handle_call({:execute, client, request}, _from, state) do
response = Pleroma.HTTP.request(client, request)
{:reply, response, state}
end
@impl true
def handle_info({:gun_data, _conn, _stream, _, _}, state) do
{:noreply, state}
end
@impl true
def handle_info({:gun_up, _conn, _protocol}, state) do
{:noreply, state}
end
@impl true
def handle_info({:gun_down, _conn, _protocol, _reason, _killed}, state) do
{:noreply, state}
end
@impl true
def handle_info({:gun_error, _conn, _stream, _error}, state) do
{:noreply, state}
end
@impl true
def handle_info({:gun_push, _conn, _stream, _new_stream, _method, _uri, _headers}, state) do
{:noreply, state}
end
@impl true
def handle_info({:gun_response, _conn, _stream, _, _status, _headers}, state) do
{:noreply, state}
end
@impl true
def handle_info(msg, state) do
Logger.warn("Received unexpected message #{inspect(__MODULE__)} #{inspect(msg)}")
{:noreply, state}
end
end

View file

@ -1,42 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Pool.Supervisor do
use Supervisor
alias Pleroma.Config
alias Pleroma.Pool
def start_link(args) do
Supervisor.start_link(__MODULE__, args, name: __MODULE__)
end
def init(_) do
conns_child = %{
id: Pool.Connections,
start:
{Pool.Connections, :start_link, [{:gun_connections, Config.get([:connections_pool])}]}
}
Supervisor.init([conns_child | pools()], strategy: :one_for_one)
end
defp pools do
pools = Config.get(:pools)
pools =
if Config.get([Pleroma.Upload, :proxy_remote]) == false do
Keyword.delete(pools, :upload)
else
pools
end
for {pool_name, pool_opts} <- pools do
pool_opts
|> Keyword.put(:id, {Pool, pool_name})
|> Keyword.put(:name, pool_name)
|> Pool.child_spec()
end
end
end

View file

@ -48,7 +48,7 @@ def stream_body(%{pid: pid, opts: opts, fin: true}) do
# if there were redirects we need to checkout old conn # if there were redirects we need to checkout old conn
conn = opts[:old_conn] || opts[:conn] conn = opts[:old_conn] || opts[:conn]
if conn, do: :ok = Pleroma.Pool.Connections.checkout(conn, self(), :gun_connections) if conn, do: :ok = Pleroma.Gun.ConnectionPool.release_conn(conn)
:done :done
end end

View file

@ -0,0 +1,76 @@
defmodule Pleroma.Telemetry.Logger do
@moduledoc "Transforms Pleroma telemetry events to logs"
require Logger
@events [
[:pleroma, :connection_pool, :reclaim, :start],
[:pleroma, :connection_pool, :reclaim, :stop],
[:pleroma, :connection_pool, :provision_failure],
[:pleroma, :connection_pool, :client_death]
]
def attach do
:telemetry.attach_many("pleroma-logger", @events, &handle_event/4, [])
end
# Passing anonymous functions instead of strings to logger is intentional,
# that way strings won't be concatenated if the message is going to be thrown
# out anyway due to higher log level configured
def handle_event(
[:pleroma, :connection_pool, :reclaim, :start],
_,
%{max_connections: max_connections, reclaim_max: reclaim_max},
_
) do
Logger.debug(fn ->
"Connection pool is exhausted (reached #{max_connections} connections). Starting idle connection cleanup to reclaim as much as #{
reclaim_max
} connections"
end)
end
def handle_event(
[:pleroma, :connection_pool, :reclaim, :stop],
%{reclaimed_count: 0},
_,
_
) do
Logger.error(fn ->
"Connection pool failed to reclaim any connections due to all of them being in use. It will have to drop requests for opening connections to new hosts"
end)
end
def handle_event(
[:pleroma, :connection_pool, :reclaim, :stop],
%{reclaimed_count: reclaimed_count},
_,
_
) do
Logger.debug(fn -> "Connection pool cleaned up #{reclaimed_count} idle connections" end)
end
def handle_event(
[:pleroma, :connection_pool, :provision_failure],
%{opts: [key | _]},
_,
_
) do
Logger.error(fn ->
"Connection pool had to refuse opening a connection to #{key} due to connection limit exhaustion"
end)
end
def handle_event(
[:pleroma, :connection_pool, :client_death],
%{client_pid: client_pid, reason: reason},
%{key: key},
_
) do
Logger.warn(fn ->
"Pool worker for #{key}: Client #{inspect(client_pid)} died before releasing the connection with #{
inspect(reason)
}"
end)
end
end

View file

@ -0,0 +1,110 @@
# Pleroma: A lightweight social networking server
# Copyright © 2015-2020 Tymon Tobolski <https://github.com/teamon/tesla/blob/master/lib/tesla/middleware/follow_redirects.ex>
# Copyright © 2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.HTTP.Middleware.FollowRedirects do
@moduledoc """
Pool-aware version of https://github.com/teamon/tesla/blob/master/lib/tesla/middleware/follow_redirects.ex
Follow 3xx redirects
## Options
- `:max_redirects` - limit number of redirects (default: `5`)
"""
alias Pleroma.Gun.ConnectionPool
@behaviour Tesla.Middleware
@max_redirects 5
@redirect_statuses [301, 302, 303, 307, 308]
@impl Tesla.Middleware
def call(env, next, opts \\ []) do
max = Keyword.get(opts, :max_redirects, @max_redirects)
redirect(env, next, max)
end
defp redirect(env, next, left) do
opts = env.opts[:adapter]
case Tesla.run(env, next) do
{:ok, %{status: status} = res} when status in @redirect_statuses and left > 0 ->
release_conn(opts)
case Tesla.get_header(res, "location") do
nil ->
{:ok, res}
location ->
location = parse_location(location, res)
case get_conn(location, opts) do
{:ok, opts} ->
%{env | opts: Keyword.put(env.opts, :adapter, opts)}
|> new_request(res.status, location)
|> redirect(next, left - 1)
e ->
e
end
end
{:ok, %{status: status}} when status in @redirect_statuses ->
release_conn(opts)
{:error, {__MODULE__, :too_many_redirects}}
{:error, _} = e ->
release_conn(opts)
e
other ->
unless opts[:body_as] == :chunks do
release_conn(opts)
end
other
end
end
defp get_conn(location, opts) do
uri = URI.parse(location)
case ConnectionPool.get_conn(uri, opts) do
{:ok, conn} ->
{:ok, Keyword.merge(opts, conn: conn)}
e ->
e
end
end
defp release_conn(opts) do
ConnectionPool.release_conn(opts[:conn])
end
# The 303 (See Other) redirect was added in HTTP/1.1 to indicate that the originally
# requested resource is not available, however a related resource (or another redirect)
# available via GET is available at the specified location.
# https://tools.ietf.org/html/rfc7231#section-6.4.4
defp new_request(env, 303, location), do: %{env | url: location, method: :get, query: []}
# The 307 (Temporary Redirect) status code indicates that the target
# resource resides temporarily under a different URI and the user agent
# MUST NOT change the request method (...)
# https://tools.ietf.org/html/rfc7231#section-6.4.7
defp new_request(env, 307, location), do: %{env | url: location}
defp new_request(env, _, location), do: %{env | url: location, query: []}
defp parse_location("https://" <> _rest = location, _env), do: location
defp parse_location("http://" <> _rest = location, _env), do: location
defp parse_location(location, env) do
env.url
|> URI.parse()
|> URI.merge(location)
|> URI.to_string()
end
end

View file

@ -10,21 +10,15 @@ defmodule Pleroma.User.NotificationSetting do
@primary_key false @primary_key false
embedded_schema do embedded_schema do
field(:followers, :boolean, default: true) field(:block_from_strangers, :boolean, default: false)
field(:follows, :boolean, default: true) field(:hide_notification_contents, :boolean, default: false)
field(:non_follows, :boolean, default: true)
field(:non_followers, :boolean, default: true)
field(:privacy_option, :boolean, default: false)
end end
def changeset(schema, params) do def changeset(schema, params) do
schema schema
|> cast(prepare_attrs(params), [ |> cast(prepare_attrs(params), [
:followers, :block_from_strangers,
:follows, :hide_notification_contents
:non_follows,
:non_followers,
:privacy_option
]) ])
end end

View file

@ -60,7 +60,7 @@ def filter(%{"type" => "Follow", "actor" => actor_id} = message) do
if score < 0.8 do if score < 0.8 do
{:ok, message} {:ok, message}
else else
{:reject, nil} {:reject, "[AntiFollowbotPolicy] Scored #{actor_id} as #{score}"}
end end
end end

View file

@ -39,14 +39,13 @@ def filter(%{"type" => "Create", "actor" => actor, "object" => object} = message
{:ok, message} {:ok, message}
{:old_user, false} -> {:old_user, false} ->
{:reject, nil} {:reject, "[AntiLinkSpamPolicy] User has no posts nor followers"}
{:error, _} -> {:error, _} ->
{:reject, nil} {:reject, "[AntiLinkSpamPolicy] Failed to get or fetch user by ap_id"}
e -> e ->
Logger.warn("[MRF anti-link-spam] WTF: unhandled error #{inspect(e)}") {:reject, "[AntiLinkSpamPolicy] Unhandled error #{inspect(e)}"}
{:reject, nil}
end end
end end

View file

@ -43,7 +43,7 @@ defp delist_message(message, _threshold), do: {:ok, message}
defp reject_message(message, threshold) when threshold > 0 do defp reject_message(message, threshold) when threshold > 0 do
with {_, recipients} <- get_recipient_count(message) do with {_, recipients} <- get_recipient_count(message) do
if recipients > threshold do if recipients > threshold do
{:reject, nil} {:reject, "[HellthreadPolicy] #{recipients} recipients is over the limit of #{threshold}"}
else else
{:ok, message} {:ok, message}
end end
@ -87,7 +87,7 @@ def filter(%{"type" => "Create", "object" => %{"type" => object_type}} = message
{:ok, message} <- delist_message(message, delist_threshold) do {:ok, message} <- delist_message(message, delist_threshold) do
{:ok, message} {:ok, message}
else else
_e -> {:reject, nil} e -> e
end end
end end

View file

@ -24,7 +24,7 @@ defp check_reject(%{"object" => %{"content" => content, "summary" => summary}} =
if Enum.any?(Pleroma.Config.get([:mrf_keyword, :reject]), fn pattern -> if Enum.any?(Pleroma.Config.get([:mrf_keyword, :reject]), fn pattern ->
string_matches?(content, pattern) or string_matches?(summary, pattern) string_matches?(content, pattern) or string_matches?(summary, pattern)
end) do end) do
{:reject, nil} {:reject, "[KeywordPolicy] Matches with rejected keyword"}
else else
{:ok, message} {:ok, message}
end end
@ -89,8 +89,9 @@ def filter(%{"type" => "Create", "object" => %{"content" => _content}} = message
{:ok, message} <- check_replace(message) do {:ok, message} <- check_replace(message) do
{:ok, message} {:ok, message}
else else
_e -> {:reject, nil} -> {:reject, "[KeywordPolicy] "}
{:reject, nil} {:reject, _} = e -> e
_e -> {:reject, "[KeywordPolicy] "}
end end
end end

View file

@ -12,8 +12,9 @@ def filter(%{"type" => "Create"} = message) do
reject_actors = Pleroma.Config.get([:mrf_mention, :actors], []) reject_actors = Pleroma.Config.get([:mrf_mention, :actors], [])
recipients = (message["to"] || []) ++ (message["cc"] || []) recipients = (message["to"] || []) ++ (message["cc"] || [])
if Enum.any?(recipients, fn recipient -> Enum.member?(reject_actors, recipient) end) do if rejected_mention =
{:reject, nil} Enum.find(recipients, fn recipient -> Enum.member?(reject_actors, recipient) end) do
{:reject, "[MentionPolicy] Rejected for mention of #{rejected_mention}"}
else else
{:ok, message} {:ok, message}
end end

View file

@ -28,7 +28,7 @@ defp check_date(%{"object" => %{"published" => published}} = message) do
defp check_reject(message, actions) do defp check_reject(message, actions) do
if :reject in actions do if :reject in actions do
{:reject, nil} {:reject, "[ObjectAgePolicy]"}
else else
{:ok, message} {:ok, message}
end end
@ -47,9 +47,8 @@ defp check_delist(message, actions) do
{:ok, message} {:ok, message}
else else
# Unhandleable error: somebody is messing around, just drop the message.
_e -> _e ->
{:reject, nil} {:reject, "[ObjectAgePolicy] Unhandled error"}
end end
else else
{:ok, message} {:ok, message}
@ -69,9 +68,8 @@ defp check_strip_followers(message, actions) do
{:ok, message} {:ok, message}
else else
# Unhandleable error: somebody is messing around, just drop the message.
_e -> _e ->
{:reject, nil} {:reject, "[ObjectAgePolicy] Unhandled error"}
end end
else else
{:ok, message} {:ok, message}

View file

@ -38,7 +38,7 @@ def filter(%{"type" => "Create"} = object) do
{:ok, object} {:ok, object}
true -> true ->
{:reject, nil} {:reject, "[RejectNonPublic] visibility: #{visibility}"}
end end
end end

View file

@ -21,7 +21,7 @@ defp check_accept(%{host: actor_host} = _actor_info, object) do
accepts == [] -> {:ok, object} accepts == [] -> {:ok, object}
actor_host == Config.get([Pleroma.Web.Endpoint, :url, :host]) -> {:ok, object} actor_host == Config.get([Pleroma.Web.Endpoint, :url, :host]) -> {:ok, object}
MRF.subdomain_match?(accepts, actor_host) -> {:ok, object} MRF.subdomain_match?(accepts, actor_host) -> {:ok, object}
true -> {:reject, nil} true -> {:reject, "[SimplePolicy] host not in accept list"}
end end
end end
@ -31,7 +31,7 @@ defp check_reject(%{host: actor_host} = _actor_info, object) do
|> MRF.subdomains_regex() |> MRF.subdomains_regex()
if MRF.subdomain_match?(rejects, actor_host) do if MRF.subdomain_match?(rejects, actor_host) do
{:reject, nil} {:reject, "[SimplePolicy] host in reject list"}
else else
{:ok, object} {:ok, object}
end end
@ -114,7 +114,7 @@ defp check_report_removal(%{host: actor_host} = _actor_info, %{"type" => "Flag"}
|> MRF.subdomains_regex() |> MRF.subdomains_regex()
if MRF.subdomain_match?(report_removal, actor_host) do if MRF.subdomain_match?(report_removal, actor_host) do
{:reject, nil} {:reject, "[SimplePolicy] host in report_removal list"}
else else
{:ok, object} {:ok, object}
end end
@ -159,7 +159,7 @@ def filter(%{"type" => "Delete", "actor" => actor} = object) do
|> MRF.subdomains_regex() |> MRF.subdomains_regex()
if MRF.subdomain_match?(reject_deletes, actor_host) do if MRF.subdomain_match?(reject_deletes, actor_host) do
{:reject, nil} {:reject, "[SimplePolicy] host in reject_deletes list"}
else else
{:ok, object} {:ok, object}
end end
@ -177,7 +177,9 @@ def filter(%{"actor" => actor} = object) do
{:ok, object} <- check_report_removal(actor_info, object) do {:ok, object} <- check_report_removal(actor_info, object) do
{:ok, object} {:ok, object}
else else
_e -> {:reject, nil} {:reject, nil} -> {:reject, "[SimplePolicy]"}
{:reject, _} = e -> e
_ -> {:reject, "[SimplePolicy]"}
end end
end end
@ -191,7 +193,9 @@ def filter(%{"id" => actor, "type" => obj_type} = object)
{:ok, object} <- check_banner_removal(actor_info, object) do {:ok, object} <- check_banner_removal(actor_info, object) do
{:ok, object} {:ok, object}
else else
_e -> {:reject, nil} {:reject, nil} -> {:reject, "[SimplePolicy]"}
{:reject, _} = e -> e
_ -> {:reject, "[SimplePolicy]"}
end end
end end

View file

@ -134,12 +134,13 @@ defp process_tag(
if user.local == true do if user.local == true do
{:ok, message} {:ok, message}
else else
{:reject, nil} {:reject,
"[TagPolicy] Follow from #{actor} tagged with mrf_tag:disable-remote-subscription"}
end end
end end
defp process_tag("mrf_tag:disable-any-subscription", %{"type" => "Follow"}), defp process_tag("mrf_tag:disable-any-subscription", %{"type" => "Follow", "actor" => actor}),
do: {:reject, nil} do: {:reject, "[TagPolicy] Follow from #{actor} tagged with mrf_tag:disable-any-subscription"}
defp process_tag(_, message), do: {:ok, message} defp process_tag(_, message), do: {:ok, message}

View file

@ -14,7 +14,7 @@ defp filter_by_list(%{"actor" => actor} = object, allow_list) do
if actor in allow_list do if actor in allow_list do
{:ok, object} {:ok, object}
else else
{:reject, nil} {:reject, "[UserAllowListPolicy] #{actor} not in the list"}
end end
end end

View file

@ -11,22 +11,26 @@ def filter(%{"type" => "Undo", "object" => child_message} = message) do
with {:ok, _} <- filter(child_message) do with {:ok, _} <- filter(child_message) do
{:ok, message} {:ok, message}
else else
{:reject, nil} -> {:reject, _} = e -> e
{:reject, nil}
end end
end end
def filter(%{"type" => message_type} = message) do def filter(%{"type" => message_type} = message) do
with accepted_vocabulary <- Pleroma.Config.get([:mrf_vocabulary, :accept]), with accepted_vocabulary <- Pleroma.Config.get([:mrf_vocabulary, :accept]),
rejected_vocabulary <- Pleroma.Config.get([:mrf_vocabulary, :reject]), rejected_vocabulary <- Pleroma.Config.get([:mrf_vocabulary, :reject]),
true <- {_, true} <-
Enum.empty?(accepted_vocabulary) || Enum.member?(accepted_vocabulary, message_type), {:accepted,
false <- Enum.empty?(accepted_vocabulary) || Enum.member?(accepted_vocabulary, message_type)},
length(rejected_vocabulary) > 0 && Enum.member?(rejected_vocabulary, message_type), {_, false} <-
{:rejected,
length(rejected_vocabulary) > 0 && Enum.member?(rejected_vocabulary, message_type)},
{:ok, _} <- filter(message["object"]) do {:ok, _} <- filter(message["object"]) do
{:ok, message} {:ok, message}
else else
_ -> {:reject, nil} {:reject, _} = e -> e
{:accepted, _} -> {:reject, "[VocabularyPolicy] #{message_type} not in accept list"}
{:rejected, _} -> {:reject, "[VocabularyPolicy] #{message_type} in reject list"}
_ -> {:reject, "[VocabularyPolicy]"}
end end
end end

View file

@ -49,7 +49,8 @@ def is_representable?(%Activity{} = activity) do
""" """
def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = params) do def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = params) do
Logger.debug("Federating #{id} to #{inbox}") Logger.debug("Federating #{id} to #{inbox}")
%{host: host, path: path} = URI.parse(inbox)
uri = URI.parse(inbox)
digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64()) digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
@ -57,8 +58,8 @@ def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = pa
signature = signature =
Pleroma.Signature.sign(actor, %{ Pleroma.Signature.sign(actor, %{
"(request-target)": "post #{path}", "(request-target)": "post #{uri.path}",
host: host, host: signature_host(uri),
"content-length": byte_size(json), "content-length": byte_size(json),
digest: digest, digest: digest,
date: date date: date
@ -76,8 +77,9 @@ def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = pa
{"digest", digest} {"digest", digest}
] ]
) do ) do
if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since], if not Map.has_key?(params, :unreachable_since) || params[:unreachable_since] do
do: Instances.set_reachable(inbox) Instances.set_reachable(inbox)
end
result result
else else
@ -96,6 +98,14 @@ def publish_one(%{actor_id: actor_id} = params) do
|> publish_one() |> publish_one()
end end
defp signature_host(%URI{port: port, scheme: scheme, host: host}) do
if port == URI.default_port(scheme) do
host
else
"#{host}:#{port}"
end
end
defp should_federate?(inbox, public) do defp should_federate?(inbox, public) do
if public do if public do
true true

View file

@ -62,15 +62,17 @@ def fix_summary(%{"summary" => _} = object) do
def fix_summary(object), do: Map.put(object, "summary", "") def fix_summary(object), do: Map.put(object, "summary", "")
def fix_addressing_list(map, field) do def fix_addressing_list(map, field) do
cond do addrs = map[field]
is_binary(map[field]) ->
Map.put(map, field, [map[field]])
is_nil(map[field]) -> cond do
Map.put(map, field, []) is_list(addrs) ->
Map.put(map, field, Enum.filter(addrs, &is_binary/1))
is_binary(addrs) ->
Map.put(map, field, [addrs])
true -> true ->
map Map.put(map, field, [])
end end
end end

View file

@ -29,6 +29,10 @@ def request_body(description, schema_ref, opts \\ []) do
} }
end end
def admin_api_params do
[Operation.parameter(:admin_token, :query, :string, "Allows authorization via admin token.")]
end
def pagination_params do def pagination_params do
[ [
Operation.parameter(:max_id, :query, :string, "Return items older than this ID"), Operation.parameter(:max_id, :query, :string, "Return items older than this ID"),

View file

@ -26,6 +26,7 @@ def show_operation do
%Schema{type: :boolean, default: false}, %Schema{type: :boolean, default: false},
"Get only saved in database settings" "Get only saved in database settings"
) )
| admin_api_params()
], ],
security: [%{"oAuth" => ["read"]}], security: [%{"oAuth" => ["read"]}],
responses: %{ responses: %{
@ -41,6 +42,7 @@ def update_operation do
summary: "Update config settings", summary: "Update config settings",
operationId: "AdminAPI.ConfigController.update", operationId: "AdminAPI.ConfigController.update",
security: [%{"oAuth" => ["write"]}], security: [%{"oAuth" => ["write"]}],
parameters: admin_api_params(),
requestBody: requestBody:
request_body("Parameters", %Schema{ request_body("Parameters", %Schema{
type: :object, type: :object,
@ -73,6 +75,7 @@ def descriptions_operation do
summary: "Get JSON with config descriptions.", summary: "Get JSON with config descriptions.",
operationId: "AdminAPI.ConfigController.descriptions", operationId: "AdminAPI.ConfigController.descriptions",
security: [%{"oAuth" => ["read"]}], security: [%{"oAuth" => ["read"]}],
parameters: admin_api_params(),
responses: %{ responses: %{
200 => 200 =>
Operation.response("Config Descriptions", "application/json", %Schema{ Operation.response("Config Descriptions", "application/json", %Schema{

View file

@ -20,6 +20,7 @@ def index_operation do
summary: "Get a list of generated invites", summary: "Get a list of generated invites",
operationId: "AdminAPI.InviteController.index", operationId: "AdminAPI.InviteController.index",
security: [%{"oAuth" => ["read:invites"]}], security: [%{"oAuth" => ["read:invites"]}],
parameters: admin_api_params(),
responses: %{ responses: %{
200 => 200 =>
Operation.response("Invites", "application/json", %Schema{ Operation.response("Invites", "application/json", %Schema{
@ -51,6 +52,7 @@ def create_operation do
summary: "Create an account registration invite token", summary: "Create an account registration invite token",
operationId: "AdminAPI.InviteController.create", operationId: "AdminAPI.InviteController.create",
security: [%{"oAuth" => ["write:invites"]}], security: [%{"oAuth" => ["write:invites"]}],
parameters: admin_api_params(),
requestBody: requestBody:
request_body("Parameters", %Schema{ request_body("Parameters", %Schema{
type: :object, type: :object,
@ -71,6 +73,7 @@ def revoke_operation do
summary: "Revoke invite by token", summary: "Revoke invite by token",
operationId: "AdminAPI.InviteController.revoke", operationId: "AdminAPI.InviteController.revoke",
security: [%{"oAuth" => ["write:invites"]}], security: [%{"oAuth" => ["write:invites"]}],
parameters: admin_api_params(),
requestBody: requestBody:
request_body( request_body(
"Parameters", "Parameters",
@ -97,6 +100,7 @@ def email_operation do
summary: "Sends registration invite via email", summary: "Sends registration invite via email",
operationId: "AdminAPI.InviteController.email", operationId: "AdminAPI.InviteController.email",
security: [%{"oAuth" => ["write:invites"]}], security: [%{"oAuth" => ["write:invites"]}],
parameters: admin_api_params(),
requestBody: requestBody:
request_body( request_body(
"Parameters", "Parameters",

View file

@ -33,6 +33,7 @@ def index_operation do
%Schema{type: :integer, default: 50}, %Schema{type: :integer, default: 50},
"Number of statuses to return" "Number of statuses to return"
) )
| admin_api_params()
], ],
responses: %{ responses: %{
200 => success_response() 200 => success_response()
@ -46,6 +47,7 @@ def delete_operation do
summary: "Remove a banned MediaProxy URL from Cachex", summary: "Remove a banned MediaProxy URL from Cachex",
operationId: "AdminAPI.MediaProxyCacheController.delete", operationId: "AdminAPI.MediaProxyCacheController.delete",
security: [%{"oAuth" => ["write:media_proxy_caches"]}], security: [%{"oAuth" => ["write:media_proxy_caches"]}],
parameters: admin_api_params(),
requestBody: requestBody:
request_body( request_body(
"Parameters", "Parameters",
@ -71,6 +73,7 @@ def purge_operation do
summary: "Purge and optionally ban a MediaProxy URL", summary: "Purge and optionally ban a MediaProxy URL",
operationId: "AdminAPI.MediaProxyCacheController.purge", operationId: "AdminAPI.MediaProxyCacheController.purge",
security: [%{"oAuth" => ["write:media_proxy_caches"]}], security: [%{"oAuth" => ["write:media_proxy_caches"]}],
parameters: admin_api_params(),
requestBody: requestBody:
request_body( request_body(
"Parameters", "Parameters",

View file

@ -36,6 +36,7 @@ def index_operation do
%Schema{type: :integer, default: 50}, %Schema{type: :integer, default: 50},
"Number of apps to return" "Number of apps to return"
) )
| admin_api_params()
], ],
responses: %{ responses: %{
200 => 200 =>
@ -72,6 +73,7 @@ def create_operation do
summary: "Create OAuth App", summary: "Create OAuth App",
operationId: "AdminAPI.OAuthAppController.create", operationId: "AdminAPI.OAuthAppController.create",
requestBody: request_body("Parameters", create_request()), requestBody: request_body("Parameters", create_request()),
parameters: admin_api_params(),
security: [%{"oAuth" => ["write"]}], security: [%{"oAuth" => ["write"]}],
responses: %{ responses: %{
200 => Operation.response("App", "application/json", oauth_app()), 200 => Operation.response("App", "application/json", oauth_app()),
@ -85,7 +87,7 @@ def update_operation do
tags: ["Admin", "oAuth Apps"], tags: ["Admin", "oAuth Apps"],
summary: "Update OAuth App", summary: "Update OAuth App",
operationId: "AdminAPI.OAuthAppController.update", operationId: "AdminAPI.OAuthAppController.update",
parameters: [id_param()], parameters: [id_param() | admin_api_params()],
security: [%{"oAuth" => ["write"]}], security: [%{"oAuth" => ["write"]}],
requestBody: request_body("Parameters", update_request()), requestBody: request_body("Parameters", update_request()),
responses: %{ responses: %{
@ -103,7 +105,7 @@ def delete_operation do
tags: ["Admin", "oAuth Apps"], tags: ["Admin", "oAuth Apps"],
summary: "Delete OAuth App", summary: "Delete OAuth App",
operationId: "AdminAPI.OAuthAppController.delete", operationId: "AdminAPI.OAuthAppController.delete",
parameters: [id_param()], parameters: [id_param() | admin_api_params()],
security: [%{"oAuth" => ["write"]}], security: [%{"oAuth" => ["write"]}],
responses: %{ responses: %{
204 => no_content_response(), 204 => no_content_response(),

View file

@ -19,6 +19,7 @@ def index_operation do
summary: "List Relays", summary: "List Relays",
operationId: "AdminAPI.RelayController.index", operationId: "AdminAPI.RelayController.index",
security: [%{"oAuth" => ["read"]}], security: [%{"oAuth" => ["read"]}],
parameters: admin_api_params(),
responses: %{ responses: %{
200 => 200 =>
Operation.response("Response", "application/json", %Schema{ Operation.response("Response", "application/json", %Schema{
@ -41,6 +42,7 @@ def follow_operation do
summary: "Follow a Relay", summary: "Follow a Relay",
operationId: "AdminAPI.RelayController.follow", operationId: "AdminAPI.RelayController.follow",
security: [%{"oAuth" => ["write:follows"]}], security: [%{"oAuth" => ["write:follows"]}],
parameters: admin_api_params(),
requestBody: requestBody:
request_body("Parameters", %Schema{ request_body("Parameters", %Schema{
type: :object, type: :object,
@ -64,6 +66,7 @@ def unfollow_operation do
summary: "Unfollow a Relay", summary: "Unfollow a Relay",
operationId: "AdminAPI.RelayController.unfollow", operationId: "AdminAPI.RelayController.unfollow",
security: [%{"oAuth" => ["write:follows"]}], security: [%{"oAuth" => ["write:follows"]}],
parameters: admin_api_params(),
requestBody: requestBody:
request_body("Parameters", %Schema{ request_body("Parameters", %Schema{
type: :object, type: :object,

View file

@ -48,6 +48,7 @@ def index_operation do
%Schema{type: :integer, default: 50}, %Schema{type: :integer, default: 50},
"Number number of log entries per page" "Number number of log entries per page"
) )
| admin_api_params()
], ],
responses: %{ responses: %{
200 => 200 =>
@ -71,7 +72,7 @@ def show_operation do
tags: ["Admin", "Reports"], tags: ["Admin", "Reports"],
summary: "Get an individual report", summary: "Get an individual report",
operationId: "AdminAPI.ReportController.show", operationId: "AdminAPI.ReportController.show",
parameters: [id_param()], parameters: [id_param() | admin_api_params()],
security: [%{"oAuth" => ["read:reports"]}], security: [%{"oAuth" => ["read:reports"]}],
responses: %{ responses: %{
200 => Operation.response("Report", "application/json", report()), 200 => Operation.response("Report", "application/json", report()),
@ -86,6 +87,7 @@ def update_operation do
summary: "Change the state of one or multiple reports", summary: "Change the state of one or multiple reports",
operationId: "AdminAPI.ReportController.update", operationId: "AdminAPI.ReportController.update",
security: [%{"oAuth" => ["write:reports"]}], security: [%{"oAuth" => ["write:reports"]}],
parameters: admin_api_params(),
requestBody: request_body("Parameters", update_request(), required: true), requestBody: request_body("Parameters", update_request(), required: true),
responses: %{ responses: %{
204 => no_content_response(), 204 => no_content_response(),
@ -100,7 +102,7 @@ def notes_create_operation do
tags: ["Admin", "Reports"], tags: ["Admin", "Reports"],
summary: "Create report note", summary: "Create report note",
operationId: "AdminAPI.ReportController.notes_create", operationId: "AdminAPI.ReportController.notes_create",
parameters: [id_param()], parameters: [id_param() | admin_api_params()],
requestBody: requestBody:
request_body("Parameters", %Schema{ request_body("Parameters", %Schema{
type: :object, type: :object,
@ -124,6 +126,7 @@ def notes_delete_operation do
parameters: [ parameters: [
Operation.parameter(:report_id, :path, :string, "Report ID"), Operation.parameter(:report_id, :path, :string, "Report ID"),
Operation.parameter(:id, :path, :string, "Note ID") Operation.parameter(:id, :path, :string, "Note ID")
| admin_api_params()
], ],
security: [%{"oAuth" => ["write:reports"]}], security: [%{"oAuth" => ["write:reports"]}],
responses: %{ responses: %{

View file

@ -55,6 +55,7 @@ def index_operation do
%Schema{type: :integer, default: 50}, %Schema{type: :integer, default: 50},
"Number of statuses to return" "Number of statuses to return"
) )
| admin_api_params()
], ],
responses: %{ responses: %{
200 => 200 =>
@ -71,7 +72,7 @@ def show_operation do
tags: ["Admin", "Statuses"], tags: ["Admin", "Statuses"],
summary: "Show Status", summary: "Show Status",
operationId: "AdminAPI.StatusController.show", operationId: "AdminAPI.StatusController.show",
parameters: [id_param()], parameters: [id_param() | admin_api_params()],
security: [%{"oAuth" => ["read:statuses"]}], security: [%{"oAuth" => ["read:statuses"]}],
responses: %{ responses: %{
200 => Operation.response("Status", "application/json", status()), 200 => Operation.response("Status", "application/json", status()),
@ -85,7 +86,7 @@ def update_operation do
tags: ["Admin", "Statuses"], tags: ["Admin", "Statuses"],
summary: "Change the scope of an individual reported status", summary: "Change the scope of an individual reported status",
operationId: "AdminAPI.StatusController.update", operationId: "AdminAPI.StatusController.update",
parameters: [id_param()], parameters: [id_param() | admin_api_params()],
security: [%{"oAuth" => ["write:statuses"]}], security: [%{"oAuth" => ["write:statuses"]}],
requestBody: request_body("Parameters", update_request(), required: true), requestBody: request_body("Parameters", update_request(), required: true),
responses: %{ responses: %{
@ -100,7 +101,7 @@ def delete_operation do
tags: ["Admin", "Statuses"], tags: ["Admin", "Statuses"],
summary: "Delete an individual reported status", summary: "Delete an individual reported status",
operationId: "AdminAPI.StatusController.delete", operationId: "AdminAPI.StatusController.delete",
parameters: [id_param()], parameters: [id_param() | admin_api_params()],
security: [%{"oAuth" => ["write:statuses"]}], security: [%{"oAuth" => ["write:statuses"]}],
responses: %{ responses: %{
200 => empty_object_response(), 200 => empty_object_response(),

View file

@ -90,11 +90,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
notification_settings: %Schema{ notification_settings: %Schema{
type: :object, type: :object,
properties: %{ properties: %{
followers: %Schema{type: :boolean}, block_from_strangers: %Schema{type: :boolean},
follows: %Schema{type: :boolean}, hide_notification_contents: %Schema{type: :boolean}
non_followers: %Schema{type: :boolean},
non_follows: %Schema{type: :boolean},
privacy_option: %Schema{type: :boolean}
} }
}, },
relationship: AccountRelationship, relationship: AccountRelationship,
@ -182,11 +179,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
"unread_conversation_count" => 0, "unread_conversation_count" => 0,
"tags" => [], "tags" => [],
"notification_settings" => %{ "notification_settings" => %{
"followers" => true, "block_from_strangers" => false,
"follows" => true, "hide_notification_contents" => false
"non_followers" => true,
"non_follows" => true,
"privacy_option" => false
}, },
"relationship" => %{ "relationship" => %{
"blocked_by" => false, "blocked_by" => false,

View file

@ -172,6 +172,11 @@ def create(%{assigns: %{user: user}, body_params: %{status: _} = params} = conn,
with_direct_conversation_id: true with_direct_conversation_id: true
) )
else else
{:error, {:reject, message}} ->
conn
|> put_status(:unprocessable_entity)
|> json(%{error: message})
{:error, message} -> {:error, message} ->
conn conn
|> put_status(:unprocessable_entity) |> put_status(:unprocessable_entity)

View file

@ -60,22 +60,28 @@ defp local?(url), do: String.starts_with?(url, Pleroma.Web.base_url())
defp whitelisted?(url) do defp whitelisted?(url) do
%{host: domain} = URI.parse(url) %{host: domain} = URI.parse(url)
mediaproxy_whitelist = Config.get([:media_proxy, :whitelist]) mediaproxy_whitelist_domains =
[:media_proxy, :whitelist]
|> Config.get()
|> Enum.map(&maybe_get_domain_from_url/1)
upload_base_url_domain = whitelist_domains =
if !is_nil(Config.get([Upload, :base_url])) do if base_url = Config.get([Upload, :base_url]) do
[URI.parse(Config.get([Upload, :base_url])).host] %{host: base_domain} = URI.parse(base_url)
[base_domain | mediaproxy_whitelist_domains]
else else
[] mediaproxy_whitelist_domains
end end
whitelist = mediaproxy_whitelist ++ upload_base_url_domain domain in whitelist_domains
Enum.any?(whitelist, fn pattern ->
String.equivalent?(domain, pattern)
end)
end end
defp maybe_get_domain_from_url("http" <> _ = url) do
URI.parse(url).host
end
defp maybe_get_domain_from_url(domain), do: domain
def encode_url(url) do def encode_url(url) do
base64 = Base.url_encode64(url, @base64_opts) base64 = Base.url_encode64(url, @base64_opts)

View file

@ -104,7 +104,7 @@ def build_content(notification, actor, object, mastodon_type \\ nil)
def build_content( def build_content(
%{ %{
user: %{notification_settings: %{privacy_option: true}} user: %{notification_settings: %{hide_notification_contents: true}}
} = notification, } = notification,
_actor, _actor,
_object, _object,

11
mix.exs
View file

@ -90,8 +90,6 @@ defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"] defp elixirc_paths(_), do: ["lib"]
defp warnings_as_errors(:prod), do: false defp warnings_as_errors(:prod), do: false
# Uncomment this if you need testing configurable_from_database logic
# defp warnings_as_errors(:dev), do: false
defp warnings_as_errors(_), do: true defp warnings_as_errors(_), do: true
# Specifies OAuth dependencies. # Specifies OAuth dependencies.
@ -137,13 +135,11 @@ defp deps do
{:poison, "~> 3.0", override: true}, {:poison, "~> 3.0", override: true},
# {:tesla, "~> 1.3", override: true}, # {:tesla, "~> 1.3", override: true},
{:tesla, {:tesla,
git: "https://git.pleroma.social/pleroma/elixir-libraries/tesla.git", github: "teamon/tesla", ref: "af3707078b10793f6a534938e56b963aff82fe3c", override: true},
ref: "61b7503cef33f00834f78ddfafe0d5d9dec2270b",
override: true},
{:castore, "~> 0.1"}, {:castore, "~> 0.1"},
{:cowlib, "~> 2.8", override: true}, {:cowlib, "~> 2.8", override: true},
{:gun, {:gun,
github: "ninenines/gun", ref: "e1a69b36b180a574c0ac314ced9613fdd52312cc", override: true}, github: "ninenines/gun", ref: "921c47146b2d9567eac7e9a4d2ccc60fffd4f327", override: true},
{:jason, "~> 1.0"}, {:jason, "~> 1.0"},
{:mogrify, "~> 0.6.1"}, {:mogrify, "~> 0.6.1"},
{:ex_aws, "~> 2.1"}, {:ex_aws, "~> 2.1"},
@ -193,6 +189,9 @@ defp deps do
{:plug_static_index_html, "~> 1.0.0"}, {:plug_static_index_html, "~> 1.0.0"},
{:excoveralls, "~> 0.12.1", only: :test}, {:excoveralls, "~> 0.12.1", only: :test},
{:flake_id, "~> 0.1.0"}, {:flake_id, "~> 0.1.0"},
{:concurrent_limiter,
git: "https://git.pleroma.social/pleroma/elixir-libraries/concurrent_limiter",
ref: "8eee96c6ba39b9286ec44c51c52d9f2758951365"},
{:remote_ip, {:remote_ip,
git: "https://git.pleroma.social/pleroma/remote_ip.git", git: "https://git.pleroma.social/pleroma/remote_ip.git",
ref: "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8"}, ref: "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8"},

View file

@ -15,6 +15,7 @@
"certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"}, "certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
"comeonin": {:hex, :comeonin, "5.3.1", "7fe612b739c78c9c1a75186ef2d322ce4d25032d119823269d0aa1e2f1e20025", [:mix], [], "hexpm", "d6222483060c17f0977fad1b7401ef0c5863c985a64352755f366aee3799c245"}, "comeonin": {:hex, :comeonin, "5.3.1", "7fe612b739c78c9c1a75186ef2d322ce4d25032d119823269d0aa1e2f1e20025", [:mix], [], "hexpm", "d6222483060c17f0977fad1b7401ef0c5863c985a64352755f366aee3799c245"},
"concurrent_limiter": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/concurrent_limiter", "8eee96c6ba39b9286ec44c51c52d9f2758951365", [ref: "8eee96c6ba39b9286ec44c51c52d9f2758951365"]},
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"}, "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"},
"cors_plug": {:hex, :cors_plug, "1.5.2", "72df63c87e4f94112f458ce9d25800900cc88608c1078f0e4faddf20933eda6e", [:mix], [{:plug, "~> 1.3 or ~> 1.4 or ~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "9af027d20dc12dd0c4345a6b87247e0c62965871feea0bfecf9764648b02cc69"}, "cors_plug": {:hex, :cors_plug, "1.5.2", "72df63c87e4f94112f458ce9d25800900cc88608c1078f0e4faddf20933eda6e", [:mix], [{:plug, "~> 1.3 or ~> 1.4 or ~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "9af027d20dc12dd0c4345a6b87247e0c62965871feea0bfecf9764648b02cc69"},
"cowboy": {:hex, :cowboy, "2.7.0", "91ed100138a764355f43316b1d23d7ff6bdb0de4ea618cb5d8677c93a7a2f115", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "04fd8c6a39edc6aaa9c26123009200fc61f92a3a94f3178c527b70b767c6e605"}, "cowboy": {:hex, :cowboy, "2.7.0", "91ed100138a764355f43316b1d23d7ff6bdb0de4ea618cb5d8677c93a7a2f115", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "04fd8c6a39edc6aaa9c26123009200fc61f92a3a94f3178c527b70b767c6e605"},
@ -49,7 +50,7 @@
"gen_stage": {:hex, :gen_stage, "0.14.3", "d0c66f1c87faa301c1a85a809a3ee9097a4264b2edf7644bf5c123237ef732bf", [:mix], [], "hexpm"}, "gen_stage": {:hex, :gen_stage, "0.14.3", "d0c66f1c87faa301c1a85a809a3ee9097a4264b2edf7644bf5c123237ef732bf", [:mix], [], "hexpm"},
"gen_state_machine": {:hex, :gen_state_machine, "2.0.5", "9ac15ec6e66acac994cc442dcc2c6f9796cf380ec4b08267223014be1c728a95", [:mix], [], "hexpm"}, "gen_state_machine": {:hex, :gen_state_machine, "2.0.5", "9ac15ec6e66acac994cc442dcc2c6f9796cf380ec4b08267223014be1c728a95", [:mix], [], "hexpm"},
"gettext": {:hex, :gettext, "0.17.4", "f13088e1ec10ce01665cf25f5ff779e7df3f2dc71b37084976cf89d1aa124d5c", [:mix], [], "hexpm", "3c75b5ea8288e2ee7ea503ff9e30dfe4d07ad3c054576a6e60040e79a801e14d"}, "gettext": {:hex, :gettext, "0.17.4", "f13088e1ec10ce01665cf25f5ff779e7df3f2dc71b37084976cf89d1aa124d5c", [:mix], [], "hexpm", "3c75b5ea8288e2ee7ea503ff9e30dfe4d07ad3c054576a6e60040e79a801e14d"},
"gun": {:git, "https://github.com/ninenines/gun.git", "e1a69b36b180a574c0ac314ced9613fdd52312cc", [ref: "e1a69b36b180a574c0ac314ced9613fdd52312cc"]}, "gun": {:git, "https://github.com/ninenines/gun.git", "921c47146b2d9567eac7e9a4d2ccc60fffd4f327", [ref: "921c47146b2d9567eac7e9a4d2ccc60fffd4f327"]},
"hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"}, "hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"},
"html_entities": {:hex, :html_entities, "0.5.1", "1c9715058b42c35a2ab65edc5b36d0ea66dd083767bef6e3edb57870ef556549", [:mix], [], "hexpm", "30efab070904eb897ff05cd52fa61c1025d7f8ef3a9ca250bc4e6513d16c32de"}, "html_entities": {:hex, :html_entities, "0.5.1", "1c9715058b42c35a2ab65edc5b36d0ea66dd083767bef6e3edb57870ef556549", [:mix], [], "hexpm", "30efab070904eb897ff05cd52fa61c1025d7f8ef3a9ca250bc4e6513d16c32de"},
"html_sanitize_ex": {:hex, :html_sanitize_ex, "1.3.0", "f005ad692b717691203f940c686208aa3d8ffd9dd4bb3699240096a51fa9564e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"}, "html_sanitize_ex": {:hex, :html_sanitize_ex, "1.3.0", "f005ad692b717691203f940c686208aa3d8ffd9dd4bb3699240096a51fa9564e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"},
@ -107,7 +108,7 @@
"swoosh": {:git, "https://github.com/swoosh/swoosh", "c96e0ca8a00d8f211ec1f042a4626b09f249caa5", [ref: "c96e0ca8a00d8f211ec1f042a4626b09f249caa5"]}, "swoosh": {:git, "https://github.com/swoosh/swoosh", "c96e0ca8a00d8f211ec1f042a4626b09f249caa5", [ref: "c96e0ca8a00d8f211ec1f042a4626b09f249caa5"]},
"syslog": {:hex, :syslog, "1.1.0", "6419a232bea84f07b56dc575225007ffe34d9fdc91abe6f1b2f254fd71d8efc2", [:rebar3], [], "hexpm", "4c6a41373c7e20587be33ef841d3de6f3beba08519809329ecc4d27b15b659e1"}, "syslog": {:hex, :syslog, "1.1.0", "6419a232bea84f07b56dc575225007ffe34d9fdc91abe6f1b2f254fd71d8efc2", [:rebar3], [], "hexpm", "4c6a41373c7e20587be33ef841d3de6f3beba08519809329ecc4d27b15b659e1"},
"telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"}, "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"},
"tesla": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/tesla.git", "61b7503cef33f00834f78ddfafe0d5d9dec2270b", [ref: "61b7503cef33f00834f78ddfafe0d5d9dec2270b"]}, "tesla": {:git, "https://github.com/teamon/tesla.git", "af3707078b10793f6a534938e56b963aff82fe3c", [ref: "af3707078b10793f6a534938e56b963aff82fe3c"]},
"timex": {:hex, :timex, "3.6.1", "efdf56d0e67a6b956cc57774353b0329c8ab7726766a11547e529357ffdc1d56", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "f354efb2400dd7a80fd9eb6c8419068c4f632da4ac47f3d8822d6e33f08bc852"}, "timex": {:hex, :timex, "3.6.1", "efdf56d0e67a6b956cc57774353b0329c8ab7726766a11547e529357ffdc1d56", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "f354efb2400dd7a80fd9eb6c8419068c4f632da4ac47f3d8822d6e33f08bc852"},
"trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"}, "trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"},
"tzdata": {:hex, :tzdata, "1.0.3", "73470ad29dde46e350c60a66e6b360d3b99d2d18b74c4c349dbebbc27a09a3eb", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a6e1ee7003c4d04ecbd21dd3ec690d4c6662db5d3bbdd7262d53cdf5e7c746c1"}, "tzdata": {:hex, :tzdata, "1.0.3", "73470ad29dde46e350c60a66e6b360d3b99d2d18b74c4c349dbebbc27a09a3eb", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a6e1ee7003c4d04ecbd21dd3ec690d4c6662db5d3bbdd7262d53cdf5e7c746c1"},

View file

@ -90,110 +90,100 @@ msgid "must be equal to %{number}"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/common_api/common_api.ex:421 #: lib/pleroma/web/common_api/common_api.ex:505
msgid "Account not found" msgid "Account not found"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/common_api/common_api.ex:249 #: lib/pleroma/web/common_api/common_api.ex:339
msgid "Already voted" msgid "Already voted"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/oauth/oauth_controller.ex:360 #: lib/pleroma/web/oauth/oauth_controller.ex:359
msgid "Bad request" msgid "Bad request"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:425 #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:426
msgid "Can't delete object" msgid "Can't delete object"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/mastodon_api/controllers/status_controller.ex:196 #: lib/pleroma/web/controller_helper.ex:105
msgid "Can't delete this post" #: lib/pleroma/web/controller_helper.ex:111
msgstr ""
#, elixir-format
#: lib/pleroma/web/controller_helper.ex:95
#: lib/pleroma/web/controller_helper.ex:101
msgid "Can't display this activity" msgid "Can't display this activity"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:227 #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:285
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:254
msgid "Can't find user" msgid "Can't find user"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:114 #: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:61
msgid "Can't get favorites" msgid "Can't get favorites"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:437 #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:438
msgid "Can't like object" msgid "Can't like object"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/common_api/utils.ex:556 #: lib/pleroma/web/common_api/utils.ex:563
msgid "Cannot post an empty status without attachments" msgid "Cannot post an empty status without attachments"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/common_api/utils.ex:504 #: lib/pleroma/web/common_api/utils.ex:511
msgid "Comment must be up to %{max_size} characters" msgid "Comment must be up to %{max_size} characters"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/config/config_db.ex:222 #: lib/pleroma/config/config_db.ex:191
msgid "Config with params %{params} not found" msgid "Config with params %{params} not found"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/common_api/common_api.ex:95 #: lib/pleroma/web/common_api/common_api.ex:181
#: lib/pleroma/web/common_api/common_api.ex:185
msgid "Could not delete" msgid "Could not delete"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/common_api/common_api.ex:141 #: lib/pleroma/web/common_api/common_api.ex:231
msgid "Could not favorite" msgid "Could not favorite"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/common_api/common_api.ex:370 #: lib/pleroma/web/common_api/common_api.ex:453
msgid "Could not pin" msgid "Could not pin"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/common_api/common_api.ex:112 #: lib/pleroma/web/common_api/common_api.ex:278
msgid "Could not repeat"
msgstr ""
#, elixir-format
#: lib/pleroma/web/common_api/common_api.ex:188
msgid "Could not unfavorite" msgid "Could not unfavorite"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/common_api/common_api.ex:380 #: lib/pleroma/web/common_api/common_api.ex:463
msgid "Could not unpin" msgid "Could not unpin"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/common_api/common_api.ex:126 #: lib/pleroma/web/common_api/common_api.ex:216
msgid "Could not unrepeat" msgid "Could not unrepeat"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/common_api/common_api.ex:428 #: lib/pleroma/web/common_api/common_api.ex:512
#: lib/pleroma/web/common_api/common_api.ex:437 #: lib/pleroma/web/common_api/common_api.ex:521
msgid "Could not update state" msgid "Could not update state"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:202 #: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:207
msgid "Error." msgid "Error."
msgstr "" msgstr ""
@ -203,8 +193,8 @@ msgid "Invalid CAPTCHA"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:117 #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:116
#: lib/pleroma/web/oauth/oauth_controller.ex:569 #: lib/pleroma/web/oauth/oauth_controller.ex:568
msgid "Invalid credentials" msgid "Invalid credentials"
msgstr "" msgstr ""
@ -214,22 +204,22 @@ msgid "Invalid credentials."
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/common_api/common_api.ex:265 #: lib/pleroma/web/common_api/common_api.ex:355
msgid "Invalid indices" msgid "Invalid indices"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/admin_api/admin_api_controller.ex:1147 #: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:29
msgid "Invalid parameters" msgid "Invalid parameters"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/common_api/utils.ex:411 #: lib/pleroma/web/common_api/utils.ex:414
msgid "Invalid password." msgid "Invalid password."
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:187 #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:220
msgid "Invalid request" msgid "Invalid request"
msgstr "" msgstr ""
@ -239,44 +229,44 @@ msgid "Kocaptcha service unavailable"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:113 #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:112
msgid "Missing parameters" msgid "Missing parameters"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/common_api/utils.ex:540 #: lib/pleroma/web/common_api/utils.ex:547
msgid "No such conversation" msgid "No such conversation"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/admin_api/admin_api_controller.ex:439 #: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:388
#: lib/pleroma/web/admin_api/admin_api_controller.ex:465 lib/pleroma/web/admin_api/admin_api_controller.ex:507 #: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:414 lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:456
msgid "No such permission_group" msgid "No such permission_group"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/plugs/uploaded_media.ex:74 #: lib/pleroma/plugs/uploaded_media.ex:84
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:485 lib/pleroma/web/admin_api/admin_api_controller.ex:1135 #: 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:73 lib/pleroma/web/ostatus/ostatus_controller.ex:143 #: lib/pleroma/web/feed/user_controller.ex:71 lib/pleroma/web/ostatus/ostatus_controller.ex:143
msgid "Not found" msgid "Not found"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/common_api/common_api.ex:241 #: lib/pleroma/web/common_api/common_api.ex:331
msgid "Poll's author can't vote" msgid "Poll's author can't vote"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:20 #: 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: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:290 #: 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 #: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:71
msgid "Record not found" msgid "Record not found"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/admin_api/admin_api_controller.ex:1153 #: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:35
#: lib/pleroma/web/feed/user_controller.ex:79 lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:32 #: lib/pleroma/web/feed/user_controller.ex:77 lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:36
#: lib/pleroma/web/ostatus/ostatus_controller.ex:149 #: lib/pleroma/web/ostatus/ostatus_controller.ex:149
msgid "Something went wrong" msgid "Something went wrong"
msgstr "" msgstr ""
@ -287,7 +277,7 @@ msgid "The message visibility must be direct"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/common_api/utils.ex:566 #: lib/pleroma/web/common_api/utils.ex:573
msgid "The status is over the character limit" msgid "The status is over the character limit"
msgstr "" msgstr ""
@ -302,65 +292,65 @@ msgid "Throttled"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/common_api/common_api.ex:266 #: lib/pleroma/web/common_api/common_api.ex:356
msgid "Too many choices" msgid "Too many choices"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:442 #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:443
msgid "Unhandled activity type" msgid "Unhandled activity type"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/admin_api/admin_api_controller.ex:536 #: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:485
msgid "You can't revoke your own admin status." msgid "You can't revoke your own admin status."
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/oauth/oauth_controller.ex:218 #: lib/pleroma/web/oauth/oauth_controller.ex:221
#: lib/pleroma/web/oauth/oauth_controller.ex:309 #: lib/pleroma/web/oauth/oauth_controller.ex:308
msgid "Your account is currently disabled" msgid "Your account is currently disabled"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/oauth/oauth_controller.ex:180 #: lib/pleroma/web/oauth/oauth_controller.ex:183
#: lib/pleroma/web/oauth/oauth_controller.ex:332 #: lib/pleroma/web/oauth/oauth_controller.ex:331
msgid "Your login is missing a confirmed e-mail address" msgid "Your login is missing a confirmed e-mail address"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:389 #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:390
msgid "can't read inbox of %{nickname} as %{as_nickname}" msgid "can't read inbox of %{nickname} as %{as_nickname}"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:472 #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:473
msgid "can't update outbox of %{nickname} as %{as_nickname}" msgid "can't update outbox of %{nickname} as %{as_nickname}"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/common_api/common_api.ex:388 #: lib/pleroma/web/common_api/common_api.ex:471
msgid "conversation is already muted" msgid "conversation is already muted"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:316 #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:314
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:491 #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:492
msgid "error" msgid "error"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:29 #: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:32
msgid "mascots can only be images" msgid "mascots can only be images"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:60 #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:62
msgid "not found" msgid "not found"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/oauth/oauth_controller.ex:395 #: lib/pleroma/web/oauth/oauth_controller.ex:394
msgid "Bad OAuth request." msgid "Bad OAuth request."
msgstr "" msgstr ""
@ -375,17 +365,17 @@ msgid "CAPTCHA expired"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/plugs/uploaded_media.ex:55 #: lib/pleroma/plugs/uploaded_media.ex:57
msgid "Failed" msgid "Failed"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/oauth/oauth_controller.ex:411 #: lib/pleroma/web/oauth/oauth_controller.ex:410
msgid "Failed to authenticate: %{message}." msgid "Failed to authenticate: %{message}."
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/oauth/oauth_controller.ex:442 #: lib/pleroma/web/oauth/oauth_controller.ex:441
msgid "Failed to set up user account." msgid "Failed to set up user account."
msgstr "" msgstr ""
@ -395,7 +385,7 @@ msgid "Insufficient permissions: %{permissions}."
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/plugs/uploaded_media.ex:94 #: lib/pleroma/plugs/uploaded_media.ex:104
msgid "Internal Error" msgid "Internal Error"
msgstr "" msgstr ""
@ -411,12 +401,12 @@ msgid "Invalid answer data"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:128 #: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:33
msgid "Nodeinfo schema version not handled" msgid "Nodeinfo schema version not handled"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/oauth/oauth_controller.ex:169 #: lib/pleroma/web/oauth/oauth_controller.ex:172
msgid "This action is outside the authorized scopes" msgid "This action is outside the authorized scopes"
msgstr "" msgstr ""
@ -426,13 +416,13 @@ msgid "Unknown error, please check the details and try again."
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/oauth/oauth_controller.ex:116 #: lib/pleroma/web/oauth/oauth_controller.ex:119
#: lib/pleroma/web/oauth/oauth_controller.ex:155 #: lib/pleroma/web/oauth/oauth_controller.ex:158
msgid "Unlisted redirect_uri." msgid "Unlisted redirect_uri."
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/oauth/oauth_controller.ex:391 #: lib/pleroma/web/oauth/oauth_controller.ex:390
msgid "Unsupported OAuth provider: %{provider}." msgid "Unsupported OAuth provider: %{provider}."
msgstr "" msgstr ""
@ -452,12 +442,12 @@ msgid "CAPTCHA Error"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/common_api/common_api.ex:200 #: lib/pleroma/web/common_api/common_api.ex:290
msgid "Could not add reaction emoji" msgid "Could not add reaction emoji"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/common_api/common_api.ex:211 #: lib/pleroma/web/common_api/common_api.ex:301
msgid "Could not remove reaction emoji" msgid "Could not remove reaction emoji"
msgstr "" msgstr ""
@ -472,39 +462,45 @@ msgid "List not found"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:124 #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:123
msgid "Missing parameter: %{name}" msgid "Missing parameter: %{name}"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/oauth/oauth_controller.ex:207 #: lib/pleroma/web/oauth/oauth_controller.ex:210
#: lib/pleroma/web/oauth/oauth_controller.ex:322 #: lib/pleroma/web/oauth/oauth_controller.ex:321
msgid "Password reset is required" msgid "Password reset is required"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/tests/auth_test_controller.ex:9 #: lib/pleroma/tests/auth_test_controller.ex:9
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:6 lib/pleroma/web/admin_api/admin_api_controller.ex:6 #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:6 lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:6
#: lib/pleroma/web/controller_helper.ex:6 lib/pleroma/web/fallback_redirect_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/feed/tag_controller.ex:6 lib/pleroma/web/feed/user_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/mailer/subscription_controller.ex:2 lib/pleroma/web/masto_fe_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/mastodon_api/controllers/account_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/app_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/mastodon_api/controllers/auth_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex:6 #: lib/pleroma/web/controller_helper.ex:6 lib/pleroma/web/embed_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/fallback_redirect_controller.ex:6 lib/pleroma/web/feed/tag_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/feed/user_controller.ex:6 lib/pleroma/web/mailer/subscription_controller.ex:2
#: lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/instance_controller.ex:6 #: lib/pleroma/web/masto_fe_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/account_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/marker_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/mastodon_api_controller.ex:14 lib/pleroma/web/mastodon_api/controllers/media_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/notification_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/poll_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/report_controller.ex:8 lib/pleroma/web/mastodon_api/controllers/scheduled_activity_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/search_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/status_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/subscription_controller.ex:7 lib/pleroma/web/mastodon_api/controllers/suggestion_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/timeline_controller.ex:6 lib/pleroma/web/media_proxy/media_proxy_controller.ex:6 #: lib/pleroma/web/mastodon_api/controllers/media_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/notification_controller.ex:6
#: lib/pleroma/web/mongooseim/mongoose_im_controller.ex:6 lib/pleroma/web/nodeinfo/nodeinfo_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/oauth/fallback_controller.ex:6 lib/pleroma/web/oauth/mfa_controller.ex:10 #: lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/search_controller.ex:6
#: lib/pleroma/web/oauth/oauth_controller.ex:6 lib/pleroma/web/ostatus/ostatus_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/pleroma_api/controllers/account_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:2 #: lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:6
#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/pleroma_api_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/scrobble_controller.ex:6
#: lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex:7 lib/pleroma/web/static_fe/static_fe_controller.ex:6 #: lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex:7 lib/pleroma/web/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/password_controller.ex:10 lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex:6
@ -519,46 +515,56 @@ msgid "Two-factor authentication enabled, you must use a access token."
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:210 #: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:210
msgid "Unexpected error occurred while adding file to pack." msgid "Unexpected error occurred while adding file to pack."
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:138 #: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:138
msgid "Unexpected error occurred while creating pack." msgid "Unexpected error occurred while creating pack."
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:278 #: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:278
msgid "Unexpected error occurred while removing file from pack." msgid "Unexpected error occurred while removing file from pack."
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:250 #: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:250
msgid "Unexpected error occurred while updating file in pack." msgid "Unexpected error occurred while updating file in pack."
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:179 #: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:179
msgid "Unexpected error occurred while updating pack metadata." msgid "Unexpected error occurred while updating pack metadata."
msgstr "" msgstr ""
#, elixir-format
#: lib/pleroma/plugs/user_is_admin_plug.ex:40
msgid "User is not an admin or OAuth admin scope is not granted."
msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61 #: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61
msgid "Web push subscription is disabled on this Pleroma instance" msgid "Web push subscription is disabled on this Pleroma instance"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/admin_api/admin_api_controller.ex:502 #: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:451
msgid "You can't revoke your own admin/moderator status." msgid "You can't revoke your own admin/moderator status."
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:105 #: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:126
msgid "authorization required for timeline view" msgid "authorization required for timeline view"
msgstr "" msgstr ""
#, elixir-format
#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:24
msgid "Access denied"
msgstr ""
#, elixir-format
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:282
msgid "This API requires an authenticated user"
msgstr ""
#, elixir-format
#: lib/pleroma/plugs/user_is_admin_plug.ex:21
msgid "User is not an admin."
msgstr ""

View file

@ -562,11 +562,11 @@ msgstr "Errore inaspettato durante l'aggiornamento del file nel pacchetto."
msgid "Unexpected error occurred while updating pack metadata." msgid "Unexpected error occurred while updating pack metadata."
msgstr "Errore inaspettato durante l'aggiornamento dei metadati del pacchetto." msgstr "Errore inaspettato durante l'aggiornamento dei metadati del pacchetto."
#: lib/pleroma/plugs/user_is_admin_plug.ex:40 #: lib/pleroma/plugs/user_is_admin_plug.ex:21
#, elixir-format #, elixir-format
msgid "User is not an admin or OAuth admin scope is not granted." msgid "User is not an admin."
msgstr "" msgstr ""
"L'utente non è un amministratore o non ha ricevuto questa autorizzazione " "L'utente non è un amministratore."
"OAuth." "OAuth."
#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61 #: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61

View file

@ -559,9 +559,9 @@ msgstr ""
msgid "Unexpected error occurred while updating pack metadata." msgid "Unexpected error occurred while updating pack metadata."
msgstr "" msgstr ""
#: lib/pleroma/plugs/user_is_admin_plug.ex:40 #: lib/pleroma/plugs/user_is_admin_plug.ex:21
#, elixir-format #, elixir-format
msgid "User is not an admin or OAuth admin scope is not granted." msgid "User is not an admin."
msgstr "" msgstr ""
#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61 #: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61

View file

@ -566,9 +566,9 @@ msgstr "Nieoczekiwany błąd podczas zmieniania pliku w paczce."
msgid "Unexpected error occurred while updating pack metadata." msgid "Unexpected error occurred while updating pack metadata."
msgstr "Nieoczekiwany błąd podczas zmieniania metadanych paczki." msgstr "Nieoczekiwany błąd podczas zmieniania metadanych paczki."
#: lib/pleroma/plugs/user_is_admin_plug.ex:40 #: lib/pleroma/plugs/user_is_admin_plug.ex:21
#, elixir-format #, elixir-format
msgid "User is not an admin or OAuth admin scope is not granted." msgid "User is not an admin."
msgstr "" msgstr ""
#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61 #: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61

View file

@ -0,0 +1,19 @@
defmodule Pleroma.Repo.Migrations.RenameNotificationPrivacyOption do
use Ecto.Migration
def up do
execute(
"UPDATE users SET notification_settings = notification_settings - 'privacy_option' || jsonb_build_object('hide_notification_contents', notification_settings->'privacy_option')
where notification_settings ? 'privacy_option'
and local"
)
end
def down do
execute(
"UPDATE users SET notification_settings = notification_settings - 'hide_notification_contents' || jsonb_build_object('privacy_option', notification_settings->'hide_notification_contents')
where notification_settings ? 'hide_notification_contents'
and local"
)
end
end

View file

@ -0,0 +1,27 @@
defmodule Elixir.Pleroma.Repo.Migrations.Oban20ConfigChanges do
use Ecto.Migration
import Ecto.Query
alias Pleroma.ConfigDB
alias Pleroma.Repo
def change do
config_entry =
from(c in ConfigDB, where: c.group == ^":pleroma" and c.key == ^"Oban")
|> select([c], struct(c, [:value, :id]))
|> Repo.one()
if config_entry do
%{value: value} = config_entry
value =
case Keyword.fetch(value, :verbose) do
{:ok, log} -> Keyword.put_new(value, :log, log)
_ -> value
end
|> Keyword.drop([:verbose, :prune])
Ecto.Changeset.change(config_entry, %{value: value})
|> Repo.update()
end
end
end

View file

@ -1 +1 @@
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1,user-scalable=no"><title>Pleroma</title><!--server-generated-meta--><link rel=icon type=image/png href=/favicon.png><link href=/static/css/app.77b1644622e3bae24b6b.css rel=stylesheet><link href=/static/fontello.1594374054351.css rel=stylesheet></head><body class=hidden><noscript>To use Pleroma, please enable JavaScript.</noscript><div id=app></div><script type=text/javascript src=/static/js/vendors~app.247dc52c7abe6a0dab87.js></script><script type=text/javascript src=/static/js/app.1e68e208590653dab5aa.js></script></body></html> <!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1,user-scalable=no"><title>Pleroma</title><!--server-generated-meta--><link rel=icon type=image/png href=/favicon.png><link href=/static/css/app.6dbc7dea4fc148c85860.css rel=stylesheet><link href=/static/fontello.1594823398494.css rel=stylesheet></head><body class=hidden><noscript>To use Pleroma, please enable JavaScript.</noscript><div id=app></div><script type=text/javascript src=/static/js/vendors~app.9e24ed238da5a8538f50.js></script><script type=text/javascript src=/static/js/app.31bba9f1e242ff273dcb.js></script></body></html>

View file

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

0
priv/static/static/fontello.json Executable file → Normal file
View file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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