Browse Source

Merge branch 'develop' into openapi/admin/config

1570-levenshtein-distance-user-search
Egor Kislitsyn 4 years ago
parent
commit
b4d5bdd6f1
No known key found for this signature in database GPG Key ID: 1B49CB15B71E7805
68 changed files with 3117 additions and 1508 deletions
  1. +1
    -0
      CHANGELOG.md
  2. +195
    -166
      benchmarks/load_testing/activities.ex
  3. +53
    -0
      benchmarks/load_testing/fetcher.ex
  4. +21
    -1
      benchmarks/load_testing/users.ex
  5. +34
    -4
      benchmarks/mix/tasks/pleroma/benchmarks/tags.ex
  6. +3
    -2
      config/config.exs
  7. +2
    -2
      docs/API/admin_api.md
  8. +9
    -5
      docs/API/differences_in_mastoapi_responses.md
  9. +29
    -0
      docs/administration/CLI_tasks/database.md
  10. +6
    -0
      docs/clients.md
  11. +9
    -14
      lib/mix/tasks/pleroma/database.ex
  12. +1
    -1
      lib/pleroma/constants.ex
  13. +1
    -16
      lib/pleroma/http/adapter_helper/hackney.ex
  14. +37
    -0
      lib/pleroma/maintenance.ex
  15. +62
    -20
      lib/pleroma/plugs/http_security_plug.ex
  16. +3
    -3
      lib/pleroma/user/query.ex
  17. +18
    -0
      lib/pleroma/web/activity_pub/activity_pub.ex
  18. +11
    -5
      lib/pleroma/web/activity_pub/builder.ex
  19. +5
    -2
      lib/pleroma/web/activity_pub/side_effects.ex
  20. +1
    -0
      lib/pleroma/web/activity_pub/utils.ex
  21. +1
    -254
      lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
  22. +78
    -0
      lib/pleroma/web/admin_api/controllers/invite_controller.ex
  23. +87
    -0
      lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex
  24. +107
    -0
      lib/pleroma/web/admin_api/controllers/report_controller.ex
  25. +1
    -3
      lib/pleroma/web/admin_api/controllers/status_controller.ex
  26. +1
    -2
      lib/pleroma/web/admin_api/search.ex
  27. +0
    -18
      lib/pleroma/web/admin_api/views/account_view.ex
  28. +25
    -0
      lib/pleroma/web/admin_api/views/invite_view.ex
  29. +148
    -0
      lib/pleroma/web/api_spec/operations/admin/invite_operation.ex
  30. +215
    -0
      lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex
  31. +237
    -0
      lib/pleroma/web/api_spec/operations/admin/report_operation.ex
  32. +2
    -2
      lib/pleroma/web/api_spec/operations/admin/status_operation.ex
  33. +1
    -1
      lib/pleroma/web/api_spec/operations/instance_operation.ex
  34. +42
    -0
      lib/pleroma/web/embed_controller.ex
  35. +20
    -3
      lib/pleroma/web/mastodon_api/controllers/account_controller.ex
  36. +17
    -0
      lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex
  37. +31
    -9
      lib/pleroma/web/mastodon_api/controllers/search_controller.ex
  38. +1
    -1
      lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
  39. +7
    -5
      lib/pleroma/web/mastodon_api/views/account_view.ex
  40. +15
    -14
      lib/pleroma/web/oauth/app.ex
  41. +15
    -13
      lib/pleroma/web/router.ex
  42. +1
    -1
      lib/pleroma/web/streamer/streamer.ex
  43. +8
    -0
      lib/pleroma/web/templates/embed/_attachment.html.eex
  44. +76
    -0
      lib/pleroma/web/templates/embed/show.html.eex
  45. +15
    -0
      lib/pleroma/web/templates/layout/embed.html.eex
  46. +74
    -0
      lib/pleroma/web/views/embed_view.ex
  47. +5
    -5
      mix.lock
  48. +43
    -41
      priv/gettext/nl/LC_MESSAGES/errors.po
  49. +33
    -0
      priv/repo/migrations/20200520155351_add_recipients_contain_blocked_domains_function.exs
  50. +115
    -0
      priv/static/embed.css
  51. +43
    -0
      priv/static/embed.js
  52. +0
    -12
      test/http/adapter_helper/hackney_test.exs
  53. +1
    -1
      test/plugs/http_security_plug_test.exs
  54. +2
    -1
      test/tasks/relay_test.exs
  55. +1
    -1
      test/user_test.exs
  56. +1
    -0
      test/web/activity_pub/relay_test.exs
  57. +5
    -756
      test/web/admin_api/controllers/admin_api_controller_test.exs
  58. +281
    -0
      test/web/admin_api/controllers/invite_controller_test.exs
  59. +220
    -0
      test/web/admin_api/controllers/oauth_app_controller_test.exs
  60. +374
    -0
      test/web/admin_api/controllers/report_controller_test.exs
  61. +8
    -0
      test/web/admin_api/controllers/status_controller_test.exs
  62. +23
    -15
      test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs
  63. +92
    -91
      test/web/mastodon_api/controllers/conversation_controller_test.exs
  64. +39
    -1
      test/web/mastodon_api/controllers/search_controller_test.exs
  65. +52
    -2
      test/web/mastodon_api/controllers/timeline_controller_test.exs
  66. +31
    -4
      test/web/mastodon_api/views/account_view_test.exs
  67. +3
    -11
      test/web/media_proxy/media_proxy_test.exs
  68. +19
    -0
      test/web/streamer/streamer_test.exs

+ 1
- 0
CHANGELOG.md View File

@@ -44,6 +44,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Fix follower/blocks import when nicknames starts with @
- Filtering of push notifications on activities from blocked domains
- Resolving Peertube accounts with Webfinger
- `blob:` urls not being allowed by connect-src CSP

## [Unreleased (patch)]



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

@@ -22,8 +22,21 @@ defmodule Pleroma.LoadTesting.Activities do
@max_concurrency 10

@visibility ~w(public private direct unlisted)
@types ~w(simple emoji mentions hell_thread attachment tag like reblog simple_thread remote)
@groups ~w(user friends non_friends)
@types [
:simple,
:emoji,
:mentions,
:hell_thread,
:attachment,
:tag,
:like,
:reblog,
:simple_thread
]
@groups [:friends_local, :friends_remote, :non_friends_local, :non_friends_local]
@remote_groups [:friends_remote, :non_friends_remote]
@friends_groups [:friends_local, :friends_remote]
@non_friends_groups [:non_friends_local, :non_friends_remote]

@spec generate(User.t(), keyword()) :: :ok
def generate(user, opts \\ []) do
@@ -34,33 +47,24 @@ defmodule Pleroma.LoadTesting.Activities do

opts = Keyword.merge(@defaults, opts)

friends =
user
|> Users.get_users(limit: opts[:friends_used], local: :local, friends?: true)
|> Enum.shuffle()
users = Users.prepare_users(user, opts)

non_friends =
user
|> Users.get_users(limit: opts[:non_friends_used], local: :local, friends?: false)
|> Enum.shuffle()
{:ok, _} = Agent.start_link(fn -> users[:non_friends_remote] end, name: :non_friends_remote)

task_data =
for visibility <- @visibility,
type <- @types,
group <- @groups,
group <- [:user | @groups],
do: {visibility, type, group}

IO.puts("Starting generating #{opts[:iterations]} iterations of activities...")

friends_thread = Enum.take(friends, 5)
non_friends_thread = Enum.take(friends, 5)

public_long_thread = fn ->
generate_long_thread("public", user, friends_thread, non_friends_thread, opts)
generate_long_thread("public", users, opts)
end

private_long_thread = fn ->
generate_long_thread("private", user, friends_thread, non_friends_thread, opts)
generate_long_thread("private", users, opts)
end

iterations = opts[:iterations]
@@ -73,10 +77,10 @@ defmodule Pleroma.LoadTesting.Activities do
i when i == iterations - 2 ->
spawn(public_long_thread)
spawn(private_long_thread)
generate_activities(user, friends, non_friends, Enum.shuffle(task_data), opts)
generate_activities(users, Enum.shuffle(task_data), opts)

_ ->
generate_activities(user, friends, non_friends, Enum.shuffle(task_data), opts)
generate_activities(users, Enum.shuffle(task_data), opts)
end
)
end)
@@ -127,16 +131,16 @@ defmodule Pleroma.LoadTesting.Activities do
end)
end

defp generate_long_thread(visibility, user, friends, non_friends, _opts) do
defp generate_long_thread(visibility, users, _opts) do
group =
if visibility == "public",
do: "friends",
else: "user"
do: :friends_local,
else: :user

tasks = get_reply_tasks(visibility, group) |> Stream.cycle() |> Enum.take(50)

