aboutsummaryrefslogtreecommitdiff
path: root/lib/pleroma/web
diff options
context:
space:
mode:
Diffstat (limited to 'lib/pleroma/web')
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub.ex435
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub_controller.ex121
-rw-r--r--lib/pleroma/web/activity_pub/builder.ex115
-rw-r--r--lib/pleroma/web/activity_pub/internal_fetch_actor.ex2
-rw-r--r--lib/pleroma/web/activity_pub/mrf.ex2
-rw-r--r--lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex2
-rw-r--r--lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex2
-rw-r--r--lib/pleroma/web/activity_pub/mrf/drop_policy.ex2
-rw-r--r--lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex2
-rw-r--r--lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex2
-rw-r--r--lib/pleroma/web/activity_pub/mrf/keyword_policy.ex2
-rw-r--r--lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex16
-rw-r--r--lib/pleroma/web/activity_pub/mrf/mention_policy.ex2
-rw-r--r--lib/pleroma/web/activity_pub/mrf/no_op_policy.ex2
-rw-r--r--lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex2
-rw-r--r--lib/pleroma/web/activity_pub/mrf/normalize_markup.ex2
-rw-r--r--lib/pleroma/web/activity_pub/mrf/object_age_policy.ex12
-rw-r--r--lib/pleroma/web/activity_pub/mrf/reject_non_public.ex2
-rw-r--r--lib/pleroma/web/activity_pub/mrf/simple_policy.ex17
-rw-r--r--lib/pleroma/web/activity_pub/mrf/subchain_policy.ex2
-rw-r--r--lib/pleroma/web/activity_pub/mrf/tag_policy.ex2
-rw-r--r--lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex2
-rw-r--r--lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex2
-rw-r--r--lib/pleroma/web/activity_pub/object_validator.ex83
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/common_validations.ex80
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/create_validator.ex30
-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.ex99
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/note_validator.ex64
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/types/date_time.ex34
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/types/object_id.ex23
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/types/recipients.ex34
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/types/uri.ex20
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/undo_validator.ex62
-rw-r--r--lib/pleroma/web/activity_pub/pipeline.ex59
-rw-r--r--lib/pleroma/web/activity_pub/publisher.ex15
-rw-r--r--lib/pleroma/web/activity_pub/relay.ex21
-rw-r--r--lib/pleroma/web/activity_pub/side_effects.ex133
-rw-r--r--lib/pleroma/web/activity_pub/transmogrifier.ex369
-rw-r--r--lib/pleroma/web/activity_pub/utils.ex207
-rw-r--r--lib/pleroma/web/activity_pub/views/object_view.ex2
-rw-r--r--lib/pleroma/web/activity_pub/views/user_view.ex16
-rw-r--r--lib/pleroma/web/activity_pub/visibility.ex16
-rw-r--r--lib/pleroma/web/admin_api/admin_api_controller.ex361
-rw-r--r--lib/pleroma/web/admin_api/report.ex2
-rw-r--r--lib/pleroma/web/admin_api/search.ex9
-rw-r--r--lib/pleroma/web/admin_api/views/account_view.ex46
-rw-r--r--lib/pleroma/web/admin_api/views/config_view.ex2
-rw-r--r--lib/pleroma/web/admin_api/views/moderation_log_view.ex2
-rw-r--r--lib/pleroma/web/admin_api/views/report_view.ex37
-rw-r--r--lib/pleroma/web/admin_api/views/status_view.ex22
-rw-r--r--lib/pleroma/web/api_spec.ex57
-rw-r--r--lib/pleroma/web/api_spec/cast_and_validate.ex139
-rw-r--r--lib/pleroma/web/api_spec/helpers.ex61
-rw-r--r--lib/pleroma/web/api_spec/operations/account_operation.ex688
-rw-r--r--lib/pleroma/web/api_spec/operations/app_operation.ex144
-rw-r--r--lib/pleroma/web/api_spec/operations/conversation_operation.ex61
-rw-r--r--lib/pleroma/web/api_spec/operations/custom_emoji_operation.ex88
-rw-r--r--lib/pleroma/web/api_spec/operations/domain_block_operation.ex83
-rw-r--r--lib/pleroma/web/api_spec/operations/filter_operation.ex227
-rw-r--r--lib/pleroma/web/api_spec/operations/follow_request_operation.ex65
-rw-r--r--lib/pleroma/web/api_spec/operations/instance_operation.ex169
-rw-r--r--lib/pleroma/web/api_spec/operations/list_operation.ex188
-rw-r--r--lib/pleroma/web/api_spec/operations/marker_operation.ex140
-rw-r--r--lib/pleroma/web/api_spec/operations/notification_operation.ex211
-rw-r--r--lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex185
-rw-r--r--lib/pleroma/web/api_spec/operations/poll_operation.ex76
-rw-r--r--lib/pleroma/web/api_spec/operations/report_operation.ex78
-rw-r--r--lib/pleroma/web/api_spec/operations/scheduled_activity_operation.ex96
-rw-r--r--lib/pleroma/web/api_spec/operations/search_operation.ex207
-rw-r--r--lib/pleroma/web/api_spec/operations/status_operation.ex499
-rw-r--r--lib/pleroma/web/api_spec/operations/subscription_operation.ex188
-rw-r--r--lib/pleroma/web/api_spec/operations/timeline_operation.ex199
-rw-r--r--lib/pleroma/web/api_spec/render_error.ex234
-rw-r--r--lib/pleroma/web/api_spec/schemas/account.ex167
-rw-r--r--lib/pleroma/web/api_spec/schemas/account_field.ex26
-rw-r--r--lib/pleroma/web/api_spec/schemas/account_relationship.ex44
-rw-r--r--lib/pleroma/web/api_spec/schemas/actor_type.ex13
-rw-r--r--lib/pleroma/web/api_spec/schemas/api_error.ex19
-rw-r--r--lib/pleroma/web/api_spec/schemas/attachment.ex68
-rw-r--r--lib/pleroma/web/api_spec/schemas/boolean_like.ex36
-rw-r--r--lib/pleroma/web/api_spec/schemas/conversation.ex41
-rw-r--r--lib/pleroma/web/api_spec/schemas/emoji.ex29
-rw-r--r--lib/pleroma/web/api_spec/schemas/flake_id.ex14
-rw-r--r--lib/pleroma/web/api_spec/schemas/list.ex23
-rw-r--r--lib/pleroma/web/api_spec/schemas/poll.ex82
-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.ex325
-rw-r--r--lib/pleroma/web/api_spec/schemas/tag.ex27
-rw-r--r--lib/pleroma/web/api_spec/schemas/visibility_scope.ex14
-rw-r--r--lib/pleroma/web/auth/authenticator.ex2
-rw-r--r--lib/pleroma/web/auth/ldap_authenticator.ex2
-rw-r--r--lib/pleroma/web/auth/pleroma_authenticator.ex6
-rw-r--r--lib/pleroma/web/auth/totp_authenticator.ex44
-rw-r--r--lib/pleroma/web/channels/user_socket.ex2
-rw-r--r--lib/pleroma/web/chat_channel.ex2
-rw-r--r--lib/pleroma/web/common_api/activity_draft.ex34
-rw-r--r--lib/pleroma/web/common_api/common_api.ex216
-rw-r--r--lib/pleroma/web/common_api/utils.ex63
-rw-r--r--lib/pleroma/web/controller_helper.ex45
-rw-r--r--lib/pleroma/web/endpoint.ex9
-rw-r--r--lib/pleroma/web/fallback_redirect_controller.ex4
-rw-r--r--lib/pleroma/web/federator/federator.ex31
-rw-r--r--lib/pleroma/web/federator/publisher.ex2
-rw-r--r--lib/pleroma/web/feed/feed_view.ex11
-rw-r--r--lib/pleroma/web/feed/tag_controller.ex8
-rw-r--r--lib/pleroma/web/feed/user_controller.ex26
-rw-r--r--lib/pleroma/web/gettext.ex2
-rw-r--r--lib/pleroma/web/masto_fe_controller.ex12
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/account_controller.ex235
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/app_controller.ex19
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/auth_controller.ex8
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex9
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex12
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex11
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/filter_controller.ex59
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex7
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/instance_controller.ex12
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/list_controller.ex32
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/marker_controller.ex14
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex22
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/media_controller.ex4
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/notification_controller.ex35
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/poll_controller.ex10
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/report_controller.ex7
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex16
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/search_controller.ex46
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/status_controller.ex125
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex46
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex26
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex109
-rw-r--r--lib/pleroma/web/mastodon_api/mastodon_api.ex25
-rw-r--r--lib/pleroma/web/mastodon_api/views/account_view.ex210
-rw-r--r--lib/pleroma/web/mastodon_api/views/app_view.ex17
-rw-r--r--lib/pleroma/web/mastodon_api/views/conversation_view.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/views/custom_emoji_view.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/views/filter_view.ex8
-rw-r--r--lib/pleroma/web/mastodon_api/views/instance_view.ex60
-rw-r--r--lib/pleroma/web/mastodon_api/views/list_view.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/views/marker_view.ex18
-rw-r--r--lib/pleroma/web/mastodon_api/views/notification_view.ex121
-rw-r--r--lib/pleroma/web/mastodon_api/views/poll_view.ex9
-rw-r--r--lib/pleroma/web/mastodon_api/views/report_view.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/views/status_view.ex126
-rw-r--r--lib/pleroma/web/mastodon_api/views/subscription_view.ex (renamed from lib/pleroma/web/mastodon_api/views/push_subscription_view.ex)6
-rw-r--r--lib/pleroma/web/mastodon_api/websocket_handler.ex138
-rw-r--r--lib/pleroma/web/media_proxy/media_proxy.ex2
-rw-r--r--lib/pleroma/web/media_proxy/media_proxy_controller.ex3
-rw-r--r--lib/pleroma/web/metadata.ex9
-rw-r--r--lib/pleroma/web/metadata/feed.ex2
-rw-r--r--lib/pleroma/web/metadata/opengraph.ex4
-rw-r--r--lib/pleroma/web/metadata/player_view.ex2
-rw-r--r--lib/pleroma/web/metadata/provider.ex2
-rw-r--r--lib/pleroma/web/metadata/rel_me.ex2
-rw-r--r--lib/pleroma/web/metadata/restrict_indexing.ex25
-rw-r--r--lib/pleroma/web/metadata/twitter_card.ex2
-rw-r--r--lib/pleroma/web/metadata/utils.ex2
-rw-r--r--lib/pleroma/web/mongooseim/mongoose_im_controller.ex9
-rw-r--r--lib/pleroma/web/nodeinfo/nodeinfo_controller.ex51
-rw-r--r--lib/pleroma/web/oauth.ex2
-rw-r--r--lib/pleroma/web/oauth/app.ex84
-rw-r--r--lib/pleroma/web/oauth/authorization.ex2
-rw-r--r--lib/pleroma/web/oauth/fallback_controller.ex2
-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.ex53
-rw-r--r--lib/pleroma/web/oauth/oauth_view.ex2
-rw-r--r--lib/pleroma/web/oauth/scopes.ex7
-rw-r--r--lib/pleroma/web/oauth/token.ex2
-rw-r--r--lib/pleroma/web/oauth/token/clean_worker.ex38
-rw-r--r--lib/pleroma/web/oauth/token/query.ex2
-rw-r--r--lib/pleroma/web/oauth/token/response.ex11
-rw-r--r--lib/pleroma/web/oauth/token/strategy/refresh_token.ex2
-rw-r--r--lib/pleroma/web/oauth/token/strategy/revoke.ex2
-rw-r--r--lib/pleroma/web/oauth/token/utils.ex2
-rw-r--r--lib/pleroma/web/ostatus/ostatus_controller.ex12
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/account_controller.ex66
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex697
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex4
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex111
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex10
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex133
-rw-r--r--lib/pleroma/web/push/impl.ex89
-rw-r--r--lib/pleroma/web/push/push.ex2
-rw-r--r--lib/pleroma/web/push/subscription.ex20
-rw-r--r--lib/pleroma/web/rel_me.ex20
-rw-r--r--lib/pleroma/web/rich_media/helpers.ex7
-rw-r--r--lib/pleroma/web/rich_media/parser.ex20
-rw-r--r--lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex2
-rw-r--r--lib/pleroma/web/rich_media/parsers/oembed_parser.ex2
-rw-r--r--lib/pleroma/web/rich_media/parsers/ogp.ex2
-rw-r--r--lib/pleroma/web/rich_media/parsers/twitter_card.ex2
-rw-r--r--lib/pleroma/web/router.ex375
-rw-r--r--lib/pleroma/web/static_fe/static_fe_controller.ex37
-rw-r--r--lib/pleroma/web/static_fe/static_fe_view.ex11
-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.ex293
-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.ex226
-rw-r--r--lib/pleroma/web/templates/feed/feed/_activity.atom.eex (renamed from lib/pleroma/web/templates/feed/feed/_activity.xml.eex)8
-rw-r--r--lib/pleroma/web/templates/feed/feed/_activity.rss.eex49
-rw-r--r--lib/pleroma/web/templates/feed/feed/_author.atom.eex (renamed from lib/pleroma/web/templates/feed/feed/_author.xml.eex)0
-rw-r--r--lib/pleroma/web/templates/feed/feed/_author.rss.eex17
-rw-r--r--lib/pleroma/web/templates/feed/feed/_tag_activity.atom.eex16
-rw-r--r--lib/pleroma/web/templates/feed/feed/_tag_activity.xml.eex15
-rw-r--r--lib/pleroma/web/templates/feed/feed/user.atom.eex (renamed from lib/pleroma/web/templates/feed/feed/user.xml.eex)4
-rw-r--r--lib/pleroma/web/templates/feed/feed/user.rss.eex20
-rw-r--r--lib/pleroma/web/templates/layout/static_fe.html.eex2
-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/static_fe/static_fe/_attachment.html.eex6
-rw-r--r--lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex10
-rw-r--r--lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex6
-rw-r--r--lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex4
-rw-r--r--lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex13
-rw-r--r--lib/pleroma/web/translation_helpers.ex2
-rw-r--r--lib/pleroma/web/twitter_api/controllers/password_controller.ex2
-rw-r--r--lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex49
-rw-r--r--lib/pleroma/web/twitter_api/controllers/util_controller.ex37
-rw-r--r--lib/pleroma/web/twitter_api/twitter_api.ex153
-rw-r--r--lib/pleroma/web/twitter_api/twitter_api_controller.ex26
-rw-r--r--lib/pleroma/web/twitter_api/views/password_view.ex2
-rw-r--r--lib/pleroma/web/twitter_api/views/remote_follow_view.ex2
-rw-r--r--lib/pleroma/web/twitter_api/views/token_view.ex2
-rw-r--r--lib/pleroma/web/twitter_api/views/util_view.ex2
-rw-r--r--lib/pleroma/web/uploader_controller.ex2
-rw-r--r--lib/pleroma/web/views/error_helpers.ex2
-rw-r--r--lib/pleroma/web/views/error_view.ex2
-rw-r--r--lib/pleroma/web/views/layout_view.ex2
-rw-r--r--lib/pleroma/web/views/masto_fe_view.ex2
-rw-r--r--lib/pleroma/web/views/streamer_view.ex4
-rw-r--r--lib/pleroma/web/web.ex132
-rw-r--r--lib/pleroma/web/web_finger/web_finger.ex74
-rw-r--r--lib/pleroma/web/web_finger/web_finger_controller.ex2
-rw-r--r--lib/pleroma/web/xml/xml.ex2
241 files changed, 10439 insertions, 3088 deletions
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 5c436941a..d752f4f04 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -1,11 +1,12 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.Activity
alias Pleroma.Activity.Ir.Topics
alias Pleroma.Config
+ alias Pleroma.Constants
alias Pleroma.Conversation
alias Pleroma.Conversation.Participation
alias Pleroma.Notification
@@ -117,13 +118,30 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
def increase_poll_votes_if_vote(%{
"object" => %{"inReplyTo" => reply_ap_id, "name" => name},
- "type" => "Create"
+ "type" => "Create",
+ "actor" => actor
}) do
- Object.increase_vote_count(reply_ap_id, name)
+ Object.increase_vote_count(reply_ap_id, name, actor)
end
def increase_poll_votes_if_vote(_create_data), do: :noop
+ @spec persist(map(), keyword()) :: {:ok, Activity.t() | Object.t()}
+ def persist(object, meta) do
+ with local <- Keyword.fetch!(meta, :local),
+ {recipients, _, _} <- get_recipients(object),
+ {:ok, activity} <-
+ Repo.insert(%Activity{
+ data: object,
+ local: local,
+ recipients: recipients,
+ actor: object["actor"]
+ }) do
+ {:ok, activity, meta}
+ end
+ end
+
+ @spec insert(map(), boolean(), boolean(), boolean()) :: {:ok, Activity.t()} | {:error, any()}
def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when is_map(map) do
with nil <- Activity.normalize(map),
map <- lazy_put_activity_defaults(map, fake),
@@ -152,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 ->
@@ -180,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),
@@ -231,12 +252,19 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
:noop
end
- def create(%{to: to, actor: actor, context: context, object: object} = params, fake \\ false) do
+ @spec create(map(), boolean()) :: {:ok, Activity.t()} | {:error, any()}
+ def create(params, fake \\ false) do
+ with {:ok, result} <- Repo.transaction(fn -> do_create(params, fake) end) do
+ result
+ end
+ end
+
+ defp do_create(%{to: to, actor: actor, context: context, object: object} = params, fake) do
additional = params[:additional] || %{}
# only accept false as false value
local = !(params[:local] == false)
published = params[:published]
- quick_insert? = Pleroma.Config.get([:env]) == :benchmark
+ quick_insert? = Config.get([:env]) == :benchmark
with create_data <-
make_create_data(
@@ -249,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
@@ -259,10 +288,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
{:ok, activity}
{:error, message} ->
- {:error, message}
+ Repo.rollback(message)
end
end
+ @spec listen(map()) :: {:ok, Activity.t()} | {:error, any()}
def listen(%{to: to, actor: actor, context: context, object: object} = params) do
additional = params[:additional] || %{}
# only accept false as false value
@@ -275,22 +305,23 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
additional
),
{:ok, activity} <- insert(listen_data, local),
+ _ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
- else
- {:error, message} ->
- {:error, message}
end
end
+ @spec accept(map()) :: {:ok, Activity.t()} | {:error, any()}
def accept(params) do
accept_or_reject("Accept", params)
end
+ @spec reject(map()) :: {:ok, Activity.t()} | {:error, any()}
def reject(params) do
accept_or_reject("Reject", params)
end
+ @spec accept_or_reject(String.t(), map()) :: {:ok, Activity.t()} | {:error, any()}
def accept_or_reject(type, %{to: to, actor: actor, object: object} = params) do
local = Map.get(params, :local, true)
activity_id = Map.get(params, :activity_id, nil)
@@ -299,11 +330,13 @@ 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
end
+ @spec update(map()) :: {:ok, Activity.t()} | {:error, any()}
def update(%{to: to, cc: cc, actor: actor, object: object} = params) do
local = !(params[:local] == false)
activity_id = params[:activity_id]
@@ -317,73 +350,14 @@ 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
- def 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
- e -> {:error, e}
- end
- end
-
- def 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
- e -> {:error, e}
- end
- end
-
- # TODO: This is weird, maybe we shouldn't check here if we can make the activity.
- def like(
- %User{ap_id: ap_id} = user,
- %Object{data: %{"id" => _}} = object,
- activity_id \\ nil,
- local \\ true
- ) do
- with nil <- get_existing_like(ap_id, object),
- like_data <- make_like_data(user, object, activity_id),
- {:ok, activity} <- insert(like_data, local),
- {:ok, object} <- add_like_to_object(activity, object),
- :ok <- maybe_federate(activity) do
- {:ok, activity, object}
- else
- %Activity{} = activity -> {:ok, activity, object}
- error -> {:error, error}
- end
- end
-
- def unlike(%User{} = actor, %Object{} = object, activity_id \\ nil, local \\ true) 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
- _e -> {:ok, object}
- 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,
@@ -391,98 +365,80 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
local \\ true,
public \\ true
) do
+ with {:ok, result} <-
+ Repo.transaction(fn -> do_announce(user, object, activity_id, local, public) end) do
+ result
+ end
+ end
+
+ defp do_announce(user, object, activity_id, local, public) do
with true <- is_announceable?(object, user, public),
+ object <- Object.get_by_id(object.id),
announce_data <- make_announce_data(user, object, activity_id, public),
{:ok, activity} <- insert(announce_data, local),
{:ok, object} <- add_announce_to_object(activity, object),
+ _ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
else
- error -> {:error, error}
+ false -> {:error, false}
+ {:error, error} -> Repo.rollback(error)
end
end
- def unannounce(
- %User{} = actor,
- %Object{} = object,
- activity_id \\ nil,
- local \\ true
- ) 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
- _e -> {:ok, object}
+ @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
+ with {:ok, result} <-
+ Repo.transaction(fn -> do_follow(follower, followed, activity_id, local) end) do
+ result
end
end
- def follow(follower, followed, activity_id \\ nil, local \\ true) do
+ defp do_follow(follower, followed, activity_id, local) do
with data <- make_follow_data(follower, followed, activity_id),
{:ok, activity} <- insert(data, local),
- :ok <- maybe_federate(activity),
- _ <- User.set_follow_state_cache(follower.ap_id, followed.ap_id, activity.data["state"]) do
+ _ <- notify_and_stream(activity),
+ :ok <- maybe_federate(activity) do
{:ok, activity}
+ else
+ {:error, error} -> Repo.rollback(error)
end
end
+ @spec unfollow(User.t(), User.t(), String.t() | nil, boolean()) ::
+ {:ok, Activity.t()} | nil | {:error, any()}
def unfollow(follower, followed, activity_id \\ nil, local \\ true) do
+ with {:ok, result} <-
+ Repo.transaction(fn -> do_unfollow(follower, followed, activity_id, local) end) do
+ result
+ end
+ end
+
+ defp do_unfollow(follower, followed, activity_id, local) do
with %Activity{} = follow_activity <- fetch_latest_follow(follower, followed),
{: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
+ nil -> nil
+ {:error, error} -> Repo.rollback(error)
end
end
- def 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
-
- def 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}
+ @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
+ with {:ok, result} <-
+ Repo.transaction(fn -> do_block(blocker, blocked, activity_id, local) end) do
+ result
end
end
- @spec block(User.t(), User.t(), String.t() | nil, boolean) :: {:ok, Activity.t() | nil}
- def block(blocker, blocked, activity_id \\ nil, local \\ true) do
- outgoing_blocks = Config.get([:activitypub, :outgoing_blocks])
+ defp do_block(blocker, blocked, activity_id, local) do
unfollow_blocked = Config.get([:activitypub, :unfollow_blocked])
if unfollow_blocked do
@@ -490,26 +446,17 @@ 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
- _e -> {:ok, nil}
+ {:error, error} -> Repo.rollback(error)
end
end
- def unblock(blocker, blocked, activity_id \\ nil, local \\ true) 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}
- end
- end
-
- @spec flag(map()) :: {:ok, Activity.t()} | any
+ @spec flag(map()) :: {:ok, Activity.t()} | {:error, any()}
def flag(
%{
actor: actor,
@@ -535,8 +482,11 @@ 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
- Enum.each(User.all_superusers(), fn superuser ->
+ User.all_superusers()
+ |> Enum.filter(fn user -> not is_nil(user.email) end)
+ |> Enum.each(fn superuser ->
superuser
|> Pleroma.Emails.AdminEmail.report(actor, account, statuses, content)
|> Pleroma.Emails.Mailer.deliver_async()
@@ -546,6 +496,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
+ @spec move(User.t(), User.t(), boolean()) :: {:ok, Activity.t()} | {:error, any()}
def move(%User{} = origin, %User{} = target, local \\ true) do
params = %{
"type" => "Move",
@@ -555,7 +506,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", %{
@@ -570,8 +522,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
- defp fetch_activities_for_context_query(context, opts) do
- public = [Pleroma.Constants.as_public()]
+ def fetch_activities_for_context_query(context, opts) do
+ public = [Constants.as_public()]
recipients =
if opts["user"],
@@ -616,10 +568,11 @@ 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
opts = Map.drop(opts, ["user"])
- [Pleroma.Constants.as_public()]
+ [Constants.as_public()]
|> fetch_activities_query(opts)
|> restrict_unlisted()
|> Pagination.fetch_paginated(opts, pagination)
@@ -701,7 +654,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
defp exclude_visibility(query, %{"exclude_visibilities" => visibility})
- when visibility not in @valid_visibilities do
+ when visibility not in [nil | @valid_visibilities] do
Logger.error("Could not exclude visibility to #{visibility}")
query
end
@@ -770,13 +723,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> Enum.reverse()
end
- def fetch_instance_activities(params) do
+ def fetch_statuses(reading_user, params) do
params =
params
|> Map.put("type", ["Create", "Announce"])
- |> Map.put("instance", params["instance"])
- fetch_activities([Pleroma.Constants.as_public()], params, :offset)
+ recipients =
+ user_activities_recipients(%{
+ "godmode" => params["godmode"],
+ "reading_user" => reading_user
+ })
+
+ fetch_activities(recipients, params, :offset)
|> Enum.reverse()
end
@@ -786,9 +744,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp user_activities_recipients(%{"reading_user" => reading_user}) do
if reading_user do
- [Pleroma.Constants.as_public()] ++ [reading_user.ap_id | User.following(reading_user)]
+ [Constants.as_public()] ++ [reading_user.ap_id | User.following(reading_user)]
else
- [Pleroma.Constants.as_public()]
+ [Constants.as_public()]
end
end
@@ -903,7 +861,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
raise "Can't use the child object without preloading!"
end
- defp restrict_media(query, %{"only_media" => val}) when val == "true" or val == "1" do
+ defp restrict_media(query, %{"only_media" => val}) when val in [true, "true", "1"] do
from(
[_activity, object] in query,
where: fragment("not (?)->'attachment' = (?)", object.data, ^[])
@@ -912,16 +870,51 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_media(query, _), do: query
- defp restrict_replies(query, %{"exclude_replies" => val}) when val == "true" or val == "1" do
+ defp restrict_replies(query, %{"exclude_replies" => val}) when val in [true, "true", "1"] do
from(
[_activity, object] in query,
where: fragment("?->>'inReplyTo' is null", object.data)
)
end
+ defp restrict_replies(query, %{
+ "reply_filtering_user" => user,
+ "reply_visibility" => "self"
+ }) do
+ from(
+ [activity, object] in query,
+ where:
+ fragment(
+ "?->>'inReplyTo' is null OR ? = ANY(?)",
+ object.data,
+ ^user.ap_id,
+ activity.recipients
+ )
+ )
+ end
+
+ defp restrict_replies(query, %{
+ "reply_filtering_user" => user,
+ "reply_visibility" => "following"
+ }) do
+ from(
+ [activity, object] in query,
+ where:
+ fragment(
+ "?->>'inReplyTo' is null OR ? && array_remove(?, ?) OR ? = ?",
+ object.data,
+ ^[user.ap_id | User.get_cached_user_friends_ap_ids(user)],
+ activity.recipients,
+ activity.actor,
+ activity.actor,
+ ^user.ap_id
+ )
+ )
+ end
+
defp restrict_replies(query, _), do: query
- defp restrict_reblogs(query, %{"exclude_reblogs" => val}) when val == "true" or val == "1" do
+ defp restrict_reblogs(query, %{"exclude_reblogs" => val}) when val in [true, "true", "1"] do
from(activity in query, where: fragment("?->>'type' != 'Announce'", activity.data))
end
@@ -995,12 +988,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
fragment(
"not (coalesce(?->'cc', '{}'::jsonb) \\?| ?)",
activity.data,
- ^[Pleroma.Constants.as_public()]
+ ^[Constants.as_public()]
)
)
end
- defp restrict_pinned(query, %{"pinned" => "true", "pinned_activity_ids" => ids}) do
+ # TODO: when all endpoints migrated to OpenAPI compare `pinned` with `true` (boolean) only,
+ # the same for `restrict_media/2`, `restrict_replies/2`, 'restrict_reblogs/2'
+ # and `restrict_muted/2`
+
+ defp restrict_pinned(query, %{"pinned" => pinned, "pinned_activity_ids" => ids})
+ when pinned in [true, "true", "1"] do
from(activity in query, where: activity.id in ^ids)
end
@@ -1097,17 +1095,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp fetch_activities_query_ap_ids_ops(opts) do
source_user = opts["muting_user"]
- ap_id_relations = if source_user, do: [:mute, :reblog_mute], else: []
+ ap_id_relationships = if source_user, do: [:mute, :reblog_mute], else: []
- ap_id_relations =
- ap_id_relations ++
+ ap_id_relationships =
+ ap_id_relationships ++
if opts["blocking_user"] && opts["blocking_user"] == source_user do
[:block]
else
[]
end
- preloaded_ap_ids = User.outgoing_relations_ap_ids(source_user, ap_id_relations)
+ preloaded_ap_ids = User.outgoing_relationships_ap_ids(source_user, ap_id_relationships)
restrict_blocked_opts = Map.merge(%{"blocked_users_ap_ids" => preloaded_ap_ids[:block]}, opts)
restrict_muted_opts = Map.merge(%{"muted_users_ap_ids" => preloaded_ap_ids[:mute]}, opts)
@@ -1133,6 +1131,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> maybe_set_thread_muted_field(opts)
|> maybe_order(opts)
|> restrict_recipients(recipients, opts["user"])
+ |> restrict_replies(opts)
|> restrict_tag(opts)
|> restrict_tag_reject(opts)
|> restrict_tag_all(opts)
@@ -1147,7 +1146,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> restrict_media(opts)
|> restrict_visibility(opts)
|> restrict_thread_visibility(opts, config)
- |> restrict_replies(opts)
|> restrict_reblogs(opts)
|> restrict_pinned(opts)
|> restrict_muted_reblogs(restrict_muted_reblogs_opts)
@@ -1169,7 +1167,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
@doc """
Fetch favorites activities of user with order by sort adds to favorites
"""
- @spec fetch_favourites(User.t(), map(), atom()) :: list(Activity.t())
+ @spec fetch_favourites(User.t(), map(), Pagination.type()) :: list(Activity.t())
def fetch_favourites(user, params \\ %{}, pagination \\ :keyset) do
user.ap_id
|> Activity.Queries.by_actor()
@@ -1207,7 +1205,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
where:
fragment("? && ?", activity.recipients, ^recipients) or
(fragment("? && ?", activity.recipients, ^recipients_with_public) and
- ^Pleroma.Constants.as_public() in activity.recipients)
+ ^Constants.as_public() in activity.recipients)
)
end
@@ -1223,6 +1221,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> Enum.reverse()
end
+ @spec upload(Upload.source(), keyword()) :: {:ok, Object.t()} | {:error, any()}
def upload(file, opts \\ []) do
with {:ok, data} <- Upload.store(file, opts) do
obj_data =
@@ -1236,6 +1235,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
+ @spec get_actor_url(any()) :: binary() | nil
+ defp get_actor_url(url) when is_binary(url), do: url
+ defp get_actor_url(%{"href" => href}) when is_binary(href), do: href
+
+ defp get_actor_url(url) when is_list(url) do
+ url
+ |> List.first()
+ |> get_actor_url()
+ end
+
+ defp get_actor_url(_url), do: nil
+
defp object_to_user_data(data) do
avatar =
data["icon"]["url"] &&
@@ -1257,18 +1268,44 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
|> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
+ emojis =
+ data
+ |> Map.get("tag", [])
+ |> Enum.filter(fn
+ %{"type" => "Emoji"} -> true
+ _ -> false
+ end)
+ |> Enum.reduce(%{}, fn %{"icon" => %{"url" => url}, "name" => name}, acc ->
+ Map.put(acc, String.trim(name, ":"), url)
+ end)
+
locked = data["manuallyApprovesFollowers"] || false
data = Transmogrifier.maybe_fix_user_object(data)
discoverable = data["discoverable"] || false
invisible = data["invisible"] || false
actor_type = data["type"] || "Person"
+ public_key =
+ if is_map(data["publicKey"]) && is_binary(data["publicKey"]["publicKeyPem"]) do
+ data["publicKey"]["publicKeyPem"]
+ else
+ nil
+ end
+
+ shared_inbox =
+ if is_map(data["endpoints"]) && is_binary(data["endpoints"]["sharedInbox"]) do
+ data["endpoints"]["sharedInbox"]
+ else
+ nil
+ end
+
user_data = %{
ap_id: data["id"],
+ uri: get_actor_url(data["url"]),
ap_enabled: true,
- source_data: data,
banner: banner,
fields: fields,
+ emoji: emojis,
locked: locked,
discoverable: discoverable,
invisible: invisible,
@@ -1278,7 +1315,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
following_address: data["following"],
bio: data["summary"],
actor_type: actor_type,
- also_known_as: Map.get(data, "alsoKnownAs", [])
+ also_known_as: Map.get(data, "alsoKnownAs", []),
+ public_key: public_key,
+ inbox: data["inbox"],
+ shared_inbox: shared_inbox
}
# nickname can be nil because of virtual actors
@@ -1319,22 +1359,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
- with {:enabled, true} <-
- {:enabled, Pleroma.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)
+ def maybe_update_follow_information(user_data) do
+ with {:enabled, true} <- {:enabled, Config.get([:instance, :external_user_synchronization])},
+ {_, 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
@@ -1381,11 +1433,22 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
def make_user_from_ap_id(ap_id) do
- if _user = User.get_cached_by_ap_id(ap_id) do
+ user = User.get_cached_by_ap_id(ap_id)
+
+ if user && !User.ap_enabled?(user) do
Transmogrifier.upgrade_user_from_ap_id(ap_id)
else
with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id) do
- User.insert_or_update_user(data)
+ if user do
+ user
+ |> User.remote_user_changeset(data)
+ |> User.update_and_set_cache()
+ else
+ data
+ |> User.remote_user_changeset()
+ |> Repo.insert()
+ |> User.set_cache()
+ end
else
e -> {:error, e}
end
diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
index 5059e3984..62ad15d85 100644
--- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ActivityPubController do
@@ -9,32 +9,50 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
alias Pleroma.Delivery
alias Pleroma.Object
alias Pleroma.Object.Fetcher
+ alias Pleroma.Plugs.EnsureAuthenticatedPlug
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.InternalFetchActor
alias Pleroma.Web.ActivityPub.ObjectView
+ alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.UserView
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
+ alias Pleroma.Web.FederatingPlug
alias Pleroma.Web.Federator
require Logger
action_fallback(:errors)
+ @federating_only_actions [:internal_fetch, :relay, :relay_following, :relay_followers]
+
+ plug(FederatingPlug when action in @federating_only_actions)
+
+ plug(
+ EnsureAuthenticatedPlug,
+ [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)
+ plug(
+ EnsureAuthenticatedPlug
+ when action in [:read_inbox, :update_outbox, :whoami, :upload_media]
+ )
+
plug(
Pleroma.Plugs.Cache,
[query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2]
when action in [:activity, :object]
)
- plug(Pleroma.Web.FederatingPlug when action in [:inbox, :relay])
plug(:set_requester_reachable when action in [:inbox])
plug(:relay_active? when action in [:relay])
- def relay_active?(conn, _) do
+ defp relay_active?(conn, _) do
if Pleroma.Config.get([:instance, :allow_relay]) do
conn
else
@@ -127,11 +145,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
end
# GET /relay/following
- def following(%{assigns: %{relay: true}} = conn, _params) do
- conn
- |> put_resp_content_type("application/activity+json")
- |> put_view(UserView)
- |> render("following.json", %{user: Relay.get_actor()})
+ def relay_following(conn, _params) do
+ with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
+ conn
+ |> put_resp_content_type("application/activity+json")
+ |> put_view(UserView)
+ |> render("following.json", %{user: Relay.get_actor()})
+ end
end
def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
@@ -164,11 +184,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
end
# GET /relay/followers
- def followers(%{assigns: %{relay: true}} = conn, _params) do
- conn
- |> put_resp_content_type("application/activity+json")
- |> put_view(UserView)
- |> render("followers.json", %{user: Relay.get_actor()})
+ def relay_followers(conn, _params) do
+ with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
+ conn
+ |> put_resp_content_type("application/activity+json")
+ |> put_view(UserView)
+ |> render("followers.json", %{user: Relay.get_actor()})
+ end
end
def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
@@ -200,13 +222,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
end
end
- def outbox(conn, %{"nickname" => nickname, "page" => page?} = params)
+ def outbox(
+ %{assigns: %{user: for_user}} = conn,
+ %{"nickname" => nickname, "page" => page?} = params
+ )
when page? in [true, "true"] do
with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- User.ensure_keys_present(user) do
activities =
if params["max_id"] do
- ActivityPub.fetch_user_activities(user, nil, %{
+ ActivityPub.fetch_user_activities(user, for_user, %{
"max_id" => params["max_id"],
# This is a hack because postgres generates inefficient queries when filtering by
# 'Answer', poll votes will be hidden by the visibility filter in this case anyway
@@ -214,7 +239,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
"limit" => 10
})
else
- ActivityPub.fetch_user_activities(user, nil, %{
+ ActivityPub.fetch_user_activities(user, for_user, %{
"limit" => 10,
"include_poll_votes" => true
})
@@ -255,8 +280,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
json(conn, "ok")
end
- # only accept relayed Creates
- def inbox(conn, %{"type" => "Create"} = params) do
+ # POST /relay/inbox -or- POST /internal/fetch/inbox
+ def inbox(conn, params) do
+ if params["type"] == "Create" && FederatingPlug.federating?() do
+ post_inbox_relayed_create(conn, params)
+ else
+ post_inbox_fallback(conn, params)
+ end
+ end
+
+ defp post_inbox_relayed_create(conn, params) do
Logger.debug(
"Signature missing or not from author, relayed Create message, fetching object from source"
)
@@ -266,10 +299,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
json(conn, "ok")
end
- def inbox(conn, params) do
+ defp post_inbox_fallback(conn, params) do
headers = Enum.into(conn.req_headers, %{})
- if String.contains?(headers["signature"], params["actor"]) do
+ if headers["signature"] && params["actor"] &&
+ String.contains?(headers["signature"], params["actor"]) do
Logger.debug(
"Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!"
)
@@ -277,7 +311,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
Logger.debug(inspect(conn.req_headers))
end
- json(conn, dgettext("errors", "error"))
+ conn
+ |> put_status(:bad_request)
+ |> json(dgettext("errors", "error"))
end
defp represent_service_actor(%User{} = user, conn) do
@@ -311,10 +347,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|> render("user.json", %{user: user})
end
- def whoami(_conn, _params), do: {:error, :not_found}
-
def read_inbox(
- %{assigns: %{user: %{nickname: nickname} = user}} = conn,
+ %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
%{"nickname" => nickname, "page" => page?} = params
)
when page? in [true, "true"] do
@@ -337,7 +371,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
})
end
- def read_inbox(%{assigns: %{user: %{nickname: nickname} = user}} = conn, %{
+ def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{
"nickname" => nickname
}) do
with {:ok, user} <- User.ensure_keys_present(user) do
@@ -348,15 +382,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
end
end
- def read_inbox(%{assigns: %{user: nil}} = conn, %{"nickname" => nickname}) do
- err = dgettext("errors", "can't read inbox of %{nickname}", nickname: nickname)
-
- conn
- |> put_status(:forbidden)
- |> json(err)
- end
-
- def read_inbox(%{assigns: %{user: %{nickname: as_nickname}}} = conn, %{
+ def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{
"nickname" => nickname
}) do
err =
@@ -370,7 +396,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|> json(err)
end
- def handle_user_activity(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"]))
@@ -386,26 +415,30 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
})
end
- def handle_user_activity(user, %{"type" => "Delete"} = params) 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")}
end
end
- def handle_user_activity(user, %{"type" => "Like"} = params) do
+ defp handle_user_activity(%User{} = user, %{"type" => "Like"} = params) do
with %Object{} = object <- Object.normalize(params["object"]),
- {:ok, activity, _object} <- ActivityPub.like(user, object) do
+ {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
+ {_, {:ok, %Activity{} = activity, _meta}} <-
+ {:common_pipeline,
+ Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
{:ok, activity}
else
_ -> {:error, dgettext("errors", "Can't like object")}
end
end
- def handle_user_activity(_, _) do
+ defp handle_user_activity(_, _) do
{:error, dgettext("errors", "Unhandled activity type")}
end
@@ -434,7 +467,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
end
end
- def update_outbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} = _) do
+ def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do
err =
dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
nickname: nickname,
@@ -446,13 +479,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|> json(err)
end
- def errors(conn, {:error, :not_found}) do
+ defp errors(conn, {:error, :not_found}) do
conn
|> put_status(:not_found)
|> json(dgettext("errors", "Not found"))
end
- def errors(conn, _e) do
+ defp errors(conn, _e) do
conn
|> put_status(:internal_server_error)
|> json(dgettext("errors", "error"))
@@ -492,7 +525,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
- HTTP Code: 201 Created
- HTTP Body: ActivityPub object to be inserted into another's `attachment` field
"""
- def upload_media(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
+ def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
with {:ok, object} <-
ActivityPub.upload(
file,
diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex
new file mode 100644
index 000000000..4a247ad0c
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/builder.ex
@@ -0,0 +1,115 @@
+defmodule Pleroma.Web.ActivityPub.Builder do
+ @moduledoc """
+ This module builds the objects. Meant to be used for creating local objects.
+
+ This module encodes our addressing policies and general shape of our objects.
+ """
+
+ alias Pleroma.Object
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.Utils
+ alias Pleroma.Web.ActivityPub.Visibility
+
+ @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
+
+ @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.
+ to =
+ if Visibility.is_public?(object) do
+ [actor.follower_address, object.data["actor"]]
+ else
+ [object.data["actor"]]
+ end
+
+ # CC everyone who's been addressed in the object, except ourself and the object actor's
+ # follower collection
+ cc =
+ (object.data["to"] ++ (object.data["cc"] || []))
+ |> List.delete(actor.ap_id)
+ |> List.delete(object_actor.follower_address)
+
+ {:ok,
+ %{
+ "id" => Utils.generate_activity_id(),
+ "actor" => actor.ap_id,
+ "object" => object.data["id"],
+ "to" => to,
+ "cc" => cc,
+ "context" => object.data["context"]
+ }, []}
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/internal_fetch_actor.ex b/lib/pleroma/web/activity_pub/internal_fetch_actor.ex
index 9213ddde7..c80272b8f 100644
--- a/lib/pleroma/web/activity_pub/internal_fetch_actor.ex
+++ b/lib/pleroma/web/activity_pub/internal_fetch_actor.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.InternalFetchActor do
diff --git a/lib/pleroma/web/activity_pub/mrf.ex b/lib/pleroma/web/activity_pub/mrf.ex
index 263ed11af..a0b3af432 100644
--- a/lib/pleroma/web/activity_pub/mrf.ex
+++ b/lib/pleroma/web/activity_pub/mrf.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF do
diff --git a/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex
index b3547ecd4..0270b96ae 100644
--- a/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy do
diff --git a/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex
index 802d10edc..9e7800997 100644
--- a/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy do
diff --git a/lib/pleroma/web/activity_pub/mrf/drop_policy.ex b/lib/pleroma/web/activity_pub/mrf/drop_policy.ex
index 4a5709974..5ab9844ff 100644
--- a/lib/pleroma/web/activity_pub/mrf/drop_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/drop_policy.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.DropPolicy do
diff --git a/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex b/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex
index 3a3e72910..2627a0007 100644
--- a/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex
+++ b/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do
diff --git a/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex b/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex
index b3c742954..1764bc789 100644
--- a/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do
diff --git a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex
index d6d1396bc..88b0d2b39 100644
--- a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
diff --git a/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex b/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex
index df774b0f7..dfab105a3 100644
--- a/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do
@@ -12,17 +12,23 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do
require Logger
- @hackney_options [
- pool: :media,
- recv_timeout: 10_000
+ @options [
+ pool: :media
]
def perform(:prefetch, url) do
Logger.debug("Prefetching #{inspect(url)}")
+ opts =
+ if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do
+ Keyword.put(@options, :recv_timeout, 10_000)
+ else
+ @options
+ end
+
url
|> MediaProxy.url()
- |> HTTP.get([], adapter: @hackney_options)
+ |> HTTP.get([], adapter: opts)
end
def perform(:preload, %{"object" => %{"attachment" => attachments}} = _message) do
diff --git a/lib/pleroma/web/activity_pub/mrf/mention_policy.ex b/lib/pleroma/web/activity_pub/mrf/mention_policy.ex
index ce8bc4580..06f003921 100644
--- a/lib/pleroma/web/activity_pub/mrf/mention_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/mention_policy.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.MentionPolicy do
diff --git a/lib/pleroma/web/activity_pub/mrf/no_op_policy.ex b/lib/pleroma/web/activity_pub/mrf/no_op_policy.ex
index 878c57925..cc2ac9d08 100644
--- a/lib/pleroma/web/activity_pub/mrf/no_op_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/no_op_policy.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.NoOpPolicy do
diff --git a/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex b/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex
index f67f48ab6..fc3475048 100644
--- a/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicy do
diff --git a/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex b/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex
index daa4c88ad..7abae37ae 100644
--- a/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex
+++ b/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkup do
diff --git a/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex b/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex
index 788508349..b0ccb63c8 100644
--- a/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
@@ -11,7 +11,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
@moduledoc "Filter activities depending on their age"
@behaviour Pleroma.Web.ActivityPub.MRF
- defp check_date(%{"published" => published} = message) do
+ defp check_date(%{"object" => %{"published" => published}} = message) do
with %DateTime{} = now <- DateTime.utc_now(),
{:ok, %DateTime{} = then, _} <- DateTime.from_iso8601(published),
max_ttl <- Config.get([:mrf_object_age, :threshold]),
@@ -96,5 +96,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
def filter(message), do: {:ok, message}
@impl true
- def describe, do: {:ok, %{}}
+ def describe do
+ mrf_object_age =
+ Pleroma.Config.get(:mrf_object_age)
+ |> Enum.into(%{})
+
+ {:ok, %{mrf_object_age: mrf_object_age}}
+ end
end
diff --git a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex
index 5a809a321..3092f3272 100644
--- a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex
+++ b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do
diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
index b94c3c78a..b7dcb1b86 100644
--- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
@@ -149,6 +149,21 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
defp check_banner_removal(_actor_info, object), do: {:ok, object}
@impl true
+ def filter(%{"type" => "Delete", "actor" => actor} = object) do
+ %{host: actor_host} = URI.parse(actor)
+
+ reject_deletes =
+ Pleroma.Config.get([:mrf_simple, :reject_deletes])
+ |> MRF.subdomains_regex()
+
+ if MRF.subdomain_match?(reject_deletes, actor_host) do
+ {:reject, nil}
+ else
+ {:ok, object}
+ end
+ end
+
+ @impl true
def filter(%{"actor" => actor} = object) do
actor_info = URI.parse(actor)
diff --git a/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex b/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex
index 77ffd1bb9..c9f20571f 100644
--- a/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.SubchainPolicy do
diff --git a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex
index c1801d2ec..c310462cb 100644
--- a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do
diff --git a/lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex b/lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex
index 7389d6a96..a927a4ed8 100644
--- a/lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy do
diff --git a/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex b/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex
index c184c3b66..6167a74e2 100644
--- a/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicy do
diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex
new file mode 100644
index 000000000..549e5e761
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validator.ex
@@ -0,0 +1,83 @@
+# 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.ObjectValidator do
+ @moduledoc """
+ This module is responsible for validating an object (which can be an activity)
+ and checking if it is both well formed and also compatible with our view of
+ the system.
+ """
+
+ alias Pleroma.Object
+ alias Pleroma.User
+ 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
+ object = stringify_keys(object |> Map.from_struct())
+ {:ok, object, meta}
+ 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 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
+ fetch_actor(object)
+ Object.normalize(object["object"])
+ :ok
+ 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
new file mode 100644
index 000000000..aeef31945
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/common_validations.ex
@@ -0,0 +1,80 @@
+# 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.CommonValidations do
+ import Ecto.Changeset
+
+ alias Pleroma.Activity
+ alias Pleroma.Object
+ alias Pleroma.User
+
+ 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
+ []
+ else
+ [{field_name, "can't find user"}]
+ end
+ end)
+ end
+
+ 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_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/create_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_validator.ex
new file mode 100644
index 000000000..926804ce7
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/create_validator.ex
@@ -0,0 +1,30 @@
+# 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.CreateNoteValidator do
+ use Ecto.Schema
+
+ alias Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.Types
+
+ import Ecto.Changeset
+
+ @primary_key false
+
+ embedded_schema do
+ field(:id, Types.ObjectID, primary_key: true)
+ field(:actor, Types.ObjectID)
+ field(:type, :string)
+ field(:to, {:array, :string})
+ field(:cc, {:array, :string})
+ field(:bto, {:array, :string}, default: [])
+ field(:bcc, {:array, :string}, default: [])
+
+ embeds_one(:object, NoteValidator)
+ end
+
+ def cast_data(data) do
+ cast(%__MODULE__{}, data, __schema__(:fields))
+ 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
new file mode 100644
index 000000000..034f25492
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/like_validator.ex
@@ -0,0 +1,99 @@
+# 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.LikeValidator do
+ use Ecto.Schema
+
+ alias Pleroma.Object
+ alias Pleroma.Web.ActivityPub.ObjectValidators.Types
+ alias Pleroma.Web.ActivityPub.Utils
+
+ 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(:to, Types.Recipients, default: [])
+ field(:cc, Types.Recipients, 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_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
+ data_cng
+ |> validate_inclusion(:type, ["Like"])
+ |> validate_required([:id, :type, :object, :actor, :context, :to, :cc])
+ |> validate_actor_presence()
+ |> validate_object_presence()
+ |> validate_existing_like()
+ end
+
+ def validate_existing_like(%{changes: %{actor: actor, object: object}} = cng) do
+ if Utils.get_existing_like(actor, %{data: %{"id" => object}}) do
+ cng
+ |> add_error(:actor, "already liked this object")
+ |> add_error(:object, "already liked by this actor")
+ else
+ cng
+ end
+ end
+
+ def validate_existing_like(cng), do: cng
+end
diff --git a/lib/pleroma/web/activity_pub/object_validators/note_validator.ex b/lib/pleroma/web/activity_pub/object_validators/note_validator.ex
new file mode 100644
index 000000000..462a5620a
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/note_validator.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.Web.ActivityPub.ObjectValidators.NoteValidator do
+ use Ecto.Schema
+
+ alias Pleroma.Web.ActivityPub.ObjectValidators.Types
+
+ import Ecto.Changeset
+
+ @primary_key false
+
+ embedded_schema do
+ field(:id, Types.ObjectID, primary_key: true)
+ field(:to, {:array, :string}, default: [])
+ field(:cc, {:array, :string}, default: [])
+ field(:bto, {:array, :string}, default: [])
+ field(:bcc, {:array, :string}, default: [])
+ # TODO: Write type
+ field(:tag, {:array, :map}, default: [])
+ field(:type, :string)
+ field(:content, :string)
+ field(:context, :string)
+ field(:actor, Types.ObjectID)
+ field(:attributedTo, Types.ObjectID)
+ field(:summary, :string)
+ field(:published, Types.DateTime)
+ # TODO: Write type
+ field(:emoji, :map, default: %{})
+ field(:sensitive, :boolean, default: false)
+ # TODO: Write type
+ field(:attachment, {:array, :map}, default: [])
+ field(:replies_count, :integer, default: 0)
+ field(:like_count, :integer, default: 0)
+ field(:announcement_count, :integer, default: 0)
+ field(:inRepyTo, :string)
+ field(:uri, Types.Uri)
+
+ field(:likes, {:array, :string}, default: [])
+ field(:announcements, {:array, :string}, default: [])
+
+ # see if needed
+ field(:conversation, :string)
+ field(:context_id, :string)
+ end
+
+ def cast_and_validate(data) do
+ data
+ |> cast_data()
+ |> validate_data()
+ end
+
+ def cast_data(data) do
+ %__MODULE__{}
+ |> cast(data, __schema__(:fields))
+ end
+
+ def validate_data(data_cng) do
+ data_cng
+ |> validate_inclusion(:type, ["Note"])
+ |> validate_required([:id, :actor, :to, :cc, :type, :content, :context])
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/object_validators/types/date_time.ex b/lib/pleroma/web/activity_pub/object_validators/types/date_time.ex
new file mode 100644
index 000000000..4f412fcde
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/types/date_time.ex
@@ -0,0 +1,34 @@
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.DateTime do
+ @moduledoc """
+ The AP standard defines the date fields in AP as xsd:DateTime. Elixir's
+ DateTime can't parse this, but it can parse the related iso8601. This
+ module punches the date until it looks like iso8601 and normalizes to
+ it.
+
+ DateTimes without a timezone offset are treated as UTC.
+
+ Reference: https://www.w3.org/TR/activitystreams-vocabulary/#dfn-published
+ """
+ use Ecto.Type
+
+ def type, do: :string
+
+ def cast(datetime) when is_binary(datetime) do
+ with {:ok, datetime, _} <- DateTime.from_iso8601(datetime) do
+ {:ok, DateTime.to_iso8601(datetime)}
+ else
+ {:error, :missing_offset} -> cast("#{datetime}Z")
+ _e -> :error
+ end
+ end
+
+ def cast(_), do: :error
+
+ 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/types/object_id.ex b/lib/pleroma/web/activity_pub/object_validators/types/object_id.ex
new file mode 100644
index 000000000..f71f76370
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/types/object_id.ex
@@ -0,0 +1,23 @@
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.ObjectID do
+ use Ecto.Type
+
+ def type, do: :string
+
+ def cast(object) when is_binary(object) do
+ # Host has to be present and scheme has to be an http scheme (for now)
+ case URI.parse(object) do
+ %URI{host: nil} -> :error
+ %URI{host: ""} -> :error
+ %URI{scheme: scheme} when scheme in ["https", "http"] -> {:ok, object}
+ _ -> :error
+ end
+ end
+
+ def cast(%{"id" => object}), do: cast(object)
+
+ def cast(_), do: :error
+
+ def dump(data), do: {:ok, data}
+
+ def load(data), do: {:ok, data}
+end
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/types/uri.ex b/lib/pleroma/web/activity_pub/object_validators/types/uri.ex
new file mode 100644
index 000000000..24845bcc0
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/types/uri.ex
@@ -0,0 +1,20 @@
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.Uri do
+ use Ecto.Type
+
+ def type, do: :string
+
+ def cast(uri) when is_binary(uri) do
+ case URI.parse(uri) do
+ %URI{host: nil} -> :error
+ %URI{host: ""} -> :error
+ %URI{scheme: scheme} when scheme in ["https", "http"] -> {:ok, uri}
+ _ -> :error
+ end
+ end
+
+ def cast(_), do: :error
+
+ def dump(data), do: {:ok, data}
+
+ def load(data), do: {:ok, data}
+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
new file mode 100644
index 000000000..657cdfdb1
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/pipeline.ex
@@ -0,0 +1,59 @@
+# 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.Pipeline do
+ alias Pleroma.Activity
+ 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() | 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, meta}} <-
+ {:persist_object, ActivityPub.persist(mrfd_object, meta)},
+ {_, {:ok, activity, meta}} <-
+ {:execute_side_effects, SideEffects.handle(activity, meta)},
+ {_, {:ok, _}} <- {:federation, maybe_federate(activity, meta)} do
+ {:ok, activity, meta}
+ else
+ {:mrf_object, {:reject, _}} -> {:ok, nil, meta}
+ e -> {:error, e}
+ end
+ end
+
+ defp maybe_federate(%Object{}, _), do: {:ok, :not_federated}
+
+ defp maybe_federate(%Activity{} = activity, meta) do
+ with {:ok, local} <- Keyword.fetch(meta, :local) do
+ do_not_federate = meta[:do_not_federate]
+
+ if !do_not_federate && local do
+ Federator.publish(activity)
+ {:ok, :federated}
+ else
+ {:ok, :not_federated}
+ end
+ else
+ _e -> {:error, :badarg}
+ end
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex
index e4e3ab44a..b70cbd043 100644
--- a/lib/pleroma/web/activity_pub/publisher.ex
+++ b/lib/pleroma/web/activity_pub/publisher.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Publisher do
@@ -141,8 +141,8 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|> Enum.map(& &1.ap_id)
end
- defp maybe_use_sharedinbox(%User{source_data: data}),
- do: (is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]
+ defp maybe_use_sharedinbox(%User{shared_inbox: nil, inbox: inbox}), do: inbox
+ defp maybe_use_sharedinbox(%User{shared_inbox: shared_inbox}), do: shared_inbox
@doc """
Determine a user inbox to use based on heuristics. These heuristics
@@ -157,7 +157,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
"""
def determine_inbox(
%Activity{data: activity_data},
- %User{source_data: data} = user
+ %User{inbox: inbox} = user
) do
to = activity_data["to"] || []
cc = activity_data["cc"] || []
@@ -174,7 +174,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
maybe_use_sharedinbox(user)
true ->
- data["inbox"]
+ inbox
end
end
@@ -192,14 +192,13 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
inboxes =
recipients
|> Enum.filter(&User.ap_enabled?/1)
- |> Enum.map(fn %{source_data: data} -> data["inbox"] end)
+ |> Enum.map(fn actor -> actor.inbox end)
|> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
|> Instances.filter_reachable()
Repo.checkout(fn ->
Enum.each(inboxes, fn {inbox, unreachable_since} ->
- %User{ap_id: ap_id} =
- Enum.find(recipients, fn %{source_data: data} -> data["inbox"] == inbox end)
+ %User{ap_id: ap_id} = Enum.find(recipients, fn actor -> actor.inbox == inbox end)
# Get all the recipients on the same host and add them to cc. Otherwise, a remote
# instance would only accept a first message for the first recipient and ignore the rest.
diff --git a/lib/pleroma/web/activity_pub/relay.ex b/lib/pleroma/web/activity_pub/relay.ex
index 48a1b71e0..729c23af7 100644
--- a/lib/pleroma/web/activity_pub/relay.ex
+++ b/lib/pleroma/web/activity_pub/relay.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Relay do
@@ -60,15 +60,28 @@ defmodule Pleroma.Web.ActivityPub.Relay do
def publish(_), do: {:error, "Not implemented"}
- @spec list() :: {:ok, [String.t()]} | {:error, any()}
- def list do
+ @spec list(boolean()) :: {:ok, [String.t()]} | {:error, any()}
+ def list(with_not_accepted \\ false) do
with %User{} = user <- get_actor() do
- list =
+ accepted =
user
|> User.following()
|> Enum.map(fn entry -> URI.parse(entry).host end)
|> Enum.uniq()
+ list =
+ if with_not_accepted do
+ without_accept =
+ user
+ |> Pleroma.Activity.following_requests_for_actor()
+ |> Enum.map(fn a -> URI.parse(a.data["object"]).host <> " (no Accept received)" end)
+ |> Enum.uniq()
+
+ accepted ++ without_accept
+ else
+ accepted
+ end
+
{:ok, list}
else
error -> format_error(error)
diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex
new file mode 100644
index 000000000..bfc2ab845
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/side_effects.ex
@@ -0,0 +1,133 @@
+defmodule Pleroma.Web.ActivityPub.SideEffects do
+ @moduledoc """
+ This module looks at an inserted object and executes the side effects that it
+ implies. For example, a `Like` activity will increase the like count on the
+ 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 \\ [])
+
+ # Tasks this handles:
+ # - Add like to object
+ # - Set up notification
+ def handle(%{data: %{"type" => "Like"}} = object, meta) do
+ liked_object = Object.get_by_ap_id(object.data["object"])
+ Utils.add_like_to_object(object, liked_object)
+
+ Notification.create_notifications(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
+
+ 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 a72d8430f..80701bb63 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Transmogrifier do
@@ -7,12 +7,17 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
A module to handle coding from internal to wire ActivityPub and back.
"""
alias Pleroma.Activity
+ alias Pleroma.EarmarkRenderer
alias Pleroma.FollowingRelationship
alias Pleroma.Object
alias Pleroma.Object.Containment
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.Types
+ alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Federator
@@ -40,6 +45,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> fix_addressing
|> fix_summary
|> fix_type(options)
+ |> fix_content
end
def fix_summary(%{"summary" => nil} = object) do
@@ -156,10 +162,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
when not is_nil(in_reply_to) do
in_reply_to_id = prepare_in_reply_to(in_reply_to)
object = Map.put(object, "inReplyToAtomUri", in_reply_to_id)
+ depth = (options[:depth] || 0) + 1
- if Federator.allowed_incoming_reply_depth?(options[:depth]) do
+ if Federator.allowed_thread_distance?(depth) do
with {:ok, replied_object} <- get_obj_helper(in_reply_to_id, options),
- %Activity{} = _ <- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
+ %Activity{} <- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
object
|> Map.put("inReplyTo", replied_object.data["id"])
|> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
@@ -201,16 +208,46 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> Map.put("conversation", context)
end
+ defp add_if_present(map, _key, nil), do: map
+
+ defp add_if_present(map, key, value) do
+ Map.put(map, key, value)
+ end
+
def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
attachments =
Enum.map(attachment, fn data ->
- media_type = data["mediaType"] || data["mimeType"]
- href = data["url"] || data["href"]
- url = [%{"type" => "Link", "mediaType" => media_type, "href" => href}]
-
- data
- |> Map.put("mediaType", media_type)
- |> Map.put("url", url)
+ url =
+ cond do
+ is_list(data["url"]) -> List.first(data["url"])
+ is_map(data["url"]) -> data["url"]
+ true -> nil
+ end
+
+ media_type =
+ cond do
+ is_map(url) && is_binary(url["mediaType"]) -> url["mediaType"]
+ is_binary(data["mediaType"]) -> data["mediaType"]
+ is_binary(data["mimeType"]) -> data["mimeType"]
+ true -> nil
+ end
+
+ href =
+ cond do
+ is_map(url) && is_binary(url["href"]) -> url["href"]
+ is_binary(data["url"]) -> data["url"]
+ is_binary(data["href"]) -> data["href"]
+ end
+
+ attachment_url =
+ %{"href" => href}
+ |> add_if_present("mediaType", media_type)
+ |> add_if_present("type", Map.get(url || %{}, "type"))
+
+ %{"url" => [attachment_url]}
+ |> add_if_present("mediaType", media_type)
+ |> add_if_present("type", data["type"])
+ |> add_if_present("name", data["name"])
end)
Map.put(object, "attachment", attachments)
@@ -228,7 +265,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
Map.put(object, "url", url["href"])
end
- def fix_url(%{"type" => "Video", "url" => url} = object) when is_list(url) do
+ def fix_url(%{"type" => object_type, "url" => url} = object)
+ when object_type in ["Video", "Audio"] and is_list(url) do
first_element = Enum.at(url, 0)
link_element = Enum.find(url, fn x -> is_map(x) and x["mimeType"] == "text/html" end)
@@ -312,7 +350,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options)
when is_binary(reply_id) do
- with true <- Federator.allowed_incoming_reply_depth?(options[:depth]),
+ with true <- Federator.allowed_thread_distance?(options[:depth]),
{:ok, %{data: %{"type" => "Question"} = _} = _} <- get_obj_helper(reply_id, options) do
Map.put(object, "type", "Answer")
else
@@ -322,6 +360,18 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def fix_type(object, _), do: object
+ defp fix_content(%{"mediaType" => "text/markdown", "content" => content} = object)
+ when is_binary(content) do
+ html_content =
+ content
+ |> Earmark.as_html!(%Earmark.Options{renderer: EarmarkRenderer})
+ |> Pleroma.HTML.filter_tags()
+
+ Map.merge(object, %{"content" => html_content, "mediaType" => "text/html"})
+ end
+
+ defp fix_content(object), do: object
+
defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
with true <- id =~ "follows",
%User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
@@ -397,7 +447,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
%{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
options
)
- when objtype in ["Article", "Event", "Note", "Video", "Page", "Question", "Answer"] do
+ when objtype in ["Article", "Event", "Note", "Video", "Page", "Question", "Answer", "Audio"] do
actor = Containment.get_actor(data)
data =
@@ -406,8 +456,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
with nil <- Activity.get_create_by_object_ap_id(object["id"]),
{:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
- options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
- object = fix_object(data["object"], options)
+ object = fix_object(object, options)
params = %{
to: data["to"],
@@ -424,7 +473,20 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
])
}
- ActivityPub.create(params)
+ with {:ok, created_activity} <- ActivityPub.create(params) do
+ reply_depth = (options[:depth] || 0) + 1
+
+ if Federator.allowed_thread_distance?(reply_depth) do
+ for reply_id <- replies(object) do
+ Pleroma.Workers.RemoteFetcherWorker.enqueue("fetch_remote", %{
+ "id" => reply_id,
+ "depth" => reply_depth
+ })
+ end
+ end
+
+ {:ok, created_activity}
+ end
else
%Activity{} = activity -> {:ok, activity}
_e -> :error
@@ -442,7 +504,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> fix_addressing
with {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
- options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
+ reply_depth = (options[:depth] || 0) + 1
+ options = Keyword.put(options, :depth, reply_depth)
object = fix_object(object, options)
params = %{
@@ -476,7 +539,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
{_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
{_, {:ok, _}} <-
{:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")},
- {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept") do
+ {:ok, _relationship} <-
+ FollowingRelationship.update(follower, followed, :follow_accept) do
ActivityPub.accept(%{
to: [follower.ap_id],
actor: followed,
@@ -486,7 +550,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
else
{:user_blocked, true} ->
{:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
- {:ok, _relationship} = FollowingRelationship.update(follower, followed, "reject")
+ {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject)
ActivityPub.reject(%{
to: [follower.ap_id],
@@ -497,7 +561,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
{:follow, {:error, _}} ->
{:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
- {:ok, _relationship} = FollowingRelationship.update(follower, followed, "reject")
+ {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject)
ActivityPub.reject(%{
to: [follower.ap_id],
@@ -507,7 +571,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
})
{:user_locked, true} ->
- {:ok, _relationship} = FollowingRelationship.update(follower, followed, "pending")
+ {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_pending)
:noop
end
@@ -527,7 +591,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
{:ok, follow_activity} <- get_follow_activity(follow_object, followed),
{: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, "accept") do
+ {: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",
@@ -537,7 +604,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
activity_id: id
})
else
- _e -> :error
+ _e ->
+ :error
end
end
@@ -550,7 +618,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
{:ok, follow_activity} <- get_follow_activity(follow_object, followed),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
- {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "reject"),
+ {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
{:ok, activity} <-
ActivityPub.reject(%{
to: follow_activity.data["to"],
@@ -594,38 +662,13 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> handle_incoming(options)
end
- def handle_incoming(
- %{"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, _object} <- ActivityPub.like(actor, object, id, false) do
+ def handle_incoming(%{"type" => type} = data, _options) when type in ["Like", "EmojiReact"] do
+ with :ok <- ObjectValidator.fetch_actor_and_object(data),
+ {:ok, activity, _meta} <-
+ Pipeline.common_pipeline(data, local: false) do
{:ok, activity}
else
- _e -> :error
- end
- 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
+ e -> {:error, e}
end
end
@@ -659,7 +702,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
{:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
actor
- |> User.upgrade_changeset(new_user_data, true)
+ |> User.remote_user_changeset(new_user_data)
|> User.update_and_set_cache()
ActivityPub.update(%{
@@ -677,55 +720,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
@@ -751,39 +764,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
@@ -806,43 +809,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,
@@ -903,6 +869,50 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def set_reply_to_uri(obj), do: obj
+ @doc """
+ Serialized Mastodon-compatible `replies` collection containing _self-replies_.
+ Based on Mastodon's ActivityPub::NoteSerializer#replies.
+ """
+ def set_replies(obj_data) do
+ replies_uris =
+ with limit when limit > 0 <-
+ Pleroma.Config.get([:activitypub, :note_replies_output_limit], 0),
+ %Object{} = object <- Object.get_cached_by_ap_id(obj_data["id"]) do
+ object
+ |> Object.self_replies()
+ |> select([o], fragment("?->>'id'", o.data))
+ |> limit(^limit)
+ |> Repo.all()
+ else
+ _ -> []
+ end
+
+ set_replies(obj_data, replies_uris)
+ end
+
+ defp set_replies(obj, []) do
+ obj
+ end
+
+ defp set_replies(obj, replies_uris) do
+ replies_collection = %{
+ "type" => "Collection",
+ "items" => replies_uris
+ }
+
+ Map.merge(obj, %{"replies" => replies_collection})
+ end
+
+ def replies(%{"replies" => %{"first" => %{"items" => items}}}) when not is_nil(items) do
+ items
+ end
+
+ def replies(%{"replies" => %{"items" => items}}) when not is_nil(items) do
+ items
+ end
+
+ def replies(_), do: []
+
# Prepares the object of an outgoing create activity.
def prepare_object(object) do
object
@@ -914,6 +924,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> prepare_attachments
|> set_conversation
|> set_reply_to_uri
+ |> set_replies
|> strip_internal_fields
|> strip_internal_tags
|> set_type
@@ -1049,13 +1060,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
def add_mention_tags(object) do
- mentions =
- object
- |> Utils.get_notified_from_object()
- |> Enum.map(&build_mention_tag/1)
+ {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)
tags = object["tag"] || []
-
Map.put(object, "tag", tags ++ mentions)
end
@@ -1065,7 +1074,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def take_emoji_tags(%User{emoji: emoji}) do
emoji
- |> Enum.flat_map(&Map.to_list/1)
+ |> Map.to_list()
|> Enum.map(&build_emoji_tag/1)
end
@@ -1094,6 +1103,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)
@@ -1112,18 +1125,24 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def prepare_attachments(object) do
attachments =
- (object["attachment"] || [])
+ object
+ |> Map.get("attachment", [])
|> Enum.map(fn data ->
[%{"mediaType" => media_type, "href" => href} | _] = data["url"]
- %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
+
+ %{
+ "url" => href,
+ "mediaType" => media_type,
+ "name" => data["name"],
+ "type" => "Document"
+ }
end)
Map.put(object, "attachment", attachments)
end
def strip_internal_fields(object) do
- object
- |> Map.drop(Pleroma.Constants.object_internal_fields())
+ Map.drop(object, Pleroma.Constants.object_internal_fields())
end
defp strip_internal_tags(%{"tag" => tags} = object) do
@@ -1159,12 +1178,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def upgrade_user_from_ap_id(ap_id) do
with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
{:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
- already_ap <- User.ap_enabled?(user),
- {:ok, user} <- upgrade_user(user, data) do
- if not already_ap do
- TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
- end
-
+ {:ok, user} <- update_user(user, data) do
+ TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
{:ok, user}
else
%User{} = user -> {:ok, user}
@@ -1172,9 +1187,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
end
- defp upgrade_user(user, data) do
+ defp update_user(user, data) do
user
- |> User.upgrade_changeset(data, true)
+ |> User.remote_user_changeset(data)
|> User.update_and_set_cache()
end
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index 10ce5eee8..f2375bcc4 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -1,11 +1,12 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
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
@@ -45,8 +46,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do
Map.put(params, "actor", get_ap_id(params["actor"]))
end
- @spec determine_explicit_mentions(map()) :: map()
- def determine_explicit_mentions(%{"tag" => tag} = _) when is_list(tag) do
+ @spec determine_explicit_mentions(map()) :: [any]
+ def determine_explicit_mentions(%{"tag" => tag}) when is_list(tag) do
Enum.flat_map(tag, fn
%{"type" => "Mention", "href" => href} -> [href]
_ -> []
@@ -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
@@ -427,7 +431,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
@doc """
Updates a follow activity's state (for locked accounts).
"""
- @spec update_follow_state_for_all(Activity.t(), String.t()) :: {:ok, Activity} | {:error, any()}
+ @spec update_follow_state_for_all(Activity.t(), String.t()) :: {:ok, Activity | nil}
def update_follow_state_for_all(
%Activity{data: %{"actor" => actor, "object" => object}} = activity,
state
@@ -440,22 +444,19 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
|> Repo.update_all([])
- User.set_follow_state_cache(actor, object, state)
-
activity = Activity.get_by_id(activity.id)
{:ok, activity}
end
def update_follow_state(
- %Activity{data: %{"actor" => actor, "object" => object}} = activity,
+ %Activity{} = activity,
state
) do
new_data = Map.put(activity.data, "state", state)
changeset = Changeset.change(activity, data: new_data)
with {:ok, activity} <- Repo.update(changeset) do
- User.set_follow_state_cache(actor, object, state)
{:ok, activity}
end
end
@@ -515,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
@@ -565,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{
@@ -691,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
@@ -784,45 +736,6 @@ defmodule Pleroma.Web.ActivityPub.Utils do
defp build_flag_object(_), do: []
- @doc """
- Fetches the OrderedCollection/OrderedCollectionPage from `from`, limiting the amount of pages fetched after
- the first one to `pages_left` pages.
- If the amount of pages is higher than the collection has, it returns whatever was there.
- """
- def fetch_ordered_collection(from, pages_left, acc \\ []) do
- with {:ok, response} <- Tesla.get(from),
- {:ok, collection} <- Jason.decode(response.body) do
- case collection["type"] do
- "OrderedCollection" ->
- # If we've encountered the OrderedCollection and not the page,
- # just call the same function on the page address
- fetch_ordered_collection(collection["first"], pages_left)
-
- "OrderedCollectionPage" ->
- if pages_left > 0 do
- # There are still more pages
- if Map.has_key?(collection, "next") do
- # There are still more pages, go deeper saving what we have into the accumulator
- fetch_ordered_collection(
- collection["next"],
- pages_left - 1,
- acc ++ collection["orderedItems"]
- )
- else
- # No more pages left, just return whatever we already have
- acc ++ collection["orderedItems"]
- end
- else
- # Got the amount of pages needed, add them all to the accumulator
- acc ++ collection["orderedItems"]
- end
-
- _ ->
- {:error, "Not an OrderedCollection or OrderedCollectionPage"}
- end
- end
- end
-
#### Report-related helpers
def get_reports(params, page, page_size) do
params =
@@ -837,102 +750,6 @@ defmodule Pleroma.Web.ActivityPub.Utils do
ActivityPub.fetch_activities([], params, :offset)
end
- def parse_report_group(activity) do
- reports = get_reports_by_status_id(activity["id"])
- max_date = Enum.max_by(reports, &NaiveDateTime.from_iso8601!(&1.data["published"]))
- actors = Enum.map(reports, & &1.user_actor)
- [%{data: %{"object" => [account_id | _]}} | _] = reports
-
- account =
- AccountView.render("show.json", %{
- user: User.get_by_ap_id(account_id)
- })
-
- status = get_status_data(activity)
-
- %{
- date: max_date.data["published"],
- account: account,
- status: status,
- actors: Enum.uniq(actors),
- reports: reports
- }
- end
-
- defp get_status_data(status) do
- case status["deleted"] do
- true ->
- %{
- "id" => status["id"],
- "deleted" => true
- }
-
- _ ->
- Activity.get_by_ap_id(status["id"])
- end
- end
-
- def get_reports_by_status_id(ap_id) do
- from(a in Activity,
- where: fragment("(?)->>'type' = 'Flag'", a.data),
- where: fragment("(?)->'object' @> ?", a.data, ^[%{id: ap_id}]),
- or_where: fragment("(?)->'object' @> ?", a.data, ^[ap_id])
- )
- |> Activity.with_preloaded_user_actor()
- |> Repo.all()
- end
-
- @spec get_reports_grouped_by_status([String.t()]) :: %{
- required(:groups) => [
- %{
- required(:date) => String.t(),
- required(:account) => %{},
- required(:status) => %{},
- required(:actors) => [%User{}],
- required(:reports) => [%Activity{}]
- }
- ]
- }
- def get_reports_grouped_by_status(activity_ids) do
- parsed_groups =
- activity_ids
- |> Enum.map(fn id ->
- id
- |> build_flag_object()
- |> parse_report_group()
- end)
-
- %{
- groups: parsed_groups
- }
- end
-
- @spec get_reported_activities() :: [
- %{
- required(:activity) => String.t(),
- required(:date) => String.t()
- }
- ]
- def get_reported_activities do
- reported_activities_query =
- from(a in Activity,
- where: fragment("(?)->>'type' = 'Flag'", a.data),
- select: %{
- activity: fragment("jsonb_array_elements((? #- '{object,0}')->'object')", a.data)
- },
- group_by: fragment("activity")
- )
-
- from(a in subquery(reported_activities_query),
- distinct: true,
- select: %{
- id: fragment("COALESCE(?->>'id'::text, ? #>> '{}')", a.activity, a.activity)
- }
- )
- |> Repo.all()
- |> Enum.map(& &1.id)
- end
-
def update_report_state(%Activity{} = activity, state)
when state in @strip_status_report_states do
{:ok, stripped_activity} = strip_report_status_data(activity)
diff --git a/lib/pleroma/web/activity_pub/views/object_view.ex b/lib/pleroma/web/activity_pub/views/object_view.ex
index d8a3ec288..e555e9999 100644
--- a/lib/pleroma/web/activity_pub/views/object_view.ex
+++ b/lib/pleroma/web/activity_pub/views/object_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectView do
diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex
index 350c4391d..34590b16d 100644
--- a/lib/pleroma/web/activity_pub/views/user_view.ex
+++ b/lib/pleroma/web/activity_pub/views/user_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.UserView do
@@ -73,21 +73,13 @@ defmodule Pleroma.Web.ActivityPub.UserView do
{:ok, _, public_key} = Keys.keys_from_pem(user.keys)
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
public_key = :public_key.pem_encode([public_key])
+ user = User.sanitize_html(user)
endpoints = render("endpoints.json", %{user: user})
emoji_tags = Transmogrifier.take_emoji_tags(user)
- fields =
- user
- |> User.fields()
- |> Enum.map(fn %{"name" => name, "value" => value} ->
- %{
- "name" => Pleroma.HTML.strip_tags(name),
- "value" => Pleroma.HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
- }
- end)
- |> Enum.map(&Map.put(&1, "type", "PropertyValue"))
+ fields = Enum.map(user.fields, &Map.put(&1, "type", "PropertyValue"))
%{
"id" => user.ap_id,
@@ -108,7 +100,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
},
"endpoints" => endpoints,
"attachment" => fields,
- "tag" => (user.source_data["tag"] || []) ++ emoji_tags,
+ "tag" => emoji_tags,
"discoverable" => user.discoverable
}
|> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))
diff --git a/lib/pleroma/web/activity_pub/visibility.ex b/lib/pleroma/web/activity_pub/visibility.ex
index e172f6d3f..453a6842e 100644
--- a/lib/pleroma/web/activity_pub/visibility.ex
+++ b/lib/pleroma/web/activity_pub/visibility.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Visibility do
@@ -44,6 +44,7 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
def is_list?(%{data: %{"listMessage" => _}}), do: true
def is_list?(_), do: false
+ @spec visible_for_user?(Activity.t(), User.t() | nil) :: boolean()
def visible_for_user?(%{actor: ap_id}, %User{ap_id: ap_id}), do: true
def visible_for_user?(%{data: %{"listMessage" => list_ap_id}} = activity, %User{} = user) do
@@ -55,14 +56,21 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
def visible_for_user?(%{data: %{"listMessage" => _}}, nil), do: false
- def visible_for_user?(activity, nil) do
- is_public?(activity)
+ def visible_for_user?(%{local: local} = activity, nil) do
+ cfg_key =
+ if local,
+ do: :local,
+ else: :remote
+
+ if Pleroma.Config.get([:restrict_unauthenticated, :activities, cfg_key]),
+ do: false,
+ else: is_public?(activity)
end
def visible_for_user?(activity, user) do
x = [user.ap_id | User.following(user)]
y = [activity.actor] ++ activity.data["to"] ++ (activity.data["cc"] || [])
- visible_for_user?(activity, nil) || Enum.any?(x, &(&1 in y))
+ is_public?(activity) || Enum.any?(x, &(&1 in y))
end
def entire_thread_visible_for_user?(%Activity{} = activity, %User{} = user) do
diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex
index 67222ebae..9821173d0 100644
--- a/lib/pleroma/web/admin_api/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/admin_api_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.AdminAPIController do
@@ -10,12 +10,16 @@ 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
+ alias Pleroma.Stats
alias Pleroma.User
alias Pleroma.UserInviteToken
alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Builder
+ alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.AdminAPI.AccountView
@@ -26,7 +30,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
alias Pleroma.Web.AdminAPI.Search
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Endpoint
+ alias Pleroma.Web.MastodonAPI.AppView
alias Pleroma.Web.MastodonAPI.StatusView
+ alias Pleroma.Web.OAuth.App
alias Pleroma.Web.Router
require Logger
@@ -37,7 +43,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
plug(
OAuthScopesPlug,
%{scopes: ["read:accounts"], admin: true}
- when action in [:list_users, :user_show, :right_get]
+ when action in [:list_users, :user_show, :right_get, :show_user_credentials]
)
plug(
@@ -45,6 +51,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
%{scopes: ["write:accounts"], admin: true}
when action in [
:get_password_reset,
+ :force_password_reset,
:user_delete,
:users_create,
:user_toggle_activation,
@@ -53,7 +60,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
:tag_users,
:untag_users,
:right_add,
- :right_delete
+ :right_add_multiple,
+ :right_delete,
+ :disable_mfa,
+ :right_delete_multiple,
+ :update_user_credentials
]
)
@@ -80,13 +91,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
plug(
OAuthScopesPlug,
%{scopes: ["write:reports"], admin: true}
- when action in [:reports_update]
+ when action in [:reports_update, :report_notes_create, :report_notes_delete]
)
plug(
OAuthScopesPlug,
%{scopes: ["read:statuses"], admin: true}
- when action == :list_user_statuses
+ when action in [:list_statuses, :list_user_statuses, :list_instance_statuses, :status_show]
)
plug(
@@ -98,34 +109,48 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
plug(
OAuthScopesPlug,
%{scopes: ["read"], admin: true}
- when action in [:config_show, :list_log]
+ when action in [
+ :config_show,
+ :list_log,
+ :stats,
+ :relay_list,
+ :config_descriptions,
+ :need_reboot
+ ]
)
plug(
OAuthScopesPlug,
%{scopes: ["write"], admin: true}
- when action == :config_update
+ when action in [
+ :restart,
+ :config_update,
+ :resend_confirmation_email,
+ :confirm_email,
+ :oauth_app_create,
+ :oauth_app_list,
+ :oauth_app_update,
+ :oauth_app_delete,
+ :reload_emoji
+ ]
)
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"
- })
-
- 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,
@@ -243,21 +268,24 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
end
def list_instance_statuses(conn, %{"instance" => instance} = params) do
+ with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
{page, page_size} = page_params(params)
activities =
- ActivityPub.fetch_instance_activities(%{
+ ActivityPub.fetch_statuses(nil, %{
"instance" => instance,
"limit" => page_size,
- "offset" => (page - 1) * 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})
+ |> render("index.json", %{activities: activities, as: :activity, skip_relationships: false})
end
def list_user_statuses(conn, %{"nickname" => nickname} = params) do
+ with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
godmode = params["godmode"] == "true" || params["godmode"] == true
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
@@ -266,12 +294,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
activities =
ActivityPub.fetch_user_activities(user, nil, %{
"limit" => page_size,
- "godmode" => godmode
+ "godmode" => godmode,
+ "exclude_reblogs" => !with_reblogs && "true"
})
conn
|> put_view(StatusView)
- |> render("index.json", %{activities: activities, as: :activity})
+ |> render("index.json", %{activities: activities, as: :activity, skip_relationships: false})
else
_ -> {:error, :not_found}
end
@@ -364,29 +393,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)
@@ -570,9 +582,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
@doc "Sends registration invite via email"
def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do
- with true <-
- Config.get([:instance, :invites_enabled]) &&
- !Config.get([:instance, :registrations_open]),
+ with {_, false} <- {:registrations_open, Config.get([:instance, :registrations_open])},
+ {_, true} <- {:invites_enabled, Config.get([:instance, :invites_enabled])},
{:ok, invite_token} <- UserInviteToken.create_invite(),
email <-
Pleroma.Emails.UserEmail.user_invitation_email(
@@ -583,6 +594,18 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
),
{:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do
json_response(conn, :no_content, "")
+ else
+ {:registrations_open, _} ->
+ errors(
+ conn,
+ {:error, "To send invites you need to set the `registrations_open` option to false."}
+ )
+
+ {:invites_enabled, _} ->
+ errors(
+ conn,
+ {:error, "To send invites you need to set the `invites_enabled` option to true."}
+ )
end
end
@@ -653,6 +676,64 @@ 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
+ conn
+ |> put_view(AccountView)
+ |> render("credentials.json", %{user: user, for: admin})
+ else
+ _ -> {:error, :not_found}
+ end
+ end
+
+ @doc "Updates a given user"
+ def update_user_credentials(
+ %{assigns: %{user: admin}} = conn,
+ %{"nickname" => nickname} = params
+ ) do
+ with {_, user} <- {:user, User.get_cached_by_nickname(nickname)},
+ {:ok, _user} <-
+ User.update_as_admin(user, params) do
+ ModerationLog.insert_log(%{
+ actor: admin,
+ subject: [user],
+ action: "updated_users"
+ })
+
+ if params["password"] do
+ User.force_password_reset_async(user)
+ end
+
+ ModerationLog.insert_log(%{
+ actor: admin,
+ subject: [user],
+ action: "force_password_reset"
+ })
+
+ json(conn, %{status: "success"})
+ else
+ {:error, changeset} ->
+ {_, {error, _}} = Enum.at(changeset.errors, 0)
+ json(conn, %{error: "New password #{error}."})
+
+ _ ->
+ json(conn, %{error: "Unable to change password."})
+ end
+ end
+
def list_reports(conn, params) do
{page, page_size} = page_params(params)
@@ -663,14 +744,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|> render("index.json", %{reports: reports})
end
- def list_grouped_reports(conn, _params) do
- statuses = Utils.get_reported_activities()
-
- conn
- |> put_view(ReportView)
- |> render("index_grouped.json", Utils.get_reports_grouped_by_status(statuses))
- end
-
def report_show(conn, %{"id" => id}) do
with %Activity{} = report <- Activity.get_by_id(id) do
conn
@@ -740,16 +813,51 @@ 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_show(conn, %{"id" => id}) do
+ with %Activity{} = activity <- Activity.get_by_id(id) do
+ conn
+ |> put_view(StatusView)
+ |> render("show.json", %{activity: activity})
+ else
+ _ -> errors(conn, {:error, :not_found})
+ end
+ end
+
def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
+ params =
+ params
+ |> Map.take(["sensitive", "visibility"])
+ |> Map.new(fn {key, value} -> {String.to_existing_atom(key), value} end)
+
with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
- {:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"])
+ {:ok, sensitive} = Ecto.Type.cast(:boolean, params[:sensitive])
ModerationLog.insert_log(%{
action: "status_update",
actor: admin,
subject: activity,
sensitive: sensitive,
- visibility: params["visibility"]
+ visibility: params[:visibility]
})
conn
@@ -809,7 +917,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
configs = ConfigDB.get_all_as_keyword()
merged =
- Config.Holder.config()
+ Config.Holder.default_config()
|> ConfigDB.merge(configs)
|> Enum.map(fn {group, values} ->
Enum.map(values, fn {key, value} ->
@@ -839,16 +947,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
end)
|> List.flatten()
- response = %{configs: merged}
-
- response =
- if Restarter.Pleroma.need_reboot?() do
- Map.put(response, :need_reboot, true)
- else
- response
- end
-
- json(conn, response)
+ json(conn, %{configs: merged, need_reboot: Restarter.Pleroma.need_reboot?()})
end
end
@@ -875,28 +974,22 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
Config.TransferTask.load_and_update_env(deleted, false)
- need_reboot? =
- Restarter.Pleroma.need_reboot?() ||
- Enum.any?(updated, fn config ->
+ if !Restarter.Pleroma.need_reboot?() do
+ changed_reboot_settings? =
+ (updated ++ deleted)
+ |> Enum.any?(fn config ->
group = ConfigDB.from_string(config.group)
key = ConfigDB.from_string(config.key)
value = ConfigDB.from_binary(config.value)
Config.TransferTask.pleroma_need_restart?(group, key, value)
end)
- response = %{configs: updated}
-
- response =
- if need_reboot? do
- Restarter.Pleroma.need_reboot()
- Map.put(response, :need_reboot, need_reboot?)
- else
- response
- end
+ if changed_reboot_settings?, do: Restarter.Pleroma.need_reboot()
+ end
conn
|> put_view(ConfigView)
- |> render("index.json", response)
+ |> render("index.json", %{configs: updated, need_reboot: Restarter.Pleroma.need_reboot?()})
end
end
@@ -908,6 +1001,10 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
end
end
+ def need_reboot(conn, _params) do
+ json(conn, %{need_reboot: Restarter.Pleroma.need_reboot?()})
+ end
+
defp configurable_from_database(conn) do
if Config.get(:configurable_from_database) do
:ok
@@ -953,25 +1050,109 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
conn |> json("")
end
- def errors(conn, {:error, :not_found}) do
+ def oauth_app_create(conn, params) do
+ params =
+ if params["name"] do
+ Map.put(params, "client_name", params["name"])
+ else
+ params
+ end
+
+ result =
+ case App.create(params) do
+ {:ok, app} ->
+ AppView.render("show.json", %{app: app, admin: true})
+
+ {:error, changeset} ->
+ App.errors(changeset)
+ end
+
+ json(conn, result)
+ end
+
+ def oauth_app_update(conn, params) do
+ params =
+ if params["name"] do
+ Map.put(params, "client_name", params["name"])
+ else
+ params
+ end
+
+ with {:ok, app} <- App.update(params) do
+ json(conn, AppView.render("show.json", %{app: app, admin: true}))
+ else
+ {:error, changeset} ->
+ json(conn, App.errors(changeset))
+
+ nil ->
+ json_response(conn, :bad_request, "")
+ end
+ end
+
+ def oauth_app_list(conn, params) do
+ {page, page_size} = page_params(params)
+
+ search_params = %{
+ client_name: params["name"],
+ client_id: params["client_id"],
+ page: page,
+ page_size: page_size
+ }
+
+ search_params =
+ if Map.has_key?(params, "trusted") do
+ Map.put(search_params, :trusted, params["trusted"])
+ else
+ search_params
+ end
+
+ with {:ok, apps, count} <- App.search(search_params) do
+ json(
+ conn,
+ AppView.render("index.json",
+ apps: apps,
+ count: count,
+ page_size: page_size,
+ admin: true
+ )
+ )
+ end
+ end
+
+ def oauth_app_delete(conn, params) do
+ with {:ok, _app} <- App.destroy(params["id"]) do
+ json_response(conn, :no_content, "")
+ else
+ _ -> json_response(conn, :bad_request, "")
+ end
+ end
+
+ def stats(conn, _) do
+ count = Stats.get_status_visibility_count()
+
+ conn
+ |> json(%{"status_visibility" => count})
+ end
+
+ defp errors(conn, {:error, :not_found}) do
conn
|> put_status(:not_found)
|> json(dgettext("errors", "Not found"))
end
- def errors(conn, {:error, reason}) do
+ defp errors(conn, {:error, reason}) do
conn
|> put_status(:bad_request)
|> json(reason)
end
- def errors(conn, {:param_cast, _}) do
+ defp errors(conn, {:param_cast, _}) do
conn
|> put_status(:bad_request)
|> json(dgettext("errors", "Invalid parameters"))
end
- def errors(conn, _) do
+ defp errors(conn, _) do
conn
|> put_status(:internal_server_error)
|> json(dgettext("errors", "Something went wrong"))
diff --git a/lib/pleroma/web/admin_api/report.ex b/lib/pleroma/web/admin_api/report.ex
index 9c3468570..8660d6520 100644
--- a/lib/pleroma/web/admin_api/report.ex
+++ b/lib/pleroma/web/admin_api/report.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.Report do
diff --git a/lib/pleroma/web/admin_api/search.ex b/lib/pleroma/web/admin_api/search.ex
index ed919833e..c28efadd5 100644
--- a/lib/pleroma/web/admin_api/search.ex
+++ b/lib/pleroma/web/admin_api/search.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.Search do
@@ -18,7 +18,12 @@ defmodule Pleroma.Web.AdminAPI.Search do
@spec user(map()) :: {:ok, [User.t()], pos_integer()}
def user(params \\ %{}) do
- query = User.Query.build(params) |> order_by([u], u.nickname)
+ query =
+ params
+ |> Map.drop([:page, :page_size])
+ |> Map.put(:exclude_service_users, true)
+ |> User.Query.build()
+ |> order_by([u], u.nickname)
paginated_query =
User.Query.paginate(query, params[:page] || 1, params[:page_size] || @page_size)
diff --git a/lib/pleroma/web/admin_api/views/account_view.ex b/lib/pleroma/web/admin_api/views/account_view.ex
index d9dba5c51..a16a3ebf0 100644
--- a/lib/pleroma/web/admin_api/views/account_view.ex
+++ b/lib/pleroma/web/admin_api/views/account_view.ex
@@ -1,11 +1,10 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.AccountView do
use Pleroma.Web, :view
- alias Pleroma.HTML
alias Pleroma.User
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.MediaProxy
@@ -24,9 +23,47 @@ defmodule Pleroma.Web.AdminAPI.AccountView do
}
end
+ def render("credentials.json", %{user: user, for: for_user}) do
+ user = User.sanitize_html(user, User.html_filter_policy(for_user))
+ avatar = User.avatar_url(user) |> MediaProxy.url()
+ banner = User.banner_url(user) |> MediaProxy.url()
+ background = image_url(user.background) |> MediaProxy.url()
+
+ user
+ |> Map.take([
+ :id,
+ :bio,
+ :email,
+ :fields,
+ :name,
+ :nickname,
+ :locked,
+ :no_rich_text,
+ :default_scope,
+ :hide_follows,
+ :hide_followers_count,
+ :hide_follows_count,
+ :hide_followers,
+ :hide_favorites,
+ :allow_following_move,
+ :show_role,
+ :skip_thread_containment,
+ :pleroma_settings_store,
+ :raw_fields,
+ :discoverable,
+ :actor_type
+ ])
+ |> Map.merge(%{
+ "avatar" => avatar,
+ "banner" => banner,
+ "background" => background
+ })
+ end
+
def render("show.json", %{user: user}) do
avatar = User.avatar_url(user) |> MediaProxy.url()
- display_name = HTML.strip_tags(user.name || user.nickname)
+ display_name = Pleroma.HTML.strip_tags(user.name || user.nickname)
+ user = User.sanitize_html(user, FastSanitize.Sanitizer.StripTags)
%{
"id" => user.id,
@@ -104,4 +141,7 @@ defmodule Pleroma.Web.AdminAPI.AccountView do
""
end
end
+
+ defp image_url(%{"url" => [%{"href" => href} | _]}), do: href
+ defp image_url(_), do: nil
end
diff --git a/lib/pleroma/web/admin_api/views/config_view.ex b/lib/pleroma/web/admin_api/views/config_view.ex
index bbb53efcd..587ef760e 100644
--- a/lib/pleroma/web/admin_api/views/config_view.ex
+++ b/lib/pleroma/web/admin_api/views/config_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.ConfigView do
diff --git a/lib/pleroma/web/admin_api/views/moderation_log_view.ex b/lib/pleroma/web/admin_api/views/moderation_log_view.ex
index e7752d1f3..112f9e0e1 100644
--- a/lib/pleroma/web/admin_api/views/moderation_log_view.ex
+++ b/lib/pleroma/web/admin_api/views/moderation_log_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.ModerationLogView do
diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex
index 4880d2992..d50969b2a 100644
--- a/lib/pleroma/web/admin_api/views/report_view.ex
+++ b/lib/pleroma/web/admin_api/views/report_view.ex
@@ -1,10 +1,10 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.ReportView do
use Pleroma.Web, :view
- alias Pleroma.Activity
+
alias Pleroma.HTML
alias Pleroma.User
alias Pleroma.Web.AdminAPI.Report
@@ -38,38 +38,17 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
actor: merge_account_views(user),
content: content,
created_at: created_at,
- statuses: StatusView.render("index.json", %{activities: statuses, as: :activity}),
+ statuses:
+ StatusView.render("index.json", %{
+ activities: statuses,
+ as: :activity,
+ skip_relationships: false
+ }),
state: report.data["state"],
notes: render(__MODULE__, "index_notes.json", %{notes: report.report_notes})
}
end
- def render("index_grouped.json", %{groups: groups}) do
- reports =
- Enum.map(groups, fn group ->
- status =
- case group.status do
- %Activity{} = activity -> StatusView.render("show.json", %{activity: activity})
- _ -> group.status
- end
-
- %{
- date: group[:date],
- account: group[:account],
- status: Map.put_new(status, "deleted", false),
- actors: Enum.map(group[:actors], &merge_account_views/1),
- reports:
- group[:reports]
- |> Enum.map(&Report.extract_report_info(&1))
- |> Enum.map(&render(__MODULE__, "show.json", &1))
- }
- end)
-
- %{
- reports: reports
- }
- end
-
def render("index_notes.json", %{notes: notes}) when is_list(notes) do
Enum.map(notes, &render(__MODULE__, "show_note.json", &1))
end
diff --git a/lib/pleroma/web/admin_api/views/status_view.ex b/lib/pleroma/web/admin_api/views/status_view.ex
index 6f2b2b09c..3637dee24 100644
--- a/lib/pleroma/web/admin_api/views/status_view.ex
+++ b/lib/pleroma/web/admin_api/views/status_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.StatusView do
@@ -8,15 +8,16 @@ defmodule Pleroma.Web.AdminAPI.StatusView do
require Pleroma.Constants
alias Pleroma.User
+ alias Pleroma.Web.MastodonAPI.StatusView
def render("index.json", opts) do
- render_many(opts.activities, __MODULE__, "show.json", opts)
+ safe_render_many(opts.activities, __MODULE__, "show.json", opts)
end
def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
- user = get_user(activity.data["actor"])
+ user = StatusView.get_user(activity.data["actor"])
- Pleroma.Web.MastodonAPI.StatusView.render("show.json", opts)
+ StatusView.render("show.json", opts)
|> Map.merge(%{account: merge_account_views(user)})
end
@@ -26,17 +27,4 @@ defmodule Pleroma.Web.AdminAPI.StatusView do
end
defp merge_account_views(_), do: %{}
-
- defp get_user(ap_id) do
- cond do
- user = User.get_cached_by_ap_id(ap_id) ->
- user
-
- user = User.get_by_guessed_nickname(ap_id) ->
- user
-
- true ->
- User.error_user(ap_id)
- end
- end
end
diff --git a/lib/pleroma/web/api_spec.ex b/lib/pleroma/web/api_spec.ex
new file mode 100644
index 000000000..79fd5f871
--- /dev/null
+++ b/lib/pleroma/web/api_spec.ex
@@ -0,0 +1,57 @@
+# 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 do
+ alias OpenApiSpex.OpenApi
+ alias OpenApiSpex.Operation
+ alias Pleroma.Web.Endpoint
+ alias Pleroma.Web.Router
+
+ @behaviour OpenApi
+
+ @impl OpenApi
+ def spec do
+ %OpenApi{
+ servers: [
+ # Populate the Server info from a phoenix endpoint
+ OpenApiSpex.Server.from_endpoint(Endpoint)
+ ],
+ info: %OpenApiSpex.Info{
+ title: "Pleroma",
+ description: Application.spec(:pleroma, :description) |> to_string(),
+ version: Application.spec(:pleroma, :vsn) |> to_string()
+ },
+ # populate the paths from a phoenix router
+ paths: OpenApiSpex.Paths.from_router(Router),
+ components: %OpenApiSpex.Components{
+ parameters: %{
+ "accountIdOrNickname" =>
+ Operation.parameter(:id, :path, :string, "Account ID or nickname",
+ example: "123",
+ required: true
+ )
+ },
+ securitySchemes: %{
+ "oAuth" => %OpenApiSpex.SecurityScheme{
+ type: "oauth2",
+ flows: %OpenApiSpex.OAuthFlows{
+ password: %OpenApiSpex.OAuthFlow{
+ authorizationUrl: "/oauth/authorize",
+ tokenUrl: "/oauth/token",
+ scopes: %{
+ "read" => "read",
+ "write" => "write",
+ "follow" => "follow",
+ "push" => "push"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ # discover request/response schemas from path specs
+ |> OpenApiSpex.resolve_schema_modules()
+ end
+end
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
new file mode 100644
index 000000000..f0b558aa5
--- /dev/null
+++ b/lib/pleroma/web/api_spec/helpers.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.Helpers do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+
+ def request_body(description, schema_ref, opts \\ []) do
+ media_types = ["application/json", "multipart/form-data", "application/x-www-form-urlencoded"]
+
+ content =
+ media_types
+ |> Enum.map(fn type ->
+ {type,
+ %OpenApiSpex.MediaType{
+ schema: schema_ref,
+ example: opts[:example],
+ examples: opts[:examples]
+ }}
+ end)
+ |> Enum.into(%{})
+
+ %OpenApiSpex.RequestBody{
+ description: description,
+ content: content,
+ required: opts[:required] || false
+ }
+ end
+
+ def pagination_params do
+ [
+ Operation.parameter(:max_id, :query, :string, "Return items older than this ID"),
+ Operation.parameter(:min_id, :query, :string, "Return the oldest items newer than this ID"),
+ Operation.parameter(
+ :since_id,
+ :query,
+ :string,
+ "Return the newest items newer than this ID"
+ ),
+ Operation.parameter(
+ :limit,
+ :query,
+ %Schema{type: :integer, default: 20},
+ "Maximum number of items to return. Will be ignored if it's more than 40"
+ )
+ ]
+ end
+
+ def empty_object_response do
+ Operation.response("Empty object", "application/json", %Schema{type: :object, example: %{}})
+ end
+
+ 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
new file mode 100644
index 000000000..70069d6f9
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/account_operation.ex
@@ -0,0 +1,688 @@
+# 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.AccountOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Reference
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.Account
+ alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship
+ 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
+
+ import Pleroma.Web.ApiSpec.Helpers
+
+ @spec open_api_operation(atom) :: Operation.t()
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ @spec create_operation() :: Operation.t()
+ def create_operation do
+ %Operation{
+ tags: ["accounts"],
+ summary: "Register an account",
+ description:
+ "Creates a user and account records. Returns an account access token for the app that initiated the request. The app should save this token for later, and should wait for the user to confirm their account by clicking a link in their email inbox.",
+ operationId: "AccountController.create",
+ requestBody: request_body("Parameters", create_request(), required: true),
+ responses: %{
+ 200 => Operation.response("Account", "application/json", create_response()),
+ 400 => Operation.response("Error", "application/json", ApiError),
+ 403 => Operation.response("Error", "application/json", ApiError),
+ 429 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def verify_credentials_operation do
+ %Operation{
+ tags: ["accounts"],
+ description: "Test to make sure that the user token works.",
+ summary: "Verify account credentials",
+ operationId: "AccountController.verify_credentials",
+ security: [%{"oAuth" => ["read:accounts"]}],
+ responses: %{
+ 200 => Operation.response("Account", "application/json", Account)
+ }
+ }
+ end
+
+ def update_credentials_operation do
+ %Operation{
+ tags: ["accounts"],
+ summary: "Update account credentials",
+ description: "Update the user's display and preferences.",
+ operationId: "AccountController.update_credentials",
+ security: [%{"oAuth" => ["write:accounts"]}],
+ requestBody: request_body("Parameters", update_creadentials_request(), required: true),
+ responses: %{
+ 200 => Operation.response("Account", "application/json", Account),
+ 403 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def relationships_operation do
+ %Operation{
+ tags: ["accounts"],
+ summary: "Check relationships to other accounts",
+ operationId: "AccountController.relationships",
+ description: "Find out whether a given account is followed, blocked, muted, etc.",
+ security: [%{"oAuth" => ["read:follows"]}],
+ parameters: [
+ Operation.parameter(
+ :id,
+ :query,
+ %Schema{
+ oneOf: [%Schema{type: :array, items: %Schema{type: :string}}, %Schema{type: :string}]
+ },
+ "Account IDs",
+ example: "123"
+ )
+ ],
+ responses: %{
+ 200 => Operation.response("Account", "application/json", array_of_relationships())
+ }
+ }
+ end
+
+ def show_operation do
+ %Operation{
+ tags: ["accounts"],
+ summary: "Account",
+ operationId: "AccountController.show",
+ description: "View information about a profile.",
+ parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
+ responses: %{
+ 200 => Operation.response("Account", "application/json", Account),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def statuses_operation do
+ %Operation{
+ tags: ["accounts"],
+ summary: "Statuses",
+ operationId: "AccountController.statuses",
+ description:
+ "Statuses posted to the given account. Public (for public statuses only), or user token + `read:statuses` (for private statuses the user is authorized to see)",
+ parameters:
+ [
+ %Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
+ Operation.parameter(:pinned, :query, BooleanLike, "Include only pinned statuses"),
+ Operation.parameter(:tagged, :query, :string, "With tag"),
+ Operation.parameter(
+ :only_media,
+ :query,
+ BooleanLike,
+ "Include only statuses with media attached"
+ ),
+ Operation.parameter(
+ :with_muted,
+ :query,
+ BooleanLike,
+ "Include statuses from muted acccounts."
+ ),
+ Operation.parameter(:exclude_reblogs, :query, BooleanLike, "Exclude reblogs"),
+ Operation.parameter(:exclude_replies, :query, BooleanLike, "Exclude replies"),
+ Operation.parameter(
+ :exclude_visibilities,
+ :query,
+ %Schema{type: :array, items: VisibilityScope},
+ "Exclude visibilities"
+ )
+ ] ++ pagination_params(),
+ responses: %{
+ 200 => Operation.response("Statuses", "application/json", array_of_statuses()),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def followers_operation do
+ %Operation{
+ tags: ["accounts"],
+ summary: "Followers",
+ operationId: "AccountController.followers",
+ 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(),
+ responses: %{
+ 200 => Operation.response("Accounts", "application/json", array_of_accounts())
+ }
+ }
+ end
+
+ def following_operation do
+ %Operation{
+ tags: ["accounts"],
+ summary: "Following",
+ operationId: "AccountController.following",
+ 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(),
+ responses: %{200 => Operation.response("Accounts", "application/json", array_of_accounts())}
+ }
+ end
+
+ def lists_operation do
+ %Operation{
+ tags: ["accounts"],
+ summary: "Lists containing this account",
+ operationId: "AccountController.lists",
+ security: [%{"oAuth" => ["read:lists"]}],
+ description: "User lists that you have added this account to.",
+ parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
+ responses: %{200 => Operation.response("Lists", "application/json", array_of_lists())}
+ }
+ end
+
+ def follow_operation do
+ %Operation{
+ tags: ["accounts"],
+ summary: "Follow",
+ operationId: "AccountController.follow",
+ security: [%{"oAuth" => ["follow", "write:follows"]}],
+ description: "Follow the given account",
+ parameters: [
+ %Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
+ Operation.parameter(
+ :reblogs,
+ :query,
+ BooleanLike,
+ "Receive this account's reblogs in home timeline? Defaults to true."
+ )
+ ],
+ responses: %{
+ 200 => Operation.response("Relationship", "application/json", AccountRelationship),
+ 400 => Operation.response("Error", "application/json", ApiError),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def unfollow_operation do
+ %Operation{
+ tags: ["accounts"],
+ summary: "Unfollow",
+ operationId: "AccountController.unfollow",
+ security: [%{"oAuth" => ["follow", "write:follows"]}],
+ description: "Unfollow the given account",
+ parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
+ responses: %{
+ 200 => Operation.response("Relationship", "application/json", AccountRelationship),
+ 400 => Operation.response("Error", "application/json", ApiError),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def mute_operation do
+ %Operation{
+ tags: ["accounts"],
+ summary: "Mute",
+ operationId: "AccountController.mute",
+ security: [%{"oAuth" => ["follow", "write:mutes"]}],
+ requestBody: request_body("Parameters", mute_request()),
+ description:
+ "Mute the given account. Clients should filter statuses and notifications from this account, if received (e.g. due to a boost in the Home timeline).",
+ parameters: [
+ %Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
+ Operation.parameter(
+ :notifications,
+ :query,
+ %Schema{allOf: [BooleanLike], default: true},
+ "Mute notifications in addition to statuses? Defaults to `true`."
+ )
+ ],
+ responses: %{
+ 200 => Operation.response("Relationship", "application/json", AccountRelationship)
+ }
+ }
+ end
+
+ def unmute_operation do
+ %Operation{
+ tags: ["accounts"],
+ summary: "Unmute",
+ operationId: "AccountController.unmute",
+ security: [%{"oAuth" => ["follow", "write:mutes"]}],
+ description: "Unmute the given account.",
+ parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
+ responses: %{
+ 200 => Operation.response("Relationship", "application/json", AccountRelationship)
+ }
+ }
+ end
+
+ def block_operation do
+ %Operation{
+ tags: ["accounts"],
+ summary: "Block",
+ operationId: "AccountController.block",
+ security: [%{"oAuth" => ["follow", "write:blocks"]}],
+ description:
+ "Block the given account. Clients should filter statuses from this account if received (e.g. due to a boost in the Home timeline)",
+ parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
+ responses: %{
+ 200 => Operation.response("Relationship", "application/json", AccountRelationship)
+ }
+ }
+ end
+
+ def unblock_operation do
+ %Operation{
+ tags: ["accounts"],
+ summary: "Unblock",
+ operationId: "AccountController.unblock",
+ security: [%{"oAuth" => ["follow", "write:blocks"]}],
+ description: "Unblock the given account.",
+ parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
+ responses: %{
+ 200 => Operation.response("Relationship", "application/json", AccountRelationship)
+ }
+ }
+ end
+
+ def follow_by_uri_operation do
+ %Operation{
+ tags: ["accounts"],
+ summary: "Follow by URI",
+ operationId: "AccountController.follows",
+ security: [%{"oAuth" => ["follow", "write:follows"]}],
+ requestBody: request_body("Parameters", follow_by_uri_request(), required: true),
+ responses: %{
+ 200 => Operation.response("Account", "application/json", AccountRelationship),
+ 400 => Operation.response("Error", "application/json", ApiError),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def mutes_operation do
+ %Operation{
+ tags: ["accounts"],
+ summary: "Muted accounts",
+ operationId: "AccountController.mutes",
+ description: "Accounts the user has muted.",
+ security: [%{"oAuth" => ["follow", "read:mutes"]}],
+ responses: %{
+ 200 => Operation.response("Accounts", "application/json", array_of_accounts())
+ }
+ }
+ end
+
+ def blocks_operation do
+ %Operation{
+ tags: ["accounts"],
+ summary: "Blocked users",
+ operationId: "AccountController.blocks",
+ description: "View your blocks. See also accounts/:id/{block,unblock}",
+ security: [%{"oAuth" => ["read:blocks"]}],
+ responses: %{
+ 200 => Operation.response("Accounts", "application/json", array_of_accounts())
+ }
+ }
+ end
+
+ def endorsements_operation do
+ %Operation{
+ tags: ["accounts"],
+ summary: "Endorsements",
+ operationId: "AccountController.endorsements",
+ description: "Not implemented",
+ security: [%{"oAuth" => ["read:accounts"]}],
+ responses: %{
+ 200 => empty_array_response()
+ }
+ }
+ end
+
+ def identity_proofs_operation do
+ %Operation{
+ tags: ["accounts"],
+ summary: "Identity proofs",
+ operationId: "AccountController.identity_proofs",
+ description: "Not implemented",
+ responses: %{
+ 200 => empty_array_response()
+ }
+ }
+ end
+
+ defp create_request do
+ %Schema{
+ title: "AccountCreateRequest",
+ description: "POST body for creating an account",
+ type: :object,
+ properties: %{
+ reason: %Schema{
+ type: :string,
+ 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,
+ description:
+ "The email address to be used for login. Required when `account_activation_required` is enabled.",
+ format: :email
+ },
+ password: %Schema{
+ type: :string,
+ description: "The password to be used for login",
+ format: :password
+ },
+ agreement: %Schema{
+ type: :boolean,
+ 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,
+ 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: ""},
+ captcha_solution: %Schema{
+ type: :string,
+ 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"},
+ token: %Schema{
+ type: :string,
+ description: "Invite token required when the registrations aren't public"
+ }
+ },
+ required: [:username, :password, :agreement],
+ example: %{
+ "username" => "cofe",
+ "email" => "cofe@example.com",
+ "password" => "secret",
+ "agreement" => "true",
+ "bio" => "☕️"
+ }
+ }
+ end
+
+ defp create_response do
+ %Schema{
+ title: "AccountCreateResponse",
+ description: "Response schema for an account",
+ type: :object,
+ properties: %{
+ token_type: %Schema{type: :string},
+ access_token: %Schema{type: :string},
+ scope: %Schema{type: :array, items: %Schema{type: :string}},
+ created_at: %Schema{type: :integer, format: :"date-time"}
+ },
+ example: %{
+ "access_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzk",
+ "created_at" => 1_585_918_714,
+ "scope" => ["read", "write", "follow", "push"],
+ "token_type" => "Bearer"
+ }
+ }
+ end
+
+ defp update_creadentials_request do
+ %Schema{
+ title: "AccountUpdateCredentialsRequest",
+ description: "POST body for creating an account",
+ type: :object,
+ properties: %{
+ bot: %Schema{
+ type: :boolean,
+ description: "Whether the account has a bot flag."
+ },
+ display_name: %Schema{
+ type: :string,
+ description: "The display name to use for the profile."
+ },
+ note: %Schema{type: :string, description: "The account bio."},
+ avatar: %Schema{
+ type: :string,
+ description: "Avatar image encoded using multipart/form-data",
+ format: :binary
+ },
+ header: %Schema{
+ type: :string,
+ description: "Header image encoded using multipart/form-data",
+ format: :binary
+ },
+ locked: %Schema{
+ type: :boolean,
+ description: "Whether manual approval of follow requests is required."
+ },
+ fields_attributes: %Schema{
+ oneOf: [
+ %Schema{type: :array, items: attribute_field()},
+ %Schema{type: :object, additionalProperties: %Schema{type: attribute_field()}}
+ ]
+ },
+ # NOTE: `source` field is not supported
+ #
+ # source: %Schema{
+ # type: :object,
+ # properties: %{
+ # privacy: %Schema{type: :string},
+ # sensitive: %Schema{type: :boolean},
+ # language: %Schema{type: :string}
+ # }
+ # },
+
+ # Pleroma-specific fields
+ no_rich_text: %Schema{
+ type: :boolean,
+ 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_count: %Schema{
+ type: :boolean,
+ description: "user's follower count will be hidden"
+ },
+ hide_follows_count: %Schema{
+ type: :boolean,
+ description: "user's follow count will be hidden"
+ },
+ hide_favorites: %Schema{
+ type: :boolean,
+ description: "user's favorites timeline will be hidden"
+ },
+ show_role: %Schema{
+ type: :boolean,
+ 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,
+ description: "Opaque user settings to be saved on the backend."
+ },
+ skip_thread_containment: %Schema{
+ type: :boolean,
+ description: "Skip filtering out broken threads"
+ },
+ allow_following_move: %Schema{
+ type: :boolean,
+ description: "Allows automatically follow moved following accounts"
+ },
+ pleroma_background_image: %Schema{
+ type: :string,
+ description: "Sets the background image of the user.",
+ format: :binary
+ },
+ discoverable: %Schema{
+ type: :boolean,
+ description:
+ "Discovery of this account in search results and other services is allowed."
+ },
+ actor_type: ActorType
+ },
+ example: %{
+ bot: false,
+ display_name: "cofe",
+ note: "foobar",
+ fields_attributes: [%{name: "foo", value: "bar"}],
+ no_rich_text: false,
+ hide_followers: true,
+ hide_follows: false,
+ hide_followers_count: false,
+ hide_follows_count: false,
+ hide_favorites: false,
+ show_role: false,
+ default_scope: "private",
+ pleroma_settings_store: %{"pleroma-fe" => %{"key" => "val"}},
+ skip_thread_containment: false,
+ allow_following_move: false,
+ discoverable: false,
+ actor_type: "Person"
+ }
+ }
+ end
+
+ def array_of_accounts do
+ %Schema{
+ title: "ArrayOfAccounts",
+ type: :array,
+ items: Account,
+ example: [Account.schema().example]
+ }
+ end
+
+ defp array_of_relationships do
+ %Schema{
+ title: "ArrayOfRelationships",
+ description: "Response schema for account relationships",
+ type: :array,
+ items: AccountRelationship,
+ example: [
+ %{
+ "id" => "1",
+ "following" => true,
+ "showing_reblogs" => true,
+ "followed_by" => true,
+ "blocking" => false,
+ "blocked_by" => true,
+ "muting" => false,
+ "muting_notifications" => false,
+ "requested" => false,
+ "domain_blocking" => false,
+ "subscribing" => false,
+ "endorsed" => true
+ },
+ %{
+ "id" => "2",
+ "following" => true,
+ "showing_reblogs" => true,
+ "followed_by" => true,
+ "blocking" => false,
+ "blocked_by" => true,
+ "muting" => true,
+ "muting_notifications" => false,
+ "requested" => true,
+ "domain_blocking" => false,
+ "subscribing" => false,
+ "endorsed" => false
+ },
+ %{
+ "id" => "3",
+ "following" => true,
+ "showing_reblogs" => true,
+ "followed_by" => true,
+ "blocking" => true,
+ "blocked_by" => false,
+ "muting" => true,
+ "muting_notifications" => false,
+ "requested" => false,
+ "domain_blocking" => true,
+ "subscribing" => true,
+ "endorsed" => false
+ }
+ ]
+ }
+ end
+
+ defp follow_by_uri_request do
+ %Schema{
+ title: "AccountFollowsRequest",
+ description: "POST body for muting an account",
+ type: :object,
+ properties: %{
+ uri: %Schema{type: :string, format: :uri}
+ },
+ required: [:uri]
+ }
+ end
+
+ defp mute_request do
+ %Schema{
+ title: "AccountMuteRequest",
+ description: "POST body for muting an account",
+ type: :object,
+ properties: %{
+ notifications: %Schema{
+ type: :boolean,
+ description: "Mute notifications in addition to statuses? Defaults to true.",
+ default: true
+ }
+ },
+ example: %{
+ "notifications" => true
+ }
+ }
+ 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" => "anotehr list"}
+ ]
+ }
+ end
+
+ defp array_of_statuses do
+ %Schema{
+ title: "ArrayOfStatuses",
+ type: :array,
+ items: Status
+ }
+ end
+
+ defp attribute_field do
+ %Schema{
+ title: "AccountAttributeField",
+ description: "Request schema for account custom fields",
+ type: :object,
+ properties: %{
+ name: %Schema{type: :string},
+ value: %Schema{type: :string}
+ },
+ required: [:name, :value],
+ example: %{
+ "name" => "Website",
+ "value" => "https://pleroma.com"
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/app_operation.ex b/lib/pleroma/web/api_spec/operations/app_operation.ex
new file mode 100644
index 000000000..f6ccd073f
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/app_operation.ex
@@ -0,0 +1,144 @@
+# 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.AppOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Helpers
+
+ @spec open_api_operation(atom) :: Operation.t()
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ @spec create_operation() :: Operation.t()
+ def create_operation do
+ %Operation{
+ tags: ["apps"],
+ summary: "Create an application",
+ description: "Create a new application to obtain OAuth2 credentials",
+ operationId: "AppController.create",
+ requestBody: Helpers.request_body("Parameters", create_request(), required: true),
+ responses: %{
+ 200 => Operation.response("App", "application/json", create_response()),
+ 422 =>
+ Operation.response(
+ "Unprocessable Entity",
+ "application/json",
+ %Schema{
+ type: :object,
+ description:
+ "If a required parameter is missing or improperly formatted, the request will fail.",
+ properties: %{
+ error: %Schema{type: :string}
+ },
+ example: %{
+ "error" => "Validation failed: Redirect URI must be an absolute URI."
+ }
+ }
+ )
+ }
+ }
+ end
+
+ def verify_credentials_operation do
+ %Operation{
+ tags: ["apps"],
+ summary: "Verify your app works",
+ description: "Confirm that the app's OAuth2 credentials work.",
+ operationId: "AppController.verify_credentials",
+ security: [%{"oAuth" => ["read"]}],
+ responses: %{
+ 200 =>
+ Operation.response("App", "application/json", %Schema{
+ type: :object,
+ description:
+ "If the Authorization header was provided with a valid token, you should see your app returned as an Application entity.",
+ properties: %{
+ name: %Schema{type: :string},
+ vapid_key: %Schema{type: :string},
+ website: %Schema{type: :string, nullable: true}
+ },
+ example: %{
+ "name" => "My App",
+ "vapid_key" =>
+ "BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M=",
+ "website" => "https://myapp.com/"
+ }
+ }),
+ 422 =>
+ Operation.response(
+ "Unauthorized",
+ "application/json",
+ %Schema{
+ type: :object,
+ description:
+ "If the Authorization header contains an invalid token, is malformed, or is not present, an error will be returned indicating an authorization failure.",
+ properties: %{
+ error: %Schema{type: :string}
+ },
+ example: %{
+ "error" => "The access token is invalid."
+ }
+ }
+ )
+ }
+ }
+ end
+
+ defp create_request do
+ %Schema{
+ title: "AppCreateRequest",
+ description: "POST body for creating an app",
+ type: :object,
+ properties: %{
+ client_name: %Schema{type: :string, description: "A name for your application."},
+ redirect_uris: %Schema{
+ type: :string,
+ description:
+ "Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter."
+ },
+ scopes: %Schema{
+ type: :string,
+ description: "Space separated list of scopes",
+ default: "read"
+ },
+ website: %Schema{type: :string, description: "A URL to the homepage of your app"}
+ },
+ required: [:client_name, :redirect_uris],
+ example: %{
+ "client_name" => "My App",
+ "redirect_uris" => "https://myapp.com/auth/callback",
+ "website" => "https://myapp.com/"
+ }
+ }
+ end
+
+ defp create_response do
+ %Schema{
+ title: "AppCreateResponse",
+ description: "Response schema for an app",
+ type: :object,
+ properties: %{
+ id: %Schema{type: :string},
+ name: %Schema{type: :string},
+ client_id: %Schema{type: :string},
+ client_secret: %Schema{type: :string},
+ redirect_uri: %Schema{type: :string},
+ vapid_key: %Schema{type: :string},
+ website: %Schema{type: :string, nullable: true}
+ },
+ example: %{
+ "id" => "123",
+ "name" => "My App",
+ "client_id" => "TWhM-tNSuncnqN7DBJmoyeLnk6K3iJJ71KKXxgL1hPM",
+ "client_secret" => "ZEaFUFmF0umgBX1qKJDjaU99Q31lDkOU8NutzTOoliw",
+ "vapid_key" =>
+ "BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M=",
+ "website" => "https://myapp.com/"
+ }
+ }
+ end
+end
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/custom_emoji_operation.ex b/lib/pleroma/web/api_spec/operations/custom_emoji_operation.ex
new file mode 100644
index 000000000..2f812ac77
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/custom_emoji_operation.ex
@@ -0,0 +1,88 @@
+# 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.CustomEmojiOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.Emoji
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def index_operation do
+ %Operation{
+ tags: ["custom_emojis"],
+ summary: "List custom custom emojis",
+ description: "Returns custom emojis that are available on the server.",
+ operationId: "CustomEmojiController.index",
+ responses: %{
+ 200 => Operation.response("Custom Emojis", "application/json", resposnse())
+ }
+ }
+ end
+
+ defp resposnse do
+ %Schema{
+ title: "CustomEmojisResponse",
+ description: "Response schema for custom emojis",
+ type: :array,
+ items: custom_emoji(),
+ example: [
+ %{
+ "category" => "Fun",
+ "shortcode" => "blank",
+ "static_url" => "https://lain.com/emoji/blank.png",
+ "tags" => ["Fun"],
+ "url" => "https://lain.com/emoji/blank.png",
+ "visible_in_picker" => false
+ },
+ %{
+ "category" => "Gif,Fun",
+ "shortcode" => "firefox",
+ "static_url" => "https://lain.com/emoji/Firefox.gif",
+ "tags" => ["Gif", "Fun"],
+ "url" => "https://lain.com/emoji/Firefox.gif",
+ "visible_in_picker" => true
+ },
+ %{
+ "category" => "pack:mixed",
+ "shortcode" => "sadcat",
+ "static_url" => "https://lain.com/emoji/mixed/sadcat.png",
+ "tags" => ["pack:mixed"],
+ "url" => "https://lain.com/emoji/mixed/sadcat.png",
+ "visible_in_picker" => true
+ }
+ ]
+ }
+ end
+
+ defp custom_emoji do
+ %Schema{
+ title: "CustomEmoji",
+ description: "Schema for a CustomEmoji",
+ allOf: [
+ Emoji,
+ %Schema{
+ type: :object,
+ properties: %{
+ category: %Schema{type: :string},
+ tags: %Schema{type: :array}
+ }
+ }
+ ],
+ example: %{
+ "category" => "Fun",
+ "shortcode" => "aaaa",
+ "url" =>
+ "https://files.mastodon.social/custom_emojis/images/000/007/118/original/aaaa.png",
+ "static_url" =>
+ "https://files.mastodon.social/custom_emojis/images/000/007/118/static/aaaa.png",
+ "visible_in_picker" => true,
+ "tags" => ["Gif", "Fun"]
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/domain_block_operation.ex b/lib/pleroma/web/api_spec/operations/domain_block_operation.ex
new file mode 100644
index 000000000..049bcf931
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/domain_block_operation.ex
@@ -0,0 +1,83 @@
+# 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.DomainBlockOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ 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: ["domain_blocks"],
+ summary: "Fetch domain blocks",
+ description: "View domains the user has blocked.",
+ security: [%{"oAuth" => ["follow", "read:blocks"]}],
+ operationId: "DomainBlockController.index",
+ responses: %{
+ 200 =>
+ Operation.response("Domain blocks", "application/json", %Schema{
+ description: "Response schema for domain blocks",
+ type: :array,
+ items: %Schema{type: :string},
+ example: ["google.com", "facebook.com"]
+ })
+ }
+ }
+ end
+
+ def create_operation do
+ %Operation{
+ tags: ["domain_blocks"],
+ summary: "Block a domain",
+ description: """
+ Block a domain to:
+
+ - hide all public posts from it
+ - hide all notifications from it
+ - remove all followers from it
+ - prevent following new users from it (but does not remove existing follows)
+ """,
+ operationId: "DomainBlockController.create",
+ requestBody: domain_block_request(),
+ security: [%{"oAuth" => ["follow", "write:blocks"]}],
+ responses: %{200 => empty_object_response()}
+ }
+ end
+
+ def delete_operation do
+ %Operation{
+ tags: ["domain_blocks"],
+ summary: "Unblock a domain",
+ description: "Remove a domain block, if it exists in the user's array of blocked domains.",
+ operationId: "DomainBlockController.delete",
+ requestBody: domain_block_request(),
+ security: [%{"oAuth" => ["follow", "write:blocks"]}],
+ responses: %{
+ 200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
+ }
+ }
+ end
+
+ defp domain_block_request do
+ request_body(
+ "Parameters",
+ %Schema{
+ type: :object,
+ properties: %{
+ domain: %Schema{type: :string}
+ },
+ required: [:domain]
+ },
+ required: true,
+ example: %{
+ "domain" => "facebook.com"
+ }
+ )
+ 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..53e57b46b
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/filter_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.FilterOperation 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: ["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{
+ type: :bolean,
+ 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{
+ type: :bolean,
+ description:
+ "Should the server irreversibly drop matching entities from home and notifications?"
+ },
+ whole_word: %Schema{
+ type: :bolean,
+ 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..880bd3f1b
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/instance_operation.ex
@@ -0,0 +1,169 @@
+# 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"}
+ },
+ example: %{
+ "avatar_upload_limit" => 2_000_000,
+ "background_upload_limit" => 4_000_000,
+ "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..06620492a
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/marker_operation.ex
@@ -0,0 +1,140 @@
+# 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,
+ properties: %{
+ last_read_id: %Schema{type: :string}
+ }
+ },
+ home: %Schema{
+ type: :object,
+ properties: %{
+ last_read_id: %Schema{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/notification_operation.ex b/lib/pleroma/web/api_spec/operations/notification_operation.ex
new file mode 100644
index 000000000..64adc5319
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/notification_operation.ex
@@ -0,0 +1,211 @@
+# 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.NotificationOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.Account
+ 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 index_operation do
+ %Operation{
+ tags: ["Notifications"],
+ summary: "Get all notifications",
+ description:
+ "Notifications concerning the user. This API returns Link headers containing links to the next/previous page. However, the links can also be constructed dynamically using query params and `id` values.",
+ operationId: "NotificationController.index",
+ security: [%{"oAuth" => ["read:notifications"]}],
+ parameters:
+ [
+ Operation.parameter(
+ :exclude_types,
+ :query,
+ %Schema{type: :array, items: notification_type()},
+ "Array of types to exclude"
+ ),
+ Operation.parameter(
+ :account_id,
+ :query,
+ %Schema{type: :string},
+ "Return only notifications received from this account"
+ ),
+ Operation.parameter(
+ :exclude_visibilities,
+ :query,
+ %Schema{type: :array, items: VisibilityScope},
+ "Exclude the notifications for activities with the given visibilities"
+ ),
+ Operation.parameter(
+ :include_types,
+ :query,
+ %Schema{type: :array, items: notification_type()},
+ "Include the notifications for activities with the given types"
+ ),
+ Operation.parameter(
+ :with_muted,
+ :query,
+ BooleanLike,
+ "Include the notifications from muted users"
+ )
+ ] ++ pagination_params(),
+ responses: %{
+ 200 =>
+ Operation.response("Array of notifications", "application/json", %Schema{
+ type: :array,
+ items: notification()
+ }),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def show_operation do
+ %Operation{
+ tags: ["Notifications"],
+ summary: "Get a single notification",
+ description: "View information about a notification with a given ID.",
+ operationId: "NotificationController.show",
+ security: [%{"oAuth" => ["read:notifications"]}],
+ parameters: [id_param()],
+ responses: %{
+ 200 => Operation.response("Notification", "application/json", notification())
+ }
+ }
+ end
+
+ def clear_operation do
+ %Operation{
+ tags: ["Notifications"],
+ summary: "Dismiss all notifications",
+ description: "Clear all notifications from the server.",
+ operationId: "NotificationController.clear",
+ security: [%{"oAuth" => ["write:notifications"]}],
+ responses: %{200 => empty_object_response()}
+ }
+ end
+
+ def dismiss_operation do
+ %Operation{
+ tags: ["Notifications"],
+ summary: "Dismiss a single notification",
+ description: "Clear a single notification from the server.",
+ operationId: "NotificationController.dismiss",
+ parameters: [id_param()],
+ security: [%{"oAuth" => ["write:notifications"]}],
+ responses: %{200 => empty_object_response()}
+ }
+ end
+
+ def dismiss_via_body_operation do
+ %Operation{
+ tags: ["Notifications"],
+ summary: "Dismiss a single notification",
+ deprecated: true,
+ description: "Clear a single notification from the server.",
+ operationId: "NotificationController.dismiss_via_body",
+ requestBody:
+ request_body(
+ "Parameters",
+ %Schema{type: :object, properties: %{id: %Schema{type: :string}}},
+ required: true
+ ),
+ security: [%{"oAuth" => ["write:notifications"]}],
+ responses: %{200 => empty_object_response()}
+ }
+ end
+
+ def destroy_multiple_operation do
+ %Operation{
+ tags: ["Notifications"],
+ summary: "Dismiss multiple notifications",
+ operationId: "NotificationController.destroy_multiple",
+ security: [%{"oAuth" => ["write:notifications"]}],
+ parameters: [
+ Operation.parameter(
+ :ids,
+ :query,
+ %Schema{type: :array, items: %Schema{type: :string}},
+ "Array of notification IDs to dismiss",
+ required: true
+ )
+ ],
+ responses: %{200 => empty_object_response()}
+ }
+ end
+
+ defp notification do
+ %Schema{
+ title: "Notification",
+ description: "Response schema for a notification",
+ type: :object,
+ properties: %{
+ id: %Schema{type: :string},
+ type: notification_type(),
+ created_at: %Schema{type: :string, format: :"date-time"},
+ account: %Schema{
+ allOf: [Account],
+ description: "The account that performed the action that generated the notification."
+ },
+ status: %Schema{
+ allOf: [Status],
+ description:
+ "Status that was the object of the notification, e.g. in mentions, reblogs, favourites, or polls.",
+ nullable: true
+ }
+ },
+ example: %{
+ "id" => "34975861",
+ "type" => "mention",
+ "created_at" => "2019-11-23T07:49:02.064Z",
+ "account" => Account.schema().example,
+ "status" => Status.schema().example
+ }
+ }
+ end
+
+ defp notification_type do
+ %Schema{
+ type: :string,
+ enum: [
+ "follow",
+ "favourite",
+ "reblog",
+ "mention",
+ "poll",
+ "pleroma:emoji_reaction",
+ "move",
+ "follow_request"
+ ],
+ description: """
+ The type of event that resulted in the notification.
+
+ - `follow` - Someone followed you
+ - `mention` - Someone mentioned you in their status
+ - `reblog` - Someone boosted one of your statuses
+ - `favourite` - Someone favourited one of your statuses
+ - `poll` - A poll you have voted in or created has ended
+ - `move` - Someone moved their account
+ - `pleroma:emoji_reaction` - Someone reacted with emoji to your status
+ """
+ }
+ end
+
+ defp id_param do
+ Operation.parameter(:id, :path, :string, "Notification ID",
+ example: "123",
+ required: true
+ )
+ end
+end
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..435991037
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex
@@ -0,0 +1,185 @@
+# 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{
+ 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,
+ 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/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
new file mode 100644
index 000000000..da4d50703
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/report_operation.ex
@@ -0,0 +1,78 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.ReportOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Helpers
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def create_operation do
+ %Operation{
+ tags: ["reports"],
+ summary: "File a report",
+ description: "Report problematic users to your moderators",
+ operationId: "ReportController.create",
+ security: [%{"oAuth" => ["follow", "write:reports"]}],
+ requestBody: Helpers.request_body("Parameters", create_request(), required: true),
+ responses: %{
+ 200 => Operation.response("Report", "application/json", create_response()),
+ 400 => Operation.response("Report", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp create_request do
+ %Schema{
+ title: "ReportCreateRequest",
+ description: "POST body for creating a report",
+ type: :object,
+ properties: %{
+ account_id: %Schema{type: :string, description: "ID of the account to report"},
+ status_ids: %Schema{
+ type: :array,
+ items: %Schema{type: :string},
+ description: "Array of Statuses to attach to the report, for context"
+ },
+ comment: %Schema{
+ type: :string,
+ description: "Reason for the report"
+ },
+ forward: %Schema{
+ type: :boolean,
+ default: false,
+ description:
+ "If the account is remote, should the report be forwarded to the remote admin?"
+ }
+ },
+ required: [:account_id],
+ example: %{
+ "account_id" => "123",
+ "status_ids" => ["1337"],
+ "comment" => "bad status!",
+ "forward" => "false"
+ }
+ }
+ end
+
+ defp create_response do
+ %Schema{
+ title: "ReportResponse",
+ type: :object,
+ properties: %{
+ id: %Schema{type: :string, description: "Report ID"},
+ action_taken: %Schema{type: :boolean, description: "Is action taken?"}
+ },
+ example: %{
+ "id" => "123",
+ "action_taken" => false
+ }
+ }
+ end
+end
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..6ea00a9a8
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/search_operation.ex
@@ -0,0 +1,207 @@
+# 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
+
+ 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"
+ )
+ | 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"
+ )
+ | 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..561db3bce
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/status_operation.ex
@@ -0,0 +1,499 @@
+# 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: [
+ Operation.parameter(:with_relationships, :query, BooleanLike, "Include relationships")
+ | 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,
+ 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{
+ type: :array,
+ items: %Schema{type: :string},
+ description: "Array of Attachment ids to be attached as media."
+ },
+ poll: %Schema{
+ 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,
+ description:
+ "Duration the poll should be open, in seconds. Must be provided with `poll[options]`"
+ },
+ multiple: %Schema{type: :boolean, description: "Allow multiple choices?"},
+ hide_totals: %Schema{
+ type: :boolean,
+ description: "Hide vote counts until the poll ends?"
+ }
+ }
+ },
+ in_reply_to_id: %Schema{
+ allOf: [FlakeID],
+ description: "ID of the status being replied to, if status is a reply"
+ },
+ sensitive: %Schema{
+ type: :boolean,
+ description: "Mark status and attached media as sensitive?"
+ },
+ spoiler_text: %Schema{
+ type: :string,
+ 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, description: "ISO 639 language code for this status."},
+ # Pleroma-specific properties:
+ preview: %Schema{
+ type: :boolean,
+ 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,
+ 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,
+ 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{
+ 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{
+ 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{
+ 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
+
+ defp 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..663b8fa11
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/subscription_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.SubscriptionOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Helpers
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+ 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{
+ type: :object,
+ properties: %{
+ alerts: %Schema{
+ type: :object,
+ properties: %{
+ follow: %Schema{type: :boolean, description: "Receive follow notifications?"},
+ favourite: %Schema{
+ type: :boolean,
+ description: "Receive favourite notifications?"
+ },
+ reblog: %Schema{type: :boolean, description: "Receive reblog notifications?"},
+ mention: %Schema{type: :boolean, description: "Receive mention notifications?"},
+ poll: %Schema{type: :boolean, 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{
+ type: :object,
+ properties: %{
+ alerts: %Schema{
+ type: :object,
+ properties: %{
+ follow: %Schema{type: :boolean, description: "Receive follow notifications?"},
+ favourite: %Schema{
+ type: :boolean,
+ description: "Receive favourite notifications?"
+ },
+ reblog: %Schema{type: :boolean, description: "Receive reblog notifications?"},
+ mention: %Schema{type: :boolean, description: "Receive mention notifications?"},
+ poll: %Schema{type: :boolean, 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..1b89035d4
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/timeline_operation.ex
@@ -0,0 +1,199 @@
+# 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(),
+ with_relationships_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: 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(),
+ with_relationships_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(),
+ with_relationships_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(),
+ with_relationships_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 with_relationships_param do
+ Operation.parameter(:with_relationships, :query, BooleanLike, "Include relationships")
+ 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
new file mode 100644
index 000000000..d476b8ef3
--- /dev/null
+++ b/lib/pleroma/web/api_spec/render_error.ex
@@ -0,0 +1,234 @@
+# 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.RenderError do
+ @behaviour Plug
+
+ import Plug.Conn, only: [put_status: 2]
+ import Phoenix.Controller, only: [json: 2]
+ import Pleroma.Web.Gettext
+
+ @impl Plug
+ def init(opts), do: opts
+
+ @impl Plug
+
+ 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)}
+
+ err ->
+ err
+ end)
+
+ conn
+ |> put_status(:bad_request)
+ |> json(%{
+ error: errors |> Enum.map(&message/1) |> Enum.join(" "),
+ errors: errors |> Enum.map(&render_error/1)
+ })
+ end
+
+ defp render_error(error) do
+ pointer = OpenApiSpex.path_to_string(error)
+
+ %{
+ title: "Invalid value",
+ source: %{
+ pointer: pointer
+ },
+ message: OpenApiSpex.Cast.Error.message(error)
+ }
+ end
+
+ defp message(%{reason: :invalid_schema_type, type: type, name: name}) do
+ gettext("%{name} - Invalid schema.type. Got: %{type}.",
+ name: name,
+ type: inspect(type)
+ )
+ end
+
+ defp message(%{reason: :null_value, name: name} = error) do
+ case error.type do
+ nil ->
+ gettext("%{name} - null value.", name: name)
+
+ type ->
+ gettext("%{name} - null value where %{type} expected.",
+ name: name,
+ type: type
+ )
+ end
+ end
+
+ defp message(%{reason: :all_of, meta: %{invalid_schema: invalid_schema}}) do
+ gettext(
+ "Failed to cast value as %{invalid_schema}. Value must be castable using `allOf` schemas listed.",
+ invalid_schema: invalid_schema
+ )
+ end
+
+ defp message(%{reason: :any_of, meta: %{failed_schemas: failed_schemas}}) do
+ gettext("Failed to cast value using any of: %{failed_schemas}.",
+ failed_schemas: failed_schemas
+ )
+ end
+
+ defp message(%{reason: :one_of, meta: %{failed_schemas: failed_schemas}}) do
+ gettext("Failed to cast value to one of: %{failed_schemas}.", failed_schemas: failed_schemas)
+ end
+
+ defp message(%{reason: :min_length, length: length, name: name}) do
+ gettext("%{name} - String length is smaller than minLength: %{length}.",
+ name: name,
+ length: length
+ )
+ end
+
+ defp message(%{reason: :max_length, length: length, name: name}) do
+ gettext("%{name} - String length is larger than maxLength: %{length}.",
+ name: name,
+ length: length
+ )
+ end
+
+ defp message(%{reason: :unique_items, name: name}) do
+ gettext("%{name} - Array items must be unique.", name: name)
+ end
+
+ defp message(%{reason: :min_items, length: min, value: array, name: name}) do
+ gettext("%{name} - Array length %{length} is smaller than minItems: %{min}.",
+ name: name,
+ length: length(array),
+ min: min
+ )
+ end
+
+ defp message(%{reason: :max_items, length: max, value: array, name: name}) do
+ gettext("%{name} - Array length %{length} is larger than maxItems: %{}.",
+ name: name,
+ length: length(array),
+ max: max
+ )
+ end
+
+ defp message(%{reason: :multiple_of, length: multiple, value: count, name: name}) do
+ gettext("%{name} - %{count} is not a multiple of %{multiple}.",
+ name: name,
+ count: count,
+ multiple: multiple
+ )
+ end
+
+ defp message(%{reason: :exclusive_max, length: max, value: value, name: name})
+ when value >= max do
+ gettext("%{name} - %{value} is larger than exclusive maximum %{max}.",
+ name: name,
+ value: value,
+ max: max
+ )
+ end
+
+ defp message(%{reason: :maximum, length: max, value: value, name: name})
+ when value > max do
+ gettext("%{name} - %{value} is larger than inclusive maximum %{max}.",
+ name: name,
+ value: value,
+ max: max
+ )
+ end
+
+ defp message(%{reason: :exclusive_multiple, length: min, value: value, name: name})
+ when value <= min do
+ gettext("%{name} - %{value} is smaller than exclusive minimum %{min}.",
+ name: name,
+ value: value,
+ min: min
+ )
+ end
+
+ defp message(%{reason: :minimum, length: min, value: value, name: name})
+ when value < min do
+ gettext("%{name} - %{value} is smaller than inclusive minimum %{min}.",
+ name: name,
+ value: value,
+ min: min
+ )
+ end
+
+ defp message(%{reason: :invalid_type, type: type, value: value, name: name}) do
+ gettext("%{name} - Invalid %{type}. Got: %{value}.",
+ name: name,
+ value: OpenApiSpex.TermType.type(value),
+ type: type
+ )
+ end
+
+ defp message(%{reason: :invalid_format, format: format, name: name}) do
+ gettext("%{name} - Invalid format. Expected %{format}.", name: name, format: inspect(format))
+ end
+
+ defp message(%{reason: :invalid_enum, name: name}) do
+ gettext("%{name} - Invalid value for enum.", name: name)
+ end
+
+ defp message(%{reason: :polymorphic_failed, type: polymorphic_type}) do
+ gettext("Failed to cast to any schema in %{polymorphic_type}",
+ polymorphic_type: polymorphic_type
+ )
+ end
+
+ defp message(%{reason: :unexpected_field, name: name}) do
+ gettext("Unexpected field: %{name}.", name: safe_string(name))
+ end
+
+ defp message(%{reason: :no_value_for_discriminator, name: field}) do
+ gettext("Value used as discriminator for `%{field}` matches no schemas.", name: field)
+ end
+
+ defp message(%{reason: :invalid_discriminator_value, name: field}) do
+ gettext("No value provided for required discriminator `%{field}`.", name: field)
+ end
+
+ defp message(%{reason: :unknown_schema, name: name}) do
+ gettext("Unknown schema: %{name}.", name: name)
+ end
+
+ defp message(%{reason: :missing_field, name: name}) do
+ gettext("Missing field: %{name}.", name: name)
+ end
+
+ defp message(%{reason: :missing_header, name: name}) do
+ gettext("Missing header: %{name}.", name: name)
+ end
+
+ defp message(%{reason: :invalid_header, name: name}) do
+ gettext("Invalid value for header: %{name}.", name: name)
+ end
+
+ defp message(%{reason: :max_properties, meta: meta}) do
+ gettext(
+ "Object property count %{property_count} is greater than maxProperties: %{max_properties}.",
+ property_count: meta.property_count,
+ max_properties: meta.max_properties
+ )
+ end
+
+ defp message(%{reason: :min_properties, meta: meta}) do
+ gettext(
+ "Object property count %{property_count} is less than minProperties: %{min_properties}",
+ property_count: meta.property_count,
+ min_properties: meta.min_properties
+ )
+ end
+
+ defp safe_string(string) do
+ to_string(string) |> String.slice(0..39)
+ end
+end
diff --git a/lib/pleroma/web/api_spec/schemas/account.ex b/lib/pleroma/web/api_spec/schemas/account.ex
new file mode 100644
index 000000000..d54e2158d
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/account.ex
@@ -0,0 +1,167 @@
+# 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.Account do
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.AccountField
+ alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship
+ alias Pleroma.Web.ApiSpec.Schemas.ActorType
+ alias Pleroma.Web.ApiSpec.Schemas.Emoji
+ alias Pleroma.Web.ApiSpec.Schemas.FlakeID
+ alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
+
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "Account",
+ description: "Response schema for an account",
+ type: :object,
+ properties: %{
+ acct: %Schema{type: :string},
+ avatar_static: %Schema{type: :string, format: :uri},
+ avatar: %Schema{type: :string, format: :uri},
+ bot: %Schema{type: :boolean},
+ created_at: %Schema{type: :string, format: "date-time"},
+ display_name: %Schema{type: :string},
+ emojis: %Schema{type: :array, items: Emoji},
+ fields: %Schema{type: :array, items: AccountField},
+ follow_requests_count: %Schema{type: :integer},
+ followers_count: %Schema{type: :integer},
+ following_count: %Schema{type: :integer},
+ header_static: %Schema{type: :string, format: :uri},
+ header: %Schema{type: :string, format: :uri},
+ id: FlakeID,
+ locked: %Schema{type: :boolean},
+ note: %Schema{type: :string, format: :html},
+ statuses_count: %Schema{type: :integer},
+ url: %Schema{type: :string, format: :uri},
+ username: %Schema{type: :string},
+ pleroma: %Schema{
+ type: :object,
+ properties: %{
+ allow_following_move: %Schema{type: :boolean},
+ background_image: %Schema{type: :string, nullable: true},
+ chat_token: %Schema{type: :string},
+ confirmation_pending: %Schema{type: :boolean},
+ hide_favorites: %Schema{type: :boolean},
+ hide_followers_count: %Schema{type: :boolean},
+ hide_followers: %Schema{type: :boolean},
+ hide_follows_count: %Schema{type: :boolean},
+ hide_follows: %Schema{type: :boolean},
+ is_admin: %Schema{type: :boolean},
+ is_moderator: %Schema{type: :boolean},
+ skip_thread_containment: %Schema{type: :boolean},
+ tags: %Schema{type: :array, items: %Schema{type: :string}},
+ unread_conversation_count: %Schema{type: :integer},
+ notification_settings: %Schema{
+ type: :object,
+ properties: %{
+ followers: %Schema{type: :boolean},
+ follows: %Schema{type: :boolean},
+ non_followers: %Schema{type: :boolean},
+ non_follows: %Schema{type: :boolean},
+ privacy_option: %Schema{type: :boolean}
+ }
+ },
+ relationship: AccountRelationship,
+ settings_store: %Schema{
+ type: :object
+ }
+ }
+ },
+ source: %Schema{
+ type: :object,
+ properties: %{
+ fields: %Schema{type: :array, items: AccountField},
+ note: %Schema{type: :string},
+ privacy: VisibilityScope,
+ sensitive: %Schema{type: :boolean},
+ pleroma: %Schema{
+ type: :object,
+ properties: %{
+ actor_type: ActorType,
+ discoverable: %Schema{type: :boolean},
+ no_rich_text: %Schema{type: :boolean},
+ show_role: %Schema{type: :boolean}
+ }
+ }
+ }
+ }
+ },
+ example: %{
+ "acct" => "foobar",
+ "avatar" => "https://mypleroma.com/images/avi.png",
+ "avatar_static" => "https://mypleroma.com/images/avi.png",
+ "bot" => false,
+ "created_at" => "2020-03-24T13:05:58.000Z",
+ "display_name" => "foobar",
+ "emojis" => [],
+ "fields" => [],
+ "follow_requests_count" => 0,
+ "followers_count" => 0,
+ "following_count" => 1,
+ "header" => "https://mypleroma.com/images/banner.png",
+ "header_static" => "https://mypleroma.com/images/banner.png",
+ "id" => "9tKi3esbG7OQgZ2920",
+ "locked" => false,
+ "note" => "cofe",
+ "pleroma" => %{
+ "allow_following_move" => true,
+ "background_image" => nil,
+ "confirmation_pending" => true,
+ "hide_favorites" => true,
+ "hide_followers" => false,
+ "hide_followers_count" => false,
+ "hide_follows" => false,
+ "hide_follows_count" => false,
+ "is_admin" => false,
+ "is_moderator" => false,
+ "skip_thread_containment" => false,
+ "chat_token" =>
+ "SFMyNTY.g3QAAAACZAAEZGF0YW0AAAASOXRLaTNlc2JHN09RZ1oyOTIwZAAGc2lnbmVkbgYARNplS3EB.Mb_Iaqew2bN1I1o79B_iP7encmVCpTKC4OtHZRxdjKc",
+ "unread_conversation_count" => 0,
+ "tags" => [],
+ "notification_settings" => %{
+ "followers" => true,
+ "follows" => true,
+ "non_followers" => true,
+ "non_follows" => true,
+ "privacy_option" => false
+ },
+ "relationship" => %{
+ "blocked_by" => false,
+ "blocking" => false,
+ "domain_blocking" => false,
+ "endorsed" => false,
+ "followed_by" => false,
+ "following" => false,
+ "id" => "9tKi3esbG7OQgZ2920",
+ "muting" => false,
+ "muting_notifications" => false,
+ "requested" => false,
+ "showing_reblogs" => true,
+ "subscribing" => false
+ },
+ "settings_store" => %{
+ "pleroma-fe" => %{}
+ }
+ },
+ "source" => %{
+ "fields" => [],
+ "note" => "foobar",
+ "pleroma" => %{
+ "actor_type" => "Person",
+ "discoverable" => false,
+ "no_rich_text" => false,
+ "show_role" => true
+ },
+ "privacy" => "public",
+ "sensitive" => false
+ },
+ "statuses_count" => 0,
+ "url" => "https://mypleroma.com/users/foobar",
+ "username" => "foobar"
+ }
+ })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/account_field.ex b/lib/pleroma/web/api_spec/schemas/account_field.ex
new file mode 100644
index 000000000..fa97073a0
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/account_field.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.ApiSpec.Schemas.AccountField do
+ alias OpenApiSpex.Schema
+
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "AccountField",
+ description: "Response schema for account custom fields",
+ type: :object,
+ properties: %{
+ name: %Schema{type: :string},
+ value: %Schema{type: :string, format: :html},
+ verified_at: %Schema{type: :string, format: :"date-time", nullable: true}
+ },
+ example: %{
+ "name" => "Website",
+ "value" =>
+ "<a href=\"https://pleroma.com\" rel=\"me nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"\">pleroma.com</span><span class=\"invisible\"></span></a>",
+ "verified_at" => "2019-08-29T04:14:55.571+00:00"
+ }
+ })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/account_relationship.ex b/lib/pleroma/web/api_spec/schemas/account_relationship.ex
new file mode 100644
index 000000000..8b982669e
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/account_relationship.ex
@@ -0,0 +1,44 @@
+# 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.AccountRelationship do
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.FlakeID
+
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "AccountRelationship",
+ description: "Response schema for relationship",
+ type: :object,
+ properties: %{
+ blocked_by: %Schema{type: :boolean},
+ blocking: %Schema{type: :boolean},
+ domain_blocking: %Schema{type: :boolean},
+ endorsed: %Schema{type: :boolean},
+ followed_by: %Schema{type: :boolean},
+ following: %Schema{type: :boolean},
+ id: FlakeID,
+ muting: %Schema{type: :boolean},
+ muting_notifications: %Schema{type: :boolean},
+ requested: %Schema{type: :boolean},
+ showing_reblogs: %Schema{type: :boolean},
+ subscribing: %Schema{type: :boolean}
+ },
+ example: %{
+ "blocked_by" => false,
+ "blocking" => false,
+ "domain_blocking" => false,
+ "endorsed" => false,
+ "followed_by" => false,
+ "following" => false,
+ "id" => "9tKi3esbG7OQgZ2920",
+ "muting" => false,
+ "muting_notifications" => false,
+ "requested" => false,
+ "showing_reblogs" => true,
+ "subscribing" => false
+ }
+ })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/actor_type.ex b/lib/pleroma/web/api_spec/schemas/actor_type.ex
new file mode 100644
index 000000000..ac9b46678
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/actor_type.ex
@@ -0,0 +1,13 @@
+# 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.ActorType do
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "ActorType",
+ type: :string,
+ enum: ["Application", "Group", "Organization", "Person", "Service"]
+ })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/api_error.ex b/lib/pleroma/web/api_spec/schemas/api_error.ex
new file mode 100644
index 000000000..5815df94c
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/api_error.ex
@@ -0,0 +1,19 @@
+# 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.ApiError do
+ alias OpenApiSpex.Schema
+
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "ApiError",
+ description: "Response schema for API error",
+ type: :object,
+ properties: %{error: %Schema{type: :string}},
+ example: %{
+ "error" => "Something went wrong"
+ }
+ })
+end
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..c146c416e
--- /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},
+ 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/boolean_like.ex b/lib/pleroma/web/api_spec/schemas/boolean_like.ex
new file mode 100644
index 000000000..f3bfb74da
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/boolean_like.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.ApiSpec.Schemas.BooleanLike do
+ alias OpenApiSpex.Schema
+
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "BooleanLike",
+ description: """
+ The following values will be treated as `false`:
+ - false
+ - 0
+ - "0",
+ - "f",
+ - "F",
+ - "false",
+ - "FALSE",
+ - "off",
+ - "OFF"
+
+ All other non-null values will be treated as `true`
+ """,
+ anyOf: [
+ %Schema{type: :boolean},
+ %Schema{type: :string},
+ %Schema{type: :integer}
+ ]
+ })
+
+ def after_cast(value, _schmea) do
+ {:ok, Pleroma.Web.ControllerHelper.truthy_param?(value)}
+ end
+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/emoji.ex b/lib/pleroma/web/api_spec/schemas/emoji.ex
new file mode 100644
index 000000000..26f35e648
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/emoji.ex
@@ -0,0 +1,29 @@
+# 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.Emoji do
+ alias OpenApiSpex.Schema
+
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "Emoji",
+ description: "Response schema for an emoji",
+ type: :object,
+ properties: %{
+ shortcode: %Schema{type: :string},
+ url: %Schema{type: :string, format: :uri},
+ static_url: %Schema{type: :string, format: :uri},
+ visible_in_picker: %Schema{type: :boolean}
+ },
+ example: %{
+ "shortcode" => "fatyoshi",
+ "url" =>
+ "https://files.mastodon.social/custom_emojis/images/000/023/920/original/e57ecb623faa0dc9.png",
+ "static_url" =>
+ "https://files.mastodon.social/custom_emojis/images/000/023/920/static/e57ecb623faa0dc9.png",
+ "visible_in_picker" => true
+ }
+ })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/flake_id.ex b/lib/pleroma/web/api_spec/schemas/flake_id.ex
new file mode 100644
index 000000000..3b5f6477a
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/flake_id.ex
@@ -0,0 +1,14 @@
+# 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.FlakeID do
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "FlakeID",
+ description:
+ "Pleroma uses 128-bit ids as opposed to Mastodon's 64 bits. However just like Mastodon's ids they are lexically sortable strings",
+ type: :string
+ })
+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
new file mode 100644
index 000000000..c62096db0
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/poll.ex
@@ -0,0 +1,82 @@
+# 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.Poll do
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.Emoji
+ alias Pleroma.Web.ApiSpec.Schemas.FlakeID
+
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "Poll",
+ description: "Represents a poll attached to a status",
+ type: :object,
+ properties: %{
+ id: FlakeID,
+ 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
new file mode 100644
index 000000000..8b87cb25b
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/status.ex
@@ -0,0 +1,325 @@
+# 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.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
+
+ OpenApiSpex.schema(%{
+ title: "Status",
+ description: "Response schema for a status",
+ type: :object,
+ properties: %{
+ 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, 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"],
+ 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, 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{
+ 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: 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{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,
+ 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},
+ 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: %{
+ name: %Schema{type: :string},
+ count: %Schema{type: :integer},
+ me: %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{allOf: [Poll], nullable: true, description: "The poll attached to the status"},
+ reblog: %Schema{
+ allOf: [%OpenApiSpex.Reference{"$ref": "#/components/schemas/Status"}],
+ 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"
+ },
+ visibility: %Schema{
+ allOf: [VisibilityScope],
+ description: "Visibility of this status"
+ }
+ },
+ example: %{
+ "account" => %{
+ "acct" => "nick6",
+ "avatar" => "http://localhost:4001/images/avi.png",
+ "avatar_static" => "http://localhost:4001/images/avi.png",
+ "bot" => false,
+ "created_at" => "2020-04-07T19:48:51.000Z",
+ "display_name" => "Test テスト User 6",
+ "emojis" => [],
+ "fields" => [],
+ "followers_count" => 1,
+ "following_count" => 0,
+ "header" => "http://localhost:4001/images/banner.png",
+ "header_static" => "http://localhost:4001/images/banner.png",
+ "id" => "9toJCsKN7SmSf3aj5c",
+ "locked" => false,
+ "note" => "Tester Number 6",
+ "pleroma" => %{
+ "background_image" => nil,
+ "confirmation_pending" => false,
+ "hide_favorites" => true,
+ "hide_followers" => false,
+ "hide_followers_count" => false,
+ "hide_follows" => false,
+ "hide_follows_count" => false,
+ "is_admin" => false,
+ "is_moderator" => false,
+ "relationship" => %{
+ "blocked_by" => false,
+ "blocking" => false,
+ "domain_blocking" => false,
+ "endorsed" => false,
+ "followed_by" => false,
+ "following" => true,
+ "id" => "9toJCsKN7SmSf3aj5c",
+ "muting" => false,
+ "muting_notifications" => false,
+ "requested" => false,
+ "showing_reblogs" => true,
+ "subscribing" => false
+ },
+ "skip_thread_containment" => false,
+ "tags" => []
+ },
+ "source" => %{
+ "fields" => [],
+ "note" => "Tester Number 6",
+ "pleroma" => %{"actor_type" => "Person", "discoverable" => false},
+ "sensitive" => false
+ },
+ "statuses_count" => 1,
+ "url" => "http://localhost:4001/users/nick6",
+ "username" => "nick6"
+ },
+ "application" => %{"name" => "Web", "website" => nil},
+ "bookmarked" => false,
+ "card" => nil,
+ "content" => "foobar",
+ "created_at" => "2020-04-07T19:48:51.000Z",
+ "emojis" => [],
+ "favourited" => false,
+ "favourites_count" => 0,
+ "id" => "9toJCu5YZW7O7gfvH6",
+ "in_reply_to_account_id" => nil,
+ "in_reply_to_id" => nil,
+ "language" => nil,
+ "media_attachments" => [],
+ "mentions" => [],
+ "muted" => false,
+ "pinned" => false,
+ "pleroma" => %{
+ "content" => %{"text/plain" => "foobar"},
+ "conversation_id" => 345_972,
+ "direct_conversation_id" => nil,
+ "emoji_reactions" => [],
+ "expires_at" => nil,
+ "in_reply_to_account_acct" => nil,
+ "local" => true,
+ "spoiler_text" => %{"text/plain" => ""},
+ "thread_muted" => false
+ },
+ "poll" => nil,
+ "reblog" => nil,
+ "reblogged" => false,
+ "reblogs_count" => 0,
+ "replies_count" => 0,
+ "sensitive" => false,
+ "spoiler_text" => "",
+ "tags" => [],
+ "uri" => "http://localhost:4001/objects/0f5dad44-0e9e-4610-b377-a2631e499190",
+ "url" => "http://localhost:4001/notice/9toJCu5YZW7O7gfvH6",
+ "visibility" => "private"
+ }
+ })
+end
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
new file mode 100644
index 000000000..831734e27
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/visibility_scope.ex
@@ -0,0 +1,14 @@
+# 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.VisibilityScope do
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "VisibilityScope",
+ description: "Status visibility",
+ type: :string,
+ enum: ["public", "unlisted", "private", "direct", "list"]
+ })
+end
diff --git a/lib/pleroma/web/auth/authenticator.ex b/lib/pleroma/web/auth/authenticator.ex
index dd49987f7..b4db312fb 100644
--- a/lib/pleroma/web/auth/authenticator.ex
+++ b/lib/pleroma/web/auth/authenticator.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Auth.Authenticator do
diff --git a/lib/pleroma/web/auth/ldap_authenticator.ex b/lib/pleroma/web/auth/ldap_authenticator.ex
index 177c05636..f63a66c03 100644
--- a/lib/pleroma/web/auth/ldap_authenticator.ex
+++ b/lib/pleroma/web/auth/ldap_authenticator.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Auth.LDAPAuthenticator do
diff --git a/lib/pleroma/web/auth/pleroma_authenticator.ex b/lib/pleroma/web/auth/pleroma_authenticator.ex
index f4234b743..a8f554aa3 100644
--- a/lib/pleroma/web/auth/pleroma_authenticator.ex
+++ b/lib/pleroma/web/auth/pleroma_authenticator.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Auth.PleromaAuthenticator do
@@ -19,8 +19,8 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do
{_, true} <- {:checkpw, AuthenticationPlug.checkpw(password, user.password_hash)} 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..04e489c83
--- /dev/null
+++ b/lib/pleroma/web/auth/totp_authenticator.ex
@@ -0,0 +1,44 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 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.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 -> Pbkdf2.verify_pass(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/channels/user_socket.ex b/lib/pleroma/web/channels/user_socket.ex
index 8e2759e3b..306ef1916 100644
--- a/lib/pleroma/web/channels/user_socket.ex
+++ b/lib/pleroma/web/channels/user_socket.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.UserSocket do
diff --git a/lib/pleroma/web/chat_channel.ex b/lib/pleroma/web/chat_channel.ex
index 840414933..38ec774f7 100644
--- a/lib/pleroma/web/chat_channel.ex
+++ b/lib/pleroma/web/chat_channel.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ChatChannel do
diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex
index f7da81b34..3f1a50b96 100644
--- a/lib/pleroma/web/common_api/activity_draft.ex
+++ b/lib/pleroma/web/common_api/activity_draft.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.CommonAPI.ActivityDraft do
@@ -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,16 +84,20 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
%__MODULE__{draft | attachments: attachments}
end
- defp in_reply_to(draft) do
- case Map.get(draft.params, "in_reply_to_status_id") do
- "" -> draft
- nil -> draft
- id -> %__MODULE__{draft | in_reply_to: Activity.get_by_id(id)}
- end
+ 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
+ %__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
+ %__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
@@ -108,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
@@ -140,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(
@@ -160,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
@@ -187,7 +191,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
end
defp preview?(draft) do
- preview? = Pleroma.Web.ControllerHelper.truthy_param?(draft.params["preview"]) || false
+ 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 2a348dcf6..601caeb46 100644
--- a/lib/pleroma/web/common_api/common_api.ex
+++ b/lib/pleroma/web/common_api/common_api.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.CommonAPI do
@@ -7,11 +7,14 @@ defmodule Pleroma.Web.CommonAPI do
alias Pleroma.ActivityExpiration
alias Pleroma.Conversation.Participation
alias Pleroma.FollowingRelationship
+ alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.ThreadMute
alias Pleroma.User
alias Pleroma.UserRelationship
alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Builder
+ alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
@@ -19,6 +22,15 @@ defmodule Pleroma.Web.CommonAPI do
import Pleroma.Web.CommonAPI.Utils
require Pleroma.Constants
+ require Logger
+
+ def unblock(blocker, blocked) do
+ with %Activity{} = 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}
+ end
+ end
def follow(follower, followed) do
timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
@@ -39,10 +51,10 @@ defmodule Pleroma.Web.CommonAPI do
end
def accept_follow_request(follower, followed) do
- with {:ok, follower} <- User.follow(follower, followed),
- %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
+ with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
+ {:ok, follower} <- User.follow(follower, followed),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
- {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept"),
+ {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
{:ok, _activity} <-
ActivityPub.accept(%{
to: [follower.ap_id],
@@ -57,7 +69,8 @@ defmodule Pleroma.Web.CommonAPI do
def reject_follow_request(follower, followed) do
with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
- {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "reject"),
+ {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
+ {:ok, _notifications} <- Notification.dismiss(follow_activity),
{:ok, _activity} <-
ActivityPub.reject(%{
to: [follower.ap_id],
@@ -70,69 +83,136 @@ defmodule Pleroma.Web.CommonAPI do
end
def delete(activity_id, user) do
- with %Activity{data: %{"object" => _}} = 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
- _ -> {: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_or_ap_id, user, params \\ %{}) do
- with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
- object <- Object.normalize(activity),
- announce_activity <- Utils.get_existing_announce(user.ap_id, object),
- public <- public_announce?(object, params) do
+ def repeat(id, user, params \\ %{}) do
+ with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id) do
+ object = Object.normalize(activity)
+ announce_activity = Utils.get_existing_announce(user.ap_id, object)
+ public = public_announce?(object, params)
+
if announce_activity do
{:ok, announce_activity, object}
else
ActivityPub.announce(user, object, nil, true, public)
end
else
- _ -> {:error, dgettext("errors", "Could not repeat")}
+ _ -> {:error, :not_found}
end
end
- def unrepeat(id_or_ap_id, user) do
- with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id) do
- object = Object.normalize(activity)
- ActivityPub.unannounce(user, object)
+ def unrepeat(id, user) do
+ with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
+ {: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")}
end
end
- def favorite(id_or_ap_id, user) do
- with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
- object <- Object.normalize(activity),
- like_activity <- Utils.get_existing_like(user.ap_id, object) do
- if like_activity do
- {:ok, like_activity, object}
- else
- ActivityPub.like(user, object)
- end
+ @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
+ def favorite(%User{} = user, id) do
+ case favorite_helper(user, id) do
+ {:ok, _} = res ->
+ res
+
+ {:error, :not_found} = res ->
+ res
+
+ {:error, e} ->
+ Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
+ {:error, dgettext("errors", "Could not favorite")}
+ end
+ end
+
+ def favorite_helper(user, id) do
+ with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
+ {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
+ {_, {:ok, %Activity{} = activity, _meta}} <-
+ {:common_pipeline,
+ Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
+ {:ok, activity}
else
- _ -> {:error, dgettext("errors", "Could not favorite")}
+ {:find_object, _} ->
+ {:error, :not_found}
+
+ {:common_pipeline,
+ {
+ :error,
+ {
+ :validate_object,
+ {
+ :error,
+ changeset
+ }
+ }
+ }} = e ->
+ if {:object, {"already liked by this actor", []}} in changeset.errors do
+ {:ok, :already_liked}
+ else
+ {:error, e}
+ end
+
+ e ->
+ {:error, e}
end
end
- def unfavorite(id_or_ap_id, user) do
- with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id) do
- object = Object.normalize(activity)
- ActivityPub.unlike(user, object)
+ def unfavorite(id, user) do
+ with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
+ {: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")}
end
end
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")}
@@ -140,8 +220,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")}
@@ -203,7 +285,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)
@@ -213,11 +295,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
@@ -275,7 +357,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?)
@@ -291,32 +373,12 @@ defmodule Pleroma.Web.CommonAPI do
defp maybe_create_activity_expiration(result, _), do: result
- # Updates the emojis for a user based on their profile
- def update(user) do
- emoji = emoji_from_profile(user)
- source_data = Map.put(user.source_data, "tag", emoji)
-
- user =
- case User.update_source_data(user, source_data) do
- {:ok, user} -> user
- _ -> user
- end
-
- ActivityPub.update(%{
- local: true,
- to: [Pleroma.Constants.as_public(), user.follower_address],
- cc: [],
- actor: user.ap_id,
- object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
- })
- end
-
- def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
+ def pin(id, %{ap_id: user_ap_id} = user) do
with %Activity{
actor: ^user_ap_id,
data: %{"type" => "Create"},
object: %Object{data: %{"type" => object_type}}
- } = activity <- get_by_id_or_ap_id(id_or_ap_id),
+ } = activity <- Activity.get_by_id_with_object(id),
true <- object_type in ["Note", "Article", "Question"],
true <- Visibility.is_public?(activity),
{:ok, _user} <- User.add_pinnned_activity(user, activity) do
@@ -327,8 +389,8 @@ defmodule Pleroma.Web.CommonAPI do
end
end
- def unpin(id_or_ap_id, user) do
- with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
+ def unpin(id, user) do
+ with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
{:ok, _user} <- User.remove_pinnned_activity(user, activity) do
{:ok, activity}
else
@@ -353,12 +415,12 @@ defmodule Pleroma.Web.CommonAPI do
def thread_muted?(%{id: nil} = _user, _activity), do: false
def thread_muted?(user, activity) do
- ThreadMute.check_muted(user.id, activity.data["context"]) != []
+ ThreadMute.exists?(user.id, activity.data["context"])
end
- def report(user, %{"account_id" => account_id} = data) do
- with {:ok, account} <- get_reported_account(account_id),
- {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
+ def report(user, data) do
+ with {:ok, account} <- get_reported_account(data.account_id),
+ {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
{:ok, statuses} <- get_report_statuses(account, data) do
ActivityPub.flag(%{
context: Utils.generate_context_id(),
@@ -366,13 +428,11 @@ defmodule Pleroma.Web.CommonAPI do
account: account,
statuses: statuses,
content: content_html,
- forward: data["forward"] || false
+ forward: Map.get(data, :forward, false)
})
end
end
- def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
-
defp get_reported_account(account_id) do
case User.get_cached_by_id(account_id) do
%User{} = account -> {:ok, account}
@@ -406,11 +466,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)
@@ -424,7 +484,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 4445894b0..e8deee223 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.CommonAPI.Utils do
@@ -10,7 +10,6 @@ defmodule Pleroma.Web.CommonAPI.Utils do
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Conversation.Participation
- alias Pleroma.Emoji
alias Pleroma.Formatter
alias Pleroma.Object
alias Pleroma.Plugs.AuthenticationPlug
@@ -18,35 +17,16 @@ defmodule Pleroma.Web.CommonAPI.Utils do
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
- alias Pleroma.Web.Endpoint
alias Pleroma.Web.MediaProxy
require Logger
require Pleroma.Constants
- # This is a hack for twidere.
- def get_by_id_or_ap_id(id) do
- activity =
- with true <- FlakeId.flake_id?(id),
- %Activity{} = activity <- Activity.get_by_id_with_object(id) do
- activity
- else
- _ -> Activity.get_create_by_object_ap_id_with_object(id)
- end
-
- activity &&
- if activity.data["type"] == "Create" do
- activity
- else
- Activity.get_create_by_object_ap_id_with_object(activity.data["object"])
- end
- end
-
- 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
@@ -57,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: []
@@ -71,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(
@@ -160,7 +140,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])
@@ -175,7 +155,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
"replies" => %{"type" => "Collection", "totalItems" => 0}
}
- {note, Map.merge(emoji, Emoji.Formatter.get_emoji_map(option))}
+ {note, Map.merge(emoji, Pleroma.Emoji.Formatter.get_emoji_map(option))}
end)
end_time =
@@ -183,7 +163,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}}
@@ -233,7 +213,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
@@ -331,7 +311,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
def format_input(text, "text/markdown", options) do
text
|> Formatter.mentions_escape(options)
- |> Earmark.as_html!()
+ |> Earmark.as_html!(%Earmark.Options{renderer: Pleroma.EarmarkRenderer})
|> Formatter.linkify(options)
|> Formatter.html_escape("text/html")
end
@@ -422,6 +402,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
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
@@ -431,19 +412,6 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
end
- def emoji_from_profile(%User{bio: bio, name: name}) do
- [bio, name]
- |> Enum.map(&Emoji.Formatter.get_emoji/1)
- |> Enum.concat()
- |> Enum.map(fn {shortcode, %Emoji{file: path}} ->
- %{
- "type" => "Emoji",
- "icon" => %{"type" => "Image", "url" => "#{Endpoint.url()}#{path}"},
- "name" => ":#{shortcode}:"
- }
- end)
- end
-
def maybe_notify_to_recipients(
recipients,
%Activity{data: %{"to" => to, "type" => _type}} = _activity
@@ -537,7 +505,8 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
end
- def get_report_statuses(%User{ap_id: actor}, %{"status_ids" => status_ids}) do
+ def get_report_statuses(%User{ap_id: actor}, %{status_ids: status_ids})
+ when is_list(status_ids) do
{:ok, Activity.all_by_actor_and_id(actor, status_ids)}
end
@@ -591,7 +560,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
limit = Pleroma.Config.get([:instance, :limit])
length = String.length(full_payload)
- if length < limit do
+ if length <= limit do
:ok
else
{:error, dgettext("errors", "The status is over the character limit")}
diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex
index e3d7a465b..eb97ae975 100644
--- a/lib/pleroma/web/controller_helper.ex
+++ b/lib/pleroma/web/controller_helper.ex
@@ -1,14 +1,22 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ControllerHelper do
use Pleroma.Web, :controller
- # As in MastoAPI, per https://api.rubyonrails.org/classes/ActiveModel/Type/Boolean.html
+ 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"]
- def truthy_param?(blank_value) when blank_value in [nil, ""], do: nil
- def truthy_param?(value), do: value not in @falsy_param_values
+
+ def explicitly_falsy_param?(value), do: value in @falsy_param_values
+
+ # Note: `nil` and `""` are considered falsy values in Pleroma
+ def falsy_param?(value),
+ do: explicitly_falsy_param?(value) or value in [nil, ""]
+
+ def truthy_param?(value), do: not falsy_param?(value)
def json_response(conn, status, json) do
conn
@@ -34,7 +42,12 @@ defmodule Pleroma.Web.ControllerHelper do
defp param_to_integer(_, default), do: default
- def add_link_headers(conn, activities, extra_params \\ %{}) do
+ def add_link_headers(conn, activities, extra_params \\ %{})
+
+ def add_link_headers(%{assigns: %{skip_link_headers: true}} = conn, _activities, _extra_params),
+ do: conn
+
+ def add_link_headers(conn, activities, extra_params) do
case List.last(activities) do
%{id: max_id} ->
params =
@@ -69,8 +82,9 @@ defmodule Pleroma.Web.ControllerHelper do
end
end
- def assign_account_by_id(%{params: %{"id" => id}} = conn, _) do
- case Pleroma.User.get_cached_by_id(id) do
+ def assign_account_by_id(conn, _) do
+ # TODO: use `conn.params[:id]` only after moving to OpenAPI
+ case Pleroma.User.get_cached_by_id(conn.params[:id] || conn.params["id"]) do
%Pleroma.User{} = account -> assign(conn, :account, account)
nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
end
@@ -87,7 +101,18 @@ defmodule Pleroma.Web.ControllerHelper do
render_error(conn, :not_implemented, "Can't display this activity")
end
- @spec put_in_if_exist(map(), atom() | String.t(), any) :: map()
- def put_in_if_exist(map, _key, nil), do: map
- def put_in_if_exist(map, key, value), do: put_in(map, key, value)
+ @spec put_if_exist(map(), atom() | String.t(), any) :: map()
+ def put_if_exist(map, _key, nil), do: map
+
+ def put_if_exist(map, key, value), do: Map.put(map, key, value)
+
+ @doc "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
+ end
end
diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex
index a77b73109..226d42c2c 100644
--- a/lib/pleroma/web/endpoint.ex
+++ b/lib/pleroma/web/endpoint.ex
@@ -1,10 +1,12 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Endpoint do
use Phoenix.Endpoint, otp_app: :pleroma
+ require Pleroma.Constants
+
socket("/socket", Pleroma.Web.UserSocket)
plug(Pleroma.Plugs.SetLocalePlug)
@@ -12,7 +14,7 @@ defmodule Pleroma.Web.Endpoint do
plug(Pleroma.Plugs.HTTPSecurityPlug)
plug(Pleroma.Plugs.UploadedMedia)
- @static_cache_control "public max-age=86400 must-revalidate"
+ @static_cache_control "public, no-cache"
# InstanceStatic needs to be before Plug.Static to be able to override shipped-static files
# If you're adding new paths to `only:` you'll need to configure them in InstanceStatic as well
@@ -34,8 +36,7 @@ defmodule Pleroma.Web.Endpoint do
Plug.Static,
at: "/",
from: :pleroma,
- only:
- ~w(index.html robots.txt static finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc),
+ only: Pleroma.Constants.static_only_files(),
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
gzip: true,
cache_control_for_etags: @static_cache_control,
diff --git a/lib/pleroma/web/fallback_redirect_controller.ex b/lib/pleroma/web/fallback_redirect_controller.ex
index 5fbf3695f..0d9d578fc 100644
--- a/lib/pleroma/web/fallback_redirect_controller.ex
+++ b/lib/pleroma/web/fallback_redirect_controller.ex
@@ -1,10 +1,12 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Fallback.RedirectController do
use Pleroma.Web, :controller
+
require Logger
+
alias Pleroma.User
alias Pleroma.Web.Metadata
diff --git a/lib/pleroma/web/federator/federator.ex b/lib/pleroma/web/federator/federator.ex
index f506a7d24..f5803578d 100644
--- a/lib/pleroma/web/federator/federator.ex
+++ b/lib/pleroma/web/federator/federator.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Federator do
@@ -15,13 +15,19 @@ defmodule Pleroma.Web.Federator do
require Logger
- @doc "Addresses [memory leaks on recursive replies fetching](https://git.pleroma.social/pleroma/pleroma/issues/161)"
+ @doc """
+ Returns `true` if the distance to target object does not exceed max configured value.
+ Serves to prevent fetching of very long threads, especially useful on smaller instances.
+ Addresses [memory leaks on recursive replies fetching](https://git.pleroma.social/pleroma/pleroma/issues/161).
+ Applies to fetching of both ancestor (reply-to) and child (reply) objects.
+ """
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
- def allowed_incoming_reply_depth?(depth) do
- max_replies_depth = Pleroma.Config.get([:instance, :federation_incoming_replies_max_depth])
+ def allowed_thread_distance?(distance) do
+ max_distance = Pleroma.Config.get([:instance, :federation_incoming_replies_max_depth])
- if max_replies_depth do
- (depth || 1) <= max_replies_depth
+ if max_distance && max_distance >= 0 do
+ # Default depth is 0 (an object has zero distance from itself in its thread)
+ (distance || 0) <= max_distance
else
true
end
@@ -66,19 +72,24 @@ defmodule Pleroma.Web.Federator do
# actor shouldn't be acting on objects outside their own AP server.
with {:ok, _user} <- ap_enabled_actor(params["actor"]),
nil <- Activity.normalize(params["id"]),
- :ok <- Containment.contain_origin_from_id(params["actor"], params),
+ {_, :ok} <-
+ {:correct_origin?, Containment.contain_origin_from_id(params["actor"], params)},
{:ok, activity} <- Transmogrifier.handle_incoming(params) do
{:ok, activity}
else
+ {:correct_origin?, _} ->
+ Logger.debug("Origin containment failure for #{params["id"]}")
+ {:error, :origin_containment_failed}
+
%Activity{} ->
Logger.debug("Already had #{params["id"]}")
- :error
+ {:error, :already_present}
- _e ->
+ e ->
# Just drop those for now
Logger.debug("Unhandled activity")
Logger.debug(Jason.encode!(params, pretty: true))
- :error
+ {:error, e}
end
end
diff --git a/lib/pleroma/web/federator/publisher.ex b/lib/pleroma/web/federator/publisher.ex
index 1d045c644..ad0201361 100644
--- a/lib/pleroma/web/federator/publisher.ex
+++ b/lib/pleroma/web/federator/publisher.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Federator.Publisher do
diff --git a/lib/pleroma/web/feed/feed_view.ex b/lib/pleroma/web/feed/feed_view.ex
index 334802e0a..1ae03e7e2 100644
--- a/lib/pleroma/web/feed/feed_view.ex
+++ b/lib/pleroma/web/feed/feed_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Feed.FeedView do
@@ -23,7 +23,7 @@ defmodule Pleroma.Web.Feed.FeedView do
def pub_date(%DateTime{} = date), do: Timex.format!(date, "{RFC822}")
def prepare_activity(activity, opts \\ []) do
- object = activity_object(activity)
+ object = Object.normalize(activity)
actor =
if opts[:actor] do
@@ -33,7 +33,6 @@ defmodule Pleroma.Web.Feed.FeedView do
%{
activity: activity,
data: Map.get(object, :data),
- object: object,
actor: actor
}
end
@@ -68,9 +67,7 @@ defmodule Pleroma.Web.Feed.FeedView do
def last_activity(activities), do: List.last(activities)
- def activity_object(activity), do: Object.normalize(activity)
-
- def activity_title(%{data: %{"content" => content}}, opts \\ %{}) do
+ def activity_title(%{"content" => content}, opts \\ %{}) do
content
|> Pleroma.Web.Metadata.Utils.scrub_html()
|> Pleroma.Emoji.Formatter.demojify()
@@ -78,7 +75,7 @@ defmodule Pleroma.Web.Feed.FeedView do
|> escape()
end
- def activity_content(%{data: %{"content" => content}}) do
+ def activity_content(%{"content" => content}) do
content
|> String.replace(~r/[\n\r]/, "")
|> escape()
diff --git a/lib/pleroma/web/feed/tag_controller.ex b/lib/pleroma/web/feed/tag_controller.ex
index 9accd0872..8133f8480 100644
--- a/lib/pleroma/web/feed/tag_controller.ex
+++ b/lib/pleroma/web/feed/tag_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Feed.TagController do
@@ -9,18 +9,18 @@ defmodule Pleroma.Web.Feed.TagController do
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.Feed.FeedView
- import Pleroma.Web.ControllerHelper, only: [put_in_if_exist: 3]
+ import Pleroma.Web.ControllerHelper, only: [put_if_exist: 3]
def feed(conn, %{"tag" => raw_tag} = params) do
{format, tag} = parse_tag(raw_tag)
activities =
%{"type" => ["Create"], "tag" => tag}
- |> put_in_if_exist("max_id", params["max_id"])
+ |> put_if_exist("max_id", params["max_id"])
|> ActivityPub.fetch_public_activities()
conn
- |> put_resp_content_type("application/atom+xml")
+ |> put_resp_content_type("application/#{format}+xml")
|> put_view(FeedView)
|> render("tag.#{format}",
activities: activities,
diff --git a/lib/pleroma/web/feed/user_controller.ex b/lib/pleroma/web/feed/user_controller.ex
index f5096834b..1b72e23dc 100644
--- a/lib/pleroma/web/feed/user_controller.ex
+++ b/lib/pleroma/web/feed/user_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Feed.UserController do
@@ -11,7 +11,7 @@ defmodule Pleroma.Web.Feed.UserController do
alias Pleroma.Web.ActivityPub.ActivityPubController
alias Pleroma.Web.Feed.FeedView
- import Pleroma.Web.ControllerHelper, only: [put_in_if_exist: 3]
+ import Pleroma.Web.ControllerHelper, only: [put_if_exist: 3]
plug(Pleroma.Plugs.SetFormatPlug when action in [:feed_redirect])
@@ -25,7 +25,12 @@ defmodule Pleroma.Web.Feed.UserController do
def feed_redirect(%{assigns: %{format: format}} = conn, _params)
when format in ["json", "activity+json"] do
- ActivityPubController.call(conn, :user)
+ with %{halted: false} = conn <-
+ Pleroma.Plugs.EnsureAuthenticatedPlug.call(conn,
+ unless_func: &Pleroma.Web.FederatingPlug.federating?/1
+ ) do
+ ActivityPubController.call(conn, :user)
+ end
end
def feed_redirect(conn, %{"nickname" => nickname}) do
@@ -35,19 +40,28 @@ defmodule Pleroma.Web.Feed.UserController do
end
def feed(conn, %{"nickname" => nickname} = params) do
+ format = get_format(conn)
+
+ format =
+ if format in ["rss", "atom"] do
+ format
+ else
+ "atom"
+ end
+
with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do
activities =
%{
"type" => ["Create"],
"actor_id" => user.ap_id
}
- |> put_in_if_exist("max_id", params["max_id"])
+ |> put_if_exist("max_id", params["max_id"])
|> ActivityPub.fetch_public_activities()
conn
- |> put_resp_content_type("application/atom+xml")
+ |> put_resp_content_type("application/#{format}+xml")
|> put_view(FeedView)
- |> render("user.xml",
+ |> render("user.#{format}",
user: user,
activities: activities,
feed_config: Pleroma.Config.get([:feed])
diff --git a/lib/pleroma/web/gettext.ex b/lib/pleroma/web/gettext.ex
index 1328b46cc..0adf428ec 100644
--- a/lib/pleroma/web/gettext.ex
+++ b/lib/pleroma/web/gettext.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Gettext do
diff --git a/lib/pleroma/web/masto_fe_controller.ex b/lib/pleroma/web/masto_fe_controller.ex
index 9f7e4943c..d0d8bc8eb 100644
--- a/lib/pleroma/web/masto_fe_controller.ex
+++ b/lib/pleroma/web/masto_fe_controller.ex
@@ -1,23 +1,29 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastoFEController do
use Pleroma.Web, :controller
+ alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :put_settings)
# Note: :index action handles attempt of unauthenticated access to private instance with redirect
+ plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action == :index)
+
plug(
OAuthScopesPlug,
- %{scopes: ["read"], fallback: :proceed_unauthenticated, skip_instance_privacy_check: true}
+ %{scopes: ["read"], fallback: :proceed_unauthenticated}
when action == :index
)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action != :index)
+ plug(
+ :skip_plug,
+ [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :manifest
+ )
@doc "GET /web/*path"
def index(%{assigns: %{user: user, token: token}} = conn, _params)
diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
index 38d14256f..b9ed2d7b2 100644
--- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
@@ -1,14 +1,20 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.AccountController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper,
- only: [add_link_headers: 2, truthy_param?: 1, assign_account_by_id: 2, json_response: 3]
-
- alias Pleroma.Emoji
+ only: [
+ add_link_headers: 2,
+ truthy_param?: 1,
+ assign_account_by_id: 2,
+ json_response: 3,
+ skip_relationships?: 1
+ ]
+
+ alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Plugs.RateLimiter
alias Pleroma.User
@@ -16,20 +22,33 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.ListView
alias Pleroma.Web.MastodonAPI.MastodonAPI
+ alias Pleroma.Web.MastodonAPI.MastodonAPIController
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.TwitterAPI.TwitterAPI
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+ plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create)
+
+ plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:show, :statuses])
+
plug(
OAuthScopesPlug,
%{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
- when action == :show
+ when action in [:show, :followers, :following]
+ )
+
+ plug(
+ OAuthScopesPlug,
+ %{fallback: :proceed_unauthenticated, scopes: ["read:statuses"]}
+ when action == :statuses
)
plug(
OAuthScopesPlug,
%{scopes: ["read:accounts"]}
- when action in [:endorsements, :verify_credentials, :followers, :following]
+ when action in [:verify_credentials, :endorsements, :identity_proofs]
)
plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)
@@ -48,52 +67,36 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
- # Note: :follows (POST /api/v1/follows) is the same as :follow, consider removing :follows
plug(
OAuthScopesPlug,
- %{scopes: ["follow", "write:follows"]} when action in [:follows, :follow, :unfollow]
+ %{scopes: ["follow", "write:follows"]} when action in [:follow_by_uri, :follow, :unfollow]
)
plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
+ @relationship_actions [:follow, :unfollow]
+ @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
+
plug(
- Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
- when action != :create
+ RateLimiter,
+ [name: :relation_id_action, params: ["id", "uri"]] when action in @relationship_actions
)
- @relations [:follow, :unfollow]
- @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
-
- plug(RateLimiter, [name: :relations_id_action, params: ["id", "uri"]] when action in @relations)
- plug(RateLimiter, [name: :relations_actions] when action in @relations)
+ plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
plug(RateLimiter, [name: :app_account_creation] when action == :create)
plug(:assign_account_by_id when action in @needs_account)
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
+
@doc "POST /api/v1/accounts"
- def create(
- %{assigns: %{app: app}} = conn,
- %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
- ) do
- params =
- params
- |> Map.take([
- "email",
- "captcha_solution",
- "captcha_token",
- "captcha_answer_data",
- "token",
- "password"
- ])
- |> Map.put("nickname", nickname)
- |> Map.put("fullname", params["fullname"] || nickname)
- |> Map.put("bio", params["bio"] || "")
- |> Map.put("confirm", params["password"])
-
- with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
+ def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
+ with :ok <- validate_email_param(params),
+ :ok <- TwitterAPI.validate_captcha(app, params),
+ {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
{:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
json(conn, %{
token_type: "Bearer",
@@ -102,7 +105,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
created_at: Token.Utils.format_created_at(token)
})
else
- {:error, errors} -> json_response(conn, :bad_request, errors)
+ {:error, error} -> json_response(conn, :bad_request, %{error: error})
end
end
@@ -114,6 +117,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
render_error(conn, :forbidden, "Invalid credentials")
end
+ defp validate_email_param(%{email: email}) when not is_nil(email), do: :ok
+
+ defp validate_email_param(_) do
+ case Pleroma.Config.get([:instance, :account_activation_required]) do
+ true -> {:error, dgettext("errors", "Missing parameter: %{name}", name: "email")}
+ _ -> :ok
+ end
+ end
+
@doc "GET /api/v1/accounts/verify_credentials"
def verify_credentials(%{assigns: %{user: user}} = conn, _) do
chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
@@ -127,19 +139,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
@doc "PATCH /api/v1/accounts/update_credentials"
- def update_credentials(%{assigns: %{user: original_user}} = conn, params) do
+ def update_credentials(%{assigns: %{user: original_user}, body_params: params} = conn, _params) do
user = original_user
params =
- if Map.has_key?(params, "fields_attributes") do
- Map.update!(params, "fields_attributes", fn fields ->
- fields
- |> normalize_fields_attributes()
- |> Enum.filter(fn %{"name" => n} -> n != "" end)
- end)
- else
- params
- end
+ params
+ |> Enum.filter(fn {_, value} -> not is_nil(value) end)
+ |> Enum.into(%{})
user_params =
[
@@ -156,54 +162,26 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
:discoverable
]
|> Enum.reduce(%{}, fn key, acc ->
- add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)})
+ add_if_present(acc, params, key, key, &{:ok, truthy_param?(&1)})
end)
- |> add_if_present(params, "display_name", :name)
- |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
- |> add_if_present(params, "avatar", :avatar, fn value ->
- with %Plug.Upload{} <- value,
- {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
- {:ok, object.data}
- end
- end)
- |> add_if_present(params, "header", :banner, fn value ->
- with %Plug.Upload{} <- value,
- {:ok, object} <- ActivityPub.upload(value, type: :banner) do
- {:ok, object.data}
- end
- end)
- |> add_if_present(params, "pleroma_background_image", :background, fn value ->
- with %Plug.Upload{} <- value,
- {:ok, object} <- ActivityPub.upload(value, type: :background) do
- {:ok, object.data}
- end
- end)
- |> add_if_present(params, "fields_attributes", :fields, fn fields ->
- fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
-
- {:ok, fields}
- end)
- |> add_if_present(params, "fields_attributes", :raw_fields)
- |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
- {:ok, Map.merge(user.pleroma_settings_store, value)}
- end)
- |> add_if_present(params, "default_scope", :default_scope)
- |> add_if_present(params, "actor_type", :actor_type)
+ |> add_if_present(params, :display_name, :name)
+ |> add_if_present(params, :note, :bio)
+ |> add_if_present(params, :avatar, :avatar)
+ |> add_if_present(params, :header, :banner)
+ |> add_if_present(params, :pleroma_background_image, :background)
+ |> add_if_present(
+ params,
+ :fields_attributes,
+ :raw_fields,
+ &{:ok, normalize_fields_attributes(&1)}
+ )
+ |> add_if_present(params, :pleroma_settings_store, :pleroma_settings_store)
+ |> add_if_present(params, :default_scope, :default_scope)
+ |> add_if_present(params, :actor_type, :actor_type)
- emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
-
- user_emojis =
- user
- |> Map.get(:emoji, [])
- |> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text))
- |> Enum.dedup()
-
- user_params = Map.put(user_params, :emoji, user_emojis)
changeset = User.update_changeset(user, user_params)
with {:ok, user} <- User.update_and_set_cache(changeset) do
- if original_user != user, do: CommonAPI.update(user)
-
render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
else
_e -> render_error(conn, :forbidden, "Invalid request")
@@ -212,7 +190,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
with true <- Map.has_key?(params, params_field),
- {:ok, new_value} <- value_function.(params[params_field]) do
+ {:ok, new_value} <- value_function.(Map.get(params, params_field)) do
Map.put(map, map_field, new_value)
else
_ -> map
@@ -223,12 +201,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
if Enum.all?(fields, &is_tuple/1) do
Enum.map(fields, fn {_, v} -> v end)
else
- fields
+ Enum.map(fields, fn
+ %{} = field -> %{"name" => field.name, "value" => field.value}
+ field -> field
+ end)
end
end
@doc "GET /api/v1/accounts/relationships"
- def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
targets = User.get_all_by_ids(List.wrap(id))
render(conn, "relationships.json", user: user, targets: targets)
@@ -238,7 +219,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
@doc "GET /api/v1/accounts/:id"
- def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
+ def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
true <- User.visible_for?(user, for_user) do
render(conn, "show.json", user: user, for: for_user)
@@ -249,23 +230,38 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
@doc "GET /api/v1/accounts/:id/statuses"
def statuses(%{assigns: %{user: reading_user}} = conn, params) do
- with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
+ with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
+ true <- User.visible_for?(user, reading_user) do
params =
params
- |> Map.put("tag", params["tagged"])
- |> Map.delete("godmode")
+ |> Map.delete(:tagged)
+ |> Enum.filter(&(not is_nil(&1)))
+ |> Map.new(fn {key, value} -> {to_string(key), value} end)
+ |> Map.put("tag", params[:tagged])
activities = ActivityPub.fetch_user_activities(user, reading_user, params)
conn
|> add_link_headers(activities)
|> put_view(StatusView)
- |> render("index.json", activities: activities, for: reading_user, as: :activity)
+ |> render("index.json",
+ activities: activities,
+ for: reading_user,
+ as: :activity,
+ skip_relationships: skip_relationships?(params)
+ )
+ else
+ _e -> render_error(conn, :not_found, "Can't find user")
end
end
@doc "GET /api/v1/accounts/:id/followers"
def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
+ params =
+ params
+ |> Enum.map(fn {key, value} -> {to_string(key), value} end)
+ |> Enum.into(%{})
+
followers =
cond do
for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
@@ -280,6 +276,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
@doc "GET /api/v1/accounts/:id/following"
def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
+ params =
+ params
+ |> Enum.map(fn {key, value} -> {to_string(key), value} end)
+ |> Enum.into(%{})
+
followers =
cond do
for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
@@ -303,11 +304,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
@doc "POST /api/v1/accounts/:id/follow"
def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
- {:error, :not_found}
+ {:error, "Can not follow yourself"}
end
- def follow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
- with {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
+ def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do
+ with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
render(conn, "relationship.json", user: follower, target: followed)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
@@ -316,7 +317,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
@doc "POST /api/v1/accounts/:id/unfollow"
def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
- {:error, :not_found}
+ {:error, "Can not unfollow yourself"}
end
def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
@@ -326,10 +327,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
@doc "POST /api/v1/accounts/:id/mute"
- def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do
- notifications? = params |> Map.get("notifications", true) |> truthy_param?()
-
- with {:ok, _user_relationships} <- User.mute(muter, muted, notifications?) do
+ def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
+ with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
render(conn, "relationship.json", user: muter, target: muted)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
@@ -357,8 +356,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})
@@ -366,14 +364,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
@doc "POST /api/v1/follows"
- def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
- with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
- {_, true} <- {:followed, follower.id != followed.id},
- {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
- render(conn, "show.json", user: followed, for: follower)
- else
- {:followed, _} -> {:error, :not_found}
- {:error, message} -> json_response(conn, :forbidden, %{error: message})
+ def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
+ case User.get_cached_by_nickname(uri) do
+ %User{} = user ->
+ conn
+ |> assign(:account, user)
+ |> follow(%{})
+
+ nil ->
+ {:error, :not_found}
end
end
@@ -390,6 +389,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
@doc "GET /api/v1/endorsements"
- def endorsements(conn, params),
- do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params)
+ def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
+
+ @doc "GET /api/v1/identity_proofs"
+ def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex
index 13a30a34d..a516b6c20 100644
--- a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex
@@ -1,10 +1,11 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.AppController do
use Pleroma.Web, :controller
+ alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Repo
alias Pleroma.Web.OAuth.App
@@ -13,18 +14,28 @@ defmodule Pleroma.Web.MastodonAPI.AppController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+ plug(
+ :skip_plug,
+ [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug]
+ when action == :create
+ )
+
plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :verify_credentials)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
@local_mastodon_name "Mastodon-Local"
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AppOperation
+
@doc "POST /api/v1/apps"
- def create(conn, params) do
+ def create(%{body_params: params} = conn, _params) do
scopes = Scopes.fetch_scopes(params, ["read"])
app_attrs =
params
- |> Map.drop(["scope", "scopes"])
- |> Map.put("scopes", scopes)
+ |> Map.take([:client_name, :redirect_uris, :website])
+ |> Map.put(:scopes, scopes)
with cs <- App.register_changeset(%App{}, app_attrs),
false <- cs.changes[:client_name] == @local_mastodon_name,
diff --git a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
index d9e51de7f..753b3db3e 100644
--- a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.AuthController do
@@ -13,10 +13,10 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
- @local_mastodon_name "Mastodon-Local"
-
plug(Pleroma.Plugs.RateLimiter, [name: :password_reset] when action == :password_reset)
+ @local_mastodon_name "Mastodon-Local"
+
@doc "GET /web/login"
def login(%{assigns: %{user: %User{}}} = conn, _params) do
redirect(conn, to: local_mastodon_root_path(conn))
@@ -86,6 +86,6 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
@spec get_or_make_app() :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
defp get_or_make_app do
%{client_name: @local_mastodon_name, redirect_uris: "."}
- |> App.get_or_make(["read", "write", "follow", "push"])
+ |> App.get_or_make(["read", "write", "follow", "push", "admin"])
end
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex
index 6c0584c54..f35ec3596 100644
--- a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ConversationController do
@@ -13,10 +13,11 @@ 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 == :read)
+ plug(OAuthScopesPlug, %{scopes: ["write:conversations"]} when action != :index)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ConversationOperation
@doc "GET /api/v1/conversations"
def index(%{assigns: %{user: user}} = conn, params) do
@@ -28,7 +29,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationController do
end
@doc "POST /api/v1/conversations/:id/read"
- def 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 391c0648b..c5f47c5df 100644
--- a/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex
@@ -1,10 +1,20 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.CustomEmojiController do
use Pleroma.Web, :controller
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+ plug(
+ :skip_plug,
+ [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]
+ when action == :index
+ )
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.CustomEmojiOperation
+
def index(conn, _params) do
render(conn, "index.json", custom_emojis: Pleroma.Emoji.get_all())
end
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 456fe7ab2..825b231ab 100644
--- a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.DomainBlockController do
@@ -8,6 +8,9 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockController do
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.DomainBlockOperation
+
plug(
OAuthScopesPlug,
%{scopes: ["follow", "read:blocks"]} when action == :index
@@ -18,21 +21,19 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockController do
%{scopes: ["follow", "write:blocks"]} when action != :index
)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
-
@doc "GET /api/v1/domain_blocks"
def index(%{assigns: %{user: user}} = conn, _) do
json(conn, Map.get(user, :domain_blocks, []))
end
@doc "POST /api/v1/domain_blocks"
- def create(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
+ def create(%{assigns: %{user: blocker}, body_params: %{domain: domain}} = conn, _params) do
User.block_domain(blocker, domain)
json(conn, %{})
end
@doc "DELETE /api/v1/domain_blocks"
- def delete(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
+ def delete(%{assigns: %{user: blocker}, body_params: %{domain: domain}} = conn, _params) do
User.unblock_domain(blocker, domain)
json(conn, %{})
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex b/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex
index 41243d5e7..0a257f604 100644
--- a/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.FallbackController do
diff --git a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
index cadef72e1..abbf0ce02 100644
--- a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.FilterController do
@@ -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,62 +18,60 @@ defmodule Pleroma.Web.MastodonAPI.FilterController do
%{scopes: ["write:filters"]} when action not in @oauth_read_actions
)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ 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 3ccbdf1c6..748b6b475 100644
--- a/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.FollowRequestController do
@@ -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,7 +22,7 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestController do
%{scopes: ["follow", "write:follows"]} when action != :index
)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.FollowRequestOperation
@doc "GET /api/v1/follow_requests"
def index(%{assigns: %{user: followed}} = conn, _params) do
@@ -44,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 a55f60fec..d8859731d 100644
--- a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex
@@ -1,10 +1,20 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
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 e0ffdba21..acdc76fd2 100644
--- a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ListController do
@@ -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])
-
- plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action in [:index, :show, :list_accounts])
-
- plug(
- OAuthScopesPlug,
- %{scopes: ["write:lists"]}
- when action in [:create, :update, :delete, :add_to_list, :remove_from_list]
- )
+ @oauth_read_actions [:index, :show, :list_accounts]
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ 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)
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 ce025624d..85310edfa 100644
--- a/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex
@@ -1,11 +1,13 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.MarkerController do
use Pleroma.Web, :controller
alias Pleroma.Plugs.OAuthScopesPlug
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
plug(
OAuthScopesPlug,
%{scopes: ["read:statuses"]}
@@ -13,17 +15,21 @@ defmodule Pleroma.Web.MastodonAPI.MarkerController do
)
plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action == :upsert)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+
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/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
index 7d839a8cf..e7767de4e 100644
--- a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
@@ -1,23 +1,35 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
+ @moduledoc """
+ Contains stubs for unimplemented Mastodon API endpoints.
+
+ Note: instead of routing directly to this controller's action,
+ it's preferable to define an action in relevant (non-generic) controller,
+ set up OAuth rules for it and call this controller's function from it.
+ """
+
use Pleroma.Web, :controller
require Logger
+ plug(
+ :skip_plug,
+ [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]
+ when action in [:empty_array, :empty_object]
+ )
+
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
- # Stubs for unimplemented mastodon api
- #
def empty_array(conn, _) do
- Logger.debug("Unimplemented, returning an empty array")
+ Logger.debug("Unimplemented, returning an empty array (list)")
json(conn, [])
end
def empty_object(conn, _) do
- Logger.debug("Unimplemented, returning an empty object")
+ Logger.debug("Unimplemented, returning an empty object (map)")
json(conn, %{})
end
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex
index ed4c08d99..e36751220 100644
--- a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.MediaController do
@@ -15,8 +15,6 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do
plug(OAuthScopesPlug, %{scopes: ["write:media"]})
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
-
@doc "POST /api/v1/media"
def create(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
with {:ok, object} <-
diff --git a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
index f2508aca4..596b85617 100644
--- a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
@@ -1,11 +1,11 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.NotificationController do
use Pleroma.Web, :controller
- import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
+ import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2, skip_relationships?: 1]
alias Pleroma.Notification
alias Pleroma.Plugs.OAuthScopesPlug
@@ -13,6 +13,8 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
@oauth_read_actions [:show, :index]
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
plug(
OAuthScopesPlug,
%{scopes: ["read:notifications"]} when action in @oauth_read_actions
@@ -20,16 +22,16 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action not in @oauth_read_actions)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.NotificationOperation
# GET /api/v1/notifications
- def index(conn, %{"account_id" => account_id} = params) do
+ def index(conn, %{account_id: account_id} = params) do
case Pleroma.User.get_cached_by_id(account_id) do
%{ap_id: account_ap_id} ->
params =
params
- |> Map.delete("account_id")
- |> Map.put("account_ap_id", account_ap_id)
+ |> Map.delete(:account_id)
+ |> Map.put(:account_ap_id, account_ap_id)
index(conn, params)
@@ -41,15 +43,20 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
end
def index(%{assigns: %{user: user}} = conn, params) do
+ params = Map.new(params, fn {k, v} -> {to_string(k), v} end)
notifications = MastodonAPI.get_notifications(user, params)
conn
|> add_link_headers(notifications)
- |> render("index.json", notifications: notifications, for: user)
+ |> render("index.json",
+ notifications: notifications,
+ for: user,
+ skip_relationships: skip_relationships?(params)
+ )
end
# GET /api/v1/notifications/:id
- def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ def show(%{assigns: %{user: user}} = conn, %{id: id}) do
with {:ok, notification} <- Notification.get(user, id) do
render(conn, "show.json", notification: notification, for: user)
else
@@ -66,8 +73,9 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
json(conn, %{})
end
- # POST /api/v1/notifications/dismiss
- def dismiss(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
+ # POST /api/v1/notifications/:id/dismiss
+
+ def dismiss(%{assigns: %{user: user}} = conn, %{id: id} = _params) do
with {:ok, _notif} <- Notification.dismiss(user, id) do
json(conn, %{})
else
@@ -78,8 +86,13 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
end
end
+ # POST /api/v1/notifications/dismiss (deprecated)
+ def dismiss_via_body(%{body_params: params} = conn, _) do
+ dismiss(conn, params)
+ end
+
# DELETE /api/v1/notifications/destroy_multiple
- def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
+ def destroy_multiple(%{assigns: %{user: user}} = conn, %{ids: ids} = _params) do
Notification.destroy_multiple(user, ids)
json(conn, %{})
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex
index d129f8672..db46ffcfc 100644
--- a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.PollController do
@@ -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,10 +24,10 @@ defmodule Pleroma.Web.MastodonAPI.PollController do
plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action == :vote)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ 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
@@ -37,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 263c2180f..405167108 100644
--- a/lib/pleroma/web/mastodon_api/controllers/report_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/report_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ReportController do
@@ -9,12 +9,13 @@ defmodule Pleroma.Web.MastodonAPI.ReportController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(OAuthScopesPlug, %{scopes: ["write:reports"]} when action == :create)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ReportOperation
@doc "POST /api/v1/reports"
- def create(%{assigns: %{user: user}} = conn, params) do
+ def create(%{assigns: %{user: user}, body_params: params} = conn, _) do
with {:ok, activity} <- Pleroma.Web.CommonAPI.report(user, params) do
render(conn, "show.json", activity: activity)
end
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 ff9276541..1719c67ea 100644
--- a/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ScheduledActivityController do
@@ -11,19 +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(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ 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)
@@ -37,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
@@ -50,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 5a5db8e00..0e0d54ba4 100644
--- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
@@ -1,30 +1,35 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.SearchController do
use Pleroma.Web, :controller
+ import Pleroma.Web.ControllerHelper, only: [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})
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ # Note: on private instances auth is required (EnsurePublicOrAuthenticatedPlug is not skipped)
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
@@ -35,7 +40,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
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" => []}
@@ -43,7 +48,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}
@@ -66,11 +71,12 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
defp search_options(params, user) do
[
- resolve: params["resolve"] == "true",
- following: params["following"] == "true",
- limit: ControllerHelper.fetch_integer_param(params, "limit"),
- offset: ControllerHelper.fetch_integer_param(params, "offset"),
- type: params["type"],
+ skip_relationships: skip_relationships?(params),
+ resolve: params[:resolve],
+ following: params[:following],
+ limit: params[:limit],
+ offset: params[:offset],
+ type: params[:type],
author: get_author(params),
for_user: user
]
@@ -79,12 +85,24 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
defp resource_search(_, "accounts", query, options) do
accounts = with_fallback(fn -> User.search(query, options) end)
- AccountView.render("index.json", users: accounts, for: options[:for_user], as: :user)
+
+ AccountView.render("index.json",
+ users: accounts,
+ for: options[:for_user],
+ as: :user,
+ skip_relationships: false
+ )
end
defp resource_search(_, "statuses", query, options) do
statuses = with_fallback(fn -> Activity.search(options[:for_user], query, options) end)
- StatusView.render("index.json", activities: statuses, for: options[:for_user], as: :activity)
+
+ StatusView.render("index.json",
+ activities: statuses,
+ for: options[:for_user],
+ as: :activity,
+ skip_relationships: options[:skip_relationships]
+ )
end
defp resource_search(:v2, "hashtags", query, _options) do
@@ -121,7 +139,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 287d1631c..25e499a77 100644
--- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
@@ -1,11 +1,12 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.StatusController do
use Pleroma.Web, :controller
- import Pleroma.Web.ControllerHelper, only: [try_render: 3, add_link_headers: 2]
+ import Pleroma.Web.ControllerHelper,
+ only: [try_render: 3, add_link_headers: 2, skip_relationships?: 1]
require Ecto.Query
@@ -23,6 +24,9 @@ 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: []}
plug(
@@ -76,8 +80,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
%{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark]
)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
-
@rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a
plug(
@@ -96,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}) do
+ def index(%{assigns: %{user: user}} = conn, %{ids: ids} = params) do
limit = 100
activities =
@@ -110,7 +114,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|> Activity.all_by_ids_with_object()
|> Enum.filter(&Visibility.visible_for_user?(&1, user))
- render(conn, "index.json", activities: activities, for: user, as: :activity)
+ render(conn, "index.json",
+ activities: activities,
+ for: user,
+ as: :activity,
+ skip_relationships: skip_relationships?(params)
+ )
end
@doc """
@@ -119,20 +128,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
- ) do
- params = Map.put(params, "in_reply_to_status_id", params["in_reply_to_id"])
+ %{
+ 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])
+
+ 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
@@ -144,8 +162,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",
@@ -162,12 +180,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",
@@ -175,20 +194,23 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
for: user,
with_direct_conversation_id: true
)
+ else
+ _ -> {:error, :not_found}
end
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
+ {:error, :not_found} = e -> e
_e -> render_error(conn, :forbidden, "Can't delete this post")
end
end
@doc "POST /api/v1/statuses/:id/reblog"
- def reblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id} = params) do
+ def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do
with {:ok, announce, _activity} <- 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})
@@ -196,45 +218,45 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
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" => ap_id_or_id}) do
- with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
- %Activity{} = activity <- Activity.get_create_by_object_ap_id(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)
end
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),
@@ -244,7 +266,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),
@@ -254,7 +276,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)
@@ -262,7 +284,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)
@@ -271,7 +293,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)
@@ -282,7 +304,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
@@ -302,7 +324,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}} <-
@@ -334,7 +356,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"], %{
@@ -348,16 +370,22 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "GET /api/v1/favourites"
- def favourites(%{assigns: %{user: user}} = conn, params) do
- activities =
- ActivityPub.fetch_favourites(
- user,
- Map.take(params, Pleroma.Pagination.page_keys())
- )
+ def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
+ 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)
+ |> render("index.json",
+ activities: activities,
+ for: user,
+ as: :activity,
+ skip_relationships: skip_relationships?(params)
+ )
end
@doc "GET /api/v1/bookmarks"
@@ -375,6 +403,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
conn
|> add_link_headers(bookmarks)
- |> render("index.json", %{activities: activities, for: user, as: :activity})
+ |> render("index.json",
+ activities: activities,
+ for: user,
+ as: :activity,
+ skip_relationships: skip_relationships?(params)
+ )
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 11f7b85d3..34eac97c5 100644
--- a/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex
@@ -1,52 +1,47 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.SubscriptionController do
@moduledoc "The module represents functions to manage user subscriptions."
use Pleroma.Web, :controller
- alias Pleroma.Web.MastodonAPI.PushSubscriptionView, as: View
alias Pleroma.Web.Push
alias Pleroma.Web.Push.Subscription
action_fallback(:errors)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(:restrict_push_enabled)
plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["push"]})
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ 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
- with true <- Push.enabled(),
- {:ok, _} <- Subscription.delete_if_exists(user, token),
+ 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
- view = View.render("push_subscription.json", subscription: subscription)
- json(conn, view)
+ render(conn, "show.json", subscription: subscription)
end
end
# Gets PushSubscription
# GET /api/v1/push/subscription
#
- def get(%{assigns: %{user: user, token: token}} = conn, _params) do
- with true <- Push.enabled(),
- {:ok, subscription} <- Subscription.get(user, token) do
- view = View.render("push_subscription.json", subscription: subscription)
- json(conn, view)
+ 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
end
# Updates PushSubscription
# PUT /api/v1/push/subscription
#
- def update(%{assigns: %{user: user, token: token}} = conn, params) do
- with true <- Push.enabled(),
- {:ok, subscription} <- Subscription.update(user, token, params) do
- view = View.render("push_subscription.json", subscription: subscription)
- json(conn, view)
+ 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
end
@@ -54,17 +49,26 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do
# DELETE /api/v1/push/subscription
#
def delete(%{assigns: %{user: user, token: token}} = conn, _params) do
- with true <- Push.enabled(),
- {:ok, _response} <- Subscription.delete(user, token),
+ with {:ok, _response} <- Subscription.delete(user, token),
do: json(conn, %{})
end
+ defp restrict_push_enabled(conn, _) do
+ if Push.enabled() do
+ conn
+ else
+ conn
+ |> render_error(:forbidden, "Web push subscription is disabled on this Pleroma instance")
+ |> halt()
+ end
+ end
+
# fallback action
#
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 b9cc8f104..f91df9ab7 100644
--- a/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.SuggestionController do
@@ -7,8 +7,26 @@ defmodule Pleroma.Web.MastodonAPI.SuggestionController do
require Logger
- @doc "GET /api/v1/suggestions"
- def index(conn, _) do
- json(conn, [])
+ 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),
+ do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params)
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
index 29964a1d4..bbd576ffd 100644
--- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
@@ -1,32 +1,54 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
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]
+ only: [add_link_headers: 2, add_link_headers: 3, skip_relationships?: 1]
alias Pleroma.Pagination
+ alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Plugs.OAuthScopesPlug
+ alias Pleroma.Plugs.RateLimiter
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:
+ # https://github.com/phoenixframework/phoenix/commit/2e8c63c01fec4dde5467dbbbf9705ff9e780735e
+
+ plug(RateLimiter, [name: :timeline, bucket_name: :direct_timeline] when action == :direct)
+ plug(RateLimiter, [name: :timeline, bucket_name: :public_timeline] when action == :public)
+ plug(RateLimiter, [name: :timeline, bucket_name: :home_timeline] when action == :home)
+ plug(RateLimiter, [name: :timeline, bucket_name: :hashtag_timeline] when action == :hashtag)
+ plug(RateLimiter, [name: :timeline, bucket_name: :list_timeline] when action == :list)
+
plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in [:home, :direct])
plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :list)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["read:statuses"], fallback: :proceed_unauthenticated}
+ when action in [:public, :hashtag]
+ )
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)
+ |> Map.put("reply_filtering_user", user)
|> Map.put("user", user)
recipients = [user.ap_id | User.following(user)]
@@ -38,13 +60,19 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
conn
|> add_link_headers(activities)
- |> render("index.json", activities: activities, for: user, as: :activity)
+ |> render("index.json",
+ activities: activities,
+ for: user,
+ as: :activity,
+ skip_relationships: skip_relationships?(params)
+ )
end
# GET /api/v1/timelines/direct
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)
@@ -57,27 +85,53 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
conn
|> add_link_headers(activities)
- |> render("index.json", activities: activities, for: user, as: :activity)
+ |> render("index.json",
+ activities: activities,
+ for: user,
+ as: :activity,
+ skip_relationships: skip_relationships?(params)
+ )
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)
- activities =
- params
- |> Map.put("type", ["Create", "Announce"])
- |> Map.put("local_only", local_only)
- |> Map.put("blocking_user", user)
- |> Map.put("muting_user", user)
- |> ActivityPub.fetch_public_activities()
+ local_only = params["local"]
- conn
- |> add_link_headers(activities, %{"local" => local_only})
- |> render("index.json", activities: activities, for: user, as: :activity)
+ cfg_key =
+ if local_only do
+ :local
+ else
+ :federated
+ end
+
+ restrict? = Pleroma.Config.get([:restrict_unauthenticated, :timelines, cfg_key])
+
+ if restrict? and is_nil(user) do
+ render_error(conn, :unauthorized, "authorization required for timeline view")
+ else
+ activities =
+ params
+ |> Map.put("type", ["Create", "Announce"])
+ |> Map.put("local_only", local_only)
+ |> Map.put("blocking_user", user)
+ |> Map.put("muting_user", user)
+ |> Map.put("reply_filtering_user", user)
+ |> ActivityPub.fetch_public_activities()
+
+ conn
+ |> add_link_headers(activities, %{"local" => local_only})
+ |> render("index.json",
+ activities: activities,
+ for: user,
+ as: :activity,
+ skip_relationships: skip_relationships?(params)
+ )
+ end
end
- def hashtag_fetching(params, user, local_only) do
+ defp hashtag_fetching(params, user, local_only) do
tags =
[params["tag"], params["any"]]
|> List.flatten()
@@ -110,20 +164,26 @@ 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
|> add_link_headers(activities, %{"local" => local_only})
- |> render("index.json", activities: activities, for: user, as: :activity)
+ |> render("index.json",
+ activities: activities,
+ for: user,
+ as: :activity,
+ skip_relationships: skip_relationships?(params)
+ )
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)
@@ -140,7 +200,12 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|> ActivityPub.fetch_activities_bounded(following, params)
|> Enum.reverse()
- render(conn, "index.json", activities: activities, for: user, as: :activity)
+ render(conn, "index.json",
+ activities: activities,
+ for: user,
+ as: :activity,
+ skip_relationships: skip_relationships?(params)
+ )
else
_e -> render_error(conn, :forbidden, "Error.")
end
diff --git a/lib/pleroma/web/mastodon_api/mastodon_api.ex b/lib/pleroma/web/mastodon_api/mastodon_api.ex
index 390a2b190..70da64a7a 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
@@ -55,6 +55,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
user
|> Notification.for_user_query(options)
+ |> restrict(:include_types, options)
|> restrict(:exclude_types, options)
|> restrict(:account_ap_id, options)
|> Pagination.fetch_paginated(params)
@@ -69,10 +70,10 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
defp cast_params(params) do
param_types = %{
exclude_types: {:array, :string},
+ include_types: {:array, :string},
exclude_visibilities: {:array, :string},
reblogs: :boolean,
with_muted: :boolean,
- with_move: :boolean,
account_ap_id: :string
}
@@ -80,14 +81,16 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
changeset.changes
end
+ defp restrict(query, :include_types, %{include_types: mastodon_types = [_ | _]}) do
+ ap_types = convert_and_filter_mastodon_types(mastodon_types)
+
+ where(query, [q, a], fragment("? @> ARRAY[?->>'type']::varchar[]", ^ap_types, a.data))
+ end
+
defp restrict(query, :exclude_types, %{exclude_types: mastodon_types = [_ | _]}) do
- ap_types =
- mastodon_types
- |> Enum.map(&Activity.from_mastodon_notification_type/1)
- |> Enum.filter(& &1)
+ ap_types = convert_and_filter_mastodon_types(mastodon_types)
- query
- |> where([q, a], not fragment("? @> ARRAY[?->>'type']::varchar[]", ^ap_types, a.data))
+ where(query, [q, a], not fragment("? @> ARRAY[?->>'type']::varchar[]", ^ap_types, a.data))
end
defp restrict(query, :account_ap_id, %{account_ap_id: account_ap_id}) do
@@ -95,4 +98,10 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
end
defp restrict(query, _, _), do: query
+
+ defp convert_and_filter_mastodon_types(types) do
+ types
+ |> Enum.map(&Activity.from_mastodon_notification_type/1)
+ |> Enum.filter(& &1)
+ end
end
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index c6d37ead7..835dfe9f4 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -1,26 +1,46 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.AccountView do
use Pleroma.Web, :view
- alias Pleroma.HTML
+ alias Pleroma.FollowingRelationship
alias Pleroma.User
+ alias Pleroma.UserRelationship
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MediaProxy
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) ->
+ UserRelationship.view_relationships_option(nil, [])
+
+ true ->
+ UserRelationship.view_relationships_option(reading_user, users)
+ end
+
+ opts = Map.put(opts, :relationships, relationships_opt)
+
users
|> render_many(AccountView, "show.json", opts)
|> Enum.filter(&Enum.any?/1)
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
@@ -28,7 +48,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
id: to_string(user.id),
acct: user.nickname,
username: username_from_nickname(user.nickname),
- url: User.profile_url(user)
+ url: user.uri || user.ap_id
}
end
@@ -36,37 +56,111 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
%{}
end
- def render("relationship.json", %{user: %User{} = user, target: %User{} = target}) do
- follow_state = User.get_cached_follow_state(user, target)
+ def render(
+ "relationship.json",
+ %{user: %User{} = reading_user, target: %User{} = target} = opts
+ ) do
+ user_relationships = get_in(opts, [:relationships, :user_relationships])
+ following_relationships = get_in(opts, [:relationships, :following_relationships])
- requested =
- if follow_state && !User.following?(user, target) do
- follow_state == "pending"
+ follow_state =
+ if following_relationships do
+ user_to_target_following_relation =
+ FollowingRelationship.find(following_relationships, reading_user, target)
+
+ User.get_follow_state(reading_user, target, user_to_target_following_relation)
else
- false
+ User.get_follow_state(reading_user, target)
end
+ followed_by =
+ if following_relationships do
+ case FollowingRelationship.find(following_relationships, target, reading_user) do
+ %{state: :follow_accept} -> true
+ _ -> false
+ end
+ else
+ User.following?(target, reading_user)
+ end
+
+ # NOTE: adjust UserRelationship.view_relationships_option/2 on new relation-related flags
%{
id: to_string(target.id),
- following: User.following?(user, target),
- followed_by: User.following?(target, user),
- blocking: User.blocks_user?(user, target),
- blocked_by: User.blocks_user?(target, user),
- muting: User.mutes?(user, target),
- muting_notifications: User.muted_notifications?(user, target),
- subscribing: User.subscribed_to?(user, target),
- requested: requested,
- domain_blocking: User.blocks_domain?(user, target),
- showing_reblogs: User.showing_reblogs?(user, target),
+ following: follow_state == :follow_accept,
+ followed_by: followed_by,
+ blocking:
+ UserRelationship.exists?(
+ user_relationships,
+ :block,
+ reading_user,
+ target,
+ &User.blocks_user?(&1, &2)
+ ),
+ blocked_by:
+ UserRelationship.exists?(
+ user_relationships,
+ :block,
+ target,
+ reading_user,
+ &User.blocks_user?(&1, &2)
+ ),
+ muting:
+ UserRelationship.exists?(
+ user_relationships,
+ :mute,
+ reading_user,
+ target,
+ &User.mutes?(&1, &2)
+ ),
+ muting_notifications:
+ UserRelationship.exists?(
+ user_relationships,
+ :notification_mute,
+ reading_user,
+ target,
+ &User.muted_notifications?(&1, &2)
+ ),
+ subscribing:
+ UserRelationship.exists?(
+ user_relationships,
+ :inverse_subscription,
+ target,
+ reading_user,
+ &User.subscribed_to?(&2, &1)
+ ),
+ requested: follow_state == :follow_pending,
+ domain_blocking: User.blocks_domain?(reading_user, target),
+ showing_reblogs:
+ not UserRelationship.exists?(
+ user_relationships,
+ :reblog_mute,
+ reading_user,
+ target,
+ &User.muting_reblogs?(&1, &2)
+ ),
endorsed: false
}
end
- def render("relationships.json", %{user: user, targets: targets}) do
- render_many(targets, AccountView, "relationship.json", user: user, as: :target)
+ def render("relationships.json", %{user: user, targets: targets} = opts) do
+ relationships_opt =
+ cond do
+ Map.has_key?(opts, :relationships) ->
+ opts[:relationships]
+
+ is_nil(user) ->
+ UserRelationship.view_relationships_option(nil, [])
+
+ true ->
+ UserRelationship.view_relationships_option(user, targets)
+ end
+
+ render_opts = %{as: :target, user: user, relationships: relationships_opt}
+ render_many(targets, AccountView, "relationship.json", render_opts)
end
defp do_render("show.json", %{user: user} = opts) do
+ user = User.sanitize_html(user, User.html_filter_policy(opts[:for]))
display_name = user.name || user.nickname
image = User.avatar_url(user) |> MediaProxy.url()
@@ -89,29 +183,25 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
bot = user.actor_type in ["Application", "Service"]
emojis =
- (user.source_data["tag"] || [])
- |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
- |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
+ Enum.map(user.emoji, fn {shortcode, url} ->
%{
- "shortcode" => String.trim(name, ":"),
- "url" => MediaProxy.url(url),
- "static_url" => MediaProxy.url(url),
+ "shortcode" => shortcode,
+ "url" => url,
+ "static_url" => url,
"visible_in_picker" => false
}
end)
- fields =
- user
- |> User.fields()
- |> Enum.map(fn %{"name" => name, "value" => value} ->
- %{
- "name" => name,
- "value" => Pleroma.HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
- }
- end)
-
- bio = HTML.filter_tags(user.bio, User.html_filter_policy(opts[:for]))
- relationship = render("relationship.json", %{user: opts[:for], target: user})
+ relationship =
+ if opts[:skip_relationships] do
+ %{}
+ else
+ render("relationship.json", %{
+ user: opts[:for],
+ target: user,
+ relationships: opts[:relationships]
+ })
+ end
%{
id: to_string(user.id),
@@ -123,17 +213,17 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
followers_count: followers_count,
following_count: following_count,
statuses_count: user.note_count,
- note: bio || "",
- url: User.profile_url(user),
+ note: user.bio || "",
+ url: user.uri || user.ap_id,
avatar: image,
avatar_static: image,
header: header,
header_static: header,
emojis: emojis,
- fields: fields,
+ fields: user.fields,
bot: bot,
source: %{
- note: HTML.strip_tags((user.bio || "") |> String.replace("<br>", "\n")),
+ note: prepare_user_bio(user),
sensitive: false,
fields: user.raw_fields,
pleroma: %{
@@ -165,8 +255,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
@@ -235,7 +337,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
@@ -262,6 +368,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/app_view.ex b/lib/pleroma/web/mastodon_api/views/app_view.ex
index beba89edb..36071cd25 100644
--- a/lib/pleroma/web/mastodon_api/views/app_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/app_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.AppView do
@@ -7,6 +7,21 @@ defmodule Pleroma.Web.MastodonAPI.AppView do
alias Pleroma.Web.OAuth.App
+ def render("index.json", %{apps: apps, count: count, page_size: page_size, admin: true}) do
+ %{
+ apps: render_many(apps, Pleroma.Web.MastodonAPI.AppView, "show.json", %{admin: true}),
+ count: count,
+ page_size: page_size
+ }
+ end
+
+ def render("show.json", %{admin: true, app: %App{} = app} = assigns) do
+ "show.json"
+ |> render(Map.delete(assigns, :admin))
+ |> Map.put(:trusted, app.trusted)
+ |> Map.put(:id, app.id)
+ end
+
def render("show.json", %{app: %App{} = app}) do
%{
id: app.id |> to_string,
diff --git a/lib/pleroma/web/mastodon_api/views/conversation_view.ex b/lib/pleroma/web/mastodon_api/views/conversation_view.ex
index 2220fbcb1..2b6f84c72 100644
--- a/lib/pleroma/web/mastodon_api/views/conversation_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/conversation_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ConversationView do
diff --git a/lib/pleroma/web/mastodon_api/views/custom_emoji_view.ex b/lib/pleroma/web/mastodon_api/views/custom_emoji_view.ex
index cb8688941..47a242b8e 100644
--- a/lib/pleroma/web/mastodon_api/views/custom_emoji_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/custom_emoji_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.CustomEmojiView do
diff --git a/lib/pleroma/web/mastodon_api/views/filter_view.ex b/lib/pleroma/web/mastodon_api/views/filter_view.ex
index a685bc7b6..aeff646f5 100644
--- a/lib/pleroma/web/mastodon_api/views/filter_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/filter_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.FilterView do
@@ -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 c4866e510..a329ffc28 100644
--- a/lib/pleroma/web/mastodon_api/views/instance_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex
@@ -1,14 +1,17 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.InstanceView do
use Pleroma.Web, :view
+ alias Pleroma.Config
+ alias Pleroma.Web.ActivityPub.MRF
+
@mastodon_api_level "2.7.2"
def render("show.json", _) do
- instance = Pleroma.Config.get(:instance)
+ instance = Config.get(:instance)
%{
uri: Pleroma.Web.base_url(),
@@ -29,7 +32,58 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
upload_limit: Keyword.get(instance, :upload_limit),
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)
+ banner_upload_limit: Keyword.get(instance, :banner_upload_limit),
+ pleroma: %{
+ metadata: %{
+ features: features(),
+ federation: federation()
+ },
+ vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
+ }
}
end
+
+ def features do
+ [
+ "pleroma_api",
+ "mastodon_api",
+ "mastodon_api_streaming",
+ "polls",
+ "pleroma_explicit_addressing",
+ "shareable_emoji_packs",
+ "multifetch",
+ "pleroma:api/v1/notifications:include_types_filter",
+ if Config.get([:media_proxy, :enabled]) do
+ "media_proxy"
+ end,
+ if Config.get([:gopher, :enabled]) do
+ "gopher"
+ end,
+ if Config.get([:chat, :enabled]) do
+ "chat"
+ end,
+ if Config.get([:instance, :allow_relay]) do
+ "relay"
+ end,
+ if Config.get([:instance, :safe_dm_mentions]) do
+ "safe_dm_mentions"
+ end,
+ "pleroma_emoji_reactions"
+ ]
+ |> Enum.filter(& &1)
+ end
+
+ def federation do
+ quarantined = Config.get([:instance, :quarantined_instances], [])
+
+ if Config.get([:instance, :mrf_transparency]) do
+ {:ok, data} = MRF.describe()
+
+ data
+ |> Map.merge(%{quarantined_instances: quarantined})
+ else
+ %{}
+ end
+ |> Map.put(:enabled, Config.get([:instance, :federating]))
+ end
end
diff --git a/lib/pleroma/web/mastodon_api/views/list_view.ex b/lib/pleroma/web/mastodon_api/views/list_view.ex
index bfda6f5b3..580596b64 100644
--- a/lib/pleroma/web/mastodon_api/views/list_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/list_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ListView do
diff --git a/lib/pleroma/web/mastodon_api/views/marker_view.ex b/lib/pleroma/web/mastodon_api/views/marker_view.ex
index 38fbeed5f..21d535d54 100644
--- a/lib/pleroma/web/mastodon_api/views/marker_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/marker_view.ex
@@ -1,17 +1,21 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
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 360ec10f0..4da1ab67f 100644
--- a/lib/pleroma/web/mastodon_api/views/notification_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.NotificationView do
@@ -8,24 +8,91 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
alias Pleroma.Activity
alias Pleroma.Notification
alias Pleroma.User
+ alias Pleroma.UserRelationship
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.NotificationView
alias Pleroma.Web.MastodonAPI.StatusView
- def render("index.json", %{notifications: notifications, for: user}) do
- safe_render_many(notifications, NotificationView, "show.json", %{for: user})
+ def render("index.json", %{notifications: notifications, for: reading_user} = opts) do
+ activities = Enum.map(notifications, & &1.activity)
+
+ parent_activities =
+ activities
+ |> Enum.filter(
+ &(Activity.mastodon_notification_type(&1) in [
+ "favourite",
+ "reblog",
+ "pleroma:emoji_reaction"
+ ])
+ )
+ |> Enum.map(& &1.data["object"])
+ |> Activity.create_by_object_ap_id()
+ |> Activity.with_preloaded_object(:left)
+ |> Pleroma.Repo.all()
+
+ relationships_opt =
+ cond do
+ Map.has_key?(opts, :relationships) ->
+ opts[:relationships]
+
+ is_nil(reading_user) ->
+ UserRelationship.view_relationships_option(nil, [])
+
+ true ->
+ move_activities_targets =
+ activities
+ |> Enum.filter(&(Activity.mastodon_notification_type(&1) == "move"))
+ |> Enum.map(&User.get_cached_by_ap_id(&1.data["target"]))
+
+ actors =
+ activities
+ |> Enum.map(fn a -> User.get_cached_by_ap_id(a.data["actor"]) end)
+ |> Enum.filter(& &1)
+ |> Kernel.++(move_activities_targets)
+
+ UserRelationship.view_relationships_option(reading_user, actors,
+ source_mutes_only: opts[:skip_relationships]
+ )
+ end
+
+ opts =
+ opts
+ |> Map.put(:parent_activities, parent_activities)
+ |> Map.put(:relationships, relationships_opt)
+
+ safe_render_many(notifications, NotificationView, "show.json", opts)
end
- def render("show.json", %{
- notification: %Notification{activity: activity} = notification,
- for: user
- }) do
+ def render(
+ "show.json",
+ %{
+ notification: %Notification{activity: activity} = notification,
+ for: reading_user
+ } = opts
+ ) do
actor = User.get_cached_by_ap_id(activity.data["actor"])
- parent_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
+
+ parent_activity_fn = fn ->
+ if opts[:parent_activities] do
+ Activity.Queries.find_by_object_ap_id(opts[:parent_activities], activity.data["object"])
+ else
+ Activity.get_create_by_object_ap_id(activity.data["object"])
+ end
+ end
+
mastodon_type = Activity.mastodon_notification_type(activity)
- with %{id: _} = account <- AccountView.render("show.json", %{user: actor, for: user}) do
+ render_opts = %{
+ relationships: opts[:relationships],
+ skip_relationships: opts[:skip_relationships]
+ }
+
+ with %{id: _} = account <-
+ AccountView.render(
+ "show.json",
+ Map.merge(render_opts, %{user: actor, for: reading_user})
+ ) do
response = %{
id: to_string(notification.id),
type: mastodon_type,
@@ -38,22 +105,25 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
case mastodon_type do
"mention" ->
- put_status(response, activity, user)
+ put_status(response, activity, reading_user, render_opts)
"favourite" ->
- put_status(response, parent_activity, user)
+ put_status(response, parent_activity_fn.(), reading_user, render_opts)
"reblog" ->
- put_status(response, parent_activity, user)
+ put_status(response, parent_activity_fn.(), reading_user, render_opts)
"move" ->
- put_target(response, activity, user)
+ # Note: :skip_relationships option being applied to _account_ rendering (here)
+ put_target(response, activity, reading_user, render_opts)
- "follow" ->
+ "pleroma:emoji_reaction" ->
response
+ |> put_status(parent_activity_fn.(), reading_user, render_opts)
+ |> put_emoji(activity)
- "pleroma:emoji_reaction" ->
- put_status(response, parent_activity, user) |> put_emoji(activity)
+ type when type in ["follow", "follow_request"] ->
+ response
_ ->
nil
@@ -64,16 +134,21 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
end
defp put_emoji(response, activity) do
- response
- |> Map.put(:emoji, activity.data["content"])
+ Map.put(response, :emoji, activity.data["content"])
end
- defp put_status(response, activity, user) do
- Map.put(response, :status, StatusView.render("show.json", %{activity: activity, for: user}))
+ defp put_status(response, activity, reading_user, opts) do
+ status_render_opts = Map.merge(opts, %{activity: activity, for: reading_user})
+ status_render = StatusView.render("show.json", status_render_opts)
+
+ Map.put(response, :status, status_render)
end
- defp put_target(response, activity, user) do
- target = User.get_cached_by_ap_id(activity.data["target"])
- Map.put(response, :target, AccountView.render("show.json", %{user: target, for: user}))
+ defp put_target(response, activity, reading_user, opts) do
+ target_user = User.get_cached_by_ap_id(activity.data["target"])
+ target_render_opts = Map.merge(opts, %{user: target_user, for: reading_user})
+ target_render = AccountView.render("show.json", target_render_opts)
+
+ Map.put(response, :target, target_render)
end
end
diff --git a/lib/pleroma/web/mastodon_api/views/poll_view.ex b/lib/pleroma/web/mastodon_api/views/poll_view.ex
index 6bb3652fb..59a5deb28 100644
--- a/lib/pleroma/web/mastodon_api/views/poll_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/poll_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.PollView do
@@ -19,6 +19,7 @@ defmodule Pleroma.Web.MastodonAPI.PollView do
expired: expired,
multiple: multiple,
votes_count: votes_count,
+ voters_count: (multiple || nil) && voters_count(object),
options: options,
voted: voted?(params),
emojis: Pleroma.Web.MastodonAPI.StatusView.build_emojis(object.data["emoji"])
@@ -62,6 +63,12 @@ defmodule Pleroma.Web.MastodonAPI.PollView do
end)
end
+ defp voters_count(%{data: %{"voters" => [_ | _] = voters}}) do
+ length(voters)
+ end
+
+ defp voters_count(_), do: 0
+
defp voted?(%{object: object} = opts) do
if opts[:for] do
existing_votes = Pleroma.Web.ActivityPub.Utils.get_existing_votes(opts[:for].ap_id, object)
diff --git a/lib/pleroma/web/mastodon_api/views/report_view.ex b/lib/pleroma/web/mastodon_api/views/report_view.ex
index 9da2dd740..98cb581ef 100644
--- a/lib/pleroma/web/mastodon_api/views/report_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/report_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ReportView do
diff --git a/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex b/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex
index fc042a276..458f6bc78 100644
--- a/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ScheduledActivityView do
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index 6b0fe9215..24167f66f 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.StatusView do
@@ -13,6 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
+ alias Pleroma.UserRelationship
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.AccountView
@@ -44,7 +45,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
end)
end
- defp get_user(ap_id) do
+ def get_user(ap_id, fake_record_fallback \\ true) do
cond do
user = User.get_cached_by_ap_id(ap_id) ->
user
@@ -52,8 +53,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
user = User.get_by_guessed_nickname(ap_id) ->
user
- true ->
+ fake_record_fallback ->
+ # TODO: refactor (fake records is never a good idea)
User.error_user(ap_id)
+
+ true ->
+ nil
end
end
@@ -71,10 +76,49 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
end
def render("index.json", opts) do
- replied_to_activities = get_replied_to_activities(opts.activities)
- opts = Map.put(opts, :replied_to_activities, replied_to_activities)
+ reading_user = opts[:for]
+
+ # To do: check AdminAPIControllerTest on the reasons behind nil activities in the list
+ activities = Enum.filter(opts.activities, & &1)
+ replied_to_activities = get_replied_to_activities(activities)
+
+ parent_activities =
+ activities
+ |> Enum.filter(&(&1.data["type"] == "Announce" && &1.data["object"]))
+ |> Enum.map(&Object.normalize(&1).data["id"])
+ |> Activity.create_by_object_ap_id()
+ |> Activity.with_preloaded_object(:left)
+ |> Activity.with_preloaded_bookmark(reading_user)
+ |> Activity.with_set_thread_muted_field(reading_user)
+ |> Repo.all()
+
+ relationships_opt =
+ cond do
+ Map.has_key?(opts, :relationships) ->
+ opts[:relationships]
+
+ is_nil(reading_user) ->
+ UserRelationship.view_relationships_option(nil, [])
+
+ true ->
+ # Note: unresolved users are filtered out
+ actors =
+ (activities ++ parent_activities)
+ |> Enum.map(&get_user(&1.data["actor"], false))
+ |> Enum.filter(& &1)
+
+ UserRelationship.view_relationships_option(reading_user, actors,
+ source_mutes_only: opts[:skip_relationships]
+ )
+ end
- safe_render_many(opts.activities, StatusView, "show.json", opts)
+ opts =
+ opts
+ |> Map.put(:replied_to_activities, replied_to_activities)
+ |> Map.put(:parent_activities, parent_activities)
+ |> Map.put(:relationships, relationships_opt)
+
+ safe_render_many(activities, StatusView, "show.json", opts)
end
def render(
@@ -85,17 +129,25 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
created_at = Utils.to_masto_date(activity.data["published"])
activity_object = Object.normalize(activity)
- reblogged_activity =
- Activity.create_by_object_ap_id(activity_object.data["id"])
- |> Activity.with_preloaded_bookmark(opts[:for])
- |> Activity.with_set_thread_muted_field(opts[:for])
- |> Repo.one()
+ reblogged_parent_activity =
+ if opts[:parent_activities] do
+ Activity.Queries.find_by_object_ap_id(
+ opts[:parent_activities],
+ activity_object.data["id"]
+ )
+ else
+ Activity.create_by_object_ap_id(activity_object.data["id"])
+ |> Activity.with_preloaded_bookmark(opts[:for])
+ |> Activity.with_set_thread_muted_field(opts[:for])
+ |> Repo.one()
+ end
- reblogged = render("show.json", Map.put(opts, :activity, reblogged_activity))
+ reblog_rendering_opts = Map.put(opts, :activity, reblogged_parent_activity)
+ reblogged = render("show.json", reblog_rendering_opts)
favorited = opts[:for] && opts[:for].ap_id in (activity_object.data["likes"] || [])
- bookmarked = Activity.get_bookmark(reblogged_activity, opts[:for]) != nil
+ bookmarked = Activity.get_bookmark(reblogged_parent_activity, opts[:for]) != nil
mentions =
activity.recipients
@@ -107,7 +159,13 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
id: to_string(activity.id),
uri: activity_object.data["id"],
url: activity_object.data["id"],
- account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
+ account:
+ AccountView.render("show.json", %{
+ user: user,
+ for: opts[:for],
+ relationships: opts[:relationships],
+ skip_relationships: opts[:skip_relationships]
+ }),
in_reply_to_id: nil,
in_reply_to_account_id: nil,
reblog: reblogged,
@@ -116,7 +174,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
reblogs_count: 0,
replies_count: 0,
favourites_count: 0,
- reblogged: reblogged?(reblogged_activity, opts[:for]),
+ reblogged: reblogged?(reblogged_parent_activity, opts[:for]),
favourited: present?(favorited),
bookmarked: present?(bookmarked),
muted: false,
@@ -183,9 +241,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
end
thread_muted? =
- case activity.thread_muted? do
- thread_muted? when is_boolean(thread_muted?) -> thread_muted?
- nil -> (opts[:for] && CommonAPI.thread_muted?(opts[:for], activity)) || false
+ cond do
+ is_nil(opts[:for]) -> false
+ is_boolean(activity.thread_muted?) -> activity.thread_muted?
+ true -> CommonAPI.thread_muted?(opts[:for], activity)
end
attachment_data = object.data["attachment"] || []
@@ -253,11 +312,28 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
_ -> []
end
+ # Status muted state (would do 1 request per status unless user mutes are preloaded)
+ muted =
+ thread_muted? ||
+ UserRelationship.exists?(
+ get_in(opts, [:relationships, :user_relationships]),
+ :mute,
+ opts[:for],
+ user,
+ fn for_user, user -> User.mutes?(for_user, user) end
+ )
+
%{
id: to_string(activity.id),
uri: object.data["id"],
url: url,
- account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
+ account:
+ AccountView.render("show.json", %{
+ user: user,
+ for: opts[:for],
+ relationships: opts[:relationships],
+ skip_relationships: opts[:skip_relationships]
+ }),
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),
reblog: nil,
@@ -270,7 +346,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
reblogged: reblogged?(activity, opts[:for]),
favourited: present?(favorited),
bookmarked: present?(bookmarked),
- muted: thread_muted? || User.mutes?(opts[:for], user),
+ muted: muted,
pinned: pinned?(activity, user),
sensitive: sensitive,
spoiler_text: summary,
@@ -421,7 +497,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
end
def render_content(%{data: %{"type" => object_type}} = object)
- when object_type in ["Video", "Event"] do
+ when object_type in ["Video", "Event", "Audio"] do
with name when not is_nil(name) and name != "" <- object.data["name"] do
"<p><a href=\"#{object.data["id"]}\">#{name}</a></p>#{object.data["content"]}"
else
@@ -453,11 +529,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
"""
@spec build_tags(list(any())) :: list(map())
def build_tags(object_tags) when is_list(object_tags) do
- object_tags = for tag when is_binary(tag) <- object_tags, do: tag
-
- Enum.reduce(object_tags, [], fn tag, tags ->
- tags ++ [%{name: tag, url: "/tag/#{URI.encode(tag)}"}]
- end)
+ object_tags
+ |> Enum.filter(&is_binary/1)
+ |> Enum.map(&%{name: &1, url: "/tag/#{URI.encode(&1)}"})
end
def build_tags(_), do: []
diff --git a/lib/pleroma/web/mastodon_api/views/push_subscription_view.ex b/lib/pleroma/web/mastodon_api/views/subscription_view.ex
index 021489711..7c67cc924 100644
--- a/lib/pleroma/web/mastodon_api/views/push_subscription_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/subscription_view.ex
@@ -1,12 +1,12 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Web.MastodonAPI.PushSubscriptionView do
+defmodule Pleroma.Web.MastodonAPI.SubscriptionView do
use Pleroma.Web, :view
alias Pleroma.Web.Push
- def render("push_subscription.json", %{subscription: subscription}) do
+ def render("show.json", %{subscription: subscription}) do
%{
id: to_string(subscription.id),
endpoint: subscription.endpoint,
diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex
index a400d1c8d..94e4595d8 100644
--- a/lib/pleroma/web/mastodon_api/websocket_handler.ex
+++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
@@ -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/media_proxy.ex b/lib/pleroma/web/media_proxy/media_proxy.ex
index 1725ab071..b2b524524 100644
--- a/lib/pleroma/web/media_proxy/media_proxy.ex
+++ b/lib/pleroma/web/media_proxy/media_proxy.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MediaProxy do
diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex
index 8403850ff..4657a4383 100644
--- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex
+++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex
@@ -1,9 +1,10 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MediaProxy.MediaProxyController do
use Pleroma.Web, :controller
+
alias Pleroma.ReverseProxy
alias Pleroma.Web.MediaProxy
diff --git a/lib/pleroma/web/metadata.ex b/lib/pleroma/web/metadata.ex
index 8761260f2..a9f70c43e 100644
--- a/lib/pleroma/web/metadata.ex
+++ b/lib/pleroma/web/metadata.ex
@@ -1,12 +1,17 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Metadata do
alias Phoenix.HTML
def build_tags(params) do
- Enum.reduce(Pleroma.Config.get([__MODULE__, :providers], []), "", fn parser, acc ->
+ providers = [
+ Pleroma.Web.Metadata.Providers.RestrictIndexing
+ | Pleroma.Config.get([__MODULE__, :providers], [])
+ ]
+
+ Enum.reduce(providers, "", fn parser, acc ->
rendered_html =
params
|> parser.build_tags()
diff --git a/lib/pleroma/web/metadata/feed.ex b/lib/pleroma/web/metadata/feed.ex
index ee48913a7..bd1459a17 100644
--- a/lib/pleroma/web/metadata/feed.ex
+++ b/lib/pleroma/web/metadata/feed.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Metadata.Providers.Feed do
diff --git a/lib/pleroma/web/metadata/opengraph.ex b/lib/pleroma/web/metadata/opengraph.ex
index e7fa7f408..68c871e71 100644
--- a/lib/pleroma/web/metadata/opengraph.ex
+++ b/lib/pleroma/web/metadata/opengraph.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Metadata.Providers.OpenGraph do
@@ -68,7 +68,7 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do
property: "og:title",
content: Utils.user_name_string(user)
], []},
- {:meta, [property: "og:url", content: User.profile_url(user)], []},
+ {:meta, [property: "og:url", content: user.uri || user.ap_id], []},
{:meta, [property: "og:description", content: truncated_bio], []},
{:meta, [property: "og:type", content: "website"], []},
{:meta, [property: "og:image", content: Utils.attachment_url(User.avatar_url(user))], []},
diff --git a/lib/pleroma/web/metadata/player_view.ex b/lib/pleroma/web/metadata/player_view.ex
index 4289ebdbd..5a918532a 100644
--- a/lib/pleroma/web/metadata/player_view.ex
+++ b/lib/pleroma/web/metadata/player_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Metadata.PlayerView do
diff --git a/lib/pleroma/web/metadata/provider.ex b/lib/pleroma/web/metadata/provider.ex
index 197fb2a77..767288f9c 100644
--- a/lib/pleroma/web/metadata/provider.ex
+++ b/lib/pleroma/web/metadata/provider.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Metadata.Providers.Provider do
diff --git a/lib/pleroma/web/metadata/rel_me.ex b/lib/pleroma/web/metadata/rel_me.ex
index 86dcc1a3b..8905c9c72 100644
--- a/lib/pleroma/web/metadata/rel_me.ex
+++ b/lib/pleroma/web/metadata/rel_me.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Metadata.Providers.RelMe do
diff --git a/lib/pleroma/web/metadata/restrict_indexing.ex b/lib/pleroma/web/metadata/restrict_indexing.ex
new file mode 100644
index 000000000..f15607896
--- /dev/null
+++ b/lib/pleroma/web/metadata/restrict_indexing.ex
@@ -0,0 +1,25 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Metadata.Providers.RestrictIndexing do
+ @behaviour Pleroma.Web.Metadata.Providers.Provider
+
+ @moduledoc """
+ Restricts indexing of remote users.
+ """
+
+ @impl true
+ def build_tags(%{user: %{local: false}}) do
+ [
+ {:meta,
+ [
+ name: "robots",
+ content: "noindex, noarchive"
+ ], []}
+ ]
+ end
+
+ @impl true
+ def build_tags(%{user: %{local: true}}), do: []
+end
diff --git a/lib/pleroma/web/metadata/twitter_card.ex b/lib/pleroma/web/metadata/twitter_card.ex
index 67419a666..5d08ce422 100644
--- a/lib/pleroma/web/metadata/twitter_card.ex
+++ b/lib/pleroma/web/metadata/twitter_card.ex
@@ -1,6 +1,6 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Metadata.Providers.TwitterCard do
diff --git a/lib/pleroma/web/metadata/utils.ex b/lib/pleroma/web/metadata/utils.ex
index 000bd9f66..2f0dfb474 100644
--- a/lib/pleroma/web/metadata/utils.ex
+++ b/lib/pleroma/web/metadata/utils.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Metadata.Utils do
diff --git a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex b/lib/pleroma/web/mongooseim/mongoose_im_controller.ex
index 358600e7d..0814b3bc3 100644
--- a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex
+++ b/lib/pleroma/web/mongooseim/mongoose_im_controller.ex
@@ -1,11 +1,10 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MongooseIM.MongooseIMController do
use Pleroma.Web, :controller
- alias Comeonin.Pbkdf2
alias Pleroma.Plugs.RateLimiter
alias Pleroma.Repo
alias Pleroma.User
@@ -14,7 +13,7 @@ defmodule Pleroma.Web.MongooseIM.MongooseIMController do
plug(RateLimiter, [name: :authentication, params: ["user"]] when action == :check_password)
def user_exists(conn, %{"user" => username}) do
- with %User{} <- Repo.get_by(User, nickname: username, local: true) do
+ with %User{} <- Repo.get_by(User, nickname: username, local: true, deactivated: false) do
conn
|> json(true)
else
@@ -26,9 +25,9 @@ defmodule Pleroma.Web.MongooseIM.MongooseIMController do
end
def check_password(conn, %{"user" => username, "pass" => password}) do
- with %User{password_hash: password_hash} <-
+ with %User{password_hash: password_hash, deactivated: false} <-
Repo.get_by(User, nickname: username, local: true),
- true <- Pbkdf2.checkpw(password, password_hash) do
+ true <- Pbkdf2.verify_pass(password, password_hash) do
conn
|> json(true)
else
diff --git a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
index 333012920..721b599d4 100644
--- a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
+++ b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
@@ -9,8 +9,8 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
alias Pleroma.Stats
alias Pleroma.User
alias Pleroma.Web
- alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.Federator.Publisher
+ alias Pleroma.Web.MastodonAPI.InstanceView
def schemas(conn, _params) do
response = %{
@@ -34,49 +34,12 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
def raw_nodeinfo do
stats = Stats.get_stats()
- quarantined = Config.get([:instance, :quarantined_instances], [])
-
staff_accounts =
User.all_superusers()
|> Enum.map(fn u -> u.ap_id end)
- federation_response =
- if Config.get([:instance, :mrf_transparency]) do
- {:ok, data} = MRF.describe()
-
- data
- |> Map.merge(%{quarantined_instances: quarantined})
- else
- %{}
- end
- |> Map.put(:enabled, Config.get([:instance, :federating]))
-
- features =
- [
- "pleroma_api",
- "mastodon_api",
- "mastodon_api_streaming",
- "polls",
- "pleroma_explicit_addressing",
- "shareable_emoji_packs",
- "multifetch",
- if Config.get([:media_proxy, :enabled]) do
- "media_proxy"
- end,
- if Config.get([:gopher, :enabled]) do
- "gopher"
- end,
- if Config.get([:chat, :enabled]) do
- "chat"
- end,
- if Config.get([:instance, :allow_relay]) do
- "relay"
- end,
- if Config.get([:instance, :safe_dm_mentions]) do
- "safe_dm_mentions"
- end
- ]
- |> Enum.filter(& &1)
+ features = InstanceView.features()
+ federation = InstanceView.federation()
%{
version: "2.0",
@@ -92,9 +55,9 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
openRegistrations: Config.get([:instance, :registrations_open]),
usage: %{
users: %{
- total: stats.user_count || 0
+ total: Map.get(stats, :user_count, 0)
},
- localPosts: stats.status_count || 0
+ localPosts: Map.get(stats, :status_count, 0)
},
metadata: %{
nodeName: Config.get([:instance, :name]),
@@ -104,7 +67,7 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
enabled: false
},
staffAccounts: staff_accounts,
- federation: federation_response,
+ federation: federation,
pollLimits: Config.get([:instance, :poll_limits]),
postFormats: Config.get([:instance, :allowed_post_formats]),
uploadLimits: %{
diff --git a/lib/pleroma/web/oauth.ex b/lib/pleroma/web/oauth.ex
index 280cf28c0..2f1b8708d 100644
--- a/lib/pleroma/web/oauth.ex
+++ b/lib/pleroma/web/oauth.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth do
diff --git a/lib/pleroma/web/oauth/app.ex b/lib/pleroma/web/oauth/app.ex
index cc3fb1ce5..6a6d5f2e2 100644
--- a/lib/pleroma/web/oauth/app.ex
+++ b/lib/pleroma/web/oauth/app.ex
@@ -1,10 +1,11 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.App do
use Ecto.Schema
import Ecto.Changeset
+ import Ecto.Query
alias Pleroma.Repo
@type t :: %__MODULE__{}
@@ -16,14 +17,24 @@ defmodule Pleroma.Web.OAuth.App do
field(:website, :string)
field(:client_id, :string)
field(:client_secret, :string)
+ field(:trusted, :boolean, default: false)
+
+ has_many(:oauth_authorizations, Pleroma.Web.OAuth.Authorization, on_delete: :delete_all)
+ has_many(:oauth_tokens, Pleroma.Web.OAuth.Token, on_delete: :delete_all)
timestamps()
end
+ @spec changeset(App.t(), map()) :: Ecto.Changeset.t()
+ def changeset(struct, params) do
+ cast(struct, params, [:client_name, :redirect_uris, :scopes, :website, :trusted])
+ end
+
+ @spec register_changeset(App.t(), map()) :: Ecto.Changeset.t()
def register_changeset(struct, params \\ %{}) do
changeset =
struct
- |> cast(params, [:client_name, :redirect_uris, :scopes, :website])
+ |> changeset(params)
|> validate_required([:client_name, :redirect_uris, :scopes])
if changeset.valid? do
@@ -41,6 +52,21 @@ defmodule Pleroma.Web.OAuth.App do
end
end
+ @spec create(map()) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
+ def create(params) do
+ with changeset <- __MODULE__.register_changeset(%__MODULE__{}, params) do
+ Repo.insert(changeset)
+ end
+ end
+
+ @spec update(map()) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
+ def update(params) do
+ with %__MODULE__{} = app <- Repo.get(__MODULE__, params["id"]),
+ changeset <- changeset(app, params) do
+ Repo.update(changeset)
+ end
+ end
+
@doc """
Gets app by attrs or create new with attrs.
And updates the scopes if need.
@@ -65,4 +91,58 @@ defmodule Pleroma.Web.OAuth.App do
|> change(%{scopes: scopes})
|> Repo.update()
end
+
+ @spec search(map()) :: {:ok, [App.t()], non_neg_integer()}
+ def search(params) do
+ query = from(a in __MODULE__)
+
+ query =
+ if params[:client_name] do
+ from(a in query, where: a.client_name == ^params[:client_name])
+ else
+ query
+ end
+
+ query =
+ if params[:client_id] do
+ from(a in query, where: a.client_id == ^params[:client_id])
+ else
+ query
+ end
+
+ query =
+ if Map.has_key?(params, :trusted) do
+ from(a in query, where: a.trusted == ^params[:trusted])
+ else
+ query
+ end
+
+ query =
+ from(u in query,
+ limit: ^params[:page_size],
+ offset: ^((params[:page] - 1) * params[:page_size])
+ )
+
+ count = Repo.aggregate(__MODULE__, :count, :id)
+
+ {:ok, Repo.all(query), count}
+ end
+
+ @spec destroy(pos_integer()) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
+ def destroy(id) do
+ with %__MODULE__{} = app <- Repo.get(__MODULE__, id) do
+ Repo.delete(app)
+ end
+ end
+
+ @spec errors(Ecto.Changeset.t()) :: map()
+ def errors(changeset) do
+ Enum.reduce(changeset.errors, %{}, fn
+ {:client_name, {error, _}}, acc ->
+ Map.put(acc, :name, error)
+
+ {key, {error, _}}, acc ->
+ Map.put(acc, key, error)
+ end)
+ end
end
diff --git a/lib/pleroma/web/oauth/authorization.ex b/lib/pleroma/web/oauth/authorization.ex
index ed42a34f3..268ee5b63 100644
--- a/lib/pleroma/web/oauth/authorization.ex
+++ b/lib/pleroma/web/oauth/authorization.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.Authorization do
diff --git a/lib/pleroma/web/oauth/fallback_controller.ex b/lib/pleroma/web/oauth/fallback_controller.ex
index dd7f08bf1..a89ced886 100644
--- a/lib/pleroma/web/oauth/fallback_controller.ex
+++ b/lib/pleroma/web/oauth/fallback_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.FallbackController do
diff --git a/lib/pleroma/web/oauth/mfa_controller.ex b/lib/pleroma/web/oauth/mfa_controller.ex
new file mode 100644
index 000000000..e52cccd85
--- /dev/null
+++ b/lib/pleroma/web/oauth/mfa_controller.ex
@@ -0,0 +1,97 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 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..e88e7066b
--- /dev/null
+++ b/lib/pleroma/web/oauth/mfa_view.ex
@@ -0,0 +1,8 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 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 528f08574..7c804233c 100644
--- a/lib/pleroma/web/oauth/oauth_controller.ex
+++ b/lib/pleroma/web/oauth/oauth_controller.ex
@@ -1,11 +1,12 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
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
@@ -25,6 +27,9 @@ defmodule Pleroma.Web.OAuth.OAuthController do
plug(:fetch_session)
plug(:fetch_flash)
+
+ plug(:skip_plug, [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug])
+
plug(RateLimiter, [name: :authentication] when action == :create_authorization)
action_fallback(Pleroma.Web.OAuth.FallbackController)
@@ -118,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 ->
@@ -178,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
@@ -228,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
@@ -241,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
@@ -267,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,
@@ -431,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
@@ -497,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
@@ -512,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/oauth_view.ex b/lib/pleroma/web/oauth/oauth_view.ex
index 9b37a91c5..94ddaf913 100644
--- a/lib/pleroma/web/oauth/oauth_view.ex
+++ b/lib/pleroma/web/oauth/oauth_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.OAuthView do
diff --git a/lib/pleroma/web/oauth/scopes.ex b/lib/pleroma/web/oauth/scopes.ex
index 151467494..6f06f1431 100644
--- a/lib/pleroma/web/oauth/scopes.ex
+++ b/lib/pleroma/web/oauth/scopes.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.Scopes do
@@ -15,9 +15,10 @@ defmodule Pleroma.Web.OAuth.Scopes do
Note: `scopes` is used by Mastodon — supporting it but sticking to
OAuth's standard `scope` wherever we control it
"""
- @spec fetch_scopes(map(), list()) :: list()
+ @spec fetch_scopes(map() | struct(), list()) :: list()
+
def fetch_scopes(params, default) do
- parse_scopes(params["scope"] || params["scopes"], default)
+ parse_scopes(params["scope"] || params["scopes"] || params[:scopes], default)
end
def parse_scopes(scopes, _default) when is_list(scopes) do
diff --git a/lib/pleroma/web/oauth/token.ex b/lib/pleroma/web/oauth/token.ex
index 8ea373805..08bb7326d 100644
--- a/lib/pleroma/web/oauth/token.ex
+++ b/lib/pleroma/web/oauth/token.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.Token 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..2c3bb9ded
--- /dev/null
+++ b/lib/pleroma/web/oauth/token/clean_worker.ex
@@ -0,0 +1,38 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 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/query.ex b/lib/pleroma/web/oauth/token/query.ex
index 9642103e6..93d6e26ed 100644
--- a/lib/pleroma/web/oauth/token/query.ex
+++ b/lib/pleroma/web/oauth/token/query.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.Token.Query do
diff --git a/lib/pleroma/web/oauth/token/response.ex b/lib/pleroma/web/oauth/token/response.ex
index 266110814..0e72c31e9 100644
--- a/lib/pleroma/web/oauth/token/response.ex
+++ b/lib/pleroma/web/oauth/token/response.ex
@@ -1,10 +1,11 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
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/oauth/token/strategy/refresh_token.ex b/lib/pleroma/web/oauth/token/strategy/refresh_token.ex
index c620050c8..debc29b0b 100644
--- a/lib/pleroma/web/oauth/token/strategy/refresh_token.ex
+++ b/lib/pleroma/web/oauth/token/strategy/refresh_token.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.Token.Strategy.RefreshToken do
diff --git a/lib/pleroma/web/oauth/token/strategy/revoke.ex b/lib/pleroma/web/oauth/token/strategy/revoke.ex
index 983f095b4..069c1ee21 100644
--- a/lib/pleroma/web/oauth/token/strategy/revoke.ex
+++ b/lib/pleroma/web/oauth/token/strategy/revoke.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.Token.Strategy.Revoke do
diff --git a/lib/pleroma/web/oauth/token/utils.ex b/lib/pleroma/web/oauth/token/utils.ex
index 1e8765e93..43aeab6b0 100644
--- a/lib/pleroma/web/oauth/token/utils.ex
+++ b/lib/pleroma/web/oauth/token/utils.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.Token.Utils do
diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex
index 01ec7941e..6971cd9f8 100644
--- a/lib/pleroma/web/ostatus/ostatus_controller.ex
+++ b/lib/pleroma/web/ostatus/ostatus_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OStatus.OStatusController do
@@ -16,6 +16,10 @@ defmodule Pleroma.Web.OStatus.OStatusController do
alias Pleroma.Web.Metadata.PlayerView
alias Pleroma.Web.Router
+ plug(Pleroma.Plugs.EnsureAuthenticatedPlug,
+ unless_func: &Pleroma.Web.FederatingPlug.federating?/1
+ )
+
plug(
RateLimiter,
[name: :ap_routes, params: ["uuid"]] when action in [:object, :activity]
@@ -135,13 +139,13 @@ defmodule Pleroma.Web.OStatus.OStatusController do
end
end
- def errors(conn, {:error, :not_found}) do
+ defp errors(conn, {:error, :not_found}) do
render_error(conn, :not_found, "Not found")
end
- def errors(conn, {:fetch_user, nil}), do: errors(conn, {:error, :not_found})
+ defp errors(conn, {:fetch_user, nil}), do: errors(conn, {:error, :not_found})
- def errors(conn, _) do
+ defp errors(conn, _) do
render_error(conn, :internal_server_error, "Something went wrong")
end
end
diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
index 773cd9a97..07078d415 100644
--- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
@@ -1,24 +1,36 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
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]
+ only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2, skip_relationships?: 1]
alias Ecto.Changeset
+ alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Plugs.RateLimiter
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
- alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.StatusView
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
+ )
+
+ plug(
OAuthScopesPlug,
%{scopes: ["follow", "write:follows"]} when action in [:subscribe, :unsubscribe]
)
@@ -34,21 +46,21 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
]
)
- plug(OAuthScopesPlug, %{scopes: ["read:favourites"]} when action == :favourites)
-
- # An extra safety measure for possible actions not guarded by OAuth permissions specification
plug(
- Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
- when action != :confirmation_resend
+ OAuthScopesPlug,
+ %{scopes: ["read:favourites"], fallback: :proceed_unauthenticated} when action == :favourites
)
plug(RateLimiter, [name: :account_confirmation_resend] when action == :confirmation_resend)
+
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
@@ -57,39 +69,33 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
end
@doc "PATCH /api/v1/pleroma/accounts/update_avatar"
- def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
- {:ok, user} =
+ def update_avatar(%{assigns: %{user: user}, body_params: %{img: ""}} = conn, _) do
+ {:ok, _user} =
user
|> Changeset.change(%{avatar: nil})
|> User.update_and_set_cache()
- CommonAPI.update(user)
-
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()
+ {:ok, _user} = user |> Changeset.change(%{avatar: data}) |> User.update_and_set_cache()
%{"url" => [%{"href" => href} | _]} = data
- CommonAPI.update(user)
-
json(conn, %{url: href})
end
@doc "PATCH /api/v1/pleroma/accounts/update_banner"
- def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
- with {:ok, user} <- User.update_banner(user, %{}) do
- CommonAPI.update(user)
+ 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),
- {:ok, user} <- User.update_banner(user, object.data) do
- CommonAPI.update(user)
+ 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
json(conn, %{url: href})
@@ -97,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
@@ -120,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,7 +146,12 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
conn
|> add_link_headers(activities)
|> put_view(StatusView)
- |> render("index.json", activities: activities, for: for_user, as: :activity)
+ |> render("index.json",
+ activities: activities,
+ for: for_user,
+ as: :activity,
+ skip_relationships: skip_relationships?(params)
+ )
end
@doc "POST /api/v1/pleroma/accounts/:id/subscribe"
diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex
index a2f6d2287..d276b96a4 100644
--- a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex
@@ -1,191 +1,93 @@
defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
use Pleroma.Web, :controller
- alias Pleroma.Plugs.OAuthScopesPlug
-
- require Logger
+ alias Pleroma.Emoji.Pack
plug(
- OAuthScopesPlug,
+ Pleroma.Plugs.OAuthScopesPlug,
%{scopes: ["write"], admin: true}
when action in [
+ :import_from_filesystem,
+ :remote,
+ :download,
:create,
+ :update,
:delete,
- :download_from,
- :list_from,
- :import_from_fs,
+ :add_file,
:update_file,
- :update_metadata
+ :delete_file
]
)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
-
- def emoji_dir_path do
- Path.join(
- Pleroma.Config.get!([:instance, :static_dir]),
- "emoji"
- )
- end
-
- @doc """
- Lists packs from the remote instance.
-
- Since JS cannot ask remote instances for their packs due to CPS, it has to
- be done by the server
- """
- def list_from(conn, %{"instance_address" => address}) do
- address = String.trim(address)
-
- if shareable_packs_available(address) do
- list_resp =
- "#{address}/api/pleroma/emoji/packs" |> Tesla.get!() |> Map.get(:body) |> Jason.decode!()
+ plug(
+ :skip_plug,
+ [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug]
+ when action in [:archive, :show, :list]
+ )
- json(conn, list_resp)
+ def remote(conn, %{"url" => url}) do
+ with {:ok, packs} <- Pack.list_remote(url) do
+ json(conn, packs)
else
- conn
- |> put_status(:internal_server_error)
- |> json(%{error: "The requested instance does not support sharing emoji packs"})
+ {:shareable, _} ->
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{error: "The requested instance does not support sharing emoji packs"})
end
end
- @doc """
- Lists the packs available on the instance as JSON.
-
- The information is public and does not require authentication. The format is
- a map of "pack directory name" to pack.json contents.
- """
- def list_packs(conn, _params) do
- # 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_dir_path())},
- {:ls, {:ok, results}} <- {:ls, File.ls(emoji_dir_path())} do
- pack_infos =
- results
- |> Enum.filter(&has_pack_json?/1)
- |> Enum.map(&load_pack/1)
- # Check if all the files are in place and can be sent
- |> Enum.map(&validate_pack/1)
- # Transform into a map of pack-name => pack-data
- |> Enum.into(%{})
-
- json(conn, pack_infos)
+ def list(conn, _params) do
+ emoji_path =
+ Path.join(
+ Pleroma.Config.get!([:instance, :static_dir]),
+ "emoji"
+ )
+
+ with {:ok, packs} <- Pack.list_local() do
+ json(conn, packs)
else
{:create_dir, {:error, e}} ->
conn
|> put_status(:internal_server_error)
- |> json(%{error: "Failed to create the emoji pack directory at #{emoji_dir_path()}: #{e}"})
+ |> json(%{error: "Failed to create the emoji pack directory at #{emoji_path}: #{e}"})
{:ls, {:error, e}} ->
conn
|> put_status(:internal_server_error)
|> json(%{
- error:
- "Failed to get the contents of the emoji pack directory at #{emoji_dir_path()}: #{e}"
+ error: "Failed to get the contents of the emoji pack directory at #{emoji_path}: #{e}"
})
end
end
- defp has_pack_json?(file) do
- dir_path = Path.join(emoji_dir_path(), file)
- # Filter to only use the pack.json packs
- File.dir?(dir_path) and File.exists?(Path.join(dir_path, "pack.json"))
- end
-
- defp load_pack(pack_name) do
- pack_path = Path.join(emoji_dir_path(), pack_name)
- pack_file = Path.join(pack_path, "pack.json")
+ def show(conn, %{"name" => name}) do
+ name = String.trim(name)
- {pack_name, Jason.decode!(File.read!(pack_file))}
- end
-
- defp validate_pack({name, pack}) do
- pack_path = Path.join(emoji_dir_path(), name)
-
- if can_download?(pack, pack_path) do
- archive_for_sha = make_archive(name, pack, pack_path)
- archive_sha = :crypto.hash(:sha256, archive_for_sha) |> Base.encode16()
-
- pack =
- pack
- |> put_in(["pack", "can-download"], true)
- |> put_in(["pack", "download-sha256"], archive_sha)
-
- {name, pack}
+ with {:ok, pack} <- Pack.show(name) do
+ json(conn, pack)
else
- {name, put_in(pack, ["pack", "can-download"], false)}
- end
- end
-
- defp can_download?(pack, pack_path) do
- # If the pack is set as shared, check if it can be downloaded
- # That means that when asked, the pack can be packed and sent to the remote
- # Otherwise, they'd have to download it from external-src
- pack["pack"]["share-files"] &&
- Enum.all?(pack["files"], fn {_, path} ->
- File.exists?(Path.join(pack_path, path))
- end)
- end
-
- defp create_archive_and_cache(name, pack, pack_dir, md5) do
- files =
- ['pack.json'] ++
- (pack["files"] |> Enum.map(fn {_, path} -> to_charlist(path) end))
-
- {:ok, {_, zip_result}} = :zip.zip('#{name}.zip', files, [:memory, cwd: to_charlist(pack_dir)])
-
- cache_seconds_per_file = Pleroma.Config.get!([:emoji, :shared_pack_cache_seconds_per_file])
- cache_ms = :timer.seconds(cache_seconds_per_file * Enum.count(files))
-
- Cachex.put!(
- :emoji_packs_cache,
- name,
- # if pack.json MD5 changes, the cache is not valid anymore
- %{pack_json_md5: md5, pack_data: zip_result},
- # Add a minute to cache time for every file in the pack
- ttl: cache_ms
- )
-
- Logger.debug("Created an archive for the '#{name}' emoji pack, \
-keeping it in cache for #{div(cache_ms, 1000)}s")
-
- zip_result
- end
-
- defp make_archive(name, pack, pack_dir) do
- # Having a different pack.json md5 invalidates cache
- pack_file_md5 = :crypto.hash(:md5, File.read!(Path.join(pack_dir, "pack.json")))
-
- case Cachex.get!(:emoji_packs_cache, name) do
- %{pack_file_md5: ^pack_file_md5, pack_data: zip_result} ->
- Logger.debug("Using cache for the '#{name}' shared emoji pack")
- zip_result
+ {:loaded, _} ->
+ conn
+ |> put_status(:not_found)
+ |> json(%{error: "Pack #{name} does not exist"})
- _ ->
- create_archive_and_cache(name, pack, pack_dir, pack_file_md5)
+ {:error, :empty_values} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "pack name cannot be empty"})
end
end
- @doc """
- An endpoint for other instances (via admin UI) or users (via browser)
- to download packs that the instance shares.
- """
- def download_shared(conn, %{"name" => name}) do
- pack_dir = Path.join(emoji_dir_path(), name)
- pack_file = Path.join(pack_dir, "pack.json")
-
- with {_, true} <- {:exists?, File.exists?(pack_file)},
- pack = Jason.decode!(File.read!(pack_file)),
- {_, true} <- {:can_download?, can_download?(pack, pack_dir)} do
- zip_result = make_archive(name, pack, pack_dir)
- send_download(conn, {:binary, zip_result}, filename: "#{name}.zip")
+ 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?, _} ->
conn
|> put_status(:forbidden)
|> json(%{
- error: "Pack #{name} cannot be downloaded from this instance, either pack sharing\
- was disabled for this pack or some files are missing"
+ error:
+ "Pack #{name} cannot be downloaded from this instance, either pack sharing was disabled for this pack or some files are missing"
})
{:exists?, _} ->
@@ -195,400 +97,197 @@ keeping it in cache for #{div(cache_ms, 1000)}s")
end
end
- defp shareable_packs_available(address) do
- "#{address}/.well-known/nodeinfo"
- |> 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")
- end
-
- @doc """
- An admin endpoint to request downloading a pack named `pack_name` from the instance
- `instance_address`.
-
- If the requested instance's admin chose to share the pack, it will be downloaded
- from that instance, otherwise it will be downloaded from the fallback source, if there is one.
- """
- def download_from(conn, %{"instance_address" => address, "pack_name" => name} = data) do
- address = String.trim(address)
-
- if shareable_packs_available(address) do
- full_pack =
- "#{address}/api/pleroma/emoji/packs/list"
- |> Tesla.get!()
- |> Map.get(:body)
- |> Jason.decode!()
- |> Map.get(name)
-
- pack_info_res =
- case full_pack["pack"] do
- %{"share-files" => true, "can-download" => true, "download-sha256" => sha} ->
- {:ok,
- %{
- sha: sha,
- uri: "#{address}/api/pleroma/emoji/packs/download_shared/#{name}"
- }}
-
- %{"fallback-src" => src, "fallback-src-sha256" => sha} when is_binary(src) ->
- {:ok,
- %{
- sha: sha,
- uri: src,
- fallback: true
- }}
-
- _ ->
- {:error,
- "The pack was not set as shared and there is no fallback src to download from"}
- end
-
- with {:ok, %{sha: sha, uri: uri} = pinfo} <- pack_info_res,
- %{body: emoji_archive} <- Tesla.get!(uri),
- {_, true} <- {:checksum, Base.decode16!(sha) == :crypto.hash(:sha256, emoji_archive)} do
- local_name = data["as"] || name
- pack_dir = Path.join(emoji_dir_path(), local_name)
- File.mkdir_p!(pack_dir)
-
- files = Enum.map(full_pack["files"], fn {_, path} -> to_charlist(path) end)
- # Fallback cannot contain a pack.json file
- files = if pinfo[:fallback], do: files, else: ['pack.json'] ++ files
-
- {:ok, _} = :zip.unzip(emoji_archive, cwd: to_charlist(pack_dir), file_list: files)
-
- # Fallback can't contain a pack.json file, since that would cause the fallback-src-sha256
- # in it to depend on itself
- if pinfo[:fallback] do
- pack_file_path = Path.join(pack_dir, "pack.json")
-
- File.write!(pack_file_path, Jason.encode!(full_pack, pretty: true))
- end
-
- json(conn, "ok")
- else
- {:error, e} ->
- conn |> put_status(:internal_server_error) |> json(%{error: e})
-
- {:checksum, _} ->
- conn
- |> put_status(:internal_server_error)
- |> json(%{error: "SHA256 for the pack doesn't match the one sent by the server"})
- end
+ def download(conn, %{"url" => url, "name" => name} = params) do
+ with :ok <- Pack.download(name, url, params["as"]) do
+ json(conn, "ok")
else
- conn
- |> put_status(:internal_server_error)
- |> json(%{error: "The requested instance does not support sharing emoji packs"})
+ {:shareable, _} ->
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{error: "The requested instance does not support sharing emoji packs"})
+
+ {:checksum, _} ->
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{error: "SHA256 for the pack doesn't match the one sent by the server"})
+
+ {:error, e} ->
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{error: e})
end
end
- @doc """
- Creates an empty pack named `name` which then can be updated via the admin UI.
- """
def create(conn, %{"name" => name}) do
- pack_dir = Path.join(emoji_dir_path(), name)
+ name = String.trim(name)
- if not File.exists?(pack_dir) do
- File.mkdir_p!(pack_dir)
-
- pack_file_p = Path.join(pack_dir, "pack.json")
+ with :ok <- Pack.create(name) do
+ json(conn, "ok")
+ else
+ {:error, :eexist} ->
+ conn
+ |> put_status(:conflict)
+ |> json(%{error: "A pack named \"#{name}\" already exists"})
- File.write!(
- pack_file_p,
- Jason.encode!(%{pack: %{}, files: %{}}, pretty: true)
- )
+ {:error, :empty_values} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "pack name cannot be empty"})
- conn |> json("ok")
- else
- conn
- |> put_status(:conflict)
- |> json(%{error: "A pack named \"#{name}\" already exists"})
+ {:error, _} ->
+ render_error(
+ conn,
+ :internal_server_error,
+ "Unexpected error occurred while creating pack."
+ )
end
end
- @doc """
- Deletes the pack `name` and all it's files.
- """
def delete(conn, %{"name" => name}) do
- pack_dir = Path.join(emoji_dir_path(), name)
+ name = String.trim(name)
+
+ with {:ok, deleted} when deleted != [] <- Pack.delete(name) do
+ json(conn, "ok")
+ else
+ {:ok, []} ->
+ conn
+ |> put_status(:not_found)
+ |> json(%{error: "Pack #{name} does not exist"})
- case File.rm_rf(pack_dir) do
- {:ok, _} ->
- conn |> json("ok")
+ {:error, :empty_values} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "pack name cannot be empty"})
- {:error, _} ->
+ {:error, _, _} ->
conn
|> put_status(:internal_server_error)
|> json(%{error: "Couldn't delete the pack #{name}"})
end
end
- @doc """
- An endpoint to update `pack_names`'s metadata.
-
- `new_data` is the new metadata for the pack, that will replace the old metadata.
- """
- def update_metadata(conn, %{"pack_name" => name, "new_data" => new_data}) do
- pack_file_p = Path.join([emoji_dir_path(), name, "pack.json"])
-
- full_pack = Jason.decode!(File.read!(pack_file_p))
-
- # The new fallback-src is in the new data and it's not the same as it was in the old data
- should_update_fb_sha =
- not is_nil(new_data["fallback-src"]) and
- new_data["fallback-src"] != full_pack["pack"]["fallback-src"]
-
- with {_, true} <- {:should_update?, should_update_fb_sha},
- %{body: pack_arch} <- Tesla.get!(new_data["fallback-src"]),
- {:ok, flist} <- :zip.unzip(pack_arch, [:memory]),
- {_, true} <- {:has_all_files?, has_all_files?(full_pack, flist)} do
- fallback_sha = :crypto.hash(:sha256, pack_arch) |> Base.encode16()
-
- new_data = Map.put(new_data, "fallback-src-sha256", fallback_sha)
- update_metadata_and_send(conn, full_pack, new_data, pack_file_p)
+ def update(conn, %{"name" => name, "metadata" => metadata}) do
+ with {:ok, pack} <- Pack.update_metadata(name, metadata) do
+ json(conn, pack.pack)
else
- {:should_update?, _} ->
- update_metadata_and_send(conn, full_pack, new_data, pack_file_p)
-
{:has_all_files?, _} ->
conn
|> put_status(:bad_request)
|> json(%{error: "The fallback archive does not have all files specified in pack.json"})
- end
- end
-
- # Check if all files from the pack.json are in the archive
- defp has_all_files?(%{"files" => files}, flist) do
- Enum.all?(files, fn {_, from_manifest} ->
- Enum.find(flist, fn {from_archive, _} ->
- to_string(from_archive) == from_manifest
- end)
- end)
- end
-
- defp update_metadata_and_send(conn, full_pack, new_data, pack_file_p) do
- full_pack = Map.put(full_pack, "pack", new_data)
- File.write!(pack_file_p, Jason.encode!(full_pack, pretty: true))
-
- # Send new data back with fallback sha filled
- json(conn, new_data)
- end
- defp get_filename(%{"filename" => filename}), do: filename
-
- defp get_filename(%{"file" => file}) do
- case file do
- %Plug.Upload{filename: filename} -> filename
- url when is_binary(url) -> Path.basename(url)
+ {:error, _} ->
+ render_error(
+ conn,
+ :internal_server_error,
+ "Unexpected error occurred while updating pack metadata."
+ )
end
end
- defp empty?(str), do: String.trim(str) == ""
+ def add_file(conn, %{"name" => name} = params) do
+ filename = params["filename"] || get_filename(params["file"])
+ shortcode = params["shortcode"] || Path.basename(filename, Path.extname(filename))
- defp update_file_and_send(conn, updated_full_pack, pack_file_p) do
- # Write the emoji pack file
- File.write!(pack_file_p, Jason.encode!(updated_full_pack, pretty: true))
-
- # Return the modified file list
- json(conn, updated_full_pack["files"])
- end
-
- @doc """
- Updates a file in a pack.
-
- Updating can mean three things:
-
- - `add` adds an emoji named `shortcode` to the pack `pack_name`,
- that means that the emoji file needs to be uploaded with the request
- (thus requiring it to be a multipart request) and be named `file`.
- There can also be an optional `filename` that will be the new emoji file name
- (if it's not there, the name will be taken from the uploaded file).
- - `update` changes emoji shortcode (from `shortcode` to `new_shortcode` or moves the file
- (from the current filename to `new_filename`)
- - `remove` removes the emoji named `shortcode` and it's associated file
- """
-
- # Add
- def update_file(
- conn,
- %{"pack_name" => pack_name, "action" => "add", "shortcode" => shortcode} = params
- ) do
- pack_dir = Path.join(emoji_dir_path(), pack_name)
- pack_file_p = Path.join(pack_dir, "pack.json")
-
- full_pack = Jason.decode!(File.read!(pack_file_p))
-
- with {_, false} <- {:has_shortcode, Map.has_key?(full_pack["files"], shortcode)},
- filename <- get_filename(params),
- false <- empty?(shortcode),
- false <- empty?(filename) do
- file_path = Path.join(pack_dir, filename)
-
- # If the name contains directories, create them
- if String.contains?(file_path, "/") do
- File.mkdir_p!(Path.dirname(file_path))
- end
-
- case params["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
-
- updated_full_pack = put_in(full_pack, ["files", shortcode], filename)
- update_file_and_send(conn, updated_full_pack, pack_file_p)
+ with {:ok, pack} <- Pack.add_file(name, shortcode, filename, params["file"]) do
+ json(conn, pack.files)
else
- {:has_shortcode, _} ->
+ {:exists, _} ->
conn
|> put_status(:conflict)
|> json(%{error: "An emoji with the \"#{shortcode}\" shortcode already exists"})
- true ->
+ {:loaded, _} ->
conn
|> put_status(:bad_request)
- |> json(%{error: "shortcode or filename cannot be empty"})
- end
- end
+ |> json(%{error: "pack \"#{name}\" is not found"})
- # Remove
- def update_file(conn, %{
- "pack_name" => pack_name,
- "action" => "remove",
- "shortcode" => shortcode
- }) do
- pack_dir = Path.join(emoji_dir_path(), pack_name)
- pack_file_p = Path.join(pack_dir, "pack.json")
+ {:error, :empty_values} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "pack name, shortcode or filename cannot be empty"})
- full_pack = Jason.decode!(File.read!(pack_file_p))
+ {:error, _} ->
+ render_error(
+ conn,
+ :internal_server_error,
+ "Unexpected error occurred while adding file to pack."
+ )
+ end
+ end
- if Map.has_key?(full_pack["files"], shortcode) do
- {emoji_file_path, updated_full_pack} = pop_in(full_pack, ["files", shortcode])
+ def update_file(conn, %{"name" => name, "shortcode" => shortcode} = params) do
+ new_shortcode = params["new_shortcode"]
+ new_filename = params["new_filename"]
+ force = params["force"] == true
- emoji_file_path = Path.join(pack_dir, emoji_file_path)
+ with {:ok, pack} <- Pack.update_file(name, shortcode, new_shortcode, new_filename, force) do
+ json(conn, pack.files)
+ else
+ {:exists, _} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "Emoji \"#{shortcode}\" does not exist"})
- # Delete the emoji file
- File.rm!(emoji_file_path)
+ {:not_used, _} ->
+ conn
+ |> put_status(:conflict)
+ |> json(%{
+ error:
+ "New shortcode \"#{new_shortcode}\" is already used. If you want to override emoji use 'force' option"
+ })
- # If the old directory has no more files, remove it
- if String.contains?(emoji_file_path, "/") do
- dir = Path.dirname(emoji_file_path)
+ {:loaded, _} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "pack \"#{name}\" is not found"})
- if Enum.empty?(File.ls!(dir)) do
- File.rmdir!(dir)
- end
- end
+ {:error, :empty_values} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "new_shortcode or new_filename cannot be empty"})
- update_file_and_send(conn, updated_full_pack, pack_file_p)
- else
- conn
- |> put_status(:bad_request)
- |> json(%{error: "Emoji \"#{shortcode}\" does not exist"})
+ {:error, _} ->
+ render_error(
+ conn,
+ :internal_server_error,
+ "Unexpected error occurred while updating file in pack."
+ )
end
end
- # Update
- def update_file(
- conn,
- %{"pack_name" => pack_name, "action" => "update", "shortcode" => shortcode} = params
- ) do
- pack_dir = Path.join(emoji_dir_path(), pack_name)
- pack_file_p = Path.join(pack_dir, "pack.json")
-
- full_pack = Jason.decode!(File.read!(pack_file_p))
-
- with {_, true} <- {:has_shortcode, Map.has_key?(full_pack["files"], shortcode)},
- %{"new_shortcode" => new_shortcode, "new_filename" => new_filename} <- params,
- false <- empty?(new_shortcode),
- false <- empty?(new_filename) do
- # First, remove the old shortcode, saving the old path
- {old_emoji_file_path, updated_full_pack} = pop_in(full_pack, ["files", shortcode])
- old_emoji_file_path = Path.join(pack_dir, old_emoji_file_path)
- new_emoji_file_path = Path.join(pack_dir, new_filename)
-
- # If the name contains directories, create them
- if String.contains?(new_emoji_file_path, "/") do
- File.mkdir_p!(Path.dirname(new_emoji_file_path))
- end
-
- # Move/Rename the old filename to a new filename
- # These are probably on the same filesystem, so just rename should work
- :ok = File.rename(old_emoji_file_path, new_emoji_file_path)
-
- # If the old directory has no more files, remove it
- if String.contains?(old_emoji_file_path, "/") do
- dir = Path.dirname(old_emoji_file_path)
-
- if Enum.empty?(File.ls!(dir)) do
- File.rmdir!(dir)
- end
- end
-
- # Then, put in the new shortcode with the new path
- updated_full_pack = put_in(updated_full_pack, ["files", new_shortcode], new_filename)
- update_file_and_send(conn, updated_full_pack, pack_file_p)
+ def delete_file(conn, %{"name" => name, "shortcode" => shortcode}) do
+ with {:ok, pack} <- Pack.delete_file(name, shortcode) do
+ json(conn, pack.files)
else
- {:has_shortcode, _} ->
+ {:exists, _} ->
conn
|> put_status(:bad_request)
|> json(%{error: "Emoji \"#{shortcode}\" does not exist"})
- true ->
+ {:loaded, _} ->
conn
|> put_status(:bad_request)
- |> json(%{error: "new_shortcode or new_filename cannot be empty"})
+ |> json(%{error: "pack \"#{name}\" is not found"})
- _ ->
+ {:error, :empty_values} ->
conn
|> put_status(:bad_request)
- |> json(%{error: "new_shortcode or new_file were not specified"})
- end
- end
+ |> json(%{error: "pack name or shortcode cannot be empty"})
- def update_file(conn, %{"action" => action}) do
- conn
- |> put_status(:bad_request)
- |> json(%{error: "Unknown action: #{action}"})
+ {:error, _} ->
+ render_error(
+ conn,
+ :internal_server_error,
+ "Unexpected error occurred while removing file from pack."
+ )
+ end
end
- @doc """
- Imports emoji from the filesystem.
-
- Importing means checking all the directories in the
- `$instance_static/emoji/` for directories which do not have
- `pack.json`. If one has an emoji.txt file, that file will be used
- to create a `pack.json` file with it's contents. If the directory has
- neither, all the files with specific configured extenstions will be
- assumed to be emojis and stored in the new `pack.json` file.
- """
- def import_from_fs(conn, _params) do
- emoji_path = emoji_dir_path()
-
- with {:ok, %{access: :read_write}} <- File.stat(emoji_path),
- {:ok, results} <- File.ls(emoji_path) do
- imported_pack_names =
- results
- |> Enum.filter(fn file ->
- dir_path = Path.join(emoji_path, file)
- # Find the directories that do NOT have pack.json
- File.dir?(dir_path) and not File.exists?(Path.join(dir_path, "pack.json"))
- end)
- |> Enum.map(&write_pack_json_contents/1)
-
- json(conn, imported_pack_names)
+ def import_from_filesystem(conn, _params) do
+ with {:ok, names} <- Pack.import_from_filesystem() do
+ json(conn, names)
else
- {:ok, %{access: _}} ->
+ {:error, :no_read_write} ->
conn
|> put_status(:internal_server_error)
|> json(%{error: "Error: emoji pack directory must be writable"})
@@ -600,44 +299,6 @@ keeping it in cache for #{div(cache_ms, 1000)}s")
end
end
- defp write_pack_json_contents(dir) do
- dir_path = Path.join(emoji_dir_path(), dir)
- emoji_txt_path = Path.join(dir_path, "emoji.txt")
-
- files_for_pack = files_for_pack(emoji_txt_path, dir_path)
- pack_json_contents = Jason.encode!(%{pack: %{}, files: files_for_pack})
-
- File.write!(Path.join(dir_path, "pack.json"), pack_json_contents)
-
- dir
- end
-
- defp files_for_pack(emoji_txt_path, dir_path) do
- if File.exists?(emoji_txt_path) do
- # There's an emoji.txt file, it's likely from a pack installed by the pack manager.
- # Make a pack.json file from the contents of that emoji.txt fileh
-
- # FIXME: Copy-pasted from Pleroma.Emoji/load_from_file_stream/2
-
- # Create a map of shortcodes to filenames from emoji.txt
- File.read!(emoji_txt_path)
- |> String.split("\n")
- |> Enum.map(&String.trim/1)
- |> Enum.map(fn line ->
- case String.split(line, ~r/,\s*/) do
- # This matches both strings with and without tags
- # and we don't care about tags here
- [name, file | _] -> {name, file}
- _ -> nil
- end
- end)
- |> Enum.filter(fn x -> not is_nil(x) end)
- |> Enum.into(%{})
- else
- # If there's no emoji.txt, assume all files
- # that are of certain extensions from the config are emojis and import them all
- pack_extensions = Pleroma.Config.get!([:emoji, :pack_extensions])
- Pleroma.Emoji.Loader.make_shortcode_to_file_map(dir_path, pack_extensions)
- end
- end
+ defp get_filename(%Plug.Upload{filename: filename}), do: filename
+ defp get_filename(url) when is_binary(url), do: Path.basename(url)
end
diff --git a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex
index 8cf552b7e..d4e0d8b7c 100644
--- a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.MascotController do
@@ -12,8 +12,6 @@ defmodule Pleroma.Web.PleromaAPI.MascotController do
plug(OAuthScopesPlug, %{scopes: ["read:accounts"]} when action == :show)
plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action != :show)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
-
@doc "GET /api/v1/pleroma/mascot"
def show(%{assigns: %{user: user}} = conn, _params) do
json(conn, User.get_mascot(user))
diff --git a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
index 108e48438..8bc77b75e 100644
--- a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
@@ -1,11 +1,11 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
use Pleroma.Web, :controller
- import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
+ import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2, skip_relationships?: 1]
alias Pleroma.Activity
alias Pleroma.Conversation.Participation
@@ -28,37 +28,53 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
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 == :update_conversation
+ %{scopes: ["write:conversations"]}
+ when action in [:update_conversation, :mark_conversations_as_read]
)
- plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :read_notification)
-
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["write:notifications"]} when action == :mark_notifications_as_read
+ )
- def emoji_reactions_by(%{assigns: %{user: user}} = conn, %{"id" => activity_id}) do
+ 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] ->
- users =
- Enum.map(user_ap_ids, &User.get_cached_by_ap_id/1)
- |> Enum.filter(& &1)
-
- %{
- 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)
- }
+ 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)
@@ -70,7 +86,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
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),
+ with {:ok, _activity} <- CommonAPI.react_with_emoji(activity_id, user, emoji),
activity <- Activity.get_by_id(activity_id) do
conn
|> put_view(StatusView)
@@ -82,7 +98,8 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
"id" => activity_id,
"emoji" => emoji
}) do
- with {:ok, _activity, _object} <- CommonAPI.unreact_with_emoji(activity_id, user, emoji),
+ with {:ok, _activity} <-
+ CommonAPI.unreact_with_emoji(activity_id, user, emoji),
activity <- Activity.get_by_id(activity_id) do
conn
|> put_view(StatusView)
@@ -96,16 +113,20 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController 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: user}} = conn,
+ %{assigns: %{user: %{id: user_id} = user}} = conn,
%{"id" => participation_id} = params
) do
- participation = Participation.get(participation_id, preload: [:conversation])
-
- if user.id == participation.user_id do
+ with %Participation{user_id: ^user_id} = participation <-
+ Participation.get(participation_id, preload: [:conversation]) do
params =
params
|> Map.put("blocking_user", user)
@@ -114,13 +135,24 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
activities =
participation.conversation.ap_id
- |> ActivityPub.fetch_activities_for_context(params)
+ |> 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})
+ |> 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
@@ -128,19 +160,26 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
%{assigns: %{user: user}} = conn,
%{"id" => participation_id, "recipients" => recipients}
) do
- participation =
- participation_id
- |> Participation.get()
-
- with true <- user.id == participation.user_id,
+ 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 read_conversations(%{assigns: %{user: user}} = conn, _params) do
+ 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)
@@ -149,7 +188,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
end
end
- def read_notification(%{assigns: %{user: user}} = conn, %{"id" => notification_id}) do
+ 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)
@@ -162,13 +201,17 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
end
end
- def read_notification(%{assigns: %{user: user}} = conn, %{"max_id" => max_id}) do
+ 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})
+ |> 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 b74b3debc..22da6c0ad 100644
--- a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.ScrobbleController do
@@ -13,10 +13,12 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleController do
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.StatusView
- plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :user_scrobbles)
- plug(OAuthScopesPlug, %{scopes: ["write"]} when action != :user_scrobbles)
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["read"], fallback: :proceed_unauthenticated} when action == :user_scrobbles
+ )
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ plug(OAuthScopesPlug, %{scopes: ["write"]} when action != :user_scrobbles)
def new_scrobble(%{assigns: %{user: user}} = conn, %{"title" => _} = params) do
params =
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..eb9989cdf
--- /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-2019 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/push/impl.ex b/lib/pleroma/web/push/impl.ex
index 34ec1d8d9..691725702 100644
--- a/lib/pleroma/web/push/impl.ex
+++ b/lib/pleroma/web/push/impl.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Push.Impl do
@@ -16,6 +16,8 @@ defmodule Pleroma.Web.Push.Impl do
require Logger
import Ecto.Query
+ defdelegate mastodon_notification_type(activity), to: Activity
+
@types ["Create", "Follow", "Announce", "Like", "Move"]
@doc "Performs sending notifications for user subscriptions"
@@ -24,40 +26,41 @@ defmodule Pleroma.Web.Push.Impl do
%{
activity: %{data: %{"type" => activity_type}} = activity,
user: %User{id: user_id}
- } = notif
+ } = notification
)
when activity_type in @types do
- actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
+ actor = User.get_cached_by_ap_id(notification.activity.data["actor"])
- type = Activity.mastodon_notification_type(notif.activity)
+ mastodon_type = mastodon_notification_type(notification.activity)
gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key)
avatar_url = User.avatar_url(actor)
object = Object.normalize(activity)
user = User.get_cached_by_id(user_id)
direct_conversation_id = Activity.direct_conversation_id(activity, user)
- for subscription <- fetch_subsriptions(user_id),
- get_in(subscription.data, ["alerts", type]) do
+ for subscription <- fetch_subscriptions(user_id),
+ Subscription.enabled?(subscription, mastodon_type) do
%{
access_token: subscription.token.token,
- notification_id: notif.id,
- notification_type: type,
+ notification_id: notification.id,
+ notification_type: mastodon_type,
icon: avatar_url,
preferred_locale: "en",
pleroma: %{
- activity_id: notif.activity.id,
+ activity_id: notification.activity.id,
direct_conversation_id: direct_conversation_id
}
}
- |> Map.merge(build_content(notif, actor, object))
+ |> Map.merge(build_content(notification, actor, object, mastodon_type))
|> Jason.encode!()
|> push_message(build_sub(subscription), gcm_api_key, subscription)
end
+ |> (&{:ok, &1}).()
end
def perform(_) do
Logger.warn("Unknown notification type")
- :error
+ {:error, :unknown_type}
end
@doc "Push message to web"
@@ -82,7 +85,7 @@ defmodule Pleroma.Web.Push.Impl do
end
@doc "Gets user subscriptions"
- def fetch_subsriptions(user_id) do
+ def fetch_subscriptions(user_id) do
Subscription
|> where(user_id: ^user_id)
|> preload(:token)
@@ -99,28 +102,35 @@ defmodule Pleroma.Web.Push.Impl do
}
end
+ def build_content(notification, actor, object, mastodon_type \\ nil)
+
def build_content(
%{
- activity: %{data: %{"directMessage" => true}},
user: %{notification_settings: %{privacy_option: true}}
- },
- actor,
- _
+ } = notification,
+ _actor,
+ _object,
+ mastodon_type
) do
- %{title: "New Direct Message", body: "@#{actor.nickname}"}
+ %{body: format_title(notification, mastodon_type)}
end
- def build_content(notif, actor, object) do
+ def build_content(notification, actor, object, mastodon_type) do
+ mastodon_type = mastodon_type || mastodon_notification_type(notification.activity)
+
%{
- title: format_title(notif),
- body: format_body(notif, actor, object)
+ title: format_title(notification, mastodon_type),
+ body: format_body(notification, actor, object, mastodon_type)
}
end
+ def format_body(activity, actor, object, mastodon_type \\ nil)
+
def format_body(
%{activity: %{data: %{"type" => "Create"}}},
actor,
- %{data: %{"content" => content}}
+ %{data: %{"content" => content}},
+ _mastodon_type
) do
"@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}"
end
@@ -128,33 +138,44 @@ defmodule Pleroma.Web.Push.Impl do
def format_body(
%{activity: %{data: %{"type" => "Announce"}}},
actor,
- %{data: %{"content" => content}}
+ %{data: %{"content" => content}},
+ _mastodon_type
) do
"@#{actor.nickname} repeated: #{Utils.scrub_html_and_truncate(content, 80)}"
end
def format_body(
- %{activity: %{data: %{"type" => type}}},
+ %{activity: %{data: %{"type" => type}}} = notification,
actor,
- _object
+ _object,
+ mastodon_type
)
when type in ["Follow", "Like"] do
- case type do
- "Follow" -> "@#{actor.nickname} has followed you"
- "Like" -> "@#{actor.nickname} has favorited your post"
+ mastodon_type = mastodon_type || mastodon_notification_type(notification.activity)
+
+ case mastodon_type do
+ "follow" -> "@#{actor.nickname} has followed you"
+ "follow_request" -> "@#{actor.nickname} has requested to follow you"
+ "favourite" -> "@#{actor.nickname} has favorited your post"
end
end
- def format_title(%{activity: %{data: %{"directMessage" => true}}}) do
+ def format_title(activity, mastodon_type \\ nil)
+
+ def format_title(%{activity: %{data: %{"directMessage" => true}}}, _mastodon_type) do
"New Direct Message"
end
- def format_title(%{activity: %{data: %{"type" => type}}}) do
- case type do
- "Create" -> "New Mention"
- "Follow" -> "New Follower"
- "Announce" -> "New Repeat"
- "Like" -> "New Favorite"
+ def format_title(%{activity: activity}, mastodon_type) do
+ mastodon_type = mastodon_type || mastodon_notification_type(activity)
+
+ case mastodon_type do
+ "mention" -> "New Mention"
+ "follow" -> "New Follower"
+ "follow_request" -> "New Follow Request"
+ "reblog" -> "New Repeat"
+ "favourite" -> "New Favorite"
+ type -> "New #{String.capitalize(type || "event")}"
end
end
end
diff --git a/lib/pleroma/web/push/push.ex b/lib/pleroma/web/push/push.ex
index 7ef1532ac..b80a6438d 100644
--- a/lib/pleroma/web/push/push.ex
+++ b/lib/pleroma/web/push/push.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Push do
diff --git a/lib/pleroma/web/push/subscription.ex b/lib/pleroma/web/push/subscription.ex
index 988fabaeb..3e401a490 100644
--- a/lib/pleroma/web/push/subscription.ex
+++ b/lib/pleroma/web/push/subscription.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Push.Subscription do
@@ -25,20 +25,28 @@ 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
+ def enabled?(subscription, "follow_request") do
+ enabled?(subscription, "follow")
+ end
+
+ def enabled?(subscription, alert_type) do
+ get_in(subscription.data, ["alerts", alert_type])
+ end
+
def create(
%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/rel_me.ex b/lib/pleroma/web/rel_me.ex
index 540fa65df..8e2b51508 100644
--- a/lib/pleroma/web/rel_me.ex
+++ b/lib/pleroma/web/rel_me.ex
@@ -1,13 +1,11 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RelMe do
- @hackney_options [
+ @options [
pool: :media,
- recv_timeout: 2_000,
- max_body: 2_000_000,
- with_body: true
+ max_body: 2_000_000
]
if Pleroma.Config.get(:env) == :test do
@@ -25,8 +23,18 @@ defmodule Pleroma.Web.RelMe do
def parse(_), do: {:error, "No URL provided"}
defp parse_url(url) do
+ opts =
+ if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do
+ Keyword.merge(@options,
+ recv_timeout: 2_000,
+ with_body: true
+ )
+ else
+ @options
+ end
+
with {:ok, %Tesla.Env{body: html, status: status}} when status in 200..299 <-
- Pleroma.HTTP.get(url, [], adapter: @hackney_options),
+ Pleroma.HTTP.get(url, [], adapter: opts),
{:ok, html_tree} <- Floki.parse_document(html),
data <-
Floki.attribute(html_tree, "link[rel~=me]", "href") ++
diff --git a/lib/pleroma/web/rich_media/helpers.ex b/lib/pleroma/web/rich_media/helpers.ex
index 6506de46c..9d3d7f978 100644
--- a/lib/pleroma/web/rich_media/helpers.ex
+++ b/lib/pleroma/web/rich_media/helpers.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright _ 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright _ 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Helpers do
@@ -64,5 +64,8 @@ defmodule Pleroma.Web.RichMedia.Helpers do
def fetch_data_for_activity(_), do: %{}
- def perform(:fetch, %Activity{} = activity), do: fetch_data_for_activity(activity)
+ def perform(:fetch, %Activity{} = activity) do
+ fetch_data_for_activity(activity)
+ :ok
+ end
end
diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex
index 9702e90f1..40980def8 100644
--- a/lib/pleroma/web/rich_media/parser.ex
+++ b/lib/pleroma/web/rich_media/parser.ex
@@ -1,13 +1,11 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Parser do
- @hackney_options [
+ @options [
pool: :media,
- recv_timeout: 2_000,
- max_body: 2_000_000,
- with_body: true
+ max_body: 2_000_000
]
defp parsers do
@@ -77,8 +75,18 @@ defmodule Pleroma.Web.RichMedia.Parser do
end
defp parse_url(url) do
+ opts =
+ if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do
+ Keyword.merge(@options,
+ recv_timeout: 2_000,
+ with_body: true
+ )
+ else
+ @options
+ end
+
try do
- {:ok, %Tesla.Env{body: html}} = Pleroma.HTTP.get(url, [], adapter: @hackney_options)
+ {:ok, %Tesla.Env{body: html}} = Pleroma.HTTP.get(url, [], adapter: opts)
html
|> parse_html()
diff --git a/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex b/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex
index fae3c462e..ae0f36702 100644
--- a/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex
+++ b/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Parsers.MetaTagsParser do
diff --git a/lib/pleroma/web/rich_media/parsers/oembed_parser.ex b/lib/pleroma/web/rich_media/parsers/oembed_parser.ex
index 875637c4d..8f32bf91b 100644
--- a/lib/pleroma/web/rich_media/parsers/oembed_parser.ex
+++ b/lib/pleroma/web/rich_media/parsers/oembed_parser.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Parsers.OEmbed do
diff --git a/lib/pleroma/web/rich_media/parsers/ogp.ex b/lib/pleroma/web/rich_media/parsers/ogp.ex
index d40fa009f..3e9012588 100644
--- a/lib/pleroma/web/rich_media/parsers/ogp.ex
+++ b/lib/pleroma/web/rich_media/parsers/ogp.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Parsers.OGP do
diff --git a/lib/pleroma/web/rich_media/parsers/twitter_card.ex b/lib/pleroma/web/rich_media/parsers/twitter_card.ex
index afaa98f3d..09d4b526e 100644
--- a/lib/pleroma/web/rich_media/parsers/twitter_card.ex
+++ b/lib/pleroma/web/rich_media/parsers/twitter_card.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Parsers.TwitterCard do
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 897215698..7a171f9fb 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Router do
@@ -16,75 +16,70 @@ defmodule Pleroma.Web.Router do
plug(Pleroma.Plugs.UserEnabledPlug)
end
- pipeline :api do
- plug(:accepts, ["json"])
- plug(:fetch_session)
+ pipeline :expect_authentication do
+ plug(Pleroma.Plugs.ExpectAuthenticatedCheckPlug)
+ end
+
+ pipeline :expect_public_instance_or_authentication do
+ plug(Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug)
+ end
+
+ pipeline :authenticate do
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.BasicAuthDecoderPlug)
plug(Pleroma.Plugs.UserFetcherPlug)
plug(Pleroma.Plugs.SessionAuthenticationPlug)
plug(Pleroma.Plugs.LegacyAuthenticationPlug)
plug(Pleroma.Plugs.AuthenticationPlug)
+ end
+
+ pipeline :after_auth do
plug(Pleroma.Plugs.UserEnabledPlug)
plug(Pleroma.Plugs.SetUserSessionIdPlug)
plug(Pleroma.Plugs.EnsureUserKeyPlug)
- plug(Pleroma.Plugs.IdempotencyPlug)
end
- pipeline :authenticated_api do
+ pipeline :base_api do
plug(:accepts, ["json"])
plug(:fetch_session)
- plug(Pleroma.Plugs.OAuthPlug)
- plug(Pleroma.Plugs.BasicAuthDecoderPlug)
- plug(Pleroma.Plugs.UserFetcherPlug)
- plug(Pleroma.Plugs.SessionAuthenticationPlug)
- plug(Pleroma.Plugs.LegacyAuthenticationPlug)
- plug(Pleroma.Plugs.AuthenticationPlug)
- plug(Pleroma.Plugs.UserEnabledPlug)
- plug(Pleroma.Plugs.SetUserSessionIdPlug)
+ plug(:authenticate)
+ plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
+ end
+
+ pipeline :api do
+ plug(:expect_public_instance_or_authentication)
+ plug(:base_api)
+ plug(:after_auth)
+ plug(Pleroma.Plugs.IdempotencyPlug)
+ end
+
+ pipeline :authenticated_api do
+ plug(:expect_authentication)
+ plug(:base_api)
+ plug(:after_auth)
plug(Pleroma.Plugs.EnsureAuthenticatedPlug)
plug(Pleroma.Plugs.IdempotencyPlug)
end
pipeline :admin_api do
- plug(:accepts, ["json"])
- plug(:fetch_session)
- plug(Pleroma.Plugs.OAuthPlug)
- plug(Pleroma.Plugs.BasicAuthDecoderPlug)
- plug(Pleroma.Plugs.UserFetcherPlug)
- plug(Pleroma.Plugs.SessionAuthenticationPlug)
- plug(Pleroma.Plugs.LegacyAuthenticationPlug)
- plug(Pleroma.Plugs.AuthenticationPlug)
+ plug(:expect_authentication)
+ plug(:base_api)
plug(Pleroma.Plugs.AdminSecretAuthenticationPlug)
- plug(Pleroma.Plugs.UserEnabledPlug)
- plug(Pleroma.Plugs.SetUserSessionIdPlug)
+ plug(:after_auth)
plug(Pleroma.Plugs.EnsureAuthenticatedPlug)
plug(Pleroma.Plugs.UserIsAdminPlug)
plug(Pleroma.Plugs.IdempotencyPlug)
end
pipeline :mastodon_html do
- plug(:accepts, ["html"])
- plug(:fetch_session)
- plug(Pleroma.Plugs.OAuthPlug)
- plug(Pleroma.Plugs.BasicAuthDecoderPlug)
- plug(Pleroma.Plugs.UserFetcherPlug)
- plug(Pleroma.Plugs.SessionAuthenticationPlug)
- plug(Pleroma.Plugs.LegacyAuthenticationPlug)
- plug(Pleroma.Plugs.AuthenticationPlug)
- plug(Pleroma.Plugs.UserEnabledPlug)
- plug(Pleroma.Plugs.SetUserSessionIdPlug)
- plug(Pleroma.Plugs.EnsureUserKeyPlug)
+ plug(:browser)
+ plug(:authenticate)
+ plug(:after_auth)
end
pipeline :pleroma_html do
- plug(:accepts, ["html"])
- plug(:fetch_session)
- plug(Pleroma.Plugs.OAuthPlug)
- plug(Pleroma.Plugs.BasicAuthDecoderPlug)
- plug(Pleroma.Plugs.UserFetcherPlug)
- plug(Pleroma.Plugs.SessionAuthenticationPlug)
- plug(Pleroma.Plugs.AuthenticationPlug)
+ plug(:browser)
+ plug(:authenticate)
plug(Pleroma.Plugs.EnsureUserKeyPlug)
end
@@ -94,10 +89,12 @@ defmodule Pleroma.Web.Router do
pipeline :config do
plug(:accepts, ["json", "xml"])
+ plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
end
pipeline :pleroma_api do
plug(:accepts, ["html", "json"])
+ plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
end
pipeline :mailbox_preview do
@@ -135,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)
@@ -173,6 +171,8 @@ defmodule Pleroma.Web.Router do
get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset)
patch("/users/force_password_reset", AdminAPIController, :force_password_reset)
+ get("/users/:nickname/credentials", AdminAPIController, :show_user_credentials)
+ patch("/users/:nickname/credentials", AdminAPIController, :update_user_credentials)
get("/users", AdminAPIController, :list_users)
get("/users/:nickname", AdminAPIController, :user_show)
@@ -184,44 +184,56 @@ defmodule Pleroma.Web.Router do
patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email)
get("/reports", AdminAPIController, :list_reports)
- get("/grouped_reports", AdminAPIController, :list_grouped_reports)
get("/reports/:id", AdminAPIController, :report_show)
patch("/reports", AdminAPIController, :reports_update)
post("/reports/:id/notes", AdminAPIController, :report_notes_create)
delete("/reports/:report_id/notes/:id", AdminAPIController, :report_notes_delete)
+ get("/statuses/:id", AdminAPIController, :status_show)
put("/statuses/:id", AdminAPIController, :status_update)
delete("/statuses/:id", AdminAPIController, :status_delete)
+ get("/statuses", AdminAPIController, :list_statuses)
get("/config", AdminAPIController, :config_show)
post("/config", AdminAPIController, :config_update)
get("/config/descriptions", AdminAPIController, :config_descriptions)
+ get("/need_reboot", AdminAPIController, :need_reboot)
get("/restart", AdminAPIController, :restart)
get("/moderation_log", AdminAPIController, :list_log)
post("/reload_emoji", AdminAPIController, :reload_emoji)
+ get("/stats", AdminAPIController, :stats)
+
+ get("/oauth_app", AdminAPIController, :oauth_app_list)
+ post("/oauth_app", AdminAPIController, :oauth_app_create)
+ patch("/oauth_app/:id", AdminAPIController, :oauth_app_update)
+ delete("/oauth_app/:id", AdminAPIController, :oauth_app_delete)
end
scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do
+ # Modifying packs
scope "/packs" do
- # Modifying packs
pipe_through(:admin_api)
- post("/import_from_fs", EmojiAPIController, :import_from_fs)
+ get("/import", EmojiAPIController, :import_from_filesystem)
+ get("/remote", EmojiAPIController, :remote)
+ post("/download", EmojiAPIController, :download)
- post("/:pack_name/update_file", EmojiAPIController, :update_file)
- post("/:pack_name/update_metadata", EmojiAPIController, :update_metadata)
- put("/:name", EmojiAPIController, :create)
+ post("/:name", EmojiAPIController, :create)
+ patch("/:name", EmojiAPIController, :update)
delete("/:name", EmojiAPIController, :delete)
- post("/download_from", EmojiAPIController, :download_from)
- post("/list_from", EmojiAPIController, :list_from)
+
+ post("/:name/files", EmojiAPIController, :add_file)
+ patch("/:name/files", EmojiAPIController, :update_file)
+ delete("/:name/files", EmojiAPIController, :delete_file)
end
+ # Pack info / downloading
scope "/packs" do
- # Pack info / downloading
- get("/", EmojiAPIController, :list_packs)
- get("/:name/download_shared/", EmojiAPIController, :download_shared)
+ get("/", EmojiAPIController, :list)
+ get("/:name", EmojiAPIController, :show)
+ get("/:name/archive", EmojiAPIController, :archive)
end
end
@@ -247,6 +259,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)
@@ -258,6 +280,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)
@@ -271,6 +297,7 @@ 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)
end
@@ -280,7 +307,7 @@ defmodule Pleroma.Web.Router do
get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses)
get("/conversations/:id", PleromaAPIController, :conversation)
- post("/conversations/read", PleromaAPIController, :read_conversations)
+ post("/conversations/read", PleromaAPIController, :mark_conversations_as_read)
end
scope [] do
@@ -289,7 +316,7 @@ defmodule Pleroma.Web.Router do
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, :read_notification)
+ post("/notifications/read", PleromaAPIController, :mark_notifications_as_read)
patch("/accounts/update_avatar", AccountController, :update_avatar)
patch("/accounts/update_banner", AccountController, :update_banner)
@@ -325,51 +352,84 @@ defmodule Pleroma.Web.Router do
pipe_through(:authenticated_api)
get("/accounts/verify_credentials", AccountController, :verify_credentials)
+ patch("/accounts/update_credentials", AccountController, :update_credentials)
get("/accounts/relationships", AccountController, :relationships)
-
get("/accounts/:id/lists", AccountController, :lists)
- get("/accounts/:id/identity_proofs", MastodonAPIController, :empty_array)
-
- get("/follow_requests", FollowRequestController, :index)
+ get("/accounts/:id/identity_proofs", AccountController, :identity_proofs)
+ get("/endorsements", AccountController, :endorsements)
get("/blocks", AccountController, :blocks)
get("/mutes", AccountController, :mutes)
- get("/timelines/home", TimelineController, :home)
- get("/timelines/direct", TimelineController, :direct)
+ post("/follows", AccountController, :follow_by_uri)
+ post("/accounts/:id/follow", AccountController, :follow)
+ post("/accounts/:id/unfollow", AccountController, :unfollow)
+ post("/accounts/:id/block", AccountController, :block)
+ post("/accounts/:id/unblock", AccountController, :unblock)
+ post("/accounts/:id/mute", AccountController, :mute)
+ post("/accounts/:id/unmute", AccountController, :unmute)
- get("/favourites", StatusController, :favourites)
- get("/bookmarks", StatusController, :bookmarks)
+ get("/apps/verify_credentials", AppController, :verify_credentials)
- get("/notifications", NotificationController, :index)
- get("/notifications/:id", NotificationController, :show)
- post("/notifications/clear", NotificationController, :clear)
- post("/notifications/dismiss", NotificationController, :dismiss)
- delete("/notifications/destroy_multiple", NotificationController, :destroy_multiple)
+ get("/conversations", ConversationController, :index)
+ post("/conversations/:id/read", ConversationController, :mark_as_read)
- get("/scheduled_statuses", ScheduledActivityController, :index)
- get("/scheduled_statuses/:id", ScheduledActivityController, :show)
+ get("/domain_blocks", DomainBlockController, :index)
+ post("/domain_blocks", DomainBlockController, :create)
+ delete("/domain_blocks", DomainBlockController, :delete)
+
+ get("/filters", FilterController, :index)
+
+ post("/filters", FilterController, :create)
+ get("/filters/:id", FilterController, :show)
+ put("/filters/:id", FilterController, :update)
+ delete("/filters/:id", FilterController, :delete)
+
+ get("/follow_requests", FollowRequestController, :index)
+ post("/follow_requests/:id/authorize", FollowRequestController, :authorize)
+ post("/follow_requests/:id/reject", FollowRequestController, :reject)
get("/lists", ListController, :index)
get("/lists/:id", ListController, :show)
get("/lists/:id/accounts", ListController, :list_accounts)
- get("/domain_blocks", DomainBlockController, :index)
+ delete("/lists/:id", ListController, :delete)
+ post("/lists", ListController, :create)
+ put("/lists/:id", ListController, :update)
+ post("/lists/:id/accounts", ListController, :add_to_list)
+ delete("/lists/:id/accounts", ListController, :remove_from_list)
- get("/filters", FilterController, :index)
+ get("/markers", MarkerController, :index)
+ post("/markers", MarkerController, :upsert)
- get("/suggestions", SuggestionController, :index)
+ post("/media", MediaController, :create)
+ put("/media/:id", MediaController, :update)
- get("/conversations", ConversationController, :index)
- post("/conversations/:id/read", ConversationController, :read)
+ get("/notifications", NotificationController, :index)
+ get("/notifications/:id", NotificationController, :show)
- get("/endorsements", AccountController, :endorsements)
+ post("/notifications/:id/dismiss", NotificationController, :dismiss)
+ post("/notifications/clear", NotificationController, :clear)
+ delete("/notifications/destroy_multiple", NotificationController, :destroy_multiple)
+ # Deprecated: was removed in Mastodon v3, use `/notifications/:id/dismiss` instead
+ post("/notifications/dismiss", NotificationController, :dismiss_via_body)
- patch("/accounts/update_credentials", AccountController, :update_credentials)
+ post("/polls/:id/votes", PollController, :vote)
+
+ post("/reports", ReportController, :create)
+
+ get("/scheduled_statuses", ScheduledActivityController, :index)
+ get("/scheduled_statuses/:id", ScheduledActivityController, :show)
+
+ put("/scheduled_statuses/:id", ScheduledActivityController, :update)
+ delete("/scheduled_statuses/:id", ScheduledActivityController, :delete)
+
+ # Unlike `GET /api/v1/accounts/:id/favourites`, demands authentication
+ get("/favourites", StatusController, :favourites)
+ get("/bookmarks", StatusController, :bookmarks)
post("/statuses", StatusController, :create)
delete("/statuses/:id", StatusController, :delete)
-
post("/statuses/:id/reblog", StatusController, :reblog)
post("/statuses/:id/unreblog", StatusController, :unreblog)
post("/statuses/:id/favourite", StatusController, :favourite)
@@ -381,49 +441,16 @@ defmodule Pleroma.Web.Router do
post("/statuses/:id/mute", StatusController, :mute_conversation)
post("/statuses/:id/unmute", StatusController, :unmute_conversation)
- put("/scheduled_statuses/:id", ScheduledActivityController, :update)
- delete("/scheduled_statuses/:id", ScheduledActivityController, :delete)
-
- post("/polls/:id/votes", PollController, :vote)
-
- post("/media", MediaController, :create)
- put("/media/:id", MediaController, :update)
-
- delete("/lists/:id", ListController, :delete)
- post("/lists", ListController, :create)
- put("/lists/:id", ListController, :update)
-
- post("/lists/:id/accounts", ListController, :add_to_list)
- delete("/lists/:id/accounts", ListController, :remove_from_list)
-
- post("/filters", FilterController, :create)
- get("/filters/:id", FilterController, :show)
- put("/filters/:id", FilterController, :update)
- delete("/filters/:id", FilterController, :delete)
-
- post("/reports", ReportController, :create)
-
- post("/follows", AccountController, :follows)
- post("/accounts/:id/follow", AccountController, :follow)
- post("/accounts/:id/unfollow", AccountController, :unfollow)
- post("/accounts/:id/block", AccountController, :block)
- post("/accounts/:id/unblock", AccountController, :unblock)
- post("/accounts/:id/mute", AccountController, :mute)
- post("/accounts/:id/unmute", AccountController, :unmute)
-
- post("/follow_requests/:id/authorize", FollowRequestController, :authorize)
- post("/follow_requests/:id/reject", FollowRequestController, :reject)
-
- post("/domain_blocks", DomainBlockController, :create)
- delete("/domain_blocks", DomainBlockController, :delete)
-
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)
- get("/markers", MarkerController, :index)
- post("/markers", MarkerController, :upsert)
+ get("/suggestions", SuggestionController, :index)
+
+ get("/timelines/home", TimelineController, :home)
+ get("/timelines/direct", TimelineController, :direct)
+ get("/timelines/list/:list_id", TimelineController, :list)
end
scope "/api/web", Pleroma.Web do
@@ -435,15 +462,24 @@ defmodule Pleroma.Web.Router do
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:api)
- post("/accounts", AccountController, :create)
get("/accounts/search", SearchController, :account_search)
+ get("/search", SearchController, :search)
+
+ get("/accounts/:id/statuses", AccountController, :statuses)
+ get("/accounts/:id/followers", AccountController, :followers)
+ get("/accounts/:id/following", AccountController, :following)
+ get("/accounts/:id", AccountController, :show)
+
+ post("/accounts", AccountController, :create)
get("/instance", InstanceController, :show)
get("/instance/peers", InstanceController, :peers)
post("/apps", AppController, :create)
- get("/apps/verify_credentials", AppController, :verify_credentials)
+ get("/statuses", StatusController, :index)
+ get("/statuses/:id", StatusController, :show)
+ get("/statuses/:id/context", StatusController, :context)
get("/statuses/:id/card", StatusController, :card)
get("/statuses/:id/favourited_by", StatusController, :favourited_by)
get("/statuses/:id/reblogged_by", StatusController, :reblogged_by)
@@ -454,20 +490,8 @@ defmodule Pleroma.Web.Router do
get("/timelines/public", TimelineController, :public)
get("/timelines/tag/:tag", TimelineController, :hashtag)
- get("/timelines/list/:list_id", TimelineController, :list)
-
- get("/statuses", StatusController, :index)
- get("/statuses/:id", StatusController, :show)
- get("/statuses/:id/context", StatusController, :context)
get("/polls/:id", PollController, :show)
-
- get("/accounts/:id/statuses", AccountController, :statuses)
- get("/accounts/:id/followers", AccountController, :followers)
- get("/accounts/:id/following", AccountController, :following)
- get("/accounts/:id", AccountController, :show)
-
- get("/search", SearchController, :search)
end
scope "/api/v2", Pleroma.Web.MastodonAPI do
@@ -496,21 +520,27 @@ defmodule Pleroma.Web.Router do
)
end
+ scope "/api" do
+ pipe_through(:base_api)
+
+ get("/openapi", OpenApiSpex.Plug.RenderSpec, [])
+ end
+
scope "/api", Pleroma.Web, as: :authenticated_twitter_api do
pipe_through(:authenticated_api)
get("/oauth_tokens", TwitterAPI.Controller, :oauth_tokens)
delete("/oauth_tokens/:id", TwitterAPI.Controller, :revoke_token)
- post("/qvitter/statuses/notifications/read", TwitterAPI.Controller, :notifications_read)
- end
-
- pipeline :ap_service_actor do
- plug(:accepts, ["activity+json", "json"])
+ post(
+ "/qvitter/statuses/notifications/read",
+ TwitterAPI.Controller,
+ :mark_notifications_as_read
+ )
end
pipeline :ostatus do
- plug(:accepts, ["html", "xml", "atom", "activity+json", "json"])
+ plug(:accepts, ["html", "xml", "rss", "atom", "activity+json", "json"])
plug(Pleroma.Plugs.StaticFEPlug)
end
@@ -519,8 +549,7 @@ defmodule Pleroma.Web.Router do
end
scope "/", Pleroma.Web do
- pipe_through(:ostatus)
- pipe_through(:http_signature)
+ pipe_through([:ostatus, :http_signature])
get("/objects/:uuid", OStatus.OStatusController, :object)
get("/activities/:uuid", OStatus.OStatusController, :activity)
@@ -538,12 +567,6 @@ defmodule Pleroma.Web.Router do
get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe)
end
- pipeline :activitypub do
- plug(:accepts, ["activity+json", "json"])
- plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
- plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug)
- end
-
scope "/", Pleroma.Web.ActivityPub do
# XXX: not really ostatus
pipe_through(:ostatus)
@@ -551,18 +574,22 @@ defmodule Pleroma.Web.Router do
get("/users/:nickname/outbox", ActivityPubController, :outbox)
end
- pipeline :activitypub_client do
+ pipeline :ap_service_actor do
plug(:accepts, ["activity+json", "json"])
+ end
+
+ # Server to Server (S2S) AP interactions
+ pipeline :activitypub do
+ plug(:ap_service_actor)
+ plug(:http_signature)
+ end
+
+ # Client to Server (C2S) AP interactions
+ pipeline :activitypub_client do
+ plug(:ap_service_actor)
plug(:fetch_session)
- plug(Pleroma.Plugs.OAuthPlug)
- plug(Pleroma.Plugs.BasicAuthDecoderPlug)
- plug(Pleroma.Plugs.UserFetcherPlug)
- plug(Pleroma.Plugs.SessionAuthenticationPlug)
- plug(Pleroma.Plugs.LegacyAuthenticationPlug)
- plug(Pleroma.Plugs.AuthenticationPlug)
- plug(Pleroma.Plugs.UserEnabledPlug)
- plug(Pleroma.Plugs.SetUserSessionIdPlug)
- plug(Pleroma.Plugs.EnsureUserKeyPlug)
+ plug(:authenticate)
+ plug(:after_auth)
end
scope "/", Pleroma.Web.ActivityPub do
@@ -574,6 +601,7 @@ defmodule Pleroma.Web.Router do
post("/users/:nickname/outbox", ActivityPubController, :update_outbox)
post("/api/ap/upload_media", ActivityPubController, :upload_media)
+ # The following two are S2S as well, see `ActivityPub.fetch_follow_information_for_user/1`:
get("/users/:nickname/followers", ActivityPubController, :followers)
get("/users/:nickname/following", ActivityPubController, :following)
end
@@ -594,8 +622,8 @@ defmodule Pleroma.Web.Router do
post("/inbox", ActivityPubController, :inbox)
end
- get("/following", ActivityPubController, :following, assigns: %{relay: true})
- get("/followers", ActivityPubController, :followers, assigns: %{relay: true})
+ get("/following", ActivityPubController, :relay_following)
+ get("/followers", ActivityPubController, :relay_followers)
end
scope "/internal/fetch", Pleroma.Web.ActivityPub do
@@ -634,12 +662,7 @@ defmodule Pleroma.Web.Router do
get("/web/*path", MastoFEController, :index)
end
- pipeline :remote_media do
- end
-
scope "/proxy/", Pleroma.Web.MediaProxy do
- pipe_through(:remote_media)
-
get("/:sig/:url", MediaProxyController, :remote)
get("/:sig/:url/:filename", MediaProxyController, :remote)
end
@@ -652,6 +675,34 @@ defmodule Pleroma.Web.Router do
end
end
+ # Test-only routes needed to test action dispatching and plug chain execution
+ if Pleroma.Config.get(:env) == :test do
+ @test_actions [
+ :do_oauth_check,
+ :fallback_oauth_check,
+ :skip_oauth_check,
+ :fallback_oauth_skip_publicity_check,
+ :skip_oauth_skip_publicity_check,
+ :missing_oauth_check_definition
+ ]
+
+ scope "/test/api", Pleroma.Tests do
+ pipe_through(:api)
+
+ for action <- @test_actions do
+ get("/#{action}", AuthTestController, action)
+ end
+ end
+
+ scope "/test/authenticated_api", Pleroma.Tests do
+ pipe_through(:authenticated_api)
+
+ for action <- @test_actions do
+ get("/#{action}", AuthTestController, action)
+ end
+ end
+ end
+
scope "/", Pleroma.Web.MongooseIM do
get("/user_exists", MongooseIMController, :user_exists)
get("/check_password", MongooseIMController, :check_password)
diff --git a/lib/pleroma/web/static_fe/static_fe_controller.ex b/lib/pleroma/web/static_fe/static_fe_controller.ex
index 8ccf15f4b..c3efb6651 100644
--- a/lib/pleroma/web/static_fe/static_fe_controller.ex
+++ b/lib/pleroma/web/static_fe/static_fe_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.StaticFE.StaticFEController do
@@ -17,6 +17,10 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do
plug(:put_view, Pleroma.Web.StaticFE.StaticFEView)
plug(:assign_id)
+ plug(Pleroma.Plugs.EnsureAuthenticatedPlug,
+ unless_func: &Pleroma.Web.FederatingPlug.federating?/1
+ )
+
@page_keys ["max_id", "min_id", "limit", "since_id", "order"]
defp get_title(%Object{data: %{"name" => name}}) when is_binary(name),
@@ -33,7 +37,7 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do
|> render("error.html", %{message: message, meta: ""})
end
- def get_counts(%Activity{} = activity) do
+ defp get_counts(%Activity{} = activity) do
%Object{data: data} = Object.normalize(activity)
%{
@@ -43,9 +47,9 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do
}
end
- def represent(%Activity{} = activity), do: represent(activity, false)
+ defp represent(%Activity{} = activity), do: represent(activity, false)
- def represent(%Activity{object: %Object{data: data}} = activity, selected) do
+ defp represent(%Activity{object: %Object{data: data}} = activity, selected) do
{:ok, user} = User.get_or_fetch(activity.object.data["actor"])
link =
@@ -54,10 +58,19 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do
_ -> data["url"] || data["external_url"] || data["id"]
end
+ content =
+ if data["content"] do
+ data["content"]
+ |> Pleroma.HTML.filter_tags()
+ |> Pleroma.Emoji.Formatter.emojify(Map.get(data, "emoji", %{}))
+ else
+ nil
+ end
+
%{
- user: user,
+ user: User.sanitize_html(user),
title: get_title(activity.object),
- content: data["content"] || nil,
+ content: content,
attachment: data["attachment"],
link: link,
published: data["published"],
@@ -109,7 +122,7 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do
next_page_id = List.last(timeline) && List.last(timeline).id
render(conn, "profile.html", %{
- user: user,
+ user: User.sanitize_html(user),
timeline: timeline,
prev_page_id: prev_page_id,
next_page_id: next_page_id,
@@ -147,17 +160,17 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do
end
end
- def assign_id(%{path_info: ["notice", notice_id]} = conn, _opts),
+ defp assign_id(%{path_info: ["notice", notice_id]} = conn, _opts),
do: assign(conn, :notice_id, notice_id)
- def assign_id(%{path_info: ["users", user_id]} = conn, _opts),
+ defp assign_id(%{path_info: ["users", user_id]} = conn, _opts),
do: assign(conn, :username_or_id, user_id)
- def assign_id(%{path_info: ["objects", object_id]} = conn, _opts),
+ defp assign_id(%{path_info: ["objects", object_id]} = conn, _opts),
do: assign(conn, :object_id, object_id)
- def assign_id(%{path_info: ["activities", activity_id]} = conn, _opts),
+ defp assign_id(%{path_info: ["activities", activity_id]} = conn, _opts),
do: assign(conn, :activity_id, activity_id)
- def assign_id(conn, _opts), do: conn
+ defp assign_id(conn, _opts), do: conn
end
diff --git a/lib/pleroma/web/static_fe/static_fe_view.ex b/lib/pleroma/web/static_fe/static_fe_view.ex
index 821ece9a9..b3d1d1ec8 100644
--- a/lib/pleroma/web/static_fe/static_fe_view.ex
+++ b/lib/pleroma/web/static_fe/static_fe_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.StaticFE.StaticFEView do
@@ -18,15 +18,6 @@ defmodule Pleroma.Web.StaticFE.StaticFEView do
@media_types ["image", "audio", "video"]
- def emoji_for_user(%User{} = user) do
- user.source_data
- |> Map.get("tag", [])
- |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
- |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
- {String.trim(name, ":"), url}
- end)
- end
-
def fetch_media_type(%{"mediaType" => mediaType}) do
Utils.fetch_media_type(@media_types, mediaType)
end
diff --git a/lib/pleroma/web/streamer/ping.ex b/lib/pleroma/web/streamer/ping.ex
deleted file mode 100644
index db3e68abe..000000000
--- a/lib/pleroma/web/streamer/ping.ex
+++ /dev/null
@@ -1,37 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 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 5ce3ebb8a..000000000
--- a/lib/pleroma/web/streamer/state.ex
+++ /dev/null
@@ -1,82 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 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 2fc7ac8cf..49a400df7 100644
--- a/lib/pleroma/web/streamer/streamer.ex
+++ b/lib/pleroma/web/streamer/streamer.ex
@@ -1,55 +1,290 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
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 cf0fa3077..000000000
--- a/lib/pleroma/web/streamer/streamer_socket.ex
+++ /dev/null
@@ -1,35 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 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 ec5985085..000000000
--- a/lib/pleroma/web/streamer/supervisor.ex
+++ /dev/null
@@ -1,37 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 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 5392c1ec3..000000000
--- a/lib/pleroma/web/streamer/worker.ex
+++ /dev/null
@@ -1,226 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 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_relations_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, %Activity{data: %{"type" => "Announce"}} = 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
-
- 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/feed/feed/_activity.xml.eex b/lib/pleroma/web/templates/feed/feed/_activity.atom.eex
index ac8a75009..78350f2aa 100644
--- a/lib/pleroma/web/templates/feed/feed/_activity.xml.eex
+++ b/lib/pleroma/web/templates/feed/feed/_activity.atom.eex
@@ -2,10 +2,10 @@
<activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
<id><%= @data["id"] %></id>
- <title><%= activity_title(@object, Keyword.get(@feed_config, :post_title, %{})) %></title>
- <content type="html"><%= activity_content(@object) %></content>
- <published><%= @data["published"] %></published>
- <updated><%= @data["published"] %></updated>
+ <title><%= activity_title(@data, Keyword.get(@feed_config, :post_title, %{})) %></title>
+ <content type="html"><%= activity_content(@data) %></content>
+ <published><%= @activity.data["published"] %></published>
+ <updated><%= @activity.data["published"] %></updated>
<ostatus:conversation ref="<%= activity_context(@activity) %>">
<%= activity_context(@activity) %>
</ostatus:conversation>
diff --git a/lib/pleroma/web/templates/feed/feed/_activity.rss.eex b/lib/pleroma/web/templates/feed/feed/_activity.rss.eex
new file mode 100644
index 000000000..a304a16af
--- /dev/null
+++ b/lib/pleroma/web/templates/feed/feed/_activity.rss.eex
@@ -0,0 +1,49 @@
+<item>
+ <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
+ <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+ <guid><%= @data["id"] %></guid>
+ <title><%= activity_title(@data, Keyword.get(@feed_config, :post_title, %{})) %></title>
+ <description><%= activity_content(@data) %></description>
+ <pubDate><%= @activity.data["published"] %></pubDate>
+ <updated><%= @activity.data["published"] %></updated>
+ <ostatus:conversation ref="<%= activity_context(@activity) %>">
+ <%= activity_context(@activity) %>
+ </ostatus:conversation>
+ <link rel="ostatus:conversation"><%= activity_context(@activity) %></link>
+
+ <%= if @data["summary"] do %>
+ <description><%= @data["summary"] %></description>
+ <% end %>
+
+ <%= if @activity.local do %>
+ <link><%= @data["id"] %></link>
+ <% else %>
+ <link><%= @data["external_url"] %></link>
+ <% end %>
+
+ <%= for tag <- @data["tag"] || [] do %>
+ <category term="<%= tag %>"></category>
+ <% end %>
+
+ <%= for attachment <- @data["attachment"] || [] do %>
+ <link type="<%= attachment_type(attachment) %>"><%= attachment_href(attachment) %></link>
+ <% end %>
+
+ <%= if @data["inReplyTo"] do %>
+ <thr:in-reply-to ref='<%= @data["inReplyTo"] %>' href='<%= get_href(@data["inReplyTo"]) %>'/>
+ <% end %>
+
+ <%= for id <- @activity.recipients do %>
+ <%= if id == Pleroma.Constants.as_public() do %>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection">http://activityschema.org/collection/public</link>
+ <% else %>
+ <%= unless Regex.match?(~r/^#{Pleroma.Web.base_url()}.+followers$/, id) do %>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person"><%= id %></link>
+ <% end %>
+ <% end %>
+ <% end %>
+
+ <%= for {emoji, file} <- @data["emoji"] || %{} do %>
+ <link name="<%= emoji %>" rel="emoji"><%= file %></link>
+ <% end %>
+</item>
diff --git a/lib/pleroma/web/templates/feed/feed/_author.xml.eex b/lib/pleroma/web/templates/feed/feed/_author.atom.eex
index 25cbffada..25cbffada 100644
--- a/lib/pleroma/web/templates/feed/feed/_author.xml.eex
+++ b/lib/pleroma/web/templates/feed/feed/_author.atom.eex
diff --git a/lib/pleroma/web/templates/feed/feed/_author.rss.eex b/lib/pleroma/web/templates/feed/feed/_author.rss.eex
new file mode 100644
index 000000000..526aeddcf
--- /dev/null
+++ b/lib/pleroma/web/templates/feed/feed/_author.rss.eex
@@ -0,0 +1,17 @@
+<managingEditor>
+ <guid><%= @user.ap_id %></guid>
+ <activity:object>http://activitystrea.ms/schema/1.0/person</activity:object>
+ <uri><%= @user.ap_id %></uri>
+ <poco:preferredUsername><%= @user.nickname %></poco:preferredUsername>
+ <poco:displayName><%= @user.name %></poco:displayName>
+ <poco:note><%= escape(@user.bio) %></poco:note>
+ <description><%= escape(@user.bio) %></description>
+ <name><%= @user.nickname %></name>
+ <link rel="avatar"><%= User.avatar_url(@user) %></link>
+ <%= if User.banner_url(@user) do %>
+ <link rel="header"><%= User.banner_url(@user) %></link>
+ <% end %>
+ <%= if @user.local do %>
+ <ap_enabled>true</ap_enabled>
+ <% end %>
+</managingEditor>
diff --git a/lib/pleroma/web/templates/feed/feed/_tag_activity.atom.eex b/lib/pleroma/web/templates/feed/feed/_tag_activity.atom.eex
index da4fa6d6c..cf5874a91 100644
--- a/lib/pleroma/web/templates/feed/feed/_tag_activity.atom.eex
+++ b/lib/pleroma/web/templates/feed/feed/_tag_activity.atom.eex
@@ -1,12 +1,12 @@
<entry>
<activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
-
+
<%= render @view_module, "_tag_author.atom", assigns %>
-
+
<id><%= @data["id"] %></id>
- <title><%= activity_title(@object, Keyword.get(@feed_config, :post_title, %{})) %></title>
- <content type="html"><%= activity_content(@object) %></content>
+ <title><%= activity_title(@data, Keyword.get(@feed_config, :post_title, %{})) %></title>
+ <content type="html"><%= activity_content(@data) %></content>
<%= if @activity.local do %>
<link type="application/atom+xml" href='<%= @data["id"] %>' rel="self"/>
@@ -15,8 +15,8 @@
<link type="text/html" href='<%= @data["external_url"] %>' rel="alternate"/>
<% end %>
- <published><%= @data["published"] %></published>
- <updated><%= @data["published"] %></updated>
+ <published><%= @activity.data["published"] %></published>
+ <updated><%= @activity.data["published"] %></updated>
<ostatus:conversation ref="<%= activity_context(@activity) %>">
<%= activity_context(@activity) %>
@@ -26,7 +26,7 @@
<%= if @data["summary"] do %>
<summary><%= @data["summary"] %></summary>
<% end %>
-
+
<%= for id <- @activity.recipients do %>
<%= if id == Pleroma.Constants.as_public() do %>
<link rel="mentioned"
@@ -40,7 +40,7 @@
<% end %>
<% end %>
<% end %>
-
+
<%= for tag <- @data["tag"] || [] do %>
<category term="<%= tag %>"></category>
<% end %>
diff --git a/lib/pleroma/web/templates/feed/feed/_tag_activity.xml.eex b/lib/pleroma/web/templates/feed/feed/_tag_activity.xml.eex
index 295574df1..2334e24a2 100644
--- a/lib/pleroma/web/templates/feed/feed/_tag_activity.xml.eex
+++ b/lib/pleroma/web/templates/feed/feed/_tag_activity.xml.eex
@@ -1,15 +1,14 @@
<item>
- <title><%= activity_title(@object, Keyword.get(@feed_config, :post_title, %{})) %></title>
-
-
+ <title><%= activity_title(@data, Keyword.get(@feed_config, :post_title, %{})) %></title>
+
+
<guid isPermalink="true"><%= activity_context(@activity) %></guid>
<link><%= activity_context(@activity) %></link>
- <pubDate><%= pub_date(@data["published"]) %></pubDate>
-
- <description><%= activity_content(@object) %></description>
+ <pubDate><%= pub_date(@activity.data["published"]) %></pubDate>
+
+ <description><%= activity_content(@data) %></description>
<%= for attachment <- @data["attachment"] || [] do %>
<enclosure url="<%= attachment_href(attachment) %>" type="<%= attachment_type(attachment) %>"/>
<% end %>
-
-</item>
+</item>
diff --git a/lib/pleroma/web/templates/feed/feed/user.xml.eex b/lib/pleroma/web/templates/feed/feed/user.atom.eex
index d274c08ae..c6acd848f 100644
--- a/lib/pleroma/web/templates/feed/feed/user.xml.eex
+++ b/lib/pleroma/web/templates/feed/feed/user.atom.eex
@@ -12,13 +12,13 @@
<logo><%= logo(@user) %></logo>
<link rel="self" href="<%= '#{user_feed_url(@conn, :feed, @user.nickname)}.atom' %>" type="application/atom+xml"/>
- <%= render @view_module, "_author.xml", assigns %>
+ <%= render @view_module, "_author.atom", assigns %>
<%= if last_activity(@activities) do %>
<link rel="next" href="<%= '#{user_feed_url(@conn, :feed, @user.nickname)}.atom?max_id=#{last_activity(@activities).id}' %>" type="application/atom+xml"/>
<% end %>
<%= for activity <- @activities do %>
- <%= render @view_module, "_activity.xml", Map.merge(assigns, prepare_activity(activity)) %>
+ <%= render @view_module, "_activity.atom", Map.merge(assigns, prepare_activity(activity)) %>
<% end %>
</feed>
diff --git a/lib/pleroma/web/templates/feed/feed/user.rss.eex b/lib/pleroma/web/templates/feed/feed/user.rss.eex
new file mode 100644
index 000000000..d69120480
--- /dev/null
+++ b/lib/pleroma/web/templates/feed/feed/user.rss.eex
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<rss version="2.0">
+ <channel>
+ <guid><%= user_feed_url(@conn, :feed, @user.nickname) <> ".rss" %></guid>
+ <title><%= @user.nickname <> "'s timeline" %></title>
+ <updated><%= most_recent_update(@activities, @user) %></updated>
+ <image><%= logo(@user) %></image>
+ <link><%= '#{user_feed_url(@conn, :feed, @user.nickname)}.rss' %></link>
+
+ <%= render @view_module, "_author.rss", assigns %>
+
+ <%= if last_activity(@activities) do %>
+ <link rel="next"><%= '#{user_feed_url(@conn, :feed, @user.nickname)}.rss?max_id=#{last_activity(@activities).id}' %></link>
+ <% end %>
+
+ <%= for activity <- @activities do %>
+ <%= render @view_module, "_activity.rss", Map.merge(assigns, prepare_activity(activity)) %>
+ <% end %>
+ </channel>
+</rss>
diff --git a/lib/pleroma/web/templates/layout/static_fe.html.eex b/lib/pleroma/web/templates/layout/static_fe.html.eex
index 819632cec..dc0ee2a5c 100644
--- a/lib/pleroma/web/templates/layout/static_fe.html.eex
+++ b/lib/pleroma/web/templates/layout/static_fe.html.eex
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui" />
<title><%= Pleroma.Config.get([:instance, :name]) %></title>
<%= Phoenix.HTML.raw(assigns[:meta] || "") %>
- <link rel="stylesheet" href="/static/static-fe.css">
+ <link rel="stylesheet" href="/static-fe/static-fe.css">
</head>
<body>
<div class="container">
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/static_fe/static_fe/_attachment.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/_attachment.html.eex
index 7e04e9550..4853e7f4b 100644
--- a/lib/pleroma/web/templates/static_fe/static_fe/_attachment.html.eex
+++ b/lib/pleroma/web/templates/static_fe/static_fe/_attachment.html.eex
@@ -1,8 +1,8 @@
<%= case @mediaType do %>
<% "audio" -> %>
-<audio src="<%= @url %>" controls="controls"></audio>
+<audio class="u-audio" src="<%= @url %>" controls="controls"></audio>
<% "video" -> %>
-<video src="<%= @url %>" controls="controls"></video>
+<video class="u-video" src="<%= @url %>" controls="controls"></video>
<% _ -> %>
-<img src="<%= @url %>" alt="<%= @name %>" title="<%= @name %>">
+<img class="u-photo" src="<%= @url %>" alt="<%= @name %>" title="<%= @name %>">
<% end %>
diff --git a/lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex
index df5e5eedd..df0244795 100644
--- a/lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex
+++ b/lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex
@@ -1,12 +1,16 @@
-<div class="activity" <%= if @selected do %> id="selected" <% end %>>
+<div class="activity h-entry" <%= if @selected do %> id="selected" <% end %>>
<p class="pull-right">
- <%= link format_date(@published), to: @link, class: "activity-link" %>
+ <a class="activity-link u-url u-uid" href="<%= @link %>">
+ <time class="dt-published" datetime="<%= @published %>">
+ <%= format_date(@published) %>
+ </time>
+ </a>
</p>
<%= render("_user_card.html", %{user: @user}) %>
<div class="activity-content">
<%= if @title != "" do %>
<details <%= if open_content?() do %>open<% end %>>
- <summary><%= raw @title %></summary>
+ <summary class="p-name"><%= raw @title %></summary>
<div class="e-content"><%= raw @content %></div>
</details>
<% else %>
diff --git a/lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex
index c7789f9ac..977b894d3 100644
--- a/lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex
+++ b/lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex
@@ -1,10 +1,10 @@
<div class="p-author h-card">
- <a class="u-url" rel="author noopener" href="<%= User.profile_url(@user) %>">
+ <a class="u-url" rel="author noopener" href="<%= (@user.uri || @user.ap_id) %>">
<div class="avatar">
- <img src="<%= User.avatar_url(@user) |> MediaProxy.url %>" width="48" height="48" alt="">
+ <img class="u-photo" src="<%= User.avatar_url(@user) |> MediaProxy.url %>" width="48" height="48" alt="">
</div>
<span class="display-name">
- <bdi><%= raw (@user.name |> Formatter.emojify(emoji_for_user(@user))) %></bdi>
+ <bdi class="p-name"><%= raw Formatter.emojify(@user.name, @user.emoji) %></bdi>
<span class="nickname"><%= @user.nickname %></span>
</span>
</a>
diff --git a/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex
index 94063c92d..3191bf450 100644
--- a/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex
+++ b/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex
@@ -7,8 +7,8 @@
<input type="hidden" name="profile" value="">
<button type="submit" class="collapse">Remote follow</button>
</form>
- <%= raw Formatter.emojify(@user.name, emoji_for_user(@user)) %> |
- <%= link "@#{@user.nickname}@#{Endpoint.host()}", to: User.profile_url(@user) %>
+ <%= raw Formatter.emojify(@user.name, @user.emoji) %> |
+ <%= link "@#{@user.nickname}@#{Endpoint.host()}", to: (@user.uri || @user.ap_id) %>
</h3>
<p><%= raw @user.bio %></p>
</header>
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/translation_helpers.ex b/lib/pleroma/web/translation_helpers.ex
index a104ea6b8..7f78ce1b9 100644
--- a/lib/pleroma/web/translation_helpers.ex
+++ b/lib/pleroma/web/translation_helpers.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TranslationHelpers do
diff --git a/lib/pleroma/web/twitter_api/controllers/password_controller.ex b/lib/pleroma/web/twitter_api/controllers/password_controller.ex
index 1941e6143..800ab8954 100644
--- a/lib/pleroma/web/twitter_api/controllers/password_controller.ex
+++ b/lib/pleroma/web/twitter_api/controllers/password_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.PasswordController do
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 fbf31c7eb..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,14 +8,18 @@ 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"]
+ plug(Pleroma.Web.FederatingPlug)
+
# Note: follower can submit the form (with password auth) not being signed in (having no token)
plug(
OAuthScopesPlug,
@@ -66,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
@@ -76,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
@@ -92,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/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
index f08b9d28c..fd2aee175 100644
--- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex
+++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.UtilController do
@@ -17,19 +17,14 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.WebFinger
+ plug(Pleroma.Web.FederatingPlug when action == :remote_subscribe)
+
plug(
OAuthScopesPlug,
%{scopes: ["follow", "write:follows"]}
when action == :follow_import
)
- # Note: follower can submit the form (with password auth) not being signed in (having no token)
- plug(
- OAuthScopesPlug,
- %{fallback: :proceed_unauthenticated, scopes: ["follow", "write:follows"]}
- when action == :do_remote_follow
- )
-
plug(OAuthScopesPlug, %{scopes: ["follow", "write:blocks"]} when action == :blocks_import)
plug(
@@ -197,15 +192,16 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
def follow_import(%{assigns: %{user: follower}} = conn, %{"list" => list}) do
- with lines <- String.split(list, "\n"),
- followed_identifiers <-
- Enum.map(lines, fn line ->
- String.split(line, ",") |> List.first()
- end)
- |> List.delete("Account address") do
- User.follow_import(follower, followed_identifiers)
- json(conn, "job started")
- end
+ followed_identifiers =
+ list
+ |> String.split("\n")
+ |> Enum.map(&(&1 |> String.split(",") |> List.first()))
+ |> List.delete("Account address")
+ |> Enum.map(&(&1 |> String.trim() |> String.trim_leading("@")))
+ |> Enum.reject(&(&1 == ""))
+
+ User.follow_import(follower, followed_identifiers)
+ json(conn, "job started")
end
def blocks_import(conn, %{"list" => %Plug.Upload{} = listfile}) do
@@ -213,10 +209,9 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
def blocks_import(%{assigns: %{user: blocker}} = conn, %{"list" => list}) do
- with blocked_identifiers <- String.split(list) do
- User.blocks_import(blocker, blocked_identifiers)
- json(conn, "job started")
- end
+ blocked_identifiers = list |> String.split() |> Enum.map(&String.trim_leading(&1, "@"))
+ User.blocks_import(blocker, blocked_identifiers)
+ json(conn, "job started")
end
def change_password(%{assigns: %{user: user}} = conn, params) do
diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex
index bfd838902..5cfb385ac 100644
--- a/lib/pleroma/web/twitter_api/twitter_api.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api.ex
@@ -1,83 +1,41 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
+ import Pleroma.Web.Gettext
+
alias Pleroma.Emails.Mailer
alias Pleroma.Emails.UserEmail
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.UserInviteToken
- require Pleroma.Constants
-
def register_user(params, opts \\ []) do
- token = params["token"]
-
- params = %{
- nickname: params["nickname"],
- name: params["fullname"],
- bio: User.parse_bio(params["bio"]),
- email: params["email"],
- password: params["password"],
- password_confirmation: params["confirm"],
- captcha_solution: params["captcha_solution"],
- captcha_token: params["captcha_token"],
- captcha_answer_data: params["captcha_answer_data"]
- }
-
- captcha_enabled = Pleroma.Config.get([Pleroma.Captcha, :enabled])
- # true if captcha is disabled or enabled and valid, false otherwise
- captcha_ok =
- if not captcha_enabled do
- :ok
- else
- Pleroma.Captcha.validate(
- params[:captcha_token],
- params[:captcha_solution],
- params[:captcha_answer_data]
- )
- end
-
- # Captcha invalid
- if captcha_ok != :ok do
- {:error, error} = captcha_ok
- # I have no idea how this error handling works
- {:error, %{error: Jason.encode!(%{captcha: [error]})}}
+ params =
+ params
+ |> Map.take([:email, :token, :password])
+ |> Map.put(:bio, params |> Map.get(:bio, "") |> User.parse_bio())
+ |> Map.put(:nickname, params[:username])
+ |> Map.put(:name, Map.get(params, :fullname, params[:username]))
+ |> Map.put(:password_confirmation, params[:password])
+
+ if Pleroma.Config.get([:instance, :registrations_open]) do
+ create_user(params, opts)
else
- registration_process(
- params,
- %{
- registrations_open: Pleroma.Config.get([:instance, :registrations_open]),
- token: token
- },
- opts
- )
+ create_user_with_invite(params, opts)
end
end
- defp registration_process(params, %{registrations_open: true}, opts) do
- create_user(params, opts)
- end
-
- defp registration_process(params, %{token: token}, opts) do
- invite =
- unless is_nil(token) do
- Repo.get_by(UserInviteToken, %{token: token})
- end
-
- valid_invite? = invite && UserInviteToken.valid_invite?(invite)
-
- case invite do
- nil ->
- {:error, "Invalid token"}
-
- invite when valid_invite? ->
- UserInviteToken.update_usage!(invite)
- create_user(params, opts)
-
- _ ->
- {:error, "Expired token"}
+ defp create_user_with_invite(params, opts) do
+ with %{token: token} when is_binary(token) <- params,
+ %UserInviteToken{} = invite <- Repo.get_by(UserInviteToken, %{token: token}),
+ true <- UserInviteToken.valid_invite?(invite) do
+ UserInviteToken.update_usage!(invite)
+ create_user(params, opts)
+ else
+ nil -> {:error, "Invalid token"}
+ _ -> {:error, "Expired token"}
end
end
@@ -90,16 +48,18 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
{:error, changeset} ->
errors =
- Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)
+ changeset
+ |> Ecto.Changeset.traverse_errors(fn {msg, _opts} -> msg end)
|> Jason.encode!()
- {:error, %{error: errors}}
+ {:error, errors}
end
end
def password_reset(nickname_or_email) do
with true <- is_binary(nickname_or_email),
- %User{local: true} = user <- User.get_by_nickname_or_email(nickname_or_email),
+ %User{local: true, email: email} = user when is_binary(email) <-
+ User.get_by_nickname_or_email(nickname_or_email),
{:ok, token_record} <- Pleroma.PasswordResetToken.create_token(user) do
user
|> UserEmail.password_reset_email(token_record.token)
@@ -110,6 +70,9 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
false ->
{:error, "bad user identifier"}
+ %User{local: true, email: nil} ->
+ {:ok, :noop}
+
%User{local: false} ->
{:error, "remote user"}
@@ -117,4 +80,58 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
{:error, "unknown user"}
end
end
+
+ def validate_captcha(app, params) do
+ if app.trusted || not Pleroma.Captcha.enabled?() do
+ :ok
+ else
+ do_validate_captcha(params)
+ end
+ end
+
+ defp do_validate_captcha(params) do
+ with :ok <- validate_captcha_presence(params),
+ :ok <-
+ Pleroma.Captcha.validate(
+ params[:captcha_token],
+ params[:captcha_solution],
+ params[:captcha_answer_data]
+ ) do
+ :ok
+ else
+ {:error, :captcha_error} ->
+ captcha_error(dgettext("errors", "CAPTCHA Error"))
+
+ {:error, :invalid} ->
+ captcha_error(dgettext("errors", "Invalid CAPTCHA"))
+
+ {:error, :kocaptcha_service_unavailable} ->
+ captcha_error(dgettext("errors", "Kocaptcha service unavailable"))
+
+ {:error, :expired} ->
+ captcha_error(dgettext("errors", "CAPTCHA expired"))
+
+ {:error, :already_used} ->
+ captcha_error(dgettext("errors", "CAPTCHA already used"))
+
+ {:error, :invalid_answer_data} ->
+ captcha_error(dgettext("errors", "Invalid answer data"))
+
+ {:error, error} ->
+ captcha_error(error)
+ end
+ end
+
+ defp validate_captcha_presence(params) do
+ [:captcha_solution, :captcha_token, :captcha_answer_data]
+ |> Enum.find_value(:ok, fn key ->
+ unless is_binary(params[key]) do
+ error = dgettext("errors", "Invalid CAPTCHA (Missing parameter: %{name})", name: key)
+ {:error, error}
+ end
+ end)
+ end
+
+ # For some reason FE expects error message to be a serialized JSON
+ defp captcha_error(error), do: {:error, Jason.encode!(%{captcha: [error]})}
end
diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
index 39f10c49f..c2de26b0b 100644
--- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
@@ -1,11 +1,12 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.Controller do
use Pleroma.Web, :controller
alias Pleroma.Notification
+ alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
alias Pleroma.Web.OAuth.Token
@@ -13,9 +14,17 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
require Logger
- plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :notifications_read)
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["write:notifications"]} when action == :mark_notifications_as_read
+ )
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ plug(
+ :skip_plug,
+ [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :confirm_email
+ )
+
+ plug(:skip_plug, OAuthScopesPlug when action in [:oauth_tokens, :revoke_token])
action_fallback(:errors)
@@ -44,13 +53,13 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
json_reply(conn, 201, "")
end
- def errors(conn, {:param_cast, _}) do
+ defp errors(conn, {:param_cast, _}) do
conn
|> put_status(400)
|> json("Invalid parameters")
end
- def errors(conn, _) do
+ defp errors(conn, _) do
conn
|> put_status(500)
|> json("Something went wrong")
@@ -62,7 +71,10 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
|> send_resp(status, json)
end
- def notifications_read(%{assigns: %{user: user}} = conn, %{"latest_id" => latest_id} = params) do
+ def mark_notifications_as_read(
+ %{assigns: %{user: user}} = conn,
+ %{"latest_id" => latest_id} = params
+ ) do
Notification.set_read_up_to(user, latest_id)
notifications = Notification.for_user(user, params)
@@ -73,7 +85,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
|> render("index.json", %{notifications: notifications, for: user})
end
- def notifications_read(%{assigns: %{user: _user}} = conn, _) do
+ def mark_notifications_as_read(%{assigns: %{user: _user}} = conn, _) do
bad_request_reply(conn, "You need to specify latest_id")
end
diff --git a/lib/pleroma/web/twitter_api/views/password_view.ex b/lib/pleroma/web/twitter_api/views/password_view.ex
index b166b925d..41462e4af 100644
--- a/lib/pleroma/web/twitter_api/views/password_view.ex
+++ b/lib/pleroma/web/twitter_api/views/password_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.PasswordView do
diff --git a/lib/pleroma/web/twitter_api/views/remote_follow_view.ex b/lib/pleroma/web/twitter_api/views/remote_follow_view.ex
index d469c4726..c05c7821c 100644
--- a/lib/pleroma/web/twitter_api/views/remote_follow_view.ex
+++ b/lib/pleroma/web/twitter_api/views/remote_follow_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.RemoteFollowView do
diff --git a/lib/pleroma/web/twitter_api/views/token_view.ex b/lib/pleroma/web/twitter_api/views/token_view.ex
index 3ff314913..c36303625 100644
--- a/lib/pleroma/web/twitter_api/views/token_view.ex
+++ b/lib/pleroma/web/twitter_api/views/token_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.TokenView do
diff --git a/lib/pleroma/web/twitter_api/views/util_view.ex b/lib/pleroma/web/twitter_api/views/util_view.ex
index f4050650e..52054e020 100644
--- a/lib/pleroma/web/twitter_api/views/util_view.ex
+++ b/lib/pleroma/web/twitter_api/views/util_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.UtilView do
diff --git a/lib/pleroma/web/uploader_controller.ex b/lib/pleroma/web/uploader_controller.ex
index 0cc172698..6533f1c0e 100644
--- a/lib/pleroma/web/uploader_controller.ex
+++ b/lib/pleroma/web/uploader_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.UploaderController do
diff --git a/lib/pleroma/web/views/error_helpers.ex b/lib/pleroma/web/views/error_helpers.ex
index bc08e60e4..df657a343 100644
--- a/lib/pleroma/web/views/error_helpers.ex
+++ b/lib/pleroma/web/views/error_helpers.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ErrorHelpers do
diff --git a/lib/pleroma/web/views/error_view.ex b/lib/pleroma/web/views/error_view.ex
index 5cb8669fe..e68d55e08 100644
--- a/lib/pleroma/web/views/error_view.ex
+++ b/lib/pleroma/web/views/error_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ErrorView do
diff --git a/lib/pleroma/web/views/layout_view.ex b/lib/pleroma/web/views/layout_view.ex
index e5183701d..3e49c6549 100644
--- a/lib/pleroma/web/views/layout_view.ex
+++ b/lib/pleroma/web/views/layout_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.LayoutView do
diff --git a/lib/pleroma/web/views/masto_fe_view.ex b/lib/pleroma/web/views/masto_fe_view.ex
index c39b7f095..c3096006e 100644
--- a/lib/pleroma/web/views/masto_fe_view.ex
+++ b/lib/pleroma/web/views/masto_fe_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastoFEView do
diff --git a/lib/pleroma/web/views/streamer_view.ex b/lib/pleroma/web/views/streamer_view.ex
index a9f14d09a..237b29ded 100644
--- a/lib/pleroma/web/views/streamer_view.ex
+++ b/lib/pleroma/web/views/streamer_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.StreamerView do
@@ -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 687346554..4f9281851 100644
--- a/lib/pleroma/web/web.ex
+++ b/lib/pleroma/web/web.ex
@@ -1,7 +1,12 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
+defmodule Pleroma.Web.Plug do
+ # Substitute for `call/2` which is defined with `use Pleroma.Web, :plug`
+ @callback perform(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t()
+end
+
defmodule Pleroma.Web do
@moduledoc """
A module that keeps using definitions for controllers,
@@ -20,11 +25,19 @@ defmodule Pleroma.Web do
below.
"""
+ alias Pleroma.Plugs.EnsureAuthenticatedPlug
+ alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
+ alias Pleroma.Plugs.ExpectAuthenticatedCheckPlug
+ alias Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug
+ alias Pleroma.Plugs.OAuthScopesPlug
+ alias Pleroma.Plugs.PlugHelper
+
def controller do
quote do
use Phoenix.Controller, namespace: Pleroma.Web
import Plug.Conn
+
import Pleroma.Web.Gettext
import Pleroma.Web.Router.Helpers
import Pleroma.Web.TranslationHelpers
@@ -34,6 +47,79 @@ defmodule Pleroma.Web do
defp set_put_layout(conn, _) do
put_layout(conn, Pleroma.Config.get(:app_layout, "app.html"))
end
+
+ # Marks plugs intentionally skipped and blocks their execution if present in plugs chain
+ defp skip_plug(conn, plug_modules) do
+ plug_modules
+ |> List.wrap()
+ |> Enum.reduce(
+ conn,
+ fn plug_module, conn ->
+ try do
+ plug_module.skip_plug(conn)
+ rescue
+ UndefinedFunctionError ->
+ raise "`#{plug_module}` is not skippable. Append `use Pleroma.Web, :plug` to its code."
+ end
+ end
+ )
+ end
+
+ # Executed just before actual controller action, invokes before-action hooks (callbacks)
+ defp action(conn, params) do
+ with %{halted: false} = conn <- maybe_drop_authentication_if_oauth_check_ignored(conn),
+ %{halted: false} = conn <- maybe_perform_public_or_authenticated_check(conn),
+ %{halted: false} = conn <- maybe_perform_authenticated_check(conn),
+ %{halted: false} = conn <- maybe_halt_on_missing_oauth_scopes_check(conn) do
+ super(conn, params)
+ end
+ end
+
+ # For non-authenticated API actions, drops auth info if OAuth scopes check was ignored
+ # (neither performed nor explicitly skipped)
+ defp maybe_drop_authentication_if_oauth_check_ignored(conn) do
+ if PlugHelper.plug_called?(conn, ExpectPublicOrAuthenticatedCheckPlug) and
+ not PlugHelper.plug_called_or_skipped?(conn, OAuthScopesPlug) do
+ OAuthScopesPlug.drop_auth_info(conn)
+ else
+ conn
+ end
+ end
+
+ # Ensures instance is public -or- user is authenticated if such check was scheduled
+ defp maybe_perform_public_or_authenticated_check(conn) do
+ if PlugHelper.plug_called?(conn, ExpectPublicOrAuthenticatedCheckPlug) do
+ EnsurePublicOrAuthenticatedPlug.call(conn, %{})
+ else
+ conn
+ end
+ end
+
+ # Ensures user is authenticated if such check was scheduled
+ # Note: runs prior to action even if it was already executed earlier in plug chain
+ # (since OAuthScopesPlug has option of proceeding unauthenticated)
+ defp maybe_perform_authenticated_check(conn) do
+ if PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) do
+ EnsureAuthenticatedPlug.call(conn, %{})
+ else
+ conn
+ end
+ end
+
+ # Halts if authenticated API action neither performs nor explicitly skips OAuth scopes check
+ defp maybe_halt_on_missing_oauth_scopes_check(conn) do
+ if PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) and
+ not PlugHelper.plug_called_or_skipped?(conn, OAuthScopesPlug) do
+ conn
+ |> render_error(
+ :forbidden,
+ "Security violation: OAuth scopes check was neither handled nor explicitly skipped."
+ )
+ |> halt()
+ else
+ conn
+ end
+ end
end
end
@@ -96,6 +182,50 @@ defmodule Pleroma.Web do
end
end
+ def plug do
+ quote do
+ @behaviour Pleroma.Web.Plug
+ @behaviour Plug
+
+ @doc """
+ Marks a plug intentionally skipped and blocks its execution if it's present in plugs chain.
+ """
+ def skip_plug(conn) do
+ PlugHelper.append_to_private_list(
+ conn,
+ PlugHelper.skipped_plugs_list_id(),
+ __MODULE__
+ )
+ end
+
+ @impl Plug
+ @doc """
+ 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__) ||
+ (options[:if_func] && !options[:if_func].(conn)) ||
+ (options[:unless_func] && options[:unless_func].(conn)) do
+ conn
+ else
+ conn =
+ PlugHelper.append_to_private_list(
+ conn,
+ PlugHelper.called_plugs_list_id(),
+ __MODULE__
+ )
+
+ apply(__MODULE__, :perform, [conn, options])
+ end
+ end
+ end
+ end
+
@doc """
When used, dispatch to the appropriate controller/view/etc.
"""
diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex
index b4cc80179..71ccf251a 100644
--- a/lib/pleroma/web/web_finger/web_finger.ex
+++ b/lib/pleroma/web/web_finger/web_finger.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.WebFinger do
@@ -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
@@ -173,7 +140,8 @@ defmodule Pleroma.Web.WebFinger do
get_template_from_xml(body)
else
_ ->
- with {:ok, %{body: body}} <- HTTP.get("https://#{domain}/.well-known/host-meta", []) do
+ with {:ok, %{body: body, status: status}} when status in 200..299 <-
+ HTTP.get("https://#{domain}/.well-known/host-meta", []) do
get_template_from_xml(body)
else
e -> {:error, "Can't find LRDD template: #{inspect(e)}"}
@@ -193,19 +161,21 @@ 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 <-
HTTP.get(
address,
- Accept: "application/xrd+xml,application/jrd+json"
+ [{"accept", "application/xrd+xml,application/jrd+json"}]
),
{:ok, %{status: status, body: body}} when status in 200..299 <- response do
doc = XML.parse_document(body)
diff --git a/lib/pleroma/web/web_finger/web_finger_controller.ex b/lib/pleroma/web/web_finger/web_finger_controller.ex
index 896eb15f9..7077b20d2 100644
--- a/lib/pleroma/web/web_finger/web_finger_controller.ex
+++ b/lib/pleroma/web/web_finger/web_finger_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.WebFinger.WebFingerController do
diff --git a/lib/pleroma/web/xml/xml.ex b/lib/pleroma/web/xml/xml.ex
index df50aac9c..c69a86a1e 100644
--- a/lib/pleroma/web/xml/xml.ex
+++ b/lib/pleroma/web/xml/xml.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.XML do