Browse Source

Merge branch 'develop' of git.pleroma.social:pleroma/pleroma into remake-remodel-dms

1570-levenshtein-distance-user-search
lain 4 years ago
parent
commit
a8ca030d85
52 changed files with 4412 additions and 2814 deletions
  1. +195
    -166
      benchmarks/load_testing/activities.ex
  2. +53
    -0
      benchmarks/load_testing/fetcher.ex
  3. +21
    -1
      benchmarks/load_testing/users.ex
  4. +34
    -4
      benchmarks/mix/tasks/pleroma/benchmarks/tags.ex
  5. +1
    -1
      config/config.exs
  6. +2
    -2
      docs/API/admin_api.md
  7. +6
    -0
      docs/clients.md
  8. +1
    -1
      lib/pleroma/constants.ex
  9. +0
    -8
      lib/pleroma/helpers/uri_helper.ex
  10. +15
    -0
      lib/pleroma/maps.ex
  11. +11
    -15
      lib/pleroma/web/activity_pub/activity_pub.ex
  12. +6
    -11
      lib/pleroma/web/activity_pub/transmogrifier.ex
  13. +9
    -10
      lib/pleroma/web/activity_pub/utils.ex
  14. +1
    -381
      lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
  15. +152
    -0
      lib/pleroma/web/admin_api/controllers/config_controller.ex
  16. +78
    -0
      lib/pleroma/web/admin_api/controllers/invite_controller.ex
  17. +77
    -0
      lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex
  18. +107
    -0
      lib/pleroma/web/admin_api/controllers/report_controller.ex
  19. +1
    -3
      lib/pleroma/web/admin_api/controllers/status_controller.ex
  20. +0
    -18
      lib/pleroma/web/admin_api/views/account_view.ex
  21. +25
    -0
      lib/pleroma/web/admin_api/views/invite_view.ex
  22. +142
    -0
      lib/pleroma/web/api_spec/operations/admin/config_operation.ex
  23. +148
    -0
      lib/pleroma/web/api_spec/operations/admin/invite_operation.ex
  24. +215
    -0
      lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex
  25. +237
    -0
      lib/pleroma/web/api_spec/operations/admin/report_operation.ex
  26. +2
    -2
      lib/pleroma/web/api_spec/operations/admin/status_operation.ex
  27. +1
    -1
      lib/pleroma/web/api_spec/operations/instance_operation.ex
  28. +0
    -5
      lib/pleroma/web/controller_helper.ex
  29. +42
    -0
      lib/pleroma/web/embed_controller.ex
  30. +1
    -3
      lib/pleroma/web/feed/tag_controller.ex
  31. +1
    -3
      lib/pleroma/web/feed/user_controller.ex
  32. +13
    -23
      lib/pleroma/web/mastodon_api/controllers/account_controller.ex
  33. +31
    -9
      lib/pleroma/web/mastodon_api/controllers/search_controller.ex
  34. +1
    -5
      lib/pleroma/web/mastodon_api/views/app_view.ex
  35. +2
    -6
      lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex
  36. +15
    -14
      lib/pleroma/web/oauth/app.ex
  37. +3
    -2
      lib/pleroma/web/oauth/oauth_controller.ex
  38. +18
    -16
      lib/pleroma/web/router.ex
  39. +8
    -0
      lib/pleroma/web/templates/embed/_attachment.html.eex
  40. +76
    -0
      lib/pleroma/web/templates/embed/show.html.eex
  41. +15
    -0
      lib/pleroma/web/templates/layout/embed.html.eex
  42. +74
    -0
      lib/pleroma/web/views/embed_view.ex
  43. +33
    -0
      priv/repo/migrations/20200520155351_add_recipients_contain_blocked_domains_function.exs
  44. +115
    -0
      priv/static/embed.css
  45. +43
    -0
      priv/static/embed.js
  46. +134
    -2103
      test/web/admin_api/controllers/admin_api_controller_test.exs
  47. +1290
    -0
      test/web/admin_api/controllers/config_controller_test.exs
  48. +281
    -0
      test/web/admin_api/controllers/invite_controller_test.exs
  49. +220
    -0
      test/web/admin_api/controllers/oauth_app_controller_test.exs
  50. +374
    -0
      test/web/admin_api/controllers/report_controller_test.exs
  51. +39
    -1
      test/web/mastodon_api/controllers/search_controller_test.exs
  52. +43
    -0
      test/web/mastodon_api/controllers/timeline_controller_test.exs

+ 195
- 166
benchmarks/load_testing/activities.ex View File

@@ -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




+ 53
- 0
benchmarks/load_testing/fetcher.ex View File

@@ -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()




+ 21
- 1
benchmarks/load_testing/users.ex View File

@@ -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

+ 34
- 4
benchmarks/mix/tasks/pleroma/benchmarks/tags.ex View File

