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

This commit is contained in:
lain 2020-06-05 16:53:56 +02:00
commit a8ca030d85
52 changed files with 4280 additions and 2682 deletions

View file

@ -22,8 +22,21 @@ defmodule Pleroma.LoadTesting.Activities do
@max_concurrency 10 @max_concurrency 10
@visibility ~w(public private direct unlisted) @visibility ~w(public private direct unlisted)
@types ~w(simple emoji mentions hell_thread attachment tag like reblog simple_thread remote) @types [
@groups ~w(user friends non_friends) :simple,
:emoji,
:mentions,
:hell_thread,
:attachment,
:tag,
:like,
:reblog,
:simple_thread
]
@groups [:friends_local, :friends_remote, :non_friends_local, :non_friends_local]
@remote_groups [:friends_remote, :non_friends_remote]
@friends_groups [:friends_local, :friends_remote]
@non_friends_groups [:non_friends_local, :non_friends_remote]
@spec generate(User.t(), keyword()) :: :ok @spec generate(User.t(), keyword()) :: :ok
def generate(user, opts \\ []) do def generate(user, opts \\ []) do
@ -34,33 +47,24 @@ def generate(user, opts \\ []) do
opts = Keyword.merge(@defaults, opts) opts = Keyword.merge(@defaults, opts)
friends = users = Users.prepare_users(user, opts)
user
|> Users.get_users(limit: opts[:friends_used], local: :local, friends?: true)
|> Enum.shuffle()
non_friends = {:ok, _} = Agent.start_link(fn -> users[:non_friends_remote] end, name: :non_friends_remote)
user
|> Users.get_users(limit: opts[:non_friends_used], local: :local, friends?: false)
|> Enum.shuffle()
task_data = task_data =
for visibility <- @visibility, for visibility <- @visibility,
type <- @types, type <- @types,
group <- @groups, group <- [:user | @groups],
do: {visibility, type, group} do: {visibility, type, group}
IO.puts("Starting generating #{opts[:iterations]} iterations of activities...") IO.puts("Starting generating #{opts[:iterations]} iterations of activities...")
friends_thread = Enum.take(friends, 5)
non_friends_thread = Enum.take(friends, 5)
public_long_thread = fn -> public_long_thread = fn ->
generate_long_thread("public", user, friends_thread, non_friends_thread, opts) generate_long_thread("public", users, opts)
end end
private_long_thread = fn -> private_long_thread = fn ->
generate_long_thread("private", user, friends_thread, non_friends_thread, opts) generate_long_thread("private", users, opts)
end end
iterations = opts[:iterations] iterations = opts[:iterations]
@ -73,10 +77,10 @@ def generate(user, opts \\ []) do
i when i == iterations - 2 -> i when i == iterations - 2 ->
spawn(public_long_thread) spawn(public_long_thread)
spawn(private_long_thread) spawn(private_long_thread)
generate_activities(user, friends, non_friends, Enum.shuffle(task_data), opts) generate_activities(users, Enum.shuffle(task_data), opts)
_ -> _ ->
generate_activities(user, friends, non_friends, Enum.shuffle(task_data), opts) generate_activities(users, Enum.shuffle(task_data), opts)
end end
) )
end) end)
@ -127,16 +131,16 @@ def generate_tagged_activities(opts \\ []) do
end) end)
end end
defp generate_long_thread(visibility, user, friends, non_friends, _opts) do defp generate_long_thread(visibility, users, _opts) do
group = group =
if visibility == "public", if visibility == "public",
do: "friends", do: :friends_local,
else: "user" else: :user
tasks = get_reply_tasks(visibility, group) |> Stream.cycle() |> Enum.take(50) tasks = get_reply_tasks(visibility, group) |> Stream.cycle() |> Enum.take(50)
{:ok, activity} = {:ok, activity} =
CommonAPI.post(user, %{ CommonAPI.post(users[:user], %{
status: "Start of #{visibility} long thread", status: "Start of #{visibility} long thread",
visibility: visibility visibility: visibility
}) })
@ -150,31 +154,28 @@ defp generate_long_thread(visibility, user, friends, non_friends, _opts) do
Map.put(state, key, activity) Map.put(state, key, activity)
end) end)
acc = {activity.id, ["@" <> user.nickname, "reply to long thread"]} acc = {activity.id, ["@" <> users[:user].nickname, "reply to long thread"]}
insert_replies_for_long_thread(tasks, visibility, user, friends, non_friends, acc) insert_replies_for_long_thread(tasks, visibility, users, acc)
IO.puts("Generating #{visibility} long thread ended\n") IO.puts("Generating #{visibility} long thread ended\n")
end end
defp insert_replies_for_long_thread(tasks, visibility, user, friends, non_friends, acc) do defp insert_replies_for_long_thread(tasks, visibility, users, acc) do
Enum.reduce(tasks, acc, fn Enum.reduce(tasks, acc, fn
"friend", {id, data} -> :user, {id, data} ->
friend = Enum.random(friends) user = users[:user]
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} ->
insert_reply(user, List.delete(data, "@" <> user.nickname), id, visibility) insert_reply(user, List.delete(data, "@" <> user.nickname), id, visibility)
group, {id, data} ->
replier = Enum.random(users[group])
insert_reply(replier, List.delete(data, "@" <> replier.nickname), id, visibility)
end) end)
end end
defp generate_activities(user, friends, non_friends, task_data, opts) do defp generate_activities(users, task_data, opts) do
Task.async_stream( Task.async_stream(
task_data, task_data,
fn {visibility, type, group} -> fn {visibility, type, group} ->
insert_activity(type, visibility, group, user, friends, non_friends, opts) insert_activity(type, visibility, group, users, opts)
end, end,
max_concurrency: @max_concurrency, max_concurrency: @max_concurrency,
timeout: 30_000 timeout: 30_000
@ -182,67 +183,104 @@ defp generate_activities(user, friends, non_friends, task_data, opts) do
|> Stream.run() |> Stream.run()
end end
defp insert_activity("simple", visibility, group, user, friends, non_friends, _opts) do defp insert_local_activity(visibility, group, users, status) do
{:ok, _activity} = {:ok, _} =
group group
|> get_actor(user, friends, non_friends) |> get_actor(users)
|> CommonAPI.post(%{status: "Simple status", visibility: visibility}) |> CommonAPI.post(%{status: status, visibility: visibility})
end end
defp insert_activity("emoji", visibility, group, user, friends, non_friends, _opts) do defp insert_remote_activity(visibility, group, users, status) do
{:ok, _activity} = actor = get_actor(group, users)
group {act_data, obj_data} = prepare_activity_data(actor, visibility, users[:user])
|> get_actor(user, friends, non_friends) {activity_data, object_data} = other_data(actor, status)
|> CommonAPI.post(%{
status: "Simple status with emoji :firefox:", activity_data
visibility: visibility |> Map.merge(act_data)
}) |> Map.put("object", Map.merge(object_data, obj_data))
|> Pleroma.Web.ActivityPub.ActivityPub.insert(false)
end end
defp insert_activity("mentions", visibility, group, user, friends, non_friends, _opts) do defp user_mentions(users) do
user_mentions = user_mentions =
get_random_mentions(friends, Enum.random(0..3)) ++ Enum.reduce(
get_random_mentions(non_friends, Enum.random(0..3)) @groups,
[],
fn group, acc ->
acc ++ get_random_mentions(users[group], Enum.random(0..2))
end
)
user_mentions = if Enum.random([true, false]),
if Enum.random([true, false]), do: ["@" <> users[:user].nickname | user_mentions],
do: ["@" <> user.nickname | user_mentions], else: user_mentions
else: user_mentions
{:ok, _activity} =
group
|> get_actor(user, friends, non_friends)
|> CommonAPI.post(%{
status: Enum.join(user_mentions, ", ") <> " simple status with mentions",
visibility: visibility
})
end end
defp insert_activity("hell_thread", visibility, group, user, friends, non_friends, _opts) do defp hell_thread_mentions(users) do
mentions = with {:ok, nil} <- Cachex.get(:user_cache, "hell_thread_mentions") do
with {:ok, nil} <- Cachex.get(:user_cache, "hell_thread_mentions") do cached =
cached = @groups
([user | Enum.take(friends, 10)] ++ Enum.take(non_friends, 10)) |> Enum.reduce([users[:user]], fn group, acc ->
|> Enum.map(&"@#{&1.nickname}") acc ++ Enum.take(users[group], 5)
|> Enum.join(", ") end)
|> Enum.map(&"@#{&1.nickname}")
|> Enum.join(", ")
Cachex.put(:user_cache, "hell_thread_mentions", cached) Cachex.put(:user_cache, "hell_thread_mentions", cached)
cached cached
else else
{:ok, cached} -> cached {:ok, cached} -> cached
end end
{:ok, _activity} =
group
|> get_actor(user, friends, non_friends)
|> CommonAPI.post(%{
status: mentions <> " hell thread status",
visibility: visibility
})
end end
defp insert_activity("attachment", visibility, group, user, friends, non_friends, _opts) do defp insert_activity(:simple, visibility, group, users, _opts)
actor = get_actor(group, user, friends, non_friends) when group in @remote_groups do
insert_remote_activity(visibility, group, users, "Remote status")
end
defp insert_activity(:simple, visibility, group, users, _opts) do
insert_local_activity(visibility, group, users, "Simple status")
end
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, users, _opts) do
actor = get_actor(group, users)
obj_data = %{ obj_data = %{
"actor" => actor.ap_id, "actor" => actor.ap_id,
@ -268,67 +306,54 @@ defp insert_activity("attachment", visibility, group, user, friends, non_friends
}) })
end end
defp insert_activity("tag", visibility, group, user, friends, non_friends, _opts) do defp insert_activity(:tag, visibility, group, users, _opts) do
{:ok, _activity} = insert_local_activity(visibility, group, users, "Status with #tag")
group
|> get_actor(user, friends, non_friends)
|> CommonAPI.post(%{status: "Status with #tag", visibility: visibility})
end end
defp insert_activity("like", visibility, group, user, friends, non_friends, opts) do defp insert_activity(:like, visibility, group, users, opts) do
actor = get_actor(group, user, friends, non_friends) actor = get_actor(group, users)
with activity_id when not is_nil(activity_id) <- get_random_create_activity_id(), with activity_id when not is_nil(activity_id) <- get_random_create_activity_id(),
{:ok, _activity} <- CommonAPI.favorite(actor, activity_id) do {:ok, _activity} <- CommonAPI.favorite(actor, activity_id) do
:ok :ok
else else
{:error, _} -> {:error, _} ->
insert_activity("like", visibility, group, user, friends, non_friends, opts) insert_activity(:like, visibility, group, users, opts)
nil -> nil ->
Process.sleep(15) Process.sleep(15)
insert_activity("like", visibility, group, user, friends, non_friends, opts) insert_activity(:like, visibility, group, users, opts)
end end
end end
defp insert_activity("reblog", visibility, group, user, friends, non_friends, opts) do defp insert_activity(:reblog, visibility, group, users, opts) do
actor = get_actor(group, user, friends, non_friends) actor = get_actor(group, users)
with activity_id when not is_nil(activity_id) <- get_random_create_activity_id(), with activity_id when not is_nil(activity_id) <- get_random_create_activity_id(),
{:ok, _activity, _object} <- CommonAPI.repeat(activity_id, actor) do {:ok, _activity} <- CommonAPI.repeat(activity_id, actor) do
:ok :ok
else else
{:error, _} -> {:error, _} ->
insert_activity("reblog", visibility, group, user, friends, non_friends, opts) insert_activity(:reblog, visibility, group, users, opts)
nil -> nil ->
Process.sleep(15) Process.sleep(15)
insert_activity("reblog", visibility, group, user, friends, non_friends, opts) insert_activity(:reblog, visibility, group, users, opts)
end end
end end
defp insert_activity("simple_thread", visibility, group, user, friends, non_friends, _opts) defp insert_activity(:simple_thread, "direct", group, users, _opts) do
when visibility in ["public", "unlisted", "private"] do actor = get_actor(group, users)
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)
tasks = get_reply_tasks("direct", group) tasks = get_reply_tasks("direct", group)
list = list =
case group do case group do
"non_friends" -> :user ->
Enum.take(non_friends, 3) group = Enum.random(@friends_groups)
Enum.take(users[group], 3)
_ -> _ ->
Enum.take(friends, 3) Enum.take(users[group], 3)
end end
data = Enum.map(list, &("@" <> &1.nickname)) data = Enum.map(list, &("@" <> &1.nickname))
@ -339,40 +364,30 @@ defp insert_activity("simple_thread", "direct", group, user, friends, non_friend
visibility: "direct" visibility: "direct"
}) })
acc = {activity.id, ["@" <> user.nickname | data] ++ ["reply to status"]} acc = {activity.id, ["@" <> users[:user].nickname | data] ++ ["reply to status"]}
insert_direct_replies(tasks, user, list, acc) insert_direct_replies(tasks, users[:user], list, acc)
end end
defp insert_activity("remote", _, "user", _, _, _, _), do: :ok defp insert_activity(:simple_thread, visibility, group, users, _opts) do
actor = get_actor(group, users)
tasks = get_reply_tasks(visibility, group)
defp insert_activity("remote", visibility, group, user, _friends, _non_friends, opts) do {:ok, activity} =
remote_friends = CommonAPI.post(users[:user], %{status: "Simple status", visibility: visibility})
Users.get_users(user, limit: opts[:friends_used], local: :external, friends?: true)
remote_non_friends = acc = {activity.id, ["@" <> actor.nickname, "reply to status"]}
Users.get_users(user, limit: opts[:non_friends_used], local: :external, friends?: false) insert_replies(tasks, visibility, users, acc)
actor = get_actor(group, user, remote_friends, remote_non_friends)
{act_data, obj_data} = prepare_activity_data(actor, visibility, user)
{activity_data, object_data} = other_data(actor)
activity_data
|> Map.merge(act_data)
|> Map.put("object", Map.merge(object_data, obj_data))
|> Pleroma.Web.ActivityPub.ActivityPub.insert(false)
end end
defp get_actor("user", user, _friends, _non_friends), do: user defp get_actor(:user, %{user: user}), do: user
defp get_actor("friends", _user, friends, _non_friends), do: Enum.random(friends) defp get_actor(group, users), do: Enum.random(users[group])
defp get_actor("non_friends", _user, _friends, non_friends), do: Enum.random(non_friends)
defp other_data(actor) do defp other_data(actor, content) do
%{host: host} = URI.parse(actor.ap_id) %{host: host} = URI.parse(actor.ap_id)
datetime = DateTime.utc_now() datetime = DateTime.utc_now()
context_id = "http://#{host}:4000/contexts/#{UUID.generate()}" context_id = "https://#{host}/contexts/#{UUID.generate()}"
activity_id = "http://#{host}:4000/activities/#{UUID.generate()}" activity_id = "https://#{host}/activities/#{UUID.generate()}"
object_id = "http://#{host}:4000/objects/#{UUID.generate()}" object_id = "https://#{host}/objects/#{UUID.generate()}"
activity_data = %{ activity_data = %{
"actor" => actor.ap_id, "actor" => actor.ap_id,
@ -389,7 +404,7 @@ defp other_data(actor) do
"attributedTo" => actor.ap_id, "attributedTo" => actor.ap_id,
"bcc" => [], "bcc" => [],
"bto" => [], "bto" => [],
"content" => "Remote post", "content" => content,
"context" => context_id, "context" => context_id,
"conversation" => context_id, "conversation" => context_id,
"emoji" => %{}, "emoji" => %{},
@ -475,51 +490,65 @@ defp prepare_activity_data(_actor, "direct", mention) do
{act_data, obj_data} {act_data, obj_data}
end end
defp get_reply_tasks("public", "user"), do: ~w(friend non_friend user) defp get_reply_tasks("public", :user) do
defp get_reply_tasks("public", "friends"), do: ~w(non_friend user friend) [:friends_local, :friends_remote, :non_friends_local, :non_friends_remote, :user]
defp get_reply_tasks("public", "non_friends"), do: ~w(user friend non_friend) end
defp get_reply_tasks(visibility, "user") when visibility in ["unlisted", "private"], defp get_reply_tasks("public", group) when group in @friends_groups do
do: ~w(friend user friend) [:non_friends_local, :non_friends_remote, :user, :friends_local, :friends_remote]
end
defp get_reply_tasks(visibility, "friends") when visibility in ["unlisted", "private"], defp get_reply_tasks("public", group) when group in @non_friends_groups do
do: ~w(user friend user) [:user, :friends_local, :friends_remote, :non_friends_local, :non_friends_remote]
end
defp get_reply_tasks(visibility, "non_friends") when visibility in ["unlisted", "private"], defp get_reply_tasks(visibility, :user) when visibility in ["unlisted", "private"] do
do: [] [:friends_local, :friends_remote, :user, :friends_local, :friends_remote]
end
defp get_reply_tasks("direct", "user"), do: ~w(friend user friend) defp get_reply_tasks(visibility, group)
defp get_reply_tasks("direct", "friends"), do: ~w(user friend user) when visibility in ["unlisted", "private"] and group in @friends_groups do
defp get_reply_tasks("direct", "non_friends"), do: ~w(user non_friend user) [:user, :friends_remote, :friends_local, :user]
end
defp insert_replies(tasks, visibility, user, friends, non_friends, acc) do defp get_reply_tasks(visibility, group)
when visibility in ["unlisted", "private"] and
group in @non_friends_groups,
do: []
defp get_reply_tasks("direct", :user), do: [:friends_local, :user, :friends_remote]
defp get_reply_tasks("direct", group) when group in @friends_groups,
do: [:user, group, :user]
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 Enum.reduce(tasks, acc, fn
"friend", {id, data} -> :user, {id, data} ->
friend = Enum.random(friends) insert_reply(users[:user], data, id, visibility)
insert_reply(friend, data, id, visibility)
"non_friend", {id, data} -> group, {id, data} ->
non_friend = Enum.random(non_friends) replier = Enum.random(users[group])
insert_reply(non_friend, data, id, visibility) insert_reply(replier, data, id, visibility)
"user", {id, data} ->
insert_reply(user, data, id, visibility)
end) end)
end end
defp insert_direct_replies(tasks, user, list, acc) do defp insert_direct_replies(tasks, user, list, acc) do
Enum.reduce(tasks, acc, fn Enum.reduce(tasks, acc, fn
group, {id, data} when group in ["friend", "non_friend"] -> :user, {id, data} ->
{reply_id, _} = insert_reply(user, List.delete(data, "@" <> user.nickname), id, "direct")
{reply_id, data}
_, {id, data} ->
actor = Enum.random(list) actor = Enum.random(list)
{reply_id, _} = {reply_id, _} =
insert_reply(actor, List.delete(data, "@" <> actor.nickname), id, "direct") insert_reply(actor, List.delete(data, "@" <> actor.nickname), id, "direct")
{reply_id, data} {reply_id, data}
"user", {id, data} ->
{reply_id, _} = insert_reply(user, List.delete(data, "@" <> user.nickname), id, "direct")
{reply_id, data}
end) end)
end end

View file

@ -36,6 +36,7 @@ defp fetch_timelines(user) do
fetch_home_timeline(user) fetch_home_timeline(user)
fetch_direct_timeline(user) fetch_direct_timeline(user)
fetch_public_timeline(user) fetch_public_timeline(user)
fetch_public_timeline(user, :with_blocks)
fetch_public_timeline(user, :local) fetch_public_timeline(user, :local)
fetch_public_timeline(user, :tag) fetch_public_timeline(user, :tag)
fetch_notifications(user) fetch_notifications(user)
@ -227,6 +228,58 @@ defp fetch_public_timeline(user, :only_media) do
fetch_public_timeline(opts, "public timeline only media") fetch_public_timeline(opts, "public timeline only media")
end end
defp fetch_public_timeline(user, :with_blocks) do
opts = opts_for_public_timeline(user)
remote_non_friends = Agent.get(:non_friends_remote, & &1)
Benchee.run(%{
"public timeline without blocks" => fn ->
ActivityPub.fetch_public_activities(opts)
end
})
Enum.each(remote_non_friends, fn non_friend ->
{:ok, _} = User.block(user, non_friend)
end)
user = User.get_by_id(user.id)
opts = Map.put(opts, "blocking_user", user)
Benchee.run(
%{
"public timeline with user block" => fn ->
ActivityPub.fetch_public_activities(opts)
end
},
)
domains =
Enum.reduce(remote_non_friends, [], fn non_friend, domains ->
{:ok, _user} = User.unblock(user, non_friend)
%{host: host} = URI.parse(non_friend.ap_id)
[host | domains]
end)
domains = Enum.uniq(domains)
Enum.each(domains, fn domain ->
{:ok, _} = User.block_domain(user, domain)
end)
user = User.get_by_id(user.id)
opts = Map.put(opts, "blocking_user", user)
Benchee.run(
%{
"public timeline with domain block" => fn opts ->
ActivityPub.fetch_public_activities(opts)
end
}
)
end
defp fetch_public_timeline(opts, title) when is_binary(title) do defp fetch_public_timeline(opts, title) when is_binary(title) do
first_page_last = ActivityPub.fetch_public_activities(opts) |> List.last() first_page_last = ActivityPub.fetch_public_activities(opts) |> List.last()

View file

@ -27,7 +27,7 @@ def generate(opts \\ []) do
make_friends(main_user, opts[:friends]) make_friends(main_user, opts[:friends])
Repo.get(User, main_user.id) User.get_by_id(main_user.id)
end end
def generate_users(max) do def generate_users(max) do
@ -166,4 +166,24 @@ defp run_stream(users, main_user) do
) )
|> Stream.run() |> Stream.run()
end end
@spec prepare_users(User.t(), keyword()) :: map()
def prepare_users(user, opts) do
friends_limit = opts[:friends_used]
non_friends_limit = opts[:non_friends_used]
%{
user: user,
friends_local: fetch_users(user, friends_limit, :local, true),
friends_remote: fetch_users(user, friends_limit, :external, true),
non_friends_local: fetch_users(user, non_friends_limit, :local, false),
non_friends_remote: fetch_users(user, non_friends_limit, :external, false)
}
end
defp fetch_users(user, limit, local, friends?) do
user
|> get_users(limit: limit, local: local, friends?: friends?)
|> Enum.shuffle()
end
end end

View file

@ -5,7 +5,6 @@ defmodule Mix.Tasks.Pleroma.Benchmarks.Tags do
import Ecto.Query import Ecto.Query
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.Web.MastodonAPI.TimelineController
def run(_args) do def run(_args) do
Mix.Pleroma.start_pleroma() Mix.Pleroma.start_pleroma()
@ -37,7 +36,7 @@ def run(_args) do
Benchee.run( Benchee.run(
%{ %{
"Hashtag fetching, any" => fn tags -> "Hashtag fetching, any" => fn tags ->
TimelineController.hashtag_fetching( hashtag_fetching(
%{ %{
"any" => tags "any" => tags
}, },
@ -47,7 +46,7 @@ def run(_args) do
end, end,
# Will always return zero results because no overlapping hashtags are generated. # Will always return zero results because no overlapping hashtags are generated.
"Hashtag fetching, all" => fn tags -> "Hashtag fetching, all" => fn tags ->
TimelineController.hashtag_fetching( hashtag_fetching(
%{ %{
"all" => tags "all" => tags
}, },
@ -67,7 +66,7 @@ def run(_args) do
Benchee.run( Benchee.run(
%{ %{
"Hashtag fetching" => fn tag -> "Hashtag fetching" => fn tag ->
TimelineController.hashtag_fetching( hashtag_fetching(
%{ %{
"tag" => tag "tag" => tag
}, },
@ -80,4 +79,35 @@ def run(_args) do
time: 5 time: 5
) )
end end
defp hashtag_fetching(params, user, local_only) do
tags =
[params["tag"], params["any"]]
|> List.flatten()
|> Enum.uniq()
|> Enum.filter(& &1)
|> Enum.map(&String.downcase(&1))
tag_all =
params
|> Map.get("all", [])
|> Enum.map(&String.downcase(&1))
tag_reject =
params
|> Map.get("none", [])
|> Enum.map(&String.downcase(&1))
_activities =
params
|> Map.put("type", "Create")
|> Map.put("local_only", local_only)
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)
|> Map.put("tag", tags)
|> Map.put("tag_all", tag_all)
|> Map.put("tag_reject", tag_reject)
|> Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities()
end
end end

View file

@ -184,7 +184,7 @@
name: "Pleroma", name: "Pleroma",
email: "example@example.com", email: "example@example.com",
notify_email: "noreply@example.com", notify_email: "noreply@example.com",
description: "A Pleroma instance, an alternative fediverse server", description: "Pleroma: An efficient and flexible fediverse server",
background_image: "/images/city.jpg", background_image: "/images/city.jpg",
limit: 5_000, limit: 5_000,
chat_limit: 5_000, chat_limit: 5_000,

View file

@ -547,7 +547,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
```json ```json
{ {
"totalReports" : 1, "total" : 1,
"reports": [ "reports": [
{ {
"account": { "account": {
@ -768,7 +768,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- 400 Bad Request `"Invalid parameters"` when `status` is missing - 400 Bad Request `"Invalid parameters"` when `status` is missing
- On success: `204`, empty response - On success: `204`, empty response
## `POST /api/pleroma/admin/reports/:report_id/notes/:id` ## `DELETE /api/pleroma/admin/reports/:report_id/notes/:id`
### Delete report note ### Delete report note

View file

@ -42,6 +42,12 @@ Feel free to contact us to be added to this list!
- Platforms: SailfishOS - Platforms: SailfishOS
- Features: No Streaming - Features: No Streaming
### Husky
- Source code: <https://git.mentality.rip/FWGS/Husky>
- Contact: [@Husky@enigmatic.observer](https://enigmatic.observer/users/Husky)
- Platforms: Android
- Features: No Streaming, Emoji Reactions, Text Formatting, FE Stickers
### Nekonium ### Nekonium
- Homepage: [F-Droid Repository](https://repo.gdgd.jp.net/), [Google Play](https://play.google.com/store/apps/details?id=com.apps.nekonium), [Amazon](https://www.amazon.co.jp/dp/B076FXPRBC/) - Homepage: [F-Droid Repository](https://repo.gdgd.jp.net/), [Google Play](https://play.google.com/store/apps/details?id=com.apps.nekonium), [Amazon](https://www.amazon.co.jp/dp/B076FXPRBC/)
- Source: <https://gogs.gdgd.jp.net/lin/nekonium> - Source: <https://gogs.gdgd.jp.net/lin/nekonium>

View file

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

View file

@ -17,14 +17,6 @@ def append_uri_params(uri, appended_params) do
|> URI.to_string() |> URI.to_string()
end end
def append_param_if_present(%{} = params, param_name, param_value) do
if param_value do
Map.put(params, param_name, param_value)
else
params
end
end
def maybe_add_base("/" <> uri, base), do: Path.join([base, uri]) def maybe_add_base("/" <> uri, base), do: Path.join([base, uri])
def maybe_add_base(uri, _base), do: uri def maybe_add_base(uri, _base), do: uri
end end

15
lib/pleroma/maps.ex Normal file
View file

@ -0,0 +1,15 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Maps do
def put_if_present(map, key, value, value_function \\ &{:ok, &1}) when is_map(map) do
with false <- is_nil(key),
false <- is_nil(value),
{:ok, new_value} <- value_function.(value) do
Map.put(map, key, new_value)
else
_ -> map
end
end
end

View file

@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.Constants alias Pleroma.Constants
alias Pleroma.Conversation alias Pleroma.Conversation
alias Pleroma.Conversation.Participation alias Pleroma.Conversation.Participation
alias Pleroma.Maps
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Object.Containment alias Pleroma.Object.Containment
@ -19,7 +20,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.MRF alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Streamer alias Pleroma.Web.Streamer
alias Pleroma.Web.WebFinger alias Pleroma.Web.WebFinger
alias Pleroma.Workers.BackgroundWorker alias Pleroma.Workers.BackgroundWorker
@ -168,12 +168,7 @@ def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when
}) })
# Splice in the child object if we have one. # Splice in the child object if we have one.
activity = activity = Maps.put_if_present(activity, :object, object)
if not is_nil(object) do
Map.put(activity, :object, object)
else
activity
end
BackgroundWorker.enqueue("fetch_data_for_activity", %{"activity_id" => activity.id}) BackgroundWorker.enqueue("fetch_data_for_activity", %{"activity_id" => activity.id})
@ -335,7 +330,7 @@ def accept_or_reject(type, %{to: to, actor: actor, object: object} = params) do
with data <- with data <-
%{"to" => to, "type" => type, "actor" => actor.ap_id, "object" => object} %{"to" => to, "type" => type, "actor" => actor.ap_id, "object" => object}
|> Utils.maybe_put("id", activity_id), |> Maps.put_if_present("id", activity_id),
{:ok, activity} <- insert(data, local), {:ok, activity} <- insert(data, local),
_ <- notify_and_stream(activity), _ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
@ -355,7 +350,7 @@ def update(%{to: to, cc: cc, actor: actor, object: object} = params) do
"actor" => actor, "actor" => actor,
"object" => object "object" => object
}, },
data <- Utils.maybe_put(data, "id", activity_id), data <- Maps.put_if_present(data, "id", activity_id),
{:ok, activity} <- insert(data, local), {:ok, activity} <- insert(data, local),
_ <- notify_and_stream(activity), _ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
@ -945,6 +940,12 @@ defp restrict_blocked(query, %{"blocking_user" => %User{} = user} = opts) do
[activity, object: o] in query, [activity, object: o] in query,
where: fragment("not (? = ANY(?))", activity.actor, ^blocked_ap_ids), where: fragment("not (? = ANY(?))", activity.actor, ^blocked_ap_ids),
where: fragment("not (? && ?)", activity.recipients, ^blocked_ap_ids), where: fragment("not (? && ?)", activity.recipients, ^blocked_ap_ids),
where:
fragment(
"recipients_contain_blocked_domains(?, ?) = false",
activity.recipients,
^domain_blocks
),
where: where:
fragment( fragment(
"not (?->>'type' = 'Announce' and ?->'to' \\?| ?)", "not (?->>'type' = 'Announce' and ?->'to' \\?| ?)",
@ -1241,12 +1242,7 @@ def fetch_activities_bounded(
@spec upload(Upload.source(), keyword()) :: {:ok, Object.t()} | {:error, any()} @spec upload(Upload.source(), keyword()) :: {:ok, Object.t()} | {:error, any()}
def upload(file, opts \\ []) do def upload(file, opts \\ []) do
with {:ok, data} <- Upload.store(file, opts) do with {:ok, data} <- Upload.store(file, opts) do
obj_data = obj_data = Maps.put_if_present(data, "actor", opts[:actor])
if opts[:actor] do
Map.put(data, "actor", opts[:actor])
else
data
end
Repo.insert(%Object{data: obj_data}) Repo.insert(%Object{data: obj_data})
end end

View file

@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
alias Pleroma.EarmarkRenderer alias Pleroma.EarmarkRenderer
alias Pleroma.FollowingRelationship alias Pleroma.FollowingRelationship
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Maps
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Object.Containment alias Pleroma.Object.Containment
alias Pleroma.Repo alias Pleroma.Repo
@ -209,12 +210,6 @@ def fix_context(object) do
|> Map.put("conversation", context) |> Map.put("conversation", context)
end end
defp add_if_present(map, _key, nil), do: map
defp add_if_present(map, key, value) do
Map.put(map, key, value)
end
def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
attachments = attachments =
Enum.map(attachment, fn data -> Enum.map(attachment, fn data ->
@ -242,13 +237,13 @@ def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachm
attachment_url = attachment_url =
%{"href" => href} %{"href" => href}
|> add_if_present("mediaType", media_type) |> Maps.put_if_present("mediaType", media_type)
|> add_if_present("type", Map.get(url || %{}, "type")) |> Maps.put_if_present("type", Map.get(url || %{}, "type"))
%{"url" => [attachment_url]} %{"url" => [attachment_url]}
|> add_if_present("mediaType", media_type) |> Maps.put_if_present("mediaType", media_type)
|> add_if_present("type", data["type"]) |> Maps.put_if_present("type", data["type"])
|> add_if_present("name", data["name"]) |> Maps.put_if_present("name", data["name"])
end) end)
Map.put(object, "attachment", attachments) Map.put(object, "attachment", attachments)

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
alias Ecto.UUID alias Ecto.UUID
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Config alias Pleroma.Config
alias Pleroma.Maps
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
@ -307,7 +308,7 @@ def make_like_data(
"cc" => cc, "cc" => cc,
"context" => object.data["context"] "context" => object.data["context"]
} }
|> maybe_put("id", activity_id) |> Maps.put_if_present("id", activity_id)
end end
def make_emoji_reaction_data(user, object, emoji, activity_id) do def make_emoji_reaction_data(user, object, emoji, activity_id) do
@ -477,7 +478,7 @@ def make_follow_data(
"object" => followed_id, "object" => followed_id,
"state" => "pending" "state" => "pending"
} }
|> maybe_put("id", activity_id) |> Maps.put_if_present("id", activity_id)
end end
def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do
@ -546,7 +547,7 @@ def make_announce_data(
"cc" => [], "cc" => [],
"context" => object.data["context"] "context" => object.data["context"]
} }
|> maybe_put("id", activity_id) |> Maps.put_if_present("id", activity_id)
end end
def make_announce_data( def make_announce_data(
@ -563,7 +564,7 @@ def make_announce_data(
"cc" => [Pleroma.Constants.as_public()], "cc" => [Pleroma.Constants.as_public()],
"context" => object.data["context"] "context" => object.data["context"]
} }
|> maybe_put("id", activity_id) |> Maps.put_if_present("id", activity_id)
end end
def make_undo_data( def make_undo_data(
@ -582,7 +583,7 @@ def make_undo_data(
"cc" => [Pleroma.Constants.as_public()], "cc" => [Pleroma.Constants.as_public()],
"context" => context "context" => context
} }
|> maybe_put("id", activity_id) |> Maps.put_if_present("id", activity_id)
end end
@spec add_announce_to_object(Activity.t(), Object.t()) :: @spec add_announce_to_object(Activity.t(), Object.t()) ::
@ -627,7 +628,7 @@ def make_unfollow_data(follower, followed, follow_activity, activity_id) do
"to" => [followed.ap_id], "to" => [followed.ap_id],
"object" => follow_activity.data "object" => follow_activity.data
} }
|> maybe_put("id", activity_id) |> Maps.put_if_present("id", activity_id)
end end
#### Block-related helpers #### Block-related helpers
@ -650,7 +651,7 @@ def make_block_data(blocker, blocked, activity_id) do
"to" => [blocked.ap_id], "to" => [blocked.ap_id],
"object" => blocked.ap_id "object" => blocked.ap_id
} }
|> maybe_put("id", activity_id) |> Maps.put_if_present("id", activity_id)
end end
#### Create-related helpers #### Create-related helpers
@ -740,6 +741,7 @@ defp build_flag_object(_), do: []
def get_reports(params, page, page_size) do def get_reports(params, page, page_size) do
params = params =
params params
|> Map.new(fn {key, value} -> {to_string(key), value} end)
|> Map.put("type", "Flag") |> Map.put("type", "Flag")
|> Map.put("skip_preload", true) |> Map.put("skip_preload", true)
|> Map.put("preload_report_notes", true) |> Map.put("preload_report_notes", true)
@ -870,7 +872,4 @@ def get_existing_votes(actor, %{data: %{"id" => id}}) do
|> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data)) |> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data))
|> Repo.all() |> Repo.all()
end end
def maybe_put(map, _key, nil), do: map
def maybe_put(map, key, value), do: Map.put(map, key, value)
end end

View file

@ -7,38 +7,25 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
import Pleroma.Web.ControllerHelper, only: [json_response: 3] import Pleroma.Web.ControllerHelper, only: [json_response: 3]
alias Pleroma.Activity
alias Pleroma.Config alias Pleroma.Config
alias Pleroma.ConfigDB
alias Pleroma.MFA alias Pleroma.MFA
alias Pleroma.ModerationLog alias Pleroma.ModerationLog
alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.ReportNote
alias Pleroma.Stats alias Pleroma.Stats
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserInviteToken
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Pipeline alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.AdminAPI alias Pleroma.Web.AdminAPI
alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.AdminAPI.ConfigView
alias Pleroma.Web.AdminAPI.ModerationLogView alias Pleroma.Web.AdminAPI.ModerationLogView
alias Pleroma.Web.AdminAPI.Report
alias Pleroma.Web.AdminAPI.ReportView
alias Pleroma.Web.AdminAPI.Search alias Pleroma.Web.AdminAPI.Search
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Endpoint alias Pleroma.Web.Endpoint
alias Pleroma.Web.MastodonAPI
alias Pleroma.Web.MastodonAPI.AppView
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.Router alias Pleroma.Web.Router
require Logger require Logger
@descriptions Pleroma.Docs.JSON.compile()
@users_page_size 50 @users_page_size 50
plug( plug(
@ -69,32 +56,12 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
] ]
) )
plug(OAuthScopesPlug, %{scopes: ["read:invites"], admin: true} when action == :invites)
plug(
OAuthScopesPlug,
%{scopes: ["write:invites"], admin: true}
when action in [:create_invite_token, :revoke_invite, :email_invite]
)
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["write:follows"], admin: true} %{scopes: ["write:follows"], admin: true}
when action in [:user_follow, :user_unfollow, :relay_follow, :relay_unfollow] when action in [:user_follow, :user_unfollow, :relay_follow, :relay_unfollow]
) )
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( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["read:statuses"], admin: true} %{scopes: ["read:statuses"], admin: true}
@ -105,11 +72,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["read"], admin: true} %{scopes: ["read"], admin: true}
when action in [ when action in [
:config_show,
:list_log, :list_log,
:stats, :stats,
:relay_list, :relay_list,
:config_descriptions,
:need_reboot :need_reboot
] ]
) )
@ -119,13 +84,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
%{scopes: ["write"], admin: true} %{scopes: ["write"], admin: true}
when action in [ when action in [
:restart, :restart,
:config_update,
:resend_confirmation_email, :resend_confirmation_email,
:confirm_email, :confirm_email,
:oauth_app_create,
:oauth_app_list,
:oauth_app_update,
:oauth_app_delete,
:reload_emoji :reload_emoji
] ]
) )
@ -294,7 +254,7 @@ def list_user_statuses(conn, %{"nickname" => nickname} = params) do
}) })
conn conn
|> put_view(MastodonAPI.StatusView) |> put_view(AdminAPI.StatusView)
|> render("index.json", %{activities: activities, as: :activity}) |> render("index.json", %{activities: activities, as: :activity})
else else
_ -> {:error, :not_found} _ -> {:error, :not_found}
@ -575,69 +535,6 @@ def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target})
end end
end end
@doc "Sends registration invite via email"
def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do
with {_, false} <- {:registrations_open, Config.get([:instance, :registrations_open])},
{_, true} <- {:invites_enabled, Config.get([:instance, :invites_enabled])},
{:ok, invite_token} <- UserInviteToken.create_invite(),
email <-
Pleroma.Emails.UserEmail.user_invitation_email(
user,
invite_token,
email,
params["name"]
),
{:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do
json_response(conn, :no_content, "")
else
{:registrations_open, _} ->
{:error, "To send invites you need to set the `registrations_open` option to false."}
{:invites_enabled, _} ->
{:error, "To send invites you need to set the `invites_enabled` option to true."}
end
end
@doc "Create an account registration invite token"
def create_invite_token(conn, params) do
opts = %{}
opts =
if params["max_use"],
do: Map.put(opts, :max_use, params["max_use"]),
else: opts
opts =
if params["expires_at"],
do: Map.put(opts, :expires_at, params["expires_at"]),
else: opts
{:ok, invite} = UserInviteToken.create_invite(opts)
json(conn, AccountView.render("invite.json", %{invite: invite}))
end
@doc "Get list of created invites"
def invites(conn, _params) do
invites = UserInviteToken.list_invites()
conn
|> put_view(AccountView)
|> render("invites.json", %{invites: invites})
end
@doc "Revokes invite by token"
def revoke_invite(conn, %{"token" => token}) do
with {:ok, invite} <- UserInviteToken.find_by_token(token),
{:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do
conn
|> put_view(AccountView)
|> render("invite.json", %{invite: updated_invite})
else
nil -> {:error, :not_found}
end
end
@doc "Get a password reset token (base64 string) for given nickname" @doc "Get a password reset token (base64 string) for given nickname"
def get_password_reset(conn, %{"nickname" => nickname}) do def get_password_reset(conn, %{"nickname" => nickname}) do
(%User{local: true} = user) = User.get_cached_by_nickname(nickname) (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
@ -724,85 +621,6 @@ def update_user_credentials(
end end
end end
def list_reports(conn, params) do
{page, page_size} = page_params(params)
reports = Utils.get_reports(params, page, page_size)
conn
|> put_view(ReportView)
|> render("index.json", %{reports: reports})
end
def report_show(conn, %{"id" => id}) do
with %Activity{} = report <- Activity.get_by_id(id) do
conn
|> put_view(ReportView)
|> render("show.json", Report.extract_report_info(report))
else
_ -> {:error, :not_found}
end
end
def reports_update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) do
result =
reports
|> Enum.map(fn report ->
with {:ok, activity} <- CommonAPI.update_report_state(report["id"], report["state"]) do
ModerationLog.insert_log(%{
action: "report_update",
actor: admin,
subject: activity
})
activity
else
{:error, message} -> %{id: report["id"], error: message}
end
end)
case Enum.any?(result, &Map.has_key?(&1, :error)) do
true -> json_response(conn, :bad_request, result)
false -> json_response(conn, :no_content, "")
end
end
def report_notes_create(%{assigns: %{user: user}} = conn, %{
"id" => report_id,
"content" => content
}) do
with {:ok, _} <- ReportNote.create(user.id, report_id, content) do
ModerationLog.insert_log(%{
action: "report_note",
actor: user,
subject: Activity.get_by_id(report_id),
text: content
})
json_response(conn, :no_content, "")
else
_ -> json_response(conn, :bad_request, "")
end
end
def report_notes_delete(%{assigns: %{user: user}} = conn, %{
"id" => note_id,
"report_id" => report_id
}) do
with {:ok, note} <- ReportNote.destroy(note_id) do
ModerationLog.insert_log(%{
action: "report_note_delete",
actor: user,
subject: Activity.get_by_id(report_id),
text: note.content
})
json_response(conn, :no_content, "")
else
_ -> json_response(conn, :bad_request, "")
end
end
def list_log(conn, params) do def list_log(conn, params) do
{page, page_size} = page_params(params) {page, page_size} = page_params(params)
@ -821,105 +639,6 @@ def list_log(conn, params) do
|> render("index.json", %{log: log}) |> render("index.json", %{log: log})
end end
def config_descriptions(conn, _params) do
descriptions = Enum.filter(@descriptions, &whitelisted_config?/1)
json(conn, descriptions)
end
def config_show(conn, %{"only_db" => true}) do
with :ok <- configurable_from_database() do
configs = Pleroma.Repo.all(ConfigDB)
conn
|> put_view(ConfigView)
|> render("index.json", %{configs: configs})
end
end
def config_show(conn, _params) do
with :ok <- configurable_from_database() do
configs = ConfigDB.get_all_as_keyword()
merged =
Config.Holder.default_config()
|> ConfigDB.merge(configs)
|> Enum.map(fn {group, values} ->
Enum.map(values, fn {key, value} ->
db =
if configs[group][key] do
ConfigDB.get_db_keys(configs[group][key], key)
end
db_value = configs[group][key]
merged_value =
if !is_nil(db_value) and Keyword.keyword?(db_value) and
ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do
ConfigDB.merge_group(group, key, value, db_value)
else
value
end
setting = %{
group: ConfigDB.convert(group),
key: ConfigDB.convert(key),
value: ConfigDB.convert(merged_value)
}
if db, do: Map.put(setting, :db, db), else: setting
end)
end)
|> List.flatten()
json(conn, %{configs: merged, need_reboot: Restarter.Pleroma.need_reboot?()})
end
end
def config_update(conn, %{"configs" => configs}) do
with :ok <- configurable_from_database() do
{_errors, results} =
configs
|> Enum.filter(&whitelisted_config?/1)
|> Enum.map(fn
%{"group" => group, "key" => key, "delete" => true} = params ->
ConfigDB.delete(%{group: group, key: key, subkeys: params["subkeys"]})
%{"group" => group, "key" => key, "value" => value} ->
ConfigDB.update_or_create(%{group: group, key: key, value: value})
end)
|> Enum.split_with(fn result -> elem(result, 0) == :error end)
{deleted, updated} =
results
|> Enum.map(fn {:ok, config} ->
Map.put(config, :db, ConfigDB.get_db_keys(config))
end)
|> Enum.split_with(fn config ->
Ecto.get_meta(config, :state) == :deleted
end)
Config.TransferTask.load_and_update_env(deleted, false)
if !Restarter.Pleroma.need_reboot?() do
changed_reboot_settings? =
(updated ++ deleted)
|> Enum.any?(fn config ->
group = ConfigDB.from_string(config.group)
key = ConfigDB.from_string(config.key)
value = ConfigDB.from_binary(config.value)
Config.TransferTask.pleroma_need_restart?(group, key, value)
end)
if changed_reboot_settings?, do: Restarter.Pleroma.need_reboot()
end
conn
|> put_view(ConfigView)
|> render("index.json", %{configs: updated, need_reboot: Restarter.Pleroma.need_reboot?()})
end
end
def restart(conn, _params) do def restart(conn, _params) do
with :ok <- configurable_from_database() do with :ok <- configurable_from_database() do
Restarter.Pleroma.restart(Config.get(:env), 50) Restarter.Pleroma.restart(Config.get(:env), 50)
@ -940,28 +659,6 @@ defp configurable_from_database do
end end
end end
defp whitelisted_config?(group, key) do
if whitelisted_configs = Config.get(:database_config_whitelist) do
Enum.any?(whitelisted_configs, fn
{whitelisted_group} ->
group == inspect(whitelisted_group)
{whitelisted_group, whitelisted_key} ->
group == inspect(whitelisted_group) && key == inspect(whitelisted_key)
end)
else
true
end
end
defp whitelisted_config?(%{"group" => group, "key" => key}) do
whitelisted_config?(group, key)
end
defp whitelisted_config?(%{:group => group} = config) do
whitelisted_config?(group, config[:key])
end
def reload_emoji(conn, _params) do def reload_emoji(conn, _params) do
Pleroma.Emoji.reload() Pleroma.Emoji.reload()
@ -996,83 +693,6 @@ def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" =
conn |> json("") conn |> json("")
end end
def oauth_app_create(conn, params) do
params =
if params["name"] do
Map.put(params, "client_name", params["name"])
else
params
end
result =
case App.create(params) do
{:ok, app} ->
AppView.render("show.json", %{app: app, admin: true})
{:error, changeset} ->
App.errors(changeset)
end
json(conn, result)
end
def oauth_app_update(conn, params) do
params =
if params["name"] do
Map.put(params, "client_name", params["name"])
else
params
end
with {:ok, app} <- App.update(params) do
json(conn, AppView.render("show.json", %{app: app, admin: true}))
else
{:error, changeset} ->
json(conn, App.errors(changeset))
nil ->
json_response(conn, :bad_request, "")
end
end
def oauth_app_list(conn, params) do
{page, page_size} = page_params(params)
search_params = %{
client_name: params["name"],
client_id: params["client_id"],
page: page,
page_size: page_size
}
search_params =
if Map.has_key?(params, "trusted") do
Map.put(search_params, :trusted, params["trusted"])
else
search_params
end
with {:ok, apps, count} <- App.search(search_params) do
json(
conn,
AppView.render("index.json",
apps: apps,
count: count,
page_size: page_size,
admin: true
)
)
end
end
def oauth_app_delete(conn, params) do
with {:ok, _app} <- App.destroy(params["id"]) do
json_response(conn, :no_content, "")
else
_ -> json_response(conn, :bad_request, "")
end
end
def stats(conn, _) do def stats(conn, _) do
count = Stats.get_status_visibility_count() count = Stats.get_status_visibility_count()

View file

@ -0,0 +1,152 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.ConfigController do
use Pleroma.Web, :controller
alias Pleroma.Config
alias Pleroma.ConfigDB
alias Pleroma.Plugs.OAuthScopesPlug
@descriptions Pleroma.Docs.JSON.compile()
plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(OAuthScopesPlug, %{scopes: ["write"], admin: true} when action == :update)
plug(
OAuthScopesPlug,
%{scopes: ["read"], admin: true}
when action in [:show, :descriptions]
)
action_fallback(Pleroma.Web.AdminAPI.FallbackController)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.ConfigOperation
def descriptions(conn, _params) do
descriptions = Enum.filter(@descriptions, &whitelisted_config?/1)
json(conn, descriptions)
end
def show(conn, %{only_db: true}) do
with :ok <- configurable_from_database() do
configs = Pleroma.Repo.all(ConfigDB)
render(conn, "index.json", %{configs: configs})
end
end
def show(conn, _params) do
with :ok <- configurable_from_database() do
configs = ConfigDB.get_all_as_keyword()
merged =
Config.Holder.default_config()
|> ConfigDB.merge(configs)
|> Enum.map(fn {group, values} ->
Enum.map(values, fn {key, value} ->
db =
if configs[group][key] do
ConfigDB.get_db_keys(configs[group][key], key)
end
db_value = configs[group][key]
merged_value =
if not is_nil(db_value) and Keyword.keyword?(db_value) and
ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do
ConfigDB.merge_group(group, key, value, db_value)
else
value
end
%{
group: ConfigDB.convert(group),
key: ConfigDB.convert(key),
value: ConfigDB.convert(merged_value)
}
|> Pleroma.Maps.put_if_present(:db, db)
end)
end)
|> List.flatten()
json(conn, %{configs: merged, need_reboot: Restarter.Pleroma.need_reboot?()})
end
end
def update(%{body_params: %{configs: configs}} = conn, _) do
with :ok <- configurable_from_database() do
results =
configs
|> Enum.filter(&whitelisted_config?/1)
|> Enum.map(fn
%{group: group, key: key, delete: true} = params ->
ConfigDB.delete(%{group: group, key: key, subkeys: params[:subkeys]})
%{group: group, key: key, value: value} ->
ConfigDB.update_or_create(%{group: group, key: key, value: value})
end)
|> Enum.reject(fn {result, _} -> result == :error end)
{deleted, updated} =
results
|> Enum.map(fn {:ok, config} ->
Map.put(config, :db, ConfigDB.get_db_keys(config))
end)
|> Enum.split_with(fn config ->
Ecto.get_meta(config, :state) == :deleted
end)
Config.TransferTask.load_and_update_env(deleted, false)
if not Restarter.Pleroma.need_reboot?() do
changed_reboot_settings? =
(updated ++ deleted)
|> Enum.any?(fn config ->
group = ConfigDB.from_string(config.group)
key = ConfigDB.from_string(config.key)
value = ConfigDB.from_binary(config.value)
Config.TransferTask.pleroma_need_restart?(group, key, value)
end)
if changed_reboot_settings?, do: Restarter.Pleroma.need_reboot()
end
render(conn, "index.json", %{
configs: updated,
need_reboot: Restarter.Pleroma.need_reboot?()
})
end
end
defp configurable_from_database do
if Config.get(:configurable_from_database) do
:ok
else
{:error, "To use this endpoint you need to enable configuration from database."}
end
end
defp whitelisted_config?(group, key) do
if whitelisted_configs = Config.get(:database_config_whitelist) do
Enum.any?(whitelisted_configs, fn
{whitelisted_group} ->
group == inspect(whitelisted_group)
{whitelisted_group, whitelisted_key} ->
group == inspect(whitelisted_group) && key == inspect(whitelisted_key)
end)
else
true
end
end
defp whitelisted_config?(%{group: group, key: key}) do
whitelisted_config?(group, key)
end
defp whitelisted_config?(%{group: group} = config) do
whitelisted_config?(group, config[:key])
end
end

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

View file

@ -0,0 +1,77 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.OAuthAppController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Web.OAuth.App
require Logger
plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(:put_view, Pleroma.Web.MastodonAPI.AppView)
plug(
OAuthScopesPlug,
%{scopes: ["write"], admin: true}
when action in [:create, :index, :update, :delete]
)
action_fallback(Pleroma.Web.AdminAPI.FallbackController)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.OAuthAppOperation
def index(conn, params) do
search_params =
params
|> Map.take([:client_id, :page, :page_size, :trusted])
|> Map.put(:client_name, params[:name])
with {:ok, apps, count} <- App.search(search_params) do
render(conn, "index.json",
apps: apps,
count: count,
page_size: params.page_size,
admin: true
)
end
end
def create(%{body_params: params} = conn, _) do
params = Pleroma.Maps.put_if_present(params, :client_name, params[:name])
case App.create(params) do
{:ok, app} ->
render(conn, "show.json", app: app, admin: true)
{:error, changeset} ->
json(conn, App.errors(changeset))
end
end
def update(%{body_params: params} = conn, %{id: id}) do
params = Pleroma.Maps.put_if_present(params, :client_name, params[:name])
with {:ok, app} <- App.update(id, params) do
render(conn, "show.json", app: app, admin: true)
else
{:error, changeset} ->
json(conn, App.errors(changeset))
nil ->
json_response(conn, :bad_request, "")
end
end
def delete(conn, params) do
with {:ok, _app} <- App.destroy(params.id) do
json_response(conn, :no_content, "")
else
_ -> json_response(conn, :bad_request, "")
end
end
end

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

View file

@ -41,9 +41,7 @@ def index(%{assigns: %{user: _admin}} = conn, params) do
def show(conn, %{id: id}) do def show(conn, %{id: id}) do
with %Activity{} = activity <- Activity.get_by_id(id) do with %Activity{} = activity <- Activity.get_by_id(id) do
conn render(conn, "show.json", %{activity: activity})
|> put_view(Pleroma.Web.AdminAPI.StatusView)
|> render("show.json", %{activity: activity})
else else
nil -> {:error, :not_found} nil -> {:error, :not_found}
end end

View file

@ -80,24 +80,6 @@ def render("show.json", %{user: user}) do
} }
end end
def render("invite.json", %{invite: invite}) do
%{
"id" => invite.id,
"token" => invite.token,
"used" => invite.used,
"expires_at" => invite.expires_at,
"uses" => invite.uses,
"max_use" => invite.max_use,
"invite_type" => invite.invite_type
}
end
def render("invites.json", %{invites: invites}) do
%{
invites: render_many(invites, AccountView, "invite.json", as: :invite)
}
end
def render("created.json", %{user: user}) do def render("created.json", %{user: user}) do
%{ %{
type: "success", type: "success",

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

View file

@ -0,0 +1,142 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Admin.ConfigOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.ApiError
import Pleroma.Web.ApiSpec.Helpers
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def show_operation do
%Operation{
tags: ["Admin", "Config"],
summary: "Get list of merged default settings with saved in database",
operationId: "AdminAPI.ConfigController.show",
parameters: [
Operation.parameter(
:only_db,
:query,
%Schema{type: :boolean, default: false},
"Get only saved in database settings"
)
],
security: [%{"oAuth" => ["read"]}],
responses: %{
200 => Operation.response("Config", "application/json", config_response()),
400 => Operation.response("Bad Request", "application/json", ApiError)
}
}
end
def update_operation do
%Operation{
tags: ["Admin", "Config"],
summary: "Update config settings",
operationId: "AdminAPI.ConfigController.update",
security: [%{"oAuth" => ["write"]}],
requestBody:
request_body("Parameters", %Schema{
type: :object,
properties: %{
configs: %Schema{
type: :array,
items: %Schema{
type: :object,
properties: %{
group: %Schema{type: :string},
key: %Schema{type: :string},
value: any(),
delete: %Schema{type: :boolean},
subkeys: %Schema{type: :array, items: %Schema{type: :string}}
}
}
}
}
}),
responses: %{
200 => Operation.response("Config", "application/json", config_response()),
400 => Operation.response("Bad Request", "application/json", ApiError)
}
}
end
def descriptions_operation do
%Operation{
tags: ["Admin", "Config"],
summary: "Get JSON with config descriptions.",
operationId: "AdminAPI.ConfigController.descriptions",
security: [%{"oAuth" => ["read"]}],
responses: %{
200 =>
Operation.response("Config Descriptions", "application/json", %Schema{
type: :array,
items: %Schema{
type: :object,
properties: %{
group: %Schema{type: :string},
key: %Schema{type: :string},
type: %Schema{oneOf: [%Schema{type: :string}, %Schema{type: :array}]},
description: %Schema{type: :string},
children: %Schema{
type: :array,
items: %Schema{
type: :object,
properties: %{
key: %Schema{type: :string},
type: %Schema{oneOf: [%Schema{type: :string}, %Schema{type: :array}]},
description: %Schema{type: :string},
suggestions: %Schema{type: :array}
}
}
}
}
}
}),
400 => Operation.response("Bad Request", "application/json", ApiError)
}
}
end
defp any do
%Schema{
oneOf: [
%Schema{type: :array},
%Schema{type: :object},
%Schema{type: :string},
%Schema{type: :integer},
%Schema{type: :boolean}
]
}
end
defp config_response do
%Schema{
type: :object,
properties: %{
configs: %Schema{
type: :array,
items: %Schema{
type: :object,
properties: %{
group: %Schema{type: :string},
key: %Schema{type: :string},
value: any()
}
}
},
need_reboot: %Schema{
type: :boolean,
description:
"If `need_reboot` is `true`, instance must be restarted, so reboot time settings can take effect"
}
}
}
end
end

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

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

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

View file

@ -74,7 +74,7 @@ def show_operation do
parameters: [id_param()], parameters: [id_param()],
security: [%{"oAuth" => ["read:statuses"]}], security: [%{"oAuth" => ["read:statuses"]}],
responses: %{ responses: %{
200 => Operation.response("Status", "application/json", Status), 200 => Operation.response("Status", "application/json", status()),
404 => Operation.response("Not Found", "application/json", ApiError) 404 => Operation.response("Not Found", "application/json", ApiError)
} }
} }
@ -123,7 +123,7 @@ defp status do
} }
end end
defp admin_account do def admin_account do
%Schema{ %Schema{
type: :object, type: :object,
properties: %{ properties: %{

View file

@ -137,7 +137,7 @@ defp instance do
"background_upload_limit" => 4_000_000, "background_upload_limit" => 4_000_000,
"background_image" => "/static/image.png", "background_image" => "/static/image.png",
"banner_upload_limit" => 4_000_000, "banner_upload_limit" => 4_000_000,
"description" => "A Pleroma instance, an alternative fediverse server", "description" => "Pleroma: An efficient and flexible fediverse server",
"email" => "lain@lain.com", "email" => "lain@lain.com",
"languages" => ["en"], "languages" => ["en"],
"max_toot_chars" => 5000, "max_toot_chars" => 5000,

View file

@ -99,11 +99,6 @@ def try_render(conn, _, _) do
render_error(conn, :not_implemented, "Can't display this activity") render_error(conn, :not_implemented, "Can't display this activity")
end end
@spec put_if_exist(map(), atom() | String.t(), any) :: map()
def put_if_exist(map, _key, nil), do: map
def put_if_exist(map, key, value), do: Map.put(map, key, value)
@doc """ @doc """
Returns true if request specifies to include embedded relationships in account objects. Returns true if request specifies to include embedded relationships in account objects.
May only be used in selected account-related endpoints; has no effect for status- or May only be used in selected account-related endpoints; has no effect for status- or

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

View file

@ -9,14 +9,12 @@ defmodule Pleroma.Web.Feed.TagController do
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.Feed.FeedView alias Pleroma.Web.Feed.FeedView
import Pleroma.Web.ControllerHelper, only: [put_if_exist: 3]
def feed(conn, %{"tag" => raw_tag} = params) do def feed(conn, %{"tag" => raw_tag} = params) do
{format, tag} = parse_tag(raw_tag) {format, tag} = parse_tag(raw_tag)
activities = activities =
%{"type" => ["Create"], "tag" => tag} %{"type" => ["Create"], "tag" => tag}
|> put_if_exist("max_id", params["max_id"]) |> Pleroma.Maps.put_if_present("max_id", params["max_id"])
|> ActivityPub.fetch_public_activities() |> ActivityPub.fetch_public_activities()
conn conn

View file

@ -11,8 +11,6 @@ defmodule Pleroma.Web.Feed.UserController do
alias Pleroma.Web.ActivityPub.ActivityPubController alias Pleroma.Web.ActivityPub.ActivityPubController
alias Pleroma.Web.Feed.FeedView alias Pleroma.Web.Feed.FeedView
import Pleroma.Web.ControllerHelper, only: [put_if_exist: 3]
plug(Pleroma.Plugs.SetFormatPlug when action in [:feed_redirect]) plug(Pleroma.Plugs.SetFormatPlug when action in [:feed_redirect])
action_fallback(:errors) action_fallback(:errors)
@ -55,7 +53,7 @@ def feed(conn, %{"nickname" => nickname} = params) do
"type" => ["Create"], "type" => ["Create"],
"actor_id" => user.ap_id "actor_id" => user.ap_id
} }
|> put_if_exist("max_id", params["max_id"]) |> Pleroma.Maps.put_if_present("max_id", params["max_id"])
|> ActivityPub.fetch_public_or_unlisted_activities() |> ActivityPub.fetch_public_or_unlisted_activities()
conn conn

View file

@ -14,6 +14,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
json_response: 3 json_response: 3
] ]
alias Pleroma.Maps
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Plugs.RateLimiter alias Pleroma.Plugs.RateLimiter
@ -160,23 +161,22 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p
:discoverable :discoverable
] ]
|> Enum.reduce(%{}, fn key, acc -> |> Enum.reduce(%{}, fn key, acc ->
add_if_present(acc, params, key, key, &{:ok, truthy_param?(&1)}) Maps.put_if_present(acc, key, params[key], &{:ok, truthy_param?(&1)})
end) end)
|> add_if_present(params, :display_name, :name) |> Maps.put_if_present(:name, params[:display_name])
|> add_if_present(params, :note, :bio) |> Maps.put_if_present(:bio, params[:note])
|> add_if_present(params, :avatar, :avatar) |> Maps.put_if_present(:avatar, params[:avatar])
|> add_if_present(params, :header, :banner) |> Maps.put_if_present(:banner, params[:header])
|> add_if_present(params, :pleroma_background_image, :background) |> Maps.put_if_present(:background, params[:pleroma_background_image])
|> add_if_present( |> Maps.put_if_present(
params,
:fields_attributes,
:raw_fields, :raw_fields,
params[:fields_attributes],
&{:ok, normalize_fields_attributes(&1)} &{:ok, normalize_fields_attributes(&1)}
) )
|> add_if_present(params, :pleroma_settings_store, :pleroma_settings_store) |> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store])
|> add_if_present(params, :default_scope, :default_scope) |> Maps.put_if_present(:default_scope, params[:default_scope])
|> add_if_present(params["source"], "privacy", :default_scope) |> Maps.put_if_present(:default_scope, params["source"]["privacy"])
|> add_if_present(params, :actor_type, :actor_type) |> Maps.put_if_present(:actor_type, params[:actor_type])
changeset = User.update_changeset(user, user_params) changeset = User.update_changeset(user, user_params)
@ -206,16 +206,6 @@ defp build_update_activity_params(user) do
} }
end end
defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
with true <- is_map(params),
true <- Map.has_key?(params, params_field),
{:ok, new_value} <- value_function.(Map.get(params, params_field)) do
Map.put(map, map_field, new_value)
else
_ -> map
end
end
defp normalize_fields_attributes(fields) do defp normalize_fields_attributes(fields) do
if Enum.all?(fields, &is_tuple/1) do if Enum.all?(fields, &is_tuple/1) do
Enum.map(fields, fn {_, v} -> v end) Enum.map(fields, fn {_, v} -> v end)

