Merge branch 'develop' into issue/1276-2
This commit is contained in:
commit
a92c713d9c
300 changed files with 7770 additions and 3577 deletions
49
lib/mix/tasks/pleroma/app.ex
Normal file
49
lib/mix/tasks/pleroma/app.ex
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Mix.Tasks.Pleroma.App do
|
||||
@moduledoc File.read!("docs/administration/CLI_tasks/oauth_app.md")
|
||||
use Mix.Task
|
||||
|
||||
import Mix.Pleroma
|
||||
|
||||
@shortdoc "Creates trusted OAuth App"
|
||||
|
||||
def run(["create" | options]) do
|
||||
start_pleroma()
|
||||
|
||||
{opts, _} =
|
||||
OptionParser.parse!(options,
|
||||
strict: [name: :string, redirect_uri: :string, scopes: :string],
|
||||
aliases: [n: :name, r: :redirect_uri, s: :scopes]
|
||||
)
|
||||
|
||||
scopes =
|
||||
if opts[:scopes] do
|
||||
String.split(opts[:scopes], ",")
|
||||
else
|
||||
["read", "write", "follow", "push"]
|
||||
end
|
||||
|
||||
params = %{
|
||||
client_name: opts[:name],
|
||||
redirect_uris: opts[:redirect_uri],
|
||||
trusted: true,
|
||||
scopes: scopes
|
||||
}
|
||||
|
||||
with {:ok, app} <- Pleroma.Web.OAuth.App.create(params) do
|
||||
shell_info("#{app.client_name} successfully created:")
|
||||
shell_info("App client_id: " <> app.client_id)
|
||||
shell_info("App client_secret: " <> app.client_secret)
|
||||
else
|
||||
{:error, changeset} ->
|
||||
shell_error("Creating failed:")
|
||||
|
||||
Enum.each(Pleroma.Web.OAuth.App.errors(changeset), fn {key, error} ->
|
||||
shell_error("#{key}: #{error}")
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -27,17 +27,13 @@ defmodule Pleroma.Activity do
|
|||
# https://github.com/tootsuite/mastodon/blob/master/app/models/notification.rb#L19
|
||||
@mastodon_notification_types %{
|
||||
"Create" => "mention",
|
||||
"Follow" => "follow",
|
||||
"Follow" => ["follow", "follow_request"],
|
||||
"Announce" => "reblog",
|
||||
"Like" => "favourite",
|
||||
"Move" => "move",
|
||||
"EmojiReact" => "pleroma:emoji_reaction"
|
||||
}
|
||||
|
||||
@mastodon_to_ap_notification_types for {k, v} <- @mastodon_notification_types,
|
||||
into: %{},
|
||||
do: {v, k}
|
||||
|
||||
schema "activities" do
|
||||
field(:data, :map)
|
||||
field(:local, :boolean, default: true)
|
||||
|
|
@ -291,15 +287,43 @@ defmodule Pleroma.Activity do
|
|||
|
||||
defp purge_web_resp_cache(nil), do: nil
|
||||
|
||||
for {ap_type, type} <- @mastodon_notification_types do
|
||||
def follow_accepted?(
|
||||
%Activity{data: %{"type" => "Follow", "object" => followed_ap_id}} = activity
|
||||
) do
|
||||
with %User{} = follower <- Activity.user_actor(activity),
|
||||
%User{} = followed <- User.get_cached_by_ap_id(followed_ap_id) do
|
||||
Pleroma.FollowingRelationship.following?(follower, followed)
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
def follow_accepted?(_), do: false
|
||||
|
||||
@spec mastodon_notification_type(Activity.t()) :: String.t() | nil
|
||||
|
||||
for {ap_type, type} <- @mastodon_notification_types, not is_list(type) do
|
||||
def mastodon_notification_type(%Activity{data: %{"type" => unquote(ap_type)}}),
|
||||
do: unquote(type)
|
||||
end
|
||||
|
||||
def mastodon_notification_type(%Activity{data: %{"type" => "Follow"}} = activity) do
|
||||
if follow_accepted?(activity) do
|
||||
"follow"
|
||||
else
|
||||
"follow_request"
|
||||
end
|
||||
end
|
||||
|
||||
def mastodon_notification_type(%Activity{}), do: nil
|
||||
|
||||
@spec from_mastodon_notification_type(String.t()) :: String.t() | nil
|
||||
@doc "Converts Mastodon notification type to AR activity type"
|
||||
def from_mastodon_notification_type(type) do
|
||||
Map.get(@mastodon_to_ap_notification_types, type)
|
||||
with {k, _v} <-
|
||||
Enum.find(@mastodon_notification_types, fn {_k, v} -> type in List.wrap(v) end) do
|
||||
k
|
||||
end
|
||||
end
|
||||
|
||||
def all_by_actor_and_id(actor, status_ids \\ [])
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ defmodule Pleroma.Config.Loader do
|
|||
@spec filter_group(atom(), keyword()) :: keyword()
|
||||
def filter_group(group, configs) do
|
||||
Enum.reject(configs[group], fn {key, _v} ->
|
||||
key in @reject_keys or (group == :phoenix and key == :serve_endpoints)
|
||||
key in @reject_keys or (group == :phoenix and key == :serve_endpoints) or group == :postgrex
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -46,14 +46,6 @@ defmodule Pleroma.Config.TransferTask do
|
|||
with {_, true} <- {:configurable, Config.get(:configurable_from_database)} do
|
||||
# We need to restart applications for loaded settings take effect
|
||||
|
||||
# TODO: some problem with prometheus after restart!
|
||||
reject_restart =
|
||||
if restart_pleroma? do
|
||||
[nil, :prometheus]
|
||||
else
|
||||
[:pleroma, nil, :prometheus]
|
||||
end
|
||||
|
||||
{logger, other} =
|
||||
(Repo.all(ConfigDB) ++ deleted_settings)
|
||||
|> Enum.map(&transform_and_merge/1)
|
||||
|
|
@ -65,10 +57,20 @@ defmodule Pleroma.Config.TransferTask do
|
|||
|
||||
started_applications = Application.started_applications()
|
||||
|
||||
# TODO: some problem with prometheus after restart!
|
||||
reject = [nil, :prometheus, :postgrex]
|
||||
|
||||
reject =
|
||||
if restart_pleroma? do
|
||||
reject
|
||||
else
|
||||
[:pleroma | reject]
|
||||
end
|
||||
|
||||
other
|
||||
|> Enum.map(&update/1)
|
||||
|> Enum.uniq()
|
||||
|> Enum.reject(&(&1 in reject_restart))
|
||||
|> Enum.reject(&(&1 in reject))
|
||||
|> maybe_set_pleroma_last()
|
||||
|> Enum.each(&restart(started_applications, &1, Config.get(:env)))
|
||||
|
||||
|
|
@ -122,7 +124,7 @@ defmodule Pleroma.Config.TransferTask do
|
|||
:ok = update_env(:logger, :backends, merged)
|
||||
end
|
||||
|
||||
defp configure({group, key, _, merged}) do
|
||||
defp configure({_, key, _, merged}) when key in [:console, :ex_syslogger] do
|
||||
merged =
|
||||
if key == :console do
|
||||
put_in(merged[:format], merged[:format] <> "\n")
|
||||
|
|
@ -136,7 +138,12 @@ defmodule Pleroma.Config.TransferTask do
|
|||
else: key
|
||||
|
||||
Logger.configure_backend(backend, merged)
|
||||
:ok = update_env(:logger, group, merged)
|
||||
:ok = update_env(:logger, key, merged)
|
||||
end
|
||||
|
||||
defp configure({_, key, _, merged}) do
|
||||
Logger.configure([{key, merged}])
|
||||
:ok = update_env(:logger, key, merged)
|
||||
end
|
||||
|
||||
defp update({group, key, value, merged}) do
|
||||
|
|
|
|||
|
|
@ -38,22 +38,14 @@ defmodule Pleroma.Emoji.Formatter do
|
|||
|
||||
def demojify(text, nil), do: text
|
||||
|
||||
@doc "Outputs a list of the emoji-shortcodes in a text"
|
||||
def get_emoji(text) when is_binary(text) do
|
||||
Enum.filter(Emoji.get_all(), fn {emoji, %Emoji{}} ->
|
||||
String.contains?(text, ":#{emoji}:")
|
||||
end)
|
||||
end
|
||||
|
||||
def get_emoji(_), do: []
|
||||
|
||||
@doc "Outputs a list of the emoji-Maps in a text"
|
||||
def get_emoji_map(text) when is_binary(text) do
|
||||
get_emoji(text)
|
||||
Emoji.get_all()
|
||||
|> Enum.filter(fn {emoji, %Emoji{}} -> String.contains?(text, ":#{emoji}:") end)
|
||||
|> Enum.reduce(%{}, fn {name, %Emoji{file: file}}, acc ->
|
||||
Map.put(acc, name, "#{Pleroma.Web.Endpoint.static_url()}#{file}")
|
||||
end)
|
||||
end
|
||||
|
||||
def get_emoji_map(_), do: []
|
||||
def get_emoji_map(_), do: %{}
|
||||
end
|
||||
|
|
|
|||
507
lib/pleroma/emoji/pack.ex
Normal file
507
lib/pleroma/emoji/pack.ex
Normal file
|
|
@ -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
|
||||
|
|
@ -10,11 +10,12 @@ defmodule Pleroma.FollowingRelationship do
|
|||
|
||||
alias Ecto.Changeset
|
||||
alias FlakeId.Ecto.CompatType
|
||||
alias Pleroma.FollowingRelationship.State
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
|
||||
schema "following_relationships" do
|
||||
field(:state, Pleroma.FollowingRelationship.State, default: :follow_pending)
|
||||
field(:state, State, default: :follow_pending)
|
||||
|
||||
belongs_to(:follower, User, type: CompatType)
|
||||
belongs_to(:following, User, type: CompatType)
|
||||
|
|
@ -22,6 +23,11 @@ defmodule Pleroma.FollowingRelationship do
|
|||
timestamps()
|
||||
end
|
||||
|
||||
@doc "Returns underlying integer code for state atom"
|
||||
def state_int_code(state_atom), do: State.__enum_map__() |> Keyword.fetch!(state_atom)
|
||||
|
||||
def accept_state_code, do: state_int_code(:follow_accept)
|
||||
|
||||
def changeset(%__MODULE__{} = following_relationship, attrs) do
|
||||
following_relationship
|
||||
|> cast(attrs, [:state])
|
||||
|
|
@ -82,6 +88,29 @@ defmodule Pleroma.FollowingRelationship do
|
|||
|> Repo.aggregate(:count, :id)
|
||||
end
|
||||
|
||||
def followers_query(%User{} = user) do
|
||||
__MODULE__
|
||||
|> join(:inner, [r], u in User, on: r.follower_id == u.id)
|
||||
|> where([r], r.following_id == ^user.id)
|
||||
|> where([r], r.state == ^:follow_accept)
|
||||
end
|
||||
|
||||
def followers_ap_ids(%User{} = user, from_ap_ids \\ nil) do
|
||||
query =
|
||||
user
|
||||
|> followers_query()
|
||||
|> select([r, u], u.ap_id)
|
||||
|
||||
query =
|
||||
if from_ap_ids do
|
||||
where(query, [r, u], u.ap_id in ^from_ap_ids)
|
||||
else
|
||||
query
|
||||
end
|
||||
|
||||
Repo.all(query)
|
||||
end
|
||||
|
||||
def following_count(%User{id: nil}), do: 0
|
||||
|
||||
def following_count(%User{} = user) do
|
||||
|
|
@ -105,12 +134,16 @@ defmodule Pleroma.FollowingRelationship do
|
|||
|> Repo.exists?()
|
||||
end
|
||||
|
||||
def following_query(%User{} = user) do
|
||||
__MODULE__
|
||||
|> join(:inner, [r], u in User, on: r.following_id == u.id)
|
||||
|> where([r], r.follower_id == ^user.id)
|
||||
|> where([r], r.state == ^:follow_accept)
|
||||
end
|
||||
|
||||
def following(%User{} = user) do
|
||||
following =
|
||||
__MODULE__
|
||||
|> join(:inner, [r], u in User, on: r.following_id == u.id)
|
||||
|> where([r], r.follower_id == ^user.id)
|
||||
|> where([r], r.state == ^:follow_accept)
|
||||
following_query(user)
|
||||
|> select([r, u], u.follower_address)
|
||||
|> Repo.all()
|
||||
|
||||
|
|
@ -171,6 +204,30 @@ defmodule Pleroma.FollowingRelationship do
|
|||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
For a query with joined activity,
|
||||
keeps rows where activity's actor is followed by user -or- is NOT domain-blocked by user.
|
||||
"""
|
||||
def keep_following_or_not_domain_blocked(query, user) do
|
||||
where(
|
||||
query,
|
||||
[_, activity],
|
||||
fragment(
|
||||
# "(actor's domain NOT in domain_blocks) OR (actor IS in followed AP IDs)"
|
||||
"""
|
||||
NOT (substring(? from '.*://([^/]*)') = ANY(?)) OR
|
||||
? = ANY(SELECT ap_id FROM users AS u INNER JOIN following_relationships AS fr
|
||||
ON u.id = fr.following_id WHERE fr.follower_id = ? AND fr.state = ?)
|
||||
""",
|
||||
activity.actor,
|
||||
^user.domain_blocks,
|
||||
activity.actor,
|
||||
^User.binary_id(user.id),
|
||||
^accept_state_code()
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
defp validate_not_self_relationship(%Changeset{} = changeset) do
|
||||
changeset
|
||||
|> validate_follower_id_following_id_inequality()
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ defmodule Pleroma.Formatter do
|
|||
def mention_handler("@" <> nickname, buffer, opts, acc) do
|
||||
case User.get_cached_by_nickname(nickname) do
|
||||
%User{id: id} = user ->
|
||||
ap_id = get_ap_id(user)
|
||||
user_url = user.uri || user.ap_id
|
||||
nickname_text = get_nickname_text(nickname, opts)
|
||||
|
||||
link =
|
||||
|
|
@ -42,7 +42,7 @@ defmodule Pleroma.Formatter do
|
|||
["@", Phoenix.HTML.Tag.content_tag(:span, nickname_text)],
|
||||
"data-user": id,
|
||||
class: "u-url mention",
|
||||
href: ap_id,
|
||||
href: user_url,
|
||||
rel: "ugc"
|
||||
),
|
||||
class: "h-card"
|
||||
|
|
@ -146,9 +146,6 @@ defmodule Pleroma.Formatter do
|
|||
end
|
||||
end
|
||||
|
||||
defp get_ap_id(%User{source_data: %{"url" => url}}) when is_binary(url), do: url
|
||||
defp get_ap_id(%User{ap_id: ap_id}), do: ap_id
|
||||
|
||||
defp get_nickname_text(nickname, %{mentions_format: :full}), do: User.full_nickname(nickname)
|
||||
defp get_nickname_text(nickname, _), do: User.local_nickname(nickname)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ defmodule Pleroma.Notification do
|
|||
|
||||
alias Ecto.Multi
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.FollowingRelationship
|
||||
alias Pleroma.Marker
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
|
|
@ -94,15 +95,13 @@ defmodule Pleroma.Notification do
|
|||
|> exclude_visibility(opts)
|
||||
end
|
||||
|
||||
# Excludes blocked users and non-followed domain-blocked users
|
||||
defp exclude_blocked(query, user, opts) do
|
||||
blocked_ap_ids = opts[:blocked_users_ap_ids] || User.blocked_users_ap_ids(user)
|
||||
|
||||
query
|
||||
|> where([n, a], a.actor not in ^blocked_ap_ids)
|
||||
|> where(
|
||||
[n, a],
|
||||
fragment("substring(? from '.*://([^/]*)')", a.actor) not in ^user.domain_blocks
|
||||
)
|
||||
|> FollowingRelationship.keep_following_or_not_domain_blocked(user)
|
||||
end
|
||||
|
||||
defp exclude_notification_muted(query, _, %{@include_muted_option => true}) do
|
||||
|
|
@ -280,6 +279,16 @@ defmodule Pleroma.Notification do
|
|||
|> Repo.delete_all()
|
||||
end
|
||||
|
||||
def dismiss(%Pleroma.Activity{} = activity) do
|
||||
Notification
|
||||
|> where([n], n.activity_id == ^activity.id)
|
||||
|> Repo.delete_all()
|
||||
|> case do
|
||||
{_, notifications} -> {:ok, notifications}
|
||||
_ -> {:error, "Cannot dismiss notification"}
|
||||
end
|
||||
end
|
||||
|
||||
def dismiss(%{id: user_id} = _user, id) do
|
||||
notification = Repo.get(Notification, id)
|
||||
|
||||
|
|
@ -302,8 +311,17 @@ defmodule Pleroma.Notification do
|
|||
end
|
||||
end
|
||||
|
||||
def create_notifications(%Activity{data: %{"type" => "Follow"}} = activity) do
|
||||
if Pleroma.Config.get([:notifications, :enable_follow_request_notifications]) ||
|
||||
Activity.follow_accepted?(activity) do
|
||||
do_create_notifications(activity)
|
||||
else
|
||||
{:ok, []}
|
||||
end
|
||||
end
|
||||
|
||||
def create_notifications(%Activity{data: %{"type" => type}} = activity)
|
||||
when type in ["Like", "Announce", "Follow", "Move", "EmojiReact"] do
|
||||
when type in ["Like", "Announce", "Move", "EmojiReact"] do
|
||||
do_create_notifications(activity)
|
||||
end
|
||||
|
||||
|
|
@ -342,10 +360,11 @@ defmodule Pleroma.Notification do
|
|||
|
||||
@doc """
|
||||
Returns a tuple with 2 elements:
|
||||
{enabled notification receivers, currently disabled receivers (blocking / [thread] muting)}
|
||||
{notification-enabled receivers, currently disabled receivers (blocking / [thread] muting)}
|
||||
|
||||
NOTE: might be called for FAKE Activities, see ActivityPub.Utils.get_notified_from_object/1
|
||||
"""
|
||||
@spec get_notified_from_activity(Activity.t(), boolean()) :: {list(User.t()), list(User.t())}
|
||||
def get_notified_from_activity(activity, local_only \\ true)
|
||||
|
||||
def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only)
|
||||
|
|
@ -358,17 +377,14 @@ defmodule Pleroma.Notification do
|
|||
|> Utils.maybe_notify_followers(activity)
|
||||
|> Enum.uniq()
|
||||
|
||||
# Since even subscribers and followers can mute / thread-mute, filtering all above AP IDs
|
||||
potential_receivers = User.get_users_from_set(potential_receiver_ap_ids, local_only)
|
||||
|
||||
notification_enabled_ap_ids =
|
||||
potential_receiver_ap_ids
|
||||
|> exclude_domain_blocker_ap_ids(activity, potential_receivers)
|
||||
|> exclude_relationship_restricted_ap_ids(activity)
|
||||
|> exclude_thread_muter_ap_ids(activity)
|
||||
|
||||
potential_receivers =
|
||||
potential_receiver_ap_ids
|
||||
|> Enum.uniq()
|
||||
|> User.get_users_from_set(local_only)
|
||||
|
||||
notification_enabled_users =
|
||||
Enum.filter(potential_receivers, fn u -> u.ap_id in notification_enabled_ap_ids end)
|
||||
|
||||
|
|
@ -377,6 +393,38 @@ defmodule Pleroma.Notification do
|
|||
|
||||
def get_notified_from_activity(_, _local_only), do: {[], []}
|
||||
|
||||
@doc "Filters out AP IDs domain-blocking and not following the activity's actor"
|
||||
def exclude_domain_blocker_ap_ids(ap_ids, activity, preloaded_users \\ [])
|
||||
|
||||
def exclude_domain_blocker_ap_ids([], _activity, _preloaded_users), do: []
|
||||
|
||||
def exclude_domain_blocker_ap_ids(ap_ids, %Activity{} = activity, preloaded_users) do
|
||||
activity_actor_domain = activity.actor && URI.parse(activity.actor).host
|
||||
|
||||
users =
|
||||
ap_ids
|
||||
|> Enum.map(fn ap_id ->
|
||||
Enum.find(preloaded_users, &(&1.ap_id == ap_id)) ||
|
||||
User.get_cached_by_ap_id(ap_id)
|
||||
end)
|
||||
|> Enum.filter(& &1)
|
||||
|
||||
domain_blocker_ap_ids = for u <- users, activity_actor_domain in u.domain_blocks, do: u.ap_id
|
||||
|
||||
domain_blocker_follower_ap_ids =
|
||||
if Enum.any?(domain_blocker_ap_ids) do
|
||||
activity
|
||||
|> Activity.user_actor()
|
||||
|> FollowingRelationship.followers_ap_ids(domain_blocker_ap_ids)
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
ap_ids
|
||||
|> Kernel.--(domain_blocker_ap_ids)
|
||||
|> Kernel.++(domain_blocker_follower_ap_ids)
|
||||
end
|
||||
|
||||
@doc "Filters out AP IDs of users basing on their relationships with activity actor user"
|
||||
def exclude_relationship_restricted_ap_ids([], _activity), do: []
|
||||
|
||||
|
|
|
|||
|
|
@ -261,7 +261,7 @@ defmodule Pleroma.Object do
|
|||
end
|
||||
end
|
||||
|
||||
def increase_vote_count(ap_id, name) do
|
||||
def increase_vote_count(ap_id, name, actor) do
|
||||
with %Object{} = object <- Object.normalize(ap_id),
|
||||
"Question" <- object.data["type"] do
|
||||
multiple = Map.has_key?(object.data, "anyOf")
|
||||
|
|
@ -276,12 +276,15 @@ defmodule Pleroma.Object do
|
|||
option
|
||||
end)
|
||||
|
||||
voters = [actor | object.data["voters"] || []] |> Enum.uniq()
|
||||
|
||||
data =
|
||||
if multiple do
|
||||
Map.put(object.data, "anyOf", options)
|
||||
else
|
||||
Map.put(object.data, "oneOf", options)
|
||||
end
|
||||
|> Map.put("voters", voters)
|
||||
|
||||
object
|
||||
|> Object.change(%{data: data})
|
||||
|
|
|
|||
|
|
@ -4,8 +4,11 @@
|
|||
|
||||
defmodule Pleroma.Plugs.AuthenticationPlug do
|
||||
alias Comeonin.Pbkdf2
|
||||
import Plug.Conn
|
||||
alias Pleroma.Plugs.OAuthScopesPlug
|
||||
alias Pleroma.User
|
||||
|
||||
import Plug.Conn
|
||||
|
||||
require Logger
|
||||
|
||||
def init(options), do: options
|
||||
|
|
@ -37,6 +40,7 @@ defmodule Pleroma.Plugs.AuthenticationPlug do
|
|||
if Pbkdf2.checkpw(password, password_hash) do
|
||||
conn
|
||||
|> assign(:user, auth_user)
|
||||
|> OAuthScopesPlug.skip_plug()
|
||||
else
|
||||
conn
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,17 +5,21 @@
|
|||
defmodule Pleroma.Plugs.EnsureAuthenticatedPlug do
|
||||
import Plug.Conn
|
||||
import Pleroma.Web.TranslationHelpers
|
||||
|
||||
alias Pleroma.User
|
||||
|
||||
use Pleroma.Web, :plug
|
||||
|
||||
def init(options) do
|
||||
options
|
||||
end
|
||||
|
||||
def call(%{assigns: %{user: %User{}}} = conn, _) do
|
||||
@impl true
|
||||
def perform(%{assigns: %{user: %User{}}} = conn, _) do
|
||||
conn
|
||||
end
|
||||
|
||||
def call(conn, options) do
|
||||
def perform(conn, options) do
|
||||
perform =
|
||||
cond do
|
||||
options[:if_func] -> options[:if_func].()
|
||||
|
|
|
|||
|
|
@ -5,14 +5,18 @@
|
|||
defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug do
|
||||
import Pleroma.Web.TranslationHelpers
|
||||
import Plug.Conn
|
||||
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.User
|
||||
|
||||
use Pleroma.Web, :plug
|
||||
|
||||
def init(options) do
|
||||
options
|
||||
end
|
||||
|
||||
def call(conn, _) do
|
||||
@impl true
|
||||
def perform(conn, _) do
|
||||
public? = Config.get!([:instance, :public])
|
||||
|
||||
case {public?, conn} do
|
||||
|
|
|
|||
20
lib/pleroma/plugs/expect_authenticated_check_plug.ex
Normal file
20
lib/pleroma/plugs/expect_authenticated_check_plug.ex
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Plugs.ExpectAuthenticatedCheckPlug do
|
||||
@moduledoc """
|
||||
Marks `Pleroma.Plugs.EnsureAuthenticatedPlug` as expected to be executed later in plug chain.
|
||||
|
||||
No-op plug which affects `Pleroma.Web` operation (is checked with `PlugHelper.plug_called?/2`).
|
||||
"""
|
||||
|
||||
use Pleroma.Web, :plug
|
||||
|
||||
def init(options), do: options
|
||||
|
||||
@impl true
|
||||
def perform(conn, _) do
|
||||
conn
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug do
|
||||
@moduledoc """
|
||||
Marks `Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug` as expected to be executed later in plug
|
||||
chain.
|
||||
|
||||
No-op plug which affects `Pleroma.Web` operation (is checked with `PlugHelper.plug_called?/2`).
|
||||
"""
|
||||
|
||||
use Pleroma.Web, :plug
|
||||
|
||||
def init(options), do: options
|
||||
|
||||
@impl true
|
||||
def perform(conn, _) do
|
||||
conn
|
||||
end
|
||||
end
|
||||
|
|
@ -75,7 +75,7 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do
|
|||
"default-src 'none'",
|
||||
"base-uri 'self'",
|
||||
"frame-ancestors 'none'",
|
||||
"img-src 'self' data: https:",
|
||||
"img-src 'self' data: blob: https:",
|
||||
"media-src 'self' https:",
|
||||
"style-src 'self' 'unsafe-inline'",
|
||||
"font-src 'self'",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
|
||||
defmodule Pleroma.Plugs.LegacyAuthenticationPlug do
|
||||
import Plug.Conn
|
||||
|
||||
alias Pleroma.Plugs.OAuthScopesPlug
|
||||
alias Pleroma.User
|
||||
|
||||
def init(options) do
|
||||
|
|
@ -27,6 +29,7 @@ defmodule Pleroma.Plugs.LegacyAuthenticationPlug do
|
|||
conn
|
||||
|> assign(:auth_user, user)
|
||||
|> assign(:user, user)
|
||||
|> OAuthScopesPlug.skip_plug()
|
||||
else
|
||||
_ ->
|
||||
conn
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do
|
|||
import Pleroma.Web.Gettext
|
||||
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
|
||||
|
||||
@behaviour Plug
|
||||
use Pleroma.Web, :plug
|
||||
|
||||
def init(%{scopes: _} = options), do: options
|
||||
|
||||
def call(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do
|
||||
@impl true
|
||||
def perform(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do
|
||||
op = options[:op] || :|
|
||||
token = assigns[:token]
|
||||
|
||||
|
|
@ -28,10 +28,7 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do
|
|||
conn
|
||||
|
||||
options[:fallback] == :proceed_unauthenticated ->
|
||||
conn
|
||||
|> assign(:user, nil)
|
||||
|> assign(:token, nil)
|
||||
|> maybe_perform_instance_privacy_check(options)
|
||||
drop_auth_info(conn)
|
||||
|
||||
true ->
|
||||
missing_scopes = scopes -- matched_scopes
|
||||
|
|
@ -47,6 +44,15 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do
|
|||
end
|
||||
end
|
||||
|
||||
@doc "Drops authentication info from connection"
|
||||
def drop_auth_info(conn) do
|
||||
# To simplify debugging, setting a private variable on `conn` if auth info is dropped
|
||||
conn
|
||||
|> put_private(:authentication_ignored, true)
|
||||
|> assign(:user, nil)
|
||||
|> assign(:token, nil)
|
||||
end
|
||||
|
||||
@doc "Filters descendants of supported scopes"
|
||||
def filter_descendants(scopes, supported_scopes) do
|
||||
Enum.filter(
|
||||
|
|
@ -68,12 +74,4 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do
|
|||
scopes
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_perform_instance_privacy_check(%Plug.Conn{} = conn, options) do
|
||||
if options[:skip_instance_privacy_check] do
|
||||
conn
|
||||
else
|
||||
EnsurePublicOrAuthenticatedPlug.call(conn, [])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
40
lib/pleroma/plugs/plug_helper.ex
Normal file
40
lib/pleroma/plugs/plug_helper.ex
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Plugs.PlugHelper do
|
||||
@moduledoc "Pleroma Plug helper"
|
||||
|
||||
@called_plugs_list_id :called_plugs
|
||||
def called_plugs_list_id, do: @called_plugs_list_id
|
||||
|
||||
@skipped_plugs_list_id :skipped_plugs
|
||||
def skipped_plugs_list_id, do: @skipped_plugs_list_id
|
||||
|
||||
@doc "Returns `true` if specified plug was called."
|
||||
def plug_called?(conn, plug_module) do
|
||||
contained_in_private_list?(conn, @called_plugs_list_id, plug_module)
|
||||
end
|
||||
|
||||
@doc "Returns `true` if specified plug was explicitly marked as skipped."
|
||||
def plug_skipped?(conn, plug_module) do
|
||||
contained_in_private_list?(conn, @skipped_plugs_list_id, plug_module)
|
||||
end
|
||||
|
||||
@doc "Returns `true` if specified plug was either called or explicitly marked as skipped."
|
||||
def plug_called_or_skipped?(conn, plug_module) do
|
||||
plug_called?(conn, plug_module) || plug_skipped?(conn, plug_module)
|
||||
end
|
||||
|
||||
# Appends plug to known list (skipped, called). Intended to be used from within plug code only.
|
||||
def append_to_private_list(conn, list_id, value) do
|
||||
list = conn.private[list_id] || []
|
||||
modified_list = Enum.uniq(list ++ [value])
|
||||
Plug.Conn.put_private(conn, list_id, modified_list)
|
||||
end
|
||||
|
||||
defp contained_in_private_list?(conn, private_variable, value) do
|
||||
list = conn.private[private_variable] || []
|
||||
value in list
|
||||
end
|
||||
end
|
||||
|
|
@ -45,11 +45,11 @@ defmodule Pleroma.Stats do
|
|||
end
|
||||
|
||||
def init(_args) do
|
||||
{:ok, get_stat_data()}
|
||||
{:ok, calculate_stat_data()}
|
||||
end
|
||||
|
||||
def handle_call(:force_update, _from, _state) do
|
||||
new_stats = get_stat_data()
|
||||
new_stats = calculate_stat_data()
|
||||
{:reply, new_stats, new_stats}
|
||||
end
|
||||
|
||||
|
|
@ -58,12 +58,12 @@ defmodule Pleroma.Stats do
|
|||
end
|
||||
|
||||
def handle_cast(:run_update, _state) do
|
||||
new_stats = get_stat_data()
|
||||
new_stats = calculate_stat_data()
|
||||
|
||||
{:noreply, new_stats}
|
||||
end
|
||||
|
||||
defp get_stat_data do
|
||||
def calculate_stat_data do
|
||||
peers =
|
||||
from(
|
||||
u in User,
|
||||
|
|
@ -77,7 +77,15 @@ defmodule Pleroma.Stats do
|
|||
|
||||
status_count = Repo.aggregate(User.Query.build(%{local: true}), :sum, :note_count)
|
||||
|
||||
user_count = Repo.aggregate(User.Query.build(%{local: true, active: true}), :count, :id)
|
||||
users_query =
|
||||
from(u in User,
|
||||
where: u.deactivated != true,
|
||||
where: u.local == true,
|
||||
where: not is_nil(u.nickname),
|
||||
where: not u.invisible
|
||||
)
|
||||
|
||||
user_count = Repo.aggregate(users_query, :count, :id)
|
||||
|
||||
%{
|
||||
peers: peers,
|
||||
|
|
|
|||
93
lib/pleroma/tests/auth_test_controller.ex
Normal file
93
lib/pleroma/tests/auth_test_controller.ex
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
# A test controller reachable only in :test env.
|
||||
defmodule Pleroma.Tests.AuthTestController do
|
||||
@moduledoc false
|
||||
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
|
||||
alias Pleroma.Plugs.OAuthScopesPlug
|
||||
alias Pleroma.User
|
||||
|
||||
# Serves only with proper OAuth token (:api and :authenticated_api)
|
||||
# Skipping EnsurePublicOrAuthenticatedPlug has no effect in this case
|
||||
#
|
||||
# Suggested use case: all :authenticated_api endpoints (makes no sense for :api endpoints)
|
||||
plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :do_oauth_check)
|
||||
|
||||
# Via :api, keeps :user if token has requested scopes (if :user is dropped, serves if public)
|
||||
# Via :authenticated_api, serves if token is present and has requested scopes
|
||||
#
|
||||
# Suggested use case: vast majority of :api endpoints (no sense for :authenticated_api ones)
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["read"], fallback: :proceed_unauthenticated}
|
||||
when action == :fallback_oauth_check
|
||||
)
|
||||
|
||||
# Keeps :user if present, executes regardless of token / token scopes
|
||||
# Fails with no :user for :authenticated_api / no user for :api on private instance
|
||||
# Note: EnsurePublicOrAuthenticatedPlug is not skipped (private instance fails on no :user)
|
||||
# Note: Basic Auth processing results in :skip_plug call for OAuthScopesPlug
|
||||
#
|
||||
# Suggested use: suppressing OAuth checks for other auth mechanisms (like Basic Auth)
|
||||
# For controller-level use, see :skip_oauth_skip_publicity_check instead
|
||||
plug(
|
||||
:skip_plug,
|
||||
OAuthScopesPlug when action == :skip_oauth_check
|
||||
)
|
||||
|
||||
# (Shouldn't be executed since the plug is skipped)
|
||||
plug(OAuthScopesPlug, %{scopes: ["admin"]} when action == :skip_oauth_check)
|
||||
|
||||
# Via :api, keeps :user if token has requested scopes, and continues with nil :user otherwise
|
||||
# Via :authenticated_api, serves if token is present and has requested scopes
|
||||
#
|
||||
# Suggested use: as :fallback_oauth_check but open with nil :user for :api on private instances
|
||||
plug(
|
||||
:skip_plug,
|
||||
EnsurePublicOrAuthenticatedPlug when action == :fallback_oauth_skip_publicity_check
|
||||
)
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["read"], fallback: :proceed_unauthenticated}
|
||||
when action == :fallback_oauth_skip_publicity_check
|
||||
)
|
||||
|
||||
# Via :api, keeps :user if present, serves regardless of token presence / scopes / :user presence
|
||||
# Via :authenticated_api, serves if :user is set (regardless of token presence and its scopes)
|
||||
#
|
||||
# Suggested use: making an :api endpoint always accessible (e.g. email confirmation endpoint)
|
||||
plug(
|
||||
:skip_plug,
|
||||
[OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug]
|
||||
when action == :skip_oauth_skip_publicity_check
|
||||
)
|
||||
|
||||
# Via :authenticated_api, always fails with 403 (endpoint is insecure)
|
||||
# Via :api, drops :user if present and serves if public (private instance rejects on no user)
|
||||
#
|
||||
# Suggested use: none; please define OAuth rules for all :api / :authenticated_api endpoints
|
||||
plug(:skip_plug, [] when action == :missing_oauth_check_definition)
|
||||
|
||||
def do_oauth_check(conn, _params), do: conn_state(conn)
|
||||
|
||||
def fallback_oauth_check(conn, _params), do: conn_state(conn)
|
||||
|
||||
def skip_oauth_check(conn, _params), do: conn_state(conn)
|
||||
|
||||
def fallback_oauth_skip_publicity_check(conn, _params), do: conn_state(conn)
|
||||
|
||||
def skip_oauth_skip_publicity_check(conn, _params), do: conn_state(conn)
|
||||
|
||||
def missing_oauth_check_definition(conn, _params), do: conn_state(conn)
|
||||
|
||||
defp conn_state(%{assigns: %{user: %User{} = user}} = conn),
|
||||
do: json(conn, %{user_id: user.id})
|
||||
|
||||
defp conn_state(conn), do: json(conn, %{user_id: nil})
|
||||
end
|
||||
|
|
@ -15,6 +15,7 @@ defmodule Pleroma.User do
|
|||
alias Pleroma.Config
|
||||
alias Pleroma.Conversation.Participation
|
||||
alias Pleroma.Delivery
|
||||
alias Pleroma.Emoji
|
||||
alias Pleroma.FollowingRelationship
|
||||
alias Pleroma.Formatter
|
||||
alias Pleroma.HTML
|
||||
|
|
@ -28,6 +29,7 @@ defmodule Pleroma.User do
|
|||
alias Pleroma.UserRelationship
|
||||
alias Pleroma.Web
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
|
||||
|
|
@ -82,6 +84,7 @@ defmodule Pleroma.User do
|
|||
field(:password, :string, virtual: true)
|
||||
field(:password_confirmation, :string, virtual: true)
|
||||
field(:keys, :string)
|
||||
field(:public_key, :string)
|
||||
field(:ap_id, :string)
|
||||
field(:avatar, :map)
|
||||
field(:local, :boolean, default: true)
|
||||
|
|
@ -94,7 +97,6 @@ defmodule Pleroma.User do
|
|||
field(:last_digest_emailed_at, :naive_datetime)
|
||||
field(:banner, :map, default: %{})
|
||||
field(:background, :map, default: %{})
|
||||
field(:source_data, :map, default: %{})
|
||||
field(:note_count, :integer, default: 0)
|
||||
field(:follower_count, :integer, default: 0)
|
||||
field(:following_count, :integer, default: 0)
|
||||
|
|
@ -112,7 +114,7 @@ defmodule Pleroma.User do
|
|||
field(:show_role, :boolean, default: true)
|
||||
field(:settings, :map, default: nil)
|
||||
field(:magic_key, :string, default: nil)
|
||||
field(:uri, :string, default: nil)
|
||||
field(:uri, Types.Uri, default: nil)
|
||||
field(:hide_followers_count, :boolean, default: false)
|
||||
field(:hide_follows_count, :boolean, default: false)
|
||||
field(:hide_followers, :boolean, default: false)
|
||||
|
|
@ -122,7 +124,7 @@ defmodule Pleroma.User do
|
|||
field(:pinned_activities, {:array, :string}, default: [])
|
||||
field(:email_notifications, :map, default: %{"digest" => false})
|
||||
field(:mascot, :map, default: nil)
|
||||
field(:emoji, {:array, :map}, default: [])
|
||||
field(:emoji, :map, default: %{})
|
||||
field(:pleroma_settings_store, :map, default: %{})
|
||||
field(:fields, {:array, :map}, default: [])
|
||||
field(:raw_fields, {:array, :map}, default: [])
|
||||
|
|
@ -132,6 +134,8 @@ defmodule Pleroma.User do
|
|||
field(:skip_thread_containment, :boolean, default: false)
|
||||
field(:actor_type, :string, default: "Person")
|
||||
field(:also_known_as, {:array, :string}, default: [])
|
||||
field(:inbox, :string)
|
||||
field(:shared_inbox, :string)
|
||||
|
||||
embeds_one(
|
||||
:notification_settings,
|
||||
|
|
@ -306,6 +310,7 @@ defmodule Pleroma.User do
|
|||
end
|
||||
end
|
||||
|
||||
# Should probably be renamed or removed
|
||||
def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"
|
||||
|
||||
def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
|
||||
|
|
@ -339,62 +344,72 @@ defmodule Pleroma.User do
|
|||
end
|
||||
end
|
||||
|
||||
def remote_user_creation(params) do
|
||||
defp fix_follower_address(%{follower_address: _, following_address: _} = params), do: params
|
||||
|
||||
defp fix_follower_address(%{nickname: nickname} = params),
|
||||
do: Map.put(params, :follower_address, ap_followers(%User{nickname: nickname}))
|
||||
|
||||
defp fix_follower_address(params), do: params
|
||||
|
||||
def remote_user_changeset(struct \\ %User{local: false}, params) do
|
||||
bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
|
||||
name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
|
||||
|
||||
name =
|
||||
case params[:name] do
|
||||
name when is_binary(name) and byte_size(name) > 0 -> name
|
||||
_ -> params[:nickname]
|
||||
end
|
||||
|
||||
params =
|
||||
params
|
||||
|> Map.put(:name, name)
|
||||
|> Map.put_new(:last_refreshed_at, NaiveDateTime.utc_now())
|
||||
|> truncate_if_exists(:name, name_limit)
|
||||
|> truncate_if_exists(:bio, bio_limit)
|
||||
|> truncate_fields_param()
|
||||
|> fix_follower_address()
|
||||
|
||||
changeset =
|
||||
%User{local: false}
|
||||
|> cast(
|
||||
params,
|
||||
[
|
||||
:bio,
|
||||
:name,
|
||||
:ap_id,
|
||||
:nickname,
|
||||
:avatar,
|
||||
:ap_enabled,
|
||||
:source_data,
|
||||
:banner,
|
||||
:locked,
|
||||
:magic_key,
|
||||
:uri,
|
||||
:hide_followers,
|
||||
:hide_follows,
|
||||
:hide_followers_count,
|
||||
:hide_follows_count,
|
||||
:follower_count,
|
||||
:fields,
|
||||
:following_count,
|
||||
:discoverable,
|
||||
:invisible,
|
||||
:actor_type,
|
||||
:also_known_as
|
||||
]
|
||||
)
|
||||
|> validate_required([:name, :ap_id])
|
||||
|> unique_constraint(:nickname)
|
||||
|> validate_format(:nickname, @email_regex)
|
||||
|> validate_length(:bio, max: bio_limit)
|
||||
|> validate_length(:name, max: name_limit)
|
||||
|> validate_fields(true)
|
||||
|
||||
case params[:source_data] do
|
||||
%{"followers" => followers, "following" => following} ->
|
||||
changeset
|
||||
|> put_change(:follower_address, followers)
|
||||
|> put_change(:following_address, following)
|
||||
|
||||
_ ->
|
||||
followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
|
||||
put_change(changeset, :follower_address, followers)
|
||||
end
|
||||
struct
|
||||
|> cast(
|
||||
params,
|
||||
[
|
||||
:bio,
|
||||
:name,
|
||||
:emoji,
|
||||
:ap_id,
|
||||
:inbox,
|
||||
:shared_inbox,
|
||||
:nickname,
|
||||
:public_key,
|
||||
:avatar,
|
||||
:ap_enabled,
|
||||
:banner,
|
||||
:locked,
|
||||
:last_refreshed_at,
|
||||
:magic_key,
|
||||
:uri,
|
||||
:follower_address,
|
||||
:following_address,
|
||||
:hide_followers,
|
||||
:hide_follows,
|
||||
:hide_followers_count,
|
||||
:hide_follows_count,
|
||||
:follower_count,
|
||||
:fields,
|
||||
:following_count,
|
||||
:discoverable,
|
||||
:invisible,
|
||||
:actor_type,
|
||||
:also_known_as
|
||||
]
|
||||
)
|
||||
|> validate_required([:name, :ap_id])
|
||||
|> unique_constraint(:nickname)
|
||||
|> validate_format(:nickname, @email_regex)
|
||||
|> validate_length(:bio, max: bio_limit)
|
||||
|> validate_length(:name, max: name_limit)
|
||||
|> validate_fields(true)
|
||||
end
|
||||
|
||||
def update_changeset(struct, params \\ %{}) do
|
||||
|
|
@ -407,7 +422,11 @@ defmodule Pleroma.User do
|
|||
[
|
||||
:bio,
|
||||
:name,
|
||||
:emoji,
|
||||
:avatar,
|
||||
:public_key,
|
||||
:inbox,
|
||||
:shared_inbox,
|
||||
:locked,
|
||||
:no_rich_text,
|
||||
:default_scope,
|
||||
|
|
@ -434,6 +453,7 @@ defmodule Pleroma.User do
|
|||
|> validate_length(:bio, max: bio_limit)
|
||||
|> validate_length(:name, min: 1, max: name_limit)
|
||||
|> put_fields()
|
||||
|> put_emoji()
|
||||
|> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)})
|
||||
|> put_change_if_present(:avatar, &put_upload(&1, :avatar))
|
||||
|> put_change_if_present(:banner, &put_upload(&1, :banner))
|
||||
|
|
@ -469,6 +489,18 @@ defmodule Pleroma.User do
|
|||
|> elem(0)
|
||||
end
|
||||
|
||||
defp put_emoji(changeset) do
|
||||
bio = get_change(changeset, :bio)
|
||||
name = get_change(changeset, :name)
|
||||
|
||||
if bio || name do
|
||||
emoji = Map.merge(Emoji.Formatter.get_emoji_map(bio), Emoji.Formatter.get_emoji_map(name))
|
||||
put_change(changeset, :emoji, emoji)
|
||||
else
|
||||
changeset
|
||||
end
|
||||
end
|
||||
|
||||
defp put_change_if_present(changeset, map_field, value_function) do
|
||||
if value = get_change(changeset, map_field) do
|
||||
with {:ok, new_value} <- value_function.(value) do
|
||||
|
|
@ -488,49 +520,6 @@ defmodule Pleroma.User do
|
|||
end
|
||||
end
|
||||
|
||||
def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
|
||||
bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
|
||||
name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
|
||||
|
||||
params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
|
||||
|
||||
params = if remote?, do: truncate_fields_param(params), else: params
|
||||
|
||||
struct
|
||||
|> cast(
|
||||
params,
|
||||
[
|
||||
:bio,
|
||||
:name,
|
||||
:follower_address,
|
||||
:following_address,
|
||||
:avatar,
|
||||
:last_refreshed_at,
|
||||
:ap_enabled,
|
||||
:source_data,
|
||||
:banner,
|
||||
:locked,
|
||||
:magic_key,
|
||||
:follower_count,
|
||||
:following_count,
|
||||
:hide_follows,
|
||||
:fields,
|
||||
:hide_followers,
|
||||
:allow_following_move,
|
||||
:discoverable,
|
||||
:hide_followers_count,
|
||||
:hide_follows_count,
|
||||
:actor_type,
|
||||
:also_known_as
|
||||
]
|
||||
)
|
||||
|> unique_constraint(:nickname)
|
||||
|> validate_format(:nickname, local_nickname_regex())
|
||||
|> validate_length(:bio, max: bio_limit)
|
||||
|> validate_length(:name, max: name_limit)
|
||||
|> validate_fields(remote?)
|
||||
end
|
||||
|
||||
def update_as_admin_changeset(struct, params) do
|
||||
struct
|
||||
|> update_changeset(params)
|
||||
|
|
@ -606,7 +595,7 @@ defmodule Pleroma.User do
|
|||
|
||||
struct
|
||||
|> confirmation_changeset(need_confirmation: need_confirmation?)
|
||||
|> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
|
||||
|> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation, :emoji])
|
||||
|> validate_required([:name, :nickname, :password, :password_confirmation])
|
||||
|> validate_confirmation(:password)
|
||||
|> unique_constraint(:email)
|
||||
|
|
@ -699,6 +688,8 @@ defmodule Pleroma.User do
|
|||
def needs_update?(_), do: true
|
||||
|
||||
@spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
|
||||
|
||||
# "Locked" (self-locked) users demand explicit authorization of follow requests
|
||||
def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do
|
||||
follow(follower, followed, :follow_pending)
|
||||
end
|
||||
|
|
@ -841,6 +832,7 @@ defmodule Pleroma.User do
|
|||
def set_cache(%User{} = user) do
|
||||
Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
|
||||
Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
|
||||
Cachex.put(:user_cache, "friends_ap_ids:#{user.nickname}", get_user_friends_ap_ids(user))
|
||||
{:ok, user}
|
||||
end
|
||||
|
||||
|
|
@ -856,9 +848,22 @@ defmodule Pleroma.User do
|
|||
end
|
||||
end
|
||||
|
||||
def get_user_friends_ap_ids(user) do
|
||||
from(u in User.get_friends_query(user), select: u.ap_id)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@spec get_cached_user_friends_ap_ids(User.t()) :: [String.t()]
|
||||
def get_cached_user_friends_ap_ids(user) do
|
||||
Cachex.fetch!(:user_cache, "friends_ap_ids:#{user.ap_id}", fn _ ->
|
||||
get_user_friends_ap_ids(user)
|
||||
end)
|
||||
end
|
||||
|
||||
def invalidate_cache(user) do
|
||||
Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
|
||||
Cachex.del(:user_cache, "nickname:#{user.nickname}")
|
||||
Cachex.del(:user_cache, "friends_ap_ids:#{user.ap_id}")
|
||||
end
|
||||
|
||||
@spec get_cached_by_ap_id(String.t()) :: User.t() | nil
|
||||
|
|
@ -1189,7 +1194,9 @@ defmodule Pleroma.User do
|
|||
end
|
||||
|
||||
@spec get_recipients_from_activity(Activity.t()) :: [User.t()]
|
||||
def get_recipients_from_activity(%Activity{recipients: to}) do
|
||||
def get_recipients_from_activity(%Activity{recipients: to, actor: actor}) do
|
||||
to = [actor | to]
|
||||
|
||||
User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
|
||||
|> Repo.all()
|
||||
end
|
||||
|
|
@ -1621,8 +1628,7 @@ defmodule Pleroma.User do
|
|||
|> set_cache()
|
||||
end
|
||||
|
||||
# AP style
|
||||
def public_key(%{source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}}) do
|
||||
def public_key(%{public_key: public_key_pem}) when is_binary(public_key_pem) do
|
||||
key =
|
||||
public_key_pem
|
||||
|> :public_key.pem_decode()
|
||||
|
|
@ -1632,7 +1638,7 @@ defmodule Pleroma.User do
|
|||
{:ok, key}
|
||||
end
|
||||
|
||||
def public_key(_), do: {:error, "not found key"}
|
||||
def public_key(_), do: {:error, "key not found"}
|
||||
|
||||
def get_public_key_for_ap_id(ap_id) do
|
||||
with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
|
||||
|
|
@ -1643,17 +1649,6 @@ defmodule Pleroma.User do
|
|||
end
|
||||
end
|
||||
|
||||
defp blank?(""), do: nil
|
||||
defp blank?(n), do: n
|
||||
|
||||
def insert_or_update_user(data) do
|
||||
data
|
||||
|> Map.put(:name, blank?(data[:name]) || data[:nickname])
|
||||
|> remote_user_creation()
|
||||
|> Repo.insert(on_conflict: {:replace_all_except, [:id]}, conflict_target: :nickname)
|
||||
|> set_cache()
|
||||
end
|
||||
|
||||
def ap_enabled?(%User{local: true}), do: true
|
||||
def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
|
||||
def ap_enabled?(_), do: false
|
||||
|
|
@ -1962,12 +1957,6 @@ defmodule Pleroma.User do
|
|||
|> update_and_set_cache()
|
||||
end
|
||||
|
||||
def update_source_data(user, source_data) do
|
||||
user
|
||||
|> cast(%{source_data: source_data}, [:source_data])
|
||||
|> update_and_set_cache()
|
||||
end
|
||||
|
||||
def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
|
||||
%{
|
||||
admin: is_admin,
|
||||
|
|
@ -1975,21 +1964,6 @@ defmodule Pleroma.User do
|
|||
}
|
||||
end
|
||||
|
||||
# ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.
|
||||
# For example: [{"name": "Pronoun", "value": "she/her"}, …]
|
||||
def fields(%{fields: nil, source_data: %{"attachment" => attachment}}) do
|
||||
limit = Pleroma.Config.get([:instance, :max_remote_account_fields], 0)
|
||||
|
||||
attachment
|
||||
|> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
|
||||
|> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
|
||||
|> Enum.take(limit)
|
||||
end
|
||||
|
||||
def fields(%{fields: nil}), do: []
|
||||
|
||||
def fields(%{fields: fields}), do: fields
|
||||
|
||||
def validate_fields(changeset, remote? \\ false) do
|
||||
limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
|
||||
limit = Pleroma.Config.get([:instance, limit_name], 0)
|
||||
|
|
@ -2177,9 +2151,7 @@ defmodule Pleroma.User do
|
|||
# - display name
|
||||
def sanitize_html(%User{} = user, filter) do
|
||||
fields =
|
||||
user
|
||||
|> User.fields()
|
||||
|> Enum.map(fn %{"name" => name, "value" => value} ->
|
||||
Enum.map(user.fields, fn %{"name" => name, "value" => value} ->
|
||||
%{
|
||||
"name" => name,
|
||||
"value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
|
||||
|
|
|
|||
|
|
@ -54,13 +54,13 @@ defmodule Pleroma.User.Query do
|
|||
select: term(),
|
||||
limit: pos_integer()
|
||||
}
|
||||
| %{}
|
||||
| map()
|
||||
|
||||
@ilike_criteria [:nickname, :name, :query]
|
||||
@equal_criteria [:email]
|
||||
@contains_criteria [:ap_id, :nickname]
|
||||
|
||||
@spec build(criteria()) :: Query.t()
|
||||
@spec build(Query.t(), criteria()) :: Query.t()
|
||||
def build(query \\ base_query(), criteria) do
|
||||
prepare_query(query, criteria)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -118,9 +118,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|
||||
def increase_poll_votes_if_vote(%{
|
||||
"object" => %{"inReplyTo" => reply_ap_id, "name" => name},
|
||||
"type" => "Create"
|
||||
"type" => "Create",
|
||||
"actor" => actor
|
||||
}) do
|
||||
Object.increase_vote_count(reply_ap_id, name)
|
||||
Object.increase_vote_count(reply_ap_id, name, actor)
|
||||
end
|
||||
|
||||
def increase_poll_votes_if_vote(_create_data), do: :noop
|
||||
|
|
@ -397,36 +398,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
end
|
||||
end
|
||||
|
||||
# TODO: This is weird, maybe we shouldn't check here if we can make the activity.
|
||||
@spec like(User.t(), Object.t(), String.t() | nil, boolean()) ::
|
||||
{:ok, Activity.t(), Object.t()} | {:error, any()}
|
||||
def like(user, object, activity_id \\ nil, local \\ true) do
|
||||
with {:ok, result} <- Repo.transaction(fn -> do_like(user, object, activity_id, local) end) do
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
defp do_like(
|
||||
%User{ap_id: ap_id} = user,
|
||||
%Object{data: %{"id" => _}} = object,
|
||||
activity_id,
|
||||
local
|
||||
) do
|
||||
with nil <- get_existing_like(ap_id, object),
|
||||
like_data <- make_like_data(user, object, activity_id),
|
||||
{:ok, activity} <- insert(like_data, local),
|
||||
{:ok, object} <- add_like_to_object(activity, object),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity, object}
|
||||
else
|
||||
%Activity{} = activity ->
|
||||
{:ok, activity, object}
|
||||
|
||||
{:error, error} ->
|
||||
Repo.rollback(error)
|
||||
end
|
||||
end
|
||||
|
||||
@spec unlike(User.t(), Object.t(), String.t() | nil, boolean()) ::
|
||||
{:ok, Activity.t(), Activity.t(), Object.t()} | {:ok, Object.t()} | {:error, any()}
|
||||
def unlike(%User{} = actor, %Object{} = object, activity_id \\ nil, local \\ true) do
|
||||
|
|
@ -467,6 +438,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|
||||
defp do_announce(user, object, activity_id, local, public) do
|
||||
with true <- is_announceable?(object, user, public),
|
||||
object <- Object.get_by_id(object.id),
|
||||
announce_data <- make_announce_data(user, object, activity_id, public),
|
||||
{:ok, activity} <- insert(announce_data, local),
|
||||
{:ok, object} <- add_announce_to_object(activity, object),
|
||||
|
|
@ -853,7 +825,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
end
|
||||
|
||||
defp exclude_visibility(query, %{"exclude_visibilities" => visibility})
|
||||
when visibility not in @valid_visibilities do
|
||||
when visibility not in [nil | @valid_visibilities] do
|
||||
Logger.error("Could not exclude visibility to #{visibility}")
|
||||
query
|
||||
end
|
||||
|
|
@ -1060,7 +1032,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
raise "Can't use the child object without preloading!"
|
||||
end
|
||||
|
||||
defp restrict_media(query, %{"only_media" => val}) when val == "true" or val == "1" do
|
||||
defp restrict_media(query, %{"only_media" => val}) when val in [true, "true", "1"] do
|
||||
from(
|
||||
[_activity, object] in query,
|
||||
where: fragment("not (?)->'attachment' = (?)", object.data, ^[])
|
||||
|
|
@ -1069,16 +1041,51 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|
||||
defp restrict_media(query, _), do: query
|
||||
|
||||
defp restrict_replies(query, %{"exclude_replies" => val}) when val == "true" or val == "1" do
|
||||
defp restrict_replies(query, %{"exclude_replies" => val}) when val in [true, "true", "1"] do
|
||||
from(
|
||||
[_activity, object] in query,
|
||||
where: fragment("?->>'inReplyTo' is null", object.data)
|
||||
)
|
||||
end
|
||||
|
||||
defp restrict_replies(query, %{
|
||||
"reply_filtering_user" => user,
|
||||
"reply_visibility" => "self"
|
||||
}) do
|
||||
from(
|
||||
[activity, object] in query,
|
||||
where:
|
||||
fragment(
|
||||
"?->>'inReplyTo' is null OR ? = ANY(?)",
|
||||
object.data,
|
||||
^user.ap_id,
|
||||
activity.recipients
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
defp restrict_replies(query, %{
|
||||
"reply_filtering_user" => user,
|
||||
"reply_visibility" => "following"
|
||||
}) do
|
||||
from(
|
||||
[activity, object] in query,
|
||||
where:
|
||||
fragment(
|
||||
"?->>'inReplyTo' is null OR ? && array_remove(?, ?) OR ? = ?",
|
||||
object.data,
|
||||
^[user.ap_id | User.get_cached_user_friends_ap_ids(user)],
|
||||
activity.recipients,
|
||||
activity.actor,
|
||||
activity.actor,
|
||||
^user.ap_id
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
defp restrict_replies(query, _), do: query
|
||||
|
||||
defp restrict_reblogs(query, %{"exclude_reblogs" => val}) when val == "true" or val == "1" do
|
||||
defp restrict_reblogs(query, %{"exclude_reblogs" => val}) when val in [true, "true", "1"] do
|
||||
from(activity in query, where: fragment("?->>'type' != 'Announce'", activity.data))
|
||||
end
|
||||
|
||||
|
|
@ -1157,7 +1164,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
)
|
||||
end
|
||||
|
||||
defp restrict_pinned(query, %{"pinned" => "true", "pinned_activity_ids" => ids}) do
|
||||
# TODO: when all endpoints migrated to OpenAPI compare `pinned` with `true` (boolean) only,
|
||||
# the same for `restrict_media/2`, `restrict_replies/2`, 'restrict_reblogs/2'
|
||||
# and `restrict_muted/2`
|
||||
|
||||
defp restrict_pinned(query, %{"pinned" => pinned, "pinned_activity_ids" => ids})
|
||||
when pinned in [true, "true", "1"] do
|
||||
from(activity in query, where: activity.id in ^ids)
|
||||
end
|
||||
|
||||
|
|
@ -1290,6 +1302,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|> maybe_set_thread_muted_field(opts)
|
||||
|> maybe_order(opts)
|
||||
|> restrict_recipients(recipients, opts["user"])
|
||||
|> restrict_replies(opts)
|
||||
|> restrict_tag(opts)
|
||||
|> restrict_tag_reject(opts)
|
||||
|> restrict_tag_all(opts)
|
||||
|
|
@ -1304,7 +1317,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|> restrict_media(opts)
|
||||
|> restrict_visibility(opts)
|
||||
|> restrict_thread_visibility(opts, config)
|
||||
|> restrict_replies(opts)
|
||||
|> restrict_reblogs(opts)
|
||||
|> restrict_pinned(opts)
|
||||
|> restrict_muted_reblogs(restrict_muted_reblogs_opts)
|
||||
|
|
@ -1427,19 +1439,44 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
|
||||
|> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
|
||||
|
||||
emojis =
|
||||
data
|
||||
|> Map.get("tag", [])
|
||||
|> Enum.filter(fn
|
||||
%{"type" => "Emoji"} -> true
|
||||
_ -> false
|
||||
end)
|
||||
|> Enum.reduce(%{}, fn %{"icon" => %{"url" => url}, "name" => name}, acc ->
|
||||
Map.put(acc, String.trim(name, ":"), url)
|
||||
end)
|
||||
|
||||
locked = data["manuallyApprovesFollowers"] || false
|
||||
data = Transmogrifier.maybe_fix_user_object(data)
|
||||
discoverable = data["discoverable"] || false
|
||||
invisible = data["invisible"] || false
|
||||
actor_type = data["type"] || "Person"
|
||||
|
||||
public_key =
|
||||
if is_map(data["publicKey"]) && is_binary(data["publicKey"]["publicKeyPem"]) do
|
||||
data["publicKey"]["publicKeyPem"]
|
||||
else
|
||||
nil
|
||||
end
|
||||
|
||||
shared_inbox =
|
||||
if is_map(data["endpoints"]) && is_binary(data["endpoints"]["sharedInbox"]) do
|
||||
data["endpoints"]["sharedInbox"]
|
||||
else
|
||||
nil
|
||||
end
|
||||
|
||||
user_data = %{
|
||||
ap_id: data["id"],
|
||||
uri: get_actor_url(data["url"]),
|
||||
ap_enabled: true,
|
||||
source_data: data,
|
||||
banner: banner,
|
||||
fields: fields,
|
||||
emoji: emojis,
|
||||
locked: locked,
|
||||
discoverable: discoverable,
|
||||
invisible: invisible,
|
||||
|
|
@ -1449,7 +1486,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
following_address: data["following"],
|
||||
bio: data["summary"],
|
||||
actor_type: actor_type,
|
||||
also_known_as: Map.get(data, "alsoKnownAs", [])
|
||||
also_known_as: Map.get(data, "alsoKnownAs", []),
|
||||
public_key: public_key,
|
||||
inbox: data["inbox"],
|
||||
shared_inbox: shared_inbox
|
||||
}
|
||||
|
||||
# nickname can be nil because of virtual actors
|
||||
|
|
@ -1551,11 +1591,22 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
end
|
||||
|
||||
def make_user_from_ap_id(ap_id) do
|
||||
if _user = User.get_cached_by_ap_id(ap_id) do
|
||||
user = User.get_cached_by_ap_id(ap_id)
|
||||
|
||||
if user && !User.ap_enabled?(user) do
|
||||
Transmogrifier.upgrade_user_from_ap_id(ap_id)
|
||||
else
|
||||
with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id) do
|
||||
User.insert_or_update_user(data)
|
||||
if user do
|
||||
user
|
||||
|> User.remote_user_changeset(data)
|
||||
|> User.update_and_set_cache()
|
||||
else
|
||||
data
|
||||
|> User.remote_user_changeset()
|
||||
|> Repo.insert()
|
||||
|> User.set_cache()
|
||||
end
|
||||
else
|
||||
e -> {:error, e}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,8 +12,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
alias Pleroma.Plugs.EnsureAuthenticatedPlug
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Builder
|
||||
alias Pleroma.Web.ActivityPub.InternalFetchActor
|
||||
alias Pleroma.Web.ActivityPub.ObjectView
|
||||
alias Pleroma.Web.ActivityPub.Pipeline
|
||||
alias Pleroma.Web.ActivityPub.Relay
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
alias Pleroma.Web.ActivityPub.UserView
|
||||
|
|
@ -421,7 +423,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
|
||||
defp handle_user_activity(%User{} = user, %{"type" => "Like"} = params) do
|
||||
with %Object{} = object <- Object.normalize(params["object"]),
|
||||
{:ok, activity, _object} <- ActivityPub.like(user, object) do
|
||||
{_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
|
||||
{_, {:ok, %Activity{} = activity, _meta}} <-
|
||||
{:common_pipeline,
|
||||
Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
|
||||
{:ok, activity}
|
||||
else
|
||||
_ -> {:error, dgettext("errors", "Can't like object")}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
|
|||
@moduledoc "Filter activities depending on their age"
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF
|
||||
|
||||
defp check_date(%{"published" => published} = message) do
|
||||
defp check_date(%{"object" => %{"published" => published}} = message) do
|
||||
with %DateTime{} = now <- DateTime.utc_now(),
|
||||
{:ok, %DateTime{} = then, _} <- DateTime.from_iso8601(published),
|
||||
max_ttl <- Config.get([:mrf_object_age, :threshold]),
|
||||
|
|
@ -96,5 +96,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
|
|||
def filter(message), do: {:ok, message}
|
||||
|
||||
@impl true
|
||||
def describe, do: {:ok, %{}}
|
||||
def describe do
|
||||
mrf_object_age =
|
||||
Pleroma.Config.get(:mrf_object_age)
|
||||
|> Enum.into(%{})
|
||||
|
||||
{:ok, %{mrf_object_age: mrf_object_age}}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -148,6 +148,21 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|
|||
|
||||
defp check_banner_removal(_actor_info, object), do: {:ok, object}
|
||||
|
||||
@impl true
|
||||
def filter(%{"type" => "Delete", "actor" => actor} = object) do
|
||||
%{host: actor_host} = URI.parse(actor)
|
||||
|
||||
reject_deletes =
|
||||
Pleroma.Config.get([:mrf_simple, :reject_deletes])
|
||||
|> MRF.subdomains_regex()
|
||||
|
||||
if MRF.subdomain_match?(reject_deletes, actor_host) do
|
||||
{:reject, nil}
|
||||
else
|
||||
{:ok, object}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def filter(%{"actor" => actor} = object) do
|
||||
actor_info = URI.parse(actor)
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator do
|
|||
field(:like_count, :integer, default: 0)
|
||||
field(:announcement_count, :integer, default: 0)
|
||||
field(:inRepyTo, :string)
|
||||
field(:uri, Types.Uri)
|
||||
|
||||
field(:likes, {:array, :string}, default: [])
|
||||
field(:announcements, {:array, :string}, default: [])
|
||||
|
|
|
|||
|
|
@ -15,15 +15,9 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.ObjectID do
|
|||
|
||||
def cast(%{"id" => object}), do: cast(object)
|
||||
|
||||
def cast(_) do
|
||||
:error
|
||||
end
|
||||
def cast(_), do: :error
|
||||
|
||||
def dump(data) do
|
||||
{:ok, data}
|
||||
end
|
||||
def dump(data), do: {:ok, data}
|
||||
|
||||
def load(data) do
|
||||
{:ok, data}
|
||||
end
|
||||
def load(data), do: {:ok, data}
|
||||
end
|
||||
|
|
|
|||
20
lib/pleroma/web/activity_pub/object_validators/types/uri.ex
Normal file
20
lib/pleroma/web/activity_pub/object_validators/types/uri.ex
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.Uri do
|
||||
use Ecto.Type
|
||||
|
||||
def type, do: :string
|
||||
|
||||
def cast(uri) when is_binary(uri) do
|
||||
case URI.parse(uri) do
|
||||
%URI{host: nil} -> :error
|
||||
%URI{host: ""} -> :error
|
||||
%URI{scheme: scheme} when scheme in ["https", "http"] -> {:ok, uri}
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
def cast(_), do: :error
|
||||
|
||||
def dump(data), do: {:ok, data}
|
||||
|
||||
def load(data), do: {:ok, data}
|
||||
end
|
||||
|
|
@ -141,8 +141,8 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|
|||
|> Enum.map(& &1.ap_id)
|
||||
end
|
||||
|
||||
defp maybe_use_sharedinbox(%User{source_data: data}),
|
||||
do: (is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]
|
||||
defp maybe_use_sharedinbox(%User{shared_inbox: nil, inbox: inbox}), do: inbox
|
||||
defp maybe_use_sharedinbox(%User{shared_inbox: shared_inbox}), do: shared_inbox
|
||||
|
||||
@doc """
|
||||
Determine a user inbox to use based on heuristics. These heuristics
|
||||
|
|
@ -157,7 +157,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|
|||
"""
|
||||
def determine_inbox(
|
||||
%Activity{data: activity_data},
|
||||
%User{source_data: data} = user
|
||||
%User{inbox: inbox} = user
|
||||
) do
|
||||
to = activity_data["to"] || []
|
||||
cc = activity_data["cc"] || []
|
||||
|
|
@ -174,7 +174,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|
|||
maybe_use_sharedinbox(user)
|
||||
|
||||
true ->
|
||||
data["inbox"]
|
||||
inbox
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -192,14 +192,13 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|
|||
inboxes =
|
||||
recipients
|
||||
|> Enum.filter(&User.ap_enabled?/1)
|
||||
|> Enum.map(fn %{source_data: data} -> data["inbox"] end)
|
||||
|> Enum.map(fn actor -> actor.inbox end)
|
||||
|> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
|
||||
|> Instances.filter_reachable()
|
||||
|
||||
Repo.checkout(fn ->
|
||||
Enum.each(inboxes, fn {inbox, unreachable_since} ->
|
||||
%User{ap_id: ap_id} =
|
||||
Enum.find(recipients, fn %{source_data: data} -> data["inbox"] == inbox end)
|
||||
%User{ap_id: ap_id} = Enum.find(recipients, fn actor -> actor.inbox == inbox end)
|
||||
|
||||
# Get all the recipients on the same host and add them to cc. Otherwise, a remote
|
||||
# instance would only accept a first message for the first recipient and ignore the rest.
|
||||
|
|
|
|||
|
|
@ -15,10 +15,17 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
# - Add like to object
|
||||
# - Set up notification
|
||||
def handle(%{data: %{"type" => "Like"}} = object, meta) do
|
||||
liked_object = Object.get_by_ap_id(object.data["object"])
|
||||
Utils.add_like_to_object(object, liked_object)
|
||||
Notification.create_notifications(object)
|
||||
{:ok, object, meta}
|
||||
{:ok, result} =
|
||||
Pleroma.Repo.transaction(fn ->
|
||||
liked_object = Object.get_by_ap_id(object.data["object"])
|
||||
Utils.add_like_to_object(object, liked_object)
|
||||
|
||||
Notification.create_notifications(object)
|
||||
|
||||
{:ok, object, meta}
|
||||
end)
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
# Nothing to do
|
||||
|
|
|
|||
|
|
@ -711,7 +711,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
{:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
|
||||
|
||||
actor
|
||||
|> User.upgrade_changeset(new_user_data, true)
|
||||
|> User.remote_user_changeset(new_user_data)
|
||||
|> User.update_and_set_cache()
|
||||
|
||||
ActivityPub.update(%{
|
||||
|
|
@ -1160,7 +1160,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|
||||
def take_emoji_tags(%User{emoji: emoji}) do
|
||||
emoji
|
||||
|> Enum.flat_map(&Map.to_list/1)
|
||||
|> Map.to_list()
|
||||
|> Enum.map(&build_emoji_tag/1)
|
||||
end
|
||||
|
||||
|
|
@ -1254,12 +1254,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
def upgrade_user_from_ap_id(ap_id) do
|
||||
with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
|
||||
{:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
|
||||
already_ap <- User.ap_enabled?(user),
|
||||
{:ok, user} <- upgrade_user(user, data) do
|
||||
if not already_ap do
|
||||
TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
|
||||
end
|
||||
|
||||
{:ok, user} <- update_user(user, data) do
|
||||
TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
|
||||
{:ok, user}
|
||||
else
|
||||
%User{} = user -> {:ok, user}
|
||||
|
|
@ -1267,9 +1263,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
end
|
||||
end
|
||||
|
||||
defp upgrade_user(user, data) do
|
||||
defp update_user(user, data) do
|
||||
user
|
||||
|> User.upgrade_changeset(data, true)
|
||||
|> User.remote_user_changeset(data)
|
||||
|> User.update_and_set_cache()
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -79,10 +79,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
|
||||
emoji_tags = Transmogrifier.take_emoji_tags(user)
|
||||
|
||||
fields =
|
||||
user
|
||||
|> User.fields()
|
||||
|> Enum.map(&Map.put(&1, "type", "PropertyValue"))
|
||||
fields = Enum.map(user.fields, &Map.put(&1, "type", "PropertyValue"))
|
||||
|
||||
%{
|
||||
"id" => user.ap_id,
|
||||
|
|
@ -103,7 +100,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
},
|
||||
"endpoints" => endpoints,
|
||||
"attachment" => fields,
|
||||
"tag" => (user.source_data["tag"] || []) ++ emoji_tags,
|
||||
"tag" => emoji_tags,
|
||||
"discoverable" => user.discoverable
|
||||
}
|
||||
|> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))
|
||||
|
|
|
|||
|
|
@ -27,7 +27,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
alias Pleroma.Web.AdminAPI.Search
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.Endpoint
|
||||
alias Pleroma.Web.MastodonAPI.AppView
|
||||
alias Pleroma.Web.MastodonAPI.StatusView
|
||||
alias Pleroma.Web.OAuth.App
|
||||
alias Pleroma.Web.Router
|
||||
|
||||
require Logger
|
||||
|
|
@ -46,6 +48,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
%{scopes: ["write:accounts"], admin: true}
|
||||
when action in [
|
||||
:get_password_reset,
|
||||
:force_password_reset,
|
||||
:user_delete,
|
||||
:users_create,
|
||||
:user_toggle_activation,
|
||||
|
|
@ -54,7 +57,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
:tag_users,
|
||||
:untag_users,
|
||||
:right_add,
|
||||
:right_add_multiple,
|
||||
:right_delete,
|
||||
:right_delete_multiple,
|
||||
:update_user_credentials
|
||||
]
|
||||
)
|
||||
|
|
@ -82,13 +87,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["write:reports"], admin: true}
|
||||
when action in [:reports_update]
|
||||
when action in [:reports_update, :report_notes_create, :report_notes_delete]
|
||||
)
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["read:statuses"], admin: true}
|
||||
when action == :list_user_statuses
|
||||
when action in [:list_statuses, :list_user_statuses, :list_instance_statuses]
|
||||
)
|
||||
|
||||
plug(
|
||||
|
|
@ -100,13 +105,30 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["read"], admin: true}
|
||||
when action in [:config_show, :list_log, :stats]
|
||||
when action in [
|
||||
:config_show,
|
||||
:list_log,
|
||||
:stats,
|
||||
:relay_list,
|
||||
:config_descriptions,
|
||||
:need_reboot
|
||||
]
|
||||
)
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["write"], admin: true}
|
||||
when action == :config_update
|
||||
when action in [
|
||||
:restart,
|
||||
:config_update,
|
||||
:resend_confirmation_email,
|
||||
:confirm_email,
|
||||
:oauth_app_create,
|
||||
:oauth_app_list,
|
||||
:oauth_app_update,
|
||||
:oauth_app_delete,
|
||||
:reload_emoji
|
||||
]
|
||||
)
|
||||
|
||||
action_fallback(:errors)
|
||||
|
|
@ -914,16 +936,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
end)
|
||||
|> List.flatten()
|
||||
|
||||
response = %{configs: merged}
|
||||
|
||||
response =
|
||||
if Restarter.Pleroma.need_reboot?() do
|
||||
Map.put(response, :need_reboot, true)
|
||||
else
|
||||
response
|
||||
end
|
||||
|
||||
json(conn, response)
|
||||
json(conn, %{configs: merged, need_reboot: Restarter.Pleroma.need_reboot?()})
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -950,28 +963,22 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
|
||||
Config.TransferTask.load_and_update_env(deleted, false)
|
||||
|
||||
need_reboot? =
|
||||
Restarter.Pleroma.need_reboot?() ||
|
||||
Enum.any?(updated, fn config ->
|
||||
if !Restarter.Pleroma.need_reboot?() do
|
||||
changed_reboot_settings? =
|
||||
(updated ++ deleted)
|
||||
|> Enum.any?(fn config ->
|
||||
group = ConfigDB.from_string(config.group)
|
||||
key = ConfigDB.from_string(config.key)
|
||||
value = ConfigDB.from_binary(config.value)
|
||||
Config.TransferTask.pleroma_need_restart?(group, key, value)
|
||||
end)
|
||||
|
||||
response = %{configs: updated}
|
||||
|
||||
response =
|
||||
if need_reboot? do
|
||||
Restarter.Pleroma.need_reboot()
|
||||
Map.put(response, :need_reboot, need_reboot?)
|
||||
else
|
||||
response
|
||||
end
|
||||
if changed_reboot_settings?, do: Restarter.Pleroma.need_reboot()
|
||||
end
|
||||
|
||||
conn
|
||||
|> put_view(ConfigView)
|
||||
|> render("index.json", response)
|
||||
|> render("index.json", %{configs: updated, need_reboot: Restarter.Pleroma.need_reboot?()})
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -983,6 +990,10 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
end
|
||||
end
|
||||
|
||||
def need_reboot(conn, _params) do
|
||||
json(conn, %{need_reboot: Restarter.Pleroma.need_reboot?()})
|
||||
end
|
||||
|
||||
defp configurable_from_database(conn) do
|
||||
if Config.get(:configurable_from_database) do
|
||||
:ok
|
||||
|
|
@ -1028,6 +1039,83 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
conn |> json("")
|
||||
end
|
||||
|
||||
def oauth_app_create(conn, params) do
|
||||
params =
|
||||
if params["name"] do
|
||||
Map.put(params, "client_name", params["name"])
|
||||
else
|
||||
params
|
||||
end
|
||||
|
||||
result =
|
||||
case App.create(params) do
|
||||
{:ok, app} ->
|
||||
AppView.render("show.json", %{app: app, admin: true})
|
||||
|
||||
{:error, changeset} ->
|
||||
App.errors(changeset)
|
||||
end
|
||||
|
||||
json(conn, result)
|
||||
end
|
||||
|
||||
def oauth_app_update(conn, params) do
|
||||
params =
|
||||
if params["name"] do
|
||||
Map.put(params, "client_name", params["name"])
|
||||
else
|
||||
params
|
||||
end
|
||||
|
||||
with {:ok, app} <- App.update(params) do
|
||||
json(conn, AppView.render("show.json", %{app: app, admin: true}))
|
||||
else
|
||||
{:error, changeset} ->
|
||||
json(conn, App.errors(changeset))
|
||||
|
||||
nil ->
|
||||
json_response(conn, :bad_request, "")
|
||||
end
|
||||
end
|
||||
|
||||
def oauth_app_list(conn, params) do
|
||||
{page, page_size} = page_params(params)
|
||||
|
||||
search_params = %{
|
||||
client_name: params["name"],
|
||||
client_id: params["client_id"],
|
||||
page: page,
|
||||
page_size: page_size
|
||||
}
|
||||
|
||||
search_params =
|
||||
if Map.has_key?(params, "trusted") do
|
||||
Map.put(search_params, :trusted, params["trusted"])
|
||||
else
|
||||
search_params
|
||||
end
|
||||
|
||||
with {:ok, apps, count} <- App.search(search_params) do
|
||||
json(
|
||||
conn,
|
||||
AppView.render("index.json",
|
||||
apps: apps,
|
||||
count: count,
|
||||
page_size: page_size,
|
||||
admin: true
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def oauth_app_delete(conn, params) do
|
||||
with {:ok, _app} <- App.destroy(params["id"]) do
|
||||
json_response(conn, :no_content, "")
|
||||
else
|
||||
_ -> json_response(conn, :bad_request, "")
|
||||
end
|
||||
end
|
||||
|
||||
def stats(conn, _) do
|
||||
count = Stats.get_status_visibility_count()
|
||||
|
||||
|
|
@ -1035,25 +1123,25 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
|> json(%{"status_visibility" => count})
|
||||
end
|
||||
|
||||
def errors(conn, {:error, :not_found}) do
|
||||
defp errors(conn, {:error, :not_found}) do
|
||||
conn
|
||||
|> put_status(:not_found)
|
||||
|> json(dgettext("errors", "Not found"))
|
||||
end
|
||||
|
||||
def errors(conn, {:error, reason}) do
|
||||
defp errors(conn, {:error, reason}) do
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(reason)
|
||||
end
|
||||
|
||||
def errors(conn, {:param_cast, _}) do
|
||||
defp errors(conn, {:param_cast, _}) do
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(dgettext("errors", "Invalid parameters"))
|
||||
end
|
||||
|
||||
def errors(conn, _) do
|
||||
defp errors(conn, _) do
|
||||
conn
|
||||
|> put_status(:internal_server_error)
|
||||
|> json(dgettext("errors", "Something went wrong"))
|
||||
|
|
|
|||
|
|
@ -8,15 +8,16 @@ defmodule Pleroma.Web.AdminAPI.StatusView do
|
|||
require Pleroma.Constants
|
||||
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.MastodonAPI.StatusView
|
||||
|
||||
def render("index.json", opts) do
|
||||
safe_render_many(opts.activities, __MODULE__, "show.json", opts)
|
||||
end
|
||||
|
||||
def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
|
||||
user = get_user(activity.data["actor"])
|
||||
user = StatusView.get_user(activity.data["actor"])
|
||||
|
||||
Pleroma.Web.MastodonAPI.StatusView.render("show.json", opts)
|
||||
StatusView.render("show.json", opts)
|
||||
|> Map.merge(%{account: merge_account_views(user)})
|
||||
end
|
||||
|
||||
|
|
@ -26,17 +27,4 @@ defmodule Pleroma.Web.AdminAPI.StatusView do
|
|||
end
|
||||
|
||||
defp merge_account_views(_), do: %{}
|
||||
|
||||
defp get_user(ap_id) do
|
||||
cond do
|
||||
user = User.get_cached_by_ap_id(ap_id) ->
|
||||
user
|
||||
|
||||
user = User.get_by_guessed_nickname(ap_id) ->
|
||||
user
|
||||
|
||||
true ->
|
||||
User.error_user(ap_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
defmodule Pleroma.Web.ApiSpec do
|
||||
alias OpenApiSpex.OpenApi
|
||||
alias OpenApiSpex.Operation
|
||||
alias Pleroma.Web.Endpoint
|
||||
alias Pleroma.Web.Router
|
||||
|
||||
|
|
@ -24,6 +25,13 @@ defmodule Pleroma.Web.ApiSpec do
|
|||
# populate the paths from a phoenix router
|
||||
paths: OpenApiSpex.Paths.from_router(Router),
|
||||
components: %OpenApiSpex.Components{
|
||||
parameters: %{
|
||||
"accountIdOrNickname" =>
|
||||
Operation.parameter(:id, :path, :string, "Account ID or nickname",
|
||||
example: "123",
|
||||
required: true
|
||||
)
|
||||
},
|
||||
securitySchemes: %{
|
||||
"oAuth" => %OpenApiSpex.SecurityScheme{
|
||||
type: "oauth2",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.Helpers do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
|
||||
def request_body(description, schema_ref, opts \\ []) do
|
||||
media_types = ["application/json", "multipart/form-data", "application/x-www-form-urlencoded"]
|
||||
|
||||
|
|
@ -24,4 +27,23 @@ defmodule Pleroma.Web.ApiSpec.Helpers do
|
|||
required: opts[:required] || false
|
||||
}
|
||||
end
|
||||
|
||||
def pagination_params do
|
||||
[
|
||||
Operation.parameter(:max_id, :query, :string, "Return items older than this ID"),
|
||||
Operation.parameter(:min_id, :query, :string, "Return the oldest items newer than this ID"),
|
||||
Operation.parameter(
|
||||
:since_id,
|
||||
:query,
|
||||
:string,
|
||||
"Return the newest items newer than this ID"
|
||||
),
|
||||
Operation.parameter(
|
||||
:limit,
|
||||
:query,
|
||||
%Schema{type: :integer, default: 20, maximum: 40},
|
||||
"Limit"
|
||||
)
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
|
|||
702
lib/pleroma/web/api_spec/operations/account_operation.ex
Normal file
702
lib/pleroma/web/api_spec/operations/account_operation.ex
Normal file
|
|
@ -0,0 +1,702 @@
|
|||
# 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.AccountOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Reference
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Account
|
||||
alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ActorType
|
||||
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
|
||||
|
||||
@spec open_api_operation(atom) :: Operation.t()
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
@spec create_operation() :: Operation.t()
|
||||
def create_operation do
|
||||
%Operation{
|
||||
tags: ["accounts"],
|
||||
summary: "Register an account",
|
||||
description:
|
||||
"Creates a user and account records. Returns an account access token for the app that initiated the request. The app should save this token for later, and should wait for the user to confirm their account by clicking a link in their email inbox.",
|
||||
operationId: "AccountController.create",
|
||||
requestBody: request_body("Parameters", create_request(), required: true),
|
||||
responses: %{
|
||||
200 => Operation.response("Account", "application/json", create_response()),
|
||||
400 => Operation.response("Error", "application/json", ApiError),
|
||||
403 => Operation.response("Error", "application/json", ApiError),
|
||||
429 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def verify_credentials_operation do
|
||||
%Operation{
|
||||
tags: ["accounts"],
|
||||
description: "Test to make sure that the user token works.",
|
||||
summary: "Verify account credentials",
|
||||
operationId: "AccountController.verify_credentials",
|
||||
security: [%{"oAuth" => ["read:accounts"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Account", "application/json", Account)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def update_credentials_operation do
|
||||
%Operation{
|
||||
tags: ["accounts"],
|
||||
summary: "Update account credentials",
|
||||
description: "Update the user's display and preferences.",
|
||||
operationId: "AccountController.update_credentials",
|
||||
security: [%{"oAuth" => ["write:accounts"]}],
|
||||
requestBody: request_body("Parameters", update_creadentials_request(), required: true),
|
||||
responses: %{
|
||||
200 => Operation.response("Account", "application/json", Account),
|
||||
403 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def relationships_operation do
|
||||
%Operation{
|
||||
tags: ["accounts"],
|
||||
summary: "Check relationships to other accounts",
|
||||
operationId: "AccountController.relationships",
|
||||
description: "Find out whether a given account is followed, blocked, muted, etc.",
|
||||
security: [%{"oAuth" => ["read:follows"]}],
|
||||
parameters: [
|
||||
Operation.parameter(
|
||||
:id,
|
||||
:query,
|
||||
%Schema{
|
||||
oneOf: [%Schema{type: :array, items: %Schema{type: :string}}, %Schema{type: :string}]
|
||||
},
|
||||
"Account IDs",
|
||||
example: "123"
|
||||
)
|
||||
],
|
||||
responses: %{
|
||||
200 => Operation.response("Account", "application/json", array_of_relationships())
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def show_operation do
|
||||
%Operation{
|
||||
tags: ["accounts"],
|
||||
summary: "Account",
|
||||
operationId: "AccountController.show",
|
||||
description: "View information about a profile.",
|
||||
parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
|
||||
responses: %{
|
||||
200 => Operation.response("Account", "application/json", Account),
|
||||
404 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def statuses_operation do
|
||||
%Operation{
|
||||
tags: ["accounts"],
|
||||
summary: "Statuses",
|
||||
operationId: "AccountController.statuses",
|
||||
description:
|
||||
"Statuses posted to the given account. Public (for public statuses only), or user token + `read:statuses` (for private statuses the user is authorized to see)",
|
||||
parameters:
|
||||
[
|
||||
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
|
||||
Operation.parameter(:pinned, :query, BooleanLike, "Include only pinned statuses"),
|
||||
Operation.parameter(:tagged, :query, :string, "With tag"),
|
||||
Operation.parameter(
|
||||
:only_media,
|
||||
:query,
|
||||
BooleanLike,
|
||||
"Include only statuses with media attached"
|
||||
),
|
||||
Operation.parameter(
|
||||
:with_muted,
|
||||
:query,
|
||||
BooleanLike,
|
||||
"Include statuses from muted acccounts."
|
||||
),
|
||||
Operation.parameter(:exclude_reblogs, :query, BooleanLike, "Exclude reblogs"),
|
||||
Operation.parameter(:exclude_replies, :query, BooleanLike, "Exclude replies"),
|
||||
Operation.parameter(
|
||||
:exclude_visibilities,
|
||||
:query,
|
||||
%Schema{type: :array, items: VisibilityScope},
|
||||
"Exclude visibilities"
|
||||
)
|
||||
] ++ pagination_params(),
|
||||
responses: %{
|
||||
200 => Operation.response("Statuses", "application/json", array_of_statuses()),
|
||||
404 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def followers_operation do
|
||||
%Operation{
|
||||
tags: ["accounts"],
|
||||
summary: "Followers",
|
||||
operationId: "AccountController.followers",
|
||||
security: [%{"oAuth" => ["read:accounts"]}],
|
||||
description:
|
||||
"Accounts which follow the given account, if network is not hidden by the account owner.",
|
||||
parameters:
|
||||
[%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}] ++ pagination_params(),
|
||||
responses: %{
|
||||
200 => Operation.response("Accounts", "application/json", array_of_accounts())
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def following_operation do
|
||||
%Operation{
|
||||
tags: ["accounts"],
|
||||
summary: "Following",
|
||||
operationId: "AccountController.following",
|
||||
security: [%{"oAuth" => ["read:accounts"]}],
|
||||
description:
|
||||
"Accounts which the given account is following, if network is not hidden by the account owner.",
|
||||
parameters:
|
||||
[%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}] ++ pagination_params(),
|
||||
responses: %{200 => Operation.response("Accounts", "application/json", array_of_accounts())}
|
||||
}
|
||||
end
|
||||
|
||||
def lists_operation do
|
||||
%Operation{
|
||||
tags: ["accounts"],
|
||||
summary: "Lists containing this account",
|
||||
operationId: "AccountController.lists",
|
||||
security: [%{"oAuth" => ["read:lists"]}],
|
||||
description: "User lists that you have added this account to.",
|
||||
parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
|
||||
responses: %{200 => Operation.response("Lists", "application/json", array_of_lists())}
|
||||
}
|
||||
end
|
||||
|
||||
def follow_operation do
|
||||
%Operation{
|
||||
tags: ["accounts"],
|
||||
summary: "Follow",
|
||||
operationId: "AccountController.follow",
|
||||
security: [%{"oAuth" => ["follow", "write:follows"]}],
|
||||
description: "Follow the given account",
|
||||
parameters: [
|
||||
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
|
||||
Operation.parameter(
|
||||
:reblogs,
|
||||
:query,
|
||||
BooleanLike,
|
||||
"Receive this account's reblogs in home timeline? Defaults to true."
|
||||
)
|
||||
],
|
||||
responses: %{
|
||||
200 => Operation.response("Relationship", "application/json", AccountRelationship),
|
||||
400 => Operation.response("Error", "application/json", ApiError),
|
||||
404 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def unfollow_operation do
|
||||
%Operation{
|
||||
tags: ["accounts"],
|
||||
summary: "Unfollow",
|
||||
operationId: "AccountController.unfollow",
|
||||
security: [%{"oAuth" => ["follow", "write:follows"]}],
|
||||
description: "Unfollow the given account",
|
||||
parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
|
||||
responses: %{
|
||||
200 => Operation.response("Relationship", "application/json", AccountRelationship),
|
||||
400 => Operation.response("Error", "application/json", ApiError),
|
||||
404 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def mute_operation do
|
||||
%Operation{
|
||||
tags: ["accounts"],
|
||||
summary: "Mute",
|
||||
operationId: "AccountController.mute",
|
||||
security: [%{"oAuth" => ["follow", "write:mutes"]}],
|
||||
requestBody: request_body("Parameters", mute_request()),
|
||||
description:
|
||||
"Mute the given account. Clients should filter statuses and notifications from this account, if received (e.g. due to a boost in the Home timeline).",
|
||||
parameters: [
|
||||
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
|
||||
Operation.parameter(
|
||||
:notifications,
|
||||
:query,
|
||||
%Schema{allOf: [BooleanLike], default: true},
|
||||
"Mute notifications in addition to statuses? Defaults to `true`."
|
||||
)
|
||||
],
|
||||
responses: %{
|
||||
200 => Operation.response("Relationship", "application/json", AccountRelationship)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def unmute_operation do
|
||||
%Operation{
|
||||
tags: ["accounts"],
|
||||
summary: "Unmute",
|
||||
operationId: "AccountController.unmute",
|
||||
security: [%{"oAuth" => ["follow", "write:mutes"]}],
|
||||
description: "Unmute the given account.",
|
||||
parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
|
||||
responses: %{
|
||||
200 => Operation.response("Relationship", "application/json", AccountRelationship)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def block_operation do
|
||||
%Operation{
|
||||
tags: ["accounts"],
|
||||
summary: "Block",
|
||||
operationId: "AccountController.block",
|
||||
security: [%{"oAuth" => ["follow", "write:blocks"]}],
|
||||
description:
|
||||
"Block the given account. Clients should filter statuses from this account if received (e.g. due to a boost in the Home timeline)",
|
||||
parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
|
||||
responses: %{
|
||||
200 => Operation.response("Relationship", "application/json", AccountRelationship)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def unblock_operation do
|
||||
%Operation{
|
||||
tags: ["accounts"],
|
||||
summary: "Unblock",
|
||||
operationId: "AccountController.unblock",
|
||||
security: [%{"oAuth" => ["follow", "write:blocks"]}],
|
||||
description: "Unblock the given account.",
|
||||
parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
|
||||
responses: %{
|
||||
200 => Operation.response("Relationship", "application/json", AccountRelationship)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def follow_by_uri_operation do
|
||||
%Operation{
|
||||
tags: ["accounts"],
|
||||
summary: "Follow by URI",
|
||||
operationId: "AccountController.follows",
|
||||
security: [%{"oAuth" => ["follow", "write:follows"]}],
|
||||
requestBody: request_body("Parameters", follow_by_uri_request(), required: true),
|
||||
responses: %{
|
||||
200 => Operation.response("Account", "application/json", AccountRelationship),
|
||||
400 => Operation.response("Error", "application/json", ApiError),
|
||||
404 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def mutes_operation do
|
||||
%Operation{
|
||||
tags: ["accounts"],
|
||||
summary: "Muted accounts",
|
||||
operationId: "AccountController.mutes",
|
||||
description: "Accounts the user has muted.",
|
||||
security: [%{"oAuth" => ["follow", "read:mutes"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Accounts", "application/json", array_of_accounts())
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def blocks_operation do
|
||||
%Operation{
|
||||
tags: ["accounts"],
|
||||
summary: "Blocked users",
|
||||
operationId: "AccountController.blocks",
|
||||
description: "View your blocks. See also accounts/:id/{block,unblock}",
|
||||
security: [%{"oAuth" => ["read:blocks"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Accounts", "application/json", array_of_accounts())
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def endorsements_operation do
|
||||
%Operation{
|
||||
tags: ["accounts"],
|
||||
summary: "Endorsements",
|
||||
operationId: "AccountController.endorsements",
|
||||
description: "Not implemented",
|
||||
security: [%{"oAuth" => ["read:accounts"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Empry array", "application/json", %Schema{type: :array})
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def identity_proofs_operation do
|
||||
%Operation{
|
||||
tags: ["accounts"],
|
||||
summary: "Identity proofs",
|
||||
operationId: "AccountController.identity_proofs",
|
||||
description: "Not implemented",
|
||||
responses: %{
|
||||
200 => Operation.response("Empry array", "application/json", %Schema{type: :array})
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp create_request do
|
||||
%Schema{
|
||||
title: "AccountCreateRequest",
|
||||
description: "POST body for creating an account",
|
||||
type: :object,
|
||||
properties: %{
|
||||
reason: %Schema{
|
||||
type: :string,
|
||||
description:
|
||||
"Text that will be reviewed by moderators if registrations require manual approval"
|
||||
},
|
||||
username: %Schema{type: :string, description: "The desired username for the account"},
|
||||
email: %Schema{
|
||||
type: :string,
|
||||
description:
|
||||
"The email address to be used for login. Required when `account_activation_required` is enabled.",
|
||||
format: :email
|
||||
},
|
||||
password: %Schema{
|
||||
type: :string,
|
||||
description: "The password to be used for login",
|
||||
format: :password
|
||||
},
|
||||
agreement: %Schema{
|
||||
type: :boolean,
|
||||
description:
|
||||
"Whether the user agrees to the local rules, terms, and policies. These should be presented to the user in order to allow them to consent before setting this parameter to TRUE."
|
||||
},
|
||||
locale: %Schema{
|
||||
type: :string,
|
||||
description: "The language of the confirmation email that will be sent"
|
||||
},
|
||||
# Pleroma-specific properties:
|
||||
fullname: %Schema{type: :string, description: "Full name"},
|
||||
bio: %Schema{type: :string, description: "Bio", default: ""},
|
||||
captcha_solution: %Schema{
|
||||
type: :string,
|
||||
description: "Provider-specific captcha solution"
|
||||
},
|
||||
captcha_token: %Schema{type: :string, description: "Provider-specific captcha token"},
|
||||
captcha_answer_data: %Schema{type: :string, description: "Provider-specific captcha data"},
|
||||
token: %Schema{
|
||||
type: :string,
|
||||
description: "Invite token required when the registrations aren't public"
|
||||
}
|
||||
},
|
||||
required: [:username, :password, :agreement],
|
||||
example: %{
|
||||
"username" => "cofe",
|
||||
"email" => "cofe@example.com",
|
||||
"password" => "secret",
|
||||
"agreement" => "true",
|
||||
"bio" => "☕️"
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp create_response do
|
||||
%Schema{
|
||||
title: "AccountCreateResponse",
|
||||
description: "Response schema for an account",
|
||||
type: :object,
|
||||
properties: %{
|
||||
token_type: %Schema{type: :string},
|
||||
access_token: %Schema{type: :string},
|
||||
scope: %Schema{type: :array, items: %Schema{type: :string}},
|
||||
created_at: %Schema{type: :integer, format: :"date-time"}
|
||||
},
|
||||
example: %{
|
||||
"access_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzk",
|
||||
"created_at" => 1_585_918_714,
|
||||
"scope" => ["read", "write", "follow", "push"],
|
||||
"token_type" => "Bearer"
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp update_creadentials_request do
|
||||
%Schema{
|
||||
title: "AccountUpdateCredentialsRequest",
|
||||
description: "POST body for creating an account",
|
||||
type: :object,
|
||||
properties: %{
|
||||
bot: %Schema{
|
||||
type: :boolean,
|
||||
description: "Whether the account has a bot flag."
|
||||
},
|
||||
display_name: %Schema{
|
||||
type: :string,
|
||||
description: "The display name to use for the profile."
|
||||
},
|
||||
note: %Schema{type: :string, description: "The account bio."},
|
||||
avatar: %Schema{
|
||||
type: :string,
|
||||
description: "Avatar image encoded using multipart/form-data",
|
||||
format: :binary
|
||||
},
|
||||
header: %Schema{
|
||||
type: :string,
|
||||
description: "Header image encoded using multipart/form-data",
|
||||
format: :binary
|
||||
},
|
||||
locked: %Schema{
|
||||
type: :boolean,
|
||||
description: "Whether manual approval of follow requests is required."
|
||||
},
|
||||
fields_attributes: %Schema{
|
||||
oneOf: [
|
||||
%Schema{type: :array, items: attribute_field()},
|
||||
%Schema{type: :object, additionalProperties: %Schema{type: attribute_field()}}
|
||||
]
|
||||
},
|
||||
# NOTE: `source` field is not supported
|
||||
#
|
||||
# source: %Schema{
|
||||
# type: :object,
|
||||
# properties: %{
|
||||
# privacy: %Schema{type: :string},
|
||||
# sensitive: %Schema{type: :boolean},
|
||||
# language: %Schema{type: :string}
|
||||
# }
|
||||
# },
|
||||
|
||||
# Pleroma-specific fields
|
||||
no_rich_text: %Schema{
|
||||
type: :boolean,
|
||||
description: "html tags are stripped from all statuses requested from the API"
|
||||
},
|
||||
hide_followers: %Schema{type: :boolean, description: "user's followers will be hidden"},
|
||||
hide_follows: %Schema{type: :boolean, description: "user's follows will be hidden"},
|
||||
hide_followers_count: %Schema{
|
||||
type: :boolean,
|
||||
description: "user's follower count will be hidden"
|
||||
},
|
||||
hide_follows_count: %Schema{
|
||||
type: :boolean,
|
||||
description: "user's follow count will be hidden"
|
||||
},
|
||||
hide_favorites: %Schema{
|
||||
type: :boolean,
|
||||
description: "user's favorites timeline will be hidden"
|
||||
},
|
||||
show_role: %Schema{
|
||||
type: :boolean,
|
||||
description: "user's role (e.g admin, moderator) will be exposed to anyone in the
|
||||
API"
|
||||
},
|
||||
default_scope: VisibilityScope,
|
||||
pleroma_settings_store: %Schema{
|
||||
type: :object,
|
||||
description: "Opaque user settings to be saved on the backend."
|
||||
},
|
||||
skip_thread_containment: %Schema{
|
||||
type: :boolean,
|
||||
description: "Skip filtering out broken threads"
|
||||
},
|
||||
allow_following_move: %Schema{
|
||||
type: :boolean,
|
||||
description: "Allows automatically follow moved following accounts"
|
||||
},
|
||||
pleroma_background_image: %Schema{
|
||||
type: :string,
|
||||
description: "Sets the background image of the user.",
|
||||
format: :binary
|
||||
},
|
||||
discoverable: %Schema{
|
||||
type: :boolean,
|
||||
description:
|
||||
"Discovery of this account in search results and other services is allowed."
|
||||
},
|
||||
actor_type: ActorType
|
||||
},
|
||||
example: %{
|
||||
bot: false,
|
||||
display_name: "cofe",
|
||||
note: "foobar",
|
||||
fields_attributes: [%{name: "foo", value: "bar"}],
|
||||
no_rich_text: false,
|
||||
hide_followers: true,
|
||||
hide_follows: false,
|
||||
hide_followers_count: false,
|
||||
hide_follows_count: false,
|
||||
hide_favorites: false,
|
||||
show_role: false,
|
||||
default_scope: "private",
|
||||
pleroma_settings_store: %{"pleroma-fe" => %{"key" => "val"}},
|
||||
skip_thread_containment: false,
|
||||
allow_following_move: false,
|
||||
discoverable: false,
|
||||
actor_type: "Person"
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp array_of_accounts do
|
||||
%Schema{
|
||||
title: "ArrayOfAccounts",
|
||||
type: :array,
|
||||
items: Account
|
||||
}
|
||||
end
|
||||
|
||||
defp array_of_relationships do
|
||||
%Schema{
|
||||
title: "ArrayOfRelationships",
|
||||
description: "Response schema for account relationships",
|
||||
type: :array,
|
||||
items: AccountRelationship,
|
||||
example: [
|
||||
%{
|
||||
"id" => "1",
|
||||
"following" => true,
|
||||
"showing_reblogs" => true,
|
||||
"followed_by" => true,
|
||||
"blocking" => false,
|
||||
"blocked_by" => true,
|
||||
"muting" => false,
|
||||
"muting_notifications" => false,
|
||||
"requested" => false,
|
||||
"domain_blocking" => false,
|
||||
"subscribing" => false,
|
||||
"endorsed" => true
|
||||
},
|
||||
%{
|
||||
"id" => "2",
|
||||
"following" => true,
|
||||
"showing_reblogs" => true,
|
||||
"followed_by" => true,
|
||||
"blocking" => false,
|
||||
"blocked_by" => true,
|
||||
"muting" => true,
|
||||
"muting_notifications" => false,
|
||||
"requested" => true,
|
||||
"domain_blocking" => false,
|
||||
"subscribing" => false,
|
||||
"endorsed" => false
|
||||
},
|
||||
%{
|
||||
"id" => "3",
|
||||
"following" => true,
|
||||
"showing_reblogs" => true,
|
||||
"followed_by" => true,
|
||||
"blocking" => true,
|
||||
"blocked_by" => false,
|
||||
"muting" => true,
|
||||
"muting_notifications" => false,
|
||||
"requested" => false,
|
||||
"domain_blocking" => true,
|
||||
"subscribing" => true,
|
||||
"endorsed" => false
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
defp follow_by_uri_request do
|
||||
%Schema{
|
||||
title: "AccountFollowsRequest",
|
||||
description: "POST body for muting an account",
|
||||
type: :object,
|
||||
properties: %{
|
||||
uri: %Schema{type: :string, format: :uri}
|
||||
},
|
||||
required: [:uri]
|
||||
}
|
||||
end
|
||||
|
||||
defp mute_request do
|
||||
%Schema{
|
||||
title: "AccountMuteRequest",
|
||||
description: "POST body for muting an account",
|
||||
type: :object,
|
||||
properties: %{
|
||||
notifications: %Schema{
|
||||
type: :boolean,
|
||||
description: "Mute notifications in addition to statuses? Defaults to true.",
|
||||
default: true
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
"notifications" => true
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp list do
|
||||
%Schema{
|
||||
title: "List",
|
||||
description: "Response schema for a list",
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
title: %Schema{type: :string}
|
||||
},
|
||||
example: %{
|
||||
"id" => "123",
|
||||
"title" => "my list"
|
||||
}
|
||||
}
|
||||
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" => "anotehr list"}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
defp array_of_statuses do
|
||||
%Schema{
|
||||
title: "ArrayOfStatuses",
|
||||
type: :array,
|
||||
items: Status
|
||||
}
|
||||
end
|
||||
|
||||
defp attribute_field do
|
||||
%Schema{
|
||||
title: "AccountAttributeField",
|
||||
description: "Request schema for account custom fields",
|
||||
type: :object,
|
||||
properties: %{
|
||||
name: %Schema{type: :string},
|
||||
value: %Schema{type: :string}
|
||||
},
|
||||
required: [:name, :value],
|
||||
example: %{
|
||||
"name" => "Website",
|
||||
"value" => "https://pleroma.com"
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
@ -6,8 +6,6 @@ defmodule Pleroma.Web.ApiSpec.AppOperation do
|
|||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Helpers
|
||||
alias Pleroma.Web.ApiSpec.Schemas.AppCreateRequest
|
||||
alias Pleroma.Web.ApiSpec.Schemas.AppCreateResponse
|
||||
|
||||
@spec open_api_operation(atom) :: Operation.t()
|
||||
def open_api_operation(action) do
|
||||
|
|
@ -22,9 +20,9 @@ defmodule Pleroma.Web.ApiSpec.AppOperation do
|
|||
summary: "Create an application",
|
||||
description: "Create a new application to obtain OAuth2 credentials",
|
||||
operationId: "AppController.create",
|
||||
requestBody: Helpers.request_body("Parameters", AppCreateRequest, required: true),
|
||||
requestBody: Helpers.request_body("Parameters", create_request(), required: true),
|
||||
responses: %{
|
||||
200 => Operation.response("App", "application/json", AppCreateResponse),
|
||||
200 => Operation.response("App", "application/json", create_response()),
|
||||
422 =>
|
||||
Operation.response(
|
||||
"Unprocessable Entity",
|
||||
|
|
@ -51,11 +49,7 @@ defmodule Pleroma.Web.ApiSpec.AppOperation do
|
|||
summary: "Verify your app works",
|
||||
description: "Confirm that the app's OAuth2 credentials work.",
|
||||
operationId: "AppController.verify_credentials",
|
||||
security: [
|
||||
%{
|
||||
"oAuth" => ["read"]
|
||||
}
|
||||
],
|
||||
security: [%{"oAuth" => ["read"]}],
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("App", "application/json", %Schema{
|
||||
|
|
@ -93,4 +87,58 @@ defmodule Pleroma.Web.ApiSpec.AppOperation do
|
|||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp create_request do
|
||||
%Schema{
|
||||
title: "AppCreateRequest",
|
||||
description: "POST body for creating an app",
|
||||
type: :object,
|
||||
properties: %{
|
||||
client_name: %Schema{type: :string, description: "A name for your application."},
|
||||
redirect_uris: %Schema{
|
||||
type: :string,
|
||||
description:
|
||||
"Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter."
|
||||
},
|
||||
scopes: %Schema{
|
||||
type: :string,
|
||||
description: "Space separated list of scopes",
|
||||
default: "read"
|
||||
},
|
||||
website: %Schema{type: :string, description: "A URL to the homepage of your app"}
|
||||
},
|
||||
required: [:client_name, :redirect_uris],
|
||||
example: %{
|
||||
"client_name" => "My App",
|
||||
"redirect_uris" => "https://myapp.com/auth/callback",
|
||||
"website" => "https://myapp.com/"
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp create_response do
|
||||
%Schema{
|
||||
title: "AppCreateResponse",
|
||||
description: "Response schema for an app",
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
name: %Schema{type: :string},
|
||||
client_id: %Schema{type: :string},
|
||||
client_secret: %Schema{type: :string},
|
||||
redirect_uri: %Schema{type: :string},
|
||||
vapid_key: %Schema{type: :string},
|
||||
website: %Schema{type: :string, nullable: true}
|
||||
},
|
||||
example: %{
|
||||
"id" => "123",
|
||||
"name" => "My App",
|
||||
"client_id" => "TWhM-tNSuncnqN7DBJmoyeLnk6K3iJJ71KKXxgL1hPM",
|
||||
"client_secret" => "ZEaFUFmF0umgBX1qKJDjaU99Q31lDkOU8NutzTOoliw",
|
||||
"vapid_key" =>
|
||||
"BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M=",
|
||||
"website" => "https://myapp.com/"
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.CustomEmojiOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Emoji
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def index_operation do
|
||||
%Operation{
|
||||
tags: ["custom_emojis"],
|
||||
summary: "List custom custom emojis",
|
||||
description: "Returns custom emojis that are available on the server.",
|
||||
operationId: "CustomEmojiController.index",
|
||||
responses: %{
|
||||
200 => Operation.response("Custom Emojis", "application/json", resposnse())
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp resposnse do
|
||||
%Schema{
|
||||
title: "CustomEmojisResponse",
|
||||
description: "Response schema for custom emojis",
|
||||
type: :array,
|
||||
items: custom_emoji(),
|
||||
example: [
|
||||
%{
|
||||
"category" => "Fun",
|
||||
"shortcode" => "blank",
|
||||
"static_url" => "https://lain.com/emoji/blank.png",
|
||||
"tags" => ["Fun"],
|
||||
"url" => "https://lain.com/emoji/blank.png",
|
||||
"visible_in_picker" => false
|
||||
},
|
||||
%{
|
||||
"category" => "Gif,Fun",
|
||||
"shortcode" => "firefox",
|
||||
"static_url" => "https://lain.com/emoji/Firefox.gif",
|
||||
"tags" => ["Gif", "Fun"],
|
||||
"url" => "https://lain.com/emoji/Firefox.gif",
|
||||
"visible_in_picker" => true
|
||||
},
|
||||
%{
|
||||
"category" => "pack:mixed",
|
||||
"shortcode" => "sadcat",
|
||||
"static_url" => "https://lain.com/emoji/mixed/sadcat.png",
|
||||
"tags" => ["pack:mixed"],
|
||||
"url" => "https://lain.com/emoji/mixed/sadcat.png",
|
||||
"visible_in_picker" => true
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
defp custom_emoji do
|
||||
%Schema{
|
||||
title: "CustomEmoji",
|
||||
description: "Schema for a CustomEmoji",
|
||||
allOf: [
|
||||
Emoji,
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
category: %Schema{type: :string},
|
||||
tags: %Schema{type: :array}
|
||||
}
|
||||
}
|
||||
],
|
||||
example: %{
|
||||
"category" => "Fun",
|
||||
"shortcode" => "aaaa",
|
||||
"url" =>
|
||||
"https://files.mastodon.social/custom_emojis/images/000/007/118/original/aaaa.png",
|
||||
"static_url" =>
|
||||
"https://files.mastodon.social/custom_emojis/images/000/007/118/static/aaaa.png",
|
||||
"visible_in_picker" => true,
|
||||
"tags" => ["Gif", "Fun"]
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
@ -6,8 +6,6 @@ defmodule Pleroma.Web.ApiSpec.DomainBlockOperation do
|
|||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Helpers
|
||||
alias Pleroma.Web.ApiSpec.Schemas.DomainBlockRequest
|
||||
alias Pleroma.Web.ApiSpec.Schemas.DomainBlocksResponse
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
|
|
@ -22,7 +20,13 @@ defmodule Pleroma.Web.ApiSpec.DomainBlockOperation do
|
|||
security: [%{"oAuth" => ["follow", "read:blocks"]}],
|
||||
operationId: "DomainBlockController.index",
|
||||
responses: %{
|
||||
200 => Operation.response("Domain blocks", "application/json", DomainBlocksResponse)
|
||||
200 =>
|
||||
Operation.response("Domain blocks", "application/json", %Schema{
|
||||
description: "Response schema for domain blocks",
|
||||
type: :array,
|
||||
items: %Schema{type: :string},
|
||||
example: ["google.com", "facebook.com"]
|
||||
})
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
@ -40,7 +44,7 @@ defmodule Pleroma.Web.ApiSpec.DomainBlockOperation do
|
|||
- prevent following new users from it (but does not remove existing follows)
|
||||
""",
|
||||
operationId: "DomainBlockController.create",
|
||||
requestBody: Helpers.request_body("Parameters", DomainBlockRequest, required: true),
|
||||
requestBody: domain_block_request(),
|
||||
security: [%{"oAuth" => ["follow", "write:blocks"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
|
||||
|
|
@ -54,11 +58,28 @@ defmodule Pleroma.Web.ApiSpec.DomainBlockOperation do
|
|||
summary: "Unblock a domain",
|
||||
description: "Remove a domain block, if it exists in the user's array of blocked domains.",
|
||||
operationId: "DomainBlockController.delete",
|
||||
requestBody: Helpers.request_body("Parameters", DomainBlockRequest, required: true),
|
||||
requestBody: domain_block_request(),
|
||||
security: [%{"oAuth" => ["follow", "write:blocks"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp domain_block_request do
|
||||
Helpers.request_body(
|
||||
"Parameters",
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
domain: %Schema{type: :string}
|
||||
},
|
||||
required: [:domain]
|
||||
},
|
||||
required: true,
|
||||
example: %{
|
||||
"domain" => "facebook.com"
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
231
lib/pleroma/web/api_spec/render_error.ex
Normal file
231
lib/pleroma/web/api_spec/render_error.ex
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
# 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.RenderError do
|
||||
@behaviour Plug
|
||||
|
||||
import Plug.Conn, only: [put_status: 2]
|
||||
import Phoenix.Controller, only: [json: 2]
|
||||
import Pleroma.Web.Gettext
|
||||
|
||||
@impl Plug
|
||||
def init(opts), do: opts
|
||||
|
||||
@impl Plug
|
||||
|
||||
def call(conn, errors) do
|
||||
errors =
|
||||
Enum.map(errors, fn
|
||||
%{name: nil} = err ->
|
||||
%OpenApiSpex.Cast.Error{err | name: List.last(err.path)}
|
||||
|
||||
err ->
|
||||
err
|
||||
end)
|
||||
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{
|
||||
error: errors |> Enum.map(&message/1) |> Enum.join(" "),
|
||||
errors: errors |> Enum.map(&render_error/1)
|
||||
})
|
||||
end
|
||||
|
||||
defp render_error(error) do
|
||||
pointer = OpenApiSpex.path_to_string(error)
|
||||
|
||||
%{
|
||||
title: "Invalid value",
|
||||
source: %{
|
||||
pointer: pointer
|
||||
},
|
||||
message: OpenApiSpex.Cast.Error.message(error)
|
||||
}
|
||||
end
|
||||
|
||||
defp message(%{reason: :invalid_schema_type, type: type, name: name}) do
|
||||
gettext("%{name} - Invalid schema.type. Got: %{type}.",
|
||||
name: name,
|
||||
type: inspect(type)
|
||||
)
|
||||
end
|
||||
|
||||
defp message(%{reason: :null_value, name: name} = error) do
|
||||
case error.type do
|
||||
nil ->
|
||||
gettext("%{name} - null value.", name: name)
|
||||
|
||||
type ->
|
||||
gettext("%{name} - null value where %{type} expected.",
|
||||
name: name,
|
||||
type: type
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp message(%{reason: :all_of, meta: %{invalid_schema: invalid_schema}}) do
|
||||
gettext(
|
||||
"Failed to cast value as %{invalid_schema}. Value must be castable using `allOf` schemas listed.",
|
||||
invalid_schema: invalid_schema
|
||||
)
|
||||
end
|
||||
|
||||
defp message(%{reason: :any_of, meta: %{failed_schemas: failed_schemas}}) do
|
||||
gettext("Failed to cast value using any of: %{failed_schemas}.",
|
||||
failed_schemas: failed_schemas
|
||||
)
|
||||
end
|
||||
|
||||
defp message(%{reason: :one_of, meta: %{failed_schemas: failed_schemas}}) do
|
||||
gettext("Failed to cast value to one of: %{failed_schemas}.", failed_schemas: failed_schemas)
|
||||
end
|
||||
|
||||
defp message(%{reason: :min_length, length: length, name: name}) do
|
||||
gettext("%{name} - String length is smaller than minLength: %{length}.",
|
||||
name: name,
|
||||
length: length
|
||||
)
|
||||
end
|
||||
|
||||
defp message(%{reason: :max_length, length: length, name: name}) do
|
||||
gettext("%{name} - String length is larger than maxLength: %{length}.",
|
||||
name: name,
|
||||
length: length
|
||||
)
|
||||
end
|
||||
|
||||
defp message(%{reason: :unique_items, name: name}) do
|
||||
gettext("%{name} - Array items must be unique.", name: name)
|
||||
end
|
||||
|
||||
defp message(%{reason: :min_items, length: min, value: array, name: name}) do
|
||||
gettext("%{name} - Array length %{length} is smaller than minItems: %{min}.",
|
||||
name: name,
|
||||
length: length(array),
|
||||
min: min
|
||||
)
|
||||
end
|
||||
|
||||
defp message(%{reason: :max_items, length: max, value: array, name: name}) do
|
||||
gettext("%{name} - Array length %{length} is larger than maxItems: %{}.",
|
||||
name: name,
|
||||
length: length(array),
|
||||
max: max
|
||||
)
|
||||
end
|
||||
|
||||
defp message(%{reason: :multiple_of, length: multiple, value: count, name: name}) do
|
||||
gettext("%{name} - %{count} is not a multiple of %{multiple}.",
|
||||
name: name,
|
||||
count: count,
|
||||
multiple: multiple
|
||||
)
|
||||
end
|
||||
|
||||
defp message(%{reason: :exclusive_max, length: max, value: value, name: name})
|
||||
when value >= max do
|
||||
gettext("%{name} - %{value} is larger than exclusive maximum %{max}.",
|
||||
name: name,
|
||||
value: value,
|
||||
max: max
|
||||
)
|
||||
end
|
||||
|
||||
defp message(%{reason: :maximum, length: max, value: value, name: name})
|
||||
when value > max do
|
||||
gettext("%{name} - %{value} is larger than inclusive maximum %{max}.",
|
||||
name: name,
|
||||
value: value,
|
||||
max: max
|
||||
)
|
||||
end
|
||||
|
||||
defp message(%{reason: :exclusive_multiple, length: min, value: value, name: name})
|
||||
when value <= min do
|
||||
gettext("%{name} - %{value} is smaller than exclusive minimum %{min}.",
|
||||
name: name,
|
||||
value: value,
|
||||
min: min
|
||||
)
|
||||
end
|
||||
|
||||
defp message(%{reason: :minimum, length: min, value: value, name: name})
|
||||
when value < min do
|
||||
gettext("%{name} - %{value} is smaller than inclusive minimum %{min}.",
|
||||
name: name,
|
||||
value: value,
|
||||
min: min
|
||||
)
|
||||
end
|
||||
|
||||
defp message(%{reason: :invalid_type, type: type, value: value, name: name}) do
|
||||
gettext("%{name} - Invalid %{type}. Got: %{value}.",
|
||||
name: name,
|
||||
value: OpenApiSpex.TermType.type(value),
|
||||
type: type
|
||||
)
|
||||
end
|
||||
|
||||
defp message(%{reason: :invalid_format, format: format, name: name}) do
|
||||
gettext("%{name} - Invalid format. Expected %{format}.", name: name, format: inspect(format))
|
||||
end
|
||||
|
||||
defp message(%{reason: :invalid_enum, name: name}) do
|
||||
gettext("%{name} - Invalid value for enum.", name: name)
|
||||
end
|
||||
|
||||
defp message(%{reason: :polymorphic_failed, type: polymorphic_type}) do
|
||||
gettext("Failed to cast to any schema in %{polymorphic_type}",
|
||||
polymorphic_type: polymorphic_type
|
||||
)
|
||||
end
|
||||
|
||||
defp message(%{reason: :unexpected_field, name: name}) do
|
||||
gettext("Unexpected field: %{name}.", name: safe_string(name))
|
||||
end
|
||||
|
||||
defp message(%{reason: :no_value_for_discriminator, name: field}) do
|
||||
gettext("Value used as discriminator for `%{field}` matches no schemas.", name: field)
|
||||
end
|
||||
|
||||
defp message(%{reason: :invalid_discriminator_value, name: field}) do
|
||||
gettext("No value provided for required discriminator `%{field}`.", name: field)
|
||||
end
|
||||
|
||||
defp message(%{reason: :unknown_schema, name: name}) do
|
||||
gettext("Unknown schema: %{name}.", name: name)
|
||||
end
|
||||
|
||||
defp message(%{reason: :missing_field, name: name}) do
|
||||
gettext("Missing field: %{name}.", name: name)
|
||||
end
|
||||
|
||||
defp message(%{reason: :missing_header, name: name}) do
|
||||
gettext("Missing header: %{name}.", name: name)
|
||||
end
|
||||
|
||||
defp message(%{reason: :invalid_header, name: name}) do
|
||||
gettext("Invalid value for header: %{name}.", name: name)
|
||||
end
|
||||
|
||||
defp message(%{reason: :max_properties, meta: meta}) do
|
||||
gettext(
|
||||
"Object property count %{property_count} is greater than maxProperties: %{max_properties}.",
|
||||
property_count: meta.property_count,
|
||||
max_properties: meta.max_properties
|
||||
)
|
||||
end
|
||||
|
||||
defp message(%{reason: :min_properties, meta: meta}) do
|
||||
gettext(
|
||||
"Object property count %{property_count} is less than minProperties: %{min_properties}",
|
||||
property_count: meta.property_count,
|
||||
min_properties: meta.min_properties
|
||||
)
|
||||
end
|
||||
|
||||
defp safe_string(string) do
|
||||
to_string(string) |> String.slice(0..39)
|
||||
end
|
||||
end
|
||||
167
lib/pleroma/web/api_spec/schemas/account.ex
Normal file
167
lib/pleroma/web/api_spec/schemas/account.ex
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
# 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.Account do
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.AccountField
|
||||
alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ActorType
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Emoji
|
||||
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
||||
alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
|
||||
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "Account",
|
||||
description: "Response schema for an account",
|
||||
type: :object,
|
||||
properties: %{
|
||||
acct: %Schema{type: :string},
|
||||
avatar_static: %Schema{type: :string, format: :uri},
|
||||
avatar: %Schema{type: :string, format: :uri},
|
||||
bot: %Schema{type: :boolean},
|
||||
created_at: %Schema{type: :string, format: "date-time"},
|
||||
display_name: %Schema{type: :string},
|
||||
emojis: %Schema{type: :array, items: Emoji},
|
||||
fields: %Schema{type: :array, items: AccountField},
|
||||
follow_requests_count: %Schema{type: :integer},
|
||||
followers_count: %Schema{type: :integer},
|
||||
following_count: %Schema{type: :integer},
|
||||
header_static: %Schema{type: :string, format: :uri},
|
||||
header: %Schema{type: :string, format: :uri},
|
||||
id: FlakeID,
|
||||
locked: %Schema{type: :boolean},
|
||||
note: %Schema{type: :string, format: :html},
|
||||
statuses_count: %Schema{type: :integer},
|
||||
url: %Schema{type: :string, format: :uri},
|
||||
username: %Schema{type: :string},
|
||||
pleroma: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
allow_following_move: %Schema{type: :boolean},
|
||||
background_image: %Schema{type: :string, nullable: true},
|
||||
chat_token: %Schema{type: :string},
|
||||
confirmation_pending: %Schema{type: :boolean},
|
||||
hide_favorites: %Schema{type: :boolean},
|
||||
hide_followers_count: %Schema{type: :boolean},
|
||||
hide_followers: %Schema{type: :boolean},
|
||||
hide_follows_count: %Schema{type: :boolean},
|
||||
hide_follows: %Schema{type: :boolean},
|
||||
is_admin: %Schema{type: :boolean},
|
||||
is_moderator: %Schema{type: :boolean},
|
||||
skip_thread_containment: %Schema{type: :boolean},
|
||||
tags: %Schema{type: :array, items: %Schema{type: :string}},
|
||||
unread_conversation_count: %Schema{type: :integer},
|
||||
notification_settings: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
followers: %Schema{type: :boolean},
|
||||
follows: %Schema{type: :boolean},
|
||||
non_followers: %Schema{type: :boolean},
|
||||
non_follows: %Schema{type: :boolean},
|
||||
privacy_option: %Schema{type: :boolean}
|
||||
}
|
||||
},
|
||||
relationship: AccountRelationship,
|
||||
settings_store: %Schema{
|
||||
type: :object
|
||||
}
|
||||
}
|
||||
},
|
||||
source: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
fields: %Schema{type: :array, items: AccountField},
|
||||
note: %Schema{type: :string},
|
||||
privacy: VisibilityScope,
|
||||
sensitive: %Schema{type: :boolean},
|
||||
pleroma: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
actor_type: ActorType,
|
||||
discoverable: %Schema{type: :boolean},
|
||||
no_rich_text: %Schema{type: :boolean},
|
||||
show_role: %Schema{type: :boolean}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
"acct" => "foobar",
|
||||
"avatar" => "https://mypleroma.com/images/avi.png",
|
||||
"avatar_static" => "https://mypleroma.com/images/avi.png",
|
||||
"bot" => false,
|
||||
"created_at" => "2020-03-24T13:05:58.000Z",
|
||||
"display_name" => "foobar",
|
||||
"emojis" => [],
|
||||
"fields" => [],
|
||||
"follow_requests_count" => 0,
|
||||
"followers_count" => 0,
|
||||
"following_count" => 1,
|
||||
"header" => "https://mypleroma.com/images/banner.png",
|
||||
"header_static" => "https://mypleroma.com/images/banner.png",
|
||||
"id" => "9tKi3esbG7OQgZ2920",
|
||||
"locked" => false,
|
||||
"note" => "cofe",
|
||||
"pleroma" => %{
|
||||
"allow_following_move" => true,
|
||||
"background_image" => nil,
|
||||
"confirmation_pending" => true,
|
||||
"hide_favorites" => true,
|
||||
"hide_followers" => false,
|
||||
"hide_followers_count" => false,
|
||||
"hide_follows" => false,
|
||||
"hide_follows_count" => false,
|
||||
"is_admin" => false,
|
||||
"is_moderator" => false,
|
||||
"skip_thread_containment" => false,
|
||||
"chat_token" =>
|
||||
"SFMyNTY.g3QAAAACZAAEZGF0YW0AAAASOXRLaTNlc2JHN09RZ1oyOTIwZAAGc2lnbmVkbgYARNplS3EB.Mb_Iaqew2bN1I1o79B_iP7encmVCpTKC4OtHZRxdjKc",
|
||||
"unread_conversation_count" => 0,
|
||||
"tags" => [],
|
||||
"notification_settings" => %{
|
||||
"followers" => true,
|
||||
"follows" => true,
|
||||
"non_followers" => true,
|
||||
"non_follows" => true,
|
||||
"privacy_option" => false
|
||||
},
|
||||
"relationship" => %{
|
||||
"blocked_by" => false,
|
||||
"blocking" => false,
|
||||
"domain_blocking" => false,
|
||||
"endorsed" => false,
|
||||
"followed_by" => false,
|
||||
"following" => false,
|
||||
"id" => "9tKi3esbG7OQgZ2920",
|
||||
"muting" => false,
|
||||
"muting_notifications" => false,
|
||||
"requested" => false,
|
||||
"showing_reblogs" => true,
|
||||
"subscribing" => false
|
||||
},
|
||||
"settings_store" => %{
|
||||
"pleroma-fe" => %{}
|
||||
}
|
||||
},
|
||||
"source" => %{
|
||||
"fields" => [],
|
||||
"note" => "foobar",
|
||||
"pleroma" => %{
|
||||
"actor_type" => "Person",
|
||||
"discoverable" => false,
|
||||
"no_rich_text" => false,
|
||||
"show_role" => true
|
||||
},
|
||||
"privacy" => "public",
|
||||
"sensitive" => false
|
||||
},
|
||||
"statuses_count" => 0,
|
||||
"url" => "https://mypleroma.com/users/foobar",
|
||||
"username" => "foobar"
|
||||
}
|
||||
})
|
||||
end
|
||||
26
lib/pleroma/web/api_spec/schemas/account_field.ex
Normal file
26
lib/pleroma/web/api_spec/schemas/account_field.ex
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.Schemas.AccountField do
|
||||
alias OpenApiSpex.Schema
|
||||
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "AccountField",
|
||||
description: "Response schema for account custom fields",
|
||||
type: :object,
|
||||
properties: %{
|
||||
name: %Schema{type: :string},
|
||||
value: %Schema{type: :string, format: :html},
|
||||
verified_at: %Schema{type: :string, format: :"date-time", nullable: true}
|
||||
},
|
||||
example: %{
|
||||
"name" => "Website",
|
||||
"value" =>
|
||||
"<a href=\"https://pleroma.com\" rel=\"me nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"\">pleroma.com</span><span class=\"invisible\"></span></a>",
|
||||
"verified_at" => "2019-08-29T04:14:55.571+00:00"
|
||||
}
|
||||
})
|
||||
end
|
||||
44
lib/pleroma/web/api_spec/schemas/account_relationship.ex
Normal file
44
lib/pleroma/web/api_spec/schemas/account_relationship.ex
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# 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.AccountRelationship do
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
||||
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "AccountRelationship",
|
||||
description: "Response schema for relationship",
|
||||
type: :object,
|
||||
properties: %{
|
||||
blocked_by: %Schema{type: :boolean},
|
||||
blocking: %Schema{type: :boolean},
|
||||
domain_blocking: %Schema{type: :boolean},
|
||||
endorsed: %Schema{type: :boolean},
|
||||
followed_by: %Schema{type: :boolean},
|
||||
following: %Schema{type: :boolean},
|
||||
id: FlakeID,
|
||||
muting: %Schema{type: :boolean},
|
||||
muting_notifications: %Schema{type: :boolean},
|
||||
requested: %Schema{type: :boolean},
|
||||
showing_reblogs: %Schema{type: :boolean},
|
||||
subscribing: %Schema{type: :boolean}
|
||||
},
|
||||
example: %{
|
||||
"blocked_by" => false,
|
||||
"blocking" => false,
|
||||
"domain_blocking" => false,
|
||||
"endorsed" => false,
|
||||
"followed_by" => false,
|
||||
"following" => false,
|
||||
"id" => "9tKi3esbG7OQgZ2920",
|
||||
"muting" => false,
|
||||
"muting_notifications" => false,
|
||||
"requested" => false,
|
||||
"showing_reblogs" => true,
|
||||
"subscribing" => false
|
||||
}
|
||||
})
|
||||
end
|
||||
13
lib/pleroma/web/api_spec/schemas/actor_type.ex
Normal file
13
lib/pleroma/web/api_spec/schemas/actor_type.ex
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# 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.ActorType do
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "ActorType",
|
||||
type: :string,
|
||||
enum: ["Application", "Group", "Organization", "Person", "Service"]
|
||||
})
|
||||
end
|
||||
|
|
@ -2,19 +2,18 @@
|
|||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.Schemas.DomainBlockRequest do
|
||||
defmodule Pleroma.Web.ApiSpec.Schemas.ApiError do
|
||||
alias OpenApiSpex.Schema
|
||||
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "DomainBlockRequest",
|
||||
title: "ApiError",
|
||||
description: "Response schema for API error",
|
||||
type: :object,
|
||||
properties: %{
|
||||
domain: %Schema{type: :string}
|
||||
},
|
||||
required: [:domain],
|
||||
properties: %{error: %Schema{type: :string}},
|
||||
example: %{
|
||||
"domain" => "facebook.com"
|
||||
"error" => "Something went wrong"
|
||||
}
|
||||
})
|
||||
end
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.Schemas.AppCreateRequest do
|
||||
alias OpenApiSpex.Schema
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "AppCreateRequest",
|
||||
description: "POST body for creating an app",
|
||||
type: :object,
|
||||
properties: %{
|
||||
client_name: %Schema{type: :string, description: "A name for your application."},
|
||||
redirect_uris: %Schema{
|
||||
type: :string,
|
||||
description:
|
||||
"Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter."
|
||||
},
|
||||
scopes: %Schema{
|
||||
type: :string,
|
||||
description: "Space separated list of scopes. If none is provided, defaults to `read`."
|
||||
},
|
||||
website: %Schema{type: :string, description: "A URL to the homepage of your app"}
|
||||
},
|
||||
required: [:client_name, :redirect_uris],
|
||||
example: %{
|
||||
"client_name" => "My App",
|
||||
"redirect_uris" => "https://myapp.com/auth/callback",
|
||||
"website" => "https://myapp.com/"
|
||||
}
|
||||
})
|
||||
end
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.Schemas.AppCreateResponse do
|
||||
alias OpenApiSpex.Schema
|
||||
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "AppCreateResponse",
|
||||
description: "Response schema for an app",
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
name: %Schema{type: :string},
|
||||
client_id: %Schema{type: :string},
|
||||
client_secret: %Schema{type: :string},
|
||||
redirect_uri: %Schema{type: :string},
|
||||
vapid_key: %Schema{type: :string},
|
||||
website: %Schema{type: :string, nullable: true}
|
||||
},
|
||||
example: %{
|
||||
"id" => "123",
|
||||
"name" => "My App",
|
||||
"client_id" => "TWhM-tNSuncnqN7DBJmoyeLnk6K3iJJ71KKXxgL1hPM",
|
||||
"client_secret" => "ZEaFUFmF0umgBX1qKJDjaU99Q31lDkOU8NutzTOoliw",
|
||||
"vapid_key" =>
|
||||
"BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M=",
|
||||
"website" => "https://myapp.com/"
|
||||
}
|
||||
})
|
||||
end
|
||||
36
lib/pleroma/web/api_spec/schemas/boolean_like.ex
Normal file
36
lib/pleroma/web/api_spec/schemas/boolean_like.ex
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.Schemas.BooleanLike do
|
||||
alias OpenApiSpex.Schema
|
||||
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "BooleanLike",
|
||||
description: """
|
||||
The following values will be treated as `false`:
|
||||
- false
|
||||
- 0
|
||||
- "0",
|
||||
- "f",
|
||||
- "F",
|
||||
- "false",
|
||||
- "FALSE",
|
||||
- "off",
|
||||
- "OFF"
|
||||
|
||||
All other non-null values will be treated as `true`
|
||||
""",
|
||||
anyOf: [
|
||||
%Schema{type: :boolean},
|
||||
%Schema{type: :string},
|
||||
%Schema{type: :integer}
|
||||
]
|
||||
})
|
||||
|
||||
def after_cast(value, _schmea) do
|
||||
{:ok, Pleroma.Web.ControllerHelper.truthy_param?(value)}
|
||||
end
|
||||
end
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.Schemas.DomainBlocksResponse do
|
||||
require OpenApiSpex
|
||||
alias OpenApiSpex.Schema
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "DomainBlocksResponse",
|
||||
description: "Response schema for domain blocks",
|
||||
type: :array,
|
||||
items: %Schema{type: :string},
|
||||
example: ["google.com", "facebook.com"]
|
||||
})
|
||||
end
|
||||
29
lib/pleroma/web/api_spec/schemas/emoji.ex
Normal file
29
lib/pleroma/web/api_spec/schemas/emoji.ex
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.Schemas.Emoji do
|
||||
alias OpenApiSpex.Schema
|
||||
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "Emoji",
|
||||
description: "Response schema for an emoji",
|
||||
type: :object,
|
||||
properties: %{
|
||||
shortcode: %Schema{type: :string},
|
||||
url: %Schema{type: :string, format: :uri},
|
||||
static_url: %Schema{type: :string, format: :uri},
|
||||
visible_in_picker: %Schema{type: :boolean}
|
||||
},
|
||||
example: %{
|
||||
"shortcode" => "fatyoshi",
|
||||
"url" =>
|
||||
"https://files.mastodon.social/custom_emojis/images/000/023/920/original/e57ecb623faa0dc9.png",
|
||||
"static_url" =>
|
||||
"https://files.mastodon.social/custom_emojis/images/000/023/920/static/e57ecb623faa0dc9.png",
|
||||
"visible_in_picker" => true
|
||||
}
|
||||
})
|
||||
end
|
||||
14
lib/pleroma/web/api_spec/schemas/flake_id.ex
Normal file
14
lib/pleroma/web/api_spec/schemas/flake_id.ex
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# 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.FlakeID do
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "FlakeID",
|
||||
description:
|
||||
"Pleroma uses 128-bit ids as opposed to Mastodon's 64 bits. However just like Mastodon's ids they are lexically sortable strings",
|
||||
type: :string
|
||||
})
|
||||
end
|
||||
36
lib/pleroma/web/api_spec/schemas/poll.ex
Normal file
36
lib/pleroma/web/api_spec/schemas/poll.ex
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.Schemas.Poll do
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Emoji
|
||||
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
||||
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "Poll",
|
||||
description: "Response schema for account custom fields",
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: FlakeID,
|
||||
expires_at: %Schema{type: :string, format: "date-time"},
|
||||
expired: %Schema{type: :boolean},
|
||||
multiple: %Schema{type: :boolean},
|
||||
votes_count: %Schema{type: :integer},
|
||||
voted: %Schema{type: :boolean},
|
||||
emojis: %Schema{type: :array, items: Emoji},
|
||||
options: %Schema{
|
||||
type: :array,
|
||||
items: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
title: %Schema{type: :string},
|
||||
votes_count: %Schema{type: :integer}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
end
|
||||
226
lib/pleroma/web/api_spec/schemas/status.ex
Normal file
226
lib/pleroma/web/api_spec/schemas/status.ex
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
# 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.Status do
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Account
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Emoji
|
||||
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Poll
|
||||
alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
|
||||
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "Status",
|
||||
description: "Response schema for a status",
|
||||
type: :object,
|
||||
properties: %{
|
||||
account: Account,
|
||||
application: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
name: %Schema{type: :string},
|
||||
website: %Schema{type: :string, nullable: true, format: :uri}
|
||||
}
|
||||
},
|
||||
bookmarked: %Schema{type: :boolean},
|
||||
card: %Schema{
|
||||
type: :object,
|
||||
nullable: true,
|
||||
properties: %{
|
||||
type: %Schema{type: :string, enum: ["link", "photo", "video", "rich"]},
|
||||
provider_name: %Schema{type: :string, nullable: true},
|
||||
provider_url: %Schema{type: :string, format: :uri},
|
||||
url: %Schema{type: :string, format: :uri},
|
||||
image: %Schema{type: :string, nullable: true, format: :uri},
|
||||
title: %Schema{type: :string},
|
||||
description: %Schema{type: :string}
|
||||
}
|
||||
},
|
||||
content: %Schema{type: :string, format: :html},
|
||||
created_at: %Schema{type: :string, format: "date-time"},
|
||||
emojis: %Schema{type: :array, items: Emoji},
|
||||
favourited: %Schema{type: :boolean},
|
||||
favourites_count: %Schema{type: :integer},
|
||||
id: FlakeID,
|
||||
in_reply_to_account_id: %Schema{type: :string, nullable: true},
|
||||
in_reply_to_id: %Schema{type: :string, nullable: true},
|
||||
language: %Schema{type: :string, nullable: true},
|
||||
media_attachments: %Schema{
|
||||
type: :array,
|
||||
items: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
url: %Schema{type: :string, format: :uri},
|
||||
remote_url: %Schema{type: :string, format: :uri},
|
||||
preview_url: %Schema{type: :string, format: :uri},
|
||||
text_url: %Schema{type: :string, format: :uri},
|
||||
description: %Schema{type: :string},
|
||||
type: %Schema{type: :string, enum: ["image", "video", "audio", "unknown"]},
|
||||
pleroma: %Schema{
|
||||
type: :object,
|
||||
properties: %{mime_type: %Schema{type: :string}}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
mentions: %Schema{
|
||||
type: :array,
|
||||
items: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
acct: %Schema{type: :string},
|
||||
username: %Schema{type: :string},
|
||||
url: %Schema{type: :string, format: :uri}
|
||||
}
|
||||
}
|
||||
},
|
||||
muted: %Schema{type: :boolean},
|
||||
pinned: %Schema{type: :boolean},
|
||||
pleroma: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
content: %Schema{type: :object, additionalProperties: %Schema{type: :string}},
|
||||
conversation_id: %Schema{type: :integer},
|
||||
direct_conversation_id: %Schema{type: :string, nullable: true},
|
||||
emoji_reactions: %Schema{
|
||||
type: :array,
|
||||
items: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
name: %Schema{type: :string},
|
||||
count: %Schema{type: :integer},
|
||||
me: %Schema{type: :boolean}
|
||||
}
|
||||
}
|
||||
},
|
||||
expires_at: %Schema{type: :string, format: "date-time", nullable: true},
|
||||
in_reply_to_account_acct: %Schema{type: :string, nullable: true},
|
||||
local: %Schema{type: :boolean},
|
||||
spoiler_text: %Schema{type: :object, additionalProperties: %Schema{type: :string}},
|
||||
thread_muted: %Schema{type: :boolean}
|
||||
}
|
||||
},
|
||||
poll: %Schema{type: Poll, nullable: true},
|
||||
reblog: %Schema{
|
||||
allOf: [%OpenApiSpex.Reference{"$ref": "#/components/schemas/Status"}],
|
||||
nullable: true
|
||||
},
|
||||
reblogged: %Schema{type: :boolean},
|
||||
reblogs_count: %Schema{type: :integer},
|
||||
replies_count: %Schema{type: :integer},
|
||||
sensitive: %Schema{type: :boolean},
|
||||
spoiler_text: %Schema{type: :string},
|
||||
tags: %Schema{
|
||||
type: :array,
|
||||
items: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
name: %Schema{type: :string},
|
||||
url: %Schema{type: :string, format: :uri}
|
||||
}
|
||||
}
|
||||
},
|
||||
uri: %Schema{type: :string, format: :uri},
|
||||
url: %Schema{type: :string, nullable: true, format: :uri},
|
||||
visibility: VisibilityScope
|
||||
},
|
||||
example: %{
|
||||
"account" => %{
|
||||
"acct" => "nick6",
|
||||
"avatar" => "http://localhost:4001/images/avi.png",
|
||||
"avatar_static" => "http://localhost:4001/images/avi.png",
|
||||
"bot" => false,
|
||||
"created_at" => "2020-04-07T19:48:51.000Z",
|
||||
"display_name" => "Test テスト User 6",
|
||||
"emojis" => [],
|
||||
"fields" => [],
|
||||
"followers_count" => 1,
|
||||
"following_count" => 0,
|
||||
"header" => "http://localhost:4001/images/banner.png",
|
||||
"header_static" => "http://localhost:4001/images/banner.png",
|
||||
"id" => "9toJCsKN7SmSf3aj5c",
|
||||
"locked" => false,
|
||||
"note" => "Tester Number 6",
|
||||
"pleroma" => %{
|
||||
"background_image" => nil,
|
||||
"confirmation_pending" => false,
|
||||
"hide_favorites" => true,
|
||||
"hide_followers" => false,
|
||||
"hide_followers_count" => false,
|
||||
"hide_follows" => false,
|
||||
"hide_follows_count" => false,
|
||||
"is_admin" => false,
|
||||
"is_moderator" => false,
|
||||
"relationship" => %{
|
||||
"blocked_by" => false,
|
||||
"blocking" => false,
|
||||
"domain_blocking" => false,
|
||||
"endorsed" => false,
|
||||
"followed_by" => false,
|
||||
"following" => true,
|
||||
"id" => "9toJCsKN7SmSf3aj5c",
|
||||
"muting" => false,
|
||||
"muting_notifications" => false,
|
||||
"requested" => false,
|
||||
"showing_reblogs" => true,
|
||||
"subscribing" => false
|
||||
},
|
||||
"skip_thread_containment" => false,
|
||||
"tags" => []
|
||||
},
|
||||
"source" => %{
|
||||
"fields" => [],
|
||||
"note" => "Tester Number 6",
|
||||
"pleroma" => %{"actor_type" => "Person", "discoverable" => false},
|
||||
"sensitive" => false
|
||||
},
|
||||
"statuses_count" => 1,
|
||||
"url" => "http://localhost:4001/users/nick6",
|
||||
"username" => "nick6"
|
||||
},
|
||||
"application" => %{"name" => "Web", "website" => nil},
|
||||
"bookmarked" => false,
|
||||
"card" => nil,
|
||||
"content" => "foobar",
|
||||
"created_at" => "2020-04-07T19:48:51.000Z",
|
||||
"emojis" => [],
|
||||
"favourited" => false,
|
||||
"favourites_count" => 0,
|
||||
"id" => "9toJCu5YZW7O7gfvH6",
|
||||
"in_reply_to_account_id" => nil,
|
||||
"in_reply_to_id" => nil,
|
||||
"language" => nil,
|
||||
"media_attachments" => [],
|
||||
"mentions" => [],
|
||||
"muted" => false,
|
||||
"pinned" => false,
|
||||
"pleroma" => %{
|
||||
"content" => %{"text/plain" => "foobar"},
|
||||
"conversation_id" => 345_972,
|
||||
"direct_conversation_id" => nil,
|
||||
"emoji_reactions" => [],
|
||||
"expires_at" => nil,
|
||||
"in_reply_to_account_acct" => nil,
|
||||
"local" => true,
|
||||
"spoiler_text" => %{"text/plain" => ""},
|
||||
"thread_muted" => false
|
||||
},
|
||||
"poll" => nil,
|
||||
"reblog" => nil,
|
||||
"reblogged" => false,
|
||||
"reblogs_count" => 0,
|
||||
"replies_count" => 0,
|
||||
"sensitive" => false,
|
||||
"spoiler_text" => "",
|
||||
"tags" => [],
|
||||
"uri" => "http://localhost:4001/objects/0f5dad44-0e9e-4610-b377-a2631e499190",
|
||||
"url" => "http://localhost:4001/notice/9toJCu5YZW7O7gfvH6",
|
||||
"visibility" => "private"
|
||||
}
|
||||
})
|
||||
end
|
||||
14
lib/pleroma/web/api_spec/schemas/visibility_scope.ex
Normal file
14
lib/pleroma/web/api_spec/schemas/visibility_scope.ex
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# 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.VisibilityScope do
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "VisibilityScope",
|
||||
description: "Status visibility",
|
||||
type: :string,
|
||||
enum: ["public", "unlisted", "private", "direct"]
|
||||
})
|
||||
end
|
||||
|
|
@ -84,14 +84,18 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
|||
%__MODULE__{draft | attachments: attachments}
|
||||
end
|
||||
|
||||
defp in_reply_to(draft) do
|
||||
case Map.get(draft.params, "in_reply_to_status_id") do
|
||||
"" -> draft
|
||||
nil -> draft
|
||||
id -> %__MODULE__{draft | in_reply_to: Activity.get_by_id(id)}
|
||||
end
|
||||
defp in_reply_to(%{params: %{"in_reply_to_status_id" => ""}} = draft), do: draft
|
||||
|
||||
defp in_reply_to(%{params: %{"in_reply_to_status_id" => id}} = draft) when is_binary(id) do
|
||||
%__MODULE__{draft | in_reply_to: Activity.get_by_id(id)}
|
||||
end
|
||||
|
||||
defp in_reply_to(%{params: %{"in_reply_to_status_id" => %Activity{} = in_reply_to}} = draft) do
|
||||
%__MODULE__{draft | in_reply_to: in_reply_to}
|
||||
end
|
||||
|
||||
defp in_reply_to(draft), do: draft
|
||||
|
||||
defp in_reply_to_conversation(draft) do
|
||||
in_reply_to_conversation = Participation.get(draft.params["in_reply_to_conversation_id"])
|
||||
%__MODULE__{draft | in_reply_to_conversation: in_reply_to_conversation}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
alias Pleroma.ActivityExpiration
|
||||
alias Pleroma.Conversation.Participation
|
||||
alias Pleroma.FollowingRelationship
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.ThreadMute
|
||||
alias Pleroma.User
|
||||
|
|
@ -61,6 +62,7 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
|
||||
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
|
||||
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
|
||||
{:ok, _notifications} <- Notification.dismiss(follow_activity),
|
||||
{:ok, _activity} <-
|
||||
ActivityPub.reject(%{
|
||||
to: [follower.ap_id],
|
||||
|
|
@ -86,8 +88,9 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
end
|
||||
end
|
||||
|
||||
def repeat(id_or_ap_id, user, params \\ %{}) do
|
||||
with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)},
|
||||
def repeat(id, user, params \\ %{}) do
|
||||
with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
|
||||
{:find_activity, Activity.get_by_id(id)},
|
||||
object <- Object.normalize(activity),
|
||||
announce_activity <- Utils.get_existing_announce(user.ap_id, object),
|
||||
public <- public_announce?(object, params) do
|
||||
|
|
@ -102,8 +105,9 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
end
|
||||
end
|
||||
|
||||
def unrepeat(id_or_ap_id, user) do
|
||||
with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)} do
|
||||
def unrepeat(id, user) do
|
||||
with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
|
||||
{:find_activity, Activity.get_by_id(id)} do
|
||||
object = Object.normalize(activity)
|
||||
ActivityPub.unannounce(user, object)
|
||||
else
|
||||
|
|
@ -160,8 +164,9 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
end
|
||||
end
|
||||
|
||||
def unfavorite(id_or_ap_id, user) do
|
||||
with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)} do
|
||||
def unfavorite(id, user) do
|
||||
with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
|
||||
{:find_activity, Activity.get_by_id(id)} do
|
||||
object = Object.normalize(activity)
|
||||
ActivityPub.unlike(user, object)
|
||||
else
|
||||
|
|
@ -332,32 +337,12 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
|
||||
defp maybe_create_activity_expiration(result, _), do: result
|
||||
|
||||
# Updates the emojis for a user based on their profile
|
||||
def update(user) do
|
||||
emoji = emoji_from_profile(user)
|
||||
source_data = Map.put(user.source_data, "tag", emoji)
|
||||
|
||||
user =
|
||||
case User.update_source_data(user, source_data) do
|
||||
{:ok, user} -> user
|
||||
_ -> user
|
||||
end
|
||||
|
||||
ActivityPub.update(%{
|
||||
local: true,
|
||||
to: [Pleroma.Constants.as_public(), user.follower_address],
|
||||
cc: [],
|
||||
actor: user.ap_id,
|
||||
object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
|
||||
})
|
||||
end
|
||||
|
||||
def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
|
||||
def pin(id, %{ap_id: user_ap_id} = user) do
|
||||
with %Activity{
|
||||
actor: ^user_ap_id,
|
||||
data: %{"type" => "Create"},
|
||||
object: %Object{data: %{"type" => object_type}}
|
||||
} = activity <- get_by_id_or_ap_id(id_or_ap_id),
|
||||
} = activity <- Activity.get_by_id_with_object(id),
|
||||
true <- object_type in ["Note", "Article", "Question"],
|
||||
true <- Visibility.is_public?(activity),
|
||||
{:ok, _user} <- User.add_pinnned_activity(user, activity) do
|
||||
|
|
@ -368,8 +353,8 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
end
|
||||
end
|
||||
|
||||
def unpin(id_or_ap_id, user) do
|
||||
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
|
||||
def unpin(id, user) do
|
||||
with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
|
||||
{:ok, _user} <- User.remove_pinnned_activity(user, activity) do
|
||||
{:ok, activity}
|
||||
else
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
|||
alias Pleroma.Activity
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.Conversation.Participation
|
||||
alias Pleroma.Emoji
|
||||
alias Pleroma.Formatter
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Plugs.AuthenticationPlug
|
||||
|
|
@ -18,30 +17,11 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
|||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.Endpoint
|
||||
alias Pleroma.Web.MediaProxy
|
||||
|
||||
require Logger
|
||||
require Pleroma.Constants
|
||||
|
||||
# This is a hack for twidere.
|
||||
def get_by_id_or_ap_id(id) do
|
||||
activity =
|
||||
with true <- FlakeId.flake_id?(id),
|
||||
%Activity{} = activity <- Activity.get_by_id_with_object(id) do
|
||||
activity
|
||||
else
|
||||
_ -> Activity.get_create_by_object_ap_id_with_object(id)
|
||||
end
|
||||
|
||||
activity &&
|
||||
if activity.data["type"] == "Create" do
|
||||
activity
|
||||
else
|
||||
Activity.get_create_by_object_ap_id_with_object(activity.data["object"])
|
||||
end
|
||||
end
|
||||
|
||||
def attachments_from_ids(%{"media_ids" => ids, "descriptions" => desc} = _) do
|
||||
attachments_from_ids_descs(ids, desc)
|
||||
end
|
||||
|
|
@ -175,7 +155,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
|||
"replies" => %{"type" => "Collection", "totalItems" => 0}
|
||||
}
|
||||
|
||||
{note, Map.merge(emoji, Emoji.Formatter.get_emoji_map(option))}
|
||||
{note, Map.merge(emoji, Pleroma.Emoji.Formatter.get_emoji_map(option))}
|
||||
end)
|
||||
|
||||
end_time =
|
||||
|
|
@ -431,19 +411,6 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
|||
end
|
||||
end
|
||||
|
||||
def emoji_from_profile(%User{bio: bio, name: name}) do
|
||||
[bio, name]
|
||||
|> Enum.map(&Emoji.Formatter.get_emoji/1)
|
||||
|> Enum.concat()
|
||||
|> Enum.map(fn {shortcode, %Emoji{file: path}} ->
|
||||
%{
|
||||
"type" => "Emoji",
|
||||
"icon" => %{"type" => "Image", "url" => "#{Endpoint.url()}#{path}"},
|
||||
"name" => ":#{shortcode}:"
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
def maybe_notify_to_recipients(
|
||||
recipients,
|
||||
%Activity{data: %{"to" => to, "type" => _type}} = _activity
|
||||
|
|
|
|||
|
|
@ -82,8 +82,9 @@ defmodule Pleroma.Web.ControllerHelper do
|
|||
end
|
||||
end
|
||||
|
||||
def assign_account_by_id(%{params: %{"id" => id}} = conn, _) do
|
||||
case Pleroma.User.get_cached_by_id(id) do
|
||||
def assign_account_by_id(conn, _) do
|
||||
# TODO: use `conn.params[:id]` only after moving to OpenAPI
|
||||
case Pleroma.User.get_cached_by_id(conn.params[:id] || conn.params["id"]) do
|
||||
%Pleroma.User{} = account -> assign(conn, :account, account)
|
||||
nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@
|
|||
|
||||
defmodule Fallback.RedirectController do
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
require Logger
|
||||
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.Metadata
|
||||
|
||||
|
|
|
|||
|
|
@ -72,19 +72,24 @@ defmodule Pleroma.Web.Federator do
|
|||
# actor shouldn't be acting on objects outside their own AP server.
|
||||
with {:ok, _user} <- ap_enabled_actor(params["actor"]),
|
||||
nil <- Activity.normalize(params["id"]),
|
||||
:ok <- Containment.contain_origin_from_id(params["actor"], params),
|
||||
{_, :ok} <-
|
||||
{:correct_origin?, Containment.contain_origin_from_id(params["actor"], params)},
|
||||
{:ok, activity} <- Transmogrifier.handle_incoming(params) do
|
||||
{:ok, activity}
|
||||
else
|
||||
{:correct_origin?, _} ->
|
||||
Logger.debug("Origin containment failure for #{params["id"]}")
|
||||
{:error, :origin_containment_failed}
|
||||
|
||||
%Activity{} ->
|
||||
Logger.debug("Already had #{params["id"]}")
|
||||
:error
|
||||
{:error, :already_present}
|
||||
|
||||
_e ->
|
||||
e ->
|
||||
# Just drop those for now
|
||||
Logger.debug("Unhandled activity")
|
||||
Logger.debug(Jason.encode!(params, pretty: true))
|
||||
:error
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ defmodule Pleroma.Web.Feed.FeedView do
|
|||
def pub_date(%DateTime{} = date), do: Timex.format!(date, "{RFC822}")
|
||||
|
||||
def prepare_activity(activity, opts \\ []) do
|
||||
object = activity_object(activity)
|
||||
object = Object.normalize(activity)
|
||||
|
||||
actor =
|
||||
if opts[:actor] do
|
||||
|
|
@ -33,7 +33,6 @@ defmodule Pleroma.Web.Feed.FeedView do
|
|||
%{
|
||||
activity: activity,
|
||||
data: Map.get(object, :data),
|
||||
object: object,
|
||||
actor: actor
|
||||
}
|
||||
end
|
||||
|
|
@ -68,9 +67,7 @@ defmodule Pleroma.Web.Feed.FeedView do
|
|||
|
||||
def last_activity(activities), do: List.last(activities)
|
||||
|
||||
def activity_object(activity), do: Object.normalize(activity)
|
||||
|
||||
def activity_title(%{data: %{"content" => content}}, opts \\ %{}) do
|
||||
def activity_title(%{"content" => content}, opts \\ %{}) do
|
||||
content
|
||||
|> Pleroma.Web.Metadata.Utils.scrub_html()
|
||||
|> Pleroma.Emoji.Formatter.demojify()
|
||||
|
|
@ -78,7 +75,7 @@ defmodule Pleroma.Web.Feed.FeedView do
|
|||
|> escape()
|
||||
end
|
||||
|
||||
def activity_content(%{data: %{"content" => content}}) do
|
||||
def activity_content(%{"content" => content}) do
|
||||
content
|
||||
|> String.replace(~r/[\n\r]/, "")
|
||||
|> escape()
|
||||
|
|
|
|||
|
|
@ -5,19 +5,25 @@
|
|||
defmodule Pleroma.Web.MastoFEController do
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
|
||||
alias Pleroma.Plugs.OAuthScopesPlug
|
||||
alias Pleroma.User
|
||||
|
||||
plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :put_settings)
|
||||
|
||||
# Note: :index action handles attempt of unauthenticated access to private instance with redirect
|
||||
plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action == :index)
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["read"], fallback: :proceed_unauthenticated, skip_instance_privacy_check: true}
|
||||
%{scopes: ["read"], fallback: :proceed_unauthenticated}
|
||||
when action == :index
|
||||
)
|
||||
|
||||
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action != :index)
|
||||
plug(
|
||||
:skip_plug,
|
||||
[OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :manifest
|
||||
)
|
||||
|
||||
@doc "GET /web/*path"
|
||||
def index(%{assigns: %{user: user, token: token}} = conn, _params)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
skip_relationships?: 1
|
||||
]
|
||||
|
||||
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
|
||||
alias Pleroma.Plugs.OAuthScopesPlug
|
||||
alias Pleroma.Plugs.RateLimiter
|
||||
alias Pleroma.User
|
||||
|
|
@ -21,20 +22,33 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.MastodonAPI.ListView
|
||||
alias Pleroma.Web.MastodonAPI.MastodonAPI
|
||||
alias Pleroma.Web.MastodonAPI.MastodonAPIController
|
||||
alias Pleroma.Web.MastodonAPI.StatusView
|
||||
alias Pleroma.Web.OAuth.Token
|
||||
alias Pleroma.Web.TwitterAPI.TwitterAPI
|
||||
|
||||
plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
|
||||
|
||||
plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create)
|
||||
|
||||
plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:show, :statuses])
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
|
||||
when action == :show
|
||||
when action in [:show, :followers, :following]
|
||||
)
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{fallback: :proceed_unauthenticated, scopes: ["read:statuses"]}
|
||||
when action == :statuses
|
||||
)
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["read:accounts"]}
|
||||
when action in [:endorsements, :verify_credentials, :followers, :following]
|
||||
when action in [:verify_credentials, :endorsements, :identity_proofs]
|
||||
)
|
||||
|
||||
plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)
|
||||
|
|
@ -53,21 +67,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
|
||||
plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
|
||||
|
||||
# Note: :follows (POST /api/v1/follows) is the same as :follow, consider removing :follows
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["follow", "write:follows"]} when action in [:follows, :follow, :unfollow]
|
||||
%{scopes: ["follow", "write:follows"]} when action in [:follow_by_uri, :follow, :unfollow]
|
||||
)
|
||||
|
||||
plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
|
||||
|
||||
plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
|
||||
|
||||
plug(
|
||||
Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
|
||||
when action not in [:create, :show, :statuses]
|
||||
)
|
||||
|
||||
@relationship_actions [:follow, :unfollow]
|
||||
@needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
|
||||
|
||||
|
|
@ -82,25 +90,26 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
|
||||
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||
|
||||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
|
||||
|
||||
@doc "POST /api/v1/accounts"
|
||||
def create(
|
||||
%{assigns: %{app: app}} = conn,
|
||||
%{"username" => nickname, "password" => _, "agreement" => true} = params
|
||||
) do
|
||||
def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
|
||||
params =
|
||||
params
|
||||
|> Map.take([
|
||||
"email",
|
||||
"captcha_solution",
|
||||
"captcha_token",
|
||||
"captcha_answer_data",
|
||||
"token",
|
||||
"password"
|
||||
:email,
|
||||
:bio,
|
||||
:captcha_solution,
|
||||
:captcha_token,
|
||||
:captcha_answer_data,
|
||||
:token,
|
||||
:password,
|
||||
:fullname
|
||||
])
|
||||
|> Map.put("nickname", nickname)
|
||||
|> Map.put("fullname", params["fullname"] || nickname)
|
||||
|> Map.put("bio", params["bio"] || "")
|
||||
|> Map.put("confirm", params["password"])
|
||||
|> Map.put(:nickname, params.username)
|
||||
|> Map.put(:fullname, Map.get(params, :fullname, params.username))
|
||||
|> Map.put(:confirm, params.password)
|
||||
|> Map.put(:trusted_app, app.trusted)
|
||||
|
||||
with :ok <- validate_email_param(params),
|
||||
{:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
|
||||
|
|
@ -124,7 +133,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
render_error(conn, :forbidden, "Invalid credentials")
|
||||
end
|
||||
|
||||
defp validate_email_param(%{"email" => _}), do: :ok
|
||||
defp validate_email_param(%{:email => email}) when not is_nil(email), do: :ok
|
||||
|
||||
defp validate_email_param(_) do
|
||||
case Pleroma.Config.get([:instance, :account_activation_required]) do
|
||||
|
|
@ -146,9 +155,14 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
end
|
||||
|
||||
@doc "PATCH /api/v1/accounts/update_credentials"
|
||||
def update_credentials(%{assigns: %{user: original_user}} = conn, params) do
|
||||
def update_credentials(%{assigns: %{user: original_user}, body_params: params} = conn, _params) do
|
||||
user = original_user
|
||||
|
||||
params =
|
||||
params
|
||||
|> Enum.filter(fn {_, value} -> not is_nil(value) end)
|
||||
|> Enum.into(%{})
|
||||
|
||||
user_params =
|
||||
[
|
||||
:no_rich_text,
|
||||
|
|
@ -164,28 +178,26 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
:discoverable
|
||||
]
|
||||
|> Enum.reduce(%{}, fn key, acc ->
|
||||
add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)})
|
||||
add_if_present(acc, params, key, key, &{:ok, truthy_param?(&1)})
|
||||
end)
|
||||
|> add_if_present(params, "display_name", :name)
|
||||
|> add_if_present(params, "note", :bio)
|
||||
|> add_if_present(params, "avatar", :avatar)
|
||||
|> add_if_present(params, "header", :banner)
|
||||
|> add_if_present(params, "pleroma_background_image", :background)
|
||||
|> add_if_present(params, :display_name, :name)
|
||||
|> add_if_present(params, :note, :bio)
|
||||
|> add_if_present(params, :avatar, :avatar)
|
||||
|> add_if_present(params, :header, :banner)
|
||||
|> add_if_present(params, :pleroma_background_image, :background)
|
||||
|> add_if_present(
|
||||
params,
|
||||
"fields_attributes",
|
||||
:fields_attributes,
|
||||
:raw_fields,
|
||||
&{:ok, normalize_fields_attributes(&1)}
|
||||
)
|
||||
|> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store)
|
||||
|> add_if_present(params, "default_scope", :default_scope)
|
||||
|> add_if_present(params, "actor_type", :actor_type)
|
||||
|> add_if_present(params, :pleroma_settings_store, :pleroma_settings_store)
|
||||
|> add_if_present(params, :default_scope, :default_scope)
|
||||
|> add_if_present(params, :actor_type, :actor_type)
|
||||
|
||||
changeset = User.update_changeset(user, user_params)
|
||||
|
||||
with {:ok, user} <- User.update_and_set_cache(changeset) do
|
||||
if original_user != user, do: CommonAPI.update(user)
|
||||
|
||||
render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
|
||||
else
|
||||
_e -> render_error(conn, :forbidden, "Invalid request")
|
||||
|
|
@ -194,7 +206,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
|
||||
defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
|
||||
with true <- Map.has_key?(params, params_field),
|
||||
{:ok, new_value} <- value_function.(params[params_field]) do
|
||||
{:ok, new_value} <- value_function.(Map.get(params, params_field)) do
|
||||
Map.put(map, map_field, new_value)
|
||||
else
|
||||
_ -> map
|
||||
|
|
@ -205,12 +217,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
if Enum.all?(fields, &is_tuple/1) do
|
||||
Enum.map(fields, fn {_, v} -> v end)
|
||||
else
|
||||
fields
|
||||
Enum.map(fields, fn
|
||||
%{} = field -> %{"name" => field.name, "value" => field.value}
|
||||
field -> field
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
@doc "GET /api/v1/accounts/relationships"
|
||||
def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
|
||||
def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
|
||||
targets = User.get_all_by_ids(List.wrap(id))
|
||||
|
||||
render(conn, "relationships.json", user: user, targets: targets)
|
||||
|
|
@ -220,7 +235,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
|
||||
|
||||
@doc "GET /api/v1/accounts/:id"
|
||||
def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
|
||||
def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
|
||||
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
|
||||
true <- User.visible_for?(user, for_user) do
|
||||
render(conn, "show.json", user: user, for: for_user)
|
||||
|
|
@ -231,12 +246,14 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
|
||||
@doc "GET /api/v1/accounts/:id/statuses"
|
||||
def statuses(%{assigns: %{user: reading_user}} = conn, params) do
|
||||
with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user),
|
||||
with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
|
||||
true <- User.visible_for?(user, reading_user) do
|
||||
params =
|
||||
params
|
||||
|> Map.put("tag", params["tagged"])
|
||||
|> Map.delete("godmode")
|
||||
|> Map.delete(:tagged)
|
||||
|> Enum.filter(&(not is_nil(&1)))
|
||||
|> Map.new(fn {key, value} -> {to_string(key), value} end)
|
||||
|> Map.put("tag", params[:tagged])
|
||||
|
||||
activities = ActivityPub.fetch_user_activities(user, reading_user, params)
|
||||
|
||||
|
|
@ -256,6 +273,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
|
||||
@doc "GET /api/v1/accounts/:id/followers"
|
||||
def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
|
||||
params =
|
||||
params
|
||||
|> Enum.map(fn {key, value} -> {to_string(key), value} end)
|
||||
|> Enum.into(%{})
|
||||
|
||||
followers =
|
||||
cond do
|
||||
for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
|
||||
|
|
@ -270,6 +292,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
|
||||
@doc "GET /api/v1/accounts/:id/following"
|
||||
def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
|
||||
params =
|
||||
params
|
||||
|> Enum.map(fn {key, value} -> {to_string(key), value} end)
|
||||
|> Enum.into(%{})
|
||||
|
||||
followers =
|
||||
cond do
|
||||
for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
|
||||
|
|
@ -293,11 +320,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
|
||||
@doc "POST /api/v1/accounts/:id/follow"
|
||||
def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
|
||||
{:error, :not_found}
|
||||
{:error, "Can not follow yourself"}
|
||||
end
|
||||
|
||||
def follow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
|
||||
with {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
|
||||
def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do
|
||||
with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
|
||||
render(conn, "relationship.json", user: follower, target: followed)
|
||||
else
|
||||
{:error, message} -> json_response(conn, :forbidden, %{error: message})
|
||||
|
|
@ -306,7 +333,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
|
||||
@doc "POST /api/v1/accounts/:id/unfollow"
|
||||
def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
|
||||
{:error, :not_found}
|
||||
{:error, "Can not unfollow yourself"}
|
||||
end
|
||||
|
||||
def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
|
||||
|
|
@ -316,10 +343,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
end
|
||||
|
||||
@doc "POST /api/v1/accounts/:id/mute"
|
||||
def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do
|
||||
notifications? = params |> Map.get("notifications", true) |> truthy_param?()
|
||||
|
||||
with {:ok, _user_relationships} <- User.mute(muter, muted, notifications?) do
|
||||
def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
|
||||
with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
|
||||
render(conn, "relationship.json", user: muter, target: muted)
|
||||
else
|
||||
{:error, message} -> json_response(conn, :forbidden, %{error: message})
|
||||
|
|
@ -356,14 +381,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
end
|
||||
|
||||
@doc "POST /api/v1/follows"
|
||||
def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
|
||||
with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
|
||||
{_, true} <- {:followed, follower.id != followed.id},
|
||||
{:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
|
||||
render(conn, "show.json", user: followed, for: follower)
|
||||
else
|
||||
{:followed, _} -> {:error, :not_found}
|
||||
{:error, message} -> json_response(conn, :forbidden, %{error: message})
|
||||
def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
|
||||
case User.get_cached_by_nickname(uri) do
|
||||
%User{} = user ->
|
||||
conn
|
||||
|> assign(:account, user)
|
||||
|> follow(%{})
|
||||
|
||||
nil ->
|
||||
{:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -380,6 +406,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
end
|
||||
|
||||
@doc "GET /api/v1/endorsements"
|
||||
def endorsements(conn, params),
|
||||
do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params)
|
||||
def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
|
||||
|
||||
@doc "GET /api/v1/identity_proofs"
|
||||
def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
defmodule Pleroma.Web.MastodonAPI.AppController do
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
|
||||
alias Pleroma.Plugs.OAuthScopesPlug
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.Web.OAuth.App
|
||||
|
|
@ -13,7 +14,14 @@ defmodule Pleroma.Web.MastodonAPI.AppController do
|
|||
|
||||
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||
|
||||
plug(
|
||||
:skip_plug,
|
||||
[OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug]
|
||||
when action == :create
|
||||
)
|
||||
|
||||
plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :verify_credentials)
|
||||
|
||||
plug(OpenApiSpex.Plug.CastAndValidate)
|
||||
|
||||
@local_mastodon_name "Mastodon-Local"
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
|
|||
|
||||
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||
|
||||
@local_mastodon_name "Mastodon-Local"
|
||||
|
||||
plug(Pleroma.Plugs.RateLimiter, [name: :password_reset] when action == :password_reset)
|
||||
|
||||
@local_mastodon_name "Mastodon-Local"
|
||||
|
||||
@doc "GET /web/login"
|
||||
def login(%{assigns: %{user: %User{}}} = conn, _params) do
|
||||
redirect(conn, to: local_mastodon_root_path(conn))
|
||||
|
|
|
|||
|
|
@ -14,9 +14,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationController do
|
|||
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||
|
||||
plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action == :index)
|
||||
plug(OAuthScopesPlug, %{scopes: ["write:conversations"]} when action == :read)
|
||||
|
||||
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
|
||||
plug(OAuthScopesPlug, %{scopes: ["write:conversations"]} when action != :index)
|
||||
|
||||
@doc "GET /api/v1/conversations"
|
||||
def index(%{assigns: %{user: user}} = conn, params) do
|
||||
|
|
@ -28,7 +26,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationController do
|
|||
end
|
||||
|
||||
@doc "POST /api/v1/conversations/:id/read"
|
||||
def read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
|
||||
def mark_as_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
|
||||
with %Participation{} = participation <-
|
||||
Repo.get_by(Participation, id: participation_id, user_id: user.id),
|
||||
{:ok, participation} <- Participation.mark_as_read(participation) do
|
||||
|
|
|
|||
|
|
@ -5,6 +5,16 @@
|
|||
defmodule Pleroma.Web.MastodonAPI.CustomEmojiController do
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
plug(OpenApiSpex.Plug.CastAndValidate)
|
||||
|
||||
plug(
|
||||
:skip_plug,
|
||||
[Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]
|
||||
when action == :index
|
||||
)
|
||||
|
||||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.CustomEmojiOperation
|
||||
|
||||
def index(conn, _params) do
|
||||
render(conn, "index.json", custom_emojis: Pleroma.Emoji.get_all())
|
||||
end
|
||||
|
|
|
|||
|
|
@ -21,8 +21,6 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockController do
|
|||
%{scopes: ["follow", "write:blocks"]} when action != :index
|
||||
)
|
||||
|
||||
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
|
||||
|
||||
@doc "GET /api/v1/domain_blocks"
|
||||
def index(%{assigns: %{user: user}} = conn, _) do
|
||||
json(conn, Map.get(user, :domain_blocks, []))
|
||||
|
|
|
|||
|
|
@ -17,8 +17,6 @@ defmodule Pleroma.Web.MastodonAPI.FilterController do
|
|||
%{scopes: ["write:filters"]} when action not in @oauth_read_actions
|
||||
)
|
||||
|
||||
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
|
||||
|
||||
@doc "GET /api/v1/filters"
|
||||
def index(%{assigns: %{user: user}} = conn, _) do
|
||||
filters = Filter.get_filters(user)
|
||||
|
|
|
|||
|
|
@ -21,8 +21,6 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestController do
|
|||
%{scopes: ["follow", "write:follows"]} when action != :index
|
||||
)
|
||||
|
||||
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
|
||||
|
||||
@doc "GET /api/v1/follow_requests"
|
||||
def index(%{assigns: %{user: followed}} = conn, _params) do
|
||||
follow_requests = User.get_follow_requests(followed)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,12 @@
|
|||
defmodule Pleroma.Web.MastodonAPI.InstanceController do
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
plug(
|
||||
:skip_plug,
|
||||
[Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]
|
||||
when action in [:show, :peers]
|
||||
)
|
||||
|
||||
@doc "GET /api/v1/instance"
|
||||
def show(conn, _params) do
|
||||
render(conn, "show.json")
|
||||
|
|
|
|||
|
|
@ -11,16 +11,16 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
|
|||
|
||||
plug(:list_by_id_and_user when action not in [:index, :create])
|
||||
|
||||
plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action in [:index, :show, :list_accounts])
|
||||
@oauth_read_actions [:index, :show, :list_accounts]
|
||||
|
||||
plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action in @oauth_read_actions)
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["write:lists"]}
|
||||
when action in [:create, :update, :delete, :add_to_list, :remove_from_list]
|
||||
when action not in @oauth_read_actions
|
||||
)
|
||||
|
||||
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
|
||||
|
||||
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||
|
||||
# GET /api/v1/lists
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.MarkerController do
|
|||
)
|
||||
|
||||
plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action == :upsert)
|
||||
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
|
||||
|
||||
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||
|
||||
# GET /api/v1/markers
|
||||
|
|
|
|||
|
|
@ -3,21 +3,33 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
||||
@moduledoc """
|
||||
Contains stubs for unimplemented Mastodon API endpoints.
|
||||
|
||||
Note: instead of routing directly to this controller's action,
|
||||
it's preferable to define an action in relevant (non-generic) controller,
|
||||
set up OAuth rules for it and call this controller's function from it.
|
||||
"""
|
||||
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
require Logger
|
||||
|
||||
plug(
|
||||
:skip_plug,
|
||||
[Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]
|
||||
when action in [:empty_array, :empty_object]
|
||||
)
|
||||
|
||||
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||
|
||||
# Stubs for unimplemented mastodon api
|
||||
#
|
||||
def empty_array(conn, _) do
|
||||
Logger.debug("Unimplemented, returning an empty array")
|
||||
Logger.debug("Unimplemented, returning an empty array (list)")
|
||||
json(conn, [])
|
||||
end
|
||||
|
||||
def empty_object(conn, _) do
|
||||
Logger.debug("Unimplemented, returning an empty object")
|
||||
Logger.debug("Unimplemented, returning an empty object (map)")
|
||||
json(conn, %{})
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -15,8 +15,6 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do
|
|||
|
||||
plug(OAuthScopesPlug, %{scopes: ["write:media"]})
|
||||
|
||||
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
|
||||
|
||||
@doc "POST /api/v1/media"
|
||||
def create(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
|
||||
with {:ok, object} <-
|
||||
|
|
|
|||
|
|
@ -20,8 +20,6 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
|
|||
|
||||
plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action not in @oauth_read_actions)
|
||||
|
||||
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
|
||||
|
||||
# GET /api/v1/notifications
|
||||
def index(conn, %{"account_id" => account_id} = params) do
|
||||
case Pleroma.User.get_cached_by_id(account_id) do
|
||||
|
|
|
|||
|
|
@ -22,8 +22,6 @@ defmodule Pleroma.Web.MastodonAPI.PollController do
|
|||
|
||||
plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action == :vote)
|
||||
|
||||
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
|
||||
|
||||
@doc "GET /api/v1/polls/:id"
|
||||
def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do
|
||||
with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@ defmodule Pleroma.Web.MastodonAPI.ReportController do
|
|||
|
||||
plug(OAuthScopesPlug, %{scopes: ["write:reports"]} when action == :create)
|
||||
|
||||
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
|
||||
|
||||
@doc "POST /api/v1/reports"
|
||||
def create(%{assigns: %{user: user}} = conn, params) do
|
||||
with {:ok, activity} <- Pleroma.Web.CommonAPI.report(user, params) do
|
||||
|
|
|
|||
|
|
@ -18,8 +18,6 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityController do
|
|||
plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in @oauth_read_actions)
|
||||
plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action not in @oauth_read_actions)
|
||||
|
||||
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
|
||||
|
||||
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||
|
||||
@doc "GET /api/v1/scheduled_statuses"
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
|
|||
# Note: Mastodon doesn't allow unauthenticated access (requires read:accounts / read:search)
|
||||
plug(OAuthScopesPlug, %{scopes: ["read:search"], fallback: :proceed_unauthenticated})
|
||||
|
||||
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
|
||||
# Note: on private instances auth is required (EnsurePublicOrAuthenticatedPlug is not skipped)
|
||||
|
||||
plug(RateLimiter, [name: :search] when action in [:search, :search2, :account_search])
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|
|||
alias Pleroma.Web.MastodonAPI.AccountView
|
||||
alias Pleroma.Web.MastodonAPI.ScheduledActivityView
|
||||
|
||||
plug(:skip_plug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action in [:index, :show])
|
||||
|
||||
@unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
|
||||
|
||||
plug(
|
||||
|
|
@ -77,8 +79,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|
|||
%{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark]
|
||||
)
|
||||
|
||||
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action not in [:index, :show])
|
||||
|
||||
@rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a
|
||||
|
||||
plug(
|
||||
|
|
@ -127,7 +127,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|
|||
def create(
|
||||
%{assigns: %{user: user}} = conn,
|
||||
%{"status" => _, "scheduled_at" => scheduled_at} = params
|
||||
) do
|
||||
)
|
||||
when not is_nil(scheduled_at) do
|
||||
params = Map.put(params, "in_reply_to_status_id", params["in_reply_to_id"])
|
||||
|
||||
with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)},
|
||||
|
|
@ -357,7 +358,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|
|||
end
|
||||
|
||||
@doc "GET /api/v1/favourites"
|
||||
def favourites(%{assigns: %{user: user}} = conn, params) do
|
||||
def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
|
||||
activities =
|
||||
ActivityPub.fetch_favourites(
|
||||
user,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do
|
|||
@moduledoc "The module represents functions to manage user subscriptions."
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
alias Pleroma.Web.MastodonAPI.PushSubscriptionView, as: View
|
||||
alias Pleroma.Web.Push
|
||||
alias Pleroma.Web.Push.Subscription
|
||||
|
||||
|
|
@ -14,17 +13,15 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do
|
|||
|
||||
plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["push"]})
|
||||
|
||||
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
|
||||
plug(:restrict_push_enabled)
|
||||
|
||||
# Creates PushSubscription
|
||||
# POST /api/v1/push/subscription
|
||||
#
|
||||
def create(%{assigns: %{user: user, token: token}} = conn, params) do
|
||||
with true <- Push.enabled(),
|
||||
{:ok, _} <- Subscription.delete_if_exists(user, token),
|
||||
with {:ok, _} <- Subscription.delete_if_exists(user, token),
|
||||
{:ok, subscription} <- Subscription.create(user, token, params) do
|
||||
view = View.render("push_subscription.json", subscription: subscription)
|
||||
json(conn, view)
|
||||
render(conn, "show.json", subscription: subscription)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -32,10 +29,8 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do
|
|||
# GET /api/v1/push/subscription
|
||||
#
|
||||
def get(%{assigns: %{user: user, token: token}} = conn, _params) do
|
||||
with true <- Push.enabled(),
|
||||
{:ok, subscription} <- Subscription.get(user, token) do
|
||||
view = View.render("push_subscription.json", subscription: subscription)
|
||||
json(conn, view)
|
||||
with {:ok, subscription} <- Subscription.get(user, token) do
|
||||
render(conn, "show.json", subscription: subscription)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -43,10 +38,8 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do
|
|||
# PUT /api/v1/push/subscription
|
||||
#
|
||||
def update(%{assigns: %{user: user, token: token}} = conn, params) do
|
||||
with true <- Push.enabled(),
|
||||
{:ok, subscription} <- Subscription.update(user, token, params) do
|
||||
view = View.render("push_subscription.json", subscription: subscription)
|
||||
json(conn, view)
|
||||
with {:ok, subscription} <- Subscription.update(user, token, params) do
|
||||
render(conn, "show.json", subscription: subscription)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -54,11 +47,20 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do
|
|||
# DELETE /api/v1/push/subscription
|
||||
#
|
||||
def delete(%{assigns: %{user: user, token: token}} = conn, _params) do
|
||||
with true <- Push.enabled(),
|
||||
{:ok, _response} <- Subscription.delete(user, token),
|
||||
with {:ok, _response} <- Subscription.delete(user, token),
|
||||
do: json(conn, %{})
|
||||
end
|
||||
|
||||
defp restrict_push_enabled(conn, _) do
|
||||
if Push.enabled() do
|
||||
conn
|
||||
else
|
||||
conn
|
||||
|> render_error(:forbidden, "Web push subscription is disabled on this Pleroma instance")
|
||||
|> halt()
|
||||
end
|
||||
end
|
||||
|
||||
# fallback action
|
||||
#
|
||||
def errors(conn, {:error, :not_found}) do
|
||||
|
|
|
|||
|
|
@ -5,10 +5,13 @@
|
|||
defmodule Pleroma.Web.MastodonAPI.SuggestionController do
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
alias Pleroma.Plugs.OAuthScopesPlug
|
||||
|
||||
require Logger
|
||||
|
||||
plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :index)
|
||||
|
||||
@doc "GET /api/v1/suggestions"
|
||||
def index(conn, _) do
|
||||
json(conn, [])
|
||||
end
|
||||
def index(conn, params),
|
||||
do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,11 +9,14 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|
|||
only: [add_link_headers: 2, add_link_headers: 3, truthy_param?: 1, skip_relationships?: 1]
|
||||
|
||||
alias Pleroma.Pagination
|
||||
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
|
||||
alias Pleroma.Plugs.OAuthScopesPlug
|
||||
alias Pleroma.Plugs.RateLimiter
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
|
||||
plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:public, :hashtag])
|
||||
|
||||
# TODO: Replace with a macro when there is a Phoenix release with the following commit in it:
|
||||
# https://github.com/phoenixframework/phoenix/commit/2e8c63c01fec4dde5467dbbbf9705ff9e780735e
|
||||
|
||||
|
|
@ -26,7 +29,11 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|
|||
plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in [:home, :direct])
|
||||
plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :list)
|
||||
|
||||
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action != :public)
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["read:statuses"], fallback: :proceed_unauthenticated}
|
||||
when action in [:public, :hashtag]
|
||||
)
|
||||
|
||||
plug(:put_view, Pleroma.Web.MastodonAPI.StatusView)
|
||||
|
||||
|
|
@ -37,6 +44,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|
|||
|> Map.put("type", ["Create", "Announce"])
|
||||
|> Map.put("blocking_user", user)
|
||||
|> Map.put("muting_user", user)
|
||||
|> Map.put("reply_filtering_user", user)
|
||||
|> Map.put("user", user)
|
||||
|
||||
recipients = [user.ap_id | User.following(user)]
|
||||
|
|
@ -93,13 +101,16 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|
|||
|
||||
restrict? = Pleroma.Config.get([:restrict_unauthenticated, :timelines, cfg_key])
|
||||
|
||||
if not (restrict? and is_nil(user)) do
|
||||
if restrict? and is_nil(user) do
|
||||
render_error(conn, :unauthorized, "authorization required for timeline view")
|
||||
else
|
||||
activities =
|
||||
params
|
||||
|> Map.put("type", ["Create", "Announce"])
|
||||
|> Map.put("local_only", local_only)
|
||||
|> Map.put("blocking_user", user)
|
||||
|> Map.put("muting_user", user)
|
||||
|> Map.put("reply_filtering_user", user)
|
||||
|> ActivityPub.fetch_public_activities()
|
||||
|
||||
conn
|
||||
|
|
@ -110,12 +121,10 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|
|||
as: :activity,
|
||||
skip_relationships: skip_relationships?(params)
|
||||
)
|
||||
else
|
||||
render_error(conn, :unauthorized, "authorization required for timeline view")
|
||||
end
|
||||
end
|
||||
|
||||
def hashtag_fetching(params, user, local_only) do
|
||||
defp hashtag_fetching(params, user, local_only) do
|
||||
tags =
|
||||
[params["tag"], params["any"]]
|
||||
|> List.flatten()
|
||||
|
|
|
|||
|
|
@ -181,13 +181,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|
|||
bot = user.actor_type in ["Application", "Service"]
|
||||
|
||||
emojis =
|
||||
(user.source_data["tag"] || [])
|
||||
|> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
|
||||
|> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
|
||||
Enum.map(user.emoji, fn {shortcode, url} ->
|
||||
%{
|
||||
"shortcode" => String.trim(name, ":"),
|
||||
"url" => MediaProxy.url(url),
|
||||
"static_url" => MediaProxy.url(url),
|
||||
"shortcode" => shortcode,
|
||||
"url" => url,
|
||||
"static_url" => url,
|
||||
"visible_in_picker" => false
|
||||
}
|
||||
end)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,21 @@ defmodule Pleroma.Web.MastodonAPI.AppView do
|
|||
|
||||
alias Pleroma.Web.OAuth.App
|
||||
|
||||
def render("index.json", %{apps: apps, count: count, page_size: page_size, admin: true}) do
|
||||
%{
|
||||
apps: render_many(apps, Pleroma.Web.MastodonAPI.AppView, "show.json", %{admin: true}),
|
||||
count: count,
|
||||
page_size: page_size
|
||||
}
|
||||
end
|
||||
|
||||
def render("show.json", %{admin: true, app: %App{} = app} = assigns) do
|
||||
"show.json"
|
||||
|> render(Map.delete(assigns, :admin))
|
||||
|> Map.put(:trusted, app.trusted)
|
||||
|> Map.put(:id, app.id)
|
||||
end
|
||||
|
||||
def render("show.json", %{app: %App{} = app}) do
|
||||
%{
|
||||
id: app.id |> to_string,
|
||||
|
|
|
|||
|
|
@ -117,14 +117,14 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
|
|||
# Note: :skip_relationships option being applied to _account_ rendering (here)
|
||||
put_target(response, activity, reading_user, render_opts)
|
||||
|
||||
"follow" ->
|
||||
response
|
||||
|
||||
"pleroma:emoji_reaction" ->
|
||||
response
|
||||
|> put_status(parent_activity_fn.(), reading_user, render_opts)
|
||||
|> put_emoji(activity)
|
||||
|
||||
type when type in ["follow", "follow_request"] ->
|
||||
response
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ defmodule Pleroma.Web.MastodonAPI.PollView do
|
|||
expired: expired,
|
||||
multiple: multiple,
|
||||
votes_count: votes_count,
|
||||
voters_count: (multiple || nil) && voters_count(object),
|
||||
options: options,
|
||||
voted: voted?(params),
|
||||
emojis: Pleroma.Web.MastodonAPI.StatusView.build_emojis(object.data["emoji"])
|
||||
|
|
@ -62,6 +63,12 @@ defmodule Pleroma.Web.MastodonAPI.PollView do
|
|||
end)
|
||||
end
|
||||
|
||||
defp voters_count(%{data: %{"voters" => [_ | _] = voters}}) do
|
||||
length(voters)
|
||||
end
|
||||
|
||||
defp voters_count(_), do: 0
|
||||
|
||||
defp voted?(%{object: object} = opts) do
|
||||
if opts[:for] do
|
||||
existing_votes = Pleroma.Web.ActivityPub.Utils.get_existing_votes(opts[:for].ap_id, object)
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
end)
|
||||
end
|
||||
|
||||
defp get_user(ap_id) do
|
||||
def get_user(ap_id, fake_record_fallback \\ true) do
|
||||
cond do
|
||||
user = User.get_cached_by_ap_id(ap_id) ->
|
||||
user
|
||||
|
|
@ -53,8 +53,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
user = User.get_by_guessed_nickname(ap_id) ->
|
||||
user
|
||||
|
||||
true ->
|
||||
fake_record_fallback ->
|
||||
# TODO: refactor (fake records is never a good idea)
|
||||
User.error_user(ap_id)
|
||||
|
||||
true ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -97,7 +101,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
UserRelationship.view_relationships_option(nil, [])
|
||||
|
||||
true ->
|
||||
actors = Enum.map(activities ++ parent_activities, &get_user(&1.data["actor"]))
|
||||
# Note: unresolved users are filtered out
|
||||
actors =
|
||||
(activities ++ parent_activities)
|
||||
|> Enum.map(&get_user(&1.data["actor"], false))
|
||||
|> Enum.filter(& &1)
|
||||
|
||||
UserRelationship.view_relationships_option(reading_user, actors,
|
||||
source_mutes_only: opts[:skip_relationships]
|
||||
|
|
@ -521,11 +529,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
"""
|
||||
@spec build_tags(list(any())) :: list(map())
|
||||
def build_tags(object_tags) when is_list(object_tags) do
|
||||
object_tags = for tag when is_binary(tag) <- object_tags, do: tag
|
||||
|
||||
Enum.reduce(object_tags, [], fn tag, tags ->
|
||||
tags ++ [%{name: tag, url: "/tag/#{URI.encode(tag)}"}]
|
||||
end)
|
||||
object_tags
|
||||
|> Enum.filter(&is_binary/1)
|
||||
|> Enum.map(&%{name: &1, url: "/tag/#{URI.encode(&1)}"})
|
||||
end
|
||||
|
||||
def build_tags(_), do: []
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.MastodonAPI.PushSubscriptionView do
|
||||
defmodule Pleroma.Web.MastodonAPI.SubscriptionView do
|
||||
use Pleroma.Web, :view
|
||||
alias Pleroma.Web.Push
|
||||
|
||||
def render("push_subscription.json", %{subscription: subscription}) do
|
||||
def render("show.json", %{subscription: subscription}) do
|
||||
%{
|
||||
id: to_string(subscription.id),
|
||||
endpoint: subscription.endpoint,
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
defmodule Pleroma.Web.MediaProxy.MediaProxyController do
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
alias Pleroma.ReverseProxy
|
||||
alias Pleroma.Web.MediaProxy
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ defmodule Pleroma.Web.MongooseIM.MongooseIMController do
|
|||
plug(RateLimiter, [name: :authentication, params: ["user"]] when action == :check_password)
|
||||
|
||||
def user_exists(conn, %{"user" => username}) do
|
||||
with %User{} <- Repo.get_by(User, nickname: username, local: true) do
|
||||
with %User{} <- Repo.get_by(User, nickname: username, local: true, deactivated: false) do
|
||||
conn
|
||||
|> json(true)
|
||||
else
|
||||
|
|
@ -26,7 +26,7 @@ defmodule Pleroma.Web.MongooseIM.MongooseIMController do
|
|||
end
|
||||
|
||||
def check_password(conn, %{"user" => username, "pass" => password}) do
|
||||
with %User{password_hash: password_hash} <-
|
||||
with %User{password_hash: password_hash, deactivated: false} <-
|
||||
Repo.get_by(User, nickname: username, local: true),
|
||||
true <- Pbkdf2.checkpw(password, password_hash) do
|
||||
conn
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
defmodule Pleroma.Web.OAuth.App do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
import Ecto.Query
|
||||
alias Pleroma.Repo
|
||||
|
||||
@type t :: %__MODULE__{}
|
||||
|
|
@ -16,14 +17,24 @@ defmodule Pleroma.Web.OAuth.App do
|
|||
field(:website, :string)
|
||||
field(:client_id, :string)
|
||||
field(:client_secret, :string)
|
||||
field(:trusted, :boolean, default: false)
|
||||
|
||||
has_many(:oauth_authorizations, Pleroma.Web.OAuth.Authorization, on_delete: :delete_all)
|
||||
has_many(:oauth_tokens, Pleroma.Web.OAuth.Token, on_delete: :delete_all)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@spec changeset(App.t(), map()) :: Ecto.Changeset.t()
|
||||
def changeset(struct, params) do
|
||||
cast(struct, params, [:client_name, :redirect_uris, :scopes, :website, :trusted])
|
||||
end
|
||||
|
||||
@spec register_changeset(App.t(), map()) :: Ecto.Changeset.t()
|
||||
def register_changeset(struct, params \\ %{}) do
|
||||
changeset =
|
||||
struct
|
||||
|> cast(params, [:client_name, :redirect_uris, :scopes, :website])
|
||||
|> changeset(params)
|
||||
|> validate_required([:client_name, :redirect_uris, :scopes])
|
||||
|
||||
if changeset.valid? do
|
||||
|
|
@ -41,6 +52,21 @@ defmodule Pleroma.Web.OAuth.App do
|
|||
end
|
||||
end
|
||||
|
||||
@spec create(map()) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
|
||||
def create(params) do
|
||||
with changeset <- __MODULE__.register_changeset(%__MODULE__{}, params) do
|
||||
Repo.insert(changeset)
|
||||
end
|
||||
end
|
||||
|
||||
@spec update(map()) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
|
||||
def update(params) do
|
||||
with %__MODULE__{} = app <- Repo.get(__MODULE__, params["id"]),
|
||||
changeset <- changeset(app, params) do
|
||||
Repo.update(changeset)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets app by attrs or create new with attrs.
|
||||
And updates the scopes if need.
|
||||
|
|
@ -65,4 +91,58 @@ defmodule Pleroma.Web.OAuth.App do
|
|||
|> change(%{scopes: scopes})
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@spec search(map()) :: {:ok, [App.t()], non_neg_integer()}
|
||||
def search(params) do
|
||||
query = from(a in __MODULE__)
|
||||
|
||||
query =
|
||||
if params[:client_name] do
|
||||
from(a in query, where: a.client_name == ^params[:client_name])
|
||||
else
|
||||
query
|
||||
end
|
||||
|
||||
query =
|
||||
if params[:client_id] do
|
||||
from(a in query, where: a.client_id == ^params[:client_id])
|
||||
else
|
||||
query
|
||||
end
|
||||
|
||||
query =
|
||||
if Map.has_key?(params, :trusted) do
|
||||
from(a in query, where: a.trusted == ^params[:trusted])
|
||||
else
|
||||
query
|
||||
end
|
||||
|
||||
query =
|
||||
from(u in query,
|
||||
limit: ^params[:page_size],
|
||||
offset: ^((params[:page] - 1) * params[:page_size])
|
||||
)
|
||||
|
||||
count = Repo.aggregate(__MODULE__, :count, :id)
|
||||
|
||||
{:ok, Repo.all(query), count}
|
||||
end
|
||||
|
||||
@spec destroy(pos_integer()) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
|
||||
def destroy(id) do
|
||||
with %__MODULE__{} = app <- Repo.get(__MODULE__, id) do
|
||||
Repo.delete(app)
|
||||
end
|
||||
end
|
||||
|
||||
@spec errors(Ecto.Changeset.t()) :: map()
|
||||
def errors(changeset) do
|
||||
Enum.reduce(changeset.errors, %{}, fn
|
||||
{:client_name, {error, _}}, acc ->
|
||||
Map.put(acc, :name, error)
|
||||
|
||||
{key, {error, _}}, acc ->
|
||||
Map.put(acc, key, error)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -25,6 +25,9 @@ defmodule Pleroma.Web.OAuth.OAuthController do
|
|||
|
||||
plug(:fetch_session)
|
||||
plug(:fetch_flash)
|
||||
|
||||
plug(:skip_plug, [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug])
|
||||
|
||||
plug(RateLimiter, [name: :authentication] when action == :create_authorization)
|
||||
|
||||
action_fallback(Pleroma.Web.OAuth.FallbackController)
|
||||
|
|
|
|||
|
|
@ -17,12 +17,8 @@ defmodule Pleroma.Web.OAuth.Scopes do
|
|||
"""
|
||||
@spec fetch_scopes(map() | struct(), list()) :: list()
|
||||
|
||||
def fetch_scopes(%Pleroma.Web.ApiSpec.Schemas.AppCreateRequest{scopes: scopes}, default) do
|
||||
parse_scopes(scopes, default)
|
||||
end
|
||||
|
||||
def fetch_scopes(params, default) do
|
||||
parse_scopes(params["scope"] || params["scopes"], default)
|
||||
parse_scopes(params["scope"] || params["scopes"] || params[:scopes], default)
|
||||
end
|
||||
|
||||
def parse_scopes(scopes, _default) when is_list(scopes) do
|
||||
|
|
|
|||
|
|
@ -9,15 +9,20 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
|
|||
only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2, skip_relationships?: 1]
|
||||
|
||||
alias Ecto.Changeset
|
||||
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
|
||||
alias Pleroma.Plugs.OAuthScopesPlug
|
||||
alias Pleroma.Plugs.RateLimiter
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.MastodonAPI.StatusView
|
||||
|
||||
require Pleroma.Constants
|
||||
|
||||
plug(
|
||||
:skip_plug,
|
||||
[OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :confirmation_resend
|
||||
)
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["follow", "write:follows"]} when action in [:subscribe, :unsubscribe]
|
||||
|
|
@ -34,15 +39,13 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
|
|||
]
|
||||
)
|
||||
|
||||
plug(OAuthScopesPlug, %{scopes: ["read:favourites"]} when action == :favourites)
|
||||
|
||||
# An extra safety measure for possible actions not guarded by OAuth permissions specification
|
||||
plug(
|
||||
Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
|
||||
when action != :confirmation_resend
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["read:favourites"], fallback: :proceed_unauthenticated} when action == :favourites
|
||||
)
|
||||
|
||||
plug(RateLimiter, [name: :account_confirmation_resend] when action == :confirmation_resend)
|
||||
|
||||
plug(:assign_account_by_id when action in [:favourites, :subscribe, :unsubscribe])
|
||||
plug(:put_view, Pleroma.Web.MastodonAPI.AccountView)
|
||||
|
||||
|
|
@ -58,38 +61,32 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
|
|||
|
||||
@doc "PATCH /api/v1/pleroma/accounts/update_avatar"
|
||||
def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
|
||||
{:ok, user} =
|
||||
{:ok, _user} =
|
||||
user
|
||||
|> Changeset.change(%{avatar: nil})
|
||||
|> User.update_and_set_cache()
|
||||
|
||||
CommonAPI.update(user)
|
||||
|
||||
json(conn, %{url: nil})
|
||||
end
|
||||
|
||||
def update_avatar(%{assigns: %{user: user}} = conn, params) do
|
||||
{:ok, %{data: data}} = ActivityPub.upload(params, type: :avatar)
|
||||
{:ok, user} = user |> Changeset.change(%{avatar: data}) |> User.update_and_set_cache()
|
||||
{:ok, _user} = user |> Changeset.change(%{avatar: data}) |> User.update_and_set_cache()
|
||||
%{"url" => [%{"href" => href} | _]} = data
|
||||
|
||||
CommonAPI.update(user)
|
||||
|
||||
json(conn, %{url: href})
|
||||
end
|
||||
|
||||
@doc "PATCH /api/v1/pleroma/accounts/update_banner"
|
||||
def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
|
||||
with {:ok, user} <- User.update_banner(user, %{}) do
|
||||
CommonAPI.update(user)
|
||||
with {:ok, _user} <- User.update_banner(user, %{}) do
|
||||
json(conn, %{url: nil})
|
||||
end
|
||||
end
|
||||
|
||||
def update_banner(%{assigns: %{user: user}} = conn, params) do
|
||||
with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
|
||||
{:ok, user} <- User.update_banner(user, object.data) do
|
||||
CommonAPI.update(user)
|
||||
{:ok, _user} <- User.update_banner(user, object.data) do
|
||||
%{"url" => [%{"href" => href} | _]} = object.data
|
||||
|
||||
json(conn, %{url: href})
|
||||
|
|
|
|||
|
|
@ -1,191 +1,93 @@
|
|||
defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
alias Pleroma.Plugs.OAuthScopesPlug
|
||||
|
||||
require Logger
|
||||
alias Pleroma.Emoji.Pack
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
Pleroma.Plugs.OAuthScopesPlug,
|
||||
%{scopes: ["write"], admin: true}
|
||||
when action in [
|
||||
:import_from_filesystem,
|
||||
:remote,
|
||||
:download,
|
||||
:create,
|
||||
:update,
|
||||
:delete,
|
||||
:download_from,
|
||||
:list_from,
|
||||
:import_from_fs,
|
||||
:add_file,
|
||||
:update_file,
|
||||
:update_metadata
|
||||
:delete_file
|
||||
]
|
||||
)
|
||||
|
||||
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
|
||||
plug(
|
||||
:skip_plug,
|
||||
[Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug]
|
||||
when action in [:archive, :show, :list]
|
||||
)
|
||||
|
||||
def emoji_dir_path do
|
||||
Path.join(
|
||||
Pleroma.Config.get!([:instance, :static_dir]),
|
||||
"emoji"
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Lists packs from the remote instance.
|
||||
|
||||
Since JS cannot ask remote instances for their packs due to CPS, it has to
|
||||
be done by the server
|
||||
"""
|
||||
def list_from(conn, %{"instance_address" => address}) do
|
||||
address = String.trim(address)
|
||||
|
||||
if shareable_packs_available(address) do
|
||||
list_resp =
|
||||
"#{address}/api/pleroma/emoji/packs" |> Tesla.get!() |> Map.get(:body) |> Jason.decode!()
|
||||
|
||||
json(conn, list_resp)
|
||||
def remote(conn, %{"url" => url}) do
|
||||
with {:ok, packs} <- Pack.list_remote(url) do
|
||||
json(conn, packs)
|
||||
else
|
||||
conn
|
||||
|> put_status(:internal_server_error)
|
||||
|> json(%{error: "The requested instance does not support sharing emoji packs"})
|
||||
{:shareable, _} ->
|
||||
conn
|
||||
|> put_status(:internal_server_error)
|
||||
|> json(%{error: "The requested instance does not support sharing emoji packs"})
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Lists the packs available on the instance as JSON.
|
||||
def list(conn, _params) do
|
||||
emoji_path =
|
||||
Path.join(
|
||||
Pleroma.Config.get!([:instance, :static_dir]),
|
||||
"emoji"
|
||||
)
|
||||
|
||||
The information is public and does not require authentication. The format is
|
||||
a map of "pack directory name" to pack.json contents.
|
||||
"""
|
||||
def list_packs(conn, _params) do
|
||||
# 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_dir_path())},
|
||||
{:ls, {:ok, results}} <- {:ls, File.ls(emoji_dir_path())} do
|
||||
pack_infos =
|
||||
results
|
||||
|> Enum.filter(&has_pack_json?/1)
|
||||
|> Enum.map(&load_pack/1)
|
||||
# Check if all the files are in place and can be sent
|
||||
|> Enum.map(&validate_pack/1)
|
||||
# Transform into a map of pack-name => pack-data
|
||||
|> Enum.into(%{})
|
||||
|
||||
json(conn, pack_infos)
|
||||
with {:ok, packs} <- Pack.list_local() do
|
||||
json(conn, packs)
|
||||
else
|
||||
{:create_dir, {:error, e}} ->
|
||||
conn
|
||||
|> put_status(:internal_server_error)
|
||||
|> json(%{error: "Failed to create the emoji pack directory at #{emoji_dir_path()}: #{e}"})
|
||||
|> json(%{error: "Failed to create the emoji pack directory at #{emoji_path}: #{e}"})
|
||||
|
||||
{:ls, {:error, e}} ->
|
||||
conn
|
||||
|> put_status(:internal_server_error)
|
||||
|> json(%{
|
||||
error:
|
||||
"Failed to get the contents of the emoji pack directory at #{emoji_dir_path()}: #{e}"
|
||||
error: "Failed to get the contents of the emoji pack directory at #{emoji_path}: #{e}"
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
defp has_pack_json?(file) do
|
||||
dir_path = Path.join(emoji_dir_path(), file)
|
||||
# Filter to only use the pack.json packs
|
||||
File.dir?(dir_path) and File.exists?(Path.join(dir_path, "pack.json"))
|
||||
end
|
||||
def show(conn, %{"name" => name}) do
|
||||
name = String.trim(name)
|
||||
|
||||
defp load_pack(pack_name) do
|
||||
pack_path = Path.join(emoji_dir_path(), pack_name)
|
||||
pack_file = Path.join(pack_path, "pack.json")
|
||||
|
||||
{pack_name, Jason.decode!(File.read!(pack_file))}
|
||||
end
|
||||
|
||||
defp validate_pack({name, pack}) do
|
||||
pack_path = Path.join(emoji_dir_path(), name)
|
||||
|
||||
if can_download?(pack, pack_path) do
|
||||
archive_for_sha = make_archive(name, pack, pack_path)
|
||||
archive_sha = :crypto.hash(:sha256, archive_for_sha) |> Base.encode16()
|
||||
|
||||
pack =
|
||||
pack
|
||||
|> put_in(["pack", "can-download"], true)
|
||||
|> put_in(["pack", "download-sha256"], archive_sha)
|
||||
|
||||
{name, pack}
|
||||
with {:ok, pack} <- Pack.show(name) do
|
||||
json(conn, pack)
|
||||
else
|
||||
{name, put_in(pack, ["pack", "can-download"], false)}
|
||||
{:loaded, _} ->
|
||||
conn
|
||||
|> put_status(:not_found)
|
||||
|> json(%{error: "Pack #{name} does not exist"})
|
||||
|
||||
{:error, :empty_values} ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "pack name cannot be empty"})
|
||||
end
|
||||
end
|
||||
|
||||
defp can_download?(pack, pack_path) 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 {_, path} ->
|
||||
File.exists?(Path.join(pack_path, path))
|
||||
end)
|
||||
end
|
||||
|
||||
defp create_archive_and_cache(name, pack, pack_dir, md5) do
|
||||
files =
|
||||
['pack.json'] ++
|
||||
(pack["files"] |> Enum.map(fn {_, path} -> to_charlist(path) end))
|
||||
|
||||
{:ok, {_, zip_result}} = :zip.zip('#{name}.zip', files, [:memory, cwd: to_charlist(pack_dir)])
|
||||
|
||||
cache_seconds_per_file = Pleroma.Config.get!([:emoji, :shared_pack_cache_seconds_per_file])
|
||||
cache_ms = :timer.seconds(cache_seconds_per_file * Enum.count(files))
|
||||
|
||||
Cachex.put!(
|
||||
:emoji_packs_cache,
|
||||
name,
|
||||
# if pack.json MD5 changes, the cache is not valid anymore
|
||||
%{pack_json_md5: md5, pack_data: zip_result},
|
||||
# Add a minute to cache time for every file in the pack
|
||||
ttl: cache_ms
|
||||
)
|
||||
|
||||
Logger.debug("Created an archive for the '#{name}' emoji pack, \
|
||||
keeping it in cache for #{div(cache_ms, 1000)}s")
|
||||
|
||||
zip_result
|
||||
end
|
||||
|
||||
defp make_archive(name, pack, pack_dir) do
|
||||
# Having a different pack.json md5 invalidates cache
|
||||
pack_file_md5 = :crypto.hash(:md5, File.read!(Path.join(pack_dir, "pack.json")))
|
||||
|
||||
case Cachex.get!(:emoji_packs_cache, name) do
|
||||
%{pack_file_md5: ^pack_file_md5, pack_data: zip_result} ->
|
||||
Logger.debug("Using cache for the '#{name}' shared emoji pack")
|
||||
zip_result
|
||||
|
||||
_ ->
|
||||
create_archive_and_cache(name, pack, pack_dir, pack_file_md5)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
An endpoint for other instances (via admin UI) or users (via browser)
|
||||
to download packs that the instance shares.
|
||||
"""
|
||||
def download_shared(conn, %{"name" => name}) do
|
||||
pack_dir = Path.join(emoji_dir_path(), name)
|
||||
pack_file = Path.join(pack_dir, "pack.json")
|
||||
|
||||
with {_, true} <- {:exists?, File.exists?(pack_file)},
|
||||
pack = Jason.decode!(File.read!(pack_file)),
|
||||
{_, true} <- {:can_download?, can_download?(pack, pack_dir)} do
|
||||
zip_result = make_archive(name, pack, pack_dir)
|
||||
send_download(conn, {:binary, zip_result}, filename: "#{name}.zip")
|
||||
def archive(conn, %{"name" => name}) do
|
||||
with {:ok, archive} <- Pack.get_archive(name) do
|
||||
send_download(conn, {:binary, archive}, filename: "#{name}.zip")
|
||||
else
|
||||
{:can_download?, _} ->
|
||||
conn
|
||||
|> put_status(:forbidden)
|
||||
|> json(%{
|
||||
error: "Pack #{name} cannot be downloaded from this instance, either pack sharing\
|
||||
was disabled for this pack or some files are missing"
|
||||
error:
|
||||
"Pack #{name} cannot be downloaded from this instance, either pack sharing was disabled for this pack or some files are missing"
|
||||
})
|
||||
|
||||
{:exists?, _} ->
|
||||
|
|
@ -195,133 +97,67 @@ keeping it in cache for #{div(cache_ms, 1000)}s")
|
|||
end
|
||||
end
|
||||
|
||||
defp shareable_packs_available(address) do
|
||||
"#{address}/.well-known/nodeinfo"
|
||||
|> 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
|
||||
|
||||
@doc """
|
||||
An admin endpoint to request downloading a pack named `pack_name` from the instance
|
||||
`instance_address`.
|
||||
|
||||
If the requested instance's admin chose to share the pack, it will be downloaded
|
||||
from that instance, otherwise it will be downloaded from the fallback source, if there is one.
|
||||
"""
|
||||
def download_from(conn, %{"instance_address" => address, "pack_name" => name} = data) do
|
||||
address = String.trim(address)
|
||||
|
||||
if shareable_packs_available(address) do
|
||||
full_pack =
|
||||
"#{address}/api/pleroma/emoji/packs/list"
|
||||
|> Tesla.get!()
|
||||
|> Map.get(:body)
|
||||
|> Jason.decode!()
|
||||
|> Map.get(name)
|
||||
|
||||
pack_info_res =
|
||||
case full_pack["pack"] do
|
||||
%{"share-files" => true, "can-download" => true, "download-sha256" => sha} ->
|
||||
{:ok,
|
||||
%{
|
||||
sha: sha,
|
||||
uri: "#{address}/api/pleroma/emoji/packs/download_shared/#{name}"
|
||||
}}
|
||||
|
||||
%{"fallback-src" => src, "fallback-src-sha256" => sha} when is_binary(src) ->
|
||||
{:ok,
|
||||
%{
|
||||
sha: sha,
|
||||
uri: 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, uri: uri} = pinfo} <- pack_info_res,
|
||||
%{body: emoji_archive} <- Tesla.get!(uri),
|
||||
{_, true} <- {:checksum, Base.decode16!(sha) == :crypto.hash(:sha256, emoji_archive)} do
|
||||
local_name = data["as"] || name
|
||||
pack_dir = Path.join(emoji_dir_path(), local_name)
|
||||
File.mkdir_p!(pack_dir)
|
||||
|
||||
files = Enum.map(full_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(emoji_archive, cwd: to_charlist(pack_dir), 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
|
||||
pack_file_path = Path.join(pack_dir, "pack.json")
|
||||
|
||||
File.write!(pack_file_path, Jason.encode!(full_pack, pretty: true))
|
||||
end
|
||||
|
||||
json(conn, "ok")
|
||||
else
|
||||
{:error, e} ->
|
||||
conn |> put_status(:internal_server_error) |> json(%{error: e})
|
||||
|
||||
{:checksum, _} ->
|
||||
conn
|
||||
|> put_status(:internal_server_error)
|
||||
|> json(%{error: "SHA256 for the pack doesn't match the one sent by the server"})
|
||||
end
|
||||
def download(conn, %{"url" => url, "name" => name} = params) do
|
||||
with :ok <- Pack.download(name, url, params["as"]) do
|
||||
json(conn, "ok")
|
||||
else
|
||||
conn
|
||||
|> put_status(:internal_server_error)
|
||||
|> json(%{error: "The requested instance does not support sharing emoji packs"})
|
||||
{:shareable, _} ->
|
||||
conn
|
||||
|> put_status(:internal_server_error)
|
||||
|> json(%{error: "The requested instance does not support sharing emoji packs"})
|
||||
|
||||
{:checksum, _} ->
|
||||
conn
|
||||
|> put_status(:internal_server_error)
|
||||
|> json(%{error: "SHA256 for the pack doesn't match the one sent by the server"})
|
||||
|
||||
{:error, e} ->
|
||||
conn
|
||||
|> put_status(:internal_server_error)
|
||||
|> json(%{error: e})
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates an empty pack named `name` which then can be updated via the admin UI.
|
||||
"""
|
||||
def create(conn, %{"name" => name}) do
|
||||
pack_dir = Path.join(emoji_dir_path(), name)
|
||||
name = String.trim(name)
|
||||
|
||||
if not File.exists?(pack_dir) do
|
||||
File.mkdir_p!(pack_dir)
|
||||
|
||||
pack_file_p = Path.join(pack_dir, "pack.json")
|
||||
|
||||
File.write!(
|
||||
pack_file_p,
|
||||
Jason.encode!(%{pack: %{}, files: %{}}, pretty: true)
|
||||
)
|
||||
|
||||
conn |> json("ok")
|
||||
with :ok <- Pack.create(name) do
|
||||
json(conn, "ok")
|
||||
else
|
||||
conn
|
||||
|> put_status(:conflict)
|
||||
|> json(%{error: "A pack named \"#{name}\" already exists"})
|
||||
{:error, :eexist} ->
|
||||
conn
|
||||
|> put_status(:conflict)
|
||||
|> json(%{error: "A pack named \"#{name}\" already exists"})
|
||||
|
||||
{:error, :empty_values} ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "pack name cannot be empty"})
|
||||
|
||||
{:error, _} ->
|
||||
render_error(
|
||||
conn,
|
||||
:internal_server_error,
|
||||
"Unexpected error occurred while creating pack."
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes the pack `name` and all it's files.
|
||||
"""
|
||||
def delete(conn, %{"name" => name}) do
|
||||
pack_dir = Path.join(emoji_dir_path(), name)
|
||||
name = String.trim(name)
|
||||
|
||||
case File.rm_rf(pack_dir) do
|
||||
{:ok, _} ->
|
||||
conn |> json("ok")
|
||||
with {:ok, deleted} when deleted != [] <- Pack.delete(name) do
|
||||
json(conn, "ok")
|
||||
else
|
||||
{:ok, []} ->
|
||||
conn
|
||||
|> put_status(:not_found)
|
||||
|> json(%{error: "Pack #{name} does not exist"})
|
||||
|
||||
{:error, :empty_values} ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "pack name cannot be empty"})
|
||||
|
||||
{:error, _, _} ->
|
||||
conn
|
||||
|
|
@ -330,265 +166,128 @@ keeping it in cache for #{div(cache_ms, 1000)}s")
|
|||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
An endpoint to update `pack_names`'s metadata.
|
||||
|
||||
`new_data` is the new metadata for the pack, that will replace the old metadata.
|
||||
"""
|
||||
def update_metadata(conn, %{"pack_name" => name, "new_data" => new_data}) do
|
||||
pack_file_p = Path.join([emoji_dir_path(), name, "pack.json"])
|
||||
|
||||
full_pack = Jason.decode!(File.read!(pack_file_p))
|
||||
|
||||
# The new fallback-src is in the new data and it's not the same as it was in the old data
|
||||
should_update_fb_sha =
|
||||
not is_nil(new_data["fallback-src"]) and
|
||||
new_data["fallback-src"] != full_pack["pack"]["fallback-src"]
|
||||
|
||||
with {_, true} <- {:should_update?, should_update_fb_sha},
|
||||
%{body: pack_arch} <- Tesla.get!(new_data["fallback-src"]),
|
||||
{:ok, flist} <- :zip.unzip(pack_arch, [:memory]),
|
||||
{_, true} <- {:has_all_files?, has_all_files?(full_pack, flist)} do
|
||||
fallback_sha = :crypto.hash(:sha256, pack_arch) |> Base.encode16()
|
||||
|
||||
new_data = Map.put(new_data, "fallback-src-sha256", fallback_sha)
|
||||
update_metadata_and_send(conn, full_pack, new_data, pack_file_p)
|
||||
def update(conn, %{"name" => name, "metadata" => metadata}) do
|
||||
with {:ok, pack} <- Pack.update_metadata(name, metadata) do
|
||||
json(conn, pack.pack)
|
||||
else
|
||||
{:should_update?, _} ->
|
||||
update_metadata_and_send(conn, full_pack, new_data, pack_file_p)
|
||||
|
||||
{:has_all_files?, _} ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "The fallback archive does not have all files specified in pack.json"})
|
||||
|
||||
{:error, _} ->
|
||||
render_error(
|
||||
conn,
|
||||
:internal_server_error,
|
||||
"Unexpected error occurred while updating pack metadata."
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# Check if all files from the pack.json are in the archive
|
||||
defp has_all_files?(%{"files" => files}, flist) do
|
||||
Enum.all?(files, fn {_, from_manifest} ->
|
||||
Enum.find(flist, fn {from_archive, _} ->
|
||||
to_string(from_archive) == from_manifest
|
||||
end)
|
||||
end)
|
||||
end
|
||||
def add_file(conn, %{"name" => name} = params) do
|
||||
filename = params["filename"] || get_filename(params["file"])
|
||||
shortcode = params["shortcode"] || Path.basename(filename, Path.extname(filename))
|
||||
|
||||
defp update_metadata_and_send(conn, full_pack, new_data, pack_file_p) do
|
||||
full_pack = Map.put(full_pack, "pack", new_data)
|
||||
File.write!(pack_file_p, Jason.encode!(full_pack, pretty: true))
|
||||
|
||||
# Send new data back with fallback sha filled
|
||||
json(conn, new_data)
|
||||
end
|
||||
|
||||
defp get_filename(%{"filename" => filename}), do: filename
|
||||
|
||||
defp get_filename(%{"file" => file}) do
|
||||
case file do
|
||||
%Plug.Upload{filename: filename} -> filename
|
||||
url when is_binary(url) -> Path.basename(url)
|
||||
end
|
||||
end
|
||||
|
||||
defp empty?(str), do: String.trim(str) == ""
|
||||
|
||||
defp update_file_and_send(conn, updated_full_pack, pack_file_p) do
|
||||
# Write the emoji pack file
|
||||
File.write!(pack_file_p, Jason.encode!(updated_full_pack, pretty: true))
|
||||
|
||||
# Return the modified file list
|
||||
json(conn, updated_full_pack["files"])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a file in a pack.
|
||||
|
||||
Updating can mean three things:
|
||||
|
||||
- `add` adds an emoji named `shortcode` to the pack `pack_name`,
|
||||
that means that the emoji file needs to be uploaded with the request
|
||||
(thus requiring it to be a multipart request) and be named `file`.
|
||||
There can also be an optional `filename` that will be the new emoji file name
|
||||
(if it's not there, the name will be taken from the uploaded file).
|
||||
- `update` changes emoji shortcode (from `shortcode` to `new_shortcode` or moves the file
|
||||
(from the current filename to `new_filename`)
|
||||
- `remove` removes the emoji named `shortcode` and it's associated file
|
||||
"""
|
||||
|
||||
# Add
|
||||
def update_file(
|
||||
conn,
|
||||
%{"pack_name" => pack_name, "action" => "add", "shortcode" => shortcode} = params
|
||||
) do
|
||||
pack_dir = Path.join(emoji_dir_path(), pack_name)
|
||||
pack_file_p = Path.join(pack_dir, "pack.json")
|
||||
|
||||
full_pack = Jason.decode!(File.read!(pack_file_p))
|
||||
|
||||
with {_, false} <- {:has_shortcode, Map.has_key?(full_pack["files"], shortcode)},
|
||||
filename <- get_filename(params),
|
||||
false <- empty?(shortcode),
|
||||
false <- empty?(filename) do
|
||||
file_path = Path.join(pack_dir, filename)
|
||||
|
||||
# If the name contains directories, create them
|
||||
if String.contains?(file_path, "/") do
|
||||
File.mkdir_p!(Path.dirname(file_path))
|
||||
end
|
||||
|
||||
case params["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
|
||||
|
||||
updated_full_pack = put_in(full_pack, ["files", shortcode], filename)
|
||||
update_file_and_send(conn, updated_full_pack, pack_file_p)
|
||||
with {:ok, pack} <- Pack.add_file(name, shortcode, filename, params["file"]) do
|
||||
json(conn, pack.files)
|
||||
else
|
||||
{:has_shortcode, _} ->
|
||||
{:exists, _} ->
|
||||
conn
|
||||
|> put_status(:conflict)
|
||||
|> json(%{error: "An emoji with the \"#{shortcode}\" shortcode already exists"})
|
||||
|
||||
true ->
|
||||
{:loaded, _} ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "shortcode or filename cannot be empty"})
|
||||
|> json(%{error: "pack \"#{name}\" is not found"})
|
||||
|
||||
{:error, :empty_values} ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "pack name, shortcode or filename cannot be empty"})
|
||||
|
||||
{:error, _} ->
|
||||
render_error(
|
||||
conn,
|
||||
:internal_server_error,
|
||||
"Unexpected error occurred while adding file to pack."
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# Remove
|
||||
def update_file(conn, %{
|
||||
"pack_name" => pack_name,
|
||||
"action" => "remove",
|
||||
"shortcode" => shortcode
|
||||
}) do
|
||||
pack_dir = Path.join(emoji_dir_path(), pack_name)
|
||||
pack_file_p = Path.join(pack_dir, "pack.json")
|
||||
def update_file(conn, %{"name" => name, "shortcode" => shortcode} = params) do
|
||||
new_shortcode = params["new_shortcode"]
|
||||
new_filename = params["new_filename"]
|
||||
force = params["force"] == true
|
||||
|
||||
full_pack = Jason.decode!(File.read!(pack_file_p))
|
||||
|
||||
if Map.has_key?(full_pack["files"], shortcode) do
|
||||
{emoji_file_path, updated_full_pack} = pop_in(full_pack, ["files", shortcode])
|
||||
|
||||
emoji_file_path = Path.join(pack_dir, emoji_file_path)
|
||||
|
||||
# Delete the emoji file
|
||||
File.rm!(emoji_file_path)
|
||||
|
||||
# If the old directory has no more files, remove it
|
||||
if String.contains?(emoji_file_path, "/") do
|
||||
dir = Path.dirname(emoji_file_path)
|
||||
|
||||
if Enum.empty?(File.ls!(dir)) do
|
||||
File.rmdir!(dir)
|
||||
end
|
||||
end
|
||||
|
||||
update_file_and_send(conn, updated_full_pack, pack_file_p)
|
||||
with {:ok, pack} <- Pack.update_file(name, shortcode, new_shortcode, new_filename, force) do
|
||||
json(conn, pack.files)
|
||||
else
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "Emoji \"#{shortcode}\" does not exist"})
|
||||
end
|
||||
end
|
||||
|
||||
# Update
|
||||
def update_file(
|
||||
conn,
|
||||
%{"pack_name" => pack_name, "action" => "update", "shortcode" => shortcode} = params
|
||||
) do
|
||||
pack_dir = Path.join(emoji_dir_path(), pack_name)
|
||||
pack_file_p = Path.join(pack_dir, "pack.json")
|
||||
|
||||
full_pack = Jason.decode!(File.read!(pack_file_p))
|
||||
|
||||
with {_, true} <- {:has_shortcode, Map.has_key?(full_pack["files"], shortcode)},
|
||||
%{"new_shortcode" => new_shortcode, "new_filename" => new_filename} <- params,
|
||||
false <- empty?(new_shortcode),
|
||||
false <- empty?(new_filename) do
|
||||
# First, remove the old shortcode, saving the old path
|
||||
{old_emoji_file_path, updated_full_pack} = pop_in(full_pack, ["files", shortcode])
|
||||
old_emoji_file_path = Path.join(pack_dir, old_emoji_file_path)
|
||||
new_emoji_file_path = Path.join(pack_dir, new_filename)
|
||||
|
||||
# If the name contains directories, create them
|
||||
if String.contains?(new_emoji_file_path, "/") do
|
||||
File.mkdir_p!(Path.dirname(new_emoji_file_path))
|
||||
end
|
||||
|
||||
# Move/Rename the old filename to a new filename
|
||||
# These are probably on the same filesystem, so just rename should work
|
||||
:ok = File.rename(old_emoji_file_path, new_emoji_file_path)
|
||||
|
||||
# If the old directory has no more files, remove it
|
||||
if String.contains?(old_emoji_file_path, "/") do
|
||||
dir = Path.dirname(old_emoji_file_path)
|
||||
|
||||
if Enum.empty?(File.ls!(dir)) do
|
||||
File.rmdir!(dir)
|
||||
end
|
||||
end
|
||||
|
||||
# Then, put in the new shortcode with the new path
|
||||
updated_full_pack = put_in(updated_full_pack, ["files", new_shortcode], new_filename)
|
||||
update_file_and_send(conn, updated_full_pack, pack_file_p)
|
||||
else
|
||||
{:has_shortcode, _} ->
|
||||
{:exists, _} ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "Emoji \"#{shortcode}\" does not exist"})
|
||||
|
||||
true ->
|
||||
{:not_used, _} ->
|
||||
conn
|
||||
|> put_status(:conflict)
|
||||
|> json(%{
|
||||
error:
|
||||
"New shortcode \"#{new_shortcode}\" is already used. If you want to override emoji use 'force' option"
|
||||
})
|
||||
|
||||
{:loaded, _} ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "pack \"#{name}\" is not found"})
|
||||
|
||||
{:error, :empty_values} ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "new_shortcode or new_filename cannot be empty"})
|
||||
|
||||
_ ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "new_shortcode or new_file were not specified"})
|
||||
{:error, _} ->
|
||||
render_error(
|
||||
conn,
|
||||
:internal_server_error,
|
||||
"Unexpected error occurred while updating file in pack."
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def update_file(conn, %{"action" => action}) do
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "Unknown action: #{action}"})
|
||||
def delete_file(conn, %{"name" => name, "shortcode" => shortcode}) do
|
||||
with {:ok, pack} <- Pack.delete_file(name, shortcode) do
|
||||
json(conn, pack.files)
|
||||
else
|
||||
{:exists, _} ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "Emoji \"#{shortcode}\" does not exist"})
|
||||
|
||||
{:loaded, _} ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "pack \"#{name}\" is not found"})
|
||||
|
||||
{:error, :empty_values} ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "pack name or shortcode cannot be empty"})
|
||||
|
||||
{:error, _} ->
|
||||
render_error(
|
||||
conn,
|
||||
:internal_server_error,
|
||||
"Unexpected error occurred while removing file from pack."
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Imports emoji from the filesystem.
|
||||
|
||||
Importing means checking all the directories in the
|
||||
`$instance_static/emoji/` for directories which do not have
|
||||
`pack.json`. If one has an emoji.txt file, that file will be used
|
||||
to create a `pack.json` file with it's contents. If the directory has
|
||||
neither, all the files with specific configured extenstions will be
|
||||
assumed to be emojis and stored in the new `pack.json` file.
|
||||
"""
|
||||
def import_from_fs(conn, _params) do
|
||||
emoji_path = emoji_dir_path()
|
||||
|
||||
with {:ok, %{access: :read_write}} <- File.stat(emoji_path),
|
||||
{:ok, results} <- File.ls(emoji_path) do
|
||||
imported_pack_names =
|
||||
results
|
||||
|> Enum.filter(fn file ->
|
||||
dir_path = Path.join(emoji_path, file)
|
||||
# Find the directories that do NOT have pack.json
|
||||
File.dir?(dir_path) and not File.exists?(Path.join(dir_path, "pack.json"))
|
||||
end)
|
||||
|> Enum.map(&write_pack_json_contents/1)
|
||||
|
||||
json(conn, imported_pack_names)
|
||||
def import_from_filesystem(conn, _params) do
|
||||
with {:ok, names} <- Pack.import_from_filesystem() do
|
||||
json(conn, names)
|
||||
else
|
||||
{:ok, %{access: _}} ->
|
||||
{:error, :no_read_write} ->
|
||||
conn
|
||||
|> put_status(:internal_server_error)
|
||||
|> json(%{error: "Error: emoji pack directory must be writable"})
|
||||
|
|
@ -600,44 +299,6 @@ keeping it in cache for #{div(cache_ms, 1000)}s")
|
|||
end
|
||||
end
|
||||
|
||||
defp write_pack_json_contents(dir) do
|
||||
dir_path = Path.join(emoji_dir_path(), dir)
|
||||
emoji_txt_path = Path.join(dir_path, "emoji.txt")
|
||||
|
||||
files_for_pack = files_for_pack(emoji_txt_path, dir_path)
|
||||
pack_json_contents = Jason.encode!(%{pack: %{}, files: files_for_pack})
|
||||
|
||||
File.write!(Path.join(dir_path, "pack.json"), pack_json_contents)
|
||||
|
||||
dir
|
||||
end
|
||||
|
||||
defp files_for_pack(emoji_txt_path, dir_path) do
|
||||
if File.exists?(emoji_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 fileh
|
||||
|
||||
# FIXME: Copy-pasted from Pleroma.Emoji/load_from_file_stream/2
|
||||
|
||||
# Create a map of shortcodes to filenames from emoji.txt
|
||||
File.read!(emoji_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 | _] -> {name, file}
|
||||
_ -> nil
|
||||
end
|
||||
end)
|
||||
|> Enum.filter(fn x -> not is_nil(x) end)
|
||||
|> 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])
|
||||
Pleroma.Emoji.Loader.make_shortcode_to_file_map(dir_path, pack_extensions)
|
||||
end
|
||||
end
|
||||
defp get_filename(%Plug.Upload{filename: filename}), do: filename
|
||||
defp get_filename(url) when is_binary(url), do: Path.basename(url)
|
||||
end
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue