Merge branch 'develop' into feature/gen-magic
This commit is contained in:
commit
04b514c567
239 changed files with 8638 additions and 2322 deletions
|
|
@ -32,7 +32,8 @@ defmodule Mix.Tasks.Pleroma.Config do
|
|||
|
||||
@spec migrate_to_db(Path.t() | nil) :: any()
|
||||
def migrate_to_db(file_path \\ nil) do
|
||||
if Pleroma.Config.get([:configurable_from_database]) do
|
||||
with true <- Pleroma.Config.get([:configurable_from_database]),
|
||||
:ok <- Pleroma.Config.DeprecationWarnings.warn() do
|
||||
config_file =
|
||||
if file_path do
|
||||
file_path
|
||||
|
|
@ -46,7 +47,8 @@ defmodule Mix.Tasks.Pleroma.Config do
|
|||
|
||||
do_migrate_to_db(config_file)
|
||||
else
|
||||
migration_error()
|
||||
:error -> deprecation_error()
|
||||
_ -> migration_error()
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -120,6 +122,10 @@ defmodule Mix.Tasks.Pleroma.Config do
|
|||
)
|
||||
end
|
||||
|
||||
defp deprecation_error do
|
||||
shell_error("Migration is not allowed until all deprecation warnings have been resolved.")
|
||||
end
|
||||
|
||||
if Code.ensure_loaded?(Config.Reader) do
|
||||
defp config_header, do: "import Config\r\n\r\n"
|
||||
defp read_file(config_file), do: Config.Reader.read_imports!(config_file)
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ defmodule Mix.Tasks.Pleroma.Database do
|
|||
where: fragment("(?)->>'likes' is not null", object.data),
|
||||
select: %{id: object.id, likes: fragment("(?)->>'likes'", object.data)}
|
||||
)
|
||||
|> Pleroma.RepoStreamer.chunk_stream(100)
|
||||
|> Pleroma.Repo.chunk_stream(100, :batches)
|
||||
|> Stream.each(fn objects ->
|
||||
ids =
|
||||
objects
|
||||
|
|
@ -145,7 +145,7 @@ defmodule Mix.Tasks.Pleroma.Database do
|
|||
|> where(local: true)
|
||||
|> where([a], fragment("(? ->> 'type'::text) = 'Create'", a.data))
|
||||
|> where([_a, o], fragment("?->>'type' = 'Note'", o.data))
|
||||
|> Pleroma.RepoStreamer.chunk_stream(100)
|
||||
|> Pleroma.Repo.chunk_stream(100, :batches)
|
||||
|> Stream.each(fn activities ->
|
||||
Enum.each(activities, fn activity ->
|
||||
expires_at =
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ defmodule Mix.Tasks.Pleroma.Email do
|
|||
use Mix.Task
|
||||
import Mix.Pleroma
|
||||
|
||||
@shortdoc "Simple Email test"
|
||||
@shortdoc "Email administrative tasks"
|
||||
@moduledoc File.read!("docs/administration/CLI_tasks/email.md")
|
||||
|
||||
def run(["test" | args]) do
|
||||
Mix.Pleroma.start_pleroma()
|
||||
start_pleroma()
|
||||
|
||||
{options, [], []} =
|
||||
OptionParser.parse(
|
||||
|
|
@ -21,4 +21,20 @@ defmodule Mix.Tasks.Pleroma.Email do
|
|||
|
||||
shell_info("Test email has been sent to #{inspect(email.to)} from #{inspect(email.from)}")
|
||||
end
|
||||
|
||||
def run(["resend_confirmation_emails"]) do
|
||||
start_pleroma()
|
||||
|
||||
shell_info("Sending emails to all unconfirmed users")
|
||||
|
||||
Pleroma.User.Query.build(%{
|
||||
local: true,
|
||||
deactivated: false,
|
||||
confirmation_pending: true,
|
||||
invisible: false
|
||||
})
|
||||
|> Pleroma.Repo.chunk_stream(500)
|
||||
|> Stream.each(&Pleroma.User.try_send_confirmation_email(&1))
|
||||
|> Stream.run()
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -21,10 +21,19 @@ defmodule Mix.Tasks.Pleroma.Relay do
|
|||
end
|
||||
end
|
||||
|
||||
def run(["unfollow", target]) do
|
||||
def run(["unfollow", target | rest]) do
|
||||
start_pleroma()
|
||||
|
||||
with {:ok, _activity} <- Relay.unfollow(target) do
|
||||
{options, [], []} =
|
||||
OptionParser.parse(
|
||||
rest,
|
||||
strict: [force: :boolean],
|
||||
aliases: [f: :force]
|
||||
)
|
||||
|
||||
force = Keyword.get(options, :force, false)
|
||||
|
||||
with {:ok, _activity} <- Relay.unfollow(target, %{force: force}) do
|
||||
# put this task to sleep to allow the genserver to push out the messages
|
||||
:timer.sleep(500)
|
||||
else
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@ defmodule Mix.Tasks.Pleroma.User do
|
|||
start_pleroma()
|
||||
|
||||
Pleroma.User.Query.build(%{nickname: "@#{instance}"})
|
||||
|> Pleroma.RepoStreamer.chunk_stream(500)
|
||||
|> Pleroma.Repo.chunk_stream(500, :batches)
|
||||
|> Stream.each(fn users ->
|
||||
users
|
||||
|> Enum.each(fn user ->
|
||||
|
|
@ -196,17 +196,24 @@ defmodule Mix.Tasks.Pleroma.User do
|
|||
OptionParser.parse(
|
||||
rest,
|
||||
strict: [
|
||||
moderator: :boolean,
|
||||
admin: :boolean,
|
||||
locked: :boolean
|
||||
confirmed: :boolean,
|
||||
locked: :boolean,
|
||||
moderator: :boolean
|
||||
]
|
||||
)
|
||||
|
||||
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
|
||||
user =
|
||||
case Keyword.get(options, :moderator) do
|
||||
case Keyword.get(options, :admin) do
|
||||
nil -> user
|
||||
value -> set_moderator(user, value)
|
||||
value -> set_admin(user, value)
|
||||
end
|
||||
|
||||
user =
|
||||
case Keyword.get(options, :confirmed) do
|
||||
nil -> user
|
||||
value -> set_confirmed(user, value)
|
||||
end
|
||||
|
||||
user =
|
||||
|
|
@ -216,9 +223,9 @@ defmodule Mix.Tasks.Pleroma.User do
|
|||
end
|
||||
|
||||
_user =
|
||||
case Keyword.get(options, :admin) do
|
||||
case Keyword.get(options, :moderator) do
|
||||
nil -> user
|
||||
value -> set_admin(user, value)
|
||||
value -> set_moderator(user, value)
|
||||
end
|
||||
else
|
||||
_ ->
|
||||
|
|
@ -353,6 +360,42 @@ defmodule Mix.Tasks.Pleroma.User do
|
|||
end
|
||||
end
|
||||
|
||||
def run(["confirm_all"]) do
|
||||
start_pleroma()
|
||||
|
||||
Pleroma.User.Query.build(%{
|
||||
local: true,
|
||||
deactivated: false,
|
||||
is_moderator: false,
|
||||
is_admin: false,
|
||||
invisible: false
|
||||
})
|
||||
|> Pleroma.Repo.chunk_stream(500, :batches)
|
||||
|> Stream.each(fn users ->
|
||||
users
|
||||
|> Enum.each(fn user -> User.need_confirmation(user, false) end)
|
||||
end)
|
||||
|> Stream.run()
|
||||
end
|
||||
|
||||
def run(["unconfirm_all"]) do
|
||||
start_pleroma()
|
||||
|
||||
Pleroma.User.Query.build(%{
|
||||
local: true,
|
||||
deactivated: false,
|
||||
is_moderator: false,
|
||||
is_admin: false,
|
||||
invisible: false
|
||||
})
|
||||
|> Pleroma.Repo.chunk_stream(500, :batches)
|
||||
|> Stream.each(fn users ->
|
||||
users
|
||||
|> Enum.each(fn user -> User.need_confirmation(user, true) end)
|
||||
end)
|
||||
|> Stream.run()
|
||||
end
|
||||
|
||||
def run(["sign_out", nickname]) do
|
||||
start_pleroma()
|
||||
|
||||
|
|
@ -370,7 +413,7 @@ defmodule Mix.Tasks.Pleroma.User do
|
|||
start_pleroma()
|
||||
|
||||
Pleroma.User.Query.build(%{local: true})
|
||||
|> Pleroma.RepoStreamer.chunk_stream(500)
|
||||
|> Pleroma.Repo.chunk_stream(500, :batches)
|
||||
|> Stream.each(fn users ->
|
||||
users
|
||||
|> Enum.each(fn user ->
|
||||
|
|
@ -410,4 +453,11 @@ defmodule Mix.Tasks.Pleroma.User do
|
|||
shell_info("Locked status of #{user.nickname}: #{user.locked}")
|
||||
user
|
||||
end
|
||||
|
||||
defp set_confirmed(user, value) do
|
||||
{:ok, user} = User.need_confirmation(user, !value)
|
||||
|
||||
shell_info("Confirmation pending status of #{user.nickname}: #{user.confirmation_pending}")
|
||||
user
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -56,7 +56,6 @@ defmodule Pleroma.Application do
|
|||
Pleroma.ApplicationRequirements.verify!()
|
||||
setup_instrumenters()
|
||||
load_custom_modules()
|
||||
check_system_commands()
|
||||
Pleroma.Docs.JSON.compile()
|
||||
|
||||
adapter = Application.get_env(:tesla, :adapter)
|
||||
|
|
@ -100,7 +99,7 @@ defmodule Pleroma.Application do
|
|||
{Oban, Config.get(Oban)}
|
||||
] ++
|
||||
task_children(@env) ++
|
||||
streamer_child(@env) ++
|
||||
dont_run_in_test(@env) ++
|
||||
chat_child(@env, chat_enabled?()) ++
|
||||
[
|
||||
Pleroma.Web.Endpoint,
|
||||
|
|
@ -189,16 +188,17 @@ defmodule Pleroma.Application do
|
|||
|
||||
defp chat_enabled?, do: Config.get([:chat, :enabled])
|
||||
|
||||
defp streamer_child(env) when env in [:test, :benchmark], do: []
|
||||
defp dont_run_in_test(env) when env in [:test, :benchmark], do: []
|
||||
|
||||
defp streamer_child(_) do
|
||||
defp dont_run_in_test(_) do
|
||||
[
|
||||
{Registry,
|
||||
[
|
||||
name: Pleroma.Web.Streamer.registry(),
|
||||
keys: :duplicate,
|
||||
partitions: System.schedulers_online()
|
||||
]}
|
||||
]},
|
||||
Pleroma.Web.FedSockets.Supervisor
|
||||
]
|
||||
end
|
||||
|
||||
|
|
@ -260,21 +260,4 @@ defmodule Pleroma.Application do
|
|||
end
|
||||
|
||||
defp http_children(_, _), do: []
|
||||
|
||||
defp check_system_commands do
|
||||
filters = Config.get([Pleroma.Upload, :filters])
|
||||
|
||||
check_filter = fn filter, command_required ->
|
||||
with true <- filter in filters,
|
||||
false <- Pleroma.Utils.command_available?(command_required) do
|
||||
Logger.error(
|
||||
"#{filter} is specified in list of Pleroma.Upload filters, but the #{command_required} command is not found"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
check_filter.(Pleroma.Upload.Filters.Exiftool, "exiftool")
|
||||
check_filter.(Pleroma.Upload.Filters.Mogrify, "mogrify")
|
||||
check_filter.(Pleroma.Upload.Filters.Mogrifun, "mogrify")
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ defmodule Pleroma.ApplicationRequirements do
|
|||
|
||||
defmodule VerifyError, do: defexception([:message])
|
||||
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.Helpers.MediaHelper
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
require Logger
|
||||
|
|
@ -16,7 +19,8 @@ defmodule Pleroma.ApplicationRequirements do
|
|||
@spec verify!() :: :ok | VerifyError.t()
|
||||
def verify! do
|
||||
:ok
|
||||
|> check_confirmation_accounts!
|
||||
|> check_system_commands!()
|
||||
|> check_confirmation_accounts!()
|
||||
|> check_migrations_applied!()
|
||||
|> check_welcome_message_config!()
|
||||
|> check_rum!()
|
||||
|
|
@ -48,7 +52,9 @@ defmodule Pleroma.ApplicationRequirements 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."
|
||||
"Account activation enabled, but no Mailer settings enabled.\n" <>
|
||||
"Please set config :pleroma, :instance, account_activation_required: false\n" <>
|
||||
"Otherwise setup and enable Mailer."
|
||||
)
|
||||
|
||||
{:error,
|
||||
|
|
@ -81,7 +87,9 @@ defmodule Pleroma.ApplicationRequirements do
|
|||
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"
|
||||
"The following migrations were not applied:\n#{down_migrations_text}" <>
|
||||
"If you want to start Pleroma anyway, set\n" <>
|
||||
"config :pleroma, :i_am_aware_this_may_cause_data_loss, disable_migration_check: true"
|
||||
)
|
||||
|
||||
{:error, "Unapplied Migrations detected"}
|
||||
|
|
@ -124,14 +132,22 @@ defmodule Pleroma.ApplicationRequirements 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/`"
|
||||
"Use `RUM` index is enabled, but were not applied migrations for it.\n" <>
|
||||
"If you want to start Pleroma anyway, set\n" <>
|
||||
"config :pleroma, :database, rum_enabled: false\n" <>
|
||||
"Otherwise 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/`"
|
||||
"Detected applied migrations to use `RUM` index, but `RUM` isn't enable in settings.\n" <>
|
||||
"If you want to use `RUM`, set\n" <>
|
||||
"config :pleroma, :database, rum_enabled: true\n" <>
|
||||
"Otherwise roll `RUM` migrations back.\n" <>
|
||||
"`mix ecto.rollback --migrations-path priv/repo/optional_migrations/rum_indexing/`"
|
||||
)
|
||||
|
||||
{:error, "RUM Migrations detected"}
|
||||
|
|
@ -140,4 +156,50 @@ defmodule Pleroma.ApplicationRequirements do
|
|||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp check_system_commands!(:ok) do
|
||||
filter_commands_statuses = [
|
||||
check_filter(Pleroma.Upload.Filters.Exiftool, "exiftool"),
|
||||
check_filter(Pleroma.Upload.Filters.Mogrify, "mogrify"),
|
||||
check_filter(Pleroma.Upload.Filters.Mogrifun, "mogrify")
|
||||
]
|
||||
|
||||
preview_proxy_commands_status =
|
||||
if !Config.get([:media_preview_proxy, :enabled]) or
|
||||
MediaHelper.missing_dependencies() == [] do
|
||||
true
|
||||
else
|
||||
Logger.error(
|
||||
"The following dependencies required by Media preview proxy " <>
|
||||
"(which is currently enabled) are not installed: " <>
|
||||
inspect(MediaHelper.missing_dependencies())
|
||||
)
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
if Enum.all?([preview_proxy_commands_status | filter_commands_statuses], & &1) do
|
||||
:ok
|
||||
else
|
||||
{:error,
|
||||
"System commands missing. Check logs and see `docs/installation` for more details."}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_system_commands!(result), do: result
|
||||
|
||||
defp check_filter(filter, command_required) do
|
||||
filters = Config.get([Pleroma.Upload, :filters])
|
||||
|
||||
if filter in filters and not Pleroma.Utils.command_available?(command_required) do
|
||||
Logger.error(
|
||||
"#{filter} is specified in list of Pleroma.Upload filters, but the " <>
|
||||
"#{command_required} command is not found"
|
||||
)
|
||||
|
||||
false
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@ defmodule Pleroma.Chat do
|
|||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
import Ecto.Query
|
||||
|
||||
alias Pleroma.Chat
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
|
||||
|
|
@ -16,6 +18,7 @@ defmodule Pleroma.Chat do
|
|||
It is a helper only, to make it easy to display a list of chats with other people, ordered by last bump. The actual messages are retrieved by querying the recipients of the ChatMessages.
|
||||
"""
|
||||
|
||||
@type t :: %__MODULE__{}
|
||||
@primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
|
||||
|
||||
schema "chats" do
|
||||
|
|
@ -39,16 +42,28 @@ defmodule Pleroma.Chat do
|
|||
|> unique_constraint(:user_id, name: :chats_user_id_recipient_index)
|
||||
end
|
||||
|
||||
@spec get_by_user_and_id(User.t(), FlakeId.Ecto.CompatType.t()) ::
|
||||
{:ok, t()} | {:error, :not_found}
|
||||
def get_by_user_and_id(%User{id: user_id}, id) do
|
||||
from(c in __MODULE__,
|
||||
where: c.id == ^id,
|
||||
where: c.user_id == ^user_id
|
||||
)
|
||||
|> Repo.find_resource()
|
||||
end
|
||||
|
||||
@spec get_by_id(FlakeId.Ecto.CompatType.t()) :: t() | nil
|
||||
def get_by_id(id) do
|
||||
__MODULE__
|
||||
|> Repo.get(id)
|
||||
Repo.get(__MODULE__, id)
|
||||
end
|
||||
|
||||
@spec get(FlakeId.Ecto.CompatType.t(), String.t()) :: t() | nil
|
||||
def get(user_id, recipient) do
|
||||
__MODULE__
|
||||
|> Repo.get_by(user_id: user_id, recipient: recipient)
|
||||
Repo.get_by(__MODULE__, user_id: user_id, recipient: recipient)
|
||||
end
|
||||
|
||||
@spec get_or_create(FlakeId.Ecto.CompatType.t(), String.t()) ::
|
||||
{:ok, t()} | {:error, Ecto.Changeset.t()}
|
||||
def get_or_create(user_id, recipient) do
|
||||
%__MODULE__{}
|
||||
|> changeset(%{user_id: user_id, recipient: recipient})
|
||||
|
|
@ -60,6 +75,8 @@ defmodule Pleroma.Chat do
|
|||
)
|
||||
end
|
||||
|
||||
@spec bump_or_create(FlakeId.Ecto.CompatType.t(), String.t()) ::
|
||||
{:ok, t()} | {:error, Ecto.Changeset.t()}
|
||||
def bump_or_create(user_id, recipient) do
|
||||
%__MODULE__{}
|
||||
|> changeset(%{user_id: user_id, recipient: recipient})
|
||||
|
|
@ -69,4 +86,12 @@ defmodule Pleroma.Chat do
|
|||
conflict_target: [:user_id, :recipient]
|
||||
)
|
||||
end
|
||||
|
||||
@spec for_user_query(FlakeId.Ecto.CompatType.t()) :: Ecto.Query.t()
|
||||
def for_user_query(user_id) do
|
||||
from(c in Chat,
|
||||
where: c.user_id == ^user_id,
|
||||
order_by: [desc: c.updated_at]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -26,38 +26,25 @@ defmodule Pleroma.Config.DeprecationWarnings do
|
|||
!!!DEPRECATION WARNING!!!
|
||||
You are using the old configuration mechanism for the hellthread filter. Please check config.md.
|
||||
""")
|
||||
end
|
||||
end
|
||||
|
||||
def mrf_user_allowlist do
|
||||
config = Config.get(:mrf_user_allowlist)
|
||||
|
||||
if config && Enum.any?(config, fn {k, _} -> is_atom(k) end) do
|
||||
rewritten =
|
||||
Enum.reduce(Config.get(:mrf_user_allowlist), Map.new(), fn {k, v}, acc ->
|
||||
Map.put(acc, to_string(k), v)
|
||||
end)
|
||||
|
||||
Config.put(:mrf_user_allowlist, rewritten)
|
||||
|
||||
Logger.error("""
|
||||
!!!DEPRECATION WARNING!!!
|
||||
As of Pleroma 2.0.7, the `mrf_user_allowlist` setting changed of format.
|
||||
Pleroma 2.1 will remove support for the old format. Please change your configuration to match this:
|
||||
|
||||
config :pleroma, :mrf_user_allowlist, #{inspect(rewritten, pretty: true)}
|
||||
""")
|
||||
:error
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
def warn do
|
||||
check_hellthread_threshold()
|
||||
mrf_user_allowlist()
|
||||
check_old_mrf_config()
|
||||
check_media_proxy_whitelist_config()
|
||||
check_welcome_message_config()
|
||||
check_gun_pool_options()
|
||||
check_activity_expiration_config()
|
||||
with :ok <- check_hellthread_threshold(),
|
||||
:ok <- check_old_mrf_config(),
|
||||
:ok <- check_media_proxy_whitelist_config(),
|
||||
:ok <- check_welcome_message_config(),
|
||||
:ok <- check_gun_pool_options(),
|
||||
:ok <- check_activity_expiration_config() do
|
||||
:ok
|
||||
else
|
||||
_ ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
def check_welcome_message_config do
|
||||
|
|
@ -70,10 +57,14 @@ defmodule Pleroma.Config.DeprecationWarnings do
|
|||
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`
|
||||
Your config is using the old namespace for Welcome messages configuration. You need to convert to the new namespace. e.g.,
|
||||
\n* `config :pleroma, :instance, welcome_user_nickname` and `config :pleroma, :instance, welcome_message` are now equal to:
|
||||
\n* `config :pleroma, :welcome, direct_message: [enabled: true, sender_nickname: "NICKNAME", message: "Your welcome message"]`"
|
||||
""")
|
||||
|
||||
:error
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -101,8 +92,11 @@ defmodule Pleroma.Config.DeprecationWarnings do
|
|||
end
|
||||
end)
|
||||
|
||||
if warning != "" do
|
||||
if warning == "" do
|
||||
:ok
|
||||
else
|
||||
Logger.warn(warning_preface <> warning)
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -115,6 +109,10 @@ defmodule Pleroma.Config.DeprecationWarnings do
|
|||
!!!DEPRECATION WARNING!!!
|
||||
Your config is using old format (only domain) for MediaProxy whitelist option. Setting should work for now, but you are advised to change format to scheme with port to prevent possible issues later.
|
||||
""")
|
||||
|
||||
:error
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -124,7 +122,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
|
|||
if timeout = pool_config[:await_up_timeout] do
|
||||
Logger.warn("""
|
||||
!!!DEPRECATION WARNING!!!
|
||||
Your config is using old setting name `await_up_timeout` instead of `connect_timeout`. Setting should work for now, but you are advised to change format to scheme with port to prevent possible issues later.
|
||||
Your config is using old setting `config :pleroma, :connections_pool, await_up_timeout`. Please change to `config :pleroma, :connections_pool, connect_timeout` to ensure compatibility with future releases.
|
||||
""")
|
||||
|
||||
Config.put(:connections_pool, Keyword.put_new(pool_config, :connect_timeout, timeout))
|
||||
|
|
@ -157,6 +155,9 @@ defmodule Pleroma.Config.DeprecationWarnings do
|
|||
Logger.warn(Enum.join([warning_preface | pool_warnings]))
|
||||
|
||||
Config.put(:pools, updated_config)
|
||||
:error
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,11 @@ defmodule Pleroma.Emails.Mailer do
|
|||
def deliver(email, config \\ [])
|
||||
|
||||
def deliver(email, config) do
|
||||
# temporary hackney fix until hackney max_connections bug is fixed
|
||||
# https://git.pleroma.social/pleroma/pleroma/-/issues/2101
|
||||
email =
|
||||
Swoosh.Email.put_private(email, :hackney_options, ssl_options: [versions: [:"tlsv1.2"]])
|
||||
|
||||
case enabled?() do
|
||||
true -> Swoosh.Mailer.deliver(email, parse_config(config))
|
||||
false -> {:error, :deliveries_disabled}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,9 @@ defmodule Pleroma.Emoji do
|
|||
end
|
||||
end
|
||||
|
||||
@spec exist?(String.t()) :: boolean()
|
||||
def exist?(name), do: not is_nil(get(name))
|
||||
|
||||
@doc "Returns all the emojos!!"
|
||||
@spec get_all() :: list({String.t(), String.t(), String.t()})
|
||||
def get_all do
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ defmodule Pleroma.Emoji.Pack do
|
|||
}
|
||||
|
||||
alias Pleroma.Emoji
|
||||
alias Pleroma.Emoji.Pack
|
||||
|
||||
@spec create(String.t()) :: {:ok, t()} | {:error, File.posix()} | {:error, :empty_values}
|
||||
def create(name) do
|
||||
|
|
@ -64,24 +65,93 @@ defmodule Pleroma.Emoji.Pack do
|
|||
end
|
||||
end
|
||||
|
||||
@spec add_file(String.t(), String.t(), Path.t(), Plug.Upload.t() | String.t()) ::
|
||||
{:ok, t()} | {:error, File.posix() | atom()}
|
||||
def add_file(name, shortcode, filename, file) do
|
||||
with :ok <- validate_not_empty([name, shortcode, filename]),
|
||||
@spec unpack_zip_emojies(list(tuple())) :: list(map())
|
||||
defp unpack_zip_emojies(zip_files) do
|
||||
Enum.reduce(zip_files, [], fn
|
||||
{_, path, s, _, _, _}, acc when elem(s, 2) == :regular ->
|
||||
with(
|
||||
filename <- Path.basename(path),
|
||||
shortcode <- Path.basename(filename, Path.extname(filename)),
|
||||
false <- Emoji.exist?(shortcode)
|
||||
) do
|
||||
[%{path: path, filename: path, shortcode: shortcode} | acc]
|
||||
else
|
||||
_ -> acc
|
||||
end
|
||||
|
||||
_, acc ->
|
||||
acc
|
||||
end)
|
||||
end
|
||||
|
||||
@spec add_file(t(), String.t(), Path.t(), Plug.Upload.t()) ::
|
||||
{:ok, t()}
|
||||
| {:error, File.posix() | atom()}
|
||||
def add_file(%Pack{} = pack, _, _, %Plug.Upload{content_type: "application/zip"} = file) do
|
||||
with {:ok, zip_files} <- :zip.table(to_charlist(file.path)),
|
||||
[_ | _] = emojies <- unpack_zip_emojies(zip_files),
|
||||
{:ok, tmp_dir} <- Pleroma.Utils.tmp_dir("emoji") do
|
||||
try do
|
||||
{:ok, _emoji_files} =
|
||||
:zip.unzip(
|
||||
to_charlist(file.path),
|
||||
[{:file_list, Enum.map(emojies, & &1[:path])}, {:cwd, tmp_dir}]
|
||||
)
|
||||
|
||||
{_, updated_pack} =
|
||||
Enum.map_reduce(emojies, pack, fn item, emoji_pack ->
|
||||
emoji_file = %Plug.Upload{
|
||||
filename: item[:filename],
|
||||
path: Path.join(tmp_dir, item[:path])
|
||||
}
|
||||
|
||||
{:ok, updated_pack} =
|
||||
do_add_file(
|
||||
emoji_pack,
|
||||
item[:shortcode],
|
||||
to_string(item[:filename]),
|
||||
emoji_file
|
||||
)
|
||||
|
||||
{item, updated_pack}
|
||||
end)
|
||||
|
||||
Emoji.reload()
|
||||
|
||||
{:ok, updated_pack}
|
||||
after
|
||||
File.rm_rf(tmp_dir)
|
||||
end
|
||||
else
|
||||
{:error, _} = error ->
|
||||
error
|
||||
|
||||
_ ->
|
||||
{:ok, pack}
|
||||
end
|
||||
end
|
||||
|
||||
def add_file(%Pack{} = pack, shortcode, filename, %Plug.Upload{} = file) do
|
||||
with :ok <- validate_not_empty([shortcode, filename]),
|
||||
:ok <- validate_emoji_not_exists(shortcode),
|
||||
{:ok, pack} <- load_pack(name),
|
||||
:ok <- save_file(file, pack, filename),
|
||||
{:ok, updated_pack} <- pack |> put_emoji(shortcode, filename) |> save_pack() do
|
||||
{:ok, updated_pack} <- do_add_file(pack, shortcode, filename, file) do
|
||||
Emoji.reload()
|
||||
{:ok, updated_pack}
|
||||
end
|
||||
end
|
||||
|
||||
@spec delete_file(String.t(), String.t()) ::
|
||||
defp do_add_file(pack, shortcode, filename, file) do
|
||||
with :ok <- save_file(file, pack, filename) do
|
||||
pack
|
||||
|> put_emoji(shortcode, filename)
|
||||
|> save_pack()
|
||||
end
|
||||
end
|
||||
|
||||
@spec delete_file(t(), String.t()) ::
|
||||
{:ok, t()} | {:error, File.posix() | atom()}
|
||||
def delete_file(name, shortcode) do
|
||||
with :ok <- validate_not_empty([name, shortcode]),
|
||||
{:ok, pack} <- load_pack(name),
|
||||
def delete_file(%Pack{} = pack, shortcode) do
|
||||
with :ok <- validate_not_empty([shortcode]),
|
||||
:ok <- remove_file(pack, shortcode),
|
||||
{:ok, updated_pack} <- pack |> delete_emoji(shortcode) |> save_pack() do
|
||||
Emoji.reload()
|
||||
|
|
@ -89,11 +159,10 @@ defmodule Pleroma.Emoji.Pack do
|
|||
end
|
||||
end
|
||||
|
||||
@spec update_file(String.t(), String.t(), String.t(), String.t(), boolean()) ::
|
||||
@spec update_file(t(), String.t(), String.t(), String.t(), boolean()) ::
|
||||
{:ok, t()} | {:error, File.posix() | atom()}
|
||||
def update_file(name, shortcode, new_shortcode, new_filename, force) do
|
||||
with :ok <- validate_not_empty([name, shortcode, new_shortcode, new_filename]),
|
||||
{:ok, pack} <- load_pack(name),
|
||||
def update_file(%Pack{} = pack, shortcode, new_shortcode, new_filename, force) do
|
||||
with :ok <- validate_not_empty([shortcode, new_shortcode, new_filename]),
|
||||
{:ok, filename} <- get_filename(pack, shortcode),
|
||||
:ok <- validate_emoji_not_exists(new_shortcode, force),
|
||||
:ok <- rename_file(pack, filename, new_filename),
|
||||
|
|
@ -129,13 +198,13 @@ defmodule Pleroma.Emoji.Pack do
|
|||
end
|
||||
end
|
||||
|
||||
@spec list_remote(String.t()) :: {:ok, map()} | {:error, atom()}
|
||||
def list_remote(url) do
|
||||
uri = url |> String.trim() |> URI.parse()
|
||||
@spec list_remote(keyword()) :: {:ok, map()} | {:error, atom()}
|
||||
def list_remote(opts) do
|
||||
uri = opts[:url] |> String.trim() |> URI.parse()
|
||||
|
||||
with :ok <- validate_shareable_packs_available(uri) do
|
||||
uri
|
||||
|> URI.merge("/api/pleroma/emoji/packs")
|
||||
|> URI.merge("/api/pleroma/emoji/packs?page=#{opts[:page]}&page_size=#{opts[:page_size]}")
|
||||
|> http_get()
|
||||
end
|
||||
end
|
||||
|
|
@ -175,7 +244,8 @@ defmodule Pleroma.Emoji.Pack do
|
|||
uri = url |> String.trim() |> URI.parse()
|
||||
|
||||
with :ok <- validate_shareable_packs_available(uri),
|
||||
{:ok, remote_pack} <- uri |> URI.merge("/api/pleroma/emoji/packs/#{name}") |> http_get(),
|
||||
{:ok, remote_pack} <-
|
||||
uri |> URI.merge("/api/pleroma/emoji/pack?name=#{name}") |> http_get(),
|
||||
{:ok, %{sha: sha, url: url} = pack_info} <- fetch_pack_info(remote_pack, uri, name),
|
||||
{:ok, archive} <- download_archive(url, sha),
|
||||
pack <- copy_as(remote_pack, as || name),
|
||||
|
|
@ -243,9 +313,10 @@ defmodule Pleroma.Emoji.Pack do
|
|||
defp validate_emoji_not_exists(_shortcode, true), do: :ok
|
||||
|
||||
defp validate_emoji_not_exists(shortcode, _) do
|
||||
case Emoji.get(shortcode) do
|
||||
nil -> :ok
|
||||
_ -> {:error, :already_exists}
|
||||
if Emoji.exist?(shortcode) do
|
||||
{:error, :already_exists}
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -386,25 +457,18 @@ defmodule Pleroma.Emoji.Pack do
|
|||
end
|
||||
end
|
||||
|
||||
defp save_file(file, pack, filename) do
|
||||
defp save_file(%Plug.Upload{path: upload_path}, pack, filename) do
|
||||
file_path = Path.join(pack.path, filename)
|
||||
create_subdirs(file_path)
|
||||
|
||||
case file do
|
||||
%Plug.Upload{path: upload_path} ->
|
||||
# Copy the uploaded file from the temporary directory
|
||||
with {:ok, _} <- File.copy(upload_path, file_path), do: :ok
|
||||
|
||||
url when is_binary(url) ->
|
||||
# Download and write the file
|
||||
file_contents = Tesla.get!(url).body
|
||||
File.write(file_path, file_contents)
|
||||
with {:ok, _} <- File.copy(upload_path, file_path) do
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp put_emoji(pack, shortcode, filename) do
|
||||
files = Map.put(pack.files, shortcode, filename)
|
||||
%{pack | files: files}
|
||||
%{pack | files: files, files_count: length(Map.keys(files))}
|
||||
end
|
||||
|
||||
defp delete_emoji(pack, shortcode) do
|
||||
|
|
@ -460,7 +524,7 @@ defmodule Pleroma.Emoji.Pack do
|
|||
defp http_get(%URI{} = url), do: url |> to_string() |> http_get()
|
||||
|
||||
defp http_get(url) do
|
||||
with {:ok, %{body: body}} <- url |> Pleroma.HTTP.get() do
|
||||
with {:ok, %{body: body}} <- Pleroma.HTTP.get(url, [], pool: :default) do
|
||||
Jason.decode(body)
|
||||
end
|
||||
end
|
||||
|
|
@ -509,7 +573,7 @@ defmodule Pleroma.Emoji.Pack do
|
|||
{:ok,
|
||||
%{
|
||||
sha: sha,
|
||||
url: URI.merge(uri, "/api/pleroma/emoji/packs/#{name}/archive") |> to_string()
|
||||
url: URI.merge(uri, "/api/pleroma/emoji/packs/archive?name=#{name}") |> to_string()
|
||||
}}
|
||||
|
||||
%{"fallback-src" => src, "fallback-src-sha256" => sha} when is_binary(src) ->
|
||||
|
|
|
|||
|
|
@ -50,10 +50,10 @@ defmodule Pleroma.Gun.Conn do
|
|||
|
||||
with open_opts <- Map.delete(opts, :tls_opts),
|
||||
{:ok, conn} <- Gun.open(proxy_host, proxy_port, open_opts),
|
||||
{:ok, _} <- Gun.await_up(conn, opts[:connect_timeout]),
|
||||
{:ok, protocol} <- Gun.await_up(conn, opts[:connect_timeout]),
|
||||
stream <- Gun.connect(conn, connect_opts),
|
||||
{:response, :fin, 200, _} <- Gun.await(conn, stream) do
|
||||
{:ok, conn}
|
||||
{:ok, conn, protocol}
|
||||
else
|
||||
error ->
|
||||
Logger.warn(
|
||||
|
|
@ -88,8 +88,8 @@ defmodule Pleroma.Gun.Conn do
|
|||
|> Map.put(:socks_opts, socks_opts)
|
||||
|
||||
with {:ok, conn} <- Gun.open(proxy_host, proxy_port, opts),
|
||||
{:ok, _} <- Gun.await_up(conn, opts[:connect_timeout]) do
|
||||
{:ok, conn}
|
||||
{:ok, protocol} <- Gun.await_up(conn, opts[:connect_timeout]) do
|
||||
{:ok, conn, protocol}
|
||||
else
|
||||
error ->
|
||||
Logger.warn(
|
||||
|
|
@ -106,8 +106,8 @@ defmodule Pleroma.Gun.Conn do
|
|||
host = Pleroma.HTTP.AdapterHelper.parse_host(host)
|
||||
|
||||
with {:ok, conn} <- Gun.open(host, port, opts),
|
||||
{:ok, _} <- Gun.await_up(conn, opts[:connect_timeout]) do
|
||||
{:ok, conn}
|
||||
{:ok, protocol} <- Gun.await_up(conn, opts[:connect_timeout]) do
|
||||
{:ok, conn, protocol}
|
||||
else
|
||||
error ->
|
||||
Logger.warn(
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ defmodule Pleroma.Gun.ConnectionPool.Worker do
|
|||
|
||||
@impl true
|
||||
def handle_continue({:connect, [key, uri, opts, client_pid]}, _) do
|
||||
with {:ok, conn_pid} <- Gun.Conn.open(uri, opts),
|
||||
with {:ok, conn_pid, protocol} <- Gun.Conn.open(uri, opts),
|
||||
Process.link(conn_pid) do
|
||||
time = :erlang.monotonic_time(:millisecond)
|
||||
|
||||
|
|
@ -27,8 +27,12 @@ defmodule Pleroma.Gun.ConnectionPool.Worker do
|
|||
send(client_pid, {:conn_pid, conn_pid})
|
||||
|
||||
{:noreply,
|
||||
%{key: key, timer: nil, client_monitors: %{client_pid => Process.monitor(client_pid)}},
|
||||
:hibernate}
|
||||
%{
|
||||
key: key,
|
||||
timer: nil,
|
||||
client_monitors: %{client_pid => Process.monitor(client_pid)},
|
||||
protocol: protocol
|
||||
}, :hibernate}
|
||||
else
|
||||
err ->
|
||||
{:stop, {:shutdown, err}, nil}
|
||||
|
|
@ -53,14 +57,20 @@ defmodule Pleroma.Gun.ConnectionPool.Worker do
|
|||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:add_client, {client_pid, _}, %{key: key} = state) do
|
||||
def handle_call(:add_client, {client_pid, _}, %{key: key, protocol: protocol} = state) do
|
||||
time = :erlang.monotonic_time(:millisecond)
|
||||
|
||||
{{conn_pid, _, _, _}, _} =
|
||||
{{conn_pid, used_by, _, _}, _} =
|
||||
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)
|
||||
|
||||
:telemetry.execute(
|
||||
[:pleroma, :connection_pool, :client, :add],
|
||||
%{client_pid: client_pid, clients: used_by},
|
||||
%{key: state.key, protocol: protocol}
|
||||
)
|
||||
|
||||
state =
|
||||
if state.timer != nil do
|
||||
Process.cancel_timer(state[:timer])
|
||||
|
|
@ -83,25 +93,18 @@ defmodule Pleroma.Gun.ConnectionPool.Worker do
|
|||
end)
|
||||
|
||||
{ref, state} = pop_in(state.client_monitors[client_pid])
|
||||
# DOWN message can receive right after `remove_client` call and cause worker to terminate
|
||||
state =
|
||||
if is_nil(ref) do
|
||||
state
|
||||
|
||||
Process.demonitor(ref, [:flush])
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
%{state | timer: timer}
|
||||
nil
|
||||
end
|
||||
|
||||
{:reply, :ok, state, :hibernate}
|
||||
{:reply, :ok, %{state | timer: timer}, :hibernate}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
|
@ -131,7 +134,7 @@ defmodule Pleroma.Gun.ConnectionPool.Worker do
|
|||
@impl true
|
||||
def handle_info({:DOWN, _ref, :process, pid, reason}, state) do
|
||||
:telemetry.execute(
|
||||
[:pleroma, :connection_pool, :client_death],
|
||||
[:pleroma, :connection_pool, :client, :dead],
|
||||
%{client_pid: pid, reason: reason},
|
||||
%{key: state.key}
|
||||
)
|
||||
|
|
|
|||
162
lib/pleroma/helpers/media_helper.ex
Normal file
162
lib/pleroma/helpers/media_helper.ex
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Helpers.MediaHelper do
|
||||
@moduledoc """
|
||||
Handles common media-related operations.
|
||||
"""
|
||||
|
||||
alias Pleroma.HTTP
|
||||
|
||||
require Logger
|
||||
|
||||
def missing_dependencies do
|
||||
Enum.reduce([imagemagick: "convert", ffmpeg: "ffmpeg"], [], fn {sym, executable}, acc ->
|
||||
if Pleroma.Utils.command_available?(executable) do
|
||||
acc
|
||||
else
|
||||
[sym | acc]
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
def image_resize(url, options) do
|
||||
with executable when is_binary(executable) <- System.find_executable("convert"),
|
||||
{:ok, args} <- prepare_image_resize_args(options),
|
||||
{:ok, env} <- HTTP.get(url, [], pool: :media),
|
||||
{:ok, fifo_path} <- mkfifo() do
|
||||
args = List.flatten([fifo_path, args])
|
||||
run_fifo(fifo_path, env, executable, args)
|
||||
else
|
||||
nil -> {:error, {:convert, :command_not_found}}
|
||||
{:error, _} = error -> error
|
||||
end
|
||||
end
|
||||
|
||||
defp prepare_image_resize_args(
|
||||
%{max_width: max_width, max_height: max_height, format: "png"} = options
|
||||
) do
|
||||
quality = options[:quality] || 85
|
||||
resize = Enum.join([max_width, "x", max_height, ">"])
|
||||
|
||||
args = [
|
||||
"-resize",
|
||||
resize,
|
||||
"-quality",
|
||||
to_string(quality),
|
||||
"png:-"
|
||||
]
|
||||
|
||||
{:ok, args}
|
||||
end
|
||||
|
||||
defp prepare_image_resize_args(%{max_width: max_width, max_height: max_height} = options) do
|
||||
quality = options[:quality] || 85
|
||||
resize = Enum.join([max_width, "x", max_height, ">"])
|
||||
|
||||
args = [
|
||||
"-interlace",
|
||||
"Plane",
|
||||
"-resize",
|
||||
resize,
|
||||
"-quality",
|
||||
to_string(quality),
|
||||
"jpg:-"
|
||||
]
|
||||
|
||||
{:ok, args}
|
||||
end
|
||||
|
||||
defp prepare_image_resize_args(_), do: {:error, :missing_options}
|
||||
|
||||
# Note: video thumbnail is intentionally not resized (always has original dimensions)
|
||||
def video_framegrab(url) do
|
||||
with executable when is_binary(executable) <- System.find_executable("ffmpeg"),
|
||||
{:ok, env} <- HTTP.get(url, [], pool: :media),
|
||||
{:ok, fifo_path} <- mkfifo(),
|
||||
args = [
|
||||
"-y",
|
||||
"-i",
|
||||
fifo_path,
|
||||
"-vframes",
|
||||
"1",
|
||||
"-f",
|
||||
"mjpeg",
|
||||
"-loglevel",
|
||||
"error",
|
||||
"-"
|
||||
] do
|
||||
run_fifo(fifo_path, env, executable, args)
|
||||
else
|
||||
nil -> {:error, {:ffmpeg, :command_not_found}}
|
||||
{:error, _} = error -> error
|
||||
end
|
||||
end
|
||||
|
||||
defp run_fifo(fifo_path, env, executable, args) do
|
||||
pid =
|
||||
Port.open({:spawn_executable, executable}, [
|
||||
:use_stdio,
|
||||
:stream,
|
||||
:exit_status,
|
||||
:binary,
|
||||
args: args
|
||||
])
|
||||
|
||||
fifo = Port.open(to_charlist(fifo_path), [:eof, :binary, :stream, :out])
|
||||
fix = Pleroma.Helpers.QtFastStart.fix(env.body)
|
||||
true = Port.command(fifo, fix)
|
||||
:erlang.port_close(fifo)
|
||||
loop_recv(pid)
|
||||
after
|
||||
File.rm(fifo_path)
|
||||
end
|
||||
|
||||
defp mkfifo do
|
||||
path = Path.join(System.tmp_dir!(), "pleroma-media-preview-pipe-#{Ecto.UUID.generate()}")
|
||||
|
||||
case System.cmd("mkfifo", [path]) do
|
||||
{_, 0} ->
|
||||
spawn(fifo_guard(path))
|
||||
{:ok, path}
|
||||
|
||||
{_, err} ->
|
||||
{:error, {:fifo_failed, err}}
|
||||
end
|
||||
end
|
||||
|
||||
defp fifo_guard(path) do
|
||||
pid = self()
|
||||
|
||||
fn ->
|
||||
ref = Process.monitor(pid)
|
||||
|
||||
receive do
|
||||
{:DOWN, ^ref, :process, ^pid, _} ->
|
||||
File.rm(path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp loop_recv(pid) do
|
||||
loop_recv(pid, <<>>)
|
||||
end
|
||||
|
||||
defp loop_recv(pid, acc) do
|
||||
receive do
|
||||
{^pid, {:data, data}} ->
|
||||
loop_recv(pid, acc <> data)
|
||||
|
||||
{^pid, {:exit_status, 0}} ->
|
||||
{:ok, acc}
|
||||
|
||||
{^pid, {:exit_status, status}} ->
|
||||
{:error, status}
|
||||
after
|
||||
5000 ->
|
||||
:erlang.port_close(pid)
|
||||
{:error, :timeout}
|
||||
end
|
||||
end
|
||||
end
|
||||
131
lib/pleroma/helpers/qt_fast_start.ex
Normal file
131
lib/pleroma/helpers/qt_fast_start.ex
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Helpers.QtFastStart do
|
||||
@moduledoc """
|
||||
(WIP) Converts a "slow start" (data before metadatas) mov/mp4 file to a "fast start" one (metadatas before data).
|
||||
"""
|
||||
|
||||
# TODO: Cleanup and optimizations
|
||||
# Inspirations: https://www.ffmpeg.org/doxygen/3.4/qt-faststart_8c_source.html
|
||||
# https://github.com/danielgtaylor/qtfaststart/blob/master/qtfaststart/processor.py
|
||||
# ISO/IEC 14496-12:2015, ISO/IEC 15444-12:2015
|
||||
# Paracetamol
|
||||
|
||||
def fix(<<0x00, 0x00, 0x00, _, 0x66, 0x74, 0x79, 0x70, _::bits>> = binary) do
|
||||
index = fix(binary, 0, nil, nil, [])
|
||||
|
||||
case index do
|
||||
:abort -> binary
|
||||
[{"ftyp", _, _, _, _}, {"mdat", _, _, _, _} | _] -> faststart(index)
|
||||
[{"ftyp", _, _, _, _}, {"free", _, _, _, _}, {"mdat", _, _, _, _} | _] -> faststart(index)
|
||||
_ -> binary
|
||||
end
|
||||
end
|
||||
|
||||
def fix(binary) do
|
||||
binary
|
||||
end
|
||||
|
||||
# MOOV have been seen before MDAT- abort
|
||||
defp fix(<<_::bits>>, _, true, false, _) do
|
||||
:abort
|
||||
end
|
||||
|
||||
defp fix(
|
||||
<<size::integer-big-size(32), fourcc::bits-size(32), rest::bits>>,
|
||||
pos,
|
||||
got_moov,
|
||||
got_mdat,
|
||||
acc
|
||||
) do
|
||||
full_size = (size - 8) * 8
|
||||
<<data::bits-size(full_size), rest::bits>> = rest
|
||||
|
||||
acc = [
|
||||
{fourcc, pos, pos + size, size,
|
||||
<<size::integer-big-size(32), fourcc::bits-size(32), data::bits>>}
|
||||
| acc
|
||||
]
|
||||
|
||||
fix(rest, pos + size, got_moov || fourcc == "moov", got_mdat || fourcc == "mdat", acc)
|
||||
end
|
||||
|
||||
defp fix(<<>>, _pos, _, _, acc) do
|
||||
:lists.reverse(acc)
|
||||
end
|
||||
|
||||
defp faststart(index) do
|
||||
{{_ftyp, _, _, _, ftyp}, index} = List.keytake(index, "ftyp", 0)
|
||||
|
||||
# Skip re-writing the free fourcc as it's kind of useless.
|
||||
# Why stream useless bytes when you can do without?
|
||||
{free_size, index} =
|
||||
case List.keytake(index, "free", 0) do
|
||||
{{_, _, _, size, _}, index} -> {size, index}
|
||||
_ -> {0, index}
|
||||
end
|
||||
|
||||
{{_moov, _, _, moov_size, moov}, index} = List.keytake(index, "moov", 0)
|
||||
offset = -free_size + moov_size
|
||||
rest = for {_, _, _, _, data} <- index, do: data, into: []
|
||||
<<moov_head::bits-size(64), moov_data::bits>> = moov
|
||||
[ftyp, moov_head, fix_moov(moov_data, offset, []), rest]
|
||||
end
|
||||
|
||||
defp fix_moov(
|
||||
<<size::integer-big-size(32), fourcc::bits-size(32), rest::bits>>,
|
||||
offset,
|
||||
acc
|
||||
) do
|
||||
full_size = (size - 8) * 8
|
||||
<<data::bits-size(full_size), rest::bits>> = rest
|
||||
|
||||
data =
|
||||
cond do
|
||||
fourcc in ["trak", "mdia", "minf", "stbl"] ->
|
||||
# Theses contains sto or co64 part
|
||||
[<<size::integer-big-size(32), fourcc::bits-size(32)>>, fix_moov(data, offset, [])]
|
||||
|
||||
fourcc in ["stco", "co64"] ->
|
||||
# fix the damn thing
|
||||
<<version::integer-big-size(32), count::integer-big-size(32), rest::bits>> = data
|
||||
|
||||
entry_size =
|
||||
case fourcc do
|
||||
"stco" -> 32
|
||||
"co64" -> 64
|
||||
end
|
||||
|
||||
[
|
||||
<<size::integer-big-size(32), fourcc::bits-size(32), version::integer-big-size(32),
|
||||
count::integer-big-size(32)>>,
|
||||
rewrite_entries(entry_size, offset, rest, [])
|
||||
]
|
||||
|
||||
true ->
|
||||
[<<size::integer-big-size(32), fourcc::bits-size(32)>>, data]
|
||||
end
|
||||
|
||||
acc = [acc | data]
|
||||
fix_moov(rest, offset, acc)
|
||||
end
|
||||
|
||||
defp fix_moov(<<>>, _, acc), do: acc
|
||||
|
||||
for size <- [32, 64] do
|
||||
defp rewrite_entries(
|
||||
unquote(size),
|
||||
offset,
|
||||
<<pos::integer-big-size(unquote(size)), rest::bits>>,
|
||||
acc
|
||||
) do
|
||||
rewrite_entries(unquote(size), offset, rest, [
|
||||
acc | <<pos + offset::integer-big-size(unquote(size))>>
|
||||
])
|
||||
end
|
||||
end
|
||||
|
||||
defp rewrite_entries(_, _, <<>>, acc), do: acc
|
||||
end
|
||||
|
|
@ -3,18 +3,22 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Helpers.UriHelper do
|
||||
def append_uri_params(uri, appended_params) do
|
||||
def modify_uri_params(uri, overridden_params, deleted_params \\ []) do
|
||||
uri = URI.parse(uri)
|
||||
appended_params = for {k, v} <- appended_params, into: %{}, do: {to_string(k), v}
|
||||
existing_params = URI.query_decoder(uri.query || "") |> Enum.into(%{})
|
||||
updated_params_keys = Enum.uniq(Map.keys(existing_params) ++ Map.keys(appended_params))
|
||||
|
||||
existing_params = URI.query_decoder(uri.query || "") |> Map.new()
|
||||
overridden_params = Map.new(overridden_params, fn {k, v} -> {to_string(k), v} end)
|
||||
deleted_params = Enum.map(deleted_params, &to_string/1)
|
||||
|
||||
updated_params =
|
||||
for k <- updated_params_keys, do: {k, appended_params[k] || existing_params[k]}
|
||||
existing_params
|
||||
|> Map.merge(overridden_params)
|
||||
|> Map.drop(deleted_params)
|
||||
|
||||
uri
|
||||
|> Map.put(:query, URI.encode_query(updated_params))
|
||||
|> URI.to_string()
|
||||
|> String.replace_suffix("?", "")
|
||||
end
|
||||
|
||||
def maybe_add_base("/" <> uri, base), do: Path.join([base, uri])
|
||||
|
|
|
|||
12
lib/pleroma/http/web_push.ex
Normal file
12
lib/pleroma/http/web_push.ex
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.HTTP.WebPush do
|
||||
@moduledoc false
|
||||
|
||||
def post(url, payload, headers) do
|
||||
list_headers = Map.to_list(headers)
|
||||
Pleroma.HTTP.post(url, payload, list_headers)
|
||||
end
|
||||
end
|
||||
|
|
@ -156,16 +156,12 @@ defmodule Pleroma.Instances.Instance do
|
|||
defp scrape_favicon(%URI{} = instance_uri) do
|
||||
try do
|
||||
with {:ok, %Tesla.Env{body: html}} <-
|
||||
Pleroma.HTTP.get(to_string(instance_uri), [{"accept", "text/html"}],
|
||||
adapter: [pool: :media]
|
||||
),
|
||||
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
|
||||
Pleroma.HTTP.get(to_string(instance_uri), [{"accept", "text/html"}], pool: :media),
|
||||
{_, [favicon_rel | _]} when is_binary(favicon_rel) <-
|
||||
{:parse,
|
||||
html |> Floki.parse_document!() |> Floki.attribute("link[rel=icon]", "href")},
|
||||
{_, favicon} when is_binary(favicon) <-
|
||||
{:merge, URI.merge(instance_uri, favicon_rel) |> to_string()} do
|
||||
favicon
|
||||
else
|
||||
_ -> nil
|
||||
|
|
|
|||
|
|
@ -19,13 +19,13 @@ defmodule Pleroma.MigrationHelper.NotificationBackfill do
|
|||
query
|
||||
|> Repo.chunk_stream(100)
|
||||
|> Enum.each(fn notification ->
|
||||
type =
|
||||
notification.activity
|
||||
|> type_from_activity()
|
||||
if notification.activity do
|
||||
type = type_from_activity(notification.activity)
|
||||
|
||||
notification
|
||||
|> Ecto.Changeset.change(%{type: type})
|
||||
|> Repo.update()
|
||||
notification
|
||||
|> Ecto.Changeset.change(%{type: type})
|
||||
|> Repo.update()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
|
|
@ -72,8 +72,7 @@ defmodule Pleroma.MigrationHelper.NotificationBackfill do
|
|||
"pleroma:emoji_reaction"
|
||||
|
||||
"Create" ->
|
||||
activity
|
||||
|> type_from_activity_object()
|
||||
type_from_activity_object(activity)
|
||||
|
||||
t ->
|
||||
raise "No notification type for activity type #{t}"
|
||||
|
|
|
|||
|
|
@ -320,6 +320,19 @@ defmodule Pleroma.ModerationLog do
|
|||
|> insert_log_entry_with_message()
|
||||
end
|
||||
|
||||
@spec insert_log(%{actor: User, action: String.t(), subject_id: String.t()}) ::
|
||||
{:ok, ModerationLog} | {:error, any}
|
||||
def insert_log(%{actor: %User{} = actor, action: "chat_message_delete", subject_id: subject_id}) do
|
||||
%ModerationLog{
|
||||
data: %{
|
||||
"actor" => %{"nickname" => actor.nickname},
|
||||
"action" => "chat_message_delete",
|
||||
"subject_id" => subject_id
|
||||
}
|
||||
}
|
||||
|> insert_log_entry_with_message()
|
||||
end
|
||||
|
||||
@spec insert_log_entry_with_message(ModerationLog) :: {:ok, ModerationLog} | {:error, any}
|
||||
defp insert_log_entry_with_message(entry) do
|
||||
entry.data["message"]
|
||||
|
|
@ -627,6 +640,17 @@ defmodule Pleroma.ModerationLog do
|
|||
"@#{actor_nickname} updated users: #{users_to_nicknames_string(subjects)}"
|
||||
end
|
||||
|
||||
@spec get_log_entry_message(ModerationLog) :: String.t()
|
||||
def get_log_entry_message(%ModerationLog{
|
||||
data: %{
|
||||
"actor" => %{"nickname" => actor_nickname},
|
||||
"action" => "chat_message_delete",
|
||||
"subject_id" => subject_id
|
||||
}
|
||||
}) do
|
||||
"@#{actor_nickname} deleted chat message ##{subject_id}"
|
||||
end
|
||||
|
||||
defp nicknames_to_string(nicknames) do
|
||||
nicknames
|
||||
|> Enum.map(&"@#{&1}")
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ defmodule Pleroma.Object.Fetcher do
|
|||
alias Pleroma.Web.ActivityPub.ObjectValidator
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
alias Pleroma.Web.Federator
|
||||
alias Pleroma.Web.FedSockets
|
||||
|
||||
require Logger
|
||||
require Pleroma.Constants
|
||||
|
|
@ -98,8 +99,8 @@ defmodule Pleroma.Object.Fetcher do
|
|||
{:containment, _} ->
|
||||
{:error, "Object containment failed."}
|
||||
|
||||
{:transmogrifier, {:error, {:reject, nil}}} ->
|
||||
{:reject, nil}
|
||||
{:transmogrifier, {:error, {:reject, e}}} ->
|
||||
{:reject, e}
|
||||
|
||||
{:transmogrifier, _} = e ->
|
||||
{:error, e}
|
||||
|
|
@ -182,27 +183,20 @@ defmodule Pleroma.Object.Fetcher do
|
|||
end
|
||||
end
|
||||
|
||||
def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do
|
||||
def fetch_and_contain_remote_object_from_id(prm, opts \\ [])
|
||||
|
||||
def fetch_and_contain_remote_object_from_id(%{"id" => id}, opts),
|
||||
do: fetch_and_contain_remote_object_from_id(id, opts)
|
||||
|
||||
def fetch_and_contain_remote_object_from_id(id, opts) when is_binary(id) do
|
||||
Logger.debug("Fetching object #{id} via AP")
|
||||
|
||||
date = Pleroma.Signature.signed_date()
|
||||
|
||||
headers =
|
||||
[{"accept", "application/activity+json"}]
|
||||
|> maybe_date_fetch(date)
|
||||
|> sign_fetch(id, date)
|
||||
|
||||
Logger.debug("Fetch headers: #{inspect(headers)}")
|
||||
|
||||
with {:scheme, true} <- {:scheme, String.starts_with?(id, "http")},
|
||||
{:ok, %{body: body, status: code}} when code in 200..299 <- HTTP.get(id, headers),
|
||||
{:ok, data} <- Jason.decode(body),
|
||||
{:ok, body} <- get_object(id, opts),
|
||||
{:ok, data} <- safe_json_decode(body),
|
||||
:ok <- Containment.contain_origin_from_id(id, data) do
|
||||
{:ok, data}
|
||||
else
|
||||
{:ok, %{status: code}} when code in [404, 410] ->
|
||||
{:error, "Object has been deleted"}
|
||||
|
||||
{:scheme, _} ->
|
||||
{:error, "Unsupported URI scheme"}
|
||||
|
||||
|
|
@ -214,8 +208,44 @@ defmodule Pleroma.Object.Fetcher do
|
|||
end
|
||||
end
|
||||
|
||||
def fetch_and_contain_remote_object_from_id(%{"id" => id}),
|
||||
do: fetch_and_contain_remote_object_from_id(id)
|
||||
def fetch_and_contain_remote_object_from_id(_id, _opts),
|
||||
do: {:error, "id must be a string"}
|
||||
|
||||
def fetch_and_contain_remote_object_from_id(_id), do: {:error, "id must be a string"}
|
||||
defp get_object(id, opts) do
|
||||
with false <- Keyword.get(opts, :force_http, false),
|
||||
{:ok, fedsocket} <- FedSockets.get_or_create_fed_socket(id) do
|
||||
Logger.debug("fetching via fedsocket - #{inspect(id)}")
|
||||
FedSockets.fetch(fedsocket, id)
|
||||
else
|
||||
_other ->
|
||||
Logger.debug("fetching via http - #{inspect(id)}")
|
||||
get_object_http(id)
|
||||
end
|
||||
end
|
||||
|
||||
defp get_object_http(id) do
|
||||
date = Pleroma.Signature.signed_date()
|
||||
|
||||
headers =
|
||||
[{"accept", "application/activity+json"}]
|
||||
|> maybe_date_fetch(date)
|
||||
|> sign_fetch(id, date)
|
||||
|
||||
case HTTP.get(id, headers) do
|
||||
{:ok, %{body: body, status: code}} when code in 200..299 ->
|
||||
{:ok, body}
|
||||
|
||||
{:ok, %{status: code}} when code in [404, 410] ->
|
||||
{:error, "Object has been deleted"}
|
||||
|
||||
{:error, e} ->
|
||||
{:error, e}
|
||||
|
||||
e ->
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
defp safe_json_decode(nil), do: {:ok, nil}
|
||||
defp safe_json_decode(json), do: Jason.decode(json)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do
|
|||
|> assign(:token, nil)
|
||||
end
|
||||
|
||||
@doc "Filters descendants of supported scopes"
|
||||
@doc "Keeps those of `scopes` which are descendants of `supported_scopes`"
|
||||
def filter_descendants(scopes, supported_scopes) do
|
||||
Enum.filter(
|
||||
scopes,
|
||||
|
|
|
|||
|
|
@ -7,48 +7,42 @@ defmodule Pleroma.Plugs.RemoteIp do
|
|||
This is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration.
|
||||
"""
|
||||
|
||||
alias Pleroma.Config
|
||||
import Plug.Conn
|
||||
|
||||
@behaviour Plug
|
||||
|
||||
@headers ~w[
|
||||
x-forwarded-for
|
||||
]
|
||||
|
||||
# https://en.wikipedia.org/wiki/Localhost
|
||||
# https://en.wikipedia.org/wiki/Private_network
|
||||
@reserved ~w[
|
||||
127.0.0.0/8
|
||||
::1/128
|
||||
fc00::/7
|
||||
10.0.0.0/8
|
||||
172.16.0.0/12
|
||||
192.168.0.0/16
|
||||
]
|
||||
|
||||
def init(_), do: nil
|
||||
|
||||
def call(%{remote_ip: original_remote_ip} = conn, _) do
|
||||
config = Pleroma.Config.get(__MODULE__, [])
|
||||
|
||||
if Keyword.get(config, :enabled, false) do
|
||||
%{remote_ip: new_remote_ip} = conn = RemoteIp.call(conn, remote_ip_opts(config))
|
||||
if Config.get([__MODULE__, :enabled]) do
|
||||
%{remote_ip: new_remote_ip} = conn = RemoteIp.call(conn, remote_ip_opts())
|
||||
assign(conn, :remote_ip_found, original_remote_ip != new_remote_ip)
|
||||
else
|
||||
conn
|
||||
end
|
||||
end
|
||||
|
||||
defp remote_ip_opts(config) do
|
||||
headers = config |> Keyword.get(:headers, @headers) |> MapSet.new()
|
||||
reserved = Keyword.get(config, :reserved, @reserved)
|
||||
defp remote_ip_opts do
|
||||
headers = Config.get([__MODULE__, :headers], []) |> MapSet.new()
|
||||
reserved = Config.get([__MODULE__, :reserved], [])
|
||||
|
||||
proxies =
|
||||
config
|
||||
|> Keyword.get(:proxies, [])
|
||||
Config.get([__MODULE__, :proxies], [])
|
||||
|> Enum.concat(reserved)
|
||||
|> Enum.map(&InetCidr.parse/1)
|
||||
|> Enum.map(&maybe_add_cidr/1)
|
||||
|
||||
{headers, proxies}
|
||||
end
|
||||
|
||||
defp maybe_add_cidr(proxy) when is_binary(proxy) do
|
||||
proxy =
|
||||
cond do
|
||||
"/" in String.codepoints(proxy) -> proxy
|
||||
InetCidr.v4?(InetCidr.parse_address!(proxy)) -> proxy <> "/32"
|
||||
InetCidr.v6?(InetCidr.parse_address!(proxy)) -> proxy <> "/128"
|
||||
end
|
||||
|
||||
InetCidr.parse(proxy, true)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -49,7 +49,21 @@ defmodule Pleroma.Repo do
|
|||
end
|
||||
end
|
||||
|
||||
def chunk_stream(query, chunk_size) do
|
||||
@doc """
|
||||
Returns a lazy enumerable that emits all entries from the data store matching the given query.
|
||||
|
||||
`returns_as` use to group records. use the `batches` option to fetch records in bulk.
|
||||
|
||||
## Examples
|
||||
|
||||
# fetch records one-by-one
|
||||
iex> Pleroma.Repo.chunk_stream(Pleroma.Activity.Queries.by_actor(ap_id), 500)
|
||||
|
||||
# fetch records in bulk
|
||||
iex> Pleroma.Repo.chunk_stream(Pleroma.Activity.Queries.by_actor(ap_id), 500, :batches)
|
||||
"""
|
||||
@spec chunk_stream(Ecto.Query.t(), integer(), atom()) :: Enumerable.t()
|
||||
def chunk_stream(query, chunk_size, returns_as \\ :one) 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
|
||||
# have individual records be the elements of the stream, instead of
|
||||
|
|
@ -69,7 +83,12 @@ defmodule Pleroma.Repo do
|
|||
|
||||
records ->
|
||||
last_id = List.last(records).id
|
||||
{records, last_id}
|
||||
|
||||
if returns_as == :one do
|
||||
{records, last_id}
|
||||
else
|
||||
{[records], last_id}
|
||||
end
|
||||
end
|
||||
end,
|
||||
fn _ -> :ok end
|
||||
|
|
|
|||
|
|
@ -1,34 +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.RepoStreamer do
|
||||
alias Pleroma.Repo
|
||||
import Ecto.Query
|
||||
|
||||
def chunk_stream(query, chunk_size) do
|
||||
Stream.unfold(0, fn
|
||||
:halt ->
|
||||
{[], :halt}
|
||||
|
||||
last_id ->
|
||||
query
|
||||
|> order_by(asc: :id)
|
||||
|> where([r], r.id > ^last_id)
|
||||
|> limit(^chunk_size)
|
||||
|> Repo.all()
|
||||
|> case do
|
||||
[] ->
|
||||
{[], :halt}
|
||||
|
||||
records ->
|
||||
last_id = List.last(records).id
|
||||
{records, last_id}
|
||||
end
|
||||
end)
|
||||
|> Stream.take_while(fn
|
||||
[] -> false
|
||||
_ -> true
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
@ -17,6 +17,9 @@ defmodule Pleroma.ReverseProxy do
|
|||
@failed_request_ttl :timer.seconds(60)
|
||||
@methods ~w(GET HEAD)
|
||||
|
||||
def max_read_duration_default, do: @max_read_duration
|
||||
def default_cache_control_header, do: @default_cache_control_header
|
||||
|
||||
@moduledoc """
|
||||
A reverse proxy.
|
||||
|
||||
|
|
@ -391,6 +394,8 @@ defmodule Pleroma.ReverseProxy do
|
|||
|
||||
defp body_size_constraint(_, _), do: :ok
|
||||
|
||||
defp check_read_duration(nil = _duration, max), do: check_read_duration(@max_read_duration, max)
|
||||
|
||||
defp check_read_duration(duration, max)
|
||||
when is_integer(duration) and is_integer(max) and max > 0 do
|
||||
if duration > max do
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ defmodule Pleroma.Signature do
|
|||
def fetch_public_key(conn) do
|
||||
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
|
||||
{:ok, actor_id} <- key_id_to_actor_id(kid),
|
||||
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
|
||||
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id, force_http: true) do
|
||||
{:ok, public_key}
|
||||
else
|
||||
e ->
|
||||
|
|
@ -50,8 +50,8 @@ defmodule Pleroma.Signature do
|
|||
def refetch_public_key(conn) do
|
||||
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
|
||||
{:ok, actor_id} <- key_id_to_actor_id(kid),
|
||||
{:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id),
|
||||
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
|
||||
{:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id, force_http: true),
|
||||
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id, force_http: true) do
|
||||
{:ok, public_key}
|
||||
else
|
||||
e ->
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ defmodule Pleroma.Telemetry.Logger do
|
|||
[:pleroma, :connection_pool, :reclaim, :start],
|
||||
[:pleroma, :connection_pool, :reclaim, :stop],
|
||||
[:pleroma, :connection_pool, :provision_failure],
|
||||
[:pleroma, :connection_pool, :client_death]
|
||||
[:pleroma, :connection_pool, :client, :dead],
|
||||
[:pleroma, :connection_pool, :client, :add]
|
||||
]
|
||||
def attach do
|
||||
:telemetry.attach_many("pleroma-logger", @events, &handle_event/4, [])
|
||||
|
|
@ -62,7 +63,7 @@ defmodule Pleroma.Telemetry.Logger do
|
|||
end
|
||||
|
||||
def handle_event(
|
||||
[:pleroma, :connection_pool, :client_death],
|
||||
[:pleroma, :connection_pool, :client, :dead],
|
||||
%{client_pid: client_pid, reason: reason},
|
||||
%{key: key},
|
||||
_
|
||||
|
|
@ -73,4 +74,17 @@ defmodule Pleroma.Telemetry.Logger do
|
|||
}"
|
||||
end)
|
||||
end
|
||||
|
||||
def handle_event(
|
||||
[:pleroma, :connection_pool, :client, :add],
|
||||
%{clients: [_, _ | _] = clients},
|
||||
%{key: key, protocol: :http},
|
||||
_
|
||||
) do
|
||||
Logger.info(fn ->
|
||||
"Pool worker for #{key}: #{length(clients)} clients are using an HTTP1 connection at the same time, head-of-line blocking might occur."
|
||||
end)
|
||||
end
|
||||
|
||||
def handle_event([:pleroma, :connection_pool, :client, :add], _, _, _), do: :ok
|
||||
end
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ defmodule Pleroma.User do
|
|||
alias Pleroma.Object
|
||||
alias Pleroma.Registration
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.RepoStreamer
|
||||
alias Pleroma.User
|
||||
alias Pleroma.UserRelationship
|
||||
alias Pleroma.Web
|
||||
|
|
@ -276,9 +275,9 @@ defmodule Pleroma.User do
|
|||
@spec account_status(User.t()) :: account_status()
|
||||
def account_status(%User{deactivated: true}), do: :deactivated
|
||||
def account_status(%User{password_reset_pending: true}), do: :password_reset_pending
|
||||
def account_status(%User{approval_pending: true}), do: :approval_pending
|
||||
def account_status(%User{local: true, approval_pending: true}), do: :approval_pending
|
||||
|
||||
def account_status(%User{confirmation_pending: true}) do
|
||||
def account_status(%User{local: true, confirmation_pending: true}) do
|
||||
if Config.get([:instance, :account_activation_required]) do
|
||||
:confirmation_pending
|
||||
else
|
||||
|
|
@ -814,7 +813,8 @@ defmodule Pleroma.User do
|
|||
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
|
||||
def try_send_confirmation_email(%User{confirmation_pending: true, email: email} = user)
|
||||
when is_binary(email) do
|
||||
if Config.get([:instance, :account_activation_required]) do
|
||||
send_confirmation_email(user)
|
||||
{:ok, :enqueued}
|
||||
|
|
@ -915,9 +915,7 @@ defmodule Pleroma.User do
|
|||
FollowingRelationship.unfollow(follower, followed)
|
||||
{:ok, followed} = update_follower_count(followed)
|
||||
|
||||
{:ok, follower} =
|
||||
follower
|
||||
|> update_following_count()
|
||||
{:ok, follower} = update_following_count(follower)
|
||||
|
||||
{:ok, follower, followed}
|
||||
|
||||
|
|
@ -1686,42 +1684,6 @@ defmodule Pleroma.User do
|
|||
|
||||
def perform(:deactivate_async, user, status), do: deactivate(user, status)
|
||||
|
||||
@spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
|
||||
def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
|
||||
when is_list(blocked_identifiers) do
|
||||
Enum.map(
|
||||
blocked_identifiers,
|
||||
fn blocked_identifier ->
|
||||
with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
|
||||
{:ok, _block} <- CommonAPI.block(blocker, blocked) do
|
||||
blocked
|
||||
else
|
||||
err ->
|
||||
Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
|
||||
err
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
def perform(:follow_import, %User{} = follower, followed_identifiers)
|
||||
when is_list(followed_identifiers) do
|
||||
Enum.map(
|
||||
followed_identifiers,
|
||||
fn followed_identifier ->
|
||||
with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
|
||||
{:ok, follower} <- maybe_direct_follow(follower, followed),
|
||||
{:ok, _, _, _} <- CommonAPI.follow(follower, followed) do
|
||||
followed
|
||||
else
|
||||
err ->
|
||||
Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
|
||||
err
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
@spec external_users_query() :: Ecto.Query.t()
|
||||
def external_users_query do
|
||||
User.Query.build(%{
|
||||
|
|
@ -1750,21 +1712,6 @@ defmodule Pleroma.User do
|
|||
Repo.all(query)
|
||||
end
|
||||
|
||||
def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
|
||||
BackgroundWorker.enqueue("blocks_import", %{
|
||||
"blocker_id" => blocker.id,
|
||||
"blocked_identifiers" => blocked_identifiers
|
||||
})
|
||||
end
|
||||
|
||||
def follow_import(%User{} = follower, followed_identifiers)
|
||||
when is_list(followed_identifiers) do
|
||||
BackgroundWorker.enqueue("follow_import", %{
|
||||
"follower_id" => follower.id,
|
||||
"followed_identifiers" => followed_identifiers
|
||||
})
|
||||
end
|
||||
|
||||
def delete_notifications_from_user_activities(%User{ap_id: ap_id}) do
|
||||
Notification
|
||||
|> join(:inner, [n], activity in assoc(n, :activity))
|
||||
|
|
@ -1775,7 +1722,7 @@ defmodule Pleroma.User do
|
|||
def delete_user_activities(%User{ap_id: ap_id} = user) do
|
||||
ap_id
|
||||
|> Activity.Queries.by_actor()
|
||||
|> RepoStreamer.chunk_stream(50)
|
||||
|> Repo.chunk_stream(50, :batches)
|
||||
|> Stream.each(fn activities ->
|
||||
Enum.each(activities, fn activity -> delete_activity(activity, user) end)
|
||||
end)
|
||||
|
|
@ -1821,12 +1768,12 @@ defmodule Pleroma.User do
|
|||
|
||||
def html_filter_policy(_), do: Config.get([:markup, :scrub_policy])
|
||||
|
||||
def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
|
||||
def fetch_by_ap_id(ap_id, opts \\ []), do: ActivityPub.make_user_from_ap_id(ap_id, opts)
|
||||
|
||||
def get_or_fetch_by_ap_id(ap_id) do
|
||||
def get_or_fetch_by_ap_id(ap_id, opts \\ []) do
|
||||
cached_user = get_cached_by_ap_id(ap_id)
|
||||
|
||||
maybe_fetched_user = needs_update?(cached_user) && fetch_by_ap_id(ap_id)
|
||||
maybe_fetched_user = needs_update?(cached_user) && fetch_by_ap_id(ap_id, opts)
|
||||
|
||||
case {cached_user, maybe_fetched_user} do
|
||||
{_, {:ok, %User{} = user}} ->
|
||||
|
|
@ -1899,8 +1846,8 @@ defmodule Pleroma.User do
|
|||
|
||||
def public_key(_), do: {:error, "key not found"}
|
||||
|
||||
def get_public_key_for_ap_id(ap_id) do
|
||||
with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
|
||||
def get_public_key_for_ap_id(ap_id, opts \\ []) do
|
||||
with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id, opts),
|
||||
{:ok, public_key} <- public_key(user) do
|
||||
{:ok, public_key}
|
||||
else
|
||||
|
|
@ -2123,6 +2070,13 @@ defmodule Pleroma.User do
|
|||
Enum.map(users, &toggle_confirmation/1)
|
||||
end
|
||||
|
||||
@spec need_confirmation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
|
||||
def need_confirmation(%User{} = user, bool) do
|
||||
user
|
||||
|> confirmation_changeset(need_confirmation: bool)
|
||||
|> update_and_set_cache()
|
||||
end
|
||||
|
||||
def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
|
||||
mascot
|
||||
end
|
||||
|
|
@ -2337,7 +2291,9 @@ defmodule Pleroma.User do
|
|||
|
||||
# if pinned activity was scheduled for deletion, we reschedule it for deletion
|
||||
if data["expires_at"] do
|
||||
{:ok, expires_at, _} = DateTime.from_iso8601(data["expires_at"])
|
||||
# MRF.ActivityExpirationPolicy used UTC timestamps for expires_at in original implementation
|
||||
{:ok, expires_at} =
|
||||
data["expires_at"] |> Pleroma.EctoType.ActivityPub.ObjectValidators.DateTime.cast()
|
||||
|
||||
Pleroma.Workers.PurgeExpiredActivity.enqueue(%{
|
||||
activity_id: id,
|
||||
|
|
|
|||
85
lib/pleroma/user/import.ex
Normal file
85
lib/pleroma/user/import.ex
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.User.Import do
|
||||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Workers.BackgroundWorker
|
||||
|
||||
require Logger
|
||||
|
||||
@spec perform(atom(), User.t(), list()) :: :ok | list() | {:error, any()}
|
||||
def perform(:mutes_import, %User{} = user, [_ | _] = identifiers) do
|
||||
Enum.map(
|
||||
identifiers,
|
||||
fn identifier ->
|
||||
with {:ok, %User{} = muted_user} <- User.get_or_fetch(identifier),
|
||||
{:ok, _} <- User.mute(user, muted_user) do
|
||||
muted_user
|
||||
else
|
||||
error -> handle_error(:mutes_import, identifier, error)
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
def perform(:blocks_import, %User{} = blocker, [_ | _] = identifiers) do
|
||||
Enum.map(
|
||||
identifiers,
|
||||
fn identifier ->
|
||||
with {:ok, %User{} = blocked} <- User.get_or_fetch(identifier),
|
||||
{:ok, _block} <- CommonAPI.block(blocker, blocked) do
|
||||
blocked
|
||||
else
|
||||
error -> handle_error(:blocks_import, identifier, error)
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
def perform(:follow_import, %User{} = follower, [_ | _] = identifiers) do
|
||||
Enum.map(
|
||||
identifiers,
|
||||
fn identifier ->
|
||||
with {:ok, %User{} = followed} <- User.get_or_fetch(identifier),
|
||||
{:ok, follower} <- User.maybe_direct_follow(follower, followed),
|
||||
{:ok, _, _, _} <- CommonAPI.follow(follower, followed) do
|
||||
followed
|
||||
else
|
||||
error -> handle_error(:follow_import, identifier, error)
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
def perform(_, _, _), do: :ok
|
||||
|
||||
defp handle_error(op, user_id, error) do
|
||||
Logger.debug("#{op} failed for #{user_id} with: #{inspect(error)}")
|
||||
error
|
||||
end
|
||||
|
||||
def blocks_import(%User{} = blocker, [_ | _] = identifiers) do
|
||||
BackgroundWorker.enqueue(
|
||||
"blocks_import",
|
||||
%{"user_id" => blocker.id, "identifiers" => identifiers}
|
||||
)
|
||||
end
|
||||
|
||||
def follow_import(%User{} = follower, [_ | _] = identifiers) do
|
||||
BackgroundWorker.enqueue(
|
||||
"follow_import",
|
||||
%{"user_id" => follower.id, "identifiers" => identifiers}
|
||||
)
|
||||
end
|
||||
|
||||
def mutes_import(%User{} = user, [_ | _] = identifiers) do
|
||||
BackgroundWorker.enqueue(
|
||||
"mutes_import",
|
||||
%{"user_id" => user.id, "identifiers" => identifiers}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -47,6 +47,7 @@ defmodule Pleroma.User.Query do
|
|||
is_moderator: boolean(),
|
||||
super_users: boolean(),
|
||||
invisible: boolean(),
|
||||
internal: boolean(),
|
||||
followers: User.t(),
|
||||
friends: User.t(),
|
||||
recipients_from_activity: [String.t()],
|
||||
|
|
@ -80,7 +81,9 @@ defmodule Pleroma.User.Query do
|
|||
end
|
||||
|
||||
defp prepare_query(query, criteria) do
|
||||
Enum.reduce(criteria, query, &compose_query/2)
|
||||
criteria
|
||||
|> Map.put_new(:internal, false)
|
||||
|> Enum.reduce(query, &compose_query/2)
|
||||
end
|
||||
|
||||
defp compose_query({key, value}, query)
|
||||
|
|
@ -107,12 +110,12 @@ defmodule Pleroma.User.Query do
|
|||
where(query, [u], fragment("? && ?", u.tags, ^tags))
|
||||
end
|
||||
|
||||
defp compose_query({:is_admin, _}, query) do
|
||||
where(query, [u], u.is_admin)
|
||||
defp compose_query({:is_admin, bool}, query) do
|
||||
where(query, [u], u.is_admin == ^bool)
|
||||
end
|
||||
|
||||
defp compose_query({:is_moderator, _}, query) do
|
||||
where(query, [u], u.is_moderator)
|
||||
defp compose_query({:is_moderator, bool}, query) do
|
||||
where(query, [u], u.is_moderator == ^bool)
|
||||
end
|
||||
|
||||
defp compose_query({:super_users, _}, query) do
|
||||
|
|
@ -129,14 +132,12 @@ defmodule Pleroma.User.Query do
|
|||
|
||||
defp compose_query({:active, _}, query) do
|
||||
User.restrict_deactivated(query)
|
||||
|> where([u], not is_nil(u.nickname))
|
||||
|> where([u], u.approval_pending == false)
|
||||
end
|
||||
|
||||
defp compose_query({:legacy_active, _}, query) do
|
||||
query
|
||||
|> where([u], fragment("not (?->'deactivated' @> 'true')", u.info))
|
||||
|> where([u], not is_nil(u.nickname))
|
||||
end
|
||||
|
||||
defp compose_query({:deactivated, false}, query) do
|
||||
|
|
@ -145,7 +146,10 @@ defmodule Pleroma.User.Query do
|
|||
|
||||
defp compose_query({:deactivated, true}, query) do
|
||||
where(query, [u], u.deactivated == ^true)
|
||||
|> where([u], not is_nil(u.nickname))
|
||||
end
|
||||
|
||||
defp compose_query({:confirmation_pending, bool}, query) do
|
||||
where(query, [u], u.confirmation_pending == ^bool)
|
||||
end
|
||||
|
||||
defp compose_query({:need_approval, _}, query) do
|
||||
|
|
@ -199,10 +203,15 @@ defmodule Pleroma.User.Query do
|
|||
limit(query, ^limit)
|
||||
end
|
||||
|
||||
defp compose_query({:internal, false}, query) do
|
||||
query
|
||||
|> where([u], not is_nil(u.nickname))
|
||||
|> where([u], not like(u.nickname, "internal.%"))
|
||||
end
|
||||
|
||||
defp compose_query(_unsupported_param, query), do: query
|
||||
|
||||
defp location_query(query, local) do
|
||||
where(query, [u], u.local == ^local)
|
||||
|> where([u], not is_nil(u.nickname))
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.User.Search do
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators.Uri, as: UriType
|
||||
alias Pleroma.Pagination
|
||||
alias Pleroma.User
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
@limit 20
|
||||
|
|
@ -19,16 +21,47 @@ defmodule Pleroma.User.Search do
|
|||
|
||||
query_string = format_query(query_string)
|
||||
|
||||
maybe_resolve(resolve, for_user, query_string)
|
||||
# If this returns anything, it should bounce to the top
|
||||
maybe_resolved = maybe_resolve(resolve, for_user, query_string)
|
||||
|
||||
top_user_ids =
|
||||
[]
|
||||
|> maybe_add_resolved(maybe_resolved)
|
||||
|> maybe_add_ap_id_match(query_string)
|
||||
|> maybe_add_uri_match(query_string)
|
||||
|
||||
results =
|
||||
query_string
|
||||
|> search_query(for_user, following)
|
||||
|> search_query(for_user, following, top_user_ids)
|
||||
|> Pagination.fetch_paginated(%{"offset" => offset, "limit" => result_limit}, :offset)
|
||||
|
||||
results
|
||||
end
|
||||
|
||||
defp maybe_add_resolved(list, {:ok, %User{} = user}) do
|
||||
[user.id | list]
|
||||
end
|
||||
|
||||
defp maybe_add_resolved(list, _), do: list
|
||||
|
||||
defp maybe_add_ap_id_match(list, query) do
|
||||
if user = User.get_cached_by_ap_id(query) do
|
||||
[user.id | list]
|
||||
else
|
||||
list
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_add_uri_match(list, query) do
|
||||
with {:ok, query} <- UriType.cast(query),
|
||||
q = from(u in User, where: u.uri == ^query, select: u.id),
|
||||
users = Pleroma.Repo.all(q) do
|
||||
users ++ list
|
||||
else
|
||||
_ -> list
|
||||
end
|
||||
end
|
||||
|
||||
defp format_query(query_string) do
|
||||
# Strip the beginning @ off if there is a query
|
||||
query_string = String.trim_leading(query_string, "@")
|
||||
|
|
@ -47,21 +80,29 @@ defmodule Pleroma.User.Search do
|
|||
end
|
||||
end
|
||||
|
||||
defp search_query(query_string, for_user, following) do
|
||||
defp search_query(query_string, for_user, following, top_user_ids) do
|
||||
for_user
|
||||
|> base_query(following)
|
||||
|> filter_blocked_user(for_user)
|
||||
|> filter_invisible_users()
|
||||
|> filter_discoverable_users()
|
||||
|> filter_internal_users()
|
||||
|> filter_blocked_domains(for_user)
|
||||
|> fts_search(query_string)
|
||||
|> select_top_users(top_user_ids)
|
||||
|> trigram_rank(query_string)
|
||||
|> boost_search_rank(for_user)
|
||||
|> boost_search_rank(for_user, top_user_ids)
|
||||
|> subquery()
|
||||
|> order_by(desc: :search_rank)
|
||||
|> maybe_restrict_local(for_user)
|
||||
end
|
||||
|
||||
defp select_top_users(query, top_user_ids) do
|
||||
from(u in query,
|
||||
or_where: u.id in ^top_user_ids
|
||||
)
|
||||
end
|
||||
|
||||
defp fts_search(query, query_string) do
|
||||
query_string = to_tsquery(query_string)
|
||||
|
||||
|
|
@ -122,6 +163,10 @@ defmodule Pleroma.User.Search do
|
|||
from(q in query, where: q.invisible == false)
|
||||
end
|
||||
|
||||
defp filter_discoverable_users(query) do
|
||||
from(q in query, where: q.discoverable == true)
|
||||
end
|
||||
|
||||
defp filter_internal_users(query) do
|
||||
from(q in query, where: q.actor_type != "Application")
|
||||
end
|
||||
|
|
@ -175,7 +220,7 @@ defmodule Pleroma.User.Search do
|
|||
|
||||
defp local_domain, do: Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host])
|
||||
|
||||
defp boost_search_rank(query, %User{} = for_user) do
|
||||
defp boost_search_rank(query, %User{} = for_user, top_user_ids) do
|
||||
friends_ids = User.get_friends_ids(for_user)
|
||||
followers_ids = User.get_followers_ids(for_user)
|
||||
|
||||
|
|
@ -187,6 +232,7 @@ defmodule Pleroma.User.Search do
|
|||
CASE WHEN (?) THEN (?) * 1.5
|
||||
WHEN (?) THEN (?) * 1.3
|
||||
WHEN (?) THEN (?) * 1.1
|
||||
WHEN (?) THEN 9001
|
||||
ELSE (?) END
|
||||
""",
|
||||
u.id in ^friends_ids and u.id in ^followers_ids,
|
||||
|
|
@ -195,11 +241,26 @@ defmodule Pleroma.User.Search do
|
|||
u.search_rank,
|
||||
u.id in ^followers_ids,
|
||||
u.search_rank,
|
||||
u.id in ^top_user_ids,
|
||||
u.search_rank
|
||||
)
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
defp boost_search_rank(query, _for_user), do: query
|
||||
defp boost_search_rank(query, _for_user, top_user_ids) do
|
||||
from(u in subquery(query),
|
||||
select_merge: %{
|
||||
search_rank:
|
||||
fragment(
|
||||
"""
|
||||
CASE WHEN (?) THEN 9001
|
||||
ELSE (?) END
|
||||
""",
|
||||
u.id in ^top_user_ids,
|
||||
u.search_rank
|
||||
)
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -24,4 +24,24 @@ defmodule Pleroma.Utils do
|
|||
def command_available?(command) do
|
||||
match?({_output, 0}, System.cmd("sh", ["-c", "command -v #{command}"]))
|
||||
end
|
||||
|
||||
@doc "creates the uniq temporary directory"
|
||||
@spec tmp_dir(String.t()) :: {:ok, String.t()} | {:error, :file.posix()}
|
||||
def tmp_dir(prefix \\ "") do
|
||||
sub_dir =
|
||||
[
|
||||
prefix,
|
||||
Timex.to_unix(Timex.now()),
|
||||
:os.getpid(),
|
||||
String.downcase(Integer.to_string(:rand.uniform(0x100000000), 36))
|
||||
]
|
||||
|> Enum.join("-")
|
||||
|
||||
tmp_dir = Path.join(System.tmp_dir!(), sub_dir)
|
||||
|
||||
case File.mkdir(tmp_dir) do
|
||||
:ok -> {:ok, tmp_dir}
|
||||
error -> error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|
||||
defp increase_replies_count_if_reply(_create_data), do: :noop
|
||||
|
||||
@object_types ~w[ChatMessage Question Answer Audio Event]
|
||||
@object_types ~w[ChatMessage Question Answer Audio Video Event Article]
|
||||
@spec persist(map(), keyword()) :: {:ok, Activity.t() | Object.t()}
|
||||
def persist(%{"type" => type} = object, meta) when type in @object_types do
|
||||
with {:ok, object} <- Object.create(object) do
|
||||
|
|
@ -154,8 +154,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
{:remote_limit_pass, _} ->
|
||||
{:error, :remote_limit}
|
||||
|
||||
{:reject, reason} ->
|
||||
{:error, reason}
|
||||
{:reject, _} = e ->
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -767,7 +767,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
end
|
||||
|
||||
defp restrict_replies(query, %{
|
||||
reply_filtering_user: user,
|
||||
reply_filtering_user: %User{} = user,
|
||||
reply_visibility: "self"
|
||||
}) do
|
||||
from(
|
||||
|
|
@ -783,14 +783,24 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
end
|
||||
|
||||
defp restrict_replies(query, %{
|
||||
reply_filtering_user: user,
|
||||
reply_filtering_user: %User{} = user,
|
||||
reply_visibility: "following"
|
||||
}) do
|
||||
from(
|
||||
[activity, object] in query,
|
||||
where:
|
||||
fragment(
|
||||
"?->>'inReplyTo' is null OR ? && array_remove(?, ?) OR ? = ?",
|
||||
"""
|
||||
?->>'type' != 'Create' -- This isn't a Create
|
||||
OR ?->>'inReplyTo' is null -- this isn't a reply
|
||||
OR ? && array_remove(?, ?) -- The recipient is us or one of our friends,
|
||||
-- unless they are the author (because authors
|
||||
-- are also part of the recipients). This leads
|
||||
-- to a bug that self-replies by friends won't
|
||||
-- show up.
|
||||
OR ? = ? -- The actor is us
|
||||
""",
|
||||
activity.data,
|
||||
object.data,
|
||||
^[user.ap_id | User.get_cached_user_friends_ap_ids(user)],
|
||||
activity.recipients,
|
||||
|
|
@ -841,7 +851,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
from(
|
||||
[activity, object: o] in query,
|
||||
where: fragment("not (? = ANY(?))", activity.actor, ^blocked_ap_ids),
|
||||
where: fragment("not (? && ?)", activity.recipients, ^blocked_ap_ids),
|
||||
where:
|
||||
fragment(
|
||||
"((not (? && ?)) or ? = ?)",
|
||||
activity.recipients,
|
||||
^blocked_ap_ids,
|
||||
activity.actor,
|
||||
^user.ap_id
|
||||
),
|
||||
where:
|
||||
fragment(
|
||||
"recipients_contain_blocked_domains(?, ?) = false",
|
||||
|
|
@ -1270,10 +1287,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|
||||
def fetch_follow_information_for_user(user) do
|
||||
with {:ok, following_data} <-
|
||||
Fetcher.fetch_and_contain_remote_object_from_id(user.following_address),
|
||||
Fetcher.fetch_and_contain_remote_object_from_id(user.following_address,
|
||||
force_http: true
|
||||
),
|
||||
{:ok, hide_follows} <- collection_private(following_data),
|
||||
{:ok, followers_data} <-
|
||||
Fetcher.fetch_and_contain_remote_object_from_id(user.follower_address),
|
||||
Fetcher.fetch_and_contain_remote_object_from_id(user.follower_address, force_http: true),
|
||||
{:ok, hide_followers} <- collection_private(followers_data) do
|
||||
{:ok,
|
||||
%{
|
||||
|
|
@ -1347,8 +1366,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
end
|
||||
end
|
||||
|
||||
def fetch_and_prepare_user_from_ap_id(ap_id) do
|
||||
with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id),
|
||||
def fetch_and_prepare_user_from_ap_id(ap_id, opts \\ []) do
|
||||
with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id, opts),
|
||||
{:ok, data} <- user_data_from_user_object(data) do
|
||||
{:ok, maybe_update_follow_information(data)}
|
||||
else
|
||||
|
|
@ -1390,13 +1409,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
end
|
||||
end
|
||||
|
||||
def make_user_from_ap_id(ap_id) do
|
||||
def make_user_from_ap_id(ap_id, opts \\ []) do
|
||||
user = User.get_cached_by_ap_id(ap_id)
|
||||
|
||||
if user && !User.ap_enabled?(user) do
|
||||
Transmogrifier.upgrade_user_from_ap_id(ap_id)
|
||||
else
|
||||
with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id) do
|
||||
with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id, opts) do
|
||||
if user do
|
||||
user
|
||||
|> User.remote_user_changeset(data)
|
||||
|
|
|
|||
|
|
@ -5,16 +5,34 @@
|
|||
defmodule Pleroma.Web.ActivityPub.MRF do
|
||||
@callback filter(Map.t()) :: {:ok | :reject, Map.t()}
|
||||
|
||||
def filter(policies, %{} = object) do
|
||||
def filter(policies, %{} = message) do
|
||||
policies
|
||||
|> Enum.reduce({:ok, object}, fn
|
||||
policy, {:ok, object} -> policy.filter(object)
|
||||
|> Enum.reduce({:ok, message}, fn
|
||||
policy, {:ok, message} -> policy.filter(message)
|
||||
_, error -> error
|
||||
end)
|
||||
end
|
||||
|
||||
def filter(%{} = object), do: get_policies() |> filter(object)
|
||||
|
||||
def pipeline_filter(%{} = message, meta) do
|
||||
object = meta[:object_data]
|
||||
ap_id = message["object"]
|
||||
|
||||
if object && ap_id do
|
||||
with {:ok, message} <- filter(Map.put(message, "object", object)) do
|
||||
meta = Keyword.put(meta, :object_data, message["object"])
|
||||
{:ok, Map.put(message, "object", ap_id), meta}
|
||||
else
|
||||
{err, message} -> {err, message, meta}
|
||||
end
|
||||
else
|
||||
{err, message} = filter(message)
|
||||
|
||||
{err, message, meta}
|
||||
end
|
||||
end
|
||||
|
||||
def get_policies do
|
||||
Pleroma.Config.get([:mrf, :policies], []) |> get_policies()
|
||||
end
|
||||
|
|
|
|||
|
|
@ -20,9 +20,17 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
|
|||
String.match?(string, pattern)
|
||||
end
|
||||
|
||||
defp check_reject(%{"object" => %{"content" => content, "summary" => summary}} = message) do
|
||||
defp object_payload(%{} = object) do
|
||||
[object["content"], object["summary"], object["name"]]
|
||||
|> Enum.filter(& &1)
|
||||
|> Enum.join("\n")
|
||||
end
|
||||
|
||||
defp check_reject(%{"object" => %{} = object} = message) do
|
||||
payload = object_payload(object)
|
||||
|
||||
if Enum.any?(Pleroma.Config.get([:mrf_keyword, :reject]), fn pattern ->
|
||||
string_matches?(content, pattern) or string_matches?(summary, pattern)
|
||||
string_matches?(payload, pattern)
|
||||
end) do
|
||||
{:reject, "[KeywordPolicy] Matches with rejected keyword"}
|
||||
else
|
||||
|
|
@ -30,12 +38,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
|
|||
end
|
||||
end
|
||||
|
||||
defp check_ftl_removal(
|
||||
%{"to" => to, "object" => %{"content" => content, "summary" => summary}} = message
|
||||
) do
|
||||
defp check_ftl_removal(%{"to" => to, "object" => %{} = object} = message) do
|
||||
payload = object_payload(object)
|
||||
|
||||
if Pleroma.Constants.as_public() in to and
|
||||
Enum.any?(Pleroma.Config.get([:mrf_keyword, :federated_timeline_removal]), fn pattern ->
|
||||
string_matches?(content, pattern) or string_matches?(summary, pattern)
|
||||
string_matches?(payload, pattern)
|
||||
end) do
|
||||
to = List.delete(to, Pleroma.Constants.as_public())
|
||||
cc = [Pleroma.Constants.as_public() | message["cc"] || []]
|
||||
|
|
@ -51,35 +59,24 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
|
|||
end
|
||||
end
|
||||
|
||||
defp check_replace(%{"object" => %{"content" => content, "summary" => summary}} = message) do
|
||||
content =
|
||||
if is_binary(content) do
|
||||
content
|
||||
else
|
||||
""
|
||||
end
|
||||
defp check_replace(%{"object" => %{} = object} = message) do
|
||||
object =
|
||||
["content", "name", "summary"]
|
||||
|> Enum.filter(fn field -> Map.has_key?(object, field) && object[field] end)
|
||||
|> Enum.reduce(object, fn field, object ->
|
||||
data =
|
||||
Enum.reduce(
|
||||
Pleroma.Config.get([:mrf_keyword, :replace]),
|
||||
object[field],
|
||||
fn {pat, repl}, acc -> String.replace(acc, pat, repl) end
|
||||
)
|
||||
|
||||
summary =
|
||||
if is_binary(summary) do
|
||||
summary
|
||||
else
|
||||
""
|
||||
end
|
||||
Map.put(object, field, data)
|
||||
end)
|
||||
|
||||
{content, summary} =
|
||||
Enum.reduce(
|
||||
Pleroma.Config.get([:mrf_keyword, :replace]),
|
||||
{content, summary},
|
||||
fn {pattern, replacement}, {content_acc, summary_acc} ->
|
||||
{String.replace(content_acc, pattern, replacement),
|
||||
String.replace(summary_acc, pattern, replacement)}
|
||||
end
|
||||
)
|
||||
message = Map.put(message, "object", object)
|
||||
|
||||
{:ok,
|
||||
message
|
||||
|> put_in(["object", "content"], content)
|
||||
|> put_in(["object", "summary"], summary)}
|
||||
{:ok, message}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
|
|
|||
|
|
@ -12,17 +12,21 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do
|
|||
|
||||
require Logger
|
||||
|
||||
@options [
|
||||
@adapter_options [
|
||||
pool: :media,
|
||||
recv_timeout: 10_000
|
||||
]
|
||||
|
||||
def perform(:prefetch, url) do
|
||||
Logger.debug("Prefetching #{inspect(url)}")
|
||||
# Fetching only proxiable resources
|
||||
if MediaProxy.enabled?() and MediaProxy.url_proxiable?(url) do
|
||||
# If preview proxy is enabled, it'll also hit media proxy (so we're caching both requests)
|
||||
prefetch_url = MediaProxy.preview_url(url)
|
||||
|
||||
url
|
||||
|> MediaProxy.url()
|
||||
|> HTTP.get([], @options)
|
||||
Logger.debug("Prefetching #{inspect(url)} as #{inspect(prefetch_url)}")
|
||||
|
||||
HTTP.get(prefetch_url, [], @adapter_options)
|
||||
end
|
||||
end
|
||||
|
||||
def perform(:preload, %{"object" => %{"attachment" => attachments}} = _message) do
|
||||
|
|
|
|||
|
|
@ -66,7 +66,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|
|||
"type" => "Create",
|
||||
"object" => child_object
|
||||
} = object
|
||||
) do
|
||||
)
|
||||
when is_map(child_object) do
|
||||
media_nsfw =
|
||||
Config.get([:mrf_simple, :media_nsfw])
|
||||
|> MRF.subdomains_regex()
|
||||
|
|
|
|||
|
|
@ -28,8 +28,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SubchainPolicy do
|
|||
}"
|
||||
)
|
||||
|
||||
subchain
|
||||
|> MRF.filter(message)
|
||||
MRF.filter(subchain, message)
|
||||
else
|
||||
_e -> {:ok, message}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,11 +12,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
|
|||
alias Pleroma.Activity
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Object.Containment
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.AcceptRejectValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.AnswerValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.AudioValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator
|
||||
|
|
@ -149,10 +151,20 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
|
|||
end
|
||||
end
|
||||
|
||||
def validate(%{"type" => "Audio"} = object, meta) do
|
||||
def validate(%{"type" => type} = object, meta) when type in ~w[Audio Video] do
|
||||
with {:ok, object} <-
|
||||
object
|
||||
|> AudioValidator.cast_and_validate()
|
||||
|> AudioVideoValidator.cast_and_validate()
|
||||
|> Ecto.Changeset.apply_action(:insert) do
|
||||
object = stringify_keys(object)
|
||||
{:ok, object, meta}
|
||||
end
|
||||
end
|
||||
|
||||
def validate(%{"type" => "Article"} = object, meta) do
|
||||
with {:ok, object} <-
|
||||
object
|
||||
|> ArticleNoteValidator.cast_and_validate()
|
||||
|> Ecto.Changeset.apply_action(:insert) do
|
||||
object = stringify_keys(object)
|
||||
{:ok, object, meta}
|
||||
|
|
@ -198,7 +210,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
|
|||
%{"type" => "Create", "object" => %{"type" => objtype} = object} = create_activity,
|
||||
meta
|
||||
)
|
||||
when objtype in ~w[Question Answer Audio Event] do
|
||||
when objtype in ~w[Question Answer Audio Video Event Article] do
|
||||
with {:ok, object_data} <- cast_and_apply(object),
|
||||
meta = Keyword.put(meta, :object_data, object_data |> stringify_keys),
|
||||
{:ok, create_activity} <-
|
||||
|
|
@ -232,14 +244,18 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
|
|||
AnswerValidator.cast_and_apply(object)
|
||||
end
|
||||
|
||||
def cast_and_apply(%{"type" => "Audio"} = object) do
|
||||
AudioValidator.cast_and_apply(object)
|
||||
def cast_and_apply(%{"type" => type} = object) when type in ~w[Audio Video] do
|
||||
AudioVideoValidator.cast_and_apply(object)
|
||||
end
|
||||
|
||||
def cast_and_apply(%{"type" => "Event"} = object) do
|
||||
EventValidator.cast_and_apply(object)
|
||||
end
|
||||
|
||||
def cast_and_apply(%{"type" => "Article"} = object) do
|
||||
ArticleNoteValidator.cast_and_apply(object)
|
||||
end
|
||||
|
||||
def cast_and_apply(o), do: {:error, {:validator_not_set, o}}
|
||||
|
||||
# is_struct/1 isn't present in Elixir 1.8.x
|
||||
|
|
@ -262,7 +278,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
|
|||
def stringify_keys(object), do: object
|
||||
|
||||
def fetch_actor(object) do
|
||||
with {:ok, actor} <- ObjectValidators.ObjectID.cast(object["actor"]) do
|
||||
with actor <- Containment.get_actor(object),
|
||||
{:ok, actor} <- ObjectValidators.ObjectID.cast(actor) do
|
||||
User.get_or_fetch_by_ap_id(actor)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioValidator do
|
||||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidator do
|
||||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||
|
|
@ -25,14 +25,19 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioValidator do
|
|||
# TODO: Write type
|
||||
field(:tag, {:array, :map}, default: [])
|
||||
field(:type, :string)
|
||||
|
||||
field(:name, :string)
|
||||
field(:summary, :string)
|
||||
field(:content, :string)
|
||||
|
||||
field(:context, :string)
|
||||
# short identifier for PleromaFE to group statuses by context
|
||||
field(:context_id, :integer)
|
||||
|
||||
# TODO: Remove actor on objects
|
||||
field(:actor, ObjectValidators.ObjectID)
|
||||
|
||||
field(:attributedTo, ObjectValidators.ObjectID)
|
||||
field(:summary, :string)
|
||||
field(:published, ObjectValidators.DateTime)
|
||||
field(:emoji, ObjectValidators.Emoji, default: %{})
|
||||
field(:sensitive, :boolean, default: false)
|
||||
|
|
@ -40,13 +45,11 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioValidator do
|
|||
field(:replies_count, :integer, default: 0)
|
||||
field(:like_count, :integer, default: 0)
|
||||
field(:announcement_count, :integer, default: 0)
|
||||
field(:inReplyTo, :string)
|
||||
field(:inReplyTo, ObjectValidators.ObjectID)
|
||||
field(:url, ObjectValidators.Uri)
|
||||
# short identifier for PleromaFE to group statuses by context
|
||||
field(:context_id, :integer)
|
||||
|
||||
field(:likes, {:array, :string}, default: [])
|
||||
field(:announcements, {:array, :string}, default: [])
|
||||
field(:likes, {:array, ObjectValidators.ObjectID}, default: [])
|
||||
field(:announcements, {:array, ObjectValidators.ObjectID}, default: [])
|
||||
end
|
||||
|
||||
def cast_and_apply(data) do
|
||||
|
|
@ -62,19 +65,14 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioValidator do
|
|||
end
|
||||
|
||||
def cast_data(data) do
|
||||
data = fix(data)
|
||||
|
||||
%__MODULE__{}
|
||||
|> changeset(data)
|
||||
end
|
||||
|
||||
defp fix_url(%{"url" => url} = data) when is_list(url) do
|
||||
attachment =
|
||||
Enum.find(url, fn x -> is_map(x) and String.starts_with?(x["mimeType"], "audio/") end)
|
||||
|
||||
link_element = Enum.find(url, fn x -> is_map(x) and x["mimeType"] == "text/html" end)
|
||||
|
||||
data
|
||||
|> Map.put("attachment", [attachment])
|
||||
|> Map.put("url", link_element["href"])
|
||||
defp fix_url(%{"url" => url} = data) when is_map(url) do
|
||||
Map.put(data, "url", url["href"])
|
||||
end
|
||||
|
||||
defp fix_url(data), do: data
|
||||
|
|
@ -83,8 +81,9 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioValidator do
|
|||
data
|
||||
|> CommonFixes.fix_defaults()
|
||||
|> CommonFixes.fix_attribution()
|
||||
|> Transmogrifier.fix_emoji()
|
||||
|> CommonFixes.fix_actor()
|
||||
|> fix_url()
|
||||
|> Transmogrifier.fix_emoji()
|
||||
end
|
||||
|
||||
def changeset(struct, data) do
|
||||
|
|
@ -97,8 +96,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioValidator do
|
|||
|
||||
def validate_data(data_cng) do
|
||||
data_cng
|
||||
|> validate_inclusion(:type, ["Audio"])
|
||||
|> validate_required([:id, :actor, :attributedTo, :type, :context, :attachment])
|
||||
|> validate_inclusion(:type, ["Article", "Note"])
|
||||
|> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id])
|
||||
|> CommonValidations.validate_any_presence([:cc, :to])
|
||||
|> CommonValidations.validate_fields_match([:actor, :attributedTo])
|
||||
|> CommonValidations.validate_actor_presence()
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do
|
||||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.UrlObjectValidator
|
||||
|
||||
import Ecto.Changeset
|
||||
|
|
@ -15,7 +16,11 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do
|
|||
field(:mediaType, :string, default: "application/octet-stream")
|
||||
field(:name, :string)
|
||||
|
||||
embeds_many(:url, UrlObjectValidator)
|
||||
embeds_many :url, UrlObjectValidator, primary_key: false do
|
||||
field(:type, :string)
|
||||
field(:href, ObjectValidators.Uri)
|
||||
field(:mediaType, :string, default: "application/octet-stream")
|
||||
end
|
||||
end
|
||||
|
||||
def cast_and_validate(data) do
|
||||
|
|
@ -37,7 +42,18 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do
|
|||
|
||||
struct
|
||||
|> cast(data, [:type, :mediaType, :name])
|
||||
|> cast_embed(:url, required: true)
|
||||
|> cast_embed(:url, with: &url_changeset/2)
|
||||
|> validate_inclusion(:type, ~w[Link Document Audio Image Video])
|
||||
|> validate_required([:type, :mediaType, :url])
|
||||
end
|
||||
|
||||
def url_changeset(struct, data) do
|
||||
data = fix_media_type(data)
|
||||
|
||||
struct
|
||||
|> cast(data, [:type, :href, :mediaType])
|
||||
|> validate_inclusion(:type, ["Link"])
|
||||
|> validate_required([:type, :href, :mediaType])
|
||||
end
|
||||
|
||||
def fix_media_type(data) do
|
||||
|
|
@ -75,6 +91,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do
|
|||
|
||||
def validate_data(cng) do
|
||||
cng
|
||||
|> validate_inclusion(:type, ~w[Document Audio Image Video])
|
||||
|> validate_required([:mediaType, :url, :type])
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,134 @@
|
|||
# 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.AudioVideoValidator do
|
||||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.EarmarkRenderer
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key false
|
||||
@derive Jason.Encoder
|
||||
|
||||
embedded_schema do
|
||||
field(:id, ObjectValidators.ObjectID, primary_key: true)
|
||||
field(:to, ObjectValidators.Recipients, default: [])
|
||||
field(:cc, ObjectValidators.Recipients, default: [])
|
||||
field(:bto, ObjectValidators.Recipients, default: [])
|
||||
field(:bcc, ObjectValidators.Recipients, default: [])
|
||||
# TODO: Write type
|
||||
field(:tag, {:array, :map}, default: [])
|
||||
field(:type, :string)
|
||||
|
||||
field(:name, :string)
|
||||
field(:summary, :string)
|
||||
field(:content, :string)
|
||||
|
||||
field(:context, :string)
|
||||
# short identifier for PleromaFE to group statuses by context
|
||||
field(:context_id, :integer)
|
||||
|
||||
# TODO: Remove actor on objects
|
||||
field(:actor, ObjectValidators.ObjectID)
|
||||
|
||||
field(:attributedTo, ObjectValidators.ObjectID)
|
||||
field(:published, ObjectValidators.DateTime)
|
||||
field(:emoji, ObjectValidators.Emoji, default: %{})
|
||||
field(:sensitive, :boolean, default: false)
|
||||
embeds_many(:attachment, AttachmentValidator)
|
||||
field(:replies_count, :integer, default: 0)
|
||||
field(:like_count, :integer, default: 0)
|
||||
field(:announcement_count, :integer, default: 0)
|
||||
field(:inReplyTo, ObjectValidators.ObjectID)
|
||||
field(:url, ObjectValidators.Uri)
|
||||
|
||||
field(:likes, {:array, ObjectValidators.ObjectID}, default: [])
|
||||
field(:announcements, {:array, ObjectValidators.ObjectID}, default: [])
|
||||
end
|
||||
|
||||
def cast_and_apply(data) do
|
||||
data
|
||||
|> cast_data
|
||||
|> apply_action(:insert)
|
||||
end
|
||||
|
||||
def cast_and_validate(data) do
|
||||
data
|
||||
|> cast_data()
|
||||
|> validate_data()
|
||||
end
|
||||
|
||||
def cast_data(data) do
|
||||
%__MODULE__{}
|
||||
|> changeset(data)
|
||||
end
|
||||
|
||||
defp fix_url(%{"url" => url} = data) when is_list(url) do
|
||||
attachment =
|
||||
Enum.find(url, fn x ->
|
||||
mime_type = x["mimeType"] || x["mediaType"] || ""
|
||||
|
||||
is_map(x) and String.starts_with?(mime_type, ["video/", "audio/"])
|
||||
end)
|
||||
|
||||
link_element =
|
||||
Enum.find(url, fn x ->
|
||||
mime_type = x["mimeType"] || x["mediaType"] || ""
|
||||
|
||||
is_map(x) and mime_type == "text/html"
|
||||
end)
|
||||
|
||||
data
|
||||
|> Map.put("attachment", [attachment])
|
||||
|> Map.put("url", link_element["href"])
|
||||
end
|
||||
|
||||
defp fix_url(data), do: data
|
||||
|
||||
defp fix_content(%{"mediaType" => "text/markdown", "content" => content} = data)
|
||||
when is_binary(content) do
|
||||
content =
|
||||
content
|
||||
|> Earmark.as_html!(%Earmark.Options{renderer: EarmarkRenderer})
|
||||
|> Pleroma.HTML.filter_tags()
|
||||
|
||||
Map.put(data, "content", content)
|
||||
end
|
||||
|
||||
defp fix_content(data), do: data
|
||||
|
||||
defp fix(data) do
|
||||
data
|
||||
|> CommonFixes.fix_defaults()
|
||||
|> CommonFixes.fix_attribution()
|
||||
|> CommonFixes.fix_actor()
|
||||
|> Transmogrifier.fix_emoji()
|
||||
|> fix_url()
|
||||
|> fix_content()
|
||||
end
|
||||
|
||||
def changeset(struct, data) do
|
||||
data = fix(data)
|
||||
|
||||
struct
|
||||
|> cast(data, __schema__(:fields) -- [:attachment])
|
||||
|> cast_embed(:attachment)
|
||||
end
|
||||
|
||||
def validate_data(data_cng) do
|
||||
data_cng
|
||||
|> validate_inclusion(:type, ["Audio", "Video"])
|
||||
|> validate_required([:id, :actor, :attributedTo, :type, :context, :attachment])
|
||||
|> CommonValidations.validate_any_presence([:cc, :to])
|
||||
|> CommonValidations.validate_fields_match([:actor, :attributedTo])
|
||||
|> CommonValidations.validate_actor_presence()
|
||||
|> CommonValidations.validate_host_match()
|
||||
end
|
||||
end
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
|
||||
alias Pleroma.Object.Containment
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
|
||||
# based on Pleroma.Web.ActivityPub.Utils.lazy_put_objects_defaults
|
||||
|
|
@ -19,4 +20,12 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
|
|||
data
|
||||
|> Map.put_new("actor", data["attributedTo"])
|
||||
end
|
||||
|
||||
def fix_actor(data) do
|
||||
actor = Containment.get_actor(data)
|
||||
|
||||
data
|
||||
|> Map.put("actor", actor)
|
||||
|> Map.put("attributedTo", actor)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -10,9 +10,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator do
|
|||
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
|
||||
|
||||
import Ecto.Changeset
|
||||
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
|
||||
|
||||
@primary_key false
|
||||
|
||||
|
|
@ -75,14 +76,15 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator do
|
|||
data
|
||||
|> fix_context(meta)
|
||||
|> fix_addressing(meta)
|
||||
|> CommonFixes.fix_actor()
|
||||
end
|
||||
|
||||
def validate_data(cng, meta \\ []) do
|
||||
cng
|
||||
|> validate_required([:actor, :type, :object])
|
||||
|> validate_inclusion(:type, ["Create"])
|
||||
|> validate_actor_presence()
|
||||
|> validate_any_presence([:to, :cc])
|
||||
|> CommonValidations.validate_actor_presence()
|
||||
|> CommonValidations.validate_any_presence([:to, :cc])
|
||||
|> validate_actors_match(meta)
|
||||
|> validate_context_match(meta)
|
||||
|> validate_object_nonexistence()
|
||||
|
|
|
|||
|
|
@ -1,73 +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.Web.ActivityPub.ObjectValidators.NoteValidator do
|
||||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key false
|
||||
|
||||
embedded_schema do
|
||||
field(:id, ObjectValidators.ObjectID, primary_key: true)
|
||||
field(:to, ObjectValidators.Recipients, default: [])
|
||||
field(:cc, ObjectValidators.Recipients, default: [])
|
||||
field(:bto, ObjectValidators.Recipients, default: [])
|
||||
field(:bcc, ObjectValidators.Recipients, default: [])
|
||||
# TODO: Write type
|
||||
field(:tag, {:array, :map}, default: [])
|
||||
field(:type, :string)
|
||||
|
||||
field(:name, :string)
|
||||
field(:summary, :string)
|
||||
field(:content, :string)
|
||||
|
||||
field(:context, :string)
|
||||
# short identifier for PleromaFE to group statuses by context
|
||||
field(:context_id, :integer)
|
||||
|
||||
field(:actor, ObjectValidators.ObjectID)
|
||||
field(:attributedTo, ObjectValidators.ObjectID)
|
||||
field(:published, ObjectValidators.DateTime)
|
||||
field(:emoji, ObjectValidators.Emoji, default: %{})
|
||||
field(:sensitive, :boolean, default: false)
|
||||
# TODO: Write type
|
||||
field(:attachment, {:array, :map}, default: [])
|
||||
field(:replies_count, :integer, default: 0)
|
||||
field(:like_count, :integer, default: 0)
|
||||
field(:announcement_count, :integer, default: 0)
|
||||
field(:inReplyTo, ObjectValidators.ObjectID)
|
||||
field(:url, ObjectValidators.Uri)
|
||||
|
||||
field(:likes, {:array, :string}, default: [])
|
||||
field(:announcements, {:array, :string}, default: [])
|
||||
end
|
||||
|
||||
def cast_and_validate(data) do
|
||||
data
|
||||
|> cast_data()
|
||||
|> validate_data()
|
||||
end
|
||||
|
||||
defp fix(data) do
|
||||
data
|
||||
|> Transmogrifier.fix_emoji()
|
||||
end
|
||||
|
||||
def cast_data(data) do
|
||||
data = fix(data)
|
||||
|
||||
%__MODULE__{}
|
||||
|> cast(data, __schema__(:fields))
|
||||
end
|
||||
|
||||
def validate_data(data_cng) do
|
||||
data_cng
|
||||
|> validate_inclusion(:type, ["Note"])
|
||||
|> validate_required([:id, :actor, :to, :cc, :type, :content, :context])
|
||||
end
|
||||
end
|
||||
|
|
@ -47,8 +47,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do
|
|||
# short identifier for PleromaFE to group statuses by context
|
||||
field(:context_id, :integer)
|
||||
|
||||
field(:likes, {:array, :string}, default: [])
|
||||
field(:announcements, {:array, :string}, default: [])
|
||||
field(:likes, {:array, ObjectValidators.ObjectID}, default: [])
|
||||
field(:announcements, {:array, ObjectValidators.ObjectID}, default: [])
|
||||
|
||||
field(:closed, ObjectValidators.DateTime)
|
||||
field(:voters, {:array, ObjectValidators.ObjectID}, default: [])
|
||||
|
|
|
|||
|
|
@ -1,24 +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.Web.ActivityPub.ObjectValidators.UrlObjectValidator do
|
||||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||
|
||||
import Ecto.Changeset
|
||||
@primary_key false
|
||||
|
||||
embedded_schema do
|
||||
field(:type, :string)
|
||||
field(:href, ObjectValidators.Uri)
|
||||
field(:mediaType, :string, default: "application/octet-stream")
|
||||
end
|
||||
|
||||
def changeset(struct, data) do
|
||||
struct
|
||||
|> cast(data, __schema__(:fields))
|
||||
|> validate_required([:type, :href, :mediaType])
|
||||
end
|
||||
end
|
||||
|
|
@ -26,13 +26,17 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do
|
|||
|
||||
{:error, e} ->
|
||||
{:error, e}
|
||||
|
||||
{:reject, e} ->
|
||||
{:reject, e}
|
||||
end
|
||||
end
|
||||
|
||||
def do_common_pipeline(object, meta) do
|
||||
with {_, {:ok, validated_object, meta}} <-
|
||||
{:validate_object, ObjectValidator.validate(object, meta)},
|
||||
{_, {:ok, mrfd_object}} <- {:mrf_object, MRF.filter(validated_object)},
|
||||
{_, {:ok, mrfd_object, meta}} <-
|
||||
{:mrf_object, MRF.pipeline_filter(validated_object, meta)},
|
||||
{_, {:ok, activity, meta}} <-
|
||||
{:persist_object, ActivityPub.persist(mrfd_object, meta)},
|
||||
{_, {:ok, activity, meta}} <-
|
||||
|
|
@ -40,7 +44,7 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do
|
|||
{_, {:ok, _}} <- {:federation, maybe_federate(activity, meta)} do
|
||||
{:ok, activity, meta}
|
||||
else
|
||||
{:mrf_object, {:reject, _}} -> {:ok, nil, meta}
|
||||
{:mrf_object, {:reject, message, _}} -> {:reject, message}
|
||||
e -> {:error, e}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|
|||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.Relay
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
alias Pleroma.Web.FedSockets
|
||||
|
||||
require Pleroma.Constants
|
||||
|
||||
|
|
@ -50,15 +51,35 @@ 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}")
|
||||
|
||||
uri = URI.parse(inbox)
|
||||
case FedSockets.get_or_create_fed_socket(inbox) do
|
||||
{:ok, fedsocket} ->
|
||||
Logger.debug("publishing via fedsockets - #{inspect(inbox)}")
|
||||
FedSockets.publish(fedsocket, json)
|
||||
|
||||
_ ->
|
||||
Logger.debug("publishing via http - #{inspect(inbox)}")
|
||||
http_publish(inbox, actor, json, params)
|
||||
end
|
||||
end
|
||||
|
||||
def publish_one(%{actor_id: actor_id} = params) do
|
||||
actor = User.get_cached_by_id(actor_id)
|
||||
|
||||
params
|
||||
|> Map.delete(:actor_id)
|
||||
|> Map.put(:actor, actor)
|
||||
|> publish_one()
|
||||
end
|
||||
|
||||
defp http_publish(inbox, actor, json, params) do
|
||||
uri = %{path: path} = URI.parse(inbox)
|
||||
digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
|
||||
|
||||
date = Pleroma.Signature.signed_date()
|
||||
|
||||
signature =
|
||||
Pleroma.Signature.sign(actor, %{
|
||||
"(request-target)": "post #{uri.path}",
|
||||
"(request-target)": "post #{path}",
|
||||
host: signature_host(uri),
|
||||
"content-length": byte_size(json),
|
||||
digest: digest,
|
||||
|
|
@ -89,15 +110,6 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|
|||
end
|
||||
end
|
||||
|
||||
def publish_one(%{actor_id: actor_id} = params) do
|
||||
actor = User.get_cached_by_id(actor_id)
|
||||
|
||||
params
|
||||
|> Map.delete(:actor_id)
|
||||
|> Map.put(:actor, actor)
|
||||
|> publish_one()
|
||||
end
|
||||
|
||||
defp signature_host(%URI{port: port, scheme: scheme, host: host}) do
|
||||
if port == URI.default_port(scheme) do
|
||||
host
|
||||
|
|
|
|||
|
|
@ -30,12 +30,16 @@ defmodule Pleroma.Web.ActivityPub.Relay do
|
|||
end
|
||||
end
|
||||
|
||||
@spec unfollow(String.t()) :: {:ok, Activity.t()} | {:error, any()}
|
||||
def unfollow(target_instance) do
|
||||
@spec unfollow(String.t(), map()) :: {:ok, Activity.t()} | {:error, any()}
|
||||
def unfollow(target_instance, opts \\ %{}) do
|
||||
with %User{} = local_user <- get_actor(),
|
||||
{:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_instance),
|
||||
{:ok, target_user} <- fetch_target_user(target_instance, opts),
|
||||
{:ok, activity} <- ActivityPub.unfollow(local_user, target_user) do
|
||||
User.unfollow(local_user, target_user)
|
||||
case target_user.id do
|
||||
nil -> User.update_following_count(local_user)
|
||||
_ -> User.unfollow(local_user, target_user)
|
||||
end
|
||||
|
||||
Logger.info("relay: unfollowed instance: #{target_instance}: id=#{activity.data["id"]}")
|
||||
{:ok, activity}
|
||||
else
|
||||
|
|
@ -43,6 +47,14 @@ defmodule Pleroma.Web.ActivityPub.Relay do
|
|||
end
|
||||
end
|
||||
|
||||
defp fetch_target_user(ap_id, opts) do
|
||||
case {opts[:force], User.get_or_fetch_by_ap_id(ap_id)} do
|
||||
{_, {:ok, %User{} = user}} -> {:ok, user}
|
||||
{true, _} -> {:ok, %User{ap_id: ap_id}}
|
||||
{_, error} -> error
|
||||
end
|
||||
end
|
||||
|
||||
@spec publish(any()) :: {:ok, Activity.t()} | {:error, any()}
|
||||
def publish(%Activity{data: %{"type" => "Create"}} = activity) do
|
||||
with %User{} = user <- get_actor(),
|
||||
|
|
|
|||
|
|
@ -336,7 +336,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
end
|
||||
|
||||
def handle_object_creation(%{"type" => objtype} = object, meta)
|
||||
when objtype in ~w[Audio Question Event] do
|
||||
when objtype in ~w[Audio Video Question Event Article] do
|
||||
with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do
|
||||
{:ok, object, meta}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
A module to handle coding from internal to wire ActivityPub and back.
|
||||
"""
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.EarmarkRenderer
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||
alias Pleroma.Maps
|
||||
alias Pleroma.Object
|
||||
|
|
@ -45,7 +44,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|> fix_addressing
|
||||
|> fix_summary
|
||||
|> fix_type(options)
|
||||
|> fix_content
|
||||
end
|
||||
|
||||
def fix_summary(%{"summary" => nil} = object) do
|
||||
|
|
@ -274,24 +272,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
Map.put(object, "url", url["href"])
|
||||
end
|
||||
|
||||
def fix_url(%{"type" => "Video", "url" => url} = object) when is_list(url) do
|
||||
attachment =
|
||||
Enum.find(url, fn x ->
|
||||
media_type = x["mediaType"] || x["mimeType"] || ""
|
||||
|
||||
is_map(x) and String.starts_with?(media_type, "video/")
|
||||
end)
|
||||
|
||||
link_element =
|
||||
Enum.find(url, fn x -> is_map(x) and (x["mediaType"] || x["mimeType"]) == "text/html" end)
|
||||
|
||||
object
|
||||
|> Map.put("attachment", [attachment])
|
||||
|> Map.put("url", link_element["href"])
|
||||
end
|
||||
|
||||
def fix_url(%{"type" => object_type, "url" => url} = object)
|
||||
when object_type != "Video" and is_list(url) do
|
||||
def fix_url(%{"url" => url} = object) when is_list(url) do
|
||||
first_element = Enum.at(url, 0)
|
||||
|
||||
url_string =
|
||||
|
|
@ -309,7 +290,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
|
||||
emoji =
|
||||
tags
|
||||
|> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
|
||||
|> Enum.filter(fn data -> is_map(data) and data["type"] == "Emoji" and data["icon"] end)
|
||||
|> Enum.reduce(%{}, fn data, mapping ->
|
||||
name = String.trim(data["name"], ":")
|
||||
|
||||
|
|
@ -371,18 +352,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|
||||
def fix_type(object, _), do: object
|
||||
|
||||
defp fix_content(%{"mediaType" => "text/markdown", "content" => content} = object)
|
||||
when is_binary(content) do
|
||||
html_content =
|
||||
content
|
||||
|> Earmark.as_html!(%Earmark.Options{renderer: EarmarkRenderer})
|
||||
|> Pleroma.HTML.filter_tags()
|
||||
|
||||
Map.merge(object, %{"content" => html_content, "mediaType" => "text/html"})
|
||||
end
|
||||
|
||||
defp fix_content(object), do: object
|
||||
|
||||
# Reduce the object list to find the reported user.
|
||||
defp get_reported(objects) do
|
||||
Enum.reduce_while(objects, nil, fn ap_id, _ ->
|
||||
|
|
@ -455,7 +424,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
%{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
|
||||
options
|
||||
)
|
||||
when objtype in ~w{Article Note Video Page} do
|
||||
when objtype in ~w{Note Page} do
|
||||
actor = Containment.get_actor(data)
|
||||
|
||||
with nil <- Activity.get_create_by_object_ap_id(object["id"]),
|
||||
|
|
@ -546,13 +515,19 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Create", "object" => %{"type" => objtype}} = data,
|
||||
%{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data,
|
||||
_options
|
||||
)
|
||||
when objtype in ~w{Question Answer ChatMessage Audio Event} do
|
||||
when objtype in ~w{Question Answer ChatMessage Audio Video Event Article} do
|
||||
data = Map.put(data, "object", strip_internal_fields(data["object"]))
|
||||
|
||||
with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
|
||||
nil <- Activity.get_create_by_object_ap_id(obj_id),
|
||||
{:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
|
||||
{:ok, activity}
|
||||
else
|
||||
%Activity{} = activity -> {:ok, activity}
|
||||
e -> e
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -1029,7 +1004,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|
||||
def upgrade_user_from_ap_id(ap_id) do
|
||||
with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
|
||||
{:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
|
||||
{:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id, force_http: true),
|
||||
{:ok, user} <- update_user(user, data) do
|
||||
TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
|
||||
{:ok, user}
|
||||
|
|
|
|||
|
|
@ -23,8 +23,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
alias Pleroma.Web.Endpoint
|
||||
alias Pleroma.Web.Router
|
||||
|
||||
require Logger
|
||||
|
||||
@users_page_size 50
|
||||
|
||||
plug(
|
||||
|
|
@ -68,6 +66,12 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
when action in [:list_user_statuses, :list_instance_statuses]
|
||||
)
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["read:chats"], admin: true}
|
||||
when action in [:list_user_chats]
|
||||
)
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["read"], admin: true}
|
||||
|
|
@ -256,6 +260,20 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
end
|
||||
end
|
||||
|
||||
def list_user_chats(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname} = _params) do
|
||||
with %User{id: user_id} <- User.get_cached_by_nickname_or_id(nickname, for: admin) do
|
||||
chats =
|
||||
Pleroma.Chat.for_user_query(user_id)
|
||||
|> Pleroma.Repo.all()
|
||||
|
||||
conn
|
||||
|> put_view(AdminAPI.ChatView)
|
||||
|> render("index.json", chats: chats)
|
||||
else
|
||||
_ -> {:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
|
||||
user = User.get_cached_by_nickname(nickname)
|
||||
|
||||
|
|
|
|||
85
lib/pleroma/web/admin_api/controllers/chat_controller.ex
Normal file
85
lib/pleroma/web/admin_api/controllers/chat_controller.ex
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.AdminAPI.ChatController do
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Chat
|
||||
alias Pleroma.Chat.MessageReference
|
||||
alias Pleroma.ModerationLog
|
||||
alias Pleroma.Pagination
|
||||
alias Pleroma.Plugs.OAuthScopesPlug
|
||||
alias Pleroma.Web.AdminAPI
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
|
||||
|
||||
require Logger
|
||||
|
||||
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["read:chats"], admin: true} when action in [:show, :messages]
|
||||
)
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["write:chats"], admin: true} when action in [:delete_message]
|
||||
)
|
||||
|
||||
action_fallback(Pleroma.Web.AdminAPI.FallbackController)
|
||||
|
||||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.ChatOperation
|
||||
|
||||
def delete_message(%{assigns: %{user: user}} = conn, %{
|
||||
message_id: message_id,
|
||||
id: chat_id
|
||||
}) do
|
||||
with %MessageReference{object: %{data: %{"id" => object_ap_id}}} = cm_ref <-
|
||||
MessageReference.get_by_id(message_id),
|
||||
^chat_id <- to_string(cm_ref.chat_id),
|
||||
%Activity{id: activity_id} <- Activity.get_create_by_object_ap_id(object_ap_id),
|
||||
{:ok, _} <- CommonAPI.delete(activity_id, user) do
|
||||
ModerationLog.insert_log(%{
|
||||
action: "chat_message_delete",
|
||||
actor: user,
|
||||
subject_id: message_id
|
||||
})
|
||||
|
||||
conn
|
||||
|> put_view(MessageReferenceView)
|
||||
|> render("show.json", chat_message_reference: cm_ref)
|
||||
else
|
||||
_e ->
|
||||
{:error, :could_not_delete}
|
||||
end
|
||||
end
|
||||
|
||||
def messages(conn, %{id: id} = params) do
|
||||
with %Chat{} = chat <- Chat.get_by_id(id) do
|
||||
cm_refs =
|
||||
chat
|
||||
|> MessageReference.for_chat_query()
|
||||
|> Pagination.fetch_paginated(params)
|
||||
|
||||
conn
|
||||
|> put_view(MessageReferenceView)
|
||||
|> render("index.json", chat_message_references: cm_refs)
|
||||
else
|
||||
_ ->
|
||||
conn
|
||||
|> put_status(:not_found)
|
||||
|> json(%{error: "not found"})
|
||||
end
|
||||
end
|
||||
|
||||
def show(conn, %{id: id}) do
|
||||
with %Chat{} = chat <- Chat.get_by_id(id) do
|
||||
conn
|
||||
|> put_view(AdminAPI.ChatView)
|
||||
|> render("show.json", chat: chat)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.AdminAPI.InstanceDocumentController do
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
alias Pleroma.Plugs.InstanceStatic
|
||||
alias Pleroma.Plugs.OAuthScopesPlug
|
||||
alias Pleroma.Web.InstanceDocument
|
||||
|
||||
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||
|
||||
action_fallback(Pleroma.Web.AdminAPI.FallbackController)
|
||||
|
||||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.InstanceDocumentOperation
|
||||
|
||||
plug(OAuthScopesPlug, %{scopes: ["read"], admin: true} when action == :show)
|
||||
plug(OAuthScopesPlug, %{scopes: ["write"], admin: true} when action in [:update, :delete])
|
||||
|
||||
def show(conn, %{name: document_name}) do
|
||||
with {:ok, url} <- InstanceDocument.get(document_name),
|
||||
{:ok, content} <- File.read(InstanceStatic.file_path(url)) do
|
||||
conn
|
||||
|> put_resp_content_type("text/html")
|
||||
|> send_resp(200, content)
|
||||
end
|
||||
end
|
||||
|
||||
def update(%{body_params: %{file: file}} = conn, %{name: document_name}) do
|
||||
with {:ok, url} <- InstanceDocument.put(document_name, file.path) do
|
||||
json(conn, %{"url" => url})
|
||||
end
|
||||
end
|
||||
|
||||
def delete(conn, %{name: document_name}) do
|
||||
with :ok <- InstanceDocument.delete(document_name) do
|
||||
json(conn, %{})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -33,11 +33,7 @@ defmodule Pleroma.Web.AdminAPI.RelayController do
|
|||
|
||||
def follow(%{assigns: %{user: admin}, body_params: %{relay_url: target}} = conn, _) do
|
||||
with {:ok, _message} <- Relay.follow(target) do
|
||||
ModerationLog.insert_log(%{
|
||||
action: "relay_follow",
|
||||
actor: admin,
|
||||
target: target
|
||||
})
|
||||
ModerationLog.insert_log(%{action: "relay_follow", actor: admin, target: target})
|
||||
|
||||
json(conn, %{actor: target, followed_back: target in Relay.following()})
|
||||
else
|
||||
|
|
@ -48,13 +44,9 @@ defmodule Pleroma.Web.AdminAPI.RelayController do
|
|||
end
|
||||
end
|
||||
|
||||
def unfollow(%{assigns: %{user: admin}, body_params: %{relay_url: target}} = conn, _) do
|
||||
with {:ok, _message} <- Relay.unfollow(target) do
|
||||
ModerationLog.insert_log(%{
|
||||
action: "relay_unfollow",
|
||||
actor: admin,
|
||||
target: target
|
||||
})
|
||||
def unfollow(%{assigns: %{user: admin}, body_params: %{relay_url: target} = params} = conn, _) do
|
||||
with {:ok, _message} <- Relay.unfollow(target, %{force: params[:force]}) do
|
||||
ModerationLog.insert_log(%{action: "relay_unfollow", actor: admin, target: target})
|
||||
|
||||
json(conn, target)
|
||||
else
|
||||
|
|
|
|||
30
lib/pleroma/web/admin_api/views/chat_view.ex
Normal file
30
lib/pleroma/web/admin_api/views/chat_view.ex
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.AdminAPI.ChatView do
|
||||
use Pleroma.Web, :view
|
||||
|
||||
alias Pleroma.Chat
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.MastodonAPI
|
||||
alias Pleroma.Web.PleromaAPI
|
||||
|
||||
def render("index.json", %{chats: chats} = opts) do
|
||||
render_many(chats, __MODULE__, "show.json", Map.delete(opts, :chats))
|
||||
end
|
||||
|
||||
def render("show.json", %{chat: %Chat{user_id: user_id}} = opts) do
|
||||
user = User.get_by_id(user_id)
|
||||
sender = MastodonAPI.AccountView.render("show.json", user: user, skip_visibility_check: true)
|
||||
|
||||
serialized_chat = PleromaAPI.ChatView.render("show.json", opts)
|
||||
|
||||
serialized_chat
|
||||
|> Map.put(:sender, sender)
|
||||
|> Map.put(:receiver, serialized_chat[:account])
|
||||
|> Map.delete(:account)
|
||||
end
|
||||
|
||||
def render(view, opts), do: PleromaAPI.ChatView.render(view, opts)
|
||||
end
|
||||
|
|
@ -8,6 +8,7 @@ defmodule Pleroma.Web.AdminAPI.StatusView do
|
|||
require Pleroma.Constants
|
||||
|
||||
alias Pleroma.Web.AdminAPI
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.MastodonAPI
|
||||
|
||||
defdelegate merge_account_views(user), to: AdminAPI.AccountView
|
||||
|
|
@ -17,7 +18,7 @@ defmodule Pleroma.Web.AdminAPI.StatusView do
|
|||
end
|
||||
|
||||
def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
|
||||
user = MastodonAPI.StatusView.get_user(activity.data["actor"])
|
||||
user = CommonAPI.get_user(activity.data["actor"])
|
||||
|
||||
MastodonAPI.StatusView.render("show.json", opts)
|
||||
|> Map.merge(%{account: merge_account_views(user)})
|
||||
|
|
|
|||
|
|
@ -13,10 +13,15 @@ defmodule Pleroma.Web.ApiSpec do
|
|||
@impl OpenApi
|
||||
def spec do
|
||||
%OpenApi{
|
||||
servers: [
|
||||
# Populate the Server info from a phoenix endpoint
|
||||
OpenApiSpex.Server.from_endpoint(Endpoint)
|
||||
],
|
||||
servers:
|
||||
if Phoenix.Endpoint.server?(:pleroma, Endpoint) do
|
||||
[
|
||||
# Populate the Server info from a phoenix endpoint
|
||||
OpenApiSpex.Server.from_endpoint(Endpoint)
|
||||
]
|
||||
else
|
||||
[]
|
||||
end,
|
||||
info: %OpenApiSpex.Info{
|
||||
title: "Pleroma",
|
||||
description: Application.spec(:pleroma, :description) |> to_string(),
|
||||
|
|
|
|||
|
|
@ -115,6 +115,10 @@ defmodule Pleroma.Web.ApiSpec.CastAndValidate do
|
|||
%{reason: :unexpected_field, name: name, path: [name]}, params ->
|
||||
Map.delete(params, name)
|
||||
|
||||
# Filter out empty params
|
||||
%{reason: :invalid_type, path: [name_atom], value: ""}, params ->
|
||||
Map.delete(params, to_string(name_atom))
|
||||
|
||||
%{reason: :invalid_enum, name: nil, path: path, value: value}, params ->
|
||||
path = path |> Enum.reverse() |> tl() |> Enum.reverse() |> list_items_to_string()
|
||||
update_in(params, path, &List.delete(&1, value))
|
||||
|
|
|
|||
|
|
@ -72,7 +72,11 @@ defmodule Pleroma.Web.ApiSpec.Helpers do
|
|||
end
|
||||
|
||||
def empty_array_response do
|
||||
Operation.response("Empty array", "application/json", %Schema{type: :array, example: []})
|
||||
Operation.response("Empty array", "application/json", %Schema{
|
||||
type: :array,
|
||||
items: %Schema{type: :object, example: %{}},
|
||||
example: []
|
||||
})
|
||||
end
|
||||
|
||||
def no_content_response do
|
||||
|
|
|
|||
|
|
@ -372,6 +372,10 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
|
|||
tags: ["accounts"],
|
||||
summary: "Identity proofs",
|
||||
operationId: "AccountController.identity_proofs",
|
||||
# Validators complains about unused path params otherwise
|
||||
parameters: [
|
||||
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}
|
||||
],
|
||||
description: "Not implemented",
|
||||
responses: %{
|
||||
200 => empty_array_response()
|
||||
|
|
@ -469,7 +473,6 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
|
|||
identifier: %Schema{type: :string},
|
||||
message: %Schema{type: :string}
|
||||
},
|
||||
required: [],
|
||||
# Note: example of successful registration with failed login response:
|
||||
# example: %{
|
||||
# "identifier" => "missing_confirmed_email",
|
||||
|
|
@ -530,7 +533,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
|
|||
nullable: true,
|
||||
oneOf: [
|
||||
%Schema{type: :array, items: attribute_field()},
|
||||
%Schema{type: :object, additionalProperties: %Schema{type: attribute_field()}}
|
||||
%Schema{type: :object, additionalProperties: attribute_field()}
|
||||
]
|
||||
},
|
||||
# NOTE: `source` field is not supported
|
||||
|
|
|
|||
96
lib/pleroma/web/api_spec/operations/admin/chat_operation.ex
Normal file
96
lib/pleroma/web/api_spec/operations/admin/chat_operation.ex
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.Admin.ChatOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Chat
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ChatMessage
|
||||
|
||||
import Pleroma.Web.ApiSpec.Helpers
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def delete_message_operation do
|
||||
%Operation{
|
||||
tags: ["admin", "chat"],
|
||||
summary: "Delete an individual chat message",
|
||||
operationId: "AdminAPI.ChatController.delete_message",
|
||||
parameters: [
|
||||
Operation.parameter(:id, :path, :string, "The ID of the Chat"),
|
||||
Operation.parameter(:message_id, :path, :string, "The ID of the message")
|
||||
],
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response(
|
||||
"The deleted ChatMessage",
|
||||
"application/json",
|
||||
ChatMessage
|
||||
)
|
||||
},
|
||||
security: [
|
||||
%{
|
||||
"oAuth" => ["write:chats"]
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def messages_operation do
|
||||
%Operation{
|
||||
tags: ["admin", "chat"],
|
||||
summary: "Get the most recent messages of the chat",
|
||||
operationId: "AdminAPI.ChatController.messages",
|
||||
parameters:
|
||||
[Operation.parameter(:id, :path, :string, "The ID of the Chat")] ++
|
||||
pagination_params(),
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response(
|
||||
"The messages in the chat",
|
||||
"application/json",
|
||||
Pleroma.Web.ApiSpec.ChatOperation.chat_messages_response()
|
||||
)
|
||||
},
|
||||
security: [
|
||||
%{
|
||||
"oAuth" => ["read:chats"]
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def show_operation do
|
||||
%Operation{
|
||||
tags: ["chat"],
|
||||
summary: "Create a chat",
|
||||
operationId: "AdminAPI.ChatController.show",
|
||||
parameters: [
|
||||
Operation.parameter(
|
||||
:id,
|
||||
:path,
|
||||
:string,
|
||||
"The id of the chat",
|
||||
required: true,
|
||||
example: "1234"
|
||||
)
|
||||
],
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response(
|
||||
"The existing chat",
|
||||
"application/json",
|
||||
Chat
|
||||
)
|
||||
},
|
||||
security: [
|
||||
%{
|
||||
"oAuth" => ["read"]
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.Admin.InstanceDocumentOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Helpers
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def show_operation do
|
||||
%Operation{
|
||||
tags: ["Admin", "InstanceDocument"],
|
||||
summary: "Get the instance document",
|
||||
operationId: "AdminAPI.InstanceDocumentController.show",
|
||||
security: [%{"oAuth" => ["read"]}],
|
||||
parameters: [
|
||||
Operation.parameter(:name, :path, %Schema{type: :string}, "The document name",
|
||||
required: true
|
||||
)
|
||||
| Helpers.admin_api_params()
|
||||
],
|
||||
responses: %{
|
||||
200 => document_content(),
|
||||
400 => Operation.response("Bad Request", "application/json", ApiError),
|
||||
403 => Operation.response("Forbidden", "application/json", ApiError),
|
||||
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def update_operation do
|
||||
%Operation{
|
||||
tags: ["Admin", "InstanceDocument"],
|
||||
summary: "Update the instance document",
|
||||
operationId: "AdminAPI.InstanceDocumentController.update",
|
||||
security: [%{"oAuth" => ["write"]}],
|
||||
requestBody: Helpers.request_body("Parameters", update_request()),
|
||||
parameters: [
|
||||
Operation.parameter(:name, :path, %Schema{type: :string}, "The document name",
|
||||
required: true
|
||||
)
|
||||
| Helpers.admin_api_params()
|
||||
],
|
||||
responses: %{
|
||||
200 => Operation.response("InstanceDocument", "application/json", instance_document()),
|
||||
400 => Operation.response("Bad Request", "application/json", ApiError),
|
||||
403 => Operation.response("Forbidden", "application/json", ApiError),
|
||||
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp update_request do
|
||||
%Schema{
|
||||
title: "UpdateRequest",
|
||||
description: "POST body for uploading the file",
|
||||
type: :object,
|
||||
required: [:file],
|
||||
properties: %{
|
||||
file: %Schema{
|
||||
type: :string,
|
||||
format: :binary,
|
||||
description: "The file to be uploaded, using multipart form data."
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def delete_operation do
|
||||
%Operation{
|
||||
tags: ["Admin", "InstanceDocument"],
|
||||
summary: "Get the instance document",
|
||||
operationId: "AdminAPI.InstanceDocumentController.delete",
|
||||
security: [%{"oAuth" => ["write"]}],
|
||||
parameters: [
|
||||
Operation.parameter(:name, :path, %Schema{type: :string}, "The document name",
|
||||
required: true
|
||||
)
|
||||
| Helpers.admin_api_params()
|
||||
],
|
||||
responses: %{
|
||||
200 => Operation.response("InstanceDocument", "application/json", instance_document()),
|
||||
400 => Operation.response("Bad Request", "application/json", ApiError),
|
||||
403 => Operation.response("Forbidden", "application/json", ApiError),
|
||||
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp instance_document do
|
||||
%Schema{
|
||||
title: "InstanceDocument",
|
||||
type: :object,
|
||||
properties: %{
|
||||
url: %Schema{type: :string}
|
||||
},
|
||||
example: %{
|
||||
"url" => "https://example.com/static/terms-of-service.html"
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp document_content do
|
||||
Operation.response("InstanceDocumentContent", "text/html", %Schema{
|
||||
type: :string,
|
||||
example: "<h1>Instance panel</h1>"
|
||||
})
|
||||
end
|
||||
end
|
||||
|
|
@ -56,7 +56,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.RelayOperation do
|
|||
operationId: "AdminAPI.RelayController.unfollow",
|
||||
security: [%{"oAuth" => ["write:follows"]}],
|
||||
parameters: admin_api_params(),
|
||||
requestBody: request_body("Parameters", relay_url()),
|
||||
requestBody: request_body("Parameters", relay_unfollow()),
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Status", "application/json", %Schema{
|
||||
|
|
@ -91,4 +91,14 @@ defmodule Pleroma.Web.ApiSpec.Admin.RelayOperation do
|
|||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp relay_unfollow do
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
relay_url: %Schema{type: :string, format: :uri},
|
||||
force: %Schema{type: :boolean, default: false}
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -158,7 +158,8 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
|
|||
"The messages in the chat",
|
||||
"application/json",
|
||||
chat_messages_response()
|
||||
)
|
||||
),
|
||||
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||
},
|
||||
security: [
|
||||
%{
|
||||
|
|
@ -184,7 +185,8 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
|
|||
"application/json",
|
||||
ChatMessage
|
||||
),
|
||||
400 => Operation.response("Bad Request", "application/json", ApiError)
|
||||
400 => Operation.response("Bad Request", "application/json", ApiError),
|
||||
422 => Operation.response("MRF Rejection", "application/json", ApiError)
|
||||
},
|
||||
security: [
|
||||
%{
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ defmodule Pleroma.Web.ApiSpec.CustomEmojiOperation do
|
|||
type: :object,
|
||||
properties: %{
|
||||
category: %Schema{type: :string},
|
||||
tags: %Schema{type: :array}
|
||||
tags: %Schema{type: :array, items: %Schema{type: :string}}
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ defmodule Pleroma.Web.ApiSpec.EmojiReactionOperation do
|
|||
parameters: [
|
||||
Operation.parameter(:id, :path, FlakeID, "Status ID", required: true),
|
||||
Operation.parameter(:emoji, :path, :string, "Filter by a single unicode emoji",
|
||||
required: false
|
||||
required: nil
|
||||
)
|
||||
],
|
||||
security: [%{"oAuth" => ["read:statuses"]}],
|
||||
|
|
|
|||
|
|
@ -187,8 +187,7 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do
|
|||
type: :object,
|
||||
properties: %{
|
||||
account_ids: %Schema{type: :array, description: "Array of account IDs", items: FlakeID}
|
||||
},
|
||||
required: required && [:account_ids]
|
||||
}
|
||||
},
|
||||
required: required
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,139 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.PleromaEmojiFileOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
|
||||
import Pleroma.Web.ApiSpec.Helpers
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def create_operation do
|
||||
%Operation{
|
||||
tags: ["Emoji Packs"],
|
||||
summary: "Add new file to the pack",
|
||||
operationId: "PleromaAPI.EmojiPackController.add_file",
|
||||
security: [%{"oAuth" => ["write"]}],
|
||||
requestBody: request_body("Parameters", create_request(), required: true),
|
||||
parameters: [name_param()],
|
||||
responses: %{
|
||||
200 => Operation.response("Files Object", "application/json", files_object()),
|
||||
422 => Operation.response("Unprocessable Entity", "application/json", ApiError),
|
||||
404 => Operation.response("Not Found", "application/json", ApiError),
|
||||
400 => Operation.response("Bad Request", "application/json", ApiError),
|
||||
409 => Operation.response("Conflict", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp create_request do
|
||||
%Schema{
|
||||
type: :object,
|
||||
required: [:file],
|
||||
properties: %{
|
||||
file: %Schema{
|
||||
description:
|
||||
"File needs to be uploaded with the multipart request or link to remote file",
|
||||
anyOf: [
|
||||
%Schema{type: :string, format: :binary},
|
||||
%Schema{type: :string, format: :uri}
|
||||
]
|
||||
},
|
||||
shortcode: %Schema{
|
||||
type: :string,
|
||||
description:
|
||||
"Shortcode for new emoji, must be unique for all emoji. If not sended, shortcode will be taken from original filename."
|
||||
},
|
||||
filename: %Schema{
|
||||
type: :string,
|
||||
description:
|
||||
"New emoji file name. If not specified will be taken from original filename."
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def update_operation do
|
||||
%Operation{
|
||||
tags: ["Emoji Packs"],
|
||||
summary: "Add new file to the pack",
|
||||
operationId: "PleromaAPI.EmojiPackController.update_file",
|
||||
security: [%{"oAuth" => ["write"]}],
|
||||
requestBody: request_body("Parameters", update_request(), required: true),
|
||||
parameters: [name_param()],
|
||||
responses: %{
|
||||
200 => Operation.response("Files Object", "application/json", files_object()),
|
||||
404 => Operation.response("Not Found", "application/json", ApiError),
|
||||
400 => Operation.response("Bad Request", "application/json", ApiError),
|
||||
409 => Operation.response("Conflict", "application/json", ApiError),
|
||||
422 => Operation.response("Unprocessable Entity", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp update_request do
|
||||
%Schema{
|
||||
type: :object,
|
||||
required: [:shortcode, :new_shortcode, :new_filename],
|
||||
properties: %{
|
||||
shortcode: %Schema{
|
||||
type: :string,
|
||||
description: "Emoji file shortcode"
|
||||
},
|
||||
new_shortcode: %Schema{
|
||||
type: :string,
|
||||
description: "New emoji file shortcode"
|
||||
},
|
||||
new_filename: %Schema{
|
||||
type: :string,
|
||||
description: "New filename for emoji file"
|
||||
},
|
||||
force: %Schema{
|
||||
type: :boolean,
|
||||
description: "With true value to overwrite existing emoji with new shortcode",
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def delete_operation do
|
||||
%Operation{
|
||||
tags: ["Emoji Packs"],
|
||||
summary: "Delete emoji file from pack",
|
||||
operationId: "PleromaAPI.EmojiPackController.delete_file",
|
||||
security: [%{"oAuth" => ["write"]}],
|
||||
parameters: [
|
||||
name_param(),
|
||||
Operation.parameter(:shortcode, :query, :string, "File shortcode",
|
||||
example: "cofe",
|
||||
required: true
|
||||
)
|
||||
],
|
||||
responses: %{
|
||||
200 => Operation.response("Files Object", "application/json", files_object()),
|
||||
400 => Operation.response("Bad Request", "application/json", ApiError),
|
||||
404 => Operation.response("Not Found", "application/json", ApiError),
|
||||
422 => Operation.response("Unprocessable Entity", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp name_param do
|
||||
Operation.parameter(:name, :query, :string, "Pack Name", example: "cofe", required: true)
|
||||
end
|
||||
|
||||
defp files_object do
|
||||
%Schema{
|
||||
type: :object,
|
||||
additionalProperties: %Schema{type: :string},
|
||||
description: "Object with emoji names as keys and filenames as values"
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
@ -19,7 +19,21 @@ defmodule Pleroma.Web.ApiSpec.PleromaEmojiPackOperation do
|
|||
tags: ["Emoji Packs"],
|
||||
summary: "Make request to another instance for emoji packs list",
|
||||
security: [%{"oAuth" => ["write"]}],
|
||||
parameters: [url_param()],
|
||||
parameters: [
|
||||
url_param(),
|
||||
Operation.parameter(
|
||||
:page,
|
||||
:query,
|
||||
%Schema{type: :integer, default: 1},
|
||||
"Page"
|
||||
),
|
||||
Operation.parameter(
|
||||
:page_size,
|
||||
:query,
|
||||
%Schema{type: :integer, default: 30},
|
||||
"Number of emoji to return"
|
||||
)
|
||||
],
|
||||
operationId: "PleromaAPI.EmojiPackController.remote",
|
||||
responses: %{
|
||||
200 => emoji_packs_response(),
|
||||
|
|
@ -175,111 +189,6 @@ defmodule Pleroma.Web.ApiSpec.PleromaEmojiPackOperation do
|
|||
}
|
||||
end
|
||||
|
||||
def add_file_operation do
|
||||
%Operation{
|
||||
tags: ["Emoji Packs"],
|
||||
summary: "Add new file to the pack",
|
||||
operationId: "PleromaAPI.EmojiPackController.add_file",
|
||||
security: [%{"oAuth" => ["write"]}],
|
||||
requestBody: request_body("Parameters", add_file_request(), required: true),
|
||||
parameters: [name_param()],
|
||||
responses: %{
|
||||
200 => Operation.response("Files Object", "application/json", files_object()),
|
||||
400 => Operation.response("Bad Request", "application/json", ApiError),
|
||||
409 => Operation.response("Conflict", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp add_file_request do
|
||||
%Schema{
|
||||
type: :object,
|
||||
required: [:file],
|
||||
properties: %{
|
||||
file: %Schema{
|
||||
description:
|
||||
"File needs to be uploaded with the multipart request or link to remote file",
|
||||
anyOf: [
|
||||
%Schema{type: :string, format: :binary},
|
||||
%Schema{type: :string, format: :uri}
|
||||
]
|
||||
},
|
||||
shortcode: %Schema{
|
||||
type: :string,
|
||||
description:
|
||||
"Shortcode for new emoji, must be unique for all emoji. If not sended, shortcode will be taken from original filename."
|
||||
},
|
||||
filename: %Schema{
|
||||
type: :string,
|
||||
description:
|
||||
"New emoji file name. If not specified will be taken from original filename."
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def update_file_operation do
|
||||
%Operation{
|
||||
tags: ["Emoji Packs"],
|
||||
summary: "Add new file to the pack",
|
||||
operationId: "PleromaAPI.EmojiPackController.update_file",
|
||||
security: [%{"oAuth" => ["write"]}],
|
||||
requestBody: request_body("Parameters", update_file_request(), required: true),
|
||||
parameters: [name_param()],
|
||||
responses: %{
|
||||
200 => Operation.response("Files Object", "application/json", files_object()),
|
||||
400 => Operation.response("Bad Request", "application/json", ApiError),
|
||||
409 => Operation.response("Conflict", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp update_file_request do
|
||||
%Schema{
|
||||
type: :object,
|
||||
required: [:shortcode, :new_shortcode, :new_filename],
|
||||
properties: %{
|
||||
shortcode: %Schema{
|
||||
type: :string,
|
||||
description: "Emoji file shortcode"
|
||||
},
|
||||
new_shortcode: %Schema{
|
||||
type: :string,
|
||||
description: "New emoji file shortcode"
|
||||
},
|
||||
new_filename: %Schema{
|
||||
type: :string,
|
||||
description: "New filename for emoji file"
|
||||
},
|
||||
force: %Schema{
|
||||
type: :boolean,
|
||||
description: "With true value to overwrite existing emoji with new shortcode",
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def delete_file_operation do
|
||||
%Operation{
|
||||
tags: ["Emoji Packs"],
|
||||
summary: "Delete emoji file from pack",
|
||||
operationId: "PleromaAPI.EmojiPackController.delete_file",
|
||||
security: [%{"oAuth" => ["write"]}],
|
||||
parameters: [
|
||||
name_param(),
|
||||
Operation.parameter(:shortcode, :query, :string, "File shortcode",
|
||||
example: "cofe",
|
||||
required: true
|
||||
)
|
||||
],
|
||||
responses: %{
|
||||
200 => Operation.response("Files Object", "application/json", files_object()),
|
||||
400 => Operation.response("Bad Request", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def import_from_filesystem_operation do
|
||||
%Operation{
|
||||
tags: ["Emoji Packs"],
|
||||
|
|
@ -297,7 +206,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaEmojiPackOperation do
|
|||
end
|
||||
|
||||
defp name_param do
|
||||
Operation.parameter(:name, :path, :string, "Pack Name", example: "cofe", required: true)
|
||||
Operation.parameter(:name, :query, :string, "Pack Name", example: "cofe", required: true)
|
||||
end
|
||||
|
||||
defp url_param do
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
|||
"application/json",
|
||||
%Schema{oneOf: [Status, ScheduledStatus]}
|
||||
),
|
||||
422 => Operation.response("Bad Request", "application/json", ApiError)
|
||||
422 => Operation.response("Bad Request / MRF Rejection", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
|
|||
80
lib/pleroma/web/api_spec/operations/user_import_operation.ex
Normal file
80
lib/pleroma/web/api_spec/operations/user_import_operation.ex
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.UserImportOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
|
||||
import Pleroma.Web.ApiSpec.Helpers
|
||||
|
||||
@spec open_api_operation(atom) :: Operation.t()
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def follow_operation do
|
||||
%Operation{
|
||||
tags: ["follow_import"],
|
||||
summary: "Imports your follows.",
|
||||
operationId: "UserImportController.follow",
|
||||
requestBody: request_body("Parameters", import_request(), required: true),
|
||||
responses: %{
|
||||
200 => ok_response(),
|
||||
500 => Operation.response("Error", "application/json", ApiError)
|
||||
},
|
||||
security: [%{"oAuth" => ["write:follow"]}]
|
||||
}
|
||||
end
|
||||
|
||||
def blocks_operation do
|
||||
%Operation{
|
||||
tags: ["blocks_import"],
|
||||
summary: "Imports your blocks.",
|
||||
operationId: "UserImportController.blocks",
|
||||
requestBody: request_body("Parameters", import_request(), required: true),
|
||||
responses: %{
|
||||
200 => ok_response(),
|
||||
500 => Operation.response("Error", "application/json", ApiError)
|
||||
},
|
||||
security: [%{"oAuth" => ["write:blocks"]}]
|
||||
}
|
||||
end
|
||||
|
||||
def mutes_operation do
|
||||
%Operation{
|
||||
tags: ["mutes_import"],
|
||||
summary: "Imports your mutes.",
|
||||
operationId: "UserImportController.mutes",
|
||||
requestBody: request_body("Parameters", import_request(), required: true),
|
||||
responses: %{
|
||||
200 => ok_response(),
|
||||
500 => Operation.response("Error", "application/json", ApiError)
|
||||
},
|
||||
security: [%{"oAuth" => ["write:mutes"]}]
|
||||
}
|
||||
end
|
||||
|
||||
defp import_request do
|
||||
%Schema{
|
||||
type: :object,
|
||||
required: [:list],
|
||||
properties: %{
|
||||
list: %Schema{
|
||||
description:
|
||||
"STRING or FILE containing a whitespace-separated list of accounts to import.",
|
||||
anyOf: [
|
||||
%Schema{type: :string, format: :binary},
|
||||
%Schema{type: :string}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp ok_response do
|
||||
Operation.response("Ok", "application/json", %Schema{type: :string, example: "ok"})
|
||||
end
|
||||
end
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessage do
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Emoji
|
||||
|
||||
require OpenApiSpex
|
||||
|
||||
|
|
@ -18,7 +19,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessage do
|
|||
chat_id: %Schema{type: :string},
|
||||
content: %Schema{type: :string, nullable: true},
|
||||
created_at: %Schema{type: :string, format: :"date-time"},
|
||||
emojis: %Schema{type: :array},
|
||||
emojis: %Schema{type: :array, items: Emoji},
|
||||
attachment: %Schema{type: :object, nullable: true},
|
||||
card: %Schema{
|
||||
type: :object,
|
||||
|
|
|
|||
|
|
@ -27,9 +27,9 @@ defmodule Pleroma.Web.ApiSpec.Schemas.ScheduledStatus do
|
|||
media_ids: %Schema{type: :array, nullable: true, items: %Schema{type: :string}},
|
||||
sensitive: %Schema{type: :boolean, nullable: true},
|
||||
spoiler_text: %Schema{type: :string, nullable: true},
|
||||
visibility: %Schema{type: VisibilityScope, nullable: true},
|
||||
visibility: %Schema{allOf: [VisibilityScope], nullable: true},
|
||||
scheduled_at: %Schema{type: :string, format: :"date-time", nullable: true},
|
||||
poll: %Schema{type: Poll, nullable: true},
|
||||
poll: %Schema{allOf: [Poll], nullable: true},
|
||||
in_reply_to_id: %Schema{type: :string, nullable: true}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,9 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
local: true
|
||||
)} do
|
||||
{:ok, activity}
|
||||
else
|
||||
{:common_pipeline, {:reject, _} = e} -> e
|
||||
e -> e
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -550,4 +553,21 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
def show_reblogs(%User{} = user, %User{} = target) do
|
||||
UserRelationship.delete_reblog_mute(user, target)
|
||||
end
|
||||
|
||||
def get_user(ap_id, fake_record_fallback \\ true) do
|
||||
cond do
|
||||
user = User.get_cached_by_ap_id(ap_id) ->
|
||||
user
|
||||
|
||||
user = User.get_by_guessed_nickname(ap_id) ->
|
||||
user
|
||||
|
||||
fake_record_fallback ->
|
||||
# TODO: refactor (fake records is never a good idea)
|
||||
User.error_user(ap_id)
|
||||
|
||||
true ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -48,13 +48,13 @@ defmodule Pleroma.Web.ControllerHelper do
|
|||
|
||||
defp param_to_integer(_, default), do: default
|
||||
|
||||
def add_link_headers(conn, activities, extra_params \\ %{})
|
||||
def add_link_headers(conn, entries, extra_params \\ %{})
|
||||
|
||||
def add_link_headers(%{assigns: %{skip_link_headers: true}} = conn, _activities, _extra_params),
|
||||
def add_link_headers(%{assigns: %{skip_link_headers: true}} = conn, _entries, _extra_params),
|
||||
do: conn
|
||||
|
||||
def add_link_headers(conn, activities, extra_params) do
|
||||
case get_pagination_fields(conn, activities, extra_params) do
|
||||
def add_link_headers(conn, entries, extra_params) do
|
||||
case get_pagination_fields(conn, entries, extra_params) do
|
||||
%{"next" => next_url, "prev" => prev_url} ->
|
||||
put_resp_header(conn, "link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
|
||||
|
||||
|
|
@ -78,19 +78,15 @@ defmodule Pleroma.Web.ControllerHelper do
|
|||
}
|
||||
end
|
||||
|
||||
def get_pagination_fields(conn, activities, extra_params \\ %{}) do
|
||||
case List.last(activities) do
|
||||
def get_pagination_fields(conn, entries, extra_params \\ %{}) do
|
||||
case List.last(entries) do
|
||||
%{pagination_id: max_id} when not is_nil(max_id) ->
|
||||
%{pagination_id: min_id} =
|
||||
activities
|
||||
|> List.first()
|
||||
%{pagination_id: min_id} = List.first(entries)
|
||||
|
||||
build_pagination_fields(conn, min_id, max_id, extra_params)
|
||||
|
||||
%{id: max_id} ->
|
||||
%{id: min_id} =
|
||||
activities
|
||||
|> List.first()
|
||||
%{id: min_id} = List.first(entries)
|
||||
|
||||
build_pagination_fields(conn, min_id, max_id, extra_params)
|
||||
|
||||
|
|
|
|||
185
lib/pleroma/web/fed_sockets/fed_registry.ex
Normal file
185
lib/pleroma/web/fed_sockets/fed_registry.ex
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.FedSockets.FedRegistry do
|
||||
@moduledoc """
|
||||
The FedRegistry stores the active FedSockets for quick retrieval.
|
||||
|
||||
The storage and retrieval portion of the FedRegistry is done in process through
|
||||
elixir's `Registry` module for speed and its ability to monitor for terminated processes.
|
||||
|
||||
Dropped connections will be caught by `Registry` and deleted. Since the next
|
||||
message will initiate a new connection there is no reason to try and reconnect at that point.
|
||||
|
||||
Normally outside modules should have no need to call or use the FedRegistry themselves.
|
||||
"""
|
||||
|
||||
alias Pleroma.Web.FedSockets.FedSocket
|
||||
alias Pleroma.Web.FedSockets.SocketInfo
|
||||
|
||||
require Logger
|
||||
|
||||
@default_rejection_duration 15 * 60 * 1000
|
||||
@rejections :fed_socket_rejections
|
||||
|
||||
@doc """
|
||||
Retrieves a FedSocket from the Registry given it's origin.
|
||||
|
||||
The origin is expected to be a string identifying the endpoint "example.com" or "example2.com:8080"
|
||||
|
||||
Will return:
|
||||
* {:ok, fed_socket} for working FedSockets
|
||||
* {:error, :rejected} for origins that have been tried and refused within the rejection duration interval
|
||||
* {:error, some_reason} usually :missing for unknown origins
|
||||
"""
|
||||
def get_fed_socket(origin) do
|
||||
case get_registry_data(origin) do
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
|
||||
{:ok, %{state: :connected} = socket_info} ->
|
||||
{:ok, socket_info}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds a connected FedSocket to the Registry.
|
||||
|
||||
Always returns {:ok, fed_socket}
|
||||
"""
|
||||
def add_fed_socket(origin, pid \\ nil) do
|
||||
origin
|
||||
|> SocketInfo.build(pid)
|
||||
|> SocketInfo.connect()
|
||||
|> add_socket_info
|
||||
end
|
||||
|
||||
defp add_socket_info(%{origin: origin, state: :connected} = socket_info) do
|
||||
case Registry.register(FedSockets.Registry, origin, socket_info) do
|
||||
{:ok, _owner} ->
|
||||
clear_prior_rejection(origin)
|
||||
Logger.debug("fedsocket added: #{inspect(origin)}")
|
||||
|
||||
{:ok, socket_info}
|
||||
|
||||
{:error, {:already_registered, _pid}} ->
|
||||
FedSocket.close(socket_info)
|
||||
existing_socket_info = Registry.lookup(FedSockets.Registry, origin)
|
||||
|
||||
{:ok, existing_socket_info}
|
||||
|
||||
_ ->
|
||||
{:error, :error_adding_socket}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Mark this origin as having rejected a connection attempt.
|
||||
This will keep it from getting additional connection attempts
|
||||
for a period of time specified in the config.
|
||||
|
||||
Always returns {:ok, new_reg_data}
|
||||
"""
|
||||
def set_host_rejected(uri) do
|
||||
new_reg_data =
|
||||
uri
|
||||
|> SocketInfo.origin()
|
||||
|> get_or_create_registry_data()
|
||||
|> set_to_rejected()
|
||||
|> save_registry_data()
|
||||
|
||||
{:ok, new_reg_data}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Retrieves the FedRegistryData from the Registry given it's origin.
|
||||
|
||||
The origin is expected to be a string identifying the endpoint "example.com" or "example2.com:8080"
|
||||
|
||||
Will return:
|
||||
* {:ok, fed_registry_data} for known origins
|
||||
* {:error, :missing} for uniknown origins
|
||||
* {:error, :cache_error} indicating some low level runtime issues
|
||||
"""
|
||||
def get_registry_data(origin) do
|
||||
case Registry.lookup(FedSockets.Registry, origin) do
|
||||
[] ->
|
||||
if is_rejected?(origin) do
|
||||
Logger.debug("previously rejected fedsocket requested")
|
||||
{:error, :rejected}
|
||||
else
|
||||
{:error, :missing}
|
||||
end
|
||||
|
||||
[{_pid, %{state: :connected} = socket_info}] ->
|
||||
{:ok, socket_info}
|
||||
|
||||
_ ->
|
||||
{:error, :cache_error}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Retrieves a map of all sockets from the Registry. The keys are the origins and the values are the corresponding SocketInfo
|
||||
"""
|
||||
def list_all do
|
||||
(list_all_connected() ++ list_all_rejected())
|
||||
|> Enum.into(%{})
|
||||
end
|
||||
|
||||
defp list_all_connected do
|
||||
FedSockets.Registry
|
||||
|> Registry.select([{{:"$1", :_, :"$3"}, [], [{{:"$1", :"$3"}}]}])
|
||||
end
|
||||
|
||||
defp list_all_rejected do
|
||||
{:ok, keys} = Cachex.keys(@rejections)
|
||||
|
||||
{:ok, registry_data} =
|
||||
Cachex.execute(@rejections, fn worker ->
|
||||
Enum.map(keys, fn k -> {k, Cachex.get!(worker, k)} end)
|
||||
end)
|
||||
|
||||
registry_data
|
||||
end
|
||||
|
||||
defp clear_prior_rejection(origin),
|
||||
do: Cachex.del(@rejections, origin)
|
||||
|
||||
defp is_rejected?(origin) do
|
||||
case Cachex.get(@rejections, origin) do
|
||||
{:ok, nil} ->
|
||||
false
|
||||
|
||||
{:ok, _} ->
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
defp get_or_create_registry_data(origin) do
|
||||
case get_registry_data(origin) do
|
||||
{:error, :missing} ->
|
||||
%SocketInfo{origin: origin}
|
||||
|
||||
{:ok, socket_info} ->
|
||||
socket_info
|
||||
end
|
||||
end
|
||||
|
||||
defp save_registry_data(%SocketInfo{origin: origin, state: :connected} = socket_info) do
|
||||
{:ok, true} = Registry.update_value(FedSockets.Registry, origin, fn _ -> socket_info end)
|
||||
socket_info
|
||||
end
|
||||
|
||||
defp save_registry_data(%SocketInfo{origin: origin, state: :rejected} = socket_info) do
|
||||
rejection_expiration =
|
||||
Pleroma.Config.get([:fed_sockets, :rejection_duration], @default_rejection_duration)
|
||||
|
||||
{:ok, true} = Cachex.put(@rejections, origin, socket_info, ttl: rejection_expiration)
|
||||
socket_info
|
||||
end
|
||||
|
||||
defp set_to_rejected(%SocketInfo{} = socket_info),
|
||||
do: %SocketInfo{socket_info | state: :rejected}
|
||||
end
|
||||
137
lib/pleroma/web/fed_sockets/fed_socket.ex
Normal file
137
lib/pleroma/web/fed_sockets/fed_socket.ex
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.FedSockets.FedSocket do
|
||||
@moduledoc """
|
||||
The FedSocket module abstracts the actions to be taken taken on connections regardless of
|
||||
whether the connection started as inbound or outbound.
|
||||
|
||||
|
||||
Normally outside modules will have no need to call the FedSocket module directly.
|
||||
"""
|
||||
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Object.Containment
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ObjectView
|
||||
alias Pleroma.Web.ActivityPub.UserView
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.FedSockets.FetchRegistry
|
||||
alias Pleroma.Web.FedSockets.IngesterWorker
|
||||
alias Pleroma.Web.FedSockets.OutgoingHandler
|
||||
alias Pleroma.Web.FedSockets.SocketInfo
|
||||
|
||||
require Logger
|
||||
|
||||
@shake "61dd18f7-f1e6-49a4-939a-a749fcdc1103"
|
||||
|
||||
def connect_to_host(uri) do
|
||||
case OutgoingHandler.start_link(uri) do
|
||||
{:ok, pid} ->
|
||||
{:ok, pid}
|
||||
|
||||
error ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
def close(%SocketInfo{pid: socket_pid}),
|
||||
do: Process.send(socket_pid, :close, [])
|
||||
|
||||
def publish(%SocketInfo{pid: socket_pid}, json) do
|
||||
%{action: :publish, data: json}
|
||||
|> Jason.encode!()
|
||||
|> send_packet(socket_pid)
|
||||
end
|
||||
|
||||
def fetch(%SocketInfo{pid: socket_pid}, id) do
|
||||
fetch_uuid = FetchRegistry.register_fetch(id)
|
||||
|
||||
%{action: :fetch, data: id, uuid: fetch_uuid}
|
||||
|> Jason.encode!()
|
||||
|> send_packet(socket_pid)
|
||||
|
||||
wait_for_fetch_to_return(fetch_uuid, 0)
|
||||
end
|
||||
|
||||
def receive_package(%SocketInfo{} = fed_socket, json) do
|
||||
json
|
||||
|> Jason.decode!()
|
||||
|> process_package(fed_socket)
|
||||
end
|
||||
|
||||
defp wait_for_fetch_to_return(uuid, cntr) do
|
||||
case FetchRegistry.check_fetch(uuid) do
|
||||
{:error, :waiting} ->
|
||||
Process.sleep(:math.pow(cntr, 3) |> Kernel.trunc())
|
||||
wait_for_fetch_to_return(uuid, cntr + 1)
|
||||
|
||||
{:error, :missing} ->
|
||||
Logger.error("FedSocket fetch timed out - #{inspect(uuid)}")
|
||||
{:error, :timeout}
|
||||
|
||||
{:ok, _fr} ->
|
||||
FetchRegistry.pop_fetch(uuid)
|
||||
end
|
||||
end
|
||||
|
||||
defp process_package(%{"action" => "publish", "data" => data}, %{origin: origin} = _fed_socket) do
|
||||
if Containment.contain_origin(origin, data) do
|
||||
IngesterWorker.enqueue("ingest", %{"object" => data})
|
||||
end
|
||||
|
||||
{:reply, %{"action" => "publish_reply", "status" => "processed"}}
|
||||
end
|
||||
|
||||
defp process_package(%{"action" => "fetch_reply", "uuid" => uuid, "data" => data}, _fed_socket) do
|
||||
FetchRegistry.register_fetch_received(uuid, data)
|
||||
{:noreply, nil}
|
||||
end
|
||||
|
||||
defp process_package(%{"action" => "fetch", "uuid" => uuid, "data" => ap_id}, _fed_socket) do
|
||||
{:ok, data} = render_fetched_data(ap_id, uuid)
|
||||
{:reply, data}
|
||||
end
|
||||
|
||||
defp process_package(%{"action" => "publish_reply"}, _fed_socket) do
|
||||
{:noreply, nil}
|
||||
end
|
||||
|
||||
defp process_package(other, _fed_socket) do
|
||||
Logger.warn("unknown json packages received #{inspect(other)}")
|
||||
{:noreply, nil}
|
||||
end
|
||||
|
||||
defp render_fetched_data(ap_id, uuid) do
|
||||
{:ok,
|
||||
%{
|
||||
"action" => "fetch_reply",
|
||||
"status" => "processed",
|
||||
"uuid" => uuid,
|
||||
"data" => represent_item(ap_id)
|
||||
}}
|
||||
end
|
||||
|
||||
defp represent_item(ap_id) do
|
||||
case User.get_by_ap_id(ap_id) do
|
||||
nil ->
|
||||
object = Object.get_cached_by_ap_id(ap_id)
|
||||
|
||||
if Visibility.is_public?(object) do
|
||||
Phoenix.View.render_to_string(ObjectView, "object.json", object: object)
|
||||
else
|
||||
nil
|
||||
end
|
||||
|
||||
user ->
|
||||
Phoenix.View.render_to_string(UserView, "user.json", user: user)
|
||||
end
|
||||
end
|
||||
|
||||
defp send_packet(data, socket_pid) do
|
||||
Process.send(socket_pid, {:send, data}, [])
|
||||
end
|
||||
|
||||
def shake, do: @shake
|
||||
end
|
||||
185
lib/pleroma/web/fed_sockets/fed_sockets.ex
Normal file
185
lib/pleroma/web/fed_sockets/fed_sockets.ex
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.FedSockets do
|
||||
@moduledoc """
|
||||
This documents the FedSockets framework. A framework for federating
|
||||
ActivityPub objects between servers via persistant WebSocket connections.
|
||||
|
||||
FedSockets allow servers to authenticate on first contact and maintain that
|
||||
connection, eliminating the need to authenticate every time data needs to be shared.
|
||||
|
||||
## Protocol
|
||||
FedSockets currently support 2 types of data transfer:
|
||||
* `publish` method which doesn't require a response
|
||||
* `fetch` method requires a response be sent
|
||||
|
||||
### Publish
|
||||
The publish operation sends a json encoded map of the shape:
|
||||
%{action: :publish, data: json}
|
||||
and accepts (but does not require) a reply of form:
|
||||
%{"action" => "publish_reply"}
|
||||
|
||||
The outgoing params represent
|
||||
* data: ActivityPub object encoded into json
|
||||
|
||||
|
||||
### Fetch
|
||||
The fetch operation sends a json encoded map of the shape:
|
||||
%{action: :fetch, data: id, uuid: fetch_uuid}
|
||||
and requires a reply of form:
|
||||
%{"action" => "fetch_reply", "uuid" => uuid, "data" => data}
|
||||
|
||||
The outgoing params represent
|
||||
* id: an ActivityPub object URI
|
||||
* uuid: a unique uuid generated by the sender
|
||||
|
||||
The reply params represent
|
||||
* data: an ActivityPub object encoded into json
|
||||
* uuid: the uuid sent along with the fetch request
|
||||
|
||||
## Examples
|
||||
Clients of FedSocket transfers shouldn't need to use any of the functions outside of this module.
|
||||
|
||||
A typical publish operation can be performed through the following code, and a fetch operation in a similar manner.
|
||||
|
||||
case FedSockets.get_or_create_fed_socket(inbox) do
|
||||
{:ok, fedsocket} ->
|
||||
FedSockets.publish(fedsocket, json)
|
||||
|
||||
_ ->
|
||||
alternative_publish(inbox, actor, json, params)
|
||||
end
|
||||
|
||||
## Configuration
|
||||
FedSockets have the following config settings
|
||||
|
||||
config :pleroma, :fed_sockets,
|
||||
enabled: true,
|
||||
ping_interval: :timer.seconds(15),
|
||||
connection_duration: :timer.hours(1),
|
||||
rejection_duration: :timer.hours(1),
|
||||
fed_socket_fetches: [
|
||||
default: 12_000,
|
||||
interval: 3_000,
|
||||
lazy: false
|
||||
]
|
||||
* enabled - turn FedSockets on or off with this flag. Can be toggled at runtime.
|
||||
* connection_duration - How long a FedSocket can sit idle before it's culled.
|
||||
* rejection_duration - After failing to make a FedSocket connection a host will be excluded
|
||||
from further connections for this amount of time
|
||||
* fed_socket_fetches - Use these parameters to pass options to the Cachex queue backing the FetchRegistry
|
||||
* fed_socket_rejections - Use these parameters to pass options to the Cachex queue backing the FedRegistry
|
||||
|
||||
Cachex options are
|
||||
* default: the minimum amount of time a fetch can wait before it times out.
|
||||
* interval: the interval between checks for timed out entries. This plus the default represent the maximum time allowed
|
||||
* lazy: leave at false for consistant and fast lookups, set to true for stricter timeout enforcement
|
||||
|
||||
"""
|
||||
require Logger
|
||||
|
||||
alias Pleroma.Web.FedSockets.FedRegistry
|
||||
alias Pleroma.Web.FedSockets.FedSocket
|
||||
alias Pleroma.Web.FedSockets.SocketInfo
|
||||
|
||||
@doc """
|
||||
returns a FedSocket for the given origin. Will reuse an existing one or create a new one.
|
||||
|
||||
address is expected to be a fully formed URL such as:
|
||||
"http://www.example.com" or "http://www.example.com:8080"
|
||||
|
||||
It can and usually does include additional path parameters,
|
||||
but these are ignored as the FedSockets are organized by host and port info alone.
|
||||
"""
|
||||
def get_or_create_fed_socket(address) do
|
||||
with {:cache, {:error, :missing}} <- {:cache, get_fed_socket(address)},
|
||||
{:connect, {:ok, _pid}} <- {:connect, FedSocket.connect_to_host(address)},
|
||||
{:cache, {:ok, fed_socket}} <- {:cache, get_fed_socket(address)} do
|
||||
Logger.debug("fedsocket created for - #{inspect(address)}")
|
||||
{:ok, fed_socket}
|
||||
else
|
||||
{:cache, {:ok, socket}} ->
|
||||
Logger.debug("fedsocket found in cache - #{inspect(address)}")
|
||||
{:ok, socket}
|
||||
|
||||
{:cache, {:error, :rejected} = e} ->
|
||||
e
|
||||
|
||||
{:connect, {:error, _host}} ->
|
||||
Logger.debug("set host rejected for - #{inspect(address)}")
|
||||
FedRegistry.set_host_rejected(address)
|
||||
{:error, :rejected}
|
||||
|
||||
{_, {:error, :disabled}} ->
|
||||
{:error, :disabled}
|
||||
|
||||
{_, {:error, reason}} ->
|
||||
Logger.warn("get_or_create_fed_socket error - #{inspect(reason)}")
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
returns a FedSocket for the given origin. Will not create a new FedSocket if one does not exist.
|
||||
|
||||
address is expected to be a fully formed URL such as:
|
||||
"http://www.example.com" or "http://www.example.com:8080"
|
||||
"""
|
||||
def get_fed_socket(address) do
|
||||
origin = SocketInfo.origin(address)
|
||||
|
||||
with {:config, true} <- {:config, Pleroma.Config.get([:fed_sockets, :enabled], false)},
|
||||
{:ok, socket} <- FedRegistry.get_fed_socket(origin) do
|
||||
{:ok, socket}
|
||||
else
|
||||
{:config, _} ->
|
||||
{:error, :disabled}
|
||||
|
||||
{:error, :rejected} ->
|
||||
Logger.debug("FedSocket previously rejected - #{inspect(origin)}")
|
||||
{:error, :rejected}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sends the supplied data via the publish protocol.
|
||||
It will not block waiting for a reply.
|
||||
Returns :ok but this is not an indication of a successful transfer.
|
||||
|
||||
the data is expected to be JSON encoded binary data.
|
||||
"""
|
||||
def publish(%SocketInfo{} = fed_socket, json) do
|
||||
FedSocket.publish(fed_socket, json)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sends the supplied data via the fetch protocol.
|
||||
It will block waiting for a reply or timeout.
|
||||
|
||||
Returns {:ok, object} where object is the requested object (or nil)
|
||||
{:error, :timeout} in the event the message was not responded to
|
||||
|
||||
the id is expected to be the URI of an ActivityPub object.
|
||||
"""
|
||||
def fetch(%SocketInfo{} = fed_socket, id) do
|
||||
FedSocket.fetch(fed_socket, id)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Disconnect all and restart FedSockets.
|
||||
This is mainly used in development and testing but could be useful in production.
|
||||
"""
|
||||
def reset do
|
||||
FedRegistry
|
||||
|> Process.whereis()
|
||||
|> Process.exit(:testing)
|
||||
end
|
||||
|
||||
def uri_for_origin(origin),
|
||||
do: "ws://#{origin}/api/fedsocket/v1"
|
||||
end
|
||||
151
lib/pleroma/web/fed_sockets/fetch_registry.ex
Normal file
151
lib/pleroma/web/fed_sockets/fetch_registry.ex
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.FedSockets.FetchRegistry do
|
||||
@moduledoc """
|
||||
The FetchRegistry acts as a broker for fetch requests and return values.
|
||||
This allows calling processes to block while waiting for a reply.
|
||||
It doesn't impose it's own process instead using `Cachex` to handle fetches in process, allowing
|
||||
multi threaded processes to avoid bottlenecking.
|
||||
|
||||
Normally outside modules will have no need to call or use the FetchRegistry themselves.
|
||||
|
||||
The `Cachex` parameters can be controlled from the config. Since exact timeout intervals
|
||||
aren't necessary the following settings are used by default:
|
||||
|
||||
config :pleroma, :fed_sockets,
|
||||
fed_socket_fetches: [
|
||||
default: 12_000,
|
||||
interval: 3_000,
|
||||
lazy: false
|
||||
]
|
||||
|
||||
"""
|
||||
|
||||
defmodule FetchRegistryData do
|
||||
defstruct uuid: nil,
|
||||
sent_json: nil,
|
||||
received_json: nil,
|
||||
sent_at: nil,
|
||||
received_at: nil
|
||||
end
|
||||
|
||||
alias Ecto.UUID
|
||||
|
||||
require Logger
|
||||
|
||||
@fetches :fed_socket_fetches
|
||||
|
||||
@doc """
|
||||
Registers a json request wth the FetchRegistry and returns the identifying UUID.
|
||||
"""
|
||||
def register_fetch(json) do
|
||||
%FetchRegistryData{uuid: uuid} =
|
||||
json
|
||||
|> new_registry_data
|
||||
|> save_registry_data
|
||||
|
||||
uuid
|
||||
end
|
||||
|
||||
@doc """
|
||||
Reports on the status of a Fetch given the identifying UUID.
|
||||
|
||||
Will return
|
||||
* {:ok, fetched_object} if a fetch has completed
|
||||
* {:error, :waiting} if a fetch is still pending
|
||||
* {:error, other_error} usually :missing to indicate a fetch that has timed out
|
||||
"""
|
||||
def check_fetch(uuid) do
|
||||
case get_registry_data(uuid) do
|
||||
{:ok, %FetchRegistryData{received_at: nil}} ->
|
||||
{:error, :waiting}
|
||||
|
||||
{:ok, %FetchRegistryData{} = reg_data} ->
|
||||
{:ok, reg_data}
|
||||
|
||||
e ->
|
||||
e
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Retrieves the response to a fetch given the identifying UUID.
|
||||
The completed fetch will be deleted from the FetchRegistry
|
||||
|
||||
Will return
|
||||
* {:ok, fetched_object} if a fetch has completed
|
||||
* {:error, :waiting} if a fetch is still pending
|
||||
* {:error, other_error} usually :missing to indicate a fetch that has timed out
|
||||
"""
|
||||
def pop_fetch(uuid) do
|
||||
case check_fetch(uuid) do
|
||||
{:ok, %FetchRegistryData{received_json: received_json}} ->
|
||||
delete_registry_data(uuid)
|
||||
{:ok, received_json}
|
||||
|
||||
e ->
|
||||
e
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
This is called to register a fetch has returned.
|
||||
It expects the result data along with the UUID that was sent in the request
|
||||
|
||||
Will return the fetched object or :error
|
||||
"""
|
||||
def register_fetch_received(uuid, data) do
|
||||
case get_registry_data(uuid) do
|
||||
{:ok, %FetchRegistryData{received_at: nil} = reg_data} ->
|
||||
reg_data
|
||||
|> set_fetch_received(data)
|
||||
|> save_registry_data()
|
||||
|
||||
{:ok, %FetchRegistryData{} = reg_data} ->
|
||||
Logger.warn("tried to add fetched data twice - #{uuid}")
|
||||
reg_data
|
||||
|
||||
{:error, _} ->
|
||||
Logger.warn("Error adding fetch to registry - #{uuid}")
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
defp new_registry_data(json) do
|
||||
%FetchRegistryData{
|
||||
uuid: UUID.generate(),
|
||||
sent_json: json,
|
||||
sent_at: :erlang.monotonic_time(:millisecond)
|
||||
}
|
||||
end
|
||||
|
||||
defp get_registry_data(origin) do
|
||||
case Cachex.get(@fetches, origin) do
|
||||
{:ok, nil} ->
|
||||
{:error, :missing}
|
||||
|
||||
{:ok, reg_data} ->
|
||||
{:ok, reg_data}
|
||||
|
||||
_ ->
|
||||
{:error, :cache_error}
|
||||
end
|
||||
end
|
||||
|
||||
defp set_fetch_received(%FetchRegistryData{} = reg_data, data),
|
||||
do: %FetchRegistryData{
|
||||
reg_data
|
||||
| received_at: :erlang.monotonic_time(:millisecond),
|
||||
received_json: data
|
||||
}
|
||||
|
||||
defp save_registry_data(%FetchRegistryData{uuid: uuid} = reg_data) do
|
||||
{:ok, true} = Cachex.put(@fetches, uuid, reg_data)
|
||||
reg_data
|
||||
end
|
||||
|
||||
defp delete_registry_data(origin),
|
||||
do: {:ok, true} = Cachex.del(@fetches, origin)
|
||||
end
|
||||
88
lib/pleroma/web/fed_sockets/incoming_handler.ex
Normal file
88
lib/pleroma/web/fed_sockets/incoming_handler.ex
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.FedSockets.IncomingHandler do
|
||||
require Logger
|
||||
|
||||
alias Pleroma.Web.FedSockets.FedRegistry
|
||||
alias Pleroma.Web.FedSockets.FedSocket
|
||||
alias Pleroma.Web.FedSockets.SocketInfo
|
||||
|
||||
import HTTPSignatures, only: [validate_conn: 1, split_signature: 1]
|
||||
|
||||
@behaviour :cowboy_websocket
|
||||
|
||||
def init(req, state) do
|
||||
shake = FedSocket.shake()
|
||||
|
||||
with true <- Pleroma.Config.get([:fed_sockets, :enabled]),
|
||||
sec_protocol <- :cowboy_req.header("sec-websocket-protocol", req, nil),
|
||||
headers = %{"(request-target)" => ^shake} <- :cowboy_req.headers(req),
|
||||
true <- validate_conn(%{req_headers: headers}),
|
||||
%{"keyId" => origin} <- split_signature(headers["signature"]) do
|
||||
req =
|
||||
if is_nil(sec_protocol) do
|
||||
req
|
||||
else
|
||||
:cowboy_req.set_resp_header("sec-websocket-protocol", sec_protocol, req)
|
||||
end
|
||||
|
||||
{:cowboy_websocket, req, %{origin: origin}, %{}}
|
||||
else
|
||||
_ ->
|
||||
{:ok, req, state}
|
||||
end
|
||||
end
|
||||
|
||||
def websocket_init(%{origin: origin}) do
|
||||
case FedRegistry.add_fed_socket(origin) do
|
||||
{:ok, socket_info} ->
|
||||
{:ok, socket_info}
|
||||
|
||||
e ->
|
||||
Logger.error("FedSocket websocket_init failed - #{inspect(e)}")
|
||||
{:error, inspect(e)}
|
||||
end
|
||||
end
|
||||
|
||||
# Use the ping to check if the connection should be expired
|
||||
def websocket_handle(:ping, socket_info) do
|
||||
if SocketInfo.expired?(socket_info) do
|
||||
{:stop, socket_info}
|
||||
else
|
||||
{:ok, socket_info, :hibernate}
|
||||
end
|
||||
end
|
||||
|
||||
def websocket_handle({:text, data}, socket_info) do
|
||||
socket_info = SocketInfo.touch(socket_info)
|
||||
|
||||
case FedSocket.receive_package(socket_info, data) do
|
||||
{:noreply, _} ->
|
||||
{:ok, socket_info}
|
||||
|
||||
{:reply, reply} ->
|
||||
{:reply, {:text, Jason.encode!(reply)}, socket_info}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("incoming error - receive_package: #{inspect(reason)}")
|
||||
{:ok, socket_info}
|
||||
end
|
||||
end
|
||||
|
||||
def websocket_info({:send, message}, socket_info) do
|
||||
socket_info = SocketInfo.touch(socket_info)
|
||||
|
||||
{:reply, {:text, message}, socket_info}
|
||||
end
|
||||
|
||||
def websocket_info(:close, state) do
|
||||
{:stop, state}
|
||||
end
|
||||
|
||||
def websocket_info(message, state) do
|
||||
Logger.debug("#{__MODULE__} unknown message #{inspect(message)}")
|
||||
{:ok, state}
|
||||
end
|
||||
end
|
||||
33
lib/pleroma/web/fed_sockets/ingester_worker.ex
Normal file
33
lib/pleroma/web/fed_sockets/ingester_worker.ex
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.FedSockets.IngesterWorker do
|
||||
use Pleroma.Workers.WorkerHelper, queue: "ingestion_queue"
|
||||
require Logger
|
||||
|
||||
alias Pleroma.Web.Federator
|
||||
|
||||
@impl Oban.Worker
|
||||
def perform(%Job{args: %{"op" => "ingest", "object" => ingestee}}) do
|
||||
try do
|
||||
ingestee
|
||||
|> Jason.decode!()
|
||||
|> do_ingestion()
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("IngesterWorker error - #{inspect(e)}")
|
||||
e
|
||||
end
|
||||
end
|
||||
|
||||
defp do_ingestion(params) do
|
||||
case Federator.incoming_ap_doc(params) do
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
|
||||
{:ok, object} ->
|
||||
{:ok, object}
|
||||
end
|
||||
end
|
||||
end
|
||||
151
lib/pleroma/web/fed_sockets/outgoing_handler.ex
Normal file
151
lib/pleroma/web/fed_sockets/outgoing_handler.ex
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.FedSockets.OutgoingHandler do
|
||||
use GenServer
|
||||
|
||||
require Logger
|
||||
|
||||
alias Pleroma.Application
|
||||
alias Pleroma.Web.ActivityPub.InternalFetchActor
|
||||
alias Pleroma.Web.FedSockets
|
||||
alias Pleroma.Web.FedSockets.FedRegistry
|
||||
alias Pleroma.Web.FedSockets.FedSocket
|
||||
alias Pleroma.Web.FedSockets.SocketInfo
|
||||
|
||||
def start_link(uri) do
|
||||
GenServer.start_link(__MODULE__, %{uri: uri})
|
||||
end
|
||||
|
||||
def init(%{uri: uri}) do
|
||||
case initiate_connection(uri) do
|
||||
{:ok, ws_origin, conn_pid} ->
|
||||
FedRegistry.add_fed_socket(ws_origin, conn_pid)
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.debug("Outgoing connection failed - #{inspect(reason)}")
|
||||
:ignore
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info({:gun_ws, conn_pid, _ref, {:text, data}}, socket_info) do
|
||||
socket_info = SocketInfo.touch(socket_info)
|
||||
|
||||
case FedSocket.receive_package(socket_info, data) do
|
||||
{:noreply, _} ->
|
||||
{:noreply, socket_info}
|
||||
|
||||
{:reply, reply} ->
|
||||
:gun.ws_send(conn_pid, {:text, Jason.encode!(reply)})
|
||||
{:noreply, socket_info}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("incoming error - receive_package: #{inspect(reason)}")
|
||||
{:noreply, socket_info}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info(:close, state) do
|
||||
Logger.debug("Sending close frame !!!!!!!")
|
||||
{:close, state}
|
||||
end
|
||||
|
||||
def handle_info({:gun_down, _pid, _prot, :closed, _}, state) do
|
||||
{:stop, :normal, state}
|
||||
end
|
||||
|
||||
def handle_info({:send, data}, %{conn_pid: conn_pid} = socket_info) do
|
||||
socket_info = SocketInfo.touch(socket_info)
|
||||
:gun.ws_send(conn_pid, {:text, data})
|
||||
{:noreply, socket_info}
|
||||
end
|
||||
|
||||
def handle_info({:gun_ws, _, _, :pong}, state) do
|
||||
{:noreply, state, :hibernate}
|
||||
end
|
||||
|
||||
def handle_info(msg, state) do
|
||||
Logger.debug("#{__MODULE__} unhandled event #{inspect(msg)}")
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def terminate(reason, state) do
|
||||
Logger.debug(
|
||||
"#{__MODULE__} terminating outgoing connection for #{inspect(state)} for #{inspect(reason)}"
|
||||
)
|
||||
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
def initiate_connection(uri) do
|
||||
ws_uri =
|
||||
uri
|
||||
|> SocketInfo.origin()
|
||||
|> FedSockets.uri_for_origin()
|
||||
|
||||
%{host: host, port: port, path: path} = URI.parse(ws_uri)
|
||||
|
||||
with {:ok, conn_pid} <- :gun.open(to_charlist(host), port, %{protocols: [:http]}),
|
||||
{:ok, _} <- :gun.await_up(conn_pid),
|
||||
reference <-
|
||||
:gun.get(conn_pid, to_charlist(path), [
|
||||
{'user-agent', to_charlist(Application.user_agent())}
|
||||
]),
|
||||
{:response, :fin, 204, _} <- :gun.await(conn_pid, reference),
|
||||
headers <- build_headers(uri),
|
||||
ref <- :gun.ws_upgrade(conn_pid, to_charlist(path), headers, %{silence_pings: false}) do
|
||||
receive do
|
||||
{:gun_upgrade, ^conn_pid, ^ref, [<<"websocket">>], _} ->
|
||||
{:ok, ws_uri, conn_pid}
|
||||
after
|
||||
15_000 ->
|
||||
Logger.debug("Fedsocket timeout connecting to #{inspect(uri)}")
|
||||
{:error, :timeout}
|
||||
end
|
||||
else
|
||||
{:response, :nofin, 404, _} ->
|
||||
{:error, :fedsockets_not_supported}
|
||||
|
||||
e ->
|
||||
Logger.debug("Fedsocket error connecting to #{inspect(uri)}")
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
defp build_headers(uri) do
|
||||
host_for_sig = uri |> URI.parse() |> host_signature()
|
||||
|
||||
shake = FedSocket.shake()
|
||||
digest = "SHA-256=" <> (:crypto.hash(:sha256, shake) |> Base.encode64())
|
||||
date = Pleroma.Signature.signed_date()
|
||||
shake_size = byte_size(shake)
|
||||
|
||||
signature_opts = %{
|
||||
"(request-target)": shake,
|
||||
"content-length": to_charlist("#{shake_size}"),
|
||||
date: date,
|
||||
digest: digest,
|
||||
host: host_for_sig
|
||||
}
|
||||
|
||||
signature = Pleroma.Signature.sign(InternalFetchActor.get_actor(), signature_opts)
|
||||
|
||||
[
|
||||
{'signature', to_charlist(signature)},
|
||||
{'date', date},
|
||||
{'digest', to_charlist(digest)},
|
||||
{'content-length', to_charlist("#{shake_size}")},
|
||||
{to_charlist("(request-target)"), to_charlist(shake)},
|
||||
{'user-agent', to_charlist(Application.user_agent())}
|
||||
]
|
||||
end
|
||||
|
||||
defp host_signature(%{host: host, scheme: scheme, port: port}) do
|
||||
if port == URI.default_port(scheme) do
|
||||
host
|
||||
else
|
||||
"#{host}:#{port}"
|
||||
end
|
||||
end
|
||||
end
|
||||
52
lib/pleroma/web/fed_sockets/socket_info.ex
Normal file
52
lib/pleroma/web/fed_sockets/socket_info.ex
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.FedSockets.SocketInfo do
|
||||
defstruct origin: nil,
|
||||
pid: nil,
|
||||
conn_pid: nil,
|
||||
state: :default,
|
||||
connected_until: nil
|
||||
|
||||
alias Pleroma.Web.FedSockets.SocketInfo
|
||||
@default_connection_duration 15 * 60 * 1000
|
||||
|
||||
def build(uri, conn_pid \\ nil) do
|
||||
uri
|
||||
|> build_origin()
|
||||
|> build_pids(conn_pid)
|
||||
|> touch()
|
||||
end
|
||||
|
||||
def touch(%SocketInfo{} = socket_info),
|
||||
do: %{socket_info | connected_until: new_ttl()}
|
||||
|
||||
def connect(%SocketInfo{} = socket_info),
|
||||
do: %{socket_info | state: :connected}
|
||||
|
||||
def expired?(%{connected_until: connected_until}),
|
||||
do: connected_until < :erlang.monotonic_time(:millisecond)
|
||||
|
||||
def origin(uri),
|
||||
do: build_origin(uri).origin
|
||||
|
||||
defp build_pids(socket_info, conn_pid),
|
||||
do: struct(socket_info, pid: self(), conn_pid: conn_pid)
|
||||
|
||||
defp build_origin(uri) when is_binary(uri),
|
||||
do: uri |> URI.parse() |> build_origin
|
||||
|
||||
defp build_origin(%{host: host, port: nil, scheme: scheme}),
|
||||
do: build_origin(%{host: host, port: URI.default_port(scheme)})
|
||||
|
||||
defp build_origin(%{host: host, port: port}),
|
||||
do: %SocketInfo{origin: "#{host}:#{port}"}
|
||||
|
||||
defp new_ttl do
|
||||
connection_duration =
|
||||
Pleroma.Config.get([:fed_sockets, :connection_duration], @default_connection_duration)
|
||||
|
||||
:erlang.monotonic_time(:millisecond) + connection_duration
|
||||
end
|
||||
end
|
||||
59
lib/pleroma/web/fed_sockets/supervisor.ex
Normal file
59
lib/pleroma/web/fed_sockets/supervisor.ex
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.FedSockets.Supervisor do
|
||||
use Supervisor
|
||||
import Cachex.Spec
|
||||
|
||||
def start_link(opts) do
|
||||
Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
|
||||
end
|
||||
|
||||
def init(args) do
|
||||
children = [
|
||||
build_cache(:fed_socket_fetches, args),
|
||||
build_cache(:fed_socket_rejections, args),
|
||||
{Registry, keys: :unique, name: FedSockets.Registry, meta: [rejected: %{}]}
|
||||
]
|
||||
|
||||
opts = [strategy: :one_for_all, name: Pleroma.Web.Streamer.Supervisor]
|
||||
Supervisor.init(children, opts)
|
||||
end
|
||||
|
||||
defp build_cache(name, args) do
|
||||
opts = get_opts(name, args)
|
||||
|
||||
%{
|
||||
id: String.to_atom("#{name}_cache"),
|
||||
start: {Cachex, :start_link, [name, opts]},
|
||||
type: :worker
|
||||
}
|
||||
end
|
||||
|
||||
defp get_opts(cache_name, args)
|
||||
when cache_name in [:fed_socket_fetches, :fed_socket_rejections] do
|
||||
default = get_opts_or_config(args, cache_name, :default, 15_000)
|
||||
interval = get_opts_or_config(args, cache_name, :interval, 3_000)
|
||||
lazy = get_opts_or_config(args, cache_name, :lazy, false)
|
||||
|
||||
[expiration: expiration(default: default, interval: interval, lazy: lazy)]
|
||||
end
|
||||
|
||||
defp get_opts(name, args) do
|
||||
Keyword.get(args, name, [])
|
||||
end
|
||||
|
||||
defp get_opts_or_config(args, name, key, default) do
|
||||
args
|
||||
|> Keyword.get(name, [])
|
||||
|> Keyword.get(key)
|
||||
|> case do
|
||||
nil ->
|
||||
Pleroma.Config.get([:fed_sockets, name, key], default)
|
||||
|
||||
value ->
|
||||
value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -66,14 +66,17 @@ defmodule Pleroma.Web.Federator do
|
|||
def perform(:incoming_ap_doc, params) do
|
||||
Logger.debug("Handling incoming AP activity")
|
||||
|
||||
params = Utils.normalize_params(params)
|
||||
actor =
|
||||
params
|
||||
|> Map.get("actor")
|
||||
|> Utils.get_ap_id()
|
||||
|
||||
# NOTE: we use the actor ID to do the containment, this is fine because an
|
||||
# actor shouldn't be acting on objects outside their own AP server.
|
||||
with {:ok, _user} <- ap_enabled_actor(params["actor"]),
|
||||
with {_, {:ok, _user}} <- {:actor, ap_enabled_actor(actor)},
|
||||
nil <- Activity.normalize(params["id"]),
|
||||
{_, :ok} <-
|
||||
{:correct_origin?, Containment.contain_origin_from_id(params["actor"], params)},
|
||||
{:correct_origin?, Containment.contain_origin_from_id(actor, params)},
|
||||
{:ok, activity} <- Transmogrifier.handle_incoming(params) do
|
||||
{:ok, activity}
|
||||
else
|
||||
|
|
@ -85,10 +88,13 @@ defmodule Pleroma.Web.Federator do
|
|||
Logger.debug("Already had #{params["id"]}")
|
||||
{:error, :already_present}
|
||||
|
||||
{:actor, e} ->
|
||||
Logger.debug("Unhandled actor #{actor}, #{inspect(e)}")
|
||||
{:error, e}
|
||||
|
||||
e ->
|
||||
# Just drop those for now
|
||||
Logger.debug("Unhandled activity")
|
||||
Logger.debug(Jason.encode!(params, pretty: true))
|
||||
Logger.debug(fn -> "Unhandled activity\n" <> Jason.encode!(params, pretty: true) end)
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
62
lib/pleroma/web/instance_document.ex
Normal file
62
lib/pleroma/web/instance_document.ex
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.InstanceDocument do
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.Web.Endpoint
|
||||
|
||||
@instance_documents %{
|
||||
"terms-of-service" => "/static/terms-of-service.html",
|
||||
"instance-panel" => "/instance/panel.html"
|
||||
}
|
||||
|
||||
@spec get(String.t()) :: {:ok, String.t()} | {:error, atom()}
|
||||
def get(document_name) do
|
||||
case Map.fetch(@instance_documents, document_name) do
|
||||
{:ok, path} -> {:ok, path}
|
||||
_ -> {:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
@spec put(String.t(), String.t()) :: {:ok, String.t()} | {:error, atom()}
|
||||
def put(document_name, origin_path) do
|
||||
with {_, {:ok, destination_path}} <-
|
||||
{:instance_document, Map.fetch(@instance_documents, document_name)},
|
||||
:ok <- put_file(origin_path, destination_path) do
|
||||
{:ok, Path.join(Endpoint.url(), destination_path)}
|
||||
else
|
||||
{:instance_document, :error} -> {:error, :not_found}
|
||||
error -> error
|
||||
end
|
||||
end
|
||||
|
||||
@spec delete(String.t()) :: :ok | {:error, atom()}
|
||||
def delete(document_name) do
|
||||
with {_, {:ok, path}} <- {:instance_document, Map.fetch(@instance_documents, document_name)},
|
||||
instance_static_dir_path <- instance_static_dir(path),
|
||||
:ok <- File.rm(instance_static_dir_path) do
|
||||
:ok
|
||||
else
|
||||
{:instance_document, :error} -> {:error, :not_found}
|
||||
{:error, :enoent} -> {:error, :not_found}
|
||||
error -> error
|
||||
end
|
||||
end
|
||||
|
||||
defp put_file(origin_path, destination_path) do
|
||||
with destination <- instance_static_dir(destination_path),
|
||||
{_, :ok} <- {:mkdir_p, File.mkdir_p(Path.dirname(destination))},
|
||||
{_, {:ok, _}} <- {:copy, File.copy(origin_path, destination)} do
|
||||
:ok
|
||||
else
|
||||
{error, _} -> {:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
defp instance_static_dir(filename) do
|
||||
[:instance, :static_dir]
|
||||
|> Config.get!()
|
||||
|> Path.join(filename)
|
||||
end
|
||||
end
|
||||
|
|
@ -5,6 +5,8 @@
|
|||
defmodule Pleroma.Web.MastodonAPI.AuthController do
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
|
||||
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.OAuth.App
|
||||
alias Pleroma.Web.OAuth.Authorization
|
||||
|
|
@ -61,9 +63,7 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
|
|||
|
||||
TwitterAPI.password_reset(nickname_or_email)
|
||||
|
||||
conn
|
||||
|> put_status(:no_content)
|
||||
|> json("")
|
||||
json_response(conn, :no_content, "")
|
||||
end
|
||||
|
||||
defp local_mastodon_root_path(conn) do
|
||||
|
|
|
|||
|
|
@ -181,8 +181,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|
|||
user = User.sanitize_html(user, User.html_filter_policy(opts[:for]))
|
||||
display_name = user.name || user.nickname
|
||||
|
||||
image = User.avatar_url(user) |> MediaProxy.url()
|
||||
avatar = User.avatar_url(user) |> MediaProxy.url()
|
||||
avatar_static = User.avatar_url(user) |> MediaProxy.preview_url(static: true)
|
||||
header = User.banner_url(user) |> MediaProxy.url()
|
||||
header_static = User.banner_url(user) |> MediaProxy.preview_url(static: true)
|
||||
|
||||
following_count =
|
||||
if !user.hide_follows_count or !user.hide_follows or opts[:for] == user do
|
||||
|
|
@ -247,10 +249,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|
|||
statuses_count: user.note_count,
|
||||
note: user.bio,
|
||||
url: user.uri || user.ap_id,
|
||||
avatar: image,
|
||||
avatar_static: image,
|
||||
avatar: avatar,
|
||||
avatar_static: avatar_static,
|
||||
header: header,
|
||||
header_static: header,
|
||||
header_static: header_static,
|
||||
emojis: emojis,
|
||||
fields: user.fields,
|
||||
bot: bot,
|
||||
|
|
|
|||
|
|
@ -55,23 +55,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
end)
|
||||
end
|
||||
|
||||
def get_user(ap_id, fake_record_fallback \\ true) do
|
||||
cond do
|
||||
user = User.get_cached_by_ap_id(ap_id) ->
|
||||
user
|
||||
|
||||
user = User.get_by_guessed_nickname(ap_id) ->
|
||||
user
|
||||
|
||||
fake_record_fallback ->
|
||||
# TODO: refactor (fake records is never a good idea)
|
||||
User.error_user(ap_id)
|
||||
|
||||
true ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defp get_context_id(%{data: %{"context_id" => context_id}}) when not is_nil(context_id),
|
||||
do: context_id
|
||||
|
||||
|
|
@ -119,7 +102,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
# Note: unresolved users are filtered out
|
||||
actors =
|
||||
(activities ++ parent_activities)
|
||||
|> Enum.map(&get_user(&1.data["actor"], false))
|
||||
|> Enum.map(&CommonAPI.get_user(&1.data["actor"], false))
|
||||
|> Enum.filter(& &1)
|
||||
|
||||
UserRelationship.view_relationships_option(reading_user, actors, subset: :source_mutes)
|
||||
|
|
@ -138,7 +121,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
"show.json",
|
||||
%{activity: %{data: %{"type" => "Announce", "object" => _object}} = activity} = opts
|
||||
) do
|
||||
user = get_user(activity.data["actor"])
|
||||
user = CommonAPI.get_user(activity.data["actor"])
|
||||
created_at = Utils.to_masto_date(activity.data["published"])
|
||||
activity_object = Object.normalize(activity)
|
||||
|
||||
|
|
@ -211,7 +194,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
|
||||
object = Object.normalize(activity)
|
||||
|
||||
user = get_user(activity.data["actor"])
|
||||
user = CommonAPI.get_user(activity.data["actor"])
|
||||
user_follower_address = user.follower_address
|
||||
|
||||
like_count = object.data["like_count"] || 0
|
||||
|
|
@ -265,7 +248,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
|
||||
reply_to = get_reply_to(activity, opts)
|
||||
|
||||
reply_to_user = reply_to && get_user(reply_to.data["actor"])
|
||||
reply_to_user = reply_to && CommonAPI.get_user(reply_to.data["actor"])
|
||||
|
||||
content =
|
||||
object
|
||||
|
|
@ -432,6 +415,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
[attachment_url | _] = attachment["url"]
|
||||
media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
|
||||
href = attachment_url["href"] |> MediaProxy.url()
|
||||
href_preview = attachment_url["href"] |> MediaProxy.preview_url()
|
||||
|
||||
type =
|
||||
cond do
|
||||
|
|
@ -447,7 +431,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
id: to_string(attachment["id"] || hash_id),
|
||||
url: href,
|
||||
remote_url: href,
|
||||
preview_url: href,
|
||||
preview_url: href_preview,
|
||||
text_url: href,
|
||||
type: type,
|
||||
description: attachment["name"],
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
|
|||
with params <- Enum.into(:cow_qs.parse_qs(qs), %{}),
|
||||
sec_websocket <- :cowboy_req.header("sec-websocket-protocol", req, nil),
|
||||
access_token <- Map.get(params, "access_token"),
|
||||
{:ok, user} <- authenticate_request(access_token, sec_websocket),
|
||||
{:ok, topic} <- Streamer.get_topic(Map.get(params, "stream"), user, params) do
|
||||
{:ok, user, oauth_token} <- authenticate_request(access_token, sec_websocket),
|
||||
{:ok, topic} <- Streamer.get_topic(params["stream"], user, oauth_token, params) do
|
||||
req =
|
||||
if sec_websocket do
|
||||
:cowboy_req.set_resp_header("sec-websocket-protocol", sec_websocket, req)
|
||||
|
|
@ -117,7 +117,7 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
|
|||
|
||||
# Public streams without authentication.
|
||||
defp authenticate_request(nil, nil) do
|
||||
{:ok, nil}
|
||||
{:ok, nil, nil}
|
||||
end
|
||||
|
||||
# Authenticated streams.
|
||||
|
|
@ -125,9 +125,9 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
|
|||
token = access_token || sec_websocket
|
||||
|
||||
with true <- is_bitstring(token),
|
||||
%Token{user_id: user_id} <- Repo.get_by(Token, token: token),
|
||||
oauth_token = %Token{user_id: user_id} <- Repo.get_by(Token, token: token),
|
||||
user = %User{} <- User.get_cached_by_id(user_id) do
|
||||
{:ok, user}
|
||||
{:ok, user, oauth_token}
|
||||
else
|
||||
_ -> {:error, :unauthorized}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ defmodule Pleroma.Web.MediaProxy.Invalidation do
|
|||
def prepare_urls(urls) do
|
||||
urls
|
||||
|> List.wrap()
|
||||
|> Enum.map(&MediaProxy.url/1)
|
||||
|> Enum.map(fn url -> [MediaProxy.url(url), MediaProxy.preview_url(url)] end)
|
||||
|> List.flatten()
|
||||
|> Enum.uniq()
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
defmodule Pleroma.Web.MediaProxy do
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.Helpers.UriHelper
|
||||
alias Pleroma.Upload
|
||||
alias Pleroma.Web
|
||||
alias Pleroma.Web.MediaProxy.Invalidation
|
||||
|
|
@ -40,27 +41,35 @@ defmodule Pleroma.Web.MediaProxy do
|
|||
def url("/" <> _ = url), do: url
|
||||
|
||||
def url(url) do
|
||||
if disabled?() or not url_proxiable?(url) do
|
||||
url
|
||||
else
|
||||
if enabled?() and url_proxiable?(url) do
|
||||
encode_url(url)
|
||||
else
|
||||
url
|
||||
end
|
||||
end
|
||||
|
||||
@spec url_proxiable?(String.t()) :: boolean()
|
||||
def url_proxiable?(url) do
|
||||
if local?(url) or whitelisted?(url) do
|
||||
false
|
||||
not local?(url) and not whitelisted?(url)
|
||||
end
|
||||
|
||||
def preview_url(url, preview_params \\ []) do
|
||||
if preview_enabled?() do
|
||||
encode_preview_url(url, preview_params)
|
||||
else
|
||||
true
|
||||
url(url)
|
||||
end
|
||||
end
|
||||
|
||||
defp disabled?, do: !Config.get([:media_proxy, :enabled], false)
|
||||
def enabled?, do: Config.get([:media_proxy, :enabled], false)
|
||||
|
||||
defp local?(url), do: String.starts_with?(url, Pleroma.Web.base_url())
|
||||
# Note: media proxy must be enabled for media preview proxy in order to load all
|
||||
# non-local non-whitelisted URLs through it and be sure that body size constraint is preserved.
|
||||
def preview_enabled?, do: enabled?() and !!Config.get([:media_preview_proxy, :enabled])
|
||||
|
||||
defp whitelisted?(url) do
|
||||
def local?(url), do: String.starts_with?(url, Pleroma.Web.base_url())
|
||||
|
||||
def whitelisted?(url) do
|
||||
%{host: domain} = URI.parse(url)
|
||||
|
||||
mediaproxy_whitelist_domains =
|
||||
|
|
@ -85,17 +94,29 @@ defmodule Pleroma.Web.MediaProxy do
|
|||
|
||||
defp maybe_get_domain_from_url(domain), do: domain
|
||||
|
||||
def encode_url(url) do
|
||||
defp base64_sig64(url) do
|
||||
base64 = Base.url_encode64(url, @base64_opts)
|
||||
|
||||
sig64 =
|
||||
base64
|
||||
|> signed_url
|
||||
|> signed_url()
|
||||
|> Base.url_encode64(@base64_opts)
|
||||
|
||||
{base64, sig64}
|
||||
end
|
||||
|
||||
def encode_url(url) do
|
||||
{base64, sig64} = base64_sig64(url)
|
||||
|
||||
build_url(sig64, base64, filename(url))
|
||||
end
|
||||
|
||||
def encode_preview_url(url, preview_params \\ []) do
|
||||
{base64, sig64} = base64_sig64(url)
|
||||
|
||||
build_preview_url(sig64, base64, filename(url), preview_params)
|
||||
end
|
||||
|
||||
def decode_url(sig, url) do
|
||||
with {:ok, sig} <- Base.url_decode64(sig, @base64_opts),
|
||||
signature when signature == sig <- signed_url(url) do
|
||||
|
|
@ -113,10 +134,14 @@ defmodule Pleroma.Web.MediaProxy do
|
|||
if path = URI.parse(url_or_path).path, do: Path.basename(path)
|
||||
end
|
||||
|
||||
def build_url(sig_base64, url_base64, filename \\ nil) do
|
||||
def base_url do
|
||||
Config.get([:media_proxy, :base_url], Web.base_url())
|
||||
end
|
||||
|
||||
defp proxy_url(path, sig_base64, url_base64, filename) do
|
||||
[
|
||||
Config.get([:media_proxy, :base_url], Web.base_url()),
|
||||
"proxy",
|
||||
base_url(),
|
||||
path,
|
||||
sig_base64,
|
||||
url_base64,
|
||||
filename
|
||||
|
|
@ -124,4 +149,38 @@ defmodule Pleroma.Web.MediaProxy do
|
|||
|> Enum.filter(& &1)
|
||||
|> Path.join()
|
||||
end
|
||||
|
||||
def build_url(sig_base64, url_base64, filename \\ nil) do
|
||||
proxy_url("proxy", sig_base64, url_base64, filename)
|
||||
end
|
||||
|
||||
def build_preview_url(sig_base64, url_base64, filename \\ nil, preview_params \\ []) do
|
||||
uri = proxy_url("proxy/preview", sig_base64, url_base64, filename)
|
||||
|
||||
UriHelper.modify_uri_params(uri, preview_params)
|
||||
end
|
||||
|
||||
def verify_request_path_and_url(
|
||||
%Plug.Conn{params: %{"filename" => _}, request_path: request_path},
|
||||
url
|
||||
) do
|
||||
verify_request_path_and_url(request_path, url)
|
||||
end
|
||||
|
||||
def verify_request_path_and_url(request_path, url) when is_binary(request_path) do
|
||||
filename = filename(url)
|
||||
|
||||
if filename && not basename_matches?(request_path, filename) do
|
||||
{:wrong_filename, filename}
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
def verify_request_path_and_url(_, _), do: :ok
|
||||
|
||||
defp basename_matches?(path, filename) do
|
||||
basename = Path.basename(path)
|
||||
basename == filename or URI.decode(basename) == filename or URI.encode(basename) == filename
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,44 +5,201 @@
|
|||
defmodule Pleroma.Web.MediaProxy.MediaProxyController do
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.Helpers.MediaHelper
|
||||
alias Pleroma.Helpers.UriHelper
|
||||
alias Pleroma.ReverseProxy
|
||||
alias Pleroma.Web.MediaProxy
|
||||
alias Plug.Conn
|
||||
|
||||
@default_proxy_opts [max_body_length: 25 * 1_048_576, http: [follow_redirect: true]]
|
||||
|
||||
def remote(conn, %{"sig" => sig64, "url" => url64} = params) do
|
||||
with config <- Pleroma.Config.get([:media_proxy], []),
|
||||
true <- Keyword.get(config, :enabled, false),
|
||||
def remote(conn, %{"sig" => sig64, "url" => url64}) do
|
||||
with {_, true} <- {:enabled, MediaProxy.enabled?()},
|
||||
{:ok, url} <- MediaProxy.decode_url(sig64, url64),
|
||||
{_, false} <- {:in_banned_urls, MediaProxy.in_banned_urls(url)},
|
||||
:ok <- filename_matches(params, conn.request_path, url) do
|
||||
ReverseProxy.call(conn, url, Keyword.get(config, :proxy_opts, @default_proxy_opts))
|
||||
:ok <- MediaProxy.verify_request_path_and_url(conn, url) do
|
||||
ReverseProxy.call(conn, url, media_proxy_opts())
|
||||
else
|
||||
error when error in [false, {:in_banned_urls, true}] ->
|
||||
send_resp(conn, 404, Plug.Conn.Status.reason_phrase(404))
|
||||
{:enabled, false} ->
|
||||
send_resp(conn, 404, Conn.Status.reason_phrase(404))
|
||||
|
||||
{:in_banned_urls, true} ->
|
||||
send_resp(conn, 404, Conn.Status.reason_phrase(404))
|
||||
|
||||
{:error, :invalid_signature} ->
|
||||
send_resp(conn, 403, Plug.Conn.Status.reason_phrase(403))
|
||||
send_resp(conn, 403, Conn.Status.reason_phrase(403))
|
||||
|
||||
{:wrong_filename, filename} ->
|
||||
redirect(conn, external: MediaProxy.build_url(sig64, url64, filename))
|
||||
end
|
||||
end
|
||||
|
||||
def filename_matches(%{"filename" => _} = _, path, url) do
|
||||
filename = MediaProxy.filename(url)
|
||||
|
||||
if filename && does_not_match(path, filename) do
|
||||
{:wrong_filename, filename}
|
||||
def preview(%Conn{} = conn, %{"sig" => sig64, "url" => url64}) do
|
||||
with {_, true} <- {:enabled, MediaProxy.preview_enabled?()},
|
||||
{:ok, url} <- MediaProxy.decode_url(sig64, url64),
|
||||
:ok <- MediaProxy.verify_request_path_and_url(conn, url) do
|
||||
handle_preview(conn, url)
|
||||
else
|
||||
:ok
|
||||
{:enabled, false} ->
|
||||
send_resp(conn, 404, Conn.Status.reason_phrase(404))
|
||||
|
||||
{:error, :invalid_signature} ->
|
||||
send_resp(conn, 403, Conn.Status.reason_phrase(403))
|
||||
|
||||
{:wrong_filename, filename} ->
|
||||
redirect(conn, external: MediaProxy.build_preview_url(sig64, url64, filename))
|
||||
end
|
||||
end
|
||||
|
||||
def filename_matches(_, _, _), do: :ok
|
||||
defp handle_preview(conn, url) do
|
||||
media_proxy_url = MediaProxy.url(url)
|
||||
|
||||
defp does_not_match(path, filename) do
|
||||
basename = Path.basename(path)
|
||||
basename != filename and URI.decode(basename) != filename and URI.encode(basename) != filename
|
||||
with {:ok, %{status: status} = head_response} when status in 200..299 <-
|
||||
Pleroma.HTTP.request("head", media_proxy_url, [], [], pool: :media) do
|
||||
content_type = Tesla.get_header(head_response, "content-type")
|
||||
content_length = Tesla.get_header(head_response, "content-length")
|
||||
content_length = content_length && String.to_integer(content_length)
|
||||
static = conn.params["static"] in ["true", true]
|
||||
|
||||
cond do
|
||||
static and content_type == "image/gif" ->
|
||||
handle_jpeg_preview(conn, media_proxy_url)
|
||||
|
||||
static ->
|
||||
drop_static_param_and_redirect(conn)
|
||||
|
||||
content_type == "image/gif" ->
|
||||
redirect(conn, external: media_proxy_url)
|
||||
|
||||
min_content_length_for_preview() > 0 and content_length > 0 and
|
||||
content_length < min_content_length_for_preview() ->
|
||||
redirect(conn, external: media_proxy_url)
|
||||
|
||||
true ->
|
||||
handle_preview(content_type, conn, media_proxy_url)
|
||||
end
|
||||
else
|
||||
# If HEAD failed, redirecting to media proxy URI doesn't make much sense; returning an error
|
||||
{_, %{status: status}} ->
|
||||
send_resp(conn, :failed_dependency, "Can't fetch HTTP headers (HTTP #{status}).")
|
||||
|
||||
{:error, :recv_response_timeout} ->
|
||||
send_resp(conn, :failed_dependency, "HEAD request timeout.")
|
||||
|
||||
_ ->
|
||||
send_resp(conn, :failed_dependency, "Can't fetch HTTP headers.")
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_preview("image/png" <> _ = _content_type, conn, media_proxy_url) do
|
||||
handle_png_preview(conn, media_proxy_url)
|
||||
end
|
||||
|
||||
defp handle_preview("image/" <> _ = _content_type, conn, media_proxy_url) do
|
||||
handle_jpeg_preview(conn, media_proxy_url)
|
||||
end
|
||||
|
||||
defp handle_preview("video/" <> _ = _content_type, conn, media_proxy_url) do
|
||||
handle_video_preview(conn, media_proxy_url)
|
||||
end
|
||||
|
||||
defp handle_preview(_unsupported_content_type, conn, media_proxy_url) do
|
||||
fallback_on_preview_error(conn, media_proxy_url)
|
||||
end
|
||||
|
||||
defp handle_png_preview(conn, media_proxy_url) do
|
||||
quality = Config.get!([:media_preview_proxy, :image_quality])
|
||||
{thumbnail_max_width, thumbnail_max_height} = thumbnail_max_dimensions()
|
||||
|
||||
with {:ok, thumbnail_binary} <-
|
||||
MediaHelper.image_resize(
|
||||
media_proxy_url,
|
||||
%{
|
||||
max_width: thumbnail_max_width,
|
||||
max_height: thumbnail_max_height,
|
||||
quality: quality,
|
||||
format: "png"
|
||||
}
|
||||
) do
|
||||
conn
|
||||
|> put_preview_response_headers(["image/png", "preview.png"])
|
||||
|> send_resp(200, thumbnail_binary)
|
||||
else
|
||||
_ ->
|
||||
fallback_on_preview_error(conn, media_proxy_url)
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_jpeg_preview(conn, media_proxy_url) do
|
||||
quality = Config.get!([:media_preview_proxy, :image_quality])
|
||||
{thumbnail_max_width, thumbnail_max_height} = thumbnail_max_dimensions()
|
||||
|
||||
with {:ok, thumbnail_binary} <-
|
||||
MediaHelper.image_resize(
|
||||
media_proxy_url,
|
||||
%{max_width: thumbnail_max_width, max_height: thumbnail_max_height, quality: quality}
|
||||
) do
|
||||
conn
|
||||
|> put_preview_response_headers()
|
||||
|> send_resp(200, thumbnail_binary)
|
||||
else
|
||||
_ ->
|
||||
fallback_on_preview_error(conn, media_proxy_url)
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_video_preview(conn, media_proxy_url) do
|
||||
with {:ok, thumbnail_binary} <-
|
||||
MediaHelper.video_framegrab(media_proxy_url) do
|
||||
conn
|
||||
|> put_preview_response_headers()
|
||||
|> send_resp(200, thumbnail_binary)
|
||||
else
|
||||
_ ->
|
||||
fallback_on_preview_error(conn, media_proxy_url)
|
||||
end
|
||||
end
|
||||
|
||||
defp drop_static_param_and_redirect(conn) do
|
||||
uri_without_static_param =
|
||||
conn
|
||||
|> current_url()
|
||||
|> UriHelper.modify_uri_params(%{}, ["static"])
|
||||
|
||||
redirect(conn, external: uri_without_static_param)
|
||||
end
|
||||
|
||||
defp fallback_on_preview_error(conn, media_proxy_url) do
|
||||
redirect(conn, external: media_proxy_url)
|
||||
end
|
||||
|
||||
defp put_preview_response_headers(
|
||||
conn,
|
||||
[content_type, filename] = _content_info \\ ["image/jpeg", "preview.jpg"]
|
||||
) do
|
||||
conn
|
||||
|> put_resp_header("content-type", content_type)
|
||||
|> put_resp_header("content-disposition", "inline; filename=\"#{filename}\"")
|
||||
|> put_resp_header("cache-control", ReverseProxy.default_cache_control_header())
|
||||
end
|
||||
|
||||
defp thumbnail_max_dimensions do
|
||||
config = media_preview_proxy_config()
|
||||
|
||||
thumbnail_max_width = Keyword.fetch!(config, :thumbnail_max_width)
|
||||
thumbnail_max_height = Keyword.fetch!(config, :thumbnail_max_height)
|
||||
|
||||
{thumbnail_max_width, thumbnail_max_height}
|
||||
end
|
||||
|
||||
defp min_content_length_for_preview do
|
||||
Keyword.get(media_preview_proxy_config(), :min_content_length, 0)
|
||||
end
|
||||
|
||||
defp media_preview_proxy_config do
|
||||
Config.get!([:media_preview_proxy])
|
||||
end
|
||||
|
||||
defp media_proxy_opts do
|
||||
Config.get([:media_proxy, :proxy_opts], [])
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ defmodule Pleroma.Web.Metadata.Providers.RestrictIndexing do
|
|||
"""
|
||||
|
||||
@impl true
|
||||
def build_tags(%{user: %{local: false}}) do
|
||||
def build_tags(%{user: %{local: true, discoverable: true}}), do: []
|
||||
|
||||
def build_tags(_) do
|
||||
[
|
||||
{:meta,
|
||||
[
|
||||
|
|
@ -19,7 +21,4 @@ defmodule Pleroma.Web.Metadata.Providers.RestrictIndexing do
|
|||
], []}
|
||||
]
|
||||
end
|
||||
|
||||
@impl true
|
||||
def build_tags(%{user: %{local: true}}), do: []
|
||||
end
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ defmodule Pleroma.Web.Metadata.Utils do
|
|||
def scrub_html(content), do: content
|
||||
|
||||
def attachment_url(url) do
|
||||
MediaProxy.url(url)
|
||||
MediaProxy.preview_url(url)
|
||||
end
|
||||
|
||||
def user_name_string(user) do
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
|
|||
redirect_uri = redirect_uri(conn, redirect_uri)
|
||||
url_params = %{access_token: token.token}
|
||||
url_params = Maps.put_if_present(url_params, :state, params["state"])
|
||||
url = UriHelper.append_uri_params(redirect_uri, url_params)
|
||||
url = UriHelper.modify_uri_params(redirect_uri, url_params)
|
||||
redirect(conn, external: url)
|
||||
else
|
||||
conn
|
||||
|
|
@ -161,7 +161,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
|
|||
redirect_uri = redirect_uri(conn, redirect_uri)
|
||||
url_params = %{code: auth.token}
|
||||
url_params = Maps.put_if_present(url_params, :state, auth_attrs["state"])
|
||||
url = UriHelper.append_uri_params(redirect_uri, url_params)
|
||||
url = UriHelper.modify_uri_params(redirect_uri, url_params)
|
||||
redirect(conn, external: url)
|
||||
else
|
||||
conn
|
||||
|
|
|
|||
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