Merge branch 'develop' into 'cleanup/masto_fe-default_settings'
# Conflicts: # lib/pleroma/web/views/masto_fe_view.ex
This commit is contained in:
commit
e1a1c8e7de
520 changed files with 10366 additions and 5911 deletions
|
|
@ -3,15 +3,53 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Mix.Pleroma do
|
||||
@apps [
|
||||
:restarter,
|
||||
:ecto,
|
||||
:ecto_sql,
|
||||
:postgrex,
|
||||
:db_connection,
|
||||
:cachex,
|
||||
:flake_id,
|
||||
:swoosh,
|
||||
:timex
|
||||
]
|
||||
@cachex_children ["object", "user"]
|
||||
@doc "Common functions to be reused in mix tasks"
|
||||
def start_pleroma do
|
||||
Pleroma.Config.Holder.save_default()
|
||||
Application.put_env(:phoenix, :serve_endpoints, false, persistent: true)
|
||||
|
||||
if Pleroma.Config.get(:env) != :test do
|
||||
Application.put_env(:logger, :console, level: :debug)
|
||||
end
|
||||
|
||||
{:ok, _} = Application.ensure_all_started(:pleroma)
|
||||
adapter = Application.get_env(:tesla, :adapter)
|
||||
|
||||
apps =
|
||||
if adapter == Tesla.Adapter.Gun do
|
||||
[:gun | @apps]
|
||||
else
|
||||
[:hackney | @apps]
|
||||
end
|
||||
|
||||
Enum.each(apps, &Application.ensure_all_started/1)
|
||||
|
||||
children =
|
||||
[
|
||||
Pleroma.Repo,
|
||||
{Pleroma.Config.TransferTask, false},
|
||||
Pleroma.Web.Endpoint,
|
||||
{Oban, Pleroma.Config.get(Oban)}
|
||||
] ++
|
||||
http_children(adapter)
|
||||
|
||||
cachex_children = Enum.map(@cachex_children, &Pleroma.Application.build_cachex(&1, []))
|
||||
|
||||
Supervisor.start_link(children ++ cachex_children,
|
||||
strategy: :one_for_one,
|
||||
name: Pleroma.Supervisor
|
||||
)
|
||||
|
||||
if Pleroma.Config.get(:env) not in [:test, :benchmark] do
|
||||
pleroma_rebooted?()
|
||||
|
|
@ -82,4 +120,11 @@ defmodule Mix.Pleroma do
|
|||
def escape_sh_path(path) do
|
||||
~S(') <> String.replace(path, ~S('), ~S(\')) <> ~S(')
|
||||
end
|
||||
|
||||
defp http_children(Tesla.Adapter.Gun) do
|
||||
Pleroma.Gun.ConnectionPool.children() ++
|
||||
[{Task, &Pleroma.HTTP.AdapterHelper.Gun.limiter_setup/0}]
|
||||
end
|
||||
|
||||
defp http_children(_), do: []
|
||||
end
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ defmodule Mix.Tasks.Pleroma.Config do
|
|||
|
||||
defp do_migrate_to_db(config_file) do
|
||||
if File.exists?(config_file) do
|
||||
shell_info("Migrating settings from file: #{Path.expand(config_file)}")
|
||||
Ecto.Adapters.SQL.query!(Repo, "TRUNCATE config;")
|
||||
Ecto.Adapters.SQL.query!(Repo, "ALTER SEQUENCE config_id_seq RESTART;")
|
||||
|
||||
|
|
@ -82,7 +83,7 @@ defmodule Mix.Tasks.Pleroma.Config do
|
|||
|
||||
defp migrate_from_db(opts) do
|
||||
if Pleroma.Config.get([:configurable_from_database]) do
|
||||
env = opts[:env] || "prod"
|
||||
env = opts[:env] || Pleroma.Config.get(:env)
|
||||
|
||||
config_path =
|
||||
if Pleroma.Config.get(:release) do
|
||||
|
|
@ -104,6 +105,10 @@ defmodule Mix.Tasks.Pleroma.Config do
|
|||
|
||||
:ok = File.close(file)
|
||||
System.cmd("mix", ["format", config_path])
|
||||
|
||||
shell_info(
|
||||
"Database configuration settings have been exported to config/#{env}.exported_from_db.secret.exs"
|
||||
)
|
||||
else
|
||||
migration_error()
|
||||
end
|
||||
|
|
@ -111,7 +116,7 @@ defmodule Mix.Tasks.Pleroma.Config do
|
|||
|
||||
defp migration_error do
|
||||
shell_error(
|
||||
"Migration is not allowed in config. You can change this behavior by setting `configurable_from_database` to true."
|
||||
"Migration is not allowed in config. You can change this behavior by setting `config :pleroma, configurable_from_database: true`"
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ defmodule Mix.Tasks.Pleroma.Instance do
|
|||
options,
|
||||
:uploads_dir,
|
||||
"What directory should media uploads go in (when using the local uploader)?",
|
||||
Pleroma.Config.get([Pleroma.Uploaders.Local, :uploads])
|
||||
Config.get([Pleroma.Uploaders.Local, :uploads])
|
||||
)
|
||||
|> Path.expand()
|
||||
|
||||
|
|
@ -154,7 +154,7 @@ defmodule Mix.Tasks.Pleroma.Instance do
|
|||
options,
|
||||
:static_dir,
|
||||
"What directory should custom public files be read from (custom emojis, frontend bundle overrides, robots.txt, etc.)?",
|
||||
Pleroma.Config.get([:instance, :static_dir])
|
||||
Config.get([:instance, :static_dir])
|
||||
)
|
||||
|> Path.expand()
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ defmodule Mix.Tasks.Pleroma.NotificationSettings do
|
|||
@moduledoc """
|
||||
Example:
|
||||
|
||||
> mix pleroma.notification_settings --privacy-option=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=false --nickname-users="parallel588" # set false only for parallel588 user
|
||||
> mix pleroma.notification_settings --hide-notification-contents=true # set true for all users
|
||||
|
||||
"""
|
||||
|
||||
|
|
@ -19,16 +19,16 @@ defmodule Mix.Tasks.Pleroma.NotificationSettings do
|
|||
OptionParser.parse(
|
||||
args,
|
||||
strict: [
|
||||
privacy_option: :boolean,
|
||||
hide_notification_contents: :boolean,
|
||||
email_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
|
||||
privacy_option
|
||||
if not is_nil(hide_notification_contents) do
|
||||
hide_notification_contents
|
||||
|> build_query(options)
|
||||
|> Pleroma.Repo.update_all([])
|
||||
end
|
||||
|
|
@ -36,15 +36,15 @@ defmodule Mix.Tasks.Pleroma.NotificationSettings do
|
|||
shell_info("Done")
|
||||
end
|
||||
|
||||
defp build_query(privacy_option, options) do
|
||||
defp build_query(hide_notification_contents, options) do
|
||||
query =
|
||||
from(u in Pleroma.User,
|
||||
update: [
|
||||
set: [
|
||||
notification_settings:
|
||||
fragment(
|
||||
"jsonb_set(notification_settings, '{privacy_option}', ?)",
|
||||
^privacy_option
|
||||
"jsonb_set(notification_settings, '{hide_notification_contents}', ?)",
|
||||
^hide_notification_contents
|
||||
)
|
||||
]
|
||||
]
|
||||
|
|
|
|||
|
|
@ -17,30 +17,53 @@ defmodule Mix.Tasks.Pleroma.RefreshCounterCache do
|
|||
def run([]) do
|
||||
Mix.Pleroma.start_pleroma()
|
||||
|
||||
["public", "unlisted", "private", "direct"]
|
||||
|> Enum.each(fn visibility ->
|
||||
count = status_visibility_count_query(visibility)
|
||||
name = "status_visibility_#{visibility}"
|
||||
CounterCache.set(name, count)
|
||||
Mix.Pleroma.shell_info("Set #{name} to #{count}")
|
||||
instances =
|
||||
Activity
|
||||
|> distinct([a], true)
|
||||
|> select([a], fragment("split_part(?, '/', 3)", a.actor))
|
||||
|> Repo.all()
|
||||
|
||||
instances
|
||||
|> Enum.with_index(1)
|
||||
|> Enum.each(fn {instance, i} ->
|
||||
counters = instance_counters(instance)
|
||||
CounterCache.set(instance, counters)
|
||||
|
||||
Mix.Pleroma.shell_info(
|
||||
"[#{i}/#{length(instances)}] Setting #{instance} counters: #{inspect(counters)}"
|
||||
)
|
||||
end)
|
||||
|
||||
Mix.Pleroma.shell_info("Done")
|
||||
end
|
||||
|
||||
defp status_visibility_count_query(visibility) do
|
||||
defp instance_counters(instance) do
|
||||
counters = %{"public" => 0, "unlisted" => 0, "private" => 0, "direct" => 0}
|
||||
|
||||
Activity
|
||||
|> where(
|
||||
|> where([a], fragment("(? ->> 'type'::text) = 'Create'", a.data))
|
||||
|> where([a], fragment("split_part(?, '/', 3) = ?", a.actor, ^instance))
|
||||
|> select(
|
||||
[a],
|
||||
{fragment(
|
||||
"activity_visibility(?, ?, ?)",
|
||||
a.actor,
|
||||
a.recipients,
|
||||
a.data
|
||||
), count(a.id)}
|
||||
)
|
||||
|> group_by(
|
||||
[a],
|
||||
fragment(
|
||||
"activity_visibility(?, ?, ?) = ?",
|
||||
"activity_visibility(?, ?, ?)",
|
||||
a.actor,
|
||||
a.recipients,
|
||||
a.data,
|
||||
^visibility
|
||||
a.data
|
||||
)
|
||||
)
|
||||
|> where([a], fragment("(? ->> 'type'::text) = 'Create'", a.data))
|
||||
|> Repo.aggregate(:count, :id, timeout: :timer.minutes(30))
|
||||
|> Repo.all(timeout: :timer.minutes(30))
|
||||
|> Enum.reduce(counters, fn {visibility, count}, acc ->
|
||||
Map.put(acc, visibility, count)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -232,7 +232,7 @@ defmodule Mix.Tasks.Pleroma.User do
|
|||
with %User{} = user <- User.get_cached_by_nickname(nickname) do
|
||||
user = user |> User.tag(tags)
|
||||
|
||||
shell_info("Tags of #{user.nickname}: #{inspect(tags)}")
|
||||
shell_info("Tags of #{user.nickname}: #{inspect(user.tags)}")
|
||||
else
|
||||
_ ->
|
||||
shell_error("Could not change user tags for #{nickname}")
|
||||
|
|
@ -245,7 +245,7 @@ defmodule Mix.Tasks.Pleroma.User do
|
|||
with %User{} = user <- User.get_cached_by_nickname(nickname) do
|
||||
user = user |> User.untag(tags)
|
||||
|
||||
shell_info("Tags of #{user.nickname}: #{inspect(tags)}")
|
||||
shell_info("Tags of #{user.nickname}: #{inspect(user.tags)}")
|
||||
else
|
||||
_ ->
|
||||
shell_error("Could not change user tags for #{nickname}")
|
||||
|
|
|
|||
|
|
@ -35,13 +35,19 @@ defmodule Pleroma.Application do
|
|||
# See http://elixir-lang.org/docs/stable/elixir/Application.html
|
||||
# for more information on OTP Applications
|
||||
def start(_type, _args) do
|
||||
Pleroma.Config.Holder.save_default()
|
||||
# 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()
|
||||
Pleroma.HTML.compile_scrubbers()
|
||||
Config.DeprecationWarnings.warn()
|
||||
Pleroma.Plugs.HTTPSecurityPlug.warn_if_disabled()
|
||||
Pleroma.Repo.check_migrations_applied!()
|
||||
Pleroma.ApplicationRequirements.verify!()
|
||||
setup_instrumenters()
|
||||
load_custom_modules()
|
||||
Pleroma.Docs.JSON.compile()
|
||||
|
||||
adapter = Application.get_env(:tesla, :adapter)
|
||||
|
||||
|
|
@ -162,7 +168,8 @@ defmodule Pleroma.Application do
|
|||
defp seconds_valid_interval,
|
||||
do: :timer.seconds(Config.get!([Pleroma.Captcha, :seconds_valid]))
|
||||
|
||||
defp build_cachex(type, opts),
|
||||
@spec build_cachex(String.t(), keyword()) :: map()
|
||||
def build_cachex(type, opts),
|
||||
do: %{
|
||||
id: String.to_atom("cachex_" <> type),
|
||||
start: {Cachex, :start_link, [String.to_atom(type <> "_cache"), opts]},
|
||||
|
|
@ -217,9 +224,7 @@ defmodule Pleroma.Application do
|
|||
|
||||
# start hackney and gun pools in tests
|
||||
defp http_children(_, :test) do
|
||||
hackney_options = Config.get([:hackney_pools, :federation])
|
||||
hackney_pool = :hackney_pool.child_spec(:federation, hackney_options)
|
||||
[hackney_pool, Pleroma.Pool.Supervisor]
|
||||
http_children(Tesla.Adapter.Hackney, nil) ++ http_children(Tesla.Adapter.Gun, nil)
|
||||
end
|
||||
|
||||
defp http_children(Tesla.Adapter.Hackney, _) do
|
||||
|
|
@ -238,7 +243,10 @@ defmodule Pleroma.Application do
|
|||
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: []
|
||||
end
|
||||
|
|
|
|||
143
lib/pleroma/application_requirements.ex
Normal file
143
lib/pleroma/application_requirements.ex
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.ApplicationRequirements do
|
||||
@moduledoc """
|
||||
The module represents the collection of validations to runs before start server.
|
||||
"""
|
||||
|
||||
defmodule VerifyError, do: defexception([:message])
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
require Logger
|
||||
|
||||
@spec verify!() :: :ok | VerifyError.t()
|
||||
def verify! do
|
||||
:ok
|
||||
|> check_confirmation_accounts!
|
||||
|> check_migrations_applied!()
|
||||
|> check_welcome_message_config!()
|
||||
|> check_rum!()
|
||||
|> handle_result()
|
||||
end
|
||||
|
||||
defp handle_result(:ok), do: :ok
|
||||
defp handle_result({:error, message}), do: raise(VerifyError, message: message)
|
||||
|
||||
defp check_welcome_message_config!(:ok) do
|
||||
if Pleroma.Config.get([:welcome, :email, :enabled], false) and
|
||||
not Pleroma.Emails.Mailer.enabled?() do
|
||||
Logger.error("""
|
||||
To send welcome email do you need to enable mail.
|
||||
\nconfig :pleroma, Pleroma.Emails.Mailer, enabled: true
|
||||
""")
|
||||
|
||||
{:error, "The mail disabled."}
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp check_welcome_message_config!(result), do: result
|
||||
|
||||
# Checks account confirmation email
|
||||
#
|
||||
def check_confirmation_accounts!(:ok) do
|
||||
if Pleroma.Config.get([:instance, :account_activation_required]) &&
|
||||
not Pleroma.Config.get([Pleroma.Emails.Mailer, :enabled]) do
|
||||
Logger.error(
|
||||
"Account activation enabled, but no Mailer settings enabled.\nPlease set config :pleroma, :instance, account_activation_required: false\nOtherwise setup and enable Mailer."
|
||||
)
|
||||
|
||||
{:error,
|
||||
"Account activation enabled, but Mailer is disabled. Cannot send confirmation emails."}
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
def check_confirmation_accounts!(result), do: result
|
||||
|
||||
# Checks for pending migrations.
|
||||
#
|
||||
def check_migrations_applied!(:ok) do
|
||||
unless Pleroma.Config.get(
|
||||
[:i_am_aware_this_may_cause_data_loss, :disable_migration_check],
|
||||
false
|
||||
) do
|
||||
{_, res, _} =
|
||||
Ecto.Migrator.with_repo(Pleroma.Repo, fn repo ->
|
||||
down_migrations =
|
||||
Ecto.Migrator.migrations(repo)
|
||||
|> Enum.reject(fn
|
||||
{:up, _, _} -> true
|
||||
{:down, _, _} -> false
|
||||
end)
|
||||
|
||||
if length(down_migrations) > 0 do
|
||||
down_migrations_text =
|
||||
Enum.map(down_migrations, fn {:down, id, name} -> "- #{name} (#{id})\n" end)
|
||||
|
||||
Logger.error(
|
||||
"The following migrations were not applied:\n#{down_migrations_text}If you want to start Pleroma anyway, set\nconfig :pleroma, :i_am_aware_this_may_cause_data_loss, disable_migration_check: true"
|
||||
)
|
||||
|
||||
{:error, "Unapplied Migrations detected"}
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end)
|
||||
|
||||
res
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
def check_migrations_applied!(result), do: result
|
||||
|
||||
# Checks for settings of RUM indexes.
|
||||
#
|
||||
defp check_rum!(:ok) do
|
||||
{_, res, _} =
|
||||
Ecto.Migrator.with_repo(Pleroma.Repo, fn repo ->
|
||||
migrate =
|
||||
from(o in "columns",
|
||||
where: o.table_name == "objects",
|
||||
where: o.column_name == "fts_content"
|
||||
)
|
||||
|> repo.exists?(prefix: "information_schema")
|
||||
|
||||
setting = Pleroma.Config.get([:database, :rum_enabled], false)
|
||||
|
||||
do_check_rum!(setting, migrate)
|
||||
end)
|
||||
|
||||
res
|
||||
end
|
||||
|
||||
defp check_rum!(result), do: result
|
||||
|
||||
defp do_check_rum!(setting, migrate) do
|
||||
case {setting, migrate} do
|
||||
{true, false} ->
|
||||
Logger.error(
|
||||
"Use `RUM` index is enabled, but were not applied migrations for it.\nIf you want to start Pleroma anyway, set\nconfig :pleroma, :database, rum_enabled: false\nOtherwise apply the following migrations:\n`mix ecto.migrate --migrations-path priv/repo/optional_migrations/rum_indexing/`"
|
||||
)
|
||||
|
||||
{:error, "Unapplied RUM Migrations detected"}
|
||||
|
||||
{false, true} ->
|
||||
Logger.error(
|
||||
"Detected applied migrations to use `RUM` index, but `RUM` isn't enable in settings.\nIf you want to use `RUM`, set\nconfig :pleroma, :database, rum_enabled: true\nOtherwise roll `RUM` migrations back.\n`mix ecto.rollback --migrations-path priv/repo/optional_migrations/rum_indexing/`"
|
||||
)
|
||||
|
||||
{:error, "RUM Migrations detected"}
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -156,7 +156,6 @@ defmodule Pleroma.ConfigDB do
|
|||
{:quack, :meta},
|
||||
{:mime, :types},
|
||||
{:cors_plug, [:max_age, :methods, :expose, :headers]},
|
||||
{:auto_linker, :opts},
|
||||
{:swarm, :node_blacklist},
|
||||
{:logger, :backends}
|
||||
]
|
||||
|
|
@ -167,7 +166,9 @@ defmodule Pleroma.ConfigDB do
|
|||
end)
|
||||
end
|
||||
|
||||
@spec delete(map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()}
|
||||
@spec delete(ConfigDB.t() | map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()}
|
||||
def delete(%ConfigDB{} = config), do: Repo.delete(config)
|
||||
|
||||
def delete(params) do
|
||||
search_opts = Map.delete(params, :subkeys)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,9 +3,23 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Config.DeprecationWarnings do
|
||||
alias Pleroma.Config
|
||||
|
||||
require Logger
|
||||
alias Pleroma.Config
|
||||
|
||||
@type config_namespace() :: [atom()]
|
||||
@type config_map() :: {config_namespace(), config_namespace(), String.t()}
|
||||
|
||||
@mrf_config_map [
|
||||
{[:instance, :rewrite_policy], [:mrf, :policies],
|
||||
"\n* `config :pleroma, :instance, rewrite_policy` is now `config :pleroma, :mrf, policies`"},
|
||||
{[:instance, :mrf_transparency], [:mrf, :transparency],
|
||||
"\n* `config :pleroma, :instance, mrf_transparency` is now `config :pleroma, :mrf, transparency`"},
|
||||
{[:instance, :mrf_transparency_exclusions], [:mrf, :transparency_exclusions],
|
||||
"\n* `config :pleroma, :instance, mrf_transparency_exclusions` is now `config :pleroma, :mrf, transparency_exclusions`"}
|
||||
]
|
||||
|
||||
def check_hellthread_threshold do
|
||||
if Config.get([:mrf_hellthread, :threshold]) do
|
||||
Logger.warn("""
|
||||
|
|
@ -39,5 +53,66 @@ defmodule Pleroma.Config.DeprecationWarnings do
|
|||
def warn do
|
||||
check_hellthread_threshold()
|
||||
mrf_user_allowlist()
|
||||
check_old_mrf_config()
|
||||
check_media_proxy_whitelist_config()
|
||||
check_welcome_message_config()
|
||||
end
|
||||
|
||||
def check_welcome_message_config do
|
||||
instance_config = Pleroma.Config.get([:instance])
|
||||
|
||||
use_old_config =
|
||||
Keyword.has_key?(instance_config, :welcome_user_nickname) or
|
||||
Keyword.has_key?(instance_config, :welcome_message)
|
||||
|
||||
if use_old_config do
|
||||
Logger.error("""
|
||||
!!!DEPRECATION WARNING!!!
|
||||
Your config is using the old namespace for Welcome messages configuration. You need to change to the new namespace:
|
||||
\n* `config :pleroma, :instance, welcome_user_nickname` is now `config :pleroma, :welcome, :direct_message, :sender_nickname`
|
||||
\n* `config :pleroma, :instance, welcome_message` is now `config :pleroma, :welcome, :direct_message, :message`
|
||||
""")
|
||||
end
|
||||
end
|
||||
|
||||
def check_old_mrf_config do
|
||||
warning_preface = """
|
||||
!!!DEPRECATION WARNING!!!
|
||||
Your config is using old namespaces for MRF configuration. They should work for now, but you are advised to change to new namespaces to prevent possible issues later:
|
||||
"""
|
||||
|
||||
move_namespace_and_warn(@mrf_config_map, warning_preface)
|
||||
end
|
||||
|
||||
@spec move_namespace_and_warn([config_map()], String.t()) :: :ok | nil
|
||||
def move_namespace_and_warn(config_map, warning_preface) do
|
||||
warning =
|
||||
Enum.reduce(config_map, "", fn
|
||||
{old, new, err_msg}, acc ->
|
||||
old_config = Config.get(old)
|
||||
|
||||
if old_config do
|
||||
Config.put(new, old_config)
|
||||
acc <> err_msg
|
||||
else
|
||||
acc
|
||||
end
|
||||
end)
|
||||
|
||||
if warning != "" do
|
||||
Logger.warn(warning_preface <> warning)
|
||||
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
|
||||
|
|
|
|||
17
lib/pleroma/config/helpers.ex
Normal file
17
lib/pleroma/config/helpers.ex
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Config.Helpers do
|
||||
alias Pleroma.Config
|
||||
|
||||
def instance_name, do: Config.get([:instance, :name])
|
||||
|
||||
defp instance_notify_email do
|
||||
Config.get([:instance, :notify_email]) || Config.get([:instance, :email])
|
||||
end
|
||||
|
||||
def sender do
|
||||
{instance_name(), instance_notify_email()}
|
||||
end
|
||||
end
|
||||
|
|
@ -12,6 +12,11 @@ defmodule Pleroma.Config.Loader do
|
|||
:swarm
|
||||
]
|
||||
|
||||
@reject_groups [
|
||||
:postgrex,
|
||||
:tesla
|
||||
]
|
||||
|
||||
if Code.ensure_loaded?(Config.Reader) do
|
||||
@reader Config.Reader
|
||||
|
||||
|
|
@ -47,7 +52,8 @@ defmodule Pleroma.Config.Loader do
|
|||
@spec filter_group(atom(), keyword()) :: keyword()
|
||||
def filter_group(group, configs) do
|
||||
Enum.reject(configs[group], fn {key, _v} ->
|
||||
key in @reject_keys or (group == :phoenix and key == :serve_endpoints) or group == :postgrex
|
||||
key in @reject_keys or group in @reject_groups or
|
||||
(group == :phoenix and key == :serve_endpoints)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -31,8 +31,8 @@ defmodule Pleroma.Config.TransferTask do
|
|||
{:pleroma, :gopher, [:enabled]}
|
||||
]
|
||||
|
||||
def start_link(_) do
|
||||
load_and_update_env()
|
||||
def start_link(restart_pleroma? \\ true) do
|
||||
load_and_update_env([], restart_pleroma?)
|
||||
if Config.get(:env) == :test, do: Ecto.Adapters.SQL.Sandbox.checkin(Repo)
|
||||
:ignore
|
||||
end
|
||||
|
|
|
|||
|
|
@ -10,32 +10,70 @@ defmodule Pleroma.CounterCache do
|
|||
import Ecto.Query
|
||||
|
||||
schema "counter_cache" do
|
||||
field(:name, :string)
|
||||
field(:count, :integer)
|
||||
field(:instance, :string)
|
||||
field(:public, :integer)
|
||||
field(:unlisted, :integer)
|
||||
field(:private, :integer)
|
||||
field(:direct, :integer)
|
||||
end
|
||||
|
||||
def changeset(struct, params) do
|
||||
struct
|
||||
|> cast(params, [:name, :count])
|
||||
|> validate_required([:name])
|
||||
|> unique_constraint(:name)
|
||||
|> cast(params, [:instance, :public, :unlisted, :private, :direct])
|
||||
|> validate_required([:instance])
|
||||
|> unique_constraint(:instance)
|
||||
end
|
||||
|
||||
def get_as_map(names) when is_list(names) do
|
||||
def get_by_instance(instance) do
|
||||
CounterCache
|
||||
|> where([cc], cc.name in ^names)
|
||||
|> Repo.all()
|
||||
|> Enum.group_by(& &1.name, & &1.count)
|
||||
|> Map.new(fn {k, v} -> {k, hd(v)} end)
|
||||
|> select([c], %{
|
||||
"public" => c.public,
|
||||
"unlisted" => c.unlisted,
|
||||
"private" => c.private,
|
||||
"direct" => c.direct
|
||||
})
|
||||
|> where([c], c.instance == ^instance)
|
||||
|> Repo.one()
|
||||
|> case do
|
||||
nil -> %{"public" => 0, "unlisted" => 0, "private" => 0, "direct" => 0}
|
||||
val -> val
|
||||
end
|
||||
end
|
||||
|
||||
def set(name, count) do
|
||||
def get_sum do
|
||||
CounterCache
|
||||
|> select([c], %{
|
||||
"public" => type(sum(c.public), :integer),
|
||||
"unlisted" => type(sum(c.unlisted), :integer),
|
||||
"private" => type(sum(c.private), :integer),
|
||||
"direct" => type(sum(c.direct), :integer)
|
||||
})
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
def set(instance, values) do
|
||||
params =
|
||||
Enum.reduce(
|
||||
["public", "private", "unlisted", "direct"],
|
||||
%{"instance" => instance},
|
||||
fn param, acc ->
|
||||
Map.put_new(acc, param, Map.get(values, param, 0))
|
||||
end
|
||||
)
|
||||
|
||||
%CounterCache{}
|
||||
|> changeset(%{"name" => name, "count" => count})
|
||||
|> changeset(params)
|
||||
|> Repo.insert(
|
||||
on_conflict: [set: [count: count]],
|
||||
on_conflict: [
|
||||
set: [
|
||||
public: params["public"],
|
||||
private: params["private"],
|
||||
unlisted: params["unlisted"],
|
||||
direct: params["direct"]
|
||||
]
|
||||
],
|
||||
returning: true,
|
||||
conflict_target: :name
|
||||
conflict_target: :instance
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,16 +6,21 @@ defmodule Pleroma.Docs.Generator do
|
|||
implementation.process(descriptions)
|
||||
end
|
||||
|
||||
@spec list_modules_in_dir(String.t(), String.t()) :: [module()]
|
||||
def list_modules_in_dir(dir, start) do
|
||||
with {:ok, files} <- File.ls(dir) do
|
||||
files
|
||||
|> Enum.filter(&String.ends_with?(&1, ".ex"))
|
||||
|> Enum.map(fn filename ->
|
||||
module = filename |> String.trim_trailing(".ex") |> Macro.camelize()
|
||||
String.to_atom(start <> module)
|
||||
end)
|
||||
end
|
||||
@spec list_behaviour_implementations(behaviour :: module()) :: [module()]
|
||||
def list_behaviour_implementations(behaviour) do
|
||||
:code.all_loaded()
|
||||
|> Enum.filter(fn {module, _} ->
|
||||
# This shouldn't be needed as all modules are expected to have module_info/1,
|
||||
# but in test enviroments some transient modules `:elixir_compiler_XX`
|
||||
# are loaded for some reason (where XX is a random integer).
|
||||
if function_exported?(module, :module_info, 1) do
|
||||
module.module_info(:attributes)
|
||||
|> Keyword.get_values(:behaviour)
|
||||
|> List.flatten()
|
||||
|> Enum.member?(behaviour)
|
||||
end
|
||||
end)
|
||||
|> Enum.map(fn {module, _} -> module end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
@ -87,6 +92,12 @@ defmodule Pleroma.Docs.Generator do
|
|||
else: string
|
||||
end
|
||||
|
||||
defp format_suggestions({:list_behaviour_implementations, behaviour}) do
|
||||
behaviour
|
||||
|> list_behaviour_implementations()
|
||||
|> format_suggestions()
|
||||
end
|
||||
|
||||
defp format_suggestions([]), do: []
|
||||
|
||||
defp format_suggestions([suggestion | tail]) do
|
||||
|
|
|
|||
|
|
@ -1,5 +1,19 @@
|
|||
defmodule Pleroma.Docs.JSON do
|
||||
@behaviour Pleroma.Docs.Generator
|
||||
@external_resource "config/description.exs"
|
||||
@raw_config Pleroma.Config.Loader.read("config/description.exs")
|
||||
@raw_descriptions @raw_config[:pleroma][:config_description]
|
||||
@term __MODULE__.Compiled
|
||||
|
||||
@spec compile :: :ok
|
||||
def compile do
|
||||
:persistent_term.put(@term, Pleroma.Docs.Generator.convert_to_strings(@raw_descriptions))
|
||||
end
|
||||
|
||||
@spec compiled_descriptions :: Map.t()
|
||||
def compiled_descriptions do
|
||||
:persistent_term.get(@term)
|
||||
end
|
||||
|
||||
@spec process(keyword()) :: {:ok, String.t()}
|
||||
def process(descriptions) do
|
||||
|
|
@ -13,11 +27,4 @@ defmodule Pleroma.Docs.JSON do
|
|||
{:ok, path}
|
||||
end
|
||||
end
|
||||
|
||||
def compile do
|
||||
with config <- Pleroma.Config.Loader.read("config/description.exs") do
|
||||
config[:pleroma][:config_description]
|
||||
|> Pleroma.Docs.Generator.convert_to_strings()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -68,6 +68,11 @@ defmodule Pleroma.Docs.Markdown do
|
|||
IO.write(file, " #{list_mark}`#{inspect(suggestion)}`\n")
|
||||
end
|
||||
|
||||
defp print_suggestions(file, {:list_behaviour_implementations, behaviour}) do
|
||||
suggestions = Pleroma.Docs.Generator.list_behaviour_implementations(behaviour)
|
||||
print_suggestions(file, suggestions)
|
||||
end
|
||||
|
||||
defp print_suggestions(_file, nil), do: nil
|
||||
|
||||
defp print_suggestions(_file, ""), do: nil
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ defmodule Pleroma.Emails.AdminEmail do
|
|||
alias Pleroma.Config
|
||||
alias Pleroma.Web.Router.Helpers
|
||||
|
||||
defp instance_config, do: Pleroma.Config.get(:instance)
|
||||
defp instance_config, do: Config.get(:instance)
|
||||
defp instance_name, do: instance_config()[:name]
|
||||
|
||||
defp instance_notify_email do
|
||||
|
|
@ -72,6 +72,8 @@ defmodule Pleroma.Emails.AdminEmail do
|
|||
<p>Reported Account: <a href="#{user_url(account)}">#{account.nickname}</a></p>
|
||||
#{comment_html}
|
||||
#{statuses_html}
|
||||
<p>
|
||||
<a href="#{Pleroma.Web.base_url()}/pleroma/admin/#/reports/index">View Reports in AdminFE</a>
|
||||
"""
|
||||
|
||||
new()
|
||||
|
|
|
|||
|
|
@ -12,17 +12,22 @@ defmodule Pleroma.Emails.UserEmail do
|
|||
alias Pleroma.Web.Endpoint
|
||||
alias Pleroma.Web.Router
|
||||
|
||||
defp instance_name, do: Config.get([:instance, :name])
|
||||
|
||||
defp sender do
|
||||
email = Config.get([:instance, :notify_email]) || Config.get([:instance, :email])
|
||||
{instance_name(), email}
|
||||
end
|
||||
import Pleroma.Config.Helpers, only: [instance_name: 0, sender: 0]
|
||||
|
||||
defp recipient(email, nil), do: email
|
||||
defp recipient(email, name), do: {name, email}
|
||||
defp recipient(%User{} = user), do: recipient(user.email, user.name)
|
||||
|
||||
@spec welcome(User.t(), map()) :: Swoosh.Email.t()
|
||||
def welcome(user, opts \\ %{}) do
|
||||
new()
|
||||
|> to(recipient(user))
|
||||
|> from(Map.get(opts, :sender, sender()))
|
||||
|> subject(Map.get(opts, :subject, "Welcome to #{instance_name()}!"))
|
||||
|> html_body(Map.get(opts, :html, "Welcome to #{instance_name()}!"))
|
||||
|> text_body(Map.get(opts, :text, "Welcome to #{instance_name()}!"))
|
||||
end
|
||||
|
||||
def password_reset_email(user, token) when is_binary(token) do
|
||||
password_reset_url = Router.Helpers.reset_password_url(Endpoint, :reset, token)
|
||||
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ defmodule Pleroma.Emoji.Loader do
|
|||
if File.exists?(emoji_txt) do
|
||||
load_from_file(emoji_txt, emoji_groups)
|
||||
else
|
||||
extensions = Pleroma.Config.get([:emoji, :pack_extensions])
|
||||
extensions = Config.get([:emoji, :pack_extensions])
|
||||
|
||||
Logger.info(
|
||||
"No emoji.txt found for pack \"#{pack_name}\", assuming all #{
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ defmodule Pleroma.Emoji.Pack do
|
|||
shortcodes =
|
||||
pack.files
|
||||
|> Map.keys()
|
||||
|> Enum.sort()
|
||||
|> paginate(opts[:page], opts[:page_size])
|
||||
|
||||
pack = Map.put(pack, :files, Map.take(pack.files, shortcodes))
|
||||
|
|
|
|||
|
|
@ -34,10 +34,18 @@ defmodule Pleroma.Filter do
|
|||
Repo.one(query)
|
||||
end
|
||||
|
||||
def get_filters(%User{id: user_id} = _user) do
|
||||
def get_active(query) do
|
||||
from(f in query, where: is_nil(f.expires_at) or f.expires_at > ^NaiveDateTime.utc_now())
|
||||
end
|
||||
|
||||
def get_irreversible(query) do
|
||||
from(f in query, where: f.hide)
|
||||
end
|
||||
|
||||
def get_filters(query \\ __MODULE__, %User{id: user_id}) do
|
||||
query =
|
||||
from(
|
||||
f in Pleroma.Filter,
|
||||
f in query,
|
||||
where: f.user_id == ^user_id,
|
||||
order_by: [desc: :id]
|
||||
)
|
||||
|
|
@ -95,4 +103,34 @@ defmodule Pleroma.Filter do
|
|||
|> validate_required([:phrase, :context])
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
def compose_regex(user_or_filters, format \\ :postgres)
|
||||
|
||||
def compose_regex(%User{} = user, format) do
|
||||
__MODULE__
|
||||
|> get_active()
|
||||
|> get_irreversible()
|
||||
|> get_filters(user)
|
||||
|> compose_regex(format)
|
||||
end
|
||||
|
||||
def compose_regex([_ | _] = filters, format) do
|
||||
phrases =
|
||||
filters
|
||||
|> Enum.map(& &1.phrase)
|
||||
|> Enum.join("|")
|
||||
|
||||
case format do
|
||||
:postgres ->
|
||||
"\\y(#{phrases})\\y"
|
||||
|
||||
:re ->
|
||||
~r/\b#{phrases}\b/i
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def compose_regex(_, _), do: nil
|
||||
end
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ defmodule Pleroma.FollowingRelationship do
|
|||
|> join(:inner, [r], f in assoc(r, :follower))
|
||||
|> where([r], r.state == ^:follow_pending)
|
||||
|> where([r], r.following_id == ^id)
|
||||
|> where([r, f], f.deactivated != true)
|
||||
|> select([r, f], f)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
|
|
|||
|
|
@ -10,11 +10,15 @@ defmodule Pleroma.Formatter do
|
|||
@link_regex ~r"((?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~%:/?#[\]@!\$&'\(\)\*\+,;=.]+)|[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+"ui
|
||||
@markdown_characters_regex ~r/(`|\*|_|{|}|[|]|\(|\)|#|\+|-|\.|!)/
|
||||
|
||||
@auto_linker_config hashtag: true,
|
||||
hashtag_handler: &Pleroma.Formatter.hashtag_handler/4,
|
||||
mention: true,
|
||||
mention_handler: &Pleroma.Formatter.mention_handler/4,
|
||||
scheme: true
|
||||
defp linkify_opts do
|
||||
Pleroma.Config.get(Pleroma.Formatter) ++
|
||||
[
|
||||
hashtag: true,
|
||||
hashtag_handler: &Pleroma.Formatter.hashtag_handler/4,
|
||||
mention: true,
|
||||
mention_handler: &Pleroma.Formatter.mention_handler/4
|
||||
]
|
||||
end
|
||||
|
||||
def escape_mention_handler("@" <> nickname = mention, buffer, _, _) do
|
||||
case User.get_cached_by_nickname(nickname) do
|
||||
|
|
@ -80,19 +84,19 @@ defmodule Pleroma.Formatter do
|
|||
@spec linkify(String.t(), keyword()) ::
|
||||
{String.t(), [{String.t(), User.t()}], [{String.t(), String.t()}]}
|
||||
def linkify(text, options \\ []) do
|
||||
options = options ++ @auto_linker_config
|
||||
options = linkify_opts() ++ options
|
||||
|
||||
if options[:safe_mention] && Regex.named_captures(@safe_mention_regex, text) do
|
||||
%{"mentions" => mentions, "rest" => rest} = Regex.named_captures(@safe_mention_regex, text)
|
||||
acc = %{mentions: MapSet.new(), tags: MapSet.new()}
|
||||
|
||||
{text_mentions, %{mentions: mentions}} = AutoLinker.link_map(mentions, acc, options)
|
||||
{text_rest, %{tags: tags}} = AutoLinker.link_map(rest, acc, options)
|
||||
{text_mentions, %{mentions: mentions}} = Linkify.link_map(mentions, acc, options)
|
||||
{text_rest, %{tags: tags}} = Linkify.link_map(rest, acc, options)
|
||||
|
||||
{text_mentions <> text_rest, MapSet.to_list(mentions), MapSet.to_list(tags)}
|
||||
else
|
||||
acc = %{mentions: MapSet.new(), tags: MapSet.new()}
|
||||
{text, %{mentions: mentions, tags: tags}} = AutoLinker.link_map(text, acc, options)
|
||||
{text, %{mentions: mentions, tags: tags}} = Linkify.link_map(text, acc, options)
|
||||
|
||||
{text, MapSet.to_list(mentions), MapSet.to_list(tags)}
|
||||
end
|
||||
|
|
@ -111,9 +115,9 @@ defmodule Pleroma.Formatter do
|
|||
|
||||
if options[:safe_mention] && Regex.named_captures(@safe_mention_regex, text) do
|
||||
%{"mentions" => mentions, "rest" => rest} = Regex.named_captures(@safe_mention_regex, text)
|
||||
AutoLinker.link(mentions, options) <> AutoLinker.link(rest, options)
|
||||
Linkify.link(mentions, options) <> Linkify.link(rest, options)
|
||||
else
|
||||
AutoLinker.link(text, options)
|
||||
Linkify.link(text, options)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -96,16 +96,18 @@ defmodule Pleroma.Gopher.Server.ProtocolHandler do
|
|||
|
||||
def response("/main/public") do
|
||||
posts =
|
||||
ActivityPub.fetch_public_activities(%{"type" => ["Create"], "local_only" => true})
|
||||
|> render_activities
|
||||
%{type: ["Create"], local_only: true}
|
||||
|> ActivityPub.fetch_public_activities()
|
||||
|> render_activities()
|
||||
|
||||
info("Welcome to the Public Timeline!") <> posts <> ".\r\n"
|
||||
end
|
||||
|
||||
def response("/main/all") do
|
||||
posts =
|
||||
ActivityPub.fetch_public_activities(%{"type" => ["Create"]})
|
||||
|> render_activities
|
||||
%{type: ["Create"]}
|
||||
|> ActivityPub.fetch_public_activities()
|
||||
|> render_activities()
|
||||
|
||||
info("Welcome to the Federated Timeline!") <> posts <> ".\r\n"
|
||||
end
|
||||
|
|
@ -130,13 +132,14 @@ defmodule Pleroma.Gopher.Server.ProtocolHandler do
|
|||
def response("/users/" <> nickname) do
|
||||
with %User{} = user <- User.get_cached_by_nickname(nickname) do
|
||||
params = %{
|
||||
"type" => ["Create"],
|
||||
"actor_id" => user.ap_id
|
||||
type: ["Create"],
|
||||
actor_id: user.ap_id
|
||||
}
|
||||
|
||||
activities =
|
||||
ActivityPub.fetch_public_activities(params)
|
||||
|> render_activities
|
||||
params
|
||||
|> ActivityPub.fetch_public_activities()
|
||||
|> render_activities()
|
||||
|
||||
info("Posts by #{user.nickname}") <> activities <> ".\r\n"
|
||||
else
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@ defmodule Pleroma.Gun.API do
|
|||
:tls_opts,
|
||||
:tcp_opts,
|
||||
:socks_opts,
|
||||
:ws_opts
|
||||
:ws_opts,
|
||||
:supervise
|
||||
]
|
||||
|
||||
@impl Gun
|
||||
|
|
|
|||
|
|
@ -3,85 +3,33 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Gun.Conn do
|
||||
@moduledoc """
|
||||
Struct for gun connection data
|
||||
"""
|
||||
alias Pleroma.Gun
|
||||
alias Pleroma.Pool.Connections
|
||||
|
||||
require Logger
|
||||
|
||||
@type gun_state :: :up | :down
|
||||
@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
|
||||
def open(%URI{} = uri, opts) do
|
||||
pool_opts = Pleroma.Config.get([:connections_pool], [])
|
||||
|
||||
opts =
|
||||
opts
|
||||
|> 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(:supervise, false)
|
||||
|> 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)
|
||||
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
|
||||
do_open(uri, opts)
|
||||
end
|
||||
|
||||
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 = [
|
||||
verify: :verify_peer,
|
||||
cacertfile: CAStore.file_path(),
|
||||
depth: 20,
|
||||
reuse_sessions: false,
|
||||
verify_fun:
|
||||
{&:ssl_verify_hostname.verify_fun/3,
|
||||
[check_hostname: Pleroma.HTTP.Connection.format_host(host)]}
|
||||
log_level: :warning,
|
||||
customize_hostname_check: [match_fun: :public_key.pkix_verify_hostname_match_fun(:https)]
|
||||
]
|
||||
|
||||
tls_opts =
|
||||
|
|
@ -105,7 +53,7 @@ defmodule Pleroma.Gun.Conn do
|
|||
{:ok, _} <- Gun.await_up(conn, opts[:await_up_timeout]),
|
||||
stream <- Gun.connect(conn, connect_opts),
|
||||
{:response, :fin, 200, _} <- Gun.await(conn, stream) do
|
||||
conn
|
||||
{:ok, conn}
|
||||
else
|
||||
error ->
|
||||
Logger.warn(
|
||||
|
|
@ -141,7 +89,7 @@ defmodule Pleroma.Gun.Conn do
|
|||
|
||||
with {:ok, conn} <- Gun.open(proxy_host, proxy_port, opts),
|
||||
{:ok, _} <- Gun.await_up(conn, opts[:await_up_timeout]) do
|
||||
conn
|
||||
{:ok, conn}
|
||||
else
|
||||
error ->
|
||||
Logger.warn(
|
||||
|
|
@ -155,11 +103,11 @@ defmodule Pleroma.Gun.Conn do
|
|||
end
|
||||
|
||||
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),
|
||||
{:ok, _} <- Gun.await_up(conn, opts[:await_up_timeout]) do
|
||||
conn
|
||||
{:ok, conn}
|
||||
else
|
||||
error ->
|
||||
Logger.warn(
|
||||
|
|
@ -171,7 +119,7 @@ defmodule Pleroma.Gun.Conn do
|
|||
end
|
||||
|
||||
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}
|
||||
end
|
||||
|
||||
|
|
@ -181,17 +129,6 @@ defmodule Pleroma.Gun.Conn do
|
|||
|
||||
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
|
||||
"#{scheme}://#{host}#{path}"
|
||||
end
|
||||
|
|
|
|||
79
lib/pleroma/gun/connection_pool.ex
Normal file
79
lib/pleroma/gun/connection_pool.ex
Normal 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
|
||||
85
lib/pleroma/gun/connection_pool/reclaimer.ex
Normal file
85
lib/pleroma/gun/connection_pool/reclaimer.ex
Normal 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
|
||||
127
lib/pleroma/gun/connection_pool/worker.ex
Normal file
127
lib/pleroma/gun/connection_pool/worker.ex
Normal 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
|
||||
45
lib/pleroma/gun/connection_pool/worker_supervisor.ex
Normal file
45
lib/pleroma/gun/connection_pool/worker_supervisor.ex
Normal 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
|
||||
|
|
@ -109,7 +109,7 @@ defmodule Pleroma.HTML do
|
|||
result =
|
||||
content
|
||||
|> Floki.parse_fragment!()
|
||||
|> Floki.filter_out("a.mention,a.hashtag,a[rel~=\"tag\"]")
|
||||
|> Floki.filter_out("a.mention,a.hashtag,a.attachment,a[rel~=\"tag\"]")
|
||||
|> Floki.attribute("a", "href")
|
||||
|> Enum.at(0)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,32 +3,30 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
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 ::
|
||||
{Connection.host(), pos_integer()}
|
||||
| {Connection.proxy_type(), Connection.host(), pos_integer()}
|
||||
|
||||
@callback options(keyword(), URI.t()) :: keyword()
|
||||
@callback after_request(keyword()) :: :ok
|
||||
|
||||
@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
|
||||
@callback get_conn(URI.t(), keyword()) :: {:ok, term()} | {:error, term()}
|
||||
|
||||
@spec format_proxy(String.t() | tuple() | nil) :: proxy() | nil
|
||||
def format_proxy(nil), do: nil
|
||||
|
||||
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, type, host, port} -> {type, host, port}
|
||||
_ -> nil
|
||||
|
|
@ -38,4 +36,105 @@ defmodule Pleroma.HTTP.AdapterHelper do
|
|||
@spec maybe_add_proxy(keyword(), proxy() | nil) :: keyword()
|
||||
def maybe_add_proxy(opts, nil), do: opts
|
||||
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
|
||||
|
|
|
|||
14
lib/pleroma/http/adapter_helper/default.ex
Normal file
14
lib/pleroma/http/adapter_helper/default.ex
Normal 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
|
||||
|
|
@ -5,8 +5,8 @@
|
|||
defmodule Pleroma.HTTP.AdapterHelper.Gun do
|
||||
@behaviour Pleroma.HTTP.AdapterHelper
|
||||
|
||||
alias Pleroma.Gun.ConnectionPool
|
||||
alias Pleroma.HTTP.AdapterHelper
|
||||
alias Pleroma.Pool.Connections
|
||||
|
||||
require Logger
|
||||
|
||||
|
|
@ -14,7 +14,7 @@ defmodule Pleroma.HTTP.AdapterHelper.Gun do
|
|||
connect_timeout: 5_000,
|
||||
domain_lookup_timeout: 5_000,
|
||||
tls_handshake_timeout: 5_000,
|
||||
retry: 1,
|
||||
retry: 0,
|
||||
retry_timeout: 1000,
|
||||
await_up_timeout: 5_000
|
||||
]
|
||||
|
|
@ -31,16 +31,7 @@ defmodule Pleroma.HTTP.AdapterHelper.Gun do
|
|||
|> Keyword.merge(config_opts)
|
||||
|> add_scheme_opts(uri)
|
||||
|> AdapterHelper.maybe_add_proxy(proxy)
|
||||
|> maybe_get_conn(uri, 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
|
||||
|> Keyword.merge(incoming_opts)
|
||||
end
|
||||
|
||||
defp add_scheme_opts(opts, %{scheme: "http"}), do: opts
|
||||
|
|
@ -48,30 +39,40 @@ defmodule Pleroma.HTTP.AdapterHelper.Gun do
|
|||
defp add_scheme_opts(opts, %{scheme: "https"}) do
|
||||
opts
|
||||
|> Keyword.put(:certificates_verification, true)
|
||||
|> Keyword.put(:tls_opts, log_level: :warning)
|
||||
end
|
||||
|
||||
defp maybe_get_conn(adapter_opts, uri, incoming_opts) do
|
||||
{receive_conn?, opts} =
|
||||
adapter_opts
|
||||
|> Keyword.merge(incoming_opts)
|
||||
|> Keyword.pop(:receive_conn, true)
|
||||
|
||||
if Connections.alive?(:gun_connections) and receive_conn? do
|
||||
checkin_conn(uri, opts)
|
||||
else
|
||||
opts
|
||||
@spec get_conn(URI.t(), keyword()) :: {:ok, keyword()} | {:error, atom()}
|
||||
def get_conn(uri, opts) do
|
||||
case ConnectionPool.get_conn(uri, opts) do
|
||||
{:ok, conn_pid} -> {:ok, Keyword.merge(opts, conn: conn_pid, close_conn: false)}
|
||||
err -> err
|
||||
end
|
||||
end
|
||||
|
||||
defp checkin_conn(uri, opts) do
|
||||
case Connections.checkin(uri, :gun_connections) do
|
||||
nil ->
|
||||
Task.start(Pleroma.Gun.Conn, :open, [uri, :gun_connections, opts])
|
||||
opts
|
||||
@prefix Pleroma.Gun.ConnectionPool
|
||||
def limiter_setup do
|
||||
wait = Pleroma.Config.get([:connections_pool, :connection_acquisition_wait])
|
||||
retries = Pleroma.Config.get([:connections_pool, :connection_acquisition_retries])
|
||||
|
||||
conn when is_pid(conn) ->
|
||||
Keyword.merge(opts, conn: conn, close_conn: false)
|
||||
end
|
||||
:pools
|
||||
|> 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)
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -24,5 +24,6 @@ defmodule Pleroma.HTTP.AdapterHelper.Hackney do
|
|||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
22
lib/pleroma/http/ex_aws.ex
Normal file
22
lib/pleroma/http/ex_aws.ex
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.HTTP.ExAws do
|
||||
@moduledoc false
|
||||
|
||||
@behaviour ExAws.Request.HttpClient
|
||||
|
||||
alias Pleroma.HTTP
|
||||
|
||||
@impl true
|
||||
def request(method, url, body \\ "", headers \\ [], http_opts \\ []) do
|
||||
case HTTP.request(method, url, body, headers, http_opts) do
|
||||
{:ok, env} ->
|
||||
{:ok, %{status_code: env.status, headers: env.headers, body: env.body}}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, %{reason: reason}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -7,7 +7,7 @@ defmodule Pleroma.HTTP do
|
|||
Wrapper for `Tesla.request/2`.
|
||||
"""
|
||||
|
||||
alias Pleroma.HTTP.Connection
|
||||
alias Pleroma.HTTP.AdapterHelper
|
||||
alias Pleroma.HTTP.Request
|
||||
alias Pleroma.HTTP.RequestBuilder, as: Builder
|
||||
alias Tesla.Client
|
||||
|
|
@ -16,6 +16,7 @@ defmodule Pleroma.HTTP do
|
|||
require Logger
|
||||
|
||||
@type t :: __MODULE__
|
||||
@type method() :: :get | :post | :put | :delete | :head
|
||||
|
||||
@doc """
|
||||
Performs GET request.
|
||||
|
|
@ -28,6 +29,9 @@ defmodule Pleroma.HTTP do
|
|||
def get(nil, _, _), do: nil
|
||||
def get(url, headers, options), do: request(:get, url, "", headers, options)
|
||||
|
||||
@spec head(Request.url(), Request.headers(), keyword()) :: {:ok, Env.t()} | {:error, any()}
|
||||
def head(url, headers \\ [], options \\ []), do: request(:head, url, "", headers, options)
|
||||
|
||||
@doc """
|
||||
Performs POST request.
|
||||
|
||||
|
|
@ -42,7 +46,7 @@ defmodule Pleroma.HTTP do
|
|||
Builds and performs http request.
|
||||
|
||||
# Arguments:
|
||||
`method` - :get, :post, :put, :delete
|
||||
`method` - :get, :post, :put, :delete, :head
|
||||
`url` - full url
|
||||
`body` - request body
|
||||
`headers` - a keyworld list of headers, e.g. `[{"content-type", "text/plain"}]`
|
||||
|
|
@ -52,53 +56,34 @@ defmodule Pleroma.HTTP do
|
|||
`{:ok, %Tesla.Env{}}` or `{:error, error}`
|
||||
|
||||
"""
|
||||
@spec request(atom(), Request.url(), String.t(), Request.headers(), keyword()) ::
|
||||
@spec request(method(), Request.url(), String.t(), Request.headers(), keyword()) ::
|
||||
{:ok, Env.t()} | {:error, any()}
|
||||
def request(method, url, body, headers, options) when is_binary(url) do
|
||||
uri = URI.parse(url)
|
||||
adapter_opts = Connection.options(uri, options[:adapter] || [])
|
||||
options = put_in(options[:adapter], adapter_opts)
|
||||
params = options[:params] || []
|
||||
request = build_request(method, headers, options, url, body, params)
|
||||
adapter_opts = AdapterHelper.options(uri, options[:adapter] || [])
|
||||
|
||||
adapter = Application.get_env(:tesla, :adapter)
|
||||
client = Tesla.client([Tesla.Middleware.FollowRedirects], adapter)
|
||||
case AdapterHelper.get_conn(uri, adapter_opts) do
|
||||
{:ok, adapter_opts} ->
|
||||
options = put_in(options[:adapter], adapter_opts)
|
||||
params = options[:params] || []
|
||||
request = build_request(method, headers, options, url, body, params)
|
||||
|
||||
pid = Process.whereis(adapter_opts[:pool])
|
||||
adapter = Application.get_env(:tesla, :adapter)
|
||||
|
||||
pool_alive? =
|
||||
if adapter == Tesla.Adapter.Gun && pid do
|
||||
Process.alive?(pid)
|
||||
else
|
||||
false
|
||||
end
|
||||
client = Tesla.client(adapter_middlewares(adapter), adapter)
|
||||
|
||||
request_opts =
|
||||
adapter_opts
|
||||
|> Enum.into(%{})
|
||||
|> Map.put(:env, Pleroma.Config.get([:env]))
|
||||
|> Map.put(:pool_alive?, pool_alive?)
|
||||
maybe_limit(
|
||||
fn ->
|
||||
request(client, request)
|
||||
end,
|
||||
adapter,
|
||||
adapter_opts
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
@spec request(Client.t(), keyword()) :: {:ok, Env.t()} | {:error, any()}
|
||||
|
|
@ -114,4 +99,19 @@ defmodule Pleroma.HTTP do
|
|||
|> Builder.add_param(:query, :query, params)
|
||||
|> Builder.convert_to_keyword()
|
||||
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
|
||||
|
||||
defp adapter_middlewares(Tesla.Adapter.Gun) do
|
||||
[Pleroma.HTTP.Middleware.FollowRedirects]
|
||||
end
|
||||
|
||||
defp adapter_middlewares(_), do: []
|
||||
end
|
||||
|
|
|
|||
25
lib/pleroma/http/tzdata.ex
Normal file
25
lib/pleroma/http/tzdata.ex
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.HTTP.Tzdata do
|
||||
@moduledoc false
|
||||
|
||||
@behaviour Tzdata.HTTPClient
|
||||
|
||||
alias Pleroma.HTTP
|
||||
|
||||
@impl true
|
||||
def get(url, headers, options) do
|
||||
with {:ok, %Tesla.Env{} = env} <- HTTP.get(url, headers, options) do
|
||||
{:ok, {env.status, env.headers, env.body}}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def head(url, headers, options) do
|
||||
with {:ok, %Tesla.Env{} = env} <- HTTP.head(url, headers, options) do
|
||||
{:ok, {env.status, env.headers}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -17,6 +17,8 @@ defmodule Pleroma.Instances.Instance do
|
|||
schema "instances" do
|
||||
field(:host, :string)
|
||||
field(:unreachable_since, :naive_datetime_usec)
|
||||
field(:favicon, :string)
|
||||
field(:favicon_updated_at, :naive_datetime)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
|
@ -25,7 +27,7 @@ defmodule Pleroma.Instances.Instance do
|
|||
|
||||
def changeset(struct, params \\ %{}) do
|
||||
struct
|
||||
|> cast(params, [:host, :unreachable_since])
|
||||
|> cast(params, [:host, :unreachable_since, :favicon, :favicon_updated_at])
|
||||
|> validate_required([:host])
|
||||
|> unique_constraint(:host)
|
||||
end
|
||||
|
|
@ -120,4 +122,48 @@ defmodule Pleroma.Instances.Instance do
|
|||
end
|
||||
|
||||
defp parse_datetime(datetime), do: datetime
|
||||
|
||||
def get_or_update_favicon(%URI{host: host} = instance_uri) do
|
||||
existing_record = Repo.get_by(Instance, %{host: host})
|
||||
now = NaiveDateTime.utc_now()
|
||||
|
||||
if existing_record && existing_record.favicon_updated_at &&
|
||||
NaiveDateTime.diff(now, existing_record.favicon_updated_at) < 86_400 do
|
||||
existing_record.favicon
|
||||
else
|
||||
favicon = scrape_favicon(instance_uri)
|
||||
|
||||
if existing_record do
|
||||
existing_record
|
||||
|> changeset(%{favicon: favicon, favicon_updated_at: now})
|
||||
|> Repo.update()
|
||||
else
|
||||
%Instance{}
|
||||
|> changeset(%{host: host, favicon: favicon, favicon_updated_at: now})
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
favicon
|
||||
end
|
||||
end
|
||||
|
||||
defp scrape_favicon(%URI{} = instance_uri) do
|
||||
try do
|
||||
with {:ok, %Tesla.Env{body: html}} <-
|
||||
Pleroma.HTTP.get(to_string(instance_uri), [{:Accept, "text/html"}]),
|
||||
favicon_rel <-
|
||||
html
|
||||
|> Floki.parse_document!()
|
||||
|> Floki.attribute("link[rel=icon]", "href")
|
||||
|> List.first(),
|
||||
favicon <- URI.merge(instance_uri, favicon_rel) |> to_string(),
|
||||
true <- is_binary(favicon) do
|
||||
favicon
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
rescue
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.MigrationHelper.NotificationBackfill do
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
|
|
@ -25,18 +24,27 @@ defmodule Pleroma.MigrationHelper.NotificationBackfill do
|
|||
|> type_from_activity()
|
||||
|
||||
notification
|
||||
|> Notification.changeset(%{type: type})
|
||||
|> Ecto.Changeset.change(%{type: type})
|
||||
|> Repo.update()
|
||||
end)
|
||||
end
|
||||
|
||||
defp get_by_ap_id(ap_id) do
|
||||
q =
|
||||
from(u in User,
|
||||
select: u.id
|
||||
)
|
||||
|
||||
Repo.get_by(q, ap_id: ap_id)
|
||||
end
|
||||
|
||||
# This is copied over from Notifications to keep this stable.
|
||||
defp type_from_activity(%{data: %{"type" => type}} = activity) do
|
||||
case type do
|
||||
"Follow" ->
|
||||
accepted_function = fn activity ->
|
||||
with %User{} = follower <- User.get_by_ap_id(activity.data["actor"]),
|
||||
%User{} = followed <- User.get_by_ap_id(activity.data["object"]) do
|
||||
with %User{} = follower <- get_by_ap_id(activity.data["actor"]),
|
||||
%User{} = followed <- get_by_ap_id(activity.data["object"]) do
|
||||
Pleroma.FollowingRelationship.following?(follower, followed)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ defmodule Pleroma.Notification do
|
|||
|> preload([n, a, o], activity: {a, object: o})
|
||||
|> exclude_notification_muted(user, exclude_notification_muted_opts)
|
||||
|> exclude_blocked(user, exclude_blocked_opts)
|
||||
|> exclude_filtered(user)
|
||||
|> exclude_visibility(opts)
|
||||
end
|
||||
|
||||
|
|
@ -158,6 +159,20 @@ defmodule Pleroma.Notification do
|
|||
|> where([n, a, o, tm], is_nil(tm.user_id))
|
||||
end
|
||||
|
||||
defp exclude_filtered(query, user) do
|
||||
case Pleroma.Filter.compose_regex(user) do
|
||||
nil ->
|
||||
query
|
||||
|
||||
regex ->
|
||||
from([_n, a, o] in query,
|
||||
where:
|
||||
fragment("not(?->>'content' ~* ?)", o.data, ^regex) or
|
||||
fragment("?->>'actor' = ?", o.data, ^user.ap_id)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@valid_visibilities ~w[direct unlisted public private]
|
||||
|
||||
defp exclude_visibility(query, %{exclude_visibilities: visibility})
|
||||
|
|
@ -337,6 +352,7 @@ defmodule Pleroma.Notification do
|
|||
end
|
||||
end
|
||||
|
||||
@spec create_notifications(Activity.t(), keyword()) :: {:ok, [Notification.t()] | []}
|
||||
def create_notifications(activity, options \\ [])
|
||||
|
||||
def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity, options) do
|
||||
|
|
@ -367,6 +383,7 @@ defmodule Pleroma.Notification do
|
|||
do_send = do_send && user in enabled_receivers
|
||||
create_notification(activity, user, do_send)
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|
||||
{:ok, notifications}
|
||||
end
|
||||
|
|
@ -480,6 +497,10 @@ defmodule Pleroma.Notification do
|
|||
end
|
||||
end
|
||||
|
||||
def get_potential_receiver_ap_ids(%{data: %{"type" => "Follow", "object" => object_id}}) do
|
||||
[object_id]
|
||||
end
|
||||
|
||||
def get_potential_receiver_ap_ids(activity) do
|
||||
[]
|
||||
|> Utils.maybe_notify_to_recipients(activity)
|
||||
|
|
@ -550,11 +571,9 @@ defmodule Pleroma.Notification do
|
|||
[
|
||||
:self,
|
||||
:invisible,
|
||||
:followers,
|
||||
:follows,
|
||||
:non_followers,
|
||||
:non_follows,
|
||||
:recently_followed
|
||||
:block_from_strangers,
|
||||
:recently_followed,
|
||||
:filtered
|
||||
]
|
||||
|> Enum.find(&skip?(&1, activity, user))
|
||||
end
|
||||
|
|
@ -573,45 +592,15 @@ defmodule Pleroma.Notification do
|
|||
end
|
||||
|
||||
def skip?(
|
||||
:followers,
|
||||
:block_from_strangers,
|
||||
%Activity{} = activity,
|
||||
%User{notification_settings: %{followers: false}} = 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
|
||||
%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?(
|
||||
: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
|
||||
def skip?(:recently_followed, %Activity{data: %{"type" => "Follow"}} = activity, %User{} = user) do
|
||||
actor = activity.data["actor"]
|
||||
|
|
@ -623,6 +612,26 @@ defmodule Pleroma.Notification do
|
|||
end)
|
||||
end
|
||||
|
||||
def skip?(:filtered, %{data: %{"type" => type}}, _) when type in ["Follow", "Move"], do: false
|
||||
|
||||
def skip?(:filtered, activity, user) do
|
||||
object = Object.normalize(activity)
|
||||
|
||||
cond do
|
||||
is_nil(object) ->
|
||||
false
|
||||
|
||||
object.data["actor"] == user.ap_id ->
|
||||
false
|
||||
|
||||
not is_nil(regex = Pleroma.Filter.compose_regex(user, :re)) ->
|
||||
Regex.match?(regex, object.data["content"])
|
||||
|
||||
true ->
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def skip?(_, _, _), do: false
|
||||
|
||||
def for_user_and_activity(user, activity) do
|
||||
|
|
|
|||
|
|
@ -83,8 +83,8 @@ defmodule Pleroma.Object.Fetcher do
|
|||
{:transmogrifier, {:error, {:reject, nil}}} ->
|
||||
{:reject, nil}
|
||||
|
||||
{:transmogrifier, _} ->
|
||||
{:error, "Transmogrifier failure."}
|
||||
{:transmogrifier, _} = e ->
|
||||
{:error, e}
|
||||
|
||||
{:object, data, nil} ->
|
||||
reinject_object(%Object{}, data)
|
||||
|
|
@ -124,6 +124,10 @@ defmodule Pleroma.Object.Fetcher do
|
|||
{:error, "Object has been deleted"} ->
|
||||
nil
|
||||
|
||||
{:reject, reason} ->
|
||||
Logger.info("Rejected #{id} while fetching: #{inspect(reason)}")
|
||||
nil
|
||||
|
||||
e ->
|
||||
Logger.error("Error while fetching #{id}: #{inspect(e)}")
|
||||
nil
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@
|
|||
|
||||
defmodule Pleroma.Plugs.AdminSecretAuthenticationPlug do
|
||||
import Plug.Conn
|
||||
|
||||
alias Pleroma.Plugs.OAuthScopesPlug
|
||||
alias Pleroma.Plugs.RateLimiter
|
||||
alias Pleroma.User
|
||||
|
||||
def init(options) do
|
||||
|
|
@ -11,7 +14,10 @@ defmodule Pleroma.Plugs.AdminSecretAuthenticationPlug do
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
|
||||
|
|
@ -26,9 +32,9 @@ defmodule Pleroma.Plugs.AdminSecretAuthenticationPlug do
|
|||
|
||||
def authenticate(%{params: %{"admin_token" => admin_token}} = conn) do
|
||||
if admin_token == secret_token() do
|
||||
assign(conn, :user, %User{is_admin: true})
|
||||
assign_admin_user(conn)
|
||||
else
|
||||
conn
|
||||
handle_bad_token(conn)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -36,8 +42,19 @@ defmodule Pleroma.Plugs.AdminSecretAuthenticationPlug do
|
|||
token = secret_token()
|
||||
|
||||
case get_req_header(conn, "x-admin-token") do
|
||||
[^token] -> assign(conn, :user, %User{is_admin: true})
|
||||
_ -> conn
|
||||
blank when blank in [[], [""]] -> conn
|
||||
[^token] -> assign_admin_user(conn)
|
||||
_ -> handle_bad_token(conn)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -69,10 +69,11 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do
|
|||
img_src = "img-src 'self' data: blob:"
|
||||
media_src = "media-src 'self'"
|
||||
|
||||
# Strict multimedia CSP enforcement only when MediaProxy is enabled
|
||||
{img_src, media_src} =
|
||||
if Config.get([:media_proxy, :enabled]) &&
|
||||
!Config.get([:media_proxy, :proxy_opts, :redirect_on_failure]) do
|
||||
sources = get_proxy_and_attachment_sources()
|
||||
sources = build_csp_multimedia_source_list()
|
||||
{[img_src, sources], [media_src, sources]}
|
||||
else
|
||||
{[img_src, " https:"], [media_src, " https:"]}
|
||||
|
|
@ -81,14 +82,14 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do
|
|||
connect_src = ["connect-src 'self' blob: ", static_url, ?\s, websocket_url]
|
||||
|
||||
connect_src =
|
||||
if Pleroma.Config.get(:env) == :dev do
|
||||
if Config.get(:env) == :dev do
|
||||
[connect_src, " http://localhost:3035/"]
|
||||
else
|
||||
connect_src
|
||||
end
|
||||
|
||||
script_src =
|
||||
if Pleroma.Config.get(:env) == :dev do
|
||||
if Config.get(:env) == :dev do
|
||||
"script-src 'self' 'unsafe-eval'"
|
||||
else
|
||||
"script-src 'self'"
|
||||
|
|
@ -107,38 +108,64 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do
|
|||
|> :erlang.iolist_to_binary()
|
||||
end
|
||||
|
||||
defp get_proxy_and_attachment_sources do
|
||||
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
|
||||
media_proxy_whitelist =
|
||||
Enum.reduce(Config.get([:media_proxy, :whitelist]), [], fn host, acc ->
|
||||
add_source(acc, host)
|
||||
end)
|
||||
[:media_proxy, :whitelist]
|
||||
|> Config.get()
|
||||
|> build_csp_from_whitelist([])
|
||||
|
||||
media_proxy_base_url =
|
||||
if Config.get([:media_proxy, :base_url]),
|
||||
do: URI.parse(Config.get([:media_proxy, :base_url])).host
|
||||
captcha_method = Config.get([Pleroma.Captcha, :method])
|
||||
captcha_endpoint = Config.get([captcha_method, :endpoint])
|
||||
|
||||
upload_base_url =
|
||||
if Config.get([Pleroma.Upload, :base_url]),
|
||||
do: URI.parse(Config.get([Pleroma.Upload, :base_url])).host
|
||||
base_endpoints =
|
||||
[
|
||||
[:media_proxy, :base_url],
|
||||
[Pleroma.Upload, :base_url],
|
||||
[Pleroma.Uploaders.S3, :public_endpoint]
|
||||
]
|
||||
|> Enum.map(&Config.get/1)
|
||||
|
||||
s3_endpoint =
|
||||
if Config.get([Pleroma.Upload, :uploader]) == Pleroma.Uploaders.S3,
|
||||
do: URI.parse(Config.get([Pleroma.Uploaders.S3, :public_endpoint])).host
|
||||
|
||||
[]
|
||||
|> add_source(media_proxy_base_url)
|
||||
|> add_source(upload_base_url)
|
||||
|> add_source(s3_endpoint)
|
||||
[captcha_endpoint | base_endpoints]
|
||||
|> Enum.map(&build_csp_param/1)
|
||||
|> Enum.reduce([], &add_source(&2, &1))
|
||||
|> add_source(media_proxy_whitelist)
|
||||
end
|
||||
|
||||
defp add_source(iodata, nil), do: iodata
|
||||
defp add_source(iodata, []), do: 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, param), do: [[param, ?;] | csp_iodata]
|
||||
|
||||
defp build_csp_param(nil), do: nil
|
||||
|
||||
defp build_csp_param(url) when is_binary(url) do
|
||||
%{host: host, scheme: scheme} = URI.parse(url)
|
||||
|
||||
if scheme do
|
||||
[scheme, "://", host]
|
||||
end
|
||||
end
|
||||
|
||||
def warn_if_disabled do
|
||||
unless Config.get([:http_security, :enabled]) do
|
||||
Logger.warn("
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ defmodule Pleroma.Plugs.StaticFEPlug do
|
|||
def init(options), do: options
|
||||
|
||||
def call(conn, _) do
|
||||
if enabled?() and accepts_html?(conn) do
|
||||
if enabled?() and requires_html?(conn) do
|
||||
conn
|
||||
|> StaticFEController.call(:show)
|
||||
|> halt()
|
||||
|
|
@ -20,10 +20,7 @@ defmodule Pleroma.Plugs.StaticFEPlug do
|
|||
|
||||
defp enabled?, do: Pleroma.Config.get([:static_fe, :enabled], false)
|
||||
|
||||
defp accepts_html?(conn) do
|
||||
case get_req_header(conn, "accept") do
|
||||
[accept | _] -> String.contains?(accept, "text/html")
|
||||
_ -> false
|
||||
end
|
||||
defp requires_html?(conn) do
|
||||
Phoenix.Controller.get_format(conn) == "html"
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -7,37 +7,18 @@ defmodule Pleroma.Plugs.UserIsAdminPlug do
|
|||
import Plug.Conn
|
||||
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.OAuth
|
||||
|
||||
def init(options) do
|
||||
options
|
||||
end
|
||||
|
||||
def call(%{assigns: %{user: %User{is_admin: true}} = assigns} = conn, _) do
|
||||
token = assigns[:token]
|
||||
|
||||
cond do
|
||||
not Pleroma.Config.enforce_oauth_admin_scope_usage?() ->
|
||||
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
|
||||
def call(%{assigns: %{user: %User{is_admin: true}}} = conn, _) do
|
||||
conn
|
||||
end
|
||||
|
||||
def call(conn, _) do
|
||||
fail(conn)
|
||||
end
|
||||
|
||||
defp fail(conn) do
|
||||
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()
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -11,9 +11,7 @@ defmodule Pleroma.Repo do
|
|||
import Ecto.Query
|
||||
require Logger
|
||||
|
||||
defmodule Instrumenter do
|
||||
use Prometheus.EctoInstrumenter
|
||||
end
|
||||
defmodule Instrumenter, do: use(Prometheus.EctoInstrumenter)
|
||||
|
||||
@doc """
|
||||
Dynamically loads the repository url from the
|
||||
|
|
@ -51,35 +49,6 @@ defmodule Pleroma.Repo do
|
|||
end
|
||||
end
|
||||
|
||||
def check_migrations_applied!() do
|
||||
unless Pleroma.Config.get(
|
||||
[:i_am_aware_this_may_cause_data_loss, :disable_migration_check],
|
||||
false
|
||||
) do
|
||||
Ecto.Migrator.with_repo(__MODULE__, fn repo ->
|
||||
down_migrations =
|
||||
Ecto.Migrator.migrations(repo)
|
||||
|> Enum.reject(fn
|
||||
{:up, _, _} -> true
|
||||
{:down, _, _} -> false
|
||||
end)
|
||||
|
||||
if length(down_migrations) > 0 do
|
||||
down_migrations_text =
|
||||
Enum.map(down_migrations, fn {:down, id, name} -> "- #{name} (#{id})\n" end)
|
||||
|
||||
Logger.error(
|
||||
"The following migrations were not applied:\n#{down_migrations_text}If you want to start Pleroma anyway, set\nconfig :pleroma, :i_am_aware_this_may_cause_data_loss, disable_migration_check: true"
|
||||
)
|
||||
|
||||
raise Pleroma.Repo.UnappliedMigrationsError
|
||||
end
|
||||
end)
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
def chunk_stream(query, chunk_size) do
|
||||
# We don't actually need start and end funcitons of resource streaming,
|
||||
# but it seems to be the only way to not fetch records one-by-one and
|
||||
|
|
@ -107,7 +76,3 @@ defmodule Pleroma.Repo do
|
|||
)
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Pleroma.Repo.UnappliedMigrationsError do
|
||||
defexception message: "Unapplied Migrations detected"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@
|
|||
defmodule Pleroma.ReverseProxy.Client.Tesla do
|
||||
@behaviour Pleroma.ReverseProxy.Client
|
||||
|
||||
alias Pleroma.Gun.ConnectionPool
|
||||
|
||||
@type headers() :: [{String.t(), String.t()}]
|
||||
@type status() :: pos_integer()
|
||||
|
||||
|
|
@ -31,6 +33,8 @@ defmodule Pleroma.ReverseProxy.Client.Tesla do
|
|||
if is_map(response.body) and method != :head do
|
||||
{:ok, response.status, response.headers, response.body}
|
||||
else
|
||||
conn_pid = response.opts[:adapter][:conn]
|
||||
ConnectionPool.release_conn(conn_pid)
|
||||
{:ok, response.status, response.headers}
|
||||
end
|
||||
else
|
||||
|
|
@ -41,15 +45,8 @@ defmodule Pleroma.ReverseProxy.Client.Tesla do
|
|||
@impl true
|
||||
@spec stream_body(map()) ::
|
||||
{:ok, binary(), map()} | {:error, atom() | String.t()} | :done | no_return()
|
||||
def stream_body(%{pid: pid, opts: opts, fin: true}) do
|
||||
# if connection was reused, but in tesla were redirects,
|
||||
# tesla returns new opened connection, which must be closed manually
|
||||
if opts[:old_conn], do: Tesla.Adapter.Gun.close(pid)
|
||||
# if there were redirects we need to checkout old conn
|
||||
conn = opts[:old_conn] || opts[:conn]
|
||||
|
||||
if conn, do: :ok = Pleroma.Pool.Connections.checkout(conn, self(), :gun_connections)
|
||||
|
||||
def stream_body(%{pid: pid, fin: true}) do
|
||||
ConnectionPool.release_conn(pid)
|
||||
:done
|
||||
end
|
||||
|
||||
|
|
@ -74,8 +71,7 @@ defmodule Pleroma.ReverseProxy.Client.Tesla do
|
|||
@impl true
|
||||
@spec close(map) :: :ok | no_return()
|
||||
def close(%{pid: pid}) do
|
||||
adapter = check_adapter()
|
||||
adapter.close(pid)
|
||||
ConnectionPool.release_conn(pid)
|
||||
end
|
||||
|
||||
defp check_adapter do
|
||||
|
|
|
|||
|
|
@ -3,12 +3,13 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.ReverseProxy do
|
||||
@range_headers ~w(range if-range)
|
||||
@keep_req_headers ~w(accept user-agent accept-encoding cache-control if-modified-since) ++
|
||||
~w(if-unmodified-since if-none-match if-range range)
|
||||
~w(if-unmodified-since if-none-match) ++ @range_headers
|
||||
@resp_cache_headers ~w(etag date last-modified)
|
||||
@keep_resp_headers @resp_cache_headers ++
|
||||
~w(content-type content-disposition content-encoding content-range) ++
|
||||
~w(accept-ranges vary)
|
||||
~w(content-length content-type content-disposition content-encoding) ++
|
||||
~w(content-range accept-ranges vary)
|
||||
@default_cache_control_header "public, max-age=1209600"
|
||||
@valid_resp_codes [200, 206, 304]
|
||||
@max_read_duration :timer.seconds(30)
|
||||
|
|
@ -164,12 +165,17 @@ defmodule Pleroma.ReverseProxy do
|
|||
{:ok, code, _, _} ->
|
||||
{:error, {:invalid_http_response, code}}
|
||||
|
||||
{:ok, code, _} ->
|
||||
{:error, {:invalid_http_response, code}}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
defp response(conn, client, url, status, headers, opts) do
|
||||
Logger.debug("#{__MODULE__} #{status} #{url} #{inspect(headers)}")
|
||||
|
||||
result =
|
||||
conn
|
||||
|> put_resp_headers(build_resp_headers(headers, opts))
|
||||
|
|
@ -220,7 +226,9 @@ defmodule Pleroma.ReverseProxy do
|
|||
end
|
||||
end
|
||||
|
||||
defp head_response(conn, _url, code, headers, opts) do
|
||||
defp head_response(conn, url, code, headers, opts) do
|
||||
Logger.debug("#{__MODULE__} #{code} #{url} #{inspect(headers)}")
|
||||
|
||||
conn
|
||||
|> put_resp_headers(build_resp_headers(headers, opts))
|
||||
|> send_resp(code, "")
|
||||
|
|
@ -262,20 +270,33 @@ defmodule Pleroma.ReverseProxy do
|
|||
headers
|
||||
|> downcase_headers()
|
||||
|> Enum.filter(fn {k, _} -> k in @keep_req_headers end)
|
||||
|> (fn headers ->
|
||||
headers = headers ++ Keyword.get(opts, :req_headers, [])
|
||||
|> build_req_range_or_encoding_header(opts)
|
||||
|> build_req_user_agent_header(opts)
|
||||
|> Keyword.merge(Keyword.get(opts, :req_headers, []))
|
||||
end
|
||||
|
||||
if Keyword.get(opts, :keep_user_agent, false) do
|
||||
List.keystore(
|
||||
headers,
|
||||
"user-agent",
|
||||
0,
|
||||
{"user-agent", Pleroma.Application.user_agent()}
|
||||
)
|
||||
else
|
||||
headers
|
||||
end
|
||||
end).()
|
||||
# Disable content-encoding if any @range_headers are requested (see #1823).
|
||||
defp build_req_range_or_encoding_header(headers, _opts) do
|
||||
range? = Enum.any?(headers, fn {header, _} -> Enum.member?(@range_headers, header) end)
|
||||
|
||||
if range? && List.keymember?(headers, "accept-encoding", 0) do
|
||||
List.keydelete(headers, "accept-encoding", 0)
|
||||
else
|
||||
headers
|
||||
end
|
||||
end
|
||||
|
||||
defp build_req_user_agent_header(headers, opts) do
|
||||
if Keyword.get(opts, :keep_user_agent, false) do
|
||||
List.keystore(
|
||||
headers,
|
||||
"user-agent",
|
||||
0,
|
||||
{"user-agent", Pleroma.Application.user_agent()}
|
||||
)
|
||||
else
|
||||
headers
|
||||
end
|
||||
end
|
||||
|
||||
defp build_resp_headers(headers, opts) do
|
||||
|
|
@ -283,7 +304,7 @@ defmodule Pleroma.ReverseProxy do
|
|||
|> Enum.filter(fn {k, _} -> k in @keep_resp_headers end)
|
||||
|> build_resp_cache_headers(opts)
|
||||
|> build_resp_content_disposition_header(opts)
|
||||
|> (fn headers -> headers ++ Keyword.get(opts, :resp_headers, []) end).()
|
||||
|> Keyword.merge(Keyword.get(opts, :resp_headers, []))
|
||||
end
|
||||
|
||||
defp build_resp_cache_headers(headers, _opts) do
|
||||
|
|
|
|||
|
|
@ -97,20 +97,11 @@ defmodule Pleroma.Stats do
|
|||
}
|
||||
end
|
||||
|
||||
def get_status_visibility_count do
|
||||
counter_cache =
|
||||
CounterCache.get_as_map([
|
||||
"status_visibility_public",
|
||||
"status_visibility_private",
|
||||
"status_visibility_unlisted",
|
||||
"status_visibility_direct"
|
||||
])
|
||||
|
||||
%{
|
||||
public: counter_cache["status_visibility_public"] || 0,
|
||||
unlisted: counter_cache["status_visibility_unlisted"] || 0,
|
||||
private: counter_cache["status_visibility_private"] || 0,
|
||||
direct: counter_cache["status_visibility_direct"] || 0
|
||||
}
|
||||
def get_status_visibility_count(instance \\ nil) do
|
||||
if is_nil(instance) do
|
||||
CounterCache.get_sum()
|
||||
else
|
||||
CounterCache.get_by_instance(instance)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
76
lib/pleroma/telemetry/logger.ex
Normal file
76
lib/pleroma/telemetry/logger.ex
Normal 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
|
||||
110
lib/pleroma/tesla/middleware/follow_redirects.ex
Normal file
110
lib/pleroma/tesla/middleware/follow_redirects.ex
Normal 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
|
||||
|
|
@ -63,6 +63,10 @@ defmodule Pleroma.Upload do
|
|||
with {:ok, upload} <- prepare_upload(upload, opts),
|
||||
upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"},
|
||||
{:ok, upload} <- Pleroma.Upload.Filter.filter(opts.filters, upload),
|
||||
description = Map.get(opts, :description) || upload.name,
|
||||
{_, true} <-
|
||||
{:description_limit,
|
||||
String.length(description) <= Pleroma.Config.get([:instance, :description_limit])},
|
||||
{:ok, url_spec} <- Pleroma.Uploaders.Uploader.put_file(opts.uploader, upload) do
|
||||
{:ok,
|
||||
%{
|
||||
|
|
@ -75,9 +79,12 @@ defmodule Pleroma.Upload do
|
|||
"href" => url_from_spec(upload, opts.base_url, url_spec)
|
||||
}
|
||||
],
|
||||
"name" => Map.get(opts, :description) || upload.name
|
||||
"name" => description
|
||||
}}
|
||||
else
|
||||
{:description_limit, _} ->
|
||||
{:error, :description_too_long}
|
||||
|
||||
{:error, error} ->
|
||||
Logger.error(
|
||||
"#{__MODULE__} store (using #{inspect(opts.uploader)}) failed: #{inspect(error)}"
|
||||
|
|
|
|||
18
lib/pleroma/upload/filter/exiftool.ex
Normal file
18
lib/pleroma/upload/filter/exiftool.ex
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Upload.Filter.Exiftool do
|
||||
@moduledoc """
|
||||
Strips GPS related EXIF tags and overwrites the file in place.
|
||||
Also strips or replaces filesystem metadata e.g., timestamps.
|
||||
"""
|
||||
@behaviour Pleroma.Upload.Filter
|
||||
|
||||
def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do
|
||||
System.cmd("exiftool", ["-overwrite_original", "-gps:all=", file], parallelism: true)
|
||||
:ok
|
||||
end
|
||||
|
||||
def filter(_), do: :ok
|
||||
end
|
||||
|
|
@ -89,7 +89,7 @@ defmodule Pleroma.User do
|
|||
field(:keys, :string)
|
||||
field(:public_key, :string)
|
||||
field(:ap_id, :string)
|
||||
field(:avatar, :map)
|
||||
field(:avatar, :map, default: %{})
|
||||
field(:local, :boolean, default: true)
|
||||
field(:follower_address, :string)
|
||||
field(:following_address, :string)
|
||||
|
|
@ -115,7 +115,7 @@ defmodule Pleroma.User do
|
|||
field(:is_moderator, :boolean, default: false)
|
||||
field(:is_admin, :boolean, default: false)
|
||||
field(:show_role, :boolean, default: true)
|
||||
field(:settings, :map, default: nil)
|
||||
field(:mastofe_settings, :map, default: nil)
|
||||
field(:uri, ObjectValidators.Uri, default: nil)
|
||||
field(:hide_followers_count, :boolean, default: false)
|
||||
field(:hide_follows_count, :boolean, default: false)
|
||||
|
|
@ -138,6 +138,7 @@ defmodule Pleroma.User do
|
|||
field(:also_known_as, {:array, :string}, default: [])
|
||||
field(:inbox, :string)
|
||||
field(:shared_inbox, :string)
|
||||
field(:accepts_chat_messages, :boolean, default: nil)
|
||||
|
||||
embeds_one(
|
||||
:notification_settings,
|
||||
|
|
@ -388,8 +389,8 @@ defmodule Pleroma.User do
|
|||
defp fix_follower_address(params), do: params
|
||||
|
||||
def remote_user_changeset(struct \\ %User{local: false}, params) do
|
||||
bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
|
||||
name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
|
||||
bio_limit = Config.get([:instance, :user_bio_length], 5000)
|
||||
name_limit = Config.get([:instance, :user_name_length], 100)
|
||||
|
||||
name =
|
||||
case params[:name] do
|
||||
|
|
@ -436,7 +437,8 @@ defmodule Pleroma.User do
|
|||
:discoverable,
|
||||
:invisible,
|
||||
:actor_type,
|
||||
:also_known_as
|
||||
:also_known_as,
|
||||
:accepts_chat_messages
|
||||
]
|
||||
)
|
||||
|> validate_required([:name, :ap_id])
|
||||
|
|
@ -448,8 +450,8 @@ defmodule Pleroma.User do
|
|||
end
|
||||
|
||||
def update_changeset(struct, params \\ %{}) do
|
||||
bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
|
||||
name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
|
||||
bio_limit = Config.get([:instance, :user_bio_length], 5000)
|
||||
name_limit = Config.get([:instance, :user_name_length], 100)
|
||||
|
||||
struct
|
||||
|> cast(
|
||||
|
|
@ -481,7 +483,8 @@ defmodule Pleroma.User do
|
|||
:pleroma_settings_store,
|
||||
:discoverable,
|
||||
:actor_type,
|
||||
:also_known_as
|
||||
:also_known_as,
|
||||
:accepts_chat_messages
|
||||
]
|
||||
)
|
||||
|> unique_constraint(:nickname)
|
||||
|
|
@ -527,11 +530,21 @@ defmodule Pleroma.User do
|
|||
end
|
||||
|
||||
defp put_emoji(changeset) do
|
||||
bio = get_change(changeset, :bio)
|
||||
name = get_change(changeset, :name)
|
||||
emojified_fields = [:bio, :name, :raw_fields]
|
||||
|
||||
if Enum.any?(changeset.changes, fn {k, _} -> k in emojified_fields end) do
|
||||
bio = Emoji.Formatter.get_emoji_map(get_field(changeset, :bio))
|
||||
name = Emoji.Formatter.get_emoji_map(get_field(changeset, :name))
|
||||
|
||||
emoji = Map.merge(bio, name)
|
||||
|
||||
emoji =
|
||||
changeset
|
||||
|> get_field(:raw_fields)
|
||||
|> Enum.reduce(emoji, fn x, acc ->
|
||||
Map.merge(acc, Emoji.Formatter.get_emoji_map(x["name"] <> x["value"]))
|
||||
end)
|
||||
|
||||
if bio || name do
|
||||
emoji = Map.merge(Emoji.Formatter.get_emoji_map(bio), Emoji.Formatter.get_emoji_map(name))
|
||||
put_change(changeset, :emoji, emoji)
|
||||
else
|
||||
changeset
|
||||
|
|
@ -539,14 +552,11 @@ defmodule Pleroma.User do
|
|||
end
|
||||
|
||||
defp put_change_if_present(changeset, map_field, value_function) do
|
||||
if value = get_change(changeset, map_field) do
|
||||
with {:ok, new_value} <- value_function.(value) do
|
||||
put_change(changeset, map_field, new_value)
|
||||
else
|
||||
_ -> changeset
|
||||
end
|
||||
with {:ok, value} <- fetch_change(changeset, map_field),
|
||||
{:ok, new_value} <- value_function.(value) do
|
||||
put_change(changeset, map_field, new_value)
|
||||
else
|
||||
changeset
|
||||
_ -> changeset
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -621,12 +631,13 @@ defmodule Pleroma.User do
|
|||
def force_password_reset(user), do: update_password_reset_pending(user, true)
|
||||
|
||||
def register_changeset(struct, params \\ %{}, opts \\ []) do
|
||||
bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
|
||||
name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
|
||||
bio_limit = Config.get([:instance, :user_bio_length], 5000)
|
||||
name_limit = Config.get([:instance, :user_name_length], 100)
|
||||
params = Map.put_new(params, :accepts_chat_messages, true)
|
||||
|
||||
need_confirmation? =
|
||||
if is_nil(opts[:need_confirmation]) do
|
||||
Pleroma.Config.get([:instance, :account_activation_required])
|
||||
Config.get([:instance, :account_activation_required])
|
||||
else
|
||||
opts[:need_confirmation]
|
||||
end
|
||||
|
|
@ -641,13 +652,14 @@ defmodule Pleroma.User do
|
|||
:nickname,
|
||||
:password,
|
||||
:password_confirmation,
|
||||
:emoji
|
||||
:emoji,
|
||||
:accepts_chat_messages
|
||||
])
|
||||
|> validate_required([:name, :nickname, :password, :password_confirmation])
|
||||
|> validate_confirmation(:password)
|
||||
|> unique_constraint(:email)
|
||||
|> unique_constraint(:nickname)
|
||||
|> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
|
||||
|> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
|
||||
|> validate_format(:nickname, local_nickname_regex())
|
||||
|> validate_format(:email, @email_regex)
|
||||
|> validate_length(:bio, max: bio_limit)
|
||||
|
|
@ -662,7 +674,7 @@ defmodule Pleroma.User do
|
|||
def maybe_validate_required_email(changeset, true), do: changeset
|
||||
|
||||
def maybe_validate_required_email(changeset, _) do
|
||||
if Pleroma.Config.get([:instance, :account_activation_required]) do
|
||||
if Config.get([:instance, :account_activation_required]) do
|
||||
validate_required(changeset, [:email])
|
||||
else
|
||||
changeset
|
||||
|
|
@ -682,7 +694,7 @@ defmodule Pleroma.User do
|
|||
end
|
||||
|
||||
defp autofollow_users(user) do
|
||||
candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
|
||||
candidates = Config.get([:instance, :autofollowed_nicknames])
|
||||
|
||||
autofollowed_users =
|
||||
User.Query.build(%{nickname: candidates, local: true, deactivated: false})
|
||||
|
|
@ -701,27 +713,52 @@ defmodule Pleroma.User do
|
|||
def post_register_action(%User{} = user) do
|
||||
with {:ok, user} <- autofollow_users(user),
|
||||
{:ok, user} <- set_cache(user),
|
||||
{:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
|
||||
{:ok, _} <- send_welcome_email(user),
|
||||
{:ok, _} <- send_welcome_message(user),
|
||||
{:ok, _} <- try_send_confirmation_email(user) do
|
||||
{:ok, user}
|
||||
end
|
||||
end
|
||||
|
||||
def try_send_confirmation_email(%User{} = user) do
|
||||
if user.confirmation_pending &&
|
||||
Pleroma.Config.get([:instance, :account_activation_required]) do
|
||||
user
|
||||
|> Pleroma.Emails.UserEmail.account_confirmation_email()
|
||||
|> Pleroma.Emails.Mailer.deliver_async()
|
||||
|
||||
def send_welcome_message(user) do
|
||||
if User.WelcomeMessage.enabled?() do
|
||||
User.WelcomeMessage.post_message(user)
|
||||
{:ok, :enqueued}
|
||||
else
|
||||
{:ok, :noop}
|
||||
end
|
||||
end
|
||||
|
||||
def try_send_confirmation_email(users) do
|
||||
Enum.each(users, &try_send_confirmation_email/1)
|
||||
def send_welcome_email(%User{email: email} = user) when is_binary(email) do
|
||||
if User.WelcomeEmail.enabled?() do
|
||||
User.WelcomeEmail.send_email(user)
|
||||
{:ok, :enqueued}
|
||||
else
|
||||
{:ok, :noop}
|
||||
end
|
||||
end
|
||||
|
||||
def send_welcome_email(_), do: {:ok, :noop}
|
||||
|
||||
@spec try_send_confirmation_email(User.t()) :: {:ok, :enqueued | :noop}
|
||||
def try_send_confirmation_email(%User{confirmation_pending: true} = user) do
|
||||
if Config.get([:instance, :account_activation_required]) do
|
||||
send_confirmation_email(user)
|
||||
{:ok, :enqueued}
|
||||
else
|
||||
{:ok, :noop}
|
||||
end
|
||||
end
|
||||
|
||||
def try_send_confirmation_email(_), do: {:ok, :noop}
|
||||
|
||||
@spec send_confirmation_email(Uset.t()) :: User.t()
|
||||
def send_confirmation_email(%User{} = user) do
|
||||
user
|
||||
|> Pleroma.Emails.UserEmail.account_confirmation_email()
|
||||
|> Pleroma.Emails.Mailer.deliver_async()
|
||||
|
||||
user
|
||||
end
|
||||
|
||||
def needs_update?(%User{local: true}), do: false
|
||||
|
|
@ -766,7 +803,7 @@ defmodule Pleroma.User do
|
|||
defdelegate following(user), to: FollowingRelationship
|
||||
|
||||
def follow(%User{} = follower, %User{} = followed, state \\ :follow_accept) do
|
||||
deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
|
||||
deny_follow_blocked = Config.get([:user, :deny_follow_blocked])
|
||||
|
||||
cond do
|
||||
followed.deactivated ->
|
||||
|
|
@ -967,7 +1004,7 @@ defmodule Pleroma.User do
|
|||
end
|
||||
|
||||
def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
|
||||
restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
|
||||
restrict_to_local = Config.get([:instance, :limit_to_local_content])
|
||||
|
||||
cond do
|
||||
is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
|
||||
|
|
@ -1163,7 +1200,7 @@ defmodule Pleroma.User do
|
|||
|
||||
@spec update_follower_count(User.t()) :: {:ok, User.t()}
|
||||
def update_follower_count(%User{} = user) do
|
||||
if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
|
||||
if user.local or !Config.get([:instance, :external_user_synchronization]) do
|
||||
follower_count = FollowingRelationship.follower_count(user)
|
||||
|
||||
user
|
||||
|
|
@ -1176,7 +1213,7 @@ defmodule Pleroma.User do
|
|||
|
||||
@spec update_following_count(User.t()) :: {:ok, User.t()}
|
||||
def update_following_count(%User{local: false} = user) do
|
||||
if Pleroma.Config.get([:instance, :external_user_synchronization]) do
|
||||
if Config.get([:instance, :external_user_synchronization]) do
|
||||
{:ok, maybe_fetch_follow_information(user)}
|
||||
else
|
||||
{:ok, user}
|
||||
|
|
@ -1263,7 +1300,7 @@ defmodule Pleroma.User do
|
|||
end
|
||||
|
||||
def subscribe(%User{} = subscriber, %User{} = target) do
|
||||
deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
|
||||
deny_follow_blocked = Config.get([:user, :deny_follow_blocked])
|
||||
|
||||
if blocks?(target, subscriber) and deny_follow_blocked do
|
||||
{:error, "Could not subscribe: #{target.nickname} is blocking you"}
|
||||
|
|
@ -1309,7 +1346,8 @@ defmodule Pleroma.User do
|
|||
|
||||
unsubscribe(blocked, blocker)
|
||||
|
||||
if following?(blocked, blocker), do: unfollow(blocked, blocker)
|
||||
unfollowing_blocked = Config.get([:activitypub, :unfollow_blocked], true)
|
||||
if unfollowing_blocked && following?(blocked, blocker), do: unfollow(blocked, blocker)
|
||||
|
||||
{:ok, blocker} = update_follower_count(blocker)
|
||||
{:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
|
||||
|
|
@ -1527,8 +1565,7 @@ defmodule Pleroma.User do
|
|||
blocked_identifiers,
|
||||
fn blocked_identifier ->
|
||||
with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
|
||||
{:ok, _user_block} <- block(blocker, blocked),
|
||||
{:ok, _} <- ActivityPub.block(blocker, blocked) do
|
||||
{:ok, _block} <- CommonAPI.block(blocker, blocked) do
|
||||
blocked
|
||||
else
|
||||
err ->
|
||||
|
|
@ -1546,7 +1583,7 @@ defmodule Pleroma.User do
|
|||
fn followed_identifier ->
|
||||
with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
|
||||
{:ok, follower} <- maybe_direct_follow(follower, followed),
|
||||
{:ok, _} <- ActivityPub.follow(follower, followed) do
|
||||
{:ok, _, _, _} <- CommonAPI.follow(follower, followed) do
|
||||
followed
|
||||
else
|
||||
err ->
|
||||
|
|
@ -1654,7 +1691,7 @@ defmodule Pleroma.User do
|
|||
Pleroma.HTML.Scrubber.TwitterText
|
||||
end
|
||||
|
||||
def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
|
||||
def html_filter_policy(_), do: Config.get([:markup, :scrub_policy])
|
||||
|
||||
def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
|
||||
|
||||
|
|
@ -1836,7 +1873,7 @@ defmodule Pleroma.User do
|
|||
end
|
||||
|
||||
defp local_nickname_regex do
|
||||
if Pleroma.Config.get([:instance, :extended_nickname_format]) do
|
||||
if Config.get([:instance, :extended_nickname_format]) do
|
||||
@extended_local_nickname_regex
|
||||
else
|
||||
@strict_local_nickname_regex
|
||||
|
|
@ -1964,8 +2001,8 @@ defmodule Pleroma.User do
|
|||
|
||||
def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
|
||||
# use instance-default
|
||||
config = Pleroma.Config.get([:assets, :mascots])
|
||||
default_mascot = Pleroma.Config.get([:assets, :default_mascot])
|
||||
config = Config.get([:assets, :mascots])
|
||||
default_mascot = Config.get([:assets, :default_mascot])
|
||||
mascot = Keyword.get(config, default_mascot)
|
||||
|
||||
%{
|
||||
|
|
@ -2060,7 +2097,7 @@ defmodule Pleroma.User do
|
|||
|
||||
def validate_fields(changeset, remote? \\ false) do
|
||||
limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
|
||||
limit = Pleroma.Config.get([:instance, limit_name], 0)
|
||||
limit = Config.get([:instance, limit_name], 0)
|
||||
|
||||
changeset
|
||||
|> validate_length(:fields, max: limit)
|
||||
|
|
@ -2074,8 +2111,8 @@ defmodule Pleroma.User do
|
|||
end
|
||||
|
||||
defp valid_field?(%{"name" => name, "value" => value}) do
|
||||
name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
|
||||
value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
|
||||
name_limit = Config.get([:instance, :account_field_name_length], 255)
|
||||
value_limit = Config.get([:instance, :account_field_value_length], 255)
|
||||
|
||||
is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
|
||||
String.length(value) <= value_limit
|
||||
|
|
@ -2085,10 +2122,10 @@ defmodule Pleroma.User do
|
|||
|
||||
defp truncate_field(%{"name" => name, "value" => value}) do
|
||||
{name, _chopped} =
|
||||
String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255))
|
||||
String.split_at(name, Config.get([:instance, :account_field_name_length], 255))
|
||||
|
||||
{value, _chopped} =
|
||||
String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255))
|
||||
String.split_at(value, Config.get([:instance, :account_field_value_length], 255))
|
||||
|
||||
%{"name" => name, "value" => value}
|
||||
end
|
||||
|
|
@ -2118,8 +2155,8 @@ defmodule Pleroma.User do
|
|||
|
||||
def mastodon_settings_update(user, settings) do
|
||||
user
|
||||
|> cast(%{settings: settings}, [:settings])
|
||||
|> validate_required([:settings])
|
||||
|> cast(%{mastofe_settings: settings}, [:mastofe_settings])
|
||||
|> validate_required([:mastofe_settings])
|
||||
|> update_and_set_cache()
|
||||
end
|
||||
|
||||
|
|
@ -2143,7 +2180,7 @@ defmodule Pleroma.User do
|
|||
|
||||
def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
|
||||
if id not in user.pinned_activities do
|
||||
max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0)
|
||||
max_pinned_statuses = Config.get([:instance, :max_pinned_statuses], 0)
|
||||
params = %{pinned_activities: user.pinned_activities ++ [id]}
|
||||
|
||||
user
|
||||
|
|
|
|||
|
|
@ -10,21 +10,15 @@ defmodule Pleroma.User.NotificationSetting do
|
|||
@primary_key false
|
||||
|
||||
embedded_schema do
|
||||
field(:followers, :boolean, default: true)
|
||||
field(:follows, :boolean, default: true)
|
||||
field(:non_follows, :boolean, default: true)
|
||||
field(:non_followers, :boolean, default: true)
|
||||
field(:privacy_option, :boolean, default: false)
|
||||
field(:block_from_strangers, :boolean, default: false)
|
||||
field(:hide_notification_contents, :boolean, default: false)
|
||||
end
|
||||
|
||||
def changeset(schema, params) do
|
||||
schema
|
||||
|> cast(prepare_attrs(params), [
|
||||
:followers,
|
||||
:follows,
|
||||
:non_follows,
|
||||
:non_followers,
|
||||
:privacy_option
|
||||
:block_from_strangers,
|
||||
:hide_notification_contents
|
||||
])
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ defmodule Pleroma.User.Search do
|
|||
|> base_query(following)
|
||||
|> filter_blocked_user(for_user)
|
||||
|> filter_invisible_users()
|
||||
|> filter_internal_users()
|
||||
|> filter_blocked_domains(for_user)
|
||||
|> fts_search(query_string)
|
||||
|> trigram_rank(query_string)
|
||||
|
|
@ -68,11 +69,15 @@ defmodule Pleroma.User.Search do
|
|||
u in query,
|
||||
where:
|
||||
fragment(
|
||||
# The fragment must _exactly_ match `users_fts_index`, otherwise the index won't work
|
||||
"""
|
||||
(to_tsvector('simple', ?) || to_tsvector('simple', ?)) @@ to_tsquery('simple', ?)
|
||||
(
|
||||
setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
|
||||
setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')
|
||||
) @@ to_tsquery('simple', ?)
|
||||
""",
|
||||
u.name,
|
||||
u.nickname,
|
||||
u.name,
|
||||
^query_string
|
||||
)
|
||||
)
|
||||
|
|
@ -87,15 +92,23 @@ defmodule Pleroma.User.Search do
|
|||
|> Enum.join(" | ")
|
||||
end
|
||||
|
||||
# Considers nickname match, localized nickname match, name match; preferences nickname match
|
||||
defp trigram_rank(query, query_string) do
|
||||
from(
|
||||
u in query,
|
||||
select_merge: %{
|
||||
search_rank:
|
||||
fragment(
|
||||
"similarity(?, trim(? || ' ' || coalesce(?, '')))",
|
||||
"""
|
||||
similarity(?, ?) +
|
||||
similarity(?, regexp_replace(?, '@.+', '')) +
|
||||
similarity(?, trim(coalesce(?, '')))
|
||||
""",
|
||||
^query_string,
|
||||
u.nickname,
|
||||
^query_string,
|
||||
u.nickname,
|
||||
^query_string,
|
||||
u.name
|
||||
)
|
||||
}
|
||||
|
|
@ -109,6 +122,10 @@ defmodule Pleroma.User.Search do
|
|||
from(q in query, where: q.invisible == false)
|
||||
end
|
||||
|
||||
defp filter_internal_users(query) do
|
||||
from(q in query, where: q.actor_type != "Application")
|
||||
end
|
||||
|
||||
defp filter_blocked_user(query, %User{} = blocker) do
|
||||
query
|
||||
|> join(:left, [u], b in Pleroma.UserRelationship,
|
||||
|
|
|
|||
62
lib/pleroma/user/welcome_email.ex
Normal file
62
lib/pleroma/user/welcome_email.ex
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.User.WelcomeEmail do
|
||||
@moduledoc """
|
||||
The module represents the functions to send welcome email.
|
||||
"""
|
||||
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.Emails
|
||||
alias Pleroma.User
|
||||
|
||||
import Pleroma.Config.Helpers, only: [instance_name: 0]
|
||||
|
||||
@spec enabled?() :: boolean()
|
||||
def enabled?, do: Config.get([:welcome, :email, :enabled], false)
|
||||
|
||||
@spec send_email(User.t()) :: {:ok, Oban.Job.t()}
|
||||
def send_email(%User{} = user) do
|
||||
user
|
||||
|> Emails.UserEmail.welcome(email_options(user))
|
||||
|> Emails.Mailer.deliver_async()
|
||||
end
|
||||
|
||||
defp email_options(user) do
|
||||
bindings = [user: user, instance_name: instance_name()]
|
||||
|
||||
%{}
|
||||
|> add_sender(Config.get([:welcome, :email, :sender], nil))
|
||||
|> add_option(:subject, bindings)
|
||||
|> add_option(:html, bindings)
|
||||
|> add_option(:text, bindings)
|
||||
end
|
||||
|
||||
defp add_option(opts, option, bindings) do
|
||||
[:welcome, :email, option]
|
||||
|> Config.get(nil)
|
||||
|> eval_string(bindings)
|
||||
|> merge_options(opts, option)
|
||||
end
|
||||
|
||||
defp add_sender(opts, {_name, _email} = sender) do
|
||||
merge_options(sender, opts, :sender)
|
||||
end
|
||||
|
||||
defp add_sender(opts, sender) when is_binary(sender) do
|
||||
add_sender(opts, {instance_name(), sender})
|
||||
end
|
||||
|
||||
defp add_sender(opts, _), do: opts
|
||||
|
||||
defp merge_options(nil, options, _option), do: options
|
||||
|
||||
defp merge_options(value, options, option) do
|
||||
Map.merge(options, %{option => value})
|
||||
end
|
||||
|
||||
defp eval_string(nil, _), do: nil
|
||||
defp eval_string("", _), do: nil
|
||||
defp eval_string(str, bindings), do: EEx.eval_string(str, bindings)
|
||||
end
|
||||
|
|
@ -3,32 +3,45 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.User.WelcomeMessage do
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.CommonAPI
|
||||
|
||||
def post_welcome_message_to_user(user) do
|
||||
with %User{} = sender_user <- welcome_user(),
|
||||
message when is_binary(message) <- welcome_message() do
|
||||
CommonAPI.post(sender_user, %{
|
||||
visibility: "direct",
|
||||
status: "@#{user.nickname}\n#{message}"
|
||||
})
|
||||
else
|
||||
_ -> {:ok, nil}
|
||||
end
|
||||
@spec enabled?() :: boolean()
|
||||
def enabled?, do: Config.get([:welcome, :direct_message, :enabled], false)
|
||||
|
||||
@spec post_message(User.t()) :: {:ok, Pleroma.Activity.t() | nil}
|
||||
def post_message(user) do
|
||||
[:welcome, :direct_message, :sender_nickname]
|
||||
|> Config.get(nil)
|
||||
|> fetch_sender()
|
||||
|> do_post(user, welcome_message())
|
||||
end
|
||||
|
||||
defp welcome_user do
|
||||
with nickname when is_binary(nickname) <-
|
||||
Pleroma.Config.get([:instance, :welcome_user_nickname]),
|
||||
%User{local: true} = user <- User.get_cached_by_nickname(nickname) do
|
||||
defp do_post(%User{} = sender, %User{nickname: nickname}, message)
|
||||
when is_binary(message) do
|
||||
CommonAPI.post(
|
||||
sender,
|
||||
%{
|
||||
visibility: "direct",
|
||||
status: "@#{nickname}\n#{message}"
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
defp do_post(_sender, _recipient, _message), do: {:ok, nil}
|
||||
|
||||
defp fetch_sender(nickname) when is_binary(nickname) do
|
||||
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
|
||||
user
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_sender(_), do: nil
|
||||
|
||||
defp welcome_message do
|
||||
Pleroma.Config.get([:instance, :welcome_message])
|
||||
Config.get([:welcome, :direct_message, :message], nil)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
alias Pleroma.Constants
|
||||
alias Pleroma.Conversation
|
||||
alias Pleroma.Conversation.Participation
|
||||
alias Pleroma.Filter
|
||||
alias Pleroma.Maps
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
|
|
@ -321,50 +322,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
end
|
||||
end
|
||||
|
||||
@spec update(map()) :: {:ok, Activity.t()} | {:error, any()}
|
||||
def update(%{to: to, cc: cc, actor: actor, object: object} = params) do
|
||||
local = !(params[:local] == false)
|
||||
activity_id = params[:activity_id]
|
||||
|
||||
data =
|
||||
%{
|
||||
"to" => to,
|
||||
"cc" => cc,
|
||||
"type" => "Update",
|
||||
"actor" => actor,
|
||||
"object" => object
|
||||
}
|
||||
|> Maps.put_if_present("id", activity_id)
|
||||
|
||||
with {:ok, activity} <- insert(data, local),
|
||||
_ <- notify_and_stream(activity),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
@spec follow(User.t(), User.t(), String.t() | nil, boolean(), keyword()) ::
|
||||
{:ok, Activity.t()} | {:error, any()}
|
||||
def follow(follower, followed, activity_id \\ nil, local \\ true, opts \\ []) do
|
||||
with {:ok, result} <-
|
||||
Repo.transaction(fn -> do_follow(follower, followed, activity_id, local, opts) end) do
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
defp do_follow(follower, followed, activity_id, local, opts) do
|
||||
skip_notify_and_stream = Keyword.get(opts, :skip_notify_and_stream, false)
|
||||
data = make_follow_data(follower, followed, activity_id)
|
||||
|
||||
with {:ok, activity} <- insert(data, local),
|
||||
_ <- skip_notify_and_stream || notify_and_stream(activity),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity}
|
||||
else
|
||||
{:error, error} -> Repo.rollback(error)
|
||||
end
|
||||
end
|
||||
|
||||
@spec unfollow(User.t(), User.t(), String.t() | nil, boolean()) ::
|
||||
{:ok, Activity.t()} | nil | {:error, any()}
|
||||
def unfollow(follower, followed, activity_id \\ nil, local \\ true) do
|
||||
|
|
@ -388,33 +345,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
end
|
||||
end
|
||||
|
||||
@spec block(User.t(), User.t(), String.t() | nil, boolean()) ::
|
||||
{:ok, Activity.t()} | {:error, any()}
|
||||
def block(blocker, blocked, activity_id \\ nil, local \\ true) do
|
||||
with {:ok, result} <-
|
||||
Repo.transaction(fn -> do_block(blocker, blocked, activity_id, local) end) do
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
defp do_block(blocker, blocked, activity_id, local) do
|
||||
unfollow_blocked = Config.get([:activitypub, :unfollow_blocked])
|
||||
|
||||
if unfollow_blocked and fetch_latest_follow(blocker, blocked) do
|
||||
unfollow(blocker, blocked, nil, local)
|
||||
end
|
||||
|
||||
block_data = make_block_data(blocker, blocked, activity_id)
|
||||
|
||||
with {:ok, activity} <- insert(block_data, local),
|
||||
_ <- notify_and_stream(activity),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity}
|
||||
else
|
||||
{:error, error} -> Repo.rollback(error)
|
||||
end
|
||||
end
|
||||
|
||||
@spec flag(map()) :: {:ok, Activity.t()} | {:error, any()}
|
||||
def flag(
|
||||
%{
|
||||
|
|
@ -495,6 +425,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|> maybe_set_thread_muted_field(opts)
|
||||
|> restrict_blocked(opts)
|
||||
|> restrict_recipients(recipients, opts[:user])
|
||||
|> restrict_filtered(opts)
|
||||
|> where(
|
||||
[activity],
|
||||
fragment(
|
||||
|
|
@ -1010,6 +941,26 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|
||||
defp restrict_instance(query, _), do: query
|
||||
|
||||
defp restrict_filtered(query, %{user: %User{} = user}) do
|
||||
case Filter.compose_regex(user) do
|
||||
nil ->
|
||||
query
|
||||
|
||||
regex ->
|
||||
from([activity, object] in query,
|
||||
where:
|
||||
fragment("not(?->>'content' ~* ?)", object.data, ^regex) or
|
||||
activity.actor == ^user.ap_id
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp restrict_filtered(query, %{blocking_user: %User{} = user}) do
|
||||
restrict_filtered(query, %{user: user})
|
||||
end
|
||||
|
||||
defp restrict_filtered(query, _), do: query
|
||||
|
||||
defp exclude_poll_votes(query, %{include_poll_votes: true}), do: query
|
||||
|
||||
defp exclude_poll_votes(query, _) do
|
||||
|
|
@ -1140,6 +1091,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|> restrict_favorited_by(opts)
|
||||
|> restrict_blocked(restrict_blocked_opts)
|
||||
|> restrict_muted(restrict_muted_opts)
|
||||
|> restrict_filtered(opts)
|
||||
|> restrict_media(opts)
|
||||
|> restrict_visibility(opts)
|
||||
|> restrict_thread_visibility(opts, config)
|
||||
|
|
@ -1148,6 +1100,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|> restrict_muted_reblogs(restrict_muted_reblogs_opts)
|
||||
|> restrict_instance(opts)
|
||||
|> restrict_announce_object_actor(opts)
|
||||
|> restrict_filtered(opts)
|
||||
|> Activity.restrict_deactivated_users()
|
||||
|> exclude_poll_votes(opts)
|
||||
|> exclude_chat_messages(opts)
|
||||
|
|
@ -1273,6 +1226,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
end)
|
||||
|
||||
locked = data["manuallyApprovesFollowers"] || false
|
||||
capabilities = data["capabilities"] || %{}
|
||||
accepts_chat_messages = capabilities["acceptsChatMessages"]
|
||||
data = Transmogrifier.maybe_fix_user_object(data)
|
||||
discoverable = data["discoverable"] || false
|
||||
invisible = data["invisible"] || false
|
||||
|
|
@ -1311,7 +1266,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
also_known_as: Map.get(data, "alsoKnownAs", []),
|
||||
public_key: public_key,
|
||||
inbox: data["inbox"],
|
||||
shared_inbox: shared_inbox
|
||||
shared_inbox: shared_inbox,
|
||||
accepts_chat_messages: accepts_chat_messages
|
||||
}
|
||||
|
||||
# nickname can be nil because of virtual actors
|
||||
|
|
@ -1414,12 +1370,41 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
Logger.debug("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
|
||||
{:error, e}
|
||||
|
||||
{:error, {:reject, reason} = e} ->
|
||||
Logger.info("Rejected user #{ap_id}: #{inspect(reason)}")
|
||||
{:error, e}
|
||||
|
||||
{:error, e} ->
|
||||
Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
def maybe_handle_clashing_nickname(data) do
|
||||
nickname = data[:nickname]
|
||||
|
||||
with %User{} = old_user <- User.get_by_nickname(nickname),
|
||||
{_, false} <- {:ap_id_comparison, data[:ap_id] == old_user.ap_id} do
|
||||
Logger.info(
|
||||
"Found an old user for #{nickname}, the old ap id is #{old_user.ap_id}, new one is #{
|
||||
data[:ap_id]
|
||||
}, renaming."
|
||||
)
|
||||
|
||||
old_user
|
||||
|> User.remote_user_changeset(%{nickname: "#{old_user.id}.#{old_user.nickname}"})
|
||||
|> User.update_and_set_cache()
|
||||
else
|
||||
{:ap_id_comparison, true} ->
|
||||
Logger.info(
|
||||
"Found an old user for #{nickname}, but the ap id #{data[:ap_id]} is the same as the new user. Race condition? Not changing anything."
|
||||
)
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def make_user_from_ap_id(ap_id) do
|
||||
user = User.get_cached_by_ap_id(ap_id)
|
||||
|
||||
|
|
@ -1432,6 +1417,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|> User.remote_user_changeset(data)
|
||||
|> User.update_and_set_cache()
|
||||
else
|
||||
maybe_handle_clashing_nickname(data)
|
||||
|
||||
data
|
||||
|> User.remote_user_changeset()
|
||||
|> Repo.insert()
|
||||
|
|
|
|||
|
|
@ -514,7 +514,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
{new_user, for_user}
|
||||
end
|
||||
|
||||
# TODO: Add support for "object" field
|
||||
@doc """
|
||||
Endpoint based on <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>
|
||||
|
||||
|
|
@ -525,6 +524,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
Response:
|
||||
- HTTP Code: 201 Created
|
||||
- HTTP Body: ActivityPub object to be inserted into another's `attachment` field
|
||||
|
||||
Note: Will not point to a URL with a `Location` header because no standalone Activity has been created.
|
||||
"""
|
||||
def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
|
||||
with {:ok, object} <-
|
||||
|
|
|
|||
|
|
@ -14,6 +14,19 @@ defmodule Pleroma.Web.ActivityPub.Builder do
|
|||
|
||||
require Pleroma.Constants
|
||||
|
||||
@spec follow(User.t(), User.t()) :: {:ok, map(), keyword()}
|
||||
def follow(follower, followed) do
|
||||
data = %{
|
||||
"id" => Utils.generate_activity_id(),
|
||||
"actor" => follower.ap_id,
|
||||
"type" => "Follow",
|
||||
"object" => followed.ap_id,
|
||||
"to" => [followed.ap_id]
|
||||
}
|
||||
|
||||
{:ok, data, []}
|
||||
end
|
||||
|
||||
@spec emoji_react(User.t(), Object.t(), String.t()) :: {:ok, map(), keyword()}
|
||||
def emoji_react(actor, object, emoji) do
|
||||
with {:ok, data, meta} <- object_action(actor, object) do
|
||||
|
|
@ -123,6 +136,33 @@ defmodule Pleroma.Web.ActivityPub.Builder do
|
|||
end
|
||||
end
|
||||
|
||||
# Retricted to user updates for now, always public
|
||||
@spec update(User.t(), Object.t()) :: {:ok, map(), keyword()}
|
||||
def update(actor, object) do
|
||||
to = [Pleroma.Constants.as_public(), actor.follower_address]
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
"id" => Utils.generate_activity_id(),
|
||||
"type" => "Update",
|
||||
"actor" => actor.ap_id,
|
||||
"object" => object,
|
||||
"to" => to
|
||||
}, []}
|
||||
end
|
||||
|
||||
@spec block(User.t(), User.t()) :: {:ok, map(), keyword()}
|
||||
def block(blocker, blocked) do
|
||||
{:ok,
|
||||
%{
|
||||
"id" => Utils.generate_activity_id(),
|
||||
"type" => "Block",
|
||||
"actor" => blocker.ap_id,
|
||||
"object" => blocked.ap_id,
|
||||
"to" => [blocked.ap_id]
|
||||
}, []}
|
||||
end
|
||||
|
||||
@spec announce(User.t(), Object.t(), keyword()) :: {:ok, map(), keyword()}
|
||||
def announce(actor, object, options \\ []) do
|
||||
public? = Keyword.get(options, :public, false)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ defmodule Pleroma.Web.ActivityPub.MRF do
|
|||
def filter(%{} = object), do: get_policies() |> filter(object)
|
||||
|
||||
def get_policies do
|
||||
Pleroma.Config.get([:instance, :rewrite_policy], []) |> get_policies()
|
||||
Pleroma.Config.get([:mrf, :policies], []) |> get_policies()
|
||||
end
|
||||
|
||||
defp get_policies(policy) when is_atom(policy), do: [policy]
|
||||
|
|
@ -51,7 +51,7 @@ defmodule Pleroma.Web.ActivityPub.MRF do
|
|||
get_policies()
|
||||
|> Enum.map(fn policy -> to_string(policy) |> String.split(".") |> List.last() end)
|
||||
|
||||
exclusions = Pleroma.Config.get([:instance, :mrf_transparency_exclusions])
|
||||
exclusions = Pleroma.Config.get([:mrf, :transparency_exclusions])
|
||||
|
||||
base =
|
||||
%{
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy do
|
|||
if score < 0.8 do
|
||||
{:ok, message}
|
||||
else
|
||||
{:reject, nil}
|
||||
{:reject, "[AntiFollowbotPolicy] Scored #{actor_id} as #{score}"}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -27,23 +27,25 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy do
|
|||
|
||||
@impl true
|
||||
def filter(%{"type" => "Create", "actor" => actor, "object" => object} = message) do
|
||||
with {:ok, %User{} = u} <- User.get_or_fetch_by_ap_id(actor),
|
||||
with {:ok, %User{local: false} = u} <- User.get_or_fetch_by_ap_id(actor),
|
||||
{:contains_links, true} <- {:contains_links, contains_links?(object)},
|
||||
{:old_user, true} <- {:old_user, old_user?(u)} do
|
||||
{:ok, message}
|
||||
else
|
||||
{:ok, %User{local: true}} ->
|
||||
{:ok, message}
|
||||
|
||||
{:contains_links, false} ->
|
||||
{:ok, message}
|
||||
|
||||
{:old_user, false} ->
|
||||
{:reject, nil}
|
||||
{:reject, "[AntiLinkSpamPolicy] User has no posts nor followers"}
|
||||
|
||||
{:error, _} ->
|
||||
{:reject, nil}
|
||||
{:reject, "[AntiLinkSpamPolicy] Failed to get or fetch user by ap_id"}
|
||||
|
||||
e ->
|
||||
Logger.warn("[MRF anti-link-spam] WTF: unhandled error #{inspect(e)}")
|
||||
{:reject, nil}
|
||||
{:reject, "[AntiLinkSpamPolicy] Unhandled error #{inspect(e)}"}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do
|
|||
defp reject_message(message, threshold) when threshold > 0 do
|
||||
with {_, recipients} <- get_recipient_count(message) do
|
||||
if recipients > threshold do
|
||||
{:reject, nil}
|
||||
{:reject, "[HellthreadPolicy] #{recipients} recipients is over the limit of #{threshold}"}
|
||||
else
|
||||
{:ok, message}
|
||||
end
|
||||
|
|
@ -87,7 +87,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do
|
|||
{:ok, message} <- delist_message(message, delist_threshold) do
|
||||
{:ok, message}
|
||||
else
|
||||
_e -> {:reject, nil}
|
||||
e -> e
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
|
|||
if Enum.any?(Pleroma.Config.get([:mrf_keyword, :reject]), fn pattern ->
|
||||
string_matches?(content, pattern) or string_matches?(summary, pattern)
|
||||
end) do
|
||||
{:reject, nil}
|
||||
{:reject, "[KeywordPolicy] Matches with rejected keyword"}
|
||||
else
|
||||
{:ok, message}
|
||||
end
|
||||
|
|
@ -89,8 +89,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
|
|||
{:ok, message} <- check_replace(message) do
|
||||
{:ok, message}
|
||||
else
|
||||
_e ->
|
||||
{:reject, nil}
|
||||
{:reject, nil} -> {:reject, "[KeywordPolicy] "}
|
||||
{:reject, _} = e -> e
|
||||
_e -> {:reject, "[KeywordPolicy] "}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -12,8 +12,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.MentionPolicy do
|
|||
reject_actors = Pleroma.Config.get([:mrf_mention, :actors], [])
|
||||
recipients = (message["to"] || []) ++ (message["cc"] || [])
|
||||
|
||||
if Enum.any?(recipients, fn recipient -> Enum.member?(reject_actors, recipient) end) do
|
||||
{:reject, nil}
|
||||
if rejected_mention =
|
||||
Enum.find(recipients, fn recipient -> Enum.member?(reject_actors, recipient) end) do
|
||||
{:reject, "[MentionPolicy] Rejected for mention of #{rejected_mention}"}
|
||||
else
|
||||
{:ok, message}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
|
|||
|
||||
defp check_reject(message, actions) do
|
||||
if :reject in actions do
|
||||
{:reject, nil}
|
||||
{:reject, "[ObjectAgePolicy]"}
|
||||
else
|
||||
{:ok, message}
|
||||
end
|
||||
|
|
@ -47,9 +47,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
|
|||
|
||||
{:ok, message}
|
||||
else
|
||||
# Unhandleable error: somebody is messing around, just drop the message.
|
||||
_e ->
|
||||
{:reject, nil}
|
||||
{:reject, "[ObjectAgePolicy] Unhandled error"}
|
||||
end
|
||||
else
|
||||
{:ok, message}
|
||||
|
|
@ -69,9 +68,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
|
|||
|
||||
{:ok, message}
|
||||
else
|
||||
# Unhandleable error: somebody is messing around, just drop the message.
|
||||
_e ->
|
||||
{:reject, nil}
|
||||
{:reject, "[ObjectAgePolicy] Unhandled error"}
|
||||
end
|
||||
else
|
||||
{:ok, message}
|
||||
|
|
@ -98,7 +96,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
|
|||
@impl true
|
||||
def describe do
|
||||
mrf_object_age =
|
||||
Pleroma.Config.get(:mrf_object_age)
|
||||
Config.get(:mrf_object_age)
|
||||
|> Enum.into(%{})
|
||||
|
||||
{:ok, %{mrf_object_age: mrf_object_age}}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do
|
|||
{:ok, object}
|
||||
|
||||
true ->
|
||||
{:reject, nil}
|
||||
{:reject, "[RejectNonPublic] visibility: #{visibility}"}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -47,5 +47,5 @@ defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do
|
|||
|
||||
@impl true
|
||||
def describe,
|
||||
do: {:ok, %{mrf_rejectnonpublic: Pleroma.Config.get(:mrf_rejectnonpublic) |> Enum.into(%{})}}
|
||||
do: {:ok, %{mrf_rejectnonpublic: Config.get(:mrf_rejectnonpublic) |> Enum.into(%{})}}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,33 +3,35 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.MRF
|
||||
@moduledoc "Filter activities depending on their origin instance"
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF
|
||||
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.MRF
|
||||
|
||||
require Pleroma.Constants
|
||||
|
||||
defp check_accept(%{host: actor_host} = _actor_info, object) do
|
||||
accepts =
|
||||
Pleroma.Config.get([:mrf_simple, :accept])
|
||||
Config.get([:mrf_simple, :accept])
|
||||
|> MRF.subdomains_regex()
|
||||
|
||||
cond do
|
||||
accepts == [] -> {:ok, object}
|
||||
actor_host == Pleroma.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}
|
||||
true -> {:reject, nil}
|
||||
true -> {:reject, "[SimplePolicy] host not in accept list"}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_reject(%{host: actor_host} = _actor_info, object) do
|
||||
rejects =
|
||||
Pleroma.Config.get([:mrf_simple, :reject])
|
||||
Config.get([:mrf_simple, :reject])
|
||||
|> MRF.subdomains_regex()
|
||||
|
||||
if MRF.subdomain_match?(rejects, actor_host) do
|
||||
{:reject, nil}
|
||||
{:reject, "[SimplePolicy] host in reject list"}
|
||||
else
|
||||
{:ok, object}
|
||||
end
|
||||
|
|
@ -41,7 +43,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|
|||
)
|
||||
when length(child_attachment) > 0 do
|
||||
media_removal =
|
||||
Pleroma.Config.get([:mrf_simple, :media_removal])
|
||||
Config.get([:mrf_simple, :media_removal])
|
||||
|> MRF.subdomains_regex()
|
||||
|
||||
object =
|
||||
|
|
@ -65,7 +67,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|
|||
} = object
|
||||
) do
|
||||
media_nsfw =
|
||||
Pleroma.Config.get([:mrf_simple, :media_nsfw])
|
||||
Config.get([:mrf_simple, :media_nsfw])
|
||||
|> MRF.subdomains_regex()
|
||||
|
||||
object =
|
||||
|
|
@ -85,7 +87,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|
|||
|
||||
defp check_ftl_removal(%{host: actor_host} = _actor_info, object) do
|
||||
timeline_removal =
|
||||
Pleroma.Config.get([:mrf_simple, :federated_timeline_removal])
|
||||
Config.get([:mrf_simple, :federated_timeline_removal])
|
||||
|> MRF.subdomains_regex()
|
||||
|
||||
object =
|
||||
|
|
@ -108,11 +110,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|
|||
|
||||
defp check_report_removal(%{host: actor_host} = _actor_info, %{"type" => "Flag"} = object) do
|
||||
report_removal =
|
||||
Pleroma.Config.get([:mrf_simple, :report_removal])
|
||||
Config.get([:mrf_simple, :report_removal])
|
||||
|> MRF.subdomains_regex()
|
||||
|
||||
if MRF.subdomain_match?(report_removal, actor_host) do
|
||||
{:reject, nil}
|
||||
{:reject, "[SimplePolicy] host in report_removal list"}
|
||||
else
|
||||
{:ok, object}
|
||||
end
|
||||
|
|
@ -122,7 +124,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|
|||
|
||||
defp check_avatar_removal(%{host: actor_host} = _actor_info, %{"icon" => _icon} = object) do
|
||||
avatar_removal =
|
||||
Pleroma.Config.get([:mrf_simple, :avatar_removal])
|
||||
Config.get([:mrf_simple, :avatar_removal])
|
||||
|> MRF.subdomains_regex()
|
||||
|
||||
if MRF.subdomain_match?(avatar_removal, actor_host) do
|
||||
|
|
@ -136,7 +138,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|
|||
|
||||
defp check_banner_removal(%{host: actor_host} = _actor_info, %{"image" => _image} = object) do
|
||||
banner_removal =
|
||||
Pleroma.Config.get([:mrf_simple, :banner_removal])
|
||||
Config.get([:mrf_simple, :banner_removal])
|
||||
|> MRF.subdomains_regex()
|
||||
|
||||
if MRF.subdomain_match?(banner_removal, actor_host) do
|
||||
|
|
@ -153,11 +155,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|
|||
%{host: actor_host} = URI.parse(actor)
|
||||
|
||||
reject_deletes =
|
||||
Pleroma.Config.get([:mrf_simple, :reject_deletes])
|
||||
Config.get([:mrf_simple, :reject_deletes])
|
||||
|> MRF.subdomains_regex()
|
||||
|
||||
if MRF.subdomain_match?(reject_deletes, actor_host) do
|
||||
{:reject, nil}
|
||||
{:reject, "[SimplePolicy] host in reject_deletes list"}
|
||||
else
|
||||
{:ok, object}
|
||||
end
|
||||
|
|
@ -175,7 +177,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|
|||
{:ok, object} <- check_report_removal(actor_info, object) do
|
||||
{:ok, object}
|
||||
else
|
||||
_e -> {:reject, nil}
|
||||
{:reject, nil} -> {:reject, "[SimplePolicy]"}
|
||||
{:reject, _} = e -> e
|
||||
_ -> {:reject, "[SimplePolicy]"}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -189,7 +193,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|
|||
{:ok, object} <- check_banner_removal(actor_info, object) do
|
||||
{:ok, object}
|
||||
else
|
||||
_e -> {:reject, nil}
|
||||
{:reject, nil} -> {:reject, "[SimplePolicy]"}
|
||||
{:reject, _} = e -> e
|
||||
_ -> {:reject, "[SimplePolicy]"}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -197,10 +203,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|
|||
|
||||
@impl true
|
||||
def describe do
|
||||
exclusions = Pleroma.Config.get([:instance, :mrf_transparency_exclusions])
|
||||
exclusions = Config.get([:mrf, :transparency_exclusions])
|
||||
|
||||
mrf_simple =
|
||||
Pleroma.Config.get(:mrf_simple)
|
||||
Config.get(:mrf_simple)
|
||||
|> Enum.map(fn {k, v} -> {k, Enum.reject(v, fn v -> v in exclusions end)} end)
|
||||
|> Enum.into(%{})
|
||||
|
||||
|
|
|
|||
|
|
@ -134,12 +134,13 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do
|
|||
if user.local == true do
|
||||
{:ok, message}
|
||||
else
|
||||
{:reject, nil}
|
||||
{:reject,
|
||||
"[TagPolicy] Follow from #{actor} tagged with mrf_tag:disable-remote-subscription"}
|
||||
end
|
||||
end
|
||||
|
||||
defp process_tag("mrf_tag:disable-any-subscription", %{"type" => "Follow"}),
|
||||
do: {:reject, nil}
|
||||
defp process_tag("mrf_tag:disable-any-subscription", %{"type" => "Follow", "actor" => actor}),
|
||||
do: {:reject, "[TagPolicy] Follow from #{actor} tagged with mrf_tag:disable-any-subscription"}
|
||||
|
||||
defp process_tag(_, message), do: {:ok, message}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy do
|
|||
if actor in allow_list do
|
||||
{:ok, object}
|
||||
else
|
||||
{:reject, nil}
|
||||
{:reject, "[UserAllowListPolicy] #{actor} not in the list"}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -11,22 +11,26 @@ defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicy do
|
|||
with {:ok, _} <- filter(child_message) do
|
||||
{:ok, message}
|
||||
else
|
||||
{:reject, nil} ->
|
||||
{:reject, nil}
|
||||
{:reject, _} = e -> e
|
||||
end
|
||||
end
|
||||
|
||||
def filter(%{"type" => message_type} = message) do
|
||||
with accepted_vocabulary <- Pleroma.Config.get([:mrf_vocabulary, :accept]),
|
||||
rejected_vocabulary <- Pleroma.Config.get([:mrf_vocabulary, :reject]),
|
||||
true <-
|
||||
Enum.empty?(accepted_vocabulary) || Enum.member?(accepted_vocabulary, message_type),
|
||||
false <-
|
||||
length(rejected_vocabulary) > 0 && Enum.member?(rejected_vocabulary, message_type),
|
||||
{_, true} <-
|
||||
{:accepted,
|
||||
Enum.empty?(accepted_vocabulary) || Enum.member?(accepted_vocabulary, message_type)},
|
||||
{_, false} <-
|
||||
{:rejected,
|
||||
length(rejected_vocabulary) > 0 && Enum.member?(rejected_vocabulary, message_type)},
|
||||
{:ok, _} <- filter(message["object"]) do
|
||||
{:ok, message}
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -13,16 +13,58 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
|
|||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.FollowValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator
|
||||
|
||||
@spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
|
||||
def validate(object, meta)
|
||||
|
||||
def validate(%{"type" => "Follow"} = object, meta) do
|
||||
with {:ok, object} <-
|
||||
object
|
||||
|> FollowValidator.cast_and_validate()
|
||||
|> Ecto.Changeset.apply_action(:insert) do
|
||||
object = stringify_keys(object)
|
||||
{:ok, object, meta}
|
||||
end
|
||||
end
|
||||
|
||||
def validate(%{"type" => "Block"} = block_activity, meta) do
|
||||
with {:ok, block_activity} <-
|
||||
block_activity
|
||||
|> BlockValidator.cast_and_validate()
|
||||
|> Ecto.Changeset.apply_action(:insert) do
|
||||
block_activity = stringify_keys(block_activity)
|
||||
outgoing_blocks = Pleroma.Config.get([:activitypub, :outgoing_blocks])
|
||||
|
||||
meta =
|
||||
if !outgoing_blocks do
|
||||
Keyword.put(meta, :do_not_federate, true)
|
||||
else
|
||||
meta
|
||||
end
|
||||
|
||||
{:ok, block_activity, meta}
|
||||
end
|
||||
end
|
||||
|
||||
def validate(%{"type" => "Update"} = update_activity, meta) do
|
||||
with {:ok, update_activity} <-
|
||||
update_activity
|
||||
|> UpdateValidator.cast_and_validate()
|
||||
|> Ecto.Changeset.apply_action(:insert) do
|
||||
update_activity = stringify_keys(update_activity)
|
||||
{:ok, update_activity, meta}
|
||||
end
|
||||
end
|
||||
|
||||
def validate(%{"type" => "Undo"} = object, meta) do
|
||||
with {:ok, object} <-
|
||||
object
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator do
|
||||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||
|
||||
import Ecto.Changeset
|
||||
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
|
||||
|
||||
@primary_key false
|
||||
|
||||
embedded_schema do
|
||||
field(:id, ObjectValidators.ObjectID, primary_key: true)
|
||||
field(:type, :string)
|
||||
field(:actor, ObjectValidators.ObjectID)
|
||||
field(:to, ObjectValidators.Recipients, default: [])
|
||||
field(:cc, ObjectValidators.Recipients, default: [])
|
||||
field(:object, ObjectValidators.ObjectID)
|
||||
end
|
||||
|
||||
def cast_data(data) do
|
||||
%__MODULE__{}
|
||||
|> cast(data, __schema__(:fields))
|
||||
end
|
||||
|
||||
def validate_data(cng) do
|
||||
cng
|
||||
|> validate_required([:id, :type, :actor, :to, :cc, :object])
|
||||
|> validate_inclusion(:type, ["Block"])
|
||||
|> validate_actor_presence()
|
||||
|> validate_actor_presence(field_name: :object)
|
||||
end
|
||||
|
||||
def cast_and_validate(data) do
|
||||
data
|
||||
|> cast_data
|
||||
|> validate_data
|
||||
end
|
||||
end
|
||||
|
|
@ -93,12 +93,14 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator do
|
|||
- If both users are in our system
|
||||
- If at least one of the users in this ChatMessage is a local user
|
||||
- If the recipient is not blocking the actor
|
||||
- If the recipient is explicitly not accepting chat messages
|
||||
"""
|
||||
def validate_local_concern(cng) do
|
||||
with actor_ap <- get_field(cng, :actor),
|
||||
{_, %User{} = actor} <- {:find_actor, User.get_cached_by_ap_id(actor_ap)},
|
||||
{_, %User{} = recipient} <-
|
||||
{:find_recipient, User.get_cached_by_ap_id(get_field(cng, :to) |> hd())},
|
||||
{_, false} <- {:not_accepting_chats?, recipient.accepts_chat_messages == false},
|
||||
{_, false} <- {:blocking_actor?, User.blocks?(recipient, actor)},
|
||||
{_, true} <- {:local?, Enum.any?([actor, recipient], & &1.local)} do
|
||||
cng
|
||||
|
|
@ -107,6 +109,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator do
|
|||
cng
|
||||
|> add_error(:actor, "actor is blocked by recipient")
|
||||
|
||||
{:not_accepting_chats?, true} ->
|
||||
cng
|
||||
|> add_error(:to, "recipient does not accept chat messages")
|
||||
|
||||
{:local?, false} ->
|
||||
cng
|
||||
|> add_error(:actor, "actor and recipient are both remote")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.FollowValidator do
|
||||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||
|
||||
import Ecto.Changeset
|
||||
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
|
||||
|
||||
@primary_key false
|
||||
|
||||
embedded_schema do
|
||||
field(:id, ObjectValidators.ObjectID, primary_key: true)
|
||||
field(:type, :string)
|
||||
field(:actor, ObjectValidators.ObjectID)
|
||||
field(:to, ObjectValidators.Recipients, default: [])
|
||||
field(:cc, ObjectValidators.Recipients, default: [])
|
||||
field(:object, ObjectValidators.ObjectID)
|
||||
field(:state, :string, default: "pending")
|
||||
end
|
||||
|
||||
def cast_data(data) do
|
||||
%__MODULE__{}
|
||||
|> cast(data, __schema__(:fields))
|
||||
end
|
||||
|
||||
def validate_data(cng) do
|
||||
cng
|
||||
|> validate_required([:id, :type, :actor, :to, :cc, :object])
|
||||
|> validate_inclusion(:type, ["Follow"])
|
||||
|> validate_inclusion(:state, ~w{pending reject accept})
|
||||
|> validate_actor_presence()
|
||||
|> validate_actor_presence(field_name: :object)
|
||||
end
|
||||
|
||||
def cast_and_validate(data) do
|
||||
data
|
||||
|> cast_data
|
||||
|> validate_data
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator do
|
||||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||
|
||||
import Ecto.Changeset
|
||||
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
|
||||
|
||||
@primary_key false
|
||||
|
||||
embedded_schema do
|
||||
field(:id, ObjectValidators.ObjectID, primary_key: true)
|
||||
field(:type, :string)
|
||||
field(:actor, ObjectValidators.ObjectID)
|
||||
field(:to, ObjectValidators.Recipients, default: [])
|
||||
field(:cc, ObjectValidators.Recipients, default: [])
|
||||
# In this case, we save the full object in this activity instead of just a
|
||||
# reference, so we can always see what was actually changed by this.
|
||||
field(:object, :map)
|
||||
end
|
||||
|
||||
def cast_data(data) do
|
||||
%__MODULE__{}
|
||||
|> cast(data, __schema__(:fields))
|
||||
end
|
||||
|
||||
def validate_data(cng) do
|
||||
cng
|
||||
|> validate_required([:id, :type, :actor, :to, :cc, :object])
|
||||
|> validate_inclusion(:type, ["Update"])
|
||||
|> validate_actor_presence()
|
||||
|> validate_updating_rights()
|
||||
end
|
||||
|
||||
def cast_and_validate(data) do
|
||||
data
|
||||
|> cast_data
|
||||
|> validate_data
|
||||
end
|
||||
|
||||
# For now we only support updating users, and here the rule is easy:
|
||||
# object id == actor id
|
||||
def validate_updating_rights(cng) do
|
||||
with actor = get_field(cng, :actor),
|
||||
object = get_field(cng, :object),
|
||||
{:ok, object_id} <- ObjectValidators.ObjectID.cast(object),
|
||||
true <- actor == object_id do
|
||||
cng
|
||||
else
|
||||
_e ->
|
||||
cng
|
||||
|> add_error(:object, "Can't be updated by this actor")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -49,7 +49,8 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|
|||
"""
|
||||
def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = params) do
|
||||
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())
|
||||
|
||||
|
|
@ -57,8 +58,8 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|
|||
|
||||
signature =
|
||||
Pleroma.Signature.sign(actor, %{
|
||||
"(request-target)": "post #{path}",
|
||||
host: host,
|
||||
"(request-target)": "post #{uri.path}",
|
||||
host: signature_host(uri),
|
||||
"content-length": byte_size(json),
|
||||
digest: digest,
|
||||
date: date
|
||||
|
|
@ -76,8 +77,9 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|
|||
{"digest", digest}
|
||||
]
|
||||
) do
|
||||
if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since],
|
||||
do: Instances.set_reachable(inbox)
|
||||
if not Map.has_key?(params, :unreachable_since) || params[:unreachable_since] do
|
||||
Instances.set_reachable(inbox)
|
||||
end
|
||||
|
||||
result
|
||||
else
|
||||
|
|
@ -96,6 +98,14 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|
|||
|> publish_one()
|
||||
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
|
||||
if public do
|
||||
true
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ defmodule Pleroma.Web.ActivityPub.Relay do
|
|||
def follow(target_instance) do
|
||||
with %User{} = local_user <- get_actor(),
|
||||
{:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_instance),
|
||||
{:ok, activity} <- ActivityPub.follow(local_user, target_user) do
|
||||
{:ok, _, _, activity} <- CommonAPI.follow(local_user, target_user) do
|
||||
Logger.info("relay: followed instance: #{target_instance}; id=#{activity.data["id"]}")
|
||||
{:ok, activity}
|
||||
else
|
||||
|
|
|
|||
|
|
@ -6,8 +6,10 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
collection, and so on.
|
||||
"""
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Activity.Ir.Topics
|
||||
alias Pleroma.Chat
|
||||
alias Pleroma.Chat.MessageReference
|
||||
alias Pleroma.FollowingRelationship
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
|
|
@ -20,6 +22,104 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
|
||||
def handle(object, meta \\ [])
|
||||
|
||||
# Tasks this handle
|
||||
# - Follows if possible
|
||||
# - Sends a notification
|
||||
# - Generates accept or reject if appropriate
|
||||
def handle(
|
||||
%{
|
||||
data: %{
|
||||
"id" => follow_id,
|
||||
"type" => "Follow",
|
||||
"object" => followed_user,
|
||||
"actor" => following_user
|
||||
}
|
||||
} = object,
|
||||
meta
|
||||
) do
|
||||
with %User{} = follower <- User.get_cached_by_ap_id(following_user),
|
||||
%User{} = followed <- User.get_cached_by_ap_id(followed_user),
|
||||
{_, {:ok, _}, _, _} <-
|
||||
{:following, User.follow(follower, followed, :follow_pending), follower, followed} do
|
||||
if followed.local && !followed.locked do
|
||||
Utils.update_follow_state_for_all(object, "accept")
|
||||
FollowingRelationship.update(follower, followed, :follow_accept)
|
||||
User.update_follower_count(followed)
|
||||
User.update_following_count(follower)
|
||||
|
||||
%{
|
||||
to: [following_user],
|
||||
actor: followed,
|
||||
object: follow_id,
|
||||
local: true
|
||||
}
|
||||
|> ActivityPub.accept()
|
||||
end
|
||||
else
|
||||
{:following, {:error, _}, follower, followed} ->
|
||||
Utils.update_follow_state_for_all(object, "reject")
|
||||
FollowingRelationship.update(follower, followed, :follow_reject)
|
||||
|
||||
if followed.local do
|
||||
%{
|
||||
to: [follower.ap_id],
|
||||
actor: followed,
|
||||
object: follow_id,
|
||||
local: true
|
||||
}
|
||||
|> ActivityPub.reject()
|
||||
end
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
|
||||
{:ok, notifications} = Notification.create_notifications(object, do_send: false)
|
||||
|
||||
meta =
|
||||
meta
|
||||
|> add_notifications(notifications)
|
||||
|
||||
updated_object = Activity.get_by_ap_id(follow_id)
|
||||
|
||||
{:ok, updated_object, meta}
|
||||
end
|
||||
|
||||
# Tasks this handles:
|
||||
# - Unfollow and block
|
||||
def handle(
|
||||
%{data: %{"type" => "Block", "object" => blocked_user, "actor" => blocking_user}} =
|
||||
object,
|
||||
meta
|
||||
) do
|
||||
with %User{} = blocker <- User.get_cached_by_ap_id(blocking_user),
|
||||
%User{} = blocked <- User.get_cached_by_ap_id(blocked_user) do
|
||||
User.block(blocker, blocked)
|
||||
end
|
||||
|
||||
{:ok, object, meta}
|
||||
end
|
||||
|
||||
# Tasks this handles:
|
||||
# - Update the user
|
||||
#
|
||||
# For a local user, we also get a changeset with the full information, so we
|
||||
# can update non-federating, non-activitypub settings as well.
|
||||
def handle(%{data: %{"type" => "Update", "object" => updated_object}} = object, meta) do
|
||||
if changeset = Keyword.get(meta, :user_update_changeset) do
|
||||
changeset
|
||||
|> User.update_and_set_cache()
|
||||
else
|
||||
{:ok, new_user_data} = ActivityPub.user_data_from_user_object(updated_object)
|
||||
|
||||
User.get_by_ap_id(updated_object["id"])
|
||||
|> User.remote_user_changeset(new_user_data)
|
||||
|> User.update_and_set_cache()
|
||||
end
|
||||
|
||||
{:ok, object, meta}
|
||||
end
|
||||
|
||||
# Tasks this handles:
|
||||
# - Add like to object
|
||||
# - Set up notification
|
||||
|
|
@ -62,7 +162,10 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
|
||||
if !User.is_internal_user?(user) do
|
||||
Notification.create_notifications(object)
|
||||
ActivityPub.stream_out(object)
|
||||
|
||||
object
|
||||
|> Topics.get_activity_topics()
|
||||
|> Streamer.stream(object)
|
||||
end
|
||||
|
||||
{:ok, object, meta}
|
||||
|
|
@ -170,14 +273,20 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
{:ok, object}
|
||||
end
|
||||
|
||||
def handle_undoing(%{data: %{"type" => "Like"}} = object) do
|
||||
with %Object{} = liked_object <- Object.get_by_ap_id(object.data["object"]),
|
||||
{:ok, _} <- Utils.remove_like_from_object(object, liked_object),
|
||||
{:ok, _} <- Repo.delete(object) do
|
||||
:ok
|
||||
defp undo_like(nil, object), do: delete_object(object)
|
||||
|
||||
defp undo_like(%Object{} = liked_object, object) do
|
||||
with {:ok, _} <- Utils.remove_like_from_object(object, liked_object) do
|
||||
delete_object(object)
|
||||
end
|
||||
end
|
||||
|
||||
def handle_undoing(%{data: %{"type" => "Like"}} = object) do
|
||||
object.data["object"]
|
||||
|> Object.get_by_ap_id()
|
||||
|> undo_like(object)
|
||||
end
|
||||
|
||||
def handle_undoing(%{data: %{"type" => "EmojiReact"}} = object) do
|
||||
with %Object{} = reacted_object <- Object.get_by_ap_id(object.data["object"]),
|
||||
{:ok, _} <- Utils.remove_emoji_reaction_from_object(object, reacted_object),
|
||||
|
|
@ -207,6 +316,11 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
|
||||
def handle_undoing(object), do: {:error, ["don't know how to handle", object]}
|
||||
|
||||
@spec delete_object(Object.t()) :: :ok | {:error, Ecto.Changeset.t()}
|
||||
defp delete_object(object) do
|
||||
with {:ok, _} <- Repo.delete(object), do: :ok
|
||||
end
|
||||
|
||||
defp send_notifications(meta) do
|
||||
Keyword.get(meta, :notifications, [])
|
||||
|> Enum.each(fn notification ->
|
||||
|
|
|
|||
|
|
@ -62,15 +62,17 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
def fix_summary(object), do: Map.put(object, "summary", "")
|
||||
|
||||
def fix_addressing_list(map, field) do
|
||||
cond do
|
||||
is_binary(map[field]) ->
|
||||
Map.put(map, field, [map[field]])
|
||||
addrs = map[field]
|
||||
|
||||
is_nil(map[field]) ->
|
||||
Map.put(map, field, [])
|
||||
cond do
|
||||
is_list(addrs) ->
|
||||
Map.put(map, field, Enum.filter(addrs, &is_binary/1))
|
||||
|
||||
is_binary(addrs) ->
|
||||
Map.put(map, field, [addrs])
|
||||
|
||||
true ->
|
||||
map
|
||||
Map.put(map, field, [])
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -176,7 +178,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|> Map.drop(["conversation"])
|
||||
else
|
||||
e ->
|
||||
Logger.error("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
|
||||
Logger.warn("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
|
||||
object
|
||||
end
|
||||
else
|
||||
|
|
@ -233,18 +235,24 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
is_map(url) && is_binary(url["href"]) -> url["href"]
|
||||
is_binary(data["url"]) -> data["url"]
|
||||
is_binary(data["href"]) -> data["href"]
|
||||
true -> nil
|
||||
end
|
||||
|
||||
attachment_url =
|
||||
%{"href" => href}
|
||||
|> Maps.put_if_present("mediaType", media_type)
|
||||
|> Maps.put_if_present("type", Map.get(url || %{}, "type"))
|
||||
if href do
|
||||
attachment_url =
|
||||
%{"href" => href}
|
||||
|> Maps.put_if_present("mediaType", media_type)
|
||||
|> Maps.put_if_present("type", Map.get(url || %{}, "type"))
|
||||
|
||||
%{"url" => [attachment_url]}
|
||||
|> Maps.put_if_present("mediaType", media_type)
|
||||
|> Maps.put_if_present("type", data["type"])
|
||||
|> Maps.put_if_present("name", data["name"])
|
||||
%{"url" => [attachment_url]}
|
||||
|> Maps.put_if_present("mediaType", media_type)
|
||||
|> Maps.put_if_present("type", data["type"])
|
||||
|> Maps.put_if_present("name", data["name"])
|
||||
else
|
||||
nil
|
||||
end
|
||||
end)
|
||||
|> Enum.filter(& &1)
|
||||
|
||||
Map.put(object, "attachment", attachments)
|
||||
end
|
||||
|
|
@ -263,12 +271,18 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|
||||
def fix_url(%{"type" => object_type, "url" => url} = object)
|
||||
when object_type in ["Video", "Audio"] and is_list(url) do
|
||||
first_element = Enum.at(url, 0)
|
||||
attachment =
|
||||
Enum.find(url, fn x ->
|
||||
media_type = x["mediaType"] || x["mimeType"] || ""
|
||||
|
||||
link_element = Enum.find(url, fn x -> is_map(x) and x["mimeType"] == "text/html" end)
|
||||
is_map(x) and String.starts_with?(media_type, ["audio/", "video/"])
|
||||
end)
|
||||
|
||||
link_element =
|
||||
Enum.find(url, fn x -> is_map(x) and (x["mediaType"] || x["mimeType"]) == "text/html" end)
|
||||
|
||||
object
|
||||
|> Map.put("attachment", [first_element])
|
||||
|> Map.put("attachment", [attachment])
|
||||
|> Map.put("url", link_element["href"])
|
||||
end
|
||||
|
||||
|
|
@ -446,12 +460,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
when objtype in ["Article", "Event", "Note", "Video", "Page", "Question", "Answer", "Audio"] do
|
||||
actor = Containment.get_actor(data)
|
||||
|
||||
data =
|
||||
Map.put(data, "actor", actor)
|
||||
|> fix_addressing
|
||||
|
||||
with nil <- Activity.get_create_by_object_ap_id(object["id"]),
|
||||
{:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
|
||||
{:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(actor),
|
||||
data <- Map.put(data, "actor", actor) |> fix_addressing() do
|
||||
object = fix_object(object, options)
|
||||
|
||||
params = %{
|
||||
|
|
@ -520,66 +531,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data,
|
||||
_options
|
||||
) do
|
||||
with %User{local: true} = followed <-
|
||||
User.get_cached_by_ap_id(Containment.get_actor(%{"actor" => followed})),
|
||||
{:ok, %User{} = follower} <-
|
||||
User.get_or_fetch_by_ap_id(Containment.get_actor(%{"actor" => follower})),
|
||||
{:ok, activity} <-
|
||||
ActivityPub.follow(follower, followed, id, false, skip_notify_and_stream: true) do
|
||||
with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
|
||||
{_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
|
||||
{_, false} <- {:user_locked, User.locked?(followed)},
|
||||
{_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
|
||||
{_, {:ok, _}} <-
|
||||
{:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")},
|
||||
{:ok, _relationship} <-
|
||||
FollowingRelationship.update(follower, followed, :follow_accept) do
|
||||
ActivityPub.accept(%{
|
||||
to: [follower.ap_id],
|
||||
actor: followed,
|
||||
object: data,
|
||||
local: true
|
||||
})
|
||||
else
|
||||
{:user_blocked, true} ->
|
||||
{:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
|
||||
{:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject)
|
||||
|
||||
ActivityPub.reject(%{
|
||||
to: [follower.ap_id],
|
||||
actor: followed,
|
||||
object: data,
|
||||
local: true
|
||||
})
|
||||
|
||||
{:follow, {:error, _}} ->
|
||||
{:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
|
||||
{:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject)
|
||||
|
||||
ActivityPub.reject(%{
|
||||
to: [follower.ap_id],
|
||||
actor: followed,
|
||||
object: data,
|
||||
local: true
|
||||
})
|
||||
|
||||
{:user_locked, true} ->
|
||||
{:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_pending)
|
||||
:noop
|
||||
end
|
||||
|
||||
ActivityPub.notify_and_stream(activity)
|
||||
{:ok, activity}
|
||||
else
|
||||
_e ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => id} = data,
|
||||
_options
|
||||
|
|
@ -673,7 +624,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
end
|
||||
|
||||
def handle_incoming(%{"type" => type} = data, _options)
|
||||
when type in ["Like", "EmojiReact", "Announce"] do
|
||||
when type in ~w{Like EmojiReact Announce} do
|
||||
with :ok <- ObjectValidator.fetch_actor_and_object(data),
|
||||
{:ok, activity, _meta} <-
|
||||
Pipeline.common_pipeline(data, local: false) do
|
||||
|
|
@ -684,35 +635,13 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
|
||||
data,
|
||||
%{"type" => type} = data,
|
||||
_options
|
||||
)
|
||||
when object_type in [
|
||||
"Person",
|
||||
"Application",
|
||||
"Service",
|
||||
"Organization"
|
||||
] do
|
||||
with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
|
||||
{:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
|
||||
|
||||
actor
|
||||
|> User.remote_user_changeset(new_user_data)
|
||||
|> User.update_and_set_cache()
|
||||
|
||||
ActivityPub.update(%{
|
||||
local: false,
|
||||
to: data["to"] || [],
|
||||
cc: data["cc"] || [],
|
||||
object: object,
|
||||
actor: actor_id,
|
||||
activity_id: data["id"]
|
||||
})
|
||||
else
|
||||
e ->
|
||||
Logger.error(e)
|
||||
:error
|
||||
when type in ~w{Update Block Follow} do
|
||||
with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
|
||||
{:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -788,21 +717,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
|
||||
_options
|
||||
) do
|
||||
with %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
|
||||
{:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
|
||||
{:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
|
||||
User.unfollow(blocker, blocked)
|
||||
User.block(blocker, blocked)
|
||||
{:ok, activity}
|
||||
else
|
||||
_e -> :error
|
||||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{
|
||||
"type" => "Move",
|
||||
|
|
|
|||
|
|
@ -719,15 +719,18 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
|
||||
case Activity.get_by_ap_id_with_object(id) do
|
||||
%Activity{} = activity ->
|
||||
activity_actor = User.get_by_ap_id(activity.object.data["actor"])
|
||||
|
||||
%{
|
||||
"type" => "Note",
|
||||
"id" => activity.data["id"],
|
||||
"content" => activity.object.data["content"],
|
||||
"published" => activity.object.data["published"],
|
||||
"actor" =>
|
||||
AccountView.render("show.json", %{
|
||||
user: User.get_by_ap_id(activity.object.data["actor"])
|
||||
})
|
||||
AccountView.render(
|
||||
"show.json",
|
||||
%{user: activity_actor, skip_visibility_check: true}
|
||||
)
|
||||
}
|
||||
|
||||
_ ->
|
||||
|
|
|
|||
|
|
@ -81,6 +81,15 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
|
||||
fields = Enum.map(user.fields, &Map.put(&1, "type", "PropertyValue"))
|
||||
|
||||
capabilities =
|
||||
if is_boolean(user.accepts_chat_messages) do
|
||||
%{
|
||||
"acceptsChatMessages" => user.accepts_chat_messages
|
||||
}
|
||||
else
|
||||
%{}
|
||||
end
|
||||
|
||||
%{
|
||||
"id" => user.ap_id,
|
||||
"type" => user.actor_type,
|
||||
|
|
@ -101,7 +110,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
"endpoints" => endpoints,
|
||||
"attachment" => fields,
|
||||
"tag" => emoji_tags,
|
||||
"discoverable" => user.discoverable
|
||||
"discoverable" => user.discoverable,
|
||||
"capabilities" => capabilities
|
||||
}
|
||||
|> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))
|
||||
|> Map.merge(maybe_make_image(&User.banner_url/2, "image", user))
|
||||
|
|
|
|||
|
|
@ -47,6 +47,10 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
|
|||
@spec visible_for_user?(Activity.t(), User.t() | nil) :: boolean()
|
||||
def visible_for_user?(%{actor: ap_id}, %User{ap_id: ap_id}), do: true
|
||||
|
||||
def visible_for_user?(nil, _), do: false
|
||||
|
||||
def visible_for_user?(%{data: %{"listMessage" => _}}, nil), do: false
|
||||
|
||||
def visible_for_user?(%{data: %{"listMessage" => list_ap_id}} = activity, %User{} = user) do
|
||||
user.ap_id in activity.data["to"] ||
|
||||
list_ap_id
|
||||
|
|
@ -54,8 +58,6 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
|
|||
|> Pleroma.List.member?(user)
|
||||
end
|
||||
|
||||
def visible_for_user?(%{data: %{"listMessage" => _}}, nil), do: false
|
||||
|
||||
def visible_for_user?(%{local: local} = activity, nil) do
|
||||
cfg_key =
|
||||
if local,
|
||||
|
|
|
|||
|
|
@ -206,8 +206,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
end
|
||||
end
|
||||
|
||||
def user_show(conn, %{"nickname" => nickname}) do
|
||||
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
|
||||
def user_show(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
|
||||
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do
|
||||
conn
|
||||
|> put_view(AccountView)
|
||||
|> render("show.json", %{user: user})
|
||||
|
|
@ -233,11 +233,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
|> render("index.json", %{activities: activities, as: :activity})
|
||||
end
|
||||
|
||||
def list_user_statuses(conn, %{"nickname" => nickname} = params) do
|
||||
def list_user_statuses(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname} = params) do
|
||||
with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
|
||||
godmode = params["godmode"] == "true" || params["godmode"] == true
|
||||
|
||||
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
|
||||
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do
|
||||
{_, page_size} = page_params(params)
|
||||
|
||||
activities =
|
||||
|
|
@ -345,7 +345,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)) do
|
||||
json(
|
||||
conn,
|
||||
AccountView.render("index.json", users: users, count: count, page_size: page_size)
|
||||
AccountView.render("index.json",
|
||||
users: users,
|
||||
count: count,
|
||||
page_size: page_size
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -526,7 +530,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
|
||||
@doc "Show a given user's credentials"
|
||||
def show_user_credentials(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
|
||||
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
|
||||
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do
|
||||
conn
|
||||
|> put_view(AccountView)
|
||||
|> render("credentials.json", %{user: user, for: admin})
|
||||
|
|
@ -616,37 +620,32 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
end
|
||||
|
||||
def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
|
||||
users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
|
||||
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
|
||||
|
||||
User.toggle_confirmation(users)
|
||||
|
||||
ModerationLog.insert_log(%{
|
||||
actor: admin,
|
||||
subject: users,
|
||||
action: "confirm_email"
|
||||
})
|
||||
ModerationLog.insert_log(%{actor: admin, subject: users, action: "confirm_email"})
|
||||
|
||||
json(conn, "")
|
||||
end
|
||||
|
||||
def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
|
||||
users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
|
||||
users =
|
||||
Enum.map(nicknames, fn nickname ->
|
||||
nickname
|
||||
|> User.get_cached_by_nickname()
|
||||
|> User.send_confirmation_email()
|
||||
end)
|
||||
|
||||
User.try_send_confirmation_email(users)
|
||||
|
||||
ModerationLog.insert_log(%{
|
||||
actor: admin,
|
||||
subject: users,
|
||||
action: "resend_confirmation_email"
|
||||
})
|
||||
ModerationLog.insert_log(%{actor: admin, subject: users, action: "resend_confirmation_email"})
|
||||
|
||||
json(conn, "")
|
||||
end
|
||||
|
||||
def stats(conn, _) do
|
||||
count = Stats.get_status_visibility_count()
|
||||
def stats(conn, params) do
|
||||
counters = Stats.get_status_visibility_count(params["instance"])
|
||||
|
||||
json(conn, %{"status_visibility" => count})
|
||||
json(conn, %{"status_visibility" => counters})
|
||||
end
|
||||
|
||||
defp page_params(params) do
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
|
|||
alias Pleroma.ConfigDB
|
||||
alias Pleroma.Plugs.OAuthScopesPlug
|
||||
|
||||
@descriptions Pleroma.Docs.JSON.compile()
|
||||
|
||||
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||
plug(OAuthScopesPlug, %{scopes: ["write"], admin: true} when action == :update)
|
||||
|
||||
|
|
@ -25,7 +23,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
|
|||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.ConfigOperation
|
||||
|
||||
def descriptions(conn, _params) do
|
||||
descriptions = Enum.filter(@descriptions, &whitelisted_config?/1)
|
||||
descriptions = Enum.filter(Pleroma.Docs.JSON.compiled_descriptions(), &whitelisted_config?/1)
|
||||
|
||||
json(conn, descriptions)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ defmodule Pleroma.Web.AdminAPI.AccountView do
|
|||
end
|
||||
|
||||
def merge_account_views(%User{} = user) do
|
||||
MastodonAPI.AccountView.render("show.json", %{user: user})
|
||||
MastodonAPI.AccountView.render("show.json", %{user: user, skip_visibility_check: true})
|
||||
|> Map.merge(AdminAPI.AccountView.render("show.json", %{user: user}))
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ defmodule Pleroma.Web.ApiSpec.CastAndValidate do
|
|||
|> List.first()
|
||||
|
||||
_ ->
|
||||
nil
|
||||
"application/json"
|
||||
end
|
||||
|
||||
private_data = Map.put(private_data, :operation_id, operation_id)
|
||||
|
|
|
|||
|
|
@ -29,6 +29,10 @@ defmodule Pleroma.Web.ApiSpec.Helpers do
|
|||
}
|
||||
end
|
||||
|
||||
def admin_api_params do
|
||||
[Operation.parameter(:admin_token, :query, :string, "Allows authorization via admin token.")]
|
||||
end
|
||||
|
||||
def pagination_params do
|
||||
[
|
||||
Operation.parameter(:max_id, :query, :string, "Return items older than this ID"),
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
|
|||
description: "Update the user's display and preferences.",
|
||||
operationId: "AccountController.update_credentials",
|
||||
security: [%{"oAuth" => ["write:accounts"]}],
|
||||
requestBody: request_body("Parameters", update_creadentials_request(), required: true),
|
||||
requestBody: request_body("Parameters", update_credentials_request(), required: true),
|
||||
responses: %{
|
||||
200 => Operation.response("Account", "application/json", Account),
|
||||
403 => Operation.response("Error", "application/json", ApiError)
|
||||
|
|
@ -159,6 +159,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
|
|||
"Accounts which follow the given account, if network is not hidden by the account owner.",
|
||||
parameters: [
|
||||
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
|
||||
Operation.parameter(:id, :query, :string, "ID of the resource owner"),
|
||||
with_relationships_param() | pagination_params()
|
||||
],
|
||||
responses: %{
|
||||
|
|
@ -177,6 +178,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
|
|||
"Accounts which the given account is following, if network is not hidden by the account owner.",
|
||||
parameters: [
|
||||
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
|
||||
Operation.parameter(:id, :query, :string, "ID of the resource owner"),
|
||||
with_relationships_param() | pagination_params()
|
||||
],
|
||||
responses: %{200 => Operation.response("Accounts", "application/json", array_of_accounts())}
|
||||
|
|
@ -203,14 +205,23 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
|
|||
security: [%{"oAuth" => ["follow", "write:follows"]}],
|
||||
description: "Follow the given account",
|
||||
parameters: [
|
||||
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
|
||||
Operation.parameter(
|
||||
:reblogs,
|
||||
:query,
|
||||
BooleanLike,
|
||||
"Receive this account's reblogs in home timeline? Defaults to true."
|
||||
)
|
||||
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}
|
||||
],
|
||||
requestBody:
|
||||
request_body(
|
||||
"Parameters",
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
reblogs: %Schema{
|
||||
type: :boolean,
|
||||
description: "Receive this account's reblogs in home timeline? Defaults to true.",
|
||||
default: true
|
||||
}
|
||||
}
|
||||
},
|
||||
required: false
|
||||
),
|
||||
responses: %{
|
||||
200 => Operation.response("Relationship", "application/json", AccountRelationship),
|
||||
400 => Operation.response("Error", "application/json", ApiError),
|
||||
|
|
@ -438,6 +449,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
|
|||
}
|
||||
end
|
||||
|
||||
# TODO: This is actually a token respone, but there's no oauth operation file yet.
|
||||
defp create_response do
|
||||
%Schema{
|
||||
title: "AccountCreateResponse",
|
||||
|
|
@ -446,19 +458,25 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
|
|||
properties: %{
|
||||
token_type: %Schema{type: :string},
|
||||
access_token: %Schema{type: :string},
|
||||
scope: %Schema{type: :array, items: %Schema{type: :string}},
|
||||
created_at: %Schema{type: :integer, format: :"date-time"}
|
||||
refresh_token: %Schema{type: :string},
|
||||
scope: %Schema{type: :string},
|
||||
created_at: %Schema{type: :integer, format: :"date-time"},
|
||||
me: %Schema{type: :string},
|
||||
expires_in: %Schema{type: :integer}
|
||||
},
|
||||
example: %{
|
||||
"token_type" => "Bearer",
|
||||
"access_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzk",
|
||||
"refresh_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzz",
|
||||
"created_at" => 1_585_918_714,
|
||||
"scope" => ["read", "write", "follow", "push"],
|
||||
"token_type" => "Bearer"
|
||||
"expires_in" => 600,
|
||||
"scope" => "read write follow push",
|
||||
"me" => "https://gensokyo.2hu/users/raymoo"
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp update_creadentials_request do
|
||||
defp update_credentials_request do
|
||||
%Schema{
|
||||
title: "AccountUpdateCredentialsRequest",
|
||||
description: "POST body for creating an account",
|
||||
|
|
@ -492,6 +510,11 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
|
|||
nullable: true,
|
||||
description: "Whether manual approval of follow requests is required."
|
||||
},
|
||||
accepts_chat_messages: %Schema{
|
||||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
description: "Whether the user accepts receiving chat messages."
|
||||
},
|
||||
fields_attributes: %Schema{
|
||||
nullable: true,
|
||||
oneOf: [
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ConfigOperation do
|
|||
%Schema{type: :boolean, default: false},
|
||||
"Get only saved in database settings"
|
||||
)
|
||||
| admin_api_params()
|
||||
],
|
||||
security: [%{"oAuth" => ["read"]}],
|
||||
responses: %{
|
||||
|
|
@ -41,6 +42,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ConfigOperation do
|
|||
summary: "Update config settings",
|
||||
operationId: "AdminAPI.ConfigController.update",
|
||||
security: [%{"oAuth" => ["write"]}],
|
||||
parameters: admin_api_params(),
|
||||
requestBody:
|
||||
request_body("Parameters", %Schema{
|
||||
type: :object,
|
||||
|
|
@ -73,6 +75,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ConfigOperation do
|
|||
summary: "Get JSON with config descriptions.",
|
||||
operationId: "AdminAPI.ConfigController.descriptions",
|
||||
security: [%{"oAuth" => ["read"]}],
|
||||
parameters: admin_api_params(),
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Config Descriptions", "application/json", %Schema{
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.InviteOperation do
|
|||
summary: "Get a list of generated invites",
|
||||
operationId: "AdminAPI.InviteController.index",
|
||||
security: [%{"oAuth" => ["read:invites"]}],
|
||||
parameters: admin_api_params(),
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Invites", "application/json", %Schema{
|
||||
|
|
@ -51,6 +52,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.InviteOperation do
|
|||
summary: "Create an account registration invite token",
|
||||
operationId: "AdminAPI.InviteController.create",
|
||||
security: [%{"oAuth" => ["write:invites"]}],
|
||||
parameters: admin_api_params(),
|
||||
requestBody:
|
||||
request_body("Parameters", %Schema{
|
||||
type: :object,
|
||||
|
|
@ -71,6 +73,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.InviteOperation do
|
|||
summary: "Revoke invite by token",
|
||||
operationId: "AdminAPI.InviteController.revoke",
|
||||
security: [%{"oAuth" => ["write:invites"]}],
|
||||
parameters: admin_api_params(),
|
||||
requestBody:
|
||||
request_body(
|
||||
"Parameters",
|
||||
|
|
@ -97,6 +100,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.InviteOperation do
|
|||
summary: "Sends registration invite via email",
|
||||
operationId: "AdminAPI.InviteController.email",
|
||||
security: [%{"oAuth" => ["write:invites"]}],
|
||||
parameters: admin_api_params(),
|
||||
requestBody:
|
||||
request_body(
|
||||
"Parameters",
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue