@@ -22,8 +22,21 @@ defmodule Pleroma.LoadTesting.Activities do | |||
@max_concurrency 10 | |||
@visibility ~w(public private direct unlisted) | |||
@types ~w(simple emoji mentions hell_thread attachment tag like reblog simple_thread remote) | |||
@groups ~w(user friends non_friends) | |||
@types [ | |||
:simple, | |||
:emoji, | |||
:mentions, | |||
:hell_thread, | |||
:attachment, | |||
:tag, | |||
:like, | |||
:reblog, | |||
:simple_thread | |||
] | |||
@groups [:friends_local, :friends_remote, :non_friends_local, :non_friends_local] | |||
@remote_groups [:friends_remote, :non_friends_remote] | |||
@friends_groups [:friends_local, :friends_remote] | |||
@non_friends_groups [:non_friends_local, :non_friends_remote] | |||
@spec generate(User.t(), keyword()) :: :ok | |||
def generate(user, opts \\ []) do | |||
@@ -34,33 +47,24 @@ defmodule Pleroma.LoadTesting.Activities do | |||
opts = Keyword.merge(@defaults, opts) | |||
friends = | |||
user | |||
|> Users.get_users(limit: opts[:friends_used], local: :local, friends?: true) | |||
|> Enum.shuffle() | |||
users = Users.prepare_users(user, opts) | |||
non_friends = | |||
user | |||
|> Users.get_users(limit: opts[:non_friends_used], local: :local, friends?: false) | |||
|> Enum.shuffle() | |||
{:ok, _} = Agent.start_link(fn -> users[:non_friends_remote] end, name: :non_friends_remote) | |||
task_data = | |||
for visibility <- @visibility, | |||
type <- @types, | |||
group <- @groups, | |||
group <- [:user | @groups], | |||
do: {visibility, type, group} | |||
IO.puts("Starting generating #{opts[:iterations]} iterations of activities...") | |||
friends_thread = Enum.take(friends, 5) | |||
non_friends_thread = Enum.take(friends, 5) | |||
public_long_thread = fn -> | |||
generate_long_thread("public", user, friends_thread, non_friends_thread, opts) | |||
generate_long_thread("public", users, opts) | |||
end | |||
private_long_thread = fn -> | |||
generate_long_thread("private", user, friends_thread, non_friends_thread, opts) | |||
generate_long_thread("private", users, opts) | |||
end | |||
iterations = opts[:iterations] | |||
@@ -73,10 +77,10 @@ defmodule Pleroma.LoadTesting.Activities do | |||
i when i == iterations - 2 -> | |||
spawn(public_long_thread) | |||
spawn(private_long_thread) | |||
generate_activities(user, friends, non_friends, Enum.shuffle(task_data), opts) | |||
generate_activities(users, Enum.shuffle(task_data), opts) | |||
_ -> | |||
generate_activities(user, friends, non_friends, Enum.shuffle(task_data), opts) | |||
generate_activities(users, Enum.shuffle(task_data), opts) | |||
end | |||
) | |||
end) | |||
@@ -127,16 +131,16 @@ defmodule Pleroma.LoadTesting.Activities do | |||
end) | |||
end | |||
defp generate_long_thread(visibility, user, friends, non_friends, _opts) do | |||
defp generate_long_thread(visibility, users, _opts) do | |||
group = | |||
if visibility == "public", | |||
do: "friends", | |||
else: "user" | |||
do: :friends_local, | |||
else: :user | |||
tasks = get_reply_tasks(visibility, group) |> Stream.cycle() |> Enum.take(50) | |||
{:ok, activity} = | |||
CommonAPI.post(user, %{ | |||
CommonAPI.post(users[:user], %{ | |||
status: "Start of #{visibility} long thread", | |||
visibility: visibility | |||
}) | |||
@@ -150,31 +154,28 @@ defmodule Pleroma.LoadTesting.Activities do | |||
Map.put(state, key, activity) | |||
end) | |||
acc = {activity.id, ["@" <> user.nickname, "reply to long thread"]} | |||
insert_replies_for_long_thread(tasks, visibility, user, friends, non_friends, acc) | |||
acc = {activity.id, ["@" <> users[:user].nickname, "reply to long thread"]} | |||
insert_replies_for_long_thread(tasks, visibility, users, acc) | |||
IO.puts("Generating #{visibility} long thread ended\n") | |||
end | |||
defp insert_replies_for_long_thread(tasks, visibility, user, friends, non_friends, acc) do | |||
defp insert_replies_for_long_thread(tasks, visibility, users, acc) do | |||
Enum.reduce(tasks, acc, fn | |||
"friend", {id, data} -> | |||
friend = Enum.random(friends) | |||
insert_reply(friend, List.delete(data, "@" <> friend.nickname), id, visibility) | |||
"non_friend", {id, data} -> | |||
non_friend = Enum.random(non_friends) | |||
insert_reply(non_friend, List.delete(data, "@" <> non_friend.nickname), id, visibility) | |||
"user", {id, data} -> | |||
:user, {id, data} -> | |||
user = users[:user] | |||
insert_reply(user, List.delete(data, "@" <> user.nickname), id, visibility) | |||
group, {id, data} -> | |||
replier = Enum.random(users[group]) | |||
insert_reply(replier, List.delete(data, "@" <> replier.nickname), id, visibility) | |||
end) | |||
end | |||
defp generate_activities(user, friends, non_friends, task_data, opts) do | |||
defp generate_activities(users, task_data, opts) do | |||
Task.async_stream( | |||
task_data, | |||
fn {visibility, type, group} -> | |||
insert_activity(type, visibility, group, user, friends, non_friends, opts) | |||
insert_activity(type, visibility, group, users, opts) | |||
end, | |||
max_concurrency: @max_concurrency, | |||
timeout: 30_000 | |||
@@ -182,67 +183,104 @@ defmodule Pleroma.LoadTesting.Activities do | |||
|> Stream.run() | |||
end | |||
defp insert_activity("simple", visibility, group, user, friends, non_friends, _opts) do | |||
{:ok, _activity} = | |||
defp insert_local_activity(visibility, group, users, status) do | |||
{:ok, _} = | |||
group | |||
|> get_actor(user, friends, non_friends) | |||
|> CommonAPI.post(%{status: "Simple status", visibility: visibility}) | |||
|> get_actor(users) | |||
|> CommonAPI.post(%{status: status, visibility: visibility}) | |||
end | |||
defp insert_activity("emoji", visibility, group, user, friends, non_friends, _opts) do | |||
{:ok, _activity} = | |||
group | |||
|> get_actor(user, friends, non_friends) | |||
|> CommonAPI.post(%{ | |||
status: "Simple status with emoji :firefox:", | |||
visibility: visibility | |||
}) | |||
defp insert_remote_activity(visibility, group, users, status) do | |||
actor = get_actor(group, users) | |||
{act_data, obj_data} = prepare_activity_data(actor, visibility, users[:user]) | |||
{activity_data, object_data} = other_data(actor, status) | |||
activity_data | |||
|> Map.merge(act_data) | |||
|> Map.put("object", Map.merge(object_data, obj_data)) | |||
|> Pleroma.Web.ActivityPub.ActivityPub.insert(false) | |||
end | |||
defp insert_activity("mentions", visibility, group, user, friends, non_friends, _opts) do | |||
defp user_mentions(users) do | |||
user_mentions = | |||
get_random_mentions(friends, Enum.random(0..3)) ++ | |||
get_random_mentions(non_friends, Enum.random(0..3)) | |||
Enum.reduce( | |||
@groups, | |||
[], | |||
fn group, acc -> | |||
acc ++ get_random_mentions(users[group], Enum.random(0..2)) | |||
end | |||
) | |||
user_mentions = | |||
if Enum.random([true, false]), | |||
do: ["@" <> user.nickname | user_mentions], | |||
else: user_mentions | |||
if Enum.random([true, false]), | |||
do: ["@" <> users[:user].nickname | user_mentions], | |||
else: user_mentions | |||
end | |||
{:ok, _activity} = | |||
group | |||
|> get_actor(user, friends, non_friends) | |||
|> CommonAPI.post(%{ | |||
status: Enum.join(user_mentions, ", ") <> " simple status with mentions", | |||
visibility: visibility | |||
}) | |||
defp hell_thread_mentions(users) do | |||
with {:ok, nil} <- Cachex.get(:user_cache, "hell_thread_mentions") do | |||
cached = | |||
@groups | |||
|> Enum.reduce([users[:user]], fn group, acc -> | |||
acc ++ Enum.take(users[group], 5) | |||
end) | |||
|> Enum.map(&"@#{&1.nickname}") | |||
|> Enum.join(", ") | |||
Cachex.put(:user_cache, "hell_thread_mentions", cached) | |||
cached | |||
else | |||
{:ok, cached} -> cached | |||
end | |||
end | |||
defp insert_activity("hell_thread", visibility, group, user, friends, non_friends, _opts) do | |||
mentions = | |||
with {:ok, nil} <- Cachex.get(:user_cache, "hell_thread_mentions") do | |||
cached = | |||
([user | Enum.take(friends, 10)] ++ Enum.take(non_friends, 10)) | |||
|> Enum.map(&"@#{&1.nickname}") | |||
|> Enum.join(", ") | |||
defp insert_activity(:simple, visibility, group, users, _opts) | |||
when group in @remote_groups do | |||
insert_remote_activity(visibility, group, users, "Remote status") | |||
end | |||
Cachex.put(:user_cache, "hell_thread_mentions", cached) | |||
cached | |||
else | |||
{:ok, cached} -> cached | |||
end | |||
defp insert_activity(:simple, visibility, group, users, _opts) do | |||
insert_local_activity(visibility, group, users, "Simple status") | |||
end | |||
{:ok, _activity} = | |||
group | |||
|> get_actor(user, friends, non_friends) | |||
|> CommonAPI.post(%{ | |||
status: mentions <> " hell thread status", | |||
visibility: visibility | |||
}) | |||
defp insert_activity(:emoji, visibility, group, users, _opts) | |||
when group in @remote_groups do | |||
insert_remote_activity(visibility, group, users, "Remote status with emoji :firefox:") | |||
end | |||
defp insert_activity(:emoji, visibility, group, users, _opts) do | |||
insert_local_activity(visibility, group, users, "Simple status with emoji :firefox:") | |||
end | |||
defp insert_activity(:mentions, visibility, group, users, _opts) | |||
when group in @remote_groups do | |||
mentions = user_mentions(users) | |||
status = Enum.join(mentions, ", ") <> " remote status with mentions" | |||
insert_remote_activity(visibility, group, users, status) | |||
end | |||
defp insert_activity(:mentions, visibility, group, users, _opts) do | |||
mentions = user_mentions(users) | |||
status = Enum.join(mentions, ", ") <> " simple status with mentions" | |||
insert_remote_activity(visibility, group, users, status) | |||
end | |||
defp insert_activity(:hell_thread, visibility, group, users, _) | |||
when group in @remote_groups do | |||
mentions = hell_thread_mentions(users) | |||
insert_remote_activity(visibility, group, users, mentions <> " remote hell thread status") | |||
end | |||
defp insert_activity(:hell_thread, visibility, group, users, _opts) do | |||
mentions = hell_thread_mentions(users) | |||
insert_local_activity(visibility, group, users, mentions <> " hell thread status") | |||
end | |||
defp insert_activity("attachment", visibility, group, user, friends, non_friends, _opts) do | |||
actor = get_actor(group, user, friends, non_friends) | |||
defp insert_activity(:attachment, visibility, group, users, _opts) do | |||
actor = get_actor(group, users) | |||
obj_data = %{ | |||
"actor" => actor.ap_id, | |||
@@ -268,67 +306,54 @@ defmodule Pleroma.LoadTesting.Activities do | |||
}) | |||
end | |||
defp insert_activity("tag", visibility, group, user, friends, non_friends, _opts) do | |||
{:ok, _activity} = | |||
group | |||
|> get_actor(user, friends, non_friends) | |||
|> CommonAPI.post(%{status: "Status with #tag", visibility: visibility}) | |||
defp insert_activity(:tag, visibility, group, users, _opts) do | |||
insert_local_activity(visibility, group, users, "Status with #tag") | |||
end | |||
defp insert_activity("like", visibility, group, user, friends, non_friends, opts) do | |||
actor = get_actor(group, user, friends, non_friends) | |||
defp insert_activity(:like, visibility, group, users, opts) do | |||
actor = get_actor(group, users) | |||
with activity_id when not is_nil(activity_id) <- get_random_create_activity_id(), | |||
{:ok, _activity} <- CommonAPI.favorite(actor, activity_id) do | |||
:ok | |||
else | |||
{:error, _} -> | |||
insert_activity("like", visibility, group, user, friends, non_friends, opts) | |||
insert_activity(:like, visibility, group, users, opts) | |||
nil -> | |||
Process.sleep(15) | |||
insert_activity("like", visibility, group, user, friends, non_friends, opts) | |||
insert_activity(:like, visibility, group, users, opts) | |||
end | |||
end | |||
defp insert_activity("reblog", visibility, group, user, friends, non_friends, opts) do | |||
actor = get_actor(group, user, friends, non_friends) | |||
defp insert_activity(:reblog, visibility, group, users, opts) do | |||
actor = get_actor(group, users) | |||
with activity_id when not is_nil(activity_id) <- get_random_create_activity_id(), | |||
{:ok, _activity, _object} <- CommonAPI.repeat(activity_id, actor) do | |||
{:ok, _activity} <- CommonAPI.repeat(activity_id, actor) do | |||
:ok | |||
else | |||
{:error, _} -> | |||
insert_activity("reblog", visibility, group, user, friends, non_friends, opts) | |||
insert_activity(:reblog, visibility, group, users, opts) | |||
nil -> | |||
Process.sleep(15) | |||
insert_activity("reblog", visibility, group, user, friends, non_friends, opts) | |||
insert_activity(:reblog, visibility, group, users, opts) | |||
end | |||
end | |||
defp insert_activity("simple_thread", visibility, group, user, friends, non_friends, _opts) | |||
when visibility in ["public", "unlisted", "private"] do | |||
actor = get_actor(group, user, friends, non_friends) | |||
tasks = get_reply_tasks(visibility, group) | |||
{:ok, activity} = CommonAPI.post(user, %{status: "Simple status", visibility: visibility}) | |||
acc = {activity.id, ["@" <> actor.nickname, "reply to status"]} | |||
insert_replies(tasks, visibility, user, friends, non_friends, acc) | |||
end | |||
defp insert_activity("simple_thread", "direct", group, user, friends, non_friends, _opts) do | |||
actor = get_actor(group, user, friends, non_friends) | |||
defp insert_activity(:simple_thread, "direct", group, users, _opts) do | |||
actor = get_actor(group, users) | |||
tasks = get_reply_tasks("direct", group) | |||
list = | |||
case group do | |||
"non_friends" -> | |||
Enum.take(non_friends, 3) | |||
:user -> | |||
group = Enum.random(@friends_groups) | |||
Enum.take(users[group], 3) | |||
_ -> | |||
Enum.take(friends, 3) | |||
Enum.take(users[group], 3) | |||
end | |||
data = Enum.map(list, &("@" <> &1.nickname)) | |||
@@ -339,40 +364,30 @@ defmodule Pleroma.LoadTesting.Activities do | |||
visibility: "direct" | |||
}) | |||
acc = {activity.id, ["@" <> user.nickname | data] ++ ["reply to status"]} | |||
insert_direct_replies(tasks, user, list, acc) | |||
acc = {activity.id, ["@" <> users[:user].nickname | data] ++ ["reply to status"]} | |||
insert_direct_replies(tasks, users[:user], list, acc) | |||
end | |||
defp insert_activity("remote", _, "user", _, _, _, _), do: :ok | |||
defp insert_activity("remote", visibility, group, user, _friends, _non_friends, opts) do | |||
remote_friends = | |||
Users.get_users(user, limit: opts[:friends_used], local: :external, friends?: true) | |||
remote_non_friends = | |||
Users.get_users(user, limit: opts[:non_friends_used], local: :external, friends?: false) | |||
actor = get_actor(group, user, remote_friends, remote_non_friends) | |||
defp insert_activity(:simple_thread, visibility, group, users, _opts) do | |||
actor = get_actor(group, users) | |||
tasks = get_reply_tasks(visibility, group) | |||
{act_data, obj_data} = prepare_activity_data(actor, visibility, user) | |||
{activity_data, object_data} = other_data(actor) | |||
{:ok, activity} = | |||
CommonAPI.post(users[:user], %{status: "Simple status", visibility: visibility}) | |||
activity_data | |||
|> Map.merge(act_data) | |||
|> Map.put("object", Map.merge(object_data, obj_data)) | |||
|> Pleroma.Web.ActivityPub.ActivityPub.insert(false) | |||
acc = {activity.id, ["@" <> actor.nickname, "reply to status"]} | |||
insert_replies(tasks, visibility, users, acc) | |||
end | |||
defp get_actor("user", user, _friends, _non_friends), do: user | |||
defp get_actor("friends", _user, friends, _non_friends), do: Enum.random(friends) | |||
defp get_actor("non_friends", _user, _friends, non_friends), do: Enum.random(non_friends) | |||
defp get_actor(:user, %{user: user}), do: user | |||
defp get_actor(group, users), do: Enum.random(users[group]) | |||
defp other_data(actor) do | |||
defp other_data(actor, content) do | |||
%{host: host} = URI.parse(actor.ap_id) | |||
datetime = DateTime.utc_now() | |||
context_id = "http://#{host}:4000/contexts/#{UUID.generate()}" | |||
activity_id = "http://#{host}:4000/activities/#{UUID.generate()}" | |||
object_id = "http://#{host}:4000/objects/#{UUID.generate()}" | |||
context_id = "https://#{host}/contexts/#{UUID.generate()}" | |||
activity_id = "https://#{host}/activities/#{UUID.generate()}" | |||
object_id = "https://#{host}/objects/#{UUID.generate()}" | |||
activity_data = %{ | |||
"actor" => actor.ap_id, | |||
@@ -389,7 +404,7 @@ defmodule Pleroma.LoadTesting.Activities do | |||
"attributedTo" => actor.ap_id, | |||
"bcc" => [], | |||
"bto" => [], | |||
"content" => "Remote post", | |||
"content" => content, | |||
"context" => context_id, | |||
"conversation" => context_id, | |||
"emoji" => %{}, | |||
@@ -475,51 +490,65 @@ defmodule Pleroma.LoadTesting.Activities do | |||
{act_data, obj_data} | |||
end | |||
defp get_reply_tasks("public", "user"), do: ~w(friend non_friend user) | |||
defp get_reply_tasks("public", "friends"), do: ~w(non_friend user friend) | |||
defp get_reply_tasks("public", "non_friends"), do: ~w(user friend non_friend) | |||
defp get_reply_tasks("public", :user) do | |||
[:friends_local, :friends_remote, :non_friends_local, :non_friends_remote, :user] | |||
end | |||
defp get_reply_tasks("public", group) when group in @friends_groups do | |||
[:non_friends_local, :non_friends_remote, :user, :friends_local, :friends_remote] | |||
end | |||
defp get_reply_tasks(visibility, "user") when visibility in ["unlisted", "private"], | |||
do: ~w(friend user friend) | |||
defp get_reply_tasks("public", group) when group in @non_friends_groups do | |||
[:user, :friends_local, :friends_remote, :non_friends_local, :non_friends_remote] | |||
end | |||
defp get_reply_tasks(visibility, "friends") when visibility in ["unlisted", "private"], | |||
do: ~w(user friend user) | |||
defp get_reply_tasks(visibility, :user) when visibility in ["unlisted", "private"] do | |||
[:friends_local, :friends_remote, :user, :friends_local, :friends_remote] | |||
end | |||
defp get_reply_tasks(visibility, "non_friends") when visibility in ["unlisted", "private"], | |||
do: [] | |||
defp get_reply_tasks(visibility, group) | |||
when visibility in ["unlisted", "private"] and group in @friends_groups do | |||
[:user, :friends_remote, :friends_local, :user] | |||
end | |||
defp get_reply_tasks("direct", "user"), do: ~w(friend user friend) | |||
defp get_reply_tasks("direct", "friends"), do: ~w(user friend user) | |||
defp get_reply_tasks("direct", "non_friends"), do: ~w(user non_friend user) | |||
defp get_reply_tasks(visibility, group) | |||
when visibility in ["unlisted", "private"] and | |||
group in @non_friends_groups, | |||
do: [] | |||
defp insert_replies(tasks, visibility, user, friends, non_friends, acc) do | |||
Enum.reduce(tasks, acc, fn | |||
"friend", {id, data} -> | |||
friend = Enum.random(friends) | |||
insert_reply(friend, data, id, visibility) | |||
defp get_reply_tasks("direct", :user), do: [:friends_local, :user, :friends_remote] | |||
"non_friend", {id, data} -> | |||
non_friend = Enum.random(non_friends) | |||
insert_reply(non_friend, data, id, visibility) | |||
defp get_reply_tasks("direct", group) when group in @friends_groups, | |||
do: [:user, group, :user] | |||
"user", {id, data} -> | |||
insert_reply(user, data, id, visibility) | |||
defp get_reply_tasks("direct", group) when group in @non_friends_groups do | |||
[:user, :non_friends_remote, :user, :non_friends_local] | |||
end | |||
defp insert_replies(tasks, visibility, users, acc) do | |||
Enum.reduce(tasks, acc, fn | |||
:user, {id, data} -> | |||
insert_reply(users[:user], data, id, visibility) | |||
group, {id, data} -> | |||
replier = Enum.random(users[group]) | |||
insert_reply(replier, data, id, visibility) | |||
end) | |||
end | |||
defp insert_direct_replies(tasks, user, list, acc) do | |||
Enum.reduce(tasks, acc, fn | |||
group, {id, data} when group in ["friend", "non_friend"] -> | |||
:user, {id, data} -> | |||
{reply_id, _} = insert_reply(user, List.delete(data, "@" <> user.nickname), id, "direct") | |||
{reply_id, data} | |||
_, {id, data} -> | |||
actor = Enum.random(list) | |||
{reply_id, _} = | |||
insert_reply(actor, List.delete(data, "@" <> actor.nickname), id, "direct") | |||
{reply_id, data} | |||
"user", {id, data} -> | |||
{reply_id, _} = insert_reply(user, List.delete(data, "@" <> user.nickname), id, "direct") | |||
{reply_id, data} | |||
end) | |||
end | |||
@@ -36,6 +36,7 @@ defmodule Pleroma.LoadTesting.Fetcher do | |||
fetch_home_timeline(user) | |||
fetch_direct_timeline(user) | |||
fetch_public_timeline(user) | |||
fetch_public_timeline(user, :with_blocks) | |||
fetch_public_timeline(user, :local) | |||
fetch_public_timeline(user, :tag) | |||
fetch_notifications(user) | |||
@@ -227,6 +228,58 @@ defmodule Pleroma.LoadTesting.Fetcher do | |||
fetch_public_timeline(opts, "public timeline only media") | |||
end | |||
defp fetch_public_timeline(user, :with_blocks) do | |||
opts = opts_for_public_timeline(user) | |||
remote_non_friends = Agent.get(:non_friends_remote, & &1) | |||
Benchee.run(%{ | |||
"public timeline without blocks" => fn -> | |||
ActivityPub.fetch_public_activities(opts) | |||
end | |||
}) | |||
Enum.each(remote_non_friends, fn non_friend -> | |||
{:ok, _} = User.block(user, non_friend) | |||
end) | |||
user = User.get_by_id(user.id) | |||
opts = Map.put(opts, "blocking_user", user) | |||
Benchee.run( | |||
%{ | |||
"public timeline with user block" => fn -> | |||
ActivityPub.fetch_public_activities(opts) | |||
end | |||
}, | |||
) | |||
domains = | |||
Enum.reduce(remote_non_friends, [], fn non_friend, domains -> | |||
{:ok, _user} = User.unblock(user, non_friend) | |||
%{host: host} = URI.parse(non_friend.ap_id) | |||
[host | domains] | |||
end) | |||
domains = Enum.uniq(domains) | |||
Enum.each(domains, fn domain -> | |||
{:ok, _} = User.block_domain(user, domain) | |||
end) | |||
user = User.get_by_id(user.id) | |||
opts = Map.put(opts, "blocking_user", user) | |||
Benchee.run( | |||
%{ | |||
"public timeline with domain block" => fn opts -> | |||
ActivityPub.fetch_public_activities(opts) | |||
end | |||
} | |||
) | |||
end | |||
defp fetch_public_timeline(opts, title) when is_binary(title) do | |||
first_page_last = ActivityPub.fetch_public_activities(opts) |> List.last() | |||
@@ -27,7 +27,7 @@ defmodule Pleroma.LoadTesting.Users do | |||
make_friends(main_user, opts[:friends]) | |||
Repo.get(User, main_user.id) | |||
User.get_by_id(main_user.id) | |||
end | |||
def generate_users(max) do | |||
@@ -166,4 +166,24 @@ defmodule Pleroma.LoadTesting.Users do | |||
) | |||
|> Stream.run() | |||
end | |||
@spec prepare_users(User.t(), keyword()) :: map() | |||
def prepare_users(user, opts) do | |||
friends_limit = opts[:friends_used] | |||
non_friends_limit = opts[:non_friends_used] | |||
%{ | |||
user: user, | |||
friends_local: fetch_users(user, friends_limit, :local, true), | |||
friends_remote: fetch_users(user, friends_limit, :external, true), | |||
non_friends_local: fetch_users(user, non_friends_limit, :local, false), | |||
non_friends_remote: fetch_users(user, non_friends_limit, :external, false) | |||
} | |||
end | |||
defp fetch_users(user, limit, local, friends?) do | |||
user | |||
|> get_users(limit: limit, local: local, friends?: friends?) | |||
|> Enum.shuffle() | |||
end | |||
end |
@@ -5,7 +5,6 @@ defmodule Mix.Tasks.Pleroma.Benchmarks.Tags do | |||
import Ecto.Query | |||
alias Pleroma.Repo | |||
alias Pleroma.Web.MastodonAPI.TimelineController | |||
def run(_args) do | |||
Mix.Pleroma.start_pleroma() | |||
@@ -37,7 +36,7 @@ defmodule Mix.Tasks.Pleroma.Benchmarks.Tags do | |||
Benchee.run( | |||
%{ | |||
"Hashtag fetching, any" => fn tags -> | |||
TimelineController.hashtag_fetching( | |||
hashtag_fetching( | |||
%{ | |||
"any" => tags | |||
}, | |||
@@ -47,7 +46,7 @@ defmodule Mix.Tasks.Pleroma.Benchmarks.Tags do | |||
end, | |||
# Will always return zero results because no overlapping hashtags are generated. | |||
"Hashtag fetching, all" => fn tags -> | |||
TimelineController.hashtag_fetching( | |||
hashtag_fetching( | |||
%{ | |||
"all" => tags | |||
}, | |||
@@ -67,7 +66,7 @@ defmodule Mix.Tasks.Pleroma.Benchmarks.Tags do | |||
Benchee.run( | |||
%{ | |||
"Hashtag fetching" => fn tag -> | |||
TimelineController.hashtag_fetching( | |||
hashtag_fetching( | |||
%{ | |||
"tag" => tag | |||
}, | |||
@@ -80,4 +79,35 @@ defmodule Mix.Tasks.Pleroma.Benchmarks.Tags do | |||
time: 5 | |||
) | |||
end | |||
defp hashtag_fetching(params, user, local_only) do | |||
tags = | |||
[params["tag"], params["any"]] | |||
|> List.flatten() | |||
|> Enum.uniq() | |||
|> Enum.filter(& &1) | |||
|> Enum.map(&String.downcase(&1)) | |||
tag_all = | |||
params | |||
|> Map.get("all", []) | |||
|> Enum.map(&String.downcase(&1)) | |||
tag_reject = | |||
params | |||
|> Map.get("none", []) | |||
|> Enum.map(&String.downcase(&1)) | |||
_activities = | |||
params | |||
|> Map.put("type", "Create") | |||
|> Map.put("local_only", local_only) | |||
|> Map.put("blocking_user", user) | |||
|> Map.put("muting_user", user) | |||
|> Map.put("user", user) | |||
|> Map.put("tag", tags) | |||
|> Map.put("tag_all", tag_all) | |||
|> Map.put("tag_reject", tag_reject) | |||
|> Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities() | |||
end | |||
end |
@@ -184,7 +184,7 @@ config :pleroma, :instance, | |||
name: "Pleroma", | |||
email: "example@example.com", | |||
notify_email: "noreply@example.com", | |||
description: "A Pleroma instance, an alternative fediverse server", | |||
description: "Pleroma: An efficient and flexible fediverse server", | |||
background_image: "/images/city.jpg", | |||
limit: 5_000, | |||
chat_limit: 5_000, | |||
@@ -547,7 +547,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret | |||
```json | |||
{ | |||
"totalReports" : 1, | |||
"total" : 1, | |||
"reports": [ | |||
{ | |||
"account": { | |||
@@ -768,7 +768,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret | |||
- 400 Bad Request `"Invalid parameters"` when `status` is missing | |||
- On success: `204`, empty response | |||
## `POST /api/pleroma/admin/reports/:report_id/notes/:id` | |||
## `DELETE /api/pleroma/admin/reports/:report_id/notes/:id` | |||
### Delete report note | |||
@@ -42,6 +42,12 @@ Feel free to contact us to be added to this list! | |||
- Platforms: SailfishOS | |||
- Features: No Streaming | |||
### Husky | |||
- Source code: <https://git.mentality.rip/FWGS/Husky> | |||
- Contact: [@Husky@enigmatic.observer](https://enigmatic.observer/users/Husky) | |||
- Platforms: Android | |||
- Features: No Streaming, Emoji Reactions, Text Formatting, FE Stickers | |||
### Nekonium | |||
- Homepage: [F-Droid Repository](https://repo.gdgd.jp.net/), [Google Play](https://play.google.com/store/apps/details?id=com.apps.nekonium), [Amazon](https://www.amazon.co.jp/dp/B076FXPRBC/) | |||
- Source: <https://gogs.gdgd.jp.net/lin/nekonium> | |||
@@ -24,6 +24,6 @@ defmodule Pleroma.Constants do | |||
const(static_only_files, | |||
do: | |||
~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc) | |||
~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc embed.js embed.css) | |||
) | |||
end |
@@ -17,14 +17,6 @@ defmodule Pleroma.Helpers.UriHelper do | |||
|> URI.to_string() | |||
end | |||
def append_param_if_present(%{} = params, param_name, param_value) do | |||
if param_value do | |||
Map.put(params, param_name, param_value) | |||
else | |||
params | |||
end | |||
end | |||
def maybe_add_base("/" <> uri, base), do: Path.join([base, uri]) | |||
def maybe_add_base(uri, _base), do: uri | |||
end |
@@ -0,0 +1,15 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.Maps do | |||
def put_if_present(map, key, value, value_function \\ &{:ok, &1}) when is_map(map) do | |||
with false <- is_nil(key), | |||
false <- is_nil(value), | |||
{:ok, new_value} <- value_function.(value) do | |||
Map.put(map, key, new_value) | |||
else | |||
_ -> map | |||
end | |||
end | |||
end |
@@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do | |||
alias Pleroma.Constants | |||
alias Pleroma.Conversation | |||
alias Pleroma.Conversation.Participation | |||
alias Pleroma.Maps | |||
alias Pleroma.Notification | |||
alias Pleroma.Object | |||
alias Pleroma.Object.Containment | |||
@@ -19,7 +20,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do | |||
alias Pleroma.User | |||
alias Pleroma.Web.ActivityPub.MRF | |||
alias Pleroma.Web.ActivityPub.Transmogrifier | |||
alias Pleroma.Web.ActivityPub.Utils | |||
alias Pleroma.Web.Streamer | |||
alias Pleroma.Web.WebFinger | |||
alias Pleroma.Workers.BackgroundWorker | |||
@@ -168,12 +168,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do | |||
}) | |||
# Splice in the child object if we have one. | |||
activity = | |||
if not is_nil(object) do | |||
Map.put(activity, :object, object) | |||
else | |||
activity | |||
end | |||
activity = Maps.put_if_present(activity, :object, object) | |||
BackgroundWorker.enqueue("fetch_data_for_activity", %{"activity_id" => activity.id}) | |||
@@ -335,7 +330,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do | |||
with data <- | |||
%{"to" => to, "type" => type, "actor" => actor.ap_id, "object" => object} | |||
|> Utils.maybe_put("id", activity_id), | |||
|> Maps.put_if_present("id", activity_id), | |||
{:ok, activity} <- insert(data, local), | |||
_ <- notify_and_stream(activity), | |||
:ok <- maybe_federate(activity) do | |||
@@ -355,7 +350,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do | |||
"actor" => actor, | |||
"object" => object | |||
}, | |||
data <- Utils.maybe_put(data, "id", activity_id), | |||
data <- Maps.put_if_present(data, "id", activity_id), | |||
{:ok, activity} <- insert(data, local), | |||
_ <- notify_and_stream(activity), | |||
:ok <- maybe_federate(activity) do | |||
@@ -947,6 +942,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do | |||
where: fragment("not (? && ?)", activity.recipients, ^blocked_ap_ids), | |||
where: | |||
fragment( | |||
"recipients_contain_blocked_domains(?, ?) = false", | |||
activity.recipients, | |||
^domain_blocks | |||
), | |||
where: | |||
fragment( | |||
"not (?->>'type' = 'Announce' and ?->'to' \\?| ?)", | |||
activity.data, | |||
activity.data, | |||
@@ -1241,12 +1242,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do | |||
@spec upload(Upload.source(), keyword()) :: {:ok, Object.t()} | {:error, any()} | |||
def upload(file, opts \\ []) do | |||
with {:ok, data} <- Upload.store(file, opts) do | |||
obj_data = | |||
if opts[:actor] do | |||
Map.put(data, "actor", opts[:actor]) | |||
else | |||
data | |||
end | |||
obj_data = Maps.put_if_present(data, "actor", opts[:actor]) | |||
Repo.insert(%Object{data: obj_data}) | |||
end | |||
@@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do | |||
alias Pleroma.EarmarkRenderer | |||
alias Pleroma.FollowingRelationship | |||
alias Pleroma.Notification | |||
alias Pleroma.Maps | |||
alias Pleroma.Object | |||
alias Pleroma.Object.Containment | |||
alias Pleroma.Repo | |||
@@ -209,12 +210,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do | |||
|> Map.put("conversation", context) | |||
end | |||
defp add_if_present(map, _key, nil), do: map | |||
defp add_if_present(map, key, value) do | |||
Map.put(map, key, value) | |||
end | |||
def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do | |||
attachments = | |||
Enum.map(attachment, fn data -> | |||
@@ -242,13 +237,13 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do | |||
attachment_url = | |||
%{"href" => href} | |||
|> add_if_present("mediaType", media_type) | |||
|> add_if_present("type", Map.get(url || %{}, "type")) | |||
|> Maps.put_if_present("mediaType", media_type) | |||
|> Maps.put_if_present("type", Map.get(url || %{}, "type")) | |||
%{"url" => [attachment_url]} | |||
|> add_if_present("mediaType", media_type) | |||
|> add_if_present("type", data["type"]) | |||
|> add_if_present("name", data["name"]) | |||
|> Maps.put_if_present("mediaType", media_type) | |||
|> Maps.put_if_present("type", data["type"]) | |||
|> Maps.put_if_present("name", data["name"]) | |||
end) | |||
Map.put(object, "attachment", attachments) | |||
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do | |||
alias Ecto.UUID | |||
alias Pleroma.Activity | |||
alias Pleroma.Config | |||
alias Pleroma.Maps | |||
alias Pleroma.Notification | |||
alias Pleroma.Object | |||
alias Pleroma.Repo | |||
@@ -307,7 +308,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do | |||
"cc" => cc, | |||
"context" => object.data["context"] | |||
} | |||
|> maybe_put("id", activity_id) | |||
|> Maps.put_if_present("id", activity_id) | |||
end | |||
def make_emoji_reaction_data(user, object, emoji, activity_id) do | |||
@@ -477,7 +478,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do | |||
"object" => followed_id, | |||
"state" => "pending" | |||
} | |||
|> maybe_put("id", activity_id) | |||
|> Maps.put_if_present("id", activity_id) | |||
end | |||
def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do | |||
@@ -546,7 +547,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do | |||
"cc" => [], | |||
"context" => object.data["context"] | |||
} | |||
|> maybe_put("id", activity_id) | |||
|> Maps.put_if_present("id", activity_id) | |||
end | |||
def make_announce_data( | |||
@@ -563,7 +564,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do | |||
"cc" => [Pleroma.Constants.as_public()], | |||
"context" => object.data["context"] | |||
} | |||
|> maybe_put("id", activity_id) | |||
|> Maps.put_if_present("id", activity_id) | |||
end | |||
def make_undo_data( | |||
@@ -582,7 +583,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do | |||
"cc" => [Pleroma.Constants.as_public()], | |||
"context" => context | |||
} | |||
|> maybe_put("id", activity_id) | |||
|> Maps.put_if_present("id", activity_id) | |||
end | |||
@spec add_announce_to_object(Activity.t(), Object.t()) :: | |||
@@ -627,7 +628,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do | |||
"to" => [followed.ap_id], | |||
"object" => follow_activity.data | |||
} | |||
|> maybe_put("id", activity_id) | |||
|> Maps.put_if_present("id", activity_id) | |||
end | |||
#### Block-related helpers | |||
@@ -650,7 +651,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do | |||
"to" => [blocked.ap_id], | |||
"object" => blocked.ap_id | |||
} | |||
|> maybe_put("id", activity_id) | |||
|> Maps.put_if_present("id", activity_id) | |||
end | |||
#### Create-related helpers | |||
@@ -740,6 +741,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do | |||
def get_reports(params, page, page_size) do | |||
params = | |||
params | |||
|> Map.new(fn {key, value} -> {to_string(key), value} end) | |||
|> Map.put("type", "Flag") | |||
|> Map.put("skip_preload", true) | |||
|> Map.put("preload_report_notes", true) | |||
@@ -870,7 +872,4 @@ defmodule Pleroma.Web.ActivityPub.Utils do | |||
|> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data)) | |||
|> Repo.all() | |||
end | |||
def maybe_put(map, _key, nil), do: map | |||
def maybe_put(map, key, value), do: Map.put(map, key, value) | |||
end |
@@ -7,38 +7,25 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do | |||
import Pleroma.Web.ControllerHelper, only: [json_response: 3] | |||
alias Pleroma.Activity | |||
alias Pleroma.Config | |||
alias Pleroma.ConfigDB | |||
alias Pleroma.MFA | |||
alias Pleroma.ModerationLog | |||
alias Pleroma.Plugs.OAuthScopesPlug | |||
alias Pleroma.ReportNote | |||
alias Pleroma.Stats | |||
alias Pleroma.User | |||
alias Pleroma.UserInviteToken | |||
alias Pleroma.Web.ActivityPub.ActivityPub | |||
alias Pleroma.Web.ActivityPub.Builder | |||
alias Pleroma.Web.ActivityPub.Pipeline | |||
alias Pleroma.Web.ActivityPub.Relay | |||
alias Pleroma.Web.ActivityPub.Utils | |||
alias Pleroma.Web.AdminAPI | |||
alias Pleroma.Web.AdminAPI.AccountView | |||
alias Pleroma.Web.AdminAPI.ConfigView | |||
alias Pleroma.Web.AdminAPI.ModerationLogView | |||
alias Pleroma.Web.AdminAPI.Report | |||
alias Pleroma.Web.AdminAPI.ReportView | |||
alias Pleroma.Web.AdminAPI.Search | |||
alias Pleroma.Web.CommonAPI | |||
alias Pleroma.Web.Endpoint | |||
alias Pleroma.Web.MastodonAPI | |||
alias Pleroma.Web.MastodonAPI.AppView | |||
alias Pleroma.Web.OAuth.App | |||
alias Pleroma.Web.Router | |||
require Logger | |||
@descriptions Pleroma.Docs.JSON.compile() | |||
@users_page_size 50 | |||
plug( | |||
@@ -69,14 +56,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do | |||
] | |||
) | |||
plug(OAuthScopesPlug, %{scopes: ["read:invites"], admin: true} when action == :invites) | |||
plug( | |||
OAuthScopesPlug, | |||
%{scopes: ["write:invites"], admin: true} | |||
when action in [:create_invite_token, :revoke_invite, :email_invite] | |||
) | |||
plug( | |||
OAuthScopesPlug, | |||
%{scopes: ["write:follows"], admin: true} | |||
@@ -85,18 +64,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do | |||
plug( | |||
OAuthScopesPlug, | |||
%{scopes: ["read:reports"], admin: true} | |||
when action in [:list_reports, :report_show] | |||
) | |||
plug( | |||
OAuthScopesPlug, | |||
%{scopes: ["write:reports"], admin: true} | |||
when action in [:reports_update, :report_notes_create, :report_notes_delete] | |||
) | |||
plug( | |||
OAuthScopesPlug, | |||
%{scopes: ["read:statuses"], admin: true} | |||
when action in [:list_user_statuses, :list_instance_statuses] | |||
) | |||
@@ -105,11 +72,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do | |||
OAuthScopesPlug, | |||
%{scopes: ["read"], admin: true} | |||
when action in [ | |||
:config_show, | |||
:list_log, | |||
:stats, | |||
:relay_list, | |||
:config_descriptions, | |||
:need_reboot | |||
] | |||
) | |||
@@ -119,13 +84,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do | |||
%{scopes: ["write"], admin: true} | |||
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 | |||
] | |||
) | |||
@@ -294,7 +254,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do | |||
}) | |||
conn | |||
|> put_view(MastodonAPI.StatusView) | |||
|> put_view(AdminAPI.StatusView) | |||
|> render("index.json", %{activities: activities, as: :activity}) | |||
else | |||
_ -> {:error, :not_found} | |||
@@ -575,69 +535,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do | |||
end | |||
end | |||
@doc "Sends registration invite via email" | |||
def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do | |||
with {_, false} <- {:registrations_open, Config.get([:instance, :registrations_open])}, | |||
{_, true} <- {:invites_enabled, Config.get([:instance, :invites_enabled])}, | |||
{:ok, invite_token} <- UserInviteToken.create_invite(), | |||
email <- | |||
Pleroma.Emails.UserEmail.user_invitation_email( | |||
user, | |||
invite_token, | |||
email, | |||
params["name"] | |||
), | |||
{:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do | |||
json_response(conn, :no_content, "") | |||
else | |||
{:registrations_open, _} -> | |||
{:error, "To send invites you need to set the `registrations_open` option to false."} | |||
{:invites_enabled, _} -> | |||
{:error, "To send invites you need to set the `invites_enabled` option to true."} | |||
end | |||
end | |||
@doc "Create an account registration invite token" | |||
def create_invite_token(conn, params) do | |||
opts = %{} | |||
opts = | |||
if params["max_use"], | |||
do: Map.put(opts, :max_use, params["max_use"]), | |||
else: opts | |||
opts = | |||
if params["expires_at"], | |||
do: Map.put(opts, :expires_at, params["expires_at"]), | |||
else: opts | |||
{:ok, invite} = UserInviteToken.create_invite(opts) | |||
json(conn, AccountView.render("invite.json", %{invite: invite})) | |||
end | |||
@doc "Get list of created invites" | |||
def invites(conn, _params) do | |||
invites = UserInviteToken.list_invites() | |||
conn | |||
|> put_view(AccountView) | |||
|> render("invites.json", %{invites: invites}) | |||
end | |||
@doc "Revokes invite by token" | |||
def revoke_invite(conn, %{"token" => token}) do | |||
with {:ok, invite} <- UserInviteToken.find_by_token(token), | |||
{:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do | |||
conn | |||
|> put_view(AccountView) | |||
|> render("invite.json", %{invite: updated_invite}) | |||
else | |||
nil -> {:error, :not_found} | |||
end | |||
end | |||
@doc "Get a password reset token (base64 string) for given nickname" | |||
def get_password_reset(conn, %{"nickname" => nickname}) do | |||
(%User{local: true} = user) = User.get_cached_by_nickname(nickname) | |||
@@ -724,85 +621,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do | |||
end | |||
end | |||
def list_reports(conn, params) do | |||
{page, page_size} = page_params(params) | |||
reports = Utils.get_reports(params, page, page_size) | |||
conn | |||
|> put_view(ReportView) | |||
|> render("index.json", %{reports: reports}) | |||
end | |||
def report_show(conn, %{"id" => id}) do | |||
with %Activity{} = report <- Activity.get_by_id(id) do | |||
conn | |||
|> put_view(ReportView) | |||
|> render("show.json", Report.extract_report_info(report)) | |||
else | |||
_ -> {:error, :not_found} | |||
end | |||
end | |||
def reports_update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) do | |||
result = | |||
reports | |||
|> Enum.map(fn report -> | |||
with {:ok, activity} <- CommonAPI.update_report_state(report["id"], report["state"]) do | |||
ModerationLog.insert_log(%{ | |||
action: "report_update", | |||
actor: admin, | |||
subject: activity | |||
}) | |||
activity | |||
else | |||
{:error, message} -> %{id: report["id"], error: message} | |||
end | |||
end) | |||
case Enum.any?(result, &Map.has_key?(&1, :error)) do | |||
true -> json_response(conn, :bad_request, result) | |||
false -> json_response(conn, :no_content, "") | |||
end | |||
end | |||
def report_notes_create(%{assigns: %{user: user}} = conn, %{ | |||
"id" => report_id, | |||
"content" => content | |||
}) do | |||
with {:ok, _} <- ReportNote.create(user.id, report_id, content) do | |||
ModerationLog.insert_log(%{ | |||
action: "report_note", | |||
actor: user, | |||
subject: Activity.get_by_id(report_id), | |||
text: content | |||
}) | |||
json_response(conn, :no_content, "") | |||
else | |||
_ -> json_response(conn, :bad_request, "") | |||
end | |||
end | |||
def report_notes_delete(%{assigns: %{user: user}} = conn, %{ | |||
"id" => note_id, | |||
"report_id" => report_id | |||
}) do | |||
with {:ok, note} <- ReportNote.destroy(note_id) do | |||
ModerationLog.insert_log(%{ | |||
action: "report_note_delete", | |||
actor: user, | |||
subject: Activity.get_by_id(report_id), | |||
text: note.content | |||
}) | |||
json_response(conn, :no_content, "") | |||
else | |||
_ -> json_response(conn, :bad_request, "") | |||
end | |||
end | |||
def list_log(conn, params) do | |||
{page, page_size} = page_params(params) | |||
@@ -821,105 +639,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do | |||
|> render("index.json", %{log: log}) | |||
end | |||
def config_descriptions(conn, _params) do | |||
descriptions = Enum.filter(@descriptions, &whitelisted_config?/1) | |||
json(conn, descriptions) | |||
end | |||
def config_show(conn, %{"only_db" => true}) do | |||
with :ok <- configurable_from_database() do | |||
configs = Pleroma.Repo.all(ConfigDB) | |||
conn | |||
|> put_view(ConfigView) | |||
|> render("index.json", %{configs: configs}) | |||
end | |||
end | |||
def config_show(conn, _params) do | |||
with :ok <- configurable_from_database() do | |||
configs = ConfigDB.get_all_as_keyword() | |||
merged = | |||
Config.Holder.default_config() | |||
|> ConfigDB.merge(configs) | |||
|> Enum.map(fn {group, values} -> | |||
Enum.map(values, fn {key, value} -> | |||
db = | |||
if configs[group][key] do | |||
ConfigDB.get_db_keys(configs[group][key], key) | |||
end | |||
db_value = configs[group][key] | |||
merged_value = | |||
if !is_nil(db_value) and Keyword.keyword?(db_value) and | |||
ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do | |||
ConfigDB.merge_group(group, key, value, db_value) | |||
else | |||
value | |||
end | |||
setting = %{ | |||
group: ConfigDB.convert(group), | |||
key: ConfigDB.convert(key), | |||
value: ConfigDB.convert(merged_value) | |||
} | |||
if db, do: Map.put(setting, :db, db), else: setting | |||
end) | |||
end) | |||
|> List.flatten() | |||
json(conn, %{configs: merged, need_reboot: Restarter.Pleroma.need_reboot?()}) | |||
end | |||
end | |||
def config_update(conn, %{"configs" => configs}) do | |||
with :ok <- configurable_from_database() do | |||
{_errors, results} = | |||
configs | |||
|> Enum.filter(&whitelisted_config?/1) | |||
|> Enum.map(fn | |||
%{"group" => group, "key" => key, "delete" => true} = params -> | |||
ConfigDB.delete(%{group: group, key: key, subkeys: params["subkeys"]}) | |||
%{"group" => group, "key" => key, "value" => value} -> | |||
ConfigDB.update_or_create(%{group: group, key: key, value: value}) | |||
end) | |||
|> Enum.split_with(fn result -> elem(result, 0) == :error end) | |||
{deleted, updated} = | |||
results | |||
|> Enum.map(fn {:ok, config} -> | |||
Map.put(config, :db, ConfigDB.get_db_keys(config)) | |||
end) | |||
|> Enum.split_with(fn config -> | |||
Ecto.get_meta(config, :state) == :deleted | |||
end) | |||
Config.TransferTask.load_and_update_env(deleted, false) | |||
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) | |||
if changed_reboot_settings?, do: Restarter.Pleroma.need_reboot() | |||
end | |||
conn | |||
|> put_view(ConfigView) | |||
|> render("index.json", %{configs: updated, need_reboot: Restarter.Pleroma.need_reboot?()}) | |||
end | |||
end | |||
def restart(conn, _params) do | |||
with :ok <- configurable_from_database() do | |||
Restarter.Pleroma.restart(Config.get(:env), 50) | |||
@@ -940,28 +659,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do | |||
end | |||
end | |||
defp whitelisted_config?(group, key) do | |||
if whitelisted_configs = Config.get(:database_config_whitelist) do | |||
Enum.any?(whitelisted_configs, fn | |||
{whitelisted_group} -> | |||
group == inspect(whitelisted_group) | |||
{whitelisted_group, whitelisted_key} -> | |||
group == inspect(whitelisted_group) && key == inspect(whitelisted_key) | |||
end) | |||
else | |||
true | |||
end | |||
end | |||
defp whitelisted_config?(%{"group" => group, "key" => key}) do | |||
whitelisted_config?(group, key) | |||
end | |||
defp whitelisted_config?(%{:group => group} = config) do | |||
whitelisted_config?(group, config[:key]) | |||
end | |||
def reload_emoji(conn, _params) do | |||
Pleroma.Emoji.reload() | |||
@@ -996,83 +693,6 @@ 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() | |||
@@ -0,0 +1,152 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.Web.AdminAPI.ConfigController do | |||
use Pleroma.Web, :controller | |||
alias Pleroma.Config | |||
alias Pleroma.ConfigDB | |||
alias Pleroma.Plugs.OAuthScopesPlug | |||
@descriptions Pleroma.Docs.JSON.compile() | |||
plug(Pleroma.Web.ApiSpec.CastAndValidate) | |||
plug(OAuthScopesPlug, %{scopes: ["write"], admin: true} when action == :update) | |||
plug( | |||
OAuthScopesPlug, | |||
%{scopes: ["read"], admin: true} | |||
when action in [:show, :descriptions] | |||
) | |||
action_fallback(Pleroma.Web.AdminAPI.FallbackController) | |||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.ConfigOperation | |||
def descriptions(conn, _params) do | |||
descriptions = Enum.filter(@descriptions, &whitelisted_config?/1) | |||
json(conn, descriptions) | |||
end | |||
def show(conn, %{only_db: true}) do | |||
with :ok <- configurable_from_database() do | |||
configs = Pleroma.Repo.all(ConfigDB) | |||
render(conn, "index.json", %{configs: configs}) | |||
end | |||
end | |||
def show(conn, _params) do | |||
with :ok <- configurable_from_database() do | |||
configs = ConfigDB.get_all_as_keyword() | |||
merged = | |||
Config.Holder.default_config() | |||
|> ConfigDB.merge(configs) | |||
|> Enum.map(fn {group, values} -> | |||
Enum.map(values, fn {key, value} -> | |||
db = | |||
if configs[group][key] do | |||
ConfigDB.get_db_keys(configs[group][key], key) | |||
end | |||
db_value = configs[group][key] | |||
merged_value = | |||
if not is_nil(db_value) and Keyword.keyword?(db_value) and | |||
ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do | |||
ConfigDB.merge_group(group, key, value, db_value) | |||
else | |||
value | |||
end | |||
%{ | |||
group: ConfigDB.convert(group), | |||
key: ConfigDB.convert(key), | |||
value: ConfigDB.convert(merged_value) | |||
} | |||
|> Pleroma.Maps.put_if_present(:db, db) | |||
end) | |||
end) | |||
|> List.flatten() | |||
json(conn, %{configs: merged, need_reboot: Restarter.Pleroma.need_reboot?()}) | |||
end | |||
end | |||
def update(%{body_params: %{configs: configs}} = conn, _) do | |||
with :ok <- configurable_from_database() do | |||
results = | |||
configs | |||
|> Enum.filter(&whitelisted_config?/1) | |||
|> Enum.map(fn | |||
%{group: group, key: key, delete: true} = params -> | |||
ConfigDB.delete(%{group: group, key: key, subkeys: params[:subkeys]}) | |||
%{group: group, key: key, value: value} -> | |||
ConfigDB.update_or_create(%{group: group, key: key, value: value}) | |||
end) | |||
|> Enum.reject(fn {result, _} -> result == :error end) | |||
{deleted, updated} = | |||
results | |||
|> Enum.map(fn {:ok, config} -> | |||
Map.put(config, :db, ConfigDB.get_db_keys(config)) | |||
end) | |||
|> Enum.split_with(fn config -> | |||
Ecto.get_meta(config, :state) == :deleted | |||
end) | |||
Config.TransferTask.load_and_update_env(deleted, false) | |||
if not 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) | |||
if changed_reboot_settings?, do: Restarter.Pleroma.need_reboot() | |||
end | |||
render(conn, "index.json", %{ | |||
configs: updated, | |||
need_reboot: Restarter.Pleroma.need_reboot?() | |||
}) | |||
end | |||
end | |||
defp configurable_from_database do | |||
if Config.get(:configurable_from_database) do | |||
:ok | |||
else | |||
{:error, "To use this endpoint you need to enable configuration from database."} | |||
end | |||
end | |||
defp whitelisted_config?(group, key) do | |||
if whitelisted_configs = Config.get(:database_config_whitelist) do | |||
Enum.any?(whitelisted_configs, fn | |||
{whitelisted_group} -> | |||
group == inspect(whitelisted_group) | |||
{whitelisted_group, whitelisted_key} -> | |||
group == inspect(whitelisted_group) && key == inspect(whitelisted_key) | |||
end) | |||
else | |||
true | |||
end | |||
end | |||
defp whitelisted_config?(%{group: group, key: key}) do | |||
whitelisted_config?(group, key) | |||
end | |||
defp whitelisted_config?(%{group: group} = config) do | |||
whitelisted_config?(group, config[:key]) | |||
end | |||
end |
@@ -0,0 +1,78 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.Web.AdminAPI.InviteController do | |||
use Pleroma.Web, :controller | |||
import Pleroma.Web.ControllerHelper, only: [json_response: 3] | |||
alias Pleroma.Config | |||
alias Pleroma.Plugs.OAuthScopesPlug | |||
alias Pleroma.UserInviteToken | |||
require Logger | |||
plug(Pleroma.Web.ApiSpec.CastAndValidate) | |||
plug(OAuthScopesPlug, %{scopes: ["read:invites"], admin: true} when action == :index) | |||
plug( | |||
OAuthScopesPlug, | |||
%{scopes: ["write:invites"], admin: true} when action in [:create, :revoke, :email] | |||
) | |||
action_fallback(Pleroma.Web.AdminAPI.FallbackController) | |||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.InviteOperation | |||
@doc "Get list of created invites" | |||
def index(conn, _params) do | |||
invites = UserInviteToken.list_invites() | |||
render(conn, "index.json", invites: invites) | |||
end | |||
@doc "Create an account registration invite token" | |||
def create(%{body_params: params} = conn, _) do | |||
{:ok, invite} = UserInviteToken.create_invite(params) | |||
render(conn, "show.json", invite: invite) | |||
end | |||
@doc "Revokes invite by token" | |||
def revoke(%{body_params: %{token: token}} = conn, _) do | |||
with {:ok, invite} <- UserInviteToken.find_by_token(token), | |||
{:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do | |||
render(conn, "show.json", invite: updated_invite) | |||
else | |||
nil -> {:error, :not_found} | |||
error -> error | |||
end | |||
end | |||
@doc "Sends registration invite via email" | |||
def email(%{assigns: %{user: user}, body_params: %{email: email} = params} = conn, _) do | |||
with {_, false} <- {:registrations_open, Config.get([:instance, :registrations_open])}, | |||
{_, true} <- {:invites_enabled, Config.get([:instance, :invites_enabled])}, | |||
{:ok, invite_token} <- UserInviteToken.create_invite(), | |||
{:ok, _} <- | |||
user | |||
|> Pleroma.Emails.UserEmail.user_invitation_email( | |||
invite_token, | |||
email, | |||
params[:name] | |||
) | |||
|> Pleroma.Emails.Mailer.deliver() do | |||
json_response(conn, :no_content, "") | |||
else | |||
{:registrations_open, _} -> | |||
{:error, "To send invites you need to set the `registrations_open` option to false."} | |||
{:invites_enabled, _} -> | |||
{:error, "To send invites you need to set the `invites_enabled` option to true."} | |||
{:error, error} -> | |||
{:error, error} | |||
end | |||
end | |||
end |
@@ -0,0 +1,77 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.Web.AdminAPI.OAuthAppController do | |||
use Pleroma.Web, :controller | |||
import Pleroma.Web.ControllerHelper, only: [json_response: 3] | |||
alias Pleroma.Plugs.OAuthScopesPlug | |||
alias Pleroma.Web.OAuth.App | |||
require Logger | |||
plug(Pleroma.Web.ApiSpec.CastAndValidate) | |||
plug(:put_view, Pleroma.Web.MastodonAPI.AppView) | |||
plug( | |||
OAuthScopesPlug, | |||
%{scopes: ["write"], admin: true} | |||
when action in [:create, :index, :update, :delete] | |||
) | |||
action_fallback(Pleroma.Web.AdminAPI.FallbackController) | |||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.OAuthAppOperation | |||
def index(conn, params) do | |||
search_params = | |||
params | |||
|> Map.take([:client_id, :page, :page_size, :trusted]) | |||
|> Map.put(:client_name, params[:name]) | |||
with {:ok, apps, count} <- App.search(search_params) do | |||
render(conn, "index.json", | |||
apps: apps, | |||
count: count, | |||
page_size: params.page_size, | |||
admin: true | |||
) | |||
end | |||
end | |||
def create(%{body_params: params} = conn, _) do | |||
params = Pleroma.Maps.put_if_present(params, :client_name, params[:name]) | |||
case App.create(params) do | |||
{:ok, app} -> | |||
render(conn, "show.json", app: app, admin: true) | |||
{:error, changeset} -> | |||
json(conn, App.errors(changeset)) | |||
end | |||
end | |||
def update(%{body_params: params} = conn, %{id: id}) do | |||
params = Pleroma.Maps.put_if_present(params, :client_name, params[:name]) | |||
with {:ok, app} <- App.update(id, params) do | |||
render(conn, "show.json", app: app, admin: true) | |||
else | |||
{:error, changeset} -> | |||
json(conn, App.errors(changeset)) | |||
nil -> | |||
json_response(conn, :bad_request, "") | |||
end | |||
end | |||
def 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 | |||
end |
@@ -0,0 +1,107 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.Web.AdminAPI.ReportController do | |||
use Pleroma.Web, :controller | |||
import Pleroma.Web.ControllerHelper, only: [json_response: 3] | |||
alias Pleroma.Activity | |||
alias Pleroma.ModerationLog | |||
alias Pleroma.Plugs.OAuthScopesPlug | |||
alias Pleroma.ReportNote | |||
alias Pleroma.Web.ActivityPub.Utils | |||
alias Pleroma.Web.AdminAPI | |||
alias Pleroma.Web.AdminAPI.Report | |||
alias Pleroma.Web.CommonAPI | |||
require Logger | |||
plug(Pleroma.Web.ApiSpec.CastAndValidate) | |||
plug(OAuthScopesPlug, %{scopes: ["read:reports"], admin: true} when action in [:index, :show]) | |||
plug( | |||
OAuthScopesPlug, | |||
%{scopes: ["write:reports"], admin: true} | |||
when action in [:update, :notes_create, :notes_delete] | |||
) | |||
action_fallback(AdminAPI.FallbackController) | |||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.ReportOperation | |||
def index(conn, params) do | |||
reports = Utils.get_reports(params, params.page, params.page_size) | |||
render(conn, "index.json", reports: reports) | |||
end | |||
def show(conn, %{id: id}) do | |||
with %Activity{} = report <- Activity.get_by_id(id) do | |||
render(conn, "show.json", Report.extract_report_info(report)) | |||
else | |||
_ -> {:error, :not_found} | |||
end | |||
end | |||
def update(%{assigns: %{user: admin}, body_params: %{reports: reports}} = conn, _) do | |||
result = | |||
Enum.map(reports, fn report -> | |||
case CommonAPI.update_report_state(report.id, report.state) do | |||
{:ok, activity} -> | |||
ModerationLog.insert_log(%{ | |||
action: "report_update", | |||
actor: admin, | |||
subject: activity | |||
}) | |||
activity | |||
{:error, message} -> | |||
%{id: report.id, error: message} | |||
end | |||
end) | |||
if Enum.any?(result, &Map.has_key?(&1, :error)) do | |||
json_response(conn, :bad_request, result) | |||
else | |||
json_response(conn, :no_content, "") | |||
end | |||
end | |||
def notes_create(%{assigns: %{user: user}, body_params: %{content: content}} = conn, %{ | |||
id: report_id | |||
}) do | |||
with {:ok, _} <- ReportNote.create(user.id, report_id, content) do | |||
ModerationLog.insert_log(%{ | |||
action: "report_note", | |||
actor: user, | |||
subject: Activity.get_by_id(report_id), | |||
text: content | |||
}) | |||
json_response(conn, :no_content, "") | |||
else | |||
_ -> json_response(conn, :bad_request, "") | |||
end | |||
end | |||
def notes_delete(%{assigns: %{user: user}} = conn, %{ | |||
id: note_id, | |||
report_id: report_id | |||
}) do | |||
with {:ok, note} <- ReportNote.destroy(note_id) do | |||
ModerationLog.insert_log(%{ | |||
action: "report_note_delete", | |||
actor: user, | |||
subject: Activity.get_by_id(report_id), | |||
text: note.content | |||
}) | |||
json_response(conn, :no_content, "") | |||
else | |||
_ -> json_response(conn, :bad_request, "") | |||
end | |||
end | |||
end |
@@ -41,9 +41,7 @@ defmodule Pleroma.Web.AdminAPI.StatusController do | |||
def show(conn, %{id: id}) do | |||
with %Activity{} = activity <- Activity.get_by_id(id) do | |||
conn | |||
|> put_view(Pleroma.Web.AdminAPI.StatusView) | |||
|> render("show.json", %{activity: activity}) | |||
render(conn, "show.json", %{activity: activity}) | |||
else | |||
nil -> {:error, :not_found} | |||
end | |||
@@ -80,24 +80,6 @@ defmodule Pleroma.Web.AdminAPI.AccountView do | |||
} | |||
end | |||
def render("invite.json", %{invite: invite}) do | |||
%{ | |||
"id" => invite.id, | |||
"token" => invite.token, | |||
"used" => invite.used, | |||
"expires_at" => invite.expires_at, | |||
"uses" => invite.uses, | |||
"max_use" => invite.max_use, | |||
"invite_type" => invite.invite_type | |||
} | |||
end | |||
def render("invites.json", %{invites: invites}) do | |||
%{ | |||
invites: render_many(invites, AccountView, "invite.json", as: :invite) | |||
} | |||
end | |||
def render("created.json", %{user: user}) do | |||
%{ | |||
type: "success", | |||
@@ -0,0 +1,25 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.Web.AdminAPI.InviteView do | |||
use Pleroma.Web, :view | |||
def render("index.json", %{invites: invites}) do | |||
%{ | |||
invites: render_many(invites, __MODULE__, "show.json", as: :invite) | |||
} | |||
end | |||
def render("show.json", %{invite: invite}) do | |||
%{ | |||
"id" => invite.id, | |||
"token" => invite.token, | |||
"used" => invite.used, | |||
"expires_at" => invite.expires_at, | |||
"uses" => invite.uses, | |||
"max_use" => invite.max_use, | |||
"invite_type" => invite.invite_type | |||
} | |||
end | |||
end |
@@ -0,0 +1,142 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.Web.ApiSpec.Admin.ConfigOperation do | |||
alias OpenApiSpex.Operation | |||
alias OpenApiSpex.Schema | |||
alias Pleroma.Web.ApiSpec.Schemas.ApiError | |||
import Pleroma.Web.ApiSpec.Helpers | |||
def open_api_operation(action) do | |||
operation = String.to_existing_atom("#{action}_operation") | |||
apply(__MODULE__, operation, []) | |||
end | |||
def show_operation do | |||
%Operation{ | |||
tags: ["Admin", "Config"], | |||
summary: "Get list of merged default settings with saved in database", | |||
operationId: "AdminAPI.ConfigController.show", | |||
parameters: [ | |||
Operation.parameter( | |||
:only_db, | |||
:query, | |||
%Schema{type: :boolean, default: false}, | |||
"Get only saved in database settings" | |||
) | |||
], | |||
security: [%{"oAuth" => ["read"]}], | |||
responses: %{ | |||
200 => Operation.response("Config", "application/json", config_response()), | |||
400 => Operation.response("Bad Request", "application/json", ApiError) | |||
} | |||
} | |||
end | |||
def update_operation do | |||
%Operation{ | |||
tags: ["Admin", "Config"], | |||
summary: "Update config settings", | |||
operationId: "AdminAPI.ConfigController.update", | |||
security: [%{"oAuth" => ["write"]}], | |||
requestBody: | |||
request_body("Parameters", %Schema{ | |||
type: :object, | |||
properties: %{ | |||
configs: %Schema{ | |||
type: :array, | |||
items: %Schema{ | |||
type: :object, | |||
properties: %{ | |||
group: %Schema{type: :string}, | |||
key: %Schema{type: :string}, | |||
value: any(), | |||
delete: %Schema{type: :boolean}, | |||
subkeys: %Schema{type: :array, items: %Schema{type: :string}} | |||
} | |||
} | |||
} | |||
} | |||
}), | |||
responses: %{ | |||
200 => Operation.response("Config", "application/json", config_response()), | |||
400 => Operation.response("Bad Request", "application/json", ApiError) | |||
} | |||
} | |||
end | |||
def descriptions_operation do | |||
%Operation{ | |||
tags: ["Admin", "Config"], | |||
summary: "Get JSON with config descriptions.", | |||
operationId: "AdminAPI.ConfigController.descriptions", | |||
security: [%{"oAuth" => ["read"]}], | |||
responses: %{ | |||
200 => | |||
Operation.response("Config Descriptions", "application/json", %Schema{ | |||
type: :array, | |||
items: %Schema{ | |||
type: :object, | |||
properties: %{ | |||
group: %Schema{type: :string}, | |||
key: %Schema{type: :string}, | |||
type: %Schema{oneOf: [%Schema{type: :string}, %Schema{type: :array}]}, | |||
description: %Schema{type: :string}, | |||
children: %Schema{ | |||
type: :array, | |||
items: %Schema{ | |||
type: :object, | |||
properties: %{ | |||
key: %Schema{type: :string}, | |||
type: %Schema{oneOf: [%Schema{type: :string}, %Schema{type: :array}]}, | |||
description: %Schema{type: :string}, | |||
suggestions: %Schema{type: :array} | |||
} | |||
} | |||
} | |||
} | |||
} | |||
}), | |||
400 => Operation.response("Bad Request", "application/json", ApiError) | |||
} | |||
} | |||
end | |||
defp any do | |||
%Schema{ | |||
oneOf: [ | |||
%Schema{type: :array}, | |||
%Schema{type: :object}, | |||
%Schema{type: :string}, | |||
%Schema{type: :integer}, | |||
%Schema{type: :boolean} | |||
] | |||
} | |||
end | |||
defp config_response do | |||
%Schema{ | |||
type: :object, | |||
properties: %{ | |||
configs: %Schema{ | |||
type: :array, | |||
items: %Schema{ | |||
type: :object, | |||
properties: %{ | |||
group: %Schema{type: :string}, | |||
key: %Schema{type: :string}, | |||
value: any() | |||
} | |||
} | |||
}, | |||
need_reboot: %Schema{ | |||
type: :boolean, | |||
description: | |||
"If `need_reboot` is `true`, instance must be restarted, so reboot time settings can take effect" | |||
} | |||
} | |||
} | |||
end | |||
end |
@@ -0,0 +1,148 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.Web.ApiSpec.Admin.InviteOperation do | |||
alias OpenApiSpex.Operation | |||
alias OpenApiSpex.Schema | |||
alias Pleroma.Web.ApiSpec.Schemas.ApiError | |||
import Pleroma.Web.ApiSpec.Helpers | |||
def open_api_operation(action) do | |||
operation = String.to_existing_atom("#{action}_operation") | |||
apply(__MODULE__, operation, []) | |||
end | |||
def index_operation do | |||
%Operation{ | |||
tags: ["Admin", "Invites"], | |||
summary: "Get a list of generated invites", | |||
operationId: "AdminAPI.InviteController.index", | |||
security: [%{"oAuth" => ["read:invites"]}], | |||
responses: %{ | |||
200 => | |||
Operation.response("Invites", "application/json", %Schema{ | |||
type: :object, | |||
properties: %{ | |||
invites: %Schema{type: :array, items: invite()} | |||
}, | |||
example: %{ | |||
"invites" => [ | |||
%{ | |||
"id" => 123, | |||
"token" => "kSQtDj_GNy2NZsL9AQDFIsHN5qdbguB6qRg3WHw6K1U=", | |||
"used" => true, | |||
"expires_at" => nil, | |||
"uses" => 0, | |||
"max_use" => nil, | |||
"invite_type" => "one_time" | |||
} | |||
] | |||
} | |||
}) | |||
} | |||
} | |||
end | |||
def create_operation do | |||
%Operation{ | |||
tags: ["Admin", "Invites"], | |||
summary: "Create an account registration invite token", | |||
operationId: "AdminAPI.InviteController.create", | |||
security: [%{"oAuth" => ["write:invites"]}], | |||
requestBody: | |||
request_body("Parameters", %Schema{ | |||
type: :object, | |||
properties: %{ | |||
max_use: %Schema{type: :integer}, | |||
expires_at: %Schema{type: :string, format: :date, example: "2020-04-20"} | |||
} | |||
}), | |||
responses: %{ | |||
200 => Operation.response("Invite", "application/json", invite()) | |||
} | |||
} | |||
end | |||
def revoke_operation do | |||
%Operation{ | |||
tags: ["Admin", "Invites"], | |||
summary: "Revoke invite by token", | |||
operationId: "AdminAPI.InviteController.revoke", | |||
security: [%{"oAuth" => ["write:invites"]}], | |||
requestBody: | |||
request_body( | |||
"Parameters", | |||
%Schema{ | |||
type: :object, | |||
required: [:token], | |||
properties: %{ | |||
token: %Schema{type: :string} | |||
} | |||
}, | |||
required: true | |||
), | |||
responses: %{ | |||
200 => Operation.response("Invite", "application/json", invite()), | |||
400 => Operation.response("Bad Request", "application/json", ApiError), | |||
404 => Operation.response("Not Found", "application/json", ApiError) | |||
} | |||
} | |||
end | |||
def email_operation do | |||
%Operation{ | |||
tags: ["Admin", "Invites"], | |||
summary: "Sends registration invite via email", | |||
operationId: "AdminAPI.InviteController.email", | |||
security: [%{"oAuth" => ["write:invites"]}], | |||
requestBody: | |||
request_body( | |||
"Parameters", | |||
%Schema{ | |||
type: :object, | |||
required: [:email], | |||
properties: %{ | |||
email: %Schema{type: :string, format: :email}, | |||
name: %Schema{type: :string} | |||
} | |||
}, | |||
required: true | |||
), | |||
responses: %{ | |||
204 => no_content_response(), | |||
400 => Operation.response("Bad Request", "application/json", ApiError), | |||
403 => Operation.response("Forbidden", "application/json", ApiError) | |||
} | |||
} | |||
end | |||
defp invite do | |||
%Schema{ | |||
title: "Invite", | |||
type: :object, | |||
properties: %{ | |||
id: %Schema{type: :integer}, | |||
token: %Schema{type: :string}, | |||
used: %Schema{type: :boolean}, | |||
expires_at: %Schema{type: :string, format: :date, nullable: true}, | |||
uses: %Schema{type: :integer}, | |||
max_use: %Schema{type: :integer, nullable: true}, | |||
invite_type: %Schema{ | |||
type: :string, | |||
enum: ["one_time", "reusable", "date_limited", "reusable_date_limited"] | |||
} | |||
}, | |||
example: %{ | |||
"id" => 123, | |||
"token" => "kSQtDj_GNy2NZsL9AQDFIsHN5qdbguB6qRg3WHw6K1U=", | |||
"used" => true, | |||
"expires_at" => nil, | |||
"uses" => 0, | |||
"max_use" => nil, | |||
"invite_type" => "one_time" | |||
} | |||
} | |||
end | |||
end |
@@ -0,0 +1,215 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do | |||
alias OpenApiSpex.Operation | |||
alias OpenApiSpex.Schema | |||
alias Pleroma.Web.ApiSpec.Schemas.ApiError | |||
import Pleroma.Web.ApiSpec.Helpers | |||
def open_api_operation(action) do | |||
operation = String.to_existing_atom("#{action}_operation") | |||
apply(__MODULE__, operation, []) | |||
end | |||
def index_operation do | |||
%Operation{ | |||
summary: "List OAuth apps", | |||
tags: ["Admin", "oAuth Apps"], | |||
operationId: "AdminAPI.OAuthAppController.index", | |||
security: [%{"oAuth" => ["write"]}], | |||
parameters: [ | |||
Operation.parameter(:name, :query, %Schema{type: :string}, "App name"), | |||
Operation.parameter(:client_id, :query, %Schema{type: :string}, "Client ID"), | |||
Operation.parameter(:page, :query, %Schema{type: :integer, default: 1}, "Page"), | |||
Operation.parameter( | |||
:trusted, | |||
:query, | |||
%Schema{type: :boolean, default: false}, | |||
"Trusted apps" | |||
), | |||
Operation.parameter( | |||
:page_size, | |||
:query, | |||
%Schema{type: :integer, default: 50}, | |||
"Number of apps to return" | |||
) | |||
], | |||
responses: %{ | |||
200 => | |||
Operation.response("List of apps", "application/json", %Schema{ | |||
type: :object, | |||
properties: %{ | |||
apps: %Schema{type: :array, items: oauth_app()}, | |||
count: %Schema{type: :integer}, | |||
page_size: %Schema{type: :integer} | |||
}, | |||
example: %{ | |||
"apps" => [ | |||
%{ | |||
"id" => 1, | |||
"name" => "App name", | |||
"client_id" => "yHoDSiWYp5mPV6AfsaVOWjdOyt5PhWRiafi6MRd1lSk", | |||
"client_secret" => "nLmis486Vqrv2o65eM9mLQx_m_4gH-Q6PcDpGIMl6FY", | |||
"redirect_uri" => "https://example.com/oauth-callback", | |||
"website" => "https://example.com", | |||
"trusted" => true | |||
} | |||
], | |||
"count" => 1, | |||
"page_size" => 50 | |||
} | |||
}) | |||
} | |||
} | |||
end | |||
def create_operation do | |||
%Operation{ | |||
tags: ["Admin", "oAuth Apps"], | |||
summary: "Create OAuth App", | |||
operationId: "AdminAPI.OAuthAppController.create", | |||
requestBody: request_body("Parameters", create_request()), | |||
security: [%{"oAuth" => ["write"]}], | |||
responses: %{ | |||
200 => Operation.response("App", "application/json", oauth_app()), | |||
400 => Operation.response("Bad Request", "application/json", ApiError) | |||
} | |||
} | |||
end | |||
def update_operation do | |||
%Operation{ | |||
tags: ["Admin", "oAuth Apps"], | |||
summary: "Update OAuth App", | |||
operationId: "AdminAPI.OAuthAppController.update", | |||
parameters: [id_param()], | |||
security: [%{"oAuth" => ["write"]}], | |||
requestBody: request_body("Parameters", update_request()), | |||
responses: %{ | |||
200 => Operation.response("App", "application/json", oauth_app()), | |||
400 => | |||
Operation.response("Bad Request", "application/json", %Schema{ | |||
oneOf: [ApiError, %Schema{type: :string}] | |||
}) | |||
} | |||
} | |||
end | |||
def delete_operation do | |||
%Operation{ | |||
tags: ["Admin", "oAuth Apps"], | |||
summary: "Delete OAuth App", | |||
operationId: "AdminAPI.OAuthAppController.delete", | |||
parameters: [id_param()], | |||
security: [%{"oAuth" => ["write"]}], | |||
responses: %{ | |||
204 => no_content_response(), | |||
400 => no_content_response() | |||
} | |||
} | |||
end | |||
defp create_request do | |||
%Schema{ | |||
title: "oAuthAppCreateRequest", | |||
type: :object, | |||
required: [:name, :redirect_uris], | |||
properties: %{ | |||
name: %Schema{type: :string, description: "Application Name"}, | |||
scopes: %Schema{type: :array, items: %Schema{type: :string}, description: "oAuth scopes"}, | |||
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." | |||
}, | |||
website: %Schema{ | |||
type: :string, | |||
nullable: true, | |||
description: "A URL to the homepage of the app" | |||
}, | |||
trusted: %Schema{ | |||
type: :boolean, | |||
nullable: true, | |||
default: false, | |||
description: "Is the app trusted?" | |||
} | |||
}, | |||
example: %{ | |||
"name" => "My App", | |||
"redirect_uris" => "https://myapp.com/auth/callback", | |||
"website" => "https://myapp.com/", | |||
"scopes" => ["read", "write"], | |||
"trusted" => true | |||
} | |||
} | |||
end | |||
defp update_request do | |||
%Schema{ | |||
title: "oAuthAppUpdateRequest", | |||
type: :object, | |||
properties: %{ | |||
name: %Schema{type: :string, description: "Application Name"}, | |||
scopes: %Schema{type: :array, items: %Schema{type: :string}, description: "oAuth scopes"}, | |||
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." | |||
}, | |||
website: %Schema{ | |||
type: :string, | |||
nullable: true, | |||
description: "A URL to the homepage of the app" | |||
}, | |||
trusted: %Schema{ | |||
type: :boolean, | |||
nullable: true, | |||
default: false, | |||
description: "Is the app trusted?" | |||
} | |||
}, | |||
example: %{ | |||
"name" => "My App", | |||
"redirect_uris" => "https://myapp.com/auth/callback", | |||
"website" => "https://myapp.com/", | |||
"scopes" => ["read", "write"], | |||
"trusted" => true | |||
} | |||
} | |||
end | |||
defp oauth_app do | |||
%Schema{ | |||
title: "oAuthApp", | |||
type: :object, | |||
properties: %{ | |||
id: %Schema{type: :integer}, | |||
name: %Schema{type: :string}, | |||
client_id: %Schema{type: :string}, | |||
client_secret: %Schema{type: :string}, | |||
redirect_uri: %Schema{type: :string}, | |||
website: %Schema{type: :string, nullable: true}, | |||
trusted: %Schema{type: :boolean} | |||
}, | |||
example: %{ | |||
"id" => 123, | |||
"name" => "My App", | |||
"client_id" => "TWhM-tNSuncnqN7DBJmoyeLnk6K3iJJ71KKXxgL1hPM", | |||
"client_secret" => "ZEaFUFmF0umgBX1qKJDjaU99Q31lDkOU8NutzTOoliw", | |||
"redirect_uri" => "https://myapp.com/oauth-callback", | |||
"website" => "https://myapp.com/", | |||
"trusted" => false | |||
} | |||
} | |||
end | |||
def id_param do | |||
Operation.parameter(:id, :path, :integer, "App ID", | |||
example: 1337, | |||
required: true | |||
) | |||
end | |||
end |
@@ -0,0 +1,237 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do | |||
alias OpenApiSpex.Operation | |||
alias OpenApiSpex.Schema | |||
alias Pleroma.Web.ApiSpec.Schemas.Account | |||
alias Pleroma.Web.ApiSpec.Schemas.ApiError | |||
alias Pleroma.Web.ApiSpec.Schemas.FlakeID | |||
alias Pleroma.Web.ApiSpec.Schemas.Status | |||
import Pleroma.Web.ApiSpec.Helpers | |||
def open_api_operation(action) do | |||
operation = String.to_existing_atom("#{action}_operation") | |||
apply(__MODULE__, operation, []) | |||
end | |||
def index_operation do | |||
%Operation{ | |||
tags: ["Admin", "Reports"], | |||
summary: "Get a list of reports", | |||
operationId: "AdminAPI.ReportController.index", | |||
security: [%{"oAuth" => ["read:reports"]}], | |||
parameters: [ | |||
Operation.parameter( | |||
:state, | |||
:query, | |||
report_state(), | |||
"Filter by report state" | |||
), | |||
Operation.parameter( | |||
:limit, | |||
:query, | |||
%Schema{type: :integer}, | |||
"The number of records to retrieve" | |||
), | |||
Operation.parameter( | |||
:page, | |||
:query, | |||
%Schema{type: :integer, default: 1}, | |||
"Page number" | |||
), | |||
Operation.parameter( | |||
:page_size, | |||
:query, | |||
%Schema{type: :integer, default: 50}, | |||
"Number number of log entries per page" | |||
) | |||
], | |||
responses: %{ | |||
200 => | |||
Operation.response("Response", "application/json", %Schema{ | |||
type: :object, | |||
properties: %{ | |||
total: %Schema{type: :integer}, | |||
reports: %Schema{ | |||
type: :array, | |||
items: report() | |||
} | |||
} | |||
}), | |||
403 => Operation.response("Forbidden", "application/json", ApiError) | |||
} | |||
} | |||
end | |||
def show_operation do | |||
%Operation{ | |||
tags: ["Admin", "Reports"], | |||
summary: "Get an individual report", | |||
operationId: "AdminAPI.ReportController.show", | |||
parameters: [id_param()], | |||
security: [%{"oAuth" => ["read:reports"]}], | |||
responses: %{ | |||
200 => Operation.response("Report", "application/json", report()), | |||
404 => Operation.response("Not Found", "application/json", ApiError) | |||
} | |||
} | |||
end | |||
def update_operation do | |||
%Operation{ | |||
tags: ["Admin", "Reports"], | |||
summary: "Change the state of one or multiple reports", | |||
operationId: "AdminAPI.ReportController.update", | |||
security: [%{"oAuth" => ["write:reports"]}], | |||
requestBody: request_body("Parameters", update_request(), required: true), | |||
responses: %{ | |||
204 => no_content_response(), | |||
400 => Operation.response("Bad Request", "application/json", update_400_response()), | |||
403 => Operation.response("Forbidden", "application/json", ApiError) | |||
} | |||
} | |||
end | |||
def notes_create_operation do | |||
%Operation{ | |||
tags: ["Admin", "Reports"], | |||
summary: "Create report note", | |||
operationId: "AdminAPI.ReportController.notes_create", | |||
parameters: [id_param()], | |||
requestBody: | |||
request_body("Parameters", %Schema{ | |||
type: :object, | |||
properties: %{ | |||
content: %Schema{type: :string, description: "The message"} | |||
} | |||
}), | |||
security: [%{"oAuth" => ["write:reports"]}], | |||
responses: %{ | |||
204 => no_content_response(), | |||
404 => Operation.response("Not Found", "application/json", ApiError) | |||
} | |||
} | |||
end | |||
def notes_delete_operation do | |||
%Operation{ | |||
tags: ["Admin", "Reports"], | |||
summary: "Delete report note", | |||
operationId: "AdminAPI.ReportController.notes_delete", | |||
parameters: [ | |||
Operation.parameter(:report_id, :path, :string, "Report ID"), | |||
Operation.parameter(:id, :path, :string, "Note ID") | |||
], | |||
security: [%{"oAuth" => ["write:reports"]}], | |||
responses: %{ | |||
204 => no_content_response(), | |||
404 => Operation.response("Not Found", "application/json", ApiError) | |||
} | |||
} | |||
end | |||
defp report_state do | |||
%Schema{type: :string, enum: ["open", "closed", "resolved"]} | |||
end | |||
defp id_param do | |||
Operation.parameter(:id, :path, FlakeID, "Report ID", | |||
example: "9umDrYheeY451cQnEe", | |||
required: true | |||
) | |||
end | |||
defp report do | |||
%Schema{ | |||
type: :object, | |||
properties: %{ | |||
id: FlakeID, | |||
state: report_state(), | |||
account: account_admin(), | |||
actor: account_admin(), | |||
content: %Schema{type: :string}, | |||
created_at: %Schema{type: :string, format: :"date-time"}, | |||
statuses: %Schema{type: :array, items: Status}, | |||
notes: %Schema{ | |||
type: :array, | |||
items: %Schema{ | |||
type: :object, | |||
properties: %{ | |||
id: %Schema{type: :integer}, | |||
user_id: FlakeID, | |||
content: %Schema{type: :string}, | |||
inserted_at: %Schema{type: :string, format: :"date-time"} | |||
} | |||
} | |||
} | |||
} | |||
} | |||
end | |||
defp account_admin do | |||
%Schema{ | |||
title: "Account", | |||
description: "Account view for admins", | |||
type: :object, | |||
properties: | |||
Map.merge(Account.schema().properties, %{ | |||
nickname: %Schema{type: :string}, | |||
deactivated: %Schema{type: :boolean}, | |||
local: %Schema{type: :boolean}, | |||
roles: %Schema{ | |||
type: :object, | |||
properties: %{ | |||
admin: %Schema{type: :boolean}, | |||
moderator: %Schema{type: :boolean} | |||
} | |||
}, | |||
confirmation_pending: %Schema{type: :boolean} | |||
}) | |||
} | |||
end | |||
defp update_request do | |||
%Schema{ | |||
type: :object, | |||
required: [:reports], | |||
properties: %{ | |||
reports: %Schema{ | |||
type: :array, | |||
items: %Schema{ | |||
type: :object, | |||
properties: %{ | |||
id: %Schema{allOf: [FlakeID], description: "Required, report ID"}, | |||
state: %Schema{ | |||
type: :string, | |||
description: | |||
"Required, the new state. Valid values are `open`, `closed` and `resolved`" | |||
} | |||
} | |||
}, | |||
example: %{ | |||
"reports" => [ | |||
%{"id" => "123", "state" => "closed"}, | |||
%{"id" => "1337", "state" => "resolved"} | |||
] | |||
} | |||
} | |||
} | |||
} | |||
end | |||
defp update_400_response do | |||
%Schema{ | |||
type: :array, | |||
items: %Schema{ | |||
type: :object, | |||
properties: %{ | |||
id: %Schema{allOf: [FlakeID], description: "Report ID"}, | |||
error: %Schema{type: :string, description: "Error message"} | |||
} | |||
} | |||
} | |||
end | |||
end |
@@ -74,7 +74,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.StatusOperation do | |||
parameters: [id_param()], | |||
security: [%{"oAuth" => ["read:statuses"]}], | |||
responses: %{ | |||
200 => Operation.response("Status", "application/json", Status), | |||
200 => Operation.response("Status", "application/json", status()), | |||
404 => Operation.response("Not Found", "application/json", ApiError) | |||
} | |||
} | |||
@@ -123,7 +123,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.StatusOperation do | |||
} | |||
end | |||
defp admin_account do | |||
def admin_account do | |||
%Schema{ | |||
type: :object, | |||
properties: %{ | |||
@@ -137,7 +137,7 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do | |||
"background_upload_limit" => 4_000_000, | |||
"background_image" => "/static/image.png", | |||
"banner_upload_limit" => 4_000_000, | |||
"description" => "A Pleroma instance, an alternative fediverse server", | |||
"description" => "Pleroma: An efficient and flexible fediverse server", | |||
"email" => "lain@lain.com", | |||
"languages" => ["en"], | |||
"max_toot_chars" => 5000, | |||
@@ -99,11 +99,6 @@ defmodule Pleroma.Web.ControllerHelper do | |||
render_error(conn, :not_implemented, "Can't display this activity") | |||
end | |||
@spec put_if_exist(map(), atom() | String.t(), any) :: map() | |||
def put_if_exist(map, _key, nil), do: map | |||
def put_if_exist(map, key, value), do: Map.put(map, key, value) | |||
@doc """ | |||
Returns true if request specifies to include embedded relationships in account objects. | |||
May only be used in selected account-related endpoints; has no effect for status- or | |||
@@ -0,0 +1,42 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.Web.EmbedController do | |||
use Pleroma.Web, :controller | |||
alias Pleroma.Activity | |||
alias Pleroma.Object | |||
alias Pleroma.User | |||
alias Pleroma.Web.ActivityPub.Visibility | |||
plug(:put_layout, :embed) | |||
def show(conn, %{"id" => id}) do | |||
with %Activity{local: true} = activity <- | |||
Activity.get_by_id_with_object(id), | |||
true <- Visibility.is_public?(activity.object) do | |||
{:ok, author} = User.get_or_fetch(activity.object.data["actor"]) | |||
conn | |||
|> delete_resp_header("x-frame-options") | |||
|> delete_resp_header("content-security-policy") | |||
|> render("show.html", | |||
activity: activity, | |||
author: User.sanitize_html(author), | |||
counts: get_counts(activity) | |||
) | |||
end | |||
end | |||
defp get_counts(%Activity{} = activity) do | |||
%Object{data: data} = Object.normalize(activity) | |||
%{ | |||
likes: Map.get(data, "like_count", 0), | |||
replies: Map.get(data, "repliesCount", 0), | |||
announces: Map.get(data, "announcement_count", 0) | |||
} | |||
end | |||
end |
@@ -9,14 +9,12 @@ defmodule Pleroma.Web.Feed.TagController do | |||
alias Pleroma.Web.ActivityPub.ActivityPub | |||
alias Pleroma.Web.Feed.FeedView | |||
import Pleroma.Web.ControllerHelper, only: [put_if_exist: 3] | |||
def feed(conn, %{"tag" => raw_tag} = params) do | |||
{format, tag} = parse_tag(raw_tag) | |||
activities = | |||
%{"type" => ["Create"], "tag" => tag} | |||
|> put_if_exist("max_id", params["max_id"]) | |||
|> Pleroma.Maps.put_if_present("max_id", params["max_id"]) | |||
|> ActivityPub.fetch_public_activities() | |||
conn | |||
@@ -11,8 +11,6 @@ defmodule Pleroma.Web.Feed.UserController do | |||
alias Pleroma.Web.ActivityPub.ActivityPubController | |||
alias Pleroma.Web.Feed.FeedView | |||
import Pleroma.Web.ControllerHelper, only: [put_if_exist: 3] | |||
plug(Pleroma.Plugs.SetFormatPlug when action in [:feed_redirect]) | |||
action_fallback(:errors) | |||
@@ -55,7 +53,7 @@ defmodule Pleroma.Web.Feed.UserController do | |||
"type" => ["Create"], | |||
"actor_id" => user.ap_id | |||
} | |||
|> put_if_exist("max_id", params["max_id"]) | |||
|> Pleroma.Maps.put_if_present("max_id", params["max_id"]) | |||
|> ActivityPub.fetch_public_or_unlisted_activities() | |||
conn | |||
@@ -14,6 +14,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do | |||
json_response: 3 | |||
] | |||
alias Pleroma.Maps | |||
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug | |||
alias Pleroma.Plugs.OAuthScopesPlug | |||
alias Pleroma.Plugs.RateLimiter | |||
@@ -160,23 +161,22 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do | |||
:discoverable | |||
] | |||
|> Enum.reduce(%{}, fn key, acc -> | |||
add_if_present(acc, params, key, key, &{:ok, truthy_param?(&1)}) | |||
Maps.put_if_present(acc, key, params[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, | |||
:fields_attributes, | |||
|> Maps.put_if_present(:name, params[:display_name]) | |||
|> Maps.put_if_present(:bio, params[:note]) | |||
|> Maps.put_if_present(:avatar, params[:avatar]) | |||
|> Maps.put_if_present(:banner, params[:header]) | |||
|> Maps.put_if_present(:background, params[:pleroma_background_image]) | |||
|> Maps.put_if_present( | |||
:raw_fields, | |||
params[:fields_attributes], | |||
&{: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["source"], "privacy", :default_scope) | |||
|> add_if_present(params, :actor_type, :actor_type) | |||
|> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store]) | |||
|> Maps.put_if_present(:default_scope, params[:default_scope]) | |||
|> Maps.put_if_present(:default_scope, params["source"]["privacy"]) | |||
|> Maps.put_if_present(:actor_type, params[:actor_type]) | |||
changeset = User.update_changeset(user, user_params) | |||
@@ -206,16 +206,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do | |||
} | |||
end | |||
defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do | |||
with true <- is_map(params), | |||
true <- Map.has_key?(params, params_field), | |||
{:ok, new_value} <- value_function.(Map.get(params, params_field)) do | |||
Map.put(map, map_field, new_value) | |||
else | |||
_ -> map | |||
end | |||
end | |||
defp normalize_fields_attributes(fields) do | |||
if Enum.all?(fields, &is_tuple/1) do | |||
Enum.map(fields, fn {_, v} -> v end) | |||
@@ -113,22 +113,44 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do | |||
query | |||
|> prepare_tags() | |||
|> Enum.map(fn tag -> | |||
tag = String.trim_leading(tag, "#") | |||
%{name: tag, url: tags_path <> tag} | |||
end) | |||
end | |||
defp resource_search(:v1, "hashtags", query, _options) do | |||
query | |||
|> prepare_tags() | |||
|> Enum.map(fn tag -> String.trim_leading(tag, "#") end) | |||
prepare_tags(query) | |||
end | |||
defp prepare_tags(query) do | |||
query | |||
|> String.split() | |||
|> Enum.uniq() | |||
|> Enum.filter(fn tag -> String.starts_with?(tag, "#") end) | |||
defp prepare_tags(query, add_joined_tag \\ true) do | |||
tags = | |||
query | |||
|> String.split(~r/[^#\w]+/u, trim: true) | |||
|> Enum.uniq_by(&String.downcase/1) | |||
explicit_tags = Enum.filter(tags, fn tag -> String.starts_with?(tag, "#") end) | |||
tags = | |||
if Enum.any?(explicit_tags) do | |||
explicit_tags | |||
else | |||
tags | |||
end | |||
tags = Enum.map(tags, fn tag -> String.trim_leading(tag, "#") end) | |||
if Enum.empty?(explicit_tags) && add_joined_tag do | |||
tags | |||
|> Kernel.++([joined_tag(tags)]) | |||
|> Enum.uniq_by(&String.downcase/1) | |||
else | |||
tags | |||
end | |||
end | |||
defp joined_tag(tags) do | |||
tags | |||
|> Enum.map(fn tag -> String.capitalize(tag) end) | |||
|> Enum.join() | |||
end | |||
defp with_fallback(f, fallback \\ []) do | |||
@@ -45,10 +45,6 @@ defmodule Pleroma.Web.MastodonAPI.AppView do | |||
defp with_vapid_key(data) do | |||
vapid_key = Application.get_env(:web_push_encryption, :vapid_details, [])[:public_key] | |||
if vapid_key do | |||
Map.put(data, "vapid_key", vapid_key) | |||
else | |||
data | |||
end | |||
Pleroma.Maps.put_if_present(data, "vapid_key", vapid_key) | |||
end | |||
end |
@@ -30,7 +30,7 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityView do | |||
defp with_media_attachments(data, _), do: data | |||
defp status_params(params) do | |||
data = %{ | |||
%{ | |||
text: params["status"], | |||
sensitive: params["sensitive"], | |||
spoiler_text: params["spoiler_text"], | |||
@@ -39,10 +39,6 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityView do | |||
poll: params["poll"], | |||
in_reply_to_id: params["in_reply_to_id"] | |||
} | |||
case params["media_ids"] do | |||
nil -> data | |||
media_ids -> Map.put(data, :media_ids, media_ids) | |||
end | |||
|> Pleroma.Maps.put_if_present(:media_ids, params["media_ids"]) | |||
end | |||
end |
@@ -25,12 +25,12 @@ defmodule Pleroma.Web.OAuth.App do | |||
timestamps() | |||
end | |||
@spec changeset(App.t(), map()) :: Ecto.Changeset.t() | |||
@spec changeset(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() | |||
@spec register_changeset(t(), map()) :: Ecto.Changeset.t() | |||
def register_changeset(struct, params \\ %{}) do | |||
changeset = | |||
struct | |||
@@ -52,18 +52,19 @@ defmodule Pleroma.Web.OAuth.App do | |||
end | |||
end | |||
@spec create(map()) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()} | |||
@spec create(map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()} | |||
def create(params) do | |||
with changeset <- __MODULE__.register_changeset(%__MODULE__{}, params) do | |||
Repo.insert(changeset) | |||
end | |||
%__MODULE__{} | |||
|> register_changeset(params) | |||
|> Repo.insert() | |||
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) | |||
@spec update(pos_integer(), map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()} | |||
def update(id, params) do | |||
with %__MODULE__{} = app <- Repo.get(__MODULE__, id) do | |||
app | |||
|> changeset(params) | |||
|> Repo.update() | |||
end | |||
end | |||
@@ -71,7 +72,7 @@ defmodule Pleroma.Web.OAuth.App do | |||
Gets app by attrs or create new with attrs. | |||
And updates the scopes if need. | |||
""" | |||
@spec get_or_make(map(), list(String.t())) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()} | |||
@spec get_or_make(map(), list(String.t())) :: {:ok, t()} | {:error, Ecto.Changeset.t()} | |||
def get_or_make(attrs, scopes) do | |||
with %__MODULE__{} = app <- Repo.get_by(__MODULE__, attrs) do | |||
update_scopes(app, scopes) | |||
@@ -92,7 +93,7 @@ defmodule Pleroma.Web.OAuth.App do | |||
|> Repo.update() | |||
end | |||
@spec search(map()) :: {:ok, [App.t()], non_neg_integer()} | |||
@spec search(map()) :: {:ok, [t()], non_neg_integer()} | |||
def search(params) do | |||
query = from(a in __MODULE__) | |||
@@ -128,7 +129,7 @@ defmodule Pleroma.Web.OAuth.App do | |||
{:ok, Repo.all(query), count} | |||
end | |||
@spec destroy(pos_integer()) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()} | |||
@spec destroy(pos_integer()) :: {:ok, t()} | {:error, Ecto.Changeset.t()} | |||
def destroy(id) do | |||
with %__MODULE__{} = app <- Repo.get(__MODULE__, id) do | |||
Repo.delete(app) | |||
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do | |||
use Pleroma.Web, :controller | |||
alias Pleroma.Helpers.UriHelper | |||
alias Pleroma.Maps | |||
alias Pleroma.MFA | |||
alias Pleroma.Plugs.RateLimiter | |||
alias Pleroma.Registration | |||
@@ -108,7 +109,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do | |||
if redirect_uri in String.split(app.redirect_uris) do | |||
redirect_uri = redirect_uri(conn, redirect_uri) | |||
url_params = %{access_token: token.token} | |||
url_params = UriHelper.append_param_if_present(url_params, :state, params["state"]) | |||
url_params = Maps.put_if_present(url_params, :state, params["state"]) | |||
url = UriHelper.append_uri_params(redirect_uri, url_params) | |||
redirect(conn, external: url) | |||
else | |||
@@ -147,7 +148,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do | |||
if redirect_uri in String.split(app.redirect_uris) do | |||
redirect_uri = redirect_uri(conn, redirect_uri) | |||
url_params = %{code: auth.token} | |||
url_params = UriHelper.append_param_if_present(url_params, :state, auth_attrs["state"]) | |||
url_params = Maps.put_if_present(url_params, :state, auth_attrs["state"]) | |||
url = UriHelper.append_uri_params(redirect_uri, url_params) | |||
redirect(conn, external: url) | |||
else | |||
@@ -164,10 +164,10 @@ defmodule Pleroma.Web.Router do | |||
post("/relay", AdminAPIController, :relay_follow) | |||
delete("/relay", AdminAPIController, :relay_unfollow) | |||
post("/users/invite_token", AdminAPIController, :create_invite_token) | |||
get("/users/invites", AdminAPIController, :invites) | |||
post("/users/revoke_invite", AdminAPIController, :revoke_invite) | |||
post("/users/email_invite", AdminAPIController, :email_invite) | |||
post("/users/invite_token", InviteController, :create) | |||
get("/users/invites", InviteController, :index) | |||
post("/users/revoke_invite", InviteController, :revoke) | |||
post("/users/email_invite", InviteController, :email) | |||
get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset) | |||
patch("/users/force_password_reset", AdminAPIController, :force_password_reset) | |||
@@ -183,20 +183,20 @@ defmodule Pleroma.Web.Router do | |||
patch("/users/confirm_email", AdminAPIController, :confirm_email) | |||
patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email) | |||
get("/reports", AdminAPIController, :list_reports) | |||
get("/reports/:id", AdminAPIController, :report_show) | |||
patch("/reports", AdminAPIController, :reports_update) | |||
post("/reports/:id/notes", AdminAPIController, :report_notes_create) | |||
delete("/reports/:report_id/notes/:id", AdminAPIController, :report_notes_delete) | |||
get("/reports", ReportController, :index) | |||
get("/reports/:id", ReportController, :show) | |||
patch("/reports", ReportController, :update) | |||
post("/reports/:id/notes", ReportController, :notes_create) | |||
delete("/reports/:report_id/notes/:id", ReportController, :notes_delete) | |||
get("/statuses/:id", StatusController, :show) | |||
put("/statuses/:id", StatusController, :update) | |||
delete("/statuses/:id", StatusController, :delete) | |||
get("/statuses", StatusController, :index) | |||
get("/config", AdminAPIController, :config_show) | |||
post("/config", AdminAPIController, :config_update) | |||
get("/config/descriptions", AdminAPIController, :config_descriptions) | |||
get("/config", ConfigController, :show) | |||
post("/config", ConfigController, :update) | |||
get("/config/descriptions", ConfigController, :descriptions) | |||
get("/need_reboot", AdminAPIController, :need_reboot) | |||
get("/restart", AdminAPIController, :restart) | |||
@@ -205,10 +205,10 @@ defmodule Pleroma.Web.Router do | |||
post("/reload_emoji", AdminAPIController, :reload_emoji) | |||
get("/stats", AdminAPIController, :stats) | |||
get("/oauth_app", AdminAPIController, :oauth_app_list) | |||
post("/oauth_app", AdminAPIController, :oauth_app_create) | |||
patch("/oauth_app/:id", AdminAPIController, :oauth_app_update) | |||
delete("/oauth_app/:id", AdminAPIController, :oauth_app_delete) | |||
get("/oauth_app", OAuthAppController, :index) | |||
post("/oauth_app", OAuthAppController, :create) | |||
patch("/oauth_app/:id", OAuthAppController, :update) | |||
delete("/oauth_app/:id", OAuthAppController, :delete) | |||
end | |||
scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do | |||
@@ -673,6 +673,8 @@ defmodule Pleroma.Web.Router do | |||
post("/auth/password", MastodonAPI.AuthController, :password_reset) | |||
get("/web/*path", MastoFEController, :index) | |||
get("/embed/:id", EmbedController, :show) | |||
end | |||
scope "/proxy/", Pleroma.Web.MediaProxy do | |||
@@ -0,0 +1,8 @@ | |||
<%= case @mediaType do %> | |||
<% "audio" -> %> | |||
<audio src="<%= @url %>" controls="controls"></audio> | |||
<% "video" -> %> | |||
<video src="<%= @url %>" controls="controls"></video> | |||
<% _ -> %> | |||
<img src="<%= @url %>" alt="<%= @name %>" title="<%= @name %>"> | |||
<% end %> |
@@ -0,0 +1,76 @@ | |||
<div> | |||
<div class="p-author h-card"> | |||
<a class="u-url" rel="author noopener" href="<%= @author.ap_id %>"> | |||
<div class="avatar"> | |||
<img src="<%= User.avatar_url(@author) |> MediaProxy.url %>" width="48" height="48" alt=""> | |||
</div> | |||
<span class="display-name" style="padding-left: 0.5em;"> | |||
<bdi><%= raw (@author.name |> Formatter.emojify(@author.emoji)) %></bdi> | |||
<span class="nickname"><%= full_nickname(@author) %></span> | |||
</span> | |||
</a> | |||
</div> | |||
<div class="activity-content" > | |||
<%= if status_title(@activity) != "" do %> | |||
<details <%= if open_content?() do %>open<% end %>> | |||
<summary><%= raw status_title(@activity) %></summary> | |||
<div><%= activity_content(@activity) %></div> | |||
</details> | |||
<% else %> | |||
<div><%= activity_content(@activity) %></div> | |||
<% end %> | |||
<%= for %{"name" => name, "url" => [url | _]} <- attachments(@activity) do %> | |||
<div class="attachment"> | |||
<%= if sensitive?(@activity) do %> | |||
<details class="nsfw"> | |||
<summary onClick="updateHeight()"><%= Gettext.gettext("sensitive media") %></summary> | |||
<div class="nsfw-content"> | |||
<%= render("_attachment.html", %{name: name, url: url["href"], | |||
mediaType: fetch_media_type(url)}) %> | |||
</div> | |||
</details> | |||
<% else %> | |||
<%= render("_attachment.html", %{name: name, url: url["href"], | |||
mediaType: fetch_media_type(url)}) %> | |||
<% end %> | |||
</div> | |||
<% end %> | |||
</div> | |||
<dl class="counts pull-right"> | |||
<dt><%= Gettext.gettext("replies") %></dt><dd><%= @counts.replies %></dd> | |||
<dt><%= Gettext.gettext("announces") %></dt><dd><%= @counts.announces %></dd> | |||
<dt><%= Gettext.gettext("likes") %></dt><dd><%= @counts.likes %></dd> | |||
</dl> | |||
<p class="date pull-left"> | |||
<%= link published(@activity), to: activity_url(@author, @activity) %> | |||
</p> | |||
</div> | |||
<script> | |||
function updateHeight() { | |||
window.requestAnimationFrame(function(){ | |||
var height = document.getElementsByTagName('html')[0].scrollHeight; | |||
window.parent.postMessage({ | |||
type: 'setHeightPleromaEmbed', | |||
id: window.parentId, | |||
height: height, | |||
}, '*'); | |||
}) | |||
} | |||
window.addEventListener('message', function(e){ | |||
var data = e.data || {}; | |||
if (!window.parent || data.type !== 'setHeightPleromaEmbed') { | |||
return; | |||
} | |||
window.parentId = data.id | |||
updateHeight() | |||
}); | |||
</script> |
@@ -0,0 +1,15 @@ | |||
<!DOCTYPE html> | |||
<html> | |||
<head> | |||
<meta charset="utf-8" /> | |||
<meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui" /> | |||
<title><%= Pleroma.Config.get([:instance, :name]) %></title> | |||
<meta content='noindex' name='robots'> | |||
<%= Phoenix.HTML.raw(assigns[:meta] || "") %> | |||
<link rel="stylesheet" href="/embed.css"> | |||
<base target="_parent"> | |||
</head> | |||
<body> | |||
<%= render @view_module, @view_template, assigns %> | |||
</body> | |||
</html> |
@@ -0,0 +1,74 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.Web.EmbedView do | |||
use Pleroma.Web, :view | |||
alias Calendar.Strftime | |||
alias Pleroma.Activity | |||
alias Pleroma.Emoji.Formatter | |||
alias Pleroma.Object | |||
alias Pleroma.User | |||
alias Pleroma.Web.Gettext | |||
alias Pleroma.Web.MediaProxy | |||
alias Pleroma.Web.Metadata.Utils | |||
alias Pleroma.Web.Router.Helpers | |||
use Phoenix.HTML | |||
@media_types ["image", "audio", "video"] | |||
defp fetch_media_type(%{"mediaType" => mediaType}) do | |||
Utils.fetch_media_type(@media_types, mediaType) | |||
end | |||
defp open_content? do | |||
Pleroma.Config.get( | |||
[:frontend_configurations, :collapse_message_with_subjects], | |||
true | |||
) | |||
end | |||
defp full_nickname(user) do | |||
%{host: host} = URI.parse(user.ap_id) | |||
"@" <> user.nickname <> "@" <> host | |||
end | |||
defp status_title(%Activity{object: %Object{data: %{"name" => name}}}) when is_binary(name), | |||
do: name | |||
defp status_title(%Activity{object: %Object{data: %{"summary" => summary}}}) | |||
when is_binary(summary), | |||
do: summary | |||
defp status_title(_), do: nil | |||
defp activity_content(%Activity{object: %Object{data: %{"content" => content}}}) do | |||
content |> Pleroma.HTML.filter_tags() |> raw() | |||
end | |||
defp activity_content(_), do: nil | |||
defp activity_url(%User{local: true}, activity) do | |||
Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity) | |||
end | |||
defp activity_url(%User{local: false}, %Activity{object: %Object{data: data}}) do | |||
data["url"] || data["external_url"] || data["id"] | |||
end | |||
defp attachments(%Activity{object: %Object{data: %{"attachment" => attachments}}}) do | |||
attachments | |||
end | |||
defp sensitive?(%Activity{object: %Object{data: %{"sensitive" => sensitive}}}) do | |||
sensitive | |||
end | |||
defp published(%Activity{object: %Object{data: %{"published" => published}}}) do | |||
published | |||
|> NaiveDateTime.from_iso8601!() | |||
|> Strftime.strftime!("%B %d, %Y, %l:%M %p") | |||
end | |||
end |
@@ -0,0 +1,33 @@ | |||
defmodule Pleroma.Repo.Migrations.AddRecipientsContainBlockedDomainsFunction do | |||
use Ecto.Migration | |||
@disable_ddl_transaction true | |||
def up do | |||
statement = """ | |||
CREATE OR REPLACE FUNCTION recipients_contain_blocked_domains(recipients varchar[], blocked_domains varchar[]) RETURNS boolean AS $$ | |||
DECLARE | |||
recipient_domain varchar; | |||
recipient varchar; | |||
BEGIN | |||
FOREACH recipient IN ARRAY recipients LOOP | |||
recipient_domain = split_part(recipient, '/', 3)::varchar; | |||
IF recipient_domain = ANY(blocked_domains) THEN | |||
RETURN TRUE; | |||
END IF; | |||
END LOOP; | |||
RETURN FALSE; | |||
END; | |||
$$ LANGUAGE plpgsql; | |||
""" | |||
execute(statement) | |||
end | |||
def down do | |||
execute( | |||
"drop function if exists recipients_contain_blocked_domains(recipients varchar[], blocked_domains varchar[])" | |||
) | |||
end | |||
end |
@@ -0,0 +1,115 @@ | |||
body { | |||
background-color: #282c37; | |||
font-family: sans-serif; | |||
color: white; | |||
margin: 0; | |||
padding: 1em; | |||
padding-bottom: 0; | |||
} | |||
.avatar { | |||
cursor: pointer; | |||
} | |||
.avatar img { | |||
float: left; | |||
border-radius: 4px; | |||
margin-right: 4px; | |||
} | |||
.activity-content { | |||
padding-top: 1em; | |||
} | |||
.attachment { | |||
margin-top: 1em; | |||
} | |||
.attachment img { | |||
max-width: 100%; | |||
} | |||
.date a { | |||
text-decoration: none; | |||
} | |||
.date a:hover { | |||
text-decoration: underline; | |||
} | |||
.date a, | |||
.counts { | |||
color: #666; | |||
font-size: 0.9em; | |||
} | |||
.counts dt, | |||
.counts dd { | |||
float: left; | |||
margin-left: 1em; | |||
} | |||
a { | |||
color: white; | |||
} | |||
.h-card { | |||
min-height: 48px; | |||
margin-bottom: 8px; | |||
} | |||
.h-card a { | |||
text-decoration: none; | |||
} | |||
.h-card a:hover { | |||
text-decoration: underline; | |||
} | |||
.display-name { | |||
padding-top: 4px; | |||
display: block; | |||
text-overflow: ellipsis; | |||
overflow: hidden; | |||
color: white; | |||
} | |||
/* keep emoji from being hilariously huge */ | |||
.display-name img { | |||
max-height: 1em; | |||
} | |||
.display-name .nickname { | |||
padding-top: 4px; | |||
display: block; | |||
} | |||
.nickname:hover { | |||
text-decoration: none; | |||
} | |||
.pull-right { | |||
float: right; | |||
} | |||
.collapse { | |||
margin: 0; | |||
width: auto; | |||
} | |||
a.button { | |||
box-sizing: border-box; | |||
display: inline-block; | |||
color: white; | |||
background-color: #419bdd; | |||
border-radius: 4px; | |||
border: none; | |||
padding: 10px; | |||
font-weight: 500; | |||
font-size: 0.9em; | |||
} | |||
a.button:hover { | |||
text-decoration: none; | |||
background-color: #61a6d9; | |||
} |
@@ -0,0 +1,43 @@ | |||
(function () { | |||
'use strict' | |||
var ready = function (loaded) { | |||
if (['interactive', 'complete'].indexOf(document.readyState) !== -1) { | |||
loaded() | |||
} else { | |||
document.addEventListener('DOMContentLoaded', loaded) | |||
} | |||
} | |||
ready(function () { | |||
var iframes = [] | |||
window.addEventListener('message', function (e) { | |||
var data = e.data || {} | |||
if (data.type !== 'setHeightPleromaEmbed' || !iframes[data.id]) { | |||
return | |||
} | |||
iframes[data.id].height = data.height | |||
}); | |||
[].forEach.call(document.querySelectorAll('iframe.pleroma-embed'), function (iframe) { | |||
iframe.scrolling = 'no' | |||
iframe.style.overflow = 'hidden' | |||
iframes.push(iframe) | |||
var id = iframes.length - 1 | |||
iframe.onload = function () { | |||
iframe.contentWindow.postMessage({ | |||
type: 'setHeightPleromaEmbed', | |||
id: id | |||
}, '*') | |||
} | |||
iframe.onload() | |||
}) | |||
}) | |||
})() |
@@ -0,0 +1,281 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.Web.AdminAPI.InviteControllerTest do | |||
use Pleroma.Web.ConnCase, async: true | |||
import Pleroma.Factory | |||
alias Pleroma.Config | |||
alias Pleroma.Repo | |||
alias Pleroma.UserInviteToken | |||
setup do | |||
admin = insert(:user, is_admin: true) | |||
token = insert(:oauth_admin_token, user: admin) | |||
conn = | |||
build_conn() | |||
|> assign(:user, admin) | |||
|> assign(:token, token) | |||
{:ok, %{admin: admin, token: token, conn: conn}} | |||
end | |||
describe "POST /api/pleroma/admin/users/email_invite, with valid config" do | |||
setup do: clear_config([:instance, :registrations_open], false) | |||
setup do: clear_config([:instance, :invites_enabled], true) | |||
test "sends invitation and returns 204", %{admin: admin, conn: conn} do | |||
recipient_email = "foo@bar.com" | |||
recipient_name = "J. D." | |||
conn = | |||
conn | |||
|> put_req_header("content-type", "application/json;charset=utf-8") | |||
|> post("/api/pleroma/admin/users/email_invite", %{ | |||
email: recipient_email, | |||
name: recipient_name | |||
}) | |||
assert json_response_and_validate_schema(conn, :no_content) | |||
token_record = List.last(Repo.all(Pleroma.UserInviteToken)) | |||
assert token_record | |||
refute token_record.used | |||
notify_email = Config.get([:instance, :notify_email]) | |||
instance_name = Config.get([:instance, :name]) | |||
email = | |||
Pleroma.Emails.UserEmail.user_invitation_email( | |||
admin, | |||
token_record, | |||
recipient_email, | |||
recipient_name | |||
) | |||
Swoosh.TestAssertions.assert_email_sent( | |||
from: {instance_name, notify_email}, | |||
to: {recipient_name, recipient_email}, | |||
html_body: email.html_body | |||
) | |||
end | |||
test "it returns 403 if requested by a non-admin" do | |||
non_admin_user = insert(:user) | |||
token = insert(:oauth_token, user: non_admin_user) | |||
conn = | |||
build_conn() | |||
|> assign(:user, non_admin_user) | |||
|> assign(:token, token) | |||
|> put_req_header("content-type", "application/json;charset=utf-8") | |||
|> post("/api/pleroma/admin/users/email_invite", %{ | |||
email: "foo@bar.com", | |||
name: "JD" | |||
}) | |||
assert json_response(conn, :forbidden) | |||
end | |||
test "email with +", %{conn: conn, admin: admin} do | |||
recipient_email = "foo+bar@baz.com" | |||
conn | |||
|> put_req_header("content-type", "application/json;charset=utf-8") | |||
|> post("/api/pleroma/admin/users/email_invite", %{email: recipient_email}) | |||
|> json_response_and_validate_schema(:no_content) | |||
token_record = | |||
Pleroma.UserInviteToken | |||
|> Repo.all() | |||
|> List.last() | |||
assert token_record | |||
refute token_record.used | |||
notify_email = Config.get([:instance, :notify_email]) | |||
instance_name = Config.get([:instance, :name]) | |||
email = | |||
Pleroma.Emails.UserEmail.user_invitation_email( | |||
admin, | |||
token_record, | |||
recipient_email | |||
) | |||
Swoosh.TestAssertions.assert_email_sent( | |||
from: {instance_name, notify_email}, | |||
to: recipient_email, | |||
html_body: email.html_body | |||
) | |||
end | |||
end | |||
describe "POST /api/pleroma/admin/users/email_invite, with invalid config" do | |||
setup do: clear_config([:instance, :registrations_open]) | |||
setup do: clear_config([:instance, :invites_enabled]) | |||
test "it returns 500 if `invites_enabled` is not enabled", %{conn: conn} do | |||
Config.put([:instance, :registrations_open], false) | |||
Config.put([:instance, :invites_enabled], false) | |||
conn = | |||
conn | |||
|> put_req_header("content-type", "application/json") | |||
|> post("/api/pleroma/admin/users/email_invite", %{ | |||
email: "foo@bar.com", | |||
name: "JD" | |||
}) | |||
assert json_response_and_validate_schema(conn, :bad_request) == | |||
%{ | |||
"error" => | |||
"To send invites you need to set the `invites_enabled` option to true." | |||
} | |||
end | |||
test "it returns 500 if `registrations_open` is enabled", %{conn: conn} do | |||
Config.put([:instance, :registrations_open], true) | |||
Config.put([:instance, :invites_enabled], true) | |||
conn = | |||
conn | |||
|> put_req_header("content-type", "application/json") | |||
|> post("/api/pleroma/admin/users/email_invite", %{ | |||
email: "foo@bar.com", | |||
name: "JD" | |||
}) | |||
assert json_response_and_validate_schema(conn, :bad_request) == | |||
%{ | |||
"error" => | |||
"To send invites you need to set the `registrations_open` option to false." | |||
} | |||
end | |||
end | |||
describe "POST /api/pleroma/admin/users/invite_token" do | |||
test "without options", %{conn: conn} do | |||
conn = | |||
conn | |||
|> put_req_header("content-type", "application/json") | |||
|> post("/api/pleroma/admin/users/invite_token") | |||
invite_json = json_response_and_validate_schema(conn, 200) | |||
invite = UserInviteToken.find_by_token!(invite_json["token"]) | |||
refute invite.used | |||
refute invite.expires_at | |||
refute invite.max_use | |||
assert invite.invite_type == "one_time" | |||
end | |||
test "with expires_at", %{conn: conn} do | |||
conn = | |||
conn | |||
|> put_req_header("content-type", "application/json") | |||
|> post("/api/pleroma/admin/users/invite_token", %{ | |||
"expires_at" => Date.to_string(Date.utc_today()) | |||
}) | |||
invite_json = json_response_and_validate_schema(conn, 200) | |||
invite = UserInviteToken.find_by_token!(invite_json["token"]) | |||
refute invite.used | |||
assert invite.expires_at == Date.utc_today() | |||
refute invite.max_use | |||
assert invite.invite_type == "date_limited" | |||
end | |||
test "with max_use", %{conn: conn} do | |||
conn = | |||
conn | |||
|> put_req_header("content-type", "application/json") | |||
|> post("/api/pleroma/admin/users/invite_token", %{"max_use" => 150}) | |||
invite_json = json_response_and_validate_schema(conn, 200) | |||
invite = UserInviteToken.find_by_token!(invite_json["token"]) | |||
refute invite.used | |||
refute invite.expires_at | |||
assert invite.max_use == 150 | |||
assert invite.invite_type == "reusable" | |||
end | |||
test "with max use and expires_at", %{conn: conn} do | |||
conn = | |||
conn | |||
|> put_req_header("content-type", "application/json") | |||
|> post("/api/pleroma/admin/users/invite_token", %{ | |||
"max_use" => 150, | |||
"expires_at" => Date.to_string(Date.utc_today()) | |||
}) | |||
invite_json = json_response_and_validate_schema(conn, 200) | |||
invite = UserInviteToken.find_by_token!(invite_json["token"]) | |||
refute invite.used | |||
assert invite.expires_at == Date.utc_today() | |||
assert invite.max_use == 150 | |||
assert invite.invite_type == "reusable_date_limited" | |||
end | |||
end | |||
describe "GET /api/pleroma/admin/users/invites" do | |||
test "no invites", %{conn: conn} do | |||
conn = get(conn, "/api/pleroma/admin/users/invites") | |||
assert json_response_and_validate_schema(conn, 200) == %{"invites" => []} | |||
end | |||
test "with invite", %{conn: conn} do | |||
{:ok, invite} = UserInviteToken.create_invite() | |||
conn = get(conn, "/api/pleroma/admin/users/invites") | |||
assert json_response_and_validate_schema(conn, 200) == %{ | |||
"invites" => [ | |||
%{ | |||
"expires_at" => nil, | |||
"id" => invite.id, | |||
"invite_type" => "one_time", | |||
"max_use" => nil, | |||
"token" => invite.token, | |||
"used" => false, | |||
"uses" => 0 | |||
} | |||
] | |||
} | |||
end | |||
end | |||
describe "POST /api/pleroma/admin/users/revoke_invite" do | |||
test "with token", %{conn: conn} do | |||
{:ok, invite} = UserInviteToken.create_invite() | |||
conn = | |||
conn | |||
|> put_req_header("content-type", "application/json") | |||
|> post("/api/pleroma/admin/users/revoke_invite", %{"token" => invite.token}) | |||
assert json_response_and_validate_schema(conn, 200) == %{ | |||
"expires_at" => nil, | |||
"id" => invite.id, | |||
"invite_type" => "one_time", | |||
"max_use" => nil, | |||
"token" => invite.token, | |||
"used" => true, | |||
"uses" => 0 | |||
} | |||
end | |||
test "with invalid token", %{conn: conn} do | |||
conn = | |||
conn | |||
|> put_req_header("content-type", "application/json") | |||
|> post("/api/pleroma/admin/users/revoke_invite", %{"token" => "foo"}) | |||
assert json_response_and_validate_schema(conn, :not_found) == %{"error" => "Not found"} | |||
end | |||
end | |||
end |
@@ -0,0 +1,220 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.Web.AdminAPI.OAuthAppControllerTest do | |||
use Pleroma.Web.ConnCase, async: true | |||
use Oban.Testing, repo: Pleroma.Repo | |||
import Pleroma.Factory | |||
alias Pleroma.Config | |||
alias Pleroma.Web | |||
setup do | |||
admin = insert(:user, is_admin: true) | |||
token = insert(:oauth_admin_token, user: admin) | |||
conn = | |||
build_conn() | |||
|> assign(:user, admin) | |||
|> assign(:token, token) | |||
{:ok, %{admin: admin, token: token, conn: conn}} | |||
end | |||
describe "POST /api/pleroma/admin/oauth_app" do | |||
test "errors", %{conn: conn} do | |||
response = | |||
conn | |||
|> put_req_header("content-type", "application/json") | |||
|> post("/api/pleroma/admin/oauth_app", %{}) | |||
|> json_response_and_validate_schema(400) | |||
assert %{ | |||
"error" => "Missing field: name. Missing field: redirect_uris." | |||
} = response | |||
end | |||
test "success", %{conn: conn} do | |||
base_url = Web.base_url() | |||
app_name = "Trusted app" | |||
response = | |||
conn | |||
|> put_req_header("content-type", "application/json") | |||
|> post("/api/pleroma/admin/oauth_app", %{ | |||
name: app_name, | |||
redirect_uris: base_url | |||
}) | |||
|> json_response_and_validate_schema(200) | |||
assert %{ | |||
"client_id" => _, | |||
"client_secret" => _, | |||
"name" => ^app_name, | |||
"redirect_uri" => ^base_url, | |||
"trusted" => false | |||
} = response | |||
end | |||
test "with trusted", %{conn: conn} do | |||
base_url = Web.base_url() | |||
app_name = "Trusted app" | |||
response = | |||
conn | |||
|> put_req_header("content-type", "application/json") | |||
|> post("/api/pleroma/admin/oauth_app", %{ | |||
name: app_name, | |||
redirect_uris: base_url, | |||
trusted: true | |||
}) | |||
|> json_response_and_validate_schema(200) | |||
assert %{ | |||
"client_id" => _, | |||
"client_secret" => _, | |||
"name" => ^app_name, | |||
"redirect_uri" => ^base_url, | |||
"trusted" => true | |||
} = response | |||
end | |||
end | |||
describe "GET /api/pleroma/admin/oauth_app" do | |||
setup do | |||
app = insert(:oauth_app) | |||
{:ok, app: app} | |||
end | |||
test "list", %{conn: conn} do | |||
response = | |||
conn | |||
|> get("/api/pleroma/admin/oauth_app") | |||
|> json_response_and_validate_schema(200) | |||
assert %{"apps" => apps, "count" => count, "page_size" => _} = response | |||
assert length(apps) == count | |||
end | |||
test "with page size", %{conn: conn} do | |||
insert(:oauth_app) | |||
page_size = 1 | |||
response = | |||
conn | |||
|> get("/api/pleroma/admin/oauth_app?page_size=#{page_size}") | |||
|> json_response_and_validate_schema(200) | |||
assert %{"apps" => apps, "count" => _, "page_size" => ^page_size} = response | |||
assert length(apps) == page_size | |||
end | |||
test "search by client name", %{conn: conn, app: app} do | |||
response = | |||
conn | |||
|> get("/api/pleroma/admin/oauth_app?name=#{app.client_name}") | |||
|> json_response_and_validate_schema(200) | |||
assert %{"apps" => [returned], "count" => _, "page_size" => _} = response | |||
assert returned["client_id"] == app.client_id | |||
assert returned["name"] == app.client_name | |||
end | |||
test "search by client id", %{conn: conn, app: app} do | |||
response = | |||
conn | |||
|> get("/api/pleroma/admin/oauth_app?client_id=#{app.client_id}") | |||
|> json_response_and_validate_schema(200) | |||
assert %{"apps" => [returned], "count" => _, "page_size" => _} = response | |||
assert returned["client_id"] == app.client_id | |||
assert returned["name"] == app.client_name | |||
end | |||
test "only trusted", %{conn: conn} do | |||
app = insert(:oauth_app, trusted: true) | |||
response = | |||
conn | |||
|> get("/api/pleroma/admin/oauth_app?trusted=true") | |||
|> json_response_and_validate_schema(200) | |||
assert %{"apps" => [returned], "count" => _, "page_size" => _} = response | |||
assert returned["client_id"] == app.client_id | |||
assert returned["name"] == app.client_name | |||
end | |||
end | |||
describe "DELETE /api/pleroma/admin/oauth_app/:id" do | |||
test "with id", %{conn: conn} do | |||
app = insert(:oauth_app) | |||
response = | |||
conn | |||
|> delete("/api/pleroma/admin/oauth_app/" <> to_string(app.id)) | |||
|> json_response_and_validate_schema(:no_content) | |||
assert response == "" | |||
end | |||
test "with non existance id", %{conn: conn} do | |||
response = | |||
conn | |||
|> delete("/api/pleroma/admin/oauth_app/0") | |||
|> json_response_and_validate_schema(:bad_request) | |||
assert response == "" | |||
end | |||
end | |||
describe "PATCH /api/pleroma/admin/oauth_app/:id" do | |||
test "with id", %{conn: conn} do | |||
app = insert(:oauth_app) | |||
name = "another name" | |||
url = "https://example.com" | |||
scopes = ["admin"] | |||
id = app.id | |||
website = "http://website.com" | |||
response = | |||
conn | |||
|> put_req_header("content-type", "application/json") | |||
|> patch("/api/pleroma/admin/oauth_app/#{id}", %{ | |||
name: name, | |||
trusted: true, | |||
redirect_uris: url, | |||
scopes: scopes, | |||
website: website | |||
}) | |||
|> json_response_and_validate_schema(200) | |||
assert %{ | |||
"client_id" => _, | |||
"client_secret" => _, | |||
"id" => ^id, | |||
"name" => ^name, | |||
"redirect_uri" => ^url, | |||
"trusted" => true, | |||
"website" => ^website | |||
} = response | |||
end | |||
test "without id", %{conn: conn} do | |||
response = | |||
conn | |||
|> put_req_header("content-type", "application/json") | |||
|> patch("/api/pleroma/admin/oauth_app/0") | |||
|> json_response_and_validate_schema(:bad_request) | |||
assert response == "" | |||
end | |||
end | |||
end |
@@ -0,0 +1,374 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.Web.AdminAPI.ReportControllerTest do | |||
use Pleroma.Web.ConnCase | |||
import Pleroma.Factory | |||
alias Pleroma.Activity | |||
alias Pleroma.Config | |||
alias Pleroma.ModerationLog | |||
alias Pleroma.Repo | |||
alias Pleroma.ReportNote | |||
alias Pleroma.Web.CommonAPI | |||
setup do | |||
admin = insert(:user, is_admin: true) | |||
token = insert(:oauth_admin_token, user: admin) | |||
conn = | |||
build_conn() | |||
|> assign(:user, admin) | |||
|> assign(:token, token) | |||
{:ok, %{admin: admin, token: token, conn: conn}} | |||
end | |||
describe "GET /api/pleroma/admin/reports/:id" do | |||
test "returns report by its id", %{conn: conn} do | |||
[reporter, target_user] = insert_pair(:user) | |||
activity = insert(:note_activity, user: target_user) | |||
{:ok, %{id: report_id}} = | |||
CommonAPI.report(reporter, %{ | |||
account_id: target_user.id, | |||
comment: "I feel offended", | |||
status_ids: [activity.id] | |||
}) | |||
response = | |||
conn | |||
|> get("/api/pleroma/admin/reports/#{report_id}") | |||
|> json_response_and_validate_schema(:ok) | |||
assert response["id"] == report_id | |||
end | |||
test "returns 404 when report id is invalid", %{conn: conn} do | |||
conn = get(conn, "/api/pleroma/admin/reports/test") | |||
assert json_response_and_validate_schema(conn, :not_found) == %{"error" => "Not found"} | |||
end | |||
end | |||
describe "PATCH /api/pleroma/admin/reports" do | |||
setup do | |||
[reporter, target_user] = insert_pair(:user) | |||
activity = insert(:note_activity, user: target_user) | |||
{:ok, %{id: report_id}} = | |||
CommonAPI.report(reporter, %{ | |||
account_id: target_user.id, | |||
comment: "I feel offended", | |||
status_ids: [activity.id] | |||
}) | |||
{:ok, %{id: second_report_id}} = | |||
CommonAPI.report(reporter, %{ | |||
account_id: target_user.id, | |||
comment: "I feel very offended", | |||
status_ids: [activity.id] | |||
}) | |||
%{ | |||
id: report_id, | |||
second_report_id: second_report_id | |||
} | |||
end | |||
test "requires admin:write:reports scope", %{conn: conn, id: id, admin: admin} do | |||
read_token = insert(:oauth_token, user: admin, scopes: ["admin:read"]) | |||
write_token = insert(:oauth_token, user: admin, scopes: ["admin:write:reports"]) | |||
response = | |||
conn | |||
|> assign(:token, read_token) | |||
|> put_req_header("content-type", "application/json") | |||
|> patch("/api/pleroma/admin/reports", %{ | |||
"reports" => [%{"state" => "resolved", "id" => id}] | |||
}) | |||
|> json_response_and_validate_schema(403) | |||
assert response == %{ | |||
"error" => "Insufficient permissions: admin:write:reports." | |||
} | |||
conn | |||
|> assign(:token, write_token) | |||
|> put_req_header("content-type", "application/json") | |||
|> patch("/api/pleroma/admin/reports", %{ | |||
"reports" => [%{"state" => "resolved", "id" => id}] | |||
}) | |||
|> json_response_and_validate_schema(:no_content) | |||
end | |||
test "mark report as resolved", %{conn: conn, id: id, admin: admin} do | |||
conn | |||
|> put_req_header("content-type", "application/json") | |||
|> patch("/api/pleroma/admin/reports", %{ | |||
"reports" => [ | |||
%{"state" => "resolved", "id" => id} | |||
] | |||
}) | |||
|> json_response_and_validate_schema(:no_content) | |||
activity = Activity.get_by_id(id) | |||
assert activity.data["state"] == "resolved" | |||
log_entry = Repo.one(ModerationLog) | |||
assert ModerationLog.get_log_entry_message(log_entry) == | |||
"@#{admin.nickname} updated report ##{id} with 'resolved' state" | |||
end | |||
test "closes report", %{conn: conn, id: id, admin: admin} do | |||
conn | |||
|> put_req_header("content-type", "application/json") | |||
|> patch("/api/pleroma/admin/reports", %{ | |||
"reports" => [ | |||
%{"state" => "closed", "id" => id} | |||
] | |||
}) | |||
|> json_response_and_validate_schema(:no_content) | |||
activity = Activity.get_by_id(id) | |||
assert activity.data["state"] == "closed" | |||
log_entry = Repo.one(ModerationLog) | |||
assert ModerationLog.get_log_entry_message(log_entry) == | |||
"@#{admin.nickname} updated report ##{id} with 'closed' state" | |||
end | |||
test "returns 400 when state is unknown", %{conn: conn, id: id} do | |||
conn = | |||
conn | |||
|> put_req_header("content-type", "application/json") | |||
|> patch("/api/pleroma/admin/reports", %{ | |||
"reports" => [ | |||
%{"state" => "test", "id" => id} | |||
] | |||
}) | |||
assert "Unsupported state" = | |||
hd(json_response_and_validate_schema(conn, :bad_request))["error"] | |||
end | |||
test "returns 404 when report is not exist", %{conn: conn} do | |||
conn = | |||
conn | |||
|> put_req_header("content-type", "application/json") | |||
|> patch("/api/pleroma/admin/reports", %{ | |||
"reports" => [ | |||
%{"state" => "closed", "id" => "test"} | |||
] | |||
}) | |||
assert hd(json_response_and_validate_schema(conn, :bad_request))["error"] == "not_found" | |||
end | |||
test "updates state of multiple reports", %{ | |||
conn: conn, | |||
id: id, | |||
admin: admin, | |||
second_report_id: second_report_id | |||
} do | |||
conn | |||
|> put_req_header("content-type", "application/json") | |||
|> patch("/api/pleroma/admin/reports", %{ | |||
"reports" => [ | |||
%{"state" => "resolved", "id" => id}, | |||
%{"state" => "closed", "id" => second_report_id} | |||
] | |||
}) | |||
|> json_response_and_validate_schema(:no_content) | |||
activity = Activity.get_by_id(id) | |||
second_activity = Activity.get_by_id(second_report_id) | |||
assert activity.data["state"] == "resolved" | |||
assert second_activity.data["state"] == "closed" | |||
[first_log_entry, second_log_entry] = Repo.all(ModerationLog) | |||
assert ModerationLog.get_log_entry_message(first_log_entry) == | |||
"@#{admin.nickname} updated report ##{id} with 'resolved' state" | |||
assert ModerationLog.get_log_entry_message(second_log_entry) == | |||
"@#{admin.nickname} updated report ##{second_report_id} with 'closed' state" | |||
end | |||
end | |||
describe "GET /api/pleroma/admin/reports" do | |||
test "returns empty response when no reports created", %{conn: conn} do | |||
response = | |||
conn | |||
|> get("/api/pleroma/admin/reports") | |||
|> json_response_and_validate_schema(:ok) | |||
assert Enum.empty?(response["reports"]) | |||
assert response["total"] == 0 | |||
end | |||
test "returns reports", %{conn: conn} do | |||
[reporter, target_user] = insert_pair(:user) | |||
activity = insert(:note_activity, user: target_user) | |||
{:ok, %{id: report_id}} = | |||
CommonAPI.report(reporter, %{ | |||
account_id: target_user.id, | |||
comment: "I feel offended", | |||
status_ids: [activity.id] | |||
}) | |||
response = | |||
conn | |||
|> get("/api/pleroma/admin/reports") | |||
|> json_response_and_validate_schema(:ok) | |||
[report] = response["reports"] | |||
assert length(response["reports"]) == 1 | |||
assert report["id"] == report_id | |||
assert response["total"] == 1 | |||
end | |||
test "returns reports with specified state", %{conn: conn} do | |||
[reporter, target_user] = insert_pair(:user) | |||
activity = insert(:note_activity, user: target_user) | |||
{:ok, %{id: first_report_id}} = | |||
CommonAPI.report(reporter, %{ | |||
account_id: target_user.id, | |||
comment: "I feel offended", | |||
status_ids: [activity.id] | |||
}) | |||
{:ok, %{id: second_report_id}} = | |||
CommonAPI.report(reporter, %{ | |||
account_id: target_user.id, | |||
comment: "I don't like this user" | |||
}) | |||
CommonAPI.update_report_state(second_report_id, "closed") | |||
response = | |||
conn | |||
|> get("/api/pleroma/admin/reports?state=open") | |||
|> json_response_and_validate_schema(:ok) | |||
assert [open_report] = response["reports"] | |||
assert length(response["reports"]) == 1 | |||
assert open_report["id"] == first_report_id | |||
assert response["total"] == 1 | |||
response = | |||
conn | |||
|> get("/api/pleroma/admin/reports?state=closed") | |||
|> json_response_and_validate_schema(:ok) | |||
assert [closed_report] = response["reports"] | |||
assert length(response["reports"]) == 1 | |||
assert closed_report["id"] == second_report_id | |||
assert response["total"] == 1 | |||
assert %{"total" => 0, "reports" => []} == | |||
conn | |||
|> get("/api/pleroma/admin/reports?state=resolved", %{ | |||
"" => "" | |||
}) | |||
|> json_response_and_validate_schema(:ok) | |||
end | |||
test "returns 403 when requested by a non-admin" do | |||
user = insert(:user) | |||
token = insert(:oauth_token, user: user) | |||
conn = | |||
build_conn() | |||
|> assign(:user, user) | |||
|> assign(:token, token) | |||
|> get("/api/pleroma/admin/reports") | |||
assert json_response(conn, :forbidden) == | |||
%{"error" => "User is not an admin or OAuth admin scope is not granted."} | |||
end | |||
test "returns 403 when requested by anonymous" do | |||
conn = get(build_conn(), "/api/pleroma/admin/reports") | |||
assert json_response(conn, :forbidden) == %{ | |||
"error" => "Invalid credentials." | |||
} | |||
end | |||
end | |||
describe "POST /api/pleroma/admin/reports/:id/notes" do | |||
setup %{conn: conn, admin: admin} do | |||
[reporter, target_user] = insert_pair(:user) | |||
activity = insert(:note_activity, user: target_user) | |||
{:ok, %{id: report_id}} = | |||
CommonAPI.report(reporter, %{ | |||
account_id: target_user.id, | |||
comment: "I feel offended", | |||
status_ids: [activity.id] | |||
}) | |||
conn | |||
|> put_req_header("content-type", "application/json") | |||
|> post("/api/pleroma/admin/reports/#{report_id}/notes", %{ | |||
content: "this is disgusting!" | |||
}) | |||
conn | |||
|> put_req_header("content-type", "application/json") | |||
|> post("/api/pleroma/admin/reports/#{report_id}/notes", %{ | |||
content: "this is disgusting2!" | |||
}) | |||
%{ | |||
admin_id: admin.id, | |||
report_id: report_id | |||
} | |||
end | |||
test "it creates report note", %{admin_id: admin_id, report_id: report_id} do | |||
assert [note, _] = Repo.all(ReportNote) | |||
assert %{ | |||
activity_id: ^report_id, | |||
content: "this is disgusting!", | |||
user_id: ^admin_id | |||
} = note | |||
end | |||
test "it returns reports with notes", %{conn: conn, admin: admin} do | |||
conn = get(conn, "/api/pleroma/admin/reports") | |||
response = json_response_and_validate_schema(conn, 200) | |||
notes = hd(response["reports"])["notes"] | |||
[note, _] = notes | |||
assert note["user"]["nickname"] == admin.nickname | |||
assert note["content"] == "this is disgusting!" | |||
assert note["created_at"] | |||
assert response["total"] == 1 | |||
end | |||
test "it deletes the note", %{conn: conn, report_id: report_id} do | |||
assert ReportNote |> Repo.all() |> length() == 2 | |||
assert [note, _] = Repo.all(ReportNote) | |||
delete(conn, "/api/pleroma/admin/reports/#{report_id}/notes/#{note.id}") | |||
assert ReportNote |> Repo.all() |> length() == 1 | |||
end | |||
end | |||
end |
@@ -71,10 +71,48 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do | |||
get(conn, "/api/v2/search?q=天子") | |||
|> json_response_and_validate_schema(200) | |||
assert results["hashtags"] == [ | |||
%{"name" => "天子", "url" => "#{Web.base_url()}/tag/天子"} | |||
] | |||
[status] = results["statuses"] | |||
assert status["id"] == to_string(activity.id) | |||
end | |||
test "constructs hashtags from search query", %{conn: conn} do | |||
results = | |||
conn | |||
|> get("/api/v2/search?#{URI.encode_query(%{q: "some text with #explicit #hashtags"})}") | |||
|> json_response_and_validate_schema(200) | |||
assert results["hashtags"] == [ | |||
%{"name" => "explicit", "url" => "#{Web.base_url()}/tag/explicit"}, | |||
%{"name" => "hashtags", "url" => "#{Web.base_url()}/tag/hashtags"} | |||
] | |||
results = | |||
conn | |||
|> get("/api/v2/search?#{URI.encode_query(%{q: "john doe JOHN DOE"})}") | |||
|> json_response_and_validate_schema(200) | |||
assert results["hashtags"] == [ | |||
%{"name" => "john", "url" => "#{Web.base_url()}/tag/john"}, | |||
%{"name" => "doe", "url" => "#{Web.base_url()}/tag/doe"}, | |||
%{"name" => "JohnDoe", "url" => "#{Web.base_url()}/tag/JohnDoe"} | |||
] | |||
results = | |||
conn | |||
|> get("/api/v2/search?#{URI.encode_query(%{q: "accident-prone"})}") | |||
|> json_response_and_validate_schema(200) | |||
assert results["hashtags"] == [ | |||
%{"name" => "accident", "url" => "#{Web.base_url()}/tag/accident"}, | |||
%{"name" => "prone", "url" => "#{Web.base_url()}/tag/prone"}, | |||
%{"name" => "AccidentProne", "url" => "#{Web.base_url()}/tag/AccidentProne"} | |||
] | |||
end | |||
test "excludes a blocked users from search results", %{conn: conn} do | |||
user = insert(:user) | |||
user_smith = insert(:user, %{nickname: "Agent", name: "I love 2hu"}) | |||
@@ -179,7 +217,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do | |||
[account | _] = results["accounts"] | |||
assert account["id"] == to_string(user_three.id) | |||
assert results["hashtags"] == [] | |||
assert results["hashtags"] == ["2hu"] | |||
[status] = results["statuses"] | |||
assert status["id"] == to_string(activity.id) | |||
@@ -97,6 +97,49 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do | |||
res_conn = get(conn, "/api/v1/timelines/public") | |||
assert length(json_response_and_validate_schema(res_conn, 200)) == 1 | |||
end | |||
test "doesn't return replies if follower is posting with blocked user" do | |||
%{conn: conn, user: blocker} = oauth_access(["read:statuses"]) | |||
[blockee, friend] = insert_list(2, :user) | |||
{:ok, blocker} = User.follow(blocker, friend) | |||
{:ok, _} = User.block(blocker, blockee) | |||
conn = assign(conn, :user, blocker) | |||
{:ok, %{id: activity_id} = activity} = CommonAPI.post(friend, %{status: "hey!"}) | |||
{:ok, reply_from_blockee} = | |||
CommonAPI.post(blockee, %{status: "heya", in_reply_to_status_id: activity}) | |||
{:ok, _reply_from_friend} = | |||
CommonAPI.post(friend, %{status: "status", in_reply_to_status_id: reply_from_blockee}) | |||
res_conn = get(conn, "/api/v1/timelines/public") | |||
[%{"id" => ^activity_id}] = json_response_and_validate_schema(res_conn, 200) | |||
end | |||
test "doesn't return replies if follow is posting with users from blocked domain" do | |||
%{conn: conn, user: blocker} = oauth_access(["read:statuses"]) | |||
friend = insert(:user) | |||
blockee = insert(:user, ap_id: "https://example.com/users/blocked") | |||
{:ok, blocker} = User.follow(blocker, friend) | |||
{:ok, blocker} = User.block_domain(blocker, "example.com") | |||
conn = assign(conn, :user, blocker) | |||
{:ok, %{id: activity_id} = activity} = CommonAPI.post(friend, %{status: "hey!"}) | |||
{:ok, reply_from_blockee} = | |||
CommonAPI.post(blockee, %{status: "heya", in_reply_to_status_id: activity}) | |||
{:ok, _reply_from_friend} = | |||
CommonAPI.post(friend, %{status: "status", in_reply_to_status_id: reply_from_blockee}) | |||
res_conn = get(conn, "/api/v1/timelines/public") | |||
activities = json_response_and_validate_schema(res_conn, 200) | |||
[%{"id" => ^activity_id}] = activities | |||
end | |||
end | |||
defp local_and_remote_activities do | |||