summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/mix/pleroma.ex3
-rw-r--r--lib/mix/tasks/pleroma/benchmark.ex113
-rw-r--r--lib/mix/tasks/pleroma/config.ex29
-rw-r--r--lib/mix/tasks/pleroma/database.ex169
-rw-r--r--lib/mix/tasks/pleroma/digest.ex2
-rw-r--r--lib/mix/tasks/pleroma/ecto/rollback.ex2
-rw-r--r--lib/mix/tasks/pleroma/emoji.ex2
-rw-r--r--lib/mix/tasks/pleroma/instance.ex4
-rw-r--r--lib/mix/tasks/pleroma/search/indexer.ex83
-rw-r--r--lib/mix/tasks/pleroma/search/meilisearch.ex145
-rw-r--r--lib/mix/tasks/pleroma/test_runner.ex25
-rw-r--r--lib/phoenix/transports/web_socket/raw.ex94
-rw-r--r--lib/pleroma/activity.ex2
-rw-r--r--lib/pleroma/activity/html.ex2
-rw-r--r--lib/pleroma/activity/queries.ex2
-rw-r--r--lib/pleroma/announcement.ex16
-rw-r--r--lib/pleroma/application.ex163
-rw-r--r--lib/pleroma/application_requirements.ex34
-rw-r--r--lib/pleroma/bookmark.ex41
-rw-r--r--lib/pleroma/bookmark_folder.ex115
-rw-r--r--lib/pleroma/caching.ex3
-rw-r--r--lib/pleroma/captcha/kocaptcha.ex2
-rw-r--r--lib/pleroma/chat.ex12
-rw-r--r--lib/pleroma/config/deprecation_warnings.ex36
-rw-r--r--lib/pleroma/config/getting.ex7
-rw-r--r--lib/pleroma/config/oban.ex2
-rw-r--r--lib/pleroma/config/release_runtime_provider.ex2
-rw-r--r--lib/pleroma/config/transfer_task.ex52
-rw-r--r--lib/pleroma/config_db.ex18
-rw-r--r--lib/pleroma/constants.ex11
-rw-r--r--lib/pleroma/conversation.ex2
-rw-r--r--lib/pleroma/conversation/participation.ex2
-rw-r--r--lib/pleroma/data_migration.ex2
-rw-r--r--lib/pleroma/docs/generator.ex4
-rw-r--r--lib/pleroma/docs/json.ex2
-rw-r--r--lib/pleroma/ecto_enums.ex8
-rw-r--r--lib/pleroma/ecto_type/activity_pub/object_validators/bare_uri.ex10
-rw-r--r--lib/pleroma/emails/user_email.ex35
-rw-r--r--lib/pleroma/emoji.ex16
-rw-r--r--lib/pleroma/emoji/loader.ex6
-rw-r--r--lib/pleroma/emoji/pack.ex20
-rw-r--r--lib/pleroma/filter.ex3
-rw-r--r--lib/pleroma/following_relationship.ex14
-rw-r--r--lib/pleroma/frontend.ex4
-rw-r--r--lib/pleroma/gopher/server.ex2
-rw-r--r--lib/pleroma/gun/conn.ex6
-rw-r--r--lib/pleroma/gun/connection_pool/reclaimer.ex2
-rw-r--r--lib/pleroma/gun/connection_pool/worker_supervisor.ex30
-rw-r--r--lib/pleroma/helpers/inet_helper.ex11
-rw-r--r--lib/pleroma/helpers/media_helper.ex175
-rw-r--r--lib/pleroma/helpers/qt_fast_start.ex37
-rw-r--r--lib/pleroma/html.ex21
-rw-r--r--lib/pleroma/http.ex21
-rw-r--r--lib/pleroma/http/adapter_helper.ex12
-rw-r--r--lib/pleroma/http/adapter_helper/gun.ex2
-rw-r--r--lib/pleroma/http/request_builder.ex4
-rw-r--r--lib/pleroma/http/web_push.ex6
-rw-r--r--lib/pleroma/http_signatures_api.ex4
-rw-r--r--lib/pleroma/instances.ex11
-rw-r--r--lib/pleroma/instances/instance.ex20
-rw-r--r--lib/pleroma/maintenance.ex2
-rw-r--r--lib/pleroma/maps.ex15
-rw-r--r--lib/pleroma/mfa.ex2
-rw-r--r--lib/pleroma/mfa/totp.ex3
-rw-r--r--lib/pleroma/migrators/hashtags_table_migrator.ex2
-rw-r--r--lib/pleroma/migrators/support/base_migrator.ex11
-rw-r--r--lib/pleroma/moderation_log.ex5
-rw-r--r--lib/pleroma/notification.ex107
-rw-r--r--lib/pleroma/object.ex86
-rw-r--r--lib/pleroma/object/fetcher.ex64
-rw-r--r--lib/pleroma/object/updater.ex5
-rw-r--r--lib/pleroma/pagination.ex9
-rw-r--r--lib/pleroma/password/pbkdf2.ex2
-rw-r--r--lib/pleroma/prom_ex.ex49
-rw-r--r--lib/pleroma/release_tasks.ex6
-rw-r--r--lib/pleroma/repo.ex2
-rw-r--r--lib/pleroma/report_note.ex8
-rw-r--r--lib/pleroma/reverse_proxy.ex32
-rw-r--r--lib/pleroma/rule.ex68
-rw-r--r--lib/pleroma/scheduled_activity.ex11
-rw-r--r--lib/pleroma/search.ex21
-rw-r--r--lib/pleroma/search/database_search.ex (renamed from lib/pleroma/activity/search.ex)74
-rw-r--r--lib/pleroma/search/healthcheck.ex86
-rw-r--r--lib/pleroma/search/meilisearch.ex198
-rw-r--r--lib/pleroma/search/qdrant_search.ex182
-rw-r--r--lib/pleroma/search/search_backend.ex42
-rw-r--r--lib/pleroma/signature.ex70
-rw-r--r--lib/pleroma/telemetry/logger.ex8
-rw-r--r--lib/pleroma/upload.ex44
-rw-r--r--lib/pleroma/upload/filter/analyze_metadata.ex44
-rw-r--r--lib/pleroma/upload/filter/exiftool/read_description.ex2
-rw-r--r--lib/pleroma/upload/filter/exiftool/strip_location.ex6
-rw-r--r--lib/pleroma/upload/filter/mogrifun.ex1
-rw-r--r--lib/pleroma/upload/filter/mogrify.ex1
-rw-r--r--lib/pleroma/uploaders/ipfs.ex72
-rw-r--r--lib/pleroma/uploaders/s3.ex15
-rw-r--r--lib/pleroma/uploaders/uploader.ex11
-rw-r--r--lib/pleroma/user.ex173
-rw-r--r--lib/pleroma/user/backup.ex348
-rw-r--r--lib/pleroma/user/import.ex4
-rw-r--r--lib/pleroma/user/query.ex4
-rw-r--r--lib/pleroma/user_invite_token.ex2
-rw-r--r--lib/pleroma/user_relationship.ex2
-rw-r--r--lib/pleroma/web.ex4
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub.ex89
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub_controller.ex33
-rw-r--r--lib/pleroma/web/activity_pub/builder.ex7
-rw-r--r--lib/pleroma/web/activity_pub/mrf.ex21
-rw-r--r--lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex2
-rw-r--r--lib/pleroma/web/activity_pub/mrf/anti_mention_spam_policy.ex87
-rw-r--r--lib/pleroma/web/activity_pub/mrf/dnsrbl_policy.ex146
-rw-r--r--lib/pleroma/web/activity_pub/mrf/follow_bot_policy.ex4
-rw-r--r--lib/pleroma/web/activity_pub/mrf/force_mention.ex59
-rw-r--r--lib/pleroma/web/activity_pub/mrf/hashtag_policy.ex4
-rw-r--r--lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex1
-rw-r--r--lib/pleroma/web/activity_pub/mrf/keyword_policy.ex5
-rw-r--r--lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex18
-rw-r--r--lib/pleroma/web/activity_pub/mrf/no_empty_policy.ex16
-rw-r--r--lib/pleroma/web/activity_pub/mrf/nsfw_api_policy.ex264
-rw-r--r--lib/pleroma/web/activity_pub/mrf/policy.ex4
-rw-r--r--lib/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy.ex2
-rw-r--r--lib/pleroma/web/activity_pub/mrf/simple_policy.ex6
-rw-r--r--lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex8
-rw-r--r--lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex1
-rw-r--r--lib/pleroma/web/activity_pub/object_validator.ex3
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/announce_validator.ex2
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex7
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex5
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/common_fields.ex1
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/common_fixes.ex9
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex8
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/question_options_validator.ex4
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/question_validator.ex1
-rw-r--r--lib/pleroma/web/activity_pub/pipeline.ex4
-rw-r--r--lib/pleroma/web/activity_pub/publisher.ex229
-rw-r--r--lib/pleroma/web/activity_pub/relay.ex4
-rw-r--r--lib/pleroma/web/activity_pub/side_effects.ex78
-rw-r--r--lib/pleroma/web/activity_pub/side_effects/handling.ex2
-rw-r--r--lib/pleroma/web/activity_pub/transmogrifier.ex21
-rw-r--r--lib/pleroma/web/activity_pub/utils.ex39
-rw-r--r--lib/pleroma/web/activity_pub/views/user_view.ex13
-rw-r--r--lib/pleroma/web/activity_pub/visibility.ex50
-rw-r--r--lib/pleroma/web/admin_api/controllers/admin_api_controller.ex5
-rw-r--r--lib/pleroma/web/admin_api/controllers/config_controller.ex6
-rw-r--r--lib/pleroma/web/admin_api/controllers/instance_document_controller.ex13
-rw-r--r--lib/pleroma/web/admin_api/controllers/invite_controller.ex15
-rw-r--r--lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex15
-rw-r--r--lib/pleroma/web/admin_api/controllers/relay_controller.ex18
-rw-r--r--lib/pleroma/web/admin_api/controllers/report_controller.ex42
-rw-r--r--lib/pleroma/web/admin_api/controllers/rule_controller.ex62
-rw-r--r--lib/pleroma/web/admin_api/controllers/user_controller.ex107
-rw-r--r--lib/pleroma/web/admin_api/views/report_view.ex17
-rw-r--r--lib/pleroma/web/admin_api/views/rule_view.ex22
-rw-r--r--lib/pleroma/web/api_spec.ex12
-rw-r--r--lib/pleroma/web/api_spec/cast_and_validate.ex20
-rw-r--r--lib/pleroma/web/api_spec/helpers.ex2
-rw-r--r--lib/pleroma/web/api_spec/operations/account_operation.ex62
-rw-r--r--lib/pleroma/web/api_spec/operations/admin/frontend_operation.ex4
-rw-r--r--lib/pleroma/web/api_spec/operations/admin/o_auth_app_operation.ex8
-rw-r--r--lib/pleroma/web/api_spec/operations/admin/report_operation.ex29
-rw-r--r--lib/pleroma/web/api_spec/operations/admin/rule_operation.ex115
-rw-r--r--lib/pleroma/web/api_spec/operations/chat_operation.ex14
-rw-r--r--lib/pleroma/web/api_spec/operations/directory_operation.ex2
-rw-r--r--lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex6
-rw-r--r--lib/pleroma/web/api_spec/operations/instance_operation.ex229
-rw-r--r--lib/pleroma/web/api_spec/operations/notification_operation.ex12
-rw-r--r--lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex2
-rw-r--r--lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex10
-rw-r--r--lib/pleroma/web/api_spec/operations/pleroma_bookmark_folder_operation.ex125
-rw-r--r--lib/pleroma/web/api_spec/operations/pleroma_notification_operation.ex8
-rw-r--r--lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex8
-rw-r--r--lib/pleroma/web/api_spec/operations/pleroma_status_operation.ex45
-rw-r--r--lib/pleroma/web/api_spec/operations/poll_operation.ex2
-rw-r--r--lib/pleroma/web/api_spec/operations/report_operation.ex9
-rw-r--r--lib/pleroma/web/api_spec/operations/scheduled_activity_operation.ex2
-rw-r--r--lib/pleroma/web/api_spec/operations/search_operation.ex8
-rw-r--r--lib/pleroma/web/api_spec/operations/status_operation.ex32
-rw-r--r--lib/pleroma/web/api_spec/operations/streaming_operation.ex2
-rw-r--r--lib/pleroma/web/api_spec/operations/timeline_operation.ex7
-rw-r--r--lib/pleroma/web/api_spec/operations/twitter_util_operation.ex10
-rw-r--r--lib/pleroma/web/api_spec/schemas/attachment.ex8
-rw-r--r--lib/pleroma/web/api_spec/schemas/bookmark_folder.ex26
-rw-r--r--lib/pleroma/web/api_spec/schemas/chat.ex2
-rw-r--r--lib/pleroma/web/api_spec/schemas/poll.ex14
-rw-r--r--lib/pleroma/web/api_spec/schemas/status.ex11
-rw-r--r--lib/pleroma/web/auth/authenticator.ex2
-rw-r--r--lib/pleroma/web/auth/ldap_authenticator.ex51
-rw-r--r--lib/pleroma/web/common_api.ex118
-rw-r--r--lib/pleroma/web/common_api/activity_draft.ex22
-rw-r--r--lib/pleroma/web/common_api/utils.ex6
-rw-r--r--lib/pleroma/web/controller_helper.ex18
-rw-r--r--lib/pleroma/web/embed_controller.ex4
-rw-r--r--lib/pleroma/web/endpoint.ex100
-rw-r--r--lib/pleroma/web/fallback/redirect_controller.ex27
-rw-r--r--lib/pleroma/web/federator.ex21
-rw-r--r--lib/pleroma/web/federator/publisher.ex109
-rw-r--r--lib/pleroma/web/feed/feed_view.ex4
-rw-r--r--lib/pleroma/web/gettext.ex4
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/account_controller.ex111
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/directory_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex18
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex4
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/instance_controller.ex12
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/list_controller.ex52
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/media_controller.ex26
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/notification_controller.ex64
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/poll_controller.ex18
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex17
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/search_controller.ex20
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/status_controller.ex200
-rw-r--r--lib/pleroma/web/mastodon_api/mastodon_api.ex6
-rw-r--r--lib/pleroma/web/mastodon_api/views/account_view.ex43
-rw-r--r--lib/pleroma/web/mastodon_api/views/instance_view.ex181
-rw-r--r--lib/pleroma/web/mastodon_api/views/notification_view.ex3
-rw-r--r--lib/pleroma/web/mastodon_api/views/poll_view.ex5
-rw-r--r--lib/pleroma/web/mastodon_api/views/status_view.ex104
-rw-r--r--lib/pleroma/web/mastodon_api/websocket_handler.ex256
-rw-r--r--lib/pleroma/web/media_proxy.ex3
-rw-r--r--lib/pleroma/web/media_proxy/media_proxy_controller.ex3
-rw-r--r--lib/pleroma/web/metadata/utils.ex5
-rw-r--r--lib/pleroma/web/nodeinfo/nodeinfo.ex2
-rw-r--r--lib/pleroma/web/o_auth/app.ex2
-rw-r--r--lib/pleroma/web/o_auth/authorization.ex8
-rw-r--r--lib/pleroma/web/o_auth/o_auth_controller.ex9
-rw-r--r--lib/pleroma/web/o_auth/token.ex9
-rw-r--r--lib/pleroma/web/o_auth/token/query.ex4
-rw-r--r--lib/pleroma/web/o_status/o_status_controller.ex9
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/backup_controller.ex2
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/bookmark_folder_controller.ex68
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/chat_controller.ex69
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex24
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex29
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex13
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/notification_controller.ex36
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/status_controller.ex66
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex41
-rw-r--r--lib/pleroma/web/pleroma_api/views/backup_view.ex10
-rw-r--r--lib/pleroma/web/pleroma_api/views/bookmark_folder_view.ex42
-rw-r--r--lib/pleroma/web/pleroma_api/views/chat/message_reference_view.ex13
-rw-r--r--lib/pleroma/web/pleroma_api/views/scrobble_view.ex1
-rw-r--r--lib/pleroma/web/plugs/cache.ex2
-rw-r--r--lib/pleroma/web/plugs/http_security_plug.ex51
-rw-r--r--lib/pleroma/web/plugs/http_signature_plug.ex107
-rw-r--r--lib/pleroma/web/plugs/logger_metadata_path.ex12
-rw-r--r--lib/pleroma/web/plugs/logger_metadata_user.ex18
-rw-r--r--lib/pleroma/web/plugs/o_auth_plug.ex6
-rw-r--r--lib/pleroma/web/plugs/o_auth_scopes_plug.ex4
-rw-r--r--lib/pleroma/web/plugs/rate_limiter.ex2
-rw-r--r--lib/pleroma/web/plugs/rate_limiter/supervisor.ex2
-rw-r--r--lib/pleroma/web/plugs/remote_ip.ex14
-rw-r--r--lib/pleroma/web/plugs/uploaded_media.ex2
-rw-r--r--lib/pleroma/web/push.ex14
-rw-r--r--lib/pleroma/web/push/impl.ex192
-rw-r--r--lib/pleroma/web/rich_media/backfill.ex68
-rw-r--r--lib/pleroma/web/rich_media/card.ex163
-rw-r--r--lib/pleroma/web/rich_media/helpers.ex128
-rw-r--r--lib/pleroma/web/rich_media/parser.ex186
-rw-r--r--lib/pleroma/web/rich_media/parser/ttl.ex15
-rw-r--r--lib/pleroma/web/rich_media/parser/ttl/aws_signed_url.ex15
-rw-r--r--lib/pleroma/web/rich_media/parser/ttl/opengraph.ex20
-rw-r--r--lib/pleroma/web/rich_media/parsers/o_embed.ex2
-rw-r--r--lib/pleroma/web/router.ex44
-rw-r--r--lib/pleroma/web/static_fe/static_fe_controller.ex3
-rw-r--r--lib/pleroma/web/streamer.ex48
-rw-r--r--lib/pleroma/web/templates/feed/feed/tag.atom.eex2
-rw-r--r--lib/pleroma/web/templates/feed/feed/tag.rss.eex2
-rw-r--r--lib/pleroma/web/templates/feed/feed/user.atom.eex4
-rw-r--r--lib/pleroma/web/templates/feed/feed/user.rss.eex6
-rw-r--r--lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex8
-rw-r--r--lib/pleroma/web/templates/o_auth/mfa/totp.html.eex8
-rw-r--r--lib/pleroma/web/templates/o_auth/o_auth/register.html.eex8
-rw-r--r--lib/pleroma/web/templates/o_auth/o_auth/show.html.eex10
-rw-r--r--lib/pleroma/web/twitter_api/controllers/password_controller.ex2
-rw-r--r--lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex17
-rw-r--r--lib/pleroma/web/twitter_api/controllers/util_controller.ex57
-rw-r--r--lib/pleroma/web/web_finger.ex10
-rw-r--r--lib/pleroma/web/web_finger/web_finger_controller.ex2
-rw-r--r--lib/pleroma/web/xml.ex2
-rw-r--r--lib/pleroma/workers/attachments_cleanup_worker.ex2
-rw-r--r--lib/pleroma/workers/background_worker.ex15
-rw-r--r--lib/pleroma/workers/backup_worker.ex52
-rw-r--r--lib/pleroma/workers/cron/digest_emails_worker.ex5
-rw-r--r--lib/pleroma/workers/cron/new_users_digest_worker.ex5
-rw-r--r--lib/pleroma/workers/delete_worker.ex24
-rw-r--r--lib/pleroma/workers/mailer_worker.ex2
-rw-r--r--lib/pleroma/workers/mute_expire_worker.ex4
-rw-r--r--lib/pleroma/workers/poll_worker.ex10
-rw-r--r--lib/pleroma/workers/publisher_worker.ex4
-rw-r--r--lib/pleroma/workers/purge_expired_activity.ex12
-rw-r--r--lib/pleroma/workers/purge_expired_filter.ex4
-rw-r--r--lib/pleroma/workers/purge_expired_token.ex2
-rw-r--r--lib/pleroma/workers/receiver_worker.ex60
-rw-r--r--lib/pleroma/workers/remote_fetcher_worker.ex24
-rw-r--r--lib/pleroma/workers/rich_media_worker.ex40
-rw-r--r--lib/pleroma/workers/scheduled_activity_worker.ex2
-rw-r--r--lib/pleroma/workers/search_indexing_worker.ex26
-rw-r--r--lib/pleroma/workers/user_refresh_worker.ex17
-rw-r--r--lib/pleroma/workers/web_pusher_worker.ex4
298 files changed, 6857 insertions, 2820 deletions
diff --git a/lib/mix/pleroma.ex b/lib/mix/pleroma.ex
index 2976085ba..c01cf054d 100644
--- a/lib/mix/pleroma.ex
+++ b/lib/mix/pleroma.ex
@@ -14,7 +14,8 @@ defmodule Mix.Pleroma do
:swoosh,
:timex,
:fast_html,
- :oban
+ :oban,
+ :logger_backends
]
@cachex_children ["object", "user", "scrubber", "web_resp"]
@doc "Common functions to be reused in mix tasks"
diff --git a/lib/mix/tasks/pleroma/benchmark.ex b/lib/mix/tasks/pleroma/benchmark.ex
deleted file mode 100644
index f32492169..000000000
--- a/lib/mix/tasks/pleroma/benchmark.ex
+++ /dev/null
@@ -1,113 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Mix.Tasks.Pleroma.Benchmark do
- import Mix.Pleroma
- use Mix.Task
-
- def run(["search"]) do
- start_pleroma()
-
- Benchee.run(%{
- "search" => fn ->
- Pleroma.Activity.search(nil, "cofe")
- end
- })
- end
-
- def run(["tag"]) do
- start_pleroma()
-
- Benchee.run(%{
- "tag" => fn ->
- %{"type" => "Create", "tag" => "cofe"}
- |> Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities()
- end
- })
- end
-
- def run(["render_timeline", nickname | _] = args) do
- start_pleroma()
- user = Pleroma.User.get_by_nickname(nickname)
-
- activities =
- %{}
- |> Map.put("type", ["Create", "Announce"])
- |> Map.put("blocking_user", user)
- |> Map.put("muting_user", user)
- |> Map.put("user", user)
- |> Map.put("limit", 4096)
- |> Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities()
- |> Enum.reverse()
-
- inputs = %{
- "1 activity" => Enum.take_random(activities, 1),
- "10 activities" => Enum.take_random(activities, 10),
- "20 activities" => Enum.take_random(activities, 20),
- "40 activities" => Enum.take_random(activities, 40),
- "80 activities" => Enum.take_random(activities, 80)
- }
-
- inputs =
- if Enum.at(args, 2) == "extended" do
- Map.merge(inputs, %{
- "200 activities" => Enum.take_random(activities, 200),
- "500 activities" => Enum.take_random(activities, 500),
- "2000 activities" => Enum.take_random(activities, 2000),
- "4096 activities" => Enum.take_random(activities, 4096)
- })
- else
- inputs
- end
-
- Benchee.run(
- %{
- "Standart rendering" => fn activities ->
- Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{
- activities: activities,
- for: user,
- as: :activity
- })
- end
- },
- inputs: inputs
- )
- end
-
- def run(["adapters"]) do
- start_pleroma()
-
- :ok =
- Pleroma.Gun.Conn.open(
- "https://httpbin.org/stream-bytes/1500",
- :gun_connections
- )
-
- Process.sleep(1_500)
-
- Benchee.run(
- %{
- "Without conn and without pool" => fn ->
- {:ok, %Tesla.Env{}} =
- Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500", [],
- 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", [], receive_conn: false)
- end,
- "With reused conn and without pool" => fn ->
- {:ok, %Tesla.Env{}} =
- 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")
- end
- },
- parallel: 10
- )
- end
-end
diff --git a/lib/mix/tasks/pleroma/config.ex b/lib/mix/tasks/pleroma/config.ex
index 3a2ea44f8..8b3b2f18b 100644
--- a/lib/mix/tasks/pleroma/config.ex
+++ b/lib/mix/tasks/pleroma/config.ex
@@ -205,6 +205,35 @@ defmodule Mix.Tasks.Pleroma.Config do
end
end
+ # Removes any policies that are not a real module
+ # as they will prevent the server from starting
+ def run(["fix_mrf_policies"]) do
+ check_configdb(fn ->
+ start_pleroma()
+
+ group = :pleroma
+ key = :mrf
+
+ %{value: value} =
+ group
+ |> ConfigDB.get_by_group_and_key(key)
+
+ policies =
+ Keyword.get(value, :policies, [])
+ |> Enum.filter(&is_atom(&1))
+ |> Enum.filter(fn mrf ->
+ case Code.ensure_compiled(mrf) do
+ {:module, _} -> true
+ {:error, _} -> false
+ end
+ end)
+
+ value = Keyword.put(value, :policies, policies)
+
+ ConfigDB.update_or_create(%{group: group, key: key, value: value})
+ end)
+ end
+
@spec migrate_to_db(Path.t() | nil) :: any()
def migrate_to_db(file_path \\ nil) do
with :ok <- Pleroma.Config.DeprecationWarnings.warn() do
diff --git a/lib/mix/tasks/pleroma/database.ex b/lib/mix/tasks/pleroma/database.ex
index ed560c177..b82d1f079 100644
--- a/lib/mix/tasks/pleroma/database.ex
+++ b/lib/mix/tasks/pleroma/database.ex
@@ -67,43 +67,168 @@ defmodule Mix.Tasks.Pleroma.Database do
OptionParser.parse(
args,
strict: [
- vacuum: :boolean
+ vacuum: :boolean,
+ keep_threads: :boolean,
+ keep_non_public: :boolean,
+ prune_orphaned_activities: :boolean
]
)
start_pleroma()
deadline = Pleroma.Config.get([:instance, :remote_post_retention_days])
+ time_deadline = NaiveDateTime.utc_now() |> NaiveDateTime.add(-(deadline * 86_400))
- Logger.info("Pruning objects older than #{deadline} days")
+ log_message = "Pruning objects older than #{deadline} days"
- time_deadline =
- NaiveDateTime.utc_now()
- |> NaiveDateTime.add(-(deadline * 86_400))
+ log_message =
+ if Keyword.get(options, :keep_non_public) do
+ log_message <> ", keeping non public posts"
+ else
+ log_message
+ end
- from(o in Object,
- where:
- fragment(
- "?->'to' \\? ? OR ?->'cc' \\? ?",
- o.data,
- ^Pleroma.Constants.as_public(),
- o.data,
- ^Pleroma.Constants.as_public()
- ),
- where: o.inserted_at < ^time_deadline,
- where:
+ log_message =
+ if Keyword.get(options, :keep_threads) do
+ log_message <> ", keeping threads intact"
+ else
+ log_message
+ end
+
+ log_message =
+ if Keyword.get(options, :prune_orphaned_activities) do
+ log_message <> ", pruning orphaned activities"
+ else
+ log_message
+ end
+
+ log_message =
+ if Keyword.get(options, :vacuum) do
+ log_message <>
+ ", doing a full vacuum (you shouldn't do this as a recurring maintanance task)"
+ else
+ log_message
+ end
+
+ Logger.info(log_message)
+
+ if Keyword.get(options, :keep_threads) do
+ # We want to delete objects from threads where
+ # 1. the newest post is still old
+ # 2. none of the activities is local
+ # 3. none of the activities is bookmarked
+ # 4. optionally none of the posts is non-public
+ deletable_context =
+ if Keyword.get(options, :keep_non_public) do
+ Pleroma.Activity
+ |> join(:left, [a], b in Pleroma.Bookmark, on: a.id == b.activity_id)
+ |> group_by([a], fragment("? ->> 'context'::text", a.data))
+ |> having(
+ [a],
+ not fragment(
+ # Posts (checked on Create Activity) is non-public
+ "bool_or((not(?->'to' \\? ? OR ?->'cc' \\? ?)) and ? ->> 'type' = 'Create')",
+ a.data,
+ ^Pleroma.Constants.as_public(),
+ a.data,
+ ^Pleroma.Constants.as_public(),
+ a.data
+ )
+ )
+ else
+ Pleroma.Activity
+ |> join(:left, [a], b in Pleroma.Bookmark, on: a.id == b.activity_id)
+ |> group_by([a], fragment("? ->> 'context'::text", a.data))
+ end
+ |> having([a], max(a.updated_at) < ^time_deadline)
+ |> having([a], not fragment("bool_or(?)", a.local))
+ |> having([_, b], fragment("max(?::text) is null", b.id))
+ |> select([a], fragment("? ->> 'context'::text", a.data))
+
+ Pleroma.Object
+ |> where([o], fragment("? ->> 'context'::text", o.data) in subquery(deletable_context))
+ else
+ if Keyword.get(options, :keep_non_public) do
+ Pleroma.Object
+ |> where(
+ [o],
+ fragment(
+ "?->'to' \\? ? OR ?->'cc' \\? ?",
+ o.data,
+ ^Pleroma.Constants.as_public(),
+ o.data,
+ ^Pleroma.Constants.as_public()
+ )
+ )
+ else
+ Pleroma.Object
+ end
+ |> where([o], o.updated_at < ^time_deadline)
+ |> where(
+ [o],
fragment("split_part(?->>'actor', '/', 3) != ?", o.data, ^Pleroma.Web.Endpoint.host())
- )
+ )
+ end
|> Repo.delete_all(timeout: :infinity)
- prune_hashtags_query = """
+ if !Keyword.get(options, :keep_threads) do
+ # Without the --keep-threads option, it's possible that bookmarked
+ # objects have been deleted. We remove the corresponding bookmarks.
+ """
+ delete from public.bookmarks
+ where id in (
+ select b.id from public.bookmarks b
+ left join public.activities a on b.activity_id = a.id
+ left join public.objects o on a."data" ->> 'object' = o.data ->> 'id'
+ where o.id is null
+ )
+ """
+ |> Repo.query([], timeout: :infinity)
+ end
+
+ if Keyword.get(options, :prune_orphaned_activities) do
+ # Prune activities who link to a single object
+ """
+ delete from public.activities
+ where id in (
+ select a.id from public.activities a
+ left join public.objects o on a.data ->> 'object' = o.data ->> 'id'
+ left join public.activities a2 on a.data ->> 'object' = a2.data ->> 'id'
+ left join public.users u on a.data ->> 'object' = u.ap_id
+ where not a.local
+ and jsonb_typeof(a."data" -> 'object') = 'string'
+ and o.id is null
+ and a2.id is null
+ and u.id is null
+ )
+ """
+ |> Repo.query([], timeout: :infinity)
+
+ # Prune activities who link to an array of objects
+ """
+ delete from public.activities
+ where id in (
+ select a.id from public.activities a
+ join json_array_elements_text((a."data" -> 'object')::json) as j on jsonb_typeof(a."data" -> 'object') = 'array'
+ left join public.objects o on j.value = o.data ->> 'id'
+ left join public.activities a2 on j.value = a2.data ->> 'id'
+ left join public.users u on j.value = u.ap_id
+ group by a.id
+ having max(o.data ->> 'id') is null
+ and max(a2.data ->> 'id') is null
+ and max(u.ap_id) is null
+ )
+ """
+ |> Repo.query([], timeout: :infinity)
+ end
+
+ """
DELETE FROM hashtags AS ht
WHERE NOT EXISTS (
SELECT 1 FROM hashtags_objects hto
WHERE ht.id = hto.hashtag_id)
"""
-
- Repo.query(prune_hashtags_query)
+ |> Repo.query()
if Keyword.get(options, :vacuum) do
Maintenance.vacuum("full")
@@ -193,7 +318,7 @@ defmodule Mix.Tasks.Pleroma.Database do
"ALTER DATABASE #{db} SET default_text_search_config = '#{tsconfig}';"
)
- # non-exist config will not raise excpetion but only give >0 messages
+ # non-exist config will not raise exception but only give >0 messages
if length(msg) > 0 do
shell_info("Error: #{inspect(msg, pretty: true)}")
else
@@ -226,7 +351,7 @@ defmodule Mix.Tasks.Pleroma.Database do
)
end
- shell_info('Done.')
+ shell_info(~c"Done.")
end
end
diff --git a/lib/mix/tasks/pleroma/digest.ex b/lib/mix/tasks/pleroma/digest.ex
index aea9c8ac5..53cac0b94 100644
--- a/lib/mix/tasks/pleroma/digest.ex
+++ b/lib/mix/tasks/pleroma/digest.ex
@@ -30,7 +30,7 @@ defmodule Mix.Tasks.Pleroma.Digest do
shell_info("Digest email have been sent to #{nickname} (#{user.email})")
else
_ ->
- shell_info("Cound't find any mentions for #{nickname} since #{last_digest_emailed_at}")
+ shell_info("Couldn't find any mentions for #{nickname} since #{last_digest_emailed_at}")
end
end
end
diff --git a/lib/mix/tasks/pleroma/ecto/rollback.ex b/lib/mix/tasks/pleroma/ecto/rollback.ex
index 3d78eaec4..121890f39 100644
--- a/lib/mix/tasks/pleroma/ecto/rollback.ex
+++ b/lib/mix/tasks/pleroma/ecto/rollback.ex
@@ -61,7 +61,7 @@ defmodule Mix.Tasks.Pleroma.Ecto.Rollback do
Logger.configure(level: :info)
if opts[:env] == "test" do
- Logger.info("Rollback succesfully")
+ Logger.info("Rollback successfully")
else
{:ok, _, _} =
Ecto.Migrator.with_repo(Pleroma.Repo, &Ecto.Migrator.run(&1, path, :down, opts))
diff --git a/lib/mix/tasks/pleroma/emoji.ex b/lib/mix/tasks/pleroma/emoji.ex
index 537f0715e..8b9c921c8 100644
--- a/lib/mix/tasks/pleroma/emoji.ex
+++ b/lib/mix/tasks/pleroma/emoji.ex
@@ -111,7 +111,7 @@ defmodule Mix.Tasks.Pleroma.Emoji do
{:ok, _} =
:zip.unzip(binary_archive,
- cwd: pack_path,
+ cwd: String.to_charlist(pack_path),
file_list: files_to_unzip
)
diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex
index 5d8b254a2..0dc30549c 100644
--- a/lib/mix/tasks/pleroma/instance.ex
+++ b/lib/mix/tasks/pleroma/instance.ex
@@ -292,7 +292,7 @@ defmodule Mix.Tasks.Pleroma.Instance do
if db_configurable? do
shell_info(
- " Please transfer your config to the database after running database migrations. Refer to \"Transfering the config to/from the database\" section of the docs for more information."
+ " Please transfer your config to the database after running database migrations. Refer to \"Transferring the config to/from the database\" section of the docs for more information."
)
end
else
@@ -352,6 +352,4 @@ defmodule Mix.Tasks.Pleroma.Instance do
enabled_filters
end
-
- defp upload_filters(_), do: []
end
diff --git a/lib/mix/tasks/pleroma/search/indexer.ex b/lib/mix/tasks/pleroma/search/indexer.ex
new file mode 100644
index 000000000..2a52472f9
--- /dev/null
+++ b/lib/mix/tasks/pleroma/search/indexer.ex
@@ -0,0 +1,83 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Mix.Tasks.Pleroma.Search.Indexer do
+ import Mix.Pleroma
+ import Ecto.Query
+
+ alias Pleroma.Workers.SearchIndexingWorker
+
+ def run(["create_index"]) do
+ start_pleroma()
+
+ with :ok <- Pleroma.Config.get([Pleroma.Search, :module]).create_index() do
+ IO.puts("Index created")
+ else
+ e -> IO.puts("Could not create index: #{inspect(e)}")
+ end
+ end
+
+ def run(["drop_index"]) do
+ start_pleroma()
+
+ with :ok <- Pleroma.Config.get([Pleroma.Search, :module]).drop_index() do
+ IO.puts("Index dropped")
+ else
+ e -> IO.puts("Could not drop index: #{inspect(e)}")
+ end
+ end
+
+ def run(["index" | options]) do
+ {options, [], []} =
+ OptionParser.parse(
+ options,
+ strict: [
+ chunk: :integer,
+ limit: :integer,
+ step: :integer
+ ]
+ )
+
+ start_pleroma()
+
+ chunk_size = Keyword.get(options, :chunk, 100)
+ limit = Keyword.get(options, :limit, 100_000)
+ per_step = Keyword.get(options, :step, 1000)
+
+ chunks = max(div(limit, per_step), 1)
+
+ 1..chunks
+ |> Enum.each(fn step ->
+ q =
+ from(a in Pleroma.Activity,
+ limit: ^per_step,
+ offset: ^per_step * (^step - 1),
+ select: [:id],
+ order_by: [desc: :id]
+ )
+
+ {:ok, ids} =
+ Pleroma.Repo.transaction(fn ->
+ Pleroma.Repo.stream(q, timeout: :infinity)
+ |> Enum.map(fn a ->
+ a.id
+ end)
+ end)
+
+ IO.puts("Got #{length(ids)} activities, adding to indexer")
+
+ ids
+ |> Enum.chunk_every(chunk_size)
+ |> Enum.each(fn chunk ->
+ IO.puts("Adding #{length(chunk)} activities to indexing queue")
+
+ chunk
+ |> Enum.map(fn id ->
+ SearchIndexingWorker.new(%{"op" => "add_to_index", "activity" => id})
+ end)
+ |> Oban.insert_all()
+ end)
+ end)
+ end
+end
diff --git a/lib/mix/tasks/pleroma/search/meilisearch.ex b/lib/mix/tasks/pleroma/search/meilisearch.ex
new file mode 100644
index 000000000..8379a0c25
--- /dev/null
+++ b/lib/mix/tasks/pleroma/search/meilisearch.ex
@@ -0,0 +1,145 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Mix.Tasks.Pleroma.Search.Meilisearch do
+ require Pleroma.Constants
+
+ import Mix.Pleroma
+ import Ecto.Query
+
+ import Pleroma.Search.Meilisearch,
+ only: [meili_post: 2, meili_put: 2, meili_get: 1, meili_delete: 1]
+
+ def run(["index"]) do
+ start_pleroma()
+ Pleroma.HTML.compile_scrubbers()
+
+ meili_version =
+ (
+ {:ok, result} = meili_get("/version")
+
+ result["pkgVersion"]
+ )
+
+ # The ranking rule syntax was changed but nothing about that is mentioned in the changelog
+ if not Version.match?(meili_version, ">= 0.25.0") do
+ raise "Meilisearch <0.24.0 not supported"
+ end
+
+ {:ok, _} =
+ meili_post(
+ "/indexes/objects/settings/ranking-rules",
+ [
+ "published:desc",
+ "words",
+ "exactness",
+ "proximity",
+ "typo",
+ "attribute",
+ "sort"
+ ]
+ )
+
+ {:ok, _} =
+ meili_post(
+ "/indexes/objects/settings/searchable-attributes",
+ [
+ "content"
+ ]
+ )
+
+ IO.puts("Created indices. Starting to insert posts.")
+
+ chunk_size = Pleroma.Config.get([Pleroma.Search.Meilisearch, :initial_indexing_chunk_size])
+
+ Pleroma.Repo.transaction(
+ fn ->
+ query =
+ from(Pleroma.Object,
+ # Only index public and unlisted posts which are notes and have some text
+ where:
+ fragment("data->>'type' = 'Note'") and
+ (fragment("data->'to' \\? ?", ^Pleroma.Constants.as_public()) or
+ fragment("data->'cc' \\? ?", ^Pleroma.Constants.as_public())),
+ order_by: [desc: fragment("data->'published'")]
+ )
+
+ count = query |> Pleroma.Repo.aggregate(:count, :data)
+ IO.puts("Entries to index: #{count}")
+
+ Pleroma.Repo.stream(
+ query,
+ timeout: :infinity
+ )
+ |> Stream.map(&Pleroma.Search.Meilisearch.object_to_search_data/1)
+ |> Stream.filter(fn o -> not is_nil(o) end)
+ |> Stream.chunk_every(chunk_size)
+ |> Stream.transform(0, fn objects, acc ->
+ new_acc = acc + Enum.count(objects)
+
+ # Reset to the beginning of the line and rewrite it
+ IO.write("\r")
+ IO.write("Indexed #{new_acc} entries")
+
+ {[objects], new_acc}
+ end)
+ |> Stream.each(fn objects ->
+ result =
+ meili_put(
+ "/indexes/objects/documents",
+ objects
+ )
+
+ with {:ok, res} <- result do
+ if not Map.has_key?(res, "uid") do
+ IO.puts("\nFailed to index: #{inspect(result)}")
+ end
+ else
+ e -> IO.puts("\nFailed to index due to network error: #{inspect(e)}")
+ end
+ end)
+ |> Stream.run()
+ end,
+ timeout: :infinity
+ )
+
+ IO.write("\n")
+ end
+
+ def run(["clear"]) do
+ start_pleroma()
+
+ meili_delete("/indexes/objects/documents")
+ end
+
+ def run(["show-keys", master_key]) do
+ start_pleroma()
+
+ endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url])
+
+ {:ok, result} =
+ Pleroma.HTTP.get(
+ Path.join(endpoint, "/keys"),
+ [{"Authorization", "Bearer #{master_key}"}]
+ )
+
+ decoded = Jason.decode!(result.body)
+
+ if decoded["results"] do
+ Enum.each(decoded["results"], fn %{"description" => desc, "key" => key} ->
+ IO.puts("#{desc}: #{key}")
+ end)
+ else
+ IO.puts("Error fetching the keys, check the master key is correct: #{inspect(decoded)}")
+ end
+ end
+
+ def run(["stats"]) do
+ start_pleroma()
+
+ {:ok, result} = meili_get("/indexes/objects/stats")
+ IO.puts("Number of entries: #{result["numberOfDocuments"]}")
+ IO.puts("Indexing? #{result["isIndexing"]}")
+ end
+end
diff --git a/lib/mix/tasks/pleroma/test_runner.ex b/lib/mix/tasks/pleroma/test_runner.ex
new file mode 100644
index 000000000..69fefb001
--- /dev/null
+++ b/lib/mix/tasks/pleroma/test_runner.ex
@@ -0,0 +1,25 @@
+defmodule Mix.Tasks.Pleroma.TestRunner do
+ @shortdoc "Retries tests once if they fail"
+
+ use Mix.Task
+
+ def run(args \\ []) do
+ case System.cmd("mix", ["test"] ++ args, into: IO.stream(:stdio, :line)) do
+ {_, 0} ->
+ :ok
+
+ _ ->
+ retry(args)
+ end
+ end
+
+ def retry(args) do
+ case System.cmd("mix", ["test", "--failed"] ++ args, into: IO.stream(:stdio, :line)) do
+ {_, 0} ->
+ :ok
+
+ _ ->
+ exit(1)
+ end
+ end
+end
diff --git a/lib/phoenix/transports/web_socket/raw.ex b/lib/phoenix/transports/web_socket/raw.ex
deleted file mode 100644
index 8cf9c32a2..000000000
--- a/lib/phoenix/transports/web_socket/raw.ex
+++ /dev/null
@@ -1,94 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Phoenix.Transports.WebSocket.Raw do
- import Plug.Conn,
- only: [
- fetch_query_params: 1,
- send_resp: 3
- ]
-
- alias Phoenix.Socket.Transport
-
- def default_config do
- [
- timeout: 60_000,
- transport_log: false,
- cowboy: Phoenix.Endpoint.CowboyWebSocket
- ]
- end
-
- def init(%Plug.Conn{method: "GET"} = conn, {endpoint, handler, transport}) do
- {_, opts} = handler.__transport__(transport)
-
- conn =
- conn
- |> fetch_query_params
- |> Transport.transport_log(opts[:transport_log])
- |> Transport.force_ssl(handler, endpoint, opts)
- |> Transport.check_origin(handler, endpoint, opts)
-
- case conn do
- %{halted: false} = conn ->
- case handler.connect(%{
- endpoint: endpoint,
- transport: transport,
- options: [serializer: nil],
- params: conn.params
- }) do
- {:ok, socket} ->
- {:ok, conn, {__MODULE__, {socket, opts}}}
-
- :error ->
- send_resp(conn, :forbidden, "")
- {:error, conn}
- end
-
- _ ->
- {:error, conn}
- end
- end
-
- def init(conn, _) do
- send_resp(conn, :bad_request, "")
- {:error, conn}
- end
-
- def ws_init({socket, config}) do
- Process.flag(:trap_exit, true)
- {:ok, %{socket: socket}, config[:timeout]}
- end
-
- def ws_handle(op, data, state) do
- state.socket.handler
- |> apply(:handle, [op, data, state])
- |> case do
- {op, data} ->
- {:reply, {op, data}, state}
-
- {op, data, state} ->
- {:reply, {op, data}, state}
-
- %{} = state ->
- {:ok, state}
-
- _ ->
- {:ok, state}
- end
- end
-
- def ws_info({_, _} = tuple, state) do
- {:reply, tuple, state}
- end
-
- def ws_info(_tuple, state), do: {:ok, state}
-
- def ws_close(state) do
- ws_handle(:closed, :normal, state)
- end
-
- def ws_terminate(reason, state) do
- ws_handle(:closed, reason, state)
- end
-end
diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex
index 3556aaf9e..8a512dc57 100644
--- a/lib/pleroma/activity.ex
+++ b/lib/pleroma/activity.ex
@@ -368,7 +368,7 @@ defmodule Pleroma.Activity do
)
end
- defdelegate search(user, query, options \\ []), to: Pleroma.Activity.Search
+ defdelegate search(user, query, options \\ []), to: Pleroma.Search.DatabaseSearch
def direct_conversation_id(activity, for_user) do
alias Pleroma.Conversation.Participation
diff --git a/lib/pleroma/activity/html.ex b/lib/pleroma/activity/html.ex
index 706b2d36c..ba284b4d5 100644
--- a/lib/pleroma/activity/html.ex
+++ b/lib/pleroma/activity/html.ex
@@ -28,7 +28,7 @@ defmodule Pleroma.Activity.HTML do
end
end
- defp add_cache_key_for(activity_id, additional_key) do
+ def add_cache_key_for(activity_id, additional_key) do
current = get_cache_keys_for(activity_id)
unless additional_key in current do
diff --git a/lib/pleroma/activity/queries.ex b/lib/pleroma/activity/queries.ex
index 81c44ac05..d770b9ff3 100644
--- a/lib/pleroma/activity/queries.ex
+++ b/lib/pleroma/activity/queries.ex
@@ -9,7 +9,7 @@ defmodule Pleroma.Activity.Queries do
import Ecto.Query, only: [from: 2, where: 3]
- @type query :: Ecto.Queryable.t() | Activity.t()
+ @type query :: Ecto.Queryable.t() | Pleroma.Activity.t()
alias Pleroma.Activity
alias Pleroma.User
diff --git a/lib/pleroma/announcement.ex b/lib/pleroma/announcement.ex
index d97c5e728..5a3c710e8 100644
--- a/lib/pleroma/announcement.ex
+++ b/lib/pleroma/announcement.ex
@@ -23,19 +23,21 @@ defmodule Pleroma.Announcement do
timestamps(type: :utc_datetime)
end
- def change(struct, params \\ %{}) do
- struct
- |> cast(validate_params(struct, params), [:data, :starts_at, :ends_at, :rendered])
+ @doc "Generates changeset for %Pleroma.Announcement{}"
+ @spec changeset(%__MODULE__{}, map()) :: %Ecto.Changeset{}
+ def changeset(announcement \\ %__MODULE__{}, params \\ %{data: %{}}) do
+ announcement
+ |> cast(validate_params(announcement, params), [:data, :starts_at, :ends_at, :rendered])
|> validate_required([:data])
end
- defp validate_params(struct, params) do
+ defp validate_params(announcement, params) do
base_data =
%{
"content" => "",
"all_day" => false
}
- |> Map.merge((struct && struct.data) || %{})
+ |> Map.merge((announcement && announcement.data) || %{})
merged_data =
Map.merge(base_data, params.data)
@@ -61,13 +63,13 @@ defmodule Pleroma.Announcement do
end
def add(params) do
- changeset = change(%__MODULE__{}, params)
+ changeset = changeset(%__MODULE__{}, params)
Repo.insert(changeset)
end
def update(announcement, params) do
- changeset = change(announcement, params)
+ changeset = changeset(announcement, params)
Repo.update(changeset)
end
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index 385e3872d..cb15dc1e9 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -14,7 +14,6 @@ defmodule Pleroma.Application do
@name Mix.Project.config()[:name]
@version Mix.Project.config()[:version]
@repository Mix.Project.config()[:source_url]
- @mix_env Mix.env()
def name, do: @name
def version, do: @version
@@ -52,9 +51,12 @@ defmodule Pleroma.Application do
Pleroma.HTML.compile_scrubbers()
Pleroma.Config.Oban.warn()
Config.DeprecationWarnings.warn()
- Pleroma.Web.Plugs.HTTPSecurityPlug.warn_if_disabled()
+
+ if Config.get([Pleroma.Web.Plugs.HTTPSecurityPlug, :enable], true) do
+ Pleroma.Web.Plugs.HTTPSecurityPlug.warn_if_disabled()
+ end
+
Pleroma.ApplicationRequirements.verify!()
- setup_instrumenters()
load_custom_modules()
Pleroma.Docs.JSON.compile()
limiters_setup()
@@ -91,6 +93,7 @@ defmodule Pleroma.Application do
# Define workers and child supervisors to be supervised
children =
[
+ Pleroma.PromEx,
Pleroma.Repo,
Config.TransferTask,
Pleroma.Emoji,
@@ -98,7 +101,7 @@ defmodule Pleroma.Application do
{Task.Supervisor, name: Pleroma.TaskSupervisor}
] ++
cachex_children() ++
- http_children(adapter, @mix_env) ++
+ http_children(adapter) ++
[
Pleroma.Stats,
Pleroma.JobQueueMonitor,
@@ -106,46 +109,22 @@ defmodule Pleroma.Application do
{Oban, Config.get(Oban)},
Pleroma.Web.Endpoint
] ++
- task_children(@mix_env) ++
- dont_run_in_test(@mix_env) ++
+ task_children() ++
+ streamer_registry() ++
+ background_migrators() ++
shout_child(shout_enabled?()) ++
- [Pleroma.Gopher.Server]
+ [Pleroma.Gopher.Server] ++
+ [Pleroma.Search.Healthcheck]
# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
# for other strategies and supported options
# If we have a lot of caches, default max_restarts can cause test
# resets to fail.
# Go for the default 3 unless we're in test
- max_restarts =
- if @mix_env == :test do
- 100
- else
- 3
- end
+ max_restarts = Application.get_env(:pleroma, __MODULE__)[:max_restarts]
opts = [strategy: :one_for_one, name: Pleroma.Supervisor, max_restarts: max_restarts]
- result = Supervisor.start_link(children, opts)
-
- set_postgres_server_version()
-
- result
- end
-
- defp set_postgres_server_version do
- version =
- with %{rows: [[version]]} <- Ecto.Adapters.SQL.query!(Pleroma.Repo, "show server_version"),
- {num, _} <- Float.parse(version) do
- num
- else
- e ->
- Logger.warn(
- "Could not get the postgres version: #{inspect(e)}.\nSetting the default value of 9.6"
- )
-
- 9.6
- end
-
- :persistent_term.put({Pleroma.Repo, :postgres_version}, version)
+ Supervisor.start_link(children, opts)
end
def load_custom_modules do
@@ -159,7 +138,7 @@ defmodule Pleroma.Application do
raise "Invalid custom modules"
{:ok, modules, _warnings} ->
- if @mix_env != :test do
+ if Application.get_env(:pleroma, __MODULE__)[:load_custom_modules] do
Enum.each(modules, fn mod ->
Logger.info("Custom module loaded: #{inspect(mod)}")
end)
@@ -170,29 +149,6 @@ defmodule Pleroma.Application do
end
end
- defp setup_instrumenters do
- require Prometheus.Registry
-
- if Application.get_env(:prometheus, Pleroma.Repo.Instrumenter) do
- :ok =
- :telemetry.attach(
- "prometheus-ecto",
- [:pleroma, :repo, :query],
- &Pleroma.Repo.Instrumenter.handle_event/4,
- %{}
- )
-
- Pleroma.Repo.Instrumenter.setup()
- end
-
- Pleroma.Web.Endpoint.MetricsExporter.setup()
- Pleroma.Web.Endpoint.PipelineInstrumenter.setup()
-
- # Note: disabled until prometheus-phx is integrated into prometheus-phoenix:
- # Pleroma.Web.Endpoint.Instrumenter.setup()
- PrometheusPhx.setup()
- end
-
defp cachex_children do
[
build_cachex("used_captcha", ttl_interval: seconds_valid_interval()),
@@ -205,6 +161,7 @@ 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("failed_media_helper_url", default_ttl: :timer.minutes(15), limit: 2_500),
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(),
@@ -237,24 +194,30 @@ defmodule Pleroma.Application do
defp shout_enabled?, do: Config.get([:shout, :enabled])
- defp dont_run_in_test(env) when env in [:test, :benchmark], do: []
-
- defp dont_run_in_test(_) do
- [
- {Registry,
- [
- name: Pleroma.Web.Streamer.registry(),
- keys: :duplicate,
- partitions: System.schedulers_online()
- ]}
- ] ++ background_migrators()
+ defp streamer_registry do
+ if Application.get_env(:pleroma, __MODULE__)[:streamer_registry] do
+ [
+ {Registry,
+ [
+ name: Pleroma.Web.Streamer.registry(),
+ keys: :duplicate,
+ partitions: System.schedulers_online()
+ ]}
+ ]
+ else
+ []
+ end
end
defp background_migrators do
- [
- Pleroma.Migrators.HashtagsTableMigrator,
- Pleroma.Migrators.ContextObjectsDeletionMigrator
- ]
+ if Application.get_env(:pleroma, __MODULE__)[:background_migrators] do
+ [
+ Pleroma.Migrators.HashtagsTableMigrator,
+ Pleroma.Migrators.ContextObjectsDeletionMigrator
+ ]
+ else
+ []
+ end
end
defp shout_child(true) do
@@ -266,37 +229,43 @@ defmodule Pleroma.Application do
defp shout_child(_), do: []
- defp task_children(:test) do
- [
+ defp task_children do
+ children = [
%{
id: :web_push_init,
start: {Task, :start_link, [&Pleroma.Web.Push.init/0]},
restart: :temporary
}
]
- end
- defp task_children(_) do
- [
- %{
- id: :web_push_init,
- start: {Task, :start_link, [&Pleroma.Web.Push.init/0]},
- restart: :temporary
- },
- %{
- id: :internal_fetch_init,
- start: {Task, :start_link, [&Pleroma.Web.ActivityPub.InternalFetchActor.init/0]},
- restart: :temporary
- }
- ]
+ if Application.get_env(:pleroma, __MODULE__)[:internal_fetch] do
+ children ++
+ [
+ %{
+ id: :internal_fetch_init,
+ start: {Task, :start_link, [&Pleroma.Web.ActivityPub.InternalFetchActor.init/0]},
+ restart: :temporary
+ }
+ ]
+ else
+ children
+ end
end
# start hackney and gun pools in tests
- defp http_children(_, :test) do
- http_children(Tesla.Adapter.Hackney, nil) ++ http_children(Tesla.Adapter.Gun, nil)
+ defp http_children(adapter) do
+ if Application.get_env(:pleroma, __MODULE__)[:test_http_pools] do
+ http_children_hackney() ++ http_children_gun()
+ else
+ cond do
+ match?(Tesla.Adapter.Hackney, adapter) -> http_children_hackney()
+ match?(Tesla.Adapter.Gun, adapter) -> http_children_gun()
+ true -> []
+ end
+ end
end
- defp http_children(Tesla.Adapter.Hackney, _) do
+ defp http_children_hackney do
pools = [:federation, :media]
pools =
@@ -312,18 +281,18 @@ defmodule Pleroma.Application do
end
end
- defp http_children(Tesla.Adapter.Gun, _) do
+ defp http_children_gun do
Pleroma.Gun.ConnectionPool.children() ++
[{Task, &Pleroma.HTTP.AdapterHelper.Gun.limiter_setup/0}]
end
- defp http_children(_, _), do: []
-
@spec limiters_setup() :: :ok
def limiters_setup do
config = Config.get(ConcurrentLimiter, [])
- [Pleroma.Web.RichMedia.Helpers, Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy]
+ [
+ Pleroma.Search
+ ]
|> Enum.each(fn module ->
mod_config = Keyword.get(config, module, [])
diff --git a/lib/pleroma/application_requirements.ex b/lib/pleroma/application_requirements.ex
index 44b1c1705..a334d12ee 100644
--- a/lib/pleroma/application_requirements.ex
+++ b/lib/pleroma/application_requirements.ex
@@ -7,7 +7,10 @@ defmodule Pleroma.ApplicationRequirements do
The module represents the collection of validations to runs before start server.
"""
- defmodule VerifyError, do: defexception([:message])
+ defmodule VerifyError do
+ defexception([:message])
+ @type t :: %__MODULE__{}
+ end
alias Pleroma.Config
alias Pleroma.Helpers.MediaHelper
@@ -25,6 +28,7 @@ defmodule Pleroma.ApplicationRequirements do
|> check_welcome_message_config!()
|> check_rum!()
|> check_repo_pool_size!()
+ |> check_mrfs()
|> handle_result()
end
@@ -34,7 +38,7 @@ defmodule Pleroma.ApplicationRequirements do
defp check_welcome_message_config!(:ok) do
if Pleroma.Config.get([:welcome, :email, :enabled], false) and
not Pleroma.Emails.Mailer.enabled?() do
- Logger.warn("""
+ Logger.warning("""
To send welcome emails, you need to enable the mailer.
Welcome emails will NOT be sent with the current config.
@@ -53,7 +57,7 @@ defmodule Pleroma.ApplicationRequirements do
def check_confirmation_accounts!(:ok) do
if Pleroma.Config.get([:instance, :account_activation_required]) &&
not Pleroma.Emails.Mailer.enabled?() do
- Logger.warn("""
+ Logger.warning("""
Account activation is required, but the mailer is disabled.
Users will NOT be able to confirm their accounts with this config.
Either disable account activation or enable the mailer.
@@ -168,8 +172,6 @@ defmodule Pleroma.ApplicationRequirements do
check_filter(Pleroma.Upload.Filter.Exiftool.ReadDescription, "exiftool"),
check_filter(Pleroma.Upload.Filter.Mogrify, "mogrify"),
check_filter(Pleroma.Upload.Filter.Mogrifun, "mogrify"),
- check_filter(Pleroma.Upload.Filter.AnalyzeMetadata, "mogrify"),
- check_filter(Pleroma.Upload.Filter.AnalyzeMetadata, "convert"),
check_filter(Pleroma.Upload.Filter.AnalyzeMetadata, "ffprobe")
]
@@ -195,8 +197,6 @@ defmodule Pleroma.ApplicationRequirements do
end
end
- defp check_system_commands!(result), do: result
-
defp check_repo_pool_size!(:ok) do
if Pleroma.Config.get([Pleroma.Repo, :pool_size], 10) != 10 and
not Pleroma.Config.get([:dangerzone, :override_repo_pool_size], false) do
@@ -235,4 +235,24 @@ defmodule Pleroma.ApplicationRequirements do
true
end
end
+
+ defp check_mrfs(:ok) do
+ mrfs = Config.get!([:mrf, :policies])
+
+ missing_mrfs =
+ Enum.reduce(mrfs, [], fn x, acc ->
+ case Code.ensure_compiled(x) do
+ {:module, _} -> acc
+ {:error, _} -> acc ++ [x]
+ end
+ end)
+
+ if Enum.empty?(missing_mrfs) do
+ :ok
+ else
+ {:error, "The following MRF modules are configured but missing: #{inspect(missing_mrfs)}"}
+ end
+ end
+
+ defp check_mrfs(result), do: result
end
diff --git a/lib/pleroma/bookmark.ex b/lib/pleroma/bookmark.ex
index 187749e86..1a2a63b82 100644
--- a/lib/pleroma/bookmark.ex
+++ b/lib/pleroma/bookmark.ex
@@ -10,6 +10,7 @@ defmodule Pleroma.Bookmark do
alias Pleroma.Activity
alias Pleroma.Bookmark
+ alias Pleroma.BookmarkFolder
alias Pleroma.Repo
alias Pleroma.User
@@ -18,33 +19,46 @@ defmodule Pleroma.Bookmark do
schema "bookmarks" do
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
+ belongs_to(:folder, BookmarkFolder, type: FlakeId.Ecto.CompatType)
timestamps()
end
- @spec create(FlakeId.Ecto.CompatType.t(), FlakeId.Ecto.CompatType.t()) ::
- {:ok, Bookmark.t()} | {:error, Changeset.t()}
- def create(user_id, activity_id) do
+ @spec create(Ecto.UUID.t(), Ecto.UUID.t()) ::
+ {:ok, Bookmark.t()} | {:error, Ecto.Changeset.t()}
+ def create(user_id, activity_id, folder_id \\ nil) do
attrs = %{
user_id: user_id,
- activity_id: activity_id
+ activity_id: activity_id,
+ folder_id: folder_id
}
%Bookmark{}
- |> cast(attrs, [:user_id, :activity_id])
+ |> cast(attrs, [:user_id, :activity_id, :folder_id])
|> validate_required([:user_id, :activity_id])
|> unique_constraint(:activity_id, name: :bookmarks_user_id_activity_id_index)
- |> Repo.insert()
+ |> Repo.insert(
+ on_conflict: [set: [folder_id: folder_id]],
+ conflict_target: [:user_id, :activity_id]
+ )
end
- @spec for_user_query(FlakeId.Ecto.CompatType.t()) :: Ecto.Query.t()
- def for_user_query(user_id) do
+ @spec for_user_query(Ecto.UUID.t()) :: Ecto.Query.t()
+ def for_user_query(user_id, folder_id \\ nil) do
Bookmark
|> where(user_id: ^user_id)
+ |> maybe_filter_by_folder(folder_id)
|> join(:inner, [b], activity in assoc(b, :activity))
|> preload([b, a], activity: a)
end
+ defp maybe_filter_by_folder(query, nil), do: query
+
+ defp maybe_filter_by_folder(query, folder_id) do
+ query
+ |> where(folder_id: ^folder_id)
+ end
+
def get(user_id, activity_id) do
Bookmark
|> where(user_id: ^user_id)
@@ -52,8 +66,8 @@ defmodule Pleroma.Bookmark do
|> Repo.one()
end
- @spec destroy(FlakeId.Ecto.CompatType.t(), FlakeId.Ecto.CompatType.t()) ::
- {:ok, Bookmark.t()} | {:error, Changeset.t()}
+ @spec destroy(Ecto.UUID.t(), Ecto.UUID.t()) ::
+ {:ok, Bookmark.t()} | {:error, Ecto.Changeset.t()}
def destroy(user_id, activity_id) do
from(b in Bookmark,
where: b.user_id == ^user_id,
@@ -62,4 +76,11 @@ defmodule Pleroma.Bookmark do
|> Repo.one()
|> Repo.delete()
end
+
+ def set_folder(bookmark, folder_id) do
+ bookmark
+ |> cast(%{folder_id: folder_id}, [:folder_id])
+ |> validate_required([:folder_id])
+ |> Repo.update()
+ end
end
diff --git a/lib/pleroma/bookmark_folder.ex b/lib/pleroma/bookmark_folder.ex
new file mode 100644
index 000000000..14d37e197
--- /dev/null
+++ b/lib/pleroma/bookmark_folder.ex
@@ -0,0 +1,115 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.BookmarkFolder do
+ use Ecto.Schema
+
+ import Ecto.Changeset
+ import Ecto.Query
+
+ alias Pleroma.BookmarkFolder
+ alias Pleroma.Emoji
+ alias Pleroma.Repo
+ alias Pleroma.User
+
+ @type t :: %__MODULE__{}
+ @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
+
+ schema "bookmark_folders" do
+ field(:name, :string)
+ field(:emoji, :string)
+
+ belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
+
+ timestamps()
+ end
+
+ def get_by_id(id), do: Repo.get_by(BookmarkFolder, id: id)
+
+ def create(user_id, name, emoji \\ nil) do
+ %BookmarkFolder{}
+ |> cast(
+ %{
+ user_id: user_id,
+ name: name,
+ emoji: emoji
+ },
+ [:user_id, :name, :emoji]
+ )
+ |> validate_required([:user_id, :name])
+ |> fix_emoji()
+ |> validate_emoji()
+ |> unique_constraint([:user_id, :name])
+ |> Repo.insert()
+ end
+
+ def update(folder_id, name, emoji \\ nil) do
+ get_by_id(folder_id)
+ |> cast(
+ %{
+ name: name,
+ emoji: emoji
+ },
+ [:name, :emoji]
+ )
+ |> fix_emoji()
+ |> validate_emoji()
+ |> unique_constraint([:user_id, :name])
+ |> Repo.update()
+ end
+
+ defp fix_emoji(changeset) do
+ with {:emoji_field, emoji} when is_binary(emoji) <-
+ {:emoji_field, get_field(changeset, :emoji)},
+ {:fixed_emoji, emoji} <-
+ {:fixed_emoji,
+ emoji
+ |> Pleroma.Emoji.fully_qualify_emoji()
+ |> Pleroma.Emoji.maybe_quote()} do
+ put_change(changeset, :emoji, emoji)
+ else
+ {:emoji_field, _} -> changeset
+ end
+ end
+
+ defp validate_emoji(changeset) do
+ validate_change(changeset, :emoji, fn
+ :emoji, nil ->
+ []
+
+ :emoji, emoji ->
+ if Emoji.unicode?(emoji) or valid_local_custom_emoji?(emoji) do
+ []
+ else
+ [emoji: "Invalid emoji"]
+ end
+ end)
+ end
+
+ defp valid_local_custom_emoji?(emoji) do
+ with %{file: _path} <- Emoji.get(emoji) do
+ true
+ else
+ _ -> false
+ end
+ end
+
+ def delete(folder_id) do
+ BookmarkFolder
+ |> Repo.get_by(id: folder_id)
+ |> Repo.delete()
+ end
+
+ def for_user(user_id) do
+ BookmarkFolder
+ |> where(user_id: ^user_id)
+ |> Repo.all()
+ end
+
+ def belongs_to_user?(folder_id, user_id) do
+ BookmarkFolder
+ |> where(id: ^folder_id, user_id: ^user_id)
+ |> Repo.exists?()
+ end
+end
diff --git a/lib/pleroma/caching.ex b/lib/pleroma/caching.ex
index eb0588708..796a465af 100644
--- a/lib/pleroma/caching.ex
+++ b/lib/pleroma/caching.ex
@@ -8,10 +8,13 @@ defmodule Pleroma.Caching do
@callback put(Cachex.cache(), any(), any(), Keyword.t()) :: {Cachex.status(), boolean()}
@callback put(Cachex.cache(), any(), any()) :: {Cachex.status(), boolean()}
@callback fetch!(Cachex.cache(), any(), function() | nil) :: any()
+ @callback fetch(Cachex.cache(), any(), function() | nil) ::
+ {atom(), any()} | {atom(), any(), any()}
# @callback del(Cachex.cache(), any(), Keyword.t()) :: {Cachex.status(), boolean()}
@callback del(Cachex.cache(), any()) :: {Cachex.status(), boolean()}
@callback stream!(Cachex.cache(), any()) :: Enumerable.t()
@callback expire_at(Cachex.cache(), binary(), number()) :: {Cachex.status(), boolean()}
+ @callback expire(Cachex.cache(), binary(), number()) :: {Cachex.status(), boolean()}
@callback exists?(Cachex.cache(), any()) :: {Cachex.status(), boolean()}
@callback execute!(Cachex.cache(), function()) :: any()
@callback get_and_update(Cachex.cache(), any(), function()) ::
diff --git a/lib/pleroma/captcha/kocaptcha.ex b/lib/pleroma/captcha/kocaptcha.ex
index e786e28b9..c4987d4fd 100644
--- a/lib/pleroma/captcha/kocaptcha.ex
+++ b/lib/pleroma/captcha/kocaptcha.ex
@@ -29,7 +29,7 @@ defmodule Pleroma.Captcha.Kocaptcha do
@impl Service
def validate(_token, captcha, answer_data) do
- # Here the token is unsed, because the unencrypted captcha answer is just passed to method
+ # Here the token is unused, because the unencrypted captcha answer is just passed to method
if not is_nil(captcha) and
:crypto.hash(:md5, captcha) |> Base.encode16() == String.upcase(answer_data),
do: :ok,
diff --git a/lib/pleroma/chat.ex b/lib/pleroma/chat.ex
index fe32ec08c..5c4dbc1ff 100644
--- a/lib/pleroma/chat.ex
+++ b/lib/pleroma/chat.ex
@@ -42,7 +42,7 @@ 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()) ::
+ @spec get_by_user_and_id(User.t(), Ecto.UUID.t()) ::
{:ok, t()} | {:error, :not_found}
def get_by_user_and_id(%User{id: user_id}, id) do
from(c in __MODULE__,
@@ -52,17 +52,17 @@ defmodule Pleroma.Chat do
|> Repo.find_resource()
end
- @spec get_by_id(FlakeId.Ecto.CompatType.t()) :: t() | nil
+ @spec get_by_id(Ecto.UUID.t()) :: t() | nil
def get_by_id(id) do
Repo.get(__MODULE__, id)
end
- @spec get(FlakeId.Ecto.CompatType.t(), String.t()) :: t() | nil
+ @spec get(Ecto.UUID.t(), String.t()) :: t() | nil
def get(user_id, recipient) do
Repo.get_by(__MODULE__, user_id: user_id, recipient: recipient)
end
- @spec get_or_create(FlakeId.Ecto.CompatType.t(), String.t()) ::
+ @spec get_or_create(Ecto.UUID.t(), String.t()) ::
{:ok, t()} | {:error, Ecto.Changeset.t()}
def get_or_create(user_id, recipient) do
%__MODULE__{}
@@ -75,7 +75,7 @@ defmodule Pleroma.Chat do
)
end
- @spec bump_or_create(FlakeId.Ecto.CompatType.t(), String.t()) ::
+ @spec bump_or_create(Ecto.UUID.t(), String.t()) ::
{:ok, t()} | {:error, Ecto.Changeset.t()}
def bump_or_create(user_id, recipient) do
%__MODULE__{}
@@ -87,7 +87,7 @@ defmodule Pleroma.Chat do
)
end
- @spec for_user_query(FlakeId.Ecto.CompatType.t()) :: Ecto.Query.t()
+ @spec for_user_query(Ecto.UUID.t()) :: Ecto.Query.t()
def for_user_query(user_id) do
from(c in Chat,
where: c.user_id == ^user_id,
diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex
index b53b15d95..58d164dc7 100644
--- a/lib/pleroma/config/deprecation_warnings.ex
+++ b/lib/pleroma/config/deprecation_warnings.ex
@@ -24,7 +24,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
filters = Config.get([Pleroma.Upload]) |> Keyword.get(:filters, [])
if Pleroma.Upload.Filter.Exiftool in filters do
- Logger.warn("""
+ Logger.warning("""
!!!DEPRECATION WARNING!!!
Your config is using Exiftool as a filter instead of Exiftool.StripLocation. This should work for now, but you are advised to change to the new configuration to prevent possible issues later:
@@ -63,7 +63,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
|> Enum.any?(fn {_, v} -> Enum.any?(v, &is_binary/1) end)
if has_strings do
- Logger.warn("""
+ Logger.warning("""
!!!DEPRECATION WARNING!!!
Your config is using strings in the SimplePolicy configuration instead of tuples. They should work for now, but you are advised to change to the new configuration to prevent possible issues later:
@@ -121,7 +121,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
has_strings = Config.get([:instance, :quarantined_instances]) |> Enum.any?(&is_binary/1)
if has_strings do
- Logger.warn("""
+ Logger.warning("""
!!!DEPRECATION WARNING!!!
Your config is using strings in the quarantined_instances configuration instead of tuples. They should work for now, but you are advised to change to the new configuration to prevent possible issues later:
@@ -158,7 +158,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
has_strings = Config.get([:mrf, :transparency_exclusions]) |> Enum.any?(&is_binary/1)
if has_strings do
- Logger.warn("""
+ Logger.warning("""
!!!DEPRECATION WARNING!!!
Your config is using strings in the transparency_exclusions configuration instead of tuples. They should work for now, but you are advised to change to the new configuration to prevent possible issues later:
@@ -172,7 +172,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
```
config :pleroma, :mrf,
- transparency_exclusions: [{"instance.tld", "Reason to exlude transparency"}]
+ transparency_exclusions: [{"instance.tld", "Reason to exclude transparency"}]
```
""")
@@ -193,7 +193,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
def check_hellthread_threshold do
if Config.get([:mrf_hellthread, :threshold]) do
- Logger.warn("""
+ Logger.warning("""
!!!DEPRECATION WARNING!!!
You are using the old configuration mechanism for the hellthread filter. Please check config.md.
""")
@@ -213,7 +213,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
check_gun_pool_options(),
check_activity_expiration_config(),
check_remote_ip_plug_name(),
- check_uploders_s3_public_endpoint(),
+ check_uploaders_s3_public_endpoint(),
check_old_chat_shoutbox(),
check_quarantined_instances_tuples(),
check_transparency_exclusions_tuples(),
@@ -256,7 +256,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
move_namespace_and_warn(@mrf_config_map, warning_preface)
end
- @spec move_namespace_and_warn([config_map()], String.t()) :: :ok | nil
+ @spec move_namespace_and_warn([config_map()], String.t()) :: :ok | :error
def move_namespace_and_warn(config_map, warning_preface) do
warning =
Enum.reduce(config_map, "", fn
@@ -274,17 +274,17 @@ defmodule Pleroma.Config.DeprecationWarnings do
if warning == "" do
:ok
else
- Logger.warn(warning_preface <> warning)
+ Logger.warning(warning_preface <> warning)
:error
end
end
- @spec check_media_proxy_whitelist_config() :: :ok | nil
+ @spec check_media_proxy_whitelist_config() :: :ok | :error
def check_media_proxy_whitelist_config do
whitelist = Config.get([:media_proxy, :whitelist])
if Enum.any?(whitelist, &(not String.starts_with?(&1, "http"))) do
- Logger.warn("""
+ Logger.warning("""
!!!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.
""")
@@ -299,7 +299,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
pool_config = Config.get(:connections_pool)
if timeout = pool_config[:await_up_timeout] do
- Logger.warn("""
+ Logger.warning("""
!!!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.
""")
@@ -331,7 +331,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
"\n* `:timeout` options in #{pool_name} pool is now `:recv_timeout`"
end)
- Logger.warn(Enum.join([warning_preface | pool_warnings]))
+ Logger.warning(Enum.join([warning_preface | pool_warnings]))
Config.put(:pools, updated_config)
:error
@@ -340,7 +340,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
end
end
- @spec check_activity_expiration_config() :: :ok | nil
+ @spec check_activity_expiration_config() :: :ok | :error
def check_activity_expiration_config do
warning_preface = """
!!!DEPRECATION WARNING!!!
@@ -356,7 +356,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
)
end
- @spec check_remote_ip_plug_name() :: :ok | nil
+ @spec check_remote_ip_plug_name() :: :ok | :error
def check_remote_ip_plug_name do
warning_preface = """
!!!DEPRECATION WARNING!!!
@@ -372,8 +372,8 @@ defmodule Pleroma.Config.DeprecationWarnings do
)
end
- @spec check_uploders_s3_public_endpoint() :: :ok | nil
- def check_uploders_s3_public_endpoint do
+ @spec check_uploaders_s3_public_endpoint() :: :ok | :error
+ def check_uploaders_s3_public_endpoint do
s3_config = Pleroma.Config.get([Pleroma.Uploaders.S3])
use_old_config = Keyword.has_key?(s3_config, :public_endpoint)
@@ -393,7 +393,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
end
end
- @spec check_old_chat_shoutbox() :: :ok | nil
+ @spec check_old_chat_shoutbox() :: :ok | :error
def check_old_chat_shoutbox do
instance_config = Pleroma.Config.get([:instance])
chat_config = Pleroma.Config.get([:chat]) || []
diff --git a/lib/pleroma/config/getting.ex b/lib/pleroma/config/getting.ex
index f9b66bba6..ec93fd02a 100644
--- a/lib/pleroma/config/getting.ex
+++ b/lib/pleroma/config/getting.ex
@@ -5,4 +5,11 @@
defmodule Pleroma.Config.Getting do
@callback get(any()) :: any()
@callback get(any(), any()) :: any()
+
+ def get(key), do: get(key, nil)
+ def get(key, default), do: impl().get(key, default)
+
+ def impl do
+ Application.get_env(:pleroma, :config_impl, Pleroma.Config)
+ end
end
diff --git a/lib/pleroma/config/oban.ex b/lib/pleroma/config/oban.ex
index 483d2bb79..836f0c1a7 100644
--- a/lib/pleroma/config/oban.ex
+++ b/lib/pleroma/config/oban.ex
@@ -23,7 +23,7 @@ defmodule Pleroma.Config.Oban do
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()
+ |> Logger.warning()
List.delete(acc, setting)
else
diff --git a/lib/pleroma/config/release_runtime_provider.ex b/lib/pleroma/config/release_runtime_provider.ex
index 9ec0f975e..351639836 100644
--- a/lib/pleroma/config/release_runtime_provider.ex
+++ b/lib/pleroma/config/release_runtime_provider.ex
@@ -21,7 +21,7 @@ defmodule Pleroma.Config.ReleaseRuntimeProvider do
with_runtime_config =
if File.exists?(config_path) do
# <https://git.pleroma.social/pleroma/pleroma/-/issues/3135>
- %File.Stat{mode: mode} = File.lstat!(config_path)
+ %File.Stat{mode: mode} = File.stat!(config_path)
if Bitwise.band(mode, 0o007) > 0 do
raise "Configuration at #{config_path} has world-permissions, execute the following: chmod o= #{config_path}"
diff --git a/lib/pleroma/config/transfer_task.ex b/lib/pleroma/config/transfer_task.ex
index 44a984019..ffc95f144 100644
--- a/lib/pleroma/config/transfer_task.ex
+++ b/lib/pleroma/config/transfer_task.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Config.TransferTask do
@@ -44,19 +44,13 @@ defmodule Pleroma.Config.TransferTask do
with {_, true} <- {:configurable, Config.get(:configurable_from_database)} do
# We need to restart applications for loaded settings take effect
- {logger, other} =
+ settings =
(Repo.all(ConfigDB) ++ deleted_settings)
|> Enum.map(&merge_with_default/1)
- |> Enum.split_with(fn {group, _, _, _} -> group in [:logger] end)
-
- logger
- |> Enum.sort()
- |> Enum.each(&configure/1)
started_applications = Application.started_applications()
- # TODO: some problem with prometheus after restart!
- reject = [nil, :prometheus, :postgrex]
+ reject = [nil, :postgrex]
reject =
if restart_pleroma? do
@@ -65,7 +59,7 @@ defmodule Pleroma.Config.TransferTask do
[:pleroma | reject]
end
- other
+ settings
|> Enum.map(&update/1)
|> Enum.uniq()
|> Enum.reject(&(&1 in reject))
@@ -103,38 +97,6 @@ defmodule Pleroma.Config.TransferTask do
{group, key, value, merged}
end
- # change logger configuration in runtime, without restart
- defp configure({_, :backends, _, merged}) do
- # removing current backends
- Enum.each(Application.get_env(:logger, :backends), &Logger.remove_backend/1)
-
- Enum.each(merged, &Logger.add_backend/1)
-
- :ok = update_env(:logger, :backends, merged)
- end
-
- defp configure({_, key, _, merged}) when key in [:console, :ex_syslogger] do
- merged =
- if key == :console do
- put_in(merged[:format], merged[:format] <> "\n")
- else
- merged
- end
-
- backend =
- if key == :ex_syslogger,
- do: {ExSyslogger, :ex_syslogger},
- else: key
-
- Logger.configure_backend(backend, merged)
- :ok = update_env(:logger, key, merged)
- end
-
- defp configure({_, key, _, merged}) do
- Logger.configure([{key, merged}])
- :ok = update_env(:logger, key, merged)
- end
-
defp update({group, key, value, merged}) do
try do
:ok = update_env(group, key, merged)
@@ -145,7 +107,7 @@ defmodule Pleroma.Config.TransferTask do
error_msg =
"updating env causes error, group: #{inspect(group)}, key: #{inspect(key)}, value: #{inspect(value)} error: #{inspect(error)}"
- Logger.warn(error_msg)
+ Logger.warning(error_msg)
nil
end
@@ -179,12 +141,12 @@ defmodule Pleroma.Config.TransferTask do
:ok = Application.start(app)
else
nil ->
- Logger.warn("#{app} is not started.")
+ Logger.warning("#{app} is not started.")
error ->
error
|> inspect()
- |> Logger.warn()
+ |> Logger.warning()
end
end
diff --git a/lib/pleroma/config_db.ex b/lib/pleroma/config_db.ex
index 846cede04..89d3050d6 100644
--- a/lib/pleroma/config_db.ex
+++ b/lib/pleroma/config_db.ex
@@ -54,7 +54,7 @@ defmodule Pleroma.ConfigDB do
@spec get_by_params(map()) :: ConfigDB.t() | nil
def get_by_params(%{group: _, key: _} = params), do: Repo.get_by(ConfigDB, params)
- @spec changeset(ConfigDB.t(), map()) :: Changeset.t()
+ @spec changeset(ConfigDB.t(), map()) :: Ecto.Changeset.t()
def changeset(config, params \\ %{}) do
config
|> cast(params, [:key, :group, :value])
@@ -138,7 +138,7 @@ defmodule Pleroma.ConfigDB do
end
end
- @spec update_or_create(map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()}
+ @spec update_or_create(map()) :: {:ok, ConfigDB.t()} | {:error, Ecto.Changeset.t()}
def update_or_create(params) do
params = Map.put(params, :value, to_elixir_types(params[:value]))
search_opts = Map.take(params, [:group, :key])
@@ -165,8 +165,7 @@ defmodule Pleroma.ConfigDB do
{:pleroma, :ecto_repos},
{:mime, :types},
{:cors_plug, [:max_age, :methods, :expose, :headers]},
- {:swarm, :node_blacklist},
- {:logger, :backends}
+ {:swarm, :node_blacklist}
]
Enum.any?(full_key_update, fn
@@ -175,7 +174,7 @@ defmodule Pleroma.ConfigDB do
end)
end
- @spec delete(ConfigDB.t() | map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()}
+ @spec delete(ConfigDB.t() | map()) :: {:ok, ConfigDB.t()} | {:error, Ecto.Changeset.t()}
def delete(%ConfigDB{} = config), do: Repo.delete(config)
def delete(params) do
@@ -385,7 +384,12 @@ defmodule Pleroma.ConfigDB do
@spec module_name?(String.t()) :: boolean()
def module_name?(string) do
- Regex.match?(~r/^(Pleroma|Phoenix|Tesla|Ueberauth|Swoosh)\./, string) or
- string in ["Oban", "Ueberauth", "ExSyslogger", "ConcurrentLimiter"]
+ if String.contains?(string, ".") do
+ [name | _] = String.split(string, ".", parts: 2)
+
+ name in ~w[Pleroma Phoenix Tesla Ueberauth Swoosh Logger LoggerBackends]
+ else
+ string in ~w[Oban Ueberauth ExSyslogger ConcurrentLimiter]
+ end
end
end
diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex
index 77bc4bfac..3a5e35301 100644
--- a/lib/pleroma/constants.ex
+++ b/lib/pleroma/constants.ex
@@ -19,7 +19,8 @@ defmodule Pleroma.Constants do
"context_id",
"deleted_activity_id",
"pleroma_internal",
- "generator"
+ "generator",
+ "rules"
]
)
@@ -76,6 +77,14 @@ defmodule Pleroma.Constants do
]
)
+ const(allowed_user_actor_types,
+ do: [
+ "Person",
+ "Service",
+ "Group"
+ ]
+ )
+
# basic regex, just there to weed out potential mistakes
# https://datatracker.ietf.org/doc/html/rfc2045#section-5.1
const(mime_regex,
diff --git a/lib/pleroma/conversation.ex b/lib/pleroma/conversation.ex
index 42028aa51..0be609a22 100644
--- a/lib/pleroma/conversation.ex
+++ b/lib/pleroma/conversation.ex
@@ -57,7 +57,7 @@ defmodule Pleroma.Conversation do
3. Bump all relevant participations to 'unread'
"""
def create_or_bump_for(activity, opts \\ []) do
- with true <- Pleroma.Web.ActivityPub.Visibility.is_direct?(activity),
+ with true <- Pleroma.Web.ActivityPub.Visibility.direct?(activity),
"Create" <- activity.data["type"],
%Object{} = object <- Object.normalize(activity, fetch: false),
true <- object.data["type"] in ["Note", "Question"],
diff --git a/lib/pleroma/conversation/participation.ex b/lib/pleroma/conversation/participation.ex
index 4ed93e5bd..5d3344cc5 100644
--- a/lib/pleroma/conversation/participation.ex
+++ b/lib/pleroma/conversation/participation.ex
@@ -12,6 +12,8 @@ defmodule Pleroma.Conversation.Participation do
import Ecto.Changeset
import Ecto.Query
+ @type t :: %__MODULE__{}
+
schema "conversation_participations" do
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:conversation, Conversation)
diff --git a/lib/pleroma/data_migration.ex b/lib/pleroma/data_migration.ex
index 8451678fc..be4bf6489 100644
--- a/lib/pleroma/data_migration.ex
+++ b/lib/pleroma/data_migration.ex
@@ -12,6 +12,8 @@ defmodule Pleroma.DataMigration do
import Ecto.Changeset
import Ecto.Query
+ @type t :: %__MODULE__{}
+
schema "data_migrations" do
field(:name, :string)
field(:state, State, default: :pending)
diff --git a/lib/pleroma/docs/generator.ex b/lib/pleroma/docs/generator.ex
index 6508f1947..93b19484f 100644
--- a/lib/pleroma/docs/generator.ex
+++ b/lib/pleroma/docs/generator.ex
@@ -15,8 +15,10 @@ defmodule Pleroma.Docs.Generator do
:code.all_loaded()
|> Enum.filter(fn {module, _} ->
# This shouldn't be needed as all modules are expected to have module_info/1,
- # but in test enviroments some transient modules `:elixir_compiler_XX`
+ # but in test environments some transient modules `:elixir_compiler_XX`
# are loaded for some reason (where XX is a random integer).
+ Code.ensure_loaded(module)
+
if function_exported?(module, :module_info, 1) do
module.module_info(:attributes)
|> Keyword.get_values(:behaviour)
diff --git a/lib/pleroma/docs/json.ex b/lib/pleroma/docs/json.ex
index 05f46f39b..f69854935 100644
--- a/lib/pleroma/docs/json.ex
+++ b/lib/pleroma/docs/json.ex
@@ -18,7 +18,7 @@ defmodule Pleroma.Docs.JSON do
:persistent_term.put(@term, Pleroma.Docs.Generator.convert_to_strings(descriptions))
end
- @spec compiled_descriptions :: Map.t()
+ @spec compiled_descriptions :: map()
def compiled_descriptions do
:persistent_term.get(@term)
end
diff --git a/lib/pleroma/ecto_enums.ex b/lib/pleroma/ecto_enums.ex
index b346b39d6..a4890b489 100644
--- a/lib/pleroma/ecto_enums.ex
+++ b/lib/pleroma/ecto_enums.ex
@@ -27,11 +27,3 @@ defenum(Pleroma.DataMigration.State,
failed: 4,
manual: 5
)
-
-defenum(Pleroma.User.Backup.State,
- pending: 1,
- running: 2,
- complete: 3,
- failed: 4,
- invalid: 5
-)
diff --git a/lib/pleroma/ecto_type/activity_pub/object_validators/bare_uri.ex b/lib/pleroma/ecto_type/activity_pub/object_validators/bare_uri.ex
index 1038296e7..a1af8faa1 100644
--- a/lib/pleroma/ecto_type/activity_pub/object_validators/bare_uri.ex
+++ b/lib/pleroma/ecto_type/activity_pub/object_validators/bare_uri.ex
@@ -8,10 +8,12 @@ defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.BareUri do
def type, do: :string
def cast(uri) when is_binary(uri) do
- case URI.parse(uri) do
- %URI{scheme: nil} -> :error
- %URI{} -> {:ok, uri}
- _ -> :error
+ parsed = URI.parse(uri)
+
+ if is_nil(parsed.scheme) do
+ :error
+ else
+ {:ok, uri}
end
end
diff --git a/lib/pleroma/emails/user_email.ex b/lib/pleroma/emails/user_email.ex
index 95b963764..10d89d2f3 100644
--- a/lib/pleroma/emails/user_email.ex
+++ b/lib/pleroma/emails/user_email.ex
@@ -345,37 +345,22 @@ defmodule Pleroma.Emails.UserEmail do
Router.Helpers.subscription_url(Endpoint, :unsubscribe, token)
end
- def backup_is_ready_email(backup, admin_user_id \\ nil) do
+ def backup_is_ready_email(backup) do
%{user: user} = Pleroma.Repo.preload(backup, :user)
Gettext.with_locale_or_default user.language do
download_url = Pleroma.Web.PleromaAPI.BackupView.download_url(backup)
html_body =
- if is_nil(admin_user_id) do
- Gettext.dpgettext(
- "static_pages",
- "account archive email body - self-requested",
- """
- <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>
- """,
- download_url: download_url
- )
- else
- admin = Pleroma.Repo.get(User, admin_user_id)
-
- Gettext.dpgettext(
- "static_pages",
- "account archive email body - admin requested",
- """
- <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>
- """,
- admin_nickname: admin.nickname,
- download_url: download_url
- )
- end
+ Gettext.dpgettext(
+ "static_pages",
+ "account archive email body",
+ """
+ <p>A full backup of your Pleroma account was requested. It's ready for download:</p>
+ <p><a href="%{download_url}">%{download_url}</a></p>
+ """,
+ download_url: download_url
+ )
new()
|> to(recipient(user))
diff --git a/lib/pleroma/emoji.ex b/lib/pleroma/emoji.ex
index 43a3447c3..21bcb0111 100644
--- a/lib/pleroma/emoji.ex
+++ b/lib/pleroma/emoji.ex
@@ -24,6 +24,8 @@ defmodule Pleroma.Emoji do
defstruct [:code, :file, :tags, :safe_code, :safe_file]
+ @type t :: %__MODULE__{}
+
@doc "Build emoji struct"
def build({code, file, tags}) do
%__MODULE__{
@@ -49,12 +51,12 @@ defmodule Pleroma.Emoji do
end
@doc "Returns the path of the emoji `name`."
- @spec get(String.t()) :: String.t() | nil
+ @spec get(String.t()) :: Pleroma.Emoji.t() | nil
def get(name) do
name = maybe_strip_name(name)
case :ets.lookup(@ets, name) do
- [{_, path}] -> path
+ [{_, emoji}] -> emoji
_ -> nil
end
end
@@ -136,23 +138,23 @@ defmodule Pleroma.Emoji do
emojis = emojis ++ regional_indicators
for emoji <- emojis do
- def is_unicode_emoji?(unquote(emoji)), do: true
+ def unicode?(unquote(emoji)), do: true
end
- def is_unicode_emoji?(_), do: false
+ def unicode?(_), do: false
@emoji_regex ~r/:[A-Za-z0-9_-]+(@.+)?:/
- def is_custom_emoji?(s) when is_binary(s), do: Regex.match?(@emoji_regex, s)
+ def custom?(s) when is_binary(s), do: Regex.match?(@emoji_regex, s)
- def is_custom_emoji?(_), do: false
+ def custom?(_), do: false
def maybe_strip_name(name) when is_binary(name), do: String.trim(name, ":")
def maybe_strip_name(name), do: name
def maybe_quote(name) when is_binary(name) do
- if is_unicode_emoji?(name) do
+ if unicode?(name) do
name
else
if String.starts_with?(name, ":") do
diff --git a/lib/pleroma/emoji/loader.ex b/lib/pleroma/emoji/loader.ex
index 97d4b8f70..b6e544323 100644
--- a/lib/pleroma/emoji/loader.ex
+++ b/lib/pleroma/emoji/loader.ex
@@ -15,8 +15,6 @@ defmodule Pleroma.Emoji.Loader do
require Logger
- @mix_env Mix.env()
-
@type pattern :: Regex.t() | module() | String.t()
@type patterns :: pattern() | [pattern()]
@type group_patterns :: keyword(patterns())
@@ -59,7 +57,7 @@ defmodule Pleroma.Emoji.Loader do
Logger.info("Found emoji packs: #{Enum.join(packs, ", ")}")
if not Enum.empty?(files) do
- Logger.warn(
+ Logger.warning(
"Found files in the emoji folder. These will be ignored, please move them to a subdirectory\nFound files: #{Enum.join(files, ", ")}"
)
end
@@ -79,7 +77,7 @@ defmodule Pleroma.Emoji.Loader do
# for testing emoji.txt entries we do not want exposed in normal operation
test_emoji =
- if @mix_env == :test do
+ if Application.get_env(:pleroma, __MODULE__)[:test_emoji] do
load_from_file("test/config/emoji.txt", emoji_groups)
else
[]
diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex
index 6e58f8898..785fdb8b2 100644
--- a/lib/pleroma/emoji/pack.ex
+++ b/lib/pleroma/emoji/pack.ex
@@ -100,7 +100,7 @@ defmodule Pleroma.Emoji.Pack do
{:ok, _emoji_files} =
:zip.unzip(
to_charlist(file.path),
- [{:file_list, Enum.map(emojies, & &1[:path])}, {:cwd, tmp_dir}]
+ [{:file_list, Enum.map(emojies, & &1[:path])}, {:cwd, String.to_charlist(tmp_dir)}]
)
{_, updated_pack} =
@@ -209,7 +209,9 @@ defmodule Pleroma.Emoji.Pack do
with :ok <- validate_shareable_packs_available(uri) do
uri
- |> URI.merge("/api/pleroma/emoji/packs?page=#{opts[:page]}&page_size=#{opts[:page_size]}")
+ |> URI.merge(
+ "/api/v1/pleroma/emoji/packs?page=#{opts[:page]}&page_size=#{opts[:page_size]}"
+ )
|> http_get()
end
end
@@ -249,8 +251,12 @@ defmodule Pleroma.Emoji.Pack do
uri = url |> String.trim() |> URI.parse()
with :ok <- validate_shareable_packs_available(uri),
+ {:ok, %{"files_count" => files_count}} <-
+ uri |> URI.merge("/api/v1/pleroma/emoji/pack?name=#{name}&page_size=0") |> http_get(),
{:ok, remote_pack} <-
- uri |> URI.merge("/api/pleroma/emoji/pack?name=#{name}") |> http_get(),
+ uri
+ |> URI.merge("/api/v1/pleroma/emoji/pack?name=#{name}&page_size=#{files_count}")
+ |> 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),
@@ -410,10 +416,10 @@ defmodule Pleroma.Emoji.Pack do
end
defp create_archive_and_cache(pack, hash) do
- files = ['pack.json' | Enum.map(pack.files, fn {_, file} -> to_charlist(file) end)]
+ files = [~c"pack.json" | Enum.map(pack.files, fn {_, file} -> to_charlist(file) end)]
{:ok, {_, result}} =
- :zip.zip('#{pack.name}.zip', files, [:memory, cwd: to_charlist(pack.path)])
+ :zip.zip(~c"#{pack.name}.zip", files, [:memory, cwd: to_charlist(pack.path)])
ttl_per_file = Pleroma.Config.get!([:emoji, :shared_pack_cache_seconds_per_file])
overall_ttl = :timer.seconds(ttl_per_file * Enum.count(files))
@@ -580,7 +586,7 @@ defmodule Pleroma.Emoji.Pack do
with :ok <- File.mkdir_p!(local_pack.path) do
files = Enum.map(remote_pack["files"], fn {_, path} -> to_charlist(path) end)
# Fallback cannot contain a pack.json file
- files = if pack_info[:fallback], do: files, else: ['pack.json' | files]
+ files = if pack_info[:fallback], do: files, else: [~c"pack.json" | files]
:zip.unzip(archive, cwd: to_charlist(local_pack.path), file_list: files)
end
@@ -592,7 +598,7 @@ defmodule Pleroma.Emoji.Pack do
{:ok,
%{
sha: sha,
- url: URI.merge(uri, "/api/pleroma/emoji/packs/archive?name=#{name}") |> to_string()
+ url: URI.merge(uri, "/api/v1/pleroma/emoji/packs/archive?name=#{name}") |> to_string()
}}
%{"fallback-src" => src, "fallback-src-sha256" => sha} when is_binary(src) ->
diff --git a/lib/pleroma/filter.ex b/lib/pleroma/filter.ex
index db88bc021..e827d3cbc 100644
--- a/lib/pleroma/filter.ex
+++ b/lib/pleroma/filter.ex
@@ -216,9 +216,6 @@ defmodule Pleroma.Filter do
:re ->
~r/\b#{phrases}\b/i
-
- _ ->
- nil
end
end
diff --git a/lib/pleroma/following_relationship.ex b/lib/pleroma/following_relationship.ex
index 15664c876..495488dfd 100644
--- a/lib/pleroma/following_relationship.ex
+++ b/lib/pleroma/following_relationship.ex
@@ -199,8 +199,8 @@ defmodule Pleroma.FollowingRelationship do
|> preload([:follower])
|> Repo.all()
|> Enum.map(fn following_relationship ->
- Pleroma.Web.CommonAPI.follow(following_relationship.follower, target)
- Pleroma.Web.CommonAPI.unfollow(following_relationship.follower, origin)
+ Pleroma.Web.CommonAPI.follow(target, following_relationship.follower)
+ Pleroma.Web.CommonAPI.unfollow(origin, following_relationship.follower)
end)
|> case do
[] ->
@@ -241,13 +241,13 @@ defmodule Pleroma.FollowingRelationship do
end
@doc """
- For a query with joined activity,
- keeps rows where activity's actor is followed by user -or- is NOT domain-blocked by user.
+ For a query with joined activity's actor,
+ keeps rows where actor is followed by user -or- is NOT domain-blocked by user.
"""
def keep_following_or_not_domain_blocked(query, user) do
where(
query,
- [_, activity],
+ [_, user_actor: user_actor],
fragment(
# "(actor's domain NOT in domain_blocks) OR (actor IS in followed AP IDs)"
"""
@@ -255,9 +255,9 @@ defmodule Pleroma.FollowingRelationship do
? = ANY(SELECT ap_id FROM users AS u INNER JOIN following_relationships AS fr
ON u.id = fr.following_id WHERE fr.follower_id = ? AND fr.state = ?)
""",
- activity.actor,
+ user_actor.ap_id,
^user.domain_blocks,
- activity.actor,
+ user_actor.ap_id,
^User.binary_id(user.id),
^accept_state_code()
)
diff --git a/lib/pleroma/frontend.ex b/lib/pleroma/frontend.ex
index ec72fb6a4..816499917 100644
--- a/lib/pleroma/frontend.ex
+++ b/lib/pleroma/frontend.ex
@@ -43,10 +43,6 @@ defmodule Pleroma.Frontend do
{: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
diff --git a/lib/pleroma/gopher/server.ex b/lib/pleroma/gopher/server.ex
index 0fde0adcf..54245c9fa 100644
--- a/lib/pleroma/gopher/server.ex
+++ b/lib/pleroma/gopher/server.ex
@@ -114,7 +114,7 @@ defmodule Pleroma.Gopher.Server.ProtocolHandler do
def response("/notices/" <> id) do
with %Activity{} = activity <- Activity.get_by_id(id),
- true <- Visibility.is_public?(activity) do
+ true <- Visibility.public?(activity) do
activities =
ActivityPub.fetch_activities_for_context(activity.data["context"])
|> render_activities
diff --git a/lib/pleroma/gun/conn.ex b/lib/pleroma/gun/conn.ex
index 7c5785def..804cd11c7 100644
--- a/lib/pleroma/gun/conn.ex
+++ b/lib/pleroma/gun/conn.ex
@@ -56,7 +56,7 @@ defmodule Pleroma.Gun.Conn do
{:ok, conn, protocol}
else
error ->
- Logger.warn(
+ Logger.warning(
"Opening proxied connection to #{compose_uri_log(uri)} failed with error #{inspect(error)}"
)
@@ -90,7 +90,7 @@ defmodule Pleroma.Gun.Conn do
{:ok, conn, protocol}
else
error ->
- Logger.warn(
+ Logger.warning(
"Opening socks proxied connection to #{compose_uri_log(uri)} failed with error #{inspect(error)}"
)
@@ -106,7 +106,7 @@ defmodule Pleroma.Gun.Conn do
{:ok, conn, protocol}
else
error ->
- Logger.warn(
+ Logger.warning(
"Opening connection to #{compose_uri_log(uri)} failed with error #{inspect(error)}"
)
diff --git a/lib/pleroma/gun/connection_pool/reclaimer.ex b/lib/pleroma/gun/connection_pool/reclaimer.ex
index efd5c9fb8..3580d38f5 100644
--- a/lib/pleroma/gun/connection_pool/reclaimer.ex
+++ b/lib/pleroma/gun/connection_pool/reclaimer.ex
@@ -9,7 +9,7 @@ defmodule Pleroma.Gun.ConnectionPool.Reclaimer do
def start_monitor do
pid =
- case :gen_server.start(__MODULE__, [], name: {:via, Registry, {registry(), "reclaimer"}}) do
+ case GenServer.start(__MODULE__, [], name: {:via, Registry, {registry(), "reclaimer"}}) do
{:ok, pid} ->
pid
diff --git a/lib/pleroma/gun/connection_pool/worker_supervisor.ex b/lib/pleroma/gun/connection_pool/worker_supervisor.ex
index d26a70be3..b9dedf61e 100644
--- a/lib/pleroma/gun/connection_pool/worker_supervisor.ex
+++ b/lib/pleroma/gun/connection_pool/worker_supervisor.ex
@@ -5,6 +5,9 @@
defmodule Pleroma.Gun.ConnectionPool.WorkerSupervisor do
@moduledoc "Supervisor for pool workers. Does not do anything except enforce max connection limit"
+ alias Pleroma.Config
+ alias Pleroma.Gun.ConnectionPool.Worker
+
use DynamicSupervisor
def start_link(opts) do
@@ -14,19 +17,28 @@ defmodule Pleroma.Gun.ConnectionPool.WorkerSupervisor do
def init(_opts) do
DynamicSupervisor.init(
strategy: :one_for_one,
- max_children: Pleroma.Config.get([:connections_pool, :max_connections])
+ max_children: Config.get([:connections_pool, :max_connections])
)
end
- def start_worker(opts, retry \\ false) do
- case DynamicSupervisor.start_child(__MODULE__, {Pleroma.Gun.ConnectionPool.Worker, opts}) do
+ def start_worker(opts, last_attempt \\ false)
+
+ def start_worker(opts, true) do
+ case DynamicSupervisor.start_child(__MODULE__, {Worker, opts}) do
+ {:error, :max_children} ->
+ :telemetry.execute([:pleroma, :connection_pool, :provision_failure], %{opts: opts})
+ {:error, :pool_full}
+
+ res ->
+ res
+ end
+ end
+
+ def start_worker(opts, false) do
+ case DynamicSupervisor.start_child(__MODULE__, {Worker, opts}) do
{:error, :max_children} ->
- if retry or free_pool() == :error do
- :telemetry.execute([:pleroma, :connection_pool, :provision_failure], %{opts: opts})
- {:error, :pool_full}
- else
- start_worker(opts, true)
- end
+ free_pool()
+ start_worker(opts, true)
res ->
res
diff --git a/lib/pleroma/helpers/inet_helper.ex b/lib/pleroma/helpers/inet_helper.ex
index 704d37f8a..00e18649e 100644
--- a/lib/pleroma/helpers/inet_helper.ex
+++ b/lib/pleroma/helpers/inet_helper.ex
@@ -16,4 +16,15 @@ defmodule Pleroma.Helpers.InetHelper do
def parse_address(ip) do
:inet.parse_address(ip)
end
+
+ def parse_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_cidr!(proxy, true)
+ end
end
diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex
index 24c845fcd..8566ab3ea 100644
--- a/lib/pleroma/helpers/media_helper.ex
+++ b/lib/pleroma/helpers/media_helper.ex
@@ -8,11 +8,14 @@ defmodule Pleroma.Helpers.MediaHelper do
"""
alias Pleroma.HTTP
+ alias Vix.Vips.Operation
require Logger
+ @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
+
def missing_dependencies do
- Enum.reduce([imagemagick: "convert", ffmpeg: "ffmpeg"], [], fn {sym, executable}, acc ->
+ Enum.reduce([ffmpeg: "ffmpeg"], [], fn {sym, executable}, acc ->
if Pleroma.Utils.command_available?(executable) do
acc
else
@@ -22,141 +25,65 @@ defmodule Pleroma.Helpers.MediaHelper do
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)
+ with {:ok, env} <- HTTP.get(url, [], http_client_opts()),
+ {:ok, resized} <-
+ Operation.thumbnail_buffer(env.body, options.max_width,
+ height: options.max_height,
+ size: :VIPS_SIZE_DOWN
+ ) do
+ if options[:format] == "png" do
+ Operation.pngsave_buffer(resized, Q: options[:quality])
+ else
+ Operation.jpegsave_buffer(resized, Q: options[:quality], interlace: true)
+ end
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)
+ @spec video_framegrab(String.t()) :: {:ok, binary()} | {:error, any()}
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)
+ {:ok, false} <- @cachex.exists?(:failed_media_helper_cache, url),
+ {:ok, env} <- HTTP.get(url, [], http_client_opts()),
+ {:ok, pid} <- StringIO.open(env.body) do
+ body_stream = IO.binstream(pid, 1)
+
+ task =
+ Task.async(fn ->
+ Exile.stream!(
+ [
+ executable,
+ "-i",
+ "pipe:0",
+ "-vframes",
+ "1",
+ "-f",
+ "mjpeg",
+ "pipe:1"
+ ],
+ input: body_stream,
+ ignore_epipe: true,
+ stderr: :disable
+ )
+ |> Enum.into(<<>>)
+ end)
+
+ case Task.yield(task, 5_000) do
+ {:ok, result} ->
+ {:ok, result}
+
+ _ ->
+ Task.shutdown(task)
+ @cachex.put(:failed_media_helper_cache, url, nil)
+ {:error, {:ffmpeg, :timeout}}
+ end
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
+ defp http_client_opts, do: Pleroma.Config.get([:media_proxy, :proxy_opts, :http], pool: :media)
end
diff --git a/lib/pleroma/helpers/qt_fast_start.ex b/lib/pleroma/helpers/qt_fast_start.ex
index 5711c7162..0b200b234 100644
--- a/lib/pleroma/helpers/qt_fast_start.ex
+++ b/lib/pleroma/helpers/qt_fast_start.ex
@@ -40,16 +40,21 @@ defmodule Pleroma.Helpers.QtFastStart do
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)
+ try 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)
+ rescue
+ _ ->
+ :abort
+ end
end
defp fix(<<>>, _pos, _, _, acc) do
@@ -121,9 +126,15 @@ defmodule Pleroma.Helpers.QtFastStart do
<<pos::integer-big-size(unquote(size)), rest::bits>>,
acc
) do
- rewrite_entries(unquote(size), offset, rest, [
- acc | <<pos + offset::integer-big-size(unquote(size))>>
- ])
+ rewrite_entries(
+ unquote(size),
+ offset,
+ rest,
+ acc ++
+ [
+ <<pos + offset::integer-big-size(unquote(size))>>
+ ]
+ )
end
end
diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex
index 5bf735c4f..4de7cbb76 100644
--- a/lib/pleroma/html.ex
+++ b/lib/pleroma/html.ex
@@ -6,8 +6,6 @@ defmodule Pleroma.HTML do
# Scrubbers are compiled on boot so they can be configured in OTP releases
# @on_load :compile_scrubbers
- @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
-
def compile_scrubbers do
dir = Path.join(:code.priv_dir(:pleroma), "scrubbers")
@@ -67,22 +65,9 @@ defmodule Pleroma.HTML do
end
end
- def extract_first_external_url_from_object(%{data: %{"content" => content}} = object)
+ @spec extract_first_external_url_from_object(Pleroma.Object.t()) :: String.t() | nil
+ def extract_first_external_url_from_object(%{data: %{"content" => content}})
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_from_object(_), do: {:error, :no_content}
-
- def extract_first_external_url(content) do
content
|> Floki.parse_fragment!()
|> Floki.find("a:not(.mention,.hashtag,.attachment,[rel~=\"tag\"])")
@@ -90,4 +75,6 @@ defmodule Pleroma.HTML do
|> Floki.attribute("href")
|> Enum.at(0)
end
+
+ def extract_first_external_url_from_object(_), do: nil
end
diff --git a/lib/pleroma/http.ex b/lib/pleroma/http.ex
index d41061538..c11317850 100644
--- a/lib/pleroma/http.ex
+++ b/lib/pleroma/http.ex
@@ -37,7 +37,7 @@ defmodule Pleroma.HTTP do
See `Pleroma.HTTP.request/5`
"""
- @spec post(Request.url(), String.t(), Request.headers(), keyword()) ::
+ @spec post(Request.url(), Tesla.Env.body(), Request.headers(), keyword()) ::
{:ok, Env.t()} | {:error, any()}
def post(url, body, headers \\ [], options \\ []),
do: request(:post, url, body, headers, options)
@@ -56,7 +56,7 @@ defmodule Pleroma.HTTP do
`{:ok, %Tesla.Env{}}` or `{:error, error}`
"""
- @spec request(method(), Request.url(), String.t(), Request.headers(), keyword()) ::
+ @spec request(method(), Request.url(), Tesla.Env.body(), Request.headers(), keyword()) ::
{:ok, Env.t()} | {:error, any()}
def request(method, url, body, headers, options) when is_binary(url) do
uri = URI.parse(url)
@@ -68,7 +68,9 @@ defmodule Pleroma.HTTP do
adapter = Application.get_env(:tesla, :adapter)
- client = Tesla.client(adapter_middlewares(adapter), adapter)
+ extra_middleware = options[:tesla_middleware] || []
+
+ client = Tesla.client(adapter_middlewares(adapter, extra_middleware), adapter)
maybe_limit(
fn ->
@@ -102,16 +104,21 @@ defmodule Pleroma.HTTP do
fun.()
end
- defp adapter_middlewares(Tesla.Adapter.Gun) do
- [Tesla.Middleware.FollowRedirects, Pleroma.Tesla.Middleware.ConnectionPool]
+ defp adapter_middlewares(Tesla.Adapter.Gun, extra_middleware) do
+ [Tesla.Middleware.FollowRedirects, Pleroma.Tesla.Middleware.ConnectionPool] ++
+ extra_middleware
+ end
+
+ defp adapter_middlewares({Tesla.Adapter.Finch, _}, extra_middleware) do
+ [Tesla.Middleware.FollowRedirects] ++ extra_middleware
end
- defp adapter_middlewares(_) do
+ defp adapter_middlewares(_, extra_middleware) do
if Pleroma.Config.get(:env) == :test do
# Emulate redirects in test env, which are handled by adapters in other environments
[Tesla.Middleware.FollowRedirects]
else
- []
+ extra_middleware
end
end
end
diff --git a/lib/pleroma/http/adapter_helper.ex b/lib/pleroma/http/adapter_helper.ex
index 252a6aba5..dcb27a29d 100644
--- a/lib/pleroma/http/adapter_helper.ex
+++ b/lib/pleroma/http/adapter_helper.ex
@@ -15,8 +15,8 @@ defmodule Pleroma.HTTP.AdapterHelper do
require Logger
@type proxy ::
- {Connection.host(), pos_integer()}
- | {Connection.proxy_type(), Connection.host(), pos_integer()}
+ {host(), pos_integer()}
+ | {proxy_type(), host(), pos_integer()}
@callback options(keyword(), URI.t()) :: keyword()
@@ -70,15 +70,15 @@ defmodule Pleroma.HTTP.AdapterHelper do
{:ok, parse_host(host), port}
else
{_, _} ->
- Logger.warn("Parsing port failed #{inspect(proxy)}")
+ Logger.warning("Parsing port failed #{inspect(proxy)}")
{:error, :invalid_proxy_port}
:error ->
- Logger.warn("Parsing port failed #{inspect(proxy)}")
+ Logger.warning("Parsing port failed #{inspect(proxy)}")
{:error, :invalid_proxy_port}
_ ->
- Logger.warn("Parsing proxy failed #{inspect(proxy)}")
+ Logger.warning("Parsing proxy failed #{inspect(proxy)}")
{:error, :invalid_proxy}
end
end
@@ -88,7 +88,7 @@ defmodule Pleroma.HTTP.AdapterHelper do
{:ok, type, parse_host(host), port}
else
_ ->
- Logger.warn("Parsing proxy failed #{inspect(proxy)}")
+ Logger.warning("Parsing proxy failed #{inspect(proxy)}")
{:error, :invalid_proxy}
end
end
diff --git a/lib/pleroma/http/adapter_helper/gun.ex b/lib/pleroma/http/adapter_helper/gun.ex
index 74ab9851e..1fe8dd4b2 100644
--- a/lib/pleroma/http/adapter_helper/gun.ex
+++ b/lib/pleroma/http/adapter_helper/gun.ex
@@ -15,7 +15,7 @@ defmodule Pleroma.HTTP.AdapterHelper.Gun do
retry_timeout: 1_000
]
- @type pool() :: :federation | :upload | :media | :default
+ @type pool() :: :federation | :upload | :media | :rich_media | :default
@spec options(keyword(), URI.t()) :: keyword()
def options(incoming_opts \\ [], %URI{} = uri) do
diff --git a/lib/pleroma/http/request_builder.ex b/lib/pleroma/http/request_builder.ex
index f16fb3b35..0a028a64c 100644
--- a/lib/pleroma/http/request_builder.ex
+++ b/lib/pleroma/http/request_builder.ex
@@ -54,12 +54,12 @@ defmodule Pleroma.HTTP.RequestBuilder do
@doc """
Add optional parameters to the request
"""
- @spec add_param(Request.t(), atom(), atom(), any()) :: Request.t()
+ @spec add_param(Request.t(), atom(), atom() | String.t(), any()) :: Request.t()
def add_param(request, :query, :query, values), do: %{request | query: values}
def add_param(request, :body, :body, value), do: %{request | body: value}
- def add_param(request, :body, key, value) do
+ def add_param(request, :body, key, value) when is_binary(key) do
request
|> Map.put(:body, Multipart.new())
|> Map.update!(
diff --git a/lib/pleroma/http/web_push.ex b/lib/pleroma/http/web_push.ex
index ca399b6c8..888079c1e 100644
--- a/lib/pleroma/http/web_push.ex
+++ b/lib/pleroma/http/web_push.ex
@@ -6,7 +6,11 @@ defmodule Pleroma.HTTP.WebPush do
@moduledoc false
def post(url, payload, headers, options \\ []) do
- list_headers = Map.to_list(headers)
+ list_headers =
+ headers
+ |> Map.to_list()
+ |> Kernel.++([{"content-type", "octet-stream"}])
+
Pleroma.HTTP.post(url, payload, list_headers, options)
end
end
diff --git a/lib/pleroma/http_signatures_api.ex b/lib/pleroma/http_signatures_api.ex
new file mode 100644
index 000000000..8e73dc98e
--- /dev/null
+++ b/lib/pleroma/http_signatures_api.ex
@@ -0,0 +1,4 @@
+defmodule Pleroma.HTTPSignaturesAPI do
+ @callback validate_conn(conn :: Plug.Conn.t()) :: boolean
+ @callback signature_for_conn(conn :: Plug.Conn.t()) :: map
+end
diff --git a/lib/pleroma/instances.ex b/lib/pleroma/instances.ex
index 782948f83..b6d83f591 100644
--- a/lib/pleroma/instances.ex
+++ b/lib/pleroma/instances.ex
@@ -7,16 +7,15 @@ defmodule Pleroma.Instances do
alias Pleroma.Instances.Instance
- def filter_reachable(urls_or_hosts), do: Instance.filter_reachable(urls_or_hosts)
+ defdelegate filter_reachable(urls_or_hosts), to: Instance
- def reachable?(url_or_host), do: Instance.reachable?(url_or_host)
+ defdelegate reachable?(url_or_host), to: Instance
- def set_reachable(url_or_host), do: Instance.set_reachable(url_or_host)
+ defdelegate set_reachable(url_or_host), to: Instance
- def set_unreachable(url_or_host, unreachable_since \\ nil),
- do: Instance.set_unreachable(url_or_host, unreachable_since)
+ defdelegate set_unreachable(url_or_host, unreachable_since \\ nil), to: Instance
- def get_consistently_unreachable, do: Instance.get_consistently_unreachable()
+ defdelegate get_consistently_unreachable, to: Instance
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 9756c66dc..288555146 100644
--- a/lib/pleroma/instances/instance.ex
+++ b/lib/pleroma/instances/instance.ex
@@ -10,7 +10,7 @@ defmodule Pleroma.Instances.Instance do
alias Pleroma.Maps
alias Pleroma.Repo
alias Pleroma.User
- alias Pleroma.Workers.BackgroundWorker
+ alias Pleroma.Workers.DeleteWorker
use Ecto.Schema
@@ -97,13 +97,9 @@ defmodule Pleroma.Instances.Instance do
def reachable?(url_or_host) when is_binary(url_or_host), do: true
def set_reachable(url_or_host) when is_binary(url_or_host) do
- with host <- host(url_or_host),
- %Instance{} = existing_record <- Repo.get_by(Instance, %{host: host}) do
- {:ok, _instance} =
- existing_record
- |> changeset(%{unreachable_since: nil})
- |> Repo.update()
- end
+ %Instance{host: host(url_or_host)}
+ |> changeset(%{unreachable_since: nil})
+ |> Repo.insert(on_conflict: {:replace, [:unreachable_since]}, conflict_target: :host)
end
def set_reachable(_), do: {:error, nil}
@@ -177,7 +173,7 @@ defmodule Pleroma.Instances.Instance do
end
rescue
e ->
- Logger.warn("Instance.get_or_update_favicon(\"#{host}\") error: #{inspect(e)}")
+ Logger.warning("Instance.get_or_update_favicon(\"#{host}\") error: #{inspect(e)}")
nil
end
@@ -205,7 +201,7 @@ defmodule Pleroma.Instances.Instance do
end
rescue
e ->
- Logger.warn(
+ Logger.warning(
"Instance.scrape_favicon(\"#{to_string(instance_uri)}\") error: #{inspect(e)}"
)
@@ -288,7 +284,7 @@ defmodule Pleroma.Instances.Instance do
end
rescue
e ->
- Logger.warn(
+ Logger.warning(
"Instance.scrape_metadata(\"#{to_string(instance_uri)}\") error: #{inspect(e)}"
)
@@ -301,7 +297,7 @@ defmodule Pleroma.Instances.Instance do
all of those users' activities and notifications.
"""
def delete_users_and_activities(host) when is_binary(host) do
- BackgroundWorker.enqueue("delete_instance", %{"host" => host})
+ DeleteWorker.enqueue("delete_instance", %{"host" => host})
end
def perform(:delete_instance, host) when is_binary(host) do
diff --git a/lib/pleroma/maintenance.ex b/lib/pleroma/maintenance.ex
index eb5a6ef42..1e39b03e6 100644
--- a/lib/pleroma/maintenance.ex
+++ b/lib/pleroma/maintenance.ex
@@ -20,7 +20,7 @@ defmodule Pleroma.Maintenance do
"full" ->
Logger.info("Running VACUUM FULL.")
- Logger.warn(
+ Logger.warning(
"Re-packing your entire database may take a while and will consume extra disk space during the process."
)
diff --git a/lib/pleroma/maps.ex b/lib/pleroma/maps.ex
index 6d586e53e..5020a8ff8 100644
--- a/lib/pleroma/maps.ex
+++ b/lib/pleroma/maps.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Maps do
@@ -18,4 +18,17 @@ defmodule Pleroma.Maps do
rescue
_ -> data
end
+
+ def filter_empty_values(data) do
+ # TODO: Change to Map.filter in Elixir 1.13+
+ data
+ |> Enum.filter(fn
+ {_k, nil} -> false
+ {_k, ""} -> false
+ {_k, []} -> false
+ {_k, %{} = v} -> Map.keys(v) != []
+ {_k, _v} -> true
+ end)
+ |> Map.new()
+ end
end
diff --git a/lib/pleroma/mfa.ex b/lib/pleroma/mfa.ex
index 01b730c76..ce30cd4ad 100644
--- a/lib/pleroma/mfa.ex
+++ b/lib/pleroma/mfa.ex
@@ -77,7 +77,7 @@ defmodule Pleroma.MFA do
{:ok, codes}
else
{:error, msg} ->
- %{error: msg}
+ {:error, msg}
end
end
diff --git a/lib/pleroma/mfa/totp.ex b/lib/pleroma/mfa/totp.ex
index 429c4b700..96fa8d71d 100644
--- a/lib/pleroma/mfa/totp.ex
+++ b/lib/pleroma/mfa/totp.ex
@@ -14,6 +14,7 @@ defmodule Pleroma.MFA.TOTP do
@doc """
https://github.com/google/google-authenticator/wiki/Key-Uri-Format
"""
+ @spec provisioning_uri(String.t(), String.t(), list()) :: String.t()
def provisioning_uri(secret, label, opts \\ []) do
query =
%{
@@ -27,7 +28,7 @@ defmodule Pleroma.MFA.TOTP do
|> URI.encode_query()
%URI{scheme: "otpauth", host: "totp", path: "/" <> label, query: query}
- |> URI.to_string()
+ |> to_string()
end
defp default_period, do: Config.get(@config_ns ++ [:period])
diff --git a/lib/pleroma/migrators/hashtags_table_migrator.ex b/lib/pleroma/migrators/hashtags_table_migrator.ex
index dca4bfa6f..bd4dd2f1d 100644
--- a/lib/pleroma/migrators/hashtags_table_migrator.ex
+++ b/lib/pleroma/migrators/hashtags_table_migrator.ex
@@ -100,7 +100,7 @@ defmodule Pleroma.Migrators.HashtagsTableMigrator do
|> where([_o, hashtags_objects], is_nil(hashtags_objects.object_id))
end
- @spec transfer_object_hashtags(Map.t()) :: {:noop | :ok | :error, integer()}
+ @spec transfer_object_hashtags(map()) :: {:noop | :ok | :error, integer()}
defp transfer_object_hashtags(object) do
embedded_tags = if Map.has_key?(object, :tag), do: object.tag, else: object.data["tag"]
hashtags = Object.object_data_hashtags(%{"tag" => embedded_tags})
diff --git a/lib/pleroma/migrators/support/base_migrator.ex b/lib/pleroma/migrators/support/base_migrator.ex
index 3bcd59fd0..76a5d4590 100644
--- a/lib/pleroma/migrators/support/base_migrator.ex
+++ b/lib/pleroma/migrators/support/base_migrator.ex
@@ -73,7 +73,7 @@ defmodule Pleroma.Migrators.Support.BaseMigrator do
data_migration.state == :manual or data_migration.name in manual_migrations ->
message = "Data migration is in manual execution or manual fix mode."
update_status(:manual, message)
- Logger.warn("#{__MODULE__}: #{message}")
+ Logger.warning("#{__MODULE__}: #{message}")
data_migration.state == :complete ->
on_complete(data_migration)
@@ -109,7 +109,7 @@ defmodule Pleroma.Migrators.Support.BaseMigrator do
Putting data migration to manual fix mode. Try running `#{__MODULE__}.retry_failed/0`.
"""
- Logger.warn("#{__MODULE__}: #{message}")
+ Logger.warning("#{__MODULE__}: #{message}")
update_status(:manual, message)
on_complete(data_migration())
@@ -125,7 +125,7 @@ defmodule Pleroma.Migrators.Support.BaseMigrator do
defp on_complete(data_migration) do
if data_migration.feature_lock || feature_state() == :disabled do
- Logger.warn(
+ Logger.warning(
"#{__MODULE__}: migration complete but feature is locked; consider enabling."
)
@@ -188,10 +188,11 @@ defmodule Pleroma.Migrators.Support.BaseMigrator do
end
defp fault_rate do
- with failures_count when is_integer(failures_count) <- failures_count() do
+ with failures_count when is_integer(failures_count) <- failures_count(),
+ true <- failures_count > 0 do
failures_count / Enum.max([get_stat(:affected_count, 0), 1])
else
- _ -> :error
+ _ -> 0
end
end
diff --git a/lib/pleroma/moderation_log.ex b/lib/pleroma/moderation_log.ex
index 7203423e2..5c3ca58b0 100644
--- a/lib/pleroma/moderation_log.ex
+++ b/lib/pleroma/moderation_log.ex
@@ -121,7 +121,7 @@ defmodule Pleroma.ModerationLog do
defp prepare_log_data(attrs), do: attrs
- @spec insert_log(log_params()) :: {:ok, ModerationLog} | {:error, any}
+ @spec insert_log(log_params()) :: {:ok, ModerationLog.t()} | {:error, any}
def insert_log(%{actor: %User{}, subject: subjects, permission: permission} = attrs) do
data =
attrs
@@ -248,7 +248,8 @@ defmodule Pleroma.ModerationLog do
|> insert_log_entry_with_message()
end
- @spec insert_log_entry_with_message(ModerationLog) :: {:ok, ModerationLog} | {:error, any}
+ @spec insert_log_entry_with_message(ModerationLog.t()) ::
+ {:ok, ModerationLog.t()} | {:error, any}
defp insert_log_entry_with_message(entry) do
entry.data["message"]
|> put_in(get_log_entry_message(entry))
diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex
index 48d467c59..75f4ba503 100644
--- a/lib/pleroma/notification.ex
+++ b/lib/pleroma/notification.ex
@@ -73,6 +73,7 @@ defmodule Pleroma.Notification do
pleroma:report
reblog
poll
+ status
}
def changeset(%Notification{} = notification, attrs) do
@@ -88,7 +89,7 @@ defmodule Pleroma.Notification do
where: q.seen == true,
select: type(q.id, :string),
limit: 1,
- order_by: [desc: :id]
+ order_by: fragment("? desc nulls last", q.id)
)
end
@@ -137,7 +138,7 @@ defmodule Pleroma.Notification do
blocked_ap_ids = opts[:blocked_users_ap_ids] || User.blocked_users_ap_ids(user)
query
- |> where([n, a], a.actor not in ^blocked_ap_ids)
+ |> where([..., user_actor: user_actor], user_actor.ap_id not in ^blocked_ap_ids)
|> FollowingRelationship.keep_following_or_not_domain_blocked(user)
end
@@ -148,7 +149,7 @@ defmodule Pleroma.Notification do
blocker_ap_ids = User.incoming_relationships_ungrouped_ap_ids(user, [:block])
query
- |> where([n, a], a.actor not in ^blocker_ap_ids)
+ |> where([..., user_actor: user_actor], user_actor.ap_id not in ^blocker_ap_ids)
end
end
@@ -161,7 +162,7 @@ defmodule Pleroma.Notification do
opts[:notification_muted_users_ap_ids] || User.notification_muted_users_ap_ids(user)
query
- |> where([n, a], a.actor not in ^notification_muted_ap_ids)
+ |> where([..., user_actor: user_actor], user_actor.ap_id not in ^notification_muted_ap_ids)
|> join(:left, [n, a], tm in ThreadMute,
on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data),
as: :thread_mute
@@ -280,15 +281,10 @@ defmodule Pleroma.Notification do
select: n.id
)
- {:ok, %{ids: {_, notification_ids}}} =
- Multi.new()
- |> Multi.update_all(:ids, query, set: [seen: true, updated_at: NaiveDateTime.utc_now()])
- |> Marker.multi_set_last_read_id(user, "notifications")
- |> Repo.transaction()
-
- for_user_query(user)
- |> where([n], n.id in ^notification_ids)
- |> Repo.all()
+ Multi.new()
+ |> Multi.update_all(:ids, query, set: [seen: true, updated_at: NaiveDateTime.utc_now()])
+ |> Marker.multi_set_last_read_id(user, "notifications")
+ |> Repo.transaction()
end
@spec read_one(User.t(), String.t()) ::
@@ -299,10 +295,6 @@ defmodule Pleroma.Notification do
|> Multi.update(:update, changeset(notification, %{seen: true}))
|> Marker.multi_set_last_read_id(user, "notifications")
|> Repo.transaction()
- |> case do
- {:ok, %{update: notification}} -> {:ok, notification}
- {:error, :update, changeset, _} -> {:error, changeset}
- end
end
end
@@ -361,37 +353,38 @@ defmodule Pleroma.Notification do
end
end
- @spec create_notifications(Activity.t(), keyword()) :: {:ok, [Notification.t()] | []}
- def create_notifications(activity, options \\ [])
+ @spec create_notifications(Activity.t()) :: {:ok, [Notification.t()] | []}
+ def create_notifications(activity)
- def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity, options) do
+ def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity) do
object = Object.normalize(activity, fetch: false)
if object && object.data["type"] == "Answer" do
{:ok, []}
else
- do_create_notifications(activity, options)
+ do_create_notifications(activity)
end
end
- def create_notifications(%Activity{data: %{"type" => type}} = activity, options)
+ def create_notifications(%Activity{data: %{"type" => type}} = activity)
when type in ["Follow", "Like", "Announce", "Move", "EmojiReact", "Flag", "Update"] do
- do_create_notifications(activity, options)
+ do_create_notifications(activity)
end
- def create_notifications(_, _), do: {:ok, []}
+ def create_notifications(_), do: {:ok, []}
- defp do_create_notifications(%Activity{} = activity, options) do
- do_send = Keyword.get(options, :do_send, true)
+ defp do_create_notifications(%Activity{} = activity) do
+ enabled_receivers = get_notified_from_activity(activity)
- {enabled_receivers, disabled_receivers} = get_notified_from_activity(activity)
- potential_receivers = enabled_receivers ++ disabled_receivers
+ enabled_subscribers = get_notified_subscribers_from_activity(activity)
notifications =
- Enum.map(potential_receivers, fn user ->
- do_send = do_send && user in enabled_receivers
- create_notification(activity, user, do_send: do_send)
- end)
+ (Enum.map(enabled_receivers, fn user ->
+ create_notification(activity, user)
+ end) ++
+ Enum.map(enabled_subscribers -- enabled_receivers, fn user ->
+ create_notification(activity, user, type: "status")
+ end))
|> Enum.reject(&is_nil/1)
{:ok, notifications}
@@ -450,7 +443,6 @@ defmodule Pleroma.Notification do
# TODO move to sql, too.
def create_notification(%Activity{} = activity, %User{} = user, opts \\ []) do
- do_send = Keyword.get(opts, :do_send, true)
type = Keyword.get(opts, :type, type_from_activity(activity))
unless skip?(activity, user, opts) do
@@ -465,11 +457,6 @@ defmodule Pleroma.Notification do
|> Marker.multi_set_last_read_id(user, "notifications")
|> Repo.transaction()
- if do_send do
- Streamer.stream(["user", "user:notification"], notification)
- Push.send(notification)
- end
-
notification
end
end
@@ -502,7 +489,7 @@ defmodule Pleroma.Notification do
NOTE: might be called for FAKE Activities, see ActivityPub.Utils.get_notified_from_object/1
"""
- @spec get_notified_from_activity(Activity.t(), boolean()) :: {list(User.t()), list(User.t())}
+ @spec get_notified_from_activity(Activity.t(), boolean()) :: list(User.t())
def get_notified_from_activity(activity, local_only \\ true)
def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only)
@@ -527,13 +514,28 @@ defmodule Pleroma.Notification do
|> exclude_relationship_restricted_ap_ids(activity)
|> exclude_thread_muter_ap_ids(activity)
- notification_enabled_users =
- Enum.filter(potential_receivers, fn u -> u.ap_id in notification_enabled_ap_ids end)
+ Enum.filter(potential_receivers, fn u -> u.ap_id in notification_enabled_ap_ids end)
+ end
+
+ def get_notified_from_activity(_, _local_only), do: []
+
+ def get_notified_subscribers_from_activity(activity, local_only \\ true)
- {notification_enabled_users, potential_receivers -- notification_enabled_users}
+ def get_notified_subscribers_from_activity(
+ %Activity{data: %{"type" => "Create"}} = activity,
+ local_only
+ ) do
+ notification_enabled_ap_ids =
+ []
+ |> Utils.maybe_notify_subscribers(activity)
+
+ potential_receivers =
+ User.get_users_from_set(notification_enabled_ap_ids, local_only: local_only)
+
+ Enum.filter(potential_receivers, fn u -> u.ap_id in notification_enabled_ap_ids end)
end
- def get_notified_from_activity(_, _local_only), do: {[], []}
+ def get_notified_subscribers_from_activity(_, _), do: []
# For some activities, only notify the author of the object
def get_potential_receiver_ap_ids(%{data: %{"type" => type, "object" => object_id}})
@@ -576,7 +578,6 @@ defmodule Pleroma.Notification do
[]
|> Utils.maybe_notify_to_recipients(activity)
|> Utils.maybe_notify_mentioned_recipients(activity)
- |> Utils.maybe_notify_subscribers(activity)
|> Utils.maybe_notify_followers(activity)
|> Enum.uniq()
end
@@ -643,6 +644,7 @@ defmodule Pleroma.Notification do
def skip?(%Activity{} = activity, %User{} = user, opts) do
[
:self,
+ :internal,
:invisible,
:block_from_strangers,
:recently_followed,
@@ -662,6 +664,12 @@ defmodule Pleroma.Notification do
end
end
+ def skip?(:internal, %Activity{} = activity, _user, _opts) do
+ actor = activity.data["actor"]
+ user = User.get_cached_by_ap_id(actor)
+ User.internal?(user)
+ end
+
def skip?(:invisible, %Activity{} = activity, _user, _opts) do
actor = activity.data["actor"]
user = User.get_cached_by_ap_id(actor)
@@ -726,7 +734,7 @@ defmodule Pleroma.Notification do
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)
+ User.mutes_user?(target_user, user) || CommonAPI.thread_muted?(activity, target_user)
end
def for_user_and_activity(user, activity) do
@@ -748,4 +756,13 @@ defmodule Pleroma.Notification do
)
|> Repo.update_all(set: [seen: true])
end
+
+ @doc "Streams a list of notifications over websockets and web push"
+ @spec stream(list(Notification.t())) :: :ok
+ def stream(notifications) do
+ Enum.each(notifications, fn notification ->
+ Streamer.stream(["user", "user:notification"], notification)
+ Push.send(notification)
+ end)
+ end
end
diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex
index aa137d250..eb44b3855 100644
--- a/lib/pleroma/object.ex
+++ b/lib/pleroma/object.ex
@@ -99,21 +99,24 @@ defmodule Pleroma.Object do
def get_by_id(nil), do: nil
def get_by_id(id), do: Repo.get(Object, id)
+ @spec get_by_id_and_maybe_refetch(integer(), list()) :: Object.t() | nil
def get_by_id_and_maybe_refetch(id, opts \\ []) do
- %{updated_at: updated_at} = object = get_by_id(id)
-
- if opts[:interval] &&
- NaiveDateTime.diff(NaiveDateTime.utc_now(), updated_at) > opts[:interval] do
- case Fetcher.refetch_object(object) do
- {:ok, %Object{} = object} ->
- object
-
- e ->
- Logger.error("Couldn't refresh #{object.data["id"]}:\n#{inspect(e)}")
- object
+ with %Object{updated_at: updated_at} = object <- get_by_id(id) do
+ if opts[:interval] &&
+ NaiveDateTime.diff(NaiveDateTime.utc_now(), updated_at) > opts[:interval] do
+ case Fetcher.refetch_object(object) do
+ {:ok, %Object{} = object} ->
+ object
+
+ e ->
+ Logger.error("Couldn't refresh #{object.data["id"]}:\n#{inspect(e)}")
+ object
+ end
+ else
+ object
end
else
- object
+ nil -> nil
end
end
@@ -177,7 +180,10 @@ defmodule Pleroma.Object do
ap_id
Keyword.get(options, :fetch) ->
- Fetcher.fetch_object_from_id!(ap_id, options)
+ case Fetcher.fetch_object_from_id(ap_id, options) do
+ {:ok, object} -> object
+ _ -> nil
+ end
true ->
get_cached_by_ap_id(ap_id)
@@ -239,17 +245,17 @@ defmodule Pleroma.Object do
{:ok, _} <- invalid_object_cache(object) do
cleanup_attachments(
Config.get([:instance, :cleanup_attachments]),
- %{"object" => object}
+ object
)
{:ok, object, deleted_activity}
end
end
- @spec cleanup_attachments(boolean(), %{required(:object) => map()}) ::
+ @spec cleanup_attachments(boolean(), Object.t()) ::
{:ok, Oban.Job.t() | nil}
- def cleanup_attachments(true, %{"object" => _} = params) do
- AttachmentsCleanupWorker.enqueue("cleanup_attachments", params)
+ def cleanup_attachments(true, %Object{} = object) do
+ AttachmentsCleanupWorker.enqueue("cleanup_attachments", %{"object" => object})
end
def cleanup_attachments(_, _), do: {:ok, nil}
@@ -328,6 +334,52 @@ defmodule Pleroma.Object do
end
end
+ def increase_quotes_count(ap_id) do
+ Object
+ |> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
+ |> update([o],
+ set: [
+ data:
+ fragment(
+ """
+ safe_jsonb_set(?, '{quotesCount}',
+ (coalesce((?->>'quotesCount')::int, 0) + 1)::varchar::jsonb, true)
+ """,
+ o.data,
+ o.data
+ )
+ ]
+ )
+ |> Repo.update_all([])
+ |> case do
+ {1, [object]} -> set_cache(object)
+ _ -> {:error, "Not found"}
+ end
+ end
+
+ def decrease_quotes_count(ap_id) do
+ Object
+ |> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
+ |> update([o],
+ set: [
+ data:
+ fragment(
+ """
+ safe_jsonb_set(?, '{quotesCount}',
+ (greatest(0, (?->>'quotesCount')::int - 1))::varchar::jsonb, true)
+ """,
+ o.data,
+ o.data
+ )
+ ]
+ )
+ |> Repo.update_all([])
+ |> case do
+ {1, [object]} -> set_cache(object)
+ _ -> {:error, "Not found"}
+ end
+ end
+
def increase_vote_count(ap_id, name, actor) do
with %Object{} = object <- Object.normalize(ap_id, fetch: false),
"Question" <- object.data["type"] do
diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex
index cc3772563..c0f671dd4 100644
--- a/lib/pleroma/object/fetcher.ex
+++ b/lib/pleroma/object/fetcher.ex
@@ -59,6 +59,7 @@ defmodule Pleroma.Object.Fetcher do
end
# Note: will create a Create activity, which we need internally at the moment.
+ @spec fetch_object_from_id(String.t(), list()) :: {:ok, Object.t()} | {:error | :reject, any()}
def fetch_object_from_id(id, options \\ []) do
with {_, nil} <- {:fetch_object, Object.get_cached_by_ap_id(id)},
{_, true} <- {:allowed_depth, Federator.allowed_thread_distance?(options[:depth])},
@@ -72,20 +73,25 @@ defmodule Pleroma.Object.Fetcher do
{:object, data, Object.normalize(activity, fetch: false)} do
{:ok, object}
else
- {:allowed_depth, false} ->
- {:error, "Max thread distance exceeded."}
+ {:allowed_depth, false} = e ->
+ log_fetch_error(id, e)
+ {:error, :allowed_depth}
- {:containment, _} ->
- {:error, "Object containment failed."}
+ {:containment, reason} = e ->
+ log_fetch_error(id, e)
+ {:error, reason}
- {:transmogrifier, {:error, {:reject, e}}} ->
- {:reject, e}
+ {:transmogrifier, {:error, {:reject, reason}}} = e ->
+ log_fetch_error(id, e)
+ {:reject, reason}
- {:transmogrifier, {:reject, e}} ->
- {:reject, e}
+ {:transmogrifier, {:reject, reason}} = e ->
+ log_fetch_error(id, e)
+ {:reject, reason}
- {:transmogrifier, _} = e ->
- {:error, e}
+ {:transmogrifier, reason} = e ->
+ log_fetch_error(id, e)
+ {:error, reason}
{:object, data, nil} ->
reinject_object(%Object{}, data)
@@ -96,14 +102,21 @@ defmodule Pleroma.Object.Fetcher do
{:fetch_object, %Object{} = object} ->
{:ok, object}
- {:fetch, {:error, error}} ->
- {:error, error}
+ {:fetch, {:error, reason}} = e ->
+ log_fetch_error(id, e)
+ {:error, reason}
e ->
- e
+ log_fetch_error(id, e)
+ {:error, e}
end
end
+ defp log_fetch_error(id, error) do
+ Logger.metadata(object: id)
+ Logger.error("Object rejected while fetching #{id} #{inspect(error)}")
+ end
+
defp prepare_activity_params(data) do
%{
"type" => "Create",
@@ -117,26 +130,6 @@ defmodule Pleroma.Object.Fetcher do
|> Maps.put_if_present("bcc", data["bcc"])
end
- def fetch_object_from_id!(id, options \\ []) do
- with {:ok, object} <- fetch_object_from_id(id, options) do
- object
- else
- {:error, %Tesla.Mock.Error{}} ->
- nil
-
- {:error, "Object has been deleted"} ->
- nil
-
- {:reject, reason} ->
- Logger.info("Rejected #{id} while fetching: #{inspect(reason)}")
- nil
-
- e ->
- Logger.error("Error while fetching #{id}: #{inspect(e)}")
- nil
- end
- end
-
defp make_signature(id, date) do
uri = URI.parse(id)
@@ -227,8 +220,11 @@ defmodule Pleroma.Object.Fetcher do
{:error, {:content_type, nil}}
end
+ {:ok, %{status: code}} when code in [401, 403] ->
+ {:error, :forbidden}
+
{:ok, %{status: code}} when code in [404, 410] ->
- {:error, "Object has been deleted"}
+ {:error, :not_found}
{:error, e} ->
{:error, e}
diff --git a/lib/pleroma/object/updater.ex b/lib/pleroma/object/updater.ex
index b1e4870ba..b80bc7faf 100644
--- a/lib/pleroma/object/updater.ex
+++ b/lib/pleroma/object/updater.ex
@@ -134,7 +134,10 @@ defmodule Pleroma.Object.Updater do
else
%{updated_object: updated_data} =
updated_data
- |> maybe_update_history(original_data, updated: updated, use_history_in_new_object?: false)
+ |> maybe_update_history(original_data,
+ updated: updated,
+ use_history_in_new_object?: false
+ )
updated_data
|> Map.put("updated", date)
diff --git a/lib/pleroma/pagination.ex b/lib/pleroma/pagination.ex
index f12ca2819..8db732cc9 100644
--- a/lib/pleroma/pagination.ex
+++ b/lib/pleroma/pagination.ex
@@ -61,15 +61,16 @@ defmodule Pleroma.Pagination do
|> Repo.all()
end
- @spec paginate(Ecto.Query.t(), map(), type(), atom() | nil) :: [Ecto.Schema.t()]
- def paginate(query, options, method \\ :keyset, table_binding \\ nil)
-
- def paginate(list, options, _method, _table_binding) when is_list(list) do
+ @spec paginate_list(list(), keyword()) :: list()
+ def paginate_list(list, options) do
offset = options[:offset] || 0
limit = options[:limit] || 0
Enum.slice(list, offset, limit)
end
+ @spec paginate(Ecto.Query.t(), map(), type(), atom() | nil) :: [Ecto.Schema.t()]
+ def paginate(query, options, method \\ :keyset, table_binding \\ nil)
+
def paginate(query, options, :keyset, table_binding) do
query
|> restrict(:min_id, options, table_binding)
diff --git a/lib/pleroma/password/pbkdf2.ex b/lib/pleroma/password/pbkdf2.ex
index 92e9e1952..9c6d2e381 100644
--- a/lib/pleroma/password/pbkdf2.ex
+++ b/lib/pleroma/password/pbkdf2.ex
@@ -28,7 +28,7 @@ defmodule Pleroma.Password.Pbkdf2 do
iterations = String.to_integer(iterations)
- digest = String.to_atom(digest)
+ digest = String.to_existing_atom(digest)
binary_hash =
KeyGenerator.generate(password, salt, digest: digest, iterations: iterations, length: 64)
diff --git a/lib/pleroma/prom_ex.ex b/lib/pleroma/prom_ex.ex
new file mode 100644
index 000000000..6608708b7
--- /dev/null
+++ b/lib/pleroma/prom_ex.ex
@@ -0,0 +1,49 @@
+defmodule Pleroma.PromEx do
+ use PromEx, otp_app: :pleroma
+
+ alias PromEx.Plugins
+
+ @impl true
+ def plugins do
+ [
+ # PromEx built in plugins
+ Plugins.Application,
+ Plugins.Beam,
+ {Plugins.Phoenix, router: Pleroma.Web.Router, endpoint: Pleroma.Web.Endpoint},
+ Plugins.Ecto,
+ Plugins.Oban
+ # Plugins.PhoenixLiveView,
+ # Plugins.Absinthe,
+ # Plugins.Broadway,
+
+ # Add your own PromEx metrics plugins
+ # Pleroma.Users.PromExPlugin
+ ]
+ end
+
+ @impl true
+ def dashboard_assigns do
+ [
+ datasource_id: Pleroma.Config.get([Pleroma.PromEx, :datasource]),
+ default_selected_interval: "30s"
+ ]
+ end
+
+ @impl true
+ def dashboards do
+ [
+ # PromEx built in Grafana dashboards
+ {:prom_ex, "application.json"},
+ {:prom_ex, "beam.json"},
+ {:prom_ex, "phoenix.json"},
+ {:prom_ex, "ecto.json"},
+ {:prom_ex, "oban.json"}
+ # {:prom_ex, "phoenix_live_view.json"},
+ # {:prom_ex, "absinthe.json"},
+ # {:prom_ex, "broadway.json"},
+
+ # Add your dashboard definitions here with the format: {:otp_app, "path_in_priv"}
+ # {:pleroma, "/grafana_dashboards/user_metrics.json"}
+ ]
+ end
+end
diff --git a/lib/pleroma/release_tasks.ex b/lib/pleroma/release_tasks.ex
index f9e8d1948..bcfcd1243 100644
--- a/lib/pleroma/release_tasks.ex
+++ b/lib/pleroma/release_tasks.ex
@@ -55,12 +55,6 @@ defmodule Pleroma.ReleaseTasks do
{:error, term} when is_binary(term) ->
IO.puts(:stderr, "The database for #{inspect(@repo)} couldn't be created: #{term}")
-
- {:error, term} ->
- IO.puts(
- :stderr,
- "The database for #{inspect(@repo)} couldn't be created: #{inspect(term)}"
- )
end
end
end
diff --git a/lib/pleroma/repo.ex b/lib/pleroma/repo.ex
index 515b0c1ff..a50a59b3b 100644
--- a/lib/pleroma/repo.ex
+++ b/lib/pleroma/repo.ex
@@ -11,8 +11,6 @@ defmodule Pleroma.Repo do
import Ecto.Query
require Logger
- defmodule Instrumenter, do: use(Prometheus.EctoInstrumenter)
-
@doc """
Dynamically loads the repository url from the
DATABASE_URL environment variable.
diff --git a/lib/pleroma/report_note.ex b/lib/pleroma/report_note.ex
index f2ad76fa8..f59e5451b 100644
--- a/lib/pleroma/report_note.ex
+++ b/lib/pleroma/report_note.ex
@@ -23,8 +23,8 @@ defmodule Pleroma.ReportNote do
timestamps()
end
- @spec create(FlakeId.Ecto.CompatType.t(), FlakeId.Ecto.CompatType.t(), String.t()) ::
- {:ok, ReportNote.t()} | {:error, Changeset.t()}
+ @spec create(Ecto.UUID.t(), Ecto.UUID.t(), String.t()) ::
+ {:ok, ReportNote.t()} | {:error, Ecto.Changeset.t()}
def create(user_id, activity_id, content) do
attrs = %{
user_id: user_id,
@@ -38,8 +38,8 @@ defmodule Pleroma.ReportNote do
|> Repo.insert()
end
- @spec destroy(FlakeId.Ecto.CompatType.t()) ::
- {:ok, ReportNote.t()} | {:error, Changeset.t()}
+ @spec destroy(Ecto.UUID.t()) ::
+ {:ok, ReportNote.t()} | {:error, Ecto.Changeset.t()}
def destroy(id) do
from(r in ReportNote, where: r.id == ^id)
|> Repo.one()
diff --git a/lib/pleroma/reverse_proxy.ex b/lib/pleroma/reverse_proxy.ex
index 2248c2713..8aec4ae58 100644
--- a/lib/pleroma/reverse_proxy.ex
+++ b/lib/pleroma/reverse_proxy.ex
@@ -81,16 +81,16 @@ defmodule Pleroma.ReverseProxy do
import Plug.Conn
@type option() ::
- {:max_read_duration, :timer.time() | :infinity}
+ {:max_read_duration, non_neg_integer() | :infinity}
| {:max_body_length, non_neg_integer() | :infinity}
- | {:failed_request_ttl, :timer.time() | :infinity}
- | {:http, []}
+ | {:failed_request_ttl, non_neg_integer() | :infinity}
+ | {:http, keyword()}
| {:req_headers, [{String.t(), String.t()}]}
| {:resp_headers, [{String.t(), String.t()}]}
- | {:inline_content_types, boolean() | [String.t()]}
+ | {:inline_content_types, boolean() | list(String.t())}
| {:redirect_on_failure, boolean()}
- @spec call(Plug.Conn.t(), url :: String.t(), [option()]) :: Plug.Conn.t()
+ @spec call(Plug.Conn.t(), String.t(), list(option())) :: Plug.Conn.t()
def call(_conn, _url, _opts \\ [])
def call(conn = %{method: method}, url, opts) when method in @methods do
@@ -180,6 +180,7 @@ defmodule Pleroma.ReverseProxy do
result =
conn
|> put_resp_headers(build_resp_headers(headers, opts))
+ |> streaming_compat
|> send_chunked(status)
|> chunk_reply(client, opts)
@@ -192,7 +193,7 @@ defmodule Pleroma.ReverseProxy do
halt(conn)
{:error, error, conn} ->
- Logger.warn(
+ Logger.warning(
"#{__MODULE__} request to #{url} failed while reading/chunking: #{inspect(error)}"
)
@@ -388,8 +389,6 @@ 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
@@ -407,10 +406,6 @@ defmodule Pleroma.ReverseProxy do
{:ok, previous_duration + duration}
end
- defp increase_read_duration(_) do
- {:ok, :no_duration_limit, :no_duration_limit}
- end
-
defp client, do: Pleroma.ReverseProxy.Client.Wrapper
defp track_failed_url(url, error, opts) do
@@ -423,4 +418,17 @@ defmodule Pleroma.ReverseProxy do
@cachex.put(:failed_proxy_url_cache, url, true, ttl: ttl)
end
+
+ # When Cowboy handles a chunked response with a content-length header it streams
+ # over HTTP 1.1 instead of chunking. Bandit cannot stream over HTTP 1.1 so the header
+ # must be stripped or it breaks RFC compliance for Transfer Encoding: Chunked. RFC9112§6.2
+ #
+ # HTTP2 is always streamed for all adapters.
+ defp streaming_compat(conn) do
+ with Phoenix.Endpoint.Cowboy2Adapter <- Pleroma.Web.Endpoint.config(:adapter) do
+ conn
+ else
+ _ -> delete_resp_header(conn, "content-length")
+ end
+ end
end
diff --git a/lib/pleroma/rule.ex b/lib/pleroma/rule.ex
new file mode 100644
index 000000000..3ba413214
--- /dev/null
+++ b/lib/pleroma/rule.ex
@@ -0,0 +1,68 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Rule do
+ use Ecto.Schema
+
+ import Ecto.Changeset
+ import Ecto.Query
+
+ alias Pleroma.Repo
+ alias Pleroma.Rule
+
+ schema "rules" do
+ field(:priority, :integer, default: 0)
+ field(:text, :string)
+ field(:hint, :string)
+
+ timestamps()
+ end
+
+ def changeset(%Rule{} = rule, params \\ %{}) do
+ rule
+ |> cast(params, [:priority, :text, :hint])
+ |> validate_required([:text])
+ end
+
+ def query do
+ Rule
+ |> order_by(asc: :priority)
+ |> order_by(asc: :id)
+ end
+
+ def get(ids) when is_list(ids) do
+ from(r in __MODULE__, where: r.id in ^ids)
+ |> Repo.all()
+ end
+
+ def get(id), do: Repo.get(__MODULE__, id)
+
+ def exists?(id) do
+ from(r in __MODULE__, where: r.id == ^id)
+ |> Repo.exists?()
+ end
+
+ def create(params) do
+ {:ok, rule} =
+ %Rule{}
+ |> changeset(params)
+ |> Repo.insert()
+
+ rule
+ end
+
+ def update(params, id) do
+ {:ok, rule} =
+ get(id)
+ |> changeset(params)
+ |> Repo.update()
+
+ rule
+ end
+
+ def delete(id) do
+ get(id)
+ |> Repo.delete()
+ end
+end
diff --git a/lib/pleroma/scheduled_activity.ex b/lib/pleroma/scheduled_activity.ex
index 0ed51ad07..c361d7d89 100644
--- a/lib/pleroma/scheduled_activity.ex
+++ b/lib/pleroma/scheduled_activity.ex
@@ -6,7 +6,6 @@ defmodule Pleroma.ScheduledActivity do
use Ecto.Schema
alias Ecto.Multi
- alias Pleroma.Config
alias Pleroma.Repo
alias Pleroma.ScheduledActivity
alias Pleroma.User
@@ -20,6 +19,8 @@ defmodule Pleroma.ScheduledActivity do
@min_offset :timer.minutes(5)
+ @config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config)
+
schema "scheduled_activities" do
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
field(:scheduled_at, :naive_datetime)
@@ -87,7 +88,7 @@ defmodule Pleroma.ScheduledActivity do
|> where([sa], type(sa.scheduled_at, :date) == type(^scheduled_at, :date))
|> select([sa], count(sa.id))
|> Repo.one()
- |> Kernel.>=(Config.get([ScheduledActivity, :daily_user_limit]))
+ |> Kernel.>=(@config_impl.get([ScheduledActivity, :daily_user_limit]))
end
def exceeds_total_user_limit?(user_id) do
@@ -95,7 +96,7 @@ defmodule Pleroma.ScheduledActivity do
|> where(user_id: ^user_id)
|> select([sa], count(sa.id))
|> Repo.one()
- |> Kernel.>=(Config.get([ScheduledActivity, :total_user_limit]))
+ |> Kernel.>=(@config_impl.get([ScheduledActivity, :total_user_limit]))
end
def far_enough?(scheduled_at) when is_binary(scheduled_at) do
@@ -123,7 +124,7 @@ defmodule Pleroma.ScheduledActivity do
def create(%User{} = user, attrs) do
Multi.new()
|> Multi.insert(:scheduled_activity, new(user, attrs))
- |> maybe_add_jobs(Config.get([ScheduledActivity, :enabled]))
+ |> maybe_add_jobs(@config_impl.get([ScheduledActivity, :enabled]))
|> Repo.transaction()
|> transaction_response
end
@@ -203,7 +204,7 @@ defmodule Pleroma.ScheduledActivity do
def job_query(scheduled_activity_id) do
from(j in Oban.Job,
- where: j.queue == "scheduled_activities",
+ where: j.queue == "federator_outgoing",
where: fragment("args ->> 'activity_id' = ?::text", ^to_string(scheduled_activity_id))
)
end
diff --git a/lib/pleroma/search.ex b/lib/pleroma/search.ex
new file mode 100644
index 000000000..b9d2a0188
--- /dev/null
+++ b/lib/pleroma/search.ex
@@ -0,0 +1,21 @@
+defmodule Pleroma.Search do
+ alias Pleroma.Workers.SearchIndexingWorker
+
+ def add_to_index(%Pleroma.Activity{id: activity_id}) do
+ SearchIndexingWorker.enqueue("add_to_index", %{"activity" => activity_id})
+ end
+
+ def remove_from_index(%Pleroma.Object{id: object_id}) do
+ SearchIndexingWorker.enqueue("remove_from_index", %{"object" => object_id})
+ end
+
+ def search(query, options) do
+ search_module = Pleroma.Config.get([Pleroma.Search, :module])
+ search_module.search(options[:for_user], query, options)
+ end
+
+ def healthcheck_endpoints do
+ search_module = Pleroma.Config.get([Pleroma.Search, :module])
+ search_module.healthcheck_endpoints()
+ end
+end
diff --git a/lib/pleroma/activity/search.ex b/lib/pleroma/search/database_search.ex
index 0b9b24aa4..aef5d1e74 100644
--- a/lib/pleroma/activity/search.ex
+++ b/lib/pleroma/search/database_search.ex
@@ -1,9 +1,10 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Activity.Search do
+defmodule Pleroma.Search.DatabaseSearch do
alias Pleroma.Activity
+ alias Pleroma.Config
alias Pleroma.Object.Fetcher
alias Pleroma.Pagination
alias Pleroma.User
@@ -13,25 +14,21 @@ defmodule Pleroma.Activity.Search do
import Ecto.Query
+ @behaviour Pleroma.Search.SearchBackend
+
+ @impl true
def search(user, search_query, options \\ []) do
- index_type = if Pleroma.Config.get([:database, :rum_enabled]), do: :rum, else: :gin
+ index_type = if Config.get([:database, :rum_enabled]), do: :rum, else: :gin
limit = Enum.min([Keyword.get(options, :limit), 40])
offset = Keyword.get(options, :offset, 0)
author = Keyword.get(options, :author)
- search_function =
- if :persistent_term.get({Pleroma.Repo, :postgres_version}) >= 11 do
- :websearch
- else
- :plain
- end
-
try do
Activity
|> Activity.with_preloaded_object()
|> Activity.restrict_deactivated_users()
|> restrict_public(user)
- |> query_with(index_type, search_query, search_function)
+ |> query_with(index_type, search_query)
|> maybe_restrict_local(user)
|> maybe_restrict_author(author)
|> maybe_restrict_blocked(user)
@@ -45,6 +42,21 @@ defmodule Pleroma.Activity.Search do
end
end
+ @impl true
+ def add_to_index(_activity), do: :ok
+
+ @impl true
+ def remove_from_index(_object), do: :ok
+
+ @impl true
+ def create_index, do: :ok
+
+ @impl true
+ def drop_index, do: :ok
+
+ @impl true
+ def healthcheck_endpoints, do: nil
+
def maybe_restrict_author(query, %User{} = author) do
Activity.Queries.by_author(query, author)
end
@@ -76,25 +88,7 @@ defmodule Pleroma.Activity.Search do
)
end
- defp query_with(q, :gin, search_query, :plain) do
- %{rows: [[tsc]]} =
- Ecto.Adapters.SQL.query!(
- Pleroma.Repo,
- "select current_setting('default_text_search_config')::regconfig::oid;"
- )
-
- from([a, o] in q,
- where:
- fragment(
- "to_tsvector(?::oid::regconfig, ?->>'content') @@ plainto_tsquery(?)",
- ^tsc,
- o.data,
- ^search_query
- )
- )
- end
-
- defp query_with(q, :gin, search_query, :websearch) do
+ defp query_with(q, :gin, search_query) do
%{rows: [[tsc]]} =
Ecto.Adapters.SQL.query!(
Pleroma.Repo,
@@ -112,19 +106,7 @@ defmodule Pleroma.Activity.Search do
)
end
- defp query_with(q, :rum, search_query, :plain) do
- from([a, o] in q,
- where:
- fragment(
- "? @@ plainto_tsquery(?)",
- o.fts_content,
- ^search_query
- ),
- order_by: [fragment("? <=> now()::date", o.inserted_at)]
- )
- end
-
- defp query_with(q, :rum, search_query, :websearch) do
+ defp query_with(q, :rum, search_query) do
from([a, o] in q,
where:
fragment(
@@ -136,8 +118,8 @@ defmodule Pleroma.Activity.Search do
)
end
- defp maybe_restrict_local(q, user) do
- limit = Pleroma.Config.get([:instance, :limit_to_local_content], :unauthenticated)
+ def maybe_restrict_local(q, user) do
+ limit = Config.get([:instance, :limit_to_local_content], :unauthenticated)
case {limit, user} do
{:all, _} -> restrict_local(q)
@@ -149,7 +131,7 @@ defmodule Pleroma.Activity.Search do
defp restrict_local(q), do: where(q, local: true)
- defp maybe_fetch(activities, user, search_query) do
+ def maybe_fetch(activities, user, search_query) do
with true <- Regex.match?(~r/https?:/, search_query),
{:ok, object} <- Fetcher.fetch_object_from_id(search_query),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
diff --git a/lib/pleroma/search/healthcheck.ex b/lib/pleroma/search/healthcheck.ex
new file mode 100644
index 000000000..e562c8478
--- /dev/null
+++ b/lib/pleroma/search/healthcheck.ex
@@ -0,0 +1,86 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+defmodule Pleroma.Search.Healthcheck do
+ @doc """
+ Monitors health of search backend to control processing of events based on health and availability.
+ """
+ use GenServer
+ require Logger
+
+ @queue :search_indexing
+ @tick :timer.seconds(5)
+ @timeout :timer.seconds(2)
+
+ def start_link(_) do
+ GenServer.start_link(__MODULE__, [], name: __MODULE__)
+ end
+
+ @impl true
+ def init(_) do
+ state = %{healthy: false}
+ {:ok, state, {:continue, :start}}
+ end
+
+ @impl true
+ def handle_continue(:start, state) do
+ tick()
+ {:noreply, state}
+ end
+
+ @impl true
+ def handle_info(:check, state) do
+ urls = Pleroma.Search.healthcheck_endpoints()
+
+ new_state =
+ if check(urls) do
+ Oban.resume_queue(queue: @queue)
+ Map.put(state, :healthy, true)
+ else
+ Oban.pause_queue(queue: @queue)
+ Map.put(state, :healthy, false)
+ end
+
+ maybe_log_state_change(state, new_state)
+
+ tick()
+ {:noreply, new_state}
+ end
+
+ @impl true
+ def handle_call(:state, _from, state) do
+ {:reply, state, state, :hibernate}
+ end
+
+ def state, do: GenServer.call(__MODULE__, :state)
+
+ def check([]), do: true
+
+ def check(urls) when is_list(urls) do
+ Enum.all?(
+ urls,
+ fn url ->
+ case Pleroma.HTTP.get(url, [], recv_timeout: @timeout) do
+ {:ok, %{status: 200}} -> true
+ _ -> false
+ end
+ end
+ )
+ end
+
+ def check(_), do: true
+
+ defp tick do
+ Process.send_after(self(), :check, @tick)
+ end
+
+ defp maybe_log_state_change(%{healthy: true}, %{healthy: false}) do
+ Logger.error("Pausing Oban queue #{@queue} due to search backend healthcheck failure")
+ end
+
+ defp maybe_log_state_change(%{healthy: false}, %{healthy: true}) do
+ Logger.info("Resuming Oban queue #{@queue} due to search backend healthcheck pass")
+ end
+
+ defp maybe_log_state_change(_, _), do: :ok
+end
diff --git a/lib/pleroma/search/meilisearch.ex b/lib/pleroma/search/meilisearch.ex
new file mode 100644
index 000000000..9bba5b30f
--- /dev/null
+++ b/lib/pleroma/search/meilisearch.ex
@@ -0,0 +1,198 @@
+defmodule Pleroma.Search.Meilisearch do
+ require Logger
+ require Pleroma.Constants
+
+ alias Pleroma.Activity
+ alias Pleroma.Config.Getting, as: Config
+
+ import Pleroma.Search.DatabaseSearch
+ import Ecto.Query
+
+ @behaviour Pleroma.Search.SearchBackend
+
+ @impl true
+ def create_index, do: :ok
+
+ @impl true
+ def drop_index, do: :ok
+
+ defp meili_headers do
+ private_key = Config.get([Pleroma.Search.Meilisearch, :private_key])
+
+ [{"Content-Type", "application/json"}] ++
+ if is_nil(private_key), do: [], else: [{"Authorization", "Bearer #{private_key}"}]
+ end
+
+ def meili_get(path) do
+ endpoint = Config.get([Pleroma.Search.Meilisearch, :url])
+
+ result =
+ Pleroma.HTTP.get(
+ Path.join(endpoint, path),
+ meili_headers()
+ )
+
+ with {:ok, res} <- result do
+ {:ok, Jason.decode!(res.body)}
+ end
+ end
+
+ def meili_post(path, params) do
+ endpoint = Config.get([Pleroma.Search.Meilisearch, :url])
+
+ result =
+ Pleroma.HTTP.post(
+ Path.join(endpoint, path),
+ Jason.encode!(params),
+ meili_headers()
+ )
+
+ with {:ok, res} <- result do
+ {:ok, Jason.decode!(res.body)}
+ end
+ end
+
+ def meili_put(path, params) do
+ endpoint = Config.get([Pleroma.Search.Meilisearch, :url])
+
+ result =
+ Pleroma.HTTP.request(
+ :put,
+ Path.join(endpoint, path),
+ Jason.encode!(params),
+ meili_headers(),
+ []
+ )
+
+ with {:ok, res} <- result do
+ {:ok, Jason.decode!(res.body)}
+ end
+ end
+
+ def meili_delete(path) do
+ endpoint = Config.get([Pleroma.Search.Meilisearch, :url])
+
+ with {:ok, _} <-
+ Pleroma.HTTP.request(
+ :delete,
+ Path.join(endpoint, path),
+ "",
+ meili_headers(),
+ []
+ ) do
+ :ok
+ else
+ _ -> {:error, "Could not remove from index"}
+ end
+ end
+
+ @impl true
+ def search(user, query, options \\ []) do
+ limit = Enum.min([Keyword.get(options, :limit), 40])
+ offset = Keyword.get(options, :offset, 0)
+ author = Keyword.get(options, :author)
+
+ res =
+ meili_post(
+ "/indexes/objects/search",
+ %{q: query, offset: offset, limit: limit}
+ )
+
+ with {:ok, result} <- res do
+ hits = result["hits"] |> Enum.map(& &1["ap"])
+
+ try do
+ hits
+ |> Activity.create_by_object_ap_id()
+ |> Activity.with_preloaded_object()
+ |> Activity.restrict_deactivated_users()
+ |> maybe_restrict_local(user)
+ |> maybe_restrict_author(author)
+ |> maybe_restrict_blocked(user)
+ |> maybe_fetch(user, query)
+ |> order_by([object: obj], desc: obj.data["published"])
+ |> Pleroma.Repo.all()
+ rescue
+ _ -> maybe_fetch([], user, query)
+ end
+ end
+ end
+
+ def object_to_search_data(object) do
+ # Only index public or unlisted Notes
+ if not is_nil(object) and object.data["type"] == "Note" and
+ not is_nil(object.data["content"]) and
+ (Pleroma.Constants.as_public() in object.data["to"] or
+ Pleroma.Constants.as_public() in object.data["cc"]) and
+ object.data["content"] not in ["", "."] do
+ data = object.data
+
+ content_str =
+ case data["content"] do
+ [nil | rest] -> to_string(rest)
+ str -> str
+ end
+
+ content =
+ with {:ok, scrubbed} <-
+ FastSanitize.Sanitizer.scrub(content_str, Pleroma.HTML.Scrubber.SearchIndexing),
+ trimmed <- String.trim(scrubbed) do
+ trimmed
+ end
+
+ # Make sure we have a non-empty string
+ if content != "" do
+ {:ok, published, _} = DateTime.from_iso8601(data["published"])
+
+ %{
+ id: object.id,
+ content: content,
+ ap: data["id"],
+ published: published |> DateTime.to_unix()
+ }
+ end
+ end
+ end
+
+ @impl true
+ def add_to_index(activity) do
+ maybe_search_data = object_to_search_data(activity.object)
+
+ if activity.data["type"] == "Create" and maybe_search_data do
+ result =
+ meili_put(
+ "/indexes/objects/documents",
+ [maybe_search_data]
+ )
+
+ with {:ok, %{"status" => "enqueued"}} <- result do
+ # Added successfully
+ :ok
+ else
+ _ ->
+ # There was an error, report it
+ Logger.error("Failed to add activity #{activity.id} to index: #{inspect(result)}")
+ {:error, result}
+ end
+ else
+ # The post isn't something we can search, that's ok
+ :ok
+ end
+ end
+
+ @impl true
+ def remove_from_index(object) do
+ meili_delete("/indexes/objects/documents/#{object.id}")
+ end
+
+ @impl true
+ def healthcheck_endpoints do
+ endpoint =
+ Config.get([Pleroma.Search.Meilisearch, :url])
+ |> URI.parse()
+ |> Map.put(:path, "/health")
+ |> URI.to_string()
+
+ [endpoint]
+ end
+end
diff --git a/lib/pleroma/search/qdrant_search.ex b/lib/pleroma/search/qdrant_search.ex
new file mode 100644
index 000000000..b659bb682
--- /dev/null
+++ b/lib/pleroma/search/qdrant_search.ex
@@ -0,0 +1,182 @@
+defmodule Pleroma.Search.QdrantSearch do
+ @behaviour Pleroma.Search.SearchBackend
+ import Ecto.Query
+
+ alias Pleroma.Activity
+ alias Pleroma.Config.Getting, as: Config
+
+ alias __MODULE__.OpenAIClient
+ alias __MODULE__.QdrantClient
+
+ import Pleroma.Search.Meilisearch, only: [object_to_search_data: 1]
+ import Pleroma.Search.DatabaseSearch, only: [maybe_fetch: 3]
+
+ @impl true
+ def create_index do
+ payload = Config.get([Pleroma.Search.QdrantSearch, :qdrant_index_configuration])
+
+ with {:ok, %{status: 200}} <- QdrantClient.put("/collections/posts", payload) do
+ :ok
+ else
+ e -> {:error, e}
+ end
+ end
+
+ @impl true
+ def drop_index do
+ with {:ok, %{status: 200}} <- QdrantClient.delete("/collections/posts") do
+ :ok
+ else
+ e -> {:error, e}
+ end
+ end
+
+ def get_embedding(text) do
+ with {:ok, %{body: %{"data" => [%{"embedding" => embedding}]}}} <-
+ OpenAIClient.post("/v1/embeddings", %{
+ input: text,
+ model: Config.get([Pleroma.Search.QdrantSearch, :openai_model])
+ }) do
+ {:ok, embedding}
+ else
+ _ ->
+ {:error, "Failed to get embedding"}
+ end
+ end
+
+ defp actor_from_activity(%{data: %{"actor" => actor}}) do
+ actor
+ end
+
+ defp actor_from_activity(_), do: nil
+
+ defp build_index_payload(activity, embedding) do
+ actor = actor_from_activity(activity)
+ published_at = activity.data["published"]
+
+ %{
+ points: [
+ %{
+ id: activity.id |> FlakeId.from_string() |> Ecto.UUID.cast!(),
+ vector: embedding,
+ payload: %{actor: actor, published_at: published_at}
+ }
+ ]
+ }
+ end
+
+ defp build_search_payload(embedding, options) do
+ base = %{
+ vector: embedding,
+ limit: options[:limit] || 20,
+ offset: options[:offset] || 0
+ }
+
+ if author = options[:author] do
+ Map.put(base, :filter, %{
+ must: [%{key: "actor", match: %{value: author.ap_id}}]
+ })
+ else
+ base
+ end
+ end
+
+ @impl true
+ def add_to_index(activity) do
+ # This will only index public or unlisted notes
+ maybe_search_data = object_to_search_data(activity.object)
+
+ if activity.data["type"] == "Create" and maybe_search_data do
+ with {:ok, embedding} <- get_embedding(maybe_search_data.content),
+ {:ok, %{status: 200}} <-
+ QdrantClient.put(
+ "/collections/posts/points",
+ build_index_payload(activity, embedding)
+ ) do
+ :ok
+ else
+ e -> {:error, e}
+ end
+ else
+ :ok
+ end
+ end
+
+ @impl true
+ def remove_from_index(object) do
+ activity = Activity.get_by_object_ap_id_with_object(object.data["id"])
+ id = activity.id |> FlakeId.from_string() |> Ecto.UUID.cast!()
+
+ with {:ok, %{status: 200}} <-
+ QdrantClient.post("/collections/posts/points/delete", %{"points" => [id]}) do
+ :ok
+ else
+ e -> {:error, e}
+ end
+ end
+
+ @impl true
+ def search(user, original_query, options) do
+ query = "Represent this sentence for searching relevant passages: #{original_query}"
+
+ with {:ok, embedding} <- get_embedding(query),
+ {:ok, %{body: %{"result" => result}}} <-
+ QdrantClient.post(
+ "/collections/posts/points/search",
+ build_search_payload(embedding, options)
+ ) do
+ ids =
+ Enum.map(result, fn %{"id" => id} ->
+ Ecto.UUID.dump!(id)
+ end)
+
+ from(a in Activity, where: a.id in ^ids)
+ |> Activity.with_preloaded_object()
+ |> Activity.restrict_deactivated_users()
+ |> Ecto.Query.order_by([a], fragment("array_position(?, ?)", ^ids, a.id))
+ |> Pleroma.Repo.all()
+ |> maybe_fetch(user, original_query)
+ else
+ _ ->
+ []
+ end
+ end
+
+ @impl true
+ def healthcheck_endpoints do
+ qdrant_health =
+ Config.get([Pleroma.Search.QdrantSearch, :qdrant_url])
+ |> URI.parse()
+ |> Map.put(:path, "/healthz")
+ |> URI.to_string()
+
+ openai_health = Config.get([Pleroma.Search.QdrantSearch, :openai_healthcheck_url])
+
+ [qdrant_health, openai_health] |> Enum.filter(& &1)
+ end
+end
+
+defmodule Pleroma.Search.QdrantSearch.OpenAIClient do
+ use Tesla
+ alias Pleroma.Config.Getting, as: Config
+
+ plug(Tesla.Middleware.BaseUrl, Config.get([Pleroma.Search.QdrantSearch, :openai_url]))
+ plug(Tesla.Middleware.JSON)
+
+ plug(Tesla.Middleware.Headers, [
+ {"Authorization",
+ "Bearer #{Pleroma.Config.get([Pleroma.Search.QdrantSearch, :openai_api_key])}"}
+ ])
+end
+
+defmodule Pleroma.Search.QdrantSearch.QdrantClient do
+ use Tesla
+ alias Pleroma.Config.Getting, as: Config
+
+ plug(Tesla.Middleware.BaseUrl, Config.get([Pleroma.Search.QdrantSearch, :qdrant_url]))
+ plug(Tesla.Middleware.JSON)
+
+ plug(Tesla.Middleware.Headers, [
+ {"api-key", Pleroma.Config.get([Pleroma.Search.QdrantSearch, :qdrant_api_key])}
+ ])
+end
diff --git a/lib/pleroma/search/search_backend.ex b/lib/pleroma/search/search_backend.ex
new file mode 100644
index 000000000..f4ed13c36
--- /dev/null
+++ b/lib/pleroma/search/search_backend.ex
@@ -0,0 +1,42 @@
+defmodule Pleroma.Search.SearchBackend do
+ @doc """
+ Search statuses with a query, restricting to only those the user should have access to.
+ """
+ @callback search(user :: Pleroma.User.t(), query :: String.t(), options :: [any()]) :: [
+ Pleroma.Activity.t()
+ ]
+
+ @doc """
+ Add the object associated with the activity to the search index.
+
+ The whole activity is passed, to allow filtering on things such as scope.
+ """
+ @callback add_to_index(activity :: Pleroma.Activity.t()) :: :ok | {:error, any()}
+
+ @doc """
+ Remove the object from the index.
+
+ Just the object, as opposed to the whole activity, is passed, since the object
+ is what contains the actual content and there is no need for filtering when removing
+ from index.
+ """
+ @callback remove_from_index(object :: Pleroma.Object.t()) :: :ok | {:error, any()}
+
+ @doc """
+ Create the index
+ """
+ @callback create_index() :: :ok | {:error, any()}
+
+ @doc """
+ Drop the index
+ """
+ @callback drop_index() :: :ok | {:error, any()}
+
+ @doc """
+ Healthcheck endpoints of search backend infrastructure to monitor for controlling
+ processing of jobs in the Oban queue.
+
+ It is expected a 200 response is healthy and other responses are unhealthy.
+ """
+ @callback healthcheck_endpoints :: list() | nil
+end
diff --git a/lib/pleroma/signature.ex b/lib/pleroma/signature.ex
index 5cfdae051..195513478 100644
--- a/lib/pleroma/signature.ex
+++ b/lib/pleroma/signature.ex
@@ -10,6 +10,14 @@ defmodule Pleroma.Signature do
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
+ import Plug.Conn, only: [put_req_header: 3]
+
+ @http_signatures_impl Application.compile_env(
+ :pleroma,
+ [__MODULE__, :http_signatures_impl],
+ HTTPSignatures
+ )
+
@known_suffixes ["/publickey", "/main-key"]
def key_id_to_actor_id(key_id) do
@@ -27,7 +35,7 @@ defmodule Pleroma.Signature do
_ ->
case Pleroma.Web.WebFinger.finger(maybe_ap_id) do
- %{"ap_id" => ap_id} -> {:ok, ap_id}
+ {:ok, %{"ap_id" => ap_id}} -> {:ok, ap_id}
_ -> {:error, maybe_ap_id}
end
end
@@ -44,8 +52,7 @@ defmodule Pleroma.Signature do
defp remove_suffix(uri, []), do: uri
def fetch_public_key(conn) do
- with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
- {:ok, actor_id} <- key_id_to_actor_id(kid),
+ with {:ok, actor_id} <- get_actor_id(conn),
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
{:ok, public_key}
else
@@ -55,8 +62,7 @@ defmodule Pleroma.Signature do
end
def refetch_public_key(conn) do
- with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
- {:ok, actor_id} <- key_id_to_actor_id(kid),
+ with {:ok, actor_id} <- get_actor_id(conn),
{:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id),
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
{:ok, public_key}
@@ -66,6 +72,16 @@ defmodule Pleroma.Signature do
end
end
+ def get_actor_id(conn) do
+ with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
+ {:ok, actor_id} <- key_id_to_actor_id(kid) do
+ {:ok, actor_id}
+ else
+ e ->
+ {:error, e}
+ end
+ end
+
def sign(%User{keys: keys} = user, headers) do
with {:ok, private_key, _} <- Keys.keys_from_pem(keys) do
HTTPSignatures.sign(private_key, user.ap_id <> "#main-key", headers)
@@ -77,4 +93,48 @@ defmodule Pleroma.Signature do
def signed_date(%NaiveDateTime{} = date) do
Timex.format!(date, "{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT")
end
+
+ @spec validate_signature(Plug.Conn.t(), String.t()) :: boolean()
+ def validate_signature(%Plug.Conn{} = conn, request_target) do
+ # Newer drafts for HTTP signatures now use @request-target instead of the
+ # old (request-target). We'll now support both for incoming signatures.
+ conn =
+ conn
+ |> put_req_header("(request-target)", request_target)
+ |> put_req_header("@request-target", request_target)
+
+ @http_signatures_impl.validate_conn(conn)
+ end
+
+ @spec validate_signature(Plug.Conn.t()) :: boolean()
+ def validate_signature(%Plug.Conn{} = conn) do
+ # This (request-target) is non-standard, but many implementations do it
+ # this way due to a misinterpretation of
+ # https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-06
+ # "path" was interpreted as not having the query, though later examples
+ # show that it must be the absolute path + query. This behavior is kept to
+ # make sure most software (Pleroma itself, Mastodon, and probably others)
+ # do not break.
+ request_target = Enum.join([String.downcase(conn.method), conn.request_path], " ")
+
+ # This is the proper way to build the @request-target, as expected by
+ # many HTTP signature libraries, clarified in the following draft:
+ # https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-11.html#section-2.2.6
+ # It is the same as before, but containing the query part as well.
+ proper_target = Enum.join([request_target, "?", conn.query_string], "")
+
+ cond do
+ # Normal, non-standard behavior but expected by Pleroma and more.
+ validate_signature(conn, request_target) ->
+ true
+
+ # Has query string and the previous one failed: let's try the standard.
+ conn.query_string != "" ->
+ validate_signature(conn, proper_target)
+
+ # If there's no query string and signature fails, it's rotten.
+ true ->
+ false
+ end
+ end
end
diff --git a/lib/pleroma/telemetry/logger.ex b/lib/pleroma/telemetry/logger.ex
index 384c70fbc..31ce3cc20 100644
--- a/lib/pleroma/telemetry/logger.ex
+++ b/lib/pleroma/telemetry/logger.ex
@@ -39,7 +39,7 @@ defmodule Pleroma.Telemetry.Logger do
_,
_
) do
- Logger.error(fn ->
+ Logger.debug(fn ->
"Connection pool failed to reclaim any connections due to all of them being in use. It will have to drop requests for opening connections to new hosts"
end)
end
@@ -59,7 +59,7 @@ defmodule Pleroma.Telemetry.Logger do
_,
_
) do
- Logger.error(fn ->
+ Logger.debug(fn ->
"Connection pool had to refuse opening a connection to #{key} due to connection limit exhaustion"
end)
end
@@ -70,7 +70,7 @@ defmodule Pleroma.Telemetry.Logger do
%{key: key},
_
) do
- Logger.warn(fn ->
+ Logger.debug(fn ->
"Pool worker for #{key}: Client #{inspect(client_pid)} died before releasing the connection with #{inspect(reason)}"
end)
end
@@ -81,7 +81,7 @@ defmodule Pleroma.Telemetry.Logger do
%{key: key, protocol: :http},
_
) do
- Logger.info(fn ->
+ Logger.debug(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
diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex
index 4aee9326f..b0aef2592 100644
--- a/lib/pleroma/upload.ex
+++ b/lib/pleroma/upload.ex
@@ -34,7 +34,6 @@ defmodule Pleroma.Upload do
"""
alias Ecto.UUID
- alias Pleroma.Config
alias Pleroma.Maps
alias Pleroma.Web.ActivityPub.Utils
require Logger
@@ -52,6 +51,7 @@ defmodule Pleroma.Upload do
| {:size_limit, nil | non_neg_integer()}
| {:uploader, module()}
| {:filters, [module()]}
+ | {:actor, String.t()}
@type t :: %__MODULE__{
id: String.t(),
@@ -76,6 +76,8 @@ defmodule Pleroma.Upload do
:path
]
+ @config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config)
+
defp get_description(upload) do
case {upload.description, Pleroma.Config.get([Pleroma.Upload, :default_description])} do
{description, _} when is_binary(description) -> description
@@ -85,7 +87,7 @@ defmodule Pleroma.Upload do
end
end
- @spec store(source, options :: [option()]) :: {:ok, Map.t()} | {:error, any()}
+ @spec store(source, options :: [option()]) :: {:ok, map()} | {: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)
@@ -174,7 +176,7 @@ 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 = Base.encode16(:crypto.hash(:sha256, data), lower: true)
+ hash = Base.encode16(:crypto.hash(:sha256, data), case: :upper)
with :ok <- check_binary_size(data, opts.size_limit),
tmp_path <- tempfile_for_image(data),
@@ -237,36 +239,45 @@ defmodule Pleroma.Upload do
""
end
- [base_url, path]
- |> Path.join()
+ if String.contains?(base_url, Pleroma.Uploaders.IPFS.placeholder()) do
+ String.replace(base_url, Pleroma.Uploaders.IPFS.placeholder(), path)
+ else
+ [base_url, path]
+ |> Path.join()
+ end
end
defp url_from_spec(_upload, _base_url, {:url, url}), do: url
+ @spec base_url() :: binary
def base_url do
- uploader = Config.get([Pleroma.Upload, :uploader])
- upload_base_url = Config.get([Pleroma.Upload, :base_url])
- public_endpoint = Config.get([uploader, :public_endpoint])
+ uploader = @config_impl.get([Pleroma.Upload, :uploader])
+ upload_fallback_url = Pleroma.Web.Endpoint.url() <> "/media/"
+ upload_base_url = @config_impl.get([Pleroma.Upload, :base_url]) || upload_fallback_url
+ public_endpoint = @config_impl.get([uploader, :public_endpoint])
case uploader do
Pleroma.Uploaders.Local ->
- upload_base_url || Pleroma.Web.Endpoint.url() <> "/media/"
+ upload_base_url
Pleroma.Uploaders.S3 ->
- bucket = Config.get([Pleroma.Uploaders.S3, :bucket])
- truncated_namespace = Config.get([Pleroma.Uploaders.S3, :truncated_namespace])
- namespace = Config.get([Pleroma.Uploaders.S3, :bucket_namespace])
+ bucket = @config_impl.get([Pleroma.Uploaders.S3, :bucket])
+ truncated_namespace = @config_impl.get([Pleroma.Uploaders.S3, :truncated_namespace])
+ namespace = @config_impl.get([Pleroma.Uploaders.S3, :bucket_namespace])
bucket_with_namespace =
cond do
!is_nil(truncated_namespace) ->
truncated_namespace
- !is_nil(namespace) ->
+ !is_nil(namespace) and !is_nil(bucket) ->
namespace <> ":" <> bucket
- true ->
+ !is_nil(bucket) ->
bucket
+
+ true ->
+ ""
end
if public_endpoint do
@@ -275,8 +286,11 @@ defmodule Pleroma.Upload do
Path.join([upload_base_url, bucket_with_namespace])
end
+ Pleroma.Uploaders.IPFS ->
+ @config_impl.get([Pleroma.Uploaders.IPFS, :get_gateway_url])
+
_ ->
- public_endpoint || upload_base_url || Pleroma.Web.Endpoint.url() <> "/media/"
+ public_endpoint || upload_base_url
end
end
end
diff --git a/lib/pleroma/upload/filter/analyze_metadata.ex b/lib/pleroma/upload/filter/analyze_metadata.ex
index 9a76a998b..7ee643277 100644
--- a/lib/pleroma/upload/filter/analyze_metadata.ex
+++ b/lib/pleroma/upload/filter/analyze_metadata.ex
@@ -8,27 +8,28 @@ defmodule Pleroma.Upload.Filter.AnalyzeMetadata do
"""
require Logger
+ alias Vix.Vips.Image
+ alias Vix.Vips.Operation
+
@behaviour Pleroma.Upload.Filter
@spec filter(Pleroma.Upload.t()) ::
{:ok, :filtered, Pleroma.Upload.t()} | {:ok, :noop} | {:error, String.t()}
def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _} = upload) do
try do
- image =
- file
- |> Mogrify.open()
- |> Mogrify.verbose()
+ {:ok, image} = Image.new_from_file(file)
+ {width, height} = {Image.width(image), Image.height(image)}
upload =
upload
- |> Map.put(:width, image.width)
- |> Map.put(:height, image.height)
- |> Map.put(:blurhash, get_blurhash(file))
+ |> Map.put(:width, width)
+ |> Map.put(:height, height)
+ |> Map.put(:blurhash, get_blurhash(image))
{:ok, :filtered, upload}
rescue
e in ErlangError ->
- Logger.warn("#{__MODULE__}: #{inspect(e)}")
+ Logger.warning("#{__MODULE__}: #{inspect(e)}")
{:ok, :noop}
end
end
@@ -45,7 +46,7 @@ defmodule Pleroma.Upload.Filter.AnalyzeMetadata do
{:ok, :filtered, upload}
rescue
e in ErlangError ->
- Logger.warn("#{__MODULE__}: #{inspect(e)}")
+ Logger.warning("#{__MODULE__}: #{inspect(e)}")
{:ok, :noop}
end
end
@@ -53,7 +54,7 @@ defmodule Pleroma.Upload.Filter.AnalyzeMetadata do
def filter(_), do: {:ok, :noop}
defp get_blurhash(file) do
- with {:ok, blurhash} <- :eblurhash.magick(file) do
+ with {:ok, blurhash} <- vips_blurhash(file) do
blurhash
else
_ -> nil
@@ -77,7 +78,28 @@ defmodule Pleroma.Upload.Filter.AnalyzeMetadata do
%{width: width, height: height}
else
nil -> {:error, {:ffprobe, :command_not_found}}
- {:error, _} = error -> error
+ error -> {:error, error}
+ end
+ end
+
+ defp vips_blurhash(%Vix.Vips.Image{} = image) do
+ with {:ok, resized_image} <- Operation.thumbnail_image(image, 100),
+ {height, width} <- {Image.height(resized_image), Image.width(resized_image)},
+ max <- max(height, width),
+ {x, y} <- {max(round(width * 5 / max), 1), max(round(height * 5 / max), 1)} do
+ {:ok, rgb} =
+ if Image.has_alpha?(resized_image) do
+ # remove alpha channel
+ resized_image
+ |> Operation.extract_band!(0, n: 3)
+ |> Image.write_to_binary()
+ else
+ Image.write_to_binary(resized_image)
+ end
+
+ Blurhash.encode(rgb, width, height, x, y)
+ else
+ _ -> nil
end
end
end
diff --git a/lib/pleroma/upload/filter/exiftool/read_description.ex b/lib/pleroma/upload/filter/exiftool/read_description.ex
index 543b22031..8c1ed82f8 100644
--- a/lib/pleroma/upload/filter/exiftool/read_description.ex
+++ b/lib/pleroma/upload/filter/exiftool/read_description.ex
@@ -10,8 +10,6 @@ defmodule Pleroma.Upload.Filter.Exiftool.ReadDescription do
"""
@behaviour Pleroma.Upload.Filter
- @spec filter(Pleroma.Upload.t()) :: {:ok, any()} | {:error, String.t()}
-
def filter(%Pleroma.Upload{description: description})
when is_binary(description),
do: {:ok, :noop}
diff --git a/lib/pleroma/upload/filter/exiftool/strip_location.ex b/lib/pleroma/upload/filter/exiftool/strip_location.ex
index f2bcc4622..1744a286d 100644
--- a/lib/pleroma/upload/filter/exiftool/strip_location.ex
+++ b/lib/pleroma/upload/filter/exiftool/strip_location.ex
@@ -9,8 +9,6 @@ defmodule Pleroma.Upload.Filter.Exiftool.StripLocation do
"""
@behaviour Pleroma.Upload.Filter
- @spec filter(Pleroma.Upload.t()) :: {:ok, any()} | {:error, String.t()}
-
# Formats not compatible with exiftool at this time
def filter(%Pleroma.Upload{content_type: "image/heic"}), do: {:ok, :noop}
def filter(%Pleroma.Upload{content_type: "image/webp"}), do: {:ok, :noop}
@@ -18,7 +16,9 @@ defmodule Pleroma.Upload.Filter.Exiftool.StripLocation do
def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do
try do
- case System.cmd("exiftool", ["-overwrite_original", "-gps:all=", file], parallelism: true) do
+ case System.cmd("exiftool", ["-overwrite_original", "-gps:all=", "-png:all=", file],
+ parallelism: true
+ ) do
{_response, 0} -> {:ok, :filtered}
{error, 1} -> {:error, error}
end
diff --git a/lib/pleroma/upload/filter/mogrifun.ex b/lib/pleroma/upload/filter/mogrifun.ex
index a0f247b70..9716580a8 100644
--- a/lib/pleroma/upload/filter/mogrifun.ex
+++ b/lib/pleroma/upload/filter/mogrifun.ex
@@ -38,7 +38,6 @@ 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
try do
Filter.Mogrify.do_filter(file, [Enum.random(@filters)])
diff --git a/lib/pleroma/upload/filter/mogrify.ex b/lib/pleroma/upload/filter/mogrify.ex
index 06efbf321..d1e166022 100644
--- a/lib/pleroma/upload/filter/mogrify.ex
+++ b/lib/pleroma/upload/filter/mogrify.ex
@@ -8,7 +8,6 @@ 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
try do
do_filter(file, Pleroma.Config.get!([__MODULE__, :args]))
diff --git a/lib/pleroma/uploaders/ipfs.ex b/lib/pleroma/uploaders/ipfs.ex
new file mode 100644
index 000000000..5930a129e
--- /dev/null
+++ b/lib/pleroma/uploaders/ipfs.ex
@@ -0,0 +1,72 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Uploaders.IPFS do
+ @behaviour Pleroma.Uploaders.Uploader
+ require Logger
+
+ alias Tesla.Multipart
+
+ @api_add "/api/v0/add"
+ @api_delete "/api/v0/files/rm"
+ @config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config)
+
+ @placeholder "{CID}"
+ def placeholder, do: @placeholder
+
+ @impl true
+ def get_file(file) do
+ b_url = Pleroma.Upload.base_url()
+
+ if String.contains?(b_url, @placeholder) do
+ {:ok, {:url, String.replace(b_url, @placeholder, URI.decode(file))}}
+ else
+ {:error, "IPFS Get URL doesn't contain 'cid' placeholder"}
+ end
+ end
+
+ @impl true
+ def put_file(%Pleroma.Upload{tempfile: tempfile}) do
+ mp =
+ Multipart.new()
+ |> Multipart.add_content_type_param("charset=utf-8")
+ |> Multipart.add_file(tempfile)
+
+ endpoint = ipfs_endpoint(@api_add)
+
+ with {:ok, %{body: body}} when is_binary(body) <-
+ Pleroma.HTTP.post(endpoint, mp, [], params: ["cid-version": "1"], pool: :upload),
+ {_, {:ok, decoded}} <- {:json, Jason.decode(body)},
+ {_, true} <- {:hash, Map.has_key?(decoded, "Hash")} do
+ {:ok, {:file, decoded["Hash"]}}
+ else
+ {:hash, false} ->
+ {:error, "JSON doesn't contain Hash key"}
+
+ {:json, error} ->
+ Logger.error("#{__MODULE__}: #{inspect(error)}")
+ {:error, "JSON decode failed"}
+
+ error ->
+ Logger.error("#{__MODULE__}: #{inspect(error)}")
+ {:error, "IPFS Gateway upload failed"}
+ end
+ end
+
+ @impl true
+ def delete_file(file) do
+ endpoint = ipfs_endpoint(@api_delete)
+
+ case Pleroma.HTTP.post(endpoint, "", [], params: [arg: file]) do
+ {:ok, %{status: 204}} -> :ok
+ error -> {:error, inspect(error)}
+ end
+ end
+
+ defp ipfs_endpoint(path) do
+ URI.parse(@config_impl.get([__MODULE__, :post_gateway_url]))
+ |> Map.put(:path, path)
+ |> URI.to_string()
+ end
+end
diff --git a/lib/pleroma/uploaders/s3.ex b/lib/pleroma/uploaders/s3.ex
index 19287c532..7b32bd8a5 100644
--- a/lib/pleroma/uploaders/s3.ex
+++ b/lib/pleroma/uploaders/s3.ex
@@ -6,7 +6,8 @@ defmodule Pleroma.Uploaders.S3 do
@behaviour Pleroma.Uploaders.Uploader
require Logger
- alias Pleroma.Config
+ @ex_aws_impl Application.compile_env(:pleroma, [__MODULE__, :ex_aws_impl], ExAws)
+ @config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config)
# The file name is re-encoded with S3's constraints here to comply with previous
# links with less strict filenames
@@ -22,7 +23,7 @@ defmodule Pleroma.Uploaders.S3 do
@impl true
def put_file(%Pleroma.Upload{} = upload) do
- config = Config.get([__MODULE__])
+ config = @config_impl.get([__MODULE__])
bucket = Keyword.get(config, :bucket)
streaming = Keyword.get(config, :streaming_enabled)
@@ -56,7 +57,7 @@ defmodule Pleroma.Uploaders.S3 do
])
end
- case ExAws.request(op) do
+ case @ex_aws_impl.request(op) do
{:ok, _} ->
{:ok, {:file, s3_name}}
@@ -69,9 +70,9 @@ defmodule Pleroma.Uploaders.S3 do
@impl true
def delete_file(file) do
[__MODULE__, :bucket]
- |> Config.get()
+ |> @config_impl.get()
|> ExAws.S3.delete_object(file)
- |> ExAws.request()
+ |> @ex_aws_impl.request()
|> case do
{:ok, %{status_code: 204}} -> :ok
error -> {:error, inspect(error)}
@@ -83,3 +84,7 @@ defmodule Pleroma.Uploaders.S3 do
String.replace(name, @regex, "-")
end
end
+
+defmodule Pleroma.Uploaders.S3.ExAwsAPI do
+ @callback request(op :: ExAws.Operation.t()) :: {:ok, ExAws.Operation.t()} | {:error, term()}
+end
diff --git a/lib/pleroma/uploaders/uploader.ex b/lib/pleroma/uploaders/uploader.ex
index 77f6f02dd..3396fe06a 100644
--- a/lib/pleroma/uploaders/uploader.ex
+++ b/lib/pleroma/uploaders/uploader.ex
@@ -5,8 +5,6 @@
defmodule Pleroma.Uploaders.Uploader do
import Pleroma.Web.Gettext
- @mix_env Mix.env()
-
@moduledoc """
Defines the contract to put and get an uploaded file to any backend.
"""
@@ -40,7 +38,7 @@ defmodule Pleroma.Uploaders.Uploader do
@callback delete_file(file :: String.t()) :: :ok | {:error, String.t()}
- @callback http_callback(Plug.Conn.t(), Map.t()) ::
+ @callback http_callback(Plug.Conn.t(), map()) ::
{:ok, Plug.Conn.t()}
| {:ok, Plug.Conn.t(), file_spec()}
| {:error, Plug.Conn.t(), String.t()}
@@ -75,10 +73,5 @@ defmodule Pleroma.Uploaders.Uploader do
end
end
- defp callback_timeout do
- case @mix_env do
- :test -> 1_000
- _ -> 30_000
- end
- end
+ defp callback_timeout, do: Application.get_env(:pleroma, __MODULE__)[:timeout]
end
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index ce125d608..e28d76a7c 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -8,6 +8,7 @@ defmodule Pleroma.User do
import Ecto.Changeset
import Ecto.Query
import Ecto, only: [assoc: 2]
+ import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
alias Ecto.Multi
alias Pleroma.Activity
@@ -37,8 +38,11 @@ defmodule Pleroma.User do
alias Pleroma.Web.OAuth
alias Pleroma.Web.RelMe
alias Pleroma.Workers.BackgroundWorker
+ alias Pleroma.Workers.DeleteWorker
+ alias Pleroma.Workers.UserRefreshWorker
require Logger
+ require Pleroma.Constants
@type t :: %__MODULE__{}
@type account_status ::
@@ -579,7 +583,7 @@ defmodule Pleroma.User do
|> validate_format(:nickname, local_nickname_regex())
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, min: 1, max: name_limit)
- |> validate_inclusion(:actor_type, ["Person", "Service"])
+ |> validate_inclusion(:actor_type, Pleroma.Constants.allowed_user_actor_types())
|> put_fields()
|> put_emoji()
|> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)})
@@ -595,9 +599,23 @@ defmodule Pleroma.User do
defp put_fields(changeset) do
if raw_fields = get_change(changeset, :raw_fields) do
+ old_fields = changeset.data.raw_fields
+
raw_fields =
raw_fields
|> Enum.filter(fn %{"name" => n} -> n != "" end)
+ |> Enum.map(fn field ->
+ previous =
+ old_fields
+ |> Enum.find(fn %{"value" => value} -> field["value"] == value end)
+
+ if previous && Map.has_key?(previous, "verified_at") do
+ field
+ |> Map.put("verified_at", previous["verified_at"])
+ else
+ field
+ end
+ end)
fields =
raw_fields
@@ -671,7 +689,7 @@ defmodule Pleroma.User do
|> validate_inclusion(:actor_type, ["Person", "Service"])
end
- @spec update_as_admin(User.t(), map()) :: {:ok, User.t()} | {:error, Changeset.t()}
+ @spec update_as_admin(User.t(), map()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def update_as_admin(user, params) do
params = Map.put(params, "password_confirmation", params["password"])
changeset = update_as_admin_changeset(user, params)
@@ -692,7 +710,7 @@ defmodule Pleroma.User do
|> put_change(:password_reset_pending, false)
end
- @spec reset_password(User.t(), map()) :: {:ok, User.t()} | {:error, Changeset.t()}
+ @spec reset_password(User.t(), map()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def reset_password(%User{} = user, params) do
reset_password(user, user, params)
end
@@ -1010,7 +1028,7 @@ defmodule Pleroma.User do
def maybe_send_confirmation_email(_), do: {:ok, :noop}
- @spec send_confirmation_email(Uset.t()) :: User.t()
+ @spec send_confirmation_email(User.t()) :: User.t()
def send_confirmation_email(%User{} = user) do
user
|> Pleroma.Emails.UserEmail.account_confirmation_email()
@@ -1047,7 +1065,8 @@ defmodule Pleroma.User do
def needs_update?(_), do: true
- @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
+ @spec maybe_direct_follow(User.t(), User.t()) ::
+ {:ok, User.t(), User.t()} | {:error, String.t()}
# "Locked" (self-locked) users demand explicit authorization of follow requests
def maybe_direct_follow(%User{} = follower, %User{local: true, is_locked: true} = followed) do
@@ -1198,6 +1217,10 @@ defmodule Pleroma.User do
def update_and_set_cache(changeset) do
with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
+ if get_change(changeset, :raw_fields) do
+ BackgroundWorker.enqueue("verify_fields_links", %{"user_id" => user.id})
+ end
+
set_cache(user)
end
end
@@ -1383,6 +1406,40 @@ defmodule Pleroma.User do
|> Repo.all()
end
+ @spec get_familiar_followers_query(User.t(), User.t(), pos_integer() | nil) :: Ecto.Query.t()
+ def get_familiar_followers_query(%User{} = user, %User{} = current_user, nil) do
+ friends =
+ get_friends_query(current_user)
+ |> where([u], not u.hide_follows)
+ |> select([u], u.id)
+
+ User.Query.build(%{is_active: true})
+ |> where([u], u.id not in ^[user.id, current_user.id])
+ |> join(:inner, [u], r in FollowingRelationship,
+ as: :followers_relationships,
+ on: r.following_id == ^user.id and r.follower_id == u.id
+ )
+ |> where([followers_relationships: r], r.state == ^:follow_accept)
+ |> where([followers_relationships: r], r.follower_id in subquery(friends))
+ end
+
+ def get_familiar_followers_query(%User{} = user, %User{} = current_user, page) do
+ user
+ |> get_familiar_followers_query(current_user, nil)
+ |> User.Query.paginate(page, 20)
+ end
+
+ @spec get_familiar_followers_query(User.t(), User.t()) :: Ecto.Query.t()
+ def get_familiar_followers_query(%User{} = user, %User{} = current_user),
+ do: get_familiar_followers_query(user, current_user, nil)
+
+ @spec get_familiar_followers(User.t(), User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
+ def get_familiar_followers(%User{} = user, %User{} = current_user, page \\ nil) do
+ user
+ |> get_familiar_followers_query(current_user, page)
+ |> Repo.all()
+ end
+
def increase_note_count(%User{} = user) do
User
|> where(id: ^user.id)
@@ -1560,7 +1617,7 @@ defmodule Pleroma.User do
unmute(muter, mutee)
else
{who, result} = error ->
- Logger.warn(
+ Logger.warning(
"User.unmute/2 failed. #{who}: #{result}, muter_id: #{muter_id}, mutee_id: #{mutee_id}"
)
@@ -1782,14 +1839,17 @@ defmodule Pleroma.User do
BackgroundWorker.enqueue("user_activation", %{"user_id" => user.id, "status" => status})
end
- @spec set_activation([User.t()], boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
+ @spec set_activation([User.t()], boolean()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def set_activation(users, status) when is_list(users) do
Repo.transaction(fn ->
- for user <- users, do: set_activation(user, status)
+ for user <- users do
+ {:ok, user} = set_activation(user, status)
+ user
+ end
end)
end
- @spec set_activation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
+ @spec set_activation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def set_activation(%User{} = user, status) do
with {:ok, user} <- set_activation_status(user, status) do
user
@@ -1867,7 +1927,7 @@ defmodule Pleroma.User do
|> update_and_set_cache()
end
- @spec purge_user_changeset(User.t()) :: Changeset.t()
+ @spec purge_user_changeset(User.t()) :: Ecto.Changeset.t()
def purge_user_changeset(user) do
# "Right to be forgotten"
# https://gdpr.eu/right-to-be-forgotten/
@@ -1923,7 +1983,7 @@ defmodule Pleroma.User do
def delete(%User{} = user) do
# Purge the user immediately
purge(user)
- BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
+ DeleteWorker.enqueue("delete_user", %{"user_id" => user.id})
end
# *Actually* delete the user from the DB
@@ -1970,8 +2030,46 @@ defmodule Pleroma.User do
maybe_delete_from_db(user)
end
+ def perform(:verify_fields_links, user) do
+ profile_urls = [user.ap_id]
+
+ fields =
+ user.raw_fields
+ |> Enum.map(&verify_field_link(&1, profile_urls))
+
+ changeset =
+ user
+ |> update_changeset(%{raw_fields: fields})
+
+ with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
+ set_cache(user)
+ end
+ end
+
def perform(:set_activation_async, user, status), do: set_activation(user, status)
+ defp verify_field_link(field, profile_urls) do
+ verified_at =
+ with %{"value" => value} <- field,
+ {:verified_at, nil} <- {:verified_at, Map.get(field, "verified_at")},
+ %{scheme: scheme, userinfo: nil, host: host}
+ when not_empty_string(host) and scheme in ["http", "https"] <-
+ URI.parse(value),
+ {:not_idn, true} <-
+ {:not_idn, match?(^host, to_string(:idna.encode(to_charlist(host))))},
+ "me" <- Pleroma.Web.RelMe.maybe_put_rel_me(value, profile_urls) do
+ CommonUtils.to_masto_date(NaiveDateTime.utc_now())
+ else
+ {:verified_at, value} when not_empty_string(value) ->
+ value
+
+ _ ->
+ nil
+ end
+
+ Map.put(field, "verified_at", verified_at)
+ end
+
@spec external_users_query() :: Ecto.Query.t()
def external_users_query do
User.Query.build(%{
@@ -2058,20 +2156,20 @@ defmodule Pleroma.User do
def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
+ @spec get_or_fetch_by_ap_id(String.t()) :: {:ok, User.t()} | {:error, any()}
def get_or_fetch_by_ap_id(ap_id) do
- cached_user = get_cached_by_ap_id(ap_id)
-
- maybe_fetched_user = needs_update?(cached_user) && fetch_by_ap_id(ap_id)
-
- case {cached_user, maybe_fetched_user} do
- {_, {:ok, %User{} = user}} ->
- {:ok, user}
-
- {%User{} = user, _} ->
- {:ok, user}
+ with cached_user = %User{} <- get_cached_by_ap_id(ap_id),
+ _ <- maybe_refresh(cached_user) do
+ {:ok, cached_user}
+ else
+ _ -> fetch_by_ap_id(ap_id)
+ end
+ end
- _ ->
- {:error, :not_found}
+ defp maybe_refresh(user) do
+ if needs_update?(user) do
+ UserRefreshWorker.new(%{"ap_id" => user.ap_id})
+ |> Oban.insert()
end
end
@@ -2136,7 +2234,7 @@ defmodule Pleroma.User do
def public_key(_), do: {:error, "key not found"}
def get_public_key_for_ap_id(ap_id) do
- with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
+ with %User{} = user <- get_cached_by_ap_id(ap_id),
{:ok, public_key} <- public_key(user) do
{:ok, public_key}
else
@@ -2252,7 +2350,7 @@ defmodule Pleroma.User do
if String.contains?(user.nickname, "@") do
user.nickname
else
- %{host: host} = URI.parse(user.ap_id)
+ host = Pleroma.Web.WebFinger.host()
user.nickname <> "@" <> host
end
end
@@ -2358,7 +2456,7 @@ defmodule Pleroma.User do
updated_user
end
- @spec set_confirmation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
+ @spec set_confirmation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def set_confirmation(%User{} = user, bool) do
user
|> confirmation_changeset(set_confirmation: bool)
@@ -2402,9 +2500,9 @@ defmodule Pleroma.User do
defp put_password_hash(changeset), do: changeset
- def is_internal_user?(%User{nickname: nil}), do: true
- def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
- def is_internal_user?(_), do: false
+ def internal?(%User{nickname: nil}), do: true
+ def internal?(%User{local: true, nickname: "internal." <> _}), do: true
+ def internal?(_), do: false
# A hack because user delete activities have a fake id for whatever reason
# TODO: Get rid of this
@@ -2536,7 +2634,7 @@ defmodule Pleroma.User do
|> update_and_set_cache()
end
- @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
+ @spec confirmation_changeset(User.t(), keyword()) :: Ecto.Changeset.t()
def confirmation_changeset(user, set_confirmation: confirmed?) do
params =
if confirmed? do
@@ -2554,9 +2652,9 @@ defmodule Pleroma.User do
cast(user, params, [:is_confirmed, :confirmation_token])
end
- @spec approval_changeset(User.t(), keyword()) :: Changeset.t()
- def approval_changeset(user, set_approval: approved?) do
- cast(user, %{is_approved: approved?}, [:is_approved])
+ @spec approval_changeset(Ecto.Changeset.t(), keyword()) :: Ecto.Changeset.t()
+ def approval_changeset(changeset, set_approval: approved?) do
+ cast(changeset, %{is_approved: approved?}, [:is_approved])
end
@spec add_pinned_object_id(User.t(), String.t()) :: {:ok, User.t()} | {:error, term()}
@@ -2632,7 +2730,7 @@ defmodule Pleroma.User do
end
end
- @spec add_to_block(User.t(), User.t()) ::
+ @spec remove_from_block(User.t(), User.t()) ::
{:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
defp remove_from_block(%User{} = user, %User{} = blocked) do
with {:ok, relationship} <- UserRelationship.delete_block(user, blocked) do
@@ -2659,10 +2757,11 @@ defmodule Pleroma.User do
# - display name
def sanitize_html(%User{} = user, filter) do
fields =
- Enum.map(user.fields, fn %{"name" => name, "value" => value} ->
+ Enum.map(user.fields, fn %{"name" => name, "value" => value} = fields ->
%{
"name" => name,
- "value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
+ "value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly),
+ "verified_at" => Map.get(fields, "verified_at")
}
end)
@@ -2681,6 +2780,8 @@ defmodule Pleroma.User do
|> update_and_set_cache()
end
+ def update_last_active_at(user), do: user
+
def active_user_count(days \\ 30) do
active_after = Timex.shift(NaiveDateTime.utc_now(), days: -days)
diff --git a/lib/pleroma/user/backup.ex b/lib/pleroma/user/backup.ex
index 447fca2a1..7feaa22bf 100644
--- a/lib/pleroma/user/backup.ex
+++ b/lib/pleroma/user/backup.ex
@@ -14,82 +14,127 @@ defmodule Pleroma.User.Backup do
alias Pleroma.Activity
alias Pleroma.Bookmark
+ alias Pleroma.Config
alias Pleroma.Repo
+ alias Pleroma.Uploaders.Uploader
alias Pleroma.User
- alias Pleroma.User.Backup.State
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.UserView
alias Pleroma.Workers.BackupWorker
+ @type t :: %__MODULE__{}
+
schema "backups" do
field(:content_type, :string)
field(:file_name, :string)
field(:file_size, :integer, default: 0)
field(:processed, :boolean, default: false)
- field(:state, State, default: :invalid)
- field(:processed_number, :integer, default: 0)
+ field(:tempdir, :string)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
timestamps()
end
- def create(user, admin_id \\ nil) do
- with :ok <- validate_limit(user, admin_id),
- {:ok, backup} <- user |> new() |> Repo.insert() do
- BackupWorker.process(backup, admin_id)
+ @doc """
+ Schedules a job to backup a user if the number of backup requests has not exceeded the limit.
+
+ Admins can directly call new/1 and schedule_backup/1 to bypass the limit.
+ """
+ @spec user(User.t()) :: {:ok, t()} | {:error, any()}
+ def user(user) do
+ days = Config.get([__MODULE__, :limit_days])
+
+ with true <- permitted?(user),
+ %__MODULE__{} = backup <- new(user),
+ {:ok, inserted_backup} <- Repo.insert(backup),
+ {:ok, %Oban.Job{}} <- schedule_backup(inserted_backup) do
+ {:ok, inserted_backup}
+ else
+ false ->
+ {:error,
+ dngettext(
+ "errors",
+ "Last export was less than a day ago",
+ "Last export was less than %{days} days ago",
+ days,
+ days: days
+ )}
+
+ e ->
+ {:error, e}
end
end
+ @doc "Generates a %Backup{} for a user with a random file name"
+ @spec new(User.t()) :: t()
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,
- state: :pending
+ tempdir: tempdir(),
+ user: user
}
end
- def delete(backup) do
- uploader = Pleroma.Config.get([Pleroma.Upload, :uploader])
+ @doc "Schedules the execution of the provided backup"
+ @spec schedule_backup(t()) :: {:ok, Oban.Job.t()} | {:error, any()}
+ def schedule_backup(backup) do
+ with false <- is_nil(backup.id) do
+ %{"op" => "process", "backup_id" => backup.id}
+ |> BackupWorker.new()
+ |> Oban.insert()
+ else
+ true ->
+ {:error, "Backup is missing id. Please insert it into the Repo first."}
+
+ e ->
+ {:error, e}
+ end
+ end
+
+ @doc "Deletes the backup archive file and removes the database record"
+ @spec delete_archive(t()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
+ def delete_archive(backup) do
+ uploader = 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
+ @doc "Schedules a job to delete the backup archive"
+ @spec schedule_delete(t()) :: {:ok, Oban.Job.t()} | {:error, any()}
+ def schedule_delete(backup) do
+ days = Config.get([__MODULE__, :purge_after_days])
+ time = 60 * 60 * 24 * days
+ scheduled_at = Calendar.NaiveDateTime.add!(backup.inserted_at, time)
- nil ->
- :ok
+ %{"op" => "delete", "backup_id" => backup.id}
+ |> BackupWorker.new(scheduled_at: scheduled_at)
+ |> Oban.insert()
+ end
+
+ defp permitted?(user) do
+ with {_, %__MODULE__{inserted_at: inserted_at}} <- {:last, get_last(user)},
+ days = Config.get([__MODULE__, :limit_days]),
+ diff = Timex.diff(NaiveDateTime.utc_now(), inserted_at, :days),
+ {_, true} <- {:diff, diff > days} do
+ true
+ else
+ {:last, nil} -> true
+ {:diff, false} -> false
end
end
- def get_last(user_id) do
+ @doc "Returns last backup for the provided user"
+ @spec get_last(User.t()) :: t()
+ def get_last(%User{id: user_id}) do
__MODULE__
|> where(user_id: ^user_id)
|> order_by(desc: :id)
@@ -97,6 +142,8 @@ defmodule Pleroma.User.Backup do
|> Repo.one()
end
+ @doc "Lists all existing backups for a user"
+ @spec list(User.t()) :: [Ecto.Schema.t() | term()]
def list(%User{id: user_id}) do
__MODULE__
|> where(user_id: ^user_id)
@@ -104,151 +151,107 @@ defmodule Pleroma.User.Backup do
|> 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)
+ @doc "Schedules deletion of all but the the most recent backup"
+ @spec remove_outdated(User.t()) :: :ok
+ def remove_outdated(user) do
+ with %__MODULE__{} = latest_backup <- get_last(user) do
+ __MODULE__
+ |> where(user_id: ^user.id)
+ |> where([b], b.id != ^latest_backup.id)
+ |> Repo.all()
+ |> Enum.each(&schedule_delete/1)
+ else
+ _ -> :ok
+ end
end
- def get(id), do: Repo.get(__MODULE__, id)
-
- defp set_state(backup, state, processed_number \\ nil) do
- struct =
- %{state: state}
- |> Pleroma.Maps.put_if_present(:processed_number, processed_number)
+ def get_by_id(id), do: Repo.get(__MODULE__, id)
+ @doc "Generates changeset for %Pleroma.User.Backup{}"
+ @spec changeset(%__MODULE__{}, map()) :: %Ecto.Changeset{}
+ def changeset(backup \\ %__MODULE__{}, attrs) do
backup
- |> cast(struct, [:state, :processed_number])
- |> Repo.update()
+ |> cast(attrs, [:content_type, :file_name, :file_size, :processed, :tempdir])
end
- def process(%__MODULE__{} = backup) do
- set_state(backup, :running, 0)
-
- current_pid = self()
-
- task =
- Task.Supervisor.async_nolink(
- Pleroma.TaskSupervisor,
- __MODULE__,
- :do_process,
- [backup, current_pid]
- )
-
- wait_backup(backup, backup.processed_number, task)
+ @doc "Updates the backup record"
+ @spec update_record(%__MODULE__{}, map()) :: {:ok, %__MODULE__{}} | {:error, %Ecto.Changeset{}}
+ def update_record(%__MODULE__{} = backup, attrs) do
+ backup
+ |> changeset(attrs)
+ |> Repo.update()
end
- def do_process(backup, current_pid) do
- with {:ok, zip_file} <- export(backup, current_pid),
- {:ok, %{size: size}} <- File.stat(zip_file),
- {:ok, _upload} <- upload(backup, zip_file) do
- backup
- |> cast(
- %{
- file_size: size,
- processed: true,
- state: :complete
- },
- [:file_size, :processed, :state]
- )
- |> Repo.update()
+ @files [
+ ~c"actor.json",
+ ~c"outbox.json",
+ ~c"likes.json",
+ ~c"bookmarks.json",
+ ~c"followers.json",
+ ~c"following.json"
+ ]
+
+ @spec run(t()) :: {:ok, t()} | {:error, :failed}
+ def run(%__MODULE__{} = backup) do
+ backup = Repo.preload(backup, :user)
+ tempfile = Path.join([backup.tempdir, backup.file_name])
+
+ with {_, :ok} <- {:mkdir, File.mkdir_p(backup.tempdir)},
+ {_, :ok} <- {:actor, actor(backup.tempdir, backup.user)},
+ {_, :ok} <- {:statuses, statuses(backup.tempdir, backup.user)},
+ {_, :ok} <- {:likes, likes(backup.tempdir, backup.user)},
+ {_, :ok} <- {:bookmarks, bookmarks(backup.tempdir, backup.user)},
+ {_, :ok} <- {:followers, followers(backup.tempdir, backup.user)},
+ {_, :ok} <- {:following, following(backup.tempdir, backup.user)},
+ {_, {:ok, _zip_path}} <-
+ {:zip, :zip.create(to_charlist(tempfile), @files, cwd: to_charlist(backup.tempdir))},
+ {_, {:ok, %File.Stat{size: zip_size}}} <- {:filestat, File.stat(tempfile)},
+ {:ok, updated_backup} <- update_record(backup, %{file_size: zip_size}) do
+ {:ok, updated_backup}
+ else
+ _ ->
+ File.rm_rf(backup.tempdir)
+ {:error, :failed}
end
end
- defp wait_backup(backup, current_processed, task) do
- wait_time = Pleroma.Config.get([__MODULE__, :process_wait_time])
-
- receive do
- {:progress, new_processed} ->
- total_processed = current_processed + new_processed
-
- set_state(backup, :running, total_processed)
- wait_backup(backup, total_processed, task)
-
- {:DOWN, _ref, _proc, _pid, reason} ->
- backup = get(backup.id)
-
- if reason != :normal do
- Logger.error("Backup #{backup.id} process ended abnormally: #{inspect(reason)}")
-
- {:ok, backup} = set_state(backup, :failed)
+ defp tempdir do
+ rand = :crypto.strong_rand_bytes(8) |> Base.url_encode64(padding: false)
+ subdir = "backup-#{rand}"
- cleanup(backup)
-
- {:error,
- %{
- backup: backup,
- reason: :exit,
- details: reason
- }}
- else
- {:ok, backup}
- end
- after
- wait_time ->
- Logger.error(
- "Backup #{backup.id} timed out after no response for #{wait_time}ms, terminating"
- )
-
- Task.Supervisor.terminate_child(Pleroma.TaskSupervisor, task.pid)
-
- {:ok, backup} = set_state(backup, :failed)
-
- cleanup(backup)
-
- {:error,
- %{
- backup: backup,
- reason: :timeout
- }}
- end
- end
+ case Config.get([__MODULE__, :tempdir]) do
+ nil ->
+ Path.join([System.tmp_dir!(), subdir])
- @files ['actor.json', 'outbox.json', 'likes.json', 'bookmarks.json']
- def export(%__MODULE__{} = backup, caller_pid) do
- backup = Repo.preload(backup, :user)
- dir = backup_tempdir(backup)
-
- with :ok <- File.mkdir(dir),
- :ok <- actor(dir, backup.user, caller_pid),
- :ok <- statuses(dir, backup.user, caller_pid),
- :ok <- likes(dir, backup.user, caller_pid),
- :ok <- bookmarks(dir, backup.user, caller_pid),
- {:ok, zip_path} <- :zip.create(String.to_charlist(dir <> ".zip"), @files, cwd: dir),
- {:ok, _} <- File.rm_rf(dir) do
- {:ok, to_string(zip_path)}
+ path ->
+ Path.join([path, subdir])
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])
+ @doc "Uploads the completed backup and marks it as processed"
+ @spec upload(t()) :: {:ok, t()}
+ def upload(%__MODULE__{tempdir: tempdir} = backup) when is_binary(tempdir) do
+ uploader = Config.get([Pleroma.Upload, :uploader])
upload = %Pleroma.Upload{
name: backup.file_name,
- tempfile: zip_path,
+ tempfile: Path.join([tempdir, backup.file_name]),
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}
+ with {:ok, _} <- Uploader.put_file(uploader, upload),
+ {:ok, uploaded_backup} <- update_record(backup, %{processed: true}),
+ {:ok, _} <- File.rm_rf(tempdir) do
+ {:ok, uploaded_backup}
end
end
- defp actor(dir, user, caller_pid) do
+ 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
- send(caller_pid, {:progress, 1})
File.write(Path.join(dir, "actor.json"), json)
end
end
@@ -267,22 +270,10 @@ defmodule Pleroma.User.Backup do
)
end
- defp should_report?(num, chunk_size), do: rem(num, chunk_size) == 0
-
- defp backup_tempdir(backup) do
- name = String.trim_trailing(backup.file_name, ".zip")
- dir(name)
- end
-
- defp cleanup(backup) do
- dir = backup_tempdir(backup)
- File.rm_rf(dir)
- end
-
- defp write(query, dir, name, fun, caller_pid) do
+ defp write(query, dir, name, fun) do
path = Path.join(dir, "#{name}.json")
- chunk_size = Pleroma.Config.get([__MODULE__, :process_chunk_size])
+ chunk_size = Config.get([__MODULE__, :process_chunk_size])
with {:ok, file} <- File.open(path, [:write, :utf8]),
:ok <- write_header(file, name) do
@@ -298,14 +289,10 @@ defmodule Pleroma.User.Backup do
end),
{:ok, str} <- Jason.encode(data),
:ok <- IO.write(file, str <> ",\n") do
- if should_report?(acc + 1, chunk_size) do
- send(caller_pid, {:progress, chunk_size})
- end
-
acc + 1
else
{:error, e} ->
- Logger.warn(
+ Logger.warning(
"Error processing backup item: #{inspect(e)}\n The item is: #{inspect(i)}"
)
@@ -316,31 +303,29 @@ defmodule Pleroma.User.Backup do
end
end)
- send(caller_pid, {:progress, rem(total, chunk_size)})
-
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, caller_pid) do
+ 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, caller_pid)
+ |> write(dir, "bookmarks", fn a -> {:ok, a.object} end)
end
- defp likes(dir, user, caller_pid) do
+ 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, caller_pid)
+ |> write(dir, "likes", fn a -> {:ok, a.object} end)
end
- defp statuses(dir, user, caller_pid) do
+ defp statuses(dir, user) do
opts =
%{}
|> Map.put(:type, ["Create", "Announce"])
@@ -360,8 +345,17 @@ defmodule Pleroma.User.Backup do
with {:ok, activity} <- Transmogrifier.prepare_outgoing(a.data) do
{:ok, Map.delete(activity, "@context")}
end
- end,
- caller_pid
+ end
)
end
+
+ defp followers(dir, user) do
+ User.get_followers_query(user)
+ |> write(dir, "followers", fn a -> {:ok, a.ap_id} end)
+ end
+
+ defp following(dir, user) do
+ User.get_friends_query(user)
+ |> write(dir, "following", fn a -> {:ok, a.ap_id} end)
+ end
end
diff --git a/lib/pleroma/user/import.ex b/lib/pleroma/user/import.ex
index 4baa7e3a4..53ffd1ab3 100644
--- a/lib/pleroma/user/import.ex
+++ b/lib/pleroma/user/import.ex
@@ -31,7 +31,7 @@ defmodule Pleroma.User.Import do
identifiers,
fn identifier ->
with {:ok, %User{} = blocked} <- User.get_or_fetch(identifier),
- {:ok, _block} <- CommonAPI.block(blocker, blocked) do
+ {:ok, _block} <- CommonAPI.block(blocked, blocker) do
blocked
else
error -> handle_error(:blocks_import, identifier, error)
@@ -46,7 +46,7 @@ defmodule Pleroma.User.Import do
fn identifier ->
with {:ok, %User{} = followed} <- User.get_or_fetch(identifier),
{:ok, follower, followed} <- User.maybe_direct_follow(follower, followed),
- {:ok, _, _, _} <- CommonAPI.follow(follower, followed) do
+ {:ok, _, _, _} <- CommonAPI.follow(followed, follower) do
followed
else
error -> handle_error(:follow_import, identifier, error)
diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex
index 3e090cac0..cd9586452 100644
--- a/lib/pleroma/user/query.ex
+++ b/lib/pleroma/user/query.ex
@@ -22,7 +22,7 @@ defmodule Pleroma.User.Query do
- pass non empty string
- e.g. Pleroma.User.Query.build(%{email: "email@example.com"})
- *contains criteria*
- - add field to @containns_criteria list
+ - add field to @contains_criteria list
- pass values list
- e.g. Pleroma.User.Query.build(%{ap_id: ["http://ap_id1", "http://ap_id2"]})
"""
@@ -71,7 +71,7 @@ defmodule Pleroma.User.Query do
@equal_criteria [:email]
@contains_criteria [:ap_id, :nickname]
- @spec build(Query.t(), criteria()) :: Query.t()
+ @spec build(Ecto.Query.t(), criteria()) :: Ecto.Query.t()
def build(query \\ base_query(), criteria) do
prepare_query(query, criteria)
end
diff --git a/lib/pleroma/user_invite_token.ex b/lib/pleroma/user_invite_token.ex
index b242a8848..4bfb3a6a7 100644
--- a/lib/pleroma/user_invite_token.ex
+++ b/lib/pleroma/user_invite_token.ex
@@ -64,7 +64,7 @@ defmodule Pleroma.UserInviteToken do
end
@spec update_invite(UserInviteToken.t(), map()) ::
- {:ok, UserInviteToken.t()} | {:error, Changeset.t()}
+ {:ok, UserInviteToken.t()} | {:error, Ecto.Changeset.t()}
def update_invite(invite, changes) do
change(invite, changes) |> Repo.update()
end
diff --git a/lib/pleroma/user_relationship.ex b/lib/pleroma/user_relationship.ex
index fbecf3129..82fcc1cdd 100644
--- a/lib/pleroma/user_relationship.ex
+++ b/lib/pleroma/user_relationship.ex
@@ -14,6 +14,8 @@ defmodule Pleroma.UserRelationship do
alias Pleroma.User
alias Pleroma.UserRelationship
+ @type t :: %__MODULE__{}
+
schema "user_relationships" do
belongs_to(:source, User, type: FlakeId.Ecto.CompatType)
belongs_to(:target, User, type: FlakeId.Ecto.CompatType)
diff --git a/lib/pleroma/web.ex b/lib/pleroma/web.ex
index aee41b0fe..e7e7e96f9 100644
--- a/lib/pleroma/web.ex
+++ b/lib/pleroma/web.ex
@@ -136,7 +136,7 @@ defmodule Pleroma.Web do
namespace: Pleroma.Web
# Import convenience functions from controllers
- import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1]
+ import Phoenix.Controller, only: [get_csrf_token: 0, view_module: 1]
import Pleroma.Web.ErrorHelpers
import Pleroma.Web.Gettext
@@ -163,7 +163,7 @@ defmodule Pleroma.Web do
"""
def safe_render_many(collection, view, template, assigns \\ %{}) do
Enum.map(collection, fn resource ->
- as = Map.get(assigns, :as) || view.__resource__
+ as = Map.get(assigns, :as) || view.__resource__()
assigns = Map.put(assigns, as, resource)
safe_render(view, template, assigns)
end)
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 3979d418e..b30b0cabe 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -74,28 +74,39 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp check_remote_limit(_), do: true
def increase_note_count_if_public(actor, object) do
- if is_public?(object), do: User.increase_note_count(actor), else: {:ok, actor}
+ if public?(object), do: User.increase_note_count(actor), else: {:ok, actor}
end
def decrease_note_count_if_public(actor, object) do
- if is_public?(object), do: User.decrease_note_count(actor), else: {:ok, actor}
+ if public?(object), do: User.decrease_note_count(actor), else: {:ok, actor}
end
def update_last_status_at_if_public(actor, object) do
- if is_public?(object), do: User.update_last_status_at(actor), else: {:ok, actor}
+ if public?(object), do: User.update_last_status_at(actor), else: {:ok, actor}
end
defp increase_replies_count_if_reply(%{
"object" => %{"inReplyTo" => reply_ap_id} = object,
"type" => "Create"
}) do
- if is_public?(object) do
+ if public?(object) do
Object.increase_replies_count(reply_ap_id)
end
end
defp increase_replies_count_if_reply(_create_data), do: :noop
+ defp increase_quotes_count_if_quote(%{
+ "object" => %{"quoteUrl" => quote_ap_id} = object,
+ "type" => "Create"
+ }) do
+ if public?(object) do
+ Object.increase_quotes_count(quote_ap_id)
+ end
+ end
+
+ defp increase_quotes_count_if_quote(_create_data), do: :noop
+
@object_types ~w[ChatMessage Question Answer Audio Video Image Event Article Note Page]
@impl true
def persist(%{"type" => type} = object, meta) when type in @object_types do
@@ -136,9 +147,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
# Splice in the child object if we have one.
activity = Maps.put_if_present(activity, :object, object)
- ConcurrentLimiter.limit(Pleroma.Web.RichMedia.Helpers, fn ->
- Task.start(fn -> Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) end)
- end)
+ Pleroma.Web.RichMedia.Card.get_by_activity(activity)
+
+ # Add local posts to search index
+ if local, do: Pleroma.Search.add_to_index(activity)
{:ok, activity}
else
@@ -163,7 +175,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
id: "pleroma:fakeid"
}
- Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
+ Pleroma.Web.RichMedia.Card.get_by_activity(activity)
{:ok, activity}
{:remote_limit_pass, _} ->
@@ -188,7 +200,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
def notify_and_stream(activity) do
- Notification.create_notifications(activity)
+ {:ok, notifications} = Notification.create_notifications(activity)
+ Notification.stream(notifications)
original_activity =
case activity do
@@ -299,11 +312,13 @@ 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_quotes_count_if_quote(create_data),
{:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity},
{:ok, _actor} <- increase_note_count_if_public(actor, activity),
{:ok, _actor} <- update_last_status_at_if_public(actor, activity),
_ <- notify_and_stream(activity),
:ok <- maybe_schedule_poll_notifications(activity),
+ :ok <- maybe_handle_group_posts(activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
@@ -483,7 +498,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
@spec fetch_latest_direct_activity_id_for_context(String.t(), keyword() | map()) ::
- FlakeId.Ecto.CompatType.t() | nil
+ Ecto.UUID.t() | nil
def fetch_latest_direct_activity_id_for_context(context, opts \\ %{}) do
context
|> fetch_activities_for_context_query(Map.merge(%{skip_preload: true}, opts))
@@ -964,8 +979,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_replies(query, %{exclude_replies: true}) do
from(
- [_activity, object] in query,
- where: fragment("?->>'inReplyTo' is null", object.data)
+ [activity, object] in query,
+ where:
+ fragment("?->>'inReplyTo' is null or ?->>'type' = 'Announce'", object.data, activity.data)
)
end
@@ -1237,6 +1253,23 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_unauthenticated(query, _), do: query
+ defp restrict_quote_url(query, %{quote_url: quote_url}) do
+ from([_activity, object] in query,
+ where: fragment("(?)->'quoteUrl' = ?", object.data, ^quote_url)
+ )
+ end
+
+ defp restrict_quote_url(query, _), do: query
+
+ defp restrict_rule(query, %{rule_id: rule_id}) do
+ from(
+ activity in query,
+ where: fragment("(?)->'rules' \\? (?)", activity.data, ^rule_id)
+ )
+ end
+
+ defp restrict_rule(query, _), do: query
+
defp exclude_poll_votes(query, %{include_poll_votes: true}), do: query
defp exclude_poll_votes(query, _) do
@@ -1399,6 +1432,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> restrict_instance(opts)
|> restrict_announce_object_actor(opts)
|> restrict_filtered(opts)
+ |> restrict_rule(opts)
+ |> restrict_quote_url(opts)
|> maybe_restrict_deactivated_users(opts)
|> exclude_poll_votes(opts)
|> exclude_chat_messages(opts)
@@ -1626,7 +1661,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
}}
else
{:error, _} = e -> e
- e -> {:error, e}
end
end
@@ -1673,9 +1707,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
Fetcher.fetch_and_contain_remote_object_from_id(first) do
{:ok, false}
else
- {:error, {:ok, %{status: code}}} when code in [401, 403] -> {:ok, true}
- {:error, _} = e -> e
- e -> {:error, e}
+ {:error, _} -> {:ok, true}
end
end
@@ -1761,24 +1793,25 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
- def pinned_fetch_task(nil), do: nil
-
- def pinned_fetch_task(%{pinned_objects: pins}) do
- if Enum.all?(pins, fn {ap_id, _} ->
- Object.get_cached_by_ap_id(ap_id) ||
- match?({:ok, _object}, Fetcher.fetch_object_from_id(ap_id))
- end) do
- :ok
- else
- :error
- end
+ def enqueue_pin_fetches(%{pinned_objects: pins}) do
+ # enqueue a task to fetch all pinned objects
+ Enum.each(pins, fn {ap_id, _} ->
+ if is_nil(Object.get_cached_by_ap_id(ap_id)) do
+ Pleroma.Workers.RemoteFetcherWorker.enqueue("fetch_remote", %{
+ "id" => ap_id,
+ "depth" => 1
+ })
+ end
+ end)
end
+ def enqueue_pin_fetches(_), do: nil
+
def make_user_from_ap_id(ap_id, additional \\ []) do
user = User.get_cached_by_ap_id(ap_id)
with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id, additional) do
- {:ok, _pid} = Task.start(fn -> pinned_fetch_task(data) end)
+ enqueue_pin_fetches(data)
if user do
user
diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
index 1357c379c..cdd054e1a 100644
--- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
@@ -52,6 +52,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
when action in [:activity, :object]
)
+ plug(:log_inbox_metadata when action in [:inbox])
plug(:set_requester_reachable when action in [:inbox])
plug(:relay_active? when action in [:relay])
@@ -273,12 +274,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
end
def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
- with %User{} = recipient <- User.get_cached_by_nickname(nickname),
- {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
+ with %User{is_active: true} = recipient <- User.get_cached_by_nickname(nickname),
+ {:ok, %User{is_active: true} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
true <- Utils.recipient_in_message(recipient, actor, params),
params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
Federator.incoming_ap_doc(params)
json(conn, "ok")
+ else
+ _ ->
+ conn
+ |> put_status(:bad_request)
+ |> json("Invalid request.")
end
end
@@ -287,10 +293,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
json(conn, "ok")
end
- def inbox(%{assigns: %{valid_signature: false}} = conn, _params) do
- conn
- |> put_status(:bad_request)
- |> json("Invalid HTTP Signature")
+ def inbox(%{assigns: %{valid_signature: false}} = conn, params) do
+ Federator.incoming_ap_doc(%{
+ method: conn.method,
+ req_headers: conn.req_headers,
+ request_path: conn.request_path,
+ params: params,
+ query_string: conn.query_string
+ })
+
+ json(conn, "ok")
end
# POST /relay/inbox -or- POST /internal/fetch/inbox
@@ -476,7 +488,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|> json(message)
e ->
- Logger.warn(fn -> "AP C2S: #{inspect(e)}" end)
+ Logger.warning(fn -> "AP C2S: #{inspect(e)}" end)
conn
|> put_status(:bad_request)
@@ -517,6 +529,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
conn
end
+ defp log_inbox_metadata(%{params: %{"actor" => actor, "type" => type}} = conn, _) do
+ Logger.metadata(actor: actor, type: type)
+ conn
+ end
+
+ defp log_inbox_metadata(conn, _), do: conn
+
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 eb0bb0e33..2a1e56278 100644
--- a/lib/pleroma/web/activity_pub/builder.ex
+++ b/lib/pleroma/web/activity_pub/builder.ex
@@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do
This module encodes our addressing policies and general shape of our objects.
"""
+ alias Pleroma.Activity
alias Pleroma.Emoji
alias Pleroma.Object
alias Pleroma.User
@@ -131,7 +132,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do
def emoji_react(actor, object, emoji) do
with {:ok, data, meta} <- object_action(actor, object) do
data =
- if Emoji.is_unicode_emoji?(emoji) do
+ if Emoji.unicode?(emoji) do
unicode_emoji_react(object, data, emoji)
else
custom_emoji_react(object, data, emoji)
@@ -347,7 +348,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do
actor.ap_id == Relay.ap_id() ->
[actor.follower_address]
- public? and Visibility.is_local_public?(object) ->
+ public? and Visibility.local_public?(object) ->
[actor.follower_address, object.data["actor"], Utils.as_local_public()]
public? ->
@@ -375,7 +376,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do
# Address the actor of the object, and our actor's follower collection if the post is public.
to =
- if Visibility.is_public?(object) do
+ if Visibility.public?(object) do
[actor.follower_address, object.data["actor"]]
else
[object.data["actor"]]
diff --git a/lib/pleroma/web/activity_pub/mrf.ex b/lib/pleroma/web/activity_pub/mrf.ex
index ff9f84497..bc418d908 100644
--- a/lib/pleroma/web/activity_pub/mrf.ex
+++ b/lib/pleroma/web/activity_pub/mrf.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF do
@@ -54,6 +54,8 @@ defmodule Pleroma.Web.ActivityPub.MRF do
@required_description_keys [:key, :related_policy]
def filter_one(policy, message) do
+ Code.ensure_loaded(policy)
+
should_plug_history? =
if function_exported?(policy, :history_awareness, 0) do
policy.history_awareness()
@@ -137,7 +139,16 @@ defmodule Pleroma.Web.ActivityPub.MRF do
@spec subdomains_regex([String.t()]) :: [Regex.t()]
def subdomains_regex(domains) when is_list(domains) do
- for domain <- domains, do: ~r(^#{String.replace(domain, "*.", "(.*\\.)*")}$)i
+ for domain <- domains do
+ try do
+ target = String.replace(domain, "*.", "(.*\\.)*")
+ ~r<^#{target}$>i
+ rescue
+ e ->
+ Logger.error("MRF: Invalid subdomain Regex: #{domain}")
+ reraise e, __STACKTRACE__
+ end
+ end
end
@spec subdomain_match?([Regex.t()], String.t()) :: boolean()
@@ -188,10 +199,12 @@ defmodule Pleroma.Web.ActivityPub.MRF do
def config_descriptions(policies) do
Enum.reduce(policies, @mrf_config_descriptions, fn policy, acc ->
+ Code.ensure_loaded(policy)
+
if function_exported?(policy, :config_description, 0) do
description =
@default_description
- |> Map.merge(policy.config_description)
+ |> Map.merge(policy.config_description())
|> Map.put(:group, :pleroma)
|> Map.put(:tab, :mrf)
|> Map.put(:type, :group)
@@ -199,7 +212,7 @@ defmodule Pleroma.Web.ActivityPub.MRF do
if Enum.all?(@required_description_keys, &Map.has_key?(description, &1)) do
[description | acc]
else
- Logger.warn(
+ Logger.warning(
"#{policy} config description doesn't have one or all required keys #{inspect(@required_description_keys)}"
)
diff --git a/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex
index 97d75ecf2..df4ba819c 100644
--- a/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex
@@ -56,8 +56,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy do
nick_score + name_score + actor_type_score
end
- defp determine_if_followbot(_), do: 0.0
-
defp bot_allowed?(%{"object" => target}, bot_actor) do
%User{} = user = normalize_by_ap_id(target)
diff --git a/lib/pleroma/web/activity_pub/mrf/anti_mention_spam_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_mention_spam_policy.ex
new file mode 100644
index 000000000..531e75ce8
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/mrf/anti_mention_spam_policy.ex
@@ -0,0 +1,87 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.AntiMentionSpamPolicy do
+ alias Pleroma.Config
+ alias Pleroma.User
+ require Pleroma.Constants
+
+ @behaviour Pleroma.Web.ActivityPub.MRF.Policy
+
+ defp user_has_posted?(%User{} = u), do: u.note_count > 0
+
+ defp user_has_age?(%User{} = u) do
+ user_age_limit = Config.get([:mrf_antimentionspam, :user_age_limit], 30_000)
+ diff = NaiveDateTime.utc_now() |> NaiveDateTime.diff(u.inserted_at, :millisecond)
+ diff >= user_age_limit
+ end
+
+ defp good_reputation?(%User{} = u) do
+ user_has_age?(u) and user_has_posted?(u)
+ end
+
+ # copied from HellthreadPolicy
+ defp get_recipient_count(message) do
+ recipients = (message["to"] || []) ++ (message["cc"] || [])
+
+ follower_collection =
+ User.get_cached_by_ap_id(message["actor"] || message["attributedTo"]).follower_address
+
+ if Enum.member?(recipients, Pleroma.Constants.as_public()) do
+ recipients =
+ recipients
+ |> List.delete(Pleroma.Constants.as_public())
+ |> List.delete(follower_collection)
+
+ {:public, length(recipients)}
+ else
+ recipients =
+ recipients
+ |> List.delete(follower_collection)
+
+ {:not_public, length(recipients)}
+ end
+ end
+
+ defp object_has_recipients?(%{"object" => object} = activity) do
+ {_, object_count} = get_recipient_count(object)
+ {_, activity_count} = get_recipient_count(activity)
+ object_count + activity_count > 0
+ end
+
+ defp object_has_recipients?(object) do
+ {_, count} = get_recipient_count(object)
+ count > 0
+ end
+
+ @impl true
+ def filter(%{"type" => "Create", "actor" => actor} = activity) do
+ with {:ok, %User{local: false} = u} <- User.get_or_fetch_by_ap_id(actor),
+ {:has_mentions, true} <- {:has_mentions, object_has_recipients?(activity)},
+ {:good_reputation, true} <- {:good_reputation, good_reputation?(u)} do
+ {:ok, activity}
+ else
+ {:ok, %User{local: true}} ->
+ {:ok, activity}
+
+ {:has_mentions, false} ->
+ {:ok, activity}
+
+ {:good_reputation, false} ->
+ {:reject, "[AntiMentionSpamPolicy] User rejected"}
+
+ {:error, _} ->
+ {:reject, "[AntiMentionSpamPolicy] Failed to get or fetch user by ap_id"}
+
+ e ->
+ {:reject, "[AntiMentionSpamPolicy] Unhandled error #{inspect(e)}"}
+ end
+ end
+
+ # in all other cases, pass through
+ def filter(message), do: {:ok, message}
+
+ @impl true
+ def describe, do: {:ok, %{}}
+end
diff --git a/lib/pleroma/web/activity_pub/mrf/dnsrbl_policy.ex b/lib/pleroma/web/activity_pub/mrf/dnsrbl_policy.ex
new file mode 100644
index 000000000..7c6bb888f
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/mrf/dnsrbl_policy.ex
@@ -0,0 +1,146 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.DNSRBLPolicy do
+ @moduledoc """
+ Dynamic activity filtering based on an RBL database
+
+ This MRF makes queries to a custom DNS server which will
+ respond with values indicating the classification of the domain
+ the activity originated from. This method has been widely used
+ in the email anti-spam industry for very fast reputation checks.
+
+ e.g., if the DNS response is 127.0.0.1 or empty, the domain is OK
+ Other values such as 127.0.0.2 may be used for specific classifications.
+
+ Information for why the host is blocked can be stored in a corresponding TXT record.
+
+ This method is fail-open so if the queries fail the activites are accepted.
+
+ An example of software meant for this purpsoe is rbldnsd which can be found
+ at http://www.corpit.ru/mjt/rbldnsd.html or mirrored at
+ https://git.pleroma.social/feld/rbldnsd
+
+ It is highly recommended that you run your own copy of rbldnsd and use an
+ external mechanism to sync/share the contents of the zone file. This is
+ important to keep the latency on the queries as low as possible and prevent
+ your DNS server from being attacked so it fails and content is permitted.
+ """
+
+ @behaviour Pleroma.Web.ActivityPub.MRF.Policy
+
+ alias Pleroma.Config
+
+ require Logger
+
+ @query_retries 1
+ @query_timeout 500
+
+ @impl true
+ def filter(%{"actor" => actor} = object) do
+ actor_info = URI.parse(actor)
+
+ with {:ok, object} <- check_rbl(actor_info, object) do
+ {:ok, object}
+ else
+ _ -> {:reject, "[DNSRBLPolicy]"}
+ end
+ end
+
+ @impl true
+ def filter(object), do: {:ok, object}
+
+ @impl true
+ def describe do
+ mrf_dnsrbl =
+ Config.get(:mrf_dnsrbl)
+ |> Enum.into(%{})
+
+ {:ok, %{mrf_dnsrbl: mrf_dnsrbl}}
+ end
+
+ @impl true
+ def config_description do
+ %{
+ key: :mrf_dnsrbl,
+ related_policy: "Pleroma.Web.ActivityPub.MRF.DNSRBLPolicy",
+ label: "MRF DNSRBL",
+ description: "DNS RealTime Blackhole Policy",
+ children: [
+ %{
+ key: :nameserver,
+ type: {:string},
+ description: "DNSRBL Nameserver to Query (IP or hostame)",
+ suggestions: ["127.0.0.1"]
+ },
+ %{
+ key: :port,
+ type: {:string},
+ description: "Nameserver port",
+ suggestions: ["53"]
+ },
+ %{
+ key: :zone,
+ type: {:string},
+ description: "Root zone for querying",
+ suggestions: ["bl.pleroma.com"]
+ }
+ ]
+ }
+ end
+
+ defp check_rbl(%{host: actor_host}, object) do
+ with false <- match?(^actor_host, Pleroma.Web.Endpoint.host()),
+ zone when not is_nil(zone) <- Keyword.get(Config.get([:mrf_dnsrbl]), :zone) do
+ query =
+ Enum.join([actor_host, zone], ".")
+ |> String.to_charlist()
+
+ rbl_response = rblquery(query)
+
+ if Enum.empty?(rbl_response) do
+ {:ok, object}
+ else
+ Task.start(fn ->
+ reason =
+ case rblquery(query, :txt) do
+ [[result]] -> result
+ _ -> "undefined"
+ end
+
+ Logger.warning(
+ "DNSRBL Rejected activity from #{actor_host} for reason: #{inspect(reason)}"
+ )
+ end)
+
+ :error
+ end
+ else
+ _ -> {:ok, object}
+ end
+ end
+
+ defp get_rblhost_ip(rblhost) do
+ case rblhost |> String.to_charlist() |> :inet_parse.address() do
+ {:ok, _} -> rblhost |> String.to_charlist() |> :inet_parse.address()
+ _ -> {:ok, rblhost |> String.to_charlist() |> :inet_res.lookup(:in, :a) |> Enum.random()}
+ end
+ end
+
+ defp rblquery(query, type \\ :a) do
+ config = Config.get([:mrf_dnsrbl])
+
+ case get_rblhost_ip(config[:nameserver]) do
+ {:ok, rblnsip} ->
+ :inet_res.lookup(query, :in, type,
+ nameservers: [{rblnsip, config[:port]}],
+ timeout: @query_timeout,
+ retry: @query_retries
+ )
+
+ _ ->
+ []
+ end
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/mrf/follow_bot_policy.ex b/lib/pleroma/web/activity_pub/mrf/follow_bot_policy.ex
index 5b6adbb4b..55ea2683c 100644
--- a/lib/pleroma/web/activity_pub/mrf/follow_bot_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/follow_bot_policy.ex
@@ -19,7 +19,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.FollowBotPolicy do
try_follow(follower, message)
else
nil ->
- Logger.warn(
+ Logger.warning(
"#{__MODULE__} skipped because of missing `:mrf_follow_bot, :follower_nickname` configuration, the :follower_nickname
account does not exist, or the account is not correctly configured as a bot."
)
@@ -49,7 +49,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.FollowBotPolicy do
"#{__MODULE__}: Follow request from #{follower.nickname} to #{user.nickname}"
)
- CommonAPI.follow(follower, user)
+ CommonAPI.follow(user, follower)
end
end)
diff --git a/lib/pleroma/web/activity_pub/mrf/force_mention.ex b/lib/pleroma/web/activity_pub/mrf/force_mention.ex
new file mode 100644
index 000000000..3853489fc
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/mrf/force_mention.ex
@@ -0,0 +1,59 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.ForceMention do
+ require Pleroma.Constants
+
+ alias Pleroma.Config
+ alias Pleroma.Object
+ alias Pleroma.User
+
+ @behaviour Pleroma.Web.ActivityPub.MRF.Policy
+
+ defp get_author(url) do
+ with %Object{data: %{"actor" => actor}} <- Object.normalize(url, fetch: false),
+ %User{ap_id: ap_id, nickname: nickname} <- User.get_cached_by_ap_id(actor) do
+ %{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"}
+ else
+ _ -> nil
+ end
+ end
+
+ defp prepend_author(tags, _, false), do: tags
+
+ defp prepend_author(tags, nil, _), do: tags
+
+ defp prepend_author(tags, url, _) do
+ actor = get_author(url)
+
+ if not is_nil(actor) do
+ [actor | tags]
+ else
+ tags
+ end
+ end
+
+ @impl true
+ def filter(%{"type" => "Create", "object" => %{"tag" => tag} = object} = activity) do
+ tag =
+ tag
+ |> prepend_author(
+ object["inReplyTo"],
+ Config.get([:mrf_force_mention, :mention_parent, true])
+ )
+ |> prepend_author(
+ object["quoteUrl"],
+ Config.get([:mrf_force_mention, :mention_quoted, true])
+ )
+ |> Enum.uniq()
+
+ {:ok, put_in(activity["object"]["tag"], tag)}
+ end
+
+ @impl true
+ def filter(object), do: {:ok, object}
+
+ @impl true
+ def describe, do: {:ok, %{}}
+end
diff --git a/lib/pleroma/web/activity_pub/mrf/hashtag_policy.ex b/lib/pleroma/web/activity_pub/mrf/hashtag_policy.ex
index b73fd974c..fdb9a9dba 100644
--- a/lib/pleroma/web/activity_pub/mrf/hashtag_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/hashtag_policy.ex
@@ -9,7 +9,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.HashtagPolicy do
alias Pleroma.Object
@moduledoc """
- Reject, TWKN-remove or Set-Sensitive messsages with specific hashtags (without the leading #)
+ Reject, TWKN-remove or Set-Sensitive messages with specific hashtags (without the leading #)
Note: This MRF Policy is always enabled, if you want to disable it you have to set empty lists.
"""
@@ -84,7 +84,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.HashtagPolicy do
if hashtags != [] do
with {:ok, message} <- check_reject(message, hashtags),
{:ok, message} <-
- (if "type" == "Create" do
+ (if type == "Create" do
check_ftl_removal(message, hashtags)
else
{:ok, message}
diff --git a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex
index 171b22c5e..b7a01c27c 100644
--- a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex
@@ -62,7 +62,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy do
key: :mrf_inline_quote,
related_policy: "Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy",
label: "MRF Inline Quote Policy",
- type: :group,
description: "Force quote url to appear in post content.",
children: [
%{
diff --git a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex
index 874fe9ab9..729da4e9c 100644
--- a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex
@@ -10,15 +10,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
@moduledoc "Reject or Word-Replace messages with a keyword or regex"
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
- defp string_matches?(string, _) when not is_binary(string) do
- false
- end
defp string_matches?(string, pattern) when is_binary(pattern) do
String.contains?(string, pattern)
end
- defp string_matches?(string, pattern) do
+ defp string_matches?(string, %Regex{} = pattern) do
String.match?(string, pattern)
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 c95d35bb9..0c5b53def 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
@@ -11,11 +11,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do
require Logger
- @adapter_options [
- pool: :media,
- recv_timeout: 10_000
- ]
-
@impl true
def history_awareness, do: :auto
@@ -27,17 +22,14 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do
Logger.debug("Prefetching #{inspect(url)} as #{inspect(prefetch_url)}")
- if Pleroma.Config.get(:env) == :test do
- fetch(prefetch_url)
- else
- ConcurrentLimiter.limit(__MODULE__, fn ->
- Task.start(fn -> fetch(prefetch_url) end)
- end)
- end
+ fetch(prefetch_url)
end
end
- defp fetch(url), do: HTTP.get(url, [], @adapter_options)
+ defp fetch(url) do
+ http_client_opts = Pleroma.Config.get([:media_proxy, :proxy_opts, :http], pool: :media)
+ HTTP.get(url, [], http_client_opts)
+ end
defp preload(%{"object" => %{"attachment" => attachments}} = _message) do
Enum.each(attachments, fn
diff --git a/lib/pleroma/web/activity_pub/mrf/no_empty_policy.ex b/lib/pleroma/web/activity_pub/mrf/no_empty_policy.ex
index 855cda3b9..12bf4ddd2 100644
--- a/lib/pleroma/web/activity_pub/mrf/no_empty_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/no_empty_policy.ex
@@ -10,9 +10,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy do
@impl true
def filter(%{"actor" => actor} = object) do
- with true <- is_local?(actor),
- true <- is_eligible_type?(object),
- true <- is_note?(object),
+ with true <- local?(actor),
+ true <- eligible_type?(object),
+ true <- note?(object),
false <- has_attachment?(object),
true <- only_mentions?(object) do
{:reject, "[NoEmptyPolicy]"}
@@ -24,7 +24,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy do
def filter(object), do: {:ok, object}
- defp is_local?(actor) do
+ defp local?(actor) do
if actor |> String.starts_with?("#{Endpoint.url()}") do
true
else
@@ -59,11 +59,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy do
defp only_mentions?(_), do: false
- defp is_note?(%{"object" => %{"type" => "Note"}}), do: true
- defp is_note?(_), do: false
+ defp note?(%{"object" => %{"type" => "Note"}}), do: true
+ defp note?(_), do: false
- defp is_eligible_type?(%{"type" => type}) when type in ["Create", "Update"], do: true
- defp is_eligible_type?(_), do: false
+ defp eligible_type?(%{"type" => type}) when type in ["Create", "Update"], do: true
+ defp eligible_type?(_), do: false
@impl true
def describe, do: {:ok, %{}}
diff --git a/lib/pleroma/web/activity_pub/mrf/nsfw_api_policy.ex b/lib/pleroma/web/activity_pub/mrf/nsfw_api_policy.ex
new file mode 100644
index 000000000..451a212d4
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/mrf/nsfw_api_policy.ex
@@ -0,0 +1,264 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.NsfwApiPolicy do
+ @moduledoc """
+ Hide, delete, or mark sensitive NSFW content with artificial intelligence.
+
+ Requires a NSFW API server, configured like so:
+
+ config :pleroma, Pleroma.Web.ActivityPub.MRF.NsfwMRF,
+ url: "http://127.0.0.1:5000/",
+ threshold: 0.7,
+ mark_sensitive: true,
+ unlist: false,
+ reject: false
+
+ The NSFW API server must implement an HTTP endpoint like this:
+
+ curl http://localhost:5000/?url=https://fedi.com/images/001.jpg
+
+ Returning a response like this:
+
+ {"score", 0.314}
+
+ Where a score is 0-1, with `1` being definitely NSFW.
+
+ A good API server is here: https://github.com/EugenCepoi/nsfw_api
+ You can run it with Docker with a one-liner:
+
+ docker run -it -p 127.0.0.1:5000:5000/tcp --env PORT=5000 eugencepoi/nsfw_api:latest
+
+ Options:
+
+ - `url`: Base URL of the API server. Default: "http://127.0.0.1:5000/"
+ - `threshold`: Lowest score to take action on. Default: `0.7`
+ - `mark_sensitive`: Mark sensitive all detected NSFW content? Default: `true`
+ - `unlist`: Unlist all detected NSFW content? Default: `false`
+ - `reject`: Reject all detected NSFW content (takes precedence)? Default: `false`
+ """
+ alias Pleroma.Config
+ alias Pleroma.Constants
+ alias Pleroma.HTTP
+ alias Pleroma.User
+
+ require Logger
+ require Pleroma.Constants
+
+ @behaviour Pleroma.Web.ActivityPub.MRF.Policy
+ @policy :mrf_nsfw_api
+
+ def build_request_url(url) do
+ Config.get([@policy, :url])
+ |> URI.parse()
+ |> fix_path()
+ |> Map.put(:query, "url=#{url}")
+ |> URI.to_string()
+ end
+
+ def parse_url(url) do
+ request = build_request_url(url)
+
+ with {:ok, %Tesla.Env{body: body}} <- HTTP.get(request) do
+ Jason.decode(body)
+ else
+ error ->
+ Logger.warning("""
+ [NsfwApiPolicy]: The API server failed. Skipping.
+ #{inspect(error)}
+ """)
+
+ error
+ end
+ end
+
+ def check_url_nsfw(url) when is_binary(url) do
+ threshold = Config.get([@policy, :threshold])
+
+ case parse_url(url) do
+ {:ok, %{"score" => score}} when score >= threshold ->
+ {:nsfw, %{url: url, score: score, threshold: threshold}}
+
+ {:ok, %{"score" => score}} ->
+ {:sfw, %{url: url, score: score, threshold: threshold}}
+
+ _ ->
+ {:sfw, %{url: url, score: nil, threshold: threshold}}
+ end
+ end
+
+ def check_url_nsfw(%{"href" => url}) when is_binary(url) do
+ check_url_nsfw(url)
+ end
+
+ def check_url_nsfw(url) do
+ threshold = Config.get([@policy, :threshold])
+ {:sfw, %{url: url, score: nil, threshold: threshold}}
+ end
+
+ def check_attachment_nsfw(%{"url" => urls} = attachment) when is_list(urls) do
+ if Enum.all?(urls, &match?({:sfw, _}, check_url_nsfw(&1))) do
+ {:sfw, attachment}
+ else
+ {:nsfw, attachment}
+ end
+ end
+
+ def check_attachment_nsfw(%{"url" => url} = attachment) when is_binary(url) do
+ case check_url_nsfw(url) do
+ {:sfw, _} -> {:sfw, attachment}
+ {:nsfw, _} -> {:nsfw, attachment}
+ end
+ end
+
+ def check_attachment_nsfw(attachment), do: {:sfw, attachment}
+
+ def check_object_nsfw(%{"attachment" => attachments} = object) when is_list(attachments) do
+ if Enum.all?(attachments, &match?({:sfw, _}, check_attachment_nsfw(&1))) do
+ {:sfw, object}
+ else
+ {:nsfw, object}
+ end
+ end
+
+ def check_object_nsfw(%{"object" => %{} = child_object} = object) do
+ case check_object_nsfw(child_object) do
+ {:sfw, _} -> {:sfw, object}
+ {:nsfw, _} -> {:nsfw, object}
+ end
+ end
+
+ def check_object_nsfw(object), do: {:sfw, object}
+
+ @impl true
+ def filter(object) do
+ with {:sfw, object} <- check_object_nsfw(object) do
+ {:ok, object}
+ else
+ {:nsfw, _data} -> handle_nsfw(object)
+ end
+ end
+
+ defp handle_nsfw(object) do
+ if Config.get([@policy, :reject]) do
+ {:reject, object}
+ else
+ {:ok,
+ object
+ |> maybe_unlist()
+ |> maybe_mark_sensitive()}
+ end
+ end
+
+ defp maybe_unlist(object) do
+ if Config.get([@policy, :unlist]) do
+ unlist(object)
+ else
+ object
+ end
+ end
+
+ defp maybe_mark_sensitive(object) do
+ if Config.get([@policy, :mark_sensitive]) do
+ mark_sensitive(object)
+ else
+ object
+ end
+ end
+
+ def unlist(%{"to" => to, "cc" => cc, "actor" => actor} = object) do
+ with %User{} = user <- User.get_cached_by_ap_id(actor) do
+ to =
+ [user.follower_address | to]
+ |> List.delete(Constants.as_public())
+ |> Enum.uniq()
+
+ cc =
+ [Constants.as_public() | cc]
+ |> List.delete(user.follower_address)
+ |> Enum.uniq()
+
+ object
+ |> Map.put("to", to)
+ |> Map.put("cc", cc)
+ else
+ _ -> raise "[NsfwApiPolicy]: Could not find user #{actor}"
+ end
+ end
+
+ def mark_sensitive(%{"object" => child_object} = object) when is_map(child_object) do
+ Map.put(object, "object", mark_sensitive(child_object))
+ end
+
+ def mark_sensitive(object) when is_map(object) do
+ tags = (object["tag"] || []) ++ ["nsfw"]
+
+ object
+ |> Map.put("tag", tags)
+ |> Map.put("sensitive", true)
+ end
+
+ # Hackney needs a trailing slash
+ defp fix_path(%URI{path: path} = uri) when is_binary(path) do
+ path = String.trim_trailing(path, "/") <> "/"
+ Map.put(uri, :path, path)
+ end
+
+ defp fix_path(%URI{path: nil} = uri), do: Map.put(uri, :path, "/")
+
+ @impl true
+ def describe do
+ options = %{
+ threshold: Config.get([@policy, :threshold]),
+ mark_sensitive: Config.get([@policy, :mark_sensitive]),
+ unlist: Config.get([@policy, :unlist]),
+ reject: Config.get([@policy, :reject])
+ }
+
+ {:ok, %{@policy => options}}
+ end
+
+ @impl true
+ def config_description do
+ %{
+ key: @policy,
+ related_policy: to_string(__MODULE__),
+ label: "NSFW API Policy",
+ description:
+ "Hide, delete, or mark sensitive NSFW content with artificial intelligence. Requires running an external API server.",
+ children: [
+ %{
+ key: :url,
+ type: :string,
+ description: "Base URL of the API server.",
+ suggestions: ["http://127.0.0.1:5000/"]
+ },
+ %{
+ key: :threshold,
+ type: :float,
+ description: "Lowest score to take action on. Between 0 and 1.",
+ suggestions: [0.7]
+ },
+ %{
+ key: :mark_sensitive,
+ type: :boolean,
+ description: "Mark sensitive all detected NSFW content?",
+ suggestions: [true]
+ },
+ %{
+ key: :unlist,
+ type: :boolean,
+ description: "Unlist sensitive all detected NSFW content?",
+ suggestions: [false]
+ },
+ %{
+ key: :reject,
+ type: :boolean,
+ description: "Reject sensitive all detected NSFW content (takes precedence)?",
+ suggestions: [false]
+ }
+ ]
+ }
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/mrf/policy.ex b/lib/pleroma/web/activity_pub/mrf/policy.ex
index 0234de4d5..1f34883e7 100644
--- a/lib/pleroma/web/activity_pub/mrf/policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/policy.ex
@@ -3,8 +3,8 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.Policy do
- @callback filter(Map.t()) :: {:ok | :reject, Map.t()}
- @callback describe() :: {:ok | :error, Map.t()}
+ @callback filter(map()) :: {:ok | :reject, map()}
+ @callback describe() :: {:ok | :error, map()}
@callback config_description() :: %{
optional(:children) => [map()],
key: atom(),
diff --git a/lib/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy.ex b/lib/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy.ex
index f1c573d1b..ac353f03f 100644
--- a/lib/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy.ex
@@ -28,7 +28,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.QuoteToLinkTagPolicy do
tags = object["tag"] || []
if Enum.any?(tags, fn tag ->
- CommonFixes.is_object_link_tag(tag) and tag["href"] == quote_url
+ CommonFixes.object_link_tag?(tag) and tag["href"] == quote_url
end) do
object
else
diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
index 829ddeaea..d708c99eb 100644
--- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
@@ -220,9 +220,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
{:ok, object} <- check_object(object) do
{:ok, object}
else
- {:reject, nil} -> {:reject, "[SimplePolicy]"}
{:reject, _} = e -> e
- _ -> {:reject, "[SimplePolicy]"}
end
end
@@ -236,9 +234,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
{:ok, object} <- check_banner_removal(actor_info, object) do
{:ok, object}
else
- {:reject, nil} -> {:reject, "[SimplePolicy]"}
{:reject, _} = e -> e
- _ -> {:reject, "[SimplePolicy]"}
end
end
@@ -249,9 +245,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
{:ok, object} <- check_reject(uri, object) do
{:ok, object}
else
- {:reject, nil} -> {:reject, "[SimplePolicy]"}
{:reject, _} = e -> e
- _ -> {:reject, "[SimplePolicy]"}
end
end
diff --git a/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex b/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex
index 12accfadd..fa6b595ea 100644
--- a/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex
@@ -34,15 +34,17 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
|> Path.basename()
|> Path.extname()
+ extension = if extension == "", do: ".png", else: extension
+
shortcode = Path.basename(shortcode)
- file_path = Path.join(emoji_dir_path, shortcode <> (extension || ".png"))
+ file_path = Path.join(emoji_dir_path, shortcode <> extension)
case File.write(file_path, response.body) do
:ok ->
shortcode
e ->
- Logger.warn("MRF.StealEmojiPolicy: Failed to write to #{file_path}: #{inspect(e)}")
+ Logger.warning("MRF.StealEmojiPolicy: Failed to write to #{file_path}: #{inspect(e)}")
nil
end
else
@@ -54,7 +56,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
end
else
e ->
- Logger.warn("MRF.StealEmojiPolicy: Failed to fetch #{url}: #{inspect(e)}")
+ Logger.warning("MRF.StealEmojiPolicy: Failed to fetch #{url}: #{inspect(e)}")
nil
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 d9deff35f..1c114558e 100644
--- a/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex
@@ -31,7 +31,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicy do
{:reject, _} = e -> e
{:accepted, _} -> {:reject, "[VocabularyPolicy] #{message_type} not in accept list"}
{:rejected, _} -> {:reject, "[VocabularyPolicy] #{message_type} in reject list"}
- _ -> {:reject, "[VocabularyPolicy]"}
end
end
diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex
index 5e0d1aa8e..b3043b93a 100644
--- a/lib/pleroma/web/activity_pub/object_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validator.ex
@@ -173,6 +173,9 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
{:object_validation, e} ->
e
+
+ {:error, %Ecto.Changeset{} = e} ->
+ {:error, e}
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 c2c7ba1a8..d0218583e 100644
--- a/lib/pleroma/web/activity_pub/object_validators/announce_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/announce_validator.ex
@@ -82,7 +82,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator do
object when is_binary(object) <- get_field(cng, :object),
%User{} = actor <- User.get_cached_by_ap_id(actor),
%Object{} = object <- Object.get_cached_by_ap_id(object),
- false <- Visibility.is_public?(object) do
+ false <- Visibility.public?(object) do
same_actor = object.data["actor"] == actor.ap_id
recipients = get_field(cng, :to) ++ get_field(cng, :cc)
local_public = Utils.as_local_public()
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 398020bff..5ee9e7549 100644
--- a/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex
@@ -12,13 +12,14 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do
@primary_key false
embedded_schema do
field(:id, :string)
- field(:type, :string)
+ field(:type, :string, default: "Link")
field(:mediaType, ObjectValidators.MIME, default: "application/octet-stream")
field(:name, :string)
+ field(:summary, :string)
field(:blurhash, :string)
embeds_many :url, UrlObjectValidator, primary_key: false do
- field(:type, :string)
+ field(:type, :string, default: "Link")
field(:href, ObjectValidators.Uri)
field(:mediaType, ObjectValidators.MIME, default: "application/octet-stream")
field(:width, :integer)
@@ -44,7 +45,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do
|> fix_url()
struct
- |> cast(data, [:id, :type, :mediaType, :name, :blurhash])
+ |> cast(data, [:id, :type, :mediaType, :name, :summary, :blurhash])
|> cast_embed(:url, with: &url_changeset/2, required: true)
|> validate_inclusion(:type, ~w[Link Document Audio Image Video])
|> validate_required([:type, :mediaType])
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 efae48cae..09e25be89 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
@@ -57,6 +57,11 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator do
|> Map.put("attachment", attachment)
end
+ def fix_attachment(%{"attachment" => attachment} = data) when attachment == [] do
+ data
+ |> Map.drop(["attachment"])
+ end
+
def fix_attachment(data), do: data
def changeset(struct, data) do
diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex
index 835ed97b7..1a5d02601 100644
--- a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex
@@ -57,6 +57,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields do
field(:replies_count, :integer, default: 0)
field(:like_count, :integer, default: 0)
field(:announcement_count, :integer, default: 0)
+ field(:quotes_count, :integer, default: 0)
field(:inReplyTo, ObjectValidators.ObjectID)
field(:quoteUrl, ObjectValidators.ObjectID)
field(:url, ObjectValidators.BareUri)
diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex
index 4d9be0bdd..4699029d4 100644
--- a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex
@@ -4,6 +4,7 @@
defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
alias Pleroma.EctoType.ActivityPub.ObjectValidators
+ alias Pleroma.Maps
alias Pleroma.Object
alias Pleroma.Object.Containment
alias Pleroma.User
@@ -24,6 +25,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
end
def fix_object_defaults(data) do
+ data = Maps.filter_empty_values(data)
+
context =
Utils.maybe_create_context(
data["context"] || data["conversation"] || data["inReplyTo"] || data["id"]
@@ -99,7 +102,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
end
def fix_quote_url(%{"tag" => [_ | _] = tags} = data) do
- tag = Enum.find(tags, &is_object_link_tag/1)
+ tag = Enum.find(tags, &object_link_tag?/1)
if not is_nil(tag) do
data
@@ -112,7 +115,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
def fix_quote_url(data), do: data
# https://codeberg.org/fediverse/fep/src/branch/main/fep/e232/fep-e232.md
- def is_object_link_tag(%{
+ def object_link_tag?(%{
"type" => "Link",
"mediaType" => media_type,
"href" => href
@@ -121,5 +124,5 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
true
end
- def is_object_link_tag(_), do: false
+ def object_link_tag?(_), do: false
end
diff --git a/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex b/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex
index a0b82b325..65ba047e6 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
@@ -74,10 +74,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
new_emoji = Pleroma.Emoji.fully_qualify_emoji(emoji)
cond do
- Pleroma.Emoji.is_unicode_emoji?(emoji) ->
+ Pleroma.Emoji.unicode?(emoji) ->
data
- Pleroma.Emoji.is_unicode_emoji?(new_emoji) ->
+ Pleroma.Emoji.unicode?(new_emoji) ->
data |> Map.put("content", new_emoji)
true ->
@@ -90,7 +90,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
defp validate_emoji(cng) do
content = get_field(cng, :content)
- if Emoji.is_unicode_emoji?(content) || Emoji.is_custom_emoji?(content) do
+ if Emoji.unicode?(content) || Emoji.custom?(content) do
cng
else
cng
@@ -101,7 +101,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
defp maybe_validate_tag_presence(cng) do
content = get_field(cng, :content)
- if Emoji.is_unicode_emoji?(content) do
+ if Emoji.unicode?(content) do
cng
else
tag = get_field(cng, :tag)
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
index 541945fa4..8d7f7b9fa 100644
--- a/lib/pleroma/web/activity_pub/object_validators/question_options_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/question_options_validator.ex
@@ -14,10 +14,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionOptionsValidator do
embeds_one :replies, Replies, primary_key: false do
field(:totalItems, :integer)
- field(:type, :string)
+ field(:type, :string, default: "Collection")
end
- field(:type, :string)
+ field(:type, :string, default: "Note")
end
def changeset(struct, data) do
diff --git a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex
index 621085e6c..7f9d4d648 100644
--- a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex
@@ -29,6 +29,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do
field(:closed, ObjectValidators.DateTime)
field(:voters, {:array, ObjectValidators.ObjectID}, default: [])
+ field(:nonAnonymous, :boolean)
embeds_many(:anyOf, QuestionOptionsValidator)
embeds_many(:oneOf, QuestionOptionsValidator)
end
diff --git a/lib/pleroma/web/activity_pub/pipeline.ex b/lib/pleroma/web/activity_pub/pipeline.ex
index ca8653ab1..7f11a4d67 100644
--- a/lib/pleroma/web/activity_pub/pipeline.ex
+++ b/lib/pleroma/web/activity_pub/pipeline.ex
@@ -23,7 +23,7 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do
defp config, do: Config.get([:pipeline, :config], Config)
@spec common_pipeline(map(), keyword()) ::
- {:ok, Activity.t() | Object.t(), keyword()} | {:error, any()}
+ {:ok, Activity.t() | Object.t(), keyword()} | {:error | :reject, any()}
def common_pipeline(object, meta) do
case Repo.transaction(fn -> do_common_pipeline(object, meta) end, Utils.query_timeout()) do
{:ok, {:ok, activity, meta}} ->
@@ -62,7 +62,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 and local and not Visibility.is_local_public?(activity) do
+ if !do_not_federate and local and not Visibility.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 af6aa0781..e040753dc 100644
--- a/lib/pleroma/web/activity_pub/publisher.ex
+++ b/lib/pleroma/web/activity_pub/publisher.ex
@@ -13,13 +13,12 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Transmogrifier
+ alias Pleroma.Workers.PublisherWorker
require Pleroma.Constants
import Pleroma.Web.ActivityPub.Visibility
- @behaviour Pleroma.Web.Federator.Publisher
-
require Logger
@moduledoc """
@@ -27,9 +26,47 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
"""
@doc """
+ Enqueue publishing a single activity.
+ """
+ @spec enqueue_one(map(), Keyword.t()) :: {:ok, %Oban.Job{}}
+ def enqueue_one(%{} = params, worker_args \\ []) do
+ PublisherWorker.enqueue(
+ "publish_one",
+ %{"params" => params},
+ worker_args
+ )
+ end
+
+ @doc """
+ Gathers a set of remote users given an IR envelope.
+ """
+ def remote_users(%User{id: user_id}, %{data: %{"to" => to} = data}) do
+ cc = Map.get(data, "cc", [])
+
+ bcc =
+ data
+ |> Map.get("bcc", [])
+ |> Enum.reduce([], fn ap_id, bcc ->
+ case Pleroma.List.get_by_ap_id(ap_id) do
+ %Pleroma.List{user_id: ^user_id} = list ->
+ {:ok, following} = Pleroma.List.get_following(list)
+ bcc ++ Enum.map(following, & &1.ap_id)
+
+ _ ->
+ bcc
+ end
+ end)
+
+ [to, cc, bcc]
+ |> Enum.concat()
+ |> Enum.map(&User.get_cached_by_ap_id/1)
+ |> Enum.filter(fn user -> user && !user.local end)
+ end
+
+ @doc """
Determine if an activity can be represented by running it through Transmogrifier.
"""
- def is_representable?(%Activity{} = activity) do
+ def representable?(%Activity{} = activity) do
with {:ok, _data} <- Transmogrifier.prepare_outgoing(activity.data) do
true
else
@@ -43,13 +80,26 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
parameters set:
* `inbox`: the inbox to publish to
- * `json`: the JSON message body representing the ActivityPub message
- * `actor`: the actor which is signing the message
- * `id`: the ActivityStreams URI of the message
+ * `activity_id`: the internal activity id
+ * `cc`: the cc recipients relevant to this inbox (optional)
"""
- def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = params) do
- Logger.debug("Federating #{id} to #{inbox}")
+ def publish_one(%{inbox: inbox, activity_id: activity_id} = params) do
+ activity = Activity.get_by_id_with_user_actor(activity_id)
+ actor = activity.user_actor
+
+ ap_id = activity.data["id"]
+ Logger.debug("Federating #{ap_id} to #{inbox}")
uri = %{path: path} = URI.parse(inbox)
+
+ {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
+
+ cc = Map.get(params, :cc)
+
+ json =
+ data
+ |> Map.put("cc", cc)
+ |> Jason.encode!()
+
digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
date = Pleroma.Signature.signed_date()
@@ -80,21 +130,31 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
result
else
- {_post_result, response} ->
+ {_post_result, %{status: code} = response} = e ->
+ unless params[:unreachable_since], do: Instances.set_unreachable(inbox)
+ Logger.metadata(activity: activity_id, inbox: inbox, status: code)
+ Logger.error("Publisher failed to inbox #{inbox} with status #{code}")
+
+ case response do
+ %{status: 400} -> {:cancel, :bad_request}
+ %{status: 403} -> {:cancel, :forbidden}
+ %{status: 404} -> {:cancel, :not_found}
+ %{status: 410} -> {:cancel, :not_found}
+ _ -> {:error, e}
+ end
+
+ {:error, :pool_full} ->
+ Logger.debug("Publisher snoozing worker job due to full connection pool")
+ {:snooze, 30}
+
+ e ->
unless params[:unreachable_since], do: Instances.set_unreachable(inbox)
- {:error, response}
+ Logger.metadata(activity: activity_id, inbox: inbox)
+ Logger.error("Publisher failed to inbox #{inbox} #{inspect(e)}")
+ {:error, e}
end
end
- def publish_one(%{actor_id: actor_id} = params) do
- actor = User.get_cached_by_id(actor_id)
-
- params
- |> Map.delete(:actor_id)
- |> Map.put(:actor, actor)
- |> publish_one()
- end
-
defp signature_host(%URI{port: port, scheme: scheme, host: host}) do
if port == URI.default_port(scheme) do
host
@@ -103,22 +163,21 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
end
end
- defp should_federate?(inbox, public) do
- if public do
- true
- else
- %{host: host} = URI.parse(inbox)
+ def should_federate?(nil, _), do: false
+ def should_federate?(_, true), do: true
- quarantined_instances =
- Config.get([:instance, :quarantined_instances], [])
- |> Pleroma.Web.ActivityPub.MRF.instance_list_from_tuples()
- |> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
+ def should_federate?(inbox, _) do
+ %{host: host} = URI.parse(inbox)
- !Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host)
- end
+ quarantined_instances =
+ Config.get([:instance, :quarantined_instances], [])
+ |> Pleroma.Web.ActivityPub.MRF.instance_list_from_tuples()
+ |> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
+
+ !Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host)
end
- @spec recipients(User.t(), Activity.t()) :: list(User.t()) | []
+ @spec recipients(User.t(), Activity.t()) :: [[User.t()]]
defp recipients(actor, activity) do
followers =
if actor.follower_address in activity.recipients do
@@ -138,7 +197,10 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
[]
end
- Pleroma.Web.Federator.Publisher.remote_users(actor, activity) ++ followers ++ fetchers
+ mentioned = remote_users(actor, activity)
+ non_mentioned = (followers ++ fetchers) -- mentioned
+
+ [mentioned, non_mentioned]
end
defp get_cc_ap_ids(ap_id, recipients) do
@@ -192,72 +254,81 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
def publish(%User{} = actor, %{data: %{"bcc" => bcc}} = activity)
when is_list(bcc) and bcc != [] do
- public = is_public?(activity)
- {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
+ public = public?(activity)
- recipients = recipients(actor, activity)
+ [priority_recipients, recipients] = recipients(actor, activity)
inboxes =
- recipients
- |> Enum.map(fn actor -> actor.inbox end)
- |> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
- |> Instances.filter_reachable()
+ [priority_recipients, recipients]
+ |> Enum.map(fn recipients ->
+ recipients
+ |> Enum.map(fn %User{} = user ->
+ determine_inbox(activity, user)
+ end)
+ |> Enum.uniq()
+ |> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
+ |> Instances.filter_reachable()
+ end)
Repo.checkout(fn ->
- Enum.each(inboxes, fn {inbox, unreachable_since} ->
- %User{ap_id: ap_id} = Enum.find(recipients, fn actor -> actor.inbox == inbox end)
-
- # Get all the recipients on the same host and add them to cc. Otherwise, a remote
- # instance would only accept a first message for the first recipient and ignore the rest.
- cc = get_cc_ap_ids(ap_id, recipients)
-
- json =
- data
- |> Map.put("cc", cc)
- |> Jason.encode!()
-
- Pleroma.Web.Federator.Publisher.enqueue_one(__MODULE__, %{
- inbox: inbox,
- json: json,
- actor_id: actor.id,
- id: activity.data["id"],
- unreachable_since: unreachable_since
- })
+ Enum.each(inboxes, fn inboxes ->
+ Enum.each(inboxes, fn {inbox, unreachable_since} ->
+ %User{ap_id: ap_id} = Enum.find(recipients, fn actor -> actor.inbox == inbox end)
+
+ # Get all the recipients on the same host and add them to cc. Otherwise, a remote
+ # instance would only accept a first message for the first recipient and ignore the rest.
+ cc = get_cc_ap_ids(ap_id, recipients)
+
+ __MODULE__.enqueue_one(%{
+ inbox: inbox,
+ cc: cc,
+ activity_id: activity.id,
+ unreachable_since: unreachable_since
+ })
+ end)
end)
end)
end
# Publishes an activity to all relevant peers.
def publish(%User{} = actor, %Activity{} = activity) do
- public = is_public?(activity)
+ public = public?(activity)
if public && Config.get([:instance, :allow_relay]) do
Logger.debug(fn -> "Relaying #{activity.data["id"]} out" end)
Relay.publish(activity)
end
- {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
- json = Jason.encode!(data)
+ [priority_inboxes, inboxes] =
+ recipients(actor, activity)
+ |> Enum.map(fn recipients ->
+ recipients
+ |> Enum.map(fn %User{} = user ->
+ determine_inbox(activity, user)
+ end)
+ |> Enum.uniq()
+ |> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
+ end)
- recipients(actor, activity)
- |> Enum.map(fn %User{} = user ->
- determine_inbox(activity, user)
- end)
- |> Enum.uniq()
- |> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
- |> Instances.filter_reachable()
- |> Enum.each(fn {inbox, unreachable_since} ->
- Pleroma.Web.Federator.Publisher.enqueue_one(
- __MODULE__,
- %{
- inbox: inbox,
- json: json,
- actor_id: actor.id,
- id: activity.data["id"],
- unreachable_since: unreachable_since
- }
- )
+ inboxes = inboxes -- priority_inboxes
+
+ [{priority_inboxes, 0}, {inboxes, 1}]
+ |> Enum.each(fn {inboxes, priority} ->
+ inboxes
+ |> Instances.filter_reachable()
+ |> Enum.each(fn {inbox, unreachable_since} ->
+ __MODULE__.enqueue_one(
+ %{
+ inbox: inbox,
+ activity_id: activity.id,
+ unreachable_since: unreachable_since
+ },
+ priority: priority
+ )
+ end)
end)
+
+ :ok
end
def gather_webfinger_links(%User{} = user) do
diff --git a/lib/pleroma/web/activity_pub/relay.ex b/lib/pleroma/web/activity_pub/relay.ex
index 2010351d1..cff234940 100644
--- a/lib/pleroma/web/activity_pub/relay.ex
+++ b/lib/pleroma/web/activity_pub/relay.ex
@@ -22,7 +22,7 @@ defmodule Pleroma.Web.ActivityPub.Relay do
def follow(target_instance) do
with %User{} = local_user <- get_actor(),
{:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_instance),
- {:ok, _, _, activity} <- CommonAPI.follow(local_user, target_user) do
+ {:ok, _, _, activity} <- CommonAPI.follow(target_user, local_user) do
Logger.info("relay: followed instance: #{target_instance}; id=#{activity.data["id"]}")
{:ok, activity}
else
@@ -58,7 +58,7 @@ defmodule Pleroma.Web.ActivityPub.Relay do
@spec publish(any()) :: {:ok, Activity.t()} | {:error, any()}
def publish(%Activity{data: %{"type" => "Create"}} = activity) do
with %User{} = user <- get_actor(),
- true <- Visibility.is_public?(activity) do
+ true <- Visibility.public?(activity) do
CommonAPI.repeat(activity.id, user)
else
error -> format_error(error)
diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex
index 098c177c7..cc1c7a0af 100644
--- a/lib/pleroma/web/activity_pub/side_effects.ex
+++ b/lib/pleroma/web/activity_pub/side_effects.ex
@@ -21,7 +21,6 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils
- alias Pleroma.Web.Push
alias Pleroma.Web.Streamer
alias Pleroma.Workers.PollWorker
@@ -125,7 +124,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
nil
end
- {:ok, notifications} = Notification.create_notifications(object, do_send: false)
+ {:ok, notifications} = Notification.create_notifications(object)
meta =
meta
@@ -184,7 +183,11 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
liked_object = Object.get_by_ap_id(object.data["object"])
Utils.add_like_to_object(object, liked_object)
- Notification.create_notifications(object)
+ {:ok, notifications} = Notification.create_notifications(object)
+
+ meta =
+ meta
+ |> add_notifications(notifications)
{:ok, object, meta}
end
@@ -197,11 +200,12 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
# - Increase replies count
# - Set up ActivityExpiration
# - Set up notifications
+ # - Index incoming posts for search (if needed)
@impl true
def handle(%{data: %{"type" => "Create"}} = activity, meta) do
with {:ok, object, meta} <- handle_object_creation(meta[:object_data], activity, meta),
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
- {:ok, notifications} = Notification.create_notifications(activity, do_send: false)
+ {:ok, notifications} = Notification.create_notifications(activity)
{:ok, _user} = ActivityPub.increase_note_count_if_public(user, object)
{:ok, _user} = ActivityPub.update_last_status_at_if_public(user, object)
@@ -209,6 +213,10 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
Object.increase_replies_count(in_reply_to)
end
+ if quote_url = object.data["quoteUrl"] do
+ Object.increase_quotes_count(quote_url)
+ end
+
reply_depth = (meta[:depth] || 0) + 1
# FIXME: Force inReplyTo to replies
@@ -222,9 +230,11 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
end
end
- ConcurrentLimiter.limit(Pleroma.Web.RichMedia.Helpers, fn ->
- Task.start(fn -> Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) end)
- end)
+ Pleroma.Web.RichMedia.Card.get_by_activity(activity)
+
+ Pleroma.Search.add_to_index(Map.put(activity, :object, object))
+
+ Utils.maybe_handle_group_posts(activity)
meta =
meta
@@ -249,11 +259,13 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
Utils.add_announce_to_object(object, announced_object)
- if !User.is_internal_user?(user) do
- Notification.create_notifications(object)
+ {:ok, notifications} = Notification.create_notifications(object)
- ap_streamer().stream_out(object)
- end
+ if !User.internal?(user), do: ap_streamer().stream_out(object)
+
+ meta =
+ meta
+ |> add_notifications(notifications)
{:ok, object, meta}
end
@@ -274,7 +286,11 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
reacted_object = Object.get_by_ap_id(object.data["object"])
Utils.add_emoji_reaction_to_object(object, reacted_object)
- Notification.create_notifications(object)
+ {:ok, notifications} = Notification.create_notifications(object)
+
+ meta =
+ meta
+ |> add_notifications(notifications)
{:ok, object, meta}
end
@@ -285,6 +301,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
# - Reduce the user note count
# - Reduce the reply count
# - Stream out the activity
+ # - Removes posts from search index (if needed)
@impl true
def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object, meta) do
deleted_object =
@@ -294,9 +311,9 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
result =
case deleted_object do
%Object{} ->
- with {:ok, deleted_object, _activity} <- Object.delete(deleted_object),
+ with {_, {:ok, deleted_object, _activity}} <- {:object, Object.delete(deleted_object)},
{_, actor} when is_binary(actor) <- {:actor, deleted_object.data["actor"]},
- %User{} = user <- User.get_cached_by_ap_id(actor) do
+ {_, %User{} = user} <- {:user, User.get_cached_by_ap_id(actor)} do
User.remove_pinned_object_id(user, deleted_object.data["id"])
{:ok, user} = ActivityPub.decrease_note_count_if_public(user, deleted_object)
@@ -305,6 +322,10 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
Object.decrease_replies_count(in_reply_to)
end
+ if quote_url = deleted_object.data["quoteUrl"] do
+ Object.decrease_quotes_count(quote_url)
+ end
+
MessageReference.delete_for_object(deleted_object)
ap_streamer().stream_out(object)
@@ -314,6 +335,17 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
{:actor, _} ->
@logger.error("The object doesn't have an actor: #{inspect(deleted_object)}")
:no_object_actor
+
+ {:user, _} ->
+ @logger.error(
+ "The object's actor could not be resolved to a user: #{inspect(deleted_object)}"
+ )
+
+ :no_object_user
+
+ {:object, _} ->
+ @logger.error("The object could not be deleted: #{inspect(deleted_object)}")
+ {:error, object}
end
%User{} ->
@@ -323,6 +355,11 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
end
if result == :ok do
+ # Only remove from index when deleting actual objects, not users or anything else
+ with %Pleroma.Object{} <- deleted_object do
+ Pleroma.Search.remove_from_index(deleted_object)
+ end
+
{:ok, object, meta}
else
{:error, result}
@@ -416,7 +453,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
) do
orig_object_ap_id = updated_object["id"]
orig_object = Object.get_by_ap_id(orig_object_ap_id)
- orig_object_data = orig_object.data
+ orig_object_data = Map.get(orig_object, :data)
updated_object =
if meta[:local] do
@@ -550,17 +587,14 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
def handle_undoing(object), do: {:error, ["don't know how to handle", object]}
- @spec delete_object(Object.t()) :: :ok | {:error, Ecto.Changeset.t()}
+ @spec delete_object(Activity.t()) :: :ok | {:error, Ecto.Changeset.t()}
defp delete_object(object) do
with {:ok, _} <- Repo.delete(object), do: :ok
end
- defp send_notifications(meta) do
+ defp stream_notifications(meta) do
Keyword.get(meta, :notifications, [])
- |> Enum.each(fn notification ->
- Streamer.stream(["user", "user:notification"], notification)
- Push.send(notification)
- end)
+ |> Notification.stream()
meta
end
@@ -591,7 +625,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
@impl true
def handle_after_transaction(meta) do
meta
- |> send_notifications()
+ |> stream_notifications()
|> send_streamables()
end
end
diff --git a/lib/pleroma/web/activity_pub/side_effects/handling.ex b/lib/pleroma/web/activity_pub/side_effects/handling.ex
index eb012f576..4751bb4ce 100644
--- a/lib/pleroma/web/activity_pub/side_effects/handling.ex
+++ b/lib/pleroma/web/activity_pub/side_effects/handling.ex
@@ -4,5 +4,5 @@
defmodule Pleroma.Web.ActivityPub.SideEffects.Handling do
@callback handle(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
- @callback handle_after_transaction(map()) :: map()
+ @callback handle_after_transaction(keyword()) :: keyword()
end
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 86d3ac60f..2f8a7f8f2 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -23,7 +23,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
import Ecto.Query
- require Logger
require Pleroma.Constants
@doc """
@@ -155,8 +154,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> Map.put("context", replied_object.data["context"] || object["conversation"])
|> Map.drop(["conversation", "inReplyToAtomUri"])
else
- e ->
- Logger.warn("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
+ _ ->
object
end
else
@@ -181,8 +179,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
{:quoting?, _} ->
object
- e ->
- Logger.warn("Couldn't fetch #{inspect(quote_url)}, error: #{inspect(e)}")
+ _ ->
object
end
end
@@ -533,6 +530,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
else
_ -> e
end
+
+ e ->
+ {:error, e}
end
end
@@ -782,7 +782,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> Object.normalize(fetch: false)
data =
- if Visibility.is_private?(object) && object.data["actor"] == ap_id do
+ if Visibility.private?(object) && object.data["actor"] == ap_id do
data |> Map.put("object", object |> Map.get(:data) |> prepare_object)
else
data |> maybe_fix_object_url
@@ -852,8 +852,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
relative_object do
Map.put(data, "object", external_url)
else
- {:fetch, e} ->
- Logger.error("Couldn't fetch #{object} #{inspect(e)}")
+ {:fetch, _} ->
data
_ ->
@@ -917,9 +916,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def add_emoji_tags(object), do: object
- defp build_emoji_tag({name, url}) do
+ def build_emoji_tag({name, url}) do
+ url = URI.encode(url)
+
%{
- "icon" => %{"url" => "#{URI.encode(url)}", "type" => "Image"},
+ "icon" => %{"url" => "#{url}", "type" => "Image"},
"name" => ":" <> name <> ":",
"type" => "Emoji",
"updated" => "1970-01-01T00:00:00Z",
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index 437220077..6c792804d 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
alias Ecto.UUID
alias Pleroma.Activity
alias Pleroma.Config
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators.ObjectID
alias Pleroma.Maps
alias Pleroma.Notification
alias Pleroma.Object
@@ -166,7 +167,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
with true <- Config.get!([:instance, :federating]),
true <- type != "Block" || outgoing_blocks,
- false <- Visibility.is_local_public?(activity) do
+ false <- Visibility.local_public?(activity) do
Pleroma.Web.Federator.publish(activity)
end
@@ -276,7 +277,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
object_actor = User.get_cached_by_ap_id(object_actor_id)
to =
- if Visibility.is_public?(object) do
+ if Visibility.public?(object) do
[actor.follower_address, object.data["actor"]]
else
[object.data["actor"]]
@@ -720,14 +721,18 @@ defmodule Pleroma.Web.ActivityPub.Utils do
#### Flag-related helpers
@spec make_flag_data(map(), map()) :: map()
- def make_flag_data(%{actor: actor, context: context, content: content} = params, additional) do
+ def make_flag_data(
+ %{actor: actor, context: context, content: content} = params,
+ additional
+ ) do
%{
"type" => "Flag",
"actor" => actor.ap_id,
"content" => content,
"object" => build_flag_object(params),
"context" => context,
- "state" => "open"
+ "state" => "open",
+ "rules" => Map.get(params, :rules, nil)
}
|> Map.merge(additional)
end
@@ -775,10 +780,9 @@ defmodule Pleroma.Web.ActivityPub.Utils do
build_flag_object(object)
nil ->
- if %Object{} = object = Object.get_by_ap_id(id) do
- build_flag_object(object)
- else
- %{"id" => id, "deleted" => true}
+ case Object.get_by_ap_id(id) do
+ %Object{} = object -> build_flag_object(object)
+ _ -> %{"id" => id, "deleted" => true}
end
end
end
@@ -852,9 +856,11 @@ defmodule Pleroma.Web.ActivityPub.Utils do
[actor | reported_activities] = activity.data["object"]
stripped_activities =
- Enum.map(reported_activities, fn
- act when is_map(act) -> act["id"]
- act when is_binary(act) -> act
+ Enum.reduce(reported_activities, [], fn act, acc ->
+ case ObjectID.cast(act) do
+ {:ok, act} -> [act | acc]
+ _ -> acc
+ end
end)
new_data = put_in(activity.data, ["object"], [actor | stripped_activities])
@@ -932,4 +938,15 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data))
|> Repo.all()
end
+
+ @spec maybe_handle_group_posts(Activity.t()) :: :ok
+ @doc "Automatically repeats posts for local group actor recipients"
+ def maybe_handle_group_posts(activity) do
+ poster = User.get_cached_by_ap_id(activity.actor)
+
+ User.get_recipients_from_activity(activity)
+ |> Enum.filter(&match?("Group", &1.actor_type))
+ |> Enum.reject(&User.blocks?(&1, poster))
+ |> Enum.each(&Pleroma.Web.CommonAPI.repeat(activity.id, &1))
+ 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 f69fca075..937e4fd67 100644
--- a/lib/pleroma/web/activity_pub/views/user_view.ex
+++ b/lib/pleroma/web/activity_pub/views/user_view.ex
@@ -46,6 +46,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
"following" => "#{user.ap_id}/following",
"followers" => "#{user.ap_id}/followers",
"inbox" => "#{user.ap_id}/inbox",
+ "outbox" => "#{user.ap_id}/outbox",
"name" => "Pleroma",
"summary" =>
"An internal service actor for this Pleroma instance. No user-serviceable parts inside.",
@@ -66,8 +67,13 @@ defmodule Pleroma.Web.ActivityPub.UserView do
def render("user.json", %{user: %User{nickname: nil} = user}),
do: render("service.json", %{user: user})
- def render("user.json", %{user: %User{nickname: "internal." <> _} = user}),
- do: render("service.json", %{user: user}) |> Map.put("preferredUsername", user.nickname)
+ def render("user.json", %{user: %User{nickname: "internal." <> _} = user}) do
+ render("service.json", %{user: user})
+ |> Map.merge(%{
+ "preferredUsername" => user.nickname,
+ "webfinger" => "acct:#{User.full_nickname(user)}"
+ })
+ end
def render("user.json", %{user: user}) do
{:ok, _, public_key} = Keys.keys_from_pem(user.keys)
@@ -120,7 +126,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do
"discoverable" => user.is_discoverable,
"capabilities" => capabilities,
"alsoKnownAs" => user.also_known_as,
- "vcard:bday" => birthday
+ "vcard:bday" => birthday,
+ "webfinger" => "acct:#{User.full_nickname(user)}"
}
|> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))
|> Map.merge(maybe_make_image(&User.banner_url/2, "image", user))
diff --git a/lib/pleroma/web/activity_pub/visibility.ex b/lib/pleroma/web/activity_pub/visibility.ex
index 7c57f88f9..97fc7fa1b 100644
--- a/lib/pleroma/web/activity_pub/visibility.ex
+++ b/lib/pleroma/web/activity_pub/visibility.ex
@@ -11,28 +11,28 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
require Pleroma.Constants
- @spec is_public?(Object.t() | Activity.t() | map()) :: boolean()
- def is_public?(%Object{data: %{"type" => "Tombstone"}}), do: false
- def is_public?(%Object{data: data}), do: is_public?(data)
- 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
+ @spec public?(Object.t() | Activity.t() | map()) :: boolean()
+ def public?(%Object{data: %{"type" => "Tombstone"}}), do: false
+ def public?(%Object{data: data}), do: public?(data)
+ def public?(%Activity{data: %{"type" => "Move"}}), do: true
+ def public?(%Activity{data: data}), do: public?(data)
+ def public?(%{"directMessage" => true}), do: false
+
+ def public?(data) do
Utils.label_in_message?(Pleroma.Constants.as_public(), data) or
Utils.label_in_message?(Utils.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 local_public?(%Object{data: data}), do: local_public?(data)
+ def local_public?(%Activity{data: data}), do: local_public?(data)
- def is_local_public?(data) do
+ def local_public?(data) do
Utils.label_in_message?(Utils.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),
+ def private?(activity) do
+ with false <- public?(activity),
%User{follower_address: follower_address} <-
User.get_cached_by_ap_id(activity.data["actor"]) do
follower_address in activity.data["to"]
@@ -41,20 +41,20 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
end
end
- def is_announceable?(activity, user, public \\ true) do
- is_public?(activity) ||
- (!public && is_private?(activity) && activity.data["actor"] == user.ap_id)
+ def announceable?(activity, user, public \\ true) do
+ public?(activity) ||
+ (!public && private?(activity) && activity.data["actor"] == user.ap_id)
end
- def is_direct?(%Activity{data: %{"directMessage" => true}}), do: true
- def is_direct?(%Object{data: %{"directMessage" => true}}), do: true
+ def direct?(%Activity{data: %{"directMessage" => true}}), do: true
+ def direct?(%Object{data: %{"directMessage" => true}}), do: true
- def is_direct?(activity) do
- !is_public?(activity) && !is_private?(activity)
+ def direct?(activity) do
+ !public?(activity) && !private?(activity)
end
- def is_list?(%{data: %{"listMessage" => _}}), do: true
- def is_list?(_), do: false
+ def list?(%{data: %{"listMessage" => _}}), do: true
+ def list?(_), do: false
@spec visible_for_user?(Object.t() | Activity.t() | nil, User.t() | nil) :: boolean()
def visible_for_user?(%Object{data: %{"type" => "Tombstone"}}, _), do: false
@@ -77,7 +77,7 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
when module in [Activity, Object] do
if restrict_unauthenticated_access?(message),
do: false,
- else: is_public?(message) and not is_local_public?(message)
+ else: public?(message) and not local_public?(message)
end
def visible_for_user?(%{__struct__: module} = message, user)
@@ -86,8 +86,8 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
y = [message.data["actor"]] ++ message.data["to"] ++ (message.data["cc"] || [])
user_is_local = user.local
- federatable = not is_local_public?(message)
- (is_public?(message) || Enum.any?(x, &(&1 in y))) and (user_is_local || federatable)
+ federatable = not local_public?(message)
+ (public?(message) || Enum.any?(x, &(&1 in y))) and (user_is_local || federatable)
end
def entire_thread_visible_for_user?(%Activity{} = activity, %User{} = user) do
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 1894000ff..0f22dd538 100644
--- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
@@ -13,6 +13,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
alias Pleroma.ModerationLog
alias Pleroma.Stats
alias Pleroma.User
+ alias Pleroma.User.Backup
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.AdminAPI
alias Pleroma.Web.AdminAPI.AccountView
@@ -429,7 +430,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
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
+ %Backup{} = backup <- Backup.new(user),
+ {:ok, inserted_backup} <- Pleroma.Repo.insert(backup),
+ {:ok, %Oban.Job{}} <- Backup.schedule_backup(inserted_backup) do
ModerationLog.insert_log(%{actor: admin, subject: user, action: "create_backup"})
json(conn, "")
diff --git a/lib/pleroma/web/admin_api/controllers/config_controller.ex b/lib/pleroma/web/admin_api/controllers/config_controller.ex
index a03318c0e..2c9c27294 100644
--- a/lib/pleroma/web/admin_api/controllers/config_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/config_controller.ex
@@ -9,7 +9,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
alias Pleroma.ConfigDB
alias Pleroma.Web.Plugs.OAuthScopesPlug
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(OAuthScopesPlug, %{scopes: ["admin:write"]} when action == :update)
plug(
@@ -76,7 +76,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
json(conn, translate_descriptions(descriptions))
end
- def show(conn, %{only_db: true}) do
+ def show(%{private: %{open_api_spex: %{params: %{only_db: true}}}} = conn, _) do
with :ok <- configurable_from_database() do
configs = Pleroma.Repo.all(ConfigDB)
@@ -128,7 +128,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
end
end
- def update(%{body_params: %{configs: configs}} = conn, _) do
+ def update(%{private: %{open_api_spex: %{body_params: %{configs: configs}}}} = conn, _) do
with :ok <- configurable_from_database() do
results =
configs
diff --git a/lib/pleroma/web/admin_api/controllers/instance_document_controller.ex b/lib/pleroma/web/admin_api/controllers/instance_document_controller.ex
index 990a94313..d76a95960 100644
--- a/lib/pleroma/web/admin_api/controllers/instance_document_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/instance_document_controller.ex
@@ -9,7 +9,7 @@ defmodule Pleroma.Web.AdminAPI.InstanceDocumentController do
alias Pleroma.Web.Plugs.InstanceStatic
alias Pleroma.Web.Plugs.OAuthScopesPlug
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
action_fallback(Pleroma.Web.AdminAPI.FallbackController)
@@ -18,7 +18,7 @@ defmodule Pleroma.Web.AdminAPI.InstanceDocumentController do
plug(OAuthScopesPlug, %{scopes: ["admin:read"]} when action == :show)
plug(OAuthScopesPlug, %{scopes: ["admin:write"]} when action in [:update, :delete])
- def show(conn, %{name: document_name}) do
+ def show(%{private: %{open_api_spex: %{params: %{name: document_name}}}} = conn, _) do
with {:ok, url} <- InstanceDocument.get(document_name),
{:ok, content} <- File.read(InstanceStatic.file_path(url)) do
conn
@@ -27,13 +27,18 @@ defmodule Pleroma.Web.AdminAPI.InstanceDocumentController do
end
end
- def update(%{body_params: %{file: file}} = conn, %{name: document_name}) do
+ def update(
+ %{
+ private: %{open_api_spex: %{body_params: %{file: file}, params: %{name: document_name}}}
+ } = conn,
+ _
+ ) do
with {:ok, url} <- InstanceDocument.put(document_name, file.path) do
json(conn, %{"url" => url})
end
end
- def delete(conn, %{name: document_name}) do
+ def delete(%{private: %{open_api_spex: %{params: %{name: document_name}}}} = conn, _) do
with :ok <- InstanceDocument.delete(document_name) do
json(conn, %{})
end
diff --git a/lib/pleroma/web/admin_api/controllers/invite_controller.ex b/lib/pleroma/web/admin_api/controllers/invite_controller.ex
index c5d759bb5..30dbc7e73 100644
--- a/lib/pleroma/web/admin_api/controllers/invite_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/invite_controller.ex
@@ -13,7 +13,7 @@ defmodule Pleroma.Web.AdminAPI.InviteController do
require Logger
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(OAuthScopesPlug, %{scopes: ["admin:read:invites"]} when action == :index)
plug(
@@ -33,25 +33,30 @@ defmodule Pleroma.Web.AdminAPI.InviteController do
end
@doc "Create an account registration invite token"
- def create(%{body_params: params} = conn, _) do
+ def create(%{private: %{open_api_spex: %{body_params: params}}} = conn, _) do
{:ok, invite} = UserInviteToken.create_invite(params)
render(conn, "show.json", invite: invite)
end
@doc "Revokes invite by token"
- def revoke(%{body_params: %{token: token}} = conn, _) do
+ def revoke(%{private: %{open_api_spex: %{body_params: %{token: token}}}} = conn, _) do
with {:ok, invite} <- UserInviteToken.find_by_token(token),
{:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do
render(conn, "show.json", invite: updated_invite)
else
nil -> {:error, :not_found}
- error -> error
end
end
@doc "Sends registration invite via email"
- def email(%{assigns: %{user: user}, body_params: %{email: email} = params} = conn, _) do
+ def email(
+ %{
+ assigns: %{user: user},
+ private: %{open_api_spex: %{body_params: %{email: email} = params}}
+ } = conn,
+ _
+ ) do
with {_, false} <- {:registrations_open, Config.get([:instance, :registrations_open])},
{_, true} <- {:invites_enabled, Config.get([:instance, :invites_enabled])},
{:ok, invite_token} <- UserInviteToken.create_invite(),
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 4d53f5451..8b43ea90f 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
@@ -11,7 +11,7 @@ defmodule Pleroma.Web.AdminAPI.MediaProxyCacheController do
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(
OAuthScopesPlug,
@@ -27,7 +27,7 @@ defmodule Pleroma.Web.AdminAPI.MediaProxyCacheController do
defdelegate open_api_operation(action), to: Spec.MediaProxyCacheOperation
- def index(%{assigns: %{user: _}} = conn, params) do
+ def index(%{assigns: %{user: _}, private: %{open_api_spex: %{params: params}}} = conn, _) do
entries = fetch_entries(params)
urls = paginate_entries(entries, params.page, params.page_size)
@@ -59,12 +59,19 @@ defmodule Pleroma.Web.AdminAPI.MediaProxyCacheController do
Enum.slice(entries, offset, page_size)
end
- def delete(%{assigns: %{user: _}, body_params: %{urls: urls}} = conn, _) do
+ def delete(
+ %{assigns: %{user: _}, private: %{open_api_spex: %{body_params: %{urls: urls}}}} = conn,
+ _
+ ) do
MediaProxy.remove_from_banned_urls(urls)
json(conn, %{})
end
- def purge(%{assigns: %{user: _}, body_params: %{urls: urls, ban: ban}} = conn, _) do
+ def purge(
+ %{assigns: %{user: _}, private: %{open_api_spex: %{body_params: %{urls: urls, ban: ban}}}} =
+ conn,
+ _
+ ) do
MediaProxy.Invalidation.purge(urls)
if ban do
diff --git a/lib/pleroma/web/admin_api/controllers/relay_controller.ex b/lib/pleroma/web/admin_api/controllers/relay_controller.ex
index 2e83fe139..1f36d3be5 100644
--- a/lib/pleroma/web/admin_api/controllers/relay_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/relay_controller.ex
@@ -11,7 +11,7 @@ defmodule Pleroma.Web.AdminAPI.RelayController do
require Logger
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(
OAuthScopesPlug,
@@ -31,7 +31,13 @@ defmodule Pleroma.Web.AdminAPI.RelayController do
end
end
- def follow(%{assigns: %{user: admin}, body_params: %{relay_url: target}} = conn, _) do
+ def follow(
+ %{
+ assigns: %{user: admin},
+ private: %{open_api_spex: %{body_params: %{relay_url: target}}}
+ } = conn,
+ _
+ ) do
with {:ok, _message} <- Relay.follow(target) do
ModerationLog.insert_log(%{action: "relay_follow", actor: admin, target: target})
@@ -44,7 +50,13 @@ defmodule Pleroma.Web.AdminAPI.RelayController do
end
end
- def unfollow(%{assigns: %{user: admin}, body_params: %{relay_url: target} = params} = conn, _) do
+ def unfollow(
+ %{
+ assigns: %{user: admin},
+ private: %{open_api_spex: %{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})
diff --git a/lib/pleroma/web/admin_api/controllers/report_controller.ex b/lib/pleroma/web/admin_api/controllers/report_controller.ex
index 15cbbcc3e..89d8cc820 100644
--- a/lib/pleroma/web/admin_api/controllers/report_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/report_controller.ex
@@ -18,7 +18,7 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
require Logger
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(OAuthScopesPlug, %{scopes: ["admin:read:reports"]} when action in [:index, :show])
plug(
@@ -31,13 +31,13 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.ReportOperation
- def index(conn, params) do
+ def index(%{private: %{open_api_spex: %{params: params}}} = conn, _) do
reports = Utils.get_reports(params, params.page, params.page_size)
render(conn, "index.json", reports: reports)
end
- def show(conn, %{id: id}) do
+ def show(%{private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
with %Activity{} = report <- Activity.get_report(id) do
render(conn, "show.json", Report.extract_report_info(report))
else
@@ -45,7 +45,13 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
end
end
- def update(%{assigns: %{user: admin}, body_params: %{reports: reports}} = conn, _) do
+ def update(
+ %{
+ assigns: %{user: admin},
+ private: %{open_api_spex: %{body_params: %{reports: reports}}}
+ } = conn,
+ _
+ ) do
result =
Enum.map(reports, fn report ->
case CommonAPI.update_report_state(report.id, report.state) do
@@ -73,9 +79,13 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
end
end
- def notes_create(%{assigns: %{user: user}, body_params: %{content: content}} = conn, %{
- id: report_id
- }) do
+ def notes_create(
+ %{
+ assigns: %{user: user},
+ private: %{open_api_spex: %{body_params: %{content: content}, params: %{id: report_id}}}
+ } = conn,
+ _
+ ) do
with {:ok, _} <- ReportNote.create(user.id, report_id, content),
report <- Activity.get_by_id_with_user_actor(report_id) do
ModerationLog.insert_log(%{
@@ -92,10 +102,20 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
end
end
- def notes_delete(%{assigns: %{user: user}} = conn, %{
- id: note_id,
- report_id: report_id
- }) do
+ def notes_delete(
+ %{
+ assigns: %{user: user},
+ private: %{
+ open_api_spex: %{
+ params: %{
+ id: note_id,
+ report_id: report_id
+ }
+ }
+ }
+ } = conn,
+ _
+ ) do
with {:ok, note} <- ReportNote.destroy(note_id),
report <- Activity.get_by_id_with_user_actor(report_id) do
ModerationLog.insert_log(%{
diff --git a/lib/pleroma/web/admin_api/controllers/rule_controller.ex b/lib/pleroma/web/admin_api/controllers/rule_controller.ex
new file mode 100644
index 000000000..5d4427b84
--- /dev/null
+++ b/lib/pleroma/web/admin_api/controllers/rule_controller.ex
@@ -0,0 +1,62 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.AdminAPI.RuleController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Repo
+ alias Pleroma.Rule
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+
+ import Pleroma.Web.ControllerHelper,
+ only: [
+ json_response: 3
+ ]
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["admin:write"]}
+ when action in [:create, :update, :delete]
+ )
+
+ plug(OAuthScopesPlug, %{scopes: ["admin:read"]} when action == :index)
+
+ action_fallback(Pleroma.Web.AdminAPI.FallbackController)
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.RuleOperation
+
+ def index(conn, _) do
+ rules =
+ Rule.query()
+ |> Repo.all()
+
+ render(conn, "index.json", rules: rules)
+ end
+
+ def create(%{body_params: params} = conn, _) do
+ rule =
+ params
+ |> Rule.create()
+
+ render(conn, "show.json", rule: rule)
+ end
+
+ def update(%{body_params: params} = conn, %{id: id}) do
+ rule =
+ params
+ |> Rule.update(id)
+
+ render(conn, "show.json", rule: rule)
+ end
+
+ def delete(conn, %{id: id}) do
+ with {:ok, _} <- Rule.delete(id) do
+ json(conn, %{})
+ else
+ _ -> json_response(conn, :bad_request, "")
+ end
+ end
+end
diff --git a/lib/pleroma/web/admin_api/controllers/user_controller.ex b/lib/pleroma/web/admin_api/controllers/user_controller.ex
index 7b4ee46a4..9ac275396 100644
--- a/lib/pleroma/web/admin_api/controllers/user_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/user_controller.ex
@@ -18,7 +18,7 @@ defmodule Pleroma.Web.AdminAPI.UserController do
@users_page_size 50
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(
OAuthScopesPlug,
@@ -51,13 +51,22 @@ defmodule Pleroma.Web.AdminAPI.UserController do
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.UserOperation
- def delete(conn, %{nickname: nickname}) do
+ def delete(%{private: %{open_api_spex: %{params: %{nickname: nickname}}}} = conn, _) do
conn
- |> Map.put(:body_params, %{nicknames: [nickname]})
- |> delete(%{})
+ |> do_deletes([nickname])
end
- def delete(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = conn, _) do
+ def delete(
+ %{
+ private: %{open_api_spex: %{body_params: %{nicknames: nicknames}}}
+ } = conn,
+ _
+ ) do
+ conn
+ |> do_deletes(nicknames)
+ end
+
+ defp do_deletes(%{assigns: %{user: admin}} = conn, nicknames) when is_list(nicknames) do
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
Enum.each(users, fn user ->
@@ -77,9 +86,13 @@ defmodule Pleroma.Web.AdminAPI.UserController do
def follow(
%{
assigns: %{user: admin},
- body_params: %{
- follower: follower_nick,
- followed: followed_nick
+ private: %{
+ open_api_spex: %{
+ body_params: %{
+ follower: follower_nick,
+ followed: followed_nick
+ }
+ }
}
} = conn,
_
@@ -102,9 +115,13 @@ defmodule Pleroma.Web.AdminAPI.UserController do
def unfollow(
%{
assigns: %{user: admin},
- body_params: %{
- follower: follower_nick,
- followed: followed_nick
+ private: %{
+ open_api_spex: %{
+ body_params: %{
+ follower: follower_nick,
+ followed: followed_nick
+ }
+ }
}
} = conn,
_
@@ -124,7 +141,13 @@ defmodule Pleroma.Web.AdminAPI.UserController do
json(conn, "ok")
end
- def create(%{assigns: %{user: admin}, body_params: %{users: users}} = conn, _) do
+ def create(
+ %{
+ assigns: %{user: admin},
+ private: %{open_api_spex: %{body_params: %{users: users}}}
+ } = conn,
+ _
+ ) do
changesets =
users
|> Enum.map(fn %{nickname: nickname, email: email, password: password} ->
@@ -178,7 +201,13 @@ defmodule Pleroma.Web.AdminAPI.UserController do
end
end
- def show(%{assigns: %{user: admin}} = conn, %{nickname: nickname}) do
+ def show(
+ %{
+ assigns: %{user: admin},
+ private: %{open_api_spex: %{params: %{nickname: nickname}}}
+ } = conn,
+ _
+ ) do
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do
render(conn, "show.json", %{user: user})
else
@@ -186,7 +215,11 @@ defmodule Pleroma.Web.AdminAPI.UserController do
end
end
- def toggle_activation(%{assigns: %{user: admin}} = conn, %{nickname: nickname}) do
+ def toggle_activation(
+ %{assigns: %{user: admin}, private: %{open_api_spex: %{params: %{nickname: nickname}}}} =
+ conn,
+ _
+ ) do
user = User.get_cached_by_nickname(nickname)
{:ok, updated_user} = User.set_activation(user, !user.is_active)
@@ -202,7 +235,13 @@ defmodule Pleroma.Web.AdminAPI.UserController do
render(conn, "show.json", user: updated_user)
end
- def activate(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = conn, _) do
+ def activate(
+ %{
+ assigns: %{user: admin},
+ private: %{open_api_spex: %{body_params: %{nicknames: nicknames}}}
+ } = conn,
+ _
+ ) do
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
{:ok, updated_users} = User.set_activation(users, true)
@@ -212,10 +251,16 @@ defmodule Pleroma.Web.AdminAPI.UserController do
action: "activate"
})
- render(conn, "index.json", users: Keyword.values(updated_users))
+ render(conn, "index.json", users: updated_users)
end
- def deactivate(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = conn, _) do
+ def deactivate(
+ %{
+ assigns: %{user: admin},
+ private: %{open_api_spex: %{body_params: %{nicknames: nicknames}}}
+ } = conn,
+ _
+ ) do
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
{:ok, updated_users} = User.set_activation(users, false)
@@ -225,10 +270,16 @@ defmodule Pleroma.Web.AdminAPI.UserController do
action: "deactivate"
})
- render(conn, "index.json", users: Keyword.values(updated_users))
+ render(conn, "index.json", users: updated_users)
end
- def approve(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = conn, _) do
+ def approve(
+ %{
+ assigns: %{user: admin},
+ private: %{open_api_spex: %{body_params: %{nicknames: nicknames}}}
+ } = conn,
+ _
+ ) do
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
{:ok, updated_users} = User.approve(users)
@@ -241,7 +292,13 @@ defmodule Pleroma.Web.AdminAPI.UserController do
render(conn, "index.json", users: updated_users)
end
- def suggest(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = conn, _) do
+ def suggest(
+ %{
+ assigns: %{user: admin},
+ private: %{open_api_spex: %{body_params: %{nicknames: nicknames}}}
+ } = conn,
+ _
+ ) do
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
{:ok, updated_users} = User.set_suggestion(users, true)
@@ -254,7 +311,13 @@ defmodule Pleroma.Web.AdminAPI.UserController do
render(conn, "index.json", users: updated_users)
end
- def unsuggest(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = conn, _) do
+ def unsuggest(
+ %{
+ assigns: %{user: admin},
+ private: %{open_api_spex: %{body_params: %{nicknames: nicknames}}}
+ } = conn,
+ _
+ ) do
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
{:ok, updated_users} = User.set_suggestion(users, false)
@@ -267,7 +330,7 @@ defmodule Pleroma.Web.AdminAPI.UserController do
render(conn, "index.json", users: updated_users)
end
- def index(conn, params) do
+ def index(%{private: %{open_api_spex: %{params: params}}} = conn, _) do
{page, page_size} = page_params(params)
filters = maybe_parse_filters(params[:filters])
diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex
index b761dbb22..b4b0be267 100644
--- a/lib/pleroma/web/admin_api/views/report_view.ex
+++ b/lib/pleroma/web/admin_api/views/report_view.ex
@@ -6,9 +6,11 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
use Pleroma.Web, :view
alias Pleroma.HTML
+ alias Pleroma.Rule
alias Pleroma.User
alias Pleroma.Web.AdminAPI
alias Pleroma.Web.AdminAPI.Report
+ alias Pleroma.Web.AdminAPI.RuleView
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.StatusView
@@ -46,7 +48,8 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
as: :activity
}),
state: report.data["state"],
- notes: render(__MODULE__, "index_notes.json", %{notes: report.report_notes})
+ notes: render(__MODULE__, "index_notes.json", %{notes: report.report_notes}),
+ rules: rules(Map.get(report.data, "rules", nil))
}
end
@@ -71,4 +74,16 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
created_at: Utils.to_masto_date(inserted_at)
}
end
+
+ defp rules(nil) do
+ []
+ end
+
+ defp rules(rule_ids) do
+ rules =
+ rule_ids
+ |> Rule.get()
+
+ render(RuleView, "index.json", rules: rules)
+ end
end
diff --git a/lib/pleroma/web/admin_api/views/rule_view.ex b/lib/pleroma/web/admin_api/views/rule_view.ex
new file mode 100644
index 000000000..606443f05
--- /dev/null
+++ b/lib/pleroma/web/admin_api/views/rule_view.ex
@@ -0,0 +1,22 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.AdminAPI.RuleView do
+ use Pleroma.Web, :view
+
+ require Pleroma.Constants
+
+ def render("index.json", %{rules: rules} = _opts) do
+ render_many(rules, __MODULE__, "show.json")
+ end
+
+ def render("show.json", %{rule: rule} = _opts) do
+ %{
+ id: to_string(rule.id),
+ priority: rule.priority,
+ text: rule.text,
+ hint: rule.hint
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec.ex b/lib/pleroma/web/api_spec.ex
index 163226ce5..314782818 100644
--- a/lib/pleroma/web/api_spec.ex
+++ b/lib/pleroma/web/api_spec.ex
@@ -43,7 +43,7 @@ defmodule Pleroma.Web.ApiSpec do
- [Mastodon API documentation](https://docs.joinmastodon.org/client/intro/)
- [Differences in Mastodon API responses from vanilla Mastodon](https://docs-develop.pleroma.social/backend/development/API/differences_in_mastoapi_responses/)
- Please report such occurences on our [issue tracker](https://git.pleroma.social/pleroma/pleroma/-/issues). Feel free to submit API questions or proposals there too!
+ Please report such occurrences on our [issue tracker](https://git.pleroma.social/pleroma/pleroma/-/issues). Feel free to submit API questions or proposals there too!
""",
# Strip environment from the version
version: Application.spec(:pleroma, :vsn) |> to_string() |> String.replace(~r/\+.*$/, ""),
@@ -94,14 +94,15 @@ defmodule Pleroma.Web.ApiSpec do
"tags" => [
"Chat administration",
"Emoji pack administration",
- "Frontend managment",
+ "Frontend management",
"Instance configuration",
"Instance documents",
+ "Instance rule managment",
"Invites",
"MediaProxy cache",
- "OAuth application managment",
+ "OAuth application management",
"Relays",
- "Report managment",
+ "Report management",
"Status administration",
"User administration",
"Announcement management"
@@ -137,7 +138,8 @@ defmodule Pleroma.Web.ApiSpec do
"Scheduled statuses",
"Search",
"Status actions",
- "Media attachments"
+ "Media attachments",
+ "Bookmark folders"
]
},
%{
diff --git a/lib/pleroma/web/api_spec/cast_and_validate.ex b/lib/pleroma/web/api_spec/cast_and_validate.ex
index add59eb88..672d1c4a1 100644
--- a/lib/pleroma/web/api_spec/cast_and_validate.ex
+++ b/lib/pleroma/web/api_spec/cast_and_validate.ex
@@ -18,6 +18,8 @@ defmodule Pleroma.Web.ApiSpec.CastAndValidate do
alias OpenApiSpex.Plug.PutApiSpec
alias Plug.Conn
+ require Logger
+
@impl Plug
def init(opts) do
opts
@@ -27,10 +29,12 @@ defmodule Pleroma.Web.ApiSpec.CastAndValidate do
@impl Plug
- def call(conn, %{operation_id: operation_id, render_error: render_error}) do
+ def call(conn, %{operation_id: operation_id, render_error: render_error} = opts) do
{spec, operation_lookup} = PutApiSpec.get_spec_and_operation_lookup(conn)
operation = operation_lookup[operation_id]
+ cast_opts = opts |> Map.take([:replace_params]) |> Map.to_list()
+
content_type =
case Conn.get_req_header(conn, "content-type") do
[header_value | _] ->
@@ -44,11 +48,15 @@ defmodule Pleroma.Web.ApiSpec.CastAndValidate do
conn = Conn.put_private(conn, :operation_id, operation_id)
- case cast_and_validate(spec, operation, conn, content_type, strict?()) do
+ case cast_and_validate(spec, operation, conn, content_type, strict?(), cast_opts) do
{:ok, conn} ->
conn
{:error, reason} ->
+ Logger.error(
+ "Strict ApiSpec: request denied to #{conn.request_path} with params #{inspect(conn.params)}"
+ )
+
opts = render_error.init(reason)
conn
@@ -94,11 +102,11 @@ defmodule Pleroma.Web.ApiSpec.CastAndValidate do
def call(conn, opts), do: OpenApiSpex.Plug.CastAndValidate.call(conn, opts)
- defp cast_and_validate(spec, operation, conn, content_type, true = _strict) do
- OpenApiSpex.cast_and_validate(spec, operation, conn, content_type)
+ defp cast_and_validate(spec, operation, conn, content_type, true = _strict, cast_opts) do
+ OpenApiSpex.cast_and_validate(spec, operation, conn, content_type, cast_opts)
end
- defp cast_and_validate(spec, operation, conn, content_type, false = _strict) do
+ defp cast_and_validate(spec, operation, conn, content_type, false = _strict, cast_opts) do
case OpenApiSpex.cast_and_validate(spec, operation, conn, content_type) do
{:ok, conn} ->
{:ok, conn}
@@ -123,7 +131,7 @@ defmodule Pleroma.Web.ApiSpec.CastAndValidate do
end)
conn = %Conn{conn | query_params: query_params}
- OpenApiSpex.cast_and_validate(spec, operation, conn, content_type)
+ OpenApiSpex.cast_and_validate(spec, operation, conn, content_type, cast_opts)
end
end
diff --git a/lib/pleroma/web/api_spec/helpers.ex b/lib/pleroma/web/api_spec/helpers.ex
index f20a9163d..7257253ba 100644
--- a/lib/pleroma/web/api_spec/helpers.ex
+++ b/lib/pleroma/web/api_spec/helpers.ex
@@ -62,7 +62,7 @@ defmodule Pleroma.Web.ApiSpec.Helpers do
Operation.parameter(
:with_relationships,
:query,
- BooleanLike,
+ BooleanLike.schema(),
"Embed relationships into accounts. **If this parameter is not set account's `pleroma.relationship` is going to be `null`.**"
)
end
diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex
index f2897a3a3..85f02166f 100644
--- a/lib/pleroma/web/api_spec/operations/account_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/account_operation.ex
@@ -11,6 +11,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
alias Pleroma.Web.ApiSpec.Schemas.ActorType
alias Pleroma.Web.ApiSpec.Schemas.ApiError
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
+ alias Pleroma.Web.ApiSpec.Schemas.FlakeID
alias Pleroma.Web.ApiSpec.Schemas.List
alias Pleroma.Web.ApiSpec.Schemas.Status
alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
@@ -122,22 +123,27 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
parameters:
[
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
- Operation.parameter(:pinned, :query, BooleanLike, "Include only pinned statuses"),
+ Operation.parameter(
+ :pinned,
+ :query,
+ BooleanLike.schema(),
+ "Include only pinned statuses"
+ ),
Operation.parameter(:tagged, :query, :string, "With tag"),
Operation.parameter(
:only_media,
:query,
- BooleanLike,
+ BooleanLike.schema(),
"Include only statuses with media attached"
),
Operation.parameter(
:with_muted,
:query,
- BooleanLike,
+ BooleanLike.schema(),
"Include statuses from muted accounts."
),
- Operation.parameter(:exclude_reblogs, :query, BooleanLike, "Exclude reblogs"),
- Operation.parameter(:exclude_replies, :query, BooleanLike, "Exclude replies"),
+ Operation.parameter(:exclude_reblogs, :query, BooleanLike.schema(), "Exclude reblogs"),
+ Operation.parameter(:exclude_replies, :query, BooleanLike.schema(), "Exclude replies"),
Operation.parameter(
:exclude_visibilities,
:query,
@@ -147,7 +153,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
Operation.parameter(
:with_muted,
:query,
- BooleanLike,
+ BooleanLike.schema(),
"Include reactions from muted accounts."
)
] ++ pagination_params(),
@@ -347,7 +353,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
summary: "Endorse",
operationId: "AccountController.endorse",
security: [%{"oAuth" => ["follow", "write:accounts"]}],
- description: "Addds the given account to endorsed accounts list.",
+ description: "Adds the given account to endorsed accounts list.",
parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
responses: %{
200 => Operation.response("Relationship", "application/json", AccountRelationship),
@@ -508,6 +514,48 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
}
end
+ def familiar_followers_operation do
+ %Operation{
+ tags: ["Retrieve account information"],
+ summary: "Followers that you follow",
+ operationId: "AccountController.familiar_followers",
+ description:
+ "Obtain a list of all accounts that follow a given account, filtered for accounts you follow.",
+ security: [%{"oAuth" => ["read:follows"]}],
+ parameters: [
+ Operation.parameter(
+ :id,
+ :query,
+ %Schema{
+ oneOf: [%Schema{type: :array, items: %Schema{type: :string}}, %Schema{type: :string}]
+ },
+ "Account IDs",
+ example: "123"
+ )
+ ],
+ responses: %{
+ 200 =>
+ Operation.response("Accounts", "application/json", %Schema{
+ title: "ArrayOfAccounts",
+ type: :array,
+ items: %Schema{
+ title: "Account",
+ type: :object,
+ properties: %{
+ id: FlakeID,
+ accounts: %Schema{
+ title: "ArrayOfAccounts",
+ type: :array,
+ items: Account,
+ example: [Account.schema().example]
+ }
+ }
+ }
+ })
+ }
+ }
+ end
+
defp create_request do
%Schema{
title: "AccountCreateRequest",
diff --git a/lib/pleroma/web/api_spec/operations/admin/frontend_operation.ex b/lib/pleroma/web/api_spec/operations/admin/frontend_operation.ex
index 3e85c44d2..e17881b49 100644
--- a/lib/pleroma/web/api_spec/operations/admin/frontend_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/admin/frontend_operation.ex
@@ -16,7 +16,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.FrontendOperation do
def index_operation do
%Operation{
- tags: ["Frontend managment"],
+ tags: ["Frontend management"],
summary: "Retrieve a list of available frontends",
operationId: "AdminAPI.FrontendController.index",
security: [%{"oAuth" => ["admin:read"]}],
@@ -29,7 +29,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.FrontendOperation do
def install_operation do
%Operation{
- tags: ["Frontend managment"],
+ tags: ["Frontend management"],
summary: "Install a frontend",
operationId: "AdminAPI.FrontendController.install",
security: [%{"oAuth" => ["admin:read"]}],
diff --git a/lib/pleroma/web/api_spec/operations/admin/o_auth_app_operation.ex b/lib/pleroma/web/api_spec/operations/admin/o_auth_app_operation.ex
index 1a05aff6a..2b2496c26 100644
--- a/lib/pleroma/web/api_spec/operations/admin/o_auth_app_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/admin/o_auth_app_operation.ex
@@ -17,7 +17,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do
def index_operation do
%Operation{
summary: "Retrieve a list of OAuth applications",
- tags: ["OAuth application managment"],
+ tags: ["OAuth application management"],
operationId: "AdminAPI.OAuthAppController.index",
security: [%{"oAuth" => ["admin:write"]}],
parameters: [
@@ -69,7 +69,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do
def create_operation do
%Operation{
- tags: ["OAuth application managment"],
+ tags: ["OAuth application management"],
summary: "Create an OAuth application",
operationId: "AdminAPI.OAuthAppController.create",
requestBody: request_body("Parameters", create_request()),
@@ -84,7 +84,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do
def update_operation do
%Operation{
- tags: ["OAuth application managment"],
+ tags: ["OAuth application management"],
summary: "Update OAuth application",
operationId: "AdminAPI.OAuthAppController.update",
parameters: [id_param() | admin_api_params()],
@@ -102,7 +102,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do
def delete_operation do
%Operation{
- tags: ["OAuth application managment"],
+ tags: ["OAuth application management"],
summary: "Delete OAuth application",
operationId: "AdminAPI.OAuthAppController.delete",
parameters: [id_param() | admin_api_params()],
diff --git a/lib/pleroma/web/api_spec/operations/admin/report_operation.ex b/lib/pleroma/web/api_spec/operations/admin/report_operation.ex
index 312e091a5..25a604beb 100644
--- a/lib/pleroma/web/api_spec/operations/admin/report_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/admin/report_operation.ex
@@ -19,7 +19,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
def index_operation do
%Operation{
- tags: ["Report managment"],
+ tags: ["Report management"],
summary: "Retrieve a list of reports",
operationId: "AdminAPI.ReportController.index",
security: [%{"oAuth" => ["admin:read:reports"]}],
@@ -31,6 +31,12 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
"Filter by report state"
),
Operation.parameter(
+ :rule_id,
+ :query,
+ %Schema{type: :string},
+ "Filter by selected rule id"
+ ),
+ Operation.parameter(
:limit,
:query,
%Schema{type: :integer},
@@ -69,7 +75,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
def show_operation do
%Operation{
- tags: ["Report managment"],
+ tags: ["Report management"],
summary: "Retrieve a report",
operationId: "AdminAPI.ReportController.show",
parameters: [id_param() | admin_api_params()],
@@ -83,7 +89,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
def update_operation do
%Operation{
- tags: ["Report managment"],
+ tags: ["Report management"],
summary: "Change state of specified reports",
operationId: "AdminAPI.ReportController.update",
security: [%{"oAuth" => ["admin:write:reports"]}],
@@ -99,7 +105,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
def notes_create_operation do
%Operation{
- tags: ["Report managment"],
+ tags: ["Report management"],
summary: "Add a note to the report",
operationId: "AdminAPI.ReportController.notes_create",
parameters: [id_param() | admin_api_params()],
@@ -120,7 +126,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
def notes_delete_operation do
%Operation{
- tags: ["Report managment"],
+ tags: ["Report management"],
summary: "Delete note attached to the report",
operationId: "AdminAPI.ReportController.notes_delete",
parameters: [
@@ -141,7 +147,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
end
def id_param do
- Operation.parameter(:id, :path, FlakeID, "Report ID",
+ Operation.parameter(:id, :path, FlakeID.schema(), "Report ID",
example: "9umDrYheeY451cQnEe",
required: true
)
@@ -169,6 +175,17 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
inserted_at: %Schema{type: :string, format: :"date-time"}
}
}
+ },
+ rules: %Schema{
+ type: :array,
+ items: %Schema{
+ type: :object,
+ properties: %{
+ id: %Schema{type: :string},
+ text: %Schema{type: :string},
+ hint: %Schema{type: :string, nullable: true}
+ }
+ }
}
}
}
diff --git a/lib/pleroma/web/api_spec/operations/admin/rule_operation.ex b/lib/pleroma/web/api_spec/operations/admin/rule_operation.ex
new file mode 100644
index 000000000..c3a3ecc7c
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/admin/rule_operation.ex
@@ -0,0 +1,115 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Admin.RuleOperation 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: ["Instance rule managment"],
+ summary: "Retrieve list of instance rules",
+ operationId: "AdminAPI.RuleController.index",
+ security: [%{"oAuth" => ["admin:read"]}],
+ responses: %{
+ 200 =>
+ Operation.response("Response", "application/json", %Schema{
+ type: :array,
+ items: rule()
+ }),
+ 403 => Operation.response("Forbidden", "application/json", ApiError)
+ }
+ }
+ end
+
+ def create_operation do
+ %Operation{
+ tags: ["Instance rule managment"],
+ summary: "Create new rule",
+ operationId: "AdminAPI.RuleController.create",
+ security: [%{"oAuth" => ["admin:write"]}],
+ parameters: admin_api_params(),
+ requestBody: request_body("Parameters", create_request(), required: true),
+ responses: %{
+ 200 => Operation.response("Response", "application/json", rule()),
+ 400 => Operation.response("Bad Request", "application/json", ApiError),
+ 403 => Operation.response("Forbidden", "application/json", ApiError)
+ }
+ }
+ end
+
+ def update_operation do
+ %Operation{
+ tags: ["Instance rule managment"],
+ summary: "Modify existing rule",
+ operationId: "AdminAPI.RuleController.update",
+ security: [%{"oAuth" => ["admin:write"]}],
+ parameters: [Operation.parameter(:id, :path, :string, "Rule ID")],
+ requestBody: request_body("Parameters", update_request(), required: true),
+ responses: %{
+ 200 => Operation.response("Response", "application/json", rule()),
+ 400 => Operation.response("Bad Request", "application/json", ApiError),
+ 403 => Operation.response("Forbidden", "application/json", ApiError)
+ }
+ }
+ end
+
+ def delete_operation do
+ %Operation{
+ tags: ["Instance rule managment"],
+ summary: "Delete rule",
+ operationId: "AdminAPI.RuleController.delete",
+ parameters: [Operation.parameter(:id, :path, :string, "Rule ID")],
+ security: [%{"oAuth" => ["admin:write"]}],
+ responses: %{
+ 200 => empty_object_response(),
+ 404 => Operation.response("Not Found", "application/json", ApiError),
+ 403 => Operation.response("Forbidden", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp create_request do
+ %Schema{
+ type: :object,
+ required: [:text],
+ properties: %{
+ priority: %Schema{type: :integer},
+ text: %Schema{type: :string},
+ hint: %Schema{type: :string}
+ }
+ }
+ end
+
+ defp update_request do
+ %Schema{
+ type: :object,
+ properties: %{
+ priority: %Schema{type: :integer},
+ text: %Schema{type: :string},
+ hint: %Schema{type: :string}
+ }
+ }
+ end
+
+ defp rule do
+ %Schema{
+ type: :object,
+ properties: %{
+ id: %Schema{type: :string},
+ priority: %Schema{type: :integer},
+ text: %Schema{type: :string},
+ hint: %Schema{type: :string, nullable: true}
+ }
+ }
+ 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 cf6a055fc..f56e57a41 100644
--- a/lib/pleroma/web/api_spec/operations/chat_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/chat_operation.ex
@@ -137,7 +137,12 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
"Deprecated due to no support for pagination. Using [/api/v2/pleroma/chats](#operation/ChatController.index2) instead is recommended.",
operationId: "ChatController.index",
parameters: [
- Operation.parameter(:with_muted, :query, BooleanLike, "Include chats from muted users")
+ Operation.parameter(
+ :with_muted,
+ :query,
+ BooleanLike.schema(),
+ "Include chats from muted users"
+ )
],
responses: %{
200 => Operation.response("The chats of the user", "application/json", chats_response())
@@ -156,7 +161,12 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
summary: "Retrieve list of chats",
operationId: "ChatController.index2",
parameters: [
- Operation.parameter(:with_muted, :query, BooleanLike, "Include chats from muted users")
+ Operation.parameter(
+ :with_muted,
+ :query,
+ BooleanLike.schema(),
+ "Include chats from muted users"
+ )
| pagination_params()
],
responses: %{
diff --git a/lib/pleroma/web/api_spec/operations/directory_operation.ex b/lib/pleroma/web/api_spec/operations/directory_operation.ex
index 23fa84dff..2eca17664 100644
--- a/lib/pleroma/web/api_spec/operations/directory_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/directory_operation.ex
@@ -29,7 +29,7 @@ defmodule Pleroma.Web.ApiSpec.DirectoryOperation do
"Order by recent activity or account creation",
required: nil
),
- Operation.parameter(:local, :query, BooleanLike, "Include local users only")
+ Operation.parameter(:local, :query, BooleanLike.schema(), "Include local users only")
] ++ pagination_params(),
responses: %{
200 =>
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 74341d64f..8d6be89a7 100644
--- a/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex
@@ -21,7 +21,7 @@ defmodule Pleroma.Web.ApiSpec.EmojiReactionOperation do
summary:
"Get an object of emoji to account mappings with accounts that reacted to the post",
parameters: [
- Operation.parameter(:id, :path, FlakeID, "Status ID", required: true),
+ Operation.parameter(:id, :path, FlakeID.schema(), "Status ID", required: true),
Operation.parameter(:emoji, :path, :string, "Filter by a single unicode emoji",
required: nil
),
@@ -45,7 +45,7 @@ defmodule Pleroma.Web.ApiSpec.EmojiReactionOperation do
tags: ["Emoji reactions"],
summary: "React to a post with a unicode emoji",
parameters: [
- Operation.parameter(:id, :path, FlakeID, "Status ID", required: true),
+ Operation.parameter(:id, :path, FlakeID.schema(), "Status ID", required: true),
Operation.parameter(:emoji, :path, :string, "A single character unicode emoji",
required: true
)
@@ -64,7 +64,7 @@ defmodule Pleroma.Web.ApiSpec.EmojiReactionOperation do
tags: ["Emoji reactions"],
summary: "Remove a reaction to a post with a unicode emoji",
parameters: [
- Operation.parameter(:id, :path, FlakeID, "Status ID", required: true),
+ Operation.parameter(:id, :path, FlakeID.schema(), "Status ID", required: true),
Operation.parameter(:emoji, :path, :string, "A single character unicode emoji",
required: true
)
diff --git a/lib/pleroma/web/api_spec/operations/instance_operation.ex b/lib/pleroma/web/api_spec/operations/instance_operation.ex
index a07be7e40..7d7a5ecc1 100644
--- a/lib/pleroma/web/api_spec/operations/instance_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/instance_operation.ex
@@ -23,6 +23,18 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
}
end
+ def show2_operation do
+ %Operation{
+ tags: ["Instance misc"],
+ summary: "Retrieve instance information",
+ description: "Information about the server",
+ operationId: "InstanceController.show2",
+ responses: %{
+ 200 => Operation.response("Instance", "application/json", instance2())
+ }
+ }
+ end
+
def peers_operation do
%Operation{
tags: ["Instance misc"],
@@ -34,10 +46,30 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
}
end
+ def rules_operation do
+ %Operation{
+ tags: ["Instance misc"],
+ summary: "Retrieve list of instance rules",
+ operationId: "InstanceController.rules",
+ responses: %{
+ 200 => Operation.response("Array of domains", "application/json", array_of_rules())
+ }
+ }
+ end
+
defp instance do
%Schema{
type: :object,
properties: %{
+ accounts: %Schema{
+ type: :object,
+ properties: %{
+ max_featured_tags: %Schema{
+ type: :integer,
+ description: "The maximum number of featured tags allowed for each account."
+ }
+ }
+ },
uri: %Schema{type: :string, description: "The domain name of the instance"},
title: %Schema{type: :string, description: "The title of the website"},
description: %Schema{
@@ -89,7 +121,7 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
languages: %Schema{
type: :array,
items: %Schema{type: :string},
- description: "Primary langauges of the website and its staff"
+ description: "Primary languages of the website and its staff"
},
registrations: %Schema{type: :boolean, description: "Whether registrations are enabled"},
# Extra (not present in Mastodon):
@@ -160,7 +192,186 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
"urls" => %{
"streaming_api" => "wss://lain.com"
},
- "version" => "2.7.2 (compatible; Pleroma 2.0.50-536-g25eec6d7-develop)"
+ "version" => "2.7.2 (compatible; Pleroma 2.0.50-536-g25eec6d7-develop)",
+ "rules" => array_of_rules()
+ }
+ }
+ end
+
+ defp instance2 do
+ %Schema{
+ type: :object,
+ properties: %{
+ domain: %Schema{type: :string, description: "The domain name of the instance"},
+ title: %Schema{type: :string, description: "The title of the website"},
+ version: %Schema{
+ type: :string,
+ description: "The version of Pleroma installed on the instance"
+ },
+ source_url: %Schema{
+ type: :string,
+ description: "The version of Pleroma installed on the instance"
+ },
+ description: %Schema{
+ type: :string,
+ description: "Admin-defined description of the Pleroma site"
+ },
+ usage: %Schema{
+ type: :object,
+ description: "Instance usage statistics",
+ properties: %{
+ users: %Schema{
+ type: :object,
+ description: "User count statistics",
+ properties: %{
+ active_month: %Schema{
+ type: :integer,
+ description: "Monthly active users"
+ }
+ }
+ }
+ }
+ },
+ email: %Schema{
+ type: :string,
+ description: "An email that may be contacted for any inquiries",
+ format: :email
+ },
+ urls: %Schema{
+ type: :object,
+ description: "URLs of interest for clients apps",
+ properties: %{}
+ },
+ stats: %Schema{
+ type: :object,
+ description: "Statistics about how much information the instance contains",
+ properties: %{
+ user_count: %Schema{
+ type: :integer,
+ description: "Users registered on this instance"
+ },
+ status_count: %Schema{
+ type: :integer,
+ description: "Statuses authored by users on instance"
+ },
+ domain_count: %Schema{
+ type: :integer,
+ description: "Domains federated with this instance"
+ }
+ }
+ },
+ thumbnail: %Schema{
+ type: :object,
+ properties: %{
+ url: %Schema{
+ type: :string,
+ description: "Banner image for the website",
+ nullable: true
+ }
+ }
+ },
+ languages: %Schema{
+ type: :array,
+ items: %Schema{type: :string},
+ description: "Primary languages of the website and its staff"
+ },
+ registrations: %Schema{
+ type: :object,
+ description: "Registrations-related configuration",
+ properties: %{
+ enabled: %Schema{
+ type: :boolean,
+ description: "Whether registrations are enabled"
+ },
+ approval_required: %Schema{
+ type: :boolean,
+ description: "Whether users need to be manually approved by admin"
+ }
+ }
+ },
+ configuration: %Schema{
+ type: :object,
+ description: "Instance configuration",
+ properties: %{
+ accounts: %Schema{
+ type: :object,
+ properties: %{
+ max_featured_tags: %Schema{
+ type: :integer,
+ description: "The maximum number of featured tags allowed for each account."
+ },
+ max_pinned_statuses: %Schema{
+ type: :integer,
+ description: "The maximum number of pinned statuses for each account."
+ }
+ }
+ },
+ urls: %Schema{
+ type: :object,
+ properties: %{
+ streaming: %Schema{
+ type: :string,
+ description: "Websockets address for push streaming"
+ }
+ }
+ },
+ statuses: %Schema{
+ type: :object,
+ description: "A map with poll limits for local statuses",
+ properties: %{
+ characters_reserved_per_url: %Schema{
+ type: :integer,
+ description:
+ "Each URL in a status will be assumed to be exactly this many characters."
+ },
+ max_characters: %Schema{
+ type: :integer,
+ description: "Posts character limit (CW/Subject included in the counter)"
+ },
+ max_media_attachments: %Schema{
+ type: :integer,
+ description: "Media attachment limit"
+ }
+ }
+ },
+ media_attachments: %Schema{
+ type: :object,
+ description: "A map with poll limits for media attachments",
+ properties: %{
+ image_size_limit: %Schema{
+ type: :integer,
+ description: "File size limit of uploaded images"
+ },
+ video_size_limit: %Schema{
+ type: :integer,
+ description: "File size limit of uploaded videos"
+ }
+ }
+ },
+ polls: %Schema{
+ type: :object,
+ description: "A map with poll limits for local polls",
+ properties: %{
+ max_options: %Schema{
+ type: :integer,
+ description: "Maximum number of options."
+ },
+ max_characters_per_option: %Schema{
+ type: :integer,
+ description: "Maximum number of characters per option."
+ },
+ min_expiration: %Schema{
+ type: :integer,
+ description: "Minimum expiration time (in seconds)."
+ },
+ max_expiration: %Schema{
+ type: :integer,
+ description: "Maximum expiration time (in seconds)."
+ }
+ }
+ }
+ }
+ }
}
}
end
@@ -172,4 +383,18 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
example: ["pleroma.site", "lain.com", "bikeshed.party"]
}
end
+
+ defp array_of_rules do
+ %Schema{
+ type: :array,
+ items: %Schema{
+ type: :object,
+ properties: %{
+ id: %Schema{type: :string},
+ text: %Schema{type: :string},
+ hint: %Schema{type: :string}
+ }
+ }
+ }
+ end
end
diff --git a/lib/pleroma/web/api_spec/operations/notification_operation.ex b/lib/pleroma/web/api_spec/operations/notification_operation.ex
index 56aa129d2..2dc0f66df 100644
--- a/lib/pleroma/web/api_spec/operations/notification_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/notification_operation.ex
@@ -62,7 +62,7 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
Operation.parameter(
:with_muted,
:query,
- BooleanLike,
+ BooleanLike.schema(),
"Include the notifications from muted users"
)
] ++ pagination_params(),
@@ -202,7 +202,11 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
"pleroma:report",
"move",
"follow_request",
- "poll"
+ "poll",
+ "status",
+ "update",
+ "admin.sign_up",
+ "admin.report"
],
description: """
The type of event that resulted in the notification.
@@ -216,6 +220,10 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
- `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
+ - `status` - Someone you are subscribed to created a status
+ - `update` - A status you boosted has been edited
+ - `admin.sign_up` - Someone signed up (optionally sent to admins)
+ - `admin.report` - A new report has been filed
"""
}
end
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex
index 5375c5b15..7340653fb 100644
--- a/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex
@@ -142,7 +142,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do
end
defp id_param do
- Operation.parameter(:id, :path, FlakeID, "Account ID",
+ Operation.parameter(:id, :path, FlakeID.schema(), "Account ID",
example: "9umDrYheeY451cQnEe",
required: true
)
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex
index 400f3825d..86f709515 100644
--- a/lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex
@@ -65,12 +65,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaBackupOperation do
file_name: %Schema{type: :string},
file_size: %Schema{type: :integer},
processed: %Schema{type: :boolean, description: "whether this backup has succeeded"},
- state: %Schema{
- type: :string,
- description: "the state of the backup",
- enum: ["pending", "running", "complete", "failed"]
- },
- processed_number: %Schema{type: :integer, description: "the number of records processed"}
+ tempdir: %Schema{type: :string}
},
example: %{
"content_type" => "application/zip",
@@ -79,8 +74,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaBackupOperation do
"file_size" => 4105,
"inserted_at" => "2020-09-08T16:42:07.000Z",
"processed" => true,
- "state" => "complete",
- "processed_number" => 20
+ "tempdir" => "/tmp/PZIMw40vmpM"
}
}
end
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_bookmark_folder_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_bookmark_folder_operation.ex
new file mode 100644
index 000000000..eaa683125
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/pleroma_bookmark_folder_operation.ex
@@ -0,0 +1,125 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.PleromaBookmarkFolderOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+ alias Pleroma.Web.ApiSpec.Schemas.BookmarkFolder
+ alias Pleroma.Web.ApiSpec.Schemas.FlakeID
+
+ import Pleroma.Web.ApiSpec.Helpers
+
+ @spec open_api_operation(any()) :: any()
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def index_operation do
+ %Operation{
+ tags: ["Bookmark folders"],
+ summary: "All bookmark folders",
+ security: [%{"oAuth" => ["read:bookmarks"]}],
+ operationId: "PleromaAPI.BookmarkFolderController.index",
+ responses: %{
+ 200 =>
+ Operation.response("Array of Bookmark Folders", "application/json", %Schema{
+ type: :array,
+ items: BookmarkFolder
+ })
+ }
+ }
+ end
+
+ def create_operation do
+ %Operation{
+ tags: ["Bookmark folders"],
+ summary: "Create a bookmark folder",
+ security: [%{"oAuth" => ["write:bookmarks"]}],
+ operationId: "PleromaAPI.BookmarkFolderController.create",
+ requestBody: request_body("Parameters", create_request(), required: true),
+ responses: %{
+ 200 => Operation.response("Bookmark Folder", "application/json", BookmarkFolder),
+ 422 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def update_operation do
+ %Operation{
+ tags: ["Bookmark folders"],
+ summary: "Update a bookmark folder",
+ security: [%{"oAuth" => ["write:bookmarks"]}],
+ operationId: "PleromaAPI.BookmarkFolderController.update",
+ parameters: [id_param()],
+ requestBody: request_body("Parameters", update_request(), required: true),
+ responses: %{
+ 200 => Operation.response("Bookmark Folder", "application/json", BookmarkFolder),
+ 403 => Operation.response("Forbidden", "application/json", ApiError),
+ 404 => Operation.response("Not Found", "application/json", ApiError),
+ 422 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def delete_operation do
+ %Operation{
+ tags: ["Bookmark folders"],
+ summary: "Delete a bookmark folder",
+ security: [%{"oAuth" => ["write:bookmarks"]}],
+ operationId: "PleromaAPI.BookmarkFolderController.delete",
+ parameters: [id_param()],
+ responses: %{
+ 200 => Operation.response("Bookmark Folder", "application/json", BookmarkFolder),
+ 403 => Operation.response("Forbidden", "application/json", ApiError),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp create_request do
+ %Schema{
+ title: "BookmarkFolderCreateRequest",
+ type: :object,
+ properties: %{
+ name: %Schema{
+ type: :string,
+ description: "Folder name"
+ },
+ emoji: %Schema{
+ type: :string,
+ nullable: true,
+ description: "Folder emoji"
+ }
+ }
+ }
+ end
+
+ defp update_request do
+ %Schema{
+ title: "BookmarkFolderUpdateRequest",
+ type: :object,
+ properties: %{
+ name: %Schema{
+ type: :string,
+ nullable: true,
+ description: "Folder name"
+ },
+ emoji: %Schema{
+ type: :string,
+ nullable: true,
+ description: "Folder emoji"
+ }
+ }
+ }
+ end
+
+ def id_param do
+ Operation.parameter(:id, :path, FlakeID.schema(), "Bookmark Folder ID",
+ example: "9umDrYheeY451cQnEe",
+ required: true
+ )
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_notification_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_notification_operation.ex
index a994345db..0e2865191 100644
--- a/lib/pleroma/web/api_spec/operations/pleroma_notification_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/pleroma_notification_operation.ex
@@ -5,7 +5,6 @@
defmodule Pleroma.Web.ApiSpec.PleromaNotificationOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
- alias Pleroma.Web.ApiSpec.NotificationOperation
alias Pleroma.Web.ApiSpec.Schemas.ApiError
import Pleroma.Web.ApiSpec.Helpers
@@ -35,12 +34,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaNotificationOperation do
Operation.response(
"A Notification or array of Notifications",
"application/json",
- %Schema{
- anyOf: [
- %Schema{type: :array, items: NotificationOperation.notification()},
- NotificationOperation.notification()
- ]
- }
+ %Schema{type: :string}
),
400 => Operation.response("Bad Request", "application/json", ApiError)
}
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex
index ca40da930..f595583b6 100644
--- a/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex
@@ -23,7 +23,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do
security: [%{"oAuth" => ["write"]}],
operationId: "PleromaAPI.ScrobbleController.create",
deprecated: true,
- requestBody: request_body("Parameters", create_request(), requried: true),
+ requestBody: request_body("Parameters", create_request(), required: true),
responses: %{
200 => Operation.response("Scrobble", "application/json", scrobble())
}
@@ -59,6 +59,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do
album: %Schema{type: :string, description: "The album of the media playing"},
artist: %Schema{type: :string, description: "The artist of the media playing"},
length: %Schema{type: :integer, description: "The length of the media playing"},
+ externalLink: %Schema{type: :string, description: "A URL referencing the media playing"},
visibility: %Schema{
allOf: [VisibilityScope],
default: "public",
@@ -69,7 +70,8 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do
"title" => "Some Title",
"artist" => "Some Artist",
"album" => "Some Album",
- "length" => 180_000
+ "length" => 180_000,
+ "externalLink" => "https://www.last.fm/music/Some+Artist/_/Some+Title"
}
}
end
@@ -83,6 +85,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do
title: %Schema{type: :string, description: "The title of the media playing"},
album: %Schema{type: :string, description: "The album of the media playing"},
artist: %Schema{type: :string, description: "The artist of the media playing"},
+ externalLink: %Schema{type: :string, description: "A URL referencing the media playing"},
length: %Schema{
type: :integer,
description: "The length of the media playing",
@@ -97,6 +100,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do
"artist" => "Some Artist",
"album" => "Some Album",
"length" => 180_000,
+ "externalLink" => "https://www.last.fm/music/Some+Artist/_/Some+Title",
"created_at" => "2019-09-28T12:40:45.000Z"
}
}
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_status_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_status_operation.ex
new file mode 100644
index 000000000..77c604952
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/pleroma_status_operation.ex
@@ -0,0 +1,45 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.PleromaStatusOperation do
+ alias OpenApiSpex.Operation
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+ alias Pleroma.Web.ApiSpec.Schemas.FlakeID
+ alias Pleroma.Web.ApiSpec.StatusOperation
+
+ import Pleroma.Web.ApiSpec.Helpers
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def quotes_operation do
+ %Operation{
+ tags: ["Retrieve status information"],
+ summary: "Quoted by",
+ description: "View quotes for a given status",
+ operationId: "PleromaAPI.StatusController.quotes",
+ parameters: [id_param() | pagination_params()],
+ security: [%{"oAuth" => ["read:statuses"]}],
+ responses: %{
+ 200 =>
+ Operation.response(
+ "Array of Status",
+ "application/json",
+ StatusOperation.array_of_statuses()
+ ),
+ 403 => Operation.response("Forbidden", "application/json", ApiError),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def id_param do
+ Operation.parameter(:id, :path, FlakeID.schema(), "Status ID",
+ example: "9umDrYheeY451cQnEe",
+ required: true
+ )
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/poll_operation.ex b/lib/pleroma/web/api_spec/operations/poll_operation.ex
index efd784f03..6dd251743 100644
--- a/lib/pleroma/web/api_spec/operations/poll_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/poll_operation.ex
@@ -47,7 +47,7 @@ defmodule Pleroma.Web.ApiSpec.PollOperation do
end
defp id_param do
- Operation.parameter(:id, :path, FlakeID, "Poll ID",
+ Operation.parameter(:id, :path, FlakeID.schema(), "Poll ID",
example: "123",
required: true
)
diff --git a/lib/pleroma/web/api_spec/operations/report_operation.ex b/lib/pleroma/web/api_spec/operations/report_operation.ex
index c74ac7d5f..f5f88974c 100644
--- a/lib/pleroma/web/api_spec/operations/report_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/report_operation.ex
@@ -53,6 +53,12 @@ defmodule Pleroma.Web.ApiSpec.ReportOperation do
default: false,
description:
"If the account is remote, should the report be forwarded to the remote admin?"
+ },
+ rule_ids: %Schema{
+ type: :array,
+ nullable: true,
+ items: %Schema{type: :string},
+ description: "Array of rules"
}
},
required: [:account_id],
@@ -60,7 +66,8 @@ defmodule Pleroma.Web.ApiSpec.ReportOperation do
"account_id" => "123",
"status_ids" => ["1337"],
"comment" => "bad status!",
- "forward" => "false"
+ "forward" => "false",
+ "rule_ids" => ["3"]
}
}
end
diff --git a/lib/pleroma/web/api_spec/operations/scheduled_activity_operation.ex b/lib/pleroma/web/api_spec/operations/scheduled_activity_operation.ex
index 802d3b6dd..c7ed02ff3 100644
--- a/lib/pleroma/web/api_spec/operations/scheduled_activity_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/scheduled_activity_operation.ex
@@ -88,7 +88,7 @@ defmodule Pleroma.Web.ApiSpec.ScheduledActivityOperation do
end
defp id_param do
- Operation.parameter(:id, :path, FlakeID, "Poll ID",
+ Operation.parameter(:id, :path, FlakeID.schema(), "Poll ID",
example: "123",
required: true
)
diff --git a/lib/pleroma/web/api_spec/operations/search_operation.ex b/lib/pleroma/web/api_spec/operations/search_operation.ex
index 1a7e49be4..c2afe8e18 100644
--- a/lib/pleroma/web/api_spec/operations/search_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/search_operation.ex
@@ -70,7 +70,7 @@ defmodule Pleroma.Web.ApiSpec.SearchOperation do
Operation.parameter(
:account_id,
:query,
- FlakeID,
+ FlakeID.schema(),
"If provided, statuses returned will be authored only by this account"
),
Operation.parameter(
@@ -79,7 +79,9 @@ defmodule Pleroma.Web.ApiSpec.SearchOperation do
%Schema{type: :string, enum: ["accounts", "hashtags", "statuses"]},
"Search type"
),
- Operation.parameter(:q, :query, %Schema{type: :string}, "The search query", required: true),
+ Operation.parameter(:q, :query, %Schema{type: :string}, "The search query",
+ required: true
+ ),
Operation.parameter(
:resolve,
:query,
@@ -116,7 +118,7 @@ defmodule Pleroma.Web.ApiSpec.SearchOperation do
Operation.parameter(
:account_id,
:query,
- FlakeID,
+ FlakeID.schema(),
"If provided, statuses returned will be authored only by this account"
),
Operation.parameter(
diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex
index c133a3aac..1717c68c8 100644
--- a/lib/pleroma/web/api_spec/operations/status_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/status_operation.ex
@@ -39,7 +39,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
Operation.parameter(
:with_muted,
:query,
- BooleanLike,
+ BooleanLike.schema(),
"Include reactions from muted acccounts."
)
],
@@ -82,7 +82,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
Operation.parameter(
:with_muted,
:query,
- BooleanLike,
+ BooleanLike.schema(),
"Include reactions from muted acccounts."
)
],
@@ -256,6 +256,18 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
description: "Privately bookmark a status",
operationId: "StatusController.bookmark",
parameters: [id_param()],
+ requestBody:
+ request_body("Parameters", %Schema{
+ title: "StatusUpdateRequest",
+ type: :object,
+ properties: %{
+ folder_id: %Schema{
+ nullable: true,
+ allOf: [FlakeID],
+ description: "ID of bookmarks folder, if any"
+ }
+ }
+ }),
responses: %{
200 => status_response()
}
@@ -430,7 +442,15 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
summary: "Bookmarked statuses",
description: "Statuses the user has bookmarked",
operationId: "StatusController.bookmarks",
- parameters: pagination_params(),
+ parameters: [
+ Operation.parameter(
+ :folder_id,
+ :query,
+ FlakeID.schema(),
+ "If provided, only display bookmarks from given folder"
+ )
+ | pagination_params()
+ ],
security: [%{"oAuth" => ["read:bookmarks"]}],
responses: %{
200 => Operation.response("Array of Statuses", "application/json", array_of_statuses())
@@ -534,7 +554,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
format: :"date-time",
nullable: true,
description:
- "ISO 8601 Datetime at which to schedule a status. Providing this paramter will cause ScheduledStatus to be returned instead of Status. Must be at least 5 minutes in the future."
+ "ISO 8601 Datetime at which to schedule a status. Providing this parameter will cause ScheduledStatus to be returned instead of Status. Must be at least 5 minutes in the future."
},
language: %Schema{
type: :string,
@@ -546,7 +566,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
allOf: [BooleanLike],
nullable: true,
description:
- "If set to `true` the post won't be actually posted, but the status entitiy would still be rendered back. This could be useful for previewing rich text/custom emoji, for example"
+ "If set to `true` the post won't be actually posted, but the status entity would still be rendered back. This could be useful for previewing rich text/custom emoji, for example"
},
content_type: %Schema{
type: :string,
@@ -685,7 +705,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
end
def id_param do
- Operation.parameter(:id, :path, FlakeID, "Status ID",
+ Operation.parameter(:id, :path, FlakeID.schema(), "Status ID",
example: "9umDrYheeY451cQnEe",
required: true
)
diff --git a/lib/pleroma/web/api_spec/operations/streaming_operation.ex b/lib/pleroma/web/api_spec/operations/streaming_operation.ex
index b580bc2f0..47bce07b3 100644
--- a/lib/pleroma/web/api_spec/operations/streaming_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/streaming_operation.ex
@@ -139,7 +139,7 @@ defmodule Pleroma.Web.ApiSpec.StreamingOperation do
end
defp get_schema(%Schema{} = schema), do: schema
- defp get_schema(schema), do: schema.schema
+ defp get_schema(schema), do: schema.schema()
defp server_sent_event_helper(name, description, type, payload, opts \\ []) do
payload_type = Keyword.get(opts, :payload_type, :json)
diff --git a/lib/pleroma/web/api_spec/operations/timeline_operation.ex b/lib/pleroma/web/api_spec/operations/timeline_operation.ex
index fbe3f763a..f55e59805 100644
--- a/lib/pleroma/web/api_spec/operations/timeline_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/timeline_operation.ex
@@ -176,7 +176,12 @@ defmodule Pleroma.Web.ApiSpec.TimelineOperation do
end
defp with_muted_param do
- Operation.parameter(:with_muted, :query, BooleanLike, "Include activities by muted users")
+ Operation.parameter(
+ :with_muted,
+ :query,
+ BooleanLike.schema(),
+ "Include activities by muted users"
+ )
end
defp exclude_visibilities_param do
diff --git a/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex b/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex
index 084329ad7..724d873c0 100644
--- a/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex
@@ -87,7 +87,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
defp change_password_request do
%Schema{
title: "ChangePasswordRequest",
- description: "POST body for changing the account's passowrd",
+ description: "POST body for changing the account's password",
type: :object,
required: [:password, :new_password, :new_password_confirmation],
properties: %{
@@ -136,23 +136,23 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
}
end
- def update_notificaton_settings_operation do
+ def update_notification_settings_operation do
%Operation{
tags: ["Settings"],
summary: "Update Notification Settings",
security: [%{"oAuth" => ["write:accounts"]}],
- operationId: "UtilController.update_notificaton_settings",
+ operationId: "UtilController.update_notification_settings",
parameters: [
Operation.parameter(
:block_from_strangers,
:query,
- BooleanLike,
+ BooleanLike.schema(),
"blocks notifications from accounts you do not follow"
),
Operation.parameter(
:hide_notification_contents,
:query,
- BooleanLike,
+ BooleanLike.schema(),
"removes the contents of a message from the push notification"
)
],
diff --git a/lib/pleroma/web/api_spec/schemas/attachment.ex b/lib/pleroma/web/api_spec/schemas/attachment.ex
index 48634a14f..4104ed25c 100644
--- a/lib/pleroma/web/api_spec/schemas/attachment.ex
+++ b/lib/pleroma/web/api_spec/schemas/attachment.ex
@@ -11,7 +11,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Attachment do
title: "Attachment",
description: "Represents a file or media attachment that can be added to a status.",
type: :object,
- requried: [:id, :url, :preview_url],
+ required: [:id, :url, :preview_url],
properties: %{
id: %Schema{type: :string, description: "The ID of the attachment in the database."},
url: %Schema{
@@ -50,7 +50,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Attachment do
pleroma: %Schema{
type: :object,
properties: %{
- mime_type: %Schema{type: :string, description: "mime type of the attachment"}
+ mime_type: %Schema{type: :string, description: "mime type of the attachment"},
+ name: %Schema{
+ type: :string,
+ description: "Name of the attachment, typically the filename"
+ }
}
}
},
diff --git a/lib/pleroma/web/api_spec/schemas/bookmark_folder.ex b/lib/pleroma/web/api_spec/schemas/bookmark_folder.ex
new file mode 100644
index 000000000..e8b4f43b7
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/bookmark_folder.ex
@@ -0,0 +1,26 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Schemas.BookmarkFolder do
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.FlakeID
+
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "BookmarkFolder",
+ description: "Response schema for a bookmark folder",
+ type: :object,
+ properties: %{
+ id: FlakeID,
+ name: %Schema{type: :string, description: "Folder name"},
+ emoji: %Schema{type: :string, description: "Folder emoji", nullable: true}
+ },
+ example: %{
+ "id" => "9toJCu5YZW7O7gfvH6",
+ "name" => "Read later",
+ "emoji" => nil
+ }
+ })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/chat.ex b/lib/pleroma/web/api_spec/schemas/chat.ex
index a07d12865..affa25a95 100644
--- a/lib/pleroma/web/api_spec/schemas/chat.ex
+++ b/lib/pleroma/web/api_spec/schemas/chat.ex
@@ -68,7 +68,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Chat do
},
"id" => "1",
"unread" => 2,
- "last_message" => ChatMessage.schema().example(),
+ "last_message" => ChatMessage.schema().example,
"updated_at" => "2020-04-21T15:06:45.000Z"
}
})
diff --git a/lib/pleroma/web/api_spec/schemas/poll.ex b/lib/pleroma/web/api_spec/schemas/poll.ex
index 91570582b..20cf5b061 100644
--- a/lib/pleroma/web/api_spec/schemas/poll.ex
+++ b/lib/pleroma/web/api_spec/schemas/poll.ex
@@ -56,6 +56,15 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Poll do
}
},
description: "Possible answers for the poll."
+ },
+ pleroma: %Schema{
+ type: :object,
+ properties: %{
+ non_anonymous: %Schema{
+ type: :boolean,
+ description: "Can voters be publicly identified?"
+ }
+ }
}
},
example: %{
@@ -79,7 +88,10 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Poll do
votes_count: 4
}
],
- emojis: []
+ emojis: [],
+ pleroma: %{
+ non_anonymous: false
+ }
}
})
end
diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex
index 07f03134a..6e537b5da 100644
--- a/lib/pleroma/web/api_spec/schemas/status.ex
+++ b/lib/pleroma/web/api_spec/schemas/status.ex
@@ -58,6 +58,10 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
format: :uri,
description: "Preview thumbnail"
},
+ image_description: %Schema{
+ type: :string,
+ description: "Alternate text that describes what is in the thumbnail"
+ },
title: %Schema{type: :string, description: "Title of linked resource"},
description: %Schema{type: :string, description: "Description of preview"}
}
@@ -213,6 +217,10 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
type: :boolean,
description: "`true` if the quoted post is visible to the user"
},
+ quotes_count: %Schema{
+ type: :integer,
+ description: "How many statuses quoted this status"
+ },
local: %Schema{
type: :boolean,
description: "`true` if the post was made on the local instance"
@@ -367,7 +375,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
"in_reply_to_account_acct" => nil,
"local" => true,
"spoiler_text" => %{"text/plain" => ""},
- "thread_muted" => false
+ "thread_muted" => false,
+ "quotes_count" => 0
},
"poll" => nil,
"reblog" => nil,
diff --git a/lib/pleroma/web/auth/authenticator.ex b/lib/pleroma/web/auth/authenticator.ex
index a0bd154db..01bf1575c 100644
--- a/lib/pleroma/web/auth/authenticator.ex
+++ b/lib/pleroma/web/auth/authenticator.ex
@@ -5,7 +5,7 @@
defmodule Pleroma.Web.Auth.Authenticator do
@callback get_user(Plug.Conn.t()) :: {:ok, user :: struct()} | {:error, any()}
@callback create_from_registration(Plug.Conn.t(), registration :: struct()) ::
- {:ok, User.t()} | {:error, any()}
+ {:ok, Pleroma.User.t()} | {:error, any()}
@callback get_registration(Plug.Conn.t()) :: {:ok, registration :: struct()} | {:error, any()}
@callback handle_error(Plug.Conn.t(), any()) :: any()
@callback auth_template() :: String.t() | nil
diff --git a/lib/pleroma/web/auth/ldap_authenticator.ex b/lib/pleroma/web/auth/ldap_authenticator.ex
index e8cd4491c..ea5620cf6 100644
--- a/lib/pleroma/web/auth/ldap_authenticator.ex
+++ b/lib/pleroma/web/auth/ldap_authenticator.ex
@@ -91,7 +91,8 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do
end
error ->
- error
+ Logger.error("Could not bind LDAP user #{name}: #{inspect(error)}")
+ {:error, {:ldap_bind_error, error}}
end
end
@@ -102,28 +103,38 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do
{:scope, :eldap.wholeSubtree()},
{:timeout, @search_timeout}
]) do
- {:ok, {:eldap_search_result, [{:eldap_entry, _, attributes}], _}} ->
- 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
-
- changeset = User.register_changeset_ldap(%User{}, params)
+ # The :eldap_search_result record structure changed in OTP 24.3 and added a controls field
+ # https://github.com/erlang/otp/pull/5538
+ {:ok, {:eldap_search_result, [{:eldap_entry, _object, attributes}], _referrals}} ->
+ try_register(name, attributes)
- case User.register(changeset) do
- {:ok, user} -> user
- error -> error
- end
+ {:ok, {:eldap_search_result, [{:eldap_entry, _object, attributes}], _referrals, _controls}} ->
+ try_register(name, attributes)
error ->
- error
+ Logger.error("Couldn't register user because LDAP search failed: #{inspect(error)}")
+ {:error, {:ldap_search_error, error}}
+ end
+ end
+
+ defp try_register(name, attributes) do
+ params = %{
+ name: name,
+ nickname: name,
+ password: nil
+ }
+
+ params =
+ case List.keyfind(attributes, ~c"mail", 0) do
+ {_, [mail]} -> Map.put_new(params, :email, :erlang.list_to_binary(mail))
+ _ -> params
+ end
+
+ changeset = User.register_changeset_ldap(%User{}, params)
+
+ case User.register(changeset) do
+ {:ok, user} -> user
+ error -> error
end
end
end
diff --git a/lib/pleroma/web/common_api.ex b/lib/pleroma/web/common_api.ex
index 82c7f70d2..b90b6a6d9 100644
--- a/lib/pleroma/web/common_api.ex
+++ b/lib/pleroma/web/common_api.ex
@@ -8,6 +8,7 @@ defmodule Pleroma.Web.CommonAPI do
alias Pleroma.Formatter
alias Pleroma.ModerationLog
alias Pleroma.Object
+ alias Pleroma.Rule
alias Pleroma.ThreadMute
alias Pleroma.User
alias Pleroma.UserRelationship
@@ -18,19 +19,23 @@ defmodule Pleroma.Web.CommonAPI do
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI.ActivityDraft
+ import Ecto.Query, only: [where: 3]
import Pleroma.Web.Gettext
import Pleroma.Web.CommonAPI.Utils
require Pleroma.Constants
require Logger
- def block(blocker, blocked) do
+ @spec block(User.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()}
+ def block(blocked, blocker) do
with {:ok, block_data, _} <- Builder.block(blocker, blocked),
{:ok, block, _} <- Pipeline.common_pipeline(block_data, local: true) do
{:ok, block}
end
end
+ @spec post_chat_message(User.t(), User.t(), String.t(), list()) ::
+ {:ok, Activity.t()} | {:error, any()}
def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do
with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]),
:ok <- validate_chat_attachment_attribution(maybe_attachment, user),
@@ -94,7 +99,8 @@ defmodule Pleroma.Web.CommonAPI do
end
end
- def unblock(blocker, blocked) do
+ @spec unblock(User.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()}
+ def unblock(blocked, blocker) do
with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},
{:ok, unblock_data, _} <- Builder.undo(blocker, block),
{:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do
@@ -113,7 +119,9 @@ defmodule Pleroma.Web.CommonAPI do
end
end
- def follow(follower, followed) do
+ @spec follow(User.t(), User.t()) ::
+ {:ok, User.t(), User.t(), Activity.t() | Object.t()} | {:error, :rejected}
+ def follow(followed, follower) do
timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
with {:ok, follow_data, _} <- Builder.follow(follower, followed),
@@ -127,7 +135,8 @@ defmodule Pleroma.Web.CommonAPI do
end
end
- def unfollow(follower, unfollowed) do
+ @spec unfollow(User.t(), User.t()) :: {:ok, User.t()} | {:error, any()}
+ def unfollow(unfollowed, follower) do
with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
{:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
{:ok, _subscription} <- User.unsubscribe(follower, unfollowed),
@@ -136,6 +145,7 @@ defmodule Pleroma.Web.CommonAPI do
end
end
+ @spec accept_follow_request(User.t(), User.t()) :: {:ok, User.t()} | {:error, any()}
def accept_follow_request(follower, followed) do
with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, accept_data, _} <- Builder.accept(followed, follow_activity),
@@ -144,6 +154,7 @@ defmodule Pleroma.Web.CommonAPI do
end
end
+ @spec reject_follow_request(User.t(), User.t()) :: {:ok, User.t()} | {:error, any()} | nil
def reject_follow_request(follower, followed) do
with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, reject_data, _} <- Builder.reject(followed, follow_activity),
@@ -152,9 +163,11 @@ defmodule Pleroma.Web.CommonAPI do
end
end
+ @spec delete(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()}
def delete(activity_id, user) do
with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(activity_id, filter: [])},
+ {_, {:ok, _}} <- {:cancel_jobs, maybe_cancel_jobs(activity)},
{_, %Object{} = object, _} <-
{:find_object, Object.normalize(activity, fetch: false), activity},
true <- User.privileged?(user, :messages_delete) || user.ap_id == object.data["actor"],
@@ -200,6 +213,7 @@ defmodule Pleroma.Web.CommonAPI do
end
end
+ @spec repeat(String.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, any()}
def repeat(id, user, params \\ %{}) do
with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
object = %Object{} <- Object.normalize(activity, fetch: false),
@@ -217,11 +231,13 @@ defmodule Pleroma.Web.CommonAPI do
end
end
+ @spec unrepeat(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()}
def unrepeat(id, user) do
with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(id)},
%Object{} = note <- Object.normalize(activity, fetch: false),
%Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
+ {_, {:ok, _}} <- {:cancel_jobs, maybe_cancel_jobs(announce)},
{:ok, undo, _} <- Builder.undo(user, announce),
{:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
{:ok, activity}
@@ -231,8 +247,8 @@ defmodule Pleroma.Web.CommonAPI do
end
end
- @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
- def favorite(%User{} = user, id) do
+ @spec favorite(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()}
+ def favorite(id, %User{} = user) do
case favorite_helper(user, id) do
{:ok, _} = res ->
res
@@ -246,7 +262,7 @@ defmodule Pleroma.Web.CommonAPI do
end
end
- def favorite_helper(user, id) do
+ defp favorite_helper(user, id) do
with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
{_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
{_, {:ok, %Activity{} = activity, _meta}} <-
@@ -269,11 +285,13 @@ defmodule Pleroma.Web.CommonAPI do
end
end
+ @spec unfavorite(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()}
def unfavorite(id, user) do
with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(id)},
%Object{} = note <- Object.normalize(activity, fetch: false),
%Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
+ {_, {:ok, _}} <- {:cancel_jobs, maybe_cancel_jobs(like)},
{:ok, undo, _} <- Builder.undo(user, like),
{:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
{:ok, activity}
@@ -283,6 +301,8 @@ defmodule Pleroma.Web.CommonAPI do
end
end
+ @spec react_with_emoji(String.t(), User.t(), String.t()) ::
+ {:ok, Activity.t()} | {:error, any()}
def react_with_emoji(id, user, emoji) do
with %Activity{} = activity <- Activity.get_by_id(id),
object <- Object.normalize(activity, fetch: false),
@@ -295,8 +315,11 @@ defmodule Pleroma.Web.CommonAPI do
end
end
+ @spec unreact_with_emoji(String.t(), User.t(), String.t()) ::
+ {:ok, Activity.t()} | {:error, any()}
def unreact_with_emoji(id, user, emoji) do
with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
+ {_, {:ok, _}} <- {:cancel_jobs, maybe_cancel_jobs(reaction_activity)},
{:ok, undo, _} <- Builder.undo(user, reaction_activity),
{:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
{:ok, activity}
@@ -306,7 +329,8 @@ defmodule Pleroma.Web.CommonAPI do
end
end
- def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
+ @spec vote(Object.t(), User.t(), list()) :: {:ok, list(), Object.t()} | {:error, any()}
+ def vote(%Object{data: %{"type" => "Question"}} = object, %User{} = user, choices) do
with :ok <- validate_not_author(object, user),
:ok <- validate_existing_votes(user, object),
{:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
@@ -367,14 +391,16 @@ defmodule Pleroma.Web.CommonAPI do
end
end
- def public_announce?(_, %{visibility: visibility})
- when visibility in ~w{public unlisted private direct},
- do: visibility in ~w(public unlisted)
+ defp public_announce?(_, %{visibility: visibility})
+ when visibility in ~w{public unlisted private direct},
+ do: visibility in ~w(public unlisted)
- def public_announce?(object, _) do
- Visibility.is_public?(object)
+ defp public_announce?(object, _) do
+ Visibility.public?(object)
end
+ @spec get_visibility(map(), map() | nil, Participation.t() | nil) ::
+ {String.t() | nil, String.t() | nil}
def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
def get_visibility(%{visibility: visibility}, in_reply_to, _)
@@ -393,6 +419,7 @@ defmodule Pleroma.Web.CommonAPI do
def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
+ @spec get_replied_to_visibility(Activity.t() | nil) :: String.t() | nil
def get_replied_to_visibility(nil), do: nil
def get_replied_to_visibility(activity) do
@@ -401,6 +428,8 @@ defmodule Pleroma.Web.CommonAPI do
end
end
+ @spec check_expiry_date({:ok, nil | integer()} | String.t()) ::
+ {:ok, boolean() | nil} | {:error, String.t()}
def check_expiry_date({:ok, nil} = res), do: res
def check_expiry_date({:ok, in_seconds}) do
@@ -418,19 +447,22 @@ defmodule Pleroma.Web.CommonAPI do
|> check_expiry_date()
end
+ @spec listen(User.t(), map()) :: {:ok, Activity.t()} | {:error, any()}
def listen(user, data) do
with {:ok, draft} <- ActivityDraft.listen(user, data) do
ActivityPub.listen(draft.changes)
end
end
+ @spec post(User.t(), map()) :: {:ok, Activity.t()} | {:error, any()}
def post(user, %{status: _} = data) do
with {:ok, draft} <- ActivityDraft.create(user, data) do
ActivityPub.create(draft.changes, draft.preview?)
end
end
- def update(user, orig_activity, changes) do
+ @spec update(Activity.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, any()}
+ def update(orig_activity, %User{} = user, changes) do
with orig_object <- Object.normalize(orig_activity),
{:ok, new_object} <- make_update_data(user, orig_object, changes),
{:ok, update_data, _} <- Builder.update(user, new_object),
@@ -500,12 +532,12 @@ defmodule Pleroma.Web.CommonAPI do
end
defp activity_is_public(activity) do
- with false <- Visibility.is_public?(activity) do
+ with false <- Visibility.public?(activity) do
{:error, :visibility_error}
end
end
- @spec unpin(String.t(), User.t()) :: {:ok, User.t()} | {:error, term()}
+ @spec unpin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()}
def unpin(id, user) do
with %Activity{} = activity <- create_activity_by_id(id),
{:ok, unpin_data, _} <- Builder.unpin(user, activity.object),
@@ -520,7 +552,8 @@ defmodule Pleroma.Web.CommonAPI do
end
end
- def add_mute(user, activity, params \\ %{}) do
+ @spec add_mute(Activity.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, any()}
+ def add_mute(activity, user, params \\ %{}) do
expires_in = Map.get(params, :expires_in, 0)
with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
@@ -539,18 +572,20 @@ defmodule Pleroma.Web.CommonAPI do
end
end
- def remove_mute(%User{} = user, %Activity{} = activity) do
+ @spec remove_mute(Activity.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()}
+ def remove_mute(%Activity{} = activity, %User{} = user) do
ThreadMute.remove_mute(user.id, activity.data["context"])
{:ok, activity}
end
- def remove_mute(user_id, activity_id) do
+ @spec remove_mute(String.t(), String.t()) :: {:ok, Activity.t()} | {:error, any()}
+ def remove_mute(activity_id, user_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)
+ remove_mute(activity, user)
else
{what, result} = error ->
- Logger.warn(
+ Logger.warning(
"CommonAPI.remove_mute/2 failed. #{what}: #{result}, user_id: #{user_id}, activity_id: #{activity_id}"
)
@@ -558,24 +593,28 @@ defmodule Pleroma.Web.CommonAPI do
end
end
- def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
+ @spec thread_muted?(Activity.t(), User.t()) :: boolean()
+ def thread_muted?(%{data: %{"context" => context}}, %User{id: user_id})
when is_binary(context) do
ThreadMute.exists?(user_id, context)
end
def thread_muted?(_, _), do: false
+ @spec report(User.t(), map()) :: {:ok, Activity.t()} | {:error, any()}
def report(user, data) do
with {:ok, account} <- get_reported_account(data.account_id),
{:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
- {:ok, statuses} <- get_report_statuses(account, data) do
+ {:ok, statuses} <- get_report_statuses(account, data),
+ rules <- get_report_rules(Map.get(data, :rule_ids, nil)) do
ActivityPub.flag(%{
context: Utils.generate_context_id(),
actor: user,
account: account,
statuses: statuses,
content: content_html,
- forward: Map.get(data, :forward, false)
+ forward: Map.get(data, :forward, false),
+ rules: rules
})
end
end
@@ -587,6 +626,17 @@ defmodule Pleroma.Web.CommonAPI do
end
end
+ defp get_report_rules(nil) do
+ nil
+ end
+
+ defp get_report_rules(rule_ids) do
+ rule_ids
+ |> Enum.filter(&Rule.exists?/1)
+ end
+
+ @spec update_report_state(String.t() | [String.t()], String.t()) ::
+ {:ok, any()} | {:error, any()}
def update_report_state(activity_ids, state) when is_list(activity_ids) do
case Utils.update_report_state(activity_ids, state) do
:ok -> {:ok, activity_ids}
@@ -599,17 +649,16 @@ defmodule Pleroma.Web.CommonAPI do
Utils.update_report_state(activity, state)
else
nil -> {:error, :not_found}
- _ -> {:error, dgettext("errors", "Could not update state")}
end
end
+ @spec update_activity_scope(String.t(), map()) :: {:ok, any()} | {:error, any()}
def update_activity_scope(activity_id, opts \\ %{}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
{:ok, activity} <- toggle_sensitive(activity, opts) do
set_visibility(activity, opts)
else
nil -> {:error, :not_found}
- {:error, reason} -> {:error, reason}
end
end
@@ -637,14 +686,17 @@ defmodule Pleroma.Web.CommonAPI do
defp set_visibility(activity, _), do: {:ok, activity}
- def hide_reblogs(%User{} = user, %User{} = target) do
+ @spec hide_reblogs(User.t(), User.t()) :: {:ok, any()} | {:error, any()}
+ def hide_reblogs(%User{} = target, %User{} = user) do
UserRelationship.create_reblog_mute(user, target)
end
- def show_reblogs(%User{} = user, %User{} = target) do
+ @spec show_reblogs(User.t(), User.t()) :: {:ok, any()} | {:error, any()}
+ def show_reblogs(%User{} = target, %User{} = user) do
UserRelationship.delete_reblog_mute(user, target)
end
+ @spec get_user(String.t(), boolean()) :: User.t() | nil
def get_user(ap_id, fake_record_fallback \\ true) do
cond do
user = User.get_cached_by_ap_id(ap_id) ->
@@ -661,4 +713,14 @@ defmodule Pleroma.Web.CommonAPI do
nil
end
end
+
+ defp maybe_cancel_jobs(%Activity{id: activity_id}) do
+ Oban.Job
+ |> where([j], j.worker == "Pleroma.Workers.PublisherWorker")
+ |> where([j], j.args["op"] == "publish_one")
+ |> where([j], j.args["params"]["activity_id"] == ^activity_id)
+ |> Oban.cancel_all_jobs()
+ end
+
+ defp maybe_cancel_jobs(_), do: {:ok, 0}
end
diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex
index ca1329284..8aa1e258d 100644
--- a/lib/pleroma/web/common_api/activity_draft.ex
+++ b/lib/pleroma/web/common_api/activity_draft.ex
@@ -14,6 +14,8 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
import Pleroma.Web.Gettext
import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
+ @type t :: %__MODULE__{}
+
defstruct valid?: true,
errors: [],
user: nil,
@@ -83,7 +85,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
defp listen_object(draft) do
object =
draft.params
- |> Map.take([:album, :artist, :title, :length])
+ |> Map.take([:album, :artist, :title, :length, :externalLink])
|> Map.new(fn {key, value} -> {to_string(key), value} end)
|> Map.put("type", "Audio")
|> Map.put("to", draft.to)
@@ -127,8 +129,22 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
defp in_reply_to(%{params: %{in_reply_to_status_id: ""}} = draft), do: draft
- defp in_reply_to(%{params: %{in_reply_to_status_id: id}} = draft) when is_binary(id) do
- %__MODULE__{draft | in_reply_to: Activity.get_by_id(id)}
+ defp in_reply_to(%{params: %{in_reply_to_status_id: :deleted}} = draft) do
+ add_error(draft, dgettext("errors", "Cannot reply to a deleted status"))
+ end
+
+ defp in_reply_to(%{params: %{in_reply_to_status_id: id} = params} = draft) when is_binary(id) do
+ activity = Activity.get_by_id(id)
+
+ params =
+ if is_nil(activity) do
+ # Deleted activities are returned as nil
+ Map.put(params, :in_reply_to_status_id, :deleted)
+ else
+ Map.put(params, :in_reply_to_status_id, activity)
+ end
+
+ in_reply_to(%{draft | params: params})
end
defp in_reply_to(%{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}} = draft) do
diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index 0f394e951..52c08f00f 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -109,7 +109,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
def get_to_and_cc(%{visibility: "direct"} = draft) do
# If the OP is a DM already, add the implicit actor.
- if draft.in_reply_to && Visibility.is_direct?(draft.in_reply_to) do
+ if draft.in_reply_to && Visibility.direct?(draft.in_reply_to) do
{Enum.uniq([draft.in_reply_to.data["actor"] | draft.mentions]), []}
else
{draft.mentions, []}
@@ -321,13 +321,13 @@ defmodule Pleroma.Web.CommonAPI.Utils do
format_asctime(date)
else
_e ->
- Logger.warn("Date #{date} in wrong format, must be ISO 8601")
+ Logger.warning("Date #{date} in wrong format, must be ISO 8601")
""
end
end
def date_to_asctime(date) do
- Logger.warn("Date #{date} in wrong format, must be ISO 8601")
+ Logger.warning("Date #{date} in wrong format, must be ISO 8601")
""
end
diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex
index 0c7fc17f4..1caf0f7e6 100644
--- a/lib/pleroma/web/controller_helper.ex
+++ b/lib/pleroma/web/controller_helper.ex
@@ -20,7 +20,7 @@ defmodule Pleroma.Web.ControllerHelper do
|> json(json)
end
- @spec fetch_integer_param(map(), String.t(), integer() | nil) :: integer() | nil
+ @spec fetch_integer_param(map(), String.t() | atom(), integer() | nil) :: integer() | nil
def fetch_integer_param(params, name, default \\ nil) do
params
|> Map.get(name, default)
@@ -53,10 +53,15 @@ defmodule Pleroma.Web.ControllerHelper do
end
end
+ # TODO: Only fetch the params from open_api_spex when everything is converted
@id_keys Pagination.page_keys() -- ["limit", "order"]
defp build_pagination_fields(conn, min_id, max_id, extra_params) do
params =
- conn.params
+ if Map.has_key?(conn.private, :open_api_spex) do
+ get_in(conn, [Access.key(:private), Access.key(:open_api_spex), Access.key(:params)])
+ else
+ conn.params
+ end
|> Map.drop(Map.keys(conn.path_params) |> Enum.map(&String.to_existing_atom/1))
|> Map.merge(extra_params)
|> Map.drop(@id_keys)
@@ -85,18 +90,15 @@ defmodule Pleroma.Web.ControllerHelper do
end
end
- def assign_account_by_id(conn, _) do
- case Pleroma.User.get_cached_by_id(conn.params.id) do
+ def assign_account_by_id(%{private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
+ case Pleroma.User.get_cached_by_id(id) do
%Pleroma.User{} = account -> assign(conn, :account, account)
nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
end
end
def try_render(conn, target, params) when is_binary(target) do
- case render(conn, target, params) do
- nil -> render_error(conn, :not_implemented, "Can't display this activity")
- res -> res
- end
+ render(conn, target, params)
end
def try_render(conn, _, _) do
diff --git a/lib/pleroma/web/embed_controller.ex b/lib/pleroma/web/embed_controller.ex
index 8b9f0a051..2ca4501a6 100644
--- a/lib/pleroma/web/embed_controller.ex
+++ b/lib/pleroma/web/embed_controller.ex
@@ -11,12 +11,10 @@ defmodule Pleroma.Web.EmbedController do
alias Pleroma.Web.ActivityPub.Visibility
- plug(:put_layout, :embed)
-
def show(conn, %{"id" => id}) do
with %Activity{local: true} = activity <-
Activity.get_by_id_with_object(id),
- true <- Visibility.is_public?(activity.object) do
+ true <- Visibility.public?(activity.object) do
{:ok, author} = User.get_or_fetch(activity.object.data["actor"])
conn
diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex
index 574f3ab63..fef907ace 100644
--- a/lib/pleroma/web/endpoint.ex
+++ b/lib/pleroma/web/endpoint.ex
@@ -9,17 +9,44 @@ defmodule Pleroma.Web.Endpoint do
alias Pleroma.Config
- socket("/socket", Pleroma.Web.UserSocket)
+ socket("/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler,
+ longpoll: false,
+ websocket: [
+ path: "/",
+ compress: false,
+ error_handler: {Pleroma.Web.MastodonAPI.WebsocketHandler, :handle_error, []},
+ fullsweep_after: 20
+ ]
+ )
+
+ socket("/socket", Pleroma.Web.UserSocket,
+ websocket: [
+ path: "/websocket",
+ serializer: [
+ {Phoenix.Socket.V1.JSONSerializer, "~> 1.0.0"},
+ {Phoenix.Socket.V2.JSONSerializer, "~> 2.0.0"}
+ ],
+ timeout: 60_000,
+ transport_log: false,
+ compress: false,
+ fullsweep_after: 20
+ ],
+ longpoll: false
+ )
+
socket("/live", Phoenix.LiveView.Socket)
plug(Plug.Telemetry, event_prefix: [:phoenix, :endpoint])
+ plug(Pleroma.Web.Plugs.LoggerMetadataPath)
+
plug(Pleroma.Web.Plugs.SetLocalePlug)
plug(CORSPlug)
plug(Pleroma.Web.Plugs.HTTPSecurityPlug)
plug(Pleroma.Web.Plugs.UploadedMedia)
- @static_cache_control "public, no-cache"
+ @static_cache_control "public, max-age=1209600"
+ @static_cache_disabled "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
@@ -30,22 +57,32 @@ defmodule Pleroma.Web.Endpoint do
from: :pleroma,
only: ["emoji", "images"],
gzip: true,
- cache_control_for_etags: "public, max-age=1209600",
+ cache_control_for_etags: @static_cache_control,
headers: %{
- "cache-control" => "public, max-age=1209600"
+ "cache-control" => @static_cache_control
}
)
plug(Pleroma.Web.Plugs.InstanceStatic,
at: "/",
gzip: true,
- cache_control_for_etags: @static_cache_control,
+ cache_control_for_etags: @static_cache_disabled,
headers: %{
- "cache-control" => @static_cache_control
+ "cache-control" => @static_cache_disabled
+ }
+ )
+
+ plug(Pleroma.Web.Plugs.FrontendStatic,
+ at: "/",
+ frontend_type: :primary,
+ only: ["index.html"],
+ gzip: true,
+ cache_control_for_etags: @static_cache_disabled,
+ headers: %{
+ "cache-control" => @static_cache_disabled
}
)
- # Careful! No `only` restriction here, as we don't know what frontends contain.
plug(Pleroma.Web.Plugs.FrontendStatic,
at: "/",
frontend_type: :primary,
@@ -62,9 +99,9 @@ defmodule Pleroma.Web.Endpoint do
at: "/pleroma/admin",
frontend_type: :admin,
gzip: true,
- cache_control_for_etags: @static_cache_control,
+ cache_control_for_etags: @static_cache_disabled,
headers: %{
- "cache-control" => @static_cache_control
+ "cache-control" => @static_cache_disabled
}
)
@@ -79,9 +116,9 @@ defmodule Pleroma.Web.Endpoint do
only: Pleroma.Constants.static_only_files(),
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
gzip: true,
- cache_control_for_etags: @static_cache_control,
+ cache_control_for_etags: @static_cache_disabled,
headers: %{
- "cache-control" => @static_cache_control
+ "cache-control" => @static_cache_disabled
}
)
@@ -138,47 +175,6 @@ defmodule Pleroma.Web.Endpoint do
plug(Pleroma.Web.Plugs.RemoteIp)
- defmodule Instrumenter do
- use Prometheus.PhoenixInstrumenter
- end
-
- defmodule PipelineInstrumenter do
- use Prometheus.PlugPipelineInstrumenter
- end
-
- defmodule MetricsExporter 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(MetricsExporterCaller)
-
plug(Pleroma.Web.Router)
@doc """
diff --git a/lib/pleroma/web/fallback/redirect_controller.ex b/lib/pleroma/web/fallback/redirect_controller.ex
index 1a86f7a53..4a0885fab 100644
--- a/lib/pleroma/web/fallback/redirect_controller.ex
+++ b/lib/pleroma/web/fallback/redirect_controller.ex
@@ -17,10 +17,28 @@ defmodule Pleroma.Web.Fallback.RedirectController do
|> json(%{error: "Not implemented"})
end
+ def add_generated_metadata(page_content, extra \\ "") do
+ title = "<title>#{Pleroma.Config.get([:instance, :name])}</title>"
+ favicon = "<link rel='icon' href='#{Pleroma.Config.get([:instance, :favicon])}'>"
+ manifest = "<link rel='manifest' href='/manifest.json'>"
+
+ page_content
+ |> String.replace(
+ "<!--server-generated-meta-->",
+ title <> favicon <> manifest <> extra
+ )
+ end
+
def redirector(conn, _params, code \\ 200) do
+ {:ok, index_content} = File.read(index_file_path())
+
+ response =
+ index_content
+ |> add_generated_metadata()
+
conn
|> put_resp_content_type("text/html")
- |> send_file(code, index_file_path())
+ |> send_resp(code, response)
end
def redirector_with_meta(conn, %{"maybe_nickname_or_id" => maybe_nickname_or_id} = params) do
@@ -34,14 +52,12 @@ defmodule Pleroma.Web.Fallback.RedirectController do
def redirector_with_meta(conn, params) do
{:ok, index_content} = File.read(index_file_path())
-
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 <> title)
+ |> add_generated_metadata(tags <> preloads)
conn
|> put_resp_content_type("text/html")
@@ -55,11 +71,10 @@ defmodule Pleroma.Web.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 <> title)
+ |> add_generated_metadata(preloads)
conn
|> put_resp_content_type("text/html")
diff --git a/lib/pleroma/web/federator.ex b/lib/pleroma/web/federator.ex
index 84b77cda1..3d3101d61 100644
--- a/lib/pleroma/web/federator.ex
+++ b/lib/pleroma/web/federator.ex
@@ -6,9 +6,9 @@ defmodule Pleroma.Web.Federator do
alias Pleroma.Activity
alias Pleroma.Object.Containment
alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.Publisher
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils
- alias Pleroma.Web.Federator.Publisher
alias Pleroma.Workers.PublisherWorker
alias Pleroma.Workers.ReceiverWorker
@@ -35,6 +35,19 @@ defmodule Pleroma.Web.Federator do
end
# Client API
+ def incoming_ap_doc(%{params: _params, req_headers: _req_headers} = args) do
+ job_args = Enum.into(args, %{}, fn {k, v} -> {Atom.to_string(k), v} end)
+
+ ReceiverWorker.enqueue(
+ "incoming_ap_doc",
+ Map.put(job_args, "timeout", :timer.seconds(20)),
+ priority: 2
+ )
+ end
+
+ def incoming_ap_doc(%{"type" => "Delete"} = params) do
+ ReceiverWorker.enqueue("incoming_ap_doc", %{"params" => params}, priority: 3, queue: :slow)
+ end
def incoming_ap_doc(params) do
ReceiverWorker.enqueue("incoming_ap_doc", %{"params" => params})
@@ -57,10 +70,8 @@ defmodule Pleroma.Web.Federator do
# Job Worker Callbacks
- @spec perform(atom(), module(), any()) :: {:ok, any()} | {:error, any()}
- def perform(:publish_one, module, params) do
- apply(module, :publish_one, [params])
- end
+ @spec perform(atom(), any()) :: {:ok, any()} | {:error, any()}
+ def perform(:publish_one, params), do: Publisher.publish_one(params)
def perform(:publish, activity) do
Logger.debug(fn -> "Running publish for #{activity.data["id"]}" end)
diff --git a/lib/pleroma/web/federator/publisher.ex b/lib/pleroma/web/federator/publisher.ex
deleted file mode 100644
index a45796e9d..000000000
--- a/lib/pleroma/web/federator/publisher.ex
+++ /dev/null
@@ -1,109 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.Federator.Publisher do
- alias Pleroma.Activity
- alias Pleroma.Config
- alias Pleroma.User
- alias Pleroma.Workers.PublisherWorker
-
- require Logger
-
- @moduledoc """
- Defines the contract used by federation implementations to publish messages to
- their peers.
- """
-
- @doc """
- Determine whether an activity can be relayed using the federation module.
- """
- @callback is_representable?(Pleroma.Activity.t()) :: boolean()
-
- @doc """
- Relays an activity to a specified peer, determined by the parameters. The
- parameters used are controlled by the federation module.
- """
- @callback publish_one(Map.t()) :: {:ok, Map.t()} | {:error, any()}
-
- @doc """
- Enqueue publishing a single activity.
- """
- @spec enqueue_one(module(), Map.t()) :: :ok
- def enqueue_one(module, %{} = params) do
- PublisherWorker.enqueue(
- "publish_one",
- %{"module" => to_string(module), "params" => params}
- )
- end
-
- @doc """
- Relays an activity to all specified peers.
- """
- @callback publish(User.t(), Activity.t()) :: :ok | {:error, any()}
-
- @spec publish(User.t(), Activity.t()) :: :ok
- def publish(%User{} = user, %Activity{} = activity) do
- Config.get([:instance, :federation_publisher_modules])
- |> Enum.each(fn module ->
- if module.is_representable?(activity) do
- Logger.debug("Publishing #{activity.data["id"]} using #{inspect(module)}")
- module.publish(user, activity)
- end
- end)
-
- :ok
- end
-
- @doc """
- Gathers links used by an outgoing federation module for WebFinger output.
- """
- @callback gather_webfinger_links(User.t()) :: list()
-
- @spec gather_webfinger_links(User.t()) :: list()
- def gather_webfinger_links(%User{} = user) do
- Config.get([:instance, :federation_publisher_modules])
- |> Enum.reduce([], fn module, links ->
- links ++ module.gather_webfinger_links(user)
- end)
- end
-
- @doc """
- Gathers nodeinfo protocol names supported by the federation module.
- """
- @callback gather_nodeinfo_protocol_names() :: list()
-
- @spec gather_nodeinfo_protocol_names() :: list()
- def gather_nodeinfo_protocol_names do
- Config.get([:instance, :federation_publisher_modules])
- |> Enum.reduce([], fn module, links ->
- links ++ module.gather_nodeinfo_protocol_names()
- end)
- end
-
- @doc """
- Gathers a set of remote users given an IR envelope.
- """
- def remote_users(%User{id: user_id}, %{data: %{"to" => to} = data}) do
- cc = Map.get(data, "cc", [])
-
- bcc =
- data
- |> Map.get("bcc", [])
- |> Enum.reduce([], fn ap_id, bcc ->
- case Pleroma.List.get_by_ap_id(ap_id) do
- %Pleroma.List{user_id: ^user_id} = list ->
- {:ok, following} = Pleroma.List.get_following(list)
- bcc ++ Enum.map(following, & &1.ap_id)
-
- _ ->
- bcc
- end
- end)
-
- [to, cc, bcc]
- |> Enum.concat()
- |> Enum.map(&User.get_cached_by_ap_id/1)
- |> Enum.filter(fn user -> user && !user.local end)
- end
-end
diff --git a/lib/pleroma/web/feed/feed_view.ex b/lib/pleroma/web/feed/feed_view.ex
index 034722eb2..e1ee33d62 100644
--- a/lib/pleroma/web/feed/feed_view.ex
+++ b/lib/pleroma/web/feed/feed_view.ex
@@ -132,7 +132,7 @@ defmodule Pleroma.Web.Feed.FeedView do
|> safe_to_string()
end
- @spec to_rfc3339(String.t() | NativeDateTime.t()) :: String.t()
+ @spec to_rfc3339(String.t() | NaiveDateTime.t()) :: String.t()
def to_rfc3339(date) when is_binary(date) do
date
|> Timex.parse!("{ISO:Extended}")
@@ -145,7 +145,7 @@ defmodule Pleroma.Web.Feed.FeedView do
|> Timex.format!("{RFC3339}")
end
- @spec to_rfc2822(String.t() | DateTime.t() | NativeDateTime.t()) :: String.t()
+ @spec to_rfc2822(String.t() | DateTime.t() | NaiveDateTime.t()) :: String.t()
def to_rfc2822(datestr) when is_binary(datestr) do
datestr
|> Timex.parse!("{ISO:Extended}")
diff --git a/lib/pleroma/web/gettext.ex b/lib/pleroma/web/gettext.ex
index 5ef49d841..1fa3f9768 100644
--- a/lib/pleroma/web/gettext.ex
+++ b/lib/pleroma/web/gettext.ex
@@ -85,12 +85,12 @@ defmodule Pleroma.Web.Gettext do
Process.get({Pleroma.Web.Gettext, :locales}, [])
end
- def is_locale_list(locales) do
+ def locale_list?(locales) do
Enum.all?(locales, &is_binary/1)
end
def put_locales(locales) do
- if is_locale_list(locales) do
+ if locale_list?(locales) do
Process.put({Pleroma.Web.Gettext, :locales}, Enum.uniq(locales))
Gettext.put_locale(Enum.at(locales, 0, Gettext.get_locale()))
:ok
diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
index 9a4b56301..80ab95a57 100644
--- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
@@ -30,7 +30,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
alias Pleroma.Web.TwitterAPI.TwitterAPI
alias Pleroma.Web.Utils.Params
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(:skip_auth when action in [:create, :lookup])
@@ -72,7 +72,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
%{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock]
)
- plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["read:follows"]} when action in [:relationships, :familiar_followers]
+ )
plug(
OAuthScopesPlug,
@@ -92,7 +95,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
plug(
RateLimiter,
- [name: :relation_id_action, params: [:id, :uri]] when action in @relationship_actions
+ [name: :relation_id_action, params: ["id", "uri"]] when action in @relationship_actions
)
plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
@@ -104,7 +107,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
@doc "POST /api/v1/accounts"
- def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
+ def create(
+ %{assigns: %{app: app}, private: %{open_api_spex: %{body_params: params}}} = conn,
+ _params
+ ) do
with :ok <- validate_email_param(params),
:ok <- TwitterAPI.validate_captcha(app, params),
{:ok, user} <- TwitterAPI.register_user(params),
@@ -168,7 +174,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
@doc "PATCH /api/v1/accounts/update_credentials"
- def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _params) do
+ def update_credentials(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: params}}} = conn,
+ _params
+ ) do
params =
params
|> Enum.filter(fn {_, value} -> not is_nil(value) end)
@@ -235,7 +244,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
# So we first build the normal local changeset, then apply it to the
# user data, but don't persist it. With this, we generate the object
# data for our update activity. We feed this and the changeset as meta
- # inforation into the pipeline, where they will be properly updated and
+ # information into the pipeline, where they will be properly updated and
# federated.
with changeset <- User.update_changeset(user, user_params),
{:ok, unpersisted_user} <- Ecto.Changeset.apply_action(changeset, :update),
@@ -289,7 +298,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
@doc "GET /api/v1/accounts/relationships"
- def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
+ def relationships(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
+ _
+ ) do
targets = User.get_all_by_ids(List.wrap(id))
render(conn, "relationships.json", user: user, targets: targets)
@@ -299,7 +311,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
@doc "GET /api/v1/accounts/:id"
- def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id} = params) do
+ def show(
+ %{
+ assigns: %{user: for_user},
+ private: %{open_api_spex: %{params: %{id: nickname_or_id} = params}}
+ } = conn,
+ _params
+ ) do
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
:visible <- User.visible_for(user, for_user) do
render(conn, "show.json",
@@ -313,7 +331,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
@doc "GET /api/v1/accounts/:id/statuses"
- def statuses(%{assigns: %{user: reading_user}} = conn, params) do
+ def statuses(
+ %{assigns: %{user: reading_user}, private: %{open_api_spex: %{params: params}}} = conn,
+ _params
+ ) do
with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
:visible <- User.visible_for(user, reading_user) do
params =
@@ -348,7 +369,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
@doc "GET /api/v1/accounts/:id/followers"
- def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
+ def followers(
+ %{assigns: %{user: for_user, account: user}, private: %{open_api_spex: %{params: params}}} =
+ conn,
+ _params
+ ) do
params =
params
|> Enum.map(fn {key, value} -> {to_string(key), value} end)
@@ -373,7 +398,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
@doc "GET /api/v1/accounts/:id/following"
- def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
+ def following(
+ %{assigns: %{user: for_user, account: user}, private: %{open_api_spex: %{params: params}}} =
+ conn,
+ _params
+ ) do
params =
params
|> Enum.map(fn {key, value} -> {to_string(key), value} end)
@@ -411,7 +440,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
{:error, "Can not follow yourself"}
end
- def follow(%{body_params: params, assigns: %{user: follower, account: followed}} = conn, _) do
+ def follow(
+ %{
+ assigns: %{user: follower, account: followed},
+ private: %{open_api_spex: %{body_params: params}}
+ } = conn,
+ _
+ ) do
with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
render(conn, "relationship.json", user: follower, target: followed)
else
@@ -425,13 +460,19 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
- with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
+ with {:ok, follower} <- CommonAPI.unfollow(followed, follower) do
render(conn, "relationship.json", user: follower, target: followed)
end
end
@doc "POST /api/v1/accounts/:id/mute"
- def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
+ def mute(
+ %{
+ assigns: %{user: muter, account: muted},
+ private: %{open_api_spex: %{body_params: params}}
+ } = conn,
+ _params
+ ) do
params =
params
|> Map.put_new(:duration, Map.get(params, :expires_in, 0))
@@ -454,7 +495,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
@doc "POST /api/v1/accounts/:id/block"
def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
- with {:ok, _activity} <- CommonAPI.block(blocker, blocked) do
+ with {:ok, _activity} <- CommonAPI.block(blocked, blocker) do
render(conn, "relationship.json", user: blocker, target: blocked)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
@@ -463,7 +504,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
@doc "POST /api/v1/accounts/:id/unblock"
def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
- with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
+ with {:ok, _activity} <- CommonAPI.unblock(blocked, blocker) do
render(conn, "relationship.json", user: blocker, target: blocked)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
@@ -472,7 +513,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
@doc "POST /api/v1/accounts/:id/note"
def note(
- %{assigns: %{user: noter, account: target}, body_params: %{comment: comment}} = conn,
+ %{
+ assigns: %{user: noter, account: target},
+ private: %{open_api_spex: %{body_params: %{comment: comment}}}
+ } = conn,
_params
) do
with {:ok, _user_note} <- UserNote.create(noter, target, comment) do
@@ -513,7 +557,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
@doc "POST /api/v1/follows"
- def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
+ def follow_by_uri(%{private: %{open_api_spex: %{body_params: %{uri: uri}}}} = conn, _) do
case User.get_cached_by_nickname(uri) do
%User{} = user ->
conn
@@ -561,7 +605,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
@doc "GET /api/v1/accounts/lookup"
- def lookup(conn, %{acct: nickname} = _params) do
+ def lookup(%{private: %{open_api_spex: %{params: %{acct: nickname}}}} = conn, _params) do
with %User{} = user <- User.get_by_nickname(nickname) do
render(conn, "show.json",
user: user,
@@ -588,6 +632,35 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
)
end
+ @doc "GET /api/v1/accounts/familiar_followers"
+ def familiar_followers(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
+ _id
+ ) do
+ users =
+ User.get_all_by_ids(List.wrap(id))
+ |> Enum.map(&%{id: &1.id, accounts: get_familiar_followers(&1, user)})
+
+ conn
+ |> render("familiar_followers.json",
+ for: user,
+ users: users,
+ as: :user
+ )
+ end
+
+ defp get_familiar_followers(%{id: id} = user, %{id: id}) do
+ User.get_familiar_followers(user, user)
+ end
+
+ defp get_familiar_followers(%{hide_followers: true}, _current_user) do
+ []
+ end
+
+ defp get_familiar_followers(user, current_user) do
+ User.get_familiar_followers(user, current_user)
+ end
+
@doc "GET /api/v1/identity_proofs"
def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/directory_controller.ex b/lib/pleroma/web/mastodon_api/controllers/directory_controller.ex
index 253f06cfb..f89425966 100644
--- a/lib/pleroma/web/mastodon_api/controllers/directory_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/directory_controller.ex
@@ -15,7 +15,7 @@ defmodule Pleroma.Web.MastodonAPI.DirectoryController do
plug(Pleroma.Web.ApiSpec.CastAndValidate)
- plug(:skip_auth when action == "index")
+ plug(:skip_auth when action == :index)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.DirectoryOperation
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 b2e347ed9..4615794a1 100644
--- a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
@@ -8,7 +8,7 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockController do
alias Pleroma.User
alias Pleroma.Web.Plugs.OAuthScopesPlug
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.DomainBlockOperation
plug(
@@ -27,23 +27,31 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockController do
end
@doc "POST /api/v1/domain_blocks"
- def create(%{assigns: %{user: blocker}, body_params: %{domain: domain}} = conn, _params) do
+ def create(
+ %{assigns: %{user: blocker}, private: %{open_api_spex: %{body_params: %{domain: domain}}}} =
+ conn,
+ _params
+ ) do
User.block_domain(blocker, domain)
json(conn, %{})
end
- def create(%{assigns: %{user: blocker}} = conn, %{domain: domain}) do
+ def create(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
User.block_domain(blocker, domain)
json(conn, %{})
end
@doc "DELETE /api/v1/domain_blocks"
- def delete(%{assigns: %{user: blocker}, body_params: %{domain: domain}} = conn, _params) do
+ def delete(
+ %{assigns: %{user: blocker}, private: %{open_api_spex: %{body_params: %{domain: domain}}}} =
+ conn,
+ _params
+ ) do
User.unblock_domain(blocker, domain)
json(conn, %{})
end
- def delete(%{assigns: %{user: blocker}} = conn, %{domain: domain}) do
+ def delete(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
User.unblock_domain(blocker, domain)
json(conn, %{})
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex
index ba6d074cc..6eee55d1b 100644
--- a/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex
@@ -9,7 +9,7 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestController do
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Plugs.OAuthScopesPlug
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(:assign_follower when action != :index)
action_fallback(:errors)
@@ -44,7 +44,7 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestController do
end
end
- defp assign_follower(%{params: %{id: id}} = conn, _) do
+ defp assign_follower(%{private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
case User.get_cached_by_id(id) do
%User{} = follower -> assign(conn, :follower, follower)
nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
diff --git a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex
index 6410e872c..b97b0e476 100644
--- a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex
@@ -7,7 +7,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceController do
plug(Pleroma.Web.ApiSpec.CastAndValidate)
- plug(:skip_auth when action in [:show, :peers])
+ plug(:skip_auth when action in [:show, :show2, :peers])
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.InstanceOperation
@@ -16,8 +16,18 @@ defmodule Pleroma.Web.MastodonAPI.InstanceController do
render(conn, "show.json")
end
+ @doc "GET /api/v2/instance"
+ def show2(conn, _params) do
+ render(conn, "show2.json")
+ end
+
@doc "GET /api/v1/instance/peers"
def peers(conn, _params) do
json(conn, Pleroma.Stats.get_peers())
end
+
+ @doc "GET /api/v1/instance/rules"
+ def rules(conn, _params) do
+ render(conn, "rules.json")
+ end
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex
index 2117aae3a..3bfc365a5 100644
--- a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex
@@ -11,7 +11,7 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
@oauth_read_actions [:index, :show, :list_accounts]
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(:list_by_id_and_user when action not in [:index, :create])
plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action in @oauth_read_actions)
plug(OAuthScopesPlug, %{scopes: ["write:lists"]} when action not in @oauth_read_actions)
@@ -21,25 +21,33 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ListOperation
# GET /api/v1/lists
- def index(%{assigns: %{user: user}} = conn, opts) do
- lists = Pleroma.List.for_user(user, opts)
+ def index(%{assigns: %{user: user}, private: %{open_api_spex: %{params: params}}} = conn, _) do
+ lists = Pleroma.List.for_user(user, params)
render(conn, "index.json", lists: lists)
end
# POST /api/v1/lists
- def create(%{assigns: %{user: user}, body_params: %{title: title}} = conn, _) do
+ def create(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: %{title: title}}}} =
+ conn,
+ _
+ ) do
with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
render(conn, "show.json", list: list)
end
end
- # GET /api/v1/lists/:id
+ # GET /api/v1/lists/:idOB
def show(%{assigns: %{list: list}} = conn, _) do
render(conn, "show.json", list: list)
end
# PUT /api/v1/lists/:id
- def update(%{assigns: %{list: list}, body_params: %{title: title}} = conn, _) do
+ def update(
+ %{assigns: %{list: list}, private: %{open_api_spex: %{body_params: %{title: title}}}} =
+ conn,
+ _
+ ) do
with {:ok, list} <- Pleroma.List.rename(list, title) do
render(conn, "show.json", list: list)
end
@@ -62,7 +70,13 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
end
# POST /api/v1/lists/:id/accounts
- def add_to_list(%{assigns: %{list: list}, body_params: %{account_ids: account_ids}} = conn, _) do
+ def add_to_list(
+ %{
+ assigns: %{list: list},
+ private: %{open_api_spex: %{body_params: %{account_ids: account_ids}}}
+ } = conn,
+ _
+ ) do
Enum.each(account_ids, fn account_id ->
with %User{} = followed <- User.get_cached_by_id(account_id) do
Pleroma.List.follow(list, followed)
@@ -74,9 +88,22 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
# DELETE /api/v1/lists/:id/accounts
def remove_from_list(
- %{assigns: %{list: list}, params: %{account_ids: account_ids}} = conn,
+ %{
+ private: %{open_api_spex: %{params: %{account_ids: account_ids}}}
+ } = conn,
_
) do
+ do_remove_from_list(conn, account_ids)
+ end
+
+ def remove_from_list(
+ %{private: %{open_api_spex: %{body_params: %{account_ids: account_ids}}}} = conn,
+ _
+ ) do
+ do_remove_from_list(conn, account_ids)
+ end
+
+ defp do_remove_from_list(%{assigns: %{list: list}} = conn, account_ids) do
Enum.each(account_ids, fn account_id ->
with %User{} = followed <- User.get_cached_by_id(account_id) do
Pleroma.List.unfollow(list, followed)
@@ -86,11 +113,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
+ defp list_by_id_and_user(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
+ _
+ ) do
case Pleroma.List.get(id, user) do
%Pleroma.List{} = list -> assign(conn, :list, list)
nil -> conn |> render_error(:not_found, "List not found") |> halt()
diff --git a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex
index 7d9a63cf4..056bad844 100644
--- a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex
@@ -12,7 +12,7 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:create, :create2])
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(OAuthScopesPlug, %{scopes: ["read:media"]} when action == :show)
plug(OAuthScopesPlug, %{scopes: ["write:media"]} when action != :show)
@@ -20,7 +20,11 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.MediaOperation
@doc "POST /api/v1/media"
- def create(%{assigns: %{user: user}, body_params: %{file: file} = data} = conn, _) do
+ def create(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: %{file: file} = data}}} =
+ conn,
+ _
+ ) do
with {:ok, object} <-
ActivityPub.upload(
file,
@@ -36,7 +40,11 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do
def create(_conn, _data), do: {:error, :bad_request}
@doc "POST /api/v2/media"
- def create2(%{assigns: %{user: user}, body_params: %{file: file} = data} = conn, _) do
+ def create2(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: %{file: file} = data}}} =
+ conn,
+ _
+ ) do
with {:ok, object} <-
ActivityPub.upload(
file,
@@ -54,7 +62,15 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do
def create2(_conn, _data), do: {:error, :bad_request}
@doc "PUT /api/v1/media/:id"
- def update(%{assigns: %{user: user}, body_params: %{description: description}} = conn, %{id: id}) do
+ def update(
+ %{
+ assigns: %{user: user},
+ private: %{
+ open_api_spex: %{body_params: %{description: description}, params: %{id: id}}
+ }
+ } = conn,
+ _
+ ) do
with %Object{} = object <- Object.get_by_id(id),
:ok <- Object.authorize_access(object, user),
{:ok, %Object{data: data}} <- Object.update_data(object, %{"name" => description}) do
@@ -67,7 +83,7 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do
def update(conn, data), do: show(conn, data)
@doc "GET /api/v1/media/:id"
- def show(%{assigns: %{user: user}} = conn, %{id: id}) do
+ def show(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
with %Object{data: data, id: object_id} = object <- Object.get_by_id(id),
:ok <- Object.authorize_access(object, user) do
attachment_data = Map.put(data, "id", object_id)
diff --git a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
index a490e8319..afd83b785 100644
--- a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
@@ -13,7 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
@oauth_read_actions [:show, :index]
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(
OAuthScopesPlug,
@@ -24,8 +24,21 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.NotificationOperation
+ @default_notification_types ~w{
+ mention
+ follow
+ follow_request
+ reblog
+ favourite
+ move
+ pleroma:emoji_reaction
+ poll
+ update
+ status
+ }
+
# GET /api/v1/notifications
- def index(conn, %{account_id: account_id} = params) do
+ def index(%{private: %{open_api_spex: %{params: %{account_id: account_id} = params}}} = conn, _) do
case Pleroma.User.get_cached_by_id(account_id) do
%{ap_id: account_ap_id} ->
params =
@@ -33,7 +46,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
|> Map.delete(:account_id)
|> Map.put(:account_ap_id, account_ap_id)
- index(conn, params)
+ do_get_notifications(conn, params)
_ ->
conn
@@ -42,18 +55,11 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
end
end
- @default_notification_types ~w{
- mention
- follow
- follow_request
- reblog
- favourite
- move
- pleroma:emoji_reaction
- poll
- update
- }
- def index(%{assigns: %{user: user}} = conn, params) do
+ def index(%{private: %{open_api_spex: %{params: params}}} = conn, _) do
+ do_get_notifications(conn, params)
+ end
+
+ defp do_get_notifications(%{assigns: %{user: user}} = conn, params) do
params =
Map.new(params, fn {k, v} -> {to_string(k), v} end)
|> Map.put_new("types", Map.get(params, :include_types, @default_notification_types))
@@ -69,7 +75,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
end
# GET /api/v1/notifications/:id
- def show(%{assigns: %{user: user}} = conn, %{id: id}) do
+ def show(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
with {:ok, notification} <- Notification.get(user, id) do
render(conn, "show.json", notification: notification, for: user)
else
@@ -88,8 +94,20 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
# POST /api/v1/notifications/:id/dismiss
- def dismiss(%{assigns: %{user: user}} = conn, %{id: id} = _params) do
- with {:ok, _notif} <- Notification.dismiss(user, id) do
+ def dismiss(%{private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
+ do_dismiss(conn, id)
+ end
+
+ # POST /api/v1/notifications/dismiss (deprecated)
+ def dismiss_via_body(
+ %{private: %{open_api_spex: %{body_params: %{id: id}}}} = conn,
+ _
+ ) do
+ do_dismiss(conn, id)
+ end
+
+ defp do_dismiss(%{assigns: %{user: user}} = conn, notification_id) do
+ with {:ok, _notif} <- Notification.dismiss(user, notification_id) do
json(conn, %{})
else
{:error, reason} ->
@@ -99,13 +117,11 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
end
end
- # POST /api/v1/notifications/dismiss (deprecated)
- def dismiss_via_body(%{body_params: params} = conn, _) do
- dismiss(conn, params)
- end
-
# DELETE /api/v1/notifications/destroy_multiple
- def destroy_multiple(%{assigns: %{user: user}} = conn, %{ids: ids} = _params) do
+ def destroy_multiple(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{ids: ids}}}} = conn,
+ _
+ ) do
Notification.destroy_multiple(user, ids)
json(conn, %{})
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex
index 002c210d2..a2af8148c 100644
--- a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex
@@ -15,7 +15,7 @@ defmodule Pleroma.Web.MastodonAPI.PollController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(
OAuthScopesPlug,
@@ -29,7 +29,7 @@ defmodule Pleroma.Web.MastodonAPI.PollController do
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
@doc "GET /api/v1/polls/:id"
- def show(%{assigns: %{user: user}} = conn, %{id: id}) do
+ def show(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
true <- Visibility.visible_for_user?(activity, user) do
@@ -41,11 +41,17 @@ defmodule Pleroma.Web.MastodonAPI.PollController do
end
@doc "POST /api/v1/polls/:id/votes"
- def vote(%{assigns: %{user: user}, body_params: %{choices: choices}} = conn, %{id: id}) do
+ def vote(
+ %{
+ assigns: %{user: user},
+ private: %{open_api_spex: %{body_params: %{choices: choices}, params: %{id: id}}}
+ } = conn,
+ _
+ ) do
with %Object{data: %{"type" => "Question"}} = object <- Object.get_by_id(id),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
true <- Visibility.visible_for_user?(activity, user),
- {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
+ {:ok, _activities, object} <- get_cached_vote_or_vote(object, user, choices) do
try_render(conn, "show.json", %{object: object, for: user})
else
nil -> render_error(conn, :not_found, "Record not found")
@@ -54,11 +60,11 @@ defmodule Pleroma.Web.MastodonAPI.PollController do
end
end
- defp get_cached_vote_or_vote(user, object, choices) do
+ defp get_cached_vote_or_vote(object, user, choices) do
idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
@cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
- case CommonAPI.vote(user, object, choices) do
+ case CommonAPI.vote(object, user, choices) do
{:error, _message} = res -> {:ignore, res}
res -> {:commit, res}
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex b/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
index 0392fcef1..1b7095ec5 100644
--- a/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
@@ -13,7 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityController do
@oauth_read_actions [:show, :index]
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in @oauth_read_actions)
plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action not in @oauth_read_actions)
plug(:assign_scheduled_activity when action != :index)
@@ -23,7 +23,7 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityController do
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ScheduledActivityOperation
@doc "GET /api/v1/scheduled_statuses"
- def index(%{assigns: %{user: user}} = conn, params) do
+ def index(%{assigns: %{user: user}, private: %{open_api_spex: %{params: params}}} = conn, _) do
params = Map.new(params, fn {key, value} -> {to_string(key), value} end)
with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
@@ -39,7 +39,13 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityController do
end
@doc "PUT /api/v1/scheduled_statuses/:id"
- def update(%{assigns: %{scheduled_activity: scheduled_activity}, body_params: params} = conn, _) do
+ def update(
+ %{
+ assigns: %{scheduled_activity: scheduled_activity},
+ private: %{open_api_spex: %{body_params: params}}
+ } = conn,
+ _
+ ) do
with {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
render(conn, "show.json", scheduled_activity: scheduled_activity)
end
@@ -52,7 +58,10 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityController do
end
end
- defp assign_scheduled_activity(%{assigns: %{user: user}, params: %{id: id}} = conn, _) do
+ defp assign_scheduled_activity(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
+ _
+ ) do
case ScheduledActivity.get(user, id) do
%ScheduledActivity{} = activity -> assign(conn, :scheduled_activity, activity)
nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
index 5e6e04734..628aa311b 100644
--- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
@@ -5,7 +5,6 @@
defmodule Pleroma.Web.MastodonAPI.SearchController do
use Pleroma.Web, :controller
- alias Pleroma.Activity
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ControllerHelper
@@ -19,7 +18,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
@search_limit 40
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
# Note: Mastodon doesn't allow unauthenticated access (requires read:accounts / read:search)
plug(OAuthScopesPlug, %{scopes: ["read:search"], fallback: :proceed_unauthenticated})
@@ -30,7 +29,11 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.SearchOperation
- def account_search(%{assigns: %{user: user}} = conn, %{q: query} = params) do
+ def account_search(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{q: query} = params}}} =
+ conn,
+ _
+ ) do
accounts = User.search(query, search_options(params, user))
conn
@@ -45,7 +48,12 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
def search2(conn, params), do: do_search(:v2, conn, params)
def search(conn, params), do: do_search(:v1, conn, params)
- defp do_search(version, %{assigns: %{user: user}} = conn, %{q: query} = params) do
+ defp do_search(
+ version,
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{q: query} = params}}} =
+ conn,
+ _
+ ) do
query = String.trim(query)
options = search_options(params, user)
timeout = Keyword.get(Repo.config(), :timeout, 15_000)
@@ -100,7 +108,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
end
defp resource_search(_, "statuses", query, options) do
- statuses = with_fallback(fn -> Activity.search(options[:for_user], query, options) end)
+ statuses = with_fallback(fn -> Pleroma.Search.search(query, options) end)
StatusView.render("index.json",
activities: statuses,
@@ -148,7 +156,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
tags
end
- Pleroma.Pagination.paginate(tags, options)
+ Pleroma.Pagination.paginate_list(tags, options)
end
defp add_joined_tag(tags) do
diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
index e594ea491..b9b236920 100644
--- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
@@ -12,6 +12,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
alias Pleroma.Activity
alias Pleroma.Bookmark
+ alias Pleroma.BookmarkFolder
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.ScheduledActivity
@@ -25,7 +26,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.Plugs.RateLimiter
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(:skip_public_check when action in [:index, :show])
@@ -37,7 +38,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
when action in [
:index,
:show,
- :card,
:context,
:show_history,
:show_source
@@ -110,7 +110,11 @@ 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}, private: %{open_api_spex: %{params: %{ids: ids} = params}}} =
+ conn,
+ _
+ ) do
limit = 100
activities =
@@ -134,7 +138,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
def create(
%{
assigns: %{user: user},
- body_params: %{status: _, scheduled_at: scheduled_at} = params
+ private: %{
+ open_api_spex: %{body_params: %{status: _, scheduled_at: scheduled_at} = params}
+ }
} = conn,
_
)
@@ -156,7 +162,13 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
else
{:far_enough, _} ->
params = Map.drop(params, [:scheduled_at])
- create(%Plug.Conn{conn | body_params: params}, %{})
+
+ put_in(
+ conn,
+ [Access.key(:private), Access.key(:open_api_spex), Access.key(:body_params)],
+ params
+ )
+ |> do_create
error ->
error
@@ -164,7 +176,35 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
# Creates a regular status
- def create(%{assigns: %{user: user}, body_params: %{status: _} = params} = conn, _) do
+ def create(
+ %{
+ private: %{open_api_spex: %{body_params: %{status: _}}}
+ } = conn,
+ _
+ ) do
+ do_create(conn)
+ end
+
+ def create(
+ %{
+ assigns: %{user: _user},
+ private: %{open_api_spex: %{body_params: %{media_ids: _} = params}}
+ } = conn,
+ _
+ ) do
+ params = Map.put(params, :status, "")
+
+ put_in(
+ conn,
+ [Access.key(:private), Access.key(:open_api_spex), Access.key(:body_params)],
+ params
+ )
+ |> do_create
+ end
+
+ defp do_create(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: params}}} = conn
+ ) do
params =
Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
|> put_application(conn)
@@ -189,13 +229,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
end
- def create(%{assigns: %{user: _user}, body_params: %{media_ids: _} = params} = conn, _) do
- params = Map.put(params, :status, "")
- create(%Plug.Conn{conn | body_params: params}, %{})
- end
-
@doc "GET /api/v1/statuses/:id/history"
- def show_history(%{assigns: assigns} = conn, %{id: id} = params) do
+ def show_history(
+ %{assigns: assigns, private: %{open_api_spex: %{params: %{id: id} = params}}} = conn,
+ _
+ ) do
with user = assigns[:user],
%Activity{} = activity <- Activity.get_by_id_with_object(id),
true <- Visibility.visible_for_user?(activity, user) do
@@ -211,7 +249,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "GET /api/v1/statuses/:id/source"
- def show_source(%{assigns: assigns} = conn, %{id: id} = _params) do
+ def show_source(%{assigns: assigns, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
with user = assigns[:user],
%Activity{} = activity <- Activity.get_by_id_with_object(id),
true <- Visibility.visible_for_user?(activity, user) do
@@ -225,14 +263,20 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "PUT /api/v1/statuses/:id"
- def update(%{assigns: %{user: user}, body_params: body_params} = conn, %{id: id} = params) do
+ def update(
+ %{
+ assigns: %{user: user},
+ private: %{open_api_spex: %{body_params: body_params, params: %{id: id} = params}}
+ } = conn,
+ _
+ ) do
with {_, %Activity{}} = {_, activity} <- {:activity, Activity.get_by_id_with_object(id)},
{_, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
{_, true} <- {:is_create, activity.data["type"] == "Create"},
actor <- Activity.user_actor(activity),
{_, true} <- {:own_status, actor.id == user.id},
changes <- body_params |> put_application(conn),
- {_, {:ok, _update_activity}} <- {:pipeline, CommonAPI.update(user, activity, changes)},
+ {_, {:ok, _update_activity}} <- {:pipeline, CommonAPI.update(activity, user, changes)},
{_, %Activity{}} = {_, activity} <- {:refetched, Activity.get_by_id_with_object(id)} do
try_render(conn, "show.json",
activity: activity,
@@ -248,7 +292,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "GET /api/v1/statuses/:id"
- def show(%{assigns: %{user: user}} = conn, %{id: id} = params) do
+ def show(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id} = params}}} =
+ conn,
+ _
+ ) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
true <- Visibility.visible_for_user?(activity, user) do
try_render(conn, "show.json",
@@ -263,7 +311,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "DELETE /api/v1/statuses/:id"
- def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
+ def delete(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
{:ok, %Activity{}} <- CommonAPI.delete(id, user) do
try_render(conn, "show.json",
@@ -278,7 +326,13 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "POST /api/v1/statuses/:id/reblog"
- def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do
+ def reblog(
+ %{
+ assigns: %{user: user},
+ private: %{open_api_spex: %{body_params: params, params: %{id: ap_id_or_id}}}
+ } = conn,
+ _
+ ) do
with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
%Activity{} = announce <- Activity.normalize(announce.data) do
try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
@@ -286,7 +340,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "POST /api/v1/statuses/:id/unreblog"
- def unreblog(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
+ def unreblog(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: activity_id}}}} =
+ conn,
+ _
+ ) do
with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
%Activity{} = activity <- Activity.get_by_id(activity_id) do
try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
@@ -294,15 +352,23 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "POST /api/v1/statuses/:id/favourite"
- def favourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
- with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
+ def favourite(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: activity_id}}}} =
+ conn,
+ _
+ ) do
+ with {:ok, _fav} <- CommonAPI.favorite(activity_id, user),
%Activity{} = activity <- Activity.get_by_id(activity_id) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end
@doc "POST /api/v1/statuses/:id/unfavourite"
- def unfavourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
+ def unfavourite(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: activity_id}}}} =
+ conn,
+ _
+ ) do
with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
%Activity{} = activity <- Activity.get_by_id(activity_id) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
@@ -310,7 +376,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "POST /api/v1/statuses/:id/pin"
- def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
+ def pin(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: ap_id_or_id}}}} =
+ conn,
+ _
+ ) do
with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
else
@@ -329,24 +399,43 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "POST /api/v1/statuses/:id/unpin"
- def unpin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
+ def unpin(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: ap_id_or_id}}}} =
+ conn,
+ _
+ ) do
with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end
@doc "POST /api/v1/statuses/:id/bookmark"
- def bookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
+ def bookmark(
+ %{
+ assigns: %{user: user},
+ private: %{open_api_spex: %{body_params: body_params, params: %{id: id}}}
+ } = conn,
+ _
+ ) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
%User{} = user <- User.get_cached_by_nickname(user.nickname),
true <- Visibility.visible_for_user?(activity, user),
- {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
+ folder_id <- Map.get(body_params, :folder_id, nil),
+ folder_id <-
+ if(folder_id && BookmarkFolder.belongs_to_user?(folder_id, user.id),
+ do: folder_id,
+ else: nil
+ ),
+ {:ok, _bookmark} <- Bookmark.create(user.id, activity.id, folder_id) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end
@doc "POST /api/v1/statuses/:id/unbookmark"
- def unbookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
+ def unbookmark(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
+ _
+ ) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
%User{} = user <- User.get_cached_by_nickname(user.nickname),
true <- Visibility.visible_for_user?(activity, user),
@@ -356,35 +445,38 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "POST /api/v1/statuses/:id/mute"
- def mute_conversation(%{assigns: %{user: user}, body_params: params} = conn, %{id: id}) do
+ def mute_conversation(
+ %{
+ assigns: %{user: user},
+ private: %{open_api_spex: %{body_params: params, params: %{id: id}}}
+ } = conn,
+ _
+ ) do
with %Activity{} = activity <- Activity.get_by_id(id),
- {:ok, activity} <- CommonAPI.add_mute(user, activity, params) do
+ {:ok, activity} <- CommonAPI.add_mute(activity, user, params) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end
@doc "POST /api/v1/statuses/:id/unmute"
- def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
+ def unmute_conversation(
+ %{
+ assigns: %{user: user},
+ private: %{open_api_spex: %{params: %{id: id}}}
+ } = conn,
+ _
+ ) do
with %Activity{} = activity <- Activity.get_by_id(id),
- {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
+ {:ok, activity} <- CommonAPI.remove_mute(activity, user) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end
- @doc "GET /api/v1/statuses/:id/card"
- @deprecated "https://github.com/tootsuite/mastodon/pull/11213"
- def card(%{assigns: %{user: user}} = conn, %{id: status_id}) do
- with %Activity{} = activity <- Activity.get_by_id(status_id),
- true <- Visibility.visible_for_user?(activity, user) do
- data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
- render(conn, "card.json", data)
- else
- _ -> render_error(conn, :not_found, "Record not found")
- end
- end
-
@doc "GET /api/v1/statuses/:id/favourited_by"
- def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
+ def favourited_by(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
+ _
+ ) do
with true <- Pleroma.Config.get([:instance, :show_reactions]),
%Activity{} = activity <- Activity.get_by_id_with_object(id),
{:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
@@ -405,7 +497,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "GET /api/v1/statuses/:id/reblogged_by"
- def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
+ def reblogged_by(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
+ _
+ ) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
{:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
%Object{data: %{"announcements" => announces, "id" => ap_id}} <-
@@ -437,7 +532,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "GET /api/v1/statuses/:id/context"
- def context(%{assigns: %{user: user}} = conn, %{id: id}) do
+ def context(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
+ _
+ ) do
with %Activity{} = activity <- Activity.get_by_id(id) do
activities =
ActivityPub.fetch_activities_for_context(activity.data["context"], %{
@@ -451,7 +549,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "GET /api/v1/favourites"
- def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
+ def favourites(
+ %{assigns: %{user: %User{} = user}, private: %{open_api_spex: %{params: params}}} = conn,
+ _
+ ) do
activities = ActivityPub.fetch_favourites(user, params)
conn
@@ -464,12 +565,13 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "GET /api/v1/bookmarks"
- def bookmarks(%{assigns: %{user: user}} = conn, params) do
+ def bookmarks(%{assigns: %{user: user}, private: %{open_api_spex: %{params: params}}} = conn, _) do
user = User.get_cached_by_id(user.id)
+ folder_id = Map.get(params, :folder_id)
bookmarks =
user.id
- |> Bookmark.for_user_query()
+ |> Bookmark.for_user_query(folder_id)
|> Pleroma.Pagination.fetch_paginated(params)
activities =
diff --git a/lib/pleroma/web/mastodon_api/mastodon_api.ex b/lib/pleroma/web/mastodon_api/mastodon_api.ex
index 467dc2fac..6dcbfb097 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api.ex
@@ -16,7 +16,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
def follow(follower, followed, params \\ %{}) do
result =
if not User.following?(follower, followed) do
- CommonAPI.follow(follower, followed)
+ CommonAPI.follow(followed, follower)
else
{:ok, follower, followed, nil}
end
@@ -30,11 +30,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
end
defp set_reblogs_visibility(false, {:ok, follower, followed, _}) do
- CommonAPI.hide_reblogs(follower, followed)
+ CommonAPI.hide_reblogs(followed, follower)
end
defp set_reblogs_visibility(_, {:ok, follower, followed, _}) do
- CommonAPI.show_reblogs(follower, followed)
+ CommonAPI.show_reblogs(followed, follower)
end
defp set_subscription(true, {:ok, follower, followed, _}) do
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index cc3e3582f..6976ca6e5 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.AccountView do
@@ -193,7 +193,28 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
render_many(targets, AccountView, "relationship.json", render_opts)
end
+ def render("familiar_followers.json", %{users: users} = opts) do
+ opts =
+ opts
+ |> Map.merge(%{as: :user})
+ |> Map.delete(:users)
+
+ users
+ |> render_many(AccountView, "familiar_followers.json", opts)
+ end
+
+ def render("familiar_followers.json", %{user: %{id: id, accounts: accounts}} = opts) do
+ accounts =
+ accounts
+ |> render_many(AccountView, "show.json", opts)
+ |> Enum.filter(&Enum.any?/1)
+
+ %{id: id, accounts: accounts}
+ end
+
defp do_render("show.json", %{user: user} = opts) do
+ self = opts[:for] == user
+
user = User.sanitize_html(user, User.html_filter_policy(opts[:for]))
display_name = user.name || user.nickname
@@ -203,16 +224,16 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
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,
+ if !user.hide_follows_count or !user.hide_follows or self,
do: user.following_count,
else: 0
followers_count =
- if !user.hide_followers_count or !user.hide_followers or opts[:for] == user,
+ if !user.hide_followers_count or !user.hide_followers or self,
do: user.follower_count,
else: 0
- bot = user.actor_type == "Service"
+ bot = bot?(user)
emojis =
Enum.map(user.emoji, fn {shortcode, raw_url} ->
@@ -249,6 +270,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
nil
end
+ last_status_at =
+ user.last_status_at &&
+ user.last_status_at |> NaiveDateTime.to_date() |> Date.to_iso8601()
+
%{
id: to_string(user.id),
username: username_from_nickname(user.nickname),
@@ -277,7 +302,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
actor_type: user.actor_type
}
},
- last_status_at: user.last_status_at,
+ last_status_at: last_status_at,
# Pleroma extensions
# Note: it's insecure to output :email but fully-qualified nickname may serve as safe stub
@@ -464,4 +489,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
defp image_url(%{"url" => [%{"href" => href} | _]}), do: href
defp image_url(_), do: nil
+
+ defp bot?(user) do
+ # Because older and/or Mastodon clients may not recognize a Group actor properly,
+ # and currently the group actor can only boost things, we should let these clients
+ # think groups are bots.
+ # See https://git.pleroma.social/pleroma/pleroma-meta/-/issues/14
+ user.actor_type == "Service" || user.actor_type == "Group"
+ 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 1b01d7371..913684928 100644
--- a/lib/pleroma/web/mastodon_api/views/instance_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex
@@ -13,12 +13,11 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
def render("show.json", _) do
instance = Config.get(:instance)
- %{
- uri: Pleroma.Web.Endpoint.url(),
- title: Keyword.get(instance, :name),
+ common_information(instance)
+ |> Map.merge(%{
+ uri: Pleroma.Web.WebFinger.host(),
description: Keyword.get(instance, :description),
short_description: Keyword.get(instance, :short_description),
- version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
email: Keyword.get(instance, :email),
urls: %{
streaming_api: Pleroma.Web.Endpoint.websocket_url()
@@ -27,9 +26,10 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
thumbnail:
URI.merge(Pleroma.Web.Endpoint.url(), Keyword.get(instance, :instance_thumbnail))
|> to_string,
- languages: Keyword.get(instance, :languages, ["en"]),
registrations: Keyword.get(instance, :registrations_open),
approval_required: Keyword.get(instance, :account_approval_required),
+ contact_account: contact_account(Keyword.get(instance, :contact_username)),
+ configuration: configuration(),
# Extra (not present in Mastodon):
max_toot_chars: Keyword.get(instance, :limit),
max_media_attachments: Keyword.get(instance, :max_media_attachments),
@@ -41,19 +41,61 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
background_image: Pleroma.Web.Endpoint.url() <> Keyword.get(instance, :background_image),
shout_limit: Config.get([:shout, :limit]),
description_limit: Keyword.get(instance, :description_limit),
- pleroma: %{
- metadata: %{
- account_activation_required: Keyword.get(instance, :account_activation_required),
- features: features(),
- federation: federation(),
- fields_limits: fields_limits(),
- post_formats: Config.get([:instance, :allowed_post_formats]),
- birthday_required: Config.get([:instance, :birthday_required]),
- birthday_min_age: Config.get([:instance, :birthday_min_age])
- },
- stats: %{mau: Pleroma.User.active_user_count()},
- vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
- }
+ chat_limit: Keyword.get(instance, :chat_limit),
+ pleroma: pleroma_configuration(instance)
+ })
+ end
+
+ def render("show2.json", _) do
+ instance = Config.get(:instance)
+
+ common_information(instance)
+ |> Map.merge(%{
+ domain: Pleroma.Web.WebFinger.host(),
+ source_url: Pleroma.Application.repository(),
+ description: Keyword.get(instance, :short_description),
+ usage: %{users: %{active_month: Pleroma.User.active_user_count()}},
+ thumbnail: %{
+ url:
+ URI.merge(Pleroma.Web.Endpoint.url(), Keyword.get(instance, :instance_thumbnail))
+ |> to_string
+ },
+ configuration: configuration2(),
+ registrations: %{
+ enabled: Keyword.get(instance, :registrations_open),
+ approval_required: Keyword.get(instance, :account_approval_required),
+ message: nil,
+ url: nil
+ },
+ contact: %{
+ email: Keyword.get(instance, :email),
+ account: contact_account(Keyword.get(instance, :contact_username))
+ },
+ # Extra (not present in Mastodon):
+ pleroma: pleroma_configuration2(instance)
+ })
+ end
+
+ def render("rules.json", _) do
+ Pleroma.Rule.query()
+ |> Pleroma.Repo.all()
+ |> render_many(__MODULE__, "rule.json", as: :rule)
+ end
+
+ def render("rule.json", %{rule: rule}) do
+ %{
+ id: to_string(rule.id),
+ text: rule.text,
+ hint: rule.hint || ""
+ }
+ end
+
+ defp common_information(instance) do
+ %{
+ languages: Keyword.get(instance, :languages, ["en"]),
+ rules: render(__MODULE__, "rules.json"),
+ title: Keyword.get(instance, :name),
+ version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})"
}
end
@@ -101,13 +143,16 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
if Config.get([:instance, :profile_directory]) do
"profile_directory"
end,
- "pleroma:get:main/ostatus"
+ "pleroma:get:main/ostatus",
+ "pleroma:group_actors",
+ "pleroma:bookmark_folders"
]
|> Enum.filter(& &1)
end
def federation do
quarantined = Config.get([:instance, :quarantined_instances], [])
+ rejected = Config.get([:instance, :rejected_instances], [])
if Config.get([:mrf, :transparency]) do
{:ok, data} = MRF.describe()
@@ -127,13 +172,19 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
|> Enum.map(fn {instance, reason} -> {instance, %{"reason" => reason}} end)
|> Map.new()
})
+ |> Map.put(
+ :rejected_instances,
+ rejected
+ |> Enum.map(fn {instance, reason} -> {instance, %{"reason" => reason}} end)
+ |> Map.new()
+ )
else
%{}
end
|> Map.put(:enabled, Config.get([:instance, :federating]))
end
- def fields_limits do
+ defp fields_limits do
%{
max_fields: Config.get([:instance, :max_account_fields]),
max_remote_fields: Config.get([:instance, :max_remote_account_fields]),
@@ -141,4 +192,94 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
value_length: Config.get([:instance, :account_field_value_length])
}
end
+
+ defp contact_account(nil), do: nil
+
+ defp contact_account("@" <> username) do
+ contact_account(username)
+ end
+
+ defp contact_account(username) do
+ user = Pleroma.User.get_cached_by_nickname(username)
+
+ if user do
+ Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user, for: nil})
+ else
+ nil
+ end
+ end
+
+ defp configuration do
+ %{
+ accounts: %{
+ max_featured_tags: 0
+ },
+ statuses: %{
+ max_characters: Config.get([:instance, :limit]),
+ max_media_attachments: Config.get([:instance, :max_media_attachments])
+ },
+ media_attachments: %{
+ image_size_limit: Config.get([:instance, :upload_limit]),
+ video_size_limit: Config.get([:instance, :upload_limit]),
+ supported_mime_types: ["application/octet-stream"]
+ },
+ polls: %{
+ max_options: Config.get([:instance, :poll_limits, :max_options]),
+ max_characters_per_option: Config.get([:instance, :poll_limits, :max_option_chars]),
+ min_expiration: Config.get([:instance, :poll_limits, :min_expiration]),
+ max_expiration: Config.get([:instance, :poll_limits, :max_expiration])
+ }
+ }
+ end
+
+ defp configuration2 do
+ configuration()
+ |> put_in([:accounts, :max_pinned_statuses], Config.get([:instance, :max_pinned_statuses], 0))
+ |> put_in([:statuses, :characters_reserved_per_url], 0)
+ |> Map.merge(%{
+ urls: %{
+ streaming: Pleroma.Web.Endpoint.websocket_url(),
+ status: Config.get([:instance, :status_page])
+ },
+ vapid: %{
+ public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
+ }
+ })
+ end
+
+ defp pleroma_configuration(instance) do
+ %{
+ metadata: %{
+ account_activation_required: Keyword.get(instance, :account_activation_required),
+ features: features(),
+ federation: federation(),
+ fields_limits: fields_limits(),
+ post_formats: Config.get([:instance, :allowed_post_formats]),
+ birthday_required: Config.get([:instance, :birthday_required]),
+ birthday_min_age: Config.get([:instance, :birthday_min_age])
+ },
+ stats: %{mau: Pleroma.User.active_user_count()},
+ vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
+ }
+ end
+
+ defp pleroma_configuration2(instance) do
+ configuration = pleroma_configuration(instance)
+
+ configuration
+ |> Map.merge(%{
+ metadata:
+ configuration.metadata
+ |> Map.merge(%{
+ 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:
+ Pleroma.Web.Endpoint.url() <> Keyword.get(instance, :background_image),
+ chat_limit: Keyword.get(instance, :chat_limit),
+ description_limit: Keyword.get(instance, :description_limit),
+ shout_limit: Config.get([:shout, :limit])
+ })
+ })
+ end
end
diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex
index 2a51f3755..3f2478719 100644
--- a/lib/pleroma/web/mastodon_api/views/notification_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex
@@ -108,6 +108,9 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
"mention" ->
put_status(response, activity, reading_user, status_render_opts)
+ "status" ->
+ put_status(response, activity, reading_user, status_render_opts)
+
"favourite" ->
put_status(response, parent_activity_fn.(), reading_user, status_render_opts)
diff --git a/lib/pleroma/web/mastodon_api/views/poll_view.ex b/lib/pleroma/web/mastodon_api/views/poll_view.ex
index 34e23873e..1e3c9f36d 100644
--- a/lib/pleroma/web/mastodon_api/views/poll_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/poll_view.ex
@@ -21,7 +21,10 @@ defmodule Pleroma.Web.MastodonAPI.PollView do
votes_count: votes_count,
voters_count: voters_count(object),
options: options,
- emojis: Pleroma.Web.MastodonAPI.StatusView.build_emojis(object.data["emoji"])
+ emojis: Pleroma.Web.MastodonAPI.StatusView.build_emojis(object.data["emoji"]),
+ pleroma: %{
+ non_anonymous: object.data["nonAnonymous"] || false
+ }
}
if params[:for] do
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index d070262cc..747638c53 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -21,6 +21,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.MediaProxy
alias Pleroma.Web.PleromaAPI.EmojiReactionController
+ alias Pleroma.Web.RichMedia.Card
import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1, visible_for_user?: 2]
@@ -29,9 +30,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
# 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)
+ Card.get_by_activity(activity)
end)
end
@@ -113,9 +112,7 @@ 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
+ # Start prefetching rich media before doing anything else
fetch_rich_media_for_activities(activities)
replied_to_activities = get_replied_to_activities(activities)
quoted_activities = get_quoted_activities(activities)
@@ -184,7 +181,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
- bookmarked = Activity.get_bookmark(reblogged_parent_activity, opts[:for]) != nil
+ bookmark = Activity.get_bookmark(reblogged_parent_activity, opts[:for])
+
+ bookmark_folder =
+ if bookmark != nil do
+ bookmark.folder_id
+ else
+ nil
+ end
mentions =
activity.recipients
@@ -213,7 +217,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
favourites_count: 0,
reblogged: reblogged?(reblogged_parent_activity, opts[:for]),
favourited: present?(favorited),
- bookmarked: present?(bookmarked),
+ bookmarked: present?(bookmark),
muted: false,
pinned: pinned?,
sensitive: false,
@@ -227,7 +231,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
emojis: [],
pleroma: %{
local: activity.local,
- pinned_at: pinned_at
+ pinned_at: pinned_at,
+ bookmark_folder: bookmark_folder
}
}
end
@@ -264,7 +269,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
- bookmarked = Activity.get_bookmark(activity, opts[:for]) != nil
+ bookmark = Activity.get_bookmark(activity, opts[:for])
+
+ bookmark_folder =
+ if bookmark != nil do
+ bookmark.folder_id
+ else
+ nil
+ end
client_posted_this_activity = opts[:for] && user.id == opts[:for].id
@@ -281,7 +293,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
cond do
is_nil(opts[:for]) -> false
is_boolean(activity.thread_muted?) -> activity.thread_muted?
- true -> CommonAPI.thread_muted?(opts[:for], activity)
+ true -> CommonAPI.thread_muted?(activity, opts[:for])
end
attachment_data = object.data["attachment"] || []
@@ -349,7 +361,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
summary = object.data["summary"] || ""
- card = render("card.json", Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity))
+ card =
+ case Card.get_by_activity(activity) do
+ %Card{} = result -> render("card.json", result)
+ _ -> nil
+ end
url =
if user.local do
@@ -418,7 +434,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
favourites_count: like_count,
reblogged: reblogged?(activity, opts[:for]),
favourited: present?(favorited),
- bookmarked: present?(bookmarked),
+ bookmarked: present?(bookmark),
muted: muted,
pinned: pinned?,
sensitive: sensitive,
@@ -447,7 +463,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
thread_muted: thread_muted?,
emoji_reactions: emoji_reactions,
parent_visible: visible_for_user?(reply_to, opts[:for]),
- pinned_at: pinned_at
+ pinned_at: pinned_at,
+ quotes_count: object.data["quotesCount"] || 0,
+ bookmark_folder: bookmark_folder
}
}
end
@@ -550,37 +568,30 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
}
end
- def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
- page_url_data = URI.parse(page_url)
-
- page_url_data =
- if is_binary(rich_media["url"]) do
- URI.merge(page_url_data, URI.parse(rich_media["url"]))
- else
- page_url_data
- end
+ def render("card.json", %Card{fields: rich_media}) do
+ page_url_data = URI.parse(rich_media["url"])
page_url = page_url_data |> to_string
- image_url_data =
- if is_binary(rich_media["image"]) do
- URI.parse(rich_media["image"])
- else
- nil
- end
-
- image_url = build_image_url(image_url_data, page_url_data)
+ image_url = proxied_url(rich_media["image"], page_url_data)
+ audio_url = proxied_url(rich_media["audio"], page_url_data)
+ video_url = proxied_url(rich_media["video"], page_url_data)
%{
type: "link",
provider_name: page_url_data.host,
provider_url: page_url_data.scheme <> "://" <> page_url_data.host,
url: page_url,
- image: image_url |> MediaProxy.url(),
+ image: image_url,
+ image_description: rich_media["image:alt"] || "",
title: rich_media["title"] || "",
description: rich_media["description"] || "",
pleroma: %{
- opengraph: rich_media
+ opengraph:
+ rich_media
+ |> Maps.put_if_present("image", image_url)
+ |> Maps.put_if_present("audio", audio_url)
+ |> Maps.put_if_present("video", video_url)
}
}
end
@@ -613,6 +624,19 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
to_string(attachment["id"] || hash_id)
end
+ description =
+ if attachment["summary"] do
+ HTML.strip_tags(attachment["summary"])
+ else
+ attachment["name"]
+ end
+
+ name = if attachment["summary"], do: attachment["name"]
+
+ pleroma =
+ %{mime_type: media_type}
+ |> Maps.put_if_present(:name, name)
+
%{
id: attachment_id,
url: href,
@@ -620,8 +644,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
preview_url: href_preview,
text_url: href,
type: type,
- description: attachment["name"],
- pleroma: %{mime_type: media_type},
+ description: description,
+ pleroma: pleroma,
blurhash: attachment["blurhash"]
}
|> Maps.put_if_present(:meta, meta)
@@ -796,8 +820,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
URI.merge(page_url_data, image_url_data) |> to_string
end
- defp build_image_url(_, _), do: nil
-
defp get_source_text(%{"content" => content} = _source) do
content
end
@@ -817,4 +839,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
defp get_source_content_type(_source) do
Utils.get_content_type(nil)
end
+
+ defp proxied_url(url, page_url_data) do
+ if is_binary(url) do
+ build_image_url(URI.parse(url), page_url_data) |> MediaProxy.url()
+ else
+ nil
+ end
+ end
end
diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex
index 07c2b62e3..730295a4c 100644
--- a/lib/pleroma/web/mastodon_api/websocket_handler.ex
+++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex
@@ -11,28 +11,21 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
alias Pleroma.Web.Streamer
alias Pleroma.Web.StreamerView
- @behaviour :cowboy_websocket
+ @behaviour Phoenix.Socket.Transport
# Client ping period.
@tick :timer.seconds(30)
- # Cowboy timeout period.
- @timeout :timer.seconds(60)
- # Hibernate every X messages
- @hibernate_every 100
-
- def init(%{qs: qs} = req, state) do
- with params <- Enum.into(:cow_qs.parse_qs(qs), %{}),
- sec_websocket <- :cowboy_req.header("sec-websocket-protocol", req, nil),
- access_token <- Map.get(params, "access_token"),
- {: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)
- else
- req
- end
+ @impl Phoenix.Socket.Transport
+ def child_spec(_opts), do: :ignore
+
+ # This only prepares the connection and is not in the process yet
+ @impl Phoenix.Socket.Transport
+ def connect(%{params: params} = transport_info) do
+ with access_token <- Map.get(params, "access_token"),
+ {:ok, user, oauth_token} <- authenticate_request(access_token),
+ {:ok, topic} <-
+ Streamer.get_topic(params["stream"], user, oauth_token, params) do
topics =
if topic do
[topic]
@@ -40,41 +33,40 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
[]
end
- {:cowboy_websocket, req,
- %{user: user, topics: topics, oauth_token: oauth_token, count: 0, timer: nil},
- %{idle_timeout: @timeout}}
+ state = %{
+ user: user,
+ topics: topics,
+ oauth_token: oauth_token,
+ count: 0,
+ timer: nil
+ }
+
+ {:ok, state}
else
{:error, :bad_topic} ->
- Logger.debug("#{__MODULE__} bad topic #{inspect(req)}")
- req = :cowboy_req.reply(404, req)
- {:ok, req, state}
+ Logger.debug("#{__MODULE__} bad topic #{inspect(transport_info)}")
+
+ {:error, :bad_topic}
{:error, :unauthorized} ->
- Logger.debug("#{__MODULE__} authentication error: #{inspect(req)}")
- req = :cowboy_req.reply(401, req)
- {:ok, req, state}
+ Logger.debug("#{__MODULE__} authentication error: #{inspect(transport_info)}")
+ {:error, :unauthorized}
end
end
- def websocket_init(state) do
- Logger.debug(
- "#{__MODULE__} accepted websocket connection for user #{(state.user || %{id: "anonymous"}).id}, topics #{state.topics}"
- )
-
+ # All subscriptions/links and messages cannot be created
+ # until the processed is launched with init/1
+ @impl Phoenix.Socket.Transport
+ def init(state) do
Enum.each(state.topics, fn topic -> Streamer.add_socket(topic, state.oauth_token) end)
- {:ok, %{state | timer: timer()}}
- end
- # Client's Pong frame.
- def websocket_handle(:pong, state) do
- if state.timer, do: Process.cancel_timer(state.timer)
- {:ok, %{state | timer: timer()}}
- end
+ Process.send_after(self(), :ping, @tick)
- # We only receive pings for now
- def websocket_handle(:ping, state), do: {:ok, state}
+ {:ok, state}
+ end
- def websocket_handle({:text, text}, state) do
+ @impl Phoenix.Socket.Transport
+ def handle_in({text, [opcode: :text]}, state) do
with {:ok, %{} = event} <- Jason.decode(text) do
handle_client_event(event, state)
else
@@ -84,50 +76,47 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
end
end
- def websocket_handle(frame, state) do
+ def handle_in(frame, state) do
Logger.error("#{__MODULE__} received frame: #{inspect(frame)}")
{:ok, state}
end
- def websocket_info({:render_with_user, view, template, item, topic}, state) do
+ @impl Phoenix.Socket.Transport
+ def handle_info({:render_with_user, view, template, item, topic}, state) do
user = %User{} = User.get_cached_by_ap_id(state.user.ap_id)
unless Streamer.filtered_by_user?(user, item) do
- websocket_info({:text, view.render(template, item, user, topic)}, %{state | user: user})
+ message = view.render(template, item, user, topic)
+ {:push, {:text, message}, %{state | user: user}}
else
{:ok, state}
end
end
- def websocket_info({:text, message}, state) do
- # If the websocket processed X messages, force an hibernate/GC.
- # We don't hibernate at every message to balance CPU usage/latency with RAM usage.
- if state.count > @hibernate_every do
- {:reply, {:text, message}, %{state | count: 0}, :hibernate}
- else
- {:reply, {:text, message}, %{state | count: state.count + 1}}
- end
+ def handle_info({:text, text}, state) do
+ {:push, {:text, text}, state}
end
- # Ping tick. We don't re-queue a timer there, it is instead queued when :pong is received.
- # As we hibernate there, reset the count to 0.
- # If the client misses :pong, Cowboy will automatically timeout the connection after
- # `@idle_timeout`.
- def websocket_info(:tick, state) do
- {:reply, :ping, %{state | timer: nil, count: 0}, :hibernate}
+ def handle_info(:ping, state) do
+ Process.send_after(self(), :ping, @tick)
+
+ {:push, {:ping, ""}, state}
end
- def websocket_info(:close, state) do
- {:stop, state}
+ def handle_info(:close, state) do
+ {:stop, {:closed, ~c"connection closed by server"}, state}
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 handle_info(msg, state) do
+ Logger.debug("#{__MODULE__} received info: #{inspect(msg)}")
- def terminate(reason, _req, state) do
+ {:ok, state}
+ end
+
+ @impl Phoenix.Socket.Transport
+ def terminate(reason, state) do
Logger.debug(
- "#{__MODULE__} terminating websocket connection for user #{(state.user || %{id: "anonymous"}).id}, topics #{state.topics || "?"}: #{inspect(reason)}"
+ "#{__MODULE__} terminating websocket connection for user #{(state.user || %{id: "anonymous"}).id}, topics #{state.topics || "?"}: #{inspect(reason)})"
)
Enum.each(state.topics, fn topic -> Streamer.remove_socket(topic) end)
@@ -135,16 +124,13 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
end
# Public streams without authentication.
- defp authenticate_request(nil, nil) do
+ defp authenticate_request(nil) do
{:ok, nil, nil}
end
# Authenticated streams.
- defp authenticate_request(access_token, sec_websocket) do
- token = access_token || sec_websocket
-
- with true <- is_bitstring(token),
- oauth_token = %Token{user_id: user_id} <- Repo.get_by(Token, token: token),
+ defp authenticate_request(access_token) do
+ with oauth_token = %Token{user_id: user_id} <- Repo.get_by(Token, token: access_token),
user = %User{} <- User.get_cached_by_id(user_id) do
{:ok, user, oauth_token}
else
@@ -152,36 +138,32 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
end
end
- defp timer do
- Process.send_after(self(), :tick, @tick)
- end
-
defp handle_client_event(%{"type" => "subscribe", "stream" => _topic} = params, state) do
with {_, {:ok, topic}} <-
{:topic, Streamer.get_topic(params["stream"], state.user, state.oauth_token, params)},
{_, false} <- {:subscribed, topic in state.topics} do
Streamer.add_socket(topic, state.oauth_token)
- {[
- {:text,
- StreamerView.render("pleroma_respond.json", %{type: "subscribe", result: "success"})}
- ], %{state | topics: [topic | state.topics]}}
+ message =
+ StreamerView.render("pleroma_respond.json", %{type: "subscribe", result: "success"})
+
+ {:reply, :ok, {:text, message}, %{state | topics: [topic | state.topics]}}
else
{:subscribed, true} ->
- {[
- {:text,
- StreamerView.render("pleroma_respond.json", %{type: "subscribe", result: "ignored"})}
- ], state}
+ message =
+ StreamerView.render("pleroma_respond.json", %{type: "subscribe", result: "ignored"})
+
+ {:reply, :error, {:text, message}, state}
{:topic, {:error, error}} ->
- {[
- {:text,
- StreamerView.render("pleroma_respond.json", %{
- type: "subscribe",
- result: "error",
- error: error
- })}
- ], state}
+ message =
+ StreamerView.render("pleroma_respond.json", %{
+ type: "subscribe",
+ result: "error",
+ error: error
+ })
+
+ {:reply, :error, {:text, message}, state}
end
end
@@ -191,26 +173,26 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
{_, true} <- {:subscribed, topic in state.topics} do
Streamer.remove_socket(topic)
- {[
- {:text,
- StreamerView.render("pleroma_respond.json", %{type: "unsubscribe", result: "success"})}
- ], %{state | topics: List.delete(state.topics, topic)}}
+ message =
+ StreamerView.render("pleroma_respond.json", %{type: "unsubscribe", result: "success"})
+
+ {:reply, :ok, {:text, message}, %{state | topics: List.delete(state.topics, topic)}}
else
{:subscribed, false} ->
- {[
- {:text,
- StreamerView.render("pleroma_respond.json", %{type: "unsubscribe", result: "ignored"})}
- ], state}
+ message =
+ StreamerView.render("pleroma_respond.json", %{type: "unsubscribe", result: "ignored"})
+
+ {:reply, :error, {:text, message}, state}
{:topic, {:error, error}} ->
- {[
- {:text,
- StreamerView.render("pleroma_respond.json", %{
- type: "unsubscribe",
- result: "error",
- error: error
- })}
- ], state}
+ message =
+ StreamerView.render("pleroma_respond.json", %{
+ type: "unsubscribe",
+ result: "error",
+ error: error
+ })
+
+ {:reply, :error, {:text, message}, state}
end
end
@@ -219,39 +201,47 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
state
) do
with {:auth, nil, nil} <- {:auth, state.user, state.oauth_token},
- {:ok, user, oauth_token} <- authenticate_request(access_token, nil) do
- {[
- {:text,
- StreamerView.render("pleroma_respond.json", %{
- type: "pleroma:authenticate",
- result: "success"
- })}
- ], %{state | user: user, oauth_token: oauth_token}}
+ {:ok, user, oauth_token} <- authenticate_request(access_token) do
+ message =
+ StreamerView.render("pleroma_respond.json", %{
+ type: "pleroma:authenticate",
+ result: "success"
+ })
+
+ {:reply, :ok, {:text, message}, %{state | user: user, oauth_token: oauth_token}}
else
{:auth, _, _} ->
- {[
- {:text,
- StreamerView.render("pleroma_respond.json", %{
- type: "pleroma:authenticate",
- result: "error",
- error: :already_authenticated
- })}
- ], state}
+ message =
+ StreamerView.render("pleroma_respond.json", %{
+ type: "pleroma:authenticate",
+ result: "error",
+ error: :already_authenticated
+ })
+
+ {:reply, :error, {:text, message}, state}
_ ->
- {[
- {:text,
- StreamerView.render("pleroma_respond.json", %{
- type: "pleroma:authenticate",
- result: "error",
- error: :unauthorized
- })}
- ], state}
+ message =
+ StreamerView.render("pleroma_respond.json", %{
+ type: "pleroma:authenticate",
+ result: "error",
+ error: :unauthorized
+ })
+
+ {:reply, :error, {:text, message}, state}
end
end
defp handle_client_event(params, state) do
Logger.error("#{__MODULE__} received unknown event: #{inspect(params)}")
- {[], state}
+ {:ok, state}
+ end
+
+ def handle_error(conn, :unauthorized) do
+ Plug.Conn.send_resp(conn, 401, "Unauthorized")
+ end
+
+ def handle_error(conn, _reason) do
+ Plug.Conn.send_resp(conn, 404, "Not Found")
end
end
diff --git a/lib/pleroma/web/media_proxy.ex b/lib/pleroma/web/media_proxy.ex
index d64760fc2..29882542c 100644
--- a/lib/pleroma/web/media_proxy.ex
+++ b/lib/pleroma/web/media_proxy.ex
@@ -75,8 +75,7 @@ defmodule Pleroma.Web.MediaProxy do
%{host: domain} = URI.parse(url)
mediaproxy_whitelist_domains =
- [:media_proxy, :whitelist]
- |> Config.get()
+ Config.get([:media_proxy, :whitelist], [])
|> Kernel.++(["#{Upload.base_url()}"])
|> Enum.map(&maybe_get_domain_from_url/1)
diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex
index bda5b36ed..0b446e0a6 100644
--- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex
+++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex
@@ -54,9 +54,10 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do
defp handle_preview(conn, url) do
media_proxy_url = MediaProxy.url(url)
+ http_client_opts = Pleroma.Config.get([:media_proxy, :proxy_opts, :http], pool: :media)
with {:ok, %{status: status} = head_response} when status in 200..299 <-
- Pleroma.HTTP.request("HEAD", media_proxy_url, [], [], pool: :media) do
+ Pleroma.HTTP.request(:head, media_proxy_url, "", [], http_client_opts) 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)
diff --git a/lib/pleroma/web/metadata/utils.ex b/lib/pleroma/web/metadata/utils.ex
index 80a8be9a2..8f61ace24 100644
--- a/lib/pleroma/web/metadata/utils.ex
+++ b/lib/pleroma/web/metadata/utils.ex
@@ -25,11 +25,14 @@ defmodule Pleroma.Web.Metadata.Utils do
|> scrub_html_and_truncate_object_field(object)
end
- def scrub_html_and_truncate(%{data: %{"content" => content}} = object) do
+ def scrub_html_and_truncate(%{data: %{"content" => content}} = object)
+ when is_binary(content) and content != "" do
content
|> scrub_html_and_truncate_object_field(object)
end
+ def scrub_html_and_truncate(%{}), do: ""
+
def scrub_html_and_truncate(content, max_length \\ 200, omission \\ "...")
when is_binary(content) do
content
diff --git a/lib/pleroma/web/nodeinfo/nodeinfo.ex b/lib/pleroma/web/nodeinfo/nodeinfo.ex
index 9e27ac26c..4d5a9a57f 100644
--- a/lib/pleroma/web/nodeinfo/nodeinfo.ex
+++ b/lib/pleroma/web/nodeinfo/nodeinfo.ex
@@ -6,7 +6,7 @@ defmodule Pleroma.Web.Nodeinfo.Nodeinfo do
alias Pleroma.Config
alias Pleroma.Stats
alias Pleroma.User
- alias Pleroma.Web.Federator.Publisher
+ alias Pleroma.Web.ActivityPub.Publisher
alias Pleroma.Web.MastodonAPI.InstanceView
# returns a nodeinfo 2.0 map, since 2.1 just adds a repository field
diff --git a/lib/pleroma/web/o_auth/app.ex b/lib/pleroma/web/o_auth/app.ex
index 0aa655381..d1bf6dd18 100644
--- a/lib/pleroma/web/o_auth/app.ex
+++ b/lib/pleroma/web/o_auth/app.ex
@@ -62,7 +62,7 @@ defmodule Pleroma.Web.OAuth.App do
|> Repo.insert()
end
- @spec update(pos_integer(), map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
+ @spec update(pos_integer(), map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()} | nil
def update(id, params) do
with %__MODULE__{} = app <- Repo.get(__MODULE__, id) do
app
diff --git a/lib/pleroma/web/o_auth/authorization.ex b/lib/pleroma/web/o_auth/authorization.ex
index 593d2d66f..22e5bfc53 100644
--- a/lib/pleroma/web/o_auth/authorization.ex
+++ b/lib/pleroma/web/o_auth/authorization.ex
@@ -28,7 +28,7 @@ defmodule Pleroma.Web.OAuth.Authorization do
end
@spec create_authorization(App.t(), User.t() | %{}, [String.t()] | nil) ::
- {:ok, Authorization.t()} | {:error, Changeset.t()}
+ {:ok, Authorization.t()} | {:error, Ecto.Changeset.t()}
def create_authorization(%App{} = app, %User{} = user, scopes \\ nil) do
%{
scopes: scopes || app.scopes,
@@ -39,7 +39,7 @@ defmodule Pleroma.Web.OAuth.Authorization do
|> Repo.insert()
end
- @spec create_changeset(map()) :: Changeset.t()
+ @spec create_changeset(map()) :: Ecto.Changeset.t()
def create_changeset(attrs \\ %{}) do
%Authorization{}
|> cast(attrs, [:user_id, :app_id, :scopes, :valid_until])
@@ -58,7 +58,7 @@ defmodule Pleroma.Web.OAuth.Authorization do
put_change(changeset, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), lifespan))
end
- @spec use_changeset(Authtorizatiton.t(), map()) :: Changeset.t()
+ @spec use_changeset(Authorization.t(), map()) :: Ecto.Changeset.t()
def use_changeset(%Authorization{} = auth, params) do
auth
|> cast(params, [:used])
@@ -66,7 +66,7 @@ defmodule Pleroma.Web.OAuth.Authorization do
end
@spec use_token(Authorization.t()) ::
- {:ok, Authorization.t()} | {:error, Changeset.t()} | {:error, String.t()}
+ {:ok, Authorization.t()} | {:error, Ecto.Changeset.t()} | {:error, String.t()}
def use_token(%Authorization{used: false, valid_until: valid_until} = auth) do
if NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) < 0 do
Repo.update(use_changeset(auth, %{used: true}))
diff --git a/lib/pleroma/web/o_auth/o_auth_controller.ex b/lib/pleroma/web/o_auth/o_auth_controller.ex
index c1fb4f378..47b03215f 100644
--- a/lib/pleroma/web/o_auth/o_auth_controller.ex
+++ b/lib/pleroma/web/o_auth/o_auth_controller.ex
@@ -310,7 +310,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
after_token_exchange(conn, %{token: token})
else
_error ->
- handle_token_exchange_error(conn, :invalid_credentails)
+ handle_token_exchange_error(conn, :invalid_credentials)
end
end
@@ -610,13 +610,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do
end
end
- @spec validate_scopes(App.t(), map() | list()) ::
+ @spec validate_scopes(App.t(), list()) ::
{:ok, list()} | {:error, :missing_scopes | :unsupported_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
diff --git a/lib/pleroma/web/o_auth/token.ex b/lib/pleroma/web/o_auth/token.ex
index 26de7bb10..9b1198b42 100644
--- a/lib/pleroma/web/o_auth/token.ex
+++ b/lib/pleroma/web/o_auth/token.ex
@@ -56,7 +56,8 @@ defmodule Pleroma.Web.OAuth.Token do
|> Repo.find_resource()
end
- @spec exchange_token(App.t(), Authorization.t()) :: {:ok, Token.t()} | {:error, Changeset.t()}
+ @spec exchange_token(App.t(), Authorization.t()) ::
+ {:ok, Token.t()} | {:error, Ecto.Changeset.t()}
def exchange_token(app, auth) do
with {:ok, auth} <- Authorization.use_token(auth),
true <- auth.app_id == app.id do
@@ -95,7 +96,7 @@ defmodule Pleroma.Web.OAuth.Token do
|> validate_required([:valid_until])
end
- @spec create(App.t(), User.t(), map()) :: {:ok, Token} | {:error, Changeset.t()}
+ @spec create(App.t(), User.t(), map()) :: {:ok, Token.t()} | {:error, Ecto.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
@@ -137,9 +138,9 @@ defmodule Pleroma.Web.OAuth.Token do
|> Repo.all()
end
- def is_expired?(%__MODULE__{valid_until: valid_until}) do
+ def expired?(%__MODULE__{valid_until: valid_until}) do
NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) > 0
end
- def is_expired?(_), do: false
+ def expired?(_), do: false
end
diff --git a/lib/pleroma/web/o_auth/token/query.ex b/lib/pleroma/web/o_auth/token/query.ex
index 4a4d2d3ef..6853ec8dd 100644
--- a/lib/pleroma/web/o_auth/token/query.ex
+++ b/lib/pleroma/web/o_auth/token/query.ex
@@ -9,10 +9,10 @@ defmodule Pleroma.Web.OAuth.Token.Query do
import Ecto.Query, only: [from: 2]
- @type query :: Ecto.Queryable.t() | Token.t()
-
alias Pleroma.Web.OAuth.Token
+ @type query :: Ecto.Queryable.t() | Token.t()
+
@spec get_by_refresh_token(query, String.t()) :: query
def get_by_refresh_token(query \\ Token, refresh_token) do
from(q in query, where: q.refresh_token == ^refresh_token)
diff --git a/lib/pleroma/web/o_status/o_status_controller.ex b/lib/pleroma/web/o_status/o_status_controller.ex
index ea4994bd0..ee7ef4a5d 100644
--- a/lib/pleroma/web/o_status/o_status_controller.ex
+++ b/lib/pleroma/web/o_status/o_status_controller.ex
@@ -37,7 +37,7 @@ defmodule Pleroma.Web.OStatus.OStatusController 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
+ {_, true} <- {:public?, Visibility.public?(activity)} do
redirect(conn, to: "/notice/#{activity.id}")
else
reason when reason in [{:public?, false}, {:activity, nil}] ->
@@ -56,7 +56,7 @@ defmodule Pleroma.Web.OStatus.OStatusController 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
+ {_, true} <- {:public?, Visibility.public?(activity)} do
redirect(conn, to: "/notice/#{activity.id}")
else
reason when reason in [{:public?, false}, {:activity, nil}] ->
@@ -69,7 +69,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do
def notice(%{assigns: %{format: format}} = conn, %{"id" => id}) do
with {_, %Activity{} = activity} <- {:activity, Activity.get_by_id_with_object(id)},
- {_, true} <- {:public?, Visibility.is_public?(activity)},
+ {_, true} <- {:public?, Visibility.public?(activity)},
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
cond do
format in ["json", "activity+json"] ->
@@ -106,13 +106,12 @@ defmodule Pleroma.Web.OStatus.OStatusController do
# Returns an HTML embedded <audio> or <video> player suitable for embed iframes.
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 <- Visibility.public?(activity),
{_, true} <- {:visible?, Visibility.visible_for_user?(activity, _reading_user = nil)},
%Object{} = object <- Object.normalize(activity, fetch: false),
%{data: %{"attachment" => [%{"url" => [url | _]} | _]}} <- object,
true <- String.starts_with?(url["mediaType"], ["audio", "video"]) do
conn
- |> put_layout(:metadata_player)
|> put_resp_header("x-frame-options", "ALLOW")
|> put_resp_header(
"content-security-policy",
diff --git a/lib/pleroma/web/pleroma_api/controllers/backup_controller.ex b/lib/pleroma/web/pleroma_api/controllers/backup_controller.ex
index b9daed22b..0115ec645 100644
--- a/lib/pleroma/web/pleroma_api/controllers/backup_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/backup_controller.ex
@@ -20,7 +20,7 @@ defmodule Pleroma.Web.PleromaAPI.BackupController do
end
def create(%{assigns: %{user: user}} = conn, _params) do
- with {:ok, _} <- Backup.create(user) do
+ with {:ok, _} <- Backup.user(user) do
backups = Backup.list(user)
render(conn, "index.json", backups: backups)
end
diff --git a/lib/pleroma/web/pleroma_api/controllers/bookmark_folder_controller.ex b/lib/pleroma/web/pleroma_api/controllers/bookmark_folder_controller.ex
new file mode 100644
index 000000000..6d6e2e940
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/bookmark_folder_controller.ex
@@ -0,0 +1,68 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.BookmarkFolderController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.BookmarkFolder
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+ # Note: scope not present in Mastodon: read:bookmarks
+ plug(OAuthScopesPlug, %{scopes: ["read:bookmarks"]} when action == :index)
+
+ # Note: scope not present in Mastodon: write:bookmarks
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["write:bookmarks"]} when action in [:create, :update, :delete]
+ )
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaBookmarkFolderOperation
+
+ action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+
+ def index(%{assigns: %{user: user}} = conn, _params) do
+ with folders <- BookmarkFolder.for_user(user.id) do
+ conn
+ |> render("index.json", %{folders: folders, as: :folder})
+ end
+ end
+
+ def create(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: params}}} = conn,
+ _
+ ) do
+ with {:ok, folder} <- BookmarkFolder.create(user.id, params[:name], params[:emoji]) do
+ render(conn, "show.json", folder: folder)
+ end
+ end
+
+ def update(
+ %{
+ assigns: %{user: user},
+ private: %{open_api_spex: %{body_params: params, params: %{id: id}}}
+ } = conn,
+ _
+ ) do
+ with true <- BookmarkFolder.belongs_to_user?(id, user.id),
+ {:ok, folder} <- BookmarkFolder.update(id, params[:name], params[:emoji]) do
+ render(conn, "show.json", folder: folder)
+ else
+ false -> {:error, :forbidden}
+ end
+ end
+
+ def delete(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
+ _
+ ) do
+ with true <- BookmarkFolder.belongs_to_user?(id, user.id),
+ {:ok, folder} <- BookmarkFolder.delete(id) do
+ render(conn, "show.json", folder: folder)
+ else
+ false -> {:error, :forbidden}
+ 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 3d7b6a4a7..58780ace2 100644
--- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex
@@ -38,14 +38,24 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
%{scopes: ["read:chats"]} when action in [:messages, :index, :index2, :show]
)
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ChatOperation
- def delete_message(%{assigns: %{user: %{id: user_id} = user}} = conn, %{
- message_id: message_id,
- id: chat_id
- }) do
+ def delete_message(
+ %{
+ assigns: %{user: %{id: user_id} = user},
+ private: %{
+ open_api_spex: %{
+ params: %{
+ message_id: message_id,
+ id: chat_id
+ }
+ }
+ }
+ } = conn,
+ _
+ ) do
with %MessageReference{} = cm_ref <-
MessageReference.get_by_id(message_id),
^chat_id <- to_string(cm_ref.chat_id),
@@ -72,11 +82,14 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
defp remove_or_delete(cm_ref, _), do: MessageReference.delete(cm_ref)
def post_chat_message(
- %{body_params: params, assigns: %{user: user}} = conn,
- %{id: id}
+ %{
+ private: %{open_api_spex: %{body_params: params, params: %{id: id}}},
+ assigns: %{user: user}
+ } = conn,
+ _
) do
with {:ok, chat} <- Chat.get_by_user_and_id(user, id),
- %User{} = recipient <- User.get_cached_by_ap_id(chat.recipient),
+ {_, %User{} = recipient} <- {:user, User.get_cached_by_ap_id(chat.recipient)},
{:ok, activity} <-
CommonAPI.post_chat_message(user, recipient, params[:content],
media_id: params[:media_id],
@@ -97,12 +110,20 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
conn
|> put_status(:bad_request)
|> json(%{error: message})
+
+ {:user, nil} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "Recipient does not exist"})
end
end
def mark_message_as_read(
- %{assigns: %{user: %{id: user_id}}} = conn,
- %{id: chat_id, message_id: message_id}
+ %{
+ assigns: %{user: %{id: user_id}},
+ private: %{open_api_spex: %{params: %{id: chat_id, message_id: message_id}}}
+ } = conn,
+ _
) do
with %MessageReference{} = cm_ref <- MessageReference.get_by_id(message_id),
^chat_id <- to_string(cm_ref.chat_id),
@@ -115,8 +136,16 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
end
def mark_as_read(
- %{body_params: %{last_read_id: last_read_id}, assigns: %{user: user}} = conn,
- %{id: id}
+ %{
+ assigns: %{user: user},
+ private: %{
+ open_api_spex: %{
+ body_params: %{last_read_id: last_read_id},
+ params: %{id: id}
+ }
+ }
+ } = conn,
+ _
) do
with {:ok, chat} <- Chat.get_by_user_and_id(user, id),
{_n, _} <- MessageReference.set_all_seen_for_chat(chat, last_read_id) do
@@ -124,7 +153,13 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
end
end
- def messages(%{assigns: %{user: user}} = conn, %{id: id} = params) do
+ def messages(
+ %{
+ assigns: %{user: user},
+ private: %{open_api_spex: %{params: %{id: id} = params}}
+ } = conn,
+ _
+ ) do
with {:ok, chat} <- Chat.get_by_user_and_id(user, id) do
chat_message_refs =
chat
@@ -138,7 +173,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
end
end
- def index(%{assigns: %{user: user}} = conn, params) do
+ def index(%{assigns: %{user: user}, private: %{open_api_spex: %{params: params}}} = conn, _) do
chats =
index_query(user, params)
|> Repo.all()
@@ -146,7 +181,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
render(conn, "index.json", chats: chats)
end
- def index2(%{assigns: %{user: user}} = conn, params) do
+ def index2(%{assigns: %{user: user}, private: %{open_api_spex: %{params: params}}} = conn, _) do
chats =
index_query(user, params)
|> Pagination.fetch_paginated(params)
@@ -166,14 +201,14 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
|> where([c], c.recipient not in ^exclude_users)
end
- def create(%{assigns: %{user: user}} = conn, %{id: id}) do
+ def create(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
with %User{ap_id: recipient} <- User.get_cached_by_id(id),
{:ok, %Chat{} = chat} <- Chat.get_or_create(user.id, recipient) do
render(conn, "show.json", chat: chat)
end
end
- def show(%{assigns: %{user: user}} = conn, %{id: id}) do
+ def show(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
with {:ok, chat} <- Chat.get_by_user_and_id(user, id) do
render(conn, "show.json", chat: chat)
end
diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex
index f854cf9c1..00fb30451 100644
--- a/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex
@@ -8,7 +8,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiFileController do
alias Pleroma.Emoji.Pack
alias Pleroma.Web.ApiSpec
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(
Pleroma.Web.Plugs.OAuthScopesPlug,
@@ -22,7 +22,10 @@ defmodule Pleroma.Web.PleromaAPI.EmojiFileController do
defdelegate open_api_operation(action), to: ApiSpec.PleromaEmojiFileOperation
- def create(%{body_params: params} = conn, %{name: pack_name}) do
+ def create(
+ %{private: %{open_api_spex: %{body_params: params, params: %{name: pack_name}}}} = conn,
+ _
+ ) do
filename = params[:filename] || get_filename(params[:file])
shortcode = params[:shortcode] || Path.basename(filename, Path.extname(filename))
@@ -49,7 +52,17 @@ defmodule Pleroma.Web.PleromaAPI.EmojiFileController do
end
end
- def update(%{body_params: %{shortcode: shortcode} = params} = conn, %{name: pack_name}) do
+ def update(
+ %{
+ private: %{
+ open_api_spex: %{
+ body_params: %{shortcode: shortcode} = params,
+ params: %{name: pack_name}
+ }
+ }
+ } = conn,
+ _
+ ) do
new_shortcode = params[:new_shortcode]
new_filename = params[:new_filename]
force = params[:force]
@@ -80,7 +93,10 @@ defmodule Pleroma.Web.PleromaAPI.EmojiFileController do
end
end
- def delete(conn, %{name: pack_name, shortcode: shortcode}) do
+ def delete(
+ %{private: %{open_api_spex: %{params: %{name: pack_name, shortcode: shortcode}}}} = conn,
+ _
+ ) do
with {:ok, pack} <- Pack.load_pack(pack_name),
{:ok, pack} <- Pack.delete_file(pack, shortcode) do
json(conn, pack.files)
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 420fea12c..32360d2a2 100644
--- a/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex
@@ -7,7 +7,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
alias Pleroma.Emoji.Pack
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(
Pleroma.Web.Plugs.OAuthScopesPlug,
@@ -26,7 +26,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaEmojiPackOperation
- def remote(conn, params) do
+ def remote(%{private: %{open_api_spex: %{params: params}}} = conn, _) do
with {:ok, packs} <-
Pack.list_remote(url: params.url, page_size: params.page_size, page: params.page) do
json(conn, packs)
@@ -38,7 +38,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
end
end
- def index(conn, params) do
+ def index(%{private: %{open_api_spex: %{params: params}}} = conn, _) do
emoji_path =
[:instance, :static_dir]
|> Pleroma.Config.get!()
@@ -61,7 +61,11 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
end
end
- def show(conn, %{name: name, page: page, page_size: page_size}) do
+ def show(
+ %{private: %{open_api_spex: %{params: %{name: name, page: page, page_size: page_size}}}} =
+ conn,
+ _
+ ) do
name = String.trim(name)
with {:ok, pack} <- Pack.show(name: name, page: page, page_size: page_size) do
@@ -90,7 +94,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
end
end
- def archive(conn, %{name: name}) do
+ def archive(%{private: %{open_api_spex: %{params: %{name: name}}}} = conn, _) do
with {:ok, archive} <- Pack.get_archive(name) do
send_download(conn, {:binary, archive}, filename: "#{name}.zip")
else
@@ -109,7 +113,10 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
end
end
- def download(%{body_params: %{url: url, name: name} = params} = conn, _) do
+ def download(
+ %{private: %{open_api_spex: %{body_params: %{url: url, name: name} = params}}} = conn,
+ _
+ ) do
with {:ok, _pack} <- Pack.download(name, url, params[:as]) do
json(conn, "ok")
else
@@ -130,7 +137,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
end
end
- def create(conn, %{name: name}) do
+ def create(%{private: %{open_api_spex: %{params: %{name: name}}}} = conn, _) do
name = String.trim(name)
with {:ok, _pack} <- Pack.create(name) do
@@ -159,7 +166,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
end
end
- def delete(conn, %{name: name}) do
+ def delete(%{private: %{open_api_spex: %{params: %{name: name}}}} = conn, _) do
name = String.trim(name)
with {:ok, deleted} when deleted != [] <- Pack.delete(name) do
@@ -184,7 +191,11 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
end
end
- def update(%{body_params: %{metadata: metadata}} = conn, %{name: name}) do
+ def update(
+ %{private: %{open_api_spex: %{body_params: %{metadata: metadata}, params: %{name: name}}}} =
+ conn,
+ _
+ ) do
with {:ok, pack} <- Pack.update_metadata(name, metadata) do
json(conn, pack.pack)
else
diff --git a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex
index 66e9d8481..3208cde98 100644
--- a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex
@@ -10,7 +10,7 @@ defmodule Pleroma.Web.PleromaAPI.MascotController do
alias Pleroma.Web.Plugs.OAuthScopesPlug
plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:update])
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(OAuthScopesPlug, %{scopes: ["read:accounts"]} when action == :show)
plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action != :show)
@@ -22,9 +22,13 @@ defmodule Pleroma.Web.PleromaAPI.MascotController do
end
@doc "PUT /api/v1/pleroma/mascot"
- def update(%{assigns: %{user: user}, body_params: %{file: file}} = conn, _) do
+ def update(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: %{file: file}}}} =
+ conn,
+ _
+ ) do
with {:content_type, "image" <> _} <- {:content_type, file.content_type},
- {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)) do
+ {_, {:ok, object}} <- {:upload, ActivityPub.upload(file, actor: User.ap_id(user))} do
attachment = render_attachment(object)
{:ok, _user} = User.mascot_update(user, attachment)
@@ -32,6 +36,9 @@ defmodule Pleroma.Web.PleromaAPI.MascotController do
else
{:content_type, _} ->
render_error(conn, :unsupported_media_type, "mascots can only be images")
+
+ {:upload, {:error, _}} ->
+ render_error(conn, :error, "error uploading file")
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 87ea81cef..435ccfabe 100644
--- a/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex
@@ -7,7 +7,7 @@ defmodule Pleroma.Web.PleromaAPI.NotificationController do
alias Pleroma.Notification
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(
Pleroma.Web.Plugs.OAuthScopesPlug,
@@ -16,9 +16,16 @@ defmodule Pleroma.Web.PleromaAPI.NotificationController do
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaNotificationOperation
- def mark_as_read(%{assigns: %{user: user}, body_params: %{id: notification_id}} = conn, _) do
- with {:ok, notification} <- Notification.read_one(user, notification_id) do
- render(conn, "show.json", notification: notification, for: user)
+ def mark_as_read(
+ %{
+ assigns: %{user: user},
+ private: %{open_api_spex: %{body_params: %{id: notification_id}}}
+ } = conn,
+ _
+ ) do
+ with {:ok, _} <- Notification.read_one(user, notification_id) do
+ conn
+ |> json("ok")
else
{:error, message} ->
conn
@@ -27,12 +34,19 @@ defmodule Pleroma.Web.PleromaAPI.NotificationController do
end
end
- def mark_as_read(%{assigns: %{user: user}, body_params: %{max_id: max_id}} = conn, _) do
- notifications =
- user
- |> Notification.set_read_up_to(max_id)
- |> Enum.take(80)
-
- render(conn, "index.json", notifications: notifications, for: user)
+ def mark_as_read(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: %{max_id: max_id}}}} =
+ conn,
+ _
+ ) do
+ with {:ok, _} <- Notification.set_read_up_to(user, max_id) do
+ conn
+ |> json("ok")
+ else
+ {:error, message} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{"error" => message})
+ end
end
end
diff --git a/lib/pleroma/web/pleroma_api/controllers/status_controller.ex b/lib/pleroma/web/pleroma_api/controllers/status_controller.ex
new file mode 100644
index 000000000..482662fdd
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/status_controller.ex
@@ -0,0 +1,66 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.StatusController do
+ use Pleroma.Web, :controller
+
+ import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
+
+ require Ecto.Query
+ require Pleroma.Constants
+
+ alias Pleroma.Activity
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Visibility
+ alias Pleroma.Web.MastodonAPI.StatusView
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+ action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["read:statuses"], fallback: :proceed_unauthenticated} when action == :quotes
+ )
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaStatusOperation
+
+ @doc "GET /api/v1/pleroma/statuses/:id/quotes"
+ def quotes(%{assigns: %{user: user}} = conn, %{id: id} = params) do
+ with %Activity{object: object} = activity <- Activity.get_by_id_with_object(id),
+ true <- Visibility.visible_for_user?(activity, user) do
+ params =
+ params
+ |> Map.put(:type, "Create")
+ |> Map.put(:blocking_user, user)
+ |> Map.put(:quote_url, object.data["id"])
+
+ recipients =
+ if user do
+ [Pleroma.Constants.as_public()] ++ [user.ap_id | User.following(user)]
+ else
+ [Pleroma.Constants.as_public()]
+ end
+
+ activities =
+ recipients
+ |> ActivityPub.fetch_activities(params)
+ |> Enum.reverse()
+
+ conn
+ |> add_link_headers(activities)
+ |> put_view(StatusView)
+ |> render("index.json",
+ activities: activities,
+ for: user,
+ as: :activity
+ )
+ else
+ nil -> {:error, :not_found}
+ false -> {:error, :not_found}
+ end
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex b/lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex
index 90428a532..96466f192 100644
--- a/lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex
@@ -15,14 +15,21 @@ defmodule Pleroma.Web.PleromaAPI.UserImportController do
plug(OAuthScopesPlug, %{scopes: ["follow", "write:blocks"]} when action == :blocks)
plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action == :mutes)
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
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)}}, %{})
+ def follow(
+ %{private: %{open_api_spex: %{body_params: %{list: %Plug.Upload{path: path}}}}} = conn,
+ _
+ ) do
+ list = File.read!(path)
+ do_follow(conn, list)
end
- def follow(%{assigns: %{user: follower}, body_params: %{list: list}} = conn, _) do
+ def follow(%{private: %{open_api_spex: %{body_params: %{list: list}}}} = conn, _),
+ do: do_follow(conn, list)
+
+ def do_follow(%{assigns: %{user: follower}} = conn, list) do
identifiers =
list
|> String.split("\n")
@@ -35,20 +42,34 @@ defmodule Pleroma.Web.PleromaAPI.UserImportController do
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)}}, %{})
+ def blocks(
+ %{private: %{open_api_spex: %{body_params: %{list: %Plug.Upload{path: path}}}}} = conn,
+ _
+ ) do
+ list = File.read!(path)
+ do_block(conn, list)
end
- def blocks(%{assigns: %{user: blocker}, body_params: %{list: list}} = conn, _) do
+ def blocks(%{private: %{open_api_spex: %{body_params: %{list: list}}}} = conn, _),
+ do: do_block(conn, list)
+
+ defp do_block(%{assigns: %{user: blocker}} = conn, list) 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)}}, %{})
+ def mutes(
+ %{private: %{open_api_spex: %{body_params: %{list: %Plug.Upload{path: path}}}}} = conn,
+ _
+ ) do
+ list = File.read!(path)
+ do_mute(conn, list)
end
- def mutes(%{assigns: %{user: user}, body_params: %{list: list}} = conn, _) do
+ def mutes(%{private: %{open_api_spex: %{body_params: %{list: list}}}} = conn, _),
+ do: do_mute(conn, list)
+
+ defp do_mute(%{assigns: %{user: user}} = conn, list) do
User.Import.mutes_import(user, prepare_user_identifiers(list))
json(conn, "job started")
end
diff --git a/lib/pleroma/web/pleroma_api/views/backup_view.ex b/lib/pleroma/web/pleroma_api/views/backup_view.ex
index 20403aeee..d778590f0 100644
--- a/lib/pleroma/web/pleroma_api/views/backup_view.ex
+++ b/lib/pleroma/web/pleroma_api/views/backup_view.ex
@@ -9,22 +9,12 @@ defmodule Pleroma.Web.PleromaAPI.BackupView do
alias Pleroma.Web.CommonAPI.Utils
def render("show.json", %{backup: %Backup{} = backup}) do
- # To deal with records before the migration
- state =
- if backup.state == :invalid do
- if backup.processed, do: :complete, else: :failed
- else
- backup.state
- end
-
%{
id: backup.id,
content_type: backup.content_type,
url: download_url(backup),
file_size: backup.file_size,
processed: backup.processed,
- state: to_string(state),
- processed_number: backup.processed_number,
inserted_at: Utils.to_masto_date(backup.inserted_at)
}
end
diff --git a/lib/pleroma/web/pleroma_api/views/bookmark_folder_view.ex b/lib/pleroma/web/pleroma_api/views/bookmark_folder_view.ex
new file mode 100644
index 000000000..12decb816
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/views/bookmark_folder_view.ex
@@ -0,0 +1,42 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.BookmarkFolderView do
+ use Pleroma.Web, :view
+
+ alias Pleroma.BookmarkFolder
+ alias Pleroma.Emoji
+ alias Pleroma.Web.Endpoint
+
+ def render("show.json", %{folder: %BookmarkFolder{} = folder}) do
+ %{
+ id: folder.id |> to_string(),
+ name: folder.name,
+ emoji: folder.emoji,
+ emoji_url: get_emoji_url(folder.emoji)
+ }
+ end
+
+ def render("index.json", %{folders: folders} = opts) do
+ render_many(folders, __MODULE__, "show.json", Map.delete(opts, :folders))
+ end
+
+ defp get_emoji_url(nil) do
+ nil
+ end
+
+ defp get_emoji_url(emoji) do
+ if Emoji.unicode?(emoji) do
+ nil
+ else
+ emoji = Emoji.get(emoji)
+
+ if emoji != nil do
+ Endpoint.url() |> URI.merge(emoji.file) |> to_string()
+ else
+ nil
+ end
+ end
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/views/chat/message_reference_view.ex b/lib/pleroma/web/pleroma_api/views/chat/message_reference_view.ex
index 241bf0010..a1c88d075 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
@@ -9,6 +9,7 @@ defmodule Pleroma.Web.PleromaAPI.Chat.MessageReferenceView do
alias Pleroma.User
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.StatusView
+ alias Pleroma.Web.RichMedia.Card
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
@@ -23,6 +24,12 @@ defmodule Pleroma.Web.PleromaAPI.Chat.MessageReferenceView do
}
}
) do
+ card =
+ case Card.get_by_object(object) do
+ %Card{} = card_data -> StatusView.render("card.json", card_data)
+ _ -> nil
+ end
+
%{
id: id |> to_string(),
content: chat_message["content"],
@@ -34,11 +41,7 @@ defmodule Pleroma.Web.PleromaAPI.Chat.MessageReferenceView do
chat_message["attachment"] &&
StatusView.render("attachment.json", attachment: chat_message["attachment"]),
unread: unread,
- card:
- StatusView.render(
- "card.json",
- Pleroma.Web.RichMedia.Helpers.fetch_data_for_object(object)
- )
+ card: card
}
|> put_idempotency_key()
end
diff --git a/lib/pleroma/web/pleroma_api/views/scrobble_view.ex b/lib/pleroma/web/pleroma_api/views/scrobble_view.ex
index a5985fb2a..edf0a2390 100644
--- a/lib/pleroma/web/pleroma_api/views/scrobble_view.ex
+++ b/lib/pleroma/web/pleroma_api/views/scrobble_view.ex
@@ -27,6 +27,7 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleView do
title: object.data["title"] |> HTML.strip_tags(),
artist: object.data["artist"] |> HTML.strip_tags(),
album: object.data["album"] |> HTML.strip_tags(),
+ externalLink: object.data["externalLink"],
length: object.data["length"]
}
end
diff --git a/lib/pleroma/web/plugs/cache.ex b/lib/pleroma/web/plugs/cache.ex
index 667477857..5a7e86ef7 100644
--- a/lib/pleroma/web/plugs/cache.ex
+++ b/lib/pleroma/web/plugs/cache.ex
@@ -20,7 +20,7 @@ defmodule Pleroma.Web.Plugs.Cache do
- `ttl`: An expiration time (time-to-live). This value should be in milliseconds or `nil` to disable expiration. Defaults to `nil`.
- `query_params`: Take URL query string into account (`true`), ignore it (`false`) or limit to specific params only (list). Defaults to `true`.
- - `tracking_fun`: A function that is called on successfull responses, no matter if the request is cached or not. It should accept a conn as the first argument and the value assigned to `tracking_fun_data` as the second.
+ - `tracking_fun`: A function that is called on successful responses, no matter if the request is cached or not. It should accept a conn as the first argument and the value assigned to `tracking_fun_data` as the second.
Additionally, you can overwrite the TTL inside a controller action by assigning `cache_ttl` to the connection struct:
diff --git a/lib/pleroma/web/plugs/http_security_plug.ex b/lib/pleroma/web/plugs/http_security_plug.ex
index 5093414c4..38f6c511e 100644
--- a/lib/pleroma/web/plugs/http_security_plug.ex
+++ b/lib/pleroma/web/plugs/http_security_plug.ex
@@ -3,26 +3,27 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.HTTPSecurityPlug do
- alias Pleroma.Config
import Plug.Conn
require Logger
+ @config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config)
+
def init(opts), do: opts
def call(conn, _options) do
- if Config.get([:http_security, :enabled]) do
+ if @config_impl.get([:http_security, :enabled]) do
conn
|> merge_resp_headers(headers())
- |> maybe_send_sts_header(Config.get([:http_security, :sts]))
+ |> maybe_send_sts_header(@config_impl.get([:http_security, :sts]))
else
conn
end
end
def primary_frontend do
- with %{"name" => frontend} <- Config.get([:frontends, :primary]),
- available <- Config.get([:frontends, :available]),
+ with %{"name" => frontend} <- @config_impl.get([:frontends, :primary]),
+ available <- @config_impl.get([:frontends, :available]),
%{} = primary_frontend <- Map.get(available, frontend) do
{:ok, primary_frontend}
end
@@ -37,8 +38,8 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlug do
end
def headers do
- referrer_policy = Config.get([:http_security, :referrer_policy])
- report_uri = Config.get([:http_security, :report_uri])
+ referrer_policy = @config_impl.get([:http_security, :referrer_policy])
+ report_uri = @config_impl.get([:http_security, :report_uri])
custom_http_frontend_headers = custom_http_frontend_headers()
headers = [
@@ -86,10 +87,10 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlug do
@csp_start [Enum.join(static_csp_rules, ";") <> ";"]
defp csp_string do
- scheme = Config.get([Pleroma.Web.Endpoint, :url])[:scheme]
+ scheme = @config_impl.get([Pleroma.Web.Endpoint, :url])[:scheme]
static_url = Pleroma.Web.Endpoint.static_url()
websocket_url = Pleroma.Web.Endpoint.websocket_url()
- report_uri = Config.get([:http_security, :report_uri])
+ report_uri = @config_impl.get([:http_security, :report_uri])
img_src = "img-src 'self' data: blob:"
media_src = "media-src 'self'"
@@ -97,8 +98,8 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlug do
# Strict multimedia CSP enforcement only when MediaProxy is enabled
{img_src, media_src, connect_src} =
- if Config.get([:media_proxy, :enabled]) &&
- !Config.get([:media_proxy, :proxy_opts, :redirect_on_failure]) do
+ if @config_impl.get([:media_proxy, :enabled]) &&
+ !@config_impl.get([:media_proxy, :proxy_opts, :redirect_on_failure]) do
sources = build_csp_multimedia_source_list()
{
@@ -115,17 +116,21 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlug do
end
connect_src =
- if Config.get(:env) == :dev do
+ if @config_impl.get([:env]) == :dev do
[connect_src, " http://localhost:3035/"]
else
connect_src
end
script_src =
- if Config.get(:env) == :dev do
- "script-src 'self' 'unsafe-eval'"
+ if @config_impl.get([:http_security, :allow_unsafe_eval]) do
+ if @config_impl.get([:env]) == :dev do
+ "script-src 'self' 'unsafe-eval'"
+ else
+ "script-src 'self' 'wasm-unsafe-eval'"
+ end
else
- "script-src 'self' 'wasm-unsafe-eval'"
+ "script-src 'self'"
end
report = if report_uri, do: ["report-uri ", report_uri, ";report-to csp-endpoint"]
@@ -161,11 +166,11 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlug do
defp build_csp_multimedia_source_list do
media_proxy_whitelist =
[:media_proxy, :whitelist]
- |> Config.get()
+ |> @config_impl.get()
|> build_csp_from_whitelist([])
- captcha_method = Config.get([Pleroma.Captcha, :method])
- captcha_endpoint = Config.get([captcha_method, :endpoint])
+ captcha_method = @config_impl.get([Pleroma.Captcha, :method])
+ captcha_endpoint = @config_impl.get([captcha_method, :endpoint])
base_endpoints =
[
@@ -173,7 +178,7 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlug do
[Pleroma.Upload, :base_url],
[Pleroma.Uploaders.S3, :public_endpoint]
]
- |> Enum.map(&Config.get/1)
+ |> Enum.map(&@config_impl.get/1)
[captcha_endpoint | base_endpoints]
|> Enum.map(&build_csp_param/1)
@@ -200,8 +205,8 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlug do
end
def warn_if_disabled do
- unless Config.get([:http_security, :enabled]) do
- Logger.warn("
+ unless Pleroma.Config.get([:http_security, :enabled]) do
+ Logger.warning("
.i;;;;i.
iYcviii;vXY:
.YXi .i1c.
@@ -245,8 +250,8 @@ your instance and your users via malicious posts:
end
defp maybe_send_sts_header(conn, true) do
- max_age_sts = Config.get([:http_security, :sts_max_age])
- max_age_ct = Config.get([:http_security, :ct_max_age])
+ max_age_sts = @config_impl.get([:http_security, :sts_max_age])
+ max_age_ct = @config_impl.get([:http_security, :ct_max_age])
merge_resp_headers(conn, [
{"strict-transport-security", "max-age=#{max_age_sts}; includeSubDomains"},
diff --git a/lib/pleroma/web/plugs/http_signature_plug.ex b/lib/pleroma/web/plugs/http_signature_plug.ex
index e814efc2c..67974599a 100644
--- a/lib/pleroma/web/plugs/http_signature_plug.ex
+++ b/lib/pleroma/web/plugs/http_signature_plug.ex
@@ -3,10 +3,18 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
+ alias Pleroma.Helpers.InetHelper
+
import Plug.Conn
import Phoenix.Controller, only: [get_format: 1, text: 2]
+
+ alias Pleroma.Signature
+ alias Pleroma.Web.ActivityPub.MRF
+
require Logger
+ @config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config)
+
def init(options) do
options
end
@@ -19,54 +27,14 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
if get_format(conn) in ["json", "activity+json"] do
conn
|> maybe_assign_valid_signature()
+ |> maybe_assign_actor_id()
|> maybe_require_signature()
+ |> maybe_filter_requests()
else
conn
end
end
- defp validate_signature(conn, request_target) do
- # Newer drafts for HTTP signatures now use @request-target instead of the
- # old (request-target). We'll now support both for incoming signatures.
- conn =
- conn
- |> put_req_header("(request-target)", request_target)
- |> put_req_header("@request-target", request_target)
-
- HTTPSignatures.validate_conn(conn)
- end
-
- defp validate_signature(conn) do
- # This (request-target) is non-standard, but many implementations do it
- # this way due to a misinterpretation of
- # https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-06
- # "path" was interpreted as not having the query, though later examples
- # show that it must be the absolute path + query. This behavior is kept to
- # make sure most software (Pleroma itself, Mastodon, and probably others)
- # do not break.
- request_target = String.downcase("#{conn.method}") <> " #{conn.request_path}"
-
- # This is the proper way to build the @request-target, as expected by
- # many HTTP signature libraries, clarified in the following draft:
- # https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-11.html#section-2.2.6
- # It is the same as before, but containing the query part as well.
- proper_target = request_target <> "?#{conn.query_string}"
-
- cond do
- # Normal, non-standard behavior but expected by Pleroma and more.
- validate_signature(conn, request_target) ->
- true
-
- # Has query string and the previous one failed: let's try the standard.
- conn.query_string != "" ->
- validate_signature(conn, proper_target)
-
- # If there's no query string and signature fails, it's rotten.
- true ->
- false
- end
- end
-
defp maybe_assign_valid_signature(conn) do
if has_signature_header?(conn) do
# we replace the digest header with the one we computed in DigestPlug
@@ -76,27 +44,70 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
conn -> conn
end
- assign(conn, :valid_signature, validate_signature(conn))
+ assign(conn, :valid_signature, Signature.validate_signature(conn))
else
Logger.debug("No signature header!")
conn
end
end
+ defp maybe_assign_actor_id(%{assigns: %{valid_signature: true}} = conn) do
+ adapter = Application.get_env(:http_signatures, :adapter)
+
+ {:ok, actor_id} = adapter.get_actor_id(conn)
+
+ assign(conn, :actor_id, actor_id)
+ end
+
+ defp maybe_assign_actor_id(conn), do: conn
+
defp has_signature_header?(conn) do
conn |> get_req_header("signature") |> Enum.at(0, false)
end
defp maybe_require_signature(%{assigns: %{valid_signature: true}} = conn), do: conn
- defp maybe_require_signature(conn) do
- if Pleroma.Config.get([:activitypub, :authorized_fetch_mode], false) do
+ defp maybe_require_signature(%{remote_ip: remote_ip} = conn) do
+ if @config_impl.get([:activitypub, :authorized_fetch_mode], false) do
+ exceptions =
+ @config_impl.get([:activitypub, :authorized_fetch_mode_exceptions], [])
+ |> Enum.map(&InetHelper.parse_cidr/1)
+
+ if Enum.any?(exceptions, fn x -> InetCidr.contains?(x, remote_ip) end) do
+ conn
+ else
+ conn
+ |> put_status(:unauthorized)
+ |> text("Request not signed")
+ |> halt()
+ end
+ else
conn
- |> put_status(:unauthorized)
- |> text("Request not signed")
- |> halt()
+ end
+ end
+
+ defp maybe_filter_requests(%{halted: true} = conn), do: conn
+
+ defp maybe_filter_requests(conn) do
+ if @config_impl.get([:activitypub, :authorized_fetch_mode], false) and
+ conn.assigns[:actor_id] do
+ %{host: host} = URI.parse(conn.assigns.actor_id)
+
+ if MRF.subdomain_match?(rejected_domains(), host) do
+ conn
+ |> put_status(:unauthorized)
+ |> halt()
+ else
+ conn
+ end
else
conn
end
end
+
+ defp rejected_domains do
+ @config_impl.get([:instance, :rejected_instances])
+ |> Pleroma.Web.ActivityPub.MRF.instance_list_from_tuples()
+ |> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
+ end
end
diff --git a/lib/pleroma/web/plugs/logger_metadata_path.ex b/lib/pleroma/web/plugs/logger_metadata_path.ex
new file mode 100644
index 000000000..a5553cfc8
--- /dev/null
+++ b/lib/pleroma/web/plugs/logger_metadata_path.ex
@@ -0,0 +1,12 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.LoggerMetadataPath do
+ def init(opts), do: opts
+
+ def call(conn, _) do
+ Logger.metadata(path: conn.request_path)
+ conn
+ end
+end
diff --git a/lib/pleroma/web/plugs/logger_metadata_user.ex b/lib/pleroma/web/plugs/logger_metadata_user.ex
new file mode 100644
index 000000000..6a5c0041d
--- /dev/null
+++ b/lib/pleroma/web/plugs/logger_metadata_user.ex
@@ -0,0 +1,18 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.LoggerMetadataUser do
+ alias Pleroma.User
+
+ def init(opts), do: opts
+
+ def call(%{assigns: %{user: user = %User{}}} = conn, _) do
+ Logger.metadata(user: user.nickname)
+ conn
+ end
+
+ def call(conn, _) do
+ conn
+ end
+end
diff --git a/lib/pleroma/web/plugs/o_auth_plug.ex b/lib/pleroma/web/plugs/o_auth_plug.ex
index ba04ddb72..488968691 100644
--- a/lib/pleroma/web/plugs/o_auth_plug.ex
+++ b/lib/pleroma/web/plugs/o_auth_plug.ex
@@ -23,14 +23,14 @@ defmodule Pleroma.Web.Plugs.OAuthPlug do
def call(conn, _) do
with {:ok, token_str} <- fetch_token_str(conn) do
with {:ok, user, user_token} <- fetch_user_and_token(token_str),
- false <- Token.is_expired?(user_token) do
+ false <- Token.expired?(user_token) do
conn
|> assign(:token, user_token)
|> assign(:user, user)
else
_ ->
with {:ok, app, app_token} <- fetch_app_and_token(token_str),
- false <- Token.is_expired?(app_token) do
+ false <- Token.expired?(app_token) do
conn
|> assign(:token, app_token)
|> assign(:app, app)
@@ -52,7 +52,7 @@ defmodule Pleroma.Web.Plugs.OAuthPlug do
where: t.token == ^token
)
- with %Token{user_id: user_id} = token_record <- Repo.one(token_query),
+ with %Token{user_id: user_id} = token_record <- Repo.one(token_query) |> Repo.preload(:user),
false <- is_nil(user_id),
%User{} = user <- User.get_cached_by_id(user_id) do
{:ok, user, token_record}
diff --git a/lib/pleroma/web/plugs/o_auth_scopes_plug.ex b/lib/pleroma/web/plugs/o_auth_scopes_plug.ex
index faf0fd8c6..08c2f61ea 100644
--- a/lib/pleroma/web/plugs/o_auth_scopes_plug.ex
+++ b/lib/pleroma/web/plugs/o_auth_scopes_plug.ex
@@ -34,7 +34,9 @@ defmodule Pleroma.Web.Plugs.OAuthScopesPlug do
permissions = Enum.join(missing_scopes, " #{op} ")
error_message =
- dgettext("errors", "Insufficient permissions: %{permissions}.", permissions: permissions)
+ dgettext("errors", "Insufficient permissions: %{permissions}.",
+ permissions: permissions
+ )
conn
|> put_resp_content_type("application/json")
diff --git a/lib/pleroma/web/plugs/rate_limiter.ex b/lib/pleroma/web/plugs/rate_limiter.ex
index 2080b06bd..aa79dbf6b 100644
--- a/lib/pleroma/web/plugs/rate_limiter.ex
+++ b/lib/pleroma/web/plugs/rate_limiter.ex
@@ -89,7 +89,7 @@ defmodule Pleroma.Web.Plugs.RateLimiter do
end
defp handle_disabled(conn) do
- Logger.warn(
+ Logger.warning(
"Rate limiter disabled due to forwarded IP not being found. Please ensure your reverse proxy is providing the X-Forwarded-For header or disable the RemoteIP plug/rate limiter."
)
diff --git a/lib/pleroma/web/plugs/rate_limiter/supervisor.ex b/lib/pleroma/web/plugs/rate_limiter/supervisor.ex
index f00f3d95e..5f79a3e3e 100644
--- a/lib/pleroma/web/plugs/rate_limiter/supervisor.ex
+++ b/lib/pleroma/web/plugs/rate_limiter/supervisor.ex
@@ -14,7 +14,7 @@ defmodule Pleroma.Web.Plugs.RateLimiter.Supervisor do
Pleroma.Web.Plugs.RateLimiter.LimiterSupervisor
]
- opts = [strategy: :one_for_one, name: Pleroma.Web.Streamer.Supervisor]
+ opts = [strategy: :one_for_one]
Supervisor.init(children, opts)
end
end
diff --git a/lib/pleroma/web/plugs/remote_ip.ex b/lib/pleroma/web/plugs/remote_ip.ex
index f207d9fef..3a4bffb50 100644
--- a/lib/pleroma/web/plugs/remote_ip.ex
+++ b/lib/pleroma/web/plugs/remote_ip.ex
@@ -8,6 +8,7 @@ defmodule Pleroma.Web.Plugs.RemoteIp do
"""
alias Pleroma.Config
+ alias Pleroma.Helpers.InetHelper
import Plug.Conn
@behaviour Plug
@@ -30,19 +31,8 @@ defmodule Pleroma.Web.Plugs.RemoteIp do
proxies =
Config.get([__MODULE__, :proxies], [])
|> Enum.concat(reserved)
- |> Enum.map(&maybe_add_cidr/1)
+ |> Enum.map(&InetHelper.parse_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/web/plugs/uploaded_media.ex b/lib/pleroma/web/plugs/uploaded_media.ex
index 8b3bc9acb..f1076da1b 100644
--- a/lib/pleroma/web/plugs/uploaded_media.ex
+++ b/lib/pleroma/web/plugs/uploaded_media.ex
@@ -105,7 +105,7 @@ defmodule Pleroma.Web.Plugs.UploadedMedia do
end
defp get_media(conn, unknown, _, _) do
- Logger.error("#{__MODULE__}: Unknown get startegy: #{inspect(unknown)}")
+ Logger.error("#{__MODULE__}: Unknown get strategy: #{inspect(unknown)}")
conn
|> send_resp(:internal_server_error, dgettext("errors", "Internal Error"))
diff --git a/lib/pleroma/web/push.ex b/lib/pleroma/web/push.ex
index 9665b0b4a..d4693f63e 100644
--- a/lib/pleroma/web/push.ex
+++ b/lib/pleroma/web/push.ex
@@ -9,7 +9,7 @@ defmodule Pleroma.Web.Push do
def init do
unless enabled() do
- Logger.warn("""
+ Logger.warning("""
VAPID key pair is not found. If you wish to enabled web push, please run
mix web_push.gen.keypair
@@ -20,17 +20,13 @@ defmodule Pleroma.Web.Push do
end
def vapid_config do
- Application.get_env(:web_push_encryption, :vapid_details, [])
+ Application.get_env(:web_push_encryption, :vapid_details, nil)
end
- def enabled do
- case vapid_config() do
- [] -> false
- list when is_list(list) -> true
- _ -> false
- end
- end
+ def enabled, do: match?([subject: _, public_key: _, private_key: _], vapid_config())
+ @spec send(Pleroma.Notification.t()) ::
+ {:ok, Oban.Job.t()} | {:error, Oban.Job.changeset() | term()}
def send(notification) do
WebPusherWorker.enqueue("web_push", %{"notification_id" => notification.id})
end
diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex
index 3c5f00764..d71e134cb 100644
--- a/lib/pleroma/web/push/impl.ex
+++ b/lib/pleroma/web/push/impl.ex
@@ -16,62 +16,70 @@ defmodule Pleroma.Web.Push.Impl do
require Logger
import Ecto.Query
+ @body_chars 140
@types ["Create", "Follow", "Announce", "Like", "Move", "EmojiReact", "Update"]
- @doc "Performs sending notifications for user subscriptions"
- @spec perform(Notification.t()) :: list(any) | :error | {:error, :unknown_type}
- def perform(
+ @doc "Builds webpush notification payloads for the subscriptions enabled by the receiving user"
+ @spec build(Notification.t()) ::
+ list(%{content: map(), subscription: Subscription.t()}) | []
+ def build(
%{
activity: %{data: %{"type" => activity_type}} = activity,
- user: %User{id: user_id}
+ user_id: user_id
} = notification
)
when activity_type in @types do
- actor = User.get_cached_by_ap_id(notification.activity.data["actor"])
+ notification_actor = User.get_cached_by_ap_id(notification.activity.data["actor"])
+ avatar_url = User.avatar_url(notification_actor)
- mastodon_type = notification.type
- gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key)
- avatar_url = User.avatar_url(actor)
object = Object.normalize(activity, fetch: false)
user = User.get_cached_by_id(user_id)
direct_conversation_id = Activity.direct_conversation_id(activity, user)
- for subscription <- fetch_subscriptions(user_id),
- Subscription.enabled?(subscription, mastodon_type) do
- %{
- access_token: subscription.token.token,
- notification_id: notification.id,
- notification_type: mastodon_type,
- icon: avatar_url,
- preferred_locale: "en",
- pleroma: %{
- activity_id: notification.activity.id,
- direct_conversation_id: direct_conversation_id
+ subscriptions = fetch_subscriptions(user_id)
+
+ subscriptions
+ |> Enum.filter(&Subscription.enabled?(&1, notification.type))
+ |> Enum.map(fn subscription ->
+ payload =
+ %{
+ access_token: subscription.token.token,
+ notification_id: notification.id,
+ notification_type: notification.type,
+ icon: avatar_url,
+ preferred_locale: "en",
+ pleroma: %{
+ activity_id: notification.activity.id,
+ direct_conversation_id: direct_conversation_id
+ }
}
- }
- |> Map.merge(build_content(notification, actor, object, mastodon_type))
- |> Jason.encode!()
- |> push_message(build_sub(subscription), gcm_api_key, subscription)
- end
- |> (&{:ok, &1}).()
+ |> Map.merge(build_content(notification, notification_actor, object))
+ |> Jason.encode!()
+
+ %{payload: payload, subscription: subscription}
+ end)
end
- def perform(_) do
- Logger.warn("Unknown notification type")
- {:error, :unknown_type}
+ def build(notif) do
+ Logger.warning("WebPush: unknown activity type: #{inspect(notif)}")
+ []
end
- @doc "Push message to web"
- def push_message(body, sub, api_key, subscription) do
- case WebPushEncryption.send_web_push(body, sub, api_key) do
+ @doc "Deliver push notification to the provided webpush subscription"
+ @spec deliver(%{payload: String.t(), subscription: Subscription.t()}) :: :ok | :error
+ def deliver(%{payload: payload, subscription: subscription}) do
+ gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key)
+ formatted_subscription = build_sub(subscription)
+
+ case WebPushEncryption.send_web_push(payload, formatted_subscription, gcm_api_key) do
+ {:ok, %{status: code}} when code in 200..299 ->
+ :ok
+
{:ok, %{status: code}} when code in 400..499 ->
Logger.debug("Removing subscription record")
Repo.delete!(subscription)
:ok
- {:ok, %{status: code}} when code in 200..299 ->
- :ok
-
{:ok, %{status: code}} ->
Logger.error("Web Push Notification failed with code: #{code}")
:error
@@ -100,106 +108,106 @@ defmodule Pleroma.Web.Push.Impl do
}
end
- def build_content(notification, actor, object, mastodon_type \\ nil)
-
def build_content(
%{
user: %{notification_settings: %{hide_notification_contents: true}}
} = notification,
- _actor,
- _object,
- mastodon_type
+ _user,
+ _object
) do
- %{body: format_title(notification, mastodon_type)}
+ %{body: format_title(notification)}
end
- def build_content(notification, actor, object, mastodon_type) do
- mastodon_type = mastodon_type || notification.type
-
+ def build_content(notification, user, object) do
%{
- title: format_title(notification, mastodon_type),
- body: format_body(notification, actor, object, mastodon_type)
+ title: format_title(notification),
+ body: format_body(notification, user, object)
}
end
- def format_body(activity, actor, object, mastodon_type \\ nil)
-
- def format_body(_activity, actor, %{data: %{"type" => "ChatMessage"} = data}, _) do
- case data["content"] do
- nil -> "@#{actor.nickname}: (Attachment)"
- content -> "@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}"
+ @spec format_body(Notification.t(), User.t(), Object.t()) :: String.t()
+ def format_body(_notification, user, %{data: %{"type" => "ChatMessage"} = object}) do
+ case object["content"] do
+ nil -> "@#{user.nickname}: (Attachment)"
+ content -> "@#{user.nickname}: #{Utils.scrub_html_and_truncate(content, @body_chars)}"
end
end
def format_body(
+ %{type: "poll"} = _notification,
+ _user,
+ %{data: %{"content" => content} = data} = _object
+ ) do
+ options = Map.get(data, "anyOf") || Map.get(data, "oneOf")
+
+ content_text = content <> "\n"
+
+ options_text = Enum.map_join(options, "\n", fn x -> "○ #{x["name"]}" end)
+
+ [content_text, options_text]
+ |> Enum.join("\n")
+ |> Utils.scrub_html_and_truncate(@body_chars)
+ end
+
+ def format_body(
%{activity: %{data: %{"type" => "Create"}}},
- actor,
- %{data: %{"content" => content}},
- _mastodon_type
+ user,
+ %{data: %{"content" => content}}
) do
- "@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}"
+ "@#{user.nickname}: #{Utils.scrub_html_and_truncate(content, @body_chars)}"
end
def format_body(
%{activity: %{data: %{"type" => "Announce"}}},
- actor,
- %{data: %{"content" => content}},
- _mastodon_type
+ user,
+ %{data: %{"content" => content}}
) do
- "@#{actor.nickname} repeated: #{Utils.scrub_html_and_truncate(content, 80)}"
+ "@#{user.nickname} repeated: #{Utils.scrub_html_and_truncate(content, @body_chars)}"
end
def format_body(
%{activity: %{data: %{"type" => "EmojiReact", "content" => content}}},
- actor,
- _object,
- _mastodon_type
+ user,
+ _object
) do
- "@#{actor.nickname} reacted with #{content}"
+ "@#{user.nickname} reacted with #{content}"
end
def format_body(
%{activity: %{data: %{"type" => type}}} = notification,
- actor,
- _object,
- mastodon_type
+ user,
+ _object
)
when type in ["Follow", "Like"] do
- mastodon_type = mastodon_type || notification.type
-
- case mastodon_type do
- "follow" -> "@#{actor.nickname} has followed you"
- "follow_request" -> "@#{actor.nickname} has requested to follow you"
- "favourite" -> "@#{actor.nickname} has favorited your post"
+ case notification.type do
+ "follow" -> "@#{user.nickname} has followed you"
+ "follow_request" -> "@#{user.nickname} has requested to follow you"
+ "favourite" -> "@#{user.nickname} has favorited your post"
end
end
def format_body(
%{activity: %{data: %{"type" => "Update"}}},
- actor,
- _object,
- _mastodon_type
+ user,
+ _object
) do
- "@#{actor.nickname} edited a status"
+ "@#{user.nickname} edited a status"
end
- def format_title(activity, mastodon_type \\ nil)
-
- def format_title(%{activity: %{data: %{"directMessage" => true}}}, _mastodon_type) do
+ @spec format_title(Notification.t()) :: String.t()
+ def format_title(%{activity: %{data: %{"directMessage" => true}}}) do
"New Direct Message"
end
- def format_title(%{type: type}, mastodon_type) do
- case mastodon_type || type do
- "mention" -> "New Mention"
- "follow" -> "New Follower"
- "follow_request" -> "New Follow Request"
- "reblog" -> "New Repeat"
- "favourite" -> "New Favorite"
- "update" -> "New Update"
- "pleroma:chat_mention" -> "New Chat Message"
- "pleroma:emoji_reaction" -> "New Reaction"
- type -> "New #{String.capitalize(type || "event")}"
- end
- end
+ def format_title(%{type: "mention"}), do: "New Mention"
+ def format_title(%{type: "status"}), do: "New Status"
+ def format_title(%{type: "follow"}), do: "New Follower"
+ def format_title(%{type: "follow_request"}), do: "New Follow Request"
+ def format_title(%{type: "reblog"}), do: "New Repeat"
+ def format_title(%{type: "favourite"}), do: "New Favorite"
+ def format_title(%{type: "update"}), do: "New Update"
+ def format_title(%{type: "pleroma:chat_mention"}), do: "New Chat Message"
+ def format_title(%{type: "pleroma:emoji_reaction"}), do: "New Reaction"
+ def format_title(%{type: "poll"}), do: "Poll Results"
+ def format_title(%{type: type}), do: "New #{String.capitalize(type || "event")}"
end
diff --git a/lib/pleroma/web/rich_media/backfill.ex b/lib/pleroma/web/rich_media/backfill.ex
new file mode 100644
index 000000000..1cd90629f
--- /dev/null
+++ b/lib/pleroma/web/rich_media/backfill.ex
@@ -0,0 +1,68 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.RichMedia.Backfill do
+ alias Pleroma.Web.RichMedia.Card
+ alias Pleroma.Web.RichMedia.Helpers
+ alias Pleroma.Web.RichMedia.Parser
+ alias Pleroma.Web.RichMedia.Parser.TTL
+ alias Pleroma.Workers.RichMediaWorker
+
+ require Logger
+
+ @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
+ @stream_out_impl Pleroma.Config.get(
+ [__MODULE__, :stream_out],
+ Pleroma.Web.ActivityPub.ActivityPub
+ )
+
+ @spec run(map()) :: :ok | Parser.parse_errors() | Helpers.get_errors()
+ def run(%{"url" => url} = args) do
+ url_hash = Card.url_to_hash(url)
+
+ case Parser.parse(url) do
+ {:ok, fields} ->
+ {:ok, card} = Card.create(url, fields)
+
+ maybe_schedule_expiration(url, fields)
+
+ with %{"activity_id" => activity_id} <- args,
+ false <- is_nil(activity_id) do
+ stream_update(args)
+ end
+
+ warm_cache(url_hash, card)
+ :ok
+
+ {:error, type} = error
+ when type in [:invalid_metadata, :body_too_large, :content_type, :validate, :get, :head] ->
+ negative_cache(url_hash)
+ error
+ end
+ end
+
+ defp maybe_schedule_expiration(url, fields) do
+ case TTL.process(fields, url) do
+ {:ok, ttl} when is_number(ttl) ->
+ timestamp = DateTime.from_unix!(ttl)
+
+ RichMediaWorker.new(%{"op" => "expire", "url" => url}, scheduled_at: timestamp)
+ |> Oban.insert()
+
+ _ ->
+ :ok
+ end
+ end
+
+ defp stream_update(%{"activity_id" => activity_id}) do
+ Pleroma.Activity.get_by_id(activity_id)
+ |> Pleroma.Activity.normalize()
+ |> @stream_out_impl.stream_out()
+ end
+
+ defp warm_cache(key, val), do: @cachex.put(:rich_media_cache, key, val)
+
+ defp negative_cache(key, ttl \\ :timer.minutes(15)),
+ do: @cachex.put(:rich_media_cache, key, :error, ttl: ttl)
+end
diff --git a/lib/pleroma/web/rich_media/card.ex b/lib/pleroma/web/rich_media/card.ex
new file mode 100644
index 000000000..abad4957e
--- /dev/null
+++ b/lib/pleroma/web/rich_media/card.ex
@@ -0,0 +1,163 @@
+defmodule Pleroma.Web.RichMedia.Card do
+ use Ecto.Schema
+ import Ecto.Changeset
+ import Ecto.Query
+
+ alias Pleroma.Activity
+ alias Pleroma.HTML
+ alias Pleroma.Object
+ alias Pleroma.Repo
+ alias Pleroma.Web.RichMedia.Parser
+ alias Pleroma.Workers.RichMediaWorker
+
+ @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
+ @config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config)
+
+ @type t :: %__MODULE__{}
+
+ schema "rich_media_card" do
+ field(:url_hash, :binary)
+ field(:fields, :map)
+
+ timestamps()
+ end
+
+ @doc false
+ def changeset(card, attrs) do
+ card
+ |> cast(attrs, [:url_hash, :fields])
+ |> validate_required([:url_hash, :fields])
+ |> unique_constraint(:url_hash)
+ end
+
+ @spec create(String.t(), map()) :: {:ok, t()}
+ def create(url, fields) do
+ url_hash = url_to_hash(url)
+
+ fields = Map.put_new(fields, "url", url)
+
+ %__MODULE__{}
+ |> changeset(%{url_hash: url_hash, fields: fields})
+ |> Repo.insert(on_conflict: {:replace, [:fields]}, conflict_target: :url_hash)
+ end
+
+ @spec delete(String.t()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} | :ok
+ def delete(url) do
+ url_hash = url_to_hash(url)
+ @cachex.del(:rich_media_cache, url_hash)
+
+ case get_by_url(url) do
+ %__MODULE__{} = card -> Repo.delete(card)
+ nil -> :ok
+ end
+ end
+
+ @spec get_by_url(String.t() | nil) :: t() | nil | :error
+ def get_by_url(url) when is_binary(url) do
+ if @config_impl.get([:rich_media, :enabled]) do
+ url_hash = url_to_hash(url)
+
+ @cachex.fetch!(:rich_media_cache, url_hash, fn _ ->
+ result =
+ __MODULE__
+ |> where(url_hash: ^url_hash)
+ |> Repo.one()
+
+ case result do
+ %__MODULE__{} = card -> {:commit, card}
+ _ -> {:ignore, nil}
+ end
+ end)
+ else
+ :error
+ end
+ end
+
+ def get_by_url(nil), do: nil
+
+ @spec get_or_backfill_by_url(String.t(), keyword()) :: t() | nil
+ def get_or_backfill_by_url(url, opts \\ []) do
+ if @config_impl.get([:rich_media, :enabled]) do
+ case get_by_url(url) do
+ %__MODULE__{} = card ->
+ card
+
+ nil ->
+ activity_id = Keyword.get(opts, :activity_id, nil)
+
+ RichMediaWorker.new(%{"op" => "backfill", "url" => url, "activity_id" => activity_id})
+ |> Oban.insert()
+
+ nil
+
+ :error ->
+ nil
+ end
+ else
+ nil
+ end
+ end
+
+ @spec get_by_object(Object.t()) :: t() | nil | :error
+ def get_by_object(object) do
+ case HTML.extract_first_external_url_from_object(object) do
+ nil -> nil
+ url -> get_or_backfill_by_url(url)
+ end
+ end
+
+ @spec get_by_activity(Activity.t()) :: t() | nil | :error
+ # Fake/Draft activity
+ def get_by_activity(%Activity{id: "pleroma:fakeid"} = activity) do
+ with {_, true} <- {:config, @config_impl.get([:rich_media, :enabled])},
+ %Object{} = object <- Object.normalize(activity, fetch: false),
+ url when not is_nil(url) <- HTML.extract_first_external_url_from_object(object) do
+ case get_by_url(url) do
+ # Cache hit
+ %__MODULE__{} = card ->
+ card
+
+ # Cache miss, but fetch for rendering the Draft
+ _ ->
+ with {:ok, fields} <- Parser.parse(url),
+ {:ok, card} <- create(url, fields) do
+ card
+ else
+ _ -> nil
+ end
+ end
+ else
+ _ ->
+ nil
+ end
+ end
+
+ def get_by_activity(activity) do
+ with %Object{} = object <- Object.normalize(activity, fetch: false),
+ {_, nil} <- {:cached, get_cached_url(object, activity.id)} do
+ nil
+ else
+ {:cached, url} ->
+ get_or_backfill_by_url(url, activity_id: activity.id)
+
+ _ ->
+ :error
+ end
+ end
+
+ @spec url_to_hash(String.t()) :: String.t()
+ def url_to_hash(url) do
+ :crypto.hash(:sha256, url) |> Base.encode16(case: :lower)
+ end
+
+ defp get_cached_url(object, activity_id) do
+ key = "URL|#{activity_id}"
+
+ @cachex.fetch!(:scrubber_cache, key, fn _ ->
+ url = HTML.extract_first_external_url_from_object(object)
+ Activity.HTML.add_cache_key_for(activity_id, key)
+
+ {:commit, url}
+ end)
+ end
+end
diff --git a/lib/pleroma/web/rich_media/helpers.ex b/lib/pleroma/web/rich_media/helpers.ex
index 0488df30e..e2889b351 100644
--- a/lib/pleroma/web/rich_media/helpers.ex
+++ b/lib/pleroma/web/rich_media/helpers.ex
@@ -3,101 +3,40 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Helpers do
- alias Pleroma.Activity
alias Pleroma.Config
- alias Pleroma.HTML
- alias Pleroma.Object
- alias Pleroma.Web.RichMedia.Parser
- @options [
- pool: :media,
- max_body: 2_000_000,
- recv_timeout: 2_000
- ]
+ require Logger
- @spec validate_page_url(URI.t() | binary()) :: :ok | :error
- defp validate_page_url(page_url) when is_binary(page_url) do
- validate_tld = Config.get([Pleroma.Formatter, :validate_tld])
-
- page_url
- |> Linkify.Parser.url?(validate_tld: validate_tld)
- |> parse_uri(page_url)
- end
-
- defp validate_page_url(%URI{host: host, scheme: "https", authority: authority})
- when is_binary(authority) do
- cond do
- host in Config.get([:rich_media, :ignore_hosts], []) ->
- :error
-
- get_tld(host) in Config.get([:rich_media, :ignore_tld], []) ->
- :error
-
- true ->
- :ok
- end
- end
-
- defp validate_page_url(_), do: :error
-
- defp parse_uri(true, url) do
- url
- |> URI.parse()
- |> validate_page_url
- end
-
- defp parse_uri(_, _), do: :error
-
- defp get_tld(host) do
- host
- |> String.split(".")
- |> Enum.reverse()
- |> hd
- end
-
- def fetch_data_for_object(object) do
- with true <- Config.get([:rich_media, :enabled]),
- {: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}
- else
- _ -> %{}
- 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, fetch: false) do
- fetch_data_for_object(object)
- else
- _ -> %{}
- end
- end
-
- def fetch_data_for_activity(_), do: %{}
+ @type get_errors :: {:error, :body_too_large | :content_type | :head | :get}
+ @spec rich_media_get(String.t()) :: {:ok, String.t()} | get_errors()
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
+ with {_, {:ok, %Tesla.Env{status: 200, headers: headers}}} <-
+ {:head, Pleroma.HTTP.head(url, headers, http_options())},
+ {_, :ok} <- {:content_type, check_content_type(headers)},
+ {_, :ok} <- {:content_length, check_content_length(headers)},
+ {_, {:ok, %Tesla.Env{status: 200, body: body}}} <-
+ {:get, Pleroma.HTTP.get(url, headers, http_options())} do
+ {:ok, body}
+ else
+ {:head, _} ->
+ Logger.debug("Rich media error for #{url}: HTTP HEAD failed")
+ {:error, :head}
- {:ok, %Tesla.Env{status: 200, headers: headers}} ->
- with :ok <- check_content_type(headers),
- :ok <- check_content_length(headers),
- do: :ok
+ {:content_type, {_, type}} ->
+ Logger.debug("Rich media error for #{url}: content-type is #{type}")
+ {:error, :content_type}
- _ ->
- :ok
- end
+ {:content_length, {_, length}} ->
+ Logger.debug("Rich media error for #{url}: content-length is #{length}")
+ {:error, :body_too_large}
- with :ok <- head_check, do: Pleroma.HTTP.get(url, headers, @options)
+ {:get, _} ->
+ Logger.debug("Rich media error for #{url}: HTTP GET failed")
+ {:error, :get}
+ end
end
defp check_content_type(headers) do
@@ -105,7 +44,7 @@ defmodule Pleroma.Web.RichMedia.Helpers do
{_, content_type} ->
case Plug.Conn.Utils.media_type(content_type) do
{:ok, "text", "html", _} -> :ok
- _ -> {:error, {:content_type, content_type}}
+ _ -> {:error, content_type}
end
_ ->
@@ -113,13 +52,14 @@ defmodule Pleroma.Web.RichMedia.Helpers do
end
end
- @max_body @options[:max_body]
defp check_content_length(headers) do
+ max_body = Keyword.get(http_options(), :max_body)
+
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}
+ {content_length, ""} when content_length <= max_body -> :ok
+ {_, ""} -> {:error, maybe_content_length}
_ -> :ok
end
@@ -127,4 +67,14 @@ defmodule Pleroma.Web.RichMedia.Helpers do
:ok
end
end
+
+ defp http_options do
+ timeout = Config.get!([:rich_media, :timeout])
+
+ [
+ pool: :rich_media,
+ max_body: Config.get([:rich_media, :max_body], 5_000_000),
+ tesla_middleware: [{Tesla.Middleware.Timeout, timeout: timeout}]
+ ]
+ end
end
diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex
index dbe81eabb..a3a522d7a 100644
--- a/lib/pleroma/web/rich_media/parser.ex
+++ b/lib/pleroma/web/rich_media/parser.ex
@@ -3,139 +3,37 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Parser do
+ alias Pleroma.Web.RichMedia.Helpers
require Logger
- @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
+ @config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config)
defp parsers do
Pleroma.Config.get([:rich_media, :parsers])
end
- 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
- 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 """
- Set the rich media cache based on the expiration time of image.
-
- Adopt behaviour `Pleroma.Web.RichMedia.Parser.TTL`
-
- ## Example
-
- defmodule MyModule do
- @behaviour Pleroma.Web.RichMedia.Parser.TTL
- def ttl(data, url) do
- image_url = Map.get(data, :image)
- # do some parsing in the url and get the ttl of the image
- # and return ttl is unix time
- parse_ttl_from_url(image_url)
- end
- end
-
- Define the module in the config
-
- config :pleroma, :rich_media,
- ttl_setters: [MyModule]
- """
- @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, :noop}
+ @type parse_errors :: {:error, :rich_media_disabled | :validate}
+
+ @spec parse(String.t()) ::
+ {:ok, map()} | parse_errors() | Helpers.get_errors()
+ def parse(url) when is_binary(url) do
+ with {_, true} <- {:config, @config_impl.get([:rich_media, :enabled])},
+ {_, :ok} <- {:validate, validate_page_url(url)},
+ {_, {:ok, data}} <- {:parse, parse_url(url)} do
+ data = Map.put(data, "url", url)
+ {:ok, data}
+ else
+ {:config, _} -> {:error, :rich_media_disabled}
+ {:validate, _} -> {:error, :validate}
+ {:parse, error} -> error
end
end
- defp get_ttl_from_image(data, url) do
- [:rich_media, :ttl_setters]
- |> Pleroma.Config.get()
- |> Enum.reduce({:ok, nil}, fn
- module, {:ok, _ttl} ->
- module.ttl(data, url)
-
- _, error ->
- error
- end)
- end
-
- 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
+ defp parse_url(url) do
+ with {:ok, body} <- Helpers.rich_media_get(url),
+ {:ok, html} <- Floki.parse_document(body) do
html
|> maybe_parse()
- |> Map.put("url", url)
|> clean_parsed_data()
|> check_parsed_data()
end
@@ -155,8 +53,8 @@ defmodule Pleroma.Web.RichMedia.Parser do
{:ok, data}
end
- defp check_parsed_data(data) do
- {:error, {:invalid_metadata, data}}
+ defp check_parsed_data(_data) do
+ {:error, :invalid_metadata}
end
defp clean_parsed_data(data) do
@@ -166,4 +64,46 @@ defmodule Pleroma.Web.RichMedia.Parser do
end)
|> Map.new()
end
+
+ @spec validate_page_url(URI.t() | binary()) :: :ok | :error
+ defp validate_page_url(page_url) when is_binary(page_url) do
+ validate_tld = @config_impl.get([Pleroma.Formatter, :validate_tld])
+
+ page_url
+ |> Linkify.Parser.url?(validate_tld: validate_tld)
+ |> parse_uri(page_url)
+ end
+
+ defp validate_page_url(%URI{host: host, scheme: "https"}) do
+ cond do
+ Linkify.Parser.ip?(host) ->
+ :error
+
+ host in @config_impl.get([:rich_media, :ignore_hosts], []) ->
+ :error
+
+ get_tld(host) in @config_impl.get([:rich_media, :ignore_tld], []) ->
+ :error
+
+ true ->
+ :ok
+ end
+ end
+
+ defp validate_page_url(_), do: :error
+
+ defp parse_uri(true, url) do
+ url
+ |> URI.parse()
+ |> validate_page_url
+ end
+
+ defp parse_uri(_, _), do: :error
+
+ defp get_tld(host) do
+ host
+ |> String.split(".")
+ |> Enum.reverse()
+ |> hd
+ end
end
diff --git a/lib/pleroma/web/rich_media/parser/ttl.ex b/lib/pleroma/web/rich_media/parser/ttl.ex
index 59d7f87ab..7e56375ff 100644
--- a/lib/pleroma/web/rich_media/parser/ttl.ex
+++ b/lib/pleroma/web/rich_media/parser/ttl.ex
@@ -3,5 +3,18 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Parser.TTL do
- @callback ttl(Map.t(), String.t()) :: Integer.t() | nil
+ @callback ttl(map(), String.t()) :: integer() | nil
+
+ @spec process(map(), String.t()) :: {:ok, integer() | nil}
+ def process(data, url) do
+ [:rich_media, :ttl_setters]
+ |> Pleroma.Config.get()
+ |> Enum.reduce_while({:ok, nil}, fn
+ module, acc ->
+ case module.ttl(data, url) do
+ ttl when is_number(ttl) -> {:halt, {:ok, ttl}}
+ _ -> {:cont, acc}
+ end
+ end)
+ end
end
diff --git a/lib/pleroma/web/rich_media/parser/ttl/aws_signed_url.ex b/lib/pleroma/web/rich_media/parser/ttl/aws_signed_url.ex
index fa41c160d..1172a120a 100644
--- a/lib/pleroma/web/rich_media/parser/ttl/aws_signed_url.ex
+++ b/lib/pleroma/web/rich_media/parser/ttl/aws_signed_url.ex
@@ -7,25 +7,26 @@ defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl do
@impl true
def ttl(data, _url) do
- image = Map.get(data, :image)
+ image = Map.get(data, "image")
- if is_aws_signed_url(image) do
+ if aws_signed_url?(image) do
image
|> parse_query_params()
|> format_query_params()
|> get_expiration_timestamp()
else
- {:error, "Not aws signed url #{inspect(image)}"}
+ nil
end
end
- defp is_aws_signed_url(image) when is_binary(image) and image != "" do
+ defp aws_signed_url?(image) when is_binary(image) and image != "" do
%URI{host: host, query: query} = URI.parse(image)
- String.contains?(host, "amazonaws.com") and String.contains?(query, "X-Amz-Expires")
+ is_binary(host) and String.contains?(host, "amazonaws.com") and
+ is_binary(query) and String.contains?(query, "X-Amz-Expires")
end
- defp is_aws_signed_url(_), do: nil
+ defp aws_signed_url?(_), do: nil
defp parse_query_params(image) do
%URI{query: query} = URI.parse(image)
@@ -45,6 +46,6 @@ defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl do
|> Map.get("X-Amz-Date")
|> Timex.parse("{ISO:Basic:Z}")
- {:ok, Timex.to_unix(date) + String.to_integer(Map.get(params, "X-Amz-Expires"))}
+ Timex.to_unix(date) + String.to_integer(Map.get(params, "X-Amz-Expires"))
end
end
diff --git a/lib/pleroma/web/rich_media/parser/ttl/opengraph.ex b/lib/pleroma/web/rich_media/parser/ttl/opengraph.ex
new file mode 100644
index 000000000..b06889669
--- /dev/null
+++ b/lib/pleroma/web/rich_media/parser/ttl/opengraph.ex
@@ -0,0 +1,20 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.RichMedia.Parser.TTL.Opengraph do
+ @behaviour Pleroma.Web.RichMedia.Parser.TTL
+
+ @impl true
+ def ttl(%{"ttl" => ttl_string}, _url) when is_binary(ttl_string) do
+ try do
+ ttl = String.to_integer(ttl_string)
+ now = DateTime.utc_now() |> DateTime.to_unix()
+ now + ttl
+ rescue
+ _ -> nil
+ end
+ end
+
+ def ttl(_, _), do: nil
+end
diff --git a/lib/pleroma/web/rich_media/parsers/o_embed.ex b/lib/pleroma/web/rich_media/parsers/o_embed.ex
index 0f303176c..35ec14426 100644
--- a/lib/pleroma/web/rich_media/parsers/o_embed.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.Web.RichMedia.Helpers.rich_media_get(url) do
+ with {:ok, json} <- Pleroma.Web.RichMedia.Helpers.rich_media_get(url) do
Jason.decode(json)
end
end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 6b9e158a3..fc40a1143 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -29,6 +29,7 @@ defmodule Pleroma.Web.Router do
pipeline :browser do
plug(:accepts, ["html"])
plug(:fetch_session)
+ plug(Pleroma.Web.Plugs.LoggerMetadataUser)
end
pipeline :oauth do
@@ -67,12 +68,14 @@ defmodule Pleroma.Web.Router do
plug(:fetch_session)
plug(:authenticate)
plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
+ plug(Pleroma.Web.Plugs.LoggerMetadataUser)
end
pipeline :no_auth_or_privacy_expectations_api do
plug(:base_api)
plug(:after_auth)
plug(Pleroma.Web.Plugs.IdempotencyPlug)
+ plug(Pleroma.Web.Plugs.LoggerMetadataUser)
end
# Pipeline for app-related endpoints (no user auth checks — app-bound tokens must be supported)
@@ -83,12 +86,14 @@ defmodule Pleroma.Web.Router do
pipeline :api do
plug(:expect_public_instance_or_user_authentication)
plug(:no_auth_or_privacy_expectations_api)
+ plug(Pleroma.Web.Plugs.LoggerMetadataUser)
end
pipeline :authenticated_api do
plug(:expect_user_authentication)
plug(:no_auth_or_privacy_expectations_api)
plug(Pleroma.Web.Plugs.EnsureAuthenticatedPlug)
+ plug(Pleroma.Web.Plugs.LoggerMetadataUser)
end
pipeline :admin_api do
@@ -99,6 +104,7 @@ defmodule Pleroma.Web.Router do
plug(Pleroma.Web.Plugs.EnsureAuthenticatedPlug)
plug(Pleroma.Web.Plugs.UserIsStaffPlug)
plug(Pleroma.Web.Plugs.IdempotencyPlug)
+ plug(Pleroma.Web.Plugs.LoggerMetadataUser)
end
pipeline :require_admin do
@@ -179,10 +185,11 @@ defmodule Pleroma.Web.Router do
plug(:browser)
plug(:authenticate)
plug(Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug)
+ plug(Pleroma.Web.Plugs.LoggerMetadataUser)
end
pipeline :well_known do
- plug(:accepts, ["json", "jrd+json", "xml", "xrd+xml"])
+ plug(:accepts, ["json", "jrd", "jrd+json", "xml", "xrd+xml"])
end
pipeline :config do
@@ -193,6 +200,7 @@ defmodule Pleroma.Web.Router do
pipeline :pleroma_api do
plug(:accepts, ["html", "json"])
plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
+ plug(Pleroma.Web.Plugs.LoggerMetadataUser)
end
pipeline :mailbox_preview do
@@ -224,6 +232,12 @@ defmodule Pleroma.Web.Router do
post("/remote_interaction", UtilController, :remote_interaction)
end
+ scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
+ pipe_through(:pleroma_api)
+
+ get("/federation_status", InstancesController, :show)
+ end
+
scope "/api/v1/pleroma", Pleroma.Web do
pipe_through(:pleroma_api)
post("/uploader_callback/:upload_path", UploaderController, :callback)
@@ -286,6 +300,11 @@ defmodule Pleroma.Web.Router do
post("/frontends/install", FrontendController, :install)
post("/backups", AdminAPIController, :create_backup)
+
+ get("/rules", RuleController, :index)
+ post("/rules", RuleController, :create)
+ patch("/rules/:id", RuleController, :update)
+ delete("/rules/:id", RuleController, :delete)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
@@ -465,6 +484,8 @@ defmodule Pleroma.Web.Router do
get("/main/ostatus", UtilController, :show_subscribe_form)
get("/ostatus_subscribe", RemoteFollowController, :follow)
post("/ostatus_subscribe", RemoteFollowController, :do_follow)
+
+ get("/authorize_interaction", RemoteFollowController, :authorize_interaction)
end
scope "/api/pleroma", Pleroma.Web.TwitterAPI do
@@ -473,7 +494,7 @@ defmodule Pleroma.Web.Router do
post("/change_email", UtilController, :change_email)
post("/change_password", UtilController, :change_password)
post("/delete_account", UtilController, :delete_account)
- put("/notification_settings", UtilController, :update_notificaton_settings)
+ put("/notification_settings", UtilController, :update_notification_settings)
post("/disable_account", UtilController, :disable_account)
post("/move_account", UtilController, :move_account)
@@ -572,12 +593,19 @@ defmodule Pleroma.Web.Router do
get("/backups", BackupController, :index)
post("/backups", BackupController, :create)
+
+ get("/bookmark_folders", BookmarkFolderController, :index)
+ post("/bookmark_folders", BookmarkFolderController, :create)
+ patch("/bookmark_folders/:id", BookmarkFolderController, :update)
+ delete("/bookmark_folders/:id", BookmarkFolderController, :delete)
end
scope [] do
pipe_through(:api)
get("/accounts/:id/favourites", AccountController, :favourites)
get("/accounts/:id/endorsements", AccountController, :endorsements)
+
+ get("/statuses/:id/quotes", StatusController, :quotes)
end
scope [] do
@@ -602,7 +630,6 @@ 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/v2/pleroma", Pleroma.Web.PleromaAPI do
@@ -619,6 +646,7 @@ defmodule Pleroma.Web.Router do
patch("/accounts/update_credentials", AccountController, :update_credentials)
get("/accounts/relationships", AccountController, :relationships)
+ get("/accounts/familiar_followers", AccountController, :familiar_followers)
get("/accounts/:id/lists", AccountController, :lists)
get("/accounts/:id/identity_proofs", AccountController, :identity_proofs)
get("/endorsements", AccountController, :endorsements)
@@ -750,11 +778,11 @@ defmodule Pleroma.Web.Router do
get("/instance", InstanceController, :show)
get("/instance/peers", InstanceController, :peers)
+ get("/instance/rules", InstanceController, :rules)
get("/statuses", StatusController, :index)
get("/statuses/:id", StatusController, :show)
get("/statuses/:id/context", StatusController, :context)
- get("/statuses/:id/card", StatusController, :card)
get("/statuses/:id/favourited_by", StatusController, :favourited_by)
get("/statuses/:id/reblogged_by", StatusController, :reblogged_by)
get("/statuses/:id/history", StatusController, :show_history)
@@ -774,11 +802,14 @@ defmodule Pleroma.Web.Router do
scope "/api/v2", Pleroma.Web.MastodonAPI do
pipe_through(:api)
+
get("/search", SearchController, :search2)
post("/media", MediaController, :create2)
get("/suggestions", SuggestionController, :index2)
+
+ get("/instance", InstanceController, :show2)
end
scope "/api", Pleroma.Web do
@@ -957,7 +988,7 @@ defmodule Pleroma.Web.Router do
scope "/" do
pipe_through([:pleroma_html, :authenticate, :require_admin])
- live_dashboard("/phoenix/live_dashboard")
+ live_dashboard("/phoenix/live_dashboard", additional_pages: [oban: Oban.LiveDashboard])
end
# Test-only routes needed to test action dispatching and plug chain execution
@@ -1003,9 +1034,8 @@ defmodule Pleroma.Web.Router do
options("/*path", RedirectController, :empty)
end
- # TODO: Change to Phoenix.Router.routes/1 for Phoenix 1.6.0+
def get_api_routes do
- __MODULE__.__routes__()
+ Phoenix.Router.routes(__MODULE__)
|> Enum.reject(fn r -> r.plug == Pleroma.Web.Fallback.RedirectController end)
|> Enum.map(fn r ->
r.path
diff --git a/lib/pleroma/web/static_fe/static_fe_controller.ex b/lib/pleroma/web/static_fe/static_fe_controller.ex
index 8019a218a..012f8e464 100644
--- a/lib/pleroma/web/static_fe/static_fe_controller.ex
+++ b/lib/pleroma/web/static_fe/static_fe_controller.ex
@@ -13,7 +13,6 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do
alias Pleroma.Web.Metadata
alias Pleroma.Web.Router.Helpers
- plug(:put_layout, :static_fe)
plug(:assign_id)
@page_keys ["max_id", "min_id", "limit", "since_id", "order"]
@@ -22,7 +21,7 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do
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 <- Visibility.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
url = Helpers.url(conn) <> conn.request_path
diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex
index 48ca82421..76dc0f42d 100644
--- a/lib/pleroma/web/streamer.ex
+++ b/lib/pleroma/web/streamer.ex
@@ -20,7 +20,6 @@ defmodule Pleroma.Web.Streamer do
alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.StreamerView
- @mix_env Mix.env()
@registry Pleroma.Web.StreamerRegistry
def registry, do: @registry
@@ -34,7 +33,7 @@ defmodule Pleroma.Web.Streamer do
stream :: String.t(),
User.t() | nil,
Token.t() | nil,
- Map.t() | nil
+ map() | nil
) ::
{:ok, topic :: String.t()} | {:error, :bad_topic} | {:error, :unauthorized}
def get_topic_and_add_socket(stream, user, oauth_token, params \\ %{}) do
@@ -60,7 +59,7 @@ defmodule Pleroma.Web.Streamer do
end
@doc "Expand and authorizes a stream"
- @spec get_topic(stream :: String.t() | nil, User.t() | nil, Token.t() | nil, Map.t()) ::
+ @spec get_topic(stream :: String.t() | nil, User.t() | nil, Token.t() | nil, map()) ::
{:ok, topic :: String.t() | nil} | {:error, :bad_topic}
def get_topic(stream, user, oauth_token, params \\ %{})
@@ -173,7 +172,13 @@ defmodule Pleroma.Web.Streamer do
def stream(topics, items) do
if should_env_send?() do
for topic <- List.wrap(topics), item <- List.wrap(items) do
- spawn(fn -> do_stream(topic, item) end)
+ fun = fn -> do_stream(topic, item) end
+
+ if Config.get([__MODULE__, :sync_streaming], false) do
+ fun.()
+ else
+ spawn(fun)
+ end
end
end
end
@@ -201,7 +206,7 @@ defmodule Pleroma.Web.Streamer do
false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, item_host),
false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, parent_host),
true <- thread_containment(item, user),
- false <- CommonAPI.thread_muted?(user, parent) do
+ false <- CommonAPI.thread_muted?(parent, user) do
false
else
_ -> true
@@ -246,7 +251,7 @@ defmodule Pleroma.Web.Streamer do
defp do_stream("list", item) do
# filter the recipient list if the activity is not public, see #270.
recipient_lists =
- case Visibility.is_public?(item) do
+ case Visibility.public?(item) do
true ->
Pleroma.List.get_lists_from_activity(item)
@@ -396,25 +401,20 @@ defmodule Pleroma.Web.Streamer do
end
end
- # In test environement, only return true if the registry is started.
- # In benchmark environment, returns false.
- # In any other environment, always returns true.
- cond do
- @mix_env == :test ->
- def should_env_send? do
- case Process.whereis(@registry) do
- nil ->
- false
+ # In dev/prod the streamer registry is expected to be started, so return true
+ # In test it is possible to have the registry started for a test so it will check
+ # In benchmark it will never find the process alive and return false
+ def should_env_send? do
+ if Application.get_env(:pleroma, Pleroma.Application)[:streamer_registry] do
+ true
+ else
+ case Process.whereis(@registry) do
+ nil ->
+ false
- pid ->
- Process.alive?(pid)
- end
+ pid ->
+ Process.alive?(pid)
end
-
- @mix_env == :benchmark ->
- def should_env_send?, do: false
-
- true ->
- def should_env_send?, do: true
+ end
end
end
diff --git a/lib/pleroma/web/templates/feed/feed/tag.atom.eex b/lib/pleroma/web/templates/feed/feed/tag.atom.eex
index 14b0ee594..3449c97ff 100644
--- a/lib/pleroma/web/templates/feed/feed/tag.atom.eex
+++ b/lib/pleroma/web/templates/feed/feed/tag.atom.eex
@@ -12,7 +12,7 @@
<subtitle><%= Gettext.dpgettext("static_pages", "tag feed description", "These are public toots tagged with #%{tag}. You can interact with them if you have an account anywhere in the fediverse.", tag: @tag) %></subtitle>
<logo><%= feed_logo() %></logo>
<updated><%= most_recent_update(@activities) %></updated>
- <link rel="self" href="<%= '#{Routes.tag_feed_url(@conn, :feed, @tag)}.atom' %>" type="application/atom+xml"/>
+ <link rel="self" href="<%= "#{Routes.tag_feed_url(@conn, :feed, @tag)}.atom" %>" type="application/atom+xml"/>
<%= for activity <- @activities do %>
<%= render Phoenix.Controller.view_module(@conn), "_tag_activity.atom", Map.merge(assigns, prepare_activity(activity, actor: true)) %>
diff --git a/lib/pleroma/web/templates/feed/feed/tag.rss.eex b/lib/pleroma/web/templates/feed/feed/tag.rss.eex
index 27dde5627..a87f9bf50 100644
--- a/lib/pleroma/web/templates/feed/feed/tag.rss.eex
+++ b/lib/pleroma/web/templates/feed/feed/tag.rss.eex
@@ -6,7 +6,7 @@
<title>#<%= @tag %></title>
<description><%= Gettext.dpgettext("static_pages", "tag feed description", "These are public toots tagged with #%{tag}. You can interact with them if you have an account anywhere in the fediverse.", tag: @tag) %></description>
- <link><%= '#{Routes.tag_feed_url(@conn, :feed, @tag)}.rss' %></link>
+ <link><%= "#{Routes.tag_feed_url(@conn, :feed, @tag)}.rss" %></link>
<webfeeds:logo><%= feed_logo() %></webfeeds:logo>
<webfeeds:accentColor>2b90d9</webfeeds:accentColor>
<%= for activity <- @activities do %>
diff --git a/lib/pleroma/web/templates/feed/feed/user.atom.eex b/lib/pleroma/web/templates/feed/feed/user.atom.eex
index e36bfc66c..c2c77cfed 100644
--- a/lib/pleroma/web/templates/feed/feed/user.atom.eex
+++ b/lib/pleroma/web/templates/feed/feed/user.atom.eex
@@ -11,12 +11,12 @@
<subtitle><%= escape(@user.bio) %></subtitle>
<updated><%= most_recent_update(@activities, @user, :atom) %></updated>
<logo><%= logo(@user) %></logo>
- <link rel="self" href="<%= '#{Routes.user_feed_url(@conn, :feed, @user.nickname)}.atom' %>" type="application/atom+xml"/>
+ <link rel="self" href="<%= "#{Routes.user_feed_url(@conn, :feed, @user.nickname)}.atom" %>" type="application/atom+xml"/>
<%= render Phoenix.Controller.view_module(@conn), "_author.atom", assigns %>
<%= if last_activity(@activities) do %>
- <link rel="next" href="<%= '#{Routes.user_feed_url(@conn, :feed, @user.nickname)}.atom?max_id=#{last_activity(@activities).id}' %>" type="application/atom+xml"/>
+ <link rel="next" href="<%= "#{Routes.user_feed_url(@conn, :feed, @user.nickname)}.atom?max_id=#{last_activity(@activities).id}" %>" type="application/atom+xml"/>
<% end %>
<%= for activity <- @activities do %>
diff --git a/lib/pleroma/web/templates/feed/feed/user.rss.eex b/lib/pleroma/web/templates/feed/feed/user.rss.eex
index fae3fcf3d..b907a7e57 100644
--- a/lib/pleroma/web/templates/feed/feed/user.rss.eex
+++ b/lib/pleroma/web/templates/feed/feed/user.rss.eex
@@ -7,20 +7,20 @@
xmlns:poco="http://portablecontacts.net/spec/1.0">
<channel>
<title><%= @user.nickname <> "'s timeline" %></title>
- <link><%= '#{Routes.user_feed_url(@conn, :feed, @user.nickname)}.rss' %></link>
+ <link><%= "#{Routes.user_feed_url(@conn, :feed, @user.nickname)}.rss" %></link>
<atom:link href="<%= Routes.user_feed_url(@conn, :feed, @user.nickname) <> ".atom" %>"
rel="self" type="application/rss+xml" />
<description><%= escape(@user.bio) %></description>
<image>
<url><%= logo(@user) %></url>
<title><%= @user.nickname <> "'s timeline" %></title>
- <link><%= '#{Routes.user_feed_url(@conn, :feed, @user.nickname)}.rss' %></link>
+ <link><%= "#{Routes.user_feed_url(@conn, :feed, @user.nickname)}.rss" %></link>
</image>
<%= render Phoenix.Controller.view_module(@conn), "_author.rss", assigns %>
<%= if last_activity(@activities) do %>
- <link rel="next"><%= '#{Routes.user_feed_url(@conn, :feed, @user.nickname)}.rss?max_id=#{last_activity(@activities).id}' %></link>
+ <link rel="next"><%= "#{Routes.user_feed_url(@conn, :feed, @user.nickname)}.rss?max_id=#{last_activity(@activities).id}" %></link>
<% end %>
<%= for activity <- @activities do %>
diff --git a/lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex b/lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex
index e45d13bdf..e3639aae7 100644
--- a/lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex
+++ b/lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex
@@ -1,8 +1,8 @@
-<%= if get_flash(@conn, :info) do %>
-<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
+<%= if Phoenix.Flash.get(@flash, :info) do %>
+<p class="alert alert-info" role="alert"><%= Phoenix.Flash.get(@flash, :info) %></p>
<% end %>
-<%= if get_flash(@conn, :error) do %>
-<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
+<%= if Phoenix.Flash.get(@flash, :error) do %>
+<p class="alert alert-danger" role="alert"><%= Phoenix.Flash.get(@flash, :error) %></p>
<% end %>
<h2><%= Gettext.dpgettext("static_pages", "mfa recover page title", "Two-factor recovery") %></h2>
diff --git a/lib/pleroma/web/templates/o_auth/mfa/totp.html.eex b/lib/pleroma/web/templates/o_auth/mfa/totp.html.eex
index 50e6c04b6..f995b8805 100644
--- a/lib/pleroma/web/templates/o_auth/mfa/totp.html.eex
+++ b/lib/pleroma/web/templates/o_auth/mfa/totp.html.eex
@@ -1,8 +1,8 @@
-<%= if get_flash(@conn, :info) do %>
-<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
+<%= if Phoenix.Flash.get(@flash, :info) do %>
+<p class="alert alert-info" role="alert"><%= Phoenix.Flash.get(@flash, :info) %></p>
<% end %>
-<%= if get_flash(@conn, :error) do %>
-<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
+<%= if Phoenix.Flash.get(@flash, :error) do %>
+<p class="alert alert-danger" role="alert"><%= Phoenix.Flash.get(@flash, :error) %></p>
<% end %>
<h2><%= Gettext.dpgettext("static_pages", "mfa auth page title", "Two-factor authentication") %></h2>
diff --git a/lib/pleroma/web/templates/o_auth/o_auth/register.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/register.html.eex
index 1f661efb2..e7f65266f 100644
--- a/lib/pleroma/web/templates/o_auth/o_auth/register.html.eex
+++ b/lib/pleroma/web/templates/o_auth/o_auth/register.html.eex
@@ -1,8 +1,8 @@
-<%= if get_flash(@conn, :info) do %>
- <p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
+<%= if Phoenix.Flash.get(@flash, :info) do %>
+ <p class="alert alert-info" role="alert"><%= Phoenix.Flash.get(@flash, :info) %></p>
<% end %>
-<%= if get_flash(@conn, :error) do %>
- <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
+<%= if Phoenix.Flash.get(@flash, :error) do %>
+ <p class="alert alert-danger" role="alert"><%= Phoenix.Flash.get(@flash, :error) %></p>
<% end %>
<h2><%= Gettext.dpgettext("static_pages", "oauth register page title", "Registration Details") %></h2>
diff --git a/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex
index b3654f3eb..6bc8eb602 100644
--- a/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex
+++ b/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex
@@ -1,8 +1,8 @@
-<%= if get_flash(@conn, :info) do %>
-<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
+<%= if Phoenix.Flash.get(@flash, :info) do %>
+<p class="alert alert-info" role="alert"><%= Phoenix.Flash.get(@flash, :info) %></p>
<% end %>
-<%= if get_flash(@conn, :error) do %>
-<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
+<%= if Phoenix.Flash.get(@flash, :error) do %>
+<p class="alert alert-danger" role="alert"><%= Phoenix.Flash.get(@flash, :error) %></p>
<% end %>
<%= form_for @conn, Routes.o_auth_path(@conn, :authorize), [as: "authorization"], fn f -> %>
@@ -13,7 +13,7 @@
<div class="account-header__avatar" style="background-image: url('<%= Pleroma.User.avatar_url(@user) %>')"></div>
<div class="account-header__meta">
<div class="account-header__display-name"><%= @user.name %></div>
- <div class="account-header__nickname">@<%= @user.nickname %>@<%= Pleroma.User.get_host(@user) %></div>
+ <div class="account-header__nickname">@<%= Pleroma.User.full_nickname(@user.nickname) %></div>
</div>
</div>
<% end %>
diff --git a/lib/pleroma/web/twitter_api/controllers/password_controller.ex b/lib/pleroma/web/twitter_api/controllers/password_controller.ex
index 31b7dd728..e5482de9d 100644
--- a/lib/pleroma/web/twitter_api/controllers/password_controller.ex
+++ b/lib/pleroma/web/twitter_api/controllers/password_controller.ex
@@ -4,7 +4,7 @@
defmodule Pleroma.Web.TwitterAPI.PasswordController do
@moduledoc """
- The module containts functions for reset password.
+ The module contains functions for password reset.
"""
use Pleroma.Web, :controller
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 6229d5d05..38ebc8c5d 100644
--- a/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex
+++ b/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex
@@ -29,7 +29,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do
# GET /ostatus_subscribe
#
def follow(%{assigns: %{user: user}} = conn, %{"acct" => acct}) do
- case is_status?(acct) do
+ case status?(acct) do
true -> follow_status(conn, user, acct)
_ -> follow_account(conn, user, acct)
end
@@ -57,7 +57,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do
defp follow_template(%User{} = _user), do: "follow.html"
defp follow_template(_), do: "follow_login.html"
- defp is_status?(acct) do
+ defp status?(acct) do
case Fetcher.fetch_and_contain_remote_object_from_id(acct) do
{:ok, %{"type" => type}} when type in @status_types ->
true
@@ -73,7 +73,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do
#
def do_follow(%{assigns: %{user: %User{} = user}} = conn, %{"user" => %{"id" => id}}) do
with {:fetch_user, %User{} = followee} <- {:fetch_user, User.get_cached_by_id(id)},
- {:ok, _, _, _} <- CommonAPI.follow(user, followee) do
+ {:ok, _, _, _} <- CommonAPI.follow(followee, user) do
redirect(conn, to: "/users/#{followee.id}")
else
error ->
@@ -90,7 +90,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do
with {_, %User{} = followee} <- {:fetch_user, User.get_cached_by_id(id)},
{_, {:ok, user}, _} <- {:auth, WrapperAuthenticator.get_user(conn), followee},
{_, _, _, false} <- {:mfa_required, followee, user, MFA.require?(user)},
- {:ok, _, _, _} <- CommonAPI.follow(user, followee) do
+ {:ok, _, _, _} <- CommonAPI.follow(followee, user) do
redirect(conn, to: "/users/#{followee.id}")
else
error ->
@@ -108,7 +108,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do
{_, _, {:ok, %{user: user}}} <- {:mfa_token, followee, MFA.Token.validate(token)},
{_, _, _, {:ok, _}} <-
{:verify_mfa_code, followee, token, TOTPAuthenticator.verify(code, user)},
- {:ok, _, _, _} <- CommonAPI.follow(user, followee) do
+ {:ok, _, _, _} <- CommonAPI.follow(followee, user) do
redirect(conn, to: "/users/#{followee.id}")
else
error ->
@@ -121,6 +121,13 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do
render(conn, "followed.html", %{error: "Insufficient permissions: follow | write:follows."})
end
+ # GET /authorize_interaction
+ #
+ def authorize_interaction(conn, %{"uri" => uri}) do
+ conn
+ |> redirect(to: Routes.remote_follow_path(conn, :follow, %{acct: uri}))
+ end
+
defp handle_follow_error(conn, {:mfa_token, followee, _} = _) do
render(conn, "follow_login.html", %{error: "Wrong username or password", followee: followee})
end
diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
index ca8a98960..6805233df 100644
--- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex
+++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
@@ -18,7 +18,8 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
alias Pleroma.Web.WebFinger
plug(
- Pleroma.Web.ApiSpec.CastAndValidate
+ Pleroma.Web.ApiSpec.CastAndValidate,
+ [replace_params: false]
when action != :remote_subscribe and action != :show_subscribe_form
)
@@ -35,7 +36,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
:change_email,
:change_password,
:delete_account,
- :update_notificaton_settings,
+ :update_notification_settings,
:disable_account,
:move_account,
:add_alias,
@@ -150,7 +151,10 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
end
- def remote_interaction(%{body_params: %{ap_id: ap_id, profile: profile}} = conn, _params) do
+ def remote_interaction(
+ %{private: %{open_api_spex: %{body_params: %{ap_id: ap_id, profile: profile}}}} = conn,
+ _params
+ ) do
with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile) do
conn
|> json(%{url: String.replace(template, "{uri}", ap_id)})
@@ -181,13 +185,16 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
json(conn, emoji)
end
- def update_notificaton_settings(%{assigns: %{user: user}} = conn, params) do
+ def update_notification_settings(%{assigns: %{user: user}} = conn, params) do
with {:ok, _} <- User.update_notification_settings(user, params) do
json(conn, %{status: "success"})
end
end
- def change_password(%{assigns: %{user: user}, body_params: body_params} = conn, %{}) do
+ def change_password(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: body_params}}} = conn,
+ _
+ ) do
case CommonAPI.Utils.confirm_current_password(user, body_params.password) do
{:ok, user} ->
with {:ok, _user} <-
@@ -200,9 +207,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
{:error, changeset} ->
{_, {error, _}} = Enum.at(changeset.errors, 0)
json(conn, %{error: "New password #{error}."})
-
- _ ->
- json(conn, %{error: "Unable to change password."})
end
{:error, msg} ->
@@ -210,7 +214,10 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
end
- def change_email(%{assigns: %{user: user}, body_params: body_params} = conn, %{}) do
+ def change_email(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: body_params}}} = conn,
+ _
+ ) do
case CommonAPI.Utils.confirm_current_password(user, body_params.password) do
{:ok, user} ->
with {:ok, _user} <- User.change_email(user, body_params.email) do
@@ -229,7 +236,13 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
end
- def delete_account(%{assigns: %{user: user}, body_params: body_params} = conn, params) do
+ def delete_account(
+ %{
+ assigns: %{user: user},
+ private: %{open_api_spex: %{body_params: body_params, params: params}}
+ } = conn,
+ _
+ ) do
# This endpoint can accept a query param or JSON body for backwards-compatibility.
# Submitting a JSON body is recommended, so passwords don't end up in server logs.
password = body_params[:password] || params[:password] || ""
@@ -244,7 +257,10 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
end
- def disable_account(%{assigns: %{user: user}} = conn, params) do
+ def disable_account(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: params}}} = conn,
+ _
+ ) do
case CommonAPI.Utils.confirm_current_password(user, params[:password]) do
{:ok, user} ->
User.set_activation_async(user, false)
@@ -255,7 +271,10 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
end
- def move_account(%{assigns: %{user: user}, body_params: body_params} = conn, %{}) do
+ def move_account(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: body_params}}} = conn,
+ _
+ ) do
case CommonAPI.Utils.confirm_current_password(user, body_params.password) do
{:ok, user} ->
with {:ok, target_user} <- find_or_fetch_user_by_nickname(body_params.target_account),
@@ -276,7 +295,10 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
end
- def add_alias(%{assigns: %{user: user}, body_params: body_params} = conn, _) do
+ def add_alias(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: body_params}}} = conn,
+ _
+ ) do
with {:ok, alias_user} <- find_user_by_nickname(body_params.alias),
{:ok, _user} <- user |> User.add_alias(alias_user) do
json(conn, %{status: "success"})
@@ -291,7 +313,10 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
end
- def delete_alias(%{assigns: %{user: user}, body_params: body_params} = conn, _) do
+ def delete_alias(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: body_params}}} = conn,
+ _
+ ) do
with {:ok, alias_user} <- find_user_by_nickname(body_params.alias),
{:ok, _user} <- user |> User.delete_alias(alias_user) do
json(conn, %{status: "success"})
@@ -306,7 +331,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
end
- def list_aliases(%{assigns: %{user: user}} = conn, %{}) do
+ def list_aliases(%{assigns: %{user: user}} = conn, _) do
alias_nicks =
user
|> User.alias_users()
@@ -319,7 +344,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
user = User.get_cached_by_nickname(nickname)
if user == nil do
- {:not_found, nil}
+ {:error, :not_found}
else
{:ok, user}
end
diff --git a/lib/pleroma/web/web_finger.ex b/lib/pleroma/web/web_finger.ex
index 398742200..e653b3338 100644
--- a/lib/pleroma/web/web_finger.ex
+++ b/lib/pleroma/web/web_finger.ex
@@ -5,8 +5,8 @@
defmodule Pleroma.Web.WebFinger do
alias Pleroma.HTTP
alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.Publisher
alias Pleroma.Web.Endpoint
- alias Pleroma.Web.Federator.Publisher
alias Pleroma.Web.XML
alias Pleroma.XmlBuilder
require Jason
@@ -70,7 +70,7 @@ defmodule Pleroma.Web.WebFinger do
def represent_user(user, "JSON") do
%{
- "subject" => "acct:#{user.nickname}@#{domain()}",
+ "subject" => "acct:#{user.nickname}@#{host()}",
"aliases" => gather_aliases(user),
"links" => gather_links(user)
}
@@ -90,13 +90,13 @@ defmodule Pleroma.Web.WebFinger do
:XRD,
%{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"},
[
- {:Subject, "acct:#{user.nickname}@#{domain()}"}
+ {:Subject, "acct:#{user.nickname}@#{host()}"}
] ++ aliases ++ links
}
|> XmlBuilder.to_doc()
end
- defp domain do
+ def host do
Pleroma.Config.get([__MODULE__, :domain]) || Pleroma.Web.Endpoint.host()
end
@@ -172,7 +172,7 @@ defmodule Pleroma.Web.WebFinger do
get_template_from_xml(body)
else
error ->
- Logger.warn("Can't find LRDD template in #{inspect(meta_url)}: #{inspect(error)}")
+ Logger.warning("Can't find LRDD template in #{inspect(meta_url)}: #{inspect(error)}")
{:error, :lrdd_not_found}
end
end
diff --git a/lib/pleroma/web/web_finger/web_finger_controller.ex b/lib/pleroma/web/web_finger/web_finger_controller.ex
index 9e5efb77f..021df9bc5 100644
--- a/lib/pleroma/web/web_finger/web_finger_controller.ex
+++ b/lib/pleroma/web/web_finger/web_finger_controller.ex
@@ -30,7 +30,7 @@ defmodule Pleroma.Web.WebFinger.WebFingerController do
end
def webfinger(%{assigns: %{format: format}} = conn, %{"resource" => resource})
- when format in ["json", "jrd+json"] do
+ when format in ["jrd", "json", "jrd+json"] do
with {:ok, response} <- WebFinger.webfinger(resource, "JSON") do
json(conn, response)
else
diff --git a/lib/pleroma/web/xml.ex b/lib/pleroma/web/xml.ex
index 64329e4ba..3997e1661 100644
--- a/lib/pleroma/web/xml.ex
+++ b/lib/pleroma/web/xml.ex
@@ -9,7 +9,7 @@ defmodule Pleroma.Web.XML do
def string_from_xpath(xpath, doc) do
try do
- {:xmlObj, :string, res} = :xmerl_xpath.string('string(#{xpath})', doc)
+ {:xmlObj, :string, res} = :xmerl_xpath.string(~c"string(#{xpath})", doc)
res =
res
diff --git a/lib/pleroma/workers/attachments_cleanup_worker.ex b/lib/pleroma/workers/attachments_cleanup_worker.ex
index 4c1764053..0b570b70b 100644
--- a/lib/pleroma/workers/attachments_cleanup_worker.ex
+++ b/lib/pleroma/workers/attachments_cleanup_worker.ex
@@ -8,7 +8,7 @@ defmodule Pleroma.Workers.AttachmentsCleanupWorker do
alias Pleroma.Object
alias Pleroma.Repo
- use Pleroma.Workers.WorkerHelper, queue: "attachments_cleanup"
+ use Pleroma.Workers.WorkerHelper, queue: "slow"
@impl Oban.Worker
def perform(%Job{
diff --git a/lib/pleroma/workers/background_worker.ex b/lib/pleroma/workers/background_worker.ex
index 794417612..870aef3c6 100644
--- a/lib/pleroma/workers/background_worker.ex
+++ b/lib/pleroma/workers/background_worker.ex
@@ -3,7 +3,6 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.BackgroundWorker do
- alias Pleroma.Instances.Instance
alias Pleroma.User
use Pleroma.Workers.WorkerHelper, queue: "background"
@@ -15,11 +14,6 @@ defmodule Pleroma.Workers.BackgroundWorker do
User.perform(:set_activation_async, user, status)
end
- def perform(%Job{args: %{"op" => "delete_user", "user_id" => user_id}}) do
- user = User.get_cached_by_id(user_id)
- User.perform(:delete, user)
- end
-
def perform(%Job{args: %{"op" => "force_password_reset", "user_id" => user_id}}) do
user = User.get_cached_by_id(user_id)
User.perform(:force_password_reset, user)
@@ -28,7 +22,7 @@ defmodule Pleroma.Workers.BackgroundWorker do
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)}
+ {:ok, User.Import.perform(String.to_existing_atom(op), user, identifiers)}
end
def perform(%Job{
@@ -40,10 +34,11 @@ defmodule Pleroma.Workers.BackgroundWorker do
Pleroma.FollowingRelationship.move_following(origin, target)
end
- def perform(%Job{args: %{"op" => "delete_instance", "host" => host}}) do
- Instance.perform(:delete_instance, host)
+ def perform(%Job{args: %{"op" => "verify_fields_links", "user_id" => user_id}}) do
+ user = User.get_by_id(user_id)
+ User.perform(:verify_fields_links, user)
end
@impl Oban.Worker
- def timeout(_job), do: :timer.seconds(900)
+ def timeout(_job), do: :timer.seconds(15)
end
diff --git a/lib/pleroma/workers/backup_worker.ex b/lib/pleroma/workers/backup_worker.ex
index a485ddb4b..d1b6fcdad 100644
--- a/lib/pleroma/workers/backup_worker.ex
+++ b/lib/pleroma/workers/backup_worker.ex
@@ -3,67 +3,49 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.BackupWorker do
- use Oban.Worker, queue: :backup, max_attempts: 1
+ use Oban.Worker, queue: :slow, max_attempts: 1
alias Oban.Job
+ alias Pleroma.Config.Getting, as: Config
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
-
@impl Oban.Worker
def perform(%Job{
- args: %{"op" => "process", "backup_id" => backup_id, "admin_user_id" => admin_user_id}
+ args: %{"op" => "process", "backup_id" => backup_id}
}) do
- with {:ok, %Backup{} = backup} <-
- backup_id |> Backup.get() |> Backup.process(),
- {:ok, _job} <- schedule_deletion(backup),
- :ok <- Backup.remove_outdated(backup),
- :ok <- maybe_deliver_email(backup, admin_user_id) do
- {:ok, backup}
+ with {_, %Backup{} = backup} <- {:get, Backup.get_by_id(backup_id)},
+ {_, {:ok, updated_backup}} <- {:run, Backup.run(backup)},
+ {_, {:ok, uploaded_backup}} <- {:upload, Backup.upload(updated_backup)},
+ {_, {:ok, _job}} <- {:delete, Backup.schedule_delete(uploaded_backup)},
+ {_, :ok} <- {:outdated, Backup.remove_outdated(uploaded_backup.user)},
+ {_, :ok} <- {:email, maybe_deliver_email(uploaded_backup)} do
+ {:ok, uploaded_backup}
+ else
+ e -> {:error, e}
end
end
def perform(%Job{args: %{"op" => "delete", "backup_id" => backup_id}}) do
- case Backup.get(backup_id) do
- %Backup{} = backup -> Backup.delete(backup)
+ case Backup.get_by_id(backup_id) do
+ %Backup{} = backup -> Backup.delete_archive(backup)
nil -> :ok
end
end
@impl Oban.Worker
- def timeout(_job), do: :infinity
+ def timeout(_job), do: Config.get([Backup, :timeout], :timer.minutes(30))
defp has_email?(user) do
not is_nil(user.email) and user.email != ""
end
- defp maybe_deliver_email(backup, admin_user_id) do
+ defp maybe_deliver_email(backup) do
has_mailer = Pleroma.Config.get([Pleroma.Emails.Mailer, :enabled])
backup = backup |> Pleroma.Repo.preload(:user)
if has_email?(backup.user) and has_mailer do
backup
- |> Pleroma.Emails.UserEmail.backup_is_ready_email(admin_user_id)
+ |> Pleroma.Emails.UserEmail.backup_is_ready_email()
|> Pleroma.Emails.Mailer.deliver()
:ok
diff --git a/lib/pleroma/workers/cron/digest_emails_worker.ex b/lib/pleroma/workers/cron/digest_emails_worker.ex
index 1540c1605..17e92d10b 100644
--- a/lib/pleroma/workers/cron/digest_emails_worker.ex
+++ b/lib/pleroma/workers/cron/digest_emails_worker.ex
@@ -7,7 +7,7 @@ defmodule Pleroma.Workers.Cron.DigestEmailsWorker do
The worker to send digest emails.
"""
- use Oban.Worker, queue: "digest_emails"
+ use Oban.Worker, queue: "mailer"
alias Pleroma.Config
alias Pleroma.Emails
@@ -58,4 +58,7 @@ defmodule Pleroma.Workers.Cron.DigestEmailsWorker do
User.touch_last_digest_emailed_at(user)
end
+
+ @impl Oban.Worker
+ def timeout(_job), do: :timer.seconds(5)
end
diff --git a/lib/pleroma/workers/cron/new_users_digest_worker.ex b/lib/pleroma/workers/cron/new_users_digest_worker.ex
index 267fe2837..1f57aad4a 100644
--- a/lib/pleroma/workers/cron/new_users_digest_worker.ex
+++ b/lib/pleroma/workers/cron/new_users_digest_worker.ex
@@ -9,7 +9,7 @@ defmodule Pleroma.Workers.Cron.NewUsersDigestWorker do
import Ecto.Query
- use Pleroma.Workers.WorkerHelper, queue: "new_users_digest"
+ use Pleroma.Workers.WorkerHelper, queue: "background"
@impl Oban.Worker
def perform(_job) do
@@ -60,4 +60,7 @@ defmodule Pleroma.Workers.Cron.NewUsersDigestWorker do
:ok
end
+
+ @impl Oban.Worker
+ def timeout(_job), do: :timer.seconds(5)
end
diff --git a/lib/pleroma/workers/delete_worker.ex b/lib/pleroma/workers/delete_worker.ex
new file mode 100644
index 000000000..97003fb69
--- /dev/null
+++ b/lib/pleroma/workers/delete_worker.ex
@@ -0,0 +1,24 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Workers.DeleteWorker do
+ alias Pleroma.Instances.Instance
+ alias Pleroma.User
+
+ use Pleroma.Workers.WorkerHelper, queue: "slow"
+
+ @impl Oban.Worker
+
+ def perform(%Job{args: %{"op" => "delete_user", "user_id" => user_id}}) do
+ user = User.get_cached_by_id(user_id)
+ User.perform(:delete, user)
+ end
+
+ def perform(%Job{args: %{"op" => "delete_instance", "host" => host}}) do
+ Instance.perform(:delete_instance, host)
+ end
+
+ @impl Oban.Worker
+ def timeout(_job), do: :timer.seconds(900)
+end
diff --git a/lib/pleroma/workers/mailer_worker.ex b/lib/pleroma/workers/mailer_worker.ex
index 940716558..652bf77e0 100644
--- a/lib/pleroma/workers/mailer_worker.ex
+++ b/lib/pleroma/workers/mailer_worker.ex
@@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.MailerWorker do
- use Pleroma.Workers.WorkerHelper, queue: "mailer"
+ use Pleroma.Workers.WorkerHelper, queue: "background"
@impl Oban.Worker
def perform(%Job{args: %{"op" => "email", "encoded_email" => encoded_email, "config" => config}}) do
diff --git a/lib/pleroma/workers/mute_expire_worker.ex b/lib/pleroma/workers/mute_expire_worker.ex
index 8ce458d48..a7ab5883a 100644
--- a/lib/pleroma/workers/mute_expire_worker.ex
+++ b/lib/pleroma/workers/mute_expire_worker.ex
@@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.MuteExpireWorker do
- use Pleroma.Workers.WorkerHelper, queue: "mute_expire"
+ use Pleroma.Workers.WorkerHelper, queue: "background"
@impl Oban.Worker
def perform(%Job{args: %{"op" => "unmute_user", "muter_id" => muter_id, "mutee_id" => mutee_id}}) do
@@ -14,7 +14,7 @@ defmodule Pleroma.Workers.MuteExpireWorker do
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)
+ Pleroma.Web.CommonAPI.remove_mute(activity_id, user_id)
:ok
end
diff --git a/lib/pleroma/workers/poll_worker.ex b/lib/pleroma/workers/poll_worker.ex
index 022d026f8..af8997e70 100644
--- a/lib/pleroma/workers/poll_worker.ex
+++ b/lib/pleroma/workers/poll_worker.ex
@@ -6,7 +6,7 @@ defmodule Pleroma.Workers.PollWorker do
@moduledoc """
Generates notifications when a poll ends.
"""
- use Pleroma.Workers.WorkerHelper, queue: "poll_notifications"
+ use Pleroma.Workers.WorkerHelper, queue: "background"
alias Pleroma.Activity
alias Pleroma.Notification
@@ -14,8 +14,12 @@ defmodule Pleroma.Workers.PollWorker do
@impl Oban.Worker
def perform(%Job{args: %{"op" => "poll_end", "activity_id" => activity_id}}) do
- with %Activity{} = activity <- find_poll_activity(activity_id) do
- Notification.create_poll_notifications(activity)
+ with %Activity{} = activity <- find_poll_activity(activity_id),
+ {:ok, notifications} <- Notification.create_poll_notifications(activity) do
+ Notification.stream(notifications)
+ else
+ {:error, :poll_activity_not_found} = e -> {:cancel, e}
+ e -> {:error, e}
end
end
diff --git a/lib/pleroma/workers/publisher_worker.ex b/lib/pleroma/workers/publisher_worker.ex
index 598ae3779..63fcf4ac2 100644
--- a/lib/pleroma/workers/publisher_worker.ex
+++ b/lib/pleroma/workers/publisher_worker.ex
@@ -18,9 +18,9 @@ defmodule Pleroma.Workers.PublisherWorker do
Federator.perform(:publish, activity)
end
- def perform(%Job{args: %{"op" => "publish_one", "module" => module_name, "params" => params}}) do
+ def perform(%Job{args: %{"op" => "publish_one", "params" => params}}) do
params = Map.new(params, fn {k, v} -> {String.to_atom(k), v} end)
- Federator.perform(:publish_one, String.to_atom(module_name), params)
+ Federator.perform(:publish_one, params)
end
@impl Oban.Worker
diff --git a/lib/pleroma/workers/purge_expired_activity.ex b/lib/pleroma/workers/purge_expired_activity.ex
index e554684fe..f48e34042 100644
--- a/lib/pleroma/workers/purge_expired_activity.ex
+++ b/lib/pleroma/workers/purge_expired_activity.ex
@@ -6,8 +6,8 @@ defmodule Pleroma.Workers.PurgeExpiredActivity do
@moduledoc """
Worker which purges expired activity.
"""
-
- use Oban.Worker, queue: :activity_expiration, max_attempts: 1, unique: [period: :infinity]
+ @queue :background
+ use Oban.Worker, queue: @queue, max_attempts: 1, unique: [period: :infinity]
import Ecto.Query
@@ -46,20 +46,22 @@ defmodule Pleroma.Workers.PurgeExpiredActivity do
defp find_activity(id) do
with nil <- Activity.get_by_id_with_object(id) do
- {:error, :activity_not_found}
+ {:cancel, :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}
+ {:cancel, :user_not_found}
end
end
def get_expiration(id) do
+ queue = Atom.to_string(@queue)
+
from(j in Oban.Job,
where: j.state == "scheduled",
- where: j.queue == "activity_expiration",
+ where: j.queue == ^queue,
where: fragment("?->>'activity_id' = ?", j.args, ^id)
)
|> Pleroma.Repo.one()
diff --git a/lib/pleroma/workers/purge_expired_filter.ex b/lib/pleroma/workers/purge_expired_filter.ex
index 9114aeb7f..1f6931e4c 100644
--- a/lib/pleroma/workers/purge_expired_filter.ex
+++ b/lib/pleroma/workers/purge_expired_filter.ex
@@ -7,7 +7,7 @@ defmodule Pleroma.Workers.PurgeExpiredFilter do
Worker which purges expired filters
"""
- use Oban.Worker, queue: :filter_expiration, max_attempts: 1, unique: [period: :infinity]
+ use Oban.Worker, queue: :background, max_attempts: 1, unique: [period: :infinity]
import Ecto.Query
@@ -38,7 +38,7 @@ defmodule Pleroma.Workers.PurgeExpiredFilter do
def get_expiration(id) do
from(j in Job,
where: j.state == "scheduled",
- where: j.queue == "filter_expiration",
+ where: j.queue == "background",
where: fragment("?->'filter_id' = ?", j.args, ^id)
)
|> Repo.one()
diff --git a/lib/pleroma/workers/purge_expired_token.ex b/lib/pleroma/workers/purge_expired_token.ex
index 2ccd9e80b..1854bf561 100644
--- a/lib/pleroma/workers/purge_expired_token.ex
+++ b/lib/pleroma/workers/purge_expired_token.ex
@@ -7,7 +7,7 @@ defmodule Pleroma.Workers.PurgeExpiredToken do
Worker which purges expired OAuth tokens
"""
- use Oban.Worker, queue: :token_expiration, max_attempts: 1
+ use Oban.Worker, queue: :background, max_attempts: 1
@spec enqueue(%{token_id: integer(), valid_until: DateTime.t(), mod: module()}) ::
{:ok, Oban.Job.t()} | {:error, Ecto.Changeset.t()}
diff --git a/lib/pleroma/workers/receiver_worker.ex b/lib/pleroma/workers/receiver_worker.ex
index cf1bb62b4..fd5c13fca 100644
--- a/lib/pleroma/workers/receiver_worker.ex
+++ b/lib/pleroma/workers/receiver_worker.ex
@@ -3,24 +3,72 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.ReceiverWorker do
+ alias Pleroma.Signature
+ alias Pleroma.User
alias Pleroma.Web.Federator
use Pleroma.Workers.WorkerHelper, queue: "federator_incoming"
@impl Oban.Worker
+
+ def perform(%Job{
+ args: %{
+ "op" => "incoming_ap_doc",
+ "method" => method,
+ "params" => params,
+ "req_headers" => req_headers,
+ "request_path" => request_path,
+ "query_string" => query_string
+ }
+ }) do
+ # Oban's serialization converts our tuple headers to lists.
+ # Revert it for the signature validation.
+ req_headers = Enum.into(req_headers, [], &List.to_tuple(&1))
+
+ conn_data = %Plug.Conn{
+ method: method,
+ params: params,
+ req_headers: req_headers,
+ request_path: request_path,
+ query_string: query_string
+ }
+
+ with {:ok, %User{} = _actor} <- User.get_or_fetch_by_ap_id(conn_data.params["actor"]),
+ {:ok, _public_key} <- Signature.refetch_public_key(conn_data),
+ {:signature, true} <- {:signature, Signature.validate_signature(conn_data)},
+ {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do
+ {:ok, res}
+ else
+ e -> process_errors(e)
+ end
+ end
+
def perform(%Job{args: %{"op" => "incoming_ap_doc", "params" => params}}) do
with {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do
{:ok, res}
else
- {:error, :origin_containment_failed} -> {:cancel, :origin_containment_failed}
- {:error, :already_present} -> {:cancel, :already_present}
- {:error, {:validate_object, reason}} -> {:cancel, reason}
- {:error, {:error, {:validate, reason}}} -> {:cancel, reason}
- {:error, {:reject, reason}} -> {:cancel, reason}
- e -> e
+ e -> process_errors(e)
end
end
@impl Oban.Worker
+ def timeout(%_{args: %{"timeout" => timeout}}), do: timeout
+
def timeout(_job), do: :timer.seconds(5)
+
+ defp process_errors(errors) do
+ case errors do
+ {:error, :origin_containment_failed} -> {:cancel, :origin_containment_failed}
+ {:error, :already_present} -> {:cancel, :already_present}
+ {:error, {:validate_object, _} = reason} -> {:cancel, reason}
+ {:error, {:error, {:validate, {:error, _changeset} = reason}}} -> {:cancel, reason}
+ {:error, {:reject, _} = reason} -> {:cancel, reason}
+ {:signature, false} -> {:cancel, :invalid_signature}
+ {:error, "Object has been deleted"} = reason -> {:cancel, reason}
+ {:error, {:side_effects, {:error, :no_object_actor}} = reason} -> {:cancel, reason}
+ {:error, :not_found} = reason -> {:cancel, reason}
+ {:error, _} = e -> e
+ e -> {:error, e}
+ end
+ end
end
diff --git a/lib/pleroma/workers/remote_fetcher_worker.ex b/lib/pleroma/workers/remote_fetcher_worker.ex
index d2a77aa17..60096e14b 100644
--- a/lib/pleroma/workers/remote_fetcher_worker.ex
+++ b/lib/pleroma/workers/remote_fetcher_worker.ex
@@ -5,13 +5,31 @@
defmodule Pleroma.Workers.RemoteFetcherWorker do
alias Pleroma.Object.Fetcher
- use Pleroma.Workers.WorkerHelper, queue: "remote_fetcher"
+ use Pleroma.Workers.WorkerHelper, queue: "background"
@impl Oban.Worker
def perform(%Job{args: %{"op" => "fetch_remote", "id" => id} = args}) do
- {:ok, _object} = Fetcher.fetch_object_from_id(id, depth: args["depth"])
+ case Fetcher.fetch_object_from_id(id, depth: args["depth"]) do
+ {:ok, _object} ->
+ :ok
+
+ {:reject, reason} ->
+ {:cancel, reason}
+
+ {:error, :forbidden} ->
+ {:cancel, :forbidden}
+
+ {:error, :not_found} ->
+ {:cancel, :not_found}
+
+ {:error, :allowed_depth} ->
+ {:cancel, :allowed_depth}
+
+ {:error, _} = e ->
+ e
+ end
end
@impl Oban.Worker
- def timeout(_job), do: :timer.seconds(10)
+ def timeout(_job), do: :timer.seconds(15)
end
diff --git a/lib/pleroma/workers/rich_media_worker.ex b/lib/pleroma/workers/rich_media_worker.ex
new file mode 100644
index 000000000..2ebf42d4f
--- /dev/null
+++ b/lib/pleroma/workers/rich_media_worker.ex
@@ -0,0 +1,40 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Workers.RichMediaWorker do
+ alias Pleroma.Config
+ alias Pleroma.Web.RichMedia.Backfill
+ alias Pleroma.Web.RichMedia.Card
+
+ use Oban.Worker, queue: :background, max_attempts: 3, unique: [period: 300]
+
+ @impl Oban.Worker
+ def perform(%Job{args: %{"op" => "expire", "url" => url} = _args}) do
+ Card.delete(url)
+ end
+
+ def perform(%Job{args: %{"op" => "backfill", "url" => _url} = args}) do
+ case Backfill.run(args) do
+ :ok ->
+ :ok
+
+ {:error, type}
+ when type in [:invalid_metadata, :body_too_large, :content_type, :validate, :get, :head] ->
+ {:cancel, type}
+
+ error ->
+ {:error, error}
+ end
+ end
+
+ # There is timeout value enforced by Tesla.Middleware.Timeout
+ # which can be found in the RichMedia.Helpers module to allow us to detect
+ # a slow/infinite data stream and insert a negative cache entry for the URL
+ # We pad it by 2 seconds to be certain a slow connection is detected and we
+ # can inject a negative cache entry for the URL
+ @impl Oban.Worker
+ def timeout(_job) do
+ Config.get!([:rich_media, :timeout]) + :timer.seconds(2)
+ end
+end
diff --git a/lib/pleroma/workers/scheduled_activity_worker.ex b/lib/pleroma/workers/scheduled_activity_worker.ex
index 4df84d00f..ab62686f4 100644
--- a/lib/pleroma/workers/scheduled_activity_worker.ex
+++ b/lib/pleroma/workers/scheduled_activity_worker.ex
@@ -7,7 +7,7 @@ defmodule Pleroma.Workers.ScheduledActivityWorker do
The worker to post scheduled activity.
"""
- use Pleroma.Workers.WorkerHelper, queue: "scheduled_activities"
+ use Pleroma.Workers.WorkerHelper, queue: "federator_outgoing"
alias Pleroma.Repo
alias Pleroma.ScheduledActivity
diff --git a/lib/pleroma/workers/search_indexing_worker.ex b/lib/pleroma/workers/search_indexing_worker.ex
new file mode 100644
index 000000000..8969ae378
--- /dev/null
+++ b/lib/pleroma/workers/search_indexing_worker.ex
@@ -0,0 +1,26 @@
+defmodule Pleroma.Workers.SearchIndexingWorker do
+ use Pleroma.Workers.WorkerHelper, queue: "search_indexing"
+
+ @impl Oban.Worker
+
+ alias Pleroma.Config.Getting, as: Config
+
+ def perform(%Job{args: %{"op" => "add_to_index", "activity" => activity_id}}) do
+ activity = Pleroma.Activity.get_by_id_with_object(activity_id)
+
+ search_module = Config.get([Pleroma.Search, :module])
+
+ search_module.add_to_index(activity)
+ end
+
+ def perform(%Job{args: %{"op" => "remove_from_index", "object" => object_id}}) do
+ object = Pleroma.Object.get_by_id(object_id)
+
+ search_module = Config.get([Pleroma.Search, :module])
+
+ search_module.remove_from_index(object)
+ end
+
+ @impl Oban.Worker
+ def timeout(_job), do: :timer.seconds(5)
+end
diff --git a/lib/pleroma/workers/user_refresh_worker.ex b/lib/pleroma/workers/user_refresh_worker.ex
new file mode 100644
index 000000000..fb90e9c9c
--- /dev/null
+++ b/lib/pleroma/workers/user_refresh_worker.ex
@@ -0,0 +1,17 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Workers.UserRefreshWorker do
+ use Oban.Worker, queue: :background, max_attempts: 1, unique: [period: 300]
+
+ alias Pleroma.User
+
+ @impl true
+ def perform(%Job{args: %{"ap_id" => ap_id}}) do
+ User.fetch_by_ap_id(ap_id)
+ end
+
+ @impl Oban.Worker
+ def timeout(_job), do: :timer.seconds(15)
+end
diff --git a/lib/pleroma/workers/web_pusher_worker.ex b/lib/pleroma/workers/web_pusher_worker.ex
index 67e84b0c9..c549d3cd6 100644
--- a/lib/pleroma/workers/web_pusher_worker.ex
+++ b/lib/pleroma/workers/web_pusher_worker.ex
@@ -5,6 +5,7 @@
defmodule Pleroma.Workers.WebPusherWorker do
alias Pleroma.Notification
alias Pleroma.Repo
+ alias Pleroma.Web.Push.Impl
use Pleroma.Workers.WorkerHelper, queue: "web_push"
@@ -15,7 +16,8 @@ defmodule Pleroma.Workers.WebPusherWorker do
|> Repo.get(notification_id)
|> Repo.preload([:activity, :user])
- Pleroma.Web.Push.Impl.perform(notification)
+ Impl.build(notification)
+ |> Enum.each(&Impl.deliver(&1))
end
@impl Oban.Worker