@@ -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

+ 1
- 1
config/config.exs View File

@@ -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,


+ 2
- 2
docs/API/admin_api.md View File

@@ -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




+ 6
- 0
docs/clients.md View File

@@ -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>


+ 1
- 1
lib/pleroma/constants.ex View File

@@ -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

+ 0
- 8
lib/pleroma/helpers/uri_helper.ex View File

@@ -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

+ 15
- 0
lib/pleroma/maps.ex View File

@@ -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

+ 11
- 15
lib/pleroma/web/activity_pub/activity_pub.ex View File

@@ -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


+ 6
- 11
lib/pleroma/web/activity_pub/transmogrifier.ex View File

@@ -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)


+ 9
- 10
lib/pleroma/web/activity_pub/utils.ex View File

@@ -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

+ 1
- 381
lib/pleroma/web/admin_api/controllers/admin_api_controller.ex View File

@@ -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()




+ 152
- 0
lib/pleroma/web/admin_api/controllers/config_controller.ex View File

@@ -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

+ 78
- 0
lib/pleroma/web/admin_api/controllers/invite_controller.ex View File

@@ -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

+ 77
- 0
lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex View File

@@ -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

+ 107
- 0
lib/pleroma/web/admin_api/controllers/report_controller.ex View File

@@ -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

+ 1
- 3
lib/pleroma/web/admin_api/controllers/status_controller.ex View File

@@ -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


+ 0
- 18
lib/pleroma/web/admin_api/views/account_view.ex View File

@@ -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",


+ 25
- 0
lib/pleroma/web/admin_api/views/invite_view.ex View File

@@ -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

+ 142
- 0
lib/pleroma/web/api_spec/operations/admin/config_operation.ex View File

@@ -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

+ 148
- 0
lib/pleroma/web/api_spec/operations/admin/invite_operation.ex View File

@@ -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

+ 215
- 0
lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex View File

@@ -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

+ 237
- 0
lib/pleroma/web/api_spec/operations/admin/report_operation.ex View File

@@ -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

+ 2
- 2
lib/pleroma/web/api_spec/operations/admin/status_operation.ex View File

@@ -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: %{


+ 1
- 1
lib/pleroma/web/api_spec/operations/instance_operation.ex View File

@@ -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,


+ 0
- 5
lib/pleroma/web/controller_helper.ex View File

@@ -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


+ 42
- 0
lib/pleroma/web/embed_controller.ex View File

@@ -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

+ 1
- 3
lib/pleroma/web/feed/tag_controller.ex View File

@@ -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


+ 1
- 3
lib/pleroma/web/feed/user_controller.ex View File

@@ -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


+ 13
- 23
lib/pleroma/web/mastodon_api/controllers/account_controller.ex View File

@@ -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)


+ 31
- 9
lib/pleroma/web/mastodon_api/controllers/search_controller.ex View File

@@ -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


+ 1
- 5
lib/pleroma/web/mastodon_api/views/app_view.ex View File

@@ -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

+ 2
- 6
lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex View File

@@ -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

+ 15
- 14
lib/pleroma/web/oauth/app.ex View File

@@ -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)


+ 3
- 2
lib/pleroma/web/oauth/oauth_controller.ex View File

@@ -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


+ 18
- 16
lib/pleroma/web/router.ex View File

@@ -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


+ 8
- 0
lib/pleroma/web/templates/embed/_attachment.html.eex View File

@@ -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 %>

+ 76
- 0
lib/pleroma/web/templates/embed/show.html.eex View File

@@ -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>

+ 15
- 0
lib/pleroma/web/templates/layout/embed.html.eex View File

@@ -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>

+ 74
- 0
lib/pleroma/web/views/embed_view.ex View File

@@ -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

+ 33
- 0
priv/repo/migrations/20200520155351_add_recipients_contain_blocked_domains_function.exs View File

@@ -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

+ 115
- 0
priv/static/embed.css View File

@@ -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;
}

+ 43
- 0
priv/static/embed.js View File

@@ -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()
})
})
})()

+ 134
- 2103
test/web/admin_api/controllers/admin_api_controller_test.exs
File diff suppressed because it is too large
View File


+ 1290
- 0
test/web/admin_api/controllers/config_controller_test.exs
File diff suppressed because it is too large
View File


+ 281
- 0
test/web/admin_api/controllers/invite_controller_test.exs View File

@@ -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

+ 220
- 0
test/web/admin_api/controllers/oauth_app_controller_test.exs View File

@@ -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

+ 374
- 0
test/web/admin_api/controllers/report_controller_test.exs View File

@@ -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

+ 39
- 1
test/web/mastodon_api/controllers/search_controller_test.exs View File

@@ -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)


+ 43
- 0
test/web/mastodon_api/controllers/timeline_controller_test.exs View File

@@ -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


Loading…
Cancel
Save