{:ok, activity} =
CommonAPI.post(user, %{
CommonAPI.post(users[:user], %{
status: "Start of #{visibility} long thread",
visibility: visibility
})
@@ -150,31 +154,28 @@ defmodule Pleroma.LoadTesting.Activities do
Map.put(state, key, activity)
end)

acc = {activity.id, ["@" <> user.nickname, "reply to long thread"]}
insert_replies_for_long_thread(tasks, visibility, user, friends, non_friends, acc)
acc = {activity.id, ["@" <> users[:user].nickname, "reply to long thread"]}
insert_replies_for_long_thread(tasks, visibility, users, acc)
IO.puts("Generating #{visibility} long thread ended\n")
end

defp insert_replies_for_long_thread(tasks, visibility, user, friends, non_friends, acc) do
defp insert_replies_for_long_thread(tasks, visibility, users, acc) do
Enum.reduce(tasks, acc, fn
"friend", {id, data} ->
friend = Enum.random(friends)
insert_reply(friend, List.delete(data, "@" <> friend.nickname), id, visibility)

"non_friend", {id, data} ->
non_friend = Enum.random(non_friends)
insert_reply(non_friend, List.delete(data, "@" <> non_friend.nickname), id, visibility)

"user", {id, data} ->
:user, {id, data} ->
user = users[:user]
insert_reply(user, List.delete(data, "@" <> user.nickname), id, visibility)

group, {id, data} ->
replier = Enum.random(users[group])
insert_reply(replier, List.delete(data, "@" <> replier.nickname), id, visibility)
end)
end

defp generate_activities(user, friends, non_friends, task_data, opts) do
defp generate_activities(users, task_data, opts) do
Task.async_stream(
task_data,
fn {visibility, type, group} ->
insert_activity(type, visibility, group, user, friends, non_friends, opts)
insert_activity(type, visibility, group, users, opts)
end,
max_concurrency: @max_concurrency,
timeout: 30_000
@@ -182,67 +183,104 @@ defmodule Pleroma.LoadTesting.Activities do
|> Stream.run()
end

defp insert_activity("simple", visibility, group, user, friends, non_friends, _opts) do
{:ok, _activity} =
defp insert_local_activity(visibility, group, users, status) do
{:ok, _} =
group
|> get_actor(user, friends, non_friends)
|> CommonAPI.post(%{status: "Simple status", visibility: visibility})
|> get_actor(users)
|> CommonAPI.post(%{status: status, visibility: visibility})
end

defp insert_activity("emoji", visibility, group, user, friends, non_friends, _opts) do
{:ok, _activity} =
group
|> get_actor(user, friends, non_friends)
|> CommonAPI.post(%{
status: "Simple status with emoji :firefox:",
visibility: visibility
})
defp insert_remote_activity(visibility, group, users, status) do
actor = get_actor(group, users)
{act_data, obj_data} = prepare_activity_data(actor, visibility, users[:user])
{activity_data, object_data} = other_data(actor, status)

activity_data
|> Map.merge(act_data)
|> Map.put("object", Map.merge(object_data, obj_data))
|> Pleroma.Web.ActivityPub.ActivityPub.insert(false)
end

defp insert_activity("mentions", visibility, group, user, friends, non_friends, _opts) do
defp user_mentions(users) do
user_mentions =
get_random_mentions(friends, Enum.random(0..3)) ++
get_random_mentions(non_friends, Enum.random(0..3))
Enum.reduce(
@groups,
[],
fn group, acc ->
acc ++ get_random_mentions(users[group], Enum.random(0..2))
end
)

user_mentions =
if Enum.random([true, false]),
do: ["@" <> user.nickname | user_mentions],
else: user_mentions
if Enum.random([true, false]),
do: ["@" <> users[:user].nickname | user_mentions],
else: user_mentions
end

{:ok, _activity} =
group
|> get_actor(user, friends, non_friends)
|> CommonAPI.post(%{
status: Enum.join(user_mentions, ", ") <> " simple status with mentions",
visibility: visibility
})
defp hell_thread_mentions(users) do
with {:ok, nil} <- Cachex.get(:user_cache, "hell_thread_mentions") do
cached =
@groups
|> Enum.reduce([users[:user]], fn group, acc ->
acc ++ Enum.take(users[group], 5)
end)
|> Enum.map(&"@#{&1.nickname}")
|> Enum.join(", ")

Cachex.put(:user_cache, "hell_thread_mentions", cached)
cached
else
{:ok, cached} -> cached
end
end

defp insert_activity("hell_thread", visibility, group, user, friends, non_friends, _opts) do
mentions =
with {:ok, nil} <- Cachex.get(:user_cache, "hell_thread_mentions") do
cached =
([user | Enum.take(friends, 10)] ++ Enum.take(non_friends, 10))
|> Enum.map(&"@#{&1.nickname}")
|> Enum.join(", ")
defp insert_activity(:simple, visibility, group, users, _opts)
when group in @remote_groups do
insert_remote_activity(visibility, group, users, "Remote status")
end

Cachex.put(:user_cache, "hell_thread_mentions", cached)
cached
else
{:ok, cached} -> cached
end
defp insert_activity(:simple, visibility, group, users, _opts) do
insert_local_activity(visibility, group, users, "Simple status")
end

{:ok, _activity} =
group
|> get_actor(user, friends, non_friends)
|> CommonAPI.post(%{
status: mentions <> " hell thread status",
visibility: visibility
})
defp insert_activity(:emoji, visibility, group, users, _opts)
when group in @remote_groups do
insert_remote_activity(visibility, group, users, "Remote status with emoji :firefox:")
end

defp insert_activity(:emoji, visibility, group, users, _opts) do
insert_local_activity(visibility, group, users, "Simple status with emoji :firefox:")
end

defp insert_activity(:mentions, visibility, group, users, _opts)
when group in @remote_groups do
mentions = user_mentions(users)

status = Enum.join(mentions, ", ") <> " remote status with mentions"

insert_remote_activity(visibility, group, users, status)
end

defp insert_activity(:mentions, visibility, group, users, _opts) do
mentions = user_mentions(users)

status = Enum.join(mentions, ", ") <> " simple status with mentions"
insert_remote_activity(visibility, group, users, status)
end

defp insert_activity(:hell_thread, visibility, group, users, _)
when group in @remote_groups do
mentions = hell_thread_mentions(users)
insert_remote_activity(visibility, group, users, mentions <> " remote hell thread status")
end

defp insert_activity(:hell_thread, visibility, group, users, _opts) do
mentions = hell_thread_mentions(users)

insert_local_activity(visibility, group, users, mentions <> " hell thread status")
end

defp insert_activity("attachment", visibility, group, user, friends, non_friends, _opts) do
actor = get_actor(group, user, friends, non_friends)
defp insert_activity(:attachment, visibility, group, users, _opts) do
actor = get_actor(group, users)

obj_data = %{
"actor" => actor.ap_id,
@@ -268,67 +306,54 @@ defmodule Pleroma.LoadTesting.Activities do
})
end

defp insert_activity("tag", visibility, group, user, friends, non_friends, _opts) do
{:ok, _activity} =
group
|> get_actor(user, friends, non_friends)
|> CommonAPI.post(%{status: "Status with #tag", visibility: visibility})
defp insert_activity(:tag, visibility, group, users, _opts) do
insert_local_activity(visibility, group, users, "Status with #tag")
end

defp insert_activity("like", visibility, group, user, friends, non_friends, opts) do
actor = get_actor(group, user, friends, non_friends)
defp insert_activity(:like, visibility, group, users, opts) do
actor = get_actor(group, users)

with activity_id when not is_nil(activity_id) <- get_random_create_activity_id(),
{:ok, _activity} <- CommonAPI.favorite(actor, activity_id) do
:ok
else
{:error, _} ->
insert_activity("like", visibility, group, user, friends, non_friends, opts)
insert_activity(:like, visibility, group, users, opts)

nil ->
Process.sleep(15)
insert_activity("like", visibility, group, user, friends, non_friends, opts)
insert_activity(:like, visibility, group, users, opts)
end
end

defp insert_activity("reblog", visibility, group, user, friends, non_friends, opts) do
actor = get_actor(group, user, friends, non_friends)
defp insert_activity(:reblog, visibility, group, users, opts) do
actor = get_actor(group, users)

with activity_id when not is_nil(activity_id) <- get_random_create_activity_id(),
{:ok, _activity, _object} <- CommonAPI.repeat(activity_id, actor) do
{:ok, _activity} <- CommonAPI.repeat(activity_id, actor) do
:ok
else
{:error, _} ->
insert_activity("reblog", visibility, group, user, friends, non_friends, opts)
insert_activity(:reblog, visibility, group, users, opts)

nil ->
Process.sleep(15)
insert_activity("reblog", visibility, group, user, friends, non_friends, opts)
insert_activity(:reblog, visibility, group, users, opts)
end
end

defp insert_activity("simple_thread", visibility, group, user, friends, non_friends, _opts)
when visibility in ["public", "unlisted", "private"] do
actor = get_actor(group, user, friends, non_friends)
tasks = get_reply_tasks(visibility, group)

{:ok, activity} = CommonAPI.post(user, %{status: "Simple status", visibility: visibility})

acc = {activity.id, ["@" <> actor.nickname, "reply to status"]}
insert_replies(tasks, visibility, user, friends, non_friends, acc)
end

defp insert_activity("simple_thread", "direct", group, user, friends, non_friends, _opts) do
actor = get_actor(group, user, friends, non_friends)
defp insert_activity(:simple_thread, "direct", group, users, _opts) do
actor = get_actor(group, users)
tasks = get_reply_tasks("direct", group)

list =
case group do
"non_friends" ->
Enum.take(non_friends, 3)
:user ->
group = Enum.random(@friends_groups)
Enum.take(users[group], 3)

_ ->
Enum.take(friends, 3)
Enum.take(users[group], 3)
end

data = Enum.map(list, &("@" <> &1.nickname))
@@ -339,40 +364,30 @@ defmodule Pleroma.LoadTesting.Activities do
visibility: "direct"
})

acc = {activity.id, ["@" <> user.nickname | data] ++ ["reply to status"]}
insert_direct_replies(tasks, user, list, acc)
acc = {activity.id, ["@" <> users[:user].nickname | data] ++ ["reply to status"]}
insert_direct_replies(tasks, users[:user], list, acc)
end

defp insert_activity("remote", _, "user", _, _, _, _), do: :ok

defp insert_activity("remote", visibility, group, user, _friends, _non_friends, opts) do
remote_friends =
Users.get_users(user, limit: opts[:friends_used], local: :external, friends?: true)

remote_non_friends =
Users.get_users(user, limit: opts[:non_friends_used], local: :external, friends?: false)

actor = get_actor(group, user, remote_friends, remote_non_friends)
defp insert_activity(:simple_thread, visibility, group, users, _opts) do
actor = get_actor(group, users)
tasks = get_reply_tasks(visibility, group)

{act_data, obj_data} = prepare_activity_data(actor, visibility, user)
{activity_data, object_data} = other_data(actor)
{:ok, activity} =
CommonAPI.post(users[:user], %{status: "Simple status", visibility: visibility})

activity_data
|> Map.merge(act_data)
|> Map.put("object", Map.merge(object_data, obj_data))
|> Pleroma.Web.ActivityPub.ActivityPub.insert(false)
acc = {activity.id, ["@" <> actor.nickname, "reply to status"]}
insert_replies(tasks, visibility, users, acc)
end

defp get_actor("user", user, _friends, _non_friends), do: user
defp get_actor("friends", _user, friends, _non_friends), do: Enum.random(friends)
defp get_actor("non_friends", _user, _friends, non_friends), do: Enum.random(non_friends)
defp get_actor(:user, %{user: user}), do: user
defp get_actor(group, users), do: Enum.random(users[group])

defp other_data(actor) do
defp other_data(actor, content) do
%{host: host} = URI.parse(actor.ap_id)
datetime = DateTime.utc_now()
context_id = "http://#{host}:4000/contexts/#{UUID.generate()}"
activity_id = "http://#{host}:4000/activities/#{UUID.generate()}"
object_id = "http://#{host}:4000/objects/#{UUID.generate()}"
context_id = "https://#{host}/contexts/#{UUID.generate()}"
activity_id = "https://#{host}/activities/#{UUID.generate()}"
object_id = "https://#{host}/objects/#{UUID.generate()}"

