Merge remote-tracking branch 'upstream/develop' into restrict-origin

This commit is contained in:
Alex Gleason 2020-10-08 17:24:09 -05:00
commit 3f9263fb16
No known key found for this signature in database
GPG key ID: 7211D1F99744FBB7
663 changed files with 16301 additions and 12107 deletions

View file

@ -14,10 +14,11 @@ defmodule Mix.Pleroma do
:swoosh,
:timex
]
@cachex_children ["object", "user"]
@cachex_children ["object", "user", "scrubber"]
@doc "Common functions to be reused in mix tasks"
def start_pleroma do
Pleroma.Config.Holder.save_default()
Pleroma.Config.Oban.warn()
Application.put_env(:phoenix, :serve_endpoints, false, persistent: true)
if Pleroma.Config.get(:env) != :test do

View file

@ -91,20 +91,17 @@ defmodule Mix.Tasks.Pleroma.Benchmark do
"Without conn and without pool" => fn ->
{:ok, %Tesla.Env{}} =
Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500", [],
adapter: [pool: :no_pool, receive_conn: false]
pool: :no_pool,
receive_conn: false
)
end,
"Without conn and with pool" => fn ->
{:ok, %Tesla.Env{}} =
Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500", [],
adapter: [receive_conn: false]
)
Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500", [], receive_conn: false)
end,
"With reused conn and without pool" => fn ->
{:ok, %Tesla.Env{}} =
Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500", [],
adapter: [pool: :no_pool]
)
Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500", [], pool: :no_pool)
end,
"With reused conn and with pool" => fn ->
{:ok, %Tesla.Env{}} = Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500")

View file

@ -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)

View file

@ -10,6 +10,7 @@ defmodule Mix.Tasks.Pleroma.Database do
alias Pleroma.User
require Logger
require Pleroma.Constants
import Ecto.Query
import Mix.Pleroma
use Mix.Task
@ -53,8 +54,6 @@ defmodule Mix.Tasks.Pleroma.Database do
end
def run(["prune_objects" | args]) do
import Ecto.Query
{options, [], []} =
OptionParser.parse(
args,
@ -94,15 +93,13 @@ defmodule Mix.Tasks.Pleroma.Database do
end
def run(["fix_likes_collections"]) do
import Ecto.Query
start_pleroma()
from(object in Object,
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
@ -130,4 +127,38 @@ defmodule Mix.Tasks.Pleroma.Database do
Maintenance.vacuum(args)
end
def run(["ensure_expiration"]) do
start_pleroma()
days = Pleroma.Config.get([:mrf_activity_expiration, :days], 365)
Pleroma.Activity
|> join(:inner, [a], o in Object,
on:
fragment(
"(?->>'id') = COALESCE((?)->'object'->> 'id', (?)->>'object')",
o.data,
a.data,
a.data
)
)
|> where(local: true)
|> where([a], fragment("(? ->> 'type'::text) = 'Create'", a.data))
|> where([_a, o], fragment("?->>'type' = 'Note'", o.data))
|> Pleroma.Repo.chunk_stream(100, :batches)
|> Stream.each(fn activities ->
Enum.each(activities, fn activity ->
expires_at =
activity.inserted_at
|> DateTime.from_naive!("Etc/UTC")
|> Timex.shift(days: days)
Pleroma.Workers.PurgeExpiredActivity.enqueue(%{
activity_id: activity.id,
expires_at: expires_at
})
end)
end)
|> Stream.run()
end
end

View file

@ -41,6 +41,10 @@ defmodule Mix.Tasks.Pleroma.Ecto.Migrate do
load_pleroma()
{opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)
if Application.get_env(:pleroma, Pleroma.Repo)[:ssl] do
Application.ensure_all_started(:ssl)
end
opts =
if opts[:to] || opts[:step] || opts[:all],
do: opts,

View file

@ -40,6 +40,10 @@ defmodule Mix.Tasks.Pleroma.Ecto.Rollback do
load_pleroma()
{opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)
if Application.get_env(:pleroma, Pleroma.Repo)[:ssl] do
Application.ensure_all_started(:ssl)
end
opts =
if opts[:to] || opts[:step] || opts[:all],
do: opts,

View file

@ -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

View file

@ -15,7 +15,7 @@ defmodule Mix.Tasks.Pleroma.Emoji do
{options, [], []} = parse_global_opts(args)
url_or_path = options[:manifest] || default_manifest()
manifest = fetch_and_decode(url_or_path)
manifest = fetch_and_decode!(url_or_path)
Enum.each(manifest, fn {name, info} ->
to_print = [
@ -42,7 +42,7 @@ defmodule Mix.Tasks.Pleroma.Emoji do
url_or_path = options[:manifest] || default_manifest()
manifest = fetch_and_decode(url_or_path)
manifest = fetch_and_decode!(url_or_path)
for pack_name <- pack_names do
if Map.has_key?(manifest, pack_name) do
@ -92,7 +92,7 @@ defmodule Mix.Tasks.Pleroma.Emoji do
])
)
files = fetch_and_decode(files_loc)
files = fetch_and_decode!(files_loc)
IO.puts(IO.ANSI.format(["Unpacking ", :bright, pack_name]))
@ -183,7 +183,7 @@ defmodule Mix.Tasks.Pleroma.Emoji do
IO.puts("Downloading the pack and generating SHA256")
binary_archive = Tesla.get!(client(), src).body
{:ok, %{body: binary_archive}} = Pleroma.HTTP.get(src)
archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16()
IO.puts("SHA256 is #{archive_sha}")
@ -243,14 +243,16 @@ defmodule Mix.Tasks.Pleroma.Emoji do
IO.puts("Emoji packs have been reloaded.")
end
defp fetch_and_decode(from) do
defp fetch_and_decode!(from) do
with {:ok, json} <- fetch(from) do
Jason.decode!(json)
else
{:error, error} -> raise "#{from} cannot be fetched. Error: #{error} occur."
end
end
defp fetch("http" <> _ = from) do
with {:ok, %{body: body}} <- Tesla.get(client(), from) do
with {:ok, %{body: body}} <- Pleroma.HTTP.get(from) do
{:ok, body}
end
end
@ -269,13 +271,5 @@ defmodule Mix.Tasks.Pleroma.Emoji do
)
end
defp client do
middleware = [
{Tesla.Middleware.FollowRedirects, [max_redirects: 3]}
]
Tesla.client(middleware)
end
defp default_manifest, do: Pleroma.Config.get!([:emoji, :default_manifest])
end

View file

@ -0,0 +1,141 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Frontend do
use Mix.Task
import Mix.Pleroma
@shortdoc "Manages bundled Pleroma frontends"
@moduledoc File.read!("docs/administration/CLI_tasks/frontend.md")
def run(["install", "none" | _args]) do
shell_info("Skipping frontend installation because none was requested")
"none"
end
def run(["install", frontend | args]) do
log_level = Logger.level()
Logger.configure(level: :warn)
start_pleroma()
{options, [], []} =
OptionParser.parse(
args,
strict: [
ref: :string,
static_dir: :string,
build_url: :string,
build_dir: :string,
file: :string
]
)
instance_static_dir =
with nil <- options[:static_dir] do
Pleroma.Config.get!([:instance, :static_dir])
end
cmd_frontend_info = %{
"name" => frontend,
"ref" => options[:ref],
"build_url" => options[:build_url],
"build_dir" => options[:build_dir]
}
config_frontend_info = Pleroma.Config.get([:frontends, :available, frontend], %{})
frontend_info =
Map.merge(config_frontend_info, cmd_frontend_info, fn _key, config, cmd ->
# This only overrides things that are actually set
cmd || config
end)
ref = frontend_info["ref"]
unless ref do
raise "No ref given or configured"
end
dest =
Path.join([
instance_static_dir,
"frontends",
frontend,
ref
])
fe_label = "#{frontend} (#{ref})"
tmp_dir = Path.join([instance_static_dir, "frontends", "tmp"])
with {_, :ok} <-
{:download_or_unzip, download_or_unzip(frontend_info, tmp_dir, options[:file])},
shell_info("Installing #{fe_label} to #{dest}"),
:ok <- install_frontend(frontend_info, tmp_dir, dest) do
File.rm_rf!(tmp_dir)
shell_info("Frontend #{fe_label} installed to #{dest}")
Logger.configure(level: log_level)
else
{:download_or_unzip, _} ->
shell_info("Could not download or unzip the frontend")
_e ->
shell_info("Could not install the frontend")
end
end
defp download_or_unzip(frontend_info, temp_dir, file) do
if file do
with {:ok, zip} <- File.read(Path.expand(file)) do
unzip(zip, temp_dir)
end
else
download_build(frontend_info, temp_dir)
end
end
def unzip(zip, dest) do
with {:ok, unzipped} <- :zip.unzip(zip, [:memory]) do
File.rm_rf!(dest)
File.mkdir_p!(dest)
Enum.each(unzipped, fn {filename, data} ->
path = filename
new_file_path = Path.join(dest, path)
new_file_path
|> Path.dirname()
|> File.mkdir_p!()
File.write!(new_file_path, data)
end)
:ok
end
end
defp download_build(frontend_info, dest) do
shell_info("Downloading pre-built bundle for #{frontend_info["name"]}")
url = String.replace(frontend_info["build_url"], "${ref}", frontend_info["ref"])
with {:ok, %{status: 200, body: zip_body}} <-
Pleroma.HTTP.get(url, [], pool: :media, recv_timeout: 120_000) do
unzip(zip_body, dest)
else
e -> {:error, e}
end
end
defp install_frontend(frontend_info, source, dest) do
from = frontend_info["build_dir"] || "dist"
File.rm_rf!(dest)
File.mkdir_p!(dest)
File.cp_r!(Path.join([source, from]), dest)
:ok
end
end

View file

@ -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
@ -35,10 +44,16 @@ defmodule Mix.Tasks.Pleroma.Relay do
def run(["list"]) do
start_pleroma()
with {:ok, list} <- Relay.list(true) do
list |> Enum.each(&shell_info(&1))
with {:ok, list} <- Relay.list() do
Enum.each(list, &print_relay_url/1)
else
{:error, e} -> shell_error("Error while fetching relay subscription list: #{inspect(e)}")
end
end
defp print_relay_url(%{followed_back: false} = relay) do
shell_info("#{relay.actor} - no Accept received (relay didn't follow back)")
end
defp print_relay_url(relay), do: shell_info(relay.actor)
end

View file

@ -1,76 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.ReleaseEnv do
use Mix.Task
import Mix.Pleroma
@shortdoc "Generate Pleroma environment file."
@moduledoc File.read!("docs/administration/CLI_tasks/release_environments.md")
def run(["gen" | rest]) do
{options, [], []} =
OptionParser.parse(
rest,
strict: [
force: :boolean,
path: :string
],
aliases: [
p: :path,
f: :force
]
)
file_path =
get_option(
options,
:path,
"Environment file path",
"./config/pleroma.env"
)
env_path = Path.expand(file_path)
proceed? =
if File.exists?(env_path) do
get_option(
options,
:force,
"Environment file already exists. Do you want to overwrite the #{env_path} file? (y/n)",
"n"
) === "y"
else
true
end
if proceed? do
case do_generate(env_path) do
{:error, reason} ->
shell_error(
File.Error.message(%{action: "write to file", reason: reason, path: env_path})
)
_ ->
shell_info("\nThe file generated: #{env_path}.\n")
shell_info("""
WARNING: before start pleroma app please make sure to make the file read-only and non-modifiable.
Example:
chmod 0444 #{file_path}
chattr +i #{file_path}
""")
end
else
shell_info("\nThe file is exist. #{env_path}.\n")
end
end
def do_generate(path) do
content = "RELEASE_COOKIE=#{Base.encode32(:crypto.strong_rand_bytes(32))}"
File.mkdir_p!(Path.dirname(path))
File.write(path, content)
end
end

View file

@ -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

View file

