diff options
Diffstat (limited to 'lib')
81 files changed, 2590 insertions, 968 deletions
diff --git a/lib/mix/tasks/pleroma/benchmark.ex b/lib/mix/tasks/pleroma/benchmark.ex index 6ab7fe8ef..dd2b9c8f2 100644 --- a/lib/mix/tasks/pleroma/benchmark.ex +++ b/lib/mix/tasks/pleroma/benchmark.ex @@ -67,8 +67,7 @@ defmodule Mix.Tasks.Pleroma.Benchmark do Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{ activities: activities, for: user, - as: :activity, - skip_relationships: true + as: :activity }) end }, diff --git a/lib/mix/tasks/pleroma/digest.ex b/lib/mix/tasks/pleroma/digest.ex index 7d09e70c5..3595f912d 100644 --- a/lib/mix/tasks/pleroma/digest.ex +++ b/lib/mix/tasks/pleroma/digest.ex @@ -1,5 +1,6 @@ defmodule Mix.Tasks.Pleroma.Digest do use Mix.Task + import Mix.Pleroma @shortdoc "Manages digest emails" @moduledoc File.read!("docs/administration/CLI_tasks/digest.md") @@ -22,12 +23,10 @@ defmodule Mix.Tasks.Pleroma.Digest do with %Swoosh.Email{} = email <- Pleroma.Emails.UserEmail.digest_email(patched_user) do {:ok, _} = Pleroma.Emails.Mailer.deliver(email) - Mix.shell().info("Digest email have been sent to #{nickname} (#{user.email})") + shell_info("Digest email have been sent to #{nickname} (#{user.email})") else _ -> - Mix.shell().info( - "Cound't find any mentions for #{nickname} since #{last_digest_emailed_at}" - ) + shell_info("Cound't find any mentions for #{nickname} since #{last_digest_emailed_at}") end end end diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index da140ac86..3635c02bc 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -144,28 +144,18 @@ defmodule Mix.Tasks.Pleroma.User do end end - def run(["unsubscribe", nickname]) do + def run(["deactivate", nickname]) do start_pleroma() with %User{} = user <- User.get_cached_by_nickname(nickname) do shell_info("Deactivating #{user.nickname}") User.deactivate(user) - - user - |> User.get_friends() - |> Enum.each(fn friend -> - user = User.get_cached_by_id(user.id) - - shell_info("Unsubscribing #{friend.nickname} from #{user.nickname}") - User.unfollow(user, friend) - end) - :timer.sleep(500) user = User.get_cached_by_id(user.id) - if Enum.empty?(User.get_friends(user)) do - shell_info("Successfully unsubscribed all followers from #{user.nickname}") + if Enum.empty?(Enum.filter(User.get_friends(user), & &1.local)) do + shell_info("Successfully unsubscribed all local followers from #{user.nickname}") end else _ -> @@ -173,7 +163,7 @@ defmodule Mix.Tasks.Pleroma.User do end end - def run(["unsubscribe_all_from_instance", instance]) do + def run(["deactivate_all_from_instance", instance]) do start_pleroma() Pleroma.User.Query.build(%{nickname: "@#{instance}"}) @@ -181,7 +171,7 @@ defmodule Mix.Tasks.Pleroma.User do |> Stream.each(fn users -> users |> Enum.each(fn user -> - run(["unsubscribe", user.nickname]) + run(["deactivate", user.nickname]) end) end) |> Stream.run() diff --git a/lib/pleroma/bbs/authenticator.ex b/lib/pleroma/bbs/authenticator.ex index d4494b003..815de7002 100644 --- a/lib/pleroma/bbs/authenticator.ex +++ b/lib/pleroma/bbs/authenticator.ex @@ -4,6 +4,7 @@ defmodule Pleroma.BBS.Authenticator do use Sshd.PasswordAuthenticator + alias Pleroma.Plugs.AuthenticationPlug alias Pleroma.User def authenticate(username, password) do @@ -11,7 +12,7 @@ defmodule Pleroma.BBS.Authenticator do password = to_string(password) with %User{} = user <- User.get_by_nickname(username) do - Pbkdf2.verify_pass(password, user.password_hash) + AuthenticationPlug.checkpw(password, user.password_hash) else _e -> false end diff --git a/lib/pleroma/config/config_db.ex b/lib/pleroma/config/config_db.ex index 4097ee5b7..2b43d4c36 100644 --- a/lib/pleroma/config/config_db.ex +++ b/lib/pleroma/config/config_db.ex @@ -278,6 +278,8 @@ defmodule Pleroma.ConfigDB do } end + defp do_convert({:partial_chain, entity}), do: %{"tuple" => [":partial_chain", inspect(entity)]} + defp do_convert(entity) when is_tuple(entity) do value = entity @@ -321,6 +323,15 @@ defmodule Pleroma.ConfigDB do {:proxy_url, {do_transform_string(type), parse_host(host), port}} end + defp do_transform(%{"tuple" => [":partial_chain", entity]}) do + {partial_chain, []} = + entity + |> String.replace(~r/[^\w|^{:,[|^,|^[|^\]^}|^\/|^\.|^"]^\s/, "") + |> Code.eval_string() + + {:partial_chain, partial_chain} + end + defp do_transform(%{"tuple" => entity}) do Enum.reduce(entity, {}, fn val, acc -> Tuple.append(acc, do_transform(val)) end) end diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex index 3a9eec5ea..06174f624 100644 --- a/lib/pleroma/constants.ex +++ b/lib/pleroma/constants.ex @@ -17,7 +17,8 @@ defmodule Pleroma.Constants do "announcement_count", "emoji", "context_id", - "deleted_activity_id" + "deleted_activity_id", + "pleroma_internal" ] ) diff --git a/lib/pleroma/docs/json.ex b/lib/pleroma/docs/json.ex index 74f8b2615..d1cf1f487 100644 --- a/lib/pleroma/docs/json.ex +++ b/lib/pleroma/docs/json.ex @@ -18,7 +18,6 @@ defmodule Pleroma.Docs.JSON do with config <- Pleroma.Config.Loader.read("config/description.exs") do config[:pleroma][:config_description] |> Pleroma.Docs.Generator.convert_to_strings() - |> Jason.encode!() end end end diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex index 242344374..eb7d598c6 100644 --- a/lib/pleroma/emoji/pack.ex +++ b/lib/pleroma/emoji/pack.ex @@ -16,162 +16,78 @@ defmodule Pleroma.Emoji.Pack do alias Pleroma.Emoji - @spec emoji_path() :: Path.t() - def emoji_path do - static = Pleroma.Config.get!([:instance, :static_dir]) - Path.join(static, "emoji") - end - @spec create(String.t()) :: :ok | {:error, File.posix()} | {:error, :empty_values} - def create(name) when byte_size(name) > 0 do - dir = Path.join(emoji_path(), name) - - with :ok <- File.mkdir(dir) do - %__MODULE__{ - pack_file: Path.join(dir, "pack.json") - } + def create(name) do + with :ok <- validate_not_empty([name]), + dir <- Path.join(emoji_path(), name), + :ok <- File.mkdir(dir) do + %__MODULE__{pack_file: Path.join(dir, "pack.json")} |> save_pack() end end - def create(_), do: {:error, :empty_values} - - @spec show(String.t()) :: {:ok, t()} | {:loaded, nil} | {:error, :empty_values} - def show(name) when byte_size(name) > 0 do - with {_, %__MODULE__{} = pack} <- {:loaded, load_pack(name)}, - {_, pack} <- validate_pack(pack) do - {:ok, pack} + @spec show(String.t()) :: {:ok, t()} | {:error, atom()} + def show(name) do + with :ok <- validate_not_empty([name]), + {:ok, pack} <- load_pack(name) do + {:ok, validate_pack(pack)} end end - def show(_), do: {:error, :empty_values} - @spec delete(String.t()) :: {:ok, [binary()]} | {:error, File.posix(), binary()} | {:error, :empty_values} - def delete(name) when byte_size(name) > 0 do - emoji_path() - |> Path.join(name) - |> File.rm_rf() - end - - def delete(_), do: {:error, :empty_values} - - @spec add_file(String.t(), String.t(), Path.t(), Plug.Upload.t() | String.t()) :: - {:ok, t()} | {:error, File.posix()} | {:error, :empty_values} - def add_file(name, shortcode, filename, file) - when byte_size(name) > 0 and byte_size(shortcode) > 0 and byte_size(filename) > 0 do - with {_, nil} <- {:exists, Emoji.get(shortcode)}, - {_, %__MODULE__{} = pack} <- {:loaded, load_pack(name)} do - file_path = Path.join(pack.path, filename) - - create_subdirs(file_path) - - case file do - %Plug.Upload{path: upload_path} -> - # Copy the uploaded file from the temporary directory - File.copy!(upload_path, file_path) - - url when is_binary(url) -> - # Download and write the file - file_contents = Tesla.get!(url).body - File.write!(file_path, file_contents) - end - - files = Map.put(pack.files, shortcode, filename) - - updated_pack = %{pack | files: files} - - case save_pack(updated_pack) do - :ok -> - Emoji.reload() - {:ok, updated_pack} - - e -> - e - end + def delete(name) do + with :ok <- validate_not_empty([name]) do + emoji_path() + |> Path.join(name) + |> File.rm_rf() end end - def add_file(_, _, _, _), do: {:error, :empty_values} - - defp create_subdirs(file_path) do - if String.contains?(file_path, "/") do - file_path - |> Path.dirname() - |> File.mkdir_p!() + @spec add_file(String.t(), String.t(), Path.t(), Plug.Upload.t() | String.t()) :: + {:ok, t()} | {:error, File.posix() | atom()} + def add_file(name, shortcode, filename, file) do + with :ok <- validate_not_empty([name, shortcode, filename]), + :ok <- validate_emoji_not_exists(shortcode), + {:ok, pack} <- load_pack(name), + :ok <- save_file(file, pack, filename), + {:ok, updated_pack} <- pack |> put_emoji(shortcode, filename) |> save_pack() do + Emoji.reload() + {:ok, updated_pack} end end @spec delete_file(String.t(), String.t()) :: - {:ok, t()} | {:error, File.posix()} | {:error, :empty_values} - def delete_file(name, shortcode) when byte_size(name) > 0 and byte_size(shortcode) > 0 do - with {_, %__MODULE__{} = pack} <- {:loaded, load_pack(name)}, - {_, {filename, files}} when not is_nil(filename) <- - {:exists, Map.pop(pack.files, shortcode)}, - emoji <- Path.join(pack.path, filename), - {_, true} <- {:exists, File.exists?(emoji)} do - emoji_dir = Path.dirname(emoji) - - File.rm!(emoji) - - if String.contains?(filename, "/") and File.ls!(emoji_dir) == [] do - File.rmdir!(emoji_dir) - end - - updated_pack = %{pack | files: files} - - case save_pack(updated_pack) do - :ok -> - Emoji.reload() - {:ok, updated_pack} - - e -> - e - end + {:ok, t()} | {:error, File.posix() | atom()} + def delete_file(name, shortcode) do + with :ok <- validate_not_empty([name, shortcode]), + {:ok, pack} <- load_pack(name), + :ok <- remove_file(pack, shortcode), + {:ok, updated_pack} <- pack |> delete_emoji(shortcode) |> save_pack() do + Emoji.reload() + {:ok, updated_pack} end end - def delete_file(_, _), do: {:error, :empty_values} - @spec update_file(String.t(), String.t(), String.t(), String.t(), boolean()) :: - {:ok, t()} | {:error, File.posix()} | {:error, :empty_values} - def update_file(name, shortcode, new_shortcode, new_filename, force) - when byte_size(name) > 0 and byte_size(shortcode) > 0 and byte_size(new_shortcode) > 0 and - byte_size(new_filename) > 0 do - with {_, %__MODULE__{} = pack} <- {:loaded, load_pack(name)}, - {_, {filename, files}} when not is_nil(filename) <- - {:exists, Map.pop(pack.files, shortcode)}, - {_, true} <- {:not_used, force or is_nil(Emoji.get(new_shortcode))} do - old_path = Path.join(pack.path, filename) - old_dir = Path.dirname(old_path) - new_path = Path.join(pack.path, new_filename) - - create_subdirs(new_path) - - :ok = File.rename(old_path, new_path) - - if String.contains?(filename, "/") and File.ls!(old_dir) == [] do - File.rmdir!(old_dir) - end - - files = Map.put(files, new_shortcode, new_filename) - - updated_pack = %{pack | files: files} - - case save_pack(updated_pack) do - :ok -> - Emoji.reload() - {:ok, updated_pack} - - e -> - e - end + {:ok, t()} | {:error, File.posix() | atom()} + def update_file(name, shortcode, new_shortcode, new_filename, force) do + with :ok <- validate_not_empty([name, shortcode, new_shortcode, new_filename]), + {:ok, pack} <- load_pack(name), + {:ok, filename} <- get_filename(pack, shortcode), + :ok <- validate_emoji_not_exists(new_shortcode, force), + :ok <- rename_file(pack, filename, new_filename), + {:ok, updated_pack} <- + pack + |> delete_emoji(shortcode) + |> put_emoji(new_shortcode, new_filename) + |> save_pack() do + Emoji.reload() + {:ok, updated_pack} end end - def update_file(_, _, _, _, _), do: {:error, :empty_values} - - @spec import_from_filesystem() :: {:ok, [String.t()]} | {:error, atom()} + @spec import_from_filesystem() :: {:ok, [String.t()]} | {:error, File.posix() | atom()} def import_from_filesystem do emoji_path = emoji_path() @@ -184,7 +100,7 @@ defmodule Pleroma.Emoji.Pack do File.dir?(path) and File.exists?(Path.join(path, "pack.json")) end) |> Enum.map(&write_pack_contents/1) - |> Enum.filter(& &1) + |> Enum.reject(&is_nil/1) {:ok, names} else @@ -193,6 +109,117 @@ defmodule Pleroma.Emoji.Pack do end end + @spec list_remote(String.t()) :: {:ok, map()} | {:error, atom()} + def list_remote(url) do + uri = url |> String.trim() |> URI.parse() + + with :ok <- validate_shareable_packs_available(uri) do + uri + |> URI.merge("/api/pleroma/emoji/packs") + |> http_get() + end + end + + @spec list_local() :: {:ok, map()} + def list_local do + with {:ok, results} <- list_packs_dir() do + packs = + results + |> Enum.map(fn name -> + case load_pack(name) do + {:ok, pack} -> pack + _ -> nil + end + end) + |> Enum.reject(&is_nil/1) + |> Map.new(fn pack -> {pack.name, validate_pack(pack)} end) + + {:ok, packs} + end + end + + @spec get_archive(String.t()) :: {:ok, binary()} | {:error, atom()} + def get_archive(name) do + with {:ok, pack} <- load_pack(name), + :ok <- validate_downloadable(pack) do + {:ok, fetch_archive(pack)} + end + end + + @spec download(String.t(), String.t(), String.t()) :: :ok | {:error, atom()} + def download(name, url, as) do + uri = url |> String.trim() |> URI.parse() + + with :ok <- validate_shareable_packs_available(uri), + {:ok, remote_pack} <- uri |> URI.merge("/api/pleroma/emoji/packs/#{name}") |> http_get(), + {:ok, %{sha: sha, url: url} = pack_info} <- fetch_pack_info(remote_pack, uri, name), + {:ok, archive} <- download_archive(url, sha), + pack <- copy_as(remote_pack, as || name), + {:ok, _} = unzip(archive, pack_info, remote_pack, pack) do + # Fallback can't contain a pack.json file, since that would cause the fallback-src-sha256 + # in it to depend on itself + if pack_info[:fallback] do + save_pack(pack) + else + {:ok, pack} + end + end + end + + @spec save_metadata(map(), t()) :: {:ok, t()} | {:error, File.posix()} + def save_metadata(metadata, %__MODULE__{} = pack) do + pack + |> Map.put(:pack, metadata) + |> save_pack() + end + + @spec update_metadata(String.t(), map()) :: {:ok, t()} | {:error, File.posix()} + def update_metadata(name, data) do + with {:ok, pack} <- load_pack(name) do + if fallback_sha_changed?(pack, data) do + update_sha_and_save_metadata(pack, data) + else + save_metadata(data, pack) + end + end + end + + @spec load_pack(String.t()) :: {:ok, t()} | {:error, :not_found} + def load_pack(name) do + pack_file = Path.join([emoji_path(), name, "pack.json"]) + + if File.exists?(pack_file) do + pack = + pack_file + |> File.read!() + |> from_json() + |> Map.put(:pack_file, pack_file) + |> Map.put(:path, Path.dirname(pack_file)) + |> Map.put(:name, name) + + {:ok, pack} + else + {:error, :not_found} + end + end + + @spec emoji_path() :: Path.t() + defp emoji_path do + [:instance, :static_dir] + |> Pleroma.Config.get!() + |> Path.join("emoji") + end + + defp validate_emoji_not_exists(shortcode, force \\ false) + defp validate_emoji_not_exists(_shortcode, true), do: :ok + + defp validate_emoji_not_exists(shortcode, _) do + case Emoji.get(shortcode) do + nil -> :ok + _ -> {:error, :already_exists} + end + end + defp write_pack_contents(path) do pack = %__MODULE__{ files: files_from_path(path), @@ -201,7 +228,7 @@ defmodule Pleroma.Emoji.Pack do } case save_pack(pack) do - :ok -> Path.basename(path) + {:ok, _pack} -> Path.basename(path) _ -> nil end end @@ -216,7 +243,8 @@ defmodule Pleroma.Emoji.Pack do # FIXME: Copy-pasted from Pleroma.Emoji/load_from_file_stream/2 # Create a map of shortcodes to filenames from emoji.txt - File.read!(txt_path) + txt_path + |> File.read!() |> String.split("\n") |> Enum.map(&String.trim/1) |> Enum.map(fn line -> @@ -226,21 +254,18 @@ defmodule Pleroma.Emoji.Pack do [name, file | _] -> file_dir_name = Path.dirname(file) - file = - if String.ends_with?(path, file_dir_name) do - Path.basename(file) - else - file - end - - {name, file} + if String.ends_with?(path, file_dir_name) do + {name, Path.basename(file)} + else + {name, file} + end _ -> nil end end) - |> Enum.filter(& &1) - |> Enum.into(%{}) + |> Enum.reject(&is_nil/1) + |> Map.new() else # If there's no emoji.txt, assume all files # that are of certain extensions from the config are emojis and import them all @@ -249,60 +274,20 @@ defmodule Pleroma.Emoji.Pack do end end - @spec list_remote(String.t()) :: {:ok, map()} - def list_remote(url) do - uri = - url - |> String.trim() - |> URI.parse() - - with {_, true} <- {:shareable, shareable_packs_available?(uri)} do - packs = - uri - |> URI.merge("/api/pleroma/emoji/packs") - |> to_string() - |> Tesla.get!() - |> Map.get(:body) - |> Jason.decode!() - - {:ok, packs} - end - end - - @spec list_local() :: {:ok, map()} - def list_local do - emoji_path = emoji_path() - - # Create the directory first if it does not exist. This is probably the first request made - # with the API so it should be sufficient - with {:create_dir, :ok} <- {:create_dir, File.mkdir_p(emoji_path)}, - {:ls, {:ok, results}} <- {:ls, File.ls(emoji_path)} do - packs = - results - |> Enum.map(&load_pack/1) - |> Enum.filter(& &1) - |> Enum.map(&validate_pack/1) - |> Map.new() - - {:ok, packs} - end - end - defp validate_pack(pack) do - if downloadable?(pack) do - archive = fetch_archive(pack) - archive_sha = :crypto.hash(:sha256, archive) |> Base.encode16() + info = + if downloadable?(pack) do + archive = fetch_archive(pack) + archive_sha = :crypto.hash(:sha256, archive) |> Base.encode16() - info = pack.pack |> Map.put("can-download", true) |> Map.put("download-sha256", archive_sha) + else + Map.put(pack.pack, "can-download", false) + end - {pack.name, Map.put(pack, :pack, info)} - else - info = Map.put(pack.pack, "can-download", false) - {pack.name, Map.put(pack, :pack, info)} - end + Map.put(pack, :pack, info) end defp downloadable?(pack) do @@ -315,26 +300,6 @@ defmodule Pleroma.Emoji.Pack do end) end - @spec get_archive(String.t()) :: {:ok, binary()} - def get_archive(name) do - with {_, %__MODULE__{} = pack} <- {:exists?, load_pack(name)}, - {_, true} <- {:can_download?, downloadable?(pack)} do - {:ok, fetch_archive(pack)} - end - end - - defp fetch_archive(pack) do - hash = :crypto.hash(:md5, File.read!(pack.pack_file)) - - case Cachex.get!(:emoji_packs_cache, pack.name) do - %{hash: ^hash, pack_data: archive} -> - archive - - _ -> - create_archive_and_cache(pack, hash) - end - end - defp create_archive_and_cache(pack, hash) do files = ['pack.json' | Enum.map(pack.files, fn {_, file} -> to_charlist(file) end)] @@ -356,152 +321,221 @@ defmodule Pleroma.Emoji.Pack do result end - @spec download(String.t(), String.t(), String.t()) :: :ok - def download(name, url, as) do - uri = - url - |> String.trim() - |> URI.parse() - - with {_, true} <- {:shareable, shareable_packs_available?(uri)} do - remote_pack = - uri - |> URI.merge("/api/pleroma/emoji/packs/#{name}") - |> to_string() - |> Tesla.get!() - |> Map.get(:body) - |> Jason.decode!() - - result = - case remote_pack["pack"] do - %{"share-files" => true, "can-download" => true, "download-sha256" => sha} -> - {:ok, - %{ - sha: sha, - url: URI.merge(uri, "/api/pleroma/emoji/packs/#{name}/archive") |> to_string() - }} - - %{"fallback-src" => src, "fallback-src-sha256" => sha} when is_binary(src) -> - {:ok, - %{ - sha: sha, - url: src, - fallback: true - }} + defp save_pack(pack) do + with {:ok, json} <- Jason.encode(pack, pretty: true), + :ok <- File.write(pack.pack_file, json) do + {:ok, pack} + end + end - _ -> - {:error, - "The pack was not set as shared and there is no fallback src to download from"} - end + defp from_json(json) do + map = Jason.decode!(json) - with {:ok, %{sha: sha, url: url} = pinfo} <- result, - %{body: archive} <- Tesla.get!(url), - {_, true} <- {:checksum, Base.decode16!(sha) == :crypto.hash(:sha256, archive)} do - local_name = as || name + struct(__MODULE__, %{files: map["files"], pack: map["pack"]}) + end - path = Path.join(emoji_path(), local_name) + defp validate_shareable_packs_available(uri) do + with {:ok, %{"links" => links}} <- uri |> URI.merge("/.well-known/nodeinfo") |> http_get(), + # Get the actual nodeinfo address and fetch it + {:ok, %{"metadata" => %{"features" => features}}} <- + links |> List.last() |> Map.get("href") |> http_get() do + if Enum.member?(features, "shareable_emoji_packs") do + :ok + else + {:error, :not_shareable} + end + end + end - pack = %__MODULE__{ - name: local_name, - path: path, - files: remote_pack["files"], - pack_file: Path.join(path, "pack.json") - } + defp validate_not_empty(list) do + if Enum.all?(list, fn i -> is_binary(i) and i != "" end) do + :ok + else + {:error, :empty_values} + end + end - File.mkdir_p!(pack.path) + defp save_file(file, pack, filename) do + file_path = Path.join(pack.path, filename) + create_subdirs(file_path) - files = Enum.map(remote_pack["files"], fn {_, path} -> to_charlist(path) end) - # Fallback cannot contain a pack.json file - files = if pinfo[:fallback], do: files, else: ['pack.json' | files] + case file do + %Plug.Upload{path: upload_path} -> + # Copy the uploaded file from the temporary directory + with {:ok, _} <- File.copy(upload_path, file_path), do: :ok - {:ok, _} = :zip.unzip(archive, cwd: to_charlist(pack.path), file_list: files) + url when is_binary(url) -> + # Download and write the file + file_contents = Tesla.get!(url).body + File.write(file_path, file_contents) + end + end - # Fallback can't contain a pack.json file, since that would cause the fallback-src-sha256 - # in it to depend on itself - if pinfo[:fallback] do - save_pack(pack) - end + defp put_emoji(pack, shortcode, filename) do + files = Map.put(pack.files, shortcode, filename) + %{pack | files: files} + end - :ok - end + defp delete_emoji(pack, shortcode) do + files = Map.delete(pack.files, shortcode) + %{pack | files: files} + end + + defp rename_file(pack, filename, new_filename) do + old_path = Path.join(pack.path, filename) + new_path = Path.join(pack.path, new_filename) + create_subdirs(new_path) + + with :ok <- File.rename(old_path, new_path) do + remove_dir_if_empty(old_path, filename) end end - defp save_pack(pack), do: File.write(pack.pack_file, Jason.encode!(pack, pretty: true)) + defp create_subdirs(file_path) do + if String.contains?(file_path, "/") do + file_path + |> Path.dirname() + |> File.mkdir_p!() + end + end - @spec save_metadata(map(), t()) :: {:ok, t()} | {:error, File.posix()} - def save_metadata(metadata, %__MODULE__{} = pack) do - pack = Map.put(pack, :pack, metadata) + defp remove_file(pack, shortcode) do + with {:ok, filename} <- get_filename(pack, shortcode), + emoji <- Path.join(pack.path, filename), + :ok <- File.rm(emoji) do + remove_dir_if_empty(emoji, filename) + end + end - with :ok <- save_pack(pack) do - {:ok, pack} + defp remove_dir_if_empty(emoji, filename) do + dir = Path.dirname(emoji) + + if String.contains?(filename, "/") and File.ls!(dir) == [] do + File.rmdir!(dir) + else + :ok end end - @spec update_metadata(String.t(), map()) :: {:ok, t()} | {:error, File.posix()} - def update_metadata(name, data) do - pack = load_pack(name) + defp get_filename(pack, shortcode) do + with %{^shortcode => filename} when is_binary(filename) <- pack.files, + true <- pack.path |> Path.join(filename) |> File.exists?() do + {:ok, filename} + else + _ -> {:error, :doesnt_exist} + end + end - fb_sha_changed? = - not is_nil(data["fallback-src"]) and data["fallback-src"] != pack.pack["fallback-src"] + defp http_get(%URI{} = url), do: url |> to_string() |> http_get() - with {_, true} <- {:update?, fb_sha_changed?}, - {:ok, %{body: zip}} <- Tesla.get(data["fallback-src"]), - {:ok, f_list} <- :zip.unzip(zip, [:memory]), - {_, true} <- {:has_all_files?, has_all_files?(pack.files, f_list)} do - fallback_sha = :crypto.hash(:sha256, zip) |> Base.encode16() + defp http_get(url) do + with {:ok, %{body: body}} <- url |> Pleroma.HTTP.get() do + Jason.decode(body) + end + end - data - |> Map.put("fallback-src-sha256", fallback_sha) - |> save_metadata(pack) + defp list_packs_dir do + emoji_path = emoji_path() + # Create the directory first if it does not exist. This is probably the first request made + # with the API so it should be sufficient + with {:create_dir, :ok} <- {:create_dir, File.mkdir_p(emoji_path)}, + {:ls, {:ok, results}} <- {:ls, File.ls(emoji_path)} do + {:ok, results} else - {:update?, _} -> save_metadata(data, pack) - e -> e + {:create_dir, {:error, e}} -> {:error, :create_dir, e} + {:ls, {:error, e}} -> {:error, :ls, e} end end - # Check if all files from the pack.json are in the archive - defp has_all_files?(files, f_list) do - Enum.all?(files, fn {_, from_manifest} -> - List.keyfind(f_list, to_charlist(from_manifest), 0) - end) + defp validate_downloadable(pack) do + if downloadable?(pack), do: :ok, else: {:error, :cant_download} end - @spec load_pack(String.t()) :: t() | nil - def load_pack(name) do - pack_file = Path.join([emoji_path(), name, "pack.json"]) + defp copy_as(remote_pack, local_name) do + path = Path.join(emoji_path(), local_name) - if File.exists?(pack_file) do - pack_file - |> File.read!() - |> from_json() - |> Map.put(:pack_file, pack_file) - |> Map.put(:path, Path.dirname(pack_file)) - |> Map.put(:name, name) + %__MODULE__{ + name: local_name, + path: path, + files: remote_pack["files"], + pack_file: Path.join(path, "pack.json") + } + end + + defp unzip(archive, pack_info, remote_pack, local_pack) do + with :ok <- File.mkdir_p!(local_pack.path) do + files = Enum.map(remote_pack["files"], fn {_, path} -> to_charlist(path) end) + # Fallback cannot contain a pack.json file + files = if pack_info[:fallback], do: files, else: ['pack.json' | files] + + :zip.unzip(archive, cwd: to_charlist(local_pack.path), file_list: files) end end - defp from_json(json) do - map = Jason.decode!(json) + defp fetch_pack_info(remote_pack, uri, name) do + case remote_pack["pack"] do + %{"share-files" => true, "can-download" => true, "download-sha256" => sha} -> + {:ok, + %{ + sha: sha, + url: URI.merge(uri, "/api/pleroma/emoji/packs/#{name}/archive") |> to_string() + }} + + %{"fallback-src" => src, "fallback-src-sha256" => sha} when is_binary(src) -> + {:ok, + %{ + sha: sha, + url: src, + fallback: true + }} - struct(__MODULE__, %{files: map["files"], pack: map["pack"]}) + _ -> + {:error, "The pack was not set as shared and there is no fallback src to download from"} + end + end + + defp download_archive(url, sha) do + with {:ok, %{body: archive}} <- Tesla.get(url) do + if Base.decode16!(sha) == :crypto.hash(:sha256, archive) do + {:ok, archive} + else + {:error, :imvalid_checksum} + end + end + end + + defp fetch_archive(pack) do + hash = :crypto.hash(:md5, File.read!(pack.pack_file)) + + case Cachex.get!(:emoji_packs_cache, pack.name) do + %{hash: ^hash, pack_data: archive} -> archive + _ -> create_archive_and_cache(pack, hash) + end + end + + defp fallback_sha_changed?(pack, data) do + is_binary(data[:"fallback-src"]) and data[:"fallback-src"] != pack.pack["fallback-src"] + end + + defp update_sha_and_save_metadata(pack, data) do + with {:ok, %{body: zip}} <- Tesla.get(data[:"fallback-src"]), + :ok <- validate_has_all_files(pack, zip) do + fallback_sha = :sha256 |> :crypto.hash(zip) |> Base.encode16() + + data + |> Map.put("fallback-src-sha256", fallback_sha) + |> save_metadata(pack) + end end - defp shareable_packs_available?(uri) do - uri - |> URI.merge("/.well-known/nodeinfo") - |> to_string() - |> Tesla.get!() - |> Map.get(:body) - |> Jason.decode!() - |> Map.get("links") - |> List.last() - |> Map.get("href") - # Get the actual nodeinfo address and fetch it - |> Tesla.get!() - |> Map.get(:body) - |> Jason.decode!() - |> get_in(["metadata", "features"]) - |> Enum.member?("shareable_emoji_packs") + defp validate_has_all_files(pack, zip) do + with {:ok, f_list} <- :zip.unzip(zip, [:memory]) do + # Check if all files from the pack.json are in the archive + pack.files + |> Enum.all?(fn {_, from_manifest} -> + List.keyfind(f_list, to_charlist(from_manifest), 0) + end) + |> if(do: :ok, else: {:error, :incomplete}) + end end end diff --git a/lib/pleroma/mfa.ex b/lib/pleroma/mfa.ex index 2b77f5426..01b743f4f 100644 --- a/lib/pleroma/mfa.ex +++ b/lib/pleroma/mfa.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.MFA do diff --git a/lib/pleroma/mfa/backup_codes.ex b/lib/pleroma/mfa/backup_codes.ex index 2b5ec34f8..9875310ff 100644 --- a/lib/pleroma/mfa/backup_codes.ex +++ b/lib/pleroma/mfa/backup_codes.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.MFA.BackupCodes do diff --git a/lib/pleroma/mfa/changeset.ex b/lib/pleroma/mfa/changeset.ex index 9b020aa8e..77c4fa202 100644 --- a/lib/pleroma/mfa/changeset.ex +++ b/lib/pleroma/mfa/changeset.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.MFA.Changeset do diff --git a/lib/pleroma/mfa/settings.ex b/lib/pleroma/mfa/settings.ex index 2764b889c..de6e2228f 100644 --- a/lib/pleroma/mfa/settings.ex +++ b/lib/pleroma/mfa/settings.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.MFA.Settings do diff --git a/lib/pleroma/mfa/token.ex b/lib/pleroma/mfa/token.ex index 25ff7fb29..0b2449971 100644 --- a/lib/pleroma/mfa/token.ex +++ b/lib/pleroma/mfa/token.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.MFA.Token do diff --git a/lib/pleroma/mfa/totp.ex b/lib/pleroma/mfa/totp.ex index 1407afc57..d2ea2b3aa 100644 --- a/lib/pleroma/mfa/totp.ex +++ b/lib/pleroma/mfa/totp.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.MFA.TOTP do diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index e678fd415..546c4ea01 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -9,11 +9,13 @@ defmodule Pleroma.Object do import Ecto.Changeset alias Pleroma.Activity + alias Pleroma.Config alias Pleroma.Object alias Pleroma.Object.Fetcher alias Pleroma.ObjectTombstone alias Pleroma.Repo alias Pleroma.User + alias Pleroma.Workers.AttachmentsCleanupWorker require Logger @@ -138,12 +140,17 @@ defmodule Pleroma.Object do def normalize(_, _, _), do: nil - # Owned objects can only be mutated by their owner - def authorize_mutation(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}), - do: actor == ap_id + # Owned objects can only be accessed by their owner + def authorize_access(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}) do + if actor == ap_id do + :ok + else + {:error, :forbidden} + end + end - # Legacy objects can be mutated by anybody - def authorize_mutation(%Object{}, %User{}), do: true + # Legacy objects can be accessed by anybody + def authorize_access(%Object{}, %User{}), do: :ok @spec get_cached_by_ap_id(String.t()) :: Object.t() | nil def get_cached_by_ap_id(ap_id) do @@ -183,27 +190,37 @@ defmodule Pleroma.Object do def delete(%Object{data: %{"id" => id}} = object) do with {:ok, _obj} = swap_object_with_tombstone(object), deleted_activity = Activity.delete_all_by_object_ap_id(id), - {:ok, true} <- Cachex.del(:object_cache, "object:#{id}"), - {:ok, _} <- Cachex.del(:web_resp_cache, URI.parse(id).path) do - with true <- Pleroma.Config.get([:instance, :cleanup_attachments]) do - {:ok, _} = - Pleroma.Workers.AttachmentsCleanupWorker.enqueue("cleanup_attachments", %{ - "object" => object - }) - end + {:ok, _} <- invalid_object_cache(object) do + cleanup_attachments( + Config.get([:instance, :cleanup_attachments]), + %{"object" => object} + ) {:ok, object, deleted_activity} end end - def prune(%Object{data: %{"id" => id}} = object) do + @spec cleanup_attachments(boolean(), %{required(:object) => map()}) :: + {:ok, Oban.Job.t() | nil} + def cleanup_attachments(true, %{"object" => _} = params) do + AttachmentsCleanupWorker.enqueue("cleanup_attachments", params) + end + + def cleanup_attachments(_, _), do: {:ok, nil} + + def prune(%Object{data: %{"id" => _id}} = object) do with {:ok, object} <- Repo.delete(object), - {:ok, true} <- Cachex.del(:object_cache, "object:#{id}"), - {:ok, _} <- Cachex.del(:web_resp_cache, URI.parse(id).path) do + {:ok, _} <- invalid_object_cache(object) do {:ok, object} end end + def invalid_object_cache(%Object{data: %{"id" => id}}) do + with {:ok, true} <- Cachex.del(:object_cache, "object:#{id}") do + Cachex.del(:web_resp_cache, URI.parse(id).path) + end + end + def set_cache(%Object{data: %{"id" => ap_id}} = object) do Cachex.put(:object_cache, "object:#{ap_id}", object) {:ok, object} diff --git a/lib/pleroma/plugs/authentication_plug.ex b/lib/pleroma/plugs/authentication_plug.ex index ae4a235bd..057ea42f1 100644 --- a/lib/pleroma/plugs/authentication_plug.ex +++ b/lib/pleroma/plugs/authentication_plug.ex @@ -16,6 +16,11 @@ defmodule Pleroma.Plugs.AuthenticationPlug do :crypt.crypt(password, password_hash) == password_hash end + def checkpw(password, "$2" <> _ = password_hash) do + # Handle bcrypt passwords for Mastodon migration + Bcrypt.verify_pass(password, password_hash) + end + def checkpw(password, "$pbkdf2" <> _ = password_hash) do Pbkdf2.verify_pass(password, password_hash) end @@ -25,6 +30,25 @@ defmodule Pleroma.Plugs.AuthenticationPlug do false end + def maybe_update_password(%User{password_hash: "$2" <> _} = user, password) do + do_update_password(user, password) + end + + def maybe_update_password(%User{password_hash: "$6" <> _} = user, password) do + do_update_password(user, password) + end + + def maybe_update_password(user, _), do: {:ok, user} + + defp do_update_password(user, password) do + user + |> User.password_update_changeset(%{ + "password" => password, + "password_confirmation" => password + }) + |> Pleroma.Repo.update() + end + def call(%{assigns: %{user: %User{}}} = conn, _), do: conn def call( @@ -36,7 +60,9 @@ defmodule Pleroma.Plugs.AuthenticationPlug do } = conn, _ ) do - if Pbkdf2.verify_pass(password, password_hash) do + if checkpw(password, password_hash) do + {:ok, auth_user} = maybe_update_password(auth_user, password) + conn |> assign(:user, auth_user) |> OAuthScopesPlug.skip_plug() diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex index 762d813d9..1be1a3a5b 100644 --- a/lib/pleroma/upload.ex +++ b/lib/pleroma/upload.ex @@ -134,7 +134,7 @@ defmodule Pleroma.Upload do end end - defp prepare_upload(%{"img" => "data:image/" <> image_data}, opts) do + defp prepare_upload(%{img: "data:image/" <> image_data}, opts) do parsed = Regex.named_captures(~r/(?<filetype>jpeg|png|gif);base64,(?<data>.*)/, image_data) data = Base.decode64!(parsed["data"], ignore: :whitespace) hash = String.downcase(Base.encode16(:crypto.hash(:sha256, data))) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index cba391072..224dfd023 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -749,7 +749,19 @@ defmodule Pleroma.User do {:error, "Not subscribed!"} end + @spec unfollow(User.t(), User.t()) :: {:ok, User.t(), Activity.t()} | {:error, String.t()} def unfollow(%User{} = follower, %User{} = followed) do + case do_unfollow(follower, followed) do + {:ok, follower, followed} -> + {:ok, follower, Utils.fetch_latest_follow(follower, followed)} + + error -> + error + end + end + + @spec do_unfollow(User.t(), User.t()) :: {:ok, User.t(), User.t()} | {:error, String.t()} + defp do_unfollow(%User{} = follower, %User{} = followed) do case get_follow_state(follower, followed) do state when state in [:follow_pending, :follow_accept] -> FollowingRelationship.unfollow(follower, followed) @@ -760,7 +772,7 @@ defmodule Pleroma.User do |> update_following_count() |> set_cache() - {:ok, follower, Utils.fetch_latest_follow(follower, followed)} + {:ok, follower, followed} nil -> {:error, "Not subscribed!"} @@ -1204,7 +1216,9 @@ defmodule Pleroma.User do def get_recipients_from_activity(%Activity{recipients: to, actor: actor}) do to = [actor | to] - User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false}) + query = User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false}) + + query |> Repo.all() end @@ -1400,15 +1414,13 @@ defmodule Pleroma.User do user |> get_followers() |> Enum.filter(& &1.local) - |> Enum.each(fn follower -> - follower |> update_following_count() |> set_cache() - end) + |> Enum.each(&set_cache(update_following_count(&1))) # Only update local user counts, remote will be update during the next pull. user |> get_friends() |> Enum.filter(& &1.local) - |> Enum.each(&update_follower_count/1) + |> Enum.each(&do_unfollow(user, &1)) {:ok, user} end @@ -1430,6 +1442,25 @@ defmodule Pleroma.User do BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id}) end + defp delete_and_invalidate_cache(%User{} = user) do + invalidate_cache(user) + Repo.delete(user) + end + + defp delete_or_deactivate(%User{local: false} = user), do: delete_and_invalidate_cache(user) + + defp delete_or_deactivate(%User{local: true} = user) do + status = account_status(user) + + if status == :confirmation_pending do + delete_and_invalidate_cache(user) + else + user + |> change(%{deactivated: true, email: nil}) + |> update_and_set_cache() + end + end + def perform(:force_password_reset, user), do: force_password_reset(user) @spec perform(atom(), User.t()) :: {:ok, User.t()} @@ -1451,14 +1482,7 @@ defmodule Pleroma.User do delete_user_activities(user) - if user.local do - user - |> change(%{deactivated: true, email: nil}) - |> update_and_set_cache() - else - invalidate_cache(user) - Repo.delete(user) - end + delete_or_deactivate(user) end def perform(:deactivate_async, user, status), do: deactivate(user, status) diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex index 3a3b04793..293bbc082 100644 --- a/lib/pleroma/user/query.ex +++ b/lib/pleroma/user/query.ex @@ -167,20 +167,18 @@ defmodule Pleroma.User.Query do end defp compose_query({:recipients_from_activity, to}, query) do - query - |> join(:left, [u], r in FollowingRelationship, - as: :relationships, - on: r.follower_id == u.id - ) - |> join(:left, [relationships: r], f in User, - as: :following, - on: f.id == r.following_id - ) - |> where( - [u, following: f, relationships: r], - u.ap_id in ^to or (f.follower_address in ^to and r.state == ^:follow_accept) + following_query = + from(u in User, + join: f in FollowingRelationship, + on: u.id == f.following_id, + where: f.state == ^:follow_accept, + where: u.follower_address in ^to, + select: f.follower_id + ) + + from(u in query, + where: u.ap_id in ^to or u.id in subquery(following_query) ) - |> distinct(true) end defp compose_query({:order_by, key}, query) do diff --git a/lib/pleroma/user_relationship.ex b/lib/pleroma/user_relationship.ex index 235ad427c..6dfdd2860 100644 --- a/lib/pleroma/user_relationship.ex +++ b/lib/pleroma/user_relationship.ex @@ -87,6 +87,22 @@ defmodule Pleroma.UserRelationship do source_to_target_rel_types \\ nil, target_to_source_rel_types \\ nil ) + + def dictionary( + _source_users, + _target_users, + [] = _source_to_target_rel_types, + [] = _target_to_source_rel_types + ) do + [] + end + + def dictionary( + source_users, + target_users, + source_to_target_rel_types, + target_to_source_rel_types + ) when is_list(source_users) and is_list(target_users) do source_user_ids = User.binary_id(source_users) target_user_ids = User.binary_id(target_users) @@ -138,11 +154,16 @@ defmodule Pleroma.UserRelationship do def view_relationships_option(%User{} = reading_user, actors, opts) do {source_to_target_rel_types, target_to_source_rel_types} = - if opts[:source_mutes_only] do - # This option is used for rendering statuses (FE needs `muted` flag for each one anyways) - {[:mute], []} - else - {[:block, :mute, :notification_mute, :reblog_mute], [:block, :inverse_subscription]} + case opts[:subset] do + :source_mutes -> + # Used for statuses rendering (FE needs `muted` flag for each status when statuses load) + {[:mute], []} + + nil -> + {[:block, :mute, :notification_mute, :reblog_mute], [:block, :inverse_subscription]} + + unknown -> + raise "Unsupported :subset option value: #{inspect(unknown)}" end user_relationships = @@ -153,7 +174,17 @@ defmodule Pleroma.UserRelationship do target_to_source_rel_types ) - following_relationships = FollowingRelationship.all_between_user_sets([reading_user], actors) + following_relationships = + case opts[:subset] do + :source_mutes -> + [] + + nil -> + FollowingRelationship.all_between_user_sets([reading_user], actors) + + unknown -> + raise "Unsupported :subset option value: #{inspect(unknown)}" + end %{user_relationships: user_relationships, following_relationships: following_relationships} end diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 9821173d0..647ceb3ba 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -22,6 +22,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do alias Pleroma.Web.ActivityPub.Pipeline alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.Utils + alias Pleroma.Web.AdminAPI alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.AdminAPI.ConfigView alias Pleroma.Web.AdminAPI.ModerationLogView @@ -30,14 +31,14 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do alias Pleroma.Web.AdminAPI.Search alias Pleroma.Web.CommonAPI alias Pleroma.Web.Endpoint + alias Pleroma.Web.MastodonAPI alias Pleroma.Web.MastodonAPI.AppView - alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.OAuth.App alias Pleroma.Web.Router require Logger - @descriptions_json Pleroma.Docs.JSON.compile() + @descriptions Pleroma.Docs.JSON.compile() @users_page_size 50 plug( @@ -280,8 +281,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do }) conn - |> put_view(Pleroma.Web.AdminAPI.StatusView) - |> render("index.json", %{activities: activities, as: :activity, skip_relationships: false}) + |> put_view(AdminAPI.StatusView) + |> render("index.json", %{activities: activities, as: :activity}) end def list_user_statuses(conn, %{"nickname" => nickname} = params) do @@ -299,8 +300,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do }) conn - |> put_view(StatusView) - |> render("index.json", %{activities: activities, as: :activity, skip_relationships: false}) + |> put_view(MastodonAPI.StatusView) + |> render("index.json", %{activities: activities, as: :activity}) else _ -> {:error, :not_found} end @@ -829,14 +830,14 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do }) conn - |> put_view(Pleroma.Web.AdminAPI.StatusView) - |> render("index.json", %{activities: activities, as: :activity, skip_relationships: false}) + |> put_view(AdminAPI.StatusView) + |> render("index.json", %{activities: activities, as: :activity}) end def status_show(conn, %{"id" => id}) do with %Activity{} = activity <- Activity.get_by_id(id) do conn - |> put_view(StatusView) + |> put_view(MastodonAPI.StatusView) |> render("show.json", %{activity: activity}) else _ -> errors(conn, {:error, :not_found}) @@ -861,7 +862,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do }) conn - |> put_view(StatusView) + |> put_view(MastodonAPI.StatusView) |> render("show.json", %{activity: activity}) end end @@ -897,9 +898,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do end def config_descriptions(conn, _params) do - conn - |> Plug.Conn.put_resp_content_type("application/json") - |> Plug.Conn.send_resp(200, @descriptions_json) + descriptions = Enum.filter(@descriptions, &whitelisted_config?/1) + + json(conn, descriptions) end def config_show(conn, %{"only_db" => true}) do @@ -954,7 +955,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do def config_update(conn, %{"configs" => configs}) do with :ok <- configurable_from_database(conn) do {_errors, results} = - Enum.map(configs, fn + configs + |> Enum.filter(&whitelisted_config?/1) + |> Enum.map(fn %{"group" => group, "key" => key, "delete" => true} = params -> ConfigDB.delete(%{group: group, key: key, subkeys: params["subkeys"]}) @@ -1016,6 +1019,28 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do end end + defp whitelisted_config?(group, key) do + if whitelisted_configs = Config.get(:database_config_whitelist) do + Enum.any?(whitelisted_configs, fn + {whitelisted_group} -> + group == inspect(whitelisted_group) + + {whitelisted_group, whitelisted_key} -> + group == inspect(whitelisted_group) && key == inspect(whitelisted_key) + end) + else + true + end + end + + defp whitelisted_config?(%{"group" => group, "key" => key}) do + whitelisted_config?(group, key) + end + + defp whitelisted_config?(%{:group => group} = config) do + whitelisted_config?(group, config[:key]) + end + def reload_emoji(conn, _params) do Pleroma.Emoji.reload() diff --git a/lib/pleroma/web/admin_api/views/account_view.ex b/lib/pleroma/web/admin_api/views/account_view.ex index a16a3ebf0..46dadb5ee 100644 --- a/lib/pleroma/web/admin_api/views/account_view.ex +++ b/lib/pleroma/web/admin_api/views/account_view.ex @@ -6,7 +6,9 @@ defmodule Pleroma.Web.AdminAPI.AccountView do use Pleroma.Web, :view alias Pleroma.User + alias Pleroma.Web.AdminAPI alias Pleroma.Web.AdminAPI.AccountView + alias Pleroma.Web.MastodonAPI alias Pleroma.Web.MediaProxy def render("index.json", %{users: users, count: count, page_size: page_size}) do @@ -119,6 +121,13 @@ defmodule Pleroma.Web.AdminAPI.AccountView do } end + def merge_account_views(%User{} = user) do + MastodonAPI.AccountView.render("show.json", %{user: user}) + |> Map.merge(AdminAPI.AccountView.render("show.json", %{user: user})) + end + + def merge_account_views(_), do: %{} + defp parse_error([]), do: "" defp parse_error(errors) do diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex index d50969b2a..f432b8c2c 100644 --- a/lib/pleroma/web/admin_api/views/report_view.ex +++ b/lib/pleroma/web/admin_api/views/report_view.ex @@ -7,10 +7,13 @@ defmodule Pleroma.Web.AdminAPI.ReportView do alias Pleroma.HTML alias Pleroma.User + alias Pleroma.Web.AdminAPI alias Pleroma.Web.AdminAPI.Report alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.MastodonAPI.StatusView + defdelegate merge_account_views(user), to: AdminAPI.AccountView + def render("index.json", %{reports: reports}) do %{ reports: @@ -41,8 +44,7 @@ defmodule Pleroma.Web.AdminAPI.ReportView do statuses: StatusView.render("index.json", %{ activities: statuses, - as: :activity, - skip_relationships: false + as: :activity }), state: report.data["state"], notes: render(__MODULE__, "index_notes.json", %{notes: report.report_notes}) @@ -70,11 +72,4 @@ defmodule Pleroma.Web.AdminAPI.ReportView do created_at: Utils.to_masto_date(inserted_at) } end - - defp merge_account_views(%User{} = user) do - Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user}) - |> Map.merge(Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user})) - end - - defp merge_account_views(_), do: %{} end diff --git a/lib/pleroma/web/admin_api/views/status_view.ex b/lib/pleroma/web/admin_api/views/status_view.ex index 3637dee24..500800be2 100644 --- a/lib/pleroma/web/admin_api/views/status_view.ex +++ b/lib/pleroma/web/admin_api/views/status_view.ex @@ -7,24 +7,19 @@ defmodule Pleroma.Web.AdminAPI.StatusView do require Pleroma.Constants - alias Pleroma.User - alias Pleroma.Web.MastodonAPI.StatusView + alias Pleroma.Web.AdminAPI + alias Pleroma.Web.MastodonAPI + + defdelegate merge_account_views(user), to: AdminAPI.AccountView def render("index.json", opts) do safe_render_many(opts.activities, __MODULE__, "show.json", opts) end def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do - user = StatusView.get_user(activity.data["actor"]) + user = MastodonAPI.StatusView.get_user(activity.data["actor"]) - StatusView.render("show.json", opts) + MastodonAPI.StatusView.render("show.json", opts) |> Map.merge(%{account: merge_account_views(user)}) end - - defp merge_account_views(%User{} = user) do - Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user}) - |> Map.merge(Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user})) - end - - defp merge_account_views(_), do: %{} end diff --git a/lib/pleroma/web/api_spec/helpers.ex b/lib/pleroma/web/api_spec/helpers.ex index 183df43ee..a9cfe0fed 100644 --- a/lib/pleroma/web/api_spec/helpers.ex +++ b/lib/pleroma/web/api_spec/helpers.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Web.ApiSpec.Helpers do alias OpenApiSpex.Operation alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.BooleanLike def request_body(description, schema_ref, opts \\ []) do media_types = ["application/json", "multipart/form-data", "application/x-www-form-urlencoded"] @@ -47,6 +48,15 @@ defmodule Pleroma.Web.ApiSpec.Helpers do ] end + def with_relationships_param do + Operation.parameter( + :with_relationships, + :query, + BooleanLike, + "Embed relationships into accounts." + ) + end + def empty_object_response do Operation.response("Empty object", "application/json", %Schema{type: :object, example: %{}}) end @@ -54,4 +64,8 @@ defmodule Pleroma.Web.ApiSpec.Helpers do def empty_array_response do Operation.response("Empty array", "application/json", %Schema{type: :array, example: []}) end + + def no_content_response do + Operation.response("No Content", "application/json", %Schema{type: :string, example: ""}) + end end diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex index 70069d6f9..20572f8ea 100644 --- a/lib/pleroma/web/api_spec/operations/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -155,8 +155,10 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do security: [%{"oAuth" => ["read:accounts"]}], description: "Accounts which follow the given account, if network is not hidden by the account owner.", - parameters: - [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}] ++ pagination_params(), + parameters: [ + %Reference{"$ref": "#/components/parameters/accountIdOrNickname"}, + with_relationships_param() | pagination_params() + ], responses: %{ 200 => Operation.response("Accounts", "application/json", array_of_accounts()) } @@ -171,8 +173,10 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do security: [%{"oAuth" => ["read:accounts"]}], description: "Accounts which the given account is following, if network is not hidden by the account owner.", - parameters: - [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}] ++ pagination_params(), + parameters: [ + %Reference{"$ref": "#/components/parameters/accountIdOrNickname"}, + with_relationships_param() | pagination_params() + ], responses: %{200 => Operation.response("Accounts", "application/json", array_of_accounts())} } end @@ -367,15 +371,18 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do title: "AccountCreateRequest", description: "POST body for creating an account", type: :object, + required: [:username, :password, :agreement], properties: %{ reason: %Schema{ type: :string, + nullable: true, description: "Text that will be reviewed by moderators if registrations require manual approval" }, username: %Schema{type: :string, description: "The desired username for the account"}, email: %Schema{ type: :string, + nullable: true, description: "The email address to be used for login. Required when `account_activation_required` is enabled.", format: :email @@ -386,29 +393,39 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do format: :password }, agreement: %Schema{ - type: :boolean, + allOf: [BooleanLike], description: "Whether the user agrees to the local rules, terms, and policies. These should be presented to the user in order to allow them to consent before setting this parameter to TRUE." }, locale: %Schema{ type: :string, + nullable: true, description: "The language of the confirmation email that will be sent" }, # Pleroma-specific properties: - fullname: %Schema{type: :string, description: "Full name"}, - bio: %Schema{type: :string, description: "Bio", default: ""}, + fullname: %Schema{type: :string, nullable: true, description: "Full name"}, + bio: %Schema{type: :string, description: "Bio", nullable: true, default: ""}, captcha_solution: %Schema{ type: :string, + nullable: true, description: "Provider-specific captcha solution" }, - captcha_token: %Schema{type: :string, description: "Provider-specific captcha token"}, - captcha_answer_data: %Schema{type: :string, description: "Provider-specific captcha data"}, + captcha_token: %Schema{ + type: :string, + nullable: true, + description: "Provider-specific captcha token" + }, + captcha_answer_data: %Schema{ + type: :string, + nullable: true, + description: "Provider-specific captcha data" + }, token: %Schema{ type: :string, + nullable: true, description: "Invite token required when the registrations aren't public" } }, - required: [:username, :password, :agreement], example: %{ "username" => "cofe", "email" => "cofe@example.com", @@ -446,29 +463,35 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do type: :object, properties: %{ bot: %Schema{ - type: :boolean, + allOf: [BooleanLike], + nullable: true, description: "Whether the account has a bot flag." }, display_name: %Schema{ type: :string, + nullable: true, description: "The display name to use for the profile." }, note: %Schema{type: :string, description: "The account bio."}, avatar: %Schema{ type: :string, + nullable: true, description: "Avatar image encoded using multipart/form-data", format: :binary }, header: %Schema{ type: :string, + nullable: true, description: "Header image encoded using multipart/form-data", format: :binary }, locked: %Schema{ - type: :boolean, + allOf: [BooleanLike], + nullable: true, description: "Whether manual approval of follow requests is required." }, fields_attributes: %Schema{ + nullable: true, oneOf: [ %Schema{type: :array, items: attribute_field()}, %Schema{type: :object, additionalProperties: %Schema{type: attribute_field()}} @@ -487,48 +510,66 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do # Pleroma-specific fields no_rich_text: %Schema{ - type: :boolean, + allOf: [BooleanLike], + nullable: true, description: "html tags are stripped from all statuses requested from the API" }, - hide_followers: %Schema{type: :boolean, description: "user's followers will be hidden"}, - hide_follows: %Schema{type: :boolean, description: "user's follows will be hidden"}, + hide_followers: %Schema{ + allOf: [BooleanLike], + nullable: true, + description: "user's followers will be hidden" + }, + hide_follows: %Schema{ + allOf: [BooleanLike], + nullable: true, + description: "user's follows will be hidden" + }, hide_followers_count: %Schema{ - type: :boolean, + allOf: [BooleanLike], + nullable: true, description: "user's follower count will be hidden" }, hide_follows_count: %Schema{ - type: :boolean, + allOf: [BooleanLike], + nullable: true, description: "user's follow count will be hidden" }, hide_favorites: %Schema{ - type: :boolean, + allOf: [BooleanLike], + nullable: true, description: "user's favorites timeline will be hidden" }, show_role: %Schema{ - type: :boolean, + allOf: [BooleanLike], + nullable: true, description: "user's role (e.g admin, moderator) will be exposed to anyone in the API" }, default_scope: VisibilityScope, pleroma_settings_store: %Schema{ type: :object, + nullable: true, description: "Opaque user settings to be saved on the backend." }, skip_thread_containment: %Schema{ - type: :boolean, + allOf: [BooleanLike], + nullable: true, description: "Skip filtering out broken threads" }, allow_following_move: %Schema{ - type: :boolean, + allOf: [BooleanLike], + nullable: true, description: "Allows automatically follow moved following accounts" }, pleroma_background_image: %Schema{ type: :string, + nullable: true, description: "Sets the background image of the user.", format: :binary }, discoverable: %Schema{ - type: :boolean, + allOf: [BooleanLike], + nullable: true, description: "Discovery of this account in search results and other services is allowed." }, @@ -624,7 +665,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do description: "POST body for muting an account", type: :object, properties: %{ - uri: %Schema{type: :string, format: :uri} + uri: %Schema{type: :string, nullable: true, format: :uri} }, required: [:uri] } @@ -637,7 +678,8 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do type: :object, properties: %{ notifications: %Schema{ - type: :boolean, + allOf: [BooleanLike], + nullable: true, description: "Mute notifications in addition to statuses? Defaults to true.", default: true } diff --git a/lib/pleroma/web/api_spec/operations/app_operation.ex b/lib/pleroma/web/api_spec/operations/app_operation.ex index f6ccd073f..ae01cbbec 100644 --- a/lib/pleroma/web/api_spec/operations/app_operation.ex +++ b/lib/pleroma/web/api_spec/operations/app_operation.ex @@ -105,7 +105,11 @@ defmodule Pleroma.Web.ApiSpec.AppOperation do description: "Space separated list of scopes", default: "read" }, - website: %Schema{type: :string, description: "A URL to the homepage of your app"} + website: %Schema{ + type: :string, + nullable: true, + description: "A URL to the homepage of your app" + } }, required: [:client_name, :redirect_uris], example: %{ diff --git a/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex b/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex new file mode 100644 index 000000000..7c08fbaa7 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex @@ -0,0 +1,102 @@ +# 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.EmojiReactionOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.Account + alias Pleroma.Web.ApiSpec.Schemas.FlakeID + alias Pleroma.Web.ApiSpec.Schemas.Status + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def index_operation do + %Operation{ + tags: ["Emoji Reactions"], + summary: + "Get an object of emoji to account mappings with accounts that reacted to the post", + parameters: [ + Operation.parameter(:id, :path, FlakeID, "Status ID", required: true), + Operation.parameter(:emoji, :path, :string, "Filter by a single unicode emoji", + required: false + ) + ], + security: [%{"oAuth" => ["read:statuses"]}], + operationId: "EmojiReactionController.index", + responses: %{ + 200 => array_of_reactions_response() + } + } + end + + def create_operation do + %Operation{ + tags: ["Emoji Reactions"], + summary: "React to a post with a unicode emoji", + parameters: [ + Operation.parameter(:id, :path, FlakeID, "Status ID", required: true), + Operation.parameter(:emoji, :path, :string, "A single character unicode emoji", + required: true + ) + ], + security: [%{"oAuth" => ["write:statuses"]}], + operationId: "EmojiReactionController.create", + responses: %{ + 200 => Operation.response("Status", "application/json", Status) + } + } + end + + def delete_operation do + %Operation{ + tags: ["Emoji Reactions"], + summary: "Remove a reaction to a post with a unicode emoji", + parameters: [ + Operation.parameter(:id, :path, FlakeID, "Status ID", required: true), + Operation.parameter(:emoji, :path, :string, "A single character unicode emoji", + required: true + ) + ], + security: [%{"oAuth" => ["write:statuses"]}], + operationId: "EmojiReactionController.delete", + responses: %{ + 200 => Operation.response("Status", "application/json", Status) + } + } + end + + defp array_of_reactions_response do + Operation.response("Array of Emoji Reactions", "application/json", %Schema{ + type: :array, + items: emoji_reaction(), + example: [emoji_reaction().example] + }) + end + + defp emoji_reaction do + %Schema{ + title: "EmojiReaction", + type: :object, + properties: %{ + name: %Schema{type: :string, description: "Emoji"}, + count: %Schema{type: :integer, description: "Count of reactions with this emoji"}, + me: %Schema{type: :boolean, description: "Did I react with this emoji?"}, + accounts: %Schema{ + type: :array, + items: Account, + description: "Array of accounts reacted with this emoji" + } + }, + example: %{ + "name" => "😱", + "count" => 1, + "me" => false, + "accounts" => [Account.schema().example] + } + } + end +end diff --git a/lib/pleroma/web/api_spec/operations/filter_operation.ex b/lib/pleroma/web/api_spec/operations/filter_operation.ex index 53e57b46b..31e576f99 100644 --- a/lib/pleroma/web/api_spec/operations/filter_operation.ex +++ b/lib/pleroma/web/api_spec/operations/filter_operation.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.ApiSpec.FilterOperation do alias OpenApiSpex.Operation alias OpenApiSpex.Schema alias Pleroma.Web.ApiSpec.Helpers + alias Pleroma.Web.ApiSpec.Schemas.BooleanLike def open_api_operation(action) do operation = String.to_existing_atom("#{action}_operation") @@ -171,7 +172,7 @@ defmodule Pleroma.Web.ApiSpec.FilterOperation do type: :object, properties: %{ irreversible: %Schema{ - type: :bolean, + allOf: [BooleanLike], description: "Should the server irreversibly drop matching entities from home and notifications?", default: false @@ -199,12 +200,14 @@ defmodule Pleroma.Web.ApiSpec.FilterOperation do "Array of enumerable strings `home`, `notifications`, `public`, `thread`. At least one context must be specified." }, irreversible: %Schema{ - type: :bolean, + allOf: [BooleanLike], + nullable: true, description: "Should the server irreversibly drop matching entities from home and notifications?" }, whole_word: %Schema{ - type: :bolean, + allOf: [BooleanLike], + nullable: true, description: "Consider word boundaries?", default: true } diff --git a/lib/pleroma/web/api_spec/operations/instance_operation.ex b/lib/pleroma/web/api_spec/operations/instance_operation.ex index 880bd3f1b..d5c335d0c 100644 --- a/lib/pleroma/web/api_spec/operations/instance_operation.ex +++ b/lib/pleroma/web/api_spec/operations/instance_operation.ex @@ -125,11 +125,17 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do }, avatar_upload_limit: %Schema{type: :integer, description: "The title of the website"}, background_upload_limit: %Schema{type: :integer, description: "The title of the website"}, - banner_upload_limit: %Schema{type: :integer, description: "The title of the website"} + banner_upload_limit: %Schema{type: :integer, description: "The title of the website"}, + background_image: %Schema{ + type: :string, + format: :uri, + description: "The background image for the website" + } }, example: %{ "avatar_upload_limit" => 2_000_000, "background_upload_limit" => 4_000_000, + "background_image" => "/static/image.png", "banner_upload_limit" => 4_000_000, "description" => "A Pleroma instance, an alternative fediverse server", "email" => "lain@lain.com", diff --git a/lib/pleroma/web/api_spec/operations/marker_operation.ex b/lib/pleroma/web/api_spec/operations/marker_operation.ex index 06620492a..714ef1f99 100644 --- a/lib/pleroma/web/api_spec/operations/marker_operation.ex +++ b/lib/pleroma/web/api_spec/operations/marker_operation.ex @@ -110,14 +110,16 @@ defmodule Pleroma.Web.ApiSpec.MarkerOperation do properties: %{ notifications: %Schema{ type: :object, + nullable: true, properties: %{ - last_read_id: %Schema{type: :string} + last_read_id: %Schema{nullable: true, type: :string} } }, home: %Schema{ type: :object, + nullable: true, properties: %{ - last_read_id: %Schema{type: :string} + last_read_id: %Schema{nullable: true, type: :string} } } }, diff --git a/lib/pleroma/web/api_spec/operations/media_operation.ex b/lib/pleroma/web/api_spec/operations/media_operation.ex new file mode 100644 index 000000000..d9c3c42db --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/media_operation.ex @@ -0,0 +1,132 @@ +# 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.MediaOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Helpers + alias Pleroma.Web.ApiSpec.Schemas.ApiError + alias Pleroma.Web.ApiSpec.Schemas.Attachment + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def create_operation do + %Operation{ + tags: ["media"], + summary: "Upload media as attachment", + description: "Creates an attachment to be used with a new status.", + operationId: "MediaController.create", + security: [%{"oAuth" => ["write:media"]}], + requestBody: Helpers.request_body("Parameters", create_request()), + responses: %{ + 200 => Operation.response("Media", "application/json", Attachment), + 401 => Operation.response("Media", "application/json", ApiError), + 422 => Operation.response("Media", "application/json", ApiError) + } + } + end + + defp create_request do + %Schema{ + title: "MediaCreateRequest", + description: "POST body for creating an attachment", + type: :object, + required: [:file], + properties: %{ + file: %Schema{ + type: :string, + format: :binary, + description: "The file to be attached, using multipart form data." + }, + description: %Schema{ + type: :string, + description: "A plain-text description of the media, for accessibility purposes." + }, + focus: %Schema{ + type: :string, + description: "Two floating points (x,y), comma-delimited, ranging from -1.0 to 1.0." + } + } + } + end + + def update_operation do + %Operation{ + tags: ["media"], + summary: "Upload media as attachment", + description: "Creates an attachment to be used with a new status.", + operationId: "MediaController.update", + security: [%{"oAuth" => ["write:media"]}], + parameters: [id_param()], + requestBody: Helpers.request_body("Parameters", update_request()), + responses: %{ + 200 => Operation.response("Media", "application/json", Attachment), + 400 => Operation.response("Media", "application/json", ApiError), + 401 => Operation.response("Media", "application/json", ApiError), + 422 => Operation.response("Media", "application/json", ApiError) + } + } + end + + defp update_request do + %Schema{ + title: "MediaUpdateRequest", + description: "POST body for updating an attachment", + type: :object, + properties: %{ + file: %Schema{ + type: :string, + format: :binary, + description: "The file to be attached, using multipart form data." + }, + description: %Schema{ + type: :string, + description: "A plain-text description of the media, for accessibility purposes." + }, + focus: %Schema{ + type: :string, + description: "Two floating points (x,y), comma-delimited, ranging from -1.0 to 1.0." + } + } + } + end + + def show_operation do + %Operation{ + tags: ["media"], + summary: "Show Uploaded media attachment", + operationId: "MediaController.show", + parameters: [id_param()], + security: [%{"oAuth" => ["read:media"]}], + responses: %{ + 200 => Operation.response("Media", "application/json", Attachment), + 401 => Operation.response("Media", "application/json", ApiError), + 422 => Operation.response("Media", "application/json", ApiError) + } + } + end + + def create2_operation do + %Operation{ + tags: ["media"], + summary: "Upload media as attachment", + description: "Creates an attachment to be used with a new status.", + operationId: "MediaController.create2", + security: [%{"oAuth" => ["write:media"]}], + requestBody: Helpers.request_body("Parameters", create_request()), + responses: %{ + 202 => Operation.response("Media", "application/json", Attachment), + 422 => Operation.response("Media", "application/json", ApiError), + 500 => Operation.response("Media", "application/json", ApiError) + } + } + end + + defp id_param do + Operation.parameter(:id, :path, :string, "The ID of the Attachment entity") + end +end diff --git a/lib/pleroma/web/api_spec/operations/notification_operation.ex b/lib/pleroma/web/api_spec/operations/notification_operation.ex index 64adc5319..46e72f8bf 100644 --- a/lib/pleroma/web/api_spec/operations/notification_operation.ex +++ b/lib/pleroma/web/api_spec/operations/notification_operation.ex @@ -145,7 +145,7 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do } end - defp notification do + def notification do %Schema{ title: "Notification", description: "Response schema for a notification", diff --git a/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex new file mode 100644 index 000000000..90922c064 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex @@ -0,0 +1,187 @@ +# 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.PleromaAccountOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship + alias Pleroma.Web.ApiSpec.Schemas.ApiError + alias Pleroma.Web.ApiSpec.Schemas.FlakeID + alias Pleroma.Web.ApiSpec.StatusOperation + + import Pleroma.Web.ApiSpec.Helpers + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def confirmation_resend_operation do + %Operation{ + tags: ["Accounts"], + summary: "Resend confirmation email. Expects `email` or `nickname`", + operationId: "PleromaAPI.AccountController.confirmation_resend", + parameters: [ + Operation.parameter(:email, :query, :string, "Email of that needs to be verified", + example: "cofe@cofe.io" + ), + Operation.parameter( + :nickname, + :query, + :string, + "Nickname of user that needs to be verified", + example: "cofefe" + ) + ], + responses: %{ + 204 => no_content_response() + } + } + end + + def update_avatar_operation do + %Operation{ + tags: ["Accounts"], + summary: "Set/clear user avatar image", + operationId: "PleromaAPI.AccountController.update_avatar", + requestBody: + request_body("Parameters", update_avatar_or_background_request(), required: true), + security: [%{"oAuth" => ["write:accounts"]}], + responses: %{ + 200 => update_response(), + 403 => Operation.response("Forbidden", "application/json", ApiError) + } + } + end + + def update_banner_operation do + %Operation{ + tags: ["Accounts"], + summary: "Set/clear user banner image", + operationId: "PleromaAPI.AccountController.update_banner", + requestBody: request_body("Parameters", update_banner_request(), required: true), + security: [%{"oAuth" => ["write:accounts"]}], + responses: %{ + 200 => update_response() + } + } + end + + def update_background_operation do + %Operation{ + tags: ["Accounts"], + summary: "Set/clear user background image", + operationId: "PleromaAPI.AccountController.update_background", + security: [%{"oAuth" => ["write:accounts"]}], + requestBody: + request_body("Parameters", update_avatar_or_background_request(), required: true), + responses: %{ + 200 => update_response() + } + } + end + + def favourites_operation do + %Operation{ + tags: ["Accounts"], + summary: "Returns favorites timeline of any user", + operationId: "PleromaAPI.AccountController.favourites", + parameters: [id_param() | pagination_params()], + security: [%{"oAuth" => ["read:favourites"]}], + responses: %{ + 200 => + Operation.response( + "Array of Statuses", + "application/json", + StatusOperation.array_of_statuses() + ), + 403 => Operation.response("Forbidden", "application/json", ApiError), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + def subscribe_operation do + %Operation{ + tags: ["Accounts"], + summary: "Subscribe to receive notifications for all statuses posted by a user", + operationId: "PleromaAPI.AccountController.subscribe", + parameters: [id_param()], + security: [%{"oAuth" => ["follow", "write:follows"]}], + responses: %{ + 200 => Operation.response("Relationship", "application/json", AccountRelationship), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + def unsubscribe_operation do + %Operation{ + tags: ["Accounts"], + summary: "Unsubscribe to stop receiving notifications from user statuses", + operationId: "PleromaAPI.AccountController.unsubscribe", + parameters: [id_param()], + security: [%{"oAuth" => ["follow", "write:follows"]}], + responses: %{ + 200 => Operation.response("Relationship", "application/json", AccountRelationship), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + defp id_param do + Operation.parameter(:id, :path, FlakeID, "Account ID", + example: "9umDrYheeY451cQnEe", + required: true + ) + end + + defp update_avatar_or_background_request do + %Schema{ + title: "PleromaAccountUpdateAvatarOrBackgroundRequest", + type: :object, + properties: %{ + img: %Schema{ + nullable: true, + type: :string, + format: :binary, + description: "Image encoded using `multipart/form-data` or an empty string to clear" + } + } + } + end + + defp update_banner_request do + %Schema{ + title: "PleromaAccountUpdateBannerRequest", + type: :object, + properties: %{ + banner: %Schema{ + type: :string, + nullable: true, + format: :binary, + description: "Image encoded using `multipart/form-data` or an empty string to clear" + } + } + } + end + + defp update_response do + Operation.response("PleromaAccountUpdateResponse", "application/json", %Schema{ + type: :object, + properties: %{ + url: %Schema{ + type: :string, + format: :uri, + nullable: true, + description: "Image URL" + } + }, + example: %{ + "url" => + "https://cofe.party/media/9d0add56-bcb6-4c0f-8225-cbbd0b6dd773/13eadb6972c9ccd3f4ffa3b8196f0e0d38b4d2f27594457c52e52946c054cd9a.gif" + } + }) + end +end diff --git a/lib/pleroma/web/api_spec/operations/pleroma_conversation_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_conversation_operation.ex new file mode 100644 index 000000000..e885eab20 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/pleroma_conversation_operation.ex @@ -0,0 +1,106 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.PleromaConversationOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.Conversation + alias Pleroma.Web.ApiSpec.Schemas.FlakeID + alias Pleroma.Web.ApiSpec.StatusOperation + + import Pleroma.Web.ApiSpec.Helpers + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def show_operation do + %Operation{ + tags: ["Conversations"], + summary: "The conversation with the given ID", + parameters: [ + Operation.parameter(:id, :path, :string, "Conversation ID", + example: "123", + required: true + ) + ], + security: [%{"oAuth" => ["read:statuses"]}], + operationId: "PleromaAPI.ConversationController.show", + responses: %{ + 200 => Operation.response("Conversation", "application/json", Conversation) + } + } + end + + def statuses_operation do + %Operation{ + tags: ["Conversations"], + summary: "Timeline for a given conversation", + parameters: [ + Operation.parameter(:id, :path, :string, "Conversation ID", + example: "123", + required: true + ) + | pagination_params() + ], + security: [%{"oAuth" => ["read:statuses"]}], + operationId: "PleromaAPI.ConversationController.statuses", + responses: %{ + 200 => + Operation.response( + "Array of Statuses", + "application/json", + StatusOperation.array_of_statuses() + ) + } + } + end + + def update_operation do + %Operation{ + tags: ["Conversations"], + summary: "Update a conversation. Used to change the set of recipients.", + parameters: [ + Operation.parameter(:id, :path, :string, "Conversation ID", + example: "123", + required: true + ), + Operation.parameter( + :recipients, + :query, + %Schema{type: :array, items: FlakeID}, + "A list of ids of users that should receive posts to this conversation. This will replace the current list of recipients, so submit the full list. The owner of owner of the conversation will always be part of the set of recipients, though.", + required: true + ) + ], + security: [%{"oAuth" => ["write:conversations"]}], + operationId: "PleromaAPI.ConversationController.update", + responses: %{ + 200 => Operation.response("Conversation", "application/json", Conversation) + } + } + end + + def mark_as_read_operation do + %Operation{ + tags: ["Conversations"], + summary: "Marks all user's conversations as read", + security: [%{"oAuth" => ["write:conversations"]}], + operationId: "PleromaAPI.ConversationController.mark_as_read", + responses: %{ + 200 => + Operation.response( + "Array of Conversations that were marked as read", + "application/json", + %Schema{ + type: :array, + items: Conversation, + example: [Conversation.schema().example] + } + ) + } + } + end +end diff --git a/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex new file mode 100644 index 000000000..567688ff5 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex @@ -0,0 +1,390 @@ +# 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.PleromaEmojiPackOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.ApiError + + import Pleroma.Web.ApiSpec.Helpers + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def remote_operation do + %Operation{ + tags: ["Emoji Packs"], + summary: "Make request to another instance for emoji packs list", + security: [%{"oAuth" => ["write"]}], + parameters: [url_param()], + operationId: "PleromaAPI.EmojiPackController.remote", + responses: %{ + 200 => emoji_packs_response(), + 500 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def index_operation do + %Operation{ + tags: ["Emoji Packs"], + summary: "Lists local custom emoji packs", + operationId: "PleromaAPI.EmojiPackController.index", + responses: %{ + 200 => emoji_packs_response() + } + } + end + + def show_operation do + %Operation{ + tags: ["Emoji Packs"], + summary: "Show emoji pack", + operationId: "PleromaAPI.EmojiPackController.show", + parameters: [name_param()], + responses: %{ + 200 => Operation.response("Emoji Pack", "application/json", emoji_pack()), + 400 => Operation.response("Bad Request", "application/json", ApiError), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + def archive_operation do + %Operation{ + tags: ["Emoji Packs"], + summary: "Requests a local pack archive from the instance", + operationId: "PleromaAPI.EmojiPackController.archive", + parameters: [name_param()], + responses: %{ + 200 => + Operation.response("Archive file", "application/octet-stream", %Schema{ + type: :string, + format: :binary + }), + 403 => Operation.response("Forbidden", "application/json", ApiError), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + def download_operation do + %Operation{ + tags: ["Emoji Packs"], + summary: "Download pack from another instance", + operationId: "PleromaAPI.EmojiPackController.download", + security: [%{"oAuth" => ["write"]}], + requestBody: request_body("Parameters", download_request(), required: true), + responses: %{ + 200 => ok_response(), + 500 => Operation.response("Error", "application/json", ApiError) + } + } + end + + defp download_request do + %Schema{ + type: :object, + required: [:url, :name], + properties: %{ + url: %Schema{ + type: :string, + format: :uri, + description: "URL of the instance to download from" + }, + name: %Schema{type: :string, format: :uri, description: "Pack Name"}, + as: %Schema{type: :string, format: :uri, description: "Save as"} + } + } + end + + def create_operation do + %Operation{ + tags: ["Emoji Packs"], + summary: "Create an empty pack", + operationId: "PleromaAPI.EmojiPackController.create", + security: [%{"oAuth" => ["write"]}], + parameters: [name_param()], + responses: %{ + 200 => ok_response(), + 400 => Operation.response("Not Found", "application/json", ApiError), + 409 => Operation.response("Conflict", "application/json", ApiError), + 500 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def delete_operation do + %Operation{ + tags: ["Emoji Packs"], + summary: "Delete a custom emoji pack", + operationId: "PleromaAPI.EmojiPackController.delete", + security: [%{"oAuth" => ["write"]}], + parameters: [name_param()], + responses: %{ + 200 => ok_response(), + 400 => Operation.response("Bad Request", "application/json", ApiError), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + def update_operation do + %Operation{ + tags: ["Emoji Packs"], + summary: "Updates (replaces) pack metadata", + operationId: "PleromaAPI.EmojiPackController.update", + security: [%{"oAuth" => ["write"]}], + requestBody: request_body("Parameters", update_request(), required: true), + parameters: [name_param()], + responses: %{ + 200 => Operation.response("Metadata", "application/json", metadata()), + 400 => Operation.response("Bad Request", "application/json", ApiError) + } + } + end + + def add_file_operation do + %Operation{ + tags: ["Emoji Packs"], + summary: "Add new file to the pack", + operationId: "PleromaAPI.EmojiPackController.add_file", + security: [%{"oAuth" => ["write"]}], + requestBody: request_body("Parameters", add_file_request(), required: true), + parameters: [name_param()], + responses: %{ + 200 => Operation.response("Files Object", "application/json", files_object()), + 400 => Operation.response("Bad Request", "application/json", ApiError), + 409 => Operation.response("Conflict", "application/json", ApiError) + } + } + end + + defp add_file_request do + %Schema{ + type: :object, + required: [:file], + properties: %{ + file: %Schema{ + description: + "File needs to be uploaded with the multipart request or link to remote file", + anyOf: [ + %Schema{type: :string, format: :binary}, + %Schema{type: :string, format: :uri} + ] + }, + shortcode: %Schema{ + type: :string, + description: + "Shortcode for new emoji, must be unique for all emoji. If not sended, shortcode will be taken from original filename." + }, + filename: %Schema{ + type: :string, + description: + "New emoji file name. If not specified will be taken from original filename." + } + } + } + end + + def update_file_operation do + %Operation{ + tags: ["Emoji Packs"], + summary: "Add new file to the pack", + operationId: "PleromaAPI.EmojiPackController.update_file", + security: [%{"oAuth" => ["write"]}], + requestBody: request_body("Parameters", update_file_request(), required: true), + parameters: [name_param()], + responses: %{ + 200 => Operation.response("Files Object", "application/json", files_object()), + 400 => Operation.response("Bad Request", "application/json", ApiError), + 409 => Operation.response("Conflict", "application/json", ApiError) + } + } + end + + defp update_file_request do + %Schema{ + type: :object, + required: [:shortcode, :new_shortcode, :new_filename], + properties: %{ + shortcode: %Schema{ + type: :string, + description: "Emoji file shortcode" + }, + new_shortcode: %Schema{ + type: :string, + description: "New emoji file shortcode" + }, + new_filename: %Schema{ + type: :string, + description: "New filename for emoji file" + }, + force: %Schema{ + type: :boolean, + description: "With true value to overwrite existing emoji with new shortcode", + default: false + } + } + } + end + + def delete_file_operation do + %Operation{ + tags: ["Emoji Packs"], + summary: "Delete emoji file from pack", + operationId: "PleromaAPI.EmojiPackController.delete_file", + security: [%{"oAuth" => ["write"]}], + parameters: [ + name_param(), + Operation.parameter(:shortcode, :query, :string, "File shortcode", + example: "cofe", + required: true + ) + ], + responses: %{ + 200 => Operation.response("Files Object", "application/json", files_object()), + 400 => Operation.response("Bad Request", "application/json", ApiError) + } + } + end + + def import_from_filesystem_operation do + %Operation{ + tags: ["Emoji Packs"], + summary: "Imports packs from filesystem", + operationId: "PleromaAPI.EmojiPackController.import", + security: [%{"oAuth" => ["write"]}], + responses: %{ + 200 => + Operation.response("Array of imported pack names", "application/json", %Schema{ + type: :array, + items: %Schema{type: :string} + }) + } + } + end + + defp name_param do + Operation.parameter(:name, :path, :string, "Pack Name", example: "cofe", required: true) + end + + defp url_param do + Operation.parameter( + :url, + :query, + %Schema{type: :string, format: :uri}, + "URL of the instance", + required: true + ) + end + + defp ok_response do + Operation.response("Ok", "application/json", %Schema{type: :string, example: "ok"}) + end + + defp emoji_packs_response do + Operation.response( + "Object with pack names as keys and pack contents as values", + "application/json", + %Schema{ + type: :object, + additionalProperties: emoji_pack(), + example: %{ + "emojos" => emoji_pack().example + } + } + ) + end + + defp emoji_pack do + %Schema{ + title: "EmojiPack", + type: :object, + properties: %{ + files: files_object(), + pack: %Schema{ + type: :object, + properties: %{ + license: %Schema{type: :string}, + homepage: %Schema{type: :string, format: :uri}, + description: %Schema{type: :string}, + "can-download": %Schema{type: :boolean}, + "share-files": %Schema{type: :boolean}, + "download-sha256": %Schema{type: :string} + } + } + }, + example: %{ + "files" => %{"emacs" => "emacs.png", "guix" => "guix.png"}, + "pack" => %{ + "license" => "Test license", + "homepage" => "https://pleroma.social", + "description" => "Test description", + "can-download" => true, + "share-files" => true, + "download-sha256" => "57482F30674FD3DE821FF48C81C00DA4D4AF1F300209253684ABA7075E5FC238" + } + } + } + end + + defp files_object do + %Schema{ + type: :object, + additionalProperties: %Schema{type: :string}, + description: "Object with emoji names as keys and filenames as values" + } + end + + defp update_request do + %Schema{ + type: :object, + properties: %{ + metadata: %Schema{ + type: :object, + description: "Metadata to replace the old one", + properties: %{ + license: %Schema{type: :string}, + homepage: %Schema{type: :string, format: :uri}, + description: %Schema{type: :string}, + "fallback-src": %Schema{ + type: :string, + format: :uri, + description: "Fallback url to download pack from" + }, + "fallback-src-sha256": %Schema{ + type: :string, + description: "SHA256 encoded for fallback pack archive" + }, + "share-files": %Schema{type: :boolean, description: "Is pack allowed for sharing?"} + } + } + } + } + end + + defp metadata do + %Schema{ + type: :object, + properties: %{ + license: %Schema{type: :string}, + homepage: %Schema{type: :string, format: :uri}, + description: %Schema{type: :string}, + "fallback-src": %Schema{ + type: :string, + format: :uri, + description: "Fallback url to download pack from" + }, + "fallback-src-sha256": %Schema{ + type: :string, + description: "SHA256 encoded for fallback pack archive" + }, + "share-files": %Schema{type: :boolean, description: "Is pack allowed for sharing?"} + } + } + end +end diff --git a/lib/pleroma/web/api_spec/operations/pleroma_mascot_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_mascot_operation.ex new file mode 100644 index 000000000..8c5f37ea6 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/pleroma_mascot_operation.ex @@ -0,0 +1,79 @@ +# 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.PleromaMascotOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.ApiError + + import Pleroma.Web.ApiSpec.Helpers + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def show_operation do + %Operation{ + tags: ["Mascot"], + summary: "Gets user mascot image", + security: [%{"oAuth" => ["read:accounts"]}], + operationId: "PleromaAPI.MascotController.show", + responses: %{ + 200 => Operation.response("Mascot", "application/json", mascot()) + } + } + end + + def update_operation do + %Operation{ + tags: ["Mascot"], + summary: "Set/clear user avatar image", + description: + "Behaves exactly the same as `POST /api/v1/upload`. Can only accept images - any attempt to upload non-image files will be met with `HTTP 415 Unsupported Media Type`.", + operationId: "PleromaAPI.MascotController.update", + requestBody: + request_body( + "Parameters", + %Schema{ + type: :object, + properties: %{ + file: %Schema{type: :string, format: :binary} + } + }, + required: true + ), + security: [%{"oAuth" => ["write:accounts"]}], + responses: %{ + 200 => Operation.response("Mascot", "application/json", mascot()), + 415 => Operation.response("Unsupported Media Type", "application/json", ApiError) + } + } + end + + defp mascot do + %Schema{ + type: :object, + properties: %{ + id: %Schema{type: :string}, + url: %Schema{type: :string, format: :uri}, + type: %Schema{type: :string}, + pleroma: %Schema{ + type: :object, + properties: %{ + mime_type: %Schema{type: :string} + } + } + }, + example: %{ + "id" => "abcdefg", + "url" => "https://pleroma.example.org/media/abcdefg.png", + "type" => "image", + "pleroma" => %{ + "mime_type" => "image/png" + } + } + } + end +end diff --git a/lib/pleroma/web/api_spec/operations/pleroma_notification_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_notification_operation.ex new file mode 100644 index 000000000..636c39a15 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/pleroma_notification_operation.ex @@ -0,0 +1,42 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.PleromaNotificationOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.NotificationOperation + alias Pleroma.Web.ApiSpec.Schemas.ApiError + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def mark_as_read_operation do + %Operation{ + tags: ["Notifications"], + summary: "Mark notifications as read. Query parameters are mutually exclusive.", + parameters: [ + Operation.parameter(:id, :query, :string, "A single notification ID to read"), + Operation.parameter(:max_id, :query, :string, "Read all notifications up to this id") + ], + security: [%{"oAuth" => ["write:notifications"]}], + operationId: "PleromaAPI.NotificationController.mark_as_read", + responses: %{ + 200 => + Operation.response( + "A Notification or array of Motifications", + "application/json", + %Schema{ + anyOf: [ + %Schema{type: :array, items: NotificationOperation.notification()}, + NotificationOperation.notification() + ] + } + ), + 400 => Operation.response("Bad Request", "application/json", ApiError) + } + } + end +end diff --git a/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex new file mode 100644 index 000000000..85a22aa0b --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex @@ -0,0 +1,102 @@ +# 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.PleromaScrobbleOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Reference + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.Account + alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope + + import Pleroma.Web.ApiSpec.Helpers + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def create_operation do + %Operation{ + tags: ["Scrobbles"], + summary: "Creates a new Listen activity for an account", + security: [%{"oAuth" => ["write"]}], + operationId: "PleromaAPI.ScrobbleController.create", + requestBody: request_body("Parameters", create_request(), requried: true), + responses: %{ + 200 => Operation.response("Scrobble", "application/json", scrobble()) + } + } + end + + def index_operation do + %Operation{ + tags: ["Scrobbles"], + summary: "Requests a list of current and recent Listen activities for an account", + operationId: "PleromaAPI.ScrobbleController.index", + parameters: [ + %Reference{"$ref": "#/components/parameters/accountIdOrNickname"} | pagination_params() + ], + security: [%{"oAuth" => ["read"]}], + responses: %{ + 200 => + Operation.response("Array of Scrobble", "application/json", %Schema{ + type: :array, + items: scrobble() + }) + } + } + end + + defp create_request do + %Schema{ + type: :object, + required: [:title], + properties: %{ + title: %Schema{type: :string, description: "The title of the media playing"}, + album: %Schema{type: :string, description: "The album of the media playing"}, + artist: %Schema{type: :string, description: "The artist of the media playing"}, + length: %Schema{type: :integer, description: "The length of the media playing"}, + visibility: %Schema{ + allOf: [VisibilityScope], + default: "public", + description: "Scrobble visibility" + } + }, + example: %{ + "title" => "Some Title", + "artist" => "Some Artist", + "album" => "Some Album", + "length" => 180_000 + } + } + end + + defp scrobble do + %Schema{ + type: :object, + properties: %{ + id: %Schema{type: :string}, + account: Account, + title: %Schema{type: :string, description: "The title of the media playing"}, + album: %Schema{type: :string, description: "The album of the media playing"}, + artist: %Schema{type: :string, description: "The artist of the media playing"}, + length: %Schema{ + type: :integer, + description: "The length of the media playing", + nullable: true + }, + created_at: %Schema{type: :string, format: :"date-time"} + }, + example: %{ + "id" => "1234", + "account" => Account.schema().example, + "title" => "Some Title", + "artist" => "Some Artist", + "album" => "Some Album", + "length" => 180_000, + "created_at" => "2019-09-28T12:40:45.000Z" + } + } + end +end diff --git a/lib/pleroma/web/api_spec/operations/report_operation.ex b/lib/pleroma/web/api_spec/operations/report_operation.ex index da4d50703..b9b4c4f79 100644 --- a/lib/pleroma/web/api_spec/operations/report_operation.ex +++ b/lib/pleroma/web/api_spec/operations/report_operation.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.ApiSpec.ReportOperation do alias OpenApiSpex.Schema alias Pleroma.Web.ApiSpec.Helpers alias Pleroma.Web.ApiSpec.Schemas.ApiError + alias Pleroma.Web.ApiSpec.Schemas.BooleanLike def open_api_operation(action) do operation = String.to_existing_atom("#{action}_operation") @@ -37,15 +38,18 @@ defmodule Pleroma.Web.ApiSpec.ReportOperation do account_id: %Schema{type: :string, description: "ID of the account to report"}, status_ids: %Schema{ type: :array, + nullable: true, items: %Schema{type: :string}, description: "Array of Statuses to attach to the report, for context" }, comment: %Schema{ type: :string, + nullable: true, description: "Reason for the report" }, forward: %Schema{ - type: :boolean, + allOf: [BooleanLike], + nullable: true, default: false, description: "If the account is remote, should the report be forwarded to the remote admin?" diff --git a/lib/pleroma/web/api_spec/operations/search_operation.ex b/lib/pleroma/web/api_spec/operations/search_operation.ex index 6ea00a9a8..169c36d87 100644 --- a/lib/pleroma/web/api_spec/operations/search_operation.ex +++ b/lib/pleroma/web/api_spec/operations/search_operation.ex @@ -19,6 +19,7 @@ defmodule Pleroma.Web.ApiSpec.SearchOperation do apply(__MODULE__, operation, []) end + # Note: `with_relationships` param is not supported (PleromaFE uses this op for autocomplete) def account_search_operation do %Operation{ tags: ["Search"], @@ -96,8 +97,8 @@ defmodule Pleroma.Web.ApiSpec.SearchOperation do :query, %Schema{type: :integer}, "Offset" - ) - | pagination_params() + ), + with_relationships_param() | pagination_params() ], responses: %{ 200 => Operation.response("Results", "application/json", results()) @@ -138,8 +139,8 @@ defmodule Pleroma.Web.ApiSpec.SearchOperation do :query, %Schema{allOf: [BooleanLike], default: false}, "Only include accounts that the user is following" - ) - | pagination_params() + ), + with_relationships_param() | pagination_params() ], responses: %{ 200 => Operation.response("Results", "application/json", results2()) diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex index a6bb87560..0682ca6e5 100644 --- a/lib/pleroma/web/api_spec/operations/status_operation.ex +++ b/lib/pleroma/web/api_spec/operations/status_operation.ex @@ -349,10 +349,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do summary: "Bookmarked statuses", description: "Statuses the user has bookmarked", operationId: "StatusController.bookmarks", - parameters: [ - Operation.parameter(:with_relationships, :query, BooleanLike, "Include relationships") - | pagination_params() - ], + parameters: pagination_params(), security: [%{"oAuth" => ["read:bookmarks"]}], responses: %{ 200 => Operation.response("Array of Statuses", "application/json", array_of_statuses()) @@ -360,7 +357,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do } end - defp array_of_statuses do + def array_of_statuses do %Schema{type: :array, items: Status, example: [Status.schema().example]} end @@ -371,15 +368,18 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do properties: %{ status: %Schema{ type: :string, + nullable: true, description: "Text content of the status. If `media_ids` is provided, this becomes optional. Attaching a `poll` is optional while `status` is provided." }, media_ids: %Schema{ + nullable: true, type: :array, items: %Schema{type: :string}, description: "Array of Attachment ids to be attached as media." }, poll: %Schema{ + nullable: true, type: :object, required: [:options], properties: %{ @@ -390,26 +390,35 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do }, expires_in: %Schema{ type: :integer, + nullable: true, description: "Duration the poll should be open, in seconds. Must be provided with `poll[options]`" }, - multiple: %Schema{type: :boolean, description: "Allow multiple choices?"}, + multiple: %Schema{ + allOf: [BooleanLike], + nullable: true, + description: "Allow multiple choices?" + }, hide_totals: %Schema{ - type: :boolean, + allOf: [BooleanLike], + nullable: true, description: "Hide vote counts until the poll ends?" } } }, in_reply_to_id: %Schema{ + nullable: true, allOf: [FlakeID], description: "ID of the status being replied to, if status is a reply" }, sensitive: %Schema{ - type: :boolean, + allOf: [BooleanLike], + nullable: true, description: "Mark status and attached media as sensitive?" }, spoiler_text: %Schema{ type: :string, + nullable: true, description: "Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field." }, @@ -420,25 +429,33 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do description: "ISO 8601 Datetime at which to schedule a status. Providing this paramter will cause ScheduledStatus to be returned instead of Status. Must be at least 5 minutes in the future." }, - language: %Schema{type: :string, description: "ISO 639 language code for this status."}, + language: %Schema{ + type: :string, + nullable: true, + description: "ISO 639 language code for this status." + }, # Pleroma-specific properties: preview: %Schema{ - type: :boolean, + allOf: [BooleanLike], + nullable: true, description: "If set to `true` the post won't be actually posted, but the status entitiy would still be rendered back. This could be useful for previewing rich text/custom emoji, for example" }, content_type: %Schema{ type: :string, + nullable: true, description: "The MIME type of the status, it is transformed into HTML by the backend. You can get the list of the supported MIME types with the nodeinfo endpoint." }, to: %Schema{ type: :array, + nullable: true, items: %Schema{type: :string}, description: "A list of nicknames (like `lain@soykaf.club` or `lain` on the local server) that will be used to determine who is going to be addressed by this post. Using this will disable the implicit addressing by mentioned names in the `status` body, only the people in the `to` list will be addressed. The normal rules for for post visibility are not affected by this and will still apply" }, visibility: %Schema{ + nullable: true, anyOf: [ VisibilityScope, %Schema{type: :string, description: "`list:LIST_ID`", example: "LIST:123"} @@ -447,11 +464,13 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do "Visibility of the posted status. Besides standard MastoAPI values (`direct`, `private`, `unlisted` or `public`) it can be used to address a List by setting it to `list:LIST_ID`" }, expires_in: %Schema{ + nullable: true, type: :integer, description: "The number of seconds the posted activity should expire in. When a posted activity expires it will be deleted from the server, and a delete request for it will be federated. This needs to be longer than an hour." }, in_reply_to_conversation_id: %Schema{ + nullable: true, type: :string, description: "Will reply to a given conversation, addressing only the people who are part of the recipient set of that conversation. Sets the visibility to `direct`." diff --git a/lib/pleroma/web/api_spec/operations/subscription_operation.ex b/lib/pleroma/web/api_spec/operations/subscription_operation.ex index 663b8fa11..c575a87e6 100644 --- a/lib/pleroma/web/api_spec/operations/subscription_operation.ex +++ b/lib/pleroma/web/api_spec/operations/subscription_operation.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.ApiSpec.SubscriptionOperation do alias OpenApiSpex.Schema alias Pleroma.Web.ApiSpec.Helpers alias Pleroma.Web.ApiSpec.Schemas.ApiError + alias Pleroma.Web.ApiSpec.Schemas.BooleanLike alias Pleroma.Web.ApiSpec.Schemas.PushSubscription def open_api_operation(action) do @@ -109,19 +110,38 @@ defmodule Pleroma.Web.ApiSpec.SubscriptionOperation do required: [:endpoint, :keys] }, data: %Schema{ + nullable: true, type: :object, properties: %{ alerts: %Schema{ + nullable: true, type: :object, properties: %{ - follow: %Schema{type: :boolean, description: "Receive follow notifications?"}, + follow: %Schema{ + allOf: [BooleanLike], + nullable: true, + description: "Receive follow notifications?" + }, favourite: %Schema{ - type: :boolean, + allOf: [BooleanLike], + nullable: true, description: "Receive favourite notifications?" }, - reblog: %Schema{type: :boolean, description: "Receive reblog notifications?"}, - mention: %Schema{type: :boolean, description: "Receive mention notifications?"}, - poll: %Schema{type: :boolean, description: "Receive poll notifications?"} + reblog: %Schema{ + allOf: [BooleanLike], + nullable: true, + description: "Receive reblog notifications?" + }, + mention: %Schema{ + allOf: [BooleanLike], + nullable: true, + description: "Receive mention notifications?" + }, + poll: %Schema{ + allOf: [BooleanLike], + nullable: true, + description: "Receive poll notifications?" + } } } } @@ -154,19 +174,38 @@ defmodule Pleroma.Web.ApiSpec.SubscriptionOperation do type: :object, properties: %{ data: %Schema{ + nullable: true, type: :object, properties: %{ alerts: %Schema{ + nullable: true, type: :object, properties: %{ - follow: %Schema{type: :boolean, description: "Receive follow notifications?"}, + follow: %Schema{ + allOf: [BooleanLike], + nullable: true, + description: "Receive follow notifications?" + }, favourite: %Schema{ - type: :boolean, + allOf: [BooleanLike], + nullable: true, description: "Receive favourite notifications?" }, - reblog: %Schema{type: :boolean, description: "Receive reblog notifications?"}, - mention: %Schema{type: :boolean, description: "Receive mention notifications?"}, - poll: %Schema{type: :boolean, description: "Receive poll notifications?"} + reblog: %Schema{ + allOf: [BooleanLike], + nullable: true, + description: "Receive reblog notifications?" + }, + mention: %Schema{ + allOf: [BooleanLike], + nullable: true, + description: "Receive mention notifications?" + }, + poll: %Schema{ + allOf: [BooleanLike], + nullable: true, + description: "Receive poll notifications?" + } } } } diff --git a/lib/pleroma/web/api_spec/operations/timeline_operation.ex b/lib/pleroma/web/api_spec/operations/timeline_operation.ex index 1b89035d4..8e19bace7 100644 --- a/lib/pleroma/web/api_spec/operations/timeline_operation.ex +++ b/lib/pleroma/web/api_spec/operations/timeline_operation.ex @@ -27,8 +27,7 @@ defmodule Pleroma.Web.ApiSpec.TimelineOperation do local_param(), with_muted_param(), exclude_visibilities_param(), - reply_visibility_param(), - with_relationships_param() | pagination_params() + reply_visibility_param() | pagination_params() ], operationId: "TimelineController.home", responses: %{ @@ -44,7 +43,7 @@ defmodule Pleroma.Web.ApiSpec.TimelineOperation do description: "View statuses with a “direct” privacy, from your account or in your notifications", deprecated: true, - parameters: pagination_params(), + parameters: [with_muted_param() | pagination_params()], security: [%{"oAuth" => ["read:statuses"]}], operationId: "TimelineController.direct", responses: %{ @@ -63,8 +62,7 @@ defmodule Pleroma.Web.ApiSpec.TimelineOperation do only_media_param(), with_muted_param(), exclude_visibilities_param(), - reply_visibility_param(), - with_relationships_param() | pagination_params() + reply_visibility_param() | pagination_params() ], operationId: "TimelineController.public", responses: %{ @@ -109,8 +107,7 @@ defmodule Pleroma.Web.ApiSpec.TimelineOperation do local_param(), only_media_param(), with_muted_param(), - exclude_visibilities_param(), - with_relationships_param() | pagination_params() + exclude_visibilities_param() | pagination_params() ], operationId: "TimelineController.hashtag", responses: %{ @@ -134,8 +131,7 @@ defmodule Pleroma.Web.ApiSpec.TimelineOperation do required: true ), with_muted_param(), - exclude_visibilities_param(), - with_relationships_param() | pagination_params() + exclude_visibilities_param() | pagination_params() ], operationId: "TimelineController.list", responses: %{ @@ -153,10 +149,6 @@ defmodule Pleroma.Web.ApiSpec.TimelineOperation do } end - defp with_relationships_param do - Operation.parameter(:with_relationships, :query, BooleanLike, "Include relationships") - end - defp local_param do Operation.parameter( :local, diff --git a/lib/pleroma/web/api_spec/schemas/attachment.ex b/lib/pleroma/web/api_spec/schemas/attachment.ex index c146c416e..c6edf6d36 100644 --- a/lib/pleroma/web/api_spec/schemas/attachment.ex +++ b/lib/pleroma/web/api_spec/schemas/attachment.ex @@ -13,7 +13,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Attachment do type: :object, requried: [:id, :url, :preview_url], properties: %{ - id: %Schema{type: :string}, + id: %Schema{type: :string, description: "The ID of the attachment in the database."}, url: %Schema{ type: :string, format: :uri, diff --git a/lib/pleroma/web/auth/pleroma_authenticator.ex b/lib/pleroma/web/auth/pleroma_authenticator.ex index a8f554aa3..200ca03dc 100644 --- a/lib/pleroma/web/auth/pleroma_authenticator.ex +++ b/lib/pleroma/web/auth/pleroma_authenticator.ex @@ -16,7 +16,8 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do def get_user(%Plug.Conn{} = conn) do with {:ok, {name, password}} <- fetch_credentials(conn), {_, %User{} = user} <- {:user, fetch_user(name)}, - {_, true} <- {:checkpw, AuthenticationPlug.checkpw(password, user.password_hash)} do + {_, true} <- {:checkpw, AuthenticationPlug.checkpw(password, user.password_hash)}, + {:ok, user} <- AuthenticationPlug.maybe_update_password(user, password) do {:ok, user} else {:error, _reason} = error -> error diff --git a/lib/pleroma/web/auth/totp_authenticator.ex b/lib/pleroma/web/auth/totp_authenticator.ex index 04e489c83..1794e407c 100644 --- a/lib/pleroma/web/auth/totp_authenticator.ex +++ b/lib/pleroma/web/auth/totp_authenticator.ex @@ -1,10 +1,11 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.Auth.TOTPAuthenticator do alias Pleroma.MFA alias Pleroma.MFA.TOTP + alias Pleroma.Plugs.AuthenticationPlug alias Pleroma.User @doc "Verify code or check backup code." @@ -30,7 +31,7 @@ defmodule Pleroma.Web.Auth.TOTPAuthenticator do code ) when is_list(codes) and is_binary(code) do - hash_code = Enum.find(codes, fn hash -> Pbkdf2.verify_pass(code, hash) end) + hash_code = Enum.find(codes, fn hash -> AuthenticationPlug.checkpw(code, hash) end) if hash_code do MFA.invalidate_backup_code(user, hash_code) diff --git a/lib/pleroma/web/chat_channel.ex b/lib/pleroma/web/chat_channel.ex index 38ec774f7..bce27897f 100644 --- a/lib/pleroma/web/chat_channel.ex +++ b/lib/pleroma/web/chat_channel.ex @@ -23,6 +23,7 @@ defmodule Pleroma.Web.ChatChannel do if String.length(text) in 1..Pleroma.Config.get([:instance, :chat_limit]) do author = User.get_cached_by_nickname(user_name) author = Pleroma.Web.MastodonAPI.AccountView.render("show.json", user: author) + message = ChatChannelState.add_message(%{text: text, author: author}) broadcast!(socket, "new_msg", message) diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 601caeb46..447dbe4e6 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -25,10 +25,21 @@ defmodule Pleroma.Web.CommonAPI do require Logger def unblock(blocker, blocked) do - with %Activity{} = block <- Utils.fetch_latest_block(blocker, blocked), + with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)}, {:ok, unblock_data, _} <- Builder.undo(blocker, block), {:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do {:ok, unblock} + else + {:fetch_block, nil} -> + if User.blocks?(blocker, blocked) do + User.unblock(blocker, blocked) + {:ok, :no_activity} + else + {:error, :not_blocking} + end + + e -> + e end end @@ -336,11 +347,14 @@ defmodule Pleroma.Web.CommonAPI do |> check_expiry_date() end - def listen(user, %{"title" => _} = data) do - with visibility <- data["visibility"] || "public", - {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil), + def listen(user, data) do + visibility = Map.get(data, :visibility, "public") + + with {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil), listen_data <- - Map.take(data, ["album", "artist", "title", "length"]) + data + |> Map.take([:album, :artist, :title, :length]) + |> Map.new(fn {key, value} -> {to_string(key), value} end) |> Map.put("type", "Audio") |> Map.put("to", to) |> Map.put("cc", cc) diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index eb97ae975..5a1316a5f 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -5,8 +5,6 @@ defmodule Pleroma.Web.ControllerHelper do use Pleroma.Web, :controller - alias Pleroma.Config - # As in Mastodon API, per https://api.rubyonrails.org/classes/ActiveModel/Type/Boolean.html @falsy_param_values [false, 0, "0", "f", "F", "false", "False", "FALSE", "off", "OFF"] @@ -106,13 +104,16 @@ defmodule Pleroma.Web.ControllerHelper do def put_if_exist(map, key, value), do: Map.put(map, key, value) - @doc "Whether to skip rendering `[:account][:pleroma][:relationship]`for statuses/notifications" - def skip_relationships?(params) do - if Config.get([:extensions, :output_relationships_in_statuses_by_default]) do - false - else - # BREAKING: older PleromaFE versions do not send this param but _do_ expect relationships. - not truthy_param?(params["with_relationships"]) - end + @doc """ + Returns true if request specifies to include embedded relationships in account objects. + May only be used in selected account-related endpoints; has no effect for status- or + notification-related endpoints. + """ + # Intended for PleromaFE: https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838 + def embed_relationships?(params) do + # To do once OpenAPI transition mess is over: just `truthy_param?(params[:with_relationships])` + params + |> Map.get(:with_relationships, params["with_relationships"]) + |> truthy_param?() end end diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index b9ed2d7b2..75512442d 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -10,8 +10,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do add_link_headers: 2, truthy_param?: 1, assign_account_by_id: 2, - json_response: 3, - skip_relationships?: 1 + embed_relationships?: 1, + json_response: 3 ] alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug @@ -177,6 +177,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do ) |> add_if_present(params, :pleroma_settings_store, :pleroma_settings_store) |> add_if_present(params, :default_scope, :default_scope) + |> add_if_present(params["source"], "privacy", :default_scope) |> add_if_present(params, :actor_type, :actor_type) changeset = User.update_changeset(user, user_params) @@ -189,7 +190,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do end defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do - with true <- Map.has_key?(params, params_field), + with true <- is_map(params), + true <- Map.has_key?(params, params_field), {:ok, new_value} <- value_function.(Map.get(params, params_field)) do Map.put(map, map_field, new_value) else @@ -247,8 +249,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do |> render("index.json", activities: activities, for: reading_user, - as: :activity, - skip_relationships: skip_relationships?(params) + as: :activity ) else _e -> render_error(conn, :not_found, "Can't find user") @@ -271,7 +272,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do conn |> add_link_headers(followers) - |> render("index.json", for: for_user, users: followers, as: :user) + # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223 + |> render("index.json", + for: for_user, + users: followers, + as: :user, + embed_relationships: embed_relationships?(params) + ) end @doc "GET /api/v1/accounts/:id/following" @@ -290,7 +297,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do conn |> add_link_headers(followers) - |> render("index.json", for: for_user, users: followers, as: :user) + # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223 + |> render("index.json", + for: for_user, + users: followers, + as: :user, + embed_relationships: embed_relationships?(params) + ) end @doc "GET /api/v1/accounts/:id/lists" diff --git a/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex b/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex index 0a257f604..8af557b61 100644 --- a/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex @@ -20,6 +20,10 @@ defmodule Pleroma.Web.MastodonAPI.FallbackController do render_error(conn, :not_found, "Record not found") end + def call(conn, {:error, :forbidden}) do + render_error(conn, :forbidden, "Access denied") + end + def call(conn, {:error, error_message}) do conn |> put_status(:bad_request) diff --git a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex index e36751220..513de279f 100644 --- a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex @@ -11,17 +11,21 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do alias Pleroma.Web.ActivityPub.ActivityPub action_fallback(Pleroma.Web.MastodonAPI.FallbackController) + plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(:put_view, Pleroma.Web.MastodonAPI.StatusView) - plug(OAuthScopesPlug, %{scopes: ["write:media"]}) + plug(OAuthScopesPlug, %{scopes: ["read:media"]} when action == :show) + plug(OAuthScopesPlug, %{scopes: ["write:media"]} when action != :show) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.MediaOperation @doc "POST /api/v1/media" - def create(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do + def create(%{assigns: %{user: user}, body_params: %{file: file} = data} = conn, _) do with {:ok, object} <- ActivityPub.upload( file, actor: User.ap_id(user), - description: Map.get(data, "description") + description: Map.get(data, :description) ) do attachment_data = Map.put(object.data, "id", object.id) @@ -29,11 +33,30 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do end end + def create(_conn, _data), do: {:error, :bad_request} + + @doc "POST /api/v2/media" + def create2(%{assigns: %{user: user}, body_params: %{file: file} = data} = conn, _) do + with {:ok, object} <- + ActivityPub.upload( + file, + actor: User.ap_id(user), + description: Map.get(data, :description) + ) do + attachment_data = Map.put(object.data, "id", object.id) + + conn + |> put_status(202) + |> render("attachment.json", %{attachment: attachment_data}) + end + end + + def create2(_conn, _data), do: {:error, :bad_request} + @doc "PUT /api/v1/media/:id" - def update(%{assigns: %{user: user}} = conn, %{"id" => id, "description" => description}) - when is_binary(description) do + def update(%{assigns: %{user: user}, body_params: %{description: description}} = conn, %{id: id}) do with %Object{} = object <- Object.get_by_id(id), - true <- Object.authorize_mutation(object, user), + :ok <- Object.authorize_access(object, user), {:ok, %Object{data: data}} <- Object.update_data(object, %{"name" => description}) do attachment_data = Map.put(data, "id", object.id) @@ -41,5 +64,17 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do end end - def update(_conn, _data), do: {:error, :bad_request} + def update(conn, data), do: show(conn, data) + + @doc "GET /api/v1/media/:id" + def show(%{assigns: %{user: user}} = conn, %{id: id}) do + with %Object{data: data, id: object_id} = object <- Object.get_by_id(id), + :ok <- Object.authorize_access(object, user) do + attachment_data = Map.put(data, "id", object_id) + + render(conn, "attachment.json", %{attachment: attachment_data}) + end + end + + def show(_conn, _data), do: {:error, :bad_request} end diff --git a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex index 596b85617..bcd12c73f 100644 --- a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex @@ -5,7 +5,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do use Pleroma.Web, :controller - import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2, skip_relationships?: 1] + import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] alias Pleroma.Notification alias Pleroma.Plugs.OAuthScopesPlug @@ -50,8 +50,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do |> add_link_headers(notifications) |> render("index.json", notifications: notifications, - for: user, - skip_relationships: skip_relationships?(params) + for: user ) end diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex index 0e0d54ba4..77e2224e4 100644 --- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex @@ -5,14 +5,13 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do use Pleroma.Web, :controller - import Pleroma.Web.ControllerHelper, only: [skip_relationships?: 1] - alias Pleroma.Activity alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.RateLimiter alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web + alias Pleroma.Web.ControllerHelper alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.StatusView @@ -34,7 +33,11 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do conn |> put_view(AccountView) - |> render("index.json", users: accounts, for: user, as: :user) + |> render("index.json", + users: accounts, + for: user, + as: :user + ) end def search2(conn, params), do: do_search(:v2, conn, params) @@ -71,13 +74,13 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do defp search_options(params, user) do [ - skip_relationships: skip_relationships?(params), resolve: params[:resolve], following: params[:following], limit: params[:limit], offset: params[:offset], type: params[:type], author: get_author(params), + embed_relationships: ControllerHelper.embed_relationships?(params), for_user: user ] |> Enum.filter(&elem(&1, 1)) @@ -90,7 +93,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do users: accounts, for: options[:for_user], as: :user, - skip_relationships: false + embed_relationships: options[:embed_relationships] ) end @@ -100,8 +103,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do StatusView.render("index.json", activities: statuses, for: options[:for_user], - as: :activity, - skip_relationships: options[:skip_relationships] + as: :activity ) end diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index 25e499a77..9dbf4f33c 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do use Pleroma.Web, :controller import Pleroma.Web.ControllerHelper, - only: [try_render: 3, add_link_headers: 2, skip_relationships?: 1] + only: [try_render: 3, add_link_headers: 2] require Ecto.Query @@ -105,7 +105,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do `ids` query param is required """ - def index(%{assigns: %{user: user}} = conn, %{ids: ids} = params) do + def index(%{assigns: %{user: user}} = conn, %{ids: ids} = _params) do limit = 100 activities = @@ -117,8 +117,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do render(conn, "index.json", activities: activities, for: user, - as: :activity, - skip_relationships: skip_relationships?(params) + as: :activity ) end @@ -383,8 +382,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do |> render("index.json", activities: activities, for: user, - as: :activity, - skip_relationships: skip_relationships?(params) + as: :activity ) end @@ -406,8 +404,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do |> render("index.json", activities: activities, for: user, - as: :activity, - skip_relationships: skip_relationships?(params) + as: :activity ) end end diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index bbd576ffd..958567510 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do use Pleroma.Web, :controller import Pleroma.Web.ControllerHelper, - only: [add_link_headers: 2, add_link_headers: 3, skip_relationships?: 1] + only: [add_link_headers: 2, add_link_headers: 3] alias Pleroma.Pagination alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug @@ -63,8 +63,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do |> render("index.json", activities: activities, for: user, - as: :activity, - skip_relationships: skip_relationships?(params) + as: :activity ) end @@ -88,8 +87,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do |> render("index.json", activities: activities, for: user, - as: :activity, - skip_relationships: skip_relationships?(params) + as: :activity ) end @@ -125,8 +123,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do |> render("index.json", activities: activities, for: user, - as: :activity, - skip_relationships: skip_relationships?(params) + as: :activity ) end end @@ -173,8 +170,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do |> render("index.json", activities: activities, for: user, - as: :activity, - skip_relationships: skip_relationships?(params) + as: :activity ) end @@ -203,8 +199,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do render(conn, "index.json", activities: activities, for: user, - as: :activity, - skip_relationships: skip_relationships?(params) + as: :activity ) else _e -> render_error(conn, :forbidden, "Error.") diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 835dfe9f4..45fffaad2 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -15,13 +15,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do def render("index.json", %{users: users} = opts) do reading_user = opts[:for] - # Note: :skip_relationships option is currently intentionally not supported for accounts relationships_opt = cond do Map.has_key?(opts, :relationships) -> opts[:relationships] - is_nil(reading_user) -> + is_nil(reading_user) || !opts[:embed_relationships] -> UserRelationship.view_relationships_option(nil, []) true -> @@ -193,14 +192,14 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do end) relationship = - if opts[:skip_relationships] do - %{} - else + if opts[:embed_relationships] do render("relationship.json", %{ user: opts[:for], target: user, relationships: opts[:relationships] }) + else + %{} end %{ diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index a329ffc28..8088306c3 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -33,6 +33,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do avatar_upload_limit: Keyword.get(instance, :avatar_upload_limit), background_upload_limit: Keyword.get(instance, :background_upload_limit), banner_upload_limit: Keyword.get(instance, :banner_upload_limit), + background_image: Keyword.get(instance, :background_image), pleroma: %{ metadata: %{ features: features(), diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex index 4da1ab67f..c46ddcf55 100644 --- a/lib/pleroma/web/mastodon_api/views/notification_view.ex +++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex @@ -51,9 +51,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do |> Enum.filter(& &1) |> Kernel.++(move_activities_targets) - UserRelationship.view_relationships_option(reading_user, actors, - source_mutes_only: opts[:skip_relationships] - ) + UserRelationship.view_relationships_option(reading_user, actors, subset: :source_mutes) end opts = @@ -83,15 +81,13 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do mastodon_type = Activity.mastodon_notification_type(activity) - render_opts = %{ - relationships: opts[:relationships], - skip_relationships: opts[:skip_relationships] - } + # Note: :relationships contain user mutes (needed for :muted flag in :status) + status_render_opts = %{relationships: opts[:relationships]} with %{id: _} = account <- AccountView.render( "show.json", - Map.merge(render_opts, %{user: actor, for: reading_user}) + %{user: actor, for: reading_user} ) do response = %{ id: to_string(notification.id), @@ -105,21 +101,20 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do case mastodon_type do "mention" -> - put_status(response, activity, reading_user, render_opts) + put_status(response, activity, reading_user, status_render_opts) "favourite" -> - put_status(response, parent_activity_fn.(), reading_user, render_opts) + put_status(response, parent_activity_fn.(), reading_user, status_render_opts) "reblog" -> - put_status(response, parent_activity_fn.(), reading_user, render_opts) + put_status(response, parent_activity_fn.(), reading_user, status_render_opts) "move" -> - # Note: :skip_relationships option being applied to _account_ rendering (here) - put_target(response, activity, reading_user, render_opts) + put_target(response, activity, reading_user, %{}) "pleroma:emoji_reaction" -> response - |> put_status(parent_activity_fn.(), reading_user, render_opts) + |> put_status(parent_activity_fn.(), reading_user, status_render_opts) |> put_emoji(activity) type when type in ["follow", "follow_request"] -> diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 24167f66f..8e3715093 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -107,9 +107,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do |> Enum.map(&get_user(&1.data["actor"], false)) |> Enum.filter(& &1) - UserRelationship.view_relationships_option(reading_user, actors, - source_mutes_only: opts[:skip_relationships] - ) + UserRelationship.view_relationships_option(reading_user, actors, subset: :source_mutes) end opts = @@ -162,9 +160,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do account: AccountView.render("show.json", %{ user: user, - for: opts[:for], - relationships: opts[:relationships], - skip_relationships: opts[:skip_relationships] + for: opts[:for] }), in_reply_to_id: nil, in_reply_to_account_id: nil, @@ -330,9 +326,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do account: AccountView.render("show.json", %{ user: user, - for: opts[:for], - relationships: opts[:relationships], - skip_relationships: opts[:skip_relationships] + for: opts[:for] }), in_reply_to_id: reply_to && to_string(reply_to.id), in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id), @@ -442,27 +436,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do } end - def render("listen.json", %{activity: %Activity{data: %{"type" => "Listen"}} = activity} = opts) do - object = Object.normalize(activity) - - user = get_user(activity.data["actor"]) - created_at = Utils.to_masto_date(activity.data["published"]) - - %{ - id: activity.id, - account: AccountView.render("show.json", %{user: user, for: opts[:for]}), - created_at: created_at, - title: object.data["title"] |> HTML.strip_tags(), - artist: object.data["artist"] |> HTML.strip_tags(), - album: object.data["album"] |> HTML.strip_tags(), - length: object.data["length"] - } - end - - def render("listens.json", opts) do - safe_render_many(opts.activities, StatusView, "listen.json", opts) - end - def render("context.json", %{activity: activity, activities: activities, user: user}) do %{ancestors: ancestors, descendants: descendants} = activities diff --git a/lib/pleroma/web/media_proxy/invalidation.ex b/lib/pleroma/web/media_proxy/invalidation.ex new file mode 100644 index 000000000..c037ff13e --- /dev/null +++ b/lib/pleroma/web/media_proxy/invalidation.ex @@ -0,0 +1,26 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MediaProxy.Invalidation do + @moduledoc false + + @callback purge(list(String.t()), map()) :: {:ok, String.t()} | {:error, String.t()} + + alias Pleroma.Config + + @spec purge(list(String.t())) :: {:ok, String.t()} | {:error, String.t()} + def purge(urls) do + [:media_proxy, :invalidation, :enabled] + |> Config.get() + |> do_purge(urls) + end + + defp do_purge(true, urls) do + provider = Config.get([:media_proxy, :invalidation, :provider]) + options = Config.get(provider) + provider.purge(urls, options) + end + + defp do_purge(_, _), do: :ok +end diff --git a/lib/pleroma/web/media_proxy/invalidations/http.ex b/lib/pleroma/web/media_proxy/invalidations/http.ex new file mode 100644 index 000000000..07248df6e --- /dev/null +++ b/lib/pleroma/web/media_proxy/invalidations/http.ex @@ -0,0 +1,40 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MediaProxy.Invalidation.Http do + @moduledoc false + @behaviour Pleroma.Web.MediaProxy.Invalidation + + require Logger + + @impl Pleroma.Web.MediaProxy.Invalidation + def purge(urls, opts) do + method = Map.get(opts, :method, :purge) + headers = Map.get(opts, :headers, []) + options = Map.get(opts, :options, []) + + Logger.debug("Running cache purge: #{inspect(urls)}") + + Enum.each(urls, fn url -> + with {:error, error} <- do_purge(method, url, headers, options) do + Logger.error("Error while cache purge: url - #{url}, error: #{inspect(error)}") + end + end) + + {:ok, "success"} + end + + defp do_purge(method, url, headers, options) do + case Pleroma.HTTP.request(method, url, "", headers, options) do + {:ok, %{status: status} = env} when 400 <= status and status < 500 -> + {:error, env} + + {:error, error} = error -> + error + + _ -> + {:ok, "success"} + end + end +end diff --git a/lib/pleroma/web/media_proxy/invalidations/script.ex b/lib/pleroma/web/media_proxy/invalidations/script.ex new file mode 100644 index 000000000..6be782132 --- /dev/null +++ b/lib/pleroma/web/media_proxy/invalidations/script.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.MediaProxy.Invalidation.Script do + @moduledoc false + + @behaviour Pleroma.Web.MediaProxy.Invalidation + + require Logger + + @impl Pleroma.Web.MediaProxy.Invalidation + def purge(urls, %{script_path: script_path} = _options) do + args = + urls + |> List.wrap() + |> Enum.uniq() + |> Enum.join(" ") + + path = Path.expand(script_path) + + Logger.debug("Running cache purge: #{inspect(urls)}, #{path}") + + case do_purge(path, [args]) do + {result, exit_status} when exit_status > 0 -> + Logger.error("Error while cache purge: #{inspect(result)}") + {:error, inspect(result)} + + _ -> + {:ok, "success"} + end + end + + def purge(_, _), do: {:error, "not found script path"} + + defp do_purge(path, args) do + System.cmd(path, args) + rescue + error -> {inspect(error), 1} + end +end diff --git a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex b/lib/pleroma/web/mongooseim/mongoose_im_controller.ex index 0814b3bc3..6cbbe8fd8 100644 --- a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex +++ b/lib/pleroma/web/mongooseim/mongoose_im_controller.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Web.MongooseIM.MongooseIMController do use Pleroma.Web, :controller + alias Pleroma.Plugs.AuthenticationPlug alias Pleroma.Plugs.RateLimiter alias Pleroma.Repo alias Pleroma.User @@ -27,7 +28,7 @@ defmodule Pleroma.Web.MongooseIM.MongooseIMController do def check_password(conn, %{"user" => username, "pass" => password}) do with %User{password_hash: password_hash, deactivated: false} <- Repo.get_by(User, nickname: username, local: true), - true <- Pbkdf2.verify_pass(password, password_hash) do + true <- AuthenticationPlug.checkpw(password, password_hash) do conn |> json(true) else diff --git a/lib/pleroma/web/oauth/mfa_controller.ex b/lib/pleroma/web/oauth/mfa_controller.ex index e52cccd85..53e19f82e 100644 --- a/lib/pleroma/web/oauth/mfa_controller.ex +++ b/lib/pleroma/web/oauth/mfa_controller.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.OAuth.MFAController do diff --git a/lib/pleroma/web/oauth/mfa_view.ex b/lib/pleroma/web/oauth/mfa_view.ex index e88e7066b..41d5578dc 100644 --- a/lib/pleroma/web/oauth/mfa_view.ex +++ b/lib/pleroma/web/oauth/mfa_view.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.OAuth.MFAView do diff --git a/lib/pleroma/web/oauth/token/clean_worker.ex b/lib/pleroma/web/oauth/token/clean_worker.ex index 2c3bb9ded..e3aa4eb7e 100644 --- a/lib/pleroma/web/oauth/token/clean_worker.ex +++ b/lib/pleroma/web/oauth/token/clean_worker.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.OAuth.Token.CleanWorker do diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex index be7477867..0a3f45620 100644 --- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do use Pleroma.Web, :controller import Pleroma.Web.ControllerHelper, - only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2, skip_relationships?: 1] + only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2] alias Ecto.Changeset alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug @@ -19,6 +19,13 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do require Pleroma.Constants plug( + OpenApiSpex.Plug.PutApiSpec, + [module: Pleroma.Web.ApiSpec] when action == :confirmation_resend + ) + + plug(Pleroma.Web.ApiSpec.CastAndValidate) + + plug( :skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :confirmation_resend ) @@ -49,9 +56,11 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do plug(:assign_account_by_id when action in [:favourites, :subscribe, :unsubscribe]) plug(:put_view, Pleroma.Web.MastodonAPI.AccountView) + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaAccountOperation + @doc "POST /api/v1/pleroma/accounts/confirmation_resend" def confirmation_resend(conn, params) do - nickname_or_email = params["email"] || params["nickname"] + nickname_or_email = params[:email] || params[:nickname] with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email), {:ok, _} <- User.try_send_confirmation_email(user) do @@ -60,7 +69,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do end @doc "PATCH /api/v1/pleroma/accounts/update_avatar" - def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do + def update_avatar(%{assigns: %{user: user}, body_params: %{img: ""}} = conn, _) do {:ok, _user} = user |> Changeset.change(%{avatar: nil}) @@ -69,7 +78,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do json(conn, %{url: nil}) end - def update_avatar(%{assigns: %{user: user}} = conn, params) do + def update_avatar(%{assigns: %{user: user}, body_params: params} = conn, _params) do {:ok, %{data: data}} = ActivityPub.upload(params, type: :avatar) {:ok, _user} = user |> Changeset.change(%{avatar: data}) |> User.update_and_set_cache() %{"url" => [%{"href" => href} | _]} = data @@ -78,14 +87,14 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do end @doc "PATCH /api/v1/pleroma/accounts/update_banner" - def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do + def update_banner(%{assigns: %{user: user}, body_params: %{banner: ""}} = conn, _) do with {:ok, _user} <- User.update_banner(user, %{}) do json(conn, %{url: nil}) end end - def update_banner(%{assigns: %{user: user}} = conn, params) do - with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner), + def update_banner(%{assigns: %{user: user}, body_params: params} = conn, _) do + with {:ok, object} <- ActivityPub.upload(%{img: params[:banner]}, type: :banner), {:ok, _user} <- User.update_banner(user, object.data) do %{"url" => [%{"href" => href} | _]} = object.data @@ -94,13 +103,13 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do end @doc "PATCH /api/v1/pleroma/accounts/update_background" - def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do + def update_background(%{assigns: %{user: user}, body_params: %{img: ""}} = conn, _) do with {:ok, _user} <- User.update_background(user, %{}) do json(conn, %{url: nil}) end end - def update_background(%{assigns: %{user: user}} = conn, params) do + def update_background(%{assigns: %{user: user}, body_params: params} = conn, _) do with {:ok, object} <- ActivityPub.upload(params, type: :background), {:ok, _user} <- User.update_background(user, object.data) do %{"url" => [%{"href" => href} | _]} = object.data @@ -117,6 +126,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do def favourites(%{assigns: %{user: for_user, account: user}} = conn, params) do params = params + |> Map.new(fn {key, value} -> {to_string(key), value} end) |> Map.put("type", "Create") |> Map.put("favorited_by", user.ap_id) |> Map.put("blocking_user", for_user) @@ -139,8 +149,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do |> render("index.json", activities: activities, for: for_user, - as: :activity, - skip_relationships: skip_relationships?(params) + as: :activity ) end diff --git a/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex b/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex new file mode 100644 index 000000000..21d5eb8d5 --- /dev/null +++ b/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex @@ -0,0 +1,95 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.ConversationController do + use Pleroma.Web, :controller + + import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] + + alias Pleroma.Conversation.Participation + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.MastodonAPI.StatusView + + plug(Pleroma.Web.ApiSpec.CastAndValidate) + plug(:put_view, Pleroma.Web.MastodonAPI.ConversationView) + plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in [:show, :statuses]) + + plug( + OAuthScopesPlug, + %{scopes: ["write:conversations"]} when action in [:update, :mark_as_read] + ) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaConversationOperation + + def show(%{assigns: %{user: %{id: user_id} = user}} = conn, %{id: participation_id}) do + with %Participation{user_id: ^user_id} = participation <- Participation.get(participation_id) do + render(conn, "participation.json", participation: participation, for: user) + else + _error -> + conn + |> put_status(:not_found) + |> json(%{"error" => "Unknown conversation id"}) + end + end + + def statuses( + %{assigns: %{user: %{id: user_id} = user}} = conn, + %{id: participation_id} = params + ) do + with %Participation{user_id: ^user_id} = participation <- + Participation.get(participation_id, preload: [:conversation]) do + params = + params + |> Map.new(fn {key, value} -> {to_string(key), value} end) + |> Map.put("blocking_user", user) + |> Map.put("muting_user", user) + |> Map.put("user", user) + + activities = + participation.conversation.ap_id + |> ActivityPub.fetch_activities_for_context_query(params) + |> Pleroma.Pagination.fetch_paginated(Map.put(params, "total", false)) + |> Enum.reverse() + + conn + |> add_link_headers(activities) + |> put_view(StatusView) + |> render("index.json", activities: activities, for: user, as: :activity) + else + _error -> + conn + |> put_status(:not_found) + |> json(%{"error" => "Unknown conversation id"}) + end + end + + def update( + %{assigns: %{user: %{id: user_id} = user}} = conn, + %{id: participation_id, recipients: recipients} + ) do + with %Participation{user_id: ^user_id} = participation <- Participation.get(participation_id), + {:ok, participation} <- Participation.set_recipients(participation, recipients) do + render(conn, "participation.json", participation: participation, for: user) + else + {:error, message} -> + conn + |> put_status(:bad_request) + |> json(%{"error" => message}) + + _error -> + conn + |> put_status(:not_found) + |> json(%{"error" => "Unknown conversation id"}) + end + end + + def mark_as_read(%{assigns: %{user: user}} = conn, _params) do + with {:ok, _, participations} <- Participation.mark_all_as_read(user) do + conn + |> add_link_headers(participations) + |> render("participations.json", participations: participations, for: user) + end + end +end diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex index d276b96a4..2c53dcde1 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex @@ -1,8 +1,10 @@ -defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do +defmodule Pleroma.Web.PleromaAPI.EmojiPackController do use Pleroma.Web, :controller alias Pleroma.Emoji.Pack + plug(Pleroma.Web.ApiSpec.CastAndValidate) + plug( Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["write"], admin: true} @@ -19,39 +21,37 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do ] ) - plug( - :skip_plug, - [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug] - when action in [:archive, :show, :list] - ) + @skip_plugs [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug] + plug(:skip_plug, @skip_plugs when action in [:archive, :show, :list]) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaEmojiPackOperation - def remote(conn, %{"url" => url}) do + def remote(conn, %{url: url}) do with {:ok, packs} <- Pack.list_remote(url) do json(conn, packs) else - {:shareable, _} -> + {:error, :not_shareable} -> conn |> put_status(:internal_server_error) |> json(%{error: "The requested instance does not support sharing emoji packs"}) end end - def list(conn, _params) do + def index(conn, _params) do emoji_path = - Path.join( - Pleroma.Config.get!([:instance, :static_dir]), - "emoji" - ) + [:instance, :static_dir] + |> Pleroma.Config.get!() + |> Path.join("emoji") with {:ok, packs} <- Pack.list_local() do json(conn, packs) else - {:create_dir, {:error, e}} -> + {:error, :create_dir, e} -> conn |> put_status(:internal_server_error) |> json(%{error: "Failed to create the emoji pack directory at #{emoji_path}: #{e}"}) - {:ls, {:error, e}} -> + {:error, :ls, e} -> conn |> put_status(:internal_server_error) |> json(%{ @@ -60,13 +60,13 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do end end - def show(conn, %{"name" => name}) do + def show(conn, %{name: name}) do name = String.trim(name) with {:ok, pack} <- Pack.show(name) do json(conn, pack) else - {:loaded, _} -> + {:error, :not_found} -> conn |> put_status(:not_found) |> json(%{error: "Pack #{name} does not exist"}) @@ -78,11 +78,11 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do end end - def archive(conn, %{"name" => name}) do + def archive(conn, %{name: name}) do with {:ok, archive} <- Pack.get_archive(name) do send_download(conn, {:binary, archive}, filename: "#{name}.zip") else - {:can_download?, _} -> + {:error, :cant_download} -> conn |> put_status(:forbidden) |> json(%{ @@ -90,23 +90,23 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do "Pack #{name} cannot be downloaded from this instance, either pack sharing was disabled for this pack or some files are missing" }) - {:exists?, _} -> + {:error, :not_found} -> conn |> put_status(:not_found) |> json(%{error: "Pack #{name} does not exist"}) end end - def download(conn, %{"url" => url, "name" => name} = params) do - with :ok <- Pack.download(name, url, params["as"]) do + def download(%{body_params: %{url: url, name: name} = params} = conn, _) do + with {:ok, _pack} <- Pack.download(name, url, params[:as]) do json(conn, "ok") else - {:shareable, _} -> + {:error, :not_shareable} -> conn |> put_status(:internal_server_error) |> json(%{error: "The requested instance does not support sharing emoji packs"}) - {:checksum, _} -> + {:error, :imvalid_checksum} -> conn |> put_status(:internal_server_error) |> json(%{error: "SHA256 for the pack doesn't match the one sent by the server"}) @@ -118,10 +118,10 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do end end - def create(conn, %{"name" => name}) do + def create(conn, %{name: name}) do name = String.trim(name) - with :ok <- Pack.create(name) do + with {:ok, _pack} <- Pack.create(name) do json(conn, "ok") else {:error, :eexist} -> @@ -143,7 +143,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do end end - def delete(conn, %{"name" => name}) do + def delete(conn, %{name: name}) do name = String.trim(name) with {:ok, deleted} when deleted != [] <- Pack.delete(name) do @@ -166,11 +166,11 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do end end - def update(conn, %{"name" => name, "metadata" => metadata}) do + def update(%{body_params: %{metadata: metadata}} = conn, %{name: name}) do with {:ok, pack} <- Pack.update_metadata(name, metadata) do json(conn, pack.pack) else - {:has_all_files?, _} -> + {:error, :incomplete} -> conn |> put_status(:bad_request) |> json(%{error: "The fallback archive does not have all files specified in pack.json"}) @@ -184,19 +184,19 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do end end - def add_file(conn, %{"name" => name} = params) do - filename = params["filename"] || get_filename(params["file"]) - shortcode = params["shortcode"] || Path.basename(filename, Path.extname(filename)) + def add_file(%{body_params: params} = conn, %{name: name}) do + filename = params[:filename] || get_filename(params[:file]) + shortcode = params[:shortcode] || Path.basename(filename, Path.extname(filename)) - with {:ok, pack} <- Pack.add_file(name, shortcode, filename, params["file"]) do + with {:ok, pack} <- Pack.add_file(name, shortcode, filename, params[:file]) do json(conn, pack.files) else - {:exists, _} -> + {:error, :already_exists} -> conn |> put_status(:conflict) |> json(%{error: "An emoji with the \"#{shortcode}\" shortcode already exists"}) - {:loaded, _} -> + {:error, :not_found} -> conn |> put_status(:bad_request) |> json(%{error: "pack \"#{name}\" is not found"}) @@ -215,20 +215,20 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do end end - def update_file(conn, %{"name" => name, "shortcode" => shortcode} = params) do - new_shortcode = params["new_shortcode"] - new_filename = params["new_filename"] - force = params["force"] == true + def update_file(%{body_params: %{shortcode: shortcode} = params} = conn, %{name: name}) do + new_shortcode = params[:new_shortcode] + new_filename = params[:new_filename] + force = params[:force] with {:ok, pack} <- Pack.update_file(name, shortcode, new_shortcode, new_filename, force) do json(conn, pack.files) else - {:exists, _} -> + {:error, :doesnt_exist} -> conn |> put_status(:bad_request) |> json(%{error: "Emoji \"#{shortcode}\" does not exist"}) - {:not_used, _} -> + {:error, :already_exists} -> conn |> put_status(:conflict) |> json(%{ @@ -236,7 +236,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do "New shortcode \"#{new_shortcode}\" is already used. If you want to override emoji use 'force' option" }) - {:loaded, _} -> + {:error, :not_found} -> conn |> put_status(:bad_request) |> json(%{error: "pack \"#{name}\" is not found"}) @@ -255,16 +255,16 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do end end - def delete_file(conn, %{"name" => name, "shortcode" => shortcode}) do + def delete_file(conn, %{name: name, shortcode: shortcode}) do with {:ok, pack} <- Pack.delete_file(name, shortcode) do json(conn, pack.files) else - {:exists, _} -> + {:error, :doesnt_exist} -> conn |> put_status(:bad_request) |> json(%{error: "Emoji \"#{shortcode}\" does not exist"}) - {:loaded, _} -> + {:error, :not_found} -> conn |> put_status(:bad_request) |> json(%{error: "pack \"#{name}\" is not found"}) diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex new file mode 100644 index 000000000..a002912f3 --- /dev/null +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex @@ -0,0 +1,61 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do + use Pleroma.Web, :controller + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.MastodonAPI.StatusView + + plug(Pleroma.Web.ApiSpec.CastAndValidate) + plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action in [:create, :delete]) + + plug( + OAuthScopesPlug, + %{scopes: ["read:statuses"], fallback: :proceed_unauthenticated} + when action == :index + ) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.EmojiReactionOperation + + def index(%{assigns: %{user: user}} = conn, %{id: activity_id} = params) do + with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id), + %Object{data: %{"reactions" => reactions}} when is_list(reactions) <- + Object.normalize(activity) do + reactions = filter(reactions, params) + render(conn, "index.json", emoji_reactions: reactions, user: user) + else + _e -> json(conn, []) + end + end + + defp filter(reactions, %{emoji: emoji}) when is_binary(emoji) do + Enum.filter(reactions, fn [e, _] -> e == emoji end) + end + + defp filter(reactions, _), do: reactions + + def create(%{assigns: %{user: user}} = conn, %{id: activity_id, emoji: emoji}) do + with {:ok, _activity} <- CommonAPI.react_with_emoji(activity_id, user, emoji) do + activity = Activity.get_by_id(activity_id) + + conn + |> put_view(StatusView) + |> render("show.json", activity: activity, for: user, as: :activity) + end + end + + def delete(%{assigns: %{user: user}} = conn, %{id: activity_id, emoji: emoji}) do + with {:ok, _activity} <- CommonAPI.unreact_with_emoji(activity_id, user, emoji) do + activity = Activity.get_by_id(activity_id) + + conn + |> put_view(StatusView) + |> render("show.json", activity: activity, for: user, as: :activity) + end + end +end diff --git a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex index d4e0d8b7c..df6c50ca5 100644 --- a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex @@ -9,16 +9,19 @@ defmodule Pleroma.Web.PleromaAPI.MascotController do alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub + plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(OAuthScopesPlug, %{scopes: ["read:accounts"]} when action == :show) plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action != :show) + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaMascotOperation + @doc "GET /api/v1/pleroma/mascot" def show(%{assigns: %{user: user}} = conn, _params) do json(conn, User.get_mascot(user)) end @doc "PUT /api/v1/pleroma/mascot" - def update(%{assigns: %{user: user}} = conn, %{"file" => file}) do + def update(%{assigns: %{user: user}, body_params: %{file: file}} = conn, _) do with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)), # Reject if not an image %{type: "image"} = attachment <- render_attachment(object) do diff --git a/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex b/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex new file mode 100644 index 000000000..0b2f678c5 --- /dev/null +++ b/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex @@ -0,0 +1,36 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.NotificationController do + use Pleroma.Web, :controller + + alias Pleroma.Notification + alias Pleroma.Plugs.OAuthScopesPlug + + plug(Pleroma.Web.ApiSpec.CastAndValidate) + plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :mark_as_read) + plug(:put_view, Pleroma.Web.MastodonAPI.NotificationView) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaNotificationOperation + + def mark_as_read(%{assigns: %{user: user}} = conn, %{id: notification_id}) do + with {:ok, notification} <- Notification.read_one(user, notification_id) do + render(conn, "show.json", notification: notification, for: user) + else + {:error, message} -> + conn + |> put_status(:bad_request) + |> json(%{"error" => message}) + end + end + + def mark_as_read(%{assigns: %{user: user}} = conn, %{max_id: max_id}) do + notifications = + user + |> Notification.set_read_up_to(max_id) + |> Enum.take(80) + + render(conn, "index.json", notifications: notifications, for: user) + end +end diff --git a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex deleted file mode 100644 index 8bc77b75e..000000000 --- a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex +++ /dev/null @@ -1,217 +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.PleromaAPI.PleromaAPIController do - use Pleroma.Web, :controller - - import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2, skip_relationships?: 1] - - alias Pleroma.Activity - alias Pleroma.Conversation.Participation - alias Pleroma.Notification - alias Pleroma.Object - alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.MastodonAPI.AccountView - alias Pleroma.Web.MastodonAPI.ConversationView - alias Pleroma.Web.MastodonAPI.NotificationView - alias Pleroma.Web.MastodonAPI.StatusView - - plug( - OAuthScopesPlug, - %{scopes: ["read:statuses"]} - when action in [:conversation, :conversation_statuses] - ) - - plug( - OAuthScopesPlug, - %{scopes: ["read:statuses"], fallback: :proceed_unauthenticated} - when action == :emoji_reactions_by - ) - - plug( - OAuthScopesPlug, - %{scopes: ["write:statuses"]} - when action in [:react_with_emoji, :unreact_with_emoji] - ) - - plug( - OAuthScopesPlug, - %{scopes: ["write:conversations"]} - when action in [:update_conversation, :mark_conversations_as_read] - ) - - plug( - OAuthScopesPlug, - %{scopes: ["write:notifications"]} when action == :mark_notifications_as_read - ) - - def emoji_reactions_by(%{assigns: %{user: user}} = conn, %{"id" => activity_id} = params) do - with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id), - %Object{data: %{"reactions" => emoji_reactions}} when is_list(emoji_reactions) <- - Object.normalize(activity) do - reactions = - emoji_reactions - |> Enum.map(fn [emoji, user_ap_ids] -> - if params["emoji"] && params["emoji"] != emoji do - nil - else - users = - Enum.map(user_ap_ids, &User.get_cached_by_ap_id/1) - |> Enum.filter(fn - %{deactivated: false} -> true - _ -> false - end) - - %{ - name: emoji, - count: length(users), - accounts: AccountView.render("index.json", %{users: users, for: user, as: :user}), - me: !!(user && user.ap_id in user_ap_ids) - } - end - end) - |> Enum.filter(& &1) - - conn - |> json(reactions) - else - _e -> - conn - |> json([]) - end - end - - def react_with_emoji(%{assigns: %{user: user}} = conn, %{"id" => activity_id, "emoji" => emoji}) do - with {:ok, _activity} <- CommonAPI.react_with_emoji(activity_id, user, emoji), - activity <- Activity.get_by_id(activity_id) do - conn - |> put_view(StatusView) - |> render("show.json", %{activity: activity, for: user, as: :activity}) - end - end - - def unreact_with_emoji(%{assigns: %{user: user}} = conn, %{ - "id" => activity_id, - "emoji" => emoji - }) do - with {:ok, _activity} <- - CommonAPI.unreact_with_emoji(activity_id, user, emoji), - activity <- Activity.get_by_id(activity_id) do - conn - |> put_view(StatusView) - |> render("show.json", %{activity: activity, for: user, as: :activity}) - end - end - - def conversation(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do - with %Participation{} = participation <- Participation.get(participation_id), - true <- user.id == participation.user_id do - conn - |> put_view(ConversationView) - |> render("participation.json", %{participation: participation, for: user}) - else - _error -> - conn - |> put_status(404) - |> json(%{"error" => "Unknown conversation id"}) - end - end - - def conversation_statuses( - %{assigns: %{user: %{id: user_id} = user}} = conn, - %{"id" => participation_id} = params - ) do - with %Participation{user_id: ^user_id} = participation <- - Participation.get(participation_id, preload: [:conversation]) do - params = - params - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("user", user) - - activities = - participation.conversation.ap_id - |> ActivityPub.fetch_activities_for_context_query(params) - |> Pleroma.Pagination.fetch_paginated(Map.put(params, "total", false)) - |> Enum.reverse() - - conn - |> add_link_headers(activities) - |> put_view(StatusView) - |> render("index.json", - activities: activities, - for: user, - as: :activity, - skip_relationships: skip_relationships?(params) - ) - else - _error -> - conn - |> put_status(404) - |> json(%{"error" => "Unknown conversation id"}) - end - end - - def update_conversation( - %{assigns: %{user: user}} = conn, - %{"id" => participation_id, "recipients" => recipients} - ) do - with %Participation{} = participation <- Participation.get(participation_id), - true <- user.id == participation.user_id, - {:ok, participation} <- Participation.set_recipients(participation, recipients) do - conn - |> put_view(ConversationView) - |> render("participation.json", %{participation: participation, for: user}) - else - {:error, message} -> - conn - |> put_status(:bad_request) - |> json(%{"error" => message}) - - _error -> - conn - |> put_status(404) - |> json(%{"error" => "Unknown conversation id"}) - end - end - - def mark_conversations_as_read(%{assigns: %{user: user}} = conn, _params) do - with {:ok, _, participations} <- Participation.mark_all_as_read(user) do - conn - |> add_link_headers(participations) - |> put_view(ConversationView) - |> render("participations.json", participations: participations, for: user) - end - end - - def mark_notifications_as_read(%{assigns: %{user: user}} = conn, %{"id" => notification_id}) do - with {:ok, notification} <- Notification.read_one(user, notification_id) do - conn - |> put_view(NotificationView) - |> render("show.json", %{notification: notification, for: user}) - else - {:error, message} -> - conn - |> put_status(:bad_request) - |> json(%{"error" => message}) - end - end - - def mark_notifications_as_read(%{assigns: %{user: user}} = conn, %{"max_id" => max_id} = params) do - with notifications <- Notification.set_read_up_to(user, max_id) do - notifications = Enum.take(notifications, 80) - - conn - |> put_view(NotificationView) - |> render("index.json", - notifications: notifications, - for: user, - skip_relationships: skip_relationships?(params) - ) - end - end -end diff --git a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex index 22da6c0ad..8665ca56c 100644 --- a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex @@ -5,34 +5,27 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleController do use Pleroma.Web, :controller - import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2, fetch_integer_param: 2] + import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.CommonAPI - alias Pleroma.Web.MastodonAPI.StatusView + + plug(Pleroma.Web.ApiSpec.CastAndValidate) plug( OAuthScopesPlug, - %{scopes: ["read"], fallback: :proceed_unauthenticated} when action == :user_scrobbles + %{scopes: ["read"], fallback: :proceed_unauthenticated} when action == :index ) - plug(OAuthScopesPlug, %{scopes: ["write"]} when action != :user_scrobbles) + plug(OAuthScopesPlug, %{scopes: ["write"]} when action == :create) - def new_scrobble(%{assigns: %{user: user}} = conn, %{"title" => _} = params) do - params = - if !params["length"] do - params - else - params - |> Map.put("length", fetch_integer_param(params, "length")) - end + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaScrobbleOperation + def create(%{assigns: %{user: user}, body_params: params} = conn, _) do with {:ok, activity} <- CommonAPI.listen(user, params) do - conn - |> put_view(StatusView) - |> render("listen.json", %{activity: activity, for: user}) + render(conn, "show.json", activity: activity, for: user) else {:error, message} -> conn @@ -41,16 +34,18 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleController do end end - def user_scrobbles(%{assigns: %{user: reading_user}} = conn, params) do - with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do - params = Map.put(params, "type", ["Listen"]) + def index(%{assigns: %{user: reading_user}} = conn, %{id: id} = params) do + with %User{} = user <- User.get_cached_by_nickname_or_id(id, for: reading_user) do + params = + params + |> Map.new(fn {key, value} -> {to_string(key), value} end) + |> Map.put("type", ["Listen"]) activities = ActivityPub.fetch_user_abstract_activities(user, reading_user, params) conn |> add_link_headers(activities) - |> put_view(StatusView) - |> render("listens.json", %{ + |> render("index.json", %{ activities: activities, for: reading_user, as: :activity diff --git a/lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex b/lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex index eb9989cdf..b86791d09 100644 --- a/lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.PleromaAPI.TwoFactorAuthenticationController do diff --git a/lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex b/lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex new file mode 100644 index 000000000..84d2d303d --- /dev/null +++ b/lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex @@ -0,0 +1,33 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.EmojiReactionView do + use Pleroma.Web, :view + + alias Pleroma.Web.MastodonAPI.AccountView + + def render("index.json", %{emoji_reactions: emoji_reactions} = opts) do + render_many(emoji_reactions, __MODULE__, "show.json", opts) + end + + def render("show.json", %{emoji_reaction: [emoji, user_ap_ids], user: user}) do + users = fetch_users(user_ap_ids) + + %{ + name: emoji, + count: length(users), + accounts: render(AccountView, "index.json", users: users, for: user, as: :user), + me: !!(user && user.ap_id in user_ap_ids) + } + end + + defp fetch_users(user_ap_ids) do + user_ap_ids + |> Enum.map(&Pleroma.User.get_cached_by_ap_id/1) + |> Enum.filter(fn + %{deactivated: false} -> true + _ -> false + end) + end +end diff --git a/lib/pleroma/web/pleroma_api/views/scrobble_view.ex b/lib/pleroma/web/pleroma_api/views/scrobble_view.ex new file mode 100644 index 000000000..bbff93abe --- /dev/null +++ b/lib/pleroma/web/pleroma_api/views/scrobble_view.ex @@ -0,0 +1,37 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.ScrobbleView do + use Pleroma.Web, :view + + require Pleroma.Constants + + alias Pleroma.Activity + alias Pleroma.HTML + alias Pleroma.Object + 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"]) + created_at = Utils.to_masto_date(activity.data["published"]) + + %{ + id: activity.id, + account: AccountView.render("show.json", %{user: user, for: opts[:for]}), + created_at: created_at, + title: object.data["title"] |> HTML.strip_tags(), + artist: object.data["artist"] |> HTML.strip_tags(), + album: object.data["album"] |> HTML.strip_tags(), + length: object.data["length"] + } + end + + def render("index.json", opts) do + safe_render_many(opts.activities, __MODULE__, "show.json", opts) + end +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 7a171f9fb..cbe320746 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -216,24 +216,25 @@ defmodule Pleroma.Web.Router do scope "/packs" do pipe_through(:admin_api) - get("/import", EmojiAPIController, :import_from_filesystem) - get("/remote", EmojiAPIController, :remote) - post("/download", EmojiAPIController, :download) + get("/import", EmojiPackController, :import_from_filesystem) + get("/remote", EmojiPackController, :remote) + post("/download", EmojiPackController, :download) - post("/:name", EmojiAPIController, :create) - patch("/:name", EmojiAPIController, :update) - delete("/:name", EmojiAPIController, :delete) + post("/:name", EmojiPackController, :create) + patch("/:name", EmojiPackController, :update) + delete("/:name", EmojiPackController, :delete) - post("/:name/files", EmojiAPIController, :add_file) - patch("/:name/files", EmojiAPIController, :update_file) - delete("/:name/files", EmojiAPIController, :delete_file) + post("/:name/files", EmojiPackController, :add_file) + patch("/:name/files", EmojiPackController, :update_file) + delete("/:name/files", EmojiPackController, :delete_file) end # Pack info / downloading scope "/packs" do - get("/", EmojiAPIController, :list) - get("/:name", EmojiAPIController, :show) - get("/:name/archive", EmojiAPIController, :archive) + pipe_through(:api) + get("/", EmojiPackController, :index) + get("/:name", EmojiPackController, :show) + get("/:name/archive", EmojiPackController, :archive) end end @@ -297,26 +298,22 @@ defmodule Pleroma.Web.Router do scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do pipe_through(:api) - get("/statuses/:id/reactions/:emoji", PleromaAPIController, :emoji_reactions_by) - get("/statuses/:id/reactions", PleromaAPIController, :emoji_reactions_by) + get("/statuses/:id/reactions/:emoji", EmojiReactionController, :index) + get("/statuses/:id/reactions", EmojiReactionController, :index) end scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do scope [] do pipe_through(:authenticated_api) - get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses) - get("/conversations/:id", PleromaAPIController, :conversation) - post("/conversations/read", PleromaAPIController, :mark_conversations_as_read) - end - - scope [] do - pipe_through(:authenticated_api) + get("/conversations/:id/statuses", ConversationController, :statuses) + get("/conversations/:id", ConversationController, :show) + post("/conversations/read", ConversationController, :mark_as_read) + patch("/conversations/:id", ConversationController, :update) - patch("/conversations/:id", PleromaAPIController, :update_conversation) - put("/statuses/:id/reactions/:emoji", PleromaAPIController, :react_with_emoji) - delete("/statuses/:id/reactions/:emoji", PleromaAPIController, :unreact_with_emoji) - post("/notifications/read", PleromaAPIController, :mark_notifications_as_read) + put("/statuses/:id/reactions/:emoji", EmojiReactionController, :create) + delete("/statuses/:id/reactions/:emoji", EmojiReactionController, :delete) + post("/notifications/read", NotificationController, :mark_as_read) patch("/accounts/update_avatar", AccountController, :update_avatar) patch("/accounts/update_banner", AccountController, :update_banner) @@ -325,7 +322,7 @@ defmodule Pleroma.Web.Router do get("/mascot", MascotController, :show) put("/mascot", MascotController, :update) - post("/scrobble", ScrobbleController, :new_scrobble) + post("/scrobble", ScrobbleController, :create) end scope [] do @@ -345,7 +342,7 @@ defmodule Pleroma.Web.Router do scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do pipe_through(:api) - get("/accounts/:id/scrobbles", ScrobbleController, :user_scrobbles) + get("/accounts/:id/scrobbles", ScrobbleController, :index) end scope "/api/v1", Pleroma.Web.MastodonAPI do @@ -403,6 +400,7 @@ defmodule Pleroma.Web.Router do post("/markers", MarkerController, :upsert) post("/media", MediaController, :create) + get("/media/:id", MediaController, :show) put("/media/:id", MediaController, :update) get("/notifications", NotificationController, :index) @@ -497,6 +495,8 @@ defmodule Pleroma.Web.Router do scope "/api/v2", Pleroma.Web.MastodonAPI do pipe_through(:api) get("/search", SearchController, :search2) + + post("/media", MediaController, :create2) end scope "/api", Pleroma.Web do diff --git a/lib/pleroma/workers/attachments_cleanup_worker.ex b/lib/pleroma/workers/attachments_cleanup_worker.ex index 3c5820a86..49352db2a 100644 --- a/lib/pleroma/workers/attachments_cleanup_worker.ex +++ b/lib/pleroma/workers/attachments_cleanup_worker.ex @@ -27,8 +27,20 @@ defmodule Pleroma.Workers.AttachmentsCleanupWorker do uploader = Pleroma.Config.get([Pleroma.Upload, :uploader]) + prefix = + case Pleroma.Config.get([Pleroma.Upload, :base_url]) do + nil -> "media" + _ -> "" + end + + base_url = + String.trim_trailing( + Pleroma.Config.get([Pleroma.Upload, :base_url], Pleroma.Web.base_url()), + "/" + ) + # find all objects for copies of the attachments, name and actor doesn't matter here - delete_ids = + object_ids_and_hrefs = from(o in Object, where: fragment( @@ -67,29 +79,28 @@ defmodule Pleroma.Workers.AttachmentsCleanupWorker do |> Enum.map(fn {href, %{id: id, count: count}} -> # only delete files that have single instance with 1 <- count do - prefix = - case Pleroma.Config.get([Pleroma.Upload, :base_url]) do - nil -> "media" - _ -> "" - end - - base_url = - String.trim_trailing( - Pleroma.Config.get([Pleroma.Upload, :base_url], Pleroma.Web.base_url()), - "/" - ) - - file_path = String.trim_leading(href, "#{base_url}/#{prefix}") + href + |> String.trim_leading("#{base_url}/#{prefix}") + |> uploader.delete_file() - uploader.delete_file(file_path) + {id, href} + else + _ -> {id, nil} end - - id end) - from(o in Object, where: o.id in ^delete_ids) + object_ids = Enum.map(object_ids_and_hrefs, fn {id, _} -> id end) + + from(o in Object, where: o.id in ^object_ids) |> Repo.delete_all() + + object_ids_and_hrefs + |> Enum.filter(fn {_, href} -> not is_nil(href) end) + |> Enum.map(&elem(&1, 1)) + |> Pleroma.Web.MediaProxy.Invalidation.purge() + + {:ok, :success} end - def perform(%{"op" => "cleanup_attachments", "object" => _object}, _job), do: :ok + def perform(%{"op" => "cleanup_attachments", "object" => _object}, _job), do: {:ok, :skip} end |