activity_data = %{
"actor" => actor.ap_id,
@@ -389,7 +404,7 @@ defmodule Pleroma.LoadTesting.Activities do
"attributedTo" => actor.ap_id,
"bcc" => [],
"bto" => [],
"content" => "Remote post",
"content" => content,
"context" => context_id,
"conversation" => context_id,
"emoji" => %{},
@@ -475,51 +490,65 @@ defmodule Pleroma.LoadTesting.Activities do
{act_data, obj_data}
end

defp get_reply_tasks("public", "user"), do: ~w(friend non_friend user)
defp get_reply_tasks("public", "friends"), do: ~w(non_friend user friend)
defp get_reply_tasks("public", "non_friends"), do: ~w(user friend non_friend)
defp get_reply_tasks("public", :user) do
[:friends_local, :friends_remote, :non_friends_local, :non_friends_remote, :user]
end

defp get_reply_tasks("public", group) when group in @friends_groups do
[:non_friends_local, :non_friends_remote, :user, :friends_local, :friends_remote]
end

defp get_reply_tasks(visibility, "user") when visibility in ["unlisted", "private"],
do: ~w(friend user friend)
defp get_reply_tasks("public", group) when group in @non_friends_groups do
[:user, :friends_local, :friends_remote, :non_friends_local, :non_friends_remote]
end

defp get_reply_tasks(visibility, "friends") when visibility in ["unlisted", "private"],
do: ~w(user friend user)
defp get_reply_tasks(visibility, :user) when visibility in ["unlisted", "private"] do
[:friends_local, :friends_remote, :user, :friends_local, :friends_remote]
end

defp get_reply_tasks(visibility, "non_friends") when visibility in ["unlisted", "private"],
do: []
defp get_reply_tasks(visibility, group)
when visibility in ["unlisted", "private"] and group in @friends_groups do
[:user, :friends_remote, :friends_local, :user]
end

defp get_reply_tasks("direct", "user"), do: ~w(friend user friend)
defp get_reply_tasks("direct", "friends"), do: ~w(user friend user)
defp get_reply_tasks("direct", "non_friends"), do: ~w(user non_friend user)
defp get_reply_tasks(visibility, group)
when visibility in ["unlisted", "private"] and
group in @non_friends_groups,
do: []

defp insert_replies(tasks, visibility, user, friends, non_friends, acc) do
Enum.reduce(tasks, acc, fn
"friend", {id, data} ->
friend = Enum.random(friends)
insert_reply(friend, data, id, visibility)
defp get_reply_tasks("direct", :user), do: [:friends_local, :user, :friends_remote]

"non_friend", {id, data} ->
non_friend = Enum.random(non_friends)
insert_reply(non_friend, data, id, visibility)
defp get_reply_tasks("direct", group) when group in @friends_groups,
do: [:user, group, :user]

"user", {id, data} ->
insert_reply(user, data, id, visibility)
defp get_reply_tasks("direct", group) when group in @non_friends_groups do
[:user, :non_friends_remote, :user, :non_friends_local]
end

defp insert_replies(tasks, visibility, users, acc) do
Enum.reduce(tasks, acc, fn
:user, {id, data} ->
insert_reply(users[:user], data, id, visibility)

group, {id, data} ->
replier = Enum.random(users[group])
insert_reply(replier, data, id, visibility)
end)
end

defp insert_direct_replies(tasks, user, list, acc) do
Enum.reduce(tasks, acc, fn
group, {id, data} when group in ["friend", "non_friend"] ->
:user, {id, data} ->
{reply_id, _} = insert_reply(user, List.delete(data, "@" <> user.nickname), id, "direct")
{reply_id, data}

_, {id, data} ->
actor = Enum.random(list)

{reply_id, _} =
insert_reply(actor, List.delete(data, "@" <> actor.nickname), id, "direct")

{reply_id, data}

"user", {id, data} ->
{reply_id, _} = insert_reply(user, List.delete(data, "@" <> user.nickname), id, "direct")
{reply_id, data}
end)
end



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

@@ -36,6 +36,7 @@ defmodule Pleroma.LoadTesting.Fetcher do
fetch_home_timeline(user)
fetch_direct_timeline(user)
fetch_public_timeline(user)
fetch_public_timeline(user, :with_blocks)
fetch_public_timeline(user, :local)
fetch_public_timeline(user, :tag)
fetch_notifications(user)
@@ -227,6 +228,58 @@ defmodule Pleroma.LoadTesting.Fetcher do
fetch_public_timeline(opts, "public timeline only media")
end

defp fetch_public_timeline(user, :with_blocks) do
opts = opts_for_public_timeline(user)

remote_non_friends = Agent.get(:non_friends_remote, & &1)

Benchee.run(%{
"public timeline without blocks" => fn ->
ActivityPub.fetch_public_activities(opts)
end
})

Enum.each(remote_non_friends, fn non_friend ->
{:ok, _} = User.block(user, non_friend)
end)

user = User.get_by_id(user.id)

opts = Map.put(opts, "blocking_user", user)

Benchee.run(
%{
"public timeline with user block" => fn ->
ActivityPub.fetch_public_activities(opts)
end
},
)

domains =
Enum.reduce(remote_non_friends, [], fn non_friend, domains ->
{:ok, _user} = User.unblock(user, non_friend)
%{host: host} = URI.parse(non_friend.ap_id)
[host | domains]
end)

domains = Enum.uniq(domains)

Enum.each(domains, fn domain ->
{:ok, _} = User.block_domain(user, domain)
end)

user = User.get_by_id(user.id)
opts = Map.put(opts, "blocking_user", user)

Benchee.run(
%{
"public timeline with domain block" => fn opts ->
ActivityPub.fetch_public_activities(opts)
end
}
)
end

defp fetch_public_timeline(opts, title) when is_binary(title) do
first_page_last = ActivityPub.fetch_public_activities(opts) |> List.last()



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

@@ -27,7 +27,7 @@ defmodule Pleroma.LoadTesting.Users do

make_friends(main_user, opts[:friends])

Repo.get(User, main_user.id)
User.get_by_id(main_user.id)
end

def generate_users(max) do
@@ -166,4 +166,24 @@ defmodule Pleroma.LoadTesting.Users do
)
|> Stream.run()
end

@spec prepare_users(User.t(), keyword()) :: map()
def prepare_users(user, opts) do
friends_limit = opts[:friends_used]
non_friends_limit = opts[:non_friends_used]

%{
user: user,
friends_local: fetch_users(user, friends_limit, :local, true),
friends_remote: fetch_users(user, friends_limit, :external, true),
non_friends_local: fetch_users(user, non_friends_limit, :local, false),
non_friends_remote: fetch_users(user, non_friends_limit, :external, false)
}
end

defp fetch_users(user, limit, local, friends?) do
user
|> get_users(limit: limit, local: local, friends?: friends?)
|> Enum.shuffle()
end
end

+ 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

alias Pleroma.Repo
alias Pleroma.Web.MastodonAPI.TimelineController

def run(_args) do
Mix.Pleroma.start_pleroma()
@@ -37,7 +36,7 @@ defmodule Mix.Tasks.Pleroma.Benchmarks.Tags do
Benchee.run(
%{
"Hashtag fetching, any" => fn tags ->
TimelineController.hashtag_fetching(
hashtag_fetching(
%{
"any" => tags
},
@@ -47,7 +46,7 @@ defmodule Mix.Tasks.Pleroma.Benchmarks.Tags do
end,
# Will always return zero results because no overlapping hashtags are generated.
"Hashtag fetching, all" => fn tags ->
TimelineController.hashtag_fetching(
hashtag_fetching(
%{
"all" => tags
},
@@ -67,7 +66,7 @@ defmodule Mix.Tasks.Pleroma.Benchmarks.Tags do
Benchee.run(
%{
"Hashtag fetching" => fn tag ->
TimelineController.hashtag_fetching(
hashtag_fetching(
%{
"tag" => tag
},
@@ -80,4 +79,35 @@ defmodule Mix.Tasks.Pleroma.Benchmarks.Tags do
time: 5
)
end

defp hashtag_fetching(params, user, local_only) do
tags =
[params["tag"], params["any"]]
|> List.flatten()
|> Enum.uniq()
|> Enum.filter(& &1)
|> Enum.map(&String.downcase(&1))

tag_all =
params
|> Map.get("all", [])
|> Enum.map(&String.downcase(&1))

tag_reject =
params
|> Map.get("none", [])
|> Enum.map(&String.downcase(&1))

_activities =
params
|> Map.put("type", "Create")
|> Map.put("local_only", local_only)
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)
|> Map.put("tag", tags)
|> Map.put("tag_all", tag_all)
|> Map.put("tag_reject", tag_reject)
|> Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities()
end
end

+ 3
- 2
config/config.exs View File

@@ -171,7 +171,8 @@ config :mime, :types, %{
"application/ld+json" => ["activity+json"]
}

config :tesla, adapter: Tesla.Adapter.Gun
config :tesla, adapter: Tesla.Adapter.Hackney

# Configures http settings, upstream proxy etc.
config :pleroma, :http,
proxy_url: nil,
@@ -183,7 +184,7 @@ config :pleroma, :instance,
name: "Pleroma",
email: "example@example.com",
notify_email: "noreply@example.com",
description: "A Pleroma instance, an alternative fediverse server",
description: "Pleroma: An efficient and flexible fediverse server",
background_image: "/images/city.jpg",
limit: 5_000,
chat_limit: 5_000,


+ 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
{
"totalReports" : 1,
"total" : 1,
"reports": [
{
"account": {
@@ -768,7 +768,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- 400 Bad Request `"Invalid parameters"` when `status` is missing
- On success: `204`, empty response

## `POST /api/pleroma/admin/reports/:report_id/notes/:id`
## `DELETE /api/pleroma/admin/reports/:report_id/notes/:id`

### Delete report note



+ 9
- 5
docs/API/differences_in_mastoapi_responses.md View File

@@ -6,10 +6,6 @@ A Pleroma instance can be identified by "<Mastodon version> (compatible; Pleroma

Pleroma uses 128-bit ids as opposed to Mastodon's 64 bits. However just like Mastodon's ids they are lexically sortable strings

## Attachment cap

Some apps operate under the assumption that no more than 4 attachments can be returned or uploaded. Pleroma however does not enforce any limits on attachment count neither when returning the status object nor when posting.

## Timelines

Adding the parameter `with_muted=true` to the timeline queries will also return activities by muted (not by blocked!) users.
@@ -32,12 +28,20 @@ Has these additional fields under the `pleroma` object:
- `thread_muted`: true if the thread the post belongs to is muted
- `emoji_reactions`: A list with emoji / reaction maps. The format is `{name: "☕", count: 1, me: true}`. Contains no information about the reacting users, for that use the `/statuses/:id/reactions` endpoint.

## Attachments
## Media Attachments

Has these additional fields under the `pleroma` object:

- `mime_type`: mime type of the attachment.

### Attachment cap

Some apps operate under the assumption that no more than 4 attachments can be returned or uploaded. Pleroma however does not enforce any limits on attachment count neither when returning the status object nor when posting.

### Limitations

Pleroma does not process remote images and therefore cannot include fields such as `meta` and `blurhash`. It does not support focal points or aspect ratios. The frontend is expected to handle it.

## Accounts

The `id` parameter can also be the `nickname` of the user. This only works in these endpoints, not the deeper nested ones for following etc.


+ 29
- 0
docs/administration/CLI_tasks/database.md View File

@@ -69,3 +69,32 @@ mix pleroma.database update_users_following_followers_counts
```sh tab="From Source"
mix pleroma.database fix_likes_collections
```

## Vacuum the database

### Analyze

Running an `analyze` vacuum job can improve performance by updating statistics used by the query planner. **It is safe to cancel this.**

```sh tab="OTP"
./bin/pleroma_ctl database vacuum analyze
```

```sh tab="From Source"
mix pleroma.database vacuum analyze
```

### Full

Running a `full` vacuum job rebuilds your entire database by reading all of the data and rewriting it into smaller
and more compact files with an optimized layout. This process will take a long time and use additional disk space as
it builds the files side-by-side the existing database files. It can make your database faster and use less disk space,
but should only be run if necessary. **It is safe to cancel this.**

```sh tab="OTP"
./bin/pleroma_ctl database vacuum full
```

```sh tab="From Source"
mix pleroma.database vacuum full
```

+ 6
- 0
docs/clients.md View File

@@ -42,6 +42,12 @@ Feel free to contact us to be added to this list!
- Platforms: SailfishOS
- Features: No Streaming

### Husky
- Source code: <https://git.mentality.rip/FWGS/Husky>
- Contact: [@Husky@enigmatic.observer](https://enigmatic.observer/users/Husky)
- Platforms: Android
- Features: No Streaming, Emoji Reactions, Text Formatting, FE Stickers

### Nekonium
- Homepage: [F-Droid Repository](https://repo.gdgd.jp.net/), [Google Play](https://play.google.com/store/apps/details?id=com.apps.nekonium), [Amazon](https://www.amazon.co.jp/dp/B076FXPRBC/)
- Source: <https://gogs.gdgd.jp.net/lin/nekonium>


+ 9
- 14
lib/mix/tasks/pleroma/database.ex View File

@@ -4,6 +4,7 @@

defmodule Mix.Tasks.Pleroma.Database do
alias Pleroma.Conversation
alias Pleroma.Maintenance
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
@@ -34,13 +35,7 @@ defmodule Mix.Tasks.Pleroma.Database do
)

if Keyword.get(options, :vacuum) do
Logger.info("Runnning VACUUM FULL")

Repo.query!(
"vacuum full;",
[],
timeout: :infinity
)
Maintenance.vacuum("full")
end
end

@@ -94,13 +89,7 @@ defmodule Mix.Tasks.Pleroma.Database do
|> Repo.delete_all(timeout: :infinity)

if Keyword.get(options, :vacuum) do
Logger.info("Runnning VACUUM FULL")

Repo.query!(
"vacuum full;",
[],
timeout: :infinity
)
Maintenance.vacuum("full")
end
end

@@ -135,4 +124,10 @@ defmodule Mix.Tasks.Pleroma.Database do
end)
|> Stream.run()
end

def run(["vacuum", args]) do
start_pleroma()

Maintenance.vacuum(args)
end
end

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

@@ -24,6 +24,6 @@ defmodule Pleroma.Constants do

const(static_only_files,
do:
~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc)
~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc embed.js embed.css)
)
end

+ 1
- 16
lib/pleroma/http/adapter_helper/hackney.ex View File

@@ -22,22 +22,7 @@ defmodule Pleroma.HTTP.AdapterHelper.Hackney do
|> Pleroma.HTTP.AdapterHelper.maybe_add_proxy(proxy)
end

defp add_scheme_opts(opts, %URI{scheme: "http"}), do: opts

defp add_scheme_opts(opts, %URI{scheme: "https", host: host}) do
ssl_opts = [
ssl_options: [
# Workaround for remote server certificate chain issues
partial_chain: &:hackney_connect.partial_chain/1,

# We don't support TLS v1.3 yet
versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"],
server_name_indication: to_charlist(host)
]
]

Keyword.merge(opts, ssl_opts)
end
defp add_scheme_opts(opts, _), do: opts

def after_request(_), do: :ok
end

+ 37
- 0
lib/pleroma/maintenance.ex View File

@@ -0,0 +1,37 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Maintenance do
alias Pleroma.Repo
require Logger

def vacuum(args) do
case args do
"analyze" ->
Logger.info("Runnning VACUUM ANALYZE.")

Repo.query!(
"vacuum analyze;",
[],
timeout: :infinity
)

"full" ->
Logger.info("Runnning VACUUM FULL.")

Logger.warn(
"Re-packing your entire database may take a while and will consume extra disk space during the process."
)

Repo.query!(
"vacuum full;",
[],
timeout: :infinity
)

_ ->
Logger.error("Error: invalid vacuum argument.")
end
end
end

+ 62
- 20
lib/pleroma/plugs/http_security_plug.ex View File

@@ -31,7 +31,7 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do
{"x-content-type-options", "nosniff"},
{"referrer-policy", referrer_policy},
{"x-download-options", "noopen"},
{"content-security-policy", csp_string() <> ";"}
{"content-security-policy", csp_string()}
]

if report_uri do
@@ -43,23 +43,46 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do
]
}

headers ++ [{"reply-to", Jason.encode!(report_group)}]
[{"reply-to", Jason.encode!(report_group)} | headers]
else
headers
end
end

static_csp_rules = [
"default-src 'none'",
"base-uri 'self'",
"frame-ancestors 'none'",
"style-src 'self' 'unsafe-inline'",
"font-src 'self'",
"manifest-src 'self'"
]

@csp_start [Enum.join(static_csp_rules, ";") <> ";"]

defp csp_string do
scheme = Config.get([Pleroma.Web.Endpoint, :url])[:scheme]
static_url = Pleroma.Web.Endpoint.static_url()
websocket_url = Pleroma.Web.Endpoint.websocket_url()
report_uri = Config.get([:http_security, :report_uri])

connect_src = "connect-src 'self' #{static_url} #{websocket_url}"
img_src = "img-src 'self' data: blob:"
media_src = "media-src 'self'"

{img_src, media_src} =
if Config.get([:media_proxy, :enabled]) &&
!Config.get([:media_proxy, :proxy_opts, :redirect_on_failure]) do
sources = get_proxy_and_attachment_sources()
{[img_src, sources], [media_src, sources]}
else
{[img_src, " https:"], [media_src, " https:"]}
end

connect_src = ["connect-src 'self' blob: ", static_url, ?\s, websocket_url]

connect_src =
if Pleroma.Config.get(:env) == :dev do
connect_src <> " http://localhost:3035/"
[connect_src, " http://localhost:3035/"]
else
connect_src
end
@@ -71,27 +94,46 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do
"script-src 'self'"
end

main_part = [
"default-src 'none'",
"base-uri 'self'",
"frame-ancestors 'none'",
"img-src 'self' data: blob: https:",
"media-src 'self' https:",
"style-src 'self' 'unsafe-inline'",
"font-src 'self'",
"manifest-src 'self'",
connect_src,
script_src
]
report = if report_uri, do: ["report-uri ", report_uri, ";report-to csp-endpoint"]
insecure = if scheme == "https", do: "upgrade-insecure-requests"

@csp_start
|> add_csp_param(img_src)
|> add_csp_param(media_src)
|> add_csp_param(connect_src)
|> add_csp_param(script_src)
|> add_csp_param(insecure)
|> add_csp_param(report)
|> :erlang.iolist_to_binary()
end

defp get_proxy_and_attachment_sources do
media_proxy_whitelist =
Enum.reduce(Config.get([:media_proxy, :whitelist]), [], fn host, acc ->
add_source(acc, host)
end)

report = if report_uri, do: ["report-uri #{report_uri}; report-to csp-endpoint"], else: []
upload_base_url =
if Config.get([Pleroma.Upload, :base_url]),
do: URI.parse(Config.get([Pleroma.Upload, :base_url])).host

insecure = if scheme == "https", do: ["upgrade-insecure-requests"], else: []
s3_endpoint =
if Config.get([Pleroma.Upload, :uploader]) == Pleroma.Uploaders.S3,
do: URI.parse(Config.get([Pleroma.Uploaders.S3, :public_endpoint])).host

(main_part ++ report ++ insecure)
|> Enum.join("; ")
[]
|> add_source(upload_base_url)
|> add_source(s3_endpoint)
|> add_source(media_proxy_whitelist)
end

defp add_source(iodata, nil), do: iodata
defp add_source(iodata, source), do: [[?\s, source] | iodata]

defp add_csp_param(csp_iodata, nil), do: csp_iodata

defp add_csp_param(csp_iodata, param), do: [[param, ?;] | csp_iodata]

def warn_if_disabled do
unless Config.get([:http_security, :enabled]) do
Logger.warn("


+ 3
- 3
lib/pleroma/user/query.ex View File

@@ -45,7 +45,7 @@ defmodule Pleroma.User.Query do
is_admin: boolean(),
is_moderator: boolean(),
super_users: boolean(),
exclude_service_users: boolean(),
invisible: boolean(),
followers: User.t(),
friends: User.t(),
recipients_from_activity: [String.t()],
@@ -89,8 +89,8 @@ defmodule Pleroma.User.Query do
where(query, [u], ilike(field(u, ^key), ^"%#{value}%"))
end

defp compose_query({:exclude_service_users, _}, query) do
where(query, [u], not like(u.ap_id, "%/relay") and not like(u.ap_id, "%/internal/fetch"))
defp compose_query({:invisible, bool}, query) when is_boolean(bool) do
where(query, [u], u.invisible == ^bool)
end

defp compose_query({key, value}, query)


+ 18
- 0
lib/pleroma/web/activity_pub/activity_pub.ex View File

@@ -938,6 +938,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
where: fragment("not (? && ?)", activity.recipients, ^blocked_ap_ids),
where:
fragment(
"recipients_contain_blocked_domains(?, ?) = false",
activity.recipients,
^domain_blocks
),
where:
fragment(
"not (?->>'type' = 'Announce' and ?->'to' \\?| ?)",
activity.data,
activity.data,
@@ -1030,6 +1036,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end

defp exclude_invisible_actors(query, %{"invisible_actors" => true}), do: query

defp exclude_invisible_actors(query, _opts) do
invisible_ap_ids =
User.Query.build(%{invisible: true, select: [:ap_id]})
|> Repo.all()
|> Enum.map(fn %{ap_id: ap_id} -> ap_id end)

from([activity] in query, where: activity.actor not in ^invisible_ap_ids)
end

defp exclude_id(query, %{"exclude_id" => id}) when is_binary(id) do
from(activity in query, where: activity.id != ^id)
end
@@ -1135,6 +1152,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> restrict_instance(opts)
|> Activity.restrict_deactivated_users()
|> exclude_poll_votes(opts)
|> exclude_invisible_actors(opts)
|> exclude_visibility(opts)
end



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

@@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do

alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility

@@ -85,15 +86,20 @@ defmodule Pleroma.Web.ActivityPub.Builder do
end
end

@spec announce(User.t(), Object.t(), keyword()) :: {:ok, map(), keyword()}
def announce(actor, object, options \\ []) do
public? = Keyword.get(options, :public, false)
to = [actor.follower_address, object.data["actor"]]

to =
if public? do
[Pleroma.Constants.as_public() | to]
else
to
cond do
actor.ap_id == Relay.relay_ap_id() ->
[actor.follower_address]

public? ->
[actor.follower_address, object.data["actor"], Pleroma.Constants.as_public()]

true ->
[actor.follower_address, object.data["actor"]]
end

{:ok,


+ 5
- 2
lib/pleroma/web/activity_pub/side_effects.ex View File

@@ -33,11 +33,14 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
# - Stream out the announce
def handle(%{data: %{"type" => "Announce"}} = object, meta) do
announced_object = Object.get_by_ap_id(object.data["object"])
user = User.get_cached_by_ap_id(object.data["actor"])

Utils.add_announce_to_object(object, announced_object)

Notification.create_notifications(object)
ActivityPub.stream_out(object)
if !User.is_internal_user?(user) do
Notification.create_notifications(object)
ActivityPub.stream_out(object)
end

{:ok, object, meta}
end


+ 1
- 0
lib/pleroma/web/activity_pub/utils.ex View File

@@ -740,6 +740,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
def get_reports(params, page, page_size) do
params =
params
|> Map.new(fn {key, value} -> {to_string(key), value} end)
|> Map.put("type", "Flag")
|> Map.put("skip_preload", true)
|> Map.put("preload_report_notes", true)


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

@@ -7,31 +7,21 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do

import Pleroma.Web.ControllerHelper, only: [json_response: 3]

alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.MFA
alias Pleroma.ModerationLog
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.ReportNote
alias Pleroma.Stats
alias Pleroma.User
alias Pleroma.UserInviteToken
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.AdminAPI
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.AdminAPI.ModerationLogView
alias Pleroma.Web.AdminAPI.Report
alias Pleroma.Web.AdminAPI.ReportView
alias Pleroma.Web.AdminAPI.Search
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Endpoint
alias Pleroma.Web.MastodonAPI
alias Pleroma.Web.MastodonAPI.AppView
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.Router

require Logger
@@ -66,14 +56,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
]
)

plug(OAuthScopesPlug, %{scopes: ["read:invites"], admin: true} when action == :invites)

plug(
OAuthScopesPlug,
%{scopes: ["write:invites"], admin: true}
when action in [:create_invite_token, :revoke_invite, :email_invite]
)

plug(
OAuthScopesPlug,
%{scopes: ["write:follows"], admin: true}
@@ -82,18 +64,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do

plug(
OAuthScopesPlug,
%{scopes: ["read:reports"], admin: true}
when action in [:list_reports, :report_show]
)

plug(
OAuthScopesPlug,
%{scopes: ["write:reports"], admin: true}
when action in [:reports_update, :report_notes_create, :report_notes_delete]
)

plug(
OAuthScopesPlug,
%{scopes: ["read:statuses"], admin: true}
when action in [:list_user_statuses, :list_instance_statuses]
)
@@ -116,10 +86,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
:restart,
:resend_confirmation_email,
:confirm_email,
:oauth_app_create,
:oauth_app_list,
:oauth_app_update,
:oauth_app_delete,
:reload_emoji
]
)
@@ -288,7 +254,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
})

conn
|> put_view(MastodonAPI.StatusView)
|> put_view(AdminAPI.StatusView)
|> render("index.json", %{activities: activities, as: :activity})
else
_ -> {:error, :not_found}
@@ -569,69 +535,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
end
end

@doc "Sends registration invite via email"
def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do
with {_, false} <- {:registrations_open, Config.get([:instance, :registrations_open])},
{_, true} <- {:invites_enabled, Config.get([:instance, :invites_enabled])},
{:ok, invite_token} <- UserInviteToken.create_invite(),
email <-
Pleroma.Emails.UserEmail.user_invitation_email(
user,
invite_token,
email,
params["name"]
),
{:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do
json_response(conn, :no_content, "")
else
{:registrations_open, _} ->
{:error, "To send invites you need to set the `registrations_open` option to false."}

{:invites_enabled, _} ->
{:error, "To send invites you need to set the `invites_enabled` option to true."}
end
end

@doc "Create an account registration invite token"
def create_invite_token(conn, params) do
opts = %{}

opts =
if params["max_use"],
do: Map.put(opts, :max_use, params["max_use"]),
else: opts

opts =
if params["expires_at"],
do: Map.put(opts, :expires_at, params["expires_at"]),
else: opts

{:ok, invite} = UserInviteToken.create_invite(opts)

json(conn, AccountView.render("invite.json", %{invite: invite}))
end

@doc "Get list of created invites"
def invites(conn, _params) do
invites = UserInviteToken.list_invites()

conn
|> put_view(AccountView)
|> render("invites.json", %{invites: invites})
end

@doc "Revokes invite by token"
def revoke_invite(conn, %{"token" => token}) do
with {:ok, invite} <- UserInviteToken.find_by_token(token),
{:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do
conn
|> put_view(AccountView)
|> render("invite.json", %{invite: updated_invite})
else
nil -> {:error, :not_found}
end
end

@doc "Get a password reset token (base64 string) for given nickname"
def get_password_reset(conn, %{"nickname" => nickname}) do
(%User{local: true} = user) = User.get_cached_by_nickname(nickname)
@@ -718,85 +621,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
end
end

def list_reports(conn, params) do
{page, page_size} = page_params(params)

reports = Utils.get_reports(params, page, page_size)

conn
|> put_view(ReportView)
|> render("index.json", %{reports: reports})
end

def report_show(conn, %{"id" => id}) do
with %Activity{} = report <- Activity.get_by_id(id) do
conn
|> put_view(ReportView)
|> render("show.json", Report.extract_report_info(report))
else
_ -> {:error, :not_found}
end
end

def reports_update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) do
result =
reports
|> Enum.map(fn report ->
with {:ok, activity} <- CommonAPI.update_report_state(report["id"], report["state"]) do
ModerationLog.insert_log(%{
action: "report_update",
actor: admin,
subject: activity
})

activity
else
{:error, message} -> %{id: report["id"], error: message}
end
end)

case Enum.any?(result, &Map.has_key?(&1, :error)) do
true -> json_response(conn, :bad_request, result)
false -> json_response(conn, :no_content, "")
end
end

def report_notes_create(%{assigns: %{user: user}} = conn, %{
"id" => report_id,
"content" => content
}) do
with {:ok, _} <- ReportNote.create(user.id, report_id, content) do
ModerationLog.insert_log(%{
action: "report_note",
actor: user,
subject: Activity.get_by_id(report_id),
text: content
})

json_response(conn, :no_content, "")
else
_ -> json_response(conn, :bad_request, "")
end
end

def report_notes_delete(%{assigns: %{user: user}} = conn, %{
"id" => note_id,
"report_id" => report_id
}) do
with {:ok, note} <- ReportNote.destroy(note_id) do
ModerationLog.insert_log(%{
action: "report_note_delete",
actor: user,
subject: Activity.get_by_id(report_id),
text: note.content
})

json_response(conn, :no_content, "")
else
_ -> json_response(conn, :bad_request, "")
end
end

def list_log(conn, params) do
{page, page_size} = page_params(params)

@@ -869,83 +693,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
conn |> json("")
end

def oauth_app_create(conn, params) do
params =
if params["name"] do
Map.put(params, "client_name", params["name"])
else
params
end

result =
case App.create(params) do
{:ok, app} ->
AppView.render("show.json", %{app: app, admin: true})

{:error, changeset} ->
App.errors(changeset)
end

json(conn, result)
end

def oauth_app_update(conn, params) do
params =
if params["name"] do
Map.put(params, "client_name", params["name"])
else
params
end

with {:ok, app} <- App.update(params) do
json(conn, AppView.render("show.json", %{app: app, admin: true}))
else
{:error, changeset} ->
json(conn, App.errors(changeset))

nil ->
json_response(conn, :bad_request, "")
end
end

def oauth_app_list(conn, params) do
{page, page_size} = page_params(params)

search_params = %{
client_name: params["name"],
client_id: params["client_id"],
page: page,
page_size: page_size
}

search_params =
if Map.has_key?(params, "trusted") do
Map.put(search_params, :trusted, params["trusted"])
else
search_params
end

with {:ok, apps, count} <- App.search(search_params) do
json(
conn,
AppView.render("index.json",
apps: apps,
count: count,
page_size: page_size,
admin: true
)
)
end
end

def oauth_app_delete(conn, params) do
with {:ok, _app} <- App.destroy(params["id"]) do
json_response(conn, :no_content, "")
else
_ -> json_response(conn, :bad_request, "")
end
end

def stats(conn, _) do
count = Stats.get_status_visibility_count()



+ 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

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

@@ -0,0 +1,87 @@
# 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 =
if params[:name] do
Map.put(params, :client_name, params[:name])
else
params
end

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 =
if params[:name] do
Map.put(params, :client_name, params.name)
else
params
end

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
with %Activity{} = activity <- Activity.get_by_id(id) do
conn
|> put_view(MastodonAPI.StatusView)
|> render("show.json", %{activity: activity})
render(conn, "show.json", %{activity: activity})
else
nil -> {:error, :not_found}
end


+ 1
- 2
lib/pleroma/web/admin_api/search.ex View File

@@ -21,7 +21,7 @@ defmodule Pleroma.Web.AdminAPI.Search do
query =
params
|> Map.drop([:page, :page_size])
|> Map.put(:exclude_service_users, true)
|> Map.put(:invisible, false)
|> User.Query.build()
|> order_by([u], u.nickname)

@@ -31,7 +31,6 @@ defmodule Pleroma.Web.AdminAPI.Search do
count = Repo.aggregate(query, :count, :id)

results = Repo.all(paginated_query)

{:ok, results, count}
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

def render("invite.json", %{invite: invite}) do
%{
"id" => invite.id,
"token" => invite.token,
"used" => invite.used,
"expires_at" => invite.expires_at,
"uses" => invite.uses,
"max_use" => invite.max_use,
"invite_type" => invite.invite_type
}
end

def render("invites.json", %{invites: invites}) do
%{
invites: render_many(invites, AccountView, "invite.json", as: :invite)
}
end

def render("created.json", %{user: user}) do
%{
type: "success",


+ 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

+ 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()],
security: [%{"oAuth" => ["read:statuses"]}],
responses: %{
200 => Operation.response("Status", "application/json", Status),
200 => Operation.response("Status", "application/json", status()),
404 => Operation.response("Not Found", "application/json", ApiError)
}
}
@@ -123,7 +123,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.StatusOperation do
}
end

defp admin_account do
def admin_account do
%Schema{
type: :object,
properties: %{


+ 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_image" => "/static/image.png",
"banner_upload_limit" => 4_000_000,
"description" => "A Pleroma instance, an alternative fediverse server",
"description" => "Pleroma: An efficient and flexible fediverse server",
"email" => "lain@lain.com",
"languages" => ["en"],
"max_toot_chars" => 5000,


+ 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

+ 20
- 3
lib/pleroma/web/mastodon_api/controllers/account_controller.ex View File

@@ -139,9 +139,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end

@doc "PATCH /api/v1/accounts/update_credentials"
def update_credentials(%{assigns: %{user: original_user}, body_params: params} = conn, _params) do
user = original_user

def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _params) do
params =
params
|> Enum.filter(fn {_, value} -> not is_nil(value) end)
@@ -183,12 +181,31 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
changeset = User.update_changeset(user, user_params)

with {:ok, user} <- User.update_and_set_cache(changeset) do
user
|> build_update_activity_params()
|> ActivityPub.update()

render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
else
_e -> render_error(conn, :forbidden, "Invalid request")
end
end

# Hotfix, handling will be redone with the pipeline
defp build_update_activity_params(user) do
object =
Pleroma.Web.ActivityPub.UserView.render("user.json", user: user)
|> Map.delete("@context")

%{
local: true,
to: [user.follower_address],
cc: [],
object: object,
actor: user.ap_id
}
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),


+ 17
- 0
lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex View File

@@ -21,6 +21,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationController do

@doc "GET /api/v1/conversations"
def index(%{assigns: %{user: user}} = conn, params) do
params = stringify_pagination_params(params)
participations = Participation.for_user_with_last_activity_id(user, params)

conn
@@ -36,4 +37,20 @@ defmodule Pleroma.Web.MastodonAPI.ConversationController do
render(conn, "participation.json", participation: participation, for: user)
end
end

defp stringify_pagination_params(params) do
atom_keys =
Pleroma.Pagination.page_keys()
|> Enum.map(&String.to_atom(&1))

str_keys =
params
|> Map.take(atom_keys)
|> Enum.map(fn {key, value} -> {to_string(key), value} end)
|> Enum.into(%{})

params
|> Map.delete(atom_keys)
|> Map.merge(str_keys)
end
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
|> prepare_tags()
|> Enum.map(fn tag ->
tag = String.trim_leading(tag, "#")
%{name: tag, url: tags_path <> tag}
end)
end

defp resource_search(:v1, "hashtags", query, _options) do
query
|> prepare_tags()
|> Enum.map(fn tag -> String.trim_leading(tag, "#") end)
prepare_tags(query)
end

defp prepare_tags(query) do
query
|> String.split()
|> Enum.uniq()
|> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
defp prepare_tags(query, add_joined_tag \\ true) do
tags =
query
|> String.split(~r/[^#\w]+/u, trim: true)
|> Enum.uniq_by(&String.downcase/1)

explicit_tags = Enum.filter(tags, fn tag -> String.starts_with?(tag, "#") end)

tags =
if Enum.any?(explicit_tags) do
explicit_tags
else
tags
end

tags = Enum.map(tags, fn tag -> String.trim_leading(tag, "#") end)

if Enum.empty?(explicit_tags) && add_joined_tag do
tags
|> Kernel.++([joined_tag(tags)])
|> Enum.uniq_by(&String.downcase/1)
else
tags
end
end

defp joined_tag(tags) do
tags
|> Enum.map(fn tag -> String.capitalize(tag) end)
|> Enum.join()
end

defp with_fallback(f, fallback \\ []) do


+ 1
- 1
lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex View File

@@ -111,7 +111,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
else
activities =
params
|> Map.put("type", ["Create", "Announce"])
|> Map.put("type", ["Create"])
|> Map.put("local_only", local_only)
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)


+ 7
- 5
lib/pleroma/web/mastodon_api/views/account_view.ex View File

@@ -182,12 +182,14 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
bot = user.actor_type in ["Application", "Service"]

emojis =
Enum.map(user.emoji, fn {shortcode, url} ->
Enum.map(user.emoji, fn {shortcode, raw_url} ->
url = MediaProxy.url(raw_url)

%{
"shortcode" => shortcode,
"url" => url,
"static_url" => url,
"visible_in_picker" => false
shortcode: shortcode,
url: url,
static_url: url,
visible_in_picker: false
}
end)



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

@@ -25,12 +25,12 @@ defmodule Pleroma.Web.OAuth.App do
timestamps()
end

@spec changeset(App.t(), map()) :: Ecto.Changeset.t()
@spec changeset(t(), map()) :: Ecto.Changeset.t()
def changeset(struct, params) do
cast(struct, params, [:client_name, :redirect_uris, :scopes, :website, :trusted])
end

@spec register_changeset(App.t(), map()) :: Ecto.Changeset.t()
@spec register_changeset(t(), map()) :: Ecto.Changeset.t()
def register_changeset(struct, params \\ %{}) do
changeset =
struct
@@ -52,18 +52,19 @@ defmodule Pleroma.Web.OAuth.App do
end
end

@spec create(map()) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
@spec create(map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
def create(params) do
with changeset <- __MODULE__.register_changeset(%__MODULE__{}, params) do
Repo.insert(changeset)
end
%__MODULE__{}
|> register_changeset(params)
|> Repo.insert()
end

@spec update(map()) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
def update(params) do
with %__MODULE__{} = app <- Repo.get(__MODULE__, params["id"]),
changeset <- changeset(app, params) do
Repo.update(changeset)
@spec update(pos_integer(), map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
def update(id, params) do
with %__MODULE__{} = app <- Repo.get(__MODULE__, id) do
app
|> changeset(params)
|> Repo.update()
end
end

@@ -71,7 +72,7 @@ defmodule Pleroma.Web.OAuth.App do
Gets app by attrs or create new with attrs.
And updates the scopes if need.
"""
@spec get_or_make(map(), list(String.t())) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
@spec get_or_make(map(), list(String.t())) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
def get_or_make(attrs, scopes) do
with %__MODULE__{} = app <- Repo.get_by(__MODULE__, attrs) do
update_scopes(app, scopes)
@@ -92,7 +93,7 @@ defmodule Pleroma.Web.OAuth.App do
|> Repo.update()
end

@spec search(map()) :: {:ok, [App.t()], non_neg_integer()}
@spec search(map()) :: {:ok, [t()], non_neg_integer()}
def search(params) do
query = from(a in __MODULE__)

@@ -128,7 +129,7 @@ defmodule Pleroma.Web.OAuth.App do
{:ok, Repo.all(query), count}
end

@spec destroy(pos_integer()) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
@spec destroy(pos_integer()) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
def destroy(id) do
with %__MODULE__{} = app <- Repo.get(__MODULE__, id) do
Repo.delete(app)


+ 15
- 13
lib/pleroma/web/router.ex View File

@@ -164,10 +164,10 @@ defmodule Pleroma.Web.Router do
post("/relay", AdminAPIController, :relay_follow)
delete("/relay", AdminAPIController, :relay_unfollow)

post("/users/invite_token", AdminAPIController, :create_invite_token)
get("/users/invites", AdminAPIController, :invites)
post("/users/revoke_invite", AdminAPIController, :revoke_invite)
post("/users/email_invite", AdminAPIController, :email_invite)
post("/users/invite_token", InviteController, :create)
get("/users/invites", InviteController, :index)
post("/users/revoke_invite", InviteController, :revoke)
post("/users/email_invite", InviteController, :email)

get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset)
patch("/users/force_password_reset", AdminAPIController, :force_password_reset)
@@ -183,11 +183,11 @@ defmodule Pleroma.Web.Router do
patch("/users/confirm_email", AdminAPIController, :confirm_email)
patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email)

get("/reports", AdminAPIController, :list_reports)
get("/reports/:id", AdminAPIController, :report_show)
patch("/reports", AdminAPIController, :reports_update)
post("/reports/:id/notes", AdminAPIController, :report_notes_create)
delete("/reports/:report_id/notes/:id", AdminAPIController, :report_notes_delete)
get("/reports", ReportController, :index)
get("/reports/:id", ReportController, :show)
patch("/reports", ReportController, :update)
post("/reports/:id/notes", ReportController, :notes_create)
delete("/reports/:report_id/notes/:id", ReportController, :notes_delete)

get("/statuses/:id", StatusController, :show)
put("/statuses/:id", StatusController, :update)
@@ -205,10 +205,10 @@ defmodule Pleroma.Web.Router do
post("/reload_emoji", AdminAPIController, :reload_emoji)
get("/stats", AdminAPIController, :stats)

get("/oauth_app", AdminAPIController, :oauth_app_list)
post("/oauth_app", AdminAPIController, :oauth_app_create)
patch("/oauth_app/:id", AdminAPIController, :oauth_app_update)
delete("/oauth_app/:id", AdminAPIController, :oauth_app_delete)
get("/oauth_app", OAuthAppController, :index)
post("/oauth_app", OAuthAppController, :create)
patch("/oauth_app/:id", OAuthAppController, :update)
delete("/oauth_app/:id", OAuthAppController, :delete)
end

scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do
@@ -664,6 +664,8 @@ defmodule Pleroma.Web.Router do
post("/auth/password", MastodonAPI.AuthController, :password_reset)

get("/web/*path", MastoFEController, :index)

get("/embed/:id", EmbedController, :show)
end

scope "/proxy/", Pleroma.Web.MediaProxy do


+ 1
- 1
lib/pleroma/web/streamer/streamer.ex View File

@@ -136,7 +136,7 @@ defmodule Pleroma.Web.Streamer do
false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, item_host),
false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, parent_host),
true <- thread_containment(item, user),
false <- CommonAPI.thread_muted?(user, item) do
false <- CommonAPI.thread_muted?(user, parent) do
false
else
_ -> true


+ 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

+ 5
- 5
mix.lock View File

@@ -12,7 +12,7 @@
"calendar": {:hex, :calendar, "0.17.6", "ec291cb2e4ba499c2e8c0ef5f4ace974e2f9d02ae9e807e711a9b0c7850b9aee", [:mix], [{:tzdata, "~> 0.5.20 or ~> 0.1.201603 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "738d0e17a93c2ccfe4ddc707bdc8e672e9074c8569498483feb1c4530fb91b2b"},
"captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "e0f16822d578866e186a0974d65ad58cddc1e2ab", [ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"]},
"castore": {:hex, :castore, "0.1.5", "591c763a637af2cc468a72f006878584bc6c306f8d111ef8ba1d4c10e0684010", [:mix], [], "hexpm", "6db356b2bc6cc22561e051ff545c20ad064af57647e436650aa24d7d06cd941a"},
"certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "805abd97539caf89ec6d4732c91e62ba9da0cda51ac462380bbd28ee697a8c42"},
"certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
"comeonin": {:hex, :comeonin, "5.3.1", "7fe612b739c78c9c1a75186ef2d322ce4d25032d119823269d0aa1e2f1e20025", [:mix], [], "hexpm", "d6222483060c17f0977fad1b7401ef0c5863c985a64352755f366aee3799c245"},
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"},
@@ -50,12 +50,12 @@
"gen_state_machine": {:hex, :gen_state_machine, "2.0.5", "9ac15ec6e66acac994cc442dcc2c6f9796cf380ec4b08267223014be1c728a95", [:mix], [], "hexpm"},
"gettext": {:hex, :gettext, "0.17.4", "f13088e1ec10ce01665cf25f5ff779e7df3f2dc71b37084976cf89d1aa124d5c", [:mix], [], "hexpm", "3c75b5ea8288e2ee7ea503ff9e30dfe4d07ad3c054576a6e60040e79a801e14d"},
"gun": {:git, "https://github.com/ninenines/gun.git", "e1a69b36b180a574c0ac314ced9613fdd52312cc", [ref: "e1a69b36b180a574c0ac314ced9613fdd52312cc"]},
"hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "e0100f8ef7d1124222c11ad362c857d3df7cb5f4204054f9f0f4a728666591fc"},
"hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"},
"html_entities": {:hex, :html_entities, "0.5.1", "1c9715058b42c35a2ab65edc5b36d0ea66dd083767bef6e3edb57870ef556549", [:mix], [], "hexpm", "30efab070904eb897ff05cd52fa61c1025d7f8ef3a9ca250bc4e6513d16c32de"},
"html_sanitize_ex": {:hex, :html_sanitize_ex, "1.3.0", "f005ad692b717691203f940c686208aa3d8ffd9dd4bb3699240096a51fa9564e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"},
"http_signatures": {:git, "https://git.pleroma.social/pleroma/http_signatures.git", "293d77bb6f4a67ac8bde1428735c3b42f22cbb30", [ref: "293d77bb6f4a67ac8bde1428735c3b42f22cbb30"]},
"httpoison": {:hex, :httpoison, "1.6.2", "ace7c8d3a361cebccbed19c283c349b3d26991eff73a1eaaa8abae2e3c8089b6", [:mix], [{:hackney, "~> 1.15 and >= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "aa2c74bd271af34239a3948779612f87df2422c2fdcfdbcec28d9c105f0773fe"},
"idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "4bdd305eb64e18b0273864920695cb18d7a2021f31a11b9c5fbcd9a253f936e2"},
"idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"},
"inet_cidr": {:hex, :inet_cidr, "1.0.4", "a05744ab7c221ca8e395c926c3919a821eb512e8f36547c062f62c4ca0cf3d6e", [:mix], [], "hexpm", "64a2d30189704ae41ca7dbdd587f5291db5d1dda1414e0774c29ffc81088c1bc"},
"jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"},
"joken": {:hex, :joken, "2.2.0", "2daa1b12be05184aff7b5ace1d43ca1f81345962285fff3f88db74927c954d3a", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "b4f92e30388206f869dd25d1af628a1d99d7586e5cf0672f64d4df84c4d2f5e9"},
@@ -102,7 +102,7 @@
"recon": {:hex, :recon, "2.5.0", "2f7fcbec2c35034bade2f9717f77059dc54eb4e929a3049ca7ba6775c0bd66cd", [:mix, :rebar3], [], "hexpm", "72f3840fedd94f06315c523f6cecf5b4827233bed7ae3fe135b2a0ebeab5e196"},
"remote_ip": {:git, "https://git.pleroma.social/pleroma/remote_ip.git", "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8", [ref: "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8"]},
"sleeplocks": {:hex, :sleeplocks, "1.1.1", "3d462a0639a6ef36cc75d6038b7393ae537ab394641beb59830a1b8271faeed3", [:rebar3], [], "hexpm", "84ee37aeff4d0d92b290fff986d6a95ac5eedf9b383fadfd1d88e9b84a1c02e1"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm", "13104d7897e38ed7f044c4de953a6c28597d1c952075eb2e328bc6d6f2bfc496"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
"sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm", "2e1ec458f892ffa81f9f8386e3f35a1af6db7a7a37748a64478f13163a1f3573"},
"swoosh": {:hex, :swoosh, "0.23.5", "bfd9404bbf5069b1be2ffd317923ce57e58b332e25dbca2a35dedd7820dfee5a", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "e3928e1d2889a308aaf3e42755809ac21cffd77cb58eef01cbfdab4ce2fd1e21"},
"syslog": {:hex, :syslog, "1.1.0", "6419a232bea84f07b56dc575225007ffe34d9fdc91abe6f1b2f254fd71d8efc2", [:rebar3], [], "hexpm", "4c6a41373c7e20587be33ef841d3de6f3beba08519809329ecc4d27b15b659e1"},
@@ -112,7 +112,7 @@
"trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"},
"tzdata": {:hex, :tzdata, "0.5.22", "f2ba9105117ee0360eae2eca389783ef7db36d533899b2e84559404dbc77ebb8", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "cd66c8a1e6a9e121d1f538b01bef459334bb4029a1ffb4eeeb5e4eae0337e7b6"},
"ueberauth": {:hex, :ueberauth, "0.6.2", "25a31111249d60bad8b65438b2306a4dc91f3208faa62f5a8c33e8713989b2e8", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "db9fbfb5ac707bc4f85a297758406340bf0358b4af737a88113c1a9eee120ac7"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm", "1d1848c40487cdb0b30e8ed975e34e025860c02e419cb615d255849f3427439d"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"},
"unsafe": {:hex, :unsafe, "1.0.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm", "6c7729a2d214806450d29766abc2afaa7a2cbecf415be64f36a6691afebb50e5"},
"web_push_encryption": {:hex, :web_push_encryption, "0.2.3", "a0ceab85a805a30852f143d22d71c434046fbdbafbc7292e7887cec500826a80", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:poison, "~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "9315c8f37c108835cf3f8e9157d7a9b8f420a34f402d1b1620a31aed5b93ecdf"},
"websocket_client": {:git, "https://github.com/jeremyong/websocket_client.git", "9a6f65d05ebf2725d62fb19262b21f1805a59fbf", []},


+ 43
- 41
priv/gettext/nl/LC_MESSAGES/errors.po View File

@@ -3,14 +3,16 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-05-15 09:37+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"PO-Revision-Date: 2020-06-02 07:36+0000\n"
"Last-Translator: Fristi <fristi@subcon.town>\n"
"Language-Team: Dutch <https://translate.pleroma.social/projects/pleroma/"
"pleroma/nl/>\n"
"Language: nl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Translate Toolkit 2.5.1\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.0.4\n"

## This file is a PO Template file.
##
@@ -23,142 +25,142 @@ msgstr ""
## effect: edit them in PO (`.po`) files instead.
## From Ecto.Changeset.cast/4
msgid "can't be blank"
msgstr ""
msgstr "kan niet leeg zijn"

## From Ecto.Changeset.unique_constraint/3
msgid "has already been taken"
msgstr ""
msgstr "is al bezet"

## From Ecto.Changeset.put_change/3
msgid "is invalid"
msgstr ""
msgstr "is ongeldig"

## From Ecto.Changeset.validate_format/3
msgid "has invalid format"
msgstr ""
msgstr "heeft een ongeldig formaat"

## From Ecto.Changeset.validate_subset/3
msgid "has an invalid entry"
msgstr ""
msgstr "heeft een ongeldige entry"

## From Ecto.Changeset.validate_exclusion/3
msgid "is reserved"
msgstr ""
msgstr "is gereserveerd"

## From Ecto.Changeset.validate_confirmation/3
msgid "does not match confirmation"
msgstr ""
msgstr "komt niet overeen met bevestiging"

## From Ecto.Changeset.no_assoc_constraint/3
msgid "is still associated with this entry"
msgstr ""
msgstr "is nog geassocieerd met deze entry"

msgid "are still associated with this entry"
msgstr ""
msgstr "zijn nog geassocieerd met deze entry"

## From Ecto.Changeset.validate_length/3
msgid "should be %{count} character(s)"
msgid_plural "should be %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgstr[0] "dient %{count} karakter te bevatten"
msgstr[1] "dient %{count} karakters te bevatten"

msgid "should have %{count} item(s)"
msgid_plural "should have %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
msgstr[0] "dient %{count} item te bevatten"
msgstr[1] "dient %{count} items te bevatten"

msgid "should be at least %{count} character(s)"
msgid_plural "should be at least %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgstr[0] "dient ten minste %{count} karakter te bevatten"
msgstr[1] "dient ten minste %{count} karakters te bevatten"

msgid "should have at least %{count} item(s)"
msgid_plural "should have at least %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
msgstr[0] "dient ten minste %{count} item te bevatten"
msgstr[1] "dient ten minste %{count} items te bevatten"

msgid "should be at most %{count} character(s)"
msgid_plural "should be at most %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgstr[0] "dient niet meer dan %{count} karakter te bevatten"
msgstr[1] "dient niet meer dan %{count} karakters te bevatten"

msgid "should have at most %{count} item(s)"
msgid_plural "should have at most %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
msgstr[0] "dient niet meer dan %{count} item te bevatten"
msgstr[1] "dient niet meer dan %{count} items te bevatten"

## From Ecto.Changeset.validate_number/3
msgid "must be less than %{number}"
msgstr ""
msgstr "dient kleiner te zijn dan %{number}"

msgid "must be greater than %{number}"
msgstr ""
msgstr "dient groter te zijn dan %{number}"

msgid "must be less than or equal to %{number}"
msgstr ""
msgstr "dient kleiner dan of gelijk te zijn aan %{number}"

msgid "must be greater than or equal to %{number}"
msgstr ""
msgstr "dient groter dan of gelijk te zijn aan %{number}"

msgid "must be equal to %{number}"
msgstr ""
msgstr "dient gelijk te zijn aan %{number}"

#: lib/pleroma/web/common_api/common_api.ex:421
#, elixir-format
msgid "Account not found"
msgstr ""
msgstr "Account niet gevonden"

#: lib/pleroma/web/common_api/common_api.ex:249
#, elixir-format
msgid "Already voted"
msgstr ""
msgstr "Al gestemd"

#: lib/pleroma/web/oauth/oauth_controller.ex:360
#, elixir-format
msgid "Bad request"
msgstr ""
msgstr "Bad request"

#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:425
#, elixir-format
msgid "Can't delete object"
msgstr ""
msgstr "Object kan niet verwijderd worden"

#: lib/pleroma/web/mastodon_api/controllers/status_controller.ex:196
#, elixir-format
msgid "Can't delete this post"
msgstr ""
msgstr "Bericht kan niet verwijderd worden"

#: lib/pleroma/web/controller_helper.ex:95
#: lib/pleroma/web/controller_helper.ex:101
#, elixir-format
msgid "Can't display this activity"
msgstr ""
msgstr "Activiteit kan niet worden getoond"

#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:227
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:254
#, elixir-format
msgid "Can't find user"
msgstr ""
msgstr "Gebruiker kan niet gevonden worden"

#: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:114
#, elixir-format
msgid "Can't get favorites"
msgstr ""
msgstr "Favorieten konden niet opgehaald worden"

#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:437
#, elixir-format
msgid "Can't like object"
msgstr ""
msgstr "Object kan niet geliked worden"

#: lib/pleroma/web/common_api/utils.ex:556
#, elixir-format
msgid "Cannot post an empty status without attachments"
msgstr ""
msgstr "Status kan niet geplaatst worden zonder tekst of bijlagen"

#: lib/pleroma/web/common_api/utils.ex:504
#, elixir-format
msgid "Comment must be up to %{max_size} characters"
msgstr ""
msgstr "Opmerking dient maximaal %{max_size} karakters te bevatten"

#: lib/pleroma/config/config_db.ex:222
#, elixir-format


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

+ 0
- 12
test/http/adapter_helper/hackney_test.exs View File

@@ -31,17 +31,5 @@ defmodule Pleroma.HTTP.AdapterHelper.HackneyTest do
assert opts[:b] == 1
refute Keyword.has_key?(opts, :proxy)
end

test "add opts for https" do
uri = URI.parse("https://domain.com")

opts = Hackney.options(uri)

assert opts[:ssl_options] == [
partial_chain: &:hackney_connect.partial_chain/1,
versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"],
server_name_indication: 'domain.com'
]
end
end
end

+ 1
- 1
test/plugs/http_security_plug_test.exs View File

@@ -67,7 +67,7 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlugTest do

[csp] = Conn.get_resp_header(conn, "content-security-policy")

assert csp =~ ~r|report-uri https://endpoint.com; report-to csp-endpoint;|
assert csp =~ ~r|report-uri https://endpoint.com;report-to csp-endpoint;|

[reply_to] = Conn.get_resp_header(conn, "reply-to")



+ 2
- 1
test/tasks/relay_test.exs View File

@@ -65,7 +65,8 @@ defmodule Mix.Tasks.Pleroma.RelayTest do
"type" => "Undo",
"actor_id" => follower_id,
"limit" => 1,
"skip_preload" => true
"skip_preload" => true,
"invisible_actors" => true
})

assert undo_activity.data["type"] == "Undo"


+ 1
- 1
test/user_test.exs View File

@@ -1802,7 +1802,7 @@ defmodule Pleroma.UserTest do
user = insert(:user)
assert User.avatar_url(user) =~ "/images/avi.png"

Pleroma.Config.put([:assets, :default_user_avatar], "avatar.png")
clear_config([:assets, :default_user_avatar], "avatar.png")

user = User.get_cached_by_nickname_or_id(user.nickname)
assert User.avatar_url(user) =~ "avatar.png"


+ 1
- 0
test/web/activity_pub/relay_test.exs View File

@@ -108,6 +108,7 @@ defmodule Pleroma.Web.ActivityPub.RelayTest do
assert {:ok, %Activity{} = activity} = Relay.publish(note)
assert activity.data["type"] == "Announce"
assert activity.data["actor"] == service_actor.ap_id
assert activity.data["to"] == [service_actor.follower_address]
assert called(Pleroma.Web.Federator.publish(activity))
end



+ 5
- 756
test/web/admin_api/controllers/admin_api_controller_test.exs View File

@@ -16,10 +16,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
alias Pleroma.MFA
alias Pleroma.ModerationLog
alias Pleroma.Repo
alias Pleroma.ReportNote
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.UserInviteToken
alias Pleroma.Web
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.CommonAPI
@@ -587,122 +585,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
end

describe "POST /api/pleroma/admin/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 =
post(
conn,
"/api/pleroma/admin/users/email_invite?email=#{recipient_email}&name=#{recipient_name}"
)

assert json_response(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)
|> 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(: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 = post(conn, "/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD")

assert json_response(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 = post(conn, "/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD")

assert json_response(conn, :bad_request) ==
%{
"error" =>
"To send invites you need to set the `registrations_open` option to false."
}
end
end

test "/api/pleroma/admin/users/:nickname/password_reset", %{conn: conn} do
user = insert(:user)

@@ -756,8 +638,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end

test "pagination works correctly with service users", %{conn: conn} do
service1 = insert(:user, ap_id: Web.base_url() <> "/relay")
service2 = insert(:user, ap_id: Web.base_url() <> "/internal/fetch")
service1 = User.get_or_create_service_actor_by_ap_id(Web.base_url() <> "/meido", "meido")
insert_list(25, :user)

assert %{"count" => 26, "page_size" => 10, "users" => users1} =
@@ -766,8 +648,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
|> json_response(200)

assert Enum.count(users1) == 10
assert service1 not in [users1]
assert service2 not in [users1]
assert service1 not in users1

assert %{"count" => 26, "page_size" => 10, "users" => users2} =
conn
@@ -775,8 +656,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
|> json_response(200)

assert Enum.count(users2) == 10
assert service1 not in [users2]
assert service2 not in [users2]
assert service1 not in users2

assert %{"count" => 26, "page_size" => 10, "users" => users3} =
conn
@@ -784,8 +664,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
|> json_response(200)

assert Enum.count(users3) == 6
assert service1 not in [users3]
assert service2 not in [users3]
assert service1 not in users3
end

test "renders empty array for the second page", %{conn: conn} do
@@ -1317,392 +1196,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
end

describe "POST /api/pleroma/admin/users/invite_token" do
test "without options", %{conn: conn} do
conn = post(conn, "/api/pleroma/admin/users/invite_token")

invite_json = json_response(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 =
post(conn, "/api/pleroma/admin/users/invite_token", %{
"expires_at" => Date.to_string(Date.utc_today())
})

invite_json = json_response(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 = post(conn, "/api/pleroma/admin/users/invite_token", %{"max_use" => 150})

invite_json = json_response(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 =
post(conn, "/api/pleroma/admin/users/invite_token", %{
"max_use" => 150,
"expires_at" => Date.to_string(Date.utc_today())
})

invite_json = json_response(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(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(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 = post(conn, "/api/pleroma/admin/users/revoke_invite", %{"token" => invite.token})

assert json_response(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 = post(conn, "/api/pleroma/admin/users/revoke_invite", %{"token" => "foo"})

assert json_response(conn, :not_found) == %{"error" => "Not found"}
end
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(: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(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)
|> patch("/api/pleroma/admin/reports", %{
"reports" => [%{"state" => "resolved", "id" => id}]
})
|> json_response(403)

assert response == %{
"error" => "Insufficient permissions: admin:write:reports."
}

conn
|> assign(:token, write_token)
|> patch("/api/pleroma/admin/reports", %{
"reports" => [%{"state" => "resolved", "id" => id}]
})
|> json_response(:no_content)
end

test "mark report as resolved", %{conn: conn, id: id, admin: admin} do
conn
|> patch("/api/pleroma/admin/reports", %{
"reports" => [
%{"state" => "resolved", "id" => id}
]
})
|> json_response(: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
|> patch("/api/pleroma/admin/reports", %{
"reports" => [
%{"state" => "closed", "id" => id}
]
})
|> json_response(: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
|> patch("/api/pleroma/admin/reports", %{
"reports" => [
%{"state" => "test", "id" => id}
]
})

assert hd(json_response(conn, :bad_request))["error"] == "Unsupported state"
end

test "returns 404 when report is not exist", %{conn: conn} do
conn =
conn
|> patch("/api/pleroma/admin/reports", %{
"reports" => [
%{"state" => "closed", "id" => "test"}
]
})

assert hd(json_response(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
|> patch("/api/pleroma/admin/reports", %{
"reports" => [
%{"state" => "resolved", "id" => id},
%{"state" => "closed", "id" => second_report_id}
]
})
|> json_response(: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(: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(: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(:ok)

[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(:ok)

[closed_report] = response["reports"]

assert length(response["reports"]) == 1
assert closed_report["id"] == second_report_id

assert response["total"] == 1

response =
conn
|> get("/api/pleroma/admin/reports", %{
"state" => "resolved"
})
|> json_response(:ok)

assert Enum.empty?(response["reports"])
assert response["total"] == 0
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 "GET /api/pleroma/admin/restart" do
setup do: clear_config(:configurable_from_database, true)

@@ -2251,65 +1744,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
end

describe "POST /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]
})

post(conn, "/api/pleroma/admin/reports/#{report_id}/notes", %{
content: "this is disgusting!"
})

post(conn, "/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
[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(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

[note, _] = Repo.all(ReportNote)

delete(conn, "/api/pleroma/admin/reports/#{report_id}/notes/#{note.id}")

assert ReportNote |> Repo.all() |> length() == 1
end
end

describe "/api/pleroma/admin/stats" do
test "status visibility count", %{conn: conn} do
@@ -2329,191 +1763,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
response["status_visibility"]
end
end

describe "POST /api/pleroma/admin/oauth_app" do
test "errors", %{conn: conn} do
response = conn |> post("/api/pleroma/admin/oauth_app", %{}) |> json_response(200)

assert response == %{"name" => "can't be blank", "redirect_uris" => "can't be blank"}
end

test "success", %{conn: conn} do
base_url = Web.base_url()
app_name = "Trusted app"

response =
conn
|> post("/api/pleroma/admin/oauth_app", %{
name: app_name,
redirect_uris: base_url
})
|> json_response(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
|> post("/api/pleroma/admin/oauth_app", %{
name: app_name,
redirect_uris: base_url,
trusted: true
})
|> json_response(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(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: to_string(page_size)})
|> json_response(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(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(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(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(:no_content)

assert response == ""
end

test "with non existance id", %{conn: conn} do
response =
conn
|> delete("/api/pleroma/admin/oauth_app/0")
|> json_response(: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
|> patch("/api/pleroma/admin/oauth_app/" <> to_string(app.id), %{
name: name,
trusted: true,
redirect_uris: url,
scopes: scopes,
website: website
})
|> json_response(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
|> patch("/api/pleroma/admin/oauth_app/0")
|> json_response(:bad_request)

assert response == ""
end
end
end

# Needed for testing


+ 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

+ 8
- 0
test/web/admin_api/controllers/status_controller_test.exs View File

@@ -42,6 +42,14 @@ defmodule Pleroma.Web.AdminAPI.StatusControllerTest do
|> json_response_and_validate_schema(200)

assert response["id"] == activity.id

account = response["account"]
actor = User.get_by_ap_id(activity.actor)

assert account["id"] == actor.id
assert account["nickname"] == actor.nickname
assert account["deactivated"] == actor.deactivated
assert account["confirmation_pending"] == actor.confirmation_pending
end
end



+ 23
- 15
test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs View File

@@ -8,6 +8,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do

use Pleroma.Web.ConnCase

import Mock
import Pleroma.Factory

setup do: clear_config([:instance, :max_account_fields])
@@ -52,24 +53,31 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do

user = Repo.get(User, user_data["id"])

res_conn =
conn
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{
"pleroma_settings_store" => %{
masto_fe: %{
theme: "blub"
clear_config([:instance, :federating], true)

with_mock Pleroma.Web.Federator,
publish: fn _activity -> :ok end do
res_conn =
conn
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{
"pleroma_settings_store" => %{
masto_fe: %{
theme: "blub"
}
}
}
})
})

assert user_data = json_response_and_validate_schema(res_conn, 200)
assert user_data = json_response_and_validate_schema(res_conn, 200)

assert user_data["pleroma"]["settings_store"] ==
%{
"pleroma_fe" => %{"theme" => "bla"},
"masto_fe" => %{"theme" => "blub"}
}
assert user_data["pleroma"]["settings_store"] ==
%{
"pleroma_fe" => %{"theme" => "bla"},
"masto_fe" => %{"theme" => "blub"}
}

assert_called(Pleroma.Web.Federator.publish(:_))
end
end

test "updates the user's bio", %{conn: conn} do


+ 92
- 91
test/web/mastodon_api/controllers/conversation_controller_test.exs View File

@@ -12,84 +12,88 @@ defmodule Pleroma.Web.MastodonAPI.ConversationControllerTest do

setup do: oauth_access(["read:statuses"])

test "returns a list of conversations", %{user: user_one, conn: conn} do
user_two = insert(:user)
user_three = insert(:user)

{:ok, user_two} = User.follow(user_two, user_one)

assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0

{:ok, direct} =
CommonAPI.post(user_one, %{
status: "Hi @#{user_two.nickname}, @#{user_three.nickname}!",
visibility: "direct"
})

assert User.get_cached_by_id(user_two.id).unread_conversation_count == 1

{:ok, _follower_only} =
CommonAPI.post(user_one, %{
status: "Hi @#{user_two.nickname}!",
visibility: "private"
})

res_conn = get(conn, "/api/v1/conversations")

assert response = json_response_and_validate_schema(res_conn, 200)

assert [
%{
"id" => res_id,
"accounts" => res_accounts,
"last_status" => res_last_status,
"unread" => unread
}
] = response

account_ids = Enum.map(res_accounts, & &1["id"])
assert length(res_accounts) == 2
assert user_two.id in account_ids
assert user_three.id in account_ids
assert is_binary(res_id)
assert unread == false
assert res_last_status["id"] == direct.id
assert User.get_cached_by_id(user_one.id).unread_conversation_count == 0
describe "returns a list of conversations" do
setup(%{user: user_one, conn: conn}) do
user_two = insert(:user)
user_three = insert(:user)

{:ok, user_two} = User.follow(user_two, user_one)

{:ok, %{user: user_one, user_two: user_two, user_three: user_three, conn: conn}}
end

test "returns correct conversations", %{
user: user_one,
user_two: user_two,
user_three: user_three,
conn: conn
} do
assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0
{:ok, direct} = create_direct_message(user_one, [user_two, user_three])

assert User.get_cached_by_id(user_two.id).unread_conversation_count == 1

{:ok, _follower_only} =
CommonAPI.post(user_one, %{
status: "Hi @#{user_two.nickname}!",
visibility: "private"
})

res_conn = get(conn, "/api/v1/conversations")

assert response = json_response_and_validate_schema(res_conn, 200)

assert [
%{
"id" => res_id,
"accounts" => res_accounts,
"last_status" => res_last_status,
"unread" => unread
}
] = response

account_ids = Enum.map(res_accounts, & &1["id"])
assert length(res_accounts) == 2
assert user_two.id in account_ids
assert user_three.id in account_ids
assert is_binary(res_id)
assert unread == false
assert res_last_status["id"] == direct.id
assert User.get_cached_by_id(user_one.id).unread_conversation_count == 0
end

test "observes limit params", %{
user: user_one,
user_two: user_two,
user_three: user_three,
conn: conn
} do
{:ok, _} = create_direct_message(user_one, [user_two, user_three])
{:ok, _} = create_direct_message(user_two, [user_one, user_three])
{:ok, _} = create_direct_message(user_three, [user_two, user_one])

res_conn = get(conn, "/api/v1/conversations?limit=1")

assert response = json_response_and_validate_schema(res_conn, 200)

assert Enum.count(response) == 1

res_conn = get(conn, "/api/v1/conversations?limit=2")

assert response = json_response_and_validate_schema(res_conn, 200)

assert Enum.count(response) == 2
end
end

test "filters conversations by recipients", %{user: user_one, conn: conn} do
user_two = insert(:user)
user_three = insert(:user)

{:ok, direct1} =
CommonAPI.post(user_one, %{
status: "Hi @#{user_two.nickname}!",
visibility: "direct"
})

{:ok, _direct2} =
CommonAPI.post(user_one, %{
status: "Hi @#{user_three.nickname}!",
visibility: "direct"
})

{:ok, direct3} =
CommonAPI.post(user_one, %{
status: "Hi @#{user_two.nickname}, @#{user_three.nickname}!",
visibility: "direct"
})

{:ok, _direct4} =
CommonAPI.post(user_two, %{
status: "Hi @#{user_three.nickname}!",
visibility: "direct"
})

{:ok, direct5} =
CommonAPI.post(user_two, %{
status: "Hi @#{user_one.nickname}!",
visibility: "direct"
})
{:ok, direct1} = create_direct_message(user_one, [user_two])
{:ok, _direct2} = create_direct_message(user_one, [user_three])
{:ok, direct3} = create_direct_message(user_one, [user_two, user_three])
{:ok, _direct4} = create_direct_message(user_two, [user_three])
{:ok, direct5} = create_direct_message(user_two, [user_one])

assert [conversation1, conversation2] =
conn
@@ -109,12 +113,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationControllerTest do

test "updates the last_status on reply", %{user: user_one, conn: conn} do
user_two = insert(:user)

{:ok, direct} =
CommonAPI.post(user_one, %{
status: "Hi @#{user_two.nickname}",
visibility: "direct"
})
{:ok, direct} = create_direct_message(user_one, [user_two])

{:ok, direct_reply} =
CommonAPI.post(user_two, %{
@@ -133,12 +132,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationControllerTest do

test "the user marks a conversation as read", %{user: user_one, conn: conn} do
user_two = insert(:user)

{:ok, direct} =
CommonAPI.post(user_one, %{
status: "Hi @#{user_two.nickname}",
visibility: "direct"
})
{:ok, direct} = create_direct_message(user_one, [user_two])

assert User.get_cached_by_id(user_one.id).unread_conversation_count == 0
assert User.get_cached_by_id(user_two.id).unread_conversation_count == 1
@@ -194,15 +188,22 @@ defmodule Pleroma.Web.MastodonAPI.ConversationControllerTest do

test "(vanilla) Mastodon frontend behaviour", %{user: user_one, conn: conn} do
user_two = insert(:user)

{:ok, direct} =
CommonAPI.post(user_one, %{
status: "Hi @#{user_two.nickname}!",
visibility: "direct"
})
{:ok, direct} = create_direct_message(user_one, [user_two])

res_conn = get(conn, "/api/v1/statuses/#{direct.id}/context")

assert %{"ancestors" => [], "descendants" => []} == json_response(res_conn, 200)
end

defp create_direct_message(sender, recips) do
hellos =
recips
|> Enum.map(fn s -> "@#{s.nickname}" end)
|> Enum.join(", ")

CommonAPI.post(sender, %{
status: "Hi #{hellos}!",
visibility: "direct"
})
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=天子")
|> json_response_and_validate_schema(200)

assert results["hashtags"] == [
%{"name" => "天子", "url" => "#{Web.base_url()}/tag/天子"}
]

[status] = results["statuses"]
assert status["id"] == to_string(activity.id)
end

test "constructs hashtags from search query", %{conn: conn} do
results =
conn
|> get("/api/v2/search?#{URI.encode_query(%{q: "some text with #explicit #hashtags"})}")
|> json_response_and_validate_schema(200)

assert results["hashtags"] == [
%{"name" => "explicit", "url" => "#{Web.base_url()}/tag/explicit"},
%{"name" => "hashtags", "url" => "#{Web.base_url()}/tag/hashtags"}
]

results =
conn
|> get("/api/v2/search?#{URI.encode_query(%{q: "john doe JOHN DOE"})}")
|> json_response_and_validate_schema(200)

assert results["hashtags"] == [
%{"name" => "john", "url" => "#{Web.base_url()}/tag/john"},
%{"name" => "doe", "url" => "#{Web.base_url()}/tag/doe"},
%{"name" => "JohnDoe", "url" => "#{Web.base_url()}/tag/JohnDoe"}
]

results =
conn
|> get("/api/v2/search?#{URI.encode_query(%{q: "accident-prone"})}")
|> json_response_and_validate_schema(200)

assert results["hashtags"] == [
%{"name" => "accident", "url" => "#{Web.base_url()}/tag/accident"},
%{"name" => "prone", "url" => "#{Web.base_url()}/tag/prone"},
%{"name" => "AccidentProne", "url" => "#{Web.base_url()}/tag/AccidentProne"}
]
end

test "excludes a blocked users from search results", %{conn: conn} do
user = insert(:user)
user_smith = insert(:user, %{nickname: "Agent", name: "I love 2hu"})
@@ -179,7 +217,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do
[account | _] = results["accounts"]
assert account["id"] == to_string(user_three.id)

assert results["hashtags"] == []
assert results["hashtags"] == ["2hu"]

[status] = results["statuses"]
assert status["id"] == to_string(activity.id)


+ 52
- 2
test/web/mastodon_api/controllers/timeline_controller_test.exs View File

@@ -60,9 +60,9 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
describe "public" do
@tag capture_log: true
test "the public timeline", %{conn: conn} do
following = insert(:user)
user = insert(:user)

{:ok, _activity} = CommonAPI.post(following, %{status: "test"})
{:ok, activity} = CommonAPI.post(user, %{status: "test"})

_activity = insert(:note_activity, local: false)

@@ -77,6 +77,13 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
conn = get(build_conn(), "/api/v1/timelines/public?local=1")

assert [%{"content" => "test"}] = json_response_and_validate_schema(conn, :ok)

# does not contain repeats
{:ok, _} = CommonAPI.repeat(activity.id, user)

conn = get(build_conn(), "/api/v1/timelines/public?local=true")

assert [_] = json_response_and_validate_schema(conn, :ok)
end

test "the public timeline includes only public statuses for an authenticated user" do
@@ -90,6 +97,49 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
res_conn = get(conn, "/api/v1/timelines/public")
assert length(json_response_and_validate_schema(res_conn, 200)) == 1
end

test "doesn't return replies if follower is posting with blocked user" do
%{conn: conn, user: blocker} = oauth_access(["read:statuses"])
[blockee, friend] = insert_list(2, :user)
{:ok, blocker} = User.follow(blocker, friend)
{:ok, _} = User.block(blocker, blockee)

conn = assign(conn, :user, blocker)

{:ok, %{id: activity_id} = activity} = CommonAPI.post(friend, %{status: "hey!"})

{:ok, reply_from_blockee} =
CommonAPI.post(blockee, %{status: "heya", in_reply_to_status_id: activity})

{:ok, _reply_from_friend} =
CommonAPI.post(friend, %{status: "status", in_reply_to_status_id: reply_from_blockee})

res_conn = get(conn, "/api/v1/timelines/public")
[%{"id" => ^activity_id}] = json_response_and_validate_schema(res_conn, 200)
end

test "doesn't return replies if follow is posting with users from blocked domain" do
%{conn: conn, user: blocker} = oauth_access(["read:statuses"])
friend = insert(:user)
blockee = insert(:user, ap_id: "https://example.com/users/blocked")
{:ok, blocker} = User.follow(blocker, friend)
{:ok, blocker} = User.block_domain(blocker, "example.com")

conn = assign(conn, :user, blocker)

{:ok, %{id: activity_id} = activity} = CommonAPI.post(friend, %{status: "hey!"})

{:ok, reply_from_blockee} =
CommonAPI.post(blockee, %{status: "heya", in_reply_to_status_id: activity})

{:ok, _reply_from_friend} =
CommonAPI.post(friend, %{status: "status", in_reply_to_status_id: reply_from_blockee})

res_conn = get(conn, "/api/v1/timelines/public")

activities = json_response_and_validate_schema(res_conn, 200)
[%{"id" => ^activity_id}] = activities
end
end

defp local_and_remote_activities do


+ 31
- 4
test/web/mastodon_api/views/account_view_test.exs View File

@@ -54,10 +54,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
header_static: "http://localhost:4001/images/banner.png",
emojis: [
%{
"static_url" => "/file.png",
"url" => "/file.png",
"shortcode" => "karjalanpiirakka",
"visible_in_picker" => false
static_url: "/file.png",
url: "/file.png",
shortcode: "karjalanpiirakka",
visible_in_picker: false
}
],
fields: [],
@@ -491,4 +491,31 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
AccountView.render("show.json", %{user: user, for: user})
end
end

test "uses mediaproxy urls when it's enabled" do
clear_config([:media_proxy, :enabled], true)

user =
insert(:user,
avatar: %{"url" => [%{"href" => "https://evil.website/avatar.png"}]},
banner: %{"url" => [%{"href" => "https://evil.website/banner.png"}]},
emoji: %{"joker_smile" => "https://evil.website/society.png"}
)

AccountView.render("show.json", %{user: user})
|> Enum.all?(fn
{key, url} when key in [:avatar, :avatar_static, :header, :header_static] ->
String.starts_with?(url, Pleroma.Web.base_url())

{:emojis, emojis} ->
Enum.all?(emojis, fn %{url: url, static_url: static_url} ->
String.starts_with?(url, Pleroma.Web.base_url()) &&
String.starts_with?(static_url, Pleroma.Web.base_url())
end)

_ ->
true
end)
|> assert()
end
end

+ 3
- 11
test/web/media_proxy/media_proxy_test.exs View File

@@ -124,15 +124,7 @@ defmodule Pleroma.Web.MediaProxyTest do
end

test "uses the configured base_url" do
base_url = Pleroma.Config.get([:media_proxy, :base_url])

if base_url do
on_exit(fn ->
Pleroma.Config.put([:media_proxy, :base_url], base_url)
end)
end

Pleroma.Config.put([:media_proxy, :base_url], "https://cache.pleroma.social")
clear_config([:media_proxy, :base_url], "https://cache.pleroma.social")

url = "https://pleroma.soykaf.com/static/logo.png"
encoded = url(url)
@@ -213,8 +205,8 @@ defmodule Pleroma.Web.MediaProxyTest do
end

test "does not change whitelisted urls" do
Pleroma.Config.put([:media_proxy, :whitelist], ["mycdn.akamai.com"])
Pleroma.Config.put([:media_proxy, :base_url], "https://cache.pleroma.social")
clear_config([:media_proxy, :whitelist], ["mycdn.akamai.com"])
clear_config([:media_proxy, :base_url], "https://cache.pleroma.social")

media_url = "https://mycdn.akamai.com"



+ 19
- 0
test/web/streamer/streamer_test.exs View File

@@ -112,6 +112,25 @@ defmodule Pleroma.Web.StreamerTest do
refute Streamer.filtered_by_user?(user, announce)
end

test "it streams boosts of mastodon user in the 'user' stream", %{user: user} do
Streamer.get_topic_and_add_socket("user", user)

other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "hey"})

data =
File.read!("test/fixtures/mastodon-announce.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
|> Map.put("actor", user.ap_id)

{:ok, %Pleroma.Activity{data: _data, local: false} = announce} =
Pleroma.Web.ActivityPub.Transmogrifier.handle_incoming(data)

assert_receive {:render_with_user, Pleroma.Web.StreamerView, "update.json", ^announce}
refute Streamer.filtered_by_user?(user, announce)
end

test "it sends notify to in the 'user' stream", %{user: user, notify: notify} do
Streamer.get_topic_and_add_socket("user", user)
Streamer.stream("user", notify)


Loading…
Cancel
Save