@ -7,7 +7,6 @@ defmodule Pleroma.Activity do
alias Pleroma.Activity
alias Pleroma.Activity.Queries
alias Pleroma.ActivityExpiration
alias Pleroma.Bookmark
alias Pleroma.Notification
alias Pleroma.Object
@ -60,8 +59,6 @@ defmodule Pleroma.Activity do
# typical case.
has_one(:object, Object, on_delete: :nothing, foreign_key: :id)
has_one(:expiration, ActivityExpiration, on_delete: :delete_all)
timestamps()
end
@ -304,14 +301,14 @@ defmodule Pleroma.Activity do
|> Repo.all()
end
def follow_requests_for_actor(%Pleroma.User{ap_id: ap_id}) do
def follow_requests_for_actor(%User{ap_id: ap_id}) do
ap_id
|> Queries.by_object_id()
|> Queries.by_type("Follow")
|> where([a], fragment("? ->> 'state' = 'pending'", a.data))
end
def following_requests_for_actor(%Pleroma.User{ap_id: ap_id}) do
def following_requests_for_actor(%User{ap_id: ap_id}) do
Queries.by_type("Follow")
|> where([a], fragment("?->>'state' = 'pending'", a.data))
|> where([a], a.actor == ^ap_id)
@ -340,4 +337,10 @@ defmodule Pleroma.Activity do
_ -> nil
end
end
@spec pinned_by_actor?(Activity.t()) :: boolean()
def pinned_by_actor?(%Activity{} = activity) do
actor = user_actor(activity)
activity.id in actor.pinned_activities
end
end

View file

@ -1,67 +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.ActivityExpiration do
use Ecto.Schema
alias Pleroma.Activity
alias Pleroma.ActivityExpiration
alias Pleroma.Repo
import Ecto.Changeset
import Ecto.Query
@type t :: %__MODULE__{}
@min_activity_lifetime :timer.hours(1)
schema "activity_expirations" do
belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
field(:scheduled_at, :naive_datetime)
end
def changeset(%ActivityExpiration{} = expiration, attrs) do
expiration
|> cast(attrs, [:scheduled_at])
|> validate_required([:scheduled_at])
|> validate_scheduled_at()
end
def get_by_activity_id(activity_id) do
ActivityExpiration
|> where([exp], exp.activity_id == ^activity_id)
|> Repo.one()
end
def create(%Activity{} = activity, scheduled_at) do
%ActivityExpiration{activity_id: activity.id}
|> changeset(%{scheduled_at: scheduled_at})
|> Repo.insert()
end
def due_expirations(offset \\ 0) do
naive_datetime =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(offset, :millisecond)
ActivityExpiration
|> where([exp], exp.scheduled_at < ^naive_datetime)
|> Repo.all()
end
def validate_scheduled_at(changeset) do
validate_change(changeset, :scheduled_at, fn _, scheduled_at ->
if not expires_late_enough?(scheduled_at) do
[scheduled_at: "an ephemeral activity must live for at least one hour"]
else
[]
end
end)
end
def expires_late_enough?(scheduled_at) do
now = NaiveDateTime.utc_now()
diff = NaiveDateTime.diff(scheduled_at, now, :millisecond)
diff > @min_activity_lifetime
end
end

View file

@ -22,13 +22,18 @@ defmodule Pleroma.Application do
def repository, do: @repository
def user_agent do
case Config.get([:http, :user_agent], :default) do
:default ->
info = "#{Pleroma.Web.base_url()} <#{Config.get([:instance, :email], "")}>"
named_version() <> "; " <> info
if Process.whereis(Pleroma.Web.Endpoint) do
case Config.get([:http, :user_agent], :default) do
:default ->
info = "#{Pleroma.Web.base_url()} <#{Config.get([:instance, :email], "")}>"
named_version() <> "; " <> info
custom ->
custom
custom ->
custom
end
else
# fallback, if endpoint is not started yet
"Pleroma Data Loader"
end
end
@ -39,9 +44,13 @@ defmodule Pleroma.Application do
# every time the application is restarted, so we disable module
# conflicts at runtime
Code.compiler_options(ignore_module_conflict: true)
# Disable warnings_as_errors at runtime, it breaks Phoenix live reload
# due to protocol consolidation warnings
Code.compiler_options(warnings_as_errors: false)
Pleroma.Telemetry.Logger.attach()
Config.Holder.save_default()
Pleroma.HTML.compile_scrubbers()
Pleroma.Config.Oban.warn()
Config.DeprecationWarnings.warn()
Pleroma.Plugs.HTTPSecurityPlug.warn_if_disabled()
Pleroma.ApplicationRequirements.verify!()
@ -89,7 +98,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,
@ -178,16 +187,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

View file

@ -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

View file

@ -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

View file

@ -81,6 +81,16 @@ defmodule Pleroma.Config do
Application.delete_env(:pleroma, key)
end
def restrict_unauthenticated_access?(resource, kind) do
setting = get([:restrict_unauthenticated, resource, kind])
if setting in [nil, :if_instance_is_private] do
!get!([:instance, :public])
else
setting
end
end
def oauth_consumer_strategies, do: get([:auth, :oauth_consumer_strategies], [])
def oauth_consumer_enabled?, do: oauth_consumer_strategies() != []

View file

