Merge branch 'develop' of https://git.pleroma.social/pleroma/pleroma into develop
This commit is contained in:
commit
780ac9cedb
868 changed files with 18337 additions and 10920 deletions
|
@ -48,6 +48,7 @@ benchmark:
|
||||||
|
|
||||||
unit-testing:
|
unit-testing:
|
||||||
stage: test
|
stage: test
|
||||||
|
retry: 2
|
||||||
cache: &testing_cache_policy
|
cache: &testing_cache_policy
|
||||||
<<: *global_cache_policy
|
<<: *global_cache_policy
|
||||||
policy: pull
|
policy: pull
|
||||||
|
@ -80,6 +81,7 @@ unit-testing:
|
||||||
|
|
||||||
unit-testing-rum:
|
unit-testing-rum:
|
||||||
stage: test
|
stage: test
|
||||||
|
retry: 2
|
||||||
cache: *testing_cache_policy
|
cache: *testing_cache_policy
|
||||||
services:
|
services:
|
||||||
- name: minibikini/postgres-with-rum:12
|
- name: minibikini/postgres-with-rum:12
|
||||||
|
|
33
CHANGELOG.md
33
CHANGELOG.md
|
@ -15,14 +15,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- **Breaking:** removed `with_move` parameter from notifications timeline.
|
- **Breaking:** removed `with_move` parameter from notifications timeline.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
- ActivityPub: Added support for existing AP ids for instances migrated from Mastodon.
|
||||||
|
- Instance: Add `background_image` to configuration and `/api/v1/instance`
|
||||||
- Instance: Extend `/api/v1/instance` with Pleroma-specific information.
|
- Instance: Extend `/api/v1/instance` with Pleroma-specific information.
|
||||||
- NodeInfo: `pleroma:api/v1/notifications:include_types_filter` to the `features` list.
|
- NodeInfo: `pleroma:api/v1/notifications:include_types_filter` to the `features` list.
|
||||||
- NodeInfo: `pleroma_emoji_reactions` to the `features` list.
|
- NodeInfo: `pleroma_emoji_reactions` to the `features` list.
|
||||||
- Configuration: `:restrict_unauthenticated` setting, restrict access for unauthenticated users to timelines (public and federate), user profiles and statuses.
|
- Configuration: `:restrict_unauthenticated` setting, restrict access for unauthenticated users to timelines (public and federate), user profiles and statuses.
|
||||||
|
- Configuration: Add `:database_config_whitelist` setting to whitelist settings which can be configured from AdminFE.
|
||||||
|
- Configuration: `filename_display_max_length` option to set filename truncate limit, if filename display enabled (0 = no limit).
|
||||||
- New HTTP adapter [gun](https://github.com/ninenines/gun). Gun adapter requires minimum OTP version of 22.2 otherwise Pleroma won’t start. For hackney OTP update is not required.
|
- New HTTP adapter [gun](https://github.com/ninenines/gun). Gun adapter requires minimum OTP version of 22.2 otherwise Pleroma won’t start. For hackney OTP update is not required.
|
||||||
- Mix task to create trusted OAuth App.
|
- Mix task to create trusted OAuth App.
|
||||||
- Notifications: Added `follow_request` notification type.
|
- Notifications: Added `follow_request` notification type.
|
||||||
- Added `:reject_deletes` group to SimplePolicy
|
- Added `:reject_deletes` group to SimplePolicy
|
||||||
|
- MRF (`EmojiStealPolicy`): New MRF Policy which allows to automatically download emojis from remote instances
|
||||||
<details>
|
<details>
|
||||||
<summary>API Changes</summary>
|
<summary>API Changes</summary>
|
||||||
- Mastodon API: Extended `/api/v1/instance`.
|
- Mastodon API: Extended `/api/v1/instance`.
|
||||||
|
@ -39,25 +44,47 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- Fix follower/blocks import when nicknames starts with @
|
- Fix follower/blocks import when nicknames starts with @
|
||||||
- Filtering of push notifications on activities from blocked domains
|
- Filtering of push notifications on activities from blocked domains
|
||||||
- Resolving Peertube accounts with Webfinger
|
- Resolving Peertube accounts with Webfinger
|
||||||
|
- `blob:` urls not being allowed by connect-src CSP
|
||||||
|
|
||||||
|
## [Unreleased (patch)]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Healthcheck reporting the number of memory currently used, rather than allocated in total
|
||||||
|
- `InsertSkeletonsForDeletedUsers` failing on some instances
|
||||||
|
|
||||||
|
## [2.0.3] - 2020-05-02
|
||||||
|
|
||||||
## [unreleased-patch]
|
|
||||||
### Security
|
### Security
|
||||||
- Disallow re-registration of previously deleted users, which allowed viewing direct messages addressed to them
|
- Disallow re-registration of previously deleted users, which allowed viewing direct messages addressed to them
|
||||||
- Mastodon API: Fix `POST /api/v1/follow_requests/:id/authorize` allowing to force a follow from a local user even if they didn't request to follow
|
- Mastodon API: Fix `POST /api/v1/follow_requests/:id/authorize` allowing to force a follow from a local user even if they didn't request to follow
|
||||||
|
- CSP: Sandbox uploads
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Logger configuration through AdminFE
|
- Notifications from blocked domains
|
||||||
|
- Potential federation issues with Mastodon versions before 3.0.0
|
||||||
- HTTP Basic Authentication permissions issue
|
- HTTP Basic Authentication permissions issue
|
||||||
|
- Follow/Block imports not being able to find the user if the nickname started with an `@`
|
||||||
|
- Instance stats counting internal users
|
||||||
|
- Inability to run a From Source release without git
|
||||||
- ObjectAgePolicy didn't filter out old messages
|
- ObjectAgePolicy didn't filter out old messages
|
||||||
- Transmogrifier: Keep object sensitive settings for outgoing representation (AP C2S)
|
- `blob:` urls not being allowed by CSP
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- NodeInfo: ObjectAgePolicy settings to the `federation` list.
|
- NodeInfo: ObjectAgePolicy settings to the `federation` list.
|
||||||
|
- Follow request notifications
|
||||||
<details>
|
<details>
|
||||||
<summary>API Changes</summary>
|
<summary>API Changes</summary>
|
||||||
- Admin API: `GET /api/pleroma/admin/need_reboot`.
|
- Admin API: `GET /api/pleroma/admin/need_reboot`.
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
### Upgrade notes
|
||||||
|
|
||||||
|
1. Restart Pleroma
|
||||||
|
2. Run database migrations (inside Pleroma directory):
|
||||||
|
- OTP: `./bin/pleroma_ctl migrate`
|
||||||
|
- From Source: `mix ecto.migrate`
|
||||||
|
|
||||||
|
|
||||||
## [2.0.2] - 2020-04-08
|
## [2.0.2] - 2020-04-08
|
||||||
### Added
|
### Added
|
||||||
- Support for Funkwhale's `Audio` activity
|
- Support for Funkwhale's `Audio` activity
|
||||||
|
|
|
@ -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)
|
||||||
|
@ -123,22 +127,22 @@ def generate_tagged_activities(opts \\ []) do
|
||||||
Enum.each(1..activity_count, fn _ ->
|
Enum.each(1..activity_count, fn _ ->
|
||||||
random = :rand.uniform()
|
random = :rand.uniform()
|
||||||
i = Enum.find_index(intervals, fn {lower, upper} -> lower <= random && upper > random end)
|
i = Enum.find_index(intervals, fn {lower, upper} -> lower <= random && upper > random end)
|
||||||
CommonAPI.post(Enum.random(users), %{"status" => "a post with the tag #tag_#{i}"})
|
CommonAPI.post(Enum.random(users), %{status: "a post with the tag #tag_#{i}"})
|
||||||
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
|
||||||
})
|
})
|
||||||
|
|
||||||
Agent.update(:benchmark_state, fn state ->
|
Agent.update(:benchmark_state, fn state ->
|
||||||
|
@ -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,
|
||||||
|
@ -262,118 +300,94 @@ defp insert_activity("attachment", visibility, group, user, friends, non_friends
|
||||||
|
|
||||||
{:ok, _activity} =
|
{:ok, _activity} =
|
||||||
CommonAPI.post(actor, %{
|
CommonAPI.post(actor, %{
|
||||||
"status" => "Post with attachment",
|
status: "Post with attachment",
|
||||||
"visibility" => visibility,
|
visibility: visibility,
|
||||||
"media_ids" => [object.id]
|
media_ids: [object.id]
|
||||||
})
|
})
|
||||||
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))
|
||||||
|
|
||||||
{:ok, activity} =
|
{:ok, activity} =
|
||||||
CommonAPI.post(actor, %{
|
CommonAPI.post(actor, %{
|
||||||
"status" => Enum.join(data, ", ") <> "simple status",
|
status: Enum.join(data, ", ") <> "simple status",
|
||||||
"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,
|
||||||
|
@ -390,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" => %{},
|
||||||
|
@ -476,60 +490,74 @@ 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
|
||||||
|
|
||||||
defp insert_reply(actor, data, activity_id, visibility) do
|
defp insert_reply(actor, data, activity_id, visibility) do
|
||||||
{:ok, reply} =
|
{:ok, reply} =
|
||||||
CommonAPI.post(actor, %{
|
CommonAPI.post(actor, %{
|
||||||
"status" => Enum.join(data, ", "),
|
status: Enum.join(data, ", "),
|
||||||
"visibility" => visibility,
|
visibility: visibility,
|
||||||
"in_reply_to_status_id" => activity_id
|
in_reply_to_status_id: activity_id
|
||||||
})
|
})
|
||||||
|
|
||||||
{reply.id, ["@" <> actor.nickname | data]}
|
{reply.id, ["@" <> actor.nickname | data]}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
@ -387,56 +440,47 @@ defp render_timelines(user) do
|
||||||
|
|
||||||
favourites = ActivityPub.fetch_favourites(user)
|
favourites = ActivityPub.fetch_favourites(user)
|
||||||
|
|
||||||
output_relationships =
|
|
||||||
!!Pleroma.Config.get([:extensions, :output_relationships_in_statuses_by_default])
|
|
||||||
|
|
||||||
Benchee.run(
|
Benchee.run(
|
||||||
%{
|
%{
|
||||||
"Rendering home timeline" => fn ->
|
"Rendering home timeline" => fn ->
|
||||||
StatusView.render("index.json", %{
|
StatusView.render("index.json", %{
|
||||||
activities: home_activities,
|
activities: home_activities,
|
||||||
for: user,
|
for: user,
|
||||||
as: :activity,
|
as: :activity
|
||||||
skip_relationships: !output_relationships
|
|
||||||
})
|
})
|
||||||
end,
|
end,
|
||||||
"Rendering direct timeline" => fn ->
|
"Rendering direct timeline" => fn ->
|
||||||
StatusView.render("index.json", %{
|
StatusView.render("index.json", %{
|
||||||
activities: direct_activities,
|
activities: direct_activities,
|
||||||
for: user,
|
for: user,
|
||||||
as: :activity,
|
as: :activity
|
||||||
skip_relationships: !output_relationships
|
|
||||||
})
|
})
|
||||||
end,
|
end,
|
||||||
"Rendering public timeline" => fn ->
|
"Rendering public timeline" => fn ->
|
||||||
StatusView.render("index.json", %{
|
StatusView.render("index.json", %{
|
||||||
activities: public_activities,
|
activities: public_activities,
|
||||||
for: user,
|
for: user,
|
||||||
as: :activity,
|
as: :activity
|
||||||
skip_relationships: !output_relationships
|
|
||||||
})
|
})
|
||||||
end,
|
end,
|
||||||
"Rendering tag timeline" => fn ->
|
"Rendering tag timeline" => fn ->
|
||||||
StatusView.render("index.json", %{
|
StatusView.render("index.json", %{
|
||||||
activities: tag_activities,
|
activities: tag_activities,
|
||||||
for: user,
|
for: user,
|
||||||
as: :activity,
|
as: :activity
|
||||||
skip_relationships: !output_relationships
|
|
||||||
})
|
})
|
||||||
end,
|
end,
|
||||||
"Rendering notifications" => fn ->
|
"Rendering notifications" => fn ->
|
||||||
Pleroma.Web.MastodonAPI.NotificationView.render("index.json", %{
|
Pleroma.Web.MastodonAPI.NotificationView.render("index.json", %{
|
||||||
notifications: notifications,
|
notifications: notifications,
|
||||||
for: user,
|
for: user
|
||||||
skip_relationships: !output_relationships
|
|
||||||
})
|
})
|
||||||
end,
|
end,
|
||||||
"Rendering favourites timeline" => fn ->
|
"Rendering favourites timeline" => fn ->
|
||||||
StatusView.render("index.json", %{
|
StatusView.render("index.json", %{
|
||||||
activities: favourites,
|
activities: favourites,
|
||||||
for: user,
|
for: user,
|
||||||
as: :activity,
|
as: :activity
|
||||||
skip_relationships: !output_relationships
|
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
},
|
},
|
||||||
|
|
|
@ -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
|
||||||
|
@ -55,7 +55,7 @@ defp generate_user(i) do
|
||||||
name: "Test テスト User #{i}",
|
name: "Test テスト User #{i}",
|
||||||
email: "user#{i}@example.com",
|
email: "user#{i}@example.com",
|
||||||
nickname: "nick#{i}",
|
nickname: "nick#{i}",
|
||||||
password_hash: Comeonin.Pbkdf2.hashpwsalt("test"),
|
password_hash: Pbkdf2.hash_pwd_salt("test"),
|
||||||
bio: "Tester Number #{i}",
|
bio: "Tester Number #{i}",
|
||||||
local: !remote
|
local: !remote
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -71,7 +71,8 @@
|
||||||
follow_redirect: true,
|
follow_redirect: true,
|
||||||
pool: :upload
|
pool: :upload
|
||||||
]
|
]
|
||||||
]
|
],
|
||||||
|
filename_display_max_length: 30
|
||||||
|
|
||||||
config :pleroma, Pleroma.Uploaders.Local, uploads: "uploads"
|
config :pleroma, Pleroma.Uploaders.Local, uploads: "uploads"
|
||||||
|
|
||||||
|
@ -170,7 +171,8 @@
|
||||||
"application/ld+json" => ["activity+json"]
|
"application/ld+json" => ["activity+json"]
|
||||||
}
|
}
|
||||||
|
|
||||||
config :tesla, adapter: Tesla.Adapter.Gun
|
config :tesla, adapter: Tesla.Adapter.Hackney
|
||||||
|
|
||||||
# Configures http settings, upstream proxy etc.
|
# Configures http settings, upstream proxy etc.
|
||||||
config :pleroma, :http,
|
config :pleroma, :http,
|
||||||
proxy_url: nil,
|
proxy_url: nil,
|
||||||
|
@ -182,7 +184,8 @@
|
||||||
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",
|
||||||
limit: 5_000,
|
limit: 5_000,
|
||||||
chat_limit: 5_000,
|
chat_limit: 5_000,
|
||||||
remote_limit: 100_000,
|
remote_limit: 100_000,
|
||||||
|
@ -252,8 +255,6 @@
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
config :pleroma, :extensions, output_relationships_in_statuses_by_default: true
|
|
||||||
|
|
||||||
config :pleroma, :feed,
|
config :pleroma, :feed,
|
||||||
post_title: %{
|
post_title: %{
|
||||||
max_length: 100,
|
max_length: 100,
|
||||||
|
@ -274,20 +275,33 @@
|
||||||
|
|
||||||
config :pleroma, :frontend_configurations,
|
config :pleroma, :frontend_configurations,
|
||||||
pleroma_fe: %{
|
pleroma_fe: %{
|
||||||
theme: "pleroma-dark",
|
alwaysShowSubjectInput: true,
|
||||||
logo: "/static/logo.png",
|
|
||||||
background: "/images/city.jpg",
|
background: "/images/city.jpg",
|
||||||
redirectRootNoLogin: "/main/all",
|
|
||||||
redirectRootLogin: "/main/friends",
|
|
||||||
showInstanceSpecificPanel: true,
|
|
||||||
scopeOptionsEnabled: false,
|
|
||||||
formattingOptionsEnabled: false,
|
|
||||||
collapseMessageWithSubject: false,
|
collapseMessageWithSubject: false,
|
||||||
|
disableChat: false,
|
||||||
|
greentext: false,
|
||||||
|
hideFilteredStatuses: false,
|
||||||
|
hideMutedPosts: false,
|
||||||
hidePostStats: false,
|
hidePostStats: false,
|
||||||
|
hideSitename: false,
|
||||||
hideUserStats: false,
|
hideUserStats: false,
|
||||||
|
loginMethod: "password",
|
||||||
|
logo: "/static/logo.png",
|
||||||
|
logoMargin: ".1em",
|
||||||
|
logoMask: true,
|
||||||
|
minimalScopesMode: false,
|
||||||
|
noAttachmentLinks: false,
|
||||||
|
nsfwCensorImage: "",
|
||||||
|
postContentType: "text/plain",
|
||||||
|
redirectRootLogin: "/main/friends",
|
||||||
|
redirectRootNoLogin: "/main/all",
|
||||||
scopeCopy: true,
|
scopeCopy: true,
|
||||||
|
sidebarRight: false,
|
||||||
|
showFeaturesPanel: true,
|
||||||
|
showInstanceSpecificPanel: false,
|
||||||
subjectLineBehavior: "email",
|
subjectLineBehavior: "email",
|
||||||
alwaysShowSubjectInput: true
|
theme: "pleroma-dark",
|
||||||
|
webPushNotifications: false
|
||||||
},
|
},
|
||||||
masto_fe: %{
|
masto_fe: %{
|
||||||
showInstanceSpecificPanel: true
|
showInstanceSpecificPanel: true
|
||||||
|
@ -379,6 +393,10 @@
|
||||||
|
|
||||||
config :pleroma, :media_proxy,
|
config :pleroma, :media_proxy,
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
invalidation: [
|
||||||
|
enabled: false,
|
||||||
|
provider: Pleroma.Web.MediaProxy.Invalidation.Script
|
||||||
|
],
|
||||||
proxy_opts: [
|
proxy_opts: [
|
||||||
redirect_on_failure: false,
|
redirect_on_failure: false,
|
||||||
max_body_length: 25 * 1_048_576,
|
max_body_length: 25 * 1_048_576,
|
||||||
|
|
|
@ -28,7 +28,8 @@
|
||||||
%{
|
%{
|
||||||
key: :filters,
|
key: :filters,
|
||||||
type: {:list, :module},
|
type: {:list, :module},
|
||||||
description: "List of filter modules for uploads",
|
description:
|
||||||
|
"List of filter modules for uploads. Module names are shortened (removed leading `Pleroma.Upload.Filter.` part), but on adding custom module you need to use full name.",
|
||||||
suggestions:
|
suggestions:
|
||||||
Generator.list_modules_in_dir(
|
Generator.list_modules_in_dir(
|
||||||
"lib/pleroma/upload/filter",
|
"lib/pleroma/upload/filter",
|
||||||
|
@ -118,6 +119,11 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :filename_display_max_length,
|
||||||
|
type: :integer,
|
||||||
|
description: "Set max length of a filename to display. 0 = no limit. Default: 30"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -678,14 +684,6 @@
|
||||||
7
|
7
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
%{
|
|
||||||
key: :federation_publisher_modules,
|
|
||||||
type: {:list, :module},
|
|
||||||
description: "List of modules for federation publishing",
|
|
||||||
suggestions: [
|
|
||||||
Pleroma.Web.ActivityPub.Publisher
|
|
||||||
]
|
|
||||||
},
|
|
||||||
%{
|
%{
|
||||||
key: :allow_relay,
|
key: :allow_relay,
|
||||||
type: :boolean,
|
type: :boolean,
|
||||||
|
@ -694,7 +692,8 @@
|
||||||
%{
|
%{
|
||||||
key: :rewrite_policy,
|
key: :rewrite_policy,
|
||||||
type: [:module, {:list, :module}],
|
type: [:module, {:list, :module}],
|
||||||
description: "A list of MRF policies enabled",
|
description:
|
||||||
|
"A list of enabled MRF policies. Module names are shortened (removed leading `Pleroma.Web.ActivityPub.MRF.` part), but on adding custom module you need to use full name.",
|
||||||
suggestions:
|
suggestions:
|
||||||
Generator.list_modules_in_dir(
|
Generator.list_modules_in_dir(
|
||||||
"lib/pleroma/web/activity_pub/mrf",
|
"lib/pleroma/web/activity_pub/mrf",
|
||||||
|
@ -712,7 +711,7 @@
|
||||||
key: :quarantined_instances,
|
key: :quarantined_instances,
|
||||||
type: {:list, :string},
|
type: {:list, :string},
|
||||||
description:
|
description:
|
||||||
"List of ActivityPub instances where private (DMs, followers-only) activities will not be send",
|
"List of ActivityPub instances where private (DMs, followers-only) activities will not be sent",
|
||||||
suggestions: [
|
suggestions: [
|
||||||
"quarantined.com",
|
"quarantined.com",
|
||||||
"*.quarantined.com"
|
"*.quarantined.com"
|
||||||
|
@ -975,6 +974,13 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :instance_thumbnail,
|
||||||
|
type: :string,
|
||||||
|
description:
|
||||||
|
"The instance thumbnail image. It will appear in [Pleroma Instances](http://distsn.org/pleroma-instances.html)",
|
||||||
|
suggestions: ["/instance/thumbnail.jpeg"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -1102,32 +1108,98 @@
|
||||||
description: "Settings for Pleroma FE",
|
description: "Settings for Pleroma FE",
|
||||||
suggestions: [
|
suggestions: [
|
||||||
%{
|
%{
|
||||||
theme: "pleroma-dark",
|
|
||||||
logo: "/static/logo.png",
|
|
||||||
background: "/images/city.jpg",
|
|
||||||
redirectRootNoLogin: "/main/all",
|
|
||||||
redirectRootLogin: "/main/friends",
|
|
||||||
showInstanceSpecificPanel: true,
|
|
||||||
scopeOptionsEnabled: false,
|
|
||||||
formattingOptionsEnabled: false,
|
|
||||||
collapseMessageWithSubject: false,
|
|
||||||
hidePostStats: false,
|
|
||||||
hideUserStats: false,
|
|
||||||
scopeCopy: true,
|
|
||||||
subjectLineBehavior: "email",
|
|
||||||
alwaysShowSubjectInput: true,
|
alwaysShowSubjectInput: true,
|
||||||
logoMask: false,
|
background: "/static/aurora_borealis.jpg",
|
||||||
|
collapseMessageWithSubject: false,
|
||||||
|
disableChat: false,
|
||||||
|
greentext: false,
|
||||||
|
hideFilteredStatuses: false,
|
||||||
|
hideMutedPosts: false,
|
||||||
|
hidePostStats: false,
|
||||||
|
hideSitename: false,
|
||||||
|
hideUserStats: false,
|
||||||
|
loginMethod: "password",
|
||||||
|
logo: "/static/logo.png",
|
||||||
logoMargin: ".1em",
|
logoMargin: ".1em",
|
||||||
stickers: false,
|
logoMask: true,
|
||||||
enableEmojiPicker: false
|
minimalScopesMode: false,
|
||||||
|
noAttachmentLinks: false,
|
||||||
|
nsfwCensorImage: "/static/img/nsfw.74818f9.png",
|
||||||
|
postContentType: "text/plain",
|
||||||
|
redirectRootLogin: "/main/friends",
|
||||||
|
redirectRootNoLogin: "/main/all",
|
||||||
|
scopeCopy: true,
|
||||||
|
sidebarRight: false,
|
||||||
|
showFeaturesPanel: true,
|
||||||
|
showInstanceSpecificPanel: false,
|
||||||
|
subjectLineBehavior: "email",
|
||||||
|
theme: "pleroma-dark",
|
||||||
|
webPushNotifications: false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
children: [
|
children: [
|
||||||
%{
|
%{
|
||||||
key: :theme,
|
key: :alwaysShowSubjectInput,
|
||||||
|
label: "Always show subject input",
|
||||||
|
type: :boolean,
|
||||||
|
description: "When disabled, auto-hide the subject field if it's empty"
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :background,
|
||||||
type: :string,
|
type: :string,
|
||||||
description: "Which theme to use, they are defined in styles.json",
|
description:
|
||||||
suggestions: ["pleroma-dark"]
|
"URL of the background, unless viewing a user profile with a background that is set",
|
||||||
|
suggestions: ["/images/city.jpg"]
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :collapseMessageWithSubject,
|
||||||
|
label: "Collapse message with subject",
|
||||||
|
type: :boolean,
|
||||||
|
description:
|
||||||
|
"When a message has a subject (aka Content Warning), collapse it by default"
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :disableChat,
|
||||||
|
label: "PleromaFE Chat",
|
||||||
|
type: :boolean,
|
||||||
|
description: "Disables PleromaFE Chat component"
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :greentext,
|
||||||
|
label: "Greentext",
|
||||||
|
type: :boolean,
|
||||||
|
description: "Enables green text on lines prefixed with the > character."
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :hideFilteredStatuses,
|
||||||
|
label: "Hide Filtered Statuses",
|
||||||
|
type: :boolean,
|
||||||
|
description: "Hides filtered statuses from timelines."
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :hideMutedPosts,
|
||||||
|
label: "Hide Muted Posts",
|
||||||
|
type: :boolean,
|
||||||
|
description: "Hides muted statuses from timelines."
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :hidePostStats,
|
||||||
|
label: "Hide post stats",
|
||||||
|
type: :boolean,
|
||||||
|
description: "Hide notices statistics (repeats, favorites, ...)"
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :hideSitename,
|
||||||
|
label: "Hide Sitename",
|
||||||
|
type: :boolean,
|
||||||
|
description: "Hides instance name from PleromaFE banner."
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :hideUserStats,
|
||||||
|
label: "Hide user stats",
|
||||||
|
type: :boolean,
|
||||||
|
description:
|
||||||
|
"Hide profile statistics (posts, posts per day, followers, followings, ...)"
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
key: :logo,
|
key: :logo,
|
||||||
|
@ -1136,11 +1208,44 @@
|
||||||
suggestions: ["/static/logo.png"]
|
suggestions: ["/static/logo.png"]
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
key: :background,
|
key: :logoMargin,
|
||||||
|
label: "Logo margin",
|
||||||
type: :string,
|
type: :string,
|
||||||
description:
|
description:
|
||||||
"URL of the background, unless viewing a user profile with a background that is set",
|
"Allows you to adjust vertical margins between logo boundary and navbar borders. " <>
|
||||||
suggestions: ["/images/city.jpg"]
|
"The idea is that to have logo's image without any extra margins and instead adjust them to your need in layout.",
|
||||||
|
suggestions: [".1em"]
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :logoMask,
|
||||||
|
label: "Logo mask",
|
||||||
|
type: :boolean,
|
||||||
|
description:
|
||||||
|
"By default it assumes logo used will be monochrome with alpha channel to be compatible with both light and dark themes. " <>
|
||||||
|
"If you want a colorful logo you must disable logoMask."
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :minimalScopesMode,
|
||||||
|
label: "Minimal scopes mode",
|
||||||
|
type: :boolean,
|
||||||
|
description:
|
||||||
|
"Limit scope selection to Direct, User default, and Scope of post replying to. " <>
|
||||||
|
"Also prevents replying to a DM with a public post from PleromaFE."
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :nsfwCensorImage,
|
||||||
|
label: "NSFW Censor Image",
|
||||||
|
type: :string,
|
||||||
|
description:
|
||||||
|
"URL of the image to use for hiding NSFW media attachments in the timeline.",
|
||||||
|
suggestions: ["/static/img/nsfw.74818f9.png"]
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :postContentType,
|
||||||
|
label: "Post Content Type",
|
||||||
|
type: {:dropdown, :atom},
|
||||||
|
description: "Default post formatting option.",
|
||||||
|
suggestions: ["text/plain", "text/html", "text/markdown", "text/bbcode"]
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
key: :redirectRootNoLogin,
|
key: :redirectRootNoLogin,
|
||||||
|
@ -1158,51 +1263,31 @@
|
||||||
"Relative URL which indicates where to redirect when a user is logged in",
|
"Relative URL which indicates where to redirect when a user is logged in",
|
||||||
suggestions: ["/main/friends"]
|
suggestions: ["/main/friends"]
|
||||||
},
|
},
|
||||||
%{
|
|
||||||
key: :showInstanceSpecificPanel,
|
|
||||||
label: "Show instance specific panel",
|
|
||||||
type: :boolean,
|
|
||||||
description: "Whenether to show the instance's specific panel"
|
|
||||||
},
|
|
||||||
%{
|
|
||||||
key: :scopeOptionsEnabled,
|
|
||||||
label: "Scope options enabled",
|
|
||||||
type: :boolean,
|
|
||||||
description: "Enable setting a notice visibility and subject/CW when posting"
|
|
||||||
},
|
|
||||||
%{
|
|
||||||
key: :formattingOptionsEnabled,
|
|
||||||
label: "Formatting options enabled",
|
|
||||||
type: :boolean,
|
|
||||||
description:
|
|
||||||
"Enable setting a formatting different than plain-text (ie. HTML, Markdown) when posting, relates to `:instance`, `allowed_post_formats`"
|
|
||||||
},
|
|
||||||
%{
|
|
||||||
key: :collapseMessageWithSubject,
|
|
||||||
label: "Collapse message with subject",
|
|
||||||
type: :boolean,
|
|
||||||
description:
|
|
||||||
"When a message has a subject (aka Content Warning), collapse it by default"
|
|
||||||
},
|
|
||||||
%{
|
|
||||||
key: :hidePostStats,
|
|
||||||
label: "Hide post stats",
|
|
||||||
type: :boolean,
|
|
||||||
description: "Hide notices statistics (repeats, favorites, ...)"
|
|
||||||
},
|
|
||||||
%{
|
|
||||||
key: :hideUserStats,
|
|
||||||
label: "Hide user stats",
|
|
||||||
type: :boolean,
|
|
||||||
description:
|
|
||||||
"Hide profile statistics (posts, posts per day, followers, followings, ...)"
|
|
||||||
},
|
|
||||||
%{
|
%{
|
||||||
key: :scopeCopy,
|
key: :scopeCopy,
|
||||||
label: "Scope copy",
|
label: "Scope copy",
|
||||||
type: :boolean,
|
type: :boolean,
|
||||||
description: "Copy the scope (private/unlisted/public) in replies to posts by default"
|
description: "Copy the scope (private/unlisted/public) in replies to posts by default"
|
||||||
},
|
},
|
||||||
|
%{
|
||||||
|
key: :sidebarRight,
|
||||||
|
label: "Sidebar on Right",
|
||||||
|
type: :boolean,
|
||||||
|
description: "Change alignment of sidebar and panels to the right."
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :showFeaturesPanel,
|
||||||
|
label: "Show instance features panel",
|
||||||
|
type: :boolean,
|
||||||
|
description:
|
||||||
|
"Enables panel displaying functionality of the instance on the About page."
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :showInstanceSpecificPanel,
|
||||||
|
label: "Show instance specific panel",
|
||||||
|
type: :boolean,
|
||||||
|
description: "Whether to show the instance's custom panel"
|
||||||
|
},
|
||||||
%{
|
%{
|
||||||
key: :subjectLineBehavior,
|
key: :subjectLineBehavior,
|
||||||
label: "Subject line behavior",
|
label: "Subject line behavior",
|
||||||
|
@ -1214,38 +1299,10 @@
|
||||||
suggestions: ["email", "masto", "noop"]
|
suggestions: ["email", "masto", "noop"]
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
key: :alwaysShowSubjectInput,
|
key: :theme,
|
||||||
label: "Always show subject input",
|
|
||||||
type: :boolean,
|
|
||||||
description: "When disabled, auto-hide the subject field if it's empty"
|
|
||||||
},
|
|
||||||
%{
|
|
||||||
key: :logoMask,
|
|
||||||
label: "Logo mask",
|
|
||||||
type: :boolean,
|
|
||||||
description:
|
|
||||||
"By default it assumes logo used will be monochrome with alpha channel to be compatible with both light and dark themes. " <>
|
|
||||||
"If you want a colorful logo you must disable logoMask."
|
|
||||||
},
|
|
||||||
%{
|
|
||||||
key: :logoMargin,
|
|
||||||
label: "Logo margin",
|
|
||||||
type: :string,
|
type: :string,
|
||||||
description:
|
description: "Which theme to use. Available themes are defined in styles.json",
|
||||||
"Allows you to adjust vertical margins between logo boundary and navbar borders. " <>
|
suggestions: ["pleroma-dark"]
|
||||||
"The idea is that to have logo's image without any extra margins and instead adjust them to your need in layout.",
|
|
||||||
suggestions: [".1em"]
|
|
||||||
},
|
|
||||||
%{
|
|
||||||
key: :stickers,
|
|
||||||
type: :boolean,
|
|
||||||
description: "Enables stickers."
|
|
||||||
},
|
|
||||||
%{
|
|
||||||
key: :enableEmojiPicker,
|
|
||||||
label: "Emoji picker",
|
|
||||||
type: :boolean,
|
|
||||||
description: "Enables emoji picker."
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -1301,6 +1358,12 @@
|
||||||
suggestions: [
|
suggestions: [
|
||||||
:pleroma_fox_tan
|
:pleroma_fox_tan
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :default_user_avatar,
|
||||||
|
type: :string,
|
||||||
|
description: "URL of the default user avatar.",
|
||||||
|
suggestions: ["/images/avi.png"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -1855,12 +1918,6 @@
|
||||||
(see https://github.com/sorentwo/oban/issues/52).
|
(see https://github.com/sorentwo/oban/issues/52).
|
||||||
""",
|
""",
|
||||||
children: [
|
children: [
|
||||||
%{
|
|
||||||
key: :repo,
|
|
||||||
type: :module,
|
|
||||||
description: "Application's Ecto repo",
|
|
||||||
suggestions: [Pleroma.Repo]
|
|
||||||
},
|
|
||||||
%{
|
%{
|
||||||
key: :verbose,
|
key: :verbose,
|
||||||
type: {:dropdown, :atom},
|
type: {:dropdown, :atom},
|
||||||
|
@ -2031,7 +2088,8 @@
|
||||||
%{
|
%{
|
||||||
key: :parsers,
|
key: :parsers,
|
||||||
type: {:list, :module},
|
type: {:list, :module},
|
||||||
description: "List of Rich Media parsers.",
|
description:
|
||||||
|
"List of Rich Media parsers. Module names are shortened (removed leading `Pleroma.Web.RichMedia.Parsers.` part), but on adding custom module you need to use full name.",
|
||||||
suggestions: [
|
suggestions: [
|
||||||
Pleroma.Web.RichMedia.Parsers.MetaTagsParser,
|
Pleroma.Web.RichMedia.Parsers.MetaTagsParser,
|
||||||
Pleroma.Web.RichMedia.Parsers.OEmbed,
|
Pleroma.Web.RichMedia.Parsers.OEmbed,
|
||||||
|
@ -2043,7 +2101,8 @@
|
||||||
key: :ttl_setters,
|
key: :ttl_setters,
|
||||||
label: "TTL setters",
|
label: "TTL setters",
|
||||||
type: {:list, :module},
|
type: {:list, :module},
|
||||||
description: "List of rich media TTL setters.",
|
description:
|
||||||
|
"List of rich media TTL setters. Module names are shortened (removed leading `Pleroma.Web.RichMedia.Parser.` part), but on adding custom module you need to use full name.",
|
||||||
suggestions: [
|
suggestions: [
|
||||||
Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl
|
Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl
|
||||||
]
|
]
|
||||||
|
@ -2633,18 +2692,6 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
%{
|
|
||||||
group: :http_signatures,
|
|
||||||
type: :group,
|
|
||||||
description: "HTTP Signatures settings",
|
|
||||||
children: [
|
|
||||||
%{
|
|
||||||
key: :adapter,
|
|
||||||
type: :module,
|
|
||||||
suggestions: [Pleroma.Signature]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
%{
|
%{
|
||||||
group: :pleroma,
|
group: :pleroma,
|
||||||
key: :http,
|
key: :http,
|
||||||
|
@ -2717,6 +2764,8 @@
|
||||||
%{
|
%{
|
||||||
key: :scrub_policy,
|
key: :scrub_policy,
|
||||||
type: {:list, :module},
|
type: {:list, :module},
|
||||||
|
description:
|
||||||
|
"Module names are shortened (removed leading `Pleroma.HTML.` part), but on adding custom module you need to use full name.",
|
||||||
suggestions: [Pleroma.HTML.Transform.MediaProxy, Pleroma.HTML.Scrubber.Default]
|
suggestions: [Pleroma.HTML.Transform.MediaProxy, Pleroma.HTML.Scrubber.Default]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -511,7 +511,23 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
|
||||||
- `discoverable`
|
- `discoverable`
|
||||||
- `actor_type`
|
- `actor_type`
|
||||||
|
|
||||||
- Response: none (code `200`)
|
- Response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"status": "success"}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"errors":
|
||||||
|
{"actor_type": "is invalid"},
|
||||||
|
{"email": "has invalid format"},
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"error": "Unable to update user."}
|
||||||
|
```
|
||||||
|
|
||||||
## `GET /api/pleroma/admin/reports`
|
## `GET /api/pleroma/admin/reports`
|
||||||
|
|
||||||
|
@ -531,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": {
|
||||||
|
@ -752,7 +768,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
|
||||||
- 400 Bad Request `"Invalid parameters"` when `status` is missing
|
- 400 Bad Request `"Invalid parameters"` when `status` is missing
|
||||||
- On success: `204`, empty response
|
- On success: `204`, empty response
|
||||||
|
|
||||||
## `POST /api/pleroma/admin/reports/:report_id/notes/:id`
|
## `DELETE /api/pleroma/admin/reports/:report_id/notes/:id`
|
||||||
|
|
||||||
### Delete report note
|
### Delete report note
|
||||||
|
|
||||||
|
|
|
@ -6,10 +6,6 @@ A Pleroma instance can be identified by "<Mastodon version> (compatible; Pleroma
|
||||||
|
|
||||||
Pleroma uses 128-bit ids as opposed to Mastodon's 64 bits. However just like Mastodon's ids they are lexically sortable strings
|
Pleroma uses 128-bit ids as opposed to Mastodon's 64 bits. However just like Mastodon's ids they are lexically sortable strings
|
||||||
|
|
||||||
## Attachment cap
|
|
||||||
|
|
||||||
Some apps operate under the assumption that no more than 4 attachments can be returned or uploaded. Pleroma however does not enforce any limits on attachment count neither when returning the status object nor when posting.
|
|
||||||
|
|
||||||
## Timelines
|
## Timelines
|
||||||
|
|
||||||
Adding the parameter `with_muted=true` to the timeline queries will also return activities by muted (not by blocked!) users.
|
Adding the parameter `with_muted=true` to the timeline queries will also return activities by muted (not by blocked!) users.
|
||||||
|
@ -32,12 +28,20 @@ Has these additional fields under the `pleroma` object:
|
||||||
- `thread_muted`: true if the thread the post belongs to is muted
|
- `thread_muted`: true if the thread the post belongs to is muted
|
||||||
- `emoji_reactions`: A list with emoji / reaction maps. The format is `{name: "☕", count: 1, me: true}`. Contains no information about the reacting users, for that use the `/statuses/:id/reactions` endpoint.
|
- `emoji_reactions`: A list with emoji / reaction maps. The format is `{name: "☕", count: 1, me: true}`. Contains no information about the reacting users, for that use the `/statuses/:id/reactions` endpoint.
|
||||||
|
|
||||||
## Attachments
|
## Media Attachments
|
||||||
|
|
||||||
Has these additional fields under the `pleroma` object:
|
Has these additional fields under the `pleroma` object:
|
||||||
|
|
||||||
- `mime_type`: mime type of the attachment.
|
- `mime_type`: mime type of the attachment.
|
||||||
|
|
||||||
|
### Attachment cap
|
||||||
|
|
||||||
|
Some apps operate under the assumption that no more than 4 attachments can be returned or uploaded. Pleroma however does not enforce any limits on attachment count neither when returning the status object nor when posting.
|
||||||
|
|
||||||
|
### Limitations
|
||||||
|
|
||||||
|
Pleroma does not process remote images and therefore cannot include fields such as `meta` and `blurhash`. It does not support focal points or aspect ratios. The frontend is expected to handle it.
|
||||||
|
|
||||||
## Accounts
|
## Accounts
|
||||||
|
|
||||||
The `id` parameter can also be the `nickname` of the user. This only works in these endpoints, not the deeper nested ones for following etc.
|
The `id` parameter can also be the `nickname` of the user. This only works in these endpoints, not the deeper nested ones for following etc.
|
||||||
|
@ -216,6 +220,7 @@ Has theses additional parameters (which are the same as in Pleroma-API):
|
||||||
- `avatar_upload_limit`: The same for avatars
|
- `avatar_upload_limit`: The same for avatars
|
||||||
- `background_upload_limit`: The same for backgrounds
|
- `background_upload_limit`: The same for backgrounds
|
||||||
- `banner_upload_limit`: The same for banners
|
- `banner_upload_limit`: The same for banners
|
||||||
|
- `background_image`: A background image that frontends can use
|
||||||
- `pleroma.metadata.features`: A list of supported features
|
- `pleroma.metadata.features`: A list of supported features
|
||||||
- `pleroma.metadata.federation`: The federation restrictions of this instance
|
- `pleroma.metadata.federation`: The federation restrictions of this instance
|
||||||
- `vapid_public_key`: The public key needed for push messages
|
- `vapid_public_key`: The public key needed for push messages
|
||||||
|
|
|
@ -265,7 +265,7 @@ See [Admin-API](admin_api.md)
|
||||||
* Method `PUT`
|
* Method `PUT`
|
||||||
* Authentication: required
|
* Authentication: required
|
||||||
* Params:
|
* Params:
|
||||||
* `image`: Multipart image
|
* `file`: Multipart image
|
||||||
* Response: JSON. Returns a mastodon media attachment entity
|
* Response: JSON. Returns a mastodon media attachment entity
|
||||||
when successful, otherwise returns HTTP 415 `{"error": "error_msg"}`
|
when successful, otherwise returns HTTP 415 `{"error": "error_msg"}`
|
||||||
* Example response:
|
* Example response:
|
||||||
|
@ -358,7 +358,7 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa
|
||||||
* `recipients`: A list of ids of users that should receive posts to this conversation. This will replace the current list of recipients, so submit the full list. The owner of owner of the conversation will always be part of the set of recipients, though.
|
* `recipients`: A list of ids of users that should receive posts to this conversation. This will replace the current list of recipients, so submit the full list. The owner of owner of the conversation will always be part of the set of recipients, though.
|
||||||
* Response: JSON, statuses (200 - healthy, 503 unhealthy)
|
* Response: JSON, statuses (200 - healthy, 503 unhealthy)
|
||||||
|
|
||||||
## `GET /api/v1/pleroma/conversations/read`
|
## `POST /api/v1/pleroma/conversations/read`
|
||||||
### Marks all user's conversations as read.
|
### Marks all user's conversations as read.
|
||||||
* Method `POST`
|
* Method `POST`
|
||||||
* Authentication: required
|
* Authentication: required
|
||||||
|
@ -426,7 +426,7 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa
|
||||||
* Authentication: required
|
* Authentication: required
|
||||||
* Params:
|
* Params:
|
||||||
* `file`: file needs to be uploaded with the multipart request or link to remote file.
|
* `file`: file needs to be uploaded with the multipart request or link to remote file.
|
||||||
* `shortcode`: (*optional*) shortcode for new emoji, must be uniq for all emoji. If not sended, shortcode will be taken from original filename.
|
* `shortcode`: (*optional*) shortcode for new emoji, must be unique for all emoji. If not sended, shortcode will be taken from original filename.
|
||||||
* `filename`: (*optional*) new emoji file name. If not specified will be taken from original filename.
|
* `filename`: (*optional*) new emoji file name. If not specified will be taken from original filename.
|
||||||
* Response: JSON, list of files for updated pack (hashmap -> shortcode => filename) with status 200, either error status with error message.
|
* Response: JSON, list of files for updated pack (hashmap -> shortcode => filename) with status 200, either error status with error message.
|
||||||
|
|
||||||
|
@ -536,7 +536,7 @@ Emoji reactions work a lot like favourites do. They make it possible to react to
|
||||||
```
|
```
|
||||||
|
|
||||||
## `GET /api/v1/pleroma/statuses/:id/reactions/:emoji`
|
## `GET /api/v1/pleroma/statuses/:id/reactions/:emoji`
|
||||||
### Get an object of emoji to account mappings with accounts that reacted to the post for a specific emoji`
|
### Get an object of emoji to account mappings with accounts that reacted to the post for a specific emoji
|
||||||
* Method: `GET`
|
* Method: `GET`
|
||||||
* Authentication: optional
|
* Authentication: optional
|
||||||
* Params: None
|
* Params: None
|
||||||
|
|
|
@ -69,3 +69,32 @@ mix pleroma.database update_users_following_followers_counts
|
||||||
```sh tab="From Source"
|
```sh tab="From Source"
|
||||||
mix pleroma.database fix_likes_collections
|
mix pleroma.database fix_likes_collections
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Vacuum the database
|
||||||
|
|
||||||
|
### Analyze
|
||||||
|
|
||||||
|
Running an `analyze` vacuum job can improve performance by updating statistics used by the query planner. **It is safe to cancel this.**
|
||||||
|
|
||||||
|
```sh tab="OTP"
|
||||||
|
./bin/pleroma_ctl database vacuum analyze
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh tab="From Source"
|
||||||
|
mix pleroma.database vacuum analyze
|
||||||
|
```
|
||||||
|
|
||||||
|
### Full
|
||||||
|
|
||||||
|
Running a `full` vacuum job rebuilds your entire database by reading all of the data and rewriting it into smaller
|
||||||
|
and more compact files with an optimized layout. This process will take a long time and use additional disk space as
|
||||||
|
it builds the files side-by-side the existing database files. It can make your database faster and use less disk space,
|
||||||
|
but should only be run if necessary. **It is safe to cancel this.**
|
||||||
|
|
||||||
|
```sh tab="OTP"
|
||||||
|
./bin/pleroma_ctl database vacuum full
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh tab="From Source"
|
||||||
|
mix pleroma.database vacuum full
|
||||||
|
```
|
|
@ -105,23 +105,23 @@ mix pleroma.user toggle_activated <nickname>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Unsubscribe local users from a user and deactivate the user
|
## Deactivate a user and unsubscribes local users from the user
|
||||||
```sh tab="OTP"
|
```sh tab="OTP"
|
||||||
./bin/pleroma_ctl user unsubscribe NICKNAME
|
./bin/pleroma_ctl user deactivate NICKNAME
|
||||||
```
|
```
|
||||||
|
|
||||||
```sh tab="From Source"
|
```sh tab="From Source"
|
||||||
mix pleroma.user unsubscribe NICKNAME
|
mix pleroma.user deactivate NICKNAME
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Unsubscribe local users from an instance and deactivate all accounts on it
|
## Deactivate all accounts from an instance and unsubscribe local users on it
|
||||||
```sh tab="OTP"
|
```sh tab="OTP"
|
||||||
./bin/pleroma_ctl user unsubscribe_all_from_instance <instance>
|
./bin/pleroma_ctl user deactivate_all_from_instance <instance>
|
||||||
```
|
```
|
||||||
|
|
||||||
```sh tab="From Source"
|
```sh tab="From Source"
|
||||||
mix pleroma.user unsubscribe_all_from_instance <instance>
|
mix pleroma.user deactivate_all_from_instance <instance>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
@ -177,4 +177,3 @@ mix pleroma.user untag <nickname> <tags>
|
||||||
```sh tab="From Source"
|
```sh tab="From Source"
|
||||||
mix pleroma.user toggle_confirmed <nickname>
|
mix pleroma.user toggle_confirmed <nickname>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -149,6 +149,11 @@ config :pleroma, :mrf_user_allowlist,
|
||||||
* `:strip_followers` removes followers from the ActivityPub recipient list, ensuring they won't be delivered to home timelines
|
* `:strip_followers` removes followers from the ActivityPub recipient list, ensuring they won't be delivered to home timelines
|
||||||
* `:reject` rejects the message entirely
|
* `:reject` rejects the message entirely
|
||||||
|
|
||||||
|
#### mrf_steal_emoji
|
||||||
|
* `hosts`: List of hosts to steal emojis from
|
||||||
|
* `rejected_shortcodes`: Regex-list of shortcodes to reject
|
||||||
|
* `size_limit`: File size limit (in bytes), checked before an emoji is saved to the disk
|
||||||
|
|
||||||
### :activitypub
|
### :activitypub
|
||||||
* `unfollow_blocked`: Whether blocks result in people getting unfollowed
|
* `unfollow_blocked`: Whether blocks result in people getting unfollowed
|
||||||
* `outgoing_blocks`: Whether to federate blocks to other instances
|
* `outgoing_blocks`: Whether to federate blocks to other instances
|
||||||
|
@ -249,6 +254,40 @@ This section describe PWA manifest instance-specific values. Currently this opti
|
||||||
* `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host/CDN fronts.
|
* `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host/CDN fronts.
|
||||||
* `proxy_opts`: All options defined in `Pleroma.ReverseProxy` documentation, defaults to `[max_body_length: (25*1_048_576)]`.
|
* `proxy_opts`: All options defined in `Pleroma.ReverseProxy` documentation, defaults to `[max_body_length: (25*1_048_576)]`.
|
||||||
* `whitelist`: List of domains to bypass the mediaproxy
|
* `whitelist`: List of domains to bypass the mediaproxy
|
||||||
|
* `invalidation`: options for remove media from cache after delete object:
|
||||||
|
* `enabled`: Enables purge cache
|
||||||
|
* `provider`: Which one of the [purge cache strategy](#purge-cache-strategy) to use.
|
||||||
|
|
||||||
|
### Purge cache strategy
|
||||||
|
|
||||||
|
#### Pleroma.Web.MediaProxy.Invalidation.Script
|
||||||
|
|
||||||
|
This strategy allow perform external bash script to purge cache.
|
||||||
|
Urls of attachments pass to script as arguments.
|
||||||
|
|
||||||
|
* `script_path`: path to external script.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```elixir
|
||||||
|
config :pleroma, Pleroma.Web.MediaProxy.Invalidation.Script,
|
||||||
|
script_path: "./installation/nginx-cache-purge.example"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Pleroma.Web.MediaProxy.Invalidation.Http
|
||||||
|
|
||||||
|
This strategy allow perform custom http request to purge cache.
|
||||||
|
|
||||||
|
* `method`: http method. default is `purge`
|
||||||
|
* `headers`: http headers. default is empty
|
||||||
|
* `options`: request options. default is empty
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```elixir
|
||||||
|
config :pleroma, Pleroma.Web.MediaProxy.Invalidation.Http,
|
||||||
|
method: :purge,
|
||||||
|
headers: [],
|
||||||
|
options: []
|
||||||
|
```
|
||||||
|
|
||||||
## Link previews
|
## Link previews
|
||||||
|
|
||||||
|
@ -459,6 +498,7 @@ the source code is here: https://github.com/koto-bank/kocaptcha. The default end
|
||||||
* `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host.
|
* `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host.
|
||||||
* `proxy_remote`: If you're using a remote uploader, Pleroma will proxy media requests instead of redirecting to it.
|
* `proxy_remote`: If you're using a remote uploader, Pleroma will proxy media requests instead of redirecting to it.
|
||||||
* `proxy_opts`: Proxy options, see `Pleroma.ReverseProxy` documentation.
|
* `proxy_opts`: Proxy options, see `Pleroma.ReverseProxy` documentation.
|
||||||
|
* `filename_display_max_length`: Set max length of a filename to display. 0 = no limit. Default: 30.
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
`strip_exif` has been replaced by `Pleroma.Upload.Filter.Mogrify`.
|
`strip_exif` has been replaced by `Pleroma.Upload.Filter.Mogrify`.
|
||||||
|
@ -619,24 +659,6 @@ config :pleroma, :workers,
|
||||||
* `enabled: false` corresponds to `config :pleroma, :workers, retries: [federator_outgoing: 1]`
|
* `enabled: false` corresponds to `config :pleroma, :workers, retries: [federator_outgoing: 1]`
|
||||||
* deprecated options: `max_jobs`, `initial_timeout`
|
* deprecated options: `max_jobs`, `initial_timeout`
|
||||||
|
|
||||||
### Pleroma.Scheduler
|
|
||||||
|
|
||||||
Configuration for [Quantum](https://github.com/quantum-elixir/quantum-core) jobs scheduler.
|
|
||||||
|
|
||||||
See [Quantum readme](https://github.com/quantum-elixir/quantum-core#usage) for the list of supported options.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```elixir
|
|
||||||
config :pleroma, Pleroma.Scheduler,
|
|
||||||
global: true,
|
|
||||||
overlap: true,
|
|
||||||
timezone: :utc,
|
|
||||||
jobs: [{"0 */6 * * * *", {Pleroma.Web.Websub, :refresh_subscriptions, []}}]
|
|
||||||
```
|
|
||||||
|
|
||||||
The above example defines a single job which invokes `Pleroma.Web.Websub.refresh_subscriptions()` every 6 hours ("0 */6 * * * *", [crontab format](https://en.wikipedia.org/wiki/Cron)).
|
|
||||||
|
|
||||||
## :web_push_encryption, :vapid_details
|
## :web_push_encryption, :vapid_details
|
||||||
|
|
||||||
Web Push Notifications configuration. You can use the mix task `mix web_push.gen.keypair` to generate it.
|
Web Push Notifications configuration. You can use the mix task `mix web_push.gen.keypair` to generate it.
|
||||||
|
@ -911,6 +933,21 @@ config :auto_linker,
|
||||||
|
|
||||||
Boolean, enables/disables in-database configuration. Read [Transfering the config to/from the database](../administration/CLI_tasks/config.md) for more information.
|
Boolean, enables/disables in-database configuration. Read [Transfering the config to/from the database](../administration/CLI_tasks/config.md) for more information.
|
||||||
|
|
||||||
|
## :database_config_whitelist
|
||||||
|
|
||||||
|
List of valid configuration sections which are allowed to be configured from the
|
||||||
|
database. Settings stored in the database before the whitelist is configured are
|
||||||
|
still applied, so it is suggested to only use the whitelist on instances that
|
||||||
|
have not migrated the config to the database.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```elixir
|
||||||
|
config :pleroma, :database_config_whitelist, [
|
||||||
|
{:pleroma, :instance},
|
||||||
|
{:pleroma, Pleroma.Web.Metadata},
|
||||||
|
{:auto_linker}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
### Multi-factor authentication - :two_factor_authentication
|
### Multi-factor authentication - :two_factor_authentication
|
||||||
* `totp` - a list containing TOTP configuration
|
* `totp` - a list containing TOTP configuration
|
||||||
|
|
31
docs/configuration/postgresql.md
Normal file
31
docs/configuration/postgresql.md
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# Optimizing your PostgreSQL performance
|
||||||
|
|
||||||
|
Pleroma performance depends to a large extent on good database performance. The default PostgreSQL settings are mostly fine, but often you can get better performance by changing a few settings.
|
||||||
|
|
||||||
|
You can use [PGTune](https://pgtune.leopard.in.ua) to get recommendations for your setup. If you do, set the "Number of Connections" field to 20, as Pleroma will only use 10 concurrent connections anyway. If you don't, it will give you advice that might even hurt your performance.
|
||||||
|
|
||||||
|
We also recommend not using the "Network Storage" option.
|
||||||
|
|
||||||
|
## Example configurations
|
||||||
|
|
||||||
|
Here are some configuration suggestions for PostgreSQL 10+.
|
||||||
|
|
||||||
|
### 1GB RAM, 1 CPU
|
||||||
|
```
|
||||||
|
shared_buffers = 256MB
|
||||||
|
effective_cache_size = 768MB
|
||||||
|
maintenance_work_mem = 64MB
|
||||||
|
work_mem = 13107kB
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2GB RAM, 2 CPU
|
||||||
|
```
|
||||||
|
shared_buffers = 512MB
|
||||||
|
effective_cache_size = 1536MB
|
||||||
|
maintenance_work_mem = 128MB
|
||||||
|
work_mem = 26214kB
|
||||||
|
max_worker_processes = 2
|
||||||
|
max_parallel_workers_per_gather = 1
|
||||||
|
max_parallel_workers = 2
|
||||||
|
```
|
||||||
|
|
38
docs/configuration/storing_remote_media.md
Normal file
38
docs/configuration/storing_remote_media.md
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# Storing Remote Media
|
||||||
|
|
||||||
|
Pleroma does not store remote/federated media by default. The best way to achieve this is to change Nginx to keep its reverse proxy cache
|
||||||
|
for a year and to activate the `MediaProxyWarmingPolicy` MRF policy in Pleroma which will automatically fetch all media through the proxy
|
||||||
|
as soon as the post is received by your instance.
|
||||||
|
|
||||||
|
## Nginx
|
||||||
|
|
||||||
|
```
|
||||||
|
proxy_cache_path /long/term/storage/path/pleroma-media-cache levels=1:2
|
||||||
|
keys_zone=pleroma_media_cache:10m inactive=1y use_temp_path=off;
|
||||||
|
|
||||||
|
location ~ ^/(media|proxy) {
|
||||||
|
proxy_cache pleroma_media_cache;
|
||||||
|
slice 1m;
|
||||||
|
proxy_cache_key $host$uri$is_args$args$slice_range;
|
||||||
|
proxy_set_header Range $slice_range;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_cache_valid 206 301 302 304 1h;
|
||||||
|
proxy_cache_valid 200 1y;
|
||||||
|
proxy_cache_use_stale error timeout invalid_header updating;
|
||||||
|
proxy_ignore_client_abort on;
|
||||||
|
proxy_buffering on;
|
||||||
|
chunked_transfer_encoding on;
|
||||||
|
proxy_ignore_headers Cache-Control Expires;
|
||||||
|
proxy_hide_header Cache-Control Expires;
|
||||||
|
proxy_pass http://127.0.0.1:4000;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pleroma
|
||||||
|
|
||||||
|
Add to your `prod.secret.exs`:
|
||||||
|
|
||||||
|
```
|
||||||
|
config :pleroma, :instance,
|
||||||
|
rewrite_policy: [Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy]
|
||||||
|
```
|
|
@ -38,8 +38,8 @@ sudo apt install git build-essential postgresql postgresql-contrib
|
||||||
* Download and add the Erlang repository:
|
* Download and add the Erlang repository:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
wget -P /tmp/ https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb
|
wget -P /tmp/ https://packages.erlang-solutions.com/erlang-solutions_2.0_all.deb
|
||||||
sudo dpkg -i /tmp/erlang-solutions_1.0_all.deb
|
sudo dpkg -i /tmp/erlang-solutions_2.0_all.deb
|
||||||
```
|
```
|
||||||
|
|
||||||
* Install Elixir and Erlang:
|
* Install Elixir and Erlang:
|
||||||
|
|
|
@ -40,8 +40,8 @@ sudo apt install git build-essential postgresql postgresql-contrib
|
||||||
|
|
||||||
* Erlangのリポジトリをダウンロードおよびインストールします。
|
* Erlangのリポジトリをダウンロードおよびインストールします。
|
||||||
```
|
```
|
||||||
wget -P /tmp/ https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb
|
wget -P /tmp/ https://packages.erlang-solutions.com/erlang-solutions_2.0_all.deb
|
||||||
sudo dpkg -i /tmp/erlang-solutions_1.0_all.deb
|
sudo dpkg -i /tmp/erlang-solutions_2.0_all.deb
|
||||||
```
|
```
|
||||||
|
|
||||||
* ElixirとErlangをインストールします、
|
* ElixirとErlangをインストールします、
|
||||||
|
|
|
@ -63,7 +63,7 @@ apt install postgresql-11-rum
|
||||||
```
|
```
|
||||||
|
|
||||||
#### (Optional) Performance configuration
|
#### (Optional) Performance configuration
|
||||||
For optimal performance, you may use [PGTune](https://pgtune.leopard.in.ua), don't forget to restart postgresql after editing the configuration
|
It is encouraged to check [Optimizing your PostgreSQL performance](../configuration/postgresql.md) document, for tips on PostgreSQL tuning.
|
||||||
|
|
||||||
```sh tab="Alpine"
|
```sh tab="Alpine"
|
||||||
rc-service postgresql restart
|
rc-service postgresql restart
|
||||||
|
|
|
@ -1,21 +1,45 @@
|
||||||
#!/sbin/openrc-run
|
#!/sbin/openrc-run
|
||||||
|
supervisor=supervise-daemon
|
||||||
# Requires OpenRC >= 0.35
|
|
||||||
directory=/opt/pleroma
|
|
||||||
|
|
||||||
command=/usr/bin/mix
|
|
||||||
command_args="phx.server"
|
|
||||||
command_user=pleroma:pleroma
|
command_user=pleroma:pleroma
|
||||||
command_background=1
|
command_background=1
|
||||||
|
|
||||||
export PORT=4000
|
|
||||||
export MIX_ENV=prod
|
|
||||||
|
|
||||||
# Ask process to terminate within 30 seconds, otherwise kill it
|
# Ask process to terminate within 30 seconds, otherwise kill it
|
||||||
retry="SIGTERM/30/SIGKILL/5"
|
retry="SIGTERM/30/SIGKILL/5"
|
||||||
|
|
||||||
pidfile="/var/run/pleroma.pid"
|
pidfile="/var/run/pleroma.pid"
|
||||||
|
directory=/opt/pleroma
|
||||||
|
healthcheck_delay=60
|
||||||
|
healthcheck_timer=30
|
||||||
|
|
||||||
|
: ${pleroma_port:-4000}
|
||||||
|
|
||||||
|
# Needs OpenRC >= 0.42
|
||||||
|
#respawn_max=0
|
||||||
|
#respawn_delay=5
|
||||||
|
|
||||||
|
# put pleroma_console=YES in /etc/conf.d/pleroma if you want to be able to
|
||||||
|
# connect to pleroma via an elixir console
|
||||||
|
if yesno "${pleroma_console}"; then
|
||||||
|
command=elixir
|
||||||
|
command_args="--name pleroma@127.0.0.1 --erl '-kernel inet_dist_listen_min 9001 inet_dist_listen_max 9001 inet_dist_use_interface {127,0,0,1}' -S mix phx.server"
|
||||||
|
|
||||||
|
start_post() {
|
||||||
|
einfo "You can get a console by using this command as pleroma's user:"
|
||||||
|
einfo "iex --name console@127.0.0.1 --remsh pleroma@127.0.0.1"
|
||||||
|
}
|
||||||
|
else
|
||||||
|
command=/usr/bin/mix
|
||||||
|
command_args="phx.server"
|
||||||
|
fi
|
||||||
|
|
||||||
|
export MIX_ENV=prod
|
||||||
|
|
||||||
depend() {
|
depend() {
|
||||||
need nginx postgresql
|
need nginx postgresql
|
||||||
|
}
|
||||||
|
|
||||||
|
healthcheck() {
|
||||||
|
# put pleroma_health=YES in /etc/conf.d/pleroma if you want healthchecking
|
||||||
|
# and make sure you have curl installed
|
||||||
|
yesno "$pleroma_health" || return 0
|
||||||
|
|
||||||
|
curl -q "localhost:${pleroma_port}/api/pleroma/healthcheck"
|
||||||
}
|
}
|
||||||
|
|
40
installation/nginx-cache-purge.sh.example
Executable file
40
installation/nginx-cache-purge.sh.example
Executable file
|
@ -0,0 +1,40 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# A simple shell script to delete a media from the Nginx cache.
|
||||||
|
|
||||||
|
SCRIPTNAME=${0##*/}
|
||||||
|
|
||||||
|
# NGINX cache directory
|
||||||
|
CACHE_DIRECTORY="/tmp/pleroma-media-cache"
|
||||||
|
|
||||||
|
## Return the files where the items are cached.
|
||||||
|
## $1 - the filename, can be a pattern .
|
||||||
|
## $2 - the cache directory.
|
||||||
|
## $3 - (optional) the number of parallel processes to run for grep.
|
||||||
|
get_cache_files() {
|
||||||
|
local max_parallel=${3-16}
|
||||||
|
find $2 -maxdepth 2 -type d | xargs -P $max_parallel -n 1 grep -E Rl "^KEY:.*$1" | sort -u
|
||||||
|
}
|
||||||
|
|
||||||
|
## Removes an item from the given cache zone.
|
||||||
|
## $1 - the filename, can be a pattern .
|
||||||
|
## $2 - the cache directory.
|
||||||
|
purge_item() {
|
||||||
|
for f in $(get_cache_files $1 $2); do
|
||||||
|
echo "found file: $f"
|
||||||
|
[ -f $f ] || continue
|
||||||
|
echo "Deleting $f from $2."
|
||||||
|
rm $f
|
||||||
|
done
|
||||||
|
} # purge_item
|
||||||
|
|
||||||
|
purge() {
|
||||||
|
for url in "$@"
|
||||||
|
do
|
||||||
|
echo "$SCRIPTNAME delete \`$url\` from cache ($CACHE_DIRECTORY)"
|
||||||
|
purge_item $url $CACHE_DIRECTORY
|
||||||
|
done
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
purge $1
|
|
@ -67,8 +67,7 @@ def run(["render_timeline", nickname | _] = args) do
|
||||||
Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{
|
Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{
|
||||||
activities: activities,
|
activities: activities,
|
||||||
for: user,
|
for: user,
|
||||||
as: :activity,
|
as: :activity
|
||||||
skip_relationships: true
|
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
defmodule Mix.Tasks.Pleroma.Database do
|
defmodule Mix.Tasks.Pleroma.Database do
|
||||||
alias Pleroma.Conversation
|
alias Pleroma.Conversation
|
||||||
|
alias Pleroma.Maintenance
|
||||||
alias Pleroma.Object
|
alias Pleroma.Object
|
||||||
alias Pleroma.Repo
|
alias Pleroma.Repo
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
|
@ -34,13 +35,7 @@ def run(["remove_embedded_objects" | args]) do
|
||||||
)
|
)
|
||||||
|
|
||||||
if Keyword.get(options, :vacuum) do
|
if Keyword.get(options, :vacuum) do
|
||||||
Logger.info("Runnning VACUUM FULL")
|
Maintenance.vacuum("full")
|
||||||
|
|
||||||
Repo.query!(
|
|
||||||
"vacuum full;",
|
|
||||||
[],
|
|
||||||
timeout: :infinity
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -94,13 +89,7 @@ def run(["prune_objects" | args]) do
|
||||||
|> Repo.delete_all(timeout: :infinity)
|
|> Repo.delete_all(timeout: :infinity)
|
||||||
|
|
||||||
if Keyword.get(options, :vacuum) do
|
if Keyword.get(options, :vacuum) do
|
||||||
Logger.info("Runnning VACUUM FULL")
|
Maintenance.vacuum("full")
|
||||||
|
|
||||||
Repo.query!(
|
|
||||||
"vacuum full;",
|
|
||||||
[],
|
|
||||||
timeout: :infinity
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -135,4 +124,10 @@ def run(["fix_likes_collections"]) do
|
||||||
end)
|
end)
|
||||||
|> Stream.run()
|
|> Stream.run()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def run(["vacuum", args]) do
|
||||||
|
start_pleroma()
|
||||||
|
|
||||||
|
Maintenance.vacuum(args)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
defmodule Mix.Tasks.Pleroma.Digest do
|
defmodule Mix.Tasks.Pleroma.Digest do
|
||||||
use Mix.Task
|
use Mix.Task
|
||||||
|
import Mix.Pleroma
|
||||||
|
|
||||||
@shortdoc "Manages digest emails"
|
@shortdoc "Manages digest emails"
|
||||||
@moduledoc File.read!("docs/administration/CLI_tasks/digest.md")
|
@moduledoc File.read!("docs/administration/CLI_tasks/digest.md")
|
||||||
|
@ -22,12 +23,10 @@ def run(["test", nickname | opts]) do
|
||||||
with %Swoosh.Email{} = email <- Pleroma.Emails.UserEmail.digest_email(patched_user) do
|
with %Swoosh.Email{} = email <- Pleroma.Emails.UserEmail.digest_email(patched_user) do
|
||||||
{:ok, _} = Pleroma.Emails.Mailer.deliver(email)
|
{:ok, _} = Pleroma.Emails.Mailer.deliver(email)
|
||||||
|
|
||||||
Mix.shell().info("Digest email have been sent to #{nickname} (#{user.email})")
|
shell_info("Digest email have been sent to #{nickname} (#{user.email})")
|
||||||
else
|
else
|
||||||
_ ->
|
_ ->
|
||||||
Mix.shell().info(
|
shell_info("Cound't find any mentions for #{nickname} since #{last_digest_emailed_at}")
|
||||||
"Cound't find any mentions for #{nickname} since #{last_digest_emailed_at}"
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -15,7 +15,7 @@ def run(["ls-packs" | args]) do
|
||||||
{options, [], []} = parse_global_opts(args)
|
{options, [], []} = parse_global_opts(args)
|
||||||
|
|
||||||
url_or_path = options[:manifest] || default_manifest()
|
url_or_path = options[:manifest] || default_manifest()
|
||||||
manifest = fetch_manifest(url_or_path)
|
manifest = fetch_and_decode(url_or_path)
|
||||||
|
|
||||||
Enum.each(manifest, fn {name, info} ->
|
Enum.each(manifest, fn {name, info} ->
|
||||||
to_print = [
|
to_print = [
|
||||||
|
@ -42,12 +42,12 @@ def run(["get-packs" | args]) do
|
||||||
|
|
||||||
url_or_path = options[:manifest] || default_manifest()
|
url_or_path = options[:manifest] || default_manifest()
|
||||||
|
|
||||||
manifest = fetch_manifest(url_or_path)
|
manifest = fetch_and_decode(url_or_path)
|
||||||
|
|
||||||
for pack_name <- pack_names do
|
for pack_name <- pack_names do
|
||||||
if Map.has_key?(manifest, pack_name) do
|
if Map.has_key?(manifest, pack_name) do
|
||||||
pack = manifest[pack_name]
|
pack = manifest[pack_name]
|
||||||
src_url = pack["src"]
|
src = pack["src"]
|
||||||
|
|
||||||
IO.puts(
|
IO.puts(
|
||||||
IO.ANSI.format([
|
IO.ANSI.format([
|
||||||
|
@ -57,11 +57,11 @@ def run(["get-packs" | args]) do
|
||||||
:normal,
|
:normal,
|
||||||
" from ",
|
" from ",
|
||||||
:underline,
|
:underline,
|
||||||
src_url
|
src
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
|
|
||||||
binary_archive = Tesla.get!(client(), src_url).body
|
{:ok, binary_archive} = fetch(src)
|
||||||
archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16()
|
archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16()
|
||||||
|
|
||||||
sha_status_text = ["SHA256 of ", :bright, pack_name, :normal, " source file is ", :bright]
|
sha_status_text = ["SHA256 of ", :bright, pack_name, :normal, " source file is ", :bright]
|
||||||
|
@ -74,8 +74,8 @@ def run(["get-packs" | args]) do
|
||||||
raise "Bad SHA256 for #{pack_name}"
|
raise "Bad SHA256 for #{pack_name}"
|
||||||
end
|
end
|
||||||
|
|
||||||
# The url specified in files should be in the same directory
|
# The location specified in files should be in the same directory
|
||||||
files_url =
|
files_loc =
|
||||||
url_or_path
|
url_or_path
|
||||||
|> Path.dirname()
|
|> Path.dirname()
|
||||||
|> Path.join(pack["files"])
|
|> Path.join(pack["files"])
|
||||||
|
@ -88,11 +88,11 @@ def run(["get-packs" | args]) do
|
||||||
:normal,
|
:normal,
|
||||||
" from ",
|
" from ",
|
||||||
:underline,
|
:underline,
|
||||||
files_url
|
files_loc
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
|
|
||||||
files = Tesla.get!(client(), files_url).body |> Jason.decode!()
|
files = fetch_and_decode(files_loc)
|
||||||
|
|
||||||
IO.puts(IO.ANSI.format(["Unpacking ", :bright, pack_name]))
|
IO.puts(IO.ANSI.format(["Unpacking ", :bright, pack_name]))
|
||||||
|
|
||||||
|
@ -237,16 +237,20 @@ def run(["gen-pack" | args]) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp fetch_manifest(from) do
|
defp fetch_and_decode(from) do
|
||||||
Jason.decode!(
|
with {:ok, json} <- fetch(from) do
|
||||||
if String.starts_with?(from, "http") do
|
Jason.decode!(json)
|
||||||
Tesla.get!(client(), from).body
|
end
|
||||||
else
|
|
||||||
File.read!(from)
|
|
||||||
end
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp fetch("http" <> _ = from) do
|
||||||
|
with {:ok, %{body: body}} <- Tesla.get(client(), from) do
|
||||||
|
{:ok, body}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fetch(path), do: File.read(path)
|
||||||
|
|
||||||
defp parse_global_opts(args) do
|
defp parse_global_opts(args) do
|
||||||
OptionParser.parse(
|
OptionParser.parse(
|
||||||
args,
|
args,
|
||||||
|
|
|
@ -147,6 +147,7 @@ def run(["gen" | rest]) do
|
||||||
"What directory should media uploads go in (when using the local uploader)?",
|
"What directory should media uploads go in (when using the local uploader)?",
|
||||||
Pleroma.Config.get([Pleroma.Uploaders.Local, :uploads])
|
Pleroma.Config.get([Pleroma.Uploaders.Local, :uploads])
|
||||||
)
|
)
|
||||||
|
|> Path.expand()
|
||||||
|
|
||||||
static_dir =
|
static_dir =
|
||||||
get_option(
|
get_option(
|
||||||
|
@ -155,6 +156,7 @@ def run(["gen" | rest]) do
|
||||||
"What directory should custom public files be read from (custom emojis, frontend bundle overrides, robots.txt, etc.)?",
|
"What directory should custom public files be read from (custom emojis, frontend bundle overrides, robots.txt, etc.)?",
|
||||||
Pleroma.Config.get([:instance, :static_dir])
|
Pleroma.Config.get([:instance, :static_dir])
|
||||||
)
|
)
|
||||||
|
|> Path.expand()
|
||||||
|
|
||||||
Config.put([:instance, :static_dir], static_dir)
|
Config.put([:instance, :static_dir], static_dir)
|
||||||
|
|
||||||
|
@ -204,7 +206,7 @@ def run(["gen" | rest]) do
|
||||||
shell_info("Writing the postgres script to #{psql_path}.")
|
shell_info("Writing the postgres script to #{psql_path}.")
|
||||||
File.write(psql_path, result_psql)
|
File.write(psql_path, result_psql)
|
||||||
|
|
||||||
write_robots_txt(indexable, template_dir)
|
write_robots_txt(static_dir, indexable, template_dir)
|
||||||
|
|
||||||
shell_info(
|
shell_info(
|
||||||
"\n All files successfully written! Refer to the installation instructions for your platform for next steps."
|
"\n All files successfully written! Refer to the installation instructions for your platform for next steps."
|
||||||
|
@ -224,15 +226,13 @@ def run(["gen" | rest]) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp write_robots_txt(indexable, template_dir) do
|
defp write_robots_txt(static_dir, indexable, template_dir) do
|
||||||
robots_txt =
|
robots_txt =
|
||||||
EEx.eval_file(
|
EEx.eval_file(
|
||||||
template_dir <> "/robots_txt.eex",
|
template_dir <> "/robots_txt.eex",
|
||||||
indexable: indexable
|
indexable: indexable
|
||||||
)
|
)
|
||||||
|
|
||||||
static_dir = Pleroma.Config.get([:instance, :static_dir], "instance/static/")
|
|
||||||
|
|
||||||
unless File.exists?(static_dir) do
|
unless File.exists?(static_dir) do
|
||||||
File.mkdir_p!(static_dir)
|
File.mkdir_p!(static_dir)
|
||||||
end
|
end
|
||||||
|
|
|
@ -144,28 +144,18 @@ def run(["reset_password", nickname]) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def run(["unsubscribe", nickname]) do
|
def run(["deactivate", nickname]) do
|
||||||
start_pleroma()
|
start_pleroma()
|
||||||
|
|
||||||
with %User{} = user <- User.get_cached_by_nickname(nickname) do
|
with %User{} = user <- User.get_cached_by_nickname(nickname) do
|
||||||
shell_info("Deactivating #{user.nickname}")
|
shell_info("Deactivating #{user.nickname}")
|
||||||
User.deactivate(user)
|
User.deactivate(user)
|
||||||
|
|
||||||
user
|
|
||||||
|> User.get_friends()
|
|
||||||
|> Enum.each(fn friend ->
|
|
||||||
user = User.get_cached_by_id(user.id)
|
|
||||||
|
|
||||||
shell_info("Unsubscribing #{friend.nickname} from #{user.nickname}")
|
|
||||||
User.unfollow(user, friend)
|
|
||||||
end)
|
|
||||||
|
|
||||||
:timer.sleep(500)
|
:timer.sleep(500)
|
||||||
|
|
||||||
user = User.get_cached_by_id(user.id)
|
user = User.get_cached_by_id(user.id)
|
||||||
|
|
||||||
if Enum.empty?(User.get_friends(user)) do
|
if Enum.empty?(Enum.filter(User.get_friends(user), & &1.local)) do
|
||||||
shell_info("Successfully unsubscribed all followers from #{user.nickname}")
|
shell_info("Successfully unsubscribed all local followers from #{user.nickname}")
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
_ ->
|
_ ->
|
||||||
|
@ -173,7 +163,7 @@ def run(["unsubscribe", nickname]) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def run(["unsubscribe_all_from_instance", instance]) do
|
def run(["deactivate_all_from_instance", instance]) do
|
||||||
start_pleroma()
|
start_pleroma()
|
||||||
|
|
||||||
Pleroma.User.Query.build(%{nickname: "@#{instance}"})
|
Pleroma.User.Query.build(%{nickname: "@#{instance}"})
|
||||||
|
@ -181,7 +171,7 @@ def run(["unsubscribe_all_from_instance", instance]) do
|
||||||
|> Stream.each(fn users ->
|
|> Stream.each(fn users ->
|
||||||
users
|
users
|
||||||
|> Enum.each(fn user ->
|
|> Enum.each(fn user ->
|
||||||
run(["unsubscribe", user.nickname])
|
run(["deactivate", user.nickname])
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|> Stream.run()
|
|> Stream.run()
|
||||||
|
|
|
@ -24,10 +24,7 @@ def by_ap_id(query \\ Activity, ap_id) do
|
||||||
|
|
||||||
@spec by_actor(query, String.t()) :: query
|
@spec by_actor(query, String.t()) :: query
|
||||||
def by_actor(query \\ Activity, actor) do
|
def by_actor(query \\ Activity, actor) do
|
||||||
from(
|
from(a in query, where: a.actor == ^actor)
|
||||||
activity in query,
|
|
||||||
where: fragment("(?)->>'actor' = ?", activity.data, ^actor)
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec by_author(query, User.t()) :: query
|
@spec by_author(query, User.t()) :: query
|
||||||
|
|
|
@ -56,7 +56,7 @@ def start(_type, _args) do
|
||||||
if (major == 22 and minor < 2) or major < 22 do
|
if (major == 22 and minor < 2) or major < 22 do
|
||||||
raise "
|
raise "
|
||||||
!!!OTP VERSION WARNING!!!
|
!!!OTP VERSION WARNING!!!
|
||||||
You are using gun adapter with OTP version #{version}, which doesn't support correct handling of unordered certificates chains.
|
You are using gun adapter with OTP version #{version}, which doesn't support correct handling of unordered certificates chains. Please update your Erlang/OTP to at least 22.2.
|
||||||
"
|
"
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
defmodule Pleroma.BBS.Authenticator do
|
defmodule Pleroma.BBS.Authenticator do
|
||||||
use Sshd.PasswordAuthenticator
|
use Sshd.PasswordAuthenticator
|
||||||
alias Comeonin.Pbkdf2
|
alias Pleroma.Plugs.AuthenticationPlug
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
|
|
||||||
def authenticate(username, password) do
|
def authenticate(username, password) do
|
||||||
|
@ -12,7 +12,7 @@ def authenticate(username, password) do
|
||||||
password = to_string(password)
|
password = to_string(password)
|
||||||
|
|
||||||
with %User{} = user <- User.get_by_nickname(username) do
|
with %User{} = user <- User.get_by_nickname(username) do
|
||||||
Pbkdf2.checkpw(password, user.password_hash)
|
AuthenticationPlug.checkpw(password, user.password_hash)
|
||||||
else
|
else
|
||||||
_e -> false
|
_e -> false
|
||||||
end
|
end
|
||||||
|
|
|
@ -66,7 +66,7 @@ def handle_command(%{user: user} = state, "r " <> text) do
|
||||||
|
|
||||||
with %Activity{} <- Activity.get_by_id(activity_id),
|
with %Activity{} <- Activity.get_by_id(activity_id),
|
||||||
{:ok, _activity} <-
|
{:ok, _activity} <-
|
||||||
CommonAPI.post(user, %{"status" => rest, "in_reply_to_status_id" => activity_id}) do
|
CommonAPI.post(user, %{status: rest, in_reply_to_status_id: activity_id}) do
|
||||||
IO.puts("Replied!")
|
IO.puts("Replied!")
|
||||||
else
|
else
|
||||||
_e -> IO.puts("Could not reply...")
|
_e -> IO.puts("Could not reply...")
|
||||||
|
@ -78,7 +78,7 @@ def handle_command(%{user: user} = state, "r " <> text) do
|
||||||
def handle_command(%{user: user} = state, "p " <> text) do
|
def handle_command(%{user: user} = state, "p " <> text) do
|
||||||
text = String.trim(text)
|
text = String.trim(text)
|
||||||
|
|
||||||
with {:ok, _activity} <- CommonAPI.post(user, %{"status" => text}) do
|
with {:ok, _activity} <- CommonAPI.post(user, %{status: text}) do
|
||||||
IO.puts("Posted!")
|
IO.puts("Posted!")
|
||||||
else
|
else
|
||||||
_e -> IO.puts("Could not post...")
|
_e -> IO.puts("Could not post...")
|
||||||
|
|
|
@ -278,6 +278,8 @@ defp do_convert({:proxy_url, {type, host, port}}) do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp do_convert({:partial_chain, entity}), do: %{"tuple" => [":partial_chain", inspect(entity)]}
|
||||||
|
|
||||||
defp do_convert(entity) when is_tuple(entity) do
|
defp do_convert(entity) when is_tuple(entity) do
|
||||||
value =
|
value =
|
||||||
entity
|
entity
|
||||||
|
@ -321,6 +323,15 @@ defp do_transform(%{"tuple" => [":proxy_url", %{"tuple" => [type, host, port]}]}
|
||||||
{:proxy_url, {do_transform_string(type), parse_host(host), port}}
|
{:proxy_url, {do_transform_string(type), parse_host(host), port}}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp do_transform(%{"tuple" => [":partial_chain", entity]}) do
|
||||||
|
{partial_chain, []} =
|
||||||
|
entity
|
||||||
|
|> String.replace(~r/[^\w|^{:,[|^,|^[|^\]^}|^\/|^\.|^"]^\s/, "")
|
||||||
|
|> Code.eval_string()
|
||||||
|
|
||||||
|
{:partial_chain, partial_chain}
|
||||||
|
end
|
||||||
|
|
||||||
defp do_transform(%{"tuple" => entity}) do
|
defp do_transform(%{"tuple" => entity}) do
|
||||||
Enum.reduce(entity, {}, fn val, acc -> Tuple.append(acc, do_transform(val)) end)
|
Enum.reduce(entity, {}, fn val, acc -> Tuple.append(acc, do_transform(val)) end)
|
||||||
end
|
end
|
||||||
|
|
|
@ -17,12 +17,13 @@ defmodule Pleroma.Constants do
|
||||||
"announcement_count",
|
"announcement_count",
|
||||||
"emoji",
|
"emoji",
|
||||||
"context_id",
|
"context_id",
|
||||||
"deleted_activity_id"
|
"deleted_activity_id",
|
||||||
|
"pleroma_internal"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
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
|
||||||
|
|
|
@ -63,7 +63,7 @@ def create_or_bump_for(activity, opts \\ []) do
|
||||||
ap_id when is_binary(ap_id) and byte_size(ap_id) > 0 <- object.data["context"] do
|
ap_id when is_binary(ap_id) and byte_size(ap_id) > 0 <- object.data["context"] do
|
||||||
{:ok, conversation} = create_for_ap_id(ap_id)
|
{:ok, conversation} = create_for_ap_id(ap_id)
|
||||||
|
|
||||||
users = User.get_users_from_set(activity.recipients, false)
|
users = User.get_users_from_set(activity.recipients, local_only: false)
|
||||||
|
|
||||||
participations =
|
participations =
|
||||||
Enum.map(users, fn user ->
|
Enum.map(users, fn user ->
|
||||||
|
|
|
@ -18,7 +18,6 @@ def compile do
|
||||||
with config <- Pleroma.Config.Loader.read("config/description.exs") do
|
with config <- Pleroma.Config.Loader.read("config/description.exs") do
|
||||||
config[:pleroma][:config_description]
|
config[:pleroma][:config_description]
|
||||||
|> Pleroma.Docs.Generator.convert_to_strings()
|
|> Pleroma.Docs.Generator.convert_to_strings()
|
||||||
|> Jason.encode!()
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,8 +14,10 @@ def new_users(to, users_and_statuses) do
|
||||||
styling = Pleroma.Config.get([Pleroma.Emails.UserEmail, :styling])
|
styling = Pleroma.Config.get([Pleroma.Emails.UserEmail, :styling])
|
||||||
|
|
||||||
logo_url =
|
logo_url =
|
||||||
Pleroma.Web.Endpoint.url() <>
|
Pleroma.Helpers.UriHelper.maybe_add_base(
|
||||||
Pleroma.Config.get([:frontend_configurations, :pleroma_fe, :logo])
|
Pleroma.Config.get([:frontend_configurations, :pleroma_fe, :logo]),
|
||||||
|
Pleroma.Web.Endpoint.url()
|
||||||
|
)
|
||||||
|
|
||||||
new()
|
new()
|
||||||
|> to({to.name, to.email})
|
|> to({to.name, to.email})
|
||||||
|
|
|
@ -16,162 +16,78 @@ defmodule Pleroma.Emoji.Pack do
|
||||||
|
|
||||||
alias Pleroma.Emoji
|
alias Pleroma.Emoji
|
||||||
|
|
||||||
@spec emoji_path() :: Path.t()
|
|
||||||
def emoji_path do
|
|
||||||
static = Pleroma.Config.get!([:instance, :static_dir])
|
|
||||||
Path.join(static, "emoji")
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec create(String.t()) :: :ok | {:error, File.posix()} | {:error, :empty_values}
|
@spec create(String.t()) :: :ok | {:error, File.posix()} | {:error, :empty_values}
|
||||||
def create(name) when byte_size(name) > 0 do
|
def create(name) do
|
||||||
dir = Path.join(emoji_path(), name)
|
with :ok <- validate_not_empty([name]),
|
||||||
|
dir <- Path.join(emoji_path(), name),
|
||||||
with :ok <- File.mkdir(dir) do
|
:ok <- File.mkdir(dir) do
|
||||||
%__MODULE__{
|
%__MODULE__{pack_file: Path.join(dir, "pack.json")}
|
||||||
pack_file: Path.join(dir, "pack.json")
|
|
||||||
}
|
|
||||||
|> save_pack()
|
|> save_pack()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def create(_), do: {:error, :empty_values}
|
@spec show(String.t()) :: {:ok, t()} | {:error, atom()}
|
||||||
|
def show(name) do
|
||||||
@spec show(String.t()) :: {:ok, t()} | {:loaded, nil} | {:error, :empty_values}
|
with :ok <- validate_not_empty([name]),
|
||||||
def show(name) when byte_size(name) > 0 do
|
{:ok, pack} <- load_pack(name) do
|
||||||
with {_, %__MODULE__{} = pack} <- {:loaded, load_pack(name)},
|
{:ok, validate_pack(pack)}
|
||||||
{_, pack} <- validate_pack(pack) do
|
|
||||||
{:ok, pack}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def show(_), do: {:error, :empty_values}
|
|
||||||
|
|
||||||
@spec delete(String.t()) ::
|
@spec delete(String.t()) ::
|
||||||
{:ok, [binary()]} | {:error, File.posix(), binary()} | {:error, :empty_values}
|
{:ok, [binary()]} | {:error, File.posix(), binary()} | {:error, :empty_values}
|
||||||
def delete(name) when byte_size(name) > 0 do
|
def delete(name) do
|
||||||
emoji_path()
|
with :ok <- validate_not_empty([name]) do
|
||||||
|> Path.join(name)
|
emoji_path()
|
||||||
|> File.rm_rf()
|
|> Path.join(name)
|
||||||
end
|
|> File.rm_rf()
|
||||||
|
|
||||||
def delete(_), do: {:error, :empty_values}
|
|
||||||
|
|
||||||
@spec add_file(String.t(), String.t(), Path.t(), Plug.Upload.t() | String.t()) ::
|
|
||||||
{:ok, t()} | {:error, File.posix()} | {:error, :empty_values}
|
|
||||||
def add_file(name, shortcode, filename, file)
|
|
||||||
when byte_size(name) > 0 and byte_size(shortcode) > 0 and byte_size(filename) > 0 do
|
|
||||||
with {_, nil} <- {:exists, Emoji.get(shortcode)},
|
|
||||||
{_, %__MODULE__{} = pack} <- {:loaded, load_pack(name)} do
|
|
||||||
file_path = Path.join(pack.path, filename)
|
|
||||||
|
|
||||||
create_subdirs(file_path)
|
|
||||||
|
|
||||||
case file do
|
|
||||||
%Plug.Upload{path: upload_path} ->
|
|
||||||
# Copy the uploaded file from the temporary directory
|
|
||||||
File.copy!(upload_path, file_path)
|
|
||||||
|
|
||||||
url when is_binary(url) ->
|
|
||||||
# Download and write the file
|
|
||||||
file_contents = Tesla.get!(url).body
|
|
||||||
File.write!(file_path, file_contents)
|
|
||||||
end
|
|
||||||
|
|
||||||
files = Map.put(pack.files, shortcode, filename)
|
|
||||||
|
|
||||||
updated_pack = %{pack | files: files}
|
|
||||||
|
|
||||||
case save_pack(updated_pack) do
|
|
||||||
:ok ->
|
|
||||||
Emoji.reload()
|
|
||||||
{:ok, updated_pack}
|
|
||||||
|
|
||||||
e ->
|
|
||||||
e
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_file(_, _, _, _), do: {:error, :empty_values}
|
@spec add_file(String.t(), String.t(), Path.t(), Plug.Upload.t() | String.t()) ::
|
||||||
|
{:ok, t()} | {:error, File.posix() | atom()}
|
||||||
defp create_subdirs(file_path) do
|
def add_file(name, shortcode, filename, file) do
|
||||||
if String.contains?(file_path, "/") do
|
with :ok <- validate_not_empty([name, shortcode, filename]),
|
||||||
file_path
|
:ok <- validate_emoji_not_exists(shortcode),
|
||||||
|> Path.dirname()
|
{:ok, pack} <- load_pack(name),
|
||||||
|> File.mkdir_p!()
|
:ok <- save_file(file, pack, filename),
|
||||||
|
{:ok, updated_pack} <- pack |> put_emoji(shortcode, filename) |> save_pack() do
|
||||||
|
Emoji.reload()
|
||||||
|
{:ok, updated_pack}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec delete_file(String.t(), String.t()) ::
|
@spec delete_file(String.t(), String.t()) ::
|
||||||
{:ok, t()} | {:error, File.posix()} | {:error, :empty_values}
|
{:ok, t()} | {:error, File.posix() | atom()}
|
||||||
def delete_file(name, shortcode) when byte_size(name) > 0 and byte_size(shortcode) > 0 do
|
def delete_file(name, shortcode) do
|
||||||
with {_, %__MODULE__{} = pack} <- {:loaded, load_pack(name)},
|
with :ok <- validate_not_empty([name, shortcode]),
|
||||||
{_, {filename, files}} when not is_nil(filename) <-
|
{:ok, pack} <- load_pack(name),
|
||||||
{:exists, Map.pop(pack.files, shortcode)},
|
:ok <- remove_file(pack, shortcode),
|
||||||
emoji <- Path.join(pack.path, filename),
|
{:ok, updated_pack} <- pack |> delete_emoji(shortcode) |> save_pack() do
|
||||||
{_, true} <- {:exists, File.exists?(emoji)} do
|
Emoji.reload()
|
||||||
emoji_dir = Path.dirname(emoji)
|
{:ok, updated_pack}
|
||||||
|
|
||||||
File.rm!(emoji)
|
|
||||||
|
|
||||||
if String.contains?(filename, "/") and File.ls!(emoji_dir) == [] do
|
|
||||||
File.rmdir!(emoji_dir)
|
|
||||||
end
|
|
||||||
|
|
||||||
updated_pack = %{pack | files: files}
|
|
||||||
|
|
||||||
case save_pack(updated_pack) do
|
|
||||||
:ok ->
|
|
||||||
Emoji.reload()
|
|
||||||
{:ok, updated_pack}
|
|
||||||
|
|
||||||
e ->
|
|
||||||
e
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete_file(_, _), do: {:error, :empty_values}
|
|
||||||
|
|
||||||
@spec update_file(String.t(), String.t(), String.t(), String.t(), boolean()) ::
|
@spec update_file(String.t(), String.t(), String.t(), String.t(), boolean()) ::
|
||||||
{:ok, t()} | {:error, File.posix()} | {:error, :empty_values}
|
{:ok, t()} | {:error, File.posix() | atom()}
|
||||||
def update_file(name, shortcode, new_shortcode, new_filename, force)
|
def update_file(name, shortcode, new_shortcode, new_filename, force) do
|
||||||
when byte_size(name) > 0 and byte_size(shortcode) > 0 and byte_size(new_shortcode) > 0 and
|
with :ok <- validate_not_empty([name, shortcode, new_shortcode, new_filename]),
|
||||||
byte_size(new_filename) > 0 do
|
{:ok, pack} <- load_pack(name),
|
||||||
with {_, %__MODULE__{} = pack} <- {:loaded, load_pack(name)},
|
{:ok, filename} <- get_filename(pack, shortcode),
|
||||||
{_, {filename, files}} when not is_nil(filename) <-
|
:ok <- validate_emoji_not_exists(new_shortcode, force),
|
||||||
{:exists, Map.pop(pack.files, shortcode)},
|
:ok <- rename_file(pack, filename, new_filename),
|
||||||
{_, true} <- {:not_used, force or is_nil(Emoji.get(new_shortcode))} do
|
{:ok, updated_pack} <-
|
||||||
old_path = Path.join(pack.path, filename)
|
pack
|
||||||
old_dir = Path.dirname(old_path)
|
|> delete_emoji(shortcode)
|
||||||
new_path = Path.join(pack.path, new_filename)
|
|> put_emoji(new_shortcode, new_filename)
|
||||||
|
|> save_pack() do
|
||||||
create_subdirs(new_path)
|
Emoji.reload()
|
||||||
|
{:ok, updated_pack}
|
||||||
:ok = File.rename(old_path, new_path)
|
|
||||||
|
|
||||||
if String.contains?(filename, "/") and File.ls!(old_dir) == [] do
|
|
||||||
File.rmdir!(old_dir)
|
|
||||||
end
|
|
||||||
|
|
||||||
files = Map.put(files, new_shortcode, new_filename)
|
|
||||||
|
|
||||||
updated_pack = %{pack | files: files}
|
|
||||||
|
|
||||||
case save_pack(updated_pack) do
|
|
||||||
:ok ->
|
|
||||||
Emoji.reload()
|
|
||||||
{:ok, updated_pack}
|
|
||||||
|
|
||||||
e ->
|
|
||||||
e
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_file(_, _, _, _, _), do: {:error, :empty_values}
|
@spec import_from_filesystem() :: {:ok, [String.t()]} | {:error, File.posix() | atom()}
|
||||||
|
|
||||||
@spec import_from_filesystem() :: {:ok, [String.t()]} | {:error, atom()}
|
|
||||||
def import_from_filesystem do
|
def import_from_filesystem do
|
||||||
emoji_path = emoji_path()
|
emoji_path = emoji_path()
|
||||||
|
|
||||||
|
@ -184,7 +100,7 @@ def import_from_filesystem do
|
||||||
File.dir?(path) and File.exists?(Path.join(path, "pack.json"))
|
File.dir?(path) and File.exists?(Path.join(path, "pack.json"))
|
||||||
end)
|
end)
|
||||||
|> Enum.map(&write_pack_contents/1)
|
|> Enum.map(&write_pack_contents/1)
|
||||||
|> Enum.filter(& &1)
|
|> Enum.reject(&is_nil/1)
|
||||||
|
|
||||||
{:ok, names}
|
{:ok, names}
|
||||||
else
|
else
|
||||||
|
@ -193,6 +109,117 @@ def import_from_filesystem do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec list_remote(String.t()) :: {:ok, map()} | {:error, atom()}
|
||||||
|
def list_remote(url) do
|
||||||
|
uri = url |> String.trim() |> URI.parse()
|
||||||
|
|
||||||
|
with :ok <- validate_shareable_packs_available(uri) do
|
||||||
|
uri
|
||||||
|
|> URI.merge("/api/pleroma/emoji/packs")
|
||||||
|
|> http_get()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec list_local() :: {:ok, map()}
|
||||||
|
def list_local do
|
||||||
|
with {:ok, results} <- list_packs_dir() do
|
||||||
|
packs =
|
||||||
|
results
|
||||||
|
|> Enum.map(fn name ->
|
||||||
|
case load_pack(name) do
|
||||||
|
{:ok, pack} -> pack
|
||||||
|
_ -> nil
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|> Enum.reject(&is_nil/1)
|
||||||
|
|> Map.new(fn pack -> {pack.name, validate_pack(pack)} end)
|
||||||
|
|
||||||
|
{:ok, packs}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec get_archive(String.t()) :: {:ok, binary()} | {:error, atom()}
|
||||||
|
def get_archive(name) do
|
||||||
|
with {:ok, pack} <- load_pack(name),
|
||||||
|
:ok <- validate_downloadable(pack) do
|
||||||
|
{:ok, fetch_archive(pack)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec download(String.t(), String.t(), String.t()) :: :ok | {:error, atom()}
|
||||||
|
def download(name, url, as) do
|
||||||
|
uri = url |> String.trim() |> URI.parse()
|
||||||
|
|
||||||
|
with :ok <- validate_shareable_packs_available(uri),
|
||||||
|
{:ok, remote_pack} <- uri |> URI.merge("/api/pleroma/emoji/packs/#{name}") |> http_get(),
|
||||||
|
{:ok, %{sha: sha, url: url} = pack_info} <- fetch_pack_info(remote_pack, uri, name),
|
||||||
|
{:ok, archive} <- download_archive(url, sha),
|
||||||
|
pack <- copy_as(remote_pack, as || name),
|
||||||
|
{:ok, _} = unzip(archive, pack_info, remote_pack, pack) do
|
||||||
|
# Fallback can't contain a pack.json file, since that would cause the fallback-src-sha256
|
||||||
|
# in it to depend on itself
|
||||||
|
if pack_info[:fallback] do
|
||||||
|
save_pack(pack)
|
||||||
|
else
|
||||||
|
{:ok, pack}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec save_metadata(map(), t()) :: {:ok, t()} | {:error, File.posix()}
|
||||||
|
def save_metadata(metadata, %__MODULE__{} = pack) do
|
||||||
|
pack
|
||||||
|
|> Map.put(:pack, metadata)
|
||||||
|
|> save_pack()
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec update_metadata(String.t(), map()) :: {:ok, t()} | {:error, File.posix()}
|
||||||
|
def update_metadata(name, data) do
|
||||||
|
with {:ok, pack} <- load_pack(name) do
|
||||||
|
if fallback_sha_changed?(pack, data) do
|
||||||
|
update_sha_and_save_metadata(pack, data)
|
||||||
|
else
|
||||||
|
save_metadata(data, pack)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec load_pack(String.t()) :: {:ok, t()} | {:error, :not_found}
|
||||||
|
def load_pack(name) do
|
||||||
|
pack_file = Path.join([emoji_path(), name, "pack.json"])
|
||||||
|
|
||||||
|
if File.exists?(pack_file) do
|
||||||
|
pack =
|
||||||
|
pack_file
|
||||||
|
|> File.read!()
|
||||||
|
|> from_json()
|
||||||
|
|> Map.put(:pack_file, pack_file)
|
||||||
|
|> Map.put(:path, Path.dirname(pack_file))
|
||||||
|
|> Map.put(:name, name)
|
||||||
|
|
||||||
|
{:ok, pack}
|
||||||
|
else
|
||||||
|
{:error, :not_found}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec emoji_path() :: Path.t()
|
||||||
|
defp emoji_path do
|
||||||
|
[:instance, :static_dir]
|
||||||
|
|> Pleroma.Config.get!()
|
||||||
|
|> Path.join("emoji")
|
||||||
|
end
|
||||||
|
|
||||||
|
defp validate_emoji_not_exists(shortcode, force \\ false)
|
||||||
|
defp validate_emoji_not_exists(_shortcode, true), do: :ok
|
||||||
|
|
||||||
|
defp validate_emoji_not_exists(shortcode, _) do
|
||||||
|
case Emoji.get(shortcode) do
|
||||||
|
nil -> :ok
|
||||||
|
_ -> {:error, :already_exists}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp write_pack_contents(path) do
|
defp write_pack_contents(path) do
|
||||||
pack = %__MODULE__{
|
pack = %__MODULE__{
|
||||||
files: files_from_path(path),
|
files: files_from_path(path),
|
||||||
|
@ -201,7 +228,7 @@ defp write_pack_contents(path) do
|
||||||
}
|
}
|
||||||
|
|
||||||
case save_pack(pack) do
|
case save_pack(pack) do
|
||||||
:ok -> Path.basename(path)
|
{:ok, _pack} -> Path.basename(path)
|
||||||
_ -> nil
|
_ -> nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -216,7 +243,8 @@ defp files_from_path(path) do
|
||||||
# FIXME: Copy-pasted from Pleroma.Emoji/load_from_file_stream/2
|
# FIXME: Copy-pasted from Pleroma.Emoji/load_from_file_stream/2
|
||||||
|
|
||||||
# Create a map of shortcodes to filenames from emoji.txt
|
# Create a map of shortcodes to filenames from emoji.txt
|
||||||
File.read!(txt_path)
|
txt_path
|
||||||
|
|> File.read!()
|
||||||
|> String.split("\n")
|
|> String.split("\n")
|
||||||
|> Enum.map(&String.trim/1)
|
|> Enum.map(&String.trim/1)
|
||||||
|> Enum.map(fn line ->
|
|> Enum.map(fn line ->
|
||||||
|
@ -226,21 +254,18 @@ defp files_from_path(path) do
|
||||||
[name, file | _] ->
|
[name, file | _] ->
|
||||||
file_dir_name = Path.dirname(file)
|
file_dir_name = Path.dirname(file)
|
||||||
|
|
||||||
file =
|
if String.ends_with?(path, file_dir_name) do
|
||||||
if String.ends_with?(path, file_dir_name) do
|
{name, Path.basename(file)}
|
||||||
Path.basename(file)
|
else
|
||||||
else
|
{name, file}
|
||||||
file
|
end
|
||||||
end
|
|
||||||
|
|
||||||
{name, file}
|
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|> Enum.filter(& &1)
|
|> Enum.reject(&is_nil/1)
|
||||||
|> Enum.into(%{})
|
|> Map.new()
|
||||||
else
|
else
|
||||||
# If there's no emoji.txt, assume all files
|
# If there's no emoji.txt, assume all files
|
||||||
# that are of certain extensions from the config are emojis and import them all
|
# that are of certain extensions from the config are emojis and import them all
|
||||||
|
@ -249,60 +274,20 @@ defp files_from_path(path) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec list_remote(String.t()) :: {:ok, map()}
|
|
||||||
def list_remote(url) do
|
|
||||||
uri =
|
|
||||||
url
|
|
||||||
|> String.trim()
|
|
||||||
|> URI.parse()
|
|
||||||
|
|
||||||
with {_, true} <- {:shareable, shareable_packs_available?(uri)} do
|
|
||||||
packs =
|
|
||||||
uri
|
|
||||||
|> URI.merge("/api/pleroma/emoji/packs")
|
|
||||||
|> to_string()
|
|
||||||
|> Tesla.get!()
|
|
||||||
|> Map.get(:body)
|
|
||||||
|> Jason.decode!()
|
|
||||||
|
|
||||||
{:ok, packs}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec list_local() :: {:ok, map()}
|
|
||||||
def list_local do
|
|
||||||
emoji_path = emoji_path()
|
|
||||||
|
|
||||||
# Create the directory first if it does not exist. This is probably the first request made
|
|
||||||
# with the API so it should be sufficient
|
|
||||||
with {:create_dir, :ok} <- {:create_dir, File.mkdir_p(emoji_path)},
|
|
||||||
{:ls, {:ok, results}} <- {:ls, File.ls(emoji_path)} do
|
|
||||||
packs =
|
|
||||||
results
|
|
||||||
|> Enum.map(&load_pack/1)
|
|
||||||
|> Enum.filter(& &1)
|
|
||||||
|> Enum.map(&validate_pack/1)
|
|
||||||
|> Map.new()
|
|
||||||
|
|
||||||
{:ok, packs}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp validate_pack(pack) do
|
defp validate_pack(pack) do
|
||||||
if downloadable?(pack) do
|
info =
|
||||||
archive = fetch_archive(pack)
|
if downloadable?(pack) do
|
||||||
archive_sha = :crypto.hash(:sha256, archive) |> Base.encode16()
|
archive = fetch_archive(pack)
|
||||||
|
archive_sha = :crypto.hash(:sha256, archive) |> Base.encode16()
|
||||||
|
|
||||||
info =
|
|
||||||
pack.pack
|
pack.pack
|
||||||
|> Map.put("can-download", true)
|
|> Map.put("can-download", true)
|
||||||
|> Map.put("download-sha256", archive_sha)
|
|> Map.put("download-sha256", archive_sha)
|
||||||
|
else
|
||||||
|
Map.put(pack.pack, "can-download", false)
|
||||||
|
end
|
||||||
|
|
||||||
{pack.name, Map.put(pack, :pack, info)}
|
Map.put(pack, :pack, info)
|
||||||
else
|
|
||||||
info = Map.put(pack.pack, "can-download", false)
|
|
||||||
{pack.name, Map.put(pack, :pack, info)}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp downloadable?(pack) do
|
defp downloadable?(pack) do
|
||||||
|
@ -315,26 +300,6 @@ defp downloadable?(pack) do
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec get_archive(String.t()) :: {:ok, binary()}
|
|
||||||
def get_archive(name) do
|
|
||||||
with {_, %__MODULE__{} = pack} <- {:exists?, load_pack(name)},
|
|
||||||
{_, true} <- {:can_download?, downloadable?(pack)} do
|
|
||||||
{:ok, fetch_archive(pack)}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp fetch_archive(pack) do
|
|
||||||
hash = :crypto.hash(:md5, File.read!(pack.pack_file))
|
|
||||||
|
|
||||||
case Cachex.get!(:emoji_packs_cache, pack.name) do
|
|
||||||
%{hash: ^hash, pack_data: archive} ->
|
|
||||||
archive
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
create_archive_and_cache(pack, hash)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp create_archive_and_cache(pack, hash) do
|
defp create_archive_and_cache(pack, hash) do
|
||||||
files = ['pack.json' | Enum.map(pack.files, fn {_, file} -> to_charlist(file) end)]
|
files = ['pack.json' | Enum.map(pack.files, fn {_, file} -> to_charlist(file) end)]
|
||||||
|
|
||||||
|
@ -356,152 +321,221 @@ defp create_archive_and_cache(pack, hash) do
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec download(String.t(), String.t(), String.t()) :: :ok
|
defp save_pack(pack) do
|
||||||
def download(name, url, as) do
|
with {:ok, json} <- Jason.encode(pack, pretty: true),
|
||||||
uri =
|
:ok <- File.write(pack.pack_file, json) do
|
||||||
url
|
|
||||||
|> String.trim()
|
|
||||||
|> URI.parse()
|
|
||||||
|
|
||||||
with {_, true} <- {:shareable, shareable_packs_available?(uri)} do
|
|
||||||
remote_pack =
|
|
||||||
uri
|
|
||||||
|> URI.merge("/api/pleroma/emoji/packs/#{name}")
|
|
||||||
|> to_string()
|
|
||||||
|> Tesla.get!()
|
|
||||||
|> Map.get(:body)
|
|
||||||
|> Jason.decode!()
|
|
||||||
|
|
||||||
result =
|
|
||||||
case remote_pack["pack"] do
|
|
||||||
%{"share-files" => true, "can-download" => true, "download-sha256" => sha} ->
|
|
||||||
{:ok,
|
|
||||||
%{
|
|
||||||
sha: sha,
|
|
||||||
url: URI.merge(uri, "/api/pleroma/emoji/packs/#{name}/archive") |> to_string()
|
|
||||||
}}
|
|
||||||
|
|
||||||
%{"fallback-src" => src, "fallback-src-sha256" => sha} when is_binary(src) ->
|
|
||||||
{:ok,
|
|
||||||
%{
|
|
||||||
sha: sha,
|
|
||||||
url: src,
|
|
||||||
fallback: true
|
|
||||||
}}
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
{:error,
|
|
||||||
"The pack was not set as shared and there is no fallback src to download from"}
|
|
||||||
end
|
|
||||||
|
|
||||||
with {:ok, %{sha: sha, url: url} = pinfo} <- result,
|
|
||||||
%{body: archive} <- Tesla.get!(url),
|
|
||||||
{_, true} <- {:checksum, Base.decode16!(sha) == :crypto.hash(:sha256, archive)} do
|
|
||||||
local_name = as || name
|
|
||||||
|
|
||||||
path = Path.join(emoji_path(), local_name)
|
|
||||||
|
|
||||||
pack = %__MODULE__{
|
|
||||||
name: local_name,
|
|
||||||
path: path,
|
|
||||||
files: remote_pack["files"],
|
|
||||||
pack_file: Path.join(path, "pack.json")
|
|
||||||
}
|
|
||||||
|
|
||||||
File.mkdir_p!(pack.path)
|
|
||||||
|
|
||||||
files = Enum.map(remote_pack["files"], fn {_, path} -> to_charlist(path) end)
|
|
||||||
# Fallback cannot contain a pack.json file
|
|
||||||
files = if pinfo[:fallback], do: files, else: ['pack.json' | files]
|
|
||||||
|
|
||||||
{:ok, _} = :zip.unzip(archive, cwd: to_charlist(pack.path), file_list: files)
|
|
||||||
|
|
||||||
# Fallback can't contain a pack.json file, since that would cause the fallback-src-sha256
|
|
||||||
# in it to depend on itself
|
|
||||||
if pinfo[:fallback] do
|
|
||||||
save_pack(pack)
|
|
||||||
end
|
|
||||||
|
|
||||||
:ok
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp save_pack(pack), do: File.write(pack.pack_file, Jason.encode!(pack, pretty: true))
|
|
||||||
|
|
||||||
@spec save_metadata(map(), t()) :: {:ok, t()} | {:error, File.posix()}
|
|
||||||
def save_metadata(metadata, %__MODULE__{} = pack) do
|
|
||||||
pack = Map.put(pack, :pack, metadata)
|
|
||||||
|
|
||||||
with :ok <- save_pack(pack) do
|
|
||||||
{:ok, pack}
|
{:ok, pack}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec update_metadata(String.t(), map()) :: {:ok, t()} | {:error, File.posix()}
|
|
||||||
def update_metadata(name, data) do
|
|
||||||
pack = load_pack(name)
|
|
||||||
|
|
||||||
fb_sha_changed? =
|
|
||||||
not is_nil(data["fallback-src"]) and data["fallback-src"] != pack.pack["fallback-src"]
|
|
||||||
|
|
||||||
with {_, true} <- {:update?, fb_sha_changed?},
|
|
||||||
{:ok, %{body: zip}} <- Tesla.get(data["fallback-src"]),
|
|
||||||
{:ok, f_list} <- :zip.unzip(zip, [:memory]),
|
|
||||||
{_, true} <- {:has_all_files?, has_all_files?(pack.files, f_list)} do
|
|
||||||
fallback_sha = :crypto.hash(:sha256, zip) |> Base.encode16()
|
|
||||||
|
|
||||||
data
|
|
||||||
|> Map.put("fallback-src-sha256", fallback_sha)
|
|
||||||
|> save_metadata(pack)
|
|
||||||
else
|
|
||||||
{:update?, _} -> save_metadata(data, pack)
|
|
||||||
e -> e
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Check if all files from the pack.json are in the archive
|
|
||||||
defp has_all_files?(files, f_list) do
|
|
||||||
Enum.all?(files, fn {_, from_manifest} ->
|
|
||||||
List.keyfind(f_list, to_charlist(from_manifest), 0)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec load_pack(String.t()) :: t() | nil
|
|
||||||
def load_pack(name) do
|
|
||||||
pack_file = Path.join([emoji_path(), name, "pack.json"])
|
|
||||||
|
|
||||||
if File.exists?(pack_file) do
|
|
||||||
pack_file
|
|
||||||
|> File.read!()
|
|
||||||
|> from_json()
|
|
||||||
|> Map.put(:pack_file, pack_file)
|
|
||||||
|> Map.put(:path, Path.dirname(pack_file))
|
|
||||||
|> Map.put(:name, name)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp from_json(json) do
|
defp from_json(json) do
|
||||||
map = Jason.decode!(json)
|
map = Jason.decode!(json)
|
||||||
|
|
||||||
struct(__MODULE__, %{files: map["files"], pack: map["pack"]})
|
struct(__MODULE__, %{files: map["files"], pack: map["pack"]})
|
||||||
end
|
end
|
||||||
|
|
||||||
defp shareable_packs_available?(uri) do
|
defp validate_shareable_packs_available(uri) do
|
||||||
uri
|
with {:ok, %{"links" => links}} <- uri |> URI.merge("/.well-known/nodeinfo") |> http_get(),
|
||||||
|> URI.merge("/.well-known/nodeinfo")
|
# Get the actual nodeinfo address and fetch it
|
||||||
|> to_string()
|
{:ok, %{"metadata" => %{"features" => features}}} <-
|
||||||
|> Tesla.get!()
|
links |> List.last() |> Map.get("href") |> http_get() do
|
||||||
|> Map.get(:body)
|
if Enum.member?(features, "shareable_emoji_packs") do
|
||||||
|> Jason.decode!()
|
:ok
|
||||||
|> Map.get("links")
|
else
|
||||||
|> List.last()
|
{:error, :not_shareable}
|
||||||
|> Map.get("href")
|
end
|
||||||
# Get the actual nodeinfo address and fetch it
|
end
|
||||||
|> Tesla.get!()
|
end
|
||||||
|> Map.get(:body)
|
|
||||||
|> Jason.decode!()
|
defp validate_not_empty(list) do
|
||||||
|> get_in(["metadata", "features"])
|
if Enum.all?(list, fn i -> is_binary(i) and i != "" end) do
|
||||||
|> Enum.member?("shareable_emoji_packs")
|
:ok
|
||||||
|
else
|
||||||
|
{:error, :empty_values}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp save_file(file, pack, filename) do
|
||||||
|
file_path = Path.join(pack.path, filename)
|
||||||
|
create_subdirs(file_path)
|
||||||
|
|
||||||
|
case file do
|
||||||
|
%Plug.Upload{path: upload_path} ->
|
||||||
|
# Copy the uploaded file from the temporary directory
|
||||||
|
with {:ok, _} <- File.copy(upload_path, file_path), do: :ok
|
||||||
|
|
||||||
|
url when is_binary(url) ->
|
||||||
|
# Download and write the file
|
||||||
|
file_contents = Tesla.get!(url).body
|
||||||
|
File.write(file_path, file_contents)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp put_emoji(pack, shortcode, filename) do
|
||||||
|
files = Map.put(pack.files, shortcode, filename)
|
||||||
|
%{pack | files: files}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp delete_emoji(pack, shortcode) do
|
||||||
|
files = Map.delete(pack.files, shortcode)
|
||||||
|
%{pack | files: files}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp rename_file(pack, filename, new_filename) do
|
||||||
|
old_path = Path.join(pack.path, filename)
|
||||||
|
new_path = Path.join(pack.path, new_filename)
|
||||||
|
create_subdirs(new_path)
|
||||||
|
|
||||||
|
with :ok <- File.rename(old_path, new_path) do
|
||||||
|
remove_dir_if_empty(old_path, filename)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create_subdirs(file_path) do
|
||||||
|
if String.contains?(file_path, "/") do
|
||||||
|
file_path
|
||||||
|
|> Path.dirname()
|
||||||
|
|> File.mkdir_p!()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp remove_file(pack, shortcode) do
|
||||||
|
with {:ok, filename} <- get_filename(pack, shortcode),
|
||||||
|
emoji <- Path.join(pack.path, filename),
|
||||||
|
:ok <- File.rm(emoji) do
|
||||||
|
remove_dir_if_empty(emoji, filename)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp remove_dir_if_empty(emoji, filename) do
|
||||||
|
dir = Path.dirname(emoji)
|
||||||
|
|
||||||
|
if String.contains?(filename, "/") and File.ls!(dir) == [] do
|
||||||
|
File.rmdir!(dir)
|
||||||
|
else
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_filename(pack, shortcode) do
|
||||||
|
with %{^shortcode => filename} when is_binary(filename) <- pack.files,
|
||||||
|
true <- pack.path |> Path.join(filename) |> File.exists?() do
|
||||||
|
{:ok, filename}
|
||||||
|
else
|
||||||
|
_ -> {:error, :doesnt_exist}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp http_get(%URI{} = url), do: url |> to_string() |> http_get()
|
||||||
|
|
||||||
|
defp http_get(url) do
|
||||||
|
with {:ok, %{body: body}} <- url |> Pleroma.HTTP.get() do
|
||||||
|
Jason.decode(body)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp list_packs_dir do
|
||||||
|
emoji_path = emoji_path()
|
||||||
|
# Create the directory first if it does not exist. This is probably the first request made
|
||||||
|
# with the API so it should be sufficient
|
||||||
|
with {:create_dir, :ok} <- {:create_dir, File.mkdir_p(emoji_path)},
|
||||||
|
{:ls, {:ok, results}} <- {:ls, File.ls(emoji_path)} do
|
||||||
|
{:ok, results}
|
||||||
|
else
|
||||||
|
{:create_dir, {:error, e}} -> {:error, :create_dir, e}
|
||||||
|
{:ls, {:error, e}} -> {:error, :ls, e}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp validate_downloadable(pack) do
|
||||||
|
if downloadable?(pack), do: :ok, else: {:error, :cant_download}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp copy_as(remote_pack, local_name) do
|
||||||
|
path = Path.join(emoji_path(), local_name)
|
||||||
|
|
||||||
|
%__MODULE__{
|
||||||
|
name: local_name,
|
||||||
|
path: path,
|
||||||
|
files: remote_pack["files"],
|
||||||
|
pack_file: Path.join(path, "pack.json")
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp unzip(archive, pack_info, remote_pack, local_pack) do
|
||||||
|
with :ok <- File.mkdir_p!(local_pack.path) do
|
||||||
|
files = Enum.map(remote_pack["files"], fn {_, path} -> to_charlist(path) end)
|
||||||
|
# Fallback cannot contain a pack.json file
|
||||||
|
files = if pack_info[:fallback], do: files, else: ['pack.json' | files]
|
||||||
|
|
||||||
|
:zip.unzip(archive, cwd: to_charlist(local_pack.path), file_list: files)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fetch_pack_info(remote_pack, uri, name) do
|
||||||
|
case remote_pack["pack"] do
|
||||||
|
%{"share-files" => true, "can-download" => true, "download-sha256" => sha} ->
|
||||||
|
{:ok,
|
||||||
|
%{
|
||||||
|
sha: sha,
|
||||||
|
url: URI.merge(uri, "/api/pleroma/emoji/packs/#{name}/archive") |> to_string()
|
||||||
|
}}
|
||||||
|
|
||||||
|
%{"fallback-src" => src, "fallback-src-sha256" => sha} when is_binary(src) ->
|
||||||
|
{:ok,
|
||||||
|
%{
|
||||||
|
sha: sha,
|
||||||
|
url: src,
|
||||||
|
fallback: true
|
||||||
|
}}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{:error, "The pack was not set as shared and there is no fallback src to download from"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp download_archive(url, sha) do
|
||||||
|
with {:ok, %{body: archive}} <- Tesla.get(url) do
|
||||||
|
if Base.decode16!(sha) == :crypto.hash(:sha256, archive) do
|
||||||
|
{:ok, archive}
|
||||||
|
else
|
||||||
|
{:error, :invalid_checksum}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fetch_archive(pack) do
|
||||||
|
hash = :crypto.hash(:md5, File.read!(pack.pack_file))
|
||||||
|
|
||||||
|
case Cachex.get!(:emoji_packs_cache, pack.name) do
|
||||||
|
%{hash: ^hash, pack_data: archive} -> archive
|
||||||
|
_ -> create_archive_and_cache(pack, hash)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fallback_sha_changed?(pack, data) do
|
||||||
|
is_binary(data[:"fallback-src"]) and data[:"fallback-src"] != pack.pack["fallback-src"]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp update_sha_and_save_metadata(pack, data) do
|
||||||
|
with {:ok, %{body: zip}} <- Tesla.get(data[:"fallback-src"]),
|
||||||
|
:ok <- validate_has_all_files(pack, zip) do
|
||||||
|
fallback_sha = :sha256 |> :crypto.hash(zip) |> Base.encode16()
|
||||||
|
|
||||||
|
data
|
||||||
|
|> Map.put("fallback-src-sha256", fallback_sha)
|
||||||
|
|> save_metadata(pack)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp validate_has_all_files(pack, zip) do
|
||||||
|
with {:ok, f_list} <- :zip.unzip(zip, [:memory]) do
|
||||||
|
# Check if all files from the pack.json are in the archive
|
||||||
|
pack.files
|
||||||
|
|> Enum.all?(fn {_, from_manifest} ->
|
||||||
|
List.keyfind(f_list, to_charlist(from_manifest), 0)
|
||||||
|
end)
|
||||||
|
|> if(do: :ok, else: {:error, :incomplete})
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -29,7 +29,7 @@ defmodule Pleroma.Healthcheck do
|
||||||
@spec system_info() :: t()
|
@spec system_info() :: t()
|
||||||
def system_info do
|
def system_info do
|
||||||
%Healthcheck{
|
%Healthcheck{
|
||||||
memory_used: Float.round(:erlang.memory(:total) / 1024 / 1024, 2)
|
memory_used: Float.round(:recon_alloc.memory(:allocated) / 1024 / 1024, 2)
|
||||||
}
|
}
|
||||||
|> assign_db_info()
|
|> assign_db_info()
|
||||||
|> assign_job_queue_stats()
|
|> assign_job_queue_stats()
|
||||||
|
|
|
@ -17,11 +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
|
def maybe_add_base("/" <> uri, base), do: Path.join([base, uri])
|
||||||
if param_value do
|
def maybe_add_base(uri, _base), do: uri
|
||||||
Map.put(params, param_name, param_value)
|
|
||||||
else
|
|
||||||
params
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -22,22 +22,7 @@ def options(connection_opts \\ [], %URI{} = uri) do
|
||||||
|> Pleroma.HTTP.AdapterHelper.maybe_add_proxy(proxy)
|
|> Pleroma.HTTP.AdapterHelper.maybe_add_proxy(proxy)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp add_scheme_opts(opts, %URI{scheme: "http"}), do: opts
|
defp add_scheme_opts(opts, _), do: opts
|
||||||
|
|
||||||
defp add_scheme_opts(opts, %URI{scheme: "https", host: host}) do
|
|
||||||
ssl_opts = [
|
|
||||||
ssl_options: [
|
|
||||||
# Workaround for remote server certificate chain issues
|
|
||||||
partial_chain: &:hackney_connect.partial_chain/1,
|
|
||||||
|
|
||||||
# We don't support TLS v1.3 yet
|
|
||||||
versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"],
|
|
||||||
server_name_indication: to_charlist(host)
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
Keyword.merge(opts, ssl_opts)
|
|
||||||
end
|
|
||||||
|
|
||||||
def after_request(_), do: :ok
|
def after_request(_), do: :ok
|
||||||
end
|
end
|
||||||
|
|
37
lib/pleroma/maintenance.ex
Normal file
37
lib/pleroma/maintenance.ex
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Maintenance do
|
||||||
|
alias Pleroma.Repo
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
def vacuum(args) do
|
||||||
|
case args do
|
||||||
|
"analyze" ->
|
||||||
|
Logger.info("Runnning VACUUM ANALYZE.")
|
||||||
|
|
||||||
|
Repo.query!(
|
||||||
|
"vacuum analyze;",
|
||||||
|
[],
|
||||||
|
timeout: :infinity
|
||||||
|
)
|
||||||
|
|
||||||
|
"full" ->
|
||||||
|
Logger.info("Runnning VACUUM FULL.")
|
||||||
|
|
||||||
|
Logger.warn(
|
||||||
|
"Re-packing your entire database may take a while and will consume extra disk space during the process."
|
||||||
|
)
|
||||||
|
|
||||||
|
Repo.query!(
|
||||||
|
"vacuum full;",
|
||||||
|
[],
|
||||||
|
timeout: :infinity
|
||||||
|
)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
Logger.error("Error: invalid vacuum argument.")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
15
lib/pleroma/maps.ex
Normal file
15
lib/pleroma/maps.ex
Normal 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
|
|
@ -1,5 +1,5 @@
|
||||||
# Pleroma: A lightweight social networking server
|
# Pleroma: A lightweight social networking server
|
||||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
defmodule Pleroma.MFA do
|
defmodule Pleroma.MFA do
|
||||||
|
@ -7,7 +7,6 @@ defmodule Pleroma.MFA do
|
||||||
The MFA context.
|
The MFA context.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
alias Comeonin.Pbkdf2
|
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
|
|
||||||
alias Pleroma.MFA.BackupCodes
|
alias Pleroma.MFA.BackupCodes
|
||||||
|
@ -72,7 +71,7 @@ def invalidate_backup_code(%User{} = user, hash_code) do
|
||||||
@spec generate_backup_codes(User.t()) :: {:ok, list(binary)} | {:error, String.t()}
|
@spec generate_backup_codes(User.t()) :: {:ok, list(binary)} | {:error, String.t()}
|
||||||
def generate_backup_codes(%User{} = user) do
|
def generate_backup_codes(%User{} = user) do
|
||||||
with codes <- BackupCodes.generate(),
|
with codes <- BackupCodes.generate(),
|
||||||
hashed_codes <- Enum.map(codes, &Pbkdf2.hashpwsalt/1),
|
hashed_codes <- Enum.map(codes, &Pbkdf2.hash_pwd_salt/1),
|
||||||
changeset <- Changeset.cast_backup_codes(user, hashed_codes),
|
changeset <- Changeset.cast_backup_codes(user, hashed_codes),
|
||||||
{:ok, _} <- User.update_and_set_cache(changeset) do
|
{:ok, _} <- User.update_and_set_cache(changeset) do
|
||||||
{:ok, codes}
|
{:ok, codes}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# Pleroma: A lightweight social networking server
|
# Pleroma: A lightweight social networking server
|
||||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
defmodule Pleroma.MFA.BackupCodes do
|
defmodule Pleroma.MFA.BackupCodes do
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# Pleroma: A lightweight social networking server
|
# Pleroma: A lightweight social networking server
|
||||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
defmodule Pleroma.MFA.Changeset do
|
defmodule Pleroma.MFA.Changeset do
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# Pleroma: A lightweight social networking server
|
# Pleroma: A lightweight social networking server
|
||||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
defmodule Pleroma.MFA.Settings do
|
defmodule Pleroma.MFA.Settings do
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# Pleroma: A lightweight social networking server
|
# Pleroma: A lightweight social networking server
|
||||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
defmodule Pleroma.MFA.Token do
|
defmodule Pleroma.MFA.Token do
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# Pleroma: A lightweight social networking server
|
# Pleroma: A lightweight social networking server
|
||||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
defmodule Pleroma.MFA.TOTP do
|
defmodule Pleroma.MFA.TOTP do
|
||||||
|
|
|
@ -92,8 +92,9 @@ def for_user_query(user, opts \\ %{}) do
|
||||||
|> join(:left, [n, a], object in Object,
|
|> join(:left, [n, a], object in Object,
|
||||||
on:
|
on:
|
||||||
fragment(
|
fragment(
|
||||||
"(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)",
|
"(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')",
|
||||||
object.data,
|
object.data,
|
||||||
|
a.data,
|
||||||
a.data
|
a.data
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -224,18 +225,8 @@ def set_read_up_to(%{id: user_id} = user, id) do
|
||||||
|> Marker.multi_set_last_read_id(user, "notifications")
|
|> Marker.multi_set_last_read_id(user, "notifications")
|
||||||
|> Repo.transaction()
|
|> Repo.transaction()
|
||||||
|
|
||||||
Notification
|
for_user_query(user)
|
||||||
|> where([n], n.id in ^notification_ids)
|
|> where([n], n.id in ^notification_ids)
|
||||||
|> join(:inner, [n], activity in assoc(n, :activity))
|
|
||||||
|> join(:left, [n, a], object in Object,
|
|
||||||
on:
|
|
||||||
fragment(
|
|
||||||
"(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)",
|
|
||||||
object.data,
|
|
||||||
a.data
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|> preload([n, a, o], activity: {a, object: o})
|
|
||||||
|> Repo.all()
|
|> Repo.all()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -368,15 +359,10 @@ def get_notified_from_activity(activity, local_only \\ true)
|
||||||
|
|
||||||
def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only)
|
def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only)
|
||||||
when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReact"] do
|
when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReact"] do
|
||||||
potential_receiver_ap_ids =
|
potential_receiver_ap_ids = get_potential_receiver_ap_ids(activity)
|
||||||
[]
|
|
||||||
|> Utils.maybe_notify_to_recipients(activity)
|
|
||||||
|> Utils.maybe_notify_mentioned_recipients(activity)
|
|
||||||
|> Utils.maybe_notify_subscribers(activity)
|
|
||||||
|> Utils.maybe_notify_followers(activity)
|
|
||||||
|> Enum.uniq()
|
|
||||||
|
|
||||||
potential_receivers = User.get_users_from_set(potential_receiver_ap_ids, local_only)
|
potential_receivers =
|
||||||
|
User.get_users_from_set(potential_receiver_ap_ids, local_only: local_only)
|
||||||
|
|
||||||
notification_enabled_ap_ids =
|
notification_enabled_ap_ids =
|
||||||
potential_receiver_ap_ids
|
potential_receiver_ap_ids
|
||||||
|
@ -392,6 +378,27 @@ def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, lo
|
||||||
|
|
||||||
def get_notified_from_activity(_, _local_only), do: {[], []}
|
def get_notified_from_activity(_, _local_only), do: {[], []}
|
||||||
|
|
||||||
|
# For some activities, only notify the author of the object
|
||||||
|
def get_potential_receiver_ap_ids(%{data: %{"type" => type, "object" => object_id}})
|
||||||
|
when type in ~w{Like Announce EmojiReact} do
|
||||||
|
case Object.get_cached_by_ap_id(object_id) do
|
||||||
|
%Object{data: %{"actor" => actor}} ->
|
||||||
|
[actor]
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_potential_receiver_ap_ids(activity) do
|
||||||
|
[]
|
||||||
|
|> Utils.maybe_notify_to_recipients(activity)
|
||||||
|
|> Utils.maybe_notify_mentioned_recipients(activity)
|
||||||
|
|> Utils.maybe_notify_subscribers(activity)
|
||||||
|
|> Utils.maybe_notify_followers(activity)
|
||||||
|
|> Enum.uniq()
|
||||||
|
end
|
||||||
|
|
||||||
@doc "Filters out AP IDs domain-blocking and not following the activity's actor"
|
@doc "Filters out AP IDs domain-blocking and not following the activity's actor"
|
||||||
def exclude_domain_blocker_ap_ids(ap_ids, activity, preloaded_users \\ [])
|
def exclude_domain_blocker_ap_ids(ap_ids, activity, preloaded_users \\ [])
|
||||||
|
|
||||||
|
|
|
@ -9,11 +9,13 @@ defmodule Pleroma.Object do
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
|
|
||||||
alias Pleroma.Activity
|
alias Pleroma.Activity
|
||||||
|
alias Pleroma.Config
|
||||||
alias Pleroma.Object
|
alias Pleroma.Object
|
||||||
alias Pleroma.Object.Fetcher
|
alias Pleroma.Object.Fetcher
|
||||||
alias Pleroma.ObjectTombstone
|
alias Pleroma.ObjectTombstone
|
||||||
alias Pleroma.Repo
|
alias Pleroma.Repo
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
|
alias Pleroma.Workers.AttachmentsCleanupWorker
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
|
@ -138,12 +140,17 @@ def normalize(ap_id, true, options) when is_binary(ap_id) do
|
||||||
|
|
||||||
def normalize(_, _, _), do: nil
|
def normalize(_, _, _), do: nil
|
||||||
|
|
||||||
# Owned objects can only be mutated by their owner
|
# Owned objects can only be accessed by their owner
|
||||||
def authorize_mutation(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}),
|
def authorize_access(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}) do
|
||||||
do: actor == ap_id
|
if actor == ap_id do
|
||||||
|
:ok
|
||||||
|
else
|
||||||
|
{:error, :forbidden}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Legacy objects can be mutated by anybody
|
# Legacy objects can be accessed by anybody
|
||||||
def authorize_mutation(%Object{}, %User{}), do: true
|
def authorize_access(%Object{}, %User{}), do: :ok
|
||||||
|
|
||||||
@spec get_cached_by_ap_id(String.t()) :: Object.t() | nil
|
@spec get_cached_by_ap_id(String.t()) :: Object.t() | nil
|
||||||
def get_cached_by_ap_id(ap_id) do
|
def get_cached_by_ap_id(ap_id) do
|
||||||
|
@ -183,27 +190,37 @@ def swap_object_with_tombstone(object) do
|
||||||
def delete(%Object{data: %{"id" => id}} = object) do
|
def delete(%Object{data: %{"id" => id}} = object) do
|
||||||
with {:ok, _obj} = swap_object_with_tombstone(object),
|
with {:ok, _obj} = swap_object_with_tombstone(object),
|
||||||
deleted_activity = Activity.delete_all_by_object_ap_id(id),
|
deleted_activity = Activity.delete_all_by_object_ap_id(id),
|
||||||
{:ok, true} <- Cachex.del(:object_cache, "object:#{id}"),
|
{:ok, _} <- invalid_object_cache(object) do
|
||||||
{:ok, _} <- Cachex.del(:web_resp_cache, URI.parse(id).path) do
|
cleanup_attachments(
|
||||||
with true <- Pleroma.Config.get([:instance, :cleanup_attachments]) do
|
Config.get([:instance, :cleanup_attachments]),
|
||||||
{:ok, _} =
|
%{"object" => object}
|
||||||
Pleroma.Workers.AttachmentsCleanupWorker.enqueue("cleanup_attachments", %{
|
)
|
||||||
"object" => object
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
{:ok, object, deleted_activity}
|
{:ok, object, deleted_activity}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def prune(%Object{data: %{"id" => id}} = object) do
|
@spec cleanup_attachments(boolean(), %{required(:object) => map()}) ::
|
||||||
|
{:ok, Oban.Job.t() | nil}
|
||||||
|
def cleanup_attachments(true, %{"object" => _} = params) do
|
||||||
|
AttachmentsCleanupWorker.enqueue("cleanup_attachments", params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def cleanup_attachments(_, _), do: {:ok, nil}
|
||||||
|
|
||||||
|
def prune(%Object{data: %{"id" => _id}} = object) do
|
||||||
with {:ok, object} <- Repo.delete(object),
|
with {:ok, object} <- Repo.delete(object),
|
||||||
{:ok, true} <- Cachex.del(:object_cache, "object:#{id}"),
|
{:ok, _} <- invalid_object_cache(object) do
|
||||||
{:ok, _} <- Cachex.del(:web_resp_cache, URI.parse(id).path) do
|
|
||||||
{:ok, object}
|
{:ok, object}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def invalid_object_cache(%Object{data: %{"id" => id}}) do
|
||||||
|
with {:ok, true} <- Cachex.del(:object_cache, "object:#{id}") do
|
||||||
|
Cachex.del(:web_resp_cache, URI.parse(id).path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def set_cache(%Object{data: %{"id" => ap_id}} = object) do
|
def set_cache(%Object{data: %{"id" => ap_id}} = object) do
|
||||||
Cachex.put(:object_cache, "object:#{ap_id}", object)
|
Cachex.put(:object_cache, "object:#{ap_id}", object)
|
||||||
{:ok, object}
|
{:ok, object}
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
defmodule Pleroma.Plugs.AuthenticationPlug do
|
defmodule Pleroma.Plugs.AuthenticationPlug do
|
||||||
alias Comeonin.Pbkdf2
|
|
||||||
alias Pleroma.Plugs.OAuthScopesPlug
|
alias Pleroma.Plugs.OAuthScopesPlug
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
|
|
||||||
|
@ -17,8 +16,13 @@ def checkpw(password, "$6" <> _ = password_hash) do
|
||||||
:crypt.crypt(password, password_hash) == password_hash
|
:crypt.crypt(password, password_hash) == password_hash
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def checkpw(password, "$2" <> _ = password_hash) do
|
||||||
|
# Handle bcrypt passwords for Mastodon migration
|
||||||
|
Bcrypt.verify_pass(password, password_hash)
|
||||||
|
end
|
||||||
|
|
||||||
def checkpw(password, "$pbkdf2" <> _ = password_hash) do
|
def checkpw(password, "$pbkdf2" <> _ = password_hash) do
|
||||||
Pbkdf2.checkpw(password, password_hash)
|
Pbkdf2.verify_pass(password, password_hash)
|
||||||
end
|
end
|
||||||
|
|
||||||
def checkpw(_password, _password_hash) do
|
def checkpw(_password, _password_hash) do
|
||||||
|
@ -26,6 +30,25 @@ def checkpw(_password, _password_hash) do
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def maybe_update_password(%User{password_hash: "$2" <> _} = user, password) do
|
||||||
|
do_update_password(user, password)
|
||||||
|
end
|
||||||
|
|
||||||
|
def maybe_update_password(%User{password_hash: "$6" <> _} = user, password) do
|
||||||
|
do_update_password(user, password)
|
||||||
|
end
|
||||||
|
|
||||||
|
def maybe_update_password(user, _), do: {:ok, user}
|
||||||
|
|
||||||
|
defp do_update_password(user, password) do
|
||||||
|
user
|
||||||
|
|> User.password_update_changeset(%{
|
||||||
|
"password" => password,
|
||||||
|
"password_confirmation" => password
|
||||||
|
})
|
||||||
|
|> Pleroma.Repo.update()
|
||||||
|
end
|
||||||
|
|
||||||
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
|
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
|
||||||
|
|
||||||
def call(
|
def call(
|
||||||
|
@ -37,7 +60,9 @@ def call(
|
||||||
} = conn,
|
} = conn,
|
||||||
_
|
_
|
||||||
) do
|
) do
|
||||||
if Pbkdf2.checkpw(password, password_hash) do
|
if checkpw(password, password_hash) do
|
||||||
|
{:ok, auth_user} = maybe_update_password(auth_user, password)
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> assign(:user, auth_user)
|
|> assign(:user, auth_user)
|
||||||
|> OAuthScopesPlug.skip_plug()
|
|> OAuthScopesPlug.skip_plug()
|
||||||
|
@ -47,7 +72,7 @@ def call(
|
||||||
end
|
end
|
||||||
|
|
||||||
def call(%{assigns: %{auth_credentials: %{password: _}}} = conn, _) do
|
def call(%{assigns: %{auth_credentials: %{password: _}}} = conn, _) do
|
||||||
Pbkdf2.dummy_checkpw()
|
Pbkdf2.no_user_verify()
|
||||||
conn
|
conn
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ defp headers do
|
||||||
{"x-content-type-options", "nosniff"},
|
{"x-content-type-options", "nosniff"},
|
||||||
{"referrer-policy", referrer_policy},
|
{"referrer-policy", referrer_policy},
|
||||||
{"x-download-options", "noopen"},
|
{"x-download-options", "noopen"},
|
||||||
{"content-security-policy", csp_string() <> ";"}
|
{"content-security-policy", csp_string()}
|
||||||
]
|
]
|
||||||
|
|
||||||
if report_uri do
|
if report_uri do
|
||||||
|
@ -43,23 +43,46 @@ defp headers do
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
headers ++ [{"reply-to", Jason.encode!(report_group)}]
|
[{"reply-to", Jason.encode!(report_group)} | headers]
|
||||||
else
|
else
|
||||||
headers
|
headers
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
static_csp_rules = [
|
||||||
|
"default-src 'none'",
|
||||||
|
"base-uri 'self'",
|
||||||
|
"frame-ancestors 'none'",
|
||||||
|
"style-src 'self' 'unsafe-inline'",
|
||||||
|
"font-src 'self'",
|
||||||
|
"manifest-src 'self'"
|
||||||
|
]
|
||||||
|
|
||||||
|
@csp_start [Enum.join(static_csp_rules, ";") <> ";"]
|
||||||
|
|
||||||
defp csp_string do
|
defp csp_string do
|
||||||
scheme = Config.get([Pleroma.Web.Endpoint, :url])[:scheme]
|
scheme = Config.get([Pleroma.Web.Endpoint, :url])[:scheme]
|
||||||
static_url = Pleroma.Web.Endpoint.static_url()
|
static_url = Pleroma.Web.Endpoint.static_url()
|
||||||
websocket_url = Pleroma.Web.Endpoint.websocket_url()
|
websocket_url = Pleroma.Web.Endpoint.websocket_url()
|
||||||
report_uri = Config.get([:http_security, :report_uri])
|
report_uri = Config.get([:http_security, :report_uri])
|
||||||
|
|
||||||
connect_src = "connect-src 'self' #{static_url} #{websocket_url}"
|
img_src = "img-src 'self' data: blob:"
|
||||||
|
media_src = "media-src 'self'"
|
||||||
|
|
||||||
|
{img_src, media_src} =
|
||||||
|
if Config.get([:media_proxy, :enabled]) &&
|
||||||
|
!Config.get([:media_proxy, :proxy_opts, :redirect_on_failure]) do
|
||||||
|
sources = get_proxy_and_attachment_sources()
|
||||||
|
{[img_src, sources], [media_src, sources]}
|
||||||
|
else
|
||||||
|
{[img_src, " https:"], [media_src, " https:"]}
|
||||||
|
end
|
||||||
|
|
||||||
|
connect_src = ["connect-src 'self' blob: ", static_url, ?\s, websocket_url]
|
||||||
|
|
||||||
connect_src =
|
connect_src =
|
||||||
if Pleroma.Config.get(:env) == :dev do
|
if Pleroma.Config.get(:env) == :dev do
|
||||||
connect_src <> " http://localhost:3035/"
|
[connect_src, " http://localhost:3035/"]
|
||||||
else
|
else
|
||||||
connect_src
|
connect_src
|
||||||
end
|
end
|
||||||
|
@ -71,27 +94,46 @@ defp csp_string do
|
||||||
"script-src 'self'"
|
"script-src 'self'"
|
||||||
end
|
end
|
||||||
|
|
||||||
main_part = [
|
report = if report_uri, do: ["report-uri ", report_uri, ";report-to csp-endpoint"]
|
||||||
"default-src 'none'",
|
insecure = if scheme == "https", do: "upgrade-insecure-requests"
|
||||||
"base-uri 'self'",
|
|
||||||
"frame-ancestors 'none'",
|
|
||||||
"img-src 'self' data: blob: https:",
|
|
||||||
"media-src 'self' https:",
|
|
||||||
"style-src 'self' 'unsafe-inline'",
|
|
||||||
"font-src 'self'",
|
|
||||||
"manifest-src 'self'",
|
|
||||||
connect_src,
|
|
||||||
script_src
|
|
||||||
]
|
|
||||||
|
|
||||||
report = if report_uri, do: ["report-uri #{report_uri}; report-to csp-endpoint"], else: []
|
@csp_start
|
||||||
|
|> add_csp_param(img_src)
|
||||||
insecure = if scheme == "https", do: ["upgrade-insecure-requests"], else: []
|
|> add_csp_param(media_src)
|
||||||
|
|> add_csp_param(connect_src)
|
||||||
(main_part ++ report ++ insecure)
|
|> add_csp_param(script_src)
|
||||||
|> Enum.join("; ")
|
|> add_csp_param(insecure)
|
||||||
|
|> add_csp_param(report)
|
||||||
|
|> :erlang.iolist_to_binary()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp get_proxy_and_attachment_sources do
|
||||||
|
media_proxy_whitelist =
|
||||||
|
Enum.reduce(Config.get([:media_proxy, :whitelist]), [], fn host, acc ->
|
||||||
|
add_source(acc, host)
|
||||||
|
end)
|
||||||
|
|
||||||
|
upload_base_url =
|
||||||
|
if Config.get([Pleroma.Upload, :base_url]),
|
||||||
|
do: URI.parse(Config.get([Pleroma.Upload, :base_url])).host
|
||||||
|
|
||||||
|
s3_endpoint =
|
||||||
|
if Config.get([Pleroma.Upload, :uploader]) == Pleroma.Uploaders.S3,
|
||||||
|
do: URI.parse(Config.get([Pleroma.Uploaders.S3, :public_endpoint])).host
|
||||||
|
|
||||||
|
[]
|
||||||
|
|> add_source(upload_base_url)
|
||||||
|
|> add_source(s3_endpoint)
|
||||||
|
|> add_source(media_proxy_whitelist)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp add_source(iodata, nil), do: iodata
|
||||||
|
defp add_source(iodata, source), do: [[?\s, source] | iodata]
|
||||||
|
|
||||||
|
defp add_csp_param(csp_iodata, nil), do: csp_iodata
|
||||||
|
|
||||||
|
defp add_csp_param(csp_iodata, param), do: [[param, ?;] | csp_iodata]
|
||||||
|
|
||||||
def warn_if_disabled do
|
def warn_if_disabled do
|
||||||
unless Config.get([:http_security, :enabled]) do
|
unless Config.get([:http_security, :enabled]) do
|
||||||
Logger.warn("
|
Logger.warn("
|
||||||
|
|
|
@ -40,7 +40,7 @@ defp with_media_attachments(
|
||||||
%{changes: %{params: %{"media_ids" => media_ids} = params}} = changeset
|
%{changes: %{params: %{"media_ids" => media_ids} = params}} = changeset
|
||||||
)
|
)
|
||||||
when is_list(media_ids) do
|
when is_list(media_ids) do
|
||||||
media_attachments = Utils.attachments_from_ids(%{"media_ids" => media_ids})
|
media_attachments = Utils.attachments_from_ids(%{media_ids: media_ids})
|
||||||
|
|
||||||
params =
|
params =
|
||||||
params
|
params
|
||||||
|
|
|
@ -134,7 +134,7 @@ defp prepare_upload(%Plug.Upload{} = file, opts) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp prepare_upload(%{"img" => "data:image/" <> image_data}, opts) do
|
defp prepare_upload(%{img: "data:image/" <> image_data}, opts) do
|
||||||
parsed = Regex.named_captures(~r/(?<filetype>jpeg|png|gif);base64,(?<data>.*)/, image_data)
|
parsed = Regex.named_captures(~r/(?<filetype>jpeg|png|gif);base64,(?<data>.*)/, image_data)
|
||||||
data = Base.decode64!(parsed["data"], ignore: :whitespace)
|
data = Base.decode64!(parsed["data"], ignore: :whitespace)
|
||||||
hash = String.downcase(Base.encode16(:crypto.hash(:sha256, data)))
|
hash = String.downcase(Base.encode16(:crypto.hash(:sha256, data)))
|
||||||
|
|
|
@ -9,7 +9,6 @@ defmodule Pleroma.User do
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
import Ecto, only: [assoc: 2]
|
import Ecto, only: [assoc: 2]
|
||||||
|
|
||||||
alias Comeonin.Pbkdf2
|
|
||||||
alias Ecto.Multi
|
alias Ecto.Multi
|
||||||
alias Pleroma.Activity
|
alias Pleroma.Activity
|
||||||
alias Pleroma.Config
|
alias Pleroma.Config
|
||||||
|
@ -306,8 +305,13 @@ def invisible?(_), do: false
|
||||||
|
|
||||||
def avatar_url(user, options \\ []) do
|
def avatar_url(user, options \\ []) do
|
||||||
case user.avatar do
|
case user.avatar do
|
||||||
%{"url" => [%{"href" => href} | _]} -> href
|
%{"url" => [%{"href" => href} | _]} ->
|
||||||
_ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
|
href
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
unless options[:no_default] do
|
||||||
|
Config.get([:assets, :default_user_avatar], "#{Web.base_url()}/images/avi.png")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -534,9 +538,10 @@ def update_as_admin_changeset(struct, params) do
|
||||||
|> delete_change(:also_known_as)
|
|> delete_change(:also_known_as)
|
||||||
|> unique_constraint(:email)
|
|> unique_constraint(:email)
|
||||||
|> validate_format(:email, @email_regex)
|
|> validate_format(:email, @email_regex)
|
||||||
|
|> validate_inclusion(:actor_type, ["Person", "Service"])
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec update_as_admin(%User{}, map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
|
@spec update_as_admin(User.t(), map()) :: {:ok, User.t()} | {:error, Changeset.t()}
|
||||||
def update_as_admin(user, params) do
|
def update_as_admin(user, params) do
|
||||||
params = Map.put(params, "password_confirmation", params["password"])
|
params = Map.put(params, "password_confirmation", params["password"])
|
||||||
changeset = update_as_admin_changeset(user, params)
|
changeset = update_as_admin_changeset(user, params)
|
||||||
|
@ -557,7 +562,7 @@ def password_update_changeset(struct, params) do
|
||||||
|> put_change(:password_reset_pending, false)
|
|> put_change(:password_reset_pending, false)
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
|
@spec reset_password(User.t(), map()) :: {:ok, User.t()} | {:error, Changeset.t()}
|
||||||
def reset_password(%User{} = user, params) do
|
def reset_password(%User{} = user, params) do
|
||||||
reset_password(user, user, params)
|
reset_password(user, user, params)
|
||||||
end
|
end
|
||||||
|
@ -750,7 +755,19 @@ def unfollow(%User{ap_id: ap_id}, %User{ap_id: ap_id}) do
|
||||||
{:error, "Not subscribed!"}
|
{:error, "Not subscribed!"}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec unfollow(User.t(), User.t()) :: {:ok, User.t(), Activity.t()} | {:error, String.t()}
|
||||||
def unfollow(%User{} = follower, %User{} = followed) do
|
def unfollow(%User{} = follower, %User{} = followed) do
|
||||||
|
case do_unfollow(follower, followed) do
|
||||||
|
{:ok, follower, followed} ->
|
||||||
|
{:ok, follower, Utils.fetch_latest_follow(follower, followed)}
|
||||||
|
|
||||||
|
error ->
|
||||||
|
error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec do_unfollow(User.t(), User.t()) :: {:ok, User.t(), User.t()} | {:error, String.t()}
|
||||||
|
defp do_unfollow(%User{} = follower, %User{} = followed) do
|
||||||
case get_follow_state(follower, followed) do
|
case get_follow_state(follower, followed) do
|
||||||
state when state in [:follow_pending, :follow_accept] ->
|
state when state in [:follow_pending, :follow_accept] ->
|
||||||
FollowingRelationship.unfollow(follower, followed)
|
FollowingRelationship.unfollow(follower, followed)
|
||||||
|
@ -761,7 +778,7 @@ def unfollow(%User{} = follower, %User{} = followed) do
|
||||||
|> update_following_count()
|
|> update_following_count()
|
||||||
|> set_cache()
|
|> set_cache()
|
||||||
|
|
||||||
{:ok, follower, Utils.fetch_latest_follow(follower, followed)}
|
{:ok, follower, followed}
|
||||||
|
|
||||||
nil ->
|
nil ->
|
||||||
{:error, "Not subscribed!"}
|
{:error, "Not subscribed!"}
|
||||||
|
@ -1192,8 +1209,9 @@ def increment_unread_conversation_count(conversation, %User{local: true} = user)
|
||||||
|
|
||||||
def increment_unread_conversation_count(_, user), do: {:ok, user}
|
def increment_unread_conversation_count(_, user), do: {:ok, user}
|
||||||
|
|
||||||
@spec get_users_from_set([String.t()], boolean()) :: [User.t()]
|
@spec get_users_from_set([String.t()], keyword()) :: [User.t()]
|
||||||
def get_users_from_set(ap_ids, local_only \\ true) do
|
def get_users_from_set(ap_ids, opts \\ []) do
|
||||||
|
local_only = Keyword.get(opts, :local_only, true)
|
||||||
criteria = %{ap_id: ap_ids, deactivated: false}
|
criteria = %{ap_id: ap_ids, deactivated: false}
|
||||||
criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
|
criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
|
||||||
|
|
||||||
|
@ -1205,7 +1223,9 @@ def get_users_from_set(ap_ids, local_only \\ true) do
|
||||||
def get_recipients_from_activity(%Activity{recipients: to, actor: actor}) do
|
def get_recipients_from_activity(%Activity{recipients: to, actor: actor}) do
|
||||||
to = [actor | to]
|
to = [actor | to]
|
||||||
|
|
||||||
User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
|
query = User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
|
||||||
|
|
||||||
|
query
|
||||||
|> Repo.all()
|
|> Repo.all()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1401,15 +1421,13 @@ def deactivate(%User{} = user, status) do
|
||||||
user
|
user
|
||||||
|> get_followers()
|
|> get_followers()
|
||||||
|> Enum.filter(& &1.local)
|
|> Enum.filter(& &1.local)
|
||||||
|> Enum.each(fn follower ->
|
|> Enum.each(&set_cache(update_following_count(&1)))
|
||||||
follower |> update_following_count() |> set_cache()
|
|
||||||
end)
|
|
||||||
|
|
||||||
# Only update local user counts, remote will be update during the next pull.
|
# Only update local user counts, remote will be update during the next pull.
|
||||||
user
|
user
|
||||||
|> get_friends()
|
|> get_friends()
|
||||||
|> Enum.filter(& &1.local)
|
|> Enum.filter(& &1.local)
|
||||||
|> Enum.each(&update_follower_count/1)
|
|> Enum.each(&do_unfollow(user, &1))
|
||||||
|
|
||||||
{:ok, user}
|
{:ok, user}
|
||||||
end
|
end
|
||||||
|
@ -1431,6 +1449,25 @@ def delete(%User{} = user) do
|
||||||
BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
|
BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp delete_and_invalidate_cache(%User{} = user) do
|
||||||
|
invalidate_cache(user)
|
||||||
|
Repo.delete(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp delete_or_deactivate(%User{local: false} = user), do: delete_and_invalidate_cache(user)
|
||||||
|
|
||||||
|
defp delete_or_deactivate(%User{local: true} = user) do
|
||||||
|
status = account_status(user)
|
||||||
|
|
||||||
|
if status == :confirmation_pending do
|
||||||
|
delete_and_invalidate_cache(user)
|
||||||
|
else
|
||||||
|
user
|
||||||
|
|> change(%{deactivated: true, email: nil})
|
||||||
|
|> update_and_set_cache()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def perform(:force_password_reset, user), do: force_password_reset(user)
|
def perform(:force_password_reset, user), do: force_password_reset(user)
|
||||||
|
|
||||||
@spec perform(atom(), User.t()) :: {:ok, User.t()}
|
@spec perform(atom(), User.t()) :: {:ok, User.t()}
|
||||||
|
@ -1452,14 +1489,7 @@ def perform(:delete, %User{} = user) do
|
||||||
|
|
||||||
delete_user_activities(user)
|
delete_user_activities(user)
|
||||||
|
|
||||||
if user.local do
|
delete_or_deactivate(user)
|
||||||
user
|
|
||||||
|> change(%{deactivated: true, email: nil})
|
|
||||||
|> update_and_set_cache()
|
|
||||||
else
|
|
||||||
invalidate_cache(user)
|
|
||||||
Repo.delete(user)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def perform(:deactivate_async, user, status), do: deactivate(user, status)
|
def perform(:deactivate_async, user, status), do: deactivate(user, status)
|
||||||
|
@ -1554,10 +1584,23 @@ def delete_user_activities(%User{ap_id: ap_id} = user) do
|
||||||
|> Stream.run()
|
|> Stream.run()
|
||||||
end
|
end
|
||||||
|
|
||||||
defp delete_activity(%{data: %{"type" => "Create", "object" => object}}, user) do
|
defp delete_activity(%{data: %{"type" => "Create", "object" => object}} = activity, user) do
|
||||||
{:ok, delete_data, _} = Builder.delete(user, object)
|
with {_, %Object{}} <- {:find_object, Object.get_by_ap_id(object)},
|
||||||
|
{:ok, delete_data, _} <- Builder.delete(user, object) do
|
||||||
|
Pipeline.common_pipeline(delete_data, local: user.local)
|
||||||
|
else
|
||||||
|
{:find_object, nil} ->
|
||||||
|
# We have the create activity, but not the object, it was probably pruned.
|
||||||
|
# Insert a tombstone and try again
|
||||||
|
with {:ok, tombstone_data, _} <- Builder.tombstone(user.ap_id, object),
|
||||||
|
{:ok, _tombstone} <- Object.create(tombstone_data) do
|
||||||
|
delete_activity(activity, user)
|
||||||
|
end
|
||||||
|
|
||||||
Pipeline.common_pipeline(delete_data, local: user.local)
|
e ->
|
||||||
|
Logger.error("Could not delete #{object} created by #{activity.data["ap_id"]}")
|
||||||
|
Logger.error("Error: #{inspect(e)}")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp delete_activity(%{data: %{"type" => type}} = activity, user)
|
defp delete_activity(%{data: %{"type" => type}} = activity, user)
|
||||||
|
@ -1577,12 +1620,19 @@ def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
|
||||||
def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
|
def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
|
||||||
|
|
||||||
def get_or_fetch_by_ap_id(ap_id) do
|
def get_or_fetch_by_ap_id(ap_id) do
|
||||||
user = get_cached_by_ap_id(ap_id)
|
cached_user = get_cached_by_ap_id(ap_id)
|
||||||
|
|
||||||
if !is_nil(user) and !needs_update?(user) do
|
maybe_fetched_user = needs_update?(cached_user) && fetch_by_ap_id(ap_id)
|
||||||
{:ok, user}
|
|
||||||
else
|
case {cached_user, maybe_fetched_user} do
|
||||||
fetch_by_ap_id(ap_id)
|
{_, {:ok, %User{} = user}} ->
|
||||||
|
{:ok, user}
|
||||||
|
|
||||||
|
{%User{} = user, _} ->
|
||||||
|
{:ok, user}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{:error, :not_found}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1913,7 +1963,7 @@ def get_ap_ids_by_nicknames(nicknames) do
|
||||||
defp put_password_hash(
|
defp put_password_hash(
|
||||||
%Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
|
%Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
|
||||||
) do
|
) do
|
||||||
change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
|
change(changeset, password_hash: Pbkdf2.hash_pwd_salt(password))
|
||||||
end
|
end
|
||||||
|
|
||||||
defp put_password_hash(changeset), do: changeset
|
defp put_password_hash(changeset), do: changeset
|
||||||
|
|
|
@ -45,7 +45,7 @@ defmodule Pleroma.User.Query do
|
||||||
is_admin: boolean(),
|
is_admin: boolean(),
|
||||||
is_moderator: boolean(),
|
is_moderator: boolean(),
|
||||||
super_users: boolean(),
|
super_users: boolean(),
|
||||||
exclude_service_users: boolean(),
|
invisible: boolean(),
|
||||||
followers: User.t(),
|
followers: User.t(),
|
||||||
friends: User.t(),
|
friends: User.t(),
|
||||||
recipients_from_activity: [String.t()],
|
recipients_from_activity: [String.t()],
|
||||||
|
@ -89,8 +89,8 @@ defp compose_query({key, value}, query)
|
||||||
where(query, [u], ilike(field(u, ^key), ^"%#{value}%"))
|
where(query, [u], ilike(field(u, ^key), ^"%#{value}%"))
|
||||||
end
|
end
|
||||||
|
|
||||||
defp compose_query({:exclude_service_users, _}, query) do
|
defp compose_query({:invisible, bool}, query) when is_boolean(bool) do
|
||||||
where(query, [u], not like(u.ap_id, "%/relay") and not like(u.ap_id, "%/internal/fetch"))
|
where(query, [u], u.invisible == ^bool)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp compose_query({key, value}, query)
|
defp compose_query({key, value}, query)
|
||||||
|
@ -167,20 +167,18 @@ defp compose_query({:friends, %User{id: id}}, query) do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp compose_query({:recipients_from_activity, to}, query) do
|
defp compose_query({:recipients_from_activity, to}, query) do
|
||||||
query
|
following_query =
|
||||||
|> join(:left, [u], r in FollowingRelationship,
|
from(u in User,
|
||||||
as: :relationships,
|
join: f in FollowingRelationship,
|
||||||
on: r.follower_id == u.id
|
on: u.id == f.following_id,
|
||||||
|
where: f.state == ^:follow_accept,
|
||||||
|
where: u.follower_address in ^to,
|
||||||
|
select: f.follower_id
|
||||||
|
)
|
||||||
|
|
||||||
|
from(u in query,
|
||||||
|
where: u.ap_id in ^to or u.id in subquery(following_query)
|
||||||
)
|
)
|
||||||
|> join(:left, [relationships: r], f in User,
|
|
||||||
as: :following,
|
|
||||||
on: f.id == r.following_id
|
|
||||||
)
|
|
||||||
|> where(
|
|
||||||
[u, following: f, relationships: r],
|
|
||||||
u.ap_id in ^to or (f.follower_address in ^to and r.state == ^:follow_accept)
|
|
||||||
)
|
|
||||||
|> distinct(true)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp compose_query({:order_by, key}, query) do
|
defp compose_query({:order_by, key}, query) do
|
||||||
|
|
|
@ -10,8 +10,8 @@ def post_welcome_message_to_user(user) do
|
||||||
with %User{} = sender_user <- welcome_user(),
|
with %User{} = sender_user <- welcome_user(),
|
||||||
message when is_binary(message) <- welcome_message() do
|
message when is_binary(message) <- welcome_message() do
|
||||||
CommonAPI.post(sender_user, %{
|
CommonAPI.post(sender_user, %{
|
||||||
"visibility" => "direct",
|
visibility: "direct",
|
||||||
"status" => "@#{user.nickname}\n#{message}"
|
status: "@#{user.nickname}\n#{message}"
|
||||||
})
|
})
|
||||||
else
|
else
|
||||||
_ -> {:ok, nil}
|
_ -> {:ok, nil}
|
||||||
|
|
|
@ -87,6 +87,22 @@ def dictionary(
|
||||||
source_to_target_rel_types \\ nil,
|
source_to_target_rel_types \\ nil,
|
||||||
target_to_source_rel_types \\ nil
|
target_to_source_rel_types \\ nil
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def dictionary(
|
||||||
|
_source_users,
|
||||||
|
_target_users,
|
||||||
|
[] = _source_to_target_rel_types,
|
||||||
|
[] = _target_to_source_rel_types
|
||||||
|
) do
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
|
def dictionary(
|
||||||
|
source_users,
|
||||||
|
target_users,
|
||||||
|
source_to_target_rel_types,
|
||||||
|
target_to_source_rel_types
|
||||||
|
)
|
||||||
when is_list(source_users) and is_list(target_users) do
|
when is_list(source_users) and is_list(target_users) do
|
||||||
source_user_ids = User.binary_id(source_users)
|
source_user_ids = User.binary_id(source_users)
|
||||||
target_user_ids = User.binary_id(target_users)
|
target_user_ids = User.binary_id(target_users)
|
||||||
|
@ -138,11 +154,16 @@ def view_relationships_option(nil = _reading_user, _actors, _opts) do
|
||||||
|
|
||||||
def view_relationships_option(%User{} = reading_user, actors, opts) do
|
def view_relationships_option(%User{} = reading_user, actors, opts) do
|
||||||
{source_to_target_rel_types, target_to_source_rel_types} =
|
{source_to_target_rel_types, target_to_source_rel_types} =
|
||||||
if opts[:source_mutes_only] do
|
case opts[:subset] do
|
||||||
# This option is used for rendering statuses (FE needs `muted` flag for each one anyways)
|
:source_mutes ->
|
||||||
{[:mute], []}
|
# Used for statuses rendering (FE needs `muted` flag for each status when statuses load)
|
||||||
else
|
{[:mute], []}
|
||||||
{[:block, :mute, :notification_mute, :reblog_mute], [:block, :inverse_subscription]}
|
|
||||||
|
nil ->
|
||||||
|
{[:block, :mute, :notification_mute, :reblog_mute], [:block, :inverse_subscription]}
|
||||||
|
|
||||||
|
unknown ->
|
||||||
|
raise "Unsupported :subset option value: #{inspect(unknown)}"
|
||||||
end
|
end
|
||||||
|
|
||||||
user_relationships =
|
user_relationships =
|
||||||
|
@ -153,7 +174,17 @@ def view_relationships_option(%User{} = reading_user, actors, opts) do
|
||||||
target_to_source_rel_types
|
target_to_source_rel_types
|
||||||
)
|
)
|
||||||
|
|
||||||
following_relationships = FollowingRelationship.all_between_user_sets([reading_user], actors)
|
following_relationships =
|
||||||
|
case opts[:subset] do
|
||||||
|
:source_mutes ->
|
||||||
|
[]
|
||||||
|
|
||||||
|
nil ->
|
||||||
|
FollowingRelationship.all_between_user_sets([reading_user], actors)
|
||||||
|
|
||||||
|
unknown ->
|
||||||
|
raise "Unsupported :subset option value: #{inspect(unknown)}"
|
||||||
|
end
|
||||||
|
|
||||||
%{user_relationships: user_relationships, following_relationships: following_relationships}
|
%{user_relationships: user_relationships, following_relationships: following_relationships}
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||||
alias Pleroma.Constants
|
alias Pleroma.Constants
|
||||||
alias Pleroma.Conversation
|
alias Pleroma.Conversation
|
||||||
alias Pleroma.Conversation.Participation
|
alias Pleroma.Conversation.Participation
|
||||||
|
alias Pleroma.Maps
|
||||||
alias Pleroma.Notification
|
alias Pleroma.Notification
|
||||||
alias Pleroma.Object
|
alias Pleroma.Object
|
||||||
alias Pleroma.Object.Containment
|
alias Pleroma.Object.Containment
|
||||||
|
@ -19,7 +20,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.Web.ActivityPub.MRF
|
alias Pleroma.Web.ActivityPub.MRF
|
||||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||||
alias Pleroma.Web.ActivityPub.Utils
|
|
||||||
alias Pleroma.Web.Streamer
|
alias Pleroma.Web.Streamer
|
||||||
alias Pleroma.Web.WebFinger
|
alias Pleroma.Web.WebFinger
|
||||||
alias Pleroma.Workers.BackgroundWorker
|
alias Pleroma.Workers.BackgroundWorker
|
||||||
|
@ -161,12 +161,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})
|
||||||
|
|
||||||
|
@ -328,7 +323,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
|
||||||
|
@ -348,7 +343,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
|
||||||
|
@ -356,36 +351,6 @@ def update(%{to: to, cc: cc, actor: actor, object: object} = params) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec announce(User.t(), Object.t(), String.t() | nil, boolean(), boolean()) ::
|
|
||||||
{:ok, Activity.t(), Object.t()} | {:error, any()}
|
|
||||||
def announce(
|
|
||||||
%User{ap_id: _} = user,
|
|
||||||
%Object{data: %{"id" => _}} = object,
|
|
||||||
activity_id \\ nil,
|
|
||||||
local \\ true,
|
|
||||||
public \\ true
|
|
||||||
) do
|
|
||||||
with {:ok, result} <-
|
|
||||||
Repo.transaction(fn -> do_announce(user, object, activity_id, local, public) end) do
|
|
||||||
result
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp do_announce(user, object, activity_id, local, public) do
|
|
||||||
with true <- is_announceable?(object, user, public),
|
|
||||||
object <- Object.get_by_id(object.id),
|
|
||||||
announce_data <- make_announce_data(user, object, activity_id, public),
|
|
||||||
{:ok, activity} <- insert(announce_data, local),
|
|
||||||
{:ok, object} <- add_announce_to_object(activity, object),
|
|
||||||
_ <- notify_and_stream(activity),
|
|
||||||
:ok <- maybe_federate(activity) do
|
|
||||||
{:ok, activity, object}
|
|
||||||
else
|
|
||||||
false -> {:error, false}
|
|
||||||
{:error, error} -> Repo.rollback(error)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec follow(User.t(), User.t(), String.t() | nil, boolean()) ::
|
@spec follow(User.t(), User.t(), String.t() | nil, boolean()) ::
|
||||||
{:ok, Activity.t()} | {:error, any()}
|
{:ok, Activity.t()} | {:error, any()}
|
||||||
def follow(follower, followed, activity_id \\ nil, local \\ true) do
|
def follow(follower, followed, activity_id \\ nil, local \\ true) do
|
||||||
|
@ -439,7 +404,6 @@ def block(blocker, blocked, activity_id \\ nil, local \\ true) do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp do_block(blocker, blocked, activity_id, local) do
|
defp do_block(blocker, blocked, activity_id, local) do
|
||||||
outgoing_blocks = Config.get([:activitypub, :outgoing_blocks])
|
|
||||||
unfollow_blocked = Config.get([:activitypub, :unfollow_blocked])
|
unfollow_blocked = Config.get([:activitypub, :unfollow_blocked])
|
||||||
|
|
||||||
if unfollow_blocked do
|
if unfollow_blocked do
|
||||||
|
@ -447,8 +411,7 @@ defp do_block(blocker, blocked, activity_id, local) do
|
||||||
if follow_activity, do: unfollow(blocker, blocked, nil, local)
|
if follow_activity, do: unfollow(blocker, blocked, nil, local)
|
||||||
end
|
end
|
||||||
|
|
||||||
with true <- outgoing_blocks,
|
with block_data <- make_block_data(blocker, blocked, activity_id),
|
||||||
block_data <- make_block_data(blocker, blocked, activity_id),
|
|
||||||
{:ok, activity} <- insert(block_data, local),
|
{:ok, activity} <- insert(block_data, local),
|
||||||
_ <- notify_and_stream(activity),
|
_ <- notify_and_stream(activity),
|
||||||
:ok <- maybe_federate(activity) do
|
:ok <- maybe_federate(activity) do
|
||||||
|
@ -570,14 +533,27 @@ def fetch_latest_activity_id_for_context(context, opts \\ %{}) do
|
||||||
|> Repo.one()
|
|> Repo.one()
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec fetch_public_activities(map(), Pagination.type()) :: [Activity.t()]
|
@spec fetch_public_or_unlisted_activities(map(), Pagination.type()) :: [Activity.t()]
|
||||||
def fetch_public_activities(opts \\ %{}, pagination \\ :keyset) do
|
def fetch_public_or_unlisted_activities(opts \\ %{}, pagination \\ :keyset) do
|
||||||
opts = Map.drop(opts, ["user"])
|
opts = Map.drop(opts, ["user"])
|
||||||
|
|
||||||
[Constants.as_public()]
|
query = fetch_activities_query([Constants.as_public()], opts)
|
||||||
|> fetch_activities_query(opts)
|
|
||||||
|> restrict_unlisted()
|
query =
|
||||||
|> Pagination.fetch_paginated(opts, pagination)
|
if opts["restrict_unlisted"] do
|
||||||
|
restrict_unlisted(query)
|
||||||
|
else
|
||||||
|
query
|
||||||
|
end
|
||||||
|
|
||||||
|
Pagination.fetch_paginated(query, opts, pagination)
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec fetch_public_activities(map(), Pagination.type()) :: [Activity.t()]
|
||||||
|
def fetch_public_activities(opts \\ %{}, pagination \\ :keyset) do
|
||||||
|
opts
|
||||||
|
|> Map.put("restrict_unlisted", true)
|
||||||
|
|> fetch_public_or_unlisted_activities(pagination)
|
||||||
end
|
end
|
||||||
|
|
||||||
@valid_visibilities ~w[direct unlisted public private]
|
@valid_visibilities ~w[direct unlisted public private]
|
||||||
|
@ -955,6 +931,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' \\?| ?)",
|
||||||
|
@ -1049,6 +1031,17 @@ defp exclude_poll_votes(query, _) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp exclude_invisible_actors(query, %{"invisible_actors" => true}), do: query
|
||||||
|
|
||||||
|
defp exclude_invisible_actors(query, _opts) do
|
||||||
|
invisible_ap_ids =
|
||||||
|
User.Query.build(%{invisible: true, select: [:ap_id]})
|
||||||
|
|> Repo.all()
|
||||||
|
|> Enum.map(fn %{ap_id: ap_id} -> ap_id end)
|
||||||
|
|
||||||
|
from([activity] in query, where: activity.actor not in ^invisible_ap_ids)
|
||||||
|
end
|
||||||
|
|
||||||
defp exclude_id(query, %{"exclude_id" => id}) when is_binary(id) do
|
defp exclude_id(query, %{"exclude_id" => id}) when is_binary(id) do
|
||||||
from(activity in query, where: activity.id != ^id)
|
from(activity in query, where: activity.id != ^id)
|
||||||
end
|
end
|
||||||
|
@ -1154,6 +1147,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|
||||||
|> restrict_instance(opts)
|
|> restrict_instance(opts)
|
||||||
|> Activity.restrict_deactivated_users()
|
|> Activity.restrict_deactivated_users()
|
||||||
|> exclude_poll_votes(opts)
|
|> exclude_poll_votes(opts)
|
||||||
|
|> exclude_invisible_actors(opts)
|
||||||
|> exclude_visibility(opts)
|
|> exclude_visibility(opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1177,7 +1171,7 @@ def fetch_favourites(user, params \\ %{}, pagination \\ :keyset) do
|
||||||
|> Activity.with_joined_object()
|
|> Activity.with_joined_object()
|
||||||
|> Object.with_joined_activity()
|
|> Object.with_joined_activity()
|
||||||
|> select([_like, object, activity], %{activity | object: object})
|
|> select([_like, object, activity], %{activity | object: object})
|
||||||
|> order_by([like, _, _], desc: like.id)
|
|> order_by([like, _, _], desc_nulls_last: like.id)
|
||||||
|> Pagination.fetch_paginated(
|
|> Pagination.fetch_paginated(
|
||||||
Map.merge(params, %{"skip_order" => true}),
|
Map.merge(params, %{"skip_order" => true}),
|
||||||
pagination,
|
pagination,
|
||||||
|
@ -1226,12 +1220,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
|
||||||
|
|
|
@ -21,6 +21,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
||||||
alias Pleroma.Web.ActivityPub.UserView
|
alias Pleroma.Web.ActivityPub.UserView
|
||||||
alias Pleroma.Web.ActivityPub.Utils
|
alias Pleroma.Web.ActivityPub.Utils
|
||||||
alias Pleroma.Web.ActivityPub.Visibility
|
alias Pleroma.Web.ActivityPub.Visibility
|
||||||
|
alias Pleroma.Web.ControllerHelper
|
||||||
|
alias Pleroma.Web.Endpoint
|
||||||
alias Pleroma.Web.FederatingPlug
|
alias Pleroma.Web.FederatingPlug
|
||||||
alias Pleroma.Web.Federator
|
alias Pleroma.Web.Federator
|
||||||
|
|
||||||
|
@ -75,8 +77,8 @@ def user(conn, %{"nickname" => nickname}) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def object(conn, %{"uuid" => uuid}) do
|
def object(conn, _) do
|
||||||
with ap_id <- o_status_url(conn, :object, uuid),
|
with ap_id <- Endpoint.url() <> conn.request_path,
|
||||||
%Object{} = object <- Object.get_cached_by_ap_id(ap_id),
|
%Object{} = object <- Object.get_cached_by_ap_id(ap_id),
|
||||||
{_, true} <- {:public?, Visibility.is_public?(object)} do
|
{_, true} <- {:public?, Visibility.is_public?(object)} do
|
||||||
conn
|
conn
|
||||||
|
@ -101,8 +103,8 @@ def track_object_fetch(conn, object_id) do
|
||||||
conn
|
conn
|
||||||
end
|
end
|
||||||
|
|
||||||
def activity(conn, %{"uuid" => uuid}) do
|
def activity(conn, _params) do
|
||||||
with ap_id <- o_status_url(conn, :activity, uuid),
|
with ap_id <- Endpoint.url() <> conn.request_path,
|
||||||
%Activity{} = activity <- Activity.normalize(ap_id),
|
%Activity{} = activity <- Activity.normalize(ap_id),
|
||||||
{_, true} <- {:public?, Visibility.is_public?(activity)} do
|
{_, true} <- {:public?, Visibility.is_public?(activity)} do
|
||||||
conn
|
conn
|
||||||
|
@ -229,27 +231,22 @@ def outbox(
|
||||||
when page? in [true, "true"] do
|
when page? in [true, "true"] do
|
||||||
with %User{} = user <- User.get_cached_by_nickname(nickname),
|
with %User{} = user <- User.get_cached_by_nickname(nickname),
|
||||||
{:ok, user} <- User.ensure_keys_present(user) do
|
{:ok, user} <- User.ensure_keys_present(user) do
|
||||||
activities =
|
# "include_poll_votes" is a hack because postgres generates inefficient
|
||||||
if params["max_id"] do
|
# queries when filtering by 'Answer', poll votes will be hidden by the
|
||||||
ActivityPub.fetch_user_activities(user, for_user, %{
|
# visibility filter in this case anyway
|
||||||
"max_id" => params["max_id"],
|
params =
|
||||||
# This is a hack because postgres generates inefficient queries when filtering by
|
params
|
||||||
# 'Answer', poll votes will be hidden by the visibility filter in this case anyway
|
|> Map.drop(["nickname", "page"])
|
||||||
"include_poll_votes" => true,
|
|> Map.put("include_poll_votes", true)
|
||||||
"limit" => 10
|
|
||||||
})
|
activities = ActivityPub.fetch_user_activities(user, for_user, params)
|
||||||
else
|
|
||||||
ActivityPub.fetch_user_activities(user, for_user, %{
|
|
||||||
"limit" => 10,
|
|
||||||
"include_poll_votes" => true
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> put_resp_content_type("application/activity+json")
|
|> put_resp_content_type("application/activity+json")
|
||||||
|> put_view(UserView)
|
|> put_view(UserView)
|
||||||
|> render("activity_collection_page.json", %{
|
|> render("activity_collection_page.json", %{
|
||||||
activities: activities,
|
activities: activities,
|
||||||
|
pagination: ControllerHelper.get_pagination_fields(conn, activities),
|
||||||
iri: "#{user.ap_id}/outbox"
|
iri: "#{user.ap_id}/outbox"
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
@ -352,21 +349,23 @@ def read_inbox(
|
||||||
%{"nickname" => nickname, "page" => page?} = params
|
%{"nickname" => nickname, "page" => page?} = params
|
||||||
)
|
)
|
||||||
when page? in [true, "true"] do
|
when page? in [true, "true"] do
|
||||||
|
params =
|
||||||
|
params
|
||||||
|
|> Map.drop(["nickname", "page"])
|
||||||
|
|> Map.put("blocking_user", user)
|
||||||
|
|> Map.put("user", user)
|
||||||
|
|
||||||
activities =
|
activities =
|
||||||
if params["max_id"] do
|
[user.ap_id | User.following(user)]
|
||||||
ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{
|
|> ActivityPub.fetch_activities(params)
|
||||||
"max_id" => params["max_id"],
|
|> Enum.reverse()
|
||||||
"limit" => 10
|
|
||||||
})
|
|
||||||
else
|
|
||||||
ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{"limit" => 10})
|
|
||||||
end
|
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> put_resp_content_type("application/activity+json")
|
|> put_resp_content_type("application/activity+json")
|
||||||
|> put_view(UserView)
|
|> put_view(UserView)
|
||||||
|> render("activity_collection_page.json", %{
|
|> render("activity_collection_page.json", %{
|
||||||
activities: activities,
|
activities: activities,
|
||||||
|
pagination: ControllerHelper.get_pagination_fields(conn, activities),
|
||||||
iri: "#{user.ap_id}/inbox"
|
iri: "#{user.ap_id}/inbox"
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,9 +7,12 @@ defmodule Pleroma.Web.ActivityPub.Builder do
|
||||||
|
|
||||||
alias Pleroma.Object
|
alias Pleroma.Object
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
|
alias Pleroma.Web.ActivityPub.Relay
|
||||||
alias Pleroma.Web.ActivityPub.Utils
|
alias Pleroma.Web.ActivityPub.Utils
|
||||||
alias Pleroma.Web.ActivityPub.Visibility
|
alias Pleroma.Web.ActivityPub.Visibility
|
||||||
|
|
||||||
|
require Pleroma.Constants
|
||||||
|
|
||||||
@spec emoji_react(User.t(), Object.t(), String.t()) :: {:ok, map(), keyword()}
|
@spec emoji_react(User.t(), Object.t(), String.t()) :: {:ok, map(), keyword()}
|
||||||
def emoji_react(actor, object, emoji) do
|
def emoji_react(actor, object, emoji) do
|
||||||
with {:ok, data, meta} <- object_action(actor, object) do
|
with {:ok, data, meta} <- object_action(actor, object) do
|
||||||
|
@ -62,6 +65,16 @@ def delete(actor, object_id) do
|
||||||
}, []}
|
}, []}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec tombstone(String.t(), String.t()) :: {:ok, map(), keyword()}
|
||||||
|
def tombstone(actor, id) do
|
||||||
|
{:ok,
|
||||||
|
%{
|
||||||
|
"id" => id,
|
||||||
|
"actor" => actor,
|
||||||
|
"type" => "Tombstone"
|
||||||
|
}, []}
|
||||||
|
end
|
||||||
|
|
||||||
@spec like(User.t(), Object.t()) :: {:ok, map(), keyword()}
|
@spec like(User.t(), Object.t()) :: {:ok, map(), keyword()}
|
||||||
def like(actor, object) do
|
def like(actor, object) do
|
||||||
with {:ok, data, meta} <- object_action(actor, object) do
|
with {:ok, data, meta} <- object_action(actor, object) do
|
||||||
|
@ -73,6 +86,34 @@ def like(actor, object) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec announce(User.t(), Object.t(), keyword()) :: {:ok, map(), keyword()}
|
||||||
|
def announce(actor, object, options \\ []) do
|
||||||
|
public? = Keyword.get(options, :public, false)
|
||||||
|
|
||||||
|
to =
|
||||||
|
cond do
|
||||||
|
actor.ap_id == Relay.relay_ap_id() ->
|
||||||
|
[actor.follower_address]
|
||||||
|
|
||||||
|
public? ->
|
||||||
|
[actor.follower_address, object.data["actor"], Pleroma.Constants.as_public()]
|
||||||
|
|
||||||
|
true ->
|
||||||
|
[actor.follower_address, object.data["actor"]]
|
||||||
|
end
|
||||||
|
|
||||||
|
{:ok,
|
||||||
|
%{
|
||||||
|
"id" => Utils.generate_activity_id(),
|
||||||
|
"actor" => actor.ap_id,
|
||||||
|
"object" => object.data["id"],
|
||||||
|
"to" => to,
|
||||||
|
"context" => object.data["context"],
|
||||||
|
"type" => "Announce",
|
||||||
|
"published" => Utils.make_date()
|
||||||
|
}, []}
|
||||||
|
end
|
||||||
|
|
||||||
@spec object_action(User.t(), Object.t()) :: {:ok, map(), keyword()}
|
@spec object_action(User.t(), Object.t()) :: {:ok, map(), keyword()}
|
||||||
defp object_action(actor, object) do
|
defp object_action(actor, object) do
|
||||||
object_actor = User.get_cached_by_ap_id(object.data["actor"])
|
object_actor = User.get_cached_by_ap_id(object.data["actor"])
|
||||||
|
|
97
lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex
Normal file
97
lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
alias Pleroma.Config
|
||||||
|
|
||||||
|
@moduledoc "Detect new emojis by their shortcode and steals them"
|
||||||
|
@behaviour Pleroma.Web.ActivityPub.MRF
|
||||||
|
|
||||||
|
defp remote_host?(host), do: host != Config.get([Pleroma.Web.Endpoint, :url, :host])
|
||||||
|
|
||||||
|
defp accept_host?(host), do: host in Config.get([:mrf_steal_emoji, :hosts], [])
|
||||||
|
|
||||||
|
defp steal_emoji({shortcode, url}) do
|
||||||
|
url = Pleroma.Web.MediaProxy.url(url)
|
||||||
|
{:ok, response} = Pleroma.HTTP.get(url)
|
||||||
|
size_limit = Config.get([:mrf_steal_emoji, :size_limit], 50_000)
|
||||||
|
|
||||||
|
if byte_size(response.body) <= size_limit do
|
||||||
|
emoji_dir_path =
|
||||||
|
Config.get(
|
||||||
|
[:mrf_steal_emoji, :path],
|
||||||
|
Path.join(Config.get([:instance, :static_dir]), "emoji/stolen")
|
||||||
|
)
|
||||||
|
|
||||||
|
extension =
|
||||||
|
url
|
||||||
|
|> URI.parse()
|
||||||
|
|> Map.get(:path)
|
||||||
|
|> Path.basename()
|
||||||
|
|> Path.extname()
|
||||||
|
|
||||||
|
file_path = Path.join([emoji_dir_path, shortcode <> (extension || ".png")])
|
||||||
|
|
||||||
|
try do
|
||||||
|
:ok = File.write(file_path, response.body)
|
||||||
|
|
||||||
|
shortcode
|
||||||
|
rescue
|
||||||
|
e ->
|
||||||
|
Logger.warn("MRF.StealEmojiPolicy: Failed to write to #{file_path}: #{inspect(e)}")
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
else
|
||||||
|
Logger.debug(
|
||||||
|
"MRF.StealEmojiPolicy: :#{shortcode}: at #{url} (#{byte_size(response.body)} B) over size limit (#{
|
||||||
|
size_limit
|
||||||
|
} B)"
|
||||||
|
)
|
||||||
|
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
rescue
|
||||||
|
e ->
|
||||||
|
Logger.warn("MRF.StealEmojiPolicy: Failed to fetch #{url}: #{inspect(e)}")
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def filter(%{"object" => %{"emoji" => foreign_emojis, "actor" => actor}} = message) do
|
||||||
|
host = URI.parse(actor).host
|
||||||
|
|
||||||
|
if remote_host?(host) and accept_host?(host) do
|
||||||
|
installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end)
|
||||||
|
|
||||||
|
new_emojis =
|
||||||
|
foreign_emojis
|
||||||
|
|> Enum.filter(fn {shortcode, _url} -> shortcode not in installed_emoji end)
|
||||||
|
|> Enum.filter(fn {shortcode, _url} ->
|
||||||
|
reject_emoji? =
|
||||||
|
Config.get([:mrf_steal_emoji, :rejected_shortcodes], [])
|
||||||
|
|> Enum.find(false, fn regex -> String.match?(shortcode, regex) end)
|
||||||
|
|
||||||
|
!reject_emoji?
|
||||||
|
end)
|
||||||
|
|> Enum.map(&steal_emoji(&1))
|
||||||
|
|> Enum.filter(& &1)
|
||||||
|
|
||||||
|
if !Enum.empty?(new_emojis) do
|
||||||
|
Logger.info("Stole new emojis: #{inspect(new_emojis)}")
|
||||||
|
Pleroma.Emoji.reload()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
{:ok, message}
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter(message), do: {:ok, message}
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def describe do
|
||||||
|
{:ok, %{}}
|
||||||
|
end
|
||||||
|
end
|
|
@ -11,6 +11,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
|
||||||
|
|
||||||
alias Pleroma.Object
|
alias Pleroma.Object
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
|
alias Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator
|
||||||
alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator
|
alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator
|
||||||
alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator
|
alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator
|
||||||
alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
|
alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
|
||||||
|
@ -58,6 +59,16 @@ def validate(%{"type" => "EmojiReact"} = object, meta) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def validate(%{"type" => "Announce"} = object, meta) do
|
||||||
|
with {:ok, object} <-
|
||||||
|
object
|
||||||
|
|> AnnounceValidator.cast_and_validate()
|
||||||
|
|> Ecto.Changeset.apply_action(:insert) do
|
||||||
|
object = stringify_keys(object |> Map.from_struct())
|
||||||
|
{:ok, object, meta}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def stringify_keys(%{__struct__: _} = object) do
|
def stringify_keys(%{__struct__: _} = object) do
|
||||||
object
|
object
|
||||||
|> Map.from_struct()
|
|> Map.from_struct()
|
||||||
|
@ -77,7 +88,7 @@ def fetch_actor(object) do
|
||||||
|
|
||||||
def fetch_actor_and_object(object) do
|
def fetch_actor_and_object(object) do
|
||||||
fetch_actor(object)
|
fetch_actor(object)
|
||||||
Object.normalize(object["object"])
|
Object.normalize(object["object"], true)
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,101 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator do
|
||||||
|
use Ecto.Schema
|
||||||
|
|
||||||
|
alias Pleroma.Object
|
||||||
|
alias Pleroma.User
|
||||||
|
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
|
||||||
|
alias Pleroma.Web.ActivityPub.Utils
|
||||||
|
alias Pleroma.Web.ActivityPub.Visibility
|
||||||
|
|
||||||
|
import Ecto.Changeset
|
||||||
|
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
|
||||||
|
|
||||||
|
require Pleroma.Constants
|
||||||
|
|
||||||
|
@primary_key false
|
||||||
|
|
||||||
|
embedded_schema do
|
||||||
|
field(:id, Types.ObjectID, primary_key: true)
|
||||||
|
field(:type, :string)
|
||||||
|
field(:object, Types.ObjectID)
|
||||||
|
field(:actor, Types.ObjectID)
|
||||||
|
field(:context, :string, autogenerate: {Utils, :generate_context_id, []})
|
||||||
|
field(:to, Types.Recipients, default: [])
|
||||||
|
field(:cc, Types.Recipients, default: [])
|
||||||
|
field(:published, Types.DateTime)
|
||||||
|
end
|
||||||
|
|
||||||
|
def cast_and_validate(data) do
|
||||||
|
data
|
||||||
|
|> cast_data()
|
||||||
|
|> validate_data()
|
||||||
|
end
|
||||||
|
|
||||||
|
def cast_data(data) do
|
||||||
|
%__MODULE__{}
|
||||||
|
|> changeset(data)
|
||||||
|
end
|
||||||
|
|
||||||
|
def changeset(struct, data) do
|
||||||
|
struct
|
||||||
|
|> cast(data, __schema__(:fields))
|
||||||
|
|> fix_after_cast()
|
||||||
|
end
|
||||||
|
|
||||||
|
def fix_after_cast(cng) do
|
||||||
|
cng
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_data(data_cng) do
|
||||||
|
data_cng
|
||||||
|
|> validate_inclusion(:type, ["Announce"])
|
||||||
|
|> validate_required([:id, :type, :object, :actor, :to, :cc])
|
||||||
|
|> validate_actor_presence()
|
||||||
|
|> validate_object_presence()
|
||||||
|
|> validate_existing_announce()
|
||||||
|
|> validate_announcable()
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_announcable(cng) do
|
||||||
|
with actor when is_binary(actor) <- get_field(cng, :actor),
|
||||||
|
object when is_binary(object) <- get_field(cng, :object),
|
||||||
|
%User{} = actor <- User.get_cached_by_ap_id(actor),
|
||||||
|
%Object{} = object <- Object.get_cached_by_ap_id(object),
|
||||||
|
false <- Visibility.is_public?(object) do
|
||||||
|
same_actor = object.data["actor"] == actor.ap_id
|
||||||
|
is_public = Pleroma.Constants.as_public() in (get_field(cng, :to) ++ get_field(cng, :cc))
|
||||||
|
|
||||||
|
cond do
|
||||||
|
same_actor && is_public ->
|
||||||
|
cng
|
||||||
|
|> add_error(:actor, "can not announce this object publicly")
|
||||||
|
|
||||||
|
!same_actor ->
|
||||||
|
cng
|
||||||
|
|> add_error(:actor, "can not announce this object")
|
||||||
|
|
||||||
|
true ->
|
||||||
|
cng
|
||||||
|
end
|
||||||
|
else
|
||||||
|
_ -> cng
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_existing_announce(cng) do
|
||||||
|
actor = get_field(cng, :actor)
|
||||||
|
object = get_field(cng, :object)
|
||||||
|
|
||||||
|
if actor && object && Utils.get_existing_announce(actor, %{data: %{"id" => object}}) do
|
||||||
|
cng
|
||||||
|
|> add_error(:actor, "already announced this object")
|
||||||
|
|> add_error(:object, "already announced by this actor")
|
||||||
|
else
|
||||||
|
cng
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -51,6 +51,7 @@ def add_deleted_activity_id(cng) do
|
||||||
Page
|
Page
|
||||||
Question
|
Question
|
||||||
Video
|
Video
|
||||||
|
Tombstone
|
||||||
}
|
}
|
||||||
def validate_data(cng) do
|
def validate_data(cng) do
|
||||||
cng
|
cng
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
defmodule Pleroma.Web.ActivityPub.Pipeline do
|
defmodule Pleroma.Web.ActivityPub.Pipeline do
|
||||||
alias Pleroma.Activity
|
alias Pleroma.Activity
|
||||||
|
alias Pleroma.Config
|
||||||
alias Pleroma.Object
|
alias Pleroma.Object
|
||||||
alias Pleroma.Repo
|
alias Pleroma.Repo
|
||||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||||
|
@ -44,7 +45,7 @@ defp maybe_federate(%Object{}, _), do: {:ok, :not_federated}
|
||||||
|
|
||||||
defp maybe_federate(%Activity{} = activity, meta) do
|
defp maybe_federate(%Activity{} = activity, meta) do
|
||||||
with {:ok, local} <- Keyword.fetch(meta, :local) do
|
with {:ok, local} <- Keyword.fetch(meta, :local) do
|
||||||
do_not_federate = meta[:do_not_federate]
|
do_not_federate = meta[:do_not_federate] || !Config.get([:instance, :federating])
|
||||||
|
|
||||||
if !do_not_federate && local do
|
if !do_not_federate && local do
|
||||||
Federator.publish(activity)
|
Federator.publish(activity)
|
||||||
|
|
|
@ -4,9 +4,10 @@
|
||||||
|
|
||||||
defmodule Pleroma.Web.ActivityPub.Relay do
|
defmodule Pleroma.Web.ActivityPub.Relay do
|
||||||
alias Pleroma.Activity
|
alias Pleroma.Activity
|
||||||
alias Pleroma.Object
|
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||||
|
alias Pleroma.Web.ActivityPub.Visibility
|
||||||
|
alias Pleroma.Web.CommonAPI
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
@relay_nickname "relay"
|
@relay_nickname "relay"
|
||||||
|
@ -48,11 +49,11 @@ def unfollow(target_instance) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec publish(any()) :: {:ok, Activity.t(), Object.t()} | {:error, any()}
|
@spec publish(any()) :: {:ok, Activity.t()} | {:error, any()}
|
||||||
def publish(%Activity{data: %{"type" => "Create"}} = activity) do
|
def publish(%Activity{data: %{"type" => "Create"}} = activity) do
|
||||||
with %User{} = user <- get_actor(),
|
with %User{} = user <- get_actor(),
|
||||||
%Object{} = object <- Object.normalize(activity) do
|
true <- Visibility.is_public?(activity) do
|
||||||
ActivityPub.announce(user, object, nil, true, false)
|
CommonAPI.repeat(activity.id, user)
|
||||||
else
|
else
|
||||||
error -> format_error(error)
|
error -> format_error(error)
|
||||||
end
|
end
|
||||||
|
|
|
@ -27,6 +27,24 @@ def handle(%{data: %{"type" => "Like"}} = object, meta) do
|
||||||
{:ok, object, meta}
|
{:ok, object, meta}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Tasks this handles:
|
||||||
|
# - Add announce to object
|
||||||
|
# - Set up notification
|
||||||
|
# - Stream out the announce
|
||||||
|
def handle(%{data: %{"type" => "Announce"}} = object, meta) do
|
||||||
|
announced_object = Object.get_by_ap_id(object.data["object"])
|
||||||
|
user = User.get_cached_by_ap_id(object.data["actor"])
|
||||||
|
|
||||||
|
Utils.add_announce_to_object(object, announced_object)
|
||||||
|
|
||||||
|
if !User.is_internal_user?(user) do
|
||||||
|
Notification.create_notifications(object)
|
||||||
|
ActivityPub.stream_out(object)
|
||||||
|
end
|
||||||
|
|
||||||
|
{:ok, object, meta}
|
||||||
|
end
|
||||||
|
|
||||||
def handle(%{data: %{"type" => "Undo", "object" => undone_object}} = object, meta) do
|
def handle(%{data: %{"type" => "Undo", "object" => undone_object}} = object, meta) do
|
||||||
with undone_object <- Activity.get_by_ap_id(undone_object),
|
with undone_object <- Activity.get_by_ap_id(undone_object),
|
||||||
:ok <- handle_undoing(undone_object) do
|
:ok <- handle_undoing(undone_object) do
|
||||||
|
|
|
@ -9,12 +9,15 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
||||||
alias Pleroma.Activity
|
alias Pleroma.Activity
|
||||||
alias Pleroma.EarmarkRenderer
|
alias Pleroma.EarmarkRenderer
|
||||||
alias Pleroma.FollowingRelationship
|
alias Pleroma.FollowingRelationship
|
||||||
|
alias Pleroma.Maps
|
||||||
alias Pleroma.Object
|
alias Pleroma.Object
|
||||||
alias Pleroma.Object.Containment
|
alias Pleroma.Object.Containment
|
||||||
alias Pleroma.Repo
|
alias Pleroma.Repo
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||||
|
alias Pleroma.Web.ActivityPub.Builder
|
||||||
alias Pleroma.Web.ActivityPub.ObjectValidator
|
alias Pleroma.Web.ActivityPub.ObjectValidator
|
||||||
|
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
|
||||||
alias Pleroma.Web.ActivityPub.Pipeline
|
alias Pleroma.Web.ActivityPub.Pipeline
|
||||||
alias Pleroma.Web.ActivityPub.Utils
|
alias Pleroma.Web.ActivityPub.Utils
|
||||||
alias Pleroma.Web.ActivityPub.Visibility
|
alias Pleroma.Web.ActivityPub.Visibility
|
||||||
|
@ -206,12 +209,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 ->
|
||||||
|
@ -239,13 +236,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)
|
||||||
|
@ -590,6 +587,9 @@ def handle_incoming(
|
||||||
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
|
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
|
||||||
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
|
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
|
||||||
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept) do
|
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept) do
|
||||||
|
User.update_follower_count(followed)
|
||||||
|
User.update_following_count(follower)
|
||||||
|
|
||||||
ActivityPub.accept(%{
|
ActivityPub.accept(%{
|
||||||
to: follow_activity.data["to"],
|
to: follow_activity.data["to"],
|
||||||
type: "Accept",
|
type: "Accept",
|
||||||
|
@ -599,7 +599,8 @@ def handle_incoming(
|
||||||
activity_id: id
|
activity_id: id
|
||||||
})
|
})
|
||||||
else
|
else
|
||||||
_e -> :error
|
_e ->
|
||||||
|
:error
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -656,7 +657,8 @@ def handle_incoming(
|
||||||
|> handle_incoming(options)
|
|> handle_incoming(options)
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_incoming(%{"type" => type} = data, _options) when type in ["Like", "EmojiReact"] do
|
def handle_incoming(%{"type" => type} = data, _options)
|
||||||
|
when type in ["Like", "EmojiReact", "Announce"] do
|
||||||
with :ok <- ObjectValidator.fetch_actor_and_object(data),
|
with :ok <- ObjectValidator.fetch_actor_and_object(data),
|
||||||
{:ok, activity, _meta} <-
|
{:ok, activity, _meta} <-
|
||||||
Pipeline.common_pipeline(data, local: false) do
|
Pipeline.common_pipeline(data, local: false) do
|
||||||
|
@ -666,21 +668,6 @@ def handle_incoming(%{"type" => type} = data, _options) when type in ["Like", "E
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_incoming(
|
|
||||||
%{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
|
|
||||||
_options
|
|
||||||
) do
|
|
||||||
with actor <- Containment.get_actor(data),
|
|
||||||
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
|
|
||||||
{:ok, object} <- get_embedded_obj_helper(object_id, actor),
|
|
||||||
public <- Visibility.is_public?(data),
|
|
||||||
{:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
|
|
||||||
{:ok, activity}
|
|
||||||
else
|
|
||||||
_e -> :error
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_incoming(
|
def handle_incoming(
|
||||||
%{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
|
%{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
|
||||||
data,
|
data,
|
||||||
|
@ -720,6 +707,19 @@ def handle_incoming(
|
||||||
) do
|
) do
|
||||||
with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
|
with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
|
||||||
{:ok, activity}
|
{:ok, activity}
|
||||||
|
else
|
||||||
|
{:error, {:validate_object, _}} = e ->
|
||||||
|
# Check if we have a create activity for this
|
||||||
|
with {:ok, object_id} <- Types.ObjectID.cast(data["object"]),
|
||||||
|
%Activity{data: %{"actor" => actor}} <-
|
||||||
|
Activity.create_by_object_ap_id(object_id) |> Repo.one(),
|
||||||
|
# We have one, insert a tombstone and retry
|
||||||
|
{:ok, tombstone_data, _} <- Builder.tombstone(actor, object_id),
|
||||||
|
{:ok, _tombstone} <- Object.create(tombstone_data) do
|
||||||
|
handle_incoming(data)
|
||||||
|
else
|
||||||
|
_ -> e
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1040,10 +1040,14 @@ def add_hashtags(object) do
|
||||||
Map.put(object, "tag", tags)
|
Map.put(object, "tag", tags)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# TODO These should be added on our side on insertion, it doesn't make much
|
||||||
|
# sense to regenerate these all the time
|
||||||
def add_mention_tags(object) do
|
def add_mention_tags(object) do
|
||||||
{enabled_receivers, disabled_receivers} = Utils.get_notified_from_object(object)
|
to = object["to"] || []
|
||||||
potential_receivers = enabled_receivers ++ disabled_receivers
|
cc = object["cc"] || []
|
||||||
mentions = Enum.map(potential_receivers, &build_mention_tag/1)
|
mentioned = User.get_users_from_set(to ++ cc, local_only: false)
|
||||||
|
|
||||||
|
mentions = Enum.map(mentioned, &build_mention_tag/1)
|
||||||
|
|
||||||
tags = object["tag"] || []
|
tags = object["tag"] || []
|
||||||
Map.put(object, "tag", tags ++ mentions)
|
Map.put(object, "tag", tags ++ mentions)
|
||||||
|
|
|
@ -6,6 +6,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
||||||
alias Ecto.Changeset
|
alias Ecto.Changeset
|
||||||
alias Ecto.UUID
|
alias Ecto.UUID
|
||||||
alias Pleroma.Activity
|
alias Pleroma.Activity
|
||||||
|
alias Pleroma.Config
|
||||||
|
alias Pleroma.Maps
|
||||||
alias Pleroma.Notification
|
alias Pleroma.Notification
|
||||||
alias Pleroma.Object
|
alias Pleroma.Object
|
||||||
alias Pleroma.Repo
|
alias Pleroma.Repo
|
||||||
|
@ -169,8 +171,11 @@ def create_context(context) do
|
||||||
Enqueues an activity for federation if it's local
|
Enqueues an activity for federation if it's local
|
||||||
"""
|
"""
|
||||||
@spec maybe_federate(any()) :: :ok
|
@spec maybe_federate(any()) :: :ok
|
||||||
def maybe_federate(%Activity{local: true} = activity) do
|
def maybe_federate(%Activity{local: true, data: %{"type" => type}} = activity) do
|
||||||
if Pleroma.Config.get!([:instance, :federating]) do
|
outgoing_blocks = Config.get([:activitypub, :outgoing_blocks])
|
||||||
|
|
||||||
|
with true <- Config.get!([:instance, :federating]),
|
||||||
|
true <- type != "Block" || outgoing_blocks do
|
||||||
Pleroma.Web.Federator.publish(activity)
|
Pleroma.Web.Federator.publish(activity)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -303,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
|
||||||
|
@ -473,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
|
||||||
|
@ -542,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(
|
||||||
|
@ -559,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(
|
||||||
|
@ -578,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()) ::
|
||||||
|
@ -623,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
|
||||||
|
@ -646,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
|
||||||
|
@ -736,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)
|
||||||
|
@ -866,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
|
||||||
|
|
|
@ -213,34 +213,24 @@ def render("activity_collection.json", %{iri: iri}) do
|
||||||
|> Map.merge(Utils.make_json_ld_header())
|
|> Map.merge(Utils.make_json_ld_header())
|
||||||
end
|
end
|
||||||
|
|
||||||
def render("activity_collection_page.json", %{activities: activities, iri: iri}) do
|
def render("activity_collection_page.json", %{
|
||||||
# this is sorted chronologically, so first activity is the newest (max)
|
activities: activities,
|
||||||
{max_id, min_id, collection} =
|
iri: iri,
|
||||||
if length(activities) > 0 do
|
pagination: pagination
|
||||||
{
|
}) do
|
||||||
Enum.at(activities, 0).id,
|
collection =
|
||||||
Enum.at(Enum.reverse(activities), 0).id,
|
Enum.map(activities, fn activity ->
|
||||||
Enum.map(activities, fn act ->
|
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
|
||||||
{:ok, data} = Transmogrifier.prepare_outgoing(act.data)
|
data
|
||||||
data
|
end)
|
||||||
end)
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
[]
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
%{
|
%{
|
||||||
"id" => "#{iri}?max_id=#{max_id}&page=true",
|
|
||||||
"type" => "OrderedCollectionPage",
|
"type" => "OrderedCollectionPage",
|
||||||
"partOf" => iri,
|
"partOf" => iri,
|
||||||
"orderedItems" => collection,
|
"orderedItems" => collection
|
||||||
"next" => "#{iri}?max_id=#{min_id}&page=true"
|
|
||||||
}
|
}
|
||||||
|> Map.merge(Utils.make_json_ld_header())
|
|> Map.merge(Utils.make_json_ld_header())
|
||||||
|
|> Map.merge(pagination)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_put_total_items(map, false, _total), do: map
|
defp maybe_put_total_items(map, false, _total), do: map
|
||||||
|
|
|
@ -7,37 +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.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.AppView
|
|
||||||
alias Pleroma.Web.MastodonAPI.StatusView
|
|
||||||
alias Pleroma.Web.OAuth.App
|
|
||||||
alias Pleroma.Web.Router
|
alias Pleroma.Web.Router
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
@descriptions_json Pleroma.Docs.JSON.compile()
|
|
||||||
@users_page_size 50
|
@users_page_size 50
|
||||||
|
|
||||||
plug(
|
plug(
|
||||||
|
@ -68,53 +56,25 @@ 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}
|
||||||
when action in [:list_statuses, :list_user_statuses, :list_instance_statuses, :status_show]
|
when action in [:list_user_statuses, :list_instance_statuses]
|
||||||
)
|
|
||||||
|
|
||||||
plug(
|
|
||||||
OAuthScopesPlug,
|
|
||||||
%{scopes: ["write:statuses"], admin: true}
|
|
||||||
when action in [:status_update, :status_delete]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
plug(
|
plug(
|
||||||
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
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
@ -124,18 +84,13 @@ 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
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
action_fallback(:errors)
|
action_fallback(AdminAPI.FallbackController)
|
||||||
|
|
||||||
def user_delete(conn, %{"nickname" => nickname}) do
|
def user_delete(conn, %{"nickname" => nickname}) do
|
||||||
user_delete(conn, %{"nicknames" => [nickname]})
|
user_delete(conn, %{"nicknames" => [nickname]})
|
||||||
|
@ -280,8 +235,8 @@ def list_instance_statuses(conn, %{"instance" => instance} = params) do
|
||||||
})
|
})
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> put_view(Pleroma.Web.AdminAPI.StatusView)
|
|> put_view(AdminAPI.StatusView)
|
||||||
|> render("index.json", %{activities: activities, as: :activity, skip_relationships: false})
|
|> render("index.json", %{activities: activities, as: :activity})
|
||||||
end
|
end
|
||||||
|
|
||||||
def list_user_statuses(conn, %{"nickname" => nickname} = params) do
|
def list_user_statuses(conn, %{"nickname" => nickname} = params) do
|
||||||
|
@ -299,8 +254,8 @@ def list_user_statuses(conn, %{"nickname" => nickname} = params) do
|
||||||
})
|
})
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> put_view(StatusView)
|
|> put_view(AdminAPI.StatusView)
|
||||||
|> render("index.json", %{activities: activities, as: :activity, skip_relationships: false})
|
|> render("index.json", %{activities: activities, as: :activity})
|
||||||
else
|
else
|
||||||
_ -> {:error, :not_found}
|
_ -> {:error, :not_found}
|
||||||
end
|
end
|
||||||
|
@ -580,75 +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, _} ->
|
|
||||||
errors(
|
|
||||||
conn,
|
|
||||||
{:error, "To send invites you need to set the `registrations_open` option to false."}
|
|
||||||
)
|
|
||||||
|
|
||||||
{:invites_enabled, _} ->
|
|
||||||
errors(
|
|
||||||
conn,
|
|
||||||
{: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)
|
||||||
|
@ -704,7 +590,7 @@ def update_user_credentials(
|
||||||
%{assigns: %{user: admin}} = conn,
|
%{assigns: %{user: admin}} = conn,
|
||||||
%{"nickname" => nickname} = params
|
%{"nickname" => nickname} = params
|
||||||
) do
|
) do
|
||||||
with {_, user} <- {:user, User.get_cached_by_nickname(nickname)},
|
with {_, %User{} = user} <- {:user, User.get_cached_by_nickname(nickname)},
|
||||||
{:ok, _user} <-
|
{:ok, _user} <-
|
||||||
User.update_as_admin(user, params) do
|
User.update_as_admin(user, params) do
|
||||||
ModerationLog.insert_log(%{
|
ModerationLog.insert_log(%{
|
||||||
|
@ -726,150 +612,12 @@ def update_user_credentials(
|
||||||
json(conn, %{status: "success"})
|
json(conn, %{status: "success"})
|
||||||
else
|
else
|
||||||
{:error, changeset} ->
|
{:error, changeset} ->
|
||||||
{_, {error, _}} = Enum.at(changeset.errors, 0)
|
errors = Map.new(changeset.errors, fn {key, {error, _}} -> {key, error} end)
|
||||||
json(conn, %{error: "New password #{error}."})
|
|
||||||
|
json(conn, %{errors: errors})
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
json(conn, %{error: "Unable to change password."})
|
json(conn, %{error: "Unable to update user."})
|
||||||
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_statuses(%{assigns: %{user: _admin}} = conn, params) do
|
|
||||||
godmode = params["godmode"] == "true" || params["godmode"] == true
|
|
||||||
local_only = params["local_only"] == "true" || params["local_only"] == true
|
|
||||||
with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
|
|
||||||
{page, page_size} = page_params(params)
|
|
||||||
|
|
||||||
activities =
|
|
||||||
ActivityPub.fetch_statuses(nil, %{
|
|
||||||
"godmode" => godmode,
|
|
||||||
"local_only" => local_only,
|
|
||||||
"limit" => page_size,
|
|
||||||
"offset" => (page - 1) * page_size,
|
|
||||||
"exclude_reblogs" => !with_reblogs && "true"
|
|
||||||
})
|
|
||||||
|
|
||||||
conn
|
|
||||||
|> put_view(Pleroma.Web.AdminAPI.StatusView)
|
|
||||||
|> render("index.json", %{activities: activities, as: :activity, skip_relationships: false})
|
|
||||||
end
|
|
||||||
|
|
||||||
def status_show(conn, %{"id" => id}) do
|
|
||||||
with %Activity{} = activity <- Activity.get_by_id(id) do
|
|
||||||
conn
|
|
||||||
|> put_view(StatusView)
|
|
||||||
|> render("show.json", %{activity: activity})
|
|
||||||
else
|
|
||||||
_ -> errors(conn, {:error, :not_found})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
|
|
||||||
with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
|
|
||||||
{:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"])
|
|
||||||
|
|
||||||
ModerationLog.insert_log(%{
|
|
||||||
action: "status_update",
|
|
||||||
actor: admin,
|
|
||||||
subject: activity,
|
|
||||||
sensitive: sensitive,
|
|
||||||
visibility: params["visibility"]
|
|
||||||
})
|
|
||||||
|
|
||||||
conn
|
|
||||||
|> put_view(StatusView)
|
|
||||||
|> render("show.json", %{activity: activity})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
|
|
||||||
with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
|
|
||||||
ModerationLog.insert_log(%{
|
|
||||||
action: "status_delete",
|
|
||||||
actor: user,
|
|
||||||
subject_id: id
|
|
||||||
})
|
|
||||||
|
|
||||||
json(conn, %{})
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -891,105 +639,8 @@ def list_log(conn, params) do
|
||||||
|> render("index.json", %{log: log})
|
|> render("index.json", %{log: log})
|
||||||
end
|
end
|
||||||
|
|
||||||
def config_descriptions(conn, _params) do
|
|
||||||
conn
|
|
||||||
|> Plug.Conn.put_resp_content_type("application/json")
|
|
||||||
|> Plug.Conn.send_resp(200, @descriptions_json)
|
|
||||||
end
|
|
||||||
|
|
||||||
def config_show(conn, %{"only_db" => true}) do
|
|
||||||
with :ok <- configurable_from_database(conn) 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(conn) 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(conn) do
|
|
||||||
{_errors, results} =
|
|
||||||
Enum.map(configs, 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(conn) do
|
with :ok <- configurable_from_database() do
|
||||||
Restarter.Pleroma.restart(Config.get(:env), 50)
|
Restarter.Pleroma.restart(Config.get(:env), 50)
|
||||||
|
|
||||||
json(conn, %{})
|
json(conn, %{})
|
||||||
|
@ -1000,14 +651,11 @@ def need_reboot(conn, _params) do
|
||||||
json(conn, %{need_reboot: Restarter.Pleroma.need_reboot?()})
|
json(conn, %{need_reboot: Restarter.Pleroma.need_reboot?()})
|
||||||
end
|
end
|
||||||
|
|
||||||
defp configurable_from_database(conn) do
|
defp configurable_from_database do
|
||||||
if Config.get(:configurable_from_database) do
|
if Config.get(:configurable_from_database) do
|
||||||
:ok
|
:ok
|
||||||
else
|
else
|
||||||
errors(
|
{:error, "To use this endpoint you need to enable configuration from database."}
|
||||||
conn,
|
|
||||||
{:error, "To use this endpoint you need to enable configuration from database."}
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1045,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()
|
||||||
|
|
||||||
|
@ -1129,30 +700,6 @@ def stats(conn, _) do
|
||||||
|> json(%{"status_visibility" => count})
|
|> json(%{"status_visibility" => count})
|
||||||
end
|
end
|
||||||
|
|
||||||
defp errors(conn, {:error, :not_found}) do
|
|
||||||
conn
|
|
||||||
|> put_status(:not_found)
|
|
||||||
|> json(dgettext("errors", "Not found"))
|
|
||||||
end
|
|
||||||
|
|
||||||
defp errors(conn, {:error, reason}) do
|
|
||||||
conn
|
|
||||||
|> put_status(:bad_request)
|
|
||||||
|> json(reason)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp errors(conn, {:param_cast, _}) do
|
|
||||||
conn
|
|
||||||
|> put_status(:bad_request)
|
|
||||||
|> json(dgettext("errors", "Invalid parameters"))
|
|
||||||
end
|
|
||||||
|
|
||||||
defp errors(conn, _) do
|
|
||||||
conn
|
|
||||||
|> put_status(:internal_server_error)
|
|
||||||
|> json(dgettext("errors", "Something went wrong"))
|
|
||||||
end
|
|
||||||
|
|
||||||
defp page_params(params) do
|
defp page_params(params) do
|
||||||
{get_page(params["page"]), get_page_size(params["page_size"])}
|
{get_page(params["page"]), get_page_size(params["page_size"])}
|
||||||
end
|
end
|
152
lib/pleroma/web/admin_api/controllers/config_controller.ex
Normal file
152
lib/pleroma/web/admin_api/controllers/config_controller.ex
Normal 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
|
31
lib/pleroma/web/admin_api/controllers/fallback_controller.ex
Normal file
31
lib/pleroma/web/admin_api/controllers/fallback_controller.ex
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# 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.FallbackController do
|
||||||
|
use Pleroma.Web, :controller
|
||||||
|
|
||||||
|
def call(conn, {:error, :not_found}) do
|
||||||
|
conn
|
||||||
|
|> put_status(:not_found)
|
||||||
|
|> json(%{error: dgettext("errors", "Not found")})
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(conn, {:error, reason}) do
|
||||||
|
conn
|
||||||
|
|> put_status(:bad_request)
|
||||||
|
|> json(%{error: reason})
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(conn, {:param_cast, _}) do
|
||||||
|
conn
|
||||||
|
|> put_status(:bad_request)
|
||||||
|
|> json(dgettext("errors", "Invalid parameters"))
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(conn, _) do
|
||||||
|
conn
|
||||||
|
|> put_status(:internal_server_error)
|
||||||
|
|> json(%{error: dgettext("errors", "Something went wrong")})
|
||||||
|
end
|
||||||
|
end
|
78
lib/pleroma/web/admin_api/controllers/invite_controller.ex
Normal file
78
lib/pleroma/web/admin_api/controllers/invite_controller.ex
Normal 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
|
|
@ -0,0 +1,77 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.AdminAPI.OAuthAppController do
|
||||||
|
use Pleroma.Web, :controller
|
||||||
|
|
||||||
|
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
|
||||||
|
|
||||||
|
alias Pleroma.Plugs.OAuthScopesPlug
|
||||||
|
alias Pleroma.Web.OAuth.App
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||||
|
plug(:put_view, Pleroma.Web.MastodonAPI.AppView)
|
||||||
|
|
||||||
|
plug(
|
||||||
|
OAuthScopesPlug,
|
||||||
|
%{scopes: ["write"], admin: true}
|
||||||
|
when action in [:create, :index, :update, :delete]
|
||||||
|
)
|
||||||
|
|
||||||
|
action_fallback(Pleroma.Web.AdminAPI.FallbackController)
|
||||||
|
|
||||||
|
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.OAuthAppOperation
|
||||||
|
|
||||||
|
def index(conn, params) do
|
||||||
|
search_params =
|
||||||
|
params
|
||||||
|
|> Map.take([:client_id, :page, :page_size, :trusted])
|
||||||
|
|> Map.put(:client_name, params[:name])
|
||||||
|
|
||||||
|
with {:ok, apps, count} <- App.search(search_params) do
|
||||||
|
render(conn, "index.json",
|
||||||
|
apps: apps,
|
||||||
|
count: count,
|
||||||
|
page_size: params.page_size,
|
||||||
|
admin: true
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create(%{body_params: params} = conn, _) do
|
||||||
|
params = Pleroma.Maps.put_if_present(params, :client_name, params[:name])
|
||||||
|
|
||||||
|
case App.create(params) do
|
||||||
|
{:ok, app} ->
|
||||||
|
render(conn, "show.json", app: app, admin: true)
|
||||||
|
|
||||||
|
{:error, changeset} ->
|
||||||
|
json(conn, App.errors(changeset))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update(%{body_params: params} = conn, %{id: id}) do
|
||||||
|
params = Pleroma.Maps.put_if_present(params, :client_name, params[:name])
|
||||||
|
|
||||||
|
with {:ok, app} <- App.update(id, params) do
|
||||||
|
render(conn, "show.json", app: app, admin: true)
|
||||||
|
else
|
||||||
|
{:error, changeset} ->
|
||||||
|
json(conn, App.errors(changeset))
|
||||||
|
|
||||||
|
nil ->
|
||||||
|
json_response(conn, :bad_request, "")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete(conn, params) do
|
||||||
|
with {:ok, _app} <- App.destroy(params.id) do
|
||||||
|
json_response(conn, :no_content, "")
|
||||||
|
else
|
||||||
|
_ -> json_response(conn, :bad_request, "")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
107
lib/pleroma/web/admin_api/controllers/report_controller.ex
Normal file
107
lib/pleroma/web/admin_api/controllers/report_controller.ex
Normal 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
|
77
lib/pleroma/web/admin_api/controllers/status_controller.ex
Normal file
77
lib/pleroma/web/admin_api/controllers/status_controller.ex
Normal 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.StatusController do
|
||||||
|
use Pleroma.Web, :controller
|
||||||
|
|
||||||
|
alias Pleroma.Activity
|
||||||
|
alias Pleroma.ModerationLog
|
||||||
|
alias Pleroma.Plugs.OAuthScopesPlug
|
||||||
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||||
|
alias Pleroma.Web.CommonAPI
|
||||||
|
alias Pleroma.Web.MastodonAPI
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||||
|
plug(OAuthScopesPlug, %{scopes: ["read:statuses"], admin: true} when action in [:index, :show])
|
||||||
|
|
||||||
|
plug(
|
||||||
|
OAuthScopesPlug,
|
||||||
|
%{scopes: ["write:statuses"], admin: true} when action in [:update, :delete]
|
||||||
|
)
|
||||||
|
|
||||||
|
action_fallback(Pleroma.Web.AdminAPI.FallbackController)
|
||||||
|
|
||||||
|
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.StatusOperation
|
||||||
|
|
||||||
|
def index(%{assigns: %{user: _admin}} = conn, params) do
|
||||||
|
activities =
|
||||||
|
ActivityPub.fetch_statuses(nil, %{
|
||||||
|
"godmode" => params.godmode,
|
||||||
|
"local_only" => params.local_only,
|
||||||
|
"limit" => params.page_size,
|
||||||
|
"offset" => (params.page - 1) * params.page_size,
|
||||||
|
"exclude_reblogs" => not params.with_reblogs
|
||||||
|
})
|
||||||
|
|
||||||
|
render(conn, "index.json", activities: activities, as: :activity)
|
||||||
|
end
|
||||||
|
|
||||||
|
def show(conn, %{id: id}) do
|
||||||
|
with %Activity{} = activity <- Activity.get_by_id(id) do
|
||||||
|
render(conn, "show.json", %{activity: activity})
|
||||||
|
else
|
||||||
|
nil -> {:error, :not_found}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update(%{assigns: %{user: admin}, body_params: params} = conn, %{id: id}) do
|
||||||
|
with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
|
||||||
|
ModerationLog.insert_log(%{
|
||||||
|
action: "status_update",
|
||||||
|
actor: admin,
|
||||||
|
subject: activity,
|
||||||
|
sensitive: params[:sensitive],
|
||||||
|
visibility: params[:visibility]
|
||||||
|
})
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_view(MastodonAPI.StatusView)
|
||||||
|
|> render("show.json", %{activity: activity})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
|
||||||
|
with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
|
||||||
|
ModerationLog.insert_log(%{
|
||||||
|
action: "status_delete",
|
||||||
|
actor: user,
|
||||||
|
subject_id: id
|
||||||
|
})
|
||||||
|
|
||||||
|
json(conn, %{})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -21,7 +21,7 @@ def user(params \\ %{}) do
|
||||||
query =
|
query =
|
||||||
params
|
params
|
||||||
|> Map.drop([:page, :page_size])
|
|> Map.drop([:page, :page_size])
|
||||||
|> Map.put(:exclude_service_users, true)
|
|> Map.put(:invisible, false)
|
||||||
|> User.Query.build()
|
|> User.Query.build()
|
||||||
|> order_by([u], u.nickname)
|
|> order_by([u], u.nickname)
|
||||||
|
|
||||||
|
@ -31,7 +31,6 @@ def user(params \\ %{}) do
|
||||||
count = Repo.aggregate(query, :count, :id)
|
count = Repo.aggregate(query, :count, :id)
|
||||||
|
|
||||||
results = Repo.all(paginated_query)
|
results = Repo.all(paginated_query)
|
||||||
|
|
||||||
{:ok, results, count}
|
{:ok, results, count}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,7 +6,9 @@ defmodule Pleroma.Web.AdminAPI.AccountView do
|
||||||
use Pleroma.Web, :view
|
use Pleroma.Web, :view
|
||||||
|
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
|
alias Pleroma.Web.AdminAPI
|
||||||
alias Pleroma.Web.AdminAPI.AccountView
|
alias Pleroma.Web.AdminAPI.AccountView
|
||||||
|
alias Pleroma.Web.MastodonAPI
|
||||||
alias Pleroma.Web.MediaProxy
|
alias Pleroma.Web.MediaProxy
|
||||||
|
|
||||||
def render("index.json", %{users: users, count: count, page_size: page_size}) do
|
def render("index.json", %{users: users, count: count, page_size: page_size}) do
|
||||||
|
@ -78,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",
|
||||||
|
@ -119,6 +103,13 @@ def render("create-error.json", %{changeset: %Ecto.Changeset{changes: changes, e
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def merge_account_views(%User{} = user) do
|
||||||
|
MastodonAPI.AccountView.render("show.json", %{user: user})
|
||||||
|
|> Map.merge(AdminAPI.AccountView.render("show.json", %{user: user}))
|
||||||
|
end
|
||||||
|
|
||||||
|
def merge_account_views(_), do: %{}
|
||||||
|
|
||||||
defp parse_error([]), do: ""
|
defp parse_error([]), do: ""
|
||||||
|
|
||||||
defp parse_error(errors) do
|
defp parse_error(errors) do
|
||||||
|
|
25
lib/pleroma/web/admin_api/views/invite_view.ex
Normal file
25
lib/pleroma/web/admin_api/views/invite_view.ex
Normal 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
|
|
@ -7,10 +7,13 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
|
||||||
|
|
||||||
alias Pleroma.HTML
|
alias Pleroma.HTML
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
|
alias Pleroma.Web.AdminAPI
|
||||||
alias Pleroma.Web.AdminAPI.Report
|
alias Pleroma.Web.AdminAPI.Report
|
||||||
alias Pleroma.Web.CommonAPI.Utils
|
alias Pleroma.Web.CommonAPI.Utils
|
||||||
alias Pleroma.Web.MastodonAPI.StatusView
|
alias Pleroma.Web.MastodonAPI.StatusView
|
||||||
|
|
||||||
|
defdelegate merge_account_views(user), to: AdminAPI.AccountView
|
||||||
|
|
||||||
def render("index.json", %{reports: reports}) do
|
def render("index.json", %{reports: reports}) do
|
||||||
%{
|
%{
|
||||||
reports:
|
reports:
|
||||||
|
@ -41,8 +44,7 @@ def render("show.json", %{report: report, user: user, account: account, statuses
|
||||||
statuses:
|
statuses:
|
||||||
StatusView.render("index.json", %{
|
StatusView.render("index.json", %{
|
||||||
activities: statuses,
|
activities: statuses,
|
||||||
as: :activity,
|
as: :activity
|
||||||
skip_relationships: false
|
|
||||||
}),
|
}),
|
||||||
state: report.data["state"],
|
state: report.data["state"],
|
||||||
notes: render(__MODULE__, "index_notes.json", %{notes: report.report_notes})
|
notes: render(__MODULE__, "index_notes.json", %{notes: report.report_notes})
|
||||||
|
@ -70,11 +72,4 @@ def render("show_note.json", %{
|
||||||
created_at: Utils.to_masto_date(inserted_at)
|
created_at: Utils.to_masto_date(inserted_at)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp merge_account_views(%User{} = user) do
|
|
||||||
Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user})
|
|
||||||
|> Map.merge(Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user}))
|
|
||||||
end
|
|
||||||
|
|
||||||
defp merge_account_views(_), do: %{}
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,24 +7,19 @@ defmodule Pleroma.Web.AdminAPI.StatusView do
|
||||||
|
|
||||||
require Pleroma.Constants
|
require Pleroma.Constants
|
||||||
|
|
||||||
alias Pleroma.User
|
alias Pleroma.Web.AdminAPI
|
||||||
alias Pleroma.Web.MastodonAPI.StatusView
|
alias Pleroma.Web.MastodonAPI
|
||||||
|
|
||||||
|
defdelegate merge_account_views(user), to: AdminAPI.AccountView
|
||||||
|
|
||||||
def render("index.json", opts) do
|
def render("index.json", opts) do
|
||||||
safe_render_many(opts.activities, __MODULE__, "show.json", opts)
|
safe_render_many(opts.activities, __MODULE__, "show.json", opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
|
def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
|
||||||
user = StatusView.get_user(activity.data["actor"])
|
user = MastodonAPI.StatusView.get_user(activity.data["actor"])
|
||||||
|
|
||||||
StatusView.render("show.json", opts)
|
MastodonAPI.StatusView.render("show.json", opts)
|
||||||
|> Map.merge(%{account: merge_account_views(user)})
|
|> Map.merge(%{account: merge_account_views(user)})
|
||||||
end
|
end
|
||||||
|
|
||||||
defp merge_account_views(%User{} = user) do
|
|
||||||
Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user})
|
|
||||||
|> Map.merge(Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user}))
|
|
||||||
end
|
|
||||||
|
|
||||||
defp merge_account_views(_), do: %{}
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
defmodule Pleroma.Web.ApiSpec.Helpers do
|
defmodule Pleroma.Web.ApiSpec.Helpers do
|
||||||
alias OpenApiSpex.Operation
|
alias OpenApiSpex.Operation
|
||||||
alias OpenApiSpex.Schema
|
alias OpenApiSpex.Schema
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
|
||||||
|
|
||||||
def request_body(description, schema_ref, opts \\ []) do
|
def request_body(description, schema_ref, opts \\ []) do
|
||||||
media_types = ["application/json", "multipart/form-data", "application/x-www-form-urlencoded"]
|
media_types = ["application/json", "multipart/form-data", "application/x-www-form-urlencoded"]
|
||||||
|
@ -47,6 +48,15 @@ def pagination_params do
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def with_relationships_param do
|
||||||
|
Operation.parameter(
|
||||||
|
:with_relationships,
|
||||||
|
:query,
|
||||||
|
BooleanLike,
|
||||||
|
"Embed relationships into accounts."
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
def empty_object_response do
|
def empty_object_response do
|
||||||
Operation.response("Empty object", "application/json", %Schema{type: :object, example: %{}})
|
Operation.response("Empty object", "application/json", %Schema{type: :object, example: %{}})
|
||||||
end
|
end
|
||||||
|
@ -54,4 +64,8 @@ def empty_object_response do
|
||||||
def empty_array_response do
|
def empty_array_response do
|
||||||
Operation.response("Empty array", "application/json", %Schema{type: :array, example: []})
|
Operation.response("Empty array", "application/json", %Schema{type: :array, example: []})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def no_content_response do
|
||||||
|
Operation.response("No Content", "application/json", %Schema{type: :string, example: ""})
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -155,8 +155,10 @@ def followers_operation do
|
||||||
security: [%{"oAuth" => ["read:accounts"]}],
|
security: [%{"oAuth" => ["read:accounts"]}],
|
||||||
description:
|
description:
|
||||||
"Accounts which follow the given account, if network is not hidden by the account owner.",
|
"Accounts which follow the given account, if network is not hidden by the account owner.",
|
||||||
parameters:
|
parameters: [
|
||||||
[%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}] ++ pagination_params(),
|
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
|
||||||
|
with_relationships_param() | pagination_params()
|
||||||
|
],
|
||||||
responses: %{
|
responses: %{
|
||||||
200 => Operation.response("Accounts", "application/json", array_of_accounts())
|
200 => Operation.response("Accounts", "application/json", array_of_accounts())
|
||||||
}
|
}
|
||||||
|
@ -171,8 +173,10 @@ def following_operation do
|
||||||
security: [%{"oAuth" => ["read:accounts"]}],
|
security: [%{"oAuth" => ["read:accounts"]}],
|
||||||
description:
|
description:
|
||||||
"Accounts which the given account is following, if network is not hidden by the account owner.",
|
"Accounts which the given account is following, if network is not hidden by the account owner.",
|
||||||
parameters:
|
parameters: [
|
||||||
[%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}] ++ pagination_params(),
|
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
|
||||||
|
with_relationships_param() | pagination_params()
|
||||||
|
],
|
||||||
responses: %{200 => Operation.response("Accounts", "application/json", array_of_accounts())}
|
responses: %{200 => Operation.response("Accounts", "application/json", array_of_accounts())}
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
@ -367,15 +371,18 @@ defp create_request do
|
||||||
title: "AccountCreateRequest",
|
title: "AccountCreateRequest",
|
||||||
description: "POST body for creating an account",
|
description: "POST body for creating an account",
|
||||||
type: :object,
|
type: :object,
|
||||||
|
required: [:username, :password, :agreement],
|
||||||
properties: %{
|
properties: %{
|
||||||
reason: %Schema{
|
reason: %Schema{
|
||||||
type: :string,
|
type: :string,
|
||||||
|
nullable: true,
|
||||||
description:
|
description:
|
||||||
"Text that will be reviewed by moderators if registrations require manual approval"
|
"Text that will be reviewed by moderators if registrations require manual approval"
|
||||||
},
|
},
|
||||||
username: %Schema{type: :string, description: "The desired username for the account"},
|
username: %Schema{type: :string, description: "The desired username for the account"},
|
||||||
email: %Schema{
|
email: %Schema{
|
||||||
type: :string,
|
type: :string,
|
||||||
|
nullable: true,
|
||||||
description:
|
description:
|
||||||
"The email address to be used for login. Required when `account_activation_required` is enabled.",
|
"The email address to be used for login. Required when `account_activation_required` is enabled.",
|
||||||
format: :email
|
format: :email
|
||||||
|
@ -386,29 +393,39 @@ defp create_request do
|
||||||
format: :password
|
format: :password
|
||||||
},
|
},
|
||||||
agreement: %Schema{
|
agreement: %Schema{
|
||||||
type: :boolean,
|
allOf: [BooleanLike],
|
||||||
description:
|
description:
|
||||||
"Whether the user agrees to the local rules, terms, and policies. These should be presented to the user in order to allow them to consent before setting this parameter to TRUE."
|
"Whether the user agrees to the local rules, terms, and policies. These should be presented to the user in order to allow them to consent before setting this parameter to TRUE."
|
||||||
},
|
},
|
||||||
locale: %Schema{
|
locale: %Schema{
|
||||||
type: :string,
|
type: :string,
|
||||||
|
nullable: true,
|
||||||
description: "The language of the confirmation email that will be sent"
|
description: "The language of the confirmation email that will be sent"
|
||||||
},
|
},
|
||||||
# Pleroma-specific properties:
|
# Pleroma-specific properties:
|
||||||
fullname: %Schema{type: :string, description: "Full name"},
|
fullname: %Schema{type: :string, nullable: true, description: "Full name"},
|
||||||
bio: %Schema{type: :string, description: "Bio", default: ""},
|
bio: %Schema{type: :string, description: "Bio", nullable: true, default: ""},
|
||||||
captcha_solution: %Schema{
|
captcha_solution: %Schema{
|
||||||
type: :string,
|
type: :string,
|
||||||
|
nullable: true,
|
||||||
description: "Provider-specific captcha solution"
|
description: "Provider-specific captcha solution"
|
||||||
},
|
},
|
||||||
captcha_token: %Schema{type: :string, description: "Provider-specific captcha token"},
|
captcha_token: %Schema{
|
||||||
captcha_answer_data: %Schema{type: :string, description: "Provider-specific captcha data"},
|
type: :string,
|
||||||
|
nullable: true,
|
||||||
|
description: "Provider-specific captcha token"
|
||||||
|
},
|
||||||
|
captcha_answer_data: %Schema{
|
||||||
|
type: :string,
|
||||||
|
nullable: true,
|
||||||
|
description: "Provider-specific captcha data"
|
||||||
|
},
|
||||||
token: %Schema{
|
token: %Schema{
|
||||||
type: :string,
|
type: :string,
|
||||||
|
nullable: true,
|
||||||
description: "Invite token required when the registrations aren't public"
|
description: "Invite token required when the registrations aren't public"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
required: [:username, :password, :agreement],
|
|
||||||
example: %{
|
example: %{
|
||||||
"username" => "cofe",
|
"username" => "cofe",
|
||||||
"email" => "cofe@example.com",
|
"email" => "cofe@example.com",
|
||||||
|
@ -446,29 +463,35 @@ defp update_creadentials_request do
|
||||||
type: :object,
|
type: :object,
|
||||||
properties: %{
|
properties: %{
|
||||||
bot: %Schema{
|
bot: %Schema{
|
||||||
type: :boolean,
|
allOf: [BooleanLike],
|
||||||
|
nullable: true,
|
||||||
description: "Whether the account has a bot flag."
|
description: "Whether the account has a bot flag."
|
||||||
},
|
},
|
||||||
display_name: %Schema{
|
display_name: %Schema{
|
||||||
type: :string,
|
type: :string,
|
||||||
|
nullable: true,
|
||||||
description: "The display name to use for the profile."
|
description: "The display name to use for the profile."
|
||||||
},
|
},
|
||||||
note: %Schema{type: :string, description: "The account bio."},
|
note: %Schema{type: :string, description: "The account bio."},
|
||||||
avatar: %Schema{
|
avatar: %Schema{
|
||||||
type: :string,
|
type: :string,
|
||||||
|
nullable: true,
|
||||||
description: "Avatar image encoded using multipart/form-data",
|
description: "Avatar image encoded using multipart/form-data",
|
||||||
format: :binary
|
format: :binary
|
||||||
},
|
},
|
||||||
header: %Schema{
|
header: %Schema{
|
||||||
type: :string,
|
type: :string,
|
||||||
|
nullable: true,
|
||||||
description: "Header image encoded using multipart/form-data",
|
description: "Header image encoded using multipart/form-data",
|
||||||
format: :binary
|
format: :binary
|
||||||
},
|
},
|
||||||
locked: %Schema{
|
locked: %Schema{
|
||||||
type: :boolean,
|
allOf: [BooleanLike],
|
||||||
|
nullable: true,
|
||||||
description: "Whether manual approval of follow requests is required."
|
description: "Whether manual approval of follow requests is required."
|
||||||
},
|
},
|
||||||
fields_attributes: %Schema{
|
fields_attributes: %Schema{
|
||||||
|
nullable: true,
|
||||||
oneOf: [
|
oneOf: [
|
||||||
%Schema{type: :array, items: attribute_field()},
|
%Schema{type: :array, items: attribute_field()},
|
||||||
%Schema{type: :object, additionalProperties: %Schema{type: attribute_field()}}
|
%Schema{type: :object, additionalProperties: %Schema{type: attribute_field()}}
|
||||||
|
@ -487,48 +510,66 @@ defp update_creadentials_request do
|
||||||
|
|
||||||
# Pleroma-specific fields
|
# Pleroma-specific fields
|
||||||
no_rich_text: %Schema{
|
no_rich_text: %Schema{
|
||||||
type: :boolean,
|
allOf: [BooleanLike],
|
||||||
|
nullable: true,
|
||||||
description: "html tags are stripped from all statuses requested from the API"
|
description: "html tags are stripped from all statuses requested from the API"
|
||||||
},
|
},
|
||||||
hide_followers: %Schema{type: :boolean, description: "user's followers will be hidden"},
|
hide_followers: %Schema{
|
||||||
hide_follows: %Schema{type: :boolean, description: "user's follows will be hidden"},
|
allOf: [BooleanLike],
|
||||||
|
nullable: true,
|
||||||
|
description: "user's followers will be hidden"
|
||||||
|
},
|
||||||
|
hide_follows: %Schema{
|
||||||
|
allOf: [BooleanLike],
|
||||||
|
nullable: true,
|
||||||
|
description: "user's follows will be hidden"
|
||||||
|
},
|
||||||
hide_followers_count: %Schema{
|
hide_followers_count: %Schema{
|
||||||
type: :boolean,
|
allOf: [BooleanLike],
|
||||||
|
nullable: true,
|
||||||
description: "user's follower count will be hidden"
|
description: "user's follower count will be hidden"
|
||||||
},
|
},
|
||||||
hide_follows_count: %Schema{
|
hide_follows_count: %Schema{
|
||||||
type: :boolean,
|
allOf: [BooleanLike],
|
||||||
|
nullable: true,
|
||||||
description: "user's follow count will be hidden"
|
description: "user's follow count will be hidden"
|
||||||
},
|
},
|
||||||
hide_favorites: %Schema{
|
hide_favorites: %Schema{
|
||||||
type: :boolean,
|
allOf: [BooleanLike],
|
||||||
|
nullable: true,
|
||||||
description: "user's favorites timeline will be hidden"
|
description: "user's favorites timeline will be hidden"
|
||||||
},
|
},
|
||||||
show_role: %Schema{
|
show_role: %Schema{
|
||||||
type: :boolean,
|
allOf: [BooleanLike],
|
||||||
|
nullable: true,
|
||||||
description: "user's role (e.g admin, moderator) will be exposed to anyone in the
|
description: "user's role (e.g admin, moderator) will be exposed to anyone in the
|
||||||
API"
|
API"
|
||||||
},
|
},
|
||||||
default_scope: VisibilityScope,
|
default_scope: VisibilityScope,
|
||||||
pleroma_settings_store: %Schema{
|
pleroma_settings_store: %Schema{
|
||||||
type: :object,
|
type: :object,
|
||||||
|
nullable: true,
|
||||||
description: "Opaque user settings to be saved on the backend."
|
description: "Opaque user settings to be saved on the backend."
|
||||||
},
|
},
|
||||||
skip_thread_containment: %Schema{
|
skip_thread_containment: %Schema{
|
||||||
type: :boolean,
|
allOf: [BooleanLike],
|
||||||
|
nullable: true,
|
||||||
description: "Skip filtering out broken threads"
|
description: "Skip filtering out broken threads"
|
||||||
},
|
},
|
||||||
allow_following_move: %Schema{
|
allow_following_move: %Schema{
|
||||||
type: :boolean,
|
allOf: [BooleanLike],
|
||||||
|
nullable: true,
|
||||||
description: "Allows automatically follow moved following accounts"
|
description: "Allows automatically follow moved following accounts"
|
||||||
},
|
},
|
||||||
pleroma_background_image: %Schema{
|
pleroma_background_image: %Schema{
|
||||||
type: :string,
|
type: :string,
|
||||||
|
nullable: true,
|
||||||
description: "Sets the background image of the user.",
|
description: "Sets the background image of the user.",
|
||||||
format: :binary
|
format: :binary
|
||||||
},
|
},
|
||||||
discoverable: %Schema{
|
discoverable: %Schema{
|
||||||
type: :boolean,
|
allOf: [BooleanLike],
|
||||||
|
nullable: true,
|
||||||
description:
|
description:
|
||||||
"Discovery of this account in search results and other services is allowed."
|
"Discovery of this account in search results and other services is allowed."
|
||||||
},
|
},
|
||||||
|
@ -624,7 +665,7 @@ defp follow_by_uri_request do
|
||||||
description: "POST body for muting an account",
|
description: "POST body for muting an account",
|
||||||
type: :object,
|
type: :object,
|
||||||
properties: %{
|
properties: %{
|
||||||
uri: %Schema{type: :string, format: :uri}
|
uri: %Schema{type: :string, nullable: true, format: :uri}
|
||||||
},
|
},
|
||||||
required: [:uri]
|
required: [:uri]
|
||||||
}
|
}
|
||||||
|
@ -637,7 +678,8 @@ defp mute_request do
|
||||||
type: :object,
|
type: :object,
|
||||||
properties: %{
|
properties: %{
|
||||||
notifications: %Schema{
|
notifications: %Schema{
|
||||||
type: :boolean,
|
allOf: [BooleanLike],
|
||||||
|
nullable: true,
|
||||||
description: "Mute notifications in addition to statuses? Defaults to true.",
|
description: "Mute notifications in addition to statuses? Defaults to true.",
|
||||||
default: true
|
default: true
|
||||||
}
|
}
|
||||||
|
|
142
lib/pleroma/web/api_spec/operations/admin/config_operation.ex
Normal file
142
lib/pleroma/web/api_spec/operations/admin/config_operation.ex
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ApiSpec.Admin.ConfigOperation do
|
||||||
|
alias OpenApiSpex.Operation
|
||||||
|
alias OpenApiSpex.Schema
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||||
|
|
||||||
|
import Pleroma.Web.ApiSpec.Helpers
|
||||||
|
|
||||||
|
def open_api_operation(action) do
|
||||||
|
operation = String.to_existing_atom("#{action}_operation")
|
||||||
|
apply(__MODULE__, operation, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def show_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Admin", "Config"],
|
||||||
|
summary: "Get list of merged default settings with saved in database",
|
||||||
|
operationId: "AdminAPI.ConfigController.show",
|
||||||
|
parameters: [
|
||||||
|
Operation.parameter(
|
||||||
|
:only_db,
|
||||||
|
:query,
|
||||||
|
%Schema{type: :boolean, default: false},
|
||||||
|
"Get only saved in database settings"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
security: [%{"oAuth" => ["read"]}],
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Config", "application/json", config_response()),
|
||||||
|
400 => Operation.response("Bad Request", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Admin", "Config"],
|
||||||
|
summary: "Update config settings",
|
||||||
|
operationId: "AdminAPI.ConfigController.update",
|
||||||
|
security: [%{"oAuth" => ["write"]}],
|
||||||
|
requestBody:
|
||||||
|
request_body("Parameters", %Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
configs: %Schema{
|
||||||
|
type: :array,
|
||||||
|
items: %Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
group: %Schema{type: :string},
|
||||||
|
key: %Schema{type: :string},
|
||||||
|
value: any(),
|
||||||
|
delete: %Schema{type: :boolean},
|
||||||
|
subkeys: %Schema{type: :array, items: %Schema{type: :string}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Config", "application/json", config_response()),
|
||||||
|
400 => Operation.response("Bad Request", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def descriptions_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Admin", "Config"],
|
||||||
|
summary: "Get JSON with config descriptions.",
|
||||||
|
operationId: "AdminAPI.ConfigController.descriptions",
|
||||||
|
security: [%{"oAuth" => ["read"]}],
|
||||||
|
responses: %{
|
||||||
|
200 =>
|
||||||
|
Operation.response("Config Descriptions", "application/json", %Schema{
|
||||||
|
type: :array,
|
||||||
|
items: %Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
group: %Schema{type: :string},
|
||||||
|
key: %Schema{type: :string},
|
||||||
|
type: %Schema{oneOf: [%Schema{type: :string}, %Schema{type: :array}]},
|
||||||
|
description: %Schema{type: :string},
|
||||||
|
children: %Schema{
|
||||||
|
type: :array,
|
||||||
|
items: %Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
key: %Schema{type: :string},
|
||||||
|
type: %Schema{oneOf: [%Schema{type: :string}, %Schema{type: :array}]},
|
||||||
|
description: %Schema{type: :string},
|
||||||
|
suggestions: %Schema{type: :array}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
400 => Operation.response("Bad Request", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp any do
|
||||||
|
%Schema{
|
||||||
|
oneOf: [
|
||||||
|
%Schema{type: :array},
|
||||||
|
%Schema{type: :object},
|
||||||
|
%Schema{type: :string},
|
||||||
|
%Schema{type: :integer},
|
||||||
|
%Schema{type: :boolean}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp config_response do
|
||||||
|
%Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
configs: %Schema{
|
||||||
|
type: :array,
|
||||||
|
items: %Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
group: %Schema{type: :string},
|
||||||
|
key: %Schema{type: :string},
|
||||||
|
value: any()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
need_reboot: %Schema{
|
||||||
|
type: :boolean,
|
||||||
|
description:
|
||||||
|
"If `need_reboot` is `true`, instance must be restarted, so reboot time settings can take effect"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
148
lib/pleroma/web/api_spec/operations/admin/invite_operation.ex
Normal file
148
lib/pleroma/web/api_spec/operations/admin/invite_operation.ex
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ApiSpec.Admin.InviteOperation do
|
||||||
|
alias OpenApiSpex.Operation
|
||||||
|
alias OpenApiSpex.Schema
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||||
|
|
||||||
|
import Pleroma.Web.ApiSpec.Helpers
|
||||||
|
|
||||||
|
def open_api_operation(action) do
|
||||||
|
operation = String.to_existing_atom("#{action}_operation")
|
||||||
|
apply(__MODULE__, operation, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def index_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Admin", "Invites"],
|
||||||
|
summary: "Get a list of generated invites",
|
||||||
|
operationId: "AdminAPI.InviteController.index",
|
||||||
|
security: [%{"oAuth" => ["read:invites"]}],
|
||||||
|
responses: %{
|
||||||
|
200 =>
|
||||||
|
Operation.response("Invites", "application/json", %Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
invites: %Schema{type: :array, items: invite()}
|
||||||
|
},
|
||||||
|
example: %{
|
||||||
|
"invites" => [
|
||||||
|
%{
|
||||||
|
"id" => 123,
|
||||||
|
"token" => "kSQtDj_GNy2NZsL9AQDFIsHN5qdbguB6qRg3WHw6K1U=",
|
||||||
|
"used" => true,
|
||||||
|
"expires_at" => nil,
|
||||||
|
"uses" => 0,
|
||||||
|
"max_use" => nil,
|
||||||
|
"invite_type" => "one_time"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Admin", "Invites"],
|
||||||
|
summary: "Create an account registration invite token",
|
||||||
|
operationId: "AdminAPI.InviteController.create",
|
||||||
|
security: [%{"oAuth" => ["write:invites"]}],
|
||||||
|
requestBody:
|
||||||
|
request_body("Parameters", %Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
max_use: %Schema{type: :integer},
|
||||||
|
expires_at: %Schema{type: :string, format: :date, example: "2020-04-20"}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Invite", "application/json", invite())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def revoke_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Admin", "Invites"],
|
||||||
|
summary: "Revoke invite by token",
|
||||||
|
operationId: "AdminAPI.InviteController.revoke",
|
||||||
|
security: [%{"oAuth" => ["write:invites"]}],
|
||||||
|
requestBody:
|
||||||
|
request_body(
|
||||||
|
"Parameters",
|
||||||
|
%Schema{
|
||||||
|
type: :object,
|
||||||
|
required: [:token],
|
||||||
|
properties: %{
|
||||||
|
token: %Schema{type: :string}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: true
|
||||||
|
),
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Invite", "application/json", invite()),
|
||||||
|
400 => Operation.response("Bad Request", "application/json", ApiError),
|
||||||
|
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def email_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Admin", "Invites"],
|
||||||
|
summary: "Sends registration invite via email",
|
||||||
|
operationId: "AdminAPI.InviteController.email",
|
||||||
|
security: [%{"oAuth" => ["write:invites"]}],
|
||||||
|
requestBody:
|
||||||
|
request_body(
|
||||||
|
"Parameters",
|
||||||
|
%Schema{
|
||||||
|
type: :object,
|
||||||
|
required: [:email],
|
||||||
|
properties: %{
|
||||||
|
email: %Schema{type: :string, format: :email},
|
||||||
|
name: %Schema{type: :string}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: true
|
||||||
|
),
|
||||||
|
responses: %{
|
||||||
|
204 => no_content_response(),
|
||||||
|
400 => Operation.response("Bad Request", "application/json", ApiError),
|
||||||
|
403 => Operation.response("Forbidden", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp invite do
|
||||||
|
%Schema{
|
||||||
|
title: "Invite",
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
id: %Schema{type: :integer},
|
||||||
|
token: %Schema{type: :string},
|
||||||
|
used: %Schema{type: :boolean},
|
||||||
|
expires_at: %Schema{type: :string, format: :date, nullable: true},
|
||||||
|
uses: %Schema{type: :integer},
|
||||||
|
max_use: %Schema{type: :integer, nullable: true},
|
||||||
|
invite_type: %Schema{
|
||||||
|
type: :string,
|
||||||
|
enum: ["one_time", "reusable", "date_limited", "reusable_date_limited"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
example: %{
|
||||||
|
"id" => 123,
|
||||||
|
"token" => "kSQtDj_GNy2NZsL9AQDFIsHN5qdbguB6qRg3WHw6K1U=",
|
||||||
|
"used" => true,
|
||||||
|
"expires_at" => nil,
|
||||||
|
"uses" => 0,
|
||||||
|
"max_use" => nil,
|
||||||
|
"invite_type" => "one_time"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
215
lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex
Normal file
215
lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex
Normal file
|
@ -0,0 +1,215 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do
|
||||||
|
alias OpenApiSpex.Operation
|
||||||
|
alias OpenApiSpex.Schema
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||||
|
|
||||||
|
import Pleroma.Web.ApiSpec.Helpers
|
||||||
|
|
||||||
|
def open_api_operation(action) do
|
||||||
|
operation = String.to_existing_atom("#{action}_operation")
|
||||||
|
apply(__MODULE__, operation, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def index_operation do
|
||||||
|
%Operation{
|
||||||
|
summary: "List OAuth apps",
|
||||||
|
tags: ["Admin", "oAuth Apps"],
|
||||||
|
operationId: "AdminAPI.OAuthAppController.index",
|
||||||
|
security: [%{"oAuth" => ["write"]}],
|
||||||
|
parameters: [
|
||||||
|
Operation.parameter(:name, :query, %Schema{type: :string}, "App name"),
|
||||||
|
Operation.parameter(:client_id, :query, %Schema{type: :string}, "Client ID"),
|
||||||
|
Operation.parameter(:page, :query, %Schema{type: :integer, default: 1}, "Page"),
|
||||||
|
Operation.parameter(
|
||||||
|
:trusted,
|
||||||
|
:query,
|
||||||
|
%Schema{type: :boolean, default: false},
|
||||||
|
"Trusted apps"
|
||||||
|
),
|
||||||
|
Operation.parameter(
|
||||||
|
:page_size,
|
||||||
|
:query,
|
||||||
|
%Schema{type: :integer, default: 50},
|
||||||
|
"Number of apps to return"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
responses: %{
|
||||||
|
200 =>
|
||||||
|
Operation.response("List of apps", "application/json", %Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
apps: %Schema{type: :array, items: oauth_app()},
|
||||||
|
count: %Schema{type: :integer},
|
||||||
|
page_size: %Schema{type: :integer}
|
||||||
|
},
|
||||||
|
example: %{
|
||||||
|
"apps" => [
|
||||||
|
%{
|
||||||
|
"id" => 1,
|
||||||
|
"name" => "App name",
|
||||||
|
"client_id" => "yHoDSiWYp5mPV6AfsaVOWjdOyt5PhWRiafi6MRd1lSk",
|
||||||
|
"client_secret" => "nLmis486Vqrv2o65eM9mLQx_m_4gH-Q6PcDpGIMl6FY",
|
||||||
|
"redirect_uri" => "https://example.com/oauth-callback",
|
||||||
|
"website" => "https://example.com",
|
||||||
|
"trusted" => true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"count" => 1,
|
||||||
|
"page_size" => 50
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Admin", "oAuth Apps"],
|
||||||
|
summary: "Create OAuth App",
|
||||||
|
operationId: "AdminAPI.OAuthAppController.create",
|
||||||
|
requestBody: request_body("Parameters", create_request()),
|
||||||
|
security: [%{"oAuth" => ["write"]}],
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("App", "application/json", oauth_app()),
|
||||||
|
400 => Operation.response("Bad Request", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Admin", "oAuth Apps"],
|
||||||
|
summary: "Update OAuth App",
|
||||||
|
operationId: "AdminAPI.OAuthAppController.update",
|
||||||
|
parameters: [id_param()],
|
||||||
|
security: [%{"oAuth" => ["write"]}],
|
||||||
|
requestBody: request_body("Parameters", update_request()),
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("App", "application/json", oauth_app()),
|
||||||
|
400 =>
|
||||||
|
Operation.response("Bad Request", "application/json", %Schema{
|
||||||
|
oneOf: [ApiError, %Schema{type: :string}]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Admin", "oAuth Apps"],
|
||||||
|
summary: "Delete OAuth App",
|
||||||
|
operationId: "AdminAPI.OAuthAppController.delete",
|
||||||
|
parameters: [id_param()],
|
||||||
|
security: [%{"oAuth" => ["write"]}],
|
||||||
|
responses: %{
|
||||||
|
204 => no_content_response(),
|
||||||
|
400 => no_content_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create_request do
|
||||||
|
%Schema{
|
||||||
|
title: "oAuthAppCreateRequest",
|
||||||
|
type: :object,
|
||||||
|
required: [:name, :redirect_uris],
|
||||||
|
properties: %{
|
||||||
|
name: %Schema{type: :string, description: "Application Name"},
|
||||||
|
scopes: %Schema{type: :array, items: %Schema{type: :string}, description: "oAuth scopes"},
|
||||||
|
redirect_uris: %Schema{
|
||||||
|
type: :string,
|
||||||
|
description:
|
||||||
|
"Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter."
|
||||||
|
},
|
||||||
|
website: %Schema{
|
||||||
|
type: :string,
|
||||||
|
nullable: true,
|
||||||
|
description: "A URL to the homepage of the app"
|
||||||
|
},
|
||||||
|
trusted: %Schema{
|
||||||
|
type: :boolean,
|
||||||
|
nullable: true,
|
||||||
|
default: false,
|
||||||
|
description: "Is the app trusted?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
example: %{
|
||||||
|
"name" => "My App",
|
||||||
|
"redirect_uris" => "https://myapp.com/auth/callback",
|
||||||
|
"website" => "https://myapp.com/",
|
||||||
|
"scopes" => ["read", "write"],
|
||||||
|
"trusted" => true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp update_request do
|
||||||
|
%Schema{
|
||||||
|
title: "oAuthAppUpdateRequest",
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
name: %Schema{type: :string, description: "Application Name"},
|
||||||
|
scopes: %Schema{type: :array, items: %Schema{type: :string}, description: "oAuth scopes"},
|
||||||
|
redirect_uris: %Schema{
|
||||||
|
type: :string,
|
||||||
|
description:
|
||||||
|
"Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter."
|
||||||
|
},
|
||||||
|
website: %Schema{
|
||||||
|
type: :string,
|
||||||
|
nullable: true,
|
||||||
|
description: "A URL to the homepage of the app"
|
||||||
|
},
|
||||||
|
trusted: %Schema{
|
||||||
|
type: :boolean,
|
||||||
|
nullable: true,
|
||||||
|
default: false,
|
||||||
|
description: "Is the app trusted?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
example: %{
|
||||||
|
"name" => "My App",
|
||||||
|
"redirect_uris" => "https://myapp.com/auth/callback",
|
||||||
|
"website" => "https://myapp.com/",
|
||||||
|
"scopes" => ["read", "write"],
|
||||||
|
"trusted" => true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp oauth_app do
|
||||||
|
%Schema{
|
||||||
|
title: "oAuthApp",
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
id: %Schema{type: :integer},
|
||||||
|
name: %Schema{type: :string},
|
||||||
|
client_id: %Schema{type: :string},
|
||||||
|
client_secret: %Schema{type: :string},
|
||||||
|
redirect_uri: %Schema{type: :string},
|
||||||
|
website: %Schema{type: :string, nullable: true},
|
||||||
|
trusted: %Schema{type: :boolean}
|
||||||
|
},
|
||||||
|
example: %{
|
||||||
|
"id" => 123,
|
||||||
|
"name" => "My App",
|
||||||
|
"client_id" => "TWhM-tNSuncnqN7DBJmoyeLnk6K3iJJ71KKXxgL1hPM",
|
||||||
|
"client_secret" => "ZEaFUFmF0umgBX1qKJDjaU99Q31lDkOU8NutzTOoliw",
|
||||||
|
"redirect_uri" => "https://myapp.com/oauth-callback",
|
||||||
|
"website" => "https://myapp.com/",
|
||||||
|
"trusted" => false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def id_param do
|
||||||
|
Operation.parameter(:id, :path, :integer, "App ID",
|
||||||
|
example: 1337,
|
||||||
|
required: true
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
237
lib/pleroma/web/api_spec/operations/admin/report_operation.ex
Normal file
237
lib/pleroma/web/api_spec/operations/admin/report_operation.ex
Normal 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
|
165
lib/pleroma/web/api_spec/operations/admin/status_operation.ex
Normal file
165
lib/pleroma/web/api_spec/operations/admin/status_operation.ex
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
# 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.StatusOperation 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
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
|
||||||
|
|
||||||
|
import Pleroma.Web.ApiSpec.Helpers
|
||||||
|
import Pleroma.Web.ApiSpec.StatusOperation, only: [id_param: 0]
|
||||||
|
|
||||||
|
def open_api_operation(action) do
|
||||||
|
operation = String.to_existing_atom("#{action}_operation")
|
||||||
|
apply(__MODULE__, operation, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def index_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Admin", "Statuses"],
|
||||||
|
operationId: "AdminAPI.StatusController.index",
|
||||||
|
security: [%{"oAuth" => ["read:statuses"]}],
|
||||||
|
parameters: [
|
||||||
|
Operation.parameter(
|
||||||
|
:godmode,
|
||||||
|
:query,
|
||||||
|
%Schema{type: :boolean, default: false},
|
||||||
|
"Allows to see private statuses"
|
||||||
|
),
|
||||||
|
Operation.parameter(
|
||||||
|
:local_only,
|
||||||
|
:query,
|
||||||
|
%Schema{type: :boolean, default: false},
|
||||||
|
"Excludes remote statuses"
|
||||||
|
),
|
||||||
|
Operation.parameter(
|
||||||
|
:with_reblogs,
|
||||||
|
:query,
|
||||||
|
%Schema{type: :boolean, default: false},
|
||||||
|
"Allows to see reblogs"
|
||||||
|
),
|
||||||
|
Operation.parameter(
|
||||||
|
:page,
|
||||||
|
:query,
|
||||||
|
%Schema{type: :integer, default: 1},
|
||||||
|
"Page"
|
||||||
|
),
|
||||||
|
Operation.parameter(
|
||||||
|
:page_size,
|
||||||
|
:query,
|
||||||
|
%Schema{type: :integer, default: 50},
|
||||||
|
"Number of statuses to return"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
responses: %{
|
||||||
|
200 =>
|
||||||
|
Operation.response("Array of statuses", "application/json", %Schema{
|
||||||
|
type: :array,
|
||||||
|
items: status()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def show_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Admin", "Statuses"],
|
||||||
|
summary: "Show Status",
|
||||||
|
operationId: "AdminAPI.StatusController.show",
|
||||||
|
parameters: [id_param()],
|
||||||
|
security: [%{"oAuth" => ["read:statuses"]}],
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Status", "application/json", status()),
|
||||||
|
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Admin", "Statuses"],
|
||||||
|
summary: "Change the scope of an individual reported status",
|
||||||
|
operationId: "AdminAPI.StatusController.update",
|
||||||
|
parameters: [id_param()],
|
||||||
|
security: [%{"oAuth" => ["write:statuses"]}],
|
||||||
|
requestBody: request_body("Parameters", update_request(), required: true),
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Status", "application/json", Status),
|
||||||
|
400 => Operation.response("Error", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Admin", "Statuses"],
|
||||||
|
summary: "Delete an individual reported status",
|
||||||
|
operationId: "AdminAPI.StatusController.delete",
|
||||||
|
parameters: [id_param()],
|
||||||
|
security: [%{"oAuth" => ["write:statuses"]}],
|
||||||
|
responses: %{
|
||||||
|
200 => empty_object_response(),
|
||||||
|
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp status do
|
||||||
|
%Schema{
|
||||||
|
anyOf: [
|
||||||
|
Status,
|
||||||
|
%Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
account: %Schema{allOf: [Account, admin_account()]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def admin_account do
|
||||||
|
%Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
id: FlakeID,
|
||||||
|
avatar: %Schema{type: :string},
|
||||||
|
nickname: %Schema{type: :string},
|
||||||
|
display_name: %Schema{type: :string},
|
||||||
|
deactivated: %Schema{type: :boolean},
|
||||||
|
local: %Schema{type: :boolean},
|
||||||
|
roles: %Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
admin: %Schema{type: :boolean},
|
||||||
|
moderator: %Schema{type: :boolean}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tags: %Schema{type: :string},
|
||||||
|
confirmation_pending: %Schema{type: :string}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp update_request do
|
||||||
|
%Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
sensitive: %Schema{
|
||||||
|
type: :boolean,
|
||||||
|
description: "Mark status and attached media as sensitive?"
|
||||||
|
},
|
||||||
|
visibility: VisibilityScope
|
||||||
|
},
|
||||||
|
example: %{
|
||||||
|
"visibility" => "private",
|
||||||
|
"sensitive" => "false"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
|
@ -105,7 +105,11 @@ defp create_request do
|
||||||
description: "Space separated list of scopes",
|
description: "Space separated list of scopes",
|
||||||
default: "read"
|
default: "read"
|
||||||
},
|
},
|
||||||
website: %Schema{type: :string, description: "A URL to the homepage of your app"}
|
website: %Schema{
|
||||||
|
type: :string,
|
||||||
|
nullable: true,
|
||||||
|
description: "A URL to the homepage of your app"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: [:client_name, :redirect_uris],
|
required: [:client_name, :redirect_uris],
|
||||||
example: %{
|
example: %{
|
||||||
|
|
104
lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex
Normal file
104
lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
# 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.EmojiReactionOperation 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
|
||||||
|
|
||||||
|
def open_api_operation(action) do
|
||||||
|
operation = String.to_existing_atom("#{action}_operation")
|
||||||
|
apply(__MODULE__, operation, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def index_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Emoji Reactions"],
|
||||||
|
summary:
|
||||||
|
"Get an object of emoji to account mappings with accounts that reacted to the post",
|
||||||
|
parameters: [
|
||||||
|
Operation.parameter(:id, :path, FlakeID, "Status ID", required: true),
|
||||||
|
Operation.parameter(:emoji, :path, :string, "Filter by a single unicode emoji",
|
||||||
|
required: false
|
||||||
|
)
|
||||||
|
],
|
||||||
|
security: [%{"oAuth" => ["read:statuses"]}],
|
||||||
|
operationId: "EmojiReactionController.index",
|
||||||
|
responses: %{
|
||||||
|
200 => array_of_reactions_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Emoji Reactions"],
|
||||||
|
summary: "React to a post with a unicode emoji",
|
||||||
|
parameters: [
|
||||||
|
Operation.parameter(:id, :path, FlakeID, "Status ID", required: true),
|
||||||
|
Operation.parameter(:emoji, :path, :string, "A single character unicode emoji",
|
||||||
|
required: true
|
||||||
|
)
|
||||||
|
],
|
||||||
|
security: [%{"oAuth" => ["write:statuses"]}],
|
||||||
|
operationId: "EmojiReactionController.create",
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Status", "application/json", Status),
|
||||||
|
400 => Operation.response("Bad Request", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Emoji Reactions"],
|
||||||
|
summary: "Remove a reaction to a post with a unicode emoji",
|
||||||
|
parameters: [
|
||||||
|
Operation.parameter(:id, :path, FlakeID, "Status ID", required: true),
|
||||||
|
Operation.parameter(:emoji, :path, :string, "A single character unicode emoji",
|
||||||
|
required: true
|
||||||
|
)
|
||||||
|
],
|
||||||
|
security: [%{"oAuth" => ["write:statuses"]}],
|
||||||
|
operationId: "EmojiReactionController.delete",
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Status", "application/json", Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp array_of_reactions_response do
|
||||||
|
Operation.response("Array of Emoji Reactions", "application/json", %Schema{
|
||||||
|
type: :array,
|
||||||
|
items: emoji_reaction(),
|
||||||
|
example: [emoji_reaction().example]
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp emoji_reaction do
|
||||||
|
%Schema{
|
||||||
|
title: "EmojiReaction",
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
name: %Schema{type: :string, description: "Emoji"},
|
||||||
|
count: %Schema{type: :integer, description: "Count of reactions with this emoji"},
|
||||||
|
me: %Schema{type: :boolean, description: "Did I react with this emoji?"},
|
||||||
|
accounts: %Schema{
|
||||||
|
type: :array,
|
||||||
|
items: Account,
|
||||||
|
description: "Array of accounts reacted with this emoji"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
example: %{
|
||||||
|
"name" => "😱",
|
||||||
|
"count" => 1,
|
||||||
|
"me" => false,
|
||||||
|
"accounts" => [Account.schema().example]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
|
@ -6,6 +6,7 @@ defmodule Pleroma.Web.ApiSpec.FilterOperation do
|
||||||
alias OpenApiSpex.Operation
|
alias OpenApiSpex.Operation
|
||||||
alias OpenApiSpex.Schema
|
alias OpenApiSpex.Schema
|
||||||
alias Pleroma.Web.ApiSpec.Helpers
|
alias Pleroma.Web.ApiSpec.Helpers
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
|
||||||
|
|
||||||
def open_api_operation(action) do
|
def open_api_operation(action) do
|
||||||
operation = String.to_existing_atom("#{action}_operation")
|
operation = String.to_existing_atom("#{action}_operation")
|
||||||
|
@ -171,7 +172,7 @@ defp create_request do
|
||||||
type: :object,
|
type: :object,
|
||||||
properties: %{
|
properties: %{
|
||||||
irreversible: %Schema{
|
irreversible: %Schema{
|
||||||
type: :bolean,
|
allOf: [BooleanLike],
|
||||||
description:
|
description:
|
||||||
"Should the server irreversibly drop matching entities from home and notifications?",
|
"Should the server irreversibly drop matching entities from home and notifications?",
|
||||||
default: false
|
default: false
|
||||||
|
@ -199,12 +200,14 @@ defp update_request do
|
||||||
"Array of enumerable strings `home`, `notifications`, `public`, `thread`. At least one context must be specified."
|
"Array of enumerable strings `home`, `notifications`, `public`, `thread`. At least one context must be specified."
|
||||||
},
|
},
|
||||||
irreversible: %Schema{
|
irreversible: %Schema{
|
||||||
type: :bolean,
|
allOf: [BooleanLike],
|
||||||
|
nullable: true,
|
||||||
description:
|
description:
|
||||||
"Should the server irreversibly drop matching entities from home and notifications?"
|
"Should the server irreversibly drop matching entities from home and notifications?"
|
||||||
},
|
},
|
||||||
whole_word: %Schema{
|
whole_word: %Schema{
|
||||||
type: :bolean,
|
allOf: [BooleanLike],
|
||||||
|
nullable: true,
|
||||||
description: "Consider word boundaries?",
|
description: "Consider word boundaries?",
|
||||||
default: true
|
default: true
|
||||||
}
|
}
|
||||||
|
|
|
@ -125,13 +125,19 @@ defp instance do
|
||||||
},
|
},
|
||||||
avatar_upload_limit: %Schema{type: :integer, description: "The title of the website"},
|
avatar_upload_limit: %Schema{type: :integer, description: "The title of the website"},
|
||||||
background_upload_limit: %Schema{type: :integer, description: "The title of the website"},
|
background_upload_limit: %Schema{type: :integer, description: "The title of the website"},
|
||||||
banner_upload_limit: %Schema{type: :integer, description: "The title of the website"}
|
banner_upload_limit: %Schema{type: :integer, description: "The title of the website"},
|
||||||
|
background_image: %Schema{
|
||||||
|
type: :string,
|
||||||
|
format: :uri,
|
||||||
|
description: "The background image for the website"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
example: %{
|
example: %{
|
||||||
"avatar_upload_limit" => 2_000_000,
|
"avatar_upload_limit" => 2_000_000,
|
||||||
"background_upload_limit" => 4_000_000,
|
"background_upload_limit" => 4_000_000,
|
||||||
|
"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,
|
||||||
|
|
|
@ -110,14 +110,16 @@ defp upsert_request do
|
||||||
properties: %{
|
properties: %{
|
||||||
notifications: %Schema{
|
notifications: %Schema{
|
||||||
type: :object,
|
type: :object,
|
||||||
|
nullable: true,
|
||||||
properties: %{
|
properties: %{
|
||||||
last_read_id: %Schema{type: :string}
|
last_read_id: %Schema{nullable: true, type: :string}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
home: %Schema{
|
home: %Schema{
|
||||||
type: :object,
|
type: :object,
|
||||||
|
nullable: true,
|
||||||
properties: %{
|
properties: %{
|
||||||
last_read_id: %Schema{type: :string}
|
last_read_id: %Schema{nullable: true, type: :string}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
132
lib/pleroma/web/api_spec/operations/media_operation.ex
Normal file
132
lib/pleroma/web/api_spec/operations/media_operation.ex
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
# 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.MediaOperation do
|
||||||
|
alias OpenApiSpex.Operation
|
||||||
|
alias OpenApiSpex.Schema
|
||||||
|
alias Pleroma.Web.ApiSpec.Helpers
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.Attachment
|
||||||
|
|
||||||
|
def open_api_operation(action) do
|
||||||
|
operation = String.to_existing_atom("#{action}_operation")
|
||||||
|
apply(__MODULE__, operation, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["media"],
|
||||||
|
summary: "Upload media as attachment",
|
||||||
|
description: "Creates an attachment to be used with a new status.",
|
||||||
|
operationId: "MediaController.create",
|
||||||
|
security: [%{"oAuth" => ["write:media"]}],
|
||||||
|
requestBody: Helpers.request_body("Parameters", create_request()),
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Media", "application/json", Attachment),
|
||||||
|
401 => Operation.response("Media", "application/json", ApiError),
|
||||||
|
422 => Operation.response("Media", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create_request do
|
||||||
|
%Schema{
|
||||||
|
title: "MediaCreateRequest",
|
||||||
|
description: "POST body for creating an attachment",
|
||||||
|
type: :object,
|
||||||
|
required: [:file],
|
||||||
|
properties: %{
|
||||||
|
file: %Schema{
|
||||||
|
type: :string,
|
||||||
|
format: :binary,
|
||||||
|
description: "The file to be attached, using multipart form data."
|
||||||
|
},
|
||||||
|
description: %Schema{
|
||||||
|
type: :string,
|
||||||
|
description: "A plain-text description of the media, for accessibility purposes."
|
||||||
|
},
|
||||||
|
focus: %Schema{
|
||||||
|
type: :string,
|
||||||
|
description: "Two floating points (x,y), comma-delimited, ranging from -1.0 to 1.0."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["media"],
|
||||||
|
summary: "Upload media as attachment",
|
||||||
|
description: "Creates an attachment to be used with a new status.",
|
||||||
|
operationId: "MediaController.update",
|
||||||
|
security: [%{"oAuth" => ["write:media"]}],
|
||||||
|
parameters: [id_param()],
|
||||||
|
requestBody: Helpers.request_body("Parameters", update_request()),
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Media", "application/json", Attachment),
|
||||||
|
400 => Operation.response("Media", "application/json", ApiError),
|
||||||
|
401 => Operation.response("Media", "application/json", ApiError),
|
||||||
|
422 => Operation.response("Media", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp update_request do
|
||||||
|
%Schema{
|
||||||
|
title: "MediaUpdateRequest",
|
||||||
|
description: "POST body for updating an attachment",
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
file: %Schema{
|
||||||
|
type: :string,
|
||||||
|
format: :binary,
|
||||||
|
description: "The file to be attached, using multipart form data."
|
||||||
|
},
|
||||||
|
description: %Schema{
|
||||||
|
type: :string,
|
||||||
|
description: "A plain-text description of the media, for accessibility purposes."
|
||||||
|
},
|
||||||
|
focus: %Schema{
|
||||||
|
type: :string,
|
||||||
|
description: "Two floating points (x,y), comma-delimited, ranging from -1.0 to 1.0."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def show_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["media"],
|
||||||
|
summary: "Show Uploaded media attachment",
|
||||||
|
operationId: "MediaController.show",
|
||||||
|
parameters: [id_param()],
|
||||||
|
security: [%{"oAuth" => ["read:media"]}],
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Media", "application/json", Attachment),
|
||||||
|
401 => Operation.response("Media", "application/json", ApiError),
|
||||||
|
422 => Operation.response("Media", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def create2_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["media"],
|
||||||
|
summary: "Upload media as attachment",
|
||||||
|
description: "Creates an attachment to be used with a new status.",
|
||||||
|
operationId: "MediaController.create2",
|
||||||
|
security: [%{"oAuth" => ["write:media"]}],
|
||||||
|
requestBody: Helpers.request_body("Parameters", create_request()),
|
||||||
|
responses: %{
|
||||||
|
202 => Operation.response("Media", "application/json", Attachment),
|
||||||
|
422 => Operation.response("Media", "application/json", ApiError),
|
||||||
|
500 => Operation.response("Media", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp id_param do
|
||||||
|
Operation.parameter(:id, :path, :string, "The ID of the Attachment entity")
|
||||||
|
end
|
||||||
|
end
|
|
@ -145,7 +145,7 @@ def destroy_multiple_operation do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp notification do
|
def notification do
|
||||||
%Schema{
|
%Schema{
|
||||||
title: "Notification",
|
title: "Notification",
|
||||||
description: "Response schema for a notification",
|
description: "Response schema for a notification",
|
||||||
|
|
187
lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex
Normal file
187
lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex
Normal file
|
@ -0,0 +1,187 @@
|
||||||
|
# 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.PleromaAccountOperation do
|
||||||
|
alias OpenApiSpex.Operation
|
||||||
|
alias OpenApiSpex.Schema
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
||||||
|
alias Pleroma.Web.ApiSpec.StatusOperation
|
||||||
|
|
||||||
|
import Pleroma.Web.ApiSpec.Helpers
|
||||||
|
|
||||||
|
def open_api_operation(action) do
|
||||||
|
operation = String.to_existing_atom("#{action}_operation")
|
||||||
|
apply(__MODULE__, operation, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def confirmation_resend_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Accounts"],
|
||||||
|
summary: "Resend confirmation email. Expects `email` or `nickname`",
|
||||||
|
operationId: "PleromaAPI.AccountController.confirmation_resend",
|
||||||
|
parameters: [
|
||||||
|
Operation.parameter(:email, :query, :string, "Email of that needs to be verified",
|
||||||
|
example: "cofe@cofe.io"
|
||||||
|
),
|
||||||
|
Operation.parameter(
|
||||||
|
:nickname,
|
||||||
|
:query,
|
||||||
|
:string,
|
||||||
|
"Nickname of user that needs to be verified",
|
||||||
|
example: "cofefe"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
responses: %{
|
||||||
|
204 => no_content_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_avatar_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Accounts"],
|
||||||
|
summary: "Set/clear user avatar image",
|
||||||
|
operationId: "PleromaAPI.AccountController.update_avatar",
|
||||||
|
requestBody:
|
||||||
|
request_body("Parameters", update_avatar_or_background_request(), required: true),
|
||||||
|
security: [%{"oAuth" => ["write:accounts"]}],
|
||||||
|
responses: %{
|
||||||
|
200 => update_response(),
|
||||||
|
403 => Operation.response("Forbidden", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_banner_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Accounts"],
|
||||||
|
summary: "Set/clear user banner image",
|
||||||
|
operationId: "PleromaAPI.AccountController.update_banner",
|
||||||
|
requestBody: request_body("Parameters", update_banner_request(), required: true),
|
||||||
|
security: [%{"oAuth" => ["write:accounts"]}],
|
||||||
|
responses: %{
|
||||||
|
200 => update_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_background_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Accounts"],
|
||||||
|
summary: "Set/clear user background image",
|
||||||
|
operationId: "PleromaAPI.AccountController.update_background",
|
||||||
|
security: [%{"oAuth" => ["write:accounts"]}],
|
||||||
|
requestBody:
|
||||||
|
request_body("Parameters", update_avatar_or_background_request(), required: true),
|
||||||
|
responses: %{
|
||||||
|
200 => update_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def favourites_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Accounts"],
|
||||||
|
summary: "Returns favorites timeline of any user",
|
||||||
|
operationId: "PleromaAPI.AccountController.favourites",
|
||||||
|
parameters: [id_param() | pagination_params()],
|
||||||
|
security: [%{"oAuth" => ["read:favourites"]}],
|
||||||
|
responses: %{
|
||||||
|
200 =>
|
||||||
|
Operation.response(
|
||||||
|
"Array of Statuses",
|
||||||
|
"application/json",
|
||||||
|
StatusOperation.array_of_statuses()
|
||||||
|
),
|
||||||
|
403 => Operation.response("Forbidden", "application/json", ApiError),
|
||||||
|
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def subscribe_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Accounts"],
|
||||||
|
summary: "Subscribe to receive notifications for all statuses posted by a user",
|
||||||
|
operationId: "PleromaAPI.AccountController.subscribe",
|
||||||
|
parameters: [id_param()],
|
||||||
|
security: [%{"oAuth" => ["follow", "write:follows"]}],
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Relationship", "application/json", AccountRelationship),
|
||||||
|
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def unsubscribe_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Accounts"],
|
||||||
|
summary: "Unsubscribe to stop receiving notifications from user statuses",
|
||||||
|
operationId: "PleromaAPI.AccountController.unsubscribe",
|
||||||
|
parameters: [id_param()],
|
||||||
|
security: [%{"oAuth" => ["follow", "write:follows"]}],
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Relationship", "application/json", AccountRelationship),
|
||||||
|
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp id_param do
|
||||||
|
Operation.parameter(:id, :path, FlakeID, "Account ID",
|
||||||
|
example: "9umDrYheeY451cQnEe",
|
||||||
|
required: true
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp update_avatar_or_background_request do
|
||||||
|
%Schema{
|
||||||
|
title: "PleromaAccountUpdateAvatarOrBackgroundRequest",
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
img: %Schema{
|
||||||
|
nullable: true,
|
||||||
|
type: :string,
|
||||||
|
format: :binary,
|
||||||
|
description: "Image encoded using `multipart/form-data` or an empty string to clear"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp update_banner_request do
|
||||||
|
%Schema{
|
||||||
|
title: "PleromaAccountUpdateBannerRequest",
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
banner: %Schema{
|
||||||
|
type: :string,
|
||||||
|
nullable: true,
|
||||||
|
format: :binary,
|
||||||
|
description: "Image encoded using `multipart/form-data` or an empty string to clear"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp update_response do
|
||||||
|
Operation.response("PleromaAccountUpdateResponse", "application/json", %Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
url: %Schema{
|
||||||
|
type: :string,
|
||||||
|
format: :uri,
|
||||||
|
nullable: true,
|
||||||
|
description: "Image URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
example: %{
|
||||||
|
"url" =>
|
||||||
|
"https://cofe.party/media/9d0add56-bcb6-4c0f-8225-cbbd0b6dd773/13eadb6972c9ccd3f4ffa3b8196f0e0d38b4d2f27594457c52e52946c054cd9a.gif"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,106 @@
|
||||||
|
# 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.PleromaConversationOperation do
|
||||||
|
alias OpenApiSpex.Operation
|
||||||
|
alias OpenApiSpex.Schema
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.Conversation
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
||||||
|
alias Pleroma.Web.ApiSpec.StatusOperation
|
||||||
|
|
||||||
|
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: ["Conversations"],
|
||||||
|
summary: "The conversation with the given ID",
|
||||||
|
parameters: [
|
||||||
|
Operation.parameter(:id, :path, :string, "Conversation ID",
|
||||||
|
example: "123",
|
||||||
|
required: true
|
||||||
|
)
|
||||||
|
],
|
||||||
|
security: [%{"oAuth" => ["read:statuses"]}],
|
||||||
|
operationId: "PleromaAPI.ConversationController.show",
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Conversation", "application/json", Conversation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def statuses_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Conversations"],
|
||||||
|
summary: "Timeline for a given conversation",
|
||||||
|
parameters: [
|
||||||
|
Operation.parameter(:id, :path, :string, "Conversation ID",
|
||||||
|
example: "123",
|
||||||
|
required: true
|
||||||
|
)
|
||||||
|
| pagination_params()
|
||||||
|
],
|
||||||
|
security: [%{"oAuth" => ["read:statuses"]}],
|
||||||
|
operationId: "PleromaAPI.ConversationController.statuses",
|
||||||
|
responses: %{
|
||||||
|
200 =>
|
||||||
|
Operation.response(
|
||||||
|
"Array of Statuses",
|
||||||
|
"application/json",
|
||||||
|
StatusOperation.array_of_statuses()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Conversations"],
|
||||||
|
summary: "Update a conversation. Used to change the set of recipients.",
|
||||||
|
parameters: [
|
||||||
|
Operation.parameter(:id, :path, :string, "Conversation ID",
|
||||||
|
example: "123",
|
||||||
|
required: true
|
||||||
|
),
|
||||||
|
Operation.parameter(
|
||||||
|
:recipients,
|
||||||
|
:query,
|
||||||
|
%Schema{type: :array, items: FlakeID},
|
||||||
|
"A list of ids of users that should receive posts to this conversation. This will replace the current list of recipients, so submit the full list. The owner of owner of the conversation will always be part of the set of recipients, though.",
|
||||||
|
required: true
|
||||||
|
)
|
||||||
|
],
|
||||||
|
security: [%{"oAuth" => ["write:conversations"]}],
|
||||||
|
operationId: "PleromaAPI.ConversationController.update",
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Conversation", "application/json", Conversation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def mark_as_read_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Conversations"],
|
||||||
|
summary: "Marks all user's conversations as read",
|
||||||
|
security: [%{"oAuth" => ["write:conversations"]}],
|
||||||
|
operationId: "PleromaAPI.ConversationController.mark_as_read",
|
||||||
|
responses: %{
|
||||||
|
200 =>
|
||||||
|
Operation.response(
|
||||||
|
"Array of Conversations that were marked as read",
|
||||||
|
"application/json",
|
||||||
|
%Schema{
|
||||||
|
type: :array,
|
||||||
|
items: Conversation,
|
||||||
|
example: [Conversation.schema().example]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue