@@ -22,8 +22,21 @@ defmodule Pleroma.LoadTesting.Activities do | |||||
@max_concurrency 10 | @max_concurrency 10 | ||||
@visibility ~w(public private direct unlisted) | @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 | @spec generate(User.t(), keyword()) :: :ok | ||||
def generate(user, opts \\ []) do | def generate(user, opts \\ []) do | ||||
@@ -34,33 +47,24 @@ defmodule Pleroma.LoadTesting.Activities do | |||||
opts = Keyword.merge(@defaults, opts) | 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 = | task_data = | ||||
for visibility <- @visibility, | for visibility <- @visibility, | ||||
type <- @types, | type <- @types, | ||||
group <- @groups, | |||||
group <- [:user | @groups], | |||||
do: {visibility, type, group} | do: {visibility, type, group} | ||||
IO.puts("Starting generating #{opts[:iterations]} iterations of activities...") | 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 -> | public_long_thread = fn -> | ||||
generate_long_thread("public", user, friends_thread, non_friends_thread, opts) | |||||
generate_long_thread("public", users, opts) | |||||
end | end | ||||
private_long_thread = fn -> | private_long_thread = fn -> | ||||
generate_long_thread("private", user, friends_thread, non_friends_thread, opts) | |||||
generate_long_thread("private", users, opts) | |||||
end | end | ||||
iterations = opts[:iterations] | iterations = opts[:iterations] | ||||
@@ -73,10 +77,10 @@ defmodule Pleroma.LoadTesting.Activities do | |||||
i when i == iterations - 2 -> | i when i == iterations - 2 -> | ||||
spawn(public_long_thread) | spawn(public_long_thread) | ||||
spawn(private_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 | ||||
) | ) | ||||
end) | end) | ||||
@@ -127,16 +131,16 @@ defmodule Pleroma.LoadTesting.Activities do | |||||
end) | end) | ||||
end | end | ||||
defp generate_long_thread(visibility, user, friends, non_friends, _opts) do | |||||
defp generate_long_thread(visibility, users, _opts) do | |||||
group = | group = | ||||
if visibility == "public", | if visibility == "public", | ||||
do: "friends", | |||||
else: "user" | |||||
do: :friends_local, | |||||
else: :user | |||||
tasks = get_reply_tasks(visibility, group) |> Stream.cycle() |> Enum.take(50) | tasks = get_reply_tasks(visibility, group) |> Stream.cycle() |> Enum.take(50) | ||||
{:ok, activity} = | {:ok, activity} = | ||||
CommonAPI.post(user, %{ | |||||
CommonAPI.post(users[:user], %{ | |||||
status: "Start of #{visibility} long thread", | status: "Start of #{visibility} long thread", | ||||
visibility: visibility | visibility: visibility | ||||
}) | }) | ||||
@@ -150,31 +154,28 @@ defmodule Pleroma.LoadTesting.Activities do | |||||
Map.put(state, key, activity) | Map.put(state, key, activity) | ||||
end) | 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") | IO.puts("Generating #{visibility} long thread ended\n") | ||||
end | 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 | 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) | 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) | ||||
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.async_stream( | ||||
task_data, | task_data, | ||||
fn {visibility, type, group} -> | fn {visibility, type, group} -> | ||||
insert_activity(type, visibility, group, user, friends, non_friends, opts) | |||||
insert_activity(type, visibility, group, users, opts) | |||||
end, | end, | ||||
max_concurrency: @max_concurrency, | max_concurrency: @max_concurrency, | ||||
timeout: 30_000 | timeout: 30_000 | ||||
@@ -182,67 +183,104 @@ defmodule Pleroma.LoadTesting.Activities do | |||||
|> Stream.run() | |> Stream.run() | ||||
end | 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 | group | ||||
|> get_actor(user, friends, non_friends) | |||||
|> CommonAPI.post(%{status: "Simple status", visibility: visibility}) | |||||
|> get_actor(users) | |||||
|> CommonAPI.post(%{status: status, visibility: visibility}) | |||||
end | 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 | end | ||||
defp insert_activity("mentions", visibility, group, user, friends, non_friends, _opts) do | |||||
defp user_mentions(users) do | |||||
user_mentions = | 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 | 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 | 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 = %{ | obj_data = %{ | ||||
"actor" => actor.ap_id, | "actor" => actor.ap_id, | ||||
@@ -268,67 +306,54 @@ defmodule Pleroma.LoadTesting.Activities do | |||||
}) | }) | ||||
end | 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 | 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(), | with activity_id when not is_nil(activity_id) <- get_random_create_activity_id(), | ||||
{:ok, _activity} <- CommonAPI.favorite(actor, activity_id) do | {:ok, _activity} <- CommonAPI.favorite(actor, activity_id) do | ||||
:ok | :ok | ||||
else | else | ||||
{:error, _} -> | {:error, _} -> | ||||
insert_activity("like", visibility, group, user, friends, non_friends, opts) | |||||
insert_activity(:like, visibility, group, users, opts) | |||||
nil -> | nil -> | ||||
Process.sleep(15) | Process.sleep(15) | ||||
insert_activity("like", visibility, group, user, friends, non_friends, opts) | |||||
insert_activity(:like, visibility, group, users, opts) | |||||
end | end | ||||
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(), | 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 | :ok | ||||
else | else | ||||
{:error, _} -> | {:error, _} -> | ||||
insert_activity("reblog", visibility, group, user, friends, non_friends, opts) | |||||
insert_activity(:reblog, visibility, group, users, opts) | |||||
nil -> | nil -> | ||||
Process.sleep(15) | Process.sleep(15) | ||||
insert_activity("reblog", visibility, group, user, friends, non_friends, opts) | |||||
insert_activity(:reblog, visibility, group, users, opts) | |||||
end | end | ||||
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) | tasks = get_reply_tasks("direct", group) | ||||
list = | list = | ||||
case group do | 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 | end | ||||
data = Enum.map(list, &("@" <> &1.nickname)) | data = Enum.map(list, &("@" <> &1.nickname)) | ||||
@@ -339,40 +364,30 @@ defmodule Pleroma.LoadTesting.Activities do | |||||
visibility: "direct" | 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 | 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 | 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) | %{host: host} = URI.parse(actor.ap_id) | ||||
datetime = DateTime.utc_now() | 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 = %{ | activity_data = %{ | ||||
"actor" => actor.ap_id, | "actor" => actor.ap_id, | ||||
@@ -389,7 +404,7 @@ defmodule Pleroma.LoadTesting.Activities do | |||||
"attributedTo" => actor.ap_id, | "attributedTo" => actor.ap_id, | ||||
"bcc" => [], | "bcc" => [], | ||||
"bto" => [], | "bto" => [], | ||||
"content" => "Remote post", | |||||
"content" => content, | |||||
"context" => context_id, | "context" => context_id, | ||||
"conversation" => context_id, | "conversation" => context_id, | ||||
"emoji" => %{}, | "emoji" => %{}, | ||||
@@ -475,51 +490,65 @@ defmodule Pleroma.LoadTesting.Activities do | |||||
{act_data, obj_data} | {act_data, obj_data} | ||||
end | 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) | ||||
end | end | ||||
defp insert_direct_replies(tasks, user, list, acc) do | defp insert_direct_replies(tasks, user, list, acc) do | ||||
Enum.reduce(tasks, acc, fn | 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) | actor = Enum.random(list) | ||||
{reply_id, _} = | {reply_id, _} = | ||||
insert_reply(actor, List.delete(data, "@" <> actor.nickname), id, "direct") | insert_reply(actor, List.delete(data, "@" <> actor.nickname), id, "direct") | ||||
{reply_id, data} | {reply_id, data} | ||||
"user", {id, data} -> | |||||
{reply_id, _} = insert_reply(user, List.delete(data, "@" <> user.nickname), id, "direct") | |||||
{reply_id, data} | |||||
end) | end) | ||||
end | end | ||||
@@ -36,6 +36,7 @@ defmodule Pleroma.LoadTesting.Fetcher do | |||||
fetch_home_timeline(user) | fetch_home_timeline(user) | ||||
fetch_direct_timeline(user) | fetch_direct_timeline(user) | ||||
fetch_public_timeline(user) | fetch_public_timeline(user) | ||||
fetch_public_timeline(user, :with_blocks) | |||||
fetch_public_timeline(user, :local) | fetch_public_timeline(user, :local) | ||||
fetch_public_timeline(user, :tag) | fetch_public_timeline(user, :tag) | ||||
fetch_notifications(user) | fetch_notifications(user) | ||||
@@ -227,6 +228,58 @@ defmodule Pleroma.LoadTesting.Fetcher do | |||||
fetch_public_timeline(opts, "public timeline only media") | fetch_public_timeline(opts, "public timeline only media") | ||||
end | 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 | defp fetch_public_timeline(opts, title) when is_binary(title) do | ||||
first_page_last = ActivityPub.fetch_public_activities(opts) |> List.last() | 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]) | make_friends(main_user, opts[:friends]) | ||||
Repo.get(User, main_user.id) | |||||
User.get_by_id(main_user.id) | |||||
end | end | ||||
def generate_users(max) do | def generate_users(max) do | ||||
@@ -166,4 +166,24 @@ defmodule Pleroma.LoadTesting.Users do | |||||
) | ) | ||||
|> Stream.run() | |> Stream.run() | ||||
end | 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 | end |
@@ -5,7 +5,6 @@ defmodule Mix.Tasks.Pleroma.Benchmarks.Tags do | |||||
import Ecto.Query | import Ecto.Query | ||||
alias Pleroma.Repo | alias Pleroma.Repo | ||||
alias Pleroma.Web.MastodonAPI.TimelineController | |||||
def run(_args) do | def run(_args) do | ||||
Mix.Pleroma.start_pleroma() | Mix.Pleroma.start_pleroma() | ||||
@@ -37,7 +36,7 @@ defmodule Mix.Tasks.Pleroma.Benchmarks.Tags do | |||||
Benchee.run( | Benchee.run( | ||||
%{ | %{ | ||||
"Hashtag fetching, any" => fn tags -> | "Hashtag fetching, any" => fn tags -> | ||||
TimelineController.hashtag_fetching( | |||||
hashtag_fetching( | |||||
%{ | %{ | ||||
"any" => tags | "any" => tags | ||||
}, | }, | ||||
@@ -47,7 +46,7 @@ defmodule Mix.Tasks.Pleroma.Benchmarks.Tags do | |||||
end, | end, | ||||
# Will always return zero results because no overlapping hashtags are generated. | # Will always return zero results because no overlapping hashtags are generated. | ||||
"Hashtag fetching, all" => fn tags -> | "Hashtag fetching, all" => fn tags -> | ||||
TimelineController.hashtag_fetching( | |||||
hashtag_fetching( | |||||
%{ | %{ | ||||
"all" => tags | "all" => tags | ||||
}, | }, | ||||
@@ -67,7 +66,7 @@ defmodule Mix.Tasks.Pleroma.Benchmarks.Tags do | |||||
Benchee.run( | Benchee.run( | ||||
%{ | %{ | ||||
"Hashtag fetching" => fn tag -> | "Hashtag fetching" => fn tag -> | ||||
TimelineController.hashtag_fetching( | |||||
hashtag_fetching( | |||||
%{ | %{ | ||||
"tag" => tag | "tag" => tag | ||||
}, | }, | ||||
@@ -80,4 +79,35 @@ defmodule Mix.Tasks.Pleroma.Benchmarks.Tags do | |||||
time: 5 | time: 5 | ||||
) | ) | ||||
end | 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 | end |
@@ -184,7 +184,7 @@ config :pleroma, :instance, | |||||
name: "Pleroma", | name: "Pleroma", | ||||
email: "example@example.com", | email: "example@example.com", | ||||
notify_email: "noreply@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", | background_image: "/images/city.jpg", | ||||
limit: 5_000, | limit: 5_000, | ||||
chat_limit: 5_000, | chat_limit: 5_000, | ||||
@@ -547,7 +547,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret | |||||
```json | ```json | ||||
{ | { | ||||
"totalReports" : 1, | |||||
"total" : 1, | |||||
"reports": [ | "reports": [ | ||||
{ | { | ||||
"account": { | "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 | - 400 Bad Request `"Invalid parameters"` when `status` is missing | ||||
- On success: `204`, empty response | - 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 | ### Delete report note | ||||
@@ -42,6 +42,12 @@ Feel free to contact us to be added to this list! | |||||
- Platforms: SailfishOS | - Platforms: SailfishOS | ||||
- Features: No Streaming | - 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 | ### 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/) | - 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> | - Source: <https://gogs.gdgd.jp.net/lin/nekonium> | ||||
@@ -24,6 +24,6 @@ defmodule Pleroma.Constants do | |||||
const(static_only_files, | const(static_only_files, | ||||
do: | 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 | end |
@@ -17,14 +17,6 @@ defmodule Pleroma.Helpers.UriHelper do | |||||
|> URI.to_string() | |> URI.to_string() | ||||
end | 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: Path.join([base, uri]) | ||||
def maybe_add_base(uri, _base), do: uri | def maybe_add_base(uri, _base), do: uri | ||||
end | 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.Constants | ||||
alias Pleroma.Conversation | alias Pleroma.Conversation | ||||
alias Pleroma.Conversation.Participation | alias Pleroma.Conversation.Participation | ||||
alias Pleroma.Maps | |||||
alias Pleroma.Notification | alias Pleroma.Notification | ||||
alias Pleroma.Object | alias Pleroma.Object | ||||
alias Pleroma.Object.Containment | alias Pleroma.Object.Containment | ||||
@@ -19,7 +20,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do | |||||
alias Pleroma.User | alias Pleroma.User | ||||
alias Pleroma.Web.ActivityPub.MRF | alias Pleroma.Web.ActivityPub.MRF | ||||
alias Pleroma.Web.ActivityPub.Transmogrifier | alias Pleroma.Web.ActivityPub.Transmogrifier | ||||
alias Pleroma.Web.ActivityPub.Utils | |||||
alias Pleroma.Web.Streamer | alias Pleroma.Web.Streamer | ||||
alias Pleroma.Web.WebFinger | alias Pleroma.Web.WebFinger | ||||
alias Pleroma.Workers.BackgroundWorker | alias Pleroma.Workers.BackgroundWorker | ||||
@@ -168,12 +168,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do | |||||
}) | }) | ||||
# Splice in the child object if we have one. | # 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}) | BackgroundWorker.enqueue("fetch_data_for_activity", %{"activity_id" => activity.id}) | ||||
@@ -335,7 +330,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do | |||||
with data <- | with data <- | ||||
%{"to" => to, "type" => type, "actor" => actor.ap_id, "object" => object} | %{"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), | {:ok, activity} <- insert(data, local), | ||||
_ <- notify_and_stream(activity), | _ <- notify_and_stream(activity), | ||||
:ok <- maybe_federate(activity) do | :ok <- maybe_federate(activity) do | ||||
@@ -355,7 +350,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do | |||||
"actor" => actor, | "actor" => actor, | ||||
"object" => object | "object" => object | ||||
}, | }, | ||||
data <- Utils.maybe_put(data, "id", activity_id), | |||||
data <- Maps.put_if_present(data, "id", activity_id), | |||||
{:ok, activity} <- insert(data, local), | {:ok, activity} <- insert(data, local), | ||||
_ <- notify_and_stream(activity), | _ <- notify_and_stream(activity), | ||||
:ok <- maybe_federate(activity) do | :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("not (? && ?)", activity.recipients, ^blocked_ap_ids), | ||||
where: | where: | ||||
fragment( | fragment( | ||||
"recipients_contain_blocked_domains(?, ?) = false", | |||||
activity.recipients, | |||||
^domain_blocks | |||||
), | |||||
where: | |||||
fragment( | |||||
"not (?->>'type' = 'Announce' and ?->'to' \\?| ?)", | "not (?->>'type' = 'Announce' and ?->'to' \\?| ?)", | ||||
activity.data, | activity.data, | ||||
activity.data, | activity.data, | ||||
@@ -1241,12 +1242,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do | |||||
@spec upload(Upload.source(), keyword()) :: {:ok, Object.t()} | {:error, any()} | @spec upload(Upload.source(), keyword()) :: {:ok, Object.t()} | {:error, any()} | ||||
def upload(file, opts \\ []) do | def upload(file, opts \\ []) do | ||||
with {:ok, data} <- Upload.store(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}) | Repo.insert(%Object{data: obj_data}) | ||||
end | end | ||||
@@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do | |||||
alias Pleroma.EarmarkRenderer | alias Pleroma.EarmarkRenderer | ||||
alias Pleroma.FollowingRelationship | alias Pleroma.FollowingRelationship | ||||
alias Pleroma.Notification | alias Pleroma.Notification | ||||
alias Pleroma.Maps | |||||
alias Pleroma.Object | alias Pleroma.Object | ||||
alias Pleroma.Object.Containment | alias Pleroma.Object.Containment | ||||
alias Pleroma.Repo | alias Pleroma.Repo | ||||
@@ -209,12 +210,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do | |||||
|> Map.put("conversation", context) | |> Map.put("conversation", context) | ||||
end | 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 | def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do | ||||
attachments = | attachments = | ||||
Enum.map(attachment, fn data -> | Enum.map(attachment, fn data -> | ||||
@@ -242,13 +237,13 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do | |||||
attachment_url = | attachment_url = | ||||
%{"href" => href} | %{"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]} | %{"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) | end) | ||||
Map.put(object, "attachment", attachments) | Map.put(object, "attachment", attachments) | ||||
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do | |||||
alias Ecto.UUID | alias Ecto.UUID | ||||
alias Pleroma.Activity | alias Pleroma.Activity | ||||
alias Pleroma.Config | alias Pleroma.Config | ||||
alias Pleroma.Maps | |||||
alias Pleroma.Notification | alias Pleroma.Notification | ||||
alias Pleroma.Object | alias Pleroma.Object | ||||
alias Pleroma.Repo | alias Pleroma.Repo | ||||
@@ -307,7 +308,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do | |||||
"cc" => cc, | "cc" => cc, | ||||
"context" => object.data["context"] | "context" => object.data["context"] | ||||
} | } | ||||
|> maybe_put("id", activity_id) | |||||
|> Maps.put_if_present("id", activity_id) | |||||
end | end | ||||
def make_emoji_reaction_data(user, object, emoji, activity_id) do | def make_emoji_reaction_data(user, object, emoji, activity_id) do | ||||
@@ -477,7 +478,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do | |||||
"object" => followed_id, | "object" => followed_id, | ||||
"state" => "pending" | "state" => "pending" | ||||
} | } | ||||
|> maybe_put("id", activity_id) | |||||
|> Maps.put_if_present("id", activity_id) | |||||
end | end | ||||
def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do | 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" => [], | "cc" => [], | ||||
"context" => object.data["context"] | "context" => object.data["context"] | ||||
} | } | ||||
|> maybe_put("id", activity_id) | |||||
|> Maps.put_if_present("id", activity_id) | |||||
end | end | ||||
def make_announce_data( | def make_announce_data( | ||||
@@ -563,7 +564,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do | |||||
"cc" => [Pleroma.Constants.as_public()], | "cc" => [Pleroma.Constants.as_public()], | ||||
"context" => object.data["context"] | "context" => object.data["context"] | ||||
} | } | ||||
|> maybe_put("id", activity_id) | |||||
|> Maps.put_if_present("id", activity_id) | |||||
end | end | ||||
def make_undo_data( | def make_undo_data( | ||||
@@ -582,7 +583,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do | |||||
"cc" => [Pleroma.Constants.as_public()], | "cc" => [Pleroma.Constants.as_public()], | ||||
"context" => context | "context" => context | ||||
} | } | ||||
|> maybe_put("id", activity_id) | |||||
|> Maps.put_if_present("id", activity_id) | |||||
end | end | ||||
@spec add_announce_to_object(Activity.t(), Object.t()) :: | @spec add_announce_to_object(Activity.t(), Object.t()) :: | ||||
@@ -627,7 +628,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do | |||||
"to" => [followed.ap_id], | "to" => [followed.ap_id], | ||||
"object" => follow_activity.data | "object" => follow_activity.data | ||||
} | } | ||||
|> maybe_put("id", activity_id) | |||||
|> Maps.put_if_present("id", activity_id) | |||||
end | end | ||||
#### Block-related helpers | #### Block-related helpers | ||||
@@ -650,7 +651,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do | |||||
"to" => [blocked.ap_id], | "to" => [blocked.ap_id], | ||||
"object" => blocked.ap_id | "object" => blocked.ap_id | ||||
} | } | ||||
|> maybe_put("id", activity_id) | |||||
|> Maps.put_if_present("id", activity_id) | |||||
end | end | ||||
#### Create-related helpers | #### Create-related helpers | ||||
@@ -740,6 +741,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do | |||||
def get_reports(params, page, page_size) do | def get_reports(params, page, page_size) do | ||||
params = | params = | ||||
params | params | ||||
|> Map.new(fn {key, value} -> {to_string(key), value} end) | |||||
|> Map.put("type", "Flag") | |> Map.put("type", "Flag") | ||||
|> Map.put("skip_preload", true) | |> Map.put("skip_preload", true) | ||||
|> Map.put("preload_report_notes", 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)) | |> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data)) | ||||
|> Repo.all() | |> Repo.all() | ||||
end | end | ||||
def maybe_put(map, _key, nil), do: map | |||||
def maybe_put(map, key, value), do: Map.put(map, key, value) | |||||
end | end |
@@ -7,38 +7,25 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do | |||||
import Pleroma.Web.ControllerHelper, only: [json_response: 3] | import Pleroma.Web.ControllerHelper, only: [json_response: 3] | ||||
alias Pleroma.Activity | |||||
alias Pleroma.Config | alias Pleroma.Config | ||||
alias Pleroma.ConfigDB | |||||
alias Pleroma.MFA | alias Pleroma.MFA | ||||
alias Pleroma.ModerationLog | alias Pleroma.ModerationLog | ||||
alias Pleroma.Plugs.OAuthScopesPlug | alias Pleroma.Plugs.OAuthScopesPlug | ||||
alias Pleroma.ReportNote | |||||
alias Pleroma.Stats | alias Pleroma.Stats | ||||
alias Pleroma.User | alias Pleroma.User | ||||
alias Pleroma.UserInviteToken | |||||
alias Pleroma.Web.ActivityPub.ActivityPub | alias Pleroma.Web.ActivityPub.ActivityPub | ||||
alias Pleroma.Web.ActivityPub.Builder | alias Pleroma.Web.ActivityPub.Builder | ||||
alias Pleroma.Web.ActivityPub.Pipeline | alias Pleroma.Web.ActivityPub.Pipeline | ||||
alias Pleroma.Web.ActivityPub.Relay | alias Pleroma.Web.ActivityPub.Relay | ||||
alias Pleroma.Web.ActivityPub.Utils | |||||
alias Pleroma.Web.AdminAPI | alias Pleroma.Web.AdminAPI | ||||
alias Pleroma.Web.AdminAPI.AccountView | alias Pleroma.Web.AdminAPI.AccountView | ||||
alias Pleroma.Web.AdminAPI.ConfigView | |||||
alias Pleroma.Web.AdminAPI.ModerationLogView | alias Pleroma.Web.AdminAPI.ModerationLogView | ||||
alias Pleroma.Web.AdminAPI.Report | |||||
alias Pleroma.Web.AdminAPI.ReportView | |||||
alias Pleroma.Web.AdminAPI.Search | alias Pleroma.Web.AdminAPI.Search | ||||
alias Pleroma.Web.CommonAPI | |||||
alias Pleroma.Web.Endpoint | alias Pleroma.Web.Endpoint | ||||
alias Pleroma.Web.MastodonAPI | |||||
alias Pleroma.Web.MastodonAPI.AppView | |||||
alias Pleroma.Web.OAuth.App | |||||
alias Pleroma.Web.Router | alias Pleroma.Web.Router | ||||
require Logger | require Logger | ||||
@descriptions Pleroma.Docs.JSON.compile() | |||||
@users_page_size 50 | @users_page_size 50 | ||||
plug( | 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( | plug( | ||||
OAuthScopesPlug, | OAuthScopesPlug, | ||||
%{scopes: ["write:follows"], admin: true} | %{scopes: ["write:follows"], admin: true} | ||||
@@ -85,18 +64,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do | |||||
plug( | plug( | ||||
OAuthScopesPlug, | 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} | %{scopes: ["read:statuses"], admin: true} | ||||
when action in [:list_user_statuses, :list_instance_statuses] | when action in [:list_user_statuses, :list_instance_statuses] | ||||
) | ) | ||||
@@ -105,11 +72,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do | |||||
OAuthScopesPlug, | OAuthScopesPlug, | ||||
%{scopes: ["read"], admin: true} | %{scopes: ["read"], admin: true} | ||||
when action in [ | when action in [ | ||||
:config_show, | |||||
:list_log, | :list_log, | ||||
:stats, | :stats, | ||||
:relay_list, | :relay_list, | ||||
:config_descriptions, | |||||
:need_reboot | :need_reboot | ||||
] | ] | ||||
) | ) | ||||
@@ -119,13 +84,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do | |||||
%{scopes: ["write"], admin: true} | %{scopes: ["write"], admin: true} | ||||
when action in [ | when action in [ | ||||
:restart, | :restart, | ||||
:config_update, | |||||
:resend_confirmation_email, | :resend_confirmation_email, | ||||
:confirm_email, | :confirm_email, | ||||
:oauth_app_create, | |||||
:oauth_app_list, | |||||
:oauth_app_update, | |||||
:oauth_app_delete, | |||||
:reload_emoji | :reload_emoji | ||||
] | ] | ||||
) | ) | ||||
@@ -294,7 +254,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do | |||||
}) | }) | ||||
conn | conn | ||||
|> put_view(MastodonAPI.StatusView) | |||||
|> put_view(AdminAPI.StatusView) | |||||
|> render("index.json", %{activities: activities, as: :activity}) | |> render("index.json", %{activities: activities, as: :activity}) | ||||
else | else | ||||
_ -> {:error, :not_found} | _ -> {:error, :not_found} | ||||
@@ -575,69 +535,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do | |||||
end | end | ||||
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" | @doc "Get a password reset token (base64 string) for given nickname" | ||||
def get_password_reset(conn, %{"nickname" => nickname}) do | def get_password_reset(conn, %{"nickname" => nickname}) do | ||||
(%User{local: true} = user) = User.get_cached_by_nickname(nickname) | (%User{local: true} = user) = User.get_cached_by_nickname(nickname) | ||||
@@ -724,85 +621,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do | |||||
end | end | ||||
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 | def list_log(conn, params) do | ||||
{page, page_size} = page_params(params) | {page, page_size} = page_params(params) | ||||
@@ -821,105 +639,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do | |||||
|> render("index.json", %{log: log}) | |> render("index.json", %{log: log}) | ||||
end | 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 | def restart(conn, _params) do | ||||
with :ok <- configurable_from_database() do | with :ok <- configurable_from_database() do | ||||
Restarter.Pleroma.restart(Config.get(:env), 50) | Restarter.Pleroma.restart(Config.get(:env), 50) | ||||
@@ -940,28 +659,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do | |||||
end | end | ||||
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 | def reload_emoji(conn, _params) do | ||||
Pleroma.Emoji.reload() | Pleroma.Emoji.reload() | ||||
@@ -996,83 +693,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do | |||||
conn |> json("") | conn |> json("") | ||||
end | 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 | def stats(conn, _) do | ||||
count = Stats.get_status_visibility_count() | 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 | def show(conn, %{id: id}) do | ||||
with %Activity{} = activity <- Activity.get_by_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 | else | ||||
nil -> {:error, :not_found} | nil -> {:error, :not_found} | ||||
end | end | ||||
@@ -80,24 +80,6 @@ defmodule Pleroma.Web.AdminAPI.AccountView do | |||||
} | } | ||||
end | 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 | def render("created.json", %{user: user}) do | ||||
%{ | %{ | ||||
type: "success", | 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()], | parameters: [id_param()], | ||||
security: [%{"oAuth" => ["read:statuses"]}], | security: [%{"oAuth" => ["read:statuses"]}], | ||||
responses: %{ | responses: %{ | ||||
200 => Operation.response("Status", "application/json", Status), | |||||
200 => Operation.response("Status", "application/json", status()), | |||||
404 => Operation.response("Not Found", "application/json", ApiError) | 404 => Operation.response("Not Found", "application/json", ApiError) | ||||
} | } | ||||
} | } | ||||
@@ -123,7 +123,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.StatusOperation do | |||||
} | } | ||||
end | end | ||||
defp admin_account do | |||||
def admin_account do | |||||
%Schema{ | %Schema{ | ||||
type: :object, | type: :object, | ||||
properties: %{ | properties: %{ | ||||
@@ -137,7 +137,7 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do | |||||
"background_upload_limit" => 4_000_000, | "background_upload_limit" => 4_000_000, | ||||
"background_image" => "/static/image.png", | "background_image" => "/static/image.png", | ||||
"banner_upload_limit" => 4_000_000, | "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", | "email" => "lain@lain.com", | ||||
"languages" => ["en"], | "languages" => ["en"], | ||||
"max_toot_chars" => 5000, | "max_toot_chars" => 5000, | ||||
@@ -99,11 +99,6 @@ defmodule Pleroma.Web.ControllerHelper do | |||||
render_error(conn, :not_implemented, "Can't display this activity") | render_error(conn, :not_implemented, "Can't display this activity") | ||||
end | 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 """ | @doc """ | ||||
Returns true if request specifies to include embedded relationships in account objects. | 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 | 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.ActivityPub.ActivityPub | ||||
alias Pleroma.Web.Feed.FeedView | alias Pleroma.Web.Feed.FeedView | ||||
import Pleroma.Web.ControllerHelper, only: [put_if_exist: 3] | |||||
def feed(conn, %{"tag" => raw_tag} = params) do | def feed(conn, %{"tag" => raw_tag} = params) do | ||||
{format, tag} = parse_tag(raw_tag) | {format, tag} = parse_tag(raw_tag) | ||||
activities = | activities = | ||||
%{"type" => ["Create"], "tag" => tag} | %{"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() | |> ActivityPub.fetch_public_activities() | ||||
conn | conn | ||||
@@ -11,8 +11,6 @@ defmodule Pleroma.Web.Feed.UserController do | |||||
alias Pleroma.Web.ActivityPub.ActivityPubController | alias Pleroma.Web.ActivityPub.ActivityPubController | ||||
alias Pleroma.Web.Feed.FeedView | alias Pleroma.Web.Feed.FeedView | ||||
import Pleroma.Web.ControllerHelper, only: [put_if_exist: 3] | |||||
plug(Pleroma.Plugs.SetFormatPlug when action in [:feed_redirect]) | plug(Pleroma.Plugs.SetFormatPlug when action in [:feed_redirect]) | ||||
action_fallback(:errors) | action_fallback(:errors) | ||||
@@ -55,7 +53,7 @@ defmodule Pleroma.Web.Feed.UserController do | |||||
"type" => ["Create"], | "type" => ["Create"], | ||||
"actor_id" => user.ap_id | "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() | |> ActivityPub.fetch_public_or_unlisted_activities() | ||||
conn | conn | ||||
@@ -14,6 +14,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do | |||||
json_response: 3 | json_response: 3 | ||||
] | ] | ||||
alias Pleroma.Maps | |||||
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug | alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug | ||||
alias Pleroma.Plugs.OAuthScopesPlug | alias Pleroma.Plugs.OAuthScopesPlug | ||||
alias Pleroma.Plugs.RateLimiter | alias Pleroma.Plugs.RateLimiter | ||||
@@ -160,23 +161,22 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do | |||||
:discoverable | :discoverable | ||||
] | ] | ||||
|> Enum.reduce(%{}, fn key, acc -> | |> 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) | 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, | :raw_fields, | ||||
params[:fields_attributes], | |||||
&{:ok, normalize_fields_attributes(&1)} | &{: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) | changeset = User.update_changeset(user, user_params) | ||||
@@ -206,16 +206,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do | |||||
} | } | ||||
end | 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 | defp normalize_fields_attributes(fields) do | ||||
if Enum.all?(fields, &is_tuple/1) do | if Enum.all?(fields, &is_tuple/1) do | ||||
Enum.map(fields, fn {_, v} -> v end) | Enum.map(fields, fn {_, v} -> v end) | ||||
@@ -113,22 +113,44 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do | |||||
query | query | ||||
|> prepare_tags() | |> prepare_tags() | ||||
|> Enum.map(fn tag -> | |> Enum.map(fn tag -> | ||||
tag = String.trim_leading(tag, "#") | |||||
%{name: tag, url: tags_path <> tag} | %{name: tag, url: tags_path <> tag} | ||||
end) | end) | ||||
end | end | ||||
defp resource_search(:v1, "hashtags", query, _options) do | defp resource_search(:v1, "hashtags", query, _options) do | ||||
query | |||||
|> prepare_tags() | |||||
|> Enum.map(fn tag -> String.trim_leading(tag, "#") end) | |||||
prepare_tags(query) | |||||
end | 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 | end | ||||
defp with_fallback(f, fallback \\ []) do | defp with_fallback(f, fallback \\ []) do | ||||
@@ -45,10 +45,6 @@ defmodule Pleroma.Web.MastodonAPI.AppView do | |||||
defp with_vapid_key(data) do | defp with_vapid_key(data) do | ||||
vapid_key = Application.get_env(:web_push_encryption, :vapid_details, [])[:public_key] | 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 | ||||
end | end |
@@ -30,7 +30,7 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityView do | |||||
defp with_media_attachments(data, _), do: data | defp with_media_attachments(data, _), do: data | ||||
defp status_params(params) do | defp status_params(params) do | ||||
data = %{ | |||||
%{ | |||||
text: params["status"], | text: params["status"], | ||||
sensitive: params["sensitive"], | sensitive: params["sensitive"], | ||||
spoiler_text: params["spoiler_text"], | spoiler_text: params["spoiler_text"], | ||||
@@ -39,10 +39,6 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityView do | |||||
poll: params["poll"], | poll: params["poll"], | ||||
in_reply_to_id: params["in_reply_to_id"] | 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 | ||||
end | end |
@@ -25,12 +25,12 @@ defmodule Pleroma.Web.OAuth.App do | |||||
timestamps() | timestamps() | ||||
end | end | ||||
@spec changeset(App.t(), map()) :: Ecto.Changeset.t() | |||||
@spec changeset(t(), map()) :: Ecto.Changeset.t() | |||||
def changeset(struct, params) do | def changeset(struct, params) do | ||||
cast(struct, params, [:client_name, :redirect_uris, :scopes, :website, :trusted]) | cast(struct, params, [:client_name, :redirect_uris, :scopes, :website, :trusted]) | ||||
end | end | ||||
@spec register_changeset(App.t(), map()) :: Ecto.Changeset.t() | |||||
@spec register_changeset(t(), map()) :: Ecto.Changeset.t() | |||||
def register_changeset(struct, params \\ %{}) do | def register_changeset(struct, params \\ %{}) do | ||||
changeset = | changeset = | ||||
struct | struct | ||||
@@ -52,18 +52,19 @@ defmodule Pleroma.Web.OAuth.App do | |||||
end | end | ||||
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 | def create(params) do | ||||
with changeset <- __MODULE__.register_changeset(%__MODULE__{}, params) do | |||||
Repo.insert(changeset) | |||||
end | |||||
%__MODULE__{} | |||||
|> register_changeset(params) | |||||
|> Repo.insert() | |||||
end | end | ||||
@spec update(map()) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()} | |||||
def update(params) do | |||||
with %__MODULE__{} = app <- Repo.get(__MODULE__, params["id"]), | |||||
changeset <- changeset(app, params) do | |||||
Repo.update(changeset) | |||||
@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 | ||||
end | end | ||||
@@ -71,7 +72,7 @@ defmodule Pleroma.Web.OAuth.App do | |||||
Gets app by attrs or create new with attrs. | Gets app by attrs or create new with attrs. | ||||
And updates the scopes if need. | 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 | def get_or_make(attrs, scopes) do | ||||
with %__MODULE__{} = app <- Repo.get_by(__MODULE__, attrs) do | with %__MODULE__{} = app <- Repo.get_by(__MODULE__, attrs) do | ||||
update_scopes(app, scopes) | update_scopes(app, scopes) | ||||
@@ -92,7 +93,7 @@ defmodule Pleroma.Web.OAuth.App do | |||||
|> Repo.update() | |> Repo.update() | ||||
end | end | ||||
@spec search(map()) :: {:ok, [App.t()], non_neg_integer()} | |||||
@spec search(map()) :: {:ok, [t()], non_neg_integer()} | |||||
def search(params) do | def search(params) do | ||||
query = from(a in __MODULE__) | query = from(a in __MODULE__) | ||||
@@ -128,7 +129,7 @@ defmodule Pleroma.Web.OAuth.App do | |||||
{:ok, Repo.all(query), count} | {:ok, Repo.all(query), count} | ||||
end | 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 | def destroy(id) do | ||||
with %__MODULE__{} = app <- Repo.get(__MODULE__, id) do | with %__MODULE__{} = app <- Repo.get(__MODULE__, id) do | ||||
Repo.delete(app) | Repo.delete(app) | ||||
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do | |||||
use Pleroma.Web, :controller | use Pleroma.Web, :controller | ||||
alias Pleroma.Helpers.UriHelper | alias Pleroma.Helpers.UriHelper | ||||
alias Pleroma.Maps | |||||
alias Pleroma.MFA | alias Pleroma.MFA | ||||
alias Pleroma.Plugs.RateLimiter | alias Pleroma.Plugs.RateLimiter | ||||
alias Pleroma.Registration | alias Pleroma.Registration | ||||
@@ -108,7 +109,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do | |||||
if redirect_uri in String.split(app.redirect_uris) do | if redirect_uri in String.split(app.redirect_uris) do | ||||
redirect_uri = redirect_uri(conn, redirect_uri) | redirect_uri = redirect_uri(conn, redirect_uri) | ||||
url_params = %{access_token: token.token} | 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) | url = UriHelper.append_uri_params(redirect_uri, url_params) | ||||
redirect(conn, external: url) | redirect(conn, external: url) | ||||
else | else | ||||
@@ -147,7 +148,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do | |||||
if redirect_uri in String.split(app.redirect_uris) do | if redirect_uri in String.split(app.redirect_uris) do | ||||
redirect_uri = redirect_uri(conn, redirect_uri) | redirect_uri = redirect_uri(conn, redirect_uri) | ||||
url_params = %{code: auth.token} | 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) | url = UriHelper.append_uri_params(redirect_uri, url_params) | ||||
redirect(conn, external: url) | redirect(conn, external: url) | ||||
else | else | ||||
@@ -164,10 +164,10 @@ defmodule Pleroma.Web.Router do | |||||
post("/relay", AdminAPIController, :relay_follow) | post("/relay", AdminAPIController, :relay_follow) | ||||
delete("/relay", AdminAPIController, :relay_unfollow) | 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) | get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset) | ||||
patch("/users/force_password_reset", AdminAPIController, :force_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/confirm_email", AdminAPIController, :confirm_email) | ||||
patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_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) | get("/statuses/:id", StatusController, :show) | ||||
put("/statuses/:id", StatusController, :update) | put("/statuses/:id", StatusController, :update) | ||||
delete("/statuses/:id", StatusController, :delete) | delete("/statuses/:id", StatusController, :delete) | ||||
get("/statuses", StatusController, :index) | 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("/need_reboot", AdminAPIController, :need_reboot) | ||||
get("/restart", AdminAPIController, :restart) | get("/restart", AdminAPIController, :restart) | ||||
@@ -205,10 +205,10 @@ defmodule Pleroma.Web.Router do | |||||
post("/reload_emoji", AdminAPIController, :reload_emoji) | post("/reload_emoji", AdminAPIController, :reload_emoji) | ||||
get("/stats", AdminAPIController, :stats) | 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 | end | ||||
scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do | scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do | ||||
@@ -673,6 +673,8 @@ defmodule Pleroma.Web.Router do | |||||
post("/auth/password", MastodonAPI.AuthController, :password_reset) | post("/auth/password", MastodonAPI.AuthController, :password_reset) | ||||
get("/web/*path", MastoFEController, :index) | get("/web/*path", MastoFEController, :index) | ||||
get("/embed/:id", EmbedController, :show) | |||||
end | end | ||||
scope "/proxy/", Pleroma.Web.MediaProxy do | 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=天子") | get(conn, "/api/v2/search?q=天子") | ||||
|> json_response_and_validate_schema(200) | |> json_response_and_validate_schema(200) | ||||
assert results["hashtags"] == [ | |||||
%{"name" => "天子", "url" => "#{Web.base_url()}/tag/天子"} | |||||
] | |||||
[status] = results["statuses"] | [status] = results["statuses"] | ||||
assert status["id"] == to_string(activity.id) | assert status["id"] == to_string(activity.id) | ||||
end | 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 | test "excludes a blocked users from search results", %{conn: conn} do | ||||
user = insert(:user) | user = insert(:user) | ||||
user_smith = insert(:user, %{nickname: "Agent", name: "I love 2hu"}) | user_smith = insert(:user, %{nickname: "Agent", name: "I love 2hu"}) | ||||
@@ -179,7 +217,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do | |||||
[account | _] = results["accounts"] | [account | _] = results["accounts"] | ||||
assert account["id"] == to_string(user_three.id) | assert account["id"] == to_string(user_three.id) | ||||
assert results["hashtags"] == [] | |||||
assert results["hashtags"] == ["2hu"] | |||||
[status] = results["statuses"] | [status] = results["statuses"] | ||||
assert status["id"] == to_string(activity.id) | 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") | res_conn = get(conn, "/api/v1/timelines/public") | ||||
assert length(json_response_and_validate_schema(res_conn, 200)) == 1 | assert length(json_response_and_validate_schema(res_conn, 200)) == 1 | ||||
end | 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 | end | ||||
defp local_and_remote_activities do | defp local_and_remote_activities do | ||||