aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/mix/tasks/pleroma/benchmark.ex3
-rw-r--r--lib/mix/tasks/pleroma/digest.ex7
-rw-r--r--lib/mix/tasks/pleroma/instance.ex8
-rw-r--r--lib/mix/tasks/pleroma/user.ex27
-rw-r--r--lib/pleroma/activity/queries.ex5
-rw-r--r--lib/pleroma/application.ex11
-rw-r--r--lib/pleroma/bbs/authenticator.ex4
-rw-r--r--lib/pleroma/bbs/handler.ex4
-rw-r--r--lib/pleroma/config/config_db.ex11
-rw-r--r--lib/pleroma/constants.ex3
-rw-r--r--lib/pleroma/conversation.ex2
-rw-r--r--lib/pleroma/conversation/participation.ex4
-rw-r--r--lib/pleroma/docs/json.ex1
-rw-r--r--lib/pleroma/emails/new_users_digest_email.ex6
-rw-r--r--lib/pleroma/emoji/pack.ex688
-rw-r--r--lib/pleroma/filter.ex9
-rw-r--r--lib/pleroma/healthcheck.ex2
-rw-r--r--lib/pleroma/helpers/uri_helper.ex3
-rw-r--r--lib/pleroma/marker.ex45
-rw-r--r--lib/pleroma/mfa.ex155
-rw-r--r--lib/pleroma/mfa/backup_codes.ex31
-rw-r--r--lib/pleroma/mfa/changeset.ex64
-rw-r--r--lib/pleroma/mfa/settings.ex24
-rw-r--r--lib/pleroma/mfa/token.ex106
-rw-r--r--lib/pleroma/mfa/totp.ex86
-rw-r--r--lib/pleroma/notification.ex102
-rw-r--r--lib/pleroma/object.ex49
-rw-r--r--lib/pleroma/plugs/authentication_plug.ex33
-rw-r--r--lib/pleroma/plugs/ensure_authenticated_plug.ex29
-rw-r--r--lib/pleroma/plugs/federating_plug.ex3
-rw-r--r--lib/pleroma/scheduled_activity.ex2
-rw-r--r--lib/pleroma/stats.ex2
-rw-r--r--lib/pleroma/upload.ex2
-rw-r--r--lib/pleroma/user.ex145
-rw-r--r--lib/pleroma/user/query.ex35
-rw-r--r--lib/pleroma/user/welcome_message.ex4
-rw-r--r--lib/pleroma/user_relationship.ex43
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub.ex295
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub_controller.ex19
-rw-r--r--lib/pleroma/web/activity_pub/builder.ex99
-rw-r--r--lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex97
-rw-r--r--lib/pleroma/web/activity_pub/object_validator.ex61
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/announce_validator.ex101
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/common_validations.ex62
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/delete_validator.ex100
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex81
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/like_validator.ex48
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/types/recipients.ex34
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/undo_validator.ex62
-rw-r--r--lib/pleroma/web/activity_pub/pipeline.ex28
-rw-r--r--lib/pleroma/web/activity_pub/relay.ex9
-rw-r--r--lib/pleroma/web/activity_pub/side_effects.ex129
-rw-r--r--lib/pleroma/web/activity_pub/transmogrifier.ex247
-rw-r--r--lib/pleroma/web/activity_pub/utils.ex59
-rw-r--r--lib/pleroma/web/admin_api/controllers/admin_api_controller.ex (renamed from lib/pleroma/web/admin_api/admin_api_controller.ex)224
-rw-r--r--lib/pleroma/web/admin_api/controllers/fallback_controller.ex31
-rw-r--r--lib/pleroma/web/admin_api/controllers/status_controller.ex79
-rw-r--r--lib/pleroma/web/admin_api/search.ex1
-rw-r--r--lib/pleroma/web/admin_api/views/account_view.ex9
-rw-r--r--lib/pleroma/web/admin_api/views/report_view.ex13
-rw-r--r--lib/pleroma/web/admin_api/views/status_view.ex17
-rw-r--r--lib/pleroma/web/api_spec.ex7
-rw-r--r--lib/pleroma/web/api_spec/cast_and_validate.ex139
-rw-r--r--lib/pleroma/web/api_spec/helpers.ex14
-rw-r--r--lib/pleroma/web/api_spec/operations/account_operation.ex114
-rw-r--r--lib/pleroma/web/api_spec/operations/admin/status_operation.ex165
-rw-r--r--lib/pleroma/web/api_spec/operations/app_operation.ex6
-rw-r--r--lib/pleroma/web/api_spec/operations/conversation_operation.ex61
-rw-r--r--lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex104
-rw-r--r--lib/pleroma/web/api_spec/operations/filter_operation.ex230
-rw-r--r--lib/pleroma/web/api_spec/operations/follow_request_operation.ex65
-rw-r--r--lib/pleroma/web/api_spec/operations/instance_operation.ex175
-rw-r--r--lib/pleroma/web/api_spec/operations/list_operation.ex188
-rw-r--r--lib/pleroma/web/api_spec/operations/marker_operation.ex142
-rw-r--r--lib/pleroma/web/api_spec/operations/media_operation.ex132
-rw-r--r--lib/pleroma/web/api_spec/operations/notification_operation.ex2
-rw-r--r--lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex187
-rw-r--r--lib/pleroma/web/api_spec/operations/pleroma_conversation_operation.ex106
-rw-r--r--lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex390
-rw-r--r--lib/pleroma/web/api_spec/operations/pleroma_mascot_operation.ex79
-rw-r--r--lib/pleroma/web/api_spec/operations/pleroma_notification_operation.ex48
-rw-r--r--lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex102
-rw-r--r--lib/pleroma/web/api_spec/operations/poll_operation.ex76
-rw-r--r--lib/pleroma/web/api_spec/operations/report_operation.ex6
-rw-r--r--lib/pleroma/web/api_spec/operations/scheduled_activity_operation.ex96
-rw-r--r--lib/pleroma/web/api_spec/operations/search_operation.ex208
-rw-r--r--lib/pleroma/web/api_spec/operations/status_operation.ex518
-rw-r--r--lib/pleroma/web/api_spec/operations/subscription_operation.ex227
-rw-r--r--lib/pleroma/web/api_spec/operations/timeline_operation.ex191
-rw-r--r--lib/pleroma/web/api_spec/render_error.ex3
-rw-r--r--lib/pleroma/web/api_spec/schemas/attachment.ex68
-rw-r--r--lib/pleroma/web/api_spec/schemas/conversation.ex41
-rw-r--r--lib/pleroma/web/api_spec/schemas/list.ex23
-rw-r--r--lib/pleroma/web/api_spec/schemas/poll.ex62
-rw-r--r--lib/pleroma/web/api_spec/schemas/push_subscription.ex66
-rw-r--r--lib/pleroma/web/api_spec/schemas/scheduled_status.ex54
-rw-r--r--lib/pleroma/web/api_spec/schemas/status.ex233
-rw-r--r--lib/pleroma/web/api_spec/schemas/tag.ex27
-rw-r--r--lib/pleroma/web/api_spec/schemas/visibility_scope.ex2
-rw-r--r--lib/pleroma/web/auth/pleroma_authenticator.ex7
-rw-r--r--lib/pleroma/web/auth/totp_authenticator.ex45
-rw-r--r--lib/pleroma/web/chat_channel.ex1
-rw-r--r--lib/pleroma/web/common_api/activity_draft.ex22
-rw-r--r--lib/pleroma/web/common_api/common_api.ex133
-rw-r--r--lib/pleroma/web/common_api/utils.ex34
-rw-r--r--lib/pleroma/web/controller_helper.ex21
-rw-r--r--lib/pleroma/web/controllers/frontend/static_controller.ex2
-rw-r--r--lib/pleroma/web/feed/user_controller.ex4
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/account_controller.ex34
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/app_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex5
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex4
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/filter_controller.ex57
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex5
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/instance_controller.ex4
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/list_controller.ex26
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/marker_controller.ex10
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/media_controller.ex49
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/notification_controller.ex7
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/poll_controller.ex8
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/report_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex12
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/search_controller.ex38
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/status_controller.ex100
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex12
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex21
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex33
-rw-r--r--lib/pleroma/web/mastodon_api/views/account_view.ex47
-rw-r--r--lib/pleroma/web/mastodon_api/views/filter_view.ex6
-rw-r--r--lib/pleroma/web/mastodon_api/views/instance_view.ex8
-rw-r--r--lib/pleroma/web/mastodon_api/views/marker_view.ex16
-rw-r--r--lib/pleroma/web/mastodon_api/views/notification_view.ex23
-rw-r--r--lib/pleroma/web/mastodon_api/views/status_view.ex33
-rw-r--r--lib/pleroma/web/mastodon_api/websocket_handler.ex136
-rw-r--r--lib/pleroma/web/media_proxy/invalidation.ex26
-rw-r--r--lib/pleroma/web/media_proxy/invalidations/http.ex40
-rw-r--r--lib/pleroma/web/media_proxy/invalidations/script.ex41
-rw-r--r--lib/pleroma/web/mongooseim/mongoose_im_controller.ex4
-rw-r--r--lib/pleroma/web/oauth/mfa_controller.ex97
-rw-r--r--lib/pleroma/web/oauth/mfa_view.ex8
-rw-r--r--lib/pleroma/web/oauth/oauth_controller.ex48
-rw-r--r--lib/pleroma/web/oauth/token/clean_worker.ex38
-rw-r--r--lib/pleroma/web/oauth/token/response.ex9
-rw-r--r--lib/pleroma/web/ostatus/ostatus_controller.ex14
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/account_controller.ex31
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex95
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex (renamed from lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex)90
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex63
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex5
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/notification_controller.ex36
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex216
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex35
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex133
-rw-r--r--lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex33
-rw-r--r--lib/pleroma/web/pleroma_api/views/scrobble_view.ex37
-rw-r--r--lib/pleroma/web/push/impl.ex9
-rw-r--r--lib/pleroma/web/push/subscription.ex10
-rw-r--r--lib/pleroma/web/router.ex82
-rw-r--r--lib/pleroma/web/streamer/ping.ex37
-rw-r--r--lib/pleroma/web/streamer/state.ex82
-rw-r--r--lib/pleroma/web/streamer/streamer.ex291
-rw-r--r--lib/pleroma/web/streamer/streamer_socket.ex35
-rw-r--r--lib/pleroma/web/streamer/supervisor.ex37
-rw-r--r--lib/pleroma/web/streamer/worker.ex208
-rw-r--r--lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex24
-rw-r--r--lib/pleroma/web/templates/o_auth/mfa/totp.html.eex24
-rw-r--r--lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex13
-rw-r--r--lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex47
-rw-r--r--lib/pleroma/web/views/streamer_view.ex2
-rw-r--r--lib/pleroma/web/web.ex10
-rw-r--r--lib/pleroma/web/web_finger/web_finger.ex67
-rw-r--r--lib/pleroma/workers/attachments_cleanup_worker.ex49
-rw-r--r--lib/pleroma/workers/cron/clear_oauth_token_worker.ex2
-rw-r--r--lib/pleroma/workers/cron/digest_emails_worker.ex2
-rw-r--r--lib/pleroma/workers/cron/new_users_digest_worker.ex4
-rw-r--r--lib/pleroma/workers/cron/purge_expired_activities_worker.ex2
-rw-r--r--lib/pleroma/workers/scheduled_activity_worker.ex2
179 files changed, 8568 insertions, 2561 deletions
diff --git a/lib/mix/tasks/pleroma/benchmark.ex b/lib/mix/tasks/pleroma/benchmark.ex
index 6ab7fe8ef..dd2b9c8f2 100644
--- a/lib/mix/tasks/pleroma/benchmark.ex
+++ b/lib/mix/tasks/pleroma/benchmark.ex
@@ -67,8 +67,7 @@ defmodule Mix.Tasks.Pleroma.Benchmark do
Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{
activities: activities,
for: user,
- as: :activity,
- skip_relationships: true
+ as: :activity
})
end
},
diff --git a/lib/mix/tasks/pleroma/digest.ex b/lib/mix/tasks/pleroma/digest.ex
index 7d09e70c5..3595f912d 100644
--- a/lib/mix/tasks/pleroma/digest.ex
+++ b/lib/mix/tasks/pleroma/digest.ex
@@ -1,5 +1,6 @@
defmodule Mix.Tasks.Pleroma.Digest do
use Mix.Task
+ import Mix.Pleroma
@shortdoc "Manages digest emails"
@moduledoc File.read!("docs/administration/CLI_tasks/digest.md")
@@ -22,12 +23,10 @@ defmodule Mix.Tasks.Pleroma.Digest do
with %Swoosh.Email{} = email <- Pleroma.Emails.UserEmail.digest_email(patched_user) do
{: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
_ ->
- Mix.shell().info(
- "Cound't find any mentions for #{nickname} since #{last_digest_emailed_at}"
- )
+ shell_info("Cound't find any mentions for #{nickname} since #{last_digest_emailed_at}")
end
end
end
diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex
index 3e0f0fdc8..e1eb57061 100644
--- a/lib/mix/tasks/pleroma/instance.ex
+++ b/lib/mix/tasks/pleroma/instance.ex
@@ -154,6 +154,7 @@ defmodule Mix.Tasks.Pleroma.Instance do
"What directory should media uploads go in (when using the local uploader)?",
Pleroma.Config.get([Pleroma.Uploaders.Local, :uploads])
)
+ |> Path.expand()
static_dir =
get_option(
@@ -162,6 +163,7 @@ defmodule Mix.Tasks.Pleroma.Instance do
"What directory should custom public files be read from (custom emojis, frontend bundle overrides, robots.txt, etc.)?",
Pleroma.Config.get([:instance, :static_dir])
)
+ |> Path.expand()
Config.put([:instance, :static_dir], static_dir)
@@ -271,7 +273,7 @@ defmodule Mix.Tasks.Pleroma.Instance do
shell_info("Writing the postgres script to #{psql_path}.")
File.write(psql_path, result_psql)
- write_robots_txt(indexable, template_dir)
+ write_robots_txt(static_dir, indexable, template_dir)
shell_info(
"\n All files successfully written! Refer to the installation instructions for your platform for next steps."
@@ -291,15 +293,13 @@ defmodule Mix.Tasks.Pleroma.Instance do
end
end
- defp write_robots_txt(indexable, template_dir) do
+ defp write_robots_txt(static_dir, indexable, template_dir) do
robots_txt =
EEx.eval_file(
template_dir <> "/robots_txt.eex",
indexable: indexable
)
- static_dir = Pleroma.Config.get([:instance, :static_dir], "instance/static/")
-
unless File.exists?(static_dir) do
File.mkdir_p!(static_dir)
end
diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex
index f15112e9e..ce2f83665 100644
--- a/lib/mix/tasks/pleroma/user.ex
+++ b/lib/mix/tasks/pleroma/user.ex
@@ -8,6 +8,8 @@ defmodule Mix.Tasks.Pleroma.User do
alias Ecto.Changeset
alias Pleroma.User
alias Pleroma.UserInviteToken
+ alias Pleroma.Web.ActivityPub.Builder
+ alias Pleroma.Web.ActivityPub.Pipeline
@shortdoc "Manages Pleroma users"
@moduledoc File.read!("docs/administration/CLI_tasks/user.md")
@@ -96,8 +98,9 @@ defmodule Mix.Tasks.Pleroma.User do
def run(["rm", nickname]) do
start_pleroma()
- with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
- User.perform(:delete, user)
+ with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
+ {:ok, delete_data, _} <- Builder.delete(user, user.ap_id),
+ {:ok, _delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
shell_info("User #{nickname} deleted.")
else
_ -> shell_error("No local user #{nickname}")
@@ -141,28 +144,18 @@ defmodule Mix.Tasks.Pleroma.User do
end
end
- def run(["unsubscribe", nickname]) do
+ def run(["deactivate", nickname]) do
start_pleroma()
with %User{} = user <- User.get_cached_by_nickname(nickname) do
shell_info("Deactivating #{user.nickname}")
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)
user = User.get_cached_by_id(user.id)
- if Enum.empty?(User.get_friends(user)) do
- shell_info("Successfully unsubscribed all followers from #{user.nickname}")
+ if Enum.empty?(Enum.filter(User.get_friends(user), & &1.local)) do
+ shell_info("Successfully unsubscribed all local followers from #{user.nickname}")
end
else
_ ->
@@ -170,7 +163,7 @@ defmodule Mix.Tasks.Pleroma.User do
end
end
- def run(["unsubscribe_all_from_instance", instance]) do
+ def run(["deactivate_all_from_instance", instance]) do
start_pleroma()
Pleroma.User.Query.build(%{nickname: "@#{instance}"})
@@ -178,7 +171,7 @@ defmodule Mix.Tasks.Pleroma.User do
|> Stream.each(fn users ->
users
|> Enum.each(fn user ->
- run(["unsubscribe", user.nickname])
+ run(["deactivate", user.nickname])
end)
end)
|> Stream.run()
diff --git a/lib/pleroma/activity/queries.ex b/lib/pleroma/activity/queries.ex
index a34c20343..c99aae44b 100644
--- a/lib/pleroma/activity/queries.ex
+++ b/lib/pleroma/activity/queries.ex
@@ -24,10 +24,7 @@ defmodule Pleroma.Activity.Queries do
@spec by_actor(query, String.t()) :: query
def by_actor(query \\ Activity, actor) do
- from(
- activity in query,
- where: fragment("(?)->>'actor' = ?", activity.data, ^actor)
- )
+ from(a in query, where: a.actor == ^actor)
end
@spec by_author(query, User.t()) :: query
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index 06e87f846..0016a5874 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -58,7 +58,7 @@ defmodule Pleroma.Application do
if (major == 22 and minor < 2) or major < 22 do
raise "
!!!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
else
@@ -175,7 +175,14 @@ defmodule Pleroma.Application do
defp streamer_child(env) when env in [:test, :benchmark], do: []
defp streamer_child(_) do
- [Pleroma.Web.Streamer.supervisor()]
+ [
+ {Registry,
+ [
+ name: Pleroma.Web.Streamer.registry(),
+ keys: :duplicate,
+ partitions: System.schedulers_online()
+ ]}
+ ]
end
defp chat_child(_env, true) do
diff --git a/lib/pleroma/bbs/authenticator.ex b/lib/pleroma/bbs/authenticator.ex
index e5b37f33e..815de7002 100644
--- a/lib/pleroma/bbs/authenticator.ex
+++ b/lib/pleroma/bbs/authenticator.ex
@@ -4,7 +4,7 @@
defmodule Pleroma.BBS.Authenticator do
use Sshd.PasswordAuthenticator
- alias Comeonin.Pbkdf2
+ alias Pleroma.Plugs.AuthenticationPlug
alias Pleroma.User
def authenticate(username, password) do
@@ -12,7 +12,7 @@ defmodule Pleroma.BBS.Authenticator do
password = to_string(password)
with %User{} = user <- User.get_by_nickname(username) do
- Pbkdf2.checkpw(password, user.password_hash)
+ AuthenticationPlug.checkpw(password, user.password_hash)
else
_e -> false
end
diff --git a/lib/pleroma/bbs/handler.ex b/lib/pleroma/bbs/handler.ex
index c7bc8ef6c..12d64c2fe 100644
--- a/lib/pleroma/bbs/handler.ex
+++ b/lib/pleroma/bbs/handler.ex
@@ -66,7 +66,7 @@ defmodule Pleroma.BBS.Handler do
with %Activity{} <- Activity.get_by_id(activity_id),
{: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!")
else
_e -> IO.puts("Could not reply...")
@@ -78,7 +78,7 @@ defmodule Pleroma.BBS.Handler do
def handle_command(%{user: user} = state, "p " <> text) do
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!")
else
_e -> IO.puts("Could not post...")
diff --git a/lib/pleroma/config/config_db.ex b/lib/pleroma/config/config_db.ex
index 4097ee5b7..2b43d4c36 100644
--- a/lib/pleroma/config/config_db.ex
+++ b/lib/pleroma/config/config_db.ex
@@ -278,6 +278,8 @@ defmodule Pleroma.ConfigDB do
}
end
+ defp do_convert({:partial_chain, entity}), do: %{"tuple" => [":partial_chain", inspect(entity)]}
+
defp do_convert(entity) when is_tuple(entity) do
value =
entity
@@ -321,6 +323,15 @@ defmodule Pleroma.ConfigDB do
{:proxy_url, {do_transform_string(type), parse_host(host), port}}
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
Enum.reduce(entity, {}, fn val, acc -> Tuple.append(acc, do_transform(val)) end)
end
diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex
index 3a9eec5ea..06174f624 100644
--- a/lib/pleroma/constants.ex
+++ b/lib/pleroma/constants.ex
@@ -17,7 +17,8 @@ defmodule Pleroma.Constants do
"announcement_count",
"emoji",
"context_id",
- "deleted_activity_id"
+ "deleted_activity_id",
+ "pleroma_internal"
]
)
diff --git a/lib/pleroma/conversation.ex b/lib/pleroma/conversation.ex
index 37d455cfc..e76eb0087 100644
--- a/lib/pleroma/conversation.ex
+++ b/lib/pleroma/conversation.ex
@@ -63,7 +63,7 @@ defmodule Pleroma.Conversation 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)
- users = User.get_users_from_set(activity.recipients, false)
+ users = User.get_users_from_set(activity.recipients, local_only: false)
participations =
Enum.map(users, fn user ->
diff --git a/lib/pleroma/conversation/participation.ex b/lib/pleroma/conversation/participation.ex
index 215265fc9..51bb1bda9 100644
--- a/lib/pleroma/conversation/participation.ex
+++ b/lib/pleroma/conversation/participation.ex
@@ -128,7 +128,7 @@ defmodule Pleroma.Conversation.Participation do
|> Pleroma.Pagination.fetch_paginated(params)
end
- def restrict_recipients(query, user, %{"recipients" => user_ids}) do
+ def restrict_recipients(query, user, %{recipients: user_ids}) do
user_binary_ids =
[user.id | user_ids]
|> Enum.uniq()
@@ -172,7 +172,7 @@ defmodule Pleroma.Conversation.Participation do
| last_activity_id: activity_id
}
end)
- |> Enum.filter(& &1.last_activity_id)
+ |> Enum.reject(&is_nil(&1.last_activity_id))
end
def get(_, _ \\ [])
diff --git a/lib/pleroma/docs/json.ex b/lib/pleroma/docs/json.ex
index 74f8b2615..d1cf1f487 100644
--- a/lib/pleroma/docs/json.ex
+++ b/lib/pleroma/docs/json.ex
@@ -18,7 +18,6 @@ defmodule Pleroma.Docs.JSON do
with config <- Pleroma.Config.Loader.read("config/description.exs") do
config[:pleroma][:config_description]
|> Pleroma.Docs.Generator.convert_to_strings()
- |> Jason.encode!()
end
end
end
diff --git a/lib/pleroma/emails/new_users_digest_email.ex b/lib/pleroma/emails/new_users_digest_email.ex
index 7d16b807f..348cbac9c 100644
--- a/lib/pleroma/emails/new_users_digest_email.ex
+++ b/lib/pleroma/emails/new_users_digest_email.ex
@@ -14,8 +14,10 @@ defmodule Pleroma.Emails.NewUsersDigestEmail do
styling = Pleroma.Config.get([Pleroma.Emails.UserEmail, :styling])
logo_url =
- Pleroma.Web.Endpoint.url() <>
- Pleroma.Config.get([:frontend_configurations, :pleroma_fe, :logo])
+ Pleroma.Helpers.UriHelper.maybe_add_base(
+ Pleroma.Config.get([:frontend_configurations, :pleroma_fe, :logo]),
+ Pleroma.Web.Endpoint.url()
+ )
new()
|> to({to.name, to.email})
diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex
index 242344374..eb7d598c6 100644
--- a/lib/pleroma/emoji/pack.ex
+++ b/lib/pleroma/emoji/pack.ex
@@ -16,162 +16,78 @@ defmodule Pleroma.Emoji.Pack do
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}
- def create(name) when byte_size(name) > 0 do
- dir = Path.join(emoji_path(), name)
-
- with :ok <- File.mkdir(dir) do
- %__MODULE__{
- pack_file: Path.join(dir, "pack.json")
- }
+ def create(name) do
+ with :ok <- validate_not_empty([name]),
+ dir <- Path.join(emoji_path(), name),
+ :ok <- File.mkdir(dir) do
+ %__MODULE__{pack_file: Path.join(dir, "pack.json")}
|> save_pack()
end
end
- def create(_), do: {:error, :empty_values}
-
- @spec show(String.t()) :: {:ok, t()} | {:loaded, nil} | {:error, :empty_values}
- def show(name) when byte_size(name) > 0 do
- with {_, %__MODULE__{} = pack} <- {:loaded, load_pack(name)},
- {_, pack} <- validate_pack(pack) do
- {:ok, pack}
+ @spec show(String.t()) :: {:ok, t()} | {:error, atom()}
+ def show(name) do
+ with :ok <- validate_not_empty([name]),
+ {:ok, pack} <- load_pack(name) do
+ {:ok, validate_pack(pack)}
end
end
- def show(_), do: {:error, :empty_values}
-
@spec delete(String.t()) ::
{:ok, [binary()]} | {:error, File.posix(), binary()} | {:error, :empty_values}
- def delete(name) when byte_size(name) > 0 do
- emoji_path()
- |> Path.join(name)
- |> File.rm_rf()
- end
-
- 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
+ def delete(name) do
+ with :ok <- validate_not_empty([name]) do
+ emoji_path()
+ |> Path.join(name)
+ |> File.rm_rf()
end
end
- def add_file(_, _, _, _), do: {:error, :empty_values}
-
- defp create_subdirs(file_path) do
- if String.contains?(file_path, "/") do
- file_path
- |> Path.dirname()
- |> File.mkdir_p!()
+ @spec add_file(String.t(), String.t(), Path.t(), Plug.Upload.t() | String.t()) ::
+ {:ok, t()} | {:error, File.posix() | atom()}
+ def add_file(name, shortcode, filename, file) do
+ with :ok <- validate_not_empty([name, shortcode, filename]),
+ :ok <- validate_emoji_not_exists(shortcode),
+ {:ok, pack} <- load_pack(name),
+ :ok <- save_file(file, pack, filename),
+ {:ok, updated_pack} <- pack |> put_emoji(shortcode, filename) |> save_pack() do
+ Emoji.reload()
+ {:ok, updated_pack}
end
end
@spec delete_file(String.t(), String.t()) ::
- {:ok, t()} | {:error, File.posix()} | {:error, :empty_values}
- def delete_file(name, shortcode) when byte_size(name) > 0 and byte_size(shortcode) > 0 do
- with {_, %__MODULE__{} = pack} <- {:loaded, load_pack(name)},
- {_, {filename, files}} when not is_nil(filename) <-
- {:exists, Map.pop(pack.files, shortcode)},
- emoji <- Path.join(pack.path, filename),
- {_, true} <- {:exists, File.exists?(emoji)} do
- emoji_dir = Path.dirname(emoji)
-
- 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
+ {:ok, t()} | {:error, File.posix() | atom()}
+ def delete_file(name, shortcode) do
+ with :ok <- validate_not_empty([name, shortcode]),
+ {:ok, pack} <- load_pack(name),
+ :ok <- remove_file(pack, shortcode),
+ {:ok, updated_pack} <- pack |> delete_emoji(shortcode) |> save_pack() do
+ Emoji.reload()
+ {:ok, updated_pack}
end
end
- def delete_file(_, _), do: {:error, :empty_values}
-
@spec update_file(String.t(), String.t(), String.t(), String.t(), boolean()) ::
- {:ok, t()} | {:error, File.posix()} | {:error, :empty_values}
- def update_file(name, shortcode, new_shortcode, new_filename, force)
- when byte_size(name) > 0 and byte_size(shortcode) > 0 and byte_size(new_shortcode) > 0 and
- byte_size(new_filename) > 0 do
- with {_, %__MODULE__{} = pack} <- {:loaded, load_pack(name)},
- {_, {filename, files}} when not is_nil(filename) <-
- {:exists, Map.pop(pack.files, shortcode)},
- {_, true} <- {:not_used, force or is_nil(Emoji.get(new_shortcode))} do
- old_path = Path.join(pack.path, filename)
- old_dir = Path.dirname(old_path)
- new_path = Path.join(pack.path, new_filename)
-
- create_subdirs(new_path)
-
- :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
+ {:ok, t()} | {:error, File.posix() | atom()}
+ def update_file(name, shortcode, new_shortcode, new_filename, force) do
+ with :ok <- validate_not_empty([name, shortcode, new_shortcode, new_filename]),
+ {:ok, pack} <- load_pack(name),
+ {:ok, filename} <- get_filename(pack, shortcode),
+ :ok <- validate_emoji_not_exists(new_shortcode, force),
+ :ok <- rename_file(pack, filename, new_filename),
+ {:ok, updated_pack} <-
+ pack
+ |> delete_emoji(shortcode)
+ |> put_emoji(new_shortcode, new_filename)
+ |> save_pack() do
+ Emoji.reload()
+ {:ok, updated_pack}
end
end
- def update_file(_, _, _, _, _), do: {:error, :empty_values}
-
- @spec import_from_filesystem() :: {:ok, [String.t()]} | {:error, atom()}
+ @spec import_from_filesystem() :: {:ok, [String.t()]} | {:error, File.posix() | atom()}
def import_from_filesystem do
emoji_path = emoji_path()
@@ -184,7 +100,7 @@ defmodule Pleroma.Emoji.Pack do
File.dir?(path) and File.exists?(Path.join(path, "pack.json"))
end)
|> Enum.map(&write_pack_contents/1)
- |> Enum.filter(& &1)
+ |> Enum.reject(&is_nil/1)
{:ok, names}
else
@@ -193,6 +109,117 @@ defmodule Pleroma.Emoji.Pack do
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
pack = %__MODULE__{
files: files_from_path(path),
@@ -201,7 +228,7 @@ defmodule Pleroma.Emoji.Pack do
}
case save_pack(pack) do
- :ok -> Path.basename(path)
+ {:ok, _pack} -> Path.basename(path)
_ -> nil
end
end
@@ -216,7 +243,8 @@ defmodule Pleroma.Emoji.Pack do
# FIXME: Copy-pasted from Pleroma.Emoji/load_from_file_stream/2
# Create a map of shortcodes to filenames from emoji.txt
- File.read!(txt_path)
+ txt_path
+ |> File.read!()
|> String.split("\n")
|> Enum.map(&String.trim/1)
|> Enum.map(fn line ->
@@ -226,21 +254,18 @@ defmodule Pleroma.Emoji.Pack do
[name, file | _] ->
file_dir_name = Path.dirname(file)
- file =
- if String.ends_with?(path, file_dir_name) do
- Path.basename(file)
- else
- file
- end
-
- {name, file}
+ if String.ends_with?(path, file_dir_name) do
+ {name, Path.basename(file)}
+ else
+ {name, file}
+ end
_ ->
nil
end
end)
- |> Enum.filter(& &1)
- |> Enum.into(%{})
+ |> Enum.reject(&is_nil/1)
+ |> Map.new()
else
# If there's no emoji.txt, assume all files
# that are of certain extensions from the config are emojis and import them all
@@ -249,60 +274,20 @@ defmodule Pleroma.Emoji.Pack do
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
- if downloadable?(pack) do
- archive = fetch_archive(pack)
- archive_sha = :crypto.hash(:sha256, archive) |> Base.encode16()
+ info =
+ if downloadable?(pack) do
+ archive = fetch_archive(pack)
+ archive_sha = :crypto.hash(:sha256, archive) |> Base.encode16()
- info =
pack.pack
|> Map.put("can-download", true)
|> Map.put("download-sha256", archive_sha)
+ else
+ Map.put(pack.pack, "can-download", false)
+ end
- {pack.name, Map.put(pack, :pack, info)}
- else
- info = Map.put(pack.pack, "can-download", false)
- {pack.name, Map.put(pack, :pack, info)}
- end
+ Map.put(pack, :pack, info)
end
defp downloadable?(pack) do
@@ -315,26 +300,6 @@ defmodule Pleroma.Emoji.Pack do
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
files = ['pack.json' | Enum.map(pack.files, fn {_, file} -> to_charlist(file) end)]
@@ -356,152 +321,221 @@ defmodule Pleroma.Emoji.Pack do
result
end
- @spec download(String.t(), String.t(), String.t()) :: :ok
- def download(name, url, as) do
- uri =
- 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
- }}
+ defp save_pack(pack) do
+ with {:ok, json} <- Jason.encode(pack, pretty: true),
+ :ok <- File.write(pack.pack_file, json) do
+ {:ok, pack}
+ end
+ end
- _ ->
- {:error,
- "The pack was not set as shared and there is no fallback src to download from"}
- end
+ defp from_json(json) do
+ map = Jason.decode!(json)
- 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
+ struct(__MODULE__, %{files: map["files"], pack: map["pack"]})
+ end
- path = Path.join(emoji_path(), local_name)
+ defp validate_shareable_packs_available(uri) do
+ with {:ok, %{"links" => links}} <- uri |> URI.merge("/.well-known/nodeinfo") |> http_get(),
+ # Get the actual nodeinfo address and fetch it
+ {:ok, %{"metadata" => %{"features" => features}}} <-
+ links |> List.last() |> Map.get("href") |> http_get() do
+ if Enum.member?(features, "shareable_emoji_packs") do
+ :ok
+ else
+ {:error, :not_shareable}
+ end
+ end
+ end
- pack = %__MODULE__{
- name: local_name,
- path: path,
- files: remote_pack["files"],
- pack_file: Path.join(path, "pack.json")
- }
+ defp validate_not_empty(list) do
+ if Enum.all?(list, fn i -> is_binary(i) and i != "" end) do
+ :ok
+ else
+ {:error, :empty_values}
+ end
+ end
- File.mkdir_p!(pack.path)
+ defp save_file(file, pack, filename) do
+ file_path = Path.join(pack.path, filename)
+ create_subdirs(file_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]
+ 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
- {:ok, _} = :zip.unzip(archive, cwd: to_charlist(pack.path), file_list: files)
+ url when is_binary(url) ->
+ # Download and write the file
+ file_contents = Tesla.get!(url).body
+ File.write(file_path, file_contents)
+ end
+ end
- # 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
+ defp put_emoji(pack, shortcode, filename) do
+ files = Map.put(pack.files, shortcode, filename)
+ %{pack | files: files}
+ end
- :ok
- 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 save_pack(pack), do: File.write(pack.pack_file, Jason.encode!(pack, pretty: true))
+ defp create_subdirs(file_path) do
+ if String.contains?(file_path, "/") do
+ file_path
+ |> Path.dirname()
+ |> File.mkdir_p!()
+ end
+ end
- @spec save_metadata(map(), t()) :: {:ok, t()} | {:error, File.posix()}
- def save_metadata(metadata, %__MODULE__{} = pack) do
- pack = Map.put(pack, :pack, metadata)
+ 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
- with :ok <- save_pack(pack) do
- {:ok, pack}
+ 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
- @spec update_metadata(String.t(), map()) :: {:ok, t()} | {:error, File.posix()}
- def update_metadata(name, data) do
- pack = load_pack(name)
+ 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
- fb_sha_changed? =
- not is_nil(data["fallback-src"]) and data["fallback-src"] != pack.pack["fallback-src"]
+ defp http_get(%URI{} = url), do: url |> to_string() |> http_get()
- 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()
+ defp http_get(url) do
+ with {:ok, %{body: body}} <- url |> Pleroma.HTTP.get() do
+ Jason.decode(body)
+ end
+ end
- data
- |> Map.put("fallback-src-sha256", fallback_sha)
- |> save_metadata(pack)
+ 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
- {:update?, _} -> save_metadata(data, pack)
- e -> e
+ {:create_dir, {:error, e}} -> {:error, :create_dir, e}
+ {:ls, {:error, e}} -> {:error, :ls, 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)
+ defp validate_downloadable(pack) do
+ if downloadable?(pack), do: :ok, else: {:error, :cant_download}
end
- @spec load_pack(String.t()) :: t() | nil
- def load_pack(name) do
- pack_file = Path.join([emoji_path(), name, "pack.json"])
+ defp copy_as(remote_pack, local_name) do
+ path = Path.join(emoji_path(), local_name)
- 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)
+ %__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 from_json(json) do
- map = Jason.decode!(json)
+ 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
+ }}
- struct(__MODULE__, %{files: map["files"], pack: map["pack"]})
+ _ ->
+ {: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, :imvalid_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 shareable_packs_available?(uri) do
- uri
- |> URI.merge("/.well-known/nodeinfo")
- |> to_string()
- |> Tesla.get!()
- |> Map.get(:body)
- |> Jason.decode!()
- |> Map.get("links")
- |> List.last()
- |> Map.get("href")
- # Get the actual nodeinfo address and fetch it
- |> Tesla.get!()
- |> Map.get(:body)
- |> Jason.decode!()
- |> get_in(["metadata", "features"])
- |> Enum.member?("shareable_emoji_packs")
+ 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
diff --git a/lib/pleroma/filter.ex b/lib/pleroma/filter.ex
index 7cb49360f..4d61b3650 100644
--- a/lib/pleroma/filter.ex
+++ b/lib/pleroma/filter.ex
@@ -89,11 +89,10 @@ defmodule Pleroma.Filter do
|> Repo.delete()
end
- def update(%Pleroma.Filter{} = filter) do
- destination = Map.from_struct(filter)
-
- Pleroma.Filter.get(filter.filter_id, %{id: filter.user_id})
- |> cast(destination, [:phrase, :context, :hide, :expires_at, :whole_word])
+ def update(%Pleroma.Filter{} = filter, params) do
+ filter
+ |> cast(params, [:phrase, :context, :hide, :expires_at, :whole_word])
+ |> validate_required([:phrase, :context])
|> Repo.update()
end
end
diff --git a/lib/pleroma/healthcheck.ex b/lib/pleroma/healthcheck.ex
index 8f7f43ec2..92ce83cb7 100644
--- a/lib/pleroma/healthcheck.ex
+++ b/lib/pleroma/healthcheck.ex
@@ -29,7 +29,7 @@ defmodule Pleroma.Healthcheck do
@spec system_info() :: t()
def system_info do
%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_job_queue_stats()
diff --git a/lib/pleroma/helpers/uri_helper.ex b/lib/pleroma/helpers/uri_helper.ex
index 256252ddb..69d8c8fe0 100644
--- a/lib/pleroma/helpers/uri_helper.ex
+++ b/lib/pleroma/helpers/uri_helper.ex
@@ -24,4 +24,7 @@ defmodule Pleroma.Helpers.UriHelper do
params
end
end
+
+ def maybe_add_base("/" <> uri, base), do: Path.join([base, uri])
+ def maybe_add_base(uri, _base), do: uri
end
diff --git a/lib/pleroma/marker.ex b/lib/pleroma/marker.ex
index 443927392..4d82860f5 100644
--- a/lib/pleroma/marker.ex
+++ b/lib/pleroma/marker.ex
@@ -9,24 +9,34 @@ defmodule Pleroma.Marker do
import Ecto.Query
alias Ecto.Multi
+ alias Pleroma.Notification
alias Pleroma.Repo
alias Pleroma.User
+ alias __MODULE__
@timelines ["notifications"]
+ @type t :: %__MODULE__{}
schema "markers" do
field(:last_read_id, :string, default: "")
field(:timeline, :string, default: "")
field(:lock_version, :integer, default: 0)
+ field(:unread_count, :integer, default: 0, virtual: true)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
timestamps()
end
+ @doc "Gets markers by user and timeline."
+ @spec get_markers(User.t(), list(String)) :: list(t())
def get_markers(user, timelines \\ []) do
- Repo.all(get_query(user, timelines))
+ user
+ |> get_query(timelines)
+ |> unread_count_query()
+ |> Repo.all()
end
+ @spec upsert(User.t(), map()) :: {:ok | :error, any()}
def upsert(%User{} = user, attrs) do
attrs
|> Map.take(@timelines)
@@ -45,6 +55,27 @@ defmodule Pleroma.Marker do
|> Repo.transaction()
end
+ @spec multi_set_last_read_id(Multi.t(), User.t(), String.t()) :: Multi.t()
+ def multi_set_last_read_id(multi, %User{} = user, "notifications") do
+ multi
+ |> Multi.run(:counters, fn _repo, _changes ->
+ {:ok, %{last_read_id: Repo.one(Notification.last_read_query(user))}}
+ end)
+ |> Multi.insert(
+ :marker,
+ fn %{counters: attrs} ->
+ %Marker{timeline: "notifications", user_id: user.id}
+ |> struct(attrs)
+ |> Ecto.Changeset.change()
+ end,
+ returning: true,
+ on_conflict: {:replace, [:last_read_id]},
+ conflict_target: [:user_id, :timeline]
+ )
+ end
+
+ def multi_set_last_read_id(multi, _, _), do: multi
+
defp get_marker(user, timeline) do
case Repo.find_resource(get_query(user, timeline)) do
{:ok, marker} -> %__MODULE__{marker | user: user}
@@ -71,4 +102,16 @@ defmodule Pleroma.Marker do
|> by_user_id(user.id)
|> by_timeline(timelines)
end
+
+ defp unread_count_query(query) do
+ from(
+ q in query,
+ left_join: n in "notifications",
+ on: n.user_id == q.user_id and n.seen == false,
+ group_by: [:id],
+ select_merge: %{
+ unread_count: fragment("count(?)", n.id)
+ }
+ )
+ end
end
diff --git a/lib/pleroma/mfa.ex b/lib/pleroma/mfa.ex
new file mode 100644
index 000000000..01b743f4f
--- /dev/null
+++ b/lib/pleroma/mfa.ex
@@ -0,0 +1,155 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.MFA do
+ @moduledoc """
+ The MFA context.
+ """
+
+ alias Pleroma.User
+
+ alias Pleroma.MFA.BackupCodes
+ alias Pleroma.MFA.Changeset
+ alias Pleroma.MFA.Settings
+ alias Pleroma.MFA.TOTP
+
+ @doc """
+ Returns MFA methods the user has enabled.
+
+ ## Examples
+
+ iex> Pleroma.MFA.supported_method(User)
+ "totp, u2f"
+ """
+ @spec supported_methods(User.t()) :: String.t()
+ def supported_methods(user) do
+ settings = fetch_settings(user)
+
+ Settings.mfa_methods()
+ |> Enum.reduce([], fn m, acc ->
+ if method_enabled?(m, settings) do
+ acc ++ [m]
+ else
+ acc
+ end
+ end)
+ |> Enum.join(",")
+ end
+
+ @doc "Checks that user enabled MFA"
+ def require?(user) do
+ fetch_settings(user).enabled
+ end
+
+ @doc """
+ Display MFA settings of user
+ """
+ def mfa_settings(user) do
+ settings = fetch_settings(user)
+
+ Settings.mfa_methods()
+ |> Enum.map(fn m -> [m, method_enabled?(m, settings)] end)
+ |> Enum.into(%{enabled: settings.enabled}, fn [a, b] -> {a, b} end)
+ end
+
+ @doc false
+ def fetch_settings(%User{} = user) do
+ user.multi_factor_authentication_settings || %Settings{}
+ end
+
+ @doc "clears backup codes"
+ def invalidate_backup_code(%User{} = user, hash_code) do
+ %{backup_codes: codes} = fetch_settings(user)
+
+ user
+ |> Changeset.cast_backup_codes(codes -- [hash_code])
+ |> User.update_and_set_cache()
+ end
+
+ @doc "generates backup codes"
+ @spec generate_backup_codes(User.t()) :: {:ok, list(binary)} | {:error, String.t()}
+ def generate_backup_codes(%User{} = user) do
+ with codes <- BackupCodes.generate(),
+ hashed_codes <- Enum.map(codes, &Pbkdf2.hash_pwd_salt/1),
+ changeset <- Changeset.cast_backup_codes(user, hashed_codes),
+ {:ok, _} <- User.update_and_set_cache(changeset) do
+ {:ok, codes}
+ else
+ {:error, msg} ->
+ %{error: msg}
+ end
+ end
+
+ @doc """
+ Generates secret key and set delivery_type to 'app' for TOTP method.
+ """
+ @spec setup_totp(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
+ def setup_totp(user) do
+ user
+ |> Changeset.setup_totp(%{secret: TOTP.generate_secret(), delivery_type: "app"})
+ |> User.update_and_set_cache()
+ end
+
+ @doc """
+ Confirms the TOTP method for user.
+
+ `attrs`:
+ `password` - current user password
+ `code` - TOTP token
+ """
+ @spec confirm_totp(User.t(), map()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t() | atom()}
+ def confirm_totp(%User{} = user, attrs) do
+ with settings <- user.multi_factor_authentication_settings.totp,
+ {:ok, :pass} <- TOTP.validate_token(settings.secret, attrs["code"]) do
+ user
+ |> Changeset.confirm_totp()
+ |> User.update_and_set_cache()
+ end
+ end
+
+ @doc """
+ Disables the TOTP method for user.
+
+ `attrs`:
+ `password` - current user password
+ """
+ @spec disable_totp(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
+ def disable_totp(%User{} = user) do
+ user
+ |> Changeset.disable_totp()
+ |> Changeset.disable()
+ |> User.update_and_set_cache()
+ end
+
+ @doc """
+ Force disables all MFA methods for user.
+ """
+ @spec disable(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
+ def disable(%User{} = user) do
+ user
+ |> Changeset.disable_totp()
+ |> Changeset.disable(true)
+ |> User.update_and_set_cache()
+ end
+
+ @doc """
+ Checks if the user has MFA method enabled.
+ """
+ def method_enabled?(method, settings) do
+ with {:ok, %{confirmed: true} = _} <- Map.fetch(settings, method) do
+ true
+ else
+ _ -> false
+ end
+ end
+
+ @doc """
+ Checks if the user has enabled at least one MFA method.
+ """
+ def enabled?(settings) do
+ Settings.mfa_methods()
+ |> Enum.map(fn m -> method_enabled?(m, settings) end)
+ |> Enum.any?()
+ end
+end
diff --git a/lib/pleroma/mfa/backup_codes.ex b/lib/pleroma/mfa/backup_codes.ex
new file mode 100644
index 000000000..9875310ff
--- /dev/null
+++ b/lib/pleroma/mfa/backup_codes.ex
@@ -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.MFA.BackupCodes do
+ @moduledoc """
+ This module contains functions for generating backup codes.
+ """
+ alias Pleroma.Config
+
+ @config_ns [:instance, :multi_factor_authentication, :backup_codes]
+
+ @doc """
+ Generates backup codes.
+ """
+ @spec generate(Keyword.t()) :: list(String.t())
+ def generate(opts \\ []) do
+ number_of_codes = Keyword.get(opts, :number_of_codes, default_backup_codes_number())
+ code_length = Keyword.get(opts, :length, default_backup_codes_code_length())
+
+ Enum.map(1..number_of_codes, fn _ ->
+ :crypto.strong_rand_bytes(div(code_length, 2))
+ |> Base.encode16(case: :lower)
+ end)
+ end
+
+ defp default_backup_codes_number, do: Config.get(@config_ns ++ [:number], 5)
+
+ defp default_backup_codes_code_length,
+ do: Config.get(@config_ns ++ [:length], 16)
+end
diff --git a/lib/pleroma/mfa/changeset.ex b/lib/pleroma/mfa/changeset.ex
new file mode 100644
index 000000000..77c4fa202
--- /dev/null
+++ b/lib/pleroma/mfa/changeset.ex
@@ -0,0 +1,64 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.MFA.Changeset do
+ alias Pleroma.MFA
+ alias Pleroma.MFA.Settings
+ alias Pleroma.User
+
+ def disable(%Ecto.Changeset{} = changeset, force \\ false) do
+ settings =
+ changeset
+ |> Ecto.Changeset.apply_changes()
+ |> MFA.fetch_settings()
+
+ if force || not MFA.enabled?(settings) do
+ put_change(changeset, %Settings{settings | enabled: false})
+ else
+ changeset
+ end
+ end
+
+ def disable_totp(%User{multi_factor_authentication_settings: settings} = user) do
+ user
+ |> put_change(%Settings{settings | totp: %Settings.TOTP{}})
+ end
+
+ def confirm_totp(%User{multi_factor_authentication_settings: settings} = user) do
+ totp_settings = %Settings.TOTP{settings.totp | confirmed: true}
+
+ user
+ |> put_change(%Settings{settings | totp: totp_settings, enabled: true})
+ end
+
+ def setup_totp(%User{} = user, attrs) do
+ mfa_settings = MFA.fetch_settings(user)
+
+ totp_settings =
+ %Settings.TOTP{}
+ |> Ecto.Changeset.cast(attrs, [:secret, :delivery_type])
+
+ user
+ |> put_change(%Settings{mfa_settings | totp: Ecto.Changeset.apply_changes(totp_settings)})
+ end
+
+ def cast_backup_codes(%User{} = user, codes) do
+ user
+ |> put_change(%Settings{
+ user.multi_factor_authentication_settings
+ | backup_codes: codes
+ })
+ end
+
+ defp put_change(%User{} = user, settings) do
+ user
+ |> Ecto.Changeset.change()
+ |> put_change(settings)
+ end
+
+ defp put_change(%Ecto.Changeset{} = changeset, settings) do
+ changeset
+ |> Ecto.Changeset.put_change(:multi_factor_authentication_settings, settings)
+ end
+end
diff --git a/lib/pleroma/mfa/settings.ex b/lib/pleroma/mfa/settings.ex
new file mode 100644
index 000000000..de6e2228f
--- /dev/null
+++ b/lib/pleroma/mfa/settings.ex
@@ -0,0 +1,24 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.MFA.Settings do
+ use Ecto.Schema
+
+ @primary_key false
+
+ @mfa_methods [:totp]
+ embedded_schema do
+ field(:enabled, :boolean, default: false)
+ field(:backup_codes, {:array, :string}, default: [])
+
+ embeds_one :totp, TOTP, on_replace: :delete, primary_key: false do
+ field(:secret, :string)
+ # app | sms
+ field(:delivery_type, :string, default: "app")
+ field(:confirmed, :boolean, default: false)
+ end
+ end
+
+ def mfa_methods, do: @mfa_methods
+end
diff --git a/lib/pleroma/mfa/token.ex b/lib/pleroma/mfa/token.ex
new file mode 100644
index 000000000..0b2449971
--- /dev/null
+++ b/lib/pleroma/mfa/token.ex
@@ -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.MFA.Token do
+ use Ecto.Schema
+ import Ecto.Query
+ import Ecto.Changeset
+
+ alias Pleroma.Repo
+ alias Pleroma.User
+ alias Pleroma.Web.OAuth.Authorization
+ alias Pleroma.Web.OAuth.Token, as: OAuthToken
+
+ @expires 300
+
+ schema "mfa_tokens" do
+ field(:token, :string)
+ field(:valid_until, :naive_datetime_usec)
+
+ belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
+ belongs_to(:authorization, Authorization)
+
+ timestamps()
+ end
+
+ def get_by_token(token) do
+ from(
+ t in __MODULE__,
+ where: t.token == ^token,
+ preload: [:user, :authorization]
+ )
+ |> Repo.find_resource()
+ end
+
+ def validate(token) do
+ with {:fetch_token, {:ok, token}} <- {:fetch_token, get_by_token(token)},
+ {:expired, false} <- {:expired, is_expired?(token)} do
+ {:ok, token}
+ else
+ {:expired, _} -> {:error, :expired_token}
+ {:fetch_token, _} -> {:error, :not_found}
+ error -> {:error, error}
+ end
+ end
+
+ def create_token(%User{} = user) do
+ %__MODULE__{}
+ |> change
+ |> assign_user(user)
+ |> put_token
+ |> put_valid_until
+ |> Repo.insert()
+ end
+
+ def create_token(user, authorization) do
+ %__MODULE__{}
+ |> change
+ |> assign_user(user)
+ |> assign_authorization(authorization)
+ |> put_token
+ |> put_valid_until
+ |> Repo.insert()
+ end
+
+ defp assign_user(changeset, user) do
+ changeset
+ |> put_assoc(:user, user)
+ |> validate_required([:user])
+ end
+
+ defp assign_authorization(changeset, authorization) do
+ changeset
+ |> put_assoc(:authorization, authorization)
+ |> validate_required([:authorization])
+ end
+
+ defp put_token(changeset) do
+ changeset
+ |> change(%{token: OAuthToken.Utils.generate_token()})
+ |> validate_required([:token])
+ |> unique_constraint(:token)
+ end
+
+ defp put_valid_until(changeset) do
+ expires_in = NaiveDateTime.add(NaiveDateTime.utc_now(), @expires)
+
+ changeset
+ |> change(%{valid_until: expires_in})
+ |> validate_required([:valid_until])
+ end
+
+ def is_expired?(%__MODULE__{valid_until: valid_until}) do
+ NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) > 0
+ end
+
+ def is_expired?(_), do: false
+
+ def delete_expired_tokens do
+ from(
+ q in __MODULE__,
+ where: fragment("?", q.valid_until) < ^Timex.now()
+ )
+ |> Repo.delete_all()
+ end
+end
diff --git a/lib/pleroma/mfa/totp.ex b/lib/pleroma/mfa/totp.ex
new file mode 100644
index 000000000..d2ea2b3aa
--- /dev/null
+++ b/lib/pleroma/mfa/totp.ex
@@ -0,0 +1,86 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.MFA.TOTP do
+ @moduledoc """
+ This module represents functions to create secrets for
+ TOTP Application as well as validate them with a time based token.
+ """
+ alias Pleroma.Config
+
+ @config_ns [:instance, :multi_factor_authentication, :totp]
+
+ @doc """
+ https://github.com/google/google-authenticator/wiki/Key-Uri-Format
+ """
+ def provisioning_uri(secret, label, opts \\ []) do
+ query =
+ %{
+ secret: secret,
+ issuer: Keyword.get(opts, :issuer, default_issuer()),
+ digits: Keyword.get(opts, :digits, default_digits()),
+ period: Keyword.get(opts, :period, default_period())
+ }
+ |> Enum.filter(fn {_, v} -> not is_nil(v) end)
+ |> Enum.into(%{})
+ |> URI.encode_query()
+
+ %URI{scheme: "otpauth", host: "totp", path: "/" <> label, query: query}
+ |> URI.to_string()
+ end
+
+ defp default_period, do: Config.get(@config_ns ++ [:period])
+ defp default_digits, do: Config.get(@config_ns ++ [:digits])
+
+ defp default_issuer,
+ do: Config.get(@config_ns ++ [:issuer], Config.get([:instance, :name]))
+
+ @doc "Creates a random Base 32 encoded string"
+ def generate_secret do
+ Base.encode32(:crypto.strong_rand_bytes(10))
+ end
+
+ @doc "Generates a valid token based on a secret"
+ def generate_token(secret) do
+ :pot.totp(secret)
+ end
+
+ @doc """
+ Validates a given token based on a secret.
+
+ optional parameters:
+ `token_length` default `6`
+ `interval_length` default `30`
+ `window` default 0
+
+ Returns {:ok, :pass} if the token is valid and
+ {:error, :invalid_token} if it is not.
+ """
+ @spec validate_token(String.t(), String.t()) ::
+ {:ok, :pass} | {:error, :invalid_token | :invalid_secret_and_token}
+ def validate_token(secret, token)
+ when is_binary(secret) and is_binary(token) do
+ opts = [
+ token_length: default_digits(),
+ interval_length: default_period()
+ ]
+
+ validate_token(secret, token, opts)
+ end
+
+ def validate_token(_, _), do: {:error, :invalid_secret_and_token}
+
+ @doc "See `validate_token/2`"
+ @spec validate_token(String.t(), String.t(), Keyword.t()) ::
+ {:ok, :pass} | {:error, :invalid_token | :invalid_secret_and_token}
+ def validate_token(secret, token, options)
+ when is_binary(secret) and is_binary(token) do
+ case :pot.valid_totp(token, secret, options) do
+ true -> {:ok, :pass}
+ false -> {:error, :invalid_token}
+ end
+ end
+
+ def validate_token(_, _, _), do: {:error, :invalid_secret_and_token}
+end
diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex
index 98289af08..7eca55ac9 100644
--- a/lib/pleroma/notification.ex
+++ b/lib/pleroma/notification.ex
@@ -5,8 +5,10 @@
defmodule Pleroma.Notification do
use Ecto.Schema
+ alias Ecto.Multi
alias Pleroma.Activity
alias Pleroma.FollowingRelationship
+ alias Pleroma.Marker
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Pagination
@@ -34,11 +36,30 @@ defmodule Pleroma.Notification do
timestamps()
end
+ @spec unread_notifications_count(User.t()) :: integer()
+ def unread_notifications_count(%User{id: user_id}) do
+ from(q in __MODULE__,
+ where: q.user_id == ^user_id and q.seen == false
+ )
+ |> Repo.aggregate(:count, :id)
+ end
+
def changeset(%Notification{} = notification, attrs) do
notification
|> cast(attrs, [:seen])
end
+ @spec last_read_query(User.t()) :: Ecto.Queryable.t()
+ def last_read_query(user) do
+ from(q in Pleroma.Notification,
+ where: q.user_id == ^user.id,
+ where: q.seen == true,
+ select: type(q.id, :string),
+ limit: 1,
+ order_by: [desc: :id]
+ )
+ end
+
defp for_user_query_ap_id_opts(user, opts) do
ap_id_relationships =
[:block] ++
@@ -71,8 +92,9 @@ defmodule Pleroma.Notification do
|> join(:left, [n, a], object in Object,
on:
fragment(
- "(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)",
+ "(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')",
object.data,
+ a.data,
a.data
)
)
@@ -185,46 +207,41 @@ defmodule Pleroma.Notification do
|> Repo.all()
end
- def set_read_up_to(%{id: user_id} = _user, id) do
+ def set_read_up_to(%{id: user_id} = user, id) do
query =
from(
n in Notification,
where: n.user_id == ^user_id,
where: n.id <= ^id,
where: n.seen == false,
- update: [
- set: [
- seen: true,
- updated_at: ^NaiveDateTime.utc_now()
- ]
- ],
# Ideally we would preload object and activities here
# but Ecto does not support preloads in update_all
select: n.id
)
- {_, notification_ids} = Repo.update_all(query, [])
+ {:ok, %{ids: {_, notification_ids}}} =
+ Multi.new()
+ |> Multi.update_all(:ids, query, set: [seen: true, updated_at: NaiveDateTime.utc_now()])
+ |> Marker.multi_set_last_read_id(user, "notifications")
+ |> Repo.transaction()
- Notification
+ for_user_query(user)
|> 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()
end
+ @spec read_one(User.t(), String.t()) ::
+ {:ok, Notification.t()} | {:error, Ecto.Changeset.t()} | nil
def read_one(%User{} = user, notification_id) do
with {:ok, %Notification{} = notification} <- get(user, notification_id) do
- notification
- |> changeset(%{seen: true})
- |> Repo.update()
+ Multi.new()
+ |> Multi.update(:update, changeset(notification, %{seen: true}))
+ |> Marker.multi_set_last_read_id(user, "notifications")
+ |> Repo.transaction()
+ |> case do
+ {:ok, %{update: notification}} -> {:ok, notification}
+ {:error, :update, changeset, _} -> {:error, changeset}
+ end
end
end
@@ -316,8 +333,11 @@ defmodule Pleroma.Notification do
# TODO move to sql, too.
def create_notification(%Activity{} = activity, %User{} = user, do_send \\ true) do
unless skip?(activity, user) do
- notification = %Notification{user_id: user.id, activity: activity}
- {:ok, notification} = Repo.insert(notification)
+ {:ok, %{notification: notification}} =
+ Multi.new()
+ |> Multi.insert(:notification, %Notification{user_id: user.id, activity: activity})
+ |> Marker.multi_set_last_read_id(user, "notifications")
+ |> Repo.transaction()
if do_send do
Streamer.stream(["user", "user:notification"], notification)
@@ -339,15 +359,10 @@ defmodule Pleroma.Notification do
def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only)
when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReact"] do
- potential_receiver_ap_ids =
- []
- |> 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_receiver_ap_ids = get_potential_receiver_ap_ids(activity)
- 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 =
potential_receiver_ap_ids
@@ -363,6 +378,27 @@ defmodule Pleroma.Notification 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"
def exclude_domain_blocker_ap_ids(ap_ids, activity, preloaded_users \\ [])
diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex
index e678fd415..546c4ea01 100644
--- a/lib/pleroma/object.ex
+++ b/lib/pleroma/object.ex
@@ -9,11 +9,13 @@ defmodule Pleroma.Object do
import Ecto.Changeset
alias Pleroma.Activity
+ alias Pleroma.Config
alias Pleroma.Object
alias Pleroma.Object.Fetcher
alias Pleroma.ObjectTombstone
alias Pleroma.Repo
alias Pleroma.User
+ alias Pleroma.Workers.AttachmentsCleanupWorker
require Logger
@@ -138,12 +140,17 @@ defmodule Pleroma.Object do
def normalize(_, _, _), do: nil
- # Owned objects can only be mutated by their owner
- def authorize_mutation(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}),
- do: actor == ap_id
+ # Owned objects can only be accessed by their owner
+ def authorize_access(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}) do
+ if actor == ap_id do
+ :ok
+ else
+ {:error, :forbidden}
+ end
+ end
- # Legacy objects can be mutated by anybody
- def authorize_mutation(%Object{}, %User{}), do: true
+ # Legacy objects can be accessed by anybody
+ def authorize_access(%Object{}, %User{}), do: :ok
@spec get_cached_by_ap_id(String.t()) :: Object.t() | nil
def get_cached_by_ap_id(ap_id) do
@@ -183,27 +190,37 @@ defmodule Pleroma.Object do
def delete(%Object{data: %{"id" => id}} = object) do
with {:ok, _obj} = swap_object_with_tombstone(object),
deleted_activity = Activity.delete_all_by_object_ap_id(id),
- {:ok, true} <- Cachex.del(:object_cache, "object:#{id}"),
- {:ok, _} <- Cachex.del(:web_resp_cache, URI.parse(id).path) do
- with true <- Pleroma.Config.get([:instance, :cleanup_attachments]) do
- {:ok, _} =
- Pleroma.Workers.AttachmentsCleanupWorker.enqueue("cleanup_attachments", %{
- "object" => object
- })
- end
+ {:ok, _} <- invalid_object_cache(object) do
+ cleanup_attachments(
+ Config.get([:instance, :cleanup_attachments]),
+ %{"object" => object}
+ )
{:ok, object, deleted_activity}
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),
- {:ok, true} <- Cachex.del(:object_cache, "object:#{id}"),
- {:ok, _} <- Cachex.del(:web_resp_cache, URI.parse(id).path) do
+ {:ok, _} <- invalid_object_cache(object) do
{:ok, object}
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
Cachex.put(:object_cache, "object:#{ap_id}", object)
{:ok, object}
diff --git a/lib/pleroma/plugs/authentication_plug.ex b/lib/pleroma/plugs/authentication_plug.ex
index 0061c69dc..057ea42f1 100644
--- a/lib/pleroma/plugs/authentication_plug.ex
+++ b/lib/pleroma/plugs/authentication_plug.ex
@@ -3,7 +3,6 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.AuthenticationPlug do
- alias Comeonin.Pbkdf2
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
@@ -17,8 +16,13 @@ defmodule Pleroma.Plugs.AuthenticationPlug do
:crypt.crypt(password, password_hash) == password_hash
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
- Pbkdf2.checkpw(password, password_hash)
+ Pbkdf2.verify_pass(password, password_hash)
end
def checkpw(_password, _password_hash) do
@@ -26,6 +30,25 @@ defmodule Pleroma.Plugs.AuthenticationPlug do
false
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(
@@ -37,7 +60,9 @@ defmodule Pleroma.Plugs.AuthenticationPlug do
} = conn,
_
) do
- if Pbkdf2.checkpw(password, password_hash) do
+ if checkpw(password, password_hash) do
+ {:ok, auth_user} = maybe_update_password(auth_user, password)
+
conn
|> assign(:user, auth_user)
|> OAuthScopesPlug.skip_plug()
@@ -47,7 +72,7 @@ defmodule Pleroma.Plugs.AuthenticationPlug do
end
def call(%{assigns: %{auth_credentials: %{password: _}}} = conn, _) do
- Pbkdf2.dummy_checkpw()
+ Pbkdf2.no_user_verify()
conn
end
diff --git a/lib/pleroma/plugs/ensure_authenticated_plug.ex b/lib/pleroma/plugs/ensure_authenticated_plug.ex
index 9c8f5597f..3fe550806 100644
--- a/lib/pleroma/plugs/ensure_authenticated_plug.ex
+++ b/lib/pleroma/plugs/ensure_authenticated_plug.ex
@@ -15,26 +15,25 @@ defmodule Pleroma.Plugs.EnsureAuthenticatedPlug do
end
@impl true
- def perform(%{assigns: %{user: %User{}}} = conn, _) do
+ def perform(
+ %{
+ assigns: %{
+ auth_credentials: %{password: _},
+ user: %User{multi_factor_authentication_settings: %{enabled: true}}
+ }
+ } = conn,
+ _
+ ) do
conn
+ |> render_error(:forbidden, "Two-factor authentication enabled, you must use a access token.")
+ |> halt()
end
- def perform(conn, options) do
- perform =
- cond do
- options[:if_func] -> options[:if_func].()
- options[:unless_func] -> !options[:unless_func].()
- true -> true
- end
-
- if perform do
- fail(conn)
- else
- conn
- end
+ def perform(%{assigns: %{user: %User{}}} = conn, _) do
+ conn
end
- def fail(conn) do
+ def perform(conn, _) do
conn
|> render_error(:forbidden, "Invalid credentials.")
|> halt()
diff --git a/lib/pleroma/plugs/federating_plug.ex b/lib/pleroma/plugs/federating_plug.ex
index 7d947339f..09038f3c6 100644
--- a/lib/pleroma/plugs/federating_plug.ex
+++ b/lib/pleroma/plugs/federating_plug.ex
@@ -19,6 +19,9 @@ defmodule Pleroma.Web.FederatingPlug do
def federating?, do: Pleroma.Config.get([:instance, :federating])
+ # Definition for the use in :if_func / :unless_func plug options
+ def federating?(_conn), do: federating?()
+
defp fail(conn) do
conn
|> put_status(404)
diff --git a/lib/pleroma/scheduled_activity.ex b/lib/pleroma/scheduled_activity.ex
index 8ff06a462..0937cb7db 100644
--- a/lib/pleroma/scheduled_activity.ex
+++ b/lib/pleroma/scheduled_activity.ex
@@ -40,7 +40,7 @@ defmodule Pleroma.ScheduledActivity do
%{changes: %{params: %{"media_ids" => media_ids} = params}} = changeset
)
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
diff --git a/lib/pleroma/stats.ex b/lib/pleroma/stats.ex
index 8d2809bbb..6b3a8a41f 100644
--- a/lib/pleroma/stats.ex
+++ b/lib/pleroma/stats.ex
@@ -91,7 +91,7 @@ defmodule Pleroma.Stats do
peers: peers,
stats: %{
domain_count: domain_count,
- status_count: status_count,
+ status_count: status_count || 0,
user_count: user_count
}
}
diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex
index 762d813d9..1be1a3a5b 100644
--- a/lib/pleroma/upload.ex
+++ b/lib/pleroma/upload.ex
@@ -134,7 +134,7 @@ defmodule Pleroma.Upload do
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)
data = Base.decode64!(parsed["data"], ignore: :whitespace)
hash = String.downcase(Base.encode16(:crypto.hash(:sha256, data)))
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 99358ddaf..42c4c4e3e 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -9,7 +9,6 @@ defmodule Pleroma.User do
import Ecto.Query
import Ecto, only: [assoc: 2]
- alias Comeonin.Pbkdf2
alias Ecto.Multi
alias Pleroma.Activity
alias Pleroma.Config
@@ -20,6 +19,7 @@ defmodule Pleroma.User do
alias Pleroma.Formatter
alias Pleroma.HTML
alias Pleroma.Keys
+ alias Pleroma.MFA
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Registration
@@ -29,7 +29,9 @@ defmodule Pleroma.User do
alias Pleroma.UserRelationship
alias Pleroma.Web
alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
+ alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
@@ -113,7 +115,6 @@ defmodule Pleroma.User do
field(:is_admin, :boolean, default: false)
field(:show_role, :boolean, default: true)
field(:settings, :map, default: nil)
- field(:magic_key, :string, default: nil)
field(:uri, Types.Uri, default: nil)
field(:hide_followers_count, :boolean, default: false)
field(:hide_follows_count, :boolean, default: false)
@@ -189,6 +190,12 @@ defmodule Pleroma.User do
# `:subscribers` is deprecated (replaced with `subscriber_users` relation)
field(:subscribers, {:array, :string}, default: [])
+ embeds_one(
+ :multi_factor_authentication_settings,
+ MFA.Settings,
+ on_replace: :delete
+ )
+
timestamps()
end
@@ -298,8 +305,13 @@ defmodule Pleroma.User do
def avatar_url(user, options \\ []) do
case user.avatar do
- %{"url" => [%{"href" => href} | _]} -> href
- _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
+ %{"url" => [%{"href" => href} | _]} ->
+ href
+
+ _ ->
+ unless options[:no_default] do
+ Config.get([:assets, :default_user_avatar], "#{Web.base_url()}/images/avi.png")
+ end
end
end
@@ -387,7 +399,6 @@ defmodule Pleroma.User do
:banner,
:locked,
:last_refreshed_at,
- :magic_key,
:uri,
:follower_address,
:following_address,
@@ -743,7 +754,19 @@ defmodule Pleroma.User do
{:error, "Not subscribed!"}
end
+ @spec unfollow(User.t(), User.t()) :: {:ok, User.t(), Activity.t()} | {:error, String.t()}
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
state when state in [:follow_pending, :follow_accept] ->
FollowingRelationship.unfollow(follower, followed)
@@ -754,7 +777,7 @@ defmodule Pleroma.User do
|> update_following_count()
|> set_cache()
- {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
+ {:ok, follower, followed}
nil ->
{:error, "Not subscribed!"}
@@ -927,6 +950,7 @@ defmodule Pleroma.User do
end
end
+ @spec get_by_nickname(String.t()) :: User.t() | nil
def get_by_nickname(nickname) do
Repo.get_by(User, nickname: nickname) ||
if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
@@ -1184,8 +1208,9 @@ defmodule Pleroma.User do
def increment_unread_conversation_count(_, user), do: {:ok, user}
- @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
- def get_users_from_set(ap_ids, local_only \\ true) do
+ @spec get_users_from_set([String.t()], keyword()) :: [User.t()]
+ 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 = if local_only, do: Map.put(criteria, :local, true), else: criteria
@@ -1197,7 +1222,9 @@ defmodule Pleroma.User do
def get_recipients_from_activity(%Activity{recipients: to, actor: actor}) do
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()
end
@@ -1393,15 +1420,13 @@ defmodule Pleroma.User do
user
|> get_followers()
|> Enum.filter(& &1.local)
- |> Enum.each(fn follower ->
- follower |> update_following_count() |> set_cache()
- end)
+ |> Enum.each(&set_cache(update_following_count(&1)))
# Only update local user counts, remote will be update during the next pull.
user
|> get_friends()
|> Enum.filter(& &1.local)
- |> Enum.each(&update_follower_count/1)
+ |> Enum.each(&do_unfollow(user, &1))
{:ok, user}
end
@@ -1423,12 +1448,29 @@ defmodule Pleroma.User do
BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
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)
@spec perform(atom(), User.t()) :: {:ok, User.t()}
def perform(:delete, %User{} = user) do
- {:ok, _user} = ActivityPub.delete(user)
-
# Remove all relationships
user
|> get_followers()
@@ -1446,14 +1488,7 @@ defmodule Pleroma.User do
delete_user_activities(user)
- if user.local do
- user
- |> change(%{deactivated: true, email: nil})
- |> update_and_set_cache()
- else
- invalidate_cache(user)
- Repo.delete(user)
- end
+ delete_or_deactivate(user)
end
def perform(:deactivate_async, user, status), do: deactivate(user, status)
@@ -1538,37 +1573,42 @@ defmodule Pleroma.User do
})
end
- def delete_user_activities(%User{ap_id: ap_id}) do
+ def delete_user_activities(%User{ap_id: ap_id} = user) do
ap_id
|> Activity.Queries.by_actor()
|> RepoStreamer.chunk_stream(50)
- |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
+ |> Stream.each(fn activities ->
+ Enum.each(activities, fn activity -> delete_activity(activity, user) end)
+ end)
|> Stream.run()
end
- defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
- activity
- |> Object.normalize()
- |> ActivityPub.delete()
- end
-
- defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
- object = Object.normalize(activity)
+ defp delete_activity(%{data: %{"type" => "Create", "object" => object}} = activity, user) do
+ 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
- activity.actor
- |> get_cached_by_ap_id()
- |> ActivityPub.unlike(object)
+ e ->
+ Logger.error("Could not delete #{object} created by #{activity.data["ap_id"]}")
+ Logger.error("Error: #{inspect(e)}")
+ end
end
- defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
- object = Object.normalize(activity)
-
- activity.actor
- |> get_cached_by_ap_id()
- |> ActivityPub.unannounce(object)
+ defp delete_activity(%{data: %{"type" => type}} = activity, user)
+ when type in ["Like", "Announce"] do
+ {:ok, undo, _} = Builder.undo(user, activity)
+ Pipeline.common_pipeline(undo, local: user.local)
end
- defp delete_activity(_activity), do: "Doing nothing"
+ defp delete_activity(_activity, _user), do: "Doing nothing"
def html_filter_policy(%User{no_rich_text: true}) do
Pleroma.HTML.Scrubber.TwitterText
@@ -1579,12 +1619,19 @@ defmodule Pleroma.User do
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
- 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
- {:ok, user}
- else
- fetch_by_ap_id(ap_id)
+ maybe_fetched_user = needs_update?(cached_user) && fetch_by_ap_id(ap_id)
+
+ case {cached_user, maybe_fetched_user} do
+ {_, {:ok, %User{} = user}} ->
+ {:ok, user}
+
+ {%User{} = user, _} ->
+ {:ok, user}
+
+ _ ->
+ {:error, :not_found}
end
end
@@ -1915,7 +1962,7 @@ defmodule Pleroma.User do
defp put_password_hash(
%Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
) do
- change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
+ change(changeset, password_hash: Pbkdf2.hash_pwd_salt(password))
end
defp put_password_hash(changeset), do: changeset
diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex
index ac77aab71..293bbc082 100644
--- a/lib/pleroma/user/query.ex
+++ b/lib/pleroma/user/query.ex
@@ -45,6 +45,7 @@ defmodule Pleroma.User.Query do
is_admin: boolean(),
is_moderator: boolean(),
super_users: boolean(),
+ exclude_service_users: boolean(),
followers: User.t(),
friends: User.t(),
recipients_from_activity: [String.t()],
@@ -88,6 +89,10 @@ defmodule Pleroma.User.Query do
where(query, [u], ilike(field(u, ^key), ^"%#{value}%"))
end
+ defp compose_query({:exclude_service_users, _}, query) do
+ where(query, [u], not like(u.ap_id, "%/relay") and not like(u.ap_id, "%/internal/fetch"))
+ end
+
defp compose_query({key, value}, query)
when key in @equal_criteria and not_empty_string(value) do
where(query, [u], ^[{key, value}])
@@ -98,7 +103,7 @@ defmodule Pleroma.User.Query do
end
defp compose_query({:tags, tags}, query) when is_list(tags) and length(tags) > 0 do
- Enum.reduce(tags, query, &prepare_tag_criteria/2)
+ where(query, [u], fragment("? && ?", u.tags, ^tags))
end
defp compose_query({:is_admin, _}, query) do
@@ -162,20 +167,18 @@ defmodule Pleroma.User.Query do
end
defp compose_query({:recipients_from_activity, to}, query) do
- query
- |> join(:left, [u], r in FollowingRelationship,
- as: :relationships,
- on: r.follower_id == u.id
- )
- |> 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)
+ following_query =
+ from(u in User,
+ join: f in FollowingRelationship,
+ 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)
)
- |> distinct(true)
end
defp compose_query({:order_by, key}, query) do
@@ -192,10 +195,6 @@ defmodule Pleroma.User.Query do
defp compose_query(_unsupported_param, query), do: query
- defp prepare_tag_criteria(tag, query) do
- or_where(query, [u], fragment("? = any(?)", ^tag, u.tags))
- end
-
defp location_query(query, local) do
where(query, [u], u.local == ^local)
|> where([u], not is_nil(u.nickname))
diff --git a/lib/pleroma/user/welcome_message.ex b/lib/pleroma/user/welcome_message.ex
index f0ac8ebae..f8f520285 100644
--- a/lib/pleroma/user/welcome_message.ex
+++ b/lib/pleroma/user/welcome_message.ex
@@ -10,8 +10,8 @@ defmodule Pleroma.User.WelcomeMessage do
with %User{} = sender_user <- welcome_user(),
message when is_binary(message) <- welcome_message() do
CommonAPI.post(sender_user, %{
- "visibility" => "direct",
- "status" => "@#{user.nickname}\n#{message}"
+ visibility: "direct",
+ status: "@#{user.nickname}\n#{message}"
})
else
_ -> {:ok, nil}
diff --git a/lib/pleroma/user_relationship.ex b/lib/pleroma/user_relationship.ex
index 235ad427c..6dfdd2860 100644
--- a/lib/pleroma/user_relationship.ex
+++ b/lib/pleroma/user_relationship.ex
@@ -87,6 +87,22 @@ defmodule Pleroma.UserRelationship do
source_to_target_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
source_user_ids = User.binary_id(source_users)
target_user_ids = User.binary_id(target_users)
@@ -138,11 +154,16 @@ defmodule Pleroma.UserRelationship do
def view_relationships_option(%User{} = reading_user, actors, opts) do
{source_to_target_rel_types, target_to_source_rel_types} =
- if opts[:source_mutes_only] do
- # This option is used for rendering statuses (FE needs `muted` flag for each one anyways)
- {[:mute], []}
- else
- {[:block, :mute, :notification_mute, :reblog_mute], [:block, :inverse_subscription]}
+ case opts[:subset] do
+ :source_mutes ->
+ # Used for statuses rendering (FE needs `muted` flag for each status when statuses load)
+ {[:mute], []}
+
+ nil ->
+ {[:block, :mute, :notification_mute, :reblog_mute], [:block, :inverse_subscription]}
+
+ unknown ->
+ raise "Unsupported :subset option value: #{inspect(unknown)}"
end
user_relationships =
@@ -153,7 +174,17 @@ defmodule Pleroma.UserRelationship do
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}
end
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 1f4a09370..b8a2873d8 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -170,12 +170,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
BackgroundWorker.enqueue("fetch_data_for_activity", %{"activity_id" => activity.id})
- Notification.create_notifications(activity)
-
- conversation = create_or_bump_conversation(activity, map["actor"])
- participations = get_participations(conversation)
- stream_out(activity)
- stream_out_participations(participations)
{:ok, activity}
else
%Activity{} = activity ->
@@ -198,6 +192,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
+ def notify_and_stream(activity) do
+ Notification.create_notifications(activity)
+
+ conversation = create_or_bump_conversation(activity, activity.actor)
+ participations = get_participations(conversation)
+ stream_out(activity)
+ stream_out_participations(participations)
+ end
+
defp create_or_bump_conversation(activity, actor) do
with {:ok, conversation} <- Conversation.create_or_bump_for(activity),
%User{} = user <- User.get_cached_by_ap_id(actor),
@@ -274,6 +277,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
_ <- increase_poll_votes_if_vote(create_data),
{:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity},
{:ok, _actor} <- increase_note_count_if_public(actor, activity),
+ _ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
@@ -301,6 +305,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
additional
),
{:ok, activity} <- insert(listen_data, local),
+ _ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
@@ -325,6 +330,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
%{"to" => to, "type" => type, "actor" => actor.ap_id, "object" => object}
|> Utils.maybe_put("id", activity_id),
{:ok, activity} <- insert(data, local),
+ _ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
@@ -344,140 +350,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
},
data <- Utils.maybe_put(data, "id", activity_id),
{:ok, activity} <- insert(data, local),
+ _ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
- @spec react_with_emoji(User.t(), Object.t(), String.t(), keyword()) ::
- {:ok, Activity.t(), Object.t()} | {:error, any()}
- def react_with_emoji(user, object, emoji, options \\ []) do
- with {:ok, result} <-
- Repo.transaction(fn -> do_react_with_emoji(user, object, emoji, options) end) do
- result
- end
- end
-
- defp do_react_with_emoji(user, object, emoji, options) do
- with local <- Keyword.get(options, :local, true),
- activity_id <- Keyword.get(options, :activity_id, nil),
- true <- Pleroma.Emoji.is_unicode_emoji?(emoji),
- reaction_data <- make_emoji_reaction_data(user, object, emoji, activity_id),
- {:ok, activity} <- insert(reaction_data, local),
- {:ok, object} <- add_emoji_reaction_to_object(activity, object),
- :ok <- maybe_federate(activity) do
- {:ok, activity, object}
- else
- false -> {:error, false}
- {:error, error} -> Repo.rollback(error)
- end
- end
-
- @spec unreact_with_emoji(User.t(), String.t(), keyword()) ::
- {:ok, Activity.t(), Object.t()} | {:error, any()}
- def unreact_with_emoji(user, reaction_id, options \\ []) do
- with {:ok, result} <-
- Repo.transaction(fn -> do_unreact_with_emoji(user, reaction_id, options) end) do
- result
- end
- end
-
- defp do_unreact_with_emoji(user, reaction_id, options) do
- with local <- Keyword.get(options, :local, true),
- activity_id <- Keyword.get(options, :activity_id, nil),
- user_ap_id <- user.ap_id,
- %Activity{actor: ^user_ap_id} = reaction_activity <- Activity.get_by_ap_id(reaction_id),
- object <- Object.normalize(reaction_activity),
- unreact_data <- make_undo_data(user, reaction_activity, activity_id),
- {:ok, activity} <- insert(unreact_data, local),
- {:ok, object} <- remove_emoji_reaction_from_object(reaction_activity, object),
- :ok <- maybe_federate(activity) do
- {:ok, activity, object}
- else
- {:error, error} -> Repo.rollback(error)
- end
- end
-
- @spec unlike(User.t(), Object.t(), String.t() | nil, boolean()) ::
- {:ok, Activity.t(), Activity.t(), Object.t()} | {:ok, Object.t()} | {:error, any()}
- def unlike(%User{} = actor, %Object{} = object, activity_id \\ nil, local \\ true) do
- with {:ok, result} <-
- Repo.transaction(fn -> do_unlike(actor, object, activity_id, local) end) do
- result
- end
- end
-
- defp do_unlike(actor, object, activity_id, local) do
- with %Activity{} = like_activity <- get_existing_like(actor.ap_id, object),
- unlike_data <- make_unlike_data(actor, like_activity, activity_id),
- {:ok, unlike_activity} <- insert(unlike_data, local),
- {:ok, _activity} <- Repo.delete(like_activity),
- {:ok, object} <- remove_like_from_object(like_activity, object),
- :ok <- maybe_federate(unlike_activity) do
- {:ok, unlike_activity, like_activity, object}
- else
- nil -> {:ok, object}
- {:error, error} -> Repo.rollback(error)
- 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),
- :ok <- maybe_federate(activity) do
- {:ok, activity, object}
- else
- false -> {:error, false}
- {:error, error} -> Repo.rollback(error)
- end
- end
-
- @spec unannounce(User.t(), Object.t(), String.t() | nil, boolean()) ::
- {:ok, Activity.t(), Object.t()} | {:ok, Object.t()} | {:error, any()}
- def unannounce(
- %User{} = actor,
- %Object{} = object,
- activity_id \\ nil,
- local \\ true
- ) do
- with {:ok, result} <-
- Repo.transaction(fn -> do_unannounce(actor, object, activity_id, local) end) do
- result
- end
- end
-
- defp do_unannounce(actor, object, activity_id, local) do
- with %Activity{} = announce_activity <- get_existing_announce(actor.ap_id, object),
- unannounce_data <- make_unannounce_data(actor, announce_activity, activity_id),
- {:ok, unannounce_activity} <- insert(unannounce_data, local),
- :ok <- maybe_federate(unannounce_activity),
- {:ok, _activity} <- Repo.delete(announce_activity),
- {:ok, object} <- remove_announce_from_object(announce_activity, object) do
- {:ok, unannounce_activity, object}
- else
- nil -> {:ok, object}
- {:error, error} -> Repo.rollback(error)
- end
- end
-
@spec follow(User.t(), User.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t()} | {:error, any()}
def follow(follower, followed, activity_id \\ nil, local \\ true) do
@@ -490,6 +368,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp do_follow(follower, followed, activity_id, local) do
with data <- make_follow_data(follower, followed, activity_id),
{:ok, activity} <- insert(data, local),
+ _ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
@@ -511,6 +390,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
{:ok, follow_activity} <- update_follow_state(follow_activity, "cancelled"),
unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_id),
{:ok, activity} <- insert(unfollow_data, local),
+ _ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
@@ -519,67 +399,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
- @spec delete(User.t() | Object.t(), keyword()) :: {:ok, User.t() | Object.t()} | {:error, any()}
- def delete(entity, options \\ []) do
- with {:ok, result} <- Repo.transaction(fn -> do_delete(entity, options) end) do
- result
- end
- end
-
- defp do_delete(%User{ap_id: ap_id, follower_address: follower_address} = user, _) do
- with data <- %{
- "to" => [follower_address],
- "type" => "Delete",
- "actor" => ap_id,
- "object" => %{"type" => "Person", "id" => ap_id}
- },
- {:ok, activity} <- insert(data, true, true, true),
- :ok <- maybe_federate(activity) do
- {:ok, user}
- end
- end
-
- defp do_delete(%Object{data: %{"id" => id, "actor" => actor}} = object, options) do
- local = Keyword.get(options, :local, true)
- activity_id = Keyword.get(options, :activity_id, nil)
- actor = Keyword.get(options, :actor, actor)
-
- user = User.get_cached_by_ap_id(actor)
- to = (object.data["to"] || []) ++ (object.data["cc"] || [])
-
- with create_activity <- Activity.get_create_by_object_ap_id(id),
- data <-
- %{
- "type" => "Delete",
- "actor" => actor,
- "object" => id,
- "to" => to,
- "deleted_activity_id" => create_activity && create_activity.id
- }
- |> maybe_put("id", activity_id),
- {:ok, activity} <- insert(data, local, false),
- {:ok, object, _create_activity} <- Object.delete(object),
- stream_out_participations(object, user),
- _ <- decrease_replies_count_if_reply(object),
- {:ok, _actor} <- decrease_note_count_if_public(user, object),
- :ok <- maybe_federate(activity) do
- {:ok, activity}
- else
- {:error, error} ->
- Repo.rollback(error)
- end
- end
-
- defp do_delete(%Object{data: %{"type" => "Tombstone", "id" => ap_id}}, _) do
- activity =
- ap_id
- |> Activity.Queries.by_object_id()
- |> Activity.Queries.by_type("Delete")
- |> Repo.one()
-
- {:ok, activity}
- end
-
@spec block(User.t(), User.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t()} | {:error, any()}
def block(blocker, blocked, activity_id \\ nil, local \\ true) do
@@ -590,7 +409,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
defp do_block(blocker, blocked, activity_id, local) do
- outgoing_blocks = Config.get([:activitypub, :outgoing_blocks])
unfollow_blocked = Config.get([:activitypub, :unfollow_blocked])
if unfollow_blocked do
@@ -598,9 +416,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
if follow_activity, do: unfollow(blocker, blocked, nil, local)
end
- with true <- outgoing_blocks,
- block_data <- make_block_data(blocker, blocked, activity_id),
+ with block_data <- make_block_data(blocker, blocked, activity_id),
{:ok, activity} <- insert(block_data, local),
+ _ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
@@ -608,27 +426,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
- @spec unblock(User.t(), User.t(), String.t() | nil, boolean()) ::
- {:ok, Activity.t()} | {:error, any()} | nil
- def unblock(blocker, blocked, activity_id \\ nil, local \\ true) do
- with {:ok, result} <-
- Repo.transaction(fn -> do_unblock(blocker, blocked, activity_id, local) end) do
- result
- end
- end
-
- defp do_unblock(blocker, blocked, activity_id, local) do
- with %Activity{} = block_activity <- fetch_latest_block(blocker, blocked),
- unblock_data <- make_unblock_data(blocker, blocked, block_activity, activity_id),
- {:ok, activity} <- insert(unblock_data, local),
- :ok <- maybe_federate(activity) do
- {:ok, activity}
- else
- nil -> nil
- {:error, error} -> Repo.rollback(error)
- end
- end
-
@spec flag(map()) :: {:ok, Activity.t()} | {:error, any()}
def flag(
%{
@@ -655,6 +452,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
with flag_data <- make_flag_data(params, additional),
{:ok, activity} <- insert(flag_data, local),
{:ok, stripped_activity} <- strip_report_status_data(activity),
+ _ <- notify_and_stream(activity),
:ok <- maybe_federate(stripped_activity) do
User.all_superusers()
|> Enum.filter(fn user -> not is_nil(user.email) end)
@@ -678,7 +476,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
}
with true <- origin.ap_id in target.also_known_as,
- {:ok, activity} <- insert(params, local) do
+ {:ok, activity} <- insert(params, local),
+ _ <- notify_and_stream(activity) do
maybe_federate(activity)
BackgroundWorker.enqueue("move_following", %{
@@ -739,14 +538,27 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> Repo.one()
end
- @spec fetch_public_activities(map(), Pagination.type()) :: [Activity.t()]
- def fetch_public_activities(opts \\ %{}, pagination \\ :keyset) do
+ @spec fetch_public_or_unlisted_activities(map(), Pagination.type()) :: [Activity.t()]
+ def fetch_public_or_unlisted_activities(opts \\ %{}, pagination \\ :keyset) do
opts = Map.drop(opts, ["user"])
- [Constants.as_public()]
- |> fetch_activities_query(opts)
- |> restrict_unlisted()
- |> Pagination.fetch_paginated(opts, pagination)
+ query = fetch_activities_query([Constants.as_public()], opts)
+
+ query =
+ 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
@valid_visibilities ~w[direct unlisted public private]
@@ -1346,7 +1158,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> Activity.with_joined_object()
|> Object.with_joined_activity()
|> select([_like, object, activity], %{activity | object: object})
- |> order_by([like, _, _], desc: like.id)
+ |> order_by([like, _, _], desc_nulls_last: like.id)
|> Pagination.fetch_paginated(
Map.merge(params, %{"skip_order" => true}),
pagination,
@@ -1530,21 +1342,34 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp normalize_counter(counter) when is_integer(counter), do: counter
defp normalize_counter(_), do: 0
- defp maybe_update_follow_information(data) do
+ def maybe_update_follow_information(user_data) do
with {:enabled, true} <- {:enabled, Config.get([:instance, :external_user_synchronization])},
- {:ok, info} <- fetch_follow_information_for_user(data) do
- info = Map.merge(data[:info] || %{}, info)
- Map.put(data, :info, info)
+ {_, true} <- {:user_type_check, user_data[:type] in ["Person", "Service"]},
+ {_, true} <-
+ {:collections_available,
+ !!(user_data[:following_address] && user_data[:follower_address])},
+ {:ok, info} <-
+ fetch_follow_information_for_user(user_data) do
+ info = Map.merge(user_data[:info] || %{}, info)
+
+ user_data
+ |> Map.put(:info, info)
else
+ {:user_type_check, false} ->
+ user_data
+
+ {:collections_available, false} ->
+ user_data
+
{:enabled, false} ->
- data
+ user_data
e ->
Logger.error(
- "Follower/Following counter update for #{data.ap_id} failed.\n" <> inspect(e)
+ "Follower/Following counter update for #{user_data.ap_id} failed.\n" <> inspect(e)
)
- data
+ user_data
end
end
diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
index f607931ab..28727d619 100644
--- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
@@ -21,6 +21,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
alias Pleroma.Web.ActivityPub.UserView
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
+ alias Pleroma.Web.Endpoint
alias Pleroma.Web.FederatingPlug
alias Pleroma.Web.Federator
@@ -34,7 +35,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
plug(
EnsureAuthenticatedPlug,
- [unless_func: &FederatingPlug.federating?/0] when action not in @federating_only_actions
+ [unless_func: &FederatingPlug.federating?/1] when action not in @federating_only_actions
)
# Note: :following and :followers must be served even without authentication (as via :api)
@@ -75,8 +76,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
end
end
- def object(conn, %{"uuid" => uuid}) do
- with ap_id <- o_status_url(conn, :object, uuid),
+ def object(conn, _) do
+ with ap_id <- Endpoint.url() <> conn.request_path,
%Object{} = object <- Object.get_cached_by_ap_id(ap_id),
{_, true} <- {:public?, Visibility.is_public?(object)} do
conn
@@ -101,8 +102,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
conn
end
- def activity(conn, %{"uuid" => uuid}) do
- with ap_id <- o_status_url(conn, :activity, uuid),
+ def activity(conn, _params) do
+ with ap_id <- Endpoint.url() <> conn.request_path,
%Activity{} = activity <- Activity.normalize(ap_id),
{_, true} <- {:public?, Visibility.is_public?(activity)} do
conn
@@ -396,7 +397,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|> json(err)
end
- defp handle_user_activity(%User{} = user, %{"type" => "Create"} = params) do
+ defp handle_user_activity(
+ %User{} = user,
+ %{"type" => "Create", "object" => %{"type" => "Note"}} = params
+ ) do
object =
params["object"]
|> Map.merge(Map.take(params, ["to", "cc"]))
@@ -415,7 +419,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
defp handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do
with %Object{} = object <- Object.normalize(params["object"]),
true <- user.is_moderator || user.ap_id == object.data["actor"],
- {:ok, delete} <- ActivityPub.delete(object) do
+ {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
+ {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
{:ok, delete}
else
_ -> {:error, dgettext("errors", "Can't delete object")}
diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex
index 429a510b8..7ece764f5 100644
--- a/lib/pleroma/web/activity_pub/builder.ex
+++ b/lib/pleroma/web/activity_pub/builder.ex
@@ -10,8 +10,106 @@ defmodule Pleroma.Web.ActivityPub.Builder do
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
+ require Pleroma.Constants
+
+ @spec emoji_react(User.t(), Object.t(), String.t()) :: {:ok, map(), keyword()}
+ def emoji_react(actor, object, emoji) do
+ with {:ok, data, meta} <- object_action(actor, object) do
+ data =
+ data
+ |> Map.put("content", emoji)
+ |> Map.put("type", "EmojiReact")
+
+ {:ok, data, meta}
+ end
+ end
+
+ @spec undo(User.t(), Activity.t()) :: {:ok, map(), keyword()}
+ def undo(actor, object) do
+ {:ok,
+ %{
+ "id" => Utils.generate_activity_id(),
+ "actor" => actor.ap_id,
+ "type" => "Undo",
+ "object" => object.data["id"],
+ "to" => object.data["to"] || [],
+ "cc" => object.data["cc"] || []
+ }, []}
+ end
+
+ @spec delete(User.t(), String.t()) :: {:ok, map(), keyword()}
+ def delete(actor, object_id) do
+ object = Object.normalize(object_id, false)
+
+ user = !object && User.get_cached_by_ap_id(object_id)
+
+ to =
+ case {object, user} do
+ {%Object{}, _} ->
+ # We are deleting an object, address everyone who was originally mentioned
+ (object.data["to"] || []) ++ (object.data["cc"] || [])
+
+ {_, %User{follower_address: follower_address}} ->
+ # We are deleting a user, address the followers of that user
+ [follower_address]
+ end
+
+ {:ok,
+ %{
+ "id" => Utils.generate_activity_id(),
+ "actor" => actor.ap_id,
+ "object" => object_id,
+ "to" => to,
+ "type" => "Delete"
+ }, []}
+ 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()}
def like(actor, object) do
+ with {:ok, data, meta} <- object_action(actor, object) do
+ data =
+ data
+ |> Map.put("type", "Like")
+
+ {:ok, data, meta}
+ end
+ end
+
+ def announce(actor, object, options \\ []) do
+ public? = Keyword.get(options, :public, false)
+ to = [actor.follower_address, object.data["actor"]]
+
+ to =
+ if public? do
+ [Pleroma.Constants.as_public() | to]
+ else
+ to
+ 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()}
+ defp object_action(actor, object) do
object_actor = User.get_cached_by_ap_id(object.data["actor"])
# Address the actor of the object, and our actor's follower collection if the post is public.
@@ -33,7 +131,6 @@ defmodule Pleroma.Web.ActivityPub.Builder do
%{
"id" => Utils.generate_activity_id(),
"actor" => actor.ap_id,
- "type" => "Like",
"object" => object.data["id"],
"to" => to,
"cc" => cc,
diff --git a/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex b/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex
new file mode 100644
index 000000000..2858af9eb
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex
@@ -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
diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex
index dc4bce059..2599067a8 100644
--- a/lib/pleroma/web/activity_pub/object_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validator.ex
@@ -11,11 +11,36 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
alias Pleroma.Object
alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.Types
+ alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator
@spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
def validate(object, meta)
+ def validate(%{"type" => "Undo"} = object, meta) do
+ with {:ok, object} <-
+ object
+ |> UndoValidator.cast_and_validate()
+ |> Ecto.Changeset.apply_action(:insert) do
+ object = stringify_keys(object)
+ {:ok, object, meta}
+ end
+ end
+
+ def validate(%{"type" => "Delete"} = object, meta) do
+ with cng <- DeleteValidator.cast_and_validate(object),
+ do_not_federate <- DeleteValidator.do_not_federate?(cng),
+ {:ok, object} <- Ecto.Changeset.apply_action(cng, :insert) do
+ object = stringify_keys(object)
+ meta = Keyword.put(meta, :do_not_federate, do_not_federate)
+ {:ok, object, meta}
+ end
+ end
+
def validate(%{"type" => "Like"} = object, meta) do
with {:ok, object} <-
object |> LikeValidator.cast_and_validate() |> Ecto.Changeset.apply_action(:insert) do
@@ -24,14 +49,46 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
end
end
+ def validate(%{"type" => "EmojiReact"} = object, meta) do
+ with {:ok, object} <-
+ object
+ |> EmojiReactValidator.cast_and_validate()
+ |> Ecto.Changeset.apply_action(:insert) do
+ object = stringify_keys(object |> Map.from_struct())
+ {:ok, object, meta}
+ 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
+ object
+ |> Map.from_struct()
+ |> stringify_keys
+ end
+
def stringify_keys(object) do
object
|> Map.new(fn {key, val} -> {to_string(key), val} end)
end
+ def fetch_actor(object) do
+ with {:ok, actor} <- Types.ObjectID.cast(object["actor"]) do
+ User.get_or_fetch_by_ap_id(actor)
+ end
+ end
+
def fetch_actor_and_object(object) do
- User.get_or_fetch_by_ap_id(object["actor"])
- Object.normalize(object["object"])
+ fetch_actor(object)
+ Object.normalize(object["object"], true)
:ok
end
end
diff --git a/lib/pleroma/web/activity_pub/object_validators/announce_validator.ex b/lib/pleroma/web/activity_pub/object_validators/announce_validator.ex
new file mode 100644
index 000000000..40f861f47
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/announce_validator.ex
@@ -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
diff --git a/lib/pleroma/web/activity_pub/object_validators/common_validations.ex b/lib/pleroma/web/activity_pub/object_validators/common_validations.ex
index b479c3918..aeef31945 100644
--- a/lib/pleroma/web/activity_pub/object_validators/common_validations.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/common_validations.ex
@@ -5,10 +5,33 @@
defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
import Ecto.Changeset
+ alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.User
- def validate_actor_presence(cng, field_name \\ :actor) do
+ def validate_recipients_presence(cng, fields \\ [:to, :cc]) do
+ non_empty =
+ fields
+ |> Enum.map(fn field -> get_field(cng, field) end)
+ |> Enum.any?(fn
+ [] -> false
+ _ -> true
+ end)
+
+ if non_empty do
+ cng
+ else
+ fields
+ |> Enum.reduce(cng, fn field, cng ->
+ cng
+ |> add_error(field, "no recipients in any field")
+ end)
+ end
+ end
+
+ def validate_actor_presence(cng, options \\ []) do
+ field_name = Keyword.get(options, :field_name, :actor)
+
cng
|> validate_change(field_name, fn field_name, actor ->
if User.get_cached_by_ap_id(actor) do
@@ -19,14 +42,39 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
end)
end
- def validate_object_presence(cng, field_name \\ :object) do
+ def validate_object_presence(cng, options \\ []) do
+ field_name = Keyword.get(options, :field_name, :object)
+ allowed_types = Keyword.get(options, :allowed_types, false)
+
cng
- |> validate_change(field_name, fn field_name, object ->
- if Object.get_cached_by_ap_id(object) do
- []
- else
- [{field_name, "can't find object"}]
+ |> validate_change(field_name, fn field_name, object_id ->
+ object = Object.get_cached_by_ap_id(object_id) || Activity.get_by_ap_id(object_id)
+
+ cond do
+ !object ->
+ [{field_name, "can't find object"}]
+
+ object && allowed_types && object.data["type"] not in allowed_types ->
+ [{field_name, "object not in allowed types"}]
+
+ true ->
+ []
end
end)
end
+
+ def validate_object_or_user_presence(cng, options \\ []) do
+ field_name = Keyword.get(options, :field_name, :object)
+ options = Keyword.put(options, :field_name, field_name)
+
+ actor_cng =
+ cng
+ |> validate_actor_presence(options)
+
+ object_cng =
+ cng
+ |> validate_object_presence(options)
+
+ if actor_cng.valid?, do: actor_cng, else: object_cng
+ end
end
diff --git a/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex b/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex
new file mode 100644
index 000000000..f42c03510
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex
@@ -0,0 +1,100 @@
+# 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.DeleteValidator do
+ use Ecto.Schema
+
+ alias Pleroma.Activity
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ObjectValidators.Types
+
+ import Ecto.Changeset
+ import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
+
+ @primary_key false
+
+ embedded_schema do
+ field(:id, Types.ObjectID, primary_key: true)
+ field(:type, :string)
+ field(:actor, Types.ObjectID)
+ field(:to, Types.Recipients, default: [])
+ field(:cc, Types.Recipients, default: [])
+ field(:deleted_activity_id, Types.ObjectID)
+ field(:object, Types.ObjectID)
+ end
+
+ def cast_data(data) do
+ %__MODULE__{}
+ |> cast(data, __schema__(:fields))
+ end
+
+ def add_deleted_activity_id(cng) do
+ object =
+ cng
+ |> get_field(:object)
+
+ with %Activity{id: id} <- Activity.get_create_by_object_ap_id(object) do
+ cng
+ |> put_change(:deleted_activity_id, id)
+ else
+ _ -> cng
+ end
+ end
+
+ @deletable_types ~w{
+ Answer
+ Article
+ Audio
+ Event
+ Note
+ Page
+ Question
+ Video
+ Tombstone
+ }
+ def validate_data(cng) do
+ cng
+ |> validate_required([:id, :type, :actor, :to, :cc, :object])
+ |> validate_inclusion(:type, ["Delete"])
+ |> validate_actor_presence()
+ |> validate_deletion_rights()
+ |> validate_object_or_user_presence(allowed_types: @deletable_types)
+ |> add_deleted_activity_id()
+ end
+
+ def do_not_federate?(cng) do
+ !same_domain?(cng)
+ end
+
+ defp same_domain?(cng) do
+ actor_uri =
+ cng
+ |> get_field(:actor)
+ |> URI.parse()
+
+ object_uri =
+ cng
+ |> get_field(:object)
+ |> URI.parse()
+
+ object_uri.host == actor_uri.host
+ end
+
+ def validate_deletion_rights(cng) do
+ actor = User.get_cached_by_ap_id(get_field(cng, :actor))
+
+ if User.superuser?(actor) || same_domain?(cng) do
+ cng
+ else
+ cng
+ |> add_error(:actor, "is not allowed to delete object")
+ end
+ end
+
+ def cast_and_validate(data) do
+ data
+ |> cast_data
+ |> validate_data
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex b/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex
new file mode 100644
index 000000000..e87519c59
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex
@@ -0,0 +1,81 @@
+# 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.EmojiReactValidator do
+ use Ecto.Schema
+
+ alias Pleroma.Object
+ alias Pleroma.Web.ActivityPub.ObjectValidators.Types
+
+ import Ecto.Changeset
+ import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
+
+ @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)
+ field(:content, :string)
+ field(:to, {:array, :string}, default: [])
+ field(:cc, {:array, :string}, default: [])
+ 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
+ |> fix_context()
+ end
+
+ def fix_context(cng) do
+ object = get_field(cng, :object)
+
+ with nil <- get_field(cng, :context),
+ %Object{data: %{"context" => context}} <- Object.get_cached_by_ap_id(object) do
+ cng
+ |> put_change(:context, context)
+ else
+ _ ->
+ cng
+ end
+ end
+
+ def validate_emoji(cng) do
+ content = get_field(cng, :content)
+
+ if Pleroma.Emoji.is_unicode_emoji?(content) do
+ cng
+ else
+ cng
+ |> add_error(:content, "must be a single character emoji")
+ end
+ end
+
+ def validate_data(data_cng) do
+ data_cng
+ |> validate_inclusion(:type, ["EmojiReact"])
+ |> validate_required([:id, :type, :object, :actor, :context, :to, :cc, :content])
+ |> validate_actor_presence()
+ |> validate_object_presence()
+ |> validate_emoji()
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/object_validators/like_validator.ex b/lib/pleroma/web/activity_pub/object_validators/like_validator.ex
index 49546ceaa..034f25492 100644
--- a/lib/pleroma/web/activity_pub/object_validators/like_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/like_validator.ex
@@ -5,6 +5,7 @@
defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator do
use Ecto.Schema
+ alias Pleroma.Object
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
alias Pleroma.Web.ActivityPub.Utils
@@ -19,8 +20,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator do
field(:object, Types.ObjectID)
field(:actor, Types.ObjectID)
field(:context, :string)
- field(:to, {:array, :string})
- field(:cc, {:array, :string})
+ field(:to, Types.Recipients, default: [])
+ field(:cc, Types.Recipients, default: [])
end
def cast_and_validate(data) do
@@ -31,7 +32,48 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator do
def cast_data(data) do
%__MODULE__{}
- |> cast(data, [:id, :type, :object, :actor, :context, :to, :cc])
+ |> changeset(data)
+ end
+
+ def changeset(struct, data) do
+ struct
+ |> cast(data, __schema__(:fields))
+ |> fix_after_cast()
+ end
+
+ def fix_after_cast(cng) do
+ cng
+ |> fix_recipients()
+ |> fix_context()
+ end
+
+ def fix_context(cng) do
+ object = get_field(cng, :object)
+
+ with nil <- get_field(cng, :context),
+ %Object{data: %{"context" => context}} <- Object.get_cached_by_ap_id(object) do
+ cng
+ |> put_change(:context, context)
+ else
+ _ ->
+ cng
+ end
+ end
+
+ def fix_recipients(cng) do
+ to = get_field(cng, :to)
+ cc = get_field(cng, :cc)
+ object = get_field(cng, :object)
+
+ with {[], []} <- {to, cc},
+ %Object{data: %{"actor" => actor}} <- Object.get_cached_by_ap_id(object),
+ {:ok, actor} <- Types.ObjectID.cast(actor) do
+ cng
+ |> put_change(:to, [actor])
+ else
+ _ ->
+ cng
+ end
end
def validate_data(data_cng) do
diff --git a/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex b/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex
new file mode 100644
index 000000000..48fe61e1a
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex
@@ -0,0 +1,34 @@
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.Recipients do
+ use Ecto.Type
+
+ alias Pleroma.Web.ActivityPub.ObjectValidators.Types.ObjectID
+
+ def type, do: {:array, ObjectID}
+
+ def cast(object) when is_binary(object) do
+ cast([object])
+ end
+
+ def cast(data) when is_list(data) do
+ data
+ |> Enum.reduce({:ok, []}, fn element, acc ->
+ case {acc, ObjectID.cast(element)} do
+ {:error, _} -> :error
+ {_, :error} -> :error
+ {{:ok, list}, {:ok, id}} -> {:ok, [id | list]}
+ end
+ end)
+ end
+
+ def cast(_) do
+ :error
+ end
+
+ def dump(data) do
+ {:ok, data}
+ end
+
+ def load(data) do
+ {:ok, data}
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/object_validators/undo_validator.ex b/lib/pleroma/web/activity_pub/object_validators/undo_validator.ex
new file mode 100644
index 000000000..d0ba418e8
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/undo_validator.ex
@@ -0,0 +1,62 @@
+# 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.UndoValidator do
+ use Ecto.Schema
+
+ alias Pleroma.Activity
+ alias Pleroma.Web.ActivityPub.ObjectValidators.Types
+
+ import Ecto.Changeset
+ import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
+
+ @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(:to, {:array, :string}, default: [])
+ field(:cc, {:array, :string}, default: [])
+ 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))
+ end
+
+ def validate_data(data_cng) do
+ data_cng
+ |> validate_inclusion(:type, ["Undo"])
+ |> validate_required([:id, :type, :object, :actor, :to, :cc])
+ |> validate_actor_presence()
+ |> validate_object_presence()
+ |> validate_undo_rights()
+ end
+
+ def validate_undo_rights(cng) do
+ actor = get_field(cng, :actor)
+ object = get_field(cng, :object)
+
+ with %Activity{data: %{"actor" => object_actor}} <- Activity.get_by_ap_id(object),
+ true <- object_actor != actor do
+ cng
+ |> add_error(:actor, "not the same as object actor")
+ else
+ _ -> cng
+ end
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/pipeline.ex b/lib/pleroma/web/activity_pub/pipeline.ex
index 7ccee54c9..0c54c4b23 100644
--- a/lib/pleroma/web/activity_pub/pipeline.ex
+++ b/lib/pleroma/web/activity_pub/pipeline.ex
@@ -4,20 +4,34 @@
defmodule Pleroma.Web.ActivityPub.Pipeline do
alias Pleroma.Activity
+ alias Pleroma.Config
+ alias Pleroma.Object
+ alias Pleroma.Repo
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.ActivityPub.ObjectValidator
alias Pleroma.Web.ActivityPub.SideEffects
alias Pleroma.Web.Federator
- @spec common_pipeline(map(), keyword()) :: {:ok, Activity.t(), keyword()} | {:error, any()}
+ @spec common_pipeline(map(), keyword()) ::
+ {:ok, Activity.t() | Object.t(), keyword()} | {:error, any()}
def common_pipeline(object, meta) do
+ case Repo.transaction(fn -> do_common_pipeline(object, meta) end) do
+ {:ok, value} ->
+ value
+
+ {:error, e} ->
+ {:error, e}
+ end
+ end
+
+ def do_common_pipeline(object, meta) do
with {_, {:ok, validated_object, meta}} <-
{:validate_object, ObjectValidator.validate(object, meta)},
{_, {:ok, mrfd_object}} <- {:mrf_object, MRF.filter(validated_object)},
- {_, {:ok, %Activity{} = activity, meta}} <-
+ {_, {:ok, activity, meta}} <-
{:persist_object, ActivityPub.persist(mrfd_object, meta)},
- {_, {:ok, %Activity{} = activity, meta}} <-
+ {_, {:ok, activity, meta}} <-
{:execute_side_effects, SideEffects.handle(activity, meta)},
{_, {:ok, _}} <- {:federation, maybe_federate(activity, meta)} do
{:ok, activity, meta}
@@ -27,9 +41,13 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do
end
end
- defp maybe_federate(activity, meta) do
+ defp maybe_federate(%Object{}, _), do: {:ok, :not_federated}
+
+ defp maybe_federate(%Activity{} = activity, meta) do
with {:ok, local} <- Keyword.fetch(meta, :local) do
- if local do
+ do_not_federate = meta[:do_not_federate] || !Config.get([:instance, :federating])
+
+ if !do_not_federate && local do
Federator.publish(activity)
{:ok, :federated}
else
diff --git a/lib/pleroma/web/activity_pub/relay.ex b/lib/pleroma/web/activity_pub/relay.ex
index 729c23af7..484178edd 100644
--- a/lib/pleroma/web/activity_pub/relay.ex
+++ b/lib/pleroma/web/activity_pub/relay.ex
@@ -4,9 +4,10 @@
defmodule Pleroma.Web.ActivityPub.Relay do
alias Pleroma.Activity
- alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Visibility
+ alias Pleroma.Web.CommonAPI
require Logger
@relay_nickname "relay"
@@ -48,11 +49,11 @@ defmodule Pleroma.Web.ActivityPub.Relay do
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
with %User{} = user <- get_actor(),
- %Object{} = object <- Object.normalize(activity) do
- ActivityPub.announce(user, object, nil, true, false)
+ true <- Visibility.is_public?(activity) do
+ CommonAPI.repeat(activity.id, user)
else
error -> format_error(error)
end
diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex
index 5981e7545..7eae0c52c 100644
--- a/lib/pleroma/web/activity_pub/side_effects.ex
+++ b/lib/pleroma/web/activity_pub/side_effects.ex
@@ -5,8 +5,12 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
liked object, a `Follow` activity will add the user to the follower
collection, and so on.
"""
+ alias Pleroma.Activity
alias Pleroma.Notification
alias Pleroma.Object
+ alias Pleroma.Repo
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
def handle(object, meta \\ [])
@@ -15,21 +19,130 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
# - Add like to object
# - Set up notification
def handle(%{data: %{"type" => "Like"}} = object, meta) do
- {:ok, result} =
- Pleroma.Repo.transaction(fn ->
- liked_object = Object.get_by_ap_id(object.data["object"])
- Utils.add_like_to_object(object, liked_object)
+ liked_object = Object.get_by_ap_id(object.data["object"])
+ Utils.add_like_to_object(object, liked_object)
- Notification.create_notifications(object)
+ Notification.create_notifications(object)
- {:ok, object, meta}
- end)
+ {:ok, object, meta}
+ 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"])
+
+ Utils.add_announce_to_object(object, announced_object)
+
+ Notification.create_notifications(object)
+ ActivityPub.stream_out(object)
+
+ {:ok, object, meta}
+ end
+
+ def handle(%{data: %{"type" => "Undo", "object" => undone_object}} = object, meta) do
+ with undone_object <- Activity.get_by_ap_id(undone_object),
+ :ok <- handle_undoing(undone_object) do
+ {:ok, object, meta}
+ end
+ end
+
+ # Tasks this handles:
+ # - Add reaction to object
+ # - Set up notification
+ def handle(%{data: %{"type" => "EmojiReact"}} = object, meta) do
+ reacted_object = Object.get_by_ap_id(object.data["object"])
+ Utils.add_emoji_reaction_to_object(object, reacted_object)
+
+ Notification.create_notifications(object)
+
+ {:ok, object, meta}
+ end
+
+ # Tasks this handles:
+ # - Delete and unpins the create activity
+ # - Replace object with Tombstone
+ # - Set up notification
+ # - Reduce the user note count
+ # - Reduce the reply count
+ # - Stream out the activity
+ def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object, meta) do
+ deleted_object =
+ Object.normalize(deleted_object, false) || User.get_cached_by_ap_id(deleted_object)
+
+ result =
+ case deleted_object do
+ %Object{} ->
+ with {:ok, deleted_object, activity} <- Object.delete(deleted_object),
+ %User{} = user <- User.get_cached_by_ap_id(deleted_object.data["actor"]) do
+ User.remove_pinnned_activity(user, activity)
+
+ {:ok, user} = ActivityPub.decrease_note_count_if_public(user, deleted_object)
+
+ if in_reply_to = deleted_object.data["inReplyTo"] do
+ Object.decrease_replies_count(in_reply_to)
+ end
+
+ ActivityPub.stream_out(object)
+ ActivityPub.stream_out_participations(deleted_object, user)
+ :ok
+ end
+
+ %User{} ->
+ with {:ok, _} <- User.delete(deleted_object) do
+ :ok
+ end
+ end
- result
+ if result == :ok do
+ Notification.create_notifications(object)
+ {:ok, object, meta}
+ else
+ {:error, result}
+ end
end
# Nothing to do
def handle(object, meta) do
{:ok, object, meta}
end
+
+ def handle_undoing(%{data: %{"type" => "Like"}} = object) do
+ with %Object{} = liked_object <- Object.get_by_ap_id(object.data["object"]),
+ {:ok, _} <- Utils.remove_like_from_object(object, liked_object),
+ {:ok, _} <- Repo.delete(object) do
+ :ok
+ end
+ end
+
+ def handle_undoing(%{data: %{"type" => "EmojiReact"}} = object) do
+ with %Object{} = reacted_object <- Object.get_by_ap_id(object.data["object"]),
+ {:ok, _} <- Utils.remove_emoji_reaction_from_object(object, reacted_object),
+ {:ok, _} <- Repo.delete(object) do
+ :ok
+ end
+ end
+
+ def handle_undoing(%{data: %{"type" => "Announce"}} = object) do
+ with %Object{} = liked_object <- Object.get_by_ap_id(object.data["object"]),
+ {:ok, _} <- Utils.remove_announce_from_object(object, liked_object),
+ {:ok, _} <- Repo.delete(object) do
+ :ok
+ end
+ end
+
+ def handle_undoing(
+ %{data: %{"type" => "Block", "actor" => blocker, "object" => blocked}} = object
+ ) do
+ with %User{} = blocker <- User.get_cached_by_ap_id(blocker),
+ %User{} = blocked <- User.get_cached_by_ap_id(blocked),
+ {:ok, _} <- User.unblock(blocker, blocked),
+ {:ok, _} <- Repo.delete(object) do
+ :ok
+ end
+ end
+
+ def handle_undoing(object), do: {:error, ["don't know how to handle", object]}
end
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index c966ec960..8443c284c 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -14,8 +14,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.ObjectValidator
- alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.Types
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
@@ -591,6 +592,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
{: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"]),
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept) do
+ User.update_follower_count(followed)
+ User.update_following_count(follower)
+
ActivityPub.accept(%{
to: follow_activity.data["to"],
type: "Accept",
@@ -600,7 +604,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
activity_id: id
})
else
- _e -> :error
+ _e ->
+ :error
end
end
@@ -657,17 +662,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> handle_incoming(options)
end
- def handle_incoming(%{"type" => "Like"} = data, _options) do
- with {_, {:ok, cast_data_sym}} <-
- {:casting_data,
- data |> LikeValidator.cast_data() |> Ecto.Changeset.apply_action(:insert)},
- cast_data = ObjectValidator.stringify_keys(Map.from_struct(cast_data_sym)),
- :ok <- ObjectValidator.fetch_actor_and_object(cast_data),
- {_, {:ok, cast_data}} <- {:ensure_context_presence, ensure_context_presence(cast_data)},
- {_, {:ok, cast_data}} <-
- {:ensure_recipients_presence, ensure_recipients_presence(cast_data)},
- {_, {:ok, activity, _meta}} <-
- {:common_pipeline, Pipeline.common_pipeline(cast_data, local: false)} do
+ def handle_incoming(%{"type" => type} = data, _options)
+ when type in ["Like", "EmojiReact", "Announce"] do
+ with :ok <- ObjectValidator.fetch_actor_and_object(data),
+ {:ok, activity, _meta} <-
+ Pipeline.common_pipeline(data, local: false) do
{:ok, activity}
else
e -> {:error, e}
@@ -675,42 +674,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
def handle_incoming(
- %{
- "type" => "EmojiReact",
- "object" => object_id,
- "actor" => _actor,
- "id" => id,
- "content" => emoji
- } = data,
- _options
- ) do
- with actor <- Containment.get_actor(data),
- {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
- {:ok, object} <- get_obj_helper(object_id),
- {:ok, activity, _object} <-
- ActivityPub.react_with_emoji(actor, object, emoji, activity_id: id, local: false) do
- {:ok, activity}
- else
- _e -> :error
- 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(
%{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
data,
_options
@@ -743,55 +706,25 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
end
- # TODO: We presently assume that any actor on the same origin domain as the object being
- # deleted has the rights to delete that object. A better way to validate whether or not
- # the object should be deleted is to refetch the object URI, which should return either
- # an error or a tombstone. This would allow us to verify that a deletion actually took
- # place.
def handle_incoming(
- %{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => id} = data,
+ %{"type" => "Delete"} = data,
_options
) do
- object_id = Utils.get_ap_id(object_id)
-
- with actor <- Containment.get_actor(data),
- {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
- {:ok, object} <- get_obj_helper(object_id),
- :ok <- Containment.contain_origin(actor.ap_id, object.data),
- {:ok, activity} <-
- ActivityPub.delete(object, local: false, activity_id: id, actor: actor.ap_id) do
+ with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
{:ok, activity}
else
- nil ->
- case User.get_cached_by_ap_id(object_id) do
- %User{ap_id: ^actor} = user ->
- User.delete(user)
-
- nil ->
- :error
+ {: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
-
- _e ->
- :error
- end
- end
-
- def handle_incoming(
- %{
- "type" => "Undo",
- "object" => %{"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_obj_helper(object_id),
- {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
- {:ok, activity}
- else
- _e -> :error
end
end
@@ -817,39 +750,29 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def handle_incoming(
%{
"type" => "Undo",
- "object" => %{"type" => "EmojiReact", "id" => reaction_activity_id},
- "actor" => _actor,
- "id" => id
+ "object" => %{"type" => type}
} = data,
_options
- ) do
- with actor <- Containment.get_actor(data),
- {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
- {:ok, activity, _} <-
- ActivityPub.unreact_with_emoji(actor, reaction_activity_id,
- activity_id: id,
- local: false
- ) do
+ )
+ when type in ["Like", "EmojiReact", "Announce", "Block"] do
+ with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
{:ok, activity}
- else
- _e -> :error
end
end
+ # For Undos that don't have the complete object attached, try to find it in our database.
def handle_incoming(
%{
"type" => "Undo",
- "object" => %{"type" => "Block", "object" => blocked},
- "actor" => blocker,
- "id" => id
- } = _data,
- _options
- ) do
- with %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
- {:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
- {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
- User.unblock(blocker, blocked)
- {:ok, activity}
+ "object" => object
+ } = activity,
+ options
+ )
+ when is_binary(object) do
+ with %Activity{data: data} <- Activity.get_by_ap_id(object) do
+ activity
+ |> Map.put("object", data)
+ |> handle_incoming(options)
else
_e -> :error
end
@@ -872,43 +795,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def handle_incoming(
%{
- "type" => "Undo",
- "object" => %{"type" => "Like", "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_obj_helper(object_id),
- {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
- {:ok, activity}
- else
- _e -> :error
- end
- end
-
- # For Undos that don't have the complete object attached, try to find it in our database.
- def handle_incoming(
- %{
- "type" => "Undo",
- "object" => object
- } = activity,
- options
- )
- when is_binary(object) do
- with %Activity{data: data} <- Activity.get_by_ap_id(object) do
- activity
- |> Map.put("object", data)
- |> handle_incoming(options)
- else
- _e -> :error
- end
- end
-
- def handle_incoming(
- %{
"type" => "Move",
"actor" => origin_actor,
"object" => origin_actor,
@@ -1159,10 +1045,14 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
Map.put(object, "tag", tags)
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
- {enabled_receivers, disabled_receivers} = Utils.get_notified_from_object(object)
- potential_receivers = enabled_receivers ++ disabled_receivers
- mentions = Enum.map(potential_receivers, &build_mention_tag/1)
+ to = object["to"] || []
+ cc = object["cc"] || []
+ mentioned = User.get_users_from_set(to ++ cc, local_only: false)
+
+ mentions = Enum.map(mentioned, &build_mention_tag/1)
tags = object["tag"] || []
Map.put(object, "tag", tags ++ mentions)
@@ -1203,6 +1093,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
Map.put(object, "conversation", object["context"])
end
+ def set_sensitive(%{"sensitive" => true} = object) do
+ object
+ end
+
def set_sensitive(object) do
tags = object["tag"] || []
Map.put(object, "sensitive", "nsfw" in tags)
@@ -1296,45 +1190,4 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def maybe_fix_user_url(data), do: data
def maybe_fix_user_object(data), do: maybe_fix_user_url(data)
-
- defp ensure_context_presence(%{"context" => context} = data) when is_binary(context),
- do: {:ok, data}
-
- defp ensure_context_presence(%{"object" => object} = data) when is_binary(object) do
- with %{data: %{"context" => context}} when is_binary(context) <- Object.normalize(object) do
- {:ok, Map.put(data, "context", context)}
- else
- _ ->
- {:error, :no_context}
- end
- end
-
- defp ensure_context_presence(_) do
- {:error, :no_context}
- end
-
- defp ensure_recipients_presence(%{"to" => [_ | _], "cc" => [_ | _]} = data),
- do: {:ok, data}
-
- defp ensure_recipients_presence(%{"object" => object} = data) do
- case Object.normalize(object) do
- %{data: %{"actor" => actor}} ->
- data =
- data
- |> Map.put("to", [actor])
- |> Map.put("cc", data["cc"] || [])
-
- {:ok, data}
-
- nil ->
- {:error, :no_object}
-
- _ ->
- {:error, :no_actor}
- end
- end
-
- defp ensure_recipients_presence(_) do
- {:error, :no_object}
- end
end
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index 2d685ecc0..f2375bcc4 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
alias Ecto.Changeset
alias Ecto.UUID
alias Pleroma.Activity
+ alias Pleroma.Config
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
@@ -169,8 +170,11 @@ defmodule Pleroma.Web.ActivityPub.Utils do
Enqueues an activity for federation if it's local
"""
@spec maybe_federate(any()) :: :ok
- def maybe_federate(%Activity{local: true} = activity) do
- if Pleroma.Config.get!([:instance, :federating]) do
+ def maybe_federate(%Activity{local: true, data: %{"type" => type}} = activity) 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)
end
@@ -512,7 +516,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
#### Announce-related helpers
@doc """
- Retruns an existing announce activity if the notice has already been announced
+ Returns an existing announce activity if the notice has already been announced
"""
@spec get_existing_announce(String.t(), map()) :: Activity.t() | nil
def get_existing_announce(actor, %{data: %{"id" => ap_id}}) do
@@ -562,45 +566,6 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|> maybe_put("id", activity_id)
end
- @doc """
- Make unannounce activity data for the given actor and object
- """
- def make_unannounce_data(
- %User{ap_id: ap_id} = user,
- %Activity{data: %{"context" => context, "object" => object}} = activity,
- activity_id
- ) do
- object = Object.normalize(object)
-
- %{
- "type" => "Undo",
- "actor" => ap_id,
- "object" => activity.data,
- "to" => [user.follower_address, object.data["actor"]],
- "cc" => [Pleroma.Constants.as_public()],
- "context" => context
- }
- |> maybe_put("id", activity_id)
- end
-
- def make_unlike_data(
- %User{ap_id: ap_id} = user,
- %Activity{data: %{"context" => context, "object" => object}} = activity,
- activity_id
- ) do
- object = Object.normalize(object)
-
- %{
- "type" => "Undo",
- "actor" => ap_id,
- "object" => activity.data,
- "to" => [user.follower_address, object.data["actor"]],
- "cc" => [Pleroma.Constants.as_public()],
- "context" => context
- }
- |> maybe_put("id", activity_id)
- end
-
def make_undo_data(
%User{ap_id: actor, follower_address: follower_address},
%Activity{
@@ -688,16 +653,6 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|> maybe_put("id", activity_id)
end
- def make_unblock_data(blocker, blocked, block_activity, activity_id) do
- %{
- "type" => "Undo",
- "actor" => blocker.ap_id,
- "to" => [blocked.ap_id],
- "object" => block_activity.data
- }
- |> maybe_put("id", activity_id)
- end
-
#### Create-related helpers
def make_create_data(params, additional) do
diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
index 816c11e01..6b1d64a2e 100644
--- a/lib/pleroma/web/admin_api/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
@@ -10,6 +10,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.ConfigDB
+ alias Pleroma.MFA
alias Pleroma.ModerationLog
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.ReportNote
@@ -17,8 +18,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
alias Pleroma.User
alias Pleroma.UserInviteToken
alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Builder
+ alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Utils
+ alias Pleroma.Web.AdminAPI
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.AdminAPI.ConfigView
alias Pleroma.Web.AdminAPI.ModerationLogView
@@ -27,14 +31,14 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
alias Pleroma.Web.AdminAPI.Search
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Endpoint
+ alias Pleroma.Web.MastodonAPI
alias Pleroma.Web.MastodonAPI.AppView
- alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.Router
require Logger
- @descriptions_json Pleroma.Docs.JSON.compile()
+ @descriptions Pleroma.Docs.JSON.compile()
@users_page_size 50
plug(
@@ -59,6 +63,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
:right_add,
:right_add_multiple,
:right_delete,
+ :disable_mfa,
:right_delete_multiple,
:update_user_credentials
]
@@ -93,13 +98,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
plug(
OAuthScopesPlug,
%{scopes: ["read:statuses"], admin: true}
- when action in [:list_statuses, :list_user_statuses, :list_instance_statuses]
- )
-
- plug(
- OAuthScopesPlug,
- %{scopes: ["write:statuses"], admin: true}
- when action in [:status_update, :status_delete]
+ when action in [:list_user_statuses, :list_instance_statuses]
)
plug(
@@ -131,25 +130,22 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
]
)
- action_fallback(:errors)
-
- def user_delete(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
- user = User.get_cached_by_nickname(nickname)
- User.delete(user)
-
- ModerationLog.insert_log(%{
- actor: admin,
- subject: [user],
- action: "delete"
- })
+ action_fallback(AdminAPI.FallbackController)
- conn
- |> json(nickname)
+ def user_delete(conn, %{"nickname" => nickname}) do
+ user_delete(conn, %{"nicknames" => [nickname]})
end
def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
- users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
- User.delete(users)
+ users =
+ nicknames
+ |> Enum.map(&User.get_cached_by_nickname/1)
+
+ users
+ |> Enum.each(fn user ->
+ {:ok, delete_data, _} = Builder.delete(admin, user.ap_id)
+ Pipeline.common_pipeline(delete_data, local: true)
+ end)
ModerationLog.insert_log(%{
actor: admin,
@@ -279,8 +275,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
})
conn
- |> put_view(Pleroma.Web.AdminAPI.StatusView)
- |> render("index.json", %{activities: activities, as: :activity, skip_relationships: false})
+ |> put_view(AdminAPI.StatusView)
+ |> render("index.json", %{activities: activities, as: :activity})
end
def list_user_statuses(conn, %{"nickname" => nickname} = params) do
@@ -298,8 +294,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
})
conn
- |> put_view(StatusView)
- |> render("index.json", %{activities: activities, as: :activity, skip_relationships: false})
+ |> put_view(MastodonAPI.StatusView)
+ |> render("index.json", %{activities: activities, as: :activity})
else
_ -> {:error, :not_found}
end
@@ -392,29 +388,12 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
email: params["email"]
}
- with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)),
- {:ok, users, count} <- filter_service_users(users, count),
- do:
- conn
- |> json(
- AccountView.render("index.json",
- users: users,
- count: count,
- page_size: page_size
- )
- )
- end
-
- defp filter_service_users(users, count) do
- filtered_users = Enum.reject(users, &service_user?/1)
- count = if Enum.any?(users, &service_user?/1), do: length(filtered_users), else: count
-
- {:ok, filtered_users, count}
- end
-
- defp service_user?(user) do
- String.match?(user.ap_id, ~r/.*\/relay$/) or
- String.match?(user.ap_id, ~r/.*\/internal\/fetch$/)
+ with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)) do
+ json(
+ conn,
+ AccountView.render("index.json", users: users, count: count, page_size: page_size)
+ )
+ end
end
@filters ~w(local external active deactivated is_admin is_moderator)
@@ -612,16 +591,10 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController 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."}
- )
+ {: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."}
- )
+ {:error, "To send invites you need to set the `invites_enabled` option to true."}
end
end
@@ -692,6 +665,18 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
json_response(conn, :no_content, "")
end
+ @doc "Disable mfa for user's account."
+ def disable_mfa(conn, %{"nickname" => nickname}) do
+ case User.get_by_nickname(nickname) do
+ %User{} = user ->
+ MFA.disable(user)
+ json(conn, nickname)
+
+ _ ->
+ {:error, :not_found}
+ end
+ end
+
@doc "Show a given user's credentials"
def show_user_credentials(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
@@ -817,56 +802,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
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_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
-
def list_log(conn, params) do
{page, page_size} = page_params(params)
@@ -886,13 +821,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
end
def config_descriptions(conn, _params) do
- conn
- |> Plug.Conn.put_resp_content_type("application/json")
- |> Plug.Conn.send_resp(200, @descriptions_json)
+ descriptions = Enum.filter(@descriptions, &whitelisted_config?/1)
+
+ json(conn, descriptions)
end
def config_show(conn, %{"only_db" => true}) do
- with :ok <- configurable_from_database(conn) do
+ with :ok <- configurable_from_database() do
configs = Pleroma.Repo.all(ConfigDB)
conn
@@ -902,7 +837,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
end
def config_show(conn, _params) do
- with :ok <- configurable_from_database(conn) do
+ with :ok <- configurable_from_database() do
configs = ConfigDB.get_all_as_keyword()
merged =
@@ -941,9 +876,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
end
def config_update(conn, %{"configs" => configs}) do
- with :ok <- configurable_from_database(conn) do
+ with :ok <- configurable_from_database() do
{_errors, results} =
- Enum.map(configs, fn
+ 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"]})
@@ -983,7 +920,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
end
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)
json(conn, %{})
@@ -994,17 +931,36 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
json(conn, %{need_reboot: Restarter.Pleroma.need_reboot?()})
end
- defp configurable_from_database(conn) do
+ defp configurable_from_database do
if Config.get(:configurable_from_database) do
:ok
else
- errors(
- conn,
- {:error, "To use this endpoint you need to enable configuration from database."}
- )
+ {: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
+
def reload_emoji(conn, _params) do
Pleroma.Emoji.reload()
@@ -1123,30 +1079,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|> json(%{"status_visibility" => count})
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
{get_page(params["page"]), get_page_size(params["page_size"])}
end
diff --git a/lib/pleroma/web/admin_api/controllers/fallback_controller.ex b/lib/pleroma/web/admin_api/controllers/fallback_controller.ex
new file mode 100644
index 000000000..82965936d
--- /dev/null
+++ b/lib/pleroma/web/admin_api/controllers/fallback_controller.ex
@@ -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
diff --git a/lib/pleroma/web/admin_api/controllers/status_controller.ex b/lib/pleroma/web/admin_api/controllers/status_controller.ex
new file mode 100644
index 000000000..08cb9c10b
--- /dev/null
+++ b/lib/pleroma/web/admin_api/controllers/status_controller.ex
@@ -0,0 +1,79 @@
+# 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
+ conn
+ |> put_view(MastodonAPI.StatusView)
+ |> render("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
diff --git a/lib/pleroma/web/admin_api/search.ex b/lib/pleroma/web/admin_api/search.ex
index 29cea1f44..c28efadd5 100644
--- a/lib/pleroma/web/admin_api/search.ex
+++ b/lib/pleroma/web/admin_api/search.ex
@@ -21,6 +21,7 @@ defmodule Pleroma.Web.AdminAPI.Search do
query =
params
|> Map.drop([:page, :page_size])
+ |> Map.put(:exclude_service_users, true)
|> User.Query.build()
|> order_by([u], u.nickname)
diff --git a/lib/pleroma/web/admin_api/views/account_view.ex b/lib/pleroma/web/admin_api/views/account_view.ex
index a16a3ebf0..46dadb5ee 100644
--- a/lib/pleroma/web/admin_api/views/account_view.ex
+++ b/lib/pleroma/web/admin_api/views/account_view.ex
@@ -6,7 +6,9 @@ defmodule Pleroma.Web.AdminAPI.AccountView do
use Pleroma.Web, :view
alias Pleroma.User
+ alias Pleroma.Web.AdminAPI
alias Pleroma.Web.AdminAPI.AccountView
+ alias Pleroma.Web.MastodonAPI
alias Pleroma.Web.MediaProxy
def render("index.json", %{users: users, count: count, page_size: page_size}) do
@@ -119,6 +121,13 @@ defmodule Pleroma.Web.AdminAPI.AccountView do
}
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(errors) do
diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex
index d50969b2a..f432b8c2c 100644
--- a/lib/pleroma/web/admin_api/views/report_view.ex
+++ b/lib/pleroma/web/admin_api/views/report_view.ex
@@ -7,10 +7,13 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
alias Pleroma.HTML
alias Pleroma.User
+ alias Pleroma.Web.AdminAPI
alias Pleroma.Web.AdminAPI.Report
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.StatusView
+ defdelegate merge_account_views(user), to: AdminAPI.AccountView
+
def render("index.json", %{reports: reports}) do
%{
reports:
@@ -41,8 +44,7 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
statuses:
StatusView.render("index.json", %{
activities: statuses,
- as: :activity,
- skip_relationships: false
+ as: :activity
}),
state: report.data["state"],
notes: render(__MODULE__, "index_notes.json", %{notes: report.report_notes})
@@ -70,11 +72,4 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
created_at: Utils.to_masto_date(inserted_at)
}
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
diff --git a/lib/pleroma/web/admin_api/views/status_view.ex b/lib/pleroma/web/admin_api/views/status_view.ex
index 3637dee24..500800be2 100644
--- a/lib/pleroma/web/admin_api/views/status_view.ex
+++ b/lib/pleroma/web/admin_api/views/status_view.ex
@@ -7,24 +7,19 @@ defmodule Pleroma.Web.AdminAPI.StatusView do
require Pleroma.Constants
- alias Pleroma.User
- alias Pleroma.Web.MastodonAPI.StatusView
+ alias Pleroma.Web.AdminAPI
+ alias Pleroma.Web.MastodonAPI
+
+ defdelegate merge_account_views(user), to: AdminAPI.AccountView
def render("index.json", opts) do
safe_render_many(opts.activities, __MODULE__, "show.json", opts)
end
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)})
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
diff --git a/lib/pleroma/web/api_spec.ex b/lib/pleroma/web/api_spec.ex
index b3c1e3ea2..79fd5f871 100644
--- a/lib/pleroma/web/api_spec.ex
+++ b/lib/pleroma/web/api_spec.ex
@@ -39,7 +39,12 @@ defmodule Pleroma.Web.ApiSpec do
password: %OpenApiSpex.OAuthFlow{
authorizationUrl: "/oauth/authorize",
tokenUrl: "/oauth/token",
- scopes: %{"read" => "read", "write" => "write", "follow" => "follow"}
+ scopes: %{
+ "read" => "read",
+ "write" => "write",
+ "follow" => "follow",
+ "push" => "push"
+ }
}
}
}
diff --git a/lib/pleroma/web/api_spec/cast_and_validate.ex b/lib/pleroma/web/api_spec/cast_and_validate.ex
new file mode 100644
index 000000000..bd9026237
--- /dev/null
+++ b/lib/pleroma/web/api_spec/cast_and_validate.ex
@@ -0,0 +1,139 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2019-2020 Moxley Stratton, Mike Buhot <https://github.com/open-api-spex/open_api_spex>, MPL-2.0
+# Copyright © 2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.CastAndValidate do
+ @moduledoc """
+ This plug is based on [`OpenApiSpex.Plug.CastAndValidate`]
+ (https://github.com/open-api-spex/open_api_spex/blob/master/lib/open_api_spex/plug/cast_and_validate.ex).
+ The main difference is ignoring unexpected query params instead of throwing
+ an error and a config option (`[Pleroma.Web.ApiSpec.CastAndValidate, :strict]`)
+ to disable this behavior. Also, the default rendering error module
+ is `Pleroma.Web.ApiSpec.RenderError`.
+ """
+
+ @behaviour Plug
+
+ alias Plug.Conn
+
+ @impl Plug
+ def init(opts) do
+ opts
+ |> Map.new()
+ |> Map.put_new(:render_error, Pleroma.Web.ApiSpec.RenderError)
+ end
+
+ @impl Plug
+ def call(%{private: %{open_api_spex: private_data}} = conn, %{
+ operation_id: operation_id,
+ render_error: render_error
+ }) do
+ spec = private_data.spec
+ operation = private_data.operation_lookup[operation_id]
+
+ content_type =
+ case Conn.get_req_header(conn, "content-type") do
+ [header_value | _] ->
+ header_value
+ |> String.split(";")
+ |> List.first()
+
+ _ ->
+ nil
+ end
+
+ private_data = Map.put(private_data, :operation_id, operation_id)
+ conn = Conn.put_private(conn, :open_api_spex, private_data)
+
+ case cast_and_validate(spec, operation, conn, content_type, strict?()) do
+ {:ok, conn} ->
+ conn
+
+ {:error, reason} ->
+ opts = render_error.init(reason)
+
+ conn
+ |> render_error.call(opts)
+ |> Plug.Conn.halt()
+ end
+ end
+
+ def call(
+ %{
+ private: %{
+ phoenix_controller: controller,
+ phoenix_action: action,
+ open_api_spex: private_data
+ }
+ } = conn,
+ opts
+ ) do
+ operation =
+ case private_data.operation_lookup[{controller, action}] do
+ nil ->
+ operation_id = controller.open_api_operation(action).operationId
+ operation = private_data.operation_lookup[operation_id]
+
+ operation_lookup =
+ private_data.operation_lookup
+ |> Map.put({controller, action}, operation)
+
+ OpenApiSpex.Plug.Cache.adapter().put(
+ private_data.spec_module,
+ {private_data.spec, operation_lookup}
+ )
+
+ operation
+
+ operation ->
+ operation
+ end
+
+ if operation.operationId do
+ call(conn, Map.put(opts, :operation_id, operation.operationId))
+ else
+ raise "operationId was not found in action API spec"
+ end
+ end
+
+ def call(conn, opts), do: OpenApiSpex.Plug.CastAndValidate.call(conn, opts)
+
+ defp cast_and_validate(spec, operation, conn, content_type, true = _strict) do
+ OpenApiSpex.cast_and_validate(spec, operation, conn, content_type)
+ end
+
+ defp cast_and_validate(spec, operation, conn, content_type, false = _strict) do
+ case OpenApiSpex.cast_and_validate(spec, operation, conn, content_type) do
+ {:ok, conn} ->
+ {:ok, conn}
+
+ # Remove unexpected query params and cast/validate again
+ {:error, errors} ->
+ query_params =
+ Enum.reduce(errors, conn.query_params, fn
+ %{reason: :unexpected_field, name: name, path: [name]}, params ->
+ Map.delete(params, name)
+
+ %{reason: :invalid_enum, name: nil, path: path, value: value}, params ->
+ path = path |> Enum.reverse() |> tl() |> Enum.reverse() |> list_items_to_string()
+ update_in(params, path, &List.delete(&1, value))
+
+ _, params ->
+ params
+ end)
+
+ conn = %Conn{conn | query_params: query_params}
+ OpenApiSpex.cast_and_validate(spec, operation, conn, content_type)
+ end
+ end
+
+ defp list_items_to_string(list) do
+ Enum.map(list, fn
+ i when is_atom(i) -> to_string(i)
+ i -> i
+ end)
+ end
+
+ defp strict?, do: Pleroma.Config.get([__MODULE__, :strict], false)
+end
diff --git a/lib/pleroma/web/api_spec/helpers.ex b/lib/pleroma/web/api_spec/helpers.ex
index 183df43ee..a9cfe0fed 100644
--- a/lib/pleroma/web/api_spec/helpers.ex
+++ b/lib/pleroma/web/api_spec/helpers.ex
@@ -5,6 +5,7 @@
defmodule Pleroma.Web.ApiSpec.Helpers do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
def request_body(description, schema_ref, opts \\ []) do
media_types = ["application/json", "multipart/form-data", "application/x-www-form-urlencoded"]
@@ -47,6 +48,15 @@ defmodule Pleroma.Web.ApiSpec.Helpers do
]
end
+ def with_relationships_param do
+ Operation.parameter(
+ :with_relationships,
+ :query,
+ BooleanLike,
+ "Embed relationships into accounts."
+ )
+ end
+
def empty_object_response do
Operation.response("Empty object", "application/json", %Schema{type: :object, example: %{}})
end
@@ -54,4 +64,8 @@ defmodule Pleroma.Web.ApiSpec.Helpers do
def empty_array_response do
Operation.response("Empty array", "application/json", %Schema{type: :array, example: []})
end
+
+ def no_content_response do
+ Operation.response("No Content", "application/json", %Schema{type: :string, example: ""})
+ end
end
diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex
index fe9548b1b..20572f8ea 100644
--- a/lib/pleroma/web/api_spec/operations/account_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/account_operation.ex
@@ -11,6 +11,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
alias Pleroma.Web.ApiSpec.Schemas.ActorType
alias Pleroma.Web.ApiSpec.Schemas.ApiError
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
+ alias Pleroma.Web.ApiSpec.Schemas.List
alias Pleroma.Web.ApiSpec.Schemas.Status
alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
@@ -154,8 +155,10 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
security: [%{"oAuth" => ["read:accounts"]}],
description:
"Accounts which follow the given account, if network is not hidden by the account owner.",
- parameters:
- [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}] ++ pagination_params(),
+ parameters: [
+ %Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
+ with_relationships_param() | pagination_params()
+ ],
responses: %{
200 => Operation.response("Accounts", "application/json", array_of_accounts())
}
@@ -170,8 +173,10 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
security: [%{"oAuth" => ["read:accounts"]}],
description:
"Accounts which the given account is following, if network is not hidden by the account owner.",
- parameters:
- [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}] ++ pagination_params(),
+ parameters: [
+ %Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
+ with_relationships_param() | pagination_params()
+ ],
responses: %{200 => Operation.response("Accounts", "application/json", array_of_accounts())}
}
end
@@ -366,15 +371,18 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
title: "AccountCreateRequest",
description: "POST body for creating an account",
type: :object,
+ required: [:username, :password, :agreement],
properties: %{
reason: %Schema{
type: :string,
+ nullable: true,
description:
"Text that will be reviewed by moderators if registrations require manual approval"
},
username: %Schema{type: :string, description: "The desired username for the account"},
email: %Schema{
type: :string,
+ nullable: true,
description:
"The email address to be used for login. Required when `account_activation_required` is enabled.",
format: :email
@@ -385,29 +393,39 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
format: :password
},
agreement: %Schema{
- type: :boolean,
+ allOf: [BooleanLike],
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."
},
locale: %Schema{
type: :string,
+ nullable: true,
description: "The language of the confirmation email that will be sent"
},
# Pleroma-specific properties:
- fullname: %Schema{type: :string, description: "Full name"},
- bio: %Schema{type: :string, description: "Bio", default: ""},
+ fullname: %Schema{type: :string, nullable: true, description: "Full name"},
+ bio: %Schema{type: :string, description: "Bio", nullable: true, default: ""},
captcha_solution: %Schema{
type: :string,
+ nullable: true,
description: "Provider-specific captcha solution"
},
- captcha_token: %Schema{type: :string, description: "Provider-specific captcha token"},
- captcha_answer_data: %Schema{type: :string, description: "Provider-specific captcha data"},
+ captcha_token: %Schema{
+ type: :string,
+ nullable: true,
+ description: "Provider-specific captcha token"
+ },
+ captcha_answer_data: %Schema{
+ type: :string,
+ nullable: true,
+ description: "Provider-specific captcha data"
+ },
token: %Schema{
type: :string,
+ nullable: true,
description: "Invite token required when the registrations aren't public"
}
},
- required: [:username, :password, :agreement],
example: %{
"username" => "cofe",
"email" => "cofe@example.com",
@@ -445,29 +463,35 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
type: :object,
properties: %{
bot: %Schema{
- type: :boolean,
+ allOf: [BooleanLike],
+ nullable: true,
description: "Whether the account has a bot flag."
},
display_name: %Schema{
type: :string,
+ nullable: true,
description: "The display name to use for the profile."
},
note: %Schema{type: :string, description: "The account bio."},
avatar: %Schema{
type: :string,
+ nullable: true,
description: "Avatar image encoded using multipart/form-data",
format: :binary
},
header: %Schema{
type: :string,
+ nullable: true,
description: "Header image encoded using multipart/form-data",
format: :binary
},
locked: %Schema{
- type: :boolean,
+ allOf: [BooleanLike],
+ nullable: true,
description: "Whether manual approval of follow requests is required."
},
fields_attributes: %Schema{
+ nullable: true,
oneOf: [
%Schema{type: :array, items: attribute_field()},
%Schema{type: :object, additionalProperties: %Schema{type: attribute_field()}}
@@ -486,48 +510,66 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
# Pleroma-specific fields
no_rich_text: %Schema{
- type: :boolean,
+ allOf: [BooleanLike],
+ nullable: true,
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_follows: %Schema{type: :boolean, description: "user's follows will be hidden"},
+ hide_followers: %Schema{
+ 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{
- type: :boolean,
+ allOf: [BooleanLike],
+ nullable: true,
description: "user's follower count will be hidden"
},
hide_follows_count: %Schema{
- type: :boolean,
+ allOf: [BooleanLike],
+ nullable: true,
description: "user's follow count will be hidden"
},
hide_favorites: %Schema{
- type: :boolean,
+ allOf: [BooleanLike],
+ nullable: true,
description: "user's favorites timeline will be hidden"
},
show_role: %Schema{
- type: :boolean,
+ allOf: [BooleanLike],
+ nullable: true,
description: "user's role (e.g admin, moderator) will be exposed to anyone in the
API"
},
default_scope: VisibilityScope,
pleroma_settings_store: %Schema{
type: :object,
+ nullable: true,
description: "Opaque user settings to be saved on the backend."
},
skip_thread_containment: %Schema{
- type: :boolean,
+ allOf: [BooleanLike],
+ nullable: true,
description: "Skip filtering out broken threads"
},
allow_following_move: %Schema{
- type: :boolean,
+ allOf: [BooleanLike],
+ nullable: true,
description: "Allows automatically follow moved following accounts"
},
pleroma_background_image: %Schema{
type: :string,
+ nullable: true,
description: "Sets the background image of the user.",
format: :binary
},
discoverable: %Schema{
- type: :boolean,
+ allOf: [BooleanLike],
+ nullable: true,
description:
"Discovery of this account in search results and other services is allowed."
},
@@ -555,11 +597,12 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
}
end
- defp array_of_accounts do
+ def array_of_accounts do
%Schema{
title: "ArrayOfAccounts",
type: :array,
- items: Account
+ items: Account,
+ example: [Account.schema().example]
}
end
@@ -622,7 +665,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
description: "POST body for muting an account",
type: :object,
properties: %{
- uri: %Schema{type: :string, format: :uri}
+ uri: %Schema{type: :string, nullable: true, format: :uri}
},
required: [:uri]
}
@@ -635,7 +678,8 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
type: :object,
properties: %{
notifications: %Schema{
- type: :boolean,
+ allOf: [BooleanLike],
+ nullable: true,
description: "Mute notifications in addition to statuses? Defaults to true.",
default: true
}
@@ -646,28 +690,12 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
}
end
- defp list do
- %Schema{
- title: "List",
- description: "Response schema for a list",
- type: :object,
- properties: %{
- id: %Schema{type: :string},
- title: %Schema{type: :string}
- },
- example: %{
- "id" => "123",
- "title" => "my list"
- }
- }
- end
-
defp array_of_lists do
%Schema{
title: "ArrayOfLists",
description: "Response schema for lists",
type: :array,
- items: list(),
+ items: List,
example: [
%{"id" => "123", "title" => "my list"},
%{"id" => "1337", "title" => "anotehr list"}
diff --git a/lib/pleroma/web/api_spec/operations/admin/status_operation.ex b/lib/pleroma/web/api_spec/operations/admin/status_operation.ex
new file mode 100644
index 000000000..0b138dc79
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/admin/status_operation.ex
@@ -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
+
+ defp 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
diff --git a/lib/pleroma/web/api_spec/operations/app_operation.ex b/lib/pleroma/web/api_spec/operations/app_operation.ex
index f6ccd073f..ae01cbbec 100644
--- a/lib/pleroma/web/api_spec/operations/app_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/app_operation.ex
@@ -105,7 +105,11 @@ defmodule Pleroma.Web.ApiSpec.AppOperation do
description: "Space separated list of scopes",
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],
example: %{
diff --git a/lib/pleroma/web/api_spec/operations/conversation_operation.ex b/lib/pleroma/web/api_spec/operations/conversation_operation.ex
new file mode 100644
index 000000000..475468893
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/conversation_operation.ex
@@ -0,0 +1,61 @@
+# 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.ConversationOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.Conversation
+ alias Pleroma.Web.ApiSpec.Schemas.FlakeID
+
+ 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: ["Conversations"],
+ summary: "Show conversation",
+ security: [%{"oAuth" => ["read:statuses"]}],
+ operationId: "ConversationController.index",
+ parameters: [
+ Operation.parameter(
+ :recipients,
+ :query,
+ %Schema{type: :array, items: FlakeID},
+ "Only return conversations with the given recipients (a list of user ids)"
+ )
+ | pagination_params()
+ ],
+ responses: %{
+ 200 =>
+ Operation.response("Array of Conversation", "application/json", %Schema{
+ type: :array,
+ items: Conversation,
+ example: [Conversation.schema().example]
+ })
+ }
+ }
+ end
+
+ def mark_as_read_operation do
+ %Operation{
+ tags: ["Conversations"],
+ summary: "Mark as read",
+ operationId: "ConversationController.mark_as_read",
+ parameters: [
+ Operation.parameter(:id, :path, :string, "Conversation ID",
+ example: "123",
+ required: true
+ )
+ ],
+ security: [%{"oAuth" => ["write:conversations"]}],
+ responses: %{
+ 200 => Operation.response("Conversation", "application/json", Conversation)
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex b/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex
new file mode 100644
index 000000000..1a49fece0
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex
@@ -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
diff --git a/lib/pleroma/web/api_spec/operations/filter_operation.ex b/lib/pleroma/web/api_spec/operations/filter_operation.ex
new file mode 100644
index 000000000..31e576f99
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/filter_operation.ex
@@ -0,0 +1,230 @@
+# 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.FilterOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Helpers
+ alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def index_operation do
+ %Operation{
+ tags: ["apps"],
+ summary: "View all filters",
+ operationId: "FilterController.index",
+ security: [%{"oAuth" => ["read:filters"]}],
+ responses: %{
+ 200 => Operation.response("Filters", "application/json", array_of_filters())
+ }
+ }
+ end
+
+ def create_operation do
+ %Operation{
+ tags: ["apps"],
+ summary: "Create a filter",
+ operationId: "FilterController.create",
+ requestBody: Helpers.request_body("Parameters", create_request(), required: true),
+ security: [%{"oAuth" => ["write:filters"]}],
+ responses: %{200 => Operation.response("Filter", "application/json", filter())}
+ }
+ end
+
+ def show_operation do
+ %Operation{
+ tags: ["apps"],
+ summary: "View all filters",
+ parameters: [id_param()],
+ operationId: "FilterController.show",
+ security: [%{"oAuth" => ["read:filters"]}],
+ responses: %{
+ 200 => Operation.response("Filter", "application/json", filter())
+ }
+ }
+ end
+
+ def update_operation do
+ %Operation{
+ tags: ["apps"],
+ summary: "Update a filter",
+ parameters: [id_param()],
+ operationId: "FilterController.update",
+ requestBody: Helpers.request_body("Parameters", update_request(), required: true),
+ security: [%{"oAuth" => ["write:filters"]}],
+ responses: %{
+ 200 => Operation.response("Filter", "application/json", filter())
+ }
+ }
+ end
+
+ def delete_operation do
+ %Operation{
+ tags: ["apps"],
+ summary: "Remove a filter",
+ parameters: [id_param()],
+ operationId: "FilterController.delete",
+ security: [%{"oAuth" => ["write:filters"]}],
+ responses: %{
+ 200 =>
+ Operation.response("Filter", "application/json", %Schema{
+ type: :object,
+ description: "Empty object"
+ })
+ }
+ }
+ end
+
+ defp id_param do
+ Operation.parameter(:id, :path, :string, "Filter ID", example: "123", required: true)
+ end
+
+ defp filter do
+ %Schema{
+ title: "Filter",
+ type: :object,
+ properties: %{
+ id: %Schema{type: :string},
+ phrase: %Schema{type: :string, description: "The text to be filtered"},
+ context: %Schema{
+ type: :array,
+ items: %Schema{type: :string, enum: ["home", "notifications", "public", "thread"]},
+ description: "The contexts in which the filter should be applied."
+ },
+ expires_at: %Schema{
+ type: :string,
+ format: :"date-time",
+ description:
+ "When the filter should no longer be applied. String (ISO 8601 Datetime), or null if the filter does not expire.",
+ nullable: true
+ },
+ irreversible: %Schema{
+ type: :boolean,
+ description:
+ "Should matching entities in home and notifications be dropped by the server?"
+ },
+ whole_word: %Schema{
+ type: :boolean,
+ description: "Should the filter consider word boundaries?"
+ }
+ },
+ example: %{
+ "id" => "5580",
+ "phrase" => "@twitter.com",
+ "context" => [
+ "home",
+ "notifications",
+ "public",
+ "thread"
+ ],
+ "whole_word" => false,
+ "expires_at" => nil,
+ "irreversible" => true
+ }
+ }
+ end
+
+ defp array_of_filters do
+ %Schema{
+ title: "ArrayOfFilters",
+ description: "Array of Filters",
+ type: :array,
+ items: filter(),
+ example: [
+ %{
+ "id" => "5580",
+ "phrase" => "@twitter.com",
+ "context" => [
+ "home",
+ "notifications",
+ "public",
+ "thread"
+ ],
+ "whole_word" => false,
+ "expires_at" => nil,
+ "irreversible" => true
+ },
+ %{
+ "id" => "6191",
+ "phrase" => ":eurovision2019:",
+ "context" => [
+ "home"
+ ],
+ "whole_word" => true,
+ "expires_at" => "2019-05-21T13:47:31.333Z",
+ "irreversible" => false
+ }
+ ]
+ }
+ end
+
+ defp create_request do
+ %Schema{
+ title: "FilterCreateRequest",
+ allOf: [
+ update_request(),
+ %Schema{
+ type: :object,
+ properties: %{
+ irreversible: %Schema{
+ allOf: [BooleanLike],
+ description:
+ "Should the server irreversibly drop matching entities from home and notifications?",
+ default: false
+ }
+ }
+ }
+ ],
+ example: %{
+ "phrase" => "knights",
+ "context" => ["home"]
+ }
+ }
+ end
+
+ defp update_request do
+ %Schema{
+ title: "FilterUpdateRequest",
+ type: :object,
+ properties: %{
+ phrase: %Schema{type: :string, description: "The text to be filtered"},
+ context: %Schema{
+ type: :array,
+ items: %Schema{type: :string, enum: ["home", "notifications", "public", "thread"]},
+ description:
+ "Array of enumerable strings `home`, `notifications`, `public`, `thread`. At least one context must be specified."
+ },
+ irreversible: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description:
+ "Should the server irreversibly drop matching entities from home and notifications?"
+ },
+ whole_word: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Consider word boundaries?",
+ default: true
+ }
+ # TODO: probably should implement filter expiration
+ # expires_in: %Schema{
+ # type: :string,
+ # format: :"date-time",
+ # description:
+ # "ISO 8601 Datetime for when the filter expires. Otherwise,
+ # null for a filter that doesn't expire."
+ # }
+ },
+ required: [:phrase, :context],
+ example: %{
+ "phrase" => "knights",
+ "context" => ["home"]
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/follow_request_operation.ex b/lib/pleroma/web/api_spec/operations/follow_request_operation.ex
new file mode 100644
index 000000000..ac4aee6da
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/follow_request_operation.ex
@@ -0,0 +1,65 @@
+# 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.FollowRequestOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.Account
+ alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def index_operation do
+ %Operation{
+ tags: ["Follow Requests"],
+ summary: "Pending Follows",
+ security: [%{"oAuth" => ["read:follows", "follow"]}],
+ operationId: "FollowRequestController.index",
+ responses: %{
+ 200 =>
+ Operation.response("Array of Account", "application/json", %Schema{
+ type: :array,
+ items: Account,
+ example: [Account.schema().example]
+ })
+ }
+ }
+ end
+
+ def authorize_operation do
+ %Operation{
+ tags: ["Follow Requests"],
+ summary: "Accept Follow",
+ operationId: "FollowRequestController.authorize",
+ parameters: [id_param()],
+ security: [%{"oAuth" => ["follow", "write:follows"]}],
+ responses: %{
+ 200 => Operation.response("Relationship", "application/json", AccountRelationship)
+ }
+ }
+ end
+
+ def reject_operation do
+ %Operation{
+ tags: ["Follow Requests"],
+ summary: "Reject Follow",
+ operationId: "FollowRequestController.reject",
+ parameters: [id_param()],
+ security: [%{"oAuth" => ["follow", "write:follows"]}],
+ responses: %{
+ 200 => Operation.response("Relationship", "application/json", AccountRelationship)
+ }
+ }
+ end
+
+ defp id_param do
+ Operation.parameter(:id, :path, :string, "Conversation ID",
+ example: "123",
+ required: true
+ )
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/instance_operation.ex b/lib/pleroma/web/api_spec/operations/instance_operation.ex
new file mode 100644
index 000000000..d5c335d0c
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/instance_operation.ex
@@ -0,0 +1,175 @@
+# 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.InstanceOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def show_operation do
+ %Operation{
+ tags: ["Instance"],
+ summary: "Fetch instance",
+ description: "Information about the server",
+ operationId: "InstanceController.show",
+ responses: %{
+ 200 => Operation.response("Instance", "application/json", instance())
+ }
+ }
+ end
+
+ def peers_operation do
+ %Operation{
+ tags: ["Instance"],
+ summary: "List of known hosts",
+ operationId: "InstanceController.peers",
+ responses: %{
+ 200 => Operation.response("Array of domains", "application/json", array_of_domains())
+ }
+ }
+ end
+
+ defp instance do
+ %Schema{
+ type: :object,
+ properties: %{
+ uri: %Schema{type: :string, description: "The domain name of the instance"},
+ title: %Schema{type: :string, description: "The title of the website"},
+ description: %Schema{
+ type: :string,
+ description: "Admin-defined description of the Pleroma site"
+ },
+ version: %Schema{
+ type: :string,
+ description: "The version of Pleroma installed on the instance"
+ },
+ email: %Schema{
+ type: :string,
+ description: "An email that may be contacted for any inquiries",
+ format: :email
+ },
+ urls: %Schema{
+ type: :object,
+ description: "URLs of interest for clients apps",
+ properties: %{
+ streaming_api: %Schema{
+ type: :string,
+ description: "Websockets address for push streaming"
+ }
+ }
+ },
+ stats: %Schema{
+ type: :object,
+ description: "Statistics about how much information the instance contains",
+ properties: %{
+ user_count: %Schema{
+ type: :integer,
+ description: "Users registered on this instance"
+ },
+ status_count: %Schema{
+ type: :integer,
+ description: "Statuses authored by users on instance"
+ },
+ domain_count: %Schema{
+ type: :integer,
+ description: "Domains federated with this instance"
+ }
+ }
+ },
+ thumbnail: %Schema{
+ type: :string,
+ description: "Banner image for the website",
+ nullable: true
+ },
+ languages: %Schema{
+ type: :array,
+ items: %Schema{type: :string},
+ description: "Primary langauges of the website and its staff"
+ },
+ registrations: %Schema{type: :boolean, description: "Whether registrations are enabled"},
+ # Extra (not present in Mastodon):
+ max_toot_chars: %Schema{
+ type: :integer,
+ description: ": Posts character limit (CW/Subject included in the counter)"
+ },
+ poll_limits: %Schema{
+ type: :object,
+ description: "A map with poll limits for local polls",
+ properties: %{
+ max_options: %Schema{
+ type: :integer,
+ description: "Maximum number of options."
+ },
+ max_option_chars: %Schema{
+ type: :integer,
+ description: "Maximum number of characters per option."
+ },
+ min_expiration: %Schema{
+ type: :integer,
+ description: "Minimum expiration time (in seconds)."
+ },
+ max_expiration: %Schema{
+ type: :integer,
+ description: "Maximum expiration time (in seconds)."
+ }
+ }
+ },
+ upload_limit: %Schema{
+ type: :integer,
+ description: "File size limit of uploads (except for avatar, background, banner)"
+ },
+ avatar_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"},
+ background_image: %Schema{
+ type: :string,
+ format: :uri,
+ description: "The background image for the website"
+ }
+ },
+ example: %{
+ "avatar_upload_limit" => 2_000_000,
+ "background_upload_limit" => 4_000_000,
+ "background_image" => "/static/image.png",
+ "banner_upload_limit" => 4_000_000,
+ "description" => "A Pleroma instance, an alternative fediverse server",
+ "email" => "lain@lain.com",
+ "languages" => ["en"],
+ "max_toot_chars" => 5000,
+ "poll_limits" => %{
+ "max_expiration" => 31_536_000,
+ "max_option_chars" => 200,
+ "max_options" => 20,
+ "min_expiration" => 0
+ },
+ "registrations" => false,
+ "stats" => %{
+ "domain_count" => 2996,
+ "status_count" => 15_802,
+ "user_count" => 5
+ },
+ "thumbnail" => "https://lain.com/instance/thumbnail.jpeg",
+ "title" => "lain.com",
+ "upload_limit" => 16_000_000,
+ "uri" => "https://lain.com",
+ "urls" => %{
+ "streaming_api" => "wss://lain.com"
+ },
+ "version" => "2.7.2 (compatible; Pleroma 2.0.50-536-g25eec6d7-develop)"
+ }
+ }
+ end
+
+ defp array_of_domains do
+ %Schema{
+ type: :array,
+ items: %Schema{type: :string},
+ example: ["pleroma.site", "lain.com", "bikeshed.party"]
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/list_operation.ex b/lib/pleroma/web/api_spec/operations/list_operation.ex
new file mode 100644
index 000000000..c88ed5dd0
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/list_operation.ex
@@ -0,0 +1,188 @@
+# 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.ListOperation 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.List
+
+ 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: ["Lists"],
+ summary: "Show user's lists",
+ description: "Fetch all lists that the user owns",
+ security: [%{"oAuth" => ["read:lists"]}],
+ operationId: "ListController.index",
+ responses: %{
+ 200 => Operation.response("Array of List", "application/json", array_of_lists())
+ }
+ }
+ end
+
+ def create_operation do
+ %Operation{
+ tags: ["Lists"],
+ summary: "Create a list",
+ description: "Fetch the list with the given ID. Used for verifying the title of a list.",
+ operationId: "ListController.create",
+ requestBody: create_update_request(),
+ security: [%{"oAuth" => ["write:lists"]}],
+ responses: %{
+ 200 => Operation.response("List", "application/json", List),
+ 400 => Operation.response("Error", "application/json", ApiError),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def show_operation do
+ %Operation{
+ tags: ["Lists"],
+ summary: "Show a single list",
+ description: "Fetch the list with the given ID. Used for verifying the title of a list.",
+ operationId: "ListController.show",
+ parameters: [id_param()],
+ security: [%{"oAuth" => ["read:lists"]}],
+ responses: %{
+ 200 => Operation.response("List", "application/json", List),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def update_operation do
+ %Operation{
+ tags: ["Lists"],
+ summary: "Update a list",
+ description: "Change the title of a list",
+ operationId: "ListController.update",
+ parameters: [id_param()],
+ requestBody: create_update_request(),
+ security: [%{"oAuth" => ["write:lists"]}],
+ responses: %{
+ 200 => Operation.response("List", "application/json", List),
+ 422 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def delete_operation do
+ %Operation{
+ tags: ["Lists"],
+ summary: "Delete a list",
+ operationId: "ListController.delete",
+ parameters: [id_param()],
+ security: [%{"oAuth" => ["write:lists"]}],
+ responses: %{
+ 200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
+ }
+ }
+ end
+
+ def list_accounts_operation do
+ %Operation{
+ tags: ["Lists"],
+ summary: "View accounts in list",
+ operationId: "ListController.list_accounts",
+ parameters: [id_param()],
+ security: [%{"oAuth" => ["read:lists"]}],
+ responses: %{
+ 200 =>
+ Operation.response("Array of Account", "application/json", %Schema{
+ type: :array,
+ items: Account
+ })
+ }
+ }
+ end
+
+ def add_to_list_operation do
+ %Operation{
+ tags: ["Lists"],
+ summary: "Add accounts to list",
+ description: "Add accounts to the given list.",
+ operationId: "ListController.add_to_list",
+ parameters: [id_param()],
+ requestBody: add_remove_accounts_request(),
+ security: [%{"oAuth" => ["write:lists"]}],
+ responses: %{
+ 200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
+ }
+ }
+ end
+
+ def remove_from_list_operation do
+ %Operation{
+ tags: ["Lists"],
+ summary: "Remove accounts from list",
+ operationId: "ListController.remove_from_list",
+ parameters: [id_param()],
+ requestBody: add_remove_accounts_request(),
+ security: [%{"oAuth" => ["write:lists"]}],
+ responses: %{
+ 200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
+ }
+ }
+ end
+
+ defp array_of_lists do
+ %Schema{
+ title: "ArrayOfLists",
+ description: "Response schema for lists",
+ type: :array,
+ items: List,
+ example: [
+ %{"id" => "123", "title" => "my list"},
+ %{"id" => "1337", "title" => "another list"}
+ ]
+ }
+ end
+
+ defp id_param do
+ Operation.parameter(:id, :path, :string, "List ID",
+ example: "123",
+ required: true
+ )
+ end
+
+ defp create_update_request do
+ request_body(
+ "Parameters",
+ %Schema{
+ description: "POST body for creating or updating a List",
+ type: :object,
+ properties: %{
+ title: %Schema{type: :string, description: "List title"}
+ },
+ required: [:title]
+ },
+ required: true
+ )
+ end
+
+ defp add_remove_accounts_request do
+ request_body(
+ "Parameters",
+ %Schema{
+ description: "POST body for adding/removing accounts to/from a List",
+ type: :object,
+ properties: %{
+ account_ids: %Schema{type: :array, description: "Array of account IDs", items: FlakeID}
+ },
+ required: [:account_ids]
+ },
+ required: true
+ )
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/marker_operation.ex b/lib/pleroma/web/api_spec/operations/marker_operation.ex
new file mode 100644
index 000000000..714ef1f99
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/marker_operation.ex
@@ -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.MarkerOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias 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: ["Markers"],
+ summary: "Get saved timeline position",
+ security: [%{"oAuth" => ["read:statuses"]}],
+ operationId: "MarkerController.index",
+ parameters: [
+ Operation.parameter(
+ :timeline,
+ :query,
+ %Schema{
+ type: :array,
+ items: %Schema{type: :string, enum: ["home", "notifications"]}
+ },
+ "Array of markers to fetch. If not provided, an empty object will be returned."
+ )
+ ],
+ responses: %{
+ 200 => Operation.response("Marker", "application/json", response()),
+ 403 => Operation.response("Error", "application/json", api_error())
+ }
+ }
+ end
+
+ def upsert_operation do
+ %Operation{
+ tags: ["Markers"],
+ summary: "Save position in timeline",
+ operationId: "MarkerController.upsert",
+ requestBody: Helpers.request_body("Parameters", upsert_request(), required: true),
+ security: [%{"oAuth" => ["follow", "write:blocks"]}],
+ responses: %{
+ 200 => Operation.response("Marker", "application/json", response()),
+ 403 => Operation.response("Error", "application/json", api_error())
+ }
+ }
+ end
+
+ defp marker do
+ %Schema{
+ title: "Marker",
+ description: "Schema for a marker",
+ type: :object,
+ properties: %{
+ last_read_id: %Schema{type: :string},
+ version: %Schema{type: :integer},
+ updated_at: %Schema{type: :string},
+ pleroma: %Schema{
+ type: :object,
+ properties: %{
+ unread_count: %Schema{type: :integer}
+ }
+ }
+ },
+ example: %{
+ "last_read_id" => "35098814",
+ "version" => 361,
+ "updated_at" => "2019-11-26T22:37:25.239Z",
+ "pleroma" => %{"unread_count" => 5}
+ }
+ }
+ end
+
+ defp response do
+ %Schema{
+ title: "MarkersResponse",
+ description: "Response schema for markers",
+ type: :object,
+ properties: %{
+ notifications: %Schema{allOf: [marker()], nullable: true},
+ home: %Schema{allOf: [marker()], nullable: true}
+ },
+ items: %Schema{type: :string},
+ example: %{
+ "notifications" => %{
+ "last_read_id" => "35098814",
+ "version" => 361,
+ "updated_at" => "2019-11-26T22:37:25.239Z",
+ "pleroma" => %{"unread_count" => 0}
+ },
+ "home" => %{
+ "last_read_id" => "103206604258487607",
+ "version" => 468,
+ "updated_at" => "2019-11-26T22:37:25.235Z",
+ "pleroma" => %{"unread_count" => 10}
+ }
+ }
+ }
+ end
+
+ defp upsert_request do
+ %Schema{
+ title: "MarkersUpsertRequest",
+ description: "Request schema for marker upsert",
+ type: :object,
+ properties: %{
+ notifications: %Schema{
+ type: :object,
+ nullable: true,
+ properties: %{
+ last_read_id: %Schema{nullable: true, type: :string}
+ }
+ },
+ home: %Schema{
+ type: :object,
+ nullable: true,
+ properties: %{
+ last_read_id: %Schema{nullable: true, type: :string}
+ }
+ }
+ },
+ example: %{
+ "home" => %{
+ "last_read_id" => "103194548672408537",
+ "version" => 462,
+ "updated_at" => "2019-11-24T19:39:39.337Z"
+ }
+ }
+ }
+ end
+
+ defp api_error do
+ %Schema{
+ type: :object,
+ properties: %{error: %Schema{type: :string}}
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/media_operation.ex b/lib/pleroma/web/api_spec/operations/media_operation.ex
new file mode 100644
index 000000000..d9c3c42db
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/media_operation.ex
@@ -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
diff --git a/lib/pleroma/web/api_spec/operations/notification_operation.ex b/lib/pleroma/web/api_spec/operations/notification_operation.ex
index 64adc5319..46e72f8bf 100644
--- a/lib/pleroma/web/api_spec/operations/notification_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/notification_operation.ex
@@ -145,7 +145,7 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
}
end
- defp notification do
+ def notification do
%Schema{
title: "Notification",
description: "Response schema for a notification",
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex
new file mode 100644
index 000000000..90922c064
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex
@@ -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
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_conversation_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_conversation_operation.ex
new file mode 100644
index 000000000..e885eab20
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/pleroma_conversation_operation.ex
@@ -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
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex
new file mode 100644
index 000000000..567688ff5
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex
@@ -0,0 +1,390 @@
+# 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.PleromaEmojiPackOperation 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 remote_operation do
+ %Operation{
+ tags: ["Emoji Packs"],
+ summary: "Make request to another instance for emoji packs list",
+ security: [%{"oAuth" => ["write"]}],
+ parameters: [url_param()],
+ operationId: "PleromaAPI.EmojiPackController.remote",
+ responses: %{
+ 200 => emoji_packs_response(),
+ 500 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def index_operation do
+ %Operation{
+ tags: ["Emoji Packs"],
+ summary: "Lists local custom emoji packs",
+ operationId: "PleromaAPI.EmojiPackController.index",
+ responses: %{
+ 200 => emoji_packs_response()
+ }
+ }
+ end
+
+ def show_operation do
+ %Operation{
+ tags: ["Emoji Packs"],
+ summary: "Show emoji pack",
+ operationId: "PleromaAPI.EmojiPackController.show",
+ parameters: [name_param()],
+ responses: %{
+ 200 => Operation.response("Emoji Pack", "application/json", emoji_pack()),
+ 400 => Operation.response("Bad Request", "application/json", ApiError),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def archive_operation do
+ %Operation{
+ tags: ["Emoji Packs"],
+ summary: "Requests a local pack archive from the instance",
+ operationId: "PleromaAPI.EmojiPackController.archive",
+ parameters: [name_param()],
+ responses: %{
+ 200 =>
+ Operation.response("Archive file", "application/octet-stream", %Schema{
+ type: :string,
+ format: :binary
+ }),
+ 403 => Operation.response("Forbidden", "application/json", ApiError),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def download_operation do
+ %Operation{
+ tags: ["Emoji Packs"],
+ summary: "Download pack from another instance",
+ operationId: "PleromaAPI.EmojiPackController.download",
+ security: [%{"oAuth" => ["write"]}],
+ requestBody: request_body("Parameters", download_request(), required: true),
+ responses: %{
+ 200 => ok_response(),
+ 500 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp download_request do
+ %Schema{
+ type: :object,
+ required: [:url, :name],
+ properties: %{
+ url: %Schema{
+ type: :string,
+ format: :uri,
+ description: "URL of the instance to download from"
+ },
+ name: %Schema{type: :string, format: :uri, description: "Pack Name"},
+ as: %Schema{type: :string, format: :uri, description: "Save as"}
+ }
+ }
+ end
+
+ def create_operation do
+ %Operation{
+ tags: ["Emoji Packs"],
+ summary: "Create an empty pack",
+ operationId: "PleromaAPI.EmojiPackController.create",
+ security: [%{"oAuth" => ["write"]}],
+ parameters: [name_param()],
+ responses: %{
+ 200 => ok_response(),
+ 400 => Operation.response("Not Found", "application/json", ApiError),
+ 409 => Operation.response("Conflict", "application/json", ApiError),
+ 500 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def delete_operation do
+ %Operation{
+ tags: ["Emoji Packs"],
+ summary: "Delete a custom emoji pack",
+ operationId: "PleromaAPI.EmojiPackController.delete",
+ security: [%{"oAuth" => ["write"]}],
+ parameters: [name_param()],
+ responses: %{
+ 200 => ok_response(),
+ 400 => Operation.response("Bad Request", "application/json", ApiError),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def update_operation do
+ %Operation{
+ tags: ["Emoji Packs"],
+ summary: "Updates (replaces) pack metadata",
+ operationId: "PleromaAPI.EmojiPackController.update",
+ security: [%{"oAuth" => ["write"]}],
+ requestBody: request_body("Parameters", update_request(), required: true),
+ parameters: [name_param()],
+ responses: %{
+ 200 => Operation.response("Metadata", "application/json", metadata()),
+ 400 => Operation.response("Bad Request", "application/json", ApiError)
+ }
+ }
+ end
+
+ def add_file_operation do
+ %Operation{
+ tags: ["Emoji Packs"],
+ summary: "Add new file to the pack",
+ operationId: "PleromaAPI.EmojiPackController.add_file",
+ security: [%{"oAuth" => ["write"]}],
+ requestBody: request_body("Parameters", add_file_request(), required: true),
+ parameters: [name_param()],
+ responses: %{
+ 200 => Operation.response("Files Object", "application/json", files_object()),
+ 400 => Operation.response("Bad Request", "application/json", ApiError),
+ 409 => Operation.response("Conflict", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp add_file_request do
+ %Schema{
+ type: :object,
+ required: [:file],
+ properties: %{
+ file: %Schema{
+ description:
+ "File needs to be uploaded with the multipart request or link to remote file",
+ anyOf: [
+ %Schema{type: :string, format: :binary},
+ %Schema{type: :string, format: :uri}
+ ]
+ },
+ shortcode: %Schema{
+ type: :string,
+ description:
+ "Shortcode for new emoji, must be unique for all emoji. If not sended, shortcode will be taken from original filename."
+ },
+ filename: %Schema{
+ type: :string,
+ description:
+ "New emoji file name. If not specified will be taken from original filename."
+ }
+ }
+ }
+ end
+
+ def update_file_operation do
+ %Operation{
+ tags: ["Emoji Packs"],
+ summary: "Add new file to the pack",
+ operationId: "PleromaAPI.EmojiPackController.update_file",
+ security: [%{"oAuth" => ["write"]}],
+ requestBody: request_body("Parameters", update_file_request(), required: true),
+ parameters: [name_param()],
+ responses: %{
+ 200 => Operation.response("Files Object", "application/json", files_object()),
+ 400 => Operation.response("Bad Request", "application/json", ApiError),
+ 409 => Operation.response("Conflict", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp update_file_request do
+ %Schema{
+ type: :object,
+ required: [:shortcode, :new_shortcode, :new_filename],
+ properties: %{
+ shortcode: %Schema{
+ type: :string,
+ description: "Emoji file shortcode"
+ },
+ new_shortcode: %Schema{
+ type: :string,
+ description: "New emoji file shortcode"
+ },
+ new_filename: %Schema{
+ type: :string,
+ description: "New filename for emoji file"
+ },
+ force: %Schema{
+ type: :boolean,
+ description: "With true value to overwrite existing emoji with new shortcode",
+ default: false
+ }
+ }
+ }
+ end
+
+ def delete_file_operation do
+ %Operation{
+ tags: ["Emoji Packs"],
+ summary: "Delete emoji file from pack",
+ operationId: "PleromaAPI.EmojiPackController.delete_file",
+ security: [%{"oAuth" => ["write"]}],
+ parameters: [
+ name_param(),
+ Operation.parameter(:shortcode, :query, :string, "File shortcode",
+ example: "cofe",
+ required: true
+ )
+ ],
+ responses: %{
+ 200 => Operation.response("Files Object", "application/json", files_object()),
+ 400 => Operation.response("Bad Request", "application/json", ApiError)
+ }
+ }
+ end
+
+ def import_from_filesystem_operation do
+ %Operation{
+ tags: ["Emoji Packs"],
+ summary: "Imports packs from filesystem",
+ operationId: "PleromaAPI.EmojiPackController.import",
+ security: [%{"oAuth" => ["write"]}],
+ responses: %{
+ 200 =>
+ Operation.response("Array of imported pack names", "application/json", %Schema{
+ type: :array,
+ items: %Schema{type: :string}
+ })
+ }
+ }
+ end
+
+ defp name_param do
+ Operation.parameter(:name, :path, :string, "Pack Name", example: "cofe", required: true)
+ end
+
+ defp url_param do
+ Operation.parameter(
+ :url,
+ :query,
+ %Schema{type: :string, format: :uri},
+ "URL of the instance",
+ required: true
+ )
+ end
+
+ defp ok_response do
+ Operation.response("Ok", "application/json", %Schema{type: :string, example: "ok"})
+ end
+
+ defp emoji_packs_response do
+ Operation.response(
+ "Object with pack names as keys and pack contents as values",
+ "application/json",
+ %Schema{
+ type: :object,
+ additionalProperties: emoji_pack(),
+ example: %{
+ "emojos" => emoji_pack().example
+ }
+ }
+ )
+ end
+
+ defp emoji_pack do
+ %Schema{
+ title: "EmojiPack",
+ type: :object,
+ properties: %{
+ files: files_object(),
+ pack: %Schema{
+ type: :object,
+ properties: %{
+ license: %Schema{type: :string},
+ homepage: %Schema{type: :string, format: :uri},
+ description: %Schema{type: :string},
+ "can-download": %Schema{type: :boolean},
+ "share-files": %Schema{type: :boolean},
+ "download-sha256": %Schema{type: :string}
+ }
+ }
+ },
+ example: %{
+ "files" => %{"emacs" => "emacs.png", "guix" => "guix.png"},
+ "pack" => %{
+ "license" => "Test license",
+ "homepage" => "https://pleroma.social",
+ "description" => "Test description",
+ "can-download" => true,
+ "share-files" => true,
+ "download-sha256" => "57482F30674FD3DE821FF48C81C00DA4D4AF1F300209253684ABA7075E5FC238"
+ }
+ }
+ }
+ end
+
+ defp files_object do
+ %Schema{
+ type: :object,
+ additionalProperties: %Schema{type: :string},
+ description: "Object with emoji names as keys and filenames as values"
+ }
+ end
+
+ defp update_request do
+ %Schema{
+ type: :object,
+ properties: %{
+ metadata: %Schema{
+ type: :object,
+ description: "Metadata to replace the old one",
+ properties: %{
+ license: %Schema{type: :string},
+ homepage: %Schema{type: :string, format: :uri},
+ description: %Schema{type: :string},
+ "fallback-src": %Schema{
+ type: :string,
+ format: :uri,
+ description: "Fallback url to download pack from"
+ },
+ "fallback-src-sha256": %Schema{
+ type: :string,
+ description: "SHA256 encoded for fallback pack archive"
+ },
+ "share-files": %Schema{type: :boolean, description: "Is pack allowed for sharing?"}
+ }
+ }
+ }
+ }
+ end
+
+ defp metadata do
+ %Schema{
+ type: :object,
+ properties: %{
+ license: %Schema{type: :string},
+ homepage: %Schema{type: :string, format: :uri},
+ description: %Schema{type: :string},
+ "fallback-src": %Schema{
+ type: :string,
+ format: :uri,
+ description: "Fallback url to download pack from"
+ },
+ "fallback-src-sha256": %Schema{
+ type: :string,
+ description: "SHA256 encoded for fallback pack archive"
+ },
+ "share-files": %Schema{type: :boolean, description: "Is pack allowed for sharing?"}
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_mascot_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_mascot_operation.ex
new file mode 100644
index 000000000..8c5f37ea6
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/pleroma_mascot_operation.ex
@@ -0,0 +1,79 @@
+# 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.PleromaMascotOperation 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: ["Mascot"],
+ summary: "Gets user mascot image",
+ security: [%{"oAuth" => ["read:accounts"]}],
+ operationId: "PleromaAPI.MascotController.show",
+ responses: %{
+ 200 => Operation.response("Mascot", "application/json", mascot())
+ }
+ }
+ end
+
+ def update_operation do
+ %Operation{
+ tags: ["Mascot"],
+ summary: "Set/clear user avatar image",
+ description:
+ "Behaves exactly the same as `POST /api/v1/upload`. Can only accept images - any attempt to upload non-image files will be met with `HTTP 415 Unsupported Media Type`.",
+ operationId: "PleromaAPI.MascotController.update",
+ requestBody:
+ request_body(
+ "Parameters",
+ %Schema{
+ type: :object,
+ properties: %{
+ file: %Schema{type: :string, format: :binary}
+ }
+ },
+ required: true
+ ),
+ security: [%{"oAuth" => ["write:accounts"]}],
+ responses: %{
+ 200 => Operation.response("Mascot", "application/json", mascot()),
+ 415 => Operation.response("Unsupported Media Type", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp mascot do
+ %Schema{
+ type: :object,
+ properties: %{
+ id: %Schema{type: :string},
+ url: %Schema{type: :string, format: :uri},
+ type: %Schema{type: :string},
+ pleroma: %Schema{
+ type: :object,
+ properties: %{
+ mime_type: %Schema{type: :string}
+ }
+ }
+ },
+ example: %{
+ "id" => "abcdefg",
+ "url" => "https://pleroma.example.org/media/abcdefg.png",
+ "type" => "image",
+ "pleroma" => %{
+ "mime_type" => "image/png"
+ }
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_notification_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_notification_operation.ex
new file mode 100644
index 000000000..b0c8db863
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/pleroma_notification_operation.ex
@@ -0,0 +1,48 @@
+# 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.PleromaNotificationOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.NotificationOperation
+ 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 mark_as_read_operation do
+ %Operation{
+ tags: ["Notifications"],
+ summary: "Mark notifications as read. Query parameters are mutually exclusive.",
+ requestBody:
+ request_body("Parameters", %Schema{
+ type: :object,
+ properties: %{
+ id: %Schema{type: :integer, description: "A single notification ID to read"},
+ max_id: %Schema{type: :integer, description: "Read all notifications up to this ID"}
+ }
+ }),
+ security: [%{"oAuth" => ["write:notifications"]}],
+ operationId: "PleromaAPI.NotificationController.mark_as_read",
+ responses: %{
+ 200 =>
+ Operation.response(
+ "A Notification or array of Motifications",
+ "application/json",
+ %Schema{
+ anyOf: [
+ %Schema{type: :array, items: NotificationOperation.notification()},
+ NotificationOperation.notification()
+ ]
+ }
+ ),
+ 400 => Operation.response("Bad Request", "application/json", ApiError)
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex
new file mode 100644
index 000000000..85a22aa0b
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex
@@ -0,0 +1,102 @@
+# 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.PleromaScrobbleOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Reference
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.Account
+ alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
+
+ import Pleroma.Web.ApiSpec.Helpers
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def create_operation do
+ %Operation{
+ tags: ["Scrobbles"],
+ summary: "Creates a new Listen activity for an account",
+ security: [%{"oAuth" => ["write"]}],
+ operationId: "PleromaAPI.ScrobbleController.create",
+ requestBody: request_body("Parameters", create_request(), requried: true),
+ responses: %{
+ 200 => Operation.response("Scrobble", "application/json", scrobble())
+ }
+ }
+ end
+
+ def index_operation do
+ %Operation{
+ tags: ["Scrobbles"],
+ summary: "Requests a list of current and recent Listen activities for an account",
+ operationId: "PleromaAPI.ScrobbleController.index",
+ parameters: [
+ %Reference{"$ref": "#/components/parameters/accountIdOrNickname"} | pagination_params()
+ ],
+ security: [%{"oAuth" => ["read"]}],
+ responses: %{
+ 200 =>
+ Operation.response("Array of Scrobble", "application/json", %Schema{
+ type: :array,
+ items: scrobble()
+ })
+ }
+ }
+ end
+
+ defp create_request do
+ %Schema{
+ type: :object,
+ required: [:title],
+ properties: %{
+ title: %Schema{type: :string, description: "The title of the media playing"},
+ album: %Schema{type: :string, description: "The album of the media playing"},
+ artist: %Schema{type: :string, description: "The artist of the media playing"},
+ length: %Schema{type: :integer, description: "The length of the media playing"},
+ visibility: %Schema{
+ allOf: [VisibilityScope],
+ default: "public",
+ description: "Scrobble visibility"
+ }
+ },
+ example: %{
+ "title" => "Some Title",
+ "artist" => "Some Artist",
+ "album" => "Some Album",
+ "length" => 180_000
+ }
+ }
+ end
+
+ defp scrobble do
+ %Schema{
+ type: :object,
+ properties: %{
+ id: %Schema{type: :string},
+ account: Account,
+ title: %Schema{type: :string, description: "The title of the media playing"},
+ album: %Schema{type: :string, description: "The album of the media playing"},
+ artist: %Schema{type: :string, description: "The artist of the media playing"},
+ length: %Schema{
+ type: :integer,
+ description: "The length of the media playing",
+ nullable: true
+ },
+ created_at: %Schema{type: :string, format: :"date-time"}
+ },
+ example: %{
+ "id" => "1234",
+ "account" => Account.schema().example,
+ "title" => "Some Title",
+ "artist" => "Some Artist",
+ "album" => "Some Album",
+ "length" => 180_000,
+ "created_at" => "2019-09-28T12:40:45.000Z"
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/poll_operation.ex b/lib/pleroma/web/api_spec/operations/poll_operation.ex
new file mode 100644
index 000000000..e15c7dc95
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/poll_operation.ex
@@ -0,0 +1,76 @@
+# 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.PollOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+ alias Pleroma.Web.ApiSpec.Schemas.FlakeID
+ alias Pleroma.Web.ApiSpec.Schemas.Poll
+
+ 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: ["Polls"],
+ summary: "View a poll",
+ security: [%{"oAuth" => ["read:statuses"]}],
+ parameters: [id_param()],
+ operationId: "PollController.show",
+ responses: %{
+ 200 => Operation.response("Poll", "application/json", Poll),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def vote_operation do
+ %Operation{
+ tags: ["Polls"],
+ summary: "Vote on a poll",
+ parameters: [id_param()],
+ operationId: "PollController.vote",
+ requestBody: vote_request(),
+ security: [%{"oAuth" => ["write:statuses"]}],
+ responses: %{
+ 200 => Operation.response("Poll", "application/json", Poll),
+ 422 => Operation.response("Error", "application/json", ApiError),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp id_param do
+ Operation.parameter(:id, :path, FlakeID, "Poll ID",
+ example: "123",
+ required: true
+ )
+ end
+
+ defp vote_request do
+ request_body(
+ "Parameters",
+ %Schema{
+ type: :object,
+ properties: %{
+ choices: %Schema{
+ type: :array,
+ items: %Schema{type: :integer},
+ description: "Array of own votes containing index for each option (starting from 0)"
+ }
+ },
+ required: [:choices]
+ },
+ required: true,
+ example: %{
+ "choices" => [0, 1, 2]
+ }
+ )
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/report_operation.ex b/lib/pleroma/web/api_spec/operations/report_operation.ex
index da4d50703..b9b4c4f79 100644
--- a/lib/pleroma/web/api_spec/operations/report_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/report_operation.ex
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.ApiSpec.ReportOperation do
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Helpers
alias Pleroma.Web.ApiSpec.Schemas.ApiError
+ alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
@@ -37,15 +38,18 @@ defmodule Pleroma.Web.ApiSpec.ReportOperation do
account_id: %Schema{type: :string, description: "ID of the account to report"},
status_ids: %Schema{
type: :array,
+ nullable: true,
items: %Schema{type: :string},
description: "Array of Statuses to attach to the report, for context"
},
comment: %Schema{
type: :string,
+ nullable: true,
description: "Reason for the report"
},
forward: %Schema{
- type: :boolean,
+ allOf: [BooleanLike],
+ nullable: true,
default: false,
description:
"If the account is remote, should the report be forwarded to the remote admin?"
diff --git a/lib/pleroma/web/api_spec/operations/scheduled_activity_operation.ex b/lib/pleroma/web/api_spec/operations/scheduled_activity_operation.ex
new file mode 100644
index 000000000..fe675a923
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/scheduled_activity_operation.ex
@@ -0,0 +1,96 @@
+# 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.ScheduledActivityOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+ alias Pleroma.Web.ApiSpec.Schemas.FlakeID
+ alias Pleroma.Web.ApiSpec.Schemas.ScheduledStatus
+
+ 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: ["Scheduled Statuses"],
+ summary: "View scheduled statuses",
+ security: [%{"oAuth" => ["read:statuses"]}],
+ parameters: pagination_params(),
+ operationId: "ScheduledActivity.index",
+ responses: %{
+ 200 =>
+ Operation.response("Array of ScheduledStatus", "application/json", %Schema{
+ type: :array,
+ items: ScheduledStatus
+ })
+ }
+ }
+ end
+
+ def show_operation do
+ %Operation{
+ tags: ["Scheduled Statuses"],
+ summary: "View a single scheduled status",
+ security: [%{"oAuth" => ["read:statuses"]}],
+ parameters: [id_param()],
+ operationId: "ScheduledActivity.show",
+ responses: %{
+ 200 => Operation.response("Scheduled Status", "application/json", ScheduledStatus),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def update_operation do
+ %Operation{
+ tags: ["Scheduled Statuses"],
+ summary: "Schedule a status",
+ operationId: "ScheduledActivity.update",
+ security: [%{"oAuth" => ["write:statuses"]}],
+ parameters: [id_param()],
+ requestBody:
+ request_body("Parameters", %Schema{
+ type: :object,
+ properties: %{
+ scheduled_at: %Schema{
+ type: :string,
+ format: :"date-time",
+ description:
+ "ISO 8601 Datetime at which the status will be published. Must be at least 5 minutes into the future."
+ }
+ }
+ }),
+ responses: %{
+ 200 => Operation.response("Scheduled Status", "application/json", ScheduledStatus),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def delete_operation do
+ %Operation{
+ tags: ["Scheduled Statuses"],
+ summary: "Cancel a scheduled status",
+ security: [%{"oAuth" => ["write:statuses"]}],
+ parameters: [id_param()],
+ operationId: "ScheduledActivity.delete",
+ responses: %{
+ 200 => Operation.response("Empty object", "application/json", %Schema{type: :object}),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp id_param do
+ Operation.parameter(:id, :path, FlakeID, "Poll ID",
+ example: "123",
+ required: true
+ )
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/search_operation.ex b/lib/pleroma/web/api_spec/operations/search_operation.ex
new file mode 100644
index 000000000..169c36d87
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/search_operation.ex
@@ -0,0 +1,208 @@
+# 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.SearchOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.AccountOperation
+ alias Pleroma.Web.ApiSpec.Schemas.Account
+ alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
+ alias Pleroma.Web.ApiSpec.Schemas.FlakeID
+ alias Pleroma.Web.ApiSpec.Schemas.Status
+ alias Pleroma.Web.ApiSpec.Schemas.Tag
+
+ import Pleroma.Web.ApiSpec.Helpers
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ # Note: `with_relationships` param is not supported (PleromaFE uses this op for autocomplete)
+ def account_search_operation do
+ %Operation{
+ tags: ["Search"],
+ summary: "Search for matching accounts by username or display name",
+ operationId: "SearchController.account_search",
+ parameters: [
+ Operation.parameter(:q, :query, %Schema{type: :string}, "What to search for",
+ required: true
+ ),
+ Operation.parameter(
+ :limit,
+ :query,
+ %Schema{type: :integer, default: 40},
+ "Maximum number of results"
+ ),
+ Operation.parameter(
+ :resolve,
+ :query,
+ %Schema{allOf: [BooleanLike], default: false},
+ "Attempt WebFinger lookup. Use this when `q` is an exact address."
+ ),
+ Operation.parameter(
+ :following,
+ :query,
+ %Schema{allOf: [BooleanLike], default: false},
+ "Only include accounts that the user is following"
+ )
+ ],
+ responses: %{
+ 200 =>
+ Operation.response(
+ "Array of Account",
+ "application/json",
+ AccountOperation.array_of_accounts()
+ )
+ }
+ }
+ end
+
+ def search_operation do
+ %Operation{
+ tags: ["Search"],
+ summary: "Search results",
+ security: [%{"oAuth" => ["read:search"]}],
+ operationId: "SearchController.search",
+ deprecated: true,
+ parameters: [
+ Operation.parameter(
+ :account_id,
+ :query,
+ FlakeID,
+ "If provided, statuses returned will be authored only by this account"
+ ),
+ Operation.parameter(
+ :type,
+ :query,
+ %Schema{type: :string, enum: ["accounts", "hashtags", "statuses"]},
+ "Search type"
+ ),
+ Operation.parameter(:q, :query, %Schema{type: :string}, "The search query", required: true),
+ Operation.parameter(
+ :resolve,
+ :query,
+ %Schema{allOf: [BooleanLike], default: false},
+ "Attempt WebFinger lookup"
+ ),
+ Operation.parameter(
+ :following,
+ :query,
+ %Schema{allOf: [BooleanLike], default: false},
+ "Only include accounts that the user is following"
+ ),
+ Operation.parameter(
+ :offset,
+ :query,
+ %Schema{type: :integer},
+ "Offset"
+ ),
+ with_relationships_param() | pagination_params()
+ ],
+ responses: %{
+ 200 => Operation.response("Results", "application/json", results())
+ }
+ }
+ end
+
+ def search2_operation do
+ %Operation{
+ tags: ["Search"],
+ summary: "Search results",
+ security: [%{"oAuth" => ["read:search"]}],
+ operationId: "SearchController.search2",
+ parameters: [
+ Operation.parameter(
+ :account_id,
+ :query,
+ FlakeID,
+ "If provided, statuses returned will be authored only by this account"
+ ),
+ Operation.parameter(
+ :type,
+ :query,
+ %Schema{type: :string, enum: ["accounts", "hashtags", "statuses"]},
+ "Search type"
+ ),
+ Operation.parameter(:q, :query, %Schema{type: :string}, "What to search for",
+ required: true
+ ),
+ Operation.parameter(
+ :resolve,
+ :query,
+ %Schema{allOf: [BooleanLike], default: false},
+ "Attempt WebFinger lookup"
+ ),
+ Operation.parameter(
+ :following,
+ :query,
+ %Schema{allOf: [BooleanLike], default: false},
+ "Only include accounts that the user is following"
+ ),
+ with_relationships_param() | pagination_params()
+ ],
+ responses: %{
+ 200 => Operation.response("Results", "application/json", results2())
+ }
+ }
+ end
+
+ defp results2 do
+ %Schema{
+ title: "SearchResults",
+ type: :object,
+ properties: %{
+ accounts: %Schema{
+ type: :array,
+ items: Account,
+ description: "Accounts which match the given query"
+ },
+ statuses: %Schema{
+ type: :array,
+ items: Status,
+ description: "Statuses which match the given query"
+ },
+ hashtags: %Schema{
+ type: :array,
+ items: Tag,
+ description: "Hashtags which match the given query"
+ }
+ },
+ example: %{
+ "accounts" => [Account.schema().example],
+ "statuses" => [Status.schema().example],
+ "hashtags" => [Tag.schema().example]
+ }
+ }
+ end
+
+ defp results do
+ %Schema{
+ title: "SearchResults",
+ type: :object,
+ properties: %{
+ accounts: %Schema{
+ type: :array,
+ items: Account,
+ description: "Accounts which match the given query"
+ },
+ statuses: %Schema{
+ type: :array,
+ items: Status,
+ description: "Statuses which match the given query"
+ },
+ hashtags: %Schema{
+ type: :array,
+ items: %Schema{type: :string},
+ description: "Hashtags which match the given query"
+ }
+ },
+ example: %{
+ "accounts" => [Account.schema().example],
+ "statuses" => [Status.schema().example],
+ "hashtags" => ["cofe"]
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex
new file mode 100644
index 000000000..ca9db01e5
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/status_operation.ex
@@ -0,0 +1,518 @@
+# 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.StatusOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.AccountOperation
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+ alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
+ alias Pleroma.Web.ApiSpec.Schemas.FlakeID
+ alias Pleroma.Web.ApiSpec.Schemas.ScheduledStatus
+ alias Pleroma.Web.ApiSpec.Schemas.Status
+ alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
+
+ 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: ["Statuses"],
+ summary: "Get multiple statuses by IDs",
+ security: [%{"oAuth" => ["read:statuses"]}],
+ parameters: [
+ Operation.parameter(
+ :ids,
+ :query,
+ %Schema{type: :array, items: FlakeID},
+ "Array of status IDs"
+ )
+ ],
+ operationId: "StatusController.index",
+ responses: %{
+ 200 => Operation.response("Array of Status", "application/json", array_of_statuses())
+ }
+ }
+ end
+
+ def create_operation do
+ %Operation{
+ tags: ["Statuses"],
+ summary: "Publish new status",
+ security: [%{"oAuth" => ["write:statuses"]}],
+ description: "Post a new status",
+ operationId: "StatusController.create",
+ requestBody: request_body("Parameters", create_request(), required: true),
+ responses: %{
+ 200 =>
+ Operation.response(
+ "Status. When `scheduled_at` is present, ScheduledStatus is returned instead",
+ "application/json",
+ %Schema{oneOf: [Status, ScheduledStatus]}
+ ),
+ 422 => Operation.response("Bad Request", "application/json", ApiError)
+ }
+ }
+ end
+
+ def show_operation do
+ %Operation{
+ tags: ["Statuses"],
+ summary: "View specific status",
+ description: "View information about a status",
+ operationId: "StatusController.show",
+ security: [%{"oAuth" => ["read:statuses"]}],
+ parameters: [id_param()],
+ responses: %{
+ 200 => status_response(),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def delete_operation do
+ %Operation{
+ tags: ["Statuses"],
+ summary: "Delete status",
+ security: [%{"oAuth" => ["write:statuses"]}],
+ description: "Delete one of your own statuses",
+ operationId: "StatusController.delete",
+ parameters: [id_param()],
+ responses: %{
+ 200 => empty_object_response(),
+ 403 => Operation.response("Forbidden", "application/json", ApiError),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def reblog_operation do
+ %Operation{
+ tags: ["Statuses"],
+ summary: "Boost",
+ security: [%{"oAuth" => ["write:statuses"]}],
+ description: "Share a status",
+ operationId: "StatusController.reblog",
+ parameters: [id_param()],
+ requestBody:
+ request_body("Parameters", %Schema{
+ type: :object,
+ properties: %{
+ visibility: %Schema{allOf: [VisibilityScope], default: "public"}
+ }
+ }),
+ responses: %{
+ 200 => status_response(),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def unreblog_operation do
+ %Operation{
+ tags: ["Statuses"],
+ summary: "Undo boost",
+ security: [%{"oAuth" => ["write:statuses"]}],
+ description: "Undo a reshare of a status",
+ operationId: "StatusController.unreblog",
+ parameters: [id_param()],
+ responses: %{
+ 200 => status_response(),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def favourite_operation do
+ %Operation{
+ tags: ["Statuses"],
+ summary: "Favourite",
+ security: [%{"oAuth" => ["write:favourites"]}],
+ description: "Add a status to your favourites list",
+ operationId: "StatusController.favourite",
+ parameters: [id_param()],
+ responses: %{
+ 200 => status_response(),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def unfavourite_operation do
+ %Operation{
+ tags: ["Statuses"],
+ summary: "Undo favourite",
+ security: [%{"oAuth" => ["write:favourites"]}],
+ description: "Remove a status from your favourites list",
+ operationId: "StatusController.unfavourite",
+ parameters: [id_param()],
+ responses: %{
+ 200 => status_response(),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def pin_operation do
+ %Operation{
+ tags: ["Statuses"],
+ summary: "Pin to profile",
+ security: [%{"oAuth" => ["write:accounts"]}],
+ description: "Feature one of your own public statuses at the top of your profile",
+ operationId: "StatusController.pin",
+ parameters: [id_param()],
+ responses: %{
+ 200 => status_response(),
+ 400 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def unpin_operation do
+ %Operation{
+ tags: ["Statuses"],
+ summary: "Unpin to profile",
+ security: [%{"oAuth" => ["write:accounts"]}],
+ description: "Unfeature a status from the top of your profile",
+ operationId: "StatusController.unpin",
+ parameters: [id_param()],
+ responses: %{
+ 200 => status_response(),
+ 400 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def bookmark_operation do
+ %Operation{
+ tags: ["Statuses"],
+ summary: "Bookmark",
+ security: [%{"oAuth" => ["write:bookmarks"]}],
+ description: "Privately bookmark a status",
+ operationId: "StatusController.bookmark",
+ parameters: [id_param()],
+ responses: %{
+ 200 => status_response()
+ }
+ }
+ end
+
+ def unbookmark_operation do
+ %Operation{
+ tags: ["Statuses"],
+ summary: "Undo bookmark",
+ security: [%{"oAuth" => ["write:bookmarks"]}],
+ description: "Remove a status from your private bookmarks",
+ operationId: "StatusController.unbookmark",
+ parameters: [id_param()],
+ responses: %{
+ 200 => status_response()
+ }
+ }
+ end
+
+ def mute_conversation_operation do
+ %Operation{
+ tags: ["Statuses"],
+ summary: "Mute conversation",
+ security: [%{"oAuth" => ["write:mutes"]}],
+ description: "Do not receive notifications for the thread that this status is part of.",
+ operationId: "StatusController.mute_conversation",
+ parameters: [id_param()],
+ responses: %{
+ 200 => status_response(),
+ 400 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def unmute_conversation_operation do
+ %Operation{
+ tags: ["Statuses"],
+ summary: "Unmute conversation",
+ security: [%{"oAuth" => ["write:mutes"]}],
+ description:
+ "Start receiving notifications again for the thread that this status is part of",
+ operationId: "StatusController.unmute_conversation",
+ parameters: [id_param()],
+ responses: %{
+ 200 => status_response(),
+ 400 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def card_operation do
+ %Operation{
+ tags: ["Statuses"],
+ deprecated: true,
+ summary: "Preview card",
+ description: "Deprecated in favor of card property inlined on Status entity",
+ operationId: "StatusController.card",
+ parameters: [id_param()],
+ security: [%{"oAuth" => ["read:statuses"]}],
+ responses: %{
+ 200 =>
+ Operation.response("Card", "application/json", %Schema{
+ type: :object,
+ nullable: true,
+ properties: %{
+ type: %Schema{type: :string, enum: ["link", "photo", "video", "rich"]},
+ provider_name: %Schema{type: :string, nullable: true},
+ provider_url: %Schema{type: :string, format: :uri},
+ url: %Schema{type: :string, format: :uri},
+ image: %Schema{type: :string, nullable: true, format: :uri},
+ title: %Schema{type: :string},
+ description: %Schema{type: :string}
+ }
+ })
+ }
+ }
+ end
+
+ def favourited_by_operation do
+ %Operation{
+ tags: ["Statuses"],
+ summary: "Favourited by",
+ description: "View who favourited a given status",
+ operationId: "StatusController.favourited_by",
+ security: [%{"oAuth" => ["read:accounts"]}],
+ parameters: [id_param()],
+ responses: %{
+ 200 =>
+ Operation.response(
+ "Array of Accounts",
+ "application/json",
+ AccountOperation.array_of_accounts()
+ ),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def reblogged_by_operation do
+ %Operation{
+ tags: ["Statuses"],
+ summary: "Boosted by",
+ description: "View who boosted a given status",
+ operationId: "StatusController.reblogged_by",
+ security: [%{"oAuth" => ["read:accounts"]}],
+ parameters: [id_param()],
+ responses: %{
+ 200 =>
+ Operation.response(
+ "Array of Accounts",
+ "application/json",
+ AccountOperation.array_of_accounts()
+ ),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def context_operation do
+ %Operation{
+ tags: ["Statuses"],
+ summary: "Parent and child statuses",
+ description: "View statuses above and below this status in the thread",
+ operationId: "StatusController.context",
+ security: [%{"oAuth" => ["read:statuses"]}],
+ parameters: [id_param()],
+ responses: %{
+ 200 => Operation.response("Context", "application/json", context())
+ }
+ }
+ end
+
+ def favourites_operation do
+ %Operation{
+ tags: ["Statuses"],
+ summary: "Favourited statuses",
+ description: "Statuses the user has favourited",
+ operationId: "StatusController.favourites",
+ parameters: pagination_params(),
+ security: [%{"oAuth" => ["read:favourites"]}],
+ responses: %{
+ 200 => Operation.response("Array of Statuses", "application/json", array_of_statuses())
+ }
+ }
+ end
+
+ def bookmarks_operation do
+ %Operation{
+ tags: ["Statuses"],
+ summary: "Bookmarked statuses",
+ description: "Statuses the user has bookmarked",
+ operationId: "StatusController.bookmarks",
+ parameters: pagination_params(),
+ security: [%{"oAuth" => ["read:bookmarks"]}],
+ responses: %{
+ 200 => Operation.response("Array of Statuses", "application/json", array_of_statuses())
+ }
+ }
+ end
+
+ def array_of_statuses do
+ %Schema{type: :array, items: Status, example: [Status.schema().example]}
+ end
+
+ defp create_request do
+ %Schema{
+ title: "StatusCreateRequest",
+ type: :object,
+ properties: %{
+ status: %Schema{
+ type: :string,
+ nullable: true,
+ description:
+ "Text content of the status. If `media_ids` is provided, this becomes optional. Attaching a `poll` is optional while `status` is provided."
+ },
+ media_ids: %Schema{
+ nullable: true,
+ type: :array,
+ items: %Schema{type: :string},
+ description: "Array of Attachment ids to be attached as media."
+ },
+ poll: %Schema{
+ nullable: true,
+ type: :object,
+ required: [:options],
+ properties: %{
+ options: %Schema{
+ type: :array,
+ items: %Schema{type: :string},
+ description: "Array of possible answers. Must be provided with `poll[expires_in]`."
+ },
+ expires_in: %Schema{
+ type: :integer,
+ nullable: true,
+ description:
+ "Duration the poll should be open, in seconds. Must be provided with `poll[options]`"
+ },
+ multiple: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Allow multiple choices?"
+ },
+ hide_totals: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Hide vote counts until the poll ends?"
+ }
+ }
+ },
+ in_reply_to_id: %Schema{
+ nullable: true,
+ allOf: [FlakeID],
+ description: "ID of the status being replied to, if status is a reply"
+ },
+ sensitive: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Mark status and attached media as sensitive?"
+ },
+ spoiler_text: %Schema{
+ type: :string,
+ nullable: true,
+ description:
+ "Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field."
+ },
+ scheduled_at: %Schema{
+ type: :string,
+ format: :"date-time",
+ nullable: true,
+ description:
+ "ISO 8601 Datetime at which to schedule a status. Providing this paramter will cause ScheduledStatus to be returned instead of Status. Must be at least 5 minutes in the future."
+ },
+ language: %Schema{
+ type: :string,
+ nullable: true,
+ description: "ISO 639 language code for this status."
+ },
+ # Pleroma-specific properties:
+ preview: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description:
+ "If set to `true` the post won't be actually posted, but the status entitiy would still be rendered back. This could be useful for previewing rich text/custom emoji, for example"
+ },
+ content_type: %Schema{
+ type: :string,
+ nullable: true,
+ description:
+ "The MIME type of the status, it is transformed into HTML by the backend. You can get the list of the supported MIME types with the nodeinfo endpoint."
+ },
+ to: %Schema{
+ type: :array,
+ nullable: true,
+ items: %Schema{type: :string},
+ description:
+ "A list of nicknames (like `lain@soykaf.club` or `lain` on the local server) that will be used to determine who is going to be addressed by this post. Using this will disable the implicit addressing by mentioned names in the `status` body, only the people in the `to` list will be addressed. The normal rules for for post visibility are not affected by this and will still apply"
+ },
+ visibility: %Schema{
+ nullable: true,
+ anyOf: [
+ VisibilityScope,
+ %Schema{type: :string, description: "`list:LIST_ID`", example: "LIST:123"}
+ ],
+ description:
+ "Visibility of the posted status. Besides standard MastoAPI values (`direct`, `private`, `unlisted` or `public`) it can be used to address a List by setting it to `list:LIST_ID`"
+ },
+ expires_in: %Schema{
+ nullable: true,
+ type: :integer,
+ description:
+ "The number of seconds the posted activity should expire in. When a posted activity expires it will be deleted from the server, and a delete request for it will be federated. This needs to be longer than an hour."
+ },
+ in_reply_to_conversation_id: %Schema{
+ nullable: true,
+ type: :string,
+ description:
+ "Will reply to a given conversation, addressing only the people who are part of the recipient set of that conversation. Sets the visibility to `direct`."
+ }
+ },
+ example: %{
+ "status" => "What time is it?",
+ "sensitive" => "false",
+ "poll" => %{
+ "options" => ["Cofe", "Adventure"],
+ "expires_in" => 420
+ }
+ }
+ }
+ end
+
+ def id_param do
+ Operation.parameter(:id, :path, FlakeID, "Status ID",
+ example: "9umDrYheeY451cQnEe",
+ required: true
+ )
+ end
+
+ defp status_response do
+ Operation.response("Status", "application/json", Status)
+ end
+
+ defp context do
+ %Schema{
+ title: "StatusContext",
+ description:
+ "Represents the tree around a given status. Used for reconstructing threads of statuses.",
+ type: :object,
+ required: [:ancestors, :descendants],
+ properties: %{
+ ancestors: array_of_statuses(),
+ descendants: array_of_statuses()
+ },
+ example: %{
+ "ancestors" => [Status.schema().example],
+ "descendants" => [Status.schema().example]
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/subscription_operation.ex b/lib/pleroma/web/api_spec/operations/subscription_operation.ex
new file mode 100644
index 000000000..c575a87e6
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/subscription_operation.ex
@@ -0,0 +1,227 @@
+# 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.SubscriptionOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Helpers
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+ alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
+ alias Pleroma.Web.ApiSpec.Schemas.PushSubscription
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def create_operation do
+ %Operation{
+ tags: ["Push Subscriptions"],
+ summary: "Subscribe to push notifications",
+ description:
+ "Add a Web Push API subscription to receive notifications. Each access token can have one push subscription. If you create a new subscription, the old subscription is deleted.",
+ operationId: "SubscriptionController.create",
+ security: [%{"oAuth" => ["push"]}],
+ requestBody: Helpers.request_body("Parameters", create_request(), required: true),
+ responses: %{
+ 200 => Operation.response("Push Subscription", "application/json", PushSubscription),
+ 400 => Operation.response("Error", "application/json", ApiError),
+ 403 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def show_operation do
+ %Operation{
+ tags: ["Push Subscriptions"],
+ summary: "Get current subscription",
+ description: "View the PushSubscription currently associated with this access token.",
+ operationId: "SubscriptionController.show",
+ security: [%{"oAuth" => ["push"]}],
+ responses: %{
+ 200 => Operation.response("Push Subscription", "application/json", PushSubscription),
+ 403 => Operation.response("Error", "application/json", ApiError),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def update_operation do
+ %Operation{
+ tags: ["Push Subscriptions"],
+ summary: "Change types of notifications",
+ description:
+ "Updates the current push subscription. Only the data part can be updated. To change fundamentals, a new subscription must be created instead.",
+ operationId: "SubscriptionController.update",
+ security: [%{"oAuth" => ["push"]}],
+ requestBody: Helpers.request_body("Parameters", update_request(), required: true),
+ responses: %{
+ 200 => Operation.response("Push Subscription", "application/json", PushSubscription),
+ 403 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def delete_operation do
+ %Operation{
+ tags: ["Push Subscriptions"],
+ summary: "Remove current subscription",
+ description: "Removes the current Web Push API subscription.",
+ operationId: "SubscriptionController.delete",
+ security: [%{"oAuth" => ["push"]}],
+ responses: %{
+ 200 => Operation.response("Empty object", "application/json", %Schema{type: :object}),
+ 403 => Operation.response("Error", "application/json", ApiError),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp create_request do
+ %Schema{
+ title: "SubscriptionCreateRequest",
+ description: "POST body for creating a push subscription",
+ type: :object,
+ properties: %{
+ subscription: %Schema{
+ type: :object,
+ properties: %{
+ endpoint: %Schema{
+ type: :string,
+ description: "Endpoint URL that is called when a notification event occurs."
+ },
+ keys: %Schema{
+ type: :object,
+ properties: %{
+ p256dh: %Schema{
+ type: :string,
+ description:
+ "User agent public key. Base64 encoded string of public key of ECDH key using `prime256v1` curve."
+ },
+ auth: %Schema{
+ type: :string,
+ description: "Auth secret. Base64 encoded string of 16 bytes of random data."
+ }
+ },
+ required: [:p256dh, :auth]
+ }
+ },
+ required: [:endpoint, :keys]
+ },
+ data: %Schema{
+ nullable: true,
+ type: :object,
+ properties: %{
+ alerts: %Schema{
+ nullable: true,
+ type: :object,
+ properties: %{
+ follow: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Receive follow notifications?"
+ },
+ favourite: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Receive favourite notifications?"
+ },
+ reblog: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Receive reblog notifications?"
+ },
+ mention: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Receive mention notifications?"
+ },
+ poll: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Receive poll notifications?"
+ }
+ }
+ }
+ }
+ }
+ },
+ required: [:subscription],
+ example: %{
+ "subscription" => %{
+ "endpoint" => "https://example.com/example/1234",
+ "keys" => %{
+ "auth" => "8eDyX_uCN0XRhSbY5hs7Hg==",
+ "p256dh" =>
+ "BCIWgsnyXDv1VkhqL2P7YRBvdeuDnlwAPT2guNhdIoW3IP7GmHh1SMKPLxRf7x8vJy6ZFK3ol2ohgn_-0yP7QQA="
+ }
+ },
+ "data" => %{
+ "alerts" => %{
+ "follow" => true,
+ "mention" => true,
+ "poll" => false
+ }
+ }
+ }
+ }
+ end
+
+ defp update_request do
+ %Schema{
+ title: "SubscriptionUpdateRequest",
+ type: :object,
+ properties: %{
+ data: %Schema{
+ nullable: true,
+ type: :object,
+ properties: %{
+ alerts: %Schema{
+ nullable: true,
+ type: :object,
+ properties: %{
+ follow: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Receive follow notifications?"
+ },
+ favourite: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Receive favourite notifications?"
+ },
+ reblog: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Receive reblog notifications?"
+ },
+ mention: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Receive mention notifications?"
+ },
+ poll: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Receive poll notifications?"
+ }
+ }
+ }
+ }
+ }
+ },
+ example: %{
+ "data" => %{
+ "alerts" => %{
+ "follow" => true,
+ "favourite" => true,
+ "reblog" => true,
+ "mention" => true,
+ "poll" => true
+ }
+ }
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/timeline_operation.ex b/lib/pleroma/web/api_spec/operations/timeline_operation.ex
new file mode 100644
index 000000000..8e19bace7
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/timeline_operation.ex
@@ -0,0 +1,191 @@
+# 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.TimelineOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+ alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
+ alias Pleroma.Web.ApiSpec.Schemas.Status
+ alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
+
+ import Pleroma.Web.ApiSpec.Helpers
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def home_operation do
+ %Operation{
+ tags: ["Timelines"],
+ summary: "Home timeline",
+ description: "View statuses from followed users",
+ security: [%{"oAuth" => ["read:statuses"]}],
+ parameters: [
+ local_param(),
+ with_muted_param(),
+ exclude_visibilities_param(),
+ reply_visibility_param() | pagination_params()
+ ],
+ operationId: "TimelineController.home",
+ responses: %{
+ 200 => Operation.response("Array of Status", "application/json", array_of_statuses())
+ }
+ }
+ end
+
+ def direct_operation do
+ %Operation{
+ tags: ["Timelines"],
+ summary: "Direct timeline",
+ description:
+ "View statuses with a “direct” privacy, from your account or in your notifications",
+ deprecated: true,
+ parameters: [with_muted_param() | pagination_params()],
+ security: [%{"oAuth" => ["read:statuses"]}],
+ operationId: "TimelineController.direct",
+ responses: %{
+ 200 => Operation.response("Array of Status", "application/json", array_of_statuses())
+ }
+ }
+ end
+
+ def public_operation do
+ %Operation{
+ tags: ["Timelines"],
+ summary: "Public timeline",
+ security: [%{"oAuth" => ["read:statuses"]}],
+ parameters: [
+ local_param(),
+ only_media_param(),
+ with_muted_param(),
+ exclude_visibilities_param(),
+ reply_visibility_param() | pagination_params()
+ ],
+ operationId: "TimelineController.public",
+ responses: %{
+ 200 => Operation.response("Array of Status", "application/json", array_of_statuses()),
+ 401 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def hashtag_operation do
+ %Operation{
+ tags: ["Timelines"],
+ summary: "Hashtag timeline",
+ description: "View public statuses containing the given hashtag",
+ security: [%{"oAuth" => ["read:statuses"]}],
+ parameters: [
+ Operation.parameter(
+ :tag,
+ :path,
+ %Schema{type: :string},
+ "Content of a #hashtag, not including # symbol.",
+ required: true
+ ),
+ Operation.parameter(
+ :any,
+ :query,
+ %Schema{type: :array, items: %Schema{type: :string}},
+ "Statuses that also includes any of these tags"
+ ),
+ Operation.parameter(
+ :all,
+ :query,
+ %Schema{type: :array, items: %Schema{type: :string}},
+ "Statuses that also includes all of these tags"
+ ),
+ Operation.parameter(
+ :none,
+ :query,
+ %Schema{type: :array, items: %Schema{type: :string}},
+ "Statuses that do not include these tags"
+ ),
+ local_param(),
+ only_media_param(),
+ with_muted_param(),
+ exclude_visibilities_param() | pagination_params()
+ ],
+ operationId: "TimelineController.hashtag",
+ responses: %{
+ 200 => Operation.response("Array of Status", "application/json", array_of_statuses())
+ }
+ }
+ end
+
+ def list_operation do
+ %Operation{
+ tags: ["Timelines"],
+ summary: "List timeline",
+ description: "View statuses in the given list timeline",
+ security: [%{"oAuth" => ["read:lists"]}],
+ parameters: [
+ Operation.parameter(
+ :list_id,
+ :path,
+ %Schema{type: :string},
+ "Local ID of the list in the database",
+ required: true
+ ),
+ with_muted_param(),
+ exclude_visibilities_param() | pagination_params()
+ ],
+ operationId: "TimelineController.list",
+ responses: %{
+ 200 => Operation.response("Array of Status", "application/json", array_of_statuses())
+ }
+ }
+ end
+
+ defp array_of_statuses do
+ %Schema{
+ title: "ArrayOfStatuses",
+ type: :array,
+ items: Status,
+ example: [Status.schema().example]
+ }
+ end
+
+ defp local_param do
+ Operation.parameter(
+ :local,
+ :query,
+ %Schema{allOf: [BooleanLike], default: false},
+ "Show only local statuses?"
+ )
+ end
+
+ defp with_muted_param do
+ Operation.parameter(:with_muted, :query, BooleanLike, "Includeactivities by muted users")
+ end
+
+ defp exclude_visibilities_param do
+ Operation.parameter(
+ :exclude_visibilities,
+ :query,
+ %Schema{type: :array, items: VisibilityScope},
+ "Exclude the statuses with the given visibilities"
+ )
+ end
+
+ defp reply_visibility_param do
+ Operation.parameter(
+ :reply_visibility,
+ :query,
+ %Schema{type: :string, enum: ["following", "self"]},
+ "Filter replies. Possible values: without parameter (default) shows all replies, `following` - replies directed to you or users you follow, `self` - replies directed to you."
+ )
+ end
+
+ defp only_media_param do
+ Operation.parameter(
+ :only_media,
+ :query,
+ %Schema{allOf: [BooleanLike], default: false},
+ "Show only statuses with media attached?"
+ )
+ end
+end
diff --git a/lib/pleroma/web/api_spec/render_error.ex b/lib/pleroma/web/api_spec/render_error.ex
index b5877ca9c..d476b8ef3 100644
--- a/lib/pleroma/web/api_spec/render_error.ex
+++ b/lib/pleroma/web/api_spec/render_error.ex
@@ -17,6 +17,9 @@ defmodule Pleroma.Web.ApiSpec.RenderError do
def call(conn, errors) do
errors =
Enum.map(errors, fn
+ %{name: nil, reason: :invalid_enum} = err ->
+ %OpenApiSpex.Cast.Error{err | name: err.value}
+
%{name: nil} = err ->
%OpenApiSpex.Cast.Error{err | name: List.last(err.path)}
diff --git a/lib/pleroma/web/api_spec/schemas/attachment.ex b/lib/pleroma/web/api_spec/schemas/attachment.ex
new file mode 100644
index 000000000..c6edf6d36
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/attachment.ex
@@ -0,0 +1,68 @@
+# 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.Schemas.Attachment do
+ alias OpenApiSpex.Schema
+
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "Attachment",
+ description: "Represents a file or media attachment that can be added to a status.",
+ type: :object,
+ requried: [:id, :url, :preview_url],
+ properties: %{
+ id: %Schema{type: :string, description: "The ID of the attachment in the database."},
+ url: %Schema{
+ type: :string,
+ format: :uri,
+ description: "The location of the original full-size attachment"
+ },
+ remote_url: %Schema{
+ type: :string,
+ format: :uri,
+ description:
+ "The location of the full-size original attachment on the remote website. String (URL), or null if the attachment is local",
+ nullable: true
+ },
+ preview_url: %Schema{
+ type: :string,
+ format: :uri,
+ description: "The location of a scaled-down preview of the attachment"
+ },
+ text_url: %Schema{
+ type: :string,
+ format: :uri,
+ description: "A shorter URL for the attachment"
+ },
+ description: %Schema{
+ type: :string,
+ nullable: true,
+ description:
+ "Alternate text that describes what is in the media attachment, to be used for the visually impaired or when media attachments do not load"
+ },
+ type: %Schema{
+ type: :string,
+ enum: ["image", "video", "audio", "unknown"],
+ description: "The type of the attachment"
+ },
+ pleroma: %Schema{
+ type: :object,
+ properties: %{
+ mime_type: %Schema{type: :string, description: "mime type of the attachment"}
+ }
+ }
+ },
+ example: %{
+ id: "1638338801",
+ type: "image",
+ url: "someurl",
+ remote_url: "someurl",
+ preview_url: "someurl",
+ text_url: "someurl",
+ description: nil,
+ pleroma: %{mime_type: "image/png"}
+ }
+ })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/conversation.ex b/lib/pleroma/web/api_spec/schemas/conversation.ex
new file mode 100644
index 000000000..d8ff5ba26
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/conversation.ex
@@ -0,0 +1,41 @@
+# 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.Schemas.Conversation do
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.Account
+ alias Pleroma.Web.ApiSpec.Schemas.Status
+
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "Conversation",
+ description: "Represents a conversation with \"direct message\" visibility.",
+ type: :object,
+ required: [:id, :accounts, :unread],
+ properties: %{
+ id: %Schema{type: :string},
+ accounts: %Schema{
+ type: :array,
+ items: Account,
+ description: "Participants in the conversation"
+ },
+ unread: %Schema{
+ type: :boolean,
+ description: "Is the conversation currently marked as unread?"
+ },
+ # last_status: Status
+ last_status: %Schema{
+ allOf: [Status],
+ description: "The last status in the conversation, to be used for optional display"
+ }
+ },
+ example: %{
+ "id" => "418450",
+ "unread" => true,
+ "accounts" => [Account.schema().example],
+ "last_status" => Status.schema().example
+ }
+ })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/list.ex b/lib/pleroma/web/api_spec/schemas/list.ex
new file mode 100644
index 000000000..b7d1685c9
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/list.ex
@@ -0,0 +1,23 @@
+# 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.Schemas.List do
+ alias OpenApiSpex.Schema
+
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "List",
+ description: "Represents a list of users",
+ type: :object,
+ properties: %{
+ id: %Schema{type: :string, description: "The internal database ID of the list"},
+ title: %Schema{type: :string, description: "The user-defined title of the list"}
+ },
+ example: %{
+ "id" => "12249",
+ "title" => "Friends"
+ }
+ })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/poll.ex b/lib/pleroma/web/api_spec/schemas/poll.ex
index 0474b550b..c62096db0 100644
--- a/lib/pleroma/web/api_spec/schemas/poll.ex
+++ b/lib/pleroma/web/api_spec/schemas/poll.ex
@@ -11,26 +11,72 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Poll do
OpenApiSpex.schema(%{
title: "Poll",
- description: "Response schema for account custom fields",
+ description: "Represents a poll attached to a status",
type: :object,
properties: %{
id: FlakeID,
- expires_at: %Schema{type: :string, format: "date-time"},
- expired: %Schema{type: :boolean},
- multiple: %Schema{type: :boolean},
- votes_count: %Schema{type: :integer},
- voted: %Schema{type: :boolean},
- emojis: %Schema{type: :array, items: Emoji},
+ expires_at: %Schema{
+ type: :string,
+ format: :"date-time",
+ nullable: true,
+ description: "When the poll ends"
+ },
+ expired: %Schema{type: :boolean, description: "Is the poll currently expired?"},
+ multiple: %Schema{
+ type: :boolean,
+ description: "Does the poll allow multiple-choice answers?"
+ },
+ votes_count: %Schema{
+ type: :integer,
+ nullable: true,
+ description: "How many votes have been received. Number, or null if `multiple` is false."
+ },
+ voted: %Schema{
+ type: :boolean,
+ nullable: true,
+ description:
+ "When called with a user token, has the authorized user voted? Boolean, or null if no current user."
+ },
+ emojis: %Schema{
+ type: :array,
+ items: Emoji,
+ description: "Custom emoji to be used for rendering poll options."
+ },
options: %Schema{
type: :array,
items: %Schema{
+ title: "PollOption",
type: :object,
properties: %{
title: %Schema{type: :string},
votes_count: %Schema{type: :integer}
}
- }
+ },
+ description: "Possible answers for the poll."
}
+ },
+ example: %{
+ id: "34830",
+ expires_at: "2019-12-05T04:05:08.302Z",
+ expired: true,
+ multiple: false,
+ votes_count: 10,
+ voters_count: nil,
+ voted: true,
+ own_votes: [
+ 1
+ ],
+ options: [
+ %{
+ title: "accept",
+ votes_count: 6
+ },
+ %{
+ title: "deny",
+ votes_count: 4
+ }
+ ],
+ emojis: []
}
})
end
diff --git a/lib/pleroma/web/api_spec/schemas/push_subscription.ex b/lib/pleroma/web/api_spec/schemas/push_subscription.ex
new file mode 100644
index 000000000..cc91b95b8
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/push_subscription.ex
@@ -0,0 +1,66 @@
+# 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.Schemas.PushSubscription do
+ alias OpenApiSpex.Schema
+
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "PushSubscription",
+ description: "Response schema for a push subscription",
+ type: :object,
+ properties: %{
+ id: %Schema{
+ anyOf: [%Schema{type: :string}, %Schema{type: :integer}],
+ description: "The id of the push subscription in the database."
+ },
+ endpoint: %Schema{type: :string, description: "Where push alerts will be sent to."},
+ server_key: %Schema{type: :string, description: "The streaming server's VAPID key."},
+ alerts: %Schema{
+ type: :object,
+ description: "Which alerts should be delivered to the endpoint.",
+ properties: %{
+ follow: %Schema{
+ type: :boolean,
+ description: "Receive a push notification when someone has followed you?"
+ },
+ favourite: %Schema{
+ type: :boolean,
+ description:
+ "Receive a push notification when a status you created has been favourited by someone else?"
+ },
+ reblog: %Schema{
+ type: :boolean,
+ description:
+ "Receive a push notification when a status you created has been boosted by someone else?"
+ },
+ mention: %Schema{
+ type: :boolean,
+ description:
+ "Receive a push notification when someone else has mentioned you in a status?"
+ },
+ poll: %Schema{
+ type: :boolean,
+ description:
+ "Receive a push notification when a poll you voted in or created has ended? "
+ }
+ }
+ }
+ },
+ example: %{
+ "id" => "328_183",
+ "endpoint" => "https://yourdomain.example/listener",
+ "alerts" => %{
+ "follow" => true,
+ "favourite" => true,
+ "reblog" => true,
+ "mention" => true,
+ "poll" => true
+ },
+ "server_key" =>
+ "BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M="
+ }
+ })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/scheduled_status.ex b/lib/pleroma/web/api_spec/schemas/scheduled_status.ex
new file mode 100644
index 000000000..0520d0848
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/scheduled_status.ex
@@ -0,0 +1,54 @@
+# 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.Schemas.ScheduledStatus do
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.Attachment
+ alias Pleroma.Web.ApiSpec.Schemas.Poll
+ alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
+
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "ScheduledStatus",
+ description: "Represents a status that will be published at a future scheduled date.",
+ type: :object,
+ required: [:id, :scheduled_at, :params],
+ properties: %{
+ id: %Schema{type: :string},
+ scheduled_at: %Schema{type: :string, format: :"date-time"},
+ media_attachments: %Schema{type: :array, items: Attachment},
+ params: %Schema{
+ type: :object,
+ required: [:text, :visibility],
+ properties: %{
+ text: %Schema{type: :string, nullable: true},
+ media_ids: %Schema{type: :array, nullable: true, items: %Schema{type: :string}},
+ sensitive: %Schema{type: :boolean, nullable: true},
+ spoiler_text: %Schema{type: :string, nullable: true},
+ visibility: %Schema{type: VisibilityScope, nullable: true},
+ scheduled_at: %Schema{type: :string, format: :"date-time", nullable: true},
+ poll: %Schema{type: Poll, nullable: true},
+ in_reply_to_id: %Schema{type: :string, nullable: true}
+ }
+ }
+ },
+ example: %{
+ id: "3221",
+ scheduled_at: "2019-12-05T12:33:01.000Z",
+ params: %{
+ text: "test content",
+ media_ids: nil,
+ sensitive: nil,
+ spoiler_text: nil,
+ visibility: nil,
+ scheduled_at: nil,
+ poll: nil,
+ idempotency: nil,
+ in_reply_to_id: nil
+ },
+ media_attachments: [Attachment.schema().example]
+ }
+ })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex
index aef0588d4..8b87cb25b 100644
--- a/lib/pleroma/web/api_spec/schemas/status.ex
+++ b/lib/pleroma/web/api_spec/schemas/status.ex
@@ -5,9 +5,11 @@
defmodule Pleroma.Web.ApiSpec.Schemas.Status do
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.Account
+ alias Pleroma.Web.ApiSpec.Schemas.Attachment
alias Pleroma.Web.ApiSpec.Schemas.Emoji
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
alias Pleroma.Web.ApiSpec.Schemas.Poll
+ alias Pleroma.Web.ApiSpec.Schemas.Tag
alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
require OpenApiSpex
@@ -17,78 +19,137 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
description: "Response schema for a status",
type: :object,
properties: %{
- account: Account,
+ account: %Schema{allOf: [Account], description: "The account that authored this status"},
application: %Schema{
+ description: "The application used to post this status",
type: :object,
properties: %{
name: %Schema{type: :string},
website: %Schema{type: :string, nullable: true, format: :uri}
}
},
- bookmarked: %Schema{type: :boolean},
+ bookmarked: %Schema{type: :boolean, description: "Have you bookmarked this status?"},
card: %Schema{
type: :object,
nullable: true,
+ description: "Preview card for links included within status content",
+ required: [:url, :title, :description, :type],
properties: %{
- type: %Schema{type: :string, enum: ["link", "photo", "video", "rich"]},
- provider_name: %Schema{type: :string, nullable: true},
- provider_url: %Schema{type: :string, format: :uri},
- url: %Schema{type: :string, format: :uri},
- image: %Schema{type: :string, nullable: true, format: :uri},
- title: %Schema{type: :string},
- description: %Schema{type: :string}
+ type: %Schema{
+ type: :string,
+ enum: ["link", "photo", "video", "rich"],
+ description: "The type of the preview card"
+ },
+ provider_name: %Schema{
+ type: :string,
+ nullable: true,
+ description: "The provider of the original resource"
+ },
+ provider_url: %Schema{
+ type: :string,
+ format: :uri,
+ description: "A link to the provider of the original resource"
+ },
+ url: %Schema{type: :string, format: :uri, description: "Location of linked resource"},
+ image: %Schema{
+ type: :string,
+ nullable: true,
+ format: :uri,
+ description: "Preview thumbnail"
+ },
+ title: %Schema{type: :string, description: "Title of linked resource"},
+ description: %Schema{type: :string, description: "Description of preview"}
}
},
- content: %Schema{type: :string, format: :html},
- created_at: %Schema{type: :string, format: "date-time"},
- emojis: %Schema{type: :array, items: Emoji},
- favourited: %Schema{type: :boolean},
- favourites_count: %Schema{type: :integer},
+ content: %Schema{type: :string, format: :html, description: "HTML-encoded status content"},
+ created_at: %Schema{
+ type: :string,
+ format: "date-time",
+ description: "The date when this status was created"
+ },
+ emojis: %Schema{
+ type: :array,
+ items: Emoji,
+ description: "Custom emoji to be used when rendering status content"
+ },
+ favourited: %Schema{type: :boolean, description: "Have you favourited this status?"},
+ favourites_count: %Schema{
+ type: :integer,
+ description: "How many favourites this status has received"
+ },
id: FlakeID,
- in_reply_to_account_id: %Schema{type: :string, nullable: true},
- in_reply_to_id: %Schema{type: :string, nullable: true},
- language: %Schema{type: :string, nullable: true},
+ in_reply_to_account_id: %Schema{
+ allOf: [FlakeID],
+ nullable: true,
+ description: "ID of the account being replied to"
+ },
+ in_reply_to_id: %Schema{
+ allOf: [FlakeID],
+ nullable: true,
+ description: "ID of the status being replied"
+ },
+ language: %Schema{
+ type: :string,
+ nullable: true,
+ description: "Primary language of this status"
+ },
media_attachments: %Schema{
type: :array,
- items: %Schema{
- type: :object,
- properties: %{
- id: %Schema{type: :string},
- url: %Schema{type: :string, format: :uri},
- remote_url: %Schema{type: :string, format: :uri},
- preview_url: %Schema{type: :string, format: :uri},
- text_url: %Schema{type: :string, format: :uri},
- description: %Schema{type: :string},
- type: %Schema{type: :string, enum: ["image", "video", "audio", "unknown"]},
- pleroma: %Schema{
- type: :object,
- properties: %{mime_type: %Schema{type: :string}}
- }
- }
- }
+ items: Attachment,
+ description: "Media that is attached to this status"
},
mentions: %Schema{
type: :array,
+ description: "Mentions of users within the status content",
items: %Schema{
type: :object,
properties: %{
- id: %Schema{type: :string},
- acct: %Schema{type: :string},
- username: %Schema{type: :string},
- url: %Schema{type: :string, format: :uri}
+ id: %Schema{allOf: [FlakeID], description: "The account id of the mentioned user"},
+ acct: %Schema{
+ type: :string,
+ description:
+ "The webfinger acct: URI of the mentioned user. Equivalent to `username` for local users, or `username@domain` for remote users."
+ },
+ username: %Schema{type: :string, description: "The username of the mentioned user"},
+ url: %Schema{
+ type: :string,
+ format: :uri,
+ description: "The location of the mentioned user's profile"
+ }
}
}
},
- muted: %Schema{type: :boolean},
- pinned: %Schema{type: :boolean},
+ muted: %Schema{
+ type: :boolean,
+ description: "Have you muted notifications for this status's conversation?"
+ },
+ pinned: %Schema{
+ type: :boolean,
+ description: "Have you pinned this status? Only appears if the status is pinnable."
+ },
pleroma: %Schema{
type: :object,
properties: %{
- content: %Schema{type: :object, additionalProperties: %Schema{type: :string}},
- conversation_id: %Schema{type: :integer},
- direct_conversation_id: %Schema{type: :string, nullable: true},
+ content: %Schema{
+ type: :object,
+ additionalProperties: %Schema{type: :string},
+ description:
+ "A map consisting of alternate representations of the `content` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`"
+ },
+ conversation_id: %Schema{
+ type: :integer,
+ description: "The ID of the AP context the status is associated with (if any)"
+ },
+ direct_conversation_id: %Schema{
+ type: :integer,
+ nullable: true,
+ description:
+ "The ID of the Mastodon direct message conversation the status is associated with (if any)"
+ },
emoji_reactions: %Schema{
type: :array,
+ description:
+ "A list with emoji / reaction maps. Contains no information about the reacting users, for that use the /statuses/:id/reactions endpoint.",
items: %Schema{
type: :object,
properties: %{
@@ -98,36 +159,74 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
}
}
},
- expires_at: %Schema{type: :string, format: "date-time", nullable: true},
- in_reply_to_account_acct: %Schema{type: :string, nullable: true},
- local: %Schema{type: :boolean},
- spoiler_text: %Schema{type: :object, additionalProperties: %Schema{type: :string}},
- thread_muted: %Schema{type: :boolean}
+ expires_at: %Schema{
+ type: :string,
+ format: "date-time",
+ nullable: true,
+ description:
+ "A datetime (ISO 8601) that states when the post will expire (be deleted automatically), or empty if the post won't expire"
+ },
+ in_reply_to_account_acct: %Schema{
+ type: :string,
+ nullable: true,
+ description: "The `acct` property of User entity for replied user (if any)"
+ },
+ local: %Schema{
+ type: :boolean,
+ description: "`true` if the post was made on the local instance"
+ },
+ spoiler_text: %Schema{
+ type: :object,
+ additionalProperties: %Schema{type: :string},
+ description:
+ "A map consisting of alternate representations of the `spoiler_text` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`."
+ },
+ thread_muted: %Schema{
+ type: :boolean,
+ description: "`true` if the thread the post belongs to is muted"
+ }
}
},
- poll: %Schema{type: Poll, nullable: true},
+ poll: %Schema{allOf: [Poll], nullable: true, description: "The poll attached to the status"},
reblog: %Schema{
allOf: [%OpenApiSpex.Reference{"$ref": "#/components/schemas/Status"}],
- nullable: true
- },
- reblogged: %Schema{type: :boolean},
- reblogs_count: %Schema{type: :integer},
- replies_count: %Schema{type: :integer},
- sensitive: %Schema{type: :boolean},
- spoiler_text: %Schema{type: :string},
- tags: %Schema{
- type: :array,
- items: %Schema{
- type: :object,
- properties: %{
- name: %Schema{type: :string},
- url: %Schema{type: :string, format: :uri}
- }
- }
+ nullable: true,
+ description: "The status being reblogged"
+ },
+ reblogged: %Schema{type: :boolean, description: "Have you boosted this status?"},
+ reblogs_count: %Schema{
+ type: :integer,
+ description: "How many boosts this status has received"
+ },
+ replies_count: %Schema{
+ type: :integer,
+ description: "How many replies this status has received"
+ },
+ sensitive: %Schema{
+ type: :boolean,
+ description: "Is this status marked as sensitive content?"
+ },
+ spoiler_text: %Schema{
+ type: :string,
+ description:
+ "Subject or summary line, below which status content is collapsed until expanded"
+ },
+ tags: %Schema{type: :array, items: Tag},
+ uri: %Schema{
+ type: :string,
+ format: :uri,
+ description: "URI of the status used for federation"
+ },
+ url: %Schema{
+ type: :string,
+ nullable: true,
+ format: :uri,
+ description: "A link to the status's HTML representation"
},
- uri: %Schema{type: :string, format: :uri},
- url: %Schema{type: :string, nullable: true, format: :uri},
- visibility: VisibilityScope
+ visibility: %Schema{
+ allOf: [VisibilityScope],
+ description: "Visibility of this status"
+ }
},
example: %{
"account" => %{
diff --git a/lib/pleroma/web/api_spec/schemas/tag.ex b/lib/pleroma/web/api_spec/schemas/tag.ex
new file mode 100644
index 000000000..e693fb83e
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/tag.ex
@@ -0,0 +1,27 @@
+# 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.Schemas.Tag do
+ alias OpenApiSpex.Schema
+
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "Tag",
+ description: "Represents a hashtag used within the content of a status",
+ type: :object,
+ properties: %{
+ name: %Schema{type: :string, description: "The value of the hashtag after the # sign"},
+ url: %Schema{
+ type: :string,
+ format: :uri,
+ description: "A link to the hashtag on the instance"
+ }
+ },
+ example: %{
+ name: "cofe",
+ url: "https://lain.com/tag/cofe"
+ }
+ })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/visibility_scope.ex b/lib/pleroma/web/api_spec/schemas/visibility_scope.ex
index 8c81a4d73..831734e27 100644
--- a/lib/pleroma/web/api_spec/schemas/visibility_scope.ex
+++ b/lib/pleroma/web/api_spec/schemas/visibility_scope.ex
@@ -9,6 +9,6 @@ defmodule Pleroma.Web.ApiSpec.Schemas.VisibilityScope do
title: "VisibilityScope",
description: "Status visibility",
type: :string,
- enum: ["public", "unlisted", "private", "direct"]
+ enum: ["public", "unlisted", "private", "direct", "list"]
})
end
diff --git a/lib/pleroma/web/auth/pleroma_authenticator.ex b/lib/pleroma/web/auth/pleroma_authenticator.ex
index cb09664ce..200ca03dc 100644
--- a/lib/pleroma/web/auth/pleroma_authenticator.ex
+++ b/lib/pleroma/web/auth/pleroma_authenticator.ex
@@ -16,11 +16,12 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do
def get_user(%Plug.Conn{} = conn) do
with {:ok, {name, password}} <- fetch_credentials(conn),
{_, %User{} = user} <- {:user, fetch_user(name)},
- {_, true} <- {:checkpw, AuthenticationPlug.checkpw(password, user.password_hash)} do
+ {_, true} <- {:checkpw, AuthenticationPlug.checkpw(password, user.password_hash)},
+ {:ok, user} <- AuthenticationPlug.maybe_update_password(user, password) do
{:ok, user}
else
- error ->
- {:error, error}
+ {:error, _reason} = error -> error
+ error -> {:error, error}
end
end
diff --git a/lib/pleroma/web/auth/totp_authenticator.ex b/lib/pleroma/web/auth/totp_authenticator.ex
new file mode 100644
index 000000000..1794e407c
--- /dev/null
+++ b/lib/pleroma/web/auth/totp_authenticator.ex
@@ -0,0 +1,45 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Auth.TOTPAuthenticator do
+ alias Pleroma.MFA
+ alias Pleroma.MFA.TOTP
+ alias Pleroma.Plugs.AuthenticationPlug
+ alias Pleroma.User
+
+ @doc "Verify code or check backup code."
+ @spec verify(String.t(), User.t()) ::
+ {:ok, :pass} | {:error, :invalid_token | :invalid_secret_and_token}
+ def verify(
+ token,
+ %User{
+ multi_factor_authentication_settings:
+ %{enabled: true, totp: %{secret: secret, confirmed: true}} = _
+ } = _user
+ )
+ when is_binary(token) and byte_size(token) > 0 do
+ TOTP.validate_token(secret, token)
+ end
+
+ def verify(_, _), do: {:error, :invalid_token}
+
+ @spec verify_recovery_code(User.t(), String.t()) ::
+ {:ok, :pass} | {:error, :invalid_token}
+ def verify_recovery_code(
+ %User{multi_factor_authentication_settings: %{enabled: true, backup_codes: codes}} = user,
+ code
+ )
+ when is_list(codes) and is_binary(code) do
+ hash_code = Enum.find(codes, fn hash -> AuthenticationPlug.checkpw(code, hash) end)
+
+ if hash_code do
+ MFA.invalidate_backup_code(user, hash_code)
+ {:ok, :pass}
+ else
+ {:error, :invalid_token}
+ end
+ end
+
+ def verify_recovery_code(_, _), do: {:error, :invalid_token}
+end
diff --git a/lib/pleroma/web/chat_channel.ex b/lib/pleroma/web/chat_channel.ex
index 38ec774f7..bce27897f 100644
--- a/lib/pleroma/web/chat_channel.ex
+++ b/lib/pleroma/web/chat_channel.ex
@@ -23,6 +23,7 @@ defmodule Pleroma.Web.ChatChannel do
if String.length(text) in 1..Pleroma.Config.get([:instance, :chat_limit]) do
author = User.get_cached_by_nickname(user_name)
author = Pleroma.Web.MastodonAPI.AccountView.render("show.json", user: author)
+
message = ChatChannelState.add_message(%{text: text, author: author})
broadcast!(socket, "new_msg", message)
diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex
index 244cf2be5..3f1a50b96 100644
--- a/lib/pleroma/web/common_api/activity_draft.ex
+++ b/lib/pleroma/web/common_api/activity_draft.ex
@@ -58,16 +58,16 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
end
defp put_params(draft, params) do
- params = Map.put_new(params, "in_reply_to_status_id", params["in_reply_to_id"])
+ params = Map.put_new(params, :in_reply_to_status_id, params[:in_reply_to_id])
%__MODULE__{draft | params: params}
end
- defp status(%{params: %{"status" => status}} = draft) do
+ defp status(%{params: %{status: status}} = draft) do
%__MODULE__{draft | status: String.trim(status)}
end
defp summary(%{params: params} = draft) do
- %__MODULE__{draft | summary: Map.get(params, "spoiler_text", "")}
+ %__MODULE__{draft | summary: Map.get(params, :spoiler_text, "")}
end
defp full_payload(%{status: status, summary: summary} = draft) do
@@ -84,20 +84,20 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
%__MODULE__{draft | attachments: attachments}
end
- defp in_reply_to(%{params: %{"in_reply_to_status_id" => ""}} = draft), do: draft
+ defp in_reply_to(%{params: %{in_reply_to_status_id: ""}} = draft), do: draft
- defp in_reply_to(%{params: %{"in_reply_to_status_id" => id}} = draft) when is_binary(id) do
+ defp in_reply_to(%{params: %{in_reply_to_status_id: id}} = draft) when is_binary(id) do
%__MODULE__{draft | in_reply_to: Activity.get_by_id(id)}
end
- defp in_reply_to(%{params: %{"in_reply_to_status_id" => %Activity{} = in_reply_to}} = draft) do
+ defp in_reply_to(%{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}} = draft) do
%__MODULE__{draft | in_reply_to: in_reply_to}
end
defp in_reply_to(draft), do: draft
defp in_reply_to_conversation(draft) do
- in_reply_to_conversation = Participation.get(draft.params["in_reply_to_conversation_id"])
+ in_reply_to_conversation = Participation.get(draft.params[:in_reply_to_conversation_id])
%__MODULE__{draft | in_reply_to_conversation: in_reply_to_conversation}
end
@@ -112,7 +112,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
end
defp expires_at(draft) do
- case CommonAPI.check_expiry_date(draft.params["expires_in"]) do
+ case CommonAPI.check_expiry_date(draft.params[:expires_in]) do
{:ok, expires_at} -> %__MODULE__{draft | expires_at: expires_at}
{:error, message} -> add_error(draft, message)
end
@@ -144,7 +144,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
addressed_users =
draft.mentions
|> Enum.map(fn {_, mentioned_user} -> mentioned_user.ap_id end)
- |> Utils.get_addressed_users(draft.params["to"])
+ |> Utils.get_addressed_users(draft.params[:to])
{to, cc} =
Utils.get_to_and_cc(
@@ -164,7 +164,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
end
defp sensitive(draft) do
- sensitive = draft.params["sensitive"] || Enum.member?(draft.tags, {"#nsfw", "nsfw"})
+ sensitive = draft.params[:sensitive] || Enum.member?(draft.tags, {"#nsfw", "nsfw"})
%__MODULE__{draft | sensitive: sensitive}
end
@@ -191,7 +191,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
end
defp preview?(draft) do
- preview? = Pleroma.Web.ControllerHelper.truthy_param?(draft.params["preview"])
+ preview? = Pleroma.Web.ControllerHelper.truthy_param?(draft.params[:preview])
%__MODULE__{draft | preview?: preview?}
end
diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex
index f9db97d24..dbb3d7ade 100644
--- a/lib/pleroma/web/common_api/common_api.ex
+++ b/lib/pleroma/web/common_api/common_api.ex
@@ -24,6 +24,25 @@ defmodule Pleroma.Web.CommonAPI do
require Pleroma.Constants
require Logger
+ def unblock(blocker, blocked) do
+ with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},
+ {:ok, unblock_data, _} <- Builder.undo(blocker, block),
+ {:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do
+ {:ok, unblock}
+ else
+ {:fetch_block, nil} ->
+ if User.blocks?(blocker, blocked) do
+ User.unblock(blocker, blocked)
+ {:ok, :no_activity}
+ else
+ {:error, :not_blocking}
+ end
+
+ e ->
+ e
+ end
+ end
+
def follow(follower, followed) do
timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
@@ -75,41 +94,63 @@ defmodule Pleroma.Web.CommonAPI do
end
def delete(activity_id, user) do
- with {_, %Activity{data: %{"object" => _}} = activity} <-
- {:find_activity, Activity.get_by_id_with_object(activity_id)},
- %Object{} = object <- Object.normalize(activity),
+ with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
+ {:find_activity, Activity.get_by_id(activity_id)},
+ {_, %Object{} = object, _} <-
+ {:find_object, Object.normalize(activity, false), activity},
true <- User.superuser?(user) || user.ap_id == object.data["actor"],
- {:ok, _} <- unpin(activity_id, user),
- {:ok, delete} <- ActivityPub.delete(object) do
+ {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
+ {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
{:ok, delete}
else
- {:find_activity, _} -> {:error, :not_found}
- _ -> {:error, dgettext("errors", "Could not delete")}
+ {:find_activity, _} ->
+ {:error, :not_found}
+
+ {:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} ->
+ # 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(actor, object),
+ {:ok, _tombstone} <- Object.create(tombstone_data) do
+ delete(activity_id, user)
+ else
+ _ ->
+ Logger.error(
+ "Could not insert tombstone for missing object on deletion. Object is #{object}."
+ )
+
+ {:error, dgettext("errors", "Could not delete")}
+ end
+
+ _ ->
+ {:error, dgettext("errors", "Could not delete")}
end
end
def repeat(id, user, params \\ %{}) do
- with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
- {:find_activity, Activity.get_by_id(id)},
- object <- Object.normalize(activity),
- announce_activity <- Utils.get_existing_announce(user.ap_id, object),
- public <- public_announce?(object, params) do
- if announce_activity do
- {:ok, announce_activity, object}
- else
- ActivityPub.announce(user, object, nil, true, public)
- end
+ with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
+ object = %Object{} <- Object.normalize(activity, false),
+ {_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
+ public = public_announce?(object, params),
+ {:ok, announce, _} <- Builder.announce(user, object, public: public),
+ {:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do
+ {:ok, activity}
else
- {:find_activity, _} -> {:error, :not_found}
- _ -> {:error, dgettext("errors", "Could not repeat")}
+ {:existing_announce, %Activity{} = announce} ->
+ {:ok, announce}
+
+ _ ->
+ {:error, :not_found}
end
end
def unrepeat(id, user) do
with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
- {:find_activity, Activity.get_by_id(id)} do
- object = Object.normalize(activity)
- ActivityPub.unannounce(user, object)
+ {:find_activity, Activity.get_by_id(id)},
+ %Object{} = note <- Object.normalize(activity, false),
+ %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
+ {:ok, undo, _} <- Builder.undo(user, announce),
+ {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
+ {:ok, activity}
else
{:find_activity, _} -> {:error, :not_found}
_ -> {:error, dgettext("errors", "Could not unrepeat")}
@@ -166,9 +207,12 @@ defmodule Pleroma.Web.CommonAPI do
def unfavorite(id, user) do
with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
- {:find_activity, Activity.get_by_id(id)} do
- object = Object.normalize(activity)
- ActivityPub.unlike(user, object)
+ {:find_activity, Activity.get_by_id(id)},
+ %Object{} = note <- Object.normalize(activity, false),
+ %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
+ {:ok, undo, _} <- Builder.undo(user, like),
+ {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
+ {:ok, activity}
else
{:find_activity, _} -> {:error, :not_found}
_ -> {:error, dgettext("errors", "Could not unfavorite")}
@@ -177,8 +221,10 @@ defmodule Pleroma.Web.CommonAPI do
def react_with_emoji(id, user, emoji) do
with %Activity{} = activity <- Activity.get_by_id(id),
- object <- Object.normalize(activity) do
- ActivityPub.react_with_emoji(user, object, emoji)
+ object <- Object.normalize(activity),
+ {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
+ {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
+ {:ok, activity}
else
_ ->
{:error, dgettext("errors", "Could not add reaction emoji")}
@@ -186,8 +232,10 @@ defmodule Pleroma.Web.CommonAPI do
end
def unreact_with_emoji(id, user, emoji) do
- with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji) do
- ActivityPub.unreact_with_emoji(user, reaction_activity.data["id"])
+ with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
+ {:ok, undo, _} <- Builder.undo(user, reaction_activity),
+ {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
+ {:ok, activity}
else
_ ->
{:error, dgettext("errors", "Could not remove reaction emoji")}
@@ -249,7 +297,7 @@ defmodule Pleroma.Web.CommonAPI do
end
end
- def public_announce?(_, %{"visibility" => visibility})
+ def public_announce?(_, %{visibility: visibility})
when visibility in ~w{public unlisted private direct},
do: visibility in ~w(public unlisted)
@@ -259,11 +307,11 @@ defmodule Pleroma.Web.CommonAPI do
def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
- def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
+ def get_visibility(%{visibility: visibility}, in_reply_to, _)
when visibility in ~w{public unlisted private direct},
do: {visibility, get_replied_to_visibility(in_reply_to)}
- def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
+ def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
visibility = {:list, String.to_integer(list_id)}
{visibility, get_replied_to_visibility(in_reply_to)}
end
@@ -300,11 +348,14 @@ defmodule Pleroma.Web.CommonAPI do
|> check_expiry_date()
end
- def listen(user, %{"title" => _} = data) do
- with visibility <- data["visibility"] || "public",
- {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
+ def listen(user, data) do
+ visibility = Map.get(data, :visibility, "public")
+
+ with {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
listen_data <-
- Map.take(data, ["album", "artist", "title", "length"])
+ data
+ |> Map.take([:album, :artist, :title, :length])
+ |> Map.new(fn {key, value} -> {to_string(key), value} end)
|> Map.put("type", "Audio")
|> Map.put("to", to)
|> Map.put("cc", cc)
@@ -321,7 +372,7 @@ defmodule Pleroma.Web.CommonAPI do
end
end
- def post(user, %{"status" => _} = data) do
+ def post(user, %{status: _} = data) do
with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
draft.changes
|> ActivityPub.create(draft.preview?)
@@ -430,11 +481,11 @@ defmodule Pleroma.Web.CommonAPI do
end
end
- defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
- toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
+ defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
+ toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
end
- defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
+ defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
when is_boolean(sensitive) do
new_data = Map.put(object.data, "sensitive", sensitive)
@@ -448,7 +499,7 @@ defmodule Pleroma.Web.CommonAPI do
defp toggle_sensitive(activity, _), do: {:ok, activity}
- defp set_visibility(activity, %{"visibility" => visibility}) do
+ defp set_visibility(activity, %{visibility: visibility}) do
Utils.update_activity_visibility(activity, visibility)
end
diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index 6540fa5d1..6ec489f9a 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -22,11 +22,11 @@ defmodule Pleroma.Web.CommonAPI.Utils do
require Logger
require Pleroma.Constants
- def attachments_from_ids(%{"media_ids" => ids, "descriptions" => desc} = _) do
+ def attachments_from_ids(%{media_ids: ids, descriptions: desc}) do
attachments_from_ids_descs(ids, desc)
end
- def attachments_from_ids(%{"media_ids" => ids} = _) do
+ def attachments_from_ids(%{media_ids: ids}) do
attachments_from_ids_no_descs(ids)
end
@@ -37,11 +37,11 @@ defmodule Pleroma.Web.CommonAPI.Utils do
def attachments_from_ids_no_descs(ids) do
Enum.map(ids, fn media_id ->
case Repo.get(Object, media_id) do
- %Object{data: data} = _ -> data
+ %Object{data: data} -> data
_ -> nil
end
end)
- |> Enum.filter(& &1)
+ |> Enum.reject(&is_nil/1)
end
def attachments_from_ids_descs([], _), do: []
@@ -51,14 +51,14 @@ defmodule Pleroma.Web.CommonAPI.Utils do
Enum.map(ids, fn media_id ->
case Repo.get(Object, media_id) do
- %Object{data: data} = _ ->
+ %Object{data: data} ->
Map.put(data, "name", descs[media_id])
_ ->
nil
end
end)
- |> Enum.filter(& &1)
+ |> Enum.reject(&is_nil/1)
end
@spec get_to_and_cc(
@@ -102,7 +102,8 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
def get_to_and_cc(_user, mentioned_users, inReplyTo, "direct", _) do
- if inReplyTo do
+ # If the OP is a DM already, add the implicit actor.
+ if inReplyTo && Visibility.is_direct?(inReplyTo) do
{Enum.uniq([inReplyTo.data["actor"] | mentioned_users]), []}
else
{mentioned_users, []}
@@ -140,7 +141,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|> make_poll_data()
end
- def make_poll_data(%{"poll" => %{"options" => options, "expires_in" => expires_in}} = data)
+ def make_poll_data(%{poll: %{options: options, expires_in: expires_in}} = data)
when is_list(options) do
limits = Pleroma.Config.get([:instance, :poll_limits])
@@ -163,7 +164,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|> DateTime.add(expires_in)
|> DateTime.to_iso8601()
- key = if truthy_param?(data["poll"]["multiple"]), do: "anyOf", else: "oneOf"
+ key = if truthy_param?(data.poll[:multiple]), do: "anyOf", else: "oneOf"
poll = %{"type" => "Question", key => option_notes, "closed" => end_time}
{:ok, {poll, emoji}}
@@ -213,7 +214,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|> Map.get("attachment_links", Config.get([:instance, :attachment_links]))
|> truthy_param?()
- content_type = get_content_type(data["content_type"])
+ content_type = get_content_type(data[:content_type])
options =
if visibility == "direct" && Config.get([:instance, :safe_dm_mentions]) do
@@ -395,13 +396,16 @@ defmodule Pleroma.Web.CommonAPI.Utils do
def to_masto_date(_), do: ""
defp shortname(name) do
- if String.length(name) < 30 do
- name
+ with max_length when max_length > 0 <-
+ Config.get([Pleroma.Upload, :filename_display_max_length], 30),
+ true <- String.length(name) > max_length do
+ String.slice(name, 0..max_length) <> "…"
else
- String.slice(name, 0..30) <> "…"
+ _ -> name
end
end
+ @spec confirm_current_password(User.t(), String.t()) :: {:ok, User.t()} | {:error, String.t()}
def confirm_current_password(user, password) do
with %User{local: true} = db_user <- User.get_cached_by_id(user.id),
true <- AuthenticationPlug.checkpw(password, db_user.password_hash) do
@@ -466,6 +470,8 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|> Enum.map(& &1.ap_id)
recipients ++ subscriber_ids
+ else
+ _e -> recipients
end
end
@@ -477,6 +483,8 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|> User.get_followers()
|> Enum.map(& &1.ap_id)
|> Enum.concat(recipients)
+ else
+ _e -> recipients
end
end
diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex
index eb97ae975..5a1316a5f 100644
--- a/lib/pleroma/web/controller_helper.ex
+++ b/lib/pleroma/web/controller_helper.ex
@@ -5,8 +5,6 @@
defmodule Pleroma.Web.ControllerHelper do
use Pleroma.Web, :controller
- alias Pleroma.Config
-
# As in Mastodon API, per https://api.rubyonrails.org/classes/ActiveModel/Type/Boolean.html
@falsy_param_values [false, 0, "0", "f", "F", "false", "False", "FALSE", "off", "OFF"]
@@ -106,13 +104,16 @@ defmodule Pleroma.Web.ControllerHelper do
def put_if_exist(map, key, value), do: Map.put(map, key, value)
- @doc "Whether to skip rendering `[:account][:pleroma][:relationship]`for statuses/notifications"
- def skip_relationships?(params) do
- if Config.get([:extensions, :output_relationships_in_statuses_by_default]) do
- false
- else
- # BREAKING: older PleromaFE versions do not send this param but _do_ expect relationships.
- not truthy_param?(params["with_relationships"])
- end
+ @doc """
+ Returns true if request specifies to include embedded relationships in account objects.
+ May only be used in selected account-related endpoints; has no effect for status- or
+ notification-related endpoints.
+ """
+ # Intended for PleromaFE: https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838
+ def embed_relationships?(params) do
+ # To do once OpenAPI transition mess is over: just `truthy_param?(params[:with_relationships])`
+ params
+ |> Map.get(:with_relationships, params["with_relationships"])
+ |> truthy_param?()
end
end
diff --git a/lib/pleroma/web/controllers/frontend/static_controller.ex b/lib/pleroma/web/controllers/frontend/static_controller.ex
index ee821c61c..54eb40350 100644
--- a/lib/pleroma/web/controllers/frontend/static_controller.ex
+++ b/lib/pleroma/web/controllers/frontend/static_controller.ex
@@ -15,7 +15,7 @@ defmodule Pleroma.Web.Frontend.StaticController do
plug(:put_layout, :static_fe)
plug(Pleroma.Plugs.EnsureAuthenticatedPlug,
- unless_func: &Pleroma.Web.FederatingPlug.federating?/0
+ unless_func: &Pleroma.Web.FederatingPlug.federating?/1
)
@page_keys ["max_id", "min_id", "limit", "since_id", "order"]
diff --git a/lib/pleroma/web/feed/user_controller.ex b/lib/pleroma/web/feed/user_controller.ex
index 3bad4663d..019eb6b86 100644
--- a/lib/pleroma/web/feed/user_controller.ex
+++ b/lib/pleroma/web/feed/user_controller.ex
@@ -30,7 +30,7 @@ defmodule Pleroma.Web.Feed.UserController do
when format in ["json", "activity+json"] do
with %{halted: false} = conn <-
Pleroma.Plugs.EnsureAuthenticatedPlug.call(conn,
- unless_func: &Pleroma.Web.FederatingPlug.federating?/0
+ unless_func: &Pleroma.Web.FederatingPlug.federating?/1
) do
ActivityPubController.call(conn, :user)
end
@@ -59,7 +59,7 @@ defmodule Pleroma.Web.Feed.UserController do
"actor_id" => user.ap_id
}
|> put_if_exist("max_id", params["max_id"])
- |> ActivityPub.fetch_public_activities()
+ |> ActivityPub.fetch_public_or_unlisted_activities()
conn
|> put_resp_content_type("application/#{format}+xml")
diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
index 61b0e2f63..47649d41d 100644
--- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
@@ -10,8 +10,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
add_link_headers: 2,
truthy_param?: 1,
assign_account_by_id: 2,
- json_response: 3,
- skip_relationships?: 1
+ embed_relationships?: 1,
+ json_response: 3
]
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
@@ -27,7 +27,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.TwitterAPI.TwitterAPI
- plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create)
@@ -81,7 +81,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
plug(
RateLimiter,
- [name: :relation_id_action, params: ["id", "uri"]] when action in @relationship_actions
+ [name: :relation_id_action, params: [:id, :uri]] when action in @relationship_actions
)
plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
@@ -177,6 +177,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
)
|> add_if_present(params, :pleroma_settings_store, :pleroma_settings_store)
|> add_if_present(params, :default_scope, :default_scope)
+ |> add_if_present(params["source"], "privacy", :default_scope)
|> add_if_present(params, :actor_type, :actor_type)
changeset = User.update_changeset(user, user_params)
@@ -189,7 +190,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
- with true <- Map.has_key?(params, params_field),
+ with true <- is_map(params),
+ true <- Map.has_key?(params, params_field),
{:ok, new_value} <- value_function.(Map.get(params, params_field)) do
Map.put(map, map_field, new_value)
else
@@ -247,8 +249,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|> render("index.json",
activities: activities,
for: reading_user,
- as: :activity,
- skip_relationships: skip_relationships?(params)
+ as: :activity
)
else
_e -> render_error(conn, :not_found, "Can't find user")
@@ -271,7 +272,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
conn
|> add_link_headers(followers)
- |> render("index.json", for: for_user, users: followers, as: :user)
+ # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
+ |> render("index.json",
+ for: for_user,
+ users: followers,
+ as: :user,
+ embed_relationships: embed_relationships?(params)
+ )
end
@doc "GET /api/v1/accounts/:id/following"
@@ -290,7 +297,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
conn
|> add_link_headers(followers)
- |> render("index.json", for: for_user, users: followers, as: :user)
+ # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
+ |> render("index.json",
+ for: for_user,
+ users: followers,
+ as: :user,
+ embed_relationships: embed_relationships?(params)
+ )
end
@doc "GET /api/v1/accounts/:id/lists"
@@ -356,8 +369,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
@doc "POST /api/v1/accounts/:id/unblock"
def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
- with {:ok, _user_block} <- User.unblock(blocker, blocked),
- {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
+ with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
render(conn, "relationship.json", user: blocker, target: blocked)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
diff --git a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex
index 408e11474..a516b6c20 100644
--- a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex
@@ -22,7 +22,7 @@ defmodule Pleroma.Web.MastodonAPI.AppController do
plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :verify_credentials)
- plug(OpenApiSpex.Plug.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
@local_mastodon_name "Mastodon-Local"
diff --git a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex
index c44641526..f35ec3596 100644
--- a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex
@@ -13,9 +13,12 @@ defmodule Pleroma.Web.MastodonAPI.ConversationController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action == :index)
plug(OAuthScopesPlug, %{scopes: ["write:conversations"]} when action != :index)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ConversationOperation
+
@doc "GET /api/v1/conversations"
def index(%{assigns: %{user: user}} = conn, params) do
participations = Participation.for_user_with_last_activity_id(user, params)
@@ -26,7 +29,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationController do
end
@doc "POST /api/v1/conversations/:id/read"
- def mark_as_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
+ def mark_as_read(%{assigns: %{user: user}} = conn, %{id: participation_id}) do
with %Participation{} = participation <-
Repo.get_by(Participation, id: participation_id, user_id: user.id),
{:ok, participation} <- Participation.mark_as_read(participation) do
diff --git a/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex b/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex
index 000ad743f..c5f47c5df 100644
--- a/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex
@@ -5,7 +5,7 @@
defmodule Pleroma.Web.MastodonAPI.CustomEmojiController do
use Pleroma.Web, :controller
- plug(OpenApiSpex.Plug.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(
:skip_plug,
diff --git a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
index c4fa383f2..825b231ab 100644
--- a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
@@ -8,7 +8,7 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockController do
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
- plug(OpenApiSpex.Plug.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.DomainBlockOperation
plug(
diff --git a/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex b/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex
index 0a257f604..8af557b61 100644
--- a/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex
@@ -20,6 +20,10 @@ defmodule Pleroma.Web.MastodonAPI.FallbackController do
render_error(conn, :not_found, "Record not found")
end
+ def call(conn, {:error, :forbidden}) do
+ render_error(conn, :forbidden, "Access denied")
+ end
+
def call(conn, {:error, error_message}) do
conn
|> put_status(:bad_request)
diff --git a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
index 7fd0562c9..abbf0ce02 100644
--- a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
@@ -10,6 +10,7 @@ defmodule Pleroma.Web.MastodonAPI.FilterController do
@oauth_read_actions [:show, :index]
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(OAuthScopesPlug, %{scopes: ["read:filters"]} when action in @oauth_read_actions)
plug(
@@ -17,60 +18,60 @@ defmodule Pleroma.Web.MastodonAPI.FilterController do
%{scopes: ["write:filters"]} when action not in @oauth_read_actions
)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.FilterOperation
+
@doc "GET /api/v1/filters"
def index(%{assigns: %{user: user}} = conn, _) do
filters = Filter.get_filters(user)
- render(conn, "filters.json", filters: filters)
+ render(conn, "index.json", filters: filters)
end
@doc "POST /api/v1/filters"
- def create(
- %{assigns: %{user: user}} = conn,
- %{"phrase" => phrase, "context" => context} = params
- ) do
+ def create(%{assigns: %{user: user}, body_params: params} = conn, _) do
query = %Filter{
user_id: user.id,
- phrase: phrase,
- context: context,
- hide: Map.get(params, "irreversible", false),
- whole_word: Map.get(params, "boolean", true)
- # expires_at
+ phrase: params.phrase,
+ context: params.context,
+ hide: params.irreversible,
+ whole_word: params.whole_word
+ # TODO: support `expires_in` parameter (as in Mastodon API)
}
{:ok, response} = Filter.create(query)
- render(conn, "filter.json", filter: response)
+ render(conn, "show.json", filter: response)
end
@doc "GET /api/v1/filters/:id"
- def show(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
+ def show(%{assigns: %{user: user}} = conn, %{id: filter_id}) do
filter = Filter.get(filter_id, user)
- render(conn, "filter.json", filter: filter)
+ render(conn, "show.json", filter: filter)
end
@doc "PUT /api/v1/filters/:id"
def update(
- %{assigns: %{user: user}} = conn,
- %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
+ %{assigns: %{user: user}, body_params: params} = conn,
+ %{id: filter_id}
) do
- query = %Filter{
- user_id: user.id,
- filter_id: filter_id,
- phrase: phrase,
- context: context,
- hide: Map.get(params, "irreversible", nil),
- whole_word: Map.get(params, "boolean", true)
- # expires_at
- }
-
- {:ok, response} = Filter.update(query)
- render(conn, "filter.json", filter: response)
+ params =
+ params
+ |> Map.delete(:irreversible)
+ |> Map.put(:hide, params[:irreversible])
+ |> Enum.reject(fn {_key, value} -> is_nil(value) end)
+ |> Map.new()
+
+ # TODO: support `expires_in` parameter (as in Mastodon API)
+
+ with %Filter{} = filter <- Filter.get(filter_id, user),
+ {:ok, %Filter{} = filter} <- Filter.update(filter, params) do
+ render(conn, "show.json", filter: filter)
+ end
end
@doc "DELETE /api/v1/filters/:id"
- def delete(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
+ def delete(%{assigns: %{user: user}} = conn, %{id: filter_id}) do
query = %Filter{
user_id: user.id,
filter_id: filter_id
diff --git a/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex
index 25f2269b9..748b6b475 100644
--- a/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex
@@ -10,6 +10,7 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestController do
alias Pleroma.Web.CommonAPI
plug(:put_view, Pleroma.Web.MastodonAPI.AccountView)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(:assign_follower when action != :index)
action_fallback(:errors)
@@ -21,6 +22,8 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestController do
%{scopes: ["follow", "write:follows"]} when action != :index
)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.FollowRequestOperation
+
@doc "GET /api/v1/follow_requests"
def index(%{assigns: %{user: followed}} = conn, _params) do
follow_requests = User.get_follow_requests(followed)
@@ -42,7 +45,7 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestController do
end
end
- defp assign_follower(%{params: %{"id" => id}} = conn, _) do
+ defp assign_follower(%{params: %{id: id}} = conn, _) do
case User.get_cached_by_id(id) do
%User{} = follower -> assign(conn, :follower, follower)
nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
diff --git a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex
index 237f85677..d8859731d 100644
--- a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex
@@ -5,12 +5,16 @@
defmodule Pleroma.Web.MastodonAPI.InstanceController do
use Pleroma.Web, :controller
+ plug(OpenApiSpex.Plug.CastAndValidate)
+
plug(
:skip_plug,
[Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]
when action in [:show, :peers]
)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.InstanceOperation
+
@doc "GET /api/v1/instance"
def show(conn, _params) do
render(conn, "show.json")
diff --git a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex
index bfe856025..acdc76fd2 100644
--- a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex
@@ -9,20 +9,17 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
alias Pleroma.User
alias Pleroma.Web.MastodonAPI.AccountView
- plug(:list_by_id_and_user when action not in [:index, :create])
-
@oauth_read_actions [:index, :show, :list_accounts]
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(:list_by_id_and_user when action not in [:index, :create])
plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action in @oauth_read_actions)
-
- plug(
- OAuthScopesPlug,
- %{scopes: ["write:lists"]}
- when action not in @oauth_read_actions
- )
+ plug(OAuthScopesPlug, %{scopes: ["write:lists"]} when action not in @oauth_read_actions)
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ListOperation
+
# GET /api/v1/lists
def index(%{assigns: %{user: user}} = conn, opts) do
lists = Pleroma.List.for_user(user, opts)
@@ -30,7 +27,7 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
end
# POST /api/v1/lists
- def create(%{assigns: %{user: user}} = conn, %{"title" => title}) do
+ def create(%{assigns: %{user: user}, body_params: %{title: title}} = conn, _) do
with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
render(conn, "show.json", list: list)
end
@@ -42,7 +39,7 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
end
# PUT /api/v1/lists/:id
- def update(%{assigns: %{list: list}} = conn, %{"title" => title}) do
+ def update(%{assigns: %{list: list}, body_params: %{title: title}} = conn, _) do
with {:ok, list} <- Pleroma.List.rename(list, title) do
render(conn, "show.json", list: list)
end
@@ -65,7 +62,7 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
end
# POST /api/v1/lists/:id/accounts
- def add_to_list(%{assigns: %{list: list}} = conn, %{"account_ids" => account_ids}) do
+ def add_to_list(%{assigns: %{list: list}, body_params: %{account_ids: account_ids}} = conn, _) do
Enum.each(account_ids, fn account_id ->
with %User{} = followed <- User.get_cached_by_id(account_id) do
Pleroma.List.follow(list, followed)
@@ -76,7 +73,10 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
end
# DELETE /api/v1/lists/:id/accounts
- def remove_from_list(%{assigns: %{list: list}} = conn, %{"account_ids" => account_ids}) do
+ def remove_from_list(
+ %{assigns: %{list: list}, body_params: %{account_ids: account_ids}} = conn,
+ _
+ ) do
Enum.each(account_ids, fn account_id ->
with %User{} = followed <- User.get_cached_by_id(account_id) do
Pleroma.List.unfollow(list, followed)
@@ -86,7 +86,7 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
json(conn, %{})
end
- defp list_by_id_and_user(%{assigns: %{user: user}, params: %{"id" => id}} = conn, _) do
+ defp list_by_id_and_user(%{assigns: %{user: user}, params: %{id: id}} = conn, _) do
case Pleroma.List.get(id, user) do
%Pleroma.List{} = list -> assign(conn, :list, list)
nil -> conn |> render_error(:not_found, "List not found") |> halt()
diff --git a/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex b/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex
index 9f9d4574e..85310edfa 100644
--- a/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex
@@ -6,6 +6,8 @@ defmodule Pleroma.Web.MastodonAPI.MarkerController do
use Pleroma.Web, :controller
alias Pleroma.Plugs.OAuthScopesPlug
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
plug(
OAuthScopesPlug,
%{scopes: ["read:statuses"]}
@@ -16,14 +18,18 @@ defmodule Pleroma.Web.MastodonAPI.MarkerController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.MarkerOperation
+
# GET /api/v1/markers
def index(%{assigns: %{user: user}} = conn, params) do
- markers = Pleroma.Marker.get_markers(user, params["timeline"])
+ markers = Pleroma.Marker.get_markers(user, params[:timeline])
render(conn, "markers.json", %{markers: markers})
end
# POST /api/v1/markers
- def upsert(%{assigns: %{user: user}} = conn, params) do
+ def upsert(%{assigns: %{user: user}, body_params: params} = conn, _) do
+ params = Map.new(params, fn {key, value} -> {to_string(key), value} end)
+
with {:ok, result} <- Pleroma.Marker.upsert(user, params),
markers <- Map.values(result) do
render(conn, "markers.json", %{markers: markers})
diff --git a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex
index e36751220..513de279f 100644
--- a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex
@@ -11,17 +11,21 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do
alias Pleroma.Web.ActivityPub.ActivityPub
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(:put_view, Pleroma.Web.MastodonAPI.StatusView)
- plug(OAuthScopesPlug, %{scopes: ["write:media"]})
+ plug(OAuthScopesPlug, %{scopes: ["read:media"]} when action == :show)
+ plug(OAuthScopesPlug, %{scopes: ["write:media"]} when action != :show)
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.MediaOperation
@doc "POST /api/v1/media"
- def create(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
+ def create(%{assigns: %{user: user}, body_params: %{file: file} = data} = conn, _) do
with {:ok, object} <-
ActivityPub.upload(
file,
actor: User.ap_id(user),
- description: Map.get(data, "description")
+ description: Map.get(data, :description)
) do
attachment_data = Map.put(object.data, "id", object.id)
@@ -29,11 +33,30 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do
end
end
+ def create(_conn, _data), do: {:error, :bad_request}
+
+ @doc "POST /api/v2/media"
+ def create2(%{assigns: %{user: user}, body_params: %{file: file} = data} = conn, _) do
+ with {:ok, object} <-
+ ActivityPub.upload(
+ file,
+ actor: User.ap_id(user),
+ description: Map.get(data, :description)
+ ) do
+ attachment_data = Map.put(object.data, "id", object.id)
+
+ conn
+ |> put_status(202)
+ |> render("attachment.json", %{attachment: attachment_data})
+ end
+ end
+
+ def create2(_conn, _data), do: {:error, :bad_request}
+
@doc "PUT /api/v1/media/:id"
- def update(%{assigns: %{user: user}} = conn, %{"id" => id, "description" => description})
- when is_binary(description) do
+ def update(%{assigns: %{user: user}, body_params: %{description: description}} = conn, %{id: id}) do
with %Object{} = object <- Object.get_by_id(id),
- true <- Object.authorize_mutation(object, user),
+ :ok <- Object.authorize_access(object, user),
{:ok, %Object{data: data}} <- Object.update_data(object, %{"name" => description}) do
attachment_data = Map.put(data, "id", object.id)
@@ -41,5 +64,17 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do
end
end
- def update(_conn, _data), do: {:error, :bad_request}
+ def update(conn, data), do: show(conn, data)
+
+ @doc "GET /api/v1/media/:id"
+ def show(%{assigns: %{user: user}} = conn, %{id: id}) do
+ with %Object{data: data, id: object_id} = object <- Object.get_by_id(id),
+ :ok <- Object.authorize_access(object, user) do
+ attachment_data = Map.put(data, "id", object_id)
+
+ render(conn, "attachment.json", %{attachment: attachment_data})
+ end
+ end
+
+ def show(_conn, _data), do: {:error, :bad_request}
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
index a14c86893..bcd12c73f 100644
--- a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
@@ -5,7 +5,7 @@
defmodule Pleroma.Web.MastodonAPI.NotificationController do
use Pleroma.Web, :controller
- import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2, skip_relationships?: 1]
+ import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
alias Pleroma.Notification
alias Pleroma.Plugs.OAuthScopesPlug
@@ -13,7 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
@oauth_read_actions [:show, :index]
- plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(
OAuthScopesPlug,
@@ -50,8 +50,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
|> add_link_headers(notifications)
|> render("index.json",
notifications: notifications,
- for: user,
- skip_relationships: skip_relationships?(params)
+ for: user
)
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex
index af9b66eff..db46ffcfc 100644
--- a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex
@@ -15,6 +15,8 @@ defmodule Pleroma.Web.MastodonAPI.PollController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
plug(
OAuthScopesPlug,
%{scopes: ["read:statuses"], fallback: :proceed_unauthenticated} when action == :show
@@ -22,8 +24,10 @@ defmodule Pleroma.Web.MastodonAPI.PollController do
plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action == :vote)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PollOperation
+
@doc "GET /api/v1/polls/:id"
- def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ def show(%{assigns: %{user: user}} = conn, %{id: id}) do
with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
true <- Visibility.visible_for_user?(activity, user) do
@@ -35,7 +39,7 @@ defmodule Pleroma.Web.MastodonAPI.PollController do
end
@doc "POST /api/v1/polls/:id/votes"
- def vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
+ def vote(%{assigns: %{user: user}, body_params: %{choices: choices}} = conn, %{id: id}) do
with %Object{data: %{"type" => "Question"}} = object <- Object.get_by_id(id),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
true <- Visibility.visible_for_user?(activity, user),
diff --git a/lib/pleroma/web/mastodon_api/controllers/report_controller.ex b/lib/pleroma/web/mastodon_api/controllers/report_controller.ex
index f65c5c62b..405167108 100644
--- a/lib/pleroma/web/mastodon_api/controllers/report_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/report_controller.ex
@@ -9,7 +9,7 @@ defmodule Pleroma.Web.MastodonAPI.ReportController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
- plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(OAuthScopesPlug, %{scopes: ["write:reports"]} when action == :create)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ReportOperation
diff --git a/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex b/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
index 899b78873..1719c67ea 100644
--- a/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
@@ -11,17 +11,21 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityController do
alias Pleroma.ScheduledActivity
alias Pleroma.Web.MastodonAPI.MastodonAPI
- plug(:assign_scheduled_activity when action != :index)
-
@oauth_read_actions [:show, :index]
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in @oauth_read_actions)
plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action not in @oauth_read_actions)
+ plug(:assign_scheduled_activity when action != :index)
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ScheduledActivityOperation
+
@doc "GET /api/v1/scheduled_statuses"
def index(%{assigns: %{user: user}} = conn, params) do
+ params = Map.new(params, fn {key, value} -> {to_string(key), value} end)
+
with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
conn
|> add_link_headers(scheduled_activities)
@@ -35,7 +39,7 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityController do
end
@doc "PUT /api/v1/scheduled_statuses/:id"
- def update(%{assigns: %{scheduled_activity: scheduled_activity}} = conn, params) do
+ def update(%{assigns: %{scheduled_activity: scheduled_activity}, body_params: params} = conn, _) do
with {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
render(conn, "show.json", scheduled_activity: scheduled_activity)
end
@@ -48,7 +52,7 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityController do
end
end
- defp assign_scheduled_activity(%{assigns: %{user: user}, params: %{"id" => id}} = conn, _) do
+ defp assign_scheduled_activity(%{assigns: %{user: user}, params: %{id: id}} = conn, _) do
case ScheduledActivity.get(user, id) do
%ScheduledActivity{} = activity -> assign(conn, :scheduled_activity, activity)
nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
index cd49da6ad..77e2224e4 100644
--- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
@@ -5,19 +5,20 @@
defmodule Pleroma.Web.MastodonAPI.SearchController do
use Pleroma.Web, :controller
- import Pleroma.Web.ControllerHelper, only: [fetch_integer_param: 2, skip_relationships?: 1]
-
alias Pleroma.Activity
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Plugs.RateLimiter
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web
+ alias Pleroma.Web.ControllerHelper
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.StatusView
require Logger
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
# Note: Mastodon doesn't allow unauthenticated access (requires read:accounts / read:search)
plug(OAuthScopesPlug, %{scopes: ["read:search"], fallback: :proceed_unauthenticated})
@@ -25,18 +26,24 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
plug(RateLimiter, [name: :search] when action in [:search, :search2, :account_search])
- def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.SearchOperation
+
+ def account_search(%{assigns: %{user: user}} = conn, %{q: query} = params) do
accounts = User.search(query, search_options(params, user))
conn
|> put_view(AccountView)
- |> render("index.json", users: accounts, for: user, as: :user)
+ |> render("index.json",
+ users: accounts,
+ for: user,
+ as: :user
+ )
end
def search2(conn, params), do: do_search(:v2, conn, params)
def search(conn, params), do: do_search(:v1, conn, params)
- defp do_search(version, %{assigns: %{user: user}} = conn, %{"q" => query} = params) do
+ defp do_search(version, %{assigns: %{user: user}} = conn, %{q: query} = params) do
options = search_options(params, user)
timeout = Keyword.get(Repo.config(), :timeout, 15_000)
default_values = %{"statuses" => [], "accounts" => [], "hashtags" => []}
@@ -44,7 +51,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
result =
default_values
|> Enum.map(fn {resource, default_value} ->
- if params["type"] in [nil, resource] do
+ if params[:type] in [nil, resource] do
{resource, fn -> resource_search(version, resource, query, options) end}
else
{resource, fn -> default_value end}
@@ -67,13 +74,13 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
defp search_options(params, user) do
[
- skip_relationships: skip_relationships?(params),
- resolve: params["resolve"] == "true",
- following: params["following"] == "true",
- limit: fetch_integer_param(params, "limit"),
- offset: fetch_integer_param(params, "offset"),
- type: params["type"],
+ resolve: params[:resolve],
+ following: params[:following],
+ limit: params[:limit],
+ offset: params[:offset],
+ type: params[:type],
author: get_author(params),
+ embed_relationships: ControllerHelper.embed_relationships?(params),
for_user: user
]
|> Enum.filter(&elem(&1, 1))
@@ -86,7 +93,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
users: accounts,
for: options[:for_user],
as: :user,
- skip_relationships: false
+ embed_relationships: options[:embed_relationships]
)
end
@@ -96,8 +103,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
StatusView.render("index.json",
activities: statuses,
for: options[:for_user],
- as: :activity,
- skip_relationships: options[:skip_relationships]
+ as: :activity
)
end
@@ -135,7 +141,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
end
end
- defp get_author(%{"account_id" => account_id}) when is_binary(account_id),
+ defp get_author(%{account_id: account_id}) when is_binary(account_id),
do: User.get_cached_by_id(account_id)
defp get_author(_params), do: nil
diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
index 9eea2e9eb..f20157a5f 100644
--- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
@@ -6,7 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper,
- only: [try_render: 3, add_link_headers: 2, skip_relationships?: 1]
+ only: [try_render: 3, add_link_headers: 2]
require Ecto.Query
@@ -24,6 +24,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.ScheduledActivityView
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(:skip_plug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action in [:index, :show])
@unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
@@ -83,13 +84,13 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
plug(
RateLimiter,
- [name: :status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]]
+ [name: :status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: [:id]]
when action in ~w(reblog unreblog)a
)
plug(
RateLimiter,
- [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]]
+ [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: [:id]]
when action in ~w(favourite unfavourite)a
)
@@ -97,12 +98,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.StatusOperation
+
@doc """
GET `/api/v1/statuses?ids[]=1&ids[]=2`
`ids` query param is required
"""
- def index(%{assigns: %{user: user}} = conn, %{"ids" => ids} = params) do
+ def index(%{assigns: %{user: user}} = conn, %{ids: ids} = _params) do
limit = 100
activities =
@@ -114,8 +117,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
render(conn, "index.json",
activities: activities,
for: user,
- as: :activity,
- skip_relationships: skip_relationships?(params)
+ as: :activity
)
end
@@ -125,21 +127,29 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
Creates a scheduled status when `scheduled_at` param is present and it's far enough
"""
def create(
- %{assigns: %{user: user}} = conn,
- %{"status" => _, "scheduled_at" => scheduled_at} = params
+ %{
+ assigns: %{user: user},
+ body_params: %{status: _, scheduled_at: scheduled_at} = params
+ } = conn,
+ _
)
when not is_nil(scheduled_at) do
- params = Map.put(params, "in_reply_to_status_id", params["in_reply_to_id"])
+ params = Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
+
+ attrs = %{
+ params: Map.new(params, fn {key, value} -> {to_string(key), value} end),
+ scheduled_at: scheduled_at
+ }
with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)},
- attrs <- %{"params" => params, "scheduled_at" => scheduled_at},
{:ok, scheduled_activity} <- ScheduledActivity.create(user, attrs) do
conn
|> put_view(ScheduledActivityView)
|> render("show.json", scheduled_activity: scheduled_activity)
else
{:far_enough, _} ->
- create(conn, Map.drop(params, ["scheduled_at"]))
+ params = Map.drop(params, [:scheduled_at])
+ create(%Plug.Conn{conn | body_params: params}, %{})
error ->
error
@@ -151,8 +161,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
Creates a regular status
"""
- def create(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
- params = Map.put(params, "in_reply_to_status_id", params["in_reply_to_id"])
+ def create(%{assigns: %{user: user}, body_params: %{status: _} = params} = conn, _) do
+ params = Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
with {:ok, activity} <- CommonAPI.post(user, params) do
try_render(conn, "show.json",
@@ -169,12 +179,13 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
end
- def create(%{assigns: %{user: _user}} = conn, %{"media_ids" => _} = params) do
- create(conn, Map.put(params, "status", ""))
+ def create(%{assigns: %{user: _user}, body_params: %{media_ids: _} = params} = conn, _) do
+ params = Map.put(params, :status, "")
+ create(%Plug.Conn{conn | body_params: params}, %{})
end
@doc "GET /api/v1/statuses/:id"
- def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ def show(%{assigns: %{user: user}} = conn, %{id: id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
true <- Visibility.visible_for_user?(activity, user) do
try_render(conn, "show.json",
@@ -188,7 +199,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "DELETE /api/v1/statuses/:id"
- def delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
json(conn, %{})
else
@@ -198,23 +209,23 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "POST /api/v1/statuses/:id/reblog"
- def reblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id} = params) do
- with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user, params),
+ def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do
+ with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
%Activity{} = announce <- Activity.normalize(announce.data) do
try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
end
end
@doc "POST /api/v1/statuses/:id/unreblog"
- def unreblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
- with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
- %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
+ def unreblog(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
+ with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
+ %Activity{} = activity <- Activity.get_by_id(activity_id) do
try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
end
end
@doc "POST /api/v1/statuses/:id/favourite"
- def favourite(%{assigns: %{user: user}} = conn, %{"id" => activity_id}) do
+ def favourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
%Activity{} = activity <- Activity.get_by_id(activity_id) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
@@ -222,29 +233,29 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "POST /api/v1/statuses/:id/unfavourite"
- def unfavourite(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
- with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
- %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
+ def unfavourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
+ with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
+ %Activity{} = activity <- Activity.get_by_id(activity_id) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end
@doc "POST /api/v1/statuses/:id/pin"
- def pin(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
+ def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end
@doc "POST /api/v1/statuses/:id/unpin"
- def unpin(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
+ def unpin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end
@doc "POST /api/v1/statuses/:id/bookmark"
- def bookmark(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ def bookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
%User{} = user <- User.get_cached_by_nickname(user.nickname),
true <- Visibility.visible_for_user?(activity, user),
@@ -254,7 +265,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "POST /api/v1/statuses/:id/unbookmark"
- def unbookmark(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ def unbookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
%User{} = user <- User.get_cached_by_nickname(user.nickname),
true <- Visibility.visible_for_user?(activity, user),
@@ -264,7 +275,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "POST /api/v1/statuses/:id/mute"
- def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ def mute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
with %Activity{} = activity <- Activity.get_by_id(id),
{:ok, activity} <- CommonAPI.add_mute(user, activity) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
@@ -272,7 +283,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "POST /api/v1/statuses/:id/unmute"
- def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
with %Activity{} = activity <- Activity.get_by_id(id),
{:ok, activity} <- CommonAPI.remove_mute(user, activity) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
@@ -281,7 +292,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
@doc "GET /api/v1/statuses/:id/card"
@deprecated "https://github.com/tootsuite/mastodon/pull/11213"
- def card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
+ def card(%{assigns: %{user: user}} = conn, %{id: status_id}) do
with %Activity{} = activity <- Activity.get_by_id(status_id),
true <- Visibility.visible_for_user?(activity, user) do
data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
@@ -292,7 +303,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "GET /api/v1/statuses/:id/favourited_by"
- def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
{:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
%Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
@@ -312,7 +323,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "GET /api/v1/statuses/:id/reblogged_by"
- def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
{:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
%Object{data: %{"announcements" => announces, "id" => ap_id}} <-
@@ -344,7 +355,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "GET /api/v1/statuses/:id/context"
- def context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ def context(%{assigns: %{user: user}} = conn, %{id: id}) do
with %Activity{} = activity <- Activity.get_by_id(id) do
activities =
ActivityPub.fetch_activities_for_context(activity.data["context"], %{
@@ -359,19 +370,19 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
@doc "GET /api/v1/favourites"
def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
- activities =
- ActivityPub.fetch_favourites(
- user,
- Map.take(params, Pleroma.Pagination.page_keys())
- )
+ params =
+ params
+ |> Map.new(fn {key, value} -> {to_string(key), value} end)
+ |> Map.take(Pleroma.Pagination.page_keys())
+
+ activities = ActivityPub.fetch_favourites(user, params)
conn
|> add_link_headers(activities)
|> render("index.json",
activities: activities,
for: user,
- as: :activity,
- skip_relationships: skip_relationships?(params)
+ as: :activity
)
end
@@ -393,8 +404,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|> render("index.json",
activities: activities,
for: user,
- as: :activity,
- skip_relationships: skip_relationships?(params)
+ as: :activity
)
end
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex b/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex
index d184ea1d0..34eac97c5 100644
--- a/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex
@@ -11,14 +11,16 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do
action_fallback(:errors)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(:restrict_push_enabled)
plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["push"]})
- plug(:restrict_push_enabled)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.SubscriptionOperation
# Creates PushSubscription
# POST /api/v1/push/subscription
#
- def create(%{assigns: %{user: user, token: token}} = conn, params) do
+ def create(%{assigns: %{user: user, token: token}, body_params: params} = conn, _) do
with {:ok, _} <- Subscription.delete_if_exists(user, token),
{:ok, subscription} <- Subscription.create(user, token, params) do
render(conn, "show.json", subscription: subscription)
@@ -28,7 +30,7 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do
# Gets PushSubscription
# GET /api/v1/push/subscription
#
- def get(%{assigns: %{user: user, token: token}} = conn, _params) do
+ def show(%{assigns: %{user: user, token: token}} = conn, _params) do
with {:ok, subscription} <- Subscription.get(user, token) do
render(conn, "show.json", subscription: subscription)
end
@@ -37,7 +39,7 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do
# Updates PushSubscription
# PUT /api/v1/push/subscription
#
- def update(%{assigns: %{user: user, token: token}} = conn, params) do
+ def update(%{assigns: %{user: user, token: token}, body_params: params} = conn, _) do
with {:ok, subscription} <- Subscription.update(user, token, params) do
render(conn, "show.json", subscription: subscription)
end
@@ -66,7 +68,7 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do
def errors(conn, {:error, :not_found}) do
conn
|> put_status(:not_found)
- |> json(dgettext("errors", "Not found"))
+ |> json(%{error: dgettext("errors", "Record not found")})
end
def errors(conn, _) do
diff --git a/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex b/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex
index c93a43969..f91df9ab7 100644
--- a/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex
@@ -5,11 +5,26 @@
defmodule Pleroma.Web.MastodonAPI.SuggestionController do
use Pleroma.Web, :controller
- alias Pleroma.Plugs.OAuthScopesPlug
-
require Logger
- plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :index)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["read"]} when action == :index)
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def index_operation do
+ %OpenApiSpex.Operation{
+ tags: ["Suggestions"],
+ summary: "Follow suggestions (Not implemented)",
+ operationId: "SuggestionController.index",
+ responses: %{
+ 200 => Pleroma.Web.ApiSpec.Helpers.empty_array_response()
+ }
+ }
+ end
@doc "GET /api/v1/suggestions"
def index(conn, params),
diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
index 2d67e19da..958567510 100644
--- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
@@ -6,7 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper,
- only: [add_link_headers: 2, add_link_headers: 3, truthy_param?: 1, skip_relationships?: 1]
+ only: [add_link_headers: 2, add_link_headers: 3]
alias Pleroma.Pagination
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
@@ -15,6 +15,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:public, :hashtag])
# TODO: Replace with a macro when there is a Phoenix release with the following commit in it:
@@ -37,10 +38,13 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
plug(:put_view, Pleroma.Web.MastodonAPI.StatusView)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.TimelineOperation
+
# GET /api/v1/timelines/home
def home(%{assigns: %{user: user}} = conn, params) do
params =
params
+ |> Map.new(fn {key, value} -> {to_string(key), value} end)
|> Map.put("type", ["Create", "Announce"])
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
@@ -59,8 +63,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|> render("index.json",
activities: activities,
for: user,
- as: :activity,
- skip_relationships: skip_relationships?(params)
+ as: :activity
)
end
@@ -68,6 +71,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
def direct(%{assigns: %{user: user}} = conn, params) do
params =
params
+ |> Map.new(fn {key, value} -> {to_string(key), value} end)
|> Map.put("type", "Create")
|> Map.put("blocking_user", user)
|> Map.put("user", user)
@@ -83,14 +87,15 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|> render("index.json",
activities: activities,
for: user,
- as: :activity,
- skip_relationships: skip_relationships?(params)
+ as: :activity
)
end
# GET /api/v1/timelines/public
def public(%{assigns: %{user: user}} = conn, params) do
- local_only = truthy_param?(params["local"])
+ params = Map.new(params, fn {key, value} -> {to_string(key), value} end)
+
+ local_only = params["local"]
cfg_key =
if local_only do
@@ -118,8 +123,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|> render("index.json",
activities: activities,
for: user,
- as: :activity,
- skip_relationships: skip_relationships?(params)
+ as: :activity
)
end
end
@@ -157,8 +161,8 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
# GET /api/v1/timelines/tag/:tag
def hashtag(%{assigns: %{user: user}} = conn, params) do
- local_only = truthy_param?(params["local"])
-
+ params = Map.new(params, fn {key, value} -> {to_string(key), value} end)
+ local_only = params["local"]
activities = hashtag_fetching(params, user, local_only)
conn
@@ -166,16 +170,16 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|> render("index.json",
activities: activities,
for: user,
- as: :activity,
- skip_relationships: skip_relationships?(params)
+ as: :activity
)
end
# GET /api/v1/timelines/list/:list_id
- def list(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
+ def list(%{assigns: %{user: user}} = conn, %{list_id: id} = params) do
with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
params =
params
+ |> Map.new(fn {key, value} -> {to_string(key), value} end)
|> Map.put("type", "Create")
|> Map.put("blocking_user", user)
|> Map.put("user", user)
@@ -195,8 +199,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
render(conn, "index.json",
activities: activities,
for: user,
- as: :activity,
- skip_relationships: skip_relationships?(params)
+ as: :activity
)
else
_e -> render_error(conn, :forbidden, "Error.")
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index b4b61e74c..45fffaad2 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -15,13 +15,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
def render("index.json", %{users: users} = opts) do
reading_user = opts[:for]
- # Note: :skip_relationships option is currently intentionally not supported for accounts
relationships_opt =
cond do
Map.has_key?(opts, :relationships) ->
opts[:relationships]
- is_nil(reading_user) ->
+ is_nil(reading_user) || !opts[:embed_relationships] ->
UserRelationship.view_relationships_option(nil, [])
true ->
@@ -36,9 +35,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
end
def render("show.json", %{user: user} = opts) do
- if User.visible_for?(user, opts[:for]),
- do: do_render("show.json", opts),
- else: %{}
+ if User.visible_for?(user, opts[:for]) do
+ do_render("show.json", opts)
+ else
+ %{}
+ end
end
def render("mention.json", %{user: user}) do
@@ -191,14 +192,14 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
end)
relationship =
- if opts[:skip_relationships] do
- %{}
- else
+ if opts[:embed_relationships] do
render("relationship.json", %{
user: opts[:for],
target: user,
relationships: opts[:relationships]
})
+ else
+ %{}
end
%{
@@ -221,7 +222,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
fields: user.fields,
bot: bot,
source: %{
- note: (user.bio || "") |> String.replace(~r(<br */?>), "\n") |> Pleroma.HTML.strip_tags(),
+ note: prepare_user_bio(user),
sensitive: false,
fields: user.raw_fields,
pleroma: %{
@@ -253,8 +254,20 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|> maybe_put_follow_requests_count(user, opts[:for])
|> maybe_put_allow_following_move(user, opts[:for])
|> maybe_put_unread_conversation_count(user, opts[:for])
+ |> maybe_put_unread_notification_count(user, opts[:for])
+ end
+
+ defp prepare_user_bio(%User{bio: ""}), do: ""
+
+ defp prepare_user_bio(%User{bio: bio}) when is_binary(bio) do
+ bio
+ |> String.replace(~r(<br */?>), "\n")
+ |> Pleroma.HTML.strip_tags()
+ |> HtmlEntities.decode()
end
+ defp prepare_user_bio(_), do: ""
+
defp username_from_nickname(string) when is_binary(string) do
hd(String.split(string, "@"))
end
@@ -323,7 +336,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
defp maybe_put_role(data, _, _), do: data
defp maybe_put_notification_settings(data, %User{id: user_id} = user, %User{id: user_id}) do
- Kernel.put_in(data, [:pleroma, :notification_settings], user.notification_settings)
+ Kernel.put_in(
+ data,
+ [:pleroma, :notification_settings],
+ Map.from_struct(user.notification_settings)
+ )
end
defp maybe_put_notification_settings(data, _, _), do: data
@@ -350,6 +367,16 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
defp maybe_put_unread_conversation_count(data, _, _), do: data
+ defp maybe_put_unread_notification_count(data, %User{id: user_id}, %User{id: user_id} = user) do
+ Kernel.put_in(
+ data,
+ [:pleroma, :unread_notifications_count],
+ Pleroma.Notification.unread_notifications_count(user)
+ )
+ end
+
+ defp maybe_put_unread_notification_count(data, _, _), do: data
+
defp image_url(%{"url" => [%{"href" => href} | _]}), do: href
defp image_url(_), do: nil
end
diff --git a/lib/pleroma/web/mastodon_api/views/filter_view.ex b/lib/pleroma/web/mastodon_api/views/filter_view.ex
index 97fd1e83f..aeff646f5 100644
--- a/lib/pleroma/web/mastodon_api/views/filter_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/filter_view.ex
@@ -7,11 +7,11 @@ defmodule Pleroma.Web.MastodonAPI.FilterView do
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.FilterView
- def render("filters.json", %{filters: filters} = opts) do
- render_many(filters, FilterView, "filter.json", opts)
+ def render("index.json", %{filters: filters}) do
+ render_many(filters, FilterView, "show.json")
end
- def render("filter.json", %{filter: filter}) do
+ def render("show.json", %{filter: filter}) do
expires_at =
if filter.expires_at do
Utils.to_masto_date(filter.expires_at)
diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex
index a329ffc28..6a630eafa 100644
--- a/lib/pleroma/web/mastodon_api/views/instance_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex
@@ -23,7 +23,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
streaming_api: Pleroma.Web.Endpoint.websocket_url()
},
stats: Pleroma.Stats.get_stats(),
- thumbnail: Pleroma.Web.base_url() <> "/instance/thumbnail.jpeg",
+ thumbnail: instance_thumbnail(),
languages: ["en"],
registrations: Keyword.get(instance, :registrations_open),
# Extra (not present in Mastodon):
@@ -33,6 +33,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
avatar_upload_limit: Keyword.get(instance, :avatar_upload_limit),
background_upload_limit: Keyword.get(instance, :background_upload_limit),
banner_upload_limit: Keyword.get(instance, :banner_upload_limit),
+ background_image: Keyword.get(instance, :background_image),
pleroma: %{
metadata: %{
features: features(),
@@ -86,4 +87,9 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
end
|> Map.put(:enabled, Config.get([:instance, :federating]))
end
+
+ defp instance_thumbnail do
+ Pleroma.Config.get([:instance, :instance_thumbnail]) ||
+ "#{Pleroma.Web.base_url()}/instance/thumbnail.jpeg"
+ end
end
diff --git a/lib/pleroma/web/mastodon_api/views/marker_view.ex b/lib/pleroma/web/mastodon_api/views/marker_view.ex
index 985368fe5..21d535d54 100644
--- a/lib/pleroma/web/mastodon_api/views/marker_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/marker_view.ex
@@ -6,12 +6,16 @@ defmodule Pleroma.Web.MastodonAPI.MarkerView do
use Pleroma.Web, :view
def render("markers.json", %{markers: markers}) do
- Enum.reduce(markers, %{}, fn m, acc ->
- Map.put_new(acc, m.timeline, %{
- last_read_id: m.last_read_id,
- version: m.lock_version,
- updated_at: NaiveDateTime.to_iso8601(m.updated_at)
- })
+ Map.new(markers, fn m ->
+ {m.timeline,
+ %{
+ last_read_id: m.last_read_id,
+ version: m.lock_version,
+ updated_at: NaiveDateTime.to_iso8601(m.updated_at),
+ pleroma: %{
+ unread_count: m.unread_count
+ }
+ }}
end)
end
end
diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex
index 4da1ab67f..c46ddcf55 100644
--- a/lib/pleroma/web/mastodon_api/views/notification_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex
@@ -51,9 +51,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
|> Enum.filter(& &1)
|> Kernel.++(move_activities_targets)
- UserRelationship.view_relationships_option(reading_user, actors,
- source_mutes_only: opts[:skip_relationships]
- )
+ UserRelationship.view_relationships_option(reading_user, actors, subset: :source_mutes)
end
opts =
@@ -83,15 +81,13 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
mastodon_type = Activity.mastodon_notification_type(activity)
- render_opts = %{
- relationships: opts[:relationships],
- skip_relationships: opts[:skip_relationships]
- }
+ # Note: :relationships contain user mutes (needed for :muted flag in :status)
+ status_render_opts = %{relationships: opts[:relationships]}
with %{id: _} = account <-
AccountView.render(
"show.json",
- Map.merge(render_opts, %{user: actor, for: reading_user})
+ %{user: actor, for: reading_user}
) do
response = %{
id: to_string(notification.id),
@@ -105,21 +101,20 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
case mastodon_type do
"mention" ->
- put_status(response, activity, reading_user, render_opts)
+ put_status(response, activity, reading_user, status_render_opts)
"favourite" ->
- put_status(response, parent_activity_fn.(), reading_user, render_opts)
+ put_status(response, parent_activity_fn.(), reading_user, status_render_opts)
"reblog" ->
- put_status(response, parent_activity_fn.(), reading_user, render_opts)
+ put_status(response, parent_activity_fn.(), reading_user, status_render_opts)
"move" ->
- # Note: :skip_relationships option being applied to _account_ rendering (here)
- put_target(response, activity, reading_user, render_opts)
+ put_target(response, activity, reading_user, %{})
"pleroma:emoji_reaction" ->
response
- |> put_status(parent_activity_fn.(), reading_user, render_opts)
+ |> put_status(parent_activity_fn.(), reading_user, status_render_opts)
|> put_emoji(activity)
type when type in ["follow", "follow_request"] ->
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index 24167f66f..8e3715093 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -107,9 +107,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|> Enum.map(&get_user(&1.data["actor"], false))
|> Enum.filter(& &1)
- UserRelationship.view_relationships_option(reading_user, actors,
- source_mutes_only: opts[:skip_relationships]
- )
+ UserRelationship.view_relationships_option(reading_user, actors, subset: :source_mutes)
end
opts =
@@ -162,9 +160,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
account:
AccountView.render("show.json", %{
user: user,
- for: opts[:for],
- relationships: opts[:relationships],
- skip_relationships: opts[:skip_relationships]
+ for: opts[:for]
}),
in_reply_to_id: nil,
in_reply_to_account_id: nil,
@@ -330,9 +326,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
account:
AccountView.render("show.json", %{
user: user,
- for: opts[:for],
- relationships: opts[:relationships],
- skip_relationships: opts[:skip_relationships]
+ for: opts[:for]
}),
in_reply_to_id: reply_to && to_string(reply_to.id),
in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
@@ -442,27 +436,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
}
end
- def render("listen.json", %{activity: %Activity{data: %{"type" => "Listen"}} = activity} = opts) do
- object = Object.normalize(activity)
-
- user = get_user(activity.data["actor"])
- created_at = Utils.to_masto_date(activity.data["published"])
-
- %{
- id: activity.id,
- account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
- created_at: created_at,
- title: object.data["title"] |> HTML.strip_tags(),
- artist: object.data["artist"] |> HTML.strip_tags(),
- album: object.data["album"] |> HTML.strip_tags(),
- length: object.data["length"]
- }
- end
-
- def render("listens.json", opts) do
- safe_render_many(opts.activities, StatusView, "listen.json", opts)
- end
-
def render("context.json", %{activity: activity, activities: activities, user: user}) do
%{ancestors: ancestors, descendants: descendants} =
activities
diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex
index 5652a37c1..94e4595d8 100644
--- a/lib/pleroma/web/mastodon_api/websocket_handler.ex
+++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex
@@ -12,29 +12,19 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
@behaviour :cowboy_websocket
- @streams [
- "public",
- "public:local",
- "public:media",
- "public:local:media",
- "user",
- "user:notification",
- "direct",
- "list",
- "hashtag"
- ]
- @anonymous_streams ["public", "public:local", "hashtag"]
-
- # Handled by periodic keepalive in Pleroma.Web.Streamer.Ping.
- @timeout :infinity
+ # Client ping period.
+ @tick :timer.seconds(30)
+ # Cowboy timeout period.
+ @timeout :timer.seconds(60)
+ # Hibernate every X messages
+ @hibernate_every 100
def init(%{qs: qs} = req, state) do
- with params <- :cow_qs.parse_qs(qs),
+ with params <- Enum.into(:cow_qs.parse_qs(qs), %{}),
sec_websocket <- :cowboy_req.header("sec-websocket-protocol", req, nil),
- access_token <- List.keyfind(params, "access_token", 0),
- {_, stream} <- List.keyfind(params, "stream", 0),
- {:ok, user} <- allow_request(stream, [access_token, sec_websocket]),
- topic when is_binary(topic) <- expand_topic(stream, params) do
+ access_token <- Map.get(params, "access_token"),
+ {:ok, user} <- authenticate_request(access_token, sec_websocket),
+ {:ok, topic} <- Streamer.get_topic(Map.get(params, "stream"), user, params) do
req =
if sec_websocket do
:cowboy_req.set_resp_header("sec-websocket-protocol", sec_websocket, req)
@@ -42,43 +32,70 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
req
end
- {:cowboy_websocket, req, %{user: user, topic: topic}, %{idle_timeout: @timeout}}
+ {:cowboy_websocket, req, %{user: user, topic: topic, count: 0, timer: nil},
+ %{idle_timeout: @timeout}}
else
- {:error, code} ->
- Logger.debug("#{__MODULE__} denied connection: #{inspect(code)} - #{inspect(req)}")
- {:ok, req} = :cowboy_req.reply(code, req)
+ {:error, :bad_topic} ->
+ Logger.debug("#{__MODULE__} bad topic #{inspect(req)}")
+ {:ok, req} = :cowboy_req.reply(404, req)
{:ok, req, state}
- error ->
- Logger.debug("#{__MODULE__} denied connection: #{inspect(error)} - #{inspect(req)}")
- {:ok, req} = :cowboy_req.reply(400, req)
+ {:error, :unauthorized} ->
+ Logger.debug("#{__MODULE__} authentication error: #{inspect(req)}")
+ {:ok, req} = :cowboy_req.reply(401, req)
{:ok, req, state}
end
end
def websocket_init(state) do
- send(self(), :subscribe)
- {:ok, state}
- end
-
- # We never receive messages.
- def websocket_handle(_frame, state) do
- {:ok, state}
- end
-
- def websocket_info(:subscribe, state) do
Logger.debug(
"#{__MODULE__} accepted websocket connection for user #{
(state.user || %{id: "anonymous"}).id
}, topic #{state.topic}"
)
- Streamer.add_socket(state.topic, streamer_socket(state))
+ Streamer.add_socket(state.topic, state.user)
+ {:ok, %{state | timer: timer()}}
+ end
+
+ # Client's Pong frame.
+ def websocket_handle(:pong, state) do
+ if state.timer, do: Process.cancel_timer(state.timer)
+ {:ok, %{state | timer: timer()}}
+ end
+
+ # We never receive messages.
+ def websocket_handle(frame, state) do
+ Logger.error("#{__MODULE__} received frame: #{inspect(frame)}")
{:ok, state}
end
+ def websocket_info({:render_with_user, view, template, item}, state) do
+ user = %User{} = User.get_cached_by_ap_id(state.user.ap_id)
+
+ unless Streamer.filtered_by_user?(user, item) do
+ websocket_info({:text, view.render(template, item, user)}, %{state | user: user})
+ else
+ {:ok, state}
+ end
+ end
+
def websocket_info({:text, message}, state) do
- {:reply, {:text, message}, state}
+ # If the websocket processed X messages, force an hibernate/GC.
+ # We don't hibernate at every message to balance CPU usage/latency with RAM usage.
+ if state.count > @hibernate_every do
+ {:reply, {:text, message}, %{state | count: 0}, :hibernate}
+ else
+ {:reply, {:text, message}, %{state | count: state.count + 1}}
+ end
+ end
+
+ # Ping tick. We don't re-queue a timer there, it is instead queued when :pong is received.
+ # As we hibernate there, reset the count to 0.
+ # If the client misses :pong, Cowboy will automatically timeout the connection after
+ # `@idle_timeout`.
+ def websocket_info(:tick, state) do
+ {:reply, :ping, %{state | timer: nil, count: 0}, :hibernate}
end
def terminate(reason, _req, state) do
@@ -88,56 +105,29 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
}, topic #{state.topic || "?"}: #{inspect(reason)}"
)
- Streamer.remove_socket(state.topic, streamer_socket(state))
+ Streamer.remove_socket(state.topic)
:ok
end
# Public streams without authentication.
- defp allow_request(stream, [nil, nil]) when stream in @anonymous_streams do
+ defp authenticate_request(nil, nil) do
{:ok, nil}
end
# Authenticated streams.
- defp allow_request(stream, [access_token, sec_websocket]) when stream in @streams do
- token =
- with {"access_token", token} <- access_token do
- token
- else
- _ -> sec_websocket
- end
+ defp authenticate_request(access_token, sec_websocket) do
+ token = access_token || sec_websocket
with true <- is_bitstring(token),
%Token{user_id: user_id} <- Repo.get_by(Token, token: token),
user = %User{} <- User.get_cached_by_id(user_id) do
{:ok, user}
else
- _ -> {:error, 403}
+ _ -> {:error, :unauthorized}
end
end
- # Not authenticated.
- defp allow_request(stream, _) when stream in @streams, do: {:error, 403}
-
- # No matching stream.
- defp allow_request(_, _), do: {:error, 404}
-
- defp expand_topic("hashtag", params) do
- case List.keyfind(params, "tag", 0) do
- {_, tag} -> "hashtag:#{tag}"
- _ -> nil
- end
- end
-
- defp expand_topic("list", params) do
- case List.keyfind(params, "list", 0) do
- {_, list} -> "list:#{list}"
- _ -> nil
- end
- end
-
- defp expand_topic(topic, _), do: topic
-
- defp streamer_socket(state) do
- %{transport_pid: self(), assigns: state}
+ defp timer do
+ Process.send_after(self(), :tick, @tick)
end
end
diff --git a/lib/pleroma/web/media_proxy/invalidation.ex b/lib/pleroma/web/media_proxy/invalidation.ex
new file mode 100644
index 000000000..c037ff13e
--- /dev/null
+++ b/lib/pleroma/web/media_proxy/invalidation.ex
@@ -0,0 +1,26 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MediaProxy.Invalidation do
+ @moduledoc false
+
+ @callback purge(list(String.t()), map()) :: {:ok, String.t()} | {:error, String.t()}
+
+ alias Pleroma.Config
+
+ @spec purge(list(String.t())) :: {:ok, String.t()} | {:error, String.t()}
+ def purge(urls) do
+ [:media_proxy, :invalidation, :enabled]
+ |> Config.get()
+ |> do_purge(urls)
+ end
+
+ defp do_purge(true, urls) do
+ provider = Config.get([:media_proxy, :invalidation, :provider])
+ options = Config.get(provider)
+ provider.purge(urls, options)
+ end
+
+ defp do_purge(_, _), do: :ok
+end
diff --git a/lib/pleroma/web/media_proxy/invalidations/http.ex b/lib/pleroma/web/media_proxy/invalidations/http.ex
new file mode 100644
index 000000000..07248df6e
--- /dev/null
+++ b/lib/pleroma/web/media_proxy/invalidations/http.ex
@@ -0,0 +1,40 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MediaProxy.Invalidation.Http do
+ @moduledoc false
+ @behaviour Pleroma.Web.MediaProxy.Invalidation
+
+ require Logger
+
+ @impl Pleroma.Web.MediaProxy.Invalidation
+ def purge(urls, opts) do
+ method = Map.get(opts, :method, :purge)
+ headers = Map.get(opts, :headers, [])
+ options = Map.get(opts, :options, [])
+
+ Logger.debug("Running cache purge: #{inspect(urls)}")
+
+ Enum.each(urls, fn url ->
+ with {:error, error} <- do_purge(method, url, headers, options) do
+ Logger.error("Error while cache purge: url - #{url}, error: #{inspect(error)}")
+ end
+ end)
+
+ {:ok, "success"}
+ end
+
+ defp do_purge(method, url, headers, options) do
+ case Pleroma.HTTP.request(method, url, "", headers, options) do
+ {:ok, %{status: status} = env} when 400 <= status and status < 500 ->
+ {:error, env}
+
+ {:error, error} = error ->
+ error
+
+ _ ->
+ {:ok, "success"}
+ end
+ end
+end
diff --git a/lib/pleroma/web/media_proxy/invalidations/script.ex b/lib/pleroma/web/media_proxy/invalidations/script.ex
new file mode 100644
index 000000000..6be782132
--- /dev/null
+++ b/lib/pleroma/web/media_proxy/invalidations/script.ex
@@ -0,0 +1,41 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MediaProxy.Invalidation.Script do
+ @moduledoc false
+
+ @behaviour Pleroma.Web.MediaProxy.Invalidation
+
+ require Logger
+
+ @impl Pleroma.Web.MediaProxy.Invalidation
+ def purge(urls, %{script_path: script_path} = _options) do
+ args =
+ urls
+ |> List.wrap()
+ |> Enum.uniq()
+ |> Enum.join(" ")
+
+ path = Path.expand(script_path)
+
+ Logger.debug("Running cache purge: #{inspect(urls)}, #{path}")
+
+ case do_purge(path, [args]) do
+ {result, exit_status} when exit_status > 0 ->
+ Logger.error("Error while cache purge: #{inspect(result)}")
+ {:error, inspect(result)}
+
+ _ ->
+ {:ok, "success"}
+ end
+ end
+
+ def purge(_, _), do: {:error, "not found script path"}
+
+ defp do_purge(path, args) do
+ System.cmd(path, args)
+ rescue
+ error -> {inspect(error), 1}
+ end
+end
diff --git a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex b/lib/pleroma/web/mongooseim/mongoose_im_controller.ex
index 1ed6ee521..6cbbe8fd8 100644
--- a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex
+++ b/lib/pleroma/web/mongooseim/mongoose_im_controller.ex
@@ -5,7 +5,7 @@
defmodule Pleroma.Web.MongooseIM.MongooseIMController do
use Pleroma.Web, :controller
- alias Comeonin.Pbkdf2
+ alias Pleroma.Plugs.AuthenticationPlug
alias Pleroma.Plugs.RateLimiter
alias Pleroma.Repo
alias Pleroma.User
@@ -28,7 +28,7 @@ defmodule Pleroma.Web.MongooseIM.MongooseIMController do
def check_password(conn, %{"user" => username, "pass" => password}) do
with %User{password_hash: password_hash, deactivated: false} <-
Repo.get_by(User, nickname: username, local: true),
- true <- Pbkdf2.checkpw(password, password_hash) do
+ true <- AuthenticationPlug.checkpw(password, password_hash) do
conn
|> json(true)
else
diff --git a/lib/pleroma/web/oauth/mfa_controller.ex b/lib/pleroma/web/oauth/mfa_controller.ex
new file mode 100644
index 000000000..53e19f82e
--- /dev/null
+++ b/lib/pleroma/web/oauth/mfa_controller.ex
@@ -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.OAuth.MFAController do
+ @moduledoc """
+ The model represents api to use Multi Factor authentications.
+ """
+
+ use Pleroma.Web, :controller
+
+ alias Pleroma.MFA
+ alias Pleroma.Web.Auth.TOTPAuthenticator
+ alias Pleroma.Web.OAuth.MFAView, as: View
+ alias Pleroma.Web.OAuth.OAuthController
+ alias Pleroma.Web.OAuth.Token
+
+ plug(:fetch_session when action in [:show, :verify])
+ plug(:fetch_flash when action in [:show, :verify])
+
+ @doc """
+ Display form to input mfa code or recovery code.
+ """
+ def show(conn, %{"mfa_token" => mfa_token} = params) do
+ template = Map.get(params, "challenge_type", "totp")
+
+ conn
+ |> put_view(View)
+ |> render("#{template}.html", %{
+ mfa_token: mfa_token,
+ redirect_uri: params["redirect_uri"],
+ state: params["state"]
+ })
+ end
+
+ @doc """
+ Verification code and continue authorization.
+ """
+ def verify(conn, %{"mfa" => %{"mfa_token" => mfa_token} = mfa_params} = _) do
+ with {:ok, %{user: user, authorization: auth}} <- MFA.Token.validate(mfa_token),
+ {:ok, _} <- validates_challenge(user, mfa_params) do
+ conn
+ |> OAuthController.after_create_authorization(auth, %{
+ "authorization" => %{
+ "redirect_uri" => mfa_params["redirect_uri"],
+ "state" => mfa_params["state"]
+ }
+ })
+ else
+ _ ->
+ conn
+ |> put_flash(:error, "Two-factor authentication failed.")
+ |> put_status(:unauthorized)
+ |> show(mfa_params)
+ end
+ end
+
+ @doc """
+ Verification second step of MFA (or recovery) and returns access token.
+
+ ## Endpoint
+ POST /oauth/mfa/challenge
+
+ params:
+ `client_id`
+ `client_secret`
+ `mfa_token` - access token to check second step of mfa
+ `challenge_type` - 'totp' or 'recovery'
+ `code`
+
+ """
+ def challenge(conn, %{"mfa_token" => mfa_token} = params) do
+ with {:ok, app} <- Token.Utils.fetch_app(conn),
+ {:ok, %{user: user, authorization: auth}} <- MFA.Token.validate(mfa_token),
+ {:ok, _} <- validates_challenge(user, params),
+ {:ok, token} <- Token.exchange_token(app, auth) do
+ json(conn, Token.Response.build(user, token))
+ else
+ _error ->
+ conn
+ |> put_status(400)
+ |> json(%{error: "Invalid code"})
+ end
+ end
+
+ # Verify TOTP Code
+ defp validates_challenge(user, %{"challenge_type" => "totp", "code" => code} = _) do
+ TOTPAuthenticator.verify(code, user)
+ end
+
+ # Verify Recovery Code
+ defp validates_challenge(user, %{"challenge_type" => "recovery", "code" => code} = _) do
+ TOTPAuthenticator.verify_recovery_code(user, code)
+ end
+
+ defp validates_challenge(_, _), do: {:error, :unsupported_challenge_type}
+end
diff --git a/lib/pleroma/web/oauth/mfa_view.ex b/lib/pleroma/web/oauth/mfa_view.ex
new file mode 100644
index 000000000..41d5578dc
--- /dev/null
+++ b/lib/pleroma/web/oauth/mfa_view.ex
@@ -0,0 +1,8 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.OAuth.MFAView do
+ use Pleroma.Web, :view
+ import Phoenix.HTML.Form
+end
diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex
index 685269877..7c804233c 100644
--- a/lib/pleroma/web/oauth/oauth_controller.ex
+++ b/lib/pleroma/web/oauth/oauth_controller.ex
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
use Pleroma.Web, :controller
alias Pleroma.Helpers.UriHelper
+ alias Pleroma.MFA
alias Pleroma.Plugs.RateLimiter
alias Pleroma.Registration
alias Pleroma.Repo
@@ -14,6 +15,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
alias Pleroma.Web.ControllerHelper
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Authorization
+ alias Pleroma.Web.OAuth.MFAController
alias Pleroma.Web.OAuth.Scopes
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken
@@ -121,7 +123,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do
%{"authorization" => _} = params,
opts \\ []
) do
- with {:ok, auth} <- do_create_authorization(conn, params, opts[:user]) do
+ with {:ok, auth, user} <- do_create_authorization(conn, params, opts[:user]),
+ {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)} do
after_create_authorization(conn, auth, params)
else
error ->
@@ -181,6 +184,22 @@ defmodule Pleroma.Web.OAuth.OAuthController do
defp handle_create_authorization_error(
%Plug.Conn{} = conn,
+ {:mfa_required, user, auth, _},
+ params
+ ) do
+ {:ok, token} = MFA.Token.create_token(user, auth)
+
+ data = %{
+ "mfa_token" => token.token,
+ "redirect_uri" => params["authorization"]["redirect_uri"],
+ "state" => params["authorization"]["state"]
+ }
+
+ MFAController.show(conn, data)
+ end
+
+ defp handle_create_authorization_error(
+ %Plug.Conn{} = conn,
{:account_status, :password_reset_pending},
%{"authorization" => _} = params
) do
@@ -231,7 +250,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do
json(conn, Token.Response.build(user, token, response_attrs))
else
- _error -> render_invalid_credentials_error(conn)
+ error ->
+ handle_token_exchange_error(conn, error)
end
end
@@ -244,6 +264,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
{:account_status, :active} <- {:account_status, User.account_status(user)},
{:ok, scopes} <- validate_scopes(app, params),
{:ok, auth} <- Authorization.create_authorization(app, user, scopes),
+ {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)},
{:ok, token} <- Token.exchange_token(app, auth) do
json(conn, Token.Response.build(user, token))
else
@@ -270,13 +291,20 @@ defmodule Pleroma.Web.OAuth.OAuthController do
{:ok, token} <- Token.exchange_token(app, auth) do
json(conn, Token.Response.build_for_client_credentials(token))
else
- _error -> render_invalid_credentials_error(conn)
+ _error ->
+ handle_token_exchange_error(conn, :invalid_credentails)
end
end
# Bad request
def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
+ defp handle_token_exchange_error(%Plug.Conn{} = conn, {:mfa_required, user, auth, _}) do
+ conn
+ |> put_status(:forbidden)
+ |> json(build_and_response_mfa_token(user, auth))
+ end
+
defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :deactivated}) do
render_error(
conn,
@@ -434,7 +462,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do
def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do
with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
%Registration{} = registration <- Repo.get(Registration, registration_id),
- {_, {:ok, auth}} <- {:create_authorization, do_create_authorization(conn, params)},
+ {_, {:ok, auth, _user}} <-
+ {:create_authorization, do_create_authorization(conn, params)},
%User{} = user <- Repo.preload(auth, :user).user,
{:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do
conn
@@ -500,8 +529,9 @@ defmodule Pleroma.Web.OAuth.OAuthController do
%App{} = app <- Repo.get_by(App, client_id: client_id),
true <- redirect_uri in String.split(app.redirect_uris),
{:ok, scopes} <- validate_scopes(app, auth_attrs),
- {:account_status, :active} <- {:account_status, User.account_status(user)} do
- Authorization.create_authorization(app, user, scopes)
+ {:account_status, :active} <- {:account_status, User.account_status(user)},
+ {:ok, auth} <- Authorization.create_authorization(app, user, scopes) do
+ {:ok, auth, user}
end
end
@@ -515,6 +545,12 @@ defmodule Pleroma.Web.OAuth.OAuthController do
defp put_session_registration_id(%Plug.Conn{} = conn, registration_id),
do: put_session(conn, :registration_id, registration_id)
+ defp build_and_response_mfa_token(user, auth) do
+ with {:ok, token} <- MFA.Token.create_token(user, auth) do
+ Token.Response.build_for_mfa_token(user, token)
+ end
+ end
+
@spec validate_scopes(App.t(), map()) ::
{:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
defp validate_scopes(%App{} = app, params) do
diff --git a/lib/pleroma/web/oauth/token/clean_worker.ex b/lib/pleroma/web/oauth/token/clean_worker.ex
new file mode 100644
index 000000000..e3aa4eb7e
--- /dev/null
+++ b/lib/pleroma/web/oauth/token/clean_worker.ex
@@ -0,0 +1,38 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.OAuth.Token.CleanWorker do
+ @moduledoc """
+ The module represents functions to clean an expired OAuth and MFA tokens.
+ """
+ use GenServer
+
+ @ten_seconds 10_000
+ @one_day 86_400_000
+
+ alias Pleroma.MFA
+ alias Pleroma.Web.OAuth
+ alias Pleroma.Workers.BackgroundWorker
+
+ def start_link(_), do: GenServer.start_link(__MODULE__, %{})
+
+ def init(_) do
+ Process.send_after(self(), :perform, @ten_seconds)
+ {:ok, nil}
+ end
+
+ @doc false
+ def handle_info(:perform, state) do
+ BackgroundWorker.enqueue("clean_expired_tokens", %{})
+ interval = Pleroma.Config.get([:oauth2, :clean_expired_tokens_interval], @one_day)
+
+ Process.send_after(self(), :perform, interval)
+ {:noreply, state}
+ end
+
+ def perform(:clean) do
+ OAuth.Token.delete_expired_tokens()
+ MFA.Token.delete_expired_tokens()
+ end
+end
diff --git a/lib/pleroma/web/oauth/token/response.ex b/lib/pleroma/web/oauth/token/response.ex
index 6f4713dee..0e72c31e9 100644
--- a/lib/pleroma/web/oauth/token/response.ex
+++ b/lib/pleroma/web/oauth/token/response.ex
@@ -5,6 +5,7 @@
defmodule Pleroma.Web.OAuth.Token.Response do
@moduledoc false
+ alias Pleroma.MFA
alias Pleroma.User
alias Pleroma.Web.OAuth.Token.Utils
@@ -32,5 +33,13 @@ defmodule Pleroma.Web.OAuth.Token.Response do
}
end
+ def build_for_mfa_token(user, mfa_token) do
+ %{
+ error: "mfa_required",
+ mfa_token: mfa_token.token,
+ supported_challenge_types: MFA.supported_methods(user)
+ }
+ end
+
defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600)
end
diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex
index 57682c1ea..08bb47aa8 100644
--- a/lib/pleroma/web/ostatus/ostatus_controller.ex
+++ b/lib/pleroma/web/ostatus/ostatus_controller.ex
@@ -16,7 +16,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do
alias Pleroma.Web.Router
plug(Pleroma.Plugs.EnsureAuthenticatedPlug,
- unless_func: &Pleroma.Web.FederatingPlug.federating?/0
+ unless_func: &Pleroma.Web.FederatingPlug.federating?/1
)
plug(
@@ -33,13 +33,13 @@ defmodule Pleroma.Web.OStatus.OStatusController do
action_fallback(:errors)
- def object(%{assigns: %{format: format}} = conn, %{"uuid" => _uuid})
+ def object(%{assigns: %{format: format}} = conn, _params)
when format in ["json", "activity+json"] do
ActivityPubController.call(conn, :object)
end
- def object(conn, %{"uuid" => uuid}) do
- with id <- o_status_url(conn, :object, uuid),
+ def object(%{assigns: %{format: _format}} = conn, _params) do
+ with id <- Endpoint.url() <> conn.request_path,
{_, %Activity{} = activity} <-
{:activity, Activity.get_create_by_object_ap_id_with_object(id)},
{_, true} <- {:public?, Visibility.is_public?(activity)} do
@@ -53,13 +53,13 @@ defmodule Pleroma.Web.OStatus.OStatusController do
end
end
- def activity(%{assigns: %{format: format}} = conn, %{"uuid" => _uuid})
+ def activity(%{assigns: %{format: format}} = conn, _params)
when format in ["json", "activity+json"] do
ActivityPubController.call(conn, :activity)
end
- def activity(conn, %{"uuid" => uuid}) do
- with id <- o_status_url(conn, :activity, uuid),
+ def activity(%{assigns: %{format: _format}} = conn, _params) do
+ with id <- Endpoint.url() <> conn.request_path,
{_, %Activity{} = activity} <- {:activity, Activity.normalize(id)},
{_, true} <- {:public?, Visibility.is_public?(activity)} do
redirect(conn, to: o_status_path(conn, :notice, activity.id))
diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
index be7477867..0a3f45620 100644
--- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
@@ -6,7 +6,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper,
- only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2, skip_relationships?: 1]
+ only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2]
alias Ecto.Changeset
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
@@ -19,6 +19,13 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
require Pleroma.Constants
plug(
+ OpenApiSpex.Plug.PutApiSpec,
+ [module: Pleroma.Web.ApiSpec] when action == :confirmation_resend
+ )
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+ plug(
:skip_plug,
[OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :confirmation_resend
)
@@ -49,9 +56,11 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
plug(:assign_account_by_id when action in [:favourites, :subscribe, :unsubscribe])
plug(:put_view, Pleroma.Web.MastodonAPI.AccountView)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaAccountOperation
+
@doc "POST /api/v1/pleroma/accounts/confirmation_resend"
def confirmation_resend(conn, params) do
- nickname_or_email = params["email"] || params["nickname"]
+ nickname_or_email = params[:email] || params[:nickname]
with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
{:ok, _} <- User.try_send_confirmation_email(user) do
@@ -60,7 +69,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
end
@doc "PATCH /api/v1/pleroma/accounts/update_avatar"
- def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
+ def update_avatar(%{assigns: %{user: user}, body_params: %{img: ""}} = conn, _) do
{:ok, _user} =
user
|> Changeset.change(%{avatar: nil})
@@ -69,7 +78,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
json(conn, %{url: nil})
end
- def update_avatar(%{assigns: %{user: user}} = conn, params) do
+ def update_avatar(%{assigns: %{user: user}, body_params: params} = conn, _params) do
{:ok, %{data: data}} = ActivityPub.upload(params, type: :avatar)
{:ok, _user} = user |> Changeset.change(%{avatar: data}) |> User.update_and_set_cache()
%{"url" => [%{"href" => href} | _]} = data
@@ -78,14 +87,14 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
end
@doc "PATCH /api/v1/pleroma/accounts/update_banner"
- def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
+ def update_banner(%{assigns: %{user: user}, body_params: %{banner: ""}} = conn, _) do
with {:ok, _user} <- User.update_banner(user, %{}) do
json(conn, %{url: nil})
end
end
- def update_banner(%{assigns: %{user: user}} = conn, params) do
- with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
+ def update_banner(%{assigns: %{user: user}, body_params: params} = conn, _) do
+ with {:ok, object} <- ActivityPub.upload(%{img: params[:banner]}, type: :banner),
{:ok, _user} <- User.update_banner(user, object.data) do
%{"url" => [%{"href" => href} | _]} = object.data
@@ -94,13 +103,13 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
end
@doc "PATCH /api/v1/pleroma/accounts/update_background"
- def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
+ def update_background(%{assigns: %{user: user}, body_params: %{img: ""}} = conn, _) do
with {:ok, _user} <- User.update_background(user, %{}) do
json(conn, %{url: nil})
end
end
- def update_background(%{assigns: %{user: user}} = conn, params) do
+ def update_background(%{assigns: %{user: user}, body_params: params} = conn, _) do
with {:ok, object} <- ActivityPub.upload(params, type: :background),
{:ok, _user} <- User.update_background(user, object.data) do
%{"url" => [%{"href" => href} | _]} = object.data
@@ -117,6 +126,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
def favourites(%{assigns: %{user: for_user, account: user}} = conn, params) do
params =
params
+ |> Map.new(fn {key, value} -> {to_string(key), value} end)
|> Map.put("type", "Create")
|> Map.put("favorited_by", user.ap_id)
|> Map.put("blocking_user", for_user)
@@ -139,8 +149,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
|> render("index.json",
activities: activities,
for: for_user,
- as: :activity,
- skip_relationships: skip_relationships?(params)
+ as: :activity
)
end
diff --git a/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex b/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex
new file mode 100644
index 000000000..21d5eb8d5
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex
@@ -0,0 +1,95 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.ConversationController do
+ use Pleroma.Web, :controller
+
+ import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
+
+ alias Pleroma.Conversation.Participation
+ alias Pleroma.Plugs.OAuthScopesPlug
+ alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.MastodonAPI.StatusView
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(:put_view, Pleroma.Web.MastodonAPI.ConversationView)
+ plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in [:show, :statuses])
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["write:conversations"]} when action in [:update, :mark_as_read]
+ )
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaConversationOperation
+
+ def show(%{assigns: %{user: %{id: user_id} = user}} = conn, %{id: participation_id}) do
+ with %Participation{user_id: ^user_id} = participation <- Participation.get(participation_id) do
+ render(conn, "participation.json", participation: participation, for: user)
+ else
+ _error ->
+ conn
+ |> put_status(:not_found)
+ |> json(%{"error" => "Unknown conversation id"})
+ end
+ end
+
+ def statuses(
+ %{assigns: %{user: %{id: user_id} = user}} = conn,
+ %{id: participation_id} = params
+ ) do
+ with %Participation{user_id: ^user_id} = participation <-
+ Participation.get(participation_id, preload: [:conversation]) do
+ params =
+ params
+ |> Map.new(fn {key, value} -> {to_string(key), value} end)
+ |> Map.put("blocking_user", user)
+ |> Map.put("muting_user", user)
+ |> Map.put("user", user)
+
+ activities =
+ participation.conversation.ap_id
+ |> ActivityPub.fetch_activities_for_context_query(params)
+ |> Pleroma.Pagination.fetch_paginated(Map.put(params, "total", false))
+ |> Enum.reverse()
+
+ conn
+ |> add_link_headers(activities)
+ |> put_view(StatusView)
+ |> render("index.json", activities: activities, for: user, as: :activity)
+ else
+ _error ->
+ conn
+ |> put_status(:not_found)
+ |> json(%{"error" => "Unknown conversation id"})
+ end
+ end
+
+ def update(
+ %{assigns: %{user: %{id: user_id} = user}} = conn,
+ %{id: participation_id, recipients: recipients}
+ ) do
+ with %Participation{user_id: ^user_id} = participation <- Participation.get(participation_id),
+ {:ok, participation} <- Participation.set_recipients(participation, recipients) do
+ render(conn, "participation.json", participation: participation, for: user)
+ else
+ {:error, message} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{"error" => message})
+
+ _error ->
+ conn
+ |> put_status(:not_found)
+ |> json(%{"error" => "Unknown conversation id"})
+ end
+ end
+
+ def mark_as_read(%{assigns: %{user: user}} = conn, _params) do
+ with {:ok, _, participations} <- Participation.mark_all_as_read(user) do
+ conn
+ |> add_link_headers(participations)
+ |> render("participations.json", participations: participations, for: user)
+ end
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex
index d276b96a4..2c53dcde1 100644
--- a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex
@@ -1,8 +1,10 @@
-defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
+defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
use Pleroma.Web, :controller
alias Pleroma.Emoji.Pack
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
plug(
Pleroma.Plugs.OAuthScopesPlug,
%{scopes: ["write"], admin: true}
@@ -19,39 +21,37 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
]
)
- plug(
- :skip_plug,
- [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug]
- when action in [:archive, :show, :list]
- )
+ @skip_plugs [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug]
+ plug(:skip_plug, @skip_plugs when action in [:archive, :show, :list])
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaEmojiPackOperation
- def remote(conn, %{"url" => url}) do
+ def remote(conn, %{url: url}) do
with {:ok, packs} <- Pack.list_remote(url) do
json(conn, packs)
else
- {:shareable, _} ->
+ {:error, :not_shareable} ->
conn
|> put_status(:internal_server_error)
|> json(%{error: "The requested instance does not support sharing emoji packs"})
end
end
- def list(conn, _params) do
+ def index(conn, _params) do
emoji_path =
- Path.join(
- Pleroma.Config.get!([:instance, :static_dir]),
- "emoji"
- )
+ [:instance, :static_dir]
+ |> Pleroma.Config.get!()
+ |> Path.join("emoji")
with {:ok, packs} <- Pack.list_local() do
json(conn, packs)
else
- {:create_dir, {:error, e}} ->
+ {:error, :create_dir, e} ->
conn
|> put_status(:internal_server_error)
|> json(%{error: "Failed to create the emoji pack directory at #{emoji_path}: #{e}"})
- {:ls, {:error, e}} ->
+ {:error, :ls, e} ->
conn
|> put_status(:internal_server_error)
|> json(%{
@@ -60,13 +60,13 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
end
end
- def show(conn, %{"name" => name}) do
+ def show(conn, %{name: name}) do
name = String.trim(name)
with {:ok, pack} <- Pack.show(name) do
json(conn, pack)
else
- {:loaded, _} ->
+ {:error, :not_found} ->
conn
|> put_status(:not_found)
|> json(%{error: "Pack #{name} does not exist"})
@@ -78,11 +78,11 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
end
end
- def archive(conn, %{"name" => name}) do
+ def archive(conn, %{name: name}) do
with {:ok, archive} <- Pack.get_archive(name) do
send_download(conn, {:binary, archive}, filename: "#{name}.zip")
else
- {:can_download?, _} ->
+ {:error, :cant_download} ->
conn
|> put_status(:forbidden)
|> json(%{
@@ -90,23 +90,23 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
"Pack #{name} cannot be downloaded from this instance, either pack sharing was disabled for this pack or some files are missing"
})
- {:exists?, _} ->
+ {:error, :not_found} ->
conn
|> put_status(:not_found)
|> json(%{error: "Pack #{name} does not exist"})
end
end
- def download(conn, %{"url" => url, "name" => name} = params) do
- with :ok <- Pack.download(name, url, params["as"]) do
+ def download(%{body_params: %{url: url, name: name} = params} = conn, _) do
+ with {:ok, _pack} <- Pack.download(name, url, params[:as]) do
json(conn, "ok")
else
- {:shareable, _} ->
+ {:error, :not_shareable} ->
conn
|> put_status(:internal_server_error)
|> json(%{error: "The requested instance does not support sharing emoji packs"})
- {:checksum, _} ->
+ {:error, :imvalid_checksum} ->
conn
|> put_status(:internal_server_error)
|> json(%{error: "SHA256 for the pack doesn't match the one sent by the server"})
@@ -118,10 +118,10 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
end
end
- def create(conn, %{"name" => name}) do
+ def create(conn, %{name: name}) do
name = String.trim(name)
- with :ok <- Pack.create(name) do
+ with {:ok, _pack} <- Pack.create(name) do
json(conn, "ok")
else
{:error, :eexist} ->
@@ -143,7 +143,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
end
end
- def delete(conn, %{"name" => name}) do
+ def delete(conn, %{name: name}) do
name = String.trim(name)
with {:ok, deleted} when deleted != [] <- Pack.delete(name) do
@@ -166,11 +166,11 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
end
end
- def update(conn, %{"name" => name, "metadata" => metadata}) do
+ def update(%{body_params: %{metadata: metadata}} = conn, %{name: name}) do
with {:ok, pack} <- Pack.update_metadata(name, metadata) do
json(conn, pack.pack)
else
- {:has_all_files?, _} ->
+ {:error, :incomplete} ->
conn
|> put_status(:bad_request)
|> json(%{error: "The fallback archive does not have all files specified in pack.json"})
@@ -184,19 +184,19 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
end
end
- def add_file(conn, %{"name" => name} = params) do
- filename = params["filename"] || get_filename(params["file"])
- shortcode = params["shortcode"] || Path.basename(filename, Path.extname(filename))
+ def add_file(%{body_params: params} = conn, %{name: name}) do
+ filename = params[:filename] || get_filename(params[:file])
+ shortcode = params[:shortcode] || Path.basename(filename, Path.extname(filename))
- with {:ok, pack} <- Pack.add_file(name, shortcode, filename, params["file"]) do
+ with {:ok, pack} <- Pack.add_file(name, shortcode, filename, params[:file]) do
json(conn, pack.files)
else
- {:exists, _} ->
+ {:error, :already_exists} ->
conn
|> put_status(:conflict)
|> json(%{error: "An emoji with the \"#{shortcode}\" shortcode already exists"})
- {:loaded, _} ->
+ {:error, :not_found} ->
conn
|> put_status(:bad_request)
|> json(%{error: "pack \"#{name}\" is not found"})
@@ -215,20 +215,20 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
end
end
- def update_file(conn, %{"name" => name, "shortcode" => shortcode} = params) do
- new_shortcode = params["new_shortcode"]
- new_filename = params["new_filename"]
- force = params["force"] == true
+ def update_file(%{body_params: %{shortcode: shortcode} = params} = conn, %{name: name}) do
+ new_shortcode = params[:new_shortcode]
+ new_filename = params[:new_filename]
+ force = params[:force]
with {:ok, pack} <- Pack.update_file(name, shortcode, new_shortcode, new_filename, force) do
json(conn, pack.files)
else
- {:exists, _} ->
+ {:error, :doesnt_exist} ->
conn
|> put_status(:bad_request)
|> json(%{error: "Emoji \"#{shortcode}\" does not exist"})
- {:not_used, _} ->
+ {:error, :already_exists} ->
conn
|> put_status(:conflict)
|> json(%{
@@ -236,7 +236,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
"New shortcode \"#{new_shortcode}\" is already used. If you want to override emoji use 'force' option"
})
- {:loaded, _} ->
+ {:error, :not_found} ->
conn
|> put_status(:bad_request)
|> json(%{error: "pack \"#{name}\" is not found"})
@@ -255,16 +255,16 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
end
end
- def delete_file(conn, %{"name" => name, "shortcode" => shortcode}) do
+ def delete_file(conn, %{name: name, shortcode: shortcode}) do
with {:ok, pack} <- Pack.delete_file(name, shortcode) do
json(conn, pack.files)
else
- {:exists, _} ->
+ {:error, :doesnt_exist} ->
conn
|> put_status(:bad_request)
|> json(%{error: "Emoji \"#{shortcode}\" does not exist"})
- {:loaded, _} ->
+ {:error, :not_found} ->
conn
|> put_status(:bad_request)
|> json(%{error: "pack \"#{name}\" is not found"})
diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex
new file mode 100644
index 000000000..19dcffdf3
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex
@@ -0,0 +1,63 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Activity
+ alias Pleroma.Object
+ alias Pleroma.Plugs.OAuthScopesPlug
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.MastodonAPI.StatusView
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action in [:create, :delete])
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["read:statuses"], fallback: :proceed_unauthenticated}
+ when action == :index
+ )
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.EmojiReactionOperation
+
+ action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+
+ def index(%{assigns: %{user: user}} = conn, %{id: activity_id} = params) do
+ with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
+ %Object{data: %{"reactions" => reactions}} when is_list(reactions) <-
+ Object.normalize(activity) do
+ reactions = filter(reactions, params)
+ render(conn, "index.json", emoji_reactions: reactions, user: user)
+ else
+ _e -> json(conn, [])
+ end
+ end
+
+ defp filter(reactions, %{emoji: emoji}) when is_binary(emoji) do
+ Enum.filter(reactions, fn [e, _] -> e == emoji end)
+ end
+
+ defp filter(reactions, _), do: reactions
+
+ def create(%{assigns: %{user: user}} = conn, %{id: activity_id, emoji: emoji}) do
+ with {:ok, _activity} <- CommonAPI.react_with_emoji(activity_id, user, emoji) do
+ activity = Activity.get_by_id(activity_id)
+
+ conn
+ |> put_view(StatusView)
+ |> render("show.json", activity: activity, for: user, as: :activity)
+ end
+ end
+
+ def delete(%{assigns: %{user: user}} = conn, %{id: activity_id, emoji: emoji}) do
+ with {:ok, _activity} <- CommonAPI.unreact_with_emoji(activity_id, user, emoji) do
+ activity = Activity.get_by_id(activity_id)
+
+ conn
+ |> put_view(StatusView)
+ |> render("show.json", activity: activity, for: user, as: :activity)
+ end
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex
index d4e0d8b7c..df6c50ca5 100644
--- a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex
@@ -9,16 +9,19 @@ defmodule Pleroma.Web.PleromaAPI.MascotController do
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(OAuthScopesPlug, %{scopes: ["read:accounts"]} when action == :show)
plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action != :show)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaMascotOperation
+
@doc "GET /api/v1/pleroma/mascot"
def show(%{assigns: %{user: user}} = conn, _params) do
json(conn, User.get_mascot(user))
end
@doc "PUT /api/v1/pleroma/mascot"
- def update(%{assigns: %{user: user}} = conn, %{"file" => file}) do
+ def update(%{assigns: %{user: user}, body_params: %{file: file}} = conn, _) do
with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
# Reject if not an image
%{type: "image"} = attachment <- render_attachment(object) do
diff --git a/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex b/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex
new file mode 100644
index 000000000..3ed8bd294
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex
@@ -0,0 +1,36 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.NotificationController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Notification
+ alias Pleroma.Plugs.OAuthScopesPlug
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :mark_as_read)
+ plug(:put_view, Pleroma.Web.MastodonAPI.NotificationView)
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaNotificationOperation
+
+ def mark_as_read(%{assigns: %{user: user}, body_params: %{id: notification_id}} = conn, _) do
+ with {:ok, notification} <- Notification.read_one(user, notification_id) do
+ render(conn, "show.json", notification: notification, for: user)
+ else
+ {:error, message} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{"error" => message})
+ end
+ end
+
+ def mark_as_read(%{assigns: %{user: user}, body_params: %{max_id: max_id}} = conn, _) do
+ notifications =
+ user
+ |> Notification.set_read_up_to(max_id)
+ |> Enum.take(80)
+
+ render(conn, "index.json", notifications: notifications, for: user)
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
deleted file mode 100644
index 1bdb3aa4d..000000000
--- a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
+++ /dev/null
@@ -1,216 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
- use Pleroma.Web, :controller
-
- import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2, skip_relationships?: 1]
-
- alias Pleroma.Activity
- alias Pleroma.Conversation.Participation
- alias Pleroma.Notification
- alias Pleroma.Object
- alias Pleroma.Plugs.OAuthScopesPlug
- alias Pleroma.User
- alias Pleroma.Web.ActivityPub.ActivityPub
- alias Pleroma.Web.CommonAPI
- alias Pleroma.Web.MastodonAPI.AccountView
- alias Pleroma.Web.MastodonAPI.ConversationView
- alias Pleroma.Web.MastodonAPI.NotificationView
- alias Pleroma.Web.MastodonAPI.StatusView
-
- plug(
- OAuthScopesPlug,
- %{scopes: ["read:statuses"]}
- when action in [:conversation, :conversation_statuses]
- )
-
- plug(
- OAuthScopesPlug,
- %{scopes: ["read:statuses"], fallback: :proceed_unauthenticated}
- when action == :emoji_reactions_by
- )
-
- plug(
- OAuthScopesPlug,
- %{scopes: ["write:statuses"]}
- when action in [:react_with_emoji, :unreact_with_emoji]
- )
-
- plug(
- OAuthScopesPlug,
- %{scopes: ["write:conversations"]}
- when action in [:update_conversation, :mark_conversations_as_read]
- )
-
- plug(
- OAuthScopesPlug,
- %{scopes: ["write:notifications"]} when action == :mark_notifications_as_read
- )
-
- def emoji_reactions_by(%{assigns: %{user: user}} = conn, %{"id" => activity_id} = params) do
- with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
- %Object{data: %{"reactions" => emoji_reactions}} when is_list(emoji_reactions) <-
- Object.normalize(activity) do
- reactions =
- emoji_reactions
- |> Enum.map(fn [emoji, user_ap_ids] ->
- if params["emoji"] && params["emoji"] != emoji do
- nil
- else
- users =
- Enum.map(user_ap_ids, &User.get_cached_by_ap_id/1)
- |> Enum.filter(fn
- %{deactivated: false} -> true
- _ -> false
- end)
-
- %{
- name: emoji,
- count: length(users),
- accounts: AccountView.render("index.json", %{users: users, for: user, as: :user}),
- me: !!(user && user.ap_id in user_ap_ids)
- }
- end
- end)
- |> Enum.filter(& &1)
-
- conn
- |> json(reactions)
- else
- _e ->
- conn
- |> json([])
- end
- end
-
- def react_with_emoji(%{assigns: %{user: user}} = conn, %{"id" => activity_id, "emoji" => emoji}) do
- with {:ok, _activity, _object} <- CommonAPI.react_with_emoji(activity_id, user, emoji),
- activity <- Activity.get_by_id(activity_id) do
- conn
- |> put_view(StatusView)
- |> render("show.json", %{activity: activity, for: user, as: :activity})
- end
- end
-
- def unreact_with_emoji(%{assigns: %{user: user}} = conn, %{
- "id" => activity_id,
- "emoji" => emoji
- }) do
- with {:ok, _activity, _object} <- CommonAPI.unreact_with_emoji(activity_id, user, emoji),
- activity <- Activity.get_by_id(activity_id) do
- conn
- |> put_view(StatusView)
- |> render("show.json", %{activity: activity, for: user, as: :activity})
- end
- end
-
- def conversation(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
- with %Participation{} = participation <- Participation.get(participation_id),
- true <- user.id == participation.user_id do
- conn
- |> put_view(ConversationView)
- |> render("participation.json", %{participation: participation, for: user})
- else
- _error ->
- conn
- |> put_status(404)
- |> json(%{"error" => "Unknown conversation id"})
- end
- end
-
- def conversation_statuses(
- %{assigns: %{user: %{id: user_id} = user}} = conn,
- %{"id" => participation_id} = params
- ) do
- with %Participation{user_id: ^user_id} = participation <-
- Participation.get(participation_id, preload: [:conversation]) do
- params =
- params
- |> Map.put("blocking_user", user)
- |> Map.put("muting_user", user)
- |> Map.put("user", user)
-
- activities =
- participation.conversation.ap_id
- |> ActivityPub.fetch_activities_for_context_query(params)
- |> Pleroma.Pagination.fetch_paginated(Map.put(params, "total", false))
- |> Enum.reverse()
-
- conn
- |> add_link_headers(activities)
- |> put_view(StatusView)
- |> render("index.json",
- activities: activities,
- for: user,
- as: :activity,
- skip_relationships: skip_relationships?(params)
- )
- else
- _error ->
- conn
- |> put_status(404)
- |> json(%{"error" => "Unknown conversation id"})
- end
- end
-
- def update_conversation(
- %{assigns: %{user: user}} = conn,
- %{"id" => participation_id, "recipients" => recipients}
- ) do
- with %Participation{} = participation <- Participation.get(participation_id),
- true <- user.id == participation.user_id,
- {:ok, participation} <- Participation.set_recipients(participation, recipients) do
- conn
- |> put_view(ConversationView)
- |> render("participation.json", %{participation: participation, for: user})
- else
- {:error, message} ->
- conn
- |> put_status(:bad_request)
- |> json(%{"error" => message})
-
- _error ->
- conn
- |> put_status(404)
- |> json(%{"error" => "Unknown conversation id"})
- end
- end
-
- def mark_conversations_as_read(%{assigns: %{user: user}} = conn, _params) do
- with {:ok, _, participations} <- Participation.mark_all_as_read(user) do
- conn
- |> add_link_headers(participations)
- |> put_view(ConversationView)
- |> render("participations.json", participations: participations, for: user)
- end
- end
-
- def mark_notifications_as_read(%{assigns: %{user: user}} = conn, %{"id" => notification_id}) do
- with {:ok, notification} <- Notification.read_one(user, notification_id) do
- conn
- |> put_view(NotificationView)
- |> render("show.json", %{notification: notification, for: user})
- else
- {:error, message} ->
- conn
- |> put_status(:bad_request)
- |> json(%{"error" => message})
- end
- end
-
- def mark_notifications_as_read(%{assigns: %{user: user}} = conn, %{"max_id" => max_id} = params) do
- with notifications <- Notification.set_read_up_to(user, max_id) do
- notifications = Enum.take(notifications, 80)
-
- conn
- |> put_view(NotificationView)
- |> render("index.json",
- notifications: notifications,
- for: user,
- skip_relationships: skip_relationships?(params)
- )
- end
- end
-end
diff --git a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex
index 22da6c0ad..8665ca56c 100644
--- a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex
@@ -5,34 +5,27 @@
defmodule Pleroma.Web.PleromaAPI.ScrobbleController do
use Pleroma.Web, :controller
- import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2, fetch_integer_param: 2]
+ import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
- alias Pleroma.Web.MastodonAPI.StatusView
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(
OAuthScopesPlug,
- %{scopes: ["read"], fallback: :proceed_unauthenticated} when action == :user_scrobbles
+ %{scopes: ["read"], fallback: :proceed_unauthenticated} when action == :index
)
- plug(OAuthScopesPlug, %{scopes: ["write"]} when action != :user_scrobbles)
+ plug(OAuthScopesPlug, %{scopes: ["write"]} when action == :create)
- def new_scrobble(%{assigns: %{user: user}} = conn, %{"title" => _} = params) do
- params =
- if !params["length"] do
- params
- else
- params
- |> Map.put("length", fetch_integer_param(params, "length"))
- end
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaScrobbleOperation
+ def create(%{assigns: %{user: user}, body_params: params} = conn, _) do
with {:ok, activity} <- CommonAPI.listen(user, params) do
- conn
- |> put_view(StatusView)
- |> render("listen.json", %{activity: activity, for: user})
+ render(conn, "show.json", activity: activity, for: user)
else
{:error, message} ->
conn
@@ -41,16 +34,18 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleController do
end
end
- def user_scrobbles(%{assigns: %{user: reading_user}} = conn, params) do
- with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
- params = Map.put(params, "type", ["Listen"])
+ def index(%{assigns: %{user: reading_user}} = conn, %{id: id} = params) do
+ with %User{} = user <- User.get_cached_by_nickname_or_id(id, for: reading_user) do
+ params =
+ params
+ |> Map.new(fn {key, value} -> {to_string(key), value} end)
+ |> Map.put("type", ["Listen"])
activities = ActivityPub.fetch_user_abstract_activities(user, reading_user, params)
conn
|> add_link_headers(activities)
- |> put_view(StatusView)
- |> render("listens.json", %{
+ |> render("index.json", %{
activities: activities,
for: reading_user,
as: :activity
diff --git a/lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex b/lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex
new file mode 100644
index 000000000..b86791d09
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex
@@ -0,0 +1,133 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.TwoFactorAuthenticationController do
+ @moduledoc "The module represents actions to manage MFA"
+ use Pleroma.Web, :controller
+
+ import Pleroma.Web.ControllerHelper, only: [json_response: 3]
+
+ alias Pleroma.MFA
+ alias Pleroma.MFA.TOTP
+ alias Pleroma.Plugs.OAuthScopesPlug
+ alias Pleroma.Web.CommonAPI.Utils
+
+ plug(OAuthScopesPlug, %{scopes: ["read:security"]} when action in [:settings])
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["write:security"]} when action in [:setup, :confirm, :disable, :backup_codes]
+ )
+
+ @doc """
+ Gets user multi factor authentication settings
+
+ ## Endpoint
+ GET /api/pleroma/accounts/mfa
+
+ """
+ def settings(%{assigns: %{user: user}} = conn, _params) do
+ json(conn, %{settings: MFA.mfa_settings(user)})
+ end
+
+ @doc """
+ Prepare setup mfa method
+
+ ## Endpoint
+ GET /api/pleroma/accounts/mfa/setup/[:method]
+
+ """
+ def setup(%{assigns: %{user: user}} = conn, %{"method" => "totp"} = _params) do
+ with {:ok, user} <- MFA.setup_totp(user),
+ %{secret: secret} = _ <- user.multi_factor_authentication_settings.totp do
+ provisioning_uri = TOTP.provisioning_uri(secret, "#{user.email}")
+
+ json(conn, %{provisioning_uri: provisioning_uri, key: secret})
+ else
+ {:error, message} ->
+ json_response(conn, :unprocessable_entity, %{error: message})
+ end
+ end
+
+ def setup(conn, _params) do
+ json_response(conn, :bad_request, %{error: "undefined method"})
+ end
+
+ @doc """
+ Confirms setup and enable mfa method
+
+ ## Endpoint
+ POST /api/pleroma/accounts/mfa/confirm/:method
+
+ - params:
+ `code` - confirmation code
+ `password` - current password
+ """
+ def confirm(
+ %{assigns: %{user: user}} = conn,
+ %{"method" => "totp", "password" => _, "code" => _} = params
+ ) do
+ with {:ok, _user} <- Utils.confirm_current_password(user, params["password"]),
+ {:ok, _user} <- MFA.confirm_totp(user, params) do
+ json(conn, %{})
+ else
+ {:error, message} ->
+ json_response(conn, :unprocessable_entity, %{error: message})
+ end
+ end
+
+ def confirm(conn, _) do
+ json_response(conn, :bad_request, %{error: "undefined mfa method"})
+ end
+
+ @doc """
+ Disable mfa method and disable mfa if need.
+ """
+ def disable(%{assigns: %{user: user}} = conn, %{"method" => "totp"} = params) do
+ with {:ok, user} <- Utils.confirm_current_password(user, params["password"]),
+ {:ok, _user} <- MFA.disable_totp(user) do
+ json(conn, %{})
+ else
+ {:error, message} ->
+ json_response(conn, :unprocessable_entity, %{error: message})
+ end
+ end
+
+ def disable(%{assigns: %{user: user}} = conn, %{"method" => "mfa"} = params) do
+ with {:ok, user} <- Utils.confirm_current_password(user, params["password"]),
+ {:ok, _user} <- MFA.disable(user) do
+ json(conn, %{})
+ else
+ {:error, message} ->
+ json_response(conn, :unprocessable_entity, %{error: message})
+ end
+ end
+
+ def disable(conn, _) do
+ json_response(conn, :bad_request, %{error: "undefined mfa method"})
+ end
+
+ @doc """
+ Generates backup codes.
+
+ ## Endpoint
+ GET /api/pleroma/accounts/mfa/backup_codes
+
+ ## Response
+ ### Success
+ `{codes: [codes]}`
+
+ ### Error
+ `{error: [error_message]}`
+
+ """
+ def backup_codes(%{assigns: %{user: user}} = conn, _params) do
+ with {:ok, codes} <- MFA.generate_backup_codes(user) do
+ json(conn, %{codes: codes})
+ else
+ {:error, message} ->
+ json_response(conn, :unprocessable_entity, %{error: message})
+ end
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex b/lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex
new file mode 100644
index 000000000..84d2d303d
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex
@@ -0,0 +1,33 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.EmojiReactionView do
+ use Pleroma.Web, :view
+
+ alias Pleroma.Web.MastodonAPI.AccountView
+
+ def render("index.json", %{emoji_reactions: emoji_reactions} = opts) do
+ render_many(emoji_reactions, __MODULE__, "show.json", opts)
+ end
+
+ def render("show.json", %{emoji_reaction: [emoji, user_ap_ids], user: user}) do
+ users = fetch_users(user_ap_ids)
+
+ %{
+ name: emoji,
+ count: length(users),
+ accounts: render(AccountView, "index.json", users: users, for: user, as: :user),
+ me: !!(user && user.ap_id in user_ap_ids)
+ }
+ end
+
+ defp fetch_users(user_ap_ids) do
+ user_ap_ids
+ |> Enum.map(&Pleroma.User.get_cached_by_ap_id/1)
+ |> Enum.filter(fn
+ %{deactivated: false} -> true
+ _ -> false
+ end)
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/views/scrobble_view.ex b/lib/pleroma/web/pleroma_api/views/scrobble_view.ex
new file mode 100644
index 000000000..bbff93abe
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/views/scrobble_view.ex
@@ -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.Web.PleromaAPI.ScrobbleView do
+ use Pleroma.Web, :view
+
+ require Pleroma.Constants
+
+ alias Pleroma.Activity
+ alias Pleroma.HTML
+ alias Pleroma.Object
+ alias Pleroma.Web.CommonAPI.Utils
+ alias Pleroma.Web.MastodonAPI.AccountView
+ alias Pleroma.Web.MastodonAPI.StatusView
+
+ def render("show.json", %{activity: %Activity{data: %{"type" => "Listen"}} = activity} = opts) do
+ object = Object.normalize(activity)
+
+ user = StatusView.get_user(activity.data["actor"])
+ created_at = Utils.to_masto_date(activity.data["published"])
+
+ %{
+ id: activity.id,
+ account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
+ created_at: created_at,
+ title: object.data["title"] |> HTML.strip_tags(),
+ artist: object.data["artist"] |> HTML.strip_tags(),
+ album: object.data["album"] |> HTML.strip_tags(),
+ length: object.data["length"]
+ }
+ end
+
+ def render("index.json", opts) do
+ safe_render_many(opts.activities, __MODULE__, "show.json", opts)
+ end
+end
diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex
index a9f893f7b..691725702 100644
--- a/lib/pleroma/web/push/impl.ex
+++ b/lib/pleroma/web/push/impl.ex
@@ -106,14 +106,13 @@ defmodule Pleroma.Web.Push.Impl do
def build_content(
%{
- activity: %{data: %{"directMessage" => true}},
user: %{notification_settings: %{privacy_option: true}}
- },
- actor,
+ } = notification,
+ _actor,
_object,
- _mastodon_type
+ mastodon_type
) do
- %{title: "New Direct Message", body: "@#{actor.nickname}"}
+ %{body: format_title(notification, mastodon_type)}
end
def build_content(notification, actor, object, mastodon_type) do
diff --git a/lib/pleroma/web/push/subscription.ex b/lib/pleroma/web/push/subscription.ex
index b99b0c5fb..3e401a490 100644
--- a/lib/pleroma/web/push/subscription.ex
+++ b/lib/pleroma/web/push/subscription.ex
@@ -25,9 +25,9 @@ defmodule Pleroma.Web.Push.Subscription do
timestamps()
end
- @supported_alert_types ~w[follow favourite mention reblog]
+ @supported_alert_types ~w[follow favourite mention reblog]a
- defp alerts(%{"data" => %{"alerts" => alerts}}) do
+ defp alerts(%{data: %{alerts: alerts}}) do
alerts = Map.take(alerts, @supported_alert_types)
%{"alerts" => alerts}
end
@@ -44,9 +44,9 @@ defmodule Pleroma.Web.Push.Subscription do
%User{} = user,
%Token{} = token,
%{
- "subscription" => %{
- "endpoint" => endpoint,
- "keys" => %{"auth" => key_auth, "p256dh" => key_p256dh}
+ subscription: %{
+ endpoint: endpoint,
+ keys: %{auth: key_auth, p256dh: key_p256dh}
}
} = params
) do
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 322e51495..52059b047 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -132,6 +132,7 @@ defmodule Pleroma.Web.Router do
post("/users/follow", AdminAPIController, :user_follow)
post("/users/unfollow", AdminAPIController, :user_unfollow)
+ put("/users/disable_mfa", AdminAPIController, :disable_mfa)
delete("/users", AdminAPIController, :user_delete)
post("/users", AdminAPIController, :users_create)
patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation)
@@ -188,9 +189,10 @@ defmodule Pleroma.Web.Router do
post("/reports/:id/notes", AdminAPIController, :report_notes_create)
delete("/reports/:report_id/notes/:id", AdminAPIController, :report_notes_delete)
- put("/statuses/:id", AdminAPIController, :status_update)
- delete("/statuses/:id", AdminAPIController, :status_delete)
- get("/statuses", AdminAPIController, :list_statuses)
+ get("/statuses/:id", StatusController, :show)
+ put("/statuses/:id", StatusController, :update)
+ delete("/statuses/:id", StatusController, :delete)
+ get("/statuses", StatusController, :index)
get("/config", AdminAPIController, :config_show)
post("/config", AdminAPIController, :config_update)
@@ -214,24 +216,25 @@ defmodule Pleroma.Web.Router do
scope "/packs" do
pipe_through(:admin_api)
- get("/import", EmojiAPIController, :import_from_filesystem)
- get("/remote", EmojiAPIController, :remote)
- post("/download", EmojiAPIController, :download)
+ get("/import", EmojiPackController, :import_from_filesystem)
+ get("/remote", EmojiPackController, :remote)
+ post("/download", EmojiPackController, :download)
- post("/:name", EmojiAPIController, :create)
- patch("/:name", EmojiAPIController, :update)
- delete("/:name", EmojiAPIController, :delete)
+ post("/:name", EmojiPackController, :create)
+ patch("/:name", EmojiPackController, :update)
+ delete("/:name", EmojiPackController, :delete)
- post("/:name/files", EmojiAPIController, :add_file)
- patch("/:name/files", EmojiAPIController, :update_file)
- delete("/:name/files", EmojiAPIController, :delete_file)
+ post("/:name/files", EmojiPackController, :add_file)
+ patch("/:name/files", EmojiPackController, :update_file)
+ delete("/:name/files", EmojiPackController, :delete_file)
end
# Pack info / downloading
scope "/packs" do
- get("/", EmojiAPIController, :list)
- get("/:name", EmojiAPIController, :show)
- get("/:name/archive", EmojiAPIController, :archive)
+ pipe_through(:api)
+ get("/", EmojiPackController, :index)
+ get("/:name", EmojiPackController, :show)
+ get("/:name/archive", EmojiPackController, :archive)
end
end
@@ -257,6 +260,16 @@ defmodule Pleroma.Web.Router do
post("/follow_import", UtilController, :follow_import)
end
+ scope "/api/pleroma", Pleroma.Web.PleromaAPI do
+ pipe_through(:authenticated_api)
+
+ get("/accounts/mfa", TwoFactorAuthenticationController, :settings)
+ get("/accounts/mfa/backup_codes", TwoFactorAuthenticationController, :backup_codes)
+ get("/accounts/mfa/setup/:method", TwoFactorAuthenticationController, :setup)
+ post("/accounts/mfa/confirm/:method", TwoFactorAuthenticationController, :confirm)
+ delete("/accounts/mfa/:method", TwoFactorAuthenticationController, :disable)
+ end
+
scope "/oauth", Pleroma.Web.OAuth do
scope [] do
pipe_through(:oauth)
@@ -268,6 +281,10 @@ defmodule Pleroma.Web.Router do
post("/revoke", OAuthController, :token_revoke)
get("/registration_details", OAuthController, :registration_details)
+ post("/mfa/challenge", MFAController, :challenge)
+ post("/mfa/verify", MFAController, :verify, as: :mfa_verify)
+ get("/mfa", MFAController, :show)
+
scope [] do
pipe_through(:browser)
@@ -281,26 +298,22 @@ defmodule Pleroma.Web.Router do
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:api)
- get("/statuses/:id/reactions/:emoji", PleromaAPIController, :emoji_reactions_by)
- get("/statuses/:id/reactions", PleromaAPIController, :emoji_reactions_by)
+ get("/statuses/:id/reactions/:emoji", EmojiReactionController, :index)
+ get("/statuses/:id/reactions", EmojiReactionController, :index)
end
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
scope [] do
pipe_through(:authenticated_api)
- get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses)
- get("/conversations/:id", PleromaAPIController, :conversation)
- post("/conversations/read", PleromaAPIController, :mark_conversations_as_read)
- end
+ get("/conversations/:id/statuses", ConversationController, :statuses)
+ get("/conversations/:id", ConversationController, :show)
+ post("/conversations/read", ConversationController, :mark_as_read)
+ patch("/conversations/:id", ConversationController, :update)
- scope [] do
- pipe_through(:authenticated_api)
-
- patch("/conversations/:id", PleromaAPIController, :update_conversation)
- put("/statuses/:id/reactions/:emoji", PleromaAPIController, :react_with_emoji)
- delete("/statuses/:id/reactions/:emoji", PleromaAPIController, :unreact_with_emoji)
- post("/notifications/read", PleromaAPIController, :mark_notifications_as_read)
+ put("/statuses/:id/reactions/:emoji", EmojiReactionController, :create)
+ delete("/statuses/:id/reactions/:emoji", EmojiReactionController, :delete)
+ post("/notifications/read", NotificationController, :mark_as_read)
patch("/accounts/update_avatar", AccountController, :update_avatar)
patch("/accounts/update_banner", AccountController, :update_banner)
@@ -309,7 +322,7 @@ defmodule Pleroma.Web.Router do
get("/mascot", MascotController, :show)
put("/mascot", MascotController, :update)
- post("/scrobble", ScrobbleController, :new_scrobble)
+ post("/scrobble", ScrobbleController, :create)
end
scope [] do
@@ -329,7 +342,7 @@ defmodule Pleroma.Web.Router do
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:api)
- get("/accounts/:id/scrobbles", ScrobbleController, :user_scrobbles)
+ get("/accounts/:id/scrobbles", ScrobbleController, :index)
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
@@ -387,6 +400,7 @@ defmodule Pleroma.Web.Router do
post("/markers", MarkerController, :upsert)
post("/media", MediaController, :create)
+ get("/media/:id", MediaController, :show)
put("/media/:id", MediaController, :update)
get("/notifications", NotificationController, :index)
@@ -426,7 +440,7 @@ defmodule Pleroma.Web.Router do
post("/statuses/:id/unmute", StatusController, :unmute_conversation)
post("/push/subscription", SubscriptionController, :create)
- get("/push/subscription", SubscriptionController, :get)
+ get("/push/subscription", SubscriptionController, :show)
put("/push/subscription", SubscriptionController, :update)
delete("/push/subscription", SubscriptionController, :delete)
@@ -481,6 +495,8 @@ defmodule Pleroma.Web.Router do
scope "/api/v2", Pleroma.Web.MastodonAPI do
pipe_through(:api)
get("/search", SearchController, :search2)
+
+ post("/media", MediaController, :create2)
end
scope "/api", Pleroma.Web do
@@ -539,6 +555,10 @@ defmodule Pleroma.Web.Router do
get("/notice/:id", OStatus.OStatusController, :notice)
get("/notice/:id/embed_player", OStatus.OStatusController, :notice_player)
+ # Mastodon compatibility routes
+ get("/users/:nickname/statuses/:id", OStatus.OStatusController, :object)
+ get("/users/:nickname/statuses/:id/activity", OStatus.OStatusController, :activity)
+
get("/users/:nickname/feed", Feed.UserController, :feed, as: :user_feed)
get("/users/:nickname", Feed.UserController, :feed_redirect, as: :user_feed)
diff --git a/lib/pleroma/web/streamer/ping.ex b/lib/pleroma/web/streamer/ping.ex
deleted file mode 100644
index 7a08202a9..000000000
--- a/lib/pleroma/web/streamer/ping.ex
+++ /dev/null
@@ -1,37 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.Streamer.Ping do
- use GenServer
- require Logger
-
- alias Pleroma.Web.Streamer.State
- alias Pleroma.Web.Streamer.StreamerSocket
-
- @keepalive_interval :timer.seconds(30)
-
- def start_link(opts) do
- ping_interval = Keyword.get(opts, :ping_interval, @keepalive_interval)
- GenServer.start_link(__MODULE__, %{ping_interval: ping_interval}, name: __MODULE__)
- end
-
- def init(%{ping_interval: ping_interval} = args) do
- Process.send_after(self(), :ping, ping_interval)
- {:ok, args}
- end
-
- def handle_info(:ping, %{ping_interval: ping_interval} = state) do
- State.get_sockets()
- |> Map.values()
- |> List.flatten()
- |> Enum.each(fn %StreamerSocket{transport_pid: transport_pid} ->
- Logger.debug("Sending keepalive ping")
- send(transport_pid, {:text, ""})
- end)
-
- Process.send_after(self(), :ping, ping_interval)
-
- {:noreply, state}
- end
-end
diff --git a/lib/pleroma/web/streamer/state.ex b/lib/pleroma/web/streamer/state.ex
deleted file mode 100644
index 999550b88..000000000
--- a/lib/pleroma/web/streamer/state.ex
+++ /dev/null
@@ -1,82 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.Streamer.State do
- use GenServer
- require Logger
-
- alias Pleroma.Web.Streamer.StreamerSocket
-
- @env Mix.env()
-
- def start_link(_) do
- GenServer.start_link(__MODULE__, %{sockets: %{}}, name: __MODULE__)
- end
-
- def add_socket(topic, socket) do
- GenServer.call(__MODULE__, {:add, topic, socket})
- end
-
- def remove_socket(topic, socket) do
- do_remove_socket(@env, topic, socket)
- end
-
- def get_sockets do
- %{sockets: stream_sockets} = GenServer.call(__MODULE__, :get_state)
- stream_sockets
- end
-
- def init(init_arg) do
- {:ok, init_arg}
- end
-
- def handle_call(:get_state, _from, state) do
- {:reply, state, state}
- end
-
- def handle_call({:add, topic, socket}, _from, %{sockets: sockets} = state) do
- internal_topic = internal_topic(topic, socket)
- stream_socket = StreamerSocket.from_socket(socket)
-
- sockets_for_topic =
- sockets
- |> Map.get(internal_topic, [])
- |> List.insert_at(0, stream_socket)
- |> Enum.uniq()
-
- state = put_in(state, [:sockets, internal_topic], sockets_for_topic)
- Logger.debug("Got new conn for #{topic}")
- {:reply, state, state}
- end
-
- def handle_call({:remove, topic, socket}, _from, %{sockets: sockets} = state) do
- internal_topic = internal_topic(topic, socket)
- stream_socket = StreamerSocket.from_socket(socket)
-
- sockets_for_topic =
- sockets
- |> Map.get(internal_topic, [])
- |> List.delete(stream_socket)
-
- state = Kernel.put_in(state, [:sockets, internal_topic], sockets_for_topic)
- {:reply, state, state}
- end
-
- defp do_remove_socket(:test, _, _) do
- :ok
- end
-
- defp do_remove_socket(_env, topic, socket) do
- GenServer.call(__MODULE__, {:remove, topic, socket})
- end
-
- defp internal_topic(topic, socket)
- when topic in ~w[user user:notification direct] do
- "#{topic}:#{socket.assigns[:user].id}"
- end
-
- defp internal_topic(topic, _) do
- topic
- end
-end
diff --git a/lib/pleroma/web/streamer/streamer.ex b/lib/pleroma/web/streamer/streamer.ex
index 814d5a729..49a400df7 100644
--- a/lib/pleroma/web/streamer/streamer.ex
+++ b/lib/pleroma/web/streamer/streamer.ex
@@ -3,53 +3,288 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Streamer do
- alias Pleroma.Web.Streamer.State
- alias Pleroma.Web.Streamer.Worker
+ require Logger
+
+ alias Pleroma.Activity
+ alias Pleroma.Config
+ alias Pleroma.Conversation.Participation
+ alias Pleroma.Notification
+ alias Pleroma.Object
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Visibility
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.StreamerView
- @timeout 60_000
@mix_env Mix.env()
+ @registry Pleroma.Web.StreamerRegistry
+
+ def registry, do: @registry
+
+ @public_streams ["public", "public:local", "public:media", "public:local:media"]
+ @user_streams ["user", "user:notification", "direct"]
+
+ @doc "Expands and authorizes a stream, and registers the process for streaming."
+ @spec get_topic_and_add_socket(stream :: String.t(), User.t() | nil, Map.t() | nil) ::
+ {:ok, topic :: String.t()} | {:error, :bad_topic} | {:error, :unauthorized}
+ def get_topic_and_add_socket(stream, user, params \\ %{}) do
+ case get_topic(stream, user, params) do
+ {:ok, topic} -> add_socket(topic, user)
+ error -> error
+ end
+ end
+
+ @doc "Expand and authorizes a stream"
+ @spec get_topic(stream :: String.t(), User.t() | nil, Map.t()) ::
+ {:ok, topic :: String.t()} | {:error, :bad_topic}
+ def get_topic(stream, user, params \\ %{})
- def add_socket(topic, socket) do
- State.add_socket(topic, socket)
+ # Allow all public steams.
+ def get_topic(stream, _, _) when stream in @public_streams do
+ {:ok, stream}
end
- def remove_socket(topic, socket) do
- State.remove_socket(topic, socket)
+ # Allow all hashtags streams.
+ def get_topic("hashtag", _, %{"tag" => tag}) do
+ {:ok, "hashtag:" <> tag}
end
- def get_sockets do
- State.get_sockets()
+ # Expand user streams.
+ def get_topic(stream, %User{} = user, _) when stream in @user_streams do
+ {:ok, stream <> ":" <> to_string(user.id)}
end
- def stream(topics, items) do
- if should_send?() do
- Task.async(fn ->
- :poolboy.transaction(
- :streamer_worker,
- &Worker.stream(&1, topics, items),
- @timeout
- )
+ def get_topic(stream, _, _) when stream in @user_streams do
+ {:error, :unauthorized}
+ end
+
+ # List streams.
+ def get_topic("list", %User{} = user, %{"list" => id}) do
+ if Pleroma.List.get(id, user) do
+ {:ok, "list:" <> to_string(id)}
+ else
+ {:error, :bad_topic}
+ end
+ end
+
+ def get_topic("list", _, _) do
+ {:error, :unauthorized}
+ end
+
+ def get_topic(_, _, _) do
+ {:error, :bad_topic}
+ end
+
+ @doc "Registers the process for streaming. Use `get_topic/3` to get the full authorized topic."
+ def add_socket(topic, user) do
+ if should_env_send?() do
+ auth? = if user, do: true
+ Registry.register(@registry, topic, auth?)
+ end
+
+ {:ok, topic}
+ end
+
+ def remove_socket(topic) do
+ if should_env_send?(), do: Registry.unregister(@registry, topic)
+ end
+
+ def stream(topics, item) when is_list(topics) do
+ if should_env_send?() do
+ Enum.each(topics, fn t ->
+ spawn(fn -> do_stream(t, item) end)
end)
end
+
+ :ok
+ end
+
+ def stream(topic, items) when is_list(items) do
+ if should_env_send?() do
+ Enum.each(items, fn i ->
+ spawn(fn -> do_stream(topic, i) end)
+ end)
+
+ :ok
+ end
end
- def supervisor, do: Pleroma.Web.Streamer.Supervisor
+ def stream(topic, item) do
+ if should_env_send?() do
+ spawn(fn -> do_stream(topic, item) end)
+ end
- defp should_send? do
- handle_should_send(@mix_env)
+ :ok
end
- defp handle_should_send(:test) do
- case Process.whereis(:streamer_worker) do
- nil ->
- false
+ def filtered_by_user?(%User{} = user, %Activity{} = item) do
+ %{block: blocked_ap_ids, mute: muted_ap_ids, reblog_mute: reblog_muted_ap_ids} =
+ User.outgoing_relationships_ap_ids(user, [:block, :mute, :reblog_mute])
+
+ recipient_blocks = MapSet.new(blocked_ap_ids ++ muted_ap_ids)
+ recipients = MapSet.new(item.recipients)
+ domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
- pid ->
- Process.alive?(pid)
+ with parent <- Object.normalize(item) || item,
+ true <-
+ Enum.all?([blocked_ap_ids, muted_ap_ids], &(item.actor not in &1)),
+ true <- item.data["type"] != "Announce" || item.actor not in reblog_muted_ap_ids,
+ true <- Enum.all?([blocked_ap_ids, muted_ap_ids], &(parent.data["actor"] not in &1)),
+ true <- MapSet.disjoint?(recipients, recipient_blocks),
+ %{host: item_host} <- URI.parse(item.actor),
+ %{host: parent_host} <- URI.parse(parent.data["actor"]),
+ false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, item_host),
+ false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, parent_host),
+ true <- thread_containment(item, user),
+ false <- CommonAPI.thread_muted?(user, item) do
+ false
+ else
+ _ -> true
end
end
- defp handle_should_send(:benchmark), do: false
+ def filtered_by_user?(%User{} = user, %Notification{activity: activity}) do
+ filtered_by_user?(user, activity)
+ end
+
+ defp do_stream("direct", item) do
+ recipient_topics =
+ User.get_recipients_from_activity(item)
+ |> Enum.map(fn %{id: id} -> "direct:#{id}" end)
+
+ Enum.each(recipient_topics, fn user_topic ->
+ Logger.debug("Trying to push direct message to #{user_topic}\n\n")
+ push_to_socket(user_topic, item)
+ end)
+ end
+
+ defp do_stream("participation", participation) do
+ user_topic = "direct:#{participation.user_id}"
+ Logger.debug("Trying to push a conversation participation to #{user_topic}\n\n")
+
+ push_to_socket(user_topic, participation)
+ end
- defp handle_should_send(_), do: true
+ defp do_stream("list", item) do
+ # filter the recipient list if the activity is not public, see #270.
+ recipient_lists =
+ case Visibility.is_public?(item) do
+ true ->
+ Pleroma.List.get_lists_from_activity(item)
+
+ _ ->
+ Pleroma.List.get_lists_from_activity(item)
+ |> Enum.filter(fn list ->
+ owner = User.get_cached_by_id(list.user_id)
+
+ Visibility.visible_for_user?(item, owner)
+ end)
+ end
+
+ recipient_topics =
+ recipient_lists
+ |> Enum.map(fn %{id: id} -> "list:#{id}" end)
+
+ Enum.each(recipient_topics, fn list_topic ->
+ Logger.debug("Trying to push message to #{list_topic}\n\n")
+ push_to_socket(list_topic, item)
+ end)
+ end
+
+ defp do_stream(topic, %Notification{} = item)
+ when topic in ["user", "user:notification"] do
+ Registry.dispatch(@registry, "#{topic}:#{item.user_id}", fn list ->
+ Enum.each(list, fn {pid, _auth} ->
+ send(pid, {:render_with_user, StreamerView, "notification.json", item})
+ end)
+ end)
+ end
+
+ defp do_stream("user", item) do
+ Logger.debug("Trying to push to users")
+
+ recipient_topics =
+ User.get_recipients_from_activity(item)
+ |> Enum.map(fn %{id: id} -> "user:#{id}" end)
+
+ Enum.each(recipient_topics, fn topic ->
+ push_to_socket(topic, item)
+ end)
+ end
+
+ defp do_stream(topic, item) do
+ Logger.debug("Trying to push to #{topic}")
+ Logger.debug("Pushing item to #{topic}")
+ push_to_socket(topic, item)
+ end
+
+ defp push_to_socket(topic, %Participation{} = participation) do
+ rendered = StreamerView.render("conversation.json", participation)
+
+ Registry.dispatch(@registry, topic, fn list ->
+ Enum.each(list, fn {pid, _} ->
+ send(pid, {:text, rendered})
+ end)
+ end)
+ end
+
+ defp push_to_socket(topic, %Activity{
+ data: %{"type" => "Delete", "deleted_activity_id" => deleted_activity_id}
+ }) do
+ rendered = Jason.encode!(%{event: "delete", payload: to_string(deleted_activity_id)})
+
+ Registry.dispatch(@registry, topic, fn list ->
+ Enum.each(list, fn {pid, _} ->
+ send(pid, {:text, rendered})
+ end)
+ end)
+ end
+
+ defp push_to_socket(_topic, %Activity{data: %{"type" => "Delete"}}), do: :noop
+
+ defp push_to_socket(topic, item) do
+ anon_render = StreamerView.render("update.json", item)
+
+ Registry.dispatch(@registry, topic, fn list ->
+ Enum.each(list, fn {pid, auth?} ->
+ if auth? do
+ send(pid, {:render_with_user, StreamerView, "update.json", item})
+ else
+ send(pid, {:text, anon_render})
+ end
+ end)
+ end)
+ end
+
+ defp thread_containment(_activity, %User{skip_thread_containment: true}), do: true
+
+ defp thread_containment(activity, user) do
+ if Config.get([:instance, :skip_thread_containment]) do
+ true
+ else
+ ActivityPub.contain_activity(activity, user)
+ end
+ end
+
+ # In test environement, only return true if the registry is started.
+ # In benchmark environment, returns false.
+ # In any other environment, always returns true.
+ cond do
+ @mix_env == :test ->
+ def should_env_send? do
+ case Process.whereis(@registry) do
+ nil ->
+ false
+
+ pid ->
+ Process.alive?(pid)
+ end
+ end
+
+ @mix_env == :benchmark ->
+ def should_env_send?, do: false
+
+ true ->
+ def should_env_send?, do: true
+ end
end
diff --git a/lib/pleroma/web/streamer/streamer_socket.ex b/lib/pleroma/web/streamer/streamer_socket.ex
deleted file mode 100644
index 7d5dcd34e..000000000
--- a/lib/pleroma/web/streamer/streamer_socket.ex
+++ /dev/null
@@ -1,35 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.Streamer.StreamerSocket do
- defstruct transport_pid: nil, user: nil
-
- alias Pleroma.User
- alias Pleroma.Web.Streamer.StreamerSocket
-
- def from_socket(%{
- transport_pid: transport_pid,
- assigns: %{user: nil}
- }) do
- %StreamerSocket{
- transport_pid: transport_pid
- }
- end
-
- def from_socket(%{
- transport_pid: transport_pid,
- assigns: %{user: %User{} = user}
- }) do
- %StreamerSocket{
- transport_pid: transport_pid,
- user: user
- }
- end
-
- def from_socket(%{transport_pid: transport_pid}) do
- %StreamerSocket{
- transport_pid: transport_pid
- }
- end
-end
diff --git a/lib/pleroma/web/streamer/supervisor.ex b/lib/pleroma/web/streamer/supervisor.ex
deleted file mode 100644
index bd9029bc0..000000000
--- a/lib/pleroma/web/streamer/supervisor.ex
+++ /dev/null
@@ -1,37 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.Streamer.Supervisor do
- use Supervisor
-
- def start_link(opts) do
- Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
- end
-
- def init(args) do
- children = [
- {Pleroma.Web.Streamer.State, args},
- {Pleroma.Web.Streamer.Ping, args},
- :poolboy.child_spec(:streamer_worker, poolboy_config())
- ]
-
- opts = [strategy: :one_for_one, name: Pleroma.Web.Streamer.Supervisor]
- Supervisor.init(children, opts)
- end
-
- defp poolboy_config do
- opts =
- Pleroma.Config.get(:streamer,
- workers: 3,
- overflow_workers: 2
- )
-
- [
- {:name, {:local, :streamer_worker}},
- {:worker_module, Pleroma.Web.Streamer.Worker},
- {:size, opts[:workers]},
- {:max_overflow, opts[:overflow_workers]}
- ]
- end
-end
diff --git a/lib/pleroma/web/streamer/worker.ex b/lib/pleroma/web/streamer/worker.ex
deleted file mode 100644
index f6160fa4d..000000000
--- a/lib/pleroma/web/streamer/worker.ex
+++ /dev/null
@@ -1,208 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.Streamer.Worker do
- use GenServer
-
- require Logger
-
- alias Pleroma.Activity
- alias Pleroma.Config
- alias Pleroma.Conversation.Participation
- alias Pleroma.Notification
- alias Pleroma.Object
- alias Pleroma.User
- alias Pleroma.Web.ActivityPub.ActivityPub
- alias Pleroma.Web.ActivityPub.Visibility
- alias Pleroma.Web.CommonAPI
- alias Pleroma.Web.Streamer.State
- alias Pleroma.Web.Streamer.StreamerSocket
- alias Pleroma.Web.StreamerView
-
- def start_link(_) do
- GenServer.start_link(__MODULE__, %{}, [])
- end
-
- def init(init_arg) do
- {:ok, init_arg}
- end
-
- def stream(pid, topics, items) do
- GenServer.call(pid, {:stream, topics, items})
- end
-
- def handle_call({:stream, topics, item}, _from, state) when is_list(topics) do
- Enum.each(topics, fn t ->
- do_stream(%{topic: t, item: item})
- end)
-
- {:reply, state, state}
- end
-
- def handle_call({:stream, topic, items}, _from, state) when is_list(items) do
- Enum.each(items, fn i ->
- do_stream(%{topic: topic, item: i})
- end)
-
- {:reply, state, state}
- end
-
- def handle_call({:stream, topic, item}, _from, state) do
- do_stream(%{topic: topic, item: item})
-
- {:reply, state, state}
- end
-
- defp do_stream(%{topic: "direct", item: item}) do
- recipient_topics =
- User.get_recipients_from_activity(item)
- |> Enum.map(fn %{id: id} -> "direct:#{id}" end)
-
- Enum.each(recipient_topics, fn user_topic ->
- Logger.debug("Trying to push direct message to #{user_topic}\n\n")
- push_to_socket(State.get_sockets(), user_topic, item)
- end)
- end
-
- defp do_stream(%{topic: "participation", item: participation}) do
- user_topic = "direct:#{participation.user_id}"
- Logger.debug("Trying to push a conversation participation to #{user_topic}\n\n")
-
- push_to_socket(State.get_sockets(), user_topic, participation)
- end
-
- defp do_stream(%{topic: "list", item: item}) do
- # filter the recipient list if the activity is not public, see #270.
- recipient_lists =
- case Visibility.is_public?(item) do
- true ->
- Pleroma.List.get_lists_from_activity(item)
-
- _ ->
- Pleroma.List.get_lists_from_activity(item)
- |> Enum.filter(fn list ->
- owner = User.get_cached_by_id(list.user_id)
-
- Visibility.visible_for_user?(item, owner)
- end)
- end
-
- recipient_topics =
- recipient_lists
- |> Enum.map(fn %{id: id} -> "list:#{id}" end)
-
- Enum.each(recipient_topics, fn list_topic ->
- Logger.debug("Trying to push message to #{list_topic}\n\n")
- push_to_socket(State.get_sockets(), list_topic, item)
- end)
- end
-
- defp do_stream(%{topic: topic, item: %Notification{} = item})
- when topic in ["user", "user:notification"] do
- State.get_sockets()
- |> Map.get("#{topic}:#{item.user_id}", [])
- |> Enum.each(fn %StreamerSocket{transport_pid: transport_pid, user: socket_user} ->
- with %User{} = user <- User.get_cached_by_ap_id(socket_user.ap_id),
- true <- should_send?(user, item) do
- send(transport_pid, {:text, StreamerView.render("notification.json", socket_user, item)})
- end
- end)
- end
-
- defp do_stream(%{topic: "user", item: item}) do
- Logger.debug("Trying to push to users")
-
- recipient_topics =
- User.get_recipients_from_activity(item)
- |> Enum.map(fn %{id: id} -> "user:#{id}" end)
-
- Enum.each(recipient_topics, fn topic ->
- push_to_socket(State.get_sockets(), topic, item)
- end)
- end
-
- defp do_stream(%{topic: topic, item: item}) do
- Logger.debug("Trying to push to #{topic}")
- Logger.debug("Pushing item to #{topic}")
- push_to_socket(State.get_sockets(), topic, item)
- end
-
- defp should_send?(%User{} = user, %Activity{} = item) do
- %{block: blocked_ap_ids, mute: muted_ap_ids, reblog_mute: reblog_muted_ap_ids} =
- User.outgoing_relationships_ap_ids(user, [:block, :mute, :reblog_mute])
-
- recipient_blocks = MapSet.new(blocked_ap_ids ++ muted_ap_ids)
- recipients = MapSet.new(item.recipients)
- domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
-
- with parent <- Object.normalize(item) || item,
- true <-
- Enum.all?([blocked_ap_ids, muted_ap_ids], &(item.actor not in &1)),
- true <- item.data["type"] != "Announce" || item.actor not in reblog_muted_ap_ids,
- true <- Enum.all?([blocked_ap_ids, muted_ap_ids], &(parent.data["actor"] not in &1)),
- true <- MapSet.disjoint?(recipients, recipient_blocks),
- %{host: item_host} <- URI.parse(item.actor),
- %{host: parent_host} <- URI.parse(parent.data["actor"]),
- false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, item_host),
- false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, parent_host),
- true <- thread_containment(item, user),
- false <- CommonAPI.thread_muted?(user, item) do
- true
- else
- _ -> false
- end
- end
-
- defp should_send?(%User{} = user, %Notification{activity: activity}) do
- should_send?(user, activity)
- end
-
- def push_to_socket(topics, topic, %Participation{} = participation) do
- Enum.each(topics[topic] || [], fn %StreamerSocket{transport_pid: transport_pid} ->
- send(transport_pid, {:text, StreamerView.render("conversation.json", participation)})
- end)
- end
-
- def push_to_socket(topics, topic, %Activity{
- data: %{"type" => "Delete", "deleted_activity_id" => deleted_activity_id}
- }) do
- Enum.each(topics[topic] || [], fn %StreamerSocket{transport_pid: transport_pid} ->
- send(
- transport_pid,
- {:text, %{event: "delete", payload: to_string(deleted_activity_id)} |> Jason.encode!()}
- )
- end)
- end
-
- def push_to_socket(_topics, _topic, %Activity{data: %{"type" => "Delete"}}), do: :noop
-
- def push_to_socket(topics, topic, item) do
- Enum.each(topics[topic] || [], fn %StreamerSocket{
- transport_pid: transport_pid,
- user: socket_user
- } ->
- # Get the current user so we have up-to-date blocks etc.
- if socket_user do
- user = User.get_cached_by_ap_id(socket_user.ap_id)
-
- if should_send?(user, item) do
- send(transport_pid, {:text, StreamerView.render("update.json", item, user)})
- end
- else
- send(transport_pid, {:text, StreamerView.render("update.json", item)})
- end
- end)
- end
-
- @spec thread_containment(Activity.t(), User.t()) :: boolean()
- defp thread_containment(_activity, %User{skip_thread_containment: true}), do: true
-
- defp thread_containment(activity, user) do
- if Config.get([:instance, :skip_thread_containment]) do
- true
- else
- ActivityPub.contain_activity(activity, user)
- end
- end
-end
diff --git a/lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex b/lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex
new file mode 100644
index 000000000..750f65386
--- /dev/null
+++ b/lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex
@@ -0,0 +1,24 @@
+<%= if get_flash(@conn, :info) do %>
+<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
+<% end %>
+<%= if get_flash(@conn, :error) do %>
+<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
+<% end %>
+
+<h2>Two-factor recovery</h2>
+
+<%= form_for @conn, mfa_verify_path(@conn, :verify), [as: "mfa"], fn f -> %>
+<div class="input">
+ <%= label f, :code, "Recovery code" %>
+ <%= text_input f, :code %>
+ <%= hidden_input f, :mfa_token, value: @mfa_token %>
+ <%= hidden_input f, :state, value: @state %>
+ <%= hidden_input f, :redirect_uri, value: @redirect_uri %>
+ <%= hidden_input f, :challenge_type, value: "recovery" %>
+</div>
+
+<%= submit "Verify" %>
+<% end %>
+<a href="<%= mfa_path(@conn, :show, %{challenge_type: "totp", mfa_token: @mfa_token, state: @state, redirect_uri: @redirect_uri}) %>">
+ Enter a two-factor code
+</a>
diff --git a/lib/pleroma/web/templates/o_auth/mfa/totp.html.eex b/lib/pleroma/web/templates/o_auth/mfa/totp.html.eex
new file mode 100644
index 000000000..af6e546b0
--- /dev/null
+++ b/lib/pleroma/web/templates/o_auth/mfa/totp.html.eex
@@ -0,0 +1,24 @@
+<%= if get_flash(@conn, :info) do %>
+<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
+<% end %>
+<%= if get_flash(@conn, :error) do %>
+<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
+<% end %>
+
+<h2>Two-factor authentication</h2>
+
+<%= form_for @conn, mfa_verify_path(@conn, :verify), [as: "mfa"], fn f -> %>
+<div class="input">
+ <%= label f, :code, "Authentication code" %>
+ <%= text_input f, :code %>
+ <%= hidden_input f, :mfa_token, value: @mfa_token %>
+ <%= hidden_input f, :state, value: @state %>
+ <%= hidden_input f, :redirect_uri, value: @redirect_uri %>
+ <%= hidden_input f, :challenge_type, value: "totp" %>
+</div>
+
+<%= submit "Verify" %>
+<% end %>
+<a href="<%= mfa_path(@conn, :show, %{challenge_type: "recovery", mfa_token: @mfa_token, state: @state, redirect_uri: @redirect_uri}) %>">
+ Enter a two-factor recovery code
+</a>
diff --git a/lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex b/lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex
new file mode 100644
index 000000000..adc3a3e3d
--- /dev/null
+++ b/lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex
@@ -0,0 +1,13 @@
+<%= if @error do %>
+<h2><%= @error %></h2>
+<% end %>
+<h2>Two-factor authentication</h2>
+<p><%= @followee.nickname %></p>
+<img height="128" width="128" src="<%= avatar_url(@followee) %>">
+<%= form_for @conn, remote_follow_path(@conn, :do_follow), [as: "mfa"], fn f -> %>
+<%= text_input f, :code, placeholder: "Authentication code", required: true %>
+<br>
+<%= hidden_input f, :id, value: @followee.id %>
+<%= hidden_input f, :token, value: @mfa_token %>
+<%= submit "Authorize" %>
+<% end %>
diff --git a/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex b/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex
index 89da760da..521dc9322 100644
--- a/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex
+++ b/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex
@@ -8,10 +8,12 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do
require Logger
alias Pleroma.Activity
+ alias Pleroma.MFA
alias Pleroma.Object.Fetcher
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
alias Pleroma.Web.Auth.Authenticator
+ alias Pleroma.Web.Auth.TOTPAuthenticator
alias Pleroma.Web.CommonAPI
@status_types ["Article", "Event", "Note", "Video", "Page", "Question"]
@@ -68,6 +70,8 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do
# POST /ostatus_subscribe
#
+ # adds a remote account in followers if user already is signed in.
+ #
def do_follow(%{assigns: %{user: %User{} = user}} = conn, %{"user" => %{"id" => id}}) do
with {:fetch_user, %User{} = followee} <- {:fetch_user, User.get_cached_by_id(id)},
{:ok, _, _, _} <- CommonAPI.follow(user, followee) do
@@ -78,9 +82,33 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do
end
end
+ # POST /ostatus_subscribe
+ #
+ # step 1.
+ # checks login\password and displays step 2 form of MFA if need.
+ #
def do_follow(conn, %{"authorization" => %{"name" => _, "password" => _, "id" => id}}) do
- with {:fetch_user, %User{} = followee} <- {:fetch_user, User.get_cached_by_id(id)},
+ with {_, %User{} = followee} <- {:fetch_user, User.get_cached_by_id(id)},
{_, {:ok, user}, _} <- {:auth, Authenticator.get_user(conn), followee},
+ {_, _, _, false} <- {:mfa_required, followee, user, MFA.require?(user)},
+ {:ok, _, _, _} <- CommonAPI.follow(user, followee) do
+ redirect(conn, to: "/users/#{followee.id}")
+ else
+ error ->
+ handle_follow_error(conn, error)
+ end
+ end
+
+ # POST /ostatus_subscribe
+ #
+ # step 2
+ # checks TOTP code. otherwise displays form with errors
+ #
+ def do_follow(conn, %{"mfa" => %{"code" => code, "token" => token, "id" => id}}) do
+ with {_, %User{} = followee} <- {:fetch_user, User.get_cached_by_id(id)},
+ {_, _, {:ok, %{user: user}}} <- {:mfa_token, followee, MFA.Token.validate(token)},
+ {_, _, _, {:ok, _}} <-
+ {:verify_mfa_code, followee, token, TOTPAuthenticator.verify(code, user)},
{:ok, _, _, _} <- CommonAPI.follow(user, followee) do
redirect(conn, to: "/users/#{followee.id}")
else
@@ -94,6 +122,23 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do
render(conn, "followed.html", %{error: "Insufficient permissions: follow | write:follows."})
end
+ defp handle_follow_error(conn, {:mfa_token, followee, _} = _) do
+ render(conn, "follow_login.html", %{error: "Wrong username or password", followee: followee})
+ end
+
+ defp handle_follow_error(conn, {:verify_mfa_code, followee, token, _} = _) do
+ render(conn, "follow_mfa.html", %{
+ error: "Wrong authentication code",
+ followee: followee,
+ mfa_token: token
+ })
+ end
+
+ defp handle_follow_error(conn, {:mfa_required, followee, user, _} = _) do
+ {:ok, %{token: token}} = MFA.Token.create_token(user)
+ render(conn, "follow_mfa.html", %{followee: followee, mfa_token: token, error: false})
+ end
+
defp handle_follow_error(conn, {:auth, _, followee} = _) do
render(conn, "follow_login.html", %{error: "Wrong username or password", followee: followee})
end
diff --git a/lib/pleroma/web/views/streamer_view.ex b/lib/pleroma/web/views/streamer_view.ex
index 443868878..237b29ded 100644
--- a/lib/pleroma/web/views/streamer_view.ex
+++ b/lib/pleroma/web/views/streamer_view.ex
@@ -25,7 +25,7 @@ defmodule Pleroma.Web.StreamerView do
|> Jason.encode!()
end
- def render("notification.json", %User{} = user, %Notification{} = notify) do
+ def render("notification.json", %Notification{} = notify, %User{} = user) do
%{
event: "notification",
payload:
diff --git a/lib/pleroma/web/web.ex b/lib/pleroma/web/web.ex
index 40ea9b4c3..69aebd877 100644
--- a/lib/pleroma/web/web.ex
+++ b/lib/pleroma/web/web.ex
@@ -204,11 +204,17 @@ defmodule Pleroma.Web do
@impl Plug
@doc """
- If marked as skipped, returns `conn`, otherwise calls `perform/2`.
+ Before-plug hook that
+ * ensures the plug is not skipped
+ * processes `:if_func` / `:unless_func` functional pre-run conditions
+ * adds plug to the list of called plugs and calls `perform/2` if checks are passed
+
Note: multiple invocations of the same plug (with different or same options) are allowed.
"""
def call(%Plug.Conn{} = conn, options) do
- if PlugHelper.plug_skipped?(conn, __MODULE__) do
+ if PlugHelper.plug_skipped?(conn, __MODULE__) ||
+ (options[:if_func] && !options[:if_func].(conn)) ||
+ (options[:unless_func] && options[:unless_func].(conn)) do
conn
else
conn =
diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex
index 7ffd0e51b..71ccf251a 100644
--- a/lib/pleroma/web/web_finger/web_finger.ex
+++ b/lib/pleroma/web/web_finger/web_finger.ex
@@ -86,54 +86,24 @@ defmodule Pleroma.Web.WebFinger do
|> XmlBuilder.to_doc()
end
- defp get_magic_key("data:application/magic-public-key," <> magic_key) do
- {:ok, magic_key}
- end
-
- defp get_magic_key(nil) do
- Logger.debug("Undefined magic key.")
- {:ok, nil}
- end
+ defp webfinger_from_xml(doc) do
+ subject = XML.string_from_xpath("//Subject", doc)
- defp get_magic_key(_) do
- {:error, "Missing magic key data."}
- end
+ subscribe_address =
+ ~s{//Link[@rel="http://ostatus.org/schema/1.0/subscribe"]/@template}
+ |> XML.string_from_xpath(doc)
- defp webfinger_from_xml(doc) do
- with magic_key <- XML.string_from_xpath(~s{//Link[@rel="magic-public-key"]/@href}, doc),
- {:ok, magic_key} <- get_magic_key(magic_key),
- topic <-
- XML.string_from_xpath(
- ~s{//Link[@rel="http://schemas.google.com/g/2010#updates-from"]/@href},
- doc
- ),
- subject <- XML.string_from_xpath("//Subject", doc),
- subscribe_address <-
- XML.string_from_xpath(
- ~s{//Link[@rel="http://ostatus.org/schema/1.0/subscribe"]/@template},
- doc
- ),
- ap_id <-
- XML.string_from_xpath(
- ~s{//Link[@rel="self" and @type="application/activity+json"]/@href},
- doc
- ) do
- data = %{
- "magic_key" => magic_key,
- "topic" => topic,
- "subject" => subject,
- "subscribe_address" => subscribe_address,
- "ap_id" => ap_id
- }
+ ap_id =
+ ~s{//Link[@rel="self" and @type="application/activity+json"]/@href}
+ |> XML.string_from_xpath(doc)
- {:ok, data}
- else
- {:error, e} ->
- {:error, e}
+ data = %{
+ "subject" => subject,
+ "subscribe_address" => subscribe_address,
+ "ap_id" => ap_id
+ }
- e ->
- {:error, e}
- end
+ {:ok, data}
end
defp webfinger_from_json(doc) do
@@ -146,9 +116,6 @@ defmodule Pleroma.Web.WebFinger do
{"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", "self"} ->
Map.put(data, "ap_id", link["href"])
- {_, "http://ostatus.org/schema/1.0/subscribe"} ->
- Map.put(data, "subscribe_address", link["template"])
-
_ ->
Logger.debug("Unhandled type: #{inspect(link["type"])}")
data
@@ -194,13 +161,15 @@ defmodule Pleroma.Web.WebFinger do
URI.parse(account).host
end
+ encoded_account = URI.encode("acct:#{account}")
+
address =
case find_lrdd_template(domain) do
{:ok, template} ->
- String.replace(template, "{uri}", URI.encode(account))
+ String.replace(template, "{uri}", encoded_account)
_ ->
- "https://#{domain}/.well-known/webfinger?resource=acct:#{account}"
+ "https://#{domain}/.well-known/webfinger?resource=#{encoded_account}"
end
with response <-
diff --git a/lib/pleroma/workers/attachments_cleanup_worker.ex b/lib/pleroma/workers/attachments_cleanup_worker.ex
index 3c5820a86..49352db2a 100644
--- a/lib/pleroma/workers/attachments_cleanup_worker.ex
+++ b/lib/pleroma/workers/attachments_cleanup_worker.ex
@@ -27,8 +27,20 @@ defmodule Pleroma.Workers.AttachmentsCleanupWorker do
uploader = Pleroma.Config.get([Pleroma.Upload, :uploader])
+ prefix =
+ case Pleroma.Config.get([Pleroma.Upload, :base_url]) do
+ nil -> "media"
+ _ -> ""
+ end
+
+ base_url =
+ String.trim_trailing(
+ Pleroma.Config.get([Pleroma.Upload, :base_url], Pleroma.Web.base_url()),
+ "/"
+ )
+
# find all objects for copies of the attachments, name and actor doesn't matter here
- delete_ids =
+ object_ids_and_hrefs =
from(o in Object,
where:
fragment(
@@ -67,29 +79,28 @@ defmodule Pleroma.Workers.AttachmentsCleanupWorker do
|> Enum.map(fn {href, %{id: id, count: count}} ->
# only delete files that have single instance
with 1 <- count do
- prefix =
- case Pleroma.Config.get([Pleroma.Upload, :base_url]) do
- nil -> "media"
- _ -> ""
- end
-
- base_url =
- String.trim_trailing(
- Pleroma.Config.get([Pleroma.Upload, :base_url], Pleroma.Web.base_url()),
- "/"
- )
-
- file_path = String.trim_leading(href, "#{base_url}/#{prefix}")
+ href
+ |> String.trim_leading("#{base_url}/#{prefix}")
+ |> uploader.delete_file()
- uploader.delete_file(file_path)
+ {id, href}
+ else
+ _ -> {id, nil}
end
-
- id
end)
- from(o in Object, where: o.id in ^delete_ids)
+ object_ids = Enum.map(object_ids_and_hrefs, fn {id, _} -> id end)
+
+ from(o in Object, where: o.id in ^object_ids)
|> Repo.delete_all()
+
+ object_ids_and_hrefs
+ |> Enum.filter(fn {_, href} -> not is_nil(href) end)
+ |> Enum.map(&elem(&1, 1))
+ |> Pleroma.Web.MediaProxy.Invalidation.purge()
+
+ {:ok, :success}
end
- def perform(%{"op" => "cleanup_attachments", "object" => _object}, _job), do: :ok
+ def perform(%{"op" => "cleanup_attachments", "object" => _object}, _job), do: {:ok, :skip}
end
diff --git a/lib/pleroma/workers/cron/clear_oauth_token_worker.ex b/lib/pleroma/workers/cron/clear_oauth_token_worker.ex
index 341eff054..a4c3b9516 100644
--- a/lib/pleroma/workers/cron/clear_oauth_token_worker.ex
+++ b/lib/pleroma/workers/cron/clear_oauth_token_worker.ex
@@ -16,6 +16,8 @@ defmodule Pleroma.Workers.Cron.ClearOauthTokenWorker do
def perform(_opts, _job) do
if Config.get([:oauth2, :clean_expired_tokens], false) do
Token.delete_expired_tokens()
+ else
+ :ok
end
end
end
diff --git a/lib/pleroma/workers/cron/digest_emails_worker.ex b/lib/pleroma/workers/cron/digest_emails_worker.ex
index dd13c3b17..7f09ff3cf 100644
--- a/lib/pleroma/workers/cron/digest_emails_worker.ex
+++ b/lib/pleroma/workers/cron/digest_emails_worker.ex
@@ -37,6 +37,8 @@ defmodule Pleroma.Workers.Cron.DigestEmailsWorker do
)
|> Repo.all()
|> send_emails
+ else
+ :ok
end
end
diff --git a/lib/pleroma/workers/cron/new_users_digest_worker.ex b/lib/pleroma/workers/cron/new_users_digest_worker.ex
index 9bd0a5621..5c816b3fe 100644
--- a/lib/pleroma/workers/cron/new_users_digest_worker.ex
+++ b/lib/pleroma/workers/cron/new_users_digest_worker.ex
@@ -55,7 +55,11 @@ defmodule Pleroma.Workers.Cron.NewUsersDigestWorker do
|> Repo.all()
|> Enum.map(&Pleroma.Emails.NewUsersDigestEmail.new_users(&1, users_and_statuses))
|> Enum.each(&Pleroma.Emails.Mailer.deliver/1)
+ else
+ :ok
end
+ else
+ :ok
end
end
end
diff --git a/lib/pleroma/workers/cron/purge_expired_activities_worker.ex b/lib/pleroma/workers/cron/purge_expired_activities_worker.ex
index b8953dd7f..84b3b84de 100644
--- a/lib/pleroma/workers/cron/purge_expired_activities_worker.ex
+++ b/lib/pleroma/workers/cron/purge_expired_activities_worker.ex
@@ -23,6 +23,8 @@ defmodule Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker do
def perform(_opts, _job) do
if Config.get([ActivityExpiration, :enabled]) do
Enum.each(ActivityExpiration.due_expirations(@interval), &delete_activity/1)
+ else
+ :ok
end
end
diff --git a/lib/pleroma/workers/scheduled_activity_worker.ex b/lib/pleroma/workers/scheduled_activity_worker.ex
index 8905f4ad0..97d1efbfb 100644
--- a/lib/pleroma/workers/scheduled_activity_worker.ex
+++ b/lib/pleroma/workers/scheduled_activity_worker.ex
@@ -30,6 +30,8 @@ defmodule Pleroma.Workers.ScheduledActivityWorker do
end
defp post_activity(%ScheduledActivity{user_id: user_id, params: params} = scheduled_activity) do
+ params = Map.new(params, fn {key, value} -> {String.to_existing_atom(key), value} end)
+
with {:delete, {:ok, _}} <- {:delete, ScheduledActivity.delete(scheduled_activity)},
{:user, %User{} = user} <- {:user, User.get_cached_by_id(user_id)},
{:post, {:ok, _}} <- {:post, CommonAPI.post(user, params)} do