diff options
Diffstat (limited to 'lib')
90 files changed, 1583 insertions, 908 deletions
diff --git a/lib/mix/pleroma.ex b/lib/mix/pleroma.ex index fe9b0d16c..49ba2aae4 100644 --- a/lib/mix/pleroma.ex +++ b/lib/mix/pleroma.ex @@ -18,6 +18,7 @@ defmodule Mix.Pleroma do @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 diff --git a/lib/mix/tasks/pleroma/benchmark.ex b/lib/mix/tasks/pleroma/benchmark.ex index dd2b9c8f2..a607d5d4f 100644 --- a/lib/mix/tasks/pleroma/benchmark.ex +++ b/lib/mix/tasks/pleroma/benchmark.ex @@ -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") diff --git a/lib/mix/tasks/pleroma/config.ex b/lib/mix/tasks/pleroma/config.ex index 904c5a74b..18f99318d 100644 --- a/lib/mix/tasks/pleroma/config.ex +++ b/lib/mix/tasks/pleroma/config.ex @@ -32,7 +32,8 @@ 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) diff --git a/lib/mix/tasks/pleroma/database.ex b/lib/mix/tasks/pleroma/database.ex index 7d8f00b08..a01c36ece 100644 --- a/lib/mix/tasks/pleroma/database.ex +++ b/lib/mix/tasks/pleroma/database.ex @@ -99,7 +99,7 @@ defmodule Mix.Tasks.Pleroma.Database do where: fragment("(?)->>'likes' is not null", object.data), select: %{id: object.id, likes: fragment("(?)->>'likes'", object.data)} ) - |> Pleroma.RepoStreamer.chunk_stream(100) + |> Pleroma.Repo.chunk_stream(100, :batches) |> Stream.each(fn objects -> ids = objects @@ -133,8 +133,7 @@ defmodule Mix.Tasks.Pleroma.Database do days = Pleroma.Config.get([:mrf_activity_expiration, :days], 365) Pleroma.Activity - |> join(:left, [a], u in assoc(a, :expiration)) - |> join(:inner, [a, _u], o in Object, + |> join(:inner, [a], o in Object, on: fragment( "(?->>'id') = COALESCE((?)->'object'->> 'id', (?)->>'object')", @@ -144,14 +143,20 @@ defmodule Mix.Tasks.Pleroma.Database do ) ) |> where(local: true) - |> where([a, u], is_nil(u)) |> where([a], fragment("(? ->> 'type'::text) = 'Create'", a.data)) - |> where([_a, _u, o], fragment("?->>'type' = 'Note'", o.data)) - |> Pleroma.RepoStreamer.chunk_stream(100) + |> 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 = Timex.shift(activity.inserted_at, days: days) - Pleroma.ActivityExpiration.create(activity, expires_at, false) + 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() diff --git a/lib/mix/tasks/pleroma/emoji.ex b/lib/mix/tasks/pleroma/emoji.ex index 8f52ee98d..1750373f9 100644 --- a/lib/mix/tasks/pleroma/emoji.ex +++ b/lib/mix/tasks/pleroma/emoji.ex @@ -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}") @@ -252,7 +252,7 @@ defmodule Mix.Tasks.Pleroma.Emoji do 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 @@ -271,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 diff --git a/lib/mix/tasks/pleroma/frontend.ex b/lib/mix/tasks/pleroma/frontend.ex index 484af6da7..cbce81ab9 100644 --- a/lib/mix/tasks/pleroma/frontend.ex +++ b/lib/mix/tasks/pleroma/frontend.ex @@ -69,7 +69,7 @@ defmodule Mix.Tasks.Pleroma.Frontend do fe_label = "#{frontend} (#{ref})" - tmp_dir = Path.join(dest, "tmp") + tmp_dir = Path.join([instance_static_dir, "frontends", "tmp"]) with {_, :ok} <- {:download_or_unzip, download_or_unzip(frontend_info, tmp_dir, options[:file])}, @@ -124,9 +124,7 @@ defmodule Mix.Tasks.Pleroma.Frontend do url = String.replace(frontend_info["build_url"], "${ref}", frontend_info["ref"]) with {:ok, %{status: 200, body: zip_body}} <- - Pleroma.HTTP.get(url, [], - adapter: [pool: :media, timeout: 120_000, recv_timeout: 120_000] - ) do + Pleroma.HTTP.get(url, [], pool: :media, recv_timeout: 120_000) do unzip(zip_body, dest) else e -> {:error, e} @@ -135,6 +133,7 @@ defmodule Mix.Tasks.Pleroma.Frontend do 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 diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index 01824aa18..b20c49d89 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -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 -> @@ -370,7 +370,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 -> diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index 97feebeaa..17af04257 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -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) diff --git a/lib/pleroma/activity_expiration.ex b/lib/pleroma/activity_expiration.ex deleted file mode 100644 index 955f0578e..000000000 --- a/lib/pleroma/activity_expiration.ex +++ /dev/null @@ -1,74 +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, validate_scheduled_at) do - expiration - |> cast(attrs, [:scheduled_at]) - |> validate_required([:scheduled_at]) - |> validate_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, validate_scheduled_at \\ true) do - %ActivityExpiration{activity_id: activity.id} - |> changeset(%{scheduled_at: scheduled_at}, validate_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) - |> limit(50) - |> preload(:activity) - |> Repo.all() - |> Enum.reject(fn %{activity: activity} -> - Activity.pinned_by_actor?(activity) - end) - end - - def validate_scheduled_at(changeset, false), do: changeset - - def validate_scheduled_at(changeset, true) 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 diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index c0b5db9f1..c39e24919 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -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 - - custom -> - custom + 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 + 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!() diff --git a/lib/pleroma/chat.ex b/lib/pleroma/chat.ex index 24a86371e..84f8806a0 100644 --- a/lib/pleroma/chat.ex +++ b/lib/pleroma/chat.ex @@ -6,7 +6,9 @@ defmodule Pleroma.Chat do use Ecto.Schema import Ecto.Changeset + import Ecto.Query + alias Pleroma.Chat alias Pleroma.Repo alias Pleroma.User @@ -69,4 +71,12 @@ 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 diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex index 0f52eb210..98c4dc9c8 100644 --- a/lib/pleroma/config/deprecation_warnings.ex +++ b/lib/pleroma/config/deprecation_warnings.ex @@ -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,6 +26,10 @@ defmodule Pleroma.Config.DeprecationWarnings do !!!DEPRECATION WARNING!!! You are using the old configuration mechanism for the hellthread filter. Please check config.md. """) + + :error + else + :ok end end @@ -47,15 +51,26 @@ defmodule Pleroma.Config.DeprecationWarnings do config :pleroma, :mrf_user_allowlist, #{inspect(rewritten, pretty: true)} """) + + :error + else + :ok end end def warn do - check_hellthread_threshold() - mrf_user_allowlist() - check_old_mrf_config() - check_media_proxy_whitelist_config() - check_welcome_message_config() + with :ok <- check_hellthread_threshold(), + :ok <- mrf_user_allowlist(), + :ok <- check_old_mrf_config(), + :ok <- check_media_proxy_whitelist_config(), + :ok <- check_welcome_message_config(), + :ok <- check_gun_pool_options(), + :ok <- check_activity_expiration_config() do + :ok + else + _ -> + :error + end end def check_welcome_message_config do @@ -72,6 +87,10 @@ defmodule Pleroma.Config.DeprecationWarnings do \n* `config :pleroma, :instance, welcome_user_nickname` is now `config :pleroma, :welcome, :direct_message, :sender_nickname` \n* `config :pleroma, :instance, welcome_message` is now `config :pleroma, :welcome, :direct_message, :message` """) + + :error + else + :ok end end @@ -99,8 +118,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 +135,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 name `await_up_timeout` instead of `connect_timeout`. Setting should work for now, but you are advised to change format to scheme with port to prevent possible issues later. + """) + + 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 diff --git a/lib/pleroma/config/oban.ex b/lib/pleroma/config/oban.ex new file mode 100644 index 000000000..9f601b1a3 --- /dev/null +++ b/lib/pleroma/config/oban.ex @@ -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 diff --git a/lib/pleroma/ecto_type/activity_pub/object_validators/emoji.ex b/lib/pleroma/ecto_type/activity_pub/object_validators/emoji.ex new file mode 100644 index 000000000..4aacc5c88 --- /dev/null +++ b/lib/pleroma/ecto_type/activity_pub/object_validators/emoji.ex @@ -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 diff --git a/lib/pleroma/gun/conn.ex b/lib/pleroma/gun/conn.ex index a3f75a4bb..477e19c6e 100644 --- a/lib/pleroma/gun/conn.ex +++ b/lib/pleroma/gun/conn.ex @@ -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( diff --git a/lib/pleroma/gun/connection_pool/worker.ex b/lib/pleroma/gun/connection_pool/worker.ex index c36332817..bf57e9e5f 100644 --- a/lib/pleroma/gun/connection_pool/worker.ex +++ b/lib/pleroma/gun/connection_pool/worker.ex @@ -15,7 +15,7 @@ defmodule Pleroma.Gun.ConnectionPool.Worker do @impl true def handle_continue({:connect, [key, uri, opts, client_pid]}, _) do - with {:ok, conn_pid} <- Gun.Conn.open(uri, opts), + with {:ok, conn_pid, protocol} <- Gun.Conn.open(uri, opts), Process.link(conn_pid) do time = :erlang.monotonic_time(:millisecond) @@ -27,8 +27,12 @@ defmodule Pleroma.Gun.ConnectionPool.Worker do send(client_pid, {:conn_pid, conn_pid}) {:noreply, - %{key: key, timer: nil, client_monitors: %{client_pid => Process.monitor(client_pid)}}, - :hibernate} + %{ + key: key, + timer: nil, + client_monitors: %{client_pid => Process.monitor(client_pid)}, + protocol: protocol + }, :hibernate} else err -> {:stop, {:shutdown, err}, nil} @@ -53,14 +57,20 @@ defmodule Pleroma.Gun.ConnectionPool.Worker do end @impl true - def handle_call(:add_client, {client_pid, _}, %{key: key} = state) do + def handle_call(:add_client, {client_pid, _}, %{key: key, protocol: protocol} = state) do time = :erlang.monotonic_time(:millisecond) - {{conn_pid, _, _, _}, _} = + {{conn_pid, used_by, _, _}, _} = Registry.update_value(@registry, key, fn {conn_pid, used_by, crf, last_reference} -> {conn_pid, [client_pid | used_by], crf(time - last_reference, crf), time} end) + :telemetry.execute( + [:pleroma, :connection_pool, :client, :add], + %{client_pid: client_pid, clients: used_by}, + %{key: state.key, protocol: protocol} + ) + state = if state.timer != nil do Process.cancel_timer(state[:timer]) @@ -83,25 +93,18 @@ defmodule Pleroma.Gun.ConnectionPool.Worker do end) {ref, state} = pop_in(state.client_monitors[client_pid]) - # DOWN message can receive right after `remove_client` call and cause worker to terminate - state = - if is_nil(ref) do - state - else - Process.demonitor(ref) - timer = - if used_by == [] do - max_idle = Pleroma.Config.get([:connections_pool, :max_idle_time], 30_000) - Process.send_after(self(), :idle_close, max_idle) - else - nil - end + Process.demonitor(ref, [:flush]) - %{state | timer: timer} + timer = + if used_by == [] do + max_idle = Pleroma.Config.get([:connections_pool, :max_idle_time], 30_000) + Process.send_after(self(), :idle_close, max_idle) + else + nil end - {:reply, :ok, state, :hibernate} + {:reply, :ok, %{state | timer: timer}, :hibernate} end @impl true @@ -131,7 +134,7 @@ defmodule Pleroma.Gun.ConnectionPool.Worker do @impl true def handle_info({:DOWN, _ref, :process, pid, reason}, state) do :telemetry.execute( - [:pleroma, :connection_pool, :client_death], + [:pleroma, :connection_pool, :client, :dead], %{client_pid: pid, reason: reason}, %{key: state.key} ) diff --git a/lib/pleroma/http/adapter_helper.ex b/lib/pleroma/http/adapter_helper.ex index 0728cbaa2..08b51578a 100644 --- a/lib/pleroma/http/adapter_helper.ex +++ b/lib/pleroma/http/adapter_helper.ex @@ -6,7 +6,7 @@ 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() @@ -19,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 @@ -47,9 +46,6 @@ defmodule Pleroma.HTTP.AdapterHelper do |> adapter_helper().options(uri) end - @spec get_conn(URI.t(), keyword()) :: {:ok, keyword()} | {:error, atom()} - def get_conn(uri, opts), do: adapter_helper().get_conn(uri, opts) - defp adapter, do: Application.get_env(:tesla, :adapter) defp adapter_helper do diff --git a/lib/pleroma/http/adapter_helper/gun.ex b/lib/pleroma/http/adapter_helper/gun.ex index 02e20f2d1..1dbb71362 100644 --- a/lib/pleroma/http/adapter_helper/gun.ex +++ b/lib/pleroma/http/adapter_helper/gun.ex @@ -6,18 +6,13 @@ defmodule Pleroma.HTTP.AdapterHelper.Gun do @behaviour Pleroma.HTTP.AdapterHelper alias Pleroma.Config - alias Pleroma.Gun.ConnectionPool alias Pleroma.HTTP.AdapterHelper require Logger @defaults [ - connect_timeout: 5_000, - domain_lookup_timeout: 5_000, - tls_handshake_timeout: 5_000, retry: 1, - retry_timeout: 1000, - await_up_timeout: 5_000 + retry_timeout: 1_000 ] @type pool() :: :federation | :upload | :media | :default @@ -46,23 +41,17 @@ defmodule Pleroma.HTTP.AdapterHelper.Gun do 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 - Keyword.put_new(opts, :timeout, pool_timeout(opts[:pool])) + # `: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, :timeout], 5_000) + default = Config.get([:pools, :default, :recv_timeout], 5_000) - Config.get([:pools, pool, :timeout], default) - 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 + Config.get([:pools, pool, :recv_timeout], default) end @prefix Pleroma.Gun.ConnectionPool diff --git a/lib/pleroma/http/adapter_helper/hackney.ex b/lib/pleroma/http/adapter_helper/hackney.ex index 62bd42485..ef84553c1 100644 --- a/lib/pleroma/http/adapter_helper/hackney.ex +++ b/lib/pleroma/http/adapter_helper/hackney.ex @@ -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,6 +16,7 @@ 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 @@ -28,6 +26,11 @@ defmodule Pleroma.HTTP.AdapterHelper.Hackney do 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 diff --git a/lib/pleroma/http/ex_aws.ex b/lib/pleroma/http/ex_aws.ex index c3f335c73..5cac3532f 100644 --- a/lib/pleroma/http/ex_aws.ex +++ b/lib/pleroma/http/ex_aws.ex @@ -11,7 +11,7 @@ defmodule Pleroma.HTTP.ExAws do @impl true def request(method, url, body \\ "", headers \\ [], http_opts \\ []) do - http_opts = Keyword.put_new(http_opts, :adapter, pool: :upload) + http_opts = Keyword.put_new(http_opts, :pool, :upload) case HTTP.request(method, url, body, headers, http_opts) do {:ok, env} -> diff --git a/lib/pleroma/http/http.ex b/lib/pleroma/http/http.ex index b37b3fa89..052597191 100644 --- a/lib/pleroma/http/http.ex +++ b/lib/pleroma/http/http.ex @@ -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] || []) - - 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) - - adapter = Application.get_env(:tesla, :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 + adapter_opts = AdapterHelper.options(uri, options || []) + + 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) + + client = Tesla.client(adapter_middlewares(adapter), adapter) + + 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: [] diff --git a/lib/pleroma/http/tzdata.ex b/lib/pleroma/http/tzdata.ex index 4539ac359..09cfdadf7 100644 --- a/lib/pleroma/http/tzdata.ex +++ b/lib/pleroma/http/tzdata.ex @@ -11,7 +11,7 @@ defmodule Pleroma.HTTP.Tzdata do @impl true def get(url, headers, options) do - options = Keyword.put_new(options, :adapter, pool: :default) + 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}} @@ -20,7 +20,7 @@ defmodule Pleroma.HTTP.Tzdata do @impl true def head(url, headers, options) do - options = Keyword.put_new(options, :adapter, pool: :default) + options = Keyword.put_new(options, :pool, :default) with {:ok, %Tesla.Env{} = env} <- HTTP.head(url, headers, options) do {:ok, {env.status, env.headers}} diff --git a/lib/pleroma/instances/instance.ex b/lib/pleroma/instances/instance.ex index 8bf53c090..6948651c7 100644 --- a/lib/pleroma/instances/instance.ex +++ b/lib/pleroma/instances/instance.ex @@ -159,13 +159,11 @@ defmodule Pleroma.Instances.Instance do Pleroma.HTTP.get(to_string(instance_uri), [{"accept", "text/html"}], adapter: [pool: :media] ), - favicon_rel <- - html - |> Floki.parse_document!() - |> Floki.attribute("link[rel=icon]", "href") - |> List.first(), - favicon <- URI.merge(instance_uri, favicon_rel) |> to_string(), - true <- is_binary(favicon) do + {_, [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 diff --git a/lib/pleroma/mfa/token.ex b/lib/pleroma/mfa/token.ex index 0b2449971..69b64c0e8 100644 --- a/lib/pleroma/mfa/token.ex +++ b/lib/pleroma/mfa/token.ex @@ -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 + + @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 - def create_token(user, authorization) do + 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 diff --git a/lib/pleroma/migration_helper/notification_backfill.ex b/lib/pleroma/migration_helper/notification_backfill.ex index d260e62ca..24f4733fe 100644 --- a/lib/pleroma/migration_helper/notification_backfill.ex +++ b/lib/pleroma/migration_helper/notification_backfill.ex @@ -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}" diff --git a/lib/pleroma/moderation_log.ex b/lib/pleroma/moderation_log.ex index 31c9afe2a..47036a6f6 100644 --- a/lib/pleroma/moderation_log.ex +++ b/lib/pleroma/moderation_log.ex @@ -320,6 +320,19 @@ 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}") diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index c1825f810..8868a910e 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -648,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 diff --git a/lib/pleroma/object/containment.ex b/lib/pleroma/object/containment.ex index bc88e8a0c..29cb3d672 100644 --- a/lib/pleroma/object/containment.ex +++ b/lib/pleroma/object/containment.ex @@ -44,13 +44,6 @@ 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 diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index 374d8704a..24dc7cb95 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -36,8 +36,7 @@ defmodule Pleroma.Object.Fetcher do defp reinject_object(%Object{data: %{"type" => "Question"}} = 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), + with data <- maybe_reinject_internal_fields(object, new_data), {:ok, data, _} <- ObjectValidator.validate(data, %{}), changeset <- Object.change(object, %{data: data}), changeset <- touch_changeset(changeset), @@ -99,8 +98,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} diff --git a/lib/pleroma/repo.ex b/lib/pleroma/repo.ex index f317e4d58..4524bd5e2 100644 --- a/lib/pleroma/repo.ex +++ b/lib/pleroma/repo.ex @@ -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 diff --git a/lib/pleroma/repo_streamer.ex b/lib/pleroma/repo_streamer.ex deleted file mode 100644 index cb4d7bb7a..000000000 --- a/lib/pleroma/repo_streamer.ex +++ /dev/null @@ -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 diff --git a/lib/pleroma/reverse_proxy/client/tesla.ex b/lib/pleroma/reverse_proxy/client/tesla.ex index d5a339681..4b118eec2 100644 --- a/lib/pleroma/reverse_proxy/client/tesla.ex +++ b/lib/pleroma/reverse_proxy/client/tesla.ex @@ -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} diff --git a/lib/pleroma/stats.ex b/lib/pleroma/stats.ex index 9a03f01db..e5c9c668b 100644 --- a/lib/pleroma/stats.ex +++ b/lib/pleroma/stats.ex @@ -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 diff --git a/lib/pleroma/telemetry/logger.ex b/lib/pleroma/telemetry/logger.ex index 4cacae02f..197b1d091 100644 --- a/lib/pleroma/telemetry/logger.ex +++ b/lib/pleroma/telemetry/logger.ex @@ -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 diff --git a/lib/pleroma/tesla/middleware/connection_pool.ex b/lib/pleroma/tesla/middleware/connection_pool.ex new file mode 100644 index 000000000..056e736ce --- /dev/null +++ b/lib/pleroma/tesla/middleware/connection_pool.ex @@ -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 diff --git a/lib/pleroma/tesla/middleware/follow_redirects.ex b/lib/pleroma/tesla/middleware/follow_redirects.ex deleted file mode 100644 index 5a7032215..000000000 --- a/lib/pleroma/tesla/middleware/follow_redirects.ex +++ /dev/null @@ -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 diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index ce5f4bc4a..1ffe60dfc 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -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) @@ -276,9 +275,9 @@ defmodule Pleroma.User do @spec account_status(User.t()) :: account_status() def account_status(%User{deactivated: true}), do: :deactivated def account_status(%User{password_reset_pending: true}), do: :password_reset_pending - def account_status(%User{approval_pending: true}), do: :approval_pending + def account_status(%User{local: true, approval_pending: true}), do: :approval_pending - def account_status(%User{confirmation_pending: true}) do + def account_status(%User{local: true, confirmation_pending: true}) do if Config.get([:instance, :account_activation_required]) do :confirmation_pending else @@ -1587,7 +1586,7 @@ defmodule Pleroma.User do # "Right to be forgotten" # https://gdpr.eu/right-to-be-forgotten/ change(user, %{ - bio: nil, + bio: "", raw_bio: nil, email: nil, name: nil, @@ -1775,7 +1774,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) @@ -2315,6 +2314,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, @@ -2327,9 +2331,19 @@ 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 + {:ok, expires_at, _} = DateTime.from_iso8601(data["expires_at"]) + + Pleroma.Workers.PurgeExpiredActivity.enqueue(%{ + activity_id: id, + expires_at: expires_at + }) + end + user |> cast(params, [:pinned_activities]) |> update_and_set_cache() diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex index 7babd47ea..b8c648672 100644 --- a/lib/pleroma/user/search.ex +++ b/lib/pleroma/user/search.ex @@ -52,6 +52,7 @@ defmodule Pleroma.User.Search do |> base_query(following) |> filter_blocked_user(for_user) |> filter_invisible_users() + |> filter_discoverable_users() |> filter_internal_users() |> filter_blocked_domains(for_user) |> fts_search(query_string) @@ -122,6 +123,10 @@ defmodule Pleroma.User.Search do from(q in query, where: q.invisible == false) end + defp filter_discoverable_users(query) do + from(q in query, where: q.discoverable == true) + end + defp filter_internal_users(query) do from(q in query, where: q.actor_type != "Application") end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index e4eafc8ac..92fc1e422 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -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 @@ -85,7 +84,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp increase_replies_count_if_reply(_create_data), do: :noop - @object_types ~w[ChatMessage Question Answer Audio Event] + @object_types ~w[ChatMessage Question Answer Audio Video Event Article] @spec persist(map(), keyword()) :: {:ok, Activity.t() | Object.t()} def persist(%{"type" => type} = object, meta) when type in @object_types do with {:ok, object} <- Object.create(object) do @@ -102,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 @@ -111,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) @@ -138,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, @@ -150,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 @@ -164,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), @@ -1224,7 +1247,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, diff --git a/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex b/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex index 7b4c78e0f..bee47b4ed 100644 --- a/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex @@ -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) diff --git a/lib/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy.ex b/lib/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy.ex new file mode 100644 index 000000000..ea9c3d3f5 --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy.ex @@ -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 diff --git a/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex b/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex index dfab105a3..98d595469 100644 --- a/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex @@ -13,22 +13,16 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do require Logger @options [ - pool: :media + pool: :media, + recv_timeout: 10_000 ] def perform(:prefetch, url) do Logger.debug("Prefetching #{inspect(url)}") - opts = - if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do - Keyword.put(@options, :recv_timeout, 10_000) - else - @options - end - url |> MediaProxy.url() - |> HTTP.get([], adapter: opts) + |> HTTP.get([], @options) end def perform(:preload, %{"object" => %{"attachment" => attachments}} = _message) do diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex index b77c06395..bd0a2a8dc 100644 --- a/lib/pleroma/web/activity_pub/object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@ -12,11 +12,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do alias Pleroma.Activity alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.Object + alias Pleroma.Object.Containment alias Pleroma.User alias Pleroma.Web.ActivityPub.ObjectValidators.AcceptRejectValidator alias Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator alias Pleroma.Web.ActivityPub.ObjectValidators.AnswerValidator - alias Pleroma.Web.ActivityPub.ObjectValidators.AudioValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator alias Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator @@ -149,10 +151,20 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do end end - def validate(%{"type" => "Audio"} = object, meta) do + def validate(%{"type" => type} = object, meta) when type in ~w[Audio Video] do with {:ok, object} <- object - |> AudioValidator.cast_and_validate() + |> AudioVideoValidator.cast_and_validate() + |> Ecto.Changeset.apply_action(:insert) do + object = stringify_keys(object) + {:ok, object, meta} + end + end + + def validate(%{"type" => "Article"} = object, meta) do + with {:ok, object} <- + object + |> ArticleNoteValidator.cast_and_validate() |> Ecto.Changeset.apply_action(:insert) do object = stringify_keys(object) {:ok, object, meta} @@ -198,7 +210,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do %{"type" => "Create", "object" => %{"type" => objtype} = object} = create_activity, meta ) - when objtype in ~w[Question Answer Audio Event] do + when objtype in ~w[Question Answer Audio Video Event Article] do with {:ok, object_data} <- cast_and_apply(object), meta = Keyword.put(meta, :object_data, object_data |> stringify_keys), {:ok, create_activity} <- @@ -232,14 +244,18 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do AnswerValidator.cast_and_apply(object) end - def cast_and_apply(%{"type" => "Audio"} = object) do - AudioValidator.cast_and_apply(object) + def cast_and_apply(%{"type" => type} = object) when type in ~w[Audio Video] do + AudioVideoValidator.cast_and_apply(object) end def cast_and_apply(%{"type" => "Event"} = object) do EventValidator.cast_and_apply(object) end + def cast_and_apply(%{"type" => "Article"} = object) do + ArticleNoteValidator.cast_and_apply(object) + end + def cast_and_apply(o), do: {:error, {:validator_not_set, o}} # is_struct/1 isn't present in Elixir 1.8.x @@ -262,7 +278,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do def stringify_keys(object), do: object def fetch_actor(object) do - with {:ok, actor} <- ObjectValidators.ObjectID.cast(object["actor"]) do + with actor <- Containment.get_actor(object), + {:ok, actor} <- ObjectValidators.ObjectID.cast(actor) do User.get_or_fetch_by_ap_id(actor) end end diff --git a/lib/pleroma/web/activity_pub/object_validators/audio_validator.ex b/lib/pleroma/web/activity_pub/object_validators/article_note_validator.ex index d1869f188..5b7dad517 100644 --- a/lib/pleroma/web/activity_pub/object_validators/audio_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/article_note_validator.ex @@ -2,13 +2,14 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioValidator do +defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidator do use Ecto.Schema alias Pleroma.EctoType.ActivityPub.ObjectValidators 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 @@ -24,29 +25,31 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioValidator do # TODO: Write type field(:tag, {:array, :map}, default: []) field(:type, :string) + + field(:name, :string) + field(:summary, :string) field(:content, :string) + field(:context, :string) + # short identifier for PleromaFE to group statuses by context + field(:context_id, :integer) # TODO: Remove actor on objects field(:actor, ObjectValidators.ObjectID) field(:attributedTo, ObjectValidators.ObjectID) - field(:summary, :string) field(:published, ObjectValidators.DateTime) - # TODO: Write type - field(:emoji, :map, default: %{}) + 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, :string) + field(:inReplyTo, ObjectValidators.ObjectID) field(:url, ObjectValidators.Uri) - # short identifier for PleromaFE to group statuses by context - field(:context_id, :integer) - field(:likes, {:array, :string}, default: []) - field(:announcements, {:array, :string}, default: []) + field(:likes, {:array, ObjectValidators.ObjectID}, default: []) + field(:announcements, {:array, ObjectValidators.ObjectID}, default: []) end def cast_and_apply(data) do @@ -62,19 +65,14 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioValidator do end def cast_data(data) do + data = fix(data) + %__MODULE__{} |> changeset(data) end - defp fix_url(%{"url" => url} = data) when is_list(url) do - attachment = - Enum.find(url, fn x -> is_map(x) and String.starts_with?(x["mimeType"], "audio/") end) - - link_element = Enum.find(url, fn x -> is_map(x) and x["mimeType"] == "text/html" end) - - data - |> Map.put("attachment", [attachment]) - |> Map.put("url", link_element["href"]) + defp fix_url(%{"url" => url} = data) when is_map(url) do + Map.put(data, "url", url["href"]) end defp fix_url(data), do: data @@ -83,7 +81,9 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioValidator do data |> CommonFixes.fix_defaults() |> CommonFixes.fix_attribution() + |> CommonFixes.fix_actor() |> fix_url() + |> Transmogrifier.fix_emoji() end def changeset(struct, data) do @@ -96,8 +96,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioValidator do def validate_data(data_cng) do data_cng - |> validate_inclusion(:type, ["Audio"]) - |> validate_required([:id, :actor, :attributedTo, :type, :context, :attachment]) + |> validate_inclusion(:type, ["Article", "Note"]) + |> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id]) |> CommonValidations.validate_any_presence([:cc, :to]) |> CommonValidations.validate_fields_match([:actor, :attributedTo]) |> CommonValidations.validate_actor_presence() diff --git a/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex index c8b148280..df102a134 100644 --- a/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do use Ecto.Schema + alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.Web.ActivityPub.ObjectValidators.UrlObjectValidator import Ecto.Changeset @@ -15,7 +16,11 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do field(:mediaType, :string, default: "application/octet-stream") field(:name, :string) - embeds_many(:url, UrlObjectValidator) + embeds_many :url, UrlObjectValidator, primary_key: false do + field(:type, :string) + field(:href, ObjectValidators.Uri) + field(:mediaType, :string, default: "application/octet-stream") + end end def cast_and_validate(data) do @@ -37,7 +42,18 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do struct |> cast(data, [:type, :mediaType, :name]) - |> cast_embed(:url, required: true) + |> cast_embed(:url, with: &url_changeset/2) + |> validate_inclusion(:type, ~w[Link Document Audio Image Video]) + |> validate_required([:type, :mediaType, :url]) + end + + def url_changeset(struct, data) do + data = fix_media_type(data) + + struct + |> cast(data, [:type, :href, :mediaType]) + |> validate_inclusion(:type, ["Link"]) + |> validate_required([:type, :href, :mediaType]) end def fix_media_type(data) do @@ -75,6 +91,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do def validate_data(cng) do cng + |> validate_inclusion(:type, ~w[Document Audio Image Video]) |> validate_required([:mediaType, :url, :type]) end end diff --git a/lib/pleroma/web/activity_pub/object_validators/audio_video_validator.ex b/lib/pleroma/web/activity_pub/object_validators/audio_video_validator.ex new file mode 100644 index 000000000..16973e5db --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/audio_video_validator.ex @@ -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 diff --git a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex index 91b475393..6acd4a771 100644 --- a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex @@ -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 diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex index 721749de0..b3638cfc7 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex @@ -3,6 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do + alias Pleroma.Object.Containment alias Pleroma.Web.ActivityPub.Utils # based on Pleroma.Web.ActivityPub.Utils.lazy_put_objects_defaults @@ -11,12 +12,20 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do Utils.create_context(data["context"] || data["conversation"]) data - |> Map.put_new("context", context) - |> Map.put_new("context_id", context_id) + |> 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 diff --git a/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex index b3dbeea57..422ee07be 100644 --- a/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex @@ -10,9 +10,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator do alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.Object + alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes + alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations import Ecto.Changeset - import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations @primary_key false @@ -75,14 +76,15 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator do data |> fix_context(meta) |> fix_addressing(meta) + |> CommonFixes.fix_actor() end def validate_data(cng, meta \\ []) do cng |> validate_required([:actor, :type, :object]) |> validate_inclusion(:type, ["Create"]) - |> validate_actor_presence() - |> validate_any_presence([:to, :cc]) + |> CommonValidations.validate_actor_presence() + |> CommonValidations.validate_any_presence([:to, :cc]) |> validate_actors_match(meta) |> validate_context_match(meta) |> validate_object_nonexistence() diff --git a/lib/pleroma/web/activity_pub/object_validators/event_validator.ex b/lib/pleroma/web/activity_pub/object_validators/event_validator.ex index 07e4821a4..0b4c99dc0 100644 --- a/lib/pleroma/web/activity_pub/object_validators/event_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/event_validator.ex @@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do 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 @@ -39,8 +40,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do field(:attributedTo, ObjectValidators.ObjectID) field(:published, ObjectValidators.DateTime) - # TODO: Write type - field(:emoji, :map, default: %{}) + field(:emoji, ObjectValidators.Emoji, default: %{}) field(:sensitive, :boolean, default: false) embeds_many(:attachment, AttachmentValidator) field(:replies_count, :integer, default: 0) @@ -74,6 +74,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do data |> CommonFixes.fix_defaults() |> CommonFixes.fix_attribution() + |> Transmogrifier.fix_emoji() end def changeset(struct, data) do diff --git a/lib/pleroma/web/activity_pub/object_validators/note_validator.ex b/lib/pleroma/web/activity_pub/object_validators/note_validator.ex deleted file mode 100644 index 20e735619..000000000 --- a/lib/pleroma/web/activity_pub/object_validators/note_validator.ex +++ /dev/null @@ -1,66 +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, ObjectValidators.Recipients, default: []) - field(:cc, ObjectValidators.Recipients, default: []) - field(:bto, ObjectValidators.Recipients, default: []) - field(:bcc, ObjectValidators.Recipients, default: []) - # TODO: Write type - field(:tag, {:array, :map}, default: []) - field(:type, :string) - - field(:name, :string) - field(:summary, :string) - field(:content, :string) - - field(:context, :string) - # short identifier for PleromaFE to group statuses by context - field(:context_id, :integer) - - field(:actor, ObjectValidators.ObjectID) - field(:attributedTo, ObjectValidators.ObjectID) - field(:published, ObjectValidators.DateTime) - # 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(:inReplyTo, ObjectValidators.ObjectID) - field(:url, ObjectValidators.Uri) - - field(:likes, {:array, :string}, default: []) - field(:announcements, {:array, :string}, default: []) - end - - def cast_and_validate(data) do - data - |> cast_data() - |> validate_data() - end - - 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 diff --git a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex index 712047424..9310485dc 100644 --- a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex @@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations alias Pleroma.Web.ActivityPub.ObjectValidators.QuestionOptionsValidator + alias Pleroma.Web.ActivityPub.Transmogrifier import Ecto.Changeset @@ -35,8 +36,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do field(:attributedTo, ObjectValidators.ObjectID) field(:summary, :string) field(:published, ObjectValidators.DateTime) - # TODO: Write type - field(:emoji, :map, default: %{}) + field(:emoji, ObjectValidators.Emoji, default: %{}) field(:sensitive, :boolean, default: false) embeds_many(:attachment, AttachmentValidator) field(:replies_count, :integer, default: 0) @@ -47,8 +47,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do # short identifier for PleromaFE to group statuses by context field(:context_id, :integer) - field(:likes, {:array, :string}, default: []) - field(:announcements, {:array, :string}, default: []) + field(:likes, {:array, ObjectValidators.ObjectID}, default: []) + field(:announcements, {:array, ObjectValidators.ObjectID}, default: []) field(:closed, ObjectValidators.DateTime) field(:voters, {:array, ObjectValidators.ObjectID}, default: []) @@ -85,6 +85,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do data |> CommonFixes.fix_defaults() |> CommonFixes.fix_attribution() + |> Transmogrifier.fix_emoji() |> fix_closed() end diff --git a/lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex b/lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex deleted file mode 100644 index 881030f38..000000000 --- a/lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex +++ /dev/null @@ -1,24 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectValidators.UrlObjectValidator do - use Ecto.Schema - - alias Pleroma.EctoType.ActivityPub.ObjectValidators - - import Ecto.Changeset - @primary_key false - - embedded_schema do - field(:type, :string) - field(:href, ObjectValidators.Uri) - field(:mediaType, :string, default: "application/octet-stream") - end - - def changeset(struct, data) do - struct - |> cast(data, __schema__(:fields)) - |> validate_required([:type, :href, :mediaType]) - end -end diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index a5e2323bd..b9a83a544 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -7,7 +7,6 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do """ alias Pleroma.Activity alias Pleroma.Activity.Ir.Topics - alias Pleroma.ActivityExpiration alias Pleroma.Chat alias Pleroma.Chat.MessageReference alias Pleroma.FollowingRelationship @@ -188,10 +187,6 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do Object.increase_replies_count(in_reply_to) end - if expires_at = activity.data["expires_at"] do - ActivityExpiration.create(activity, expires_at) - end - BackgroundWorker.enqueue("fetch_data_for_activity", %{"activity_id" => activity.id}) meta = @@ -341,7 +336,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do end def handle_object_creation(%{"type" => objtype} = object, meta) - when objtype in ~w[Audio Question Event] do + when objtype in ~w[Audio Video Question Event Article] do with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do {:ok, object, meta} end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 63dd227c1..fcca014f0 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -7,7 +7,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do A module to handle coding from internal to wire ActivityPub and back. """ alias Pleroma.Activity - alias Pleroma.EarmarkRenderer alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.Maps alias Pleroma.Object @@ -45,7 +44,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do |> fix_addressing |> fix_summary |> fix_type(options) - |> fix_content end def fix_summary(%{"summary" => nil} = object) do @@ -168,7 +166,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options) when not is_nil(in_reply_to) do in_reply_to_id = prepare_in_reply_to(in_reply_to) - object = Map.put(object, "inReplyToAtomUri", in_reply_to_id) depth = (options[:depth] || 0) + 1 if Federator.allowed_thread_distance?(depth) do @@ -176,9 +173,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do %Activity{} <- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do object |> Map.put("inReplyTo", replied_object.data["id"]) - |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id) |> Map.put("context", replied_object.data["context"] || object["conversation"]) - |> Map.drop(["conversation"]) + |> Map.drop(["conversation", "inReplyToAtomUri"]) else e -> Logger.warn("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}") @@ -276,24 +272,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do Map.put(object, "url", url["href"]) end - def fix_url(%{"type" => "Video", "url" => url} = object) when is_list(url) do - attachment = - Enum.find(url, fn x -> - media_type = x["mediaType"] || x["mimeType"] || "" - - is_map(x) and String.starts_with?(media_type, "video/") - end) - - link_element = - Enum.find(url, fn x -> is_map(x) and (x["mediaType"] || x["mimeType"]) == "text/html" end) - - object - |> Map.put("attachment", [attachment]) - |> Map.put("url", link_element["href"]) - end - - def fix_url(%{"type" => object_type, "url" => url} = object) - when object_type != "Video" and is_list(url) do + def fix_url(%{"url" => url} = object) when is_list(url) do first_element = Enum.at(url, 0) url_string = @@ -318,9 +297,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do Map.put(mapping, name, data["icon"]["url"]) end) - # we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats - emoji = Map.merge(object["emoji"] || %{}, emoji) - Map.put(object, "emoji", emoji) end @@ -376,18 +352,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def fix_type(object, _), do: object - defp fix_content(%{"mediaType" => "text/markdown", "content" => content} = object) - when is_binary(content) do - html_content = - content - |> Earmark.as_html!(%Earmark.Options{renderer: EarmarkRenderer}) - |> Pleroma.HTML.filter_tags() - - Map.merge(object, %{"content" => html_content, "mediaType" => "text/html"}) - end - - defp fix_content(object), do: object - # Reduce the object list to find the reported user. defp get_reported(objects) do Enum.reduce_while(objects, nil, fn ap_id, _ -> @@ -460,7 +424,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do %{"type" => "Create", "object" => %{"type" => objtype} = object} = data, options ) - when objtype in ~w{Article Note Video Page} do + when objtype in ~w{Note Page} do actor = Containment.get_actor(data) with nil <- Activity.get_create_by_object_ap_id(object["id"]), @@ -554,7 +518,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do %{"type" => "Create", "object" => %{"type" => objtype}} = data, _options ) - when objtype in ~w{Question Answer ChatMessage Audio Event} do + when objtype in ~w{Question Answer ChatMessage Audio Video Event Article} do + data = Map.put(data, "object", strip_internal_fields(data["object"])) + with {:ok, %User{}} <- ObjectValidator.fetch_actor(data), {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do {:ok, activity} diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex index f5e4d49f9..d5713c3dd 100644 --- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex @@ -23,8 +23,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do alias Pleroma.Web.Endpoint alias Pleroma.Web.Router - require Logger - @users_page_size 50 plug( @@ -70,6 +68,12 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do plug( OAuthScopesPlug, + %{scopes: ["read:chats"], admin: true} + when action in [:list_user_chats] + ) + + plug( + OAuthScopesPlug, %{scopes: ["read"], admin: true} when action in [ :list_log, @@ -256,6 +260,20 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do end end + def list_user_chats(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname} = _params) do + with %User{id: user_id} <- User.get_cached_by_nickname_or_id(nickname, for: admin) do + chats = + Pleroma.Chat.for_user_query(user_id) + |> Pleroma.Repo.all() + + conn + |> put_view(AdminAPI.ChatView) + |> render("index.json", chats: chats) + else + _ -> {:error, :not_found} + end + end + def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do user = User.get_cached_by_nickname(nickname) diff --git a/lib/pleroma/web/admin_api/controllers/chat_controller.ex b/lib/pleroma/web/admin_api/controllers/chat_controller.ex new file mode 100644 index 000000000..967600d69 --- /dev/null +++ b/lib/pleroma/web/admin_api/controllers/chat_controller.ex @@ -0,0 +1,85 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.ChatController do + use Pleroma.Web, :controller + + alias Pleroma.Activity + alias Pleroma.Chat + alias Pleroma.Chat.MessageReference + alias Pleroma.ModerationLog + alias Pleroma.Pagination + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.AdminAPI + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView + + require Logger + + plug(Pleroma.Web.ApiSpec.CastAndValidate) + + plug( + OAuthScopesPlug, + %{scopes: ["read:chats"], admin: true} when action in [:show, :messages] + ) + + plug( + OAuthScopesPlug, + %{scopes: ["write:chats"], admin: true} when action in [:delete_message] + ) + + action_fallback(Pleroma.Web.AdminAPI.FallbackController) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.ChatOperation + + def delete_message(%{assigns: %{user: user}} = conn, %{ + message_id: message_id, + id: chat_id + }) do + with %MessageReference{object: %{data: %{"id" => object_ap_id}}} = cm_ref <- + MessageReference.get_by_id(message_id), + ^chat_id <- to_string(cm_ref.chat_id), + %Activity{id: activity_id} <- Activity.get_create_by_object_ap_id(object_ap_id), + {:ok, _} <- CommonAPI.delete(activity_id, user) do + ModerationLog.insert_log(%{ + action: "chat_message_delete", + actor: user, + subject_id: message_id + }) + + conn + |> put_view(MessageReferenceView) + |> render("show.json", chat_message_reference: cm_ref) + else + _e -> + {:error, :could_not_delete} + end + end + + def messages(conn, %{id: id} = params) do + with %Chat{} = chat <- Chat.get_by_id(id) do + cm_refs = + chat + |> MessageReference.for_chat_query() + |> Pagination.fetch_paginated(params) + + conn + |> put_view(MessageReferenceView) + |> render("index.json", chat_message_references: cm_refs) + else + _ -> + conn + |> put_status(:not_found) + |> json(%{error: "not found"}) + end + end + + def show(conn, %{id: id}) do + with %Chat{} = chat <- Chat.get_by_id(id) do + conn + |> put_view(AdminAPI.ChatView) + |> render("show.json", chat: chat) + end + end +end diff --git a/lib/pleroma/web/admin_api/controllers/instance_document_controller.ex b/lib/pleroma/web/admin_api/controllers/instance_document_controller.ex new file mode 100644 index 000000000..504d9b517 --- /dev/null +++ b/lib/pleroma/web/admin_api/controllers/instance_document_controller.ex @@ -0,0 +1,41 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.InstanceDocumentController do + use Pleroma.Web, :controller + + alias Pleroma.Plugs.InstanceStatic + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.InstanceDocument + + plug(Pleroma.Web.ApiSpec.CastAndValidate) + + action_fallback(Pleroma.Web.AdminAPI.FallbackController) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.InstanceDocumentOperation + + plug(OAuthScopesPlug, %{scopes: ["read"], admin: true} when action == :show) + plug(OAuthScopesPlug, %{scopes: ["write"], admin: true} when action in [:update, :delete]) + + def show(conn, %{name: document_name}) do + with {:ok, url} <- InstanceDocument.get(document_name), + {:ok, content} <- File.read(InstanceStatic.file_path(url)) do + conn + |> put_resp_content_type("text/html") + |> send_resp(200, content) + end + end + + def update(%{body_params: %{file: file}} = conn, %{name: document_name}) do + with {:ok, url} <- InstanceDocument.put(document_name, file.path) do + json(conn, %{"url" => url}) + end + end + + def delete(conn, %{name: document_name}) do + with :ok <- InstanceDocument.delete(document_name) do + json(conn, %{}) + end + end +end diff --git a/lib/pleroma/web/admin_api/views/chat_view.ex b/lib/pleroma/web/admin_api/views/chat_view.ex new file mode 100644 index 000000000..847df1423 --- /dev/null +++ b/lib/pleroma/web/admin_api/views/chat_view.ex @@ -0,0 +1,30 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.ChatView do + use Pleroma.Web, :view + + alias Pleroma.Chat + alias Pleroma.User + alias Pleroma.Web.MastodonAPI + alias Pleroma.Web.PleromaAPI + + def render("index.json", %{chats: chats} = opts) do + render_many(chats, __MODULE__, "show.json", Map.delete(opts, :chats)) + end + + def render("show.json", %{chat: %Chat{user_id: user_id}} = opts) do + user = User.get_by_id(user_id) + sender = MastodonAPI.AccountView.render("show.json", user: user, skip_visibility_check: true) + + serialized_chat = PleromaAPI.ChatView.render("show.json", opts) + + serialized_chat + |> Map.put(:sender, sender) + |> Map.put(:receiver, serialized_chat[:account]) + |> Map.delete(:account) + end + + def render(view, opts), do: PleromaAPI.ChatView.render(view, opts) +end diff --git a/lib/pleroma/web/admin_api/views/status_view.ex b/lib/pleroma/web/admin_api/views/status_view.ex index 500800be2..6042a22b6 100644 --- a/lib/pleroma/web/admin_api/views/status_view.ex +++ b/lib/pleroma/web/admin_api/views/status_view.ex @@ -8,6 +8,7 @@ defmodule Pleroma.Web.AdminAPI.StatusView do require Pleroma.Constants alias Pleroma.Web.AdminAPI + alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI defdelegate merge_account_views(user), to: AdminAPI.AccountView @@ -17,7 +18,7 @@ defmodule Pleroma.Web.AdminAPI.StatusView do end def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do - user = MastodonAPI.StatusView.get_user(activity.data["actor"]) + user = CommonAPI.get_user(activity.data["actor"]) MastodonAPI.StatusView.render("show.json", opts) |> Map.merge(%{account: merge_account_views(user)}) diff --git a/lib/pleroma/web/api_spec/operations/admin/chat_operation.ex b/lib/pleroma/web/api_spec/operations/admin/chat_operation.ex new file mode 100644 index 000000000..d3e5dfc1c --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/admin/chat_operation.ex @@ -0,0 +1,96 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Admin.ChatOperation do + alias OpenApiSpex.Operation + alias Pleroma.Web.ApiSpec.Schemas.Chat + alias Pleroma.Web.ApiSpec.Schemas.ChatMessage + + import Pleroma.Web.ApiSpec.Helpers + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def delete_message_operation do + %Operation{ + tags: ["admin", "chat"], + summary: "Delete an individual chat message", + operationId: "AdminAPI.ChatController.delete_message", + parameters: [ + Operation.parameter(:id, :path, :string, "The ID of the Chat"), + Operation.parameter(:message_id, :path, :string, "The ID of the message") + ], + responses: %{ + 200 => + Operation.response( + "The deleted ChatMessage", + "application/json", + ChatMessage + ) + }, + security: [ + %{ + "oAuth" => ["write:chats"] + } + ] + } + end + + def messages_operation do + %Operation{ + tags: ["admin", "chat"], + summary: "Get the most recent messages of the chat", + operationId: "AdminAPI.ChatController.messages", + parameters: + [Operation.parameter(:id, :path, :string, "The ID of the Chat")] ++ + pagination_params(), + responses: %{ + 200 => + Operation.response( + "The messages in the chat", + "application/json", + Pleroma.Web.ApiSpec.ChatOperation.chat_messages_response() + ) + }, + security: [ + %{ + "oAuth" => ["read:chats"] + } + ] + } + end + + def show_operation do + %Operation{ + tags: ["chat"], + summary: "Create a chat", + operationId: "AdminAPI.ChatController.show", + parameters: [ + Operation.parameter( + :id, + :path, + :string, + "The id of the chat", + required: true, + example: "1234" + ) + ], + responses: %{ + 200 => + Operation.response( + "The existing chat", + "application/json", + Chat + ) + }, + security: [ + %{ + "oAuth" => ["read"] + } + ] + } + end +end diff --git a/lib/pleroma/web/api_spec/operations/admin/instance_document_operation.ex b/lib/pleroma/web/api_spec/operations/admin/instance_document_operation.ex new file mode 100644 index 000000000..a120ff4e8 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/admin/instance_document_operation.ex @@ -0,0 +1,115 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Admin.InstanceDocumentOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Helpers + alias Pleroma.Web.ApiSpec.Schemas.ApiError + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def show_operation do + %Operation{ + tags: ["Admin", "InstanceDocument"], + summary: "Get the instance document", + operationId: "AdminAPI.InstanceDocumentController.show", + security: [%{"oAuth" => ["read"]}], + parameters: [ + Operation.parameter(:name, :path, %Schema{type: :string}, "The document name", + required: true + ) + | Helpers.admin_api_params() + ], + responses: %{ + 200 => document_content(), + 400 => Operation.response("Bad Request", "application/json", ApiError), + 403 => Operation.response("Forbidden", "application/json", ApiError), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + def update_operation do + %Operation{ + tags: ["Admin", "InstanceDocument"], + summary: "Update the instance document", + operationId: "AdminAPI.InstanceDocumentController.update", + security: [%{"oAuth" => ["write"]}], + requestBody: Helpers.request_body("Parameters", update_request()), + parameters: [ + Operation.parameter(:name, :path, %Schema{type: :string}, "The document name", + required: true + ) + | Helpers.admin_api_params() + ], + responses: %{ + 200 => Operation.response("InstanceDocument", "application/json", instance_document()), + 400 => Operation.response("Bad Request", "application/json", ApiError), + 403 => Operation.response("Forbidden", "application/json", ApiError), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + defp update_request do + %Schema{ + title: "UpdateRequest", + description: "POST body for uploading the file", + type: :object, + required: [:file], + properties: %{ + file: %Schema{ + type: :string, + format: :binary, + description: "The file to be uploaded, using multipart form data." + } + } + } + end + + def delete_operation do + %Operation{ + tags: ["Admin", "InstanceDocument"], + summary: "Get the instance document", + operationId: "AdminAPI.InstanceDocumentController.delete", + security: [%{"oAuth" => ["write"]}], + parameters: [ + Operation.parameter(:name, :path, %Schema{type: :string}, "The document name", + required: true + ) + | Helpers.admin_api_params() + ], + responses: %{ + 200 => Operation.response("InstanceDocument", "application/json", instance_document()), + 400 => Operation.response("Bad Request", "application/json", ApiError), + 403 => Operation.response("Forbidden", "application/json", ApiError), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + defp instance_document do + %Schema{ + title: "InstanceDocument", + type: :object, + properties: %{ + url: %Schema{type: :string} + }, + example: %{ + "url" => "https://example.com/static/terms-of-service.html" + } + } + end + + defp document_content do + Operation.response("InstanceDocumentContent", "text/html", %Schema{ + type: :string, + example: "<h1>Instance panel</h1>" + }) + end +end diff --git a/lib/pleroma/web/api_spec/operations/list_operation.ex b/lib/pleroma/web/api_spec/operations/list_operation.ex index c88ed5dd0..15039052e 100644 --- a/lib/pleroma/web/api_spec/operations/list_operation.ex +++ b/lib/pleroma/web/api_spec/operations/list_operation.ex @@ -114,7 +114,7 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do description: "Add accounts to the given list.", operationId: "ListController.add_to_list", parameters: [id_param()], - requestBody: add_remove_accounts_request(), + requestBody: add_remove_accounts_request(true), security: [%{"oAuth" => ["write:lists"]}], responses: %{ 200 => Operation.response("Empty object", "application/json", %Schema{type: :object}) @@ -127,8 +127,16 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do tags: ["Lists"], summary: "Remove accounts from list", operationId: "ListController.remove_from_list", - parameters: [id_param()], - requestBody: add_remove_accounts_request(), + parameters: [ + id_param(), + Operation.parameter( + :account_ids, + :query, + %Schema{type: :array, items: %Schema{type: :string}}, + "Array of account IDs" + ) + ], + requestBody: add_remove_accounts_request(false), security: [%{"oAuth" => ["write:lists"]}], responses: %{ 200 => Operation.response("Empty object", "application/json", %Schema{type: :object}) @@ -171,7 +179,7 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do ) end - defp add_remove_accounts_request do + defp add_remove_accounts_request(required) when is_boolean(required) do request_body( "Parameters", %Schema{ @@ -180,9 +188,9 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do properties: %{ account_ids: %Schema{type: :array, description: "Array of account IDs", items: FlakeID} }, - required: [:account_ids] + required: required && [:account_ids] }, - required: true + required: required ) end end diff --git a/lib/pleroma/web/auth/pleroma_authenticator.ex b/lib/pleroma/web/auth/pleroma_authenticator.ex index 200ca03dc..c611b3e09 100644 --- a/lib/pleroma/web/auth/pleroma_authenticator.ex +++ b/lib/pleroma/web/auth/pleroma_authenticator.ex @@ -68,7 +68,7 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do nickname = value([registration_attrs["nickname"], Registration.nickname(registration)]) email = value([registration_attrs["email"], Registration.email(registration)]) name = value([registration_attrs["name"], Registration.name(registration)]) || nickname - bio = value([registration_attrs["bio"], Registration.description(registration)]) + bio = value([registration_attrs["bio"], Registration.description(registration)]) || "" random_password = :crypto.strong_rand_bytes(64) |> Base.encode64() diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index f849b2e01..548f76609 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -202,7 +202,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do additional = case draft.expires_at do - %NaiveDateTime{} = expires_at -> Map.put(additional, "expires_at", expires_at) + %DateTime{} = expires_at -> Map.put(additional, "expires_at", expires_at) _ -> additional end diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 3c7b9e794..60a50b027 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -4,7 +4,6 @@ defmodule Pleroma.Web.CommonAPI do alias Pleroma.Activity - alias Pleroma.ActivityExpiration alias Pleroma.Conversation.Participation alias Pleroma.Formatter alias Pleroma.Object @@ -384,9 +383,9 @@ defmodule Pleroma.Web.CommonAPI do def check_expiry_date({:ok, nil} = res), do: res def check_expiry_date({:ok, in_seconds}) do - expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds) + expiry = DateTime.add(DateTime.utc_now(), in_seconds) - if ActivityExpiration.expires_late_enough?(expiry) do + if Pleroma.Workers.PurgeExpiredActivity.expires_late_enough?(expiry) do {:ok, expiry} else {:error, "Expiry date is too soon"} @@ -455,7 +454,8 @@ defmodule Pleroma.Web.CommonAPI do end def add_mute(user, activity) do - with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do + with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]), + _ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do {:ok, activity} else {:error, _} -> {:error, dgettext("errors", "conversation is already muted")} @@ -553,4 +553,21 @@ defmodule Pleroma.Web.CommonAPI do def show_reblogs(%User{} = user, %User{} = target) do UserRelationship.delete_reblog_mute(user, target) end + + def get_user(ap_id, fake_record_fallback \\ true) do + cond do + user = User.get_cached_by_ap_id(ap_id) -> + user + + user = User.get_by_guessed_nickname(ap_id) -> + user + + fake_record_fallback -> + # TODO: refactor (fake records is never a good idea) + User.error_user(ap_id) + + true -> + nil + end + end end diff --git a/lib/pleroma/web/instance_document.ex b/lib/pleroma/web/instance_document.ex new file mode 100644 index 000000000..df5caebf0 --- /dev/null +++ b/lib/pleroma/web/instance_document.ex @@ -0,0 +1,62 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.InstanceDocument do + alias Pleroma.Config + alias Pleroma.Web.Endpoint + + @instance_documents %{ + "terms-of-service" => "/static/terms-of-service.html", + "instance-panel" => "/instance/panel.html" + } + + @spec get(String.t()) :: {:ok, String.t()} | {:error, atom()} + def get(document_name) do + case Map.fetch(@instance_documents, document_name) do + {:ok, path} -> {:ok, path} + _ -> {:error, :not_found} + end + end + + @spec put(String.t(), String.t()) :: {:ok, String.t()} | {:error, atom()} + def put(document_name, origin_path) do + with {_, {:ok, destination_path}} <- + {:instance_document, Map.fetch(@instance_documents, document_name)}, + :ok <- put_file(origin_path, destination_path) do + {:ok, Path.join(Endpoint.url(), destination_path)} + else + {:instance_document, :error} -> {:error, :not_found} + error -> error + end + end + + @spec delete(String.t()) :: :ok | {:error, atom()} + def delete(document_name) do + with {_, {:ok, path}} <- {:instance_document, Map.fetch(@instance_documents, document_name)}, + instance_static_dir_path <- instance_static_dir(path), + :ok <- File.rm(instance_static_dir_path) do + :ok + else + {:instance_document, :error} -> {:error, :not_found} + {:error, :enoent} -> {:error, :not_found} + error -> error + end + end + + defp put_file(origin_path, destination_path) do + with destination <- instance_static_dir(destination_path), + {_, :ok} <- {:mkdir_p, File.mkdir_p(Path.dirname(destination))}, + {_, {:ok, _}} <- {:copy, File.copy(origin_path, destination)} do + :ok + else + {error, _} -> {:error, error} + end + end + + defp instance_static_dir(filename) do + [:instance, :static_dir] + |> Config.get!() + |> Path.join(filename) + end +end diff --git a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex index acdc76fd2..5daeaa780 100644 --- a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex @@ -74,7 +74,7 @@ defmodule Pleroma.Web.MastodonAPI.ListController do # DELETE /api/v1/lists/:id/accounts def remove_from_list( - %{assigns: %{list: list}, body_params: %{account_ids: account_ids}} = conn, + %{assigns: %{list: list}, params: %{account_ids: account_ids}} = conn, _ ) do Enum.each(account_ids, fn account_id -> @@ -86,6 +86,10 @@ defmodule Pleroma.Web.MastodonAPI.ListController do json(conn, %{}) end + def remove_from_list(%{body_params: params} = conn, _) do + remove_from_list(%{conn | params: params}, %{}) + end + defp list_by_id_and_user(%{assigns: %{user: user}, params: %{id: id}} = conn, _) do case Pleroma.List.get(id, user) do %Pleroma.List{} = list -> assign(conn, :list, list) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 864c0417f..d2a30a548 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -245,7 +245,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do followers_count: followers_count, following_count: following_count, statuses_count: user.note_count, - note: user.bio || "", + note: user.bio, url: user.uri || user.ap_id, avatar: image, avatar_static: image, diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 3fe1967be..94b8dc8c6 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -8,7 +8,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do require Pleroma.Constants alias Pleroma.Activity - alias Pleroma.ActivityExpiration alias Pleroma.HTML alias Pleroma.Object alias Pleroma.Repo @@ -56,23 +55,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do end) end - def get_user(ap_id, fake_record_fallback \\ true) do - cond do - user = User.get_cached_by_ap_id(ap_id) -> - user - - user = User.get_by_guessed_nickname(ap_id) -> - user - - fake_record_fallback -> - # TODO: refactor (fake records is never a good idea) - User.error_user(ap_id) - - true -> - nil - end - end - defp get_context_id(%{data: %{"context_id" => context_id}}) when not is_nil(context_id), do: context_id @@ -120,7 +102,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do # Note: unresolved users are filtered out actors = (activities ++ parent_activities) - |> Enum.map(&get_user(&1.data["actor"], false)) + |> Enum.map(&CommonAPI.get_user(&1.data["actor"], false)) |> Enum.filter(& &1) UserRelationship.view_relationships_option(reading_user, actors, subset: :source_mutes) @@ -139,7 +121,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do "show.json", %{activity: %{data: %{"type" => "Announce", "object" => _object}} = activity} = opts ) do - user = get_user(activity.data["actor"]) + user = CommonAPI.get_user(activity.data["actor"]) created_at = Utils.to_masto_date(activity.data["published"]) activity_object = Object.normalize(activity) @@ -212,7 +194,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do object = Object.normalize(activity) - user = get_user(activity.data["actor"]) + user = CommonAPI.get_user(activity.data["actor"]) user_follower_address = user.follower_address like_count = object.data["like_count"] || 0 @@ -245,8 +227,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do expires_at = with true <- client_posted_this_activity, - %ActivityExpiration{scheduled_at: scheduled_at} <- - ActivityExpiration.get_by_activity_id(activity.id) do + %Oban.Job{scheduled_at: scheduled_at} <- + Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id) do scheduled_at else _ -> nil @@ -266,7 +248,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do reply_to = get_reply_to(activity, opts) - reply_to_user = reply_to && get_user(reply_to.data["actor"]) + reply_to_user = reply_to && CommonAPI.get_user(reply_to.data["actor"]) content = object diff --git a/lib/pleroma/web/metadata/opengraph.ex b/lib/pleroma/web/metadata/opengraph.ex index 68c871e71..bb1b23208 100644 --- a/lib/pleroma/web/metadata/opengraph.ex +++ b/lib/pleroma/web/metadata/opengraph.ex @@ -61,7 +61,7 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do @impl Provider def build_tags(%{user: user}) do - with truncated_bio = Utils.scrub_html_and_truncate(user.bio || "") do + with truncated_bio = Utils.scrub_html_and_truncate(user.bio) do [ {:meta, [ diff --git a/lib/pleroma/web/metadata/restrict_indexing.ex b/lib/pleroma/web/metadata/restrict_indexing.ex index f15607896..a1dcb6e15 100644 --- a/lib/pleroma/web/metadata/restrict_indexing.ex +++ b/lib/pleroma/web/metadata/restrict_indexing.ex @@ -10,7 +10,9 @@ defmodule Pleroma.Web.Metadata.Providers.RestrictIndexing do """ @impl true - def build_tags(%{user: %{local: false}}) do + def build_tags(%{user: %{local: true, discoverable: true}}), do: [] + + def build_tags(_) do [ {:meta, [ @@ -19,7 +21,4 @@ defmodule Pleroma.Web.Metadata.Providers.RestrictIndexing do ], []} ] end - - @impl true - def build_tags(%{user: %{local: true}}), do: [] end diff --git a/lib/pleroma/web/metadata/twitter_card.ex b/lib/pleroma/web/metadata/twitter_card.ex index 5d08ce422..df34b033f 100644 --- a/lib/pleroma/web/metadata/twitter_card.ex +++ b/lib/pleroma/web/metadata/twitter_card.ex @@ -40,7 +40,7 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do @impl Provider def build_tags(%{user: user}) do - with truncated_bio = Utils.scrub_html_and_truncate(user.bio || "") do + with truncated_bio = Utils.scrub_html_and_truncate(user.bio) do [ title_tag(user), {:meta, [property: "twitter:description", content: truncated_bio], []}, diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index 06b116368..26e68be42 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -200,7 +200,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do {:mfa_required, user, auth, _}, params ) do - {:ok, token} = MFA.Token.create_token(user, auth) + {:ok, token} = MFA.Token.create(user, auth) data = %{ "mfa_token" => token.token, @@ -582,7 +582,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do do: put_session(conn, :registration_id, registration_id) defp build_and_response_mfa_token(user, auth) do - with {:ok, token} <- MFA.Token.create_token(user, auth) do + with {:ok, token} <- MFA.Token.create(user, auth) do MFAView.render("mfa_response.json", %{token: token, user: user}) end end diff --git a/lib/pleroma/web/oauth/token.ex b/lib/pleroma/web/oauth/token.ex index 08bb7326d..de37998f2 100644 --- a/lib/pleroma/web/oauth/token.ex +++ b/lib/pleroma/web/oauth/token.ex @@ -50,7 +50,7 @@ defmodule Pleroma.Web.OAuth.Token do true <- auth.app_id == app.id do user = if auth.user_id, do: User.get_cached_by_id(auth.user_id), else: %User{} - create_token( + create( app, user, %{scopes: auth.scopes} @@ -83,8 +83,22 @@ defmodule Pleroma.Web.OAuth.Token do |> validate_required([:valid_until]) end - @spec create_token(App.t(), User.t(), map()) :: {:ok, Token} | {:error, Changeset.t()} - def create_token(%App{} = app, %User{} = user, attrs \\ %{}) do + @spec create(App.t(), User.t(), map()) :: {:ok, Token} | {:error, Changeset.t()} + def create(%App{} = app, %User{} = user, attrs \\ %{}) do + with {:ok, token} <- do_create(app, user, attrs) do + if Pleroma.Config.get([:oauth2, :clean_expired_tokens]) do + Pleroma.Workers.PurgeExpiredToken.enqueue(%{ + token_id: token.id, + valid_until: DateTime.from_naive!(token.valid_until, "Etc/UTC"), + mod: __MODULE__ + }) + end + + {:ok, token} + end + end + + defp do_create(app, user, attrs) do %__MODULE__{user_id: user.id, app_id: app.id} |> cast(%{scopes: attrs[:scopes] || app.scopes}, [:scopes]) |> validate_required([:scopes, :app_id]) @@ -105,11 +119,6 @@ defmodule Pleroma.Web.OAuth.Token do |> Repo.delete_all() end - def delete_expired_tokens do - Query.get_expired_tokens() - |> Repo.delete_all() - end - def get_user_tokens(%User{id: user_id}) do Query.get_by_user(user_id) |> Query.preload([:app]) diff --git a/lib/pleroma/web/oauth/token/clean_worker.ex b/lib/pleroma/web/oauth/token/clean_worker.ex deleted file mode 100644 index e3aa4eb7e..000000000 --- a/lib/pleroma/web/oauth/token/clean_worker.ex +++ /dev/null @@ -1,38 +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.OAuth.Token.CleanWorker do - @moduledoc """ - The module represents functions to clean an expired OAuth and MFA tokens. - """ - use GenServer - - @ten_seconds 10_000 - @one_day 86_400_000 - - alias Pleroma.MFA - alias Pleroma.Web.OAuth - alias Pleroma.Workers.BackgroundWorker - - def start_link(_), do: GenServer.start_link(__MODULE__, %{}) - - def init(_) do - Process.send_after(self(), :perform, @ten_seconds) - {:ok, nil} - end - - @doc false - def handle_info(:perform, state) do - BackgroundWorker.enqueue("clean_expired_tokens", %{}) - interval = Pleroma.Config.get([:oauth2, :clean_expired_tokens_interval], @one_day) - - Process.send_after(self(), :perform, interval) - {:noreply, state} - end - - def perform(:clean) do - OAuth.Token.delete_expired_tokens() - MFA.Token.delete_expired_tokens() - end -end diff --git a/lib/pleroma/web/oauth/token/query.ex b/lib/pleroma/web/oauth/token/query.ex index 93d6e26ed..fd6d9b112 100644 --- a/lib/pleroma/web/oauth/token/query.ex +++ b/lib/pleroma/web/oauth/token/query.ex @@ -33,12 +33,6 @@ defmodule Pleroma.Web.OAuth.Token.Query do from(q in query, where: q.id == ^id) end - @spec get_expired_tokens(query, DateTime.t() | nil) :: query - def get_expired_tokens(query \\ Token, date \\ nil) do - expired_date = date || Timex.now() - from(q in query, where: fragment("?", q.valid_until) < ^expired_date) - end - @spec get_by_user(query, String.t()) :: query def get_by_user(query \\ Token, user_id) do from(q in query, where: q.user_id == ^user_id) diff --git a/lib/pleroma/web/oauth/token/strategy/refresh_token.ex b/lib/pleroma/web/oauth/token/strategy/refresh_token.ex index debc29b0b..625b0fde2 100644 --- a/lib/pleroma/web/oauth/token/strategy/refresh_token.ex +++ b/lib/pleroma/web/oauth/token/strategy/refresh_token.ex @@ -46,7 +46,7 @@ defmodule Pleroma.Web.OAuth.Token.Strategy.RefreshToken do defp create_access_token({:error, error}, _), do: {:error, error} defp create_access_token({:ok, token}, %{app: app, user: user} = token_params) do - Token.create_token(app, user, add_refresh_token(token_params, token.refresh_token)) + Token.create(app, user, add_refresh_token(token_params, token.refresh_token)) end defp add_refresh_token(params, token) do diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex index ea0921c77..867cff829 100644 --- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -156,13 +156,8 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do blocked_ap_ids = User.blocked_users_ap_ids(user) chats = - from(c in Chat, - where: c.user_id == ^user_id, - where: c.recipient not in ^blocked_ap_ids, - order_by: [desc: c.updated_at], - inner_join: u in User, - on: u.ap_id == c.recipient - ) + Chat.for_user_query(user_id) + |> where([c], c.recipient not in ^blocked_ap_ids) |> Repo.all() conn diff --git a/lib/pleroma/web/pleroma_api/views/scrobble_view.ex b/lib/pleroma/web/pleroma_api/views/scrobble_view.ex index bbff93abe..95bd4c368 100644 --- a/lib/pleroma/web/pleroma_api/views/scrobble_view.ex +++ b/lib/pleroma/web/pleroma_api/views/scrobble_view.ex @@ -10,14 +10,14 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleView do alias Pleroma.Activity alias Pleroma.HTML alias Pleroma.Object + alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.MastodonAPI.AccountView - alias Pleroma.Web.MastodonAPI.StatusView def render("show.json", %{activity: %Activity{data: %{"type" => "Listen"}} = activity} = opts) do object = Object.normalize(activity) - user = StatusView.get_user(activity.data["actor"]) + user = CommonAPI.get_user(activity.data["actor"]) created_at = Utils.to_masto_date(activity.data["published"]) %{ diff --git a/lib/pleroma/web/rel_me.ex b/lib/pleroma/web/rel_me.ex index 8e2b51508..28f75b18d 100644 --- a/lib/pleroma/web/rel_me.ex +++ b/lib/pleroma/web/rel_me.ex @@ -5,7 +5,8 @@ defmodule Pleroma.Web.RelMe do @options [ pool: :media, - max_body: 2_000_000 + max_body: 2_000_000, + recv_timeout: 2_000 ] if Pleroma.Config.get(:env) == :test do @@ -23,18 +24,8 @@ defmodule Pleroma.Web.RelMe do def parse(_), do: {:error, "No URL provided"} defp parse_url(url) do - opts = - if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do - Keyword.merge(@options, - recv_timeout: 2_000, - with_body: true - ) - else - @options - end - with {:ok, %Tesla.Env{body: html, status: status}} when status in 200..299 <- - Pleroma.HTTP.get(url, [], adapter: opts), + Pleroma.HTTP.get(url, [], @options), {:ok, html_tree} <- Floki.parse_document(html), data <- Floki.attribute(html_tree, "link[rel~=me]", "href") ++ diff --git a/lib/pleroma/web/rich_media/helpers.ex b/lib/pleroma/web/rich_media/helpers.ex index b7852c6e3..d7a19df4a 100644 --- a/lib/pleroma/web/rich_media/helpers.ex +++ b/lib/pleroma/web/rich_media/helpers.ex @@ -9,14 +9,15 @@ defmodule Pleroma.Web.RichMedia.Helpers do alias Pleroma.Object alias Pleroma.Web.RichMedia.Parser - @rich_media_options [ + @options [ pool: :media, - max_body: 2_000_000 + max_body: 2_000_000, + recv_timeout: 2_000 ] @spec validate_page_url(URI.t() | binary()) :: :ok | :error defp validate_page_url(page_url) when is_binary(page_url) do - validate_tld = Pleroma.Config.get([Pleroma.Formatter, :validate_tld]) + validate_tld = Config.get([Pleroma.Formatter, :validate_tld]) page_url |> Linkify.Parser.url?(validate_tld: validate_tld) @@ -86,18 +87,8 @@ defmodule Pleroma.Web.RichMedia.Helpers do def rich_media_get(url) do headers = [{"user-agent", Pleroma.Application.user_agent() <> "; Bot"}] - options = - if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do - Keyword.merge(@rich_media_options, - recv_timeout: 2_000, - with_body: true - ) - else - @rich_media_options - end - head_check = - case Pleroma.HTTP.head(url, headers, adapter: options) do + case Pleroma.HTTP.head(url, headers, @options) do # If the HEAD request didn't reach the server for whatever reason, # we assume the GET that comes right after won't either {:error, _} = e -> @@ -112,7 +103,7 @@ defmodule Pleroma.Web.RichMedia.Helpers do :ok end - with :ok <- head_check, do: Pleroma.HTTP.get(url, headers, adapter: options) + with :ok <- head_check, do: Pleroma.HTTP.get(url, headers, @options) end defp check_content_type(headers) do @@ -128,7 +119,7 @@ defmodule Pleroma.Web.RichMedia.Helpers do end end - @max_body @rich_media_options[:max_body] + @max_body @options[:max_body] defp check_content_length(headers) do case List.keyfind(headers, "content-length", 0) do {_, maybe_content_length} -> diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex index 569249f51..c70d2fdba 100644 --- a/lib/pleroma/web/rich_media/parser.ex +++ b/lib/pleroma/web/rich_media/parser.ex @@ -20,31 +20,61 @@ defmodule Pleroma.Web.RichMedia.Parser do with {:ok, data} <- get_cached_or_parse(url), {:ok, _} <- set_ttl_based_on_image(data, url) do {:ok, data} - else - error -> - Logger.error(fn -> "Rich media error: #{inspect(error)}" end) end end defp get_cached_or_parse(url) do - case Cachex.fetch!(:rich_media_cache, url, fn _ -> {:commit, parse_url(url)} end) do - {:ok, _data} = res -> - res - - {:error, :body_too_large} = e -> - e - - {:error, {:content_type, _}} = e -> - e - - # The TTL is not set for the errors above, since they are unlikely to change - # with time - {:error, _} = e -> - ttl = Pleroma.Config.get([:rich_media, :failure_backoff], 60_000) - Cachex.expire(:rich_media_cache, url, ttl) - e + case Cachex.fetch(:rich_media_cache, url, fn -> + case parse_url(url) do + {:ok, _} = res -> + {:commit, res} + + {:error, reason} = e -> + # Unfortunately we have to log errors here, instead of doing that + # along with ttl setting at the bottom. Otherwise we can get log spam + # if more than one process was waiting for the rich media card + # while it was generated. Ideally we would set ttl here as well, + # so we don't override it number_of_waiters_on_generation + # times, but one, obviously, can't set ttl for not-yet-created entry + # and Cachex doesn't support returning ttl from the fetch callback. + log_error(url, reason) + {:commit, e} + end + end) do + {action, res} when action in [:commit, :ok] -> + case res do + {:ok, _data} = res -> + res + + {:error, reason} = e -> + if action == :commit, do: set_error_ttl(url, reason) + e + end + + {:error, e} -> + {:error, {:cachex_error, e}} end end + + defp set_error_ttl(_url, :body_too_large), do: :ok + defp set_error_ttl(_url, {:content_type, _}), do: :ok + + # The TTL is not set for the errors above, since they are unlikely to change + # with time + + defp set_error_ttl(url, _reason) do + ttl = Pleroma.Config.get([:rich_media, :failure_backoff], 60_000) + Cachex.expire(:rich_media_cache, url, ttl) + :ok + end + + defp log_error(url, {:invalid_metadata, data}) do + Logger.debug(fn -> "Incomplete or invalid metadata for #{url}: #{inspect(data)}" end) + end + + defp log_error(url, reason) do + Logger.warn(fn -> "Rich media error for #{url}: #{inspect(reason)}" end) + end end @doc """ @@ -98,7 +128,7 @@ defmodule Pleroma.Web.RichMedia.Parser do end) end - defp parse_url(url) do + def parse_url(url) do with {:ok, %Tesla.Env{body: html}} <- Pleroma.Web.RichMedia.Helpers.rich_media_get(url), {:ok, html} <- Floki.parse_document(html) do html @@ -124,7 +154,7 @@ defmodule Pleroma.Web.RichMedia.Parser do end defp check_parsed_data(data) do - {:error, "Found metadata was invalid or incomplete: #{inspect(data)}"} + {:error, {:invalid_metadata, data}} end defp clean_parsed_data(data) do diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index c6433cc53..a4a58c2c4 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -178,9 +178,14 @@ defmodule Pleroma.Web.Router do get("/users", AdminAPIController, :list_users) get("/users/:nickname", AdminAPIController, :user_show) get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses) + get("/users/:nickname/chats", AdminAPIController, :list_user_chats) get("/instances/:instance/statuses", AdminAPIController, :list_instance_statuses) + get("/instance_document/:name", InstanceDocumentController, :show) + patch("/instance_document/:name", InstanceDocumentController, :update) + delete("/instance_document/:name", InstanceDocumentController, :delete) + patch("/users/confirm_email", AdminAPIController, :confirm_email) patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email) @@ -214,6 +219,10 @@ defmodule Pleroma.Web.Router do get("/media_proxy_caches", MediaProxyCacheController, :index) post("/media_proxy_caches/delete", MediaProxyCacheController, :delete) post("/media_proxy_caches/purge", MediaProxyCacheController, :purge) + + get("/chats/:id", ChatController, :show) + get("/chats/:id/messages", ChatController, :messages) + delete("/chats/:id/messages/:message_id", ChatController, :delete_message) end scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do diff --git a/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex b/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex index 521dc9322..072d889e2 100644 --- a/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex @@ -135,7 +135,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do end defp handle_follow_error(conn, {:mfa_required, followee, user, _} = _) do - {:ok, %{token: token}} = MFA.Token.create_token(user) + {:ok, %{token: token}} = MFA.Token.create(user) render(conn, "follow_mfa.html", %{followee: followee, mfa_token: token, error: false}) end diff --git a/lib/pleroma/workers/cron/clear_oauth_token_worker.ex b/lib/pleroma/workers/cron/clear_oauth_token_worker.ex deleted file mode 100644 index 276f47efc..000000000 --- a/lib/pleroma/workers/cron/clear_oauth_token_worker.ex +++ /dev/null @@ -1,23 +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.Workers.Cron.ClearOauthTokenWorker do - @moduledoc """ - The worker to cleanup expired oAuth tokens. - """ - - use Oban.Worker, queue: "background" - - alias Pleroma.Config - alias Pleroma.Web.OAuth.Token - - @impl Oban.Worker - def perform(_job) do - if Config.get([:oauth2, :clean_expired_tokens], false) do - Token.delete_expired_tokens() - end - - :ok - end -end diff --git a/lib/pleroma/workers/cron/purge_expired_activities_worker.ex b/lib/pleroma/workers/cron/purge_expired_activities_worker.ex deleted file mode 100644 index 6549207fc..000000000 --- a/lib/pleroma/workers/cron/purge_expired_activities_worker.ex +++ /dev/null @@ -1,48 +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.Workers.Cron.PurgeExpiredActivitiesWorker do - @moduledoc """ - The worker to purge expired activities. - """ - - use Oban.Worker, queue: "activity_expiration" - - alias Pleroma.Activity - alias Pleroma.ActivityExpiration - alias Pleroma.Config - alias Pleroma.User - alias Pleroma.Web.CommonAPI - - require Logger - - @interval :timer.minutes(1) - - @impl Oban.Worker - def perform(_job) do - if Config.get([ActivityExpiration, :enabled]) do - Enum.each(ActivityExpiration.due_expirations(@interval), &delete_activity/1) - end - after - :ok - end - - def delete_activity(%ActivityExpiration{activity_id: activity_id}) do - with {:activity, %Activity{} = activity} <- - {:activity, Activity.get_by_id_with_object(activity_id)}, - {:user, %User{} = user} <- {:user, User.get_by_ap_id(activity.object.data["actor"])} do - CommonAPI.delete(activity.id, user) - else - {:activity, _} -> - Logger.error( - "#{__MODULE__} Couldn't delete expired activity: not found activity ##{activity_id}" - ) - - {:user, _} -> - Logger.error( - "#{__MODULE__} Couldn't delete expired activity: not found actor of ##{activity_id}" - ) - end - end -end diff --git a/lib/pleroma/workers/cron/stats_worker.ex b/lib/pleroma/workers/cron/stats_worker.ex deleted file mode 100644 index 6a79540bc..000000000 --- a/lib/pleroma/workers/cron/stats_worker.ex +++ /dev/null @@ -1,17 +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.Workers.Cron.StatsWorker do - @moduledoc """ - The worker to update peers statistics. - """ - - use Oban.Worker, queue: "background" - - @impl Oban.Worker - def perform(_job) do - Pleroma.Stats.do_collect() - :ok - end -end diff --git a/lib/pleroma/workers/purge_expired_activity.ex b/lib/pleroma/workers/purge_expired_activity.ex new file mode 100644 index 000000000..c168890a2 --- /dev/null +++ b/lib/pleroma/workers/purge_expired_activity.ex @@ -0,0 +1,72 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.PurgeExpiredActivity do + @moduledoc """ + Worker which purges expired activity. + """ + + use Oban.Worker, queue: :activity_expiration, max_attempts: 1 + + import Ecto.Query + + alias Pleroma.Activity + + @spec enqueue(map()) :: + {:ok, Oban.Job.t()} + | {:error, :expired_activities_disabled} + | {:error, :expiration_too_close} + def enqueue(args) do + with true <- enabled?() do + {scheduled_at, args} = Map.pop(args, :expires_at) + + args + |> new(scheduled_at: scheduled_at) + |> Oban.insert() + end + end + + @impl true + def perform(%Oban.Job{args: %{"activity_id" => id}}) do + with %Activity{} = activity <- find_activity(id), + %Pleroma.User{} = user <- find_user(activity.object.data["actor"]) do + Pleroma.Web.CommonAPI.delete(activity.id, user) + end + end + + defp enabled? do + with false <- Pleroma.Config.get([__MODULE__, :enabled], false) do + {:error, :expired_activities_disabled} + end + end + + defp find_activity(id) do + with nil <- Activity.get_by_id_with_object(id) do + {:error, :activity_not_found} + end + end + + defp find_user(ap_id) do + with nil <- Pleroma.User.get_by_ap_id(ap_id) do + {:error, :user_not_found} + end + end + + def get_expiration(id) do + from(j in Oban.Job, + where: j.state == "scheduled", + where: j.queue == "activity_expiration", + where: fragment("?->>'activity_id' = ?", j.args, ^id) + ) + |> Pleroma.Repo.one() + end + + @spec expires_late_enough?(DateTime.t()) :: boolean() + def expires_late_enough?(scheduled_at) do + now = DateTime.utc_now() + diff = DateTime.diff(scheduled_at, now, :millisecond) + min_lifetime = Pleroma.Config.get([__MODULE__, :min_lifetime], 600) + diff > :timer.seconds(min_lifetime) + end +end diff --git a/lib/pleroma/workers/purge_expired_token.ex b/lib/pleroma/workers/purge_expired_token.ex new file mode 100644 index 000000000..a81e0cd28 --- /dev/null +++ b/lib/pleroma/workers/purge_expired_token.ex @@ -0,0 +1,29 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.PurgeExpiredToken do + @moduledoc """ + Worker which purges expired OAuth tokens + """ + + use Oban.Worker, queue: :token_expiration, max_attempts: 1 + + @spec enqueue(%{token_id: integer(), valid_until: DateTime.t(), mod: module()}) :: + {:ok, Oban.Job.t()} | {:error, Ecto.Changeset.t()} + def enqueue(args) do + {scheduled_at, args} = Map.pop(args, :valid_until) + + args + |> __MODULE__.new(scheduled_at: scheduled_at) + |> Oban.insert() + end + + @impl true + def perform(%Oban.Job{args: %{"token_id" => id, "mod" => module}}) do + module + |> String.to_existing_atom() + |> Pleroma.Repo.get(id) + |> Pleroma.Repo.delete() + end +end |