View file

@ -113,22 +113,44 @@ defp resource_search(:v2, "hashtags", query, _options) do
query query
|> prepare_tags() |> prepare_tags()
|> Enum.map(fn tag -> |> Enum.map(fn tag ->
tag = String.trim_leading(tag, "#")
%{name: tag, url: tags_path <> tag} %{name: tag, url: tags_path <> tag}
end) end)
end end
defp resource_search(:v1, "hashtags", query, _options) do defp resource_search(:v1, "hashtags", query, _options) do
query prepare_tags(query)
|> prepare_tags()
|> Enum.map(fn tag -> String.trim_leading(tag, "#") end)
end end
defp prepare_tags(query) do defp prepare_tags(query, add_joined_tag \\ true) do
query tags =
|> String.split() query
|> Enum.uniq() |> String.split(~r/[^#\w]+/u, trim: true)
|> Enum.filter(fn tag -> String.starts_with?(tag, "#") end) |> Enum.uniq_by(&String.downcase/1)
explicit_tags = Enum.filter(tags, fn tag -> String.starts_with?(tag, "#") end)
tags =
if Enum.any?(explicit_tags) do
explicit_tags
else
tags
end
tags = Enum.map(tags, fn tag -> String.trim_leading(tag, "#") end)
if Enum.empty?(explicit_tags) && add_joined_tag do
tags
|> Kernel.++([joined_tag(tags)])
|> Enum.uniq_by(&String.downcase/1)
else
tags
end
end
defp joined_tag(tags) do
tags
|> Enum.map(fn tag -> String.capitalize(tag) end)
|> Enum.join()
end end
defp with_fallback(f, fallback \\ []) do defp with_fallback(f, fallback \\ []) do

View file

@ -45,10 +45,6 @@ def render("short.json", %{app: %App{website: webiste, client_name: name}}) do
defp with_vapid_key(data) do defp with_vapid_key(data) do
vapid_key = Application.get_env(:web_push_encryption, :vapid_details, [])[:public_key] vapid_key = Application.get_env(:web_push_encryption, :vapid_details, [])[:public_key]
if vapid_key do Pleroma.Maps.put_if_present(data, "vapid_key", vapid_key)
Map.put(data, "vapid_key", vapid_key)
else
data
end
end end
end end

View file

@ -30,7 +30,7 @@ defp with_media_attachments(data, %{params: %{"media_attachments" => media_attac
defp with_media_attachments(data, _), do: data defp with_media_attachments(data, _), do: data
defp status_params(params) do defp status_params(params) do
data = %{ %{
text: params["status"], text: params["status"],
sensitive: params["sensitive"], sensitive: params["sensitive"],
spoiler_text: params["spoiler_text"], spoiler_text: params["spoiler_text"],
@ -39,10 +39,6 @@ defp status_params(params) do
poll: params["poll"], poll: params["poll"],
in_reply_to_id: params["in_reply_to_id"] in_reply_to_id: params["in_reply_to_id"]
} }
|> Pleroma.Maps.put_if_present(:media_ids, params["media_ids"])
case params["media_ids"] do
nil -> data
media_ids -> Map.put(data, :media_ids, media_ids)
end
end end
end end

View file

@ -25,12 +25,12 @@ defmodule Pleroma.Web.OAuth.App do
timestamps() timestamps()
end end
@spec changeset(App.t(), map()) :: Ecto.Changeset.t() @spec changeset(t(), map()) :: Ecto.Changeset.t()
def changeset(struct, params) do def changeset(struct, params) do
cast(struct, params, [:client_name, :redirect_uris, :scopes, :website, :trusted]) cast(struct, params, [:client_name, :redirect_uris, :scopes, :website, :trusted])
end end
@spec register_changeset(App.t(), map()) :: Ecto.Changeset.t() @spec register_changeset(t(), map()) :: Ecto.Changeset.t()
def register_changeset(struct, params \\ %{}) do def register_changeset(struct, params \\ %{}) do
changeset = changeset =
struct struct
@ -52,18 +52,19 @@ def register_changeset(struct, params \\ %{}) do
end end
end end
@spec create(map()) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()} @spec create(map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
def create(params) do def create(params) do
with changeset <- __MODULE__.register_changeset(%__MODULE__{}, params) do %__MODULE__{}
Repo.insert(changeset) |> register_changeset(params)
end |> Repo.insert()
end end
@spec update(map()) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()} @spec update(pos_integer(), map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
def update(params) do def update(id, params) do
with %__MODULE__{} = app <- Repo.get(__MODULE__, params["id"]), with %__MODULE__{} = app <- Repo.get(__MODULE__, id) do
changeset <- changeset(app, params) do app
Repo.update(changeset) |> changeset(params)
|> Repo.update()
end end
end end
@ -71,7 +72,7 @@ def update(params) do
Gets app by attrs or create new with attrs. Gets app by attrs or create new with attrs.
And updates the scopes if need. And updates the scopes if need.
""" """
@spec get_or_make(map(), list(String.t())) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()} @spec get_or_make(map(), list(String.t())) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
def get_or_make(attrs, scopes) do def get_or_make(attrs, scopes) do
with %__MODULE__{} = app <- Repo.get_by(__MODULE__, attrs) do with %__MODULE__{} = app <- Repo.get_by(__MODULE__, attrs) do
update_scopes(app, scopes) update_scopes(app, scopes)
@ -92,7 +93,7 @@ defp update_scopes(%__MODULE__{} = app, scopes) do
|> Repo.update() |> Repo.update()
end end
@spec search(map()) :: {:ok, [App.t()], non_neg_integer()} @spec search(map()) :: {:ok, [t()], non_neg_integer()}
def search(params) do def search(params) do
query = from(a in __MODULE__) query = from(a in __MODULE__)
@ -128,7 +129,7 @@ def search(params) do
{:ok, Repo.all(query), count} {:ok, Repo.all(query), count}
end end
@spec destroy(pos_integer()) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()} @spec destroy(pos_integer()) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
def destroy(id) do def destroy(id) do
with %__MODULE__{} = app <- Repo.get(__MODULE__, id) do with %__MODULE__{} = app <- Repo.get(__MODULE__, id) do
Repo.delete(app) Repo.delete(app)

View file

@ -6,6 +6,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
alias Pleroma.Helpers.UriHelper alias Pleroma.Helpers.UriHelper
alias Pleroma.Maps
alias Pleroma.MFA alias Pleroma.MFA
alias Pleroma.Plugs.RateLimiter alias Pleroma.Plugs.RateLimiter
alias Pleroma.Registration alias Pleroma.Registration
@ -108,7 +109,7 @@ defp handle_existing_authorization(
if redirect_uri in String.split(app.redirect_uris) do if redirect_uri in String.split(app.redirect_uris) do
redirect_uri = redirect_uri(conn, redirect_uri) redirect_uri = redirect_uri(conn, redirect_uri)
url_params = %{access_token: token.token} url_params = %{access_token: token.token}
url_params = UriHelper.append_param_if_present(url_params, :state, params["state"]) url_params = Maps.put_if_present(url_params, :state, params["state"])
url = UriHelper.append_uri_params(redirect_uri, url_params) url = UriHelper.append_uri_params(redirect_uri, url_params)
redirect(conn, external: url) redirect(conn, external: url)
else else
@ -147,7 +148,7 @@ def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
if redirect_uri in String.split(app.redirect_uris) do if redirect_uri in String.split(app.redirect_uris) do
redirect_uri = redirect_uri(conn, redirect_uri) redirect_uri = redirect_uri(conn, redirect_uri)
url_params = %{code: auth.token} url_params = %{code: auth.token}
url_params = UriHelper.append_param_if_present(url_params, :state, auth_attrs["state"]) url_params = Maps.put_if_present(url_params, :state, auth_attrs["state"])
url = UriHelper.append_uri_params(redirect_uri, url_params) url = UriHelper.append_uri_params(redirect_uri, url_params)
redirect(conn, external: url) redirect(conn, external: url)
else else

View file

@ -164,10 +164,10 @@ defmodule Pleroma.Web.Router do
post("/relay", AdminAPIController, :relay_follow) post("/relay", AdminAPIController, :relay_follow)
delete("/relay", AdminAPIController, :relay_unfollow) delete("/relay", AdminAPIController, :relay_unfollow)
post("/users/invite_token", AdminAPIController, :create_invite_token) post("/users/invite_token", InviteController, :create)
get("/users/invites", AdminAPIController, :invites) get("/users/invites", InviteController, :index)
post("/users/revoke_invite", AdminAPIController, :revoke_invite) post("/users/revoke_invite", InviteController, :revoke)
post("/users/email_invite", AdminAPIController, :email_invite) post("/users/email_invite", InviteController, :email)
get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset) get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset)
patch("/users/force_password_reset", AdminAPIController, :force_password_reset) patch("/users/force_password_reset", AdminAPIController, :force_password_reset)
@ -183,20 +183,20 @@ defmodule Pleroma.Web.Router do
patch("/users/confirm_email", AdminAPIController, :confirm_email) patch("/users/confirm_email", AdminAPIController, :confirm_email)
patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email) patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email)
get("/reports", AdminAPIController, :list_reports) get("/reports", ReportController, :index)
get("/reports/:id", AdminAPIController, :report_show) get("/reports/:id", ReportController, :show)
patch("/reports", AdminAPIController, :reports_update) patch("/reports", ReportController, :update)
post("/reports/:id/notes", AdminAPIController, :report_notes_create) post("/reports/:id/notes", ReportController, :notes_create)
delete("/reports/:report_id/notes/:id", AdminAPIController, :report_notes_delete) delete("/reports/:report_id/notes/:id", ReportController, :notes_delete)
get("/statuses/:id", StatusController, :show) get("/statuses/:id", StatusController, :show)
put("/statuses/:id", StatusController, :update) put("/statuses/:id", StatusController, :update)
delete("/statuses/:id", StatusController, :delete) delete("/statuses/:id", StatusController, :delete)
get("/statuses", StatusController, :index) get("/statuses", StatusController, :index)
get("/config", AdminAPIController, :config_show) get("/config", ConfigController, :show)
post("/config", AdminAPIController, :config_update) post("/config", ConfigController, :update)
get("/config/descriptions", AdminAPIController, :config_descriptions) get("/config/descriptions", ConfigController, :descriptions)
get("/need_reboot", AdminAPIController, :need_reboot) get("/need_reboot", AdminAPIController, :need_reboot)
get("/restart", AdminAPIController, :restart) get("/restart", AdminAPIController, :restart)
@ -205,10 +205,10 @@ defmodule Pleroma.Web.Router do
post("/reload_emoji", AdminAPIController, :reload_emoji) post("/reload_emoji", AdminAPIController, :reload_emoji)
get("/stats", AdminAPIController, :stats) get("/stats", AdminAPIController, :stats)
get("/oauth_app", AdminAPIController, :oauth_app_list) get("/oauth_app", OAuthAppController, :index)
post("/oauth_app", AdminAPIController, :oauth_app_create) post("/oauth_app", OAuthAppController, :create)
patch("/oauth_app/:id", AdminAPIController, :oauth_app_update) patch("/oauth_app/:id", OAuthAppController, :update)
delete("/oauth_app/:id", AdminAPIController, :oauth_app_delete) delete("/oauth_app/:id", OAuthAppController, :delete)
end end
scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do
@ -673,6 +673,8 @@ defmodule Pleroma.Web.Router do
post("/auth/password", MastodonAPI.AuthController, :password_reset) post("/auth/password", MastodonAPI.AuthController, :password_reset)
get("/web/*path", MastoFEController, :index) get("/web/*path", MastoFEController, :index)
get("/embed/:id", EmbedController, :show)
end end
scope "/proxy/", Pleroma.Web.MediaProxy do scope "/proxy/", Pleroma.Web.MediaProxy do

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

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>

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>

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

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
priv/static/embed.css Normal file
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
priv/static/embed.js Normal file
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()
})
})
})()

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

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

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

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

