summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/mix/pleroma.ex4
-rw-r--r--lib/mix/tasks/pleroma/benchmark.ex11
-rw-r--r--lib/mix/tasks/pleroma/config.ex10
-rw-r--r--lib/mix/tasks/pleroma/count_statuses.ex4
-rw-r--r--lib/mix/tasks/pleroma/database.ex41
-rw-r--r--lib/mix/tasks/pleroma/digest.ex4
-rw-r--r--lib/mix/tasks/pleroma/docs.ex4
-rw-r--r--lib/mix/tasks/pleroma/ecto.ex (renamed from lib/mix/tasks/pleroma/ecto/ecto.ex)0
-rw-r--r--lib/mix/tasks/pleroma/ecto/migrate.ex4
-rw-r--r--lib/mix/tasks/pleroma/ecto/rollback.ex4
-rw-r--r--lib/mix/tasks/pleroma/email.ex24
-rw-r--r--lib/mix/tasks/pleroma/emoji.ex22
-rw-r--r--lib/mix/tasks/pleroma/frontend.ex36
-rw-r--r--lib/mix/tasks/pleroma/instance.ex64
-rw-r--r--lib/mix/tasks/pleroma/notification_settings.ex4
-rw-r--r--lib/mix/tasks/pleroma/relay.ex23
-rw-r--r--lib/mix/tasks/pleroma/robots_txt.ex (renamed from lib/mix/tasks/pleroma/robotstxt.ex)0
-rw-r--r--lib/mix/tasks/pleroma/user.ex72
-rw-r--r--lib/phoenix/transports/web_socket/raw.ex (renamed from lib/transports.ex)7
-rw-r--r--lib/pleroma/activity.ex37
-rw-r--r--lib/pleroma/activity/ir/topics.ex13
-rw-r--r--lib/pleroma/activity/search.ex5
-rw-r--r--lib/pleroma/activity_expiration.ex67
-rw-r--r--lib/pleroma/application.ex66
-rw-r--r--lib/pleroma/application_requirements.ex72
-rw-r--r--lib/pleroma/bbs/authenticator.ex2
-rw-r--r--lib/pleroma/captcha.ex (renamed from lib/pleroma/captcha/captcha.ex)0
-rw-r--r--lib/pleroma/captcha/kocaptcha.ex5
-rw-r--r--lib/pleroma/captcha/native.ex3
-rw-r--r--lib/pleroma/captcha/service.ex (renamed from lib/pleroma/captcha/captcha_service.ex)0
-rw-r--r--lib/pleroma/chat.ex33
-rw-r--r--lib/pleroma/config.ex51
-rw-r--r--lib/pleroma/config/deprecation_warnings.ex138
-rw-r--r--lib/pleroma/config/oban.ex38
-rw-r--r--lib/pleroma/config_db.ex (renamed from lib/pleroma/config/config_db.ex)0
-rw-r--r--lib/pleroma/constants.ex2
-rw-r--r--lib/pleroma/conversation.ex6
-rw-r--r--lib/pleroma/conversation/participation.ex27
-rw-r--r--lib/pleroma/conversation/participation/recipient_ship.ex (renamed from lib/pleroma/conversation/participation_recipient_ship.ex)0
-rw-r--r--lib/pleroma/docs/generator.ex4
-rw-r--r--lib/pleroma/docs/json.ex10
-rw-r--r--lib/pleroma/docs/markdown.ex4
-rw-r--r--lib/pleroma/ecto_type/activity_pub/object_validators/emoji.ex34
-rw-r--r--lib/pleroma/emails/admin_email.ex26
-rw-r--r--lib/pleroma/emails/mailer.ex5
-rw-r--r--lib/pleroma/emails/user_email.ex57
-rw-r--r--lib/pleroma/emoji.ex3
-rw-r--r--lib/pleroma/emoji/pack.ex144
-rw-r--r--lib/pleroma/following_relationship.ex14
-rw-r--r--lib/pleroma/frontend.ex110
-rw-r--r--lib/pleroma/gun.ex (renamed from lib/pleroma/gun/gun.ex)0
-rw-r--r--lib/pleroma/gun/conn.ex14
-rw-r--r--lib/pleroma/gun/connection_pool.ex15
-rw-r--r--lib/pleroma/gun/connection_pool/reclaimer.ex4
-rw-r--r--lib/pleroma/gun/connection_pool/worker.ex82
-rw-r--r--lib/pleroma/gun/connection_pool/worker_supervisor.ex4
-rw-r--r--lib/pleroma/helpers/inet_helper.ex19
-rw-r--r--lib/pleroma/helpers/media_helper.ex162
-rw-r--r--lib/pleroma/helpers/qt_fast_start.ex131
-rw-r--r--lib/pleroma/helpers/uri_helper.ex14
-rw-r--r--lib/pleroma/html.ex33
-rw-r--r--lib/pleroma/http.ex (renamed from lib/pleroma/http/http.ex)43
-rw-r--r--lib/pleroma/http/adapter_helper.ex21
-rw-r--r--lib/pleroma/http/adapter_helper/default.ex4
-rw-r--r--lib/pleroma/http/adapter_helper/gun.ex46
-rw-r--r--lib/pleroma/http/adapter_helper/hackney.ex23
-rw-r--r--lib/pleroma/http/ex_aws.ex2
-rw-r--r--lib/pleroma/http/request_builder.ex6
-rw-r--r--lib/pleroma/http/tzdata.ex4
-rw-r--r--lib/pleroma/http/web_push.ex12
-rw-r--r--lib/pleroma/instances.ex1
-rw-r--r--lib/pleroma/instances/instance.ex38
-rw-r--r--lib/pleroma/job_queue_monitor.ex14
-rw-r--r--lib/pleroma/jwt.ex4
-rw-r--r--lib/pleroma/mfa/token.ex71
-rw-r--r--lib/pleroma/migration_helper/notification_backfill.ex15
-rw-r--r--lib/pleroma/mime.ex120
-rw-r--r--lib/pleroma/moderation_log.ex49
-rw-r--r--lib/pleroma/notification.ex31
-rw-r--r--lib/pleroma/object.ex15
-rw-r--r--lib/pleroma/object/containment.ex9
-rw-r--r--lib/pleroma/object/fetcher.ex113
-rw-r--r--lib/pleroma/password_reset_token.ex11
-rw-r--r--lib/pleroma/plugs/remote_ip.ex51
-rw-r--r--lib/pleroma/repo.ex23
-rw-r--r--lib/pleroma/repo_streamer.ex34
-rw-r--r--lib/pleroma/reverse_proxy.ex (renamed from lib/pleroma/reverse_proxy/reverse_proxy.ex)5
-rw-r--r--lib/pleroma/reverse_proxy/client/hackney.ex1
-rw-r--r--lib/pleroma/reverse_proxy/client/tesla.ex2
-rw-r--r--lib/pleroma/stats.ex78
-rw-r--r--lib/pleroma/telemetry/logger.ex22
-rw-r--r--lib/pleroma/tesla/middleware/connection_pool.ex50
-rw-r--r--lib/pleroma/tesla/middleware/follow_redirects.ex110
-rw-r--r--lib/pleroma/tests/auth_test_controller.ex4
-rw-r--r--lib/pleroma/upload.ex30
-rw-r--r--lib/pleroma/upload/filter.ex13
-rw-r--r--lib/pleroma/upload/filter/anonymize_filename.ex4
-rw-r--r--lib/pleroma/upload/filter/dedupe.ex4
-rw-r--r--lib/pleroma/upload/filter/exiftool.ex18
-rw-r--r--lib/pleroma/upload/filter/mogrifun.ex17
-rw-r--r--lib/pleroma/upload/filter/mogrify.ex14
-rw-r--r--lib/pleroma/uploaders/s3.ex23
-rw-r--r--lib/pleroma/uploaders/uploader.ex2
-rw-r--r--lib/pleroma/user.ex445
-rw-r--r--lib/pleroma/user/backup.ex258
-rw-r--r--lib/pleroma/user/import.ex85
-rw-r--r--lib/pleroma/user/query.ex45
-rw-r--r--lib/pleroma/user/search.ex72
-rw-r--r--lib/pleroma/user/welcome_chat_message.ex45
-rw-r--r--lib/pleroma/utils.ex35
-rw-r--r--lib/pleroma/web.ex (renamed from lib/pleroma/web/web.ex)19
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub.ex204
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub_controller.ex70
-rw-r--r--lib/pleroma/web/activity_pub/builder.ex57
-rw-r--r--lib/pleroma/web/activity_pub/mrf.ex118
-rw-r--r--lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex26
-rw-r--r--lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex3
-rw-r--r--lib/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy.ex56
-rw-r--r--lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex27
-rw-r--r--lib/pleroma/web/activity_pub/mrf/keyword_policy.ex109
-rw-r--r--lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex37
-rw-r--r--lib/pleroma/web/activity_pub/mrf/mention_policy.ex18
-rw-r--r--lib/pleroma/web/activity_pub/mrf/normalize_markup.ex19
-rw-r--r--lib/pleroma/web/activity_pub/mrf/object_age_policy.ex41
-rw-r--r--lib/pleroma/web/activity_pub/mrf/reject_non_public.ex23
-rw-r--r--lib/pleroma/web/activity_pub/mrf/simple_policy.ex108
-rw-r--r--lib/pleroma/web/activity_pub/mrf/subchain_policy.ex27
-rw-r--r--lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex21
-rw-r--r--lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex28
-rw-r--r--lib/pleroma/web/activity_pub/object_validator.ex118
-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.ex7
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/answer_validator.ex62
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/article_note_validator.ex106
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex66
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/audio_video_validator.ex134
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex2
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/common_fixes.ex31
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/common_validations.ex73
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex146
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/create_note_validator.ex9
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/delete_validator.ex28
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex4
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/event_validator.ex97
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/note_validator.ex63
-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.ex112
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/undo_validator.ex4
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex24
-rw-r--r--lib/pleroma/web/activity_pub/pipeline.ex11
-rw-r--r--lib/pleroma/web/activity_pub/publisher.ex10
-rw-r--r--lib/pleroma/web/activity_pub/relay.ex76
-rw-r--r--lib/pleroma/web/activity_pub/side_effects.ex148
-rw-r--r--lib/pleroma/web/activity_pub/transmogrifier.ex234
-rw-r--r--lib/pleroma/web/activity_pub/utils.ex47
-rw-r--r--lib/pleroma/web/activity_pub/views/user_view.ex5
-rw-r--r--lib/pleroma/web/activity_pub/visibility.ex59
-rw-r--r--lib/pleroma/web/admin_api/controllers/admin_api_controller.ex271
-rw-r--r--lib/pleroma/web/admin_api/controllers/chat_controller.ex85
-rw-r--r--lib/pleroma/web/admin_api/controllers/config_controller.ex2
-rw-r--r--lib/pleroma/web/admin_api/controllers/frontend_controller.ex40
-rw-r--r--lib/pleroma/web/admin_api/controllers/instance_document_controller.ex41
-rw-r--r--lib/pleroma/web/admin_api/controllers/invite_controller.ex2
-rw-r--r--lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex45
-rw-r--r--lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex (renamed from lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex)2
-rw-r--r--lib/pleroma/web/admin_api/controllers/relay_controller.ex20
-rw-r--r--lib/pleroma/web/admin_api/controllers/report_controller.ex4
-rw-r--r--lib/pleroma/web/admin_api/controllers/status_controller.ex2
-rw-r--r--lib/pleroma/web/admin_api/controllers/user_controller.ex281
-rw-r--r--lib/pleroma/web/admin_api/views/account_view.ex9
-rw-r--r--lib/pleroma/web/admin_api/views/chat_view.ex30
-rw-r--r--lib/pleroma/web/admin_api/views/frontend_view.ex21
-rw-r--r--lib/pleroma/web/admin_api/views/media_proxy_cache_view.ex8
-rw-r--r--lib/pleroma/web/admin_api/views/report_view.ex2
-rw-r--r--lib/pleroma/web/admin_api/views/status_view.ex3
-rw-r--r--lib/pleroma/web/api_spec.ex13
-rw-r--r--lib/pleroma/web/api_spec/cast_and_validate.ex4
-rw-r--r--lib/pleroma/web/api_spec/helpers.ex6
-rw-r--r--lib/pleroma/web/api_spec/operations/account_operation.ex45
-rw-r--r--lib/pleroma/web/api_spec/operations/admin/chat_operation.ex96
-rw-r--r--lib/pleroma/web/api_spec/operations/admin/frontend_operation.ex85
-rw-r--r--lib/pleroma/web/api_spec/operations/admin/instance_document_operation.ex115
-rw-r--r--lib/pleroma/web/api_spec/operations/admin/media_proxy_cache_operation.ex47
-rw-r--r--lib/pleroma/web/api_spec/operations/admin/o_auth_app_operation.ex (renamed from lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex)0
-rw-r--r--lib/pleroma/web/api_spec/operations/admin/relay_operation.ex60
-rw-r--r--lib/pleroma/web/api_spec/operations/chat_operation.ex12
-rw-r--r--lib/pleroma/web/api_spec/operations/custom_emoji_operation.ex2
-rw-r--r--lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex8
-rw-r--r--lib/pleroma/web/api_spec/operations/list_operation.ex21
-rw-r--r--lib/pleroma/web/api_spec/operations/notification_operation.ex3
-rw-r--r--lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex79
-rw-r--r--lib/pleroma/web/api_spec/operations/pleroma_emoji_file_operation.ex139
-rw-r--r--lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex123
-rw-r--r--lib/pleroma/web/api_spec/operations/pleroma_instances_operation.ex40
-rw-r--r--lib/pleroma/web/api_spec/operations/status_operation.ex40
-rw-r--r--lib/pleroma/web/api_spec/operations/subscription_operation.ex15
-rw-r--r--lib/pleroma/web/api_spec/operations/timeline_operation.ex12
-rw-r--r--lib/pleroma/web/api_spec/operations/user_import_operation.ex80
-rw-r--r--lib/pleroma/web/api_spec/schemas/account.ex2
-rw-r--r--lib/pleroma/web/api_spec/schemas/chat.ex2
-rw-r--r--lib/pleroma/web/api_spec/schemas/chat_message.ex38
-rw-r--r--lib/pleroma/web/api_spec/schemas/poll.ex9
-rw-r--r--lib/pleroma/web/api_spec/schemas/scheduled_status.ex4
-rw-r--r--lib/pleroma/web/api_spec/schemas/status.ex2
-rw-r--r--lib/pleroma/web/api_spec/schemas/visibility_scope.ex2
-rw-r--r--lib/pleroma/web/auth/ldap_authenticator.ex43
-rw-r--r--lib/pleroma/web/auth/pleroma_authenticator.ex4
-rw-r--r--lib/pleroma/web/auth/totp_authenticator.ex2
-rw-r--r--lib/pleroma/web/common_api.ex (renamed from lib/pleroma/web/common_api/common_api.ex)151
-rw-r--r--lib/pleroma/web/common_api/activity_draft.ex79
-rw-r--r--lib/pleroma/web/common_api/utils.ex137
-rw-r--r--lib/pleroma/web/controller_helper.ex26
-rw-r--r--lib/pleroma/web/endpoint.ex79
-rw-r--r--lib/pleroma/web/fallback/redirect_controller.ex (renamed from lib/pleroma/web/fallback_redirect_controller.ex)10
-rw-r--r--lib/pleroma/web/federator.ex (renamed from lib/pleroma/web/federator/federator.ex)16
-rw-r--r--lib/pleroma/web/feed/feed_view.ex2
-rw-r--r--lib/pleroma/web/feed/tag_controller.ex23
-rw-r--r--lib/pleroma/web/feed/user_controller.ex23
-rw-r--r--lib/pleroma/web/instance_document.ex62
-rw-r--r--lib/pleroma/web/mailer/subscription_controller.ex4
-rw-r--r--lib/pleroma/web/masto_fe_controller.ex4
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/account_controller.ex74
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/app_controller.ex4
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/auth_controller.ex22
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/filter_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/instance_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/list_controller.ex8
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/marker_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/media_controller.ex3
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/notification_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/poll_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/report_controller.ex4
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/search_controller.ex4
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/status_controller.ex36
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex33
-rw-r--r--lib/pleroma/web/mastodon_api/views/account_view.ex18
-rw-r--r--lib/pleroma/web/mastodon_api/views/conversation_view.ex14
-rw-r--r--lib/pleroma/web/mastodon_api/views/filter_view.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/views/instance_view.ex5
-rw-r--r--lib/pleroma/web/mastodon_api/views/notification_view.ex11
-rw-r--r--lib/pleroma/web/mastodon_api/views/poll_view.ex20
-rw-r--r--lib/pleroma/web/mastodon_api/views/status_view.ex97
-rw-r--r--lib/pleroma/web/mastodon_api/websocket_handler.ex22
-rw-r--r--lib/pleroma/web/media_proxy.ex (renamed from lib/pleroma/web/media_proxy/media_proxy.ex)100
-rw-r--r--lib/pleroma/web/media_proxy/invalidation.ex4
-rw-r--r--lib/pleroma/web/media_proxy/invalidation/http.ex (renamed from lib/pleroma/web/media_proxy/invalidations/http.ex)2
-rw-r--r--lib/pleroma/web/media_proxy/invalidation/script.ex (renamed from lib/pleroma/web/media_proxy/invalidations/script.ex)0
-rw-r--r--lib/pleroma/web/media_proxy/media_proxy_controller.ex195
-rw-r--r--lib/pleroma/web/metadata.ex11
-rw-r--r--lib/pleroma/web/metadata/providers/feed.ex (renamed from lib/pleroma/web/metadata/feed.ex)0
-rw-r--r--lib/pleroma/web/metadata/providers/open_graph.ex (renamed from lib/pleroma/web/metadata/opengraph.ex)2
-rw-r--r--lib/pleroma/web/metadata/providers/provider.ex (renamed from lib/pleroma/web/metadata/provider.ex)0
-rw-r--r--lib/pleroma/web/metadata/providers/rel_me.ex (renamed from lib/pleroma/web/metadata/rel_me.ex)0
-rw-r--r--lib/pleroma/web/metadata/providers/restrict_indexing.ex (renamed from lib/pleroma/web/metadata/restrict_indexing.ex)9
-rw-r--r--lib/pleroma/web/metadata/providers/twitter_card.ex (renamed from lib/pleroma/web/metadata/twitter_card.ex)2
-rw-r--r--lib/pleroma/web/metadata/utils.ex2
-rw-r--r--lib/pleroma/web/mongoose_im/mongoose_im_controller.ex (renamed from lib/pleroma/web/mongooseim/mongoose_im_controller.ex)4
-rw-r--r--lib/pleroma/web/o_auth.ex (renamed from lib/pleroma/web/oauth.ex)0
-rw-r--r--lib/pleroma/web/o_auth/app.ex (renamed from lib/pleroma/web/oauth/app.ex)0
-rw-r--r--lib/pleroma/web/o_auth/authorization.ex (renamed from lib/pleroma/web/oauth/authorization.ex)0
-rw-r--r--lib/pleroma/web/o_auth/fallback_controller.ex (renamed from lib/pleroma/web/oauth/fallback_controller.ex)0
-rw-r--r--lib/pleroma/web/o_auth/mfa_controller.ex (renamed from lib/pleroma/web/oauth/mfa_controller.ex)0
-rw-r--r--lib/pleroma/web/o_auth/mfa_view.ex (renamed from lib/pleroma/web/oauth/mfa_view.ex)0
-rw-r--r--lib/pleroma/web/o_auth/o_auth_controller.ex (renamed from lib/pleroma/web/oauth/oauth_controller.ex)85
-rw-r--r--lib/pleroma/web/o_auth/o_auth_view.ex (renamed from lib/pleroma/web/oauth/oauth_view.ex)0
-rw-r--r--lib/pleroma/web/o_auth/scopes.ex (renamed from lib/pleroma/web/oauth/scopes.ex)2
-rw-r--r--lib/pleroma/web/o_auth/token.ex (renamed from lib/pleroma/web/oauth/token.ex)25
-rw-r--r--lib/pleroma/web/o_auth/token/query.ex (renamed from lib/pleroma/web/oauth/token/query.ex)6
-rw-r--r--lib/pleroma/web/o_auth/token/strategy/refresh_token.ex (renamed from lib/pleroma/web/oauth/token/strategy/refresh_token.ex)2
-rw-r--r--lib/pleroma/web/o_auth/token/strategy/revoke.ex (renamed from lib/pleroma/web/oauth/token/strategy/revoke.ex)0
-rw-r--r--lib/pleroma/web/o_auth/token/utils.ex (renamed from lib/pleroma/web/oauth/token/utils.ex)0
-rw-r--r--lib/pleroma/web/o_status/o_status_controller.ex (renamed from lib/pleroma/web/ostatus/ostatus_controller.ex)23
-rw-r--r--lib/pleroma/web/oauth/token/clean_worker.ex38
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/account_controller.ex11
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/backup_controller.ex28
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/chat_controller.ex115
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex2
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex137
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex125
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex37
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/instances_controller.ex21
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex12
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/notification_controller.ex8
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex2
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex2
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex61
-rw-r--r--lib/pleroma/web/pleroma_api/views/backup_view.ex28
-rw-r--r--lib/pleroma/web/pleroma_api/views/chat/message_reference_view.ex20
-rw-r--r--lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex2
-rw-r--r--lib/pleroma/web/pleroma_api/views/scrobble_view.ex4
-rw-r--r--lib/pleroma/web/plug.ex8
-rw-r--r--lib/pleroma/web/plugs/admin_secret_authentication_plug.ex (renamed from lib/pleroma/plugs/admin_secret_authentication_plug.ex)6
-rw-r--r--lib/pleroma/web/plugs/authentication_plug.ex (renamed from lib/pleroma/plugs/authentication_plug.ex)5
-rw-r--r--lib/pleroma/web/plugs/basic_auth_decoder_plug.ex (renamed from lib/pleroma/plugs/basic_auth_decoder_plug.ex)2
-rw-r--r--lib/pleroma/web/plugs/cache.ex (renamed from lib/pleroma/plugs/cache.ex)6
-rw-r--r--lib/pleroma/web/plugs/digest_plug.ex (renamed from lib/pleroma/plugs/digest.ex)0
-rw-r--r--lib/pleroma/web/plugs/ensure_authenticated_plug.ex (renamed from lib/pleroma/plugs/ensure_authenticated_plug.ex)2
-rw-r--r--lib/pleroma/web/plugs/ensure_public_or_authenticated_plug.ex (renamed from lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex)2
-rw-r--r--lib/pleroma/web/plugs/ensure_user_key_plug.ex (renamed from lib/pleroma/plugs/ensure_user_key_plug.ex)2
-rw-r--r--lib/pleroma/web/plugs/expect_authenticated_check_plug.ex (renamed from lib/pleroma/plugs/expect_authenticated_check_plug.ex)4
-rw-r--r--lib/pleroma/web/plugs/expect_public_or_authenticated_check_plug.ex (renamed from lib/pleroma/plugs/expect_public_or_authenticated_check_plug.ex)4
-rw-r--r--lib/pleroma/web/plugs/federating_plug.ex (renamed from lib/pleroma/plugs/federating_plug.ex)2
-rw-r--r--lib/pleroma/web/plugs/frontend_static.ex59
-rw-r--r--lib/pleroma/web/plugs/http_security_plug.ex (renamed from lib/pleroma/plugs/http_security_plug.ex)2
-rw-r--r--lib/pleroma/web/plugs/http_signature_plug.ex (renamed from lib/pleroma/plugs/http_signature.ex)0
-rw-r--r--lib/pleroma/web/plugs/idempotency_plug.ex (renamed from lib/pleroma/plugs/idempotency_plug.ex)2
-rw-r--r--lib/pleroma/web/plugs/instance_static.ex (renamed from lib/pleroma/plugs/instance_static.ex)17
-rw-r--r--lib/pleroma/web/plugs/legacy_authentication_plug.ex (renamed from lib/pleroma/plugs/legacy_authentication_plug.ex)5
-rw-r--r--lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex (renamed from lib/pleroma/plugs/mapped_signature_to_identity_plug.ex)0
-rw-r--r--lib/pleroma/web/plugs/o_auth_plug.ex (renamed from lib/pleroma/plugs/oauth_plug.ex)2
-rw-r--r--lib/pleroma/web/plugs/o_auth_scopes_plug.ex (renamed from lib/pleroma/plugs/oauth_scopes_plug.ex)4
-rw-r--r--lib/pleroma/web/plugs/plug_helper.ex (renamed from lib/pleroma/plugs/plug_helper.ex)2
-rw-r--r--lib/pleroma/web/plugs/rate_limiter.ex (renamed from lib/pleroma/plugs/rate_limiter/rate_limiter.ex)16
-rw-r--r--lib/pleroma/web/plugs/rate_limiter/limiter_supervisor.ex (renamed from lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex)6
-rw-r--r--lib/pleroma/web/plugs/rate_limiter/supervisor.ex (renamed from lib/pleroma/plugs/rate_limiter/supervisor.ex)8
-rw-r--r--lib/pleroma/web/plugs/remote_ip.ex48
-rw-r--r--lib/pleroma/web/plugs/session_authentication_plug.ex (renamed from lib/pleroma/plugs/session_authentication_plug.ex)2
-rw-r--r--lib/pleroma/web/plugs/set_format_plug.ex (renamed from lib/pleroma/plugs/set_format_plug.ex)2
-rw-r--r--lib/pleroma/web/plugs/set_locale_plug.ex (renamed from lib/pleroma/plugs/set_locale_plug.ex)2
-rw-r--r--lib/pleroma/web/plugs/set_user_session_id_plug.ex (renamed from lib/pleroma/plugs/set_user_session_id_plug.ex)2
-rw-r--r--lib/pleroma/web/plugs/static_fe_plug.ex (renamed from lib/pleroma/plugs/static_fe_plug.ex)2
-rw-r--r--lib/pleroma/web/plugs/trailing_format_plug.ex (renamed from lib/pleroma/plugs/trailing_format_plug.ex)2
-rw-r--r--lib/pleroma/web/plugs/uploaded_media.ex (renamed from lib/pleroma/plugs/uploaded_media.ex)2
-rw-r--r--lib/pleroma/web/plugs/user_enabled_plug.ex (renamed from lib/pleroma/plugs/user_enabled_plug.ex)2
-rw-r--r--lib/pleroma/web/plugs/user_fetcher_plug.ex (renamed from lib/pleroma/plugs/user_fetcher_plug.ex)2
-rw-r--r--lib/pleroma/web/plugs/user_is_admin_plug.ex (renamed from lib/pleroma/plugs/user_is_admin_plug.ex)2
-rw-r--r--lib/pleroma/web/preload/providers/instance.ex (renamed from lib/pleroma/web/preload/instance.ex)11
-rw-r--r--lib/pleroma/web/preload/providers/provider.ex (renamed from lib/pleroma/web/preload/provider.ex)0
-rw-r--r--lib/pleroma/web/preload/providers/timelines.ex (renamed from lib/pleroma/web/preload/timelines.ex)2
-rw-r--r--lib/pleroma/web/preload/providers/user.ex (renamed from lib/pleroma/web/preload/user.ex)0
-rw-r--r--lib/pleroma/web/push.ex (renamed from lib/pleroma/web/push/push.ex)0
-rw-r--r--lib/pleroma/web/push/impl.ex24
-rw-r--r--lib/pleroma/web/push/subscription.ex3
-rw-r--r--lib/pleroma/web/rel_me.ex15
-rw-r--r--lib/pleroma/web/rich_media/helpers.ex75
-rw-r--r--lib/pleroma/web/rich_media/parser.ex122
-rw-r--r--lib/pleroma/web/rich_media/parser/ttl.ex7
-rw-r--r--lib/pleroma/web/rich_media/parser/ttl/aws_signed_url.ex (renamed from lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex)21
-rw-r--r--lib/pleroma/web/rich_media/parsers/o_embed.ex (renamed from lib/pleroma/web/rich_media/parsers/oembed_parser.ex)2
-rw-r--r--lib/pleroma/web/rich_media/parsers/ttl/ttl.ex3
-rw-r--r--lib/pleroma/web/router.ex177
-rw-r--r--lib/pleroma/web/static_fe/static_fe_controller.ex177
-rw-r--r--lib/pleroma/web/streamer.ex (renamed from lib/pleroma/web/streamer/streamer.ex)79
-rw-r--r--lib/pleroma/web/templates/feed/feed/_activity.atom.eex2
-rw-r--r--lib/pleroma/web/templates/feed/feed/_activity.rss.eex2
-rw-r--r--lib/pleroma/web/templates/layout/app.html.eex4
-rw-r--r--lib/pleroma/web/templates/layout/email_styled.html.eex2
-rw-r--r--lib/pleroma/web/templates/layout/metadata_player.html.eex2
-rw-r--r--lib/pleroma/web/templates/layout/static_fe.html.eex2
-rw-r--r--lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex2
-rw-r--r--lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex2
-rw-r--r--lib/pleroma/web/twitter_api/controller.ex (renamed from lib/pleroma/web/twitter_api/twitter_api_controller.ex)4
-rw-r--r--lib/pleroma/web/twitter_api/controllers/password_controller.ex1
-rw-r--r--lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex7
-rw-r--r--lib/pleroma/web/twitter_api/controllers/util_controller.ex45
-rw-r--r--lib/pleroma/web/twitter_api/twitter_api.ex27
-rw-r--r--lib/pleroma/web/twitter_api/views/util_view.ex6
-rw-r--r--lib/pleroma/web/views/email_view.ex4
-rw-r--r--lib/pleroma/web/views/mailer/subscription_view.ex4
-rw-r--r--lib/pleroma/web/views/masto_fe_view.ex32
-rw-r--r--lib/pleroma/web/web_finger.ex (renamed from lib/pleroma/web/web_finger/web_finger.ex)28
-rw-r--r--lib/pleroma/web/web_finger/web_finger_controller.ex4
-rw-r--r--lib/pleroma/web/xml.ex (renamed from lib/pleroma/web/xml/xml.ex)0
-rw-r--r--lib/pleroma/workers/background_worker.ex39
-rw-r--r--lib/pleroma/workers/backup_worker.ex54
-rw-r--r--lib/pleroma/workers/cron/clear_oauth_token_worker.ex23
-rw-r--r--lib/pleroma/workers/cron/digest_emails_worker.ex4
-rw-r--r--lib/pleroma/workers/cron/new_users_digest_worker.ex6
-rw-r--r--lib/pleroma/workers/cron/purge_expired_activities_worker.ex48
-rw-r--r--lib/pleroma/workers/cron/stats_worker.ex16
-rw-r--r--lib/pleroma/workers/mute_expire_worker.ex20
-rw-r--r--lib/pleroma/workers/purge_expired_activity.ex72
-rw-r--r--lib/pleroma/workers/purge_expired_token.ex29
-rw-r--r--lib/pleroma/xml_builder.ex (renamed from lib/xml_builder.ex)0
382 files changed, 8803 insertions, 3521 deletions
diff --git a/lib/mix/pleroma.ex b/lib/mix/pleroma.ex
index 074492a46..6df1cf538 100644
--- a/lib/mix/pleroma.ex
+++ b/lib/mix/pleroma.ex
@@ -14,10 +14,12 @@ defmodule Mix.Pleroma do
:swoosh,
:timex
]
- @cachex_children ["object", "user"]
+ @cachex_children ["object", "user", "scrubber", "web_resp"]
@doc "Common functions to be reused in mix tasks"
def start_pleroma do
Pleroma.Config.Holder.save_default()
+ Pleroma.Config.Oban.warn()
+ Pleroma.Application.limiters_setup()
Application.put_env(:phoenix, :serve_endpoints, false, persistent: true)
if Pleroma.Config.get(:env) != :test do
diff --git a/lib/mix/tasks/pleroma/benchmark.ex b/lib/mix/tasks/pleroma/benchmark.ex
index dd2b9c8f2..a607d5d4f 100644
--- a/lib/mix/tasks/pleroma/benchmark.ex
+++ b/lib/mix/tasks/pleroma/benchmark.ex
@@ -91,20 +91,17 @@ defmodule Mix.Tasks.Pleroma.Benchmark do
"Without conn and without pool" => fn ->
{:ok, %Tesla.Env{}} =
Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500", [],
- adapter: [pool: :no_pool, receive_conn: false]
+ pool: :no_pool,
+ receive_conn: false
)
end,
"Without conn and with pool" => fn ->
{:ok, %Tesla.Env{}} =
- Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500", [],
- adapter: [receive_conn: false]
- )
+ Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500", [], receive_conn: false)
end,
"With reused conn and without pool" => fn ->
{:ok, %Tesla.Env{}} =
- Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500", [],
- adapter: [pool: :no_pool]
- )
+ Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500", [], pool: :no_pool)
end,
"With reused conn and with pool" => fn ->
{:ok, %Tesla.Env{}} = Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500")
diff --git a/lib/mix/tasks/pleroma/config.ex b/lib/mix/tasks/pleroma/config.ex
index 904c5a74b..18f99318d 100644
--- a/lib/mix/tasks/pleroma/config.ex
+++ b/lib/mix/tasks/pleroma/config.ex
@@ -32,7 +32,8 @@ defmodule Mix.Tasks.Pleroma.Config do
@spec migrate_to_db(Path.t() | nil) :: any()
def migrate_to_db(file_path \\ nil) do
- if Pleroma.Config.get([:configurable_from_database]) do
+ with true <- Pleroma.Config.get([:configurable_from_database]),
+ :ok <- Pleroma.Config.DeprecationWarnings.warn() do
config_file =
if file_path do
file_path
@@ -46,7 +47,8 @@ defmodule Mix.Tasks.Pleroma.Config do
do_migrate_to_db(config_file)
else
- migration_error()
+ :error -> deprecation_error()
+ _ -> migration_error()
end
end
@@ -120,6 +122,10 @@ defmodule Mix.Tasks.Pleroma.Config do
)
end
+ defp deprecation_error do
+ shell_error("Migration is not allowed until all deprecation warnings have been resolved.")
+ end
+
if Code.ensure_loaded?(Config.Reader) do
defp config_header, do: "import Config\r\n\r\n"
defp read_file(config_file), do: Config.Reader.read_imports!(config_file)
diff --git a/lib/mix/tasks/pleroma/count_statuses.ex b/lib/mix/tasks/pleroma/count_statuses.ex
index e1e8195dd..8761d8f17 100644
--- a/lib/mix/tasks/pleroma/count_statuses.ex
+++ b/lib/mix/tasks/pleroma/count_statuses.ex
@@ -1,3 +1,7 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
defmodule Mix.Tasks.Pleroma.CountStatuses do
@shortdoc "Re-counts statuses for all users"
diff --git a/lib/mix/tasks/pleroma/database.ex b/lib/mix/tasks/pleroma/database.ex
index 82e2abdcb..a01c36ece 100644
--- a/lib/mix/tasks/pleroma/database.ex
+++ b/lib/mix/tasks/pleroma/database.ex
@@ -10,6 +10,7 @@ defmodule Mix.Tasks.Pleroma.Database do
alias Pleroma.User
require Logger
require Pleroma.Constants
+ import Ecto.Query
import Mix.Pleroma
use Mix.Task
@@ -53,8 +54,6 @@ defmodule Mix.Tasks.Pleroma.Database do
end
def run(["prune_objects" | args]) do
- import Ecto.Query
-
{options, [], []} =
OptionParser.parse(
args,
@@ -94,15 +93,13 @@ defmodule Mix.Tasks.Pleroma.Database do
end
def run(["fix_likes_collections"]) do
- import Ecto.Query
-
start_pleroma()
from(object in Object,
where: fragment("(?)->>'likes' is not null", object.data),
select: %{id: object.id, likes: fragment("(?)->>'likes'", object.data)}
)
- |> Pleroma.RepoStreamer.chunk_stream(100)
+ |> Pleroma.Repo.chunk_stream(100, :batches)
|> Stream.each(fn objects ->
ids =
objects
@@ -130,4 +127,38 @@ defmodule Mix.Tasks.Pleroma.Database do
Maintenance.vacuum(args)
end
+
+ def run(["ensure_expiration"]) do
+ start_pleroma()
+ days = Pleroma.Config.get([:mrf_activity_expiration, :days], 365)
+
+ Pleroma.Activity
+ |> join(:inner, [a], o in Object,
+ on:
+ fragment(
+ "(?->>'id') = COALESCE((?)->'object'->> 'id', (?)->>'object')",
+ o.data,
+ a.data,
+ a.data
+ )
+ )
+ |> where(local: true)
+ |> where([a], fragment("(? ->> 'type'::text) = 'Create'", a.data))
+ |> where([_a, o], fragment("?->>'type' = 'Note'", o.data))
+ |> Pleroma.Repo.chunk_stream(100, :batches)
+ |> Stream.each(fn activities ->
+ Enum.each(activities, fn activity ->
+ expires_at =
+ activity.inserted_at
+ |> DateTime.from_naive!("Etc/UTC")
+ |> Timex.shift(days: days)
+
+ Pleroma.Workers.PurgeExpiredActivity.enqueue(%{
+ activity_id: activity.id,
+ expires_at: expires_at
+ })
+ end)
+ end)
+ |> Stream.run()
+ end
end
diff --git a/lib/mix/tasks/pleroma/digest.ex b/lib/mix/tasks/pleroma/digest.ex
index 3595f912d..cac148b88 100644
--- a/lib/mix/tasks/pleroma/digest.ex
+++ b/lib/mix/tasks/pleroma/digest.ex
@@ -1,3 +1,7 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
defmodule Mix.Tasks.Pleroma.Digest do
use Mix.Task
import Mix.Pleroma
diff --git a/lib/mix/tasks/pleroma/docs.ex b/lib/mix/tasks/pleroma/docs.ex
index 6088fc71d..ad5c37fc9 100644
--- a/lib/mix/tasks/pleroma/docs.ex
+++ b/lib/mix/tasks/pleroma/docs.ex
@@ -1,3 +1,7 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
defmodule Mix.Tasks.Pleroma.Docs do
use Mix.Task
import Mix.Pleroma
diff --git a/lib/mix/tasks/pleroma/ecto/ecto.ex b/lib/mix/tasks/pleroma/ecto.ex
index 3363cd45f..3363cd45f 100644
--- a/lib/mix/tasks/pleroma/ecto/ecto.ex
+++ b/lib/mix/tasks/pleroma/ecto.ex
diff --git a/lib/mix/tasks/pleroma/ecto/migrate.ex b/lib/mix/tasks/pleroma/ecto/migrate.ex
index bc8ed29fb..e903bd171 100644
--- a/lib/mix/tasks/pleroma/ecto/migrate.ex
+++ b/lib/mix/tasks/pleroma/ecto/migrate.ex
@@ -41,6 +41,10 @@ defmodule Mix.Tasks.Pleroma.Ecto.Migrate do
load_pleroma()
{opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)
+ if Application.get_env(:pleroma, Pleroma.Repo)[:ssl] do
+ Application.ensure_all_started(:ssl)
+ end
+
opts =
if opts[:to] || opts[:step] || opts[:all],
do: opts,
diff --git a/lib/mix/tasks/pleroma/ecto/rollback.ex b/lib/mix/tasks/pleroma/ecto/rollback.ex
index f43bd0b98..3dba952cb 100644
--- a/lib/mix/tasks/pleroma/ecto/rollback.ex
+++ b/lib/mix/tasks/pleroma/ecto/rollback.ex
@@ -40,6 +40,10 @@ defmodule Mix.Tasks.Pleroma.Ecto.Rollback do
load_pleroma()
{opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)
+ if Application.get_env(:pleroma, Pleroma.Repo)[:ssl] do
+ Application.ensure_all_started(:ssl)
+ end
+
opts =
if opts[:to] || opts[:step] || opts[:all],
do: opts,
diff --git a/lib/mix/tasks/pleroma/email.ex b/lib/mix/tasks/pleroma/email.ex
index d3fac6ec8..bc5facc09 100644
--- a/lib/mix/tasks/pleroma/email.ex
+++ b/lib/mix/tasks/pleroma/email.ex
@@ -1,12 +1,16 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
defmodule Mix.Tasks.Pleroma.Email do
use Mix.Task
import Mix.Pleroma
- @shortdoc "Simple Email test"
+ @shortdoc "Email administrative tasks"
@moduledoc File.read!("docs/administration/CLI_tasks/email.md")
def run(["test" | args]) do
- Mix.Pleroma.start_pleroma()
+ start_pleroma()
{options, [], []} =
OptionParser.parse(
@@ -21,4 +25,20 @@ defmodule Mix.Tasks.Pleroma.Email do
shell_info("Test email has been sent to #{inspect(email.to)} from #{inspect(email.from)}")
end
+
+ def run(["resend_confirmation_emails"]) do
+ start_pleroma()
+
+ shell_info("Sending emails to all unconfirmed users")
+
+ Pleroma.User.Query.build(%{
+ local: true,
+ deactivated: false,
+ confirmation_pending: true,
+ invisible: false
+ })
+ |> Pleroma.Repo.chunk_stream(500)
+ |> Stream.each(&Pleroma.User.try_send_confirmation_email(&1))
+ |> Stream.run()
+ end
end
diff --git a/lib/mix/tasks/pleroma/emoji.ex b/lib/mix/tasks/pleroma/emoji.ex
index f4eaeac98..1750373f9 100644
--- a/lib/mix/tasks/pleroma/emoji.ex
+++ b/lib/mix/tasks/pleroma/emoji.ex
@@ -15,7 +15,7 @@ defmodule Mix.Tasks.Pleroma.Emoji do
{options, [], []} = parse_global_opts(args)
url_or_path = options[:manifest] || default_manifest()
- manifest = fetch_and_decode(url_or_path)
+ manifest = fetch_and_decode!(url_or_path)
Enum.each(manifest, fn {name, info} ->
to_print = [
@@ -42,7 +42,7 @@ defmodule Mix.Tasks.Pleroma.Emoji do
url_or_path = options[:manifest] || default_manifest()
- manifest = fetch_and_decode(url_or_path)
+ manifest = fetch_and_decode!(url_or_path)
for pack_name <- pack_names do
if Map.has_key?(manifest, pack_name) do
@@ -92,7 +92,7 @@ defmodule Mix.Tasks.Pleroma.Emoji do
])
)
- files = fetch_and_decode(files_loc)
+ files = fetch_and_decode!(files_loc)
IO.puts(IO.ANSI.format(["Unpacking ", :bright, pack_name]))
@@ -183,7 +183,7 @@ defmodule Mix.Tasks.Pleroma.Emoji do
IO.puts("Downloading the pack and generating SHA256")
- binary_archive = Tesla.get!(client(), src).body
+ {:ok, %{body: binary_archive}} = Pleroma.HTTP.get(src)
archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16()
IO.puts("SHA256 is #{archive_sha}")
@@ -243,14 +243,16 @@ defmodule Mix.Tasks.Pleroma.Emoji do
IO.puts("Emoji packs have been reloaded.")
end
- defp fetch_and_decode(from) do
+ defp fetch_and_decode!(from) do
with {:ok, json} <- fetch(from) do
Jason.decode!(json)
+ else
+ {:error, error} -> raise "#{from} cannot be fetched. Error: #{error} occur."
end
end
defp fetch("http" <> _ = from) do
- with {:ok, %{body: body}} <- Tesla.get(client(), from) do
+ with {:ok, %{body: body}} <- Pleroma.HTTP.get(from) do
{:ok, body}
end
end
@@ -269,13 +271,5 @@ defmodule Mix.Tasks.Pleroma.Emoji do
)
end
- defp client do
- middleware = [
- {Tesla.Middleware.FollowRedirects, [max_redirects: 3]}
- ]
-
- Tesla.client(middleware)
- end
-
defp default_manifest, do: Pleroma.Config.get!([:emoji, :default_manifest])
end
diff --git a/lib/mix/tasks/pleroma/frontend.ex b/lib/mix/tasks/pleroma/frontend.ex
new file mode 100644
index 000000000..f15dbc38b
--- /dev/null
+++ b/lib/mix/tasks/pleroma/frontend.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 Mix.Tasks.Pleroma.Frontend do
+ use Mix.Task
+
+ import Mix.Pleroma
+
+ @shortdoc "Manages bundled Pleroma frontends"
+
+ @moduledoc File.read!("docs/administration/CLI_tasks/frontend.md")
+
+ def run(["install", "none" | _args]) do
+ shell_info("Skipping frontend installation because none was requested")
+ "none"
+ end
+
+ def run(["install", frontend | args]) do
+ start_pleroma()
+
+ {options, [], []} =
+ OptionParser.parse(
+ args,
+ strict: [
+ ref: :string,
+ static_dir: :string,
+ build_url: :string,
+ build_dir: :string,
+ file: :string
+ ]
+ )
+
+ Pleroma.Frontend.install(frontend, options)
+ end
+end
diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex
index 91440b453..ac8688424 100644
--- a/lib/mix/tasks/pleroma/instance.ex
+++ b/lib/mix/tasks/pleroma/instance.ex
@@ -33,7 +33,10 @@ defmodule Mix.Tasks.Pleroma.Instance do
uploads_dir: :string,
static_dir: :string,
listen_ip: :string,
- listen_port: :string
+ listen_port: :string,
+ strip_uploads: :string,
+ anonymize_uploads: :string,
+ dedupe_uploads: :string
],
aliases: [
o: :output,
@@ -158,6 +161,30 @@ defmodule Mix.Tasks.Pleroma.Instance do
)
|> Path.expand()
+ strip_uploads =
+ get_option(
+ options,
+ :strip_uploads,
+ "Do you want to strip location (GPS) data from uploaded images? (y/n)",
+ "y"
+ ) === "y"
+
+ anonymize_uploads =
+ get_option(
+ options,
+ :anonymize_uploads,
+ "Do you want to anonymize the filenames of uploads? (y/n)",
+ "n"
+ ) === "y"
+
+ dedupe_uploads =
+ get_option(
+ options,
+ :dedupe_uploads,
+ "Do you want to deduplicate uploaded files? (y/n)",
+ "n"
+ ) === "y"
+
Config.put([:instance, :static_dir], static_dir)
secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64)
@@ -188,7 +215,13 @@ defmodule Mix.Tasks.Pleroma.Instance do
uploads_dir: uploads_dir,
rum_enabled: rum_enabled,
listen_ip: listen_ip,
- listen_port: listen_port
+ listen_port: listen_port,
+ upload_filters:
+ upload_filters(%{
+ strip: strip_uploads,
+ anonymize: anonymize_uploads,
+ dedupe: dedupe_uploads
+ })
)
result_psql =
@@ -247,4 +280,31 @@ defmodule Mix.Tasks.Pleroma.Instance do
File.write(robots_txt_path, robots_txt)
shell_info("Writing #{robots_txt_path}.")
end
+
+ defp upload_filters(filters) when is_map(filters) do
+ enabled_filters =
+ if filters.strip do
+ [Pleroma.Upload.Filter.Exiftool]
+ else
+ []
+ end
+
+ enabled_filters =
+ if filters.anonymize do
+ enabled_filters ++ [Pleroma.Upload.Filter.AnonymizeFilename]
+ else
+ enabled_filters
+ end
+
+ enabled_filters =
+ if filters.dedupe do
+ enabled_filters ++ [Pleroma.Upload.Filter.Dedupe]
+ else
+ enabled_filters
+ end
+
+ enabled_filters
+ end
+
+ defp upload_filters(_), do: []
end
diff --git a/lib/mix/tasks/pleroma/notification_settings.ex b/lib/mix/tasks/pleroma/notification_settings.ex
index 00f5ba7bf..f99275de1 100644
--- a/lib/mix/tasks/pleroma/notification_settings.ex
+++ b/lib/mix/tasks/pleroma/notification_settings.ex
@@ -1,3 +1,7 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
defmodule Mix.Tasks.Pleroma.NotificationSettings do
@shortdoc "Enable&Disable privacy option for push notifications"
@moduledoc """
diff --git a/lib/mix/tasks/pleroma/relay.ex b/lib/mix/tasks/pleroma/relay.ex
index c3312507e..bb808ca47 100644
--- a/lib/mix/tasks/pleroma/relay.ex
+++ b/lib/mix/tasks/pleroma/relay.ex
@@ -21,10 +21,19 @@ defmodule Mix.Tasks.Pleroma.Relay do
end
end
- def run(["unfollow", target]) do
+ def run(["unfollow", target | rest]) do
start_pleroma()
- with {:ok, _activity} <- Relay.unfollow(target) do
+ {options, [], []} =
+ OptionParser.parse(
+ rest,
+ strict: [force: :boolean],
+ aliases: [f: :force]
+ )
+
+ force = Keyword.get(options, :force, false)
+
+ with {:ok, _activity} <- Relay.unfollow(target, %{force: force}) do
# put this task to sleep to allow the genserver to push out the messages
:timer.sleep(500)
else
@@ -35,10 +44,16 @@ defmodule Mix.Tasks.Pleroma.Relay do
def run(["list"]) do
start_pleroma()
- with {:ok, list} <- Relay.list(true) do
- list |> Enum.each(&shell_info(&1))
+ with {:ok, list} <- Relay.list() do
+ Enum.each(list, &print_relay_url/1)
else
{:error, e} -> shell_error("Error while fetching relay subscription list: #{inspect(e)}")
end
end
+
+ defp print_relay_url(%{followed_back: false} = relay) do
+ shell_info("#{relay.actor} - no Accept received (relay didn't follow back)")
+ end
+
+ defp print_relay_url(relay), do: shell_info(relay.actor)
end
diff --git a/lib/mix/tasks/pleroma/robotstxt.ex b/lib/mix/tasks/pleroma/robots_txt.ex
index 24f08180e..24f08180e 100644
--- a/lib/mix/tasks/pleroma/robotstxt.ex
+++ b/lib/mix/tasks/pleroma/robots_txt.ex
diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex
index 01824aa18..a8d251411 100644
--- a/lib/mix/tasks/pleroma/user.ex
+++ b/lib/mix/tasks/pleroma/user.ex
@@ -179,7 +179,7 @@ defmodule Mix.Tasks.Pleroma.User do
start_pleroma()
Pleroma.User.Query.build(%{nickname: "@#{instance}"})
- |> Pleroma.RepoStreamer.chunk_stream(500)
+ |> Pleroma.Repo.chunk_stream(500, :batches)
|> Stream.each(fn users ->
users
|> Enum.each(fn user ->
@@ -196,17 +196,24 @@ defmodule Mix.Tasks.Pleroma.User do
OptionParser.parse(
rest,
strict: [
- moderator: :boolean,
admin: :boolean,
- locked: :boolean
+ confirmed: :boolean,
+ locked: :boolean,
+ moderator: :boolean
]
)
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
user =
- case Keyword.get(options, :moderator) do
+ case Keyword.get(options, :admin) do
nil -> user
- value -> set_moderator(user, value)
+ value -> set_admin(user, value)
+ end
+
+ user =
+ case Keyword.get(options, :confirmed) do
+ nil -> user
+ value -> set_confirmed(user, value)
end
user =
@@ -216,9 +223,9 @@ defmodule Mix.Tasks.Pleroma.User do
end
_user =
- case Keyword.get(options, :admin) do
+ case Keyword.get(options, :moderator) do
nil -> user
- value -> set_admin(user, value)
+ value -> set_moderator(user, value)
end
else
_ ->
@@ -353,6 +360,42 @@ defmodule Mix.Tasks.Pleroma.User do
end
end
+ def run(["confirm_all"]) do
+ start_pleroma()
+
+ Pleroma.User.Query.build(%{
+ local: true,
+ deactivated: false,
+ is_moderator: false,
+ is_admin: false,
+ invisible: false
+ })
+ |> Pleroma.Repo.chunk_stream(500, :batches)
+ |> Stream.each(fn users ->
+ users
+ |> Enum.each(fn user -> User.need_confirmation(user, false) end)
+ end)
+ |> Stream.run()
+ end
+
+ def run(["unconfirm_all"]) do
+ start_pleroma()
+
+ Pleroma.User.Query.build(%{
+ local: true,
+ deactivated: false,
+ is_moderator: false,
+ is_admin: false,
+ invisible: false
+ })
+ |> Pleroma.Repo.chunk_stream(500, :batches)
+ |> Stream.each(fn users ->
+ users
+ |> Enum.each(fn user -> User.need_confirmation(user, true) end)
+ end)
+ |> Stream.run()
+ end
+
def run(["sign_out", nickname]) do
start_pleroma()
@@ -370,13 +413,13 @@ defmodule Mix.Tasks.Pleroma.User do
start_pleroma()
Pleroma.User.Query.build(%{local: true})
- |> Pleroma.RepoStreamer.chunk_stream(500)
+ |> Pleroma.Repo.chunk_stream(500, :batches)
|> Stream.each(fn users ->
users
|> Enum.each(fn user ->
shell_info(
"#{user.nickname} moderator: #{user.is_moderator}, admin: #{user.is_admin}, locked: #{
- user.locked
+ user.is_locked
}, deactivated: #{user.deactivated}"
)
end)
@@ -404,10 +447,17 @@ defmodule Mix.Tasks.Pleroma.User do
defp set_locked(user, value) do
{:ok, user} =
user
- |> Changeset.change(%{locked: value})
+ |> Changeset.change(%{is_locked: value})
|> User.update_and_set_cache()
- shell_info("Locked status of #{user.nickname}: #{user.locked}")
+ shell_info("Locked status of #{user.nickname}: #{user.is_locked}")
+ user
+ end
+
+ defp set_confirmed(user, value) do
+ {:ok, user} = User.need_confirmation(user, !value)
+
+ shell_info("Confirmation pending status of #{user.nickname}: #{user.confirmation_pending}")
user
end
end
diff --git a/lib/transports.ex b/lib/phoenix/transports/web_socket/raw.ex
index aab7fad99..c3665bebe 100644
--- a/lib/transports.ex
+++ b/lib/phoenix/transports/web_socket/raw.ex
@@ -31,7 +31,12 @@ defmodule Phoenix.Transports.WebSocket.Raw do
case conn do
%{halted: false} = conn ->
- case Transport.connect(endpoint, handler, transport, __MODULE__, nil, conn.params) do
+ case handler.connect(%{
+ endpoint: endpoint,
+ transport: transport,
+ options: [serializer: nil],
+ params: conn.params
+ }) do
{:ok, socket} ->
{:ok, conn, {__MODULE__, {socket, opts}}}
diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex
index c3cea8d2a..8559ae6a9 100644
--- a/lib/pleroma/activity.ex
+++ b/lib/pleroma/activity.ex
@@ -7,7 +7,6 @@ defmodule Pleroma.Activity do
alias Pleroma.Activity
alias Pleroma.Activity.Queries
- alias Pleroma.ActivityExpiration
alias Pleroma.Bookmark
alias Pleroma.Notification
alias Pleroma.Object
@@ -15,6 +14,7 @@ defmodule Pleroma.Activity do
alias Pleroma.ReportNote
alias Pleroma.ThreadMute
alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ActivityPub
import Ecto.Changeset
import Ecto.Query
@@ -60,8 +60,6 @@ defmodule Pleroma.Activity do
# typical case.
has_one(:object, Object, on_delete: :nothing, foreign_key: :id)
- has_one(:expiration, ActivityExpiration, on_delete: :delete_all)
-
timestamps()
end
@@ -156,6 +154,18 @@ defmodule Pleroma.Activity do
def get_bookmark(_, _), do: nil
+ def get_report(activity_id) do
+ opts = %{
+ type: "Flag",
+ skip_preload: true,
+ preload_report_notes: true
+ }
+
+ ActivityPub.fetch_activities_query([], opts)
+ |> where(id: ^activity_id)
+ |> Repo.one()
+ end
+
def change(struct, params \\ %{}) do
struct
|> cast(params, [:data, :recipients])
@@ -304,14 +314,14 @@ defmodule Pleroma.Activity do
|> Repo.all()
end
- def follow_requests_for_actor(%Pleroma.User{ap_id: ap_id}) do
+ def follow_requests_for_actor(%User{ap_id: ap_id}) do
ap_id
|> Queries.by_object_id()
|> Queries.by_type("Follow")
|> where([a], fragment("? ->> 'state' = 'pending'", a.data))
end
- def following_requests_for_actor(%Pleroma.User{ap_id: ap_id}) do
+ def following_requests_for_actor(%User{ap_id: ap_id}) do
Queries.by_type("Follow")
|> where([a], fragment("?->>'state' = 'pending'", a.data))
|> where([a], a.actor == ^ap_id)
@@ -340,4 +350,21 @@ 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
+
+ @spec get_by_object_ap_id_with_object(String.t()) :: t() | nil
+ def get_by_object_ap_id_with_object(ap_id) when is_binary(ap_id) do
+ ap_id
+ |> Queries.by_object_id()
+ |> with_preloaded_object()
+ |> first()
+ |> Repo.one()
+ end
+
+ def get_by_object_ap_id_with_object(_), do: nil
end
diff --git a/lib/pleroma/activity/ir/topics.ex b/lib/pleroma/activity/ir/topics.ex
index 9e65bedad..fe2e8cb5c 100644
--- a/lib/pleroma/activity/ir/topics.ex
+++ b/lib/pleroma/activity/ir/topics.ex
@@ -40,7 +40,8 @@ defmodule Pleroma.Activity.Ir.Topics do
end
defp item_creation_tags(tags, object, %{data: %{"type" => "Create"}} = activity) do
- tags ++ hashtags_to_topics(object) ++ attachment_topics(object, activity)
+ tags ++
+ remote_topics(activity) ++ hashtags_to_topics(object) ++ attachment_topics(object, activity)
end
defp item_creation_tags(tags, _, _) do
@@ -55,9 +56,19 @@ defmodule Pleroma.Activity.Ir.Topics do
defp hashtags_to_topics(_), do: []
+ defp remote_topics(%{local: true}), do: []
+
+ defp remote_topics(%{actor: actor}) when is_binary(actor),
+ do: ["public:remote:" <> URI.parse(actor).host]
+
+ defp remote_topics(_), do: []
+
defp attachment_topics(%{data: %{"attachment" => []}}, _act), do: []
defp attachment_topics(_object, %{local: true}), do: ["public:media", "public:local:media"]
+ defp attachment_topics(_object, %{actor: actor}) when is_binary(actor),
+ do: ["public:media", "public:remote:media:" <> URI.parse(actor).host]
+
defp attachment_topics(_object, _act), do: ["public:media"]
end
diff --git a/lib/pleroma/activity/search.ex b/lib/pleroma/activity/search.ex
index ceb365bb3..382c81118 100644
--- a/lib/pleroma/activity/search.ex
+++ b/lib/pleroma/activity/search.ex
@@ -27,7 +27,10 @@ defmodule Pleroma.Activity.Search do
|> maybe_restrict_local(user)
|> maybe_restrict_author(author)
|> maybe_restrict_blocked(user)
- |> Pagination.fetch_paginated(%{"offset" => offset, "limit" => limit}, :offset)
+ |> Pagination.fetch_paginated(
+ %{"offset" => offset, "limit" => limit, "skip_order" => index_type == :rum},
+ :offset
+ )
|> maybe_fetch(user, search_query)
end
diff --git a/lib/pleroma/activity_expiration.ex b/lib/pleroma/activity_expiration.ex
deleted file mode 100644
index db9c88d84..000000000
--- a/lib/pleroma/activity_expiration.ex
+++ /dev/null
@@ -1,67 +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.ActivityExpiration do
- use Ecto.Schema
-
- alias Pleroma.Activity
- alias Pleroma.ActivityExpiration
- alias Pleroma.Repo
-
- import Ecto.Changeset
- import Ecto.Query
-
- @type t :: %__MODULE__{}
- @min_activity_lifetime :timer.hours(1)
-
- schema "activity_expirations" do
- belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
- field(:scheduled_at, :naive_datetime)
- end
-
- def changeset(%ActivityExpiration{} = expiration, attrs) do
- expiration
- |> cast(attrs, [:scheduled_at])
- |> validate_required([:scheduled_at])
- |> validate_scheduled_at()
- end
-
- def get_by_activity_id(activity_id) do
- ActivityExpiration
- |> where([exp], exp.activity_id == ^activity_id)
- |> Repo.one()
- end
-
- def create(%Activity{} = activity, scheduled_at) do
- %ActivityExpiration{activity_id: activity.id}
- |> changeset(%{scheduled_at: scheduled_at})
- |> Repo.insert()
- end
-
- def due_expirations(offset \\ 0) do
- naive_datetime =
- NaiveDateTime.utc_now()
- |> NaiveDateTime.add(offset, :millisecond)
-
- ActivityExpiration
- |> where([exp], exp.scheduled_at < ^naive_datetime)
- |> Repo.all()
- end
-
- def validate_scheduled_at(changeset) 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"]
- else
- []
- end
- end)
- end
-
- def expires_late_enough?(scheduled_at) do
- now = NaiveDateTime.utc_now()
- diff = NaiveDateTime.diff(scheduled_at, now, :millisecond)
- diff > @min_activity_lifetime
- end
-end
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index 0ffb55358..ced14f87f 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -22,13 +22,18 @@ defmodule Pleroma.Application do
def repository, do: @repository
def user_agent do
- case Config.get([:http, :user_agent], :default) do
- :default ->
- info = "#{Pleroma.Web.base_url()} <#{Config.get([:instance, :email], "")}>"
- named_version() <> "; " <> info
-
- custom ->
- custom
+ if Process.whereis(Pleroma.Web.Endpoint) do
+ case Config.get([:http, :user_agent], :default) do
+ :default ->
+ info = "#{Pleroma.Web.base_url()} <#{Config.get([:instance, :email], "")}>"
+ named_version() <> "; " <> info
+
+ custom ->
+ custom
+ end
+ else
+ # fallback, if endpoint is not started yet
+ "Pleroma Data Loader"
end
end
@@ -39,15 +44,20 @@ defmodule Pleroma.Application do
# every time the application is restarted, so we disable module
# conflicts at runtime
Code.compiler_options(ignore_module_conflict: true)
+ # Disable warnings_as_errors at runtime, it breaks Phoenix live reload
+ # due to protocol consolidation warnings
+ Code.compiler_options(warnings_as_errors: false)
Pleroma.Telemetry.Logger.attach()
Config.Holder.save_default()
Pleroma.HTML.compile_scrubbers()
+ Pleroma.Config.Oban.warn()
Config.DeprecationWarnings.warn()
- Pleroma.Plugs.HTTPSecurityPlug.warn_if_disabled()
+ Pleroma.Web.Plugs.HTTPSecurityPlug.warn_if_disabled()
Pleroma.ApplicationRequirements.verify!()
setup_instrumenters()
load_custom_modules()
Pleroma.Docs.JSON.compile()
+ limiters_setup()
adapter = Application.get_env(:tesla, :adapter)
@@ -79,18 +89,19 @@ defmodule Pleroma.Application do
Pleroma.Repo,
Config.TransferTask,
Pleroma.Emoji,
- Pleroma.Plugs.RateLimiter.Supervisor
+ Pleroma.Web.Plugs.RateLimiter.Supervisor
] ++
cachex_children() ++
http_children(adapter, @env) ++
[
Pleroma.Stats,
Pleroma.JobQueueMonitor,
+ {Majic.Pool, [name: Pleroma.MajicPool, pool_size: Config.get([:majic_pool, :size], 2)]},
{Oban, Config.get(Oban)}
] ++
task_children(@env) ++
- streamer_child(@env) ++
- chat_child(@env, chat_enabled?()) ++
+ dont_run_in_test(@env) ++
+ chat_child(chat_enabled?()) ++
[
Pleroma.Web.Endpoint,
Pleroma.Gopher.Server
@@ -141,7 +152,10 @@ defmodule Pleroma.Application do
Pleroma.Web.Endpoint.MetricsExporter.setup()
Pleroma.Web.Endpoint.PipelineInstrumenter.setup()
- Pleroma.Web.Endpoint.Instrumenter.setup()
+
+ # Note: disabled until prometheus-phx is integrated into prometheus-phoenix:
+ # Pleroma.Web.Endpoint.Instrumenter.setup()
+ PrometheusPhx.setup()
end
defp cachex_children do
@@ -155,7 +169,11 @@ defmodule Pleroma.Application do
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("banned_urls", default_ttl: :timer.hours(24 * 30), limit: 5_000)
+ build_cachex("banned_urls", default_ttl: :timer.hours(24 * 30), limit: 5_000),
+ build_cachex("chat_message_id_idempotency_key",
+ expiration: chat_message_id_idempotency_key_expiration(),
+ limit: 500_000
+ )
]
end
@@ -165,6 +183,9 @@ defmodule Pleroma.Application do
defp idempotency_expiration,
do: expiration(default: :timer.seconds(6 * 60 * 60), interval: :timer.seconds(60))
+ defp chat_message_id_idempotency_key_expiration,
+ do: expiration(default: :timer.minutes(2), interval: :timer.seconds(60))
+
defp seconds_valid_interval,
do: :timer.seconds(Config.get!([Pleroma.Captcha, :seconds_valid]))
@@ -178,9 +199,9 @@ defmodule Pleroma.Application do
defp chat_enabled?, do: Config.get([:chat, :enabled])
- defp streamer_child(env) when env in [:test, :benchmark], do: []
+ defp dont_run_in_test(env) when env in [:test, :benchmark], do: []
- defp streamer_child(_) do
+ defp dont_run_in_test(_) do
[
{Registry,
[
@@ -191,11 +212,14 @@ defmodule Pleroma.Application do
]
end
- defp chat_child(_env, true) do
- [Pleroma.Web.ChatChannel.ChatChannelState]
+ defp chat_child(true) do
+ [
+ Pleroma.Web.ChatChannel.ChatChannelState,
+ {Phoenix.PubSub, [name: Pleroma.PubSub, adapter: Phoenix.PubSub.PG2]}
+ ]
end
- defp chat_child(_, _), do: []
+ defp chat_child(_), do: []
defp task_children(:test) do
[
@@ -249,4 +273,10 @@ defmodule Pleroma.Application do
end
defp http_children(_, _), do: []
+
+ @spec limiters_setup() :: :ok
+ def limiters_setup do
+ [Pleroma.Web.RichMedia.Helpers, Pleroma.Web.MediaProxy]
+ |> Enum.each(&ConcurrentLimiter.new(&1, 1, 0))
+ end
end
diff --git a/lib/pleroma/application_requirements.ex b/lib/pleroma/application_requirements.ex
index 16f62b6f5..b977257a3 100644
--- a/lib/pleroma/application_requirements.ex
+++ b/lib/pleroma/application_requirements.ex
@@ -9,6 +9,9 @@ defmodule Pleroma.ApplicationRequirements do
defmodule VerifyError, do: defexception([:message])
+ alias Pleroma.Config
+ alias Pleroma.Helpers.MediaHelper
+
import Ecto.Query
require Logger
@@ -16,7 +19,8 @@ defmodule Pleroma.ApplicationRequirements do
@spec verify!() :: :ok | VerifyError.t()
def verify! do
:ok
- |> check_confirmation_accounts!
+ |> check_system_commands!()
+ |> check_confirmation_accounts!()
|> check_migrations_applied!()
|> check_welcome_message_config!()
|> check_rum!()
@@ -48,7 +52,9 @@ defmodule Pleroma.ApplicationRequirements 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."
+ "Account activation enabled, but no Mailer settings enabled.\n" <>
+ "Please set config :pleroma, :instance, account_activation_required: false\n" <>
+ "Otherwise setup and enable Mailer."
)
{:error,
@@ -81,7 +87,9 @@ defmodule Pleroma.ApplicationRequirements do
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"
+ "The following migrations were not applied:\n#{down_migrations_text}" <>
+ "If you want to start Pleroma anyway, set\n" <>
+ "config :pleroma, :i_am_aware_this_may_cause_data_loss, disable_migration_check: true"
)
{:error, "Unapplied Migrations detected"}
@@ -124,14 +132,22 @@ defmodule Pleroma.ApplicationRequirements 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/`"
+ "Use `RUM` index is enabled, but were not applied migrations for it.\n" <>
+ "If you want to start Pleroma anyway, set\n" <>
+ "config :pleroma, :database, rum_enabled: false\n" <>
+ "Otherwise 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/`"
+ "Detected applied migrations to use `RUM` index, but `RUM` isn't enable in settings.\n" <>
+ "If you want to use `RUM`, set\n" <>
+ "config :pleroma, :database, rum_enabled: true\n" <>
+ "Otherwise roll `RUM` migrations back.\n" <>
+ "`mix ecto.rollback --migrations-path priv/repo/optional_migrations/rum_indexing/`"
)
{:error, "RUM Migrations detected"}
@@ -140,4 +156,50 @@ defmodule Pleroma.ApplicationRequirements do
:ok
end
end
+
+ defp check_system_commands!(:ok) do
+ filter_commands_statuses = [
+ check_filter(Pleroma.Upload.Filters.Exiftool, "exiftool"),
+ check_filter(Pleroma.Upload.Filters.Mogrify, "mogrify"),
+ check_filter(Pleroma.Upload.Filters.Mogrifun, "mogrify")
+ ]
+
+ preview_proxy_commands_status =
+ if !Config.get([:media_preview_proxy, :enabled]) or
+ MediaHelper.missing_dependencies() == [] do
+ true
+ else
+ Logger.error(
+ "The following dependencies required by Media preview proxy " <>
+ "(which is currently enabled) are not installed: " <>
+ inspect(MediaHelper.missing_dependencies())
+ )
+
+ false
+ end
+
+ if Enum.all?([preview_proxy_commands_status | filter_commands_statuses], & &1) do
+ :ok
+ else
+ {:error,
+ "System commands missing. Check logs and see `docs/installation` for more details."}
+ end
+ end
+
+ defp check_system_commands!(result), do: result
+
+ defp check_filter(filter, command_required) do
+ filters = Config.get([Pleroma.Upload, :filters])
+
+ if filter in filters and not 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"
+ )
+
+ false
+ else
+ true
+ end
+ end
end
diff --git a/lib/pleroma/bbs/authenticator.ex b/lib/pleroma/bbs/authenticator.ex
index 815de7002..83ebb756d 100644
--- a/lib/pleroma/bbs/authenticator.ex
+++ b/lib/pleroma/bbs/authenticator.ex
@@ -4,8 +4,8 @@
defmodule Pleroma.BBS.Authenticator do
use Sshd.PasswordAuthenticator
- alias Pleroma.Plugs.AuthenticationPlug
alias Pleroma.User
+ alias Pleroma.Web.Plugs.AuthenticationPlug
def authenticate(username, password) do
username = to_string(username)
diff --git a/lib/pleroma/captcha/captcha.ex b/lib/pleroma/captcha.ex
index 6ab754b6f..6ab754b6f 100644
--- a/lib/pleroma/captcha/captcha.ex
+++ b/lib/pleroma/captcha.ex
diff --git a/lib/pleroma/captcha/kocaptcha.ex b/lib/pleroma/captcha/kocaptcha.ex
index 6bc2fa158..201b55ab4 100644
--- a/lib/pleroma/captcha/kocaptcha.ex
+++ b/lib/pleroma/captcha/kocaptcha.ex
@@ -10,7 +10,7 @@ defmodule Pleroma.Captcha.Kocaptcha do
def new do
endpoint = Pleroma.Config.get!([__MODULE__, :endpoint])
- case Tesla.get(endpoint <> "/new") do
+ case Pleroma.HTTP.get(endpoint <> "/new") do
{:error, _} ->
%{error: :kocaptcha_service_unavailable}
@@ -21,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
diff --git a/lib/pleroma/captcha/native.ex b/lib/pleroma/captcha/native.ex
index a90631d61..8d604d2b2 100644
--- a/lib/pleroma/captcha/native.ex
+++ b/lib/pleroma/captcha/native.ex
@@ -17,7 +17,8 @@ defmodule Pleroma.Captcha.Native do
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
diff --git a/lib/pleroma/captcha/captcha_service.ex b/lib/pleroma/captcha/service.ex
index 959038cef..959038cef 100644
--- a/lib/pleroma/captcha/captcha_service.ex
+++ b/lib/pleroma/captcha/service.ex
diff --git a/lib/pleroma/chat.ex b/lib/pleroma/chat.ex
index 24a86371e..28007cd9f 100644
--- a/lib/pleroma/chat.ex
+++ b/lib/pleroma/chat.ex
@@ -6,7 +6,9 @@ defmodule Pleroma.Chat do
use Ecto.Schema
import Ecto.Changeset
+ import Ecto.Query
+ alias Pleroma.Chat
alias Pleroma.Repo
alias Pleroma.User
@@ -16,6 +18,7 @@ defmodule Pleroma.Chat do
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.
"""
+ @type t :: %__MODULE__{}
@primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
schema "chats" do
@@ -39,16 +42,28 @@ defmodule Pleroma.Chat do
|> unique_constraint(:user_id, name: :chats_user_id_recipient_index)
end
+ @spec get_by_user_and_id(User.t(), FlakeId.Ecto.CompatType.t()) ::
+ {:ok, t()} | {:error, :not_found}
+ def get_by_user_and_id(%User{id: user_id}, id) do
+ from(c in __MODULE__,
+ where: c.id == ^id,
+ where: c.user_id == ^user_id
+ )
+ |> Repo.find_resource()
+ end
+
+ @spec get_by_id(FlakeId.Ecto.CompatType.t()) :: t() | nil
def get_by_id(id) do
- __MODULE__
- |> Repo.get(id)
+ Repo.get(__MODULE__, id)
end
+ @spec get(FlakeId.Ecto.CompatType.t(), String.t()) :: t() | nil
def get(user_id, recipient) do
- __MODULE__
- |> Repo.get_by(user_id: user_id, recipient: recipient)
+ Repo.get_by(__MODULE__, user_id: user_id, recipient: recipient)
end
+ @spec get_or_create(FlakeId.Ecto.CompatType.t(), String.t()) ::
+ {:ok, t()} | {:error, Ecto.Changeset.t()}
def get_or_create(user_id, recipient) do
%__MODULE__{}
|> changeset(%{user_id: user_id, recipient: recipient})
@@ -60,6 +75,8 @@ defmodule Pleroma.Chat do
)
end
+ @spec bump_or_create(FlakeId.Ecto.CompatType.t(), String.t()) ::
+ {:ok, t()} | {:error, Ecto.Changeset.t()}
def bump_or_create(user_id, recipient) do
%__MODULE__{}
|> changeset(%{user_id: user_id, recipient: recipient})
@@ -69,4 +86,12 @@ defmodule Pleroma.Chat do
conflict_target: [:user_id, :recipient]
)
end
+
+ @spec for_user_query(FlakeId.Ecto.CompatType.t()) :: Ecto.Query.t()
+ def for_user_query(user_id) do
+ from(c in Chat,
+ where: c.user_id == ^user_id,
+ order_by: [desc: c.updated_at]
+ )
+ 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/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex
index 0f52eb210..59c6b0f58 100644
--- a/lib/pleroma/config/deprecation_warnings.ex
+++ b/lib/pleroma/config/deprecation_warnings.ex
@@ -8,7 +8,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
require Logger
alias Pleroma.Config
- @type config_namespace() :: [atom()]
+ @type config_namespace() :: atom() | [atom()]
@type config_map() :: {config_namespace(), config_namespace(), String.t()}
@mrf_config_map [
@@ -26,36 +26,26 @@ defmodule Pleroma.Config.DeprecationWarnings do
!!!DEPRECATION WARNING!!!
You are using the old configuration mechanism for the hellthread filter. Please check config.md.
""")
- end
- end
-
- def mrf_user_allowlist do
- config = Config.get(:mrf_user_allowlist)
-
- if config && Enum.any?(config, fn {k, _} -> is_atom(k) end) do
- rewritten =
- Enum.reduce(Config.get(:mrf_user_allowlist), Map.new(), fn {k, v}, acc ->
- Map.put(acc, to_string(k), v)
- end)
-
- Config.put(:mrf_user_allowlist, rewritten)
- Logger.error("""
- !!!DEPRECATION WARNING!!!
- As of Pleroma 2.0.7, the `mrf_user_allowlist` setting changed of format.
- Pleroma 2.1 will remove support for the old format. Please change your configuration to match this:
-
- config :pleroma, :mrf_user_allowlist, #{inspect(rewritten, pretty: true)}
- """)
+ :error
+ else
+ :ok
end
end
def warn do
- check_hellthread_threshold()
- mrf_user_allowlist()
- check_old_mrf_config()
- check_media_proxy_whitelist_config()
- check_welcome_message_config()
+ with :ok <- check_hellthread_threshold(),
+ :ok <- check_old_mrf_config(),
+ :ok <- check_media_proxy_whitelist_config(),
+ :ok <- check_welcome_message_config(),
+ :ok <- check_gun_pool_options(),
+ :ok <- check_activity_expiration_config(),
+ :ok <- check_remote_ip_plug_name() do
+ :ok
+ else
+ _ ->
+ :error
+ end
end
def check_welcome_message_config do
@@ -68,10 +58,14 @@ defmodule Pleroma.Config.DeprecationWarnings do
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`
+ Your config is using the old namespace for Welcome messages configuration. You need to convert to the new namespace. e.g.,
+ \n* `config :pleroma, :instance, welcome_user_nickname` and `config :pleroma, :instance, welcome_message` are now equal to:
+ \n* `config :pleroma, :welcome, direct_message: [enabled: true, sender_nickname: "NICKNAME", message: "Your welcome message"]`"
""")
+
+ :error
+ else
+ :ok
end
end
@@ -99,8 +93,11 @@ defmodule Pleroma.Config.DeprecationWarnings do
end
end)
- if warning != "" do
+ if warning == "" do
+ :ok
+ else
Logger.warn(warning_preface <> warning)
+ :error
end
end
@@ -113,6 +110,87 @@ defmodule Pleroma.Config.DeprecationWarnings do
!!!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.
""")
+
+ :error
+ else
+ :ok
end
end
+
+ def check_gun_pool_options do
+ pool_config = Config.get(:connections_pool)
+
+ if timeout = pool_config[:await_up_timeout] do
+ Logger.warn("""
+ !!!DEPRECATION WARNING!!!
+ Your config is using old setting `config :pleroma, :connections_pool, await_up_timeout`. Please change to `config :pleroma, :connections_pool, connect_timeout` to ensure compatibility with future releases.
+ """)
+
+ Config.put(:connections_pool, Keyword.put_new(pool_config, :connect_timeout, timeout))
+ end
+
+ pools_configs = Config.get(:pools)
+
+ warning_preface = """
+ !!!DEPRECATION WARNING!!!
+ Your config is using old setting name `timeout` instead of `recv_timeout` in pool settings. Setting should work for now, but you are advised to change format to scheme with port to prevent possible issues later.
+ """
+
+ updated_config =
+ Enum.reduce(pools_configs, [], fn {pool_name, config}, acc ->
+ if timeout = config[:timeout] do
+ Keyword.put(acc, pool_name, Keyword.put_new(config, :recv_timeout, timeout))
+ else
+ acc
+ end
+ end)
+
+ if updated_config != [] do
+ pool_warnings =
+ updated_config
+ |> Keyword.keys()
+ |> Enum.map(fn pool_name ->
+ "\n* `:timeout` options in #{pool_name} pool is now `:recv_timeout`"
+ end)
+
+ Logger.warn(Enum.join([warning_preface | pool_warnings]))
+
+ Config.put(:pools, updated_config)
+ :error
+ else
+ :ok
+ end
+ end
+
+ @spec check_activity_expiration_config() :: :ok | nil
+ def check_activity_expiration_config do
+ warning_preface = """
+ !!!DEPRECATION WARNING!!!
+ Your config is using old namespace for activity expiration configuration. Setting should work for now, but you are advised to change to new namespace to prevent possible issues later:
+ """
+
+ move_namespace_and_warn(
+ [
+ {Pleroma.ActivityExpiration, Pleroma.Workers.PurgeExpiredActivity,
+ "\n* `config :pleroma, Pleroma.ActivityExpiration` is now `config :pleroma, Pleroma.Workers.PurgeExpiredActivity`"}
+ ],
+ warning_preface
+ )
+ end
+
+ @spec check_remote_ip_plug_name() :: :ok | nil
+ def check_remote_ip_plug_name do
+ warning_preface = """
+ !!!DEPRECATION WARNING!!!
+ Your config is using old namespace for RemoteIp Plug. Setting should work for now, but you are advised to change to new namespace to prevent possible issues later:
+ """
+
+ move_namespace_and_warn(
+ [
+ {Pleroma.Plugs.RemoteIp, Pleroma.Web.Plugs.RemoteIp,
+ "\n* `config :pleroma, Pleroma.Plugs.RemoteIp` is now `config :pleroma, Pleroma.Web.Plugs.RemoteIp`"}
+ ],
+ warning_preface
+ )
+ end
end
diff --git a/lib/pleroma/config/oban.ex b/lib/pleroma/config/oban.ex
new file mode 100644
index 000000000..8e0351d52
--- /dev/null
+++ b/lib/pleroma/config/oban.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.Config.Oban do
+ require Logger
+
+ def warn do
+ oban_config = Pleroma.Config.get(Oban)
+
+ crontab =
+ [
+ Pleroma.Workers.Cron.StatsWorker,
+ Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker,
+ Pleroma.Workers.Cron.ClearOauthTokenWorker
+ ]
+ |> Enum.reduce(oban_config[:crontab], fn removed_worker, acc ->
+ with acc when is_list(acc) <- acc,
+ setting when is_tuple(setting) <-
+ Enum.find(acc, fn {_, worker} -> worker == removed_worker end) do
+ """
+ !!!OBAN CONFIG WARNING!!!
+ You are using old workers in Oban crontab settings, which were removed.
+ Please, remove setting from crontab in your config file (prod.secret.exs): #{
+ inspect(setting)
+ }
+ """
+ |> Logger.warn()
+
+ List.delete(acc, setting)
+ else
+ _ -> acc
+ end
+ end)
+
+ Pleroma.Config.put(Oban, Keyword.put(oban_config, :crontab, crontab))
+ end
+end
diff --git a/lib/pleroma/config/config_db.ex b/lib/pleroma/config_db.ex
index e5b7811aa..e5b7811aa 100644
--- a/lib/pleroma/config/config_db.ex
+++ b/lib/pleroma/config_db.ex
diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex
index 13eeaa96b..cf8182d55 100644
--- a/lib/pleroma/constants.ex
+++ b/lib/pleroma/constants.ex
@@ -26,4 +26,6 @@ defmodule Pleroma.Constants do
do:
~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc embed.js embed.css)
)
+
+ def as_local_public, do: Pleroma.Web.base_url() <> "/#Public"
end
diff --git a/lib/pleroma/conversation.ex b/lib/pleroma/conversation.ex
index e76eb0087..77933f0be 100644
--- a/lib/pleroma/conversation.ex
+++ b/lib/pleroma/conversation.ex
@@ -43,7 +43,7 @@ defmodule Pleroma.Conversation do
def maybe_create_recipientships(participation, activity) do
participation = Repo.preload(participation, :recipients)
- if participation.recipients |> Enum.empty?() do
+ if Enum.empty?(participation.recipients) do
recipients = User.get_all_by_ap_id(activity.recipients)
RecipientShip.create(recipients, participation)
end
@@ -69,10 +69,6 @@ defmodule Pleroma.Conversation do
Enum.map(users, fn user ->
invisible_conversation = Enum.any?(users, &User.blocks?(user, &1))
- unless invisible_conversation do
- User.increment_unread_conversation_count(conversation, user)
- end
-
opts = Keyword.put(opts, :invisible_conversation, invisible_conversation)
{:ok, participation} =
diff --git a/lib/pleroma/conversation/participation.ex b/lib/pleroma/conversation/participation.ex
index 8bc3e85d6..4c32b273a 100644
--- a/lib/pleroma/conversation/participation.ex
+++ b/lib/pleroma/conversation/participation.ex
@@ -63,21 +63,10 @@ defmodule Pleroma.Conversation.Participation do
end
end
- def mark_as_read(participation) do
- __MODULE__
- |> where(id: ^participation.id)
- |> update(set: [read: true])
- |> select([p], p)
- |> Repo.update_all([])
- |> case do
- {1, [participation]} ->
- participation = Repo.preload(participation, :user)
- User.set_unread_conversation_count(participation.user)
- {:ok, participation}
-
- error ->
- error
- end
+ def mark_as_read(%__MODULE__{} = participation) do
+ participation
+ |> change(read: true)
+ |> Repo.update()
end
def mark_all_as_read(%User{local: true} = user, %User{} = target_user) do
@@ -93,7 +82,6 @@ defmodule Pleroma.Conversation.Participation do
|> update([p], set: [read: true])
|> Repo.update_all([])
- {:ok, user} = User.set_unread_conversation_count(user)
{:ok, user, []}
end
@@ -108,7 +96,6 @@ defmodule Pleroma.Conversation.Participation do
|> select([p], p)
|> Repo.update_all([])
- {:ok, user} = User.set_unread_conversation_count(user)
{:ok, user, participations}
end
@@ -220,6 +207,12 @@ defmodule Pleroma.Conversation.Participation do
{:ok, Repo.preload(participation, :recipients, force: true)}
end
+ @spec unread_count(User.t()) :: integer()
+ def unread_count(%User{id: user_id}) do
+ from(q in __MODULE__, where: q.user_id == ^user_id and q.read == false)
+ |> Repo.aggregate(:count, :id)
+ end
+
def unread_conversation_count_for_user(user) do
from(p in __MODULE__,
where: p.user_id == ^user.id,
diff --git a/lib/pleroma/conversation/participation_recipient_ship.ex b/lib/pleroma/conversation/participation/recipient_ship.ex
index de40bacac..de40bacac 100644
--- a/lib/pleroma/conversation/participation_recipient_ship.ex
+++ b/lib/pleroma/conversation/participation/recipient_ship.ex
diff --git a/lib/pleroma/docs/generator.ex b/lib/pleroma/docs/generator.ex
index a671a6278..a70f83b73 100644
--- a/lib/pleroma/docs/generator.ex
+++ b/lib/pleroma/docs/generator.ex
@@ -1,3 +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.Docs.Generator do
@callback process(keyword()) :: {:ok, String.t()}
diff --git a/lib/pleroma/docs/json.ex b/lib/pleroma/docs/json.ex
index feeb4320e..a583e2a5b 100644
--- a/lib/pleroma/docs/json.ex
+++ b/lib/pleroma/docs/json.ex
@@ -1,3 +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.Docs.JSON do
@behaviour Pleroma.Docs.Generator
@external_resource "config/description.exs"
@@ -7,7 +11,11 @@ defmodule Pleroma.Docs.JSON do
@spec compile :: :ok
def compile do
- :persistent_term.put(@term, Pleroma.Docs.Generator.convert_to_strings(@raw_descriptions))
+ descriptions =
+ Pleroma.Web.ActivityPub.MRF.config_descriptions()
+ |> Enum.reduce(@raw_descriptions, fn description, acc -> [description | acc] end)
+
+ :persistent_term.put(@term, Pleroma.Docs.Generator.convert_to_strings(descriptions))
end
@spec compiled_descriptions :: Map.t()
diff --git a/lib/pleroma/docs/markdown.ex b/lib/pleroma/docs/markdown.ex
index da3f20f43..eac0789a6 100644
--- a/lib/pleroma/docs/markdown.ex
+++ b/lib/pleroma/docs/markdown.ex
@@ -1,3 +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.Docs.Markdown do
@behaviour Pleroma.Docs.Generator
diff --git a/lib/pleroma/ecto_type/activity_pub/object_validators/emoji.ex b/lib/pleroma/ecto_type/activity_pub/object_validators/emoji.ex
new file mode 100644
index 000000000..4aacc5c88
--- /dev/null
+++ b/lib/pleroma/ecto_type/activity_pub/object_validators/emoji.ex
@@ -0,0 +1,34 @@
+# 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.Emoji do
+ use Ecto.Type
+
+ def type, do: :map
+
+ def cast(data) when is_map(data) do
+ has_invalid_emoji? =
+ Enum.find(data, fn
+ {name, uri} when is_binary(name) and is_binary(uri) ->
+ # based on ObjectValidators.Uri.cast()
+ case URI.parse(uri) do
+ %URI{host: nil} -> true
+ %URI{host: ""} -> true
+ %URI{scheme: scheme} when scheme in ["https", "http"] -> false
+ _ -> true
+ end
+
+ {_name, _uri} ->
+ true
+ end)
+
+ if has_invalid_emoji?, do: :error, else: {:ok, data}
+ end
+
+ def cast(_data), do: :error
+
+ def dump(data), do: {:ok, data}
+
+ def load(data), do: {:ok, data}
+end
diff --git a/lib/pleroma/emails/admin_email.ex b/lib/pleroma/emails/admin_email.ex
index aa0b2a66b..d5757c12a 100644
--- a/lib/pleroma/emails/admin_email.ex
+++ b/lib/pleroma/emails/admin_email.ex
@@ -8,6 +8,7 @@ defmodule Pleroma.Emails.AdminEmail do
import Swoosh.Email
alias Pleroma.Config
+ alias Pleroma.HTML
alias Pleroma.Web.Router.Helpers
defp instance_config, do: Config.get(:instance)
@@ -17,10 +18,6 @@ defmodule Pleroma.Emails.AdminEmail do
Keyword.get(instance_config(), :notify_email, instance_config()[:email])
end
- defp user_url(user) do
- Helpers.user_feed_url(Pleroma.Web.Endpoint, :feed_redirect, user.id)
- end
-
def test_email(mail_to \\ nil) do
html_body = """
<h3>Instance Test Email</h3>
@@ -51,6 +48,9 @@ defmodule Pleroma.Emails.AdminEmail do
status_url = Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, id)
"<li><a href=\"#{status_url}\">#{status_url}</li>"
+ %{"id" => id} when is_binary(id) ->
+ "<li><a href=\"#{id}\">#{id}</li>"
+
id when is_binary(id) ->
"<li><a href=\"#{id}\">#{id}</li>"
end)
@@ -68,8 +68,8 @@ defmodule Pleroma.Emails.AdminEmail do
end
html_body = """
- <p>Reported by: <a href="#{user_url(reporter)}">#{reporter.nickname}</a></p>
- <p>Reported Account: <a href="#{user_url(account)}">#{account.nickname}</a></p>
+ <p>Reported by: <a href="#{reporter.ap_id}">#{reporter.nickname}</a></p>
+ <p>Reported Account: <a href="#{account.ap_id}">#{account.nickname}</a></p>
#{comment_html}
#{statuses_html}
<p>
@@ -82,4 +82,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="#{account.ap_id}">@#{account.nickname}</a></p>
+ <blockquote>#{HTML.strip_tags(account.registration_reason)}</blockquote>
+ <a href="#{Pleroma.Web.base_url()}/pleroma/admin/#/users/#{account.id}/">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/mailer.ex b/lib/pleroma/emails/mailer.ex
index 8b1bdef75..5108c71c8 100644
--- a/lib/pleroma/emails/mailer.ex
+++ b/lib/pleroma/emails/mailer.ex
@@ -35,6 +35,11 @@ defmodule Pleroma.Emails.Mailer do
def deliver(email, config \\ [])
def deliver(email, config) do
+ # temporary hackney fix until hackney max_connections bug is fixed
+ # https://git.pleroma.social/pleroma/pleroma/-/issues/2101
+ email =
+ Swoosh.Email.put_private(email, :hackney_options, ssl_options: [versions: [:"tlsv1.2"]])
+
case enabled?() do
true -> Swoosh.Mailer.deliver(email, parse_config(config))
false -> {:error, :deliveries_disabled}
diff --git a/lib/pleroma/emails/user_email.ex b/lib/pleroma/emails/user_email.ex
index 313533859..806a61fd2 100644
--- a/lib/pleroma/emails/user_email.ex
+++ b/lib/pleroma/emails/user_email.ex
@@ -107,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])
@@ -180,4 +189,30 @@ defmodule Pleroma.Emails.UserEmail do
Router.Helpers.subscription_url(Endpoint, :unsubscribe, token)
end
+
+ def backup_is_ready_email(backup, admin_user_id \\ nil) do
+ %{user: user} = Pleroma.Repo.preload(backup, :user)
+ download_url = Pleroma.Web.PleromaAPI.BackupView.download_url(backup)
+
+ html_body =
+ if is_nil(admin_user_id) do
+ """
+ <p>You requested a full backup of your Pleroma account. It's ready for download:</p>
+ <p><a href="#{download_url}">#{download_url}</a></p>
+ """
+ else
+ admin = Pleroma.Repo.get(User, admin_user_id)
+
+ """
+ <p>Admin @#{admin.nickname} requested a full backup of your Pleroma account. It's ready for download:</p>
+ <p><a href="#{download_url}">#{download_url}</a></p>
+ """
+ end
+
+ new()
+ |> to(recipient(user))
+ |> from(sender())
+ |> subject("Your account archive is ready")
+ |> html_body(html_body)
+ end
end
diff --git a/lib/pleroma/emoji.ex b/lib/pleroma/emoji.ex
index f6016d73f..04936155b 100644
--- a/lib/pleroma/emoji.ex
+++ b/lib/pleroma/emoji.ex
@@ -56,6 +56,9 @@ defmodule Pleroma.Emoji do
end
end
+ @spec exist?(String.t()) :: boolean()
+ def exist?(name), do: not is_nil(get(name))
+
@doc "Returns all the emojos!!"
@spec get_all() :: list({String.t(), String.t(), String.t()})
def get_all do
diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex
index d076ae312..ca58e5432 100644
--- a/lib/pleroma/emoji/pack.ex
+++ b/lib/pleroma/emoji/pack.ex
@@ -1,3 +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.Emoji.Pack do
@derive {Jason.Encoder, only: [:files, :pack, :files_count]}
defstruct files: %{},
@@ -17,6 +21,7 @@ defmodule Pleroma.Emoji.Pack do
}
alias Pleroma.Emoji
+ alias Pleroma.Emoji.Pack
@spec create(String.t()) :: {:ok, t()} | {:error, File.posix()} | {:error, :empty_values}
def create(name) do
@@ -64,24 +69,93 @@ defmodule Pleroma.Emoji.Pack do
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]),
+ @spec unpack_zip_emojies(list(tuple())) :: list(map())
+ defp unpack_zip_emojies(zip_files) do
+ Enum.reduce(zip_files, [], fn
+ {_, path, s, _, _, _}, acc when elem(s, 2) == :regular ->
+ with(
+ filename <- Path.basename(path),
+ shortcode <- Path.basename(filename, Path.extname(filename)),
+ false <- Emoji.exist?(shortcode)
+ ) do
+ [%{path: path, filename: path, shortcode: shortcode} | acc]
+ else
+ _ -> acc
+ end
+
+ _, acc ->
+ acc
+ end)
+ end
+
+ @spec add_file(t(), String.t(), Path.t(), Plug.Upload.t()) ::
+ {:ok, t()}
+ | {:error, File.posix() | atom()}
+ def add_file(%Pack{} = pack, _, _, %Plug.Upload{content_type: "application/zip"} = file) do
+ with {:ok, zip_files} <- :zip.table(to_charlist(file.path)),
+ [_ | _] = emojies <- unpack_zip_emojies(zip_files),
+ {:ok, tmp_dir} <- Pleroma.Utils.tmp_dir("emoji") do
+ try do
+ {:ok, _emoji_files} =
+ :zip.unzip(
+ to_charlist(file.path),
+ [{:file_list, Enum.map(emojies, & &1[:path])}, {:cwd, tmp_dir}]
+ )
+
+ {_, updated_pack} =
+ Enum.map_reduce(emojies, pack, fn item, emoji_pack ->
+ emoji_file = %Plug.Upload{
+ filename: item[:filename],
+ path: Path.join(tmp_dir, item[:path])
+ }
+
+ {:ok, updated_pack} =
+ do_add_file(
+ emoji_pack,
+ item[:shortcode],
+ to_string(item[:filename]),
+ emoji_file
+ )
+
+ {item, updated_pack}
+ end)
+
+ Emoji.reload()
+
+ {:ok, updated_pack}
+ after
+ File.rm_rf(tmp_dir)
+ end
+ else
+ {:error, _} = error ->
+ error
+
+ _ ->
+ {:ok, pack}
+ end
+ end
+
+ def add_file(%Pack{} = pack, shortcode, filename, %Plug.Upload{} = file) do
+ with :ok <- validate_not_empty([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
+ {:ok, updated_pack} <- do_add_file(pack, shortcode, filename, file) do
Emoji.reload()
{:ok, updated_pack}
end
end
- @spec delete_file(String.t(), String.t()) ::
+ defp do_add_file(pack, shortcode, filename, file) do
+ with :ok <- save_file(file, pack, filename) do
+ pack
+ |> put_emoji(shortcode, filename)
+ |> save_pack()
+ end
+ end
+
+ @spec delete_file(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),
+ def delete_file(%Pack{} = pack, shortcode) do
+ with :ok <- validate_not_empty([shortcode]),
:ok <- remove_file(pack, shortcode),
{:ok, updated_pack} <- pack |> delete_emoji(shortcode) |> save_pack() do
Emoji.reload()
@@ -89,11 +163,10 @@ defmodule Pleroma.Emoji.Pack do
end
end
- @spec update_file(String.t(), String.t(), String.t(), String.t(), boolean()) ::
+ @spec update_file(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),
+ def update_file(%Pack{} = pack, shortcode, new_shortcode, new_filename, force) do
+ with :ok <- validate_not_empty([shortcode, new_shortcode, new_filename]),
{:ok, filename} <- get_filename(pack, shortcode),
:ok <- validate_emoji_not_exists(new_shortcode, force),
:ok <- rename_file(pack, filename, new_filename),
@@ -129,13 +202,13 @@ defmodule Pleroma.Emoji.Pack do
end
end
- @spec list_remote(String.t()) :: {:ok, map()} | {:error, atom()}
- def list_remote(url) do
- uri = url |> String.trim() |> URI.parse()
+ @spec list_remote(keyword()) :: {:ok, map()} | {:error, atom()}
+ def list_remote(opts) do
+ uri = opts[:url] |> String.trim() |> URI.parse()
with :ok <- validate_shareable_packs_available(uri) do
uri
- |> URI.merge("/api/pleroma/emoji/packs")
+ |> URI.merge("/api/pleroma/emoji/packs?page=#{opts[:page]}&page_size=#{opts[:page_size]}")
|> http_get()
end
end
@@ -175,7 +248,8 @@ defmodule Pleroma.Emoji.Pack 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, remote_pack} <-
+ uri |> URI.merge("/api/pleroma/emoji/pack?name=#{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),
@@ -243,9 +317,10 @@ defmodule Pleroma.Emoji.Pack do
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}
+ if Emoji.exist?(shortcode) do
+ {:error, :already_exists}
+ else
+ :ok
end
end
@@ -386,25 +461,18 @@ defmodule Pleroma.Emoji.Pack do
end
end
- defp save_file(file, pack, filename) do
+ defp save_file(%Plug.Upload{path: upload_path}, 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)
+ with {:ok, _} <- File.copy(upload_path, file_path) do
+ :ok
end
end
defp put_emoji(pack, shortcode, filename) do
files = Map.put(pack.files, shortcode, filename)
- %{pack | files: files}
+ %{pack | files: files, files_count: length(Map.keys(files))}
end
defp delete_emoji(pack, shortcode) do
@@ -460,7 +528,7 @@ defmodule Pleroma.Emoji.Pack do
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
+ with {:ok, %{body: body}} <- Pleroma.HTTP.get(url, [], pool: :default) do
Jason.decode(body)
end
end
@@ -509,7 +577,7 @@ defmodule Pleroma.Emoji.Pack do
{:ok,
%{
sha: sha,
- url: URI.merge(uri, "/api/pleroma/emoji/packs/#{name}/archive") |> to_string()
+ url: URI.merge(uri, "/api/pleroma/emoji/packs/archive?name=#{name}") |> to_string()
}}
%{"fallback-src" => src, "fallback-src-sha256" => sha} when is_binary(src) ->
@@ -526,7 +594,7 @@ defmodule Pleroma.Emoji.Pack do
end
defp download_archive(url, sha) do
- with {:ok, %{body: archive}} <- Tesla.get(url) do
+ with {:ok, %{body: archive}} <- Pleroma.HTTP.get(url) do
if Base.decode16!(sha) == :crypto.hash(:sha256, archive) do
{:ok, archive}
else
@@ -549,7 +617,7 @@ defmodule Pleroma.Emoji.Pack do
end
defp update_sha_and_save_metadata(pack, data) do
- with {:ok, %{body: zip}} <- Tesla.get(data[:"fallback-src"]),
+ with {:ok, %{body: zip}} <- Pleroma.HTTP.get(data[:"fallback-src"]),
:ok <- validate_has_all_files(pack, zip) do
fallback_sha = :sha256 |> :crypto.hash(zip) |> Base.encode16()
diff --git a/lib/pleroma/following_relationship.ex b/lib/pleroma/following_relationship.ex
index c2020d30a..2039a259d 100644
--- a/lib/pleroma/following_relationship.ex
+++ b/lib/pleroma/following_relationship.ex
@@ -95,7 +95,11 @@ defmodule Pleroma.FollowingRelationship do
|> where([r], r.state == ^:follow_accept)
end
- def followers_ap_ids(%User{} = user, from_ap_ids \\ nil) do
+ 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()
@@ -260,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/frontend.ex b/lib/pleroma/frontend.ex
new file mode 100644
index 000000000..bf935a728
--- /dev/null
+++ b/lib/pleroma/frontend.ex
@@ -0,0 +1,110 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Frontend do
+ alias Pleroma.Config
+
+ require Logger
+
+ def install(name, opts \\ []) do
+ frontend_info = %{
+ "ref" => opts[:ref],
+ "build_url" => opts[:build_url],
+ "build_dir" => opts[:build_dir]
+ }
+
+ frontend_info =
+ [:frontends, :available, name]
+ |> Config.get(%{})
+ |> Map.merge(frontend_info, fn _key, config, cmd ->
+ # This only overrides things that are actually set
+ cmd || config
+ end)
+
+ ref = frontend_info["ref"]
+
+ unless ref do
+ raise "No ref given or configured"
+ end
+
+ dest = Path.join([dir(), name, ref])
+
+ label = "#{name} (#{ref})"
+ tmp_dir = Path.join(dir(), "tmp")
+
+ with {_, :ok} <-
+ {:download_or_unzip, download_or_unzip(frontend_info, tmp_dir, opts[:file])},
+ Logger.info("Installing #{label} to #{dest}"),
+ :ok <- install_frontend(frontend_info, tmp_dir, dest) do
+ File.rm_rf!(tmp_dir)
+ Logger.info("Frontend #{label} installed to #{dest}")
+ else
+ {:download_or_unzip, _} ->
+ Logger.info("Could not download or unzip the frontend")
+ {:error, "Could not download or unzip the frontend"}
+
+ _e ->
+ Logger.info("Could not install the frontend")
+ {:error, "Could not install the frontend"}
+ end
+ end
+
+ def dir(opts \\ []) do
+ if is_nil(opts[:static_dir]) do
+ Pleroma.Config.get!([:instance, :static_dir])
+ else
+ opts[:static_dir]
+ end
+ |> Path.join("frontends")
+ end
+
+ defp download_or_unzip(frontend_info, temp_dir, nil),
+ do: download_build(frontend_info, temp_dir)
+
+ defp download_or_unzip(_frontend_info, temp_dir, file) do
+ with {:ok, zip} <- File.read(Path.expand(file)) do
+ unzip(zip, temp_dir)
+ end
+ end
+
+ def unzip(zip, dest) do
+ with {:ok, unzipped} <- :zip.unzip(zip, [:memory]) do
+ File.rm_rf!(dest)
+ File.mkdir_p!(dest)
+
+ Enum.each(unzipped, fn {filename, data} ->
+ path = filename
+
+ new_file_path = Path.join(dest, path)
+
+ new_file_path
+ |> Path.dirname()
+ |> File.mkdir_p!()
+
+ File.write!(new_file_path, data)
+ end)
+ end
+ end
+
+ defp download_build(frontend_info, dest) do
+ Logger.info("Downloading pre-built bundle for #{frontend_info["name"]}")
+ url = String.replace(frontend_info["build_url"], "${ref}", frontend_info["ref"])
+
+ with {:ok, %{status: 200, body: zip_body}} <-
+ Pleroma.HTTP.get(url, [], pool: :media, recv_timeout: 120_000) do
+ unzip(zip_body, dest)
+ else
+ {:error, e} -> {:error, e}
+ e -> {:error, e}
+ end
+ end
+
+ defp install_frontend(frontend_info, source, dest) do
+ from = frontend_info["build_dir"] || "dist"
+ File.rm_rf!(dest)
+ File.mkdir_p!(dest)
+ File.cp_r!(Path.join([source, from]), dest)
+ :ok
+ end
+end
diff --git a/lib/pleroma/gun/gun.ex b/lib/pleroma/gun.ex
index 4043e4880..4043e4880 100644
--- a/lib/pleroma/gun/gun.ex
+++ b/lib/pleroma/gun.ex
diff --git a/lib/pleroma/gun/conn.ex b/lib/pleroma/gun/conn.ex
index a3f75a4bb..477e19c6e 100644
--- a/lib/pleroma/gun/conn.ex
+++ b/lib/pleroma/gun/conn.ex
@@ -13,7 +13,7 @@ defmodule Pleroma.Gun.Conn do
opts =
opts
|> Enum.into(%{})
- |> Map.put_new(:await_up_timeout, pool_opts[:await_up_timeout] || 5_000)
+ |> Map.put_new(:connect_timeout, pool_opts[:connect_timeout] || 5_000)
|> Map.put_new(:supervise, false)
|> maybe_add_tls_opts(uri)
@@ -50,10 +50,10 @@ defmodule Pleroma.Gun.Conn do
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]),
+ {:ok, protocol} <- Gun.await_up(conn, opts[:connect_timeout]),
stream <- Gun.connect(conn, connect_opts),
{:response, :fin, 200, _} <- Gun.await(conn, stream) do
- {:ok, conn}
+ {:ok, conn, protocol}
else
error ->
Logger.warn(
@@ -88,8 +88,8 @@ defmodule Pleroma.Gun.Conn do
|> 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}
+ {:ok, protocol} <- Gun.await_up(conn, opts[:connect_timeout]) do
+ {:ok, conn, protocol}
else
error ->
Logger.warn(
@@ -106,8 +106,8 @@ defmodule Pleroma.Gun.Conn 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}
+ {:ok, protocol} <- Gun.await_up(conn, opts[:connect_timeout]) do
+ {:ok, conn, protocol}
else
error ->
Logger.warn(
diff --git a/lib/pleroma/gun/connection_pool.ex b/lib/pleroma/gun/connection_pool.ex
index 8b41a668c..e322f192a 100644
--- a/lib/pleroma/gun/connection_pool.ex
+++ b/lib/pleroma/gun/connection_pool.ex
@@ -1,3 +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.Gun.ConnectionPool do
@registry __MODULE__
@@ -10,6 +14,7 @@ defmodule Pleroma.Gun.ConnectionPool do
]
end
+ @spec get_conn(URI.t(), keyword()) :: {:ok, pid()} | {:error, term()}
def get_conn(uri, opts) do
key = "#{uri.scheme}:#{uri.host}:#{uri.port}"
@@ -19,7 +24,7 @@ defmodule Pleroma.Gun.ConnectionPool do
get_gun_pid_from_worker(worker_pid, true)
[{worker_pid, {gun_pid, _used_by, _crf, _last_reference}}] ->
- GenServer.cast(worker_pid, {:add_client, self(), false})
+ GenServer.call(worker_pid, :add_client)
{:ok, gun_pid}
[] ->
@@ -45,7 +50,7 @@ defmodule Pleroma.Gun.ConnectionPool do
# so instead we use cast + monitor
ref = Process.monitor(worker_pid)
- if register, do: GenServer.cast(worker_pid, {:add_client, self(), true})
+ if register, do: GenServer.cast(worker_pid, {:add_client, self()})
receive do
{:conn_pid, pid} ->
@@ -54,12 +59,14 @@ defmodule Pleroma.Gun.ConnectionPool do
{:DOWN, ^ref, :process, ^worker_pid, reason} ->
case reason do
- {:shutdown, error} -> error
+ {: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)
@@ -70,7 +77,7 @@ defmodule Pleroma.Gun.ConnectionPool do
case query_result do
[worker_pid] ->
- GenServer.cast(worker_pid, {:remove_client, self()})
+ GenServer.call(worker_pid, :remove_client)
[] ->
:ok
diff --git a/lib/pleroma/gun/connection_pool/reclaimer.ex b/lib/pleroma/gun/connection_pool/reclaimer.ex
index cea800882..241e8b04f 100644
--- a/lib/pleroma/gun/connection_pool/reclaimer.ex
+++ b/lib/pleroma/gun/connection_pool/reclaimer.ex
@@ -1,3 +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.Gun.ConnectionPool.Reclaimer do
use GenServer, restart: :temporary
diff --git a/lib/pleroma/gun/connection_pool/worker.ex b/lib/pleroma/gun/connection_pool/worker.ex
index f33447cb6..b71816bed 100644
--- a/lib/pleroma/gun/connection_pool/worker.ex
+++ b/lib/pleroma/gun/connection_pool/worker.ex
@@ -1,3 +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.Gun.ConnectionPool.Worker do
alias Pleroma.Gun
use GenServer, restart: :temporary
@@ -15,7 +19,7 @@ defmodule Pleroma.Gun.ConnectionPool.Worker do
@impl true
def handle_continue({:connect, [key, uri, opts, client_pid]}, _) do
- with {:ok, conn_pid} <- Gun.Conn.open(uri, opts),
+ with {:ok, conn_pid, protocol} <- Gun.Conn.open(uri, opts),
Process.link(conn_pid) do
time = :erlang.monotonic_time(:millisecond)
@@ -27,8 +31,12 @@ defmodule Pleroma.Gun.ConnectionPool.Worker do
send(client_pid, {:conn_pid, conn_pid})
{:noreply,
- %{key: key, timer: nil, client_monitors: %{client_pid => Process.monitor(client_pid)}},
- :hibernate}
+ %{
+ key: key,
+ timer: nil,
+ client_monitors: %{client_pid => Process.monitor(client_pid)},
+ protocol: protocol
+ }, :hibernate}
else
err ->
{:stop, {:shutdown, err}, nil}
@@ -36,15 +44,36 @@ defmodule Pleroma.Gun.ConnectionPool.Worker do
end
@impl true
- def handle_cast({:add_client, client_pid, send_pid_back}, %{key: key} = state) do
+ 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, protocol: protocol} = state) do
time = :erlang.monotonic_time(:millisecond)
- {{conn_pid, _, _, _}, _} =
+ {{conn_pid, used_by, _, _}, _} =
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)
- if send_pid_back, do: send(client_pid, {:conn_pid, conn_pid})
+ :telemetry.execute(
+ [:pleroma, :connection_pool, :client, :add],
+ %{client_pid: client_pid, clients: used_by},
+ %{key: state.key, protocol: protocol}
+ )
state =
if state.timer != nil do
@@ -57,18 +86,19 @@ defmodule Pleroma.Gun.ConnectionPool.Worker do
ref = Process.monitor(client_pid)
state = put_in(state.client_monitors[client_pid], ref)
- {:noreply, state, :hibernate}
+ {:reply, conn_pid, state, :hibernate}
end
@impl true
- def handle_cast({:remove_client, client_pid}, %{key: key} = state) do
+ 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)
+
+ Process.demonitor(ref, [:flush])
timer =
if used_by == [] do
@@ -78,7 +108,7 @@ defmodule Pleroma.Gun.ConnectionPool.Worker do
nil
end
- {:noreply, %{state | timer: timer}, :hibernate}
+ {:reply, :ok, %{state | timer: timer}, :hibernate}
end
@impl true
@@ -88,36 +118,32 @@ defmodule Pleroma.Gun.ConnectionPool.Worker do
{:stop, :normal, state}
end
+ @impl true
+ def handle_info({:gun_up, _pid, _protocol}, state) do
+ {:noreply, state, :hibernate}
+ 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
+ # Otherwise, wait for retry
@impl true
- def handle_info({:gun_down, _pid, _protocol, _reason, _killed_streams} = down_message, state) do
- {:stop, {:error, down_message}, state}
+ def handle_info({:gun_down, _pid, _protocol, _reason, _killed_streams}, state) do
+ {:noreply, state, :hibernate}
end
@impl true
def handle_info({:DOWN, _ref, :process, pid, reason}, state) do
- # Sometimes the client is dead before we demonitor it in :remove_client, so the message
- # arrives anyway
-
- case state.client_monitors[pid] do
- nil ->
- {:noreply, state, :hibernate}
+ :telemetry.execute(
+ [:pleroma, :connection_pool, :client, :dead],
+ %{client_pid: pid, reason: reason},
+ %{key: state.key}
+ )
- _ref ->
- :telemetry.execute(
- [:pleroma, :connection_pool, :client_death],
- %{client_pid: pid, reason: reason},
- %{key: state.key}
- )
-
- handle_cast({:remove_client, pid}, state)
- end
+ handle_cast({:remove_client, pid}, state)
end
# LRFU policy: https://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.55.1478
diff --git a/lib/pleroma/gun/connection_pool/worker_supervisor.ex b/lib/pleroma/gun/connection_pool/worker_supervisor.ex
index 39615c956..4c23bcbd9 100644
--- a/lib/pleroma/gun/connection_pool/worker_supervisor.ex
+++ b/lib/pleroma/gun/connection_pool/worker_supervisor.ex
@@ -1,3 +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.Gun.ConnectionPool.WorkerSupervisor do
@moduledoc "Supervisor for pool workers. Does not do anything except enforce max connection limit"
diff --git a/lib/pleroma/helpers/inet_helper.ex b/lib/pleroma/helpers/inet_helper.ex
new file mode 100644
index 000000000..126f82381
--- /dev/null
+++ b/lib/pleroma/helpers/inet_helper.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.Helpers.InetHelper do
+ def parse_address(ip) when is_tuple(ip) do
+ {:ok, ip}
+ end
+
+ def parse_address(ip) when is_binary(ip) do
+ ip
+ |> String.to_charlist()
+ |> parse_address()
+ end
+
+ def parse_address(ip) do
+ :inet.parse_address(ip)
+ end
+end
diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex
new file mode 100644
index 000000000..6b799173e
--- /dev/null
+++ b/lib/pleroma/helpers/media_helper.ex
@@ -0,0 +1,162 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Helpers.MediaHelper do
+ @moduledoc """
+ Handles common media-related operations.
+ """
+
+ alias Pleroma.HTTP
+
+ require Logger
+
+ def missing_dependencies do
+ Enum.reduce([imagemagick: "convert", ffmpeg: "ffmpeg"], [], fn {sym, executable}, acc ->
+ if Pleroma.Utils.command_available?(executable) do
+ acc
+ else
+ [sym | acc]
+ end
+ end)
+ end
+
+ def image_resize(url, options) do
+ with executable when is_binary(executable) <- System.find_executable("convert"),
+ {:ok, args} <- prepare_image_resize_args(options),
+ {:ok, env} <- HTTP.get(url, [], pool: :media),
+ {:ok, fifo_path} <- mkfifo() do
+ args = List.flatten([fifo_path, args])
+ run_fifo(fifo_path, env, executable, args)
+ else
+ nil -> {:error, {:convert, :command_not_found}}
+ {:error, _} = error -> error
+ end
+ end
+
+ defp prepare_image_resize_args(
+ %{max_width: max_width, max_height: max_height, format: "png"} = options
+ ) do
+ quality = options[:quality] || 85
+ resize = Enum.join([max_width, "x", max_height, ">"])
+
+ args = [
+ "-resize",
+ resize,
+ "-quality",
+ to_string(quality),
+ "png:-"
+ ]
+
+ {:ok, args}
+ end
+
+ defp prepare_image_resize_args(%{max_width: max_width, max_height: max_height} = options) do
+ quality = options[:quality] || 85
+ resize = Enum.join([max_width, "x", max_height, ">"])
+
+ args = [
+ "-interlace",
+ "Plane",
+ "-resize",
+ resize,
+ "-quality",
+ to_string(quality),
+ "jpg:-"
+ ]
+
+ {:ok, args}
+ end
+
+ defp prepare_image_resize_args(_), do: {:error, :missing_options}
+
+ # Note: video thumbnail is intentionally not resized (always has original dimensions)
+ def video_framegrab(url) do
+ with executable when is_binary(executable) <- System.find_executable("ffmpeg"),
+ {:ok, env} <- HTTP.get(url, [], pool: :media),
+ {:ok, fifo_path} <- mkfifo(),
+ args = [
+ "-y",
+ "-i",
+ fifo_path,
+ "-vframes",
+ "1",
+ "-f",
+ "mjpeg",
+ "-loglevel",
+ "error",
+ "-"
+ ] do
+ run_fifo(fifo_path, env, executable, args)
+ else
+ nil -> {:error, {:ffmpeg, :command_not_found}}
+ {:error, _} = error -> error
+ end
+ end
+
+ defp run_fifo(fifo_path, env, executable, args) do
+ pid =
+ Port.open({:spawn_executable, executable}, [
+ :use_stdio,
+ :stream,
+ :exit_status,
+ :binary,
+ args: args
+ ])
+
+ fifo = Port.open(to_charlist(fifo_path), [:eof, :binary, :stream, :out])
+ fix = Pleroma.Helpers.QtFastStart.fix(env.body)
+ true = Port.command(fifo, fix)
+ :erlang.port_close(fifo)
+ loop_recv(pid)
+ after
+ File.rm(fifo_path)
+ end
+
+ defp mkfifo do
+ path = Path.join(System.tmp_dir!(), "pleroma-media-preview-pipe-#{Ecto.UUID.generate()}")
+
+ case System.cmd("mkfifo", [path]) do
+ {_, 0} ->
+ spawn(fifo_guard(path))
+ {:ok, path}
+
+ {_, err} ->
+ {:error, {:fifo_failed, err}}
+ end
+ end
+
+ defp fifo_guard(path) do
+ pid = self()
+
+ fn ->
+ ref = Process.monitor(pid)
+
+ receive do
+ {:DOWN, ^ref, :process, ^pid, _} ->
+ File.rm(path)
+ end
+ end
+ end
+
+ defp loop_recv(pid) do
+ loop_recv(pid, <<>>)
+ end
+
+ defp loop_recv(pid, acc) do
+ receive do
+ {^pid, {:data, data}} ->
+ loop_recv(pid, acc <> data)
+
+ {^pid, {:exit_status, 0}} ->
+ {:ok, acc}
+
+ {^pid, {:exit_status, status}} ->
+ {:error, status}
+ after
+ 5000 ->
+ :erlang.port_close(pid)
+ {:error, :timeout}
+ end
+ end
+end
diff --git a/lib/pleroma/helpers/qt_fast_start.ex b/lib/pleroma/helpers/qt_fast_start.ex
new file mode 100644
index 000000000..bb93224b5
--- /dev/null
+++ b/lib/pleroma/helpers/qt_fast_start.ex
@@ -0,0 +1,131 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Helpers.QtFastStart do
+ @moduledoc """
+ (WIP) Converts a "slow start" (data before metadatas) mov/mp4 file to a "fast start" one (metadatas before data).
+ """
+
+ # TODO: Cleanup and optimizations
+ # Inspirations: https://www.ffmpeg.org/doxygen/3.4/qt-faststart_8c_source.html
+ # https://github.com/danielgtaylor/qtfaststart/blob/master/qtfaststart/processor.py
+ # ISO/IEC 14496-12:2015, ISO/IEC 15444-12:2015
+ # Paracetamol
+
+ def fix(<<0x00, 0x00, 0x00, _, 0x66, 0x74, 0x79, 0x70, _::bits>> = binary) do
+ index = fix(binary, 0, nil, nil, [])
+
+ case index do
+ :abort -> binary
+ [{"ftyp", _, _, _, _}, {"mdat", _, _, _, _} | _] -> faststart(index)
+ [{"ftyp", _, _, _, _}, {"free", _, _, _, _}, {"mdat", _, _, _, _} | _] -> faststart(index)
+ _ -> binary
+ end
+ end
+
+ def fix(binary) do
+ binary
+ end
+
+ # MOOV have been seen before MDAT- abort
+ defp fix(<<_::bits>>, _, true, false, _) do
+ :abort
+ end
+
+ defp fix(
+ <<size::integer-big-size(32), fourcc::bits-size(32), rest::bits>>,
+ pos,
+ got_moov,
+ got_mdat,
+ acc
+ ) do
+ full_size = (size - 8) * 8
+ <<data::bits-size(full_size), rest::bits>> = rest
+
+ acc = [
+ {fourcc, pos, pos + size, size,
+ <<size::integer-big-size(32), fourcc::bits-size(32), data::bits>>}
+ | acc
+ ]
+
+ fix(rest, pos + size, got_moov || fourcc == "moov", got_mdat || fourcc == "mdat", acc)
+ end
+
+ defp fix(<<>>, _pos, _, _, acc) do
+ :lists.reverse(acc)
+ end
+
+ defp faststart(index) do
+ {{_ftyp, _, _, _, ftyp}, index} = List.keytake(index, "ftyp", 0)
+
+ # Skip re-writing the free fourcc as it's kind of useless.
+ # Why stream useless bytes when you can do without?
+ {free_size, index} =
+ case List.keytake(index, "free", 0) do
+ {{_, _, _, size, _}, index} -> {size, index}
+ _ -> {0, index}
+ end
+
+ {{_moov, _, _, moov_size, moov}, index} = List.keytake(index, "moov", 0)
+ offset = -free_size + moov_size
+ rest = for {_, _, _, _, data} <- index, do: data, into: []
+ <<moov_head::bits-size(64), moov_data::bits>> = moov
+ [ftyp, moov_head, fix_moov(moov_data, offset, []), rest]
+ end
+
+ defp fix_moov(
+ <<size::integer-big-size(32), fourcc::bits-size(32), rest::bits>>,
+ offset,
+ acc
+ ) do
+ full_size = (size - 8) * 8
+ <<data::bits-size(full_size), rest::bits>> = rest
+
+ data =
+ cond do
+ fourcc in ["trak", "mdia", "minf", "stbl"] ->
+ # Theses contains sto or co64 part
+ [<<size::integer-big-size(32), fourcc::bits-size(32)>>, fix_moov(data, offset, [])]
+
+ fourcc in ["stco", "co64"] ->
+ # fix the damn thing
+ <<version::integer-big-size(32), count::integer-big-size(32), rest::bits>> = data
+
+ entry_size =
+ case fourcc do
+ "stco" -> 32
+ "co64" -> 64
+ end
+
+ [
+ <<size::integer-big-size(32), fourcc::bits-size(32), version::integer-big-size(32),
+ count::integer-big-size(32)>>,
+ rewrite_entries(entry_size, offset, rest, [])
+ ]
+
+ true ->
+ [<<size::integer-big-size(32), fourcc::bits-size(32)>>, data]
+ end
+
+ acc = [acc | data]
+ fix_moov(rest, offset, acc)
+ end
+
+ defp fix_moov(<<>>, _, acc), do: acc
+
+ for size <- [32, 64] do
+ defp rewrite_entries(
+ unquote(size),
+ offset,
+ <<pos::integer-big-size(unquote(size)), rest::bits>>,
+ acc
+ ) do
+ rewrite_entries(unquote(size), offset, rest, [
+ acc | <<pos + offset::integer-big-size(unquote(size))>>
+ ])
+ end
+ end
+
+ defp rewrite_entries(_, _, <<>>, acc), do: acc
+end
diff --git a/lib/pleroma/helpers/uri_helper.ex b/lib/pleroma/helpers/uri_helper.ex
index 6d205a636..f1301f055 100644
--- a/lib/pleroma/helpers/uri_helper.ex
+++ b/lib/pleroma/helpers/uri_helper.ex
@@ -3,18 +3,22 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Helpers.UriHelper do
- def append_uri_params(uri, appended_params) do
+ def modify_uri_params(uri, overridden_params, deleted_params \\ []) do
uri = URI.parse(uri)
- appended_params = for {k, v} <- appended_params, into: %{}, do: {to_string(k), v}
- existing_params = URI.query_decoder(uri.query || "") |> Enum.into(%{})
- updated_params_keys = Enum.uniq(Map.keys(existing_params) ++ Map.keys(appended_params))
+
+ existing_params = URI.query_decoder(uri.query || "") |> Map.new()
+ overridden_params = Map.new(overridden_params, fn {k, v} -> {to_string(k), v} end)
+ deleted_params = Enum.map(deleted_params, &to_string/1)
updated_params =
- for k <- updated_params_keys, do: {k, appended_params[k] || existing_params[k]}
+ existing_params
+ |> Map.merge(overridden_params)
+ |> Map.drop(deleted_params)
uri
|> Map.put(:query, URI.encode_query(updated_params))
|> URI.to_string()
+ |> String.replace_suffix("?", "")
end
def maybe_add_base("/" <> uri, base), do: Path.join([base, uri])
diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex
index dc1b9b840..43e9145be 100644
--- a/lib/pleroma/html.ex
+++ b/lib/pleroma/html.ex
@@ -100,20 +100,27 @@ defmodule Pleroma.HTML do
end)
end
- def extract_first_external_url(_, nil), do: {:error, "No content"}
+ def extract_first_external_url_from_object(%{data: %{"content" => content}} = object)
+ when is_binary(content) do
+ unless object.data["fake"] do
+ key = "URL|#{object.id}"
+
+ Cachex.fetch!(:scrubber_cache, key, fn _key ->
+ {:commit, {:ok, extract_first_external_url(content)}}
+ end)
+ else
+ {:ok, extract_first_external_url(content)}
+ end
+ end
- def extract_first_external_url(object, content) do
- key = "URL|#{object.id}"
+ def extract_first_external_url_from_object(_), do: {:error, :no_content}
- Cachex.fetch!(:scrubber_cache, key, fn _key ->
- result =
- content
- |> Floki.parse_fragment!()
- |> Floki.filter_out("a.mention,a.hashtag,a.attachment,a[rel~=\"tag\"]")
- |> Floki.attribute("a", "href")
- |> Enum.at(0)
-
- {:commit, {:ok, result}}
- end)
+ def extract_first_external_url(content) do
+ content
+ |> Floki.parse_fragment!()
+ |> Floki.find("a:not(.mention,.hashtag,.attachment,[rel~=\"tag\"])")
+ |> Enum.take(1)
+ |> Floki.attribute("href")
+ |> Enum.at(0)
end
end
diff --git a/lib/pleroma/http/http.ex b/lib/pleroma/http.ex
index b37b3fa89..052597191 100644
--- a/lib/pleroma/http/http.ex
+++ b/lib/pleroma/http.ex
@@ -60,30 +60,23 @@ defmodule Pleroma.HTTP do
{: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] || [])
-
- 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)
-
- adapter = Application.get_env(:tesla, :adapter)
-
- client = Tesla.client(adapter_middlewares(adapter), adapter)
-
- maybe_limit(
- fn ->
- request(client, request)
- end,
- adapter,
- adapter_opts
- )
-
- # Connection release is handled in a custom FollowRedirects middleware
- err ->
- err
- end
+ adapter_opts = AdapterHelper.options(uri, options || [])
+
+ options = put_in(options[:adapter], adapter_opts)
+ params = options[:params] || []
+ request = build_request(method, headers, options, url, body, params)
+
+ adapter = Application.get_env(:tesla, :adapter)
+
+ client = Tesla.client(adapter_middlewares(adapter), adapter)
+
+ maybe_limit(
+ fn ->
+ request(client, request)
+ end,
+ adapter,
+ adapter_opts
+ )
end
@spec request(Client.t(), keyword()) :: {:ok, Env.t()} | {:error, any()}
@@ -110,7 +103,7 @@ defmodule Pleroma.HTTP do
end
defp adapter_middlewares(Tesla.Adapter.Gun) do
- [Pleroma.HTTP.Middleware.FollowRedirects]
+ [Tesla.Middleware.FollowRedirects, Pleroma.Tesla.Middleware.ConnectionPool]
end
defp adapter_middlewares(_), do: []
diff --git a/lib/pleroma/http/adapter_helper.ex b/lib/pleroma/http/adapter_helper.ex
index 9ec3836b0..08b51578a 100644
--- a/lib/pleroma/http/adapter_helper.ex
+++ b/lib/pleroma/http/adapter_helper.ex
@@ -6,12 +6,11 @@ defmodule Pleroma.HTTP.AdapterHelper do
@moduledoc """
Configure Tesla.Client with default and customized adapter options.
"""
- @defaults [pool: :federation]
+ @defaults [pool: :federation, connect_timeout: 5_000, recv_timeout: 5_000]
@type proxy_type() :: :socks4 | :socks5
@type host() :: charlist() | :inet.ip_address()
- alias Pleroma.Config
alias Pleroma.HTTP.AdapterHelper
require Logger
@@ -20,7 +19,6 @@ defmodule Pleroma.HTTP.AdapterHelper do
| {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
@@ -44,27 +42,10 @@ defmodule Pleroma.HTTP.AdapterHelper do
@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
diff --git a/lib/pleroma/http/adapter_helper/default.ex b/lib/pleroma/http/adapter_helper/default.ex
index e13441316..8567a616b 100644
--- a/lib/pleroma/http/adapter_helper/default.ex
+++ b/lib/pleroma/http/adapter_helper/default.ex
@@ -1,3 +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.HTTP.AdapterHelper.Default do
alias Pleroma.HTTP.AdapterHelper
diff --git a/lib/pleroma/http/adapter_helper/gun.ex b/lib/pleroma/http/adapter_helper/gun.ex
index b4ff8306c..1dbb71362 100644
--- a/lib/pleroma/http/adapter_helper/gun.ex
+++ b/lib/pleroma/http/adapter_helper/gun.ex
@@ -5,57 +5,62 @@
defmodule Pleroma.HTTP.AdapterHelper.Gun do
@behaviour Pleroma.HTTP.AdapterHelper
- alias Pleroma.Gun.ConnectionPool
+ alias Pleroma.Config
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
+ retry: 1,
+ retry_timeout: 1_000
]
+ @type pool() :: :federation | :upload | :media | :default
+
@spec options(keyword(), URI.t()) :: keyword()
def options(incoming_opts \\ [], %URI{} = uri) do
proxy =
- Pleroma.Config.get([:http, :proxy_url])
+ [:http, :proxy_url]
+ |> Config.get()
|> AdapterHelper.format_proxy()
- config_opts = Pleroma.Config.get([:http, :adapter], [])
+ config_opts = Config.get([:http, :adapter], [])
@defaults
|> Keyword.merge(config_opts)
|> add_scheme_opts(uri)
|> AdapterHelper.maybe_add_proxy(proxy)
|> Keyword.merge(incoming_opts)
+ |> put_timeout()
end
defp add_scheme_opts(opts, %{scheme: "http"}), do: opts
defp add_scheme_opts(opts, %{scheme: "https"}) do
- opts
- |> Keyword.put(:certificates_verification, true)
+ Keyword.put(opts, :certificates_verification, true)
+ end
+
+ defp put_timeout(opts) do
+ {recv_timeout, opts} = Keyword.pop(opts, :recv_timeout, pool_timeout(opts[:pool]))
+ # this is the timeout to receive a message from Gun
+ # `:timeout` key is used in Tesla
+ Keyword.put(opts, :timeout, recv_timeout)
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
+ @spec pool_timeout(pool()) :: non_neg_integer()
+ def pool_timeout(pool) do
+ default = Config.get([:pools, :default, :recv_timeout], 5_000)
+
+ Config.get([:pools, pool, :recv_timeout], default)
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])
+ wait = Config.get([:connections_pool, :connection_acquisition_wait])
+ retries = Config.get([:connections_pool, :connection_acquisition_retries])
:pools
- |> Pleroma.Config.get([])
+ |> Config.get([])
|> Enum.each(fn {name, opts} ->
max_running = Keyword.get(opts, :size, 50)
max_waiting = Keyword.get(opts, :max_waiting, 10)
@@ -69,7 +74,6 @@ defmodule Pleroma.HTTP.AdapterHelper.Gun do
case result do
:ok -> :ok
{:error, :existing} -> :ok
- e -> raise e
end
end)
diff --git a/lib/pleroma/http/adapter_helper/hackney.ex b/lib/pleroma/http/adapter_helper/hackney.ex
index cd569422b..ff60513fd 100644
--- a/lib/pleroma/http/adapter_helper/hackney.ex
+++ b/lib/pleroma/http/adapter_helper/hackney.ex
@@ -1,12 +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.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
+ force_redirect: true
]
@spec options(keyword(), URI.t()) :: keyword()
@@ -19,11 +20,21 @@ defmodule Pleroma.HTTP.AdapterHelper.Hackney do
|> Keyword.merge(config_opts)
|> Keyword.merge(connection_opts)
|> add_scheme_opts(uri)
+ |> maybe_add_with_body()
|> Pleroma.HTTP.AdapterHelper.maybe_add_proxy(proxy)
end
+ defp add_scheme_opts(opts, %URI{scheme: "https"}) do
+ Keyword.put(opts, :ssl_options, versions: [:"tlsv1.2", :"tlsv1.1", :tlsv1])
+ end
+
defp add_scheme_opts(opts, _), do: opts
- @spec get_conn(URI.t(), keyword()) :: {:ok, keyword()}
- def get_conn(_uri, opts), do: {:ok, opts}
+ defp maybe_add_with_body(opts) do
+ if opts[:max_body] do
+ Keyword.put(opts, :with_body, true)
+ else
+ opts
+ end
+ end
end
diff --git a/lib/pleroma/http/ex_aws.ex b/lib/pleroma/http/ex_aws.ex
index e53e64077..5cac3532f 100644
--- a/lib/pleroma/http/ex_aws.ex
+++ b/lib/pleroma/http/ex_aws.ex
@@ -11,6 +11,8 @@ defmodule Pleroma.HTTP.ExAws do
@impl true
def request(method, url, body \\ "", headers \\ [], http_opts \\ []) do
+ http_opts = Keyword.put_new(http_opts, :pool, :upload)
+
case HTTP.request(method, url, body, headers, http_opts) do
{:ok, env} ->
{:ok, %{status_code: env.status, headers: env.headers, body: env.body}}
diff --git a/lib/pleroma/http/request_builder.ex b/lib/pleroma/http/request_builder.ex
index 2fc876d92..8a44a001d 100644
--- a/lib/pleroma/http/request_builder.ex
+++ b/lib/pleroma/http/request_builder.ex
@@ -34,10 +34,12 @@ defmodule Pleroma.HTTP.RequestBuilder do
@spec headers(Request.t(), Request.headers()) :: Request.t()
def headers(request, headers) do
headers_list =
- if Pleroma.Config.get([:http, :send_user_agent]) do
+ 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
- headers
+ _ ->
+ headers
end
%{request | headers: headers_list}
diff --git a/lib/pleroma/http/tzdata.ex b/lib/pleroma/http/tzdata.ex
index 34bb253a7..09cfdadf7 100644
--- a/lib/pleroma/http/tzdata.ex
+++ b/lib/pleroma/http/tzdata.ex
@@ -11,6 +11,8 @@ defmodule Pleroma.HTTP.Tzdata do
@impl true
def get(url, headers, options) do
+ options = Keyword.put_new(options, :pool, :default)
+
with {:ok, %Tesla.Env{} = env} <- HTTP.get(url, headers, options) do
{:ok, {env.status, env.headers, env.body}}
end
@@ -18,6 +20,8 @@ defmodule Pleroma.HTTP.Tzdata do
@impl true
def head(url, headers, options) do
+ options = Keyword.put_new(options, :pool, :default)
+
with {:ok, %Tesla.Env{} = env} <- HTTP.head(url, headers, options) do
{:ok, {env.status, env.headers}}
end
diff --git a/lib/pleroma/http/web_push.ex b/lib/pleroma/http/web_push.ex
new file mode 100644
index 000000000..78148a12e
--- /dev/null
+++ b/lib/pleroma/http/web_push.ex
@@ -0,0 +1,12 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.HTTP.WebPush do
+ @moduledoc false
+
+ def post(url, payload, headers) do
+ list_headers = Map.to_list(headers)
+ Pleroma.HTTP.post(url, payload, list_headers)
+ end
+end
diff --git a/lib/pleroma/instances.ex b/lib/pleroma/instances.ex
index 557e8decf..7315bd7cb 100644
--- a/lib/pleroma/instances.ex
+++ b/lib/pleroma/instances.ex
@@ -11,6 +11,7 @@ defmodule Pleroma.Instances do
defdelegate reachable?(url_or_host), to: @adapter
defdelegate set_reachable(url_or_host), to: @adapter
defdelegate set_unreachable(url_or_host, unreachable_since \\ nil), to: @adapter
+ defdelegate get_consistently_unreachable(), to: @adapter
def set_consistently_unreachable(url_or_host),
do: set_unreachable(url_or_host, reachability_datetime_threshold())
diff --git a/lib/pleroma/instances/instance.ex b/lib/pleroma/instances/instance.ex
index a1f935232..df471a39d 100644
--- a/lib/pleroma/instances/instance.ex
+++ b/lib/pleroma/instances/instance.ex
@@ -14,6 +14,8 @@ defmodule Pleroma.Instances.Instance do
import Ecto.Query
import Ecto.Changeset
+ require Logger
+
schema "instances" do
field(:host, :string)
field(:unreachable_since, :naive_datetime_usec)
@@ -117,6 +119,17 @@ defmodule Pleroma.Instances.Instance do
def set_unreachable(_, _), do: {:error, nil}
+ def get_consistently_unreachable do
+ reachability_datetime_threshold = Instances.reachability_datetime_threshold()
+
+ from(i in Instance,
+ where: ^reachability_datetime_threshold > i.unreachable_since,
+ order_by: i.unreachable_since,
+ select: {i.host, i.unreachable_since}
+ )
+ |> Repo.all()
+ end
+
defp parse_datetime(datetime) when is_binary(datetime) do
NaiveDateTime.from_iso8601(datetime)
end
@@ -145,25 +158,32 @@ defmodule Pleroma.Instances.Instance do
favicon
end
+ rescue
+ e ->
+ Logger.warn("Instance.get_or_update_favicon(\"#{host}\") error: #{inspect(e)}")
+ nil
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
+ Pleroma.HTTP.get(to_string(instance_uri), [{"accept", "text/html"}], pool: :media),
+ {_, [favicon_rel | _]} when is_binary(favicon_rel) <-
+ {:parse,
+ html |> Floki.parse_document!() |> Floki.attribute("link[rel=icon]", "href")},
+ {_, favicon} when is_binary(favicon) <-
+ {:merge, URI.merge(instance_uri, favicon_rel) |> to_string()} do
favicon
else
_ -> nil
end
rescue
- _ -> nil
+ e ->
+ Logger.warn(
+ "Instance.scrape_favicon(\"#{to_string(instance_uri)}\") error: #{inspect(e)}"
+ )
+
+ 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/jwt.ex b/lib/pleroma/jwt.ex
index 10102ff5d..faeb77781 100644
--- a/lib/pleroma/jwt.ex
+++ b/lib/pleroma/jwt.ex
@@ -1,3 +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.JWT do
use Joken.Config
diff --git a/lib/pleroma/mfa/token.ex b/lib/pleroma/mfa/token.ex
index 0b2449971..69b64c0e8 100644
--- a/lib/pleroma/mfa/token.ex
+++ b/lib/pleroma/mfa/token.ex
@@ -10,10 +10,11 @@ defmodule Pleroma.MFA.Token do
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.OAuth.Authorization
- alias Pleroma.Web.OAuth.Token, as: OAuthToken
@expires 300
+ @type t() :: %__MODULE__{}
+
schema "mfa_tokens" do
field(:token, :string)
field(:valid_until, :naive_datetime_usec)
@@ -24,6 +25,7 @@ defmodule Pleroma.MFA.Token do
timestamps()
end
+ @spec get_by_token(String.t()) :: {:ok, t()} | {:error, :not_found}
def get_by_token(token) do
from(
t in __MODULE__,
@@ -33,33 +35,40 @@ defmodule Pleroma.MFA.Token do
|> 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
+ @spec validate(String.t()) :: {:ok, t()} | {:error, :not_found} | {:error, :expired_token}
+ def validate(token_str) do
+ with {:ok, token} <- get_by_token(token_str),
+ false <- 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()
+ defp expired?(%__MODULE__{valid_until: valid_until}) do
+ with true <- NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) > 0 do
+ {:error, :expired_token}
+ end
+ end
+
+ @spec create(User.t(), Authorization.t() | nil) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
+ def create(user, authorization \\ nil) do
+ with {:ok, token} <- do_create(user, authorization) do
+ Pleroma.Workers.PurgeExpiredToken.enqueue(%{
+ token_id: token.id,
+ valid_until: DateTime.from_naive!(token.valid_until, "Etc/UTC"),
+ mod: __MODULE__
+ })
+
+ {:ok, token}
+ end
end
- def create_token(user, authorization) do
+ defp do_create(user, authorization) do
%__MODULE__{}
- |> change
+ |> change()
|> assign_user(user)
- |> assign_authorization(authorization)
- |> put_token
- |> put_valid_until
+ |> maybe_assign_authorization(authorization)
+ |> put_token()
+ |> put_valid_until()
|> Repo.insert()
end
@@ -69,15 +78,19 @@ defmodule Pleroma.MFA.Token do
|> validate_required([:user])
end
- defp assign_authorization(changeset, authorization) do
+ defp maybe_assign_authorization(changeset, %Authorization{} = authorization) do
changeset
|> put_assoc(:authorization, authorization)
|> validate_required([:authorization])
end
+ defp maybe_assign_authorization(changeset, _), do: changeset
+
defp put_token(changeset) do
+ token = Pleroma.Web.OAuth.Token.Utils.generate_token()
+
changeset
- |> change(%{token: OAuthToken.Utils.generate_token()})
+ |> change(%{token: token})
|> validate_required([:token])
|> unique_constraint(:token)
end
@@ -89,18 +102,4 @@ defmodule Pleroma.MFA.Token do
|> 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/migration_helper/notification_backfill.ex b/lib/pleroma/migration_helper/notification_backfill.ex
index d260e62ca..24f4733fe 100644
--- a/lib/pleroma/migration_helper/notification_backfill.ex
+++ b/lib/pleroma/migration_helper/notification_backfill.ex
@@ -19,13 +19,13 @@ defmodule Pleroma.MigrationHelper.NotificationBackfill do
query
|> Repo.chunk_stream(100)
|> Enum.each(fn notification ->
- type =
- notification.activity
- |> type_from_activity()
+ if notification.activity do
+ type = type_from_activity(notification.activity)
- notification
- |> Ecto.Changeset.change(%{type: type})
- |> Repo.update()
+ notification
+ |> Ecto.Changeset.change(%{type: type})
+ |> Repo.update()
+ end
end)
end
@@ -72,8 +72,7 @@ defmodule Pleroma.MigrationHelper.NotificationBackfill do
"pleroma:emoji_reaction"
"Create" ->
- activity
- |> type_from_activity_object()
+ type_from_activity_object(activity)
t ->
raise "No notification type for activity type #{t}"
diff --git a/lib/pleroma/mime.ex b/lib/pleroma/mime.ex
deleted file mode 100644
index 6ee055f50..000000000
--- a/lib/pleroma/mime.ex
+++ /dev/null
@@ -1,120 +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.MIME do
- @moduledoc """
- Returns the mime-type of a binary and optionally a normalized file-name.
- """
- @default "application/octet-stream"
- @read_bytes 35
-
- @spec file_mime_type(String.t(), String.t()) ::
- {:ok, content_type :: String.t(), filename :: String.t()} | {:error, any()} | :error
- def file_mime_type(path, filename) do
- with {:ok, content_type} <- file_mime_type(path),
- filename <- fix_extension(filename, content_type) do
- {:ok, content_type, filename}
- end
- end
-
- @spec file_mime_type(String.t()) :: {:ok, String.t()} | {:error, any()} | :error
- def file_mime_type(filename) do
- File.open(filename, [:read], fn f ->
- check_mime_type(IO.binread(f, @read_bytes))
- end)
- end
-
- def bin_mime_type(binary, filename) do
- with {:ok, content_type} <- bin_mime_type(binary),
- filename <- fix_extension(filename, content_type) do
- {:ok, content_type, filename}
- end
- end
-
- @spec bin_mime_type(binary()) :: {:ok, String.t()} | :error
- def bin_mime_type(<<head::binary-size(@read_bytes), _::binary>>) do
- {:ok, check_mime_type(head)}
- end
-
- def bin_mime_type(_), do: :error
-
- def mime_type(<<_::binary>>), do: {:ok, @default}
-
- defp fix_extension(filename, content_type) do
- parts = String.split(filename, ".")
-
- new_filename =
- if length(parts) > 1 do
- Enum.drop(parts, -1) |> Enum.join(".")
- else
- Enum.join(parts)
- end
-
- cond do
- content_type == "application/octet-stream" ->
- filename
-
- ext = List.first(MIME.extensions(content_type)) ->
- new_filename <> "." <> ext
-
- true ->
- Enum.join([new_filename, String.split(content_type, "/") |> List.last()], ".")
- end
- end
-
- defp check_mime_type(<<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, _::binary>>) do
- "image/png"
- end
-
- defp check_mime_type(<<0x47, 0x49, 0x46, 0x38, _, 0x61, _::binary>>) do
- "image/gif"
- end
-
- defp check_mime_type(<<0xFF, 0xD8, 0xFF, _::binary>>) do
- "image/jpeg"
- end
-
- defp check_mime_type(<<0x1A, 0x45, 0xDF, 0xA3, _::binary>>) do
- "video/webm"
- end
-
- defp check_mime_type(<<0x00, 0x00, 0x00, _, 0x66, 0x74, 0x79, 0x70, _::binary>>) do
- "video/mp4"
- end
-
- defp check_mime_type(<<0x49, 0x44, 0x33, _::binary>>) do
- "audio/mpeg"
- end
-
- defp check_mime_type(<<255, 251, _, 68, 0, 0, 0, 0, _::binary>>) do
- "audio/mpeg"
- end
-
- defp check_mime_type(
- <<0x4F, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00, _::size(160), 0x80, 0x74, 0x68, 0x65,
- 0x6F, 0x72, 0x61, _::binary>>
- ) do
- "video/ogg"
- end
-
- defp check_mime_type(<<0x4F, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00, _::binary>>) do
- "audio/ogg"
- end
-
- defp check_mime_type(<<"RIFF", _::binary-size(4), "WAVE", _::binary>>) do
- "audio/wav"
- end
-
- defp check_mime_type(<<"RIFF", _::binary-size(4), "WEBP", _::binary>>) do
- "image/webp"
- end
-
- defp check_mime_type(<<"RIFF", _::binary-size(4), "AVI.", _::binary>>) do
- "video/avi"
- end
-
- defp check_mime_type(_) do
- @default
- end
-end
diff --git a/lib/pleroma/moderation_log.ex b/lib/pleroma/moderation_log.ex
index 7aacd9d80..142dd8e0a 100644
--- a/lib/pleroma/moderation_log.ex
+++ b/lib/pleroma/moderation_log.ex
@@ -1,3 +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.ModerationLog do
use Ecto.Schema
@@ -320,6 +324,19 @@ defmodule Pleroma.ModerationLog do
|> insert_log_entry_with_message()
end
+ @spec insert_log(%{actor: User, action: String.t(), subject_id: String.t()}) ::
+ {:ok, ModerationLog} | {:error, any}
+ def insert_log(%{actor: %User{} = actor, action: "chat_message_delete", subject_id: subject_id}) do
+ %ModerationLog{
+ data: %{
+ "actor" => %{"nickname" => actor.nickname},
+ "action" => "chat_message_delete",
+ "subject_id" => subject_id
+ }
+ }
+ |> insert_log_entry_with_message()
+ end
+
@spec insert_log_entry_with_message(ModerationLog) :: {:ok, ModerationLog} | {:error, any}
defp insert_log_entry_with_message(entry) do
entry.data["message"]
@@ -413,6 +430,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"
@@ -616,6 +644,27 @@ defmodule Pleroma.ModerationLog do
"@#{actor_nickname} updated users: #{users_to_nicknames_string(subjects)}"
end
+ @spec get_log_entry_message(ModerationLog) :: String.t()
+ def get_log_entry_message(%ModerationLog{
+ data: %{
+ "actor" => %{"nickname" => actor_nickname},
+ "action" => "chat_message_delete",
+ "subject_id" => subject_id
+ }
+ }) do
+ "@#{actor_nickname} deleted chat message ##{subject_id}"
+ end
+
+ def get_log_entry_message(%ModerationLog{
+ data: %{
+ "actor" => %{"nickname" => actor_nickname},
+ "action" => "create_backup",
+ "subject" => %{"nickname" => user_nickname}
+ }
+ }) do
+ "@#{actor_nickname} requested account backup for @#{user_nickname}"
+ end
+
defp nicknames_to_string(nicknames) do
nicknames
|> Enum.map(&"@#{&1}")
diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex
index 0b171563b..dd7a1c824 100644
--- a/lib/pleroma/notification.ex
+++ b/lib/pleroma/notification.ex
@@ -15,6 +15,7 @@ defmodule Pleroma.Notification do
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
@@ -69,6 +70,7 @@ defmodule Pleroma.Notification do
move
pleroma:chat_mention
pleroma:emoji_reaction
+ pleroma:report
reblog
}
@@ -366,7 +368,7 @@ defmodule Pleroma.Notification do
end
def create_notifications(%Activity{data: %{"type" => type}} = activity, options)
- when type in ["Follow", "Like", "Announce", "Move", "EmojiReact"] do
+ when type in ["Follow", "Like", "Announce", "Move", "EmojiReact", "Flag"] do
do_create_notifications(activity, options)
end
@@ -409,6 +411,9 @@ defmodule Pleroma.Notification do
"EmojiReact" ->
"pleroma:emoji_reaction"
+ "Flag" ->
+ "pleroma:report"
+
# Compatibility with old reactions
"EmojiReaction" ->
"pleroma:emoji_reaction"
@@ -441,6 +446,7 @@ defmodule Pleroma.Notification do
|> 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")
@@ -465,7 +471,7 @@ defmodule Pleroma.Notification do
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
+ when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReact", "Flag"] do
potential_receiver_ap_ids = get_potential_receiver_ap_ids(activity)
potential_receivers =
@@ -501,6 +507,10 @@ defmodule Pleroma.Notification do
[object_id]
end
+ def get_potential_receiver_ap_ids(%{data: %{"type" => "Flag"}}) do
+ User.all_superusers() |> Enum.map(fn user -> user.ap_id end)
+ end
+
def get_potential_receiver_ap_ids(activity) do
[]
|> Utils.maybe_notify_to_recipients(activity)
@@ -634,6 +644,11 @@ defmodule Pleroma.Notification do
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,
@@ -641,4 +656,16 @@ defmodule Pleroma.Notification do
)
|> Repo.one()
end
+
+ @spec mark_context_as_read(User.t(), String.t()) :: {integer(), nil | [term()]}
+ def mark_context_as_read(%User{id: id}, context) do
+ from(
+ n in Notification,
+ join: a in assoc(n, :activity),
+ where: n.user_id == ^id,
+ where: n.seen == false,
+ where: fragment("?->>'context'", a.data) == ^context
+ )
+ |> Repo.update_all(set: [seen: true])
+ end
end
diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex
index 546c4ea01..052ad413b 100644
--- a/lib/pleroma/object.ex
+++ b/lib/pleroma/object.ex
@@ -255,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)))
@@ -281,10 +285,10 @@ defmodule Pleroma.Object 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))
@@ -296,11 +300,8 @@ defmodule Pleroma.Object do
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
diff --git a/lib/pleroma/object/containment.ex b/lib/pleroma/object/containment.ex
index 99608b8a5..29cb3d672 100644
--- a/lib/pleroma/object/containment.ex
+++ b/lib/pleroma/object/containment.ex
@@ -44,18 +44,11 @@ defmodule Pleroma.Object.Containment 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.
- if Mix.env() == :test do
- defp compare_uris(_, %URI{scheme: "tag"}), do: :ok
- end
-
defp compare_uris(%URI{host: host} = _id_uri, %URI{host: host} = _other_uri), do: :ok
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 e74c87269..20d8f687d 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,38 @@ 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 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 +69,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}
@@ -80,8 +98,8 @@ defmodule Pleroma.Object.Fetcher do
{:containment, _} ->
{:error, "Object containment failed."}
- {:transmogrifier, {:error, {:reject, nil}}} ->
- {:reject, nil}
+ {:transmogrifier, {:error, {:reject, e}}} ->
+ {:reject, e}
{:transmogrifier, _} = e ->
{:error, e}
@@ -106,8 +124,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
@@ -145,12 +163,12 @@ defmodule Pleroma.Object.Fetcher do
date: date
})
- [{"signature", signature}]
+ {"signature", signature}
end
defp sign_fetch(headers, id, date) do
if Pleroma.Config.get([:activitypub, :sign_object_fetches]) do
- headers ++ make_signature(id, date)
+ [make_signature(id, date) | headers]
else
headers
end
@@ -158,15 +176,41 @@ defmodule Pleroma.Object.Fetcher do
defp maybe_date_fetch(headers, date) do
if Pleroma.Config.get([:activitypub, :sign_object_fetches]) do
- headers ++ [{"date", date}]
+ [{"date", date} | headers]
else
headers
end
end
+ def fetch_and_contain_remote_object_from_id(id)
+
+ def fetch_and_contain_remote_object_from_id(%{"id" => id}),
+ do: fetch_and_contain_remote_object_from_id(id)
+
def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do
Logger.debug("Fetching object #{id} via AP")
+ with {:scheme, true} <- {:scheme, String.starts_with?(id, "http")},
+ {:ok, body} <- get_object(id),
+ {:ok, data} <- safe_json_decode(body),
+ :ok <- Containment.contain_origin_from_id(id, data) do
+ {:ok, data}
+ else
+ {:scheme, _} ->
+ {:error, "Unsupported URI scheme"}
+
+ {:error, e} ->
+ {:error, e}
+
+ e ->
+ {:error, e}
+ end
+ end
+
+ def fetch_and_contain_remote_object_from_id(_id),
+ do: {:error, "id must be a string"}
+
+ defp get_object(id) do
date = Pleroma.Signature.signed_date()
headers =
@@ -174,20 +218,29 @@ defmodule Pleroma.Object.Fetcher do
|> maybe_date_fetch(date)
|> sign_fetch(id, date)
- Logger.debug("Fetch headers: #{inspect(headers)}")
+ case HTTP.get(id, headers) do
+ {:ok, %{body: body, status: code, headers: headers}} when code in 200..299 ->
+ case List.keyfind(headers, "content-type", 0) do
+ {_, content_type} ->
+ case Plug.Conn.Utils.media_type(content_type) do
+ {:ok, "application", "activity+json", _} ->
+ {:ok, body}
+
+ {:ok, "application", "ld+json",
+ %{"profile" => "https://www.w3.org/ns/activitystreams"}} ->
+ {:ok, body}
+
+ _ ->
+ {:error, {:content_type, content_type}}
+ end
+
+ _ ->
+ {:error, {:content_type, nil}}
+ end
- with {:scheme, true} <- {:scheme, String.starts_with?(id, "http")},
- {:ok, %{body: body, status: code}} when code in 200..299 <- HTTP.get(id, headers),
- {:ok, data} <- Jason.decode(body),
- :ok <- Containment.contain_origin_from_id(id, data) do
- {:ok, data}
- else
{:ok, %{status: code}} when code in [404, 410] ->
{:error, "Object has been deleted"}
- {:scheme, _} ->
- {:error, "Unsupported URI scheme"}
-
{:error, e} ->
{:error, e}
@@ -196,8 +249,6 @@ defmodule Pleroma.Object.Fetcher do
end
end
- def fetch_and_contain_remote_object_from_id(%{"id" => id}),
- do: fetch_and_contain_remote_object_from_id(id)
-
- def fetch_and_contain_remote_object_from_id(_id), do: {:error, "id must be a string"}
+ defp safe_json_decode(nil), do: {:ok, nil}
+ defp safe_json_decode(json), do: Jason.decode(json)
end
diff --git a/lib/pleroma/password_reset_token.ex b/lib/pleroma/password_reset_token.ex
index 787bd4781..fea5b1c22 100644
--- a/lib/pleroma/password_reset_token.ex
+++ b/lib/pleroma/password_reset_token.ex
@@ -40,6 +40,7 @@ defmodule Pleroma.PasswordResetToken do
@spec reset_password(binary(), map()) :: {:ok, User.t()} | {:error, binary()}
def reset_password(token, data) do
with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}),
+ false <- expired?(token),
%User{} = user <- User.get_cached_by_id(token.user_id),
{:ok, _user} <- User.reset_password(user, data),
{:ok, token} <- Repo.update(used_changeset(token)) do
@@ -48,4 +49,14 @@ defmodule Pleroma.PasswordResetToken do
_e -> {:error, token}
end
end
+
+ def expired?(%__MODULE__{inserted_at: inserted_at}) do
+ validity = Pleroma.Config.get([:instance, :password_reset_token_validity], 0)
+
+ now = NaiveDateTime.utc_now()
+
+ difference = NaiveDateTime.diff(now, inserted_at)
+
+ difference > validity
+ end
end
diff --git a/lib/pleroma/plugs/remote_ip.ex b/lib/pleroma/plugs/remote_ip.ex
deleted file mode 100644
index 2eca4f8f6..000000000
--- a/lib/pleroma/plugs/remote_ip.ex
+++ /dev/null
@@ -1,51 +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.RemoteIp do
- @moduledoc """
- This is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration.
- """
-
- @behaviour Plug
-
- @headers ~w[
- x-forwarded-for
- ]
-
- # https://en.wikipedia.org/wiki/Localhost
- # https://en.wikipedia.org/wiki/Private_network
- @reserved ~w[
- 127.0.0.0/8
- ::1/128
- fc00::/7
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
- ]
-
- def init(_), do: nil
-
- def call(conn, _) do
- config = Pleroma.Config.get(__MODULE__, [])
-
- if Keyword.get(config, :enabled, false) do
- RemoteIp.call(conn, remote_ip_opts(config))
- else
- conn
- end
- end
-
- defp remote_ip_opts(config) do
- headers = config |> Keyword.get(:headers, @headers) |> MapSet.new()
- reserved = Keyword.get(config, :reserved, @reserved)
-
- proxies =
- config
- |> Keyword.get(:proxies, [])
- |> Enum.concat(reserved)
- |> Enum.map(&InetCidr.parse/1)
-
- {headers, proxies}
- end
-end
diff --git a/lib/pleroma/repo.ex b/lib/pleroma/repo.ex
index f317e4d58..4524bd5e2 100644
--- a/lib/pleroma/repo.ex
+++ b/lib/pleroma/repo.ex
@@ -49,7 +49,21 @@ defmodule Pleroma.Repo do
end
end
- def chunk_stream(query, chunk_size) do
+ @doc """
+ Returns a lazy enumerable that emits all entries from the data store matching the given query.
+
+ `returns_as` use to group records. use the `batches` option to fetch records in bulk.
+
+ ## Examples
+
+ # fetch records one-by-one
+ iex> Pleroma.Repo.chunk_stream(Pleroma.Activity.Queries.by_actor(ap_id), 500)
+
+ # fetch records in bulk
+ iex> Pleroma.Repo.chunk_stream(Pleroma.Activity.Queries.by_actor(ap_id), 500, :batches)
+ """
+ @spec chunk_stream(Ecto.Query.t(), integer(), atom()) :: Enumerable.t()
+ def chunk_stream(query, chunk_size, returns_as \\ :one) 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
@@ -69,7 +83,12 @@ defmodule Pleroma.Repo do
records ->
last_id = List.last(records).id
- {records, last_id}
+
+ if returns_as == :one do
+ {records, last_id}
+ else
+ {[records], last_id}
+ end
end
end,
fn _ -> :ok end
diff --git a/lib/pleroma/repo_streamer.ex b/lib/pleroma/repo_streamer.ex
deleted file mode 100644
index cb4d7bb7a..000000000
--- a/lib/pleroma/repo_streamer.ex
+++ /dev/null
@@ -1,34 +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.RepoStreamer do
- alias Pleroma.Repo
- import Ecto.Query
-
- def chunk_stream(query, chunk_size) do
- Stream.unfold(0, fn
- :halt ->
- {[], :halt}
-
- last_id ->
- query
- |> order_by(asc: :id)
- |> where([r], r.id > ^last_id)
- |> limit(^chunk_size)
- |> Repo.all()
- |> case do
- [] ->
- {[], :halt}
-
- records ->
- last_id = List.last(records).id
- {records, last_id}
- end
- end)
- |> Stream.take_while(fn
- [] -> false
- _ -> true
- end)
- end
-end
diff --git a/lib/pleroma/reverse_proxy/reverse_proxy.ex b/lib/pleroma/reverse_proxy.ex
index 0de4e2309..8ae1157df 100644
--- a/lib/pleroma/reverse_proxy/reverse_proxy.ex
+++ b/lib/pleroma/reverse_proxy.ex
@@ -17,6 +17,9 @@ defmodule Pleroma.ReverseProxy do
@failed_request_ttl :timer.seconds(60)
@methods ~w(GET HEAD)
+ def max_read_duration_default, do: @max_read_duration
+ def default_cache_control_header, do: @default_cache_control_header
+
@moduledoc """
A reverse proxy.
@@ -391,6 +394,8 @@ defmodule Pleroma.ReverseProxy do
defp body_size_constraint(_, _), do: :ok
+ defp check_read_duration(nil = _duration, max), do: check_read_duration(@max_read_duration, max)
+
defp check_read_duration(duration, max)
when is_integer(duration) and is_integer(max) and max > 0 do
if duration > max do
diff --git a/lib/pleroma/reverse_proxy/client/hackney.ex b/lib/pleroma/reverse_proxy/client/hackney.ex
index e84118a90..ad988fac3 100644
--- a/lib/pleroma/reverse_proxy/client/hackney.ex
+++ b/lib/pleroma/reverse_proxy/client/hackney.ex
@@ -7,6 +7,7 @@ defmodule Pleroma.ReverseProxy.Client.Hackney do
@impl true
def request(method, url, headers, body, opts \\ []) do
+ opts = Keyword.put(opts, :ssl_options, versions: [:"tlsv1.2", :"tlsv1.1", :tlsv1])
:hackney.request(method, url, headers, body, opts)
end
diff --git a/lib/pleroma/reverse_proxy/client/tesla.ex b/lib/pleroma/reverse_proxy/client/tesla.ex
index d5a339681..4b118eec2 100644
--- a/lib/pleroma/reverse_proxy/client/tesla.ex
+++ b/lib/pleroma/reverse_proxy/client/tesla.ex
@@ -28,7 +28,7 @@ defmodule Pleroma.ReverseProxy.Client.Tesla do
url,
body,
headers,
- Keyword.put(opts, :adapter, opts)
+ opts
) do
if is_map(response.body) and method != :head do
{:ok, response.status, response.headers, response.body}
diff --git a/lib/pleroma/stats.ex b/lib/pleroma/stats.ex
index 9a03f01db..48afe901e 100644
--- a/lib/pleroma/stats.ex
+++ b/lib/pleroma/stats.ex
@@ -3,12 +3,15 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Stats do
+ use GenServer
+
import Ecto.Query
+
alias Pleroma.CounterCache
alias Pleroma.Repo
alias Pleroma.User
- use GenServer
+ @interval :timer.seconds(60)
def start_link(_) do
GenServer.start_link(
@@ -18,18 +21,22 @@ defmodule Pleroma.Stats do
)
end
+ @impl true
+ def init(_args) do
+ {:ok, nil, {:continue, :calculate_stats}}
+ end
+
@doc "Performs update stats"
def force_update do
GenServer.call(__MODULE__, :force_update)
end
- @doc "Performs collect stats"
- def do_collect do
- GenServer.cast(__MODULE__, :run_update)
- end
-
@doc "Returns stats data"
- @spec get_stats() :: %{domain_count: integer(), status_count: integer(), user_count: integer()}
+ @spec get_stats() :: %{
+ domain_count: non_neg_integer(),
+ status_count: non_neg_integer(),
+ user_count: non_neg_integer()
+ }
def get_stats do
%{stats: stats} = GenServer.call(__MODULE__, :get_state)
@@ -44,25 +51,14 @@ defmodule Pleroma.Stats do
peers
end
- def init(_args) do
- {:ok, calculate_stat_data()}
- end
-
- def handle_call(:force_update, _from, _state) do
- new_stats = calculate_stat_data()
- {:reply, new_stats, new_stats}
- end
-
- def handle_call(:get_state, _from, state) do
- {:reply, state, state}
- end
-
- def handle_cast(:run_update, _state) do
- new_stats = calculate_stat_data()
-
- {:noreply, new_stats}
- end
-
+ @spec calculate_stat_data() :: %{
+ peers: list(),
+ stats: %{
+ domain_count: non_neg_integer(),
+ status_count: non_neg_integer(),
+ user_count: non_neg_integer()
+ }
+ }
def calculate_stat_data do
peers =
from(
@@ -97,6 +93,7 @@ defmodule Pleroma.Stats do
}
end
+ @spec get_status_visibility_count(String.t() | nil) :: map()
def get_status_visibility_count(instance \\ nil) do
if is_nil(instance) do
CounterCache.get_sum()
@@ -104,4 +101,33 @@ defmodule Pleroma.Stats do
CounterCache.get_by_instance(instance)
end
end
+
+ @impl true
+ def handle_continue(:calculate_stats, _) do
+ stats = calculate_stat_data()
+
+ unless Pleroma.Config.get(:env) == :test do
+ Process.send_after(self(), :run_update, @interval)
+ end
+
+ {:noreply, stats}
+ end
+
+ @impl true
+ def handle_call(:force_update, _from, _state) do
+ new_stats = calculate_stat_data()
+ {:reply, new_stats, new_stats}
+ end
+
+ @impl true
+ def handle_call(:get_state, _from, state) do
+ {:reply, state, state}
+ end
+
+ @impl true
+ def handle_info(:run_update, _) do
+ new_stats = calculate_stat_data()
+ Process.send_after(self(), :run_update, @interval)
+ {:noreply, new_stats}
+ end
end
diff --git a/lib/pleroma/telemetry/logger.ex b/lib/pleroma/telemetry/logger.ex
index 4cacae02f..003079cf3 100644
--- a/lib/pleroma/telemetry/logger.ex
+++ b/lib/pleroma/telemetry/logger.ex
@@ -1,3 +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.Telemetry.Logger do
@moduledoc "Transforms Pleroma telemetry events to logs"
@@ -7,7 +11,8 @@ defmodule Pleroma.Telemetry.Logger do
[:pleroma, :connection_pool, :reclaim, :start],
[:pleroma, :connection_pool, :reclaim, :stop],
[:pleroma, :connection_pool, :provision_failure],
- [:pleroma, :connection_pool, :client_death]
+ [:pleroma, :connection_pool, :client, :dead],
+ [:pleroma, :connection_pool, :client, :add]
]
def attach do
:telemetry.attach_many("pleroma-logger", @events, &handle_event/4, [])
@@ -62,7 +67,7 @@ defmodule Pleroma.Telemetry.Logger do
end
def handle_event(
- [:pleroma, :connection_pool, :client_death],
+ [:pleroma, :connection_pool, :client, :dead],
%{client_pid: client_pid, reason: reason},
%{key: key},
_
@@ -73,4 +78,17 @@ defmodule Pleroma.Telemetry.Logger do
}"
end)
end
+
+ def handle_event(
+ [:pleroma, :connection_pool, :client, :add],
+ %{clients: [_, _ | _] = clients},
+ %{key: key, protocol: :http},
+ _
+ ) do
+ Logger.info(fn ->
+ "Pool worker for #{key}: #{length(clients)} clients are using an HTTP1 connection at the same time, head-of-line blocking might occur."
+ end)
+ end
+
+ def handle_event([:pleroma, :connection_pool, :client, :add], _, _, _), do: :ok
end
diff --git a/lib/pleroma/tesla/middleware/connection_pool.ex b/lib/pleroma/tesla/middleware/connection_pool.ex
new file mode 100644
index 000000000..056e736ce
--- /dev/null
+++ b/lib/pleroma/tesla/middleware/connection_pool.ex
@@ -0,0 +1,50 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Tesla.Middleware.ConnectionPool do
+ @moduledoc """
+ Middleware to get/release connections from `Pleroma.Gun.ConnectionPool`
+ """
+
+ @behaviour Tesla.Middleware
+
+ alias Pleroma.Gun.ConnectionPool
+
+ @impl Tesla.Middleware
+ def call(%Tesla.Env{url: url, opts: opts} = env, next, _) do
+ uri = URI.parse(url)
+
+ # Avoid leaking connections when the middleware is called twice
+ # with body_as: :chunks. We assume only the middleware can set
+ # opts[:adapter][:conn]
+ if opts[:adapter][:conn] do
+ ConnectionPool.release_conn(opts[:adapter][:conn])
+ end
+
+ case ConnectionPool.get_conn(uri, opts[:adapter]) do
+ {:ok, conn_pid} ->
+ adapter_opts = Keyword.merge(opts[:adapter], conn: conn_pid, close_conn: false)
+ opts = Keyword.put(opts, :adapter, adapter_opts)
+ env = %{env | opts: opts}
+
+ case Tesla.run(env, next) do
+ {:ok, env} ->
+ unless opts[:adapter][:body_as] == :chunks do
+ ConnectionPool.release_conn(conn_pid)
+ {_, res} = pop_in(env.opts[:adapter][:conn])
+ {:ok, res}
+ else
+ {:ok, env}
+ end
+
+ err ->
+ ConnectionPool.release_conn(conn_pid)
+ err
+ end
+
+ err ->
+ err
+ end
+ end
+end
diff --git a/lib/pleroma/tesla/middleware/follow_redirects.ex b/lib/pleroma/tesla/middleware/follow_redirects.ex
deleted file mode 100644
index 5a7032215..000000000
--- a/lib/pleroma/tesla/middleware/follow_redirects.ex
+++ /dev/null
@@ -1,110 +0,0 @@
-# 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
index fb04411d9..b30d83567 100644
--- a/lib/pleroma/tests/auth_test_controller.ex
+++ b/lib/pleroma/tests/auth_test_controller.ex
@@ -8,9 +8,9 @@ defmodule Pleroma.Tests.AuthTestController do
use Pleroma.Web, :controller
- alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
- alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
+ alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
# Serves only with proper OAuth token (:api and :authenticated_api)
# Skipping EnsurePublicOrAuthenticatedPlug has no effect in this case
diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex
index 0fa6b89dc..db2cc1dae 100644
--- a/lib/pleroma/upload.ex
+++ b/lib/pleroma/upload.ex
@@ -56,14 +56,24 @@ 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()}
+ @doc "Store a file. If using a `Plug.Upload{}` as the source, be sure to use `Majic.Plug` to ensure its content_type and filename is correct."
def store(upload, opts \\ []) do
opts = get_opts(opts)
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 = Map.get(opts, :description) || upload.name,
+ description = get_description(opts, upload),
{_, true} <-
{:description_limit,
String.length(description) <= Pleroma.Config.get([:instance, :description_limit])},
@@ -130,14 +140,13 @@ defmodule Pleroma.Upload do
end
defp prepare_upload(%Plug.Upload{} = file, opts) do
- with :ok <- check_file_size(file.path, opts.size_limit),
- {:ok, content_type, name} <- Pleroma.MIME.file_mime_type(file.path, file.filename) do
+ with :ok <- check_file_size(file.path, opts.size_limit) do
{:ok,
%__MODULE__{
id: UUID.generate(),
- name: name,
+ name: file.filename,
tempfile: file.path,
- content_type: content_type
+ content_type: file.content_type
}}
end
end
@@ -145,16 +154,17 @@ defmodule Pleroma.Upload 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)))
+ hash = Base.encode16(:crypto.hash(:sha256, data), lower: true)
with :ok <- check_binary_size(data, opts.size_limit),
tmp_path <- tempfile_for_image(data),
- {:ok, content_type, name} <-
- Pleroma.MIME.bin_mime_type(data, hash <> "." <> parsed["filetype"]) do
+ {:ok, %{mime_type: content_type}} <-
+ Majic.perform({:bytes, data}, pool: Pleroma.MajicPool),
+ [ext | _] <- MIME.extensions(content_type) do
{:ok,
%__MODULE__{
id: UUID.generate(),
- name: name,
+ name: hash <> "." <> ext,
tempfile: tmp_path,
content_type: content_type
}}
@@ -163,7 +173,7 @@ defmodule Pleroma.Upload do
# For Mix.Tasks.MigrateLocalUploads
defp prepare_upload(%__MODULE__{tempfile: path} = upload, _opts) do
- with {:ok, content_type} <- Pleroma.MIME.file_mime_type(path) do
+ with {:ok, %{mime_type: content_type}} <- Majic.perform(path, pool: Pleroma.MajicPool) do
{:ok, %__MODULE__{upload | content_type: content_type}}
end
end
diff --git a/lib/pleroma/upload/filter.ex b/lib/pleroma/upload/filter.ex
index dbdadc97f..661135634 100644
--- a/lib/pleroma/upload/filter.ex
+++ b/lib/pleroma/upload/filter.ex
@@ -15,7 +15,11 @@ defmodule Pleroma.Upload.Filter do
require Logger
- @callback filter(Pleroma.Upload.t()) :: :ok | {:ok, Pleroma.Upload.t()} | {:error, any()}
+ @callback filter(Pleroma.Upload.t()) ::
+ {:ok, :filtered}
+ | {:ok, :noop}
+ | {:ok, :filtered, Pleroma.Upload.t()}
+ | {:error, any()}
@spec filter([module()], Pleroma.Upload.t()) :: {:ok, Pleroma.Upload.t()} | {:error, any()}
@@ -25,10 +29,13 @@ defmodule Pleroma.Upload.Filter do
def filter([filter | rest], upload) do
case filter.filter(upload) do
- :ok ->
+ {:ok, :filtered} ->
filter(rest, upload)
- {:ok, upload} ->
+ {:ok, :filtered, upload} ->
+ filter(rest, upload)
+
+ {:ok, :noop} ->
filter(rest, upload)
error ->
diff --git a/lib/pleroma/upload/filter/anonymize_filename.ex b/lib/pleroma/upload/filter/anonymize_filename.ex
index 07ead8203..fc456e4f4 100644
--- a/lib/pleroma/upload/filter/anonymize_filename.ex
+++ b/lib/pleroma/upload/filter/anonymize_filename.ex
@@ -16,9 +16,11 @@ defmodule Pleroma.Upload.Filter.AnonymizeFilename do
def filter(%Upload{name: name} = upload) do
extension = List.last(String.split(name, "."))
name = predefined_name(extension) || random(extension)
- {:ok, %Upload{upload | name: name}}
+ {:ok, :filtered, %Upload{upload | name: name}}
end
+ def filter(_), do: {:ok, :noop}
+
@spec predefined_name(String.t()) :: String.t() | nil
defp predefined_name(extension) do
with name when not is_nil(name) <- Config.get([__MODULE__, :text]),
diff --git a/lib/pleroma/upload/filter/dedupe.ex b/lib/pleroma/upload/filter/dedupe.ex
index 41218a918..86cbc8996 100644
--- a/lib/pleroma/upload/filter/dedupe.ex
+++ b/lib/pleroma/upload/filter/dedupe.ex
@@ -17,8 +17,8 @@ defmodule Pleroma.Upload.Filter.Dedupe do
|> Base.encode16(case: :lower)
filename = shasum <> "." <> extension
- {:ok, %Upload{upload | id: shasum, path: filename}}
+ {:ok, :filtered, %Upload{upload | id: shasum, path: filename}}
end
- def filter(_), do: :ok
+ def filter(_), do: {:ok, :noop}
end
diff --git a/lib/pleroma/upload/filter/exiftool.ex b/lib/pleroma/upload/filter/exiftool.ex
index c7fb6aefa..1fd0cfdaa 100644
--- a/lib/pleroma/upload/filter/exiftool.ex
+++ b/lib/pleroma/upload/filter/exiftool.ex
@@ -9,10 +9,22 @@ defmodule Pleroma.Upload.Filter.Exiftool do
"""
@behaviour Pleroma.Upload.Filter
+ @spec filter(Pleroma.Upload.t()) :: {:ok, any()} | {:error, String.t()}
+
+ # webp is not compatible with exiftool at this time
+ def filter(%Pleroma.Upload{content_type: "image/webp"}), do: {:ok, :noop}
+
def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do
- System.cmd("exiftool", ["-overwrite_original", "-gps:all=", file], parallelism: true)
- :ok
+ try do
+ case System.cmd("exiftool", ["-overwrite_original", "-gps:all=", file], parallelism: true) do
+ {_response, 0} -> {:ok, :filtered}
+ {error, 1} -> {:error, error}
+ end
+ rescue
+ _e in ErlangError ->
+ {:error, "exiftool command not found"}
+ end
end
- def filter(_), do: :ok
+ def filter(_), do: {:ok, :noop}
end
diff --git a/lib/pleroma/upload/filter/mogrifun.ex b/lib/pleroma/upload/filter/mogrifun.ex
index 7d95577a4..363e5cf0f 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,11 +38,16 @@ defmodule Pleroma.Upload.Filter.Mogrifun do
[{"fill", "yellow"}, {"tint", "40"}]
]
+ @spec filter(Pleroma.Upload.t()) :: {:ok, atom()} | {: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, :filtered}
+ rescue
+ _e in ErlangError ->
+ {:error, "mogrify command not found"}
+ end
end
- def filter(_), do: :ok
+ def filter(_), do: {:ok, :noop}
end
diff --git a/lib/pleroma/upload/filter/mogrify.ex b/lib/pleroma/upload/filter/mogrify.ex
index 2eb758006..71968fd9c 100644
--- a/lib/pleroma/upload/filter/mogrify.ex
+++ b/lib/pleroma/upload/filter/mogrify.ex
@@ -8,14 +8,18 @@ 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, :atom} | {: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, :filtered}
+ rescue
+ _e in ErlangError ->
+ {:error, "mogrify command not found"}
+ end
end
- def filter(_), do: :ok
+ def filter(_), do: {:ok, :noop}
def do_filter(file, filters) do
file
diff --git a/lib/pleroma/uploaders/s3.ex b/lib/pleroma/uploaders/s3.ex
index a13ff23b6..6dbef9085 100644
--- a/lib/pleroma/uploaders/s3.ex
+++ b/lib/pleroma/uploaders/s3.ex
@@ -46,12 +46,23 @@ defmodule Pleroma.Uploaders.S3 do
op =
if streaming do
- upload.tempfile
- |> ExAws.S3.Upload.stream_file()
- |> ExAws.S3.upload(bucket, s3_name, [
- {:acl, :public_read},
- {:content_type, upload.content_type}
- ])
+ op =
+ upload.tempfile
+ |> ExAws.S3.Upload.stream_file()
+ |> ExAws.S3.upload(bucket, s3_name, [
+ {:acl, :public_read},
+ {:content_type, upload.content_type}
+ ])
+
+ if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Gun do
+ # set s3 upload timeout to respect :upload pool timeout
+ # timeout should be slightly larger, so s3 can retry upload on fail
+ timeout = Pleroma.HTTP.AdapterHelper.Gun.pool_timeout(:upload) + 1_000
+ opts = Keyword.put(op.opts, :timeout, timeout)
+ Map.put(op, :opts, opts)
+ else
+ op
+ end
else
{:ok, file_data} = File.read(upload.tempfile)
diff --git a/lib/pleroma/uploaders/uploader.ex b/lib/pleroma/uploaders/uploader.ex
index 9a94534e9..6249eceb1 100644
--- a/lib/pleroma/uploaders/uploader.ex
+++ b/lib/pleroma/uploaders/uploader.ex
@@ -12,7 +12,7 @@ defmodule Pleroma.Uploaders.Uploader do
@doc """
Instructs how to get the file from the backend.
- Used by `Pleroma.Plugs.UploadedMedia`.
+ Used by `Pleroma.Web.Plugs.UploadedMedia`.
"""
@type get_method :: {:static_dir, directory :: String.t()} | {:url, url :: String.t()}
@callback get_file(file :: String.t()) :: {:ok, get_method()}
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index ed1db04c9..bcd5256c8 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -25,7 +25,6 @@ defmodule Pleroma.User do
alias Pleroma.Object
alias Pleroma.Registration
alias Pleroma.Repo
- alias Pleroma.RepoStreamer
alias Pleroma.User
alias Pleroma.UserRelationship
alias Pleroma.Web
@@ -42,7 +41,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
@@ -78,7 +82,7 @@ defmodule Pleroma.User do
]
schema "users" do
- field(:bio, :string)
+ field(:bio, :string, default: "")
field(:raw_bio, :string)
field(:email, :string)
field(:name, :string)
@@ -103,9 +107,11 @@ defmodule Pleroma.User do
field(:note_count, :integer, default: 0)
field(:follower_count, :integer, default: 0)
field(:following_count, :integer, default: 0)
- field(:locked, :boolean, default: false)
+ field(:is_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: [])
@@ -122,7 +128,6 @@ defmodule Pleroma.User do
field(:hide_followers, :boolean, default: false)
field(:hide_follows, :boolean, default: false)
field(:hide_favorites, :boolean, default: true)
- field(:unread_conversation_count, :integer, default: 0)
field(:pinned_activities, {:array, :string}, default: [])
field(:email_notifications, :map, default: %{"digest" => false})
field(:mascot, :map, default: nil)
@@ -130,7 +135,7 @@ defmodule Pleroma.User do
field(:pleroma_settings_store, :map, default: %{})
field(:fields, {:array, :map}, default: [])
field(:raw_fields, {:array, :map}, default: [])
- field(:discoverable, :boolean, default: false)
+ field(:is_discoverable, :boolean, default: false)
field(:invisible, :boolean, default: false)
field(:allow_following_move, :boolean, default: true)
field(:skip_thread_containment, :boolean, default: false)
@@ -240,6 +245,25 @@ defmodule Pleroma.User do
end
end
+ def cached_blocked_users_ap_ids(user) do
+ Cachex.fetch!(:user_cache, "blocked_users_ap_ids:#{user.ap_id}", fn _ ->
+ blocked_users_ap_ids(user)
+ end)
+ end
+
+ def cached_muted_users_ap_ids(user) do
+ Cachex.fetch!(:user_cache, "muted_users_ap_ids:#{user.ap_id}", fn _ ->
+ muted_users_ap_ids(user)
+ 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>>
@@ -262,8 +286,9 @@ 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{local: true, approval_pending: true}), do: :approval_pending
- def account_status(%User{confirmation_pending: true}) do
+ def account_status(%User{local: true, confirmation_pending: true}) do
if Config.get([:instance, :account_activation_required]) do
:confirmation_pending
else
@@ -303,10 +328,12 @@ defmodule Pleroma.User do
def visible_for(_, _), do: :invisible
- defp restrict_unauthenticated?(%User{local: local}) do
- config_key = if local, do: :local, else: :remote
+ defp restrict_unauthenticated?(%User{local: true}) do
+ Config.restrict_unauthenticated_access?(:profiles, :local)
+ end
- Config.get([:restrict_unauthenticated, :profiles, config_key], false)
+ defp restrict_unauthenticated?(%User{local: _}) do
+ Config.restrict_unauthenticated_access?(:profiles, :remote)
end
defp visible_account_status(user) do
@@ -362,8 +389,6 @@ defmodule Pleroma.User 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))
@@ -412,7 +437,6 @@ defmodule Pleroma.User do
params,
[
:bio,
- :name,
:emoji,
:ap_id,
:inbox,
@@ -422,7 +446,7 @@ defmodule Pleroma.User do
:avatar,
:ap_enabled,
:banner,
- :locked,
+ :is_locked,
:last_refreshed_at,
:uri,
:follower_address,
@@ -434,14 +458,16 @@ defmodule Pleroma.User do
:follower_count,
:fields,
:following_count,
- :discoverable,
+ :is_discoverable,
:invisible,
:actor_type,
:also_known_as,
:accepts_chat_messages
]
)
- |> validate_required([:name, :ap_id])
+ |> cast(params, [:name], empty_values: [])
+ |> validate_required([:ap_id])
+ |> validate_required([:name], trim: false)
|> unique_constraint(:nickname)
|> validate_format(:nickname, @email_regex)
|> validate_length(:bio, max: bio_limit)
@@ -465,7 +491,7 @@ defmodule Pleroma.User do
:public_key,
:inbox,
:shared_inbox,
- :locked,
+ :is_locked,
:no_rich_text,
:default_scope,
:banner,
@@ -481,7 +507,7 @@ defmodule Pleroma.User do
:fields,
:raw_fields,
:pleroma_settings_store,
- :discoverable,
+ :is_discoverable,
:actor_type,
:also_known_as,
:accepts_chat_messages
@@ -630,9 +656,38 @@ 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 = 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? =
@@ -642,8 +697,16 @@ defmodule Pleroma.User do
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?)
+ |> approval_changeset(need_approval: need_approval?)
|> cast(params, [
:bio,
:raw_bio,
@@ -653,17 +716,28 @@ defmodule Pleroma.User do
:password,
:password_confirmation,
:emoji,
- :accepts_chat_messages
+ :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, 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()
@@ -703,6 +777,16 @@ defmodule Pleroma.User do
follow_all(user, autofollowed_users)
end
+ defp autofollowing_users(user) do
+ candidates = Config.get([:instance, :autofollowing_nicknames])
+
+ User.Query.build(%{nickname: candidates, local: true, deactivated: false})
+ |> Repo.all()
+ |> Enum.each(&follow(&1, user, :follow_accept))
+
+ {:ok, :success}
+ end
+
@doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
def register(%Ecto.Changeset{} = changeset) do
with {:ok, user} <- Repo.insert(changeset) do
@@ -712,9 +796,11 @@ defmodule Pleroma.User do
def post_register_action(%User{} = user) do
with {:ok, user} <- autofollow_users(user),
+ {:ok, _} <- autofollowing_users(user),
{:ok, user} <- set_cache(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
@@ -729,6 +815,15 @@ defmodule Pleroma.User do
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 send_welcome_email(%User{email: email} = user) when is_binary(email) do
if User.WelcomeEmail.enabled?() do
User.WelcomeEmail.send_email(user)
@@ -741,7 +836,8 @@ defmodule Pleroma.User do
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
+ def try_send_confirmation_email(%User{confirmation_pending: true, email: email} = user)
+ when is_binary(email) do
if Config.get([:instance, :account_activation_required]) do
send_confirmation_email(user)
{:ok, :enqueued}
@@ -774,7 +870,7 @@ defmodule Pleroma.User do
@spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
# "Locked" (self-locked) users demand explicit authorization of follow requests
- def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do
+ def maybe_direct_follow(%User{} = follower, %User{local: true, is_locked: true} = followed) do
follow(follower, followed, :follow_pending)
end
@@ -800,8 +896,6 @@ 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 = Config.get([:user, :deny_follow_blocked])
@@ -844,9 +938,7 @@ defmodule Pleroma.User do
FollowingRelationship.unfollow(follower, followed)
{:ok, followed} = update_follower_count(followed)
- {:ok, follower} =
- follower
- |> update_following_count()
+ {:ok, follower} = update_following_count(follower)
{:ok, follower, followed}
@@ -855,8 +947,6 @@ defmodule Pleroma.User do
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)
@@ -887,7 +977,7 @@ defmodule Pleroma.User do
end
def locked?(%User{} = user) do
- user.locked || false
+ user.is_locked || false
end
def get_by_id(id) do
@@ -958,6 +1048,8 @@ defmodule Pleroma.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}")
+ Cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
+ Cachex.del(:user_cache, "muted_users_ap_ids:#{user.ap_id}")
end
@spec get_cached_by_ap_id(String.t()) :: User.t() | nil
@@ -1056,31 +1148,31 @@ defmodule Pleroma.User do
User.Query.build(%{followers: user, deactivated: false})
end
- def get_followers_query(user, page) do
+ def get_followers_query(%User{} = user, page) do
user
|> get_followers_query(nil)
|> User.Query.paginate(page, 20)
end
@spec get_followers_query(User.t()) :: Ecto.Query.t()
- def get_followers_query(user), do: get_followers_query(user, nil)
+ def get_followers_query(%User{} = user), do: get_followers_query(user, nil)
@spec get_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
- def get_followers(user, page \\ nil) do
+ def get_followers(%User{} = user, page \\ nil) do
user
|> get_followers_query(page)
|> Repo.all()
end
@spec get_external_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
- def get_external_followers(user, page \\ nil) do
+ def get_external_followers(%User{} = user, page \\ nil) do
user
|> get_followers_query(page)
|> User.Query.build(%{external: true})
|> Repo.all()
end
- def get_followers_ids(user, page \\ nil) do
+ def get_followers_ids(%User{} = user, page \\ nil) do
user
|> get_followers_query(page)
|> select([u], u.id)
@@ -1092,37 +1184,35 @@ defmodule Pleroma.User do
User.Query.build(%{friends: user, deactivated: false})
end
- def get_friends_query(user, page) do
+ def get_friends_query(%User{} = user, page) do
user
|> get_friends_query(nil)
|> User.Query.paginate(page, 20)
end
@spec get_friends_query(User.t()) :: Ecto.Query.t()
- def get_friends_query(user), do: get_friends_query(user, nil)
+ def get_friends_query(%User{} = user), do: get_friends_query(user, nil)
- def get_friends(user, page \\ nil) do
+ def get_friends(%User{} = user, page \\ nil) do
user
|> get_friends_query(page)
|> Repo.all()
end
- def get_friends_ap_ids(user) do
+ def get_friends_ap_ids(%User{} = user) do
user
|> get_friends_query(nil)
|> select([u], u.ap_id)
|> Repo.all()
end
- def get_friends_ids(user, page \\ nil) do
+ def get_friends_ids(%User{} = user, page \\ nil) do
user
|> get_friends_query(page)
|> select([u], u.id)
|> Repo.all()
end
- defdelegate get_follow_requests(user), to: FollowingRelationship
-
def increase_note_count(%User{} = user) do
User
|> where(id: ^user.id)
@@ -1228,47 +1318,6 @@ defmodule Pleroma.User do
|> update_and_set_cache()
end
- def set_unread_conversation_count(%User{local: true} = user) do
- unread_query = Participation.unread_conversation_count_for_user(user)
-
- User
- |> join(:inner, [u], p in subquery(unread_query))
- |> update([u, p],
- set: [unread_conversation_count: p.count]
- )
- |> where([u], u.id == ^user.id)
- |> select([u], u)
- |> Repo.update_all([])
- |> case do
- {1, [user]} -> set_cache(user)
- _ -> {:error, user}
- end
- end
-
- def set_unread_conversation_count(user), do: {:ok, user}
-
- def increment_unread_conversation_count(conversation, %User{local: true} = user) do
- unread_query =
- Participation.unread_conversation_count_for_user(user)
- |> where([p], p.conversation_id == ^conversation.id)
-
- User
- |> join(:inner, [u], p in subquery(unread_query))
- |> update([u, p],
- inc: [unread_conversation_count: 1]
- )
- |> where([u], u.id == ^user.id)
- |> where([u, p], p.count == 0)
- |> select([u], u)
- |> Repo.update_all([])
- |> case do
- {1, [user]} -> set_cache(user)
- _ -> {:error, user}
- end
- end
-
- def increment_unread_conversation_count(_, user), do: {:ok, user}
-
@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)
@@ -1289,14 +1338,51 @@ defmodule Pleroma.User do
|> Repo.all()
end
- @spec mute(User.t(), User.t(), boolean()) ::
+ @spec mute(User.t(), User.t(), map()) ::
{:ok, list(UserRelationship.t())} | {:error, String.t()}
- def mute(%User{} = muter, %User{} = mutee, notifications? \\ true) do
- add_to_mutes(muter, mutee, notifications?)
+ def mute(%User{} = muter, %User{} = mutee, params \\ %{}) do
+ notifications? = Map.get(params, :notifications, true)
+ expires_in = Map.get(params, :expires_in, 0)
+
+ with {:ok, user_mute} <- UserRelationship.create_mute(muter, mutee),
+ {:ok, user_notification_mute} <-
+ (notifications? && UserRelationship.create_notification_mute(muter, mutee)) ||
+ {:ok, nil} do
+ if expires_in > 0 do
+ Pleroma.Workers.MuteExpireWorker.enqueue(
+ "unmute_user",
+ %{"muter_id" => muter.id, "mutee_id" => mutee.id},
+ schedule_in: expires_in
+ )
+ end
+
+ Cachex.del(:user_cache, "muted_users_ap_ids:#{muter.ap_id}")
+
+ {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
+ end
end
def unmute(%User{} = muter, %User{} = mutee) do
- remove_from_mutes(muter, mutee)
+ with {:ok, user_mute} <- UserRelationship.delete_mute(muter, mutee),
+ {:ok, user_notification_mute} <-
+ UserRelationship.delete_notification_mute(muter, mutee) do
+ Cachex.del(:user_cache, "muted_users_ap_ids:#{muter.ap_id}")
+ {:ok, [user_mute, user_notification_mute]}
+ end
+ end
+
+ def unmute(muter_id, mutee_id) do
+ with {:muter, %User{} = muter} <- {:muter, User.get_by_id(muter_id)},
+ {:mutee, %User{} = mutee} <- {:mutee, User.get_by_id(mutee_id)} do
+ unmute(muter, mutee)
+ else
+ {who, result} = error ->
+ Logger.warn(
+ "User.unmute/2 failed. #{who}: #{result}, muter_id: #{muter_id}, mutee_id: #{mutee_id}"
+ )
+
+ {:error, error}
+ end
end
def subscribe(%User{} = subscriber, %User{} = target) do
@@ -1494,6 +1580,19 @@ defmodule Pleroma.User do
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}, [])
@@ -1502,6 +1601,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: "",
+ 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,
+ is_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: [],
+ is_discoverable: false,
+ also_known_as: []
+ })
+ end
+
def delete(users) when is_list(users) do
for user <- users, do: delete(user)
end
@@ -1520,12 +1662,17 @@ defmodule Pleroma.User do
defp delete_or_deactivate(%User{local: true} = user) do
status = account_status(user)
- if status == :confirmation_pending do
- delete_and_invalidate_cache(user)
- else
- user
- |> change(%{deactivated: true, email: nil})
- |> update_and_set_cache()
+ 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
@@ -1558,42 +1705,6 @@ defmodule Pleroma.User do
def perform(:deactivate_async, user, status), do: deactivate(user, status)
- @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
- def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
- when is_list(blocked_identifiers) do
- Enum.map(
- blocked_identifiers,
- fn blocked_identifier ->
- with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
- {:ok, _block} <- CommonAPI.block(blocker, blocked) do
- blocked
- else
- err ->
- Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
- err
- end
- end
- )
- end
-
- def perform(:follow_import, %User{} = follower, followed_identifiers)
- when is_list(followed_identifiers) do
- Enum.map(
- followed_identifiers,
- fn followed_identifier ->
- with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
- {:ok, follower} <- maybe_direct_follow(follower, followed),
- {:ok, _, _, _} <- CommonAPI.follow(follower, followed) do
- followed
- else
- err ->
- Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
- err
- end
- end
- )
- end
-
@spec external_users_query() :: Ecto.Query.t()
def external_users_query do
User.Query.build(%{
@@ -1622,21 +1733,6 @@ defmodule Pleroma.User do
Repo.all(query)
end
- def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
- BackgroundWorker.enqueue("blocks_import", %{
- "blocker_id" => blocker.id,
- "blocked_identifiers" => blocked_identifiers
- })
- end
-
- def follow_import(%User{} = follower, followed_identifiers)
- when is_list(followed_identifiers) do
- BackgroundWorker.enqueue("follow_import", %{
- "follower_id" => follower.id,
- "followed_identifiers" => followed_identifiers
- })
- end
-
def delete_notifications_from_user_activities(%User{ap_id: ap_id}) do
Notification
|> join(:inner, [n], activity in assoc(n, :activity))
@@ -1647,7 +1743,7 @@ defmodule Pleroma.User do
def delete_user_activities(%User{ap_id: ap_id} = user) do
ap_id
|> Activity.Queries.by_actor()
- |> RepoStreamer.chunk_stream(50)
+ |> Repo.chunk_stream(50, :batches)
|> Stream.each(fn activities ->
Enum.each(activities, fn activity -> delete_activity(activity, user) end)
end)
@@ -1995,6 +2091,13 @@ defmodule Pleroma.User do
Enum.map(users, &toggle_confirmation/1)
end
+ @spec need_confirmation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
+ def need_confirmation(%User{} = user, bool) do
+ user
+ |> confirmation_changeset(need_confirmation: bool)
+ |> update_and_set_cache()
+ end
+
def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
mascot
end
@@ -2034,8 +2137,6 @@ 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
@@ -2178,11 +2279,22 @@ 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 = Config.get([:instance, :max_pinned_statuses], 0)
params = %{pinned_activities: user.pinned_activities ++ [id]}
+ # if pinned activity was scheduled for deletion, we remove job
+ if expiration = Pleroma.Workers.PurgeExpiredActivity.get_expiration(id) do
+ Oban.cancel_job(expiration.id)
+ end
+
user
|> cast(params, [:pinned_activities])
|> validate_length(:pinned_activities,
@@ -2195,9 +2307,21 @@ defmodule Pleroma.User do
|> update_and_set_cache()
end
- def remove_pinnned_activity(user, %Pleroma.Activity{id: id}) do
+ def remove_pinnned_activity(user, %Pleroma.Activity{id: id, data: data}) do
params = %{pinned_activities: List.delete(user.pinned_activities, id)}
+ # if pinned activity was scheduled for deletion, we reschedule it for deletion
+ if data["expires_at"] do
+ # MRF.ActivityExpirationPolicy used UTC timestamps for expires_at in original implementation
+ {:ok, expires_at} =
+ data["expires_at"] |> Pleroma.EctoType.ActivityPub.ObjectValidators.DateTime.cast()
+
+ Pleroma.Workers.PurgeExpiredActivity.enqueue(%{
+ activity_id: id,
+ expires_at: expires_at
+ })
+ end
+
user
|> cast(params, [:pinned_activities])
|> update_and_set_cache()
@@ -2238,29 +2362,18 @@ defmodule Pleroma.User do
@spec add_to_block(User.t(), User.t()) ::
{:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
defp add_to_block(%User{} = user, %User{} = blocked) do
- UserRelationship.create_block(user, blocked)
+ with {:ok, relationship} <- UserRelationship.create_block(user, blocked) do
+ Cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
+ {:ok, relationship}
+ end
end
@spec add_to_block(User.t(), User.t()) ::
{:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
defp remove_from_block(%User{} = user, %User{} = blocked) do
- UserRelationship.delete_block(user, blocked)
- end
-
- defp add_to_mutes(%User{} = user, %User{} = muted_user, notifications?) do
- with {:ok, user_mute} <- UserRelationship.create_mute(user, muted_user),
- {:ok, user_notification_mute} <-
- (notifications? && UserRelationship.create_notification_mute(user, muted_user)) ||
- {:ok, nil} do
- {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
- end
- end
-
- defp remove_from_mutes(user, %User{} = muted_user) do
- with {:ok, user_mute} <- UserRelationship.delete_mute(user, muted_user),
- {:ok, user_notification_mute} <-
- UserRelationship.delete_notification_mute(user, muted_user) do
- {:ok, [user_mute, user_notification_mute]}
+ with {:ok, relationship} <- UserRelationship.delete_block(user, blocked) do
+ Cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
+ {:ok, relationship}
end
end
diff --git a/lib/pleroma/user/backup.ex b/lib/pleroma/user/backup.ex
new file mode 100644
index 000000000..a9041fd94
--- /dev/null
+++ b/lib/pleroma/user/backup.ex
@@ -0,0 +1,258 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.User.Backup do
+ use Ecto.Schema
+
+ import Ecto.Changeset
+ import Ecto.Query
+ import Pleroma.Web.Gettext
+
+ require Pleroma.Constants
+
+ alias Pleroma.Activity
+ alias Pleroma.Bookmark
+ alias Pleroma.Repo
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Transmogrifier
+ alias Pleroma.Web.ActivityPub.UserView
+ alias Pleroma.Workers.BackupWorker
+
+ schema "backups" do
+ field(:content_type, :string)
+ field(:file_name, :string)
+ field(:file_size, :integer, default: 0)
+ field(:processed, :boolean, default: false)
+
+ belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
+
+ timestamps()
+ end
+
+ def create(user, admin_id \\ nil) do
+ with :ok <- validate_email_enabled(),
+ :ok <- validate_user_email(user),
+ :ok <- validate_limit(user, admin_id),
+ {:ok, backup} <- user |> new() |> Repo.insert() do
+ BackupWorker.process(backup, admin_id)
+ end
+ end
+
+ def new(user) do
+ rand_str = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false)
+ datetime = Calendar.NaiveDateTime.Format.iso8601_basic(NaiveDateTime.utc_now())
+ name = "archive-#{user.nickname}-#{datetime}-#{rand_str}.zip"
+
+ %__MODULE__{
+ user_id: user.id,
+ content_type: "application/zip",
+ file_name: name
+ }
+ end
+
+ def delete(backup) do
+ uploader = Pleroma.Config.get([Pleroma.Upload, :uploader])
+
+ with :ok <- uploader.delete_file(Path.join("backups", backup.file_name)) do
+ Repo.delete(backup)
+ end
+ end
+
+ defp validate_limit(_user, admin_id) when is_binary(admin_id), do: :ok
+
+ defp validate_limit(user, nil) do
+ case get_last(user.id) do
+ %__MODULE__{inserted_at: inserted_at} ->
+ days = Pleroma.Config.get([__MODULE__, :limit_days])
+ diff = Timex.diff(NaiveDateTime.utc_now(), inserted_at, :days)
+
+ if diff > days do
+ :ok
+ else
+ {:error,
+ dngettext(
+ "errors",
+ "Last export was less than a day ago",
+ "Last export was less than %{days} days ago",
+ days,
+ days: days
+ )}
+ end
+
+ nil ->
+ :ok
+ end
+ end
+
+ defp validate_email_enabled do
+ if Pleroma.Config.get([Pleroma.Emails.Mailer, :enabled]) do
+ :ok
+ else
+ {:error, dgettext("errors", "Backups require enabled email")}
+ end
+ end
+
+ defp validate_user_email(%User{email: nil}) do
+ {:error, dgettext("errors", "Email is required")}
+ end
+
+ defp validate_user_email(%User{email: email}) when is_binary(email), do: :ok
+
+ def get_last(user_id) do
+ __MODULE__
+ |> where(user_id: ^user_id)
+ |> order_by(desc: :id)
+ |> limit(1)
+ |> Repo.one()
+ end
+
+ def list(%User{id: user_id}) do
+ __MODULE__
+ |> where(user_id: ^user_id)
+ |> order_by(desc: :id)
+ |> Repo.all()
+ end
+
+ def remove_outdated(%__MODULE__{id: latest_id, user_id: user_id}) do
+ __MODULE__
+ |> where(user_id: ^user_id)
+ |> where([b], b.id != ^latest_id)
+ |> Repo.all()
+ |> Enum.each(&BackupWorker.delete/1)
+ end
+
+ def get(id), do: Repo.get(__MODULE__, id)
+
+ def process(%__MODULE__{} = backup) do
+ with {:ok, zip_file} <- export(backup),
+ {:ok, %{size: size}} <- File.stat(zip_file),
+ {:ok, _upload} <- upload(backup, zip_file) do
+ backup
+ |> cast(%{file_size: size, processed: true}, [:file_size, :processed])
+ |> Repo.update()
+ end
+ end
+
+ @files ['actor.json', 'outbox.json', 'likes.json', 'bookmarks.json']
+ def export(%__MODULE__{} = backup) do
+ backup = Repo.preload(backup, :user)
+ name = String.trim_trailing(backup.file_name, ".zip")
+ dir = dir(name)
+
+ with :ok <- File.mkdir(dir),
+ :ok <- actor(dir, backup.user),
+ :ok <- statuses(dir, backup.user),
+ :ok <- likes(dir, backup.user),
+ :ok <- bookmarks(dir, backup.user),
+ {:ok, zip_path} <- :zip.create(String.to_charlist(dir <> ".zip"), @files, cwd: dir),
+ {:ok, _} <- File.rm_rf(dir) do
+ {:ok, to_string(zip_path)}
+ end
+ end
+
+ def dir(name) do
+ dir = Pleroma.Config.get([__MODULE__, :dir]) || System.tmp_dir!()
+ Path.join(dir, name)
+ end
+
+ def upload(%__MODULE__{} = backup, zip_path) do
+ uploader = Pleroma.Config.get([Pleroma.Upload, :uploader])
+
+ upload = %Pleroma.Upload{
+ name: backup.file_name,
+ tempfile: zip_path,
+ content_type: backup.content_type,
+ path: Path.join("backups", backup.file_name)
+ }
+
+ with {:ok, _} <- Pleroma.Uploaders.Uploader.put_file(uploader, upload),
+ :ok <- File.rm(zip_path) do
+ {:ok, upload}
+ end
+ end
+
+ defp actor(dir, user) do
+ with {:ok, json} <-
+ UserView.render("user.json", %{user: user})
+ |> Map.merge(%{"likes" => "likes.json", "bookmarks" => "bookmarks.json"})
+ |> Jason.encode() do
+ File.write(Path.join(dir, "actor.json"), json)
+ end
+ end
+
+ defp write_header(file, name) do
+ IO.write(
+ file,
+ """
+ {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "id": "#{name}.json",
+ "type": "OrderedCollection",
+ "orderedItems": [
+
+ """
+ )
+ end
+
+ defp write(query, dir, name, fun) do
+ path = Path.join(dir, "#{name}.json")
+
+ with {:ok, file} <- File.open(path, [:write, :utf8]),
+ :ok <- write_header(file, name) do
+ total =
+ query
+ |> Pleroma.Repo.chunk_stream(100)
+ |> Enum.reduce(0, fn i, acc ->
+ with {:ok, data} <- fun.(i),
+ {:ok, str} <- Jason.encode(data),
+ :ok <- IO.write(file, str <> ",\n") do
+ acc + 1
+ else
+ _ -> acc
+ end
+ end)
+
+ with :ok <- :file.pwrite(file, {:eof, -2}, "\n],\n \"totalItems\": #{total}}") do
+ File.close(file)
+ end
+ end
+ end
+
+ defp bookmarks(dir, %{id: user_id} = _user) do
+ Bookmark
+ |> where(user_id: ^user_id)
+ |> join(:inner, [b], activity in assoc(b, :activity))
+ |> select([b, a], %{id: b.id, object: fragment("(?)->>'object'", a.data)})
+ |> write(dir, "bookmarks", fn a -> {:ok, a.object} end)
+ end
+
+ defp likes(dir, user) do
+ user.ap_id
+ |> Activity.Queries.by_actor()
+ |> Activity.Queries.by_type("Like")
+ |> select([like], %{id: like.id, object: fragment("(?)->>'object'", like.data)})
+ |> write(dir, "likes", fn a -> {:ok, a.object} end)
+ end
+
+ defp statuses(dir, user) do
+ opts =
+ %{}
+ |> Map.put(:type, ["Create", "Announce"])
+ |> Map.put(:actor_id, user.ap_id)
+
+ [
+ [Pleroma.Constants.as_public(), user.ap_id],
+ User.following(user),
+ Pleroma.List.memberships(user)
+ ]
+ |> Enum.concat()
+ |> ActivityPub.fetch_activities_query(opts)
+ |> write(dir, "outbox", fn a ->
+ with {:ok, activity} <- Transmogrifier.prepare_outgoing(a.data) do
+ {:ok, Map.delete(activity, "@context")}
+ end
+ end)
+ end
+end
diff --git a/lib/pleroma/user/import.ex b/lib/pleroma/user/import.ex
new file mode 100644
index 000000000..e458021c8
--- /dev/null
+++ b/lib/pleroma/user/import.ex
@@ -0,0 +1,85 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.User.Import do
+ use Ecto.Schema
+
+ alias Pleroma.User
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Workers.BackgroundWorker
+
+ require Logger
+
+ @spec perform(atom(), User.t(), list()) :: :ok | list() | {:error, any()}
+ def perform(:mutes_import, %User{} = user, [_ | _] = identifiers) do
+ Enum.map(
+ identifiers,
+ fn identifier ->
+ with {:ok, %User{} = muted_user} <- User.get_or_fetch(identifier),
+ {:ok, _} <- User.mute(user, muted_user) do
+ muted_user
+ else
+ error -> handle_error(:mutes_import, identifier, error)
+ end
+ end
+ )
+ end
+
+ def perform(:blocks_import, %User{} = blocker, [_ | _] = identifiers) do
+ Enum.map(
+ identifiers,
+ fn identifier ->
+ with {:ok, %User{} = blocked} <- User.get_or_fetch(identifier),
+ {:ok, _block} <- CommonAPI.block(blocker, blocked) do
+ blocked
+ else
+ error -> handle_error(:blocks_import, identifier, error)
+ end
+ end
+ )
+ end
+
+ def perform(:follow_import, %User{} = follower, [_ | _] = identifiers) do
+ Enum.map(
+ identifiers,
+ fn identifier ->
+ with {:ok, %User{} = followed} <- User.get_or_fetch(identifier),
+ {:ok, follower} <- User.maybe_direct_follow(follower, followed),
+ {:ok, _, _, _} <- CommonAPI.follow(follower, followed) do
+ followed
+ else
+ error -> handle_error(:follow_import, identifier, error)
+ end
+ end
+ )
+ end
+
+ def perform(_, _, _), do: :ok
+
+ defp handle_error(op, user_id, error) do
+ Logger.debug("#{op} failed for #{user_id} with: #{inspect(error)}")
+ error
+ end
+
+ def blocks_import(%User{} = blocker, [_ | _] = identifiers) do
+ BackgroundWorker.enqueue(
+ "blocks_import",
+ %{"user_id" => blocker.id, "identifiers" => identifiers}
+ )
+ end
+
+ def follow_import(%User{} = follower, [_ | _] = identifiers) do
+ BackgroundWorker.enqueue(
+ "follow_import",
+ %{"user_id" => follower.id, "identifiers" => identifiers}
+ )
+ end
+
+ def mutes_import(%User{} = user, [_ | _] = identifiers) do
+ BackgroundWorker.enqueue(
+ "mutes_import",
+ %{"user_id" => user.id, "identifiers" => identifiers}
+ )
+ end
+end
diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex
index 66ffe9090..7ef2a1455 100644
--- a/lib/pleroma/user/query.ex
+++ b/lib/pleroma/user/query.ex
@@ -42,10 +42,13 @@ defmodule Pleroma.User.Query do
external: boolean(),
active: boolean(),
deactivated: boolean(),
+ need_approval: boolean(),
+ unconfirmed: boolean(),
is_admin: boolean(),
is_moderator: boolean(),
super_users: boolean(),
invisible: boolean(),
+ internal: boolean(),
followers: User.t(),
friends: User.t(),
recipients_from_activity: [String.t()],
@@ -53,7 +56,8 @@ defmodule Pleroma.User.Query do
ap_id: [String.t()],
order_by: term(),
select: term(),
- limit: pos_integer()
+ limit: pos_integer(),
+ actor_types: [String.t()]
}
| map()
@@ -79,7 +83,9 @@ defmodule Pleroma.User.Query do
end
defp prepare_query(query, criteria) do
- Enum.reduce(criteria, query, &compose_query/2)
+ criteria
+ |> Map.put_new(:internal, false)
+ |> Enum.reduce(query, &compose_query/2)
end
defp compose_query({key, value}, query)
@@ -106,12 +112,16 @@ defmodule Pleroma.User.Query do
where(query, [u], fragment("? && ?", u.tags, ^tags))
end
- defp compose_query({:is_admin, _}, query) do
- where(query, [u], u.is_admin)
+ defp compose_query({:is_admin, bool}, query) do
+ where(query, [u], u.is_admin == ^bool)
end
- defp compose_query({:is_moderator, _}, query) do
- where(query, [u], u.is_moderator)
+ defp compose_query({:actor_types, actor_types}, query) when is_list(actor_types) do
+ where(query, [u], u.actor_type in ^actor_types)
+ end
+
+ defp compose_query({:is_moderator, bool}, query) do
+ where(query, [u], u.is_moderator == ^bool)
end
defp compose_query({:super_users, _}, query) do
@@ -128,13 +138,12 @@ 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
query
|> where([u], fragment("not (?->'deactivated' @> 'true')", u.info))
- |> where([u], not is_nil(u.nickname))
end
defp compose_query({:deactivated, false}, query) do
@@ -143,7 +152,18 @@ defmodule Pleroma.User.Query do
defp compose_query({:deactivated, true}, query) do
where(query, [u], u.deactivated == ^true)
- |> where([u], not is_nil(u.nickname))
+ end
+
+ defp compose_query({:confirmation_pending, bool}, query) do
+ where(query, [u], u.confirmation_pending == ^bool)
+ end
+
+ defp compose_query({:need_approval, _}, query) do
+ where(query, [u], u.approval_pending)
+ end
+
+ defp compose_query({:unconfirmed, _}, query) do
+ where(query, [u], u.confirmation_pending)
end
defp compose_query({:followers, %User{id: id}}, query) do
@@ -193,10 +213,15 @@ defmodule Pleroma.User.Query do
limit(query, ^limit)
end
+ defp compose_query({:internal, false}, query) do
+ query
+ |> where([u], not is_nil(u.nickname))
+ |> where([u], not like(u.nickname, "internal.%"))
+ end
+
defp compose_query(_unsupported_param, query), do: query
defp location_query(query, local) do
where(query, [u], u.local == ^local)
- |> where([u], not is_nil(u.nickname))
end
end
diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex
index d4fd31069..f1761ef03 100644
--- a/lib/pleroma/user/search.ex
+++ b/lib/pleroma/user/search.ex
@@ -3,8 +3,10 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.User.Search do
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators.Uri, as: UriType
alias Pleroma.Pagination
alias Pleroma.User
+
import Ecto.Query
@limit 20
@@ -19,16 +21,47 @@ defmodule Pleroma.User.Search do
query_string = format_query(query_string)
- maybe_resolve(resolve, for_user, query_string)
+ # If this returns anything, it should bounce to the top
+ maybe_resolved = maybe_resolve(resolve, for_user, query_string)
+
+ top_user_ids =
+ []
+ |> maybe_add_resolved(maybe_resolved)
+ |> maybe_add_ap_id_match(query_string)
+ |> maybe_add_uri_match(query_string)
results =
query_string
- |> search_query(for_user, following)
+ |> search_query(for_user, following, top_user_ids)
|> Pagination.fetch_paginated(%{"offset" => offset, "limit" => result_limit}, :offset)
results
end
+ defp maybe_add_resolved(list, {:ok, %User{} = user}) do
+ [user.id | list]
+ end
+
+ defp maybe_add_resolved(list, _), do: list
+
+ defp maybe_add_ap_id_match(list, query) do
+ if user = User.get_cached_by_ap_id(query) do
+ [user.id | list]
+ else
+ list
+ end
+ end
+
+ defp maybe_add_uri_match(list, query) do
+ with {:ok, query} <- UriType.cast(query),
+ q = from(u in User, where: u.uri == ^query, select: u.id),
+ users = Pleroma.Repo.all(q) do
+ users ++ list
+ else
+ _ -> list
+ end
+ end
+
defp format_query(query_string) do
# Strip the beginning @ off if there is a query
query_string = String.trim_leading(query_string, "@")
@@ -47,7 +80,7 @@ defmodule Pleroma.User.Search do
end
end
- defp search_query(query_string, for_user, following) do
+ defp search_query(query_string, for_user, following, top_user_ids) do
for_user
|> base_query(following)
|> filter_blocked_user(for_user)
@@ -55,13 +88,20 @@ defmodule Pleroma.User.Search do
|> filter_internal_users()
|> filter_blocked_domains(for_user)
|> fts_search(query_string)
+ |> select_top_users(top_user_ids)
|> trigram_rank(query_string)
- |> boost_search_rank(for_user)
+ |> boost_search_rank(for_user, top_user_ids)
|> subquery()
|> order_by(desc: :search_rank)
|> maybe_restrict_local(for_user)
end
+ defp select_top_users(query, top_user_ids) do
+ from(u in query,
+ or_where: u.id in ^top_user_ids
+ )
+ end
+
defp fts_search(query, query_string) do
query_string = to_tsquery(query_string)
@@ -115,8 +155,8 @@ defmodule Pleroma.User.Search do
)
end
- defp base_query(_user, false), do: User
- defp base_query(user, true), do: User.get_followers_query(user)
+ defp base_query(%User{} = user, true), do: User.get_friends_query(user)
+ defp base_query(_user, _following), do: User
defp filter_invisible_users(query) do
from(q in query, where: q.invisible == false)
@@ -175,7 +215,7 @@ defmodule Pleroma.User.Search do
defp local_domain, do: Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host])
- defp boost_search_rank(query, %User{} = for_user) do
+ defp boost_search_rank(query, %User{} = for_user, top_user_ids) do
friends_ids = User.get_friends_ids(for_user)
followers_ids = User.get_followers_ids(for_user)
@@ -187,6 +227,7 @@ defmodule Pleroma.User.Search do
CASE WHEN (?) THEN (?) * 1.5
WHEN (?) THEN (?) * 1.3
WHEN (?) THEN (?) * 1.1
+ WHEN (?) THEN 9001
ELSE (?) END
""",
u.id in ^friends_ids and u.id in ^followers_ids,
@@ -195,11 +236,26 @@ defmodule Pleroma.User.Search do
u.search_rank,
u.id in ^followers_ids,
u.search_rank,
+ u.id in ^top_user_ids,
u.search_rank
)
}
)
end
- defp boost_search_rank(query, _for_user), do: query
+ defp boost_search_rank(query, _for_user, top_user_ids) do
+ from(u in subquery(query),
+ select_merge: %{
+ search_rank:
+ fragment(
+ """
+ CASE WHEN (?) THEN 9001
+ ELSE (?) END
+ """,
+ u.id in ^top_user_ids,
+ u.search_rank
+ )
+ }
+ )
+ end
end
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/utils.ex b/lib/pleroma/utils.ex
index 6b8e3accf..e95766223 100644
--- a/lib/pleroma/utils.ex
+++ b/lib/pleroma/utils.ex
@@ -9,4 +9,39 @@ 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
+
+ @doc "creates the uniq temporary directory"
+ @spec tmp_dir(String.t()) :: {:ok, String.t()} | {:error, :file.posix()}
+ def tmp_dir(prefix \\ "") do
+ sub_dir =
+ [
+ prefix,
+ Timex.to_unix(Timex.now()),
+ :os.getpid(),
+ String.downcase(Integer.to_string(:rand.uniform(0x100000000), 36))
+ ]
+ |> Enum.join("-")
+
+ tmp_dir = Path.join(System.tmp_dir!(), sub_dir)
+
+ case File.mkdir(tmp_dir) do
+ :ok -> {:ok, tmp_dir}
+ error -> error
+ end
+ end
end
diff --git a/lib/pleroma/web/web.ex b/lib/pleroma/web.ex
index 4f9281851..6ed19d3dd 100644
--- a/lib/pleroma/web/web.ex
+++ b/lib/pleroma/web.ex
@@ -2,11 +2,6 @@
# 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,
@@ -25,12 +20,12 @@ 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
+ alias Pleroma.Web.Plugs.EnsureAuthenticatedPlug
+ alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug
+ alias Pleroma.Web.Plugs.ExpectAuthenticatedCheckPlug
+ alias Pleroma.Web.Plugs.ExpectPublicOrAuthenticatedCheckPlug
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+ alias Pleroma.Web.Plugs.PlugHelper
def controller do
quote do
@@ -177,7 +172,7 @@ defmodule Pleroma.Web do
def channel do
quote do
# credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
- use Phoenix.Channel
+ import Phoenix.Channel
import Pleroma.Web.Gettext
end
end
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index a4db1d87c..1c91bc074 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -5,7 +5,6 @@
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
@@ -66,7 +65,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp check_remote_limit(_), do: true
- defp increase_note_count_if_public(actor, object) do
+ def increase_note_count_if_public(actor, object) do
if is_public?(object), do: User.increase_note_count(actor), else: {:ok, actor}
end
@@ -85,17 +84,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp increase_replies_count_if_reply(_create_data), do: :noop
- defp increase_poll_votes_if_vote(%{
- "object" => %{"inReplyTo" => reply_ap_id, "name" => name},
- "type" => "Create",
- "actor" => actor
- }) do
- Object.increase_vote_count(reply_ap_id, name, actor)
- end
-
- defp increase_poll_votes_if_vote(_create_data), do: :noop
-
- @object_types ["ChatMessage"]
+ @object_types ~w[ChatMessage Question Answer Audio Video Event Article]
@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
@@ -112,7 +101,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
local: local,
recipients: recipients,
actor: object["actor"]
- }) do
+ }),
+ # TODO: add tests for expired activities, when Note type will be supported in new pipeline
+ {:ok, _} <- maybe_create_activity_expiration(activity) do
{:ok, activity, meta}
end
end
@@ -121,33 +112,35 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when is_map(map) do
with nil <- Activity.normalize(map),
map <- lazy_put_activity_defaults(map, fake),
- true <- bypass_actor_check || check_actor_is_active(map["actor"]),
- {_, true} <- {:remote_limit_error, check_remote_limit(map)},
+ {_, true} <- {:actor_check, bypass_actor_check || check_actor_is_active(map["actor"])},
+ {_, true} <- {:remote_limit_pass, check_remote_limit(map)},
{:ok, map} <- MRF.filter(map),
{recipients, _, _} = get_recipients(map),
{:fake, false, map, recipients} <- {:fake, fake, map, recipients},
{:containment, :ok} <- {:containment, Containment.contain_child(map)},
- {:ok, map, object} <- insert_full_object(map) do
- {:ok, activity} =
- %Activity{
- data: map,
- local: local,
- actor: map["actor"],
- recipients: recipients
- }
- |> Repo.insert()
- |> maybe_create_activity_expiration()
-
+ {:ok, map, object} <- insert_full_object(map),
+ {:ok, activity} <- insert_activity_with_expiration(map, local, recipients) do
# Splice in the child object if we have one.
activity = Maps.put_if_present(activity, :object, object)
- BackgroundWorker.enqueue("fetch_data_for_activity", %{"activity_id" => activity.id})
+ ConcurrentLimiter.limit(Pleroma.Web.RichMedia.Helpers, fn ->
+ Task.start(fn -> Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) end)
+ end)
{:ok, activity}
else
%Activity{} = activity ->
{:ok, activity}
+ {:actor_check, _} ->
+ {:error, false}
+
+ {:containment, _} = error ->
+ error
+
+ {:error, _} = error ->
+ error
+
{:fake, true, map, recipients} ->
activity = %Activity{
data: map,
@@ -160,8 +153,24 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
{:ok, activity}
- error ->
- {:error, error}
+ {:remote_limit_pass, _} ->
+ {:error, :remote_limit}
+
+ {:reject, _} = e ->
+ {:error, e}
+ end
+ end
+
+ defp insert_activity_with_expiration(data, local, recipients) do
+ struct = %Activity{
+ data: data,
+ local: local,
+ actor: data["actor"],
+ recipients: recipients
+ }
+
+ with {:ok, activity} <- Repo.insert(struct) do
+ maybe_create_activity_expiration(activity)
end
end
@@ -174,13 +183,19 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
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
+ defp maybe_create_activity_expiration(
+ %{data: %{"expires_at" => %DateTime{} = expires_at}} = activity
+ ) do
+ with {:ok, _job} <-
+ Pleroma.Workers.PurgeExpiredActivity.enqueue(%{
+ activity_id: activity.id,
+ expires_at: expires_at
+ }) do
{:ok, activity}
end
end
- defp maybe_create_activity_expiration(result), do: result
+ defp maybe_create_activity_expiration(activity), do: {:ok, activity}
defp create_or_bump_conversation(activity, actor) do
with {:ok, conversation} <- Conversation.create_or_bump_for(activity),
@@ -258,7 +273,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
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),
@@ -296,32 +310,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
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()}
- defp 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)
-
- data =
- %{"to" => to, "type" => type, "actor" => actor.ap_id, "object" => object}
- |> Maps.put_if_present("id", activity_id)
-
- with {:ok, activity} <- insert(data, local),
- _ <- notify_and_stream(activity),
- :ok <- maybe_federate(activity) do
- {:ok, activity}
- end
- end
-
@spec unfollow(User.t(), User.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t()} | nil | {:error, any()}
def unfollow(follower, followed, activity_id \\ nil, local \\ true) do
@@ -346,15 +334,21 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
@spec flag(map()) :: {:ok, Activity.t()} | {:error, any()}
- def flag(
- %{
- actor: actor,
- context: _context,
- account: account,
- statuses: statuses,
- content: content
- } = params
- ) do
+ def flag(params) do
+ with {:ok, result} <- Repo.transaction(fn -> do_flag(params) end) do
+ result
+ end
+ end
+
+ defp do_flag(
+ %{
+ actor: actor,
+ context: _context,
+ account: account,
+ statuses: statuses,
+ content: content
+ } = params
+ ) do
# only accept false as false value
local = !(params[:local] == false)
forward = !(params[:forward] == false)
@@ -372,7 +366,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
{:ok, activity} <- insert(flag_data, local),
{:ok, stripped_activity} <- strip_report_status_data(activity),
_ <- notify_and_stream(activity),
- :ok <- maybe_federate(stripped_activity) do
+ :ok <-
+ maybe_federate(stripped_activity) do
User.all_superusers()
|> Enum.filter(fn user -> not is_nil(user.email) end)
|> Enum.each(fn superuser ->
@@ -382,6 +377,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end)
{:ok, activity}
+ else
+ {:error, error} -> Repo.rollback(error)
end
end
@@ -781,7 +778,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
defp restrict_replies(query, %{
- reply_filtering_user: user,
+ reply_filtering_user: %User{} = user,
reply_visibility: "self"
}) do
from(
@@ -797,14 +794,24 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
defp restrict_replies(query, %{
- reply_filtering_user: user,
+ reply_filtering_user: %User{} = user,
reply_visibility: "following"
}) do
from(
[activity, object] in query,
where:
fragment(
- "?->>'inReplyTo' is null OR ? && array_remove(?, ?) OR ? = ?",
+ """
+ ?->>'type' != 'Create' -- This isn't a Create
+ OR ?->>'inReplyTo' is null -- this isn't a reply
+ OR ? && array_remove(?, ?) -- The recipient is us or one of our friends,
+ -- unless they are the author (because authors
+ -- are also part of the recipients). This leads
+ -- to a bug that self-replies by friends won't
+ -- show up.
+ OR ? = ? -- The actor is us
+ """,
+ activity.data,
object.data,
^[user.ap_id | User.get_cached_user_friends_ap_ids(user)],
activity.recipients,
@@ -831,7 +838,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
query =
from([activity] in query,
where: fragment("not (? = ANY(?))", activity.actor, ^mutes),
- where: fragment("not (?->'to' \\?| ?)", activity.data, ^mutes)
+ where:
+ fragment(
+ "not (?->'to' \\?| ?) or ? = ?",
+ activity.data,
+ ^mutes,
+ activity.actor,
+ ^user.ap_id
+ )
)
unless opts[:skip_preload] do
@@ -855,7 +869,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
from(
[activity, object: o] in query,
where: fragment("not (? = ANY(?))", activity.actor, ^blocked_ap_ids),
- where: fragment("not (? && ?)", activity.recipients, ^blocked_ap_ids),
+ where:
+ fragment(
+ "((not (? && ?)) or ? = ?)",
+ activity.recipients,
+ ^blocked_ap_ids,
+ activity.actor,
+ ^user.ap_id
+ ),
where:
fragment(
"recipients_contain_blocked_domains(?, ?) = false",
@@ -927,16 +948,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_muted_reblogs(query, _), do: query
- defp restrict_instance(query, %{instance: instance}) do
- users =
- from(
- u in User,
- select: u.ap_id,
- where: fragment("? LIKE ?", u.nickname, ^"%@#{instance}")
- )
- |> Repo.all()
-
- from(activity in query, where: activity.actor in ^users)
+ defp restrict_instance(query, %{instance: instance}) when is_binary(instance) do
+ from(
+ activity in query,
+ where: fragment("split_part(actor::text, '/'::text, 3) = ?", ^instance)
+ )
end
defp restrict_instance(query, _), do: query
@@ -1225,11 +1241,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
{String.trim(name, ":"), url}
end)
- locked = data["manuallyApprovesFollowers"] || false
+ is_locked = data["manuallyApprovesFollowers"] || false
capabilities = data["capabilities"] || %{}
accepts_chat_messages = capabilities["acceptsChatMessages"]
data = Transmogrifier.maybe_fix_user_object(data)
- discoverable = data["discoverable"] || false
+ is_discoverable = data["discoverable"] || false
invisible = data["invisible"] || false
actor_type = data["type"] || "Person"
@@ -1254,14 +1270,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
banner: banner,
fields: fields,
emoji: emojis,
- locked: locked,
- discoverable: discoverable,
+ is_locked: is_locked,
+ is_discoverable: is_discoverable,
invisible: invisible,
avatar: avatar,
name: data["name"],
follower_address: data["followers"],
following_address: data["following"],
- bio: data["summary"],
+ bio: data["summary"] || "",
actor_type: actor_type,
also_known_as: Map.get(data, "alsoKnownAs", []),
public_key: public_key,
@@ -1366,6 +1382,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
{:ok, data} <- user_data_from_user_object(data) do
{:ok, maybe_update_follow_information(data)}
else
+ # If this has been deleted, only log a debug and not an error
{:error, "Object has been deleted" = e} ->
Logger.debug("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
{:error, e}
@@ -1381,9 +1398,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
def maybe_handle_clashing_nickname(data) do
- nickname = data[:nickname]
-
- with %User{} = old_user <- User.get_by_nickname(nickname),
+ 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 #{
@@ -1397,7 +1413,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
else
{:ap_id_comparison, true} ->
Logger.info(
- "Found an old user for #{nickname}, but the ap id #{data[:ap_id]} is the same as the new user. Race condition? Not changing anything."
+ "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."
)
_ ->
diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
index 220c4fe52..7e5647f8f 100644
--- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
@@ -9,7 +9,6 @@ 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
@@ -23,8 +22,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.ControllerHelper
alias Pleroma.Web.Endpoint
- alias Pleroma.Web.FederatingPlug
alias Pleroma.Web.Federator
+ alias Pleroma.Web.Plugs.EnsureAuthenticatedPlug
+ alias Pleroma.Web.Plugs.FederatingPlug
require Logger
@@ -45,8 +45,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
when action in [:read_inbox, :update_outbox, :whoami, :upload_media]
)
+ plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:upload_media])
+
plug(
- Pleroma.Plugs.Cache,
+ Pleroma.Web.Plugs.Cache,
[query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2]
when action in [:activity, :object]
)
@@ -80,7 +82,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
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
+ {_, true} <- {:public?, Visibility.is_public?(object)},
+ {_, false} <- {:local?, Visibility.is_local_public?(object)} do
conn
|> assign(:tracking_fun_data, object.id)
|> set_cache_ttl_for(object)
@@ -90,6 +93,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
else
{:public?, false} ->
{:error, :not_found}
+
+ {:local?, true} ->
+ {:error, :not_found}
end
end
@@ -106,7 +112,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
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
+ {_, true} <- {:public?, Visibility.is_public?(activity)},
+ {_, false} <- {:local?, Visibility.is_local_public?(activity)} do
conn
|> maybe_set_tracking_data(activity)
|> set_cache_ttl_for(activity)
@@ -115,6 +122,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|> render("object.json", object: activity)
else
{:public?, false} -> {:error, :not_found}
+ {:local?, true} -> {:error, :not_found}
nil -> {:error, :not_found}
end
end
@@ -399,21 +407,30 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
defp handle_user_activity(
%User{} = user,
- %{"type" => "Create", "object" => %{"type" => "Note"}} = params
+ %{"type" => "Create", "object" => %{"type" => "Note"} = object} = params
) do
- object =
- params["object"]
- |> Map.merge(Map.take(params, ["to", "cc"]))
- |> Map.put("attributedTo", user.ap_id())
- |> Transmogrifier.fix_object()
-
- ActivityPub.create(%{
- to: params["to"],
- actor: user,
- context: object["context"],
- object: object,
- additional: Map.take(params, ["cc"])
- })
+ content = if is_binary(object["content"]), do: object["content"], else: ""
+ name = if is_binary(object["name"]), do: object["name"], else: ""
+ summary = if is_binary(object["summary"]), do: object["summary"], else: ""
+ length = String.length(content <> name <> summary)
+
+ if length > Pleroma.Config.get([:instance, :limit]) do
+ {:error, dgettext("errors", "Note is over the character limit")}
+ else
+ object =
+ object
+ |> Map.merge(Map.take(params, ["to", "cc"]))
+ |> Map.put("attributedTo", user.ap_id)
+ |> Transmogrifier.fix_object()
+
+ ActivityPub.create(%{
+ to: params["to"],
+ actor: user,
+ context: object["context"],
+ object: object,
+ additional: Map.take(params, ["cc"])
+ })
+ end
end
defp handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do
@@ -447,7 +464,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
%{assigns: %{user: %User{nickname: nickname} = user}} = conn,
%{"nickname" => nickname} = params
) do
- actor = user.ap_id()
+ actor = user.ap_id
params =
params
@@ -514,19 +531,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
{new_user, for_user}
end
- @doc """
- Endpoint based on <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>
-
- Parameters:
- - (required) `file`: data of the media
- - (optionnal) `description`: description of the media, intended for accessibility
-
- 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{} = user}} = conn, %{"file" => file} = data) do
with {:ok, object} <-
ActivityPub.upload(
diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex
index d5f3610ed..e99f6fd83 100644
--- a/lib/pleroma/web/activity_pub/builder.ex
+++ b/lib/pleroma/web/activity_pub/builder.ex
@@ -1,3 +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.ActivityPub.Builder do
@moduledoc """
This module builds the objects. Meant to be used for creating local objects.
@@ -14,6 +18,28 @@ defmodule Pleroma.Web.ActivityPub.Builder do
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 = %{
@@ -80,6 +106,13 @@ defmodule Pleroma.Web.ActivityPub.Builder do
end
def create(actor, object, recipients) do
+ context =
+ if is_map(object) do
+ object["context"]
+ else
+ nil
+ end
+
{:ok,
%{
"id" => Utils.generate_activity_id(),
@@ -88,7 +121,8 @@ defmodule Pleroma.Web.ActivityPub.Builder do
"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
@@ -115,6 +149,22 @@ defmodule Pleroma.Web.ActivityPub.Builder do
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,
@@ -169,9 +219,12 @@ defmodule Pleroma.Web.ActivityPub.Builder do
to =
cond do
- actor.ap_id == Relay.relay_ap_id() ->
+ actor.ap_id == Relay.ap_id() ->
[actor.follower_address]
+ public? and Visibility.is_local_public?(object) ->
+ [actor.follower_address, object.data["actor"], Pleroma.Constants.as_local_public()]
+
public? ->
[actor.follower_address, object.data["actor"], Pleroma.Constants.as_public()]
diff --git a/lib/pleroma/web/activity_pub/mrf.ex b/lib/pleroma/web/activity_pub/mrf.ex
index 206d6af52..6e73b2f22 100644
--- a/lib/pleroma/web/activity_pub/mrf.ex
+++ b/lib/pleroma/web/activity_pub/mrf.ex
@@ -3,18 +3,91 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF do
+ require Logger
+
+ @mrf_config_descriptions [
+ %{
+ group: :pleroma,
+ key: :mrf,
+ tab: :mrf,
+ label: "MRF",
+ type: :group,
+ description: "General MRF settings",
+ children: [
+ %{
+ key: :policies,
+ type: [:module, {:list, :module}],
+ description:
+ "A list of MRF policies enabled. Module names are shortened (removed leading `Pleroma.Web.ActivityPub.MRF.` part), but on adding custom module you need to use full name.",
+ suggestions: {:list_behaviour_implementations, Pleroma.Web.ActivityPub.MRF}
+ },
+ %{
+ key: :transparency,
+ label: "MRF transparency",
+ type: :boolean,
+ description:
+ "Make the content of your Message Rewrite Facility settings public (via nodeinfo)"
+ },
+ %{
+ key: :transparency_exclusions,
+ label: "MRF transparency exclusions",
+ type: {:list, :string},
+ description:
+ "Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.",
+ suggestions: [
+ "exclusion.com"
+ ]
+ }
+ ]
+ }
+ ]
+
+ @default_description %{
+ label: "",
+ description: ""
+ }
+
+ @required_description_keys [:key, :related_policy]
+
@callback filter(Map.t()) :: {:ok | :reject, Map.t()}
+ @callback describe() :: {:ok | :error, Map.t()}
+ @callback config_description() :: %{
+ optional(:children) => [map()],
+ key: atom(),
+ related_policy: String.t(),
+ label: String.t(),
+ description: String.t()
+ }
+ @optional_callbacks config_description: 0
- def filter(policies, %{} = object) do
+ def filter(policies, %{} = message) do
policies
- |> Enum.reduce({:ok, object}, fn
- policy, {:ok, object} -> policy.filter(object)
+ |> Enum.reduce({:ok, message}, fn
+ policy, {:ok, message} -> policy.filter(message)
_, error -> error
end)
end
def filter(%{} = object), do: get_policies() |> filter(object)
+ def pipeline_filter(%{} = message, meta) do
+ object = meta[:object_data]
+ ap_id = message["object"]
+
+ if object && ap_id do
+ with {:ok, message} <- filter(Map.put(message, "object", object)) do
+ meta = Keyword.put(meta, :object_data, message["object"])
+ {:ok, Map.put(message, "object", ap_id), meta}
+ else
+ {err, message} -> {err, message, meta}
+ end
+ else
+ {err, message} = filter(message)
+
+ {err, message, meta}
+ end
+ end
+
def get_policies do
Pleroma.Config.get([:mrf, :policies], []) |> get_policies()
end
@@ -33,8 +106,6 @@ defmodule Pleroma.Web.ActivityPub.MRF do
Enum.any?(domains, fn domain -> Regex.match?(domain, host) end)
end
- @callback describe() :: {:ok | :error, Map.t()}
-
def describe(policies) do
{:ok, policy_configs} =
policies
@@ -64,4 +135,41 @@ defmodule Pleroma.Web.ActivityPub.MRF do
end
def describe, do: get_policies() |> describe()
+
+ def config_descriptions do
+ Pleroma.Web.ActivityPub.MRF
+ |> Pleroma.Docs.Generator.list_behaviour_implementations()
+ |> config_descriptions()
+ end
+
+ def config_descriptions(policies) do
+ Enum.reduce(policies, @mrf_config_descriptions, fn policy, acc ->
+ if function_exported?(policy, :config_description, 0) do
+ description =
+ @default_description
+ |> Map.merge(policy.config_description)
+ |> Map.put(:group, :pleroma)
+ |> Map.put(:tab, :mrf)
+ |> Map.put(:type, :group)
+
+ if Enum.all?(@required_description_keys, &Map.has_key?(description, &1)) do
+ [description | acc]
+ else
+ Logger.warn(
+ "#{policy} config description doesn't have one or all required keys #{
+ inspect(@required_description_keys)
+ }"
+ )
+
+ acc
+ end
+ else
+ Logger.debug(
+ "#{policy} is excluded from config descriptions, because does not implement `config_description/0` method."
+ )
+
+ acc
+ end
+ end)
+ end
end
diff --git a/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex b/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex
index 8e47f1e02..655a2ced0 100644
--- a/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex
@@ -21,8 +21,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy do
@impl true
def describe, do: {:ok, %{}}
- defp local?(%{"id" => id}) do
- String.starts_with?(id, Pleroma.Web.Endpoint.url())
+ defp local?(%{"actor" => actor}) do
+ String.starts_with?(actor, Pleroma.Web.Endpoint.url())
end
defp note?(activity) do
@@ -31,13 +31,31 @@ defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy do
defp maybe_add_expiration(activity) do
days = Pleroma.Config.get([:mrf_activity_expiration, :days], 365)
- expires_at = NaiveDateTime.utc_now() |> Timex.shift(days: days)
+ expires_at = DateTime.utc_now() |> Timex.shift(days: days)
with %{"expires_at" => existing_expires_at} <- activity,
- :lt <- NaiveDateTime.compare(existing_expires_at, expires_at) do
+ :lt <- DateTime.compare(existing_expires_at, expires_at) do
activity
else
_ -> Map.put(activity, "expires_at", expires_at)
end
end
+
+ @impl true
+ def config_description do
+ %{
+ key: :mrf_activity_expiration,
+ related_policy: "Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy",
+ label: "MRF Activity Expiration Policy",
+ description: "Adds automatic expiration to all local activities",
+ children: [
+ %{
+ key: :days,
+ type: :integer,
+ description: "Default global expiration time for all local activities (in days)",
+ suggestions: [90, 365]
+ }
+ ]
+ }
+ 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/force_bot_unlisted_policy.ex b/lib/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy.ex
new file mode 100644
index 000000000..ea9c3d3f5
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy.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.MRF.ForceBotUnlistedPolicy do
+ alias Pleroma.User
+ @behaviour Pleroma.Web.ActivityPub.MRF
+ @moduledoc "Remove bot posts from federated timeline"
+
+ require Pleroma.Constants
+
+ defp check_by_actor_type(user), do: user.actor_type in ["Application", "Service"]
+ defp check_by_nickname(user), do: Regex.match?(~r/bot@|ebooks@/i, user.nickname)
+
+ defp check_if_bot(user), do: check_by_actor_type(user) or check_by_nickname(user)
+
+ @impl true
+ def filter(
+ %{
+ "type" => "Create",
+ "to" => to,
+ "cc" => cc,
+ "actor" => actor,
+ "object" => object
+ } = message
+ ) do
+ user = User.get_cached_by_ap_id(actor)
+ isbot = check_if_bot(user)
+
+ if isbot and Enum.member?(to, Pleroma.Constants.as_public()) do
+ to = List.delete(to, Pleroma.Constants.as_public()) ++ [user.follower_address]
+ cc = List.delete(cc, user.follower_address) ++ [Pleroma.Constants.as_public()]
+
+ object =
+ object
+ |> Map.put("to", to)
+ |> Map.put("cc", cc)
+
+ message =
+ message
+ |> Map.put("to", to)
+ |> Map.put("cc", cc)
+ |> Map.put("object", object)
+
+ {:ok, message}
+ else
+ {:ok, message}
+ end
+ end
+
+ @impl true
+ def filter(message), do: {:ok, message}
+
+ @impl true
+ def describe, do: {:ok, %{}}
+end
diff --git a/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex b/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex
index 9ba07b4e3..3fd5c1e0a 100644
--- a/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex
@@ -97,4 +97,31 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do
@impl true
def describe,
do: {:ok, %{mrf_hellthread: Pleroma.Config.get(:mrf_hellthread) |> Enum.into(%{})}}
+
+ @impl true
+ def config_description do
+ %{
+ key: :mrf_hellthread,
+ related_policy: "Pleroma.Web.ActivityPub.MRF.HellthreadPolicy",
+ label: "MRF Hellthread",
+ description: "Block messages with excessive user mentions",
+ children: [
+ %{
+ key: :delist_threshold,
+ type: :integer,
+ description:
+ "Number of mentioned users after which the message gets removed from timelines and" <>
+ "disables notifications. Set to 0 to disable.",
+ suggestions: [10]
+ },
+ %{
+ key: :reject_threshold,
+ type: :integer,
+ description:
+ "Number of mentioned users after which the messaged gets rejected. Set to 0 to disable.",
+ suggestions: [20]
+ }
+ ]
+ }
+ 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 15e09dcf0..ded0fe7f2 100644
--- a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex
@@ -20,9 +20,17 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
String.match?(string, pattern)
end
- defp check_reject(%{"object" => %{"content" => content, "summary" => summary}} = message) do
+ defp object_payload(%{} = object) do
+ [object["content"], object["summary"], object["name"]]
+ |> Enum.filter(& &1)
+ |> Enum.join("\n")
+ end
+
+ defp check_reject(%{"object" => %{} = object} = message) do
+ payload = object_payload(object)
+
if Enum.any?(Pleroma.Config.get([:mrf_keyword, :reject]), fn pattern ->
- string_matches?(content, pattern) or string_matches?(summary, pattern)
+ string_matches?(payload, pattern)
end) do
{:reject, "[KeywordPolicy] Matches with rejected keyword"}
else
@@ -30,12 +38,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
end
end
- defp check_ftl_removal(
- %{"to" => to, "object" => %{"content" => content, "summary" => summary}} = message
- ) do
+ defp check_ftl_removal(%{"to" => to, "object" => %{} = object} = message) do
+ payload = object_payload(object)
+
if Pleroma.Constants.as_public() in to and
Enum.any?(Pleroma.Config.get([:mrf_keyword, :federated_timeline_removal]), fn pattern ->
- string_matches?(content, pattern) or string_matches?(summary, pattern)
+ string_matches?(payload, pattern)
end) do
to = List.delete(to, Pleroma.Constants.as_public())
cc = [Pleroma.Constants.as_public() | message["cc"] || []]
@@ -51,35 +59,24 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
end
end
- defp check_replace(%{"object" => %{"content" => content, "summary" => summary}} = message) do
- content =
- if is_binary(content) do
- content
- else
- ""
- end
-
- summary =
- if is_binary(summary) do
- summary
- else
- ""
- end
-
- {content, summary} =
- Enum.reduce(
- Pleroma.Config.get([:mrf_keyword, :replace]),
- {content, summary},
- fn {pattern, replacement}, {content_acc, summary_acc} ->
- {String.replace(content_acc, pattern, replacement),
- String.replace(summary_acc, pattern, replacement)}
- end
- )
-
- {:ok,
- message
- |> put_in(["object", "content"], content)
- |> put_in(["object", "summary"], summary)}
+ defp check_replace(%{"object" => %{} = object} = message) do
+ object =
+ ["content", "name", "summary"]
+ |> Enum.filter(fn field -> Map.has_key?(object, field) && object[field] end)
+ |> Enum.reduce(object, fn field, object ->
+ data =
+ Enum.reduce(
+ Pleroma.Config.get([:mrf_keyword, :replace]),
+ object[field],
+ fn {pat, repl}, acc -> String.replace(acc, pat, repl) end
+ )
+
+ Map.put(object, field, data)
+ end)
+
+ message = Map.put(message, "object", object)
+
+ {:ok, message}
end
@impl true
@@ -129,4 +126,46 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
{:ok, %{mrf_keyword: mrf_keyword}}
end
+
+ @impl true
+ def config_description do
+ %{
+ key: :mrf_keyword,
+ related_policy: "Pleroma.Web.ActivityPub.MRF.KeywordPolicy",
+ label: "MRF Keyword",
+ description:
+ "Reject or Word-Replace messages matching a keyword or [Regex](https://hexdocs.pm/elixir/Regex.html).",
+ children: [
+ %{
+ key: :reject,
+ type: {:list, :string},
+ description: """
+ A list of patterns which result in message being rejected.
+
+ Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
+ """,
+ suggestions: ["foo", ~r/foo/iu]
+ },
+ %{
+ key: :federated_timeline_removal,
+ type: {:list, :string},
+ description: """
+ A list of patterns which result in message being removed from federated timelines (a.k.a unlisted).
+
+ Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
+ """,
+ suggestions: ["foo", ~r/foo/iu]
+ },
+ %{
+ key: :replace,
+ type: {:list, :tuple},
+ description: """
+ **Pattern**: a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
+
+ **Replacement**: a string. Leaving the field empty is permitted.
+ """
+ }
+ ]
+ }
+ 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 dfab105a3..816cc89bf 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
@@ -8,36 +8,41 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do
alias Pleroma.HTTP
alias Pleroma.Web.MediaProxy
- alias Pleroma.Workers.BackgroundWorker
require Logger
- @options [
- pool: :media
+ @adapter_options [
+ pool: :media,
+ recv_timeout: 10_000
]
- def perform(:prefetch, url) do
- Logger.debug("Prefetching #{inspect(url)}")
+ defp prefetch(url) do
+ # Fetching only proxiable resources
+ if MediaProxy.enabled?() and MediaProxy.url_proxiable?(url) do
+ # If preview proxy is enabled, it'll also hit media proxy (so we're caching both requests)
+ prefetch_url = MediaProxy.preview_url(url)
- opts =
- if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do
- Keyword.put(@options, :recv_timeout, 10_000)
+ Logger.debug("Prefetching #{inspect(url)} as #{inspect(prefetch_url)}")
+
+ if Pleroma.Config.get(:env) == :test do
+ fetch(prefetch_url)
else
- @options
+ ConcurrentLimiter.limit(MediaProxy, fn ->
+ Task.start(fn -> fetch(prefetch_url) end)
+ end)
end
-
- url
- |> MediaProxy.url()
- |> HTTP.get([], adapter: opts)
+ end
end
- def perform(:preload, %{"object" => %{"attachment" => attachments}} = _message) do
+ defp fetch(url), do: HTTP.get(url, [], @adapter_options)
+
+ defp preload(%{"object" => %{"attachment" => attachments}} = _message) do
Enum.each(attachments, fn
%{"url" => url} when is_list(url) ->
url
|> Enum.each(fn
%{"href" => href} ->
- BackgroundWorker.enqueue("media_proxy_prefetch", %{"url" => href})
+ prefetch(href)
x ->
Logger.debug("Unhandled attachment URL object #{inspect(x)}")
@@ -53,7 +58,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do
%{"type" => "Create", "object" => %{"attachment" => attachments} = _object} = message
)
when is_list(attachments) and length(attachments) > 0 do
- BackgroundWorker.enqueue("media_proxy_preload", %{"message" => message})
+ preload(message)
{:ok, message}
end
diff --git a/lib/pleroma/web/activity_pub/mrf/mention_policy.ex b/lib/pleroma/web/activity_pub/mrf/mention_policy.ex
index 7910ca131..9c096712a 100644
--- a/lib/pleroma/web/activity_pub/mrf/mention_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/mention_policy.ex
@@ -25,4 +25,22 @@ defmodule Pleroma.Web.ActivityPub.MRF.MentionPolicy do
@impl true
def describe, do: {:ok, %{}}
+
+ @impl true
+ def config_description do
+ %{
+ key: :mrf_mention,
+ related_policy: "Pleroma.Web.ActivityPub.MRF.MentionPolicy",
+ label: "MRF Mention",
+ description: "Block messages which mention a specific user",
+ children: [
+ %{
+ key: :actors,
+ type: {:list, :string},
+ description: "A list of actors for which any post mentioning them will be dropped",
+ suggestions: ["actor1", "actor2"]
+ }
+ ]
+ }
+ end
end
diff --git a/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex b/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex
index 7abae37ae..e00575c2a 100644
--- a/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex
+++ b/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex
@@ -8,6 +8,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkup do
@behaviour Pleroma.Web.ActivityPub.MRF
+ @impl true
def filter(%{"type" => "Create", "object" => child_object} = object) do
scrub_policy = Pleroma.Config.get([:mrf_normalize_markup, :scrub_policy])
@@ -22,5 +23,23 @@ defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkup do
def filter(object), do: {:ok, object}
+ @impl true
def describe, do: {:ok, %{}}
+
+ @impl true
+ def config_description do
+ %{
+ key: :mrf_normalize_markup,
+ related_policy: "Pleroma.Web.ActivityPub.MRF.NormalizeMarkup",
+ label: "MRF Normalize Markup",
+ description: "MRF NormalizeMarkup settings. Scrub configured hypertext markup.",
+ children: [
+ %{
+ key: :scrub_policy,
+ type: :module,
+ suggestions: [Pleroma.HTML.Scrubber.Default]
+ }
+ ]
+ }
+ end
end
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 5f111c72f..eb0481f20 100644
--- a/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex
@@ -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
@@ -58,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
@@ -101,4 +106,32 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
{:ok, %{mrf_object_age: mrf_object_age}}
end
+
+ @impl true
+ def config_description do
+ %{
+ key: :mrf_object_age,
+ related_policy: "Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy",
+ label: "MRF Object Age",
+ description:
+ "Rejects or delists posts based on their timestamp deviance from your server's clock.",
+ children: [
+ %{
+ key: :threshold,
+ type: :integer,
+ description: "Required age (in seconds) of a post before actions are taken.",
+ suggestions: [172_800]
+ },
+ %{
+ key: :actions,
+ type: {:list, :atom},
+ description:
+ "A list of actions to apply to the post. `:delist` removes the post from public timelines; " <>
+ "`:strip_followers` removes followers from the ActivityPub recipient list ensuring they won't be delivered to home timelines; " <>
+ "`:reject` rejects the message entirely",
+ suggestions: [:delist, :strip_followers, :reject]
+ }
+ ]
+ }
+ end
end
diff --git a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex
index 0b9ed2224..cd7665e31 100644
--- a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex
+++ b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex
@@ -48,4 +48,27 @@ defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do
@impl true
def describe,
do: {:ok, %{mrf_rejectnonpublic: Config.get(:mrf_rejectnonpublic) |> Enum.into(%{})}}
+
+ @impl true
+ def config_description do
+ %{
+ key: :mrf_rejectnonpublic,
+ related_policy: "Pleroma.Web.ActivityPub.MRF.RejectNonPublic",
+ description: "RejectNonPublic drops posts with non-public visibility settings.",
+ label: "MRF Reject Non Public",
+ children: [
+ %{
+ key: :allow_followersonly,
+ label: "Allow followers-only",
+ type: :boolean,
+ description: "Whether to allow followers-only posts"
+ },
+ %{
+ key: :allow_direct,
+ type: :boolean,
+ description: "Whether to allow direct messages"
+ }
+ ]
+ }
+ end
end
diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
index b77b8c7b4..6cd91826d 100644
--- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
@behaviour Pleroma.Web.ActivityPub.MRF
alias Pleroma.Config
+ alias Pleroma.FollowingRelationship
alias Pleroma.User
alias Pleroma.Web.ActivityPub.MRF
@@ -65,7 +66,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
"type" => "Create",
"object" => child_object
} = object
- ) do
+ )
+ when is_map(child_object) do
media_nsfw =
Config.get([:mrf_simple, :media_nsfw])
|> MRF.subdomains_regex()
@@ -108,6 +110,35 @@ 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 =
Config.get([:mrf_simple, :report_removal])
@@ -174,6 +205,7 @@ 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
@@ -212,4 +244,78 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
{:ok, %{mrf_simple: mrf_simple}}
end
+
+ @impl true
+ def config_description do
+ %{
+ key: :mrf_simple,
+ related_policy: "Pleroma.Web.ActivityPub.MRF.SimplePolicy",
+ label: "MRF Simple",
+ description: "Simple ingress policies",
+ children: [
+ %{
+ key: :media_removal,
+ type: {:list, :string},
+ description: "List of instances to strip media attachments from",
+ suggestions: ["example.com", "*.example.com"]
+ },
+ %{
+ key: :media_nsfw,
+ label: "Media NSFW",
+ type: {:list, :string},
+ description: "List of instances to tag all media as NSFW (sensitive) from",
+ suggestions: ["example.com", "*.example.com"]
+ },
+ %{
+ key: :federated_timeline_removal,
+ type: {:list, :string},
+ description:
+ "List of instances to remove from the Federated (aka The Whole Known Network) Timeline",
+ suggestions: ["example.com", "*.example.com"]
+ },
+ %{
+ key: :reject,
+ type: {:list, :string},
+ description: "List of instances to reject activities from (except deletes)",
+ suggestions: ["example.com", "*.example.com"]
+ },
+ %{
+ key: :accept,
+ type: {:list, :string},
+ description: "List of instances to only accept activities from (except deletes)",
+ suggestions: ["example.com", "*.example.com"]
+ },
+ %{
+ key: :followers_only,
+ type: {:list, :string},
+ description: "Force posts from the given instances to be visible by followers only",
+ suggestions: ["example.com", "*.example.com"]
+ },
+ %{
+ key: :report_removal,
+ type: {:list, :string},
+ description: "List of instances to reject reports from",
+ suggestions: ["example.com", "*.example.com"]
+ },
+ %{
+ key: :avatar_removal,
+ type: {:list, :string},
+ description: "List of instances to strip avatars from",
+ suggestions: ["example.com", "*.example.com"]
+ },
+ %{
+ key: :banner_removal,
+ type: {:list, :string},
+ description: "List of instances to strip banners from",
+ suggestions: ["example.com", "*.example.com"]
+ },
+ %{
+ key: :reject_deletes,
+ type: {:list, :string},
+ description: "List of instances to reject deletions from",
+ suggestions: ["example.com", "*.example.com"]
+ }
+ ]
+ }
+ end
end
diff --git a/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex b/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex
index c9f20571f..2ec45260a 100644
--- a/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex
@@ -28,8 +28,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SubchainPolicy do
}"
)
- subchain
- |> MRF.filter(message)
+ MRF.filter(subchain, message)
else
_e -> {:ok, message}
end
@@ -40,4 +39,28 @@ defmodule Pleroma.Web.ActivityPub.MRF.SubchainPolicy do
@impl true
def describe, do: {:ok, %{}}
+
+ @impl true
+ def config_description do
+ %{
+ key: :mrf_subchain,
+ related_policy: "Pleroma.Web.ActivityPub.MRF.SubchainPolicy",
+ label: "MRF Subchain",
+ description:
+ "This policy processes messages through an alternate pipeline when a given message matches certain criteria." <>
+ " All criteria are configured as a map of regular expressions to lists of policy modules.",
+ children: [
+ %{
+ key: :match_actor,
+ type: {:map, {:list, :string}},
+ description: "Matches a series of regular expressions against the actor field",
+ suggestions: [
+ %{
+ ~r/https:\/\/example.com/s => [Pleroma.Web.ActivityPub.MRF.DropPolicy]
+ }
+ ]
+ }
+ ]
+ }
+ end
end
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 1a28f2ba2..e9d0d0503 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
@@ -41,4 +41,25 @@ defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy do
{:ok, %{mrf_user_allowlist: mrf_user_allowlist}}
end
+
+ # TODO: change way of getting settings on `lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex:18` to use `hosts` subkey
+ # @impl true
+ # def config_description do
+ # %{
+ # key: :mrf_user_allowlist,
+ # related_policy: "Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy",
+ # description: "Accept-list of users from specified instances",
+ # children: [
+ # %{
+ # key: :hosts,
+ # type: :map,
+ # description:
+ # "The keys in this section are the domain names that the policy should apply to." <>
+ # " Each key should be assigned a list of users that should be allowed " <>
+ # "through by their ActivityPub ID",
+ # suggestions: [%{"example.org" => ["https://example.org/users/admin"]}]
+ # }
+ # ]
+ # }
+ # 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 a6c545570..f325cb680 100644
--- a/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicy do
@behaviour Pleroma.Web.ActivityPub.MRF
+ @impl true
def filter(%{"type" => "Undo", "object" => child_message} = message) do
with {:ok, _} <- filter(child_message) do
{:ok, message}
@@ -36,6 +37,33 @@ defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicy do
def filter(message), do: {:ok, message}
+ @impl true
def describe,
do: {:ok, %{mrf_vocabulary: Pleroma.Config.get(:mrf_vocabulary) |> Enum.into(%{})}}
+
+ @impl true
+ def config_description do
+ %{
+ key: :mrf_vocabulary,
+ related_policy: "Pleroma.Web.ActivityPub.MRF.VocabularyPolicy",
+ label: "MRF Vocabulary",
+ description: "Filter messages which belong to certain activity vocabularies",
+ children: [
+ %{
+ key: :accept,
+ type: {:list, :string},
+ description:
+ "A list of ActivityStreams terms to accept. If empty, all supported messages are accepted.",
+ suggestions: ["Create", "Follow", "Mention", "Announce", "Like"]
+ },
+ %{
+ key: :reject,
+ type: {:list, :string},
+ description:
+ "A list of ActivityStreams terms to reject. If empty, no messages are rejected.",
+ suggestions: ["Create", "Follow", "Mention", "Announce", "Like"]
+ }
+ ]
+ }
+ end
end
diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex
index 0dcc7be4d..bd0a2a8dc 100644
--- a/lib/pleroma/web/activity_pub/object_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validator.ex
@@ -12,21 +12,50 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
alias Pleroma.Activity
alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Object
+ alias Pleroma.Object.Containment
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.ArticleNoteValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator
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
@@ -112,17 +141,60 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
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" => type} = object, meta) when type in ~w[Audio Video] do
+ with {:ok, object} <-
+ object
+ |> AudioVideoValidator.cast_and_validate()
+ |> Ecto.Changeset.apply_action(:insert) do
+ object = stringify_keys(object)
+ {:ok, object, meta}
+ end
+ end
+
+ def validate(%{"type" => "Article"} = object, meta) do
+ with {:ok, object} <-
+ object
+ |> ArticleNoteValidator.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 |> Map.from_struct())
+ object = stringify_keys(object)
{:ok, object, meta}
end
end
- def validate(%{"type" => "Create", "object" => object} = create_activity, meta) do
+ 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} <-
@@ -134,12 +206,28 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
end
end
+ def validate(
+ %{"type" => "Create", "object" => %{"type" => objtype} = object} = create_activity,
+ meta
+ )
+ when objtype in ~w[Question Answer Audio Video Event Article] 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 |> Map.from_struct())
+ object = stringify_keys(object)
{:ok, object, meta}
end
end
@@ -148,8 +236,29 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator 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" => type} = object) when type in ~w[Audio Video] do
+ AudioVideoValidator.cast_and_apply(object)
+ end
+
+ def cast_and_apply(%{"type" => "Event"} = object) do
+ EventValidator.cast_and_apply(object)
+ end
+
+ def cast_and_apply(%{"type" => "Article"} = object) do
+ ArticleNoteValidator.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()
@@ -169,7 +278,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
def stringify_keys(object), do: object
def fetch_actor(object) do
- with {:ok, actor} <- ObjectValidators.ObjectID.cast(object["actor"]) do
+ with actor <- Containment.get_actor(object),
+ {:ok, actor} <- ObjectValidators.ObjectID.cast(actor) do
User.get_or_fetch_by_ap_id(actor)
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
index 6f757f49c..338957db8 100644
--- a/lib/pleroma/web/activity_pub/object_validators/announce_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/announce_validator.ex
@@ -67,7 +67,12 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator do
%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))
+ recipients = get_field(cng, :to) ++ get_field(cng, :cc)
+ local_public = Pleroma.Constants.as_local_public()
+
+ is_public =
+ Enum.member?(recipients, Pleroma.Constants.as_public()) or
+ Enum.member?(recipients, local_public)
cond do
same_actor && is_public ->
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/article_note_validator.ex b/lib/pleroma/web/activity_pub/object_validators/article_note_validator.ex
new file mode 100644
index 000000000..5b7dad517
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/article_note_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.ArticleNoteValidator 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.Transmogrifier
+
+ 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(: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)
+ field(:emoji, ObjectValidators.Emoji, 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
+ data = fix(data)
+
+ %__MODULE__{}
+ |> changeset(data)
+ end
+
+ defp fix_url(%{"url" => url} = data) when is_map(url) do
+ Map.put(data, "url", url["href"])
+ end
+
+ defp fix_url(data), do: data
+
+ defp fix(data) do
+ data
+ |> CommonFixes.fix_defaults()
+ |> CommonFixes.fix_attribution()
+ |> CommonFixes.fix_actor()
+ |> fix_url()
+ |> Transmogrifier.fix_emoji()
+ 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, ["Article", "Note"])
+ |> 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/attachment_validator.ex b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex
index f53bb02be..f96fd54bf 100644
--- a/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex
@@ -5,6 +5,7 @@
defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do
use Ecto.Schema
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Web.ActivityPub.ObjectValidators.UrlObjectValidator
import Ecto.Changeset
@@ -14,8 +15,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do
field(:type, :string)
field(:mediaType, :string, default: "application/octet-stream")
field(:name, :string)
+ field(:blurhash, :string)
- embeds_many(:url, UrlObjectValidator)
+ embeds_many :url, UrlObjectValidator, primary_key: false do
+ field(:type, :string)
+ field(:href, ObjectValidators.Uri)
+ field(:mediaType, :string, default: "application/octet-stream")
+ end
end
def cast_and_validate(data) do
@@ -36,45 +42,57 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do
|> fix_url()
struct
- |> cast(data, [:type, :mediaType, :name])
- |> cast_embed(:url, required: true)
+ |> cast(data, [:type, :mediaType, :name, :blurhash])
+ |> cast_embed(:url, with: &url_changeset/2)
+ |> validate_inclusion(:type, ~w[Link Document Audio Image Video])
+ |> validate_required([:type, :mediaType, :url])
+ end
+
+ def url_changeset(struct, data) do
+ data = fix_media_type(data)
+
+ struct
+ |> cast(data, [:type, :href, :mediaType])
+ |> validate_inclusion(:type, ["Link"])
+ |> validate_required([:type, :href, :mediaType])
end
def fix_media_type(data) do
- data =
- data
- |> Map.put_new("mediaType", data["mimeType"])
+ data = Map.put_new(data, "mediaType", data["mimeType"])
if MIME.valid?(data["mediaType"]) do
data
else
- data
- |> Map.put("mediaType", "application/octet-stream")
+ Map.put(data, "mediaType", "application/octet-stream")
end
end
- def fix_url(data) do
- case data["url"] do
- url when is_binary(url) ->
- data
- |> Map.put(
- "url",
- [
- %{
- "href" => url,
- "type" => "Link",
- "mediaType" => data["mediaType"]
- }
- ]
- )
-
- _ ->
+ 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_inclusion(:type, ~w[Document Audio Image Video])
|> validate_required([:mediaType, :url, :type])
end
end
diff --git a/lib/pleroma/web/activity_pub/object_validators/audio_video_validator.ex b/lib/pleroma/web/activity_pub/object_validators/audio_video_validator.ex
new file mode 100644
index 000000000..16973e5db
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/audio_video_validator.ex
@@ -0,0 +1,134 @@
+# 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.AudioVideoValidator do
+ use Ecto.Schema
+
+ alias Pleroma.EarmarkRenderer
+ 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.Transmogrifier
+
+ 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(: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)
+ field(:emoji, ObjectValidators.Emoji, 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_url(%{"url" => url} = data) when is_list(url) do
+ attachment =
+ Enum.find(url, fn x ->
+ mime_type = x["mimeType"] || x["mediaType"] || ""
+
+ is_map(x) and String.starts_with?(mime_type, ["video/", "audio/"])
+ end)
+
+ link_element =
+ Enum.find(url, fn x ->
+ mime_type = x["mimeType"] || x["mediaType"] || ""
+
+ is_map(x) and mime_type == "text/html"
+ end)
+
+ data
+ |> Map.put("attachment", [attachment])
+ |> Map.put("url", link_element["href"])
+ end
+
+ defp fix_url(data), do: data
+
+ defp fix_content(%{"mediaType" => "text/markdown", "content" => content} = data)
+ when is_binary(content) do
+ content =
+ content
+ |> Earmark.as_html!(%Earmark.Options{renderer: EarmarkRenderer})
+ |> Pleroma.HTML.filter_tags()
+
+ Map.put(data, "content", content)
+ end
+
+ defp fix_content(data), do: data
+
+ defp fix(data) do
+ data
+ |> CommonFixes.fix_defaults()
+ |> CommonFixes.fix_attribution()
+ |> CommonFixes.fix_actor()
+ |> Transmogrifier.fix_emoji()
+ |> fix_url()
+ |> fix_content()
+ 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", "Video"])
+ |> 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/chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex
index 91b475393..6acd4a771 100644
--- a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex
@@ -22,7 +22,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator do
field(:content, ObjectValidators.SafeText)
field(:actor, ObjectValidators.ObjectID)
field(:published, ObjectValidators.DateTime)
- field(:emoji, :map, default: %{})
+ field(:emoji, ObjectValidators.Emoji, default: %{})
embeds_one(:attachment, AttachmentValidator)
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..b3638cfc7
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex
@@ -0,0 +1,31 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
+ alias Pleroma.Object.Containment
+ 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("context", context)
+ |> Map.put("context_id", context_id)
+ end
+
+ def fix_attribution(data) do
+ data
+ |> Map.put_new("actor", data["attributedTo"])
+ end
+
+ def fix_actor(data) do
+ actor = Containment.get_actor(data)
+
+ data
+ |> Map.put("actor", actor)
+ |> Map.put("attributedTo", actor)
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/object_validators/common_validations.ex b/lib/pleroma/web/activity_pub/object_validators/common_validations.ex
index aeef31945..603d87b8e 100644
--- a/lib/pleroma/web/activity_pub/object_validators/common_validations.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/common_validations.ex
@@ -9,7 +9,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
alias Pleroma.Object
alias Pleroma.User
- def validate_recipients_presence(cng, fields \\ [:to, :cc]) do
+ def validate_any_presence(cng, fields) do
non_empty =
fields
|> Enum.map(fn field -> get_field(cng, field) end)
@@ -24,7 +24,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
fields
|> Enum.reduce(cng, fn field, cng ->
cng
- |> add_error(field, "no recipients in any field")
+ |> add_error(field, "none of #{inspect(fields)} present")
end)
end
end
@@ -34,10 +34,15 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
cng
|> validate_change(field_name, fn field_name, actor ->
- if User.get_cached_by_ap_id(actor) do
- []
- else
- [{field_name, "can't find user"}]
+ 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
@@ -77,4 +82,60 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
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_generic_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex
new file mode 100644
index 000000000..422ee07be
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex
@@ -0,0 +1,146 @@
+# 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
+ alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
+ alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
+
+ 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(: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)
+ |> CommonFixes.fix_actor()
+ end
+
+ def validate_data(cng, meta \\ []) do
+ cng
+ |> validate_required([:actor, :type, :object])
+ |> validate_inclusion(:type, ["Create"])
+ |> CommonValidations.validate_actor_presence()
+ |> CommonValidations.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
index 316bd0c07..9b9743c4a 100644
--- a/lib/pleroma/web/activity_pub/object_validators/create_note_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/create_note_validator.ex
@@ -16,11 +16,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateNoteValidator do
field(:id, ObjectValidators.ObjectID, primary_key: true)
field(:actor, ObjectValidators.ObjectID)
field(:type, :string)
- field(:to, {:array, :string})
- field(:cc, {:array, :string})
- field(:bto, {:array, :string}, default: [])
- field(:bcc, {:array, :string}, default: [])
-
+ 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
diff --git a/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex b/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex
index 93a7b0e0b..2634e8d4d 100644
--- a/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex
@@ -7,7 +7,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator do
alias Pleroma.Activity
alias Pleroma.EctoType.ActivityPub.ObjectValidators
- alias Pleroma.User
import Ecto.Changeset
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
@@ -59,7 +58,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator do
|> validate_required([:id, :type, :actor, :to, :cc, :object])
|> validate_inclusion(:type, ["Delete"])
|> validate_actor_presence()
- |> validate_deletion_rights()
+ |> validate_modification_rights()
|> validate_object_or_user_presence(allowed_types: @deletable_types)
|> add_deleted_activity_id()
end
@@ -68,31 +67,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator do
!same_domain?(cng)
end
- defp same_domain?(cng) do
- actor_uri =
- cng
- |> get_field(:actor)
- |> URI.parse()
-
- object_uri =
- cng
- |> get_field(:object)
- |> URI.parse()
-
- object_uri.host == actor_uri.host
- end
-
- def validate_deletion_rights(cng) do
- actor = User.get_cached_by_ap_id(get_field(cng, :actor))
-
- if User.superuser?(actor) || same_domain?(cng) do
- cng
- else
- cng
- |> add_error(:actor, "is not allowed to delete object")
- end
- end
-
def cast_and_validate(data) do
data
|> cast_data
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
index a543af1f8..336c92d35 100644
--- a/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex
@@ -20,8 +20,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
field(:actor, ObjectValidators.ObjectID)
field(:context, :string)
field(:content, :string)
- field(:to, {:array, :string}, default: [])
- field(:cc, {:array, :string}, default: [])
+ field(:to, ObjectValidators.Recipients, default: [])
+ field(:cc, ObjectValidators.Recipients, default: [])
end
def cast_and_validate(data) do
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..0b4c99dc0
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/event_validator.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.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
+ alias Pleroma.Web.ActivityPub.Transmogrifier
+
+ 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)
+ field(:emoji, ObjectValidators.Emoji, 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()
+ |> Transmogrifier.fix_emoji()
+ 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/note_validator.ex b/lib/pleroma/web/activity_pub/object_validators/note_validator.ex
deleted file mode 100644
index 56b93dde8..000000000
--- a/lib/pleroma/web/activity_pub/object_validators/note_validator.ex
+++ /dev/null
@@ -1,63 +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.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, {:array, :string}, default: [])
- field(:cc, {:array, :string}, default: [])
- field(:bto, {:array, :string}, default: [])
- field(:bcc, {:array, :string}, default: [])
- # TODO: Write type
- field(:tag, {:array, :map}, default: [])
- field(:type, :string)
- field(:content, :string)
- field(:context, :string)
- field(:actor, 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)
- # TODO: Write type
- field(:attachment, {:array, :map}, default: [])
- field(:replies_count, :integer, default: 0)
- field(:like_count, :integer, default: 0)
- field(:announcement_count, :integer, default: 0)
- field(:inRepyTo, :string)
- field(:uri, ObjectValidators.Uri)
-
- field(:likes, {:array, :string}, default: [])
- field(:announcements, {:array, :string}, default: [])
-
- # see if needed
- field(:context_id, :string)
- end
-
- def cast_and_validate(data) do
- data
- |> cast_data()
- |> validate_data()
- end
-
- def cast_data(data) do
- %__MODULE__{}
- |> cast(data, __schema__(:fields))
- end
-
- def validate_data(data_cng) do
- data_cng
- |> validate_inclusion(:type, ["Note"])
- |> validate_required([:id, :actor, :to, :cc, :type, :content, :context])
- end
-end
diff --git a/lib/pleroma/web/activity_pub/object_validators/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..9310485dc
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex
@@ -0,0 +1,112 @@
+# 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
+ alias Pleroma.Web.ActivityPub.Transmogrifier
+
+ 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)
+ field(:emoji, ObjectValidators.Emoji, 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, ObjectValidators.ObjectID}, default: [])
+ field(:announcements, {:array, ObjectValidators.ObjectID}, 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()
+ |> Transmogrifier.fix_emoji()
+ |> 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
index e8d2d39c1..8cae94467 100644
--- a/lib/pleroma/web/activity_pub/object_validators/undo_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/undo_validator.ex
@@ -18,8 +18,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator do
field(:type, :string)
field(:object, ObjectValidators.ObjectID)
field(:actor, ObjectValidators.ObjectID)
- field(:to, {:array, :string}, default: [])
- field(:cc, {:array, :string}, default: [])
+ field(:to, ObjectValidators.Recipients, default: [])
+ field(:cc, ObjectValidators.Recipients, default: [])
end
def cast_and_validate(data) do
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
deleted file mode 100644
index f64fac46d..000000000
--- a/lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex
+++ /dev/null
@@ -1,24 +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.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)
- 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
index 36e325c37..98c32a42b 100644
--- a/lib/pleroma/web/activity_pub/pipeline.ex
+++ b/lib/pleroma/web/activity_pub/pipeline.ex
@@ -11,6 +11,7 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do
alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.ActivityPub.ObjectValidator
alias Pleroma.Web.ActivityPub.SideEffects
+ alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Federator
@spec common_pipeline(map(), keyword()) ::
@@ -26,13 +27,17 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do
{:error, e} ->
{:error, e}
+
+ {:reject, e} ->
+ {:reject, 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, mrfd_object, meta}} <-
+ {:mrf_object, MRF.pipeline_filter(validated_object, meta)},
{_, {:ok, activity, meta}} <-
{:persist_object, ActivityPub.persist(mrfd_object, meta)},
{_, {:ok, activity, meta}} <-
@@ -40,7 +45,7 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do
{_, {:ok, _}} <- {:federation, maybe_federate(activity, meta)} do
{:ok, activity, meta}
else
- {:mrf_object, {:reject, _}} -> {:ok, nil, meta}
+ {:mrf_object, {:reject, message, _}} -> {:reject, message}
e -> {:error, e}
end
end
@@ -51,7 +56,7 @@ defmodule Pleroma.Web.ActivityPub.Pipeline 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
+ if !do_not_federate and local and not Visibility.is_local_public?(activity) do
activity =
if object = Keyword.get(meta, :object_data) do
%{activity | data: Map.put(activity.data, "object", object)}
diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex
index d88f7f3ee..5ab3562bf 100644
--- a/lib/pleroma/web/activity_pub/publisher.ex
+++ b/lib/pleroma/web/activity_pub/publisher.ex
@@ -49,16 +49,14 @@ 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}")
-
- uri = URI.parse(inbox)
-
+ uri = %{path: path} = URI.parse(inbox)
digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
date = Pleroma.Signature.signed_date()
signature =
Pleroma.Signature.sign(actor, %{
- "(request-target)": "post #{uri.path}",
+ "(request-target)": "post #{path}",
host: signature_host(uri),
"content-length": byte_size(json),
digest: digest,
@@ -230,9 +228,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
end)
end
- @doc """
- Publishes an activity to all relevant peers.
- """
+ # Publishes an activity to all relevant peers.
def publish(%User{} = actor, %Activity{} = activity) do
public = is_public?(activity)
diff --git a/lib/pleroma/web/activity_pub/relay.ex b/lib/pleroma/web/activity_pub/relay.ex
index b09764d2b..6606e1780 100644
--- a/lib/pleroma/web/activity_pub/relay.ex
+++ b/lib/pleroma/web/activity_pub/relay.ex
@@ -10,19 +10,13 @@ defmodule Pleroma.Web.ActivityPub.Relay do
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
@@ -36,12 +30,16 @@ defmodule Pleroma.Web.ActivityPub.Relay do
end
end
- @spec unfollow(String.t()) :: {:ok, Activity.t()} | {:error, any()}
- def unfollow(target_instance) do
+ @spec unfollow(String.t(), map()) :: {:ok, Activity.t()} | {:error, any()}
+ def unfollow(target_instance, opts \\ %{}) do
with %User{} = local_user <- get_actor(),
- {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_instance),
+ {:ok, target_user} <- fetch_target_user(target_instance, opts),
{:ok, activity} <- ActivityPub.unfollow(local_user, target_user) do
- User.unfollow(local_user, target_user)
+ case target_user.id do
+ nil -> User.update_following_count(local_user)
+ _ -> User.unfollow(local_user, target_user)
+ end
+
Logger.info("relay: unfollowed instance: #{target_instance}: id=#{activity.data["id"]}")
{:ok, activity}
else
@@ -49,6 +47,14 @@ defmodule Pleroma.Web.ActivityPub.Relay do
end
end
+ defp fetch_target_user(ap_id, opts) do
+ case {opts[:force], User.get_or_fetch_by_ap_id(ap_id)} do
+ {_, {:ok, %User{} = user}} -> {:ok, user}
+ {true, _} -> {:ok, %User{ap_id: ap_id}}
+ {_, error} -> error
+ end
+ end
+
@spec publish(any()) :: {:ok, Activity.t()} | {:error, any()}
def publish(%Activity{data: %{"type" => "Create"}} = activity) do
with %User{} = user <- get_actor(),
@@ -61,34 +67,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
index 1d2c296a5..4d8fb721e 100644
--- a/lib/pleroma/web/activity_pub/side_effects.ex
+++ b/lib/pleroma/web/activity_pub/side_effects.ex
@@ -1,3 +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.ActivityPub.SideEffects do
@moduledoc """
This module looks at an inserted object and executes the side effects that it
@@ -15,13 +19,69 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
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
+ 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
@@ -41,34 +101,14 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
%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
- Utils.update_follow_state_for_all(object, "accept")
- FollowingRelationship.update(follower, followed, :follow_accept)
- User.update_follower_count(followed)
- User.update_following_count(follower)
-
- %{
- to: [following_user],
- actor: followed,
- object: follow_id,
- local: true
- }
- |> ActivityPub.accept()
+ if followed.local && !followed.is_locked do
+ {:ok, accept_data, _} = Builder.accept(followed, object)
+ {:ok, _activity, _} = Pipeline.common_pipeline(accept_data, local: true)
end
else
- {:following, {:error, _}, follower, followed} ->
- Utils.update_follow_state_for_all(object, "reject")
- FollowingRelationship.update(follower, followed, :follow_reject)
-
- if followed.local do
- %{
- to: [follower.ap_id],
- actor: followed,
- object: follow_id,
- local: true
- }
- |> ActivityPub.reject()
- end
+ {:following, {:error, _}, _follower, followed} ->
+ {:ok, reject_data, _} = Builder.reject(followed, object)
+ {:ok, _activity, _} = Pipeline.common_pipeline(reject_data, local: true)
_ ->
nil
@@ -135,10 +175,24 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
# 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) 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"] && object.data["type"] != "Answer" do
+ Object.increase_replies_count(in_reply_to)
+ end
+
+ ConcurrentLimiter.limit(Pleroma.Web.RichMedia.Helpers, fn ->
+ Task.start(fn -> Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) end)
+ end)
meta =
meta
@@ -199,13 +253,15 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
# - 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)
+ Object.normalize(deleted_object, false) ||
+ User.get_cached_by_ap_id(deleted_object)
result =
case deleted_object do
%Object{} ->
with {:ok, deleted_object, activity} <- Object.delete(deleted_object),
- %User{} = user <- User.get_cached_by_ap_id(deleted_object.data["actor"]) do
+ {_, 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)
@@ -219,6 +275,10 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
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{} ->
@@ -247,11 +307,18 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
streamables =
[[actor, recipient], [recipient, actor]]
+ |> Enum.uniq()
|> 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)
+ Cachex.put(
+ :chat_message_id_idempotency_key_cache,
+ cm_ref.id,
+ meta[:idempotency_key]
+ )
+
{
["user", "user:pleroma_chat"],
{user, %{cm_ref | chat: chat, object: object}}
@@ -268,9 +335,28 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
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 Video Question Event Article] do
+ with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do
+ {:ok, object, meta}
+ end
+ end
+
# Nothing to do
- def handle_object_creation(object) do
- {:ok, object}
+ def handle_object_creation(object, meta) do
+ {:ok, object, meta}
end
defp undo_like(nil, object), do: delete_object(object)
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 35aa05eb5..565d32433 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -7,11 +7,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
A module to handle coding from internal to wire ActivityPub and back.
"""
alias Pleroma.Activity
- alias Pleroma.EarmarkRenderer
alias Pleroma.EctoType.ActivityPub.ObjectValidators
- alias Pleroma.FollowingRelationship
alias Pleroma.Maps
- alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Object.Containment
alias Pleroma.Repo
@@ -43,11 +40,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> fix_in_reply_to(options)
|> fix_emoji
|> fix_tag
+ |> set_sensitive
|> fix_content_map
|> fix_addressing
|> fix_summary
|> fix_type(options)
- |> fix_content
end
def fix_summary(%{"summary" => nil} = object) do
@@ -157,7 +154,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 \\ [])
@@ -165,7 +167,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
when not is_nil(in_reply_to) do
in_reply_to_id = prepare_in_reply_to(in_reply_to)
- object = Map.put(object, "inReplyToAtomUri", in_reply_to_id)
depth = (options[:depth] || 0) + 1
if Federator.allowed_thread_distance?(depth) do
@@ -173,9 +174,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
%Activity{} <- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
object
|> Map.put("inReplyTo", replied_object.data["id"])
- |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
|> Map.put("context", replied_object.data["context"] || object["conversation"])
- |> Map.drop(["conversation"])
+ |> Map.drop(["conversation", "inReplyToAtomUri"])
else
e ->
Logger.warn("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
@@ -240,14 +240,19 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
if href do
attachment_url =
- %{"href" => href}
+ %{
+ "href" => href,
+ "type" => Map.get(url || %{}, "type", "Link")
+ }
|> Maps.put_if_present("mediaType", media_type)
- |> Maps.put_if_present("type", Map.get(url || %{}, "type"))
- %{"url" => [attachment_url]}
+ %{
+ "url" => [attachment_url],
+ "type" => data["type"] || "Document"
+ }
|> Maps.put_if_present("mediaType", media_type)
- |> Maps.put_if_present("type", data["type"])
|> Maps.put_if_present("name", data["name"])
+ |> Maps.put_if_present("blurhash", data["blurhash"])
else
nil
end
@@ -269,25 +274,7 @@ 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
- attachment =
- Enum.find(url, fn x ->
- media_type = x["mediaType"] || x["mimeType"] || ""
-
- is_map(x) and String.starts_with?(media_type, ["audio/", "video/"])
- end)
-
- link_element =
- Enum.find(url, fn x -> is_map(x) and (x["mediaType"] || x["mimeType"]) == "text/html" end)
-
- object
- |> Map.put("attachment", [attachment])
- |> Map.put("url", link_element["href"])
- end
-
- def fix_url(%{"type" => object_type, "url" => url} = object)
- when object_type != "Video" and is_list(url) do
+ def fix_url(%{"url" => url} = object) when is_list(url) do
first_element = Enum.at(url, 0)
url_string =
@@ -305,16 +292,13 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
emoji =
tags
- |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
+ |> Enum.filter(fn data -> is_map(data) and data["type"] == "Emoji" and data["icon"] end)
|> Enum.reduce(%{}, fn data, mapping ->
name = String.trim(data["name"], ":")
Map.put(mapping, name, data["icon"]["url"])
end)
- # we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats
- emoji = Map.merge(object["emoji"] || %{}, emoji)
-
Map.put(object, "emoji", emoji)
end
@@ -331,19 +315,21 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
tags =
tag
|> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
- |> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
+ |> Enum.map(fn %{"name" => name} ->
+ name
+ |> String.slice(1..-1)
+ |> String.downcase()
+ end)
Map.put(object, "tag", tag ++ tags)
end
- def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
- combined = [tag, String.slice(hashtag, 1..-1)]
-
- Map.put(object, "tag", combined)
+ def fix_tag(%{"tag" => %{} = tag} = object) do
+ object
+ |> Map.put("tag", [tag])
+ |> fix_tag
end
- def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag])
-
def fix_tag(object), do: object
# content map usually only has one language so this will do for now.
@@ -370,44 +356,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def fix_type(object, _), do: object
- defp fix_content(%{"mediaType" => "text/markdown", "content" => content} = object)
- when is_binary(content) do
- html_content =
- content
- |> Earmark.as_html!(%Earmark.Options{renderer: EarmarkRenderer})
- |> Pleroma.HTML.filter_tags()
-
- Map.merge(object, %{"content" => html_content, "mediaType" => "text/html"})
- end
-
- defp fix_content(object), do: object
-
- defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
- with true <- id =~ "follows",
- %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
- %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)
-
- _ ->
- {:error, nil}
- end
- end
-
# Reduce the object list to find the reported user.
defp get_reported(objects) do
Enum.reduce_while(objects, nil, fn ap_id, _ ->
@@ -419,6 +367,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
@@ -457,30 +428,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{Note Page} do
actor = Containment.get_actor(data)
with nil <- Activity.get_create_by_object_ap_id(object["id"]),
- {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(actor),
- data <- Map.put(data, "actor", actor) |> fix_addressing() do
- object = fix_object(object, options)
-
- params = %{
- to: data["to"],
- object: object,
- actor: user,
- context: object["context"],
- 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
@@ -531,60 +490,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
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)
-
- Notification.update_notification_type(followed, follow_activity)
-
- 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" => "❤️",
@@ -614,12 +519,19 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
def handle_incoming(
- %{"type" => "Create", "object" => %{"type" => "ChatMessage"}} = data,
+ %{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data,
_options
- ) do
+ )
+ when objtype in ~w{Question Answer ChatMessage Audio Video Event Article} do
+ data = Map.put(data, "object", strip_internal_fields(data["object"]))
+
with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
+ nil <- Activity.get_create_by_object_ap_id(obj_id),
{:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
{:ok, activity}
+ else
+ %Activity{} = activity -> {:ok, activity}
+ e -> e
end
end
@@ -638,9 +550,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
%{"type" => type} = data,
_options
)
- when type in ~w{Update Block Follow} 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, _} <-
+ Pipeline.common_pipeline(data, local: false) do
{:ok, activity}
end
end
@@ -649,7 +562,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
%{"type" => "Delete"} = data,
_options
) do
- with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
+ with {:ok, activity, _} <-
+ Pipeline.common_pipeline(data, local: false) do
{:ok, activity}
else
{:error, {:validate_object, _}} = e ->
@@ -1017,7 +931,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
Map.put(object, "conversation", object["context"])
end
- def set_sensitive(%{"sensitive" => true} = object) do
+ def set_sensitive(%{"sensitive" => _} = object) do
object
end
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index 713b0ca1f..ea1c3a04a 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -175,7 +175,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do
outgoing_blocks = Config.get([:activitypub, :outgoing_blocks])
with true <- Config.get!([:instance, :federating]),
- true <- type != "Block" || outgoing_blocks do
+ true <- type != "Block" || outgoing_blocks,
+ false <- Visibility.is_local_public?(activity) do
Pleroma.Web.Federator.publish(activity)
end
@@ -701,14 +702,30 @@ defmodule Pleroma.Web.ActivityPub.Utils do
def make_flag_data(_, _), do: %{}
- defp build_flag_object(%{account: account, statuses: statuses} = _) do
- [account.ap_id] ++ build_flag_object(%{statuses: statuses})
+ defp build_flag_object(%{account: account, statuses: statuses}) do
+ [account.ap_id | build_flag_object(%{statuses: statuses})]
end
defp build_flag_object(%{statuses: statuses}) do
Enum.map(statuses || [], &build_flag_object/1)
end
+ defp build_flag_object(%Activity{data: %{"id" => id}, object: %{data: data}}) do
+ activity_actor = User.get_by_ap_id(data["actor"])
+
+ %{
+ "type" => "Note",
+ "id" => id,
+ "content" => data["content"],
+ "published" => data["published"],
+ "actor" =>
+ AccountView.render(
+ "show.json",
+ %{user: activity_actor, skip_visibility_check: true}
+ )
+ }
+ end
+
defp build_flag_object(act) when is_map(act) or is_binary(act) do
id =
case act do
@@ -719,22 +736,14 @@ 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: activity_actor, skip_visibility_check: true}
- )
- }
-
- _ ->
- %{"id" => id, "deleted" => true}
+ build_flag_object(activity)
+
+ nil ->
+ if activity = Activity.get_by_object_ap_id_with_object(id) do
+ build_flag_object(activity)
+ else
+ %{"id" => id, "deleted" => true}
+ end
end
end
diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex
index 3a4564912..93c9f436c 100644
--- a/lib/pleroma/web/activity_pub/views/user_view.ex
+++ b/lib/pleroma/web/activity_pub/views/user_view.ex
@@ -101,7 +101,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
"name" => user.name,
"summary" => user.bio,
"url" => user.ap_id,
- "manuallyApprovesFollowers" => user.locked,
+ "manuallyApprovesFollowers" => user.is_locked,
"publicKey" => %{
"id" => "#{user.ap_id}#main-key",
"owner" => user.ap_id,
@@ -110,7 +110,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do
"endpoints" => endpoints,
"attachment" => fields,
"tag" => emoji_tags,
- "discoverable" => user.discoverable,
+ # Note: key name is indeed "discoverable" (not an error)
+ "discoverable" => user.is_discoverable,
"capabilities" => capabilities
}
|> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))
diff --git a/lib/pleroma/web/activity_pub/visibility.ex b/lib/pleroma/web/activity_pub/visibility.ex
index 343f41caa..2cb5a2bd0 100644
--- a/lib/pleroma/web/activity_pub/visibility.ex
+++ b/lib/pleroma/web/activity_pub/visibility.ex
@@ -17,7 +17,19 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
def is_public?(%Activity{data: %{"type" => "Move"}}), do: true
def is_public?(%Activity{data: data}), do: is_public?(data)
def is_public?(%{"directMessage" => true}), do: false
- def is_public?(data), do: Utils.label_in_message?(Pleroma.Constants.as_public(), data)
+
+ def is_public?(data) do
+ Utils.label_in_message?(Pleroma.Constants.as_public(), data) or
+ Utils.label_in_message?(Pleroma.Constants.as_local_public(), data)
+ end
+
+ def is_local_public?(%Object{data: data}), do: is_local_public?(data)
+ def is_local_public?(%Activity{data: data}), do: is_local_public?(data)
+
+ def is_local_public?(data) do
+ Utils.label_in_message?(Pleroma.Constants.as_local_public(), data) and
+ not Utils.label_in_message?(Pleroma.Constants.as_public(), data)
+ end
def is_private?(activity) do
with false <- is_public?(activity),
@@ -44,32 +56,30 @@ 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
+ @spec visible_for_user?(Activity.t() | nil, User.t() | nil) :: boolean()
+ def visible_for_user?(%Activity{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?(%Activity{data: %{"listMessage" => _}}, nil), do: false
- def visible_for_user?(%{data: %{"listMessage" => list_ap_id}} = activity, %User{} = user) do
+ def visible_for_user?(
+ %Activity{data: %{"listMessage" => list_ap_id}} = activity,
+ %User{} = user
+ ) do
user.ap_id in activity.data["to"] ||
list_ap_id
|> Pleroma.List.get_by_ap_id()
|> Pleroma.List.member?(user)
end
- def visible_for_user?(%{local: local} = activity, nil) do
- cfg_key =
- if local,
- do: :local,
- else: :remote
-
- if Pleroma.Config.get([:restrict_unauthenticated, :activities, cfg_key]),
+ def visible_for_user?(%Activity{} = activity, nil) do
+ if restrict_unauthenticated_access?(activity),
do: false,
else: is_public?(activity)
end
- def visible_for_user?(activity, user) do
+ def visible_for_user?(%Activity{} = activity, user) do
x = [user.ap_id | User.following(user)]
y = [activity.actor] ++ activity.data["to"] ++ (activity.data["cc"] || [])
is_public?(activity) || Enum.any?(x, &(&1 in y))
@@ -85,6 +95,26 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
result
end
+ def restrict_unauthenticated_access?(%Activity{local: local}) do
+ restrict_unauthenticated_access_to_activity?(local)
+ end
+
+ def restrict_unauthenticated_access?(%Object{} = object) do
+ object
+ |> Object.local?()
+ |> restrict_unauthenticated_access_to_activity?()
+ end
+
+ def restrict_unauthenticated_access?(%User{} = user) do
+ User.visible_for(user, _reading_user = nil)
+ end
+
+ defp restrict_unauthenticated_access_to_activity?(local?) when is_boolean(local?) do
+ cfg_key = if local?, do: :local, else: :remote
+
+ Pleroma.Config.restrict_unauthenticated_access?(:activities, cfg_key)
+ end
+
def get_visibility(object) do
to = object.data["to"] || []
cc = object.data["cc"] || []
@@ -96,6 +126,9 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
Pleroma.Constants.as_public() in cc ->
"unlisted"
+ Pleroma.Constants.as_local_public() in to ->
+ "local"
+
# this should use the sql for the object's activity
Enum.any?(to, &String.contains?(&1, "/followers")) ->
"private"
diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
index 5101e28d6..5c2c282b3 100644
--- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
@@ -5,32 +5,28 @@
defmodule Pleroma.Web.AdminAPI.AdminAPIController do
use Pleroma.Web, :controller
- import Pleroma.Web.ControllerHelper, only: [json_response: 3]
+ import Pleroma.Web.ControllerHelper,
+ only: [json_response: 3, fetch_integer_param: 3]
alias Pleroma.Config
alias Pleroma.MFA
alias Pleroma.ModerationLog
- alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Stats
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
- alias Pleroma.Web.ActivityPub.Builder
- alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.AdminAPI
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.AdminAPI.ModerationLogView
- alias Pleroma.Web.AdminAPI.Search
alias Pleroma.Web.Endpoint
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.Router
- require Logger
-
@users_page_size 50
plug(
OAuthScopesPlug,
%{scopes: ["read:accounts"], admin: true}
- when action in [:list_users, :user_show, :right_get, :show_user_credentials]
+ when action in [:right_get, :show_user_credentials, :create_backup]
)
plug(
@@ -39,11 +35,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
when action in [
:get_password_reset,
:force_password_reset,
- :user_delete,
- :users_create,
- :user_toggle_activation,
- :user_activate,
- :user_deactivate,
:tag_users,
:untag_users,
:right_add,
@@ -57,14 +48,14 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
plug(
OAuthScopesPlug,
- %{scopes: ["write:follows"], admin: true}
- when action in [:user_follow, :user_unfollow]
+ %{scopes: ["read:statuses"], admin: true}
+ when action in [:list_user_statuses, :list_instance_statuses]
)
plug(
OAuthScopesPlug,
- %{scopes: ["read:statuses"], admin: true}
- when action in [:list_user_statuses, :list_instance_statuses]
+ %{scopes: ["read:chats"], admin: true}
+ when action in [:list_user_chats]
)
plug(
@@ -90,132 +81,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
action_fallback(AdminAPI.FallbackController)
- 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)
-
- 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,
- subject: users,
- action: "delete"
- })
-
- json(conn, nicknames)
- end
-
- def user_follow(%{assigns: %{user: admin}} = conn, %{
- "follower" => follower_nick,
- "followed" => followed_nick
- }) do
- with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
- %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
- User.follow(follower, followed)
-
- ModerationLog.insert_log(%{
- actor: admin,
- followed: followed,
- follower: follower,
- action: "follow"
- })
- end
-
- json(conn, "ok")
- end
-
- def user_unfollow(%{assigns: %{user: admin}} = conn, %{
- "follower" => follower_nick,
- "followed" => followed_nick
- }) do
- with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
- %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
- User.unfollow(follower, followed)
-
- ModerationLog.insert_log(%{
- actor: admin,
- followed: followed,
- follower: follower,
- action: "unfollow"
- })
- end
-
- json(conn, "ok")
- end
-
- def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
- changesets =
- Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} ->
- user_data = %{
- nickname: nickname,
- name: nickname,
- email: email,
- password: password,
- password_confirmation: password,
- bio: "."
- }
-
- User.register_changeset(%User{}, user_data, need_confirmation: false)
- end)
- |> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
- Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
- end)
-
- case Pleroma.Repo.transaction(changesets) do
- {:ok, users} ->
- res =
- users
- |> Map.values()
- |> Enum.map(fn user ->
- {:ok, user} = User.post_register_action(user)
-
- user
- end)
- |> Enum.map(&AccountView.render("created.json", %{user: &1}))
-
- ModerationLog.insert_log(%{
- actor: admin,
- subjects: Map.values(users),
- action: "create"
- })
-
- json(conn, res)
-
- {:error, id, changeset, _} ->
- res =
- Enum.map(changesets.operations, fn
- {current_id, {:changeset, _current_changeset, _}} when current_id == id ->
- AccountView.render("create-error.json", %{changeset: changeset})
-
- {_, {:changeset, current_changeset, _}} ->
- AccountView.render("create-error.json", %{changeset: current_changeset})
- end)
-
- conn
- |> put_status(:conflict)
- |> json(res)
- end
- end
-
- 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})
- else
- _ -> {:error, :not_found}
- end
- end
-
def list_instance_statuses(conn, %{"instance" => instance} = params) do
with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
{page, page_size} = page_params(params)
@@ -255,52 +120,18 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
end
end
- def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
- user = User.get_cached_by_nickname(nickname)
-
- {:ok, updated_user} = User.deactivate(user, !user.deactivated)
-
- action = if user.deactivated, do: "activate", else: "deactivate"
-
- ModerationLog.insert_log(%{
- actor: admin,
- subject: [user],
- action: action
- })
-
- conn
- |> put_view(AccountView)
- |> render("show.json", %{user: updated_user})
- end
+ def list_user_chats(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname} = _params) do
+ with %User{id: user_id} <- User.get_cached_by_nickname_or_id(nickname, for: admin) do
+ chats =
+ Pleroma.Chat.for_user_query(user_id)
+ |> Pleroma.Repo.all()
- def user_activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
- users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
- {:ok, updated_users} = User.deactivate(users, false)
-
- ModerationLog.insert_log(%{
- actor: admin,
- subject: users,
- action: "activate"
- })
-
- conn
- |> put_view(AccountView)
- |> render("index.json", %{users: Keyword.values(updated_users)})
- end
-
- def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
- users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
- {:ok, updated_users} = User.deactivate(users, true)
-
- ModerationLog.insert_log(%{
- actor: admin,
- subject: users,
- action: "deactivate"
- })
-
- conn
- |> put_view(AccountView)
- |> render("index.json", %{users: Keyword.values(updated_users)})
+ conn
+ |> put_view(AdminAPI.ChatView)
+ |> render("index.json", chats: chats)
+ else
+ _ -> {:error, :not_found}
+ end
end
def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
@@ -329,44 +160,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
end
end
- def list_users(conn, params) do
- {page, page_size} = page_params(params)
- filters = maybe_parse_filters(params["filters"])
-
- search_params = %{
- query: params["query"],
- page: page,
- page_size: page_size,
- tags: params["tags"],
- name: params["name"],
- email: params["email"]
- }
-
- 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)
-
- @spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
- defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
-
- defp maybe_parse_filters(filters) do
- filters
- |> String.split(",")
- |> Enum.filter(&Enum.member?(@filters, &1))
- |> Enum.map(&String.to_atom/1)
- |> Map.new(&{&1, true})
- end
-
def right_add_multiple(%{assigns: %{user: admin}} = conn, %{
"permission_group" => permission_group,
"nicknames" => nicknames
@@ -648,25 +441,19 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
json(conn, %{"status_visibility" => counters})
end
- defp page_params(params) do
- {get_page(params["page"]), get_page_size(params["page_size"])}
- end
-
- defp get_page(page_string) when is_nil(page_string), do: 1
+ def create_backup(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
+ with %User{} = user <- User.get_by_nickname(nickname),
+ {:ok, _} <- Pleroma.User.Backup.create(user, admin.id) do
+ ModerationLog.insert_log(%{actor: admin, subject: user, action: "create_backup"})
- defp get_page(page_string) do
- case Integer.parse(page_string) do
- {page, _} -> page
- :error -> 1
+ json(conn, "")
end
end
- defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
-
- defp get_page_size(page_size_string) do
- case Integer.parse(page_size_string) do
- {page_size, _} -> page_size
- :error -> @users_page_size
- end
+ defp page_params(params) do
+ {
+ fetch_integer_param(params, "page", 1),
+ fetch_integer_param(params, "page_size", @users_page_size)
+ }
end
end
diff --git a/lib/pleroma/web/admin_api/controllers/chat_controller.ex b/lib/pleroma/web/admin_api/controllers/chat_controller.ex
new file mode 100644
index 000000000..af8ff8292
--- /dev/null
+++ b/lib/pleroma/web/admin_api/controllers/chat_controller.ex
@@ -0,0 +1,85 @@
+# 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.ChatController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Activity
+ alias Pleroma.Chat
+ alias Pleroma.Chat.MessageReference
+ alias Pleroma.ModerationLog
+ alias Pleroma.Pagination
+ alias Pleroma.Web.AdminAPI
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+
+ require Logger
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["read:chats"], admin: true} when action in [:show, :messages]
+ )
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["write:chats"], admin: true} when action in [:delete_message]
+ )
+
+ action_fallback(Pleroma.Web.AdminAPI.FallbackController)
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.ChatOperation
+
+ def delete_message(%{assigns: %{user: user}} = conn, %{
+ message_id: message_id,
+ id: chat_id
+ }) do
+ with %MessageReference{object: %{data: %{"id" => object_ap_id}}} = cm_ref <-
+ MessageReference.get_by_id(message_id),
+ ^chat_id <- to_string(cm_ref.chat_id),
+ %Activity{id: activity_id} <- Activity.get_create_by_object_ap_id(object_ap_id),
+ {:ok, _} <- CommonAPI.delete(activity_id, user) do
+ ModerationLog.insert_log(%{
+ action: "chat_message_delete",
+ actor: user,
+ subject_id: message_id
+ })
+
+ conn
+ |> put_view(MessageReferenceView)
+ |> render("show.json", chat_message_reference: cm_ref)
+ else
+ _e ->
+ {:error, :could_not_delete}
+ end
+ end
+
+ def messages(conn, %{id: id} = params) do
+ with %Chat{} = chat <- Chat.get_by_id(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 show(conn, %{id: id}) do
+ with %Chat{} = chat <- Chat.get_by_id(id) do
+ conn
+ |> put_view(AdminAPI.ChatView)
+ |> render("show.json", chat: chat)
+ end
+ end
+end
diff --git a/lib/pleroma/web/admin_api/controllers/config_controller.ex b/lib/pleroma/web/admin_api/controllers/config_controller.ex
index 0df13007f..5d155af3d 100644
--- a/lib/pleroma/web/admin_api/controllers/config_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/config_controller.ex
@@ -7,7 +7,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
alias Pleroma.Config
alias Pleroma.ConfigDB
- alias Pleroma.Plugs.OAuthScopesPlug
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(OAuthScopesPlug, %{scopes: ["write"], admin: true} when action == :update)
diff --git a/lib/pleroma/web/admin_api/controllers/frontend_controller.ex b/lib/pleroma/web/admin_api/controllers/frontend_controller.ex
new file mode 100644
index 000000000..fac3522b8
--- /dev/null
+++ b/lib/pleroma/web/admin_api/controllers/frontend_controller.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.AdminAPI.FrontendController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Config
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(OAuthScopesPlug, %{scopes: ["write"], admin: true} when action == :install)
+ 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.FrontendOperation
+
+ def index(conn, _params) do
+ installed = installed()
+
+ frontends =
+ [:frontends, :available]
+ |> Config.get([])
+ |> Enum.map(fn {name, desc} ->
+ Map.put(desc, "installed", name in installed)
+ end)
+
+ render(conn, "index.json", frontends: frontends)
+ end
+
+ def install(%{body_params: params} = conn, _params) do
+ with :ok <- Pleroma.Frontend.install(params.name, Map.delete(params, :name)) do
+ index(conn, %{})
+ end
+ end
+
+ defp installed do
+ File.ls!(Pleroma.Frontend.dir())
+ end
+end
diff --git a/lib/pleroma/web/admin_api/controllers/instance_document_controller.ex b/lib/pleroma/web/admin_api/controllers/instance_document_controller.ex
new file mode 100644
index 000000000..37dbfeb72
--- /dev/null
+++ b/lib/pleroma/web/admin_api/controllers/instance_document_controller.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.AdminAPI.InstanceDocumentController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Web.InstanceDocument
+ alias Pleroma.Web.Plugs.InstanceStatic
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+ action_fallback(Pleroma.Web.AdminAPI.FallbackController)
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.InstanceDocumentOperation
+
+ plug(OAuthScopesPlug, %{scopes: ["read"], admin: true} when action == :show)
+ plug(OAuthScopesPlug, %{scopes: ["write"], admin: true} when action in [:update, :delete])
+
+ def show(conn, %{name: document_name}) do
+ with {:ok, url} <- InstanceDocument.get(document_name),
+ {:ok, content} <- File.read(InstanceStatic.file_path(url)) do
+ conn
+ |> put_resp_content_type("text/html")
+ |> send_resp(200, content)
+ end
+ end
+
+ def update(%{body_params: %{file: file}} = conn, %{name: document_name}) do
+ with {:ok, url} <- InstanceDocument.put(document_name, file.path) do
+ json(conn, %{"url" => url})
+ end
+ end
+
+ def delete(conn, %{name: document_name}) do
+ with :ok <- InstanceDocument.delete(document_name) do
+ json(conn, %{})
+ end
+ end
+end
diff --git a/lib/pleroma/web/admin_api/controllers/invite_controller.ex b/lib/pleroma/web/admin_api/controllers/invite_controller.ex
index 7d169b8d2..6a9b4038a 100644
--- a/lib/pleroma/web/admin_api/controllers/invite_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/invite_controller.ex
@@ -8,8 +8,8 @@ defmodule Pleroma.Web.AdminAPI.InviteController do
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
alias Pleroma.Config
- alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.UserInviteToken
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
require Logger
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
index e2759d59f..6d92e9f7f 100644
--- a/lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex
@@ -5,9 +5,9 @@
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
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
plug(Pleroma.Web.ApiSpec.CastAndValidate)
@@ -26,29 +26,40 @@ defmodule Pleroma.Web.AdminAPI.MediaProxyCacheController do
defdelegate open_api_operation(action), to: Spec.MediaProxyCacheOperation
def index(%{assigns: %{user: _}} = conn, params) do
- cursor =
- :banned_urls_cache
- |> :ets.table([{:traverse, {:select, Cachex.Query.create(true, :key)}}])
- |> :qlc.cursor()
+ 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
- urls =
- case params.page do
- 1 ->
- :qlc.next_answers(cursor, params.page_size)
+ defp filter_entries(stream, query) when is_binary(query) do
+ regex = ~r/#{query}/i
- _ ->
- :qlc.next_answers(cursor, (params.page - 1) * params.page_size)
- :qlc.next_answers(cursor, params.page_size)
- end
+ stream
+ |> Enum.filter(fn url -> String.match?(url, regex) end)
+ |> Enum.to_list()
+ end
- :qlc.delete_cursor(cursor)
+ defp filter_entries(stream, _), do: Enum.to_list(stream)
- render(conn, "index.json", urls: urls)
+ 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)
- render(conn, "index.json", urls: urls)
+ json(conn, %{})
end
def purge(%{assigns: %{user: _}, body_params: %{urls: urls, ban: ban}} = conn, _) do
@@ -58,6 +69,6 @@ defmodule Pleroma.Web.AdminAPI.MediaProxyCacheController do
MediaProxy.put_in_banned_urls(urls)
end
- render(conn, "index.json", urls: urls)
+ json(conn, %{})
end
end
diff --git a/lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex b/lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex
index dca23ea73..116a05a4d 100644
--- a/lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex
@@ -7,8 +7,8 @@ defmodule Pleroma.Web.AdminAPI.OAuthAppController do
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
- alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Web.OAuth.App
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
require Logger
diff --git a/lib/pleroma/web/admin_api/controllers/relay_controller.ex b/lib/pleroma/web/admin_api/controllers/relay_controller.ex
index cf9f3a14b..611388447 100644
--- a/lib/pleroma/web/admin_api/controllers/relay_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/relay_controller.ex
@@ -6,8 +6,8 @@ defmodule Pleroma.Web.AdminAPI.RelayController do
use Pleroma.Web, :controller
alias Pleroma.ModerationLog
- alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Web.ActivityPub.Relay
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
require Logger
@@ -33,13 +33,9 @@ defmodule Pleroma.Web.AdminAPI.RelayController do
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
- })
+ ModerationLog.insert_log(%{action: "relay_follow", actor: admin, target: target})
- json(conn, target)
+ json(conn, %{actor: target, followed_back: target in Relay.following()})
else
_ ->
conn
@@ -48,13 +44,9 @@ defmodule Pleroma.Web.AdminAPI.RelayController do
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
- })
+ def unfollow(%{assigns: %{user: admin}, body_params: %{relay_url: target} = params} = conn, _) do
+ with {:ok, _message} <- Relay.unfollow(target, %{force: params[:force]}) do
+ ModerationLog.insert_log(%{action: "relay_unfollow", actor: admin, target: target})
json(conn, target)
else
diff --git a/lib/pleroma/web/admin_api/controllers/report_controller.ex b/lib/pleroma/web/admin_api/controllers/report_controller.ex
index 4c011e174..6a0e56f5f 100644
--- a/lib/pleroma/web/admin_api/controllers/report_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/report_controller.ex
@@ -9,12 +9,12 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
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
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
require Logger
@@ -38,7 +38,7 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
end
def show(conn, %{id: id}) do
- with %Activity{} = report <- Activity.get_by_id(id) do
+ with %Activity{} = report <- Activity.get_report(id) do
render(conn, "show.json", Report.extract_report_info(report))
else
_ -> {:error, :not_found}
diff --git a/lib/pleroma/web/admin_api/controllers/status_controller.ex b/lib/pleroma/web/admin_api/controllers/status_controller.ex
index bc48cc527..2bb437cfe 100644
--- a/lib/pleroma/web/admin_api/controllers/status_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/status_controller.ex
@@ -7,10 +7,10 @@ defmodule Pleroma.Web.AdminAPI.StatusController do
alias Pleroma.Activity
alias Pleroma.ModerationLog
- alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
require Logger
diff --git a/lib/pleroma/web/admin_api/controllers/user_controller.ex b/lib/pleroma/web/admin_api/controllers/user_controller.ex
new file mode 100644
index 000000000..a2a1c875d
--- /dev/null
+++ b/lib/pleroma/web/admin_api/controllers/user_controller.ex
@@ -0,0 +1,281 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.AdminAPI.UserController do
+ use Pleroma.Web, :controller
+
+ import Pleroma.Web.ControllerHelper,
+ only: [fetch_integer_param: 3]
+
+ alias Pleroma.ModerationLog
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.Builder
+ alias Pleroma.Web.ActivityPub.Pipeline
+ alias Pleroma.Web.AdminAPI
+ alias Pleroma.Web.AdminAPI.AccountView
+ alias Pleroma.Web.AdminAPI.Search
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+
+ @users_page_size 50
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["read:accounts"], admin: true}
+ when action in [:list, :show]
+ )
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["write:accounts"], admin: true}
+ when action in [
+ :delete,
+ :create,
+ :toggle_activation,
+ :activate,
+ :deactivate,
+ :approve
+ ]
+ )
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["write:follows"], admin: true}
+ when action in [:follow, :unfollow]
+ )
+
+ action_fallback(AdminAPI.FallbackController)
+
+ def delete(conn, %{"nickname" => nickname}) do
+ delete(conn, %{"nicknames" => [nickname]})
+ end
+
+ def delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
+ users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
+
+ Enum.each(users, fn user ->
+ {:ok, delete_data, _} = Builder.delete(admin, user.ap_id)
+ Pipeline.common_pipeline(delete_data, local: true)
+ end)
+
+ ModerationLog.insert_log(%{
+ actor: admin,
+ subject: users,
+ action: "delete"
+ })
+
+ json(conn, nicknames)
+ end
+
+ def follow(%{assigns: %{user: admin}} = conn, %{
+ "follower" => follower_nick,
+ "followed" => followed_nick
+ }) do
+ with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
+ %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
+ User.follow(follower, followed)
+
+ ModerationLog.insert_log(%{
+ actor: admin,
+ followed: followed,
+ follower: follower,
+ action: "follow"
+ })
+ end
+
+ json(conn, "ok")
+ end
+
+ def unfollow(%{assigns: %{user: admin}} = conn, %{
+ "follower" => follower_nick,
+ "followed" => followed_nick
+ }) do
+ with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
+ %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
+ User.unfollow(follower, followed)
+
+ ModerationLog.insert_log(%{
+ actor: admin,
+ followed: followed,
+ follower: follower,
+ action: "unfollow"
+ })
+ end
+
+ json(conn, "ok")
+ end
+
+ def create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
+ changesets =
+ Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} ->
+ user_data = %{
+ nickname: nickname,
+ name: nickname,
+ email: email,
+ password: password,
+ password_confirmation: password,
+ bio: "."
+ }
+
+ User.register_changeset(%User{}, user_data, need_confirmation: false)
+ end)
+ |> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
+ Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
+ end)
+
+ case Pleroma.Repo.transaction(changesets) do
+ {:ok, users} ->
+ res =
+ users
+ |> Map.values()
+ |> Enum.map(fn user ->
+ {:ok, user} = User.post_register_action(user)
+
+ user
+ end)
+ |> Enum.map(&AccountView.render("created.json", %{user: &1}))
+
+ ModerationLog.insert_log(%{
+ actor: admin,
+ subjects: Map.values(users),
+ action: "create"
+ })
+
+ json(conn, res)
+
+ {:error, id, changeset, _} ->
+ res =
+ Enum.map(changesets.operations, fn
+ {current_id, {:changeset, _current_changeset, _}} when current_id == id ->
+ AccountView.render("create-error.json", %{changeset: changeset})
+
+ {_, {:changeset, current_changeset, _}} ->
+ AccountView.render("create-error.json", %{changeset: current_changeset})
+ end)
+
+ conn
+ |> put_status(:conflict)
+ |> json(res)
+ end
+ end
+
+ def 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})
+ else
+ _ -> {:error, :not_found}
+ end
+ end
+
+ def toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
+ user = User.get_cached_by_nickname(nickname)
+
+ {:ok, updated_user} = User.deactivate(user, !user.deactivated)
+
+ action = if user.deactivated, do: "activate", else: "deactivate"
+
+ ModerationLog.insert_log(%{
+ actor: admin,
+ subject: [user],
+ action: action
+ })
+
+ conn
+ |> put_view(AccountView)
+ |> render("show.json", %{user: updated_user})
+ end
+
+ def activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
+ users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
+ {:ok, updated_users} = User.deactivate(users, false)
+
+ ModerationLog.insert_log(%{
+ actor: admin,
+ subject: users,
+ action: "activate"
+ })
+
+ conn
+ |> put_view(AccountView)
+ |> render("index.json", %{users: Keyword.values(updated_users)})
+ end
+
+ def deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
+ users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
+ {:ok, updated_users} = User.deactivate(users, true)
+
+ ModerationLog.insert_log(%{
+ actor: admin,
+ subject: users,
+ action: "deactivate"
+ })
+
+ conn
+ |> put_view(AccountView)
+ |> render("index.json", %{users: Keyword.values(updated_users)})
+ end
+
+ def 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 list(conn, params) do
+ {page, page_size} = page_params(params)
+ filters = maybe_parse_filters(params["filters"])
+
+ search_params =
+ %{
+ query: params["query"],
+ page: page,
+ page_size: page_size,
+ tags: params["tags"],
+ name: params["name"],
+ email: params["email"],
+ actor_types: params["actor_types"]
+ }
+ |> Map.merge(filters)
+
+ with {:ok, users, count} <- Search.user(search_params) do
+ json(
+ conn,
+ AccountView.render("index.json",
+ users: users,
+ count: count,
+ page_size: page_size
+ )
+ )
+ end
+ end
+
+ @filters ~w(local external active deactivated need_approval unconfirmed 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: %{}
+
+ defp maybe_parse_filters(filters) do
+ filters
+ |> String.split(",")
+ |> Enum.filter(&Enum.member?(@filters, &1))
+ |> Map.new(&{String.to_existing_atom(&1), true})
+ end
+
+ defp page_params(params) do
+ {
+ fetch_integer_param(params, "page", 1),
+ fetch_integer_param(params, "page_size", @users_page_size)
+ }
+ 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 88fbb5315..8bac24d3e 100644
--- a/lib/pleroma/web/admin_api/views/account_view.ex
+++ b/lib/pleroma/web/admin_api/views/account_view.ex
@@ -39,7 +39,7 @@ defmodule Pleroma.Web.AdminAPI.AccountView do
:fields,
:name,
:nickname,
- :locked,
+ :is_locked,
:no_rich_text,
:default_scope,
:hide_follows,
@@ -52,7 +52,7 @@ defmodule Pleroma.Web.AdminAPI.AccountView do
:skip_thread_containment,
:pleroma_settings_store,
:raw_fields,
- :discoverable,
+ :is_discoverable,
:actor_type
])
|> Map.merge(%{
@@ -77,7 +77,10 @@ defmodule Pleroma.Web.AdminAPI.AccountView do
"roles" => User.roles(user),
"tags" => user.tags || [],
"confirmation_pending" => user.confirmation_pending,
- "url" => user.uri || user.ap_id
+ "approval_pending" => user.approval_pending,
+ "url" => user.uri || user.ap_id,
+ "registration_reason" => user.registration_reason,
+ "actor_type" => user.actor_type
}
end
diff --git a/lib/pleroma/web/admin_api/views/chat_view.ex b/lib/pleroma/web/admin_api/views/chat_view.ex
new file mode 100644
index 000000000..847df1423
--- /dev/null
+++ b/lib/pleroma/web/admin_api/views/chat_view.ex
@@ -0,0 +1,30 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.AdminAPI.ChatView do
+ use Pleroma.Web, :view
+
+ alias Pleroma.Chat
+ alias Pleroma.User
+ alias Pleroma.Web.MastodonAPI
+ alias Pleroma.Web.PleromaAPI
+
+ def render("index.json", %{chats: chats} = opts) do
+ render_many(chats, __MODULE__, "show.json", Map.delete(opts, :chats))
+ end
+
+ def render("show.json", %{chat: %Chat{user_id: user_id}} = opts) do
+ user = User.get_by_id(user_id)
+ sender = MastodonAPI.AccountView.render("show.json", user: user, skip_visibility_check: true)
+
+ serialized_chat = PleromaAPI.ChatView.render("show.json", opts)
+
+ serialized_chat
+ |> Map.put(:sender, sender)
+ |> Map.put(:receiver, serialized_chat[:account])
+ |> Map.delete(:account)
+ end
+
+ def render(view, opts), do: PleromaAPI.ChatView.render(view, opts)
+end
diff --git a/lib/pleroma/web/admin_api/views/frontend_view.ex b/lib/pleroma/web/admin_api/views/frontend_view.ex
new file mode 100644
index 000000000..374841d0b
--- /dev/null
+++ b/lib/pleroma/web/admin_api/views/frontend_view.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.Web.AdminAPI.FrontendView do
+ use Pleroma.Web, :view
+
+ def render("index.json", %{frontends: frontends}) do
+ render_many(frontends, __MODULE__, "show.json")
+ end
+
+ def render("show.json", %{frontend: frontend}) do
+ %{
+ name: frontend["name"],
+ git: frontend["git"],
+ build_url: frontend["build_url"],
+ ref: frontend["ref"],
+ installed: frontend["installed"]
+ }
+ 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
index c97400beb..a803bda0b 100644
--- a/lib/pleroma/web/admin_api/views/media_proxy_cache_view.ex
+++ b/lib/pleroma/web/admin_api/views/media_proxy_cache_view.ex
@@ -5,7 +5,11 @@
defmodule Pleroma.Web.AdminAPI.MediaProxyCacheView do
use Pleroma.Web, :view
- def render("index.json", %{urls: urls}) do
- %{urls: urls}
+ 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 773f798fe..535556370 100644
--- a/lib/pleroma/web/admin_api/views/report_view.ex
+++ b/lib/pleroma/web/admin_api/views/report_view.ex
@@ -52,7 +52,7 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
end
def render("index_notes.json", %{notes: notes}) when is_list(notes) do
- Enum.map(notes, &render(__MODULE__, "show_note.json", &1))
+ Enum.map(notes, &render(__MODULE__, "show_note.json", Map.from_struct(&1)))
end
def render("index_notes.json", _), do: []
diff --git a/lib/pleroma/web/admin_api/views/status_view.ex b/lib/pleroma/web/admin_api/views/status_view.ex
index 500800be2..6042a22b6 100644
--- a/lib/pleroma/web/admin_api/views/status_view.ex
+++ b/lib/pleroma/web/admin_api/views/status_view.ex
@@ -8,6 +8,7 @@ defmodule Pleroma.Web.AdminAPI.StatusView do
require Pleroma.Constants
alias Pleroma.Web.AdminAPI
+ alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI
defdelegate merge_account_views(user), to: AdminAPI.AccountView
@@ -17,7 +18,7 @@ defmodule Pleroma.Web.AdminAPI.StatusView do
end
def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
- user = MastodonAPI.StatusView.get_user(activity.data["actor"])
+ user = CommonAPI.get_user(activity.data["actor"])
MastodonAPI.StatusView.render("show.json", opts)
|> Map.merge(%{account: merge_account_views(user)})
diff --git a/lib/pleroma/web/api_spec.ex b/lib/pleroma/web/api_spec.ex
index 79fd5f871..93a5273e3 100644
--- a/lib/pleroma/web/api_spec.ex
+++ b/lib/pleroma/web/api_spec.ex
@@ -13,10 +13,15 @@ defmodule Pleroma.Web.ApiSpec do
@impl OpenApi
def spec do
%OpenApi{
- servers: [
- # Populate the Server info from a phoenix endpoint
- OpenApiSpex.Server.from_endpoint(Endpoint)
- ],
+ servers:
+ if Phoenix.Endpoint.server?(:pleroma, Endpoint) do
+ [
+ # Populate the Server info from a phoenix endpoint
+ OpenApiSpex.Server.from_endpoint(Endpoint)
+ ]
+ else
+ []
+ end,
info: %OpenApiSpex.Info{
title: "Pleroma",
description: Application.spec(:pleroma, :description) |> to_string(),
diff --git a/lib/pleroma/web/api_spec/cast_and_validate.ex b/lib/pleroma/web/api_spec/cast_and_validate.ex
index fbfc27d6f..6d1a7ebbc 100644
--- a/lib/pleroma/web/api_spec/cast_and_validate.ex
+++ b/lib/pleroma/web/api_spec/cast_and_validate.ex
@@ -115,6 +115,10 @@ defmodule Pleroma.Web.ApiSpec.CastAndValidate do
%{reason: :unexpected_field, name: name, path: [name]}, params ->
Map.delete(params, name)
+ # Filter out empty params
+ %{reason: :invalid_type, path: [name_atom], value: ""}, params ->
+ Map.delete(params, to_string(name_atom))
+
%{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))
diff --git a/lib/pleroma/web/api_spec/helpers.ex b/lib/pleroma/web/api_spec/helpers.ex
index 2a7f1a706..34de2ed57 100644
--- a/lib/pleroma/web/api_spec/helpers.ex
+++ b/lib/pleroma/web/api_spec/helpers.ex
@@ -72,7 +72,11 @@ defmodule Pleroma.Web.ApiSpec.Helpers do
end
def empty_array_response do
- Operation.response("Empty array", "application/json", %Schema{type: :array, example: []})
+ Operation.response("Empty array", "application/json", %Schema{
+ type: :array,
+ items: %Schema{type: :object, example: %{}},
+ example: []
+ })
end
def no_content_response do
diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex
index 50c8e0242..280100c3d 100644
--- a/lib/pleroma/web/api_spec/operations/account_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/account_operation.ex
@@ -139,6 +139,12 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
:query,
%Schema{type: :array, items: VisibilityScope},
"Exclude visibilities"
+ ),
+ Operation.parameter(
+ :with_muted,
+ :query,
+ BooleanLike,
+ "Include reactions from muted acccounts."
)
] ++ pagination_params(),
responses: %{
@@ -262,6 +268,12 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
:query,
%Schema{allOf: [BooleanLike], default: true},
"Mute notifications in addition to statuses? Defaults to `true`."
+ ),
+ Operation.parameter(
+ :expires_in,
+ :query,
+ %Schema{type: :integer, default: 0},
+ "Expire the mute in `expires_in` seconds. Default 0 for infinity"
)
],
responses: %{
@@ -335,6 +347,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
operationId: "AccountController.mutes",
description: "Accounts the user has muted.",
security: [%{"oAuth" => ["follow", "read:mutes"]}],
+ parameters: pagination_params(),
responses: %{
200 => Operation.response("Accounts", "application/json", array_of_accounts())
}
@@ -348,6 +361,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
operationId: "AccountController.blocks",
description: "View your blocks. See also accounts/:id/{block,unblock}",
security: [%{"oAuth" => ["read:blocks"]}],
+ parameters: pagination_params(),
responses: %{
200 => Operation.response("Accounts", "application/json", array_of_accounts())
}
@@ -372,6 +386,10 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
tags: ["accounts"],
summary: "Identity proofs",
operationId: "AccountController.identity_proofs",
+ # Validators complains about unused path params otherwise
+ parameters: [
+ %Reference{"$ref": "#/components/parameters/accountIdOrNickname"}
+ ],
description: "Not implemented",
responses: %{
200 => empty_array_response()
@@ -449,21 +467,31 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
}
end
- # TODO: This is actually a token respone, but there's no oauth operation file yet.
+ # 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}
+ expires_in: %Schema{type: :integer},
+ #
+ # The response when registration succeeds but auto-login fails (no token):
+ identifier: %Schema{type: :string},
+ message: %Schema{type: :string}
},
+ # 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",
@@ -519,7 +547,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
nullable: true,
oneOf: [
%Schema{type: :array, items: attribute_field()},
- %Schema{type: :object, additionalProperties: %Schema{type: attribute_field()}}
+ %Schema{type: :object, additionalProperties: attribute_field()}
]
},
# NOTE: `source` field is not supported
@@ -596,7 +624,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
allOf: [BooleanLike],
nullable: true,
description:
- "Discovery of this account in search results and other services is allowed."
+ "Discovery (listing, indexing) of this account by external services (search bots etc.) is allowed."
},
actor_type: ActorType
},
@@ -707,10 +735,17 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
nullable: true,
description: "Mute notifications in addition to statuses? Defaults to true.",
default: true
+ },
+ expires_in: %Schema{
+ type: :integer,
+ nullable: true,
+ description: "Expire the mute in `expires_in` seconds. Default 0 for infinity",
+ default: 0
}
},
example: %{
- "notifications" => true
+ "notifications" => true,
+ "expires_in" => 86_400
}
}
end
diff --git a/lib/pleroma/web/api_spec/operations/admin/chat_operation.ex b/lib/pleroma/web/api_spec/operations/admin/chat_operation.ex
new file mode 100644
index 000000000..d3e5dfc1c
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/admin/chat_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.Admin.ChatOperation do
+ alias OpenApiSpex.Operation
+ alias Pleroma.Web.ApiSpec.Schemas.Chat
+ alias Pleroma.Web.ApiSpec.Schemas.ChatMessage
+
+ import Pleroma.Web.ApiSpec.Helpers
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def delete_message_operation do
+ %Operation{
+ tags: ["admin", "chat"],
+ summary: "Delete an individual chat message",
+ operationId: "AdminAPI.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 messages_operation do
+ %Operation{
+ tags: ["admin", "chat"],
+ summary: "Get the most recent messages of the chat",
+ operationId: "AdminAPI.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",
+ Pleroma.Web.ApiSpec.ChatOperation.chat_messages_response()
+ )
+ },
+ security: [
+ %{
+ "oAuth" => ["read:chats"]
+ }
+ ]
+ }
+ end
+
+ def show_operation do
+ %Operation{
+ tags: ["chat"],
+ summary: "Create a chat",
+ operationId: "AdminAPI.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
+end
diff --git a/lib/pleroma/web/api_spec/operations/admin/frontend_operation.ex b/lib/pleroma/web/api_spec/operations/admin/frontend_operation.ex
new file mode 100644
index 000000000..96d4cdee7
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/admin/frontend_operation.ex
@@ -0,0 +1,85 @@
+# 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.FrontendOperation 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", "Reports"],
+ summary: "Get a list of available frontends",
+ operationId: "AdminAPI.FrontendController.index",
+ security: [%{"oAuth" => ["read"]}],
+ responses: %{
+ 200 => Operation.response("Response", "application/json", list_of_frontends()),
+ 403 => Operation.response("Forbidden", "application/json", ApiError)
+ }
+ }
+ end
+
+ def install_operation do
+ %Operation{
+ tags: ["Admin", "Reports"],
+ summary: "Install a frontend",
+ operationId: "AdminAPI.FrontendController.install",
+ security: [%{"oAuth" => ["read"]}],
+ requestBody: request_body("Parameters", install_request(), required: true),
+ responses: %{
+ 200 => Operation.response("Response", "application/json", list_of_frontends()),
+ 403 => Operation.response("Forbidden", "application/json", ApiError),
+ 400 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp list_of_frontends do
+ %Schema{
+ type: :array,
+ items: %Schema{
+ type: :object,
+ properties: %{
+ name: %Schema{type: :string},
+ git: %Schema{type: :string, format: :uri, nullable: true},
+ build_url: %Schema{type: :string, format: :uri, nullable: true},
+ ref: %Schema{type: :string},
+ installed: %Schema{type: :boolean}
+ }
+ }
+ }
+ end
+
+ defp install_request do
+ %Schema{
+ title: "FrontendInstallRequest",
+ type: :object,
+ required: [:name],
+ properties: %{
+ name: %Schema{
+ type: :string
+ },
+ ref: %Schema{
+ type: :string
+ },
+ file: %Schema{
+ type: :string
+ },
+ build_url: %Schema{
+ type: :string
+ },
+ build_dir: %Schema{
+ type: :string
+ }
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/admin/instance_document_operation.ex b/lib/pleroma/web/api_spec/operations/admin/instance_document_operation.ex
new file mode 100644
index 000000000..a120ff4e8
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/admin/instance_document_operation.ex
@@ -0,0 +1,115 @@
+# 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.InstanceDocumentOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Helpers
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def show_operation do
+ %Operation{
+ tags: ["Admin", "InstanceDocument"],
+ summary: "Get the instance document",
+ operationId: "AdminAPI.InstanceDocumentController.show",
+ security: [%{"oAuth" => ["read"]}],
+ parameters: [
+ Operation.parameter(:name, :path, %Schema{type: :string}, "The document name",
+ required: true
+ )
+ | Helpers.admin_api_params()
+ ],
+ responses: %{
+ 200 => document_content(),
+ 400 => Operation.response("Bad Request", "application/json", ApiError),
+ 403 => Operation.response("Forbidden", "application/json", ApiError),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def update_operation do
+ %Operation{
+ tags: ["Admin", "InstanceDocument"],
+ summary: "Update the instance document",
+ operationId: "AdminAPI.InstanceDocumentController.update",
+ security: [%{"oAuth" => ["write"]}],
+ requestBody: Helpers.request_body("Parameters", update_request()),
+ parameters: [
+ Operation.parameter(:name, :path, %Schema{type: :string}, "The document name",
+ required: true
+ )
+ | Helpers.admin_api_params()
+ ],
+ responses: %{
+ 200 => Operation.response("InstanceDocument", "application/json", instance_document()),
+ 400 => Operation.response("Bad Request", "application/json", ApiError),
+ 403 => Operation.response("Forbidden", "application/json", ApiError),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp update_request do
+ %Schema{
+ title: "UpdateRequest",
+ description: "POST body for uploading the file",
+ type: :object,
+ required: [:file],
+ properties: %{
+ file: %Schema{
+ type: :string,
+ format: :binary,
+ description: "The file to be uploaded, using multipart form data."
+ }
+ }
+ }
+ end
+
+ def delete_operation do
+ %Operation{
+ tags: ["Admin", "InstanceDocument"],
+ summary: "Get the instance document",
+ operationId: "AdminAPI.InstanceDocumentController.delete",
+ security: [%{"oAuth" => ["write"]}],
+ parameters: [
+ Operation.parameter(:name, :path, %Schema{type: :string}, "The document name",
+ required: true
+ )
+ | Helpers.admin_api_params()
+ ],
+ responses: %{
+ 200 => Operation.response("InstanceDocument", "application/json", instance_document()),
+ 400 => Operation.response("Bad Request", "application/json", ApiError),
+ 403 => Operation.response("Forbidden", "application/json", ApiError),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp instance_document do
+ %Schema{
+ title: "InstanceDocument",
+ type: :object,
+ properties: %{
+ url: %Schema{type: :string}
+ },
+ example: %{
+ "url" => "https://example.com/static/terms-of-service.html"
+ }
+ }
+ end
+
+ defp document_content do
+ Operation.response("InstanceDocumentContent", "text/html", %Schema{
+ type: :string,
+ example: "<h1>Instance panel</h1>"
+ })
+ 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
index 20d033f66..ab45d6633 100644
--- 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
@@ -22,6 +22,12 @@ defmodule Pleroma.Web.ApiSpec.Admin.MediaProxyCacheOperation do
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},
@@ -36,7 +42,26 @@ defmodule Pleroma.Web.ApiSpec.Admin.MediaProxyCacheOperation do
| admin_api_params()
],
responses: %{
- 200 => success_response()
+ 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
@@ -61,7 +86,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.MediaProxyCacheOperation do
required: true
),
responses: %{
- 200 => success_response(),
+ 200 => empty_object_response(),
400 => Operation.response("Error", "application/json", ApiError)
}
}
@@ -88,25 +113,9 @@ defmodule Pleroma.Web.ApiSpec.Admin.MediaProxyCacheOperation do
required: true
),
responses: %{
- 200 => success_response(),
+ 200 => empty_object_response(),
400 => Operation.response("Error", "application/json", ApiError)
}
}
end
-
- defp success_response do
- Operation.response("Array of banned MediaProxy URLs in Cachex", "application/json", %Schema{
- type: :object,
- properties: %{
- urls: %Schema{
- type: :array,
- items: %Schema{
- type: :string,
- format: :uri,
- description: "MediaProxy URLs"
- }
- }
- }
- })
- end
end
diff --git a/lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex b/lib/pleroma/web/api_spec/operations/admin/o_auth_app_operation.ex
index a75f3e622..a75f3e622 100644
--- a/lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/admin/o_auth_app_operation.ex
diff --git a/lib/pleroma/web/api_spec/operations/admin/relay_operation.ex b/lib/pleroma/web/api_spec/operations/admin/relay_operation.ex
index 67ee5eee0..f754bb9f5 100644
--- a/lib/pleroma/web/api_spec/operations/admin/relay_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/admin/relay_operation.ex
@@ -27,8 +27,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.RelayOperation do
properties: %{
relays: %Schema{
type: :array,
- items: %Schema{type: :string},
- example: ["lain.com", "mstdn.io"]
+ items: relay()
}
}
})
@@ -43,19 +42,9 @@ defmodule Pleroma.Web.ApiSpec.Admin.RelayOperation do
operationId: "AdminAPI.RelayController.follow",
security: [%{"oAuth" => ["write:follows"]}],
parameters: admin_api_params(),
- requestBody:
- request_body("Parameters", %Schema{
- type: :object,
- properties: %{
- relay_url: %Schema{type: :string, format: :uri}
- }
- }),
+ requestBody: request_body("Parameters", relay_url()),
responses: %{
- 200 =>
- Operation.response("Status", "application/json", %Schema{
- type: :string,
- example: "http://mastodon.example.org/users/admin"
- })
+ 200 => Operation.response("Status", "application/json", relay())
}
}
end
@@ -67,13 +56,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.RelayOperation do
operationId: "AdminAPI.RelayController.unfollow",
security: [%{"oAuth" => ["write:follows"]}],
parameters: admin_api_params(),
- requestBody:
- request_body("Parameters", %Schema{
- type: :object,
- properties: %{
- relay_url: %Schema{type: :string, format: :uri}
- }
- }),
+ requestBody: request_body("Parameters", relay_unfollow()),
responses: %{
200 =>
Operation.response("Status", "application/json", %Schema{
@@ -83,4 +66,39 @@ defmodule Pleroma.Web.ApiSpec.Admin.RelayOperation do
}
}
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
+
+ defp relay_unfollow do
+ %Schema{
+ type: :object,
+ properties: %{
+ relay_url: %Schema{type: :string, format: :uri},
+ force: %Schema{type: :boolean, default: false}
+ }
+ }
+ end
end
diff --git a/lib/pleroma/web/api_spec/operations/chat_operation.ex b/lib/pleroma/web/api_spec/operations/chat_operation.ex
index b1a0d26ab..560b81f17 100644
--- a/lib/pleroma/web/api_spec/operations/chat_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/chat_operation.ex
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.ApiError
+ alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
alias Pleroma.Web.ApiSpec.Schemas.Chat
alias Pleroma.Web.ApiSpec.Schemas.ChatMessage
@@ -132,7 +133,10 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
tags: ["chat"],
summary: "Get a list of chats that you participated in",
operationId: "ChatController.index",
- parameters: pagination_params(),
+ parameters: [
+ Operation.parameter(:with_muted, :query, BooleanLike, "Include chats from muted users")
+ | pagination_params()
+ ],
responses: %{
200 => Operation.response("The chats of the user", "application/json", chats_response())
},
@@ -158,7 +162,8 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
"The messages in the chat",
"application/json",
chat_messages_response()
- )
+ ),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
},
security: [
%{
@@ -184,7 +189,8 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
"application/json",
ChatMessage
),
- 400 => Operation.response("Bad Request", "application/json", ApiError)
+ 400 => Operation.response("Bad Request", "application/json", ApiError),
+ 422 => Operation.response("MRF Rejection", "application/json", ApiError)
},
security: [
%{
diff --git a/lib/pleroma/web/api_spec/operations/custom_emoji_operation.ex b/lib/pleroma/web/api_spec/operations/custom_emoji_operation.ex
index 2f812ac77..5ff263ceb 100644
--- a/lib/pleroma/web/api_spec/operations/custom_emoji_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/custom_emoji_operation.ex
@@ -69,7 +69,7 @@ defmodule Pleroma.Web.ApiSpec.CustomEmojiOperation do
type: :object,
properties: %{
category: %Schema{type: :string},
- tags: %Schema{type: :array}
+ tags: %Schema{type: :array, items: %Schema{type: :string}}
}
}
],
diff --git a/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex b/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex
index 1a49fece0..9d0e39fc7 100644
--- a/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex
@@ -23,7 +23,13 @@ defmodule Pleroma.Web.ApiSpec.EmojiReactionOperation do
parameters: [
Operation.parameter(:id, :path, FlakeID, "Status ID", required: true),
Operation.parameter(:emoji, :path, :string, "Filter by a single unicode emoji",
- required: false
+ required: nil
+ ),
+ Operation.parameter(
+ :with_muted,
+ :query,
+ :boolean,
+ "Include reactions from muted acccounts."
)
],
security: [%{"oAuth" => ["read:statuses"]}],
diff --git a/lib/pleroma/web/api_spec/operations/list_operation.ex b/lib/pleroma/web/api_spec/operations/list_operation.ex
index c88ed5dd0..f6e73968a 100644
--- a/lib/pleroma/web/api_spec/operations/list_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/list_operation.ex
@@ -114,7 +114,7 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do
description: "Add accounts to the given list.",
operationId: "ListController.add_to_list",
parameters: [id_param()],
- requestBody: add_remove_accounts_request(),
+ requestBody: add_remove_accounts_request(true),
security: [%{"oAuth" => ["write:lists"]}],
responses: %{
200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
@@ -127,8 +127,16 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do
tags: ["Lists"],
summary: "Remove accounts from list",
operationId: "ListController.remove_from_list",
- parameters: [id_param()],
- requestBody: add_remove_accounts_request(),
+ parameters: [
+ id_param(),
+ Operation.parameter(
+ :account_ids,
+ :query,
+ %Schema{type: :array, items: %Schema{type: :string}},
+ "Array of account IDs"
+ )
+ ],
+ requestBody: add_remove_accounts_request(false),
security: [%{"oAuth" => ["write:lists"]}],
responses: %{
200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
@@ -171,7 +179,7 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do
)
end
- defp add_remove_accounts_request do
+ defp add_remove_accounts_request(required) when is_boolean(required) do
request_body(
"Parameters",
%Schema{
@@ -179,10 +187,9 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do
type: :object,
properties: %{
account_ids: %Schema{type: :array, description: "Array of account IDs", items: FlakeID}
- },
- required: [:account_ids]
+ }
},
- required: true
+ required: required
)
end
end
diff --git a/lib/pleroma/web/api_spec/operations/notification_operation.ex b/lib/pleroma/web/api_spec/operations/notification_operation.ex
index f09be64cb..264a530d2 100644
--- a/lib/pleroma/web/api_spec/operations/notification_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/notification_operation.ex
@@ -193,6 +193,7 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
"mention",
"pleroma:emoji_reaction",
"pleroma:chat_mention",
+ "pleroma:report",
"move",
"follow_request"
],
@@ -206,6 +207,8 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
- `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
+ - `pleroma:chat_mention` - Someone mentioned you in a chat message
+ - `pleroma:report` - Someone was reported
"""
}
end
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex
new file mode 100644
index 000000000..6993794db
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/pleroma_backup_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.PleromaBackupOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def index_operation do
+ %Operation{
+ tags: ["Backups"],
+ summary: "List backups",
+ security: [%{"oAuth" => ["read:account"]}],
+ operationId: "PleromaAPI.BackupController.index",
+ responses: %{
+ 200 =>
+ Operation.response(
+ "An array of backups",
+ "application/json",
+ %Schema{
+ type: :array,
+ items: backup()
+ }
+ ),
+ 400 => Operation.response("Bad Request", "application/json", ApiError)
+ }
+ }
+ end
+
+ def create_operation do
+ %Operation{
+ tags: ["Backups"],
+ summary: "Create a backup",
+ security: [%{"oAuth" => ["read:account"]}],
+ operationId: "PleromaAPI.BackupController.create",
+ responses: %{
+ 200 =>
+ Operation.response(
+ "An array of backups",
+ "application/json",
+ %Schema{
+ type: :array,
+ items: backup()
+ }
+ ),
+ 400 => Operation.response("Bad Request", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp backup do
+ %Schema{
+ title: "Backup",
+ description: "Response schema for a backup",
+ type: :object,
+ properties: %{
+ inserted_at: %Schema{type: :string, format: :"date-time"},
+ content_type: %Schema{type: :string},
+ file_name: %Schema{type: :string},
+ file_size: %Schema{type: :integer},
+ processed: %Schema{type: :boolean}
+ },
+ example: %{
+ "content_type" => "application/zip",
+ "file_name" =>
+ "https://cofe.fe:4000/media/backups/archive-foobar-20200908T164207-Yr7vuT5Wycv-sN3kSN2iJ0k-9pMo60j9qmvRCdDqIew.zip",
+ "file_size" => 4105,
+ "inserted_at" => "2020-09-08T16:42:07.000Z",
+ "processed" => true
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_emoji_file_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_emoji_file_operation.ex
new file mode 100644
index 000000000..a56641426
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/pleroma_emoji_file_operation.ex
@@ -0,0 +1,139 @@
+# 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.PleromaEmojiFileOperation 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 create_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", create_request(), required: true),
+ parameters: [name_param()],
+ responses: %{
+ 200 => Operation.response("Files Object", "application/json", files_object()),
+ 422 => Operation.response("Unprocessable Entity", "application/json", ApiError),
+ 404 => Operation.response("Not Found", "application/json", ApiError),
+ 400 => Operation.response("Bad Request", "application/json", ApiError),
+ 409 => Operation.response("Conflict", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp create_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_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_request(), required: true),
+ parameters: [name_param()],
+ responses: %{
+ 200 => Operation.response("Files Object", "application/json", files_object()),
+ 404 => Operation.response("Not Found", "application/json", ApiError),
+ 400 => Operation.response("Bad Request", "application/json", ApiError),
+ 409 => Operation.response("Conflict", "application/json", ApiError),
+ 422 => Operation.response("Unprocessable Entity", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp update_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_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),
+ 404 => Operation.response("Not Found", "application/json", ApiError),
+ 422 => Operation.response("Unprocessable Entity", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp name_param do
+ Operation.parameter(:name, :query, :string, "Pack Name", example: "cofe", required: true)
+ end
+
+ defp files_object do
+ %Schema{
+ type: :object,
+ additionalProperties: %Schema{type: :string},
+ description: "Object with emoji names as keys and filenames as values"
+ }
+ 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
index b2b4f8713..79f52dcb3 100644
--- a/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex
@@ -19,7 +19,21 @@ defmodule Pleroma.Web.ApiSpec.PleromaEmojiPackOperation do
tags: ["Emoji Packs"],
summary: "Make request to another instance for emoji packs list",
security: [%{"oAuth" => ["write"]}],
- parameters: [url_param()],
+ parameters: [
+ url_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"
+ )
+ ],
operationId: "PleromaAPI.EmojiPackController.remote",
responses: %{
200 => emoji_packs_response(),
@@ -175,111 +189,6 @@ defmodule Pleroma.Web.ApiSpec.PleromaEmojiPackOperation do
}
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"],
@@ -297,7 +206,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaEmojiPackOperation do
end
defp name_param do
- Operation.parameter(:name, :path, :string, "Pack Name", example: "cofe", required: true)
+ Operation.parameter(:name, :query, :string, "Pack Name", example: "cofe", required: true)
end
defp url_param do
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_instances_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_instances_operation.ex
new file mode 100644
index 000000000..2c455b0df
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/pleroma_instances_operation.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.ApiSpec.PleromaInstancesOperation 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: ["PleromaInstances"],
+ summary: "Instances federation status",
+ description: "Information about instances deemed unreachable by the server",
+ operationId: "PleromaInstances.show",
+ responses: %{
+ 200 => Operation.response("PleromaInstances", "application/json", pleroma_instances())
+ }
+ }
+ end
+
+ def pleroma_instances do
+ %Schema{
+ type: :object,
+ properties: %{
+ unreachable: %Schema{
+ type: :object,
+ properties: %{hostname: %Schema{type: :string, format: :"date-time"}}
+ }
+ },
+ example: %{
+ "unreachable" => %{"consistently-unreachable.name" => "2020-10-14 22:07:58.216473"}
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex
index 5bd4619d5..4ab918d83 100644
--- a/lib/pleroma/web/api_spec/operations/status_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/status_operation.ex
@@ -31,6 +31,12 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
:query,
%Schema{type: :array, items: FlakeID},
"Array of status IDs"
+ ),
+ Operation.parameter(
+ :with_muted,
+ :query,
+ BooleanLike,
+ "Include reactions from muted acccounts."
)
],
operationId: "StatusController.index",
@@ -55,7 +61,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
"application/json",
%Schema{oneOf: [Status, ScheduledStatus]}
),
- 422 => Operation.response("Bad Request", "application/json", ApiError)
+ 422 => Operation.response("Bad Request / MRF Rejection", "application/json", ApiError)
}
}
end
@@ -67,7 +73,15 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
description: "View information about a status",
operationId: "StatusController.show",
security: [%{"oAuth" => ["read:statuses"]}],
- parameters: [id_param()],
+ parameters: [
+ id_param(),
+ Operation.parameter(
+ :with_muted,
+ :query,
+ BooleanLike,
+ "Include reactions from muted acccounts."
+ )
+ ],
responses: %{
200 => status_response(),
404 => Operation.response("Not Found", "application/json", ApiError)
@@ -223,7 +237,27 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
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()],
+ requestBody:
+ request_body("Parameters", %Schema{
+ type: :object,
+ properties: %{
+ expires_in: %Schema{
+ type: :integer,
+ nullable: true,
+ description: "Expire the mute in `expires_in` seconds. Default 0 for infinity",
+ default: 0
+ }
+ }
+ }),
+ parameters: [
+ id_param(),
+ Operation.parameter(
+ :expires_in,
+ :query,
+ %Schema{type: :integer, default: 0},
+ "Expire the mute in `expires_in` seconds. Default 0 for infinity"
+ )
+ ],
responses: %{
200 => status_response(),
400 => Operation.response("Error", "application/json", ApiError)
diff --git a/lib/pleroma/web/api_spec/operations/subscription_operation.ex b/lib/pleroma/web/api_spec/operations/subscription_operation.ex
index 775dd795d..67c7ea8f3 100644
--- a/lib/pleroma/web/api_spec/operations/subscription_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/subscription_operation.ex
@@ -146,6 +146,11 @@ defmodule Pleroma.Web.ApiSpec.SubscriptionOperation do
allOf: [BooleanLike],
nullable: true,
description: "Receive chat notifications?"
+ },
+ "pleroma:emoji_reaction": %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Receive emoji reaction notifications?"
}
}
}
@@ -210,6 +215,16 @@ defmodule Pleroma.Web.ApiSpec.SubscriptionOperation do
allOf: [BooleanLike],
nullable: true,
description: "Receive poll notifications?"
+ },
+ "pleroma:chat_mention": %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Receive chat notifications?"
+ },
+ "pleroma:emoji_reaction": %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Receive emoji reaction notifications?"
}
}
}
diff --git a/lib/pleroma/web/api_spec/operations/timeline_operation.ex b/lib/pleroma/web/api_spec/operations/timeline_operation.ex
index 8e19bace7..95720df9f 100644
--- a/lib/pleroma/web/api_spec/operations/timeline_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/timeline_operation.ex
@@ -59,6 +59,7 @@ defmodule Pleroma.Web.ApiSpec.TimelineOperation do
security: [%{"oAuth" => ["read:statuses"]}],
parameters: [
local_param(),
+ instance_param(),
only_media_param(),
with_muted_param(),
exclude_visibilities_param(),
@@ -158,8 +159,17 @@ defmodule Pleroma.Web.ApiSpec.TimelineOperation do
)
end
+ defp instance_param do
+ Operation.parameter(
+ :instance,
+ :query,
+ %Schema{type: :string},
+ "Show only statuses from the given domain"
+ )
+ end
+
defp with_muted_param do
- Operation.parameter(:with_muted, :query, BooleanLike, "Includeactivities by muted users")
+ Operation.parameter(:with_muted, :query, BooleanLike, "Include activities by muted users")
end
defp exclude_visibilities_param do
diff --git a/lib/pleroma/web/api_spec/operations/user_import_operation.ex b/lib/pleroma/web/api_spec/operations/user_import_operation.ex
new file mode 100644
index 000000000..a50314fb7
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/user_import_operation.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.ApiSpec.UserImportOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+
+ 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 follow_operation do
+ %Operation{
+ tags: ["follow_import"],
+ summary: "Imports your follows.",
+ operationId: "UserImportController.follow",
+ requestBody: request_body("Parameters", import_request(), required: true),
+ responses: %{
+ 200 => ok_response(),
+ 500 => Operation.response("Error", "application/json", ApiError)
+ },
+ security: [%{"oAuth" => ["write:follow"]}]
+ }
+ end
+
+ def blocks_operation do
+ %Operation{
+ tags: ["blocks_import"],
+ summary: "Imports your blocks.",
+ operationId: "UserImportController.blocks",
+ requestBody: request_body("Parameters", import_request(), required: true),
+ responses: %{
+ 200 => ok_response(),
+ 500 => Operation.response("Error", "application/json", ApiError)
+ },
+ security: [%{"oAuth" => ["write:blocks"]}]
+ }
+ end
+
+ def mutes_operation do
+ %Operation{
+ tags: ["mutes_import"],
+ summary: "Imports your mutes.",
+ operationId: "UserImportController.mutes",
+ requestBody: request_body("Parameters", import_request(), required: true),
+ responses: %{
+ 200 => ok_response(),
+ 500 => Operation.response("Error", "application/json", ApiError)
+ },
+ security: [%{"oAuth" => ["write:mutes"]}]
+ }
+ end
+
+ defp import_request do
+ %Schema{
+ type: :object,
+ required: [:list],
+ properties: %{
+ list: %Schema{
+ description:
+ "STRING or FILE containing a whitespace-separated list of accounts to import.",
+ anyOf: [
+ %Schema{type: :string, format: :binary},
+ %Schema{type: :string}
+ ]
+ }
+ }
+ }
+ end
+
+ defp ok_response do
+ Operation.response("Ok", "application/json", %Schema{type: :string, example: "ok"})
+ end
+end
diff --git a/lib/pleroma/web/api_spec/schemas/account.ex b/lib/pleroma/web/api_spec/schemas/account.ex
index ca79f0747..684f6fc92 100644
--- a/lib/pleroma/web/api_spec/schemas/account.ex
+++ b/lib/pleroma/web/api_spec/schemas/account.ex
@@ -127,7 +127,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
discoverable: %Schema{
type: :boolean,
description:
- "whether the user allows discovery of the account in search results and other services."
+ "whether the user allows indexing / listing of the account by external services (search engines etc.)."
},
no_rich_text: %Schema{
type: :boolean,
diff --git a/lib/pleroma/web/api_spec/schemas/chat.ex b/lib/pleroma/web/api_spec/schemas/chat.ex
index b4986b734..65f908e33 100644
--- a/lib/pleroma/web/api_spec/schemas/chat.ex
+++ b/lib/pleroma/web/api_spec/schemas/chat.ex
@@ -50,7 +50,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Chat do
"fields" => []
},
"statuses_count" => 1,
- "locked" => false,
+ "is_locked" => false,
"created_at" => "2020-04-16T13:40:15.000Z",
"display_name" => "lain",
"fields" => [],
diff --git a/lib/pleroma/web/api_spec/schemas/chat_message.ex b/lib/pleroma/web/api_spec/schemas/chat_message.ex
index 3ee85aa76..9d2799618 100644
--- a/lib/pleroma/web/api_spec/schemas/chat_message.ex
+++ b/lib/pleroma/web/api_spec/schemas/chat_message.ex
@@ -4,6 +4,7 @@
defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessage do
alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.Emoji
require OpenApiSpex
@@ -18,14 +19,47 @@ defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessage do
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}
+ emojis: %Schema{type: :array, items: Emoji},
+ 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",
diff --git a/lib/pleroma/web/api_spec/schemas/poll.ex b/lib/pleroma/web/api_spec/schemas/poll.ex
index c62096db0..0dfa60b97 100644
--- a/lib/pleroma/web/api_spec/schemas/poll.ex
+++ b/lib/pleroma/web/api_spec/schemas/poll.ex
@@ -28,8 +28,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Poll do
},
votes_count: %Schema{
type: :integer,
- nullable: true,
- description: "How many votes have been received. Number, or null if `multiple` is false."
+ description: "How many votes have been received. Number."
+ },
+ voters_count: %Schema{
+ type: :integer,
+ description: "How many unique accounts have voted. Number."
},
voted: %Schema{
type: :boolean,
@@ -61,7 +64,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Poll do
expired: true,
multiple: false,
votes_count: 10,
- voters_count: nil,
+ voters_count: 10,
voted: true,
own_votes: [
1
diff --git a/lib/pleroma/web/api_spec/schemas/scheduled_status.ex b/lib/pleroma/web/api_spec/schemas/scheduled_status.ex
index 0520d0848..addefa9d3 100644
--- a/lib/pleroma/web/api_spec/schemas/scheduled_status.ex
+++ b/lib/pleroma/web/api_spec/schemas/scheduled_status.ex
@@ -27,9 +27,9 @@ defmodule Pleroma.Web.ApiSpec.Schemas.ScheduledStatus do
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},
+ visibility: %Schema{allOf: [VisibilityScope], nullable: true},
scheduled_at: %Schema{type: :string, format: :"date-time", nullable: true},
- poll: %Schema{type: Poll, nullable: true},
+ poll: %Schema{allOf: [Poll], nullable: true},
in_reply_to_id: %Schema{type: :string, nullable: true}
}
}
diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex
index 947e42890..e6890df2d 100644
--- a/lib/pleroma/web/api_spec/schemas/status.ex
+++ b/lib/pleroma/web/api_spec/schemas/status.ex
@@ -252,7 +252,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
"header" => "http://localhost:4001/images/banner.png",
"header_static" => "http://localhost:4001/images/banner.png",
"id" => "9toJCsKN7SmSf3aj5c",
- "locked" => false,
+ "is_locked" => false,
"note" => "Tester Number 6",
"pleroma" => %{
"background_image" => nil,
diff --git a/lib/pleroma/web/api_spec/schemas/visibility_scope.ex b/lib/pleroma/web/api_spec/schemas/visibility_scope.ex
index 831734e27..633269a92 100644
--- a/lib/pleroma/web/api_spec/schemas/visibility_scope.ex
+++ b/lib/pleroma/web/api_spec/schemas/visibility_scope.ex
@@ -9,6 +9,6 @@ defmodule Pleroma.Web.ApiSpec.Schemas.VisibilityScope do
title: "VisibilityScope",
description: "Status visibility",
type: :string,
- enum: ["public", "unlisted", "private", "direct", "list"]
+ enum: ["public", "unlisted", "local", "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 200ca03dc..d6d2a8d06 100644
--- a/lib/pleroma/web/auth/pleroma_authenticator.ex
+++ b/lib/pleroma/web/auth/pleroma_authenticator.ex
@@ -3,10 +3,10 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Auth.PleromaAuthenticator do
- alias Pleroma.Plugs.AuthenticationPlug
alias Pleroma.Registration
alias Pleroma.Repo
alias Pleroma.User
+ alias Pleroma.Web.Plugs.AuthenticationPlug
import Pleroma.Web.Auth.Authenticator,
only: [fetch_credentials: 1, fetch_user: 1]
@@ -68,7 +68,7 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do
nickname = value([registration_attrs["nickname"], Registration.nickname(registration)])
email = value([registration_attrs["email"], Registration.email(registration)])
name = value([registration_attrs["name"], Registration.name(registration)]) || nickname
- bio = value([registration_attrs["bio"], Registration.description(registration)])
+ bio = value([registration_attrs["bio"], Registration.description(registration)]) || ""
random_password = :crypto.strong_rand_bytes(64) |> Base.encode64()
diff --git a/lib/pleroma/web/auth/totp_authenticator.ex b/lib/pleroma/web/auth/totp_authenticator.ex
index 1794e407c..edc9871ea 100644
--- a/lib/pleroma/web/auth/totp_authenticator.ex
+++ b/lib/pleroma/web/auth/totp_authenticator.ex
@@ -5,8 +5,8 @@
defmodule Pleroma.Web.Auth.TOTPAuthenticator do
alias Pleroma.MFA
alias Pleroma.MFA.TOTP
- alias Pleroma.Plugs.AuthenticationPlug
alias Pleroma.User
+ alias Pleroma.Web.Plugs.AuthenticationPlug
@doc "Verify code or check backup code."
@spec verify(String.t(), User.t()) ::
diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api.ex
index 4d5b0decf..e59254791 100644
--- a/lib/pleroma/web/common_api/common_api.ex
+++ b/lib/pleroma/web/common_api.ex
@@ -4,11 +4,8 @@
defmodule Pleroma.Web.CommonAPI do
alias Pleroma.Activity
- alias Pleroma.ActivityExpiration
alias Pleroma.Conversation.Participation
- alias Pleroma.FollowingRelationship
alias Pleroma.Formatter
- alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.ThreadMute
alias Pleroma.User
@@ -18,6 +15,7 @@ defmodule Pleroma.Web.CommonAPI do
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
+ alias Pleroma.Web.CommonAPI.ActivityDraft
import Pleroma.Web.Gettext
import Pleroma.Web.CommonAPI.Utils
@@ -48,9 +46,13 @@ defmodule Pleroma.Web.CommonAPI do
{_, {:ok, %Activity{} = activity, _meta}} <-
{:common_pipeline,
Pipeline.common_pipeline(create_activity_data,
- local: true
+ local: true,
+ idempotency_key: opts[:idempotency_key]
)} do
{:ok, activity}
+ else
+ {:common_pipeline, {:reject, _} = e} -> e
+ e -> e
end
end
@@ -122,33 +124,16 @@ 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
- Notification.update_notification_type(followed, follow_activity)
+ {: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
@@ -308,18 +293,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"])
@@ -340,8 +326,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)
@@ -368,7 +359,7 @@ defmodule Pleroma.Web.CommonAPI do
def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
def get_visibility(%{visibility: visibility}, in_reply_to, _)
- when visibility in ~w{public unlisted private direct},
+ when visibility in ~w{public local unlisted private direct},
do: {visibility, get_replied_to_visibility(in_reply_to)}
def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
@@ -394,9 +385,9 @@ defmodule Pleroma.Web.CommonAPI do
def check_expiry_date({:ok, nil} = res), do: res
def check_expiry_date({:ok, in_seconds}) do
- expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
+ expiry = DateTime.add(DateTime.utc_now(), in_seconds)
- if ActivityExpiration.expires_late_enough?(expiry) do
+ if Pleroma.Workers.PurgeExpiredActivity.expires_late_enough?(expiry) do
{:ok, expiry}
else
{:error, "Expiry date is too soon"}
@@ -409,31 +400,13 @@ defmodule Pleroma.Web.CommonAPI do
end
def listen(user, data) do
- visibility = Map.get(data, :visibility, "public")
-
- with {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
- listen_data <-
- 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)
- |> Map.put("actor", user.ap_id),
- {:ok, activity} <-
- ActivityPub.listen(%{
- actor: user,
- to: to,
- object: listen_data,
- context: Utils.generate_context_id(),
- additional: %{"cc" => cc}
- }) do
- {:ok, activity}
+ with {:ok, draft} <- ActivityDraft.listen(user, data) do
+ ActivityPub.listen(draft.changes)
end
end
def post(user, %{status: _} = data) do
- with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
+ with {:ok, draft} <- ActivityDraft.create(user, data) do
ActivityPub.create(draft.changes, draft.preview?)
end
end
@@ -464,21 +437,48 @@ defmodule Pleroma.Web.CommonAPI do
end
end
- def add_mute(user, activity) do
- with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
+ def add_mute(user, activity, params \\ %{}) do
+ expires_in = Map.get(params, :expires_in, 0)
+
+ with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
+ _ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do
+ if expires_in > 0 do
+ Pleroma.Workers.MuteExpireWorker.enqueue(
+ "unmute_conversation",
+ %{"user_id" => user.id, "activity_id" => activity.id},
+ schedule_in: expires_in
+ )
+ end
+
{:ok, activity}
else
{:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
end
end
- def remove_mute(user, activity) do
+ def remove_mute(%User{} = user, %Activity{} = activity) do
ThreadMute.remove_mute(user.id, activity.data["context"])
{:ok, activity}
end
+ def remove_mute(user_id, activity_id) do
+ with {:user, %User{} = user} <- {:user, User.get_by_id(user_id)},
+ {:activity, %Activity{} = activity} <- {:activity, Activity.get_by_id(activity_id)} do
+ remove_mute(user, activity)
+ else
+ {what, result} = error ->
+ Logger.warn(
+ "CommonAPI.remove_mute/2 failed. #{what}: #{result}, user_id: #{user_id}, activity_id: #{
+ activity_id
+ }"
+ )
+
+ {:error, error}
+ end
+ end
+
def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
- when is_binary("context") do
+ when is_binary(context) do
ThreadMute.exists?(user_id, context)
end
@@ -563,4 +563,21 @@ defmodule Pleroma.Web.CommonAPI do
def show_reblogs(%User{} = user, %User{} = target) do
UserRelationship.delete_reblog_mute(user, target)
end
+
+ def get_user(ap_id, fake_record_fallback \\ true) do
+ cond do
+ user = User.get_cached_by_ap_id(ap_id) ->
+ user
+
+ user = User.get_by_guessed_nickname(ap_id) ->
+ user
+
+ fake_record_fallback ->
+ # TODO: refactor (fake records is never a good idea)
+ User.error_user(ap_id)
+
+ true ->
+ nil
+ end
+ end
end
diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex
index f849b2e01..aa2616d9e 100644
--- a/lib/pleroma/web/common_api/activity_draft.ex
+++ b/lib/pleroma/web/common_api/activity_draft.ex
@@ -22,7 +22,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
in_reply_to_conversation: nil,
visibility: nil,
expires_at: nil,
- poll: nil,
+ extra: nil,
emoji: %{},
content_html: nil,
mentions: [],
@@ -35,9 +35,14 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
preview?: false,
changes: %{}
- def create(user, params) do
+ def new(user, params) do
%__MODULE__{user: user}
|> put_params(params)
+ end
+
+ def create(user, params) do
+ user
+ |> new(params)
|> status()
|> summary()
|> with_valid(&attachments/1)
@@ -57,6 +62,30 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|> validate()
end
+ def listen(user, params) do
+ user
+ |> new(params)
+ |> visibility()
+ |> to_and_cc()
+ |> context()
+ |> listen_object()
+ |> with_valid(&changes/1)
+ |> validate()
+ end
+
+ defp listen_object(draft) do
+ object =
+ draft.params
+ |> Map.take([:album, :artist, :title, :length])
+ |> Map.new(fn {key, value} -> {to_string(key), value} end)
+ |> Map.put("type", "Audio")
+ |> Map.put("to", draft.to)
+ |> Map.put("cc", draft.cc)
+ |> Map.put("actor", draft.user.ap_id)
+
+ %__MODULE__{draft | object: object}
+ end
+
defp put_params(draft, params) do
params = Map.put_new(params, :in_reply_to_status_id, params[:in_reply_to_id])
%__MODULE__{draft | params: params}
@@ -121,7 +150,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
defp poll(draft) do
case Utils.make_poll_data(draft.params) do
{:ok, {poll, poll_emoji}} ->
- %__MODULE__{draft | poll: poll, emoji: Map.merge(draft.emoji, poll_emoji)}
+ %__MODULE__{draft | extra: poll, emoji: Map.merge(draft.emoji, poll_emoji)}
{:error, message} ->
add_error(draft, message)
@@ -129,32 +158,18 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
end
defp content(draft) do
- {content_html, mentions, tags} =
- Utils.make_content_html(
- draft.status,
- draft.attachments,
- draft.params,
- draft.visibility
- )
-
- %__MODULE__{draft | content_html: content_html, mentions: mentions, tags: tags}
- end
+ {content_html, mentioned_users, tags} = Utils.make_content_html(draft)
- defp to_and_cc(draft) do
- addressed_users =
- draft.mentions
+ mentions =
+ mentioned_users
|> Enum.map(fn {_, mentioned_user} -> mentioned_user.ap_id end)
|> Utils.get_addressed_users(draft.params[:to])
- {to, cc} =
- Utils.get_to_and_cc(
- draft.user,
- addressed_users,
- draft.in_reply_to,
- draft.visibility,
- draft.in_reply_to_conversation
- )
+ %__MODULE__{draft | content_html: content_html, mentions: mentions, tags: tags}
+ end
+ defp to_and_cc(draft) do
+ {to, cc} = Utils.get_to_and_cc(draft)
%__MODULE__{draft | to: to, cc: cc}
end
@@ -172,19 +187,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
emoji = Map.merge(Pleroma.Emoji.Formatter.get_emoji_map(draft.full_payload), draft.emoji)
object =
- Utils.make_note_data(
- draft.user.ap_id,
- draft.to,
- draft.context,
- draft.content_html,
- draft.attachments,
- draft.in_reply_to,
- draft.tags,
- draft.summary,
- draft.cc,
- draft.sensitive,
- draft.poll
- )
+ Utils.make_note_data(draft)
|> Map.put("emoji", emoji)
|> Map.put("source", draft.status)
@@ -202,7 +205,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
additional =
case draft.expires_at do
- %NaiveDateTime{} = expires_at -> Map.put(additional, "expires_at", expires_at)
+ %DateTime{} = expires_at -> Map.put(additional, "expires_at", expires_at)
_ -> additional
end
diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index 9c38b73eb..1c74ea787 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -12,12 +12,13 @@ defmodule Pleroma.Web.CommonAPI.Utils do
alias Pleroma.Conversation.Participation
alias Pleroma.Formatter
alias Pleroma.Object
- alias Pleroma.Plugs.AuthenticationPlug
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
+ alias Pleroma.Web.CommonAPI.ActivityDraft
alias Pleroma.Web.MediaProxy
+ alias Pleroma.Web.Plugs.AuthenticationPlug
require Logger
require Pleroma.Constants
@@ -50,67 +51,62 @@ defmodule Pleroma.Web.CommonAPI.Utils do
{_, descs} = Jason.decode(descs_str)
Enum.map(ids, fn media_id ->
- case Repo.get(Object, media_id) do
- %Object{data: data} ->
- Map.put(data, "name", descs[media_id])
-
- _ ->
- nil
+ with %Object{data: data} <- Repo.get(Object, media_id) do
+ Map.put(data, "name", descs[media_id])
end
end)
|> Enum.reject(&is_nil/1)
end
- @spec get_to_and_cc(
- User.t(),
- list(String.t()),
- Activity.t() | nil,
- String.t(),
- Participation.t() | nil
- ) :: {list(String.t()), list(String.t())}
+ @spec get_to_and_cc(ActivityDraft.t()) :: {list(String.t()), list(String.t())}
- def get_to_and_cc(_, _, _, _, %Participation{} = participation) do
+ def get_to_and_cc(%{in_reply_to_conversation: %Participation{} = participation}) do
participation = Repo.preload(participation, :recipients)
{Enum.map(participation.recipients, & &1.ap_id), []}
end
- def get_to_and_cc(user, mentioned_users, inReplyTo, "public", _) do
- to = [Pleroma.Constants.as_public() | mentioned_users]
- cc = [user.follower_address]
+ def get_to_and_cc(%{visibility: visibility} = draft) when visibility in ["public", "local"] do
+ to =
+ case visibility do
+ "public" -> [Pleroma.Constants.as_public() | draft.mentions]
+ "local" -> [Pleroma.Constants.as_local_public() | draft.mentions]
+ end
+
+ cc = [draft.user.follower_address]
- if inReplyTo do
- {Enum.uniq([inReplyTo.data["actor"] | to]), cc}
+ if draft.in_reply_to do
+ {Enum.uniq([draft.in_reply_to.data["actor"] | to]), cc}
else
{to, cc}
end
end
- def get_to_and_cc(user, mentioned_users, inReplyTo, "unlisted", _) do
- to = [user.follower_address | mentioned_users]
+ def get_to_and_cc(%{visibility: "unlisted"} = draft) do
+ to = [draft.user.follower_address | draft.mentions]
cc = [Pleroma.Constants.as_public()]
- if inReplyTo do
- {Enum.uniq([inReplyTo.data["actor"] | to]), cc}
+ if draft.in_reply_to do
+ {Enum.uniq([draft.in_reply_to.data["actor"] | to]), cc}
else
{to, cc}
end
end
- def get_to_and_cc(user, mentioned_users, inReplyTo, "private", _) do
- {to, cc} = get_to_and_cc(user, mentioned_users, inReplyTo, "direct", nil)
- {[user.follower_address | to], cc}
+ def get_to_and_cc(%{visibility: "private"} = draft) do
+ {to, cc} = get_to_and_cc(struct(draft, visibility: "direct"))
+ {[draft.user.follower_address | to], cc}
end
- def get_to_and_cc(_user, mentioned_users, inReplyTo, "direct", _) do
+ def get_to_and_cc(%{visibility: "direct"} = draft) 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]), []}
+ if draft.in_reply_to && Visibility.is_direct?(draft.in_reply_to) do
+ {Enum.uniq([draft.in_reply_to.data["actor"] | draft.mentions]), []}
else
- {mentioned_users, []}
+ {draft.mentions, []}
end
end
- def get_to_and_cc(_user, mentions, _inReplyTo, {:list, _}, _), do: {mentions, []}
+ def get_to_and_cc(%{visibility: {:list, _}, mentions: mentions}), do: {mentions, []}
def get_addressed_users(_, to) when is_list(to) do
User.get_ap_ids_by_nicknames(to)
@@ -203,30 +199,25 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
end
- def make_content_html(
- status,
- attachments,
- data,
- visibility
- ) do
+ def make_content_html(%ActivityDraft{} = draft) do
attachment_links =
- data
+ draft.params
|> Map.get("attachment_links", Config.get([:instance, :attachment_links]))
|> truthy_param?()
- content_type = get_content_type(data[:content_type])
+ content_type = get_content_type(draft.params[:content_type])
options =
- if visibility == "direct" && Config.get([:instance, :safe_dm_mentions]) do
+ if draft.visibility == "direct" && Config.get([:instance, :safe_dm_mentions]) do
[safe_mention: true]
else
[]
end
- status
+ draft.status
|> format_input(content_type, options)
- |> maybe_add_attachments(attachments, attachment_links)
- |> maybe_add_nsfw_tag(data)
+ |> maybe_add_attachments(draft.attachments, attachment_links)
+ |> maybe_add_nsfw_tag(draft.params)
end
defp get_content_type(content_type) do
@@ -274,7 +265,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
def format_input(text, format, options \\ [])
@doc """
- Formatting text to plain text.
+ Formatting text to plain text, BBCode, HTML, or Markdown
"""
def format_input(text, "text/plain", options) do
text
@@ -285,9 +276,6 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end).()
end
- @doc """
- Formatting text as BBCode.
- """
def format_input(text, "text/bbcode", options) do
text
|> String.replace(~r/\r/, "")
@@ -297,18 +285,12 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|> Formatter.linkify(options)
end
- @doc """
- Formatting text to html.
- """
def format_input(text, "text/html", options) do
text
|> Formatter.html_escape("text/html")
|> Formatter.linkify(options)
end
- @doc """
- Formatting text to markdown.
- """
def format_input(text, "text/markdown", options) do
text
|> Formatter.mentions_escape(options)
@@ -317,33 +299,21 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|> Formatter.html_escape("text/html")
end
- def make_note_data(
- actor,
- to,
- context,
- content_html,
- attachments,
- in_reply_to,
- tags,
- summary \\ nil,
- cc \\ [],
- sensitive \\ false,
- extra_params \\ %{}
- ) do
+ def make_note_data(%ActivityDraft{} = draft) do
%{
"type" => "Note",
- "to" => to,
- "cc" => cc,
- "content" => content_html,
- "summary" => summary,
- "sensitive" => truthy_param?(sensitive),
- "context" => context,
- "attachment" => attachments,
- "actor" => actor,
- "tag" => Keyword.values(tags) |> Enum.uniq()
+ "to" => draft.to,
+ "cc" => draft.cc,
+ "content" => draft.content_html,
+ "summary" => draft.summary,
+ "sensitive" => draft.sensitive,
+ "context" => draft.context,
+ "attachment" => draft.attachments,
+ "actor" => draft.user.ap_id,
+ "tag" => Keyword.values(draft.tags) |> Enum.uniq()
}
- |> add_in_reply_to(in_reply_to)
- |> Map.merge(extra_params)
+ |> add_in_reply_to(draft.in_reply_to)
+ |> Map.merge(draft.extra)
end
defp add_in_reply_to(object, nil), do: object
@@ -548,17 +518,6 @@ 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
diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex
index 69946fb81..69188a882 100644
--- a/lib/pleroma/web/controller_helper.ex
+++ b/lib/pleroma/web/controller_helper.ex
@@ -18,6 +18,12 @@ defmodule Pleroma.Web.ControllerHelper do
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
|> put_status(status)
@@ -42,13 +48,13 @@ defmodule Pleroma.Web.ControllerHelper do
defp param_to_integer(_, default), do: default
- def add_link_headers(conn, activities, extra_params \\ %{})
+ def add_link_headers(conn, entries, extra_params \\ %{})
- def add_link_headers(%{assigns: %{skip_link_headers: true}} = conn, _activities, _extra_params),
+ def add_link_headers(%{assigns: %{skip_link_headers: true}} = conn, _entries, _extra_params),
do: conn
- def add_link_headers(conn, activities, extra_params) do
- case get_pagination_fields(conn, activities, extra_params) do
+ def add_link_headers(conn, entries, extra_params) do
+ case get_pagination_fields(conn, entries, extra_params) do
%{"next" => next_url, "prev" => prev_url} ->
put_resp_header(conn, "link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
@@ -72,19 +78,15 @@ defmodule Pleroma.Web.ControllerHelper do
}
end
- def get_pagination_fields(conn, activities, extra_params \\ %{}) do
- case List.last(activities) do
+ def get_pagination_fields(conn, entries, extra_params \\ %{}) do
+ case List.last(entries) do
%{pagination_id: max_id} when not is_nil(max_id) ->
- %{pagination_id: min_id} =
- activities
- |> List.first()
+ %{pagination_id: min_id} = List.first(entries)
build_pagination_fields(conn, min_id, max_id, extra_params)
%{id: max_id} ->
- %{id: min_id} =
- activities
- |> List.first()
+ %{id: min_id} = List.first(entries)
build_pagination_fields(conn, min_id, max_id, extra_params)
diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex
index 226d42c2c..f26542e88 100644
--- a/lib/pleroma/web/endpoint.ex
+++ b/lib/pleroma/web/endpoint.ex
@@ -7,19 +7,23 @@ defmodule Pleroma.Web.Endpoint do
require Pleroma.Constants
+ alias Pleroma.Config
+
socket("/socket", Pleroma.Web.UserSocket)
- plug(Pleroma.Plugs.SetLocalePlug)
+ plug(Plug.Telemetry, event_prefix: [:phoenix, :endpoint])
+
+ plug(Pleroma.Web.Plugs.SetLocalePlug)
plug(CORSPlug)
- plug(Pleroma.Plugs.HTTPSecurityPlug)
- plug(Pleroma.Plugs.UploadedMedia)
+ plug(Pleroma.Web.Plugs.HTTPSecurityPlug)
+ plug(Pleroma.Web.Plugs.UploadedMedia)
@static_cache_control "public, no-cache"
# InstanceStatic needs to be before Plug.Static to be able to override shipped-static files
# If you're adding new paths to `only:` you'll need to configure them in InstanceStatic as well
# Cache-control headers are duplicated in case we turn off etags in the future
- plug(Pleroma.Plugs.InstanceStatic,
+ plug(Pleroma.Web.Plugs.InstanceStatic,
at: "/",
gzip: true,
cache_control_for_etags: @static_cache_control,
@@ -28,6 +32,29 @@ defmodule Pleroma.Web.Endpoint do
}
)
+ # Careful! No `only` restriction here, as we don't know what frontends contain.
+ plug(Pleroma.Web.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.Web.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 +72,6 @@ defmodule Pleroma.Web.Endpoint do
}
)
- plug(Plug.Static.IndexHtml, at: "/pleroma/admin/")
-
plug(Plug.Static,
at: "/pleroma/admin/",
from: {:pleroma, "priv/static/adminfe/"}
@@ -58,26 +83,26 @@ defmodule Pleroma.Web.Endpoint do
plug(Phoenix.CodeReloader)
end
- plug(Pleroma.Plugs.TrailingFormatPlug)
+ plug(Pleroma.Web.Plugs.TrailingFormatPlug)
plug(Plug.RequestId)
plug(Plug.Logger, log: :debug)
plug(Plug.Parsers,
parsers: [
:urlencoded,
- {:multipart, length: {Pleroma.Config, :get, [[:instance, :upload_limit]]}},
+ {:multipart, length: {Config, :get, [[:instance, :upload_limit]]}},
:json
],
pass: ["*/*"],
json_decoder: Jason,
- length: Pleroma.Config.get([:instance, :upload_limit]),
+ length: Config.get([:instance, :upload_limit]),
body_reader: {Pleroma.Web.Plugs.DigestPlug, :read_body, []}
)
plug(Plug.MethodOverride)
plug(Plug.Head)
- secure_cookies = Pleroma.Config.get([__MODULE__, :secure_cookie_flag])
+ secure_cookies = Config.get([__MODULE__, :secure_cookie_flag])
cookie_name =
if secure_cookies,
@@ -85,7 +110,7 @@ defmodule Pleroma.Web.Endpoint do
else: "pleroma_key"
extra =
- Pleroma.Config.get([__MODULE__, :extra_cookie_attrs])
+ Config.get([__MODULE__, :extra_cookie_attrs])
|> Enum.join(";")
# The session will be stored in the cookie and signed,
@@ -95,13 +120,13 @@ defmodule Pleroma.Web.Endpoint do
Plug.Session,
store: :cookie,
key: cookie_name,
- signing_salt: Pleroma.Config.get([__MODULE__, :signing_salt], "CqaoopA2"),
+ signing_salt: Config.get([__MODULE__, :signing_salt], "CqaoopA2"),
http_only: true,
secure: secure_cookies,
extra: extra
)
- plug(Pleroma.Plugs.RemoteIp)
+ plug(Pleroma.Web.Plugs.RemoteIp)
defmodule Instrumenter do
use Prometheus.PhoenixInstrumenter
@@ -115,8 +140,34 @@ defmodule Pleroma.Web.Endpoint do
use Prometheus.PlugExporter
end
+ defmodule MetricsExporterCaller do
+ @behaviour Plug
+
+ def init(opts), do: opts
+
+ def call(conn, opts) do
+ prometheus_config = Application.get_env(:prometheus, MetricsExporter, [])
+ ip_whitelist = List.wrap(prometheus_config[:ip_whitelist])
+
+ cond do
+ !prometheus_config[:enabled] ->
+ conn
+
+ ip_whitelist != [] and
+ !Enum.find(ip_whitelist, fn ip ->
+ Pleroma.Helpers.InetHelper.parse_address(ip) == {:ok, conn.remote_ip}
+ end) ->
+ conn
+
+ true ->
+ MetricsExporter.call(conn, opts)
+ end
+ end
+ end
+
plug(PipelineInstrumenter)
- plug(MetricsExporter)
+
+ plug(MetricsExporterCaller)
plug(Pleroma.Web.Router)
diff --git a/lib/pleroma/web/fallback_redirect_controller.ex b/lib/pleroma/web/fallback/redirect_controller.ex
index 431ad5485..1ac1319f8 100644
--- a/lib/pleroma/web/fallback_redirect_controller.ex
+++ b/lib/pleroma/web/fallback/redirect_controller.ex
@@ -2,7 +2,7 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Fallback.RedirectController do
+defmodule Pleroma.Web.Fallback.RedirectController do
use Pleroma.Web, :controller
require Logger
@@ -37,10 +37,11 @@ defmodule Fallback.RedirectController do
tags = build_tags(conn, params)
preloads = preload_data(conn, params)
+ title = "<title>#{Pleroma.Config.get([:instance, :name])}</title>"
response =
index_content
- |> String.replace("<!--server-generated-meta-->", tags <> preloads)
+ |> String.replace("<!--server-generated-meta-->", tags <> preloads <> title)
conn
|> put_resp_content_type("text/html")
@@ -54,10 +55,11 @@ defmodule Fallback.RedirectController do
def redirector_with_preload(conn, params) do
{:ok, index_content} = File.read(index_file_path())
preloads = preload_data(conn, params)
+ title = "<title>#{Pleroma.Config.get([:instance, :name])}</title>"
response =
index_content
- |> String.replace("<!--server-generated-meta-->", preloads)
+ |> String.replace("<!--server-generated-meta-->", preloads <> title)
conn
|> put_resp_content_type("text/html")
@@ -75,7 +77,7 @@ defmodule Fallback.RedirectController do
end
defp index_file_path do
- Pleroma.Plugs.InstanceStatic.file_path("index.html")
+ Pleroma.Web.Plugs.InstanceStatic.file_path("index.html")
end
defp build_tags(conn, params) do
diff --git a/lib/pleroma/web/federator/federator.ex b/lib/pleroma/web/federator.ex
index f5803578d..130654145 100644
--- a/lib/pleroma/web/federator/federator.ex
+++ b/lib/pleroma/web/federator.ex
@@ -66,14 +66,17 @@ defmodule Pleroma.Web.Federator do
def perform(:incoming_ap_doc, params) do
Logger.debug("Handling incoming AP activity")
- params = Utils.normalize_params(params)
+ actor =
+ params
+ |> Map.get("actor")
+ |> Utils.get_ap_id()
# NOTE: we use the actor ID to do the containment, this is fine because an
# actor shouldn't be acting on objects outside their own AP server.
- with {:ok, _user} <- ap_enabled_actor(params["actor"]),
+ with {_, {:ok, _user}} <- {:actor, ap_enabled_actor(actor)},
nil <- Activity.normalize(params["id"]),
{_, :ok} <-
- {:correct_origin?, Containment.contain_origin_from_id(params["actor"], params)},
+ {:correct_origin?, Containment.contain_origin_from_id(actor, params)},
{:ok, activity} <- Transmogrifier.handle_incoming(params) do
{:ok, activity}
else
@@ -85,10 +88,13 @@ defmodule Pleroma.Web.Federator do
Logger.debug("Already had #{params["id"]}")
{:error, :already_present}
+ {:actor, e} ->
+ Logger.debug("Unhandled actor #{actor}, #{inspect(e)}")
+ {:error, e}
+
e ->
# Just drop those for now
- Logger.debug("Unhandled activity")
- Logger.debug(Jason.encode!(params, pretty: true))
+ Logger.debug(fn -> "Unhandled activity\n" <> Jason.encode!(params, pretty: true) end)
{:error, e}
end
end
diff --git a/lib/pleroma/web/feed/feed_view.ex b/lib/pleroma/web/feed/feed_view.ex
index 1ae03e7e2..56c024617 100644
--- a/lib/pleroma/web/feed/feed_view.ex
+++ b/lib/pleroma/web/feed/feed_view.ex
@@ -83,7 +83,7 @@ defmodule Pleroma.Web.Feed.FeedView do
def activity_content(_), do: ""
- def activity_context(activity), do: activity.data["context"]
+ def activity_context(activity), do: escape(activity.data["context"])
def attachment_href(attachment) do
attachment["url"]
diff --git a/lib/pleroma/web/feed/tag_controller.ex b/lib/pleroma/web/feed/tag_controller.ex
index 39b2a766a..218cdbdf3 100644
--- a/lib/pleroma/web/feed/tag_controller.ex
+++ b/lib/pleroma/web/feed/tag_controller.ex
@@ -9,7 +9,15 @@ defmodule Pleroma.Web.Feed.TagController do
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.Feed.FeedView
- def feed(conn, %{"tag" => raw_tag} = params) do
+ def feed(conn, params) do
+ if Config.get!([:instance, :public]) do
+ render_feed(conn, params)
+ else
+ render_error(conn, :not_found, "Not found")
+ end
+ end
+
+ defp render_feed(conn, %{"tag" => raw_tag} = params) do
{format, tag} = parse_tag(raw_tag)
activities =
@@ -28,12 +36,13 @@ defmodule Pleroma.Web.Feed.TagController do
end
@spec parse_tag(binary() | any()) :: {format :: String.t(), tag :: String.t()}
- defp parse_tag(raw_tag) when is_binary(raw_tag) do
- case Enum.reverse(String.split(raw_tag, ".")) do
- [format | tag] when format in ["atom", "rss"] -> {format, Enum.join(tag, ".")}
- _ -> {"rss", raw_tag}
+ defp parse_tag(raw_tag) do
+ case is_binary(raw_tag) && Enum.reverse(String.split(raw_tag, ".")) do
+ [format | tag] when format in ["rss", "atom"] ->
+ {format, Enum.join(tag, ".")}
+
+ _ ->
+ {"atom", raw_tag}
end
end
-
- defp parse_tag(raw_tag), do: {"rss", raw_tag}
end
diff --git a/lib/pleroma/web/feed/user_controller.ex b/lib/pleroma/web/feed/user_controller.ex
index d56f43818..a5013d2c0 100644
--- a/lib/pleroma/web/feed/user_controller.ex
+++ b/lib/pleroma/web/feed/user_controller.ex
@@ -5,30 +5,25 @@
defmodule Pleroma.Web.Feed.UserController do
use Pleroma.Web, :controller
- alias Fallback.RedirectController
+ alias Pleroma.Config
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.ActivityPubController
alias Pleroma.Web.Feed.FeedView
- plug(Pleroma.Plugs.SetFormatPlug when action in [:feed_redirect])
+ plug(Pleroma.Web.Plugs.SetFormatPlug when action in [:feed_redirect])
action_fallback(:errors)
def feed_redirect(%{assigns: %{format: "html"}} = conn, %{"nickname" => nickname}) do
with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname_or_id(nickname)} do
- RedirectController.redirector_with_meta(conn, %{user: user})
+ Pleroma.Web.Fallback.RedirectController.redirector_with_meta(conn, %{user: user})
end
end
def feed_redirect(%{assigns: %{format: format}} = conn, _params)
when format in ["json", "activity+json"] do
- with %{halted: false} = conn <-
- Pleroma.Plugs.EnsureAuthenticatedPlug.call(conn,
- unless_func: &Pleroma.Web.FederatingPlug.federating?/1
- ) do
- ActivityPubController.call(conn, :user)
- end
+ ActivityPubController.call(conn, :user)
end
def feed_redirect(conn, %{"nickname" => nickname}) do
@@ -41,13 +36,14 @@ defmodule Pleroma.Web.Feed.UserController do
format = get_format(conn)
format =
- if format in ["rss", "atom"] do
+ if format in ["atom", "rss"] do
format
else
"atom"
end
- with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do
+ with {_, %User{local: true} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)},
+ {_, :visible} <- {:visibility, User.visible_for(user, _reading_user = nil)} do
activities =
%{
type: ["Create"],
@@ -62,7 +58,7 @@ defmodule Pleroma.Web.Feed.UserController do
|> render("user.#{format}",
user: user,
activities: activities,
- feed_config: Pleroma.Config.get([:feed])
+ feed_config: Config.get([:feed])
)
end
end
@@ -71,8 +67,11 @@ 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, {:visibility, _}), do: errors(conn, {:error, :not_found})
+
def errors(conn, _) do
render_error(conn, :internal_server_error, "Something went wrong")
end
diff --git a/lib/pleroma/web/instance_document.ex b/lib/pleroma/web/instance_document.ex
new file mode 100644
index 000000000..df5caebf0
--- /dev/null
+++ b/lib/pleroma/web/instance_document.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.InstanceDocument do
+ alias Pleroma.Config
+ alias Pleroma.Web.Endpoint
+
+ @instance_documents %{
+ "terms-of-service" => "/static/terms-of-service.html",
+ "instance-panel" => "/instance/panel.html"
+ }
+
+ @spec get(String.t()) :: {:ok, String.t()} | {:error, atom()}
+ def get(document_name) do
+ case Map.fetch(@instance_documents, document_name) do
+ {:ok, path} -> {:ok, path}
+ _ -> {:error, :not_found}
+ end
+ end
+
+ @spec put(String.t(), String.t()) :: {:ok, String.t()} | {:error, atom()}
+ def put(document_name, origin_path) do
+ with {_, {:ok, destination_path}} <-
+ {:instance_document, Map.fetch(@instance_documents, document_name)},
+ :ok <- put_file(origin_path, destination_path) do
+ {:ok, Path.join(Endpoint.url(), destination_path)}
+ else
+ {:instance_document, :error} -> {:error, :not_found}
+ error -> error
+ end
+ end
+
+ @spec delete(String.t()) :: :ok | {:error, atom()}
+ def delete(document_name) do
+ with {_, {:ok, path}} <- {:instance_document, Map.fetch(@instance_documents, document_name)},
+ instance_static_dir_path <- instance_static_dir(path),
+ :ok <- File.rm(instance_static_dir_path) do
+ :ok
+ else
+ {:instance_document, :error} -> {:error, :not_found}
+ {:error, :enoent} -> {:error, :not_found}
+ error -> error
+ end
+ end
+
+ defp put_file(origin_path, destination_path) do
+ with destination <- instance_static_dir(destination_path),
+ {_, :ok} <- {:mkdir_p, File.mkdir_p(Path.dirname(destination))},
+ {_, {:ok, _}} <- {:copy, File.copy(origin_path, destination)} do
+ :ok
+ else
+ {error, _} -> {:error, error}
+ end
+ end
+
+ defp instance_static_dir(filename) do
+ [:instance, :static_dir]
+ |> Config.get!()
+ |> Path.join(filename)
+ end
+end
diff --git a/lib/pleroma/web/mailer/subscription_controller.ex b/lib/pleroma/web/mailer/subscription_controller.ex
index 478a83518..ace44afd1 100644
--- a/lib/pleroma/web/mailer/subscription_controller.ex
+++ b/lib/pleroma/web/mailer/subscription_controller.ex
@@ -1,3 +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.Mailer.SubscriptionController do
use Pleroma.Web, :controller
diff --git a/lib/pleroma/web/masto_fe_controller.ex b/lib/pleroma/web/masto_fe_controller.ex
index 43ec70021..08f92d55f 100644
--- a/lib/pleroma/web/masto_fe_controller.ex
+++ b/lib/pleroma/web/masto_fe_controller.ex
@@ -5,9 +5,9 @@
defmodule Pleroma.Web.MastoFEController do
use Pleroma.Web, :controller
- alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
- alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
+ alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :put_settings)
diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
index fe5d022f5..7011b7eb1 100644
--- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
@@ -15,9 +15,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
]
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
@@ -27,8 +24,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
alias Pleroma.Web.MastodonAPI.MastodonAPI
alias Pleroma.Web.MastodonAPI.MastodonAPIController
alias Pleroma.Web.MastodonAPI.StatusView
+ alias Pleroma.Web.OAuth.OAuthController
alias Pleroma.Web.OAuth.OAuthView
- alias Pleroma.Web.OAuth.Token
+ alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+ alias Pleroma.Web.Plugs.RateLimiter
alias Pleroma.Web.TwitterAPI.TwitterAPI
plug(Pleroma.Web.ApiSpec.CastAndValidate)
@@ -100,11 +100,34 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
with :ok <- validate_email_param(params),
:ok <- TwitterAPI.validate_captcha(app, params),
- {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
- {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
+ {: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, error} -> json_response(conn, :bad_request, %{error: error})
+ {: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
@@ -154,7 +177,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
user_params =
[
:no_rich_text,
- :locked,
:hide_followers_count,
:hide_follows_count,
:hide_followers,
@@ -163,7 +185,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
:show_role,
:skip_thread_containment,
:allow_following_move,
- :discoverable,
:accepts_chat_messages
]
|> Enum.reduce(%{}, fn key, acc ->
@@ -187,6 +208,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
if bot, do: {:ok, "Service"}, else: {:ok, "Person"}
end)
|> Maps.put_if_present(:actor_type, params[:actor_type])
+ # Note: param name is indeed :locked (not an error)
+ |> Maps.put_if_present(:is_locked, params[:locked])
+ # Note: param name is indeed :discoverable (not an error)
+ |> Maps.put_if_present(:is_discoverable, params[:discoverable])
# What happens here:
#
@@ -203,7 +228,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
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: user)
+ Pleroma.Web.ActivityPub.UserView.render("user.json", user: unpersisted_user)
|> Map.delete("@context"),
{:ok, update_data, []} <- Builder.update(user, updated_object),
{:ok, _update, _} <-
@@ -269,7 +294,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|> render("index.json",
activities: activities,
for: reading_user,
- as: :activity
+ as: :activity,
+ with_muted: Map.get(params, :with_muted, false)
)
else
error -> user_visibility_error(conn, error)
@@ -371,7 +397,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
@doc "POST /api/v1/accounts/:id/mute"
def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
- with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
+ with {:ok, _user_relationships} <- User.mute(muter, muted, params) do
render(conn, "relationship.json", user: muter, target: muted)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
@@ -419,15 +445,27 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
@doc "GET /api/v1/mutes"
- def mutes(%{assigns: %{user: user}} = conn, _) do
- users = User.muted_users(user, _restrict_deactivated = true)
- render(conn, "index.json", users: users, for: user, as: :user)
+ def mutes(%{assigns: %{user: user}} = conn, params) do
+ users =
+ user
+ |> User.muted_users_relation(_restrict_deactivated = true)
+ |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true))
+
+ conn
+ |> add_link_headers(users)
+ |> render("index.json", users: users, for: user, as: :user)
end
@doc "GET /api/v1/blocks"
- def blocks(%{assigns: %{user: user}} = conn, _) do
- users = User.blocked_users(user, _restrict_deactivated = true)
- render(conn, "index.json", users: users, for: user, as: :user)
+ def blocks(%{assigns: %{user: user}} = conn, params) do
+ users =
+ user
+ |> User.blocked_users_relation(_restrict_deactivated = true)
+ |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true))
+
+ conn
+ |> add_link_headers(users)
+ |> render("index.json", users: users, for: user, as: :user)
end
@doc "GET /api/v1/endorsements"
diff --git a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex
index a516b6c20..143dcf80c 100644
--- a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex
@@ -5,12 +5,12 @@
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
alias Pleroma.Web.OAuth.Scopes
alias Pleroma.Web.OAuth.Token
+ alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
diff --git a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
index 753b3db3e..9cc3984d0 100644
--- a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
@@ -5,6 +5,8 @@
defmodule Pleroma.Web.MastodonAPI.AuthController do
use Pleroma.Web, :controller
+ import Pleroma.Web.ControllerHelper, only: [json_response: 3]
+
alias Pleroma.User
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Authorization
@@ -13,7 +15,7 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
- plug(Pleroma.Plugs.RateLimiter, [name: :password_reset] when action == :password_reset)
+ plug(Pleroma.Web.Plugs.RateLimiter, [name: :password_reset] when action == :password_reset)
@local_mastodon_name "Mastodon-Local"
@@ -22,7 +24,7 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
redirect(conn, to: local_mastodon_root_path(conn))
end
- @doc "Local Mastodon FE login init action"
+ # Local Mastodon FE login init action
def login(conn, %{"code" => auth_token}) do
with {:ok, app} <- get_or_make_app(),
{:ok, auth} <- Authorization.get_by_token(app, auth_token),
@@ -33,7 +35,7 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
end
end
- @doc "Local Mastodon FE callback action"
+ # Local Mastodon FE callback action
def login(conn, _) do
with {:ok, app} <- get_or_make_app() do
path =
@@ -59,17 +61,9 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
def password_reset(conn, params) do
nickname_or_email = params["email"] || params["nickname"]
- with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
- conn
- |> put_status(:no_content)
- |> json("")
- else
- {:error, "unknown user"} ->
- send_resp(conn, :not_found, "")
-
- {:error, _} ->
- send_resp(conn, :bad_request, "")
- end
+ TwitterAPI.password_reset(nickname_or_email)
+
+ json_response(conn, :no_content, "")
end
defp local_mastodon_root_path(conn) do
diff --git a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex
index f35ec3596..61347d8db 100644
--- a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex
@@ -8,8 +8,8 @@ defmodule Pleroma.Web.MastodonAPI.ConversationController do
import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
alias Pleroma.Conversation.Participation
- alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Repo
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
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 c5f47c5df..872cb1f4d 100644
--- a/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex
@@ -9,7 +9,7 @@ defmodule Pleroma.Web.MastodonAPI.CustomEmojiController do
plug(
:skip_plug,
- [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]
+ [Pleroma.Web.Plugs.OAuthScopesPlug, Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug]
when action == :index
)
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 9c2d093cd..503bd7d5f 100644
--- a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
@@ -5,8 +5,8 @@
defmodule Pleroma.Web.MastodonAPI.DomainBlockController do
use Pleroma.Web, :controller
- alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
plug(Pleroma.Web.ApiSpec.CastAndValidate)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.DomainBlockOperation
diff --git a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
index abbf0ce02..c71a34b15 100644
--- a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
@@ -6,7 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.FilterController do
use Pleroma.Web, :controller
alias Pleroma.Filter
- alias Pleroma.Plugs.OAuthScopesPlug
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
@oauth_read_actions [:show, :index]
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 748b6b475..f8cd7fa9f 100644
--- a/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex
@@ -5,9 +5,9 @@
defmodule Pleroma.Web.MastodonAPI.FollowRequestController do
use Pleroma.Web, :controller
- alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
plug(:put_view, Pleroma.Web.MastodonAPI.AccountView)
plug(Pleroma.Web.ApiSpec.CastAndValidate)
diff --git a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex
index d8859731d..07a32491a 100644
--- a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex
@@ -9,7 +9,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceController do
plug(
:skip_plug,
- [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]
+ [Pleroma.Web.Plugs.OAuthScopesPlug, Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug]
when action in [:show, :peers]
)
diff --git a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex
index acdc76fd2..f6b51bf02 100644
--- a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex
@@ -5,9 +5,9 @@
defmodule Pleroma.Web.MastodonAPI.ListController do
use Pleroma.Web, :controller
- alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
alias Pleroma.Web.MastodonAPI.AccountView
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
@oauth_read_actions [:index, :show, :list_accounts]
@@ -74,7 +74,7 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
# DELETE /api/v1/lists/:id/accounts
def remove_from_list(
- %{assigns: %{list: list}, body_params: %{account_ids: account_ids}} = conn,
+ %{assigns: %{list: list}, params: %{account_ids: account_ids}} = conn,
_
) do
Enum.each(account_ids, fn account_id ->
@@ -86,6 +86,10 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
json(conn, %{})
end
+ def remove_from_list(%{body_params: params} = conn, _) do
+ remove_from_list(%{conn | params: params}, %{})
+ end
+
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)
diff --git a/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex b/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex
index 85310edfa..0628b2b49 100644
--- a/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex
@@ -4,7 +4,7 @@
defmodule Pleroma.Web.MastodonAPI.MarkerController do
use Pleroma.Web, :controller
- alias Pleroma.Plugs.OAuthScopesPlug
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
plug(Pleroma.Web.ApiSpec.CastAndValidate)
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 e7767de4e..9cf682c7b 100644
--- a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
@@ -17,7 +17,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
plug(
:skip_plug,
- [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]
+ [Pleroma.Web.Plugs.OAuthScopesPlug, Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug]
when action in [:empty_array, :empty_object]
)
diff --git a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex
index 513de279f..161193134 100644
--- a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex
@@ -6,11 +6,12 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do
use Pleroma.Web, :controller
alias Pleroma.Object
- alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+ plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:create, :create2])
plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(:put_view, Pleroma.Web.MastodonAPI.StatusView)
diff --git a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
index e25cef30b..c3c8606f2 100644
--- a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
@@ -8,8 +8,8 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
alias Pleroma.Notification
- alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Web.MastodonAPI.MastodonAPI
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
@oauth_read_actions [:show, :index]
diff --git a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex
index db46ffcfc..3dcd1c44f 100644
--- a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex
@@ -9,9 +9,9 @@ defmodule Pleroma.Web.MastodonAPI.PollController do
alias Pleroma.Activity
alias Pleroma.Object
- alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
diff --git a/lib/pleroma/web/mastodon_api/controllers/report_controller.ex b/lib/pleroma/web/mastodon_api/controllers/report_controller.ex
index 405167108..156544f40 100644
--- a/lib/pleroma/web/mastodon_api/controllers/report_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/report_controller.ex
@@ -3,14 +3,12 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ReportController do
- alias Pleroma.Plugs.OAuthScopesPlug
-
use Pleroma.Web, :controller
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
plug(Pleroma.Web.ApiSpec.CastAndValidate)
- plug(OAuthScopesPlug, %{scopes: ["write:reports"]} when action == :create)
+ plug(Pleroma.Web.Plugs.OAuthScopesPlug, %{scopes: ["write:reports"]} when action == :create)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ReportOperation
diff --git a/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex b/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
index 1719c67ea..322a46497 100644
--- a/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
@@ -7,9 +7,9 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityController do
import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
- alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.ScheduledActivity
alias Pleroma.Web.MastodonAPI.MastodonAPI
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
@oauth_read_actions [:show, :index]
diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
index 5a983db39..0043c3a56 100644
--- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
@@ -6,14 +6,14 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
use Pleroma.Web, :controller
alias Pleroma.Activity
- alias Pleroma.Plugs.OAuthScopesPlug
- alias Pleroma.Plugs.RateLimiter
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web
alias Pleroma.Web.ControllerHelper
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.StatusView
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+ alias Pleroma.Web.Plugs.RateLimiter
require Logger
diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
index 9bb2ef117..9e3a584f0 100644
--- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
@@ -13,8 +13,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
alias Pleroma.Activity
alias Pleroma.Bookmark
alias Pleroma.Object
- alias Pleroma.Plugs.OAuthScopesPlug
- alias Pleroma.Plugs.RateLimiter
alias Pleroma.Repo
alias Pleroma.ScheduledActivity
alias Pleroma.User
@@ -23,9 +21,15 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.ScheduledActivityView
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+ alias Pleroma.Web.Plugs.RateLimiter
plug(Pleroma.Web.ApiSpec.CastAndValidate)
- plug(:skip_plug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action in [:index, :show])
+
+ plug(
+ :skip_plug,
+ Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug when action in [:index, :show]
+ )
@unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
@@ -105,7 +109,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
`ids` query param is required
"""
- def index(%{assigns: %{user: user}} = conn, %{ids: ids} = _params) do
+ def index(%{assigns: %{user: user}} = conn, %{ids: ids} = params) do
limit = 100
activities =
@@ -117,15 +121,15 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
render(conn, "index.json",
activities: activities,
for: user,
- as: :activity
+ as: :activity,
+ with_muted: Map.get(params, :with_muted, false)
)
end
@doc """
POST /api/v1/statuses
-
- Creates a scheduled status when `scheduled_at` param is present and it's far enough
"""
+ # Creates a scheduled status when `scheduled_at` param is present and it's far enough
def create(
%{
assigns: %{user: user},
@@ -156,11 +160,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
end
- @doc """
- POST /api/v1/statuses
-
- Creates a regular status
- """
+ # Creates a regular status
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])
@@ -190,13 +190,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "GET /api/v1/statuses/:id"
- def show(%{assigns: %{user: user}} = conn, %{id: id}) do
+ def show(%{assigns: %{user: user}} = conn, %{id: id} = params) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
true <- Visibility.visible_for_user?(activity, user) do
try_render(conn, "show.json",
activity: activity,
for: user,
- with_direct_conversation_id: true
+ with_direct_conversation_id: true,
+ with_muted: Map.get(params, :with_muted, false)
)
else
_ -> {:error, :not_found}
@@ -285,9 +286,9 @@ 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}, body_params: params} = conn, %{id: id}) do
with %Activity{} = activity <- Activity.get_by_id(id),
- {:ok, activity} <- CommonAPI.add_mute(user, activity) do
+ {:ok, activity} <- CommonAPI.add_mute(user, activity, params) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end
@@ -314,7 +315,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
@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),
+ 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 =
diff --git a/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex b/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex
index 34eac97c5..20138908c 100644
--- a/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex
@@ -13,7 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do
plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(:restrict_push_enabled)
- plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["push"]})
+ plug(Pleroma.Web.Plugs.OAuthScopesPlug, %{scopes: ["push"]})
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.SubscriptionOperation
diff --git a/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex b/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex
index f91df9ab7..5765271cf 100644
--- a/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex
@@ -8,7 +8,7 @@ defmodule Pleroma.Web.MastodonAPI.SuggestionController do
require Logger
plug(Pleroma.Web.ApiSpec.CastAndValidate)
- plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["read"]} when action == :index)
+ plug(Pleroma.Web.Plugs.OAuthScopesPlug, %{scopes: ["read"]} when action == :index)
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
index ab7b1d6aa..852bd0695 100644
--- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
@@ -8,12 +8,13 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
import Pleroma.Web.ControllerHelper,
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
+ alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+ alias Pleroma.Web.Plugs.RateLimiter
plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:public, :hashtag])
@@ -61,7 +62,8 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|> render("index.json",
activities: activities,
for: user,
- as: :activity
+ as: :activity,
+ with_muted: Map.get(params, :with_muted, false)
)
end
@@ -89,11 +91,11 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
end
defp restrict_unauthenticated?(true = _local_only) do
- Pleroma.Config.get([:restrict_unauthenticated, :timelines, :local])
+ Config.restrict_unauthenticated_access?(:timelines, :local)
end
defp restrict_unauthenticated?(_) do
- Pleroma.Config.get([:restrict_unauthenticated, :timelines, :federated])
+ Config.restrict_unauthenticated_access?(:timelines, :federated)
end
# GET /api/v1/timelines/public
@@ -110,6 +112,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|> Map.put(:blocking_user, user)
|> Map.put(:muting_user, user)
|> Map.put(:reply_filtering_user, user)
+ |> Map.put(:instance, params[:instance])
|> ActivityPub.fetch_public_activities()
conn
@@ -117,7 +120,8 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|> render("index.json",
activities: activities,
for: user,
- as: :activity
+ as: :activity,
+ with_muted: Map.get(params, :with_muted, false)
)
end
end
@@ -171,7 +175,8 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|> render("index.json",
activities: activities,
for: user,
- as: :activity
+ as: :activity,
+ with_muted: Map.get(params, :with_muted, false)
)
end
end
@@ -181,11 +186,10 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
params =
params
- |> Map.new(fn {key, value} -> {to_string(key), value} end)
- |> Map.put("type", "Create")
- |> Map.put("blocking_user", user)
- |> Map.put("user", user)
- |> 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).
@@ -201,7 +205,8 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
render(conn, "index.json",
activities: activities,
for: user,
- as: :activity
+ as: :activity,
+ with_muted: Map.get(params, :with_muted, false)
)
else
_e -> render_error(conn, :forbidden, "Error.")
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index 864c0417f..3158d09ed 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -181,8 +181,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
user = User.sanitize_html(user, User.html_filter_policy(opts[:for]))
display_name = user.name || user.nickname
- image = User.avatar_url(user) |> MediaProxy.url()
+ avatar = User.avatar_url(user) |> MediaProxy.url()
+ avatar_static = User.avatar_url(user) |> MediaProxy.preview_url(static: true)
header = User.banner_url(user) |> MediaProxy.url()
+ header_static = User.banner_url(user) |> MediaProxy.preview_url(static: true)
following_count =
if !user.hide_follows_count or !user.hide_follows or opts[:for] == user do
@@ -240,17 +242,17 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
username: username_from_nickname(user.nickname),
acct: user.nickname,
display_name: display_name,
- locked: user.locked,
+ locked: user.is_locked,
created_at: Utils.to_masto_date(user.inserted_at),
followers_count: followers_count,
following_count: following_count,
statuses_count: user.note_count,
- note: user.bio || "",
+ note: user.bio,
url: user.uri || user.ap_id,
- avatar: image,
- avatar_static: image,
+ avatar: avatar,
+ avatar_static: avatar_static,
header: header,
- header_static: header,
+ header_static: header_static,
emojis: emojis,
fields: user.fields,
bot: bot,
@@ -259,7 +261,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
sensitive: false,
fields: user.raw_fields,
pleroma: %{
- discoverable: user.discoverable,
+ discoverable: user.is_discoverable,
actor_type: user.actor_type
}
},
@@ -386,7 +388,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
data
|> Kernel.put_in(
[:pleroma, :unread_conversation_count],
- user.unread_conversation_count
+ Pleroma.Conversation.Participation.unread_count(user)
)
end
diff --git a/lib/pleroma/web/mastodon_api/views/conversation_view.ex b/lib/pleroma/web/mastodon_api/views/conversation_view.ex
index a91994915..82fcff062 100644
--- a/lib/pleroma/web/mastodon_api/views/conversation_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/conversation_view.ex
@@ -33,8 +33,15 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do
end
activity = Activity.get_by_id_with_object(last_activity_id)
- # Conversations return all users except the current user.
- users = Enum.reject(participation.recipients, &(&1.id == user.id))
+
+ # Conversations return all users except the current user,
+ # except when the current user is the only participant
+ users =
+ if length(participation.recipients) > 1 do
+ Enum.reject(participation.recipients, &(&1.id == user.id))
+ else
+ participation.recipients
+ end
%{
id: participation.id |> to_string(),
@@ -43,7 +50,8 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do
last_status:
render(StatusView, "show.json",
activity: activity,
- direct_conversation_id: participation.id
+ direct_conversation_id: participation.id,
+ for: user
)
}
end
diff --git a/lib/pleroma/web/mastodon_api/views/filter_view.ex b/lib/pleroma/web/mastodon_api/views/filter_view.ex
index aeff646f5..c37f624e0 100644
--- a/lib/pleroma/web/mastodon_api/views/filter_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/filter_view.ex
@@ -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 cd3bc7f00..c5aca5506 100644
--- a/lib/pleroma/web/mastodon_api/views/instance_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex
@@ -23,9 +23,10 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
streaming_api: Pleroma.Web.Endpoint.websocket_url()
},
stats: Pleroma.Stats.get_stats(),
- thumbnail: Keyword.get(instance, :instance_thumbnail),
+ thumbnail: Pleroma.Web.base_url() <> 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),
@@ -33,7 +34,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
avatar_upload_limit: Keyword.get(instance, :avatar_upload_limit),
background_upload_limit: Keyword.get(instance, :background_upload_limit),
banner_upload_limit: Keyword.get(instance, :banner_upload_limit),
- background_image: Keyword.get(instance, :background_image),
+ background_image: Pleroma.Web.base_url() <> Keyword.get(instance, :background_image),
chat_limit: Keyword.get(instance, :chat_limit),
description_limit: Keyword.get(instance, :description_limit),
pleroma: %{
diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex
index c97e6d32f..5b06a6b51 100644
--- a/lib/pleroma/web/mastodon_api/views/notification_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex
@@ -11,6 +11,8 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.UserRelationship
+ alias Pleroma.Web.AdminAPI.Report
+ alias Pleroma.Web.AdminAPI.ReportView
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.NotificationView
@@ -118,11 +120,20 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
"pleroma:chat_mention" ->
put_chat_message(response, activity, reading_user, status_render_opts)
+ "pleroma:report" ->
+ put_report(response, activity)
+
type when type in ["follow", "follow_request"] ->
response
end
end
+ defp put_report(response, activity) do
+ report_render = ReportView.render("show.json", Report.extract_report_info(activity))
+
+ Map.put(response, :report, report_render)
+ end
+
defp put_emoji(response, activity) do
Map.put(response, :emoji, activity.data["content"])
end
diff --git a/lib/pleroma/web/mastodon_api/views/poll_view.ex b/lib/pleroma/web/mastodon_api/views/poll_view.ex
index 59a5deb28..4101f21d0 100644
--- a/lib/pleroma/web/mastodon_api/views/poll_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/poll_view.ex
@@ -19,7 +19,7 @@ defmodule Pleroma.Web.MastodonAPI.PollView do
expired: expired,
multiple: multiple,
votes_count: votes_count,
- voters_count: (multiple || nil) && voters_count(object),
+ voters_count: voters_count(object),
options: options,
voted: voted?(params),
emojis: Pleroma.Web.MastodonAPI.StatusView.build_emojis(object.data["emoji"])
@@ -28,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}))
_ ->
@@ -40,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
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index 91b41ef59..2301e21cf 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -8,7 +8,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
require Pleroma.Constants
alias Pleroma.Activity
- alias Pleroma.ActivityExpiration
alias Pleroma.HTML
alias Pleroma.Object
alias Pleroma.Repo
@@ -20,9 +19,21 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
alias Pleroma.Web.MastodonAPI.PollView
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.MediaProxy
+ alias Pleroma.Web.PleromaAPI.EmojiReactionController
import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1, visible_for_user?: 2]
+ # This is a naive way to do this, just spawning a process per activity
+ # to fetch the preview. However it should be fine considering
+ # pagination is restricted to 40 activities at a time
+ defp fetch_rich_media_for_activities(activities) do
+ Enum.each(activities, fn activity ->
+ spawn(fn ->
+ Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
+ end)
+ end)
+ end
+
# TODO: Add cached version.
defp get_replied_to_activities([]), do: %{}
@@ -45,23 +56,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
end)
end
- def get_user(ap_id, fake_record_fallback \\ true) do
- cond do
- user = User.get_cached_by_ap_id(ap_id) ->
- user
-
- user = User.get_by_guessed_nickname(ap_id) ->
- user
-
- fake_record_fallback ->
- # TODO: refactor (fake records is never a good idea)
- User.error_user(ap_id)
-
- true ->
- nil
- end
- end
-
defp get_context_id(%{data: %{"context_id" => context_id}}) when not is_nil(context_id),
do: context_id
@@ -80,6 +74,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
# To do: check AdminAPIControllerTest on the reasons behind nil activities in the list
activities = Enum.filter(opts.activities, & &1)
+
+ # Start fetching rich media before doing anything else, so that later calls to get the cards
+ # only block for timeout in the worst case, as opposed to
+ # length(activities_with_links) * timeout
+ fetch_rich_media_for_activities(activities)
replied_to_activities = get_replied_to_activities(activities)
parent_activities =
@@ -104,7 +103,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
# Note: unresolved users are filtered out
actors =
(activities ++ parent_activities)
- |> Enum.map(&get_user(&1.data["actor"], false))
+ |> Enum.map(&CommonAPI.get_user(&1.data["actor"], false))
|> Enum.filter(& &1)
UserRelationship.view_relationships_option(reading_user, actors, subset: :source_mutes)
@@ -123,7 +122,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
"show.json",
%{activity: %{data: %{"type" => "Announce", "object" => _object}} = activity} = opts
) do
- user = get_user(activity.data["actor"])
+ user = CommonAPI.get_user(activity.data["actor"])
created_at = Utils.to_masto_date(activity.data["published"])
activity_object = Object.normalize(activity)
@@ -196,7 +195,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
object = Object.normalize(activity)
- user = get_user(activity.data["actor"])
+ user = CommonAPI.get_user(activity.data["actor"])
user_follower_address = user.follower_address
like_count = object.data["like_count"] || 0
@@ -229,8 +228,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
expires_at =
with true <- client_posted_this_activity,
- %ActivityExpiration{scheduled_at: scheduled_at} <-
- ActivityExpiration.get_by_activity_id(activity.id) do
+ %Oban.Job{scheduled_at: scheduled_at} <-
+ Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id) do
scheduled_at
else
_ -> nil
@@ -250,7 +249,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
reply_to = get_reply_to(activity, opts)
- reply_to_user = reply_to && get_user(reply_to.data["actor"])
+ reply_to_user = reply_to && CommonAPI.get_user(reply_to.data["actor"])
content =
object
@@ -296,21 +295,16 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
end
emoji_reactions =
- with %{data: %{"reactions" => emoji_reactions}} <- object do
- 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
+ object.data
+ |> Map.get("reactions", [])
+ |> EmojiReactionController.filter_allowed_users(
+ opts[:for],
+ Map.get(opts, :with_muted, false)
+ )
+ |> Stream.map(fn {emoji, users} ->
+ build_emoji_map(emoji, users, opts[:for])
+ end)
+ |> Enum.to_list()
# Status muted state (would do 1 request per status unless user mutes are preloaded)
muted =
@@ -417,6 +411,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
[attachment_url | _] = attachment["url"]
media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
href = attachment_url["href"] |> MediaProxy.url()
+ href_preview = attachment_url["href"] |> MediaProxy.preview_url()
type =
cond do
@@ -432,11 +427,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
id: to_string(attachment["id"] || hash_id),
url: href,
remote_url: href,
- preview_url: href,
+ preview_url: href_preview,
text_url: href,
type: type,
description: attachment["name"],
- pleroma: %{mime_type: media_type}
+ pleroma: %{mime_type: media_type},
+ blurhash: attachment["blurhash"]
}
end
@@ -473,23 +469,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"] || ""
diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex
index 94e4595d8..439cdd716 100644
--- a/lib/pleroma/web/mastodon_api/websocket_handler.ex
+++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex
@@ -23,8 +23,8 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
with params <- Enum.into(:cow_qs.parse_qs(qs), %{}),
sec_websocket <- :cowboy_req.header("sec-websocket-protocol", req, nil),
access_token <- Map.get(params, "access_token"),
- {:ok, user} <- authenticate_request(access_token, sec_websocket),
- {:ok, topic} <- Streamer.get_topic(Map.get(params, "stream"), user, params) do
+ {:ok, user, oauth_token} <- authenticate_request(access_token, sec_websocket),
+ {:ok, topic} <- Streamer.get_topic(params["stream"], user, oauth_token, params) do
req =
if sec_websocket do
:cowboy_req.set_resp_header("sec-websocket-protocol", sec_websocket, req)
@@ -37,12 +37,12 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
else
{:error, :bad_topic} ->
Logger.debug("#{__MODULE__} bad topic #{inspect(req)}")
- {:ok, req} = :cowboy_req.reply(404, req)
+ req = :cowboy_req.reply(404, req)
{:ok, req, state}
{:error, :unauthorized} ->
Logger.debug("#{__MODULE__} authentication error: #{inspect(req)}")
- {:ok, req} = :cowboy_req.reply(401, req)
+ req = :cowboy_req.reply(401, req)
{:ok, req, state}
end
end
@@ -64,7 +64,9 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
{:ok, %{state | timer: timer()}}
end
- # We never receive messages.
+ # We only receive pings for now
+ def websocket_handle(:ping, state), do: {:ok, state}
+
def websocket_handle(frame, state) do
Logger.error("#{__MODULE__} received frame: #{inspect(frame)}")
{:ok, state}
@@ -98,6 +100,10 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
{:reply, :ping, %{state | timer: nil, count: 0}, :hibernate}
end
+ # State can be `[]` only in case we terminate before switching to websocket,
+ # we already log errors for these cases in `init/1`, so just do nothing here
+ def terminate(_reason, _req, []), do: :ok
+
def terminate(reason, _req, state) do
Logger.debug(
"#{__MODULE__} terminating websocket connection for user #{
@@ -111,7 +117,7 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
# Public streams without authentication.
defp authenticate_request(nil, nil) do
- {:ok, nil}
+ {:ok, nil, nil}
end
# Authenticated streams.
@@ -119,9 +125,9 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
token = access_token || sec_websocket
with true <- is_bitstring(token),
- %Token{user_id: user_id} <- Repo.get_by(Token, token: token),
+ oauth_token = %Token{user_id: user_id} <- Repo.get_by(Token, token: token),
user = %User{} <- User.get_cached_by_id(user_id) do
- {:ok, user}
+ {:ok, user, oauth_token}
else
_ -> {:error, :unauthorized}
end
diff --git a/lib/pleroma/web/media_proxy/media_proxy.ex b/lib/pleroma/web/media_proxy.ex
index dfbfcea6b..8656b8cad 100644
--- a/lib/pleroma/web/media_proxy/media_proxy.ex
+++ b/lib/pleroma/web/media_proxy.ex
@@ -4,60 +4,72 @@
defmodule Pleroma.Web.MediaProxy do
alias Pleroma.Config
+ alias Pleroma.Helpers.UriHelper
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?(:banned_urls_cache, url(url)), 1)
+ 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!(:banned_urls_cache, fn cache ->
+ 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(:banned_urls_cache, url(url))
+ Cachex.del(@cache_table, url(url))
end
def put_in_banned_urls(urls) when is_list(urls) do
- Cachex.execute!(:banned_urls_cache, fn cache ->
+ 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(:banned_urls_cache, url(url), true)
+ 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 not url_proxiable?(url) do
- url
- else
+ if enabled?() and url_proxiable?(url) do
encode_url(url)
+ else
+ url
end
end
@spec url_proxiable?(String.t()) :: boolean()
def url_proxiable?(url) do
- if local?(url) or whitelisted?(url) do
- false
+ not local?(url) and not whitelisted?(url)
+ end
+
+ def preview_url(url, preview_params \\ []) do
+ if preview_enabled?() do
+ encode_preview_url(url, preview_params)
else
- true
+ url(url)
end
end
- defp disabled?, do: !Config.get([:media_proxy, :enabled], false)
+ def enabled?, do: Config.get([:media_proxy, :enabled], false)
- defp local?(url), do: String.starts_with?(url, Pleroma.Web.base_url())
+ # Note: media proxy must be enabled for media preview proxy in order to load all
+ # non-local non-whitelisted URLs through it and be sure that body size constraint is preserved.
+ def preview_enabled?, do: enabled?() and !!Config.get([:media_preview_proxy, :enabled])
- defp whitelisted?(url) do
+ def local?(url), do: String.starts_with?(url, Pleroma.Web.base_url())
+
+ def whitelisted?(url) do
%{host: domain} = URI.parse(url)
mediaproxy_whitelist_domains =
@@ -82,17 +94,29 @@ defmodule Pleroma.Web.MediaProxy do
defp maybe_get_domain_from_url(domain), do: domain
- def encode_url(url) do
+ defp base64_sig64(url) do
base64 = Base.url_encode64(url, @base64_opts)
sig64 =
base64
- |> signed_url
+ |> signed_url()
|> Base.url_encode64(@base64_opts)
+ {base64, sig64}
+ end
+
+ def encode_url(url) do
+ {base64, sig64} = base64_sig64(url)
+
build_url(sig64, base64, filename(url))
end
+ def encode_preview_url(url, preview_params \\ []) do
+ {base64, sig64} = base64_sig64(url)
+
+ build_preview_url(sig64, base64, filename(url), preview_params)
+ end
+
def decode_url(sig, url) do
with {:ok, sig} <- Base.url_decode64(sig, @base64_opts),
signature when signature == sig <- signed_url(url) do
@@ -110,10 +134,14 @@ defmodule Pleroma.Web.MediaProxy do
if path = URI.parse(url_or_path).path, do: Path.basename(path)
end
- def build_url(sig_base64, url_base64, filename \\ nil) do
+ def base_url do
+ Config.get([:media_proxy, :base_url], Web.base_url())
+ end
+
+ defp proxy_url(path, sig_base64, url_base64, filename) do
[
- Config.get([:media_proxy, :base_url], Web.base_url()),
- "proxy",
+ base_url(),
+ path,
sig_base64,
url_base64,
filename
@@ -121,4 +149,38 @@ defmodule Pleroma.Web.MediaProxy do
|> Enum.filter(& &1)
|> Path.join()
end
+
+ def build_url(sig_base64, url_base64, filename \\ nil) do
+ proxy_url("proxy", sig_base64, url_base64, filename)
+ end
+
+ def build_preview_url(sig_base64, url_base64, filename \\ nil, preview_params \\ []) do
+ uri = proxy_url("proxy/preview", sig_base64, url_base64, filename)
+
+ UriHelper.modify_uri_params(uri, preview_params)
+ end
+
+ def verify_request_path_and_url(
+ %Plug.Conn{params: %{"filename" => _}, request_path: request_path},
+ url
+ ) do
+ verify_request_path_and_url(request_path, url)
+ end
+
+ def verify_request_path_and_url(request_path, url) when is_binary(request_path) do
+ filename = filename(url)
+
+ if filename && not basename_matches?(request_path, filename) do
+ {:wrong_filename, filename}
+ else
+ :ok
+ end
+ end
+
+ def verify_request_path_and_url(_, _), do: :ok
+
+ defp basename_matches?(path, filename) do
+ basename = Path.basename(path)
+ basename == filename or URI.decode(basename) == filename or URI.encode(basename) == filename
+ end
end
diff --git a/lib/pleroma/web/media_proxy/invalidation.ex b/lib/pleroma/web/media_proxy/invalidation.ex
index 5808861e6..4f4340478 100644
--- a/lib/pleroma/web/media_proxy/invalidation.ex
+++ b/lib/pleroma/web/media_proxy/invalidation.ex
@@ -33,6 +33,8 @@ defmodule Pleroma.Web.MediaProxy.Invalidation do
def prepare_urls(urls) do
urls
|> List.wrap()
- |> Enum.map(&MediaProxy.url/1)
+ |> Enum.map(fn url -> [MediaProxy.url(url), MediaProxy.preview_url(url)] end)
+ |> List.flatten()
+ |> Enum.uniq()
end
end
diff --git a/lib/pleroma/web/media_proxy/invalidations/http.ex b/lib/pleroma/web/media_proxy/invalidation/http.ex
index bb81d8888..0b0cde68c 100644
--- a/lib/pleroma/web/media_proxy/invalidations/http.ex
+++ b/lib/pleroma/web/media_proxy/invalidation/http.ex
@@ -30,7 +30,7 @@ defmodule Pleroma.Web.MediaProxy.Invalidation.Http do
{:ok, %{status: status} = env} when 400 <= status and status < 500 ->
{:error, env}
- {:error, error} = error ->
+ {:error, _} = error ->
error
_ ->
diff --git a/lib/pleroma/web/media_proxy/invalidations/script.ex b/lib/pleroma/web/media_proxy/invalidation/script.ex
index d32ffc50b..d32ffc50b 100644
--- a/lib/pleroma/web/media_proxy/invalidations/script.ex
+++ b/lib/pleroma/web/media_proxy/invalidation/script.ex
diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex
index 9a64b0ef3..90651ed9b 100644
--- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex
+++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex
@@ -5,44 +5,201 @@
defmodule Pleroma.Web.MediaProxy.MediaProxyController do
use Pleroma.Web, :controller
+ alias Pleroma.Config
+ alias Pleroma.Helpers.MediaHelper
+ alias Pleroma.Helpers.UriHelper
alias Pleroma.ReverseProxy
alias Pleroma.Web.MediaProxy
+ alias Plug.Conn
- @default_proxy_opts [max_body_length: 25 * 1_048_576, http: [follow_redirect: true]]
-
- def remote(conn, %{"sig" => sig64, "url" => url64} = params) do
- with config <- Pleroma.Config.get([:media_proxy], []),
- true <- Keyword.get(config, :enabled, false),
+ def remote(conn, %{"sig" => sig64, "url" => url64}) do
+ with {_, true} <- {:enabled, MediaProxy.enabled?()},
{: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))
+ :ok <- MediaProxy.verify_request_path_and_url(conn, url) do
+ ReverseProxy.call(conn, url, media_proxy_opts())
else
- error when error in [false, {:in_banned_urls, true}] ->
- send_resp(conn, 404, Plug.Conn.Status.reason_phrase(404))
+ {:enabled, false} ->
+ send_resp(conn, 404, Conn.Status.reason_phrase(404))
+
+ {:in_banned_urls, true} ->
+ send_resp(conn, 404, Conn.Status.reason_phrase(404))
{:error, :invalid_signature} ->
- send_resp(conn, 403, Plug.Conn.Status.reason_phrase(403))
+ send_resp(conn, 403, Conn.Status.reason_phrase(403))
{:wrong_filename, filename} ->
redirect(conn, external: MediaProxy.build_url(sig64, url64, filename))
end
end
- def filename_matches(%{"filename" => _} = _, path, url) do
- filename = MediaProxy.filename(url)
+ def preview(%Conn{} = conn, %{"sig" => sig64, "url" => url64}) do
+ with {_, true} <- {:enabled, MediaProxy.preview_enabled?()},
+ {:ok, url} <- MediaProxy.decode_url(sig64, url64),
+ :ok <- MediaProxy.verify_request_path_and_url(conn, url) do
+ handle_preview(conn, url)
+ else
+ {:enabled, false} ->
+ send_resp(conn, 404, Conn.Status.reason_phrase(404))
+
+ {:error, :invalid_signature} ->
+ send_resp(conn, 403, Conn.Status.reason_phrase(403))
+
+ {:wrong_filename, filename} ->
+ redirect(conn, external: MediaProxy.build_preview_url(sig64, url64, filename))
+ end
+ end
+
+ defp handle_preview(conn, url) do
+ media_proxy_url = MediaProxy.url(url)
+
+ with {:ok, %{status: status} = head_response} when status in 200..299 <-
+ Pleroma.HTTP.request("head", media_proxy_url, [], [], pool: :media) do
+ content_type = Tesla.get_header(head_response, "content-type")
+ content_length = Tesla.get_header(head_response, "content-length")
+ content_length = content_length && String.to_integer(content_length)
+ static = conn.params["static"] in ["true", true]
+
+ cond do
+ static and content_type == "image/gif" ->
+ handle_jpeg_preview(conn, media_proxy_url)
+
+ static ->
+ drop_static_param_and_redirect(conn)
+
+ content_type == "image/gif" ->
+ redirect(conn, external: media_proxy_url)
+
+ min_content_length_for_preview() > 0 and content_length > 0 and
+ content_length < min_content_length_for_preview() ->
+ redirect(conn, external: media_proxy_url)
+
+ true ->
+ handle_preview(content_type, conn, media_proxy_url)
+ end
+ else
+ # If HEAD failed, redirecting to media proxy URI doesn't make much sense; returning an error
+ {_, %{status: status}} ->
+ send_resp(conn, :failed_dependency, "Can't fetch HTTP headers (HTTP #{status}).")
+
+ {:error, :recv_response_timeout} ->
+ send_resp(conn, :failed_dependency, "HEAD request timeout.")
+
+ _ ->
+ send_resp(conn, :failed_dependency, "Can't fetch HTTP headers.")
+ end
+ end
+
+ defp handle_preview("image/png" <> _ = _content_type, conn, media_proxy_url) do
+ handle_png_preview(conn, media_proxy_url)
+ end
+
+ defp handle_preview("image/" <> _ = _content_type, conn, media_proxy_url) do
+ handle_jpeg_preview(conn, media_proxy_url)
+ end
+
+ defp handle_preview("video/" <> _ = _content_type, conn, media_proxy_url) do
+ handle_video_preview(conn, media_proxy_url)
+ end
+
+ defp handle_preview(_unsupported_content_type, conn, media_proxy_url) do
+ fallback_on_preview_error(conn, media_proxy_url)
+ end
+
+ defp handle_png_preview(conn, media_proxy_url) do
+ quality = Config.get!([:media_preview_proxy, :image_quality])
+ {thumbnail_max_width, thumbnail_max_height} = thumbnail_max_dimensions()
+
+ with {:ok, thumbnail_binary} <-
+ MediaHelper.image_resize(
+ media_proxy_url,
+ %{
+ max_width: thumbnail_max_width,
+ max_height: thumbnail_max_height,
+ quality: quality,
+ format: "png"
+ }
+ ) do
+ conn
+ |> put_preview_response_headers(["image/png", "preview.png"])
+ |> send_resp(200, thumbnail_binary)
+ else
+ _ ->
+ fallback_on_preview_error(conn, media_proxy_url)
+ end
+ end
+
+ defp handle_jpeg_preview(conn, media_proxy_url) do
+ quality = Config.get!([:media_preview_proxy, :image_quality])
+ {thumbnail_max_width, thumbnail_max_height} = thumbnail_max_dimensions()
- if filename && does_not_match(path, filename) do
- {:wrong_filename, filename}
+ with {:ok, thumbnail_binary} <-
+ MediaHelper.image_resize(
+ media_proxy_url,
+ %{max_width: thumbnail_max_width, max_height: thumbnail_max_height, quality: quality}
+ ) do
+ conn
+ |> put_preview_response_headers()
+ |> send_resp(200, thumbnail_binary)
else
- :ok
+ _ ->
+ fallback_on_preview_error(conn, media_proxy_url)
end
end
- def filename_matches(_, _, _), do: :ok
+ defp handle_video_preview(conn, media_proxy_url) do
+ with {:ok, thumbnail_binary} <-
+ MediaHelper.video_framegrab(media_proxy_url) do
+ conn
+ |> put_preview_response_headers()
+ |> send_resp(200, thumbnail_binary)
+ else
+ _ ->
+ fallback_on_preview_error(conn, media_proxy_url)
+ end
+ end
+
+ defp drop_static_param_and_redirect(conn) do
+ uri_without_static_param =
+ conn
+ |> current_url()
+ |> UriHelper.modify_uri_params(%{}, ["static"])
+
+ redirect(conn, external: uri_without_static_param)
+ end
+
+ defp fallback_on_preview_error(conn, media_proxy_url) do
+ redirect(conn, external: media_proxy_url)
+ end
+
+ defp put_preview_response_headers(
+ conn,
+ [content_type, filename] = _content_info \\ ["image/jpeg", "preview.jpg"]
+ ) do
+ conn
+ |> put_resp_header("content-type", content_type)
+ |> put_resp_header("content-disposition", "inline; filename=\"#{filename}\"")
+ |> put_resp_header("cache-control", ReverseProxy.default_cache_control_header())
+ end
+
+ defp thumbnail_max_dimensions do
+ config = media_preview_proxy_config()
+
+ thumbnail_max_width = Keyword.fetch!(config, :thumbnail_max_width)
+ thumbnail_max_height = Keyword.fetch!(config, :thumbnail_max_height)
+
+ {thumbnail_max_width, thumbnail_max_height}
+ end
+
+ defp min_content_length_for_preview do
+ Keyword.get(media_preview_proxy_config(), :min_content_length, 0)
+ end
+
+ defp media_preview_proxy_config do
+ Config.get!([:media_preview_proxy])
+ end
- defp does_not_match(path, filename) do
- basename = Path.basename(path)
- basename != filename and URI.decode(basename) != filename and URI.encode(basename) != filename
+ defp media_proxy_opts do
+ Config.get([:media_proxy, :proxy_opts], [])
end
end
diff --git a/lib/pleroma/web/metadata.ex b/lib/pleroma/web/metadata.ex
index a9f70c43e..0f2d8d1e7 100644
--- a/lib/pleroma/web/metadata.ex
+++ b/lib/pleroma/web/metadata.ex
@@ -7,8 +7,9 @@ defmodule Pleroma.Web.Metadata do
def build_tags(params) do
providers = [
+ Pleroma.Web.Metadata.Providers.RelMe,
Pleroma.Web.Metadata.Providers.RestrictIndexing
- | Pleroma.Config.get([__MODULE__, :providers], [])
+ | activated_providers()
]
Enum.reduce(providers, "", fn parser, acc ->
@@ -42,4 +43,12 @@ defmodule Pleroma.Web.Metadata do
def activity_nsfw?(_) do
false
end
+
+ defp activated_providers do
+ unless Pleroma.Config.restrict_unauthenticated_access?(:activities, :local) do
+ [Pleroma.Web.Metadata.Providers.Feed | Pleroma.Config.get([__MODULE__, :providers], [])]
+ else
+ []
+ end
+ end
end
diff --git a/lib/pleroma/web/metadata/feed.ex b/lib/pleroma/web/metadata/providers/feed.ex
index bd1459a17..bd1459a17 100644
--- a/lib/pleroma/web/metadata/feed.ex
+++ b/lib/pleroma/web/metadata/providers/feed.ex
diff --git a/lib/pleroma/web/metadata/opengraph.ex b/lib/pleroma/web/metadata/providers/open_graph.ex
index 68c871e71..bb1b23208 100644
--- a/lib/pleroma/web/metadata/opengraph.ex
+++ b/lib/pleroma/web/metadata/providers/open_graph.ex
@@ -61,7 +61,7 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do
@impl Provider
def build_tags(%{user: user}) do
- with truncated_bio = Utils.scrub_html_and_truncate(user.bio || "") do
+ with truncated_bio = Utils.scrub_html_and_truncate(user.bio) do
[
{:meta,
[
diff --git a/lib/pleroma/web/metadata/provider.ex b/lib/pleroma/web/metadata/providers/provider.ex
index 767288f9c..767288f9c 100644
--- a/lib/pleroma/web/metadata/provider.ex
+++ b/lib/pleroma/web/metadata/providers/provider.ex
diff --git a/lib/pleroma/web/metadata/rel_me.ex b/lib/pleroma/web/metadata/providers/rel_me.ex
index 8905c9c72..8905c9c72 100644
--- a/lib/pleroma/web/metadata/rel_me.ex
+++ b/lib/pleroma/web/metadata/providers/rel_me.ex
diff --git a/lib/pleroma/web/metadata/restrict_indexing.ex b/lib/pleroma/web/metadata/providers/restrict_indexing.ex
index f15607896..a08a04b4a 100644
--- a/lib/pleroma/web/metadata/restrict_indexing.ex
+++ b/lib/pleroma/web/metadata/providers/restrict_indexing.ex
@@ -6,11 +6,13 @@ defmodule Pleroma.Web.Metadata.Providers.RestrictIndexing do
@behaviour Pleroma.Web.Metadata.Providers.Provider
@moduledoc """
- Restricts indexing of remote users.
+ Restricts indexing of remote and/or non-discoverable users.
"""
@impl true
- def build_tags(%{user: %{local: false}}) do
+ def build_tags(%{user: %{local: true, is_discoverable: true}}), do: []
+
+ def build_tags(_) do
[
{:meta,
[
@@ -19,7 +21,4 @@ defmodule Pleroma.Web.Metadata.Providers.RestrictIndexing do
], []}
]
end
-
- @impl true
- def build_tags(%{user: %{local: true}}), do: []
end
diff --git a/lib/pleroma/web/metadata/twitter_card.ex b/lib/pleroma/web/metadata/providers/twitter_card.ex
index 5d08ce422..df34b033f 100644
--- a/lib/pleroma/web/metadata/twitter_card.ex
+++ b/lib/pleroma/web/metadata/providers/twitter_card.ex
@@ -40,7 +40,7 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do
@impl Provider
def build_tags(%{user: user}) do
- with truncated_bio = Utils.scrub_html_and_truncate(user.bio || "") do
+ with truncated_bio = Utils.scrub_html_and_truncate(user.bio) do
[
title_tag(user),
{:meta, [property: "twitter:description", content: truncated_bio], []},
diff --git a/lib/pleroma/web/metadata/utils.ex b/lib/pleroma/web/metadata/utils.ex
index 2f0dfb474..8a206e019 100644
--- a/lib/pleroma/web/metadata/utils.ex
+++ b/lib/pleroma/web/metadata/utils.ex
@@ -38,7 +38,7 @@ defmodule Pleroma.Web.Metadata.Utils do
def scrub_html(content), do: content
def attachment_url(url) do
- MediaProxy.url(url)
+ MediaProxy.preview_url(url)
end
def user_name_string(user) do
diff --git a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex b/lib/pleroma/web/mongoose_im/mongoose_im_controller.ex
index 6cbbe8fd8..2a5c7c356 100644
--- a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex
+++ b/lib/pleroma/web/mongoose_im/mongoose_im_controller.ex
@@ -5,10 +5,10 @@
defmodule Pleroma.Web.MongooseIM.MongooseIMController do
use Pleroma.Web, :controller
- alias Pleroma.Plugs.AuthenticationPlug
- alias Pleroma.Plugs.RateLimiter
alias Pleroma.Repo
alias Pleroma.User
+ alias Pleroma.Web.Plugs.AuthenticationPlug
+ alias Pleroma.Web.Plugs.RateLimiter
plug(RateLimiter, [name: :authentication] when action in [:user_exists, :check_password])
plug(RateLimiter, [name: :authentication, params: ["user"]] when action == :check_password)
diff --git a/lib/pleroma/web/oauth.ex b/lib/pleroma/web/o_auth.ex
index 2f1b8708d..2f1b8708d 100644
--- a/lib/pleroma/web/oauth.ex
+++ b/lib/pleroma/web/o_auth.ex
diff --git a/lib/pleroma/web/oauth/app.ex b/lib/pleroma/web/o_auth/app.ex
index df99472e1..df99472e1 100644
--- a/lib/pleroma/web/oauth/app.ex
+++ b/lib/pleroma/web/o_auth/app.ex
diff --git a/lib/pleroma/web/oauth/authorization.ex b/lib/pleroma/web/o_auth/authorization.ex
index 268ee5b63..268ee5b63 100644
--- a/lib/pleroma/web/oauth/authorization.ex
+++ b/lib/pleroma/web/o_auth/authorization.ex
diff --git a/lib/pleroma/web/oauth/fallback_controller.ex b/lib/pleroma/web/o_auth/fallback_controller.ex
index a89ced886..a89ced886 100644
--- a/lib/pleroma/web/oauth/fallback_controller.ex
+++ b/lib/pleroma/web/o_auth/fallback_controller.ex
diff --git a/lib/pleroma/web/oauth/mfa_controller.ex b/lib/pleroma/web/o_auth/mfa_controller.ex
index f102c93e7..f102c93e7 100644
--- a/lib/pleroma/web/oauth/mfa_controller.ex
+++ b/lib/pleroma/web/o_auth/mfa_controller.ex
diff --git a/lib/pleroma/web/oauth/mfa_view.ex b/lib/pleroma/web/o_auth/mfa_view.ex
index 5d87db268..5d87db268 100644
--- a/lib/pleroma/web/oauth/mfa_view.ex
+++ b/lib/pleroma/web/o_auth/mfa_view.ex
diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/o_auth/o_auth_controller.ex
index 7683589cf..d2f9d1ceb 100644
--- a/lib/pleroma/web/oauth/oauth_controller.ex
+++ b/lib/pleroma/web/o_auth/o_auth_controller.ex
@@ -8,7 +8,6 @@ defmodule Pleroma.Web.OAuth.OAuthController do
alias Pleroma.Helpers.UriHelper
alias Pleroma.Maps
alias Pleroma.MFA
- alias Pleroma.Plugs.RateLimiter
alias Pleroma.Registration
alias Pleroma.Repo
alias Pleroma.User
@@ -23,6 +22,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken
alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken
+ alias Pleroma.Web.Plugs.RateLimiter
require Logger
@@ -31,7 +31,10 @@ defmodule Pleroma.Web.OAuth.OAuthController do
plug(:fetch_session)
plug(:fetch_flash)
- plug(:skip_plug, [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug])
+ plug(:skip_plug, [
+ Pleroma.Web.Plugs.OAuthScopesPlug,
+ Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug
+ ])
plug(RateLimiter, [name: :authentication] when action == :create_authorization)
@@ -76,6 +79,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"],
@@ -112,7 +122,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
redirect_uri = redirect_uri(conn, redirect_uri)
url_params = %{access_token: token.token}
url_params = Maps.put_if_present(url_params, :state, params["state"])
- url = UriHelper.append_uri_params(redirect_uri, url_params)
+ url = UriHelper.modify_uri_params(redirect_uri, url_params)
redirect(conn, external: url)
else
conn
@@ -138,7 +148,10 @@ defmodule Pleroma.Web.OAuth.OAuthController do
def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
"authorization" => %{"redirect_uri" => @oob_token_redirect_uri}
}) do
- render(conn, "oob_authorization_created.html", %{auth: auth})
+ # Enforcing the view to reuse the template when calling from other controllers
+ conn
+ |> put_view(OAuthView)
+ |> render("oob_authorization_created.html", %{auth: auth})
end
def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
@@ -151,7 +164,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
redirect_uri = redirect_uri(conn, redirect_uri)
url_params = %{code: auth.token}
url_params = Maps.put_if_present(url_params, :state, auth_attrs["state"])
- url = UriHelper.append_uri_params(redirect_uri, url_params)
+ url = UriHelper.modify_uri_params(redirect_uri, url_params)
redirect(conn, external: url)
else
conn
@@ -190,7 +203,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
{:mfa_required, user, auth, _},
params
) do
- {:ok, token} = MFA.Token.create_token(user, auth)
+ {:ok, token} = MFA.Token.create(user, auth)
data = %{
"mfa_token" => token.token,
@@ -260,11 +273,8 @@ 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),
- {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)},
- {:ok, token} <- Token.exchange_token(app, auth) do
+ 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 ->
@@ -337,6 +347,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
@@ -512,6 +532,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,
%{
@@ -521,19 +543,37 @@ 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)},
- {:ok, auth} <- Authorization.create_authorization(app, user, scopes) do
+ 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
+
# Special case: Local MastodonFE
defp redirect_uri(%Plug.Conn{} = conn, "."), do: auth_url(conn, :login)
@@ -545,17 +585,20 @@ defmodule Pleroma.Web.OAuth.OAuthController do
do: put_session(conn, :registration_id, registration_id)
defp build_and_response_mfa_token(user, auth) do
- with {:ok, token} <- MFA.Token.create_token(user, auth) do
+ with {:ok, token} <- MFA.Token.create(user, auth) do
MFAView.render("mfa_response.json", %{token: token, user: user})
end
end
- @spec validate_scopes(App.t(), map()) ::
+ @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/o_auth/o_auth_view.ex
index f55247ebd..f55247ebd 100644
--- a/lib/pleroma/web/oauth/oauth_view.ex
+++ b/lib/pleroma/web/o_auth/o_auth_view.ex
diff --git a/lib/pleroma/web/oauth/scopes.ex b/lib/pleroma/web/o_auth/scopes.ex
index 6f06f1431..90b9a0471 100644
--- a/lib/pleroma/web/oauth/scopes.ex
+++ b/lib/pleroma/web/o_auth/scopes.ex
@@ -7,7 +7,7 @@ defmodule Pleroma.Web.OAuth.Scopes do
Functions for dealing with scopes.
"""
- alias Pleroma.Plugs.OAuthScopesPlug
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
@doc """
Fetch scopes from request params.
diff --git a/lib/pleroma/web/oauth/token.ex b/lib/pleroma/web/o_auth/token.ex
index 08bb7326d..de37998f2 100644
--- a/lib/pleroma/web/oauth/token.ex
+++ b/lib/pleroma/web/o_auth/token.ex
@@ -50,7 +50,7 @@ defmodule Pleroma.Web.OAuth.Token do
true <- auth.app_id == app.id do
user = if auth.user_id, do: User.get_cached_by_id(auth.user_id), else: %User{}
- create_token(
+ create(
app,
user,
%{scopes: auth.scopes}
@@ -83,8 +83,22 @@ defmodule Pleroma.Web.OAuth.Token do
|> validate_required([:valid_until])
end
- @spec create_token(App.t(), User.t(), map()) :: {:ok, Token} | {:error, Changeset.t()}
- def create_token(%App{} = app, %User{} = user, attrs \\ %{}) do
+ @spec create(App.t(), User.t(), map()) :: {:ok, Token} | {:error, Changeset.t()}
+ def create(%App{} = app, %User{} = user, attrs \\ %{}) do
+ with {:ok, token} <- do_create(app, user, attrs) do
+ if Pleroma.Config.get([:oauth2, :clean_expired_tokens]) do
+ Pleroma.Workers.PurgeExpiredToken.enqueue(%{
+ token_id: token.id,
+ valid_until: DateTime.from_naive!(token.valid_until, "Etc/UTC"),
+ mod: __MODULE__
+ })
+ end
+
+ {:ok, token}
+ end
+ end
+
+ defp do_create(app, user, attrs) do
%__MODULE__{user_id: user.id, app_id: app.id}
|> cast(%{scopes: attrs[:scopes] || app.scopes}, [:scopes])
|> validate_required([:scopes, :app_id])
@@ -105,11 +119,6 @@ defmodule Pleroma.Web.OAuth.Token do
|> Repo.delete_all()
end
- def delete_expired_tokens do
- Query.get_expired_tokens()
- |> Repo.delete_all()
- end
-
def get_user_tokens(%User{id: user_id}) do
Query.get_by_user(user_id)
|> Query.preload([:app])
diff --git a/lib/pleroma/web/oauth/token/query.ex b/lib/pleroma/web/o_auth/token/query.ex
index 93d6e26ed..fd6d9b112 100644
--- a/lib/pleroma/web/oauth/token/query.ex
+++ b/lib/pleroma/web/o_auth/token/query.ex
@@ -33,12 +33,6 @@ defmodule Pleroma.Web.OAuth.Token.Query do
from(q in query, where: q.id == ^id)
end
- @spec get_expired_tokens(query, DateTime.t() | nil) :: query
- def get_expired_tokens(query \\ Token, date \\ nil) do
- expired_date = date || Timex.now()
- from(q in query, where: fragment("?", q.valid_until) < ^expired_date)
- end
-
@spec get_by_user(query, String.t()) :: query
def get_by_user(query \\ Token, user_id) do
from(q in query, where: q.user_id == ^user_id)
diff --git a/lib/pleroma/web/oauth/token/strategy/refresh_token.ex b/lib/pleroma/web/o_auth/token/strategy/refresh_token.ex
index debc29b0b..625b0fde2 100644
--- a/lib/pleroma/web/oauth/token/strategy/refresh_token.ex
+++ b/lib/pleroma/web/o_auth/token/strategy/refresh_token.ex
@@ -46,7 +46,7 @@ defmodule Pleroma.Web.OAuth.Token.Strategy.RefreshToken do
defp create_access_token({:error, error}, _), do: {:error, error}
defp create_access_token({:ok, token}, %{app: app, user: user} = token_params) do
- Token.create_token(app, user, add_refresh_token(token_params, token.refresh_token))
+ Token.create(app, user, add_refresh_token(token_params, token.refresh_token))
end
defp add_refresh_token(params, token) do
diff --git a/lib/pleroma/web/oauth/token/strategy/revoke.ex b/lib/pleroma/web/o_auth/token/strategy/revoke.ex
index 069c1ee21..069c1ee21 100644
--- a/lib/pleroma/web/oauth/token/strategy/revoke.ex
+++ b/lib/pleroma/web/o_auth/token/strategy/revoke.ex
diff --git a/lib/pleroma/web/oauth/token/utils.ex b/lib/pleroma/web/o_auth/token/utils.ex
index 43aeab6b0..43aeab6b0 100644
--- a/lib/pleroma/web/oauth/token/utils.ex
+++ b/lib/pleroma/web/o_auth/token/utils.ex
diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/o_status/o_status_controller.ex
index de1b0b3f0..668ae0ea4 100644
--- a/lib/pleroma/web/ostatus/ostatus_controller.ex
+++ b/lib/pleroma/web/o_status/o_status_controller.ex
@@ -5,28 +5,24 @@
defmodule Pleroma.Web.OStatus.OStatusController do
use Pleroma.Web, :controller
- alias Fallback.RedirectController
alias Pleroma.Activity
alias Pleroma.Object
- alias Pleroma.Plugs.RateLimiter
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPubController
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Endpoint
+ alias Pleroma.Web.Fallback.RedirectController
alias Pleroma.Web.Metadata.PlayerView
+ alias Pleroma.Web.Plugs.RateLimiter
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]
)
plug(
- Pleroma.Plugs.SetFormatPlug
+ Pleroma.Web.Plugs.SetFormatPlug
when action in [:object, :activity, :notice]
)
@@ -37,14 +33,12 @@ defmodule Pleroma.Web.OStatus.OStatusController do
ActivityPubController.call(conn, :object)
end
- def object(%{assigns: %{format: format}} = conn, _params) do
+ def object(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
- case format do
- _ -> redirect(conn, to: "/notice/#{activity.id}")
- end
+ redirect(conn, to: "/notice/#{activity.id}")
else
reason when reason in [{:public?, false}, {:activity, nil}] ->
{:error, :not_found}
@@ -59,13 +53,11 @@ defmodule Pleroma.Web.OStatus.OStatusController do
ActivityPubController.call(conn, :activity)
end
- def activity(%{assigns: %{format: format}} = conn, _params) do
+ def activity(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
- _ -> redirect(conn, to: "/notice/#{activity.id}")
- end
+ redirect(conn, to: "/notice/#{activity.id}")
else
reason when reason in [{:public?, false}, {:activity, nil}] ->
{:error, :not_found}
@@ -119,6 +111,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do
def notice_player(conn, %{"id" => id}) do
with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id_with_object(id),
true <- Visibility.is_public?(activity),
+ {_, true} <- {:visible?, Visibility.visible_for_user?(activity, _reading_user = nil)},
%Object{} = object <- Object.normalize(activity),
%{data: %{"attachment" => [%{"url" => [url | _]} | _]}} <- object,
true <- String.starts_with?(url["mediaType"], ["audio", "video"]) do
diff --git a/lib/pleroma/web/oauth/token/clean_worker.ex b/lib/pleroma/web/oauth/token/clean_worker.ex
deleted file mode 100644
index e3aa4eb7e..000000000
--- a/lib/pleroma/web/oauth/token/clean_worker.ex
+++ /dev/null
@@ -1,38 +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.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/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
index 563edded7..30cf83567 100644
--- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
@@ -8,16 +8,21 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
import Pleroma.Web.ControllerHelper,
only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2]
- alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
- alias Pleroma.Plugs.OAuthScopesPlug
- alias Pleroma.Plugs.RateLimiter
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.MastodonAPI.StatusView
+ alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+ alias Pleroma.Web.Plugs.RateLimiter
require Pleroma.Constants
plug(
+ Majic.Plug,
+ [pool: Pleroma.MajicPool] when action in [:update_avatar, :update_background, :update_banner]
+ )
+
+ plug(
OpenApiSpex.Plug.PutApiSpec,
[module: Pleroma.Web.ApiSpec] when action == :confirmation_resend
)
diff --git a/lib/pleroma/web/pleroma_api/controllers/backup_controller.ex b/lib/pleroma/web/pleroma_api/controllers/backup_controller.ex
new file mode 100644
index 000000000..dd0a2e22f
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/backup_controller.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.Web.PleromaAPI.BackupController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.User.Backup
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+
+ action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+ plug(OAuthScopesPlug, %{scopes: ["read:accounts"]} when action in [:index, :create])
+ plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaBackupOperation
+
+ def index(%{assigns: %{user: user}} = conn, _params) do
+ backups = Backup.list(user)
+ render(conn, "index.json", backups: backups)
+ end
+
+ def create(%{assigns: %{user: user}} = conn, _params) do
+ with {:ok, _} <- Backup.create(user) do
+ backups = Backup.list(user)
+ render(conn, "index.json", backups: backups)
+ end
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex
index e8a1746d4..bfc0a1f19 100644
--- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex
@@ -4,17 +4,18 @@
defmodule Pleroma.Web.PleromaAPI.ChatController do
use Pleroma.Web, :controller
+ import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
+
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
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
import Ecto.Query
@@ -47,7 +48,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
}) do
with %MessageReference{} = cm_ref <-
MessageReference.get_by_id(message_id),
- ^chat_id <- cm_ref.chat_id |> to_string(),
+ ^chat_id <- to_string(cm_ref.chat_id),
%Chat{user_id: ^user_id} <- Chat.get_by_id(chat_id),
{:ok, _} <- remove_or_delete(cm_ref, user) do
conn
@@ -68,38 +69,43 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
end
end
- defp remove_or_delete(cm_ref, _) do
- cm_ref
- |> MessageReference.delete()
- end
+ defp remove_or_delete(cm_ref, _), do: MessageReference.delete(cm_ref)
def post_chat_message(
- %{body_params: params, assigns: %{user: %{id: user_id} = user}} = conn,
- %{
- id: id
- }
+ %{body_params: params, assigns: %{user: user}} = conn,
+ %{id: id}
) do
- with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id),
+ with {:ok, chat} <- Chat.get_by_user_and_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]
+ media_id: params[:media_id],
+ idempotency_key: idempotency_key(conn)
),
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)
+ else
+ {:reject, message} ->
+ conn
+ |> put_status(:unprocessable_entity)
+ |> json(%{error: message})
+
+ {:error, message} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: message})
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(),
+ 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 <- to_string(cm_ref.chat_id),
%Chat{user_id: ^user_id} <- Chat.get_by_id(chat_id),
{:ok, cm_ref} <- MessageReference.mark_as_read(cm_ref) do
conn
@@ -109,69 +115,60 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
end
def mark_as_read(
- %{
- body_params: %{last_read_id: last_read_id},
- assigns: %{user: %{id: user_id}}
- } = conn,
+ %{body_params: %{last_read_id: last_read_id}, assigns: %{user: user}} = 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)
+ with {:ok, chat} <- Chat.get_by_user_and_id(user, id),
+ {_n, _} <- MessageReference.set_all_seen_for_chat(chat, last_read_id) do
+ render(conn, "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 =
+ def messages(%{assigns: %{user: user}} = conn, %{id: id} = params) do
+ with {:ok, chat} <- Chat.get_by_user_and_id(user, id) do
+ chat_message_refs =
chat
|> MessageReference.for_chat_query()
|> Pagination.fetch_paginated(params)
conn
+ |> add_link_headers(chat_message_refs)
|> put_view(MessageReferenceView)
- |> render("index.json", chat_message_references: cm_refs)
- else
- _ ->
- conn
- |> put_status(:not_found)
- |> json(%{error: "not found"})
+ |> render("index.json", chat_message_references: chat_message_refs)
end
end
- def index(%{assigns: %{user: %{id: user_id} = user}} = conn, _params) do
- blocked_ap_ids = User.blocked_users_ap_ids(user)
+ def index(%{assigns: %{user: %{id: user_id} = user}} = conn, params) do
+ exclude_users =
+ User.cached_blocked_users_ap_ids(user) ++
+ if params[:with_muted], do: [], else: User.cached_muted_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]
- )
+ user_id
+ |> Chat.for_user_query()
+ |> where([c], c.recipient not in ^exclude_users)
|> Repo.all()
- conn
- |> put_view(ChatView)
- |> render("index.json", chats: chats)
+ render(conn, "index.json", chats: chats)
end
- def create(%{assigns: %{user: user}} = conn, params) do
- with %User{ap_id: recipient} <- User.get_by_id(params[:id]),
+ def create(%{assigns: %{user: user}} = conn, %{id: id}) do
+ with %User{ap_id: recipient} <- User.get_cached_by_id(id),
{:ok, %Chat{} = chat} <- Chat.get_or_create(user.id, recipient) do
- conn
- |> put_view(ChatView)
- |> render("show.json", chat: chat)
+ render(conn, "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)
+ def show(%{assigns: %{user: user}} = conn, %{id: id}) do
+ with {:ok, chat} <- Chat.get_by_user_and_id(user, id) do
+ render(conn, "show.json", chat: chat)
+ end
+ end
+
+ defp idempotency_key(conn) do
+ case get_req_header(conn, "idempotency-key") do
+ [key] -> key
+ _ -> nil
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
index 3d007f324..df52b7566 100644
--- a/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex
@@ -8,9 +8,9 @@ defmodule Pleroma.Web.PleromaAPI.ConversationController do
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
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(:put_view, Pleroma.Web.MastodonAPI.ConversationView)
diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex
new file mode 100644
index 000000000..428c97de6
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex
@@ -0,0 +1,137 @@
+# 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.EmojiFileController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Emoji.Pack
+ alias Pleroma.Web.ApiSpec
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+ plug(
+ Pleroma.Web.Plugs.OAuthScopesPlug,
+ %{scopes: ["write"], admin: true}
+ when action in [
+ :create,
+ :update,
+ :delete
+ ]
+ )
+
+ defdelegate open_api_operation(action), to: ApiSpec.PleromaEmojiFileOperation
+
+ def create(%{body_params: params} = conn, %{name: pack_name}) do
+ filename = params[:filename] || get_filename(params[:file])
+ shortcode = params[:shortcode] || Path.basename(filename, Path.extname(filename))
+
+ with {:ok, pack} <- Pack.load_pack(pack_name),
+ {:ok, file} <- get_file(params[:file]),
+ {:ok, pack} <- Pack.add_file(pack, shortcode, filename, 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, :empty_values} ->
+ conn
+ |> put_status(:unprocessable_entity)
+ |> json(%{error: "pack name, shortcode or filename cannot be empty"})
+
+ {:error, _} = error ->
+ handle_error(conn, error, %{pack_name: pack_name})
+ end
+ end
+
+ def update(%{body_params: %{shortcode: shortcode} = params} = conn, %{name: pack_name}) do
+ new_shortcode = params[:new_shortcode]
+ new_filename = params[:new_filename]
+ force = params[:force]
+
+ with {:ok, pack} <- Pack.load_pack(pack_name),
+ {:ok, pack} <- Pack.update_file(pack, shortcode, new_shortcode, new_filename, force) do
+ json(conn, pack.files)
+ else
+ {: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, :empty_values} ->
+ conn
+ |> put_status(:unprocessable_entity)
+ |> json(%{error: "new_shortcode or new_filename cannot be empty"})
+
+ {:error, _} = error ->
+ handle_error(conn, error, %{pack_name: pack_name, code: shortcode})
+ end
+ end
+
+ def delete(conn, %{name: pack_name, shortcode: shortcode}) do
+ with {:ok, pack} <- Pack.load_pack(pack_name),
+ {:ok, pack} <- Pack.delete_file(pack, shortcode) do
+ json(conn, pack.files)
+ else
+ {:error, :empty_values} ->
+ conn
+ |> put_status(:unprocessable_entity)
+ |> json(%{error: "pack name or shortcode cannot be empty"})
+
+ {:error, _} = error ->
+ handle_error(conn, error, %{pack_name: pack_name, code: shortcode})
+ end
+ end
+
+ defp handle_error(conn, {:error, :doesnt_exist}, %{code: emoji_code}) do
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "Emoji \"#{emoji_code}\" does not exist"})
+ end
+
+ defp handle_error(conn, {:error, :not_found}, %{pack_name: pack_name}) do
+ conn
+ |> put_status(:not_found)
+ |> json(%{error: "pack \"#{pack_name}\" is not found"})
+ end
+
+ defp handle_error(conn, {:error, _}, _) do
+ render_error(
+ conn,
+ :internal_server_error,
+ "Unexpected error occurred while adding file to pack."
+ )
+ end
+
+ defp get_filename(%Plug.Upload{filename: filename}), do: filename
+ defp get_filename(url) when is_binary(url), do: Path.basename(url)
+
+ def get_file(%Plug.Upload{} = file), do: {:ok, file}
+
+ def get_file(url) when is_binary(url) do
+ with {:ok, %Tesla.Env{body: body, status: code, headers: headers}}
+ when code in 200..299 <- Pleroma.HTTP.get(url) do
+ path = Plug.Upload.random_file!("emoji")
+
+ content_type =
+ case List.keyfind(headers, "content-type", 0) do
+ {"content-type", value} -> value
+ nil -> nil
+ end
+
+ File.write(path, body)
+
+ {:ok,
+ %Plug.Upload{
+ filename: Path.basename(url),
+ path: path,
+ content_type: content_type
+ }}
+ 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
index 657f46324..a9accc5af 100644
--- a/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex
@@ -1,3 +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.PleromaAPI.EmojiPackController do
use Pleroma.Web, :controller
@@ -6,7 +10,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(
- Pleroma.Plugs.OAuthScopesPlug,
+ Pleroma.Web.Plugs.OAuthScopesPlug,
%{scopes: ["write"], admin: true}
when action in [
:import_from_filesystem,
@@ -14,20 +18,21 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
:download,
:create,
:update,
- :delete,
- :add_file,
- :update_file,
- :delete_file
+ :delete
]
)
- @skip_plugs [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]
- plug(:skip_plug, @skip_plugs when action in [:index, :show, :archive])
+ @skip_plugs [
+ Pleroma.Web.Plugs.OAuthScopesPlug,
+ Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug
+ ]
+ plug(:skip_plug, @skip_plugs when action in [:index, :archive, :show])
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaEmojiPackOperation
- def remote(conn, %{url: url}) do
- with {:ok, packs} <- Pack.list_remote(url) do
+ def remote(conn, params) do
+ with {:ok, packs} <-
+ Pack.list_remote(url: params.url, page_size: params.page_size, page: params.page) do
json(conn, packs)
else
{:error, :not_shareable} ->
@@ -184,105 +189,6 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
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)
@@ -298,7 +204,4 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
|> 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
index 19dcffdf3..dd9c746dc 100644
--- a/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex
@@ -7,9 +7,10 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do
alias Pleroma.Activity
alias Pleroma.Object
- alias Pleroma.Plugs.OAuthScopesPlug
+ alias Pleroma.User
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.StatusView
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action in [:create, :delete])
@@ -25,16 +26,46 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
def index(%{assigns: %{user: user}} = conn, %{id: activity_id} = params) do
- with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
+ 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)
+ reactions =
+ reactions
+ |> filter(params)
+ |> filter_allowed_users(user, Map.get(params, :with_muted, false))
+
render(conn, "index.json", emoji_reactions: reactions, user: user)
else
_e -> json(conn, [])
end
end
+ def filter_allowed_users(reactions, user, with_muted) do
+ exclude_ap_ids =
+ if is_nil(user) do
+ []
+ else
+ User.cached_blocked_users_ap_ids(user) ++
+ if not with_muted, do: User.cached_muted_users_ap_ids(user), else: []
+ end
+
+ filter_emoji = fn emoji, users ->
+ case Enum.reject(users, &(&1 in exclude_ap_ids)) do
+ [] -> nil
+ users -> {emoji, users}
+ end
+ end
+
+ reactions
+ |> Stream.map(fn
+ [emoji, users] when is_list(users) -> filter_emoji.(emoji, users)
+ {emoji, users} when is_list(users) -> filter_emoji.(emoji, users)
+ _ -> nil
+ end)
+ |> Stream.reject(&is_nil/1)
+ end
+
defp filter(reactions, %{emoji: emoji}) when is_binary(emoji) do
Enum.filter(reactions, fn [e, _] -> e == emoji end)
end
diff --git a/lib/pleroma/web/pleroma_api/controllers/instances_controller.ex b/lib/pleroma/web/pleroma_api/controllers/instances_controller.ex
new file mode 100644
index 000000000..9e97480df
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/instances_controller.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.Web.PleromaAPI.InstancesController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Instances
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaInstancesOperation
+
+ def show(conn, _params) do
+ unreachable =
+ Instances.get_consistently_unreachable()
+ |> Map.new(fn {host, date} -> {host, to_string(date)} end)
+
+ json(conn, %{"unreachable" => unreachable})
+ 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 df6c50ca5..15210f1e6 100644
--- a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex
@@ -5,10 +5,11 @@
defmodule Pleroma.Web.PleromaAPI.MascotController do
use Pleroma.Web, :controller
- alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+ plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:update])
plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(OAuthScopesPlug, %{scopes: ["read:accounts"]} when action == :show)
plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action != :show)
@@ -22,14 +23,15 @@ defmodule Pleroma.Web.PleromaAPI.MascotController do
@doc "PUT /api/v1/pleroma/mascot"
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
+ with {:content_type, "image" <> _} <- {:content_type, file.content_type},
+ {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)) do
+ attachment = render_attachment(object)
{:ok, _user} = User.mascot_update(user, attachment)
json(conn, attachment)
else
- %{type: _} -> render_error(conn, :unsupported_media_type, "mascots can only be images")
+ {:content_type, _} ->
+ render_error(conn, :unsupported_media_type, "mascots can only be images")
end
end
diff --git a/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex b/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex
index 3ed8bd294..fa32aaa84 100644
--- a/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex
@@ -6,10 +6,14 @@ 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(
+ Pleroma.Web.Plugs.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
diff --git a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex
index e9a4fba92..632d65434 100644
--- a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex
@@ -7,10 +7,10 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleController do
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.Plugs.OAuthScopesPlug
plug(Pleroma.Web.ApiSpec.CastAndValidate)
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
index b86791d09..eba452300 100644
--- a/lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex
@@ -10,8 +10,8 @@ defmodule Pleroma.Web.PleromaAPI.TwoFactorAuthenticationController do
alias Pleroma.MFA
alias Pleroma.MFA.TOTP
- alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Web.CommonAPI.Utils
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
plug(OAuthScopesPlug, %{scopes: ["read:security"]} when action in [:settings])
diff --git a/lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex b/lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex
new file mode 100644
index 000000000..7f089af1c
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/user_import_controller.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.PleromaAPI.UserImportController do
+ use Pleroma.Web, :controller
+
+ require Logger
+
+ alias Pleroma.User
+ alias Pleroma.Web.ApiSpec
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+
+ plug(OAuthScopesPlug, %{scopes: ["follow", "write:follows"]} when action == :follow)
+ plug(OAuthScopesPlug, %{scopes: ["follow", "write:blocks"]} when action == :blocks)
+ plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action == :mutes)
+
+ plug(OpenApiSpex.Plug.CastAndValidate)
+ defdelegate open_api_operation(action), to: ApiSpec.UserImportOperation
+
+ def follow(%{body_params: %{list: %Plug.Upload{path: path}}} = conn, _) do
+ follow(%Plug.Conn{conn | body_params: %{list: File.read!(path)}}, %{})
+ end
+
+ def follow(%{assigns: %{user: follower}, body_params: %{list: list}} = conn, _) do
+ identifiers =
+ list
+ |> String.split("\n")
+ |> Enum.map(&(&1 |> String.split(",") |> List.first()))
+ |> List.delete("Account address")
+ |> Enum.map(&(&1 |> String.trim() |> String.trim_leading("@")))
+ |> Enum.reject(&(&1 == ""))
+
+ User.Import.follow_import(follower, identifiers)
+ json(conn, "job started")
+ end
+
+ def blocks(%{body_params: %{list: %Plug.Upload{path: path}}} = conn, _) do
+ blocks(%Plug.Conn{conn | body_params: %{list: File.read!(path)}}, %{})
+ end
+
+ def blocks(%{assigns: %{user: blocker}, body_params: %{list: list}} = conn, _) do
+ User.Import.blocks_import(blocker, prepare_user_identifiers(list))
+ json(conn, "job started")
+ end
+
+ def mutes(%{body_params: %{list: %Plug.Upload{path: path}}} = conn, _) do
+ mutes(%Plug.Conn{conn | body_params: %{list: File.read!(path)}}, %{})
+ end
+
+ def mutes(%{assigns: %{user: user}, body_params: %{list: list}} = conn, _) do
+ User.Import.mutes_import(user, prepare_user_identifiers(list))
+ json(conn, "job started")
+ end
+
+ defp prepare_user_identifiers(list) do
+ list
+ |> String.split()
+ |> Enum.map(&String.trim_leading(&1, "@"))
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/views/backup_view.ex b/lib/pleroma/web/pleroma_api/views/backup_view.ex
new file mode 100644
index 000000000..af75876aa
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/views/backup_view.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.Web.PleromaAPI.BackupView do
+ use Pleroma.Web, :view
+
+ alias Pleroma.User.Backup
+ alias Pleroma.Web.CommonAPI.Utils
+
+ def render("show.json", %{backup: %Backup{} = backup}) do
+ %{
+ content_type: backup.content_type,
+ url: download_url(backup),
+ file_size: backup.file_size,
+ processed: backup.processed,
+ inserted_at: Utils.to_masto_date(backup.inserted_at)
+ }
+ end
+
+ def render("index.json", %{backups: backups}) do
+ render_many(backups, __MODULE__, "show.json")
+ end
+
+ def download_url(%Backup{file_name: file_name}) do
+ Pleroma.Web.Endpoint.url() <> "/media/backups/" <> file_name
+ 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
index f2112a86e..c058fb340 100644
--- a/lib/pleroma/web/pleroma_api/views/chat/message_reference_view.ex
+++ b/lib/pleroma/web/pleroma_api/views/chat/message_reference_view.ex
@@ -5,6 +5,7 @@
defmodule Pleroma.Web.PleromaAPI.Chat.MessageReferenceView do
use Pleroma.Web, :view
+ alias Pleroma.Maps
alias Pleroma.User
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.StatusView
@@ -14,7 +15,7 @@ defmodule Pleroma.Web.PleromaAPI.Chat.MessageReferenceView do
%{
chat_message_reference: %{
id: id,
- object: %{data: chat_message},
+ object: %{data: chat_message} = object,
chat_id: chat_id,
unread: unread
}
@@ -30,8 +31,14 @@ defmodule Pleroma.Web.PleromaAPI.Chat.MessageReferenceView do
attachment:
chat_message["attachment"] &&
StatusView.render("attachment.json", attachment: chat_message["attachment"]),
- unread: unread
+ unread: unread,
+ card:
+ StatusView.render(
+ "card.json",
+ Pleroma.Web.RichMedia.Helpers.fetch_data_for_object(object)
+ )
}
+ |> put_idempotency_key()
end
def render("index.json", opts) do
@@ -42,4 +49,13 @@ defmodule Pleroma.Web.PleromaAPI.Chat.MessageReferenceView do
Map.put(opts, :as, :chat_message_reference)
)
end
+
+ defp put_idempotency_key(data) do
+ with {:ok, idempotency_key} <- Cachex.get(:chat_message_id_idempotency_key_cache, data.id) do
+ data
+ |> Maps.put_if_present(:idempotency_key, idempotency_key)
+ else
+ _ -> data
+ 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
index e0f98b50a..110e8a041 100644
--- a/lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex
+++ b/lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex
@@ -11,7 +11,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionView do
render_many(emoji_reactions, __MODULE__, "show.json", opts)
end
- def render("show.json", %{emoji_reaction: [emoji, user_ap_ids], user: user}) do
+ def render("show.json", %{emoji_reaction: {emoji, user_ap_ids}, user: user}) do
users = fetch_users(user_ap_ids)
%{
diff --git a/lib/pleroma/web/pleroma_api/views/scrobble_view.ex b/lib/pleroma/web/pleroma_api/views/scrobble_view.ex
index bbff93abe..95bd4c368 100644
--- a/lib/pleroma/web/pleroma_api/views/scrobble_view.ex
+++ b/lib/pleroma/web/pleroma_api/views/scrobble_view.ex
@@ -10,14 +10,14 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleView do
alias Pleroma.Activity
alias Pleroma.HTML
alias Pleroma.Object
+ alias Pleroma.Web.CommonAPI
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"])
+ user = CommonAPI.get_user(activity.data["actor"])
created_at = Utils.to_masto_date(activity.data["published"])
%{
diff --git a/lib/pleroma/web/plug.ex b/lib/pleroma/web/plug.ex
new file mode 100644
index 000000000..840b35072
--- /dev/null
+++ b/lib/pleroma/web/plug.ex
@@ -0,0 +1,8 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.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
diff --git a/lib/pleroma/plugs/admin_secret_authentication_plug.ex b/lib/pleroma/web/plugs/admin_secret_authentication_plug.ex
index 2e54df47a..d7d4e4092 100644
--- a/lib/pleroma/plugs/admin_secret_authentication_plug.ex
+++ b/lib/pleroma/web/plugs/admin_secret_authentication_plug.ex
@@ -2,12 +2,12 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Plugs.AdminSecretAuthenticationPlug do
+defmodule Pleroma.Web.Plugs.AdminSecretAuthenticationPlug do
import Plug.Conn
- alias Pleroma.Plugs.OAuthScopesPlug
- alias Pleroma.Plugs.RateLimiter
alias Pleroma.User
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+ alias Pleroma.Web.Plugs.RateLimiter
def init(options) do
options
diff --git a/lib/pleroma/plugs/authentication_plug.ex b/lib/pleroma/web/plugs/authentication_plug.ex
index 057ea42f1..e2a8b1b69 100644
--- a/lib/pleroma/plugs/authentication_plug.ex
+++ b/lib/pleroma/web/plugs/authentication_plug.ex
@@ -2,8 +2,7 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Plugs.AuthenticationPlug do
- alias Pleroma.Plugs.OAuthScopesPlug
+defmodule Pleroma.Web.Plugs.AuthenticationPlug do
alias Pleroma.User
import Plug.Conn
@@ -65,7 +64,7 @@ defmodule Pleroma.Plugs.AuthenticationPlug do
conn
|> assign(:user, auth_user)
- |> OAuthScopesPlug.skip_plug()
+ |> Pleroma.Web.Plugs.OAuthScopesPlug.skip_plug()
else
conn
end
diff --git a/lib/pleroma/plugs/basic_auth_decoder_plug.ex b/lib/pleroma/web/plugs/basic_auth_decoder_plug.ex
index af7ecb0d8..4dadfb000 100644
--- a/lib/pleroma/plugs/basic_auth_decoder_plug.ex
+++ b/lib/pleroma/web/plugs/basic_auth_decoder_plug.ex
@@ -2,7 +2,7 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Plugs.BasicAuthDecoderPlug do
+defmodule Pleroma.Web.Plugs.BasicAuthDecoderPlug do
import Plug.Conn
def init(options) do
diff --git a/lib/pleroma/plugs/cache.ex b/lib/pleroma/web/plugs/cache.ex
index f65c2a189..6de01804a 100644
--- a/lib/pleroma/plugs/cache.ex
+++ b/lib/pleroma/web/plugs/cache.ex
@@ -2,19 +2,19 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Plugs.Cache do
+defmodule Pleroma.Web.Plugs.Cache do
@moduledoc """
Caches successful GET responses.
To enable the cache add the plug to a router pipeline or controller:
- plug(Pleroma.Plugs.Cache)
+ plug(Pleroma.Web.Plugs.Cache)
## Configuration
To configure the plug you need to pass settings as the second argument to the `plug/2` macro:
- plug(Pleroma.Plugs.Cache, [ttl: nil, query_params: true])
+ plug(Pleroma.Web.Plugs.Cache, [ttl: nil, query_params: true])
Available options:
diff --git a/lib/pleroma/plugs/digest.ex b/lib/pleroma/web/plugs/digest_plug.ex
index b521b3073..b521b3073 100644
--- a/lib/pleroma/plugs/digest.ex
+++ b/lib/pleroma/web/plugs/digest_plug.ex
diff --git a/lib/pleroma/plugs/ensure_authenticated_plug.ex b/lib/pleroma/web/plugs/ensure_authenticated_plug.ex
index 3fe550806..ea2af6881 100644
--- a/lib/pleroma/plugs/ensure_authenticated_plug.ex
+++ b/lib/pleroma/web/plugs/ensure_authenticated_plug.ex
@@ -2,7 +2,7 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Plugs.EnsureAuthenticatedPlug do
+defmodule Pleroma.Web.Plugs.EnsureAuthenticatedPlug do
import Plug.Conn
import Pleroma.Web.TranslationHelpers
diff --git a/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex b/lib/pleroma/web/plugs/ensure_public_or_authenticated_plug.ex
index 7265bb87a..3bebdac6d 100644
--- a/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex
+++ b/lib/pleroma/web/plugs/ensure_public_or_authenticated_plug.ex
@@ -2,7 +2,7 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug do
+defmodule Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug do
import Pleroma.Web.TranslationHelpers
import Plug.Conn
diff --git a/lib/pleroma/plugs/ensure_user_key_plug.ex b/lib/pleroma/web/plugs/ensure_user_key_plug.ex
index 9795cdbde..70d3091f0 100644
--- a/lib/pleroma/plugs/ensure_user_key_plug.ex
+++ b/lib/pleroma/web/plugs/ensure_user_key_plug.ex
@@ -2,7 +2,7 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Plugs.EnsureUserKeyPlug do
+defmodule Pleroma.Web.Plugs.EnsureUserKeyPlug do
import Plug.Conn
def init(opts) do
diff --git a/lib/pleroma/plugs/expect_authenticated_check_plug.ex b/lib/pleroma/web/plugs/expect_authenticated_check_plug.ex
index 66b8d5de5..0925ded4d 100644
--- a/lib/pleroma/plugs/expect_authenticated_check_plug.ex
+++ b/lib/pleroma/web/plugs/expect_authenticated_check_plug.ex
@@ -2,9 +2,9 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Plugs.ExpectAuthenticatedCheckPlug do
+defmodule Pleroma.Web.Plugs.ExpectAuthenticatedCheckPlug do
@moduledoc """
- Marks `Pleroma.Plugs.EnsureAuthenticatedPlug` as expected to be executed later in plug chain.
+ Marks `Pleroma.Web.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`).
"""
diff --git a/lib/pleroma/plugs/expect_public_or_authenticated_check_plug.ex b/lib/pleroma/web/plugs/expect_public_or_authenticated_check_plug.ex
index ba0ef76bd..ace512a78 100644
--- a/lib/pleroma/plugs/expect_public_or_authenticated_check_plug.ex
+++ b/lib/pleroma/web/plugs/expect_public_or_authenticated_check_plug.ex
@@ -2,9 +2,9 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug do
+defmodule Pleroma.Web.Plugs.ExpectPublicOrAuthenticatedCheckPlug do
@moduledoc """
- Marks `Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug` as expected to be executed later in plug
+ Marks `Pleroma.Web.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`).
diff --git a/lib/pleroma/plugs/federating_plug.ex b/lib/pleroma/web/plugs/federating_plug.ex
index 09038f3c6..3c90a7644 100644
--- a/lib/pleroma/plugs/federating_plug.ex
+++ b/lib/pleroma/web/plugs/federating_plug.ex
@@ -2,7 +2,7 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Web.FederatingPlug do
+defmodule Pleroma.Web.Plugs.FederatingPlug do
import Plug.Conn
def init(options) do
diff --git a/lib/pleroma/web/plugs/frontend_static.ex b/lib/pleroma/web/plugs/frontend_static.ex
new file mode 100644
index 000000000..1b0b36813
--- /dev/null
+++ b/lib/pleroma/web/plugs/frontend_static.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.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
+ with false <- invalid_path?(conn.path_info),
+ frontend_type <- Map.get(opts, :frontend_type, :primary),
+ path when not is_nil(path) <- file_path("", frontend_type) do
+ call_static(conn, opts, path)
+ else
+ _ ->
+ conn
+ end
+ end
+
+ defp invalid_path?(list) do
+ invalid_path?(list, :binary.compile_pattern(["/", "\\", ":", "\0"]))
+ end
+
+ defp invalid_path?([h | _], _match) when h in [".", "..", ""], do: true
+ defp invalid_path?([h | t], match), do: String.contains?(h, match) or invalid_path?(t)
+ defp invalid_path?([], _match), do: false
+
+ defp call_static(conn, opts, from) do
+ opts = Map.put(opts, :from, from)
+ Plug.Static.call(conn, opts)
+ end
+end
diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/web/plugs/http_security_plug.ex
index c363b193b..45aaf188e 100644
--- a/lib/pleroma/plugs/http_security_plug.ex
+++ b/lib/pleroma/web/plugs/http_security_plug.ex
@@ -2,7 +2,7 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Plugs.HTTPSecurityPlug do
+defmodule Pleroma.Web.Plugs.HTTPSecurityPlug do
alias Pleroma.Config
import Plug.Conn
diff --git a/lib/pleroma/plugs/http_signature.ex b/lib/pleroma/web/plugs/http_signature_plug.ex
index 036e2a773..036e2a773 100644
--- a/lib/pleroma/plugs/http_signature.ex
+++ b/lib/pleroma/web/plugs/http_signature_plug.ex
diff --git a/lib/pleroma/plugs/idempotency_plug.ex b/lib/pleroma/web/plugs/idempotency_plug.ex
index f41397075..254a790b0 100644
--- a/lib/pleroma/plugs/idempotency_plug.ex
+++ b/lib/pleroma/web/plugs/idempotency_plug.ex
@@ -2,7 +2,7 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Plugs.IdempotencyPlug do
+defmodule Pleroma.Web.Plugs.IdempotencyPlug do
import Phoenix.Controller, only: [json: 2]
import Plug.Conn
diff --git a/lib/pleroma/plugs/instance_static.ex b/lib/pleroma/web/plugs/instance_static.ex
index 7516f75c3..54b9175df 100644
--- a/lib/pleroma/plugs/instance_static.ex
+++ b/lib/pleroma/web/plugs/instance_static.ex
@@ -2,7 +2,7 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Plugs.InstanceStatic do
+defmodule Pleroma.Web.Plugs.InstanceStatic do
require Pleroma.Constants
@moduledoc """
@@ -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.Web.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/legacy_authentication_plug.ex b/lib/pleroma/web/plugs/legacy_authentication_plug.ex
index d346e01a6..2a54d0b59 100644
--- a/lib/pleroma/plugs/legacy_authentication_plug.ex
+++ b/lib/pleroma/web/plugs/legacy_authentication_plug.ex
@@ -2,10 +2,9 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Plugs.LegacyAuthenticationPlug do
+defmodule Pleroma.Web.Plugs.LegacyAuthenticationPlug do
import Plug.Conn
- alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
def init(options) do
@@ -29,7 +28,7 @@ defmodule Pleroma.Plugs.LegacyAuthenticationPlug do
conn
|> assign(:auth_user, user)
|> assign(:user, user)
- |> OAuthScopesPlug.skip_plug()
+ |> Pleroma.Web.Plugs.OAuthScopesPlug.skip_plug()
else
_ ->
conn
diff --git a/lib/pleroma/plugs/mapped_signature_to_identity_plug.ex b/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex
index f44d4dee5..f44d4dee5 100644
--- a/lib/pleroma/plugs/mapped_signature_to_identity_plug.ex
+++ b/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex
diff --git a/lib/pleroma/plugs/oauth_plug.ex b/lib/pleroma/web/plugs/o_auth_plug.ex
index 6fa71ef47..c7b58d90f 100644
--- a/lib/pleroma/plugs/oauth_plug.ex
+++ b/lib/pleroma/web/plugs/o_auth_plug.ex
@@ -2,7 +2,7 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Plugs.OAuthPlug do
+defmodule Pleroma.Web.Plugs.OAuthPlug do
import Plug.Conn
import Ecto.Query
diff --git a/lib/pleroma/plugs/oauth_scopes_plug.ex b/lib/pleroma/web/plugs/o_auth_scopes_plug.ex
index efc25b79f..cfc30837c 100644
--- a/lib/pleroma/plugs/oauth_scopes_plug.ex
+++ b/lib/pleroma/web/plugs/o_auth_scopes_plug.ex
@@ -2,7 +2,7 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Plugs.OAuthScopesPlug do
+defmodule Pleroma.Web.Plugs.OAuthScopesPlug do
import Plug.Conn
import Pleroma.Web.Gettext
@@ -53,7 +53,7 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do
|> assign(:token, nil)
end
- @doc "Filters descendants of supported scopes"
+ @doc "Keeps those of `scopes` which are descendants of `supported_scopes`"
def filter_descendants(scopes, supported_scopes) do
Enum.filter(
scopes,
diff --git a/lib/pleroma/plugs/plug_helper.ex b/lib/pleroma/web/plugs/plug_helper.ex
index 9c67be8ef..b314e7596 100644
--- a/lib/pleroma/plugs/plug_helper.ex
+++ b/lib/pleroma/web/plugs/plug_helper.ex
@@ -2,7 +2,7 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Plugs.PlugHelper do
+defmodule Pleroma.Web.Plugs.PlugHelper do
@moduledoc "Pleroma Plug helper"
@called_plugs_list_id :called_plugs
diff --git a/lib/pleroma/plugs/rate_limiter/rate_limiter.ex b/lib/pleroma/web/plugs/rate_limiter.ex
index c51e2c634..a589610d1 100644
--- a/lib/pleroma/plugs/rate_limiter/rate_limiter.ex
+++ b/lib/pleroma/web/plugs/rate_limiter.ex
@@ -2,7 +2,7 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Plugs.RateLimiter do
+defmodule Pleroma.Web.Plugs.RateLimiter do
@moduledoc """
## Configuration
@@ -35,8 +35,8 @@ defmodule Pleroma.Plugs.RateLimiter do
AllowedSyntax:
- plug(Pleroma.Plugs.RateLimiter, name: :limiter_name)
- plug(Pleroma.Plugs.RateLimiter, options) # :name is a required option
+ plug(Pleroma.Web.Plugs.RateLimiter, name: :limiter_name)
+ plug(Pleroma.Web.Plugs.RateLimiter, options) # :name is a required option
Allowed options:
@@ -46,11 +46,11 @@ defmodule Pleroma.Plugs.RateLimiter do
Inside a controller:
- plug(Pleroma.Plugs.RateLimiter, [name: :one] when action == :one)
- plug(Pleroma.Plugs.RateLimiter, [name: :two] when action in [:two, :three])
+ plug(Pleroma.Web.Plugs.RateLimiter, [name: :one] when action == :one)
+ plug(Pleroma.Web.Plugs.RateLimiter, [name: :two] when action in [:two, :three])
plug(
- Pleroma.Plugs.RateLimiter,
+ Pleroma.Web.Plugs.RateLimiter,
[name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]]
when action in ~w(fav_status unfav_status)a
)
@@ -59,7 +59,7 @@ defmodule Pleroma.Plugs.RateLimiter do
pipeline :api do
...
- plug(Pleroma.Plugs.RateLimiter, name: :one)
+ plug(Pleroma.Web.Plugs.RateLimiter, name: :one)
...
end
"""
@@ -67,8 +67,8 @@ defmodule Pleroma.Plugs.RateLimiter do
import Plug.Conn
alias Pleroma.Config
- alias Pleroma.Plugs.RateLimiter.LimiterSupervisor
alias Pleroma.User
+ alias Pleroma.Web.Plugs.RateLimiter.LimiterSupervisor
require Logger
diff --git a/lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex b/lib/pleroma/web/plugs/rate_limiter/limiter_supervisor.ex
index 884268d96..5642bb205 100644
--- a/lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex
+++ b/lib/pleroma/web/plugs/rate_limiter/limiter_supervisor.ex
@@ -1,4 +1,8 @@
-defmodule Pleroma.Plugs.RateLimiter.LimiterSupervisor do
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.RateLimiter.LimiterSupervisor do
use DynamicSupervisor
import Cachex.Spec
diff --git a/lib/pleroma/plugs/rate_limiter/supervisor.ex b/lib/pleroma/web/plugs/rate_limiter/supervisor.ex
index 9672f7876..a1c84063d 100644
--- a/lib/pleroma/plugs/rate_limiter/supervisor.ex
+++ b/lib/pleroma/web/plugs/rate_limiter/supervisor.ex
@@ -1,4 +1,8 @@
-defmodule Pleroma.Plugs.RateLimiter.Supervisor do
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.RateLimiter.Supervisor do
use Supervisor
def start_link(opts) do
@@ -7,7 +11,7 @@ defmodule Pleroma.Plugs.RateLimiter.Supervisor do
def init(_args) do
children = [
- Pleroma.Plugs.RateLimiter.LimiterSupervisor
+ Pleroma.Web.Plugs.RateLimiter.LimiterSupervisor
]
opts = [strategy: :one_for_one, name: Pleroma.Web.Streamer.Supervisor]
diff --git a/lib/pleroma/web/plugs/remote_ip.ex b/lib/pleroma/web/plugs/remote_ip.ex
new file mode 100644
index 000000000..401e2cbfa
--- /dev/null
+++ b/lib/pleroma/web/plugs/remote_ip.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.Plugs.RemoteIp do
+ @moduledoc """
+ This is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration.
+ """
+
+ alias Pleroma.Config
+ import Plug.Conn
+
+ @behaviour Plug
+
+ def init(_), do: nil
+
+ def call(%{remote_ip: original_remote_ip} = conn, _) do
+ if Config.get([__MODULE__, :enabled]) do
+ %{remote_ip: new_remote_ip} = conn = RemoteIp.call(conn, remote_ip_opts())
+ assign(conn, :remote_ip_found, original_remote_ip != new_remote_ip)
+ else
+ conn
+ end
+ end
+
+ defp remote_ip_opts do
+ headers = Config.get([__MODULE__, :headers], []) |> MapSet.new()
+ reserved = Config.get([__MODULE__, :reserved], [])
+
+ proxies =
+ Config.get([__MODULE__, :proxies], [])
+ |> Enum.concat(reserved)
+ |> Enum.map(&maybe_add_cidr/1)
+
+ {headers, proxies}
+ end
+
+ defp maybe_add_cidr(proxy) when is_binary(proxy) do
+ proxy =
+ cond do
+ "/" in String.codepoints(proxy) -> proxy
+ InetCidr.v4?(InetCidr.parse_address!(proxy)) -> proxy <> "/32"
+ InetCidr.v6?(InetCidr.parse_address!(proxy)) -> proxy <> "/128"
+ end
+
+ InetCidr.parse(proxy, true)
+ end
+end
diff --git a/lib/pleroma/plugs/session_authentication_plug.ex b/lib/pleroma/web/plugs/session_authentication_plug.ex
index 0f83a5e53..6e176d553 100644
--- a/lib/pleroma/plugs/session_authentication_plug.ex
+++ b/lib/pleroma/web/plugs/session_authentication_plug.ex
@@ -2,7 +2,7 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Plugs.SessionAuthenticationPlug do
+defmodule Pleroma.Web.Plugs.SessionAuthenticationPlug do
import Plug.Conn
def init(options) do
diff --git a/lib/pleroma/plugs/set_format_plug.ex b/lib/pleroma/web/plugs/set_format_plug.ex
index c03fcb28d..c16d2f81d 100644
--- a/lib/pleroma/plugs/set_format_plug.ex
+++ b/lib/pleroma/web/plugs/set_format_plug.ex
@@ -2,7 +2,7 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Plugs.SetFormatPlug do
+defmodule Pleroma.Web.Plugs.SetFormatPlug do
import Plug.Conn, only: [assign: 3, fetch_query_params: 1]
def init(_), do: nil
diff --git a/lib/pleroma/plugs/set_locale_plug.ex b/lib/pleroma/web/plugs/set_locale_plug.ex
index 9a21d0a9d..d9d24b93f 100644
--- a/lib/pleroma/plugs/set_locale_plug.ex
+++ b/lib/pleroma/web/plugs/set_locale_plug.ex
@@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
# NOTE: this module is based on https://github.com/smeevil/set_locale
-defmodule Pleroma.Plugs.SetLocalePlug do
+defmodule Pleroma.Web.Plugs.SetLocalePlug do
import Plug.Conn, only: [get_req_header: 2, assign: 3]
def init(_), do: nil
diff --git a/lib/pleroma/plugs/set_user_session_id_plug.ex b/lib/pleroma/web/plugs/set_user_session_id_plug.ex
index 730c4ac74..e520159e4 100644
--- a/lib/pleroma/plugs/set_user_session_id_plug.ex
+++ b/lib/pleroma/web/plugs/set_user_session_id_plug.ex
@@ -2,7 +2,7 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Plugs.SetUserSessionIdPlug do
+defmodule Pleroma.Web.Plugs.SetUserSessionIdPlug do
import Plug.Conn
alias Pleroma.User
diff --git a/lib/pleroma/plugs/static_fe_plug.ex b/lib/pleroma/web/plugs/static_fe_plug.ex
index 143665c71..658a1052e 100644
--- a/lib/pleroma/plugs/static_fe_plug.ex
+++ b/lib/pleroma/web/plugs/static_fe_plug.ex
@@ -2,7 +2,7 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Plugs.StaticFEPlug do
+defmodule Pleroma.Web.Plugs.StaticFEPlug do
import Plug.Conn
alias Pleroma.Web.StaticFE.StaticFEController
diff --git a/lib/pleroma/plugs/trailing_format_plug.ex b/lib/pleroma/web/plugs/trailing_format_plug.ex
index 8b4d5fc9f..e3f57c14a 100644
--- a/lib/pleroma/plugs/trailing_format_plug.ex
+++ b/lib/pleroma/web/plugs/trailing_format_plug.ex
@@ -2,7 +2,7 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Plugs.TrailingFormatPlug do
+defmodule Pleroma.Web.Plugs.TrailingFormatPlug do
@moduledoc "Calls TrailingFormatPlug for specific paths. Ideally we would just do this in the router, but TrailingFormatPlug needs to be called before Plug.Parsers."
@behaviour Plug
diff --git a/lib/pleroma/plugs/uploaded_media.ex b/lib/pleroma/web/plugs/uploaded_media.ex
index 40984cfc0..402a8bb34 100644
--- a/lib/pleroma/plugs/uploaded_media.ex
+++ b/lib/pleroma/web/plugs/uploaded_media.ex
@@ -2,7 +2,7 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Plugs.UploadedMedia do
+defmodule Pleroma.Web.Plugs.UploadedMedia do
@moduledoc """
"""
diff --git a/lib/pleroma/plugs/user_enabled_plug.ex b/lib/pleroma/web/plugs/user_enabled_plug.ex
index 23e800a74..fa28ee48b 100644
--- a/lib/pleroma/plugs/user_enabled_plug.ex
+++ b/lib/pleroma/web/plugs/user_enabled_plug.ex
@@ -2,7 +2,7 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Plugs.UserEnabledPlug do
+defmodule Pleroma.Web.Plugs.UserEnabledPlug do
import Plug.Conn
alias Pleroma.User
diff --git a/lib/pleroma/plugs/user_fetcher_plug.ex b/lib/pleroma/web/plugs/user_fetcher_plug.ex
index 235c77d85..4039600da 100644
--- a/lib/pleroma/plugs/user_fetcher_plug.ex
+++ b/lib/pleroma/web/plugs/user_fetcher_plug.ex
@@ -2,7 +2,7 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Plugs.UserFetcherPlug do
+defmodule Pleroma.Web.Plugs.UserFetcherPlug do
alias Pleroma.User
import Plug.Conn
diff --git a/lib/pleroma/plugs/user_is_admin_plug.ex b/lib/pleroma/web/plugs/user_is_admin_plug.ex
index 488a61d1d..531c965f0 100644
--- a/lib/pleroma/plugs/user_is_admin_plug.ex
+++ b/lib/pleroma/web/plugs/user_is_admin_plug.ex
@@ -2,7 +2,7 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Plugs.UserIsAdminPlug do
+defmodule Pleroma.Web.Plugs.UserIsAdminPlug do
import Pleroma.Web.TranslationHelpers
import Plug.Conn
diff --git a/lib/pleroma/web/preload/instance.ex b/lib/pleroma/web/preload/providers/instance.ex
index 50d1f3382..a549bb1eb 100644
--- a/lib/pleroma/web/preload/instance.ex
+++ b/lib/pleroma/web/preload/providers/instance.ex
@@ -3,15 +3,17 @@
# 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.Plugs.InstanceStatic
alias Pleroma.Web.Preload.Providers.Provider
+ alias Pleroma.Web.TwitterAPI.UtilView
@behaviour Provider
@instance_url "/api/v1/instance"
@panel_url "/instance/panel.html"
@nodeinfo_url "/nodeinfo/2.0.json"
+ @fe_config_url "/api/pleroma/frontend_configurations"
@impl Provider
def generate_terms(_params) do
@@ -19,6 +21,7 @@ defmodule Pleroma.Web.Preload.Providers.Instance do
|> build_info_tag()
|> build_panel_tag()
|> build_nodeinfo_tag()
+ |> build_fe_config_tag()
end
defp build_info_tag(acc) do
@@ -47,4 +50,10 @@ defmodule Pleroma.Web.Preload.Providers.Instance do
Map.put(acc, @nodeinfo_url, nodeinfo_data)
end
end
+
+ defp build_fe_config_tag(acc) do
+ fe_data = UtilView.render("frontend_configurations.json", %{})
+
+ Map.put(acc, @fe_config_url, fe_data)
+ end
end
diff --git a/lib/pleroma/web/preload/provider.ex b/lib/pleroma/web/preload/providers/provider.ex
index 7ef595a34..7ef595a34 100644
--- a/lib/pleroma/web/preload/provider.ex
+++ b/lib/pleroma/web/preload/providers/provider.ex
diff --git a/lib/pleroma/web/preload/timelines.ex b/lib/pleroma/web/preload/providers/timelines.ex
index 57de04051..b279a865d 100644
--- a/lib/pleroma/web/preload/timelines.ex
+++ b/lib/pleroma/web/preload/providers/timelines.ex
@@ -16,7 +16,7 @@ defmodule Pleroma.Web.Preload.Providers.Timelines do
end
def build_public_tag(acc, params) do
- if Pleroma.Config.get([:restrict_unauthenticated, :timelines, :federated], true) do
+ if Pleroma.Config.restrict_unauthenticated_access?(:timelines, :federated) do
acc
else
Map.put(acc, @public_url, public_timeline(params))
diff --git a/lib/pleroma/web/preload/user.ex b/lib/pleroma/web/preload/providers/user.ex
index b3d2e9b8d..b3d2e9b8d 100644
--- a/lib/pleroma/web/preload/user.ex
+++ b/lib/pleroma/web/preload/providers/user.ex
diff --git a/lib/pleroma/web/push/push.ex b/lib/pleroma/web/push.ex
index b80a6438d..b80a6438d 100644
--- a/lib/pleroma/web/push/push.ex
+++ b/lib/pleroma/web/push.ex
diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex
index 16368485e..82152dffa 100644
--- a/lib/pleroma/web/push/impl.ex
+++ b/lib/pleroma/web/push/impl.ex
@@ -16,10 +16,10 @@ defmodule Pleroma.Web.Push.Impl do
require Logger
import Ecto.Query
- @types ["Create", "Follow", "Announce", "Like", "Move"]
+ @types ["Create", "Follow", "Announce", "Like", "Move", "EmojiReact"]
@doc "Performs sending notifications for user subscriptions"
- @spec perform(Notification.t()) :: list(any) | :error
+ @spec perform(Notification.t()) :: list(any) | :error | {:error, :unknown_type}
def perform(
%{
activity: %{data: %{"type" => activity_type}} = activity,
@@ -64,20 +64,20 @@ defmodule Pleroma.Web.Push.Impl do
@doc "Push message to web"
def push_message(body, sub, api_key, subscription) do
case WebPushEncryption.send_web_push(body, sub, api_key) do
- {:ok, %{status_code: code}} when 400 <= code and code < 500 ->
+ {:ok, %{status: code}} when code in 400..499 ->
Logger.debug("Removing subscription record")
Repo.delete!(subscription)
:ok
- {:ok, %{status_code: code}} when 200 <= code and code < 300 ->
+ {:ok, %{status: code}} when code in 200..299 ->
:ok
- {:ok, %{status_code: code}} ->
+ {:ok, %{status: code}} ->
Logger.error("Web Push Notification failed with code: #{code}")
:error
- _ ->
- Logger.error("Web Push Notification failed with unknown error")
+ error ->
+ Logger.error("Web Push Notification failed with #{inspect(error)}")
:error
end
end
@@ -150,6 +150,15 @@ defmodule Pleroma.Web.Push.Impl do
end
def format_body(
+ %{activity: %{data: %{"type" => "EmojiReact", "content" => content}}},
+ actor,
+ _object,
+ _mastodon_type
+ ) do
+ "@#{actor.nickname} reacted with #{content}"
+ end
+
+ def format_body(
%{activity: %{data: %{"type" => type}}} = notification,
actor,
_object,
@@ -179,6 +188,7 @@ defmodule Pleroma.Web.Push.Impl do
"reblog" -> "New Repeat"
"favourite" -> "New Favorite"
"pleroma:chat_mention" -> "New Chat Message"
+ "pleroma:emoji_reaction" -> "New Reaction"
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 5b5aa0d59..749a573ba 100644
--- a/lib/pleroma/web/push/subscription.ex
+++ b/lib/pleroma/web/push/subscription.ex
@@ -25,7 +25,8 @@ defmodule Pleroma.Web.Push.Subscription do
timestamps()
end
- @supported_alert_types ~w[follow favourite mention reblog pleroma:chat_mention]a
+ # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
+ @supported_alert_types ~w[follow favourite mention reblog pleroma:chat_mention pleroma:emoji_reaction]a
defp alerts(%{data: %{alerts: alerts}}) do
alerts = Map.take(alerts, @supported_alert_types)
diff --git a/lib/pleroma/web/rel_me.ex b/lib/pleroma/web/rel_me.ex
index 8e2b51508..28f75b18d 100644
--- a/lib/pleroma/web/rel_me.ex
+++ b/lib/pleroma/web/rel_me.ex
@@ -5,7 +5,8 @@
defmodule Pleroma.Web.RelMe do
@options [
pool: :media,
- max_body: 2_000_000
+ max_body: 2_000_000,
+ recv_timeout: 2_000
]
if Pleroma.Config.get(:env) == :test do
@@ -23,18 +24,8 @@ 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: opts),
+ Pleroma.HTTP.get(url, [], @options),
{: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 747f2dc6b..442bf9995 100644
--- a/lib/pleroma/web/rich_media/helpers.ex
+++ b/lib/pleroma/web/rich_media/helpers.ex
@@ -9,9 +9,15 @@ defmodule Pleroma.Web.RichMedia.Helpers do
alias Pleroma.Object
alias Pleroma.Web.RichMedia.Parser
+ @options [
+ pool: :media,
+ max_body: 2_000_000,
+ recv_timeout: 2_000
+ ]
+
@spec validate_page_url(URI.t() | binary()) :: :ok | :error
defp validate_page_url(page_url) when is_binary(page_url) do
- validate_tld = Pleroma.Config.get([Pleroma.Formatter, :validate_tld])
+ validate_tld = Config.get([Pleroma.Formatter, :validate_tld])
page_url
|> Linkify.Parser.url?(validate_tld: validate_tld)
@@ -49,11 +55,10 @@ 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_from_object(object),
: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,64 @@ 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
+ def rich_media_get(url) do
+ headers = [{"user-agent", Pleroma.Application.user_agent() <> "; Bot"}]
+
+ head_check =
+ case Pleroma.HTTP.head(url, headers, @options) do
+ # If the HEAD request didn't reach the server for whatever reason,
+ # we assume the GET that comes right after won't either
+ {:error, _} = e ->
+ e
+
+ {:ok, %Tesla.Env{status: 200, headers: headers}} ->
+ with :ok <- check_content_type(headers),
+ :ok <- check_content_length(headers),
+ do: :ok
+
+ _ ->
+ :ok
+ end
+
+ with :ok <- head_check, do: Pleroma.HTTP.get(url, headers, @options)
+ end
+
+ defp check_content_type(headers) do
+ case List.keyfind(headers, "content-type", 0) do
+ {_, content_type} ->
+ case Plug.Conn.Utils.media_type(content_type) do
+ {:ok, "text", "html", _} -> :ok
+ _ -> {:error, {:content_type, content_type}}
+ end
+
+ _ ->
+ :ok
+ end
+ end
+
+ @max_body @options[:max_body]
+ defp check_content_length(headers) do
+ case List.keyfind(headers, "content-length", 0) do
+ {_, maybe_content_length} ->
+ case Integer.parse(maybe_content_length) do
+ {content_length, ""} when content_length <= @max_body -> :ok
+ {_, ""} -> {:error, :body_too_large}
+ _ -> :ok
+ end
+
+ _ ->
+ :ok
+ end
end
end
diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex
index c8a767935..c70d2fdba 100644
--- a/lib/pleroma/web/rich_media/parser.ex
+++ b/lib/pleroma/web/rich_media/parser.ex
@@ -3,10 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Parser do
- @options [
- pool: :media,
- max_body: 2_000_000
- ]
+ require Logger
defp parsers do
Pleroma.Config.get([:rich_media, :parsers])
@@ -15,19 +12,69 @@ defmodule Pleroma.Web.RichMedia.Parser do
def parse(nil), do: {:error, "No URL provided"}
if Pleroma.Config.get(:env) == :test do
+ @spec parse(String.t()) :: {:ok, map()} | {:error, any()}
def parse(url), do: parse_url(url)
else
+ @spec parse(String.t()) :: {:ok, map()} | {:error, any()}
def parse(url) do
- try do
- Cachex.fetch!(:rich_media_cache, url, fn _ ->
- {:commit, parse_url(url)}
- end)
- |> set_ttl_based_on_image(url)
- rescue
- e ->
- {:error, "Cachex error: #{inspect(e)}"}
+ with {:ok, data} <- get_cached_or_parse(url),
+ {:ok, _} <- set_ttl_based_on_image(data, url) do
+ {:ok, data}
end
end
+
+ defp get_cached_or_parse(url) do
+ case Cachex.fetch(:rich_media_cache, url, fn ->
+ case parse_url(url) do
+ {:ok, _} = res ->
+ {:commit, res}
+
+ {:error, reason} = e ->
+ # Unfortunately we have to log errors here, instead of doing that
+ # along with ttl setting at the bottom. Otherwise we can get log spam
+ # if more than one process was waiting for the rich media card
+ # while it was generated. Ideally we would set ttl here as well,
+ # so we don't override it number_of_waiters_on_generation
+ # times, but one, obviously, can't set ttl for not-yet-created entry
+ # and Cachex doesn't support returning ttl from the fetch callback.
+ log_error(url, reason)
+ {:commit, e}
+ end
+ end) do
+ {action, res} when action in [:commit, :ok] ->
+ case res do
+ {:ok, _data} = res ->
+ res
+
+ {:error, reason} = e ->
+ if action == :commit, do: set_error_ttl(url, reason)
+ e
+ end
+
+ {:error, e} ->
+ {:error, {:cachex_error, e}}
+ end
+ end
+
+ defp set_error_ttl(_url, :body_too_large), do: :ok
+ defp set_error_ttl(_url, {:content_type, _}), do: :ok
+
+ # The TTL is not set for the errors above, since they are unlikely to change
+ # with time
+
+ defp set_error_ttl(url, _reason) do
+ ttl = Pleroma.Config.get([:rich_media, :failure_backoff], 60_000)
+ Cachex.expire(:rich_media_cache, url, ttl)
+ :ok
+ end
+
+ defp log_error(url, {:invalid_metadata, data}) do
+ Logger.debug(fn -> "Incomplete or invalid metadata for #{url}: #{inspect(data)}" end)
+ end
+
+ defp log_error(url, reason) do
+ Logger.warn(fn -> "Rich media error for #{url}: #{inspect(reason)}" end)
+ end
end
@doc """
@@ -52,19 +99,26 @@ defmodule Pleroma.Web.RichMedia.Parser do
config :pleroma, :rich_media,
ttl_setters: [MyModule]
"""
- def set_ttl_based_on_image({:ok, data}, url) do
- with {:ok, nil} <- Cachex.ttl(:rich_media_cache, url),
- ttl when is_number(ttl) <- get_ttl_from_image(data, url) do
- Cachex.expire_at(:rich_media_cache, url, ttl * 1000)
- {:ok, data}
- else
+ @spec set_ttl_based_on_image(map(), String.t()) ::
+ {:ok, Integer.t() | :noop} | {:error, :no_key}
+ def set_ttl_based_on_image(data, url) do
+ case get_ttl_from_image(data, url) do
+ {:ok, ttl} when is_number(ttl) ->
+ ttl = ttl * 1000
+
+ case Cachex.expire_at(:rich_media_cache, url, ttl) do
+ {:ok, true} -> {:ok, ttl}
+ {:ok, false} -> {:error, :no_key}
+ end
+
_ ->
- {:ok, data}
+ {:ok, :noop}
end
end
defp get_ttl_from_image(data, url) do
- Pleroma.Config.get([:rich_media, :ttl_setters])
+ [:rich_media, :ttl_setters]
+ |> Pleroma.Config.get()
|> Enum.reduce({:ok, nil}, fn
module, {:ok, _ttl} ->
module.ttl(data, url)
@@ -74,37 +128,17 @@ defmodule Pleroma.Web.RichMedia.Parser do
end)
end
- defp parse_url(url) do
- opts =
- if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do
- Keyword.merge(@options,
- recv_timeout: 2_000,
- with_body: true
- )
- else
- @options
- end
-
- try do
- rich_media_agent = Pleroma.Application.user_agent() <> "; Bot"
-
- {:ok, %Tesla.Env{body: html}} =
- Pleroma.HTTP.get(url, [{"user-agent", rich_media_agent}], adapter: opts)
-
+ def parse_url(url) do
+ with {:ok, %Tesla.Env{body: html}} <- Pleroma.Web.RichMedia.Helpers.rich_media_get(url),
+ {:ok, html} <- Floki.parse_document(html) do
html
- |> parse_html()
|> maybe_parse()
|> Map.put("url", url)
|> clean_parsed_data()
|> check_parsed_data()
- rescue
- e ->
- {:error, "Parsing error: #{inspect(e)} #{inspect(__STACKTRACE__)}"}
end
end
- defp parse_html(html), do: Floki.parse_document!(html)
-
defp maybe_parse(html) do
Enum.reduce_while(parsers(), %{}, fn parser, acc ->
case parser.parse(html, acc) do
@@ -120,7 +154,7 @@ defmodule Pleroma.Web.RichMedia.Parser do
end
defp check_parsed_data(data) do
- {:error, "Found metadata was invalid or incomplete: #{inspect(data)}"}
+ {:error, {:invalid_metadata, data}}
end
defp clean_parsed_data(data) do
diff --git a/lib/pleroma/web/rich_media/parser/ttl.ex b/lib/pleroma/web/rich_media/parser/ttl.ex
new file mode 100644
index 000000000..8353f0fff
--- /dev/null
+++ b/lib/pleroma/web/rich_media/parser/ttl.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.RichMedia.Parser.TTL do
+ @callback ttl(Map.t(), String.t()) :: Integer.t() | nil
+end
diff --git a/lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex b/lib/pleroma/web/rich_media/parser/ttl/aws_signed_url.ex
index 0dc1efdaf..fc4ef79c0 100644
--- a/lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex
+++ b/lib/pleroma/web/rich_media/parser/ttl/aws_signed_url.ex
@@ -1,7 +1,11 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl do
@behaviour Pleroma.Web.RichMedia.Parser.TTL
- @impl Pleroma.Web.RichMedia.Parser.TTL
+ @impl true
def ttl(data, _url) do
image = Map.get(data, :image)
@@ -10,20 +14,15 @@ defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl do
|> parse_query_params()
|> format_query_params()
|> get_expiration_timestamp()
+ else
+ {:error, "Not aws signed url #{inspect(image)}"}
end
end
- defp is_aws_signed_url(""), do: nil
- defp is_aws_signed_url(nil), do: nil
-
- defp is_aws_signed_url(image) when is_binary(image) do
+ defp is_aws_signed_url(image) when is_binary(image) and image != "" do
%URI{host: host, query: query} = URI.parse(image)
- if String.contains?(host, "amazonaws.com") and String.contains?(query, "X-Amz-Expires") do
- image
- else
- nil
- end
+ String.contains?(host, "amazonaws.com") and String.contains?(query, "X-Amz-Expires")
end
defp is_aws_signed_url(_), do: nil
@@ -46,6 +45,6 @@ defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl do
|> Map.get("X-Amz-Date")
|> Timex.parse("{ISO:Basic:Z}")
- Timex.to_unix(date) + String.to_integer(Map.get(params, "X-Amz-Expires"))
+ {:ok, Timex.to_unix(date) + String.to_integer(Map.get(params, "X-Amz-Expires"))}
end
end
diff --git a/lib/pleroma/web/rich_media/parsers/oembed_parser.ex b/lib/pleroma/web/rich_media/parsers/o_embed.ex
index 6bdeac89c..1fe6729c3 100644
--- a/lib/pleroma/web/rich_media/parsers/oembed_parser.ex
+++ b/lib/pleroma/web/rich_media/parsers/o_embed.ex
@@ -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/ttl/ttl.ex b/lib/pleroma/web/rich_media/parsers/ttl/ttl.ex
deleted file mode 100644
index 6b3ec6d30..000000000
--- a/lib/pleroma/web/rich_media/parsers/ttl/ttl.ex
+++ /dev/null
@@ -1,3 +0,0 @@
-defmodule Pleroma.Web.RichMedia.Parser.TTL do
- @callback ttl(Map.t(), String.t()) :: {:ok, Integer.t()} | {:error, String.t()}
-end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 386308362..75a885377 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -5,6 +5,26 @@
defmodule Pleroma.Web.Router do
use Pleroma.Web, :router
+ pipeline :accepts_html do
+ plug(:accepts, ["html"])
+ end
+
+ pipeline :accepts_html_xml do
+ plug(:accepts, ["html", "xml", "rss", "atom"])
+ end
+
+ pipeline :accepts_html_json do
+ plug(:accepts, ["html", "activity+json", "json"])
+ end
+
+ pipeline :accepts_html_xml_json do
+ plug(:accepts, ["html", "xml", "rss", "atom", "activity+json", "json"])
+ end
+
+ pipeline :accepts_xml_rss_atom do
+ plug(:accepts, ["xml", "rss", "atom"])
+ end
+
pipeline :browser do
plug(:accepts, ["html"])
plug(:fetch_session)
@@ -12,31 +32,31 @@ defmodule Pleroma.Web.Router do
pipeline :oauth do
plug(:fetch_session)
- plug(Pleroma.Plugs.OAuthPlug)
- plug(Pleroma.Plugs.UserEnabledPlug)
+ plug(Pleroma.Web.Plugs.OAuthPlug)
+ plug(Pleroma.Web.Plugs.UserEnabledPlug)
end
pipeline :expect_authentication do
- plug(Pleroma.Plugs.ExpectAuthenticatedCheckPlug)
+ plug(Pleroma.Web.Plugs.ExpectAuthenticatedCheckPlug)
end
pipeline :expect_public_instance_or_authentication do
- plug(Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug)
+ plug(Pleroma.Web.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)
+ plug(Pleroma.Web.Plugs.OAuthPlug)
+ plug(Pleroma.Web.Plugs.BasicAuthDecoderPlug)
+ plug(Pleroma.Web.Plugs.UserFetcherPlug)
+ plug(Pleroma.Web.Plugs.SessionAuthenticationPlug)
+ plug(Pleroma.Web.Plugs.LegacyAuthenticationPlug)
+ plug(Pleroma.Web.Plugs.AuthenticationPlug)
end
pipeline :after_auth do
- plug(Pleroma.Plugs.UserEnabledPlug)
- plug(Pleroma.Plugs.SetUserSessionIdPlug)
- plug(Pleroma.Plugs.EnsureUserKeyPlug)
+ plug(Pleroma.Web.Plugs.UserEnabledPlug)
+ plug(Pleroma.Web.Plugs.SetUserSessionIdPlug)
+ plug(Pleroma.Web.Plugs.EnsureUserKeyPlug)
end
pipeline :base_api do
@@ -50,25 +70,25 @@ defmodule Pleroma.Web.Router do
plug(:expect_public_instance_or_authentication)
plug(:base_api)
plug(:after_auth)
- plug(Pleroma.Plugs.IdempotencyPlug)
+ plug(Pleroma.Web.Plugs.IdempotencyPlug)
end
pipeline :authenticated_api do
plug(:expect_authentication)
plug(:base_api)
plug(:after_auth)
- plug(Pleroma.Plugs.EnsureAuthenticatedPlug)
- plug(Pleroma.Plugs.IdempotencyPlug)
+ plug(Pleroma.Web.Plugs.EnsureAuthenticatedPlug)
+ plug(Pleroma.Web.Plugs.IdempotencyPlug)
end
pipeline :admin_api do
plug(:expect_authentication)
plug(:base_api)
- plug(Pleroma.Plugs.AdminSecretAuthenticationPlug)
+ plug(Pleroma.Web.Plugs.AdminSecretAuthenticationPlug)
plug(:after_auth)
- plug(Pleroma.Plugs.EnsureAuthenticatedPlug)
- plug(Pleroma.Plugs.UserIsAdminPlug)
- plug(Pleroma.Plugs.IdempotencyPlug)
+ plug(Pleroma.Web.Plugs.EnsureAuthenticatedPlug)
+ plug(Pleroma.Web.Plugs.UserIsAdminPlug)
+ plug(Pleroma.Web.Plugs.IdempotencyPlug)
end
pipeline :mastodon_html do
@@ -80,7 +100,7 @@ defmodule Pleroma.Web.Router do
pipeline :pleroma_html do
plug(:browser)
plug(:authenticate)
- plug(Pleroma.Plugs.EnsureUserKeyPlug)
+ plug(Pleroma.Web.Plugs.EnsureUserKeyPlug)
end
pipeline :well_known do
@@ -129,15 +149,7 @@ defmodule Pleroma.Web.Router do
scope "/api/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:admin_api)
- 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)
put("/users/tag", AdminAPIController, :tag_users)
delete("/users/tag", AdminAPIController, :untag_users)
@@ -160,6 +172,15 @@ defmodule Pleroma.Web.Router do
:right_delete_multiple
)
+ post("/users/follow", UserController, :follow)
+ post("/users/unfollow", UserController, :unfollow)
+ delete("/users", UserController, :delete)
+ post("/users", UserController, :create)
+ patch("/users/:nickname/toggle_activation", UserController, :toggle_activation)
+ patch("/users/activate", UserController, :activate)
+ patch("/users/deactivate", UserController, :deactivate)
+ patch("/users/approve", UserController, :approve)
+
get("/relay", RelayController, :index)
post("/relay", RelayController, :follow)
delete("/relay", RelayController, :unfollow)
@@ -174,12 +195,17 @@ defmodule Pleroma.Web.Router do
get("/users/:nickname/credentials", AdminAPIController, :show_user_credentials)
patch("/users/:nickname/credentials", AdminAPIController, :update_user_credentials)
- get("/users", AdminAPIController, :list_users)
- get("/users/:nickname", AdminAPIController, :user_show)
+ get("/users", UserController, :list)
+ get("/users/:nickname", UserController, :show)
get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses)
+ get("/users/:nickname/chats", AdminAPIController, :list_user_chats)
get("/instances/:instance/statuses", AdminAPIController, :list_instance_statuses)
+ get("/instance_document/:name", InstanceDocumentController, :show)
+ patch("/instance_document/:name", InstanceDocumentController, :update)
+ delete("/instance_document/:name", InstanceDocumentController, :delete)
+
patch("/users/confirm_email", AdminAPIController, :confirm_email)
patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email)
@@ -213,9 +239,32 @@ defmodule Pleroma.Web.Router do
get("/media_proxy_caches", MediaProxyCacheController, :index)
post("/media_proxy_caches/delete", MediaProxyCacheController, :delete)
post("/media_proxy_caches/purge", MediaProxyCacheController, :purge)
+
+ get("/chats/:id", ChatController, :show)
+ get("/chats/:id/messages", ChatController, :messages)
+ delete("/chats/:id/messages/:message_id", ChatController, :delete_message)
+
+ get("/frontends", FrontendController, :index)
+ post("/frontends/install", FrontendController, :install)
+
+ post("/backups", AdminAPIController, :create_backup)
end
scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do
+ scope "/pack" do
+ pipe_through(:admin_api)
+
+ post("/", EmojiPackController, :create)
+ patch("/", EmojiPackController, :update)
+ delete("/", EmojiPackController, :delete)
+ end
+
+ scope "/pack" do
+ pipe_through(:api)
+
+ get("/", EmojiPackController, :show)
+ end
+
# Modifying packs
scope "/packs" do
pipe_through(:admin_api)
@@ -224,21 +273,17 @@ defmodule Pleroma.Web.Router do
get("/remote", EmojiPackController, :remote)
post("/download", EmojiPackController, :download)
- post("/:name", EmojiPackController, :create)
- patch("/:name", EmojiPackController, :update)
- delete("/:name", EmojiPackController, :delete)
-
- post("/:name/files", EmojiPackController, :add_file)
- patch("/:name/files", EmojiPackController, :update_file)
- delete("/:name/files", EmojiPackController, :delete_file)
+ post("/files", EmojiFileController, :create)
+ patch("/files", EmojiFileController, :update)
+ delete("/files", EmojiFileController, :delete)
end
# Pack info / downloading
scope "/packs" do
pipe_through(:api)
+
get("/", EmojiPackController, :index)
- get("/:name", EmojiPackController, :show)
- get("/:name/archive", EmojiPackController, :archive)
+ get("/archive", EmojiPackController, :archive)
end
end
@@ -259,14 +304,15 @@ defmodule Pleroma.Web.Router do
post("/delete_account", UtilController, :delete_account)
put("/notification_settings", UtilController, :update_notificaton_settings)
post("/disable_account", UtilController, :disable_account)
-
- post("/blocks_import", UtilController, :blocks_import)
- post("/follow_import", UtilController, :follow_import)
end
scope "/api/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:authenticated_api)
+ post("/mutes_import", UserImportController, :mutes)
+ post("/blocks_import", UserImportController, :blocks)
+ post("/follow_import", UserImportController, :follow)
+
get("/accounts/mfa", TwoFactorAuthenticationController, :settings)
get("/accounts/mfa/backup_codes", TwoFactorAuthenticationController, :backup_codes)
get("/accounts/mfa/setup/:method", TwoFactorAuthenticationController, :setup)
@@ -332,6 +378,9 @@ defmodule Pleroma.Web.Router do
put("/mascot", MascotController, :update)
post("/scrobble", ScrobbleController, :create)
+
+ get("/backups", BackupController, :index)
+ post("/backups", BackupController, :create)
end
scope [] do
@@ -352,6 +401,7 @@ defmodule Pleroma.Web.Router do
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:api)
get("/accounts/:id/scrobbles", ScrobbleController, :index)
+ get("/federation_status", InstancesController, :show)
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
@@ -545,30 +595,43 @@ defmodule Pleroma.Web.Router do
)
end
- pipeline :ostatus do
- plug(:accepts, ["html", "xml", "rss", "atom", "activity+json", "json"])
- plug(Pleroma.Plugs.StaticFEPlug)
- end
-
- pipeline :oembed do
- plug(:accepts, ["json", "xml"])
- end
-
scope "/", Pleroma.Web do
- pipe_through([:ostatus, :http_signature])
+ # Note: html format is supported only if static FE is enabled
+ # Note: http signature is only considered for json requests (no auth for non-json requests)
+ pipe_through([:accepts_html_json, :http_signature, Pleroma.Web.Plugs.StaticFEPlug])
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)
+ end
- get("/users/:nickname/feed", Feed.UserController, :feed, as: :user_feed)
+ scope "/", Pleroma.Web do
+ # Note: html format is supported only if static FE is enabled
+ # Note: http signature is only considered for json requests (no auth for non-json requests)
+ pipe_through([:accepts_html_xml_json, :http_signature, Pleroma.Web.Plugs.StaticFEPlug])
+
+ # Note: returns user _profile_ for json requests, redirects to user _feed_ for non-json ones
get("/users/:nickname", Feed.UserController, :feed_redirect, as: :user_feed)
+ end
+ scope "/", Pleroma.Web do
+ # Note: html format is supported only if static FE is enabled
+ pipe_through([:accepts_html_xml, Pleroma.Web.Plugs.StaticFEPlug])
+
+ get("/users/:nickname/feed", Feed.UserController, :feed, as: :user_feed)
+ end
+
+ scope "/", Pleroma.Web do
+ pipe_through(:accepts_html)
+ get("/notice/:id/embed_player", OStatus.OStatusController, :notice_player)
+ end
+
+ scope "/", Pleroma.Web do
+ pipe_through(:accepts_xml_rss_atom)
get("/tags/:tag", Feed.TagController, :feed, as: :tag_feed)
end
@@ -669,6 +732,8 @@ defmodule Pleroma.Web.Router do
end
scope "/proxy/", Pleroma.Web.MediaProxy do
+ get("/preview/:sig/:url", MediaProxyController, :preview)
+ get("/preview/:sig/:url/:filename", MediaProxyController, :preview)
get("/:sig/:url", MediaProxyController, :remote)
get("/:sig/:url/:filename", MediaProxyController, :remote)
end
@@ -714,7 +779,7 @@ defmodule Pleroma.Web.Router do
get("/check_password", MongooseIMController, :check_password)
end
- scope "/", Fallback do
+ scope "/", Pleroma.Web.Fallback do
get("/registration/:token", RedirectController, :registration_page)
get("/:maybe_nickname_or_id", RedirectController, :redirector_with_meta)
get("/api*path", RedirectController, :api_not_implemented)
diff --git a/lib/pleroma/web/static_fe/static_fe_controller.ex b/lib/pleroma/web/static_fe/static_fe_controller.ex
index a7a891b13..bdec0897a 100644
--- a/lib/pleroma/web/static_fe/static_fe_controller.ex
+++ b/lib/pleroma/web/static_fe/static_fe_controller.ex
@@ -17,74 +17,14 @@ 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),
- do: name
-
- defp get_title(%Object{data: %{"summary" => summary}}) when is_binary(summary),
- do: summary
-
- defp get_title(_), do: nil
-
- defp not_found(conn, message) do
- conn
- |> put_status(404)
- |> render("error.html", %{message: message, meta: ""})
- end
-
- defp get_counts(%Activity{} = activity) do
- %Object{data: data} = Object.normalize(activity)
-
- %{
- likes: data["like_count"] || 0,
- replies: data["repliesCount"] || 0,
- announces: data["announcement_count"] || 0
- }
- end
-
- defp represent(%Activity{} = activity), do: represent(activity, false)
-
- defp represent(%Activity{object: %Object{data: data}} = activity, selected) do
- {:ok, user} = User.get_or_fetch(activity.object.data["actor"])
-
- link =
- case user.local do
- true -> Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity)
- _ -> data["url"] || data["external_url"] || data["id"]
- end
-
- content =
- if data["content"] do
- data["content"]
- |> Pleroma.HTML.filter_tags()
- |> Pleroma.Emoji.Formatter.emojify(Map.get(data, "emoji", %{}))
- else
- nil
- end
-
- %{
- user: User.sanitize_html(user),
- title: get_title(activity.object),
- content: content,
- attachment: data["attachment"],
- link: link,
- published: data["published"],
- sensitive: data["sensitive"],
- selected: selected,
- counts: get_counts(activity),
- id: activity.id
- }
- end
-
+ @doc "Renders requested local public activity or public activities of requested user"
def show(%{assigns: %{notice_id: notice_id}} = conn, _params) do
with %Activity{local: true} = activity <-
Activity.get_by_id_with_object(notice_id),
true <- Visibility.is_public?(activity.object),
+ {_, true} <- {:visible?, Visibility.visible_for_user?(activity, _reading_user = nil)},
%User{} = user <- User.get_by_ap_id(activity.object.data["actor"]) do
meta = Metadata.build_tags(%{activity_id: notice_id, object: activity.object, user: user})
@@ -107,34 +47,35 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do
end
def show(%{assigns: %{username_or_id: username_or_id}} = conn, params) do
- case User.get_cached_by_nickname_or_id(username_or_id) 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 =
- user
- |> ActivityPub.fetch_user_activities(nil, params)
- |> Enum.map(&represent/1)
-
- prev_page_id =
- (params["min_id"] || params["max_id"]) &&
- List.first(timeline) && List.first(timeline).id
-
- next_page_id = List.last(timeline) && List.last(timeline).id
-
- render(conn, "profile.html", %{
- user: User.sanitize_html(user),
- timeline: timeline,
- prev_page_id: prev_page_id,
- next_page_id: next_page_id,
- meta: meta
- })
+ with {_, %User{local: true} = user} <-
+ {:fetch_user, User.get_cached_by_nickname_or_id(username_or_id)},
+ {_, :visible} <- {:visibility, User.visible_for(user, _reading_user = nil)} do
+ 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 =
+ user
+ |> ActivityPub.fetch_user_activities(_reading_user = nil, params)
+ |> Enum.map(&represent/1)
+
+ prev_page_id =
+ (params["min_id"] || params["max_id"]) &&
+ List.first(timeline) && List.first(timeline).id
+
+ next_page_id = List.last(timeline) && List.last(timeline).id
+
+ render(conn, "profile.html", %{
+ user: User.sanitize_html(user),
+ timeline: timeline,
+ prev_page_id: prev_page_id,
+ next_page_id: next_page_id,
+ meta: meta
+ })
+ else
_ ->
not_found(conn, "User not found.")
end
@@ -166,6 +107,64 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do
end
end
+ defp get_title(%Object{data: %{"name" => name}}) when is_binary(name),
+ do: name
+
+ defp get_title(%Object{data: %{"summary" => summary}}) when is_binary(summary),
+ do: summary
+
+ defp get_title(_), do: nil
+
+ defp not_found(conn, message) do
+ conn
+ |> put_status(404)
+ |> render("error.html", %{message: message, meta: ""})
+ end
+
+ defp get_counts(%Activity{} = activity) do
+ %Object{data: data} = Object.normalize(activity)
+
+ %{
+ likes: data["like_count"] || 0,
+ replies: data["repliesCount"] || 0,
+ announces: data["announcement_count"] || 0
+ }
+ end
+
+ defp represent(%Activity{} = activity), do: represent(activity, false)
+
+ defp represent(%Activity{object: %Object{data: data}} = activity, selected) do
+ {:ok, user} = User.get_or_fetch(activity.object.data["actor"])
+
+ link =
+ case user.local do
+ true -> Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity)
+ _ -> data["url"] || data["external_url"] || data["id"]
+ end
+
+ content =
+ if data["content"] do
+ data["content"]
+ |> Pleroma.HTML.filter_tags()
+ |> Pleroma.Emoji.Formatter.emojify(Map.get(data, "emoji", %{}))
+ else
+ nil
+ end
+
+ %{
+ user: User.sanitize_html(user),
+ title: get_title(activity.object),
+ content: content,
+ attachment: data["attachment"],
+ link: link,
+ published: data["published"],
+ sensitive: data["sensitive"],
+ selected: selected,
+ counts: get_counts(activity),
+ id: activity.id
+ }
+ end
+
defp assign_id(%{path_info: ["notice", notice_id]} = conn, _opts),
do: assign(conn, :notice_id, notice_id)
diff --git a/lib/pleroma/web/streamer/streamer.ex b/lib/pleroma/web/streamer.ex
index d1d70e556..71fe27c89 100644
--- a/lib/pleroma/web/streamer/streamer.ex
+++ b/lib/pleroma/web/streamer.ex
@@ -15,6 +15,8 @@ defmodule Pleroma.Web.Streamer do
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.OAuth.Token
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.StreamerView
@mix_env Mix.env()
@@ -26,53 +28,96 @@ defmodule Pleroma.Web.Streamer do
@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(), User.t() | nil, Map.t() | nil) ::
+ @spec get_topic_and_add_socket(
+ stream :: String.t(),
+ User.t() | nil,
+ Token.t() | nil,
+ Map.t() | nil
+ ) ::
{:ok, topic :: String.t()} | {:error, :bad_topic} | {:error, :unauthorized}
- def get_topic_and_add_socket(stream, user, params \\ %{}) do
- case get_topic(stream, user, params) do
+ def get_topic_and_add_socket(stream, user, oauth_token, params \\ %{}) do
+ case get_topic(stream, user, oauth_token, params) do
{:ok, topic} -> add_socket(topic, user)
error -> error
end
end
@doc "Expand and authorizes a stream"
- @spec get_topic(stream :: String.t(), User.t() | nil, Map.t()) ::
+ @spec get_topic(stream :: String.t(), User.t() | nil, Token.t() | nil, Map.t()) ::
{:ok, topic :: String.t()} | {:error, :bad_topic}
- def get_topic(stream, user, params \\ %{})
+ def get_topic(stream, user, oauth_token, params \\ %{})
# Allow all public steams.
- def get_topic(stream, _, _) when stream in @public_streams do
+ def get_topic(stream, _user, _oauth_token, _params) when stream in @public_streams do
{:ok, stream}
end
# Allow all hashtags streams.
- def get_topic("hashtag", _, %{"tag" => tag}) do
+ def get_topic("hashtag", _user, _oauth_token, %{"tag" => tag} = _params) do
{:ok, "hashtag:" <> tag}
end
+ # Allow remote instance streams.
+ def get_topic("public:remote", _user, _oauth_token, %{"instance" => instance} = _params) do
+ {:ok, "public:remote:" <> instance}
+ end
+
+ def get_topic("public:remote:media", _user, _oauth_token, %{"instance" => instance} = _params) do
+ {:ok, "public:remote:media:" <> instance}
+ end
+
# Expand user streams.
- def get_topic(stream, %User{} = user, _) when stream in @user_streams do
- {:ok, stream <> ":" <> to_string(user.id)}
+ def get_topic(
+ stream,
+ %User{id: user_id} = user,
+ %Token{user_id: token_user_id} = oauth_token,
+ _params
+ )
+ when stream in @user_streams and user_id == token_user_id do
+ # Note: "read" works for all user streams (not mentioning it since it's an ancestor scope)
+ required_scopes =
+ if stream == "user:notification" do
+ ["read:notifications"]
+ else
+ ["read:statuses"]
+ end
+
+ if OAuthScopesPlug.filter_descendants(required_scopes, oauth_token.scopes) == [] do
+ {:error, :unauthorized}
+ else
+ {:ok, stream <> ":" <> to_string(user.id)}
+ end
end
- def get_topic(stream, _, _) when stream in @user_streams do
+ def get_topic(stream, _user, _oauth_token, _params) when stream in @user_streams do
{:error, :unauthorized}
end
# List streams.
- def get_topic("list", %User{} = user, %{"list" => id}) do
- if Pleroma.List.get(id, user) do
- {:ok, "list:" <> to_string(id)}
- else
- {:error, :bad_topic}
+ def get_topic(
+ "list",
+ %User{id: user_id} = user,
+ %Token{user_id: token_user_id} = oauth_token,
+ %{"list" => id}
+ )
+ when user_id == token_user_id do
+ cond do
+ OAuthScopesPlug.filter_descendants(["read", "read:lists"], oauth_token.scopes) == [] ->
+ {:error, :unauthorized}
+
+ Pleroma.List.get(id, user) ->
+ {:ok, "list:" <> to_string(id)}
+
+ true ->
+ {:error, :bad_topic}
end
end
- def get_topic("list", _, _) do
+ def get_topic("list", _user, _oauth_token, _params) do
{:error, :unauthorized}
end
- def get_topic(_, _, _) do
+ def get_topic(_stream, _user, _oauth_token, _params) do
{:error, :bad_topic}
end
diff --git a/lib/pleroma/web/templates/feed/feed/_activity.atom.eex b/lib/pleroma/web/templates/feed/feed/_activity.atom.eex
index 78350f2aa..3fd150c4e 100644
--- a/lib/pleroma/web/templates/feed/feed/_activity.atom.eex
+++ b/lib/pleroma/web/templates/feed/feed/_activity.atom.eex
@@ -12,7 +12,7 @@
<link href="<%= activity_context(@activity) %>" rel="ostatus:conversation"/>
<%= if @data["summary"] do %>
- <summary><%= @data["summary"] %></summary>
+ <summary><%= escape(@data["summary"]) %></summary>
<% end %>
<%= if @activity.local do %>
diff --git a/lib/pleroma/web/templates/feed/feed/_activity.rss.eex b/lib/pleroma/web/templates/feed/feed/_activity.rss.eex
index a304a16af..42960de7d 100644
--- a/lib/pleroma/web/templates/feed/feed/_activity.rss.eex
+++ b/lib/pleroma/web/templates/feed/feed/_activity.rss.eex
@@ -12,7 +12,7 @@
<link rel="ostatus:conversation"><%= activity_context(@activity) %></link>
<%= if @data["summary"] do %>
- <description><%= @data["summary"] %></description>
+ <description><%= escape(@data["summary"]) %></description>
<% end %>
<%= if @activity.local do %>
diff --git a/lib/pleroma/web/templates/layout/app.html.eex b/lib/pleroma/web/templates/layout/app.html.eex
index 5836ec1e0..3f28f1920 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;
}
@@ -228,7 +228,7 @@
<body>
<div class="container">
<h1><%= Pleroma.Config.get([:instance, :name]) %></h1>
- <%= render @view_module, @view_template, assigns %>
+ <%= @inner_content %>
</div>
</body>
</html>
diff --git a/lib/pleroma/web/templates/layout/email_styled.html.eex b/lib/pleroma/web/templates/layout/email_styled.html.eex
index ca2caaf4d..82cabd889 100644
--- a/lib/pleroma/web/templates/layout/email_styled.html.eex
+++ b/lib/pleroma/web/templates/layout/email_styled.html.eex
@@ -181,7 +181,7 @@
</div>
</div>
<% end %>
- <%= render @view_module, @view_template, assigns %>
+ <%= @inner_content %>
</td>
</tr>
diff --git a/lib/pleroma/web/templates/layout/metadata_player.html.eex b/lib/pleroma/web/templates/layout/metadata_player.html.eex
index 460f28094..c00f6fa21 100644
--- a/lib/pleroma/web/templates/layout/metadata_player.html.eex
+++ b/lib/pleroma/web/templates/layout/metadata_player.html.eex
@@ -10,7 +10,7 @@ video, audio {
}
</style>
-<%= render @view_module, @view_template, assigns %>
+<%= @inner_content %>
</body>
</html>
diff --git a/lib/pleroma/web/templates/layout/static_fe.html.eex b/lib/pleroma/web/templates/layout/static_fe.html.eex
index dc0ee2a5c..e6adb526b 100644
--- a/lib/pleroma/web/templates/layout/static_fe.html.eex
+++ b/lib/pleroma/web/templates/layout/static_fe.html.eex
@@ -9,7 +9,7 @@
</head>
<body>
<div class="container">
- <%= render @view_module, @view_template, assigns %>
+ <%= @inner_content %>
</div>
</body>
</html>
diff --git a/lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex
index 8443d906b..ffabe29a6 100644
--- a/lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex
+++ b/lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex
@@ -1,2 +1,2 @@
<h1>Successfully authorized</h1>
-<h2>Token code is <%= @auth.token %></h2>
+<h2>Token code is <br><%= @auth.token %></h2>
diff --git a/lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex
index 961aad976..82785c4b9 100644
--- a/lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex
+++ b/lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex
@@ -1,2 +1,2 @@
<h1>Authorization exists</h1>
-<h2>Access token is <%= @token.token %></h2>
+<h2>Access token is <br><%= @token.token %></h2>
diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/controller.ex
index c2de26b0b..f42dba442 100644
--- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex
+++ b/lib/pleroma/web/twitter_api/controller.ex
@@ -6,10 +6,10 @@ 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
+ alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.TwitterAPI.TokenView
require Logger
diff --git a/lib/pleroma/web/twitter_api/controllers/password_controller.ex b/lib/pleroma/web/twitter_api/controllers/password_controller.ex
index 800ab8954..b1a9d810e 100644
--- a/lib/pleroma/web/twitter_api/controllers/password_controller.ex
+++ b/lib/pleroma/web/twitter_api/controllers/password_controller.ex
@@ -17,6 +17,7 @@ defmodule Pleroma.Web.TwitterAPI.PasswordController do
def reset(conn, %{"token" => token}) do
with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}),
+ false <- PasswordResetToken.expired?(token),
%User{} = user <- User.get_cached_by_id(token.user_id) do
render(conn, "reset.html", %{
token: token,
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 521dc9322..4480a4922 100644
--- a/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex
+++ b/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex
@@ -10,7 +10,6 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do
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
@@ -18,11 +17,11 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do
@status_types ["Article", "Event", "Note", "Video", "Page", "Question"]
- plug(Pleroma.Web.FederatingPlug)
+ plug(Pleroma.Web.Plugs.FederatingPlug)
# Note: follower can submit the form (with password auth) not being signed in (having no token)
plug(
- OAuthScopesPlug,
+ Pleroma.Web.Plugs.OAuthScopesPlug,
%{fallback: :proceed_unauthenticated, scopes: ["follow", "write:follows"]}
when action in [:do_follow]
)
@@ -135,7 +134,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do
end
defp handle_follow_error(conn, {:mfa_required, followee, user, _} = _) do
- {:ok, %{token: token}} = MFA.Token.create_token(user)
+ {:ok, %{token: token}} = MFA.Token.create(user)
render(conn, "follow_mfa.html", %{followee: followee, mfa_token: token, error: false})
end
diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
index f02c4075c..9ead0d626 100644
--- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex
+++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
@@ -11,20 +11,12 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
alias Pleroma.Emoji
alias Pleroma.Healthcheck
alias Pleroma.Notification
- alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.WebFinger
- plug(Pleroma.Web.FederatingPlug when action == :remote_subscribe)
-
- plug(
- OAuthScopesPlug,
- %{scopes: ["follow", "write:follows"]}
- when action == :follow_import
- )
-
- plug(OAuthScopesPlug, %{scopes: ["follow", "write:blocks"]} when action == :blocks_import)
+ plug(Pleroma.Web.Plugs.FederatingPlug when action == :remote_subscribe)
plug(
OAuthScopesPlug,
@@ -82,11 +74,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
def frontend_configurations(conn, _params) do
- config =
- Config.get(:frontend_configurations, %{})
- |> Enum.into(%{})
-
- json(conn, config)
+ render(conn, "frontend_configurations.json")
end
def emoji(conn, _params) do
@@ -104,33 +92,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
end
- def follow_import(conn, %{"list" => %Plug.Upload{} = listfile}) do
- follow_import(conn, %{"list" => File.read!(listfile.path)})
- end
-
- def follow_import(%{assigns: %{user: follower}} = conn, %{"list" => list}) do
- followed_identifiers =
- list
- |> String.split("\n")
- |> Enum.map(&(&1 |> String.split(",") |> List.first()))
- |> List.delete("Account address")
- |> Enum.map(&(&1 |> String.trim() |> String.trim_leading("@")))
- |> Enum.reject(&(&1 == ""))
-
- User.follow_import(follower, followed_identifiers)
- json(conn, "job started")
- end
-
- def blocks_import(conn, %{"list" => %Plug.Upload{} = listfile}) do
- blocks_import(conn, %{"list" => File.read!(listfile.path)})
- end
-
- def blocks_import(%{assigns: %{user: blocker}} = conn, %{"list" => list}) do
- blocked_identifiers = list |> String.split() |> Enum.map(&String.trim_leading(&1, "@"))
- User.blocks_import(blocker, blocked_identifiers)
- json(conn, "job started")
- end
-
def change_password(%{assigns: %{user: user}} = conn, params) do
case CommonAPI.Utils.confirm_current_password(user, params["password"]) do
{:ok, user} ->
diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex
index 5cfb385ac..5d7948507 100644
--- a/lib/pleroma/web/twitter_api/twitter_api.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api.ex
@@ -19,6 +19,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
|> 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)
@@ -44,6 +45,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
case User.register(changeset) do
{:ok, user} ->
+ maybe_notify_admins(user)
{:ok, user}
{:error, changeset} ->
@@ -56,9 +58,21 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
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 is_binary(email) <-
+ %User{local: true, email: email, deactivated: false} = user when is_binary(email) <-
User.get_by_nickname_or_email(nickname_or_email),
{:ok, token_record} <- Pleroma.PasswordResetToken.create_token(user) do
user
@@ -67,17 +81,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
{:ok, :enqueued}
else
- false ->
- {:error, "bad user identifier"}
-
- %User{local: true, email: nil} ->
+ _ ->
{:ok, :noop}
-
- %User{local: false} ->
- {:error, "remote user"}
-
- nil ->
- {:error, "unknown user"}
end
end
diff --git a/lib/pleroma/web/twitter_api/views/util_view.ex b/lib/pleroma/web/twitter_api/views/util_view.ex
index d3bdb4f62..98eea1d18 100644
--- a/lib/pleroma/web/twitter_api/views/util_view.ex
+++ b/lib/pleroma/web/twitter_api/views/util_view.ex
@@ -5,6 +5,7 @@
defmodule Pleroma.Web.TwitterAPI.UtilView do
use Pleroma.Web, :view
import Phoenix.HTML.Form
+ alias Pleroma.Config
alias Pleroma.Web
def status_net_config(instance) do
@@ -19,4 +20,9 @@ defmodule Pleroma.Web.TwitterAPI.UtilView do
</config>
"""
end
+
+ def render("frontend_configurations.json", _) do
+ Config.get(:frontend_configurations, %{})
+ |> Enum.into(%{})
+ end
end
diff --git a/lib/pleroma/web/views/email_view.ex b/lib/pleroma/web/views/email_view.ex
index 6b0fbe61e..bcdee6571 100644
--- a/lib/pleroma/web/views/email_view.ex
+++ b/lib/pleroma/web/views/email_view.ex
@@ -1,3 +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.EmailView do
use Pleroma.Web, :view
import Phoenix.HTML
diff --git a/lib/pleroma/web/views/mailer/subscription_view.ex b/lib/pleroma/web/views/mailer/subscription_view.ex
index fc3d20816..4562a9d6c 100644
--- a/lib/pleroma/web/views/mailer/subscription_view.ex
+++ b/lib/pleroma/web/views/mailer/subscription_view.ex
@@ -1,3 +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.Mailer.SubscriptionView do
use Pleroma.Web, :view
end
diff --git a/lib/pleroma/web/views/masto_fe_view.ex b/lib/pleroma/web/views/masto_fe_view.ex
index f739dacb6..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.mastofe_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/web_finger/web_finger.ex b/lib/pleroma/web/web_finger.ex
index 71ccf251a..6629f5356 100644
--- a/lib/pleroma/web/web_finger/web_finger.ex
+++ b/lib/pleroma/web/web_finger.ex
@@ -136,12 +136,12 @@ defmodule Pleroma.Web.WebFinger do
def find_lrdd_template(domain) do
with {:ok, %{status: status, body: body}} when status in 200..299 <-
- HTTP.get("http://#{domain}/.well-known/host-meta", []) do
+ HTTP.get("http://#{domain}/.well-known/host-meta") do
get_template_from_xml(body)
else
_ ->
with {:ok, %{body: body, status: status}} when status in 200..299 <-
- HTTP.get("https://#{domain}/.well-known/host-meta", []) do
+ HTTP.get("https://#{domain}/.well-known/host-meta") do
get_template_from_xml(body)
else
e -> {:error, "Can't find LRDD template: #{inspect(e)}"}
@@ -149,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, "@")
@@ -163,16 +175,8 @@ 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"}]
diff --git a/lib/pleroma/web/web_finger/web_finger_controller.ex b/lib/pleroma/web/web_finger/web_finger_controller.ex
index 7077b20d2..9f0938fc0 100644
--- a/lib/pleroma/web/web_finger/web_finger_controller.ex
+++ b/lib/pleroma/web/web_finger/web_finger_controller.ex
@@ -7,8 +7,8 @@ defmodule Pleroma.Web.WebFinger.WebFingerController do
alias Pleroma.Web.WebFinger
- plug(Pleroma.Plugs.SetFormatPlug)
- plug(Pleroma.Web.FederatingPlug)
+ plug(Pleroma.Web.Plugs.SetFormatPlug)
+ plug(Pleroma.Web.Plugs.FederatingPlug)
def host_meta(conn, _params) do
xml = WebFinger.host_meta()
diff --git a/lib/pleroma/web/xml/xml.ex b/lib/pleroma/web/xml.ex
index c69a86a1e..c69a86a1e 100644
--- a/lib/pleroma/web/xml/xml.ex
+++ b/lib/pleroma/web/xml.ex
diff --git a/lib/pleroma/workers/background_worker.ex b/lib/pleroma/workers/background_worker.ex
index cec5a7462..0647c65ae 100644
--- a/lib/pleroma/workers/background_worker.ex
+++ b/lib/pleroma/workers/background_worker.ex
@@ -3,9 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.BackgroundWorker do
- alias Pleroma.Activity
alias Pleroma.User
- alias Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy
use Pleroma.Workers.WorkerHelper, queue: "background"
@@ -26,39 +24,10 @@ defmodule Pleroma.Workers.BackgroundWorker do
User.perform(:force_password_reset, user)
end
- def perform(%Job{
- args: %{
- "op" => "blocks_import",
- "blocker_id" => blocker_id,
- "blocked_identifiers" => blocked_identifiers
- }
- }) do
- blocker = User.get_cached_by_id(blocker_id)
- {:ok, User.perform(:blocks_import, blocker, blocked_identifiers)}
- end
-
- def perform(%Job{
- args: %{
- "op" => "follow_import",
- "follower_id" => follower_id,
- "followed_identifiers" => followed_identifiers
- }
- }) do
- follower = User.get_cached_by_id(follower_id)
- {:ok, User.perform(:follow_import, follower, followed_identifiers)}
- end
-
- def perform(%Job{args: %{"op" => "media_proxy_preload", "message" => message}}) do
- MediaProxyWarmingPolicy.perform(:preload, message)
- end
-
- def perform(%Job{args: %{"op" => "media_proxy_prefetch", "url" => url}}) do
- MediaProxyWarmingPolicy.perform(:prefetch, url)
- end
-
- 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)
+ def perform(%Job{args: %{"op" => op, "user_id" => user_id, "identifiers" => identifiers}})
+ when op in ["blocks_import", "follow_import", "mutes_import"] do
+ user = User.get_cached_by_id(user_id)
+ {:ok, User.Import.perform(String.to_atom(op), user, identifiers)}
end
def perform(%Job{
diff --git a/lib/pleroma/workers/backup_worker.ex b/lib/pleroma/workers/backup_worker.ex
new file mode 100644
index 000000000..5b4985983
--- /dev/null
+++ b/lib/pleroma/workers/backup_worker.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.Workers.BackupWorker do
+ use Oban.Worker, queue: :backup, max_attempts: 1
+
+ alias Oban.Job
+ alias Pleroma.User.Backup
+
+ def process(backup, admin_user_id \\ nil) do
+ %{"op" => "process", "backup_id" => backup.id, "admin_user_id" => admin_user_id}
+ |> new()
+ |> Oban.insert()
+ end
+
+ def schedule_deletion(backup) do
+ days = Pleroma.Config.get([Backup, :purge_after_days])
+ time = 60 * 60 * 24 * days
+ scheduled_at = Calendar.NaiveDateTime.add!(backup.inserted_at, time)
+
+ %{"op" => "delete", "backup_id" => backup.id}
+ |> new(scheduled_at: scheduled_at)
+ |> Oban.insert()
+ end
+
+ def delete(backup) do
+ %{"op" => "delete", "backup_id" => backup.id}
+ |> new()
+ |> Oban.insert()
+ end
+
+ def perform(%Job{
+ args: %{"op" => "process", "backup_id" => backup_id, "admin_user_id" => admin_user_id}
+ }) do
+ with {:ok, %Backup{} = backup} <-
+ backup_id |> Backup.get() |> Backup.process(),
+ {:ok, _job} <- schedule_deletion(backup),
+ :ok <- Backup.remove_outdated(backup),
+ {:ok, _} <-
+ backup
+ |> Pleroma.Emails.UserEmail.backup_is_ready_email(admin_user_id)
+ |> Pleroma.Emails.Mailer.deliver() do
+ {:ok, backup}
+ end
+ end
+
+ def perform(%Job{args: %{"op" => "delete", "backup_id" => backup_id}}) do
+ case Backup.get(backup_id) do
+ %Backup{} = backup -> Backup.delete(backup)
+ nil -> :ok
+ end
+ end
+end
diff --git a/lib/pleroma/workers/cron/clear_oauth_token_worker.ex b/lib/pleroma/workers/cron/clear_oauth_token_worker.ex
deleted file mode 100644
index d41be4e87..000000000
--- a/lib/pleroma/workers/cron/clear_oauth_token_worker.ex
+++ /dev/null
@@ -1,23 +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.Workers.Cron.ClearOauthTokenWorker do
- @moduledoc """
- The worker to cleanup expired oAuth tokens.
- """
-
- use Oban.Worker, queue: "background"
-
- alias Pleroma.Config
- alias Pleroma.Web.OAuth.Token
-
- @impl Oban.Worker
- def perform(_job) do
- if Config.get([:oauth2, :clean_expired_tokens], false) do
- Token.delete_expired_tokens()
- else
- :ok
- end
- end
-end
diff --git a/lib/pleroma/workers/cron/digest_emails_worker.ex b/lib/pleroma/workers/cron/digest_emails_worker.ex
index ee646229f..0c56f00fb 100644
--- a/lib/pleroma/workers/cron/digest_emails_worker.ex
+++ b/lib/pleroma/workers/cron/digest_emails_worker.ex
@@ -37,9 +37,9 @@ defmodule Pleroma.Workers.Cron.DigestEmailsWorker do
)
|> Repo.all()
|> send_emails
- else
- :ok
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 abc8a5e95..8bbaed83d 100644
--- a/lib/pleroma/workers/cron/new_users_digest_worker.ex
+++ b/lib/pleroma/workers/cron/new_users_digest_worker.ex
@@ -55,11 +55,9 @@ defmodule Pleroma.Workers.Cron.NewUsersDigestWorker do
|> Repo.all()
|> Enum.map(&Pleroma.Emails.NewUsersDigestEmail.new_users(&1, users_and_statuses))
|> Enum.each(&Pleroma.Emails.Mailer.deliver/1)
- else
- :ok
end
- else
- :ok
end
+
+ :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
deleted file mode 100644
index e926c5dc8..000000000
--- a/lib/pleroma/workers/cron/purge_expired_activities_worker.ex
+++ /dev/null
@@ -1,48 +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.Workers.Cron.PurgeExpiredActivitiesWorker do
- @moduledoc """
- The worker to purge expired activities.
- """
-
- use Oban.Worker, queue: "activity_expiration"
-
- alias Pleroma.Activity
- alias Pleroma.ActivityExpiration
- alias Pleroma.Config
- alias Pleroma.User
- alias Pleroma.Web.CommonAPI
-
- require Logger
-
- @interval :timer.minutes(1)
-
- @impl Oban.Worker
- def perform(_job) do
- if Config.get([ActivityExpiration, :enabled]) do
- Enum.each(ActivityExpiration.due_expirations(@interval), &delete_activity/1)
- else
- :ok
- end
- end
-
- def delete_activity(%ActivityExpiration{activity_id: activity_id}) do
- with {:activity, %Activity{} = activity} <-
- {:activity, Activity.get_by_id_with_object(activity_id)},
- {:user, %User{} = user} <- {:user, User.get_by_ap_id(activity.object.data["actor"])} do
- CommonAPI.delete(activity.id, user)
- else
- {:activity, _} ->
- Logger.error(
- "#{__MODULE__} Couldn't delete expired activity: not found activity ##{activity_id}"
- )
-
- {:user, _} ->
- Logger.error(
- "#{__MODULE__} Couldn't delete expired activity: not found actorof ##{activity_id}"
- )
- end
- end
-end
diff --git a/lib/pleroma/workers/cron/stats_worker.ex b/lib/pleroma/workers/cron/stats_worker.ex
deleted file mode 100644
index e54bd9a7f..000000000
--- a/lib/pleroma/workers/cron/stats_worker.ex
+++ /dev/null
@@ -1,16 +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.Workers.Cron.StatsWorker do
- @moduledoc """
- The worker to update peers statistics.
- """
-
- use Oban.Worker, queue: "background"
-
- @impl Oban.Worker
- def perform(_job) do
- Pleroma.Stats.do_collect()
- end
-end
diff --git a/lib/pleroma/workers/mute_expire_worker.ex b/lib/pleroma/workers/mute_expire_worker.ex
new file mode 100644
index 000000000..32a12ba85
--- /dev/null
+++ b/lib/pleroma/workers/mute_expire_worker.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.Workers.MuteExpireWorker do
+ use Pleroma.Workers.WorkerHelper, queue: "mute_expire"
+
+ @impl Oban.Worker
+ def perform(%Job{args: %{"op" => "unmute_user", "muter_id" => muter_id, "mutee_id" => mutee_id}}) do
+ Pleroma.User.unmute(muter_id, mutee_id)
+ :ok
+ end
+
+ def perform(%Job{
+ args: %{"op" => "unmute_conversation", "user_id" => user_id, "activity_id" => activity_id}
+ }) do
+ Pleroma.Web.CommonAPI.remove_mute(user_id, activity_id)
+ :ok
+ end
+end
diff --git a/lib/pleroma/workers/purge_expired_activity.ex b/lib/pleroma/workers/purge_expired_activity.ex
new file mode 100644
index 000000000..c168890a2
--- /dev/null
+++ b/lib/pleroma/workers/purge_expired_activity.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.Workers.PurgeExpiredActivity do
+ @moduledoc """
+ Worker which purges expired activity.
+ """
+
+ use Oban.Worker, queue: :activity_expiration, max_attempts: 1
+
+ import Ecto.Query
+
+ alias Pleroma.Activity
+
+ @spec enqueue(map()) ::
+ {:ok, Oban.Job.t()}
+ | {:error, :expired_activities_disabled}
+ | {:error, :expiration_too_close}
+ def enqueue(args) do
+ with true <- enabled?() do
+ {scheduled_at, args} = Map.pop(args, :expires_at)
+
+ args
+ |> new(scheduled_at: scheduled_at)
+ |> Oban.insert()
+ end
+ end
+
+ @impl true
+ def perform(%Oban.Job{args: %{"activity_id" => id}}) do
+ with %Activity{} = activity <- find_activity(id),
+ %Pleroma.User{} = user <- find_user(activity.object.data["actor"]) do
+ Pleroma.Web.CommonAPI.delete(activity.id, user)
+ end
+ end
+
+ defp enabled? do
+ with false <- Pleroma.Config.get([__MODULE__, :enabled], false) do
+ {:error, :expired_activities_disabled}
+ end
+ end
+
+ defp find_activity(id) do
+ with nil <- Activity.get_by_id_with_object(id) do
+ {:error, :activity_not_found}
+ end
+ end
+
+ defp find_user(ap_id) do
+ with nil <- Pleroma.User.get_by_ap_id(ap_id) do
+ {:error, :user_not_found}
+ end
+ end
+
+ def get_expiration(id) do
+ from(j in Oban.Job,
+ where: j.state == "scheduled",
+ where: j.queue == "activity_expiration",
+ where: fragment("?->>'activity_id' = ?", j.args, ^id)
+ )
+ |> Pleroma.Repo.one()
+ end
+
+ @spec expires_late_enough?(DateTime.t()) :: boolean()
+ def expires_late_enough?(scheduled_at) do
+ now = DateTime.utc_now()
+ diff = DateTime.diff(scheduled_at, now, :millisecond)
+ min_lifetime = Pleroma.Config.get([__MODULE__, :min_lifetime], 600)
+ diff > :timer.seconds(min_lifetime)
+ end
+end
diff --git a/lib/pleroma/workers/purge_expired_token.ex b/lib/pleroma/workers/purge_expired_token.ex
new file mode 100644
index 000000000..a81e0cd28
--- /dev/null
+++ b/lib/pleroma/workers/purge_expired_token.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.Workers.PurgeExpiredToken do
+ @moduledoc """
+ Worker which purges expired OAuth tokens
+ """
+
+ use Oban.Worker, queue: :token_expiration, max_attempts: 1
+
+ @spec enqueue(%{token_id: integer(), valid_until: DateTime.t(), mod: module()}) ::
+ {:ok, Oban.Job.t()} | {:error, Ecto.Changeset.t()}
+ def enqueue(args) do
+ {scheduled_at, args} = Map.pop(args, :valid_until)
+
+ args
+ |> __MODULE__.new(scheduled_at: scheduled_at)
+ |> Oban.insert()
+ end
+
+ @impl true
+ def perform(%Oban.Job{args: %{"token_id" => id, "mod" => module}}) do
+ module
+ |> String.to_existing_atom()
+ |> Pleroma.Repo.get(id)
+ |> Pleroma.Repo.delete()
+ end
+end
diff --git a/lib/xml_builder.ex b/lib/pleroma/xml_builder.ex
index 33b63a71f..33b63a71f 100644
--- a/lib/xml_builder.ex
+++ b/lib/pleroma/xml_builder.ex