@ -8,7 +8,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
require Logger
alias Pleroma.Config
@type config_namespace() :: [atom()]
@type config_namespace() :: atom() | [atom()]
@type config_map() :: {config_namespace(), config_namespace(), String.t()}
@mrf_config_map [
@ -26,36 +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()
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
@ -68,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
@ -99,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
@ -113,6 +109,71 @@ 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
def check_gun_pool_options do
pool_config = Config.get(:connections_pool)
if timeout = pool_config[:await_up_timeout] do
Logger.warn("""
!!!DEPRECATION WARNING!!!
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))
end
pools_configs = Config.get(:pools)
warning_preface = """
!!!DEPRECATION WARNING!!!
Your config is using old setting name `timeout` instead of `recv_timeout` in pool settings. Setting should work for now, but you are advised to change format to scheme with port to prevent possible issues later.
"""
updated_config =
Enum.reduce(pools_configs, [], fn {pool_name, config}, acc ->
if timeout = config[:timeout] do
Keyword.put(acc, pool_name, Keyword.put_new(config, :recv_timeout, timeout))
else
acc
end
end)
if updated_config != [] do
pool_warnings =
updated_config
|> Keyword.keys()
|> Enum.map(fn pool_name ->
"\n* `:timeout` options in #{pool_name} pool is now `:recv_timeout`"
end)
Logger.warn(Enum.join([warning_preface | pool_warnings]))
Config.put(:pools, updated_config)
:error
else
:ok
end
end
@spec check_activity_expiration_config() :: :ok | nil
def check_activity_expiration_config do
warning_preface = """
!!!DEPRECATION WARNING!!!
Your config is using old namespace for activity expiration configuration. Setting should work for now, but you are advised to change to new namespace to prevent possible issues later:
"""
move_namespace_and_warn(
[
{Pleroma.ActivityExpiration, Pleroma.Workers.PurgeExpiredActivity,
"\n* `config :pleroma, Pleroma.ActivityExpiration` is now `config :pleroma, Pleroma.Workers.PurgeExpiredActivity`"}
],
warning_preface
)
end
end

View file

@ -0,0 +1,34 @@
defmodule Pleroma.Config.Oban do
require Logger
def warn do
oban_config = Pleroma.Config.get(Oban)
crontab =
[
Pleroma.Workers.Cron.StatsWorker,
Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker,
Pleroma.Workers.Cron.ClearOauthTokenWorker
]
|> Enum.reduce(oban_config[:crontab], fn removed_worker, acc ->
with acc when is_list(acc) <- acc,
setting when is_tuple(setting) <-
Enum.find(acc, fn {_, worker} -> worker == removed_worker end) do
"""
!!!OBAN CONFIG WARNING!!!
You are using old workers in Oban crontab settings, which were removed.
Please, remove setting from crontab in your config file (prod.secret.exs): #{
inspect(setting)
}
"""
|> Logger.warn()
List.delete(acc, setting)
else
_ -> acc
end
end)
Pleroma.Config.put(Oban, Keyword.put(oban_config, :crontab, crontab))
end
end

View file

@ -0,0 +1,34 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.Emoji do
use Ecto.Type
def type, do: :map
def cast(data) when is_map(data) do
has_invalid_emoji? =
Enum.find(data, fn
{name, uri} when is_binary(name) and is_binary(uri) ->
# based on ObjectValidators.Uri.cast()
case URI.parse(uri) do
%URI{host: nil} -> true
%URI{host: ""} -> true
%URI{scheme: scheme} when scheme in ["https", "http"] -> false
_ -> true
end
{_name, _uri} ->
true
end)
if has_invalid_emoji?, do: :error, else: {:ok, data}
end
def cast(_data), do: :error
def dump(data), do: {:ok, data}
def load(data), do: {:ok, data}
end

View file

@ -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}

View file

@ -107,25 +107,34 @@ defmodule Pleroma.Emails.UserEmail do
|> Enum.filter(&(&1.activity.data["type"] == "Create"))
|> Enum.map(fn notification ->
object = Pleroma.Object.normalize(notification.activity)
object = update_in(object.data["content"], &format_links/1)
%{
data: notification,
object: object,
from: User.get_by_ap_id(notification.activity.actor)
}
if not is_nil(object) do
object = update_in(object.data["content"], &format_links/1)
%{
data: notification,
object: object,
from: User.get_by_ap_id(notification.activity.actor)
}
end
end)
|> Enum.filter(& &1)
followers =
notifications
|> Enum.filter(&(&1.activity.data["type"] == "Follow"))
|> Enum.map(fn notification ->
%{
data: notification,
object: Pleroma.Object.normalize(notification.activity),
from: User.get_by_ap_id(notification.activity.actor)
}
from = User.get_by_ap_id(notification.activity.actor)
if not is_nil(from) do
%{
data: notification,
object: Pleroma.Object.normalize(notification.activity),
from: User.get_by_ap_id(notification.activity.actor)
}
end
end)
|> Enum.filter(& &1)
unless Enum.empty?(mentions) do
styling = Config.get([__MODULE__, :styling])

View file

@ -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

View file

@ -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) ->

View file

@ -264,4 +264,12 @@ defmodule Pleroma.FollowingRelationship do
end
end)
end
@spec following_ap_ids(User.t()) :: [String.t()]
def following_ap_ids(%User{} = user) do
user
|> following_query()
|> select([r, u], u.ap_id)
|> Repo.all()
end
end

View file

@ -13,7 +13,7 @@ defmodule Pleroma.Gun.Conn do
opts =
opts
|> Enum.into(%{})
|> Map.put_new(:await_up_timeout, pool_opts[:await_up_timeout] || 5_000)
|> Map.put_new(:connect_timeout, pool_opts[:connect_timeout] || 5_000)
|> Map.put_new(:supervise, false)
|> maybe_add_tls_opts(uri)
@ -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[:await_up_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[:await_up_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[:await_up_timeout]) do
{:ok, conn}
{:ok, protocol} <- Gun.await_up(conn, opts[:connect_timeout]) do
{:ok, conn, protocol}
else
error ->
Logger.warn(

View file

@ -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,7 +93,8 @@ defmodule Pleroma.Gun.ConnectionPool.Worker do
end)
{ref, state} = pop_in(state.client_monitors[client_pid])
Process.demonitor(ref)
Process.demonitor(ref, [:flush])
timer =
if used_by == [] do
@ -103,22 +114,27 @@ defmodule Pleroma.Gun.ConnectionPool.Worker do
{:stop, :normal, state}
end
@impl true
def handle_info({:gun_up, _pid, _protocol}, state) do
{:noreply, state, :hibernate}
end
# Gracefully shutdown if the connection got closed without any streams left
@impl true
def handle_info({:gun_down, _pid, _protocol, _reason, []}, state) do
{:stop, :normal, state}
end
# Otherwise, shutdown with an error
# Otherwise, wait for retry
@impl true
def handle_info({:gun_down, _pid, _protocol, _reason, _killed_streams} = down_message, state) do
{:stop, {:error, down_message}, state}
def handle_info({:gun_down, _pid, _protocol, _reason, _killed_streams}, state) do
{:noreply, state, :hibernate}
end
@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}
)

View 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

View 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

View file

@ -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])

View file

@ -100,20 +100,27 @@ defmodule Pleroma.HTML do
end)
end
def extract_first_external_url(_, nil), do: {:error, "No content"}
def extract_first_external_url_from_object(%{data: %{"content" => content}} = object)
when is_binary(content) do
unless object.data["fake"] do
key = "URL|#{object.id}"
def extract_first_external_url(object, content) do
key = "URL|#{object.id}"
Cachex.fetch!(:scrubber_cache, key, fn _key ->
{:commit, {:ok, extract_first_external_url(content)}}
end)
else
{:ok, extract_first_external_url(content)}
end
end
Cachex.fetch!(:scrubber_cache, key, fn _key ->
result =
content
|> Floki.parse_fragment!()
|> Floki.filter_out("a.mention,a.hashtag,a.attachment,a[rel~=\"tag\"]")
|> Floki.attribute("a", "href")
|> Enum.at(0)
def extract_first_external_url_from_object(_), do: {:error, :no_content}
{:commit, {:ok, result}}
end)
def extract_first_external_url(content) do
content
|> Floki.parse_fragment!()
|> Floki.find("a:not(.mention,.hashtag,.attachment,[rel~=\"tag\"])")
|> Enum.take(1)
|> Floki.attribute("href")
|> Enum.at(0)
end
end

View file

@ -6,12 +6,11 @@ defmodule Pleroma.HTTP.AdapterHelper do
@moduledoc """
Configure Tesla.Client with default and customized adapter options.
"""
@defaults [pool: :federation]
@defaults [pool: :federation, connect_timeout: 5_000, recv_timeout: 5_000]
@type proxy_type() :: :socks4 | :socks5
@type host() :: charlist() | :inet.ip_address()
alias Pleroma.Config
alias Pleroma.HTTP.AdapterHelper
require Logger
@ -20,7 +19,6 @@ defmodule Pleroma.HTTP.AdapterHelper do
| {Connection.proxy_type(), Connection.host(), pos_integer()}
@callback options(keyword(), URI.t()) :: keyword()
@callback get_conn(URI.t(), keyword()) :: {:ok, term()} | {:error, term()}
@spec format_proxy(String.t() | tuple() | nil) :: proxy() | nil
def format_proxy(nil), do: nil
@ -44,27 +42,10 @@ defmodule Pleroma.HTTP.AdapterHelper do
@spec options(URI.t(), keyword()) :: keyword()
def options(%URI{} = uri, opts \\ []) do
@defaults
|> put_timeout()
|> Keyword.merge(opts)
|> adapter_helper().options(uri)
end
# For Hackney, this is the time a connection can stay idle in the pool.
# For Gun, this is the timeout to receive a message from Gun.
defp put_timeout(opts) do
{config_key, default} =
if adapter() == Tesla.Adapter.Gun do
{:pools, Config.get([:pools, :default, :timeout], 5_000)}
else
{:hackney_pools, 10_000}
end
timeout = Config.get([config_key, opts[:pool], :timeout], default)
Keyword.merge(opts, timeout: timeout)
end
def get_conn(uri, opts), do: adapter_helper().get_conn(uri, opts)
defp adapter, do: Application.get_env(:tesla, :adapter)
defp adapter_helper do

View file

@ -5,57 +5,62 @@
defmodule Pleroma.HTTP.AdapterHelper.Gun do
@behaviour Pleroma.HTTP.AdapterHelper
alias Pleroma.Gun.ConnectionPool
alias Pleroma.Config
alias Pleroma.HTTP.AdapterHelper
require Logger
@defaults [
connect_timeout: 5_000,
domain_lookup_timeout: 5_000,
tls_handshake_timeout: 5_000,
retry: 0,
retry_timeout: 1000,
await_up_timeout: 5_000
retry: 1,
retry_timeout: 1_000
]
@type pool() :: :federation | :upload | :media | :default
@spec options(keyword(), URI.t()) :: keyword()
def options(incoming_opts \\ [], %URI{} = uri) do
proxy =
Pleroma.Config.get([:http, :proxy_url])
[:http, :proxy_url]
|> Config.get()
|> AdapterHelper.format_proxy()
config_opts = Pleroma.Config.get([:http, :adapter], [])
config_opts = Config.get([:http, :adapter], [])
@defaults
|> Keyword.merge(config_opts)
|> add_scheme_opts(uri)
|> AdapterHelper.maybe_add_proxy(proxy)
|> Keyword.merge(incoming_opts)
|> put_timeout()
end
defp add_scheme_opts(opts, %{scheme: "http"}), do: opts
defp add_scheme_opts(opts, %{scheme: "https"}) do
opts
|> Keyword.put(:certificates_verification, true)
Keyword.put(opts, :certificates_verification, true)
end
@spec get_conn(URI.t(), keyword()) :: {:ok, keyword()} | {:error, atom()}
def get_conn(uri, opts) do
case ConnectionPool.get_conn(uri, opts) do
{:ok, conn_pid} -> {:ok, Keyword.merge(opts, conn: conn_pid, close_conn: false)}
err -> err
end
defp put_timeout(opts) do
{recv_timeout, opts} = Keyword.pop(opts, :recv_timeout, pool_timeout(opts[:pool]))
# this is the timeout to receive a message from Gun
# `:timeout` key is used in Tesla
Keyword.put(opts, :timeout, recv_timeout)
end
@spec pool_timeout(pool()) :: non_neg_integer()
def pool_timeout(pool) do
default = Config.get([:pools, :default, :recv_timeout], 5_000)
Config.get([:pools, pool, :recv_timeout], default)
end
@prefix Pleroma.Gun.ConnectionPool
def limiter_setup do
wait = Pleroma.Config.get([:connections_pool, :connection_acquisition_wait])
retries = Pleroma.Config.get([:connections_pool, :connection_acquisition_retries])
wait = Config.get([:connections_pool, :connection_acquisition_wait])
retries = Config.get([:connections_pool, :connection_acquisition_retries])
:pools
|> Pleroma.Config.get([])
|> Config.get([])
|> Enum.each(fn {name, opts} ->
max_running = Keyword.get(opts, :size, 50)
max_waiting = Keyword.get(opts, :max_waiting, 10)
@ -69,7 +74,6 @@ defmodule Pleroma.HTTP.AdapterHelper.Gun do
case result do
:ok -> :ok
{:error, :existing} -> :ok
e -> raise e
end
end)

View file

@ -2,11 +2,8 @@ defmodule Pleroma.HTTP.AdapterHelper.Hackney do
@behaviour Pleroma.HTTP.AdapterHelper
@defaults [
connect_timeout: 10_000,
recv_timeout: 20_000,
follow_redirect: true,
force_redirect: true,
pool: :federation
force_redirect: true
]
@spec options(keyword(), URI.t()) :: keyword()
@ -19,11 +16,21 @@ defmodule Pleroma.HTTP.AdapterHelper.Hackney do
|> Keyword.merge(config_opts)
|> Keyword.merge(connection_opts)
|> add_scheme_opts(uri)
|> maybe_add_with_body()
|> Pleroma.HTTP.AdapterHelper.maybe_add_proxy(proxy)
end
defp add_scheme_opts(opts, %URI{scheme: "https"}) do
Keyword.put(opts, :ssl_options, versions: [:"tlsv1.2", :"tlsv1.1", :tlsv1])
end
defp add_scheme_opts(opts, _), do: opts
@spec get_conn(URI.t(), keyword()) :: {:ok, keyword()}
def get_conn(_uri, opts), do: {:ok, opts}
defp maybe_add_with_body(opts) do
if opts[:max_body] do
Keyword.put(opts, :with_body, true)
else
opts
end
end
end

View file

@ -11,6 +11,8 @@ defmodule Pleroma.HTTP.ExAws do
@impl true
def request(method, url, body \\ "", headers \\ [], http_opts \\ []) do
http_opts = Keyword.put_new(http_opts, :pool, :upload)
case HTTP.request(method, url, body, headers, http_opts) do
{:ok, env} ->
{:ok, %{status_code: env.status, headers: env.headers, body: env.body}}

View file

@ -60,30 +60,23 @@ defmodule Pleroma.HTTP do
{:ok, Env.t()} | {:error, any()}
def request(method, url, body, headers, options) when is_binary(url) do
uri = URI.parse(url)
adapter_opts = AdapterHelper.options(uri, options[:adapter] || [])
adapter_opts = AdapterHelper.options(uri, options || [])
case AdapterHelper.get_conn(uri, adapter_opts) do
{:ok, adapter_opts} ->
options = put_in(options[:adapter], adapter_opts)
params = options[:params] || []
request = build_request(method, headers, options, url, body, params)
options = put_in(options[:adapter], adapter_opts)
params = options[:params] || []
request = build_request(method, headers, options, url, body, params)
adapter = Application.get_env(:tesla, :adapter)
adapter = Application.get_env(:tesla, :adapter)
client = Tesla.client(adapter_middlewares(adapter), adapter)
client = Tesla.client(adapter_middlewares(adapter), adapter)
maybe_limit(
fn ->
request(client, request)
end,
adapter,
adapter_opts
)
# Connection release is handled in a custom FollowRedirects middleware
err ->
err
end
maybe_limit(
fn ->
request(client, request)
end,
adapter,
adapter_opts
)
end
@spec request(Client.t(), keyword()) :: {:ok, Env.t()} | {:error, any()}
@ -110,7 +103,7 @@ defmodule Pleroma.HTTP do
end
defp adapter_middlewares(Tesla.Adapter.Gun) do
[Pleroma.HTTP.Middleware.FollowRedirects]
[Tesla.Middleware.FollowRedirects, Pleroma.Tesla.Middleware.ConnectionPool]
end
defp adapter_middlewares(_), do: []

View file

@ -11,6 +11,8 @@ defmodule Pleroma.HTTP.Tzdata do
@impl true
def get(url, headers, options) do
options = Keyword.put_new(options, :pool, :default)
with {:ok, %Tesla.Env{} = env} <- HTTP.get(url, headers, options) do
{:ok, {env.status, env.headers, env.body}}
end
@ -18,6 +20,8 @@ defmodule Pleroma.HTTP.Tzdata do
@impl true
def head(url, headers, options) do
options = Keyword.put_new(options, :pool, :default)
with {:ok, %Tesla.Env{} = env} <- HTTP.head(url, headers, options) do
{:ok, {env.status, env.headers}}
end

View 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

View file

@ -14,6 +14,8 @@ defmodule Pleroma.Instances.Instance do
import Ecto.Query
import Ecto.Changeset
require Logger
schema "instances" do
field(:host, :string)
field(:unreachable_since, :naive_datetime_usec)
@ -145,25 +147,32 @@ defmodule Pleroma.Instances.Instance do
favicon
end
rescue
e ->
Logger.warn("Instance.get_or_update_favicon(\"#{host}\") error: #{inspect(e)}")
nil
end
defp scrape_favicon(%URI{} = instance_uri) do
try do
with {:ok, %Tesla.Env{body: html}} <-
Pleroma.HTTP.get(to_string(instance_uri), [{:Accept, "text/html"}]),
favicon_rel <-
html
|> Floki.parse_document!()
|> Floki.attribute("link[rel=icon]", "href")
|> List.first(),
favicon <- URI.merge(instance_uri, favicon_rel) |> to_string(),
true <- is_binary(favicon) do
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
end
rescue
_ -> nil
e ->
Logger.warn(
"Instance.scrape_favicon(\"#{to_string(instance_uri)}\") error: #{inspect(e)}"
)
nil
end
end
end

View file

@ -15,8 +15,8 @@ defmodule Pleroma.JobQueueMonitor do
@impl true
def init(state) do
:telemetry.attach("oban-monitor-failure", [:oban, :failure], &handle_event/4, nil)
:telemetry.attach("oban-monitor-success", [:oban, :success], &handle_event/4, nil)
:telemetry.attach("oban-monitor-failure", [:oban, :job, :exception], &handle_event/4, nil)
:telemetry.attach("oban-monitor-success", [:oban, :job, :stop], &handle_event/4, nil)
{:ok, state}
end
@ -25,8 +25,11 @@ defmodule Pleroma.JobQueueMonitor do
GenServer.call(__MODULE__, :stats)
end
def handle_event([:oban, status], %{duration: duration}, meta, _) do
GenServer.cast(__MODULE__, {:process_event, status, duration, meta})
def handle_event([:oban, :job, event], %{duration: duration}, meta, _) do
GenServer.cast(
__MODULE__,
{:process_event, mapping_status(event), duration, meta}
)
end
@impl true
@ -75,4 +78,7 @@ defmodule Pleroma.JobQueueMonitor do
|> Map.update!(:processed_jobs, &(&1 + 1))
|> Map.update!(status, &(&1 + 1))
end
defp mapping_status(:stop), do: :success
defp mapping_status(:exception), do: :failure
end

View file

@ -10,10 +10,11 @@ defmodule Pleroma.MFA.Token do
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.Token, as: OAuthToken
@expires 300
@type t() :: %__MODULE__{}
schema "mfa_tokens" do
field(:token, :string)
field(:valid_until, :naive_datetime_usec)
@ -24,6 +25,7 @@ defmodule Pleroma.MFA.Token do
timestamps()
end
@spec get_by_token(String.t()) :: {:ok, t()} | {:error, :not_found}
def get_by_token(token) do
from(
t in __MODULE__,
@ -33,33 +35,40 @@ defmodule Pleroma.MFA.Token do
|> Repo.find_resource()
end
def validate(token) do
with {:fetch_token, {:ok, token}} <- {:fetch_token, get_by_token(token)},
{:expired, false} <- {:expired, is_expired?(token)} do
@spec validate(String.t()) :: {:ok, t()} | {:error, :not_found} | {:error, :expired_token}
def validate(token_str) do
with {:ok, token} <- get_by_token(token_str),
false <- expired?(token) do
{:ok, token}
else
{:expired, _} -> {:error, :expired_token}
{:fetch_token, _} -> {:error, :not_found}
error -> {:error, error}
end
end
def create_token(%User{} = user) do
%__MODULE__{}
|> change
|> assign_user(user)
|> put_token
|> put_valid_until
|> Repo.insert()
defp expired?(%__MODULE__{valid_until: valid_until}) do
with true <- NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) > 0 do
{:error, :expired_token}
end
end
def create_token(user, authorization) do
@spec create(User.t(), Authorization.t() | nil) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
def create(user, authorization \\ nil) do
with {:ok, token} <- do_create(user, authorization) do
Pleroma.Workers.PurgeExpiredToken.enqueue(%{
token_id: token.id,
valid_until: DateTime.from_naive!(token.valid_until, "Etc/UTC"),
mod: __MODULE__
})
{:ok, token}
end
end
defp do_create(user, authorization) do
%__MODULE__{}
|> change
|> change()
|> assign_user(user)
|> assign_authorization(authorization)
|> put_token
|> put_valid_until
|> maybe_assign_authorization(authorization)
|> put_token()
|> put_valid_until()
|> Repo.insert()
end
@ -69,15 +78,19 @@ defmodule Pleroma.MFA.Token do
|> validate_required([:user])
end
defp assign_authorization(changeset, authorization) do
defp maybe_assign_authorization(changeset, %Authorization{} = authorization) do
changeset
|> put_assoc(:authorization, authorization)
|> validate_required([:authorization])
end
defp maybe_assign_authorization(changeset, _), do: changeset
defp put_token(changeset) do
token = Pleroma.Web.OAuth.Token.Utils.generate_token()
changeset
|> change(%{token: OAuthToken.Utils.generate_token()})
|> change(%{token: token})
|> validate_required([:token])
|> unique_constraint(:token)
end
@ -89,18 +102,4 @@ defmodule Pleroma.MFA.Token do
|> change(%{valid_until: expires_in})
|> validate_required([:valid_until])
end
def is_expired?(%__MODULE__{valid_until: valid_until}) do
NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) > 0
end
def is_expired?(_), do: false
def delete_expired_tokens do
from(
q in __MODULE__,
where: fragment("?", q.valid_until) < ^Timex.now()
)
|> Repo.delete_all()
end
end

View file

@ -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}"

View file

@ -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}")

View file

@ -15,6 +15,7 @@ defmodule Pleroma.Notification do
alias Pleroma.Repo
alias Pleroma.ThreadMute
alias Pleroma.User
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.Push
alias Pleroma.Web.Streamer
@ -441,6 +442,7 @@ defmodule Pleroma.Notification do
|> Multi.insert(:notification, %Notification{
user_id: user.id,
activity: activity,
seen: mark_as_read?(activity, user),
type: type_from_activity(activity)
})
|> Marker.multi_set_last_read_id(user, "notifications")
@ -634,6 +636,11 @@ defmodule Pleroma.Notification do
def skip?(_, _, _), do: false
def mark_as_read?(activity, target_user) do
user = Activity.user_actor(activity)
User.mutes_user?(target_user, user) || CommonAPI.thread_muted?(target_user, activity)
end
def for_user_and_activity(user, activity) do
from(n in __MODULE__,
where: n.user_id == ^user.id,
@ -641,4 +648,16 @@ defmodule Pleroma.Notification do
)
|> Repo.one()
end
@spec mark_context_as_read(User.t(), String.t()) :: {integer(), nil | [term()]}
def mark_context_as_read(%User{id: id}, context) do
from(
n in Notification,
join: a in assoc(n, :activity),
where: n.user_id == ^id,
where: n.seen == false,
where: fragment("?->>'context'", a.data) == ^context
)
|> Repo.update_all(set: [seen: true])
end
end

View file

@ -255,6 +255,10 @@ defmodule Pleroma.Object do
end
end
defp poll_is_multiple?(%Object{data: %{"anyOf" => [_ | _]}}), do: true
defp poll_is_multiple?(_), do: false
def decrease_replies_count(ap_id) do
Object
|> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
@ -281,10 +285,10 @@ defmodule Pleroma.Object do
def increase_vote_count(ap_id, name, actor) do
with %Object{} = object <- Object.normalize(ap_id),
"Question" <- object.data["type"] do
multiple = Map.has_key?(object.data, "anyOf")
key = if poll_is_multiple?(object), do: "anyOf", else: "oneOf"
options =
(object.data["anyOf"] || object.data["oneOf"] || [])
object.data[key]
|> Enum.map(fn
%{"name" => ^name} = option ->
Kernel.update_in(option["replies"]["totalItems"], &(&1 + 1))
@ -296,11 +300,8 @@ defmodule Pleroma.Object do
voters = [actor | object.data["voters"] || []] |> Enum.uniq()
data =
if multiple do
Map.put(object.data, "anyOf", options)
else
Map.put(object.data, "oneOf", options)
end
object.data
|> Map.put(key, options)
|> Map.put("voters", voters)
object

View file

@ -44,18 +44,11 @@ defmodule Pleroma.Object.Containment do
nil
end
# TODO: We explicitly allow 'tag' URIs through, due to references to legacy OStatus
# objects being present in the test suite environment. Once these objects are
# removed, please also remove this.
if Mix.env() == :test do
defp compare_uris(_, %URI{scheme: "tag"}), do: :ok
end
defp compare_uris(%URI{host: host} = _id_uri, %URI{host: host} = _other_uri), do: :ok
defp compare_uris(_id_uri, _other_uri), do: :error
@doc """
Checks that an imported AP object's actor matches the domain it came from.
Checks that an imported AP object's actor matches the host it came from.
"""
def contain_origin(_id, %{"actor" => nil}), do: :error

View file

@ -9,8 +9,10 @@ defmodule Pleroma.Object.Fetcher do
alias Pleroma.Repo
alias Pleroma.Signature
alias Pleroma.Web.ActivityPub.InternalFetchActor
alias Pleroma.Web.ActivityPub.ObjectValidator
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.Federator
alias Pleroma.Web.FedSockets
require Logger
require Pleroma.Constants
@ -23,21 +25,38 @@ defmodule Pleroma.Object.Fetcher do
Ecto.Changeset.put_change(changeset, :updated_at, updated_at)
end
defp maybe_reinject_internal_fields(data, %{data: %{} = old_data}) do
defp maybe_reinject_internal_fields(%{data: %{} = old_data}, new_data) do
internal_fields = Map.take(old_data, Pleroma.Constants.object_internal_fields())
Map.merge(data, internal_fields)
Map.merge(new_data, internal_fields)
end
defp maybe_reinject_internal_fields(data, _), do: data
defp maybe_reinject_internal_fields(_, new_data), do: new_data
@spec reinject_object(struct(), map()) :: {:ok, Object.t()} | {:error, any()}
defp reinject_object(struct, data) do
Logger.debug("Reinjecting object #{data["id"]}")
defp reinject_object(%Object{data: %{"type" => "Question"}} = object, new_data) do
Logger.debug("Reinjecting object #{new_data["id"]}")
with data <- Transmogrifier.fix_object(data),
data <- maybe_reinject_internal_fields(data, struct),
changeset <- Object.change(struct, %{data: data}),
with data <- maybe_reinject_internal_fields(object, new_data),
{:ok, data, _} <- ObjectValidator.validate(data, %{}),
changeset <- Object.change(object, %{data: data}),
changeset <- touch_changeset(changeset),
{:ok, object} <- Repo.insert_or_update(changeset),
{:ok, object} <- Object.set_cache(object) do
{:ok, object}
else
e ->
Logger.error("Error while processing object: #{inspect(e)}")
{:error, e}
end
end
defp reinject_object(%Object{} = object, new_data) do
Logger.debug("Reinjecting object #{new_data["id"]}")
with new_data <- Transmogrifier.fix_object(new_data),
data <- maybe_reinject_internal_fields(object, new_data),
changeset <- Object.change(object, %{data: data}),
changeset <- touch_changeset(changeset),
{:ok, object} <- Repo.insert_or_update(changeset),
{:ok, object} <- Object.set_cache(object) do
@ -51,8 +70,8 @@ defmodule Pleroma.Object.Fetcher do
def refetch_object(%Object{data: %{"id" => id}} = object) do
with {:local, false} <- {:local, Object.local?(object)},
{:ok, data} <- fetch_and_contain_remote_object_from_id(id),
{:ok, object} <- reinject_object(object, data) do
{:ok, new_data} <- fetch_and_contain_remote_object_from_id(id),
{:ok, object} <- reinject_object(object, new_data) do
{:ok, object}
else
{:local, true} -> {:ok, object}
@ -80,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}
@ -106,8 +125,8 @@ defmodule Pleroma.Object.Fetcher do
defp prepare_activity_params(data) do
%{
"type" => "Create",
"to" => data["to"],
"cc" => data["cc"],
"to" => data["to"] || [],
"cc" => data["cc"] || [],
# Should we seriously keep this attributedTo thing?
"actor" => data["actor"] || data["attributedTo"],
"object" => data
@ -145,12 +164,12 @@ defmodule Pleroma.Object.Fetcher do
date: date
})
[{"signature", signature}]
{"signature", signature}
end
defp sign_fetch(headers, id, date) do
if Pleroma.Config.get([:activitypub, :sign_object_fetches]) do
headers ++ make_signature(id, date)
[make_signature(id, date) | headers]
else
headers
end
@ -158,33 +177,26 @@ defmodule Pleroma.Object.Fetcher do
defp maybe_date_fetch(headers, date) do
if Pleroma.Config.get([:activitypub, :sign_object_fetches]) do
headers ++ [{"date", date}]
[{"date", date} | headers]
else
headers
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"}
@ -196,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

View file

@ -30,6 +30,7 @@ defmodule Pleroma.Plugs.FrontendStatic do
opts
|> Keyword.put(:from, "__unconfigured_frontend_static_plug")
|> Plug.Static.init()
|> Map.put(:frontend_type, opts[:frontend_type])
end
def call(conn, opts) do

View file

@ -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,

View file

@ -7,45 +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(conn, _) do
config = Pleroma.Config.get(__MODULE__, [])
if Keyword.get(config, :enabled, false) do
RemoteIp.call(conn, remote_ip_opts(config))
def call(%{remote_ip: original_remote_ip} = conn, _) do
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

View file

@ -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

View file

@ -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

View file

@ -7,6 +7,7 @@ defmodule Pleroma.ReverseProxy.Client.Hackney do
@impl true
def request(method, url, headers, body, opts \\ []) do
opts = Keyword.put(opts, :ssl_options, versions: [:"tlsv1.2", :"tlsv1.1", :tlsv1])
:hackney.request(method, url, headers, body, opts)
end

View file

@ -28,7 +28,7 @@ defmodule Pleroma.ReverseProxy.Client.Tesla do
url,
body,
headers,
Keyword.put(opts, :adapter, opts)
opts
) do
if is_map(response.body) and method != :head do
{:ok, response.status, response.headers, response.body}

View file

@ -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

View file

@ -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 ->

View file

@ -3,12 +3,15 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Stats do
use GenServer
import Ecto.Query
alias Pleroma.CounterCache
alias Pleroma.Repo
alias Pleroma.User
use GenServer
@interval :timer.seconds(60)
def start_link(_) do
GenServer.start_link(
@ -18,6 +21,12 @@ defmodule Pleroma.Stats do
)
end
@impl true
def init(_args) do
if Pleroma.Config.get(:env) == :test, do: :ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo)
{:ok, nil, {:continue, :calculate_stats}}
end
@doc "Performs update stats"
def force_update do
GenServer.call(__MODULE__, :force_update)
@ -29,7 +38,11 @@ defmodule Pleroma.Stats do
end
@doc "Returns stats data"
@spec get_stats() :: %{domain_count: integer(), status_count: integer(), user_count: integer()}
@spec get_stats() :: %{
domain_count: non_neg_integer(),
status_count: non_neg_integer(),
user_count: non_neg_integer()
}
def get_stats do
%{stats: stats} = GenServer.call(__MODULE__, :get_state)
@ -44,25 +57,14 @@ defmodule Pleroma.Stats do
peers
end
def init(_args) do
{:ok, calculate_stat_data()}
end
def handle_call(:force_update, _from, _state) do
new_stats = calculate_stat_data()
{:reply, new_stats, new_stats}
end
def handle_call(:get_state, _from, state) do
{:reply, state, state}
end
def handle_cast(:run_update, _state) do
new_stats = calculate_stat_data()
{:noreply, new_stats}
end
@spec calculate_stat_data() :: %{
peers: list(),
stats: %{
domain_count: non_neg_integer(),
status_count: non_neg_integer(),
user_count: non_neg_integer()
}
}
def calculate_stat_data do
peers =
from(
@ -97,6 +99,7 @@ defmodule Pleroma.Stats do
}
end
@spec get_status_visibility_count(String.t() | nil) :: map()
def get_status_visibility_count(instance \\ nil) do
if is_nil(instance) do
CounterCache.get_sum()
@ -104,4 +107,36 @@ defmodule Pleroma.Stats do
CounterCache.get_by_instance(instance)
end
end
@impl true
def handle_continue(:calculate_stats, _) do
stats = calculate_stat_data()
Process.send_after(self(), :run_update, @interval)
{:noreply, stats}
end
@impl true
def handle_call(:force_update, _from, _state) do
new_stats = calculate_stat_data()
{:reply, new_stats, new_stats}
end
@impl true
def handle_call(:get_state, _from, state) do
{:reply, state, state}
end
@impl true
def handle_cast(:run_update, _state) do
new_stats = calculate_stat_data()
{:noreply, new_stats}
end
@impl true
def handle_info(:run_update, _) do
new_stats = calculate_stat_data()
Process.send_after(self(), :run_update, @interval)
{:noreply, new_stats}
end
end

View file

@ -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

View file

@ -0,0 +1,50 @@
# Pleroma: A lightweight social networking server
# Copyright © 2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Tesla.Middleware.ConnectionPool do
@moduledoc """
Middleware to get/release connections from `Pleroma.Gun.ConnectionPool`
"""
@behaviour Tesla.Middleware
alias Pleroma.Gun.ConnectionPool
@impl Tesla.Middleware
def call(%Tesla.Env{url: url, opts: opts} = env, next, _) do
uri = URI.parse(url)
# Avoid leaking connections when the middleware is called twice
# with body_as: :chunks. We assume only the middleware can set
# opts[:adapter][:conn]
if opts[:adapter][:conn] do
ConnectionPool.release_conn(opts[:adapter][:conn])
end
case ConnectionPool.get_conn(uri, opts[:adapter]) do
{:ok, conn_pid} ->
adapter_opts = Keyword.merge(opts[:adapter], conn: conn_pid, close_conn: false)
opts = Keyword.put(opts, :adapter, adapter_opts)
env = %{env | opts: opts}
case Tesla.run(env, next) do
{:ok, env} ->
unless opts[:adapter][:body_as] == :chunks do
ConnectionPool.release_conn(conn_pid)
{_, res} = pop_in(env.opts[:adapter][:conn])
{:ok, res}
else
{:ok, env}
end
err ->
ConnectionPool.release_conn(conn_pid)
err
end
err ->
err
end
end
end

View file

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

View file

@ -56,6 +56,15 @@ defmodule Pleroma.Upload do
}
defstruct [:id, :name, :tempfile, :content_type, :path]
defp get_description(opts, upload) do
case {opts[:description], Pleroma.Config.get([Pleroma.Upload, :default_description])} do
{description, _} when is_binary(description) -> description
{_, :filename} -> upload.name
{_, str} when is_binary(str) -> str
_ -> ""
end
end
@spec store(source, options :: [option()]) :: {:ok, Map.t()} | {:error, any()}
def store(upload, opts \\ []) do
opts = get_opts(opts)
@ -63,7 +72,7 @@ defmodule Pleroma.Upload do
with {:ok, upload} <- prepare_upload(upload, opts),
upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"},
{:ok, upload} <- Pleroma.Upload.Filter.filter(opts.filters, upload),
description = Map.get(opts, :description) || upload.name,
description = get_description(opts, upload),
{_, true} <-
{:description_limit,
String.length(description) <= Pleroma.Config.get([:instance, :description_limit])},

View file

@ -15,7 +15,11 @@ defmodule Pleroma.Upload.Filter do
require Logger
@callback filter(Pleroma.Upload.t()) :: :ok | {:ok, Pleroma.Upload.t()} | {:error, any()}
@callback filter(Pleroma.Upload.t()) ::
{:ok, :filtered}
| {:ok, :noop}
| {:ok, :filtered, Pleroma.Upload.t()}
| {:error, any()}
@spec filter([module()], Pleroma.Upload.t()) :: {:ok, Pleroma.Upload.t()} | {:error, any()}
@ -25,10 +29,13 @@ defmodule Pleroma.Upload.Filter do
def filter([filter | rest], upload) do
case filter.filter(upload) do
:ok ->
{:ok, :filtered} ->
filter(rest, upload)
{:ok, upload} ->
{:ok, :filtered, upload} ->
filter(rest, upload)
{:ok, :noop} ->
filter(rest, upload)
error ->

View file

@ -16,9 +16,11 @@ defmodule Pleroma.Upload.Filter.AnonymizeFilename do
def filter(%Upload{name: name} = upload) do
extension = List.last(String.split(name, "."))
name = predefined_name(extension) || random(extension)
{:ok, %Upload{upload | name: name}}
{:ok, :filtered, %Upload{upload | name: name}}
end
def filter(_), do: {:ok, :noop}
@spec predefined_name(String.t()) :: String.t() | nil
defp predefined_name(extension) do
with name when not is_nil(name) <- Config.get([__MODULE__, :text]),

View file

@ -17,8 +17,8 @@ defmodule Pleroma.Upload.Filter.Dedupe do
|> Base.encode16(case: :lower)
filename = shasum <> "." <> extension
{:ok, %Upload{upload | id: shasum, path: filename}}
{:ok, :filtered, %Upload{upload | id: shasum, path: filename}}
end
def filter(_), do: :ok
def filter(_), do: {:ok, :noop}
end

View file

@ -9,10 +9,22 @@ defmodule Pleroma.Upload.Filter.Exiftool do
"""
@behaviour Pleroma.Upload.Filter
@spec filter(Pleroma.Upload.t()) :: {:ok, any()} | {:error, String.t()}
# webp is not compatible with exiftool at this time
def filter(%Pleroma.Upload{content_type: "image/webp"}), do: {:ok, :noop}
def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do
System.cmd("exiftool", ["-overwrite_original", "-gps:all=", file], parallelism: true)
:ok
try do
case System.cmd("exiftool", ["-overwrite_original", "-gps:all=", file], parallelism: true) do
{_response, 0} -> {:ok, :filtered}
{error, 1} -> {:error, error}
end
rescue
_e in ErlangError ->
{:error, "exiftool command not found"}
end
end
def filter(_), do: :ok
def filter(_), do: {:ok, :noop}
end

View file

@ -6,6 +6,10 @@ defmodule Pleroma.Upload.Filter.Mogrifun do
@behaviour Pleroma.Upload.Filter
alias Pleroma.Upload.Filter
@moduledoc """
This module is just an example of an Upload filter. It's not supposed to be used in production.
"""
@filters [
{"implode", "1"},
{"-raise", "20"},
@ -34,11 +38,16 @@ defmodule Pleroma.Upload.Filter.Mogrifun do
[{"fill", "yellow"}, {"tint", "40"}]
]
@spec filter(Pleroma.Upload.t()) :: {:ok, atom()} | {:error, String.t()}
def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do
Filter.Mogrify.do_filter(file, [Enum.random(@filters)])
:ok
try do
Filter.Mogrify.do_filter(file, [Enum.random(@filters)])
{:ok, :filtered}
rescue
_e in ErlangError ->
{:error, "mogrify command not found"}
end
end
def filter(_), do: :ok
def filter(_), do: {:ok, :noop}
end

View file

@ -8,14 +8,18 @@ defmodule Pleroma.Upload.Filter.Mogrify do
@type conversion :: action :: String.t() | {action :: String.t(), opts :: String.t()}
@type conversions :: conversion() | [conversion()]
@spec filter(Pleroma.Upload.t()) :: {:ok, :atom} | {:error, String.t()}
def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do
filters = Pleroma.Config.get!([__MODULE__, :args])
do_filter(file, filters)
:ok
try do
do_filter(file, Pleroma.Config.get!([__MODULE__, :args]))
{:ok, :filtered}
rescue
_e in ErlangError ->
{:error, "mogrify command not found"}
end
end
def filter(_), do: :ok
def filter(_), do: {:ok, :noop}
def do_filter(file, filters) do
file

View file

@ -46,12 +46,23 @@ defmodule Pleroma.Uploaders.S3 do
op =
if streaming do
upload.tempfile
|> ExAws.S3.Upload.stream_file()
|> ExAws.S3.upload(bucket, s3_name, [
{:acl, :public_read},
{:content_type, upload.content_type}
])
op =
upload.tempfile
|> ExAws.S3.Upload.stream_file()
|> ExAws.S3.upload(bucket, s3_name, [
{:acl, :public_read},
{:content_type, upload.content_type}
])
if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Gun do
# set s3 upload timeout to respect :upload pool timeout
# timeout should be slightly larger, so s3 can retry upload on fail
timeout = Pleroma.HTTP.AdapterHelper.Gun.pool_timeout(:upload) + 1_000
opts = Keyword.put(op.opts, :timeout, timeout)
Map.put(op, :opts, opts)
else
op
end
else
{:ok, file_data} = File.read(upload.tempfile)

View file

@ -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
@ -83,7 +82,7 @@ defmodule Pleroma.User do
]
schema "users" do
field(:bio, :string)
field(:bio, :string, default: "")
field(:raw_bio, :string)
field(:email, :string)
field(:name, :string)
@ -247,6 +246,13 @@ defmodule Pleroma.User do
end
end
defdelegate following_count(user), to: FollowingRelationship
defdelegate following(user), to: FollowingRelationship
defdelegate following?(follower, followed), to: FollowingRelationship
defdelegate following_ap_ids(user), to: FollowingRelationship
defdelegate get_follow_requests(user), to: FollowingRelationship
defdelegate search(query, opts \\ []), to: User.Search
@doc """
Dumps Flake Id to SQL-compatible format (16-byte UUID).
E.g. "9pQtDGXuq4p3VlcJEm" -> <<0, 0, 1, 110, 179, 218, 42, 92, 213, 41, 44, 227, 95, 213, 0, 0>>
@ -269,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
@ -311,10 +317,12 @@ defmodule Pleroma.User do
def visible_for(_, _), do: :invisible
defp restrict_unauthenticated?(%User{local: local}) do
config_key = if local, do: :local, else: :remote
defp restrict_unauthenticated?(%User{local: true}) do
Config.restrict_unauthenticated_access?(:profiles, :local)
end
Config.get([:restrict_unauthenticated, :profiles, config_key], false)
defp restrict_unauthenticated?(%User{local: _}) do
Config.restrict_unauthenticated_access?(:profiles, :remote)
end
defp visible_account_status(user) do
@ -370,8 +378,6 @@ defmodule Pleroma.User do
from(u in query, where: u.deactivated != ^true)
end
defdelegate following_count(user), to: FollowingRelationship
defp truncate_fields_param(params) do
if Map.has_key?(params, :fields) do
Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
@ -638,6 +644,34 @@ defmodule Pleroma.User do
@spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def force_password_reset(user), do: update_password_reset_pending(user, true)
# Used to auto-register LDAP accounts which won't have a password hash stored locally
def register_changeset_ldap(struct, params = %{password: password})
when is_nil(password) do
params = Map.put_new(params, :accepts_chat_messages, true)
params =
if Map.has_key?(params, :email) do
Map.put_new(params, :email, params[:email])
else
params
end
struct
|> cast(params, [
:name,
:nickname,
:email,
:accepts_chat_messages
])
|> validate_required([:name, :nickname])
|> unique_constraint(:nickname)
|> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
|> validate_format(:nickname, local_nickname_regex())
|> put_ap_id()
|> unique_constraint(:ap_id)
|> put_following_and_follower_address()
end
def register_changeset(struct, params \\ %{}, opts \\ []) do
bio_limit = Config.get([:instance, :user_bio_length], 5000)
name_limit = Config.get([:instance, :user_name_length], 100)
@ -779,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}
@ -838,8 +873,6 @@ defmodule Pleroma.User do
set_cache(follower)
end
defdelegate following(user), to: FollowingRelationship
def follow(%User{} = follower, %User{} = followed, state \\ :follow_accept) do
deny_follow_blocked = Config.get([:user, :deny_follow_blocked])
@ -882,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}
@ -893,8 +924,6 @@ defmodule Pleroma.User do
end
end
defdelegate following?(follower, followed), to: FollowingRelationship
@doc "Returns follow state as Pleroma.FollowingRelationship.State value"
def get_follow_state(%User{} = follower, %User{} = following) do
following_relationship = FollowingRelationship.get(follower, following)
@ -1094,31 +1123,31 @@ defmodule Pleroma.User do
User.Query.build(%{followers: user, deactivated: false})
end
def get_followers_query(user, page) do
def get_followers_query(%User{} = user, page) do
user
|> get_followers_query(nil)
|> User.Query.paginate(page, 20)
end
@spec get_followers_query(User.t()) :: Ecto.Query.t()
def get_followers_query(user), do: get_followers_query(user, nil)
def get_followers_query(%User{} = user), do: get_followers_query(user, nil)
@spec get_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
def get_followers(user, page \\ nil) do
def get_followers(%User{} = user, page \\ nil) do
user
|> get_followers_query(page)
|> Repo.all()
end
@spec get_external_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
def get_external_followers(user, page \\ nil) do
def get_external_followers(%User{} = user, page \\ nil) do
user
|> get_followers_query(page)
|> User.Query.build(%{external: true})
|> Repo.all()
end
def get_followers_ids(user, page \\ nil) do
def get_followers_ids(%User{} = user, page \\ nil) do
user
|> get_followers_query(page)
|> select([u], u.id)
@ -1130,37 +1159,35 @@ defmodule Pleroma.User do
User.Query.build(%{friends: user, deactivated: false})
end
def get_friends_query(user, page) do
def get_friends_query(%User{} = user, page) do
user
|> get_friends_query(nil)
|> User.Query.paginate(page, 20)
end
@spec get_friends_query(User.t()) :: Ecto.Query.t()
def get_friends_query(user), do: get_friends_query(user, nil)
def get_friends_query(%User{} = user), do: get_friends_query(user, nil)
def get_friends(user, page \\ nil) do
def get_friends(%User{} = user, page \\ nil) do
user
|> get_friends_query(page)
|> Repo.all()
end
def get_friends_ap_ids(user) do
def get_friends_ap_ids(%User{} = user) do
user
|> get_friends_query(nil)
|> select([u], u.ap_id)
|> Repo.all()
end
def get_friends_ids(user, page \\ nil) do
def get_friends_ids(%User{} = user, page \\ nil) do
user
|> get_friends_query(page)
|> select([u], u.id)
|> Repo.all()
end
defdelegate get_follow_requests(user), to: FollowingRelationship
def increase_note_count(%User{} = user) do
User
|> where(id: ^user.id)
@ -1553,6 +1580,49 @@ defmodule Pleroma.User do
|> update_and_set_cache()
end
@spec purge_user_changeset(User.t()) :: Changeset.t()
def purge_user_changeset(user) do
# "Right to be forgotten"
# https://gdpr.eu/right-to-be-forgotten/
change(user, %{
bio: "",
raw_bio: nil,
email: nil,
name: nil,
password_hash: nil,
keys: nil,
public_key: nil,
avatar: %{},
tags: [],
last_refreshed_at: nil,
last_digest_emailed_at: nil,
banner: %{},
background: %{},
note_count: 0,
follower_count: 0,
following_count: 0,
locked: false,
confirmation_pending: false,
password_reset_pending: false,
approval_pending: false,
registration_reason: nil,
confirmation_token: nil,
domain_blocks: [],
deactivated: true,
ap_enabled: false,
is_moderator: false,
is_admin: false,
mastofe_settings: nil,
mascot: nil,
emoji: %{},
pleroma_settings_store: %{},
fields: [],
raw_fields: [],
discoverable: false,
also_known_as: []
})
end
def delete(users) when is_list(users) do
for user <- users, do: delete(user)
end
@ -1580,7 +1650,7 @@ defmodule Pleroma.User do
_ ->
user
|> change(%{deactivated: true, email: nil})
|> purge_user_changeset()
|> update_and_set_cache()
end
end
@ -1614,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(%{
@ -1678,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))
@ -1703,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)
@ -1749,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}} ->
@ -1827,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
@ -2051,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
@ -2090,8 +2116,6 @@ defmodule Pleroma.User do
|> Repo.all()
end
defdelegate search(query, opts \\ []), to: User.Search
defp put_password_hash(
%Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
) do
@ -2245,6 +2269,11 @@ defmodule Pleroma.User do
max_pinned_statuses = Config.get([:instance, :max_pinned_statuses], 0)
params = %{pinned_activities: user.pinned_activities ++ [id]}
# if pinned activity was scheduled for deletion, we remove job
if expiration = Pleroma.Workers.PurgeExpiredActivity.get_expiration(id) do
Oban.cancel_job(expiration.id)
end
user
|> cast(params, [:pinned_activities])
|> validate_length(:pinned_activities,
@ -2257,9 +2286,21 @@ defmodule Pleroma.User do
|> update_and_set_cache()
end
def remove_pinnned_activity(user, %Pleroma.Activity{id: id}) do
def remove_pinnned_activity(user, %Pleroma.Activity{id: id, data: data}) do
params = %{pinned_activities: List.delete(user.pinned_activities, id)}
# if pinned activity was scheduled for deletion, we reschedule it for deletion
if data["expires_at"] do
# 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,
expires_at: expires_at
})
end
user
|> cast(params, [:pinned_activities])
|> update_and_set_cache()

View 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

View file

@ -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,13 +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
@ -144,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
@ -198,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

View file

@ -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)
@ -115,13 +156,17 @@ defmodule Pleroma.User.Search do
)
end
defp base_query(_user, false), do: User
defp base_query(user, true), do: User.get_followers_query(user)
defp base_query(%User{} = user, true), do: User.get_friends_query(user)
defp base_query(_user, _following), do: User
defp filter_invisible_users(query) do
from(q in query, where: q.invisible == false)
end
defp filter_discoverable_users(query) do
from(q in query, where: q.discoverable == true)
end
defp filter_internal_users(query) do
from(q in query, where: q.actor_type != "Application")
end
@ -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

View file

@ -9,4 +9,39 @@ defmodule Pleroma.Utils do
|> Enum.map(&Path.join(dir, &1))
|> Kernel.ParallelCompiler.compile()
end
@doc """
POSIX-compliant check if command is available in the system
## Examples
iex> command_available?("git")
true
iex> command_available?("wrongcmd")
false
"""
@spec command_available?(String.t()) :: boolean()
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

View file

@ -5,7 +5,6 @@
defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.Activity
alias Pleroma.Activity.Ir.Topics
alias Pleroma.ActivityExpiration
alias Pleroma.Config
alias Pleroma.Constants
alias Pleroma.Conversation
@ -66,7 +65,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp check_remote_limit(_), do: true
defp increase_note_count_if_public(actor, object) do
def increase_note_count_if_public(actor, object) do
if is_public?(object), do: User.increase_note_count(actor), else: {:ok, actor}
end
@ -85,17 +84,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp increase_replies_count_if_reply(_create_data), do: :noop
defp increase_poll_votes_if_vote(%{
"object" => %{"inReplyTo" => reply_ap_id, "name" => name},
"type" => "Create",
"actor" => actor
}) do
Object.increase_vote_count(reply_ap_id, name, actor)
end
defp increase_poll_votes_if_vote(_create_data), do: :noop
@object_types ["ChatMessage"]
@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
@ -112,7 +101,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
local: local,
recipients: recipients,
actor: object["actor"]
}) do
}),
# TODO: add tests for expired activities, when Note type will be supported in new pipeline
{:ok, _} <- maybe_create_activity_expiration(activity) do
{:ok, activity, meta}
end
end
@ -121,23 +112,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when is_map(map) do
with nil <- Activity.normalize(map),
map <- lazy_put_activity_defaults(map, fake),
true <- bypass_actor_check || check_actor_is_active(map["actor"]),
{_, true} <- {:remote_limit_error, check_remote_limit(map)},
{_, true} <- {:actor_check, bypass_actor_check || check_actor_is_active(map["actor"])},
{_, true} <- {:remote_limit_pass, check_remote_limit(map)},
{:ok, map} <- MRF.filter(map),
{recipients, _, _} = get_recipients(map),
{:fake, false, map, recipients} <- {:fake, fake, map, recipients},
{:containment, :ok} <- {:containment, Containment.contain_child(map)},
{:ok, map, object} <- insert_full_object(map) do
{:ok, activity} =
%Activity{
data: map,
local: local,
actor: map["actor"],
recipients: recipients
}
|> Repo.insert()
|> maybe_create_activity_expiration()
{:ok, map, object} <- insert_full_object(map),
{:ok, activity} <- insert_activity_with_expiration(map, local, recipients) do
# Splice in the child object if we have one.
activity = Maps.put_if_present(activity, :object, object)
@ -148,6 +130,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
%Activity{} = activity ->
{:ok, activity}
{:actor_check, _} ->
{:error, false}
{:containment, _} = error ->
error
{:error, _} = error ->
error
{:fake, true, map, recipients} ->
activity = %Activity{
data: map,
@ -160,8 +151,24 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
{:ok, activity}
error ->
{:error, error}
{:remote_limit_pass, _} ->
{:error, :remote_limit}
{:reject, _} = e ->
{:error, e}
end
end
defp insert_activity_with_expiration(data, local, recipients) do
struct = %Activity{
data: data,
local: local,
actor: data["actor"],
recipients: recipients
}
with {:ok, activity} <- Repo.insert(struct) do
maybe_create_activity_expiration(activity)
end
end
@ -174,13 +181,19 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
stream_out_participations(participations)
end
defp maybe_create_activity_expiration({:ok, %{data: %{"expires_at" => expires_at}} = activity}) do
with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
defp maybe_create_activity_expiration(
%{data: %{"expires_at" => %DateTime{} = expires_at}} = activity
) do
with {:ok, _job} <-
Pleroma.Workers.PurgeExpiredActivity.enqueue(%{
activity_id: activity.id,
expires_at: expires_at
}) do
{:ok, activity}
end
end
defp maybe_create_activity_expiration(result), do: result
defp maybe_create_activity_expiration(activity), do: {:ok, activity}
defp create_or_bump_conversation(activity, actor) do
with {:ok, conversation} <- Conversation.create_or_bump_for(activity),
@ -258,7 +271,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
with {:ok, activity} <- insert(create_data, local, fake),
{:fake, false, activity} <- {:fake, fake, activity},
_ <- increase_replies_count_if_reply(create_data),
_ <- increase_poll_votes_if_vote(create_data),
{:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity},
{:ok, _actor} <- increase_note_count_if_public(actor, activity),
_ <- notify_and_stream(activity),
@ -296,32 +308,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
@spec accept(map()) :: {:ok, Activity.t()} | {:error, any()}
def accept(params) do
accept_or_reject("Accept", params)
end
@spec reject(map()) :: {:ok, Activity.t()} | {:error, any()}
def reject(params) do
accept_or_reject("Reject", params)
end
@spec accept_or_reject(String.t(), map()) :: {:ok, Activity.t()} | {:error, any()}
defp accept_or_reject(type, %{to: to, actor: actor, object: object} = params) do
local = Map.get(params, :local, true)
activity_id = Map.get(params, :activity_id, nil)
data =
%{"to" => to, "type" => type, "actor" => actor.ap_id, "object" => object}
|> Maps.put_if_present("id", activity_id)
with {:ok, activity} <- insert(data, local),
_ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
@spec unfollow(User.t(), User.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t()} | nil | {:error, any()}
def unfollow(follower, followed, activity_id \\ nil, local \\ true) do
@ -781,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(
@ -797,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,
@ -855,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",
@ -1256,7 +1259,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
name: data["name"],
follower_address: data["followers"],
following_address: data["following"],
bio: data["summary"],
bio: data["summary"] || "",
actor_type: actor_type,
also_known_as: Map.get(data, "alsoKnownAs", []),
public_key: public_key,
@ -1279,10 +1282,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,
%{
@ -1356,8 +1361,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
@ -1376,9 +1381,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
def maybe_handle_clashing_nickname(data) do
nickname = data[:nickname]
with %User{} = old_user <- User.get_by_nickname(nickname),
with nickname when is_binary(nickname) <- data[:nickname],
%User{} = old_user <- User.get_by_nickname(nickname),
{_, false} <- {:ap_id_comparison, data[:ap_id] == old_user.ap_id} do
Logger.info(
"Found an old user for #{nickname}, the old ap id is #{old_user.ap_id}, new one is #{
@ -1392,7 +1396,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
else
{:ap_id_comparison, true} ->
Logger.info(
"Found an old user for #{nickname}, but the ap id #{data[:ap_id]} is the same as the new user. Race condition? Not changing anything."
"Found an old user for #{data[:nickname]}, but the ap id #{data[:ap_id]} is the same as the new user. Race condition? Not changing anything."
)
_ ->
@ -1400,13 +1404,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)

View file

@ -399,21 +399,30 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
defp handle_user_activity(
%User{} = user,
%{"type" => "Create", "object" => %{"type" => "Note"}} = params
%{"type" => "Create", "object" => %{"type" => "Note"} = object} = params
) do
object =
params["object"]
|> Map.merge(Map.take(params, ["to", "cc"]))
|> Map.put("attributedTo", user.ap_id())
|> Transmogrifier.fix_object()
content = if is_binary(object["content"]), do: object["content"], else: ""
name = if is_binary(object["name"]), do: object["name"], else: ""
summary = if is_binary(object["summary"]), do: object["summary"], else: ""
length = String.length(content <> name <> summary)
ActivityPub.create(%{
to: params["to"],
actor: user,
context: object["context"],
object: object,
additional: Map.take(params, ["cc"])
})
if length > Pleroma.Config.get([:instance, :limit]) do
{:error, dgettext("errors", "Note is over the character limit")}
else
object =
object
|> Map.merge(Map.take(params, ["to", "cc"]))
|> Map.put("attributedTo", user.ap_id())
|> Transmogrifier.fix_object()
ActivityPub.create(%{
to: params["to"],
actor: user,
context: object["context"],
object: object,
additional: Map.take(params, ["cc"])
})
end
end
defp handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do

View file

@ -14,6 +14,28 @@ defmodule Pleroma.Web.ActivityPub.Builder do
require Pleroma.Constants
def accept_or_reject(actor, activity, type) do
data = %{
"id" => Utils.generate_activity_id(),
"actor" => actor.ap_id,
"type" => type,
"object" => activity.data["id"],
"to" => [activity.actor]
}
{:ok, data, []}
end
@spec reject(User.t(), Activity.t()) :: {:ok, map(), keyword()}
def reject(actor, rejected_activity) do
accept_or_reject(actor, rejected_activity, "Reject")
end
@spec accept(User.t(), Activity.t()) :: {:ok, map(), keyword()}
def accept(actor, accepted_activity) do
accept_or_reject(actor, accepted_activity, "Accept")
end
@spec follow(User.t(), User.t()) :: {:ok, map(), keyword()}
def follow(follower, followed) do
data = %{
@ -80,6 +102,13 @@ defmodule Pleroma.Web.ActivityPub.Builder do
end
def create(actor, object, recipients) do
context =
if is_map(object) do
object["context"]
else
nil
end
{:ok,
%{
"id" => Utils.generate_activity_id(),
@ -88,7 +117,8 @@ defmodule Pleroma.Web.ActivityPub.Builder do
"object" => object,
"type" => "Create",
"published" => DateTime.utc_now() |> DateTime.to_iso8601()
}, []}
}
|> Pleroma.Maps.put_if_present("context", context), []}
end
def chat_message(actor, recipient, content, opts \\ []) do
@ -115,6 +145,22 @@ defmodule Pleroma.Web.ActivityPub.Builder do
end
end
def answer(user, object, name) do
{:ok,
%{
"type" => "Answer",
"actor" => user.ap_id,
"attributedTo" => user.ap_id,
"cc" => [object.data["actor"]],
"to" => [],
"name" => name,
"inReplyTo" => object.data["id"],
"context" => object.data["context"],
"published" => DateTime.utc_now() |> DateTime.to_iso8601(),
"id" => Utils.generate_object_id()
}, []}
end
@spec tombstone(String.t(), String.t()) :: {:ok, map(), keyword()}
def tombstone(actor, id) do
{:ok,
@ -169,7 +215,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do
to =
cond do
actor.ap_id == Relay.relay_ap_id() ->
actor.ap_id == Relay.ap_id() ->
[actor.follower_address]
public? ->

View file

@ -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

View file

@ -31,10 +31,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy do
defp maybe_add_expiration(activity) do
days = Pleroma.Config.get([:mrf_activity_expiration, :days], 365)
expires_at = NaiveDateTime.utc_now() |> Timex.shift(days: days)
expires_at = DateTime.utc_now() |> Timex.shift(days: days)
with %{"expires_at" => existing_expires_at} <- activity,
:lt <- NaiveDateTime.compare(existing_expires_at, expires_at) do
:lt <- DateTime.compare(existing_expires_at, expires_at) do
activity
else
_ -> Map.put(activity, "expires_at", expires_at)

View file

@ -0,0 +1,56 @@
# 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.MRF.ForceBotUnlistedPolicy do
alias Pleroma.User
@behaviour Pleroma.Web.ActivityPub.MRF
@moduledoc "Remove bot posts from federated timeline"
require Pleroma.Constants
defp check_by_actor_type(user), do: user.actor_type in ["Application", "Service"]
defp check_by_nickname(user), do: Regex.match?(~r/bot@|ebooks@/i, user.nickname)
defp check_if_bot(user), do: check_by_actor_type(user) or check_by_nickname(user)
@impl true
def filter(
%{
"type" => "Create",
"to" => to,
"cc" => cc,
"actor" => actor,
"object" => object
} = message
) do
user = User.get_cached_by_ap_id(actor)
isbot = check_if_bot(user)
if isbot and Enum.member?(to, Pleroma.Constants.as_public()) do
to = List.delete(to, Pleroma.Constants.as_public()) ++ [user.follower_address]
cc = List.delete(cc, user.follower_address) ++ [Pleroma.Constants.as_public()]
object =
object
|> Map.put("to", to)
|> Map.put("cc", cc)
message =
message
|> Map.put("to", to)
|> Map.put("cc", cc)
|> Map.put("object", object)
{:ok, message}
else
{:ok, message}
end
end
@impl true
def filter(message), do: {:ok, message}
@impl true
def describe, do: {:ok, %{}}
end

View file

@ -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

View file

@ -12,23 +12,21 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do
require Logger
@options [
pool: :media
@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)
opts =
if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do
Keyword.put(@options, :recv_timeout, 10_000)
else
@options
end
Logger.debug("Prefetching #{inspect(url)} as #{inspect(prefetch_url)}")
url
|> MediaProxy.url()
|> HTTP.get([], adapter: opts)
HTTP.get(prefetch_url, [], @adapter_options)
end
end
def perform(:preload, %{"object" => %{"attachment" => attachments}} = _message) do

View file

@ -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()

View file

@ -28,8 +28,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SubchainPolicy do
}"
)
subchain
|> MRF.filter(message)
MRF.filter(subchain, message)
else
_e -> {:ok, message}
end

View file

@ -12,21 +12,50 @@ 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.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
alias Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.EventValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.FollowValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator
@spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
def validate(object, meta)
def validate(%{"type" => type} = object, meta)
when type in ~w[Accept Reject] do
with {:ok, object} <-
object
|> AcceptRejectValidator.cast_and_validate()
|> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object)
{:ok, object, meta}
end
end
def validate(%{"type" => "Event"} = object, meta) do
with {:ok, object} <-
object
|> EventValidator.cast_and_validate()
|> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object)
{:ok, object, meta}
end
end
def validate(%{"type" => "Follow"} = object, meta) do
with {:ok, object} <-
object
@ -112,17 +141,60 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
end
end
def validate(%{"type" => "Question"} = object, meta) do
with {:ok, object} <-
object
|> QuestionValidator.cast_and_validate()
|> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object)
{:ok, object, meta}
end
end
def validate(%{"type" => type} = object, meta) when type in ~w[Audio Video] do
with {:ok, object} <-
object
|> 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}
end
end
def validate(%{"type" => "Answer"} = object, meta) do
with {:ok, object} <-
object
|> AnswerValidator.cast_and_validate()
|> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object)
{:ok, object, meta}
end
end
def validate(%{"type" => "EmojiReact"} = object, meta) do
with {:ok, object} <-
object
|> EmojiReactValidator.cast_and_validate()
|> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object |> Map.from_struct())
object = stringify_keys(object)
{:ok, object, meta}
end
end
def validate(%{"type" => "Create", "object" => object} = create_activity, meta) do
def validate(
%{"type" => "Create", "object" => %{"type" => "ChatMessage"} = object} = create_activity,
meta
) do
with {:ok, object_data} <- cast_and_apply(object),
meta = Keyword.put(meta, :object_data, object_data |> stringify_keys),
{:ok, create_activity} <-
@ -134,12 +206,28 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
end
end
def validate(
%{"type" => "Create", "object" => %{"type" => objtype} = object} = create_activity,
meta
)
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} <-
create_activity
|> CreateGenericValidator.cast_and_validate(meta)
|> Ecto.Changeset.apply_action(:insert) do
create_activity = stringify_keys(create_activity)
{:ok, create_activity, meta}
end
end
def validate(%{"type" => "Announce"} = object, meta) do
with {:ok, object} <-
object
|> AnnounceValidator.cast_and_validate()
|> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object |> Map.from_struct())
object = stringify_keys(object)
{:ok, object, meta}
end
end
@ -148,8 +236,29 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
ChatMessageValidator.cast_and_apply(object)
end
def cast_and_apply(%{"type" => "Question"} = object) do
QuestionValidator.cast_and_apply(object)
end
def cast_and_apply(%{"type" => "Answer"} = object) do
AnswerValidator.cast_and_apply(object)
end
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
def stringify_keys(%{__struct__: _} = object) do
object
|> Map.from_struct()
@ -169,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

View file

@ -0,0 +1,56 @@
# 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.AcceptRejectValidator do
use Ecto.Schema
alias Pleroma.Activity
alias Pleroma.EctoType.ActivityPub.ObjectValidators
import Ecto.Changeset
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
@primary_key false
embedded_schema do
field(:id, ObjectValidators.ObjectID, primary_key: true)
field(:type, :string)
field(:object, ObjectValidators.ObjectID)
field(:actor, ObjectValidators.ObjectID)
field(:to, ObjectValidators.Recipients, default: [])
field(:cc, ObjectValidators.Recipients, default: [])
end
def cast_data(data) do
%__MODULE__{}
|> cast(data, __schema__(:fields))
end
def validate_data(cng) do
cng
|> validate_required([:id, :type, :actor, :to, :cc, :object])
|> validate_inclusion(:type, ["Accept", "Reject"])
|> validate_actor_presence()
|> validate_object_presence(allowed_types: ["Follow"])
|> validate_accept_reject_rights()
end
def cast_and_validate(data) do
data
|> cast_data
|> validate_data
end
def validate_accept_reject_rights(cng) do
with object_id when is_binary(object_id) <- get_field(cng, :object),
%Activity{data: %{"object" => followed_actor}} <- Activity.get_by_ap_id(object_id),
true <- followed_actor == get_field(cng, :actor) do
cng
else
_e ->
cng
|> add_error(:actor, "can't accept or reject the given activity")
end
end
end

View 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.ActivityPub.ObjectValidators.AnswerValidator do
use Ecto.Schema
alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
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: [])
field(:type, :string)
field(:name, :string)
field(:inReplyTo, ObjectValidators.ObjectID)
field(:attributedTo, ObjectValidators.ObjectID)
# TODO: Remove actor on objects
field(:actor, ObjectValidators.ObjectID)
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
def changeset(struct, data) do
struct
|> cast(data, __schema__(:fields))
end
def validate_data(data_cng) do
data_cng
|> validate_inclusion(:type, ["Answer"])
|> validate_required([:id, :inReplyTo, :name, :attributedTo, :actor])
|> CommonValidations.validate_any_presence([:cc, :to])
|> CommonValidations.validate_fields_match([:actor, :attributedTo])
|> CommonValidations.validate_actor_presence()
|> CommonValidations.validate_host_match()
end
end

View file

@ -0,0 +1,106 @@
# 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.ArticleNoteValidator do
use Ecto.Schema
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
data = fix(data)
%__MODULE__{}
|> changeset(data)
end
defp fix_url(%{"url" => url} = data) when is_map(url) do
Map.put(data, "url", url["href"])
end
defp fix_url(data), do: data
defp fix(data) do
data
|> CommonFixes.fix_defaults()
|> CommonFixes.fix_attribution()
|> CommonFixes.fix_actor()
|> fix_url()
|> Transmogrifier.fix_emoji()
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, ["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()
|> CommonValidations.validate_host_match()
end
end

View file

@ -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,44 +42,56 @@ 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
data =
data
|> Map.put_new("mediaType", data["mimeType"])
data = Map.put_new(data, "mediaType", data["mimeType"])
if MIME.valid?(data["mediaType"]) do
data
else
data
|> Map.put("mediaType", "application/octet-stream")
Map.put(data, "mediaType", "application/octet-stream")
end
end
def fix_url(data) do
case data["url"] do
url when is_binary(url) ->
data
|> Map.put(
"url",
[
%{
"href" => url,
"type" => "Link",
"mediaType" => data["mediaType"]
}
]
)
defp handle_href(href, mediaType) do
[
%{
"href" => href,
"type" => "Link",
"mediaType" => mediaType
}
]
end
_ ->
defp fix_url(data) do
cond do
is_binary(data["url"]) ->
Map.put(data, "url", handle_href(data["url"], data["mediaType"]))
is_binary(data["href"]) and data["url"] == nil ->
Map.put(data, "url", handle_href(data["href"], data["mediaType"]))
true ->
data
end
end
def validate_data(cng) do
cng
|> validate_inclusion(:type, ~w[Document Audio Image Video])
|> validate_required([:mediaType, :url, :type])
end
end

View file

@ -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

View file

@ -22,7 +22,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator do
field(:content, ObjectValidators.SafeText)
field(:actor, ObjectValidators.ObjectID)
field(:published, ObjectValidators.DateTime)
field(:emoji, :map, default: %{})
field(:emoji, ObjectValidators.Emoji, default: %{})
embeds_one(:attachment, AttachmentValidator)
end

View file

@ -0,0 +1,31 @@
# 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.CommonFixes do
alias Pleroma.Object.Containment
alias Pleroma.Web.ActivityPub.Utils
# based on Pleroma.Web.ActivityPub.Utils.lazy_put_objects_defaults
def fix_defaults(data) do
%{data: %{"id" => context}, id: context_id} =
Utils.create_context(data["context"] || data["conversation"])
data
|> Map.put("context", context)
|> Map.put("context_id", context_id)
end
def fix_attribution(data) 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

View file

@ -9,7 +9,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
alias Pleroma.Object
alias Pleroma.User
def validate_recipients_presence(cng, fields \\ [:to, :cc]) do
def validate_any_presence(cng, fields) do
non_empty =
fields
|> Enum.map(fn field -> get_field(cng, field) end)
@ -24,7 +24,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
fields
|> Enum.reduce(cng, fn field, cng ->
cng
|> add_error(field, "no recipients in any field")
|> add_error(field, "none of #{inspect(fields)} present")
end)
end
end
@ -82,4 +82,60 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
if actor_cng.valid?, do: actor_cng, else: object_cng
end
def validate_host_match(cng, fields \\ [:id, :actor]) do
if same_domain?(cng, fields) do
cng
else
fields
|> Enum.reduce(cng, fn field, cng ->
cng
|> add_error(field, "hosts of #{inspect(fields)} aren't matching")
end)
end
end
def validate_fields_match(cng, fields) do
if map_unique?(cng, fields) do
cng
else
fields
|> Enum.reduce(cng, fn field, cng ->
cng
|> add_error(field, "Fields #{inspect(fields)} aren't matching")
end)
end
end
defp map_unique?(cng, fields, func \\ & &1) do
Enum.reduce_while(fields, nil, fn field, acc ->
value =
cng
|> get_field(field)
|> func.()
case {value, acc} do
{value, nil} -> {:cont, value}
{value, value} -> {:cont, value}
_ -> {:halt, false}
end
end)
end
def same_domain?(cng, fields \\ [:actor, :object]) do
map_unique?(cng, fields, fn value -> URI.parse(value).host end)
end
# This figures out if a user is able to create, delete or modify something
# based on the domain and superuser status
def validate_modification_rights(cng) do
actor = User.get_cached_by_ap_id(get_field(cng, :actor))
if User.superuser?(actor) || same_domain?(cng) do
cng
else
cng
|> add_error(:actor, "is not allowed to modify object")
end
end
end

View file

@ -0,0 +1,146 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
# Code based on CreateChatMessageValidator
# NOTES
# - doesn't embed, will only get the object id
defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator do
use Ecto.Schema
alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Object
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
import Ecto.Changeset
@primary_key false
embedded_schema do
field(:id, ObjectValidators.ObjectID, primary_key: true)
field(:actor, ObjectValidators.ObjectID)
field(:type, :string)
field(:to, ObjectValidators.Recipients, default: [])
field(:cc, ObjectValidators.Recipients, default: [])
field(:object, ObjectValidators.ObjectID)
field(:expires_at, ObjectValidators.DateTime)
# Should be moved to object, done for CommonAPI.Utils.make_context
field(:context, :string)
end
def cast_data(data, meta \\ []) do
data = fix(data, meta)
%__MODULE__{}
|> changeset(data)
end
def cast_and_apply(data) do
data
|> cast_data
|> apply_action(:insert)
end
def cast_and_validate(data, meta \\ []) do
data
|> cast_data(meta)
|> validate_data(meta)
end
def changeset(struct, data) do
struct
|> cast(data, __schema__(:fields))
end
defp fix_context(data, meta) do
if object = meta[:object_data] do
Map.put_new(data, "context", object["context"])
else
data
end
end
defp fix_addressing(data, meta) do
if object = meta[:object_data] do
data
|> Map.put_new("to", object["to"] || [])
|> Map.put_new("cc", object["cc"] || [])
else
data
end
end
defp fix(data, meta) 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"])
|> CommonValidations.validate_actor_presence()
|> CommonValidations.validate_any_presence([:to, :cc])
|> validate_actors_match(meta)
|> validate_context_match(meta)
|> validate_object_nonexistence()
|> validate_object_containment()
end
def validate_object_containment(cng) do
actor = get_field(cng, :actor)
cng
|> validate_change(:object, fn :object, object_id ->
%URI{host: object_id_host} = URI.parse(object_id)
%URI{host: actor_host} = URI.parse(actor)
if object_id_host == actor_host do
[]
else
[{:object, "The host of the object id doesn't match with the host of the actor"}]
end
end)
end
def validate_object_nonexistence(cng) do
cng
|> validate_change(:object, fn :object, object_id ->
if Object.get_cached_by_ap_id(object_id) do
[{:object, "The object to create already exists"}]
else
[]
end
end)
end
def validate_actors_match(cng, meta) do
attributed_to = meta[:object_data]["attributedTo"] || meta[:object_data]["actor"]
cng
|> validate_change(:actor, fn :actor, actor ->
if actor == attributed_to do
[]
else
[{:actor, "Actor doesn't match with object attributedTo"}]
end
end)
end
def validate_context_match(cng, %{object_data: %{"context" => object_context}}) do
cng
|> validate_change(:context, fn :context, context ->
if context == object_context do
[]
else
[{:context, "context field not matching between Create and object (#{object_context})"}]
end
end)
end
def validate_context_match(cng, _), do: cng
end

View file

@ -16,11 +16,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateNoteValidator do
field(:id, ObjectValidators.ObjectID, primary_key: true)
field(:actor, ObjectValidators.ObjectID)
field(:type, :string)
field(:to, {:array, :string})
field(:cc, {:array, :string})
field(:bto, {:array, :string}, default: [])
field(:bcc, {:array, :string}, default: [])
field(:to, ObjectValidators.Recipients, default: [])
field(:cc, ObjectValidators.Recipients, default: [])
field(:bto, ObjectValidators.Recipients, default: [])
field(:bcc, ObjectValidators.Recipients, default: [])
embeds_one(:object, NoteValidator)
end

View file

@ -7,7 +7,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator do
alias Pleroma.Activity
alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.User
import Ecto.Changeset
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
@ -59,7 +58,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator do
|> validate_required([:id, :type, :actor, :to, :cc, :object])
|> validate_inclusion(:type, ["Delete"])
|> validate_actor_presence()
|> validate_deletion_rights()
|> validate_modification_rights()
|> validate_object_or_user_presence(allowed_types: @deletable_types)
|> add_deleted_activity_id()
end
@ -68,31 +67,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator do
!same_domain?(cng)
end
defp same_domain?(cng) do
actor_uri =
cng
|> get_field(:actor)
|> URI.parse()
object_uri =
cng
|> get_field(:object)
|> URI.parse()
object_uri.host == actor_uri.host
end
def validate_deletion_rights(cng) do
actor = User.get_cached_by_ap_id(get_field(cng, :actor))
if User.superuser?(actor) || same_domain?(cng) do
cng
else
cng
|> add_error(:actor, "is not allowed to delete object")
end
end
def cast_and_validate(data) do
data
|> cast_data

View file

@ -20,8 +20,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
field(:actor, ObjectValidators.ObjectID)
field(:context, :string)
field(:content, :string)
field(:to, {:array, :string}, default: [])
field(:cc, {:array, :string}, default: [])
field(:to, ObjectValidators.Recipients, default: [])
field(:cc, ObjectValidators.Recipients, default: [])
end
def cast_and_validate(data) do

View file

@ -0,0 +1,97 @@
# 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.EventValidator do
use Ecto.Schema
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
# Extends from NoteValidator
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(data) do
data
|> CommonFixes.fix_defaults()
|> CommonFixes.fix_attribution()
|> Transmogrifier.fix_emoji()
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, ["Event"])
|> 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()
|> CommonValidations.validate_host_match()
end
end

View file

@ -1,63 +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
import Ecto.Changeset
@primary_key false
embedded_schema do
field(:id, ObjectValidators.ObjectID, primary_key: true)
field(:to, {:array, :string}, default: [])
field(:cc, {:array, :string}, default: [])
field(:bto, {:array, :string}, default: [])
field(:bcc, {:array, :string}, default: [])
# TODO: Write type
field(:tag, {:array, :map}, default: [])
field(:type, :string)
field(:content, :string)
field(:context, :string)
field(:actor, ObjectValidators.ObjectID)
field(:attributedTo, ObjectValidators.ObjectID)
field(:summary, :string)
field(:published, ObjectValidators.DateTime)
# TODO: Write type
field(:emoji, :map, 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(:inRepyTo, :string)
field(:uri, ObjectValidators.Uri)
field(:likes, {:array, :string}, default: [])
field(:announcements, {:array, :string}, default: [])
# see if needed
field(:context_id, :string)
end
def cast_and_validate(data) do
data
|> cast_data()
|> validate_data()
end
def cast_data(data) do
%__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

View file

@ -0,0 +1,37 @@
# 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.QuestionOptionsValidator do
use Ecto.Schema
import Ecto.Changeset
@primary_key false
embedded_schema do
field(:name, :string)
embeds_one :replies, Replies, primary_key: false do
field(:totalItems, :integer)
field(:type, :string)
end
field(:type, :string)
end
def changeset(struct, data) do
struct
|> cast(data, [:name, :type])
|> cast_embed(:replies, with: &replies_changeset/2)
|> validate_inclusion(:type, ["Note"])
|> validate_required([:name, :type])
end
def replies_changeset(struct, data) do
struct
|> cast(data, [:totalItems, :type])
|> validate_inclusion(:type, ["Collection"])
|> validate_required([:type])
end
end

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