Merge branch 'develop' of https://git.pleroma.social/pleroma/pleroma into develop
commit
e18958a5d7
@ -0,0 +1,507 @@
|
||||
defmodule Pleroma.Emoji.Pack do
|
||||
@derive {Jason.Encoder, only: [:files, :pack]}
|
||||
defstruct files: %{},
|
||||
pack_file: nil,
|
||||
path: nil,
|
||||
pack: %{},
|
||||
name: nil
|
||||
|
||||
@type t() :: %__MODULE__{
|
||||
files: %{String.t() => Path.t()},
|
||||
pack_file: Path.t(),
|
||||
path: Path.t(),
|
||||
pack: map(),
|
||||
name: String.t()
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|> 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}
|
||||
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
|
||||
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!()
|
||||
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
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
def update_file(_, _, _, _, _), do: {:error, :empty_values}
|
||||
|
||||
@spec import_from_filesystem() :: {:ok, [String.t()]} | {:error, atom()}
|
||||
def import_from_filesystem do
|
||||
emoji_path = emoji_path()
|
||||
|
||||
with {:ok, %{access: :read_write}} <- File.stat(emoji_path),
|
||||
{:ok, results} <- File.ls(emoji_path) do
|
||||
names =
|
||||
results
|
||||
|> Enum.map(&Path.join(emoji_path, &1))
|
||||
|> Enum.reject(fn path ->
|
||||
File.dir?(path) and File.exists?(Path.join(path, "pack.json"))
|
||||
end)
|
||||
|> Enum.map(&write_pack_contents/1)
|
||||
|> Enum.filter(& &1)
|
||||
|
||||
{:ok, names}
|
||||
else
|
||||
{:ok, %{access: _}} -> {:error, :no_read_write}
|
||||
e -> e
|
||||
end
|
||||
end
|
||||
|
||||
defp write_pack_contents(path) do
|
||||
pack = %__MODULE__{
|
||||
files: files_from_path(path),
|
||||
path: path,
|
||||
pack_file: Path.join(path, "pack.json")
|
||||
}
|
||||
|
||||
case save_pack(pack) do
|
||||
:ok -> Path.basename(path)
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp files_from_path(path) do
|
||||
txt_path = Path.join(path, "emoji.txt")
|
||||
|
||||
if File.exists?(txt_path) do
|
||||
# There's an emoji.txt file, it's likely from a pack installed by the pack manager.
|
||||
# Make a pack.json file from the contents of that emoji.txt file
|
||||
|
||||
# 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)
|
||||
|> String.split("\n")
|
||||
|> Enum.map(&String.trim/1)
|
||||
|> Enum.map(fn line ->
|
||||
case String.split(line, ~r/,\s*/) do
|
||||
# This matches both strings with and without tags
|
||||
# and we don't care about tags here
|
||||
[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}
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end)
|
||||
|> Enum.filter(& &1)
|
||||
|> Enum.into(%{})
|
||||
else
|
||||
# If there's no emoji.txt, assume all files
|
||||
# that are of certain extensions from the config are emojis and import them all
|
||||
pack_extensions = Pleroma.Config.get!([:emoji, :pack_extensions])
|
||||
Emoji.Loader.make_shortcode_to_file_map(path, pack_extensions)
|
||||
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 =
|
||||
pack.pack
|
||||
|> Map.put("can-download", true)
|
||||
|> Map.put("download-sha256", archive_sha)
|
||||
|
||||
{pack.name, Map.put(pack, :pack, info)}
|
||||
else
|
||||
info = Map.put(pack.pack, "can-download", false)
|
||||
{pack.name, Map.put(pack, :pack, info)}
|
||||
end
|
||||
end
|
||||
|
||||
defp downloadable?(pack) do
|
||||
# If the pack is set as shared, check if it can be downloaded
|
||||
# That means that when asked, the pack can be packed and sent to the remote
|
||||
# Otherwise, they'd have to download it from external-src
|
||||
pack.pack["share-files"] &&
|
||||
Enum.all?(pack.files, fn {_, file} ->
|
||||
File.exists?(Path.join(pack.path, file))
|
||||
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)]
|
||||
|
||||
{:ok, {_, result}} =
|
||||
:zip.zip('#{pack.name}.zip', files, [:memory, cwd: to_charlist(pack.path)])
|
||||
|
||||
ttl_per_file = Pleroma.Config.get!([:emoji, :shared_pack_cache_seconds_per_file])
|
||||
overall_ttl = :timer.seconds(ttl_per_file * Enum.count(files))
|
||||
|
||||
Cachex.put!(
|
||||
:emoji_packs_cache,
|
||||
pack.name,
|
||||
# if pack.json MD5 changes, the cache is not valid anymore
|
||||
%{hash: hash, pack_data: result},
|
||||
# Add a minute to cache time for every file in the pack
|
||||
ttl: overall_ttl
|
||||
)
|
||||
|
||||
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
|
||||
}}
|
||||
|
||||
_ ->
|
||||
{:error,
|
||||
"The pack was not set as shared and there is no fallback src to download from"}
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
path = Path.join(emoji_path(), local_name)
|
||||
|
||||
pack = %__MODULE__{
|
||||
name: local_name,
|
||||
path: path,
|
||||
files: remote_pack["files"],
|
||||
pack_file: Path.join(path, "pack.json")
|
||||
}
|
||||
|
||||
File.mkdir_p!(pack.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]
|
||||
|
||||
{:ok, _} = :zip.unzip(archive, cwd: to_charlist(pack.path), file_list: files)
|
||||
|
||||
# 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
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp save_pack(pack), do: File.write(pack.pack_file, Jason.encode!(pack, pretty: true))
|
||||
|
||||
@spec save_metadata(map(), t()) :: {:ok, t()} | {:error, File.posix()}
|
||||
def save_metadata(metadata, %__MODULE__{} = pack) do
|
||||
pack = Map.put(pack, :pack, metadata)
|
||||
|
||||
with :ok <- save_pack(pack) do
|
||||
{:ok, pack}
|
||||
end
|
||||
end
|
||||
|
||||
@spec update_metadata(String.t(), map()) :: {:ok, t()} | {:error, File.posix()}
|
||||
def update_metadata(name, data) do
|
||||
pack = load_pack(name)
|
||||
|
||||
fb_sha_changed? =
|
||||
not is_nil(data["fallback-src"]) and data["fallback-src"] != pack.pack["fallback-src"]
|
||||
|
||||
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()
|
||||
|
||||
data
|
||||
|> Map.put("fallback-src-sha256", fallback_sha)
|
||||
|> save_metadata(pack)
|
||||
else
|
||||
{:update?, _} -> save_metadata(data, pack)
|
||||
e -> 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)
|
||||
end
|
||||
|
||||
@spec load_pack(String.t()) :: t() | nil
|
||||
def load_pack(name) do
|
||||
pack_file = Path.join([emoji_path(), name, "pack.json"])
|
||||
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
||||
defp from_json(json) do
|
||||
map = Jason.decode!(json)
|
||||
|
||||
struct(__MODULE__, %{files: map["files"], pack: map["pack"]})
|
||||
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")
|
||||
end
|
||||
end
|
@ -0,0 +1,156 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.MFA do
|
||||
@moduledoc """
|
||||
The MFA context.
|
||||
"""
|
||||
|
||||
alias Comeonin.Pbkdf2
|
||||
alias Pleroma.User
|
||||
|
||||
alias Pleroma.MFA.BackupCodes
|
||||
alias Pleroma.MFA.Changeset
|
||||
alias Pleroma.MFA.Settings
|
||||
alias Pleroma.MFA.TOTP
|
||||
|
||||
@doc """
|
||||
Returns MFA methods the user has enabled.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Pleroma.MFA.supported_method(User)
|
||||
"totp, u2f"
|
||||
"""
|
||||
@spec supported_methods(User.t()) :: String.t()
|
||||
def supported_methods(user) do
|
||||
settings = fetch_settings(user)
|
||||
|
||||
Settings.mfa_methods()
|
||||
|> Enum.reduce([], fn m, acc ->
|
||||
if method_enabled?(m, settings) do
|
||||
acc ++ [m]
|
||||
else
|
||||
acc
|
||||
end
|
||||
end)
|
||||
|> Enum.join(",")
|
||||
end
|
||||
|
||||
@doc "Checks that user enabled MFA"
|
||||
def require?(user) do
|
||||
fetch_settings(user).enabled
|
||||
end
|
||||
|
||||
@doc """
|
||||
Display MFA settings of user
|
||||
"""
|
||||
def mfa_settings(user) do
|
||||
settings = fetch_settings(user)
|
||||
|
||||
Settings.mfa_methods()
|
||||
|> Enum.map(fn m -> [m, method_enabled?(m, settings)] end)
|
||||
|> Enum.into(%{enabled: settings.enabled}, fn [a, b] -> {a, b} end)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def fetch_settings(%User{} = user) do
|
||||
user.multi_factor_authentication_settings || %Settings{}
|
||||
end
|
||||
|
||||
@doc "clears backup codes"
|
||||
def invalidate_backup_code(%User{} = user, hash_code) do
|
||||
%{backup_codes: codes} = fetch_settings(user)
|
||||
|
||||
user
|
||||
|> Changeset.cast_backup_codes(codes -- [hash_code])
|
||||
|> User.update_and_set_cache()
|
||||
end
|
||||
|
||||
@doc "generates backup codes"
|
||||
@spec generate_backup_codes(User.t()) :: {:ok, list(binary)} | {:error, String.t()}
|
||||
def generate_backup_codes(%User{} = user) do
|
||||
with codes <- BackupCodes.generate(),
|
||||
hashed_codes <- Enum.map(codes, &Pbkdf2.hashpwsalt/1),
|
||||
changeset <- Changeset.cast_backup_codes(user, hashed_codes),
|
||||
{:ok, _} <- User.update_and_set_cache(changeset) do
|
||||
{:ok, codes}
|
||||
else
|
||||
{:error, msg} ->
|
||||
%{error: msg}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates secret key and set delivery_type to 'app' for TOTP method.
|
||||
"""
|
||||
@spec setup_totp(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
|
||||
def setup_totp(user) do
|
||||
user
|
||||
|> Changeset.setup_totp(%{secret: TOTP.generate_secret(), delivery_type: "app"})
|
||||
|> User.update_and_set_cache()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Confirms the TOTP method for user.
|
||||
|
||||
`attrs`:
|
||||
`password` - current user password
|
||||
`code` - TOTP token
|
||||
"""
|
||||
@spec confirm_totp(User.t(), map()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t() | atom()}
|
||||
def confirm_totp(%User{} = user, attrs) do
|
||||
with settings <- user.multi_factor_authentication_settings.totp,
|
||||
{:ok, :pass} <- TOTP.validate_token(settings.secret, attrs["code"]) do
|
||||
user
|
||||
|> Changeset.confirm_totp()
|
||||
|> User.update_and_set_cache()
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Disables the TOTP method for user.
|
||||
|
||||
`attrs`:
|
||||
`password` - current user password
|
||||
"""
|
||||
@spec disable_totp(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
|
||||
def disable_totp(%User{} = user) do
|
||||
user
|
||||
|> Changeset.disable_totp()
|
||||
|> Changeset.disable()
|
||||
|> User.update_and_set_cache()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Force disables all MFA methods for user.
|
||||
"""
|
||||
@spec disable(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
|
||||
def disable(%User{} = user) do
|
||||
user
|
||||
|> Changeset.disable_totp()
|
||||
|> Changeset.disable(true)
|
||||
|> User.update_and_set_cache()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if the user has MFA method enabled.
|
||||
"""
|
||||
def method_enabled?(method, settings) do
|
||||
with {:ok, %{confirmed: true} = _} <- Map.fetch(settings, method) do
|
||||
true
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if the user has enabled at least one MFA method.
|
||||
"""
|
||||
def enabled?(settings) do
|
||||
Settings.mfa_methods()
|
||||
|> Enum.map(fn m -> method_enabled?(m, settings) end)
|
||||
|> Enum.any?()
|
||||
end
|
||||
end
|
@ -0,0 +1,31 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.MFA.BackupCodes do
|
||||
@moduledoc """
|
||||
This module contains functions for generating backup codes.
|
||||
"""
|
||||
alias Pleroma.Config
|
||||
|
||||
@config_ns [:instance, :multi_factor_authentication, :backup_codes]
|
||||
|
||||
@doc """
|
||||
Generates backup codes.
|
||||
"""
|
||||
@spec generate(Keyword.t()) :: list(String.t())
|
||||
def generate(opts \\ []) do
|
||||
number_of_codes = Keyword.get(opts, :number_of_codes, default_backup_codes_number())
|
||||
code_length = Keyword.get(opts, :length, default_backup_codes_code_length())
|
||||
|
||||
Enum.map(1..number_of_codes, fn _ ->
|
||||
:crypto.strong_rand_bytes(div(code_length, 2))
|
||||
|> Base.encode16(case: :lower)
|
||||
end)
|
||||
end
|
||||
|
||||
defp default_backup_codes_number, do: Config.get(@config_ns ++ [:number], 5)
|
||||
|
||||
defp default_backup_codes_code_length,
|
||||
do: Config.get(@config_ns ++ [:length], 16)
|
||||
end
|
@ -0,0 +1,64 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.MFA.Changeset do
|
||||
alias Pleroma.MFA
|
||||
alias Pleroma.MFA.Settings
|
||||
alias Pleroma.User
|
||||
|
||||
def disable(%Ecto.Changeset{} = changeset, force \\ false) do
|
||||
settings =
|
||||
changeset
|
||||
|> Ecto.Changeset.apply_changes()
|
||||
|> MFA.fetch_settings()
|
||||
|
||||
if force || not MFA.enabled?(settings) do
|
||||
put_change(changeset, %Settings{settings | enabled: false})
|
||||
else
|
||||
changeset
|
||||
end
|
||||
end
|
||||
|
||||
def disable_totp(%User{multi_factor_authentication_settings: settings} = user) do
|
||||
user
|
||||
|> put_change(%Settings{settings | totp: %Settings.TOTP{}})
|
||||
end
|
||||
|
||||
def confirm_totp(%User{multi_factor_authentication_settings: settings} = user) do
|
||||
totp_settings = %Settings.TOTP{settings.totp | confirmed: true}
|
||||
|
||||
user
|
||||
|> put_change(%Settings{settings | totp: totp_settings, enabled: true})
|
||||
end
|
||||
|
||||
def setup_totp(%User{} = user, attrs) do
|
||||
mfa_settings = MFA.fetch_settings(user)
|
||||
|
||||
totp_settings =
|
||||
%Settings.TOTP{}
|
||||
|> Ecto.Changeset.cast(attrs, [:secret, :delivery_type])
|
||||
|
||||
user
|
||||
|> put_change(%Settings{mfa_settings | totp: Ecto.Changeset.apply_changes(totp_settings)})
|
||||
end
|
||||
|
||||
def cast_backup_codes(%User{} = user, codes) do
|
||||
user
|
||||
|> put_change(%Settings{
|
||||
user.multi_factor_authentication_settings
|
||||
| backup_codes: codes
|
||||
})
|
||||
end
|
||||
|
||||
defp put_change(%User{} = user, settings) do
|
||||
user
|
||||
|> Ecto.Changeset.change()
|
||||
|> put_change(settings)
|
||||
end
|
||||
|
||||
defp put_change(%Ecto.Changeset{} = changeset, settings) do
|
||||
changeset
|
||||
|> Ecto.Changeset.put_change(:multi_factor_authentication_settings, settings)
|
||||
end
|
||||
end
|
@ -0,0 +1,24 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.MFA.Settings do
|
||||
use Ecto.Schema
|
||||
|
||||
@primary_key false
|
||||
|
||||
@mfa_methods [:totp]
|
||||
embedded_schema do
|
||||
field(:enabled, :boolean, default: false)
|
||||
field(:backup_codes, {:array, :string}, default: [])
|
||||
|
||||
embeds_one :totp, TOTP, on_replace: :delete, primary_key: false do
|
||||
field(:secret, :string)
|
||||
# app | sms
|
||||
field(:delivery_type, :string, default: "app")
|
||||
field(:confirmed, :boolean, default: false)
|
||||
end
|
||||
end
|
||||
|
||||
def mfa_methods, do: @mfa_methods
|
||||
end
|
@ -0,0 +1,106 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.MFA.Token do
|
||||
use Ecto.Schema
|
||||
import Ecto.Query
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.OAuth.Authorization
|
||||
alias Pleroma.Web.OAuth.Token, as: OAuthToken
|
||||
|
||||
@expires 300
|
||||
|
||||
schema "mfa_tokens" do
|
||||
field(:token, :string)
|
||||
field(:valid_until, :naive_datetime_usec)
|
||||
|
||||
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
|
||||
belongs_to(:authorization, Authorization)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
def get_by_token(token) do
|
||||
from(
|
||||
t in __MODULE__,
|
||||
where: t.token == ^token,
|
||||
preload: [:user, :authorization]
|
||||
)
|
||||
|> Repo.find_resource()
|
||||
end
|
||||
|
||||
def validate(token) do
|
||||
with {:fetch_token, {:ok, token}} <- {:fetch_token, get_by_token(token)},
|
||||
{:expired, false} <- {:expired, is_expired?(token)} do
|
||||
{:ok, token}
|
||||
else
|
||||
{:expired, _} -> {:error, :expired_token}
|
||||
{:fetch_token, _} -> {:error, :not_found}
|
||||
error -> {:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
def create_token(%User{} = user) do
|
||||
%__MODULE__{}
|
||||
|> change
|
||||
|> assign_user(user)
|
||||
|> put_token
|
||||
|> put_valid_until
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
def create_token(user, authorization) do
|
||||
%__MODULE__{}
|
||||
|> change
|
||||
|> assign_user(user)
|
||||
|> assign_authorization(authorization)
|
||||
|> put_token
|
||||
|> put_valid_until
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
defp assign_user(changeset, user) do
|
||||
changeset
|
||||
|> put_assoc(:user, user)
|
||||
|> validate_required([:user])
|
||||
end
|
||||
|
||||
defp assign_authorization(changeset, authorization) do
|
||||
changeset
|
||||
|> put_assoc(:authorization, authorization)
|
||||
|> validate_required([:authorization])
|
||||
end
|
||||
|
||||
defp put_token(changeset) do
|
||||
changeset
|
||||
|> change(%{token: OAuthToken.Utils.generate_token()})
|
||||
|> validate_required([:token])
|
||||
|> unique_constraint(:token)
|
||||
end
|
||||
|
||||
defp put_valid_until(changeset) do
|
||||
expires_in = NaiveDateTime.add(NaiveDateTime.utc_now(), @expires)
|
||||
|
||||
changeset
|
||||
|> change(%{valid_until: expires_in})
|
||||
|> validate_required([:valid_until])
|
||||
end
|
||||
|
||||
def is_expired?(%__MODULE__{valid_until: valid_until}) do
|
||||
NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) > 0
|
||||
end
|
||||
|
||||
def is_expired?(_), do: false
|
||||
|
||||
def delete_expired_tokens do
|
||||
from(
|
||||
q in __MODULE__,
|
||||
where: fragment("?", q.valid_until) < ^Timex.now()
|
||||
)
|
||||
|> Repo.delete_all()
|
||||
end
|
||||
end
|
@ -0,0 +1,86 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.MFA.TOTP do
|
||||
@moduledoc """
|
||||
This module represents functions to create secrets for
|
||||
TOTP Application as well as validate them with a time based token.
|
||||
"""
|
||||
alias Pleroma.Config
|
||||
|
||||
@config_ns [:instance, :multi_factor_authentication, :totp]
|
||||
|
||||
@doc """
|
||||
https://github.com/google/google-authenticator/wiki/Key-Uri-Format
|
||||
"""
|
||||
def provisioning_uri(secret, label, opts \\ []) do
|
||||
query =
|
||||
%{
|
||||
secret: secret,
|
||||
issuer: Keyword.get(opts, :issuer, default_issuer()),
|
||||
digits: Keyword.get(opts, :digits, default_digits()),
|
||||
period: Keyword.get(opts, :period, default_period())
|
||||
}
|
||||
|> Enum.filter(fn {_, v} -> not is_nil(v) end)
|
||||
|> Enum.into(%{})
|
||||
|> URI.encode_query()
|
||||
|
||||
%URI{scheme: "otpauth", host: "totp", path: "/" <> label, query: query}
|
||||
|> URI.to_string()
|
||||
end
|
||||
|
||||
defp default_period, do: Config.get(@config_ns ++ [:period])
|
||||
defp default_digits, do: Config.get(@config_ns ++ [:digits])
|
||||
|
||||
defp default_issuer,
|
||||
do: Config.get(@config_ns ++ [:issuer], Config.get([:instance, :name]))
|
||||
|
||||
@doc "Creates a random Base 32 encoded string"
|
||||
def generate_secret do
|
||||
Base.encode32(:crypto.strong_rand_bytes(10))
|
||||
end
|
||||
|
||||
@doc "Generates a valid token based on a secret"
|
||||
def generate_token(secret) do
|
||||
:pot.totp(secret)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Validates a given token based on a secret.
|
||||
|
||||
optional parameters:
|
||||
`token_length` default `6`
|
||||
`interval_length` default `30`
|
||||
`window` default 0
|
||||
|
||||
Returns {:ok, :pass} if the token is valid and
|
||||
{:error, :invalid_token} if it is not.
|
||||
"""
|
||||
@spec validate_token(String.t(), String.t()) ::
|
||||
{:ok, :pass} | {:error, :invalid_token | :invalid_secret_and_token}
|
||||
def validate_token(secret, token)
|
||||
when is_binary(secret) and is_binary(token) do
|
||||
opts = [
|
||||
token_length: default_digits(),
|
||||
interval_length: default_period()
|
||||
]
|
||||
|
||||
validate_token(secret, token, opts)
|
||||
end
|
||||
|
||||
def validate_token(_, _), do: {:error, :invalid_secret_and_token}
|
||||
|
||||
@doc "See `validate_token/2`"
|
||||
@spec validate_token(String.t(), String.t(), Keyword.t()) ::
|
||||
{:ok, :pass} | {:error, :invalid_token | :invalid_secret_and_token}
|
||||
def validate_token(secret, token, options)
|
||||
when is_binary(secret) and is_binary(token) do
|
||||
case :pot.valid_totp(token, secret, options) do
|
||||
true -> {:ok, :pass}
|
||||
false -> {:error, :invalid_token}
|
||||
end
|
||||
end
|
||||
|
||||
def validate_token(_, _, _), do: {:error, :invalid_secret_and_token}
|
||||
end
|
@ -0,0 +1,99 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator do
|
||||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
|
||||
|
||||
import Ecto.Changeset
|
||||
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
|
||||
|
||||
@primary_key false
|
||||
|
||||
embedded_schema do
|
||||
field(:id, Types.ObjectID, primary_key: true)
|
||||
field(:type, :string)
|
||||
field(:actor, Types.ObjectID)
|
||||
field(:to, Types.Recipients, default: [])
|
||||
field(:cc, Types.Recipients, default: [])
|
||||
field(:deleted_activity_id, Types.ObjectID)
|
||||
field(:object, Types.ObjectID)
|
||||
end
|
||||
|
||||
def cast_data(data) do
|
||||
%__MODULE__{}
|
||||
|> cast(data, __schema__(:fields))
|
||||
end
|
||||
|
||||
def add_deleted_activity_id(cng) do
|
||||
object =
|
||||
cng
|
||||
|> get_field(:object)
|
||||
|
||||
with %Activity{id: id} <- Activity.get_create_by_object_ap_id(object) do
|
||||
cng
|
||||
|> put_change(:deleted_activity_id, id)
|
||||
else
|
||||
_ -> cng
|
||||
end
|
||||
end
|
||||
|
||||
@deletable_types ~w{
|
||||
Answer
|
||||
Article
|
||||
Audio
|
||||
Event
|
||||
Note
|
||||
Page
|
||||
Question
|
||||
Video
|
||||
}
|
||||
def validate_data(cng) do
|
||||
cng
|
||||
|> validate_required([:id, :type, :actor, :to, :cc, :object])
|
||||
|> validate_inclusion(:type, ["Delete"])
|
||||
|> validate_actor_presence()
|
||||
|> validate_deletion_rights()
|
||||
|> validate_object_or_user_presence(allowed_types: @deletable_types)
|
||||
|> add_deleted_activity_id()
|
||||
end
|
||||
|
||||
def do_not_federate?(cng) do
|
||||
!same_domain?(cng)
|
||||
end
|
||||
|
||||
defp same_domain?(cng) do
|
||||
actor_uri =
|
||||
cng
|
||||
|> get_field(:actor)
|
||||
|> URI.parse()
|
||||
|
||||
object_uri =
|
||||
cng
|
||||
|> get_field(:object)
|
||||
|> URI.parse()
|
||||
|
||||
object_uri.host == actor_uri.host
|
||||
end
|
||||
|
||||
def validate_deletion_rights(cng) do
|
||||
actor = User.get_cached_by_ap_id(get_field(cng, :actor))
|
||||
|
||||
if User.superuser?(actor) || same_domain?(cng) do
|
||||
cng
|
||||
else
|
||||
cng
|
||||
|> add_error(:actor, "is not allowed to delete object")
|
||||
end
|
||||
end
|
||||
|
||||
def cast_and_validate(data) do
|
||||
data
|
||||
|> cast_data
|
||||
|> validate_data
|
||||
end
|
||||
end
|
@ -0,0 +1,81 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
|
||||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
|
||||
|
||||
import Ecto.Changeset
|
||||
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
|
||||
|
||||
@primary_key false
|
||||
|
||||
embedded_schema do
|
||||
field(:id, Types.ObjectID, primary_key: true)
|
||||
field(:type, :string)
|
||||
field(:object, Types.ObjectID)
|
||||
field(:actor, Types.ObjectID)
|
||||
field(:context, :string)
|
||||
field(:content, :string)
|
||||
field(:to, {:array, :string}, default: [])
|
||||
field(:cc, {:array, :string}, default: [])
|
||||
end
|
||||
|
||||
def cast_and_validate(data) do
|
||||
data
|
||||
|> cast_data()
|
||||
|> validate_data()
|
||||
end
|
||||
|
||||
def cast_data(data) do
|
||||
%__MODULE__{}
|
||||
|> changeset(data)
|
||||
end
|
||||
|
||||
def changeset(struct, data) do
|
||||
struct
|
||||
|> cast(data, __schema__(:fields))
|
||||
|> fix_after_cast()
|
||||
end
|
||||
|
||||
def fix_after_cast(cng) do
|
||||
cng
|
||||
|> fix_context()
|
||||
end
|
||||
|
||||
def fix_context(cng) do
|
||||
object = get_field(cng, :object)
|
||||
|
||||
with nil <- get_field(cng, :context),
|
||||
%Object{data: %{"context" => context}} <- Object.get_cached_by_ap_id(object) do
|
||||
cng
|
||||
|> put_change(:context, context)
|
||||
else
|
||||
_ ->
|
||||
cng
|
||||
end
|
||||
end
|
||||
|
||||
def validate_emoji(cng) do
|
||||
content = get_field(cng, :content)
|
||||
|
||||
if Pleroma.Emoji.is_unicode_emoji?(content) do
|
||||
cng
|
||||
else
|
||||
cng
|
||||
|> add_error(:content, "must be a single character emoji")
|
||||
end
|
||||
end
|
||||
|
||||
def validate_data(data_cng) do
|
||||
data_cng
|
||||
|> validate_inclusion(:type, ["EmojiReact"])
|
||||
|> validate_required([:id, :type, :object, :actor, :context, :to, :cc, :content])
|
||||
|> validate_actor_presence()
|
||||
|> validate_object_presence()
|
||||
|> validate_emoji()
|
||||
end
|
||||
end
|
@ -0,0 +1,34 @@
|
||||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.Recipients do
|
||||
use Ecto.Type
|
||||
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.Types.ObjectID
|
||||
|
||||
def type, do: {:array, ObjectID}
|
||||
|
||||
def cast(object) when is_binary(object) do
|
||||
cast([object])
|
||||
end
|
||||
|
||||
def cast(data) when is_list(data) do
|
||||
data
|
||||
|> Enum.reduce({:ok, []}, fn element, acc ->
|
||||
case {acc, ObjectID.cast(element)} do
|
||||
{:error, _} -> :error
|
||||
{_, :error} -> :error
|
||||
{{:ok, list}, {:ok, id}} -> {:ok, [id | list]}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
def cast(_) do
|
||||
:error
|
||||
end
|
||||
|
||||
def dump(data) do
|
||||
{:ok, data}
|
||||
end
|
||||
|
||||
def load(data) do
|
||||
{:ok, data}
|
||||
end
|
||||
end
|
@ -0,0 +1,62 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator do
|
||||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
|
||||
|
||||
import Ecto.Changeset
|
||||
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
|
||||
|
||||
@primary_key false
|
||||
|
||||
embedded_schema do
|
||||
field(:id, Types.ObjectID, primary_key: true)
|
||||
field(:type, :string)
|
||||
field(:object, Types.ObjectID)
|
||||
field(:actor, Types.ObjectID)
|
||||
field(:to, {:array, :string}, default: [])
|
||||
field(:cc, {:array, :string}, default: [])
|
||||
end
|
||||
|
||||
def cast_and_validate(data) do
|
||||
data
|
||||
|> cast_data()
|
||||
|> validate_data()
|
||||
end
|
||||
|
||||
def cast_data(data) do
|
||||
%__MODULE__{}
|
||||
|> changeset(data)
|
||||
end
|
||||
|
||||
def changeset(struct, data) do
|
||||
struct
|
||||
|> cast(data, __schema__(:fields))
|
||||
end
|
||||
|
||||
def validate_data(data_cng) do
|
||||
data_cng
|
||||
|> validate_inclusion(:type, ["Undo"])
|
||||
|> validate_required([:id, :type, :object, :actor, :to, :cc])
|
||||
|> validate_actor_presence()
|
||||
|> validate_object_presence()
|
||||
|> validate_undo_rights()
|
||||
end
|
||||
|
||||
def validate_undo_rights(cng) do
|
||||
actor = get_field(cng, :actor)
|
||||
object = get_field(cng, :object)
|
||||
|
||||
with %Activity{data: %{"actor" => object_actor}} <- Activity.get_by_ap_id(object),
|
||||
true <- object_actor != actor do
|
||||
cng
|
||||
|> add_error(:actor, "not the same as object actor")
|
||||
else
|
||||
_ -> cng
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,139 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2019-2020 Moxley Stratton, Mike Buhot <https://github.com/open-api-spex/open_api_spex>, MPL-2.0
|
||||
# Copyright © 2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.CastAndValidate do
|
||||
@moduledoc """
|
||||
This plug is based on [`OpenApiSpex.Plug.CastAndValidate`]
|
||||
(https://github.com/open-api-spex/open_api_spex/blob/master/lib/open_api_spex/plug/cast_and_validate.ex).
|
||||
The main difference is ignoring unexpected query params instead of throwing
|
||||
an error and a config option (`[Pleroma.Web.ApiSpec.CastAndValidate, :strict]`)
|
||||
to disable this behavior. Also, the default rendering error module
|
||||
is `Pleroma.Web.ApiSpec.RenderError`.
|
||||
"""
|
||||
|
||||
@behaviour Plug
|
||||
|
||||
alias Plug.Conn
|
||||
|
||||
@impl Plug
|
||||
def init(opts) do
|
||||
opts
|
||||
|> Map.new()
|
||||
|> Map.put_new(:render_error, Pleroma.Web.ApiSpec.RenderError)
|
||||
end
|
||||
|
||||
@impl Plug
|
||||
def call(%{private: %{open_api_spex: private_data}} = conn, %{
|
||||
operation_id: operation_id,
|
||||
render_error: render_error
|
||||
}) do
|
||||
spec = private_data.spec
|
||||
operation = private_data.operation_lookup[operation_id]
|
||||
|
||||
content_type =
|
||||
case Conn.get_req_header(conn, "content-type") do
|
||||
[header_value | _] ->
|
||||
header_value
|
||||
|> String.split(";")
|
||||
|> List.first()
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
|
||||
private_data = Map.put(private_data, :operation_id, operation_id)
|
||||
conn = Conn.put_private(conn, :open_api_spex, private_data)
|
||||
|
||||
case cast_and_validate(spec, operation, conn, content_type, strict?()) do
|
||||
{:ok, conn} ->
|
||||
conn
|
||||
|
||||
{:error, reason} ->
|
||||
opts = render_error.init(reason)
|
||||
|
||||
conn
|
||||
|> render_error.call(opts)
|
||||
|> Plug.Conn.halt()
|
||||
end
|
||||
end
|
||||
|
||||
def call(
|
||||
%{
|
||||
private: %{
|
||||
phoenix_controller: controller,
|
||||
phoenix_action: action,
|
||||
open_api_spex: private_data
|
||||
}
|
||||
} = conn,
|
||||
opts
|
||||
) do
|
||||
operation =
|
||||
case private_data.operation_lookup[{controller, action}] do
|
||||
nil ->
|
||||
operation_id = controller.open_api_operation(action).operationId
|
||||
operation = private_data.operation_lookup[operation_id]
|
||||
|
||||
operation_lookup =
|
||||
private_data.operation_lookup
|
||||
|> Map.put({controller, action}, operation)
|
||||
|
||||
OpenApiSpex.Plug.Cache.adapter().put(
|
||||
private_data.spec_module,
|
||||
{private_data.spec, operation_lookup}
|
||||
)
|
||||
|
||||
operation
|
||||
|
||||
operation ->
|
||||
operation
|
||||
end
|
||||
|
||||
if operation.operationId do
|
||||
call(conn, Map.put(opts, :operation_id, operation.operationId))
|
||||
else
|
||||
raise "operationId was not found in action API spec"
|
||||
end
|
||||
end
|
||||
|
||||
def call(conn, opts), do: OpenApiSpex.Plug.CastAndValidate.call(conn, opts)
|
||||
|
||||
defp cast_and_validate(spec, operation, conn, content_type, true = _strict) do
|
||||
OpenApiSpex.cast_and_validate(spec, operation, conn, content_type)
|
||||
end
|
||||
|
||||
defp cast_and_validate(spec, operation, conn, content_type, false = _strict) do
|
||||
case OpenApiSpex.cast_and_validate(spec, operation, conn, content_type) do
|
||||
{:ok, conn} ->
|
||||
{:ok, conn}
|
||||
|
||||
# Remove unexpected query params and cast/validate again
|
||||
{:error, errors} ->
|
||||
query_params =
|
||||
Enum.reduce(errors, conn.query_params, fn
|
||||
%{reason: :unexpected_field, name: name, path: [name]}, params ->
|
||||
Map.delete(params, name)
|
||||
|
||||
%{reason: :invalid_enum, name: nil, path: path, value: value}, params ->
|
||||
path = path |> Enum.reverse() |> tl() |> Enum.reverse() |> list_items_to_string()
|
||||
update_in(params, path, &List.delete(&1, value))
|
||||
|
||||
_, params ->
|
||||
params
|
||||
end)
|
||||
|
||||
conn = %Conn{conn | query_params: query_params}
|
||||
OpenApiSpex.cast_and_validate(spec, operation, conn, content_type)
|
||||
end
|
||||
end
|
||||
|
||||
defp list_items_to_string(list) do
|
||||
Enum.map(list, fn
|
||||
i when is_atom(i) -> to_string(i)
|
||||
i -> i
|
||||
end)
|
||||
end
|
||||
|
||||
defp strict?, do: Pleroma.Config.get([__MODULE__, :strict], false)
|
||||
end
|
@ -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.ApiSpec.ConversationOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Conversation
|
||||
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
||||
|
||||
import Pleroma.Web.ApiSpec.Helpers
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def index_operation do
|
||||
%Operation{
|
||||
tags: ["Conversations"],
|
||||
summary: "Show conversation",
|
||||
security: [%{"oAuth" => ["read:statuses"]}],
|
||||
operationId: "ConversationController.index",
|
||||
parameters: [
|
||||
Operation.parameter(
|
||||
:recipients,
|
||||
:query,
|
||||
%Schema{type: :array, items: FlakeID},
|
||||
"Only return conversations with the given recipients (a list of user ids)"
|
||||
)
|
||||
| pagination_params()
|
||||
],
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Array of Conversation", "application/json", %Schema{
|
||||
type: :array,
|
||||
items: Conversation,
|
||||
example: [Conversation.schema().example]
|
||||
})
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def mark_as_read_operation do
|
||||
%Operation{
|
||||
tags: ["Conversations"],
|
||||
summary: "Mark as read",
|
||||
operationId: "ConversationController.mark_as_read",
|
||||
parameters: [
|
||||
Operation.parameter(:id, :path, :string, "Conversation ID",
|
||||
example: "123",
|
||||
required: true
|
||||
)
|
||||
],
|
||||
security: [%{"oAuth" => ["write:conversations"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Conversation", "application/json", Conversation)
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
@ -0,0 +1,227 @@
|
||||
# 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.FilterOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Helpers
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def index_operation do
|
||||
%Operation{
|
||||
tags: ["apps"],
|
||||
summary: "View all filters",
|
||||
operationId: "FilterController.index",
|
||||
security: [%{"oAuth" => ["read:filters"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Filters", "application/json", array_of_filters())
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def create_operation do
|
||||
%Operation{
|
||||
tags: ["apps"],
|
||||
summary: "Create a filter",
|
||||
operationId: "FilterController.create",
|
||||
requestBody: Helpers.request_body("Parameters", create_request(), required: true),
|
||||
security: [%{"oAuth" => ["write:filters"]}],
|
||||
responses: %{200 => Operation.response("Filter", "application/json", filter())}
|
||||
}
|
||||
end
|
||||
|
||||
def show_operation do
|
||||
%Operation{
|
||||
tags: ["apps"],
|
||||
summary: "View all filters",
|
||||
parameters: [id_param()],
|
||||
operationId: "FilterController.show",
|
||||
security: [%{"oAuth" => ["read:filters"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Filter", "application/json", filter())
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def update_operation do
|
||||
%Operation{
|
||||
tags: ["apps"],
|
||||
summary: "Update a filter",
|
||||
parameters: [id_param()],
|
||||
operationId: "FilterController.update",
|
||||
requestBody: Helpers.request_body("Parameters", update_request(), required: true),
|
||||
security: [%{"oAuth" => ["write:filters"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Filter", "application/json", filter())
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def delete_operation do
|
||||
%Operation{
|
||||
tags: ["apps"],
|
||||
summary: "Remove a filter",
|
||||
parameters: [id_param()],
|
||||
operationId: "FilterController.delete",
|
||||
security: [%{"oAuth" => ["write:filters"]}],
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Filter", "application/json", %Schema{
|
||||
type: :object,
|
||||
description: "Empty object"
|
||||
})
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp id_param do
|
||||
Operation.parameter(:id, :path, :string, "Filter ID", example: "123", required: true)
|
||||
end
|
||||
|
||||
defp filter do
|
||||
%Schema{
|
||||
title: "Filter",
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
phrase: %Schema{type: :string, description: "The text to be filtered"},
|
||||
context: %Schema{
|
||||
type: :array,
|
||||
items: %Schema{type: :string, enum: ["home", "notifications", "public", "thread"]},
|
||||
description: "The contexts in which the filter should be applied."
|
||||
},
|
||||
expires_at: %Schema{
|
||||
type: :string,
|
||||
format: :"date-time",
|
||||
description:
|
||||
"When the filter should no longer be applied. String (ISO 8601 Datetime), or null if the filter does not expire.",
|
||||
nullable: true
|
||||
},
|
||||
irreversible: %Schema{
|
||||
type: :boolean,
|
||||
description:
|
||||
"Should matching entities in home and notifications be dropped by the server?"
|
||||
},
|
||||
whole_word: %Schema{
|
||||
type: :boolean,
|
||||
description: "Should the filter consider word boundaries?"
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
"id" => "5580",
|
||||
"phrase" => "@twitter.com",
|
||||
"context" => [
|
||||
"home",
|
||||
"notifications",
|
||||
"public",
|
||||
"thread"
|
||||
],
|
||||
"whole_word" => false,
|
||||
"expires_at" => nil,
|
||||
"irreversible" => true
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp array_of_filters do
|
||||
%Schema{
|
||||
title: "ArrayOfFilters",
|
||||
description: "Array of Filters",
|
||||
type: :array,
|
||||
items: filter(),
|
||||
example: [
|
||||
%{
|
||||
"id" => "5580",
|
||||
"phrase" => "@twitter.com",
|
||||
"context" => [
|
||||
"home",
|
||||
"notifications",
|
||||
"public",
|
||||
"thread"
|
||||
],
|
||||
"whole_word" => false,
|
||||
"expires_at" => nil,
|
||||
"irreversible" => true
|
||||
},
|
||||
%{
|
||||
"id" => "6191",
|
||||
"phrase" => ":eurovision2019:",
|
||||
"context" => [
|
||||
"home"
|
||||
],
|
||||
"whole_word" => true,
|
||||
"expires_at" => "2019-05-21T13:47:31.333Z",
|
||||
"irreversible" => false
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
defp create_request do
|
||||
%Schema{
|
||||
title: "FilterCreateRequest",
|
||||
allOf: [
|
||||
update_request(),
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
irreversible: %Schema{
|
||||
type: :bolean,
|
||||
description:
|
||||
"Should the server irreversibly drop matching entities from home and notifications?",
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
example: %{
|
||||
"phrase" => "knights",
|
||||
"context" => ["home"]
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp update_request do
|
||||
%Schema{
|
||||
title: "FilterUpdateRequest",
|
||||
type: :object,
|
||||
properties: %{
|
||||
phrase: %Schema{type: :string, description: "The text to be filtered"},
|
||||
context: %Schema{
|
||||
type: :array,
|
||||
items: %Schema{type: :string, enum: ["home", "notifications", "public", "thread"]},
|
||||
description:
|
||||
"Array of enumerable strings `home`, `notifications`, `public`, `thread`. At least one context must be specified."
|
||||
},
|
||||
irreversible: %Schema{
|
||||
type: :bolean,
|
||||
description:
|
||||
"Should the server irreversibly drop matching entities from home and notifications?"
|
||||
},
|
||||
whole_word: %Schema{
|
||||
type: :bolean,
|
||||
description: "Consider word boundaries?",
|
||||
default: true
|
||||
}
|
||||
# TODO: probably should implement filter expiration
|
||||
# expires_in: %Schema{
|
||||
# type: :string,
|
||||
# format: :"date-time",
|
||||
# description:
|
||||
# "ISO 8601 Datetime for when the filter expires. Otherwise,
|
||||
# null for a filter that doesn't expire."
|
||||
# }
|
||||
},
|
||||
required: [:phrase, :context],
|
||||
example: %{
|
||||
"phrase" => "knights",
|
||||
"context" => ["home"]
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
@ -0,0 +1,65 @@
|
||||
# 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.FollowRequestOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Account
|
||||
alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def index_operation do
|
||||
%Operation{
|
||||
tags: ["Follow Requests"],
|
||||
summary: "Pending Follows",
|
||||
security: [%{"oAuth" => ["read:follows", "follow"]}],
|
||||
operationId: "FollowRequestController.index",
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Array of Account", "application/json", %Schema{
|
||||
type: :array,
|
||||
items: Account,
|
||||
example: [Account.schema().example]
|
||||
})
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def authorize_operation do
|
||||
%Operation{
|
||||
tags: ["Follow Requests"],
|
||||
summary: "Accept Follow",
|
||||
operationId: "FollowRequestController.authorize",
|
||||
parameters: [id_param()],
|
||||
security: [%{"oAuth" => ["follow", "write:follows"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Relationship", "application/json", AccountRelationship)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def reject_operation do
|
||||
%Operation{
|
||||
tags: ["Follow Requests"],
|
||||
summary: "Reject Follow",
|
||||
operationId: "FollowRequestController.reject",
|
||||
parameters: [id_param()],
|
||||
security: [%{"oAuth" => ["follow", "write:follows"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Relationship", "application/json", AccountRelationship)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp id_param do
|
||||
Operation.parameter(:id, :path, :string, "Conversation ID",
|
||||
example: "123",
|
||||
required: true
|
||||
)
|
||||
end
|
||||
end
|
@ -0,0 +1,169 @@
|
||||
# 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.InstanceOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def show_operation do
|
||||
%Operation{
|
||||
tags: ["Instance"],
|
||||
summary: "Fetch instance",
|
||||
description: "Information about the server",
|
||||
operationId: "InstanceController.show",
|
||||
responses: %{
|
||||
200 => Operation.response("Instance", "application/json", instance())
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def peers_operation do
|
||||
%Operation{
|
||||
tags: ["Instance"],
|
||||
summary: "List of known hosts",
|
||||
operationId: "InstanceController.peers",
|
||||
responses: %{
|
||||
200 => Operation.response("Array of domains", "application/json", array_of_domains())
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp instance do
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
uri: %Schema{type: :string, description: "The domain name of the instance"},
|
||||
title: %Schema{type: :string, description: "The title of the website"},
|
||||
description: %Schema{
|
||||
type: :string,
|
||||
description: "Admin-defined description of the Pleroma site"
|
||||
},
|
||||
version: %Schema{
|
||||
type: :string,
|
||||
description: "The version of Pleroma installed on the instance"
|
||||
},
|
||||
email: %Schema{
|
||||
type: :string,
|
||||
description: "An email that may be contacted for any inquiries",
|
||||
format: :email
|
||||
},
|
||||
urls: %Schema{
|
||||
type: :object,
|
||||
description: "URLs of interest for clients apps",
|
||||
properties: %{
|
||||
streaming_api: %Schema{
|
||||
type: :string,
|
||||
description: "Websockets address for push streaming"
|
||||
}
|
||||
}
|
||||
},
|
||||
stats: %Schema{
|
||||
type: :object,
|
||||
description: "Statistics about how much information the instance contains",
|
||||
properties: %{
|
||||
user_count: %Schema{
|
||||
type: :integer,
|
||||
description: "Users registered on this instance"
|
||||
},
|
||||
status_count: %Schema{
|
||||
type: :integer,
|
||||
description: "Statuses authored by users on instance"
|
||||
},
|
||||
domain_count: %Schema{
|
||||
type: :integer,
|
||||
description: "Domains federated with this instance"
|
||||
}
|
||||
}
|
||||
},
|
||||
thumbnail: %Schema{
|
||||
type: :string,
|
||||
description: "Banner image for the website",
|
||||
nullable: true
|
||||
},
|
||||
languages: %Schema{
|
||||
type: :array,
|
||||
items: %Schema{type: :string},
|
||||
description: "Primary langauges of the website and its staff"
|
||||
},
|
||||
registrations: %Schema{type: :boolean, description: "Whether registrations are enabled"},
|
||||
# Extra (not present in Mastodon):
|
||||
max_toot_chars: %Schema{
|
||||
type: :integer,
|
||||
description: ": Posts character limit (CW/Subject included in the counter)"
|
||||
},
|
||||
poll_limits: %Schema{
|
||||
type: :object,
|
||||
description: "A map with poll limits for local polls",
|
||||
properties: %{
|
||||
max_options: %Schema{
|
||||
type: :integer,
|
||||
description: "Maximum number of options."
|
||||
},
|
||||
max_option_chars: %Schema{
|
||||
type: :integer,
|
||||
description: "Maximum number of characters per option."
|
||||
},
|
||||
min_expiration: %Schema{
|
||||
type: :integer,
|
||||
description: "Minimum expiration time (in seconds)."
|
||||
},
|
||||
max_expiration: %Schema{
|
||||
type: :integer,
|
||||
description: "Maximum expiration time (in seconds)."
|
||||
}
|
||||
}
|
||||
},
|
||||
upload_limit: %Schema{
|
||||
type: :integer,
|
||||
description: "File size limit of uploads (except for avatar, background, banner)"
|
||||
},
|
||||
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"}
|
||||
},
|
||||
example: %{
|
||||
"avatar_upload_limit" => 2_000_000,
|
||||
"background_upload_limit" => 4_000_000,
|
||||
"banner_upload_limit" => 4_000_000,
|
||||
"description" => "A Pleroma instance, an alternative fediverse server",
|
||||
"email" => "lain@lain.com",
|
||||
"languages" => ["en"],
|
||||
"max_toot_chars" => 5000,
|
||||
"poll_limits" => %{
|
||||
"max_expiration" => 31_536_000,
|
||||
"max_option_chars" => 200,
|
||||
"max_options" => 20,
|
||||
"min_expiration" => 0
|
||||
},
|
||||
"registrations" => false,
|
||||
"stats" => %{
|
||||
"domain_count" => 2996,
|
||||
"status_count" => 15_802,
|
||||
"user_count" => 5
|
||||
},
|
||||
"thumbnail" => "https://lain.com/instance/thumbnail.jpeg",
|
||||
"title" => "lain.com",
|
||||
"upload_limit" => 16_000_000,
|
||||
"uri" => "https://lain.com",
|
||||
"urls" => %{
|
||||
"streaming_api" => "wss://lain.com"
|
||||
},
|
||||
"version" => "2.7.2 (compatible; Pleroma 2.0.50-536-g25eec6d7-develop)"
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp array_of_domains do
|
||||
%Schema{
|
||||
type: :array,
|
||||
items: %Schema{type: :string},
|
||||
example: ["pleroma.site", "lain.com", "bikeshed.party"]
|
||||
}
|
||||
end
|
||||
end
|
@ -0,0 +1,188 @@
|
||||
# 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.ListOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Account
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
||||
alias Pleroma.Web.ApiSpec.Schemas.List
|
||||
|
||||
import Pleroma.Web.ApiSpec.Helpers
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def index_operation do
|
||||
%Operation{
|
||||
tags: ["Lists"],
|
||||
summary: "Show user's lists",
|
||||
description: "Fetch all lists that the user owns",
|
||||
security: [%{"oAuth" => ["read:lists"]}],
|
||||
operationId: "ListController.index",
|
||||
responses: %{
|
||||
200 => Operation.response("Array of List", "application/json", array_of_lists())
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def create_operation do
|
||||
%Operation{
|
||||
tags: ["Lists"],
|
||||
summary: "Create a list",
|
||||
description: "Fetch the list with the given ID. Used for verifying the title of a list.",
|
||||
operationId: "ListController.create",
|
||||
requestBody: create_update_request(),
|
||||
security: [%{"oAuth" => ["write:lists"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("List", "application/json", List),
|
||||
400 => Operation.response("Error", "application/json", ApiError),
|
||||
404 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def show_operation do
|
||||
%Operation{
|
||||
tags: ["Lists"],
|
||||
summary: "Show a single list",
|
||||
description: "Fetch the list with the given ID. Used for verifying the title of a list.",
|
||||
operationId: "ListController.show",
|
||||
parameters: [id_param()],
|
||||
security: [%{"oAuth" => ["read:lists"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("List", "application/json", List),
|
||||
404 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def update_operation do
|
||||
%Operation{
|
||||
tags: ["Lists"],
|
||||
summary: "Update a list",
|
||||
description: "Change the title of a list",
|
||||
operationId: "ListController.update",
|
||||
parameters: [id_param()],
|
||||
requestBody: create_update_request(),
|
||||
security: [%{"oAuth" => ["write:lists"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("List", "application/json", List),
|
||||
422 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def delete_operation do
|
||||
%Operation{
|
||||
tags: ["Lists"],
|
||||
summary: "Delete a list",
|
||||
operationId: "ListController.delete",
|
||||
parameters: [id_param()],
|
||||
security: [%{"oAuth" => ["write:lists"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def list_accounts_operation do
|
||||
%Operation{
|
||||
tags: ["Lists"],
|
||||
summary: "View accounts in list",
|
||||
operationId: "ListController.list_accounts",
|
||||
parameters: [id_param()],
|
||||
security: [%{"oAuth" => ["read:lists"]}],
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Array of Account", "application/json", %Schema{
|
||||
type: :array,
|
||||
items: Account
|
||||
})
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def add_to_list_operation do
|
||||
%Operation{
|
||||
tags: ["Lists"],
|
||||
summary: "Add accounts to list",
|
||||
description: "Add accounts to the given list.",
|
||||
operationId: "ListController.add_to_list",
|
||||
parameters: [id_param()],
|
||||
requestBody: add_remove_accounts_request(),
|
||||
security: [%{"oAuth" => ["write:lists"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def remove_from_list_operation do
|
||||
%Operation{
|
||||
tags: ["Lists"],
|
||||
summary: "Remove accounts from list",
|
||||
operationId: "ListController.remove_from_list",
|
||||
parameters: [id_param()],
|
||||
requestBody: add_remove_accounts_request(),
|
||||
security: [%{"oAuth" => ["write:lists"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp array_of_lists do
|
||||
%Schema{
|
||||
title: "ArrayOfLists",
|
||||
description: "Response schema for lists",
|
||||
type: :array,
|
||||
items: List,
|
||||
example: [
|
||||
%{"id" => "123", "title" => "my list"},
|
||||
%{"id" => "1337", "title" => "another list"}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
defp id_param do
|
||||
Operation.parameter(:id, :path, :string, "List ID",
|
||||
example: "123",
|
||||
required: true
|
||||
)
|
||||
end
|
||||
|
||||
defp create_update_request do
|
||||
request_body(
|
||||
"Parameters",
|
||||
%Schema{
|
||||
description: "POST body for creating or updating a List",
|
||||
type: :object,
|
||||
properties: %{
|
||||
title: %Schema{type: :string, description: "List title"}
|
||||
},
|
||||
required: [:title]
|
||||
},
|
||||
required: true
|
||||
)
|
||||
end
|
||||
|
||||
defp add_remove_accounts_request do
|
||||
request_body(
|
||||
"Parameters",
|
||||
%Schema{
|
||||
description: "POST body for adding/removing accounts to/from a List",
|
||||
type: :object,
|
||||
properties: %{
|
||||
account_ids: %Schema{type: :array, description: "Array of account IDs", items: FlakeID}
|
||||
},
|
||||
required: [:account_ids]
|
||||
},
|
||||
required: true
|
||||
)
|
||||
end
|
||||
end
|
@ -0,0 +1,140 @@
|
||||
# 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.MarkerOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Helpers
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def index_operation do
|
||||
%Operation{
|
||||
tags: ["Markers"],
|
||||
summary: "Get saved timeline position",
|
||||
security: [%{"oAuth" => ["read:statuses"]}],
|
||||
operationId: "MarkerController.index",
|
||||
parameters: [
|
||||
Operation.parameter(
|
||||
:timeline,
|
||||
:query,
|
||||
%Schema{
|
||||
type: :array,
|
||||
items: %Schema{type: :string, enum: ["home", "notifications"]}
|
||||
},
|
||||
"Array of markers to fetch. If not provided, an empty object will be returned."
|
||||
)
|
||||
],
|
||||
responses: %{
|
||||
200 => Operation.response("Marker", "application/json", response()),
|
||||
403 => Operation.response("Error", "application/json", api_error())
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def upsert_operation do
|
||||
%Operation{
|
||||
tags: ["Markers"],
|
||||
summary: "Save position in timeline",
|
||||
operationId: "MarkerController.upsert",
|
||||
requestBody: Helpers.request_body("Parameters", upsert_request(), required: true),
|
||||
security: [%{"oAuth" => ["follow", "write:blocks"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Marker", "application/json", response()),
|
||||
403 => Operation.response("Error", "application/json", api_error())
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp marker do
|
||||
%Schema{
|
||||
title: "Marker",
|
||||
description: "Schema for a marker",
|
||||
type: :object,
|
||||
properties: %{
|
||||
last_read_id: %Schema{type: :string},
|
||||
version: %Schema{type: :integer},
|
||||
updated_at: %Schema{type: :string},
|
||||
pleroma: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
unread_count: %Schema{type: :integer}
|
||||
}
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
"last_read_id" => "35098814",
|
||||
"version" => 361,
|
||||
"updated_at" => "2019-11-26T22:37:25.239Z",
|
||||
"pleroma" => %{"unread_count" => 5}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp response do
|
||||
%Schema{
|
||||
title: "MarkersResponse",
|
||||
description: "Response schema for markers",
|
||||
type: :object,
|
||||
properties: %{
|
||||
notifications: %Schema{allOf: [marker()], nullable: true},
|
||||
home: %Schema{allOf: [marker()], nullable: true}
|
||||
},
|
||||
items: %Schema{type: :string},
|
||||
example: %{
|
||||
"notifications" => %{
|
||||
"last_read_id" => "35098814",
|
||||
"version" => 361,
|
||||
"updated_at" => "2019-11-26T22:37:25.239Z",
|
||||
"pleroma" => %{"unread_count" => 0}
|
||||
},
|
||||
"home" => %{
|
||||
"last_read_id" => "103206604258487607",
|
||||
"version" => 468,
|
||||
"updated_at" => "2019-11-26T22:37:25.235Z",
|
||||
"pleroma" => %{"unread_count" => 10}
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp upsert_request do
|
||||
%Schema{
|
||||
title: "MarkersUpsertRequest",
|
||||
description: "Request schema for marker upsert",
|
||||
type: :object,
|
||||
properties: %{
|
||||
notifications: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
last_read_id: %Schema{type: :string}
|
||||
}
|
||||
},
|
||||
home: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
last_read_id: %Schema{type: :string}
|
||||
}
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
"home" => %{
|
||||
"last_read_id" => "103194548672408537",
|
||||
"version" => 462,
|
||||
"updated_at" => "2019-11-24T19:39:39.337Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp api_error do
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{error: %Schema{type: :string}}
|
||||
}
|
||||
end
|
||||
end
|
@ -0,0 +1,211 @@
|
||||
# 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.NotificationOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Account
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Status
|
||||
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 index_operation do
|
||||
%Operation{
|
||||
tags: ["Notifications"],
|
||||
summary: "Get all notifications",
|
||||
description:
|
||||
"Notifications concerning the user. This API returns Link headers containing links to the next/previous page. However, the links can also be constructed dynamically using query params and `id` values.",
|
||||
operationId: "NotificationController.index",
|
||||
security: [%{"oAuth" => ["read:notifications"]}],
|
||||
parameters:
|
||||
[
|
||||
Operation.parameter(
|
||||
:exclude_types,
|
||||
:query,
|
||||
%Schema{type: :array, items: notification_type()},
|
||||
"Array of types to exclude"
|
||||
),
|
||||
Operation.parameter(
|
||||
:account_id,
|
||||
:query,
|
||||
%Schema{type: :string},
|
||||
"Return only notifications received from this account"
|
||||
),
|
||||
Operation.parameter(
|
||||
:exclude_visibilities,
|
||||
:query,
|
||||
%Schema{type: :array, items: VisibilityScope},
|
||||
"Exclude the notifications for activities with the given visibilities"
|
||||
),
|
||||
Operation.parameter(
|
||||
:include_types,
|
||||
:query,
|
||||
%Schema{type: :array, items: notification_type()},
|
||||
"Include the notifications for activities with the given types"
|
||||
),
|
||||
Operation.parameter(
|
||||
:with_muted,
|
||||
:query,
|
||||
BooleanLike,
|
||||
"Include the notifications from muted users"
|
||||
)
|
||||
] ++ pagination_params(),
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Array of notifications", "application/json", %Schema{
|
||||
type: :array,
|
||||
items: notification()
|
||||
}),
|
||||
404 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def show_operation do
|
||||
%Operation{
|
||||
tags: ["Notifications"],
|
||||
summary: "Get a single notification",
|
||||
description: "View information about a notification with a given ID.",
|
||||
operationId: "NotificationController.show",
|
||||
security: [%{"oAuth" => ["read:notifications"]}],
|
||||
parameters: [id_param()],
|
||||
responses: %{
|
||||
200 => Operation.response("Notification", "application/json", notification())
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def clear_operation do
|
||||
%Operation{
|
||||
tags: ["Notifications"],
|
||||
summary: "Dismiss all notifications",
|
||||
description: "Clear all notifications from the server.",
|
||||
operationId: "NotificationController.clear",
|
||||
security: [%{"oAuth" => ["write:notifications"]}],
|
||||
responses: %{200 => empty_object_response()}
|
||||
}
|
||||
end
|
||||
|
||||
def dismiss_operation do
|
||||
%Operation{
|
||||
tags: ["Notifications"],
|
||||
summary: "Dismiss a single notification",
|
||||
description: "Clear a single notification from the server.",
|
||||
operationId: "NotificationController.dismiss",
|
||||
parameters: [id_param()],
|
||||
security: [%{"oAuth" => ["write:notifications"]}],
|
||||
responses: %{200 => empty_object_response()}
|
||||
}
|
||||
end
|
||||
|
||||
def dismiss_via_body_operation do
|
||||
%Operation{
|
||||
tags: ["Notifications"],
|
||||
summary: "Dismiss a single notification",
|
||||
deprecated: true,
|
||||
description: "Clear a single notification from the server.",
|
||||
operationId: "NotificationController.dismiss_via_body",
|
||||
requestBody:
|
||||
request_body(
|
||||
"Parameters",
|
||||
%Schema{type: :object, properties: %{id: %Schema{type: :string}}},
|
||||
required: true
|
||||
),
|
||||
security: [%{"oAuth" => ["write:notifications"]}],
|
||||
responses: %{200 => empty_object_response()}
|
||||
}
|
||||
end
|
||||
|
||||
def destroy_multiple_operation do
|
||||
%Operation{
|
||||
tags: ["Notifications"],
|
||||
summary: "Dismiss multiple notifications",
|
||||
operationId: "NotificationController.destroy_multiple",
|
||||
security: [%{"oAuth" => ["write:notifications"]}],
|
||||
parameters: [
|
||||
Operation.parameter(
|
||||
:ids,
|
||||
:query,
|
||||
%Schema{type: :array, items: %Schema{type: :string}},
|
||||
"Array of notification IDs to dismiss",
|
||||
required: true
|
||||
)
|
||||
],
|
||||
responses: %{200 => empty_object_response()}
|
||||
}
|
||||
end
|
||||
|
||||
defp notification do
|
||||
%Schema{
|
||||
title: "Notification",
|
||||
description: "Response schema for a notification",
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
type: notification_type(),
|
||||
created_at: %Schema{type: :string, format: :"date-time"},
|
||||
account: %Schema{
|
||||
allOf: [Account],
|
||||
description: "The account that performed the action that generated the notification."
|
||||
},
|
||||
status: %Schema{
|
||||
allOf: [Status],
|
||||
description:
|
||||
"Status that was the object of the notification, e.g. in mentions, reblogs, favourites, or polls.",
|
||||
nullable: true
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
"id" => "34975861",
|
||||
"type" => "mention",
|
||||
"created_at" => "2019-11-23T07:49:02.064Z",
|
||||
"account" => Account.schema().example,
|
||||
"status" => Status.schema().example
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp notification_type do
|
||||
%Schema{
|
||||
type: :string,
|
||||
enum: [
|
||||
"follow",
|
||||
"favourite",
|
||||
"reblog",
|
||||
"mention",
|
||||
"poll",
|
||||
"pleroma:emoji_reaction",
|
||||
"move",
|
||||
"follow_request"
|
||||
],
|
||||
description: """
|
||||
The type of event that resulted in the notification.
|
||||
|
||||
- `follow` - Someone followed you
|
||||
- `mention` - Someone mentioned you in their status
|
||||
- `reblog` - Someone boosted one of your statuses
|
||||
- `favourite` - Someone favourited one of your statuses
|
||||
- `poll` - A poll you have voted in or created has ended
|
||||
- `move` - Someone moved their account
|
||||
- `pleroma:emoji_reaction` - Someone reacted with emoji to your status
|
||||
"""
|
||||
}
|
||||
end
|
||||
|
||||
defp id_param do
|
||||
Operation.parameter(:id, :path, :string, "Notification ID",
|
||||
example: "123",
|
||||
required: true
|
||||
)
|
||||
end
|
||||
end
|
@ -0,0 +1,76 @@
|
||||
# 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.PollOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Poll
|
||||
|
||||
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: ["Polls"],
|
||||
summary: "View a poll",
|
||||
security: [%{"oAuth" => ["read:statuses"]}],
|
||||
parameters: [id_param()],
|
||||
operationId: "PollController.show",
|
||||
responses: %{
|
||||
200 => Operation.response("Poll", "application/json", Poll),
|
||||
404 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def vote_operation do
|
||||
%Operation{
|
||||
tags: ["Polls"],
|
||||
summary: "Vote on a poll",
|
||||
parameters: [id_param()],
|
||||
operationId: "PollController.vote",
|
||||
requestBody: vote_request(),
|
||||
security: [%{"oAuth" => ["write:statuses"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Poll", "application/json", Poll),
|
||||
422 => Operation.response("Error", "application/json", ApiError),
|
||||
404 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp id_param do
|
||||
Operation.parameter(:id, :path, FlakeID, "Poll ID",
|
||||
example: "123",
|
||||
required: true
|
||||
)
|
||||
end
|
||||
|
||||
defp vote_request do
|
||||
request_body(
|
||||
"Parameters",
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
choices: %Schema{
|
||||
type: :array,
|
||||
items: %Schema{type: :integer},
|
||||
description: "Array of own votes containing index for each option (starting from 0)"
|
||||
}
|
||||
},
|
||||
required: [:choices]
|
||||
},
|
||||
required: true,
|
||||
example: %{
|
||||
"choices" => [0, 1, 2]
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
@ -0,0 +1,78 @@
|
||||
# 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.ReportOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Helpers
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def create_operation do
|
||||
%Operation{
|
||||
tags: ["reports"],
|
||||
summary: "File a report",
|
||||
description: "Report problematic users to your moderators",
|
||||
operationId: "ReportController.create",
|
||||
security: [%{"oAuth" => ["follow", "write:reports"]}],
|
||||
requestBody: Helpers.request_body("Parameters", create_request(), required: true),
|
||||
responses: %{
|
||||
200 => Operation.response("Report", "application/json", create_response()),
|
||||
400 => Operation.response("Report", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp create_request do
|
||||
%Schema{
|
||||
title: "ReportCreateRequest",
|
||||
description: "POST body for creating a report",
|
||||
type: :object,
|
||||
properties: %{
|
||||
account_id: %Schema{type: :string, description: "ID of the account to report"},
|
||||
status_ids: %Schema{
|
||||
type: :array,
|
||||
items: %Schema{type: :string},
|
||||
description: "Array of Statuses to attach to the report, for context"
|
||||
},
|
||||
comment: %Schema{
|
||||
type: :string,
|
||||
description: "Reason for the report"
|
||||
},
|
||||
forward: %Schema{
|
||||
type: :boolean,
|
||||
default: false,
|
||||
description:
|
||||
"If the account is remote, should the report be forwarded to the remote admin?"
|
||||
}
|
||||
},
|
||||
required: [:account_id],
|
||||
example: %{
|
||||
"account_id" => "123",
|
||||
"status_ids" => ["1337"],
|
||||
"comment" => "bad status!",
|
||||
"forward" => "false"
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp create_response do
|
||||
%Schema{
|
||||
title: "ReportResponse",
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string, description: "Report ID"},
|
||||
action_taken: %Schema{type: :boolean, description: "Is action taken?"}
|
||||
},
|
||||
example: %{
|
||||
"id" => "123",
|
||||
"action_taken" => false
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
@ -0,0 +1,96 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.ScheduledActivityOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ScheduledStatus
|
||||
|
||||
import Pleroma.Web.ApiSpec.Helpers
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def index_operation do
|
||||
%Operation{
|
||||
tags: ["Scheduled Statuses"],
|
||||
summary: "View scheduled statuses",
|
||||
security: [%{"oAuth" => ["read:statuses"]}],
|
||||
parameters: pagination_params(),
|
||||
operationId: "ScheduledActivity.index",
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Array of ScheduledStatus", "application/json", %Schema{
|
||||
type: :array,
|
||||
items: ScheduledStatus
|
||||
})
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def show_operation do
|
||||
%Operation{
|
||||
tags: ["Scheduled Statuses"],
|
||||
summary: "View a single scheduled status",
|
||||
security: [%{"oAuth" => ["read:statuses"]}],
|
||||
parameters: [id_param()],
|
||||
operationId: "ScheduledActivity.show",
|
||||
responses: %{
|
||||
200 => Operation.response("Scheduled Status", "application/json", ScheduledStatus),
|
||||
404 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def update_operation do
|
||||
%Operation{
|
||||
tags: ["Scheduled Statuses"],
|
||||
summary: "Schedule a status",
|
||||
operationId: "ScheduledActivity.update",
|
||||
security: [%{"oAuth" => ["write:statuses"]}],
|
||||
parameters: [id_param()],
|
||||
requestBody:
|
||||
request_body("Parameters", %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
scheduled_at: %Schema{
|
||||
type: :string,
|
||||
format: :"date-time",
|
||||
description:
|
||||
"ISO 8601 Datetime at which the status will be published. Must be at least 5 minutes into the future."
|
||||
}
|
||||
}
|
||||
}),
|
||||
responses: %{
|
||||
200 => Operation.response("Scheduled Status", "application/json", ScheduledStatus),
|
||||
404 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def delete_operation do
|
||||
%Operation{
|
||||
tags: ["Scheduled Statuses"],
|
||||
summary: "Cancel a scheduled status",
|
||||
security: [%{"oAuth" => ["write:statuses"]}],
|
||||
parameters: [id_param()],
|
||||
operationId: "ScheduledActivity.delete",
|
||||
responses: %{
|
||||
200 => Operation.response("Empty object", "application/json", %Schema{type: :object}),
|
||||
404 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp id_param do
|
||||
Operation.parameter(:id, :path, FlakeID, "Poll ID",
|
||||
example: "123",
|
||||
required: true
|
||||
)
|
||||
end
|
||||
end
|
@ -0,0 +1,207 @@
|
||||
# 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.SearchOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.AccountOperation
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Account
|
||||
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
|
||||
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Status
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Tag
|
||||
|
||||
import Pleroma.Web.ApiSpec.Helpers
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def account_search_operation do
|
||||
%Operation{
|
||||
tags: ["Search"],
|
||||
summary: "Search for matching accounts by username or display name",
|
||||
operationId: "SearchController.account_search",
|
||||
parameters: [
|
||||
Operation.parameter(:q, :query, %Schema{type: :string}, "What to search for",
|
||||
required: true
|
||||
),
|
||||
Operation.parameter(
|
||||
:limit,
|
||||
:query,
|
||||
%Schema{type: :integer, default: 40},
|
||||
"Maximum number of results"
|
||||
),
|
||||
Operation.parameter(
|
||||
:resolve,
|
||||
:query,
|
||||
%Schema{allOf: [BooleanLike], default: false},
|
||||
"Attempt WebFinger lookup. Use this when `q` is an exact address."
|
||||
),
|
||||
Operation.parameter(
|
||||
:following,
|
||||
:query,
|
||||
%Schema{allOf: [BooleanLike], default: false},
|
||||
"Only include accounts that the user is following"
|
||||
)
|
||||
],
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response(
|
||||
"Array of Account",
|
||||
"application/json",
|
||||
AccountOperation.array_of_accounts()
|
||||
)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def search_operation do
|
||||
%Operation{
|
||||
tags: ["Search"],
|
||||
summary: "Search results",
|
||||
security: [%{"oAuth" => ["read:search"]}],
|
||||
operationId: "SearchController.search",
|
||||
deprecated: true,
|
||||
parameters: [
|
||||
Operation.parameter(
|
||||
:account_id,
|
||||
:query,
|
||||
FlakeID,
|
||||
"If provided, statuses returned will be authored only by this account"
|
||||
),
|
||||
Operation.parameter(
|
||||
:type,
|
||||
:query,
|
||||
%Schema{type: :string, enum: ["accounts", "hashtags", "statuses"]},
|
||||
"Search type"
|
||||
),
|
||||
Operation.parameter(:q, :query, %Schema{type: :string}, "The search query", required: true),
|
||||
Operation.parameter(
|
||||
:resolve,
|
||||
:query,
|
||||
%Schema{allOf: [BooleanLike], default: false},
|
||||
"Attempt WebFinger lookup"
|
||||
),
|
||||
Operation.parameter(
|
||||
:following,
|
||||
:query,
|
||||
%Schema{allOf: [BooleanLike], default: false},
|
||||
"Only include accounts that the user is following"
|
||||
),
|
||||
Operation.parameter(
|
||||
:offset,
|
||||
:query,
|
||||
%Schema{type: :integer},
|
||||
"Offset"
|
||||
)
|
||||
| pagination_params()
|
||||
],
|
||||
responses: %{
|
||||
200 => Operation.response("Results", "application/json", results())
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def search2_operation do
|
||||
%Operation{
|
||||
tags: ["Search"],
|
||||
summary: "Search results",
|
||||
security: [%{"oAuth" => ["read:search"]}],
|
||||
operationId: "SearchController.search2",
|
||||
parameters: [
|
||||
Operation.parameter(
|
||||
:account_id,
|
||||
:query,
|
||||
FlakeID,
|
||||
"If provided, statuses returned will be authored only by this account"
|
||||
),
|
||||
Operation.parameter(
|
||||
:type,
|
||||
:query,
|
||||
%Schema{type: :string, enum: ["accounts", "hashtags", "statuses"]},
|
||||
"Search type"
|
||||
),
|
||||
Operation.parameter(:q, :query, %Schema{type: :string}, "What to search for",
|
||||
required: true
|
||||
),
|
||||
Operation.parameter(
|
||||
:resolve,
|
||||
:query,
|
||||
%Schema{allOf: [BooleanLike], default: false},
|
||||
"Attempt WebFinger lookup"
|
||||
),
|
||||
Operation.parameter(
|
||||
:following,
|
||||
:query,
|
||||
%Schema{allOf: [BooleanLike], default: false},
|
||||
"Only include accounts that the user is following"
|
||||
)
|
||||
| pagination_params()
|
||||
],
|
||||
responses: %{
|
||||
200 => Operation.response("Results", "application/json", results2())
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp results2 do
|
||||
%Schema{
|
||||
title: "SearchResults",
|
||||
type: :object,
|
||||
properties: %{
|
||||
accounts: %Schema{
|
||||
type: :array,
|
||||
items: Account,
|
||||
description: "Accounts which match the given query"
|
||||
},
|
||||
statuses: %Schema{
|
||||
type: :array,
|
||||
items: Status,
|
||||
description: "Statuses which match the given query"
|
||||
},
|
||||
hashtags: %Schema{
|
||||
type: :array,
|
||||
items: Tag,
|
||||
description: "Hashtags which match the given query"
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
"accounts" => [Account.schema().example],
|
||||
"statuses" => [Status.schema().example],
|
||||
"hashtags" => [Tag.schema().example]
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp results do
|
||||
%Schema{
|
||||
title: "SearchResults",
|
||||
type: :object,
|
||||
properties: %{
|
||||
accounts: %Schema{
|
||||
type: :array,
|
||||
items: Account,
|
||||
description: "Accounts which match the given query"
|
||||
},
|
||||
statuses: %Schema{
|
||||
type: :array,
|
||||
items: Status,
|
||||
description: "Statuses which match the given query"
|
||||
},
|
||||
hashtags: %Schema{
|
||||
type: :array,
|
||||
items: %Schema{type: :string},
|
||||
description: "Hashtags which match the given query"
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
"accounts" => [Account.schema().example],
|
||||
"statuses" => [Status.schema().example],
|
||||
"hashtags" => ["cofe"]
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
@ -0,0 +1,188 @@
|
||||
# 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.SubscriptionOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Helpers
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
alias Pleroma.Web.ApiSpec.Schemas.PushSubscription
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def create_operation do
|
||||
%Operation{
|
||||
tags: ["Push Subscriptions"],
|
||||
summary: "Subscribe to push notifications",
|
||||
description:
|
||||
"Add a Web Push API subscription to receive notifications. Each access token can have one push subscription. If you create a new subscription, the old subscription is deleted.",
|
||||
operationId: "SubscriptionController.create",
|
||||
security: [%{"oAuth" => ["push"]}],
|
||||
requestBody: Helpers.request_body("Parameters", create_request(), required: true),
|
||||
responses: %{
|
||||
200 => Operation.response("Push Subscription", "application/json", PushSubscription),
|
||||
400 => Operation.response("Error", "application/json", ApiError),
|
||||
403 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def show_operation do
|
||||
%Operation{
|
||||
tags: ["Push Subscriptions"],
|
||||
summary: "Get current subscription",
|
||||
description: "View the PushSubscription currently associated with this access token.",
|
||||
operationId: "SubscriptionController.show",
|
||||
security: [%{"oAuth" => ["push"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Push Subscription", "application/json", PushSubscription),
|
||||
403 => Operation.response("Error", "application/json", ApiError),
|
||||
404 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def update_operation do
|
||||
%Operation{
|
||||
tags: ["Push Subscriptions"],
|
||||
summary: "Change types of notifications",
|
||||
description:
|
||||
"Updates the current push subscription. Only the data part can be updated. To change fundamentals, a new subscription must be created instead.",
|
||||
operationId: "SubscriptionController.update",
|
||||
security: [%{"oAuth" => ["push"]}],
|
||||
requestBody: Helpers.request_body("Parameters", update_request(), required: true),
|
||||
responses: %{
|
||||
200 => Operation.response("Push Subscription", "application/json", PushSubscription),
|
||||
403 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def delete_operation do
|
||||
%Operation{
|
||||
tags: ["Push Subscriptions"],
|
||||
summary: "Remove current subscription",
|
||||
description: "Removes the current Web Push API subscription.",
|
||||
operationId: "SubscriptionController.delete",
|
||||
security: [%{"oAuth" => ["push"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Empty object", "application/json", %Schema{type: :object}),
|
||||
403 => Operation.response("Error", "application/json", ApiError),
|
||||
404 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp create_request do
|
||||
%Schema{
|
||||
title: "SubscriptionCreateRequest",
|
||||
description: "POST body for creating a push subscription",
|
||||
type: :object,
|
||||
properties: %{
|
||||
subscription: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
endpoint: %Schema{
|
||||
type: :string,
|
||||
description: "Endpoint URL that is called when a notification event occurs."
|
||||
},
|
||||
keys: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
p256dh: %Schema{
|
||||
type: :string,
|
||||
description:
|
||||
"User agent public key. Base64 encoded string of public key of ECDH key using `prime256v1` curve."
|
||||
},
|
||||
auth: %Schema{
|
||||
type: :string,
|
||||
description: "Auth secret. Base64 encoded string of 16 bytes of random data."
|
||||
}
|
||||
},
|
||||
required: [:p256dh, :auth]
|
||||
}
|
||||
},
|
||||
required: [:endpoint, :keys]
|
||||
},
|
||||
data: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
alerts: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
follow: %Schema{type: :boolean, description: "Receive follow notifications?"},
|
||||
favourite: %Schema{
|
||||
type: :boolean,
|
||||
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?"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
required: [:subscription],
|
||||
example: %{
|
||||
"subscription" => %{
|
||||
"endpoint" => "https://example.com/example/1234",
|
||||
"keys" => %{
|
||||
"auth" => "8eDyX_uCN0XRhSbY5hs7Hg==",
|
||||
"p256dh" =>
|
||||
"BCIWgsnyXDv1VkhqL2P7YRBvdeuDnlwAPT2guNhdIoW3IP7GmHh1SMKPLxRf7x8vJy6ZFK3ol2ohgn_-0yP7QQA="
|
||||
}
|
||||
},
|
||||
"data" => %{
|
||||
"alerts" => %{
|
||||
"follow" => true,
|
||||
"mention" => true,
|
||||
"poll" => false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp update_request do
|
||||
%Schema{
|
||||
title: "SubscriptionUpdateRequest",
|
||||
type: :object,
|
||||
properties: %{
|
||||
data: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
alerts: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
follow: %Schema{type: :boolean, description: "Receive follow notifications?"},
|
||||
favourite: %Schema{
|
||||
type: :boolean,
|
||||
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?"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
"data" => %{
|
||||
"alerts" => %{
|
||||
"follow" => true,
|
||||
"favourite" => true,
|
||||
"reblog" => true,
|
||||
"mention" => true,
|
||||
"poll" => true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
@ -0,0 +1,68 @@
|
||||
# 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.Schemas.Attachment do
|
||||
alias OpenApiSpex.Schema
|
||||
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "Attachment",
|
||||
description: "Represents a file or media attachment that can be added to a status.",
|
||||
type: :object,
|
||||
requried: [:id, :url, :preview_url],
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
url: %Schema{
|
||||
type: :string,
|
||||
format: :uri,
|
||||
description: "The location of the original full-size attachment"
|
||||
},
|
||||
remote_url: %Schema{
|
||||
type: :string,
|
||||
format: :uri,
|
||||
description:
|
||||
"The location of the full-size original attachment on the remote website. String (URL), or null if the attachment is local",
|
||||
nullable: true
|
||||
},
|
||||
preview_url: %Schema{
|
||||
type: :string,
|
||||
format: :uri,
|
||||
description: "The location of a scaled-down preview of the attachment"
|
||||
},
|
||||
text_url: %Schema{
|
||||
type: :string,
|
||||
format: :uri,
|
||||
description: "A shorter URL for the attachment"
|
||||
},
|
||||
description: %Schema{
|
||||
type: :string,
|
||||
nullable: true,
|
||||
description:
|
||||
"Alternate text that describes what is in the media attachment, to be used for the visually impaired or when media attachments do not load"
|
||||
},
|
||||
type: %Schema{
|
||||
type: :string,
|
||||
enum: ["image", "video", "audio", "unknown"],
|
||||
description: "The type of the attachment"
|
||||
},
|
||||
pleroma: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
mime_type: %Schema{type: :string, description: "mime type of the attachment"}
|
||||
}
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
id: "1638338801",
|
||||
type: "image",
|
||||
url: "someurl",
|
||||
remote_url: "someurl",
|
||||
preview_url: "someurl",
|
||||
text_url: "someurl",
|
||||
description: nil,
|
||||
pleroma: %{mime_type: "image/png"}
|
||||
}
|
||||
})
|
||||
end
|
@ -0,0 +1,41 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.Schemas.Conversation do
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Account
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Status
|
||||
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "Conversation",
|
||||
description: "Represents a conversation with \"direct message\" visibility.",
|
||||
type: :object,
|
||||
required: [:id, :accounts, :unread],
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
accounts: %Schema{
|
||||
type: :array,
|
||||
items: Account,
|
||||
description: "Participants in the conversation"
|
||||
},
|
||||
unread: %Schema{
|
||||
type: :boolean,
|
||||
description: "Is the conversation currently marked as unread?"
|
||||
},
|
||||
# last_status: Status
|
||||
last_status: %Schema{
|
||||
allOf: [Status],
|
||||
description: "The last status in the conversation, to be used for optional display"
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
"id" => "418450",
|
||||
"unread" => true,
|
||||
"accounts" => [Account.schema().example],
|
||||
"last_status" => Status.schema().example
|
||||
}
|
||||
})
|
||||
end
|
@ -0,0 +1,23 @@
|
||||
# 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.Schemas.List do
|
||||
alias OpenApiSpex.Schema
|
||||
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "List",
|
||||
description: "Represents a list of users",
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string, description: "The internal database ID of the list"},
|
||||
title: %Schema{type: :string, description: "The user-defined title of the list"}
|
||||
},
|
||||
example: %{
|
||||
"id" => "12249",
|
||||
"title" => "Friends"
|
||||
}
|
||||
})
|
||||
end
|
@ -0,0 +1,66 @@
|
||||
# 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.Schemas.PushSubscription do
|
||||
alias OpenApiSpex.Schema
|
||||
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "PushSubscription",
|
||||
description: "Response schema for a push subscription",
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{
|
||||
anyOf: [%Schema{type: :string}, %Schema{type: :integer}],
|
||||
description: "The id of the push subscription in the database."
|
||||
},
|
||||
endpoint: %Schema{type: :string, description: "Where push alerts will be sent to."},
|
||||
server_key: %Schema{type: :string, description: "The streaming server's VAPID key."},
|
||||
alerts: %Schema{
|
||||
type: :object,
|
||||
description: "Which alerts should be delivered to the endpoint.",
|
||||
properties: %{
|
||||
follow: %Schema{
|
||||
type: :boolean,
|
||||
description: "Receive a push notification when someone has followed you?"
|
||||
},
|
||||
favourite: %Schema{
|
||||
type: :boolean,
|
||||
description:
|
||||
"Receive a push notification when a status you created has been favourited by someone else?"
|
||||
},
|
||||
reblog: %Schema{
|
||||
type: :boolean,
|
||||
description:
|
||||
"Receive a push notification when a status you created has been boosted by someone else?"
|
||||
},
|
||||
mention: %Schema{
|
||||
type: :boolean,
|
||||
description:
|
||||
"Receive a push notification when someone else has mentioned you in a status?"
|
||||
},
|
||||
poll: %Schema{
|
||||
type: :boolean,
|
||||
description:
|
||||
"Receive a push notification when a poll you voted in or created has ended? "
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
"id" => "328_183",
|
||||
"endpoint" => "https://yourdomain.example/listener",
|
||||
"alerts" => %{
|
||||
"follow" => true,
|
||||
"favourite" => true,
|
||||
"reblog" => true,
|
||||
"mention" => true,
|
||||
"poll" => true
|
||||
},
|
||||
"server_key" =>
|
||||
"BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M="
|
||||
}
|
||||
})
|
||||
end
|
@ -0,0 +1,54 @@
|
||||
# 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.Schemas.ScheduledStatus do
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Attachment
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Poll
|
||||
alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
|
||||
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "ScheduledStatus",
|
||||
description: "Represents a status that will be published at a future scheduled date.",
|
||||
type: :object,
|
||||
required: [:id, :scheduled_at, :params],
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
scheduled_at: %Schema{type: :string, format: :"date-time"},
|
||||
media_attachments: %Schema{type: :array, items: Attachment},
|
||||
params: %Schema{
|
||||
type: :object,
|
||||
required: [:text, :visibility],
|
||||
properties: %{
|
||||
text: %Schema{type: :string, nullable: true},
|
||||
media_ids: %Schema{type: :array, nullable: true, items: %Schema{type: :string}},
|
||||
sensitive: %Schema{type: :boolean, nullable: true},
|
||||
spoiler_text: %Schema{type: :string, nullable: true},
|
||||
visibility: %Schema{type: VisibilityScope, nullable: true},
|
||||
scheduled_at: %Schema{type: :string, format: :"date-time", nullable: true},
|
||||
poll: %Schema{type: Poll, nullable: true},
|
||||
in_reply_to_id: %Schema{type: :string, nullable: true}
|
||||
}
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
id: "3221",
|
||||
scheduled_at: "2019-12-05T12:33:01.000Z",
|
||||
params: %{
|
||||
text: "test content",
|
||||
media_ids: nil,
|
||||
sensitive: nil,
|
||||
spoiler_text: nil,
|
||||
visibility: nil,
|
||||
scheduled_at: nil,
|
||||
poll: nil,
|
||||
idempotency: nil,
|
||||
in_reply_to_id: nil
|
||||
},
|
||||
media_attachments: [Attachment.schema().example]
|
||||
}
|
||||
})
|
||||
end
|
@ -0,0 +1,27 @@
|
||||
# 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.Schemas.Tag do
|
||||
alias OpenApiSpex.Schema
|
||||
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "Tag",
|
||||
description: "Represents a hashtag used within the content of a status",
|
||||
type: :object,
|
||||
properties: %{
|
||||
name: %Schema{type: :string, description: "The value of the hashtag after the # sign"},
|
||||
url: %Schema{
|
||||
type: :string,
|
||||
format: :uri,
|
||||
description: "A link to the hashtag on the instance"
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
name: "cofe",
|
||||
url: "https://lain.com/tag/cofe"
|
||||
}
|
||||
})
|
||||
end
|
@ -0,0 +1,45 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Auth.TOTPAuthenticator do
|
||||
alias Comeonin.Pbkdf2
|
||||
alias Pleroma.MFA
|
||||
alias Pleroma.MFA.TOTP
|
||||
alias Pleroma.User
|
||||
|
||||
@doc "Verify code or check backup code."
|
||||
@spec verify(String.t(), User.t()) ::
|
||||
{:ok, :pass} | {:error, :invalid_token | :invalid_secret_and_token}
|
||||
def verify(
|
||||
token,
|
||||
%User{
|
||||
multi_factor_authentication_settings:
|
||||
%{enabled: true, totp: %{secret: secret, confirmed: true}} = _
|
||||
} = _user
|
||||
)
|
||||
when is_binary(token) and byte_size(token) > 0 do
|
||||
TOTP.validate_token(secret, token)
|
||||
end
|
||||
|
||||
def verify(_, _), do: {:error, :invalid_token}
|
||||
|
||||
@spec verify_recovery_code(User.t(), String.t()) ::
|
||||
{:ok, :pass} | {:error, :invalid_token}
|
||||
def verify_recovery_code(
|
||||
%User{multi_factor_authentication_settings: %{enabled: true, backup_codes: codes}} = user,
|
||||
code
|
||||
)
|
||||
when is_list(codes) and is_binary(code) do
|
||||
hash_code = Enum.find(codes, fn hash -> Pbkdf2.checkpw(code, hash) end)
|
||||
|
||||
if hash_code do
|
||||
MFA.invalidate_backup_code(user, hash_code)
|
||||
{:ok, :pass}
|
||||
else
|
||||
{:error, :invalid_token}
|
||||
end
|
||||
end
|
||||
|
||||
def verify_recovery_code(_, _), do: {:error, :invalid_token}
|
||||
end
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue