summaryrefslogtreecommitdiff
path: root/lib/pleroma
diff options
context:
space:
mode:
Diffstat (limited to 'lib/pleroma')
-rw-r--r--lib/pleroma/activity.ex46
-rw-r--r--lib/pleroma/activity/queries.ex7
-rw-r--r--lib/pleroma/activity_expiration.ex17
-rw-r--r--lib/pleroma/application.ex143
-rw-r--r--lib/pleroma/application_requirements.ex143
-rw-r--r--lib/pleroma/bbs/authenticator.ex4
-rw-r--r--lib/pleroma/bbs/handler.ex12
-rw-r--r--lib/pleroma/captcha/captcha.ex135
-rw-r--r--lib/pleroma/captcha/kocaptcha.ex8
-rw-r--r--lib/pleroma/captcha/native.ex8
-rw-r--r--lib/pleroma/chat.ex72
-rw-r--r--lib/pleroma/chat/message_reference.ex117
-rw-r--r--lib/pleroma/config.ex51
-rw-r--r--lib/pleroma/config/config_db.ex284
-rw-r--r--lib/pleroma/config/deprecation_warnings.ex75
-rw-r--r--lib/pleroma/config/helpers.ex17
-rw-r--r--lib/pleroma/config/loader.ex8
-rw-r--r--lib/pleroma/config/transfer_task.ex168
-rw-r--r--lib/pleroma/constants.ex5
-rw-r--r--lib/pleroma/conversation.ex2
-rw-r--r--lib/pleroma/conversation/participation.ex26
-rw-r--r--lib/pleroma/counter_cache.ex66
-rw-r--r--lib/pleroma/docs/generator.ex31
-rw-r--r--lib/pleroma/docs/json.ex22
-rw-r--r--lib/pleroma/docs/markdown.ex5
-rw-r--r--lib/pleroma/ecto_type/activity_pub/object_validators/date_time.ex38
-rw-r--r--lib/pleroma/ecto_type/activity_pub/object_validators/object_id.ex27
-rw-r--r--lib/pleroma/ecto_type/activity_pub/object_validators/recipients.ex40
-rw-r--r--lib/pleroma/ecto_type/activity_pub/object_validators/safe_text.ex25
-rw-r--r--lib/pleroma/ecto_type/activity_pub/object_validators/uri.ex24
-rw-r--r--lib/pleroma/ecto_type/config/atom.ex26
-rw-r--r--lib/pleroma/ecto_type/config/binary_value.ex27
-rw-r--r--lib/pleroma/emails/admin_email.ex19
-rw-r--r--lib/pleroma/emails/new_users_digest_email.ex6
-rw-r--r--lib/pleroma/emails/user_email.ex48
-rw-r--r--lib/pleroma/emoji/formatter.ex14
-rw-r--r--lib/pleroma/emoji/loader.ex2
-rw-r--r--lib/pleroma/emoji/pack.ex572
-rw-r--r--lib/pleroma/filter.ex51
-rw-r--r--lib/pleroma/following_relationship.ex86
-rw-r--r--lib/pleroma/formatter.ex57
-rw-r--r--lib/pleroma/gopher/server.ex19
-rw-r--r--lib/pleroma/gun/api.ex46
-rw-r--r--lib/pleroma/gun/conn.ex135
-rw-r--r--lib/pleroma/gun/connection_pool.ex82
-rw-r--r--lib/pleroma/gun/connection_pool/reclaimer.ex85
-rw-r--r--lib/pleroma/gun/connection_pool/worker.ex133
-rw-r--r--lib/pleroma/gun/connection_pool/worker_supervisor.ex45
-rw-r--r--lib/pleroma/gun/gun.ex31
-rw-r--r--lib/pleroma/helpers/uri_helper.ex9
-rw-r--r--lib/pleroma/html.ex2
-rw-r--r--lib/pleroma/http/adapter_helper.ex140
-rw-r--r--lib/pleroma/http/adapter_helper/default.ex14
-rw-r--r--lib/pleroma/http/adapter_helper/gun.ex78
-rw-r--r--lib/pleroma/http/adapter_helper/hackney.ex29
-rw-r--r--lib/pleroma/http/connection.ex43
-rw-r--r--lib/pleroma/http/ex_aws.ex22
-rw-r--r--lib/pleroma/http/http.ex135
-rw-r--r--lib/pleroma/http/request.ex23
-rw-r--r--lib/pleroma/http/request_builder.ex125
-rw-r--r--lib/pleroma/http/tzdata.ex25
-rw-r--r--lib/pleroma/instances/instance.ex48
-rw-r--r--lib/pleroma/job_queue_monitor.ex14
-rw-r--r--lib/pleroma/maintenance.ex37
-rw-r--r--lib/pleroma/maps.ex15
-rw-r--r--lib/pleroma/marker.ex45
-rw-r--r--lib/pleroma/mfa.ex155
-rw-r--r--lib/pleroma/mfa/backup_codes.ex31
-rw-r--r--lib/pleroma/mfa/changeset.ex64
-rw-r--r--lib/pleroma/mfa/settings.ex24
-rw-r--r--lib/pleroma/mfa/token.ex106
-rw-r--r--lib/pleroma/mfa/totp.ex86
-rw-r--r--lib/pleroma/migration_helper/notification_backfill.ex93
-rw-r--r--lib/pleroma/moderation_log.ex11
-rw-r--r--lib/pleroma/notification.ex348
-rw-r--r--lib/pleroma/object.ex69
-rw-r--r--lib/pleroma/object/containment.ex14
-rw-r--r--lib/pleroma/object/fetcher.ex57
-rw-r--r--lib/pleroma/otp_version.ex28
-rw-r--r--lib/pleroma/pagination.ex20
-rw-r--r--lib/pleroma/plugs/admin_secret_authentication_plug.ex27
-rw-r--r--lib/pleroma/plugs/auth_expected_plug.ex17
-rw-r--r--lib/pleroma/plugs/authentication_plug.ex33
-rw-r--r--lib/pleroma/plugs/ensure_authenticated_plug.ex24
-rw-r--r--lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex6
-rw-r--r--lib/pleroma/plugs/expect_authenticated_check_plug.ex20
-rw-r--r--lib/pleroma/plugs/expect_public_or_authenticated_check_plug.ex21
-rw-r--r--lib/pleroma/plugs/federating_plug.ex21
-rw-r--r--lib/pleroma/plugs/frontend_static.ex55
-rw-r--r--lib/pleroma/plugs/http_security_plug.ex77
-rw-r--r--lib/pleroma/plugs/instance_static.ex15
-rw-r--r--lib/pleroma/plugs/mapped_signature_to_identity_plug.ex6
-rw-r--r--lib/pleroma/plugs/oauth_scopes_plug.ex27
-rw-r--r--lib/pleroma/plugs/rate_limiter/rate_limiter.ex17
-rw-r--r--lib/pleroma/plugs/static_fe_plug.ex9
-rw-r--r--lib/pleroma/plugs/uploaded_media.ex16
-rw-r--r--lib/pleroma/plugs/user_is_admin_plug.ex25
-rw-r--r--lib/pleroma/repo.ex59
-rw-r--r--lib/pleroma/reverse_proxy/client.ex26
-rw-r--r--lib/pleroma/reverse_proxy/client/hackney.ex24
-rw-r--r--lib/pleroma/reverse_proxy/client/tesla.ex86
-rw-r--r--lib/pleroma/reverse_proxy/reverse_proxy.ex79
-rw-r--r--lib/pleroma/scheduled_activity.ex2
-rw-r--r--lib/pleroma/signature.ex13
-rw-r--r--lib/pleroma/stats.ex23
-rw-r--r--lib/pleroma/telemetry/logger.ex76
-rw-r--r--lib/pleroma/tesla/middleware/follow_redirects.ex110
-rw-r--r--lib/pleroma/tests/auth_test_controller.ex93
-rw-r--r--lib/pleroma/tests/oauth_test_controller.ex31
-rw-r--r--lib/pleroma/thread_mute.ex8
-rw-r--r--lib/pleroma/upload.ex21
-rw-r--r--lib/pleroma/upload/filter/exiftool.ex26
-rw-r--r--lib/pleroma/upload/filter/mogrifun.ex15
-rw-r--r--lib/pleroma/upload/filter/mogrify.ex12
-rw-r--r--lib/pleroma/user.ex820
-rw-r--r--lib/pleroma/user/notification_setting.ex14
-rw-r--r--lib/pleroma/user/query.ex21
-rw-r--r--lib/pleroma/user/search.ex23
-rw-r--r--lib/pleroma/user/welcome_chat_message.ex45
-rw-r--r--lib/pleroma/user/welcome_email.ex62
-rw-r--r--lib/pleroma/user/welcome_message.ex43
-rw-r--r--lib/pleroma/user_relationship.ex51
-rw-r--r--lib/pleroma/utils.ex15
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub.ex1026
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub_controller.ex117
-rw-r--r--lib/pleroma/web/activity_pub/builder.ex269
-rw-r--r--lib/pleroma/web/activity_pub/mrf.ex11
-rw-r--r--lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex43
-rw-r--r--lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex4
-rw-r--r--lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex12
-rw-r--r--lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex3
-rw-r--r--lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex11
-rw-r--r--lib/pleroma/web/activity_pub/mrf/keyword_policy.ex7
-rw-r--r--lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex14
-rw-r--r--lib/pleroma/web/activity_pub/mrf/mention_policy.ex5
-rw-r--r--lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex2
-rw-r--r--lib/pleroma/web/activity_pub/mrf/object_age_policy.ex23
-rw-r--r--lib/pleroma/web/activity_pub/mrf/reject_non_public.ex4
-rw-r--r--lib/pleroma/web/activity_pub/mrf/simple_policy.ex88
-rw-r--r--lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex97
-rw-r--r--lib/pleroma/web/activity_pub/mrf/tag_policy.ex7
-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.ex18
-rw-r--r--lib/pleroma/web/activity_pub/object_validator.ex275
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/accept_reject_validator.ex56
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/announce_validator.ex101
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/answer_validator.ex62
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex80
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/audio_validator.ex106
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/block_validator.ex42
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex129
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/common_fixes.ex22
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/common_validations.ex141
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex91
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex144
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/create_note_validator.ex29
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/delete_validator.ex75
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex81
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/event_validator.ex96
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/follow_validator.ex44
-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.ex66
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/question_options_validator.ex37
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/question_validator.ex111
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/undo_validator.ex62
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/update_validator.ex59
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex24
-rw-r--r--lib/pleroma/web/activity_pub/pipeline.ex71
-rw-r--r--lib/pleroma/web/activity_pub/publisher.ex33
-rw-r--r--lib/pleroma/web/activity_pub/relay.ex67
-rw-r--r--lib/pleroma/web/activity_pub/side_effects.ex441
-rw-r--r--lib/pleroma/web/activity_pub/transmogrifier.ex545
-rw-r--r--lib/pleroma/web/activity_pub/utils.ex193
-rw-r--r--lib/pleroma/web/activity_pub/views/user_view.ex19
-rw-r--r--lib/pleroma/web/activity_pub/visibility.ex15
-rw-r--r--lib/pleroma/web/admin_api/controllers/admin_api_controller.ex (renamed from lib/pleroma/web/admin_api/admin_api_controller.ex)611
-rw-r--r--lib/pleroma/web/admin_api/controllers/config_controller.ex150
-rw-r--r--lib/pleroma/web/admin_api/controllers/fallback_controller.ex37
-rw-r--r--lib/pleroma/web/admin_api/controllers/invite_controller.ex78
-rw-r--r--lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex74
-rw-r--r--lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex77
-rw-r--r--lib/pleroma/web/admin_api/controllers/relay_controller.ex67
-rw-r--r--lib/pleroma/web/admin_api/controllers/report_controller.ex107
-rw-r--r--lib/pleroma/web/admin_api/controllers/status_controller.ex77
-rw-r--r--lib/pleroma/web/admin_api/search.ex2
-rw-r--r--lib/pleroma/web/admin_api/views/account_view.ex33
-rw-r--r--lib/pleroma/web/admin_api/views/config_view.ex19
-rw-r--r--lib/pleroma/web/admin_api/views/invite_view.ex25
-rw-r--r--lib/pleroma/web/admin_api/views/media_proxy_cache_view.ex15
-rw-r--r--lib/pleroma/web/admin_api/views/report_view.ex46
-rw-r--r--lib/pleroma/web/admin_api/views/status_view.ex29
-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.ex81
-rw-r--r--lib/pleroma/web/api_spec/operations/account_operation.ex766
-rw-r--r--lib/pleroma/web/api_spec/operations/admin/config_operation.ex145
-rw-r--r--lib/pleroma/web/api_spec/operations/admin/invite_operation.ex152
-rw-r--r--lib/pleroma/web/api_spec/operations/admin/media_proxy_cache_operation.ex121
-rw-r--r--lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex217
-rw-r--r--lib/pleroma/web/api_spec/operations/admin/relay_operation.ex94
-rw-r--r--lib/pleroma/web/api_spec/operations/admin/report_operation.ex240
-rw-r--r--lib/pleroma/web/api_spec/operations/admin/status_operation.ex166
-rw-r--r--lib/pleroma/web/api_spec/operations/app_operation.ex148
-rw-r--r--lib/pleroma/web/api_spec/operations/chat_operation.ex355
-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.ex86
-rw-r--r--lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex104
-rw-r--r--lib/pleroma/web/api_spec/operations/filter_operation.ex230
-rw-r--r--lib/pleroma/web/api_spec/operations/follow_request_operation.ex65
-rw-r--r--lib/pleroma/web/api_spec/operations/instance_operation.ex175
-rw-r--r--lib/pleroma/web/api_spec/operations/list_operation.ex188
-rw-r--r--lib/pleroma/web/api_spec/operations/marker_operation.ex142
-rw-r--r--lib/pleroma/web/api_spec/operations/media_operation.ex132
-rw-r--r--lib/pleroma/web/api_spec/operations/notification_operation.ex219
-rw-r--r--lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex96
-rw-r--r--lib/pleroma/web/api_spec/operations/pleroma_conversation_operation.ex106
-rw-r--r--lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex418
-rw-r--r--lib/pleroma/web/api_spec/operations/pleroma_mascot_operation.ex79
-rw-r--r--lib/pleroma/web/api_spec/operations/pleroma_notification_operation.ex48
-rw-r--r--lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex102
-rw-r--r--lib/pleroma/web/api_spec/operations/poll_operation.ex76
-rw-r--r--lib/pleroma/web/api_spec/operations/report_operation.ex82
-rw-r--r--lib/pleroma/web/api_spec/operations/scheduled_activity_operation.ex96
-rw-r--r--lib/pleroma/web/api_spec/operations/search_operation.ex208
-rw-r--r--lib/pleroma/web/api_spec/operations/status_operation.ex519
-rw-r--r--lib/pleroma/web/api_spec/operations/subscription_operation.ex232
-rw-r--r--lib/pleroma/web/api_spec/operations/timeline_operation.ex191
-rw-r--r--lib/pleroma/web/api_spec/render_error.ex234
-rw-r--r--lib/pleroma/web/api_spec/schemas/account.ex220
-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/chat.ex75
-rw-r--r--lib/pleroma/web/api_spec/schemas/chat_message.ex74
-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.ex334
-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/ldap_authenticator.ex43
-rw-r--r--lib/pleroma/web/auth/pleroma_authenticator.ex7
-rw-r--r--lib/pleroma/web/auth/totp_authenticator.ex45
-rw-r--r--lib/pleroma/web/chat_channel.ex7
-rw-r--r--lib/pleroma/web/common_api/activity_draft.ex42
-rw-r--r--lib/pleroma/web/common_api/common_api.ex367
-rw-r--r--lib/pleroma/web/common_api/utils.ex73
-rw-r--r--lib/pleroma/web/controller_helper.ex84
-rw-r--r--lib/pleroma/web/embed_controller.ex42
-rw-r--r--lib/pleroma/web/endpoint.ex25
-rw-r--r--lib/pleroma/web/fallback_redirect_controller.ex81
-rw-r--r--lib/pleroma/web/federator/federator.ex13
-rw-r--r--lib/pleroma/web/feed/feed_view.ex9
-rw-r--r--lib/pleroma/web/feed/tag_controller.ex8
-rw-r--r--lib/pleroma/web/feed/user_controller.ex33
-rw-r--r--lib/pleroma/web/masto_fe_controller.ex12
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/account_controller.ex297
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/app_controller.ex17
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/auth_controller.ex4
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex7
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex10
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex19
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex4
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/filter_controller.ex57
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex5
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/instance_controller.ex10
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/list_controller.ex30
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/marker_controller.ex12
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex8
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/media_controller.ex49
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/notification_controller.ex42
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/poll_controller.ex8
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/report_controller.ex5
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex14
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/search_controller.ex114
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/status_controller.ex145
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex44
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex21
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex157
-rw-r--r--lib/pleroma/web/mastodon_api/mastodon_api.ex16
-rw-r--r--lib/pleroma/web/mastodon_api/views/account_view.ex219
-rw-r--r--lib/pleroma/web/mastodon_api/views/app_view.ex21
-rw-r--r--lib/pleroma/web/mastodon_api/views/conversation_view.ex13
-rw-r--r--lib/pleroma/web/mastodon_api/views/filter_view.ex8
-rw-r--r--lib/pleroma/web/mastodon_api/views/instance_view.ex77
-rw-r--r--lib/pleroma/web/mastodon_api/views/marker_view.ex16
-rw-r--r--lib/pleroma/web/mastodon_api/views/notification_view.ex162
-rw-r--r--lib/pleroma/web/mastodon_api/views/poll_view.ex25
-rw-r--r--lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex8
-rw-r--r--lib/pleroma/web/mastodon_api/views/status_view.ex186
-rw-r--r--lib/pleroma/web/mastodon_api/views/subscription_view.ex (renamed from lib/pleroma/web/mastodon_api/views/push_subscription_view.ex)4
-rw-r--r--lib/pleroma/web/mastodon_api/websocket_handler.ex67
-rw-r--r--lib/pleroma/web/media_proxy/invalidation.ex38
-rw-r--r--lib/pleroma/web/media_proxy/invalidations/http.ex40
-rw-r--r--lib/pleroma/web/media_proxy/invalidations/script.ex43
-rw-r--r--lib/pleroma/web/media_proxy/media_proxy.ex64
-rw-r--r--lib/pleroma/web/media_proxy/media_proxy_controller.ex4
-rw-r--r--lib/pleroma/web/metadata.ex7
-rw-r--r--lib/pleroma/web/metadata/restrict_indexing.ex25
-rw-r--r--lib/pleroma/web/mongooseim/mongoose_im_controller.ex8
-rw-r--r--lib/pleroma/web/nodeinfo/nodeinfo.ex91
-rw-r--r--lib/pleroma/web/nodeinfo/nodeinfo_controller.ex151
-rw-r--r--lib/pleroma/web/oauth/app.ex85
-rw-r--r--lib/pleroma/web/oauth/mfa_controller.ex98
-rw-r--r--lib/pleroma/web/oauth/mfa_view.ex17
-rw-r--r--lib/pleroma/web/oauth/oauth_controller.ex131
-rw-r--r--lib/pleroma/web/oauth/oauth_view.ex22
-rw-r--r--lib/pleroma/web/oauth/scopes.ex5
-rw-r--r--lib/pleroma/web/oauth/token/clean_worker.ex38
-rw-r--r--lib/pleroma/web/oauth/token/response.ex36
-rw-r--r--lib/pleroma/web/ostatus/ostatus_controller.ex22
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/account_controller.ex102
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/chat_controller.ex179
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex94
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex643
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex304
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex64
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex5
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/notification_controller.ex36
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex199
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex36
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex133
-rw-r--r--lib/pleroma/web/pleroma_api/views/chat/message_reference_view.ex50
-rw-r--r--lib/pleroma/web/pleroma_api/views/chat_view.ex44
-rw-r--r--lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex33
-rw-r--r--lib/pleroma/web/pleroma_api/views/scrobble_view.ex37
-rw-r--r--lib/pleroma/web/preload.ex36
-rw-r--r--lib/pleroma/web/preload/instance.ex50
-rw-r--r--lib/pleroma/web/preload/provider.ex7
-rw-r--r--lib/pleroma/web/preload/timelines.ex39
-rw-r--r--lib/pleroma/web/preload/user.ex26
-rw-r--r--lib/pleroma/web/push/impl.ex38
-rw-r--r--lib/pleroma/web/push/subscription.ex10
-rw-r--r--lib/pleroma/web/rel_me.ex18
-rw-r--r--lib/pleroma/web/rich_media/helpers.ex40
-rw-r--r--lib/pleroma/web/rich_media/parser.ex13
-rw-r--r--lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex25
-rw-r--r--lib/pleroma/web/rich_media/parsers/oembed_parser.ex6
-rw-r--r--lib/pleroma/web/rich_media/parsers/ogp.ex11
-rw-r--r--lib/pleroma/web/rich_media/parsers/twitter_card.ex15
-rw-r--r--lib/pleroma/web/router.ex437
-rw-r--r--lib/pleroma/web/static_fe/static_fe_controller.ex32
-rw-r--r--lib/pleroma/web/static_fe/static_fe_view.ex9
-rw-r--r--lib/pleroma/web/streamer/ping.ex37
-rw-r--r--lib/pleroma/web/streamer/state.ex71
-rw-r--r--lib/pleroma/web/streamer/streamer.ex261
-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/embed/_attachment.html.eex8
-rw-r--r--lib/pleroma/web/templates/embed/show.html.eex76
-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/app.html.eex2
-rw-r--r--lib/pleroma/web/templates/layout/embed.html.eex15
-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.eex4
-rw-r--r--lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex2
-rw-r--r--lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex13
-rw-r--r--lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex49
-rw-r--r--lib/pleroma/web/twitter_api/controllers/util_controller.ex94
-rw-r--r--lib/pleroma/web/twitter_api/twitter_api.ex161
-rw-r--r--lib/pleroma/web/twitter_api/twitter_api_controller.ex24
-rw-r--r--lib/pleroma/web/twitter_api/views/util_view.ex14
-rw-r--r--lib/pleroma/web/views/embed_view.ex74
-rw-r--r--lib/pleroma/web/views/masto_fe_view.ex32
-rw-r--r--lib/pleroma/web/views/streamer_view.ex25
-rw-r--r--lib/pleroma/web/web.ex105
-rw-r--r--lib/pleroma/web/web_finger/web_finger.ex90
-rw-r--r--lib/pleroma/workers/attachments_cleanup_worker.ex158
-rw-r--r--lib/pleroma/workers/background_worker.ex34
-rw-r--r--lib/pleroma/workers/cron/clear_oauth_token_worker.ex4
-rw-r--r--lib/pleroma/workers/cron/digest_emails_worker.ex4
-rw-r--r--lib/pleroma/workers/cron/new_users_digest_worker.ex4
-rw-r--r--lib/pleroma/workers/cron/purge_expired_activities_worker.ex6
-rw-r--r--lib/pleroma/workers/cron/stats_worker.ex3
-rw-r--r--lib/pleroma/workers/mailer_worker.ex2
-rw-r--r--lib/pleroma/workers/publisher_worker.ex6
-rw-r--r--lib/pleroma/workers/receiver_worker.ex2
-rw-r--r--lib/pleroma/workers/remote_fetcher_worker.ex8
-rw-r--r--lib/pleroma/workers/scheduled_activity_worker.ex4
-rw-r--r--lib/pleroma/workers/transmogrifier_worker.ex2
-rw-r--r--lib/pleroma/workers/web_pusher_worker.ex2
-rw-r--r--lib/pleroma/workers/worker_helper.ex4
401 files changed, 23589 insertions, 6567 deletions
diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex
index 6213d0eb7..97feebeaa 100644
--- a/lib/pleroma/activity.ex
+++ b/lib/pleroma/activity.ex
@@ -24,16 +24,6 @@ defmodule Pleroma.Activity do
@primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
- # https://github.com/tootsuite/mastodon/blob/master/app/models/notification.rb#L19
- @mastodon_notification_types %{
- "Create" => "mention",
- "Follow" => ["follow", "follow_request"],
- "Announce" => "reblog",
- "Like" => "favourite",
- "Move" => "move",
- "EmojiReact" => "pleroma:emoji_reaction"
- }
-
schema "activities" do
field(:data, :map)
field(:local, :boolean, default: true)
@@ -41,6 +31,10 @@ defmodule Pleroma.Activity do
field(:recipients, {:array, :string}, default: [])
field(:thread_muted?, :boolean, virtual: true)
+ # A field that can be used if you need to join some kind of other
+ # id to order / paginate this field by
+ field(:pagination_id, :string, virtual: true)
+
# This is a fake relation,
# do not use outside of with_preloaded_user_actor/with_joined_user_actor
has_one(:user_actor, User, on_delete: :nothing, foreign_key: :id)
@@ -300,32 +294,6 @@ defmodule Pleroma.Activity do
def follow_accepted?(_), do: false
- @spec mastodon_notification_type(Activity.t()) :: String.t() | nil
-
- for {ap_type, type} <- @mastodon_notification_types, not is_list(type) do
- def mastodon_notification_type(%Activity{data: %{"type" => unquote(ap_type)}}),
- do: unquote(type)
- end
-
- def mastodon_notification_type(%Activity{data: %{"type" => "Follow"}} = activity) do
- if follow_accepted?(activity) do
- "follow"
- else
- "follow_request"
- end
- end
-
- def mastodon_notification_type(%Activity{}), do: nil
-
- @spec from_mastodon_notification_type(String.t()) :: String.t() | nil
- @doc "Converts Mastodon notification type to AR activity type"
- def from_mastodon_notification_type(type) do
- with {k, _v} <-
- Enum.find(@mastodon_notification_types, fn {_k, v} -> type in List.wrap(v) end) do
- k
- end
- end
-
def all_by_actor_and_id(actor, status_ids \\ [])
def all_by_actor_and_id(_actor, []), do: []
@@ -372,4 +340,10 @@ defmodule Pleroma.Activity do
_ -> nil
end
end
+
+ @spec pinned_by_actor?(Activity.t()) :: boolean()
+ def pinned_by_actor?(%Activity{} = activity) do
+ actor = user_actor(activity)
+ activity.id in actor.pinned_activities
+ end
end
diff --git a/lib/pleroma/activity/queries.ex b/lib/pleroma/activity/queries.ex
index 633eca0b3..c99aae44b 100644
--- a/lib/pleroma/activity/queries.ex
+++ b/lib/pleroma/activity/queries.ex
@@ -32,6 +32,13 @@ defmodule Pleroma.Activity.Queries do
from(a in query, where: a.actor == ^ap_id)
end
+ def find_by_object_ap_id(activities, object_ap_id) do
+ Enum.find(
+ activities,
+ &(object_ap_id in [is_map(&1.data["object"]) && &1.data["object"]["id"], &1.data["object"]])
+ )
+ end
+
@spec by_object_id(query, String.t() | [String.t()]) :: query
def by_object_id(query \\ Activity, object_id)
diff --git a/lib/pleroma/activity_expiration.ex b/lib/pleroma/activity_expiration.ex
index db9c88d84..955f0578e 100644
--- a/lib/pleroma/activity_expiration.ex
+++ b/lib/pleroma/activity_expiration.ex
@@ -20,11 +20,11 @@ defmodule Pleroma.ActivityExpiration do
field(:scheduled_at, :naive_datetime)
end
- def changeset(%ActivityExpiration{} = expiration, attrs) do
+ def changeset(%ActivityExpiration{} = expiration, attrs, validate_scheduled_at) do
expiration
|> cast(attrs, [:scheduled_at])
|> validate_required([:scheduled_at])
- |> validate_scheduled_at()
+ |> validate_scheduled_at(validate_scheduled_at)
end
def get_by_activity_id(activity_id) do
@@ -33,9 +33,9 @@ defmodule Pleroma.ActivityExpiration do
|> Repo.one()
end
- def create(%Activity{} = activity, scheduled_at) do
+ def create(%Activity{} = activity, scheduled_at, validate_scheduled_at \\ true) do
%ActivityExpiration{activity_id: activity.id}
- |> changeset(%{scheduled_at: scheduled_at})
+ |> changeset(%{scheduled_at: scheduled_at}, validate_scheduled_at)
|> Repo.insert()
end
@@ -46,10 +46,17 @@ defmodule Pleroma.ActivityExpiration do
ActivityExpiration
|> where([exp], exp.scheduled_at < ^naive_datetime)
+ |> limit(50)
+ |> preload(:activity)
|> Repo.all()
+ |> Enum.reject(fn %{activity: activity} ->
+ Activity.pinned_by_actor?(activity)
+ end)
end
- def validate_scheduled_at(changeset) do
+ def validate_scheduled_at(changeset, false), do: changeset
+
+ def validate_scheduled_at(changeset, true) do
validate_change(changeset, :scheduled_at, fn _, scheduled_at ->
if not expires_late_enough?(scheduled_at) do
[scheduled_at: "an ephemeral activity must live for at least one hour"]
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index 33f1705df..c0b5db9f1 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -3,8 +3,12 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Application do
- import Cachex.Spec
use Application
+
+ import Cachex.Spec
+
+ alias Pleroma.Config
+
require Logger
@name Mix.Project.config()[:name]
@@ -18,9 +22,9 @@ defmodule Pleroma.Application do
def repository, do: @repository
def user_agent do
- case Pleroma.Config.get([:http, :user_agent], :default) do
+ case Config.get([:http, :user_agent], :default) do
:default ->
- info = "#{Pleroma.Web.base_url()} <#{Pleroma.Config.get([:instance, :email], "")}>"
+ info = "#{Pleroma.Web.base_url()} <#{Config.get([:instance, :email], "")}>"
named_version() <> "; " <> info
custom ->
@@ -31,29 +35,59 @@ defmodule Pleroma.Application do
# See http://elixir-lang.org/docs/stable/elixir/Application.html
# for more information on OTP Applications
def start(_type, _args) do
- Pleroma.Config.Holder.save_default()
+ # Scrubbers are compiled at runtime and therefore will cause a conflict
+ # every time the application is restarted, so we disable module
+ # conflicts at runtime
+ Code.compiler_options(ignore_module_conflict: true)
+ Pleroma.Telemetry.Logger.attach()
+ Config.Holder.save_default()
Pleroma.HTML.compile_scrubbers()
- Pleroma.Config.DeprecationWarnings.warn()
+ Config.DeprecationWarnings.warn()
Pleroma.Plugs.HTTPSecurityPlug.warn_if_disabled()
- Pleroma.Repo.check_migrations_applied!()
+ Pleroma.ApplicationRequirements.verify!()
setup_instrumenters()
load_custom_modules()
+ check_system_commands()
+ Pleroma.Docs.JSON.compile()
+
+ adapter = Application.get_env(:tesla, :adapter)
+
+ if adapter == Tesla.Adapter.Gun do
+ if version = Pleroma.OTPVersion.version() do
+ [major, minor] =
+ version
+ |> String.split(".")
+ |> Enum.map(&String.to_integer/1)
+ |> Enum.take(2)
+
+ if (major == 22 and minor < 2) or major < 22 do
+ raise "
+ !!!OTP VERSION WARNING!!!
+ You are using gun adapter with OTP version #{version}, which doesn't support correct handling of unordered certificates chains. Please update your Erlang/OTP to at least 22.2.
+ "
+ end
+ else
+ raise "
+ !!!OTP VERSION WARNING!!!
+ To support correct handling of unordered certificates chains - OTP version must be > 22.2.
+ "
+ end
+ end
# Define workers and child supervisors to be supervised
children =
[
Pleroma.Repo,
- Pleroma.Config.TransferTask,
+ Config.TransferTask,
Pleroma.Emoji,
- Pleroma.Captcha,
Pleroma.Plugs.RateLimiter.Supervisor
] ++
cachex_children() ++
- hackney_pool_children() ++
+ http_children(adapter, @env) ++
[
Pleroma.Stats,
Pleroma.JobQueueMonitor,
- {Oban, Pleroma.Config.get(Oban)}
+ {Oban, Config.get(Oban)}
] ++
task_children(@env) ++
streamer_child(@env) ++
@@ -70,7 +104,7 @@ defmodule Pleroma.Application do
end
def load_custom_modules do
- dir = Pleroma.Config.get([:modules, :runtime_dir])
+ dir = Config.get([:modules, :runtime_dir])
if dir && File.exists?(dir) do
dir
@@ -111,20 +145,6 @@ defmodule Pleroma.Application do
Pleroma.Web.Endpoint.Instrumenter.setup()
end
- def enabled_hackney_pools do
- [:media] ++
- if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do
- [:federation]
- else
- []
- end ++
- if Pleroma.Config.get([Pleroma.Upload, :proxy_remote]) do
- [:upload]
- else
- []
- end
- end
-
defp cachex_children do
[
build_cachex("used_captcha", ttl_interval: seconds_valid_interval()),
@@ -135,7 +155,8 @@ defmodule Pleroma.Application do
build_cachex("idempotency", expiration: idempotency_expiration(), limit: 2500),
build_cachex("web_resp", limit: 2500),
build_cachex("emoji_packs", expiration: emoji_packs_expiration(), limit: 10),
- build_cachex("failed_proxy_url", limit: 2500)
+ build_cachex("failed_proxy_url", limit: 2500),
+ build_cachex("banned_urls", default_ttl: :timer.hours(24 * 30), limit: 5_000)
]
end
@@ -146,21 +167,29 @@ defmodule Pleroma.Application do
do: expiration(default: :timer.seconds(6 * 60 * 60), interval: :timer.seconds(60))
defp seconds_valid_interval,
- do: :timer.seconds(Pleroma.Config.get!([Pleroma.Captcha, :seconds_valid]))
+ do: :timer.seconds(Config.get!([Pleroma.Captcha, :seconds_valid]))
- defp build_cachex(type, opts),
+ @spec build_cachex(String.t(), keyword()) :: map()
+ def build_cachex(type, opts),
do: %{
id: String.to_atom("cachex_" <> type),
start: {Cachex, :start_link, [String.to_atom(type <> "_cache"), opts]},
type: :worker
}
- defp chat_enabled?, do: Pleroma.Config.get([:chat, :enabled])
+ defp chat_enabled?, do: Config.get([:chat, :enabled])
- defp streamer_child(:test), do: []
+ defp streamer_child(env) when env in [:test, :benchmark], do: []
defp streamer_child(_) do
- [Pleroma.Web.Streamer.supervisor()]
+ [
+ {Registry,
+ [
+ name: Pleroma.Web.Streamer.registry(),
+ keys: :duplicate,
+ partitions: System.schedulers_online()
+ ]}
+ ]
end
defp chat_child(_env, true) do
@@ -169,13 +198,6 @@ defmodule Pleroma.Application do
defp chat_child(_, _), do: []
- defp hackney_pool_children do
- for pool <- enabled_hackney_pools() do
- options = Pleroma.Config.get([:hackney_pools, pool])
- :hackney_pool.child_spec(pool, options)
- end
- end
-
defp task_children(:test) do
[
%{
@@ -200,4 +222,49 @@ defmodule Pleroma.Application do
}
]
end
+
+ # start hackney and gun pools in tests
+ defp http_children(_, :test) do
+ http_children(Tesla.Adapter.Hackney, nil) ++ http_children(Tesla.Adapter.Gun, nil)
+ end
+
+ defp http_children(Tesla.Adapter.Hackney, _) do
+ pools = [:federation, :media]
+
+ pools =
+ if Config.get([Pleroma.Upload, :proxy_remote]) do
+ [:upload | pools]
+ else
+ pools
+ end
+
+ for pool <- pools do
+ options = Config.get([:hackney_pools, pool])
+ :hackney_pool.child_spec(pool, options)
+ end
+ end
+
+ defp http_children(Tesla.Adapter.Gun, _) do
+ Pleroma.Gun.ConnectionPool.children() ++
+ [{Task, &Pleroma.HTTP.AdapterHelper.Gun.limiter_setup/0}]
+ end
+
+ defp http_children(_, _), do: []
+
+ defp check_system_commands do
+ filters = Config.get([Pleroma.Upload, :filters])
+
+ check_filter = fn filter, command_required ->
+ with true <- filter in filters,
+ false <- Pleroma.Utils.command_available?(command_required) do
+ Logger.error(
+ "#{filter} is specified in list of Pleroma.Upload filters, but the #{command_required} command is not found"
+ )
+ end
+ end
+
+ check_filter.(Pleroma.Upload.Filters.Exiftool, "exiftool")
+ check_filter.(Pleroma.Upload.Filters.Mogrify, "mogrify")
+ check_filter.(Pleroma.Upload.Filters.Mogrifun, "mogrify")
+ end
end
diff --git a/lib/pleroma/application_requirements.ex b/lib/pleroma/application_requirements.ex
new file mode 100644
index 000000000..16f62b6f5
--- /dev/null
+++ b/lib/pleroma/application_requirements.ex
@@ -0,0 +1,143 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.ApplicationRequirements do
+ @moduledoc """
+ The module represents the collection of validations to runs before start server.
+ """
+
+ defmodule VerifyError, do: defexception([:message])
+
+ import Ecto.Query
+
+ require Logger
+
+ @spec verify!() :: :ok | VerifyError.t()
+ def verify! do
+ :ok
+ |> check_confirmation_accounts!
+ |> check_migrations_applied!()
+ |> check_welcome_message_config!()
+ |> check_rum!()
+ |> handle_result()
+ end
+
+ defp handle_result(:ok), do: :ok
+ defp handle_result({:error, message}), do: raise(VerifyError, message: message)
+
+ defp check_welcome_message_config!(:ok) do
+ if Pleroma.Config.get([:welcome, :email, :enabled], false) and
+ not Pleroma.Emails.Mailer.enabled?() do
+ Logger.error("""
+ To send welcome email do you need to enable mail.
+ \nconfig :pleroma, Pleroma.Emails.Mailer, enabled: true
+ """)
+
+ {:error, "The mail disabled."}
+ else
+ :ok
+ end
+ end
+
+ defp check_welcome_message_config!(result), do: result
+
+ # Checks account confirmation email
+ #
+ def check_confirmation_accounts!(:ok) do
+ if Pleroma.Config.get([:instance, :account_activation_required]) &&
+ not Pleroma.Config.get([Pleroma.Emails.Mailer, :enabled]) do
+ Logger.error(
+ "Account activation enabled, but no Mailer settings enabled.\nPlease set config :pleroma, :instance, account_activation_required: false\nOtherwise setup and enable Mailer."
+ )
+
+ {:error,
+ "Account activation enabled, but Mailer is disabled. Cannot send confirmation emails."}
+ else
+ :ok
+ end
+ end
+
+ def check_confirmation_accounts!(result), do: result
+
+ # Checks for pending migrations.
+ #
+ def check_migrations_applied!(:ok) do
+ unless Pleroma.Config.get(
+ [:i_am_aware_this_may_cause_data_loss, :disable_migration_check],
+ false
+ ) do
+ {_, res, _} =
+ Ecto.Migrator.with_repo(Pleroma.Repo, fn repo ->
+ down_migrations =
+ Ecto.Migrator.migrations(repo)
+ |> Enum.reject(fn
+ {:up, _, _} -> true
+ {:down, _, _} -> false
+ end)
+
+ if length(down_migrations) > 0 do
+ down_migrations_text =
+ Enum.map(down_migrations, fn {:down, id, name} -> "- #{name} (#{id})\n" end)
+
+ Logger.error(
+ "The following migrations were not applied:\n#{down_migrations_text}If you want to start Pleroma anyway, set\nconfig :pleroma, :i_am_aware_this_may_cause_data_loss, disable_migration_check: true"
+ )
+
+ {:error, "Unapplied Migrations detected"}
+ else
+ :ok
+ end
+ end)
+
+ res
+ else
+ :ok
+ end
+ end
+
+ def check_migrations_applied!(result), do: result
+
+ # Checks for settings of RUM indexes.
+ #
+ defp check_rum!(:ok) do
+ {_, res, _} =
+ Ecto.Migrator.with_repo(Pleroma.Repo, fn repo ->
+ migrate =
+ from(o in "columns",
+ where: o.table_name == "objects",
+ where: o.column_name == "fts_content"
+ )
+ |> repo.exists?(prefix: "information_schema")
+
+ setting = Pleroma.Config.get([:database, :rum_enabled], false)
+
+ do_check_rum!(setting, migrate)
+ end)
+
+ res
+ end
+
+ defp check_rum!(result), do: result
+
+ defp do_check_rum!(setting, migrate) do
+ case {setting, migrate} do
+ {true, false} ->
+ Logger.error(
+ "Use `RUM` index is enabled, but were not applied migrations for it.\nIf you want to start Pleroma anyway, set\nconfig :pleroma, :database, rum_enabled: false\nOtherwise apply the following migrations:\n`mix ecto.migrate --migrations-path priv/repo/optional_migrations/rum_indexing/`"
+ )
+
+ {:error, "Unapplied RUM Migrations detected"}
+
+ {false, true} ->
+ Logger.error(
+ "Detected applied migrations to use `RUM` index, but `RUM` isn't enable in settings.\nIf you want to use `RUM`, set\nconfig :pleroma, :database, rum_enabled: true\nOtherwise roll `RUM` migrations back.\n`mix ecto.rollback --migrations-path priv/repo/optional_migrations/rum_indexing/`"
+ )
+
+ {:error, "RUM Migrations detected"}
+
+ _ ->
+ :ok
+ end
+ end
+end
diff --git a/lib/pleroma/bbs/authenticator.ex b/lib/pleroma/bbs/authenticator.ex
index e5b37f33e..815de7002 100644
--- a/lib/pleroma/bbs/authenticator.ex
+++ b/lib/pleroma/bbs/authenticator.ex
@@ -4,7 +4,7 @@
defmodule Pleroma.BBS.Authenticator do
use Sshd.PasswordAuthenticator
- alias Comeonin.Pbkdf2
+ alias Pleroma.Plugs.AuthenticationPlug
alias Pleroma.User
def authenticate(username, password) do
@@ -12,7 +12,7 @@ defmodule Pleroma.BBS.Authenticator do
password = to_string(password)
with %User{} = user <- User.get_by_nickname(username) do
- Pbkdf2.checkpw(password, user.password_hash)
+ AuthenticationPlug.checkpw(password, user.password_hash)
else
_e -> false
end
diff --git a/lib/pleroma/bbs/handler.ex b/lib/pleroma/bbs/handler.ex
index c7bc8ef6c..cd523cf7d 100644
--- a/lib/pleroma/bbs/handler.ex
+++ b/lib/pleroma/bbs/handler.ex
@@ -66,7 +66,7 @@ defmodule Pleroma.BBS.Handler do
with %Activity{} <- Activity.get_by_id(activity_id),
{:ok, _activity} <-
- CommonAPI.post(user, %{"status" => rest, "in_reply_to_status_id" => activity_id}) do
+ CommonAPI.post(user, %{status: rest, in_reply_to_status_id: activity_id}) do
IO.puts("Replied!")
else
_e -> IO.puts("Could not reply...")
@@ -78,7 +78,7 @@ defmodule Pleroma.BBS.Handler do
def handle_command(%{user: user} = state, "p " <> text) do
text = String.trim(text)
- with {:ok, _activity} <- CommonAPI.post(user, %{"status" => text}) do
+ with {:ok, _activity} <- CommonAPI.post(user, %{status: text}) do
IO.puts("Posted!")
else
_e -> IO.puts("Could not post...")
@@ -92,10 +92,10 @@ defmodule Pleroma.BBS.Handler do
params =
%{}
- |> Map.put("type", ["Create"])
- |> Map.put("blocking_user", user)
- |> Map.put("muting_user", user)
- |> Map.put("user", user)
+ |> Map.put(:type, ["Create"])
+ |> Map.put(:blocking_user, user)
+ |> Map.put(:muting_user, user)
+ |> Map.put(:user, user)
activities =
[user.ap_id | Pleroma.User.following(user)]
diff --git a/lib/pleroma/captcha/captcha.ex b/lib/pleroma/captcha/captcha.ex
index cf75c3adc..6ab754b6f 100644
--- a/lib/pleroma/captcha/captcha.ex
+++ b/lib/pleroma/captcha/captcha.ex
@@ -3,53 +3,22 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Captcha do
- import Pleroma.Web.Gettext
-
alias Calendar.DateTime
alias Plug.Crypto.KeyGenerator
alias Plug.Crypto.MessageEncryptor
- use GenServer
-
- @doc false
- def start_link(_) do
- GenServer.start_link(__MODULE__, [], name: __MODULE__)
- end
-
- @doc false
- def init(_) do
- {:ok, nil}
- end
-
@doc """
Ask the configured captcha service for a new captcha
"""
def new do
- GenServer.call(__MODULE__, :new)
- end
-
- @doc """
- Ask the configured captcha service to validate the captcha
- """
- def validate(token, captcha, answer_data) do
- GenServer.call(__MODULE__, {:validate, token, captcha, answer_data})
- end
-
- @doc false
- def handle_call(:new, _from, state) do
- enabled = Pleroma.Config.get([__MODULE__, :enabled])
-
- if !enabled do
- {:reply, %{type: :none}, state}
+ if not enabled?() do
+ %{type: :none}
else
new_captcha = method().new()
- secret_key_base = Pleroma.Config.get!([Pleroma.Web.Endpoint, :secret_key_base])
-
# This make salt a little different for two keys
- token = new_captcha[:token]
- secret = KeyGenerator.generate(secret_key_base, token <> "_encrypt")
- sign_secret = KeyGenerator.generate(secret_key_base, token <> "_sign")
+ {secret, sign_secret} = secret_pair(new_captcha[:token])
+
# Basically copy what Phoenix.Token does here, add the time to
# the actual data and make it a binary to then encrypt it
encrypted_captcha_answer =
@@ -60,55 +29,73 @@ defmodule Pleroma.Captcha do
|> :erlang.term_to_binary()
|> MessageEncryptor.encrypt(secret, sign_secret)
- {
- :reply,
- # Replace the answer with the encrypted answer
- %{new_captcha | answer_data: encrypted_captcha_answer},
- state
- }
+ # Replace the answer with the encrypted answer
+ %{new_captcha | answer_data: encrypted_captcha_answer}
end
end
- @doc false
- def handle_call({:validate, token, captcha, answer_data}, _from, state) do
+ @doc """
+ Ask the configured captcha service to validate the captcha
+ """
+ def validate(token, captcha, answer_data) do
+ with {:ok, %{at: at, answer_data: answer_md5}} <- validate_answer_data(token, answer_data),
+ :ok <- validate_expiration(at),
+ :ok <- validate_usage(token),
+ :ok <- method().validate(token, captcha, answer_md5),
+ {:ok, _} <- mark_captcha_as_used(token) do
+ :ok
+ end
+ end
+
+ def enabled?, do: Pleroma.Config.get([__MODULE__, :enabled], false)
+
+ defp seconds_valid, do: Pleroma.Config.get!([__MODULE__, :seconds_valid])
+
+ defp secret_pair(token) do
secret_key_base = Pleroma.Config.get!([Pleroma.Web.Endpoint, :secret_key_base])
secret = KeyGenerator.generate(secret_key_base, token <> "_encrypt")
sign_secret = KeyGenerator.generate(secret_key_base, token <> "_sign")
+ {secret, sign_secret}
+ end
+
+ defp validate_answer_data(token, answer_data) do
+ {secret, sign_secret} = secret_pair(token)
+
+ with false <- is_nil(answer_data),
+ {:ok, data} <- MessageEncryptor.decrypt(answer_data, secret, sign_secret),
+ %{at: at, answer_data: answer_md5} <- :erlang.binary_to_term(data) do
+ {:ok, %{at: at, answer_data: answer_md5}}
+ else
+ _ -> {:error, :invalid_answer_data}
+ end
+ end
+
+ defp validate_expiration(created_at) do
# If the time found is less than (current_time-seconds_valid) then the time has already passed
# Later we check that the time found is more than the presumed invalidatation time, that means
# that the data is still valid and the captcha can be checked
- seconds_valid = Pleroma.Config.get!([Pleroma.Captcha, :seconds_valid])
- valid_if_after = DateTime.subtract!(DateTime.now_utc(), seconds_valid)
-
- result =
- with false <- is_nil(answer_data),
- {:ok, data} <- MessageEncryptor.decrypt(answer_data, secret, sign_secret),
- %{at: at, answer_data: answer_md5} <- :erlang.binary_to_term(data) do
- try do
- if DateTime.before?(at, valid_if_after),
- do: throw({:error, dgettext("errors", "CAPTCHA expired")})
-
- if not is_nil(Cachex.get!(:used_captcha_cache, token)),
- do: throw({:error, dgettext("errors", "CAPTCHA already used")})
-
- res = method().validate(token, captcha, answer_md5)
- # Throw if an error occurs
- if res != :ok, do: throw(res)
-
- # Mark this captcha as used
- {:ok, _} =
- Cachex.put(:used_captcha_cache, token, true, ttl: :timer.seconds(seconds_valid))
-
- :ok
- catch
- :throw, e -> e
- end
- else
- _ -> {:error, dgettext("errors", "Invalid answer data")}
- end
-
- {:reply, result, state}
+
+ valid_if_after = DateTime.subtract!(DateTime.now_utc(), seconds_valid())
+
+ if DateTime.before?(created_at, valid_if_after) do
+ {:error, :expired}
+ else
+ :ok
+ end
+ end
+
+ defp validate_usage(token) do
+ if is_nil(Cachex.get!(:used_captcha_cache, token)) do
+ :ok
+ else
+ {:error, :already_used}
+ end
+ end
+
+ defp mark_captcha_as_used(token) do
+ ttl = seconds_valid() |> :timer.seconds()
+ Cachex.put(:used_captcha_cache, token, true, ttl: ttl)
end
defp method, do: Pleroma.Config.get!([__MODULE__, :method])
diff --git a/lib/pleroma/captcha/kocaptcha.ex b/lib/pleroma/captcha/kocaptcha.ex
index 06ceb20b6..337506647 100644
--- a/lib/pleroma/captcha/kocaptcha.ex
+++ b/lib/pleroma/captcha/kocaptcha.ex
@@ -3,7 +3,6 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Captcha.Kocaptcha do
- import Pleroma.Web.Gettext
alias Pleroma.Captcha.Service
@behaviour Service
@@ -13,7 +12,7 @@ defmodule Pleroma.Captcha.Kocaptcha do
case Tesla.get(endpoint <> "/new") do
{:error, _} ->
- %{error: dgettext("errors", "Kocaptcha service unavailable")}
+ %{error: :kocaptcha_service_unavailable}
{:ok, res} ->
json_resp = Jason.decode!(res.body)
@@ -22,7 +21,8 @@ defmodule Pleroma.Captcha.Kocaptcha do
type: :kocaptcha,
token: json_resp["token"],
url: endpoint <> json_resp["url"],
- answer_data: json_resp["md5"]
+ answer_data: json_resp["md5"],
+ seconds_valid: Pleroma.Config.get([Pleroma.Captcha, :seconds_valid])
}
end
end
@@ -33,6 +33,6 @@ defmodule Pleroma.Captcha.Kocaptcha do
if not is_nil(captcha) and
:crypto.hash(:md5, captcha) |> Base.encode16() == String.upcase(answer_data),
do: :ok,
- else: {:error, dgettext("errors", "Invalid CAPTCHA")}
+ else: {:error, :invalid}
end
end
diff --git a/lib/pleroma/captcha/native.ex b/lib/pleroma/captcha/native.ex
index 06c479ca9..8d604d2b2 100644
--- a/lib/pleroma/captcha/native.ex
+++ b/lib/pleroma/captcha/native.ex
@@ -3,7 +3,6 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Captcha.Native do
- import Pleroma.Web.Gettext
alias Pleroma.Captcha.Service
@behaviour Service
@@ -11,21 +10,22 @@ defmodule Pleroma.Captcha.Native do
def new do
case Captcha.get() do
:error ->
- %{error: dgettext("errors", "Captcha error")}
+ %{error: :captcha_error}
{:ok, answer_data, img_binary} ->
%{
type: :native,
token: token(),
url: "data:image/png;base64," <> Base.encode64(img_binary),
- answer_data: answer_data
+ answer_data: answer_data,
+ seconds_valid: Pleroma.Config.get([Pleroma.Captcha, :seconds_valid])
}
end
end
@impl Service
def validate(_token, captcha, captcha) when not is_nil(captcha), do: :ok
- def validate(_token, _captcha, _answer), do: {:error, dgettext("errors", "Invalid CAPTCHA")}
+ def validate(_token, _captcha, _answer), do: {:error, :invalid}
defp token do
10
diff --git a/lib/pleroma/chat.ex b/lib/pleroma/chat.ex
new file mode 100644
index 000000000..24a86371e
--- /dev/null
+++ b/lib/pleroma/chat.ex
@@ -0,0 +1,72 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Chat do
+ use Ecto.Schema
+
+ import Ecto.Changeset
+
+ alias Pleroma.Repo
+ alias Pleroma.User
+
+ @moduledoc """
+ Chat keeps a reference to ChatMessage conversations between a user and an recipient. The recipient can be a user (for now) or a group (not implemented yet).
+
+ It is a helper only, to make it easy to display a list of chats with other people, ordered by last bump. The actual messages are retrieved by querying the recipients of the ChatMessages.
+ """
+
+ @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
+
+ schema "chats" do
+ belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
+ field(:recipient, :string)
+
+ timestamps()
+ end
+
+ def changeset(struct, params) do
+ struct
+ |> cast(params, [:user_id, :recipient])
+ |> validate_change(:recipient, fn
+ :recipient, recipient ->
+ case User.get_cached_by_ap_id(recipient) do
+ nil -> [recipient: "must be an existing user"]
+ _ -> []
+ end
+ end)
+ |> validate_required([:user_id, :recipient])
+ |> unique_constraint(:user_id, name: :chats_user_id_recipient_index)
+ end
+
+ def get_by_id(id) do
+ __MODULE__
+ |> Repo.get(id)
+ end
+
+ def get(user_id, recipient) do
+ __MODULE__
+ |> Repo.get_by(user_id: user_id, recipient: recipient)
+ end
+
+ def get_or_create(user_id, recipient) do
+ %__MODULE__{}
+ |> changeset(%{user_id: user_id, recipient: recipient})
+ |> Repo.insert(
+ # Need to set something, otherwise we get nothing back at all
+ on_conflict: [set: [recipient: recipient]],
+ returning: true,
+ conflict_target: [:user_id, :recipient]
+ )
+ end
+
+ def bump_or_create(user_id, recipient) do
+ %__MODULE__{}
+ |> changeset(%{user_id: user_id, recipient: recipient})
+ |> Repo.insert(
+ on_conflict: [set: [updated_at: NaiveDateTime.utc_now()]],
+ returning: true,
+ conflict_target: [:user_id, :recipient]
+ )
+ end
+end
diff --git a/lib/pleroma/chat/message_reference.ex b/lib/pleroma/chat/message_reference.ex
new file mode 100644
index 000000000..131ae0186
--- /dev/null
+++ b/lib/pleroma/chat/message_reference.ex
@@ -0,0 +1,117 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Chat.MessageReference do
+ @moduledoc """
+ A reference that builds a relation between an AP chat message that a user can see and whether it has been seen
+ by them, or should be displayed to them. Used to build the chat view that is presented to the user.
+ """
+
+ use Ecto.Schema
+
+ alias Pleroma.Chat
+ alias Pleroma.Object
+ alias Pleroma.Repo
+
+ import Ecto.Changeset
+ import Ecto.Query
+
+ @primary_key {:id, FlakeId.Ecto.Type, autogenerate: true}
+
+ schema "chat_message_references" do
+ belongs_to(:object, Object)
+ belongs_to(:chat, Chat, type: FlakeId.Ecto.CompatType)
+
+ field(:unread, :boolean, default: true)
+
+ timestamps()
+ end
+
+ def changeset(struct, params) do
+ struct
+ |> cast(params, [:object_id, :chat_id, :unread])
+ |> validate_required([:object_id, :chat_id, :unread])
+ end
+
+ def get_by_id(id) do
+ __MODULE__
+ |> Repo.get(id)
+ |> Repo.preload(:object)
+ end
+
+ def delete(cm_ref) do
+ cm_ref
+ |> Repo.delete()
+ end
+
+ def delete_for_object(%{id: object_id}) do
+ from(cr in __MODULE__,
+ where: cr.object_id == ^object_id
+ )
+ |> Repo.delete_all()
+ end
+
+ def for_chat_and_object(%{id: chat_id}, %{id: object_id}) do
+ __MODULE__
+ |> Repo.get_by(chat_id: chat_id, object_id: object_id)
+ |> Repo.preload(:object)
+ end
+
+ def for_chat_query(chat) do
+ from(cr in __MODULE__,
+ where: cr.chat_id == ^chat.id,
+ order_by: [desc: :id],
+ preload: [:object]
+ )
+ end
+
+ def last_message_for_chat(chat) do
+ chat
+ |> for_chat_query()
+ |> limit(1)
+ |> Repo.one()
+ end
+
+ def create(chat, object, unread) do
+ params = %{
+ chat_id: chat.id,
+ object_id: object.id,
+ unread: unread
+ }
+
+ %__MODULE__{}
+ |> changeset(params)
+ |> Repo.insert()
+ end
+
+ def unread_count_for_chat(chat) do
+ chat
+ |> for_chat_query()
+ |> where([cmr], cmr.unread == true)
+ |> Repo.aggregate(:count)
+ end
+
+ def mark_as_read(cm_ref) do
+ cm_ref
+ |> changeset(%{unread: false})
+ |> Repo.update()
+ end
+
+ def set_all_seen_for_chat(chat, last_read_id \\ nil) do
+ query =
+ chat
+ |> for_chat_query()
+ |> exclude(:order_by)
+ |> exclude(:preload)
+ |> where([cmr], cmr.unread == true)
+
+ if last_read_id do
+ query
+ |> where([cmr], cmr.id <= ^last_read_id)
+ else
+ query
+ end
+ |> Repo.update_all(set: [unread: false])
+ end
+end
diff --git a/lib/pleroma/config.ex b/lib/pleroma/config.ex
index cc80deff5..97f877595 100644
--- a/lib/pleroma/config.ex
+++ b/lib/pleroma/config.ex
@@ -11,12 +11,10 @@ defmodule Pleroma.Config do
def get([key], default), do: get(key, default)
- def get([parent_key | keys], default) do
- case :pleroma
- |> Application.get_env(parent_key)
- |> get_in(keys) do
- nil -> default
- any -> any
+ def get([_ | _] = path, default) do
+ case fetch(path) do
+ {:ok, value} -> value
+ :error -> default
end
end
@@ -34,6 +32,24 @@ defmodule Pleroma.Config do
end
end
+ def fetch(key) when is_atom(key), do: fetch([key])
+
+ def fetch([root_key | keys]) do
+ Enum.reduce_while(keys, Application.fetch_env(:pleroma, root_key), fn
+ key, {:ok, config} when is_map(config) or is_list(config) ->
+ case Access.fetch(config, key) do
+ :error ->
+ {:halt, :error}
+
+ value ->
+ {:cont, value}
+ end
+
+ _key, _config ->
+ {:halt, :error}
+ end)
+ end
+
def put([key], value), do: put(key, value)
def put([parent_key | keys], value) do
@@ -50,18 +66,31 @@ defmodule Pleroma.Config do
def delete([key]), do: delete(key)
- def delete([parent_key | keys]) do
- {_, parent} =
- Application.get_env(:pleroma, parent_key)
- |> get_and_update_in(keys, fn _ -> :pop end)
+ def delete([parent_key | keys] = path) do
+ with {:ok, _} <- fetch(path) do
+ {_, parent} =
+ parent_key
+ |> get()
+ |> get_and_update_in(keys, fn _ -> :pop end)
- Application.put_env(:pleroma, parent_key, parent)
+ Application.put_env(:pleroma, parent_key, parent)
+ end
end
def delete(key) do
Application.delete_env(:pleroma, key)
end
+ def restrict_unauthenticated_access?(resource, kind) do
+ setting = get([:restrict_unauthenticated, resource, kind])
+
+ if setting in [nil, :if_instance_is_private] do
+ !get!([:instance, :public])
+ else
+ setting
+ end
+ end
+
def oauth_consumer_strategies, do: get([:auth, :oauth_consumer_strategies], [])
def oauth_consumer_enabled?, do: oauth_consumer_strategies() != []
diff --git a/lib/pleroma/config/config_db.ex b/lib/pleroma/config/config_db.ex
index 2b43d4c36..e5b7811aa 100644
--- a/lib/pleroma/config/config_db.ex
+++ b/lib/pleroma/config/config_db.ex
@@ -6,7 +6,7 @@ defmodule Pleroma.ConfigDB do
use Ecto.Schema
import Ecto.Changeset
- import Ecto.Query
+ import Ecto.Query, only: [select: 3]
import Pleroma.Web.Gettext
alias __MODULE__
@@ -14,16 +14,6 @@ defmodule Pleroma.ConfigDB do
@type t :: %__MODULE__{}
- @full_key_update [
- {:pleroma, :ecto_repos},
- {:quack, :meta},
- {:mime, :types},
- {:cors_plug, [:max_age, :methods, :expose, :headers]},
- {:auto_linker, :opts},
- {:swarm, :node_blacklist},
- {:logger, :backends}
- ]
-
@full_subkey_update [
{:pleroma, :assets, :mascots},
{:pleroma, :emoji, :groups},
@@ -32,14 +22,10 @@ defmodule Pleroma.ConfigDB do
{:pleroma, :mrf_keyword, :replace}
]
- @regex ~r/^~r(?'delimiter'[\/|"'([{<]{1})(?'pattern'.+)[\/|"')\]}>]{1}(?'modifier'[uismxfU]*)/u
-
- @delimiters ["/", "|", "\"", "'", {"(", ")"}, {"[", "]"}, {"{", "}"}, {"<", ">"}]
-
schema "config" do
- field(:key, :string)
- field(:group, :string)
- field(:value, :binary)
+ field(:key, Pleroma.EctoType.Config.Atom)
+ field(:group, Pleroma.EctoType.Config.Atom)
+ field(:value, Pleroma.EctoType.Config.BinaryValue)
field(:db, {:array, :string}, virtual: true, default: [])
timestamps()
@@ -51,10 +37,6 @@ defmodule Pleroma.ConfigDB do
|> select([c], {c.group, c.key, c.value})
|> Repo.all()
|> Enum.reduce([], fn {group, key, value}, acc ->
- group = ConfigDB.from_string(group)
- key = ConfigDB.from_string(key)
- value = from_binary(value)
-
Keyword.update(acc, group, [{key, value}], &Keyword.merge(&1, [{key, value}]))
end)
end
@@ -64,50 +46,41 @@ defmodule Pleroma.ConfigDB do
@spec changeset(ConfigDB.t(), map()) :: Changeset.t()
def changeset(config, params \\ %{}) do
- params = Map.put(params, :value, transform(params[:value]))
-
config
|> cast(params, [:key, :group, :value])
|> validate_required([:key, :group, :value])
|> unique_constraint(:key, name: :config_group_key_index)
end
- @spec create(map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()}
- def create(params) do
+ defp create(params) do
%ConfigDB{}
|> changeset(params)
|> Repo.insert()
end
- @spec update(ConfigDB.t(), map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()}
- def update(%ConfigDB{} = config, %{value: value}) do
+ defp update(%ConfigDB{} = config, %{value: value}) do
config
|> changeset(%{value: value})
|> Repo.update()
end
- @spec get_db_keys(ConfigDB.t()) :: [String.t()]
- def get_db_keys(%ConfigDB{} = config) do
- config.value
- |> ConfigDB.from_binary()
- |> get_db_keys(config.key)
- end
-
@spec get_db_keys(keyword(), any()) :: [String.t()]
def get_db_keys(value, key) do
- if Keyword.keyword?(value) do
- value |> Keyword.keys() |> Enum.map(&convert(&1))
- else
- [convert(key)]
- end
+ keys =
+ if Keyword.keyword?(value) do
+ Keyword.keys(value)
+ else
+ [key]
+ end
+
+ Enum.map(keys, &to_json_types(&1))
end
@spec merge_group(atom(), atom(), keyword(), keyword()) :: keyword()
def merge_group(group, key, old_value, new_value) do
- new_keys = to_map_set(new_value)
+ new_keys = to_mapset(new_value)
- intersect_keys =
- old_value |> to_map_set() |> MapSet.intersection(new_keys) |> MapSet.to_list()
+ intersect_keys = old_value |> to_mapset() |> MapSet.intersection(new_keys) |> MapSet.to_list()
merged_value = ConfigDB.merge(old_value, new_value)
@@ -120,12 +93,10 @@ defmodule Pleroma.ConfigDB do
[]
end)
|> List.flatten()
- |> Enum.reduce(merged_value, fn subkey, acc ->
- Keyword.put(acc, subkey, new_value[subkey])
- end)
+ |> Enum.reduce(merged_value, &Keyword.put(&2, &1, new_value[&1]))
end
- defp to_map_set(keyword) do
+ defp to_mapset(keyword) do
keyword
|> Keyword.keys()
|> MapSet.new()
@@ -159,57 +130,54 @@ defmodule Pleroma.ConfigDB do
@spec update_or_create(map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()}
def update_or_create(params) do
+ params = Map.put(params, :value, to_elixir_types(params[:value]))
search_opts = Map.take(params, [:group, :key])
with %ConfigDB{} = config <- ConfigDB.get_by_params(search_opts),
- {:partial_update, true, config} <-
- {:partial_update, can_be_partially_updated?(config), config},
- old_value <- from_binary(config.value),
- transformed_value <- do_transform(params[:value]),
- {:can_be_merged, true, config} <- {:can_be_merged, is_list(transformed_value), config},
- new_value <-
- merge_group(
- ConfigDB.from_string(config.group),
- ConfigDB.from_string(config.key),
- old_value,
- transformed_value
- ) do
- ConfigDB.update(config, %{value: new_value})
+ {_, true, config} <- {:partial_update, can_be_partially_updated?(config), config},
+ {_, true, config} <-
+ {:can_be_merged, is_list(params[:value]) and is_list(config.value), config} do
+ new_value = merge_group(config.group, config.key, config.value, params[:value])
+ update(config, %{value: new_value})
else
{reason, false, config} when reason in [:partial_update, :can_be_merged] ->
- ConfigDB.update(config, params)
+ update(config, params)
nil ->
- ConfigDB.create(params)
+ create(params)
end
end
defp can_be_partially_updated?(%ConfigDB{} = config), do: not only_full_update?(config)
- defp only_full_update?(%ConfigDB{} = config) do
- config_group = ConfigDB.from_string(config.group)
- config_key = ConfigDB.from_string(config.key)
-
- Enum.any?(@full_key_update, fn
- {group, key} when is_list(key) ->
- config_group == group and config_key in key
-
- {group, key} ->
- config_group == group and config_key == key
+ defp only_full_update?(%ConfigDB{group: group, key: key}) do
+ full_key_update = [
+ {:pleroma, :ecto_repos},
+ {:quack, :meta},
+ {:mime, :types},
+ {:cors_plug, [:max_age, :methods, :expose, :headers]},
+ {:swarm, :node_blacklist},
+ {:logger, :backends}
+ ]
+
+ Enum.any?(full_key_update, fn
+ {s_group, s_key} ->
+ group == s_group and ((is_list(s_key) and key in s_key) or key == s_key)
end)
end
- @spec delete(map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()}
+ @spec delete(ConfigDB.t() | map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()}
+ def delete(%ConfigDB{} = config), do: Repo.delete(config)
+
def delete(params) do
search_opts = Map.delete(params, :subkeys)
with %ConfigDB{} = config <- ConfigDB.get_by_params(search_opts),
{config, sub_keys} when is_list(sub_keys) <- {config, params[:subkeys]},
- old_value <- from_binary(config.value),
- keys <- Enum.map(sub_keys, &do_transform_string(&1)),
- {:partial_remove, config, new_value} when new_value != [] <-
- {:partial_remove, config, Keyword.drop(old_value, keys)} do
- ConfigDB.update(config, %{value: new_value})
+ keys <- Enum.map(sub_keys, &string_to_elixir_types(&1)),
+ {_, config, new_value} when new_value != [] <-
+ {:partial_remove, config, Keyword.drop(config.value, keys)} do
+ update(config, %{value: new_value})
else
{:partial_remove, config, []} ->
Repo.delete(config)
@@ -225,37 +193,32 @@ defmodule Pleroma.ConfigDB do
end
end
- @spec from_binary(binary()) :: term()
- def from_binary(binary), do: :erlang.binary_to_term(binary)
-
- @spec from_binary_with_convert(binary()) :: any()
- def from_binary_with_convert(binary) do
- binary
- |> from_binary()
- |> do_convert()
+ @spec to_json_types(term()) :: map() | list() | boolean() | String.t()
+ def to_json_types(entity) when is_list(entity) do
+ Enum.map(entity, &to_json_types/1)
end
- @spec from_string(String.t()) :: atom() | no_return()
- def from_string(string), do: do_transform_string(string)
+ def to_json_types(%Regex{} = entity), do: inspect(entity)
- @spec convert(any()) :: any()
- def convert(entity), do: do_convert(entity)
-
- defp do_convert(entity) when is_list(entity) do
- for v <- entity, into: [], do: do_convert(v)
+ def to_json_types(entity) when is_map(entity) do
+ Map.new(entity, fn {k, v} -> {to_json_types(k), to_json_types(v)} end)
end
- defp do_convert(%Regex{} = entity), do: inspect(entity)
+ def to_json_types({:args, args}) when is_list(args) do
+ arguments =
+ Enum.map(args, fn
+ arg when is_tuple(arg) -> inspect(arg)
+ arg -> to_json_types(arg)
+ end)
- defp do_convert(entity) when is_map(entity) do
- for {k, v} <- entity, into: %{}, do: {do_convert(k), do_convert(v)}
+ %{"tuple" => [":args", arguments]}
end
- defp do_convert({:proxy_url, {type, :localhost, port}}) do
- %{"tuple" => [":proxy_url", %{"tuple" => [do_convert(type), "localhost", port]}]}
+ def to_json_types({:proxy_url, {type, :localhost, port}}) do
+ %{"tuple" => [":proxy_url", %{"tuple" => [to_json_types(type), "localhost", port]}]}
end
- defp do_convert({:proxy_url, {type, host, port}}) when is_tuple(host) do
+ def to_json_types({:proxy_url, {type, host, port}}) when is_tuple(host) do
ip =
host
|> :inet_parse.ntoa()
@@ -264,66 +227,64 @@ defmodule Pleroma.ConfigDB do
%{
"tuple" => [
":proxy_url",
- %{"tuple" => [do_convert(type), ip, port]}
+ %{"tuple" => [to_json_types(type), ip, port]}
]
}
end
- defp do_convert({:proxy_url, {type, host, port}}) do
+ def to_json_types({:proxy_url, {type, host, port}}) do
%{
"tuple" => [
":proxy_url",
- %{"tuple" => [do_convert(type), to_string(host), port]}
+ %{"tuple" => [to_json_types(type), to_string(host), port]}
]
}
end
- defp do_convert({:partial_chain, entity}), do: %{"tuple" => [":partial_chain", inspect(entity)]}
+ def to_json_types({:partial_chain, entity}),
+ do: %{"tuple" => [":partial_chain", inspect(entity)]}
- defp do_convert(entity) when is_tuple(entity) do
+ def to_json_types(entity) when is_tuple(entity) do
value =
entity
|> Tuple.to_list()
- |> do_convert()
+ |> to_json_types()
%{"tuple" => value}
end
- defp do_convert(entity) when is_boolean(entity) or is_number(entity) or is_nil(entity) do
+ def to_json_types(entity) when is_binary(entity), do: entity
+
+ def to_json_types(entity) when is_boolean(entity) or is_number(entity) or is_nil(entity) do
entity
end
- defp do_convert(entity)
- when is_atom(entity) and entity in [:"tlsv1.1", :"tlsv1.2", :"tlsv1.3"] do
+ def to_json_types(entity) when entity in [:"tlsv1.1", :"tlsv1.2", :"tlsv1.3"] do
":#{entity}"
end
- defp do_convert(entity) when is_atom(entity), do: inspect(entity)
+ def to_json_types(entity) when is_atom(entity), do: inspect(entity)
- defp do_convert(entity) when is_binary(entity), do: entity
+ @spec to_elixir_types(boolean() | String.t() | map() | list()) :: term()
+ def to_elixir_types(%{"tuple" => [":args", args]}) when is_list(args) do
+ arguments =
+ Enum.map(args, fn arg ->
+ if String.contains?(arg, ["{", "}"]) do
+ {elem, []} = Code.eval_string(arg)
+ elem
+ else
+ to_elixir_types(arg)
+ end
+ end)
- @spec transform(any()) :: binary() | no_return()
- def transform(entity) when is_binary(entity) or is_map(entity) or is_list(entity) do
- entity
- |> do_transform()
- |> to_binary()
+ {:args, arguments}
end
- def transform(entity), do: to_binary(entity)
-
- @spec transform_with_out_binary(any()) :: any()
- def transform_with_out_binary(entity), do: do_transform(entity)
-
- @spec to_binary(any()) :: binary()
- def to_binary(entity), do: :erlang.term_to_binary(entity)
-
- defp do_transform(%Regex{} = entity), do: entity
-
- defp do_transform(%{"tuple" => [":proxy_url", %{"tuple" => [type, host, port]}]}) do
- {:proxy_url, {do_transform_string(type), parse_host(host), port}}
+ def to_elixir_types(%{"tuple" => [":proxy_url", %{"tuple" => [type, host, port]}]}) do
+ {:proxy_url, {string_to_elixir_types(type), parse_host(host), port}}
end
- defp do_transform(%{"tuple" => [":partial_chain", entity]}) do
+ def to_elixir_types(%{"tuple" => [":partial_chain", entity]}) do
{partial_chain, []} =
entity
|> String.replace(~r/[^\w|^{:,[|^,|^[|^\]^}|^\/|^\.|^"]^\s/, "")
@@ -332,25 +293,51 @@ defmodule Pleroma.ConfigDB do
{:partial_chain, partial_chain}
end
- defp do_transform(%{"tuple" => entity}) do
- Enum.reduce(entity, {}, fn val, acc -> Tuple.append(acc, do_transform(val)) end)
+ def to_elixir_types(%{"tuple" => entity}) do
+ Enum.reduce(entity, {}, &Tuple.append(&2, to_elixir_types(&1)))
end
- defp do_transform(entity) when is_map(entity) do
- for {k, v} <- entity, into: %{}, do: {do_transform(k), do_transform(v)}
+ def to_elixir_types(entity) when is_map(entity) do
+ Map.new(entity, fn {k, v} -> {to_elixir_types(k), to_elixir_types(v)} end)
end
- defp do_transform(entity) when is_list(entity) do
- for v <- entity, into: [], do: do_transform(v)
+ def to_elixir_types(entity) when is_list(entity) do
+ Enum.map(entity, &to_elixir_types/1)
end
- defp do_transform(entity) when is_binary(entity) do
+ def to_elixir_types(entity) when is_binary(entity) do
entity
|> String.trim()
- |> do_transform_string()
+ |> string_to_elixir_types()
end
- defp do_transform(entity), do: entity
+ def to_elixir_types(entity), do: entity
+
+ @spec string_to_elixir_types(String.t()) ::
+ atom() | Regex.t() | module() | String.t() | no_return()
+ def string_to_elixir_types("~r" <> _pattern = regex) do
+ pattern =
+ ~r/^~r(?'delimiter'[\/|"'([{<]{1})(?'pattern'.+)[\/|"')\]}>]{1}(?'modifier'[uismxfU]*)/u
+
+ delimiters = ["/", "|", "\"", "'", {"(", ")"}, {"[", "]"}, {"{", "}"}, {"<", ">"}]
+
+ with %{"modifier" => modifier, "pattern" => pattern, "delimiter" => regex_delimiter} <-
+ Regex.named_captures(pattern, regex),
+ {:ok, {leading, closing}} <- find_valid_delimiter(delimiters, pattern, regex_delimiter),
+ {result, _} <- Code.eval_string("~r#{leading}#{pattern}#{closing}#{modifier}") do
+ result
+ end
+ end
+
+ def string_to_elixir_types(":" <> atom), do: String.to_atom(atom)
+
+ def string_to_elixir_types(value) do
+ if module_name?(value) do
+ String.to_existing_atom("Elixir." <> value)
+ else
+ value
+ end
+ end
defp parse_host("localhost"), do: :localhost
@@ -387,27 +374,8 @@ defmodule Pleroma.ConfigDB do
end
end
- defp do_transform_string("~r" <> _pattern = regex) do
- with %{"modifier" => modifier, "pattern" => pattern, "delimiter" => regex_delimiter} <-
- Regex.named_captures(@regex, regex),
- {:ok, {leading, closing}} <- find_valid_delimiter(@delimiters, pattern, regex_delimiter),
- {result, _} <- Code.eval_string("~r#{leading}#{pattern}#{closing}#{modifier}") do
- result
- end
- end
-
- defp do_transform_string(":" <> atom), do: String.to_atom(atom)
-
- defp do_transform_string(value) do
- if is_module_name?(value) do
- String.to_existing_atom("Elixir." <> value)
- else
- value
- end
- end
-
- @spec is_module_name?(String.t()) :: boolean()
- def is_module_name?(string) do
+ @spec module_name?(String.t()) :: boolean()
+ def module_name?(string) do
Regex.match?(~r/^(Pleroma|Phoenix|Tesla|Quack|Ueberauth|Swoosh)\./, string) or
string in ["Oban", "Ueberauth", "ExSyslogger"]
end
diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex
index b68ded01f..0f52eb210 100644
--- a/lib/pleroma/config/deprecation_warnings.ex
+++ b/lib/pleroma/config/deprecation_warnings.ex
@@ -3,9 +3,23 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Config.DeprecationWarnings do
+ alias Pleroma.Config
+
require Logger
alias Pleroma.Config
+ @type config_namespace() :: [atom()]
+ @type config_map() :: {config_namespace(), config_namespace(), String.t()}
+
+ @mrf_config_map [
+ {[:instance, :rewrite_policy], [:mrf, :policies],
+ "\n* `config :pleroma, :instance, rewrite_policy` is now `config :pleroma, :mrf, policies`"},
+ {[:instance, :mrf_transparency], [:mrf, :transparency],
+ "\n* `config :pleroma, :instance, mrf_transparency` is now `config :pleroma, :mrf, transparency`"},
+ {[:instance, :mrf_transparency_exclusions], [:mrf, :transparency_exclusions],
+ "\n* `config :pleroma, :instance, mrf_transparency_exclusions` is now `config :pleroma, :mrf, transparency_exclusions`"}
+ ]
+
def check_hellthread_threshold do
if Config.get([:mrf_hellthread, :threshold]) do
Logger.warn("""
@@ -39,5 +53,66 @@ defmodule Pleroma.Config.DeprecationWarnings do
def warn do
check_hellthread_threshold()
mrf_user_allowlist()
+ check_old_mrf_config()
+ check_media_proxy_whitelist_config()
+ check_welcome_message_config()
+ end
+
+ def check_welcome_message_config do
+ instance_config = Pleroma.Config.get([:instance])
+
+ use_old_config =
+ Keyword.has_key?(instance_config, :welcome_user_nickname) or
+ Keyword.has_key?(instance_config, :welcome_message)
+
+ if use_old_config do
+ Logger.error("""
+ !!!DEPRECATION WARNING!!!
+ Your config is using the old namespace for Welcome messages configuration. You need to change to the new namespace:
+ \n* `config :pleroma, :instance, welcome_user_nickname` is now `config :pleroma, :welcome, :direct_message, :sender_nickname`
+ \n* `config :pleroma, :instance, welcome_message` is now `config :pleroma, :welcome, :direct_message, :message`
+ """)
+ end
+ end
+
+ def check_old_mrf_config do
+ warning_preface = """
+ !!!DEPRECATION WARNING!!!
+ Your config is using old namespaces for MRF configuration. They should work for now, but you are advised to change to new namespaces to prevent possible issues later:
+ """
+
+ move_namespace_and_warn(@mrf_config_map, warning_preface)
+ end
+
+ @spec move_namespace_and_warn([config_map()], String.t()) :: :ok | nil
+ def move_namespace_and_warn(config_map, warning_preface) do
+ warning =
+ Enum.reduce(config_map, "", fn
+ {old, new, err_msg}, acc ->
+ old_config = Config.get(old)
+
+ if old_config do
+ Config.put(new, old_config)
+ acc <> err_msg
+ else
+ acc
+ end
+ end)
+
+ if warning != "" do
+ Logger.warn(warning_preface <> warning)
+ end
+ end
+
+ @spec check_media_proxy_whitelist_config() :: :ok | nil
+ def check_media_proxy_whitelist_config do
+ whitelist = Config.get([:media_proxy, :whitelist])
+
+ if Enum.any?(whitelist, &(not String.starts_with?(&1, "http"))) do
+ Logger.warn("""
+ !!!DEPRECATION WARNING!!!
+ Your config is using old format (only domain) for MediaProxy whitelist option. Setting should work for now, but you are advised to change format to scheme with port to prevent possible issues later.
+ """)
+ end
end
end
diff --git a/lib/pleroma/config/helpers.ex b/lib/pleroma/config/helpers.ex
new file mode 100644
index 000000000..3dce40ea0
--- /dev/null
+++ b/lib/pleroma/config/helpers.ex
@@ -0,0 +1,17 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Config.Helpers do
+ alias Pleroma.Config
+
+ def instance_name, do: Config.get([:instance, :name])
+
+ defp instance_notify_email do
+ Config.get([:instance, :notify_email]) || Config.get([:instance, :email])
+ end
+
+ def sender do
+ {instance_name(), instance_notify_email()}
+ end
+end
diff --git a/lib/pleroma/config/loader.ex b/lib/pleroma/config/loader.ex
index 6ca6550bd..64e7de6df 100644
--- a/lib/pleroma/config/loader.ex
+++ b/lib/pleroma/config/loader.ex
@@ -12,6 +12,11 @@ defmodule Pleroma.Config.Loader do
:swarm
]
+ @reject_groups [
+ :postgrex,
+ :tesla
+ ]
+
if Code.ensure_loaded?(Config.Reader) do
@reader Config.Reader
@@ -47,7 +52,8 @@ defmodule Pleroma.Config.Loader do
@spec filter_group(atom(), keyword()) :: keyword()
def filter_group(group, configs) do
Enum.reject(configs[group], fn {key, _v} ->
- key in @reject_keys or (group == :phoenix and key == :serve_endpoints)
+ key in @reject_keys or group in @reject_groups or
+ (group == :phoenix and key == :serve_endpoints)
end)
end
end
diff --git a/lib/pleroma/config/transfer_task.ex b/lib/pleroma/config/transfer_task.ex
index 7c3449b5e..a0d7b7d71 100644
--- a/lib/pleroma/config/transfer_task.ex
+++ b/lib/pleroma/config/transfer_task.ex
@@ -5,6 +5,7 @@
defmodule Pleroma.Config.TransferTask do
use Task
+ alias Pleroma.Config
alias Pleroma.ConfigDB
alias Pleroma.Repo
@@ -18,59 +19,56 @@ defmodule Pleroma.Config.TransferTask do
{:pleroma, Oban},
{:pleroma, :rate_limit},
{:pleroma, :markup},
- {:plerome, :streamer}
+ {:pleroma, :streamer},
+ {:pleroma, :pools},
+ {:pleroma, :connections_pool}
]
@reboot_time_subkeys [
{:pleroma, Pleroma.Captcha, [:seconds_valid]},
{:pleroma, Pleroma.Upload, [:proxy_remote]},
{:pleroma, :instance, [:upload_limit]},
- {:pleroma, :email_notifications, [:digest]},
- {:pleroma, :oauth2, [:clean_expired_tokens]},
- {:pleroma, Pleroma.ActivityExpiration, [:enabled]},
- {:pleroma, Pleroma.ScheduledActivity, [:enabled]},
{:pleroma, :gopher, [:enabled]}
]
- @reject [nil, :prometheus]
-
- def start_link(_) do
- load_and_update_env()
- if Pleroma.Config.get(:env) == :test, do: Ecto.Adapters.SQL.Sandbox.checkin(Repo)
+ def start_link(restart_pleroma? \\ true) do
+ load_and_update_env([], restart_pleroma?)
+ if Config.get(:env) == :test, do: Ecto.Adapters.SQL.Sandbox.checkin(Repo)
:ignore
end
- @spec load_and_update_env([ConfigDB.t()]) :: :ok | false
- def load_and_update_env(deleted \\ [], restart_pleroma? \\ true) do
- with {:configurable, true} <-
- {:configurable, Pleroma.Config.get(:configurable_from_database)},
- true <- Ecto.Adapters.SQL.table_exists?(Repo, "config"),
- started_applications <- Application.started_applications() do
+ @spec load_and_update_env([ConfigDB.t()], boolean()) :: :ok
+ def load_and_update_env(deleted_settings \\ [], restart_pleroma? \\ true) do
+ with {_, true} <- {:configurable, Config.get(:configurable_from_database)} do
# We need to restart applications for loaded settings take effect
- in_db = Repo.all(ConfigDB)
+ {logger, other} =
+ (Repo.all(ConfigDB) ++ deleted_settings)
+ |> Enum.map(&merge_with_default/1)
+ |> Enum.split_with(fn {group, _, _, _} -> group in [:logger, :quack] end)
- with_deleted = in_db ++ deleted
+ logger
+ |> Enum.sort()
+ |> Enum.each(&configure/1)
- reject_for_restart = if restart_pleroma?, do: @reject, else: [:pleroma | @reject]
+ started_applications = Application.started_applications()
- applications =
- with_deleted
- |> Enum.map(&merge_and_update(&1))
- |> Enum.uniq()
- # TODO: some problem with prometheus after restart!
- |> Enum.reject(&(&1 in reject_for_restart))
+ # TODO: some problem with prometheus after restart!
+ reject = [nil, :prometheus, :postgrex]
- # to be ensured that pleroma will be restarted last
- applications =
- if :pleroma in applications do
- List.delete(applications, :pleroma) ++ [:pleroma]
+ reject =
+ if restart_pleroma? do
+ reject
else
- Restarter.Pleroma.rebooted()
- applications
+ [:pleroma | reject]
end
- Enum.each(applications, &restart(started_applications, &1, Pleroma.Config.get(:env)))
+ other
+ |> Enum.map(&update/1)
+ |> Enum.uniq()
+ |> Enum.reject(&(&1 in reject))
+ |> maybe_set_pleroma_last()
+ |> Enum.each(&restart(started_applications, &1, Config.get(:env)))
:ok
else
@@ -78,51 +76,79 @@ defmodule Pleroma.Config.TransferTask do
end
end
- defp merge_and_update(setting) do
- try do
- key = ConfigDB.from_string(setting.key)
- group = ConfigDB.from_string(setting.group)
+ defp maybe_set_pleroma_last(apps) do
+ # to be ensured that pleroma will be restarted last
+ if :pleroma in apps do
+ apps
+ |> List.delete(:pleroma)
+ |> List.insert_at(-1, :pleroma)
+ else
+ Restarter.Pleroma.rebooted()
+ apps
+ end
+ end
- default = Pleroma.Config.Holder.default_config(group, key)
- value = ConfigDB.from_binary(setting.value)
+ defp merge_with_default(%{group: group, key: key, value: value} = setting) do
+ default = Config.Holder.default_config(group, key)
- merged_value =
- if Ecto.get_meta(setting, :state) == :deleted do
- default
- else
- if can_be_merged?(default, value) do
- ConfigDB.merge_group(group, key, default, value)
- else
- value
- end
- end
+ merged =
+ cond do
+ Ecto.get_meta(setting, :state) == :deleted -> default
+ can_be_merged?(default, value) -> ConfigDB.merge_group(group, key, default, value)
+ true -> value
+ end
- :ok = update_env(group, key, merged_value)
+ {group, key, value, merged}
+ end
- if group != :logger do
- if group != :pleroma or pleroma_need_restart?(group, key, value) do
- group
- end
- else
- # change logger configuration in runtime, without restart
- if Keyword.keyword?(merged_value) and
- key not in [:compile_time_application, :backends, :compile_time_purge_matching] do
- Logger.configure_backend(key, merged_value)
- else
- Logger.configure([{key, merged_value}])
- end
+ # change logger configuration in runtime, without restart
+ defp configure({:quack, key, _, merged}) do
+ Logger.configure_backend(Quack.Logger, [{key, merged}])
+ :ok = update_env(:quack, key, merged)
+ end
- nil
+ defp configure({_, :backends, _, merged}) do
+ # removing current backends
+ Enum.each(Application.get_env(:logger, :backends), &Logger.remove_backend/1)
+
+ Enum.each(merged, &Logger.add_backend/1)
+
+ :ok = update_env(:logger, :backends, merged)
+ end
+
+ defp configure({_, key, _, merged}) when key in [:console, :ex_syslogger] do
+ merged =
+ if key == :console do
+ put_in(merged[:format], merged[:format] <> "\n")
+ else
+ merged
end
+
+ backend =
+ if key == :ex_syslogger,
+ do: {ExSyslogger, :ex_syslogger},
+ else: key
+
+ Logger.configure_backend(backend, merged)
+ :ok = update_env(:logger, key, merged)
+ end
+
+ defp configure({_, key, _, merged}) do
+ Logger.configure([{key, merged}])
+ :ok = update_env(:logger, key, merged)
+ end
+
+ defp update({group, key, value, merged}) do
+ try do
+ :ok = update_env(group, key, merged)
+
+ if group != :pleroma or pleroma_need_restart?(group, key, value), do: group
rescue
error ->
error_msg =
- "updating env causes error, group: " <>
- inspect(setting.group) <>
- " key: " <>
- inspect(setting.key) <>
- " value: " <>
- inspect(ConfigDB.from_binary(setting.value)) <> " error: " <> inspect(error)
+ "updating env causes error, group: #{inspect(group)}, key: #{inspect(key)}, value: #{
+ inspect(value)
+ } error: #{inspect(error)}"
Logger.warn(error_msg)
@@ -130,6 +156,9 @@ defmodule Pleroma.Config.TransferTask do
end
end
+ defp update_env(group, key, nil), do: Application.delete_env(group, key)
+ defp update_env(group, key, value), do: Application.put_env(group, key, value)
+
@spec pleroma_need_restart?(atom(), atom(), any()) :: boolean()
def pleroma_need_restart?(group, key, value) do
group_and_key_need_reboot?(group, key) or group_and_subkey_need_reboot?(group, key, value)
@@ -147,9 +176,6 @@ defmodule Pleroma.Config.TransferTask do
end)
end
- defp update_env(group, key, nil), do: Application.delete_env(group, key)
- defp update_env(group, key, value), do: Application.put_env(group, key, value)
-
defp restart(_, :pleroma, env), do: Restarter.Pleroma.restart_after_boot(env)
defp restart(started_applications, app, _) do
diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex
index 3a9eec5ea..13eeaa96b 100644
--- a/lib/pleroma/constants.ex
+++ b/lib/pleroma/constants.ex
@@ -17,12 +17,13 @@ defmodule Pleroma.Constants do
"announcement_count",
"emoji",
"context_id",
- "deleted_activity_id"
+ "deleted_activity_id",
+ "pleroma_internal"
]
)
const(static_only_files,
do:
- ~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc)
+ ~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc embed.js embed.css)
)
end
diff --git a/lib/pleroma/conversation.ex b/lib/pleroma/conversation.ex
index 37d455cfc..e76eb0087 100644
--- a/lib/pleroma/conversation.ex
+++ b/lib/pleroma/conversation.ex
@@ -63,7 +63,7 @@ defmodule Pleroma.Conversation do
ap_id when is_binary(ap_id) and byte_size(ap_id) > 0 <- object.data["context"] do
{:ok, conversation} = create_for_ap_id(ap_id)
- users = User.get_users_from_set(activity.recipients, false)
+ users = User.get_users_from_set(activity.recipients, local_only: false)
participations =
Enum.map(users, fn user ->
diff --git a/lib/pleroma/conversation/participation.ex b/lib/pleroma/conversation/participation.ex
index 693825cf5..8bc3e85d6 100644
--- a/lib/pleroma/conversation/participation.ex
+++ b/lib/pleroma/conversation/participation.ex
@@ -128,22 +128,19 @@ defmodule Pleroma.Conversation.Participation do
|> Pleroma.Pagination.fetch_paginated(params)
end
- def restrict_recipients(query, user, %{"recipients" => user_ids}) do
- user_ids =
+ def restrict_recipients(query, user, %{recipients: user_ids}) do
+ user_binary_ids =
[user.id | user_ids]
|> Enum.uniq()
- |> Enum.reduce([], fn user_id, acc ->
- {:ok, user_id} = FlakeId.Ecto.CompatType.dump(user_id)
- [user_id | acc]
- end)
+ |> User.binary_id()
conversation_subquery =
__MODULE__
|> group_by([p], p.conversation_id)
|> having(
[p],
- count(p.user_id) == ^length(user_ids) and
- fragment("array_agg(?) @> ?", p.user_id, ^user_ids)
+ count(p.user_id) == ^length(user_binary_ids) and
+ fragment("array_agg(?) @> ?", p.user_id, ^user_binary_ids)
)
|> select([p], %{id: p.conversation_id})
@@ -165,17 +162,20 @@ defmodule Pleroma.Conversation.Participation do
for_user(user, params)
|> Enum.map(fn participation ->
activity_id =
- ActivityPub.fetch_latest_activity_id_for_context(participation.conversation.ap_id, %{
- "user" => user,
- "blocking_user" => user
- })
+ ActivityPub.fetch_latest_direct_activity_id_for_context(
+ participation.conversation.ap_id,
+ %{
+ user: user,
+ blocking_user: user
+ }
+ )
%{
participation
| last_activity_id: activity_id
}
end)
- |> Enum.filter(& &1.last_activity_id)
+ |> Enum.reject(&is_nil(&1.last_activity_id))
end
def get(_, _ \\ [])
diff --git a/lib/pleroma/counter_cache.ex b/lib/pleroma/counter_cache.ex
index 4d348a413..ebd1f603d 100644
--- a/lib/pleroma/counter_cache.ex
+++ b/lib/pleroma/counter_cache.ex
@@ -10,32 +10,70 @@ defmodule Pleroma.CounterCache do
import Ecto.Query
schema "counter_cache" do
- field(:name, :string)
- field(:count, :integer)
+ field(:instance, :string)
+ field(:public, :integer)
+ field(:unlisted, :integer)
+ field(:private, :integer)
+ field(:direct, :integer)
end
def changeset(struct, params) do
struct
- |> cast(params, [:name, :count])
- |> validate_required([:name])
- |> unique_constraint(:name)
+ |> cast(params, [:instance, :public, :unlisted, :private, :direct])
+ |> validate_required([:instance])
+ |> unique_constraint(:instance)
end
- def get_as_map(names) when is_list(names) do
+ def get_by_instance(instance) do
CounterCache
- |> where([cc], cc.name in ^names)
- |> Repo.all()
- |> Enum.group_by(& &1.name, & &1.count)
- |> Map.new(fn {k, v} -> {k, hd(v)} end)
+ |> select([c], %{
+ "public" => c.public,
+ "unlisted" => c.unlisted,
+ "private" => c.private,
+ "direct" => c.direct
+ })
+ |> where([c], c.instance == ^instance)
+ |> Repo.one()
+ |> case do
+ nil -> %{"public" => 0, "unlisted" => 0, "private" => 0, "direct" => 0}
+ val -> val
+ end
end
- def set(name, count) do
+ def get_sum do
+ CounterCache
+ |> select([c], %{
+ "public" => type(sum(c.public), :integer),
+ "unlisted" => type(sum(c.unlisted), :integer),
+ "private" => type(sum(c.private), :integer),
+ "direct" => type(sum(c.direct), :integer)
+ })
+ |> Repo.one()
+ end
+
+ def set(instance, values) do
+ params =
+ Enum.reduce(
+ ["public", "private", "unlisted", "direct"],
+ %{"instance" => instance},
+ fn param, acc ->
+ Map.put_new(acc, param, Map.get(values, param, 0))
+ end
+ )
+
%CounterCache{}
- |> changeset(%{"name" => name, "count" => count})
+ |> changeset(params)
|> Repo.insert(
- on_conflict: [set: [count: count]],
+ on_conflict: [
+ set: [
+ public: params["public"],
+ private: params["private"],
+ unlisted: params["unlisted"],
+ direct: params["direct"]
+ ]
+ ],
returning: true,
- conflict_target: :name
+ conflict_target: :instance
)
end
end
diff --git a/lib/pleroma/docs/generator.ex b/lib/pleroma/docs/generator.ex
index e0fc8cd02..a671a6278 100644
--- a/lib/pleroma/docs/generator.ex
+++ b/lib/pleroma/docs/generator.ex
@@ -6,16 +6,21 @@ defmodule Pleroma.Docs.Generator do
implementation.process(descriptions)
end
- @spec list_modules_in_dir(String.t(), String.t()) :: [module()]
- def list_modules_in_dir(dir, start) do
- with {:ok, files} <- File.ls(dir) do
- files
- |> Enum.filter(&String.ends_with?(&1, ".ex"))
- |> Enum.map(fn filename ->
- module = filename |> String.trim_trailing(".ex") |> Macro.camelize()
- String.to_atom(start <> module)
- end)
- end
+ @spec list_behaviour_implementations(behaviour :: module()) :: [module()]
+ def list_behaviour_implementations(behaviour) do
+ :code.all_loaded()
+ |> Enum.filter(fn {module, _} ->
+ # This shouldn't be needed as all modules are expected to have module_info/1,
+ # but in test enviroments some transient modules `:elixir_compiler_XX`
+ # are loaded for some reason (where XX is a random integer).
+ if function_exported?(module, :module_info, 1) do
+ module.module_info(:attributes)
+ |> Keyword.get_values(:behaviour)
+ |> List.flatten()
+ |> Enum.member?(behaviour)
+ end
+ end)
+ |> Enum.map(fn {module, _} -> module end)
end
@doc """
@@ -87,6 +92,12 @@ defmodule Pleroma.Docs.Generator do
else: string
end
+ defp format_suggestions({:list_behaviour_implementations, behaviour}) do
+ behaviour
+ |> list_behaviour_implementations()
+ |> format_suggestions()
+ end
+
defp format_suggestions([]), do: []
defp format_suggestions([suggestion | tail]) do
diff --git a/lib/pleroma/docs/json.ex b/lib/pleroma/docs/json.ex
index 74f8b2615..feeb4320e 100644
--- a/lib/pleroma/docs/json.ex
+++ b/lib/pleroma/docs/json.ex
@@ -1,5 +1,19 @@
defmodule Pleroma.Docs.JSON do
@behaviour Pleroma.Docs.Generator
+ @external_resource "config/description.exs"
+ @raw_config Pleroma.Config.Loader.read("config/description.exs")
+ @raw_descriptions @raw_config[:pleroma][:config_description]
+ @term __MODULE__.Compiled
+
+ @spec compile :: :ok
+ def compile do
+ :persistent_term.put(@term, Pleroma.Docs.Generator.convert_to_strings(@raw_descriptions))
+ end
+
+ @spec compiled_descriptions :: Map.t()
+ def compiled_descriptions do
+ :persistent_term.get(@term)
+ end
@spec process(keyword()) :: {:ok, String.t()}
def process(descriptions) do
@@ -13,12 +27,4 @@ defmodule Pleroma.Docs.JSON do
{:ok, path}
end
end
-
- def compile do
- with config <- Pleroma.Config.Loader.read("config/description.exs") do
- config[:pleroma][:config_description]
- |> Pleroma.Docs.Generator.convert_to_strings()
- |> Jason.encode!()
- end
- end
end
diff --git a/lib/pleroma/docs/markdown.ex b/lib/pleroma/docs/markdown.ex
index 68b106499..da3f20f43 100644
--- a/lib/pleroma/docs/markdown.ex
+++ b/lib/pleroma/docs/markdown.ex
@@ -68,6 +68,11 @@ defmodule Pleroma.Docs.Markdown do
IO.write(file, " #{list_mark}`#{inspect(suggestion)}`\n")
end
+ defp print_suggestions(file, {:list_behaviour_implementations, behaviour}) do
+ suggestions = Pleroma.Docs.Generator.list_behaviour_implementations(behaviour)
+ print_suggestions(file, suggestions)
+ end
+
defp print_suggestions(_file, nil), do: nil
defp print_suggestions(_file, ""), do: nil
diff --git a/lib/pleroma/ecto_type/activity_pub/object_validators/date_time.ex b/lib/pleroma/ecto_type/activity_pub/object_validators/date_time.ex
new file mode 100644
index 000000000..d852c0abd
--- /dev/null
+++ b/lib/pleroma/ecto_type/activity_pub/object_validators/date_time.ex
@@ -0,0 +1,38 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.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/ecto_type/activity_pub/object_validators/object_id.ex b/lib/pleroma/ecto_type/activity_pub/object_validators/object_id.ex
new file mode 100644
index 000000000..8034235b0
--- /dev/null
+++ b/lib/pleroma/ecto_type/activity_pub/object_validators/object_id.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.EctoType.ActivityPub.ObjectValidators.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/ecto_type/activity_pub/object_validators/recipients.ex b/lib/pleroma/ecto_type/activity_pub/object_validators/recipients.ex
new file mode 100644
index 000000000..205527a96
--- /dev/null
+++ b/lib/pleroma/ecto_type/activity_pub/object_validators/recipients.ex
@@ -0,0 +1,40 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.Recipients do
+ use Ecto.Type
+
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators.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_while({:ok, []}, fn element, {:ok, list} ->
+ case ObjectID.cast(element) do
+ {:ok, id} ->
+ {:cont, {:ok, [id | list]}}
+
+ _ ->
+ {:halt, :error}
+ 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/ecto_type/activity_pub/object_validators/safe_text.ex b/lib/pleroma/ecto_type/activity_pub/object_validators/safe_text.ex
new file mode 100644
index 000000000..7f0405c7b
--- /dev/null
+++ b/lib/pleroma/ecto_type/activity_pub/object_validators/safe_text.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.EctoType.ActivityPub.ObjectValidators.SafeText do
+ use Ecto.Type
+
+ alias Pleroma.HTML
+
+ def type, do: :string
+
+ def cast(str) when is_binary(str) do
+ {:ok, HTML.filter_tags(str)}
+ 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/ecto_type/activity_pub/object_validators/uri.ex b/lib/pleroma/ecto_type/activity_pub/object_validators/uri.ex
new file mode 100644
index 000000000..2054c26be
--- /dev/null
+++ b/lib/pleroma/ecto_type/activity_pub/object_validators/uri.ex
@@ -0,0 +1,24 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.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/ecto_type/config/atom.ex b/lib/pleroma/ecto_type/config/atom.ex
new file mode 100644
index 000000000..df565d432
--- /dev/null
+++ b/lib/pleroma/ecto_type/config/atom.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.EctoType.Config.Atom do
+ use Ecto.Type
+
+ def type, do: :atom
+
+ def cast(key) when is_atom(key) do
+ {:ok, key}
+ end
+
+ def cast(key) when is_binary(key) do
+ {:ok, Pleroma.ConfigDB.string_to_elixir_types(key)}
+ end
+
+ def cast(_), do: :error
+
+ def load(key) do
+ {:ok, Pleroma.ConfigDB.string_to_elixir_types(key)}
+ end
+
+ def dump(key) when is_atom(key), do: {:ok, inspect(key)}
+ def dump(_), do: :error
+end
diff --git a/lib/pleroma/ecto_type/config/binary_value.ex b/lib/pleroma/ecto_type/config/binary_value.ex
new file mode 100644
index 000000000..bbd2608c5
--- /dev/null
+++ b/lib/pleroma/ecto_type/config/binary_value.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.EctoType.Config.BinaryValue do
+ use Ecto.Type
+
+ def type, do: :term
+
+ def cast(value) when is_binary(value) do
+ if String.valid?(value) do
+ {:ok, value}
+ else
+ {:ok, :erlang.binary_to_term(value)}
+ end
+ end
+
+ def cast(value), do: {:ok, value}
+
+ def load(value) when is_binary(value) do
+ {:ok, :erlang.binary_to_term(value)}
+ end
+
+ def dump(value) do
+ {:ok, :erlang.term_to_binary(value)}
+ end
+end
diff --git a/lib/pleroma/emails/admin_email.ex b/lib/pleroma/emails/admin_email.ex
index 55f61024e..c27ad1065 100644
--- a/lib/pleroma/emails/admin_email.ex
+++ b/lib/pleroma/emails/admin_email.ex
@@ -8,9 +8,10 @@ defmodule Pleroma.Emails.AdminEmail do
import Swoosh.Email
alias Pleroma.Config
+ alias Pleroma.HTML
alias Pleroma.Web.Router.Helpers
- defp instance_config, do: Pleroma.Config.get(:instance)
+ defp instance_config, do: Config.get(:instance)
defp instance_name, do: instance_config()[:name]
defp instance_notify_email do
@@ -72,6 +73,8 @@ defmodule Pleroma.Emails.AdminEmail do
<p>Reported Account: <a href="#{user_url(account)}">#{account.nickname}</a></p>
#{comment_html}
#{statuses_html}
+ <p>
+ <a href="#{Pleroma.Web.base_url()}/pleroma/admin/#/reports/index">View Reports in AdminFE</a>
"""
new()
@@ -80,4 +83,18 @@ defmodule Pleroma.Emails.AdminEmail do
|> subject("#{instance_name()} Report")
|> html_body(html_body)
end
+
+ def new_unapproved_registration(to, account) do
+ html_body = """
+ <p>New account for review: <a href="#{user_url(account)}">@#{account.nickname}</a></p>
+ <blockquote>#{HTML.strip_tags(account.registration_reason)}</blockquote>
+ <a href="#{Pleroma.Web.base_url()}/pleroma/admin">Visit AdminFE</a>
+ """
+
+ new()
+ |> to({to.name, to.email})
+ |> from({instance_name(), instance_notify_email()})
+ |> subject("New account up for review on #{instance_name()} (@#{account.nickname})")
+ |> html_body(html_body)
+ end
end
diff --git a/lib/pleroma/emails/new_users_digest_email.ex b/lib/pleroma/emails/new_users_digest_email.ex
index 7d16b807f..348cbac9c 100644
--- a/lib/pleroma/emails/new_users_digest_email.ex
+++ b/lib/pleroma/emails/new_users_digest_email.ex
@@ -14,8 +14,10 @@ defmodule Pleroma.Emails.NewUsersDigestEmail do
styling = Pleroma.Config.get([Pleroma.Emails.UserEmail, :styling])
logo_url =
- Pleroma.Web.Endpoint.url() <>
- Pleroma.Config.get([:frontend_configurations, :pleroma_fe, :logo])
+ Pleroma.Helpers.UriHelper.maybe_add_base(
+ Pleroma.Config.get([:frontend_configurations, :pleroma_fe, :logo]),
+ Pleroma.Web.Endpoint.url()
+ )
new()
|> to({to.name, to.email})
diff --git a/lib/pleroma/emails/user_email.ex b/lib/pleroma/emails/user_email.ex
index dfadc10b3..1d8c72ae9 100644
--- a/lib/pleroma/emails/user_email.ex
+++ b/lib/pleroma/emails/user_email.ex
@@ -12,17 +12,22 @@ defmodule Pleroma.Emails.UserEmail do
alias Pleroma.Web.Endpoint
alias Pleroma.Web.Router
- defp instance_name, do: Config.get([:instance, :name])
-
- defp sender do
- email = Config.get([:instance, :notify_email]) || Config.get([:instance, :email])
- {instance_name(), email}
- end
+ import Pleroma.Config.Helpers, only: [instance_name: 0, sender: 0]
defp recipient(email, nil), do: email
defp recipient(email, name), do: {name, email}
defp recipient(%User{} = user), do: recipient(user.email, user.name)
+ @spec welcome(User.t(), map()) :: Swoosh.Email.t()
+ def welcome(user, opts \\ %{}) do
+ new()
+ |> to(recipient(user))
+ |> from(Map.get(opts, :sender, sender()))
+ |> subject(Map.get(opts, :subject, "Welcome to #{instance_name()}!"))
+ |> html_body(Map.get(opts, :html, "Welcome to #{instance_name()}!"))
+ |> text_body(Map.get(opts, :text, "Welcome to #{instance_name()}!"))
+ end
+
def password_reset_email(user, token) when is_binary(token) do
password_reset_url = Router.Helpers.reset_password_url(Endpoint, :reset, token)
@@ -102,25 +107,34 @@ defmodule Pleroma.Emails.UserEmail do
|> Enum.filter(&(&1.activity.data["type"] == "Create"))
|> Enum.map(fn notification ->
object = Pleroma.Object.normalize(notification.activity)
- object = update_in(object.data["content"], &format_links/1)
- %{
- data: notification,
- object: object,
- from: User.get_by_ap_id(notification.activity.actor)
- }
+ if not is_nil(object) do
+ object = update_in(object.data["content"], &format_links/1)
+
+ %{
+ data: notification,
+ object: object,
+ from: User.get_by_ap_id(notification.activity.actor)
+ }
+ end
end)
+ |> Enum.filter(& &1)
followers =
notifications
|> Enum.filter(&(&1.activity.data["type"] == "Follow"))
|> Enum.map(fn notification ->
- %{
- data: notification,
- object: Pleroma.Object.normalize(notification.activity),
- from: User.get_by_ap_id(notification.activity.actor)
- }
+ from = User.get_by_ap_id(notification.activity.actor)
+
+ if not is_nil(from) do
+ %{
+ data: notification,
+ object: Pleroma.Object.normalize(notification.activity),
+ from: User.get_by_ap_id(notification.activity.actor)
+ }
+ end
end)
+ |> Enum.filter(& &1)
unless Enum.empty?(mentions) do
styling = Config.get([__MODULE__, :styling])
diff --git a/lib/pleroma/emoji/formatter.ex b/lib/pleroma/emoji/formatter.ex
index 59ff2cac3..dc45b8a38 100644
--- a/lib/pleroma/emoji/formatter.ex
+++ b/lib/pleroma/emoji/formatter.ex
@@ -38,22 +38,14 @@ defmodule Pleroma.Emoji.Formatter do
def demojify(text, nil), do: text
- @doc "Outputs a list of the emoji-shortcodes in a text"
- def get_emoji(text) when is_binary(text) do
- Enum.filter(Emoji.get_all(), fn {emoji, %Emoji{}} ->
- String.contains?(text, ":#{emoji}:")
- end)
- end
-
- def get_emoji(_), do: []
-
@doc "Outputs a list of the emoji-Maps in a text"
def get_emoji_map(text) when is_binary(text) do
- get_emoji(text)
+ Emoji.get_all()
+ |> Enum.filter(fn {emoji, %Emoji{}} -> String.contains?(text, ":#{emoji}:") end)
|> Enum.reduce(%{}, fn {name, %Emoji{file: file}}, acc ->
Map.put(acc, name, "#{Pleroma.Web.Endpoint.static_url()}#{file}")
end)
end
- def get_emoji_map(_), do: []
+ def get_emoji_map(_), do: %{}
end
diff --git a/lib/pleroma/emoji/loader.ex b/lib/pleroma/emoji/loader.ex
index 3de2dc762..03a6bca0b 100644
--- a/lib/pleroma/emoji/loader.ex
+++ b/lib/pleroma/emoji/loader.ex
@@ -108,7 +108,7 @@ defmodule Pleroma.Emoji.Loader do
if File.exists?(emoji_txt) do
load_from_file(emoji_txt, emoji_groups)
else
- extensions = Pleroma.Config.get([:emoji, :pack_extensions])
+ extensions = Config.get([:emoji, :pack_extensions])
Logger.info(
"No emoji.txt found for pack \"#{pack_name}\", assuming all #{
diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex
new file mode 100644
index 000000000..d076ae312
--- /dev/null
+++ b/lib/pleroma/emoji/pack.ex
@@ -0,0 +1,572 @@
+defmodule Pleroma.Emoji.Pack do
+ @derive {Jason.Encoder, only: [:files, :pack, :files_count]}
+ defstruct files: %{},
+ files_count: 0,
+ pack_file: nil,
+ path: nil,
+ pack: %{},
+ name: nil
+
+ @type t() :: %__MODULE__{
+ files: %{String.t() => Path.t()},
+ files_count: non_neg_integer(),
+ pack_file: Path.t(),
+ path: Path.t(),
+ pack: map(),
+ name: String.t()
+ }
+
+ alias Pleroma.Emoji
+
+ @spec create(String.t()) :: {:ok, t()} | {:error, File.posix()} | {:error, :empty_values}
+ def create(name) do
+ with :ok <- validate_not_empty([name]),
+ dir <- Path.join(emoji_path(), name),
+ :ok <- File.mkdir(dir) do
+ %__MODULE__{pack_file: Path.join(dir, "pack.json")}
+ |> save_pack()
+ end
+ end
+
+ defp paginate(entities, 1, page_size), do: Enum.take(entities, page_size)
+
+ defp paginate(entities, page, page_size) do
+ entities
+ |> Enum.chunk_every(page_size)
+ |> Enum.at(page - 1)
+ end
+
+ @spec show(keyword()) :: {:ok, t()} | {:error, atom()}
+ def show(opts) do
+ name = opts[:name]
+
+ with :ok <- validate_not_empty([name]),
+ {:ok, pack} <- load_pack(name) do
+ shortcodes =
+ pack.files
+ |> Map.keys()
+ |> Enum.sort()
+ |> paginate(opts[:page], opts[:page_size])
+
+ pack = Map.put(pack, :files, Map.take(pack.files, shortcodes))
+
+ {:ok, validate_pack(pack)}
+ end
+ end
+
+ @spec delete(String.t()) ::
+ {:ok, [binary()]} | {:error, File.posix(), binary()} | {:error, :empty_values}
+ def delete(name) do
+ with :ok <- validate_not_empty([name]) do
+ emoji_path()
+ |> Path.join(name)
+ |> File.rm_rf()
+ end
+ end
+
+ @spec add_file(String.t(), String.t(), Path.t(), Plug.Upload.t() | String.t()) ::
+ {:ok, t()} | {:error, File.posix() | atom()}
+ def add_file(name, shortcode, filename, file) do
+ with :ok <- validate_not_empty([name, shortcode, filename]),
+ :ok <- validate_emoji_not_exists(shortcode),
+ {:ok, pack} <- load_pack(name),
+ :ok <- save_file(file, pack, filename),
+ {:ok, updated_pack} <- pack |> put_emoji(shortcode, filename) |> save_pack() do
+ Emoji.reload()
+ {:ok, updated_pack}
+ end
+ end
+
+ @spec delete_file(String.t(), String.t()) ::
+ {:ok, t()} | {:error, File.posix() | atom()}
+ def delete_file(name, shortcode) do
+ with :ok <- validate_not_empty([name, shortcode]),
+ {:ok, pack} <- load_pack(name),
+ :ok <- remove_file(pack, shortcode),
+ {:ok, updated_pack} <- pack |> delete_emoji(shortcode) |> save_pack() do
+ Emoji.reload()
+ {:ok, updated_pack}
+ end
+ end
+
+ @spec update_file(String.t(), String.t(), String.t(), String.t(), boolean()) ::
+ {:ok, t()} | {:error, File.posix() | atom()}
+ def update_file(name, shortcode, new_shortcode, new_filename, force) do
+ with :ok <- validate_not_empty([name, shortcode, new_shortcode, new_filename]),
+ {:ok, pack} <- load_pack(name),
+ {:ok, filename} <- get_filename(pack, shortcode),
+ :ok <- validate_emoji_not_exists(new_shortcode, force),
+ :ok <- rename_file(pack, filename, new_filename),
+ {:ok, updated_pack} <-
+ pack
+ |> delete_emoji(shortcode)
+ |> put_emoji(new_shortcode, new_filename)
+ |> save_pack() do
+ Emoji.reload()
+ {:ok, updated_pack}
+ end
+ end
+
+ @spec import_from_filesystem() :: {:ok, [String.t()]} | {:error, File.posix() | atom()}
+ def import_from_filesystem do
+ emoji_path = emoji_path()
+
+ with {:ok, %{access: :read_write}} <- File.stat(emoji_path),
+ {:ok, results} <- File.ls(emoji_path) do
+ names =
+ results
+ |> Enum.map(&Path.join(emoji_path, &1))
+ |> Enum.reject(fn path ->
+ File.dir?(path) and File.exists?(Path.join(path, "pack.json"))
+ end)
+ |> Enum.map(&write_pack_contents/1)
+ |> Enum.reject(&is_nil/1)
+
+ {:ok, names}
+ else
+ {:ok, %{access: _}} -> {:error, :no_read_write}
+ e -> e
+ end
+ end
+
+ @spec list_remote(String.t()) :: {:ok, map()} | {:error, atom()}
+ def list_remote(url) do
+ uri = url |> String.trim() |> URI.parse()
+
+ with :ok <- validate_shareable_packs_available(uri) do
+ uri
+ |> URI.merge("/api/pleroma/emoji/packs")
+ |> http_get()
+ end
+ end
+
+ @spec list_local(keyword()) :: {:ok, map(), non_neg_integer()}
+ def list_local(opts) do
+ with {:ok, results} <- list_packs_dir() do
+ all_packs =
+ results
+ |> Enum.map(fn name ->
+ case load_pack(name) do
+ {:ok, pack} -> pack
+ _ -> nil
+ end
+ end)
+ |> Enum.reject(&is_nil/1)
+
+ packs =
+ all_packs
+ |> paginate(opts[:page], opts[:page_size])
+ |> Map.new(fn pack -> {pack.name, validate_pack(pack)} end)
+
+ {:ok, packs, length(all_packs)}
+ end
+ end
+
+ @spec get_archive(String.t()) :: {:ok, binary()} | {:error, atom()}
+ def get_archive(name) do
+ with {:ok, pack} <- load_pack(name),
+ :ok <- validate_downloadable(pack) do
+ {:ok, fetch_archive(pack)}
+ end
+ end
+
+ @spec download(String.t(), String.t(), String.t()) :: {:ok, t()} | {:error, atom()}
+ def download(name, url, as) do
+ uri = url |> String.trim() |> URI.parse()
+
+ with :ok <- validate_shareable_packs_available(uri),
+ {:ok, remote_pack} <- uri |> URI.merge("/api/pleroma/emoji/packs/#{name}") |> http_get(),
+ {:ok, %{sha: sha, url: url} = pack_info} <- fetch_pack_info(remote_pack, uri, name),
+ {:ok, archive} <- download_archive(url, sha),
+ pack <- copy_as(remote_pack, as || name),
+ {:ok, _} = unzip(archive, pack_info, remote_pack, pack) do
+ # Fallback can't contain a pack.json file, since that would cause the fallback-src-sha256
+ # in it to depend on itself
+ if pack_info[:fallback] do
+ save_pack(pack)
+ else
+ {:ok, pack}
+ end
+ end
+ end
+
+ @spec save_metadata(map(), t()) :: {:ok, t()} | {:error, File.posix()}
+ def save_metadata(metadata, %__MODULE__{} = pack) do
+ pack
+ |> Map.put(:pack, metadata)
+ |> save_pack()
+ end
+
+ @spec update_metadata(String.t(), map()) :: {:ok, t()} | {:error, File.posix()}
+ def update_metadata(name, data) do
+ with {:ok, pack} <- load_pack(name) do
+ if fallback_sha_changed?(pack, data) do
+ update_sha_and_save_metadata(pack, data)
+ else
+ save_metadata(data, pack)
+ end
+ end
+ end
+
+ @spec load_pack(String.t()) :: {:ok, t()} | {:error, :not_found}
+ def load_pack(name) do
+ pack_file = Path.join([emoji_path(), name, "pack.json"])
+
+ if File.exists?(pack_file) do
+ pack =
+ pack_file
+ |> File.read!()
+ |> from_json()
+ |> Map.put(:pack_file, pack_file)
+ |> Map.put(:path, Path.dirname(pack_file))
+ |> Map.put(:name, name)
+
+ files_count =
+ pack.files
+ |> Map.keys()
+ |> length()
+
+ {:ok, Map.put(pack, :files_count, files_count)}
+ else
+ {:error, :not_found}
+ end
+ end
+
+ @spec emoji_path() :: Path.t()
+ defp emoji_path do
+ [:instance, :static_dir]
+ |> Pleroma.Config.get!()
+ |> Path.join("emoji")
+ end
+
+ defp validate_emoji_not_exists(shortcode, force \\ false)
+ defp validate_emoji_not_exists(_shortcode, true), do: :ok
+
+ defp validate_emoji_not_exists(shortcode, _) do
+ case Emoji.get(shortcode) do
+ nil -> :ok
+ _ -> {:error, :already_exists}
+ end
+ end
+
+ defp write_pack_contents(path) do
+ pack = %__MODULE__{
+ files: files_from_path(path),
+ path: path,
+ pack_file: Path.join(path, "pack.json")
+ }
+
+ case save_pack(pack) do
+ {:ok, _pack} -> Path.basename(path)
+ _ -> nil
+ end
+ end
+
+ defp files_from_path(path) do
+ txt_path = Path.join(path, "emoji.txt")
+
+ if File.exists?(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 file
+
+ # FIXME: Copy-pasted from Pleroma.Emoji/load_from_file_stream/2
+
+ # Create a map of shortcodes to filenames from emoji.txt
+ txt_path
+ |> File.read!()
+ |> 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 | _] ->
+ file_dir_name = Path.dirname(file)
+
+ if String.ends_with?(path, file_dir_name) do
+ {name, Path.basename(file)}
+ else
+ {name, file}
+ end
+
+ _ ->
+ nil
+ end
+ end)
+ |> Enum.reject(&is_nil/1)
+ |> Map.new()
+ else
+ # If there's no emoji.txt, assume all files
+ # that are of certain extensions from the config are emojis and import them all
+ pack_extensions = Pleroma.Config.get!([:emoji, :pack_extensions])
+ Emoji.Loader.make_shortcode_to_file_map(path, pack_extensions)
+ end
+ end
+
+ defp validate_pack(pack) do
+ info =
+ if downloadable?(pack) do
+ archive = fetch_archive(pack)
+ archive_sha = :crypto.hash(:sha256, archive) |> Base.encode16()
+
+ pack.pack
+ |> Map.put("can-download", true)
+ |> Map.put("download-sha256", archive_sha)
+ else
+ Map.put(pack.pack, "can-download", false)
+ end
+
+ Map.put(pack, :pack, info)
+ end
+
+ defp downloadable?(pack) 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 {_, file} ->
+ pack.path
+ |> Path.join(file)
+ |> File.exists?()
+ end)
+ end
+
+ defp create_archive_and_cache(pack, hash) do
+ files = ['pack.json' | Enum.map(pack.files, fn {_, file} -> to_charlist(file) end)]
+
+ {:ok, {_, result}} =
+ :zip.zip('#{pack.name}.zip', files, [:memory, cwd: to_charlist(pack.path)])
+
+ ttl_per_file = Pleroma.Config.get!([:emoji, :shared_pack_cache_seconds_per_file])
+ overall_ttl = :timer.seconds(ttl_per_file * Enum.count(files))
+
+ Cachex.put!(
+ :emoji_packs_cache,
+ pack.name,
+ # if pack.json MD5 changes, the cache is not valid anymore
+ %{hash: hash, pack_data: result},
+ # Add a minute to cache time for every file in the pack
+ ttl: overall_ttl
+ )
+
+ result
+ end
+
+ defp save_pack(pack) do
+ with {:ok, json} <- Jason.encode(pack, pretty: true),
+ :ok <- File.write(pack.pack_file, json) do
+ {:ok, pack}
+ end
+ end
+
+ defp from_json(json) do
+ map = Jason.decode!(json)
+
+ struct(__MODULE__, %{files: map["files"], pack: map["pack"]})
+ end
+
+ defp validate_shareable_packs_available(uri) do
+ with {:ok, %{"links" => links}} <- uri |> URI.merge("/.well-known/nodeinfo") |> http_get(),
+ # Get the actual nodeinfo address and fetch it
+ {:ok, %{"metadata" => %{"features" => features}}} <-
+ links |> List.last() |> Map.get("href") |> http_get() do
+ if Enum.member?(features, "shareable_emoji_packs") do
+ :ok
+ else
+ {:error, :not_shareable}
+ end
+ end
+ end
+
+ defp validate_not_empty(list) do
+ if Enum.all?(list, fn i -> is_binary(i) and i != "" end) do
+ :ok
+ else
+ {:error, :empty_values}
+ end
+ end
+
+ defp save_file(file, pack, filename) do
+ file_path = Path.join(pack.path, filename)
+ create_subdirs(file_path)
+
+ case file do
+ %Plug.Upload{path: upload_path} ->
+ # Copy the uploaded file from the temporary directory
+ with {:ok, _} <- File.copy(upload_path, file_path), do: :ok
+
+ url when is_binary(url) ->
+ # Download and write the file
+ file_contents = Tesla.get!(url).body
+ File.write(file_path, file_contents)
+ end
+ end
+
+ defp put_emoji(pack, shortcode, filename) do
+ files = Map.put(pack.files, shortcode, filename)
+ %{pack | files: files}
+ end
+
+ defp delete_emoji(pack, shortcode) do
+ files = Map.delete(pack.files, shortcode)
+ %{pack | files: files}
+ end
+
+ defp rename_file(pack, filename, new_filename) do
+ old_path = Path.join(pack.path, filename)
+ new_path = Path.join(pack.path, new_filename)
+ create_subdirs(new_path)
+
+ with :ok <- File.rename(old_path, new_path) do
+ remove_dir_if_empty(old_path, filename)
+ end
+ end
+
+ defp create_subdirs(file_path) do
+ if String.contains?(file_path, "/") do
+ file_path
+ |> Path.dirname()
+ |> File.mkdir_p!()
+ end
+ end
+
+ defp remove_file(pack, shortcode) do
+ with {:ok, filename} <- get_filename(pack, shortcode),
+ emoji <- Path.join(pack.path, filename),
+ :ok <- File.rm(emoji) do
+ remove_dir_if_empty(emoji, filename)
+ end
+ end
+
+ defp remove_dir_if_empty(emoji, filename) do
+ dir = Path.dirname(emoji)
+
+ if String.contains?(filename, "/") and File.ls!(dir) == [] do
+ File.rmdir!(dir)
+ else
+ :ok
+ end
+ end
+
+ defp get_filename(pack, shortcode) do
+ with %{^shortcode => filename} when is_binary(filename) <- pack.files,
+ true <- pack.path |> Path.join(filename) |> File.exists?() do
+ {:ok, filename}
+ else
+ _ -> {:error, :doesnt_exist}
+ end
+ end
+
+ defp http_get(%URI{} = url), do: url |> to_string() |> http_get()
+
+ defp http_get(url) do
+ with {:ok, %{body: body}} <- url |> Pleroma.HTTP.get() do
+ Jason.decode(body)
+ end
+ end
+
+ defp list_packs_dir do
+ emoji_path = emoji_path()
+ # Create the directory first if it does not exist. This is probably the first request made
+ # with the API so it should be sufficient
+ with {:create_dir, :ok} <- {:create_dir, File.mkdir_p(emoji_path)},
+ {:ls, {:ok, results}} <- {:ls, File.ls(emoji_path)} do
+ {:ok, Enum.sort(results)}
+ else
+ {:create_dir, {:error, e}} -> {:error, :create_dir, e}
+ {:ls, {:error, e}} -> {:error, :ls, e}
+ end
+ end
+
+ defp validate_downloadable(pack) do
+ if downloadable?(pack), do: :ok, else: {:error, :cant_download}
+ end
+
+ defp copy_as(remote_pack, local_name) do
+ path = Path.join(emoji_path(), local_name)
+
+ %__MODULE__{
+ name: local_name,
+ path: path,
+ files: remote_pack["files"],
+ pack_file: Path.join(path, "pack.json")
+ }
+ end
+
+ defp unzip(archive, pack_info, remote_pack, local_pack) do
+ with :ok <- File.mkdir_p!(local_pack.path) do
+ files = Enum.map(remote_pack["files"], fn {_, path} -> to_charlist(path) end)
+ # Fallback cannot contain a pack.json file
+ files = if pack_info[:fallback], do: files, else: ['pack.json' | files]
+
+ :zip.unzip(archive, cwd: to_charlist(local_pack.path), file_list: files)
+ end
+ end
+
+ defp fetch_pack_info(remote_pack, uri, name) do
+ case remote_pack["pack"] do
+ %{"share-files" => true, "can-download" => true, "download-sha256" => sha} ->
+ {:ok,
+ %{
+ sha: sha,
+ url: URI.merge(uri, "/api/pleroma/emoji/packs/#{name}/archive") |> to_string()
+ }}
+
+ %{"fallback-src" => src, "fallback-src-sha256" => sha} when is_binary(src) ->
+ {:ok,
+ %{
+ sha: sha,
+ url: src,
+ fallback: true
+ }}
+
+ _ ->
+ {:error, "The pack was not set as shared and there is no fallback src to download from"}
+ end
+ end
+
+ defp download_archive(url, sha) do
+ with {:ok, %{body: archive}} <- Tesla.get(url) do
+ if Base.decode16!(sha) == :crypto.hash(:sha256, archive) do
+ {:ok, archive}
+ else
+ {:error, :invalid_checksum}
+ end
+ end
+ end
+
+ defp fetch_archive(pack) do
+ hash = :crypto.hash(:md5, File.read!(pack.pack_file))
+
+ case Cachex.get!(:emoji_packs_cache, pack.name) do
+ %{hash: ^hash, pack_data: archive} -> archive
+ _ -> create_archive_and_cache(pack, hash)
+ end
+ end
+
+ defp fallback_sha_changed?(pack, data) do
+ is_binary(data[:"fallback-src"]) and data[:"fallback-src"] != pack.pack["fallback-src"]
+ end
+
+ defp update_sha_and_save_metadata(pack, data) do
+ with {:ok, %{body: zip}} <- Tesla.get(data[:"fallback-src"]),
+ :ok <- validate_has_all_files(pack, zip) do
+ fallback_sha = :sha256 |> :crypto.hash(zip) |> Base.encode16()
+
+ data
+ |> Map.put("fallback-src-sha256", fallback_sha)
+ |> save_metadata(pack)
+ end
+ end
+
+ defp validate_has_all_files(pack, zip) do
+ with {:ok, f_list} <- :zip.unzip(zip, [:memory]) do
+ # Check if all files from the pack.json are in the archive
+ pack.files
+ |> Enum.all?(fn {_, from_manifest} ->
+ List.keyfind(f_list, to_charlist(from_manifest), 0)
+ end)
+ |> if(do: :ok, else: {:error, :incomplete})
+ end
+ end
+end
diff --git a/lib/pleroma/filter.ex b/lib/pleroma/filter.ex
index 7cb49360f..5d6df9530 100644
--- a/lib/pleroma/filter.ex
+++ b/lib/pleroma/filter.ex
@@ -34,10 +34,18 @@ defmodule Pleroma.Filter do
Repo.one(query)
end
- def get_filters(%User{id: user_id} = _user) do
+ def get_active(query) do
+ from(f in query, where: is_nil(f.expires_at) or f.expires_at > ^NaiveDateTime.utc_now())
+ end
+
+ def get_irreversible(query) do
+ from(f in query, where: f.hide)
+ end
+
+ def get_filters(query \\ __MODULE__, %User{id: user_id}) do
query =
from(
- f in Pleroma.Filter,
+ f in query,
where: f.user_id == ^user_id,
order_by: [desc: :id]
)
@@ -89,11 +97,40 @@ defmodule Pleroma.Filter do
|> Repo.delete()
end
- def update(%Pleroma.Filter{} = filter) do
- destination = Map.from_struct(filter)
-
- Pleroma.Filter.get(filter.filter_id, %{id: filter.user_id})
- |> cast(destination, [:phrase, :context, :hide, :expires_at, :whole_word])
+ def update(%Pleroma.Filter{} = filter, params) do
+ filter
+ |> cast(params, [:phrase, :context, :hide, :expires_at, :whole_word])
+ |> validate_required([:phrase, :context])
|> Repo.update()
end
+
+ def compose_regex(user_or_filters, format \\ :postgres)
+
+ def compose_regex(%User{} = user, format) do
+ __MODULE__
+ |> get_active()
+ |> get_irreversible()
+ |> get_filters(user)
+ |> compose_regex(format)
+ end
+
+ def compose_regex([_ | _] = filters, format) do
+ phrases =
+ filters
+ |> Enum.map(& &1.phrase)
+ |> Enum.join("|")
+
+ case format do
+ :postgres ->
+ "\\y(#{phrases})\\y"
+
+ :re ->
+ ~r/\b#{phrases}\b/i
+
+ _ ->
+ nil
+ end
+ end
+
+ def compose_regex(_, _), do: nil
end
diff --git a/lib/pleroma/following_relationship.ex b/lib/pleroma/following_relationship.ex
index 9ccf40495..2039a259d 100644
--- a/lib/pleroma/following_relationship.ex
+++ b/lib/pleroma/following_relationship.ex
@@ -10,11 +10,12 @@ defmodule Pleroma.FollowingRelationship do
alias Ecto.Changeset
alias FlakeId.Ecto.CompatType
+ alias Pleroma.FollowingRelationship.State
alias Pleroma.Repo
alias Pleroma.User
schema "following_relationships" do
- field(:state, Pleroma.FollowingRelationship.State, default: :follow_pending)
+ field(:state, State, default: :follow_pending)
belongs_to(:follower, User, type: CompatType)
belongs_to(:following, User, type: CompatType)
@@ -22,6 +23,11 @@ defmodule Pleroma.FollowingRelationship do
timestamps()
end
+ @doc "Returns underlying integer code for state atom"
+ def state_int_code(state_atom), do: State.__enum_map__() |> Keyword.fetch!(state_atom)
+
+ def accept_state_code, do: state_int_code(:follow_accept)
+
def changeset(%__MODULE__{} = following_relationship, attrs) do
following_relationship
|> cast(attrs, [:state])
@@ -82,6 +88,33 @@ defmodule Pleroma.FollowingRelationship do
|> Repo.aggregate(:count, :id)
end
+ def followers_query(%User{} = user) do
+ __MODULE__
+ |> join(:inner, [r], u in User, on: r.follower_id == u.id)
+ |> where([r], r.following_id == ^user.id)
+ |> where([r], r.state == ^:follow_accept)
+ end
+
+ def followers_ap_ids(user, from_ap_ids \\ nil)
+
+ def followers_ap_ids(_, []), do: []
+
+ def followers_ap_ids(%User{} = user, from_ap_ids) do
+ query =
+ user
+ |> followers_query()
+ |> select([r, u], u.ap_id)
+
+ query =
+ if from_ap_ids do
+ where(query, [r, u], u.ap_id in ^from_ap_ids)
+ else
+ query
+ end
+
+ Repo.all(query)
+ end
+
def following_count(%User{id: nil}), do: 0
def following_count(%User{} = user) do
@@ -95,6 +128,7 @@ defmodule Pleroma.FollowingRelationship do
|> join(:inner, [r], f in assoc(r, :follower))
|> where([r], r.state == ^:follow_pending)
|> where([r], r.following_id == ^id)
+ |> where([r, f], f.deactivated != true)
|> select([r, f], f)
|> Repo.all()
end
@@ -105,12 +139,22 @@ defmodule Pleroma.FollowingRelationship do
|> Repo.exists?()
end
+ def following_query(%User{} = user) do
+ __MODULE__
+ |> join(:inner, [r], u in User, on: r.following_id == u.id)
+ |> where([r], r.follower_id == ^user.id)
+ |> where([r], r.state == ^:follow_accept)
+ end
+
+ def outgoing_pending_follow_requests_query(%User{} = follower) do
+ __MODULE__
+ |> where([r], r.follower_id == ^follower.id)
+ |> where([r], r.state == ^:follow_pending)
+ end
+
def following(%User{} = user) do
following =
- __MODULE__
- |> join(:inner, [r], u in User, on: r.following_id == u.id)
- |> where([r], r.follower_id == ^user.id)
- |> where([r], r.state == ^:follow_accept)
+ following_query(user)
|> select([r, u], u.follower_address)
|> Repo.all()
@@ -171,6 +215,30 @@ defmodule Pleroma.FollowingRelationship do
end)
end
+ @doc """
+ For a query with joined activity,
+ keeps rows where activity's actor is followed by user -or- is NOT domain-blocked by user.
+ """
+ def keep_following_or_not_domain_blocked(query, user) do
+ where(
+ query,
+ [_, activity],
+ fragment(
+ # "(actor's domain NOT in domain_blocks) OR (actor IS in followed AP IDs)"
+ """
+ NOT (substring(? from '.*://([^/]*)') = ANY(?)) OR
+ ? = ANY(SELECT ap_id FROM users AS u INNER JOIN following_relationships AS fr
+ ON u.id = fr.following_id WHERE fr.follower_id = ? AND fr.state = ?)
+ """,
+ activity.actor,
+ ^user.domain_blocks,
+ activity.actor,
+ ^User.binary_id(user.id),
+ ^accept_state_code()
+ )
+ )
+ end
+
defp validate_not_self_relationship(%Changeset{} = changeset) do
changeset
|> validate_follower_id_following_id_inequality()
@@ -196,4 +264,12 @@ defmodule Pleroma.FollowingRelationship do
end
end)
end
+
+ @spec following_ap_ids(User.t()) :: [String.t()]
+ def following_ap_ids(%User{} = user) do
+ user
+ |> following_query()
+ |> select([r, u], u.ap_id)
+ |> Repo.all()
+ end
end
diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex
index e2a658cb3..0c450eae4 100644
--- a/lib/pleroma/formatter.ex
+++ b/lib/pleroma/formatter.ex
@@ -10,11 +10,15 @@ defmodule Pleroma.Formatter do
@link_regex ~r"((?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~%:/?#[\]@!\$&'\(\)\*\+,;=.]+)|[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+"ui
@markdown_characters_regex ~r/(`|\*|_|{|}|[|]|\(|\)|#|\+|-|\.|!)/
- @auto_linker_config hashtag: true,
- hashtag_handler: &Pleroma.Formatter.hashtag_handler/4,
- mention: true,
- mention_handler: &Pleroma.Formatter.mention_handler/4,
- scheme: true
+ defp linkify_opts do
+ Pleroma.Config.get(Pleroma.Formatter) ++
+ [
+ hashtag: true,
+ hashtag_handler: &Pleroma.Formatter.hashtag_handler/4,
+ mention: true,
+ mention_handler: &Pleroma.Formatter.mention_handler/4
+ ]
+ end
def escape_mention_handler("@" <> nickname = mention, buffer, _, _) do
case User.get_cached_by_nickname(nickname) do
@@ -31,13 +35,23 @@ defmodule Pleroma.Formatter do
def mention_handler("@" <> nickname, buffer, opts, acc) do
case User.get_cached_by_nickname(nickname) do
%User{id: id} = user ->
- ap_id = get_ap_id(user)
+ user_url = user.uri || user.ap_id
nickname_text = get_nickname_text(nickname, opts)
link =
- ~s(<span class="h-card"><a data-user="#{id}" class="u-url mention" href="#{ap_id}" rel="ugc">@<span>#{
- nickname_text
- }</span></a></span>)
+ Phoenix.HTML.Tag.content_tag(
+ :span,
+ Phoenix.HTML.Tag.content_tag(
+ :a,
+ ["@", Phoenix.HTML.Tag.content_tag(:span, nickname_text)],
+ "data-user": id,
+ class: "u-url mention",
+ href: user_url,
+ rel: "ugc"
+ ),
+ class: "h-card"
+ )
+ |> Phoenix.HTML.safe_to_string()
{link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, user})}}
@@ -49,7 +63,15 @@ defmodule Pleroma.Formatter do
def hashtag_handler("#" <> tag = tag_text, _buffer, _opts, acc) do
tag = String.downcase(tag)
url = "#{Pleroma.Web.base_url()}/tag/#{tag}"
- link = ~s(<a class="hashtag" data-tag="#{tag}" href="#{url}" rel="tag ugc">#{tag_text}</a>)
+
+ link =
+ Phoenix.HTML.Tag.content_tag(:a, tag_text,
+ class: "hashtag",
+ "data-tag": tag,
+ href: url,
+ rel: "tag ugc"
+ )
+ |> Phoenix.HTML.safe_to_string()
{link, %{acc | tags: MapSet.put(acc.tags, {tag_text, tag})}}
end
@@ -62,19 +84,19 @@ defmodule Pleroma.Formatter do
@spec linkify(String.t(), keyword()) ::
{String.t(), [{String.t(), User.t()}], [{String.t(), String.t()}]}
def linkify(text, options \\ []) do
- options = options ++ @auto_linker_config
+ options = linkify_opts() ++ options
if options[:safe_mention] && Regex.named_captures(@safe_mention_regex, text) do
%{"mentions" => mentions, "rest" => rest} = Regex.named_captures(@safe_mention_regex, text)
acc = %{mentions: MapSet.new(), tags: MapSet.new()}
- {text_mentions, %{mentions: mentions}} = AutoLinker.link_map(mentions, acc, options)
- {text_rest, %{tags: tags}} = AutoLinker.link_map(rest, acc, options)
+ {text_mentions, %{mentions: mentions}} = Linkify.link_map(mentions, acc, options)
+ {text_rest, %{tags: tags}} = Linkify.link_map(rest, acc, options)
{text_mentions <> text_rest, MapSet.to_list(mentions), MapSet.to_list(tags)}
else
acc = %{mentions: MapSet.new(), tags: MapSet.new()}
- {text, %{mentions: mentions, tags: tags}} = AutoLinker.link_map(text, acc, options)
+ {text, %{mentions: mentions, tags: tags}} = Linkify.link_map(text, acc, options)
{text, MapSet.to_list(mentions), MapSet.to_list(tags)}
end
@@ -93,9 +115,9 @@ defmodule Pleroma.Formatter do
if options[:safe_mention] && Regex.named_captures(@safe_mention_regex, text) do
%{"mentions" => mentions, "rest" => rest} = Regex.named_captures(@safe_mention_regex, text)
- AutoLinker.link(mentions, options) <> AutoLinker.link(rest, options)
+ Linkify.link(mentions, options) <> Linkify.link(rest, options)
else
- AutoLinker.link(text, options)
+ Linkify.link(text, options)
end
end
@@ -128,9 +150,6 @@ defmodule Pleroma.Formatter do
end
end
- defp get_ap_id(%User{source_data: %{"url" => url}}) when is_binary(url), do: url
- defp get_ap_id(%User{ap_id: ap_id}), do: ap_id
-
defp get_nickname_text(nickname, %{mentions_format: :full}), do: User.full_nickname(nickname)
defp get_nickname_text(nickname, _), do: User.local_nickname(nickname)
end
diff --git a/lib/pleroma/gopher/server.ex b/lib/pleroma/gopher/server.ex
index 3d56d50a9..e9f54c4c0 100644
--- a/lib/pleroma/gopher/server.ex
+++ b/lib/pleroma/gopher/server.ex
@@ -96,16 +96,18 @@ defmodule Pleroma.Gopher.Server.ProtocolHandler do
def response("/main/public") do
posts =
- ActivityPub.fetch_public_activities(%{"type" => ["Create"], "local_only" => true})
- |> render_activities
+ %{type: ["Create"], local_only: true}
+ |> ActivityPub.fetch_public_activities()
+ |> render_activities()
info("Welcome to the Public Timeline!") <> posts <> ".\r\n"
end
def response("/main/all") do
posts =
- ActivityPub.fetch_public_activities(%{"type" => ["Create"]})
- |> render_activities
+ %{type: ["Create"]}
+ |> ActivityPub.fetch_public_activities()
+ |> render_activities()
info("Welcome to the Federated Timeline!") <> posts <> ".\r\n"
end
@@ -130,13 +132,14 @@ defmodule Pleroma.Gopher.Server.ProtocolHandler do
def response("/users/" <> nickname) do
with %User{} = user <- User.get_cached_by_nickname(nickname) do
params = %{
- "type" => ["Create"],
- "actor_id" => user.ap_id
+ type: ["Create"],
+ actor_id: user.ap_id
}
activities =
- ActivityPub.fetch_public_activities(params)
- |> render_activities
+ params
+ |> ActivityPub.fetch_public_activities()
+ |> render_activities()
info("Posts by #{user.nickname}") <> activities <> ".\r\n"
else
diff --git a/lib/pleroma/gun/api.ex b/lib/pleroma/gun/api.ex
new file mode 100644
index 000000000..09be74392
--- /dev/null
+++ b/lib/pleroma/gun/api.ex
@@ -0,0 +1,46 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Gun.API do
+ @behaviour Pleroma.Gun
+
+ alias Pleroma.Gun
+
+ @gun_keys [
+ :connect_timeout,
+ :http_opts,
+ :http2_opts,
+ :protocols,
+ :retry,
+ :retry_timeout,
+ :trace,
+ :transport,
+ :tls_opts,
+ :tcp_opts,
+ :socks_opts,
+ :ws_opts,
+ :supervise
+ ]
+
+ @impl Gun
+ def open(host, port, opts \\ %{}), do: :gun.open(host, port, Map.take(opts, @gun_keys))
+
+ @impl Gun
+ defdelegate info(pid), to: :gun
+
+ @impl Gun
+ defdelegate close(pid), to: :gun
+
+ @impl Gun
+ defdelegate await_up(pid, timeout \\ 5_000), to: :gun
+
+ @impl Gun
+ defdelegate connect(pid, opts), to: :gun
+
+ @impl Gun
+ defdelegate await(pid, ref), to: :gun
+
+ @impl Gun
+ defdelegate set_owner(pid, owner), to: :gun
+end
diff --git a/lib/pleroma/gun/conn.ex b/lib/pleroma/gun/conn.ex
new file mode 100644
index 000000000..a3f75a4bb
--- /dev/null
+++ b/lib/pleroma/gun/conn.ex
@@ -0,0 +1,135 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Gun.Conn do
+ alias Pleroma.Gun
+
+ require Logger
+
+ def open(%URI{} = uri, opts) do
+ pool_opts = Pleroma.Config.get([:connections_pool], [])
+
+ opts =
+ opts
+ |> Enum.into(%{})
+ |> Map.put_new(:await_up_timeout, pool_opts[:await_up_timeout] || 5_000)
+ |> Map.put_new(:supervise, false)
+ |> maybe_add_tls_opts(uri)
+
+ do_open(uri, opts)
+ end
+
+ defp maybe_add_tls_opts(opts, %URI{scheme: "http"}), do: opts
+
+ defp maybe_add_tls_opts(opts, %URI{scheme: "https"}) do
+ tls_opts = [
+ verify: :verify_peer,
+ cacertfile: CAStore.file_path(),
+ depth: 20,
+ reuse_sessions: false,
+ log_level: :warning,
+ customize_hostname_check: [match_fun: :public_key.pkix_verify_hostname_match_fun(:https)]
+ ]
+
+ tls_opts =
+ if Keyword.keyword?(opts[:tls_opts]) do
+ Keyword.merge(tls_opts, opts[:tls_opts])
+ else
+ tls_opts
+ end
+
+ Map.put(opts, :tls_opts, tls_opts)
+ end
+
+ defp do_open(uri, %{proxy: {proxy_host, proxy_port}} = opts) do
+ connect_opts =
+ uri
+ |> destination_opts()
+ |> add_http2_opts(uri.scheme, Map.get(opts, :tls_opts, []))
+
+ with open_opts <- Map.delete(opts, :tls_opts),
+ {:ok, conn} <- Gun.open(proxy_host, proxy_port, open_opts),
+ {:ok, _} <- Gun.await_up(conn, opts[:await_up_timeout]),
+ stream <- Gun.connect(conn, connect_opts),
+ {:response, :fin, 200, _} <- Gun.await(conn, stream) do
+ {:ok, conn}
+ else
+ error ->
+ Logger.warn(
+ "Opening proxied connection to #{compose_uri_log(uri)} failed with error #{
+ inspect(error)
+ }"
+ )
+
+ error
+ end
+ end
+
+ defp do_open(uri, %{proxy: {proxy_type, proxy_host, proxy_port}} = opts) do
+ version =
+ proxy_type
+ |> to_string()
+ |> String.last()
+ |> case do
+ "4" -> 4
+ _ -> 5
+ end
+
+ socks_opts =
+ uri
+ |> destination_opts()
+ |> add_http2_opts(uri.scheme, Map.get(opts, :tls_opts, []))
+ |> Map.put(:version, version)
+
+ opts =
+ opts
+ |> Map.put(:protocols, [:socks])
+ |> Map.put(:socks_opts, socks_opts)
+
+ with {:ok, conn} <- Gun.open(proxy_host, proxy_port, opts),
+ {:ok, _} <- Gun.await_up(conn, opts[:await_up_timeout]) do
+ {:ok, conn}
+ else
+ error ->
+ Logger.warn(
+ "Opening socks proxied connection to #{compose_uri_log(uri)} failed with error #{
+ inspect(error)
+ }"
+ )
+
+ error
+ end
+ end
+
+ defp do_open(%URI{host: host, port: port} = uri, opts) do
+ host = Pleroma.HTTP.AdapterHelper.parse_host(host)
+
+ with {:ok, conn} <- Gun.open(host, port, opts),
+ {:ok, _} <- Gun.await_up(conn, opts[:await_up_timeout]) do
+ {:ok, conn}
+ else
+ error ->
+ Logger.warn(
+ "Opening connection to #{compose_uri_log(uri)} failed with error #{inspect(error)}"
+ )
+
+ error
+ end
+ end
+
+ defp destination_opts(%URI{host: host, port: port}) do
+ host = Pleroma.HTTP.AdapterHelper.parse_host(host)
+ %{host: host, port: port}
+ end
+
+ defp add_http2_opts(opts, "https", tls_opts) do
+ Map.merge(opts, %{protocols: [:http2], transport: :tls, tls_opts: tls_opts})
+ end
+
+ defp add_http2_opts(opts, _, _), do: opts
+
+ def compose_uri_log(%URI{scheme: scheme, host: host, path: path}) do
+ "#{scheme}://#{host}#{path}"
+ end
+end
diff --git a/lib/pleroma/gun/connection_pool.ex b/lib/pleroma/gun/connection_pool.ex
new file mode 100644
index 000000000..f34602b73
--- /dev/null
+++ b/lib/pleroma/gun/connection_pool.ex
@@ -0,0 +1,82 @@
+defmodule Pleroma.Gun.ConnectionPool do
+ @registry __MODULE__
+
+ alias Pleroma.Gun.ConnectionPool.WorkerSupervisor
+
+ def children do
+ [
+ {Registry, keys: :unique, name: @registry},
+ Pleroma.Gun.ConnectionPool.WorkerSupervisor
+ ]
+ end
+
+ @spec get_conn(URI.t(), keyword()) :: {:ok, pid()} | {:error, term()}
+ def get_conn(uri, opts) do
+ key = "#{uri.scheme}:#{uri.host}:#{uri.port}"
+
+ case Registry.lookup(@registry, key) do
+ # The key has already been registered, but connection is not up yet
+ [{worker_pid, nil}] ->
+ get_gun_pid_from_worker(worker_pid, true)
+
+ [{worker_pid, {gun_pid, _used_by, _crf, _last_reference}}] ->
+ GenServer.call(worker_pid, :add_client)
+ {:ok, gun_pid}
+
+ [] ->
+ # :gun.set_owner fails in :connected state for whatevever reason,
+ # so we open the connection in the process directly and send it's pid back
+ # We trust gun to handle timeouts by itself
+ case WorkerSupervisor.start_worker([key, uri, opts, self()]) do
+ {:ok, worker_pid} ->
+ get_gun_pid_from_worker(worker_pid, false)
+
+ {:error, {:already_started, worker_pid}} ->
+ get_gun_pid_from_worker(worker_pid, true)
+
+ err ->
+ err
+ end
+ end
+ end
+
+ defp get_gun_pid_from_worker(worker_pid, register) do
+ # GenServer.call will block the process for timeout length if
+ # the server crashes on startup (which will happen if gun fails to connect)
+ # so instead we use cast + monitor
+
+ ref = Process.monitor(worker_pid)
+ if register, do: GenServer.cast(worker_pid, {:add_client, self()})
+
+ receive do
+ {:conn_pid, pid} ->
+ Process.demonitor(ref)
+ {:ok, pid}
+
+ {:DOWN, ^ref, :process, ^worker_pid, reason} ->
+ case reason do
+ {:shutdown, {:error, _} = error} -> error
+ {:shutdown, error} -> {:error, error}
+ _ -> {:error, reason}
+ end
+ end
+ end
+
+ @spec release_conn(pid()) :: :ok
+ def release_conn(conn_pid) do
+ # :ets.fun2ms(fn {_, {worker_pid, {gun_pid, _, _, _}}} when gun_pid == conn_pid ->
+ # worker_pid end)
+ query_result =
+ Registry.select(@registry, [
+ {{:_, :"$1", {:"$2", :_, :_, :_}}, [{:==, :"$2", conn_pid}], [:"$1"]}
+ ])
+
+ case query_result do
+ [worker_pid] ->
+ GenServer.call(worker_pid, :remove_client)
+
+ [] ->
+ :ok
+ end
+ end
+end
diff --git a/lib/pleroma/gun/connection_pool/reclaimer.ex b/lib/pleroma/gun/connection_pool/reclaimer.ex
new file mode 100644
index 000000000..cea800882
--- /dev/null
+++ b/lib/pleroma/gun/connection_pool/reclaimer.ex
@@ -0,0 +1,85 @@
+defmodule Pleroma.Gun.ConnectionPool.Reclaimer do
+ use GenServer, restart: :temporary
+
+ @registry Pleroma.Gun.ConnectionPool
+
+ def start_monitor do
+ pid =
+ case :gen_server.start(__MODULE__, [], name: {:via, Registry, {@registry, "reclaimer"}}) do
+ {:ok, pid} ->
+ pid
+
+ {:error, {:already_registered, pid}} ->
+ pid
+ end
+
+ {pid, Process.monitor(pid)}
+ end
+
+ @impl true
+ def init(_) do
+ {:ok, nil, {:continue, :reclaim}}
+ end
+
+ @impl true
+ def handle_continue(:reclaim, _) do
+ max_connections = Pleroma.Config.get([:connections_pool, :max_connections])
+
+ reclaim_max =
+ [:connections_pool, :reclaim_multiplier]
+ |> Pleroma.Config.get()
+ |> Kernel.*(max_connections)
+ |> round
+ |> max(1)
+
+ :telemetry.execute([:pleroma, :connection_pool, :reclaim, :start], %{}, %{
+ max_connections: max_connections,
+ reclaim_max: reclaim_max
+ })
+
+ # :ets.fun2ms(
+ # fn {_, {worker_pid, {_, used_by, crf, last_reference}}} when used_by == [] ->
+ # {worker_pid, crf, last_reference} end)
+ unused_conns =
+ Registry.select(
+ @registry,
+ [
+ {{:_, :"$1", {:_, :"$2", :"$3", :"$4"}}, [{:==, :"$2", []}], [{{:"$1", :"$3", :"$4"}}]}
+ ]
+ )
+
+ case unused_conns do
+ [] ->
+ :telemetry.execute(
+ [:pleroma, :connection_pool, :reclaim, :stop],
+ %{reclaimed_count: 0},
+ %{
+ max_connections: max_connections
+ }
+ )
+
+ {:stop, :no_unused_conns, nil}
+
+ unused_conns ->
+ reclaimed =
+ unused_conns
+ |> Enum.sort(fn {_pid1, crf1, last_reference1}, {_pid2, crf2, last_reference2} ->
+ crf1 <= crf2 and last_reference1 <= last_reference2
+ end)
+ |> Enum.take(reclaim_max)
+
+ reclaimed
+ |> Enum.each(fn {pid, _, _} ->
+ DynamicSupervisor.terminate_child(Pleroma.Gun.ConnectionPool.WorkerSupervisor, pid)
+ end)
+
+ :telemetry.execute(
+ [:pleroma, :connection_pool, :reclaim, :stop],
+ %{reclaimed_count: Enum.count(reclaimed)},
+ %{max_connections: max_connections}
+ )
+
+ {:stop, :normal, nil}
+ end
+ end
+end
diff --git a/lib/pleroma/gun/connection_pool/worker.ex b/lib/pleroma/gun/connection_pool/worker.ex
new file mode 100644
index 000000000..fec9d0efa
--- /dev/null
+++ b/lib/pleroma/gun/connection_pool/worker.ex
@@ -0,0 +1,133 @@
+defmodule Pleroma.Gun.ConnectionPool.Worker do
+ alias Pleroma.Gun
+ use GenServer, restart: :temporary
+
+ @registry Pleroma.Gun.ConnectionPool
+
+ def start_link([key | _] = opts) do
+ GenServer.start_link(__MODULE__, opts, name: {:via, Registry, {@registry, key}})
+ end
+
+ @impl true
+ def init([_key, _uri, _opts, _client_pid] = opts) do
+ {:ok, nil, {:continue, {:connect, opts}}}
+ end
+
+ @impl true
+ def handle_continue({:connect, [key, uri, opts, client_pid]}, _) do
+ with {:ok, conn_pid} <- Gun.Conn.open(uri, opts),
+ Process.link(conn_pid) do
+ time = :erlang.monotonic_time(:millisecond)
+
+ {_, _} =
+ Registry.update_value(@registry, key, fn _ ->
+ {conn_pid, [client_pid], 1, time}
+ end)
+
+ send(client_pid, {:conn_pid, conn_pid})
+
+ {:noreply,
+ %{key: key, timer: nil, client_monitors: %{client_pid => Process.monitor(client_pid)}},
+ :hibernate}
+ else
+ err ->
+ {:stop, {:shutdown, err}, nil}
+ end
+ end
+
+ @impl true
+ def handle_cast({:add_client, client_pid}, state) do
+ case handle_call(:add_client, {client_pid, nil}, state) do
+ {:reply, conn_pid, state, :hibernate} ->
+ send(client_pid, {:conn_pid, conn_pid})
+ {:noreply, state, :hibernate}
+ end
+ end
+
+ @impl true
+ def handle_cast({:remove_client, client_pid}, state) do
+ case handle_call(:remove_client, {client_pid, nil}, state) do
+ {:reply, _, state, :hibernate} ->
+ {:noreply, state, :hibernate}
+ end
+ end
+
+ @impl true
+ def handle_call(:add_client, {client_pid, _}, %{key: key} = state) do
+ time = :erlang.monotonic_time(:millisecond)
+
+ {{conn_pid, _, _, _}, _} =
+ Registry.update_value(@registry, key, fn {conn_pid, used_by, crf, last_reference} ->
+ {conn_pid, [client_pid | used_by], crf(time - last_reference, crf), time}
+ end)
+
+ state =
+ if state.timer != nil do
+ Process.cancel_timer(state[:timer])
+ %{state | timer: nil}
+ else
+ state
+ end
+
+ ref = Process.monitor(client_pid)
+
+ state = put_in(state.client_monitors[client_pid], ref)
+ {:reply, conn_pid, state, :hibernate}
+ end
+
+ @impl true
+ def handle_call(:remove_client, {client_pid, _}, %{key: key} = state) do
+ {{_conn_pid, used_by, _crf, _last_reference}, _} =
+ Registry.update_value(@registry, key, fn {conn_pid, used_by, crf, last_reference} ->
+ {conn_pid, List.delete(used_by, client_pid), crf, last_reference}
+ end)
+
+ {ref, state} = pop_in(state.client_monitors[client_pid])
+ Process.demonitor(ref)
+
+ timer =
+ if used_by == [] do
+ max_idle = Pleroma.Config.get([:connections_pool, :max_idle_time], 30_000)
+ Process.send_after(self(), :idle_close, max_idle)
+ else
+ nil
+ end
+
+ {:reply, :ok, %{state | timer: timer}, :hibernate}
+ end
+
+ @impl true
+ def handle_info(:idle_close, state) do
+ # Gun monitors the owner process, and will close the connection automatically
+ # when it's terminated
+ {:stop, :normal, state}
+ end
+
+ # Gracefully shutdown if the connection got closed without any streams left
+ @impl true
+ def handle_info({:gun_down, _pid, _protocol, _reason, []}, state) do
+ {:stop, :normal, state}
+ end
+
+ # Otherwise, shutdown with an error
+ @impl true
+ def handle_info({:gun_down, _pid, _protocol, _reason, _killed_streams} = down_message, state) do
+ {:stop, {:error, down_message}, state}
+ end
+
+ @impl true
+ def handle_info({:DOWN, _ref, :process, pid, reason}, state) do
+ :telemetry.execute(
+ [:pleroma, :connection_pool, :client_death],
+ %{client_pid: pid, reason: reason},
+ %{key: state.key}
+ )
+
+ handle_cast({:remove_client, pid}, state)
+ end
+
+ # LRFU policy: https://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.55.1478
+ defp crf(time_delta, prev_crf) do
+ 1 + :math.pow(0.5, 0.0001 * time_delta) * prev_crf
+ end
+end
diff --git a/lib/pleroma/gun/connection_pool/worker_supervisor.ex b/lib/pleroma/gun/connection_pool/worker_supervisor.ex
new file mode 100644
index 000000000..39615c956
--- /dev/null
+++ b/lib/pleroma/gun/connection_pool/worker_supervisor.ex
@@ -0,0 +1,45 @@
+defmodule Pleroma.Gun.ConnectionPool.WorkerSupervisor do
+ @moduledoc "Supervisor for pool workers. Does not do anything except enforce max connection limit"
+
+ use DynamicSupervisor
+
+ def start_link(opts) do
+ DynamicSupervisor.start_link(__MODULE__, opts, name: __MODULE__)
+ end
+
+ def init(_opts) do
+ DynamicSupervisor.init(
+ strategy: :one_for_one,
+ max_children: Pleroma.Config.get([:connections_pool, :max_connections])
+ )
+ end
+
+ def start_worker(opts, retry \\ false) do
+ case DynamicSupervisor.start_child(__MODULE__, {Pleroma.Gun.ConnectionPool.Worker, opts}) do
+ {:error, :max_children} ->
+ if retry or free_pool() == :error do
+ :telemetry.execute([:pleroma, :connection_pool, :provision_failure], %{opts: opts})
+ {:error, :pool_full}
+ else
+ start_worker(opts, true)
+ end
+
+ res ->
+ res
+ end
+ end
+
+ defp free_pool do
+ wait_for_reclaimer_finish(Pleroma.Gun.ConnectionPool.Reclaimer.start_monitor())
+ end
+
+ defp wait_for_reclaimer_finish({pid, mon}) do
+ receive do
+ {:DOWN, ^mon, :process, ^pid, :no_unused_conns} ->
+ :error
+
+ {:DOWN, ^mon, :process, ^pid, :normal} ->
+ :ok
+ end
+ end
+end
diff --git a/lib/pleroma/gun/gun.ex b/lib/pleroma/gun/gun.ex
new file mode 100644
index 000000000..4043e4880
--- /dev/null
+++ b/lib/pleroma/gun/gun.ex
@@ -0,0 +1,31 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Gun do
+ @callback open(charlist(), pos_integer(), map()) :: {:ok, pid()}
+ @callback info(pid()) :: map()
+ @callback close(pid()) :: :ok
+ @callback await_up(pid, pos_integer()) :: {:ok, atom()} | {:error, atom()}
+ @callback connect(pid(), map()) :: reference()
+ @callback await(pid(), reference()) :: {:response, :fin, 200, []}
+ @callback set_owner(pid(), pid()) :: :ok
+
+ @api Pleroma.Config.get([Pleroma.Gun], Pleroma.Gun.API)
+
+ defp api, do: @api
+
+ def open(host, port, opts), do: api().open(host, port, opts)
+
+ def info(pid), do: api().info(pid)
+
+ def close(pid), do: api().close(pid)
+
+ def await_up(pid, timeout \\ 5_000), do: api().await_up(pid, timeout)
+
+ def connect(pid, opts), do: api().connect(pid, opts)
+
+ def await(pid, ref), do: api().await(pid, ref)
+
+ def set_owner(pid, owner), do: api().set_owner(pid, owner)
+end
diff --git a/lib/pleroma/helpers/uri_helper.ex b/lib/pleroma/helpers/uri_helper.ex
index 256252ddb..6d205a636 100644
--- a/lib/pleroma/helpers/uri_helper.ex
+++ b/lib/pleroma/helpers/uri_helper.ex
@@ -17,11 +17,6 @@ defmodule Pleroma.Helpers.UriHelper do
|> URI.to_string()
end
- def append_param_if_present(%{} = params, param_name, param_value) do
- if param_value do
- Map.put(params, param_name, param_value)
- else
- params
- end
- end
+ def maybe_add_base("/" <> uri, base), do: Path.join([base, uri])
+ def maybe_add_base(uri, _base), do: uri
end
diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex
index d78c5f202..dc1b9b840 100644
--- a/lib/pleroma/html.ex
+++ b/lib/pleroma/html.ex
@@ -109,7 +109,7 @@ defmodule Pleroma.HTML do
result =
content
|> Floki.parse_fragment!()
- |> Floki.filter_out("a.mention,a.hashtag,a[rel~=\"tag\"]")
+ |> Floki.filter_out("a.mention,a.hashtag,a.attachment,a[rel~=\"tag\"]")
|> Floki.attribute("a", "href")
|> Enum.at(0)
diff --git a/lib/pleroma/http/adapter_helper.ex b/lib/pleroma/http/adapter_helper.ex
new file mode 100644
index 000000000..9ec3836b0
--- /dev/null
+++ b/lib/pleroma/http/adapter_helper.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.HTTP.AdapterHelper do
+ @moduledoc """
+ Configure Tesla.Client with default and customized adapter options.
+ """
+ @defaults [pool: :federation]
+
+ @type proxy_type() :: :socks4 | :socks5
+ @type host() :: charlist() | :inet.ip_address()
+
+ alias Pleroma.Config
+ alias Pleroma.HTTP.AdapterHelper
+ require Logger
+
+ @type proxy ::
+ {Connection.host(), pos_integer()}
+ | {Connection.proxy_type(), Connection.host(), pos_integer()}
+
+ @callback options(keyword(), URI.t()) :: keyword()
+ @callback get_conn(URI.t(), keyword()) :: {:ok, term()} | {:error, term()}
+
+ @spec format_proxy(String.t() | tuple() | nil) :: proxy() | nil
+ def format_proxy(nil), do: nil
+
+ def format_proxy(proxy_url) do
+ case parse_proxy(proxy_url) do
+ {:ok, host, port} -> {host, port}
+ {:ok, type, host, port} -> {type, host, port}
+ _ -> nil
+ end
+ end
+
+ @spec maybe_add_proxy(keyword(), proxy() | nil) :: keyword()
+ def maybe_add_proxy(opts, nil), do: opts
+ def maybe_add_proxy(opts, proxy), do: Keyword.put_new(opts, :proxy, proxy)
+
+ @doc """
+ Merge default connection & adapter options with received ones.
+ """
+
+ @spec options(URI.t(), keyword()) :: keyword()
+ def options(%URI{} = uri, opts \\ []) do
+ @defaults
+ |> put_timeout()
+ |> Keyword.merge(opts)
+ |> adapter_helper().options(uri)
+ end
+
+ # For Hackney, this is the time a connection can stay idle in the pool.
+ # For Gun, this is the timeout to receive a message from Gun.
+ defp put_timeout(opts) do
+ {config_key, default} =
+ if adapter() == Tesla.Adapter.Gun do
+ {:pools, Config.get([:pools, :default, :timeout], 5_000)}
+ else
+ {:hackney_pools, 10_000}
+ end
+
+ timeout = Config.get([config_key, opts[:pool], :timeout], default)
+
+ Keyword.merge(opts, timeout: timeout)
+ end
+
+ def get_conn(uri, opts), do: adapter_helper().get_conn(uri, opts)
+ defp adapter, do: Application.get_env(:tesla, :adapter)
+
+ defp adapter_helper do
+ case adapter() do
+ Tesla.Adapter.Gun -> AdapterHelper.Gun
+ Tesla.Adapter.Hackney -> AdapterHelper.Hackney
+ _ -> AdapterHelper.Default
+ end
+ end
+
+ @spec parse_proxy(String.t() | tuple() | nil) ::
+ {:ok, host(), pos_integer()}
+ | {:ok, proxy_type(), host(), pos_integer()}
+ | {:error, atom()}
+ | nil
+
+ def parse_proxy(nil), do: nil
+
+ def parse_proxy(proxy) when is_binary(proxy) do
+ with [host, port] <- String.split(proxy, ":"),
+ {port, ""} <- Integer.parse(port) do
+ {:ok, parse_host(host), port}
+ else
+ {_, _} ->
+ Logger.warn("Parsing port failed #{inspect(proxy)}")
+ {:error, :invalid_proxy_port}
+
+ :error ->
+ Logger.warn("Parsing port failed #{inspect(proxy)}")
+ {:error, :invalid_proxy_port}
+
+ _ ->
+ Logger.warn("Parsing proxy failed #{inspect(proxy)}")
+ {:error, :invalid_proxy}
+ end
+ end
+
+ def parse_proxy(proxy) when is_tuple(proxy) do
+ with {type, host, port} <- proxy do
+ {:ok, type, parse_host(host), port}
+ else
+ _ ->
+ Logger.warn("Parsing proxy failed #{inspect(proxy)}")
+ {:error, :invalid_proxy}
+ end
+ end
+
+ @spec parse_host(String.t() | atom() | charlist()) :: charlist() | :inet.ip_address()
+ def parse_host(host) when is_list(host), do: host
+ def parse_host(host) when is_atom(host), do: to_charlist(host)
+
+ def parse_host(host) when is_binary(host) do
+ host = to_charlist(host)
+
+ case :inet.parse_address(host) do
+ {:error, :einval} -> host
+ {:ok, ip} -> ip
+ end
+ end
+
+ @spec format_host(String.t()) :: charlist()
+ def format_host(host) do
+ host_charlist = to_charlist(host)
+
+ case :inet.parse_address(host_charlist) do
+ {:error, :einval} ->
+ :idna.encode(host_charlist)
+
+ {:ok, _ip} ->
+ host_charlist
+ end
+ end
+end
diff --git a/lib/pleroma/http/adapter_helper/default.ex b/lib/pleroma/http/adapter_helper/default.ex
new file mode 100644
index 000000000..e13441316
--- /dev/null
+++ b/lib/pleroma/http/adapter_helper/default.ex
@@ -0,0 +1,14 @@
+defmodule Pleroma.HTTP.AdapterHelper.Default do
+ alias Pleroma.HTTP.AdapterHelper
+
+ @behaviour Pleroma.HTTP.AdapterHelper
+
+ @spec options(keyword(), URI.t()) :: keyword()
+ def options(opts, _uri) do
+ proxy = Pleroma.Config.get([:http, :proxy_url], nil)
+ AdapterHelper.maybe_add_proxy(opts, AdapterHelper.format_proxy(proxy))
+ end
+
+ @spec get_conn(URI.t(), keyword()) :: {:ok, keyword()}
+ def get_conn(_uri, opts), do: {:ok, opts}
+end
diff --git a/lib/pleroma/http/adapter_helper/gun.ex b/lib/pleroma/http/adapter_helper/gun.ex
new file mode 100644
index 000000000..b4ff8306c
--- /dev/null
+++ b/lib/pleroma/http/adapter_helper/gun.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.HTTP.AdapterHelper.Gun do
+ @behaviour Pleroma.HTTP.AdapterHelper
+
+ alias Pleroma.Gun.ConnectionPool
+ alias Pleroma.HTTP.AdapterHelper
+
+ require Logger
+
+ @defaults [
+ connect_timeout: 5_000,
+ domain_lookup_timeout: 5_000,
+ tls_handshake_timeout: 5_000,
+ retry: 0,
+ retry_timeout: 1000,
+ await_up_timeout: 5_000
+ ]
+
+ @spec options(keyword(), URI.t()) :: keyword()
+ def options(incoming_opts \\ [], %URI{} = uri) do
+ proxy =
+ Pleroma.Config.get([:http, :proxy_url])
+ |> AdapterHelper.format_proxy()
+
+ config_opts = Pleroma.Config.get([:http, :adapter], [])
+
+ @defaults
+ |> Keyword.merge(config_opts)
+ |> add_scheme_opts(uri)
+ |> AdapterHelper.maybe_add_proxy(proxy)
+ |> Keyword.merge(incoming_opts)
+ end
+
+ defp add_scheme_opts(opts, %{scheme: "http"}), do: opts
+
+ defp add_scheme_opts(opts, %{scheme: "https"}) do
+ opts
+ |> Keyword.put(:certificates_verification, true)
+ end
+
+ @spec get_conn(URI.t(), keyword()) :: {:ok, keyword()} | {:error, atom()}
+ def get_conn(uri, opts) do
+ case ConnectionPool.get_conn(uri, opts) do
+ {:ok, conn_pid} -> {:ok, Keyword.merge(opts, conn: conn_pid, close_conn: false)}
+ err -> err
+ end
+ end
+
+ @prefix Pleroma.Gun.ConnectionPool
+ def limiter_setup do
+ wait = Pleroma.Config.get([:connections_pool, :connection_acquisition_wait])
+ retries = Pleroma.Config.get([:connections_pool, :connection_acquisition_retries])
+
+ :pools
+ |> Pleroma.Config.get([])
+ |> Enum.each(fn {name, opts} ->
+ max_running = Keyword.get(opts, :size, 50)
+ max_waiting = Keyword.get(opts, :max_waiting, 10)
+
+ result =
+ ConcurrentLimiter.new(:"#{@prefix}.#{name}", max_running, max_waiting,
+ wait: wait,
+ max_retries: retries
+ )
+
+ case result do
+ :ok -> :ok
+ {:error, :existing} -> :ok
+ e -> raise e
+ end
+ end)
+
+ :ok
+ end
+end
diff --git a/lib/pleroma/http/adapter_helper/hackney.ex b/lib/pleroma/http/adapter_helper/hackney.ex
new file mode 100644
index 000000000..cd569422b
--- /dev/null
+++ b/lib/pleroma/http/adapter_helper/hackney.ex
@@ -0,0 +1,29 @@
+defmodule Pleroma.HTTP.AdapterHelper.Hackney do
+ @behaviour Pleroma.HTTP.AdapterHelper
+
+ @defaults [
+ connect_timeout: 10_000,
+ recv_timeout: 20_000,
+ follow_redirect: true,
+ force_redirect: true,
+ pool: :federation
+ ]
+
+ @spec options(keyword(), URI.t()) :: keyword()
+ def options(connection_opts \\ [], %URI{} = uri) do
+ proxy = Pleroma.Config.get([:http, :proxy_url])
+
+ config_opts = Pleroma.Config.get([:http, :adapter], [])
+
+ @defaults
+ |> Keyword.merge(config_opts)
+ |> Keyword.merge(connection_opts)
+ |> add_scheme_opts(uri)
+ |> Pleroma.HTTP.AdapterHelper.maybe_add_proxy(proxy)
+ end
+
+ defp add_scheme_opts(opts, _), do: opts
+
+ @spec get_conn(URI.t(), keyword()) :: {:ok, keyword()}
+ def get_conn(_uri, opts), do: {:ok, opts}
+end
diff --git a/lib/pleroma/http/connection.ex b/lib/pleroma/http/connection.ex
deleted file mode 100644
index 80e6c30d6..000000000
--- a/lib/pleroma/http/connection.ex
+++ /dev/null
@@ -1,43 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.HTTP.Connection do
- @moduledoc """
- Connection for http-requests.
- """
-
- @hackney_options [
- connect_timeout: 10_000,
- recv_timeout: 20_000,
- follow_redirect: true,
- force_redirect: true,
- pool: :federation
- ]
- @adapter Application.get_env(:tesla, :adapter)
-
- @doc """
- Configure a client connection
-
- # Returns
-
- Tesla.Env.client
- """
- @spec new(Keyword.t()) :: Tesla.Env.client()
- def new(opts \\ []) do
- Tesla.client([], {@adapter, hackney_options(opts)})
- end
-
- # fetch Hackney options
- #
- def hackney_options(opts) do
- options = Keyword.get(opts, :adapter, [])
- adapter_options = Pleroma.Config.get([:http, :adapter], [])
- proxy_url = Pleroma.Config.get([:http, :proxy_url], nil)
-
- @hackney_options
- |> Keyword.merge(adapter_options)
- |> Keyword.merge(options)
- |> Keyword.merge(proxy: proxy_url)
- end
-end
diff --git a/lib/pleroma/http/ex_aws.ex b/lib/pleroma/http/ex_aws.ex
new file mode 100644
index 000000000..e53e64077
--- /dev/null
+++ b/lib/pleroma/http/ex_aws.ex
@@ -0,0 +1,22 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.HTTP.ExAws do
+ @moduledoc false
+
+ @behaviour ExAws.Request.HttpClient
+
+ alias Pleroma.HTTP
+
+ @impl true
+ def request(method, url, body \\ "", headers \\ [], http_opts \\ []) do
+ case HTTP.request(method, url, body, headers, http_opts) do
+ {:ok, env} ->
+ {:ok, %{status_code: env.status, headers: env.headers, body: env.body}}
+
+ {:error, reason} ->
+ {:error, %{reason: reason}}
+ end
+ end
+end
diff --git a/lib/pleroma/http/http.ex b/lib/pleroma/http/http.ex
index ee5b5e127..b37b3fa89 100644
--- a/lib/pleroma/http/http.ex
+++ b/lib/pleroma/http/http.ex
@@ -4,21 +4,51 @@
defmodule Pleroma.HTTP do
@moduledoc """
-
+ Wrapper for `Tesla.request/2`.
"""
- alias Pleroma.HTTP.Connection
+ alias Pleroma.HTTP.AdapterHelper
+ alias Pleroma.HTTP.Request
alias Pleroma.HTTP.RequestBuilder, as: Builder
+ alias Tesla.Client
+ alias Tesla.Env
+
+ require Logger
@type t :: __MODULE__
+ @type method() :: :get | :post | :put | :delete | :head
+
+ @doc """
+ Performs GET request.
+
+ See `Pleroma.HTTP.request/5`
+ """
+ @spec get(Request.url() | nil, Request.headers(), keyword()) ::
+ nil | {:ok, Env.t()} | {:error, any()}
+ def get(url, headers \\ [], options \\ [])
+ def get(nil, _, _), do: nil
+ def get(url, headers, options), do: request(:get, url, "", headers, options)
+
+ @spec head(Request.url(), Request.headers(), keyword()) :: {:ok, Env.t()} | {:error, any()}
+ def head(url, headers \\ [], options \\ []), do: request(:head, url, "", headers, options)
@doc """
- Builds and perform http request.
+ Performs POST request.
+
+ See `Pleroma.HTTP.request/5`
+ """
+ @spec post(Request.url(), String.t(), Request.headers(), keyword()) ::
+ {:ok, Env.t()} | {:error, any()}
+ def post(url, body, headers \\ [], options \\ []),
+ do: request(:post, url, body, headers, options)
+
+ @doc """
+ Builds and performs http request.
# Arguments:
- `method` - :get, :post, :put, :delete
- `url`
- `body`
+ `method` - :get, :post, :put, :delete, :head
+ `url` - full url
+ `body` - request body
`headers` - a keyworld list of headers, e.g. `[{"content-type", "text/plain"}]`
`options` - custom, per-request middleware or adapter options
@@ -26,61 +56,62 @@ defmodule Pleroma.HTTP do
`{:ok, %Tesla.Env{}}` or `{:error, error}`
"""
- def request(method, url, body \\ "", headers \\ [], options \\ []) do
- try do
- options =
- process_request_options(options)
- |> process_sni_options(url)
-
- params = Keyword.get(options, :params, [])
-
- %{}
- |> Builder.method(method)
- |> Builder.headers(headers)
- |> Builder.opts(options)
- |> Builder.url(url)
- |> Builder.add_param(:body, :body, body)
- |> Builder.add_param(:query, :query, params)
- |> Enum.into([])
- |> (&Tesla.request(Connection.new(options), &1)).()
- rescue
- e ->
- {:error, e}
- catch
- :exit, e ->
- {:error, e}
- end
- end
+ @spec request(method(), Request.url(), String.t(), Request.headers(), keyword()) ::
+ {:ok, Env.t()} | {:error, any()}
+ def request(method, url, body, headers, options) when is_binary(url) do
+ uri = URI.parse(url)
+ adapter_opts = AdapterHelper.options(uri, options[:adapter] || [])
- defp process_sni_options(options, nil), do: options
+ case AdapterHelper.get_conn(uri, adapter_opts) do
+ {:ok, adapter_opts} ->
+ options = put_in(options[:adapter], adapter_opts)
+ params = options[:params] || []
+ request = build_request(method, headers, options, url, body, params)
- defp process_sni_options(options, url) do
- uri = URI.parse(url)
- host = uri.host |> to_charlist()
+ adapter = Application.get_env(:tesla, :adapter)
+
+ client = Tesla.client(adapter_middlewares(adapter), adapter)
+
+ maybe_limit(
+ fn ->
+ request(client, request)
+ end,
+ adapter,
+ adapter_opts
+ )
- case uri.scheme do
- "https" -> options ++ [ssl: [server_name_indication: host]]
- _ -> options
+ # Connection release is handled in a custom FollowRedirects middleware
+ err ->
+ err
end
end
- def process_request_options(options) do
- Keyword.merge(Pleroma.HTTP.Connection.hackney_options([]), options)
+ @spec request(Client.t(), keyword()) :: {:ok, Env.t()} | {:error, any()}
+ def request(client, request), do: Tesla.request(client, request)
+
+ defp build_request(method, headers, options, url, body, params) do
+ Builder.new()
+ |> Builder.method(method)
+ |> Builder.headers(headers)
+ |> Builder.opts(options)
+ |> Builder.url(url)
+ |> Builder.add_param(:body, :body, body)
+ |> Builder.add_param(:query, :query, params)
+ |> Builder.convert_to_keyword()
end
- @doc """
- Performs GET request.
+ @prefix Pleroma.Gun.ConnectionPool
+ defp maybe_limit(fun, Tesla.Adapter.Gun, opts) do
+ ConcurrentLimiter.limit(:"#{@prefix}.#{opts[:pool] || :default}", fun)
+ end
- See `Pleroma.HTTP.request/5`
- """
- def get(url, headers \\ [], options \\ []),
- do: request(:get, url, "", headers, options)
+ defp maybe_limit(fun, _, _) do
+ fun.()
+ end
- @doc """
- Performs POST request.
+ defp adapter_middlewares(Tesla.Adapter.Gun) do
+ [Pleroma.HTTP.Middleware.FollowRedirects]
+ end
- See `Pleroma.HTTP.request/5`
- """
- def post(url, body, headers \\ [], options \\ []),
- do: request(:post, url, body, headers, options)
+ defp adapter_middlewares(_), do: []
end
diff --git a/lib/pleroma/http/request.ex b/lib/pleroma/http/request.ex
new file mode 100644
index 000000000..761bd6ccf
--- /dev/null
+++ b/lib/pleroma/http/request.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.HTTP.Request do
+ @moduledoc """
+ Request struct.
+ """
+ defstruct method: :get, url: "", query: [], headers: [], body: "", opts: []
+
+ @type method :: :head | :get | :delete | :trace | :options | :post | :put | :patch
+ @type url :: String.t()
+ @type headers :: [{String.t(), String.t()}]
+
+ @type t :: %__MODULE__{
+ method: method(),
+ url: url(),
+ query: keyword(),
+ headers: headers(),
+ body: String.t(),
+ opts: keyword()
+ }
+end
diff --git a/lib/pleroma/http/request_builder.ex b/lib/pleroma/http/request_builder.ex
index 77ef4bfd8..8a44a001d 100644
--- a/lib/pleroma/http/request_builder.ex
+++ b/lib/pleroma/http/request_builder.ex
@@ -7,136 +7,89 @@ defmodule Pleroma.HTTP.RequestBuilder do
Helper functions for building Tesla requests
"""
- @doc """
- Specify the request method when building a request
-
- ## Parameters
-
- - request (Map) - Collected request options
- - m (atom) - Request method
-
- ## Returns
+ alias Pleroma.HTTP.Request
+ alias Tesla.Multipart
- Map
+ @doc """
+ Creates new request
"""
- @spec method(map(), atom) :: map()
- def method(request, m) do
- Map.put_new(request, :method, m)
- end
+ @spec new(Request.t()) :: Request.t()
+ def new(%Request{} = request \\ %Request{}), do: request
@doc """
Specify the request method when building a request
+ """
+ @spec method(Request.t(), Request.method()) :: Request.t()
+ def method(request, m), do: %{request | method: m}
- ## Parameters
-
- - request (Map) - Collected request options
- - u (String) - Request URL
-
- ## Returns
-
- Map
+ @doc """
+ Specify the request method when building a request
"""
- @spec url(map(), String.t()) :: map()
- def url(request, u) do
- Map.put_new(request, :url, u)
- end
+ @spec url(Request.t(), Request.url()) :: Request.t()
+ def url(request, u), do: %{request | url: u}
@doc """
Add headers to the request
"""
- @spec headers(map(), list(tuple)) :: map()
- def headers(request, header_list) do
- header_list =
- if Pleroma.Config.get([:http, :send_user_agent]) do
- header_list ++ [{"User-Agent", Pleroma.Application.user_agent()}]
+ @spec headers(Request.t(), Request.headers()) :: Request.t()
+ def headers(request, headers) do
+ headers_list =
+ with true <- Pleroma.Config.get([:http, :send_user_agent]),
+ nil <- Enum.find(headers, fn {key, _val} -> String.downcase(key) == "user-agent" end) do
+ [{"user-agent", Pleroma.Application.user_agent()} | headers]
else
- header_list
+ _ ->
+ headers
end
- Map.put_new(request, :headers, header_list)
+ %{request | headers: headers_list}
end
@doc """
Add custom, per-request middleware or adapter options to the request
"""
- @spec opts(map(), Keyword.t()) :: map()
- def opts(request, options) do
- Map.put_new(request, :opts, options)
- end
-
- @doc """
- Add optional parameters to the request
-
- ## Parameters
-
- - request (Map) - Collected request options
- - definitions (Map) - Map of parameter name to parameter location.
- - options (KeywordList) - The provided optional parameters
-
- ## Returns
-
- Map
- """
- @spec add_optional_params(map(), %{optional(atom) => atom}, keyword()) :: map()
- def add_optional_params(request, _, []), do: request
-
- def add_optional_params(request, definitions, [{key, value} | tail]) do
- case definitions do
- %{^key => location} ->
- request
- |> add_param(location, key, value)
- |> add_optional_params(definitions, tail)
-
- _ ->
- add_optional_params(request, definitions, tail)
- end
- end
+ @spec opts(Request.t(), keyword()) :: Request.t()
+ def opts(request, options), do: %{request | opts: options}
@doc """
Add optional parameters to the request
-
- ## Parameters
-
- - request (Map) - Collected request options
- - location (atom) - Where to put the parameter
- - key (atom) - The name of the parameter
- - value (any) - The value of the parameter
-
- ## Returns
-
- Map
"""
- @spec add_param(map(), atom, atom, any()) :: map()
- def add_param(request, :query, :query, values), do: Map.put(request, :query, values)
+ @spec add_param(Request.t(), atom(), atom(), any()) :: Request.t()
+ def add_param(request, :query, :query, values), do: %{request | query: values}
- def add_param(request, :body, :body, value), do: Map.put(request, :body, value)
+ def add_param(request, :body, :body, value), do: %{request | body: value}
def add_param(request, :body, key, value) do
request
- |> Map.put_new_lazy(:body, &Tesla.Multipart.new/0)
+ |> Map.put(:body, Multipart.new())
|> Map.update!(
:body,
- &Tesla.Multipart.add_field(
+ &Multipart.add_field(
&1,
key,
Jason.encode!(value),
- headers: [{:"Content-Type", "application/json"}]
+ headers: [{"content-type", "application/json"}]
)
)
end
def add_param(request, :file, name, path) do
request
- |> Map.put_new_lazy(:body, &Tesla.Multipart.new/0)
- |> Map.update!(:body, &Tesla.Multipart.add_file(&1, path, name: name))
+ |> Map.put(:body, Multipart.new())
+ |> Map.update!(:body, &Multipart.add_file(&1, path, name: name))
end
def add_param(request, :form, name, value) do
- request
- |> Map.update(:body, %{name => value}, &Map.put(&1, name, value))
+ Map.update(request, :body, %{name => value}, &Map.put(&1, name, value))
end
def add_param(request, location, key, value) do
Map.update(request, location, [{key, value}], &(&1 ++ [{key, value}]))
end
+
+ def convert_to_keyword(request) do
+ request
+ |> Map.from_struct()
+ |> Enum.into([])
+ end
end
diff --git a/lib/pleroma/http/tzdata.ex b/lib/pleroma/http/tzdata.ex
new file mode 100644
index 000000000..34bb253a7
--- /dev/null
+++ b/lib/pleroma/http/tzdata.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.HTTP.Tzdata do
+ @moduledoc false
+
+ @behaviour Tzdata.HTTPClient
+
+ alias Pleroma.HTTP
+
+ @impl true
+ def get(url, headers, options) do
+ with {:ok, %Tesla.Env{} = env} <- HTTP.get(url, headers, options) do
+ {:ok, {env.status, env.headers, env.body}}
+ end
+ end
+
+ @impl true
+ def head(url, headers, options) do
+ with {:ok, %Tesla.Env{} = env} <- HTTP.head(url, headers, options) do
+ {:ok, {env.status, env.headers}}
+ end
+ end
+end
diff --git a/lib/pleroma/instances/instance.ex b/lib/pleroma/instances/instance.ex
index 74458c09a..a1f935232 100644
--- a/lib/pleroma/instances/instance.ex
+++ b/lib/pleroma/instances/instance.ex
@@ -17,6 +17,8 @@ defmodule Pleroma.Instances.Instance do
schema "instances" do
field(:host, :string)
field(:unreachable_since, :naive_datetime_usec)
+ field(:favicon, :string)
+ field(:favicon_updated_at, :naive_datetime)
timestamps()
end
@@ -25,7 +27,7 @@ defmodule Pleroma.Instances.Instance do
def changeset(struct, params \\ %{}) do
struct
- |> cast(params, [:host, :unreachable_since])
+ |> cast(params, [:host, :unreachable_since, :favicon, :favicon_updated_at])
|> validate_required([:host])
|> unique_constraint(:host)
end
@@ -120,4 +122,48 @@ defmodule Pleroma.Instances.Instance do
end
defp parse_datetime(datetime), do: datetime
+
+ def get_or_update_favicon(%URI{host: host} = instance_uri) do
+ existing_record = Repo.get_by(Instance, %{host: host})
+ now = NaiveDateTime.utc_now()
+
+ if existing_record && existing_record.favicon_updated_at &&
+ NaiveDateTime.diff(now, existing_record.favicon_updated_at) < 86_400 do
+ existing_record.favicon
+ else
+ favicon = scrape_favicon(instance_uri)
+
+ if existing_record do
+ existing_record
+ |> changeset(%{favicon: favicon, favicon_updated_at: now})
+ |> Repo.update()
+ else
+ %Instance{}
+ |> changeset(%{host: host, favicon: favicon, favicon_updated_at: now})
+ |> Repo.insert()
+ end
+
+ favicon
+ end
+ end
+
+ defp scrape_favicon(%URI{} = instance_uri) do
+ try do
+ with {:ok, %Tesla.Env{body: html}} <-
+ Pleroma.HTTP.get(to_string(instance_uri), [{:Accept, "text/html"}]),
+ favicon_rel <-
+ html
+ |> Floki.parse_document!()
+ |> Floki.attribute("link[rel=icon]", "href")
+ |> List.first(),
+ favicon <- URI.merge(instance_uri, favicon_rel) |> to_string(),
+ true <- is_binary(favicon) do
+ favicon
+ else
+ _ -> nil
+ end
+ rescue
+ _ -> nil
+ end
+ end
end
diff --git a/lib/pleroma/job_queue_monitor.ex b/lib/pleroma/job_queue_monitor.ex
index 2ecf261f3..c255a61ec 100644
--- a/lib/pleroma/job_queue_monitor.ex
+++ b/lib/pleroma/job_queue_monitor.ex
@@ -15,8 +15,8 @@ defmodule Pleroma.JobQueueMonitor do
@impl true
def init(state) do
- :telemetry.attach("oban-monitor-failure", [:oban, :failure], &handle_event/4, nil)
- :telemetry.attach("oban-monitor-success", [:oban, :success], &handle_event/4, nil)
+ :telemetry.attach("oban-monitor-failure", [:oban, :job, :exception], &handle_event/4, nil)
+ :telemetry.attach("oban-monitor-success", [:oban, :job, :stop], &handle_event/4, nil)
{:ok, state}
end
@@ -25,8 +25,11 @@ defmodule Pleroma.JobQueueMonitor do
GenServer.call(__MODULE__, :stats)
end
- def handle_event([:oban, status], %{duration: duration}, meta, _) do
- GenServer.cast(__MODULE__, {:process_event, status, duration, meta})
+ def handle_event([:oban, :job, event], %{duration: duration}, meta, _) do
+ GenServer.cast(
+ __MODULE__,
+ {:process_event, mapping_status(event), duration, meta}
+ )
end
@impl true
@@ -75,4 +78,7 @@ defmodule Pleroma.JobQueueMonitor do
|> Map.update!(:processed_jobs, &(&1 + 1))
|> Map.update!(status, &(&1 + 1))
end
+
+ defp mapping_status(:stop), do: :success
+ defp mapping_status(:exception), do: :failure
end
diff --git a/lib/pleroma/maintenance.ex b/lib/pleroma/maintenance.ex
new file mode 100644
index 000000000..326c17825
--- /dev/null
+++ b/lib/pleroma/maintenance.ex
@@ -0,0 +1,37 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Maintenance do
+ alias Pleroma.Repo
+ require Logger
+
+ def vacuum(args) do
+ case args do
+ "analyze" ->
+ Logger.info("Runnning VACUUM ANALYZE.")
+
+ Repo.query!(
+ "vacuum analyze;",
+ [],
+ timeout: :infinity
+ )
+
+ "full" ->
+ Logger.info("Runnning VACUUM FULL.")
+
+ Logger.warn(
+ "Re-packing your entire database may take a while and will consume extra disk space during the process."
+ )
+
+ Repo.query!(
+ "vacuum full;",
+ [],
+ timeout: :infinity
+ )
+
+ _ ->
+ Logger.error("Error: invalid vacuum argument.")
+ end
+ end
+end
diff --git a/lib/pleroma/maps.ex b/lib/pleroma/maps.ex
new file mode 100644
index 000000000..ab2e32e2f
--- /dev/null
+++ b/lib/pleroma/maps.ex
@@ -0,0 +1,15 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Maps do
+ def put_if_present(map, key, value, value_function \\ &{:ok, &1}) when is_map(map) do
+ with false <- is_nil(key),
+ false <- is_nil(value),
+ {:ok, new_value} <- value_function.(value) do
+ Map.put(map, key, new_value)
+ else
+ _ -> map
+ end
+ end
+end
diff --git a/lib/pleroma/marker.ex b/lib/pleroma/marker.ex
index 443927392..4d82860f5 100644
--- a/lib/pleroma/marker.ex
+++ b/lib/pleroma/marker.ex
@@ -9,24 +9,34 @@ defmodule Pleroma.Marker do
import Ecto.Query
alias Ecto.Multi
+ alias Pleroma.Notification
alias Pleroma.Repo
alias Pleroma.User
+ alias __MODULE__
@timelines ["notifications"]
+ @type t :: %__MODULE__{}
schema "markers" do
field(:last_read_id, :string, default: "")
field(:timeline, :string, default: "")
field(:lock_version, :integer, default: 0)
+ field(:unread_count, :integer, default: 0, virtual: true)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
timestamps()
end
+ @doc "Gets markers by user and timeline."
+ @spec get_markers(User.t(), list(String)) :: list(t())
def get_markers(user, timelines \\ []) do
- Repo.all(get_query(user, timelines))
+ user
+ |> get_query(timelines)
+ |> unread_count_query()
+ |> Repo.all()
end
+ @spec upsert(User.t(), map()) :: {:ok | :error, any()}
def upsert(%User{} = user, attrs) do
attrs
|> Map.take(@timelines)
@@ -45,6 +55,27 @@ defmodule Pleroma.Marker do
|> Repo.transaction()
end
+ @spec multi_set_last_read_id(Multi.t(), User.t(), String.t()) :: Multi.t()
+ def multi_set_last_read_id(multi, %User{} = user, "notifications") do
+ multi
+ |> Multi.run(:counters, fn _repo, _changes ->
+ {:ok, %{last_read_id: Repo.one(Notification.last_read_query(user))}}
+ end)
+ |> Multi.insert(
+ :marker,
+ fn %{counters: attrs} ->
+ %Marker{timeline: "notifications", user_id: user.id}
+ |> struct(attrs)
+ |> Ecto.Changeset.change()
+ end,
+ returning: true,
+ on_conflict: {:replace, [:last_read_id]},
+ conflict_target: [:user_id, :timeline]
+ )
+ end
+
+ def multi_set_last_read_id(multi, _, _), do: multi
+
defp get_marker(user, timeline) do
case Repo.find_resource(get_query(user, timeline)) do
{:ok, marker} -> %__MODULE__{marker | user: user}
@@ -71,4 +102,16 @@ defmodule Pleroma.Marker do
|> by_user_id(user.id)
|> by_timeline(timelines)
end
+
+ defp unread_count_query(query) do
+ from(
+ q in query,
+ left_join: n in "notifications",
+ on: n.user_id == q.user_id and n.seen == false,
+ group_by: [:id],
+ select_merge: %{
+ unread_count: fragment("count(?)", n.id)
+ }
+ )
+ end
end
diff --git a/lib/pleroma/mfa.ex b/lib/pleroma/mfa.ex
new file mode 100644
index 000000000..01b743f4f
--- /dev/null
+++ b/lib/pleroma/mfa.ex
@@ -0,0 +1,155 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.MFA do
+ @moduledoc """
+ The MFA context.
+ """
+
+ alias Pleroma.User
+
+ alias Pleroma.MFA.BackupCodes
+ alias Pleroma.MFA.Changeset
+ alias Pleroma.MFA.Settings
+ alias Pleroma.MFA.TOTP
+
+ @doc """
+ Returns MFA methods the user has enabled.
+
+ ## Examples
+
+ iex> Pleroma.MFA.supported_method(User)
+ "totp, u2f"
+ """
+ @spec supported_methods(User.t()) :: String.t()
+ def supported_methods(user) do
+ settings = fetch_settings(user)
+
+ Settings.mfa_methods()
+ |> Enum.reduce([], fn m, acc ->
+ if method_enabled?(m, settings) do
+ acc ++ [m]
+ else
+ acc
+ end
+ end)
+ |> Enum.join(",")
+ end
+
+ @doc "Checks that user enabled MFA"
+ def require?(user) do
+ fetch_settings(user).enabled
+ end
+
+ @doc """
+ Display MFA settings of user
+ """
+ def mfa_settings(user) do
+ settings = fetch_settings(user)
+
+ Settings.mfa_methods()
+ |> Enum.map(fn m -> [m, method_enabled?(m, settings)] end)
+ |> Enum.into(%{enabled: settings.enabled}, fn [a, b] -> {a, b} end)
+ end
+
+ @doc false
+ def fetch_settings(%User{} = user) do
+ user.multi_factor_authentication_settings || %Settings{}
+ end
+
+ @doc "clears backup codes"
+ def invalidate_backup_code(%User{} = user, hash_code) do
+ %{backup_codes: codes} = fetch_settings(user)
+
+ user
+ |> Changeset.cast_backup_codes(codes -- [hash_code])
+ |> User.update_and_set_cache()
+ end
+
+ @doc "generates backup codes"
+ @spec generate_backup_codes(User.t()) :: {:ok, list(binary)} | {:error, String.t()}
+ def generate_backup_codes(%User{} = user) do
+ with codes <- BackupCodes.generate(),
+ hashed_codes <- Enum.map(codes, &Pbkdf2.hash_pwd_salt/1),
+ changeset <- Changeset.cast_backup_codes(user, hashed_codes),
+ {:ok, _} <- User.update_and_set_cache(changeset) do
+ {:ok, codes}
+ else
+ {:error, msg} ->
+ %{error: msg}
+ end
+ end
+
+ @doc """
+ Generates secret key and set delivery_type to 'app' for TOTP method.
+ """
+ @spec setup_totp(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
+ def setup_totp(user) do
+ user
+ |> Changeset.setup_totp(%{secret: TOTP.generate_secret(), delivery_type: "app"})
+ |> User.update_and_set_cache()
+ end
+
+ @doc """
+ Confirms the TOTP method for user.
+
+ `attrs`:
+ `password` - current user password
+ `code` - TOTP token
+ """
+ @spec confirm_totp(User.t(), map()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t() | atom()}
+ def confirm_totp(%User{} = user, attrs) do
+ with settings <- user.multi_factor_authentication_settings.totp,
+ {:ok, :pass} <- TOTP.validate_token(settings.secret, attrs["code"]) do
+ user
+ |> Changeset.confirm_totp()
+ |> User.update_and_set_cache()
+ end
+ end
+
+ @doc """
+ Disables the TOTP method for user.
+
+ `attrs`:
+ `password` - current user password
+ """
+ @spec disable_totp(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
+ def disable_totp(%User{} = user) do
+ user
+ |> Changeset.disable_totp()
+ |> Changeset.disable()
+ |> User.update_and_set_cache()
+ end
+
+ @doc """
+ Force disables all MFA methods for user.
+ """
+ @spec disable(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
+ def disable(%User{} = user) do
+ user
+ |> Changeset.disable_totp()
+ |> Changeset.disable(true)
+ |> User.update_and_set_cache()
+ end
+
+ @doc """
+ Checks if the user has MFA method enabled.
+ """
+ def method_enabled?(method, settings) do
+ with {:ok, %{confirmed: true} = _} <- Map.fetch(settings, method) do
+ true
+ else
+ _ -> false
+ end
+ end
+
+ @doc """
+ Checks if the user has enabled at least one MFA method.
+ """
+ def enabled?(settings) do
+ Settings.mfa_methods()
+ |> Enum.map(fn m -> method_enabled?(m, settings) end)
+ |> Enum.any?()
+ end
+end
diff --git a/lib/pleroma/mfa/backup_codes.ex b/lib/pleroma/mfa/backup_codes.ex
new file mode 100644
index 000000000..9875310ff
--- /dev/null
+++ b/lib/pleroma/mfa/backup_codes.ex
@@ -0,0 +1,31 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.MFA.BackupCodes do
+ @moduledoc """
+ This module contains functions for generating backup codes.
+ """
+ alias Pleroma.Config
+
+ @config_ns [:instance, :multi_factor_authentication, :backup_codes]
+
+ @doc """
+ Generates backup codes.
+ """
+ @spec generate(Keyword.t()) :: list(String.t())
+ def generate(opts \\ []) do
+ number_of_codes = Keyword.get(opts, :number_of_codes, default_backup_codes_number())
+ code_length = Keyword.get(opts, :length, default_backup_codes_code_length())
+
+ Enum.map(1..number_of_codes, fn _ ->
+ :crypto.strong_rand_bytes(div(code_length, 2))
+ |> Base.encode16(case: :lower)
+ end)
+ end
+
+ defp default_backup_codes_number, do: Config.get(@config_ns ++ [:number], 5)
+
+ defp default_backup_codes_code_length,
+ do: Config.get(@config_ns ++ [:length], 16)
+end
diff --git a/lib/pleroma/mfa/changeset.ex b/lib/pleroma/mfa/changeset.ex
new file mode 100644
index 000000000..77c4fa202
--- /dev/null
+++ b/lib/pleroma/mfa/changeset.ex
@@ -0,0 +1,64 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.MFA.Changeset do
+ alias Pleroma.MFA
+ alias Pleroma.MFA.Settings
+ alias Pleroma.User
+
+ def disable(%Ecto.Changeset{} = changeset, force \\ false) do
+ settings =
+ changeset
+ |> Ecto.Changeset.apply_changes()
+ |> MFA.fetch_settings()
+
+ if force || not MFA.enabled?(settings) do
+ put_change(changeset, %Settings{settings | enabled: false})
+ else
+ changeset
+ end
+ end
+
+ def disable_totp(%User{multi_factor_authentication_settings: settings} = user) do
+ user
+ |> put_change(%Settings{settings | totp: %Settings.TOTP{}})
+ end
+
+ def confirm_totp(%User{multi_factor_authentication_settings: settings} = user) do
+ totp_settings = %Settings.TOTP{settings.totp | confirmed: true}
+
+ user
+ |> put_change(%Settings{settings | totp: totp_settings, enabled: true})
+ end
+
+ def setup_totp(%User{} = user, attrs) do
+ mfa_settings = MFA.fetch_settings(user)
+
+ totp_settings =
+ %Settings.TOTP{}
+ |> Ecto.Changeset.cast(attrs, [:secret, :delivery_type])
+
+ user
+ |> put_change(%Settings{mfa_settings | totp: Ecto.Changeset.apply_changes(totp_settings)})
+ end
+
+ def cast_backup_codes(%User{} = user, codes) do
+ user
+ |> put_change(%Settings{
+ user.multi_factor_authentication_settings
+ | backup_codes: codes
+ })
+ end
+
+ defp put_change(%User{} = user, settings) do
+ user
+ |> Ecto.Changeset.change()
+ |> put_change(settings)
+ end
+
+ defp put_change(%Ecto.Changeset{} = changeset, settings) do
+ changeset
+ |> Ecto.Changeset.put_change(:multi_factor_authentication_settings, settings)
+ end
+end
diff --git a/lib/pleroma/mfa/settings.ex b/lib/pleroma/mfa/settings.ex
new file mode 100644
index 000000000..de6e2228f
--- /dev/null
+++ b/lib/pleroma/mfa/settings.ex
@@ -0,0 +1,24 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.MFA.Settings do
+ use Ecto.Schema
+
+ @primary_key false
+
+ @mfa_methods [:totp]
+ embedded_schema do
+ field(:enabled, :boolean, default: false)
+ field(:backup_codes, {:array, :string}, default: [])
+
+ embeds_one :totp, TOTP, on_replace: :delete, primary_key: false do
+ field(:secret, :string)
+ # app | sms
+ field(:delivery_type, :string, default: "app")
+ field(:confirmed, :boolean, default: false)
+ end
+ end
+
+ def mfa_methods, do: @mfa_methods
+end
diff --git a/lib/pleroma/mfa/token.ex b/lib/pleroma/mfa/token.ex
new file mode 100644
index 000000000..0b2449971
--- /dev/null
+++ b/lib/pleroma/mfa/token.ex
@@ -0,0 +1,106 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.MFA.Token do
+ use Ecto.Schema
+ import Ecto.Query
+ import Ecto.Changeset
+
+ alias Pleroma.Repo
+ alias Pleroma.User
+ alias Pleroma.Web.OAuth.Authorization
+ alias Pleroma.Web.OAuth.Token, as: OAuthToken
+
+ @expires 300
+
+ schema "mfa_tokens" do
+ field(:token, :string)
+ field(:valid_until, :naive_datetime_usec)
+
+ belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
+ belongs_to(:authorization, Authorization)
+
+ timestamps()
+ end
+
+ def get_by_token(token) do
+ from(
+ t in __MODULE__,
+ where: t.token == ^token,
+ preload: [:user, :authorization]
+ )
+ |> Repo.find_resource()
+ end
+
+ def validate(token) do
+ with {:fetch_token, {:ok, token}} <- {:fetch_token, get_by_token(token)},
+ {:expired, false} <- {:expired, is_expired?(token)} do
+ {:ok, token}
+ else
+ {:expired, _} -> {:error, :expired_token}
+ {:fetch_token, _} -> {:error, :not_found}
+ error -> {:error, error}
+ end
+ end
+
+ def create_token(%User{} = user) do
+ %__MODULE__{}
+ |> change
+ |> assign_user(user)
+ |> put_token
+ |> put_valid_until
+ |> Repo.insert()
+ end
+
+ def create_token(user, authorization) do
+ %__MODULE__{}
+ |> change
+ |> assign_user(user)
+ |> assign_authorization(authorization)
+ |> put_token
+ |> put_valid_until
+ |> Repo.insert()
+ end
+
+ defp assign_user(changeset, user) do
+ changeset
+ |> put_assoc(:user, user)
+ |> validate_required([:user])
+ end
+
+ defp assign_authorization(changeset, authorization) do
+ changeset
+ |> put_assoc(:authorization, authorization)
+ |> validate_required([:authorization])
+ end
+
+ defp put_token(changeset) do
+ changeset
+ |> change(%{token: OAuthToken.Utils.generate_token()})
+ |> validate_required([:token])
+ |> unique_constraint(:token)
+ end
+
+ defp put_valid_until(changeset) do
+ expires_in = NaiveDateTime.add(NaiveDateTime.utc_now(), @expires)
+
+ changeset
+ |> change(%{valid_until: expires_in})
+ |> validate_required([:valid_until])
+ end
+
+ def is_expired?(%__MODULE__{valid_until: valid_until}) do
+ NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) > 0
+ end
+
+ def is_expired?(_), do: false
+
+ def delete_expired_tokens do
+ from(
+ q in __MODULE__,
+ where: fragment("?", q.valid_until) < ^Timex.now()
+ )
+ |> Repo.delete_all()
+ end
+end
diff --git a/lib/pleroma/mfa/totp.ex b/lib/pleroma/mfa/totp.ex
new file mode 100644
index 000000000..d2ea2b3aa
--- /dev/null
+++ b/lib/pleroma/mfa/totp.ex
@@ -0,0 +1,86 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.MFA.TOTP do
+ @moduledoc """
+ This module represents functions to create secrets for
+ TOTP Application as well as validate them with a time based token.
+ """
+ alias Pleroma.Config
+
+ @config_ns [:instance, :multi_factor_authentication, :totp]
+
+ @doc """
+ https://github.com/google/google-authenticator/wiki/Key-Uri-Format
+ """
+ def provisioning_uri(secret, label, opts \\ []) do
+ query =
+ %{
+ secret: secret,
+ issuer: Keyword.get(opts, :issuer, default_issuer()),
+ digits: Keyword.get(opts, :digits, default_digits()),
+ period: Keyword.get(opts, :period, default_period())
+ }
+ |> Enum.filter(fn {_, v} -> not is_nil(v) end)
+ |> Enum.into(%{})
+ |> URI.encode_query()
+
+ %URI{scheme: "otpauth", host: "totp", path: "/" <> label, query: query}
+ |> URI.to_string()
+ end
+
+ defp default_period, do: Config.get(@config_ns ++ [:period])
+ defp default_digits, do: Config.get(@config_ns ++ [:digits])
+
+ defp default_issuer,
+ do: Config.get(@config_ns ++ [:issuer], Config.get([:instance, :name]))
+
+ @doc "Creates a random Base 32 encoded string"
+ def generate_secret do
+ Base.encode32(:crypto.strong_rand_bytes(10))
+ end
+
+ @doc "Generates a valid token based on a secret"
+ def generate_token(secret) do
+ :pot.totp(secret)
+ end
+
+ @doc """
+ Validates a given token based on a secret.
+
+ optional parameters:
+ `token_length` default `6`
+ `interval_length` default `30`
+ `window` default 0
+
+ Returns {:ok, :pass} if the token is valid and
+ {:error, :invalid_token} if it is not.
+ """
+ @spec validate_token(String.t(), String.t()) ::
+ {:ok, :pass} | {:error, :invalid_token | :invalid_secret_and_token}
+ def validate_token(secret, token)
+ when is_binary(secret) and is_binary(token) do
+ opts = [
+ token_length: default_digits(),
+ interval_length: default_period()
+ ]
+
+ validate_token(secret, token, opts)
+ end
+
+ def validate_token(_, _), do: {:error, :invalid_secret_and_token}
+
+ @doc "See `validate_token/2`"
+ @spec validate_token(String.t(), String.t(), Keyword.t()) ::
+ {:ok, :pass} | {:error, :invalid_token | :invalid_secret_and_token}
+ def validate_token(secret, token, options)
+ when is_binary(secret) and is_binary(token) do
+ case :pot.valid_totp(token, secret, options) do
+ true -> {:ok, :pass}
+ false -> {:error, :invalid_token}
+ end
+ end
+
+ def validate_token(_, _, _), do: {:error, :invalid_secret_and_token}
+end
diff --git a/lib/pleroma/migration_helper/notification_backfill.ex b/lib/pleroma/migration_helper/notification_backfill.ex
new file mode 100644
index 000000000..d260e62ca
--- /dev/null
+++ b/lib/pleroma/migration_helper/notification_backfill.ex
@@ -0,0 +1,93 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.MigrationHelper.NotificationBackfill do
+ alias Pleroma.Object
+ alias Pleroma.Repo
+ alias Pleroma.User
+
+ import Ecto.Query
+
+ def fill_in_notification_types do
+ query =
+ from(n in Pleroma.Notification,
+ where: is_nil(n.type),
+ preload: :activity
+ )
+
+ query
+ |> Repo.chunk_stream(100)
+ |> Enum.each(fn notification ->
+ type =
+ notification.activity
+ |> type_from_activity()
+
+ notification
+ |> Ecto.Changeset.change(%{type: type})
+ |> Repo.update()
+ end)
+ end
+
+ defp get_by_ap_id(ap_id) do
+ q =
+ from(u in User,
+ select: u.id
+ )
+
+ Repo.get_by(q, ap_id: ap_id)
+ end
+
+ # This is copied over from Notifications to keep this stable.
+ defp type_from_activity(%{data: %{"type" => type}} = activity) do
+ case type do
+ "Follow" ->
+ accepted_function = fn activity ->
+ with %User{} = follower <- get_by_ap_id(activity.data["actor"]),
+ %User{} = followed <- get_by_ap_id(activity.data["object"]) do
+ Pleroma.FollowingRelationship.following?(follower, followed)
+ end
+ end
+
+ if accepted_function.(activity) do
+ "follow"
+ else
+ "follow_request"
+ end
+
+ "Announce" ->
+ "reblog"
+
+ "Like" ->
+ "favourite"
+
+ "Move" ->
+ "move"
+
+ "EmojiReact" ->
+ "pleroma:emoji_reaction"
+
+ # Compatibility with old reactions
+ "EmojiReaction" ->
+ "pleroma:emoji_reaction"
+
+ "Create" ->
+ activity
+ |> type_from_activity_object()
+
+ t ->
+ raise "No notification type for activity type #{t}"
+ end
+ end
+
+ defp type_from_activity_object(%{data: %{"type" => "Create", "object" => %{}}}), do: "mention"
+
+ defp type_from_activity_object(%{data: %{"type" => "Create"}} = activity) do
+ object = Object.get_by_ap_id(activity.data["object"])
+
+ case object && object.data["type"] do
+ "ChatMessage" -> "pleroma:chat_mention"
+ _ -> "mention"
+ end
+ end
+end
diff --git a/lib/pleroma/moderation_log.ex b/lib/pleroma/moderation_log.ex
index 7aacd9d80..31c9afe2a 100644
--- a/lib/pleroma/moderation_log.ex
+++ b/lib/pleroma/moderation_log.ex
@@ -413,6 +413,17 @@ defmodule Pleroma.ModerationLog do
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
+ "action" => "approve",
+ "subject" => users
+ }
+ }) do
+ "@#{actor_nickname} approved users: #{users_to_nicknames_string(users)}"
+ end
+
+ @spec get_log_entry_message(ModerationLog) :: String.t()
+ def get_log_entry_message(%ModerationLog{
+ data: %{
+ "actor" => %{"nickname" => actor_nickname},
"nicknames" => nicknames,
"tags" => tags,
"action" => "tag"
diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex
index 8c6887a6b..c1825f810 100644
--- a/lib/pleroma/notification.ex
+++ b/lib/pleroma/notification.ex
@@ -5,13 +5,17 @@
defmodule Pleroma.Notification do
use Ecto.Schema
+ alias Ecto.Multi
alias Pleroma.Activity
+ alias Pleroma.FollowingRelationship
+ alias Pleroma.Marker
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Pagination
alias Pleroma.Repo
alias Pleroma.ThreadMute
alias Pleroma.User
+ alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.Push
alias Pleroma.Web.Streamer
@@ -27,15 +31,63 @@ defmodule Pleroma.Notification do
schema "notifications" do
field(:seen, :boolean, default: false)
+ # This is an enum type in the database. If you add a new notification type,
+ # remember to add a migration to add it to the `notifications_type` enum
+ # as well.
+ field(:type, :string)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
timestamps()
end
+ def update_notification_type(user, activity) do
+ with %__MODULE__{} = notification <-
+ Repo.get_by(__MODULE__, user_id: user.id, activity_id: activity.id) do
+ type =
+ activity
+ |> type_from_activity()
+
+ notification
+ |> changeset(%{type: type})
+ |> Repo.update()
+ end
+ end
+
+ @spec unread_notifications_count(User.t()) :: integer()
+ def unread_notifications_count(%User{id: user_id}) do
+ from(q in __MODULE__,
+ where: q.user_id == ^user_id and q.seen == false
+ )
+ |> Repo.aggregate(:count, :id)
+ end
+
+ @notification_types ~w{
+ favourite
+ follow
+ follow_request
+ mention
+ move
+ pleroma:chat_mention
+ pleroma:emoji_reaction
+ reblog
+ }
+
def changeset(%Notification{} = notification, attrs) do
notification
- |> cast(attrs, [:seen])
+ |> cast(attrs, [:seen, :type])
+ |> validate_inclusion(:type, @notification_types)
+ end
+
+ @spec last_read_query(User.t()) :: Ecto.Queryable.t()
+ def last_read_query(user) do
+ from(q in Pleroma.Notification,
+ where: q.user_id == ^user.id,
+ where: q.seen == true,
+ select: type(q.id, :string),
+ limit: 1,
+ order_by: [desc: :id]
+ )
end
defp for_user_query_ap_id_opts(user, opts) do
@@ -79,19 +131,17 @@ defmodule Pleroma.Notification do
|> preload([n, a, o], activity: {a, object: o})
|> exclude_notification_muted(user, exclude_notification_muted_opts)
|> exclude_blocked(user, exclude_blocked_opts)
+ |> exclude_filtered(user)
|> exclude_visibility(opts)
- |> exclude_move(opts)
end
+ # Excludes blocked users and non-followed domain-blocked users
defp exclude_blocked(query, user, opts) do
blocked_ap_ids = opts[:blocked_users_ap_ids] || User.blocked_users_ap_ids(user)
query
|> where([n, a], a.actor not in ^blocked_ap_ids)
- |> where(
- [n, a],
- fragment("substring(? from '.*://([^/]*)')", a.actor) not in ^user.domain_blocks
- )
+ |> FollowingRelationship.keep_following_or_not_domain_blocked(user)
end
defp exclude_notification_muted(query, _, %{@include_muted_option => true}) do
@@ -110,12 +160,18 @@ defmodule Pleroma.Notification do
|> where([n, a, o, tm], is_nil(tm.user_id))
end
- defp exclude_move(query, %{with_move: true}) do
- query
- end
+ defp exclude_filtered(query, user) do
+ case Pleroma.Filter.compose_regex(user) do
+ nil ->
+ query
- defp exclude_move(query, _opts) do
- where(query, [n, a], fragment("?->>'type' != 'Move'", a.data))
+ regex ->
+ from([_n, a, o] in query,
+ where:
+ fragment("not(?->>'content' ~* ?)", o.data, ^regex) or
+ fragment("?->>'actor' = ?", o.data, ^user.ap_id)
+ )
+ end
end
@valid_visibilities ~w[direct unlisted public private]
@@ -126,8 +182,16 @@ defmodule Pleroma.Notification do
query
|> join(:left, [n, a], mutated_activity in Pleroma.Activity,
on:
- fragment("?->>'context'", a.data) ==
- fragment("?->>'context'", mutated_activity.data) and
+ fragment(
+ "COALESCE((?->'object')->>'id', ?->>'object')",
+ a.data,
+ a.data
+ ) ==
+ fragment(
+ "COALESCE((?->'object')->>'id', ?->>'object')",
+ mutated_activity.data,
+ mutated_activity.data
+ ) and
fragment("(?->>'type' = 'Like' or ?->>'type' = 'Announce')", a.data, a.data) and
fragment("?->>'type'", mutated_activity.data) == "Create",
as: :mutated_activity
@@ -203,29 +267,34 @@ defmodule Pleroma.Notification do
where: n.user_id == ^user_id,
where: n.id <= ^id,
where: n.seen == false,
- update: [
- set: [
- seen: true,
- updated_at: ^NaiveDateTime.utc_now()
- ]
- ],
# Ideally we would preload object and activities here
# but Ecto does not support preloads in update_all
select: n.id
)
- {_, notification_ids} = Repo.update_all(query, [])
+ {:ok, %{ids: {_, notification_ids}}} =
+ Multi.new()
+ |> Multi.update_all(:ids, query, set: [seen: true, updated_at: NaiveDateTime.utc_now()])
+ |> Marker.multi_set_last_read_id(user, "notifications")
+ |> Repo.transaction()
for_user_query(user)
|> where([n], n.id in ^notification_ids)
|> Repo.all()
end
+ @spec read_one(User.t(), String.t()) ::
+ {:ok, Notification.t()} | {:error, Ecto.Changeset.t()} | nil
def read_one(%User{} = user, notification_id) do
with {:ok, %Notification{} = notification} <- get(user, notification_id) do
- notification
- |> changeset(%{seen: true})
- |> Repo.update()
+ Multi.new()
+ |> Multi.update(:update, changeset(notification, %{seen: true}))
+ |> Marker.multi_set_last_read_id(user, "notifications")
+ |> Repo.transaction()
+ |> case do
+ {:ok, %{update: notification}} -> {:ok, notification}
+ {:error, :update, changeset, _} -> {:error, changeset}
+ end
end
end
@@ -284,41 +353,100 @@ defmodule Pleroma.Notification do
end
end
- def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity) do
- object = Object.normalize(activity)
+ @spec create_notifications(Activity.t(), keyword()) :: {:ok, [Notification.t()] | []}
+ def create_notifications(activity, options \\ [])
+
+ def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity, options) do
+ object = Object.normalize(activity, false)
if object && object.data["type"] == "Answer" do
{:ok, []}
else
- do_create_notifications(activity)
+ do_create_notifications(activity, options)
end
end
- def create_notifications(%Activity{data: %{"type" => type}} = activity)
+ def create_notifications(%Activity{data: %{"type" => type}} = activity, options)
when type in ["Follow", "Like", "Announce", "Move", "EmojiReact"] do
- do_create_notifications(activity)
+ do_create_notifications(activity, options)
end
- def create_notifications(_), do: {:ok, []}
+ def create_notifications(_, _), do: {:ok, []}
+
+ defp do_create_notifications(%Activity{} = activity, options) do
+ do_send = Keyword.get(options, :do_send, true)
- defp do_create_notifications(%Activity{} = activity) do
{enabled_receivers, disabled_receivers} = get_notified_from_activity(activity)
potential_receivers = enabled_receivers ++ disabled_receivers
notifications =
Enum.map(potential_receivers, fn user ->
- do_send = user in enabled_receivers
+ do_send = do_send && user in enabled_receivers
create_notification(activity, user, do_send)
end)
+ |> Enum.reject(&is_nil/1)
{:ok, notifications}
end
+ defp type_from_activity(%{data: %{"type" => type}} = activity) do
+ case type do
+ "Follow" ->
+ if Activity.follow_accepted?(activity) do
+ "follow"
+ else
+ "follow_request"
+ end
+
+ "Announce" ->
+ "reblog"
+
+ "Like" ->
+ "favourite"
+
+ "Move" ->
+ "move"
+
+ "EmojiReact" ->
+ "pleroma:emoji_reaction"
+
+ # Compatibility with old reactions
+ "EmojiReaction" ->
+ "pleroma:emoji_reaction"
+
+ "Create" ->
+ activity
+ |> type_from_activity_object()
+
+ t ->
+ raise "No notification type for activity type #{t}"
+ end
+ end
+
+ defp type_from_activity_object(%{data: %{"type" => "Create", "object" => %{}}}), do: "mention"
+
+ defp type_from_activity_object(%{data: %{"type" => "Create"}} = activity) do
+ object = Object.get_by_ap_id(activity.data["object"])
+
+ case object && object.data["type"] do
+ "ChatMessage" -> "pleroma:chat_mention"
+ _ -> "mention"
+ end
+ end
+
# TODO move to sql, too.
def create_notification(%Activity{} = activity, %User{} = user, do_send \\ true) do
unless skip?(activity, user) do
- notification = %Notification{user_id: user.id, activity: activity}
- {:ok, notification} = Repo.insert(notification)
+ {:ok, %{notification: notification}} =
+ Multi.new()
+ |> Multi.insert(:notification, %Notification{
+ user_id: user.id,
+ activity: activity,
+ seen: mark_as_read?(activity, user),
+ type: type_from_activity(activity)
+ })
+ |> Marker.multi_set_last_read_id(user, "notifications")
+ |> Repo.transaction()
if do_send do
Streamer.stream(["user", "user:notification"], notification)
@@ -331,33 +459,26 @@ defmodule Pleroma.Notification do
@doc """
Returns a tuple with 2 elements:
- {enabled notification receivers, currently disabled receivers (blocking / [thread] muting)}
+ {notification-enabled receivers, currently disabled receivers (blocking / [thread] muting)}
NOTE: might be called for FAKE Activities, see ActivityPub.Utils.get_notified_from_object/1
"""
+ @spec get_notified_from_activity(Activity.t(), boolean()) :: {list(User.t()), list(User.t())}
def get_notified_from_activity(activity, local_only \\ true)
def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only)
when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReact"] do
- potential_receiver_ap_ids =
- []
- |> Utils.maybe_notify_to_recipients(activity)
- |> Utils.maybe_notify_mentioned_recipients(activity)
- |> Utils.maybe_notify_subscribers(activity)
- |> Utils.maybe_notify_followers(activity)
- |> Enum.uniq()
+ potential_receiver_ap_ids = get_potential_receiver_ap_ids(activity)
+
+ potential_receivers =
+ User.get_users_from_set(potential_receiver_ap_ids, local_only: local_only)
- # Since even subscribers and followers can mute / thread-mute, filtering all above AP IDs
notification_enabled_ap_ids =
potential_receiver_ap_ids
+ |> exclude_domain_blocker_ap_ids(activity, potential_receivers)
|> exclude_relationship_restricted_ap_ids(activity)
|> exclude_thread_muter_ap_ids(activity)
- potential_receivers =
- potential_receiver_ap_ids
- |> Enum.uniq()
- |> User.get_users_from_set(local_only)
-
notification_enabled_users =
Enum.filter(potential_receivers, fn u -> u.ap_id in notification_enabled_ap_ids end)
@@ -366,6 +487,63 @@ defmodule Pleroma.Notification do
def get_notified_from_activity(_, _local_only), do: {[], []}
+ # For some activities, only notify the author of the object
+ def get_potential_receiver_ap_ids(%{data: %{"type" => type, "object" => object_id}})
+ when type in ~w{Like Announce EmojiReact} do
+ case Object.get_cached_by_ap_id(object_id) do
+ %Object{data: %{"actor" => actor}} ->
+ [actor]
+
+ _ ->
+ []
+ end
+ end
+
+ def get_potential_receiver_ap_ids(%{data: %{"type" => "Follow", "object" => object_id}}) do
+ [object_id]
+ end
+
+ def get_potential_receiver_ap_ids(activity) do
+ []
+ |> Utils.maybe_notify_to_recipients(activity)
+ |> Utils.maybe_notify_mentioned_recipients(activity)
+ |> Utils.maybe_notify_subscribers(activity)
+ |> Utils.maybe_notify_followers(activity)
+ |> Enum.uniq()
+ end
+
+ @doc "Filters out AP IDs domain-blocking and not following the activity's actor"
+ def exclude_domain_blocker_ap_ids(ap_ids, activity, preloaded_users \\ [])
+
+ def exclude_domain_blocker_ap_ids([], _activity, _preloaded_users), do: []
+
+ def exclude_domain_blocker_ap_ids(ap_ids, %Activity{} = activity, preloaded_users) do
+ activity_actor_domain = activity.actor && URI.parse(activity.actor).host
+
+ users =
+ ap_ids
+ |> Enum.map(fn ap_id ->
+ Enum.find(preloaded_users, &(&1.ap_id == ap_id)) ||
+ User.get_cached_by_ap_id(ap_id)
+ end)
+ |> Enum.filter(& &1)
+
+ domain_blocker_ap_ids = for u <- users, activity_actor_domain in u.domain_blocks, do: u.ap_id
+
+ domain_blocker_follower_ap_ids =
+ if Enum.any?(domain_blocker_ap_ids) do
+ activity
+ |> Activity.user_actor()
+ |> FollowingRelationship.followers_ap_ids(domain_blocker_ap_ids)
+ else
+ []
+ end
+
+ ap_ids
+ |> Kernel.--(domain_blocker_ap_ids)
+ |> Kernel.++(domain_blocker_follower_ap_ids)
+ end
+
@doc "Filters out AP IDs of users basing on their relationships with activity actor user"
def exclude_relationship_restricted_ap_ids([], _activity), do: []
@@ -394,11 +572,10 @@ defmodule Pleroma.Notification do
def skip?(%Activity{} = activity, %User{} = user) do
[
:self,
- :followers,
- :follows,
- :non_followers,
- :non_follows,
- :recently_followed
+ :invisible,
+ :block_from_strangers,
+ :recently_followed,
+ :filtered
]
|> Enum.find(&skip?(&1, activity, user))
end
@@ -410,46 +587,22 @@ defmodule Pleroma.Notification do
activity.data["actor"] == user.ap_id
end
- def skip?(
- :followers,
- %Activity{} = activity,
- %User{notification_settings: %{followers: false}} = user
- ) do
+ def skip?(:invisible, %Activity{} = activity, _) do
actor = activity.data["actor"]
- follower = User.get_cached_by_ap_id(actor)
- User.following?(follower, user)
+ user = User.get_cached_by_ap_id(actor)
+ User.invisible?(user)
end
def skip?(
- :non_followers,
+ :block_from_strangers,
%Activity{} = activity,
- %User{notification_settings: %{non_followers: false}} = user
+ %User{notification_settings: %{block_from_strangers: true}} = user
) do
actor = activity.data["actor"]
follower = User.get_cached_by_ap_id(actor)
!User.following?(follower, user)
end
- def skip?(
- :follows,
- %Activity{} = activity,
- %User{notification_settings: %{follows: false}} = user
- ) do
- actor = activity.data["actor"]
- followed = User.get_cached_by_ap_id(actor)
- User.following?(user, followed)
- end
-
- def skip?(
- :non_follows,
- %Activity{} = activity,
- %User{notification_settings: %{non_follows: false}} = user
- ) do
- actor = activity.data["actor"]
- followed = User.get_cached_by_ap_id(actor)
- !User.following?(user, followed)
- end
-
# To do: consider defining recency in hours and checking FollowingRelationship with a single SQL
def skip?(:recently_followed, %Activity{data: %{"type" => "Follow"}} = activity, %User{} = user) do
actor = activity.data["actor"]
@@ -461,5 +614,38 @@ defmodule Pleroma.Notification do
end)
end
+ def skip?(:filtered, %{data: %{"type" => type}}, _) when type in ["Follow", "Move"], do: false
+
+ def skip?(:filtered, activity, user) do
+ object = Object.normalize(activity)
+
+ cond do
+ is_nil(object) ->
+ false
+
+ object.data["actor"] == user.ap_id ->
+ false
+
+ not is_nil(regex = Pleroma.Filter.compose_regex(user, :re)) ->
+ Regex.match?(regex, object.data["content"])
+
+ true ->
+ false
+ end
+ end
+
def skip?(_, _, _), do: false
+
+ def mark_as_read?(activity, target_user) do
+ user = Activity.user_actor(activity)
+ User.mutes_user?(target_user, user) || CommonAPI.thread_muted?(target_user, activity)
+ end
+
+ def for_user_and_activity(user, activity) do
+ from(n in __MODULE__,
+ where: n.user_id == ^user.id,
+ where: n.activity_id == ^activity.id
+ )
+ |> Repo.one()
+ end
end
diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex
index 9574432f0..052ad413b 100644
--- a/lib/pleroma/object.ex
+++ b/lib/pleroma/object.ex
@@ -9,11 +9,13 @@ defmodule Pleroma.Object do
import Ecto.Changeset
alias Pleroma.Activity
+ alias Pleroma.Config
alias Pleroma.Object
alias Pleroma.Object.Fetcher
alias Pleroma.ObjectTombstone
alias Pleroma.Repo
alias Pleroma.User
+ alias Pleroma.Workers.AttachmentsCleanupWorker
require Logger
@@ -138,12 +140,17 @@ defmodule Pleroma.Object do
def normalize(_, _, _), do: nil
- # Owned objects can only be mutated by their owner
- def authorize_mutation(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}),
- do: actor == ap_id
+ # Owned objects can only be accessed by their owner
+ def authorize_access(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}) do
+ if actor == ap_id do
+ :ok
+ else
+ {:error, :forbidden}
+ end
+ end
- # Legacy objects can be mutated by anybody
- def authorize_mutation(%Object{}, %User{}), do: true
+ # Legacy objects can be accessed by anybody
+ def authorize_access(%Object{}, %User{}), do: :ok
@spec get_cached_by_ap_id(String.t()) :: Object.t() | nil
def get_cached_by_ap_id(ap_id) do
@@ -183,27 +190,37 @@ defmodule Pleroma.Object do
def delete(%Object{data: %{"id" => id}} = object) do
with {:ok, _obj} = swap_object_with_tombstone(object),
deleted_activity = Activity.delete_all_by_object_ap_id(id),
- {:ok, true} <- Cachex.del(:object_cache, "object:#{id}"),
- {:ok, _} <- Cachex.del(:web_resp_cache, URI.parse(id).path) do
- with true <- Pleroma.Config.get([:instance, :cleanup_attachments]) do
- {:ok, _} =
- Pleroma.Workers.AttachmentsCleanupWorker.enqueue("cleanup_attachments", %{
- "object" => object
- })
- end
+ {:ok, _} <- invalid_object_cache(object) do
+ cleanup_attachments(
+ Config.get([:instance, :cleanup_attachments]),
+ %{"object" => object}
+ )
{:ok, object, deleted_activity}
end
end
- def prune(%Object{data: %{"id" => id}} = object) do
+ @spec cleanup_attachments(boolean(), %{required(:object) => map()}) ::
+ {:ok, Oban.Job.t() | nil}
+ def cleanup_attachments(true, %{"object" => _} = params) do
+ AttachmentsCleanupWorker.enqueue("cleanup_attachments", params)
+ end
+
+ def cleanup_attachments(_, _), do: {:ok, nil}
+
+ def prune(%Object{data: %{"id" => _id}} = object) do
with {:ok, object} <- Repo.delete(object),
- {:ok, true} <- Cachex.del(:object_cache, "object:#{id}"),
- {:ok, _} <- Cachex.del(:web_resp_cache, URI.parse(id).path) do
+ {:ok, _} <- invalid_object_cache(object) do
{:ok, object}
end
end
+ def invalid_object_cache(%Object{data: %{"id" => id}}) do
+ with {:ok, true} <- Cachex.del(:object_cache, "object:#{id}") do
+ Cachex.del(:web_resp_cache, URI.parse(id).path)
+ end
+ end
+
def set_cache(%Object{data: %{"id" => ap_id}} = object) do
Cachex.put(:object_cache, "object:#{ap_id}", object)
{:ok, object}
@@ -238,6 +255,10 @@ defmodule Pleroma.Object do
end
end
+ defp poll_is_multiple?(%Object{data: %{"anyOf" => [_ | _]}}), do: true
+
+ defp poll_is_multiple?(_), do: false
+
def decrease_replies_count(ap_id) do
Object
|> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
@@ -261,13 +282,13 @@ defmodule Pleroma.Object do
end
end
- def increase_vote_count(ap_id, name) do
+ def increase_vote_count(ap_id, name, actor) do
with %Object{} = object <- Object.normalize(ap_id),
"Question" <- object.data["type"] do
- multiple = Map.has_key?(object.data, "anyOf")
+ key = if poll_is_multiple?(object), do: "anyOf", else: "oneOf"
options =
- (object.data["anyOf"] || object.data["oneOf"] || [])
+ object.data[key]
|> Enum.map(fn
%{"name" => ^name} = option ->
Kernel.update_in(option["replies"]["totalItems"], &(&1 + 1))
@@ -276,12 +297,12 @@ defmodule Pleroma.Object do
option
end)
+ voters = [actor | object.data["voters"] || []] |> Enum.uniq()
+
data =
- if multiple do
- Map.put(object.data, "anyOf", options)
- else
- Map.put(object.data, "oneOf", options)
- end
+ object.data
+ |> Map.put(key, options)
+ |> Map.put("voters", voters)
object
|> Object.change(%{data: data})
diff --git a/lib/pleroma/object/containment.ex b/lib/pleroma/object/containment.ex
index 9ae6a5600..bc88e8a0c 100644
--- a/lib/pleroma/object/containment.ex
+++ b/lib/pleroma/object/containment.ex
@@ -32,6 +32,18 @@ defmodule Pleroma.Object.Containment do
get_actor(%{"actor" => actor})
end
+ def get_object(%{"object" => id}) when is_binary(id) do
+ id
+ end
+
+ def get_object(%{"object" => %{"id" => id}}) when is_binary(id) do
+ id
+ end
+
+ def get_object(_) do
+ nil
+ end
+
# TODO: We explicitly allow 'tag' URIs through, due to references to legacy OStatus
# objects being present in the test suite environment. Once these objects are
# removed, please also remove this.
@@ -43,7 +55,7 @@ defmodule Pleroma.Object.Containment do
defp compare_uris(_id_uri, _other_uri), do: :error
@doc """
- Checks that an imported AP object's actor matches the domain it came from.
+ Checks that an imported AP object's actor matches the host it came from.
"""
def contain_origin(_id, %{"actor" => nil}), do: :error
diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex
index eaa13d1e7..6fdbc8efd 100644
--- a/lib/pleroma/object/fetcher.ex
+++ b/lib/pleroma/object/fetcher.ex
@@ -9,6 +9,7 @@ defmodule Pleroma.Object.Fetcher do
alias Pleroma.Repo
alias Pleroma.Signature
alias Pleroma.Web.ActivityPub.InternalFetchActor
+ alias Pleroma.Web.ActivityPub.ObjectValidator
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.Federator
@@ -23,21 +24,39 @@ defmodule Pleroma.Object.Fetcher do
Ecto.Changeset.put_change(changeset, :updated_at, updated_at)
end
- defp maybe_reinject_internal_fields(data, %{data: %{} = old_data}) do
+ defp maybe_reinject_internal_fields(%{data: %{} = old_data}, new_data) do
internal_fields = Map.take(old_data, Pleroma.Constants.object_internal_fields())
- Map.merge(data, internal_fields)
+ Map.merge(new_data, internal_fields)
end
- defp maybe_reinject_internal_fields(data, _), do: data
+ defp maybe_reinject_internal_fields(_, new_data), do: new_data
@spec reinject_object(struct(), map()) :: {:ok, Object.t()} | {:error, any()}
- defp reinject_object(struct, data) do
- Logger.debug("Reinjecting object #{data["id"]}")
+ defp reinject_object(%Object{data: %{"type" => "Question"}} = object, new_data) do
+ Logger.debug("Reinjecting object #{new_data["id"]}")
- with data <- Transmogrifier.fix_object(data),
- data <- maybe_reinject_internal_fields(data, struct),
- changeset <- Object.change(struct, %{data: data}),
+ with new_data <- Transmogrifier.fix_object(new_data),
+ data <- maybe_reinject_internal_fields(object, new_data),
+ {:ok, data, _} <- ObjectValidator.validate(data, %{}),
+ changeset <- Object.change(object, %{data: data}),
+ changeset <- touch_changeset(changeset),
+ {:ok, object} <- Repo.insert_or_update(changeset),
+ {:ok, object} <- Object.set_cache(object) do
+ {:ok, object}
+ else
+ e ->
+ Logger.error("Error while processing object: #{inspect(e)}")
+ {:error, e}
+ end
+ end
+
+ defp reinject_object(%Object{} = object, new_data) do
+ Logger.debug("Reinjecting object #{new_data["id"]}")
+
+ with new_data <- Transmogrifier.fix_object(new_data),
+ data <- maybe_reinject_internal_fields(object, new_data),
+ changeset <- Object.change(object, %{data: data}),
changeset <- touch_changeset(changeset),
{:ok, object} <- Repo.insert_or_update(changeset),
{:ok, object} <- Object.set_cache(object) do
@@ -51,8 +70,8 @@ defmodule Pleroma.Object.Fetcher do
def refetch_object(%Object{data: %{"id" => id}} = object) do
with {:local, false} <- {:local, Object.local?(object)},
- {:ok, data} <- fetch_and_contain_remote_object_from_id(id),
- {:ok, object} <- reinject_object(object, data) do
+ {:ok, new_data} <- fetch_and_contain_remote_object_from_id(id),
+ {:ok, object} <- reinject_object(object, new_data) do
{:ok, object}
else
{:local, true} -> {:ok, object}
@@ -83,8 +102,8 @@ defmodule Pleroma.Object.Fetcher do
{:transmogrifier, {:error, {:reject, nil}}} ->
{:reject, nil}
- {:transmogrifier, _} ->
- {:error, "Transmogrifier failure."}
+ {:transmogrifier, _} = e ->
+ {:error, e}
{:object, data, nil} ->
reinject_object(%Object{}, data)
@@ -106,8 +125,8 @@ defmodule Pleroma.Object.Fetcher do
defp prepare_activity_params(data) do
%{
"type" => "Create",
- "to" => data["to"],
- "cc" => data["cc"],
+ "to" => data["to"] || [],
+ "cc" => data["cc"] || [],
# Should we seriously keep this attributedTo thing?
"actor" => data["actor"] || data["attributedTo"],
"object" => data
@@ -124,6 +143,10 @@ defmodule Pleroma.Object.Fetcher do
{:error, "Object has been deleted"} ->
nil
+ {:reject, reason} ->
+ Logger.info("Rejected #{id} while fetching: #{inspect(reason)}")
+ nil
+
e ->
Logger.error("Error while fetching #{id}: #{inspect(e)}")
nil
@@ -141,7 +164,7 @@ defmodule Pleroma.Object.Fetcher do
date: date
})
- [{:Signature, signature}]
+ [{"signature", signature}]
end
defp sign_fetch(headers, id, date) do
@@ -154,7 +177,7 @@ defmodule Pleroma.Object.Fetcher do
defp maybe_date_fetch(headers, date) do
if Pleroma.Config.get([:activitypub, :sign_object_fetches]) do
- headers ++ [{:Date, date}]
+ headers ++ [{"date", date}]
else
headers
end
@@ -166,7 +189,7 @@ defmodule Pleroma.Object.Fetcher do
date = Pleroma.Signature.signed_date()
headers =
- [{:Accept, "application/activity+json"}]
+ [{"accept", "application/activity+json"}]
|> maybe_date_fetch(date)
|> sign_fetch(id, date)
diff --git a/lib/pleroma/otp_version.ex b/lib/pleroma/otp_version.ex
new file mode 100644
index 000000000..114d0054f
--- /dev/null
+++ b/lib/pleroma/otp_version.ex
@@ -0,0 +1,28 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.OTPVersion do
+ @spec version() :: String.t() | nil
+ def version do
+ # OTP Version https://erlang.org/doc/system_principles/versions.html#otp-version
+ [
+ Path.join(:code.root_dir(), "OTP_VERSION"),
+ Path.join([:code.root_dir(), "releases", :erlang.system_info(:otp_release), "OTP_VERSION"])
+ ]
+ |> get_version_from_files()
+ end
+
+ @spec get_version_from_files([Path.t()]) :: String.t() | nil
+ def get_version_from_files([]), do: nil
+
+ def get_version_from_files([path | paths]) do
+ if File.exists?(path) do
+ path
+ |> File.read!()
+ |> String.replace(~r/\r|\n|\s/, "")
+ else
+ get_version_from_files(paths)
+ end
+ end
+end
diff --git a/lib/pleroma/pagination.ex b/lib/pleroma/pagination.ex
index d43a96cd2..9a3795769 100644
--- a/lib/pleroma/pagination.ex
+++ b/lib/pleroma/pagination.ex
@@ -23,12 +23,12 @@ defmodule Pleroma.Pagination do
@spec fetch_paginated(Ecto.Query.t(), map(), type(), atom() | nil) :: [Ecto.Schema.t()]
def fetch_paginated(query, params, type \\ :keyset, table_binding \\ nil)
- def fetch_paginated(query, %{"total" => true} = params, :keyset, table_binding) do
+ def fetch_paginated(query, %{total: true} = params, :keyset, table_binding) do
total = Repo.aggregate(query, :count, :id)
%{
total: total,
- items: fetch_paginated(query, Map.drop(params, ["total"]), :keyset, table_binding)
+ items: fetch_paginated(query, Map.drop(params, [:total]), :keyset, table_binding)
}
end
@@ -41,7 +41,7 @@ defmodule Pleroma.Pagination do
|> enforce_order(options)
end
- def fetch_paginated(query, %{"total" => true} = params, :offset, table_binding) do
+ def fetch_paginated(query, %{total: true} = params, :offset, table_binding) do
total =
query
|> Ecto.Query.exclude(:left_join)
@@ -49,7 +49,7 @@ defmodule Pleroma.Pagination do
%{
total: total,
- items: fetch_paginated(query, Map.drop(params, ["total"]), :offset, table_binding)
+ items: fetch_paginated(query, Map.drop(params, [:total]), :offset, table_binding)
}
end
@@ -64,6 +64,12 @@ defmodule Pleroma.Pagination do
@spec paginate(Ecto.Query.t(), map(), type(), atom() | nil) :: [Ecto.Schema.t()]
def paginate(query, options, method \\ :keyset, table_binding \\ nil)
+ def paginate(list, options, _method, _table_binding) when is_list(list) do
+ offset = options[:offset] || 0
+ limit = options[:limit] || 0
+ Enum.slice(list, offset, limit)
+ end
+
def paginate(query, options, :keyset, table_binding) do
query
|> restrict(:min_id, options, table_binding)
@@ -90,12 +96,6 @@ defmodule Pleroma.Pagination do
skip_order: :boolean
}
- params =
- Enum.reduce(params, %{}, fn
- {key, _value}, acc when is_atom(key) -> Map.drop(acc, [key])
- {key, value}, acc -> Map.put(acc, key, value)
- end)
-
changeset = cast({%{}, param_types}, params, Map.keys(param_types))
changeset.changes
end
diff --git a/lib/pleroma/plugs/admin_secret_authentication_plug.ex b/lib/pleroma/plugs/admin_secret_authentication_plug.ex
index b4b47a31f..2e54df47a 100644
--- a/lib/pleroma/plugs/admin_secret_authentication_plug.ex
+++ b/lib/pleroma/plugs/admin_secret_authentication_plug.ex
@@ -4,6 +4,9 @@
defmodule Pleroma.Plugs.AdminSecretAuthenticationPlug do
import Plug.Conn
+
+ alias Pleroma.Plugs.OAuthScopesPlug
+ alias Pleroma.Plugs.RateLimiter
alias Pleroma.User
def init(options) do
@@ -11,7 +14,10 @@ defmodule Pleroma.Plugs.AdminSecretAuthenticationPlug do
end
def secret_token do
- Pleroma.Config.get(:admin_token)
+ case Pleroma.Config.get(:admin_token) do
+ blank when blank in [nil, ""] -> nil
+ token -> token
+ end
end
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
@@ -26,9 +32,9 @@ defmodule Pleroma.Plugs.AdminSecretAuthenticationPlug do
def authenticate(%{params: %{"admin_token" => admin_token}} = conn) do
if admin_token == secret_token() do
- assign(conn, :user, %User{is_admin: true})
+ assign_admin_user(conn)
else
- conn
+ handle_bad_token(conn)
end
end
@@ -36,8 +42,19 @@ defmodule Pleroma.Plugs.AdminSecretAuthenticationPlug do
token = secret_token()
case get_req_header(conn, "x-admin-token") do
- [^token] -> assign(conn, :user, %User{is_admin: true})
- _ -> conn
+ blank when blank in [[], [""]] -> conn
+ [^token] -> assign_admin_user(conn)
+ _ -> handle_bad_token(conn)
end
end
+
+ defp assign_admin_user(conn) do
+ conn
+ |> assign(:user, %User{is_admin: true})
+ |> OAuthScopesPlug.skip_plug()
+ end
+
+ defp handle_bad_token(conn) do
+ RateLimiter.call(conn, name: :authentication)
+ end
end
diff --git a/lib/pleroma/plugs/auth_expected_plug.ex b/lib/pleroma/plugs/auth_expected_plug.ex
deleted file mode 100644
index f79597dc3..000000000
--- a/lib/pleroma/plugs/auth_expected_plug.ex
+++ /dev/null
@@ -1,17 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Plugs.AuthExpectedPlug do
- import Plug.Conn
-
- def init(options), do: options
-
- def call(conn, _) do
- put_private(conn, :auth_expected, true)
- end
-
- def auth_expected?(conn) do
- conn.private[:auth_expected]
- end
-end
diff --git a/lib/pleroma/plugs/authentication_plug.ex b/lib/pleroma/plugs/authentication_plug.ex
index 0061c69dc..057ea42f1 100644
--- a/lib/pleroma/plugs/authentication_plug.ex
+++ b/lib/pleroma/plugs/authentication_plug.ex
@@ -3,7 +3,6 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.AuthenticationPlug do
- alias Comeonin.Pbkdf2
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
@@ -17,8 +16,13 @@ defmodule Pleroma.Plugs.AuthenticationPlug do
:crypt.crypt(password, password_hash) == password_hash
end
+ def checkpw(password, "$2" <> _ = password_hash) do
+ # Handle bcrypt passwords for Mastodon migration
+ Bcrypt.verify_pass(password, password_hash)
+ end
+
def checkpw(password, "$pbkdf2" <> _ = password_hash) do
- Pbkdf2.checkpw(password, password_hash)
+ Pbkdf2.verify_pass(password, password_hash)
end
def checkpw(_password, _password_hash) do
@@ -26,6 +30,25 @@ defmodule Pleroma.Plugs.AuthenticationPlug do
false
end
+ def maybe_update_password(%User{password_hash: "$2" <> _} = user, password) do
+ do_update_password(user, password)
+ end
+
+ def maybe_update_password(%User{password_hash: "$6" <> _} = user, password) do
+ do_update_password(user, password)
+ end
+
+ def maybe_update_password(user, _), do: {:ok, user}
+
+ defp do_update_password(user, password) do
+ user
+ |> User.password_update_changeset(%{
+ "password" => password,
+ "password_confirmation" => password
+ })
+ |> Pleroma.Repo.update()
+ end
+
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
def call(
@@ -37,7 +60,9 @@ defmodule Pleroma.Plugs.AuthenticationPlug do
} = conn,
_
) do
- if Pbkdf2.checkpw(password, password_hash) do
+ if checkpw(password, password_hash) do
+ {:ok, auth_user} = maybe_update_password(auth_user, password)
+
conn
|> assign(:user, auth_user)
|> OAuthScopesPlug.skip_plug()
@@ -47,7 +72,7 @@ defmodule Pleroma.Plugs.AuthenticationPlug do
end
def call(%{assigns: %{auth_credentials: %{password: _}}} = conn, _) do
- Pbkdf2.dummy_checkpw()
+ Pbkdf2.no_user_verify()
conn
end
diff --git a/lib/pleroma/plugs/ensure_authenticated_plug.ex b/lib/pleroma/plugs/ensure_authenticated_plug.ex
index 6f9b840a9..3fe550806 100644
--- a/lib/pleroma/plugs/ensure_authenticated_plug.ex
+++ b/lib/pleroma/plugs/ensure_authenticated_plug.ex
@@ -5,19 +5,37 @@
defmodule Pleroma.Plugs.EnsureAuthenticatedPlug do
import Plug.Conn
import Pleroma.Web.TranslationHelpers
+
alias Pleroma.User
+ use Pleroma.Web, :plug
+
def init(options) do
options
end
- def call(%{assigns: %{user: %User{}}} = conn, _) do
+ @impl true
+ def perform(
+ %{
+ assigns: %{
+ auth_credentials: %{password: _},
+ user: %User{multi_factor_authentication_settings: %{enabled: true}}
+ }
+ } = conn,
+ _
+ ) do
+ conn
+ |> render_error(:forbidden, "Two-factor authentication enabled, you must use a access token.")
+ |> halt()
+ end
+
+ def perform(%{assigns: %{user: %User{}}} = conn, _) do
conn
end
- def call(conn, _) do
+ def perform(conn, _) do
conn
|> render_error(:forbidden, "Invalid credentials.")
- |> halt
+ |> halt()
end
end
diff --git a/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex b/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex
index d980ff13d..7265bb87a 100644
--- a/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex
+++ b/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex
@@ -5,14 +5,18 @@
defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug do
import Pleroma.Web.TranslationHelpers
import Plug.Conn
+
alias Pleroma.Config
alias Pleroma.User
+ use Pleroma.Web, :plug
+
def init(options) do
options
end
- def call(conn, _) do
+ @impl true
+ def perform(conn, _) do
public? = Config.get!([:instance, :public])
case {public?, conn} do
diff --git a/lib/pleroma/plugs/expect_authenticated_check_plug.ex b/lib/pleroma/plugs/expect_authenticated_check_plug.ex
new file mode 100644
index 000000000..66b8d5de5
--- /dev/null
+++ b/lib/pleroma/plugs/expect_authenticated_check_plug.ex
@@ -0,0 +1,20 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Plugs.ExpectAuthenticatedCheckPlug do
+ @moduledoc """
+ Marks `Pleroma.Plugs.EnsureAuthenticatedPlug` as expected to be executed later in plug chain.
+
+ No-op plug which affects `Pleroma.Web` operation (is checked with `PlugHelper.plug_called?/2`).
+ """
+
+ use Pleroma.Web, :plug
+
+ def init(options), do: options
+
+ @impl true
+ def perform(conn, _) do
+ conn
+ end
+end
diff --git a/lib/pleroma/plugs/expect_public_or_authenticated_check_plug.ex b/lib/pleroma/plugs/expect_public_or_authenticated_check_plug.ex
new file mode 100644
index 000000000..ba0ef76bd
--- /dev/null
+++ b/lib/pleroma/plugs/expect_public_or_authenticated_check_plug.ex
@@ -0,0 +1,21 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug do
+ @moduledoc """
+ Marks `Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug` as expected to be executed later in plug
+ chain.
+
+ No-op plug which affects `Pleroma.Web` operation (is checked with `PlugHelper.plug_called?/2`).
+ """
+
+ use Pleroma.Web, :plug
+
+ def init(options), do: options
+
+ @impl true
+ def perform(conn, _) do
+ conn
+ end
+end
diff --git a/lib/pleroma/plugs/federating_plug.ex b/lib/pleroma/plugs/federating_plug.ex
index d3943586d..09038f3c6 100644
--- a/lib/pleroma/plugs/federating_plug.ex
+++ b/lib/pleroma/plugs/federating_plug.ex
@@ -10,14 +10,23 @@ defmodule Pleroma.Web.FederatingPlug do
end
def call(conn, _opts) do
- if Pleroma.Config.get([:instance, :federating]) do
+ if federating?() do
conn
else
- conn
- |> put_status(404)
- |> Phoenix.Controller.put_view(Pleroma.Web.ErrorView)
- |> Phoenix.Controller.render("404.json")
- |> halt()
+ fail(conn)
end
end
+
+ def federating?, do: Pleroma.Config.get([:instance, :federating])
+
+ # Definition for the use in :if_func / :unless_func plug options
+ def federating?(_conn), do: federating?()
+
+ defp fail(conn) do
+ conn
+ |> put_status(404)
+ |> Phoenix.Controller.put_view(Pleroma.Web.ErrorView)
+ |> Phoenix.Controller.render("404.json")
+ |> halt()
+ end
end
diff --git a/lib/pleroma/plugs/frontend_static.ex b/lib/pleroma/plugs/frontend_static.ex
new file mode 100644
index 000000000..11a0d5382
--- /dev/null
+++ b/lib/pleroma/plugs/frontend_static.ex
@@ -0,0 +1,55 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Plugs.FrontendStatic do
+ require Pleroma.Constants
+
+ @moduledoc """
+ This is a shim to call `Plug.Static` but with runtime `from` configuration`. It dispatches to the different frontends.
+ """
+ @behaviour Plug
+
+ def file_path(path, frontend_type \\ :primary) do
+ if configuration = Pleroma.Config.get([:frontends, frontend_type]) do
+ instance_static_path = Pleroma.Config.get([:instance, :static_dir], "instance/static")
+
+ Path.join([
+ instance_static_path,
+ "frontends",
+ configuration["name"],
+ configuration["ref"],
+ path
+ ])
+ else
+ nil
+ end
+ end
+
+ def init(opts) do
+ opts
+ |> Keyword.put(:from, "__unconfigured_frontend_static_plug")
+ |> Plug.Static.init()
+ |> Map.put(:frontend_type, opts[:frontend_type])
+ end
+
+ def call(conn, opts) do
+ frontend_type = Map.get(opts, :frontend_type, :primary)
+ path = file_path("", frontend_type)
+
+ if path do
+ conn
+ |> call_static(opts, path)
+ else
+ conn
+ end
+ end
+
+ defp call_static(conn, opts, from) do
+ opts =
+ opts
+ |> Map.put(:from, from)
+
+ Plug.Static.call(conn, opts)
+ end
+end
diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex
index cad0ad4a0..c363b193b 100644
--- a/lib/pleroma/plugs/http_security_plug.ex
+++ b/lib/pleroma/plugs/http_security_plug.ex
@@ -69,26 +69,27 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do
img_src = "img-src 'self' data: blob:"
media_src = "media-src 'self'"
+ # Strict multimedia CSP enforcement only when MediaProxy is enabled
{img_src, media_src} =
if Config.get([:media_proxy, :enabled]) &&
!Config.get([:media_proxy, :proxy_opts, :redirect_on_failure]) do
- sources = get_proxy_and_attachment_sources()
+ sources = build_csp_multimedia_source_list()
{[img_src, sources], [media_src, sources]}
else
{[img_src, " https:"], [media_src, " https:"]}
end
- connect_src = ["connect-src 'self' ", static_url, ?\s, websocket_url]
+ connect_src = ["connect-src 'self' blob: ", static_url, ?\s, websocket_url]
connect_src =
- if Pleroma.Config.get(:env) == :dev do
+ if Config.get(:env) == :dev do
[connect_src, " http://localhost:3035/"]
else
connect_src
end
script_src =
- if Pleroma.Config.get(:env) == :dev do
+ if Config.get(:env) == :dev do
"script-src 'self' 'unsafe-eval'"
else
"script-src 'self'"
@@ -107,38 +108,64 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do
|> :erlang.iolist_to_binary()
end
- defp get_proxy_and_attachment_sources do
+ defp build_csp_from_whitelist([], acc), do: acc
+
+ defp build_csp_from_whitelist([last], acc) do
+ [build_csp_param_from_whitelist(last) | acc]
+ end
+
+ defp build_csp_from_whitelist([head | tail], acc) do
+ build_csp_from_whitelist(tail, [[?\s, build_csp_param_from_whitelist(head)] | acc])
+ end
+
+ # TODO: use `build_csp_param/1` after removing support bare domains for media proxy whitelist
+ defp build_csp_param_from_whitelist("http" <> _ = url) do
+ build_csp_param(url)
+ end
+
+ defp build_csp_param_from_whitelist(url), do: url
+
+ defp build_csp_multimedia_source_list do
media_proxy_whitelist =
- Enum.reduce(Config.get([:media_proxy, :whitelist]), [], fn host, acc ->
- add_source(acc, host)
- end)
-
- media_proxy_base_url =
- if Config.get([:media_proxy, :base_url]),
- do: URI.parse(Config.get([:media_proxy, :base_url])).host
-
- upload_base_url =
- if Config.get([Pleroma.Upload, :base_url]),
- do: URI.parse(Config.get([Pleroma.Upload, :base_url])).host
-
- s3_endpoint =
- if Config.get([Pleroma.Upload, :uploader]) == Pleroma.Uploaders.S3,
- do: URI.parse(Config.get([Pleroma.Uploaders.S3, :public_endpoint])).host
-
- []
- |> add_source(media_proxy_base_url)
- |> add_source(upload_base_url)
- |> add_source(s3_endpoint)
+ [:media_proxy, :whitelist]
+ |> Config.get()
+ |> build_csp_from_whitelist([])
+
+ captcha_method = Config.get([Pleroma.Captcha, :method])
+ captcha_endpoint = Config.get([captcha_method, :endpoint])
+
+ base_endpoints =
+ [
+ [:media_proxy, :base_url],
+ [Pleroma.Upload, :base_url],
+ [Pleroma.Uploaders.S3, :public_endpoint]
+ ]
+ |> Enum.map(&Config.get/1)
+
+ [captcha_endpoint | base_endpoints]
+ |> Enum.map(&build_csp_param/1)
+ |> Enum.reduce([], &add_source(&2, &1))
|> add_source(media_proxy_whitelist)
end
defp add_source(iodata, nil), do: iodata
+ defp add_source(iodata, []), do: iodata
defp add_source(iodata, source), do: [[?\s, source] | iodata]
defp add_csp_param(csp_iodata, nil), do: csp_iodata
defp add_csp_param(csp_iodata, param), do: [[param, ?;] | csp_iodata]
+ defp build_csp_param(nil), do: nil
+
+ defp build_csp_param(url) when is_binary(url) do
+ %{host: host, scheme: scheme} = URI.parse(url)
+
+ if scheme do
+ [scheme, "://", host]
+ end
+ end
+
def warn_if_disabled do
unless Config.get([:http_security, :enabled]) do
Logger.warn("
diff --git a/lib/pleroma/plugs/instance_static.ex b/lib/pleroma/plugs/instance_static.ex
index 7516f75c3..0fb57e422 100644
--- a/lib/pleroma/plugs/instance_static.ex
+++ b/lib/pleroma/plugs/instance_static.ex
@@ -16,28 +16,24 @@ defmodule Pleroma.Plugs.InstanceStatic do
instance_path =
Path.join(Pleroma.Config.get([:instance, :static_dir], "instance/static/"), path)
- if File.exists?(instance_path) do
- instance_path
- else
+ frontend_path = Pleroma.Plugs.FrontendStatic.file_path(path, :primary)
+
+ (File.exists?(instance_path) && instance_path) ||
+ (frontend_path && File.exists?(frontend_path) && frontend_path) ||
Path.join(Application.app_dir(:pleroma, "priv/static/"), path)
- end
end
def init(opts) do
opts
|> Keyword.put(:from, "__unconfigured_instance_static_plug")
- |> Keyword.put(:at, "/__unconfigured_instance_static_plug")
|> Plug.Static.init()
end
for only <- Pleroma.Constants.static_only_files() do
- at = Plug.Router.Utils.split("/")
-
def call(%{request_path: "/" <> unquote(only) <> _} = conn, opts) do
call_static(
conn,
opts,
- unquote(at),
Pleroma.Config.get([:instance, :static_dir], "instance/static")
)
end
@@ -47,11 +43,10 @@ defmodule Pleroma.Plugs.InstanceStatic do
conn
end
- defp call_static(conn, opts, at, from) do
+ defp call_static(conn, opts, from) do
opts =
opts
|> Map.put(:from, from)
- |> Map.put(:at, at)
Plug.Static.call(conn, opts)
end
diff --git a/lib/pleroma/plugs/mapped_signature_to_identity_plug.ex b/lib/pleroma/plugs/mapped_signature_to_identity_plug.ex
index 4cc93adb0..f44d4dee5 100644
--- a/lib/pleroma/plugs/mapped_signature_to_identity_plug.ex
+++ b/lib/pleroma/plugs/mapped_signature_to_identity_plug.ex
@@ -43,13 +43,13 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do
else
{:user_match, false} ->
Logger.debug("Failed to map identity from signature (payload actor mismatch)")
- Logger.debug("key_id=#{key_id_from_conn(conn)}, actor=#{actor}")
+ Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{inspect(actor)}")
assign(conn, :valid_signature, false)
# remove me once testsuite uses mapped capabilities instead of what we do now
{:user, nil} ->
Logger.debug("Failed to map identity from signature (lookup failure)")
- Logger.debug("key_id=#{key_id_from_conn(conn)}, actor=#{actor}")
+ Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{actor}")
conn
end
end
@@ -61,7 +61,7 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do
else
_ ->
Logger.debug("Failed to map identity from signature (no payload actor mismatch)")
- Logger.debug("key_id=#{key_id_from_conn(conn)}")
+ Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}")
assign(conn, :valid_signature, false)
end
end
diff --git a/lib/pleroma/plugs/oauth_scopes_plug.ex b/lib/pleroma/plugs/oauth_scopes_plug.ex
index 66f48c28c..efc25b79f 100644
--- a/lib/pleroma/plugs/oauth_scopes_plug.ex
+++ b/lib/pleroma/plugs/oauth_scopes_plug.ex
@@ -7,15 +7,12 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do
import Pleroma.Web.Gettext
alias Pleroma.Config
- alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
- alias Pleroma.Plugs.PlugHelper
use Pleroma.Web, :plug
- @behaviour Plug
-
def init(%{scopes: _} = options), do: options
+ @impl true
def perform(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do
op = options[:op] || :|
token = assigns[:token]
@@ -31,10 +28,7 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do
conn
options[:fallback] == :proceed_unauthenticated ->
- conn
- |> assign(:user, nil)
- |> assign(:token, nil)
- |> maybe_perform_instance_privacy_check(options)
+ drop_auth_info(conn)
true ->
missing_scopes = scopes -- matched_scopes
@@ -50,6 +44,15 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do
end
end
+ @doc "Drops authentication info from connection"
+ def drop_auth_info(conn) do
+ # To simplify debugging, setting a private variable on `conn` if auth info is dropped
+ conn
+ |> put_private(:authentication_ignored, true)
+ |> assign(:user, nil)
+ |> assign(:token, nil)
+ end
+
@doc "Filters descendants of supported scopes"
def filter_descendants(scopes, supported_scopes) do
Enum.filter(
@@ -71,12 +74,4 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do
scopes
end
end
-
- defp maybe_perform_instance_privacy_check(%Plug.Conn{} = conn, options) do
- if options[:skip_instance_privacy_check] do
- conn
- else
- EnsurePublicOrAuthenticatedPlug.call(conn, [])
- end
- end
end
diff --git a/lib/pleroma/plugs/rate_limiter/rate_limiter.ex b/lib/pleroma/plugs/rate_limiter/rate_limiter.ex
index 1529da717..c51e2c634 100644
--- a/lib/pleroma/plugs/rate_limiter/rate_limiter.ex
+++ b/lib/pleroma/plugs/rate_limiter/rate_limiter.ex
@@ -110,20 +110,9 @@ defmodule Pleroma.Plugs.RateLimiter do
end
def disabled?(conn) do
- localhost_or_socket =
- case Config.get([Pleroma.Web.Endpoint, :http, :ip]) do
- {127, 0, 0, 1} -> true
- {0, 0, 0, 0, 0, 0, 0, 1} -> true
- {:local, _} -> true
- _ -> false
- end
-
- remote_ip_not_found =
- if Map.has_key?(conn.assigns, :remote_ip_found),
- do: !conn.assigns.remote_ip_found,
- else: false
-
- localhost_or_socket and remote_ip_not_found
+ if Map.has_key?(conn.assigns, :remote_ip_found),
+ do: !conn.assigns.remote_ip_found,
+ else: false
end
@inspect_bucket_not_found {:error, :not_found}
diff --git a/lib/pleroma/plugs/static_fe_plug.ex b/lib/pleroma/plugs/static_fe_plug.ex
index 156e6788e..143665c71 100644
--- a/lib/pleroma/plugs/static_fe_plug.ex
+++ b/lib/pleroma/plugs/static_fe_plug.ex
@@ -9,7 +9,7 @@ defmodule Pleroma.Plugs.StaticFEPlug do
def init(options), do: options
def call(conn, _) do
- if enabled?() and accepts_html?(conn) do
+ if enabled?() and requires_html?(conn) do
conn
|> StaticFEController.call(:show)
|> halt()
@@ -20,10 +20,7 @@ defmodule Pleroma.Plugs.StaticFEPlug do
defp enabled?, do: Pleroma.Config.get([:static_fe, :enabled], false)
- defp accepts_html?(conn) do
- case get_req_header(conn, "accept") do
- [accept | _] -> String.contains?(accept, "text/html")
- _ -> false
- end
+ defp requires_html?(conn) do
+ Phoenix.Controller.get_format(conn) == "html"
end
end
diff --git a/lib/pleroma/plugs/uploaded_media.ex b/lib/pleroma/plugs/uploaded_media.ex
index 94147e0c4..40984cfc0 100644
--- a/lib/pleroma/plugs/uploaded_media.ex
+++ b/lib/pleroma/plugs/uploaded_media.ex
@@ -10,6 +10,8 @@ defmodule Pleroma.Plugs.UploadedMedia do
import Pleroma.Web.Gettext
require Logger
+ alias Pleroma.Web.MediaProxy
+
@behaviour Plug
# no slashes
@path "media"
@@ -35,8 +37,7 @@ defmodule Pleroma.Plugs.UploadedMedia do
%{query_params: %{"name" => name}} = conn ->
name = String.replace(name, "\"", "\\\"")
- conn
- |> put_resp_header("content-disposition", "filename=\"#{name}\"")
+ put_resp_header(conn, "content-disposition", "filename=\"#{name}\"")
conn ->
conn
@@ -47,7 +48,8 @@ defmodule Pleroma.Plugs.UploadedMedia do
with uploader <- Keyword.fetch!(config, :uploader),
proxy_remote = Keyword.get(config, :proxy_remote, false),
- {:ok, get_method} <- uploader.get_file(file) do
+ {:ok, get_method} <- uploader.get_file(file),
+ false <- media_is_banned(conn, get_method) do
get_media(conn, get_method, proxy_remote, opts)
else
_ ->
@@ -59,6 +61,14 @@ defmodule Pleroma.Plugs.UploadedMedia do
def call(conn, _opts), do: conn
+ defp media_is_banned(%{request_path: path} = _conn, {:static_dir, _}) do
+ MediaProxy.in_banned_urls(Pleroma.Web.base_url() <> path)
+ end
+
+ defp media_is_banned(_, {:url, url}), do: MediaProxy.in_banned_urls(url)
+
+ defp media_is_banned(_, _), do: false
+
defp get_media(conn, {:static_dir, directory}, _, opts) do
static_opts =
Map.get(opts, :static_plug_opts)
diff --git a/lib/pleroma/plugs/user_is_admin_plug.ex b/lib/pleroma/plugs/user_is_admin_plug.ex
index 2748102df..488a61d1d 100644
--- a/lib/pleroma/plugs/user_is_admin_plug.ex
+++ b/lib/pleroma/plugs/user_is_admin_plug.ex
@@ -7,37 +7,18 @@ defmodule Pleroma.Plugs.UserIsAdminPlug do
import Plug.Conn
alias Pleroma.User
- alias Pleroma.Web.OAuth
def init(options) do
options
end
- def call(%{assigns: %{user: %User{is_admin: true}} = assigns} = conn, _) do
- token = assigns[:token]
-
- cond do
- not Pleroma.Config.enforce_oauth_admin_scope_usage?() ->
- conn
-
- token && OAuth.Scopes.contains_admin_scopes?(token.scopes) ->
- # Note: checking for _any_ admin scope presence, not necessarily fitting requested action.
- # Thus, controller must explicitly invoke OAuthScopesPlug to verify scope requirements.
- # Admin might opt out of admin scope for some apps to block any admin actions from them.
- conn
-
- true ->
- fail(conn)
- end
+ def call(%{assigns: %{user: %User{is_admin: true}}} = conn, _) do
+ conn
end
def call(conn, _) do
- fail(conn)
- end
-
- defp fail(conn) do
conn
- |> render_error(:forbidden, "User is not an admin or OAuth admin scope is not granted.")
+ |> render_error(:forbidden, "User is not an admin.")
|> halt()
end
end
diff --git a/lib/pleroma/repo.ex b/lib/pleroma/repo.ex
index f62138466..f317e4d58 100644
--- a/lib/pleroma/repo.ex
+++ b/lib/pleroma/repo.ex
@@ -8,11 +8,10 @@ defmodule Pleroma.Repo do
adapter: Ecto.Adapters.Postgres,
migration_timestamps: [type: :naive_datetime_usec]
+ import Ecto.Query
require Logger
- defmodule Instrumenter do
- use Prometheus.EctoInstrumenter
- end
+ defmodule Instrumenter, do: use(Prometheus.EctoInstrumenter)
@doc """
Dynamically loads the repository url from the
@@ -50,36 +49,30 @@ defmodule Pleroma.Repo do
end
end
- def check_migrations_applied!() do
- unless Pleroma.Config.get(
- [:i_am_aware_this_may_cause_data_loss, :disable_migration_check],
- false
- ) do
- Ecto.Migrator.with_repo(__MODULE__, fn repo ->
- down_migrations =
- Ecto.Migrator.migrations(repo)
- |> Enum.reject(fn
- {:up, _, _} -> true
- {:down, _, _} -> false
- end)
-
- if length(down_migrations) > 0 do
- down_migrations_text =
- Enum.map(down_migrations, fn {:down, id, name} -> "- #{name} (#{id})\n" end)
-
- Logger.error(
- "The following migrations were not applied:\n#{down_migrations_text}If you want to start Pleroma anyway, set\nconfig :pleroma, :i_am_aware_this_may_cause_data_loss, disable_migration_check: true"
- )
+ def chunk_stream(query, chunk_size) do
+ # We don't actually need start and end funcitons of resource streaming,
+ # but it seems to be the only way to not fetch records one-by-one and
+ # have individual records be the elements of the stream, instead of
+ # lists of records
+ Stream.resource(
+ fn -> 0 end,
+ fn
+ last_id ->
+ query
+ |> order_by(asc: :id)
+ |> where([r], r.id > ^last_id)
+ |> limit(^chunk_size)
+ |> all()
+ |> case do
+ [] ->
+ {:halt, last_id}
- raise Pleroma.Repo.UnappliedMigrationsError
- end
- end)
- else
- :ok
- end
+ records ->
+ last_id = List.last(records).id
+ {records, last_id}
+ end
+ end,
+ fn _ -> :ok end
+ )
end
end
-
-defmodule Pleroma.Repo.UnappliedMigrationsError do
- defexception message: "Unapplied Migrations detected"
-end
diff --git a/lib/pleroma/reverse_proxy/client.ex b/lib/pleroma/reverse_proxy/client.ex
index 26d14fabd..0d13ff174 100644
--- a/lib/pleroma/reverse_proxy/client.ex
+++ b/lib/pleroma/reverse_proxy/client.ex
@@ -3,19 +3,23 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ReverseProxy.Client do
- @callback request(atom(), String.t(), [tuple()], String.t(), list()) ::
- {:ok, pos_integer(), [tuple()], reference() | map()}
- | {:ok, pos_integer(), [tuple()]}
+ @type status :: pos_integer()
+ @type header_name :: String.t()
+ @type header_value :: String.t()
+ @type headers :: [{header_name(), header_value()}]
+
+ @callback request(atom(), String.t(), headers(), String.t(), list()) ::
+ {:ok, status(), headers(), reference() | map()}
+ | {:ok, status(), headers()}
| {:ok, reference()}
| {:error, term()}
- @callback stream_body(reference() | pid() | map()) ::
- {:ok, binary()} | :done | {:error, String.t()}
+ @callback stream_body(map()) :: {:ok, binary(), map()} | :done | {:error, atom() | String.t()}
@callback close(reference() | pid() | map()) :: :ok
- def request(method, url, headers, "", opts \\ []) do
- client().request(method, url, headers, "", opts)
+ def request(method, url, headers, body \\ "", opts \\ []) do
+ client().request(method, url, headers, body, opts)
end
def stream_body(ref), do: client().stream_body(ref)
@@ -23,6 +27,12 @@ defmodule Pleroma.ReverseProxy.Client do
def close(ref), do: client().close(ref)
defp client do
- Pleroma.Config.get([Pleroma.ReverseProxy.Client], :hackney)
+ :tesla
+ |> Application.get_env(:adapter)
+ |> client()
end
+
+ defp client(Tesla.Adapter.Hackney), do: Pleroma.ReverseProxy.Client.Hackney
+ defp client(Tesla.Adapter.Gun), do: Pleroma.ReverseProxy.Client.Tesla
+ defp client(_), do: Pleroma.Config.get!(Pleroma.ReverseProxy.Client)
end
diff --git a/lib/pleroma/reverse_proxy/client/hackney.ex b/lib/pleroma/reverse_proxy/client/hackney.ex
new file mode 100644
index 000000000..e84118a90
--- /dev/null
+++ b/lib/pleroma/reverse_proxy/client/hackney.ex
@@ -0,0 +1,24 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.ReverseProxy.Client.Hackney do
+ @behaviour Pleroma.ReverseProxy.Client
+
+ @impl true
+ def request(method, url, headers, body, opts \\ []) do
+ :hackney.request(method, url, headers, body, opts)
+ end
+
+ @impl true
+ def stream_body(ref) do
+ case :hackney.stream_body(ref) do
+ :done -> :done
+ {:ok, data} -> {:ok, data, ref}
+ {:error, error} -> {:error, error}
+ end
+ end
+
+ @impl true
+ def close(ref), do: :hackney.close(ref)
+end
diff --git a/lib/pleroma/reverse_proxy/client/tesla.ex b/lib/pleroma/reverse_proxy/client/tesla.ex
new file mode 100644
index 000000000..d5a339681
--- /dev/null
+++ b/lib/pleroma/reverse_proxy/client/tesla.ex
@@ -0,0 +1,86 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.ReverseProxy.Client.Tesla do
+ @behaviour Pleroma.ReverseProxy.Client
+
+ alias Pleroma.Gun.ConnectionPool
+
+ @type headers() :: [{String.t(), String.t()}]
+ @type status() :: pos_integer()
+
+ @spec request(atom(), String.t(), headers(), String.t(), keyword()) ::
+ {:ok, status(), headers}
+ | {:ok, status(), headers, map()}
+ | {:error, atom() | String.t()}
+ | no_return()
+
+ @impl true
+ def request(method, url, headers, body, opts \\ []) do
+ check_adapter()
+
+ opts = Keyword.put(opts, :body_as, :chunks)
+
+ with {:ok, response} <-
+ Pleroma.HTTP.request(
+ method,
+ url,
+ body,
+ headers,
+ Keyword.put(opts, :adapter, opts)
+ ) do
+ if is_map(response.body) and method != :head do
+ {:ok, response.status, response.headers, response.body}
+ else
+ conn_pid = response.opts[:adapter][:conn]
+ ConnectionPool.release_conn(conn_pid)
+ {:ok, response.status, response.headers}
+ end
+ else
+ {:error, error} -> {:error, error}
+ end
+ end
+
+ @impl true
+ @spec stream_body(map()) ::
+ {:ok, binary(), map()} | {:error, atom() | String.t()} | :done | no_return()
+ def stream_body(%{pid: pid, fin: true}) do
+ ConnectionPool.release_conn(pid)
+ :done
+ end
+
+ def stream_body(client) do
+ case read_chunk!(client) do
+ {:fin, body} ->
+ {:ok, body, Map.put(client, :fin, true)}
+
+ {:nofin, part} ->
+ {:ok, part, client}
+
+ {:error, error} ->
+ {:error, error}
+ end
+ end
+
+ defp read_chunk!(%{pid: pid, stream: stream, opts: opts}) do
+ adapter = check_adapter()
+ adapter.read_chunk(pid, stream, opts)
+ end
+
+ @impl true
+ @spec close(map) :: :ok | no_return()
+ def close(%{pid: pid}) do
+ ConnectionPool.release_conn(pid)
+ end
+
+ defp check_adapter do
+ adapter = Application.get_env(:tesla, :adapter)
+
+ unless adapter == Tesla.Adapter.Gun do
+ raise "#{adapter} doesn't support reading body in chunks"
+ end
+
+ adapter
+ end
+end
diff --git a/lib/pleroma/reverse_proxy/reverse_proxy.ex b/lib/pleroma/reverse_proxy/reverse_proxy.ex
index 8b713b8f4..0de4e2309 100644
--- a/lib/pleroma/reverse_proxy/reverse_proxy.ex
+++ b/lib/pleroma/reverse_proxy/reverse_proxy.ex
@@ -3,14 +3,13 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ReverseProxy do
- alias Pleroma.HTTP
-
+ @range_headers ~w(range if-range)
@keep_req_headers ~w(accept user-agent accept-encoding cache-control if-modified-since) ++
- ~w(if-unmodified-since if-none-match if-range range)
+ ~w(if-unmodified-since if-none-match) ++ @range_headers
@resp_cache_headers ~w(etag date last-modified)
@keep_resp_headers @resp_cache_headers ++
- ~w(content-type content-disposition content-encoding content-range) ++
- ~w(accept-ranges vary)
+ ~w(content-length content-type content-disposition content-encoding) ++
+ ~w(content-range accept-ranges vary)
@default_cache_control_header "public, max-age=1209600"
@valid_resp_codes [200, 206, 304]
@max_read_duration :timer.seconds(30)
@@ -58,10 +57,10 @@ defmodule Pleroma.ReverseProxy do
* `req_headers`, `resp_headers` additional headers.
- * `http`: options for [hackney](https://github.com/benoitc/hackney).
+ * `http`: options for [hackney](https://github.com/benoitc/hackney) or [gun](https://github.com/ninenines/gun).
"""
- @default_hackney_options [pool: :media]
+ @default_options [pool: :media]
@inline_content_types [
"image/gif",
@@ -94,11 +93,7 @@ defmodule Pleroma.ReverseProxy do
def call(_conn, _url, _opts \\ [])
def call(conn = %{method: method}, url, opts) when method in @methods do
- hackney_opts =
- Pleroma.HTTP.Connection.hackney_options([])
- |> Keyword.merge(@default_hackney_options)
- |> Keyword.merge(Keyword.get(opts, :http, []))
- |> HTTP.process_request_options()
+ client_opts = Keyword.merge(@default_options, Keyword.get(opts, :http, []))
req_headers = build_req_headers(conn.req_headers, opts)
@@ -110,7 +105,7 @@ defmodule Pleroma.ReverseProxy do
end
with {:ok, nil} <- Cachex.get(:failed_proxy_url_cache, url),
- {:ok, code, headers, client} <- request(method, url, req_headers, hackney_opts),
+ {:ok, code, headers, client} <- request(method, url, req_headers, client_opts),
:ok <-
header_length_constraint(
headers,
@@ -156,11 +151,11 @@ defmodule Pleroma.ReverseProxy do
|> halt()
end
- defp request(method, url, headers, hackney_opts) do
+ defp request(method, url, headers, opts) do
Logger.debug("#{__MODULE__} #{method} #{url} #{inspect(headers)}")
method = method |> String.downcase() |> String.to_existing_atom()
- case client().request(method, url, headers, "", hackney_opts) do
+ case client().request(method, url, headers, "", opts) do
{:ok, code, headers, client} when code in @valid_resp_codes ->
{:ok, code, downcase_headers(headers), client}
@@ -170,12 +165,17 @@ defmodule Pleroma.ReverseProxy do
{:ok, code, _, _} ->
{:error, {:invalid_http_response, code}}
+ {:ok, code, _} ->
+ {:error, {:invalid_http_response, code}}
+
{:error, error} ->
{:error, error}
end
end
defp response(conn, client, url, status, headers, opts) do
+ Logger.debug("#{__MODULE__} #{status} #{url} #{inspect(headers)}")
+
result =
conn
|> put_resp_headers(build_resp_headers(headers, opts))
@@ -210,7 +210,7 @@ defmodule Pleroma.ReverseProxy do
duration,
Keyword.get(opts, :max_read_duration, @max_read_duration)
),
- {:ok, data} <- client().stream_body(client),
+ {:ok, data, client} <- client().stream_body(client),
{:ok, duration} <- increase_read_duration(duration),
sent_so_far = sent_so_far + byte_size(data),
:ok <-
@@ -226,7 +226,9 @@ defmodule Pleroma.ReverseProxy do
end
end
- defp head_response(conn, _url, code, headers, opts) do
+ defp head_response(conn, url, code, headers, opts) do
+ Logger.debug("#{__MODULE__} #{code} #{url} #{inspect(headers)}")
+
conn
|> put_resp_headers(build_resp_headers(headers, opts))
|> send_resp(code, "")
@@ -268,20 +270,33 @@ defmodule Pleroma.ReverseProxy do
headers
|> downcase_headers()
|> Enum.filter(fn {k, _} -> k in @keep_req_headers end)
- |> (fn headers ->
- headers = headers ++ Keyword.get(opts, :req_headers, [])
-
- if Keyword.get(opts, :keep_user_agent, false) do
- List.keystore(
- headers,
- "user-agent",
- 0,
- {"user-agent", Pleroma.Application.user_agent()}
- )
- else
- headers
- end
- end).()
+ |> build_req_range_or_encoding_header(opts)
+ |> build_req_user_agent_header(opts)
+ |> Keyword.merge(Keyword.get(opts, :req_headers, []))
+ end
+
+ # Disable content-encoding if any @range_headers are requested (see #1823).
+ defp build_req_range_or_encoding_header(headers, _opts) do
+ range? = Enum.any?(headers, fn {header, _} -> Enum.member?(@range_headers, header) end)
+
+ if range? && List.keymember?(headers, "accept-encoding", 0) do
+ List.keydelete(headers, "accept-encoding", 0)
+ else
+ headers
+ end
+ end
+
+ defp build_req_user_agent_header(headers, opts) do
+ if Keyword.get(opts, :keep_user_agent, false) do
+ List.keystore(
+ headers,
+ "user-agent",
+ 0,
+ {"user-agent", Pleroma.Application.user_agent()}
+ )
+ else
+ headers
+ end
end
defp build_resp_headers(headers, opts) do
@@ -289,7 +304,7 @@ defmodule Pleroma.ReverseProxy do
|> Enum.filter(fn {k, _} -> k in @keep_resp_headers end)
|> build_resp_cache_headers(opts)
|> build_resp_content_disposition_header(opts)
- |> (fn headers -> headers ++ Keyword.get(opts, :resp_headers, []) end).()
+ |> Keyword.merge(Keyword.get(opts, :resp_headers, []))
end
defp build_resp_cache_headers(headers, _opts) do
diff --git a/lib/pleroma/scheduled_activity.ex b/lib/pleroma/scheduled_activity.ex
index 8ff06a462..0937cb7db 100644
--- a/lib/pleroma/scheduled_activity.ex
+++ b/lib/pleroma/scheduled_activity.ex
@@ -40,7 +40,7 @@ defmodule Pleroma.ScheduledActivity do
%{changes: %{params: %{"media_ids" => media_ids} = params}} = changeset
)
when is_list(media_ids) do
- media_attachments = Utils.attachments_from_ids(%{"media_ids" => media_ids})
+ media_attachments = Utils.attachments_from_ids(%{media_ids: media_ids})
params =
params
diff --git a/lib/pleroma/signature.ex b/lib/pleroma/signature.ex
index 7006eb2c0..3aa6909d2 100644
--- a/lib/pleroma/signature.ex
+++ b/lib/pleroma/signature.ex
@@ -5,6 +5,7 @@
defmodule Pleroma.Signature do
@behaviour HTTPSignatures.Adapter
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Keys
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
@@ -21,14 +22,16 @@ defmodule Pleroma.Signature do
uri
end
- case uri do
- %URI{scheme: scheme} when scheme in ["https", "http"] ->
- {:ok, URI.to_string(uri)}
+ maybe_ap_id = URI.to_string(uri)
+
+ case ObjectValidators.ObjectID.cast(maybe_ap_id) do
+ {:ok, ap_id} ->
+ {:ok, ap_id}
_ ->
- case Pleroma.Web.WebFinger.finger(URI.to_string(uri)) do
+ case Pleroma.Web.WebFinger.finger(maybe_ap_id) do
%{"ap_id" => ap_id} -> {:ok, ap_id}
- _ -> {:error, URI.to_string(uri)}
+ _ -> {:error, maybe_ap_id}
end
end
end
diff --git a/lib/pleroma/stats.ex b/lib/pleroma/stats.ex
index 8d2809bbb..9a03f01db 100644
--- a/lib/pleroma/stats.ex
+++ b/lib/pleroma/stats.ex
@@ -91,26 +91,17 @@ defmodule Pleroma.Stats do
peers: peers,
stats: %{
domain_count: domain_count,
- status_count: status_count,
+ status_count: status_count || 0,
user_count: user_count
}
}
end
- def get_status_visibility_count do
- counter_cache =
- CounterCache.get_as_map([
- "status_visibility_public",
- "status_visibility_private",
- "status_visibility_unlisted",
- "status_visibility_direct"
- ])
-
- %{
- public: counter_cache["status_visibility_public"] || 0,
- unlisted: counter_cache["status_visibility_unlisted"] || 0,
- private: counter_cache["status_visibility_private"] || 0,
- direct: counter_cache["status_visibility_direct"] || 0
- }
+ def get_status_visibility_count(instance \\ nil) do
+ if is_nil(instance) do
+ CounterCache.get_sum()
+ else
+ CounterCache.get_by_instance(instance)
+ end
end
end
diff --git a/lib/pleroma/telemetry/logger.ex b/lib/pleroma/telemetry/logger.ex
new file mode 100644
index 000000000..4cacae02f
--- /dev/null
+++ b/lib/pleroma/telemetry/logger.ex
@@ -0,0 +1,76 @@
+defmodule Pleroma.Telemetry.Logger do
+ @moduledoc "Transforms Pleroma telemetry events to logs"
+
+ require Logger
+
+ @events [
+ [:pleroma, :connection_pool, :reclaim, :start],
+ [:pleroma, :connection_pool, :reclaim, :stop],
+ [:pleroma, :connection_pool, :provision_failure],
+ [:pleroma, :connection_pool, :client_death]
+ ]
+ def attach do
+ :telemetry.attach_many("pleroma-logger", @events, &handle_event/4, [])
+ end
+
+ # Passing anonymous functions instead of strings to logger is intentional,
+ # that way strings won't be concatenated if the message is going to be thrown
+ # out anyway due to higher log level configured
+
+ def handle_event(
+ [:pleroma, :connection_pool, :reclaim, :start],
+ _,
+ %{max_connections: max_connections, reclaim_max: reclaim_max},
+ _
+ ) do
+ Logger.debug(fn ->
+ "Connection pool is exhausted (reached #{max_connections} connections). Starting idle connection cleanup to reclaim as much as #{
+ reclaim_max
+ } connections"
+ end)
+ end
+
+ def handle_event(
+ [:pleroma, :connection_pool, :reclaim, :stop],
+ %{reclaimed_count: 0},
+ _,
+ _
+ ) do
+ Logger.error(fn ->
+ "Connection pool failed to reclaim any connections due to all of them being in use. It will have to drop requests for opening connections to new hosts"
+ end)
+ end
+
+ def handle_event(
+ [:pleroma, :connection_pool, :reclaim, :stop],
+ %{reclaimed_count: reclaimed_count},
+ _,
+ _
+ ) do
+ Logger.debug(fn -> "Connection pool cleaned up #{reclaimed_count} idle connections" end)
+ end
+
+ def handle_event(
+ [:pleroma, :connection_pool, :provision_failure],
+ %{opts: [key | _]},
+ _,
+ _
+ ) do
+ Logger.error(fn ->
+ "Connection pool had to refuse opening a connection to #{key} due to connection limit exhaustion"
+ end)
+ end
+
+ def handle_event(
+ [:pleroma, :connection_pool, :client_death],
+ %{client_pid: client_pid, reason: reason},
+ %{key: key},
+ _
+ ) do
+ Logger.warn(fn ->
+ "Pool worker for #{key}: Client #{inspect(client_pid)} died before releasing the connection with #{
+ inspect(reason)
+ }"
+ end)
+ end
+end
diff --git a/lib/pleroma/tesla/middleware/follow_redirects.ex b/lib/pleroma/tesla/middleware/follow_redirects.ex
new file mode 100644
index 000000000..5a7032215
--- /dev/null
+++ b/lib/pleroma/tesla/middleware/follow_redirects.ex
@@ -0,0 +1,110 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2015-2020 Tymon Tobolski <https://github.com/teamon/tesla/blob/master/lib/tesla/middleware/follow_redirects.ex>
+# Copyright © 2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.HTTP.Middleware.FollowRedirects do
+ @moduledoc """
+ Pool-aware version of https://github.com/teamon/tesla/blob/master/lib/tesla/middleware/follow_redirects.ex
+
+ Follow 3xx redirects
+ ## Options
+ - `:max_redirects` - limit number of redirects (default: `5`)
+ """
+
+ alias Pleroma.Gun.ConnectionPool
+
+ @behaviour Tesla.Middleware
+
+ @max_redirects 5
+ @redirect_statuses [301, 302, 303, 307, 308]
+
+ @impl Tesla.Middleware
+ def call(env, next, opts \\ []) do
+ max = Keyword.get(opts, :max_redirects, @max_redirects)
+
+ redirect(env, next, max)
+ end
+
+ defp redirect(env, next, left) do
+ opts = env.opts[:adapter]
+
+ case Tesla.run(env, next) do
+ {:ok, %{status: status} = res} when status in @redirect_statuses and left > 0 ->
+ release_conn(opts)
+
+ case Tesla.get_header(res, "location") do
+ nil ->
+ {:ok, res}
+
+ location ->
+ location = parse_location(location, res)
+
+ case get_conn(location, opts) do
+ {:ok, opts} ->
+ %{env | opts: Keyword.put(env.opts, :adapter, opts)}
+ |> new_request(res.status, location)
+ |> redirect(next, left - 1)
+
+ e ->
+ e
+ end
+ end
+
+ {:ok, %{status: status}} when status in @redirect_statuses ->
+ release_conn(opts)
+ {:error, {__MODULE__, :too_many_redirects}}
+
+ {:error, _} = e ->
+ release_conn(opts)
+ e
+
+ other ->
+ unless opts[:body_as] == :chunks do
+ release_conn(opts)
+ end
+
+ other
+ end
+ end
+
+ defp get_conn(location, opts) do
+ uri = URI.parse(location)
+
+ case ConnectionPool.get_conn(uri, opts) do
+ {:ok, conn} ->
+ {:ok, Keyword.merge(opts, conn: conn)}
+
+ e ->
+ e
+ end
+ end
+
+ defp release_conn(opts) do
+ ConnectionPool.release_conn(opts[:conn])
+ end
+
+ # The 303 (See Other) redirect was added in HTTP/1.1 to indicate that the originally
+ # requested resource is not available, however a related resource (or another redirect)
+ # available via GET is available at the specified location.
+ # https://tools.ietf.org/html/rfc7231#section-6.4.4
+ defp new_request(env, 303, location), do: %{env | url: location, method: :get, query: []}
+
+ # The 307 (Temporary Redirect) status code indicates that the target
+ # resource resides temporarily under a different URI and the user agent
+ # MUST NOT change the request method (...)
+ # https://tools.ietf.org/html/rfc7231#section-6.4.7
+ defp new_request(env, 307, location), do: %{env | url: location}
+
+ defp new_request(env, _, location), do: %{env | url: location, query: []}
+
+ defp parse_location("https://" <> _rest = location, _env), do: location
+ defp parse_location("http://" <> _rest = location, _env), do: location
+
+ defp parse_location(location, env) do
+ env.url
+ |> URI.parse()
+ |> URI.merge(location)
+ |> URI.to_string()
+ end
+end
diff --git a/lib/pleroma/tests/auth_test_controller.ex b/lib/pleroma/tests/auth_test_controller.ex
new file mode 100644
index 000000000..fb04411d9
--- /dev/null
+++ b/lib/pleroma/tests/auth_test_controller.ex
@@ -0,0 +1,93 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+# A test controller reachable only in :test env.
+defmodule Pleroma.Tests.AuthTestController do
+ @moduledoc false
+
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
+ alias Pleroma.Plugs.OAuthScopesPlug
+ alias Pleroma.User
+
+ # Serves only with proper OAuth token (:api and :authenticated_api)
+ # Skipping EnsurePublicOrAuthenticatedPlug has no effect in this case
+ #
+ # Suggested use case: all :authenticated_api endpoints (makes no sense for :api endpoints)
+ plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :do_oauth_check)
+
+ # Via :api, keeps :user if token has requested scopes (if :user is dropped, serves if public)
+ # Via :authenticated_api, serves if token is present and has requested scopes
+ #
+ # Suggested use case: vast majority of :api endpoints (no sense for :authenticated_api ones)
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["read"], fallback: :proceed_unauthenticated}
+ when action == :fallback_oauth_check
+ )
+
+ # Keeps :user if present, executes regardless of token / token scopes
+ # Fails with no :user for :authenticated_api / no user for :api on private instance
+ # Note: EnsurePublicOrAuthenticatedPlug is not skipped (private instance fails on no :user)
+ # Note: Basic Auth processing results in :skip_plug call for OAuthScopesPlug
+ #
+ # Suggested use: suppressing OAuth checks for other auth mechanisms (like Basic Auth)
+ # For controller-level use, see :skip_oauth_skip_publicity_check instead
+ plug(
+ :skip_plug,
+ OAuthScopesPlug when action == :skip_oauth_check
+ )
+
+ # (Shouldn't be executed since the plug is skipped)
+ plug(OAuthScopesPlug, %{scopes: ["admin"]} when action == :skip_oauth_check)
+
+ # Via :api, keeps :user if token has requested scopes, and continues with nil :user otherwise
+ # Via :authenticated_api, serves if token is present and has requested scopes
+ #
+ # Suggested use: as :fallback_oauth_check but open with nil :user for :api on private instances
+ plug(
+ :skip_plug,
+ EnsurePublicOrAuthenticatedPlug when action == :fallback_oauth_skip_publicity_check
+ )
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["read"], fallback: :proceed_unauthenticated}
+ when action == :fallback_oauth_skip_publicity_check
+ )
+
+ # Via :api, keeps :user if present, serves regardless of token presence / scopes / :user presence
+ # Via :authenticated_api, serves if :user is set (regardless of token presence and its scopes)
+ #
+ # Suggested use: making an :api endpoint always accessible (e.g. email confirmation endpoint)
+ plug(
+ :skip_plug,
+ [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug]
+ when action == :skip_oauth_skip_publicity_check
+ )
+
+ # Via :authenticated_api, always fails with 403 (endpoint is insecure)
+ # Via :api, drops :user if present and serves if public (private instance rejects on no user)
+ #
+ # Suggested use: none; please define OAuth rules for all :api / :authenticated_api endpoints
+ plug(:skip_plug, [] when action == :missing_oauth_check_definition)
+
+ def do_oauth_check(conn, _params), do: conn_state(conn)
+
+ def fallback_oauth_check(conn, _params), do: conn_state(conn)
+
+ def skip_oauth_check(conn, _params), do: conn_state(conn)
+
+ def fallback_oauth_skip_publicity_check(conn, _params), do: conn_state(conn)
+
+ def skip_oauth_skip_publicity_check(conn, _params), do: conn_state(conn)
+
+ def missing_oauth_check_definition(conn, _params), do: conn_state(conn)
+
+ defp conn_state(%{assigns: %{user: %User{} = user}} = conn),
+ do: json(conn, %{user_id: user.id})
+
+ defp conn_state(conn), do: json(conn, %{user_id: nil})
+end
diff --git a/lib/pleroma/tests/oauth_test_controller.ex b/lib/pleroma/tests/oauth_test_controller.ex
deleted file mode 100644
index 58d517f78..000000000
--- a/lib/pleroma/tests/oauth_test_controller.ex
+++ /dev/null
@@ -1,31 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-# A test controller reachable only in :test env.
-# Serves to test OAuth scopes check skipping / enforcement.
-defmodule Pleroma.Tests.OAuthTestController do
- @moduledoc false
-
- use Pleroma.Web, :controller
-
- alias Pleroma.Plugs.OAuthScopesPlug
-
- plug(:skip_plug, OAuthScopesPlug when action == :skipped_oauth)
-
- plug(OAuthScopesPlug, %{scopes: ["read"]} when action != :missed_oauth)
-
- def skipped_oauth(conn, _params) do
- noop(conn)
- end
-
- def performed_oauth(conn, _params) do
- noop(conn)
- end
-
- def missed_oauth(conn, _params) do
- noop(conn)
- end
-
- defp noop(conn), do: json(conn, %{})
-end
diff --git a/lib/pleroma/thread_mute.ex b/lib/pleroma/thread_mute.ex
index a7ea13891..be01d541d 100644
--- a/lib/pleroma/thread_mute.ex
+++ b/lib/pleroma/thread_mute.ex
@@ -25,10 +25,10 @@ defmodule Pleroma.ThreadMute do
end
def query(user_id, context) do
- {:ok, user_id} = FlakeId.Ecto.CompatType.dump(user_id)
+ user_binary_id = User.binary_id(user_id)
ThreadMute
- |> where(user_id: ^user_id)
+ |> where(user_id: ^user_binary_id)
|> where(context: ^context)
end
@@ -68,8 +68,8 @@ defmodule Pleroma.ThreadMute do
|> Repo.delete_all()
end
- def check_muted(user_id, context) do
+ def exists?(user_id, context) do
query(user_id, context)
- |> Repo.all()
+ |> Repo.exists?()
end
end
diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex
index 762d813d9..015c87593 100644
--- a/lib/pleroma/upload.ex
+++ b/lib/pleroma/upload.ex
@@ -56,6 +56,15 @@ defmodule Pleroma.Upload do
}
defstruct [:id, :name, :tempfile, :content_type, :path]
+ defp get_description(opts, upload) do
+ case {opts[:description], Pleroma.Config.get([Pleroma.Upload, :default_description])} do
+ {description, _} when is_binary(description) -> description
+ {_, :filename} -> upload.name
+ {_, str} when is_binary(str) -> str
+ _ -> ""
+ end
+ end
+
@spec store(source, options :: [option()]) :: {:ok, Map.t()} | {:error, any()}
def store(upload, opts \\ []) do
opts = get_opts(opts)
@@ -63,10 +72,15 @@ defmodule Pleroma.Upload do
with {:ok, upload} <- prepare_upload(upload, opts),
upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"},
{:ok, upload} <- Pleroma.Upload.Filter.filter(opts.filters, upload),
+ description = get_description(opts, upload),
+ {_, true} <-
+ {:description_limit,
+ String.length(description) <= Pleroma.Config.get([:instance, :description_limit])},
{:ok, url_spec} <- Pleroma.Uploaders.Uploader.put_file(opts.uploader, upload) do
{:ok,
%{
"type" => opts.activity_type,
+ "mediaType" => upload.content_type,
"url" => [
%{
"type" => "Link",
@@ -74,9 +88,12 @@ defmodule Pleroma.Upload do
"href" => url_from_spec(upload, opts.base_url, url_spec)
}
],
- "name" => Map.get(opts, :description) || upload.name
+ "name" => description
}}
else
+ {:description_limit, _} ->
+ {:error, :description_too_long}
+
{:error, error} ->
Logger.error(
"#{__MODULE__} store (using #{inspect(opts.uploader)}) failed: #{inspect(error)}"
@@ -134,7 +151,7 @@ defmodule Pleroma.Upload do
end
end
- defp prepare_upload(%{"img" => "data:image/" <> image_data}, opts) do
+ defp prepare_upload(%{img: "data:image/" <> image_data}, opts) do
parsed = Regex.named_captures(~r/(?<filetype>jpeg|png|gif);base64,(?<data>.*)/, image_data)
data = Base.decode64!(parsed["data"], ignore: :whitespace)
hash = String.downcase(Base.encode16(:crypto.hash(:sha256, data)))
diff --git a/lib/pleroma/upload/filter/exiftool.ex b/lib/pleroma/upload/filter/exiftool.ex
new file mode 100644
index 000000000..ea8798fe3
--- /dev/null
+++ b/lib/pleroma/upload/filter/exiftool.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.Upload.Filter.Exiftool do
+ @moduledoc """
+ Strips GPS related EXIF tags and overwrites the file in place.
+ Also strips or replaces filesystem metadata e.g., timestamps.
+ """
+ @behaviour Pleroma.Upload.Filter
+
+ @spec filter(Pleroma.Upload.t()) :: :ok | {:error, String.t()}
+ def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do
+ try do
+ case System.cmd("exiftool", ["-overwrite_original", "-gps:all=", file], parallelism: true) do
+ {_response, 0} -> :ok
+ {error, 1} -> {:error, error}
+ end
+ rescue
+ _e in ErlangError ->
+ {:error, "exiftool command not found"}
+ end
+ end
+
+ def filter(_), do: :ok
+end
diff --git a/lib/pleroma/upload/filter/mogrifun.ex b/lib/pleroma/upload/filter/mogrifun.ex
index 7d95577a4..c8fa7b190 100644
--- a/lib/pleroma/upload/filter/mogrifun.ex
+++ b/lib/pleroma/upload/filter/mogrifun.ex
@@ -6,6 +6,10 @@ defmodule Pleroma.Upload.Filter.Mogrifun do
@behaviour Pleroma.Upload.Filter
alias Pleroma.Upload.Filter
+ @moduledoc """
+ This module is just an example of an Upload filter. It's not supposed to be used in production.
+ """
+
@filters [
{"implode", "1"},
{"-raise", "20"},
@@ -34,10 +38,15 @@ defmodule Pleroma.Upload.Filter.Mogrifun do
[{"fill", "yellow"}, {"tint", "40"}]
]
+ @spec filter(Pleroma.Upload.t()) :: :ok | {:error, String.t()}
def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do
- Filter.Mogrify.do_filter(file, [Enum.random(@filters)])
-
- :ok
+ try do
+ Filter.Mogrify.do_filter(file, [Enum.random(@filters)])
+ :ok
+ rescue
+ _e in ErlangError ->
+ {:error, "mogrify command not found"}
+ end
end
def filter(_), do: :ok
diff --git a/lib/pleroma/upload/filter/mogrify.ex b/lib/pleroma/upload/filter/mogrify.ex
index 2eb758006..7a45add5a 100644
--- a/lib/pleroma/upload/filter/mogrify.ex
+++ b/lib/pleroma/upload/filter/mogrify.ex
@@ -8,11 +8,15 @@ defmodule Pleroma.Upload.Filter.Mogrify do
@type conversion :: action :: String.t() | {action :: String.t(), opts :: String.t()}
@type conversions :: conversion() | [conversion()]
+ @spec filter(Pleroma.Upload.t()) :: :ok | {:error, String.t()}
def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do
- filters = Pleroma.Config.get!([__MODULE__, :args])
-
- do_filter(file, filters)
- :ok
+ try do
+ do_filter(file, Pleroma.Config.get!([__MODULE__, :args]))
+ :ok
+ rescue
+ _e in ErlangError ->
+ {:error, "mogrify command not found"}
+ end
end
def filter(_), do: :ok
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index fbfdc68e8..d2ad9516f 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -9,16 +9,18 @@ defmodule Pleroma.User do
import Ecto.Query
import Ecto, only: [assoc: 2]
- alias Comeonin.Pbkdf2
alias Ecto.Multi
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Conversation.Participation
alias Pleroma.Delivery
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators
+ alias Pleroma.Emoji
alias Pleroma.FollowingRelationship
alias Pleroma.Formatter
alias Pleroma.HTML
alias Pleroma.Keys
+ alias Pleroma.MFA
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Registration
@@ -28,6 +30,8 @@ defmodule Pleroma.User do
alias Pleroma.UserRelationship
alias Pleroma.Web
alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Builder
+ alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
@@ -38,7 +42,12 @@ defmodule Pleroma.User do
require Logger
@type t :: %__MODULE__{}
- @type account_status :: :active | :deactivated | :password_reset_pending | :confirmation_pending
+ @type account_status ::
+ :active
+ | :deactivated
+ | :password_reset_pending
+ | :confirmation_pending
+ | :approval_pending
@primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
# credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
@@ -75,6 +84,7 @@ defmodule Pleroma.User do
schema "users" do
field(:bio, :string)
+ field(:raw_bio, :string)
field(:email, :string)
field(:name, :string)
field(:nickname, :string)
@@ -82,8 +92,9 @@ defmodule Pleroma.User do
field(:password, :string, virtual: true)
field(:password_confirmation, :string, virtual: true)
field(:keys, :string)
+ field(:public_key, :string)
field(:ap_id, :string)
- field(:avatar, :map)
+ field(:avatar, :map, default: %{})
field(:local, :boolean, default: true)
field(:follower_address, :string)
field(:following_address, :string)
@@ -94,13 +105,14 @@ defmodule Pleroma.User do
field(:last_digest_emailed_at, :naive_datetime)
field(:banner, :map, default: %{})
field(:background, :map, default: %{})
- field(:source_data, :map, default: %{})
field(:note_count, :integer, default: 0)
field(:follower_count, :integer, default: 0)
field(:following_count, :integer, default: 0)
field(:locked, :boolean, default: false)
field(:confirmation_pending, :boolean, default: false)
field(:password_reset_pending, :boolean, default: false)
+ field(:approval_pending, :boolean, default: false)
+ field(:registration_reason, :string, default: nil)
field(:confirmation_token, :string, default: nil)
field(:default_scope, :string, default: "public")
field(:domain_blocks, {:array, :string}, default: [])
@@ -110,9 +122,8 @@ defmodule Pleroma.User do
field(:is_moderator, :boolean, default: false)
field(:is_admin, :boolean, default: false)
field(:show_role, :boolean, default: true)
- field(:settings, :map, default: nil)
- field(:magic_key, :string, default: nil)
- field(:uri, :string, default: nil)
+ field(:mastofe_settings, :map, default: nil)
+ field(:uri, ObjectValidators.Uri, default: nil)
field(:hide_followers_count, :boolean, default: false)
field(:hide_follows_count, :boolean, default: false)
field(:hide_followers, :boolean, default: false)
@@ -122,7 +133,7 @@ defmodule Pleroma.User do
field(:pinned_activities, {:array, :string}, default: [])
field(:email_notifications, :map, default: %{"digest" => false})
field(:mascot, :map, default: nil)
- field(:emoji, {:array, :map}, default: [])
+ field(:emoji, :map, default: %{})
field(:pleroma_settings_store, :map, default: %{})
field(:fields, {:array, :map}, default: [])
field(:raw_fields, {:array, :map}, default: [])
@@ -132,6 +143,9 @@ defmodule Pleroma.User do
field(:skip_thread_containment, :boolean, default: false)
field(:actor_type, :string, default: "Person")
field(:also_known_as, {:array, :string}, default: [])
+ field(:inbox, :string)
+ field(:shared_inbox, :string)
+ field(:accepts_chat_messages, :boolean, default: nil)
embeds_one(
:notification_settings,
@@ -185,6 +199,12 @@ defmodule Pleroma.User do
# `:subscribers` is deprecated (replaced with `subscriber_users` relation)
field(:subscribers, {:array, :string}, default: [])
+ embeds_one(
+ :multi_factor_authentication_settings,
+ MFA.Settings,
+ on_replace: :delete
+ )
+
timestamps()
end
@@ -227,6 +247,13 @@ defmodule Pleroma.User do
end
end
+ defdelegate following_count(user), to: FollowingRelationship
+ defdelegate following(user), to: FollowingRelationship
+ defdelegate following?(follower, followed), to: FollowingRelationship
+ defdelegate following_ap_ids(user), to: FollowingRelationship
+ defdelegate get_follow_requests(user), to: FollowingRelationship
+ defdelegate search(query, opts \\ []), to: User.Search
+
@doc """
Dumps Flake Id to SQL-compatible format (16-byte UUID).
E.g. "9pQtDGXuq4p3VlcJEm" -> <<0, 0, 1, 110, 179, 218, 42, 92, 213, 41, 44, 227, 95, 213, 0, 0>>
@@ -249,28 +276,65 @@ defmodule Pleroma.User do
@spec account_status(User.t()) :: account_status()
def account_status(%User{deactivated: true}), do: :deactivated
def account_status(%User{password_reset_pending: true}), do: :password_reset_pending
+ def account_status(%User{approval_pending: true}), do: :approval_pending
def account_status(%User{confirmation_pending: true}) do
- case Config.get([:instance, :account_activation_required]) do
- true -> :confirmation_pending
- _ -> :active
+ if Config.get([:instance, :account_activation_required]) do
+ :confirmation_pending
+ else
+ :active
end
end
def account_status(%User{}), do: :active
- @spec visible_for?(User.t(), User.t() | nil) :: boolean()
- def visible_for?(user, for_user \\ nil)
+ @spec visible_for(User.t(), User.t() | nil) ::
+ :visible
+ | :invisible
+ | :restricted_unauthenticated
+ | :deactivated
+ | :confirmation_pending
+ def visible_for(user, for_user \\ nil)
+
+ def visible_for(%User{invisible: true}, _), do: :invisible
+
+ def visible_for(%User{id: user_id}, %User{id: user_id}), do: :visible
+
+ def visible_for(%User{} = user, nil) do
+ if restrict_unauthenticated?(user) do
+ :restrict_unauthenticated
+ else
+ visible_account_status(user)
+ end
+ end
+
+ def visible_for(%User{} = user, for_user) do
+ if superuser?(for_user) do
+ :visible
+ else
+ visible_account_status(user)
+ end
+ end
- def visible_for?(%User{invisible: true}, _), do: false
+ def visible_for(_, _), do: :invisible
- def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
+ defp restrict_unauthenticated?(%User{local: true}) do
+ Config.restrict_unauthenticated_access?(:profiles, :local)
+ end
- def visible_for?(%User{} = user, for_user) do
- account_status(user) == :active || superuser?(for_user)
+ defp restrict_unauthenticated?(%User{local: _}) do
+ Config.restrict_unauthenticated_access?(:profiles, :remote)
end
- def visible_for?(_, _), do: false
+ defp visible_account_status(user) do
+ status = account_status(user)
+
+ if status in [:active, :password_reset_pending] do
+ :visible
+ else
+ status
+ end
+ end
@spec superuser?(User.t()) :: boolean()
def superuser?(%User{local: true, is_admin: true}), do: true
@@ -283,8 +347,13 @@ defmodule Pleroma.User do
def avatar_url(user, options \\ []) do
case user.avatar do
- %{"url" => [%{"href" => href} | _]} -> href
- _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
+ %{"url" => [%{"href" => href} | _]} ->
+ href
+
+ _ ->
+ unless options[:no_default] do
+ Config.get([:assets, :default_user_avatar], "#{Web.base_url()}/images/avi.png")
+ end
end
end
@@ -295,6 +364,7 @@ defmodule Pleroma.User do
end
end
+ # Should probably be renamed or removed
def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"
def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
@@ -304,31 +374,11 @@ defmodule Pleroma.User do
def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
- def follow_state(%User{} = user, %User{} = target) do
- case Utils.fetch_latest_follow(user, target) do
- %{data: %{"state" => state}} -> state
- # Ideally this would be nil, but then Cachex does not commit the value
- _ -> false
- end
- end
-
- def get_cached_follow_state(user, target) do
- key = "follow_state:#{user.ap_id}|#{target.ap_id}"
- Cachex.fetch!(:user_cache, key, fn _ -> {:commit, follow_state(user, target)} end)
- end
-
- @spec set_follow_state_cache(String.t(), String.t(), String.t()) :: {:ok | :error, boolean()}
- def set_follow_state_cache(user_ap_id, target_ap_id, state) do
- Cachex.put(:user_cache, "follow_state:#{user_ap_id}|#{target_ap_id}", state)
- end
-
@spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
def restrict_deactivated(query) do
from(u in query, where: u.deactivated != ^true)
end
- defdelegate following_count(user), to: FollowingRelationship
-
defp truncate_fields_param(params) do
if Map.has_key?(params, :fields) do
Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
@@ -346,75 +396,90 @@ defmodule Pleroma.User do
end
end
- def remote_user_creation(params) do
- bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
- name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
+ defp fix_follower_address(%{follower_address: _, following_address: _} = params), do: params
+
+ defp fix_follower_address(%{nickname: nickname} = params),
+ do: Map.put(params, :follower_address, ap_followers(%User{nickname: nickname}))
+
+ defp fix_follower_address(params), do: params
+
+ def remote_user_changeset(struct \\ %User{local: false}, params) do
+ bio_limit = Config.get([:instance, :user_bio_length], 5000)
+ name_limit = Config.get([:instance, :user_name_length], 100)
+
+ name =
+ case params[:name] do
+ name when is_binary(name) and byte_size(name) > 0 -> name
+ _ -> params[:nickname]
+ end
params =
params
+ |> Map.put(:name, name)
+ |> Map.put_new(:last_refreshed_at, NaiveDateTime.utc_now())
|> truncate_if_exists(:name, name_limit)
|> truncate_if_exists(:bio, bio_limit)
|> truncate_fields_param()
+ |> fix_follower_address()
- changeset =
- %User{local: false}
- |> cast(
- params,
- [
- :bio,
- :name,
- :ap_id,
- :nickname,
- :avatar,
- :ap_enabled,
- :source_data,
- :banner,
- :locked,
- :magic_key,
- :uri,
- :hide_followers,
- :hide_follows,
- :hide_followers_count,
- :hide_follows_count,
- :follower_count,
- :fields,
- :following_count,
- :discoverable,
- :invisible,
- :actor_type,
- :also_known_as
- ]
- )
- |> validate_required([:name, :ap_id])
- |> unique_constraint(:nickname)
- |> validate_format(:nickname, @email_regex)
- |> validate_length(:bio, max: bio_limit)
- |> validate_length(:name, max: name_limit)
- |> validate_fields(true)
-
- case params[:source_data] do
- %{"followers" => followers, "following" => following} ->
- changeset
- |> put_change(:follower_address, followers)
- |> put_change(:following_address, following)
-
- _ ->
- followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
- put_change(changeset, :follower_address, followers)
- end
+ struct
+ |> cast(
+ params,
+ [
+ :bio,
+ :name,
+ :emoji,
+ :ap_id,
+ :inbox,
+ :shared_inbox,
+ :nickname,
+ :public_key,
+ :avatar,
+ :ap_enabled,
+ :banner,
+ :locked,
+ :last_refreshed_at,
+ :uri,
+ :follower_address,
+ :following_address,
+ :hide_followers,
+ :hide_follows,
+ :hide_followers_count,
+ :hide_follows_count,
+ :follower_count,
+ :fields,
+ :following_count,
+ :discoverable,
+ :invisible,
+ :actor_type,
+ :also_known_as,
+ :accepts_chat_messages
+ ]
+ )
+ |> validate_required([:name, :ap_id])
+ |> unique_constraint(:nickname)
+ |> validate_format(:nickname, @email_regex)
+ |> validate_length(:bio, max: bio_limit)
+ |> validate_length(:name, max: name_limit)
+ |> validate_fields(true)
end
def update_changeset(struct, params \\ %{}) do
- bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
- name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
+ bio_limit = Config.get([:instance, :user_bio_length], 5000)
+ name_limit = Config.get([:instance, :user_name_length], 100)
struct
|> cast(
params,
[
:bio,
+ :raw_bio,
:name,
+ :emoji,
:avatar,
+ :public_key,
+ :inbox,
+ :shared_inbox,
:locked,
:no_rich_text,
:default_scope,
@@ -433,14 +498,17 @@ defmodule Pleroma.User do
:pleroma_settings_store,
:discoverable,
:actor_type,
- :also_known_as
+ :also_known_as,
+ :accepts_chat_messages
]
)
|> unique_constraint(:nickname)
|> validate_format(:nickname, local_nickname_regex())
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, min: 1, max: name_limit)
+ |> validate_inclusion(:actor_type, ["Person", "Service"])
|> put_fields()
+ |> put_emoji()
|> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)})
|> put_change_if_present(:avatar, &put_upload(&1, :avatar))
|> put_change_if_present(:banner, &put_upload(&1, :banner))
@@ -476,18 +544,37 @@ defmodule Pleroma.User do
|> elem(0)
end
- defp put_change_if_present(changeset, map_field, value_function) do
- if value = get_change(changeset, map_field) do
- with {:ok, new_value} <- value_function.(value) do
- put_change(changeset, map_field, new_value)
- else
- _ -> changeset
- end
+ defp put_emoji(changeset) do
+ emojified_fields = [:bio, :name, :raw_fields]
+
+ if Enum.any?(changeset.changes, fn {k, _} -> k in emojified_fields end) do
+ bio = Emoji.Formatter.get_emoji_map(get_field(changeset, :bio))
+ name = Emoji.Formatter.get_emoji_map(get_field(changeset, :name))
+
+ emoji = Map.merge(bio, name)
+
+ emoji =
+ changeset
+ |> get_field(:raw_fields)
+ |> Enum.reduce(emoji, fn x, acc ->
+ Map.merge(acc, Emoji.Formatter.get_emoji_map(x["name"] <> x["value"]))
+ end)
+
+ put_change(changeset, :emoji, emoji)
else
changeset
end
end
+ defp put_change_if_present(changeset, map_field, value_function) do
+ with {:ok, value} <- fetch_change(changeset, map_field),
+ {:ok, new_value} <- value_function.(value) do
+ put_change(changeset, map_field, new_value)
+ else
+ _ -> changeset
+ end
+ end
+
defp put_upload(value, type) do
with %Plug.Upload{} <- value,
{:ok, object} <- ActivityPub.upload(value, type: type) do
@@ -495,57 +582,6 @@ defmodule Pleroma.User do
end
end
- def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
- bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
- name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
-
- params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
-
- params =
- if remote? do
- params
- |> truncate_fields_param()
- |> truncate_if_exists(:name, name_limit)
- |> truncate_if_exists(:bio, bio_limit)
- else
- params
- end
-
- struct
- |> cast(
- params,
- [
- :bio,
- :name,
- :follower_address,
- :following_address,
- :avatar,
- :last_refreshed_at,
- :ap_enabled,
- :source_data,
- :banner,
- :locked,
- :magic_key,
- :follower_count,
- :following_count,
- :hide_follows,
- :fields,
- :hide_followers,
- :allow_following_move,
- :discoverable,
- :hide_followers_count,
- :hide_follows_count,
- :actor_type,
- :also_known_as
- ]
- )
- |> unique_constraint(:nickname)
- |> validate_format(:nickname, local_nickname_regex())
- |> validate_length(:bio, max: bio_limit)
- |> validate_length(:name, max: name_limit)
- |> validate_fields(remote?)
- end
-
def update_as_admin_changeset(struct, params) do
struct
|> update_changeset(params)
@@ -553,9 +589,10 @@ defmodule Pleroma.User do
|> delete_change(:also_known_as)
|> unique_constraint(:email)
|> validate_format(:email, @email_regex)
+ |> validate_inclusion(:actor_type, ["Person", "Service"])
end
- @spec update_as_admin(%User{}, map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
+ @spec update_as_admin(User.t(), map()) :: {:ok, User.t()} | {:error, Changeset.t()}
def update_as_admin(user, params) do
params = Map.put(params, "password_confirmation", params["password"])
changeset = update_as_admin_changeset(user, params)
@@ -576,7 +613,7 @@ defmodule Pleroma.User do
|> put_change(:password_reset_pending, false)
end
- @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
+ @spec reset_password(User.t(), map()) :: {:ok, User.t()} | {:error, Changeset.t()}
def reset_password(%User{} = user, params) do
reset_password(user, user, params)
end
@@ -608,29 +645,88 @@ defmodule Pleroma.User do
@spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def force_password_reset(user), do: update_password_reset_pending(user, true)
+ # Used to auto-register LDAP accounts which won't have a password hash stored locally
+ def register_changeset_ldap(struct, params = %{password: password})
+ when is_nil(password) do
+ params = Map.put_new(params, :accepts_chat_messages, true)
+
+ params =
+ if Map.has_key?(params, :email) do
+ Map.put_new(params, :email, params[:email])
+ else
+ params
+ end
+
+ struct
+ |> cast(params, [
+ :name,
+ :nickname,
+ :email,
+ :accepts_chat_messages
+ ])
+ |> validate_required([:name, :nickname])
+ |> unique_constraint(:nickname)
+ |> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
+ |> validate_format(:nickname, local_nickname_regex())
+ |> put_ap_id()
+ |> unique_constraint(:ap_id)
+ |> put_following_and_follower_address()
+ end
+
def register_changeset(struct, params \\ %{}, opts \\ []) do
- bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
- name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
+ bio_limit = Config.get([:instance, :user_bio_length], 5000)
+ name_limit = Config.get([:instance, :user_name_length], 100)
+ reason_limit = Config.get([:instance, :registration_reason_length], 500)
+ params = Map.put_new(params, :accepts_chat_messages, true)
need_confirmation? =
if is_nil(opts[:need_confirmation]) do
- Pleroma.Config.get([:instance, :account_activation_required])
+ Config.get([:instance, :account_activation_required])
else
opts[:need_confirmation]
end
+ need_approval? =
+ if is_nil(opts[:need_approval]) do
+ Config.get([:instance, :account_approval_required])
+ else
+ opts[:need_approval]
+ end
+
struct
|> confirmation_changeset(need_confirmation: need_confirmation?)
- |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
+ |> approval_changeset(need_approval: need_approval?)
+ |> cast(params, [
+ :bio,
+ :raw_bio,
+ :email,
+ :name,
+ :nickname,
+ :password,
+ :password_confirmation,
+ :emoji,
+ :accepts_chat_messages,
+ :registration_reason
+ ])
|> validate_required([:name, :nickname, :password, :password_confirmation])
|> validate_confirmation(:password)
|> unique_constraint(:email)
+ |> validate_format(:email, @email_regex)
+ |> validate_change(:email, fn :email, email ->
+ valid? =
+ Config.get([User, :email_blacklist])
+ |> Enum.all?(fn blacklisted_domain ->
+ !String.ends_with?(email, ["@" <> blacklisted_domain, "." <> blacklisted_domain])
+ end)
+
+ if valid?, do: [], else: [email: "Invalid email"]
+ end)
|> unique_constraint(:nickname)
- |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
+ |> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
|> validate_format(:nickname, local_nickname_regex())
- |> validate_format(:email, @email_regex)
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, min: 1, max: name_limit)
+ |> validate_length(:registration_reason, max: reason_limit)
|> maybe_validate_required_email(opts[:external])
|> put_password_hash
|> put_ap_id()
@@ -641,7 +737,7 @@ defmodule Pleroma.User do
def maybe_validate_required_email(changeset, true), do: changeset
def maybe_validate_required_email(changeset, _) do
- if Pleroma.Config.get([:instance, :account_activation_required]) do
+ if Config.get([:instance, :account_activation_required]) do
validate_required(changeset, [:email])
else
changeset
@@ -661,7 +757,7 @@ defmodule Pleroma.User do
end
defp autofollow_users(user) do
- candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
+ candidates = Config.get([:instance, :autofollowed_nicknames])
autofollowed_users =
User.Query.build(%{nickname: candidates, local: true, deactivated: false})
@@ -680,27 +776,62 @@ defmodule Pleroma.User do
def post_register_action(%User{} = user) do
with {:ok, user} <- autofollow_users(user),
{:ok, user} <- set_cache(user),
- {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
+ {:ok, _} <- send_welcome_email(user),
+ {:ok, _} <- send_welcome_message(user),
+ {:ok, _} <- send_welcome_chat_message(user),
{:ok, _} <- try_send_confirmation_email(user) do
{:ok, user}
end
end
- def try_send_confirmation_email(%User{} = user) do
- if user.confirmation_pending &&
- Pleroma.Config.get([:instance, :account_activation_required]) do
- user
- |> Pleroma.Emails.UserEmail.account_confirmation_email()
- |> Pleroma.Emails.Mailer.deliver_async()
+ def send_welcome_message(user) do
+ if User.WelcomeMessage.enabled?() do
+ User.WelcomeMessage.post_message(user)
+ {:ok, :enqueued}
+ else
+ {:ok, :noop}
+ end
+ end
+ def send_welcome_chat_message(user) do
+ if User.WelcomeChatMessage.enabled?() do
+ User.WelcomeChatMessage.post_message(user)
{:ok, :enqueued}
else
{:ok, :noop}
end
end
- def try_send_confirmation_email(users) do
- Enum.each(users, &try_send_confirmation_email/1)
+ def send_welcome_email(%User{email: email} = user) when is_binary(email) do
+ if User.WelcomeEmail.enabled?() do
+ User.WelcomeEmail.send_email(user)
+ {:ok, :enqueued}
+ else
+ {:ok, :noop}
+ end
+ end
+
+ def send_welcome_email(_), do: {:ok, :noop}
+
+ @spec try_send_confirmation_email(User.t()) :: {:ok, :enqueued | :noop}
+ def try_send_confirmation_email(%User{confirmation_pending: true} = user) do
+ if Config.get([:instance, :account_activation_required]) do
+ send_confirmation_email(user)
+ {:ok, :enqueued}
+ else
+ {:ok, :noop}
+ end
+ end
+
+ def try_send_confirmation_email(_), do: {:ok, :noop}
+
+ @spec send_confirmation_email(Uset.t()) :: User.t()
+ def send_confirmation_email(%User{} = user) do
+ user
+ |> Pleroma.Emails.UserEmail.account_confirmation_email()
+ |> Pleroma.Emails.Mailer.deliver_async()
+
+ user
end
def needs_update?(%User{local: true}), do: false
@@ -742,10 +873,8 @@ defmodule Pleroma.User do
set_cache(follower)
end
- defdelegate following(user), to: FollowingRelationship
-
def follow(%User{} = follower, %User{} = followed, state \\ :follow_accept) do
- deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
+ deny_follow_blocked = Config.get([:user, :deny_follow_blocked])
cond do
followed.deactivated ->
@@ -761,7 +890,6 @@ defmodule Pleroma.User do
follower
|> update_following_count()
- |> set_cache()
end
end
@@ -769,7 +897,19 @@ defmodule Pleroma.User do
{:error, "Not subscribed!"}
end
+ @spec unfollow(User.t(), User.t()) :: {:ok, User.t(), Activity.t()} | {:error, String.t()}
def unfollow(%User{} = follower, %User{} = followed) do
+ case do_unfollow(follower, followed) do
+ {:ok, follower, followed} ->
+ {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
+
+ error ->
+ error
+ end
+ end
+
+ @spec do_unfollow(User.t(), User.t()) :: {:ok, User.t(), User.t()} | {:error, String.t()}
+ defp do_unfollow(%User{} = follower, %User{} = followed) do
case get_follow_state(follower, followed) do
state when state in [:follow_pending, :follow_accept] ->
FollowingRelationship.unfollow(follower, followed)
@@ -778,21 +918,25 @@ defmodule Pleroma.User do
{:ok, follower} =
follower
|> update_following_count()
- |> set_cache()
- {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
+ {:ok, follower, followed}
nil ->
{:error, "Not subscribed!"}
end
end
- defdelegate following?(follower, followed), to: FollowingRelationship
-
@doc "Returns follow state as Pleroma.FollowingRelationship.State value"
def get_follow_state(%User{} = follower, %User{} = following) do
following_relationship = FollowingRelationship.get(follower, following)
+ get_follow_state(follower, following, following_relationship)
+ end
+ def get_follow_state(
+ %User{} = follower,
+ %User{} = following,
+ following_relationship
+ ) do
case {following_relationship, following.local} do
{nil, false} ->
case Utils.fetch_latest_follow(follower, following) do
@@ -851,6 +995,7 @@ defmodule Pleroma.User do
def set_cache(%User{} = user) do
Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
+ Cachex.put(:user_cache, "friends_ap_ids:#{user.nickname}", get_user_friends_ap_ids(user))
{:ok, user}
end
@@ -866,9 +1011,22 @@ defmodule Pleroma.User do
end
end
+ def get_user_friends_ap_ids(user) do
+ from(u in User.get_friends_query(user), select: u.ap_id)
+ |> Repo.all()
+ end
+
+ @spec get_cached_user_friends_ap_ids(User.t()) :: [String.t()]
+ def get_cached_user_friends_ap_ids(user) do
+ Cachex.fetch!(:user_cache, "friends_ap_ids:#{user.ap_id}", fn _ ->
+ get_user_friends_ap_ids(user)
+ end)
+ end
+
def invalidate_cache(user) do
Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
Cachex.del(:user_cache, "nickname:#{user.nickname}")
+ Cachex.del(:user_cache, "friends_ap_ids:#{user.ap_id}")
end
@spec get_cached_by_ap_id(String.t()) :: User.t() | nil
@@ -915,7 +1073,7 @@ defmodule Pleroma.User do
end
def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
- restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
+ restrict_to_local = Config.get([:instance, :limit_to_local_content])
cond do
is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
@@ -932,6 +1090,7 @@ defmodule Pleroma.User do
end
end
+ @spec get_by_nickname(String.t()) :: User.t() | nil
def get_by_nickname(nickname) do
Repo.get_by(User, nickname: nickname) ||
if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
@@ -1031,8 +1190,6 @@ defmodule Pleroma.User do
|> Repo.all()
end
- defdelegate get_follow_requests(user), to: FollowingRelationship
-
def increase_note_count(%User{} = user) do
User
|> where(id: ^user.id)
@@ -1108,35 +1265,25 @@ defmodule Pleroma.User do
])
end
+ @spec update_follower_count(User.t()) :: {:ok, User.t()}
def update_follower_count(%User{} = user) do
- if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
- follower_count_query =
- User.Query.build(%{followers: user, deactivated: false})
- |> select([u], %{count: count(u.id)})
-
- User
- |> where(id: ^user.id)
- |> join(:inner, [u], s in subquery(follower_count_query))
- |> update([u, s],
- set: [follower_count: s.count]
- )
- |> select([u], u)
- |> Repo.update_all([])
- |> case do
- {1, [user]} -> set_cache(user)
- _ -> {:error, user}
- end
+ if user.local or !Config.get([:instance, :external_user_synchronization]) do
+ follower_count = FollowingRelationship.follower_count(user)
+
+ user
+ |> follow_information_changeset(%{follower_count: follower_count})
+ |> update_and_set_cache
else
{:ok, maybe_fetch_follow_information(user)}
end
end
- @spec update_following_count(User.t()) :: User.t()
+ @spec update_following_count(User.t()) :: {:ok, User.t()}
def update_following_count(%User{local: false} = user) do
- if Pleroma.Config.get([:instance, :external_user_synchronization]) do
- maybe_fetch_follow_information(user)
+ if Config.get([:instance, :external_user_synchronization]) do
+ {:ok, maybe_fetch_follow_information(user)}
else
- user
+ {:ok, user}
end
end
@@ -1145,7 +1292,7 @@ defmodule Pleroma.User do
user
|> follow_information_changeset(%{following_count: following_count})
- |> Repo.update!()
+ |> update_and_set_cache()
end
def set_unread_conversation_count(%User{local: true} = user) do
@@ -1189,8 +1336,9 @@ defmodule Pleroma.User do
def increment_unread_conversation_count(_, user), do: {:ok, user}
- @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
- def get_users_from_set(ap_ids, local_only \\ true) do
+ @spec get_users_from_set([String.t()], keyword()) :: [User.t()]
+ def get_users_from_set(ap_ids, opts \\ []) do
+ local_only = Keyword.get(opts, :local_only, true)
criteria = %{ap_id: ap_ids, deactivated: false}
criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
@@ -1219,7 +1367,7 @@ defmodule Pleroma.User do
end
def subscribe(%User{} = subscriber, %User{} = target) do
- deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
+ deny_follow_blocked = Config.get([:user, :deny_follow_blocked])
if blocks?(target, subscriber) and deny_follow_blocked do
{:error, "Could not subscribe: #{target.nickname} is blocking you"}
@@ -1265,7 +1413,8 @@ defmodule Pleroma.User do
unsubscribe(blocked, blocker)
- if following?(blocked, blocker), do: unfollow(blocked, blocker)
+ unfollowing_blocked = Config.get([:activitypub, :unfollow_blocked], true)
+ if unfollowing_blocked && following?(blocked, blocker), do: unfollow(blocked, blocker)
{:ok, blocker} = update_follower_count(blocker)
{:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
@@ -1400,20 +1549,31 @@ defmodule Pleroma.User do
user
|> get_followers()
|> Enum.filter(& &1.local)
- |> Enum.each(fn follower ->
- follower |> update_following_count() |> set_cache()
- end)
+ |> Enum.each(&set_cache(update_following_count(&1)))
# Only update local user counts, remote will be update during the next pull.
user
|> get_friends()
|> Enum.filter(& &1.local)
- |> Enum.each(&update_follower_count/1)
+ |> Enum.each(&do_unfollow(user, &1))
{:ok, user}
end
end
+ def approve(users) when is_list(users) do
+ Repo.transaction(fn ->
+ Enum.map(users, fn user ->
+ with {:ok, user} <- approve(user), do: user
+ end)
+ end)
+ end
+
+ def approve(%User{} = user) do
+ change(user, approval_pending: false)
+ |> update_and_set_cache()
+ end
+
def update_notification_settings(%User{} = user, settings) do
user
|> cast(%{notification_settings: settings}, [])
@@ -1422,6 +1582,49 @@ defmodule Pleroma.User do
|> update_and_set_cache()
end
+ @spec purge_user_changeset(User.t()) :: Changeset.t()
+ def purge_user_changeset(user) do
+ # "Right to be forgotten"
+ # https://gdpr.eu/right-to-be-forgotten/
+ change(user, %{
+ bio: nil,
+ raw_bio: nil,
+ email: nil,
+ name: nil,
+ password_hash: nil,
+ keys: nil,
+ public_key: nil,
+ avatar: %{},
+ tags: [],
+ last_refreshed_at: nil,
+ last_digest_emailed_at: nil,
+ banner: %{},
+ background: %{},
+ note_count: 0,
+ follower_count: 0,
+ following_count: 0,
+ locked: false,
+ confirmation_pending: false,
+ password_reset_pending: false,
+ approval_pending: false,
+ registration_reason: nil,
+ confirmation_token: nil,
+ domain_blocks: [],
+ deactivated: true,
+ ap_enabled: false,
+ is_moderator: false,
+ is_admin: false,
+ mastofe_settings: nil,
+ mascot: nil,
+ emoji: %{},
+ pleroma_settings_store: %{},
+ fields: [],
+ raw_fields: [],
+ discoverable: false,
+ also_known_as: []
+ })
+ end
+
def delete(users) when is_list(users) do
for user <- users, do: delete(user)
end
@@ -1430,12 +1633,34 @@ defmodule Pleroma.User do
BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
end
+ defp delete_and_invalidate_cache(%User{} = user) do
+ invalidate_cache(user)
+ Repo.delete(user)
+ end
+
+ defp delete_or_deactivate(%User{local: false} = user), do: delete_and_invalidate_cache(user)
+
+ defp delete_or_deactivate(%User{local: true} = user) do
+ status = account_status(user)
+
+ case status do
+ :confirmation_pending ->
+ delete_and_invalidate_cache(user)
+
+ :approval_pending ->
+ delete_and_invalidate_cache(user)
+
+ _ ->
+ user
+ |> purge_user_changeset()
+ |> update_and_set_cache()
+ end
+ end
+
def perform(:force_password_reset, user), do: force_password_reset(user)
@spec perform(atom(), User.t()) :: {:ok, User.t()}
def perform(:delete, %User{} = user) do
- {:ok, _user} = ActivityPub.delete(user)
-
# Remove all relationships
user
|> get_followers()
@@ -1452,15 +1677,11 @@ defmodule Pleroma.User do
end)
delete_user_activities(user)
+ delete_notifications_from_user_activities(user)
- if user.local do
- user
- |> change(%{deactivated: true, email: nil})
- |> update_and_set_cache()
- else
- invalidate_cache(user)
- Repo.delete(user)
- end
+ delete_outgoing_pending_follow_requests(user)
+
+ delete_or_deactivate(user)
end
def perform(:deactivate_async, user, status), do: deactivate(user, status)
@@ -1472,8 +1693,7 @@ defmodule Pleroma.User do
blocked_identifiers,
fn blocked_identifier ->
with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
- {:ok, _user_block} <- block(blocker, blocked),
- {:ok, _} <- ActivityPub.block(blocker, blocked) do
+ {:ok, _block} <- CommonAPI.block(blocker, blocked) do
blocked
else
err ->
@@ -1491,7 +1711,7 @@ defmodule Pleroma.User do
fn followed_identifier ->
with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
{:ok, follower} <- maybe_direct_follow(follower, followed),
- {:ok, _} <- ActivityPub.follow(follower, followed) do
+ {:ok, _, _, _} <- CommonAPI.follow(follower, followed) do
followed
else
err ->
@@ -1545,53 +1765,78 @@ defmodule Pleroma.User do
})
end
- def delete_user_activities(%User{ap_id: ap_id}) do
+ def delete_notifications_from_user_activities(%User{ap_id: ap_id}) do
+ Notification
+ |> join(:inner, [n], activity in assoc(n, :activity))
+ |> where([n, a], fragment("? = ?", a.actor, ^ap_id))
+ |> Repo.delete_all()
+ end
+
+ def delete_user_activities(%User{ap_id: ap_id} = user) do
ap_id
|> Activity.Queries.by_actor()
|> RepoStreamer.chunk_stream(50)
- |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
+ |> Stream.each(fn activities ->
+ Enum.each(activities, fn activity -> delete_activity(activity, user) end)
+ end)
|> Stream.run()
end
- defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
- activity
- |> Object.normalize()
- |> ActivityPub.delete()
- end
+ defp delete_activity(%{data: %{"type" => "Create", "object" => object}} = activity, user) do
+ with {_, %Object{}} <- {:find_object, Object.get_by_ap_id(object)},
+ {:ok, delete_data, _} <- Builder.delete(user, object) do
+ Pipeline.common_pipeline(delete_data, local: user.local)
+ else
+ {:find_object, nil} ->
+ # We have the create activity, but not the object, it was probably pruned.
+ # Insert a tombstone and try again
+ with {:ok, tombstone_data, _} <- Builder.tombstone(user.ap_id, object),
+ {:ok, _tombstone} <- Object.create(tombstone_data) do
+ delete_activity(activity, user)
+ end
- defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
- object = Object.normalize(activity)
+ e ->
+ Logger.error("Could not delete #{object} created by #{activity.data["ap_id"]}")
+ Logger.error("Error: #{inspect(e)}")
+ end
+ end
- activity.actor
- |> get_cached_by_ap_id()
- |> ActivityPub.unlike(object)
+ defp delete_activity(%{data: %{"type" => type}} = activity, user)
+ when type in ["Like", "Announce"] do
+ {:ok, undo, _} = Builder.undo(user, activity)
+ Pipeline.common_pipeline(undo, local: user.local)
end
- defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
- object = Object.normalize(activity)
+ defp delete_activity(_activity, _user), do: "Doing nothing"
- activity.actor
- |> get_cached_by_ap_id()
- |> ActivityPub.unannounce(object)
+ defp delete_outgoing_pending_follow_requests(user) do
+ user
+ |> FollowingRelationship.outgoing_pending_follow_requests_query()
+ |> Repo.delete_all()
end
- defp delete_activity(_activity), do: "Doing nothing"
-
def html_filter_policy(%User{no_rich_text: true}) do
Pleroma.HTML.Scrubber.TwitterText
end
- def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
+ def html_filter_policy(_), do: Config.get([:markup, :scrub_policy])
def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
def get_or_fetch_by_ap_id(ap_id) do
- user = get_cached_by_ap_id(ap_id)
+ cached_user = get_cached_by_ap_id(ap_id)
- if !is_nil(user) and !needs_update?(user) do
- {:ok, user}
- else
- fetch_by_ap_id(ap_id)
+ maybe_fetched_user = needs_update?(cached_user) && fetch_by_ap_id(ap_id)
+
+ case {cached_user, maybe_fetched_user} do
+ {_, {:ok, %User{} = user}} ->
+ {:ok, user}
+
+ {%User{} = user, _} ->
+ {:ok, user}
+
+ _ ->
+ {:error, :not_found}
end
end
@@ -1642,8 +1887,7 @@ defmodule Pleroma.User do
|> set_cache()
end
- # AP style
- def public_key(%{source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}}) do
+ def public_key(%{public_key: public_key_pem}) when is_binary(public_key_pem) do
key =
public_key_pem
|> :public_key.pem_decode()
@@ -1653,7 +1897,7 @@ defmodule Pleroma.User do
{:ok, key}
end
- def public_key(_), do: {:error, "not found key"}
+ def public_key(_), do: {:error, "key not found"}
def get_public_key_for_ap_id(ap_id) do
with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
@@ -1664,17 +1908,6 @@ defmodule Pleroma.User do
end
end
- defp blank?(""), do: nil
- defp blank?(n), do: n
-
- def insert_or_update_user(data) do
- data
- |> Map.put(:name, blank?(data[:name]) || data[:nickname])
- |> remote_user_creation()
- |> Repo.insert(on_conflict: {:replace_all_except, [:id]}, conflict_target: :nickname)
- |> set_cache()
- end
-
def ap_enabled?(%User{local: true}), do: true
def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
def ap_enabled?(_), do: false
@@ -1768,7 +2001,7 @@ defmodule Pleroma.User do
end
defp local_nickname_regex do
- if Pleroma.Config.get([:instance, :extended_nickname_format]) do
+ if Config.get([:instance, :extended_nickname_format]) do
@extended_local_nickname_regex
else
@strict_local_nickname_regex
@@ -1800,8 +2033,12 @@ defmodule Pleroma.User do
|> Repo.all()
end
+ def muting_reblogs?(%User{} = user, %User{} = target) do
+ UserRelationship.reblog_mute_exists?(user, target)
+ end
+
def showing_reblogs?(%User{} = user, %User{} = target) do
- not UserRelationship.reblog_mute_exists?(user, target)
+ not muting_reblogs?(user, target)
end
@doc """
@@ -1892,8 +2129,8 @@ defmodule Pleroma.User do
def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
# use instance-default
- config = Pleroma.Config.get([:assets, :mascots])
- default_mascot = Pleroma.Config.get([:assets, :default_mascot])
+ config = Config.get([:assets, :mascots])
+ default_mascot = Config.get([:assets, :default_mascot])
mascot = Keyword.get(config, default_mascot)
%{
@@ -1925,12 +2162,10 @@ defmodule Pleroma.User do
|> Repo.all()
end
- defdelegate search(query, opts \\ []), to: User.Search
-
defp put_password_hash(
%Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
) do
- change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
+ change(changeset, password_hash: Pbkdf2.hash_pwd_salt(password))
end
defp put_password_hash(changeset), do: changeset
@@ -1979,12 +2214,6 @@ defmodule Pleroma.User do
|> update_and_set_cache()
end
- def update_source_data(user, source_data) do
- user
- |> cast(%{source_data: source_data}, [:source_data])
- |> update_and_set_cache()
- end
-
def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
%{
admin: is_admin,
@@ -1992,24 +2221,9 @@ defmodule Pleroma.User do
}
end
- # ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.
- # For example: [{"name": "Pronoun", "value": "she/her"}, …]
- def fields(%{fields: nil, source_data: %{"attachment" => attachment}}) do
- limit = Pleroma.Config.get([:instance, :max_remote_account_fields], 0)
-
- attachment
- |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
- |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
- |> Enum.take(limit)
- end
-
- def fields(%{fields: nil}), do: []
-
- def fields(%{fields: fields}), do: fields
-
def validate_fields(changeset, remote? \\ false) do
limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
- limit = Pleroma.Config.get([:instance, limit_name], 0)
+ limit = Config.get([:instance, limit_name], 0)
changeset
|> validate_length(:fields, max: limit)
@@ -2023,8 +2237,8 @@ defmodule Pleroma.User do
end
defp valid_field?(%{"name" => name, "value" => value}) do
- name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
- value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
+ name_limit = Config.get([:instance, :account_field_name_length], 255)
+ value_limit = Config.get([:instance, :account_field_value_length], 255)
is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
String.length(value) <= value_limit
@@ -2034,10 +2248,10 @@ defmodule Pleroma.User do
defp truncate_field(%{"name" => name, "value" => value}) do
{name, _chopped} =
- String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255))
+ String.split_at(name, Config.get([:instance, :account_field_name_length], 255))
{value, _chopped} =
- String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255))
+ String.split_at(value, Config.get([:instance, :account_field_value_length], 255))
%{"name" => name, "value" => value}
end
@@ -2067,8 +2281,8 @@ defmodule Pleroma.User do
def mastodon_settings_update(user, settings) do
user
- |> cast(%{settings: settings}, [:settings])
- |> validate_required([:settings])
+ |> cast(%{mastofe_settings: settings}, [:mastofe_settings])
+ |> validate_required([:mastofe_settings])
|> update_and_set_cache()
end
@@ -2090,9 +2304,15 @@ defmodule Pleroma.User do
cast(user, params, [:confirmation_pending, :confirmation_token])
end
+ @spec approval_changeset(User.t(), keyword()) :: Changeset.t()
+ def approval_changeset(user, need_approval: need_approval?) do
+ params = if need_approval?, do: %{approval_pending: true}, else: %{approval_pending: false}
+ cast(user, params, [:approval_pending])
+ end
+
def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
if id not in user.pinned_activities do
- max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0)
+ max_pinned_statuses = Config.get([:instance, :max_pinned_statuses], 0)
params = %{pinned_activities: user.pinned_activities ++ [id]}
user
@@ -2194,9 +2414,7 @@ defmodule Pleroma.User do
# - display name
def sanitize_html(%User{} = user, filter) do
fields =
- user
- |> User.fields()
- |> Enum.map(fn %{"name" => name, "value" => value} ->
+ Enum.map(user.fields, fn %{"name" => name, "value" => value} ->
%{
"name" => name,
"value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
diff --git a/lib/pleroma/user/notification_setting.ex b/lib/pleroma/user/notification_setting.ex
index 4bd55e139..7d9e8a000 100644
--- a/lib/pleroma/user/notification_setting.ex
+++ b/lib/pleroma/user/notification_setting.ex
@@ -10,21 +10,15 @@ defmodule Pleroma.User.NotificationSetting do
@primary_key false
embedded_schema do
- field(:followers, :boolean, default: true)
- field(:follows, :boolean, default: true)
- field(:non_follows, :boolean, default: true)
- field(:non_followers, :boolean, default: true)
- field(:privacy_option, :boolean, default: false)
+ field(:block_from_strangers, :boolean, default: false)
+ field(:hide_notification_contents, :boolean, default: false)
end
def changeset(schema, params) do
schema
|> cast(prepare_attrs(params), [
- :followers,
- :follows,
- :non_follows,
- :non_followers,
- :privacy_option
+ :block_from_strangers,
+ :hide_notification_contents
])
end
diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex
index a387f9b77..d618432ff 100644
--- a/lib/pleroma/user/query.ex
+++ b/lib/pleroma/user/query.ex
@@ -42,9 +42,11 @@ defmodule Pleroma.User.Query do
external: boolean(),
active: boolean(),
deactivated: boolean(),
+ need_approval: boolean(),
is_admin: boolean(),
is_moderator: boolean(),
super_users: boolean(),
+ invisible: boolean(),
followers: User.t(),
friends: User.t(),
recipients_from_activity: [String.t()],
@@ -54,13 +56,13 @@ defmodule Pleroma.User.Query do
select: term(),
limit: pos_integer()
}
- | %{}
+ | map()
@ilike_criteria [:nickname, :name, :query]
@equal_criteria [:email]
@contains_criteria [:ap_id, :nickname]
- @spec build(criteria()) :: Query.t()
+ @spec build(Query.t(), criteria()) :: Query.t()
def build(query \\ base_query(), criteria) do
prepare_query(query, criteria)
end
@@ -88,6 +90,10 @@ defmodule Pleroma.User.Query do
where(query, [u], ilike(field(u, ^key), ^"%#{value}%"))
end
+ defp compose_query({:invisible, bool}, query) when is_boolean(bool) do
+ where(query, [u], u.invisible == ^bool)
+ end
+
defp compose_query({key, value}, query)
when key in @equal_criteria and not_empty_string(value) do
where(query, [u], ^[{key, value}])
@@ -98,7 +104,7 @@ defmodule Pleroma.User.Query do
end
defp compose_query({:tags, tags}, query) when is_list(tags) and length(tags) > 0 do
- Enum.reduce(tags, query, &prepare_tag_criteria/2)
+ where(query, [u], fragment("? && ?", u.tags, ^tags))
end
defp compose_query({:is_admin, _}, query) do
@@ -124,6 +130,7 @@ defmodule Pleroma.User.Query do
defp compose_query({:active, _}, query) do
User.restrict_deactivated(query)
|> where([u], not is_nil(u.nickname))
+ |> where([u], u.approval_pending == false)
end
defp compose_query({:legacy_active, _}, query) do
@@ -141,6 +148,10 @@ defmodule Pleroma.User.Query do
|> where([u], not is_nil(u.nickname))
end
+ defp compose_query({:need_approval, _}, query) do
+ where(query, [u], u.approval_pending)
+ end
+
defp compose_query({:followers, %User{id: id}}, query) do
query
|> where([u], u.id != ^id)
@@ -190,10 +201,6 @@ defmodule Pleroma.User.Query do
defp compose_query(_unsupported_param, query), do: query
- defp prepare_tag_criteria(tag, query) do
- or_where(query, [u], fragment("? = any(?)", ^tag, u.tags))
- end
-
defp location_query(query, local) do
where(query, [u], u.local == ^local)
|> where([u], not is_nil(u.nickname))
diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex
index cec59c372..d4fd31069 100644
--- a/lib/pleroma/user/search.ex
+++ b/lib/pleroma/user/search.ex
@@ -52,6 +52,7 @@ defmodule Pleroma.User.Search do
|> base_query(following)
|> filter_blocked_user(for_user)
|> filter_invisible_users()
+ |> filter_internal_users()
|> filter_blocked_domains(for_user)
|> fts_search(query_string)
|> trigram_rank(query_string)
@@ -68,11 +69,15 @@ defmodule Pleroma.User.Search do
u in query,
where:
fragment(
+ # The fragment must _exactly_ match `users_fts_index`, otherwise the index won't work
"""
- (to_tsvector('simple', ?) || to_tsvector('simple', ?)) @@ to_tsquery('simple', ?)
+ (
+ setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
+ setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')
+ ) @@ to_tsquery('simple', ?)
""",
- u.name,
u.nickname,
+ u.name,
^query_string
)
)
@@ -87,15 +92,23 @@ defmodule Pleroma.User.Search do
|> Enum.join(" | ")
end
+ # Considers nickname match, localized nickname match, name match; preferences nickname match
defp trigram_rank(query, query_string) do
from(
u in query,
select_merge: %{
search_rank:
fragment(
- "similarity(?, trim(? || ' ' || coalesce(?, '')))",
+ """
+ similarity(?, ?) +
+ similarity(?, regexp_replace(?, '@.+', '')) +
+ similarity(?, trim(coalesce(?, '')))
+ """,
^query_string,
u.nickname,
+ ^query_string,
+ u.nickname,
+ ^query_string,
u.name
)
}
@@ -109,6 +122,10 @@ defmodule Pleroma.User.Search do
from(q in query, where: q.invisible == false)
end
+ defp filter_internal_users(query) do
+ from(q in query, where: q.actor_type != "Application")
+ end
+
defp filter_blocked_user(query, %User{} = blocker) do
query
|> join(:left, [u], b in Pleroma.UserRelationship,
diff --git a/lib/pleroma/user/welcome_chat_message.ex b/lib/pleroma/user/welcome_chat_message.ex
new file mode 100644
index 000000000..3e7d1f424
--- /dev/null
+++ b/lib/pleroma/user/welcome_chat_message.ex
@@ -0,0 +1,45 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.User.WelcomeChatMessage do
+ alias Pleroma.Config
+ alias Pleroma.User
+ alias Pleroma.Web.CommonAPI
+
+ @spec enabled?() :: boolean()
+ def enabled?, do: Config.get([:welcome, :chat_message, :enabled], false)
+
+ @spec post_message(User.t()) :: {:ok, Pleroma.Activity.t() | nil}
+ def post_message(user) do
+ [:welcome, :chat_message, :sender_nickname]
+ |> Config.get(nil)
+ |> fetch_sender()
+ |> do_post(user, welcome_message())
+ end
+
+ defp do_post(%User{} = sender, recipient, message)
+ when is_binary(message) do
+ CommonAPI.post_chat_message(
+ sender,
+ recipient,
+ message
+ )
+ end
+
+ defp do_post(_sender, _recipient, _message), do: {:ok, nil}
+
+ defp fetch_sender(nickname) when is_binary(nickname) do
+ with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
+ user
+ else
+ _ -> nil
+ end
+ end
+
+ defp fetch_sender(_), do: nil
+
+ defp welcome_message do
+ Config.get([:welcome, :chat_message, :message], nil)
+ end
+end
diff --git a/lib/pleroma/user/welcome_email.ex b/lib/pleroma/user/welcome_email.ex
new file mode 100644
index 000000000..5322000d4
--- /dev/null
+++ b/lib/pleroma/user/welcome_email.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.User.WelcomeEmail do
+ @moduledoc """
+ The module represents the functions to send welcome email.
+ """
+
+ alias Pleroma.Config
+ alias Pleroma.Emails
+ alias Pleroma.User
+
+ import Pleroma.Config.Helpers, only: [instance_name: 0]
+
+ @spec enabled?() :: boolean()
+ def enabled?, do: Config.get([:welcome, :email, :enabled], false)
+
+ @spec send_email(User.t()) :: {:ok, Oban.Job.t()}
+ def send_email(%User{} = user) do
+ user
+ |> Emails.UserEmail.welcome(email_options(user))
+ |> Emails.Mailer.deliver_async()
+ end
+
+ defp email_options(user) do
+ bindings = [user: user, instance_name: instance_name()]
+
+ %{}
+ |> add_sender(Config.get([:welcome, :email, :sender], nil))
+ |> add_option(:subject, bindings)
+ |> add_option(:html, bindings)
+ |> add_option(:text, bindings)
+ end
+
+ defp add_option(opts, option, bindings) do
+ [:welcome, :email, option]
+ |> Config.get(nil)
+ |> eval_string(bindings)
+ |> merge_options(opts, option)
+ end
+
+ defp add_sender(opts, {_name, _email} = sender) do
+ merge_options(sender, opts, :sender)
+ end
+
+ defp add_sender(opts, sender) when is_binary(sender) do
+ add_sender(opts, {instance_name(), sender})
+ end
+
+ defp add_sender(opts, _), do: opts
+
+ defp merge_options(nil, options, _option), do: options
+
+ defp merge_options(value, options, option) do
+ Map.merge(options, %{option => value})
+ end
+
+ defp eval_string(nil, _), do: nil
+ defp eval_string("", _), do: nil
+ defp eval_string(str, bindings), do: EEx.eval_string(str, bindings)
+end
diff --git a/lib/pleroma/user/welcome_message.ex b/lib/pleroma/user/welcome_message.ex
index f0ac8ebae..86e1c0678 100644
--- a/lib/pleroma/user/welcome_message.ex
+++ b/lib/pleroma/user/welcome_message.ex
@@ -3,32 +3,45 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.User.WelcomeMessage do
+ alias Pleroma.Config
alias Pleroma.User
alias Pleroma.Web.CommonAPI
- def post_welcome_message_to_user(user) do
- with %User{} = sender_user <- welcome_user(),
- message when is_binary(message) <- welcome_message() do
- CommonAPI.post(sender_user, %{
- "visibility" => "direct",
- "status" => "@#{user.nickname}\n#{message}"
- })
- else
- _ -> {:ok, nil}
- end
+ @spec enabled?() :: boolean()
+ def enabled?, do: Config.get([:welcome, :direct_message, :enabled], false)
+
+ @spec post_message(User.t()) :: {:ok, Pleroma.Activity.t() | nil}
+ def post_message(user) do
+ [:welcome, :direct_message, :sender_nickname]
+ |> Config.get(nil)
+ |> fetch_sender()
+ |> do_post(user, welcome_message())
end
- defp welcome_user do
- with nickname when is_binary(nickname) <-
- Pleroma.Config.get([:instance, :welcome_user_nickname]),
- %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
+ defp do_post(%User{} = sender, %User{nickname: nickname}, message)
+ when is_binary(message) do
+ CommonAPI.post(
+ sender,
+ %{
+ visibility: "direct",
+ status: "@#{nickname}\n#{message}"
+ }
+ )
+ end
+
+ defp do_post(_sender, _recipient, _message), do: {:ok, nil}
+
+ defp fetch_sender(nickname) when is_binary(nickname) do
+ with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
user
else
_ -> nil
end
end
+ defp fetch_sender(_), do: nil
+
defp welcome_message do
- Pleroma.Config.get([:instance, :welcome_message])
+ Config.get([:welcome, :direct_message, :message], nil)
end
end
diff --git a/lib/pleroma/user_relationship.ex b/lib/pleroma/user_relationship.ex
index ad0d303b1..6dfdd2860 100644
--- a/lib/pleroma/user_relationship.ex
+++ b/lib/pleroma/user_relationship.ex
@@ -87,6 +87,22 @@ defmodule Pleroma.UserRelationship do
source_to_target_rel_types \\ nil,
target_to_source_rel_types \\ nil
)
+
+ def dictionary(
+ _source_users,
+ _target_users,
+ [] = _source_to_target_rel_types,
+ [] = _target_to_source_rel_types
+ ) do
+ []
+ end
+
+ def dictionary(
+ source_users,
+ target_users,
+ source_to_target_rel_types,
+ target_to_source_rel_types
+ )
when is_list(source_users) and is_list(target_users) do
source_user_ids = User.binary_id(source_users)
target_user_ids = User.binary_id(target_users)
@@ -130,20 +146,45 @@ defmodule Pleroma.UserRelationship do
end
@doc ":relationships option for StatusView / AccountView / NotificationView"
- def view_relationships_option(nil = _reading_user, _actors) do
+ def view_relationships_option(reading_user, actors, opts \\ [])
+
+ def view_relationships_option(nil = _reading_user, _actors, _opts) do
%{user_relationships: [], following_relationships: []}
end
- def view_relationships_option(%User{} = reading_user, actors) do
+ def view_relationships_option(%User{} = reading_user, actors, opts) do
+ {source_to_target_rel_types, target_to_source_rel_types} =
+ case opts[:subset] do
+ :source_mutes ->
+ # Used for statuses rendering (FE needs `muted` flag for each status when statuses load)
+ {[:mute], []}
+
+ nil ->
+ {[:block, :mute, :notification_mute, :reblog_mute], [:block, :inverse_subscription]}
+
+ unknown ->
+ raise "Unsupported :subset option value: #{inspect(unknown)}"
+ end
+
user_relationships =
UserRelationship.dictionary(
[reading_user],
actors,
- [:block, :mute, :notification_mute, :reblog_mute],
- [:block, :inverse_subscription]
+ source_to_target_rel_types,
+ target_to_source_rel_types
)
- following_relationships = FollowingRelationship.all_between_user_sets([reading_user], actors)
+ following_relationships =
+ case opts[:subset] do
+ :source_mutes ->
+ []
+
+ nil ->
+ FollowingRelationship.all_between_user_sets([reading_user], actors)
+
+ unknown ->
+ raise "Unsupported :subset option value: #{inspect(unknown)}"
+ end
%{user_relationships: user_relationships, following_relationships: following_relationships}
end
diff --git a/lib/pleroma/utils.ex b/lib/pleroma/utils.ex
index 6b8e3accf..21d1159be 100644
--- a/lib/pleroma/utils.ex
+++ b/lib/pleroma/utils.ex
@@ -9,4 +9,19 @@ defmodule Pleroma.Utils do
|> Enum.map(&Path.join(dir, &1))
|> Kernel.ParallelCompiler.compile()
end
+
+ @doc """
+ POSIX-compliant check if command is available in the system
+
+ ## Examples
+ iex> command_available?("git")
+ true
+ iex> command_available?("wrongcmd")
+ false
+
+ """
+ @spec command_available?(String.t()) :: boolean()
+ def command_available?(command) do
+ match?({_output, 0}, System.cmd("sh", ["-c", "command -v #{command}"]))
+ end
end
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 5ce91a8d3..624a508ae 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -5,10 +5,13 @@
defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.Activity
alias Pleroma.Activity.Ir.Topics
+ alias Pleroma.ActivityExpiration
alias Pleroma.Config
alias Pleroma.Constants
alias Pleroma.Conversation
alias Pleroma.Conversation.Participation
+ alias Pleroma.Filter
+ alias Pleroma.Maps
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Object.Containment
@@ -19,7 +22,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.User
alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.ActivityPub.Transmogrifier
- alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Streamer
alias Pleroma.Web.WebFinger
alias Pleroma.Workers.BackgroundWorker
@@ -31,25 +33,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
require Logger
require Pleroma.Constants
- # For Announce activities, we filter the recipients based on following status for any actors
- # that match actual users. See issue #164 for more information about why this is necessary.
- defp get_recipients(%{"type" => "Announce"} = data) do
- to = Map.get(data, "to", [])
- cc = Map.get(data, "cc", [])
- bcc = Map.get(data, "bcc", [])
- actor = User.get_cached_by_ap_id(data["actor"])
-
- recipients =
- Enum.filter(Enum.concat([to, cc, bcc]), fn recipient ->
- case User.get_cached_by_ap_id(recipient) do
- nil -> true
- user -> User.following?(user, actor)
- end
- end)
-
- {recipients, to, cc}
- end
-
defp get_recipients(%{"type" => "Create"} = data) do
to = Map.get(data, "to", [])
cc = Map.get(data, "cc", [])
@@ -67,16 +50,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
{recipients, to, cc}
end
- defp check_actor_is_active(actor) do
- if not is_nil(actor) do
- with user <- User.get_cached_by_ap_id(actor),
- false <- user.deactivated do
- true
- else
- _e -> false
- end
- else
- true
+ defp check_actor_is_active(nil), do: true
+
+ defp check_actor_is_active(actor) when is_binary(actor) do
+ case User.get_cached_by_ap_id(actor) do
+ %User{deactivated: deactivated} -> not deactivated
+ _ -> false
end
end
@@ -95,36 +74,39 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
if is_public?(object), do: User.decrease_note_count(actor), else: {:ok, actor}
end
- def increase_replies_count_if_reply(%{
- "object" => %{"inReplyTo" => reply_ap_id} = object,
- "type" => "Create"
- }) do
+ defp increase_replies_count_if_reply(%{
+ "object" => %{"inReplyTo" => reply_ap_id} = object,
+ "type" => "Create"
+ }) do
if is_public?(object) do
Object.increase_replies_count(reply_ap_id)
end
end
- def increase_replies_count_if_reply(_create_data), do: :noop
+ defp increase_replies_count_if_reply(_create_data), do: :noop
- def decrease_replies_count_if_reply(%Object{
- data: %{"inReplyTo" => reply_ap_id} = object
- }) do
- if is_public?(object) do
- Object.decrease_replies_count(reply_ap_id)
+ @object_types ~w[ChatMessage Question Answer Audio Event]
+ @spec persist(map(), keyword()) :: {:ok, Activity.t() | Object.t()}
+ def persist(%{"type" => type} = object, meta) when type in @object_types do
+ with {:ok, object} <- Object.create(object) do
+ {:ok, object, meta}
end
end
- def decrease_replies_count_if_reply(_object), do: :noop
-
- def increase_poll_votes_if_vote(%{
- "object" => %{"inReplyTo" => reply_ap_id, "name" => name},
- "type" => "Create"
- }) do
- Object.increase_vote_count(reply_ap_id, name)
+ 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
- def increase_poll_votes_if_vote(_create_data), do: :noop
-
@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),
@@ -137,29 +119,20 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
{:containment, :ok} <- {:containment, Containment.contain_child(map)},
{:ok, map, object} <- insert_full_object(map) do
{:ok, activity} =
- Repo.insert(%Activity{
+ %Activity{
data: map,
local: local,
actor: map["actor"],
recipients: recipients
- })
+ }
+ |> Repo.insert()
+ |> maybe_create_activity_expiration()
# Splice in the child object if we have one.
- activity =
- if not is_nil(object) do
- Map.put(activity, :object, object)
- else
- activity
- end
+ activity = Maps.put_if_present(activity, :object, object)
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 ->
@@ -182,10 +155,27 @@ 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 maybe_create_activity_expiration({:ok, %{data: %{"expires_at" => expires_at}} = activity}) do
+ with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
+ {:ok, activity}
+ end
+ end
+
+ defp maybe_create_activity_expiration(result), do: result
+
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),
- Participation.mark_as_read(user, conversation) do
+ %User{} = user <- User.get_cached_by_ap_id(actor) do
+ Participation.mark_as_read(user, conversation)
{:ok, conversation}
end
end
@@ -207,13 +197,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
def stream_out_participations(%Object{data: %{"context" => context}}, user) do
- with %Conversation{} = conversation <- Conversation.get_for_ap_id(context),
- conversation = Repo.preload(conversation, :participations),
- last_activity_id =
- fetch_latest_activity_id_for_context(conversation.ap_id, %{
- "user" => user,
- "blocking_user" => user
- }) do
+ with %Conversation{} = conversation <- Conversation.get_for_ap_id(context) do
+ conversation = Repo.preload(conversation, :participations)
+
+ last_activity_id =
+ fetch_latest_direct_activity_id_for_context(conversation.ap_id, %{
+ user: user,
+ blocking_user: user
+ })
+
if last_activity_id do
stream_out_participations(conversation.participations)
end
@@ -247,17 +239,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
published = params[:published]
quick_insert? = Config.get([:env]) == :benchmark
- with create_data <-
- make_create_data(
- %{to: to, actor: actor, published: published, context: context, object: object},
- additional
- ),
- {:ok, activity} <- insert(create_data, local, fake),
+ create_data =
+ make_create_data(
+ %{to: to, actor: actor, published: published, context: context, object: object},
+ additional
+ )
+
+ with {:ok, activity} <- insert(create_data, local, fake),
{:fake, false, activity} <- {:fake, fake, activity},
_ <- increase_replies_count_if_reply(create_data),
- _ <- 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
@@ -279,235 +272,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
local = !(params[:local] == false)
published = params[:published]
- with listen_data <-
- make_listen_data(
- %{to: to, actor: actor, published: published, context: context, object: object},
- additional
- ),
- {:ok, activity} <- insert(listen_data, local),
- :ok <- maybe_federate(activity) do
- {:ok, activity}
- 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)
-
- with data <-
- %{"to" => to, "type" => type, "actor" => actor.ap_id, "object" => object}
- |> Utils.maybe_put("id", activity_id),
- {:ok, activity} <- insert(data, local),
- :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]
-
- with data <- %{
- "to" => to,
- "cc" => cc,
- "type" => "Update",
- "actor" => actor,
- "object" => object
- },
- data <- Utils.maybe_put(data, "id", activity_id),
- {:ok, activity} <- insert(data, local),
- :ok <- maybe_federate(activity) do
- {:ok, activity}
- end
- end
-
- @spec react_with_emoji(User.t(), Object.t(), String.t(), keyword()) ::
- {:ok, Activity.t(), Object.t()} | {:error, any()}
- def react_with_emoji(user, object, emoji, options \\ []) do
- with {:ok, result} <-
- Repo.transaction(fn -> do_react_with_emoji(user, object, emoji, options) end) do
- result
- end
- end
-
- defp do_react_with_emoji(user, object, emoji, options) do
- with local <- Keyword.get(options, :local, true),
- activity_id <- Keyword.get(options, :activity_id, nil),
- true <- Pleroma.Emoji.is_unicode_emoji?(emoji),
- reaction_data <- make_emoji_reaction_data(user, object, emoji, activity_id),
- {:ok, activity} <- insert(reaction_data, local),
- {:ok, object} <- add_emoji_reaction_to_object(activity, object),
- :ok <- maybe_federate(activity) do
- {:ok, activity, object}
- else
- false -> {:error, false}
- {:error, error} -> Repo.rollback(error)
- end
- end
-
- @spec unreact_with_emoji(User.t(), String.t(), keyword()) ::
- {:ok, Activity.t(), Object.t()} | {:error, any()}
- def unreact_with_emoji(user, reaction_id, options \\ []) do
- with {:ok, result} <-
- Repo.transaction(fn -> do_unreact_with_emoji(user, reaction_id, options) end) do
- result
- end
- end
-
- defp do_unreact_with_emoji(user, reaction_id, options) do
- with local <- Keyword.get(options, :local, true),
- activity_id <- Keyword.get(options, :activity_id, nil),
- user_ap_id <- user.ap_id,
- %Activity{actor: ^user_ap_id} = reaction_activity <- Activity.get_by_ap_id(reaction_id),
- object <- Object.normalize(reaction_activity),
- unreact_data <- make_undo_data(user, reaction_activity, activity_id),
- {:ok, activity} <- insert(unreact_data, local),
- {:ok, object} <- remove_emoji_reaction_from_object(reaction_activity, object),
- :ok <- maybe_federate(activity) do
- {:ok, activity, object}
- else
- {:error, error} -> Repo.rollback(error)
- end
- end
-
- # TODO: This is weird, maybe we shouldn't check here if we can make the activity.
- @spec like(User.t(), Object.t(), String.t() | nil, boolean()) ::
- {:ok, Activity.t(), Object.t()} | {:error, any()}
- def like(user, object, activity_id \\ nil, local \\ true) do
- with {:ok, result} <- Repo.transaction(fn -> do_like(user, object, activity_id, local) end) do
- result
- end
- end
-
- defp do_like(
- %User{ap_id: ap_id} = user,
- %Object{data: %{"id" => _}} = object,
- activity_id,
- local
- ) 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} ->
- Repo.rollback(error)
- end
- end
-
- @spec unlike(User.t(), Object.t(), String.t() | nil, boolean()) ::
- {:ok, Activity.t(), Activity.t(), Object.t()} | {:ok, Object.t()} | {:error, any()}
- def unlike(%User{} = actor, %Object{} = object, activity_id \\ nil, local \\ true) do
- with {:ok, result} <-
- Repo.transaction(fn -> do_unlike(actor, object, activity_id, local) end) do
- result
- end
- end
-
- defp do_unlike(actor, object, activity_id, local) do
- with %Activity{} = like_activity <- get_existing_like(actor.ap_id, object),
- unlike_data <- make_unlike_data(actor, like_activity, activity_id),
- {:ok, unlike_activity} <- insert(unlike_data, local),
- {:ok, _activity} <- Repo.delete(like_activity),
- {:ok, object} <- remove_like_from_object(like_activity, object),
- :ok <- maybe_federate(unlike_activity) do
- {:ok, unlike_activity, like_activity, object}
- else
- nil -> {:ok, object}
- {:error, error} -> Repo.rollback(error)
- end
- end
-
- @spec announce(User.t(), Object.t(), String.t() | nil, boolean(), boolean()) ::
- {:ok, Activity.t(), Object.t()} | {:error, any()}
- def announce(
- %User{ap_id: _} = user,
- %Object{data: %{"id" => _}} = object,
- activity_id \\ nil,
- local \\ true,
- public \\ true
- ) do
- with {:ok, result} <-
- Repo.transaction(fn -> do_announce(user, object, activity_id, local, public) end) do
- result
- end
- end
+ listen_data =
+ make_listen_data(
+ %{to: to, actor: actor, published: published, context: context, object: object},
+ additional
+ )
- defp do_announce(user, object, activity_id, local, public) do
- with true <- is_announceable?(object, user, public),
- announce_data <- make_announce_data(user, object, activity_id, public),
- {:ok, activity} <- insert(announce_data, local),
- {:ok, object} <- add_announce_to_object(activity, object),
+ with {:ok, activity} <- insert(listen_data, local),
+ _ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
- {:ok, activity, object}
- else
- false -> {:error, false}
- {:error, error} -> Repo.rollback(error)
- end
- end
-
- @spec unannounce(User.t(), Object.t(), String.t() | nil, boolean()) ::
- {:ok, Activity.t(), Object.t()} | {:ok, Object.t()} | {:error, any()}
- def unannounce(
- %User{} = actor,
- %Object{} = object,
- activity_id \\ nil,
- local \\ true
- ) do
- with {:ok, result} <-
- Repo.transaction(fn -> do_unannounce(actor, object, activity_id, local) end) do
- result
- end
- end
-
- defp do_unannounce(actor, object, activity_id, local) do
- with %Activity{} = announce_activity <- get_existing_announce(actor.ap_id, object),
- unannounce_data <- make_unannounce_data(actor, announce_activity, activity_id),
- {:ok, unannounce_activity} <- insert(unannounce_data, local),
- :ok <- maybe_federate(unannounce_activity),
- {:ok, _activity} <- Repo.delete(announce_activity),
- {:ok, object} <- remove_announce_from_object(announce_activity, object) do
- {:ok, unannounce_activity, object}
- else
- nil -> {:ok, object}
- {:error, error} -> Repo.rollback(error)
- end
- end
-
- @spec follow(User.t(), User.t(), String.t() | nil, boolean()) ::
- {:ok, Activity.t()} | {:error, any()}
- def follow(follower, followed, activity_id \\ nil, local \\ true) do
- with {:ok, result} <-
- Repo.transaction(fn -> do_follow(follower, followed, activity_id, local) end) do
- result
- end
- end
-
- 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
{:ok, activity}
- else
- {:error, error} -> Repo.rollback(error)
end
end
@@ -525,114 +299,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
{:ok, follow_activity} <- update_follow_state(follow_activity, "cancelled"),
unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_id),
{:ok, activity} <- insert(unfollow_data, local),
- :ok <- maybe_federate(activity) do
- {:ok, activity}
- else
- nil -> nil
- {:error, error} -> Repo.rollback(error)
- end
- end
-
- @spec delete(User.t() | Object.t(), keyword()) :: {:ok, User.t() | Object.t()} | {:error, any()}
- def delete(entity, options \\ []) do
- with {:ok, result} <- Repo.transaction(fn -> do_delete(entity, options) end) do
- result
- end
- end
-
- defp do_delete(%User{ap_id: ap_id, follower_address: follower_address} = user, _) do
- with data <- %{
- "to" => [follower_address],
- "type" => "Delete",
- "actor" => ap_id,
- "object" => %{"type" => "Person", "id" => ap_id}
- },
- {:ok, activity} <- insert(data, true, true, true),
- :ok <- maybe_federate(activity) do
- {:ok, user}
- end
- end
-
- defp do_delete(%Object{data: %{"id" => id, "actor" => actor}} = object, options) do
- local = Keyword.get(options, :local, true)
- activity_id = Keyword.get(options, :activity_id, nil)
- actor = Keyword.get(options, :actor, actor)
-
- user = User.get_cached_by_ap_id(actor)
- to = (object.data["to"] || []) ++ (object.data["cc"] || [])
-
- with create_activity <- Activity.get_create_by_object_ap_id(id),
- data <-
- %{
- "type" => "Delete",
- "actor" => actor,
- "object" => id,
- "to" => to,
- "deleted_activity_id" => create_activity && create_activity.id
- }
- |> maybe_put("id", activity_id),
- {:ok, activity} <- insert(data, local, false),
- {:ok, object, _create_activity} <- Object.delete(object),
- stream_out_participations(object, user),
- _ <- decrease_replies_count_if_reply(object),
- {:ok, _actor} <- decrease_note_count_if_public(user, object),
- :ok <- maybe_federate(activity) do
- {:ok, activity}
- else
- {:error, error} ->
- Repo.rollback(error)
- end
- end
-
- defp do_delete(%Object{data: %{"type" => "Tombstone", "id" => ap_id}}, _) do
- activity =
- ap_id
- |> Activity.Queries.by_object_id()
- |> Activity.Queries.by_type("Delete")
- |> Repo.one()
-
- {:ok, activity}
- end
-
- @spec block(User.t(), User.t(), String.t() | nil, boolean()) ::
- {:ok, Activity.t()} | {:error, any()}
- def block(blocker, blocked, activity_id \\ nil, local \\ true) do
- with {:ok, result} <-
- Repo.transaction(fn -> do_block(blocker, blocked, activity_id, local) end) do
- result
- end
- end
-
- defp do_block(blocker, blocked, activity_id, local) do
- unfollow_blocked = Config.get([:activitypub, :unfollow_blocked])
-
- if unfollow_blocked do
- follow_activity = fetch_latest_follow(blocker, blocked)
- if follow_activity, do: unfollow(blocker, blocked, nil, local)
- end
-
- with block_data <- make_block_data(blocker, blocked, activity_id),
- {:ok, activity} <- insert(block_data, local),
- :ok <- maybe_federate(activity) do
- {:ok, activity}
- else
- {:error, error} -> Repo.rollback(error)
- end
- end
-
- @spec unblock(User.t(), User.t(), String.t() | nil, boolean()) ::
- {:ok, Activity.t()} | {:error, any()} | nil
- def unblock(blocker, blocked, activity_id \\ nil, local \\ true) do
- with {:ok, result} <-
- Repo.transaction(fn -> do_unblock(blocker, blocked, activity_id, local) end) do
- result
- end
- end
-
- defp do_unblock(blocker, blocked, activity_id, local) do
- with %Activity{} = block_activity <- fetch_latest_block(blocker, blocked),
- unblock_data <- make_unblock_data(blocker, blocked, block_activity, activity_id),
- {:ok, activity} <- insert(unblock_data, local),
+ _ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
@@ -667,6 +334,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
with flag_data <- make_flag_data(params, additional),
{:ok, activity} <- insert(flag_data, local),
{:ok, stripped_activity} <- strip_report_status_data(activity),
+ _ <- notify_and_stream(activity),
:ok <- maybe_federate(stripped_activity) do
User.all_superusers()
|> Enum.filter(fn user -> not is_nil(user.email) end)
@@ -690,7 +358,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", %{
@@ -705,12 +374,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
- defp fetch_activities_for_context_query(context, opts) do
+ def fetch_activities_for_context_query(context, opts) do
public = [Constants.as_public()]
recipients =
- if opts["user"],
- do: [opts["user"].ap_id | User.following(opts["user"])] ++ public,
+ if opts[:user],
+ do: [opts[:user].ap_id | User.following(opts[:user])] ++ public,
else: public
from(activity in Activity)
@@ -718,7 +387,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> maybe_preload_bookmarks(opts)
|> maybe_set_thread_muted_field(opts)
|> restrict_blocked(opts)
- |> restrict_recipients(recipients, opts["user"])
+ |> restrict_recipients(recipients, opts[:user])
+ |> restrict_filtered(opts)
|> where(
[activity],
fragment(
@@ -741,45 +411,50 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> Repo.all()
end
- @spec fetch_latest_activity_id_for_context(String.t(), keyword() | map()) ::
+ @spec fetch_latest_direct_activity_id_for_context(String.t(), keyword() | map()) ::
FlakeId.Ecto.CompatType.t() | nil
- def fetch_latest_activity_id_for_context(context, opts \\ %{}) do
+ def fetch_latest_direct_activity_id_for_context(context, opts \\ %{}) do
context
- |> fetch_activities_for_context_query(Map.merge(%{"skip_preload" => true}, opts))
+ |> fetch_activities_for_context_query(Map.merge(%{skip_preload: true}, opts))
+ |> restrict_visibility(%{visibility: "direct"})
|> limit(1)
|> select([a], a.id)
|> 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"])
+ @spec fetch_public_or_unlisted_activities(map(), Pagination.type()) :: [Activity.t()]
+ def fetch_public_or_unlisted_activities(opts \\ %{}, pagination \\ :keyset) do
+ opts = Map.delete(opts, :user)
[Constants.as_public()]
|> fetch_activities_query(opts)
- |> restrict_unlisted()
+ |> restrict_unlisted(opts)
|> Pagination.fetch_paginated(opts, pagination)
end
+ @spec fetch_public_activities(map(), Pagination.type()) :: [Activity.t()]
+ def fetch_public_activities(opts \\ %{}, pagination \\ :keyset) do
+ opts
+ |> Map.put(:restrict_unlisted, true)
+ |> fetch_public_or_unlisted_activities(pagination)
+ end
+
@valid_visibilities ~w[direct unlisted public private]
defp restrict_visibility(query, %{visibility: visibility})
when is_list(visibility) do
if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
- query =
- from(
- a in query,
- where:
- fragment(
- "activity_visibility(?, ?, ?) = ANY (?)",
- a.actor,
- a.recipients,
- a.data,
- ^visibility
- )
- )
-
- query
+ from(
+ a in query,
+ where:
+ fragment(
+ "activity_visibility(?, ?, ?) = ANY (?)",
+ a.actor,
+ a.recipients,
+ a.data,
+ ^visibility
+ )
+ )
else
Logger.error("Could not restrict visibility to #{visibility}")
end
@@ -801,7 +476,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_visibility(query, _visibility), do: query
- defp exclude_visibility(query, %{"exclude_visibilities" => visibility})
+ defp exclude_visibility(query, %{exclude_visibilities: visibility})
when is_list(visibility) do
if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
from(
@@ -821,7 +496,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
- defp exclude_visibility(query, %{"exclude_visibilities" => visibility})
+ defp exclude_visibility(query, %{exclude_visibilities: visibility})
when visibility in @valid_visibilities do
from(
a in query,
@@ -836,8 +511,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
)
end
- defp exclude_visibility(query, %{"exclude_visibilities" => visibility})
- when visibility not in @valid_visibilities do
+ defp exclude_visibility(query, %{exclude_visibilities: visibility})
+ when visibility not in [nil | @valid_visibilities] do
Logger.error("Could not exclude visibility to #{visibility}")
query
end
@@ -847,14 +522,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_thread_visibility(query, _, %{skip_thread_containment: true} = _),
do: query
- defp restrict_thread_visibility(
- query,
- %{"user" => %User{skip_thread_containment: true}},
- _
- ),
- do: query
+ defp restrict_thread_visibility(query, %{user: %User{skip_thread_containment: true}}, _),
+ do: query
- defp restrict_thread_visibility(query, %{"user" => %User{ap_id: ap_id}}, _) do
+ defp restrict_thread_visibility(query, %{user: %User{ap_id: ap_id}}, _) do
from(
a in query,
where: fragment("thread_visibility(?, (?)->>'id') = true", ^ap_id, a.data)
@@ -866,87 +537,99 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
def fetch_user_abstract_activities(user, reading_user, params \\ %{}) do
params =
params
- |> Map.put("user", reading_user)
- |> Map.put("actor_id", user.ap_id)
-
- recipients =
- user_activities_recipients(%{
- "godmode" => params["godmode"],
- "reading_user" => reading_user
- })
+ |> Map.put(:user, reading_user)
+ |> Map.put(:actor_id, user.ap_id)
- fetch_activities(recipients, params)
+ %{
+ godmode: params[:godmode],
+ reading_user: reading_user
+ }
+ |> user_activities_recipients()
+ |> fetch_activities(params)
|> Enum.reverse()
end
def fetch_user_activities(user, reading_user, params \\ %{}) do
params =
params
- |> Map.put("type", ["Create", "Announce"])
- |> Map.put("user", reading_user)
- |> Map.put("actor_id", user.ap_id)
- |> Map.put("pinned_activity_ids", user.pinned_activities)
+ |> Map.put(:type, ["Create", "Announce"])
+ |> Map.put(:user, reading_user)
+ |> Map.put(:actor_id, user.ap_id)
+ |> Map.put(:pinned_activity_ids, user.pinned_activities)
params =
if User.blocks?(reading_user, user) do
params
else
params
- |> Map.put("blocking_user", reading_user)
- |> Map.put("muting_user", reading_user)
+ |> Map.put(:blocking_user, reading_user)
+ |> Map.put(:muting_user, reading_user)
end
- recipients =
- user_activities_recipients(%{
- "godmode" => params["godmode"],
- "reading_user" => reading_user
- })
-
- fetch_activities(recipients, params)
+ %{
+ godmode: params[:godmode],
+ reading_user: reading_user
+ }
+ |> user_activities_recipients()
+ |> fetch_activities(params)
|> Enum.reverse()
end
def fetch_statuses(reading_user, params) do
- params =
- params
- |> Map.put("type", ["Create", "Announce"])
-
- recipients =
- user_activities_recipients(%{
- "godmode" => params["godmode"],
- "reading_user" => reading_user
- })
+ params = Map.put(params, :type, ["Create", "Announce"])
- fetch_activities(recipients, params, :offset)
+ %{
+ godmode: params[:godmode],
+ reading_user: reading_user
+ }
+ |> user_activities_recipients()
+ |> fetch_activities(params, :offset)
|> Enum.reverse()
end
- defp user_activities_recipients(%{"godmode" => true}) do
- []
- end
+ defp user_activities_recipients(%{godmode: true}), do: []
- defp user_activities_recipients(%{"reading_user" => reading_user}) do
+ defp user_activities_recipients(%{reading_user: reading_user}) do
if reading_user do
- [Constants.as_public()] ++ [reading_user.ap_id | User.following(reading_user)]
+ [Constants.as_public(), reading_user.ap_id | User.following(reading_user)]
else
[Constants.as_public()]
end
end
- defp restrict_since(query, %{"since_id" => ""}), do: query
+ defp restrict_announce_object_actor(_query, %{announce_filtering_user: _, skip_preload: true}) do
+ raise "Can't use the child object without preloading!"
+ end
- defp restrict_since(query, %{"since_id" => since_id}) do
+ defp restrict_announce_object_actor(query, %{announce_filtering_user: %{ap_id: actor}}) do
+ from(
+ [activity, object] in query,
+ where:
+ fragment(
+ "?->>'type' != ? or ?->>'actor' != ?",
+ activity.data,
+ "Announce",
+ object.data,
+ ^actor
+ )
+ )
+ end
+
+ defp restrict_announce_object_actor(query, _), do: query
+
+ defp restrict_since(query, %{since_id: ""}), do: query
+
+ defp restrict_since(query, %{since_id: since_id}) do
from(activity in query, where: activity.id > ^since_id)
end
defp restrict_since(query, _), do: query
- defp restrict_tag_reject(_query, %{"tag_reject" => _tag_reject, "skip_preload" => true}) do
+ defp restrict_tag_reject(_query, %{tag_reject: _tag_reject, skip_preload: true}) do
raise "Can't use the child object without preloading!"
end
- defp restrict_tag_reject(query, %{"tag_reject" => tag_reject})
- when is_list(tag_reject) and tag_reject != [] do
+ defp restrict_tag_reject(query, %{tag_reject: [_ | _] = tag_reject}) do
from(
[_activity, object] in query,
where: fragment("not (?)->'tag' \\?| (?)", object.data, ^tag_reject)
@@ -955,12 +638,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_tag_reject(query, _), do: query
- defp restrict_tag_all(_query, %{"tag_all" => _tag_all, "skip_preload" => true}) do
+ defp restrict_tag_all(_query, %{tag_all: _tag_all, skip_preload: true}) do
raise "Can't use the child object without preloading!"
end
- defp restrict_tag_all(query, %{"tag_all" => tag_all})
- when is_list(tag_all) and tag_all != [] do
+ defp restrict_tag_all(query, %{tag_all: [_ | _] = tag_all}) do
from(
[_activity, object] in query,
where: fragment("(?)->'tag' \\?& (?)", object.data, ^tag_all)
@@ -969,18 +651,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_tag_all(query, _), do: query
- defp restrict_tag(_query, %{"tag" => _tag, "skip_preload" => true}) do
+ defp restrict_tag(_query, %{tag: _tag, skip_preload: true}) do
raise "Can't use the child object without preloading!"
end
- defp restrict_tag(query, %{"tag" => tag}) when is_list(tag) do
+ defp restrict_tag(query, %{tag: tag}) when is_list(tag) do
from(
[_activity, object] in query,
where: fragment("(?)->'tag' \\?| (?)", object.data, ^tag)
)
end
- defp restrict_tag(query, %{"tag" => tag}) when is_binary(tag) do
+ defp restrict_tag(query, %{tag: tag}) when is_binary(tag) do
from(
[_activity, object] in query,
where: fragment("(?)->'tag' \\? (?)", object.data, ^tag)
@@ -1003,35 +685,35 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
)
end
- defp restrict_local(query, %{"local_only" => true}) do
+ defp restrict_local(query, %{local_only: true}) do
from(activity in query, where: activity.local == true)
end
defp restrict_local(query, _), do: query
- defp restrict_actor(query, %{"actor_id" => actor_id}) do
+ defp restrict_actor(query, %{actor_id: actor_id}) do
from(activity in query, where: activity.actor == ^actor_id)
end
defp restrict_actor(query, _), do: query
- defp restrict_type(query, %{"type" => type}) when is_binary(type) do
+ defp restrict_type(query, %{type: type}) when is_binary(type) do
from(activity in query, where: fragment("?->>'type' = ?", activity.data, ^type))
end
- defp restrict_type(query, %{"type" => type}) do
+ defp restrict_type(query, %{type: type}) do
from(activity in query, where: fragment("?->>'type' = ANY(?)", activity.data, ^type))
end
defp restrict_type(query, _), do: query
- defp restrict_state(query, %{"state" => state}) do
+ defp restrict_state(query, %{state: state}) do
from(activity in query, where: fragment("?->>'state' = ?", activity.data, ^state))
end
defp restrict_state(query, _), do: query
- defp restrict_favorited_by(query, %{"favorited_by" => ap_id}) do
+ defp restrict_favorited_by(query, %{favorited_by: ap_id}) do
from(
[_activity, object] in query,
where: fragment("(?)->'likes' \\? (?)", object.data, ^ap_id)
@@ -1040,38 +722,74 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_favorited_by(query, _), do: query
- defp restrict_media(_query, %{"only_media" => _val, "skip_preload" => true}) do
+ defp restrict_media(_query, %{only_media: _val, skip_preload: true}) 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: true}) do
from(
- [_activity, object] in query,
+ [activity, object] in query,
+ where: fragment("(?)->>'type' = ?", activity.data, "Create"),
where: fragment("not (?)->'attachment' = (?)", object.data, ^[])
)
end
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: true}) 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: true}) do
from(activity in query, where: fragment("?->>'type' != 'Announce'", activity.data))
end
defp restrict_reblogs(query, _), do: query
- defp restrict_muted(query, %{"with_muted" => val}) when val in [true, "true", "1"], do: query
+ defp restrict_muted(query, %{with_muted: true}), do: query
- defp restrict_muted(query, %{"muting_user" => %User{} = user} = opts) do
- mutes = opts["muted_users_ap_ids"] || User.muted_users_ap_ids(user)
+ defp restrict_muted(query, %{muting_user: %User{} = user} = opts) do
+ mutes = opts[:muted_users_ap_ids] || User.muted_users_ap_ids(user)
query =
from([activity] in query,
@@ -1079,7 +797,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
where: fragment("not (?->'to' \\?| ?)", activity.data, ^mutes)
)
- unless opts["skip_preload"] do
+ unless opts[:skip_preload] do
from([thread_mute: tm] in query, where: is_nil(tm.user_id))
else
query
@@ -1088,8 +806,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_muted(query, _), do: query
- defp restrict_blocked(query, %{"blocking_user" => %User{} = user} = opts) do
- blocked_ap_ids = opts["blocked_users_ap_ids"] || User.blocked_users_ap_ids(user)
+ defp restrict_blocked(query, %{blocking_user: %User{} = user} = opts) do
+ blocked_ap_ids = opts[:blocked_users_ap_ids] || User.blocked_users_ap_ids(user)
domain_blocks = user.domain_blocks || []
following_ap_ids = User.get_friends_ap_ids(user)
@@ -1103,6 +821,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
where: fragment("not (? && ?)", activity.recipients, ^blocked_ap_ids),
where:
fragment(
+ "recipients_contain_blocked_domains(?, ?) = false",
+ activity.recipients,
+ ^domain_blocks
+ ),
+ where:
+ fragment(
"not (?->>'type' = 'Announce' and ?->'to' \\?| ?)",
activity.data,
activity.data,
@@ -1129,7 +853,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_blocked(query, _), do: query
- defp restrict_unlisted(query) do
+ defp restrict_unlisted(query, %{restrict_unlisted: true}) do
from(
activity in query,
where:
@@ -1141,14 +865,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
)
end
- defp restrict_pinned(query, %{"pinned" => "true", "pinned_activity_ids" => ids}) do
+ defp restrict_unlisted(query, _), do: query
+
+ defp restrict_pinned(query, %{pinned: true, pinned_activity_ids: ids}) do
from(activity in query, where: activity.id in ^ids)
end
defp restrict_pinned(query, _), do: query
- defp restrict_muted_reblogs(query, %{"muting_user" => %User{} = user} = opts) do
- muted_reblogs = opts["reblog_muted_users_ap_ids"] || User.reblog_muted_users_ap_ids(user)
+ defp restrict_muted_reblogs(query, %{muting_user: %User{} = user} = opts) do
+ muted_reblogs = opts[:reblog_muted_users_ap_ids] || User.reblog_muted_users_ap_ids(user)
from(
activity in query,
@@ -1164,7 +890,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_muted_reblogs(query, _), do: query
- defp restrict_instance(query, %{"instance" => instance}) do
+ defp restrict_instance(query, %{instance: instance}) do
users =
from(
u in User,
@@ -1178,7 +904,27 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_instance(query, _), do: query
- defp exclude_poll_votes(query, %{"include_poll_votes" => true}), do: query
+ defp restrict_filtered(query, %{user: %User{} = user}) do
+ case Filter.compose_regex(user) do
+ nil ->
+ query
+
+ regex ->
+ from([activity, object] in query,
+ where:
+ fragment("not(?->>'content' ~* ?)", object.data, ^regex) or
+ activity.actor == ^user.ap_id
+ )
+ end
+ end
+
+ defp restrict_filtered(query, %{blocking_user: %User{} = user}) do
+ restrict_filtered(query, %{user: user})
+ end
+
+ defp restrict_filtered(query, _), do: query
+
+ defp exclude_poll_votes(query, %{include_poll_votes: true}), do: query
defp exclude_poll_votes(query, _) do
if has_named_binding?(query, :object) do
@@ -1190,38 +936,61 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
- defp exclude_id(query, %{"exclude_id" => id}) when is_binary(id) do
+ defp exclude_chat_messages(query, %{include_chat_messages: true}), do: query
+
+ defp exclude_chat_messages(query, _) do
+ if has_named_binding?(query, :object) do
+ from([activity, object: o] in query,
+ where: fragment("not(?->>'type' = ?)", o.data, "ChatMessage")
+ )
+ else
+ query
+ end
+ end
+
+ defp exclude_invisible_actors(query, %{invisible_actors: true}), do: query
+
+ defp exclude_invisible_actors(query, _opts) do
+ invisible_ap_ids =
+ User.Query.build(%{invisible: true, select: [:ap_id]})
+ |> Repo.all()
+ |> Enum.map(fn %{ap_id: ap_id} -> ap_id end)
+
+ from([activity] in query, where: activity.actor not in ^invisible_ap_ids)
+ end
+
+ defp exclude_id(query, %{exclude_id: id}) when is_binary(id) do
from(activity in query, where: activity.id != ^id)
end
defp exclude_id(query, _), do: query
- defp maybe_preload_objects(query, %{"skip_preload" => true}), do: query
+ defp maybe_preload_objects(query, %{skip_preload: true}), do: query
defp maybe_preload_objects(query, _) do
query
|> Activity.with_preloaded_object()
end
- defp maybe_preload_bookmarks(query, %{"skip_preload" => true}), do: query
+ defp maybe_preload_bookmarks(query, %{skip_preload: true}), do: query
defp maybe_preload_bookmarks(query, opts) do
query
- |> Activity.with_preloaded_bookmark(opts["user"])
+ |> Activity.with_preloaded_bookmark(opts[:user])
end
- defp maybe_preload_report_notes(query, %{"preload_report_notes" => true}) do
+ defp maybe_preload_report_notes(query, %{preload_report_notes: true}) do
query
|> Activity.with_preloaded_report_notes()
end
defp maybe_preload_report_notes(query, _), do: query
- defp maybe_set_thread_muted_field(query, %{"skip_preload" => true}), do: query
+ defp maybe_set_thread_muted_field(query, %{skip_preload: true}), do: query
defp maybe_set_thread_muted_field(query, opts) do
query
- |> Activity.with_set_thread_muted_field(opts["muting_user"] || opts["user"])
+ |> Activity.with_set_thread_muted_field(opts[:muting_user] || opts[:user])
end
defp maybe_order(query, %{order: :desc}) do
@@ -1237,24 +1006,23 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp maybe_order(query, _), do: query
defp fetch_activities_query_ap_ids_ops(opts) do
- source_user = opts["muting_user"]
+ source_user = opts[:muting_user]
ap_id_relationships = if source_user, do: [:mute, :reblog_mute], else: []
ap_id_relationships =
- ap_id_relationships ++
- if opts["blocking_user"] && opts["blocking_user"] == source_user do
- [:block]
- else
- []
- end
+ if opts[:blocking_user] && opts[:blocking_user] == source_user do
+ [:block | ap_id_relationships]
+ else
+ ap_id_relationships
+ end
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)
+ 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)
restrict_muted_reblogs_opts =
- Map.merge(%{"reblog_muted_users_ap_ids" => preloaded_ap_ids[:reblog_mute]}, opts)
+ Map.merge(%{reblog_muted_users_ap_ids: preloaded_ap_ids[:reblog_mute]}, opts)
{restrict_blocked_opts, restrict_muted_opts, restrict_muted_reblogs_opts}
end
@@ -1273,7 +1041,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> maybe_preload_report_notes(opts)
|> maybe_set_thread_muted_field(opts)
|> maybe_order(opts)
- |> restrict_recipients(recipients, opts["user"])
+ |> restrict_recipients(recipients, opts[:user])
+ |> restrict_replies(opts)
|> restrict_tag(opts)
|> restrict_tag_reject(opts)
|> restrict_tag_all(opts)
@@ -1285,26 +1054,30 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> restrict_favorited_by(opts)
|> restrict_blocked(restrict_blocked_opts)
|> restrict_muted(restrict_muted_opts)
+ |> restrict_filtered(opts)
|> 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)
|> restrict_instance(opts)
+ |> restrict_announce_object_actor(opts)
+ |> restrict_filtered(opts)
|> Activity.restrict_deactivated_users()
|> exclude_poll_votes(opts)
+ |> exclude_chat_messages(opts)
+ |> exclude_invisible_actors(opts)
|> exclude_visibility(opts)
end
def fetch_activities(recipients, opts \\ %{}, pagination \\ :keyset) do
- list_memberships = Pleroma.List.memberships(opts["user"])
+ list_memberships = Pleroma.List.memberships(opts[:user])
fetch_activities_query(recipients ++ list_memberships, opts)
|> Pagination.fetch_paginated(opts, pagination)
|> Enum.reverse()
- |> maybe_update_cc(list_memberships, opts["user"])
+ |> maybe_update_cc(list_memberships, opts[:user])
end
@doc """
@@ -1317,19 +1090,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> Activity.Queries.by_type("Like")
|> Activity.with_joined_object()
|> Object.with_joined_activity()
- |> select([_like, object, activity], %{activity | object: object})
+ |> select([like, object, activity], %{activity | object: object, pagination_id: like.id})
|> order_by([like, _, _], desc_nulls_last: like.id)
|> Pagination.fetch_paginated(
- Map.merge(params, %{"skip_order" => true}),
- pagination,
- :object_activity
+ Map.merge(params, %{skip_order: true}),
+ pagination
)
end
- defp maybe_update_cc(activities, list_memberships, %User{ap_id: user_ap_id})
- when is_list(list_memberships) and length(list_memberships) > 0 do
+ defp maybe_update_cc(activities, [_ | _] = list_memberships, %User{ap_id: user_ap_id}) do
Enum.map(activities, fn
- %{data: %{"bcc" => bcc}} = activity when is_list(bcc) and length(bcc) > 0 ->
+ %{data: %{"bcc" => [_ | _] = bcc}} = activity ->
if Enum.any?(bcc, &(&1 in list_memberships)) do
update_in(activity.data["cc"], &[user_ap_id | &1])
else
@@ -1343,7 +1114,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp maybe_update_cc(activities, _, _), do: activities
- def fetch_activities_bounded_query(query, recipients, recipients_with_public) do
+ defp fetch_activities_bounded_query(query, recipients, recipients_with_public) do
from(activity in query,
where:
fragment("? && ?", activity.recipients, ^recipients) or
@@ -1367,12 +1138,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
@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 =
- if opts[:actor] do
- Map.put(data, "actor", opts[:actor])
- else
- data
- end
+ obj_data = Maps.put_if_present(data, "actor", opts[:actor])
Repo.insert(%Object{data: obj_data})
end
@@ -1411,19 +1177,46 @@ 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)
+ |> Map.new(fn %{"icon" => %{"url" => url}, "name" => name} ->
+ {String.trim(name, ":"), url}
+ end)
+
locked = data["manuallyApprovesFollowers"] || false
+ capabilities = data["capabilities"] || %{}
+ accepts_chat_messages = capabilities["acceptsChatMessages"]
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,
@@ -1433,22 +1226,23 @@ 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,
+ accepts_chat_messages: accepts_chat_messages
}
# nickname can be nil because of virtual actors
- user_data =
- if data["preferredUsername"] do
- Map.put(
- user_data,
- :nickname,
- "#{data["preferredUsername"]}@#{URI.parse(data["id"]).host}"
- )
- else
- Map.put(user_data, :nickname, nil)
- end
-
- {:ok, user_data}
+ if data["preferredUsername"] do
+ Map.put(
+ user_data,
+ :nickname,
+ "#{data["preferredUsername"]}@#{URI.parse(data["id"]).host}"
+ )
+ else
+ Map.put(user_data, :nickname, nil)
+ end
end
def fetch_follow_information_for_user(user) do
@@ -1474,21 +1268,34 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp normalize_counter(counter) when is_integer(counter), do: counter
defp normalize_counter(_), do: 0
- defp maybe_update_follow_information(data) do
+ def maybe_update_follow_information(user_data) do
with {:enabled, true} <- {:enabled, Config.get([:instance, :external_user_synchronization])},
- {:ok, info} <- fetch_follow_information_for_user(data) do
- info = Map.merge(data[:info] || %{}, info)
- Map.put(data, :info, info)
+ {_, true} <- {:user_type_check, user_data[:type] in ["Person", "Service"]},
+ {_, true} <-
+ {:collections_available,
+ !!(user_data[:following_address] && user_data[:follower_address])},
+ {:ok, info} <-
+ fetch_follow_information_for_user(user_data) do
+ info = Map.merge(user_data[:info] || %{}, info)
+
+ user_data
+ |> Map.put(:info, info)
else
+ {:user_type_check, false} ->
+ user_data
+
+ {:collections_available, false} ->
+ user_data
+
{:enabled, false} ->
- data
+ user_data
e ->
Logger.error(
- "Follower/Following counter update for #{data.ap_id} failed.\n" <> inspect(e)
+ "Follower/Following counter update for #{user_data.ap_id} failed.\n" <> inspect(e)
)
- data
+ user_data
end
end
@@ -1510,9 +1317,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp collection_private(_data), do: {:ok, true}
def user_data_from_user_object(data) do
- with {:ok, data} <- MRF.filter(data),
- {:ok, data} <- object_to_user_data(data) do
- {:ok, data}
+ with {:ok, data} <- MRF.filter(data) do
+ {:ok, object_to_user_data(data)}
else
e -> {:error, e}
end
@@ -1520,28 +1326,66 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
def fetch_and_prepare_user_from_ap_id(ap_id) do
with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id),
- {:ok, data} <- user_data_from_user_object(data),
- data <- maybe_update_follow_information(data) do
- {:ok, data}
+ {:ok, data} <- user_data_from_user_object(data) do
+ {:ok, maybe_update_follow_information(data)}
else
- {:error, "Object has been deleted"} = e ->
+ {:error, "Object has been deleted" = e} ->
Logger.debug("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
{:error, e}
- e ->
+ {:error, {:reject, reason} = e} ->
+ Logger.info("Rejected user #{ap_id}: #{inspect(reason)}")
+ {:error, e}
+
+ {:error, e} ->
Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
{:error, e}
end
end
+ def maybe_handle_clashing_nickname(data) do
+ with nickname when is_binary(nickname) <- data[:nickname],
+ %User{} = old_user <- User.get_by_nickname(nickname),
+ {_, false} <- {:ap_id_comparison, data[:ap_id] == old_user.ap_id} do
+ Logger.info(
+ "Found an old user for #{nickname}, the old ap id is #{old_user.ap_id}, new one is #{
+ data[:ap_id]
+ }, renaming."
+ )
+
+ old_user
+ |> User.remote_user_changeset(%{nickname: "#{old_user.id}.#{old_user.nickname}"})
+ |> User.update_and_set_cache()
+ else
+ {:ap_id_comparison, true} ->
+ Logger.info(
+ "Found an old user for #{data[:nickname]}, but the ap id #{data[:ap_id]} is the same as the new user. Race condition? Not changing anything."
+ )
+
+ _ ->
+ nil
+ end
+ 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)
- else
- e -> {:error, e}
+ if user do
+ user
+ |> User.remote_user_changeset(data)
+ |> User.update_and_set_cache()
+ else
+ maybe_handle_clashing_nickname(data)
+
+ data
+ |> User.remote_user_changeset()
+ |> Repo.insert()
+ |> User.set_cache()
+ end
end
end
end
@@ -1555,7 +1399,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
# filter out broken threads
- def contain_broken_threads(%Activity{} = activity, %User{} = user) do
+ defp contain_broken_threads(%Activity{} = activity, %User{} = user) do
entire_thread_visible_for_user?(activity, user)
end
@@ -1566,7 +1410,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
def fetch_direct_messages_query do
Activity
- |> restrict_type(%{"type" => "Create"})
+ |> restrict_type(%{type: "Create"})
|> restrict_visibility(%{visibility: "direct"})
|> order_by([activity], asc: activity.id)
end
diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
index a64199cd6..220c4fe52 100644
--- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
@@ -9,33 +9,52 @@ 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.ControllerHelper
+ alias Pleroma.Web.Endpoint
+ 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
@@ -58,8 +77,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
end
end
- def object(conn, %{"uuid" => uuid}) do
- with ap_id <- o_status_url(conn, :object, uuid),
+ def object(conn, _) do
+ with ap_id <- Endpoint.url() <> conn.request_path,
%Object{} = object <- Object.get_cached_by_ap_id(ap_id),
{_, true} <- {:public?, Visibility.is_public?(object)} do
conn
@@ -84,8 +103,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
conn
end
- def activity(conn, %{"uuid" => uuid}) do
- with ap_id <- o_status_url(conn, :activity, uuid),
+ def activity(conn, _params) do
+ with ap_id <- Endpoint.url() <> conn.request_path,
%Activity{} = activity <- Activity.normalize(ap_id),
{_, true} <- {:public?, Visibility.is_public?(activity)} do
conn
@@ -128,11 +147,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
@@ -165,11 +186,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
@@ -215,6 +238,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
params
|> Map.drop(["nickname", "page"])
|> Map.put("include_poll_votes", true)
+ |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
activities = ActivityPub.fetch_user_activities(user, for_user, params)
@@ -254,8 +278,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"
)
@@ -265,10 +297,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!"
)
@@ -276,7 +309,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
@@ -310,10 +345,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
@@ -322,6 +355,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|> Map.drop(["nickname", "page"])
|> Map.put("blocking_user", user)
|> Map.put("user", user)
+ |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
activities =
[user.ap_id | User.following(user)]
@@ -338,7 +372,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
@@ -349,15 +383,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 =
@@ -390,19 +416,23 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
})
end
- defp 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
- defp 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")}
@@ -438,7 +468,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,
@@ -450,13 +480,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"))
@@ -484,7 +514,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
{new_user, for_user}
end
- # TODO: Add support for "object" field
@doc """
Endpoint based on <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>
@@ -495,8 +524,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
Response:
- HTTP Code: 201 Created
- HTTP Body: ActivityPub object to be inserted into another's `attachment` field
+
+ Note: Will not point to a URL with a `Location` header because no standalone Activity has been created.
"""
- 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..9a7b7d9de
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/builder.ex
@@ -0,0 +1,269 @@
+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.Emoji
+ alias Pleroma.Object
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.Relay
+ alias Pleroma.Web.ActivityPub.Utils
+ alias Pleroma.Web.ActivityPub.Visibility
+
+ require Pleroma.Constants
+
+ def accept_or_reject(actor, activity, type) do
+ data = %{
+ "id" => Utils.generate_activity_id(),
+ "actor" => actor.ap_id,
+ "type" => type,
+ "object" => activity.data["id"],
+ "to" => [activity.actor]
+ }
+
+ {:ok, data, []}
+ end
+
+ @spec reject(User.t(), Activity.t()) :: {:ok, map(), keyword()}
+ def reject(actor, rejected_activity) do
+ accept_or_reject(actor, rejected_activity, "Reject")
+ end
+
+ @spec accept(User.t(), Activity.t()) :: {:ok, map(), keyword()}
+ def accept(actor, accepted_activity) do
+ accept_or_reject(actor, accepted_activity, "Accept")
+ end
+
+ @spec follow(User.t(), User.t()) :: {:ok, map(), keyword()}
+ def follow(follower, followed) do
+ data = %{
+ "id" => Utils.generate_activity_id(),
+ "actor" => follower.ap_id,
+ "type" => "Follow",
+ "object" => followed.ap_id,
+ "to" => [followed.ap_id]
+ }
+
+ {:ok, data, []}
+ end
+
+ @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
+
+ def create(actor, object, recipients) do
+ context =
+ if is_map(object) do
+ object["context"]
+ else
+ nil
+ end
+
+ {:ok,
+ %{
+ "id" => Utils.generate_activity_id(),
+ "actor" => actor.ap_id,
+ "to" => recipients,
+ "object" => object,
+ "type" => "Create",
+ "published" => DateTime.utc_now() |> DateTime.to_iso8601()
+ }
+ |> Pleroma.Maps.put_if_present("context", context), []}
+ end
+
+ def chat_message(actor, recipient, content, opts \\ []) do
+ basic = %{
+ "id" => Utils.generate_object_id(),
+ "actor" => actor.ap_id,
+ "type" => "ChatMessage",
+ "to" => [recipient],
+ "content" => content,
+ "published" => DateTime.utc_now() |> DateTime.to_iso8601(),
+ "emoji" => Emoji.Formatter.get_emoji_map(content)
+ }
+
+ case opts[:attachment] do
+ %Object{data: attachment_data} ->
+ {
+ :ok,
+ Map.put(basic, "attachment", attachment_data),
+ []
+ }
+
+ _ ->
+ {:ok, basic, []}
+ end
+ end
+
+ def answer(user, object, name) do
+ {:ok,
+ %{
+ "type" => "Answer",
+ "actor" => user.ap_id,
+ "attributedTo" => user.ap_id,
+ "cc" => [object.data["actor"]],
+ "to" => [],
+ "name" => name,
+ "inReplyTo" => object.data["id"],
+ "context" => object.data["context"],
+ "published" => DateTime.utc_now() |> DateTime.to_iso8601(),
+ "id" => Utils.generate_object_id()
+ }, []}
+ 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
+
+ # Retricted to user updates for now, always public
+ @spec update(User.t(), Object.t()) :: {:ok, map(), keyword()}
+ def update(actor, object) do
+ to = [Pleroma.Constants.as_public(), actor.follower_address]
+
+ {:ok,
+ %{
+ "id" => Utils.generate_activity_id(),
+ "type" => "Update",
+ "actor" => actor.ap_id,
+ "object" => object,
+ "to" => to
+ }, []}
+ end
+
+ @spec block(User.t(), User.t()) :: {:ok, map(), keyword()}
+ def block(blocker, blocked) do
+ {:ok,
+ %{
+ "id" => Utils.generate_activity_id(),
+ "type" => "Block",
+ "actor" => blocker.ap_id,
+ "object" => blocked.ap_id,
+ "to" => [blocked.ap_id]
+ }, []}
+ end
+
+ @spec announce(User.t(), Object.t(), keyword()) :: {:ok, map(), keyword()}
+ def announce(actor, object, options \\ []) do
+ public? = Keyword.get(options, :public, false)
+
+ to =
+ cond do
+ actor.ap_id == Relay.ap_id() ->
+ [actor.follower_address]
+
+ public? ->
+ [actor.follower_address, object.data["actor"], Pleroma.Constants.as_public()]
+
+ true ->
+ [actor.follower_address, object.data["actor"]]
+ end
+
+ {:ok,
+ %{
+ "id" => Utils.generate_activity_id(),
+ "actor" => actor.ap_id,
+ "object" => object.data["id"],
+ "to" => to,
+ "context" => object.data["context"],
+ "type" => "Announce",
+ "published" => Utils.make_date()
+ }, []}
+ end
+
+ @spec object_action(User.t(), Object.t()) :: {:ok, map(), keyword()}
+ 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/mrf.ex b/lib/pleroma/web/activity_pub/mrf.ex
index a0b3af432..206d6af52 100644
--- a/lib/pleroma/web/activity_pub/mrf.ex
+++ b/lib/pleroma/web/activity_pub/mrf.ex
@@ -8,18 +8,15 @@ defmodule Pleroma.Web.ActivityPub.MRF do
def filter(policies, %{} = object) do
policies
|> Enum.reduce({:ok, object}, fn
- policy, {:ok, object} ->
- policy.filter(object)
-
- _, error ->
- error
+ policy, {:ok, object} -> policy.filter(object)
+ _, error -> error
end)
end
def filter(%{} = object), do: get_policies() |> filter(object)
def get_policies do
- Pleroma.Config.get([:instance, :rewrite_policy], []) |> get_policies()
+ Pleroma.Config.get([:mrf, :policies], []) |> get_policies()
end
defp get_policies(policy) when is_atom(policy), do: [policy]
@@ -54,7 +51,7 @@ defmodule Pleroma.Web.ActivityPub.MRF do
get_policies()
|> Enum.map(fn policy -> to_string(policy) |> String.split(".") |> List.last() end)
- exclusions = Pleroma.Config.get([:instance, :mrf_transparency_exclusions])
+ exclusions = Pleroma.Config.get([:mrf, :transparency_exclusions])
base =
%{
diff --git a/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex b/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex
new file mode 100644
index 000000000..7b4c78e0f
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex
@@ -0,0 +1,43 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy do
+ @moduledoc "Adds expiration to all local Create activities"
+ @behaviour Pleroma.Web.ActivityPub.MRF
+
+ @impl true
+ def filter(activity) do
+ activity =
+ if note?(activity) and local?(activity) do
+ maybe_add_expiration(activity)
+ else
+ activity
+ end
+
+ {:ok, activity}
+ end
+
+ @impl true
+ def describe, do: {:ok, %{}}
+
+ defp local?(%{"actor" => actor}) do
+ String.starts_with?(actor, Pleroma.Web.Endpoint.url())
+ end
+
+ defp note?(activity) do
+ match?(%{"type" => "Create", "object" => %{"type" => "Note"}}, activity)
+ end
+
+ defp maybe_add_expiration(activity) do
+ days = Pleroma.Config.get([:mrf_activity_expiration, :days], 365)
+ expires_at = NaiveDateTime.utc_now() |> Timex.shift(days: days)
+
+ with %{"expires_at" => existing_expires_at} <- activity,
+ :lt <- NaiveDateTime.compare(existing_expires_at, expires_at) do
+ activity
+ else
+ _ -> Map.put(activity, "expires_at", expires_at)
+ end
+ end
+end
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..b96388489 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
@@ -60,7 +60,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy do
if score < 0.8 do
{:ok, message}
else
- {:reject, nil}
+ {:reject, "[AntiFollowbotPolicy] Scored #{actor_id} as #{score}"}
end
end
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 9e7800997..b22464111 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
@@ -27,23 +27,25 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy do
@impl true
def filter(%{"type" => "Create", "actor" => actor, "object" => object} = message) do
- with {:ok, %User{} = u} <- User.get_or_fetch_by_ap_id(actor),
+ with {:ok, %User{local: false} = u} <- User.get_or_fetch_by_ap_id(actor),
{:contains_links, true} <- {:contains_links, contains_links?(object)},
{:old_user, true} <- {:old_user, old_user?(u)} do
{:ok, message}
else
+ {:ok, %User{local: true}} ->
+ {:ok, message}
+
{:contains_links, false} ->
{:ok, message}
{:old_user, false} ->
- {:reject, nil}
+ {:reject, "[AntiLinkSpamPolicy] User has no posts nor followers"}
{:error, _} ->
- {:reject, nil}
+ {:reject, "[AntiLinkSpamPolicy] Failed to get or fetch user by ap_id"}
e ->
- Logger.warn("[MRF anti-link-spam] WTF: unhandled error #{inspect(e)}")
- {:reject, nil}
+ {:reject, "[AntiLinkSpamPolicy] Unhandled error #{inspect(e)}"}
end
end
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 2627a0007..3bf70b894 100644
--- a/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex
+++ b/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex
@@ -27,7 +27,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do
def filter_by_summary(_in_reply_to, child), do: child
- def filter(%{"type" => "Create", "object" => child_object} = object) do
+ def filter(%{"type" => "Create", "object" => child_object} = object)
+ when is_map(child_object) do
child =
child_object["inReplyTo"]
|> Object.normalize(child_object["inReplyTo"])
diff --git a/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex b/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex
index 1764bc789..9ba07b4e3 100644
--- a/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex
@@ -13,8 +13,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do
defp delist_message(message, threshold) when threshold > 0 do
follower_collection = User.get_cached_by_ap_id(message["actor"]).follower_address
+ to = message["to"] || []
+ cc = message["cc"] || []
- follower_collection? = Enum.member?(message["to"] ++ message["cc"], follower_collection)
+ follower_collection? = Enum.member?(to ++ cc, follower_collection)
message =
case get_recipient_count(message) do
@@ -41,7 +43,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do
defp reject_message(message, threshold) when threshold > 0 do
with {_, recipients} <- get_recipient_count(message) do
if recipients > threshold do
- {:reject, nil}
+ {:reject, "[HellthreadPolicy] #{recipients} recipients is over the limit of #{threshold}"}
else
{:ok, message}
end
@@ -71,7 +73,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do
end
@impl true
- def filter(%{"type" => "Create"} = message) do
+ def filter(%{"type" => "Create", "object" => %{"type" => object_type}} = message)
+ when object_type in ~w{Note Article} do
reject_threshold =
Pleroma.Config.get(
[:mrf_hellthread, :reject_threshold],
@@ -84,7 +87,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do
{:ok, message} <- delist_message(message, delist_threshold) do
{:ok, message}
else
- _e -> {:reject, nil}
+ e -> e
end
end
diff --git a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex
index 88b0d2b39..15e09dcf0 100644
--- a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex
@@ -24,7 +24,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
if Enum.any?(Pleroma.Config.get([:mrf_keyword, :reject]), fn pattern ->
string_matches?(content, pattern) or string_matches?(summary, pattern)
end) do
- {:reject, nil}
+ {:reject, "[KeywordPolicy] Matches with rejected keyword"}
else
{:ok, message}
end
@@ -89,8 +89,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
{:ok, message} <- check_replace(message) do
{:ok, message}
else
- _e ->
- {:reject, nil}
+ {:reject, nil} -> {:reject, "[KeywordPolicy] "}
+ {:reject, _} = e -> e
+ _e -> {:reject, "[KeywordPolicy] "}
end
end
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 d9a0acfd3..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
@@ -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 06f003921..7910ca131 100644
--- a/lib/pleroma/web/activity_pub/mrf/mention_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/mention_policy.ex
@@ -12,8 +12,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.MentionPolicy do
reject_actors = Pleroma.Config.get([:mrf_mention, :actors], [])
recipients = (message["to"] || []) ++ (message["cc"] || [])
- if Enum.any?(recipients, fn recipient -> Enum.member?(reject_actors, recipient) end) do
- {:reject, nil}
+ if rejected_mention =
+ Enum.find(recipients, fn recipient -> Enum.member?(reject_actors, recipient) end) do
+ {:reject, "[MentionPolicy] Rejected for mention of #{rejected_mention}"}
else
{:ok, message}
end
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/object_age_policy.ex b/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex
index b0ccb63c8..d45d2d7e3 100644
--- a/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex
@@ -28,7 +28,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
defp check_reject(message, actions) do
if :reject in actions do
- {:reject, nil}
+ {:reject, "[ObjectAgePolicy]"}
else
{:ok, message}
end
@@ -37,8 +37,13 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
defp check_delist(message, actions) do
if :delist in actions do
with %User{} = user <- User.get_cached_by_ap_id(message["actor"]) do
- to = List.delete(message["to"], Pleroma.Constants.as_public()) ++ [user.follower_address]
- cc = List.delete(message["cc"], user.follower_address) ++ [Pleroma.Constants.as_public()]
+ to =
+ List.delete(message["to"] || [], Pleroma.Constants.as_public()) ++
+ [user.follower_address]
+
+ cc =
+ List.delete(message["cc"] || [], user.follower_address) ++
+ [Pleroma.Constants.as_public()]
message =
message
@@ -47,9 +52,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
{:ok, message}
else
- # Unhandleable error: somebody is messing around, just drop the message.
_e ->
- {:reject, nil}
+ {:reject, "[ObjectAgePolicy] Unhandled error"}
end
else
{:ok, message}
@@ -59,8 +63,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
defp check_strip_followers(message, actions) do
if :strip_followers in actions do
with %User{} = user <- User.get_cached_by_ap_id(message["actor"]) do
- to = List.delete(message["to"], user.follower_address)
- cc = List.delete(message["cc"], user.follower_address)
+ to = List.delete(message["to"] || [], user.follower_address)
+ cc = List.delete(message["cc"] || [], user.follower_address)
message =
message
@@ -69,9 +73,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
{:ok, message}
else
- # Unhandleable error: somebody is messing around, just drop the message.
_e ->
- {:reject, nil}
+ {:reject, "[ObjectAgePolicy] Unhandled error"}
end
else
{:ok, message}
@@ -98,7 +101,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
@impl true
def describe do
mrf_object_age =
- Pleroma.Config.get(:mrf_object_age)
+ Config.get(:mrf_object_age)
|> Enum.into(%{})
{:ok, %{mrf_object_age: mrf_object_age}}
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 3092f3272..0b9ed2224 100644
--- a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex
+++ b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex
@@ -38,7 +38,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do
{:ok, object}
true ->
- {:reject, nil}
+ {:reject, "[RejectNonPublic] visibility: #{visibility}"}
end
end
@@ -47,5 +47,5 @@ defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do
@impl true
def describe,
- do: {:ok, %{mrf_rejectnonpublic: Pleroma.Config.get(:mrf_rejectnonpublic) |> Enum.into(%{})}}
+ do: {:ok, %{mrf_rejectnonpublic: Config.get(:mrf_rejectnonpublic) |> Enum.into(%{})}}
end
diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
index 4edc007fd..bb193475a 100644
--- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
@@ -3,33 +3,36 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
- alias Pleroma.User
- alias Pleroma.Web.ActivityPub.MRF
@moduledoc "Filter activities depending on their origin instance"
@behaviour Pleroma.Web.ActivityPub.MRF
+ alias Pleroma.Config
+ alias Pleroma.FollowingRelationship
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.MRF
+
require Pleroma.Constants
defp check_accept(%{host: actor_host} = _actor_info, object) do
accepts =
- Pleroma.Config.get([:mrf_simple, :accept])
+ Config.get([:mrf_simple, :accept])
|> MRF.subdomains_regex()
cond do
accepts == [] -> {:ok, object}
- actor_host == Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host]) -> {:ok, object}
+ actor_host == Config.get([Pleroma.Web.Endpoint, :url, :host]) -> {:ok, object}
MRF.subdomain_match?(accepts, actor_host) -> {:ok, object}
- true -> {:reject, nil}
+ true -> {:reject, "[SimplePolicy] host not in accept list"}
end
end
defp check_reject(%{host: actor_host} = _actor_info, object) do
rejects =
- Pleroma.Config.get([:mrf_simple, :reject])
+ Config.get([:mrf_simple, :reject])
|> MRF.subdomains_regex()
if MRF.subdomain_match?(rejects, actor_host) do
- {:reject, nil}
+ {:reject, "[SimplePolicy] host in reject list"}
else
{:ok, object}
end
@@ -41,7 +44,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
)
when length(child_attachment) > 0 do
media_removal =
- Pleroma.Config.get([:mrf_simple, :media_removal])
+ Config.get([:mrf_simple, :media_removal])
|> MRF.subdomains_regex()
object =
@@ -65,7 +68,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
} = object
) do
media_nsfw =
- Pleroma.Config.get([:mrf_simple, :media_nsfw])
+ Config.get([:mrf_simple, :media_nsfw])
|> MRF.subdomains_regex()
object =
@@ -85,7 +88,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
defp check_ftl_removal(%{host: actor_host} = _actor_info, object) do
timeline_removal =
- Pleroma.Config.get([:mrf_simple, :federated_timeline_removal])
+ Config.get([:mrf_simple, :federated_timeline_removal])
|> MRF.subdomains_regex()
object =
@@ -106,13 +109,42 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
{:ok, object}
end
+ defp intersection(list1, list2) do
+ list1 -- list1 -- list2
+ end
+
+ defp check_followers_only(%{host: actor_host} = _actor_info, object) do
+ followers_only =
+ Config.get([:mrf_simple, :followers_only])
+ |> MRF.subdomains_regex()
+
+ object =
+ with true <- MRF.subdomain_match?(followers_only, actor_host),
+ user <- User.get_cached_by_ap_id(object["actor"]) do
+ # Don't use Map.get/3 intentionally, these must not be nil
+ fixed_to = object["to"] || []
+ fixed_cc = object["cc"] || []
+
+ to = FollowingRelationship.followers_ap_ids(user, fixed_to)
+ cc = FollowingRelationship.followers_ap_ids(user, fixed_cc)
+
+ object
+ |> Map.put("to", intersection([user.follower_address | to], fixed_to))
+ |> Map.put("cc", intersection([user.follower_address | cc], fixed_cc))
+ else
+ _ -> object
+ end
+
+ {:ok, object}
+ end
+
defp check_report_removal(%{host: actor_host} = _actor_info, %{"type" => "Flag"} = object) do
report_removal =
- Pleroma.Config.get([:mrf_simple, :report_removal])
+ Config.get([:mrf_simple, :report_removal])
|> MRF.subdomains_regex()
if MRF.subdomain_match?(report_removal, actor_host) do
- {:reject, nil}
+ {:reject, "[SimplePolicy] host in report_removal list"}
else
{:ok, object}
end
@@ -122,7 +154,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
defp check_avatar_removal(%{host: actor_host} = _actor_info, %{"icon" => _icon} = object) do
avatar_removal =
- Pleroma.Config.get([:mrf_simple, :avatar_removal])
+ Config.get([:mrf_simple, :avatar_removal])
|> MRF.subdomains_regex()
if MRF.subdomain_match?(avatar_removal, actor_host) do
@@ -136,7 +168,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
defp check_banner_removal(%{host: actor_host} = _actor_info, %{"image" => _image} = object) do
banner_removal =
- Pleroma.Config.get([:mrf_simple, :banner_removal])
+ Config.get([:mrf_simple, :banner_removal])
|> MRF.subdomains_regex()
if MRF.subdomain_match?(banner_removal, actor_host) do
@@ -149,6 +181,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 =
+ Config.get([:mrf_simple, :reject_deletes])
+ |> MRF.subdomains_regex()
+
+ if MRF.subdomain_match?(reject_deletes, actor_host) do
+ {:reject, "[SimplePolicy] host in reject_deletes list"}
+ else
+ {:ok, object}
+ end
+ end
+
+ @impl true
def filter(%{"actor" => actor} = object) do
actor_info = URI.parse(actor)
@@ -157,10 +204,13 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
{:ok, object} <- check_media_removal(actor_info, object),
{:ok, object} <- check_media_nsfw(actor_info, object),
{:ok, object} <- check_ftl_removal(actor_info, object),
+ {:ok, object} <- check_followers_only(actor_info, object),
{:ok, object} <- check_report_removal(actor_info, object) do
{:ok, object}
else
- _e -> {:reject, nil}
+ {:reject, nil} -> {:reject, "[SimplePolicy]"}
+ {:reject, _} = e -> e
+ _ -> {:reject, "[SimplePolicy]"}
end
end
@@ -174,7 +224,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
{:ok, object} <- check_banner_removal(actor_info, object) do
{:ok, object}
else
- _e -> {:reject, nil}
+ {:reject, nil} -> {:reject, "[SimplePolicy]"}
+ {:reject, _} = e -> e
+ _ -> {:reject, "[SimplePolicy]"}
end
end
@@ -182,10 +234,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
@impl true
def describe do
- exclusions = Pleroma.Config.get([:instance, :mrf_transparency_exclusions])
+ exclusions = Config.get([:mrf, :transparency_exclusions])
mrf_simple =
- Pleroma.Config.get(:mrf_simple)
+ Config.get(:mrf_simple)
|> Enum.map(fn {k, v} -> {k, Enum.reject(v, fn v -> v in exclusions end)} end)
|> Enum.into(%{})
diff --git a/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex b/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex
new file mode 100644
index 000000000..2858af9eb
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex
@@ -0,0 +1,97 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
+ require Logger
+
+ alias Pleroma.Config
+
+ @moduledoc "Detect new emojis by their shortcode and steals them"
+ @behaviour Pleroma.Web.ActivityPub.MRF
+
+ defp remote_host?(host), do: host != Config.get([Pleroma.Web.Endpoint, :url, :host])
+
+ defp accept_host?(host), do: host in Config.get([:mrf_steal_emoji, :hosts], [])
+
+ defp steal_emoji({shortcode, url}) do
+ url = Pleroma.Web.MediaProxy.url(url)
+ {:ok, response} = Pleroma.HTTP.get(url)
+ size_limit = Config.get([:mrf_steal_emoji, :size_limit], 50_000)
+
+ if byte_size(response.body) <= size_limit do
+ emoji_dir_path =
+ Config.get(
+ [:mrf_steal_emoji, :path],
+ Path.join(Config.get([:instance, :static_dir]), "emoji/stolen")
+ )
+
+ extension =
+ url
+ |> URI.parse()
+ |> Map.get(:path)
+ |> Path.basename()
+ |> Path.extname()
+
+ file_path = Path.join([emoji_dir_path, shortcode <> (extension || ".png")])
+
+ try do
+ :ok = File.write(file_path, response.body)
+
+ shortcode
+ rescue
+ e ->
+ Logger.warn("MRF.StealEmojiPolicy: Failed to write to #{file_path}: #{inspect(e)}")
+ nil
+ end
+ else
+ Logger.debug(
+ "MRF.StealEmojiPolicy: :#{shortcode}: at #{url} (#{byte_size(response.body)} B) over size limit (#{
+ size_limit
+ } B)"
+ )
+
+ nil
+ end
+ rescue
+ e ->
+ Logger.warn("MRF.StealEmojiPolicy: Failed to fetch #{url}: #{inspect(e)}")
+ nil
+ end
+
+ @impl true
+ def filter(%{"object" => %{"emoji" => foreign_emojis, "actor" => actor}} = message) do
+ host = URI.parse(actor).host
+
+ if remote_host?(host) and accept_host?(host) do
+ installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end)
+
+ new_emojis =
+ foreign_emojis
+ |> Enum.filter(fn {shortcode, _url} -> shortcode not in installed_emoji end)
+ |> Enum.filter(fn {shortcode, _url} ->
+ reject_emoji? =
+ Config.get([:mrf_steal_emoji, :rejected_shortcodes], [])
+ |> Enum.find(false, fn regex -> String.match?(shortcode, regex) end)
+
+ !reject_emoji?
+ end)
+ |> Enum.map(&steal_emoji(&1))
+ |> Enum.filter(& &1)
+
+ if !Enum.empty?(new_emojis) do
+ Logger.info("Stole new emojis: #{inspect(new_emojis)}")
+ Pleroma.Emoji.reload()
+ end
+ end
+
+ {:ok, message}
+ end
+
+ def filter(message), do: {:ok, message}
+
+ @impl true
+ def describe do
+ {:ok, %{}}
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex
index c310462cb..febabda08 100644
--- a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex
@@ -134,12 +134,13 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do
if user.local == true do
{:ok, message}
else
- {:reject, nil}
+ {:reject,
+ "[TagPolicy] Follow from #{actor} tagged with mrf_tag:disable-remote-subscription"}
end
end
- defp process_tag("mrf_tag:disable-any-subscription", %{"type" => "Follow"}),
- do: {:reject, nil}
+ defp process_tag("mrf_tag:disable-any-subscription", %{"type" => "Follow", "actor" => actor}),
+ do: {:reject, "[TagPolicy] Follow from #{actor} tagged with mrf_tag:disable-any-subscription"}
defp process_tag(_, message), do: {:ok, message}
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 651aed70f..1a28f2ba2 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
@@ -14,7 +14,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy do
if actor in allow_list do
{:ok, object}
else
- {:reject, nil}
+ {:reject, "[UserAllowListPolicy] #{actor} not in the list"}
end
end
diff --git a/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex b/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex
index 6167a74e2..a6c545570 100644
--- a/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex
@@ -11,22 +11,26 @@ defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicy do
with {:ok, _} <- filter(child_message) do
{:ok, message}
else
- {:reject, nil} ->
- {:reject, nil}
+ {:reject, _} = e -> e
end
end
def filter(%{"type" => message_type} = message) do
with accepted_vocabulary <- Pleroma.Config.get([:mrf_vocabulary, :accept]),
rejected_vocabulary <- Pleroma.Config.get([:mrf_vocabulary, :reject]),
- true <-
- Enum.empty?(accepted_vocabulary) || Enum.member?(accepted_vocabulary, message_type),
- false <-
- length(rejected_vocabulary) > 0 && Enum.member?(rejected_vocabulary, message_type),
+ {_, true} <-
+ {:accepted,
+ Enum.empty?(accepted_vocabulary) || Enum.member?(accepted_vocabulary, message_type)},
+ {_, false} <-
+ {:rejected,
+ length(rejected_vocabulary) > 0 && Enum.member?(rejected_vocabulary, message_type)},
{:ok, _} <- filter(message["object"]) do
{:ok, message}
else
- _ -> {:reject, nil}
+ {:reject, _} = e -> e
+ {:accepted, _} -> {:reject, "[VocabularyPolicy] #{message_type} not in accept list"}
+ {:rejected, _} -> {:reject, "[VocabularyPolicy] #{message_type} in reject list"}
+ _ -> {:reject, "[VocabularyPolicy]"}
end
end
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..b77c06395
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validator.ex
@@ -0,0 +1,275 @@
+# 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.Activity
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators
+ alias Pleroma.Object
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ObjectValidators.AcceptRejectValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.AnswerValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.AudioValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.EventValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.FollowValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator
+
+ @spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
+ def validate(object, meta)
+
+ def validate(%{"type" => type} = object, meta)
+ when type in ~w[Accept Reject] do
+ with {:ok, object} <-
+ object
+ |> AcceptRejectValidator.cast_and_validate()
+ |> Ecto.Changeset.apply_action(:insert) do
+ object = stringify_keys(object)
+ {:ok, object, meta}
+ end
+ end
+
+ def validate(%{"type" => "Event"} = object, meta) do
+ with {:ok, object} <-
+ object
+ |> EventValidator.cast_and_validate()
+ |> Ecto.Changeset.apply_action(:insert) do
+ object = stringify_keys(object)
+ {:ok, object, meta}
+ end
+ end
+
+ def validate(%{"type" => "Follow"} = object, meta) do
+ with {:ok, object} <-
+ object
+ |> FollowValidator.cast_and_validate()
+ |> Ecto.Changeset.apply_action(:insert) do
+ object = stringify_keys(object)
+ {:ok, object, meta}
+ end
+ end
+
+ def validate(%{"type" => "Block"} = block_activity, meta) do
+ with {:ok, block_activity} <-
+ block_activity
+ |> BlockValidator.cast_and_validate()
+ |> Ecto.Changeset.apply_action(:insert) do
+ block_activity = stringify_keys(block_activity)
+ outgoing_blocks = Pleroma.Config.get([:activitypub, :outgoing_blocks])
+
+ meta =
+ if !outgoing_blocks do
+ Keyword.put(meta, :do_not_federate, true)
+ else
+ meta
+ end
+
+ {:ok, block_activity, meta}
+ end
+ end
+
+ def validate(%{"type" => "Update"} = update_activity, meta) do
+ with {:ok, update_activity} <-
+ update_activity
+ |> UpdateValidator.cast_and_validate()
+ |> Ecto.Changeset.apply_action(:insert) do
+ update_activity = stringify_keys(update_activity)
+ {:ok, update_activity, meta}
+ end
+ end
+
+ 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)
+ undone_object = Activity.get_by_ap_id(object["object"])
+
+ meta =
+ meta
+ |> Keyword.put(:object_data, undone_object.data)
+
+ {: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)
+ {:ok, object, meta}
+ end
+ end
+
+ def validate(%{"type" => "ChatMessage"} = object, meta) do
+ with {:ok, object} <-
+ object
+ |> ChatMessageValidator.cast_and_validate()
+ |> Ecto.Changeset.apply_action(:insert) do
+ object = stringify_keys(object)
+ {:ok, object, meta}
+ end
+ end
+
+ def validate(%{"type" => "Question"} = object, meta) do
+ with {:ok, object} <-
+ object
+ |> QuestionValidator.cast_and_validate()
+ |> Ecto.Changeset.apply_action(:insert) do
+ object = stringify_keys(object)
+ {:ok, object, meta}
+ end
+ end
+
+ def validate(%{"type" => "Audio"} = object, meta) do
+ with {:ok, object} <-
+ object
+ |> AudioValidator.cast_and_validate()
+ |> Ecto.Changeset.apply_action(:insert) do
+ object = stringify_keys(object)
+ {:ok, object, meta}
+ end
+ end
+
+ def validate(%{"type" => "Answer"} = object, meta) do
+ with {:ok, object} <-
+ object
+ |> AnswerValidator.cast_and_validate()
+ |> Ecto.Changeset.apply_action(:insert) do
+ object = stringify_keys(object)
+ {: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)
+ {:ok, object, meta}
+ end
+ end
+
+ def validate(
+ %{"type" => "Create", "object" => %{"type" => "ChatMessage"} = object} = create_activity,
+ meta
+ ) do
+ with {:ok, object_data} <- cast_and_apply(object),
+ meta = Keyword.put(meta, :object_data, object_data |> stringify_keys),
+ {:ok, create_activity} <-
+ create_activity
+ |> CreateChatMessageValidator.cast_and_validate(meta)
+ |> Ecto.Changeset.apply_action(:insert) do
+ create_activity = stringify_keys(create_activity)
+ {:ok, create_activity, meta}
+ end
+ end
+
+ def validate(
+ %{"type" => "Create", "object" => %{"type" => objtype} = object} = create_activity,
+ meta
+ )
+ when objtype in ~w[Question Answer Audio Event] do
+ with {:ok, object_data} <- cast_and_apply(object),
+ meta = Keyword.put(meta, :object_data, object_data |> stringify_keys),
+ {:ok, create_activity} <-
+ create_activity
+ |> CreateGenericValidator.cast_and_validate(meta)
+ |> Ecto.Changeset.apply_action(:insert) do
+ create_activity = stringify_keys(create_activity)
+ {:ok, create_activity, meta}
+ end
+ end
+
+ def validate(%{"type" => "Announce"} = object, meta) do
+ with {:ok, object} <-
+ object
+ |> AnnounceValidator.cast_and_validate()
+ |> Ecto.Changeset.apply_action(:insert) do
+ object = stringify_keys(object)
+ {:ok, object, meta}
+ end
+ end
+
+ def cast_and_apply(%{"type" => "ChatMessage"} = object) do
+ ChatMessageValidator.cast_and_apply(object)
+ end
+
+ def cast_and_apply(%{"type" => "Question"} = object) do
+ QuestionValidator.cast_and_apply(object)
+ end
+
+ def cast_and_apply(%{"type" => "Answer"} = object) do
+ AnswerValidator.cast_and_apply(object)
+ end
+
+ def cast_and_apply(%{"type" => "Audio"} = object) do
+ AudioValidator.cast_and_apply(object)
+ end
+
+ def cast_and_apply(%{"type" => "Event"} = object) do
+ EventValidator.cast_and_apply(object)
+ end
+
+ def cast_and_apply(o), do: {:error, {:validator_not_set, o}}
+
+ # is_struct/1 isn't present in Elixir 1.8.x
+ def stringify_keys(%{__struct__: _} = object) do
+ object
+ |> Map.from_struct()
+ |> stringify_keys
+ end
+
+ def stringify_keys(object) when is_map(object) do
+ object
+ |> Map.new(fn {key, val} -> {to_string(key), stringify_keys(val)} end)
+ end
+
+ def stringify_keys(object) when is_list(object) do
+ object
+ |> Enum.map(&stringify_keys/1)
+ end
+
+ def stringify_keys(object), do: object
+
+ def fetch_actor(object) do
+ with {:ok, actor} <- ObjectValidators.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"], true)
+ :ok
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/object_validators/accept_reject_validator.ex b/lib/pleroma/web/activity_pub/object_validators/accept_reject_validator.ex
new file mode 100644
index 000000000..179beda58
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/accept_reject_validator.ex
@@ -0,0 +1,56 @@
+# 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.AcceptRejectValidator do
+ use Ecto.Schema
+
+ alias Pleroma.Activity
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators
+
+ import Ecto.Changeset
+ import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
+
+ @primary_key false
+
+ embedded_schema do
+ field(:id, ObjectValidators.ObjectID, primary_key: true)
+ field(:type, :string)
+ field(:object, ObjectValidators.ObjectID)
+ field(:actor, ObjectValidators.ObjectID)
+ field(:to, ObjectValidators.Recipients, default: [])
+ field(:cc, ObjectValidators.Recipients, default: [])
+ end
+
+ def cast_data(data) do
+ %__MODULE__{}
+ |> cast(data, __schema__(:fields))
+ end
+
+ def validate_data(cng) do
+ cng
+ |> validate_required([:id, :type, :actor, :to, :cc, :object])
+ |> validate_inclusion(:type, ["Accept", "Reject"])
+ |> validate_actor_presence()
+ |> validate_object_presence(allowed_types: ["Follow"])
+ |> validate_accept_reject_rights()
+ end
+
+ def cast_and_validate(data) do
+ data
+ |> cast_data
+ |> validate_data
+ end
+
+ def validate_accept_reject_rights(cng) do
+ with object_id when is_binary(object_id) <- get_field(cng, :object),
+ %Activity{data: %{"object" => followed_actor}} <- Activity.get_by_ap_id(object_id),
+ true <- followed_actor == get_field(cng, :actor) do
+ cng
+ else
+ _e ->
+ cng
+ |> add_error(:actor, "can't accept or reject the given activity")
+ end
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/object_validators/announce_validator.ex b/lib/pleroma/web/activity_pub/object_validators/announce_validator.ex
new file mode 100644
index 000000000..6f757f49c
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/announce_validator.ex
@@ -0,0 +1,101 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator do
+ use Ecto.Schema
+
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators
+ alias Pleroma.Object
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.Utils
+ alias Pleroma.Web.ActivityPub.Visibility
+
+ import Ecto.Changeset
+ import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
+
+ require Pleroma.Constants
+
+ @primary_key false
+
+ embedded_schema do
+ field(:id, ObjectValidators.ObjectID, primary_key: true)
+ field(:type, :string)
+ field(:object, ObjectValidators.ObjectID)
+ field(:actor, ObjectValidators.ObjectID)
+ field(:context, :string, autogenerate: {Utils, :generate_context_id, []})
+ field(:to, ObjectValidators.Recipients, default: [])
+ field(:cc, ObjectValidators.Recipients, default: [])
+ field(:published, ObjectValidators.DateTime)
+ end
+
+ def cast_and_validate(data) do
+ data
+ |> cast_data()
+ |> validate_data()
+ end
+
+ def cast_data(data) do
+ %__MODULE__{}
+ |> changeset(data)
+ end
+
+ def changeset(struct, data) do
+ struct
+ |> cast(data, __schema__(:fields))
+ |> fix_after_cast()
+ end
+
+ def fix_after_cast(cng) do
+ cng
+ end
+
+ def validate_data(data_cng) do
+ data_cng
+ |> validate_inclusion(:type, ["Announce"])
+ |> validate_required([:id, :type, :object, :actor, :to, :cc])
+ |> validate_actor_presence()
+ |> validate_object_presence()
+ |> validate_existing_announce()
+ |> validate_announcable()
+ end
+
+ def validate_announcable(cng) do
+ with actor when is_binary(actor) <- get_field(cng, :actor),
+ object when is_binary(object) <- get_field(cng, :object),
+ %User{} = actor <- User.get_cached_by_ap_id(actor),
+ %Object{} = object <- Object.get_cached_by_ap_id(object),
+ false <- Visibility.is_public?(object) do
+ same_actor = object.data["actor"] == actor.ap_id
+ is_public = Pleroma.Constants.as_public() in (get_field(cng, :to) ++ get_field(cng, :cc))
+
+ cond do
+ same_actor && is_public ->
+ cng
+ |> add_error(:actor, "can not announce this object publicly")
+
+ !same_actor ->
+ cng
+ |> add_error(:actor, "can not announce this object")
+
+ true ->
+ cng
+ end
+ else
+ _ -> cng
+ end
+ end
+
+ def validate_existing_announce(cng) do
+ actor = get_field(cng, :actor)
+ object = get_field(cng, :object)
+
+ if actor && object && Utils.get_existing_announce(actor, %{data: %{"id" => object}}) do
+ cng
+ |> add_error(:actor, "already announced this object")
+ |> add_error(:object, "already announced by this actor")
+ else
+ cng
+ end
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/object_validators/answer_validator.ex b/lib/pleroma/web/activity_pub/object_validators/answer_validator.ex
new file mode 100644
index 000000000..b9fbaf4f6
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/answer_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.AnswerValidator do
+ use Ecto.Schema
+
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators
+ alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
+
+ import Ecto.Changeset
+
+ @primary_key false
+ @derive Jason.Encoder
+
+ embedded_schema do
+ field(:id, ObjectValidators.ObjectID, primary_key: true)
+ field(:to, ObjectValidators.Recipients, default: [])
+ field(:cc, ObjectValidators.Recipients, default: [])
+ field(:bto, ObjectValidators.Recipients, default: [])
+ field(:bcc, ObjectValidators.Recipients, default: [])
+ field(:type, :string)
+ field(:name, :string)
+ field(:inReplyTo, ObjectValidators.ObjectID)
+ field(:attributedTo, ObjectValidators.ObjectID)
+
+ # TODO: Remove actor on objects
+ field(:actor, ObjectValidators.ObjectID)
+ end
+
+ def cast_and_apply(data) do
+ data
+ |> cast_data()
+ |> apply_action(:insert)
+ 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, ["Answer"])
+ |> validate_required([:id, :inReplyTo, :name, :attributedTo, :actor])
+ |> CommonValidations.validate_any_presence([:cc, :to])
+ |> CommonValidations.validate_fields_match([:actor, :attributedTo])
+ |> CommonValidations.validate_actor_presence()
+ |> CommonValidations.validate_host_match()
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex
new file mode 100644
index 000000000..c8b148280
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.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.AttachmentValidator do
+ use Ecto.Schema
+
+ alias Pleroma.Web.ActivityPub.ObjectValidators.UrlObjectValidator
+
+ import Ecto.Changeset
+
+ @primary_key false
+ embedded_schema do
+ field(:type, :string)
+ field(:mediaType, :string, default: "application/octet-stream")
+ field(:name, :string)
+
+ embeds_many(:url, UrlObjectValidator)
+ 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
+ data =
+ data
+ |> fix_media_type()
+ |> fix_url()
+
+ struct
+ |> cast(data, [:type, :mediaType, :name])
+ |> cast_embed(:url, required: true)
+ end
+
+ def fix_media_type(data) do
+ data = Map.put_new(data, "mediaType", data["mimeType"])
+
+ if MIME.valid?(data["mediaType"]) do
+ data
+ else
+ Map.put(data, "mediaType", "application/octet-stream")
+ end
+ end
+
+ defp handle_href(href, mediaType) do
+ [
+ %{
+ "href" => href,
+ "type" => "Link",
+ "mediaType" => mediaType
+ }
+ ]
+ end
+
+ defp fix_url(data) do
+ cond do
+ is_binary(data["url"]) ->
+ Map.put(data, "url", handle_href(data["url"], data["mediaType"]))
+
+ is_binary(data["href"]) and data["url"] == nil ->
+ Map.put(data, "url", handle_href(data["href"], data["mediaType"]))
+
+ true ->
+ data
+ end
+ end
+
+ def validate_data(cng) do
+ cng
+ |> validate_required([:mediaType, :url, :type])
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/object_validators/audio_validator.ex b/lib/pleroma/web/activity_pub/object_validators/audio_validator.ex
new file mode 100644
index 000000000..d1869f188
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/audio_validator.ex
@@ -0,0 +1,106 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioValidator do
+ use Ecto.Schema
+
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators
+ alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
+ alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
+
+ import Ecto.Changeset
+
+ @primary_key false
+ @derive Jason.Encoder
+
+ embedded_schema do
+ field(:id, ObjectValidators.ObjectID, primary_key: true)
+ field(:to, ObjectValidators.Recipients, default: [])
+ field(:cc, ObjectValidators.Recipients, default: [])
+ field(:bto, ObjectValidators.Recipients, default: [])
+ field(:bcc, ObjectValidators.Recipients, default: [])
+ # TODO: Write type
+ field(:tag, {:array, :map}, default: [])
+ field(:type, :string)
+ field(:content, :string)
+ field(:context, :string)
+
+ # TODO: Remove actor on objects
+ field(:actor, ObjectValidators.ObjectID)
+
+ field(:attributedTo, ObjectValidators.ObjectID)
+ field(:summary, :string)
+ field(:published, ObjectValidators.DateTime)
+ # TODO: Write type
+ field(:emoji, :map, default: %{})
+ field(:sensitive, :boolean, default: false)
+ embeds_many(:attachment, AttachmentValidator)
+ field(:replies_count, :integer, default: 0)
+ field(:like_count, :integer, default: 0)
+ field(:announcement_count, :integer, default: 0)
+ field(:inReplyTo, :string)
+ field(:url, ObjectValidators.Uri)
+ # short identifier for PleromaFE to group statuses by context
+ field(:context_id, :integer)
+
+ field(:likes, {:array, :string}, default: [])
+ field(:announcements, {:array, :string}, default: [])
+ end
+
+ def cast_and_apply(data) do
+ data
+ |> cast_data
+ |> apply_action(:insert)
+ end
+
+ def cast_and_validate(data) do
+ data
+ |> cast_data()
+ |> validate_data()
+ end
+
+ def cast_data(data) do
+ %__MODULE__{}
+ |> changeset(data)
+ end
+
+ defp fix_url(%{"url" => url} = data) when is_list(url) do
+ attachment =
+ Enum.find(url, fn x -> is_map(x) and String.starts_with?(x["mimeType"], "audio/") end)
+
+ link_element = Enum.find(url, fn x -> is_map(x) and x["mimeType"] == "text/html" end)
+
+ data
+ |> Map.put("attachment", [attachment])
+ |> Map.put("url", link_element["href"])
+ end
+
+ defp fix_url(data), do: data
+
+ defp fix(data) do
+ data
+ |> CommonFixes.fix_defaults()
+ |> CommonFixes.fix_attribution()
+ |> fix_url()
+ end
+
+ def changeset(struct, data) do
+ data = fix(data)
+
+ struct
+ |> cast(data, __schema__(:fields) -- [:attachment])
+ |> cast_embed(:attachment)
+ end
+
+ def validate_data(data_cng) do
+ data_cng
+ |> validate_inclusion(:type, ["Audio"])
+ |> validate_required([:id, :actor, :attributedTo, :type, :context, :attachment])
+ |> CommonValidations.validate_any_presence([:cc, :to])
+ |> CommonValidations.validate_fields_match([:actor, :attributedTo])
+ |> CommonValidations.validate_actor_presence()
+ |> CommonValidations.validate_host_match()
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/object_validators/block_validator.ex b/lib/pleroma/web/activity_pub/object_validators/block_validator.ex
new file mode 100644
index 000000000..1dde77198
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/block_validator.ex
@@ -0,0 +1,42 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator do
+ use Ecto.Schema
+
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators
+
+ import Ecto.Changeset
+ import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
+
+ @primary_key false
+
+ embedded_schema do
+ field(:id, ObjectValidators.ObjectID, primary_key: true)
+ field(:type, :string)
+ field(:actor, ObjectValidators.ObjectID)
+ field(:to, ObjectValidators.Recipients, default: [])
+ field(:cc, ObjectValidators.Recipients, default: [])
+ field(:object, ObjectValidators.ObjectID)
+ end
+
+ def cast_data(data) do
+ %__MODULE__{}
+ |> cast(data, __schema__(:fields))
+ end
+
+ def validate_data(cng) do
+ cng
+ |> validate_required([:id, :type, :actor, :to, :cc, :object])
+ |> validate_inclusion(:type, ["Block"])
+ |> validate_actor_presence()
+ |> validate_actor_presence(field_name: :object)
+ end
+
+ def cast_and_validate(data) do
+ data
+ |> cast_data
+ |> validate_data
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex
new file mode 100644
index 000000000..91b475393
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex
@@ -0,0 +1,129 @@
+# 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.ChatMessageValidator do
+ use Ecto.Schema
+
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator
+
+ import Ecto.Changeset
+ import Pleroma.Web.ActivityPub.Transmogrifier, only: [fix_emoji: 1]
+
+ @primary_key false
+ @derive Jason.Encoder
+
+ embedded_schema do
+ field(:id, ObjectValidators.ObjectID, primary_key: true)
+ field(:to, ObjectValidators.Recipients, default: [])
+ field(:type, :string)
+ field(:content, ObjectValidators.SafeText)
+ field(:actor, ObjectValidators.ObjectID)
+ field(:published, ObjectValidators.DateTime)
+ field(:emoji, :map, default: %{})
+
+ embeds_one(:attachment, AttachmentValidator)
+ end
+
+ def cast_and_apply(data) do
+ data
+ |> cast_data
+ |> apply_action(:insert)
+ end
+
+ def cast_and_validate(data) do
+ data
+ |> cast_data()
+ |> validate_data()
+ end
+
+ def cast_data(data) do
+ %__MODULE__{}
+ |> changeset(data)
+ end
+
+ def fix(data) do
+ data
+ |> fix_emoji()
+ |> fix_attachment()
+ |> Map.put_new("actor", data["attributedTo"])
+ end
+
+ # Throws everything but the first one away
+ def fix_attachment(%{"attachment" => [attachment | _]} = data) do
+ data
+ |> Map.put("attachment", attachment)
+ end
+
+ def fix_attachment(data), do: data
+
+ def changeset(struct, data) do
+ data = fix(data)
+
+ struct
+ |> cast(data, List.delete(__schema__(:fields), :attachment))
+ |> cast_embed(:attachment)
+ end
+
+ def validate_data(data_cng) do
+ data_cng
+ |> validate_inclusion(:type, ["ChatMessage"])
+ |> validate_required([:id, :actor, :to, :type, :published])
+ |> validate_content_or_attachment()
+ |> validate_length(:to, is: 1)
+ |> validate_length(:content, max: Pleroma.Config.get([:instance, :remote_limit]))
+ |> validate_local_concern()
+ end
+
+ def validate_content_or_attachment(cng) do
+ attachment = get_field(cng, :attachment)
+
+ if attachment do
+ cng
+ else
+ cng
+ |> validate_required([:content])
+ end
+ end
+
+ @doc """
+ Validates the following
+ - If both users are in our system
+ - If at least one of the users in this ChatMessage is a local user
+ - If the recipient is not blocking the actor
+ - If the recipient is explicitly not accepting chat messages
+ """
+ def validate_local_concern(cng) do
+ with actor_ap <- get_field(cng, :actor),
+ {_, %User{} = actor} <- {:find_actor, User.get_cached_by_ap_id(actor_ap)},
+ {_, %User{} = recipient} <-
+ {:find_recipient, User.get_cached_by_ap_id(get_field(cng, :to) |> hd())},
+ {_, false} <- {:not_accepting_chats?, recipient.accepts_chat_messages == false},
+ {_, false} <- {:blocking_actor?, User.blocks?(recipient, actor)},
+ {_, true} <- {:local?, Enum.any?([actor, recipient], & &1.local)} do
+ cng
+ else
+ {:blocking_actor?, true} ->
+ cng
+ |> add_error(:actor, "actor is blocked by recipient")
+
+ {:not_accepting_chats?, true} ->
+ cng
+ |> add_error(:to, "recipient does not accept chat messages")
+
+ {:local?, false} ->
+ cng
+ |> add_error(:actor, "actor and recipient are both remote")
+
+ {:find_actor, _} ->
+ cng
+ |> add_error(:actor, "can't find user")
+
+ {:find_recipient, _} ->
+ cng
+ |> add_error(:to, "can't find user")
+ end
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex
new file mode 100644
index 000000000..721749de0
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex
@@ -0,0 +1,22 @@
+# 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.CommonFixes do
+ alias Pleroma.Web.ActivityPub.Utils
+
+ # based on Pleroma.Web.ActivityPub.Utils.lazy_put_objects_defaults
+ def fix_defaults(data) do
+ %{data: %{"id" => context}, id: context_id} =
+ Utils.create_context(data["context"] || data["conversation"])
+
+ data
+ |> Map.put_new("context", context)
+ |> Map.put_new("context_id", context_id)
+ end
+
+ def fix_attribution(data) do
+ data
+ |> Map.put_new("actor", data["attributedTo"])
+ 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..603d87b8e
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/common_validations.ex
@@ -0,0 +1,141 @@
+# 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_any_presence(cng, fields) 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, "none of #{inspect(fields)} present")
+ 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 ->
+ case User.get_cached_by_ap_id(actor) do
+ %User{deactivated: true} ->
+ [{field_name, "user is deactivated"}]
+
+ %User{} ->
+ []
+
+ _ ->
+ [{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
+
+ def validate_host_match(cng, fields \\ [:id, :actor]) do
+ if same_domain?(cng, fields) do
+ cng
+ else
+ fields
+ |> Enum.reduce(cng, fn field, cng ->
+ cng
+ |> add_error(field, "hosts of #{inspect(fields)} aren't matching")
+ end)
+ end
+ end
+
+ def validate_fields_match(cng, fields) do
+ if map_unique?(cng, fields) do
+ cng
+ else
+ fields
+ |> Enum.reduce(cng, fn field, cng ->
+ cng
+ |> add_error(field, "Fields #{inspect(fields)} aren't matching")
+ end)
+ end
+ end
+
+ defp map_unique?(cng, fields, func \\ & &1) do
+ Enum.reduce_while(fields, nil, fn field, acc ->
+ value =
+ cng
+ |> get_field(field)
+ |> func.()
+
+ case {value, acc} do
+ {value, nil} -> {:cont, value}
+ {value, value} -> {:cont, value}
+ _ -> {:halt, false}
+ end
+ end)
+ end
+
+ def same_domain?(cng, fields \\ [:actor, :object]) do
+ map_unique?(cng, fields, fn value -> URI.parse(value).host end)
+ end
+
+ # This figures out if a user is able to create, delete or modify something
+ # based on the domain and superuser status
+ def validate_modification_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 modify object")
+ end
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex
new file mode 100644
index 000000000..7269f9ff0
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex
@@ -0,0 +1,91 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+# NOTES
+# - Can probably be a generic create validator
+# - doesn't embed, will only get the object id
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator do
+ use Ecto.Schema
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators
+
+ alias Pleroma.Object
+
+ import Ecto.Changeset
+ import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
+
+ @primary_key false
+
+ embedded_schema do
+ field(:id, ObjectValidators.ObjectID, primary_key: true)
+ field(:actor, ObjectValidators.ObjectID)
+ field(:type, :string)
+ field(:to, ObjectValidators.Recipients, default: [])
+ field(:object, ObjectValidators.ObjectID)
+ end
+
+ def cast_and_apply(data) do
+ data
+ |> cast_data
+ |> apply_action(:insert)
+ end
+
+ def cast_data(data) do
+ cast(%__MODULE__{}, data, __schema__(:fields))
+ end
+
+ def cast_and_validate(data, meta \\ []) do
+ cast_data(data)
+ |> validate_data(meta)
+ end
+
+ def validate_data(cng, meta \\ []) do
+ cng
+ |> validate_required([:id, :actor, :to, :type, :object])
+ |> validate_inclusion(:type, ["Create"])
+ |> validate_actor_presence()
+ |> validate_recipients_match(meta)
+ |> validate_actors_match(meta)
+ |> validate_object_nonexistence()
+ end
+
+ def validate_object_nonexistence(cng) do
+ cng
+ |> validate_change(:object, fn :object, object_id ->
+ if Object.get_cached_by_ap_id(object_id) do
+ [{:object, "The object to create already exists"}]
+ else
+ []
+ end
+ end)
+ end
+
+ def validate_actors_match(cng, meta) do
+ object_actor = meta[:object_data]["actor"]
+
+ cng
+ |> validate_change(:actor, fn :actor, actor ->
+ if actor == object_actor do
+ []
+ else
+ [{:actor, "Actor doesn't match with object actor"}]
+ end
+ end)
+ end
+
+ def validate_recipients_match(cng, meta) do
+ object_recipients = meta[:object_data]["to"] || []
+
+ cng
+ |> validate_change(:to, fn :to, recipients ->
+ activity_set = MapSet.new(recipients)
+ object_set = MapSet.new(object_recipients)
+
+ if MapSet.equal?(activity_set, object_set) do
+ []
+ else
+ [{:to, "Recipients don't match with object recipients"}]
+ end
+ end)
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex
new file mode 100644
index 000000000..b3dbeea57
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.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
+
+# Code based on CreateChatMessageValidator
+# NOTES
+# - doesn't embed, will only get the object id
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator do
+ use Ecto.Schema
+
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators
+ alias Pleroma.Object
+
+ import Ecto.Changeset
+ import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
+
+ @primary_key false
+
+ embedded_schema do
+ field(:id, ObjectValidators.ObjectID, primary_key: true)
+ field(:actor, ObjectValidators.ObjectID)
+ field(:type, :string)
+ field(:to, ObjectValidators.Recipients, default: [])
+ field(:cc, ObjectValidators.Recipients, default: [])
+ field(:object, ObjectValidators.ObjectID)
+ field(:expires_at, ObjectValidators.DateTime)
+
+ # Should be moved to object, done for CommonAPI.Utils.make_context
+ field(:context, :string)
+ end
+
+ def cast_data(data, meta \\ []) do
+ data = fix(data, meta)
+
+ %__MODULE__{}
+ |> changeset(data)
+ end
+
+ def cast_and_apply(data) do
+ data
+ |> cast_data
+ |> apply_action(:insert)
+ end
+
+ def cast_and_validate(data, meta \\ []) do
+ data
+ |> cast_data(meta)
+ |> validate_data(meta)
+ end
+
+ def changeset(struct, data) do
+ struct
+ |> cast(data, __schema__(:fields))
+ end
+
+ defp fix_context(data, meta) do
+ if object = meta[:object_data] do
+ Map.put_new(data, "context", object["context"])
+ else
+ data
+ end
+ end
+
+ defp fix_addressing(data, meta) do
+ if object = meta[:object_data] do
+ data
+ |> Map.put_new("to", object["to"] || [])
+ |> Map.put_new("cc", object["cc"] || [])
+ else
+ data
+ end
+ end
+
+ defp fix(data, meta) do
+ data
+ |> fix_context(meta)
+ |> fix_addressing(meta)
+ end
+
+ def validate_data(cng, meta \\ []) do
+ cng
+ |> validate_required([:actor, :type, :object])
+ |> validate_inclusion(:type, ["Create"])
+ |> validate_actor_presence()
+ |> validate_any_presence([:to, :cc])
+ |> validate_actors_match(meta)
+ |> validate_context_match(meta)
+ |> validate_object_nonexistence()
+ |> validate_object_containment()
+ end
+
+ def validate_object_containment(cng) do
+ actor = get_field(cng, :actor)
+
+ cng
+ |> validate_change(:object, fn :object, object_id ->
+ %URI{host: object_id_host} = URI.parse(object_id)
+ %URI{host: actor_host} = URI.parse(actor)
+
+ if object_id_host == actor_host do
+ []
+ else
+ [{:object, "The host of the object id doesn't match with the host of the actor"}]
+ end
+ end)
+ end
+
+ def validate_object_nonexistence(cng) do
+ cng
+ |> validate_change(:object, fn :object, object_id ->
+ if Object.get_cached_by_ap_id(object_id) do
+ [{:object, "The object to create already exists"}]
+ else
+ []
+ end
+ end)
+ end
+
+ def validate_actors_match(cng, meta) do
+ attributed_to = meta[:object_data]["attributedTo"] || meta[:object_data]["actor"]
+
+ cng
+ |> validate_change(:actor, fn :actor, actor ->
+ if actor == attributed_to do
+ []
+ else
+ [{:actor, "Actor doesn't match with object attributedTo"}]
+ end
+ end)
+ end
+
+ def validate_context_match(cng, %{object_data: %{"context" => object_context}}) do
+ cng
+ |> validate_change(:context, fn :context, context ->
+ if context == object_context do
+ []
+ else
+ [{:context, "context field not matching between Create and object (#{object_context})"}]
+ end
+ end)
+ end
+
+ def validate_context_match(cng, _), do: cng
+end
diff --git a/lib/pleroma/web/activity_pub/object_validators/create_note_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_note_validator.ex
new file mode 100644
index 000000000..9b9743c4a
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/create_note_validator.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.ActivityPub.ObjectValidators.CreateNoteValidator do
+ use Ecto.Schema
+
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators
+ alias Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator
+
+ import Ecto.Changeset
+
+ @primary_key false
+
+ embedded_schema do
+ field(:id, ObjectValidators.ObjectID, primary_key: true)
+ field(:actor, ObjectValidators.ObjectID)
+ field(:type, :string)
+ field(:to, ObjectValidators.Recipients, default: [])
+ field(:cc, ObjectValidators.Recipients, default: [])
+ field(:bto, ObjectValidators.Recipients, default: [])
+ field(:bcc, ObjectValidators.Recipients, 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..2634e8d4d
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex
@@ -0,0 +1,75 @@
+# 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.EctoType.ActivityPub.ObjectValidators
+
+ import Ecto.Changeset
+ import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
+
+ @primary_key false
+
+ embedded_schema do
+ field(:id, ObjectValidators.ObjectID, primary_key: true)
+ field(:type, :string)
+ field(:actor, ObjectValidators.ObjectID)
+ field(:to, ObjectValidators.Recipients, default: [])
+ field(:cc, ObjectValidators.Recipients, default: [])
+ field(:deleted_activity_id, ObjectValidators.ObjectID)
+ field(:object, ObjectValidators.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
+ ChatMessage
+ Event
+ Note
+ Page
+ Question
+ Tombstone
+ Video
+ }
+ def validate_data(cng) do
+ cng
+ |> validate_required([:id, :type, :actor, :to, :cc, :object])
+ |> validate_inclusion(:type, ["Delete"])
+ |> validate_actor_presence()
+ |> validate_modification_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
+
+ 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..336c92d35
--- /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.EctoType.ActivityPub.ObjectValidators
+ alias Pleroma.Object
+
+ import Ecto.Changeset
+ import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
+
+ @primary_key false
+
+ embedded_schema do
+ field(:id, ObjectValidators.ObjectID, primary_key: true)
+ field(:type, :string)
+ field(:object, ObjectValidators.ObjectID)
+ field(:actor, ObjectValidators.ObjectID)
+ field(:context, :string)
+ field(:content, :string)
+ field(:to, ObjectValidators.Recipients, default: [])
+ field(:cc, ObjectValidators.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_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/event_validator.ex b/lib/pleroma/web/activity_pub/object_validators/event_validator.ex
new file mode 100644
index 000000000..07e4821a4
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/event_validator.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.ActivityPub.ObjectValidators.EventValidator do
+ use Ecto.Schema
+
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators
+ alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
+ alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
+
+ import Ecto.Changeset
+
+ @primary_key false
+ @derive Jason.Encoder
+
+ # Extends from NoteValidator
+ embedded_schema do
+ field(:id, ObjectValidators.ObjectID, primary_key: true)
+ field(:to, ObjectValidators.Recipients, default: [])
+ field(:cc, ObjectValidators.Recipients, default: [])
+ field(:bto, ObjectValidators.Recipients, default: [])
+ field(:bcc, ObjectValidators.Recipients, default: [])
+ # TODO: Write type
+ field(:tag, {:array, :map}, default: [])
+ field(:type, :string)
+
+ field(:name, :string)
+ field(:summary, :string)
+ field(:content, :string)
+
+ field(:context, :string)
+ # short identifier for PleromaFE to group statuses by context
+ field(:context_id, :integer)
+
+ # TODO: Remove actor on objects
+ field(:actor, ObjectValidators.ObjectID)
+
+ field(:attributedTo, ObjectValidators.ObjectID)
+ field(:published, ObjectValidators.DateTime)
+ # TODO: Write type
+ field(:emoji, :map, default: %{})
+ field(:sensitive, :boolean, default: false)
+ embeds_many(:attachment, AttachmentValidator)
+ field(:replies_count, :integer, default: 0)
+ field(:like_count, :integer, default: 0)
+ field(:announcement_count, :integer, default: 0)
+ field(:inReplyTo, ObjectValidators.ObjectID)
+ field(:url, ObjectValidators.Uri)
+
+ field(:likes, {:array, ObjectValidators.ObjectID}, default: [])
+ field(:announcements, {:array, ObjectValidators.ObjectID}, default: [])
+ end
+
+ def cast_and_apply(data) do
+ data
+ |> cast_data
+ |> apply_action(:insert)
+ end
+
+ def cast_and_validate(data) do
+ data
+ |> cast_data()
+ |> validate_data()
+ end
+
+ def cast_data(data) do
+ %__MODULE__{}
+ |> changeset(data)
+ end
+
+ defp fix(data) do
+ data
+ |> CommonFixes.fix_defaults()
+ |> CommonFixes.fix_attribution()
+ end
+
+ def changeset(struct, data) do
+ data = fix(data)
+
+ struct
+ |> cast(data, __schema__(:fields) -- [:attachment])
+ |> cast_embed(:attachment)
+ end
+
+ def validate_data(data_cng) do
+ data_cng
+ |> validate_inclusion(:type, ["Event"])
+ |> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id])
+ |> CommonValidations.validate_any_presence([:cc, :to])
+ |> CommonValidations.validate_fields_match([:actor, :attributedTo])
+ |> CommonValidations.validate_actor_presence()
+ |> CommonValidations.validate_host_match()
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/object_validators/follow_validator.ex b/lib/pleroma/web/activity_pub/object_validators/follow_validator.ex
new file mode 100644
index 000000000..ca2724616
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/follow_validator.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.ActivityPub.ObjectValidators.FollowValidator do
+ use Ecto.Schema
+
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators
+
+ import Ecto.Changeset
+ import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
+
+ @primary_key false
+
+ embedded_schema do
+ field(:id, ObjectValidators.ObjectID, primary_key: true)
+ field(:type, :string)
+ field(:actor, ObjectValidators.ObjectID)
+ field(:to, ObjectValidators.Recipients, default: [])
+ field(:cc, ObjectValidators.Recipients, default: [])
+ field(:object, ObjectValidators.ObjectID)
+ field(:state, :string, default: "pending")
+ end
+
+ def cast_data(data) do
+ %__MODULE__{}
+ |> cast(data, __schema__(:fields))
+ end
+
+ def validate_data(cng) do
+ cng
+ |> validate_required([:id, :type, :actor, :to, :cc, :object])
+ |> validate_inclusion(:type, ["Follow"])
+ |> validate_inclusion(:state, ~w{pending reject accept})
+ |> validate_actor_presence()
+ |> validate_actor_presence(field_name: :object)
+ end
+
+ def cast_and_validate(data) do
+ data
+ |> cast_data
+ |> validate_data
+ 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..493e4c247
--- /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.EctoType.ActivityPub.ObjectValidators
+ alias Pleroma.Object
+ alias Pleroma.Web.ActivityPub.Utils
+
+ import Ecto.Changeset
+ import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
+
+ @primary_key false
+
+ embedded_schema do
+ field(:id, ObjectValidators.ObjectID, primary_key: true)
+ field(:type, :string)
+ field(:object, ObjectValidators.ObjectID)
+ field(:actor, ObjectValidators.ObjectID)
+ field(:context, :string)
+ field(:to, ObjectValidators.Recipients, default: [])
+ field(:cc, ObjectValidators.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} <- ObjectValidators.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..20e735619
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/note_validator.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.ActivityPub.ObjectValidators.NoteValidator do
+ use Ecto.Schema
+
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators
+
+ import Ecto.Changeset
+
+ @primary_key false
+
+ embedded_schema do
+ field(:id, ObjectValidators.ObjectID, primary_key: true)
+ field(:to, ObjectValidators.Recipients, default: [])
+ field(:cc, ObjectValidators.Recipients, default: [])
+ field(:bto, ObjectValidators.Recipients, default: [])
+ field(:bcc, ObjectValidators.Recipients, default: [])
+ # TODO: Write type
+ field(:tag, {:array, :map}, default: [])
+ field(:type, :string)
+
+ field(:name, :string)
+ field(:summary, :string)
+ field(:content, :string)
+
+ field(:context, :string)
+ # short identifier for PleromaFE to group statuses by context
+ field(:context_id, :integer)
+
+ field(:actor, ObjectValidators.ObjectID)
+ field(:attributedTo, ObjectValidators.ObjectID)
+ field(:published, ObjectValidators.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(:inReplyTo, ObjectValidators.ObjectID)
+ field(:url, ObjectValidators.Uri)
+
+ field(:likes, {:array, :string}, default: [])
+ field(:announcements, {:array, :string}, default: [])
+ 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/question_options_validator.ex b/lib/pleroma/web/activity_pub/object_validators/question_options_validator.ex
new file mode 100644
index 000000000..478b3b5cf
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/question_options_validator.ex
@@ -0,0 +1,37 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionOptionsValidator do
+ use Ecto.Schema
+
+ import Ecto.Changeset
+
+ @primary_key false
+
+ embedded_schema do
+ field(:name, :string)
+
+ embeds_one :replies, Replies, primary_key: false do
+ field(:totalItems, :integer)
+ field(:type, :string)
+ end
+
+ field(:type, :string)
+ end
+
+ def changeset(struct, data) do
+ struct
+ |> cast(data, [:name, :type])
+ |> cast_embed(:replies, with: &replies_changeset/2)
+ |> validate_inclusion(:type, ["Note"])
+ |> validate_required([:name, :type])
+ end
+
+ def replies_changeset(struct, data) do
+ struct
+ |> cast(data, [:totalItems, :type])
+ |> validate_inclusion(:type, ["Collection"])
+ |> validate_required([:type])
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex
new file mode 100644
index 000000000..712047424
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex
@@ -0,0 +1,111 @@
+# 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.QuestionValidator do
+ use Ecto.Schema
+
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators
+ alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
+ alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
+ alias Pleroma.Web.ActivityPub.ObjectValidators.QuestionOptionsValidator
+
+ import Ecto.Changeset
+
+ @primary_key false
+ @derive Jason.Encoder
+
+ # Extends from NoteValidator
+ embedded_schema do
+ field(:id, ObjectValidators.ObjectID, primary_key: true)
+ field(:to, ObjectValidators.Recipients, default: [])
+ field(:cc, ObjectValidators.Recipients, default: [])
+ field(:bto, ObjectValidators.Recipients, default: [])
+ field(:bcc, ObjectValidators.Recipients, default: [])
+ # TODO: Write type
+ field(:tag, {:array, :map}, default: [])
+ field(:type, :string)
+ field(:content, :string)
+ field(:context, :string)
+
+ # TODO: Remove actor on objects
+ field(:actor, ObjectValidators.ObjectID)
+
+ field(:attributedTo, ObjectValidators.ObjectID)
+ field(:summary, :string)
+ field(:published, ObjectValidators.DateTime)
+ # TODO: Write type
+ field(:emoji, :map, default: %{})
+ field(:sensitive, :boolean, default: false)
+ embeds_many(:attachment, AttachmentValidator)
+ field(:replies_count, :integer, default: 0)
+ field(:like_count, :integer, default: 0)
+ field(:announcement_count, :integer, default: 0)
+ field(:inReplyTo, ObjectValidators.ObjectID)
+ field(:url, ObjectValidators.Uri)
+ # short identifier for PleromaFE to group statuses by context
+ field(:context_id, :integer)
+
+ field(:likes, {:array, :string}, default: [])
+ field(:announcements, {:array, :string}, default: [])
+
+ field(:closed, ObjectValidators.DateTime)
+ field(:voters, {:array, ObjectValidators.ObjectID}, default: [])
+ embeds_many(:anyOf, QuestionOptionsValidator)
+ embeds_many(:oneOf, QuestionOptionsValidator)
+ end
+
+ def cast_and_apply(data) do
+ data
+ |> cast_data
+ |> apply_action(:insert)
+ end
+
+ def cast_and_validate(data) do
+ data
+ |> cast_data()
+ |> validate_data()
+ end
+
+ def cast_data(data) do
+ %__MODULE__{}
+ |> changeset(data)
+ end
+
+ defp fix_closed(data) do
+ cond do
+ is_binary(data["closed"]) -> data
+ is_binary(data["endTime"]) -> Map.put(data, "closed", data["endTime"])
+ true -> Map.drop(data, ["closed"])
+ end
+ end
+
+ defp fix(data) do
+ data
+ |> CommonFixes.fix_defaults()
+ |> CommonFixes.fix_attribution()
+ |> fix_closed()
+ end
+
+ def changeset(struct, data) do
+ data = fix(data)
+
+ struct
+ |> cast(data, __schema__(:fields) -- [:anyOf, :oneOf, :attachment])
+ |> cast_embed(:attachment)
+ |> cast_embed(:anyOf)
+ |> cast_embed(:oneOf)
+ end
+
+ def validate_data(data_cng) do
+ data_cng
+ |> validate_inclusion(:type, ["Question"])
+ |> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id])
+ |> CommonValidations.validate_any_presence([:cc, :to])
+ |> CommonValidations.validate_fields_match([:actor, :attributedTo])
+ |> CommonValidations.validate_actor_presence()
+ |> CommonValidations.validate_any_presence([:oneOf, :anyOf])
+ |> CommonValidations.validate_host_match()
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/object_validators/undo_validator.ex b/lib/pleroma/web/activity_pub/object_validators/undo_validator.ex
new file mode 100644
index 000000000..8cae94467
--- /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.EctoType.ActivityPub.ObjectValidators
+
+ import Ecto.Changeset
+ import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
+
+ @primary_key false
+
+ embedded_schema do
+ field(:id, ObjectValidators.ObjectID, primary_key: true)
+ field(:type, :string)
+ field(:object, ObjectValidators.ObjectID)
+ field(:actor, ObjectValidators.ObjectID)
+ field(:to, ObjectValidators.Recipients, default: [])
+ field(:cc, ObjectValidators.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))
+ 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/object_validators/update_validator.ex b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex
new file mode 100644
index 000000000..b4ba5ede0
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/update_validator.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.ObjectValidators.UpdateValidator do
+ use Ecto.Schema
+
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators
+
+ import Ecto.Changeset
+ import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
+
+ @primary_key false
+
+ embedded_schema do
+ field(:id, ObjectValidators.ObjectID, primary_key: true)
+ field(:type, :string)
+ field(:actor, ObjectValidators.ObjectID)
+ field(:to, ObjectValidators.Recipients, default: [])
+ field(:cc, ObjectValidators.Recipients, default: [])
+ # In this case, we save the full object in this activity instead of just a
+ # reference, so we can always see what was actually changed by this.
+ field(:object, :map)
+ end
+
+ def cast_data(data) do
+ %__MODULE__{}
+ |> cast(data, __schema__(:fields))
+ end
+
+ def validate_data(cng) do
+ cng
+ |> validate_required([:id, :type, :actor, :to, :cc, :object])
+ |> validate_inclusion(:type, ["Update"])
+ |> validate_actor_presence()
+ |> validate_updating_rights()
+ end
+
+ def cast_and_validate(data) do
+ data
+ |> cast_data
+ |> validate_data
+ end
+
+ # For now we only support updating users, and here the rule is easy:
+ # object id == actor id
+ def validate_updating_rights(cng) do
+ with actor = get_field(cng, :actor),
+ object = get_field(cng, :object),
+ {:ok, object_id} <- ObjectValidators.ObjectID.cast(object),
+ true <- actor == object_id do
+ cng
+ else
+ _e ->
+ cng
+ |> add_error(:object, "Can't be updated by this actor")
+ end
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex b/lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex
new file mode 100644
index 000000000..881030f38
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex
@@ -0,0 +1,24 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.UrlObjectValidator do
+ use Ecto.Schema
+
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators
+
+ import Ecto.Changeset
+ @primary_key false
+
+ embedded_schema do
+ field(:type, :string)
+ field(:href, ObjectValidators.Uri)
+ field(:mediaType, :string, default: "application/octet-stream")
+ end
+
+ def changeset(struct, data) do
+ struct
+ |> cast(data, __schema__(:fields))
+ |> validate_required([:type, :href, :mediaType])
+ 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..36e325c37
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/pipeline.ex
@@ -0,0 +1,71 @@
+# 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.Config
+ alias Pleroma.Object
+ alias Pleroma.Repo
+ alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.MRF
+ alias Pleroma.Web.ActivityPub.ObjectValidator
+ alias Pleroma.Web.ActivityPub.SideEffects
+ alias Pleroma.Web.Federator
+
+ @spec common_pipeline(map(), keyword()) ::
+ {:ok, Activity.t() | Object.t(), keyword()} | {:error, any()}
+ def common_pipeline(object, meta) do
+ case Repo.transaction(fn -> do_common_pipeline(object, meta) end) do
+ {:ok, {:ok, activity, meta}} ->
+ SideEffects.handle_after_transaction(meta)
+ {:ok, activity, meta}
+
+ {: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] || !Config.get([:instance, :federating])
+
+ if !do_not_federate && local do
+ activity =
+ if object = Keyword.get(meta, :object_data) do
+ %{activity | data: Map.put(activity.data, "object", object)}
+ else
+ activity
+ end
+
+ 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 6c558e7f0..d88f7f3ee 100644
--- a/lib/pleroma/web/activity_pub/publisher.ex
+++ b/lib/pleroma/web/activity_pub/publisher.ex
@@ -49,7 +49,8 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
"""
def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = params) do
Logger.debug("Federating #{id} to #{inbox}")
- %{host: host, path: path} = URI.parse(inbox)
+
+ uri = URI.parse(inbox)
digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
@@ -57,8 +58,8 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
signature =
Pleroma.Signature.sign(actor, %{
- "(request-target)": "post #{path}",
- host: host,
+ "(request-target)": "post #{uri.path}",
+ host: signature_host(uri),
"content-length": byte_size(json),
digest: digest,
date: date
@@ -76,8 +77,9 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
{"digest", digest}
]
) do
- if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since],
- do: Instances.set_reachable(inbox)
+ if not Map.has_key?(params, :unreachable_since) || params[:unreachable_since] do
+ Instances.set_reachable(inbox)
+ end
result
else
@@ -96,6 +98,14 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|> publish_one()
end
+ defp signature_host(%URI{port: port, scheme: scheme, host: host}) do
+ if port == URI.default_port(scheme) do
+ host
+ else
+ "#{host}:#{port}"
+ end
+ end
+
defp should_federate?(inbox, public) do
if public do
true
@@ -141,8 +151,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 +167,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 +184,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
maybe_use_sharedinbox(user)
true ->
- data["inbox"]
+ inbox
end
end
@@ -192,14 +202,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 729c23af7..b65710a94 100644
--- a/lib/pleroma/web/activity_pub/relay.ex
+++ b/lib/pleroma/web/activity_pub/relay.ex
@@ -4,30 +4,25 @@
defmodule Pleroma.Web.ActivityPub.Relay do
alias Pleroma.Activity
- alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Visibility
+ alias Pleroma.Web.CommonAPI
require Logger
- @relay_nickname "relay"
+ @nickname "relay"
- def get_actor do
- actor =
- relay_ap_id()
- |> User.get_or_create_service_actor_by_ap_id(@relay_nickname)
+ @spec ap_id() :: String.t()
+ def ap_id, do: "#{Pleroma.Web.Endpoint.url()}/#{@nickname}"
- actor
- end
-
- def relay_ap_id do
- "#{Pleroma.Web.Endpoint.url()}/relay"
- end
+ @spec get_actor() :: User.t() | nil
+ def get_actor, do: User.get_or_create_service_actor_by_ap_id(ap_id(), @nickname)
@spec follow(String.t()) :: {:ok, Activity.t()} | {:error, any()}
def follow(target_instance) do
with %User{} = local_user <- get_actor(),
{:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_instance),
- {:ok, activity} <- ActivityPub.follow(local_user, target_user) do
+ {:ok, _, _, activity} <- CommonAPI.follow(local_user, target_user) do
Logger.info("relay: followed instance: #{target_instance}; id=#{activity.data["id"]}")
{:ok, activity}
else
@@ -48,11 +43,11 @@ defmodule Pleroma.Web.ActivityPub.Relay do
end
end
- @spec publish(any()) :: {:ok, Activity.t(), Object.t()} | {:error, any()}
+ @spec publish(any()) :: {:ok, Activity.t()} | {:error, any()}
def publish(%Activity{data: %{"type" => "Create"}} = activity) do
with %User{} = user <- get_actor(),
- %Object{} = object <- Object.normalize(activity) do
- ActivityPub.announce(user, object, nil, true, false)
+ true <- Visibility.is_public?(activity) do
+ CommonAPI.repeat(activity.id, user)
else
error -> format_error(error)
end
@@ -60,34 +55,38 @@ defmodule Pleroma.Web.ActivityPub.Relay do
def publish(_), do: {:error, "Not implemented"}
- @spec list(boolean()) :: {:ok, [String.t()]} | {:error, any()}
- def list(with_not_accepted \\ false) do
+ @spec list() :: {:ok, [%{actor: String.t(), followed_back: boolean()}]} | {:error, any()}
+ def list do
with %User{} = user <- get_actor() do
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()
+ |> following()
+ |> Enum.map(fn actor -> %{actor: actor, followed_back: true} end)
- accepted ++ without_accept
- else
- accepted
- end
+ without_accept =
+ user
+ |> Pleroma.Activity.following_requests_for_actor()
+ |> Enum.map(fn activity -> %{actor: activity.data["object"], followed_back: false} end)
+ |> Enum.uniq()
- {:ok, list}
+ {:ok, accepted ++ without_accept}
else
error -> format_error(error)
end
end
+ @spec following() :: [String.t()]
+ def following do
+ get_actor()
+ |> following()
+ end
+
+ defp following(user) do
+ user
+ |> User.following_ap_ids()
+ |> Enum.uniq()
+ end
+
defp format_error({:error, error}), do: format_error(error)
defp format_error(error) do
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..a5e2323bd
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/side_effects.ex
@@ -0,0 +1,441 @@
+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.Activity.Ir.Topics
+ alias Pleroma.ActivityExpiration
+ alias Pleroma.Chat
+ alias Pleroma.Chat.MessageReference
+ alias Pleroma.FollowingRelationship
+ alias Pleroma.Notification
+ alias Pleroma.Object
+ alias Pleroma.Repo
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Builder
+ alias Pleroma.Web.ActivityPub.Pipeline
+ alias Pleroma.Web.ActivityPub.Utils
+ alias Pleroma.Web.Push
+ alias Pleroma.Web.Streamer
+ alias Pleroma.Workers.BackgroundWorker
+
+ require Logger
+
+ def handle(object, meta \\ [])
+
+ # Task this handles
+ # - Follows
+ # - Sends a notification
+ def handle(
+ %{
+ data: %{
+ "actor" => actor,
+ "type" => "Accept",
+ "object" => follow_activity_id
+ }
+ } = object,
+ meta
+ ) do
+ with %Activity{actor: follower_id} = follow_activity <-
+ Activity.get_by_ap_id(follow_activity_id),
+ %User{} = followed <- User.get_cached_by_ap_id(actor),
+ %User{} = follower <- User.get_cached_by_ap_id(follower_id),
+ {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
+ {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept) do
+ Notification.update_notification_type(followed, follow_activity)
+ User.update_follower_count(followed)
+ User.update_following_count(follower)
+ end
+
+ {:ok, object, meta}
+ end
+
+ # Task this handles
+ # - Rejects all existing follow activities for this person
+ # - Updates the follow state
+ # - Dismisses notification
+ def handle(
+ %{
+ data: %{
+ "actor" => actor,
+ "type" => "Reject",
+ "object" => follow_activity_id
+ }
+ } = object,
+ meta
+ ) do
+ with %Activity{actor: follower_id} = follow_activity <-
+ Activity.get_by_ap_id(follow_activity_id),
+ %User{} = followed <- User.get_cached_by_ap_id(actor),
+ %User{} = follower <- User.get_cached_by_ap_id(follower_id),
+ {:ok, _follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject") do
+ FollowingRelationship.update(follower, followed, :follow_reject)
+ Notification.dismiss(follow_activity)
+ end
+
+ {:ok, object, meta}
+ end
+
+ # Tasks this handle
+ # - Follows if possible
+ # - Sends a notification
+ # - Generates accept or reject if appropriate
+ def handle(
+ %{
+ data: %{
+ "id" => follow_id,
+ "type" => "Follow",
+ "object" => followed_user,
+ "actor" => following_user
+ }
+ } = object,
+ meta
+ ) do
+ with %User{} = follower <- User.get_cached_by_ap_id(following_user),
+ %User{} = followed <- User.get_cached_by_ap_id(followed_user),
+ {_, {:ok, _}, _, _} <-
+ {:following, User.follow(follower, followed, :follow_pending), follower, followed} do
+ if followed.local && !followed.locked do
+ {:ok, accept_data, _} = Builder.accept(followed, object)
+ {:ok, _activity, _} = Pipeline.common_pipeline(accept_data, local: true)
+ end
+ else
+ {:following, {:error, _}, _follower, followed} ->
+ {:ok, reject_data, _} = Builder.reject(followed, object)
+ {:ok, _activity, _} = Pipeline.common_pipeline(reject_data, local: true)
+
+ _ ->
+ nil
+ end
+
+ {:ok, notifications} = Notification.create_notifications(object, do_send: false)
+
+ meta =
+ meta
+ |> add_notifications(notifications)
+
+ updated_object = Activity.get_by_ap_id(follow_id)
+
+ {:ok, updated_object, meta}
+ end
+
+ # Tasks this handles:
+ # - Unfollow and block
+ def handle(
+ %{data: %{"type" => "Block", "object" => blocked_user, "actor" => blocking_user}} =
+ object,
+ meta
+ ) do
+ with %User{} = blocker <- User.get_cached_by_ap_id(blocking_user),
+ %User{} = blocked <- User.get_cached_by_ap_id(blocked_user) do
+ User.block(blocker, blocked)
+ end
+
+ {:ok, object, meta}
+ end
+
+ # Tasks this handles:
+ # - Update the user
+ #
+ # For a local user, we also get a changeset with the full information, so we
+ # can update non-federating, non-activitypub settings as well.
+ def handle(%{data: %{"type" => "Update", "object" => updated_object}} = object, meta) do
+ if changeset = Keyword.get(meta, :user_update_changeset) do
+ changeset
+ |> User.update_and_set_cache()
+ else
+ {:ok, new_user_data} = ActivityPub.user_data_from_user_object(updated_object)
+
+ User.get_by_ap_id(updated_object["id"])
+ |> User.remote_user_changeset(new_user_data)
+ |> User.update_and_set_cache()
+ end
+
+ {:ok, object, meta}
+ end
+
+ # 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
+
+ # Tasks this handles
+ # - Actually create object
+ # - Rollback if we couldn't create it
+ # - Increase the user note count
+ # - Increase the reply count
+ # - Increase replies count
+ # - Set up ActivityExpiration
+ # - Set up notifications
+ def handle(%{data: %{"type" => "Create"}} = activity, meta) do
+ with {:ok, object, meta} <- handle_object_creation(meta[:object_data], meta),
+ %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
+ {:ok, notifications} = Notification.create_notifications(activity, do_send: false)
+ {:ok, _user} = ActivityPub.increase_note_count_if_public(user, object)
+
+ if in_reply_to = object.data["inReplyTo"] do
+ Object.increase_replies_count(in_reply_to)
+ end
+
+ if expires_at = activity.data["expires_at"] do
+ ActivityExpiration.create(activity, expires_at)
+ end
+
+ BackgroundWorker.enqueue("fetch_data_for_activity", %{"activity_id" => activity.id})
+
+ meta =
+ meta
+ |> add_notifications(notifications)
+
+ {:ok, activity, meta}
+ else
+ e -> Repo.rollback(e)
+ end
+ end
+
+ # Tasks this handles:
+ # - Add announce to object
+ # - Set up notification
+ # - Stream out the announce
+ def handle(%{data: %{"type" => "Announce"}} = object, meta) do
+ announced_object = Object.get_by_ap_id(object.data["object"])
+ user = User.get_cached_by_ap_id(object.data["actor"])
+
+ Utils.add_announce_to_object(object, announced_object)
+
+ if !User.is_internal_user?(user) do
+ Notification.create_notifications(object)
+
+ object
+ |> Topics.get_activity_topics()
+ |> Streamer.stream(object)
+ end
+
+ {: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),
+ {_, actor} when is_binary(actor) <- {:actor, deleted_object.data["actor"]},
+ %User{} = user <- User.get_cached_by_ap_id(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
+
+ MessageReference.delete_for_object(deleted_object)
+
+ ActivityPub.stream_out(object)
+ ActivityPub.stream_out_participations(deleted_object, user)
+ :ok
+ else
+ {:actor, _} ->
+ Logger.error("The object doesn't have an actor: #{inspect(deleted_object)}")
+ :no_object_actor
+ 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_object_creation(%{"type" => "ChatMessage"} = object, meta) do
+ with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do
+ actor = User.get_cached_by_ap_id(object.data["actor"])
+ recipient = User.get_cached_by_ap_id(hd(object.data["to"]))
+
+ streamables =
+ [[actor, recipient], [recipient, actor]]
+ |> Enum.map(fn [user, other_user] ->
+ if user.local do
+ {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id)
+ {:ok, cm_ref} = MessageReference.create(chat, object, user.ap_id != actor.ap_id)
+
+ {
+ ["user", "user:pleroma_chat"],
+ {user, %{cm_ref | chat: chat, object: object}}
+ }
+ end
+ end)
+ |> Enum.filter(& &1)
+
+ meta =
+ meta
+ |> add_streamables(streamables)
+
+ {:ok, object, meta}
+ end
+ end
+
+ def handle_object_creation(%{"type" => "Answer"} = object_map, meta) do
+ with {:ok, object, meta} <- Pipeline.common_pipeline(object_map, meta) do
+ Object.increase_vote_count(
+ object.data["inReplyTo"],
+ object.data["name"],
+ object.data["actor"]
+ )
+
+ {:ok, object, meta}
+ end
+ end
+
+ def handle_object_creation(%{"type" => objtype} = object, meta)
+ when objtype in ~w[Audio Question Event] do
+ with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do
+ {:ok, object, meta}
+ end
+ end
+
+ # Nothing to do
+ def handle_object_creation(object, meta) do
+ {:ok, object, meta}
+ end
+
+ defp undo_like(nil, object), do: delete_object(object)
+
+ defp undo_like(%Object{} = liked_object, object) do
+ with {:ok, _} <- Utils.remove_like_from_object(object, liked_object) do
+ delete_object(object)
+ end
+ end
+
+ def handle_undoing(%{data: %{"type" => "Like"}} = object) do
+ object.data["object"]
+ |> Object.get_by_ap_id()
+ |> undo_like(object)
+ 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]}
+
+ @spec delete_object(Object.t()) :: :ok | {:error, Ecto.Changeset.t()}
+ defp delete_object(object) do
+ with {:ok, _} <- Repo.delete(object), do: :ok
+ end
+
+ defp send_notifications(meta) do
+ Keyword.get(meta, :notifications, [])
+ |> Enum.each(fn notification ->
+ Streamer.stream(["user", "user:notification"], notification)
+ Push.send(notification)
+ end)
+
+ meta
+ end
+
+ defp send_streamables(meta) do
+ Keyword.get(meta, :streamables, [])
+ |> Enum.each(fn {topics, items} ->
+ Streamer.stream(topics, items)
+ end)
+
+ meta
+ end
+
+ defp add_streamables(meta, streamables) do
+ existing = Keyword.get(meta, :streamables, [])
+
+ meta
+ |> Keyword.put(:streamables, streamables ++ existing)
+ end
+
+ defp add_notifications(meta, notifications) do
+ existing = Keyword.get(meta, :notifications, [])
+
+ meta
+ |> Keyword.put(:notifications, notifications ++ existing)
+ end
+
+ def handle_after_transaction(meta) do
+ meta
+ |> send_notifications()
+ |> send_streamables()
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 3fc4762d6..76298c4a0 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -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.FollowingRelationship
+ alias Pleroma.EarmarkRenderer
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators
+ alias Pleroma.Maps
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.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
@@ -54,15 +60,17 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def fix_summary(object), do: Map.put(object, "summary", "")
def fix_addressing_list(map, field) do
+ addrs = map[field]
+
cond do
- is_binary(map[field]) ->
- Map.put(map, field, [map[field]])
+ is_list(addrs) ->
+ Map.put(map, field, Enum.filter(addrs, &is_binary/1))
- is_nil(map[field]) ->
- Map.put(map, field, [])
+ is_binary(addrs) ->
+ Map.put(map, field, [addrs])
true ->
- map
+ Map.put(map, field, [])
end
end
@@ -147,7 +155,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
def fix_actor(%{"attributedTo" => actor} = object) do
- Map.put(object, "actor", Containment.get_actor(%{"actor" => actor}))
+ actor = Containment.get_actor(%{"actor" => actor})
+
+ # TODO: Remove actor field for Objects
+ object
+ |> Map.put("actor", actor)
+ |> Map.put("attributedTo", actor)
end
def fix_in_reply_to(object, options \\ [])
@@ -164,11 +177,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
object
|> Map.put("inReplyTo", replied_object.data["id"])
|> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
- |> Map.put("conversation", replied_object.data["context"] || object["conversation"])
|> Map.put("context", replied_object.data["context"] || object["conversation"])
+ |> Map.drop(["conversation"])
else
e ->
- Logger.error("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
+ Logger.warn("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
object
end
else
@@ -199,20 +212,54 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
object
|> Map.put("context", context)
- |> Map.put("conversation", context)
+ |> Map.drop(["conversation"])
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}]
+ url =
+ cond do
+ is_list(data["url"]) -> List.first(data["url"])
+ is_map(data["url"]) -> data["url"]
+ true -> nil
+ end
- data
- |> Map.put("mediaType", media_type)
- |> Map.put("url", url)
+ media_type =
+ cond do
+ is_map(url) && MIME.valid?(url["mediaType"]) -> url["mediaType"]
+ MIME.valid?(data["mediaType"]) -> data["mediaType"]
+ MIME.valid?(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"]
+ true -> nil
+ end
+
+ if href do
+ attachment_url =
+ %{
+ "href" => href,
+ "type" => Map.get(url || %{}, "type", "Link")
+ }
+ |> Maps.put_if_present("mediaType", media_type)
+
+ %{
+ "url" => [attachment_url],
+ "type" => data["type"] || "Document"
+ }
+ |> Maps.put_if_present("mediaType", media_type)
+ |> Maps.put_if_present("name", data["name"])
+ else
+ nil
+ end
end)
+ |> Enum.filter(& &1)
Map.put(object, "attachment", attachments)
end
@@ -229,14 +276,19 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
Map.put(object, "url", url["href"])
end
- 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)
+ def fix_url(%{"type" => "Video", "url" => url} = object) when is_list(url) do
+ attachment =
+ Enum.find(url, fn x ->
+ media_type = x["mediaType"] || x["mimeType"] || ""
- link_element = Enum.find(url, fn x -> is_map(x) and x["mimeType"] == "text/html" end)
+ is_map(x) and String.starts_with?(media_type, "video/")
+ end)
+
+ link_element =
+ Enum.find(url, fn x -> is_map(x) and (x["mediaType"] || x["mimeType"]) == "text/html" end)
object
- |> Map.put("attachment", [first_element])
+ |> Map.put("attachment", [attachment])
|> Map.put("url", link_element["href"])
end
@@ -324,32 +376,18 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def fix_type(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),
- %Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
- {:ok, activity}
- else
- _ -> {:error, nil}
- end
- end
-
- defp mastodon_follow_hack(_, _), do: {:error, nil}
-
- defp get_follow_activity(follow_object, followed) do
- with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
- {_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
- {:ok, activity}
- else
- # Can't find the activity. This might a Mastodon 2.3 "Accept"
- {:activity, nil} ->
- mastodon_follow_hack(follow_object, followed)
+ 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()
- _ ->
- {:error, nil}
- end
+ Map.merge(object, %{"content" => html_content, "mediaType" => "text/html"})
end
+ defp fix_content(object), do: object
+
# Reduce the object list to find the reported user.
defp get_reported(objects) do
Enum.reduce_while(objects, nil, fn ap_id, _ ->
@@ -361,6 +399,29 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end)
end
+ # Compatibility wrapper for Mastodon votes
+ defp handle_create(%{"object" => %{"type" => "Answer"}} = data, _user) do
+ handle_incoming(data)
+ end
+
+ defp handle_create(%{"object" => object} = data, user) do
+ %{
+ to: data["to"],
+ object: object,
+ actor: user,
+ context: object["context"],
+ local: false,
+ published: data["published"],
+ additional:
+ Map.take(data, [
+ "cc",
+ "directMessage",
+ "id"
+ ])
+ }
+ |> ActivityPub.create()
+ end
+
def handle_incoming(data, options \\ [])
# Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
@@ -399,33 +460,18 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
%{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
options
)
- when objtype in ["Article", "Event", "Note", "Video", "Page", "Question", "Answer", "Audio"] do
+ when objtype in ~w{Article Note Video Page} do
actor = Containment.get_actor(data)
- data =
- Map.put(data, "actor", actor)
- |> fix_addressing
-
with nil <- Activity.get_create_by_object_ap_id(object["id"]),
- {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
- object = fix_object(object, options)
-
- params = %{
- to: data["to"],
- object: object,
- actor: user,
- context: object["conversation"],
- local: false,
- published: data["published"],
- additional:
- Map.take(data, [
- "cc",
- "directMessage",
- "id"
- ])
- }
+ {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(actor) do
+ data =
+ data
+ |> Map.put("object", fix_object(object, options))
+ |> Map.put("actor", actor)
+ |> fix_addressing()
- with {:ok, created_activity} <- ActivityPub.create(params) do
+ with {:ok, created_activity} <- handle_create(data, user) do
reply_depth = (options[:depth] || 0) + 1
if Federator.allowed_thread_distance?(reply_depth) do
@@ -476,116 +522,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
end
- def handle_incoming(
- %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data,
- _options
- ) do
- with %User{local: true} = followed <-
- User.get_cached_by_ap_id(Containment.get_actor(%{"actor" => followed})),
- {:ok, %User{} = follower} <-
- User.get_or_fetch_by_ap_id(Containment.get_actor(%{"actor" => follower})),
- {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
- with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
- {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
- {_, false} <- {:user_locked, User.locked?(followed)},
- {_, {: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, :follow_accept) do
- ActivityPub.accept(%{
- to: [follower.ap_id],
- actor: followed,
- object: data,
- local: true
- })
- else
- {:user_blocked, true} ->
- {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
- {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject)
-
- ActivityPub.reject(%{
- to: [follower.ap_id],
- actor: followed,
- object: data,
- local: true
- })
-
- {:follow, {:error, _}} ->
- {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
- {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject)
-
- ActivityPub.reject(%{
- to: [follower.ap_id],
- actor: followed,
- object: data,
- local: true
- })
-
- {:user_locked, true} ->
- {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_pending)
- :noop
- end
-
- {:ok, activity}
- else
- _e ->
- :error
- end
- end
-
- def handle_incoming(
- %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => id} = data,
- _options
- ) do
- with actor <- Containment.get_actor(data),
- {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
- {: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, :follow_accept) do
- User.update_follower_count(followed)
- User.update_following_count(follower)
-
- ActivityPub.accept(%{
- to: follow_activity.data["to"],
- type: "Accept",
- actor: followed,
- object: follow_activity.data["id"],
- local: false,
- activity_id: id
- })
- else
- _e ->
- :error
- end
- end
-
- def handle_incoming(
- %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => id} = data,
- _options
- ) do
- with actor <- Containment.get_actor(data),
- {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
- {: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, :follow_reject),
- {:ok, activity} <-
- ActivityPub.reject(%{
- to: follow_activity.data["to"],
- type: "Reject",
- actor: followed,
- object: follow_activity.data["id"],
- local: false,
- activity_id: id
- }) do
- {:ok, activity}
- else
- _e -> :error
- end
- end
-
@misskey_reactions %{
"like" => "👍",
"love" => "❤️",
@@ -615,137 +551,59 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
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
- {:ok, activity}
- else
- _e -> :error
- end
- end
-
- def handle_incoming(
- %{
- "type" => "EmojiReact",
- "object" => object_id,
- "actor" => _actor,
- "id" => id,
- "content" => emoji
- } = data,
+ %{"type" => "Create", "object" => %{"type" => objtype}} = 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
+ )
+ when objtype in ~w{Question Answer ChatMessage Audio Event} do
+ with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
+ {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
{:ok, activity}
- else
- _e -> :error
end
end
- def handle_incoming(
- %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
- _options
- ) do
- with actor <- Containment.get_actor(data),
- {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
- {:ok, object} <- get_embedded_obj_helper(object_id, actor),
- public <- Visibility.is_public?(data),
- {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
+ def handle_incoming(%{"type" => type} = data, _options)
+ when type in ~w{Like EmojiReact Announce} do
+ with :ok <- ObjectValidator.fetch_actor_and_object(data),
+ {:ok, activity, _meta} <-
+ Pipeline.common_pipeline(data, local: false) do
{:ok, activity}
else
- _e -> :error
+ e -> {:error, e}
end
end
def handle_incoming(
- %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
- data,
+ %{"type" => type} = data,
_options
)
- when object_type in [
- "Person",
- "Application",
- "Service",
- "Organization"
- ] do
- with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
- {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
-
- actor
- |> User.upgrade_changeset(new_user_data, true)
- |> User.update_and_set_cache()
-
- ActivityPub.update(%{
- local: false,
- to: data["to"] || [],
- cc: data["cc"] || [],
- object: object,
- actor: actor_id,
- activity_id: data["id"]
- })
- else
- e ->
- Logger.error(e)
- :error
- 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,
- _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
+ when type in ~w{Update Block Follow Accept Reject} do
+ with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
+ {: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
- end
-
- _e ->
- :error
end
end
def handle_incoming(
- %{
- "type" => "Undo",
- "object" => %{"type" => "Announce", "object" => object_id},
- "actor" => _actor,
- "id" => id
- } = data,
+ %{"type" => "Delete"} = 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
+ with {:ok, activity, _} <-
+ Pipeline.common_pipeline(data, local: false) do
{:ok, activity}
else
- _e -> :error
+ {:error, {:validate_object, _}} = e ->
+ # Check if we have a create activity for this
+ with {:ok, object_id} <- ObjectValidators.ObjectID.cast(data["object"]),
+ %Activity{data: %{"actor" => actor}} <-
+ Activity.create_by_object_ap_id(object_id) |> Repo.one(),
+ # We have one, insert a tombstone and retry
+ {:ok, tombstone_data, _} <- Builder.tombstone(actor, object_id),
+ {:ok, _tombstone} <- Object.create(tombstone_data) do
+ handle_incoming(data)
+ else
+ _ -> e
+ end
end
end
@@ -771,75 +629,13 @@ 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
- {:ok, activity}
- else
- _e -> :error
- end
- end
-
- 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}
- else
- _e -> :error
- end
- end
-
- def handle_incoming(
- %{"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.block(blocker, blocked, id, false) do
- User.unfollow(blocker, blocked)
- User.block(blocker, blocked)
- {:ok, activity}
- else
- _e -> :error
- end
- end
-
- 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
+ )
+ 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
@@ -1113,10 +909,14 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
Map.put(object, "tag", tags)
end
+ # TODO These should be added on our side on insertion, it doesn't make much
+ # sense to regenerate these all the time
def add_mention_tags(object) do
- {enabled_receivers, disabled_receivers} = Utils.get_notified_from_object(object)
- potential_receivers = enabled_receivers ++ disabled_receivers
- mentions = Enum.map(potential_receivers, &build_mention_tag/1)
+ to = object["to"] || []
+ cc = object["cc"] || []
+ mentioned = User.get_users_from_set(to ++ cc, local_only: false)
+
+ mentions = Enum.map(mentioned, &build_mention_tag/1)
tags = object["tag"] || []
Map.put(object, "tag", tags ++ mentions)
@@ -1128,7 +928,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
@@ -1157,6 +957,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)
@@ -1173,20 +977,29 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
Map.put(object, "attributedTo", attributed_to)
end
+ # TODO: Revisit this
+ def prepare_attachments(%{"type" => "ChatMessage"} = object), do: object
+
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
@@ -1222,12 +1035,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}
@@ -1235,9 +1044,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 a49cfa35e..713b0ca1f 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
alias Ecto.UUID
alias Pleroma.Activity
alias Pleroma.Config
+ alias Pleroma.Maps
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
@@ -244,7 +245,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
Inserts a full object if it is contained in an activity.
"""
def insert_full_object(%{"object" => %{"type" => type} = object_data} = map)
- when is_map(object_data) and type in @supported_object_types do
+ when type in @supported_object_types do
with {:ok, object} <- Object.create(object_data) do
map = Map.put(map, "object", object.data["id"])
@@ -307,7 +308,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
"cc" => cc,
"context" => object.data["context"]
}
- |> maybe_put("id", activity_id)
+ |> Maps.put_if_present("id", activity_id)
end
def make_emoji_reaction_data(user, object, emoji, activity_id) do
@@ -444,22 +445,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
@@ -480,7 +478,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
"object" => followed_id,
"state" => "pending"
}
- |> maybe_put("id", activity_id)
+ |> Maps.put_if_present("id", activity_id)
end
def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do
@@ -519,7 +517,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
@@ -549,7 +547,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
"cc" => [],
"context" => object.data["context"]
}
- |> maybe_put("id", activity_id)
+ |> Maps.put_if_present("id", activity_id)
end
def make_announce_data(
@@ -566,46 +564,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
"cc" => [Pleroma.Constants.as_public()],
"context" => object.data["context"]
}
- |> 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)
+ |> Maps.put_if_present("id", activity_id)
end
def make_undo_data(
@@ -624,7 +583,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
"cc" => [Pleroma.Constants.as_public()],
"context" => context
}
- |> maybe_put("id", activity_id)
+ |> Maps.put_if_present("id", activity_id)
end
@spec add_announce_to_object(Activity.t(), Object.t()) ::
@@ -669,7 +628,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
"to" => [followed.ap_id],
"object" => follow_activity.data
}
- |> maybe_put("id", activity_id)
+ |> Maps.put_if_present("id", activity_id)
end
#### Block-related helpers
@@ -692,17 +651,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
"to" => [blocked.ap_id],
"object" => blocked.ap_id
}
- |> 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)
+ |> Maps.put_if_present("id", activity_id)
end
#### Create-related helpers
@@ -770,15 +719,18 @@ defmodule Pleroma.Web.ActivityPub.Utils do
case Activity.get_by_ap_id_with_object(id) do
%Activity{} = activity ->
+ activity_actor = User.get_by_ap_id(activity.object.data["actor"])
+
%{
"type" => "Note",
"id" => activity.data["id"],
"content" => activity.object.data["content"],
"published" => activity.object.data["published"],
"actor" =>
- AccountView.render("show.json", %{
- user: User.get_by_ap_id(activity.object.data["actor"])
- })
+ AccountView.render(
+ "show.json",
+ %{user: activity_actor, skip_visibility_check: true}
+ )
}
_ ->
@@ -792,112 +744,16 @@ defmodule Pleroma.Web.ActivityPub.Utils do
def get_reports(params, page, page_size) do
params =
params
- |> Map.put("type", "Flag")
- |> Map.put("skip_preload", true)
- |> Map.put("preload_report_notes", true)
- |> Map.put("total", true)
- |> Map.put("limit", page_size)
- |> Map.put("offset", (page - 1) * page_size)
+ |> Map.put(:type, "Flag")
+ |> Map.put(:skip_preload, true)
+ |> Map.put(:preload_report_notes, true)
+ |> Map.put(:total, true)
+ |> Map.put(:limit, page_size)
+ |> Map.put(:offset, (page - 1) * page_size)
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)
@@ -1018,7 +874,4 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data))
|> Repo.all()
end
-
- def maybe_put(map, _key, nil), do: map
- def maybe_put(map, key, value), do: Map.put(map, key, value)
end
diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex
index 3396777d7..3a4564912 100644
--- a/lib/pleroma/web/activity_pub/views/user_view.ex
+++ b/lib/pleroma/web/activity_pub/views/user_view.ex
@@ -79,10 +79,16 @@ defmodule Pleroma.Web.ActivityPub.UserView do
emoji_tags = Transmogrifier.take_emoji_tags(user)
- fields =
- user
- |> User.fields()
- |> Enum.map(&Map.put(&1, "type", "PropertyValue"))
+ fields = Enum.map(user.fields, &Map.put(&1, "type", "PropertyValue"))
+
+ capabilities =
+ if is_boolean(user.accepts_chat_messages) do
+ %{
+ "acceptsChatMessages" => user.accepts_chat_messages
+ }
+ else
+ %{}
+ end
%{
"id" => user.ap_id,
@@ -103,8 +109,9 @@ defmodule Pleroma.Web.ActivityPub.UserView do
},
"endpoints" => endpoints,
"attachment" => fields,
- "tag" => (user.source_data["tag"] || []) ++ emoji_tags,
- "discoverable" => user.discoverable
+ "tag" => emoji_tags,
+ "discoverable" => user.discoverable,
+ "capabilities" => capabilities
}
|> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))
|> Map.merge(maybe_make_image(&User.banner_url/2, "image", user))
diff --git a/lib/pleroma/web/activity_pub/visibility.ex b/lib/pleroma/web/activity_pub/visibility.ex
index 6f226fc92..5c349bb7a 100644
--- a/lib/pleroma/web/activity_pub/visibility.ex
+++ b/lib/pleroma/web/activity_pub/visibility.ex
@@ -44,8 +44,13 @@ 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?(nil, _), do: false
+
+ def visible_for_user?(%{data: %{"listMessage" => _}}, nil), do: false
+
def visible_for_user?(%{data: %{"listMessage" => list_ap_id}} = activity, %User{} = user) do
user.ap_id in activity.data["to"] ||
list_ap_id
@@ -53,16 +58,18 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
|> Pleroma.List.member?(user)
end
- def visible_for_user?(%{data: %{"listMessage" => _}}, nil), do: false
+ def visible_for_user?(%{local: local} = activity, nil) do
+ cfg_key = if local, do: :local, else: :remote
- def visible_for_user?(activity, nil) do
- is_public?(activity)
+ if Pleroma.Config.restrict_unauthenticated_access?(: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/controllers/admin_api_controller.ex
index e1869678e..aa2af1ab5 100644
--- a/lib/pleroma/web/admin_api/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
@@ -7,32 +7,24 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
- 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.Relay
- alias Pleroma.Web.ActivityPub.Utils
+ alias Pleroma.Web.ActivityPub.Builder
+ alias Pleroma.Web.ActivityPub.Pipeline
+ alias Pleroma.Web.AdminAPI
alias Pleroma.Web.AdminAPI.AccountView
- alias Pleroma.Web.AdminAPI.ConfigView
alias Pleroma.Web.AdminAPI.ModerationLogView
- alias Pleroma.Web.AdminAPI.Report
- alias Pleroma.Web.AdminAPI.ReportView
alias Pleroma.Web.AdminAPI.Search
- alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Endpoint
- alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.Router
require Logger
- @descriptions_json Pleroma.Docs.JSON.compile()
@users_page_size 50
plug(
@@ -46,88 +38,73 @@ 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,
:user_activate,
:user_deactivate,
+ :user_approve,
:tag_users,
:untag_users,
:right_add,
+ :right_add_multiple,
:right_delete,
+ :disable_mfa,
+ :right_delete_multiple,
:update_user_credentials
]
)
- plug(OAuthScopesPlug, %{scopes: ["read:invites"], admin: true} when action == :invites)
-
- plug(
- OAuthScopesPlug,
- %{scopes: ["write:invites"], admin: true}
- when action in [:create_invite_token, :revoke_invite, :email_invite]
- )
-
plug(
OAuthScopesPlug,
%{scopes: ["write:follows"], admin: true}
- when action in [:user_follow, :user_unfollow, :relay_follow, :relay_unfollow]
- )
-
- plug(
- OAuthScopesPlug,
- %{scopes: ["read:reports"], admin: true}
- when action in [:list_reports, :report_show]
- )
-
- plug(
- OAuthScopesPlug,
- %{scopes: ["write:reports"], admin: true}
- when action in [:reports_update]
+ when action in [:user_follow, :user_unfollow]
)
plug(
OAuthScopesPlug,
%{scopes: ["read:statuses"], admin: true}
- when action == :list_user_statuses
- )
-
- plug(
- OAuthScopesPlug,
- %{scopes: ["write:statuses"], admin: true}
- when action in [:status_update, :status_delete]
+ when action in [:list_user_statuses, :list_instance_statuses]
)
plug(
OAuthScopesPlug,
%{scopes: ["read"], admin: true}
- when action in [:config_show, :list_log, :stats]
+ when action in [
+ :list_log,
+ :stats,
+ :need_reboot
+ ]
)
plug(
OAuthScopesPlug,
%{scopes: ["write"], admin: true}
- when action == :config_update
+ when action in [
+ :restart,
+ :resend_confirmation_email,
+ :confirm_email,
+ :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"
- })
+ action_fallback(AdminAPI.FallbackController)
- conn
- |> json(nickname)
+ def user_delete(conn, %{"nickname" => nickname}) do
+ user_delete(conn, %{"nicknames" => [nickname]})
end
def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
- users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
- User.delete(users)
+ users =
+ nicknames
+ |> Enum.map(&User.get_cached_by_nickname/1)
+
+ users
+ |> Enum.each(fn user ->
+ {:ok, delete_data, _} = Builder.delete(admin, user.ap_id)
+ Pipeline.common_pipeline(delete_data, local: true)
+ end)
ModerationLog.insert_log(%{
actor: admin,
@@ -135,8 +112,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
action: "delete"
})
- conn
- |> json(nicknames)
+ json(conn, nicknames)
end
def user_follow(%{assigns: %{user: admin}} = conn, %{
@@ -155,8 +131,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
})
end
- conn
- |> json("ok")
+ json(conn, "ok")
end
def user_unfollow(%{assigns: %{user: admin}} = conn, %{
@@ -175,8 +150,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
})
end
- conn
- |> json("ok")
+ json(conn, "ok")
end
def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
@@ -215,8 +189,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
action: "create"
})
- conn
- |> json(res)
+ json(conn, res)
{:error, id, changeset, _} ->
res =
@@ -234,8 +207,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
end
end
- def user_show(conn, %{"nickname" => nickname}) do
- with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
+ def user_show(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
+ with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do
conn
|> put_view(AccountView)
|> render("show.json", %{user: user})
@@ -250,33 +223,33 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
activities =
ActivityPub.fetch_statuses(nil, %{
- "instance" => instance,
- "limit" => page_size,
- "offset" => (page - 1) * page_size,
- "exclude_reblogs" => !with_reblogs && "true"
+ instance: instance,
+ limit: page_size,
+ offset: (page - 1) * page_size,
+ exclude_reblogs: not with_reblogs
})
conn
- |> put_view(Pleroma.Web.AdminAPI.StatusView)
+ |> put_view(AdminAPI.StatusView)
|> render("index.json", %{activities: activities, as: :activity})
end
- def list_user_statuses(conn, %{"nickname" => nickname} = params) do
+ def list_user_statuses(%{assigns: %{user: admin}} = 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
+ with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do
{_, page_size} = page_params(params)
activities =
ActivityPub.fetch_user_activities(user, nil, %{
- "limit" => page_size,
- "godmode" => godmode,
- "exclude_reblogs" => !with_reblogs && "true"
+ limit: page_size,
+ godmode: godmode,
+ exclude_reblogs: not with_reblogs
})
conn
- |> put_view(StatusView)
+ |> put_view(AdminAPI.StatusView)
|> render("index.json", %{activities: activities, as: :activity})
else
_ -> {:error, :not_found}
@@ -331,6 +304,21 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|> render("index.json", %{users: Keyword.values(updated_users)})
end
+ def user_approve(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
+ users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
+ {:ok, updated_users} = User.approve(users)
+
+ ModerationLog.insert_log(%{
+ actor: admin,
+ subject: users,
+ action: "approve"
+ })
+
+ conn
+ |> put_view(AccountView)
+ |> render("index.json", %{users: updated_users})
+ end
+
def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
with {:ok, _} <- User.tag(nicknames, tags) do
ModerationLog.insert_log(%{
@@ -370,32 +358,19 @@ 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)
+ @filters ~w(local external active deactivated need_approval is_admin is_moderator)
@spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
@@ -404,8 +379,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
filters
|> String.split(",")
|> Enum.filter(&Enum.member?(@filters, &1))
- |> Enum.map(&String.to_atom(&1))
- |> Enum.into(%{}, &{&1, true})
+ |> Enum.map(&String.to_atom/1)
+ |> Map.new(&{&1, true})
end
def right_add_multiple(%{assigns: %{user: admin}} = conn, %{
@@ -530,108 +505,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
render_error(conn, :forbidden, "You can't revoke your own admin status.")
end
- def relay_list(conn, _params) do
- with {:ok, list} <- Relay.list() do
- json(conn, %{relays: list})
- else
- _ ->
- conn
- |> put_status(500)
- end
- end
-
- def relay_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
- with {:ok, _message} <- Relay.follow(target) do
- ModerationLog.insert_log(%{
- action: "relay_follow",
- actor: admin,
- target: target
- })
-
- json(conn, target)
- else
- _ ->
- conn
- |> put_status(500)
- |> json(target)
- end
- end
-
- def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
- with {:ok, _message} <- Relay.unfollow(target) do
- ModerationLog.insert_log(%{
- action: "relay_unfollow",
- actor: admin,
- target: target
- })
-
- json(conn, target)
- else
- _ ->
- conn
- |> put_status(500)
- |> json(target)
- end
- end
-
- @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]),
- {:ok, invite_token} <- UserInviteToken.create_invite(),
- email <-
- Pleroma.Emails.UserEmail.user_invitation_email(
- user,
- invite_token,
- email,
- params["name"]
- ),
- {:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do
- json_response(conn, :no_content, "")
- end
- end
-
- @doc "Create an account registration invite token"
- def create_invite_token(conn, params) do
- opts = %{}
-
- opts =
- if params["max_use"],
- do: Map.put(opts, :max_use, params["max_use"]),
- else: opts
-
- opts =
- if params["expires_at"],
- do: Map.put(opts, :expires_at, params["expires_at"]),
- else: opts
-
- {:ok, invite} = UserInviteToken.create_invite(opts)
-
- json(conn, AccountView.render("invite.json", %{invite: invite}))
- end
-
- @doc "Get list of created invites"
- def invites(conn, _params) do
- invites = UserInviteToken.list_invites()
-
- conn
- |> put_view(AccountView)
- |> render("invites.json", %{invites: invites})
- end
-
- @doc "Revokes invite by token"
- def revoke_invite(conn, %{"token" => token}) do
- with {:ok, invite} <- UserInviteToken.find_by_token(token),
- {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do
- conn
- |> put_view(AccountView)
- |> render("invite.json", %{invite: updated_invite})
- else
- nil -> {:error, :not_found}
- end
- end
-
@doc "Get a password reset token (base64 string) for given nickname"
def get_password_reset(conn, %{"nickname" => nickname}) do
(%User{local: true} = user) = User.get_cached_by_nickname(nickname)
@@ -659,9 +532,21 @@ 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
+ with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do
conn
|> put_view(AccountView)
|> render("credentials.json", %{user: user, for: admin})
@@ -675,7 +560,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
%{assigns: %{user: admin}} = conn,
%{"nickname" => nickname} = params
) do
- with {_, user} <- {:user, User.get_cached_by_nickname(nickname)},
+ with {_, %User{} = user} <- {:user, User.get_cached_by_nickname(nickname)},
{:ok, _user} <-
User.update_as_admin(user, params) do
ModerationLog.insert_log(%{
@@ -697,148 +582,12 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
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
+ errors = Map.new(changeset.errors, fn {key, {error, _}} -> {key, error} end)
- def list_reports(conn, params) do
- {page, page_size} = page_params(params)
-
- reports = Utils.get_reports(params, page, page_size)
-
- conn
- |> put_view(ReportView)
- |> render("index.json", %{reports: reports})
- end
-
- def 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
- |> put_view(ReportView)
- |> render("show.json", Report.extract_report_info(report))
- else
- _ -> {:error, :not_found}
- end
- end
-
- def reports_update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) do
- result =
- reports
- |> Enum.map(fn report ->
- with {:ok, activity} <- CommonAPI.update_report_state(report["id"], report["state"]) do
- ModerationLog.insert_log(%{
- action: "report_update",
- actor: admin,
- subject: activity
- })
-
- activity
- else
- {:error, message} -> %{id: report["id"], error: message}
- end
- end)
-
- case Enum.any?(result, &Map.has_key?(&1, :error)) do
- true -> json_response(conn, :bad_request, result)
- false -> json_response(conn, :no_content, "")
- end
- end
-
- def report_notes_create(%{assigns: %{user: user}} = conn, %{
- "id" => report_id,
- "content" => content
- }) do
- with {:ok, _} <- ReportNote.create(user.id, report_id, content) do
- ModerationLog.insert_log(%{
- action: "report_note",
- actor: user,
- subject: Activity.get_by_id(report_id),
- text: content
- })
-
- json_response(conn, :no_content, "")
- else
- _ -> json_response(conn, :bad_request, "")
- end
- end
-
- def report_notes_delete(%{assigns: %{user: user}} = conn, %{
- "id" => note_id,
- "report_id" => report_id
- }) do
- with {:ok, note} <- ReportNote.destroy(note_id) do
- ModerationLog.insert_log(%{
- action: "report_note_delete",
- actor: user,
- subject: Activity.get_by_id(report_id),
- text: note.content
- })
-
- json_response(conn, :no_content, "")
- else
- _ -> json_response(conn, :bad_request, "")
- end
- end
-
- def list_statuses(%{assigns: %{user: admin}} = conn, params) do
- godmode = params["godmode"] == "true" || params["godmode"] == true
- local_only = params["local_only"] == "true" || params["local_only"] == true
- with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
- {page, page_size} = page_params(params)
-
- activities =
- ActivityPub.fetch_statuses(admin, %{
- "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})
- end
-
- def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
- with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
- {:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"])
-
- ModerationLog.insert_log(%{
- action: "status_update",
- actor: admin,
- subject: activity,
- sensitive: sensitive,
- visibility: params["visibility"]
- })
-
- conn
- |> put_view(StatusView)
- |> render("show.json", %{activity: activity})
- end
- end
+ {:errors, errors}
- def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
- ModerationLog.insert_log(%{
- action: "status_delete",
- actor: user,
- subject_id: id
- })
-
- json(conn, %{})
+ _ ->
+ {:error, :not_found}
end
end
@@ -860,105 +609,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|> render("index.json", %{log: log})
end
- def config_descriptions(conn, _params) do
- conn
- |> Plug.Conn.put_resp_content_type("application/json")
- |> Plug.Conn.send_resp(200, @descriptions_json)
- end
-
- def config_show(conn, %{"only_db" => true}) do
- with :ok <- configurable_from_database(conn) do
- configs = Pleroma.Repo.all(ConfigDB)
-
- conn
- |> put_view(ConfigView)
- |> render("index.json", %{configs: configs})
- end
- end
-
- def config_show(conn, _params) do
- with :ok <- configurable_from_database(conn) do
- configs = ConfigDB.get_all_as_keyword()
-
- merged =
- Config.Holder.default_config()
- |> ConfigDB.merge(configs)
- |> Enum.map(fn {group, values} ->
- Enum.map(values, fn {key, value} ->
- db =
- if configs[group][key] do
- ConfigDB.get_db_keys(configs[group][key], key)
- end
-
- db_value = configs[group][key]
-
- merged_value =
- if !is_nil(db_value) and Keyword.keyword?(db_value) and
- ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do
- ConfigDB.merge_group(group, key, value, db_value)
- else
- value
- end
-
- setting = %{
- group: ConfigDB.convert(group),
- key: ConfigDB.convert(key),
- value: ConfigDB.convert(merged_value)
- }
-
- if db, do: Map.put(setting, :db, db), else: setting
- end)
- end)
- |> List.flatten()
-
- json(conn, %{configs: merged, need_reboot: Restarter.Pleroma.need_reboot?()})
- end
- end
-
- def config_update(conn, %{"configs" => configs}) do
- with :ok <- configurable_from_database(conn) do
- {_errors, results} =
- Enum.map(configs, fn
- %{"group" => group, "key" => key, "delete" => true} = params ->
- ConfigDB.delete(%{group: group, key: key, subkeys: params["subkeys"]})
-
- %{"group" => group, "key" => key, "value" => value} ->
- ConfigDB.update_or_create(%{group: group, key: key, value: value})
- end)
- |> Enum.split_with(fn result -> elem(result, 0) == :error end)
-
- {deleted, updated} =
- results
- |> Enum.map(fn {:ok, config} ->
- Map.put(config, :db, ConfigDB.get_db_keys(config))
- end)
- |> Enum.split_with(fn config ->
- Ecto.get_meta(config, :state) == :deleted
- end)
-
- Config.TransferTask.load_and_update_env(deleted, false)
-
- if !Restarter.Pleroma.need_reboot?() do
- changed_reboot_settings? =
- (updated ++ deleted)
- |> Enum.any?(fn config ->
- group = ConfigDB.from_string(config.group)
- key = ConfigDB.from_string(config.key)
- value = ConfigDB.from_binary(config.value)
- Config.TransferTask.pleroma_need_restart?(group, key, value)
- end)
-
- if changed_reboot_settings?, do: Restarter.Pleroma.need_reboot()
- end
-
- conn
- |> put_view(ConfigView)
- |> render("index.json", %{configs: updated, need_reboot: Restarter.Pleroma.need_reboot?()})
- end
- end
-
def restart(conn, _params) do
- with :ok <- configurable_from_database(conn) do
+ with :ok <- configurable_from_database() do
Restarter.Pleroma.restart(Config.get(:env), 50)
json(conn, %{})
@@ -969,80 +621,47 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
json(conn, %{need_reboot: Restarter.Pleroma.need_reboot?()})
end
- defp configurable_from_database(conn) do
+ defp configurable_from_database do
if Config.get(:configurable_from_database) do
:ok
else
- errors(
- conn,
- {:error, "To use this endpoint you need to enable configuration from database."}
- )
+ {:error, "To use this endpoint you need to enable configuration from database."}
end
end
def reload_emoji(conn, _params) do
Pleroma.Emoji.reload()
- conn |> json("ok")
+ json(conn, "ok")
end
def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
- users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
+ users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
User.toggle_confirmation(users)
- ModerationLog.insert_log(%{
- actor: admin,
- subject: users,
- action: "confirm_email"
- })
+ ModerationLog.insert_log(%{actor: admin, subject: users, action: "confirm_email"})
- conn |> json("")
+ json(conn, "")
end
def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
- users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
-
- User.try_send_confirmation_email(users)
-
- ModerationLog.insert_log(%{
- actor: admin,
- subject: users,
- action: "resend_confirmation_email"
- })
-
- conn |> json("")
- end
-
- def stats(conn, _) do
- count = Stats.get_status_visibility_count()
-
- conn
- |> json(%{"status_visibility" => count})
- end
+ users =
+ Enum.map(nicknames, fn nickname ->
+ nickname
+ |> User.get_cached_by_nickname()
+ |> User.send_confirmation_email()
+ end)
- def errors(conn, {:error, :not_found}) do
- conn
- |> put_status(:not_found)
- |> json(dgettext("errors", "Not found"))
- end
+ ModerationLog.insert_log(%{actor: admin, subject: users, action: "resend_confirmation_email"})
- def errors(conn, {:error, reason}) do
- conn
- |> put_status(:bad_request)
- |> json(reason)
+ json(conn, "")
end
- def errors(conn, {:param_cast, _}) do
- conn
- |> put_status(:bad_request)
- |> json(dgettext("errors", "Invalid parameters"))
- end
+ def stats(conn, params) do
+ counters = Stats.get_status_visibility_count(params["instance"])
- def errors(conn, _) do
- conn
- |> put_status(:internal_server_error)
- |> json(dgettext("errors", "Something went wrong"))
+ json(conn, %{"status_visibility" => counters})
end
defp page_params(params) do
diff --git a/lib/pleroma/web/admin_api/controllers/config_controller.ex b/lib/pleroma/web/admin_api/controllers/config_controller.ex
new file mode 100644
index 000000000..0df13007f
--- /dev/null
+++ b/lib/pleroma/web/admin_api/controllers/config_controller.ex
@@ -0,0 +1,150 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.AdminAPI.ConfigController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Config
+ alias Pleroma.ConfigDB
+ alias Pleroma.Plugs.OAuthScopesPlug
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(OAuthScopesPlug, %{scopes: ["write"], admin: true} when action == :update)
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["read"], admin: true}
+ when action in [:show, :descriptions]
+ )
+
+ action_fallback(Pleroma.Web.AdminAPI.FallbackController)
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.ConfigOperation
+
+ def descriptions(conn, _params) do
+ descriptions = Enum.filter(Pleroma.Docs.JSON.compiled_descriptions(), &whitelisted_config?/1)
+
+ json(conn, descriptions)
+ end
+
+ def show(conn, %{only_db: true}) do
+ with :ok <- configurable_from_database() do
+ configs = Pleroma.Repo.all(ConfigDB)
+
+ render(conn, "index.json", %{
+ configs: configs,
+ need_reboot: Restarter.Pleroma.need_reboot?()
+ })
+ end
+ end
+
+ def show(conn, _params) do
+ with :ok <- configurable_from_database() do
+ configs = ConfigDB.get_all_as_keyword()
+
+ merged =
+ Config.Holder.default_config()
+ |> ConfigDB.merge(configs)
+ |> Enum.map(fn {group, values} ->
+ Enum.map(values, fn {key, value} ->
+ db =
+ if configs[group][key] do
+ ConfigDB.get_db_keys(configs[group][key], key)
+ end
+
+ db_value = configs[group][key]
+
+ merged_value =
+ if not is_nil(db_value) and Keyword.keyword?(db_value) and
+ ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do
+ ConfigDB.merge_group(group, key, value, db_value)
+ else
+ value
+ end
+
+ %ConfigDB{
+ group: group,
+ key: key,
+ value: merged_value
+ }
+ |> Pleroma.Maps.put_if_present(:db, db)
+ end)
+ end)
+ |> List.flatten()
+
+ render(conn, "index.json", %{
+ configs: merged,
+ need_reboot: Restarter.Pleroma.need_reboot?()
+ })
+ end
+ end
+
+ def update(%{body_params: %{configs: configs}} = conn, _) do
+ with :ok <- configurable_from_database() do
+ results =
+ configs
+ |> Enum.filter(&whitelisted_config?/1)
+ |> Enum.map(fn
+ %{group: group, key: key, delete: true} = params ->
+ ConfigDB.delete(%{group: group, key: key, subkeys: params[:subkeys]})
+
+ %{group: group, key: key, value: value} ->
+ ConfigDB.update_or_create(%{group: group, key: key, value: value})
+ end)
+ |> Enum.reject(fn {result, _} -> result == :error end)
+
+ {deleted, updated} =
+ results
+ |> Enum.map(fn {:ok, %{key: key, value: value} = config} ->
+ Map.put(config, :db, ConfigDB.get_db_keys(value, key))
+ end)
+ |> Enum.split_with(&(Ecto.get_meta(&1, :state) == :deleted))
+
+ Config.TransferTask.load_and_update_env(deleted, false)
+
+ if not Restarter.Pleroma.need_reboot?() do
+ changed_reboot_settings? =
+ (updated ++ deleted)
+ |> Enum.any?(&Config.TransferTask.pleroma_need_restart?(&1.group, &1.key, &1.value))
+
+ if changed_reboot_settings?, do: Restarter.Pleroma.need_reboot()
+ end
+
+ render(conn, "index.json", %{
+ configs: updated,
+ need_reboot: Restarter.Pleroma.need_reboot?()
+ })
+ end
+ end
+
+ defp configurable_from_database do
+ if Config.get(:configurable_from_database) do
+ :ok
+ else
+ {:error, "To use this endpoint you need to enable configuration from database."}
+ end
+ end
+
+ defp whitelisted_config?(group, key) do
+ if whitelisted_configs = Config.get(:database_config_whitelist) do
+ Enum.any?(whitelisted_configs, fn
+ {whitelisted_group} ->
+ group == inspect(whitelisted_group)
+
+ {whitelisted_group, whitelisted_key} ->
+ group == inspect(whitelisted_group) && key == inspect(whitelisted_key)
+ end)
+ else
+ true
+ end
+ end
+
+ defp whitelisted_config?(%{group: group, key: key}) do
+ whitelisted_config?(group, key)
+ end
+
+ defp whitelisted_config?(%{group: group} = config) do
+ whitelisted_config?(group, config[:key])
+ end
+end
diff --git a/lib/pleroma/web/admin_api/controllers/fallback_controller.ex b/lib/pleroma/web/admin_api/controllers/fallback_controller.ex
new file mode 100644
index 000000000..34d90db07
--- /dev/null
+++ b/lib/pleroma/web/admin_api/controllers/fallback_controller.ex
@@ -0,0 +1,37 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.AdminAPI.FallbackController do
+ use Pleroma.Web, :controller
+
+ def call(conn, {:error, :not_found}) do
+ conn
+ |> put_status(:not_found)
+ |> json(%{error: dgettext("errors", "Not found")})
+ end
+
+ def call(conn, {:error, reason}) do
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: reason})
+ end
+
+ def call(conn, {:errors, errors}) do
+ conn
+ |> put_status(:bad_request)
+ |> json(%{errors: errors})
+ end
+
+ def call(conn, {:param_cast, _}) do
+ conn
+ |> put_status(:bad_request)
+ |> json(dgettext("errors", "Invalid parameters"))
+ end
+
+ def call(conn, _) do
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{error: dgettext("errors", "Something went wrong")})
+ end
+end
diff --git a/lib/pleroma/web/admin_api/controllers/invite_controller.ex b/lib/pleroma/web/admin_api/controllers/invite_controller.ex
new file mode 100644
index 000000000..7d169b8d2
--- /dev/null
+++ b/lib/pleroma/web/admin_api/controllers/invite_controller.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.AdminAPI.InviteController do
+ use Pleroma.Web, :controller
+
+ import Pleroma.Web.ControllerHelper, only: [json_response: 3]
+
+ alias Pleroma.Config
+ alias Pleroma.Plugs.OAuthScopesPlug
+ alias Pleroma.UserInviteToken
+
+ require Logger
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(OAuthScopesPlug, %{scopes: ["read:invites"], admin: true} when action == :index)
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["write:invites"], admin: true} when action in [:create, :revoke, :email]
+ )
+
+ action_fallback(Pleroma.Web.AdminAPI.FallbackController)
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.InviteOperation
+
+ @doc "Get list of created invites"
+ def index(conn, _params) do
+ invites = UserInviteToken.list_invites()
+
+ render(conn, "index.json", invites: invites)
+ end
+
+ @doc "Create an account registration invite token"
+ def create(%{body_params: params} = conn, _) do
+ {:ok, invite} = UserInviteToken.create_invite(params)
+
+ render(conn, "show.json", invite: invite)
+ end
+
+ @doc "Revokes invite by token"
+ def revoke(%{body_params: %{token: token}} = conn, _) do
+ with {:ok, invite} <- UserInviteToken.find_by_token(token),
+ {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do
+ render(conn, "show.json", invite: updated_invite)
+ else
+ nil -> {:error, :not_found}
+ error -> error
+ end
+ end
+
+ @doc "Sends registration invite via email"
+ def email(%{assigns: %{user: user}, body_params: %{email: email} = params} = conn, _) do
+ with {_, false} <- {:registrations_open, Config.get([:instance, :registrations_open])},
+ {_, true} <- {:invites_enabled, Config.get([:instance, :invites_enabled])},
+ {:ok, invite_token} <- UserInviteToken.create_invite(),
+ {:ok, _} <-
+ user
+ |> Pleroma.Emails.UserEmail.user_invitation_email(
+ invite_token,
+ email,
+ params[:name]
+ )
+ |> Pleroma.Emails.Mailer.deliver() do
+ json_response(conn, :no_content, "")
+ else
+ {:registrations_open, _} ->
+ {:error, "To send invites you need to set the `registrations_open` option to false."}
+
+ {:invites_enabled, _} ->
+ {:error, "To send invites you need to set the `invites_enabled` option to true."}
+
+ {:error, error} ->
+ {:error, error}
+ end
+ end
+end
diff --git a/lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex b/lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex
new file mode 100644
index 000000000..131e22d78
--- /dev/null
+++ b/lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex
@@ -0,0 +1,74 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.AdminAPI.MediaProxyCacheController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Plugs.OAuthScopesPlug
+ alias Pleroma.Web.ApiSpec.Admin, as: Spec
+ alias Pleroma.Web.MediaProxy
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["read:media_proxy_caches"], admin: true} when action in [:index]
+ )
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["write:media_proxy_caches"], admin: true} when action in [:purge, :delete]
+ )
+
+ action_fallback(Pleroma.Web.AdminAPI.FallbackController)
+
+ defdelegate open_api_operation(action), to: Spec.MediaProxyCacheOperation
+
+ def index(%{assigns: %{user: _}} = conn, params) do
+ entries = fetch_entries(params)
+ urls = paginate_entries(entries, params.page, params.page_size)
+
+ render(conn, "index.json",
+ urls: urls,
+ page_size: params.page_size,
+ count: length(entries)
+ )
+ end
+
+ defp fetch_entries(params) do
+ MediaProxy.cache_table()
+ |> Cachex.stream!(Cachex.Query.create(true, :key))
+ |> filter_entries(params[:query])
+ end
+
+ defp filter_entries(stream, query) when is_binary(query) do
+ regex = ~r/#{query}/i
+
+ stream
+ |> Enum.filter(fn url -> String.match?(url, regex) end)
+ |> Enum.to_list()
+ end
+
+ defp filter_entries(stream, _), do: Enum.to_list(stream)
+
+ defp paginate_entries(entries, page, page_size) do
+ offset = page_size * (page - 1)
+ Enum.slice(entries, offset, page_size)
+ end
+
+ def delete(%{assigns: %{user: _}, body_params: %{urls: urls}} = conn, _) do
+ MediaProxy.remove_from_banned_urls(urls)
+ json(conn, %{})
+ end
+
+ def purge(%{assigns: %{user: _}, body_params: %{urls: urls, ban: ban}} = conn, _) do
+ MediaProxy.Invalidation.purge(urls)
+
+ if ban do
+ MediaProxy.put_in_banned_urls(urls)
+ end
+
+ json(conn, %{})
+ end
+end
diff --git a/lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex b/lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex
new file mode 100644
index 000000000..dca23ea73
--- /dev/null
+++ b/lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex
@@ -0,0 +1,77 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.AdminAPI.OAuthAppController do
+ use Pleroma.Web, :controller
+
+ import Pleroma.Web.ControllerHelper, only: [json_response: 3]
+
+ alias Pleroma.Plugs.OAuthScopesPlug
+ alias Pleroma.Web.OAuth.App
+
+ require Logger
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(:put_view, Pleroma.Web.MastodonAPI.AppView)
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["write"], admin: true}
+ when action in [:create, :index, :update, :delete]
+ )
+
+ action_fallback(Pleroma.Web.AdminAPI.FallbackController)
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.OAuthAppOperation
+
+ def index(conn, params) do
+ search_params =
+ params
+ |> Map.take([:client_id, :page, :page_size, :trusted])
+ |> Map.put(:client_name, params[:name])
+
+ with {:ok, apps, count} <- App.search(search_params) do
+ render(conn, "index.json",
+ apps: apps,
+ count: count,
+ page_size: params.page_size,
+ admin: true
+ )
+ end
+ end
+
+ def create(%{body_params: params} = conn, _) do
+ params = Pleroma.Maps.put_if_present(params, :client_name, params[:name])
+
+ case App.create(params) do
+ {:ok, app} ->
+ render(conn, "show.json", app: app, admin: true)
+
+ {:error, changeset} ->
+ json(conn, App.errors(changeset))
+ end
+ end
+
+ def update(%{body_params: params} = conn, %{id: id}) do
+ params = Pleroma.Maps.put_if_present(params, :client_name, params[:name])
+
+ with {:ok, app} <- App.update(id, params) do
+ render(conn, "show.json", app: app, admin: true)
+ else
+ {:error, changeset} ->
+ json(conn, App.errors(changeset))
+
+ nil ->
+ json_response(conn, :bad_request, "")
+ end
+ end
+
+ def delete(conn, params) do
+ with {:ok, _app} <- App.destroy(params.id) do
+ json_response(conn, :no_content, "")
+ else
+ _ -> json_response(conn, :bad_request, "")
+ end
+ end
+end
diff --git a/lib/pleroma/web/admin_api/controllers/relay_controller.ex b/lib/pleroma/web/admin_api/controllers/relay_controller.ex
new file mode 100644
index 000000000..95d06dde7
--- /dev/null
+++ b/lib/pleroma/web/admin_api/controllers/relay_controller.ex
@@ -0,0 +1,67 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.AdminAPI.RelayController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.ModerationLog
+ alias Pleroma.Plugs.OAuthScopesPlug
+ alias Pleroma.Web.ActivityPub.Relay
+
+ require Logger
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["write:follows"], admin: true}
+ when action in [:follow, :unfollow]
+ )
+
+ plug(OAuthScopesPlug, %{scopes: ["read"], admin: true} when action == :index)
+
+ action_fallback(Pleroma.Web.AdminAPI.FallbackController)
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.RelayOperation
+
+ def index(conn, _params) do
+ with {:ok, list} <- Relay.list() do
+ json(conn, %{relays: list})
+ end
+ end
+
+ def follow(%{assigns: %{user: admin}, body_params: %{relay_url: target}} = conn, _) do
+ with {:ok, _message} <- Relay.follow(target) do
+ ModerationLog.insert_log(%{
+ action: "relay_follow",
+ actor: admin,
+ target: target
+ })
+
+ json(conn, %{actor: target, followed_back: target in Relay.following()})
+ else
+ _ ->
+ conn
+ |> put_status(500)
+ |> json(target)
+ end
+ end
+
+ def unfollow(%{assigns: %{user: admin}, body_params: %{relay_url: target}} = conn, _) do
+ with {:ok, _message} <- Relay.unfollow(target) do
+ ModerationLog.insert_log(%{
+ action: "relay_unfollow",
+ actor: admin,
+ target: target
+ })
+
+ json(conn, target)
+ else
+ _ ->
+ conn
+ |> put_status(500)
+ |> json(target)
+ end
+ end
+end
diff --git a/lib/pleroma/web/admin_api/controllers/report_controller.ex b/lib/pleroma/web/admin_api/controllers/report_controller.ex
new file mode 100644
index 000000000..4c011e174
--- /dev/null
+++ b/lib/pleroma/web/admin_api/controllers/report_controller.ex
@@ -0,0 +1,107 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.AdminAPI.ReportController do
+ use Pleroma.Web, :controller
+
+ import Pleroma.Web.ControllerHelper, only: [json_response: 3]
+
+ alias Pleroma.Activity
+ alias Pleroma.ModerationLog
+ alias Pleroma.Plugs.OAuthScopesPlug
+ alias Pleroma.ReportNote
+ alias Pleroma.Web.ActivityPub.Utils
+ alias Pleroma.Web.AdminAPI
+ alias Pleroma.Web.AdminAPI.Report
+ alias Pleroma.Web.CommonAPI
+
+ require Logger
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(OAuthScopesPlug, %{scopes: ["read:reports"], admin: true} when action in [:index, :show])
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["write:reports"], admin: true}
+ when action in [:update, :notes_create, :notes_delete]
+ )
+
+ action_fallback(AdminAPI.FallbackController)
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.ReportOperation
+
+ def index(conn, params) do
+ reports = Utils.get_reports(params, params.page, params.page_size)
+
+ render(conn, "index.json", reports: reports)
+ end
+
+ def show(conn, %{id: id}) do
+ with %Activity{} = report <- Activity.get_by_id(id) do
+ render(conn, "show.json", Report.extract_report_info(report))
+ else
+ _ -> {:error, :not_found}
+ end
+ end
+
+ def update(%{assigns: %{user: admin}, body_params: %{reports: reports}} = conn, _) do
+ result =
+ Enum.map(reports, fn report ->
+ case CommonAPI.update_report_state(report.id, report.state) do
+ {:ok, activity} ->
+ ModerationLog.insert_log(%{
+ action: "report_update",
+ actor: admin,
+ subject: activity
+ })
+
+ activity
+
+ {:error, message} ->
+ %{id: report.id, error: message}
+ end
+ end)
+
+ if Enum.any?(result, &Map.has_key?(&1, :error)) do
+ json_response(conn, :bad_request, result)
+ else
+ json_response(conn, :no_content, "")
+ end
+ end
+
+ def notes_create(%{assigns: %{user: user}, body_params: %{content: content}} = conn, %{
+ id: report_id
+ }) do
+ with {:ok, _} <- ReportNote.create(user.id, report_id, content) do
+ ModerationLog.insert_log(%{
+ action: "report_note",
+ actor: user,
+ subject: Activity.get_by_id(report_id),
+ text: content
+ })
+
+ json_response(conn, :no_content, "")
+ else
+ _ -> json_response(conn, :bad_request, "")
+ end
+ end
+
+ def notes_delete(%{assigns: %{user: user}} = conn, %{
+ id: note_id,
+ report_id: report_id
+ }) do
+ with {:ok, note} <- ReportNote.destroy(note_id) do
+ ModerationLog.insert_log(%{
+ action: "report_note_delete",
+ actor: user,
+ subject: Activity.get_by_id(report_id),
+ text: note.content
+ })
+
+ json_response(conn, :no_content, "")
+ else
+ _ -> json_response(conn, :bad_request, "")
+ end
+ end
+end
diff --git a/lib/pleroma/web/admin_api/controllers/status_controller.ex b/lib/pleroma/web/admin_api/controllers/status_controller.ex
new file mode 100644
index 000000000..bc48cc527
--- /dev/null
+++ b/lib/pleroma/web/admin_api/controllers/status_controller.ex
@@ -0,0 +1,77 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.AdminAPI.StatusController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Activity
+ alias Pleroma.ModerationLog
+ alias Pleroma.Plugs.OAuthScopesPlug
+ alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.MastodonAPI
+
+ require Logger
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(OAuthScopesPlug, %{scopes: ["read:statuses"], admin: true} when action in [:index, :show])
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["write:statuses"], admin: true} when action in [:update, :delete]
+ )
+
+ action_fallback(Pleroma.Web.AdminAPI.FallbackController)
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.StatusOperation
+
+ def index(%{assigns: %{user: _admin}} = conn, params) do
+ activities =
+ ActivityPub.fetch_statuses(nil, %{
+ godmode: params.godmode,
+ local_only: params.local_only,
+ limit: params.page_size,
+ offset: (params.page - 1) * params.page_size,
+ exclude_reblogs: not params.with_reblogs
+ })
+
+ render(conn, "index.json", activities: activities, as: :activity)
+ end
+
+ def show(conn, %{id: id}) do
+ with %Activity{} = activity <- Activity.get_by_id(id) do
+ render(conn, "show.json", %{activity: activity})
+ else
+ nil -> {:error, :not_found}
+ end
+ end
+
+ def update(%{assigns: %{user: admin}, body_params: params} = conn, %{id: id}) do
+ with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
+ ModerationLog.insert_log(%{
+ action: "status_update",
+ actor: admin,
+ subject: activity,
+ sensitive: params[:sensitive],
+ visibility: params[:visibility]
+ })
+
+ conn
+ |> put_view(MastodonAPI.StatusView)
+ |> render("show.json", %{activity: activity})
+ end
+ end
+
+ def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
+ with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
+ ModerationLog.insert_log(%{
+ action: "status_delete",
+ actor: user,
+ subject_id: id
+ })
+
+ json(conn, %{})
+ end
+ end
+end
diff --git a/lib/pleroma/web/admin_api/search.ex b/lib/pleroma/web/admin_api/search.ex
index 29cea1f44..0bfb8f022 100644
--- a/lib/pleroma/web/admin_api/search.ex
+++ b/lib/pleroma/web/admin_api/search.ex
@@ -21,6 +21,7 @@ defmodule Pleroma.Web.AdminAPI.Search do
query =
params
|> Map.drop([:page, :page_size])
+ |> Map.put(:invisible, false)
|> User.Query.build()
|> order_by([u], u.nickname)
@@ -30,7 +31,6 @@ defmodule Pleroma.Web.AdminAPI.Search do
count = Repo.aggregate(query, :count, :id)
results = Repo.all(paginated_query)
-
{:ok, results, count}
end
end
diff --git a/lib/pleroma/web/admin_api/views/account_view.ex b/lib/pleroma/web/admin_api/views/account_view.ex
index a16a3ebf0..9c477feab 100644
--- a/lib/pleroma/web/admin_api/views/account_view.ex
+++ b/lib/pleroma/web/admin_api/views/account_view.ex
@@ -6,7 +6,9 @@ defmodule Pleroma.Web.AdminAPI.AccountView do
use Pleroma.Web, :view
alias Pleroma.User
+ alias Pleroma.Web.AdminAPI
alias Pleroma.Web.AdminAPI.AccountView
+ alias Pleroma.Web.MastodonAPI
alias Pleroma.Web.MediaProxy
def render("index.json", %{users: users, count: count, page_size: page_size}) do
@@ -74,25 +76,11 @@ defmodule Pleroma.Web.AdminAPI.AccountView do
"local" => user.local,
"roles" => User.roles(user),
"tags" => user.tags || [],
- "confirmation_pending" => user.confirmation_pending
- }
- end
-
- def render("invite.json", %{invite: invite}) do
- %{
- "id" => invite.id,
- "token" => invite.token,
- "used" => invite.used,
- "expires_at" => invite.expires_at,
- "uses" => invite.uses,
- "max_use" => invite.max_use,
- "invite_type" => invite.invite_type
- }
- end
-
- def render("invites.json", %{invites: invites}) do
- %{
- invites: render_many(invites, AccountView, "invite.json", as: :invite)
+ "confirmation_pending" => user.confirmation_pending,
+ "approval_pending" => user.approval_pending,
+ "url" => user.uri || user.ap_id,
+ "registration_reason" => user.registration_reason,
+ "actor_type" => user.actor_type
}
end
@@ -119,6 +107,13 @@ defmodule Pleroma.Web.AdminAPI.AccountView do
}
end
+ def merge_account_views(%User{} = user) do
+ MastodonAPI.AccountView.render("show.json", %{user: user, skip_visibility_check: true})
+ |> Map.merge(AdminAPI.AccountView.render("show.json", %{user: user}))
+ end
+
+ def merge_account_views(_), do: %{}
+
defp parse_error([]), do: ""
defp parse_error(errors) do
diff --git a/lib/pleroma/web/admin_api/views/config_view.ex b/lib/pleroma/web/admin_api/views/config_view.ex
index 587ef760e..d2d8b5907 100644
--- a/lib/pleroma/web/admin_api/views/config_view.ex
+++ b/lib/pleroma/web/admin_api/views/config_view.ex
@@ -5,23 +5,20 @@
defmodule Pleroma.Web.AdminAPI.ConfigView do
use Pleroma.Web, :view
+ alias Pleroma.ConfigDB
+
def render("index.json", %{configs: configs} = params) do
- map = %{
- configs: render_many(configs, __MODULE__, "show.json", as: :config)
+ %{
+ configs: render_many(configs, __MODULE__, "show.json", as: :config),
+ need_reboot: params[:need_reboot]
}
-
- if params[:need_reboot] do
- Map.put(map, :need_reboot, true)
- else
- map
- end
end
def render("show.json", %{config: config}) do
map = %{
- key: config.key,
- group: config.group,
- value: Pleroma.ConfigDB.from_binary_with_convert(config.value)
+ key: ConfigDB.to_json_types(config.key),
+ group: ConfigDB.to_json_types(config.group),
+ value: ConfigDB.to_json_types(config.value)
}
if config.db != [] do
diff --git a/lib/pleroma/web/admin_api/views/invite_view.ex b/lib/pleroma/web/admin_api/views/invite_view.ex
new file mode 100644
index 000000000..f93cb6916
--- /dev/null
+++ b/lib/pleroma/web/admin_api/views/invite_view.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.AdminAPI.InviteView do
+ use Pleroma.Web, :view
+
+ def render("index.json", %{invites: invites}) do
+ %{
+ invites: render_many(invites, __MODULE__, "show.json", as: :invite)
+ }
+ end
+
+ def render("show.json", %{invite: invite}) do
+ %{
+ "id" => invite.id,
+ "token" => invite.token,
+ "used" => invite.used,
+ "expires_at" => invite.expires_at,
+ "uses" => invite.uses,
+ "max_use" => invite.max_use,
+ "invite_type" => invite.invite_type
+ }
+ end
+end
diff --git a/lib/pleroma/web/admin_api/views/media_proxy_cache_view.ex b/lib/pleroma/web/admin_api/views/media_proxy_cache_view.ex
new file mode 100644
index 000000000..a803bda0b
--- /dev/null
+++ b/lib/pleroma/web/admin_api/views/media_proxy_cache_view.ex
@@ -0,0 +1,15 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.AdminAPI.MediaProxyCacheView do
+ use Pleroma.Web, :view
+
+ def render("index.json", %{urls: urls, page_size: page_size, count: count}) do
+ %{
+ urls: urls,
+ count: count,
+ page_size: page_size
+ }
+ end
+end
diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex
index fc8733ce8..773f798fe 100644
--- a/lib/pleroma/web/admin_api/views/report_view.ex
+++ b/lib/pleroma/web/admin_api/views/report_view.ex
@@ -4,18 +4,21 @@
defmodule Pleroma.Web.AdminAPI.ReportView do
use Pleroma.Web, :view
- alias Pleroma.Activity
+
alias Pleroma.HTML
alias Pleroma.User
+ alias Pleroma.Web.AdminAPI
alias Pleroma.Web.AdminAPI.Report
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.StatusView
+ defdelegate merge_account_views(user), to: AdminAPI.AccountView
+
def render("index.json", %{reports: reports}) do
%{
reports:
reports[:items]
- |> Enum.map(&Report.extract_report_info(&1))
+ |> Enum.map(&Report.extract_report_info/1)
|> Enum.map(&render(__MODULE__, "show.json", &1))
|> Enum.reverse(),
total: reports[:total]
@@ -38,38 +41,16 @@ 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
+ }),
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
@@ -91,11 +72,4 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
created_at: Utils.to_masto_date(inserted_at)
}
end
-
- defp merge_account_views(%User{} = user) do
- Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user})
- |> Map.merge(Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user}))
- end
-
- defp merge_account_views(_), do: %{}
end
diff --git a/lib/pleroma/web/admin_api/views/status_view.ex b/lib/pleroma/web/admin_api/views/status_view.ex
index 360ddc22c..500800be2 100644
--- a/lib/pleroma/web/admin_api/views/status_view.ex
+++ b/lib/pleroma/web/admin_api/views/status_view.ex
@@ -7,36 +7,19 @@ defmodule Pleroma.Web.AdminAPI.StatusView do
require Pleroma.Constants
- alias Pleroma.User
+ alias Pleroma.Web.AdminAPI
+ alias Pleroma.Web.MastodonAPI
+
+ defdelegate merge_account_views(user), to: AdminAPI.AccountView
def render("index.json", opts) do
safe_render_many(opts.activities, __MODULE__, "show.json", opts)
end
def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
- user = get_user(activity.data["actor"])
+ user = MastodonAPI.StatusView.get_user(activity.data["actor"])
- Pleroma.Web.MastodonAPI.StatusView.render("show.json", opts)
+ MastodonAPI.StatusView.render("show.json", opts)
|> Map.merge(%{account: merge_account_views(user)})
end
-
- defp merge_account_views(%User{} = user) do
- Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user})
- |> Map.merge(Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user}))
- end
-
- defp merge_account_views(_), do: %{}
-
- 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..fbfc27d6f
--- /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()
+
+ _ ->
+ "application/json"
+ 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..2a7f1a706
--- /dev/null
+++ b/lib/pleroma/web/api_spec/helpers.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.ApiSpec.Helpers do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
+
+ def request_body(description, schema_ref, opts \\ []) do
+ media_types = ["application/json", "multipart/form-data", "application/x-www-form-urlencoded"]
+
+ 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 admin_api_params do
+ [Operation.parameter(:admin_token, :query, :string, "Allows authorization via admin token.")]
+ 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(
+ :offset,
+ :query,
+ %Schema{type: :integer, default: 0},
+ "Return items past this number of items"
+ ),
+ 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 with_relationships_param do
+ Operation.parameter(
+ :with_relationships,
+ :query,
+ BooleanLike,
+ "Embed relationships into accounts."
+ )
+ end
+
+ def empty_object_response do
+ Operation.response("Empty object", "application/json", %Schema{type: :object, example: %{}})
+ end
+
+ 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..aaebc9b5c
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/account_operation.ex
@@ -0,0 +1,766 @@
+# 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_credentials_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),
+ 401 => Operation.response("Error", "application/json", ApiError),
+ 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()),
+ 401 => Operation.response("Error", "application/json", ApiError),
+ 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"},
+ Operation.parameter(:id, :query, :string, "ID of the resource owner"),
+ with_relationships_param() | 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"},
+ Operation.parameter(:id, :query, :string, "ID of the resource owner"),
+ with_relationships_param() | 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"}
+ ],
+ requestBody:
+ request_body(
+ "Parameters",
+ %Schema{
+ type: :object,
+ properties: %{
+ reblogs: %Schema{
+ type: :boolean,
+ description: "Receive this account's reblogs in home timeline? Defaults to true.",
+ default: true
+ }
+ }
+ },
+ required: false
+ ),
+ 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,
+ required: [:username, :password, :agreement],
+ properties: %{
+ reason: %Schema{
+ type: :string,
+ nullable: true,
+ description:
+ "Text that will be reviewed by moderators if registrations require manual approval"
+ },
+ username: %Schema{type: :string, description: "The desired username for the account"},
+ email: %Schema{
+ type: :string,
+ nullable: true,
+ description:
+ "The email address to be used for login. Required when `account_activation_required` is enabled.",
+ format: :email
+ },
+ password: %Schema{
+ type: :string,
+ description: "The password to be used for login",
+ format: :password
+ },
+ agreement: %Schema{
+ allOf: [BooleanLike],
+ description:
+ "Whether the user agrees to the local rules, terms, and policies. These should be presented to the user in order to allow them to consent before setting this parameter to TRUE."
+ },
+ locale: %Schema{
+ type: :string,
+ nullable: true,
+ description: "The language of the confirmation email that will be sent"
+ },
+ # Pleroma-specific properties:
+ fullname: %Schema{type: :string, nullable: true, description: "Full name"},
+ bio: %Schema{type: :string, description: "Bio", nullable: true, default: ""},
+ captcha_solution: %Schema{
+ type: :string,
+ nullable: true,
+ description: "Provider-specific captcha solution"
+ },
+ captcha_token: %Schema{
+ type: :string,
+ nullable: true,
+ description: "Provider-specific captcha token"
+ },
+ captcha_answer_data: %Schema{
+ type: :string,
+ nullable: true,
+ description: "Provider-specific captcha data"
+ },
+ token: %Schema{
+ type: :string,
+ nullable: true,
+ description: "Invite token required when the registrations aren't public"
+ }
+ },
+ example: %{
+ "username" => "cofe",
+ "email" => "cofe@example.com",
+ "password" => "secret",
+ "agreement" => "true",
+ "bio" => "☕️"
+ }
+ }
+ end
+
+ # Note: this is a token response (if login succeeds!), but there's no oauth operation file yet.
+ defp create_response do
+ %Schema{
+ title: "AccountCreateResponse",
+ description: "Response schema for an account",
+ type: :object,
+ properties: %{
+ # The response when auto-login on create succeeds (token is issued):
+ token_type: %Schema{type: :string},
+ access_token: %Schema{type: :string},
+ refresh_token: %Schema{type: :string},
+ scope: %Schema{type: :string},
+ created_at: %Schema{type: :integer, format: :"date-time"},
+ me: %Schema{type: :string},
+ expires_in: %Schema{type: :integer},
+ #
+ # The response when registration succeeds but auto-login fails (no token):
+ identifier: %Schema{type: :string},
+ message: %Schema{type: :string}
+ },
+ required: [],
+ # Note: example of successful registration with failed login response:
+ # example: %{
+ # "identifier" => "missing_confirmed_email",
+ # "message" => "You have been registered. Please check your email for further instructions."
+ # },
+ example: %{
+ "token_type" => "Bearer",
+ "access_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzk",
+ "refresh_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzz",
+ "created_at" => 1_585_918_714,
+ "expires_in" => 600,
+ "scope" => "read write follow push",
+ "me" => "https://gensokyo.2hu/users/raymoo"
+ }
+ }
+ end
+
+ defp update_credentials_request do
+ %Schema{
+ title: "AccountUpdateCredentialsRequest",
+ description: "POST body for creating an account",
+ type: :object,
+ properties: %{
+ bot: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Whether the account has a bot flag."
+ },
+ display_name: %Schema{
+ type: :string,
+ nullable: true,
+ description: "The display name to use for the profile."
+ },
+ note: %Schema{type: :string, description: "The account bio."},
+ avatar: %Schema{
+ type: :string,
+ nullable: true,
+ description: "Avatar image encoded using multipart/form-data",
+ format: :binary
+ },
+ header: %Schema{
+ type: :string,
+ nullable: true,
+ description: "Header image encoded using multipart/form-data",
+ format: :binary
+ },
+ locked: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Whether manual approval of follow requests is required."
+ },
+ accepts_chat_messages: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Whether the user accepts receiving chat messages."
+ },
+ fields_attributes: %Schema{
+ nullable: true,
+ 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{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "html tags are stripped from all statuses requested from the API"
+ },
+ hide_followers: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "user's followers will be hidden"
+ },
+ hide_follows: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "user's follows will be hidden"
+ },
+ hide_followers_count: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "user's follower count will be hidden"
+ },
+ hide_follows_count: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "user's follow count will be hidden"
+ },
+ hide_favorites: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "user's favorites timeline will be hidden"
+ },
+ show_role: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "user's role (e.g admin, moderator) will be exposed to anyone in the
+ API"
+ },
+ default_scope: VisibilityScope,
+ pleroma_settings_store: %Schema{
+ type: :object,
+ nullable: true,
+ description: "Opaque user settings to be saved on the backend."
+ },
+ skip_thread_containment: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Skip filtering out broken threads"
+ },
+ allow_following_move: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Allows automatically follow moved following accounts"
+ },
+ pleroma_background_image: %Schema{
+ type: :string,
+ nullable: true,
+ description: "Sets the background image of the user.",
+ format: :binary
+ },
+ discoverable: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ 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, nullable: true, format: :uri}
+ },
+ required: [:uri]
+ }
+ end
+
+ defp mute_request do
+ %Schema{
+ title: "AccountMuteRequest",
+ description: "POST body for muting an account",
+ type: :object,
+ properties: %{
+ notifications: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ 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/admin/config_operation.ex b/lib/pleroma/web/api_spec/operations/admin/config_operation.ex
new file mode 100644
index 000000000..3a8380797
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/admin/config_operation.ex
@@ -0,0 +1,145 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Admin.ConfigOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+
+ import Pleroma.Web.ApiSpec.Helpers
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def show_operation do
+ %Operation{
+ tags: ["Admin", "Config"],
+ summary: "Get list of merged default settings with saved in database",
+ operationId: "AdminAPI.ConfigController.show",
+ parameters: [
+ Operation.parameter(
+ :only_db,
+ :query,
+ %Schema{type: :boolean, default: false},
+ "Get only saved in database settings"
+ )
+ | admin_api_params()
+ ],
+ security: [%{"oAuth" => ["read"]}],
+ responses: %{
+ 200 => Operation.response("Config", "application/json", config_response()),
+ 400 => Operation.response("Bad Request", "application/json", ApiError)
+ }
+ }
+ end
+
+ def update_operation do
+ %Operation{
+ tags: ["Admin", "Config"],
+ summary: "Update config settings",
+ operationId: "AdminAPI.ConfigController.update",
+ security: [%{"oAuth" => ["write"]}],
+ parameters: admin_api_params(),
+ requestBody:
+ request_body("Parameters", %Schema{
+ type: :object,
+ properties: %{
+ configs: %Schema{
+ type: :array,
+ items: %Schema{
+ type: :object,
+ properties: %{
+ group: %Schema{type: :string},
+ key: %Schema{type: :string},
+ value: any(),
+ delete: %Schema{type: :boolean},
+ subkeys: %Schema{type: :array, items: %Schema{type: :string}}
+ }
+ }
+ }
+ }
+ }),
+ responses: %{
+ 200 => Operation.response("Config", "application/json", config_response()),
+ 400 => Operation.response("Bad Request", "application/json", ApiError)
+ }
+ }
+ end
+
+ def descriptions_operation do
+ %Operation{
+ tags: ["Admin", "Config"],
+ summary: "Get JSON with config descriptions.",
+ operationId: "AdminAPI.ConfigController.descriptions",
+ security: [%{"oAuth" => ["read"]}],
+ parameters: admin_api_params(),
+ responses: %{
+ 200 =>
+ Operation.response("Config Descriptions", "application/json", %Schema{
+ type: :array,
+ items: %Schema{
+ type: :object,
+ properties: %{
+ group: %Schema{type: :string},
+ key: %Schema{type: :string},
+ type: %Schema{oneOf: [%Schema{type: :string}, %Schema{type: :array}]},
+ description: %Schema{type: :string},
+ children: %Schema{
+ type: :array,
+ items: %Schema{
+ type: :object,
+ properties: %{
+ key: %Schema{type: :string},
+ type: %Schema{oneOf: [%Schema{type: :string}, %Schema{type: :array}]},
+ description: %Schema{type: :string},
+ suggestions: %Schema{type: :array}
+ }
+ }
+ }
+ }
+ }
+ }),
+ 400 => Operation.response("Bad Request", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp any do
+ %Schema{
+ oneOf: [
+ %Schema{type: :array},
+ %Schema{type: :object},
+ %Schema{type: :string},
+ %Schema{type: :integer},
+ %Schema{type: :boolean}
+ ]
+ }
+ end
+
+ defp config_response do
+ %Schema{
+ type: :object,
+ properties: %{
+ configs: %Schema{
+ type: :array,
+ items: %Schema{
+ type: :object,
+ properties: %{
+ group: %Schema{type: :string},
+ key: %Schema{type: :string},
+ value: any()
+ }
+ }
+ },
+ need_reboot: %Schema{
+ type: :boolean,
+ description:
+ "If `need_reboot` is `true`, instance must be restarted, so reboot time settings can take effect"
+ }
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/admin/invite_operation.ex b/lib/pleroma/web/api_spec/operations/admin/invite_operation.ex
new file mode 100644
index 000000000..801024d75
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/admin/invite_operation.ex
@@ -0,0 +1,152 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Admin.InviteOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+
+ import Pleroma.Web.ApiSpec.Helpers
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def index_operation do
+ %Operation{
+ tags: ["Admin", "Invites"],
+ summary: "Get a list of generated invites",
+ operationId: "AdminAPI.InviteController.index",
+ security: [%{"oAuth" => ["read:invites"]}],
+ parameters: admin_api_params(),
+ responses: %{
+ 200 =>
+ Operation.response("Invites", "application/json", %Schema{
+ type: :object,
+ properties: %{
+ invites: %Schema{type: :array, items: invite()}
+ },
+ example: %{
+ "invites" => [
+ %{
+ "id" => 123,
+ "token" => "kSQtDj_GNy2NZsL9AQDFIsHN5qdbguB6qRg3WHw6K1U=",
+ "used" => true,
+ "expires_at" => nil,
+ "uses" => 0,
+ "max_use" => nil,
+ "invite_type" => "one_time"
+ }
+ ]
+ }
+ })
+ }
+ }
+ end
+
+ def create_operation do
+ %Operation{
+ tags: ["Admin", "Invites"],
+ summary: "Create an account registration invite token",
+ operationId: "AdminAPI.InviteController.create",
+ security: [%{"oAuth" => ["write:invites"]}],
+ parameters: admin_api_params(),
+ requestBody:
+ request_body("Parameters", %Schema{
+ type: :object,
+ properties: %{
+ max_use: %Schema{type: :integer},
+ expires_at: %Schema{type: :string, format: :date, example: "2020-04-20"}
+ }
+ }),
+ responses: %{
+ 200 => Operation.response("Invite", "application/json", invite())
+ }
+ }
+ end
+
+ def revoke_operation do
+ %Operation{
+ tags: ["Admin", "Invites"],
+ summary: "Revoke invite by token",
+ operationId: "AdminAPI.InviteController.revoke",
+ security: [%{"oAuth" => ["write:invites"]}],
+ parameters: admin_api_params(),
+ requestBody:
+ request_body(
+ "Parameters",
+ %Schema{
+ type: :object,
+ required: [:token],
+ properties: %{
+ token: %Schema{type: :string}
+ }
+ },
+ required: true
+ ),
+ responses: %{
+ 200 => Operation.response("Invite", "application/json", invite()),
+ 400 => Operation.response("Bad Request", "application/json", ApiError),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def email_operation do
+ %Operation{
+ tags: ["Admin", "Invites"],
+ summary: "Sends registration invite via email",
+ operationId: "AdminAPI.InviteController.email",
+ security: [%{"oAuth" => ["write:invites"]}],
+ parameters: admin_api_params(),
+ requestBody:
+ request_body(
+ "Parameters",
+ %Schema{
+ type: :object,
+ required: [:email],
+ properties: %{
+ email: %Schema{type: :string, format: :email},
+ name: %Schema{type: :string}
+ }
+ },
+ required: true
+ ),
+ responses: %{
+ 204 => no_content_response(),
+ 400 => Operation.response("Bad Request", "application/json", ApiError),
+ 403 => Operation.response("Forbidden", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp invite do
+ %Schema{
+ title: "Invite",
+ type: :object,
+ properties: %{
+ id: %Schema{type: :integer},
+ token: %Schema{type: :string},
+ used: %Schema{type: :boolean},
+ expires_at: %Schema{type: :string, format: :date, nullable: true},
+ uses: %Schema{type: :integer},
+ max_use: %Schema{type: :integer, nullable: true},
+ invite_type: %Schema{
+ type: :string,
+ enum: ["one_time", "reusable", "date_limited", "reusable_date_limited"]
+ }
+ },
+ example: %{
+ "id" => 123,
+ "token" => "kSQtDj_GNy2NZsL9AQDFIsHN5qdbguB6qRg3WHw6K1U=",
+ "used" => true,
+ "expires_at" => nil,
+ "uses" => 0,
+ "max_use" => nil,
+ "invite_type" => "one_time"
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/admin/media_proxy_cache_operation.ex b/lib/pleroma/web/api_spec/operations/admin/media_proxy_cache_operation.ex
new file mode 100644
index 000000000..ab45d6633
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/admin/media_proxy_cache_operation.ex
@@ -0,0 +1,121 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Admin.MediaProxyCacheOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+
+ import Pleroma.Web.ApiSpec.Helpers
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def index_operation do
+ %Operation{
+ tags: ["Admin", "MediaProxyCache"],
+ summary: "Fetch a paginated list of all banned MediaProxy URLs in Cachex",
+ operationId: "AdminAPI.MediaProxyCacheController.index",
+ security: [%{"oAuth" => ["read:media_proxy_caches"]}],
+ parameters: [
+ Operation.parameter(
+ :query,
+ :query,
+ %Schema{type: :string, default: nil},
+ "Page"
+ ),
+ Operation.parameter(
+ :page,
+ :query,
+ %Schema{type: :integer, default: 1},
+ "Page"
+ ),
+ Operation.parameter(
+ :page_size,
+ :query,
+ %Schema{type: :integer, default: 50},
+ "Number of statuses to return"
+ )
+ | admin_api_params()
+ ],
+ responses: %{
+ 200 =>
+ Operation.response(
+ "Array of banned MediaProxy URLs in Cachex",
+ "application/json",
+ %Schema{
+ type: :object,
+ properties: %{
+ count: %Schema{type: :integer},
+ page_size: %Schema{type: :integer},
+ urls: %Schema{
+ type: :array,
+ items: %Schema{
+ type: :string,
+ format: :uri,
+ description: "MediaProxy URLs"
+ }
+ }
+ }
+ }
+ )
+ }
+ }
+ end
+
+ def delete_operation do
+ %Operation{
+ tags: ["Admin", "MediaProxyCache"],
+ summary: "Remove a banned MediaProxy URL from Cachex",
+ operationId: "AdminAPI.MediaProxyCacheController.delete",
+ security: [%{"oAuth" => ["write:media_proxy_caches"]}],
+ parameters: admin_api_params(),
+ requestBody:
+ request_body(
+ "Parameters",
+ %Schema{
+ type: :object,
+ required: [:urls],
+ properties: %{
+ urls: %Schema{type: :array, items: %Schema{type: :string, format: :uri}}
+ }
+ },
+ required: true
+ ),
+ responses: %{
+ 200 => empty_object_response(),
+ 400 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def purge_operation do
+ %Operation{
+ tags: ["Admin", "MediaProxyCache"],
+ summary: "Purge and optionally ban a MediaProxy URL",
+ operationId: "AdminAPI.MediaProxyCacheController.purge",
+ security: [%{"oAuth" => ["write:media_proxy_caches"]}],
+ parameters: admin_api_params(),
+ requestBody:
+ request_body(
+ "Parameters",
+ %Schema{
+ type: :object,
+ required: [:urls],
+ properties: %{
+ urls: %Schema{type: :array, items: %Schema{type: :string, format: :uri}},
+ ban: %Schema{type: :boolean, default: true}
+ }
+ },
+ required: true
+ ),
+ responses: %{
+ 200 => empty_object_response(),
+ 400 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex b/lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex
new file mode 100644
index 000000000..a75f3e622
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex
@@ -0,0 +1,217 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+
+ import Pleroma.Web.ApiSpec.Helpers
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def index_operation do
+ %Operation{
+ summary: "List OAuth apps",
+ tags: ["Admin", "oAuth Apps"],
+ operationId: "AdminAPI.OAuthAppController.index",
+ security: [%{"oAuth" => ["write"]}],
+ parameters: [
+ Operation.parameter(:name, :query, %Schema{type: :string}, "App name"),
+ Operation.parameter(:client_id, :query, %Schema{type: :string}, "Client ID"),
+ Operation.parameter(:page, :query, %Schema{type: :integer, default: 1}, "Page"),
+ Operation.parameter(
+ :trusted,
+ :query,
+ %Schema{type: :boolean, default: false},
+ "Trusted apps"
+ ),
+ Operation.parameter(
+ :page_size,
+ :query,
+ %Schema{type: :integer, default: 50},
+ "Number of apps to return"
+ )
+ | admin_api_params()
+ ],
+ responses: %{
+ 200 =>
+ Operation.response("List of apps", "application/json", %Schema{
+ type: :object,
+ properties: %{
+ apps: %Schema{type: :array, items: oauth_app()},
+ count: %Schema{type: :integer},
+ page_size: %Schema{type: :integer}
+ },
+ example: %{
+ "apps" => [
+ %{
+ "id" => 1,
+ "name" => "App name",
+ "client_id" => "yHoDSiWYp5mPV6AfsaVOWjdOyt5PhWRiafi6MRd1lSk",
+ "client_secret" => "nLmis486Vqrv2o65eM9mLQx_m_4gH-Q6PcDpGIMl6FY",
+ "redirect_uri" => "https://example.com/oauth-callback",
+ "website" => "https://example.com",
+ "trusted" => true
+ }
+ ],
+ "count" => 1,
+ "page_size" => 50
+ }
+ })
+ }
+ }
+ end
+
+ def create_operation do
+ %Operation{
+ tags: ["Admin", "oAuth Apps"],
+ summary: "Create OAuth App",
+ operationId: "AdminAPI.OAuthAppController.create",
+ requestBody: request_body("Parameters", create_request()),
+ parameters: admin_api_params(),
+ security: [%{"oAuth" => ["write"]}],
+ responses: %{
+ 200 => Operation.response("App", "application/json", oauth_app()),
+ 400 => Operation.response("Bad Request", "application/json", ApiError)
+ }
+ }
+ end
+
+ def update_operation do
+ %Operation{
+ tags: ["Admin", "oAuth Apps"],
+ summary: "Update OAuth App",
+ operationId: "AdminAPI.OAuthAppController.update",
+ parameters: [id_param() | admin_api_params()],
+ security: [%{"oAuth" => ["write"]}],
+ requestBody: request_body("Parameters", update_request()),
+ responses: %{
+ 200 => Operation.response("App", "application/json", oauth_app()),
+ 400 =>
+ Operation.response("Bad Request", "application/json", %Schema{
+ oneOf: [ApiError, %Schema{type: :string}]
+ })
+ }
+ }
+ end
+
+ def delete_operation do
+ %Operation{
+ tags: ["Admin", "oAuth Apps"],
+ summary: "Delete OAuth App",
+ operationId: "AdminAPI.OAuthAppController.delete",
+ parameters: [id_param() | admin_api_params()],
+ security: [%{"oAuth" => ["write"]}],
+ responses: %{
+ 204 => no_content_response(),
+ 400 => no_content_response()
+ }
+ }
+ end
+
+ defp create_request do
+ %Schema{
+ title: "oAuthAppCreateRequest",
+ type: :object,
+ required: [:name, :redirect_uris],
+ properties: %{
+ name: %Schema{type: :string, description: "Application Name"},
+ scopes: %Schema{type: :array, items: %Schema{type: :string}, description: "oAuth scopes"},
+ redirect_uris: %Schema{
+ type: :string,
+ description:
+ "Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter."
+ },
+ website: %Schema{
+ type: :string,
+ nullable: true,
+ description: "A URL to the homepage of the app"
+ },
+ trusted: %Schema{
+ type: :boolean,
+ nullable: true,
+ default: false,
+ description: "Is the app trusted?"
+ }
+ },
+ example: %{
+ "name" => "My App",
+ "redirect_uris" => "https://myapp.com/auth/callback",
+ "website" => "https://myapp.com/",
+ "scopes" => ["read", "write"],
+ "trusted" => true
+ }
+ }
+ end
+
+ defp update_request do
+ %Schema{
+ title: "oAuthAppUpdateRequest",
+ type: :object,
+ properties: %{
+ name: %Schema{type: :string, description: "Application Name"},
+ scopes: %Schema{type: :array, items: %Schema{type: :string}, description: "oAuth scopes"},
+ redirect_uris: %Schema{
+ type: :string,
+ description:
+ "Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter."
+ },
+ website: %Schema{
+ type: :string,
+ nullable: true,
+ description: "A URL to the homepage of the app"
+ },
+ trusted: %Schema{
+ type: :boolean,
+ nullable: true,
+ default: false,
+ description: "Is the app trusted?"
+ }
+ },
+ example: %{
+ "name" => "My App",
+ "redirect_uris" => "https://myapp.com/auth/callback",
+ "website" => "https://myapp.com/",
+ "scopes" => ["read", "write"],
+ "trusted" => true
+ }
+ }
+ end
+
+ defp oauth_app do
+ %Schema{
+ title: "oAuthApp",
+ type: :object,
+ properties: %{
+ id: %Schema{type: :integer},
+ name: %Schema{type: :string},
+ client_id: %Schema{type: :string},
+ client_secret: %Schema{type: :string},
+ redirect_uri: %Schema{type: :string},
+ website: %Schema{type: :string, nullable: true},
+ trusted: %Schema{type: :boolean}
+ },
+ example: %{
+ "id" => 123,
+ "name" => "My App",
+ "client_id" => "TWhM-tNSuncnqN7DBJmoyeLnk6K3iJJ71KKXxgL1hPM",
+ "client_secret" => "ZEaFUFmF0umgBX1qKJDjaU99Q31lDkOU8NutzTOoliw",
+ "redirect_uri" => "https://myapp.com/oauth-callback",
+ "website" => "https://myapp.com/",
+ "trusted" => false
+ }
+ }
+ end
+
+ def id_param do
+ Operation.parameter(:id, :path, :integer, "App ID",
+ example: 1337,
+ required: true
+ )
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/admin/relay_operation.ex b/lib/pleroma/web/api_spec/operations/admin/relay_operation.ex
new file mode 100644
index 000000000..e06b2d164
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/admin/relay_operation.ex
@@ -0,0 +1,94 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Admin.RelayOperation 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: ["Admin", "Relays"],
+ summary: "List Relays",
+ operationId: "AdminAPI.RelayController.index",
+ security: [%{"oAuth" => ["read"]}],
+ parameters: admin_api_params(),
+ responses: %{
+ 200 =>
+ Operation.response("Response", "application/json", %Schema{
+ type: :object,
+ properties: %{
+ relays: %Schema{
+ type: :array,
+ items: relay()
+ }
+ }
+ })
+ }
+ }
+ end
+
+ def follow_operation do
+ %Operation{
+ tags: ["Admin", "Relays"],
+ summary: "Follow a Relay",
+ operationId: "AdminAPI.RelayController.follow",
+ security: [%{"oAuth" => ["write:follows"]}],
+ parameters: admin_api_params(),
+ requestBody: request_body("Parameters", relay_url()),
+ responses: %{
+ 200 => Operation.response("Status", "application/json", relay())
+ }
+ }
+ end
+
+ def unfollow_operation do
+ %Operation{
+ tags: ["Admin", "Relays"],
+ summary: "Unfollow a Relay",
+ operationId: "AdminAPI.RelayController.unfollow",
+ security: [%{"oAuth" => ["write:follows"]}],
+ parameters: admin_api_params(),
+ requestBody: request_body("Parameters", relay_url()),
+ responses: %{
+ 200 =>
+ Operation.response("Status", "application/json", %Schema{
+ type: :string,
+ example: "http://mastodon.example.org/users/admin"
+ })
+ }
+ }
+ end
+
+ defp relay do
+ %Schema{
+ type: :object,
+ properties: %{
+ actor: %Schema{
+ type: :string,
+ example: "https://example.com/relay"
+ },
+ followed_back: %Schema{
+ type: :boolean,
+ description: "Is relay followed back by this actor?"
+ }
+ }
+ }
+ end
+
+ defp relay_url do
+ %Schema{
+ type: :object,
+ properties: %{
+ relay_url: %Schema{type: :string, format: :uri}
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/admin/report_operation.ex b/lib/pleroma/web/api_spec/operations/admin/report_operation.ex
new file mode 100644
index 000000000..3bb7ec49e
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/admin/report_operation.ex
@@ -0,0 +1,240 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.Account
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+ alias Pleroma.Web.ApiSpec.Schemas.FlakeID
+ alias Pleroma.Web.ApiSpec.Schemas.Status
+
+ import Pleroma.Web.ApiSpec.Helpers
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def index_operation do
+ %Operation{
+ tags: ["Admin", "Reports"],
+ summary: "Get a list of reports",
+ operationId: "AdminAPI.ReportController.index",
+ security: [%{"oAuth" => ["read:reports"]}],
+ parameters: [
+ Operation.parameter(
+ :state,
+ :query,
+ report_state(),
+ "Filter by report state"
+ ),
+ Operation.parameter(
+ :limit,
+ :query,
+ %Schema{type: :integer},
+ "The number of records to retrieve"
+ ),
+ Operation.parameter(
+ :page,
+ :query,
+ %Schema{type: :integer, default: 1},
+ "Page number"
+ ),
+ Operation.parameter(
+ :page_size,
+ :query,
+ %Schema{type: :integer, default: 50},
+ "Number number of log entries per page"
+ )
+ | admin_api_params()
+ ],
+ responses: %{
+ 200 =>
+ Operation.response("Response", "application/json", %Schema{
+ type: :object,
+ properties: %{
+ total: %Schema{type: :integer},
+ reports: %Schema{
+ type: :array,
+ items: report()
+ }
+ }
+ }),
+ 403 => Operation.response("Forbidden", "application/json", ApiError)
+ }
+ }
+ end
+
+ def show_operation do
+ %Operation{
+ tags: ["Admin", "Reports"],
+ summary: "Get an individual report",
+ operationId: "AdminAPI.ReportController.show",
+ parameters: [id_param() | admin_api_params()],
+ security: [%{"oAuth" => ["read:reports"]}],
+ responses: %{
+ 200 => Operation.response("Report", "application/json", report()),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def update_operation do
+ %Operation{
+ tags: ["Admin", "Reports"],
+ summary: "Change the state of one or multiple reports",
+ operationId: "AdminAPI.ReportController.update",
+ security: [%{"oAuth" => ["write:reports"]}],
+ parameters: admin_api_params(),
+ requestBody: request_body("Parameters", update_request(), required: true),
+ responses: %{
+ 204 => no_content_response(),
+ 400 => Operation.response("Bad Request", "application/json", update_400_response()),
+ 403 => Operation.response("Forbidden", "application/json", ApiError)
+ }
+ }
+ end
+
+ def notes_create_operation do
+ %Operation{
+ tags: ["Admin", "Reports"],
+ summary: "Create report note",
+ operationId: "AdminAPI.ReportController.notes_create",
+ parameters: [id_param() | admin_api_params()],
+ requestBody:
+ request_body("Parameters", %Schema{
+ type: :object,
+ properties: %{
+ content: %Schema{type: :string, description: "The message"}
+ }
+ }),
+ security: [%{"oAuth" => ["write:reports"]}],
+ responses: %{
+ 204 => no_content_response(),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def notes_delete_operation do
+ %Operation{
+ tags: ["Admin", "Reports"],
+ summary: "Delete report note",
+ operationId: "AdminAPI.ReportController.notes_delete",
+ parameters: [
+ Operation.parameter(:report_id, :path, :string, "Report ID"),
+ Operation.parameter(:id, :path, :string, "Note ID")
+ | admin_api_params()
+ ],
+ security: [%{"oAuth" => ["write:reports"]}],
+ responses: %{
+ 204 => no_content_response(),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp report_state do
+ %Schema{type: :string, enum: ["open", "closed", "resolved"]}
+ end
+
+ defp id_param do
+ Operation.parameter(:id, :path, FlakeID, "Report ID",
+ example: "9umDrYheeY451cQnEe",
+ required: true
+ )
+ end
+
+ defp report do
+ %Schema{
+ type: :object,
+ properties: %{
+ id: FlakeID,
+ state: report_state(),
+ account: account_admin(),
+ actor: account_admin(),
+ content: %Schema{type: :string},
+ created_at: %Schema{type: :string, format: :"date-time"},
+ statuses: %Schema{type: :array, items: Status},
+ notes: %Schema{
+ type: :array,
+ items: %Schema{
+ type: :object,
+ properties: %{
+ id: %Schema{type: :integer},
+ user_id: FlakeID,
+ content: %Schema{type: :string},
+ inserted_at: %Schema{type: :string, format: :"date-time"}
+ }
+ }
+ }
+ }
+ }
+ end
+
+ defp account_admin do
+ %Schema{
+ title: "Account",
+ description: "Account view for admins",
+ type: :object,
+ properties:
+ Map.merge(Account.schema().properties, %{
+ nickname: %Schema{type: :string},
+ deactivated: %Schema{type: :boolean},
+ local: %Schema{type: :boolean},
+ roles: %Schema{
+ type: :object,
+ properties: %{
+ admin: %Schema{type: :boolean},
+ moderator: %Schema{type: :boolean}
+ }
+ },
+ confirmation_pending: %Schema{type: :boolean}
+ })
+ }
+ end
+
+ defp update_request do
+ %Schema{
+ type: :object,
+ required: [:reports],
+ properties: %{
+ reports: %Schema{
+ type: :array,
+ items: %Schema{
+ type: :object,
+ properties: %{
+ id: %Schema{allOf: [FlakeID], description: "Required, report ID"},
+ state: %Schema{
+ type: :string,
+ description:
+ "Required, the new state. Valid values are `open`, `closed` and `resolved`"
+ }
+ }
+ },
+ example: %{
+ "reports" => [
+ %{"id" => "123", "state" => "closed"},
+ %{"id" => "1337", "state" => "resolved"}
+ ]
+ }
+ }
+ }
+ }
+ end
+
+ defp update_400_response do
+ %Schema{
+ type: :array,
+ items: %Schema{
+ type: :object,
+ properties: %{
+ id: %Schema{allOf: [FlakeID], description: "Report ID"},
+ error: %Schema{type: :string, description: "Error message"}
+ }
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/admin/status_operation.ex b/lib/pleroma/web/api_spec/operations/admin/status_operation.ex
new file mode 100644
index 000000000..c105838a4
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/admin/status_operation.ex
@@ -0,0 +1,166 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Admin.StatusOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.Account
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+ alias Pleroma.Web.ApiSpec.Schemas.FlakeID
+ alias Pleroma.Web.ApiSpec.Schemas.Status
+ alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
+
+ import Pleroma.Web.ApiSpec.Helpers
+ import Pleroma.Web.ApiSpec.StatusOperation, only: [id_param: 0]
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def index_operation do
+ %Operation{
+ tags: ["Admin", "Statuses"],
+ operationId: "AdminAPI.StatusController.index",
+ security: [%{"oAuth" => ["read:statuses"]}],
+ parameters: [
+ Operation.parameter(
+ :godmode,
+ :query,
+ %Schema{type: :boolean, default: false},
+ "Allows to see private statuses"
+ ),
+ Operation.parameter(
+ :local_only,
+ :query,
+ %Schema{type: :boolean, default: false},
+ "Excludes remote statuses"
+ ),
+ Operation.parameter(
+ :with_reblogs,
+ :query,
+ %Schema{type: :boolean, default: false},
+ "Allows to see reblogs"
+ ),
+ Operation.parameter(
+ :page,
+ :query,
+ %Schema{type: :integer, default: 1},
+ "Page"
+ ),
+ Operation.parameter(
+ :page_size,
+ :query,
+ %Schema{type: :integer, default: 50},
+ "Number of statuses to return"
+ )
+ | admin_api_params()
+ ],
+ responses: %{
+ 200 =>
+ Operation.response("Array of statuses", "application/json", %Schema{
+ type: :array,
+ items: status()
+ })
+ }
+ }
+ end
+
+ def show_operation do
+ %Operation{
+ tags: ["Admin", "Statuses"],
+ summary: "Show Status",
+ operationId: "AdminAPI.StatusController.show",
+ parameters: [id_param() | admin_api_params()],
+ security: [%{"oAuth" => ["read:statuses"]}],
+ responses: %{
+ 200 => Operation.response("Status", "application/json", status()),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def update_operation do
+ %Operation{
+ tags: ["Admin", "Statuses"],
+ summary: "Change the scope of an individual reported status",
+ operationId: "AdminAPI.StatusController.update",
+ parameters: [id_param() | admin_api_params()],
+ security: [%{"oAuth" => ["write:statuses"]}],
+ requestBody: request_body("Parameters", update_request(), required: true),
+ responses: %{
+ 200 => Operation.response("Status", "application/json", Status),
+ 400 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def delete_operation do
+ %Operation{
+ tags: ["Admin", "Statuses"],
+ summary: "Delete an individual reported status",
+ operationId: "AdminAPI.StatusController.delete",
+ parameters: [id_param() | admin_api_params()],
+ security: [%{"oAuth" => ["write:statuses"]}],
+ responses: %{
+ 200 => empty_object_response(),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp status do
+ %Schema{
+ anyOf: [
+ Status,
+ %Schema{
+ type: :object,
+ properties: %{
+ account: %Schema{allOf: [Account, admin_account()]}
+ }
+ }
+ ]
+ }
+ end
+
+ def admin_account do
+ %Schema{
+ type: :object,
+ properties: %{
+ id: FlakeID,
+ avatar: %Schema{type: :string},
+ nickname: %Schema{type: :string},
+ display_name: %Schema{type: :string},
+ deactivated: %Schema{type: :boolean},
+ local: %Schema{type: :boolean},
+ roles: %Schema{
+ type: :object,
+ properties: %{
+ admin: %Schema{type: :boolean},
+ moderator: %Schema{type: :boolean}
+ }
+ },
+ tags: %Schema{type: :string},
+ confirmation_pending: %Schema{type: :string}
+ }
+ }
+ end
+
+ defp update_request do
+ %Schema{
+ type: :object,
+ properties: %{
+ sensitive: %Schema{
+ type: :boolean,
+ description: "Mark status and attached media as sensitive?"
+ },
+ visibility: VisibilityScope
+ },
+ example: %{
+ "visibility" => "private",
+ "sensitive" => "false"
+ }
+ }
+ end
+end
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..ae01cbbec
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/app_operation.ex
@@ -0,0 +1,148 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.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,
+ nullable: true,
+ 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/chat_operation.ex b/lib/pleroma/web/api_spec/operations/chat_operation.ex
new file mode 100644
index 000000000..b1a0d26ab
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/chat_operation.ex
@@ -0,0 +1,355 @@
+# 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.ChatOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+ alias Pleroma.Web.ApiSpec.Schemas.Chat
+ alias Pleroma.Web.ApiSpec.Schemas.ChatMessage
+
+ 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
+
+ def mark_as_read_operation do
+ %Operation{
+ tags: ["chat"],
+ summary: "Mark all messages in the chat as read",
+ operationId: "ChatController.mark_as_read",
+ parameters: [Operation.parameter(:id, :path, :string, "The ID of the Chat")],
+ requestBody: request_body("Parameters", mark_as_read()),
+ responses: %{
+ 200 =>
+ Operation.response(
+ "The updated chat",
+ "application/json",
+ Chat
+ )
+ },
+ security: [
+ %{
+ "oAuth" => ["write:chats"]
+ }
+ ]
+ }
+ end
+
+ def mark_message_as_read_operation do
+ %Operation{
+ tags: ["chat"],
+ summary: "Mark one message in the chat as read",
+ operationId: "ChatController.mark_message_as_read",
+ parameters: [
+ Operation.parameter(:id, :path, :string, "The ID of the Chat"),
+ Operation.parameter(:message_id, :path, :string, "The ID of the message")
+ ],
+ responses: %{
+ 200 =>
+ Operation.response(
+ "The read ChatMessage",
+ "application/json",
+ ChatMessage
+ )
+ },
+ security: [
+ %{
+ "oAuth" => ["write:chats"]
+ }
+ ]
+ }
+ end
+
+ def show_operation do
+ %Operation{
+ tags: ["chat"],
+ summary: "Create a chat",
+ operationId: "ChatController.show",
+ parameters: [
+ Operation.parameter(
+ :id,
+ :path,
+ :string,
+ "The id of the chat",
+ required: true,
+ example: "1234"
+ )
+ ],
+ responses: %{
+ 200 =>
+ Operation.response(
+ "The existing chat",
+ "application/json",
+ Chat
+ )
+ },
+ security: [
+ %{
+ "oAuth" => ["read"]
+ }
+ ]
+ }
+ end
+
+ def create_operation do
+ %Operation{
+ tags: ["chat"],
+ summary: "Create a chat",
+ operationId: "ChatController.create",
+ parameters: [
+ Operation.parameter(
+ :id,
+ :path,
+ :string,
+ "The account id of the recipient of this chat",
+ required: true,
+ example: "someflakeid"
+ )
+ ],
+ responses: %{
+ 200 =>
+ Operation.response(
+ "The created or existing chat",
+ "application/json",
+ Chat
+ )
+ },
+ security: [
+ %{
+ "oAuth" => ["write:chats"]
+ }
+ ]
+ }
+ end
+
+ def index_operation do
+ %Operation{
+ tags: ["chat"],
+ summary: "Get a list of chats that you participated in",
+ operationId: "ChatController.index",
+ parameters: pagination_params(),
+ responses: %{
+ 200 => Operation.response("The chats of the user", "application/json", chats_response())
+ },
+ security: [
+ %{
+ "oAuth" => ["read:chats"]
+ }
+ ]
+ }
+ end
+
+ def messages_operation do
+ %Operation{
+ tags: ["chat"],
+ summary: "Get the most recent messages of the chat",
+ operationId: "ChatController.messages",
+ parameters:
+ [Operation.parameter(:id, :path, :string, "The ID of the Chat")] ++
+ pagination_params(),
+ responses: %{
+ 200 =>
+ Operation.response(
+ "The messages in the chat",
+ "application/json",
+ chat_messages_response()
+ )
+ },
+ security: [
+ %{
+ "oAuth" => ["read:chats"]
+ }
+ ]
+ }
+ end
+
+ def post_chat_message_operation do
+ %Operation{
+ tags: ["chat"],
+ summary: "Post a message to the chat",
+ operationId: "ChatController.post_chat_message",
+ parameters: [
+ Operation.parameter(:id, :path, :string, "The ID of the Chat")
+ ],
+ requestBody: request_body("Parameters", chat_message_create()),
+ responses: %{
+ 200 =>
+ Operation.response(
+ "The newly created ChatMessage",
+ "application/json",
+ ChatMessage
+ ),
+ 400 => Operation.response("Bad Request", "application/json", ApiError)
+ },
+ security: [
+ %{
+ "oAuth" => ["write:chats"]
+ }
+ ]
+ }
+ end
+
+ def delete_message_operation do
+ %Operation{
+ tags: ["chat"],
+ summary: "delete_message",
+ operationId: "ChatController.delete_message",
+ parameters: [
+ Operation.parameter(:id, :path, :string, "The ID of the Chat"),
+ Operation.parameter(:message_id, :path, :string, "The ID of the message")
+ ],
+ responses: %{
+ 200 =>
+ Operation.response(
+ "The deleted ChatMessage",
+ "application/json",
+ ChatMessage
+ )
+ },
+ security: [
+ %{
+ "oAuth" => ["write:chats"]
+ }
+ ]
+ }
+ end
+
+ def chats_response do
+ %Schema{
+ title: "ChatsResponse",
+ description: "Response schema for multiple Chats",
+ type: :array,
+ items: Chat,
+ example: [
+ %{
+ "account" => %{
+ "pleroma" => %{
+ "is_admin" => false,
+ "confirmation_pending" => false,
+ "hide_followers_count" => false,
+ "is_moderator" => false,
+ "hide_favorites" => true,
+ "ap_id" => "https://dontbulling.me/users/lain",
+ "hide_follows_count" => false,
+ "hide_follows" => false,
+ "background_image" => nil,
+ "skip_thread_containment" => false,
+ "hide_followers" => false,
+ "relationship" => %{},
+ "tags" => []
+ },
+ "avatar" =>
+ "https://dontbulling.me/media/065a4dd3c6740dab13ff9c71ec7d240bb9f8be9205c9e7467fb2202117da1e32.jpg",
+ "following_count" => 0,
+ "header_static" => "https://originalpatchou.li/images/banner.png",
+ "source" => %{
+ "sensitive" => false,
+ "note" => "lain",
+ "pleroma" => %{
+ "discoverable" => false,
+ "actor_type" => "Person"
+ },
+ "fields" => []
+ },
+ "statuses_count" => 1,
+ "locked" => false,
+ "created_at" => "2020-04-16T13:40:15.000Z",
+ "display_name" => "lain",
+ "fields" => [],
+ "acct" => "lain@dontbulling.me",
+ "id" => "9u6Qw6TAZANpqokMkK",
+ "emojis" => [],
+ "avatar_static" =>
+ "https://dontbulling.me/media/065a4dd3c6740dab13ff9c71ec7d240bb9f8be9205c9e7467fb2202117da1e32.jpg",
+ "username" => "lain",
+ "followers_count" => 0,
+ "header" => "https://originalpatchou.li/images/banner.png",
+ "bot" => false,
+ "note" => "lain",
+ "url" => "https://dontbulling.me/users/lain"
+ },
+ "id" => "1",
+ "unread" => 2
+ }
+ ]
+ }
+ end
+
+ def chat_messages_response do
+ %Schema{
+ title: "ChatMessagesResponse",
+ description: "Response schema for multiple ChatMessages",
+ type: :array,
+ items: ChatMessage,
+ example: [
+ %{
+ "emojis" => [
+ %{
+ "static_url" => "https://dontbulling.me/emoji/Firefox.gif",
+ "visible_in_picker" => false,
+ "shortcode" => "firefox",
+ "url" => "https://dontbulling.me/emoji/Firefox.gif"
+ }
+ ],
+ "created_at" => "2020-04-21T15:11:46.000Z",
+ "content" => "Check this out :firefox:",
+ "id" => "13",
+ "chat_id" => "1",
+ "account_id" => "someflakeid",
+ "unread" => false
+ },
+ %{
+ "account_id" => "someflakeid",
+ "content" => "Whats' up?",
+ "id" => "12",
+ "chat_id" => "1",
+ "emojis" => [],
+ "created_at" => "2020-04-21T15:06:45.000Z",
+ "unread" => false
+ }
+ ]
+ }
+ end
+
+ def chat_message_create do
+ %Schema{
+ title: "ChatMessageCreateRequest",
+ description: "POST body for creating an chat message",
+ type: :object,
+ properties: %{
+ content: %Schema{
+ type: :string,
+ description: "The content of your message. Optional if media_id is present"
+ },
+ media_id: %Schema{type: :string, description: "The id of an upload"}
+ },
+ example: %{
+ "content" => "Hey wanna buy feet pics?",
+ "media_id" => "134234"
+ }
+ }
+ end
+
+ def mark_as_read do
+ %Schema{
+ title: "MarkAsReadRequest",
+ description: "POST body for marking a number of chat messages as read",
+ type: :object,
+ required: [:last_read_id],
+ properties: %{
+ last_read_id: %Schema{
+ type: :string,
+ description: "The content of your message."
+ }
+ },
+ example: %{
+ "last_read_id" => "abcdef12456"
+ }
+ }
+ 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..1e0da8209
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/domain_block_operation.ex
@@ -0,0 +1,86 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.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
+
+ # Supporting domain query parameter is deprecated in Mastodon API
+ 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(),
+ parameters: [Operation.parameter(:domain, :query, %Schema{type: :string}, "Domain name")],
+ security: [%{"oAuth" => ["follow", "write:blocks"]}],
+ responses: %{200 => empty_object_response()}
+ }
+ end
+
+ # Supporting domain query parameter is deprecated in Mastodon API
+ 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(),
+ parameters: [Operation.parameter(:domain, :query, %Schema{type: :string}, "Domain name")],
+ 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: false,
+ example: %{
+ "domain" => "facebook.com"
+ }
+ )
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex b/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex
new file mode 100644
index 000000000..1a49fece0
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex
@@ -0,0 +1,104 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.EmojiReactionOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.Account
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+ alias Pleroma.Web.ApiSpec.Schemas.FlakeID
+ alias Pleroma.Web.ApiSpec.Schemas.Status
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def index_operation do
+ %Operation{
+ tags: ["Emoji Reactions"],
+ summary:
+ "Get an object of emoji to account mappings with accounts that reacted to the post",
+ parameters: [
+ Operation.parameter(:id, :path, FlakeID, "Status ID", required: true),
+ Operation.parameter(:emoji, :path, :string, "Filter by a single unicode emoji",
+ required: false
+ )
+ ],
+ security: [%{"oAuth" => ["read:statuses"]}],
+ operationId: "EmojiReactionController.index",
+ responses: %{
+ 200 => array_of_reactions_response()
+ }
+ }
+ end
+
+ def create_operation do
+ %Operation{
+ tags: ["Emoji Reactions"],
+ summary: "React to a post with a unicode emoji",
+ parameters: [
+ Operation.parameter(:id, :path, FlakeID, "Status ID", required: true),
+ Operation.parameter(:emoji, :path, :string, "A single character unicode emoji",
+ required: true
+ )
+ ],
+ security: [%{"oAuth" => ["write:statuses"]}],
+ operationId: "EmojiReactionController.create",
+ responses: %{
+ 200 => Operation.response("Status", "application/json", Status),
+ 400 => Operation.response("Bad Request", "application/json", ApiError)
+ }
+ }
+ end
+
+ def delete_operation do
+ %Operation{
+ tags: ["Emoji Reactions"],
+ summary: "Remove a reaction to a post with a unicode emoji",
+ parameters: [
+ Operation.parameter(:id, :path, FlakeID, "Status ID", required: true),
+ Operation.parameter(:emoji, :path, :string, "A single character unicode emoji",
+ required: true
+ )
+ ],
+ security: [%{"oAuth" => ["write:statuses"]}],
+ operationId: "EmojiReactionController.delete",
+ responses: %{
+ 200 => Operation.response("Status", "application/json", Status)
+ }
+ }
+ end
+
+ defp array_of_reactions_response do
+ Operation.response("Array of Emoji Reactions", "application/json", %Schema{
+ type: :array,
+ items: emoji_reaction(),
+ example: [emoji_reaction().example]
+ })
+ end
+
+ defp emoji_reaction do
+ %Schema{
+ title: "EmojiReaction",
+ type: :object,
+ properties: %{
+ name: %Schema{type: :string, description: "Emoji"},
+ count: %Schema{type: :integer, description: "Count of reactions with this emoji"},
+ me: %Schema{type: :boolean, description: "Did I react with this emoji?"},
+ accounts: %Schema{
+ type: :array,
+ items: Account,
+ description: "Array of accounts reacted with this emoji"
+ }
+ },
+ example: %{
+ "name" => "😱",
+ "count" => 1,
+ "me" => false,
+ "accounts" => [Account.schema().example]
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/filter_operation.ex b/lib/pleroma/web/api_spec/operations/filter_operation.ex
new file mode 100644
index 000000000..31e576f99
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/filter_operation.ex
@@ -0,0 +1,230 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.FilterOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Helpers
+ alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def index_operation do
+ %Operation{
+ tags: ["apps"],
+ summary: "View all filters",
+ operationId: "FilterController.index",
+ security: [%{"oAuth" => ["read:filters"]}],
+ responses: %{
+ 200 => Operation.response("Filters", "application/json", array_of_filters())
+ }
+ }
+ end
+
+ def create_operation do
+ %Operation{
+ tags: ["apps"],
+ summary: "Create a filter",
+ operationId: "FilterController.create",
+ requestBody: Helpers.request_body("Parameters", create_request(), required: true),
+ security: [%{"oAuth" => ["write:filters"]}],
+ responses: %{200 => Operation.response("Filter", "application/json", filter())}
+ }
+ end
+
+ def show_operation do
+ %Operation{
+ tags: ["apps"],
+ summary: "View all filters",
+ parameters: [id_param()],
+ operationId: "FilterController.show",
+ security: [%{"oAuth" => ["read:filters"]}],
+ responses: %{
+ 200 => Operation.response("Filter", "application/json", filter())
+ }
+ }
+ end
+
+ def update_operation do
+ %Operation{
+ tags: ["apps"],
+ summary: "Update a filter",
+ parameters: [id_param()],
+ operationId: "FilterController.update",
+ requestBody: Helpers.request_body("Parameters", update_request(), required: true),
+ security: [%{"oAuth" => ["write:filters"]}],
+ responses: %{
+ 200 => Operation.response("Filter", "application/json", filter())
+ }
+ }
+ end
+
+ def delete_operation do
+ %Operation{
+ tags: ["apps"],
+ summary: "Remove a filter",
+ parameters: [id_param()],
+ operationId: "FilterController.delete",
+ security: [%{"oAuth" => ["write:filters"]}],
+ responses: %{
+ 200 =>
+ Operation.response("Filter", "application/json", %Schema{
+ type: :object,
+ description: "Empty object"
+ })
+ }
+ }
+ end
+
+ defp id_param do
+ Operation.parameter(:id, :path, :string, "Filter ID", example: "123", required: true)
+ end
+
+ defp filter do
+ %Schema{
+ title: "Filter",
+ type: :object,
+ properties: %{
+ id: %Schema{type: :string},
+ phrase: %Schema{type: :string, description: "The text to be filtered"},
+ context: %Schema{
+ type: :array,
+ items: %Schema{type: :string, enum: ["home", "notifications", "public", "thread"]},
+ description: "The contexts in which the filter should be applied."
+ },
+ expires_at: %Schema{
+ type: :string,
+ format: :"date-time",
+ description:
+ "When the filter should no longer be applied. String (ISO 8601 Datetime), or null if the filter does not expire.",
+ nullable: true
+ },
+ irreversible: %Schema{
+ type: :boolean,
+ description:
+ "Should matching entities in home and notifications be dropped by the server?"
+ },
+ whole_word: %Schema{
+ type: :boolean,
+ description: "Should the filter consider word boundaries?"
+ }
+ },
+ example: %{
+ "id" => "5580",
+ "phrase" => "@twitter.com",
+ "context" => [
+ "home",
+ "notifications",
+ "public",
+ "thread"
+ ],
+ "whole_word" => false,
+ "expires_at" => nil,
+ "irreversible" => true
+ }
+ }
+ end
+
+ defp array_of_filters do
+ %Schema{
+ title: "ArrayOfFilters",
+ description: "Array of Filters",
+ type: :array,
+ items: filter(),
+ example: [
+ %{
+ "id" => "5580",
+ "phrase" => "@twitter.com",
+ "context" => [
+ "home",
+ "notifications",
+ "public",
+ "thread"
+ ],
+ "whole_word" => false,
+ "expires_at" => nil,
+ "irreversible" => true
+ },
+ %{
+ "id" => "6191",
+ "phrase" => ":eurovision2019:",
+ "context" => [
+ "home"
+ ],
+ "whole_word" => true,
+ "expires_at" => "2019-05-21T13:47:31.333Z",
+ "irreversible" => false
+ }
+ ]
+ }
+ end
+
+ defp create_request do
+ %Schema{
+ title: "FilterCreateRequest",
+ allOf: [
+ update_request(),
+ %Schema{
+ type: :object,
+ properties: %{
+ irreversible: %Schema{
+ allOf: [BooleanLike],
+ description:
+ "Should the server irreversibly drop matching entities from home and notifications?",
+ default: false
+ }
+ }
+ }
+ ],
+ example: %{
+ "phrase" => "knights",
+ "context" => ["home"]
+ }
+ }
+ end
+
+ defp update_request do
+ %Schema{
+ title: "FilterUpdateRequest",
+ type: :object,
+ properties: %{
+ phrase: %Schema{type: :string, description: "The text to be filtered"},
+ context: %Schema{
+ type: :array,
+ items: %Schema{type: :string, enum: ["home", "notifications", "public", "thread"]},
+ description:
+ "Array of enumerable strings `home`, `notifications`, `public`, `thread`. At least one context must be specified."
+ },
+ irreversible: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description:
+ "Should the server irreversibly drop matching entities from home and notifications?"
+ },
+ whole_word: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Consider word boundaries?",
+ default: true
+ }
+ # TODO: probably should implement filter expiration
+ # expires_in: %Schema{
+ # type: :string,
+ # format: :"date-time",
+ # description:
+ # "ISO 8601 Datetime for when the filter expires. Otherwise,
+ # null for a filter that doesn't expire."
+ # }
+ },
+ required: [:phrase, :context],
+ example: %{
+ "phrase" => "knights",
+ "context" => ["home"]
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/follow_request_operation.ex b/lib/pleroma/web/api_spec/operations/follow_request_operation.ex
new file mode 100644
index 000000000..ac4aee6da
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/follow_request_operation.ex
@@ -0,0 +1,65 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.FollowRequestOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.Account
+ alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def index_operation do
+ %Operation{
+ tags: ["Follow Requests"],
+ summary: "Pending Follows",
+ security: [%{"oAuth" => ["read:follows", "follow"]}],
+ operationId: "FollowRequestController.index",
+ responses: %{
+ 200 =>
+ Operation.response("Array of Account", "application/json", %Schema{
+ type: :array,
+ items: Account,
+ example: [Account.schema().example]
+ })
+ }
+ }
+ end
+
+ def authorize_operation do
+ %Operation{
+ tags: ["Follow Requests"],
+ summary: "Accept Follow",
+ operationId: "FollowRequestController.authorize",
+ parameters: [id_param()],
+ security: [%{"oAuth" => ["follow", "write:follows"]}],
+ responses: %{
+ 200 => Operation.response("Relationship", "application/json", AccountRelationship)
+ }
+ }
+ end
+
+ def reject_operation do
+ %Operation{
+ tags: ["Follow Requests"],
+ summary: "Reject Follow",
+ operationId: "FollowRequestController.reject",
+ parameters: [id_param()],
+ security: [%{"oAuth" => ["follow", "write:follows"]}],
+ responses: %{
+ 200 => Operation.response("Relationship", "application/json", AccountRelationship)
+ }
+ }
+ end
+
+ defp id_param do
+ Operation.parameter(:id, :path, :string, "Conversation ID",
+ example: "123",
+ required: true
+ )
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/instance_operation.ex b/lib/pleroma/web/api_spec/operations/instance_operation.ex
new file mode 100644
index 000000000..bf39ae643
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/instance_operation.ex
@@ -0,0 +1,175 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.InstanceOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def show_operation do
+ %Operation{
+ tags: ["Instance"],
+ summary: "Fetch instance",
+ description: "Information about the server",
+ operationId: "InstanceController.show",
+ responses: %{
+ 200 => Operation.response("Instance", "application/json", instance())
+ }
+ }
+ end
+
+ def peers_operation do
+ %Operation{
+ tags: ["Instance"],
+ summary: "List of known hosts",
+ operationId: "InstanceController.peers",
+ responses: %{
+ 200 => Operation.response("Array of domains", "application/json", array_of_domains())
+ }
+ }
+ end
+
+ defp instance do
+ %Schema{
+ type: :object,
+ properties: %{
+ uri: %Schema{type: :string, description: "The domain name of the instance"},
+ title: %Schema{type: :string, description: "The title of the website"},
+ description: %Schema{
+ type: :string,
+ description: "Admin-defined description of the Pleroma site"
+ },
+ version: %Schema{
+ type: :string,
+ description: "The version of Pleroma installed on the instance"
+ },
+ email: %Schema{
+ type: :string,
+ description: "An email that may be contacted for any inquiries",
+ format: :email
+ },
+ urls: %Schema{
+ type: :object,
+ description: "URLs of interest for clients apps",
+ properties: %{
+ streaming_api: %Schema{
+ type: :string,
+ description: "Websockets address for push streaming"
+ }
+ }
+ },
+ stats: %Schema{
+ type: :object,
+ description: "Statistics about how much information the instance contains",
+ properties: %{
+ user_count: %Schema{
+ type: :integer,
+ description: "Users registered on this instance"
+ },
+ status_count: %Schema{
+ type: :integer,
+ description: "Statuses authored by users on instance"
+ },
+ domain_count: %Schema{
+ type: :integer,
+ description: "Domains federated with this instance"
+ }
+ }
+ },
+ thumbnail: %Schema{
+ type: :string,
+ description: "Banner image for the website",
+ nullable: true
+ },
+ languages: %Schema{
+ type: :array,
+ items: %Schema{type: :string},
+ description: "Primary langauges of the website and its staff"
+ },
+ registrations: %Schema{type: :boolean, description: "Whether registrations are enabled"},
+ # Extra (not present in Mastodon):
+ max_toot_chars: %Schema{
+ type: :integer,
+ description: ": Posts character limit (CW/Subject included in the counter)"
+ },
+ poll_limits: %Schema{
+ type: :object,
+ description: "A map with poll limits for local polls",
+ properties: %{
+ max_options: %Schema{
+ type: :integer,
+ description: "Maximum number of options."
+ },
+ max_option_chars: %Schema{
+ type: :integer,
+ description: "Maximum number of characters per option."
+ },
+ min_expiration: %Schema{
+ type: :integer,
+ description: "Minimum expiration time (in seconds)."
+ },
+ max_expiration: %Schema{
+ type: :integer,
+ description: "Maximum expiration time (in seconds)."
+ }
+ }
+ },
+ upload_limit: %Schema{
+ type: :integer,
+ description: "File size limit of uploads (except for avatar, background, banner)"
+ },
+ avatar_upload_limit: %Schema{type: :integer, description: "The title of the website"},
+ background_upload_limit: %Schema{type: :integer, description: "The title of the website"},
+ banner_upload_limit: %Schema{type: :integer, description: "The title of the website"},
+ background_image: %Schema{
+ type: :string,
+ format: :uri,
+ description: "The background image for the website"
+ }
+ },
+ example: %{
+ "avatar_upload_limit" => 2_000_000,
+ "background_upload_limit" => 4_000_000,
+ "background_image" => "/static/image.png",
+ "banner_upload_limit" => 4_000_000,
+ "description" => "Pleroma: An efficient and flexible fediverse server",
+ "email" => "lain@lain.com",
+ "languages" => ["en"],
+ "max_toot_chars" => 5000,
+ "poll_limits" => %{
+ "max_expiration" => 31_536_000,
+ "max_option_chars" => 200,
+ "max_options" => 20,
+ "min_expiration" => 0
+ },
+ "registrations" => false,
+ "stats" => %{
+ "domain_count" => 2996,
+ "status_count" => 15_802,
+ "user_count" => 5
+ },
+ "thumbnail" => "https://lain.com/instance/thumbnail.jpeg",
+ "title" => "lain.com",
+ "upload_limit" => 16_000_000,
+ "uri" => "https://lain.com",
+ "urls" => %{
+ "streaming_api" => "wss://lain.com"
+ },
+ "version" => "2.7.2 (compatible; Pleroma 2.0.50-536-g25eec6d7-develop)"
+ }
+ }
+ end
+
+ defp array_of_domains do
+ %Schema{
+ type: :array,
+ items: %Schema{type: :string},
+ example: ["pleroma.site", "lain.com", "bikeshed.party"]
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/list_operation.ex b/lib/pleroma/web/api_spec/operations/list_operation.ex
new file mode 100644
index 000000000..c88ed5dd0
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/list_operation.ex
@@ -0,0 +1,188 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.ListOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.Account
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+ alias Pleroma.Web.ApiSpec.Schemas.FlakeID
+ alias Pleroma.Web.ApiSpec.Schemas.List
+
+ import Pleroma.Web.ApiSpec.Helpers
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def index_operation do
+ %Operation{
+ tags: ["Lists"],
+ summary: "Show user's lists",
+ description: "Fetch all lists that the user owns",
+ security: [%{"oAuth" => ["read:lists"]}],
+ operationId: "ListController.index",
+ responses: %{
+ 200 => Operation.response("Array of List", "application/json", array_of_lists())
+ }
+ }
+ end
+
+ def create_operation do
+ %Operation{
+ tags: ["Lists"],
+ summary: "Create a list",
+ description: "Fetch the list with the given ID. Used for verifying the title of a list.",
+ operationId: "ListController.create",
+ requestBody: create_update_request(),
+ security: [%{"oAuth" => ["write:lists"]}],
+ responses: %{
+ 200 => Operation.response("List", "application/json", List),
+ 400 => Operation.response("Error", "application/json", ApiError),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def show_operation do
+ %Operation{
+ tags: ["Lists"],
+ summary: "Show a single list",
+ description: "Fetch the list with the given ID. Used for verifying the title of a list.",
+ operationId: "ListController.show",
+ parameters: [id_param()],
+ security: [%{"oAuth" => ["read:lists"]}],
+ responses: %{
+ 200 => Operation.response("List", "application/json", List),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def update_operation do
+ %Operation{
+ tags: ["Lists"],
+ summary: "Update a list",
+ description: "Change the title of a list",
+ operationId: "ListController.update",
+ parameters: [id_param()],
+ requestBody: create_update_request(),
+ security: [%{"oAuth" => ["write:lists"]}],
+ responses: %{
+ 200 => Operation.response("List", "application/json", List),
+ 422 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def delete_operation do
+ %Operation{
+ tags: ["Lists"],
+ summary: "Delete a list",
+ operationId: "ListController.delete",
+ parameters: [id_param()],
+ security: [%{"oAuth" => ["write:lists"]}],
+ responses: %{
+ 200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
+ }
+ }
+ end
+
+ def list_accounts_operation do
+ %Operation{
+ tags: ["Lists"],
+ summary: "View accounts in list",
+ operationId: "ListController.list_accounts",
+ parameters: [id_param()],
+ security: [%{"oAuth" => ["read:lists"]}],
+ responses: %{
+ 200 =>
+ Operation.response("Array of Account", "application/json", %Schema{
+ type: :array,
+ items: Account
+ })
+ }
+ }
+ end
+
+ def add_to_list_operation do
+ %Operation{
+ tags: ["Lists"],
+ summary: "Add accounts to list",
+ description: "Add accounts to the given list.",
+ operationId: "ListController.add_to_list",
+ parameters: [id_param()],
+ requestBody: add_remove_accounts_request(),
+ security: [%{"oAuth" => ["write:lists"]}],
+ responses: %{
+ 200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
+ }
+ }
+ end
+
+ def remove_from_list_operation do
+ %Operation{
+ tags: ["Lists"],
+ summary: "Remove accounts from list",
+ operationId: "ListController.remove_from_list",
+ parameters: [id_param()],
+ requestBody: add_remove_accounts_request(),
+ security: [%{"oAuth" => ["write:lists"]}],
+ responses: %{
+ 200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
+ }
+ }
+ end
+
+ defp array_of_lists do
+ %Schema{
+ title: "ArrayOfLists",
+ description: "Response schema for lists",
+ type: :array,
+ items: List,
+ example: [
+ %{"id" => "123", "title" => "my list"},
+ %{"id" => "1337", "title" => "another list"}
+ ]
+ }
+ end
+
+ defp id_param do
+ Operation.parameter(:id, :path, :string, "List ID",
+ example: "123",
+ required: true
+ )
+ end
+
+ defp create_update_request do
+ request_body(
+ "Parameters",
+ %Schema{
+ description: "POST body for creating or updating a List",
+ type: :object,
+ properties: %{
+ title: %Schema{type: :string, description: "List title"}
+ },
+ required: [:title]
+ },
+ required: true
+ )
+ end
+
+ defp add_remove_accounts_request do
+ request_body(
+ "Parameters",
+ %Schema{
+ description: "POST body for adding/removing accounts to/from a List",
+ type: :object,
+ properties: %{
+ account_ids: %Schema{type: :array, description: "Array of account IDs", items: FlakeID}
+ },
+ required: [:account_ids]
+ },
+ required: true
+ )
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/marker_operation.ex b/lib/pleroma/web/api_spec/operations/marker_operation.ex
new file mode 100644
index 000000000..714ef1f99
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/marker_operation.ex
@@ -0,0 +1,142 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.MarkerOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Helpers
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def index_operation do
+ %Operation{
+ tags: ["Markers"],
+ summary: "Get saved timeline position",
+ security: [%{"oAuth" => ["read:statuses"]}],
+ operationId: "MarkerController.index",
+ parameters: [
+ Operation.parameter(
+ :timeline,
+ :query,
+ %Schema{
+ type: :array,
+ items: %Schema{type: :string, enum: ["home", "notifications"]}
+ },
+ "Array of markers to fetch. If not provided, an empty object will be returned."
+ )
+ ],
+ responses: %{
+ 200 => Operation.response("Marker", "application/json", response()),
+ 403 => Operation.response("Error", "application/json", api_error())
+ }
+ }
+ end
+
+ def upsert_operation do
+ %Operation{
+ tags: ["Markers"],
+ summary: "Save position in timeline",
+ operationId: "MarkerController.upsert",
+ requestBody: Helpers.request_body("Parameters", upsert_request(), required: true),
+ security: [%{"oAuth" => ["follow", "write:blocks"]}],
+ responses: %{
+ 200 => Operation.response("Marker", "application/json", response()),
+ 403 => Operation.response("Error", "application/json", api_error())
+ }
+ }
+ end
+
+ defp marker do
+ %Schema{
+ title: "Marker",
+ description: "Schema for a marker",
+ type: :object,
+ properties: %{
+ last_read_id: %Schema{type: :string},
+ version: %Schema{type: :integer},
+ updated_at: %Schema{type: :string},
+ pleroma: %Schema{
+ type: :object,
+ properties: %{
+ unread_count: %Schema{type: :integer}
+ }
+ }
+ },
+ example: %{
+ "last_read_id" => "35098814",
+ "version" => 361,
+ "updated_at" => "2019-11-26T22:37:25.239Z",
+ "pleroma" => %{"unread_count" => 5}
+ }
+ }
+ end
+
+ defp response do
+ %Schema{
+ title: "MarkersResponse",
+ description: "Response schema for markers",
+ type: :object,
+ properties: %{
+ notifications: %Schema{allOf: [marker()], nullable: true},
+ home: %Schema{allOf: [marker()], nullable: true}
+ },
+ items: %Schema{type: :string},
+ example: %{
+ "notifications" => %{
+ "last_read_id" => "35098814",
+ "version" => 361,
+ "updated_at" => "2019-11-26T22:37:25.239Z",
+ "pleroma" => %{"unread_count" => 0}
+ },
+ "home" => %{
+ "last_read_id" => "103206604258487607",
+ "version" => 468,
+ "updated_at" => "2019-11-26T22:37:25.235Z",
+ "pleroma" => %{"unread_count" => 10}
+ }
+ }
+ }
+ end
+
+ defp upsert_request do
+ %Schema{
+ title: "MarkersUpsertRequest",
+ description: "Request schema for marker upsert",
+ type: :object,
+ properties: %{
+ notifications: %Schema{
+ type: :object,
+ nullable: true,
+ properties: %{
+ last_read_id: %Schema{nullable: true, type: :string}
+ }
+ },
+ home: %Schema{
+ type: :object,
+ nullable: true,
+ properties: %{
+ last_read_id: %Schema{nullable: true, type: :string}
+ }
+ }
+ },
+ example: %{
+ "home" => %{
+ "last_read_id" => "103194548672408537",
+ "version" => 462,
+ "updated_at" => "2019-11-24T19:39:39.337Z"
+ }
+ }
+ }
+ end
+
+ defp api_error do
+ %Schema{
+ type: :object,
+ properties: %{error: %Schema{type: :string}}
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/media_operation.ex b/lib/pleroma/web/api_spec/operations/media_operation.ex
new file mode 100644
index 000000000..d9c3c42db
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/media_operation.ex
@@ -0,0 +1,132 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.MediaOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Helpers
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+ alias Pleroma.Web.ApiSpec.Schemas.Attachment
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def create_operation do
+ %Operation{
+ tags: ["media"],
+ summary: "Upload media as attachment",
+ description: "Creates an attachment to be used with a new status.",
+ operationId: "MediaController.create",
+ security: [%{"oAuth" => ["write:media"]}],
+ requestBody: Helpers.request_body("Parameters", create_request()),
+ responses: %{
+ 200 => Operation.response("Media", "application/json", Attachment),
+ 401 => Operation.response("Media", "application/json", ApiError),
+ 422 => Operation.response("Media", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp create_request do
+ %Schema{
+ title: "MediaCreateRequest",
+ description: "POST body for creating an attachment",
+ type: :object,
+ required: [:file],
+ properties: %{
+ file: %Schema{
+ type: :string,
+ format: :binary,
+ description: "The file to be attached, using multipart form data."
+ },
+ description: %Schema{
+ type: :string,
+ description: "A plain-text description of the media, for accessibility purposes."
+ },
+ focus: %Schema{
+ type: :string,
+ description: "Two floating points (x,y), comma-delimited, ranging from -1.0 to 1.0."
+ }
+ }
+ }
+ end
+
+ def update_operation do
+ %Operation{
+ tags: ["media"],
+ summary: "Upload media as attachment",
+ description: "Creates an attachment to be used with a new status.",
+ operationId: "MediaController.update",
+ security: [%{"oAuth" => ["write:media"]}],
+ parameters: [id_param()],
+ requestBody: Helpers.request_body("Parameters", update_request()),
+ responses: %{
+ 200 => Operation.response("Media", "application/json", Attachment),
+ 400 => Operation.response("Media", "application/json", ApiError),
+ 401 => Operation.response("Media", "application/json", ApiError),
+ 422 => Operation.response("Media", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp update_request do
+ %Schema{
+ title: "MediaUpdateRequest",
+ description: "POST body for updating an attachment",
+ type: :object,
+ properties: %{
+ file: %Schema{
+ type: :string,
+ format: :binary,
+ description: "The file to be attached, using multipart form data."
+ },
+ description: %Schema{
+ type: :string,
+ description: "A plain-text description of the media, for accessibility purposes."
+ },
+ focus: %Schema{
+ type: :string,
+ description: "Two floating points (x,y), comma-delimited, ranging from -1.0 to 1.0."
+ }
+ }
+ }
+ end
+
+ def show_operation do
+ %Operation{
+ tags: ["media"],
+ summary: "Show Uploaded media attachment",
+ operationId: "MediaController.show",
+ parameters: [id_param()],
+ security: [%{"oAuth" => ["read:media"]}],
+ responses: %{
+ 200 => Operation.response("Media", "application/json", Attachment),
+ 401 => Operation.response("Media", "application/json", ApiError),
+ 422 => Operation.response("Media", "application/json", ApiError)
+ }
+ }
+ end
+
+ def create2_operation do
+ %Operation{
+ tags: ["media"],
+ summary: "Upload media as attachment",
+ description: "Creates an attachment to be used with a new status.",
+ operationId: "MediaController.create2",
+ security: [%{"oAuth" => ["write:media"]}],
+ requestBody: Helpers.request_body("Parameters", create_request()),
+ responses: %{
+ 202 => Operation.response("Media", "application/json", Attachment),
+ 422 => Operation.response("Media", "application/json", ApiError),
+ 500 => Operation.response("Media", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp id_param do
+ Operation.parameter(:id, :path, :string, "The ID of the Attachment entity")
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/notification_operation.ex b/lib/pleroma/web/api_spec/operations/notification_operation.ex
new file mode 100644
index 000000000..f09be64cb
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/notification_operation.ex
@@ -0,0 +1,219 @@
+# 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
+
+ def 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
+ },
+ pleroma: %Schema{
+ type: :object,
+ properties: %{
+ is_seen: %Schema{type: :boolean},
+ is_muted: %Schema{type: :boolean}
+ }
+ }
+ },
+ example: %{
+ "id" => "34975861",
+ "type" => "mention",
+ "created_at" => "2019-11-23T07:49:02.064Z",
+ "account" => Account.schema().example,
+ "status" => Status.schema().example,
+ "pleroma" => %{"is_seen" => false, "is_muted" => false}
+ }
+ }
+ end
+
+ defp notification_type do
+ %Schema{
+ type: :string,
+ enum: [
+ "follow",
+ "favourite",
+ "reblog",
+ "mention",
+ "pleroma:emoji_reaction",
+ "pleroma:chat_mention",
+ "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..97836b2eb
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/pleroma_account_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.PleromaAccountOperation do
+ alias OpenApiSpex.Operation
+ 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 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
+end
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_conversation_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_conversation_operation.ex
new file mode 100644
index 000000000..e885eab20
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/pleroma_conversation_operation.ex
@@ -0,0 +1,106 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.PleromaConversationOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.Conversation
+ alias Pleroma.Web.ApiSpec.Schemas.FlakeID
+ alias Pleroma.Web.ApiSpec.StatusOperation
+
+ import Pleroma.Web.ApiSpec.Helpers
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def show_operation do
+ %Operation{
+ tags: ["Conversations"],
+ summary: "The conversation with the given ID",
+ parameters: [
+ Operation.parameter(:id, :path, :string, "Conversation ID",
+ example: "123",
+ required: true
+ )
+ ],
+ security: [%{"oAuth" => ["read:statuses"]}],
+ operationId: "PleromaAPI.ConversationController.show",
+ responses: %{
+ 200 => Operation.response("Conversation", "application/json", Conversation)
+ }
+ }
+ end
+
+ def statuses_operation do
+ %Operation{
+ tags: ["Conversations"],
+ summary: "Timeline for a given conversation",
+ parameters: [
+ Operation.parameter(:id, :path, :string, "Conversation ID",
+ example: "123",
+ required: true
+ )
+ | pagination_params()
+ ],
+ security: [%{"oAuth" => ["read:statuses"]}],
+ operationId: "PleromaAPI.ConversationController.statuses",
+ responses: %{
+ 200 =>
+ Operation.response(
+ "Array of Statuses",
+ "application/json",
+ StatusOperation.array_of_statuses()
+ )
+ }
+ }
+ end
+
+ def update_operation do
+ %Operation{
+ tags: ["Conversations"],
+ summary: "Update a conversation. Used to change the set of recipients.",
+ parameters: [
+ Operation.parameter(:id, :path, :string, "Conversation ID",
+ example: "123",
+ required: true
+ ),
+ Operation.parameter(
+ :recipients,
+ :query,
+ %Schema{type: :array, items: FlakeID},
+ "A list of ids of users that should receive posts to this conversation. This will replace the current list of recipients, so submit the full list. The owner of owner of the conversation will always be part of the set of recipients, though.",
+ required: true
+ )
+ ],
+ security: [%{"oAuth" => ["write:conversations"]}],
+ operationId: "PleromaAPI.ConversationController.update",
+ responses: %{
+ 200 => Operation.response("Conversation", "application/json", Conversation)
+ }
+ }
+ end
+
+ def mark_as_read_operation do
+ %Operation{
+ tags: ["Conversations"],
+ summary: "Marks all user's conversations as read",
+ security: [%{"oAuth" => ["write:conversations"]}],
+ operationId: "PleromaAPI.ConversationController.mark_as_read",
+ responses: %{
+ 200 =>
+ Operation.response(
+ "Array of Conversations that were marked as read",
+ "application/json",
+ %Schema{
+ type: :array,
+ items: Conversation,
+ example: [Conversation.schema().example]
+ }
+ )
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex
new file mode 100644
index 000000000..b2b4f8713
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex
@@ -0,0 +1,418 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.PleromaEmojiPackOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+
+ import Pleroma.Web.ApiSpec.Helpers
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def remote_operation do
+ %Operation{
+ tags: ["Emoji Packs"],
+ summary: "Make request to another instance for emoji packs list",
+ security: [%{"oAuth" => ["write"]}],
+ parameters: [url_param()],
+ operationId: "PleromaAPI.EmojiPackController.remote",
+ responses: %{
+ 200 => emoji_packs_response(),
+ 500 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def index_operation do
+ %Operation{
+ tags: ["Emoji Packs"],
+ summary: "Lists local custom emoji packs",
+ operationId: "PleromaAPI.EmojiPackController.index",
+ parameters: [
+ Operation.parameter(
+ :page,
+ :query,
+ %Schema{type: :integer, default: 1},
+ "Page"
+ ),
+ Operation.parameter(
+ :page_size,
+ :query,
+ %Schema{type: :integer, default: 50},
+ "Number of emoji packs to return"
+ )
+ ],
+ responses: %{
+ 200 => emoji_packs_response()
+ }
+ }
+ end
+
+ def show_operation do
+ %Operation{
+ tags: ["Emoji Packs"],
+ summary: "Show emoji pack",
+ operationId: "PleromaAPI.EmojiPackController.show",
+ parameters: [
+ name_param(),
+ Operation.parameter(
+ :page,
+ :query,
+ %Schema{type: :integer, default: 1},
+ "Page"
+ ),
+ Operation.parameter(
+ :page_size,
+ :query,
+ %Schema{type: :integer, default: 30},
+ "Number of emoji to return"
+ )
+ ],
+ responses: %{
+ 200 => Operation.response("Emoji Pack", "application/json", emoji_pack()),
+ 400 => Operation.response("Bad Request", "application/json", ApiError),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def archive_operation do
+ %Operation{
+ tags: ["Emoji Packs"],
+ summary: "Requests a local pack archive from the instance",
+ operationId: "PleromaAPI.EmojiPackController.archive",
+ parameters: [name_param()],
+ responses: %{
+ 200 =>
+ Operation.response("Archive file", "application/octet-stream", %Schema{
+ type: :string,
+ format: :binary
+ }),
+ 403 => Operation.response("Forbidden", "application/json", ApiError),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def download_operation do
+ %Operation{
+ tags: ["Emoji Packs"],
+ summary: "Download pack from another instance",
+ operationId: "PleromaAPI.EmojiPackController.download",
+ security: [%{"oAuth" => ["write"]}],
+ requestBody: request_body("Parameters", download_request(), required: true),
+ responses: %{
+ 200 => ok_response(),
+ 500 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp download_request do
+ %Schema{
+ type: :object,
+ required: [:url, :name],
+ properties: %{
+ url: %Schema{
+ type: :string,
+ format: :uri,
+ description: "URL of the instance to download from"
+ },
+ name: %Schema{type: :string, format: :uri, description: "Pack Name"},
+ as: %Schema{type: :string, format: :uri, description: "Save as"}
+ }
+ }
+ end
+
+ def create_operation do
+ %Operation{
+ tags: ["Emoji Packs"],
+ summary: "Create an empty pack",
+ operationId: "PleromaAPI.EmojiPackController.create",
+ security: [%{"oAuth" => ["write"]}],
+ parameters: [name_param()],
+ responses: %{
+ 200 => ok_response(),
+ 400 => Operation.response("Not Found", "application/json", ApiError),
+ 409 => Operation.response("Conflict", "application/json", ApiError),
+ 500 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def delete_operation do
+ %Operation{
+ tags: ["Emoji Packs"],
+ summary: "Delete a custom emoji pack",
+ operationId: "PleromaAPI.EmojiPackController.delete",
+ security: [%{"oAuth" => ["write"]}],
+ parameters: [name_param()],
+ responses: %{
+ 200 => ok_response(),
+ 400 => Operation.response("Bad Request", "application/json", ApiError),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def update_operation do
+ %Operation{
+ tags: ["Emoji Packs"],
+ summary: "Updates (replaces) pack metadata",
+ operationId: "PleromaAPI.EmojiPackController.update",
+ security: [%{"oAuth" => ["write"]}],
+ requestBody: request_body("Parameters", update_request(), required: true),
+ parameters: [name_param()],
+ responses: %{
+ 200 => Operation.response("Metadata", "application/json", metadata()),
+ 400 => Operation.response("Bad Request", "application/json", ApiError)
+ }
+ }
+ end
+
+ def add_file_operation do
+ %Operation{
+ tags: ["Emoji Packs"],
+ summary: "Add new file to the pack",
+ operationId: "PleromaAPI.EmojiPackController.add_file",
+ security: [%{"oAuth" => ["write"]}],
+ requestBody: request_body("Parameters", add_file_request(), required: true),
+ parameters: [name_param()],
+ responses: %{
+ 200 => Operation.response("Files Object", "application/json", files_object()),
+ 400 => Operation.response("Bad Request", "application/json", ApiError),
+ 409 => Operation.response("Conflict", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp add_file_request do
+ %Schema{
+ type: :object,
+ required: [:file],
+ properties: %{
+ file: %Schema{
+ description:
+ "File needs to be uploaded with the multipart request or link to remote file",
+ anyOf: [
+ %Schema{type: :string, format: :binary},
+ %Schema{type: :string, format: :uri}
+ ]
+ },
+ shortcode: %Schema{
+ type: :string,
+ description:
+ "Shortcode for new emoji, must be unique for all emoji. If not sended, shortcode will be taken from original filename."
+ },
+ filename: %Schema{
+ type: :string,
+ description:
+ "New emoji file name. If not specified will be taken from original filename."
+ }
+ }
+ }
+ end
+
+ def update_file_operation do
+ %Operation{
+ tags: ["Emoji Packs"],
+ summary: "Add new file to the pack",
+ operationId: "PleromaAPI.EmojiPackController.update_file",
+ security: [%{"oAuth" => ["write"]}],
+ requestBody: request_body("Parameters", update_file_request(), required: true),
+ parameters: [name_param()],
+ responses: %{
+ 200 => Operation.response("Files Object", "application/json", files_object()),
+ 400 => Operation.response("Bad Request", "application/json", ApiError),
+ 409 => Operation.response("Conflict", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp update_file_request do
+ %Schema{
+ type: :object,
+ required: [:shortcode, :new_shortcode, :new_filename],
+ properties: %{
+ shortcode: %Schema{
+ type: :string,
+ description: "Emoji file shortcode"
+ },
+ new_shortcode: %Schema{
+ type: :string,
+ description: "New emoji file shortcode"
+ },
+ new_filename: %Schema{
+ type: :string,
+ description: "New filename for emoji file"
+ },
+ force: %Schema{
+ type: :boolean,
+ description: "With true value to overwrite existing emoji with new shortcode",
+ default: false
+ }
+ }
+ }
+ end
+
+ def delete_file_operation do
+ %Operation{
+ tags: ["Emoji Packs"],
+ summary: "Delete emoji file from pack",
+ operationId: "PleromaAPI.EmojiPackController.delete_file",
+ security: [%{"oAuth" => ["write"]}],
+ parameters: [
+ name_param(),
+ Operation.parameter(:shortcode, :query, :string, "File shortcode",
+ example: "cofe",
+ required: true
+ )
+ ],
+ responses: %{
+ 200 => Operation.response("Files Object", "application/json", files_object()),
+ 400 => Operation.response("Bad Request", "application/json", ApiError)
+ }
+ }
+ end
+
+ def import_from_filesystem_operation do
+ %Operation{
+ tags: ["Emoji Packs"],
+ summary: "Imports packs from filesystem",
+ operationId: "PleromaAPI.EmojiPackController.import",
+ security: [%{"oAuth" => ["write"]}],
+ responses: %{
+ 200 =>
+ Operation.response("Array of imported pack names", "application/json", %Schema{
+ type: :array,
+ items: %Schema{type: :string}
+ })
+ }
+ }
+ end
+
+ defp name_param do
+ Operation.parameter(:name, :path, :string, "Pack Name", example: "cofe", required: true)
+ end
+
+ defp url_param do
+ Operation.parameter(
+ :url,
+ :query,
+ %Schema{type: :string, format: :uri},
+ "URL of the instance",
+ required: true
+ )
+ end
+
+ defp ok_response do
+ Operation.response("Ok", "application/json", %Schema{type: :string, example: "ok"})
+ end
+
+ defp emoji_packs_response do
+ Operation.response(
+ "Object with pack names as keys and pack contents as values",
+ "application/json",
+ %Schema{
+ type: :object,
+ additionalProperties: emoji_pack(),
+ example: %{
+ "emojos" => emoji_pack().example
+ }
+ }
+ )
+ end
+
+ defp emoji_pack do
+ %Schema{
+ title: "EmojiPack",
+ type: :object,
+ properties: %{
+ files: files_object(),
+ pack: %Schema{
+ type: :object,
+ properties: %{
+ license: %Schema{type: :string},
+ homepage: %Schema{type: :string, format: :uri},
+ description: %Schema{type: :string},
+ "can-download": %Schema{type: :boolean},
+ "share-files": %Schema{type: :boolean},
+ "download-sha256": %Schema{type: :string}
+ }
+ }
+ },
+ example: %{
+ "files" => %{"emacs" => "emacs.png", "guix" => "guix.png"},
+ "pack" => %{
+ "license" => "Test license",
+ "homepage" => "https://pleroma.social",
+ "description" => "Test description",
+ "can-download" => true,
+ "share-files" => true,
+ "download-sha256" => "57482F30674FD3DE821FF48C81C00DA4D4AF1F300209253684ABA7075E5FC238"
+ }
+ }
+ }
+ end
+
+ defp files_object do
+ %Schema{
+ type: :object,
+ additionalProperties: %Schema{type: :string},
+ description: "Object with emoji names as keys and filenames as values"
+ }
+ end
+
+ defp update_request do
+ %Schema{
+ type: :object,
+ properties: %{
+ metadata: %Schema{
+ type: :object,
+ description: "Metadata to replace the old one",
+ properties: %{
+ license: %Schema{type: :string},
+ homepage: %Schema{type: :string, format: :uri},
+ description: %Schema{type: :string},
+ "fallback-src": %Schema{
+ type: :string,
+ format: :uri,
+ description: "Fallback url to download pack from"
+ },
+ "fallback-src-sha256": %Schema{
+ type: :string,
+ description: "SHA256 encoded for fallback pack archive"
+ },
+ "share-files": %Schema{type: :boolean, description: "Is pack allowed for sharing?"}
+ }
+ }
+ }
+ }
+ end
+
+ defp metadata do
+ %Schema{
+ type: :object,
+ properties: %{
+ license: %Schema{type: :string},
+ homepage: %Schema{type: :string, format: :uri},
+ description: %Schema{type: :string},
+ "fallback-src": %Schema{
+ type: :string,
+ format: :uri,
+ description: "Fallback url to download pack from"
+ },
+ "fallback-src-sha256": %Schema{
+ type: :string,
+ description: "SHA256 encoded for fallback pack archive"
+ },
+ "share-files": %Schema{type: :boolean, description: "Is pack allowed for sharing?"}
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_mascot_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_mascot_operation.ex
new file mode 100644
index 000000000..8c5f37ea6
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/pleroma_mascot_operation.ex
@@ -0,0 +1,79 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.PleromaMascotOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+
+ import Pleroma.Web.ApiSpec.Helpers
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def show_operation do
+ %Operation{
+ tags: ["Mascot"],
+ summary: "Gets user mascot image",
+ security: [%{"oAuth" => ["read:accounts"]}],
+ operationId: "PleromaAPI.MascotController.show",
+ responses: %{
+ 200 => Operation.response("Mascot", "application/json", mascot())
+ }
+ }
+ end
+
+ def update_operation do
+ %Operation{
+ tags: ["Mascot"],
+ summary: "Set/clear user avatar image",
+ description:
+ "Behaves exactly the same as `POST /api/v1/upload`. Can only accept images - any attempt to upload non-image files will be met with `HTTP 415 Unsupported Media Type`.",
+ operationId: "PleromaAPI.MascotController.update",
+ requestBody:
+ request_body(
+ "Parameters",
+ %Schema{
+ type: :object,
+ properties: %{
+ file: %Schema{type: :string, format: :binary}
+ }
+ },
+ required: true
+ ),
+ security: [%{"oAuth" => ["write:accounts"]}],
+ responses: %{
+ 200 => Operation.response("Mascot", "application/json", mascot()),
+ 415 => Operation.response("Unsupported Media Type", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp mascot do
+ %Schema{
+ type: :object,
+ properties: %{
+ id: %Schema{type: :string},
+ url: %Schema{type: :string, format: :uri},
+ type: %Schema{type: :string},
+ pleroma: %Schema{
+ type: :object,
+ properties: %{
+ mime_type: %Schema{type: :string}
+ }
+ }
+ },
+ example: %{
+ "id" => "abcdefg",
+ "url" => "https://pleroma.example.org/media/abcdefg.png",
+ "type" => "image",
+ "pleroma" => %{
+ "mime_type" => "image/png"
+ }
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_notification_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_notification_operation.ex
new file mode 100644
index 000000000..b0c8db863
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/pleroma_notification_operation.ex
@@ -0,0 +1,48 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.PleromaNotificationOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.NotificationOperation
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+
+ import Pleroma.Web.ApiSpec.Helpers
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def mark_as_read_operation do
+ %Operation{
+ tags: ["Notifications"],
+ summary: "Mark notifications as read. Query parameters are mutually exclusive.",
+ requestBody:
+ request_body("Parameters", %Schema{
+ type: :object,
+ properties: %{
+ id: %Schema{type: :integer, description: "A single notification ID to read"},
+ max_id: %Schema{type: :integer, description: "Read all notifications up to this ID"}
+ }
+ }),
+ security: [%{"oAuth" => ["write:notifications"]}],
+ operationId: "PleromaAPI.NotificationController.mark_as_read",
+ responses: %{
+ 200 =>
+ Operation.response(
+ "A Notification or array of Motifications",
+ "application/json",
+ %Schema{
+ anyOf: [
+ %Schema{type: :array, items: NotificationOperation.notification()},
+ NotificationOperation.notification()
+ ]
+ }
+ ),
+ 400 => Operation.response("Bad Request", "application/json", ApiError)
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex
new file mode 100644
index 000000000..85a22aa0b
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex
@@ -0,0 +1,102 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Reference
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.Account
+ alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
+
+ import Pleroma.Web.ApiSpec.Helpers
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def create_operation do
+ %Operation{
+ tags: ["Scrobbles"],
+ summary: "Creates a new Listen activity for an account",
+ security: [%{"oAuth" => ["write"]}],
+ operationId: "PleromaAPI.ScrobbleController.create",
+ requestBody: request_body("Parameters", create_request(), requried: true),
+ responses: %{
+ 200 => Operation.response("Scrobble", "application/json", scrobble())
+ }
+ }
+ end
+
+ def index_operation do
+ %Operation{
+ tags: ["Scrobbles"],
+ summary: "Requests a list of current and recent Listen activities for an account",
+ operationId: "PleromaAPI.ScrobbleController.index",
+ parameters: [
+ %Reference{"$ref": "#/components/parameters/accountIdOrNickname"} | pagination_params()
+ ],
+ security: [%{"oAuth" => ["read"]}],
+ responses: %{
+ 200 =>
+ Operation.response("Array of Scrobble", "application/json", %Schema{
+ type: :array,
+ items: scrobble()
+ })
+ }
+ }
+ end
+
+ defp create_request do
+ %Schema{
+ type: :object,
+ required: [:title],
+ properties: %{
+ title: %Schema{type: :string, description: "The title of the media playing"},
+ album: %Schema{type: :string, description: "The album of the media playing"},
+ artist: %Schema{type: :string, description: "The artist of the media playing"},
+ length: %Schema{type: :integer, description: "The length of the media playing"},
+ visibility: %Schema{
+ allOf: [VisibilityScope],
+ default: "public",
+ description: "Scrobble visibility"
+ }
+ },
+ example: %{
+ "title" => "Some Title",
+ "artist" => "Some Artist",
+ "album" => "Some Album",
+ "length" => 180_000
+ }
+ }
+ end
+
+ defp scrobble do
+ %Schema{
+ type: :object,
+ properties: %{
+ id: %Schema{type: :string},
+ account: Account,
+ title: %Schema{type: :string, description: "The title of the media playing"},
+ album: %Schema{type: :string, description: "The album of the media playing"},
+ artist: %Schema{type: :string, description: "The artist of the media playing"},
+ length: %Schema{
+ type: :integer,
+ description: "The length of the media playing",
+ nullable: true
+ },
+ created_at: %Schema{type: :string, format: :"date-time"}
+ },
+ example: %{
+ "id" => "1234",
+ "account" => Account.schema().example,
+ "title" => "Some Title",
+ "artist" => "Some Artist",
+ "album" => "Some Album",
+ "length" => 180_000,
+ "created_at" => "2019-09-28T12:40:45.000Z"
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/poll_operation.ex b/lib/pleroma/web/api_spec/operations/poll_operation.ex
new file mode 100644
index 000000000..e15c7dc95
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/poll_operation.ex
@@ -0,0 +1,76 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.PollOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+ alias Pleroma.Web.ApiSpec.Schemas.FlakeID
+ alias Pleroma.Web.ApiSpec.Schemas.Poll
+
+ import Pleroma.Web.ApiSpec.Helpers
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def show_operation do
+ %Operation{
+ tags: ["Polls"],
+ summary: "View a poll",
+ security: [%{"oAuth" => ["read:statuses"]}],
+ parameters: [id_param()],
+ operationId: "PollController.show",
+ responses: %{
+ 200 => Operation.response("Poll", "application/json", Poll),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def vote_operation do
+ %Operation{
+ tags: ["Polls"],
+ summary: "Vote on a poll",
+ parameters: [id_param()],
+ operationId: "PollController.vote",
+ requestBody: vote_request(),
+ security: [%{"oAuth" => ["write:statuses"]}],
+ responses: %{
+ 200 => Operation.response("Poll", "application/json", Poll),
+ 422 => Operation.response("Error", "application/json", ApiError),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp id_param do
+ Operation.parameter(:id, :path, FlakeID, "Poll ID",
+ example: "123",
+ required: true
+ )
+ end
+
+ defp vote_request do
+ request_body(
+ "Parameters",
+ %Schema{
+ type: :object,
+ properties: %{
+ choices: %Schema{
+ type: :array,
+ items: %Schema{type: :integer},
+ description: "Array of own votes containing index for each option (starting from 0)"
+ }
+ },
+ required: [:choices]
+ },
+ required: true,
+ example: %{
+ "choices" => [0, 1, 2]
+ }
+ )
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/report_operation.ex b/lib/pleroma/web/api_spec/operations/report_operation.ex
new file mode 100644
index 000000000..b9b4c4f79
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/report_operation.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.ReportOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Helpers
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+ alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ 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,
+ nullable: true,
+ items: %Schema{type: :string},
+ description: "Array of Statuses to attach to the report, for context"
+ },
+ comment: %Schema{
+ type: :string,
+ nullable: true,
+ description: "Reason for the report"
+ },
+ forward: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ 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..169c36d87
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/search_operation.ex
@@ -0,0 +1,208 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.SearchOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.AccountOperation
+ alias Pleroma.Web.ApiSpec.Schemas.Account
+ alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
+ alias Pleroma.Web.ApiSpec.Schemas.FlakeID
+ alias Pleroma.Web.ApiSpec.Schemas.Status
+ alias Pleroma.Web.ApiSpec.Schemas.Tag
+
+ import Pleroma.Web.ApiSpec.Helpers
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ # Note: `with_relationships` param is not supported (PleromaFE uses this op for autocomplete)
+ def account_search_operation do
+ %Operation{
+ tags: ["Search"],
+ summary: "Search for matching accounts by username or display name",
+ operationId: "SearchController.account_search",
+ parameters: [
+ Operation.parameter(:q, :query, %Schema{type: :string}, "What to search for",
+ required: true
+ ),
+ Operation.parameter(
+ :limit,
+ :query,
+ %Schema{type: :integer, default: 40},
+ "Maximum number of results"
+ ),
+ Operation.parameter(
+ :resolve,
+ :query,
+ %Schema{allOf: [BooleanLike], default: false},
+ "Attempt WebFinger lookup. Use this when `q` is an exact address."
+ ),
+ Operation.parameter(
+ :following,
+ :query,
+ %Schema{allOf: [BooleanLike], default: false},
+ "Only include accounts that the user is following"
+ )
+ ],
+ responses: %{
+ 200 =>
+ Operation.response(
+ "Array of Account",
+ "application/json",
+ AccountOperation.array_of_accounts()
+ )
+ }
+ }
+ end
+
+ def search_operation do
+ %Operation{
+ tags: ["Search"],
+ summary: "Search results",
+ security: [%{"oAuth" => ["read:search"]}],
+ operationId: "SearchController.search",
+ deprecated: true,
+ parameters: [
+ Operation.parameter(
+ :account_id,
+ :query,
+ FlakeID,
+ "If provided, statuses returned will be authored only by this account"
+ ),
+ Operation.parameter(
+ :type,
+ :query,
+ %Schema{type: :string, enum: ["accounts", "hashtags", "statuses"]},
+ "Search type"
+ ),
+ Operation.parameter(:q, :query, %Schema{type: :string}, "The search query", required: true),
+ Operation.parameter(
+ :resolve,
+ :query,
+ %Schema{allOf: [BooleanLike], default: false},
+ "Attempt WebFinger lookup"
+ ),
+ Operation.parameter(
+ :following,
+ :query,
+ %Schema{allOf: [BooleanLike], default: false},
+ "Only include accounts that the user is following"
+ ),
+ Operation.parameter(
+ :offset,
+ :query,
+ %Schema{type: :integer},
+ "Offset"
+ ),
+ with_relationships_param() | pagination_params()
+ ],
+ responses: %{
+ 200 => Operation.response("Results", "application/json", results())
+ }
+ }
+ end
+
+ def search2_operation do
+ %Operation{
+ tags: ["Search"],
+ summary: "Search results",
+ security: [%{"oAuth" => ["read:search"]}],
+ operationId: "SearchController.search2",
+ parameters: [
+ Operation.parameter(
+ :account_id,
+ :query,
+ FlakeID,
+ "If provided, statuses returned will be authored only by this account"
+ ),
+ Operation.parameter(
+ :type,
+ :query,
+ %Schema{type: :string, enum: ["accounts", "hashtags", "statuses"]},
+ "Search type"
+ ),
+ Operation.parameter(:q, :query, %Schema{type: :string}, "What to search for",
+ required: true
+ ),
+ Operation.parameter(
+ :resolve,
+ :query,
+ %Schema{allOf: [BooleanLike], default: false},
+ "Attempt WebFinger lookup"
+ ),
+ Operation.parameter(
+ :following,
+ :query,
+ %Schema{allOf: [BooleanLike], default: false},
+ "Only include accounts that the user is following"
+ ),
+ with_relationships_param() | pagination_params()
+ ],
+ responses: %{
+ 200 => Operation.response("Results", "application/json", results2())
+ }
+ }
+ end
+
+ defp results2 do
+ %Schema{
+ title: "SearchResults",
+ type: :object,
+ properties: %{
+ accounts: %Schema{
+ type: :array,
+ items: Account,
+ description: "Accounts which match the given query"
+ },
+ statuses: %Schema{
+ type: :array,
+ items: Status,
+ description: "Statuses which match the given query"
+ },
+ hashtags: %Schema{
+ type: :array,
+ items: Tag,
+ description: "Hashtags which match the given query"
+ }
+ },
+ example: %{
+ "accounts" => [Account.schema().example],
+ "statuses" => [Status.schema().example],
+ "hashtags" => [Tag.schema().example]
+ }
+ }
+ end
+
+ defp results do
+ %Schema{
+ title: "SearchResults",
+ type: :object,
+ properties: %{
+ accounts: %Schema{
+ type: :array,
+ items: Account,
+ description: "Accounts which match the given query"
+ },
+ statuses: %Schema{
+ type: :array,
+ items: Status,
+ description: "Statuses which match the given query"
+ },
+ hashtags: %Schema{
+ type: :array,
+ items: %Schema{type: :string},
+ description: "Hashtags which match the given query"
+ }
+ },
+ example: %{
+ "accounts" => [Account.schema().example],
+ "statuses" => [Status.schema().example],
+ "hashtags" => ["cofe"]
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex
new file mode 100644
index 000000000..5bd4619d5
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/status_operation.ex
@@ -0,0 +1,519 @@
+# 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 => status_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. Please note that you have to use the link headers to paginate this. You can not build the query parameters yourself.",
+ operationId: "StatusController.favourites",
+ parameters: pagination_params(),
+ security: [%{"oAuth" => ["read:favourites"]}],
+ responses: %{
+ 200 => Operation.response("Array of Statuses", "application/json", array_of_statuses())
+ }
+ }
+ end
+
+ def bookmarks_operation do
+ %Operation{
+ tags: ["Statuses"],
+ summary: "Bookmarked statuses",
+ description: "Statuses the user has bookmarked",
+ operationId: "StatusController.bookmarks",
+ parameters: pagination_params(),
+ security: [%{"oAuth" => ["read:bookmarks"]}],
+ responses: %{
+ 200 => Operation.response("Array of Statuses", "application/json", array_of_statuses())
+ }
+ }
+ end
+
+ def array_of_statuses do
+ %Schema{type: :array, items: Status, example: [Status.schema().example]}
+ end
+
+ defp create_request do
+ %Schema{
+ title: "StatusCreateRequest",
+ type: :object,
+ properties: %{
+ status: %Schema{
+ type: :string,
+ nullable: true,
+ description:
+ "Text content of the status. If `media_ids` is provided, this becomes optional. Attaching a `poll` is optional while `status` is provided."
+ },
+ media_ids: %Schema{
+ nullable: true,
+ type: :array,
+ items: %Schema{type: :string},
+ description: "Array of Attachment ids to be attached as media."
+ },
+ poll: %Schema{
+ nullable: true,
+ type: :object,
+ required: [:options],
+ properties: %{
+ options: %Schema{
+ type: :array,
+ items: %Schema{type: :string},
+ description: "Array of possible answers. Must be provided with `poll[expires_in]`."
+ },
+ expires_in: %Schema{
+ type: :integer,
+ nullable: true,
+ description:
+ "Duration the poll should be open, in seconds. Must be provided with `poll[options]`"
+ },
+ multiple: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Allow multiple choices?"
+ },
+ hide_totals: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Hide vote counts until the poll ends?"
+ }
+ }
+ },
+ in_reply_to_id: %Schema{
+ nullable: true,
+ allOf: [FlakeID],
+ description: "ID of the status being replied to, if status is a reply"
+ },
+ sensitive: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Mark status and attached media as sensitive?"
+ },
+ spoiler_text: %Schema{
+ type: :string,
+ nullable: true,
+ description:
+ "Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field."
+ },
+ scheduled_at: %Schema{
+ type: :string,
+ format: :"date-time",
+ nullable: true,
+ description:
+ "ISO 8601 Datetime at which to schedule a status. Providing this paramter will cause ScheduledStatus to be returned instead of Status. Must be at least 5 minutes in the future."
+ },
+ language: %Schema{
+ type: :string,
+ nullable: true,
+ description: "ISO 639 language code for this status."
+ },
+ # Pleroma-specific properties:
+ preview: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description:
+ "If set to `true` the post won't be actually posted, but the status entitiy would still be rendered back. This could be useful for previewing rich text/custom emoji, for example"
+ },
+ content_type: %Schema{
+ type: :string,
+ nullable: true,
+ description:
+ "The MIME type of the status, it is transformed into HTML by the backend. You can get the list of the supported MIME types with the nodeinfo endpoint."
+ },
+ to: %Schema{
+ type: :array,
+ nullable: true,
+ items: %Schema{type: :string},
+ description:
+ "A list of nicknames (like `lain@soykaf.club` or `lain` on the local server) that will be used to determine who is going to be addressed by this post. Using this will disable the implicit addressing by mentioned names in the `status` body, only the people in the `to` list will be addressed. The normal rules for for post visibility are not affected by this and will still apply"
+ },
+ visibility: %Schema{
+ nullable: true,
+ anyOf: [
+ VisibilityScope,
+ %Schema{type: :string, description: "`list:LIST_ID`", example: "LIST:123"}
+ ],
+ description:
+ "Visibility of the posted status. Besides standard MastoAPI values (`direct`, `private`, `unlisted` or `public`) it can be used to address a List by setting it to `list:LIST_ID`"
+ },
+ expires_in: %Schema{
+ nullable: true,
+ type: :integer,
+ description:
+ "The number of seconds the posted activity should expire in. When a posted activity expires it will be deleted from the server, and a delete request for it will be federated. This needs to be longer than an hour."
+ },
+ in_reply_to_conversation_id: %Schema{
+ nullable: true,
+ type: :string,
+ description:
+ "Will reply to a given conversation, addressing only the people who are part of the recipient set of that conversation. Sets the visibility to `direct`."
+ }
+ },
+ example: %{
+ "status" => "What time is it?",
+ "sensitive" => "false",
+ "poll" => %{
+ "options" => ["Cofe", "Adventure"],
+ "expires_in" => 420
+ }
+ }
+ }
+ end
+
+ def id_param do
+ Operation.parameter(:id, :path, FlakeID, "Status ID",
+ example: "9umDrYheeY451cQnEe",
+ required: true
+ )
+ end
+
+ defp status_response do
+ Operation.response("Status", "application/json", Status)
+ end
+
+ defp context do
+ %Schema{
+ title: "StatusContext",
+ description:
+ "Represents the tree around a given status. Used for reconstructing threads of statuses.",
+ type: :object,
+ required: [:ancestors, :descendants],
+ properties: %{
+ ancestors: array_of_statuses(),
+ descendants: array_of_statuses()
+ },
+ example: %{
+ "ancestors" => [Status.schema().example],
+ "descendants" => [Status.schema().example]
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/subscription_operation.ex b/lib/pleroma/web/api_spec/operations/subscription_operation.ex
new file mode 100644
index 000000000..775dd795d
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/subscription_operation.ex
@@ -0,0 +1,232 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.SubscriptionOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Helpers
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+ alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
+ alias Pleroma.Web.ApiSpec.Schemas.PushSubscription
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def create_operation do
+ %Operation{
+ tags: ["Push Subscriptions"],
+ summary: "Subscribe to push notifications",
+ description:
+ "Add a Web Push API subscription to receive notifications. Each access token can have one push subscription. If you create a new subscription, the old subscription is deleted.",
+ operationId: "SubscriptionController.create",
+ security: [%{"oAuth" => ["push"]}],
+ requestBody: Helpers.request_body("Parameters", create_request(), required: true),
+ responses: %{
+ 200 => Operation.response("Push Subscription", "application/json", PushSubscription),
+ 400 => Operation.response("Error", "application/json", ApiError),
+ 403 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def show_operation do
+ %Operation{
+ tags: ["Push Subscriptions"],
+ summary: "Get current subscription",
+ description: "View the PushSubscription currently associated with this access token.",
+ operationId: "SubscriptionController.show",
+ security: [%{"oAuth" => ["push"]}],
+ responses: %{
+ 200 => Operation.response("Push Subscription", "application/json", PushSubscription),
+ 403 => Operation.response("Error", "application/json", ApiError),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def update_operation do
+ %Operation{
+ tags: ["Push Subscriptions"],
+ summary: "Change types of notifications",
+ description:
+ "Updates the current push subscription. Only the data part can be updated. To change fundamentals, a new subscription must be created instead.",
+ operationId: "SubscriptionController.update",
+ security: [%{"oAuth" => ["push"]}],
+ requestBody: Helpers.request_body("Parameters", update_request(), required: true),
+ responses: %{
+ 200 => Operation.response("Push Subscription", "application/json", PushSubscription),
+ 403 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def delete_operation do
+ %Operation{
+ tags: ["Push Subscriptions"],
+ summary: "Remove current subscription",
+ description: "Removes the current Web Push API subscription.",
+ operationId: "SubscriptionController.delete",
+ security: [%{"oAuth" => ["push"]}],
+ responses: %{
+ 200 => Operation.response("Empty object", "application/json", %Schema{type: :object}),
+ 403 => Operation.response("Error", "application/json", ApiError),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp create_request do
+ %Schema{
+ title: "SubscriptionCreateRequest",
+ description: "POST body for creating a push subscription",
+ type: :object,
+ properties: %{
+ subscription: %Schema{
+ type: :object,
+ properties: %{
+ endpoint: %Schema{
+ type: :string,
+ description: "Endpoint URL that is called when a notification event occurs."
+ },
+ keys: %Schema{
+ type: :object,
+ properties: %{
+ p256dh: %Schema{
+ type: :string,
+ description:
+ "User agent public key. Base64 encoded string of public key of ECDH key using `prime256v1` curve."
+ },
+ auth: %Schema{
+ type: :string,
+ description: "Auth secret. Base64 encoded string of 16 bytes of random data."
+ }
+ },
+ required: [:p256dh, :auth]
+ }
+ },
+ required: [:endpoint, :keys]
+ },
+ data: %Schema{
+ nullable: true,
+ type: :object,
+ properties: %{
+ alerts: %Schema{
+ nullable: true,
+ type: :object,
+ properties: %{
+ follow: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Receive follow notifications?"
+ },
+ favourite: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Receive favourite notifications?"
+ },
+ reblog: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Receive reblog notifications?"
+ },
+ mention: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Receive mention notifications?"
+ },
+ poll: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Receive poll notifications?"
+ },
+ "pleroma:chat_mention": %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Receive chat notifications?"
+ }
+ }
+ }
+ }
+ }
+ },
+ required: [:subscription],
+ example: %{
+ "subscription" => %{
+ "endpoint" => "https://example.com/example/1234",
+ "keys" => %{
+ "auth" => "8eDyX_uCN0XRhSbY5hs7Hg==",
+ "p256dh" =>
+ "BCIWgsnyXDv1VkhqL2P7YRBvdeuDnlwAPT2guNhdIoW3IP7GmHh1SMKPLxRf7x8vJy6ZFK3ol2ohgn_-0yP7QQA="
+ }
+ },
+ "data" => %{
+ "alerts" => %{
+ "follow" => true,
+ "mention" => true,
+ "poll" => false
+ }
+ }
+ }
+ }
+ end
+
+ defp update_request do
+ %Schema{
+ title: "SubscriptionUpdateRequest",
+ type: :object,
+ properties: %{
+ data: %Schema{
+ nullable: true,
+ type: :object,
+ properties: %{
+ alerts: %Schema{
+ nullable: true,
+ type: :object,
+ properties: %{
+ follow: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Receive follow notifications?"
+ },
+ favourite: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Receive favourite notifications?"
+ },
+ reblog: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Receive reblog notifications?"
+ },
+ mention: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Receive mention notifications?"
+ },
+ poll: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Receive poll notifications?"
+ }
+ }
+ }
+ }
+ }
+ },
+ example: %{
+ "data" => %{
+ "alerts" => %{
+ "follow" => true,
+ "favourite" => true,
+ "reblog" => true,
+ "mention" => true,
+ "poll" => true
+ }
+ }
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/timeline_operation.ex b/lib/pleroma/web/api_spec/operations/timeline_operation.ex
new file mode 100644
index 000000000..8e19bace7
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/timeline_operation.ex
@@ -0,0 +1,191 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.TimelineOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+ alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
+ alias Pleroma.Web.ApiSpec.Schemas.Status
+ alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
+
+ import Pleroma.Web.ApiSpec.Helpers
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def home_operation do
+ %Operation{
+ tags: ["Timelines"],
+ summary: "Home timeline",
+ description: "View statuses from followed users",
+ security: [%{"oAuth" => ["read:statuses"]}],
+ parameters: [
+ local_param(),
+ with_muted_param(),
+ exclude_visibilities_param(),
+ reply_visibility_param() | pagination_params()
+ ],
+ operationId: "TimelineController.home",
+ responses: %{
+ 200 => Operation.response("Array of Status", "application/json", array_of_statuses())
+ }
+ }
+ end
+
+ def direct_operation do
+ %Operation{
+ tags: ["Timelines"],
+ summary: "Direct timeline",
+ description:
+ "View statuses with a “direct” privacy, from your account or in your notifications",
+ deprecated: true,
+ parameters: [with_muted_param() | pagination_params()],
+ security: [%{"oAuth" => ["read:statuses"]}],
+ operationId: "TimelineController.direct",
+ responses: %{
+ 200 => Operation.response("Array of Status", "application/json", array_of_statuses())
+ }
+ }
+ end
+
+ def public_operation do
+ %Operation{
+ tags: ["Timelines"],
+ summary: "Public timeline",
+ security: [%{"oAuth" => ["read:statuses"]}],
+ parameters: [
+ local_param(),
+ only_media_param(),
+ with_muted_param(),
+ exclude_visibilities_param(),
+ reply_visibility_param() | pagination_params()
+ ],
+ operationId: "TimelineController.public",
+ responses: %{
+ 200 => Operation.response("Array of Status", "application/json", array_of_statuses()),
+ 401 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def hashtag_operation do
+ %Operation{
+ tags: ["Timelines"],
+ summary: "Hashtag timeline",
+ description: "View public statuses containing the given hashtag",
+ security: [%{"oAuth" => ["read:statuses"]}],
+ parameters: [
+ Operation.parameter(
+ :tag,
+ :path,
+ %Schema{type: :string},
+ "Content of a #hashtag, not including # symbol.",
+ required: true
+ ),
+ Operation.parameter(
+ :any,
+ :query,
+ %Schema{type: :array, items: %Schema{type: :string}},
+ "Statuses that also includes any of these tags"
+ ),
+ Operation.parameter(
+ :all,
+ :query,
+ %Schema{type: :array, items: %Schema{type: :string}},
+ "Statuses that also includes all of these tags"
+ ),
+ Operation.parameter(
+ :none,
+ :query,
+ %Schema{type: :array, items: %Schema{type: :string}},
+ "Statuses that do not include these tags"
+ ),
+ local_param(),
+ only_media_param(),
+ with_muted_param(),
+ exclude_visibilities_param() | pagination_params()
+ ],
+ operationId: "TimelineController.hashtag",
+ responses: %{
+ 200 => Operation.response("Array of Status", "application/json", array_of_statuses())
+ }
+ }
+ end
+
+ def list_operation do
+ %Operation{
+ tags: ["Timelines"],
+ summary: "List timeline",
+ description: "View statuses in the given list timeline",
+ security: [%{"oAuth" => ["read:lists"]}],
+ parameters: [
+ Operation.parameter(
+ :list_id,
+ :path,
+ %Schema{type: :string},
+ "Local ID of the list in the database",
+ required: true
+ ),
+ with_muted_param(),
+ exclude_visibilities_param() | pagination_params()
+ ],
+ operationId: "TimelineController.list",
+ responses: %{
+ 200 => Operation.response("Array of Status", "application/json", array_of_statuses())
+ }
+ }
+ end
+
+ defp array_of_statuses do
+ %Schema{
+ title: "ArrayOfStatuses",
+ type: :array,
+ items: Status,
+ example: [Status.schema().example]
+ }
+ end
+
+ defp local_param do
+ Operation.parameter(
+ :local,
+ :query,
+ %Schema{allOf: [BooleanLike], default: false},
+ "Show only local statuses?"
+ )
+ end
+
+ defp with_muted_param do
+ Operation.parameter(:with_muted, :query, BooleanLike, "Includeactivities by muted users")
+ end
+
+ defp exclude_visibilities_param do
+ Operation.parameter(
+ :exclude_visibilities,
+ :query,
+ %Schema{type: :array, items: VisibilityScope},
+ "Exclude the statuses with the given visibilities"
+ )
+ end
+
+ defp reply_visibility_param do
+ Operation.parameter(
+ :reply_visibility,
+ :query,
+ %Schema{type: :string, enum: ["following", "self"]},
+ "Filter replies. Possible values: without parameter (default) shows all replies, `following` - replies directed to you or users you follow, `self` - replies directed to you."
+ )
+ end
+
+ defp only_media_param do
+ Operation.parameter(
+ :only_media,
+ :query,
+ %Schema{allOf: [BooleanLike], default: false},
+ "Show only statuses with media attached?"
+ )
+ end
+end
diff --git a/lib/pleroma/web/api_spec/render_error.ex b/lib/pleroma/web/api_spec/render_error.ex
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..ca79f0747
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/account.ex
@@ -0,0 +1,220 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.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,
+ description: "whether the user allows automatically follow moved following accounts"
+ },
+ background_image: %Schema{type: :string, nullable: true, format: :uri},
+ chat_token: %Schema{type: :string},
+ confirmation_pending: %Schema{
+ type: :boolean,
+ description:
+ "whether the user account is waiting on email confirmation to be activated"
+ },
+ hide_favorites: %Schema{type: :boolean},
+ hide_followers_count: %Schema{
+ type: :boolean,
+ description: "whether the user has follower stat hiding enabled"
+ },
+ hide_followers: %Schema{
+ type: :boolean,
+ description: "whether the user has follower hiding enabled"
+ },
+ hide_follows_count: %Schema{
+ type: :boolean,
+ description: "whether the user has follow stat hiding enabled"
+ },
+ hide_follows: %Schema{
+ type: :boolean,
+ description: "whether the user has follow hiding enabled"
+ },
+ is_admin: %Schema{
+ type: :boolean,
+ description: "whether the user is an admin of the local instance"
+ },
+ is_moderator: %Schema{
+ type: :boolean,
+ description: "whether the user is a moderator of the local instance"
+ },
+ skip_thread_containment: %Schema{type: :boolean},
+ tags: %Schema{
+ type: :array,
+ items: %Schema{type: :string},
+ description:
+ "List of tags being used for things like extra roles or moderation(ie. marking all media as nsfw all)."
+ },
+ unread_conversation_count: %Schema{
+ type: :integer,
+ description: "The count of unread conversations. Only returned to the account owner."
+ },
+ notification_settings: %Schema{
+ type: :object,
+ properties: %{
+ block_from_strangers: %Schema{type: :boolean},
+ hide_notification_contents: %Schema{type: :boolean}
+ }
+ },
+ relationship: AccountRelationship,
+ settings_store: %Schema{
+ type: :object,
+ description:
+ "A generic map of settings for frontends. Opaque to the backend. Only returned in `verify_credentials` and `update_credentials`"
+ },
+ accepts_chat_messages: %Schema{type: :boolean, nullable: true},
+ favicon: %Schema{
+ type: :string,
+ format: :uri,
+ nullable: true,
+ description: "Favicon image of the user's instance"
+ }
+ }
+ },
+ source: %Schema{
+ type: :object,
+ properties: %{
+ fields: %Schema{type: :array, items: AccountField},
+ note: %Schema{
+ type: :string,
+ description:
+ "Plaintext version of the bio without formatting applied by the backend, used for editing the bio."
+ },
+ privacy: VisibilityScope,
+ sensitive: %Schema{type: :boolean},
+ pleroma: %Schema{
+ type: :object,
+ properties: %{
+ actor_type: ActorType,
+ discoverable: %Schema{
+ type: :boolean,
+ description:
+ "whether the user allows discovery of the account in search results and other services."
+ },
+ no_rich_text: %Schema{
+ type: :boolean,
+ description:
+ "whether the HTML tags for rich-text formatting are stripped from all statuses requested from the API."
+ },
+ show_role: %Schema{
+ type: :boolean,
+ description:
+ "whether the user wants their role (e.g admin, moderator) to be shown"
+ }
+ }
+ }
+ }
+ }
+ },
+ 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,
+ "accepts_chat_messages" => true,
+ "chat_token" =>
+ "SFMyNTY.g3QAAAACZAAEZGF0YW0AAAASOXRLaTNlc2JHN09RZ1oyOTIwZAAGc2lnbmVkbgYARNplS3EB.Mb_Iaqew2bN1I1o79B_iP7encmVCpTKC4OtHZRxdjKc",
+ "unread_conversation_count" => 0,
+ "tags" => [],
+ "notification_settings" => %{
+ "block_from_strangers" => false,
+ "hide_notification_contents" => 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..c6edf6d36
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/attachment.ex
@@ -0,0 +1,68 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Schemas.Attachment do
+ alias OpenApiSpex.Schema
+
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "Attachment",
+ description: "Represents a file or media attachment that can be added to a status.",
+ type: :object,
+ requried: [:id, :url, :preview_url],
+ properties: %{
+ id: %Schema{type: :string, description: "The ID of the attachment in the database."},
+ url: %Schema{
+ type: :string,
+ format: :uri,
+ description: "The location of the original full-size attachment"
+ },
+ remote_url: %Schema{
+ type: :string,
+ format: :uri,
+ description:
+ "The location of the full-size original attachment on the remote website. String (URL), or null if the attachment is local",
+ nullable: true
+ },
+ preview_url: %Schema{
+ type: :string,
+ format: :uri,
+ description: "The location of a scaled-down preview of the attachment"
+ },
+ text_url: %Schema{
+ type: :string,
+ format: :uri,
+ description: "A shorter URL for the attachment"
+ },
+ description: %Schema{
+ type: :string,
+ nullable: true,
+ description:
+ "Alternate text that describes what is in the media attachment, to be used for the visually impaired or when media attachments do not load"
+ },
+ type: %Schema{
+ type: :string,
+ enum: ["image", "video", "audio", "unknown"],
+ description: "The type of the attachment"
+ },
+ pleroma: %Schema{
+ type: :object,
+ properties: %{
+ mime_type: %Schema{type: :string, description: "mime type of the attachment"}
+ }
+ }
+ },
+ example: %{
+ id: "1638338801",
+ type: "image",
+ url: "someurl",
+ remote_url: "someurl",
+ preview_url: "someurl",
+ text_url: "someurl",
+ description: nil,
+ pleroma: %{mime_type: "image/png"}
+ }
+ })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/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/chat.ex b/lib/pleroma/web/api_spec/schemas/chat.ex
new file mode 100644
index 000000000..b4986b734
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/chat.ex
@@ -0,0 +1,75 @@
+# 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.Chat do
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.ChatMessage
+
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "Chat",
+ description: "Response schema for a Chat",
+ type: :object,
+ properties: %{
+ id: %Schema{type: :string},
+ account: %Schema{type: :object},
+ unread: %Schema{type: :integer},
+ last_message: ChatMessage,
+ updated_at: %Schema{type: :string, format: :"date-time"}
+ },
+ example: %{
+ "account" => %{
+ "pleroma" => %{
+ "is_admin" => false,
+ "confirmation_pending" => false,
+ "hide_followers_count" => false,
+ "is_moderator" => false,
+ "hide_favorites" => true,
+ "ap_id" => "https://dontbulling.me/users/lain",
+ "hide_follows_count" => false,
+ "hide_follows" => false,
+ "background_image" => nil,
+ "skip_thread_containment" => false,
+ "hide_followers" => false,
+ "relationship" => %{},
+ "tags" => []
+ },
+ "avatar" =>
+ "https://dontbulling.me/media/065a4dd3c6740dab13ff9c71ec7d240bb9f8be9205c9e7467fb2202117da1e32.jpg",
+ "following_count" => 0,
+ "header_static" => "https://originalpatchou.li/images/banner.png",
+ "source" => %{
+ "sensitive" => false,
+ "note" => "lain",
+ "pleroma" => %{
+ "discoverable" => false,
+ "actor_type" => "Person"
+ },
+ "fields" => []
+ },
+ "statuses_count" => 1,
+ "locked" => false,
+ "created_at" => "2020-04-16T13:40:15.000Z",
+ "display_name" => "lain",
+ "fields" => [],
+ "acct" => "lain@dontbulling.me",
+ "id" => "9u6Qw6TAZANpqokMkK",
+ "emojis" => [],
+ "avatar_static" =>
+ "https://dontbulling.me/media/065a4dd3c6740dab13ff9c71ec7d240bb9f8be9205c9e7467fb2202117da1e32.jpg",
+ "username" => "lain",
+ "followers_count" => 0,
+ "header" => "https://originalpatchou.li/images/banner.png",
+ "bot" => false,
+ "note" => "lain",
+ "url" => "https://dontbulling.me/users/lain"
+ },
+ "id" => "1",
+ "unread" => 2,
+ "last_message" => ChatMessage.schema().example(),
+ "updated_at" => "2020-04-21T15:06:45.000Z"
+ }
+ })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/chat_message.ex b/lib/pleroma/web/api_spec/schemas/chat_message.ex
new file mode 100644
index 000000000..bbf2a4427
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/chat_message.ex
@@ -0,0 +1,74 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessage do
+ alias OpenApiSpex.Schema
+
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "ChatMessage",
+ description: "Response schema for a ChatMessage",
+ nullable: true,
+ type: :object,
+ properties: %{
+ id: %Schema{type: :string},
+ account_id: %Schema{type: :string, description: "The Mastodon API id of the actor"},
+ chat_id: %Schema{type: :string},
+ content: %Schema{type: :string, nullable: true},
+ created_at: %Schema{type: :string, format: :"date-time"},
+ emojis: %Schema{type: :array},
+ attachment: %Schema{type: :object, nullable: true},
+ 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"}
+ }
+ }
+ },
+ example: %{
+ "account_id" => "someflakeid",
+ "chat_id" => "1",
+ "content" => "hey you again",
+ "created_at" => "2020-04-21T15:06:45.000Z",
+ "card" => nil,
+ "emojis" => [
+ %{
+ "static_url" => "https://dontbulling.me/emoji/Firefox.gif",
+ "visible_in_picker" => false,
+ "shortcode" => "firefox",
+ "url" => "https://dontbulling.me/emoji/Firefox.gif"
+ }
+ ],
+ "id" => "14",
+ "attachment" => nil
+ }
+ })
+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..947e42890
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/status.ex
@@ -0,0 +1,334 @@
+# 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"},
+ text: %Schema{
+ type: :string,
+ description: "Original unformatted content in plain text",
+ nullable: true
+ },
+ 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"
+ },
+ parent_visible: %Schema{
+ type: :boolean,
+ description: "`true` if the parent post is visible to the user"
+ }
+ }
+ },
+ 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/ldap_authenticator.ex b/lib/pleroma/web/auth/ldap_authenticator.ex
index f63a66c03..402ab428b 100644
--- a/lib/pleroma/web/auth/ldap_authenticator.ex
+++ b/lib/pleroma/web/auth/ldap_authenticator.ex
@@ -28,10 +28,6 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do
%User{} = user <- ldap_user(name, password) do
{:ok, user}
else
- {:error, {:ldap_connection_error, _}} ->
- # When LDAP is unavailable, try default authenticator
- @base.get_user(conn)
-
{:ldap, _} ->
@base.get_user(conn)
@@ -92,7 +88,7 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do
user
_ ->
- register_user(connection, base, uid, name, password)
+ register_user(connection, base, uid, name)
end
error ->
@@ -100,34 +96,31 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do
end
end
- defp register_user(connection, base, uid, name, password) do
+ defp register_user(connection, base, uid, name) do
case :eldap.search(connection, [
{:base, to_charlist(base)},
{:filter, :eldap.equalityMatch(to_charlist(uid), to_charlist(name))},
{:scope, :eldap.wholeSubtree()},
- {:attributes, ['mail', 'email']},
{:timeout, @search_timeout}
]) do
{:ok, {:eldap_search_result, [{:eldap_entry, _, attributes}], _}} ->
- with {_, [mail]} <- List.keyfind(attributes, 'mail', 0) do
- params = %{
- email: :erlang.list_to_binary(mail),
- name: name,
- nickname: name,
- password: password,
- password_confirmation: password
- }
-
- changeset = User.register_changeset(%User{}, params)
-
- case User.register(changeset) do
- {:ok, user} -> user
- error -> error
+ params = %{
+ name: name,
+ nickname: name,
+ password: nil
+ }
+
+ params =
+ case List.keyfind(attributes, 'mail', 0) do
+ {_, [mail]} -> Map.put_new(params, :email, :erlang.list_to_binary(mail))
+ _ -> params
end
- else
- _ ->
- Logger.error("Could not find LDAP attribute mail: #{inspect(attributes)}")
- {:error, :ldap_registration_missing_attributes}
+
+ changeset = User.register_changeset_ldap(%User{}, params)
+
+ case User.register(changeset) do
+ {:ok, user} -> user
+ error -> error
end
error ->
diff --git a/lib/pleroma/web/auth/pleroma_authenticator.ex b/lib/pleroma/web/auth/pleroma_authenticator.ex
index cb09664ce..200ca03dc 100644
--- a/lib/pleroma/web/auth/pleroma_authenticator.ex
+++ b/lib/pleroma/web/auth/pleroma_authenticator.ex
@@ -16,11 +16,12 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do
def get_user(%Plug.Conn{} = conn) do
with {:ok, {name, password}} <- fetch_credentials(conn),
{_, %User{} = user} <- {:user, fetch_user(name)},
- {_, true} <- {:checkpw, AuthenticationPlug.checkpw(password, user.password_hash)} do
+ {_, true} <- {:checkpw, AuthenticationPlug.checkpw(password, user.password_hash)},
+ {:ok, user} <- AuthenticationPlug.maybe_update_password(user, password) do
{:ok, user}
else
- error ->
- {:error, error}
+ {:error, _reason} = error -> error
+ error -> {:error, error}
end
end
diff --git a/lib/pleroma/web/auth/totp_authenticator.ex b/lib/pleroma/web/auth/totp_authenticator.ex
new file mode 100644
index 000000000..1794e407c
--- /dev/null
+++ b/lib/pleroma/web/auth/totp_authenticator.ex
@@ -0,0 +1,45 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Auth.TOTPAuthenticator do
+ alias Pleroma.MFA
+ alias Pleroma.MFA.TOTP
+ alias Pleroma.Plugs.AuthenticationPlug
+ alias Pleroma.User
+
+ @doc "Verify code or check backup code."
+ @spec verify(String.t(), User.t()) ::
+ {:ok, :pass} | {:error, :invalid_token | :invalid_secret_and_token}
+ def verify(
+ token,
+ %User{
+ multi_factor_authentication_settings:
+ %{enabled: true, totp: %{secret: secret, confirmed: true}} = _
+ } = _user
+ )
+ when is_binary(token) and byte_size(token) > 0 do
+ TOTP.validate_token(secret, token)
+ end
+
+ def verify(_, _), do: {:error, :invalid_token}
+
+ @spec verify_recovery_code(User.t(), String.t()) ::
+ {:ok, :pass} | {:error, :invalid_token}
+ def verify_recovery_code(
+ %User{multi_factor_authentication_settings: %{enabled: true, backup_codes: codes}} = user,
+ code
+ )
+ when is_list(codes) and is_binary(code) do
+ hash_code = Enum.find(codes, fn hash -> AuthenticationPlug.checkpw(code, hash) end)
+
+ if hash_code do
+ MFA.invalidate_backup_code(user, hash_code)
+ {:ok, :pass}
+ else
+ {:error, :invalid_token}
+ end
+ end
+
+ def verify_recovery_code(_, _), do: {:error, :invalid_token}
+end
diff --git a/lib/pleroma/web/chat_channel.ex b/lib/pleroma/web/chat_channel.ex
index 38ec774f7..3b1469c19 100644
--- a/lib/pleroma/web/chat_channel.ex
+++ b/lib/pleroma/web/chat_channel.ex
@@ -4,8 +4,10 @@
defmodule Pleroma.Web.ChatChannel do
use Phoenix.Channel
+
alias Pleroma.User
alias Pleroma.Web.ChatChannel.ChatChannelState
+ alias Pleroma.Web.MastodonAPI.AccountView
def join("chat:public", _message, socket) do
send(self(), :after_join)
@@ -22,8 +24,9 @@ defmodule Pleroma.Web.ChatChannel do
if String.length(text) in 1..Pleroma.Config.get([:instance, :chat_limit]) do
author = User.get_cached_by_nickname(user_name)
- author = Pleroma.Web.MastodonAPI.AccountView.render("show.json", user: author)
- message = ChatChannelState.add_message(%{text: text, author: author})
+ author_json = AccountView.render("show.json", user: author, skip_visibility_check: true)
+
+ message = ChatChannelState.add_message(%{text: text, author: author_json})
broadcast!(socket, "new_msg", message)
end
diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex
index c4356f93b..f849b2e01 100644
--- a/lib/pleroma/web/common_api/activity_draft.ex
+++ b/lib/pleroma/web/common_api/activity_draft.ex
@@ -58,16 +58,16 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
end
defp put_params(draft, params) do
- params = Map.put_new(params, "in_reply_to_status_id", params["in_reply_to_id"])
+ params = Map.put_new(params, :in_reply_to_status_id, params[:in_reply_to_id])
%__MODULE__{draft | params: params}
end
- defp status(%{params: %{"status" => status}} = draft) do
+ defp status(%{params: %{status: status}} = draft) do
%__MODULE__{draft | status: String.trim(status)}
end
defp summary(%{params: params} = draft) do
- %__MODULE__{draft | summary: Map.get(params, "spoiler_text", "")}
+ %__MODULE__{draft | summary: Map.get(params, :spoiler_text, "")}
end
defp full_payload(%{status: status, summary: summary} = draft) do
@@ -84,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
@@ -182,17 +186,25 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
draft.poll
)
|> Map.put("emoji", emoji)
+ |> Map.put("source", draft.status)
%__MODULE__{draft | object: object}
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
defp changes(draft) do
direct? = draft.visibility == "direct"
+ additional = %{"cc" => draft.cc, "directMessage" => direct?}
+
+ additional =
+ case draft.expires_at do
+ %NaiveDateTime{} = expires_at -> Map.put(additional, "expires_at", expires_at)
+ _ -> additional
+ end
changes =
%{
@@ -200,7 +212,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
actor: draft.user,
context: draft.context,
object: draft.object,
- additional: %{"cc" => draft.cc, "directMessage" => direct?}
+ additional: additional
}
|> Utils.maybe_add_list_data(draft.user, draft.visibility)
diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex
index 74e9e8cfa..5ad2b91c2 100644
--- a/lib/pleroma/web/common_api/common_api.ex
+++ b/lib/pleroma/web/common_api/common_api.ex
@@ -6,13 +6,14 @@ defmodule Pleroma.Web.CommonAPI do
alias Pleroma.Activity
alias Pleroma.ActivityExpiration
alias Pleroma.Conversation.Participation
- alias Pleroma.FollowingRelationship
- alias Pleroma.Notification
+ alias Pleroma.Formatter
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
@@ -20,14 +21,92 @@ defmodule Pleroma.Web.CommonAPI do
import Pleroma.Web.CommonAPI.Utils
require Pleroma.Constants
+ require Logger
+
+ def block(blocker, blocked) do
+ with {:ok, block_data, _} <- Builder.block(blocker, blocked),
+ {:ok, block, _} <- Pipeline.common_pipeline(block_data, local: true) do
+ {:ok, block}
+ end
+ end
+
+ def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do
+ with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]),
+ :ok <- validate_chat_content_length(content, !!maybe_attachment),
+ {_, {:ok, chat_message_data, _meta}} <-
+ {:build_object,
+ Builder.chat_message(
+ user,
+ recipient.ap_id,
+ content |> format_chat_content,
+ attachment: maybe_attachment
+ )},
+ {_, {:ok, create_activity_data, _meta}} <-
+ {:build_create_activity, Builder.create(user, chat_message_data, [recipient.ap_id])},
+ {_, {:ok, %Activity{} = activity, _meta}} <-
+ {:common_pipeline,
+ Pipeline.common_pipeline(create_activity_data,
+ local: true
+ )} do
+ {:ok, activity}
+ end
+ end
+
+ defp format_chat_content(nil), do: nil
+
+ defp format_chat_content(content) do
+ {text, _, _} =
+ content
+ |> Formatter.html_escape("text/plain")
+ |> Formatter.linkify()
+ |> (fn {text, mentions, tags} ->
+ {String.replace(text, ~r/\r?\n/, "<br>"), mentions, tags}
+ end).()
+
+ text
+ end
+
+ defp validate_chat_content_length(_, true), do: :ok
+ defp validate_chat_content_length(nil, false), do: {:error, :no_content}
+
+ defp validate_chat_content_length(content, _) do
+ if String.length(content) <= Pleroma.Config.get([:instance, :chat_limit]) do
+ :ok
+ else
+ {:error, :content_too_long}
+ end
+ end
+
+ def unblock(blocker, blocked) do
+ with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},
+ {:ok, unblock_data, _} <- Builder.undo(blocker, block),
+ {:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do
+ {:ok, unblock}
+ else
+ {:fetch_block, nil} ->
+ if User.blocks?(blocker, blocked) do
+ User.unblock(blocker, blocked)
+ {:ok, :no_activity}
+ else
+ {:error, :not_blocking}
+ end
+
+ e ->
+ e
+ end
+ end
def follow(follower, followed) do
timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
- with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
- {:ok, activity} <- ActivityPub.follow(follower, followed),
+ with {:ok, follow_data, _} <- Builder.follow(follower, followed),
+ {:ok, activity, _} <- Pipeline.common_pipeline(follow_data, local: true),
{:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
- {:ok, follower, followed, activity}
+ if activity.data["state"] == "reject" do
+ {:error, :rejected}
+ else
+ {:ok, follower, followed, activity}
+ end
end
end
@@ -41,98 +120,140 @@ defmodule Pleroma.Web.CommonAPI do
def accept_follow_request(follower, followed) do
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, :follow_accept),
- {:ok, _activity} <-
- ActivityPub.accept(%{
- to: [follower.ap_id],
- actor: followed,
- object: follow_activity.data["id"],
- type: "Accept"
- }) do
+ {:ok, accept_data, _} <- Builder.accept(followed, follow_activity),
+ {:ok, _activity, _} <- Pipeline.common_pipeline(accept_data, local: true) do
{:ok, follower}
end
end
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, :follow_reject),
- {:ok, _notifications} <- Notification.dismiss(follow_activity),
- {:ok, _activity} <-
- ActivityPub.reject(%{
- to: [follower.ap_id],
- actor: followed,
- object: follow_activity.data["id"],
- type: "Reject"
- }) do
+ {:ok, reject_data, _} <- Builder.reject(followed, follow_activity),
+ {:ok, _activity, _} <- Pipeline.common_pipeline(reject_data, local: true) do
{:ok, follower}
end
end
def delete(activity_id, user) do
- with {_, %Activity{data: %{"object" => _}} = activity} <-
- {:find_activity, Activity.get_by_id_with_object(activity_id)},
- %Object{} = object <- Object.normalize(activity),
+ with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
+ {:find_activity, Activity.get_by_id(activity_id)},
+ {_, %Object{} = object, _} <-
+ {:find_object, Object.normalize(activity, false), activity},
true <- User.superuser?(user) || user.ap_id == object.data["actor"],
- {:ok, _} <- unpin(activity_id, user),
- {:ok, delete} <- ActivityPub.delete(object) do
+ {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
+ {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
{:ok, delete}
else
- {:find_activity, _} -> {:error, :not_found}
- _ -> {:error, dgettext("errors", "Could not delete")}
+ {:find_activity, _} ->
+ {:error, :not_found}
+
+ {:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} ->
+ # We have the create activity, but not the object, it was probably pruned.
+ # Insert a tombstone and try again
+ with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object),
+ {:ok, _tombstone} <- Object.create(tombstone_data) do
+ delete(activity_id, user)
+ else
+ _ ->
+ Logger.error(
+ "Could not insert tombstone for missing object on deletion. Object is #{object}."
+ )
+
+ {:error, dgettext("errors", "Could not delete")}
+ end
+
+ _ ->
+ {:error, dgettext("errors", "Could not delete")}
end
end
def repeat(id, user, params \\ %{}) do
- with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
- {:find_activity, Activity.get_by_id(id)},
- object <- Object.normalize(activity),
- announce_activity <- Utils.get_existing_announce(user.ap_id, object),
- public <- public_announce?(object, params) do
- if announce_activity do
- {:ok, announce_activity, object}
- else
- ActivityPub.announce(user, object, nil, true, public)
- end
+ with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
+ object = %Object{} <- Object.normalize(activity, false),
+ {_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
+ public = public_announce?(object, params),
+ {:ok, announce, _} <- Builder.announce(user, object, public: public),
+ {:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do
+ {:ok, activity}
else
- {:find_activity, _} -> {:error, :not_found}
- _ -> {:error, dgettext("errors", "Could not repeat")}
+ {:existing_announce, %Activity{} = announce} ->
+ {:ok, announce}
+
+ _ ->
+ {:error, :not_found}
end
end
def unrepeat(id, user) do
with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
- {:find_activity, Activity.get_by_id(id)} do
- object = Object.normalize(activity)
- ActivityPub.unannounce(user, object)
+ {:find_activity, Activity.get_by_id(id)},
+ %Object{} = note <- Object.normalize(activity, false),
+ %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
+ {:ok, undo, _} <- Builder.undo(user, announce),
+ {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
+ {:ok, activity}
else
{:find_activity, _} -> {:error, :not_found}
_ -> {:error, dgettext("errors", "Could not unrepeat")}
end
end
- def favorite(id, user) do
- with {_, %Activity{} = activity} <- {:find_activity, Activity.get_by_id(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
- {:find_activity, _} -> {:error, :not_found}
- _ -> {: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, user) do
with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
- {:find_activity, Activity.get_by_id(id)} do
- object = Object.normalize(activity)
- ActivityPub.unlike(user, object)
+ {:find_activity, Activity.get_by_id(id)},
+ %Object{} = note <- Object.normalize(activity, false),
+ %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
+ {:ok, undo, _} <- Builder.undo(user, like),
+ {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
+ {:ok, activity}
else
{:find_activity, _} -> {:error, :not_found}
_ -> {:error, dgettext("errors", "Could not unfavorite")}
@@ -141,8 +262,10 @@ defmodule Pleroma.Web.CommonAPI do
def react_with_emoji(id, user, emoji) do
with %Activity{} = activity <- Activity.get_by_id(id),
- object <- Object.normalize(activity) do
- ActivityPub.react_with_emoji(user, object, emoji)
+ object <- Object.normalize(activity),
+ {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
+ {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
+ {:ok, activity}
else
_ ->
{:error, dgettext("errors", "Could not add reaction emoji")}
@@ -150,8 +273,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")}
@@ -164,18 +289,19 @@ defmodule Pleroma.Web.CommonAPI do
{:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
answer_activities =
Enum.map(choices, fn index ->
- answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
-
- {:ok, activity} =
- ActivityPub.create(%{
- to: answer_data["to"],
- actor: user,
- context: object.data["context"],
- object: answer_data,
- additional: %{"cc" => answer_data["cc"]}
- })
-
- activity
+ {:ok, answer_object, _meta} =
+ Builder.answer(user, object, Enum.at(options, index)["name"])
+
+ {:ok, activity_data, _meta} = Builder.create(user, answer_object, [])
+
+ {:ok, activity, _meta} =
+ activity_data
+ |> Map.put("cc", answer_object["cc"])
+ |> Map.put("context", answer_object["context"])
+ |> Pipeline.common_pipeline(local: true)
+
+ # TODO: Do preload of Pleroma.Object in Pipeline
+ Activity.normalize(activity.data)
end)
object = Object.get_cached_by_ap_id(object.data["id"])
@@ -196,8 +322,13 @@ defmodule Pleroma.Web.CommonAPI do
end
end
- defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
- defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
+ defp get_options_and_max_count(%{data: %{"anyOf" => any_of}})
+ when is_list(any_of) and any_of != [],
+ do: {any_of, Enum.count(any_of)}
+
+ defp get_options_and_max_count(%{data: %{"oneOf" => one_of}})
+ when is_list(one_of) and one_of != [],
+ do: {one_of, 1}
defp normalize_and_validate_choices(choices, object) do
choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
@@ -213,7 +344,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)
@@ -223,11 +354,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
@@ -264,11 +395,14 @@ defmodule Pleroma.Web.CommonAPI do
|> check_expiry_date()
end
- def listen(user, %{"title" => _} = data) do
- with visibility <- data["visibility"] || "public",
- {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
+ def listen(user, data) do
+ visibility = Map.get(data, :visibility, "public")
+
+ with {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
listen_data <-
- Map.take(data, ["album", "artist", "title", "length"])
+ data
+ |> Map.take([:album, :artist, :title, :length])
+ |> Map.new(fn {key, value} -> {to_string(key), value} end)
|> Map.put("type", "Audio")
|> Map.put("to", to)
|> Map.put("cc", cc)
@@ -285,42 +419,12 @@ 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?)
- |> maybe_create_activity_expiration(draft.expires_at)
- end
- end
-
- defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
- with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
- {:ok, activity}
+ ActivityPub.create(draft.changes, draft.preview?)
end
end
- 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, %{ap_id: user_ap_id} = user) do
with %Activity{
actor: ^user_ap_id,
@@ -360,15 +464,16 @@ defmodule Pleroma.Web.CommonAPI do
{:ok, activity}
end
- def thread_muted?(%{id: nil} = _user, _activity), do: false
-
- def thread_muted?(user, activity) do
- ThreadMute.check_muted(user.id, activity.data["context"]) != []
+ def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
+ when is_binary(context) do
+ ThreadMute.exists?(user_id, 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 thread_muted?(_, _), do: false
+
+ 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(),
@@ -376,13 +481,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}
@@ -416,11 +519,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)
@@ -434,7 +537,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 26dcd463c..9d7b24eb2 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -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,17 +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
- 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
@@ -39,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: []
@@ -53,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(
@@ -104,7 +102,8 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
def get_to_and_cc(_user, mentioned_users, inReplyTo, "direct", _) do
- if inReplyTo do
+ # If the OP is a DM already, add the implicit actor.
+ if inReplyTo && Visibility.is_direct?(inReplyTo) do
{Enum.uniq([inReplyTo.data["actor"] | mentioned_users]), []}
else
{mentioned_users, []}
@@ -142,9 +141,9 @@ 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])
+ limits = Config.get([:instance, :poll_limits])
with :ok <- validate_poll_expiration(expires_in, limits),
:ok <- validate_poll_options_amount(options, limits),
@@ -157,7 +156,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 =
@@ -165,7 +164,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|> DateTime.add(expires_in)
|> DateTime.to_iso8601()
- key = if truthy_param?(data["poll"]["multiple"]), do: "anyOf", else: "oneOf"
+ key = if truthy_param?(data.poll[:multiple]), do: "anyOf", else: "oneOf"
poll = %{"type" => "Question", key => option_notes, "closed" => end_time}
{:ok, {poll, emoji}}
@@ -215,7 +214,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|> Map.get("attachment_links", Config.get([:instance, :attachment_links]))
|> truthy_param?()
- content_type = get_content_type(data["content_type"])
+ content_type = get_content_type(data[:content_type])
options =
if visibility == "direct" && Config.get([:instance, :safe_dm_mentions]) do
@@ -397,13 +396,16 @@ defmodule Pleroma.Web.CommonAPI.Utils do
def to_masto_date(_), do: ""
defp shortname(name) do
- if String.length(name) < 30 do
- name
+ with max_length when max_length > 0 <-
+ Config.get([Pleroma.Upload, :filename_display_max_length], 30),
+ true <- String.length(name) > max_length do
+ String.slice(name, 0..max_length) <> "…"
else
- String.slice(name, 0..30) <> "…"
+ _ -> name
end
end
+ @spec confirm_current_password(User.t(), String.t()) :: {:ok, User.t()} | {:error, String.t()}
def confirm_current_password(user, password) do
with %User{local: true} = db_user <- User.get_cached_by_id(user.id),
true <- AuthenticationPlug.checkpw(password, db_user.password_hash) do
@@ -413,19 +415,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
@@ -440,7 +429,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
%Activity{data: %{"to" => _to, "type" => type} = data} = activity
)
when type == "Create" do
- object = Object.normalize(activity)
+ object = Object.normalize(activity, false)
object_data =
cond do
@@ -481,6 +470,8 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|> Enum.map(& &1.ap_id)
recipients ++ subscriber_ids
+ else
+ _e -> recipients
end
end
@@ -492,6 +483,8 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|> User.get_followers()
|> Enum.map(& &1.ap_id)
|> Enum.concat(recipients)
+ else
+ _e -> recipients
end
end
@@ -509,7 +502,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
def make_report_content_html(nil), do: {:ok, {nil, [], []}}
def make_report_content_html(comment) do
- max_size = Pleroma.Config.get([:instance, :max_report_comment_size], 1000)
+ max_size = Config.get([:instance, :max_report_comment_size], 1000)
if String.length(comment) <= max_size do
{:ok, format_input(comment, "text/plain")}
@@ -519,7 +512,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
@@ -554,23 +548,12 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
end
- def make_answer_data(%User{ap_id: ap_id}, object, name) do
- %{
- "type" => "Answer",
- "actor" => ap_id,
- "cc" => [object.data["actor"]],
- "to" => [],
- "name" => name,
- "inReplyTo" => object.data["id"]
- }
- end
-
def validate_character_limit("" = _full_payload, [] = _attachments) do
{:error, dgettext("errors", "Cannot post an empty status without attachments")}
end
def validate_character_limit(full_payload, _attachments) do
- limit = Pleroma.Config.get([:instance, :limit])
+ limit = Config.get([:instance, :limit])
length = String.length(full_payload)
if length <= limit do
diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex
index 1e0491a96..6445966e0 100644
--- a/lib/pleroma/web/controller_helper.ex
+++ b/lib/pleroma/web/controller_helper.ex
@@ -9,8 +9,20 @@ defmodule Pleroma.Web.ControllerHelper do
# 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, _) when status in [204, :no_content] do
+ conn
+ |> put_resp_header("content-type", "application/json")
+ |> send_resp(status, "")
+ end
def json_response(conn, status, json) do
conn
@@ -51,43 +63,44 @@ defmodule Pleroma.Web.ControllerHelper do
end
end
+ @id_keys Pagination.page_keys() -- ["limit", "order"]
+ defp build_pagination_fields(conn, min_id, max_id, extra_params) do
+ params =
+ conn.params
+ |> Map.drop(Map.keys(conn.path_params))
+ |> Map.merge(extra_params)
+ |> Map.drop(@id_keys)
+
+ %{
+ "next" => current_url(conn, Map.put(params, :max_id, max_id)),
+ "prev" => current_url(conn, Map.put(params, :min_id, min_id)),
+ "id" => current_url(conn)
+ }
+ end
+
def get_pagination_fields(conn, activities, extra_params \\ %{}) do
case List.last(activities) do
- %{id: max_id} ->
- params =
- conn.params
- |> Map.drop(Map.keys(conn.path_params))
- |> Map.merge(extra_params)
- |> Map.drop(Pagination.page_keys() -- ["limit", "order"])
+ %{pagination_id: max_id} when not is_nil(max_id) ->
+ %{pagination_id: min_id} =
+ activities
+ |> List.first()
+
+ build_pagination_fields(conn, min_id, max_id, extra_params)
- min_id =
+ %{id: max_id} ->
+ %{id: min_id} =
activities
|> List.first()
- |> Map.get(:id)
-
- fields = %{
- "next" => current_url(conn, Map.put(params, :max_id, max_id)),
- "prev" => current_url(conn, Map.put(params, :min_id, min_id))
- }
-
- # Generating an `id` without already present pagination keys would
- # need a query-restriction with an `q.id >= ^id` or `q.id <= ^id`
- # instead of the `q.id > ^min_id` and `q.id < ^max_id`.
- # This is because we only have ids present inside of the page, while
- # `min_id`, `since_id` and `max_id` requires to know one outside of it.
- if Map.take(conn.params, Pagination.page_keys() -- ["limit", "order"]) != [] do
- Map.put(fields, "id", current_url(conn, conn.params))
- else
- fields
- end
+
+ build_pagination_fields(conn, min_id, max_id, extra_params)
_ ->
%{}
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
+ case Pleroma.User.get_cached_by_id(conn.params.id) do
%Pleroma.User{} = account -> assign(conn, :account, account)
nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
end
@@ -104,7 +117,16 @@ 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)
+ @doc """
+ Returns true if request specifies to include embedded relationships in account objects.
+ May only be used in selected account-related endpoints; has no effect for status- or
+ notification-related endpoints.
+ """
+ # Intended for PleromaFE: https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838
+ def embed_relationships?(params) do
+ # To do once OpenAPI transition mess is over: just `truthy_param?(params[:with_relationships])`
+ params
+ |> Map.get(:with_relationships, params["with_relationships"])
+ |> truthy_param?()
+ end
end
diff --git a/lib/pleroma/web/embed_controller.ex b/lib/pleroma/web/embed_controller.ex
new file mode 100644
index 000000000..f6b8a5ee1
--- /dev/null
+++ b/lib/pleroma/web/embed_controller.ex
@@ -0,0 +1,42 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.EmbedController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Activity
+ alias Pleroma.Object
+ alias Pleroma.User
+
+ alias Pleroma.Web.ActivityPub.Visibility
+
+ plug(:put_layout, :embed)
+
+ def show(conn, %{"id" => id}) do
+ with %Activity{local: true} = activity <-
+ Activity.get_by_id_with_object(id),
+ true <- Visibility.is_public?(activity.object) do
+ {:ok, author} = User.get_or_fetch(activity.object.data["actor"])
+
+ conn
+ |> delete_resp_header("x-frame-options")
+ |> delete_resp_header("content-security-policy")
+ |> render("show.html",
+ activity: activity,
+ author: User.sanitize_html(author),
+ counts: get_counts(activity)
+ )
+ end
+ end
+
+ defp get_counts(%Activity{} = activity) do
+ %Object{data: data} = Object.normalize(activity)
+
+ %{
+ likes: Map.get(data, "like_count", 0),
+ replies: Map.get(data, "repliesCount", 0),
+ announces: Map.get(data, "announcement_count", 0)
+ }
+ end
+end
diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex
index 226d42c2c..8b153763d 100644
--- a/lib/pleroma/web/endpoint.ex
+++ b/lib/pleroma/web/endpoint.ex
@@ -28,6 +28,29 @@ defmodule Pleroma.Web.Endpoint do
}
)
+ # Careful! No `only` restriction here, as we don't know what frontends contain.
+ plug(Pleroma.Plugs.FrontendStatic,
+ at: "/",
+ frontend_type: :primary,
+ gzip: true,
+ cache_control_for_etags: @static_cache_control,
+ headers: %{
+ "cache-control" => @static_cache_control
+ }
+ )
+
+ plug(Plug.Static.IndexHtml, at: "/pleroma/admin/")
+
+ plug(Pleroma.Plugs.FrontendStatic,
+ at: "/pleroma/admin",
+ frontend_type: :admin,
+ gzip: true,
+ cache_control_for_etags: @static_cache_control,
+ headers: %{
+ "cache-control" => @static_cache_control
+ }
+ )
+
# Serve at "/" the static files from "priv/static" directory.
#
# You should set gzip to true if you are running phoenix.digest
@@ -45,8 +68,6 @@ defmodule Pleroma.Web.Endpoint do
}
)
- plug(Plug.Static.IndexHtml, at: "/pleroma/admin/")
-
plug(Plug.Static,
at: "/pleroma/admin/",
from: {:pleroma, "priv/static/adminfe/"}
diff --git a/lib/pleroma/web/fallback_redirect_controller.ex b/lib/pleroma/web/fallback_redirect_controller.ex
index c13518030..431ad5485 100644
--- a/lib/pleroma/web/fallback_redirect_controller.ex
+++ b/lib/pleroma/web/fallback_redirect_controller.ex
@@ -4,9 +4,12 @@
defmodule Fallback.RedirectController do
use Pleroma.Web, :controller
+
require Logger
+
alias Pleroma.User
alias Pleroma.Web.Metadata
+ alias Pleroma.Web.Preload
def api_not_implemented(conn, _params) do
conn
@@ -14,16 +17,7 @@ defmodule Fallback.RedirectController do
|> json(%{error: "Not implemented"})
end
- def redirector(conn, _params, code \\ 200)
-
- # redirect to admin section
- # /pleroma/admin -> /pleroma/admin/
- #
- def redirector(conn, %{"path" => ["pleroma", "admin"]} = _, _code) do
- redirect(conn, to: "/pleroma/admin/")
- end
-
- def redirector(conn, _params, code) do
+ def redirector(conn, _params, code \\ 200) do
conn
|> put_resp_content_type("text/html")
|> send_file(code, index_file_path())
@@ -41,28 +35,33 @@ defmodule Fallback.RedirectController do
def redirector_with_meta(conn, params) do
{:ok, index_content} = File.read(index_file_path())
- tags =
- try do
- Metadata.build_tags(params)
- rescue
- e ->
- Logger.error(
- "Metadata rendering for #{conn.request_path} failed.\n" <>
- Exception.format(:error, e, __STACKTRACE__)
- )
-
- ""
- end
+ tags = build_tags(conn, params)
+ preloads = preload_data(conn, params)
- response = String.replace(index_content, "<!--server-generated-meta-->", tags)
+ response =
+ index_content
+ |> String.replace("<!--server-generated-meta-->", tags <> preloads)
conn
|> put_resp_content_type("text/html")
|> send_resp(200, response)
end
- def index_file_path do
- Pleroma.Plugs.InstanceStatic.file_path("index.html")
+ def redirector_with_preload(conn, %{"path" => ["pleroma", "admin"]}) do
+ redirect(conn, to: "/pleroma/admin/")
+ end
+
+ def redirector_with_preload(conn, params) do
+ {:ok, index_content} = File.read(index_file_path())
+ preloads = preload_data(conn, params)
+
+ response =
+ index_content
+ |> String.replace("<!--server-generated-meta-->", preloads)
+
+ conn
+ |> put_resp_content_type("text/html")
+ |> send_resp(200, response)
end
def registration_page(conn, params) do
@@ -74,4 +73,36 @@ defmodule Fallback.RedirectController do
|> put_status(204)
|> text("")
end
+
+ defp index_file_path do
+ Pleroma.Plugs.InstanceStatic.file_path("index.html")
+ end
+
+ defp build_tags(conn, params) do
+ try do
+ Metadata.build_tags(params)
+ rescue
+ e ->
+ Logger.error(
+ "Metadata rendering for #{conn.request_path} failed.\n" <>
+ Exception.format(:error, e, __STACKTRACE__)
+ )
+
+ ""
+ end
+ end
+
+ defp preload_data(conn, params) do
+ try do
+ Preload.build_tags(conn, params)
+ rescue
+ e ->
+ Logger.error(
+ "Preloading for #{conn.request_path} failed.\n" <>
+ Exception.format(:error, e, __STACKTRACE__)
+ )
+
+ ""
+ end
+ end
end
diff --git a/lib/pleroma/web/federator/federator.ex b/lib/pleroma/web/federator/federator.ex
index fd904ef0a..f5803578d 100644
--- a/lib/pleroma/web/federator/federator.ex
+++ b/lib/pleroma/web/federator/federator.ex
@@ -72,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/feed/feed_view.ex b/lib/pleroma/web/feed/feed_view.ex
index e18adaea8..1ae03e7e2 100644
--- a/lib/pleroma/web/feed/feed_view.ex
+++ b/lib/pleroma/web/feed/feed_view.ex
@@ -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 75c9ea17e..39b2a766a 100644
--- a/lib/pleroma/web/feed/tag_controller.ex
+++ b/lib/pleroma/web/feed/tag_controller.ex
@@ -9,18 +9,16 @@ 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]
-
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"])
+ %{type: ["Create"], tag: tag}
+ |> Pleroma.Maps.put_if_present(: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 59aabb549..9cd334a33 100644
--- a/lib/pleroma/web/feed/user_controller.ex
+++ b/lib/pleroma/web/feed/user_controller.ex
@@ -11,8 +11,6 @@ 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]
-
plug(Pleroma.Plugs.SetFormatPlug when action in [:feed_redirect])
action_fallback(:errors)
@@ -25,7 +23,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 +38,28 @@ defmodule Pleroma.Web.Feed.UserController do
end
def feed(conn, %{"nickname" => nickname} = params) do
- with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do
+ format = get_format(conn)
+
+ format =
+ if format in ["rss", "atom"] do
+ format
+ else
+ "atom"
+ end
+
+ with {_, %User{local: true} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do
activities =
%{
- "type" => ["Create"],
- "actor_id" => user.ap_id
+ type: ["Create"],
+ actor_id: user.ap_id
}
- |> put_in_if_exist("max_id", params["max_id"])
- |> ActivityPub.fetch_public_activities()
+ |> Pleroma.Maps.put_if_present(:max_id, params["max_id"])
+ |> ActivityPub.fetch_public_or_unlisted_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])
@@ -59,6 +71,7 @@ defmodule Pleroma.Web.Feed.UserController do
render_error(conn, :not_found, "Not found")
end
+ def errors(conn, {:fetch_user, %User{local: false}}), do: errors(conn, {:error, :not_found})
def errors(conn, {:fetch_user, nil}), do: errors(conn, {:error, :not_found})
def errors(conn, _) do
diff --git a/lib/pleroma/web/masto_fe_controller.ex b/lib/pleroma/web/masto_fe_controller.ex
index 557cde328..43ec70021 100644
--- a/lib/pleroma/web/masto_fe_controller.ex
+++ b/lib/pleroma/web/masto_fe_controller.ex
@@ -5,19 +5,25 @@
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 not in [:index, :manifest])
+ plug(
+ :skip_plug,
+ [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :manifest
+ )
@doc "GET /web/*path"
def index(%{assigns: %{user: user, token: token}} = conn, _params)
@@ -43,7 +49,7 @@ defmodule Pleroma.Web.MastoFEController do
|> render("manifest.json")
end
- @doc "PUT /api/web/settings"
+ @doc "PUT /api/web/settings: Backend-obscure settings blob for MastoFE, don't parse/reuse elsewhere"
def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
with {:ok, _} <- User.mastodon_settings_update(user, settings) do
json(conn, %{})
diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
index 229d4be28..95d8452df 100644
--- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
@@ -6,32 +6,53 @@ 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]
-
+ only: [
+ add_link_headers: 2,
+ truthy_param?: 1,
+ assign_account_by_id: 2,
+ embed_relationships?: 1,
+ json_response: 3
+ ]
+
+ alias Pleroma.Maps
+ alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Plugs.RateLimiter
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Builder
+ alias Pleroma.Web.ActivityPub.Pipeline
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.OAuth.OAuthController
+ alias Pleroma.Web.OAuth.OAuthView
alias Pleroma.Web.TwitterAPI.TwitterAPI
- plug(:skip_plug, OAuthScopesPlug when action == :identity_proofs)
+ 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)
@@ -50,27 +71,21 @@ 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])
- plug(
- Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
- when action != :create
- )
-
@relationship_actions [:follow, :unfollow]
@needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
plug(
RateLimiter,
- [name: :relation_id_action, params: ["id", "uri"]] when action in @relationship_actions
+ [name: :relation_id_action, params: [:id, :uri]] when action in @relationship_actions
)
plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
@@ -79,37 +94,40 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
- @doc "POST /api/v1/accounts"
- def create(
- %{assigns: %{app: app}} = conn,
- %{"username" => nickname, "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"])
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
+ @doc "POST /api/v1/accounts"
+ def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
with :ok <- validate_email_param(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",
- access_token: token.token,
- scope: app.scopes,
- created_at: Token.Utils.format_created_at(token)
- })
+ :ok <- TwitterAPI.validate_captcha(app, params),
+ {:ok, user} <- TwitterAPI.register_user(params),
+ {_, {:ok, token}} <-
+ {:login, OAuthController.login(user, app, app.scopes)} do
+ json(conn, OAuthView.render("token.json", %{user: user, token: token}))
else
- {:error, errors} -> json_response(conn, :bad_request, errors)
+ {:login, {:account_status, :confirmation_pending}} ->
+ json_response(conn, :ok, %{
+ message: "You have been registered. Please check your email for further instructions.",
+ identifier: "missing_confirmed_email"
+ })
+
+ {:login, {:account_status, :approval_pending}} ->
+ json_response(conn, :ok, %{
+ message:
+ "You have been registered. You'll be able to log in once your account is approved.",
+ identifier: "awaiting_approval"
+ })
+
+ {:login, _} ->
+ json_response(conn, :ok, %{
+ message:
+ "You have been registered. Some post-registration steps may be pending. " <>
+ "Please log in manually.",
+ identifier: "manual_login_required"
+ })
+
+ {:error, error} ->
+ json_response(conn, :bad_request, %{error: error})
end
end
@@ -121,11 +139,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
render_error(conn, :forbidden, "Invalid credentials")
end
- defp validate_email_param(%{"email" => _}), do: :ok
+ 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, %{"error" => "Missing parameters"}}
+ true -> {:error, dgettext("errors", "Missing parameter: %{name}", name: "email")}
_ -> :ok
end
end
@@ -143,8 +161,18 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
@doc "PATCH /api/v1/accounts/update_credentials"
- def update_credentials(%{assigns: %{user: original_user}} = conn, params) do
- user = original_user
+ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _params) do
+ params =
+ params
+ |> Enum.filter(fn {_, value} -> not is_nil(value) end)
+ |> Enum.into(%{})
+
+ # We use an empty string as a special value to reset
+ # avatars, banners, backgrounds
+ user_image_value = fn
+ "" -> {:ok, nil}
+ value -> {:ok, value}
+ end
user_params =
[
@@ -158,56 +186,77 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
:show_role,
:skip_thread_containment,
:allow_following_move,
- :discoverable
+ :discoverable,
+ :accepts_chat_messages
]
|> Enum.reduce(%{}, fn key, acc ->
- add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)})
+ Maps.put_if_present(acc, key, params[key], &{:ok, truthy_param?(&1)})
end)
- |> 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",
+ |> Maps.put_if_present(:name, params[:display_name])
+ |> Maps.put_if_present(:bio, params[:note])
+ |> Maps.put_if_present(:raw_bio, params[:note])
+ |> Maps.put_if_present(:avatar, params[:avatar], user_image_value)
+ |> Maps.put_if_present(:banner, params[:header], user_image_value)
+ |> Maps.put_if_present(:background, params[:pleroma_background_image], user_image_value)
+ |> Maps.put_if_present(
:raw_fields,
+ params[:fields_attributes],
&{: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)
-
- 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)
+ |> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store])
+ |> Maps.put_if_present(:default_scope, params[:default_scope])
+ |> Maps.put_if_present(:default_scope, params["source"]["privacy"])
+ |> Maps.put_if_present(:actor_type, params[:bot], fn bot ->
+ if bot, do: {:ok, "Service"}, else: {:ok, "Person"}
+ end)
+ |> Maps.put_if_present(:actor_type, params[:actor_type])
+
+ # What happens here:
+ #
+ # We want to update the user through the pipeline, but the ActivityPub
+ # update information is not quite enough for this, because this also
+ # contains local settings that don't federate and don't even appear
+ # in the Update activity.
+ #
+ # So we first build the normal local changeset, then apply it to the
+ # user data, but don't persist it. With this, we generate the object
+ # data for our update activity. We feed this and the changeset as meta
+ # inforation into the pipeline, where they will be properly updated and
+ # federated.
+ with changeset <- User.update_changeset(user, user_params),
+ {:ok, unpersisted_user} <- Ecto.Changeset.apply_action(changeset, :update),
+ updated_object <-
+ Pleroma.Web.ActivityPub.UserView.render("user.json", user: unpersisted_user)
+ |> Map.delete("@context"),
+ {:ok, update_data, []} <- Builder.update(user, updated_object),
+ {:ok, _update, _} <-
+ Pipeline.common_pipeline(update_data,
+ local: true,
+ user_update_changeset: changeset
+ ) do
+ render(conn, "show.json",
+ user: unpersisted_user,
+ for: unpersisted_user,
+ with_pleroma_settings: true
+ )
else
_e -> render_error(conn, :forbidden, "Invalid request")
end
end
- 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
- Map.put(map, map_field, new_value)
- else
- _ -> map
- end
- end
-
defp normalize_fields_attributes(fields) 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)
@@ -217,34 +266,56 @@ 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
+ :visible <- User.visible_for(user, for_user) do
render(conn, "show.json", user: user, for: for_user)
else
- _e -> render_error(conn, :not_found, "Can't find user")
+ error -> user_visibility_error(conn, error)
end
end
@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),
+ :visible <- User.visible_for(user, reading_user) do
params =
params
- |> Map.put("tag", params["tagged"])
- |> Map.delete("godmode")
+ |> Map.delete(:tagged)
+ |> 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
+ )
+ else
+ error -> user_visibility_error(conn, error)
+ end
+ end
+
+ defp user_visibility_error(conn, error) do
+ case error do
+ :restrict_unauthenticated ->
+ render_error(conn, :unauthorized, "This API requires an authenticated user")
+
+ _ ->
+ 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)
@@ -254,11 +325,22 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
conn
|> add_link_headers(followers)
- |> render("index.json", for: for_user, users: followers, as: :user)
+ # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
+ |> render("index.json",
+ for: for_user,
+ users: followers,
+ as: :user,
+ embed_relationships: embed_relationships?(params)
+ )
end
@doc "GET /api/v1/accounts/:id/following"
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)
@@ -268,7 +350,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
conn
|> add_link_headers(followers)
- |> render("index.json", for: for_user, users: followers, as: :user)
+ # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
+ |> render("index.json",
+ for: for_user,
+ users: followers,
+ as: :user,
+ embed_relationships: embed_relationships?(params)
+ )
end
@doc "GET /api/v1/accounts/:id/lists"
@@ -282,11 +370,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(%{body_params: params, assigns: %{user: follower, account: followed}} = conn, _) 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})
@@ -295,7 +383,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
@@ -305,10 +393,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})
@@ -326,8 +412,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
@doc "POST /api/v1/accounts/:id/block"
def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
- with {:ok, _user_block} <- User.block(blocker, blocked),
- {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
+ with {:ok, _activity} <- CommonAPI.block(blocker, blocked) do
render(conn, "relationship.json", user: blocker, target: blocked)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
@@ -336,8 +421,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})
@@ -345,14 +429,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
diff --git a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex
index 5e2871f18..a516b6c20 100644
--- a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex
@@ -5,6 +5,7 @@
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 37b389382..753b3db3e 100644
--- a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
@@ -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))
diff --git a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex
index 7c9b11bf1..f35ec3596 100644
--- a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex
@@ -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 d82de1db5..c5f47c5df 100644
--- a/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex
@@ -5,6 +5,16 @@
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 e4156cbe6..9c2d093cd 100644
--- a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
@@ -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,29 @@ 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
+
+ def create(%{assigns: %{user: blocker}} = conn, %{domain: domain}) 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
+
+ def delete(%{assigns: %{user: blocker}} = conn, %{domain: domain}) 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 0a257f604..8af557b61 100644
--- a/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex
@@ -20,6 +20,10 @@ defmodule Pleroma.Web.MastodonAPI.FallbackController do
render_error(conn, :not_found, "Record not found")
end
+ def call(conn, {:error, :forbidden}) do
+ render_error(conn, :forbidden, "Access denied")
+ end
+
def call(conn, {:error, error_message}) do
conn
|> put_status(:bad_request)
diff --git a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
index 7b0b937a2..abbf0ce02 100644
--- a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
@@ -10,6 +10,7 @@ defmodule Pleroma.Web.MastodonAPI.FilterController do
@oauth_read_actions [:show, :index]
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(OAuthScopesPlug, %{scopes: ["read:filters"]} when action in @oauth_read_actions)
plug(
@@ -17,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 1ca86f63f..748b6b475 100644
--- a/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex
@@ -10,6 +10,7 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestController do
alias Pleroma.Web.CommonAPI
plug(:put_view, Pleroma.Web.MastodonAPI.AccountView)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(:assign_follower when action != :index)
action_fallback(:errors)
@@ -21,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 27b5b1a52..d8859731d 100644
--- a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex
@@ -5,6 +5,16 @@
defmodule Pleroma.Web.MastodonAPI.InstanceController do
use Pleroma.Web, :controller
+ plug(OpenApiSpex.Plug.CastAndValidate)
+
+ plug(
+ :skip_plug,
+ [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]
+ when action in [:show, :peers]
+ )
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.InstanceOperation
+
@doc "GET /api/v1/instance"
def show(conn, _params) do
render(conn, "show.json")
diff --git a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex
index dac4daa7b..acdc76fd2 100644
--- a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex
@@ -9,20 +9,17 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
alias Pleroma.User
alias Pleroma.Web.MastodonAPI.AccountView
- plug(:list_by_id_and_user when action not in [:index, :create])
-
- 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 58e8a30c2..85310edfa 100644
--- a/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex
@@ -6,6 +6,8 @@ defmodule Pleroma.Web.MastodonAPI.MarkerController do
use Pleroma.Web, :controller
alias Pleroma.Plugs.OAuthScopesPlug
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
plug(
OAuthScopesPlug,
%{scopes: ["read:statuses"]}
@@ -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 ac8c18f24..e7767de4e 100644
--- a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
@@ -15,9 +15,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
require Logger
- plug(:skip_plug, Pleroma.Plugs.OAuthScopesPlug when action in [:empty_array, :empty_object])
-
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ plug(
+ :skip_plug,
+ [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]
+ when action in [:empty_array, :empty_object]
+ )
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
diff --git a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex
index 2b6f00952..513de279f 100644
--- a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex
@@ -11,19 +11,21 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do
alias Pleroma.Web.ActivityPub.ActivityPub
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(:put_view, Pleroma.Web.MastodonAPI.StatusView)
- plug(OAuthScopesPlug, %{scopes: ["write:media"]})
+ plug(OAuthScopesPlug, %{scopes: ["read:media"]} when action == :show)
+ plug(OAuthScopesPlug, %{scopes: ["write:media"]} when action != :show)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.MediaOperation
@doc "POST /api/v1/media"
- def create(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
+ def create(%{assigns: %{user: user}, body_params: %{file: file} = data} = conn, _) do
with {:ok, object} <-
ActivityPub.upload(
file,
actor: User.ap_id(user),
- description: Map.get(data, "description")
+ description: Map.get(data, :description)
) do
attachment_data = Map.put(object.data, "id", object.id)
@@ -31,11 +33,30 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do
end
end
+ def create(_conn, _data), do: {:error, :bad_request}
+
+ @doc "POST /api/v2/media"
+ def create2(%{assigns: %{user: user}, body_params: %{file: file} = data} = conn, _) do
+ with {:ok, object} <-
+ ActivityPub.upload(
+ file,
+ actor: User.ap_id(user),
+ description: Map.get(data, :description)
+ ) do
+ attachment_data = Map.put(object.data, "id", object.id)
+
+ conn
+ |> put_status(202)
+ |> render("attachment.json", %{attachment: attachment_data})
+ end
+ end
+
+ def create2(_conn, _data), do: {:error, :bad_request}
+
@doc "PUT /api/v1/media/:id"
- def update(%{assigns: %{user: user}} = conn, %{"id" => id, "description" => description})
- when is_binary(description) do
+ def update(%{assigns: %{user: user}, body_params: %{description: description}} = conn, %{id: id}) do
with %Object{} = object <- Object.get_by_id(id),
- true <- Object.authorize_mutation(object, user),
+ :ok <- Object.authorize_access(object, user),
{:ok, %Object{data: data}} <- Object.update_data(object, %{"name" => description}) do
attachment_data = Map.put(data, "id", object.id)
@@ -43,5 +64,17 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do
end
end
- def update(_conn, _data), do: {:error, :bad_request}
+ def update(conn, data), do: show(conn, data)
+
+ @doc "GET /api/v1/media/:id"
+ def show(%{assigns: %{user: user}} = conn, %{id: id}) do
+ with %Object{data: data, id: object_id} = object <- Object.get_by_id(id),
+ :ok <- Object.authorize_access(object, user) do
+ attachment_data = Map.put(data, "id", object_id)
+
+ render(conn, "attachment.json", %{attachment: attachment_data})
+ end
+ end
+
+ def show(_conn, _data), do: {:error, :bad_request}
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
index 0c9218454..e25cef30b 100644
--- a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
@@ -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)
@@ -40,16 +42,32 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
end
end
+ @default_notification_types ~w{
+ mention
+ follow
+ follow_request
+ reblog
+ favourite
+ move
+ pleroma:emoji_reaction
+ }
def index(%{assigns: %{user: user}} = conn, params) do
+ params =
+ Map.new(params, fn {k, v} -> {to_string(k), v} end)
+ |> Map.put_new("include_types", @default_notification_types)
+
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
+ )
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 +84,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 +97,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 d9f894118..db46ffcfc 100644
--- a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex
@@ -15,6 +15,8 @@ defmodule Pleroma.Web.MastodonAPI.PollController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
plug(
OAuthScopesPlug,
%{scopes: ["read:statuses"], fallback: :proceed_unauthenticated} when action == :show
@@ -22,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 f5782be13..405167108 100644
--- a/lib/pleroma/web/mastodon_api/controllers/report_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/report_controller.ex
@@ -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 e1e6bd89b..1719c67ea 100644
--- a/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
@@ -11,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 fcab4ef63..5a983db39 100644
--- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
@@ -17,25 +17,34 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
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
|> put_view(AccountView)
- |> render("index.json", users: accounts, for: user, as: :user)
+ |> render("index.json",
+ users: accounts,
+ for: user,
+ as: :user
+ )
end
def search2(conn, params), do: do_search(:v2, conn, params)
def search(conn, params), do: do_search(:v1, conn, params)
- defp do_search(version, %{assigns: %{user: user}} = conn, %{"q" => query} = params) do
+ defp do_search(version, %{assigns: %{user: user}} = conn, %{q: query} = params) do
+ query = String.trim(query)
options = search_options(params, user)
timeout = Keyword.get(Repo.config(), :timeout, 15_000)
default_values = %{"statuses" => [], "accounts" => [], "hashtags" => []}
@@ -43,7 +52,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,12 +75,13 @@ 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"],
+ resolve: params[:resolve],
+ following: params[:following],
+ limit: params[:limit],
+ offset: params[:offset],
+ type: params[:type],
author: get_author(params),
+ embed_relationships: ControllerHelper.embed_relationships?(params),
for_user: user
]
|> Enum.filter(&elem(&1, 1))
@@ -79,36 +89,90 @@ 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],
+ embed_relationships: options[:embed_relationships]
+ )
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
+ )
end
- defp resource_search(:v2, "hashtags", query, _options) do
+ defp resource_search(:v2, "hashtags", query, options) do
tags_path = Web.base_url() <> "/tag/"
query
- |> prepare_tags()
+ |> prepare_tags(options)
|> Enum.map(fn tag ->
- tag = String.trim_leading(tag, "#")
%{name: tag, url: tags_path <> tag}
end)
end
- defp resource_search(:v1, "hashtags", query, _options) do
- query
- |> prepare_tags()
- |> Enum.map(fn tag -> String.trim_leading(tag, "#") end)
+ defp resource_search(:v1, "hashtags", query, options) do
+ prepare_tags(query, options)
end
- defp prepare_tags(query) do
- query
- |> String.split()
- |> Enum.uniq()
- |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
+ defp prepare_tags(query, options) do
+ tags =
+ query
+ |> preprocess_uri_query()
+ |> String.split(~r/[^#\w]+/u, trim: true)
+ |> Enum.uniq_by(&String.downcase/1)
+
+ explicit_tags = Enum.filter(tags, fn tag -> String.starts_with?(tag, "#") end)
+
+ tags =
+ if Enum.any?(explicit_tags) do
+ explicit_tags
+ else
+ tags
+ end
+
+ tags = Enum.map(tags, fn tag -> String.trim_leading(tag, "#") end)
+
+ tags =
+ if Enum.empty?(explicit_tags) && !options[:skip_joined_tag] do
+ add_joined_tag(tags)
+ else
+ tags
+ end
+
+ Pleroma.Pagination.paginate(tags, options)
+ end
+
+ defp add_joined_tag(tags) do
+ tags
+ |> Kernel.++([joined_tag(tags)])
+ |> Enum.uniq_by(&String.downcase/1)
+ end
+
+ # If `query` is a URI, returns last component of its path, otherwise returns `query`
+ defp preprocess_uri_query(query) do
+ if query =~ ~r/https?:\/\// do
+ query
+ |> String.trim_trailing("/")
+ |> URI.parse()
+ |> Map.get(:path)
+ |> String.split("/")
+ |> Enum.at(-1)
+ else
+ query
+ end
+ end
+
+ defp joined_tag(tags) do
+ tags
+ |> Enum.map(fn tag -> String.capitalize(tag) end)
+ |> Enum.join()
end
defp with_fallback(f, fallback \\ []) do
@@ -121,7 +185,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 5c90065f6..ecfa38489 100644
--- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
@@ -5,7 +5,8 @@
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]
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,19 +80,17 @@ 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(
RateLimiter,
- [name: :status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]]
+ [name: :status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: [:id]]
when action in ~w(reblog unreblog)a
)
plug(
RateLimiter,
- [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]]
+ [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: [:id]]
when action in ~w(favourite unfavourite)a
)
@@ -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,11 @@ 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
+ )
end
@doc """
@@ -119,20 +127,29 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
Creates a scheduled status when `scheduled_at` param is present and it's far enough
"""
def create(
- %{assigns: %{user: user}} = conn,
- %{"status" => _, "scheduled_at" => scheduled_at} = params
- ) 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 +161,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
Creates a regular status
"""
- def create(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
- params = Map.put(params, "in_reply_to_status_id", params["in_reply_to_id"])
+ def create(%{assigns: %{user: user}, body_params: %{status: _} = params} = conn, _) do
+ params = Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
with {:ok, activity} <- CommonAPI.post(user, params) do
try_render(conn, "show.json",
@@ -155,6 +172,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
with_direct_conversation_id: true
)
else
+ {:error, {:reject, message}} ->
+ conn
+ |> put_status(:unprocessable_entity)
+ |> json(%{error: message})
+
{:error, message} ->
conn
|> put_status(:unprocessable_entity)
@@ -162,12 +184,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",
@@ -181,63 +204,68 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "DELETE /api/v1/statuses/:id"
- def delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
- json(conn, %{})
+ def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
+ with %Activity{} = activity <- Activity.get_by_id_with_object(id),
+ {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
+ try_render(conn, "show.json",
+ activity: activity,
+ for: user,
+ with_direct_conversation_id: true,
+ with_source: true
+ )
else
- {:error, :not_found} = e -> e
- _e -> render_error(conn, :forbidden, "Can't delete this post")
+ _e -> {:error, :not_found}
end
end
@doc "POST /api/v1/statuses/:id/reblog"
- def reblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id} = params) do
- with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user, params),
+ def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do
+ with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
%Activity{} = announce <- Activity.normalize(announce.data) do
try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
end
end
@doc "POST /api/v1/statuses/:id/unreblog"
- def unreblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
- with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
- %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
+ def unreblog(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
+ with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
+ %Activity{} = activity <- Activity.get_by_id(activity_id) do
try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
end
end
@doc "POST /api/v1/statuses/:id/favourite"
- def favourite(%{assigns: %{user: user}} = conn, %{"id" => 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),
@@ -247,7 +275,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),
@@ -257,7 +285,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)
@@ -265,7 +293,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)
@@ -274,7 +302,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)
@@ -285,8 +313,9 @@ 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
- with %Activity{} = activity <- Activity.get_by_id_with_object(id),
+ def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
+ with true <- Pleroma.Config.get([:instance, :show_reactions]),
+ %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
users =
@@ -305,7 +334,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}} <-
@@ -337,13 +366,13 @@ 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"], %{
- "blocking_user" => user,
- "user" => user,
- "exclude_id" => activity.id
+ blocking_user: user,
+ user: user,
+ exclude_id: activity.id
})
render(conn, "context.json", activity: activity, activities: activities, user: user)
@@ -351,16 +380,16 @@ 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
+ 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
+ )
end
@doc "GET /api/v1/bookmarks"
@@ -378,6 +407,10 @@ 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
+ )
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 11df6fc4a..34eac97c5 100644
--- a/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex
@@ -6,47 +6,42 @@ 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 c93a43969..f91df9ab7 100644
--- a/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex
@@ -5,11 +5,26 @@
defmodule Pleroma.Web.MastodonAPI.SuggestionController do
use Pleroma.Web, :controller
- alias Pleroma.Plugs.OAuthScopesPlug
-
require Logger
- plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :index)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["read"]} when action == :index)
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def index_operation do
+ %OpenApiSpex.Operation{
+ tags: ["Suggestions"],
+ summary: "Follow suggestions (Not implemented)",
+ operationId: "SuggestionController.index",
+ responses: %{
+ 200 => Pleroma.Web.ApiSpec.Helpers.empty_array_response()
+ }
+ }
+ end
@doc "GET /api/v1/suggestions"
def index(conn, params),
diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
index c3cebd71e..5272790d3 100644
--- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
@@ -6,17 +6,21 @@ 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]
+ alias Pleroma.Config
alias Pleroma.Pagination
+ alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Plugs.RateLimiter
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
- # TODO: Replace with a macro when there is a Phoenix release with
+ 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
- # in it
plug(RateLimiter, [name: :timeline, bucket_name: :direct_timeline] when action == :direct)
plug(RateLimiter, [name: :timeline, bucket_name: :public_timeline] when action == :public)
@@ -27,18 +31,26 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
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.put("type", ["Create", "Announce"])
- |> Map.put("blocking_user", user)
- |> Map.put("muting_user", user)
- |> Map.put("user", user)
+ |> Map.put(:type, ["Create", "Announce"])
+ |> Map.put(:blocking_user, user)
+ |> Map.put(:muting_user, user)
+ |> Map.put(:reply_filtering_user, user)
+ |> Map.put(:announce_filtering_user, user)
+ |> Map.put(:user, user)
activities =
[user.ap_id | User.following(user)]
@@ -47,16 +59,20 @@ 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
+ )
end
# GET /api/v1/timelines/direct
def direct(%{assigns: %{user: user}} = conn, params) do
params =
params
- |> Map.put("type", "Create")
- |> Map.put("blocking_user", user)
- |> Map.put("user", user)
+ |> Map.put(:type, "Create")
+ |> Map.put(:blocking_user, user)
+ |> Map.put(:user, user)
|> Map.put(:visibility, "direct")
activities =
@@ -66,77 +82,110 @@ 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
+ )
+ end
+
+ defp restrict_unauthenticated?(true = _local_only) do
+ Config.restrict_unauthenticated_access?(:timelines, :local)
+ end
+
+ defp restrict_unauthenticated?(_) do
+ Config.restrict_unauthenticated_access?(:timelines, :federated)
end
# GET /api/v1/timelines/public
def public(%{assigns: %{user: user}} = conn, params) do
- local_only = truthy_param?(params["local"])
+ local_only = params[:local]
- 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()
+ if is_nil(user) and restrict_unauthenticated?(local_only) do
+ fail_on_bad_auth(conn)
+ else
+ activities =
+ params
+ |> Map.put(:type, ["Create"])
+ |> 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
+ )
+ end
+ end
- conn
- |> add_link_headers(activities, %{"local" => local_only})
- |> render("index.json", activities: activities, for: user, as: :activity)
+ defp fail_on_bad_auth(conn) do
+ render_error(conn, :unauthorized, "authorization required for timeline view")
end
- def hashtag_fetching(params, user, local_only) do
+ defp hashtag_fetching(params, user, local_only) do
tags =
- [params["tag"], params["any"]]
+ [params[:tag], params[:any]]
|> List.flatten()
|> Enum.uniq()
- |> Enum.filter(& &1)
- |> Enum.map(&String.downcase(&1))
+ |> Enum.reject(&is_nil/1)
+ |> Enum.map(&String.downcase/1)
tag_all =
params
- |> Map.get("all", [])
- |> Enum.map(&String.downcase(&1))
+ |> Map.get(:all, [])
+ |> Enum.map(&String.downcase/1)
tag_reject =
params
- |> Map.get("none", [])
- |> Enum.map(&String.downcase(&1))
+ |> Map.get(:none, [])
+ |> Enum.map(&String.downcase/1)
_activities =
params
- |> Map.put("type", "Create")
- |> Map.put("local_only", local_only)
- |> Map.put("blocking_user", user)
- |> Map.put("muting_user", user)
- |> Map.put("user", user)
- |> Map.put("tag", tags)
- |> Map.put("tag_all", tag_all)
- |> Map.put("tag_reject", tag_reject)
+ |> Map.put(:type, "Create")
+ |> Map.put(:local_only, local_only)
+ |> Map.put(:blocking_user, user)
+ |> Map.put(:muting_user, user)
+ |> Map.put(:user, user)
+ |> Map.put(:tag, tags)
+ |> Map.put(:tag_all, tag_all)
+ |> Map.put(:tag_reject, tag_reject)
|> ActivityPub.fetch_public_activities()
end
# GET /api/v1/timelines/tag/:tag
def hashtag(%{assigns: %{user: user}} = conn, params) do
- local_only = truthy_param?(params["local"])
+ 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)
+ if is_nil(user) and restrict_unauthenticated?(local_only) do
+ fail_on_bad_auth(conn)
+ else
+ activities = hashtag_fetching(params, user, local_only)
+
+ conn
+ |> add_link_headers(activities, %{"local" => local_only})
+ |> render("index.json",
+ activities: activities,
+ for: user,
+ as: :activity
+ )
+ end
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.put("type", "Create")
- |> Map.put("blocking_user", user)
- |> Map.put("user", user)
- |> Map.put("muting_user", user)
+ |> Map.put(:type, "Create")
+ |> Map.put(:blocking_user, user)
+ |> Map.put(:user, user)
+ |> Map.put(:muting_user, user)
# we must filter the following list for the user to avoid leaking statuses the user
# does not actually have permission to see (for more info, peruse security issue #270).
@@ -149,7 +198,11 @@ 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
+ )
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 3fe2be521..694bf5ca8 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api.ex
@@ -6,7 +6,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
import Ecto.Query
import Ecto.Changeset
- alias Pleroma.Activity
alias Pleroma.Notification
alias Pleroma.Pagination
alias Pleroma.ScheduledActivity
@@ -55,6 +54,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 +69,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 +80,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
changeset.changes
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)
+ defp restrict(query, :include_types, %{include_types: mastodon_types = [_ | _]}) do
+ where(query, [n], n.type in ^mastodon_types)
+ end
- query
- |> where([q, a], not fragment("? @> ARRAY[?->>'type']::varchar[]", ^ap_types, a.data))
+ defp restrict(query, :exclude_types, %{exclude_types: mastodon_types = [_ | _]}) do
+ where(query, [n], n.type not in ^mastodon_types)
end
defp restrict(query, :account_ap_id, %{account_ap_id: account_ap_id}) do
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index 6ff84c957..864c0417f 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -5,21 +5,60 @@
defmodule Pleroma.Web.MastodonAPI.AccountView do
use Pleroma.Web, :view
+ 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]
+
+ relationships_opt =
+ cond do
+ Map.has_key?(opts, :relationships) ->
+ opts[:relationships]
+
+ is_nil(reading_user) || !opts[:embed_relationships] ->
+ UserRelationship.view_relationships_option(nil, [])
+
+ true ->
+ UserRelationship.view_relationships_option(reading_user, users)
+ end
+
+ opts =
+ opts
+ |> Map.merge(%{relationships: relationships_opt, as: :user})
+ |> Map.delete(:users)
+
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: %{}
+ @doc """
+ Renders specified user account.
+ :skip_visibility_check option skips visibility check and renders any user (local or remote)
+ regardless of [:pleroma, :restrict_unauthenticated] setting.
+ :for option specifies the requester and can be a User record or nil.
+ Only use `user: user, for: user` when `user` is the actual requester of own profile.
+ """
+ def render("show.json", %{user: _user, skip_visibility_check: true} = opts) do
+ do_render("show.json", opts)
+ end
+
+ def render("show.json", %{user: user, for: for_user_or_nil} = opts) do
+ if User.visible_for(user, for_user_or_nil) == :visible do
+ do_render("show.json", opts)
+ else
+ %{}
+ end
+ end
+
+ def render("show.json", _) do
+ raise "In order to prevent account accessibility issues, " <>
+ ":skip_visibility_check or :for option is required."
end
def render("mention.json", %{user: user}) do
@@ -35,34 +74,107 @@ 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
@@ -86,21 +198,42 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
0
end
- bot = user.actor_type in ["Application", "Service"]
+ bot = user.actor_type == "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, raw_url} ->
+ url = MediaProxy.url(raw_url)
+
%{
- "shortcode" => String.trim(name, ":"),
- "url" => MediaProxy.url(url),
- "static_url" => MediaProxy.url(url),
- "visible_in_picker" => false
+ shortcode: shortcode,
+ url: url,
+ static_url: url,
+ visible_in_picker: false
}
end)
- relationship = render("relationship.json", %{user: opts[:for], target: user})
+ relationship =
+ if opts[:embed_relationships] do
+ render("relationship.json", %{
+ user: opts[:for],
+ target: user,
+ relationships: opts[:relationships]
+ })
+ else
+ %{}
+ end
+
+ favicon =
+ if Pleroma.Config.get([:instances_favicons, :enabled]) do
+ user
+ |> Map.get(:ap_id, "")
+ |> URI.parse()
+ |> URI.merge("/")
+ |> Pleroma.Instances.Instance.get_or_update_favicon()
+ |> MediaProxy.url()
+ else
+ nil
+ end
%{
id: to_string(user.id),
@@ -122,7 +255,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
fields: user.fields,
bot: bot,
source: %{
- note: (user.bio || "") |> String.replace(~r(<br */?>), "\n") |> Pleroma.HTML.strip_tags(),
+ note: user.raw_bio || "",
sensitive: false,
fields: user.raw_fields,
pleroma: %{
@@ -133,6 +266,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
# Pleroma extension
pleroma: %{
+ ap_id: user.ap_id,
confirmation_pending: user.confirmation_pending,
tags: user.tags,
hide_followers_count: user.hide_followers_count,
@@ -142,7 +276,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
hide_favorites: user.hide_favorites,
relationship: relationship,
skip_thread_containment: user.skip_thread_containment,
- background_image: image_url(user.background) |> MediaProxy.url()
+ background_image: image_url(user.background) |> MediaProxy.url(),
+ accepts_chat_messages: user.accepts_chat_messages,
+ favicon: favicon
}
}
|> maybe_put_role(user, opts[:for])
@@ -154,6 +290,7 @@ 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 username_from_nickname(string) when is_binary(string) do
@@ -224,7 +361,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
@@ -251,6 +392,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 d934e2107..e44272c6f 100644
--- a/lib/pleroma/web/mastodon_api/views/app_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/app_view.ex
@@ -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,
@@ -30,10 +45,6 @@ defmodule Pleroma.Web.MastodonAPI.AppView do
defp with_vapid_key(data) do
vapid_key = Application.get_env(:web_push_encryption, :vapid_details, [])[:public_key]
- if vapid_key do
- Map.put(data, "vapid_key", vapid_key)
- else
- data
- end
+ Pleroma.Maps.put_if_present(data, "vapid_key", vapid_key)
end
end
diff --git a/lib/pleroma/web/mastodon_api/views/conversation_view.ex b/lib/pleroma/web/mastodon_api/views/conversation_view.ex
index 2b6f84c72..a91994915 100644
--- a/lib/pleroma/web/mastodon_api/views/conversation_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/conversation_view.ex
@@ -23,10 +23,13 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do
last_activity_id =
with nil <- participation.last_activity_id do
- ActivityPub.fetch_latest_activity_id_for_context(participation.conversation.ap_id, %{
- "user" => user,
- "blocking_user" => user
- })
+ ActivityPub.fetch_latest_direct_activity_id_for_context(
+ participation.conversation.ap_id,
+ %{
+ user: user,
+ blocking_user: user
+ }
+ )
end
activity = Activity.get_by_id_with_object(last_activity_id)
@@ -35,7 +38,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do
%{
id: participation.id |> to_string(),
- accounts: render(AccountView, "index.json", users: users, as: :user),
+ accounts: render(AccountView, "index.json", users: users, for: user),
unread: !participation.read,
last_status:
render(StatusView, "show.json",
diff --git a/lib/pleroma/web/mastodon_api/views/filter_view.ex b/lib/pleroma/web/mastodon_api/views/filter_view.ex
index 97fd1e83f..c37f624e0 100644
--- a/lib/pleroma/web/mastodon_api/views/filter_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/filter_view.ex
@@ -7,11 +7,11 @@ defmodule Pleroma.Web.MastodonAPI.FilterView do
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.FilterView
- def render("filters.json", %{filters: filters} = opts) do
- render_many(filters, FilterView, "filter.json", opts)
+ def render("index.json", %{filters: filters}) do
+ render_many(filters, FilterView, "show.json")
end
- def render("filter.json", %{filter: filter}) do
+ def render("show.json", %{filter: filter}) do
expires_at =
if filter.expires_at do
Utils.to_masto_date(filter.expires_at)
@@ -25,7 +25,7 @@ defmodule Pleroma.Web.MastodonAPI.FilterView do
context: filter.context,
expires_at: expires_at,
irreversible: filter.hide,
- whole_word: false
+ whole_word: filter.whole_word
}
end
end
diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex
index 67214dbea..ea2d3aa9c 100644
--- a/lib/pleroma/web/mastodon_api/views/instance_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex
@@ -5,10 +5,13 @@
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(),
@@ -20,16 +23,84 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
streaming_api: Pleroma.Web.Endpoint.websocket_url()
},
stats: Pleroma.Stats.get_stats(),
- thumbnail: Pleroma.Web.base_url() <> "/instance/thumbnail.jpeg",
+ thumbnail: Keyword.get(instance, :instance_thumbnail),
languages: ["en"],
registrations: Keyword.get(instance, :registrations_open),
+ approval_required: Keyword.get(instance, :account_approval_required),
# Extra (not present in Mastodon):
max_toot_chars: Keyword.get(instance, :limit),
poll_limits: Keyword.get(instance, :poll_limits),
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),
+ background_image: Keyword.get(instance, :background_image),
+ chat_limit: Keyword.get(instance, :chat_limit),
+ description_limit: Keyword.get(instance, :description_limit),
+ pleroma: %{
+ metadata: %{
+ account_activation_required: Keyword.get(instance, :account_activation_required),
+ features: features(),
+ federation: federation(),
+ fields_limits: fields_limits(),
+ post_formats: Config.get([:instance, :allowed_post_formats])
+ },
+ 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",
+ "pleroma_chat_messages"
+ ]
+ |> Enum.filter(& &1)
+ end
+
+ def federation do
+ quarantined = Config.get([:instance, :quarantined_instances], [])
+
+ if Config.get([:mrf, :transparency]) do
+ {:ok, data} = MRF.describe()
+
+ data
+ |> Map.merge(%{quarantined_instances: quarantined})
+ else
+ %{}
+ end
+ |> Map.put(:enabled, Config.get([:instance, :federating]))
+ end
+
+ def fields_limits do
+ %{
+ max_fields: Config.get([:instance, :max_account_fields]),
+ max_remote_fields: Config.get([:instance, :max_remote_account_fields]),
+ name_length: Config.get([:instance, :account_field_name_length]),
+ value_length: Config.get([:instance, :account_field_value_length])
}
end
end
diff --git a/lib/pleroma/web/mastodon_api/views/marker_view.ex b/lib/pleroma/web/mastodon_api/views/marker_view.ex
index 985368fe5..21d535d54 100644
--- a/lib/pleroma/web/mastodon_api/views/marker_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/marker_view.ex
@@ -6,12 +6,16 @@ defmodule Pleroma.Web.MastodonAPI.MarkerView do
use Pleroma.Web, :view
def render("markers.json", %{markers: markers}) do
- Enum.reduce(markers, %{}, fn m, acc ->
- Map.put_new(acc, m.timeline, %{
- last_read_id: m.last_read_id,
- version: m.lock_version,
- updated_at: NaiveDateTime.to_iso8601(m.updated_at)
- })
+ Map.new(markers, fn m ->
+ {m.timeline,
+ %{
+ last_read_id: m.last_read_id,
+ version: m.lock_version,
+ updated_at: NaiveDateTime.to_iso8601(m.updated_at),
+ pleroma: %{
+ unread_count: m.unread_count
+ }
+ }}
end)
end
end
diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex
index 1720fbead..c97e6d32f 100644
--- a/lib/pleroma/web/mastodon_api/views/notification_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex
@@ -6,74 +6,150 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
use Pleroma.Web, :view
alias Pleroma.Activity
+ alias Pleroma.Chat.MessageReference
alias Pleroma.Notification
+ alias Pleroma.Object
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
+ alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
- def render("index.json", %{notifications: notifications, for: user}) do
- safe_render_many(notifications, NotificationView, "show.json", %{for: user})
+ @parent_types ~w{Like Announce EmojiReact}
+
+ def render("index.json", %{notifications: notifications, for: reading_user} = opts) do
+ activities = Enum.map(notifications, & &1.activity)
+
+ parent_activities =
+ activities
+ |> Enum.filter(fn
+ %{data: %{"type" => type}} ->
+ type in @parent_types
+ end)
+ |> 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(&(&1.data["type"] == "Move"))
+ |> Enum.map(&User.get_cached_by_ap_id(&1.data["target"]))
+ |> Enum.filter(& &1)
+
+ 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, subset: :source_mutes)
+ 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"])
- mastodon_type = Activity.mastodon_notification_type(activity)
-
- with %{id: _} = account <- AccountView.render("show.json", %{user: actor, for: user}) do
- response = %{
- id: to_string(notification.id),
- type: mastodon_type,
- created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at),
- account: account,
- pleroma: %{
- is_seen: notification.seen
- }
+
+ 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
+
+ # Note: :relationships contain user mutes (needed for :muted flag in :status)
+ status_render_opts = %{relationships: opts[:relationships]}
+ account = AccountView.render("show.json", %{user: actor, for: reading_user})
+
+ response = %{
+ id: to_string(notification.id),
+ type: notification.type,
+ created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at),
+ account: account,
+ pleroma: %{
+ is_muted: User.mutes?(reading_user, actor),
+ is_seen: notification.seen
}
+ }
- case mastodon_type do
- "mention" ->
- put_status(response, activity, user)
+ case notification.type do
+ "mention" ->
+ put_status(response, activity, reading_user, status_render_opts)
- "favourite" ->
- put_status(response, parent_activity, user)
+ "favourite" ->
+ put_status(response, parent_activity_fn.(), reading_user, status_render_opts)
- "reblog" ->
- put_status(response, parent_activity, user)
+ "reblog" ->
+ put_status(response, parent_activity_fn.(), reading_user, status_render_opts)
- "move" ->
- put_target(response, activity, user)
+ "move" ->
+ put_target(response, activity, reading_user, %{})
- "pleroma:emoji_reaction" ->
- put_status(response, parent_activity, user) |> put_emoji(activity)
+ "pleroma:emoji_reaction" ->
+ response
+ |> put_status(parent_activity_fn.(), reading_user, status_render_opts)
+ |> put_emoji(activity)
- type when type in ["follow", "follow_request"] ->
- response
+ "pleroma:chat_mention" ->
+ put_chat_message(response, activity, reading_user, status_render_opts)
- _ ->
- nil
- end
- else
- _ -> nil
+ type when type in ["follow", "follow_request"] ->
+ response
end
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_chat_message(response, activity, reading_user, opts) do
+ object = Object.normalize(activity)
+ author = User.get_cached_by_ap_id(object.data["actor"])
+ chat = Pleroma.Chat.get(reading_user.id, author.ap_id)
+ cm_ref = MessageReference.for_chat_and_object(chat, object)
+ render_opts = Map.merge(opts, %{for: reading_user, chat_message_reference: cm_ref})
+ chat_message_render = MessageReferenceView.render("show.json", render_opts)
+
+ Map.put(response, :chat_message, chat_message_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_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, 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 40edbb213..1208dc9a0 100644
--- a/lib/pleroma/web/mastodon_api/views/poll_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/poll_view.ex
@@ -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"])
@@ -27,10 +28,10 @@ defmodule Pleroma.Web.MastodonAPI.PollView do
def render("show.json", %{object: object} = params) do
case object.data do
- %{"anyOf" => options} when is_list(options) ->
+ %{"anyOf" => [_ | _] = options} ->
render(__MODULE__, "show.json", Map.merge(params, %{multiple: true, options: options}))
- %{"oneOf" => options} when is_list(options) ->
+ %{"oneOf" => [_ | _] = options} ->
render(__MODULE__, "show.json", Map.merge(params, %{multiple: false, options: options}))
_ ->
@@ -39,15 +40,13 @@ defmodule Pleroma.Web.MastodonAPI.PollView do
end
defp end_time_and_expired(object) do
- case object.data["closed"] || object.data["endTime"] do
- end_time when is_binary(end_time) ->
- end_time = NaiveDateTime.from_iso8601!(end_time)
- expired = NaiveDateTime.compare(end_time, NaiveDateTime.utc_now()) == :lt
+ if object.data["closed"] do
+ end_time = NaiveDateTime.from_iso8601!(object.data["closed"])
+ expired = NaiveDateTime.compare(end_time, NaiveDateTime.utc_now()) == :lt
- {Utils.to_masto_date(end_time), expired}
-
- _ ->
- {nil, false}
+ {Utils.to_masto_date(end_time), expired}
+ else
+ {nil, false}
end
end
@@ -62,6 +61,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/scheduled_activity_view.ex b/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex
index 458f6bc78..5b896bf3b 100644
--- a/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex
@@ -30,7 +30,7 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityView do
defp with_media_attachments(data, _), do: data
defp status_params(params) do
- data = %{
+ %{
text: params["status"],
sensitive: params["sensitive"],
spoiler_text: params["spoiler_text"],
@@ -39,10 +39,6 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityView do
poll: params["poll"],
in_reply_to_id: params["in_reply_to_id"]
}
-
- case params["media_ids"] do
- nil -> data
- media_ids -> Map.put(data, :media_ids, media_ids)
- end
+ |> Pleroma.Maps.put_if_present(:media_ids, params["media_ids"])
end
end
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index f0bdb49c3..01b8bb6bb 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -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
@@ -20,7 +21,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.MediaProxy
- import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1]
+ import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1, visible_for_user?: 2]
# TODO: Add cached version.
defp get_replied_to_activities([]), do: %{}
@@ -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,47 @@ 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)
- safe_render_many(opts.activities, StatusView, "show.json", opts)
+ 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, subset: :source_mutes)
+ end
+
+ 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 +127,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 +157,11 @@ 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]
+ }),
in_reply_to_id: nil,
in_reply_to_account_id: nil,
reblog: reblogged,
@@ -116,7 +170,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 +237,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"] || []
@@ -242,27 +297,47 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
emoji_reactions =
with %{data: %{"reactions" => emoji_reactions}} <- object do
- Enum.map(emoji_reactions, fn [emoji, users] ->
- %{
- name: emoji,
- count: length(users),
- me: !!(opts[:for] && opts[:for].ap_id in users)
- }
+ Enum.map(emoji_reactions, fn
+ [emoji, users] when is_list(users) ->
+ build_emoji_map(emoji, users, opts[:for])
+
+ {emoji, users} when is_list(users) ->
+ build_emoji_map(emoji, users, opts[:for])
+
+ _ ->
+ nil
end)
+ |> Enum.reject(&is_nil/1)
else
_ -> []
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]
+ }),
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,
card: card,
content: content_html,
+ text: opts[:with_source] && object.data["source"],
created_at: created_at,
reblogs_count: announcement_count,
replies_count: object.data["repliesCount"] || 0,
@@ -270,7 +345,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,
@@ -294,7 +369,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
expires_at: expires_at,
direct_conversation_id: direct_conversation_id,
thread_muted: thread_muted?,
- emoji_reactions: emoji_reactions
+ emoji_reactions: emoji_reactions,
+ parent_visible: visible_for_user?(reply_to, opts[:for])
}
}
end
@@ -364,27 +440,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
}
end
- def render("listen.json", %{activity: %Activity{data: %{"type" => "Listen"}} = activity} = opts) do
- object = Object.normalize(activity)
-
- user = get_user(activity.data["actor"])
- created_at = Utils.to_masto_date(activity.data["published"])
-
- %{
- id: activity.id,
- account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
- created_at: created_at,
- title: object.data["title"] |> HTML.strip_tags(),
- artist: object.data["artist"] |> HTML.strip_tags(),
- album: object.data["album"] |> HTML.strip_tags(),
- length: object.data["length"]
- }
- end
-
- def render("listens.json", opts) do
- safe_render_many(opts.activities, StatusView, "listen.json", opts)
- end
-
def render("context.json", %{activity: activity, activities: activities, user: user}) do
%{ancestors: ancestors, descendants: descendants} =
activities
@@ -418,23 +473,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
end
end
- def render_content(%{data: %{"type" => object_type}} = object)
- 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
- _ -> object.data["content"] || ""
- end
- end
+ def render_content(%{data: %{"name" => name}} = object) when not is_nil(name) and name != "" do
+ url = object.data["url"] || object.data["id"]
- def render_content(%{data: %{"type" => object_type}} = object)
- when object_type in ["Article", "Page"] do
- with summary when not is_nil(summary) and summary != "" <- object.data["name"],
- url when is_bitstring(url) <- object.data["url"] do
- "<p><a href=\"#{url}\">#{summary}</a></p>#{object.data["content"]}"
- else
- _ -> object.data["content"] || ""
- end
+ "<p><a href=\"#{url}\">#{name}</a></p>#{object.data["content"]}"
end
def render_content(object), do: object.data["content"] || ""
@@ -451,11 +493,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: []
@@ -496,4 +536,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
defp pinned?(%Activity{id: id}, %User{pinned_activities: pinned_activities}),
do: id in pinned_activities
+
+ defp build_emoji_map(emoji, users, current_user) do
+ %{
+ name: emoji,
+ count: length(users),
+ me: !!(current_user && current_user.ap_id in users)
+ }
+ end
end
diff --git a/lib/pleroma/web/mastodon_api/views/push_subscription_view.ex b/lib/pleroma/web/mastodon_api/views/subscription_view.ex
index d32cef6e2..7c67cc924 100644
--- a/lib/pleroma/web/mastodon_api/views/push_subscription_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/subscription_view.ex
@@ -2,11 +2,11 @@
# 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 b1aebe014..94e4595d8 100644
--- a/lib/pleroma/web/mastodon_api/websocket_handler.ex
+++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex
@@ -12,8 +12,12 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
@behaviour :cowboy_websocket
- # 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 <- Enum.into(:cow_qs.parse_qs(qs), %{}),
@@ -28,7 +32,8 @@ 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, :bad_topic} ->
Logger.debug("#{__MODULE__} bad topic #{inspect(req)}")
@@ -43,28 +48,54 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
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
@@ -74,7 +105,7 @@ 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
@@ -96,7 +127,7 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
end
end
- defp streamer_socket(state) do
- %{transport_pid: self(), assigns: state}
+ defp timer do
+ Process.send_after(self(), :tick, @tick)
end
end
diff --git a/lib/pleroma/web/media_proxy/invalidation.ex b/lib/pleroma/web/media_proxy/invalidation.ex
new file mode 100644
index 000000000..5808861e6
--- /dev/null
+++ b/lib/pleroma/web/media_proxy/invalidation.ex
@@ -0,0 +1,38 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MediaProxy.Invalidation do
+ @moduledoc false
+
+ @callback purge(list(String.t()), Keyword.t()) :: {:ok, list(String.t())} | {:error, String.t()}
+
+ alias Pleroma.Config
+ alias Pleroma.Web.MediaProxy
+
+ @spec enabled?() :: boolean()
+ def enabled?, do: Config.get([:media_proxy, :invalidation, :enabled])
+
+ @spec purge(list(String.t()) | String.t()) :: {:ok, list(String.t())} | {:error, String.t()}
+ def purge(urls) do
+ prepared_urls = prepare_urls(urls)
+
+ if enabled?() do
+ do_purge(prepared_urls)
+ else
+ {:ok, prepared_urls}
+ end
+ end
+
+ defp do_purge(urls) do
+ provider = Config.get([:media_proxy, :invalidation, :provider])
+ options = Config.get(provider)
+ provider.purge(urls, options)
+ end
+
+ def prepare_urls(urls) do
+ urls
+ |> List.wrap()
+ |> Enum.map(&MediaProxy.url/1)
+ end
+end
diff --git a/lib/pleroma/web/media_proxy/invalidations/http.ex b/lib/pleroma/web/media_proxy/invalidations/http.ex
new file mode 100644
index 000000000..bb81d8888
--- /dev/null
+++ b/lib/pleroma/web/media_proxy/invalidations/http.ex
@@ -0,0 +1,40 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MediaProxy.Invalidation.Http do
+ @moduledoc false
+ @behaviour Pleroma.Web.MediaProxy.Invalidation
+
+ require Logger
+
+ @impl Pleroma.Web.MediaProxy.Invalidation
+ def purge(urls, opts \\ []) do
+ method = Keyword.get(opts, :method, :purge)
+ headers = Keyword.get(opts, :headers, [])
+ options = Keyword.get(opts, :options, [])
+
+ Logger.debug("Running cache purge: #{inspect(urls)}")
+
+ Enum.each(urls, fn url ->
+ with {:error, error} <- do_purge(method, url, headers, options) do
+ Logger.error("Error while cache purge: url - #{url}, error: #{inspect(error)}")
+ end
+ end)
+
+ {:ok, urls}
+ end
+
+ defp do_purge(method, url, headers, options) do
+ case Pleroma.HTTP.request(method, url, "", headers, options) do
+ {:ok, %{status: status} = env} when 400 <= status and status < 500 ->
+ {:error, env}
+
+ {:error, error} = error ->
+ error
+
+ _ ->
+ {:ok, "success"}
+ end
+ end
+end
diff --git a/lib/pleroma/web/media_proxy/invalidations/script.ex b/lib/pleroma/web/media_proxy/invalidations/script.ex
new file mode 100644
index 000000000..d32ffc50b
--- /dev/null
+++ b/lib/pleroma/web/media_proxy/invalidations/script.ex
@@ -0,0 +1,43 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MediaProxy.Invalidation.Script do
+ @moduledoc false
+
+ @behaviour Pleroma.Web.MediaProxy.Invalidation
+
+ require Logger
+
+ @impl Pleroma.Web.MediaProxy.Invalidation
+ def purge(urls, opts \\ []) do
+ args =
+ urls
+ |> List.wrap()
+ |> Enum.uniq()
+ |> Enum.join(" ")
+
+ opts
+ |> Keyword.get(:script_path)
+ |> do_purge([args])
+ |> handle_result(urls)
+ end
+
+ defp do_purge(script_path, args) when is_binary(script_path) do
+ path = Path.expand(script_path)
+ Logger.debug("Running cache purge: #{inspect(args)}, #{inspect(path)}")
+ System.cmd(path, args)
+ rescue
+ error -> error
+ end
+
+ defp do_purge(_, _), do: {:error, "not found script path"}
+
+ defp handle_result({_result, 0}, urls), do: {:ok, urls}
+ defp handle_result({:error, error}, urls), do: handle_result(error, urls)
+
+ defp handle_result(error, _) do
+ Logger.error("Error while cache purge: #{inspect(error)}")
+ {:error, inspect(error)}
+ end
+end
diff --git a/lib/pleroma/web/media_proxy/media_proxy.ex b/lib/pleroma/web/media_proxy/media_proxy.ex
index b2b524524..e18dd8224 100644
--- a/lib/pleroma/web/media_proxy/media_proxy.ex
+++ b/lib/pleroma/web/media_proxy/media_proxy.ex
@@ -6,20 +6,56 @@ defmodule Pleroma.Web.MediaProxy do
alias Pleroma.Config
alias Pleroma.Upload
alias Pleroma.Web
+ alias Pleroma.Web.MediaProxy.Invalidation
@base64_opts [padding: false]
+ @cache_table :banned_urls_cache
+
+ def cache_table, do: @cache_table
+
+ @spec in_banned_urls(String.t()) :: boolean()
+ def in_banned_urls(url), do: elem(Cachex.exists?(@cache_table, url(url)), 1)
+
+ def remove_from_banned_urls(urls) when is_list(urls) do
+ Cachex.execute!(@cache_table, fn cache ->
+ Enum.each(Invalidation.prepare_urls(urls), &Cachex.del(cache, &1))
+ end)
+ end
+
+ def remove_from_banned_urls(url) when is_binary(url) do
+ Cachex.del(@cache_table, url(url))
+ end
+
+ def put_in_banned_urls(urls) when is_list(urls) do
+ Cachex.execute!(@cache_table, fn cache ->
+ Enum.each(Invalidation.prepare_urls(urls), &Cachex.put(cache, &1, true))
+ end)
+ end
+
+ def put_in_banned_urls(url) when is_binary(url) do
+ Cachex.put(@cache_table, url(url), true)
+ end
def url(url) when is_nil(url) or url == "", do: nil
def url("/" <> _ = url), do: url
def url(url) do
- if disabled?() or local?(url) or whitelisted?(url) do
+ if disabled?() or not url_proxiable?(url) do
url
else
encode_url(url)
end
end
+ @spec url_proxiable?(String.t()) :: boolean()
+ def url_proxiable?(url) do
+ if local?(url) or whitelisted?(url) do
+ false
+ else
+ true
+ end
+ end
+
defp disabled?, do: !Config.get([:media_proxy, :enabled], false)
defp local?(url), do: String.starts_with?(url, Pleroma.Web.base_url())
@@ -27,22 +63,28 @@ defmodule Pleroma.Web.MediaProxy do
defp whitelisted?(url) do
%{host: domain} = URI.parse(url)
- mediaproxy_whitelist = Config.get([:media_proxy, :whitelist])
+ mediaproxy_whitelist_domains =
+ [:media_proxy, :whitelist]
+ |> Config.get()
+ |> Enum.map(&maybe_get_domain_from_url/1)
- upload_base_url_domain =
- if !is_nil(Config.get([Upload, :base_url])) do
- [URI.parse(Config.get([Upload, :base_url])).host]
+ whitelist_domains =
+ if base_url = Config.get([Upload, :base_url]) do
+ %{host: base_domain} = URI.parse(base_url)
+ [base_domain | mediaproxy_whitelist_domains]
else
- []
+ mediaproxy_whitelist_domains
end
- whitelist = mediaproxy_whitelist ++ upload_base_url_domain
+ domain in whitelist_domains
+ end
- Enum.any?(whitelist, fn pattern ->
- String.equivalent?(domain, pattern)
- end)
+ defp maybe_get_domain_from_url("http" <> _ = url) do
+ URI.parse(url).host
end
+ defp maybe_get_domain_from_url(domain), do: domain
+
def encode_url(url) do
base64 = Base.url_encode64(url, @base64_opts)
@@ -73,7 +115,7 @@ defmodule Pleroma.Web.MediaProxy do
def build_url(sig_base64, url_base64, filename \\ nil) do
[
- Pleroma.Config.get([:media_proxy, :base_url], Web.base_url()),
+ Config.get([:media_proxy, :base_url], Web.base_url()),
"proxy",
sig_base64,
url_base64,
diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex
index 1a09ac62a..9a64b0ef3 100644
--- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex
+++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex
@@ -4,6 +4,7 @@
defmodule Pleroma.Web.MediaProxy.MediaProxyController do
use Pleroma.Web, :controller
+
alias Pleroma.ReverseProxy
alias Pleroma.Web.MediaProxy
@@ -13,10 +14,11 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do
with config <- Pleroma.Config.get([:media_proxy], []),
true <- Keyword.get(config, :enabled, false),
{:ok, url} <- MediaProxy.decode_url(sig64, url64),
+ {_, false} <- {:in_banned_urls, MediaProxy.in_banned_urls(url)},
:ok <- filename_matches(params, conn.request_path, url) do
ReverseProxy.call(conn, url, Keyword.get(config, :proxy_opts, @default_proxy_opts))
else
- false ->
+ error when error in [false, {:in_banned_urls, true}] ->
send_resp(conn, 404, Plug.Conn.Status.reason_phrase(404))
{:error, :invalid_signature} ->
diff --git a/lib/pleroma/web/metadata.ex b/lib/pleroma/web/metadata.ex
index c9aac27dc..a9f70c43e 100644
--- a/lib/pleroma/web/metadata.ex
+++ b/lib/pleroma/web/metadata.ex
@@ -6,7 +6,12 @@ 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/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/mongooseim/mongoose_im_controller.ex b/lib/pleroma/web/mongooseim/mongoose_im_controller.ex
index 04d823b36..6cbbe8fd8 100644
--- a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex
+++ b/lib/pleroma/web/mongooseim/mongoose_im_controller.ex
@@ -5,7 +5,7 @@
defmodule Pleroma.Web.MongooseIM.MongooseIMController do
use Pleroma.Web, :controller
- alias Comeonin.Pbkdf2
+ alias Pleroma.Plugs.AuthenticationPlug
alias Pleroma.Plugs.RateLimiter
alias Pleroma.Repo
alias Pleroma.User
@@ -14,7 +14,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 +26,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 <- AuthenticationPlug.checkpw(password, password_hash) do
conn
|> json(true)
else
diff --git a/lib/pleroma/web/nodeinfo/nodeinfo.ex b/lib/pleroma/web/nodeinfo/nodeinfo.ex
new file mode 100644
index 000000000..47fa46376
--- /dev/null
+++ b/lib/pleroma/web/nodeinfo/nodeinfo.ex
@@ -0,0 +1,91 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Nodeinfo.Nodeinfo do
+ alias Pleroma.Config
+ alias Pleroma.Stats
+ alias Pleroma.User
+ alias Pleroma.Web.Federator.Publisher
+ alias Pleroma.Web.MastodonAPI.InstanceView
+
+ # returns a nodeinfo 2.0 map, since 2.1 just adds a repository field
+ # under software.
+ def get_nodeinfo("2.0") do
+ stats = Stats.get_stats()
+
+ staff_accounts =
+ User.all_superusers()
+ |> Enum.map(fn u -> u.ap_id end)
+
+ federation = InstanceView.federation()
+ features = InstanceView.features()
+
+ %{
+ version: "2.0",
+ software: %{
+ name: Pleroma.Application.name() |> String.downcase(),
+ version: Pleroma.Application.version()
+ },
+ protocols: Publisher.gather_nodeinfo_protocol_names(),
+ services: %{
+ inbound: [],
+ outbound: []
+ },
+ openRegistrations: Config.get([:instance, :registrations_open]),
+ usage: %{
+ users: %{
+ total: Map.get(stats, :user_count, 0)
+ },
+ localPosts: Map.get(stats, :status_count, 0)
+ },
+ metadata: %{
+ nodeName: Config.get([:instance, :name]),
+ nodeDescription: Config.get([:instance, :description]),
+ private: !Config.get([:instance, :public], true),
+ suggestions: %{
+ enabled: false
+ },
+ staffAccounts: staff_accounts,
+ federation: federation,
+ pollLimits: Config.get([:instance, :poll_limits]),
+ postFormats: Config.get([:instance, :allowed_post_formats]),
+ uploadLimits: %{
+ general: Config.get([:instance, :upload_limit]),
+ avatar: Config.get([:instance, :avatar_upload_limit]),
+ banner: Config.get([:instance, :banner_upload_limit]),
+ background: Config.get([:instance, :background_upload_limit])
+ },
+ fieldsLimits: %{
+ maxFields: Config.get([:instance, :max_account_fields]),
+ maxRemoteFields: Config.get([:instance, :max_remote_account_fields]),
+ nameLength: Config.get([:instance, :account_field_name_length]),
+ valueLength: Config.get([:instance, :account_field_value_length])
+ },
+ accountActivationRequired: Config.get([:instance, :account_activation_required], false),
+ invitesEnabled: Config.get([:instance, :invites_enabled], false),
+ mailerEnabled: Config.get([Pleroma.Emails.Mailer, :enabled], false),
+ features: features,
+ restrictedNicknames: Config.get([Pleroma.User, :restricted_nicknames]),
+ skipThreadContainment: Config.get([:instance, :skip_thread_containment], false)
+ }
+ }
+ end
+
+ def get_nodeinfo("2.1") do
+ raw_response = get_nodeinfo("2.0")
+
+ updated_software =
+ raw_response
+ |> Map.get(:software)
+ |> Map.put(:repository, Pleroma.Application.repository())
+
+ raw_response
+ |> Map.put(:software, updated_software)
+ |> Map.put(:version, "2.1")
+ end
+
+ def get_nodeinfo(_version) do
+ {:error, :missing}
+ end
+end
diff --git a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
index 18eb41333..8c7a9e565 100644
--- a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
+++ b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
@@ -5,12 +5,8 @@
defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
use Pleroma.Web, :controller
- alias Pleroma.Config
- alias Pleroma.Stats
- alias Pleroma.User
alias Pleroma.Web
- alias Pleroma.Web.ActivityPub.MRF
- alias Pleroma.Web.Federator.Publisher
+ alias Pleroma.Web.Nodeinfo.Nodeinfo
def schemas(conn, _params) do
response = %{
@@ -29,139 +25,20 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
json(conn, response)
end
- # returns a nodeinfo 2.0 map, since 2.1 just adds a repository field
- # under software.
- 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)
-
- %{
- version: "2.0",
- software: %{
- name: Pleroma.Application.name() |> String.downcase(),
- version: Pleroma.Application.version()
- },
- protocols: Publisher.gather_nodeinfo_protocol_names(),
- services: %{
- inbound: [],
- outbound: []
- },
- openRegistrations: Config.get([:instance, :registrations_open]),
- usage: %{
- users: %{
- total: Map.get(stats, :user_count, 0)
- },
- localPosts: Map.get(stats, :status_count, 0)
- },
- metadata: %{
- nodeName: Config.get([:instance, :name]),
- nodeDescription: Config.get([:instance, :description]),
- private: !Config.get([:instance, :public], true),
- suggestions: %{
- enabled: false
- },
- staffAccounts: staff_accounts,
- federation: federation_response,
- pollLimits: Config.get([:instance, :poll_limits]),
- postFormats: Config.get([:instance, :allowed_post_formats]),
- uploadLimits: %{
- general: Config.get([:instance, :upload_limit]),
- avatar: Config.get([:instance, :avatar_upload_limit]),
- banner: Config.get([:instance, :banner_upload_limit]),
- background: Config.get([:instance, :background_upload_limit])
- },
- fieldsLimits: %{
- maxFields: Config.get([:instance, :max_account_fields]),
- maxRemoteFields: Config.get([:instance, :max_remote_account_fields]),
- nameLength: Config.get([:instance, :account_field_name_length]),
- valueLength: Config.get([:instance, :account_field_value_length])
- },
- accountActivationRequired: Config.get([:instance, :account_activation_required], false),
- invitesEnabled: Config.get([:instance, :invites_enabled], false),
- mailerEnabled: Config.get([Pleroma.Emails.Mailer, :enabled], false),
- features: features,
- restrictedNicknames: Config.get([Pleroma.User, :restricted_nicknames]),
- skipThreadContainment: Config.get([:instance, :skip_thread_containment], false)
- }
- }
- end
-
# Schema definition: https://github.com/jhass/nodeinfo/blob/master/schemas/2.0/schema.json
# and https://github.com/jhass/nodeinfo/blob/master/schemas/2.1/schema.json
- def nodeinfo(conn, %{"version" => "2.0"}) do
- conn
- |> put_resp_header(
- "content-type",
- "application/json; profile=http://nodeinfo.diaspora.software/ns/schema/2.0#; charset=utf-8"
- )
- |> json(raw_nodeinfo())
- end
-
- def nodeinfo(conn, %{"version" => "2.1"}) do
- raw_response = raw_nodeinfo()
-
- updated_software =
- raw_response
- |> Map.get(:software)
- |> Map.put(:repository, Pleroma.Application.repository())
-
- response =
- raw_response
- |> Map.put(:software, updated_software)
- |> Map.put(:version, "2.1")
-
- conn
- |> put_resp_header(
- "content-type",
- "application/json; profile=http://nodeinfo.diaspora.software/ns/schema/2.1#; charset=utf-8"
- )
- |> json(response)
- end
-
- def nodeinfo(conn, _) do
- render_error(conn, :not_found, "Nodeinfo schema version not handled")
+ def nodeinfo(conn, %{"version" => version}) do
+ case Nodeinfo.get_nodeinfo(version) do
+ {:error, :missing} ->
+ render_error(conn, :not_found, "Nodeinfo schema version not handled")
+
+ node_info ->
+ conn
+ |> put_resp_header(
+ "content-type",
+ "application/json; profile=http://nodeinfo.diaspora.software/ns/schema/2.0#; charset=utf-8"
+ )
+ |> json(node_info)
+ end
end
end
diff --git a/lib/pleroma/web/oauth/app.ex b/lib/pleroma/web/oauth/app.ex
index 01ed326f4..df99472e1 100644
--- a/lib/pleroma/web/oauth/app.ex
+++ b/lib/pleroma/web/oauth/app.ex
@@ -5,6 +5,7 @@
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(t(), map()) :: Ecto.Changeset.t()
+ def changeset(struct, params) do
+ cast(struct, params, [:client_name, :redirect_uris, :scopes, :website, :trusted])
+ end
+
+ @spec register_changeset(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,11 +52,27 @@ defmodule Pleroma.Web.OAuth.App do
end
end
+ @spec create(map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
+ def create(params) do
+ %__MODULE__{}
+ |> register_changeset(params)
+ |> Repo.insert()
+ end
+
+ @spec update(pos_integer(), map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
+ def update(id, params) do
+ with %__MODULE__{} = app <- Repo.get(__MODULE__, id) do
+ app
+ |> changeset(params)
+ |> Repo.update()
+ end
+ end
+
@doc """
Gets app by attrs or create new with attrs.
And updates the scopes if need.
"""
- @spec get_or_make(map(), list(String.t())) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
+ @spec get_or_make(map(), list(String.t())) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
def get_or_make(attrs, scopes) do
with %__MODULE__{} = app <- Repo.get_by(__MODULE__, attrs) do
update_scopes(app, scopes)
@@ -65,4 +92,58 @@ defmodule Pleroma.Web.OAuth.App do
|> change(%{scopes: scopes})
|> Repo.update()
end
+
+ @spec search(map()) :: {:ok, [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, 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/mfa_controller.ex b/lib/pleroma/web/oauth/mfa_controller.ex
new file mode 100644
index 000000000..f102c93e7
--- /dev/null
+++ b/lib/pleroma/web/oauth/mfa_controller.ex
@@ -0,0 +1,98 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.OAuth.MFAController do
+ @moduledoc """
+ The model represents api to use Multi Factor authentications.
+ """
+
+ use Pleroma.Web, :controller
+
+ alias Pleroma.MFA
+ alias Pleroma.Web.Auth.TOTPAuthenticator
+ alias Pleroma.Web.OAuth.MFAView, as: View
+ alias Pleroma.Web.OAuth.OAuthController
+ alias Pleroma.Web.OAuth.OAuthView
+ 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, OAuthView.render("token.json", %{user: user, token: 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..5d87db268
--- /dev/null
+++ b/lib/pleroma/web/oauth/mfa_view.ex
@@ -0,0 +1,17 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.OAuth.MFAView do
+ use Pleroma.Web, :view
+ import Phoenix.HTML.Form
+ alias Pleroma.MFA
+
+ def render("mfa_response.json", %{token: token, user: user}) do
+ %{
+ error: "mfa_required",
+ mfa_token: token.token,
+ supported_challenge_types: MFA.supported_methods(user)
+ }
+ end
+end
diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex
index 0121cd661..dd00600ea 100644
--- a/lib/pleroma/web/oauth/oauth_controller.ex
+++ b/lib/pleroma/web/oauth/oauth_controller.ex
@@ -6,6 +6,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do
use Pleroma.Web, :controller
alias Pleroma.Helpers.UriHelper
+ alias Pleroma.Maps
+ alias Pleroma.MFA
alias Pleroma.Plugs.RateLimiter
alias Pleroma.Registration
alias Pleroma.Repo
@@ -14,6 +16,9 @@ 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.MFAView
+ alias Pleroma.Web.OAuth.OAuthView
alias Pleroma.Web.OAuth.Scopes
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken
@@ -25,9 +30,10 @@ defmodule Pleroma.Web.OAuth.OAuthController do
plug(:fetch_session)
plug(:fetch_flash)
- plug(RateLimiter, [name: :authentication] when action == :create_authorization)
- plug(:skip_plug, Pleroma.Plugs.OAuthScopesPlug)
+ plug(:skip_plug, [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug])
+
+ plug(RateLimiter, [name: :authentication] when action == :create_authorization)
action_fallback(Pleroma.Web.OAuth.FallbackController)
@@ -70,6 +76,13 @@ defmodule Pleroma.Web.OAuth.OAuthController do
available_scopes = (app && app.scopes) || []
scopes = Scopes.fetch_scopes(params, available_scopes)
+ scopes =
+ if scopes == [] do
+ available_scopes
+ else
+ scopes
+ end
+
# Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template
render(conn, Authenticator.auth_template(), %{
response_type: params["response_type"],
@@ -105,7 +118,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
if redirect_uri in String.split(app.redirect_uris) do
redirect_uri = redirect_uri(conn, redirect_uri)
url_params = %{access_token: token.token}
- url_params = UriHelper.append_param_if_present(url_params, :state, params["state"])
+ url_params = Maps.put_if_present(url_params, :state, params["state"])
url = UriHelper.append_uri_params(redirect_uri, url_params)
redirect(conn, external: url)
else
@@ -120,7 +133,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 ->
@@ -143,7 +157,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
if redirect_uri in String.split(app.redirect_uris) do
redirect_uri = redirect_uri(conn, redirect_uri)
url_params = %{code: auth.token}
- url_params = UriHelper.append_param_if_present(url_params, :state, auth_attrs["state"])
+ url_params = Maps.put_if_present(url_params, :state, auth_attrs["state"])
url = UriHelper.append_uri_params(redirect_uri, url_params)
redirect(conn, external: url)
else
@@ -180,6 +194,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
@@ -212,9 +242,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
with {:ok, app} <- Token.Utils.fetch_app(conn),
{:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token),
{:ok, token} <- RefreshToken.grant(token) do
- response_attrs = %{created_at: Token.Utils.format_created_at(token)}
-
- json(conn, Token.Response.build(user, token, response_attrs))
+ json(conn, OAuthView.render("token.json", %{user: user, token: token}))
else
_error -> render_invalid_credentials_error(conn)
end
@@ -226,11 +254,10 @@ defmodule Pleroma.Web.OAuth.OAuthController do
{:ok, auth} <- Authorization.get_by_token(app, fixed_token),
%User{} = user <- User.get_cached_by_id(auth.user_id),
{:ok, token} <- Token.exchange_token(app, auth) do
- response_attrs = %{created_at: Token.Utils.format_created_at(token)}
-
- json(conn, Token.Response.build(user, token, response_attrs))
+ json(conn, OAuthView.render("token.json", %{user: user, token: token}))
else
- _error -> render_invalid_credentials_error(conn)
+ error ->
+ handle_token_exchange_error(conn, error)
end
end
@@ -240,11 +267,9 @@ defmodule Pleroma.Web.OAuth.OAuthController do
) do
with {:ok, %User{} = user} <- Authenticator.get_user(conn),
{:ok, app} <- Token.Utils.fetch_app(conn),
- {:account_status, :active} <- {:account_status, User.account_status(user)},
- {:ok, scopes} <- validate_scopes(app, params),
- {:ok, auth} <- Authorization.create_authorization(app, user, scopes),
- {:ok, token} <- Token.exchange_token(app, auth) do
- json(conn, Token.Response.build(user, token))
+ requested_scopes <- Scopes.fetch_scopes(params, app.scopes),
+ {:ok, token} <- login(user, app, requested_scopes) do
+ json(conn, OAuthView.render("token.json", %{user: user, token: token}))
else
error ->
handle_token_exchange_error(conn, error)
@@ -267,15 +292,22 @@ defmodule Pleroma.Web.OAuth.OAuthController do
with {:ok, app} <- Token.Utils.fetch_app(conn),
{:ok, auth} <- Authorization.create_authorization(app, %User{}),
{:ok, token} <- Token.exchange_token(app, auth) do
- json(conn, Token.Response.build_for_client_credentials(token))
+ json(conn, OAuthView.render("token.json", %{token: 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,
@@ -309,6 +341,16 @@ defmodule Pleroma.Web.OAuth.OAuthController do
)
end
+ defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :approval_pending}) do
+ render_error(
+ conn,
+ :forbidden,
+ "Your account is awaiting approval.",
+ %{},
+ "awaiting_approval"
+ )
+ end
+
defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do
render_invalid_credentials_error(conn)
end
@@ -433,7 +475,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
@@ -483,6 +526,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do
end
end
+ defp do_create_authorization(conn, auth_attrs, user \\ nil)
+
defp do_create_authorization(
%Plug.Conn{} = conn,
%{
@@ -492,15 +537,34 @@ defmodule Pleroma.Web.OAuth.OAuthController do
"redirect_uri" => redirect_uri
} = auth_attrs
},
- user \\ nil
+ user
) do
with {_, {:ok, %User{} = user}} <-
{:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)},
%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)
+ requested_scopes <- Scopes.fetch_scopes(auth_attrs, app.scopes),
+ {:ok, auth} <- do_create_authorization(user, app, requested_scopes) do
+ {:ok, auth, user}
+ end
+ end
+
+ defp do_create_authorization(%User{} = user, %App{} = app, requested_scopes)
+ when is_list(requested_scopes) do
+ with {:account_status, :active} <- {:account_status, User.account_status(user)},
+ {:ok, scopes} <- validate_scopes(app, requested_scopes),
+ {:ok, auth} <- Authorization.create_authorization(app, user, scopes) do
+ {:ok, auth}
+ end
+ end
+
+ # Note: intended to be a private function but opened for AccountController that logs in on signup
+ @doc "If checks pass, creates authorization and token for given user, app and requested scopes."
+ def login(%User{} = user, %App{} = app, requested_scopes) when is_list(requested_scopes) do
+ with {:ok, auth} <- do_create_authorization(user, app, requested_scopes),
+ {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)},
+ {:ok, token} <- Token.exchange_token(app, auth) do
+ {:ok, token}
end
end
@@ -514,12 +578,21 @@ defmodule Pleroma.Web.OAuth.OAuthController do
defp put_session_registration_id(%Plug.Conn{} = conn, registration_id),
do: put_session(conn, :registration_id, registration_id)
- @spec validate_scopes(App.t(), map()) ::
+ defp build_and_response_mfa_token(user, auth) do
+ with {:ok, token} <- MFA.Token.create_token(user, auth) do
+ MFAView.render("mfa_response.json", %{token: token, user: user})
+ end
+ end
+
+ @spec validate_scopes(App.t(), map() | list()) ::
{:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
- defp validate_scopes(%App{} = app, params) do
- params
- |> Scopes.fetch_scopes(app.scopes)
- |> Scopes.validate(app.scopes)
+ defp validate_scopes(%App{} = app, params) when is_map(params) do
+ requested_scopes = Scopes.fetch_scopes(params, app.scopes)
+ validate_scopes(app, requested_scopes)
+ end
+
+ defp validate_scopes(%App{} = app, requested_scopes) when is_list(requested_scopes) do
+ Scopes.validate(requested_scopes, app.scopes)
end
def default_redirect_uri(%App{} = app) do
diff --git a/lib/pleroma/web/oauth/oauth_view.ex b/lib/pleroma/web/oauth/oauth_view.ex
index 94ddaf913..f55247ebd 100644
--- a/lib/pleroma/web/oauth/oauth_view.ex
+++ b/lib/pleroma/web/oauth/oauth_view.ex
@@ -5,4 +5,26 @@
defmodule Pleroma.Web.OAuth.OAuthView do
use Pleroma.Web, :view
import Phoenix.HTML.Form
+
+ alias Pleroma.Web.OAuth.Token.Utils
+
+ def render("token.json", %{token: token} = opts) do
+ response = %{
+ token_type: "Bearer",
+ access_token: token.token,
+ refresh_token: token.refresh_token,
+ expires_in: expires_in(),
+ scope: Enum.join(token.scopes, " "),
+ created_at: Utils.format_created_at(token)
+ }
+
+ if user = opts[:user] do
+ response
+ |> Map.put(:me, user.ap_id)
+ else
+ response
+ end
+ end
+
+ defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600)
end
diff --git a/lib/pleroma/web/oauth/scopes.ex b/lib/pleroma/web/oauth/scopes.ex
index 8ecf901f3..6f06f1431 100644
--- a/lib/pleroma/web/oauth/scopes.ex
+++ b/lib/pleroma/web/oauth/scopes.ex
@@ -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/clean_worker.ex b/lib/pleroma/web/oauth/token/clean_worker.ex
new file mode 100644
index 000000000..e3aa4eb7e
--- /dev/null
+++ b/lib/pleroma/web/oauth/token/clean_worker.ex
@@ -0,0 +1,38 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.OAuth.Token.CleanWorker do
+ @moduledoc """
+ The module represents functions to clean an expired OAuth and MFA tokens.
+ """
+ use GenServer
+
+ @ten_seconds 10_000
+ @one_day 86_400_000
+
+ alias Pleroma.MFA
+ alias Pleroma.Web.OAuth
+ alias Pleroma.Workers.BackgroundWorker
+
+ def start_link(_), do: GenServer.start_link(__MODULE__, %{})
+
+ def init(_) do
+ Process.send_after(self(), :perform, @ten_seconds)
+ {:ok, nil}
+ end
+
+ @doc false
+ def handle_info(:perform, state) do
+ BackgroundWorker.enqueue("clean_expired_tokens", %{})
+ interval = Pleroma.Config.get([:oauth2, :clean_expired_tokens_interval], @one_day)
+
+ Process.send_after(self(), :perform, interval)
+ {:noreply, state}
+ end
+
+ def perform(:clean) do
+ OAuth.Token.delete_expired_tokens()
+ MFA.Token.delete_expired_tokens()
+ end
+end
diff --git a/lib/pleroma/web/oauth/token/response.ex b/lib/pleroma/web/oauth/token/response.ex
deleted file mode 100644
index 6f4713dee..000000000
--- a/lib/pleroma/web/oauth/token/response.ex
+++ /dev/null
@@ -1,36 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.OAuth.Token.Response do
- @moduledoc false
-
- alias Pleroma.User
- alias Pleroma.Web.OAuth.Token.Utils
-
- @doc false
- def build(%User{} = user, token, opts \\ %{}) do
- %{
- token_type: "Bearer",
- access_token: token.token,
- refresh_token: token.refresh_token,
- expires_in: expires_in(),
- scope: Enum.join(token.scopes, " "),
- me: user.ap_id
- }
- |> Map.merge(opts)
- end
-
- def build_for_client_credentials(token) do
- %{
- token_type: "Bearer",
- access_token: token.token,
- refresh_token: token.refresh_token,
- created_at: Utils.format_created_at(token),
- expires_in: expires_in(),
- scope: Enum.join(token.scopes, " ")
- }
- end
-
- defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600)
-end
diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex
index c443c888c..de1b0b3f0 100644
--- a/lib/pleroma/web/ostatus/ostatus_controller.ex
+++ b/lib/pleroma/web/ostatus/ostatus_controller.ex
@@ -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]
@@ -28,13 +32,13 @@ defmodule Pleroma.Web.OStatus.OStatusController do
action_fallback(:errors)
- def object(%{assigns: %{format: format}} = conn, %{"uuid" => _uuid})
+ def object(%{assigns: %{format: format}} = conn, _params)
when format in ["json", "activity+json"] do
ActivityPubController.call(conn, :object)
end
- def object(%{assigns: %{format: format}} = conn, %{"uuid" => uuid}) do
- with id <- o_status_url(conn, :object, uuid),
+ def object(%{assigns: %{format: format}} = conn, _params) do
+ with id <- Endpoint.url() <> conn.request_path,
{_, %Activity{} = activity} <-
{:activity, Activity.get_create_by_object_ap_id_with_object(id)},
{_, true} <- {:public?, Visibility.is_public?(activity)} do
@@ -50,13 +54,13 @@ defmodule Pleroma.Web.OStatus.OStatusController do
end
end
- def activity(%{assigns: %{format: format}} = conn, %{"uuid" => _uuid})
+ def activity(%{assigns: %{format: format}} = conn, _params)
when format in ["json", "activity+json"] do
ActivityPubController.call(conn, :activity)
end
- def activity(%{assigns: %{format: format}} = conn, %{"uuid" => uuid}) do
- with id <- o_status_url(conn, :activity, uuid),
+ def activity(%{assigns: %{format: format}} = conn, _params) do
+ with id <- Endpoint.url() <> conn.request_path,
{_, %Activity{} = activity} <- {:activity, Activity.normalize(id)},
{_, true} <- {:public?, Visibility.is_public?(activity)} do
case format do
@@ -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 dcba67d03..563edded7 100644
--- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
@@ -8,47 +8,47 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
import Pleroma.Web.ControllerHelper,
only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2]
- 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(
- OAuthScopesPlug,
- %{scopes: ["follow", "write:follows"]} when action in [:subscribe, :unsubscribe]
+ OpenApiSpex.Plug.PutApiSpec,
+ [module: Pleroma.Web.ApiSpec] when action == :confirmation_resend
)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
plug(
- OAuthScopesPlug,
- %{scopes: ["write:accounts"]}
- # Note: the following actions are not permission-secured in Mastodon:
- when action in [
- :update_avatar,
- :update_banner,
- :update_background
- ]
+ :skip_plug,
+ [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :confirmation_resend
)
- plug(OAuthScopesPlug, %{scopes: ["read:favourites"]} when action == :favourites)
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["follow", "write:follows"]} when action in [:subscribe, :unsubscribe]
+ )
- # 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
@@ -56,62 +56,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
end
end
- @doc "PATCH /api/v1/pleroma/accounts/update_avatar"
- def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) 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
- {:ok, %{data: data}} = ActivityPub.upload(params, type: :avatar)
- {: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)
- 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)
- %{"url" => [%{"href" => href} | _]} = object.data
-
- json(conn, %{url: href})
- end
- end
-
- @doc "PATCH /api/v1/pleroma/accounts/update_background"
- def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
- with {:ok, _user} <- User.update_background(user, %{}) do
- json(conn, %{url: nil})
- end
- end
-
- def update_background(%{assigns: %{user: user}} = conn, params) do
- with {:ok, object} <- ActivityPub.upload(params, type: :background),
- {:ok, _user} <- User.update_background(user, object.data) do
- %{"url" => [%{"href" => href} | _]} = object.data
-
- json(conn, %{url: href})
- end
- end
-
@doc "GET /api/v1/pleroma/accounts/:id/favourites"
def favourites(%{assigns: %{account: %{hide_favorites: true}}} = conn, _params) do
render_error(conn, :forbidden, "Can't get favorites")
@@ -120,9 +64,9 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
def favourites(%{assigns: %{user: for_user, account: user}} = conn, params) do
params =
params
- |> Map.put("type", "Create")
- |> Map.put("favorited_by", user.ap_id)
- |> Map.put("blocking_user", for_user)
+ |> Map.put(:type, "Create")
+ |> Map.put(:favorited_by, user.ap_id)
+ |> Map.put(:blocking_user, for_user)
recipients =
if for_user do
@@ -139,7 +83,11 @@ 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
+ )
end
@doc "POST /api/v1/pleroma/accounts/:id/subscribe"
diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex
new file mode 100644
index 000000000..1f2e953f7
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex
@@ -0,0 +1,179 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+defmodule Pleroma.Web.PleromaAPI.ChatController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Activity
+ alias Pleroma.Chat
+ alias Pleroma.Chat.MessageReference
+ alias Pleroma.Object
+ alias Pleroma.Pagination
+ alias Pleroma.Plugs.OAuthScopesPlug
+ alias Pleroma.Repo
+ alias Pleroma.User
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
+ alias Pleroma.Web.PleromaAPI.ChatView
+
+ import Ecto.Query
+
+ action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["write:chats"]}
+ when action in [
+ :post_chat_message,
+ :create,
+ :mark_as_read,
+ :mark_message_as_read,
+ :delete_message
+ ]
+ )
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["read:chats"]} when action in [:messages, :index, :show]
+ )
+
+ plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ChatOperation
+
+ def delete_message(%{assigns: %{user: %{id: user_id} = user}} = conn, %{
+ message_id: message_id,
+ id: chat_id
+ }) do
+ with %MessageReference{} = cm_ref <-
+ MessageReference.get_by_id(message_id),
+ ^chat_id <- cm_ref.chat_id |> to_string(),
+ %Chat{user_id: ^user_id} <- Chat.get_by_id(chat_id),
+ {:ok, _} <- remove_or_delete(cm_ref, user) do
+ conn
+ |> put_view(MessageReferenceView)
+ |> render("show.json", chat_message_reference: cm_ref)
+ else
+ _e ->
+ {:error, :could_not_delete}
+ end
+ end
+
+ defp remove_or_delete(
+ %{object: %{data: %{"actor" => actor, "id" => id}}},
+ %{ap_id: actor} = user
+ ) do
+ with %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
+ CommonAPI.delete(activity.id, user)
+ end
+ end
+
+ defp remove_or_delete(cm_ref, _) do
+ cm_ref
+ |> MessageReference.delete()
+ end
+
+ def post_chat_message(
+ %{body_params: params, assigns: %{user: %{id: user_id} = user}} = conn,
+ %{
+ id: id
+ }
+ ) do
+ with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id),
+ %User{} = recipient <- User.get_cached_by_ap_id(chat.recipient),
+ {:ok, activity} <-
+ CommonAPI.post_chat_message(user, recipient, params[:content],
+ media_id: params[:media_id]
+ ),
+ message <- Object.normalize(activity, false),
+ cm_ref <- MessageReference.for_chat_and_object(chat, message) do
+ conn
+ |> put_view(MessageReferenceView)
+ |> render("show.json", chat_message_reference: cm_ref)
+ end
+ end
+
+ def mark_message_as_read(%{assigns: %{user: %{id: user_id}}} = conn, %{
+ id: chat_id,
+ message_id: message_id
+ }) do
+ with %MessageReference{} = cm_ref <-
+ MessageReference.get_by_id(message_id),
+ ^chat_id <- cm_ref.chat_id |> to_string(),
+ %Chat{user_id: ^user_id} <- Chat.get_by_id(chat_id),
+ {:ok, cm_ref} <- MessageReference.mark_as_read(cm_ref) do
+ conn
+ |> put_view(MessageReferenceView)
+ |> render("show.json", chat_message_reference: cm_ref)
+ end
+ end
+
+ def mark_as_read(
+ %{
+ body_params: %{last_read_id: last_read_id},
+ assigns: %{user: %{id: user_id}}
+ } = conn,
+ %{id: id}
+ ) do
+ with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id),
+ {_n, _} <-
+ MessageReference.set_all_seen_for_chat(chat, last_read_id) do
+ conn
+ |> put_view(ChatView)
+ |> render("show.json", chat: chat)
+ end
+ end
+
+ def messages(%{assigns: %{user: %{id: user_id}}} = conn, %{id: id} = params) do
+ with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id) do
+ cm_refs =
+ chat
+ |> MessageReference.for_chat_query()
+ |> Pagination.fetch_paginated(params)
+
+ conn
+ |> put_view(MessageReferenceView)
+ |> render("index.json", chat_message_references: cm_refs)
+ else
+ _ ->
+ conn
+ |> put_status(:not_found)
+ |> json(%{error: "not found"})
+ end
+ end
+
+ def index(%{assigns: %{user: %{id: user_id} = user}} = conn, _params) do
+ blocked_ap_ids = User.blocked_users_ap_ids(user)
+
+ chats =
+ from(c in Chat,
+ where: c.user_id == ^user_id,
+ where: c.recipient not in ^blocked_ap_ids,
+ order_by: [desc: c.updated_at],
+ inner_join: u in User,
+ on: u.ap_id == c.recipient
+ )
+ |> Repo.all()
+
+ conn
+ |> put_view(ChatView)
+ |> render("index.json", chats: chats)
+ end
+
+ def create(%{assigns: %{user: user}} = conn, params) do
+ with %User{ap_id: recipient} <- User.get_by_id(params[:id]),
+ {:ok, %Chat{} = chat} <- Chat.get_or_create(user.id, recipient) do
+ conn
+ |> put_view(ChatView)
+ |> render("show.json", chat: chat)
+ end
+ end
+
+ def show(%{assigns: %{user: user}} = conn, params) do
+ with %Chat{} = chat <- Repo.get_by(Chat, user_id: user.id, id: params[:id]) do
+ conn
+ |> put_view(ChatView)
+ |> render("show.json", chat: chat)
+ end
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex b/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex
new file mode 100644
index 000000000..3d007f324
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex
@@ -0,0 +1,94 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.ConversationController do
+ use Pleroma.Web, :controller
+
+ import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
+
+ alias Pleroma.Conversation.Participation
+ alias Pleroma.Plugs.OAuthScopesPlug
+ alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.MastodonAPI.StatusView
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(:put_view, Pleroma.Web.MastodonAPI.ConversationView)
+ plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in [:show, :statuses])
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["write:conversations"]} when action in [:update, :mark_as_read]
+ )
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaConversationOperation
+
+ def show(%{assigns: %{user: %{id: user_id} = user}} = conn, %{id: participation_id}) do
+ with %Participation{user_id: ^user_id} = participation <- Participation.get(participation_id) do
+ render(conn, "participation.json", participation: participation, for: user)
+ else
+ _error ->
+ conn
+ |> put_status(:not_found)
+ |> json(%{"error" => "Unknown conversation id"})
+ end
+ end
+
+ def statuses(
+ %{assigns: %{user: %{id: user_id} = user}} = conn,
+ %{id: participation_id} = params
+ ) do
+ with %Participation{user_id: ^user_id} = participation <-
+ Participation.get(participation_id, preload: [:conversation]) do
+ params =
+ params
+ |> Map.put(:blocking_user, user)
+ |> Map.put(:muting_user, user)
+ |> Map.put(:user, user)
+
+ activities =
+ participation.conversation.ap_id
+ |> ActivityPub.fetch_activities_for_context_query(params)
+ |> Pleroma.Pagination.fetch_paginated(Map.put(params, :total, false))
+ |> Enum.reverse()
+
+ conn
+ |> add_link_headers(activities)
+ |> put_view(StatusView)
+ |> render("index.json", activities: activities, for: user, as: :activity)
+ else
+ _error ->
+ conn
+ |> put_status(:not_found)
+ |> json(%{"error" => "Unknown conversation id"})
+ end
+ end
+
+ def update(
+ %{assigns: %{user: %{id: user_id} = user}} = conn,
+ %{id: participation_id, recipients: recipients}
+ ) do
+ with %Participation{user_id: ^user_id} = participation <- Participation.get(participation_id),
+ {:ok, participation} <- Participation.set_recipients(participation, recipients) do
+ render(conn, "participation.json", participation: participation, for: user)
+ else
+ {:error, message} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{"error" => message})
+
+ _error ->
+ conn
+ |> put_status(:not_found)
+ |> json(%{"error" => "Unknown conversation id"})
+ end
+ end
+
+ def mark_as_read(%{assigns: %{user: user}} = conn, _params) do
+ with {:ok, _, participations} <- Participation.mark_all_as_read(user) do
+ conn
+ |> add_link_headers(participations)
+ |> render("participations.json", participations: participations, for: user)
+ end
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex
deleted file mode 100644
index 03e95e020..000000000
--- a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex
+++ /dev/null
@@ -1,643 +0,0 @@
-defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
- use Pleroma.Web, :controller
-
- alias Pleroma.Plugs.OAuthScopesPlug
-
- require Logger
-
- plug(
- OAuthScopesPlug,
- %{scopes: ["write"], admin: true}
- when action in [
- :create,
- :delete,
- :download_from,
- :list_from,
- :import_from_fs,
- :update_file,
- :update_metadata
- ]
- )
-
- 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!()
-
- json(conn, list_resp)
- else
- 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)
- 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}"})
-
- {: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}"
- })
- 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")
-
- {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}
- 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
-
- _ ->
- create_archive_and_cache(name, pack, pack_dir, pack_file_md5)
- 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")
- 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"
- })
-
- {:exists?, _} ->
- conn
- |> put_status(:not_found)
- |> json(%{error: "Pack #{name} does not exist"})
- 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
- else
- conn
- |> put_status(:internal_server_error)
- |> json(%{error: "The requested instance does not support sharing emoji packs"})
- 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)
-
- if not File.exists?(pack_dir) do
- File.mkdir_p!(pack_dir)
-
- pack_file_p = Path.join(pack_dir, "pack.json")
-
- File.write!(
- pack_file_p,
- Jason.encode!(%{pack: %{}, files: %{}}, pretty: true)
- )
-
- conn |> json("ok")
- else
- conn
- |> put_status(:conflict)
- |> json(%{error: "A pack named \"#{name}\" already exists"})
- 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)
-
- case File.rm_rf(pack_dir) do
- {:ok, _} ->
- conn |> json("ok")
-
- {: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)
- 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)
- end
- end
-
- defp empty?(str), do: String.trim(str) == ""
-
- 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)
- else
- {:has_shortcode, _} ->
- conn
- |> put_status(:conflict)
- |> json(%{error: "An emoji with the \"#{shortcode}\" shortcode already exists"})
-
- true ->
- conn
- |> put_status(:bad_request)
- |> json(%{error: "shortcode or filename cannot be empty"})
- end
- end
-
- # 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")
-
- full_pack = Jason.decode!(File.read!(pack_file_p))
-
- if Map.has_key?(full_pack["files"], shortcode) do
- {emoji_file_path, updated_full_pack} = pop_in(full_pack, ["files", shortcode])
-
- emoji_file_path = Path.join(pack_dir, emoji_file_path)
-
- # Delete the emoji file
- File.rm!(emoji_file_path)
-
- # If the old directory has no more files, remove it
- if String.contains?(emoji_file_path, "/") do
- dir = Path.dirname(emoji_file_path)
-
- if Enum.empty?(File.ls!(dir)) do
- File.rmdir!(dir)
- end
- end
-
- update_file_and_send(conn, updated_full_pack, pack_file_p)
- else
- conn
- |> put_status(:bad_request)
- |> json(%{error: "Emoji \"#{shortcode}\" does not exist"})
- 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)
- else
- {:has_shortcode, _} ->
- conn
- |> put_status(:bad_request)
- |> json(%{error: "Emoji \"#{shortcode}\" does not exist"})
-
- true ->
- conn
- |> put_status(:bad_request)
- |> json(%{error: "new_shortcode or new_filename cannot be empty"})
-
- _ ->
- conn
- |> put_status(:bad_request)
- |> json(%{error: "new_shortcode or new_file were not specified"})
- end
- end
-
- def update_file(conn, %{"action" => action}) do
- conn
- |> put_status(:bad_request)
- |> json(%{error: "Unknown action: #{action}"})
- 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)
- else
- {:ok, %{access: _}} ->
- conn
- |> put_status(:internal_server_error)
- |> json(%{error: "Error: emoji pack directory must be writable"})
-
- {:error, _} ->
- conn
- |> put_status(:internal_server_error)
- |> json(%{error: "Error accessing emoji pack directory"})
- 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
-end
diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex
new file mode 100644
index 000000000..657f46324
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex
@@ -0,0 +1,304 @@
+defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Emoji.Pack
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+ plug(
+ Pleroma.Plugs.OAuthScopesPlug,
+ %{scopes: ["write"], admin: true}
+ when action in [
+ :import_from_filesystem,
+ :remote,
+ :download,
+ :create,
+ :update,
+ :delete,
+ :add_file,
+ :update_file,
+ :delete_file
+ ]
+ )
+
+ @skip_plugs [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]
+ plug(:skip_plug, @skip_plugs when action in [:index, :show, :archive])
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaEmojiPackOperation
+
+ def remote(conn, %{url: url}) do
+ with {:ok, packs} <- Pack.list_remote(url) do
+ json(conn, packs)
+ else
+ {:error, :not_shareable} ->
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{error: "The requested instance does not support sharing emoji packs"})
+ end
+ end
+
+ def index(conn, params) do
+ emoji_path =
+ [:instance, :static_dir]
+ |> Pleroma.Config.get!()
+ |> Path.join("emoji")
+
+ with {:ok, packs, count} <- Pack.list_local(page: params.page, page_size: params.page_size) do
+ json(conn, %{packs: packs, count: count})
+ else
+ {:error, :create_dir, e} ->
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{error: "Failed to create the emoji pack directory at #{emoji_path}: #{e}"})
+
+ {:error, :ls, e} ->
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{
+ error: "Failed to get the contents of the emoji pack directory at #{emoji_path}: #{e}"
+ })
+ end
+ end
+
+ def show(conn, %{name: name, page: page, page_size: page_size}) do
+ name = String.trim(name)
+
+ with {:ok, pack} <- Pack.show(name: name, page: page, page_size: page_size) do
+ json(conn, pack)
+ else
+ {:error, :not_found} ->
+ conn
+ |> put_status(:not_found)
+ |> json(%{error: "Pack #{name} does not exist"})
+
+ {:error, :empty_values} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "pack name cannot be empty"})
+ end
+ end
+
+ def archive(conn, %{name: name}) do
+ with {:ok, archive} <- Pack.get_archive(name) do
+ send_download(conn, {:binary, archive}, filename: "#{name}.zip")
+ else
+ {:error, :cant_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, :not_found} ->
+ conn
+ |> put_status(:not_found)
+ |> json(%{error: "Pack #{name} does not exist"})
+ end
+ end
+
+ def download(%{body_params: %{url: url, name: name} = params} = conn, _) do
+ with {:ok, _pack} <- Pack.download(name, url, params[:as]) do
+ json(conn, "ok")
+ else
+ {:error, :not_shareable} ->
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{error: "The requested instance does not support sharing emoji packs"})
+
+ {:error, :invalid_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
+
+ def create(conn, %{name: name}) do
+ name = String.trim(name)
+
+ with {:ok, _pack} <- Pack.create(name) do
+ json(conn, "ok")
+ else
+ {:error, :eexist} ->
+ conn
+ |> put_status(:conflict)
+ |> json(%{error: "A pack named \"#{name}\" already exists"})
+
+ {:error, :empty_values} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "pack name cannot be empty"})
+
+ {:error, _} ->
+ render_error(
+ conn,
+ :internal_server_error,
+ "Unexpected error occurred while creating pack."
+ )
+ end
+ end
+
+ def delete(conn, %{name: name}) do
+ 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"})
+
+ {:error, :empty_values} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "pack name cannot be empty"})
+
+ {:error, _, _} ->
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{error: "Couldn't delete the pack #{name}"})
+ end
+ end
+
+ def update(%{body_params: %{metadata: metadata}} = conn, %{name: name}) do
+ with {:ok, pack} <- Pack.update_metadata(name, metadata) do
+ json(conn, pack.pack)
+ else
+ {:error, :incomplete} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "The fallback archive does not have all files specified in pack.json"})
+
+ {:error, _} ->
+ render_error(
+ conn,
+ :internal_server_error,
+ "Unexpected error occurred while updating pack metadata."
+ )
+ end
+ end
+
+ def add_file(%{body_params: params} = conn, %{name: name}) do
+ filename = params[:filename] || get_filename(params[:file])
+ shortcode = params[:shortcode] || Path.basename(filename, Path.extname(filename))
+
+ with {:ok, pack} <- Pack.add_file(name, shortcode, filename, params[:file]) do
+ json(conn, pack.files)
+ else
+ {:error, :already_exists} ->
+ conn
+ |> put_status(:conflict)
+ |> json(%{error: "An emoji with the \"#{shortcode}\" shortcode already exists"})
+
+ {:error, :not_found} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "pack \"#{name}\" is not found"})
+
+ {:error, :empty_values} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "pack name, shortcode or filename cannot be empty"})
+
+ {:error, _} ->
+ render_error(
+ conn,
+ :internal_server_error,
+ "Unexpected error occurred while adding file to pack."
+ )
+ end
+ end
+
+ def update_file(%{body_params: %{shortcode: shortcode} = params} = conn, %{name: name}) do
+ new_shortcode = params[:new_shortcode]
+ new_filename = params[:new_filename]
+ force = params[:force]
+
+ with {:ok, pack} <- Pack.update_file(name, shortcode, new_shortcode, new_filename, force) do
+ json(conn, pack.files)
+ else
+ {:error, :doesnt_exist} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "Emoji \"#{shortcode}\" does not exist"})
+
+ {:error, :already_exists} ->
+ conn
+ |> put_status(:conflict)
+ |> json(%{
+ error:
+ "New shortcode \"#{new_shortcode}\" is already used. If you want to override emoji use 'force' option"
+ })
+
+ {:error, :not_found} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "pack \"#{name}\" is not found"})
+
+ {:error, :empty_values} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "new_shortcode or new_filename cannot be empty"})
+
+ {:error, _} ->
+ render_error(
+ conn,
+ :internal_server_error,
+ "Unexpected error occurred while updating file in pack."
+ )
+ end
+ end
+
+ def delete_file(conn, %{name: name, shortcode: shortcode}) do
+ with {:ok, pack} <- Pack.delete_file(name, shortcode) do
+ json(conn, pack.files)
+ else
+ {:error, :doesnt_exist} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "Emoji \"#{shortcode}\" does not exist"})
+
+ {:error, :not_found} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "pack \"#{name}\" is not found"})
+
+ {:error, :empty_values} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "pack name or shortcode cannot be empty"})
+
+ {:error, _} ->
+ render_error(
+ conn,
+ :internal_server_error,
+ "Unexpected error occurred while removing file from pack."
+ )
+ end
+ end
+
+ def import_from_filesystem(conn, _params) do
+ with {:ok, names} <- Pack.import_from_filesystem() do
+ json(conn, names)
+ else
+ {:error, :no_read_write} ->
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{error: "Error: emoji pack directory must be writable"})
+
+ {:error, _} ->
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{error: "Error accessing emoji pack directory"})
+ 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/emoji_reaction_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex
new file mode 100644
index 000000000..7f9254c13
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.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.PleromaAPI.EmojiReactionController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Activity
+ alias Pleroma.Object
+ alias Pleroma.Plugs.OAuthScopesPlug
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.MastodonAPI.StatusView
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action in [:create, :delete])
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["read:statuses"], fallback: :proceed_unauthenticated}
+ when action == :index
+ )
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.EmojiReactionOperation
+
+ action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+
+ def index(%{assigns: %{user: user}} = conn, %{id: activity_id} = params) do
+ with true <- Pleroma.Config.get([:instance, :show_reactions]),
+ %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
+ %Object{data: %{"reactions" => reactions}} when is_list(reactions) <-
+ Object.normalize(activity) do
+ reactions = filter(reactions, params)
+ render(conn, "index.json", emoji_reactions: reactions, user: user)
+ else
+ _e -> json(conn, [])
+ end
+ end
+
+ defp filter(reactions, %{emoji: emoji}) when is_binary(emoji) do
+ Enum.filter(reactions, fn [e, _] -> e == emoji end)
+ end
+
+ defp filter(reactions, _), do: reactions
+
+ def create(%{assigns: %{user: user}} = conn, %{id: activity_id, emoji: emoji}) do
+ with {:ok, _activity} <- CommonAPI.react_with_emoji(activity_id, user, emoji) do
+ activity = Activity.get_by_id(activity_id)
+
+ conn
+ |> put_view(StatusView)
+ |> render("show.json", activity: activity, for: user, as: :activity)
+ end
+ end
+
+ def delete(%{assigns: %{user: user}} = conn, %{id: activity_id, emoji: emoji}) do
+ with {:ok, _activity} <- CommonAPI.unreact_with_emoji(activity_id, user, emoji) do
+ activity = Activity.get_by_id(activity_id)
+
+ conn
+ |> put_view(StatusView)
+ |> render("show.json", activity: activity, for: user, as: :activity)
+ end
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex
index d9c1c8636..df6c50ca5 100644
--- a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex
@@ -9,10 +9,11 @@ defmodule Pleroma.Web.PleromaAPI.MascotController do
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(OAuthScopesPlug, %{scopes: ["read:accounts"]} when action == :show)
plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action != :show)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaMascotOperation
@doc "GET /api/v1/pleroma/mascot"
def show(%{assigns: %{user: user}} = conn, _params) do
@@ -20,7 +21,7 @@ defmodule Pleroma.Web.PleromaAPI.MascotController do
end
@doc "PUT /api/v1/pleroma/mascot"
- def update(%{assigns: %{user: user}} = conn, %{"file" => file}) do
+ def update(%{assigns: %{user: user}, body_params: %{file: file}} = conn, _) do
with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
# Reject if not an image
%{type: "image"} = attachment <- render_attachment(object) do
diff --git a/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex b/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex
new file mode 100644
index 000000000..3ed8bd294
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex
@@ -0,0 +1,36 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.NotificationController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Notification
+ alias Pleroma.Plugs.OAuthScopesPlug
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :mark_as_read)
+ plug(:put_view, Pleroma.Web.MastodonAPI.NotificationView)
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaNotificationOperation
+
+ def mark_as_read(%{assigns: %{user: user}, body_params: %{id: notification_id}} = conn, _) do
+ with {:ok, notification} <- Notification.read_one(user, notification_id) do
+ render(conn, "show.json", notification: notification, for: user)
+ else
+ {:error, message} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{"error" => message})
+ end
+ end
+
+ def mark_as_read(%{assigns: %{user: user}, body_params: %{max_id: max_id}} = conn, _) do
+ notifications =
+ user
+ |> Notification.set_read_up_to(max_id)
+ |> Enum.take(80)
+
+ render(conn, "index.json", notifications: notifications, for: user)
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
deleted file mode 100644
index f0867c2c1..000000000
--- a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
+++ /dev/null
@@ -1,199 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
- use Pleroma.Web, :controller
-
- import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
-
- alias Pleroma.Activity
- alias Pleroma.Conversation.Participation
- alias Pleroma.Notification
- alias Pleroma.Object
- alias Pleroma.Plugs.OAuthScopesPlug
- alias Pleroma.User
- alias Pleroma.Web.ActivityPub.ActivityPub
- alias Pleroma.Web.CommonAPI
- alias Pleroma.Web.MastodonAPI.AccountView
- alias Pleroma.Web.MastodonAPI.ConversationView
- alias Pleroma.Web.MastodonAPI.NotificationView
- alias Pleroma.Web.MastodonAPI.StatusView
-
- plug(
- OAuthScopesPlug,
- %{scopes: ["read:statuses"]}
- when action in [:conversation, :conversation_statuses]
- )
-
- plug(
- OAuthScopesPlug,
- %{scopes: ["write:statuses"]}
- when action in [:react_with_emoji, :unreact_with_emoji]
- )
-
- plug(
- OAuthScopesPlug,
- %{scopes: ["write:conversations"]} when action in [:update_conversation, :read_conversations]
- )
-
- plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :read_notification)
-
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
-
- def emoji_reactions_by(%{assigns: %{user: user}} = conn, %{"id" => activity_id} = params) do
- with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
- %Object{data: %{"reactions" => emoji_reactions}} when is_list(emoji_reactions) <-
- Object.normalize(activity) do
- reactions =
- emoji_reactions
- |> Enum.map(fn [emoji, user_ap_ids] ->
- if params["emoji"] && params["emoji"] != emoji do
- nil
- else
- users =
- Enum.map(user_ap_ids, &User.get_cached_by_ap_id/1)
- |> Enum.filter(fn
- %{deactivated: false} -> true
- _ -> false
- end)
-
- %{
- name: emoji,
- count: length(users),
- accounts: AccountView.render("index.json", %{users: users, for: user, as: :user}),
- me: !!(user && user.ap_id in user_ap_ids)
- }
- end
- end)
- |> Enum.filter(& &1)
-
- conn
- |> json(reactions)
- else
- _e ->
- conn
- |> json([])
- end
- end
-
- def react_with_emoji(%{assigns: %{user: user}} = conn, %{"id" => activity_id, "emoji" => emoji}) do
- with {:ok, _activity, _object} <- CommonAPI.react_with_emoji(activity_id, user, emoji),
- activity <- Activity.get_by_id(activity_id) do
- conn
- |> put_view(StatusView)
- |> render("show.json", %{activity: activity, for: user, as: :activity})
- end
- end
-
- def unreact_with_emoji(%{assigns: %{user: user}} = conn, %{
- "id" => activity_id,
- "emoji" => emoji
- }) do
- with {:ok, _activity, _object} <- CommonAPI.unreact_with_emoji(activity_id, user, emoji),
- activity <- Activity.get_by_id(activity_id) do
- conn
- |> put_view(StatusView)
- |> render("show.json", %{activity: activity, for: user, as: :activity})
- end
- end
-
- def conversation(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
- with %Participation{} = participation <- Participation.get(participation_id),
- true <- user.id == participation.user_id do
- conn
- |> put_view(ConversationView)
- |> render("participation.json", %{participation: participation, for: user})
- else
- _error ->
- conn
- |> put_status(404)
- |> json(%{"error" => "Unknown conversation id"})
- end
- end
-
- def conversation_statuses(
- %{assigns: %{user: user}} = conn,
- %{"id" => participation_id} = params
- ) do
- with %Participation{} = participation <-
- Participation.get(participation_id, preload: [:conversation]),
- true <- user.id == participation.user_id do
- params =
- params
- |> Map.put("blocking_user", user)
- |> Map.put("muting_user", user)
- |> Map.put("user", user)
-
- activities =
- participation.conversation.ap_id
- |> ActivityPub.fetch_activities_for_context(params)
- |> Enum.reverse()
-
- conn
- |> add_link_headers(activities)
- |> put_view(StatusView)
- |> render("index.json", %{activities: activities, for: user, as: :activity})
- else
- _error ->
- conn
- |> put_status(404)
- |> json(%{"error" => "Unknown conversation id"})
- end
- end
-
- def update_conversation(
- %{assigns: %{user: user}} = conn,
- %{"id" => participation_id, "recipients" => recipients}
- ) do
- with %Participation{} = participation <- Participation.get(participation_id),
- true <- user.id == participation.user_id,
- {:ok, participation} <- Participation.set_recipients(participation, recipients) do
- conn
- |> put_view(ConversationView)
- |> render("participation.json", %{participation: participation, for: user})
- else
- {:error, message} ->
- conn
- |> put_status(:bad_request)
- |> json(%{"error" => message})
-
- _error ->
- conn
- |> put_status(404)
- |> json(%{"error" => "Unknown conversation id"})
- end
- end
-
- def read_conversations(%{assigns: %{user: user}} = conn, _params) do
- with {:ok, _, participations} <- Participation.mark_all_as_read(user) do
- conn
- |> add_link_headers(participations)
- |> put_view(ConversationView)
- |> render("participations.json", participations: participations, for: user)
- end
- end
-
- def read_notification(%{assigns: %{user: user}} = conn, %{"id" => notification_id}) do
- with {:ok, notification} <- Notification.read_one(user, notification_id) do
- conn
- |> put_view(NotificationView)
- |> render("show.json", %{notification: notification, for: user})
- else
- {:error, message} ->
- conn
- |> put_status(:bad_request)
- |> json(%{"error" => message})
- end
- end
-
- def read_notification(%{assigns: %{user: user}} = conn, %{"max_id" => max_id}) 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})
- 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 4463ec477..e9a4fba92 100644
--- a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex
@@ -5,32 +5,27 @@
defmodule Pleroma.Web.PleromaAPI.ScrobbleController do
use Pleroma.Web, :controller
- import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2, fetch_integer_param: 2]
+ import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
- alias Pleroma.Web.MastodonAPI.StatusView
- plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :user_scrobbles)
- plug(OAuthScopesPlug, %{scopes: ["write"]} when action != :user_scrobbles)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["read"], fallback: :proceed_unauthenticated} when action == :index
+ )
- def new_scrobble(%{assigns: %{user: user}} = conn, %{"title" => _} = params) do
- params =
- if !params["length"] do
- params
- else
- params
- |> Map.put("length", fetch_integer_param(params, "length"))
- end
+ plug(OAuthScopesPlug, %{scopes: ["write"]} when action == :create)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaScrobbleOperation
+
+ def create(%{assigns: %{user: user}, body_params: params} = conn, _) do
with {:ok, activity} <- CommonAPI.listen(user, params) do
- conn
- |> put_view(StatusView)
- |> render("listen.json", %{activity: activity, for: user})
+ render(conn, "show.json", activity: activity, for: user)
else
{:error, message} ->
conn
@@ -39,16 +34,15 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleController do
end
end
- def user_scrobbles(%{assigns: %{user: reading_user}} = conn, params) do
- with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
- params = Map.put(params, "type", ["Listen"])
+ def index(%{assigns: %{user: reading_user}} = conn, %{id: id} = params) do
+ with %User{} = user <- User.get_cached_by_nickname_or_id(id, for: reading_user) do
+ params = Map.put(params, :type, ["Listen"])
activities = ActivityPub.fetch_user_abstract_activities(user, reading_user, params)
conn
|> add_link_headers(activities)
- |> put_view(StatusView)
- |> render("listens.json", %{
+ |> render("index.json", %{
activities: activities,
for: reading_user,
as: :activity
diff --git a/lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex b/lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex
new file mode 100644
index 000000000..b86791d09
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex
@@ -0,0 +1,133 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.TwoFactorAuthenticationController do
+ @moduledoc "The module represents actions to manage MFA"
+ use Pleroma.Web, :controller
+
+ import Pleroma.Web.ControllerHelper, only: [json_response: 3]
+
+ alias Pleroma.MFA
+ alias Pleroma.MFA.TOTP
+ alias Pleroma.Plugs.OAuthScopesPlug
+ alias Pleroma.Web.CommonAPI.Utils
+
+ plug(OAuthScopesPlug, %{scopes: ["read:security"]} when action in [:settings])
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["write:security"]} when action in [:setup, :confirm, :disable, :backup_codes]
+ )
+
+ @doc """
+ Gets user multi factor authentication settings
+
+ ## Endpoint
+ GET /api/pleroma/accounts/mfa
+
+ """
+ def settings(%{assigns: %{user: user}} = conn, _params) do
+ json(conn, %{settings: MFA.mfa_settings(user)})
+ end
+
+ @doc """
+ Prepare setup mfa method
+
+ ## Endpoint
+ GET /api/pleroma/accounts/mfa/setup/[:method]
+
+ """
+ def setup(%{assigns: %{user: user}} = conn, %{"method" => "totp"} = _params) do
+ with {:ok, user} <- MFA.setup_totp(user),
+ %{secret: secret} = _ <- user.multi_factor_authentication_settings.totp do
+ provisioning_uri = TOTP.provisioning_uri(secret, "#{user.email}")
+
+ json(conn, %{provisioning_uri: provisioning_uri, key: secret})
+ else
+ {:error, message} ->
+ json_response(conn, :unprocessable_entity, %{error: message})
+ end
+ end
+
+ def setup(conn, _params) do
+ json_response(conn, :bad_request, %{error: "undefined method"})
+ end
+
+ @doc """
+ Confirms setup and enable mfa method
+
+ ## Endpoint
+ POST /api/pleroma/accounts/mfa/confirm/:method
+
+ - params:
+ `code` - confirmation code
+ `password` - current password
+ """
+ def confirm(
+ %{assigns: %{user: user}} = conn,
+ %{"method" => "totp", "password" => _, "code" => _} = params
+ ) do
+ with {:ok, _user} <- Utils.confirm_current_password(user, params["password"]),
+ {:ok, _user} <- MFA.confirm_totp(user, params) do
+ json(conn, %{})
+ else
+ {:error, message} ->
+ json_response(conn, :unprocessable_entity, %{error: message})
+ end
+ end
+
+ def confirm(conn, _) do
+ json_response(conn, :bad_request, %{error: "undefined mfa method"})
+ end
+
+ @doc """
+ Disable mfa method and disable mfa if need.
+ """
+ def disable(%{assigns: %{user: user}} = conn, %{"method" => "totp"} = params) do
+ with {:ok, user} <- Utils.confirm_current_password(user, params["password"]),
+ {:ok, _user} <- MFA.disable_totp(user) do
+ json(conn, %{})
+ else
+ {:error, message} ->
+ json_response(conn, :unprocessable_entity, %{error: message})
+ end
+ end
+
+ def disable(%{assigns: %{user: user}} = conn, %{"method" => "mfa"} = params) do
+ with {:ok, user} <- Utils.confirm_current_password(user, params["password"]),
+ {:ok, _user} <- MFA.disable(user) do
+ json(conn, %{})
+ else
+ {:error, message} ->
+ json_response(conn, :unprocessable_entity, %{error: message})
+ end
+ end
+
+ def disable(conn, _) do
+ json_response(conn, :bad_request, %{error: "undefined mfa method"})
+ end
+
+ @doc """
+ Generates backup codes.
+
+ ## Endpoint
+ GET /api/pleroma/accounts/mfa/backup_codes
+
+ ## Response
+ ### Success
+ `{codes: [codes]}`
+
+ ### Error
+ `{error: [error_message]}`
+
+ """
+ def backup_codes(%{assigns: %{user: user}} = conn, _params) do
+ with {:ok, codes} <- MFA.generate_backup_codes(user) do
+ json(conn, %{codes: codes})
+ else
+ {:error, message} ->
+ json_response(conn, :unprocessable_entity, %{error: message})
+ end
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/views/chat/message_reference_view.ex b/lib/pleroma/web/pleroma_api/views/chat/message_reference_view.ex
new file mode 100644
index 000000000..d4e08b50d
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/views/chat/message_reference_view.ex
@@ -0,0 +1,50 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.Chat.MessageReferenceView do
+ use Pleroma.Web, :view
+
+ alias Pleroma.User
+ alias Pleroma.Web.CommonAPI.Utils
+ alias Pleroma.Web.MastodonAPI.StatusView
+
+ def render(
+ "show.json",
+ %{
+ chat_message_reference: %{
+ id: id,
+ object: %{data: chat_message} = object,
+ chat_id: chat_id,
+ unread: unread
+ }
+ }
+ ) do
+ %{
+ id: id |> to_string(),
+ content: chat_message["content"],
+ chat_id: chat_id |> to_string(),
+ account_id: User.get_cached_by_ap_id(chat_message["actor"]).id,
+ created_at: Utils.to_masto_date(chat_message["published"]),
+ emojis: StatusView.build_emojis(chat_message["emoji"]),
+ attachment:
+ chat_message["attachment"] &&
+ StatusView.render("attachment.json", attachment: chat_message["attachment"]),
+ unread: unread,
+ card:
+ StatusView.render(
+ "card.json",
+ Pleroma.Web.RichMedia.Helpers.fetch_data_for_object(object)
+ )
+ }
+ end
+
+ def render("index.json", opts) do
+ render_many(
+ opts[:chat_message_references],
+ __MODULE__,
+ "show.json",
+ Map.put(opts, :as, :chat_message_reference)
+ )
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/views/chat_view.ex b/lib/pleroma/web/pleroma_api/views/chat_view.ex
new file mode 100644
index 000000000..04dc20d51
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/views/chat_view.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.PleromaAPI.ChatView do
+ use Pleroma.Web, :view
+
+ alias Pleroma.Chat
+ alias Pleroma.Chat.MessageReference
+ alias Pleroma.User
+ alias Pleroma.Web.CommonAPI.Utils
+ alias Pleroma.Web.MastodonAPI.AccountView
+ alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
+
+ def render("show.json", %{chat: %Chat{} = chat} = opts) do
+ recipient = User.get_cached_by_ap_id(chat.recipient)
+ last_message = opts[:last_message] || MessageReference.last_message_for_chat(chat)
+ account_view_opts = account_view_opts(opts, recipient)
+
+ %{
+ id: chat.id |> to_string(),
+ account: AccountView.render("show.json", account_view_opts),
+ unread: MessageReference.unread_count_for_chat(chat),
+ last_message:
+ last_message &&
+ MessageReferenceView.render("show.json", chat_message_reference: last_message),
+ updated_at: Utils.to_masto_date(chat.updated_at)
+ }
+ end
+
+ def render("index.json", %{chats: chats} = opts) do
+ render_many(chats, __MODULE__, "show.json", Map.delete(opts, :chats))
+ end
+
+ defp account_view_opts(opts, recipient) do
+ account_view_opts = Map.put(opts, :user, recipient)
+
+ if Map.has_key?(account_view_opts, :for) do
+ account_view_opts
+ else
+ Map.put(account_view_opts, :skip_visibility_check, true)
+ end
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex b/lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex
new file mode 100644
index 000000000..e0f98b50a
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex
@@ -0,0 +1,33 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.EmojiReactionView do
+ use Pleroma.Web, :view
+
+ alias Pleroma.Web.MastodonAPI.AccountView
+
+ def render("index.json", %{emoji_reactions: emoji_reactions} = opts) do
+ render_many(emoji_reactions, __MODULE__, "show.json", opts)
+ end
+
+ def render("show.json", %{emoji_reaction: [emoji, user_ap_ids], user: user}) do
+ users = fetch_users(user_ap_ids)
+
+ %{
+ name: emoji,
+ count: length(users),
+ accounts: render(AccountView, "index.json", users: users, for: user),
+ me: !!(user && user.ap_id in user_ap_ids)
+ }
+ end
+
+ defp fetch_users(user_ap_ids) do
+ user_ap_ids
+ |> Enum.map(&Pleroma.User.get_cached_by_ap_id/1)
+ |> Enum.filter(fn
+ %{deactivated: false} -> true
+ _ -> false
+ end)
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/views/scrobble_view.ex b/lib/pleroma/web/pleroma_api/views/scrobble_view.ex
new file mode 100644
index 000000000..bbff93abe
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/views/scrobble_view.ex
@@ -0,0 +1,37 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.ScrobbleView do
+ use Pleroma.Web, :view
+
+ require Pleroma.Constants
+
+ alias Pleroma.Activity
+ alias Pleroma.HTML
+ alias Pleroma.Object
+ alias Pleroma.Web.CommonAPI.Utils
+ alias Pleroma.Web.MastodonAPI.AccountView
+ alias Pleroma.Web.MastodonAPI.StatusView
+
+ def render("show.json", %{activity: %Activity{data: %{"type" => "Listen"}} = activity} = opts) do
+ object = Object.normalize(activity)
+
+ user = StatusView.get_user(activity.data["actor"])
+ created_at = Utils.to_masto_date(activity.data["published"])
+
+ %{
+ id: activity.id,
+ account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
+ created_at: created_at,
+ title: object.data["title"] |> HTML.strip_tags(),
+ artist: object.data["artist"] |> HTML.strip_tags(),
+ album: object.data["album"] |> HTML.strip_tags(),
+ length: object.data["length"]
+ }
+ end
+
+ def render("index.json", opts) do
+ safe_render_many(opts.activities, __MODULE__, "show.json", opts)
+ end
+end
diff --git a/lib/pleroma/web/preload.ex b/lib/pleroma/web/preload.ex
new file mode 100644
index 000000000..90e454468
--- /dev/null
+++ b/lib/pleroma/web/preload.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.Preload do
+ alias Phoenix.HTML
+ require Logger
+
+ def build_tags(_conn, params) do
+ preload_data =
+ Enum.reduce(Pleroma.Config.get([__MODULE__, :providers], []), %{}, fn parser, acc ->
+ terms =
+ params
+ |> parser.generate_terms()
+ |> Enum.map(fn {k, v} -> {k, Base.encode64(Jason.encode!(v))} end)
+ |> Enum.into(%{})
+
+ Map.merge(acc, terms)
+ end)
+
+ rendered_html =
+ preload_data
+ |> Jason.encode!()
+ |> build_script_tag()
+ |> HTML.safe_to_string()
+
+ rendered_html
+ end
+
+ def build_script_tag(content) do
+ HTML.Tag.content_tag(:script, HTML.raw(content),
+ id: "initial-results",
+ type: "application/json"
+ )
+ end
+end
diff --git a/lib/pleroma/web/preload/instance.ex b/lib/pleroma/web/preload/instance.ex
new file mode 100644
index 000000000..50d1f3382
--- /dev/null
+++ b/lib/pleroma/web/preload/instance.ex
@@ -0,0 +1,50 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Preload.Providers.Instance do
+ alias Pleroma.Plugs.InstanceStatic
+ alias Pleroma.Web.MastodonAPI.InstanceView
+ alias Pleroma.Web.Nodeinfo.Nodeinfo
+ alias Pleroma.Web.Preload.Providers.Provider
+
+ @behaviour Provider
+ @instance_url "/api/v1/instance"
+ @panel_url "/instance/panel.html"
+ @nodeinfo_url "/nodeinfo/2.0.json"
+
+ @impl Provider
+ def generate_terms(_params) do
+ %{}
+ |> build_info_tag()
+ |> build_panel_tag()
+ |> build_nodeinfo_tag()
+ end
+
+ defp build_info_tag(acc) do
+ info_data = InstanceView.render("show.json", %{})
+
+ Map.put(acc, @instance_url, info_data)
+ end
+
+ defp build_panel_tag(acc) do
+ instance_path = InstanceStatic.file_path(@panel_url |> to_string())
+
+ if File.exists?(instance_path) do
+ panel_data = File.read!(instance_path)
+ Map.put(acc, @panel_url, panel_data)
+ else
+ acc
+ end
+ end
+
+ defp build_nodeinfo_tag(acc) do
+ case Nodeinfo.get_nodeinfo("2.0") do
+ {:error, _} ->
+ acc
+
+ nodeinfo_data ->
+ Map.put(acc, @nodeinfo_url, nodeinfo_data)
+ end
+ end
+end
diff --git a/lib/pleroma/web/preload/provider.ex b/lib/pleroma/web/preload/provider.ex
new file mode 100644
index 000000000..7ef595a34
--- /dev/null
+++ b/lib/pleroma/web/preload/provider.ex
@@ -0,0 +1,7 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Preload.Providers.Provider do
+ @callback generate_terms(map()) :: map()
+end
diff --git a/lib/pleroma/web/preload/timelines.ex b/lib/pleroma/web/preload/timelines.ex
new file mode 100644
index 000000000..b279a865d
--- /dev/null
+++ b/lib/pleroma/web/preload/timelines.ex
@@ -0,0 +1,39 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Preload.Providers.Timelines do
+ alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.MastodonAPI.StatusView
+ alias Pleroma.Web.Preload.Providers.Provider
+
+ @behaviour Provider
+ @public_url "/api/v1/timelines/public"
+
+ @impl Provider
+ def generate_terms(params) do
+ build_public_tag(%{}, params)
+ end
+
+ def build_public_tag(acc, params) do
+ if Pleroma.Config.restrict_unauthenticated_access?(:timelines, :federated) do
+ acc
+ else
+ Map.put(acc, @public_url, public_timeline(params))
+ end
+ end
+
+ defp public_timeline(%{"path" => ["main", "all"]}), do: get_public_timeline(false)
+
+ defp public_timeline(_params), do: get_public_timeline(true)
+
+ defp get_public_timeline(local_only) do
+ activities =
+ ActivityPub.fetch_public_activities(%{
+ type: ["Create"],
+ local_only: local_only
+ })
+
+ StatusView.render("index.json", activities: activities, for: nil, as: :activity)
+ end
+end
diff --git a/lib/pleroma/web/preload/user.ex b/lib/pleroma/web/preload/user.ex
new file mode 100644
index 000000000..b3d2e9b8d
--- /dev/null
+++ b/lib/pleroma/web/preload/user.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.Preload.Providers.User do
+ alias Pleroma.User
+ alias Pleroma.Web.MastodonAPI.AccountView
+ alias Pleroma.Web.Preload.Providers.Provider
+
+ @behaviour Provider
+ @account_url_base "/api/v1/accounts"
+
+ @impl Provider
+ def generate_terms(%{user: user}) do
+ build_accounts_tag(%{}, user)
+ end
+
+ def generate_terms(_params), do: %{}
+
+ def build_accounts_tag(acc, %User{} = user) do
+ account_data = AccountView.render("show.json", %{user: user, for: user})
+ Map.put(acc, "#{@account_url_base}/#{user.id}", account_data)
+ end
+
+ def build_accounts_tag(acc, _), do: acc
+end
diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex
index f1740a6e0..16368485e 100644
--- a/lib/pleroma/web/push/impl.ex
+++ b/lib/pleroma/web/push/impl.ex
@@ -16,8 +16,6 @@ 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"
@@ -31,10 +29,10 @@ defmodule Pleroma.Web.Push.Impl do
when activity_type in @types do
actor = User.get_cached_by_ap_id(notification.activity.data["actor"])
- mastodon_type = mastodon_notification_type(notification.activity)
+ mastodon_type = notification.type
gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key)
avatar_url = User.avatar_url(actor)
- object = Object.normalize(activity)
+ object = Object.normalize(activity, false)
user = User.get_cached_by_id(user_id)
direct_conversation_id = Activity.direct_conversation_id(activity, user)
@@ -55,11 +53,12 @@ defmodule Pleroma.Web.Push.Impl do
|> 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"
@@ -105,18 +104,17 @@ defmodule Pleroma.Web.Push.Impl do
def build_content(
%{
- activity: %{data: %{"directMessage" => true}},
- user: %{notification_settings: %{privacy_option: true}}
- },
- actor,
+ user: %{notification_settings: %{hide_notification_contents: true}}
+ } = notification,
+ _actor,
_object,
- _mastodon_type
+ mastodon_type
) do
- %{title: "New Direct Message", body: "@#{actor.nickname}"}
+ %{body: format_title(notification, mastodon_type)}
end
def build_content(notification, actor, object, mastodon_type) do
- mastodon_type = mastodon_type || mastodon_notification_type(notification.activity)
+ mastodon_type = mastodon_type || notification.type
%{
title: format_title(notification, mastodon_type),
@@ -126,6 +124,13 @@ defmodule Pleroma.Web.Push.Impl do
def format_body(activity, actor, object, mastodon_type \\ nil)
+ def format_body(_activity, actor, %{data: %{"type" => "ChatMessage", "content" => content}}, _) do
+ case content do
+ nil -> "@#{actor.nickname}: (Attachment)"
+ content -> "@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}"
+ end
+ end
+
def format_body(
%{activity: %{data: %{"type" => "Create"}}},
actor,
@@ -151,7 +156,7 @@ defmodule Pleroma.Web.Push.Impl do
mastodon_type
)
when type in ["Follow", "Like"] do
- mastodon_type = mastodon_type || mastodon_notification_type(notification.activity)
+ mastodon_type = mastodon_type || notification.type
case mastodon_type do
"follow" -> "@#{actor.nickname} has followed you"
@@ -166,15 +171,14 @@ defmodule Pleroma.Web.Push.Impl do
"New Direct Message"
end
- def format_title(%{activity: activity}, mastodon_type) do
- mastodon_type = mastodon_type || mastodon_notification_type(activity)
-
- case mastodon_type do
+ def format_title(%{type: type}, mastodon_type) do
+ case mastodon_type || type do
"mention" -> "New Mention"
"follow" -> "New Follower"
"follow_request" -> "New Follow Request"
"reblog" -> "New Repeat"
"favourite" -> "New Favorite"
+ "pleroma:chat_mention" -> "New Chat Message"
type -> "New #{String.capitalize(type || "event")}"
end
end
diff --git a/lib/pleroma/web/push/subscription.ex b/lib/pleroma/web/push/subscription.ex
index b99b0c5fb..5b5aa0d59 100644
--- a/lib/pleroma/web/push/subscription.ex
+++ b/lib/pleroma/web/push/subscription.ex
@@ -25,9 +25,9 @@ defmodule Pleroma.Web.Push.Subscription do
timestamps()
end
- @supported_alert_types ~w[follow favourite mention reblog]
+ @supported_alert_types ~w[follow favourite mention reblog pleroma:chat_mention]a
- defp alerts(%{"data" => %{"alerts" => alerts}}) do
+ defp alerts(%{data: %{alerts: alerts}}) do
alerts = Map.take(alerts, @supported_alert_types)
%{"alerts" => alerts}
end
@@ -44,9 +44,9 @@ defmodule Pleroma.Web.Push.Subscription do
%User{} = user,
%Token{} = token,
%{
- "subscription" => %{
- "endpoint" => endpoint,
- "keys" => %{"auth" => key_auth, "p256dh" => key_p256dh}
+ subscription: %{
+ endpoint: endpoint,
+ keys: %{auth: key_auth, p256dh: key_p256dh}
}
} = params
) do
diff --git a/lib/pleroma/web/rel_me.ex b/lib/pleroma/web/rel_me.ex
index e97c398dc..8e2b51508 100644
--- a/lib/pleroma/web/rel_me.ex
+++ b/lib/pleroma/web/rel_me.ex
@@ -3,11 +3,9 @@
# 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 1729141e9..6210f2c5a 100644
--- a/lib/pleroma/web/rich_media/helpers.ex
+++ b/lib/pleroma/web/rich_media/helpers.ex
@@ -9,12 +9,17 @@ defmodule Pleroma.Web.RichMedia.Helpers do
alias Pleroma.Object
alias Pleroma.Web.RichMedia.Parser
+ @rich_media_options [
+ pool: :media,
+ max_body: 2_000_000
+ ]
+
@spec validate_page_url(URI.t() | binary()) :: :ok | :error
defp validate_page_url(page_url) when is_binary(page_url) do
- validate_tld = Application.get_env(:auto_linker, :opts)[:validate_tld]
+ validate_tld = Pleroma.Config.get([Pleroma.Formatter, :validate_tld])
page_url
- |> AutoLinker.Parser.url?(scheme: true, validate_tld: validate_tld)
+ |> Linkify.Parser.url?(validate_tld: validate_tld)
|> parse_uri(page_url)
end
@@ -49,11 +54,11 @@ defmodule Pleroma.Web.RichMedia.Helpers do
|> hd
end
- def fetch_data_for_activity(%Activity{data: %{"type" => "Create"}} = activity) do
+ def fetch_data_for_object(object) do
with true <- Config.get([:rich_media, :enabled]),
- %Object{} = object <- Object.normalize(activity),
false <- object.data["sensitive"] || false,
- {:ok, page_url} <- HTML.extract_first_external_url(object, object.data["content"]),
+ {:ok, page_url} <-
+ HTML.extract_first_external_url(object, object.data["content"]),
:ok <- validate_page_url(page_url),
{:ok, rich_media} <- Parser.parse(page_url) do
%{page_url: page_url, rich_media: rich_media}
@@ -62,10 +67,35 @@ defmodule Pleroma.Web.RichMedia.Helpers do
end
end
+ def fetch_data_for_activity(%Activity{data: %{"type" => "Create"}} = activity) do
+ with true <- Config.get([:rich_media, :enabled]),
+ %Object{} = object <- Object.normalize(activity) do
+ fetch_data_for_object(object)
+ else
+ _ -> %{}
+ end
+ end
+
def fetch_data_for_activity(_), do: %{}
def perform(:fetch, %Activity{} = activity) do
fetch_data_for_activity(activity)
:ok
end
+
+ def rich_media_get(url) do
+ headers = [{"user-agent", Pleroma.Application.user_agent() <> "; Bot"}]
+
+ options =
+ if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do
+ Keyword.merge(@rich_media_options,
+ recv_timeout: 2_000,
+ with_body: true
+ )
+ else
+ @rich_media_options
+ end
+
+ Pleroma.HTTP.get(url, headers, options)
+ end
end
diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex
index 7b45ecb9c..ca592833f 100644
--- a/lib/pleroma/web/rich_media/parser.ex
+++ b/lib/pleroma/web/rich_media/parser.ex
@@ -3,13 +3,6 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Parser do
- @hackney_options [
- pool: :media,
- recv_timeout: 2_000,
- max_body: 2_000_000,
- with_body: true
- ]
-
defp parsers do
Pleroma.Config.get([:rich_media, :parsers])
end
@@ -78,7 +71,7 @@ defmodule Pleroma.Web.RichMedia.Parser do
defp parse_url(url) do
try do
- {:ok, %Tesla.Env{body: html}} = Pleroma.HTTP.get(url, [], adapter: @hackney_options)
+ {:ok, %Tesla.Env{body: html}} = Pleroma.Web.RichMedia.Helpers.rich_media_get(url)
html
|> parse_html()
@@ -97,8 +90,8 @@ defmodule Pleroma.Web.RichMedia.Parser do
defp maybe_parse(html) do
Enum.reduce_while(parsers(), %{}, fn parser, acc ->
case parser.parse(html, acc) do
- {:ok, data} -> {:halt, data}
- {:error, _msg} -> {:cont, acc}
+ data when data != %{} -> {:halt, data}
+ _ -> {:cont, acc}
end
end)
end
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 2762b5902..3d577e254 100644
--- a/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex
+++ b/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex
@@ -3,22 +3,15 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Parsers.MetaTagsParser do
- def parse(html, data, prefix, error_message, key_name, value_name \\ "content") do
- meta_data =
- html
- |> get_elements(key_name, prefix)
- |> Enum.reduce(data, fn el, acc ->
- attributes = normalize_attributes(el, prefix, key_name, value_name)
-
- Map.merge(acc, attributes)
- end)
- |> maybe_put_title(html)
-
- if Enum.empty?(meta_data) do
- {:error, error_message}
- else
- {:ok, meta_data}
- end
+ def parse(data, html, prefix, key_name, value_name \\ "content") do
+ html
+ |> get_elements(key_name, prefix)
+ |> Enum.reduce(data, fn el, acc ->
+ attributes = normalize_attributes(el, prefix, key_name, value_name)
+
+ Map.merge(acc, attributes)
+ end)
+ |> maybe_put_title(html)
end
defp get_elements(html, key_name, prefix) do
diff --git a/lib/pleroma/web/rich_media/parsers/oembed_parser.ex b/lib/pleroma/web/rich_media/parsers/oembed_parser.ex
index db8ccf15d..1fe6729c3 100644
--- a/lib/pleroma/web/rich_media/parsers/oembed_parser.ex
+++ b/lib/pleroma/web/rich_media/parsers/oembed_parser.ex
@@ -7,9 +7,9 @@ defmodule Pleroma.Web.RichMedia.Parsers.OEmbed do
with elements = [_ | _] <- get_discovery_data(html),
oembed_url when is_binary(oembed_url) <- get_oembed_url(elements),
{:ok, oembed_data} <- get_oembed_data(oembed_url) do
- {:ok, oembed_data}
+ oembed_data
else
- _e -> {:error, "No OEmbed data found"}
+ _e -> %{}
end
end
@@ -22,7 +22,7 @@ defmodule Pleroma.Web.RichMedia.Parsers.OEmbed do
end
defp get_oembed_data(url) do
- with {:ok, %Tesla.Env{body: json}} <- Pleroma.HTTP.get(url, [], adapter: [pool: :media]) do
+ with {:ok, %Tesla.Env{body: json}} <- Pleroma.Web.RichMedia.Helpers.rich_media_get(url) do
Jason.decode(json)
end
end
diff --git a/lib/pleroma/web/rich_media/parsers/ogp.ex b/lib/pleroma/web/rich_media/parsers/ogp.ex
index 3e9012588..b3b3b059c 100644
--- a/lib/pleroma/web/rich_media/parsers/ogp.ex
+++ b/lib/pleroma/web/rich_media/parsers/ogp.ex
@@ -3,13 +3,8 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Parsers.OGP do
- def parse(html, data) do
- Pleroma.Web.RichMedia.Parsers.MetaTagsParser.parse(
- html,
- data,
- "og",
- "No OGP metadata found",
- "property"
- )
+ @deprecated "OGP parser is deprecated. Use TwitterCard instead."
+ def parse(_html, _data) do
+ %{}
end
end
diff --git a/lib/pleroma/web/rich_media/parsers/twitter_card.ex b/lib/pleroma/web/rich_media/parsers/twitter_card.ex
index 09d4b526e..4a04865d2 100644
--- a/lib/pleroma/web/rich_media/parsers/twitter_card.ex
+++ b/lib/pleroma/web/rich_media/parsers/twitter_card.ex
@@ -5,18 +5,11 @@
defmodule Pleroma.Web.RichMedia.Parsers.TwitterCard do
alias Pleroma.Web.RichMedia.Parsers.MetaTagsParser
- @spec parse(String.t(), map()) :: {:ok, map()} | {:error, String.t()}
+ @spec parse(list(), map()) :: map()
def parse(html, data) do
data
- |> parse_name_attrs(html)
- |> parse_property_attrs(html)
- end
-
- defp parse_name_attrs(data, html) do
- MetaTagsParser.parse(html, data, "twitter", %{}, "name")
- end
-
- defp parse_property_attrs({_, data}, html) do
- MetaTagsParser.parse(html, data, "twitter", "No twitter card metadata found", "property")
+ |> MetaTagsParser.parse(html, "og", "property")
+ |> MetaTagsParser.parse(html, "twitter", "name")
+ |> MetaTagsParser.parse(html, "twitter", "property")
end
end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index cb4cc619a..c6433cc53 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -16,76 +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.AuthExpectedPlug)
- 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
@@ -95,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
@@ -136,11 +132,13 @@ 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)
patch("/users/activate", AdminAPIController, :user_activate)
patch("/users/deactivate", AdminAPIController, :user_deactivate)
+ patch("/users/approve", AdminAPIController, :user_approve)
put("/users/tag", AdminAPIController, :tag_users)
delete("/users/tag", AdminAPIController, :untag_users)
@@ -163,14 +161,14 @@ defmodule Pleroma.Web.Router do
:right_delete_multiple
)
- get("/relay", AdminAPIController, :relay_list)
- post("/relay", AdminAPIController, :relay_follow)
- delete("/relay", AdminAPIController, :relay_unfollow)
+ get("/relay", RelayController, :index)
+ post("/relay", RelayController, :follow)
+ delete("/relay", RelayController, :unfollow)
- post("/users/invite_token", AdminAPIController, :create_invite_token)
- get("/users/invites", AdminAPIController, :invites)
- post("/users/revoke_invite", AdminAPIController, :revoke_invite)
- post("/users/email_invite", AdminAPIController, :email_invite)
+ post("/users/invite_token", InviteController, :create)
+ get("/users/invites", InviteController, :index)
+ post("/users/revoke_invite", InviteController, :revoke)
+ post("/users/email_invite", InviteController, :email)
get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset)
patch("/users/force_password_reset", AdminAPIController, :force_password_reset)
@@ -186,20 +184,20 @@ defmodule Pleroma.Web.Router do
patch("/users/confirm_email", AdminAPIController, :confirm_email)
patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email)
- get("/reports", AdminAPIController, :list_reports)
- get("/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("/reports", ReportController, :index)
+ get("/reports/:id", ReportController, :show)
+ patch("/reports", ReportController, :update)
+ post("/reports/:id/notes", ReportController, :notes_create)
+ delete("/reports/:report_id/notes/:id", ReportController, :notes_delete)
- put("/statuses/:id", AdminAPIController, :status_update)
- delete("/statuses/:id", AdminAPIController, :status_delete)
- get("/statuses", AdminAPIController, :list_statuses)
+ get("/statuses/:id", StatusController, :show)
+ put("/statuses/:id", StatusController, :update)
+ delete("/statuses/:id", StatusController, :delete)
+ get("/statuses", StatusController, :index)
- get("/config", AdminAPIController, :config_show)
- post("/config", AdminAPIController, :config_update)
- get("/config/descriptions", AdminAPIController, :config_descriptions)
+ get("/config", ConfigController, :show)
+ post("/config", ConfigController, :update)
+ get("/config/descriptions", ConfigController, :descriptions)
get("/need_reboot", AdminAPIController, :need_reboot)
get("/restart", AdminAPIController, :restart)
@@ -207,27 +205,41 @@ defmodule Pleroma.Web.Router do
post("/reload_emoji", AdminAPIController, :reload_emoji)
get("/stats", AdminAPIController, :stats)
+
+ get("/oauth_app", OAuthAppController, :index)
+ post("/oauth_app", OAuthAppController, :create)
+ patch("/oauth_app/:id", OAuthAppController, :update)
+ delete("/oauth_app/:id", OAuthAppController, :delete)
+
+ get("/media_proxy_caches", MediaProxyCacheController, :index)
+ post("/media_proxy_caches/delete", MediaProxyCacheController, :delete)
+ post("/media_proxy_caches/purge", MediaProxyCacheController, :purge)
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", EmojiPackController, :import_from_filesystem)
+ get("/remote", EmojiPackController, :remote)
+ post("/download", EmojiPackController, :download)
+
+ post("/:name", EmojiPackController, :create)
+ patch("/:name", EmojiPackController, :update)
+ delete("/:name", EmojiPackController, :delete)
- post("/:pack_name/update_file", EmojiAPIController, :update_file)
- post("/:pack_name/update_metadata", EmojiAPIController, :update_metadata)
- put("/:name", EmojiAPIController, :create)
- delete("/:name", EmojiAPIController, :delete)
- post("/download_from", EmojiAPIController, :download_from)
- post("/list_from", EmojiAPIController, :list_from)
+ post("/:name/files", EmojiPackController, :add_file)
+ patch("/:name/files", EmojiPackController, :update_file)
+ delete("/:name/files", EmojiPackController, :delete_file)
end
+ # Pack info / downloading
scope "/packs" do
- # Pack info / downloading
- get("/", EmojiAPIController, :list_packs)
- get("/:name/download_shared/", EmojiAPIController, :download_shared)
+ pipe_through(:api)
+ get("/", EmojiPackController, :index)
+ get("/:name", EmojiPackController, :show)
+ get("/:name/archive", EmojiPackController, :archive)
end
end
@@ -253,6 +265,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)
@@ -264,6 +286,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)
@@ -277,35 +303,36 @@ defmodule Pleroma.Web.Router do
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:api)
- get("/statuses/:id/reactions/:emoji", PleromaAPIController, :emoji_reactions_by)
- get("/statuses/:id/reactions", PleromaAPIController, :emoji_reactions_by)
+ get("/statuses/:id/reactions/:emoji", EmojiReactionController, :index)
+ get("/statuses/:id/reactions", EmojiReactionController, :index)
end
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
scope [] do
pipe_through(:authenticated_api)
- get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses)
- get("/conversations/:id", PleromaAPIController, :conversation)
- post("/conversations/read", PleromaAPIController, :read_conversations)
- end
+ post("/chats/by-account-id/:id", ChatController, :create)
+ get("/chats", ChatController, :index)
+ get("/chats/:id", ChatController, :show)
+ get("/chats/:id/messages", ChatController, :messages)
+ post("/chats/:id/messages", ChatController, :post_chat_message)
+ delete("/chats/:id/messages/:message_id", ChatController, :delete_message)
+ post("/chats/:id/read", ChatController, :mark_as_read)
+ post("/chats/:id/messages/:message_id/read", ChatController, :mark_message_as_read)
- scope [] do
- pipe_through(:authenticated_api)
-
- patch("/conversations/:id", PleromaAPIController, :update_conversation)
- put("/statuses/:id/reactions/:emoji", PleromaAPIController, :react_with_emoji)
- delete("/statuses/:id/reactions/:emoji", PleromaAPIController, :unreact_with_emoji)
- post("/notifications/read", PleromaAPIController, :read_notification)
+ get("/conversations/:id/statuses", ConversationController, :statuses)
+ get("/conversations/:id", ConversationController, :show)
+ post("/conversations/read", ConversationController, :mark_as_read)
+ patch("/conversations/:id", ConversationController, :update)
- patch("/accounts/update_avatar", AccountController, :update_avatar)
- patch("/accounts/update_banner", AccountController, :update_banner)
- patch("/accounts/update_background", AccountController, :update_background)
+ put("/statuses/:id/reactions/:emoji", EmojiReactionController, :create)
+ delete("/statuses/:id/reactions/:emoji", EmojiReactionController, :delete)
+ post("/notifications/read", NotificationController, :mark_as_read)
get("/mascot", MascotController, :show)
put("/mascot", MascotController, :update)
- post("/scrobble", ScrobbleController, :new_scrobble)
+ post("/scrobble", ScrobbleController, :create)
end
scope [] do
@@ -325,58 +352,92 @@ defmodule Pleroma.Web.Router do
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:api)
- get("/accounts/:id/scrobbles", ScrobbleController, :user_scrobbles)
+ get("/accounts/:id/scrobbles", ScrobbleController, :index)
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
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", AccountController, :identity_proofs)
-
- get("/follow_requests", FollowRequestController, :index)
+ 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)
+ get("/media/:id", MediaController, :show)
+ 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)
@@ -388,69 +449,46 @@ 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
pipe_through(:authenticated_api)
+ # Backend-obscure settings blob for MastoFE, don't parse/reuse elsewhere
put("/settings", MastoFEController, :put_settings)
end
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)
@@ -461,34 +499,20 @@ 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
pipe_through(:api)
get("/search", SearchController, :search2)
+
+ post("/media", MediaController, :create2)
end
scope "/api", Pleroma.Web do
pipe_through(:config)
- get("/help/test", TwitterAPI.UtilController, :help_test)
- post("/help/test", TwitterAPI.UtilController, :help_test)
- get("/statusnet/config", TwitterAPI.UtilController, :config)
- get("/statusnet/version", TwitterAPI.UtilController, :version)
get("/pleroma/frontend_configurations", TwitterAPI.UtilController, :frontend_configurations)
end
@@ -503,21 +527,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
@@ -526,14 +556,17 @@ 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)
get("/notice/:id", OStatus.OStatusController, :notice)
get("/notice/:id/embed_player", OStatus.OStatusController, :notice_player)
+ # Mastodon compatibility routes
+ get("/users/:nickname/statuses/:id", OStatus.OStatusController, :object)
+ get("/users/:nickname/statuses/:id/activity", OStatus.OStatusController, :activity)
+
get("/users/:nickname/feed", Feed.UserController, :feed, as: :user_feed)
get("/users/:nickname", Feed.UserController, :feed_redirect, as: :user_feed)
@@ -545,6 +578,10 @@ defmodule Pleroma.Web.Router do
get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe)
end
+ pipeline :ap_service_actor do
+ plug(:accepts, ["activity+json", "json"])
+ end
+
# Server to Server (S2S) AP interactions
pipeline :activitypub do
plug(:ap_service_actor)
@@ -553,17 +590,10 @@ defmodule Pleroma.Web.Router do
# Client to Server (C2S) AP interactions
pipeline :activitypub_client do
- plug(:accepts, ["activity+json", "json"])
+ 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
@@ -576,6 +606,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
@@ -596,8 +627,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,14 +665,11 @@ defmodule Pleroma.Web.Router do
post("/auth/password", MastodonAPI.AuthController, :password_reset)
get("/web/*path", MastoFEController, :index)
- end
- pipeline :remote_media do
+ get("/embed/:id", EmbedController, :show)
end
scope "/proxy/", Pleroma.Web.MediaProxy do
- pipe_through(:remote_media)
-
get("/:sig/:url", MediaProxyController, :remote)
get("/:sig/:url/:filename", MediaProxyController, :remote)
end
@@ -656,11 +684,28 @@ defmodule Pleroma.Web.Router do
# 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 <- [:skipped_oauth, :performed_oauth, :missed_oauth] do
- get("/#{action}", OAuthTestController, action)
+ for action <- @test_actions do
+ get("/#{action}", AuthTestController, action)
end
end
end
@@ -674,7 +719,7 @@ defmodule Pleroma.Web.Router do
get("/registration/:token", RedirectController, :registration_page)
get("/:maybe_nickname_or_id", RedirectController, :redirector_with_meta)
get("/api*path", RedirectController, :api_not_implemented)
- get("/*path", RedirectController, :redirector)
+ get("/*path", RedirectController, :redirector_with_preload)
options("/*path", RedirectController, :empty)
end
diff --git a/lib/pleroma/web/static_fe/static_fe_controller.ex b/lib/pleroma/web/static_fe/static_fe_controller.ex
index 98977bc19..a7a891b13 100644
--- a/lib/pleroma/web/static_fe/static_fe_controller.ex
+++ b/lib/pleroma/web/static_fe/static_fe_controller.ex
@@ -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 =
@@ -56,7 +60,9 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do
content =
if data["content"] do
- Pleroma.HTML.filter_tags(data["content"])
+ data["content"]
+ |> Pleroma.HTML.filter_tags()
+ |> Pleroma.Emoji.Formatter.emojify(Map.get(data, "emoji", %{}))
else
nil
end
@@ -105,8 +111,14 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do
%User{} = user ->
meta = Metadata.build_tags(%{user: user})
+ params =
+ params
+ |> Map.take(@page_keys)
+ |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
+
timeline =
- ActivityPub.fetch_user_activities(user, nil, Map.take(params, @page_keys))
+ user
+ |> ActivityPub.fetch_user_activities(nil, params)
|> Enum.map(&represent/1)
prev_page_id =
@@ -154,17 +166,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 66d87620c..b3d1d1ec8 100644
--- a/lib/pleroma/web/static_fe/static_fe_view.ex
+++ b/lib/pleroma/web/static_fe/static_fe_view.ex
@@ -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 7a08202a9..000000000
--- a/lib/pleroma/web/streamer/ping.ex
+++ /dev/null
@@ -1,37 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.Streamer.Ping do
- use GenServer
- require Logger
-
- alias Pleroma.Web.Streamer.State
- alias Pleroma.Web.Streamer.StreamerSocket
-
- @keepalive_interval :timer.seconds(30)
-
- def start_link(opts) do
- ping_interval = Keyword.get(opts, :ping_interval, @keepalive_interval)
- GenServer.start_link(__MODULE__, %{ping_interval: ping_interval}, name: __MODULE__)
- end
-
- def init(%{ping_interval: ping_interval} = args) do
- Process.send_after(self(), :ping, ping_interval)
- {:ok, args}
- end
-
- def handle_info(:ping, %{ping_interval: ping_interval} = state) do
- State.get_sockets()
- |> Map.values()
- |> List.flatten()
- |> Enum.each(fn %StreamerSocket{transport_pid: transport_pid} ->
- Logger.debug("Sending keepalive ping")
- send(transport_pid, {:text, ""})
- end)
-
- Process.send_after(self(), :ping, ping_interval)
-
- {:noreply, state}
- end
-end
diff --git a/lib/pleroma/web/streamer/state.ex b/lib/pleroma/web/streamer/state.ex
deleted file mode 100644
index 4eb462a1a..000000000
--- a/lib/pleroma/web/streamer/state.ex
+++ /dev/null
@@ -1,71 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.Streamer.State do
- use GenServer
- require Logger
-
- alias Pleroma.Web.Streamer.StreamerSocket
-
- @env Mix.env()
-
- def start_link(_) do
- GenServer.start_link(__MODULE__, %{sockets: %{}}, name: __MODULE__)
- end
-
- def add_socket(topic, socket) do
- GenServer.call(__MODULE__, {:add, topic, socket})
- end
-
- def remove_socket(topic, socket) do
- do_remove_socket(@env, topic, socket)
- end
-
- def get_sockets do
- %{sockets: stream_sockets} = GenServer.call(__MODULE__, :get_state)
- stream_sockets
- end
-
- def init(init_arg) do
- {:ok, init_arg}
- end
-
- def handle_call(:get_state, _from, state) do
- {:reply, state, state}
- end
-
- def handle_call({:add, topic, socket}, _from, %{sockets: sockets} = state) do
- stream_socket = StreamerSocket.from_socket(socket)
-
- sockets_for_topic =
- sockets
- |> Map.get(topic, [])
- |> List.insert_at(0, stream_socket)
- |> Enum.uniq()
-
- state = put_in(state, [:sockets, 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
- stream_socket = StreamerSocket.from_socket(socket)
-
- sockets_for_topic =
- sockets
- |> Map.get(topic, [])
- |> List.delete(stream_socket)
-
- state = Kernel.put_in(state, [:sockets, 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
-end
diff --git a/lib/pleroma/web/streamer/streamer.ex b/lib/pleroma/web/streamer/streamer.ex
index b7294d084..d1d70e556 100644
--- a/lib/pleroma/web/streamer/streamer.ex
+++ b/lib/pleroma/web/streamer/streamer.ex
@@ -3,33 +3,35 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Streamer do
+ require Logger
+
+ alias Pleroma.Activity
+ alias Pleroma.Chat.MessageReference
+ alias Pleroma.Config
+ alias Pleroma.Conversation.Participation
+ alias Pleroma.Notification
+ alias Pleroma.Object
alias Pleroma.User
- alias Pleroma.Web.Streamer.State
- alias Pleroma.Web.Streamer.Worker
+ 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"]
+ @user_streams ["user", "user:notification", "direct", "user:pleroma_chat"]
@doc "Expands and authorizes a stream, and registers the process for streaming."
- @spec get_topic_and_add_socket(stream :: String.t(), State.t(), Map.t() | nil) ::
+ @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, socket, params \\ %{}) do
- user =
- case socket do
- %{assigns: %{user: user}} -> user
- _ -> nil
- end
-
+ def get_topic_and_add_socket(stream, user, params \\ %{}) do
case get_topic(stream, user, params) do
- {:ok, topic} ->
- add_socket(topic, socket)
- {:ok, topic}
-
- error ->
- error
+ {:ok, topic} -> add_socket(topic, user)
+ error -> error
end
end
@@ -74,47 +76,220 @@ defmodule Pleroma.Web.Streamer do
{:error, :bad_topic}
end
- def add_socket(topic, socket) do
- State.add_socket(topic, socket)
- 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
- def remove_socket(topic, socket) do
- State.remove_socket(topic, socket)
+ {:ok, topic}
end
- def get_sockets do
- State.get_sockets()
+ def remove_socket(topic) do
+ if should_env_send?(), do: Registry.unregister(@registry, topic)
end
def stream(topics, items) do
- if should_send?() do
- Task.async(fn ->
- :poolboy.transaction(
- :streamer_worker,
- &Worker.stream(&1, topics, items),
- @timeout
- )
+ if should_env_send?() do
+ List.wrap(topics)
+ |> Enum.each(fn topic ->
+ List.wrap(items)
+ |> Enum.each(fn item ->
+ spawn(fn -> do_stream(topic, item) end)
+ end)
end)
end
+
+ :ok
+ end
+
+ def filtered_by_user?(user, item, streamed_type \\ :activity)
+
+ def filtered_by_user?(%User{} = user, %Activity{} = item, streamed_type) do
+ %{block: blocked_ap_ids, mute: muted_ap_ids, reblog_mute: reblog_muted_ap_ids} =
+ User.outgoing_relationships_ap_ids(user, [:block, :mute, :reblog_mute])
+
+ recipient_blocks = MapSet.new(blocked_ap_ids ++ muted_ap_ids)
+ recipients = MapSet.new(item.recipients)
+ domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
+
+ with parent <- Object.normalize(item) || item,
+ true <-
+ Enum.all?([blocked_ap_ids, muted_ap_ids], &(item.actor not in &1)),
+ true <- item.data["type"] != "Announce" || item.actor not in reblog_muted_ap_ids,
+ true <-
+ !(streamed_type == :activity && item.data["type"] == "Announce" &&
+ parent.data["actor"] == user.ap_id),
+ 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, parent) do
+ false
+ else
+ _ -> true
+ end
end
- def supervisor, do: Pleroma.Web.Streamer.Supervisor
+ def filtered_by_user?(%User{} = user, %Notification{activity: activity}, _) do
+ filtered_by_user?(user, activity, :notification)
+ 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 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)
- defp should_send? do
- handle_should_send(@mix_env)
+ _ ->
+ 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 handle_should_send(:test) do
- case Process.whereis(:streamer_worker) do
- nil ->
- false
+ defp do_stream(topic, {user, %MessageReference{} = cm_ref})
+ when topic in ["user", "user:pleroma_chat"] do
+ topic = "#{topic}:#{user.id}"
- pid ->
- Process.alive?(pid)
+ text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref})
+
+ Registry.dispatch(@registry, topic, fn list ->
+ Enum.each(list, fn {pid, _auth} ->
+ send(pid, {:text, text})
+ 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
- defp handle_should_send(:benchmark), do: false
+ # 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
- defp handle_should_send(_), do: true
+ pid ->
+ Process.alive?(pid)
+ end
+ end
+
+ @mix_env == :benchmark ->
+ def should_env_send?, do: false
+
+ true ->
+ def should_env_send?, do: true
+ end
end
diff --git a/lib/pleroma/web/streamer/streamer_socket.ex b/lib/pleroma/web/streamer/streamer_socket.ex
deleted file mode 100644
index 7d5dcd34e..000000000
--- a/lib/pleroma/web/streamer/streamer_socket.ex
+++ /dev/null
@@ -1,35 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.Streamer.StreamerSocket do
- defstruct transport_pid: nil, user: nil
-
- alias Pleroma.User
- alias Pleroma.Web.Streamer.StreamerSocket
-
- def from_socket(%{
- transport_pid: transport_pid,
- assigns: %{user: nil}
- }) do
- %StreamerSocket{
- transport_pid: transport_pid
- }
- end
-
- def from_socket(%{
- transport_pid: transport_pid,
- assigns: %{user: %User{} = user}
- }) do
- %StreamerSocket{
- transport_pid: transport_pid,
- user: user
- }
- end
-
- def from_socket(%{transport_pid: transport_pid}) do
- %StreamerSocket{
- transport_pid: transport_pid
- }
- end
-end
diff --git a/lib/pleroma/web/streamer/supervisor.ex b/lib/pleroma/web/streamer/supervisor.ex
deleted file mode 100644
index bd9029bc0..000000000
--- a/lib/pleroma/web/streamer/supervisor.ex
+++ /dev/null
@@ -1,37 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.Streamer.Supervisor do
- use Supervisor
-
- def start_link(opts) do
- Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
- end
-
- def init(args) do
- children = [
- {Pleroma.Web.Streamer.State, args},
- {Pleroma.Web.Streamer.Ping, args},
- :poolboy.child_spec(:streamer_worker, poolboy_config())
- ]
-
- opts = [strategy: :one_for_one, name: Pleroma.Web.Streamer.Supervisor]
- Supervisor.init(children, opts)
- end
-
- defp poolboy_config do
- opts =
- Pleroma.Config.get(:streamer,
- workers: 3,
- overflow_workers: 2
- )
-
- [
- {:name, {:local, :streamer_worker}},
- {:worker_module, Pleroma.Web.Streamer.Worker},
- {:size, opts[:workers]},
- {:max_overflow, opts[:overflow_workers]}
- ]
- end
-end
diff --git a/lib/pleroma/web/streamer/worker.ex b/lib/pleroma/web/streamer/worker.ex
deleted file mode 100644
index c669e917d..000000000
--- a/lib/pleroma/web/streamer/worker.ex
+++ /dev/null
@@ -1,226 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.Streamer.Worker do
- use GenServer
-
- require Logger
-
- alias Pleroma.Activity
- alias Pleroma.Config
- alias Pleroma.Conversation.Participation
- alias Pleroma.Notification
- alias Pleroma.Object
- alias Pleroma.User
- alias Pleroma.Web.ActivityPub.ActivityPub
- alias Pleroma.Web.ActivityPub.Visibility
- alias Pleroma.Web.CommonAPI
- alias Pleroma.Web.Streamer.State
- alias Pleroma.Web.Streamer.StreamerSocket
- alias Pleroma.Web.StreamerView
-
- def start_link(_) do
- GenServer.start_link(__MODULE__, %{}, [])
- end
-
- def init(init_arg) do
- {:ok, init_arg}
- end
-
- def stream(pid, topics, items) do
- GenServer.call(pid, {:stream, topics, items})
- end
-
- def handle_call({:stream, topics, item}, _from, state) when is_list(topics) do
- Enum.each(topics, fn t ->
- do_stream(%{topic: t, item: item})
- end)
-
- {:reply, state, state}
- end
-
- def handle_call({:stream, topic, items}, _from, state) when is_list(items) do
- Enum.each(items, fn i ->
- do_stream(%{topic: topic, item: i})
- end)
-
- {:reply, state, state}
- end
-
- def handle_call({:stream, topic, item}, _from, state) do
- do_stream(%{topic: topic, item: item})
-
- {:reply, state, state}
- end
-
- defp do_stream(%{topic: "direct", item: item}) do
- recipient_topics =
- User.get_recipients_from_activity(item)
- |> Enum.map(fn %{id: id} -> "direct:#{id}" end)
-
- Enum.each(recipient_topics, fn user_topic ->
- Logger.debug("Trying to push direct message to #{user_topic}\n\n")
- push_to_socket(State.get_sockets(), user_topic, item)
- end)
- end
-
- defp do_stream(%{topic: "participation", item: participation}) do
- user_topic = "direct:#{participation.user_id}"
- Logger.debug("Trying to push a conversation participation to #{user_topic}\n\n")
-
- push_to_socket(State.get_sockets(), user_topic, participation)
- end
-
- defp do_stream(%{topic: "list", item: item}) do
- # filter the recipient list if the activity is not public, see #270.
- recipient_lists =
- case Visibility.is_public?(item) do
- true ->
- Pleroma.List.get_lists_from_activity(item)
-
- _ ->
- Pleroma.List.get_lists_from_activity(item)
- |> Enum.filter(fn list ->
- owner = User.get_cached_by_id(list.user_id)
-
- Visibility.visible_for_user?(item, owner)
- end)
- end
-
- recipient_topics =
- recipient_lists
- |> Enum.map(fn %{id: id} -> "list:#{id}" end)
-
- Enum.each(recipient_topics, fn list_topic ->
- Logger.debug("Trying to push message to #{list_topic}\n\n")
- push_to_socket(State.get_sockets(), list_topic, item)
- end)
- end
-
- defp do_stream(%{topic: topic, item: %Notification{} = item})
- when topic in ["user", "user:notification"] do
- State.get_sockets()
- |> Map.get("#{topic}:#{item.user_id}", [])
- |> Enum.each(fn %StreamerSocket{transport_pid: transport_pid, user: socket_user} ->
- with %User{} = user <- User.get_cached_by_ap_id(socket_user.ap_id),
- true <- should_send?(user, item) do
- send(transport_pid, {:text, StreamerView.render("notification.json", socket_user, item)})
- end
- end)
- end
-
- defp do_stream(%{topic: "user", item: item}) do
- Logger.debug("Trying to push to users")
-
- recipient_topics =
- User.get_recipients_from_activity(item)
- |> Enum.map(fn %{id: id} -> "user:#{id}" end)
-
- Enum.each(recipient_topics, fn topic ->
- push_to_socket(State.get_sockets(), topic, item)
- end)
- end
-
- defp do_stream(%{topic: topic, item: item}) do
- Logger.debug("Trying to push to #{topic}")
- Logger.debug("Pushing item to #{topic}")
- push_to_socket(State.get_sockets(), topic, item)
- end
-
- defp should_send?(%User{} = user, %Activity{} = item) do
- %{block: blocked_ap_ids, mute: muted_ap_ids, reblog_mute: reblog_muted_ap_ids} =
- User.outgoing_relationships_ap_ids(user, [:block, :mute, :reblog_mute])
-
- recipient_blocks = MapSet.new(blocked_ap_ids ++ muted_ap_ids)
- recipients = MapSet.new(item.recipients)
- domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
-
- with parent <- Object.normalize(item) || item,
- true <-
- Enum.all?([blocked_ap_ids, muted_ap_ids], &(item.actor not in &1)),
- true <- item.data["type"] != "Announce" || item.actor not in reblog_muted_ap_ids,
- true <- Enum.all?([blocked_ap_ids, muted_ap_ids], &(parent.data["actor"] not in &1)),
- true <- MapSet.disjoint?(recipients, recipient_blocks),
- %{host: item_host} <- URI.parse(item.actor),
- %{host: parent_host} <- URI.parse(parent.data["actor"]),
- false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, item_host),
- false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, parent_host),
- true <- thread_containment(item, user),
- false <- CommonAPI.thread_muted?(user, parent) 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/embed/_attachment.html.eex b/lib/pleroma/web/templates/embed/_attachment.html.eex
new file mode 100644
index 000000000..7e04e9550
--- /dev/null
+++ b/lib/pleroma/web/templates/embed/_attachment.html.eex
@@ -0,0 +1,8 @@
+<%= case @mediaType do %>
+<% "audio" -> %>
+<audio src="<%= @url %>" controls="controls"></audio>
+<% "video" -> %>
+<video src="<%= @url %>" controls="controls"></video>
+<% _ -> %>
+<img src="<%= @url %>" alt="<%= @name %>" title="<%= @name %>">
+<% end %>
diff --git a/lib/pleroma/web/templates/embed/show.html.eex b/lib/pleroma/web/templates/embed/show.html.eex
new file mode 100644
index 000000000..05a3f0ee3
--- /dev/null
+++ b/lib/pleroma/web/templates/embed/show.html.eex
@@ -0,0 +1,76 @@
+<div>
+ <div class="p-author h-card">
+ <a class="u-url" rel="author noopener" href="<%= @author.ap_id %>">
+ <div class="avatar">
+ <img src="<%= User.avatar_url(@author) |> MediaProxy.url %>" width="48" height="48" alt="">
+ </div>
+ <span class="display-name" style="padding-left: 0.5em;">
+ <bdi><%= raw (@author.name |> Formatter.emojify(@author.emoji)) %></bdi>
+ <span class="nickname"><%= full_nickname(@author) %></span>
+ </span>
+ </a>
+ </div>
+
+ <div class="activity-content" >
+ <%= if status_title(@activity) != "" do %>
+ <details <%= if open_content?() do %>open<% end %>>
+ <summary><%= raw status_title(@activity) %></summary>
+ <div><%= activity_content(@activity) %></div>
+ </details>
+ <% else %>
+ <div><%= activity_content(@activity) %></div>
+ <% end %>
+ <%= for %{"name" => name, "url" => [url | _]} <- attachments(@activity) do %>
+ <div class="attachment">
+ <%= if sensitive?(@activity) do %>
+ <details class="nsfw">
+ <summary onClick="updateHeight()"><%= Gettext.gettext("sensitive media") %></summary>
+ <div class="nsfw-content">
+ <%= render("_attachment.html", %{name: name, url: url["href"],
+ mediaType: fetch_media_type(url)}) %>
+ </div>
+ </details>
+ <% else %>
+ <%= render("_attachment.html", %{name: name, url: url["href"],
+ mediaType: fetch_media_type(url)}) %>
+ <% end %>
+ </div>
+ <% end %>
+ </div>
+
+ <dl class="counts pull-right">
+ <dt><%= Gettext.gettext("replies") %></dt><dd><%= @counts.replies %></dd>
+ <dt><%= Gettext.gettext("announces") %></dt><dd><%= @counts.announces %></dd>
+ <dt><%= Gettext.gettext("likes") %></dt><dd><%= @counts.likes %></dd>
+ </dl>
+
+ <p class="date pull-left">
+ <%= link published(@activity), to: activity_url(@author, @activity) %>
+ </p>
+</div>
+
+<script>
+function updateHeight() {
+ window.requestAnimationFrame(function(){
+ var height = document.getElementsByTagName('html')[0].scrollHeight;
+
+ window.parent.postMessage({
+ type: 'setHeightPleromaEmbed',
+ id: window.parentId,
+ height: height,
+ }, '*');
+ })
+}
+
+window.addEventListener('message', function(e){
+ var data = e.data || {};
+
+ if (!window.parent || data.type !== 'setHeightPleromaEmbed') {
+ return;
+ }
+
+ window.parentId = data.id
+
+ updateHeight()
+});
+</script>
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/app.html.eex b/lib/pleroma/web/templates/layout/app.html.eex
index 5836ec1e0..51603fe0c 100644
--- a/lib/pleroma/web/templates/layout/app.html.eex
+++ b/lib/pleroma/web/templates/layout/app.html.eex
@@ -37,7 +37,7 @@
}
a {
- color: color: #d8a070;
+ color: #d8a070;
text-decoration: none;
}
diff --git a/lib/pleroma/web/templates/layout/embed.html.eex b/lib/pleroma/web/templates/layout/embed.html.eex
new file mode 100644
index 000000000..8b905f070
--- /dev/null
+++ b/lib/pleroma/web/templates/layout/embed.html.eex
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui" />
+ <title><%= Pleroma.Config.get([:instance, :name]) %></title>
+ <meta content='noindex' name='robots'>
+ <%= Phoenix.HTML.raw(assigns[:meta] || "") %>
+ <link rel="stylesheet" href="/embed.css">
+ <base target="_parent">
+ </head>
+ <body>
+ <%= render @view_module, @view_template, assigns %>
+ </body>
+</html>
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..5ab59b57b
--- /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, [autocomplete: false, autocorrect: "off", autocapitalize: "off", autofocus: true, spellcheck: false] %>
+ <%= 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..af85777eb
--- /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, [autocomplete: false, autocorrect: "off", autocapitalize: "off", autofocus: true, pattern: "[0-9]*", spellcheck: false] %>
+ <%= 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 2a7582d45..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.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 e7d2aecad..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,7 +7,7 @@
<input type="hidden" name="profile" value="">
<button type="submit" class="collapse">Remote follow</button>
</form>
- <%= raw Formatter.emojify(@user.name, emoji_for_user(@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>
diff --git a/lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex b/lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex
new file mode 100644
index 000000000..adc3a3e3d
--- /dev/null
+++ b/lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex
@@ -0,0 +1,13 @@
+<%= if @error do %>
+<h2><%= @error %></h2>
+<% end %>
+<h2>Two-factor authentication</h2>
+<p><%= @followee.nickname %></p>
+<img height="128" width="128" src="<%= avatar_url(@followee) %>">
+<%= form_for @conn, remote_follow_path(@conn, :do_follow), [as: "mfa"], fn f -> %>
+<%= text_input f, :code, placeholder: "Authentication code", required: true %>
+<br>
+<%= hidden_input f, :id, value: @followee.id %>
+<%= hidden_input f, :token, value: @mfa_token %>
+<%= submit "Authorize" %>
+<% end %>
diff --git a/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex b/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex
index 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 1873d78df..f02c4075c 100644
--- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex
+++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
@@ -13,23 +13,17 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
alias Pleroma.Notification
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
- alias Pleroma.Web
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(
@@ -46,12 +40,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :notifications_read)
- plug(Pleroma.Plugs.SetFormatPlug when action in [:config, :version])
-
- def help_test(conn, _params) do
- json(conn, "ok")
- end
-
def remote_subscribe(conn, %{"nickname" => nick, "profile" => _}) do
with %User{} = user <- User.get_cached_by_nickname(nick),
avatar = User.avatar_url(user) do
@@ -93,90 +81,14 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
end
- def config(%{assigns: %{format: "xml"}} = conn, _params) do
- instance = Pleroma.Config.get(:instance)
-
- response = """
- <config>
- <site>
- <name>#{Keyword.get(instance, :name)}</name>
- <site>#{Web.base_url()}</site>
- <textlimit>#{Keyword.get(instance, :limit)}</textlimit>
- <closed>#{!Keyword.get(instance, :registrations_open)}</closed>
- </site>
- </config>
- """
-
- conn
- |> put_resp_content_type("application/xml")
- |> send_resp(200, response)
- end
-
- def config(conn, _params) do
- instance = Pleroma.Config.get(:instance)
-
- vapid_public_key = Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
-
- uploadlimit = %{
- uploadlimit: to_string(Keyword.get(instance, :upload_limit)),
- avatarlimit: to_string(Keyword.get(instance, :avatar_upload_limit)),
- backgroundlimit: to_string(Keyword.get(instance, :background_upload_limit)),
- bannerlimit: to_string(Keyword.get(instance, :banner_upload_limit))
- }
-
- data = %{
- name: Keyword.get(instance, :name),
- description: Keyword.get(instance, :description),
- server: Web.base_url(),
- textlimit: to_string(Keyword.get(instance, :limit)),
- uploadlimit: uploadlimit,
- closed: bool_to_val(Keyword.get(instance, :registrations_open), "0", "1"),
- private: bool_to_val(Keyword.get(instance, :public, true), "0", "1"),
- vapidPublicKey: vapid_public_key,
- accountActivationRequired:
- bool_to_val(Keyword.get(instance, :account_activation_required, false)),
- invitesEnabled: bool_to_val(Keyword.get(instance, :invites_enabled, false)),
- safeDMMentionsEnabled: bool_to_val(Pleroma.Config.get([:instance, :safe_dm_mentions]))
- }
-
- managed_config = Keyword.get(instance, :managed_config)
-
- data =
- if managed_config do
- pleroma_fe = Pleroma.Config.get([:frontend_configurations, :pleroma_fe])
- Map.put(data, "pleromafe", pleroma_fe)
- else
- data
- end
-
- json(conn, %{site: data})
- end
-
- defp bool_to_val(true), do: "1"
- defp bool_to_val(_), do: "0"
- defp bool_to_val(true, val, _), do: val
- defp bool_to_val(_, _, val), do: val
-
def frontend_configurations(conn, _params) do
config =
- Pleroma.Config.get(:frontend_configurations, %{})
+ Config.get(:frontend_configurations, %{})
|> Enum.into(%{})
json(conn, config)
end
- def version(%{assigns: %{format: "xml"}} = conn, _params) do
- version = Pleroma.Application.named_version()
-
- conn
- |> put_resp_content_type("application/xml")
- |> send_resp(200, "<version>#{version}</version>")
- end
-
- def version(conn, _params) do
- json(conn, Pleroma.Application.named_version())
- end
-
def emoji(conn, _params) do
emoji =
Enum.reduce(Emoji.get_all(), %{}, fn {code, %Emoji{file: file, tags: tags}}, acc ->
diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex
index f9c0994da..2294d9d0d 100644
--- a/lib/pleroma/web/twitter_api/twitter_api.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api.ex
@@ -3,81 +3,40 @@
# 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])
+ |> Map.put(:registration_reason, params[:reason])
+
+ 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
@@ -86,20 +45,34 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
case User.register(changeset) do
{:ok, user} ->
+ maybe_notify_admins(user)
{:ok, user}
{: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
+
+ defp maybe_notify_admins(%User{} = account) do
+ if Pleroma.Config.get([:instance, :account_approval_required]) do
+ User.all_superusers()
+ |> Enum.filter(fn user -> not is_nil(user.email) end)
+ |> Enum.each(fn superuser ->
+ superuser
+ |> Pleroma.Emails.AdminEmail.new_unapproved_registration(account)
+ |> Pleroma.Emails.Mailer.deliver_async()
+ end)
end
end
def password_reset(nickname_or_email) do
with true <- is_binary(nickname_or_email),
- %User{local: true, email: email} = user when not is_nil(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
@@ -121,4 +94,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 31adc2817..c2de26b0b 100644
--- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
@@ -6,6 +6,7 @@ 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,11 +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(:skip_plug, OAuthScopesPlug when action in [:oauth_tokens, :revoke_token])
+ plug(
+ :skip_plug,
+ [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :confirm_email
+ )
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ plug(:skip_plug, OAuthScopesPlug when action in [:oauth_tokens, :revoke_token])
action_fallback(:errors)
@@ -46,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")
@@ -64,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)
@@ -75,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/util_view.ex b/lib/pleroma/web/twitter_api/views/util_view.ex
index 52054e020..d3bdb4f62 100644
--- a/lib/pleroma/web/twitter_api/views/util_view.ex
+++ b/lib/pleroma/web/twitter_api/views/util_view.ex
@@ -5,4 +5,18 @@
defmodule Pleroma.Web.TwitterAPI.UtilView do
use Pleroma.Web, :view
import Phoenix.HTML.Form
+ alias Pleroma.Web
+
+ def status_net_config(instance) do
+ """
+ <config>
+ <site>
+ <name>#{Keyword.get(instance, :name)}</name>
+ <site>#{Web.base_url()}</site>
+ <textlimit>#{Keyword.get(instance, :limit)}</textlimit>
+ <closed>#{!Keyword.get(instance, :registrations_open)}</closed>
+ </site>
+ </config>
+ """
+ end
end
diff --git a/lib/pleroma/web/views/embed_view.ex b/lib/pleroma/web/views/embed_view.ex
new file mode 100644
index 000000000..5f50bd155
--- /dev/null
+++ b/lib/pleroma/web/views/embed_view.ex
@@ -0,0 +1,74 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.EmbedView do
+ use Pleroma.Web, :view
+
+ alias Calendar.Strftime
+ alias Pleroma.Activity
+ alias Pleroma.Emoji.Formatter
+ alias Pleroma.Object
+ alias Pleroma.User
+ alias Pleroma.Web.Gettext
+ alias Pleroma.Web.MediaProxy
+ alias Pleroma.Web.Metadata.Utils
+ alias Pleroma.Web.Router.Helpers
+
+ use Phoenix.HTML
+
+ @media_types ["image", "audio", "video"]
+
+ defp fetch_media_type(%{"mediaType" => mediaType}) do
+ Utils.fetch_media_type(@media_types, mediaType)
+ end
+
+ defp open_content? do
+ Pleroma.Config.get(
+ [:frontend_configurations, :collapse_message_with_subjects],
+ true
+ )
+ end
+
+ defp full_nickname(user) do
+ %{host: host} = URI.parse(user.ap_id)
+ "@" <> user.nickname <> "@" <> host
+ end
+
+ defp status_title(%Activity{object: %Object{data: %{"name" => name}}}) when is_binary(name),
+ do: name
+
+ defp status_title(%Activity{object: %Object{data: %{"summary" => summary}}})
+ when is_binary(summary),
+ do: summary
+
+ defp status_title(_), do: nil
+
+ defp activity_content(%Activity{object: %Object{data: %{"content" => content}}}) do
+ content |> Pleroma.HTML.filter_tags() |> raw()
+ end
+
+ defp activity_content(_), do: nil
+
+ defp activity_url(%User{local: true}, activity) do
+ Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity)
+ end
+
+ defp activity_url(%User{local: false}, %Activity{object: %Object{data: data}}) do
+ data["url"] || data["external_url"] || data["id"]
+ end
+
+ defp attachments(%Activity{object: %Object{data: %{"attachment" => attachments}}}) do
+ attachments
+ end
+
+ defp sensitive?(%Activity{object: %Object{data: %{"sensitive" => sensitive}}}) do
+ sensitive
+ end
+
+ defp published(%Activity{object: %Object{data: %{"published" => published}}}) do
+ published
+ |> NaiveDateTime.from_iso8601!()
+ |> Strftime.strftime!("%B %d, %Y, %l:%M %p")
+ end
+end
diff --git a/lib/pleroma/web/views/masto_fe_view.ex b/lib/pleroma/web/views/masto_fe_view.ex
index c3096006e..b1669d198 100644
--- a/lib/pleroma/web/views/masto_fe_view.ex
+++ b/lib/pleroma/web/views/masto_fe_view.ex
@@ -9,36 +9,6 @@ defmodule Pleroma.Web.MastoFEView do
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.CustomEmojiView
- @default_settings %{
- onboarded: true,
- home: %{
- shows: %{
- reblog: true,
- reply: true
- }
- },
- notifications: %{
- alerts: %{
- follow: true,
- favourite: true,
- reblog: true,
- mention: true
- },
- shows: %{
- follow: true,
- favourite: true,
- reblog: true,
- mention: true
- },
- sounds: %{
- follow: true,
- favourite: true,
- reblog: true,
- mention: true
- }
- }
- }
-
def initial_state(token, user, custom_emojis) do
limit = Config.get([:instance, :limit])
@@ -86,7 +56,7 @@ defmodule Pleroma.Web.MastoFEView do
"video\/mp4"
]
},
- settings: user.settings || @default_settings,
+ settings: user.mastofe_settings || %{},
push_subscription: nil,
accounts: %{user.id => render(AccountView, "show.json", user: user, for: user)},
custom_emojis: render(CustomEmojiView, "index.json", custom_emojis: custom_emojis),
diff --git a/lib/pleroma/web/views/streamer_view.ex b/lib/pleroma/web/views/streamer_view.ex
index 443868878..476a33245 100644
--- a/lib/pleroma/web/views/streamer_view.ex
+++ b/lib/pleroma/web/views/streamer_view.ex
@@ -25,7 +25,7 @@ defmodule Pleroma.Web.StreamerView do
|> Jason.encode!()
end
- def render("notification.json", %User{} = user, %Notification{} = notify) do
+ def render("notification.json", %Notification{} = notify, %User{} = user) do
%{
event: "notification",
payload:
@@ -51,6 +51,29 @@ defmodule Pleroma.Web.StreamerView do
|> Jason.encode!()
end
+ def render("chat_update.json", %{chat_message_reference: cm_ref}) do
+ # Explicitly giving the cmr for the object here, so we don't accidentally
+ # send a later 'last_message' that was inserted between inserting this and
+ # streaming it out
+ #
+ # It also contains the chat with a cache of the correct unread count
+ Logger.debug("Trying to stream out #{inspect(cm_ref)}")
+
+ representation =
+ Pleroma.Web.PleromaAPI.ChatView.render(
+ "show.json",
+ %{last_message: cm_ref, chat: cm_ref.chat}
+ )
+
+ %{
+ event: "pleroma:chat_update",
+ payload:
+ representation
+ |> Jason.encode!()
+ }
+ |> Jason.encode!()
+ end
+
def render("conversation.json", %Participation{} = participation) do
%{
event: "conversation",
diff --git a/lib/pleroma/web/web.ex b/lib/pleroma/web/web.ex
index bf48ce26c..4f9281851 100644
--- a/lib/pleroma/web/web.ex
+++ b/lib/pleroma/web/web.ex
@@ -2,6 +2,11 @@
# 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,44 +25,91 @@ 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
- alias Pleroma.Plugs.PlugHelper
-
plug(:set_put_layout)
defp set_put_layout(conn, _) do
put_layout(conn, Pleroma.Config.get(:app_layout, "app.html"))
end
- # Marks a plug intentionally skipped and blocks its execution if it's present in plugs chain
- defp skip_plug(conn, plug_module) do
- try do
- plug_module.skip_plug(conn)
- rescue
- UndefinedFunctionError ->
- raise "#{plug_module} is not skippable. Append `use Pleroma.Web, :plug` to its code."
- 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 %Plug.Conn{halted: false} <- maybe_halt_on_missing_oauth_scopes_check(conn) 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 Pleroma.Plugs.AuthExpectedPlug.auth_expected?(conn) &&
- not PlugHelper.plug_called_or_skipped?(conn, Pleroma.Plugs.OAuthScopesPlug) do
+ if PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) and
+ not PlugHelper.plug_called_or_skipped?(conn, OAuthScopesPlug) do
conn
|> render_error(
:forbidden,
@@ -132,7 +184,8 @@ defmodule Pleroma.Web do
def plug do
quote do
- alias Pleroma.Plugs.PlugHelper
+ @behaviour Pleroma.Web.Plug
+ @behaviour Plug
@doc """
Marks a plug intentionally skipped and blocks its execution if it's present in plugs chain.
@@ -146,14 +199,28 @@ defmodule Pleroma.Web do
end
@impl Plug
- @doc "If marked as skipped, returns `conn`, and calls `perform/2` otherwise."
+ @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__) 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(PlugHelper.called_plugs_list_id(), __MODULE__)
- |> perform(options)
+ conn =
+ PlugHelper.append_to_private_list(
+ conn,
+ PlugHelper.called_plugs_list_id(),
+ __MODULE__
+ )
+
+ apply(__MODULE__, :perform, [conn, options])
end
end
end
diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex
index 8f71820d7..c4051e63e 100644
--- a/lib/pleroma/web/web_finger/web_finger.ex
+++ b/lib/pleroma/web/web_finger/web_finger.ex
@@ -86,54 +86,24 @@ defmodule Pleroma.Web.WebFinger do
|> XmlBuilder.to_doc()
end
- defp get_magic_key("data:application/magic-public-key," <> magic_key) do
- {:ok, magic_key}
- end
+ defp webfinger_from_xml(doc) do
+ subject = XML.string_from_xpath("//Subject", doc)
- defp get_magic_key(nil) do
- Logger.debug("Undefined magic key.")
- {:ok, nil}
- end
+ subscribe_address =
+ ~s{//Link[@rel="http://ostatus.org/schema/1.0/subscribe"]/@template}
+ |> XML.string_from_xpath(doc)
- defp get_magic_key(_) do
- {:error, "Missing magic key data."}
- end
+ ap_id =
+ ~s{//Link[@rel="self" and @type="application/activity+json"]/@href}
+ |> 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
- }
-
- {: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)}"}
@@ -181,6 +149,18 @@ defmodule Pleroma.Web.WebFinger do
end
end
+ defp get_address_from_domain(domain, encoded_account) when is_binary(domain) do
+ case find_lrdd_template(domain) do
+ {:ok, template} ->
+ String.replace(template, "{uri}", encoded_account)
+
+ _ ->
+ "https://#{domain}/.well-known/webfinger?resource=#{encoded_account}"
+ end
+ end
+
+ defp get_address_from_domain(_, _), do: nil
+
@spec finger(String.t()) :: {:ok, map()} | {:error, any()}
def finger(account) do
account = String.trim_leading(account, "@")
@@ -195,19 +175,11 @@ defmodule Pleroma.Web.WebFinger do
encoded_account = URI.encode("acct:#{account}")
- address =
- case find_lrdd_template(domain) do
- {:ok, template} ->
- String.replace(template, "{uri}", encoded_account)
-
- _ ->
- "https://#{domain}/.well-known/webfinger?resource=#{encoded_account}"
- end
-
- with response <-
+ with address when is_binary(address) <- get_address_from_domain(domain, encoded_account),
+ 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/workers/attachments_cleanup_worker.ex b/lib/pleroma/workers/attachments_cleanup_worker.ex
index 3c5820a86..58226b395 100644
--- a/lib/pleroma/workers/attachments_cleanup_worker.ex
+++ b/lib/pleroma/workers/attachments_cleanup_worker.ex
@@ -11,85 +11,103 @@ defmodule Pleroma.Workers.AttachmentsCleanupWorker do
use Pleroma.Workers.WorkerHelper, queue: "attachments_cleanup"
@impl Oban.Worker
- def perform(
- %{
+ def perform(%Job{
+ args: %{
"op" => "cleanup_attachments",
"object" => %{"data" => %{"attachment" => [_ | _] = attachments, "actor" => actor}}
- },
- _job
- ) do
- hrefs =
- Enum.flat_map(attachments, fn attachment ->
- Enum.map(attachment["url"], & &1["href"])
- end)
+ }
+ }) do
+ attachments
+ |> Enum.flat_map(fn item -> Enum.map(item["url"], & &1["href"]) end)
+ |> fetch_objects
+ |> prepare_objects(actor, Enum.map(attachments, & &1["name"]))
+ |> filter_objects
+ |> do_clean
+
+ {:ok, :success}
+ end
- names = Enum.map(attachments, & &1["name"])
+ def perform(%Job{args: %{"op" => "cleanup_attachments", "object" => _object}}), do: {:ok, :skip}
+ defp do_clean({object_ids, attachment_urls}) do
uploader = Pleroma.Config.get([Pleroma.Upload, :uploader])
- # find all objects for copies of the attachments, name and actor doesn't matter here
- delete_ids =
- from(o in Object,
- where:
- fragment(
- "to_jsonb(array(select jsonb_array_elements((?)#>'{url}') ->> 'href' where jsonb_typeof((?)#>'{url}') = 'array'))::jsonb \\?| (?)",
- o.data,
- o.data,
- ^hrefs
- )
+ prefix =
+ case Pleroma.Config.get([Pleroma.Upload, :base_url]) do
+ nil -> "media"
+ _ -> ""
+ end
+
+ base_url =
+ String.trim_trailing(
+ Pleroma.Config.get([Pleroma.Upload, :base_url], Pleroma.Web.base_url()),
+ "/"
)
- # The query above can be time consumptive on large instances until we
- # refactor how uploads are stored
- |> Repo.all(timeout: :infinity)
- # we should delete 1 object for any given attachment, but don't delete
- # files if there are more than 1 object for it
- |> Enum.reduce(%{}, fn %{
- id: id,
- data: %{
- "url" => [%{"href" => href}],
- "actor" => obj_actor,
- "name" => name
- }
- },
- acc ->
- Map.update(acc, href, %{id: id, count: 1}, fn val ->
- case obj_actor == actor and name in names do
- true ->
- # set id of the actor's object that will be deleted
- %{val | id: id, count: val.count + 1}
-
- false ->
- # another actor's object, just increase count to not delete file
- %{val | count: val.count + 1}
- end
- end)
- end)
- |> Enum.map(fn {href, %{id: id, count: count}} ->
- # only delete files that have single instance
- with 1 <- count do
- prefix =
- case Pleroma.Config.get([Pleroma.Upload, :base_url]) do
- nil -> "media"
- _ -> ""
- end
-
- base_url =
- String.trim_trailing(
- Pleroma.Config.get([Pleroma.Upload, :base_url], Pleroma.Web.base_url()),
- "/"
- )
-
- file_path = String.trim_leading(href, "#{base_url}/#{prefix}")
-
- uploader.delete_file(file_path)
- end
- id
- end)
+ Enum.each(attachment_urls, fn href ->
+ href
+ |> String.trim_leading("#{base_url}/#{prefix}")
+ |> uploader.delete_file()
+ end)
+
+ delete_objects(object_ids)
+ end
- from(o in Object, where: o.id in ^delete_ids)
- |> Repo.delete_all()
+ defp delete_objects([_ | _] = object_ids) do
+ Repo.delete_all(from(o in Object, where: o.id in ^object_ids))
end
- def perform(%{"op" => "cleanup_attachments", "object" => _object}, _job), do: :ok
+ defp delete_objects(_), do: :ok
+
+ # we should delete 1 object for any given attachment, but don't delete
+ # files if there are more than 1 object for it
+ defp filter_objects(objects) do
+ Enum.reduce(objects, {[], []}, fn {href, %{id: id, count: count}}, {ids, hrefs} ->
+ with 1 <- count do
+ {ids ++ [id], hrefs ++ [href]}
+ else
+ _ -> {ids ++ [id], hrefs}
+ end
+ end)
+ end
+
+ defp prepare_objects(objects, actor, names) do
+ objects
+ |> Enum.reduce(%{}, fn %{
+ id: id,
+ data: %{
+ "url" => [%{"href" => href}],
+ "actor" => obj_actor,
+ "name" => name
+ }
+ },
+ acc ->
+ Map.update(acc, href, %{id: id, count: 1}, fn val ->
+ case obj_actor == actor and name in names do
+ true ->
+ # set id of the actor's object that will be deleted
+ %{val | id: id, count: val.count + 1}
+
+ false ->
+ # another actor's object, just increase count to not delete file
+ %{val | count: val.count + 1}
+ end
+ end)
+ end)
+ end
+
+ defp fetch_objects(hrefs) do
+ from(o in Object,
+ where:
+ fragment(
+ "to_jsonb(array(select jsonb_array_elements((?)#>'{url}') ->> 'href' where jsonb_typeof((?)#>'{url}') = 'array'))::jsonb \\?| (?)",
+ o.data,
+ o.data,
+ ^hrefs
+ )
+ )
+ # The query above can be time consumptive on large instances until we
+ # refactor how uploads are stored
+ |> Repo.all(timeout: :infinity)
+ end
end
diff --git a/lib/pleroma/workers/background_worker.ex b/lib/pleroma/workers/background_worker.ex
index 57c3a9c3a..cec5a7462 100644
--- a/lib/pleroma/workers/background_worker.ex
+++ b/lib/pleroma/workers/background_worker.ex
@@ -11,59 +11,59 @@ defmodule Pleroma.Workers.BackgroundWorker do
@impl Oban.Worker
- def perform(%{"op" => "deactivate_user", "user_id" => user_id, "status" => status}, _job) do
+ def perform(%Job{args: %{"op" => "deactivate_user", "user_id" => user_id, "status" => status}}) do
user = User.get_cached_by_id(user_id)
User.perform(:deactivate_async, user, status)
end
- def perform(%{"op" => "delete_user", "user_id" => user_id}, _job) do
+ def perform(%Job{args: %{"op" => "delete_user", "user_id" => user_id}}) do
user = User.get_cached_by_id(user_id)
User.perform(:delete, user)
end
- def perform(%{"op" => "force_password_reset", "user_id" => user_id}, _job) do
+ def perform(%Job{args: %{"op" => "force_password_reset", "user_id" => user_id}}) do
user = User.get_cached_by_id(user_id)
User.perform(:force_password_reset, user)
end
- def perform(
- %{
+ def perform(%Job{
+ args: %{
"op" => "blocks_import",
"blocker_id" => blocker_id,
"blocked_identifiers" => blocked_identifiers
- },
- _job
- ) do
+ }
+ }) do
blocker = User.get_cached_by_id(blocker_id)
{:ok, User.perform(:blocks_import, blocker, blocked_identifiers)}
end
- def perform(
- %{
+ def perform(%Job{
+ args: %{
"op" => "follow_import",
"follower_id" => follower_id,
"followed_identifiers" => followed_identifiers
- },
- _job
- ) do
+ }
+ }) do
follower = User.get_cached_by_id(follower_id)
{:ok, User.perform(:follow_import, follower, followed_identifiers)}
end
- def perform(%{"op" => "media_proxy_preload", "message" => message}, _job) do
+ def perform(%Job{args: %{"op" => "media_proxy_preload", "message" => message}}) do
MediaProxyWarmingPolicy.perform(:preload, message)
end
- def perform(%{"op" => "media_proxy_prefetch", "url" => url}, _job) do
+ def perform(%Job{args: %{"op" => "media_proxy_prefetch", "url" => url}}) do
MediaProxyWarmingPolicy.perform(:prefetch, url)
end
- def perform(%{"op" => "fetch_data_for_activity", "activity_id" => activity_id}, _job) do
+ def perform(%Job{args: %{"op" => "fetch_data_for_activity", "activity_id" => activity_id}}) do
activity = Activity.get_by_id(activity_id)
Pleroma.Web.RichMedia.Helpers.perform(:fetch, activity)
end
- def perform(%{"op" => "move_following", "origin_id" => origin_id, "target_id" => target_id}, _) do
+ def perform(%Job{
+ args: %{"op" => "move_following", "origin_id" => origin_id, "target_id" => target_id}
+ }) do
origin = User.get_cached_by_id(origin_id)
target = User.get_cached_by_id(target_id)
diff --git a/lib/pleroma/workers/cron/clear_oauth_token_worker.ex b/lib/pleroma/workers/cron/clear_oauth_token_worker.ex
index 341eff054..276f47efc 100644
--- a/lib/pleroma/workers/cron/clear_oauth_token_worker.ex
+++ b/lib/pleroma/workers/cron/clear_oauth_token_worker.ex
@@ -13,9 +13,11 @@ defmodule Pleroma.Workers.Cron.ClearOauthTokenWorker do
alias Pleroma.Web.OAuth.Token
@impl Oban.Worker
- def perform(_opts, _job) do
+ def perform(_job) do
if Config.get([:oauth2, :clean_expired_tokens], false) do
Token.delete_expired_tokens()
end
+
+ :ok
end
end
diff --git a/lib/pleroma/workers/cron/digest_emails_worker.ex b/lib/pleroma/workers/cron/digest_emails_worker.ex
index dd13c3b17..0c56f00fb 100644
--- a/lib/pleroma/workers/cron/digest_emails_worker.ex
+++ b/lib/pleroma/workers/cron/digest_emails_worker.ex
@@ -19,7 +19,7 @@ defmodule Pleroma.Workers.Cron.DigestEmailsWorker do
require Logger
@impl Oban.Worker
- def perform(_opts, _job) do
+ def perform(_job) do
config = Config.get([:email_notifications, :digest])
if config[:active] do
@@ -38,6 +38,8 @@ defmodule Pleroma.Workers.Cron.DigestEmailsWorker do
|> Repo.all()
|> send_emails
end
+
+ :ok
end
def send_emails(users) do
diff --git a/lib/pleroma/workers/cron/new_users_digest_worker.ex b/lib/pleroma/workers/cron/new_users_digest_worker.ex
index 9bd0a5621..8bbaed83d 100644
--- a/lib/pleroma/workers/cron/new_users_digest_worker.ex
+++ b/lib/pleroma/workers/cron/new_users_digest_worker.ex
@@ -12,7 +12,7 @@ defmodule Pleroma.Workers.Cron.NewUsersDigestWorker do
use Pleroma.Workers.WorkerHelper, queue: "new_users_digest"
@impl Oban.Worker
- def perform(_args, _job) do
+ def perform(_job) do
if Pleroma.Config.get([Pleroma.Emails.NewUsersDigestEmail, :enabled]) do
today = NaiveDateTime.utc_now() |> Timex.beginning_of_day()
@@ -57,5 +57,7 @@ defmodule Pleroma.Workers.Cron.NewUsersDigestWorker do
|> Enum.each(&Pleroma.Emails.Mailer.deliver/1)
end
end
+
+ :ok
end
end
diff --git a/lib/pleroma/workers/cron/purge_expired_activities_worker.ex b/lib/pleroma/workers/cron/purge_expired_activities_worker.ex
index b8953dd7f..6549207fc 100644
--- a/lib/pleroma/workers/cron/purge_expired_activities_worker.ex
+++ b/lib/pleroma/workers/cron/purge_expired_activities_worker.ex
@@ -20,10 +20,12 @@ defmodule Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker do
@interval :timer.minutes(1)
@impl Oban.Worker
- def perform(_opts, _job) do
+ def perform(_job) do
if Config.get([ActivityExpiration, :enabled]) do
Enum.each(ActivityExpiration.due_expirations(@interval), &delete_activity/1)
end
+ after
+ :ok
end
def delete_activity(%ActivityExpiration{activity_id: activity_id}) do
@@ -39,7 +41,7 @@ defmodule Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker do
{:user, _} ->
Logger.error(
- "#{__MODULE__} Couldn't delete expired activity: not found actorof ##{activity_id}"
+ "#{__MODULE__} Couldn't delete expired activity: not found actor of ##{activity_id}"
)
end
end
diff --git a/lib/pleroma/workers/cron/stats_worker.ex b/lib/pleroma/workers/cron/stats_worker.ex
index e9b8d59c4..6a79540bc 100644
--- a/lib/pleroma/workers/cron/stats_worker.ex
+++ b/lib/pleroma/workers/cron/stats_worker.ex
@@ -10,7 +10,8 @@ defmodule Pleroma.Workers.Cron.StatsWorker do
use Oban.Worker, queue: "background"
@impl Oban.Worker
- def perform(_opts, _job) do
+ def perform(_job) do
Pleroma.Stats.do_collect()
+ :ok
end
end
diff --git a/lib/pleroma/workers/mailer_worker.ex b/lib/pleroma/workers/mailer_worker.ex
index 6955338a5..32273cfa5 100644
--- a/lib/pleroma/workers/mailer_worker.ex
+++ b/lib/pleroma/workers/mailer_worker.ex
@@ -6,7 +6,7 @@ defmodule Pleroma.Workers.MailerWorker do
use Pleroma.Workers.WorkerHelper, queue: "mailer"
@impl Oban.Worker
- def perform(%{"op" => "email", "encoded_email" => encoded_email, "config" => config}, _job) do
+ def perform(%Job{args: %{"op" => "email", "encoded_email" => encoded_email, "config" => config}}) do
encoded_email
|> Base.decode64!()
|> :erlang.binary_to_term()
diff --git a/lib/pleroma/workers/publisher_worker.ex b/lib/pleroma/workers/publisher_worker.ex
index daf79efc0..e739c3cd0 100644
--- a/lib/pleroma/workers/publisher_worker.ex
+++ b/lib/pleroma/workers/publisher_worker.ex
@@ -8,17 +8,17 @@ defmodule Pleroma.Workers.PublisherWorker do
use Pleroma.Workers.WorkerHelper, queue: "federator_outgoing"
- def backoff(attempt) when is_integer(attempt) do
+ def backoff(%Job{attempt: attempt}) when is_integer(attempt) do
Pleroma.Workers.WorkerHelper.sidekiq_backoff(attempt, 5)
end
@impl Oban.Worker
- def perform(%{"op" => "publish", "activity_id" => activity_id}, _job) do
+ def perform(%Job{args: %{"op" => "publish", "activity_id" => activity_id}}) do
activity = Activity.get_by_id(activity_id)
Federator.perform(:publish, activity)
end
- def perform(%{"op" => "publish_one", "module" => module_name, "params" => params}, _job) do
+ def perform(%Job{args: %{"op" => "publish_one", "module" => module_name, "params" => params}}) do
params = Map.new(params, fn {k, v} -> {String.to_atom(k), v} end)
Federator.perform(:publish_one, String.to_atom(module_name), params)
end
diff --git a/lib/pleroma/workers/receiver_worker.ex b/lib/pleroma/workers/receiver_worker.ex
index f7a7124f3..1b97af1a8 100644
--- a/lib/pleroma/workers/receiver_worker.ex
+++ b/lib/pleroma/workers/receiver_worker.ex
@@ -8,7 +8,7 @@ defmodule Pleroma.Workers.ReceiverWorker do
use Pleroma.Workers.WorkerHelper, queue: "federator_incoming"
@impl Oban.Worker
- def perform(%{"op" => "incoming_ap_doc", "params" => params}, _job) do
+ def perform(%Job{args: %{"op" => "incoming_ap_doc", "params" => params}}) do
Federator.perform(:incoming_ap_doc, params)
end
end
diff --git a/lib/pleroma/workers/remote_fetcher_worker.ex b/lib/pleroma/workers/remote_fetcher_worker.ex
index ec6534f21..27e2e3386 100644
--- a/lib/pleroma/workers/remote_fetcher_worker.ex
+++ b/lib/pleroma/workers/remote_fetcher_worker.ex
@@ -8,13 +8,7 @@ defmodule Pleroma.Workers.RemoteFetcherWorker do
use Pleroma.Workers.WorkerHelper, queue: "remote_fetcher"
@impl Oban.Worker
- def perform(
- %{
- "op" => "fetch_remote",
- "id" => id
- } = args,
- _job
- ) do
+ def perform(%Job{args: %{"op" => "fetch_remote", "id" => id} = args}) do
{:ok, _object} = Fetcher.fetch_object_from_id(id, depth: args["depth"])
end
end
diff --git a/lib/pleroma/workers/scheduled_activity_worker.ex b/lib/pleroma/workers/scheduled_activity_worker.ex
index 8905f4ad0..dd9986fe4 100644
--- a/lib/pleroma/workers/scheduled_activity_worker.ex
+++ b/lib/pleroma/workers/scheduled_activity_worker.ex
@@ -17,7 +17,7 @@ defmodule Pleroma.Workers.ScheduledActivityWorker do
require Logger
@impl Oban.Worker
- def perform(%{"activity_id" => activity_id}, _job) do
+ def perform(%Job{args: %{"activity_id" => activity_id}}) do
if Config.get([ScheduledActivity, :enabled]) do
case Pleroma.Repo.get(ScheduledActivity, activity_id) do
%ScheduledActivity{} = scheduled_activity ->
@@ -30,6 +30,8 @@ defmodule Pleroma.Workers.ScheduledActivityWorker do
end
defp post_activity(%ScheduledActivity{user_id: user_id, params: params} = scheduled_activity) do
+ params = Map.new(params, fn {key, value} -> {String.to_existing_atom(key), value} end)
+
with {:delete, {:ok, _}} <- {:delete, ScheduledActivity.delete(scheduled_activity)},
{:user, %User{} = user} <- {:user, User.get_cached_by_id(user_id)},
{:post, {:ok, _}} <- {:post, CommonAPI.post(user, params)} do
diff --git a/lib/pleroma/workers/transmogrifier_worker.ex b/lib/pleroma/workers/transmogrifier_worker.ex
index 11239ca5e..15f36375c 100644
--- a/lib/pleroma/workers/transmogrifier_worker.ex
+++ b/lib/pleroma/workers/transmogrifier_worker.ex
@@ -8,7 +8,7 @@ defmodule Pleroma.Workers.TransmogrifierWorker do
use Pleroma.Workers.WorkerHelper, queue: "transmogrifier"
@impl Oban.Worker
- def perform(%{"op" => "user_upgrade", "user_id" => user_id}, _job) do
+ def perform(%Job{args: %{"op" => "user_upgrade", "user_id" => user_id}}) do
user = User.get_cached_by_id(user_id)
Pleroma.Web.ActivityPub.Transmogrifier.perform(:user_upgrade, user)
end
diff --git a/lib/pleroma/workers/web_pusher_worker.ex b/lib/pleroma/workers/web_pusher_worker.ex
index 58ad25e39..0cfdc6a6f 100644
--- a/lib/pleroma/workers/web_pusher_worker.ex
+++ b/lib/pleroma/workers/web_pusher_worker.ex
@@ -9,7 +9,7 @@ defmodule Pleroma.Workers.WebPusherWorker do
use Pleroma.Workers.WorkerHelper, queue: "web_push"
@impl Oban.Worker
- def perform(%{"op" => "web_push", "notification_id" => notification_id}, _job) do
+ def perform(%Job{args: %{"op" => "web_push", "notification_id" => notification_id}}) do
notification =
Notification
|> Repo.get(notification_id)
diff --git a/lib/pleroma/workers/worker_helper.ex b/lib/pleroma/workers/worker_helper.ex
index d1f90c35b..7d1289be2 100644
--- a/lib/pleroma/workers/worker_helper.ex
+++ b/lib/pleroma/workers/worker_helper.ex
@@ -32,6 +32,8 @@ defmodule Pleroma.Workers.WorkerHelper do
queue: unquote(queue),
max_attempts: 1
+ alias Oban.Job
+
def enqueue(op, params, worker_args \\ []) do
params = Map.merge(%{"op" => op}, params)
queue_atom = String.to_atom(unquote(queue))
@@ -39,7 +41,7 @@ defmodule Pleroma.Workers.WorkerHelper do
unquote(caller_module)
|> apply(:new, [params, worker_args])
- |> Pleroma.Repo.insert()
+ |> Oban.insert()
end
end
end