View file

@ -71,10 +71,48 @@ test "search", %{conn: conn} do
get(conn, "/api/v2/search?q=天子") get(conn, "/api/v2/search?q=天子")
|> json_response_and_validate_schema(200) |> json_response_and_validate_schema(200)
assert results["hashtags"] == [
%{"name" => "天子", "url" => "#{Web.base_url()}/tag/天子"}
]
[status] = results["statuses"] [status] = results["statuses"]
assert status["id"] == to_string(activity.id) assert status["id"] == to_string(activity.id)
end end
test "constructs hashtags from search query", %{conn: conn} do
results =
conn
|> get("/api/v2/search?#{URI.encode_query(%{q: "some text with #explicit #hashtags"})}")
|> json_response_and_validate_schema(200)
assert results["hashtags"] == [
%{"name" => "explicit", "url" => "#{Web.base_url()}/tag/explicit"},
%{"name" => "hashtags", "url" => "#{Web.base_url()}/tag/hashtags"}
]
results =
conn
|> get("/api/v2/search?#{URI.encode_query(%{q: "john doe JOHN DOE"})}")
|> json_response_and_validate_schema(200)
assert results["hashtags"] == [
%{"name" => "john", "url" => "#{Web.base_url()}/tag/john"},
%{"name" => "doe", "url" => "#{Web.base_url()}/tag/doe"},
%{"name" => "JohnDoe", "url" => "#{Web.base_url()}/tag/JohnDoe"}
]
results =
conn
|> get("/api/v2/search?#{URI.encode_query(%{q: "accident-prone"})}")
|> json_response_and_validate_schema(200)
assert results["hashtags"] == [
%{"name" => "accident", "url" => "#{Web.base_url()}/tag/accident"},
%{"name" => "prone", "url" => "#{Web.base_url()}/tag/prone"},
%{"name" => "AccidentProne", "url" => "#{Web.base_url()}/tag/AccidentProne"}
]
end
test "excludes a blocked users from search results", %{conn: conn} do test "excludes a blocked users from search results", %{conn: conn} do
user = insert(:user) user = insert(:user)
user_smith = insert(:user, %{nickname: "Agent", name: "I love 2hu"}) user_smith = insert(:user, %{nickname: "Agent", name: "I love 2hu"})
@ -179,7 +217,7 @@ test "search", %{conn: conn} do
[account | _] = results["accounts"] [account | _] = results["accounts"]
assert account["id"] == to_string(user_three.id) assert account["id"] == to_string(user_three.id)
assert results["hashtags"] == [] assert results["hashtags"] == ["2hu"]
[status] = results["statuses"] [status] = results["statuses"]
assert status["id"] == to_string(activity.id) assert status["id"] == to_string(activity.id)

View file

@ -97,6 +97,49 @@ test "the public timeline includes only public statuses for an authenticated use
res_conn = get(conn, "/api/v1/timelines/public") res_conn = get(conn, "/api/v1/timelines/public")
assert length(json_response_and_validate_schema(res_conn, 200)) == 1 assert length(json_response_and_validate_schema(res_conn, 200)) == 1
end end
test "doesn't return replies if follower is posting with blocked user" do
%{conn: conn, user: blocker} = oauth_access(["read:statuses"])
[blockee, friend] = insert_list(2, :user)
{:ok, blocker} = User.follow(blocker, friend)
{:ok, _} = User.block(blocker, blockee)
conn = assign(conn, :user, blocker)
{:ok, %{id: activity_id} = activity} = CommonAPI.post(friend, %{status: "hey!"})
{:ok, reply_from_blockee} =
CommonAPI.post(blockee, %{status: "heya", in_reply_to_status_id: activity})
{:ok, _reply_from_friend} =
CommonAPI.post(friend, %{status: "status", in_reply_to_status_id: reply_from_blockee})
res_conn = get(conn, "/api/v1/timelines/public")
[%{"id" => ^activity_id}] = json_response_and_validate_schema(res_conn, 200)
end
test "doesn't return replies if follow is posting with users from blocked domain" do
%{conn: conn, user: blocker} = oauth_access(["read:statuses"])
friend = insert(:user)
blockee = insert(:user, ap_id: "https://example.com/users/blocked")
{:ok, blocker} = User.follow(blocker, friend)
{:ok, blocker} = User.block_domain(blocker, "example.com")
conn = assign(conn, :user, blocker)
{:ok, %{id: activity_id} = activity} = CommonAPI.post(friend, %{status: "hey!"})
{:ok, reply_from_blockee} =
CommonAPI.post(blockee, %{status: "heya", in_reply_to_status_id: activity})
{:ok, _reply_from_friend} =
CommonAPI.post(friend, %{status: "status", in_reply_to_status_id: reply_from_blockee})
res_conn = get(conn, "/api/v1/timelines/public")
activities = json_response_and_validate_schema(res_conn, 200)
[%{"id" => ^activity_id}] = activities
end
end end
defp local_and_remote_activities do defp local_and_remote_activities do