aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/jason_types.ex9
-rw-r--r--lib/mix/pleroma.ex30
-rw-r--r--lib/mix/tasks/pleroma/app.ex2
-rw-r--r--lib/mix/tasks/pleroma/benchmark.ex2
-rw-r--r--lib/mix/tasks/pleroma/config.ex331
-rw-r--r--lib/mix/tasks/pleroma/count_statuses.ex2
-rw-r--r--lib/mix/tasks/pleroma/database.ex61
-rw-r--r--lib/mix/tasks/pleroma/digest.ex2
-rw-r--r--lib/mix/tasks/pleroma/docs.ex2
-rw-r--r--lib/mix/tasks/pleroma/ecto.ex2
-rw-r--r--lib/mix/tasks/pleroma/ecto/migrate.ex2
-rw-r--r--lib/mix/tasks/pleroma/ecto/rollback.ex7
-rw-r--r--lib/mix/tasks/pleroma/email.ex8
-rw-r--r--lib/mix/tasks/pleroma/emoji.ex2
-rw-r--r--lib/mix/tasks/pleroma/frontend.ex109
-rw-r--r--lib/mix/tasks/pleroma/instance.ex28
-rw-r--r--lib/mix/tasks/pleroma/notification_settings.ex2
-rw-r--r--lib/mix/tasks/pleroma/openapi_spec.ex8
-rw-r--r--lib/mix/tasks/pleroma/refresh_counter_cache.ex2
-rw-r--r--lib/mix/tasks/pleroma/relay.ex2
-rw-r--r--lib/mix/tasks/pleroma/robots_txt.ex2
-rw-r--r--lib/mix/tasks/pleroma/uploads.ex2
-rw-r--r--lib/mix/tasks/pleroma/user.ex74
-rw-r--r--lib/phoenix/transports/web_socket/raw.ex2
-rw-r--r--lib/pleroma/activity.ex34
-rw-r--r--lib/pleroma/activity/ir/topics.ex17
-rw-r--r--lib/pleroma/activity/queries.ex2
-rw-r--r--lib/pleroma/activity/search.ex42
-rw-r--r--lib/pleroma/application.ex67
-rw-r--r--lib/pleroma/application_requirements.ex27
-rw-r--r--lib/pleroma/bbs/authenticator.ex2
-rw-r--r--lib/pleroma/bbs/handler.ex2
-rw-r--r--lib/pleroma/bookmark.ex2
-rw-r--r--lib/pleroma/caching.ex19
-rw-r--r--lib/pleroma/captcha.ex8
-rw-r--r--lib/pleroma/captcha/kocaptcha.ex2
-rw-r--r--lib/pleroma/captcha/native.ex2
-rw-r--r--lib/pleroma/captcha/service.ex2
-rw-r--r--lib/pleroma/chat.ex2
-rw-r--r--lib/pleroma/chat/message_reference.ex2
-rw-r--r--lib/pleroma/clippy.ex2
-rw-r--r--lib/pleroma/config.ex19
-rw-r--r--lib/pleroma/config/deprecation_warnings.ex26
-rw-r--r--lib/pleroma/config/getting.ex8
-rw-r--r--lib/pleroma/config/helpers.ex2
-rw-r--r--lib/pleroma/config/holder.ex2
-rw-r--r--lib/pleroma/config/loader.ex2
-rw-r--r--lib/pleroma/config/oban.ex2
-rw-r--r--lib/pleroma/config/transfer_task.ex2
-rw-r--r--lib/pleroma/config_db.ex16
-rw-r--r--lib/pleroma/constants.ex7
-rw-r--r--lib/pleroma/conversation.ex16
-rw-r--r--lib/pleroma/conversation/participation.ex33
-rw-r--r--lib/pleroma/conversation/participation/recipient_ship.ex2
-rw-r--r--lib/pleroma/counter_cache.ex2
-rw-r--r--lib/pleroma/delivery.ex2
-rw-r--r--lib/pleroma/docs/generator.ex2
-rw-r--r--lib/pleroma/docs/json.ex8
-rw-r--r--lib/pleroma/docs/markdown.ex2
-rw-r--r--lib/pleroma/earmark_renderer.ex2
-rw-r--r--lib/pleroma/ecto_enums.ex2
-rw-r--r--lib/pleroma/ecto_type/activity_pub/object_validators/date_time.ex2
-rw-r--r--lib/pleroma/ecto_type/activity_pub/object_validators/emoji.ex2
-rw-r--r--lib/pleroma/ecto_type/activity_pub/object_validators/object_id.ex2
-rw-r--r--lib/pleroma/ecto_type/activity_pub/object_validators/recipients.ex2
-rw-r--r--lib/pleroma/ecto_type/activity_pub/object_validators/safe_text.ex2
-rw-r--r--lib/pleroma/ecto_type/activity_pub/object_validators/uri.ex2
-rw-r--r--lib/pleroma/ecto_type/config/atom.ex2
-rw-r--r--lib/pleroma/ecto_type/config/binary_value.ex2
-rw-r--r--lib/pleroma/emails/admin_email.ex12
-rw-r--r--lib/pleroma/emails/mailer.ex2
-rw-r--r--lib/pleroma/emails/new_users_digest_email.ex2
-rw-r--r--lib/pleroma/emails/user_email.ex67
-rw-r--r--lib/pleroma/emoji-data.txt769
-rw-r--r--lib/pleroma/emoji-test.txt4879
-rw-r--r--lib/pleroma/emoji.ex31
-rw-r--r--lib/pleroma/emoji/formatter.ex5
-rw-r--r--lib/pleroma/emoji/loader.ex15
-rw-r--r--lib/pleroma/emoji/pack.ex70
-rw-r--r--lib/pleroma/filter.ex126
-rw-r--r--lib/pleroma/following_relationship.ex46
-rw-r--r--lib/pleroma/formatter.ex2
-rw-r--r--lib/pleroma/frontend.ex110
-rw-r--r--lib/pleroma/gopher/server.ex4
-rw-r--r--lib/pleroma/gun.ex2
-rw-r--r--lib/pleroma/gun/api.ex2
-rw-r--r--lib/pleroma/gun/conn.ex2
-rw-r--r--lib/pleroma/gun/connection_pool.ex2
-rw-r--r--lib/pleroma/gun/connection_pool/reclaimer.ex2
-rw-r--r--lib/pleroma/gun/connection_pool/worker.ex2
-rw-r--r--lib/pleroma/gun/connection_pool/worker_supervisor.ex2
-rw-r--r--lib/pleroma/healthcheck.ex2
-rw-r--r--lib/pleroma/helpers/auth_helper.ex46
-rw-r--r--lib/pleroma/helpers/inet_helper.ex2
-rw-r--r--lib/pleroma/helpers/media_helper.ex2
-rw-r--r--lib/pleroma/helpers/qt_fast_start.ex2
-rw-r--r--lib/pleroma/helpers/uri_helper.ex2
-rw-r--r--lib/pleroma/html.ex10
-rw-r--r--lib/pleroma/http.ex2
-rw-r--r--lib/pleroma/http/adapter_helper.ex2
-rw-r--r--lib/pleroma/http/adapter_helper/default.ex2
-rw-r--r--lib/pleroma/http/adapter_helper/gun.ex2
-rw-r--r--lib/pleroma/http/adapter_helper/hackney.ex2
-rw-r--r--lib/pleroma/http/ex_aws.ex2
-rw-r--r--lib/pleroma/http/request.ex2
-rw-r--r--lib/pleroma/http/request_builder.ex2
-rw-r--r--lib/pleroma/http/tzdata.ex2
-rw-r--r--lib/pleroma/http/web_push.ex2
-rw-r--r--lib/pleroma/instances.ex3
-rw-r--r--lib/pleroma/instances/instance.ex28
-rw-r--r--lib/pleroma/job_queue_monitor.ex2
-rw-r--r--lib/pleroma/jwt.ex2
-rw-r--r--lib/pleroma/keys.ex2
-rw-r--r--lib/pleroma/list.ex10
-rw-r--r--lib/pleroma/logging.ex7
-rw-r--r--lib/pleroma/maintenance.ex2
-rw-r--r--lib/pleroma/maps.ex2
-rw-r--r--lib/pleroma/marker.ex2
-rw-r--r--lib/pleroma/mfa.ex4
-rw-r--r--lib/pleroma/mfa/backup_codes.ex2
-rw-r--r--lib/pleroma/mfa/changeset.ex2
-rw-r--r--lib/pleroma/mfa/settings.ex2
-rw-r--r--lib/pleroma/mfa/token.ex2
-rw-r--r--lib/pleroma/mfa/totp.ex2
-rw-r--r--lib/pleroma/migration_helper/notification_backfill.ex2
-rw-r--r--lib/pleroma/moderation_log.ex443
-rw-r--r--lib/pleroma/notification.ex32
-rw-r--r--lib/pleroma/object.ex47
-rw-r--r--lib/pleroma/object/containment.ex2
-rw-r--r--lib/pleroma/object/fetcher.ex33
-rw-r--r--lib/pleroma/object_tombstone.ex2
-rw-r--r--lib/pleroma/otp_version.ex2
-rw-r--r--lib/pleroma/pagination.ex2
-rw-r--r--lib/pleroma/password/pbkdf2.ex55
-rw-r--r--lib/pleroma/password_reset_token.ex13
-rw-r--r--lib/pleroma/registration.ex2
-rw-r--r--lib/pleroma/release_tasks.ex2
-rw-r--r--lib/pleroma/repo.ex2
-rw-r--r--lib/pleroma/report_note.ex2
-rw-r--r--lib/pleroma/reverse_proxy.ex34
-rw-r--r--lib/pleroma/reverse_proxy/client.ex2
-rw-r--r--lib/pleroma/reverse_proxy/client/hackney.ex2
-rw-r--r--lib/pleroma/reverse_proxy/client/tesla.ex2
-rw-r--r--lib/pleroma/scheduled_activity.ex2
-rw-r--r--lib/pleroma/signature.ex8
-rw-r--r--lib/pleroma/stats.ex29
-rw-r--r--lib/pleroma/telemetry/logger.ex2
-rw-r--r--lib/pleroma/tesla/middleware/connection_pool.ex2
-rw-r--r--lib/pleroma/tests/auth_test_controller.ex2
-rw-r--r--lib/pleroma/thread_mute.ex2
-rw-r--r--lib/pleroma/upload.ex56
-rw-r--r--lib/pleroma/upload/filter.ex2
-rw-r--r--lib/pleroma/upload/filter/anonymize_filename.ex2
-rw-r--r--lib/pleroma/upload/filter/dedupe.ex2
-rw-r--r--lib/pleroma/upload/filter/exiftool.ex9
-rw-r--r--lib/pleroma/upload/filter/mogrifun.ex6
-rw-r--r--lib/pleroma/upload/filter/mogrify.ex6
-rw-r--r--lib/pleroma/uploaders/local.ex2
-rw-r--r--lib/pleroma/uploaders/s3.ex20
-rw-r--r--lib/pleroma/uploaders/uploader.ex6
-rw-r--r--lib/pleroma/user.ex463
-rw-r--r--lib/pleroma/user/backup.ex258
-rw-r--r--lib/pleroma/user/import.ex4
-rw-r--r--lib/pleroma/user/notification_setting.ex2
-rw-r--r--lib/pleroma/user/query.ex27
-rw-r--r--lib/pleroma/user/search.ex9
-rw-r--r--lib/pleroma/user/welcome_chat_message.ex2
-rw-r--r--lib/pleroma/user/welcome_email.ex2
-rw-r--r--lib/pleroma/user/welcome_message.ex2
-rw-r--r--lib/pleroma/user_invite_token.ex2
-rw-r--r--lib/pleroma/user_relationship.ex2
-rw-r--r--lib/pleroma/utils.ex23
-rw-r--r--lib/pleroma/web.ex20
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub.ex101
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub/persisting.ex7
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub/streaming.ex12
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub_controller.ex39
-rw-r--r--lib/pleroma/web/activity_pub/builder.ex7
-rw-r--r--lib/pleroma/web/activity_pub/internal_fetch_actor.ex2
-rw-r--r--lib/pleroma/web/activity_pub/mrf.ex99
-rw-r--r--lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex20
-rw-r--r--lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex2
-rw-r--r--lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex2
-rw-r--r--lib/pleroma/web/activity_pub/mrf/drop_policy.ex2
-rw-r--r--lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex4
-rw-r--r--lib/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy.ex2
-rw-r--r--lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex29
-rw-r--r--lib/pleroma/web/activity_pub/mrf/keyword_policy.ex44
-rw-r--r--lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex21
-rw-r--r--lib/pleroma/web/activity_pub/mrf/mention_policy.ex20
-rw-r--r--lib/pleroma/web/activity_pub/mrf/no_empty_policy.ex61
-rw-r--r--lib/pleroma/web/activity_pub/mrf/no_op_policy.ex2
-rw-r--r--lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex2
-rw-r--r--lib/pleroma/web/activity_pub/mrf/normalize_markup.ex21
-rw-r--r--lib/pleroma/web/activity_pub/mrf/object_age_policy.ex30
-rw-r--r--lib/pleroma/web/activity_pub/mrf/pipeline_filtering.ex7
-rw-r--r--lib/pleroma/web/activity_pub/mrf/reject_non_public.ex25
-rw-r--r--lib/pleroma/web/activity_pub/mrf/simple_policy.ex76
-rw-r--r--lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex2
-rw-r--r--lib/pleroma/web/activity_pub/mrf/subchain_policy.ex26
-rw-r--r--lib/pleroma/web/activity_pub/mrf/tag_policy.ex2
-rw-r--r--lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex23
-rw-r--r--lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex30
-rw-r--r--lib/pleroma/web/activity_pub/object_validator.ex8
-rw-r--r--lib/pleroma/web/activity_pub/object_validator/validating.ex7
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/accept_reject_validator.ex2
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/announce_validator.ex9
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/answer_validator.ex2
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/article_note_validator.ex2
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex5
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/audio_video_validator.ex34
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/block_validator.ex2
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex2
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/common_fixes.ex2
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/common_validations.ex4
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex2
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex2
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/create_note_validator.ex2
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/delete_validator.ex2
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex2
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/event_validator.ex2
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/follow_validator.ex2
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/like_validator.ex2
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/question_options_validator.ex2
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/question_validator.ex2
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/undo_validator.ex2
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/update_validator.ex2
-rw-r--r--lib/pleroma/web/activity_pub/pipeline.ex26
-rw-r--r--lib/pleroma/web/activity_pub/publisher.ex36
-rw-r--r--lib/pleroma/web/activity_pub/relay.ex2
-rw-r--r--lib/pleroma/web/activity_pub/side_effects.ex50
-rw-r--r--lib/pleroma/web/activity_pub/side_effects/handling.ex8
-rw-r--r--lib/pleroma/web/activity_pub/transmogrifier.ex17
-rw-r--r--lib/pleroma/web/activity_pub/utils.ex5
-rw-r--r--lib/pleroma/web/activity_pub/views/object_view.ex6
-rw-r--r--lib/pleroma/web/activity_pub/views/user_view.ex8
-rw-r--r--lib/pleroma/web/activity_pub/visibility.ex38
-rw-r--r--lib/pleroma/web/admin_api/controllers/admin_api_controller.ex308
-rw-r--r--lib/pleroma/web/admin_api/controllers/chat_controller.ex6
-rw-r--r--lib/pleroma/web/admin_api/controllers/config_controller.ex8
-rw-r--r--lib/pleroma/web/admin_api/controllers/fallback_controller.ex2
-rw-r--r--lib/pleroma/web/admin_api/controllers/frontend_controller.ex40
-rw-r--r--lib/pleroma/web/admin_api/controllers/instance_document_controller.ex6
-rw-r--r--lib/pleroma/web/admin_api/controllers/invite_controller.ex6
-rw-r--r--lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex10
-rw-r--r--lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex4
-rw-r--r--lib/pleroma/web/admin_api/controllers/relay_controller.ex6
-rw-r--r--lib/pleroma/web/admin_api/controllers/report_controller.ex25
-rw-r--r--lib/pleroma/web/admin_api/controllers/status_controller.ex6
-rw-r--r--lib/pleroma/web/admin_api/controllers/user_controller.ex281
-rw-r--r--lib/pleroma/web/admin_api/report.ex2
-rw-r--r--lib/pleroma/web/admin_api/search.ex2
-rw-r--r--lib/pleroma/web/admin_api/views/account_view.ex11
-rw-r--r--lib/pleroma/web/admin_api/views/chat_view.ex2
-rw-r--r--lib/pleroma/web/admin_api/views/config_view.ex2
-rw-r--r--lib/pleroma/web/admin_api/views/frontend_view.ex21
-rw-r--r--lib/pleroma/web/admin_api/views/invite_view.ex2
-rw-r--r--lib/pleroma/web/admin_api/views/media_proxy_cache_view.ex2
-rw-r--r--lib/pleroma/web/admin_api/views/moderation_log_view.ex3
-rw-r--r--lib/pleroma/web/admin_api/views/report_view.ex5
-rw-r--r--lib/pleroma/web/admin_api/views/status_view.ex6
-rw-r--r--lib/pleroma/web/api_spec.ex95
-rw-r--r--lib/pleroma/web/api_spec/cast_and_validate.ex2
-rw-r--r--lib/pleroma/web/api_spec/helpers.ex4
-rw-r--r--lib/pleroma/web/api_spec/operations/account_operation.ex87
-rw-r--r--lib/pleroma/web/api_spec/operations/admin/chat_operation.ex16
-rw-r--r--lib/pleroma/web/api_spec/operations/admin/config_operation.ex20
-rw-r--r--lib/pleroma/web/api_spec/operations/admin/frontend_operation.ex85
-rw-r--r--lib/pleroma/web/api_spec/operations/admin/instance_document_operation.ex20
-rw-r--r--lib/pleroma/web/api_spec/operations/admin/invite_operation.ex18
-rw-r--r--lib/pleroma/web/api_spec/operations/admin/media_proxy_cache_operation.ex22
-rw-r--r--lib/pleroma/web/api_spec/operations/admin/o_auth_app_operation.ex26
-rw-r--r--lib/pleroma/web/api_spec/operations/admin/relay_operation.ex20
-rw-r--r--lib/pleroma/web/api_spec/operations/admin/report_operation.ex40
-rw-r--r--lib/pleroma/web/api_spec/operations/admin/status_operation.ex29
-rw-r--r--lib/pleroma/web/api_spec/operations/app_operation.ex8
-rw-r--r--lib/pleroma/web/api_spec/operations/chat_operation.ex58
-rw-r--r--lib/pleroma/web/api_spec/operations/conversation_operation.ex33
-rw-r--r--lib/pleroma/web/api_spec/operations/custom_emoji_operation.ex6
-rw-r--r--lib/pleroma/web/api_spec/operations/domain_block_operation.ex11
-rw-r--r--lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex16
-rw-r--r--lib/pleroma/web/api_spec/operations/filter_operation.ex49
-rw-r--r--lib/pleroma/web/api_spec/operations/follow_request_operation.ex14
-rw-r--r--lib/pleroma/web/api_spec/operations/instance_operation.ex6
-rw-r--r--lib/pleroma/web/api_spec/operations/list_operation.ex10
-rw-r--r--lib/pleroma/web/api_spec/operations/marker_operation.ex2
-rw-r--r--lib/pleroma/web/api_spec/operations/media_operation.ex16
-rw-r--r--lib/pleroma/web/api_spec/operations/notification_operation.ex11
-rw-r--r--lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex23
-rw-r--r--lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex79
-rw-r--r--lib/pleroma/web/api_spec/operations/pleroma_conversation_operation.ex11
-rw-r--r--lib/pleroma/web/api_spec/operations/pleroma_emoji_file_operation.ex17
-rw-r--r--lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex38
-rw-r--r--lib/pleroma/web/api_spec/operations/pleroma_instances_operation.ex40
-rw-r--r--lib/pleroma/web/api_spec/operations/pleroma_mascot_operation.ex6
-rw-r--r--lib/pleroma/web/api_spec/operations/pleroma_notification_operation.ex7
-rw-r--r--lib/pleroma/web/api_spec/operations/pleroma_report_operation.ex97
-rw-r--r--lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex2
-rw-r--r--lib/pleroma/web/api_spec/operations/poll_operation.ex2
-rw-r--r--lib/pleroma/web/api_spec/operations/report_operation.ex4
-rw-r--r--lib/pleroma/web/api_spec/operations/scheduled_activity_operation.ex10
-rw-r--r--lib/pleroma/web/api_spec/operations/search_operation.ex2
-rw-r--r--lib/pleroma/web/api_spec/operations/status_operation.ex158
-rw-r--r--lib/pleroma/web/api_spec/operations/subscription_operation.ex31
-rw-r--r--lib/pleroma/web/api_spec/operations/timeline_operation.ex33
-rw-r--r--lib/pleroma/web/api_spec/operations/user_import_operation.ex14
-rw-r--r--lib/pleroma/web/api_spec/render_error.ex2
-rw-r--r--lib/pleroma/web/api_spec/schemas/account.ex12
-rw-r--r--lib/pleroma/web/api_spec/schemas/account_field.ex2
-rw-r--r--lib/pleroma/web/api_spec/schemas/account_relationship.ex4
-rw-r--r--lib/pleroma/web/api_spec/schemas/actor_type.ex2
-rw-r--r--lib/pleroma/web/api_spec/schemas/api_error.ex2
-rw-r--r--lib/pleroma/web/api_spec/schemas/attachment.ex2
-rw-r--r--lib/pleroma/web/api_spec/schemas/boolean_like.ex2
-rw-r--r--lib/pleroma/web/api_spec/schemas/chat.ex4
-rw-r--r--lib/pleroma/web/api_spec/schemas/chat_message.ex8
-rw-r--r--lib/pleroma/web/api_spec/schemas/conversation.ex2
-rw-r--r--lib/pleroma/web/api_spec/schemas/emoji.ex2
-rw-r--r--lib/pleroma/web/api_spec/schemas/flake_id.ex2
-rw-r--r--lib/pleroma/web/api_spec/schemas/list.ex2
-rw-r--r--lib/pleroma/web/api_spec/schemas/poll.ex11
-rw-r--r--lib/pleroma/web/api_spec/schemas/push_subscription.ex2
-rw-r--r--lib/pleroma/web/api_spec/schemas/scheduled_status.ex12
-rw-r--r--lib/pleroma/web/api_spec/schemas/status.ex9
-rw-r--r--lib/pleroma/web/api_spec/schemas/tag.ex2
-rw-r--r--lib/pleroma/web/api_spec/schemas/visibility_scope.ex4
-rw-r--r--lib/pleroma/web/auth/authenticator.ex2
-rw-r--r--lib/pleroma/web/auth/ldap_authenticator.ex2
-rw-r--r--lib/pleroma/web/auth/pleroma_authenticator.ex4
-rw-r--r--lib/pleroma/web/auth/totp_authenticator.ex2
-rw-r--r--lib/pleroma/web/channels/user_socket.ex2
-rw-r--r--lib/pleroma/web/chat_channel.ex2
-rw-r--r--lib/pleroma/web/common_api.ex74
-rw-r--r--lib/pleroma/web/common_api/activity_draft.ex80
-rw-r--r--lib/pleroma/web/common_api/utils.ex119
-rw-r--r--lib/pleroma/web/controller_helper.ex4
-rw-r--r--lib/pleroma/web/embed_controller.ex4
-rw-r--r--lib/pleroma/web/endpoint.ex14
-rw-r--r--lib/pleroma/web/fallback/legacy_pleroma_api_rerouter_plug.ex26
-rw-r--r--lib/pleroma/web/fallback/redirect_controller.ex8
-rw-r--r--lib/pleroma/web/fed_sockets.ex185
-rw-r--r--lib/pleroma/web/fed_sockets/fed_registry.ex185
-rw-r--r--lib/pleroma/web/fed_sockets/fed_socket.ex137
-rw-r--r--lib/pleroma/web/fed_sockets/fetch_registry.ex151
-rw-r--r--lib/pleroma/web/fed_sockets/incoming_handler.ex88
-rw-r--r--lib/pleroma/web/fed_sockets/ingester_worker.ex33
-rw-r--r--lib/pleroma/web/fed_sockets/outgoing_handler.ex151
-rw-r--r--lib/pleroma/web/fed_sockets/socket_info.ex52
-rw-r--r--lib/pleroma/web/fed_sockets/supervisor.ex59
-rw-r--r--lib/pleroma/web/federator.ex6
-rw-r--r--lib/pleroma/web/federator/publisher.ex2
-rw-r--r--lib/pleroma/web/federator/publishing.ex7
-rw-r--r--lib/pleroma/web/feed/feed_view.ex8
-rw-r--r--lib/pleroma/web/feed/tag_controller.ex2
-rw-r--r--lib/pleroma/web/feed/user_controller.ex2
-rw-r--r--lib/pleroma/web/gettext.ex2
-rw-r--r--lib/pleroma/web/instance_document.ex2
-rw-r--r--lib/pleroma/web/mailer/subscription_controller.ex2
-rw-r--r--lib/pleroma/web/masto_fe_controller.ex36
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/account_controller.ex53
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/app_controller.ex22
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/auth_controller.ex67
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex11
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/filter_controller.ex63
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/instance_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/list_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/marker_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/media_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/notification_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/poll_controller.ex6
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/report_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/search_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/status_controller.ex42
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex18
-rw-r--r--lib/pleroma/web/mastodon_api/mastodon_api.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/views/account_view.ex31
-rw-r--r--lib/pleroma/web/mastodon_api/views/app_view.ex6
-rw-r--r--lib/pleroma/web/mastodon_api/views/conversation_view.ex16
-rw-r--r--lib/pleroma/web/mastodon_api/views/custom_emoji_view.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/views/filter_view.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/views/instance_view.ex7
-rw-r--r--lib/pleroma/web/mastodon_api/views/list_view.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/views/marker_view.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/views/notification_view.ex15
-rw-r--r--lib/pleroma/web/mastodon_api/views/poll_view.ex42
-rw-r--r--lib/pleroma/web/mastodon_api/views/report_view.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex5
-rw-r--r--lib/pleroma/web/mastodon_api/views/status_view.ex63
-rw-r--r--lib/pleroma/web/mastodon_api/views/subscription_view.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/websocket_handler.ex2
-rw-r--r--lib/pleroma/web/media_proxy.ex31
-rw-r--r--lib/pleroma/web/media_proxy/invalidation.ex2
-rw-r--r--lib/pleroma/web/media_proxy/invalidation/http.ex2
-rw-r--r--lib/pleroma/web/media_proxy/invalidation/script.ex21
-rw-r--r--lib/pleroma/web/media_proxy/media_proxy_controller.ex2
-rw-r--r--lib/pleroma/web/metadata.ex2
-rw-r--r--lib/pleroma/web/metadata/player_view.ex2
-rw-r--r--lib/pleroma/web/metadata/providers/feed.ex2
-rw-r--r--lib/pleroma/web/metadata/providers/open_graph.ex2
-rw-r--r--lib/pleroma/web/metadata/providers/provider.ex2
-rw-r--r--lib/pleroma/web/metadata/providers/rel_me.ex2
-rw-r--r--lib/pleroma/web/metadata/providers/restrict_indexing.ex6
-rw-r--r--lib/pleroma/web/metadata/providers/twitter_card.ex2
-rw-r--r--lib/pleroma/web/metadata/utils.ex2
-rw-r--r--lib/pleroma/web/mongoose_im/mongoose_im_controller.ex6
-rw-r--r--lib/pleroma/web/nodeinfo/nodeinfo.ex2
-rw-r--r--lib/pleroma/web/nodeinfo/nodeinfo_controller.ex2
-rw-r--r--lib/pleroma/web/o_auth.ex2
-rw-r--r--lib/pleroma/web/o_auth/app.ex2
-rw-r--r--lib/pleroma/web/o_auth/authorization.ex6
-rw-r--r--lib/pleroma/web/o_auth/fallback_controller.ex2
-rw-r--r--lib/pleroma/web/o_auth/mfa_controller.ex5
-rw-r--r--lib/pleroma/web/o_auth/mfa_view.ex2
-rw-r--r--lib/pleroma/web/o_auth/o_auth_controller.ex52
-rw-r--r--lib/pleroma/web/o_auth/o_auth_view.ex6
-rw-r--r--lib/pleroma/web/o_auth/scopes.ex2
-rw-r--r--lib/pleroma/web/o_auth/token.ex22
-rw-r--r--lib/pleroma/web/o_auth/token/query.ex2
-rw-r--r--lib/pleroma/web/o_auth/token/strategy/refresh_token.ex2
-rw-r--r--lib/pleroma/web/o_auth/token/strategy/revoke.ex2
-rw-r--r--lib/pleroma/web/o_auth/token/utils.ex2
-rw-r--r--lib/pleroma/web/o_status/o_status_controller.ex14
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/account_controller.ex4
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/backup_controller.ex28
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/chat_controller.ex58
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex2
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex41
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex67
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex36
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/instances_controller.ex21
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex2
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/notification_controller.ex2
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/report_controller.ex46
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex2
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex2
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex2
-rw-r--r--lib/pleroma/web/pleroma_api/views/backup_view.ex29
-rw-r--r--lib/pleroma/web/pleroma_api/views/chat/message_reference_view.ex15
-rw-r--r--lib/pleroma/web/pleroma_api/views/chat_view.ex2
-rw-r--r--lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex6
-rw-r--r--lib/pleroma/web/pleroma_api/views/report_view.ex55
-rw-r--r--lib/pleroma/web/pleroma_api/views/scrobble_view.ex4
-rw-r--r--lib/pleroma/web/plug.ex2
-rw-r--r--lib/pleroma/web/plugs/admin_secret_authentication_plug.ex24
-rw-r--r--lib/pleroma/web/plugs/authentication_plug.ex67
-rw-r--r--lib/pleroma/web/plugs/basic_auth_decoder_plug.ex8
-rw-r--r--lib/pleroma/web/plugs/cache.ex10
-rw-r--r--lib/pleroma/web/plugs/digest_plug.ex20
-rw-r--r--lib/pleroma/web/plugs/ensure_authenticated_plug.ex6
-rw-r--r--lib/pleroma/web/plugs/ensure_public_or_authenticated_plug.ex7
-rw-r--r--lib/pleroma/web/plugs/ensure_user_key_plug.ex18
-rw-r--r--lib/pleroma/web/plugs/ensure_user_token_assigns_plug.ex41
-rw-r--r--lib/pleroma/web/plugs/expect_authenticated_check_plug.ex2
-rw-r--r--lib/pleroma/web/plugs/expect_public_or_authenticated_check_plug.ex2
-rw-r--r--lib/pleroma/web/plugs/federating_plug.ex2
-rw-r--r--lib/pleroma/web/plugs/frontend_static.ex11
-rw-r--r--lib/pleroma/web/plugs/http_security_plug.ex28
-rw-r--r--lib/pleroma/web/plugs/http_signature_plug.ex2
-rw-r--r--lib/pleroma/web/plugs/idempotency_plug.ex8
-rw-r--r--lib/pleroma/web/plugs/instance_static.ex2
-rw-r--r--lib/pleroma/web/plugs/legacy_authentication_plug.ex41
-rw-r--r--lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex53
-rw-r--r--lib/pleroma/web/plugs/o_auth_plug.ex95
-rw-r--r--lib/pleroma/web/plugs/o_auth_scopes_plug.ex25
-rw-r--r--lib/pleroma/web/plugs/plug_helper.ex2
-rw-r--r--lib/pleroma/web/plugs/rate_limiter.ex8
-rw-r--r--lib/pleroma/web/plugs/rate_limiter/limiter_supervisor.ex2
-rw-r--r--lib/pleroma/web/plugs/rate_limiter/supervisor.ex2
-rw-r--r--lib/pleroma/web/plugs/remote_ip.ex2
-rw-r--r--lib/pleroma/web/plugs/session_authentication_plug.ex21
-rw-r--r--lib/pleroma/web/plugs/set_format_plug.ex2
-rw-r--r--lib/pleroma/web/plugs/set_locale_plug.ex2
-rw-r--r--lib/pleroma/web/plugs/set_user_session_id_plug.ex11
-rw-r--r--lib/pleroma/web/plugs/static_fe_plug.ex2
-rw-r--r--lib/pleroma/web/plugs/trailing_format_plug.ex2
-rw-r--r--lib/pleroma/web/plugs/uploaded_media.ex13
-rw-r--r--lib/pleroma/web/plugs/user_enabled_plug.ex11
-rw-r--r--lib/pleroma/web/plugs/user_fetcher_plug.ex8
-rw-r--r--lib/pleroma/web/plugs/user_is_admin_plug.ex2
-rw-r--r--lib/pleroma/web/plugs/user_tracking_plug.ex30
-rw-r--r--lib/pleroma/web/preload.ex2
-rw-r--r--lib/pleroma/web/preload/providers/instance.ex2
-rw-r--r--lib/pleroma/web/preload/providers/provider.ex2
-rw-r--r--lib/pleroma/web/preload/providers/timelines.ex2
-rw-r--r--lib/pleroma/web/preload/providers/user.ex2
-rw-r--r--lib/pleroma/web/push.ex2
-rw-r--r--lib/pleroma/web/push/impl.ex16
-rw-r--r--lib/pleroma/web/push/subscription.ex5
-rw-r--r--lib/pleroma/web/rel_me.ex5
-rw-r--r--lib/pleroma/web/rich_media/helpers.ex7
-rw-r--r--lib/pleroma/web/rich_media/parser.ex10
-rw-r--r--lib/pleroma/web/rich_media/parser/ttl.ex2
-rw-r--r--lib/pleroma/web/rich_media/parser/ttl/aws_signed_url.ex2
-rw-r--r--lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex2
-rw-r--r--lib/pleroma/web/rich_media/parsers/o_embed.ex2
-rw-r--r--lib/pleroma/web/rich_media/parsers/ogp.ex2
-rw-r--r--lib/pleroma/web/rich_media/parsers/twitter_card.ex2
-rw-r--r--lib/pleroma/web/router.ex125
-rw-r--r--lib/pleroma/web/static_fe/static_fe_controller.ex4
-rw-r--r--lib/pleroma/web/static_fe/static_fe_view.ex2
-rw-r--r--lib/pleroma/web/streamer.ex55
-rw-r--r--lib/pleroma/web/templates/email/digest.html.eex2
-rw-r--r--lib/pleroma/web/templates/embed/show.html.eex2
-rw-r--r--lib/pleroma/web/templates/feed/feed/_activity.atom.eex2
-rw-r--r--lib/pleroma/web/templates/feed/feed/_activity.rss.eex5
-rw-r--r--lib/pleroma/web/templates/layout/app.html.eex236
-rw-r--r--lib/pleroma/web/templates/o_auth/o_auth/show.html.eex66
-rw-r--r--lib/pleroma/web/translation_helpers.ex2
-rw-r--r--lib/pleroma/web/twitter_api/controller.ex9
-rw-r--r--lib/pleroma/web/twitter_api/controllers/password_controller.ex3
-rw-r--r--lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex2
-rw-r--r--lib/pleroma/web/twitter_api/controllers/util_controller.ex4
-rw-r--r--lib/pleroma/web/twitter_api/twitter_api.ex17
-rw-r--r--lib/pleroma/web/twitter_api/views/password_view.ex2
-rw-r--r--lib/pleroma/web/twitter_api/views/remote_follow_view.ex2
-rw-r--r--lib/pleroma/web/twitter_api/views/token_view.ex2
-rw-r--r--lib/pleroma/web/twitter_api/views/util_view.ex2
-rw-r--r--lib/pleroma/web/uploader_controller.ex2
-rw-r--r--lib/pleroma/web/views/email_view.ex2
-rw-r--r--lib/pleroma/web/views/embed_view.ex9
-rw-r--r--lib/pleroma/web/views/error_helpers.ex2
-rw-r--r--lib/pleroma/web/views/error_view.ex2
-rw-r--r--lib/pleroma/web/views/layout_view.ex2
-rw-r--r--lib/pleroma/web/views/mailer/subscription_view.ex2
-rw-r--r--lib/pleroma/web/views/masto_fe_view.ex2
-rw-r--r--lib/pleroma/web/views/streamer_view.ex24
-rw-r--r--lib/pleroma/web/web_finger.ex21
-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.ex26
-rw-r--r--lib/pleroma/workers/background_worker.ex21
-rw-r--r--lib/pleroma/workers/backup_worker.ex54
-rw-r--r--lib/pleroma/workers/cron/digest_emails_worker.ex2
-rw-r--r--lib/pleroma/workers/cron/new_users_digest_worker.ex2
-rw-r--r--lib/pleroma/workers/mailer_worker.ex2
-rw-r--r--lib/pleroma/workers/mute_expire_worker.ex20
-rw-r--r--lib/pleroma/workers/publisher_worker.ex2
-rw-r--r--lib/pleroma/workers/purge_expired_activity.ex4
-rw-r--r--lib/pleroma/workers/purge_expired_filter.ex43
-rw-r--r--lib/pleroma/workers/purge_expired_token.ex2
-rw-r--r--lib/pleroma/workers/receiver_worker.ex2
-rw-r--r--lib/pleroma/workers/remote_fetcher_worker.ex2
-rw-r--r--lib/pleroma/workers/scheduled_activity_worker.ex56
-rw-r--r--lib/pleroma/workers/transmogrifier_worker.ex2
-rw-r--r--lib/pleroma/workers/web_pusher_worker.ex2
-rw-r--r--lib/pleroma/workers/worker_helper.ex2
-rw-r--r--lib/pleroma/xml_builder.ex2
555 files changed, 11206 insertions, 5160 deletions
diff --git a/lib/jason_types.ex b/lib/jason_types.ex
deleted file mode 100644
index f1fdc96f4..000000000
--- a/lib/jason_types.ex
+++ /dev/null
@@ -1,9 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-Postgrex.Types.define(
- Pleroma.PostgresTypes,
- [] ++ Ecto.Adapters.Postgres.extensions(),
- json: Jason
-)
diff --git a/lib/mix/pleroma.ex b/lib/mix/pleroma.ex
index 3de11efce..2b6c7d6bb 100644
--- a/lib/mix/pleroma.ex
+++ b/lib/mix/pleroma.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Pleroma do
@@ -12,17 +12,20 @@ defmodule Mix.Pleroma do
:cachex,
:flake_id,
:swoosh,
- :timex
+ :timex,
+ :fast_html,
+ :oban
]
@cachex_children ["object", "user", "scrubber", "web_resp"]
@doc "Common functions to be reused in mix tasks"
def start_pleroma do
Pleroma.Config.Holder.save_default()
Pleroma.Config.Oban.warn()
+ Pleroma.Application.limiters_setup()
Application.put_env(:phoenix, :serve_endpoints, false, persistent: true)
- if Pleroma.Config.get(:env) != :test do
- Application.put_env(:logger, :console, level: :debug)
+ unless System.get_env("DEBUG") do
+ Logger.remove_backend(:console)
end
adapter = Application.get_env(:tesla, :adapter)
@@ -36,12 +39,23 @@ defmodule Mix.Pleroma do
Enum.each(apps, &Application.ensure_all_started/1)
+ oban_config = [
+ crontab: [],
+ repo: Pleroma.Repo,
+ log: false,
+ queues: [],
+ plugins: []
+ ]
+
children =
[
Pleroma.Repo,
+ Pleroma.Emoji,
{Pleroma.Config.TransferTask, false},
Pleroma.Web.Endpoint,
- {Oban, Pleroma.Config.get(Oban)}
+ {Oban, oban_config},
+ {Majic.Pool,
+ [name: Pleroma.MajicPool, pool_size: Pleroma.Config.get([:majic_pool, :size], 2)]}
] ++
http_children(adapter)
@@ -97,12 +111,6 @@ defmodule Mix.Pleroma do
end
end
- def shell_yes?(message) do
- if mix_shell?(),
- do: Mix.shell().yes?("Continue?"),
- else: shell_prompt(message, "Continue?") in ~w(Yn Y y)
- end
-
def shell_info(message) do
if mix_shell?(),
do: Mix.shell().info(message),
diff --git a/lib/mix/tasks/pleroma/app.ex b/lib/mix/tasks/pleroma/app.ex
index 463e2449f..0bf7ffabc 100644
--- a/lib/mix/tasks/pleroma/app.ex
+++ b/lib/mix/tasks/pleroma/app.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.App do
diff --git a/lib/mix/tasks/pleroma/benchmark.ex b/lib/mix/tasks/pleroma/benchmark.ex
index a607d5d4f..fdf99747a 100644
--- a/lib/mix/tasks/pleroma/benchmark.ex
+++ b/lib/mix/tasks/pleroma/benchmark.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Benchmark do
diff --git a/lib/mix/tasks/pleroma/config.ex b/lib/mix/tasks/pleroma/config.ex
index 18f99318d..1962154b9 100644
--- a/lib/mix/tasks/pleroma/config.ex
+++ b/lib/mix/tasks/pleroma/config.ex
@@ -1,10 +1,11 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Config do
use Mix.Task
+ import Ecto.Query
import Mix.Pleroma
alias Pleroma.ConfigDB
@@ -14,26 +15,199 @@ defmodule Mix.Tasks.Pleroma.Config do
@moduledoc File.read!("docs/administration/CLI_tasks/config.md")
def run(["migrate_to_db"]) do
- start_pleroma()
- migrate_to_db()
+ check_configdb(fn ->
+ start_pleroma()
+ migrate_to_db()
+ end)
end
def run(["migrate_from_db" | options]) do
+ check_configdb(fn ->
+ start_pleroma()
+
+ {opts, _} =
+ OptionParser.parse!(options,
+ strict: [env: :string, delete: :boolean],
+ aliases: [d: :delete]
+ )
+
+ migrate_from_db(opts)
+ end)
+ end
+
+ def run(["dump"]) do
+ check_configdb(fn ->
+ start_pleroma()
+
+ header = config_header()
+
+ settings =
+ ConfigDB
+ |> Repo.all()
+ |> Enum.sort()
+
+ unless settings == [] do
+ shell_info("#{header}")
+
+ Enum.each(settings, &dump(&1))
+ else
+ shell_error("No settings in ConfigDB.")
+ end
+ end)
+ end
+
+ def run(["dump", group, key]) do
+ check_configdb(fn ->
+ start_pleroma()
+
+ group = maybe_atomize(group)
+ key = maybe_atomize(key)
+
+ group
+ |> ConfigDB.get_by_group_and_key(key)
+ |> dump()
+ end)
+ end
+
+ def run(["dump", group]) do
+ check_configdb(fn ->
+ start_pleroma()
+
+ group = maybe_atomize(group)
+
+ dump_group(group)
+ end)
+ end
+
+ def run(["groups"]) do
+ check_configdb(fn ->
+ start_pleroma()
+
+ groups =
+ ConfigDB
+ |> distinct([c], true)
+ |> select([c], c.group)
+ |> Repo.all()
+
+ if length(groups) > 0 do
+ shell_info("The following configuration groups are set in ConfigDB:\r\n")
+ groups |> Enum.each(fn x -> shell_info("- #{x}") end)
+ shell_info("\r\n")
+ end
+ end)
+ end
+
+ def run(["reset", "--force"]) do
+ check_configdb(fn ->
+ start_pleroma()
+ truncatedb()
+ shell_info("The ConfigDB settings have been removed from the database.")
+ end)
+ end
+
+ def run(["reset"]) do
+ check_configdb(fn ->
+ start_pleroma()
+
+ shell_info("The following settings will be permanently removed:")
+
+ ConfigDB
+ |> Repo.all()
+ |> Enum.sort()
+ |> Enum.each(&dump(&1))
+
+ shell_error("\nTHIS CANNOT BE UNDONE!")
+
+ if shell_prompt("Are you sure you want to continue?", "n") in ~w(Yn Y y) do
+ truncatedb()
+
+ shell_info("The ConfigDB settings have been removed from the database.")
+ else
+ shell_error("No changes made.")
+ end
+ end)
+ end
+
+ def run(["delete", "--force", group, key]) do
+ start_pleroma()
+
+ group = maybe_atomize(group)
+ key = maybe_atomize(key)
+
+ with true <- key_exists?(group, key) do
+ shell_info("The following settings will be removed from ConfigDB:\n")
+
+ group
+ |> ConfigDB.get_by_group_and_key(key)
+ |> dump()
+
+ delete_key(group, key)
+ else
+ _ ->
+ shell_error("No settings in ConfigDB for #{inspect(group)}, #{inspect(key)}. Aborting.")
+ end
+ end
+
+ def run(["delete", "--force", group]) do
start_pleroma()
- {opts, _} =
- OptionParser.parse!(options,
- strict: [env: :string, delete: :boolean],
- aliases: [d: :delete]
- )
+ group = maybe_atomize(group)
- migrate_from_db(opts)
+ with true <- group_exists?(group) do
+ shell_info("The following settings will be removed from ConfigDB:\n")
+ dump_group(group)
+ delete_group(group)
+ else
+ _ -> shell_error("No settings in ConfigDB for #{inspect(group)}. Aborting.")
+ end
+ end
+
+ def run(["delete", group, key]) do
+ start_pleroma()
+
+ group = maybe_atomize(group)
+ key = maybe_atomize(key)
+
+ with true <- key_exists?(group, key) do
+ shell_info("The following settings will be removed from ConfigDB:\n")
+
+ group
+ |> ConfigDB.get_by_group_and_key(key)
+ |> dump()
+
+ if shell_prompt("Are you sure you want to continue?", "n") in ~w(Yn Y y) do
+ delete_key(group, key)
+ else
+ shell_error("No changes made.")
+ end
+ else
+ _ ->
+ shell_error("No settings in ConfigDB for #{inspect(group)}, #{inspect(key)}. Aborting.")
+ end
+ end
+
+ def run(["delete", group]) do
+ start_pleroma()
+
+ group = maybe_atomize(group)
+
+ with true <- group_exists?(group) do
+ shell_info("The following settings will be removed from ConfigDB:\n")
+ dump_group(group)
+
+ if shell_prompt("Are you sure you want to continue?", "n") in ~w(Yn Y y) do
+ delete_group(group)
+ else
+ shell_error("No changes made.")
+ end
+ else
+ _ -> shell_error("No settings in ConfigDB for #{inspect(group)}. Aborting.")
+ end
end
@spec migrate_to_db(Path.t() | nil) :: any()
def migrate_to_db(file_path \\ nil) do
- with true <- Pleroma.Config.get([:configurable_from_database]),
- :ok <- Pleroma.Config.DeprecationWarnings.warn() do
+ with :ok <- Pleroma.Config.DeprecationWarnings.warn() do
config_file =
if file_path do
file_path
@@ -47,16 +221,15 @@ defmodule Mix.Tasks.Pleroma.Config do
do_migrate_to_db(config_file)
else
- :error -> deprecation_error()
- _ -> migration_error()
+ _ ->
+ shell_error("Migration is not allowed until all deprecation warnings have been resolved.")
end
end
defp do_migrate_to_db(config_file) do
if File.exists?(config_file) do
shell_info("Migrating settings from file: #{Path.expand(config_file)}")
- Ecto.Adapters.SQL.query!(Repo, "TRUNCATE config;")
- Ecto.Adapters.SQL.query!(Repo, "ALTER SEQUENCE config_id_seq RESTART;")
+ truncatedb()
custom_config =
config_file
@@ -80,52 +253,38 @@ defmodule Mix.Tasks.Pleroma.Config do
shell_info("Settings for key #{key} migrated.")
end)
- shell_info("Settings for group :#{group} migrated.")
+ shell_info("Settings for group #{inspect(group)} migrated.")
end
defp migrate_from_db(opts) do
- if Pleroma.Config.get([:configurable_from_database]) do
- env = opts[:env] || Pleroma.Config.get(:env)
-
- config_path =
- if Pleroma.Config.get(:release) do
- :config_path
- |> Pleroma.Config.get()
- |> Path.dirname()
- else
- "config"
- end
- |> Path.join("#{env}.exported_from_db.secret.exs")
+ env = opts[:env] || Pleroma.Config.get(:env)
- file = File.open!(config_path, [:write, :utf8])
+ config_path =
+ if Pleroma.Config.get(:release) do
+ :config_path
+ |> Pleroma.Config.get()
+ |> Path.dirname()
+ else
+ "config"
+ end
+ |> Path.join("#{env}.exported_from_db.secret.exs")
- IO.write(file, config_header())
+ file = File.open!(config_path, [:write, :utf8])
- ConfigDB
- |> Repo.all()
- |> Enum.each(&write_and_delete(&1, file, opts[:delete]))
+ IO.write(file, config_header())
- :ok = File.close(file)
- System.cmd("mix", ["format", config_path])
+ ConfigDB
+ |> Repo.all()
+ |> Enum.each(&write_and_delete(&1, file, opts[:delete]))
- shell_info(
- "Database configuration settings have been exported to config/#{env}.exported_from_db.secret.exs"
- )
- else
- migration_error()
- end
- end
+ :ok = File.close(file)
+ System.cmd("mix", ["format", config_path])
- defp migration_error do
- shell_error(
- "Migration is not allowed in config. You can change this behavior by setting `config :pleroma, configurable_from_database: true`"
+ shell_info(
+ "Database configuration settings have been exported to config/#{env}.exported_from_db.secret.exs"
)
end
- defp deprecation_error do
- shell_error("Migration is not allowed until all deprecation warnings have been resolved.")
- end
-
if Code.ensure_loaded?(Config.Reader) do
defp config_header, do: "import Config\r\n\r\n"
defp read_file(config_file), do: Config.Reader.read_imports!(config_file)
@@ -150,8 +309,80 @@ defmodule Mix.Tasks.Pleroma.Config do
defp delete(config, true) do
{:ok, _} = Repo.delete(config)
- shell_info("#{config.key} deleted from DB.")
+
+ shell_info(
+ "config #{inspect(config.group)}, #{inspect(config.key)} was deleted from the ConfigDB."
+ )
end
defp delete(_config, _), do: :ok
+
+ defp dump(%ConfigDB{} = config) do
+ value = inspect(config.value, limit: :infinity)
+
+ shell_info("config #{inspect(config.group)}, #{inspect(config.key)}, #{value}\r\n\r\n")
+ end
+
+ defp dump(_), do: :noop
+
+ defp dump_group(group) when is_atom(group) do
+ group
+ |> ConfigDB.get_all_by_group()
+ |> Enum.each(&dump/1)
+ end
+
+ defp group_exists?(group) do
+ group
+ |> ConfigDB.get_all_by_group()
+ |> Enum.any?()
+ end
+
+ defp key_exists?(group, key) do
+ group
+ |> ConfigDB.get_by_group_and_key(key)
+ |> is_nil
+ |> Kernel.!()
+ end
+
+ defp maybe_atomize(arg) when is_atom(arg), do: arg
+
+ defp maybe_atomize(":" <> arg), do: maybe_atomize(arg)
+
+ defp maybe_atomize(arg) when is_binary(arg) do
+ if ConfigDB.module_name?(arg) do
+ String.to_existing_atom("Elixir." <> arg)
+ else
+ String.to_atom(arg)
+ end
+ end
+
+ defp check_configdb(callback) do
+ with true <- Pleroma.Config.get([:configurable_from_database]) do
+ callback.()
+ else
+ _ ->
+ shell_error(
+ "ConfigDB not enabled. Please check the value of :configurable_from_database in your configuration."
+ )
+ end
+ end
+
+ defp delete_key(group, key) do
+ check_configdb(fn ->
+ ConfigDB.delete(%{group: group, key: key})
+ end)
+ end
+
+ defp delete_group(group) do
+ check_configdb(fn ->
+ group
+ |> ConfigDB.get_all_by_group()
+ |> Enum.each(&ConfigDB.delete/1)
+ end)
+ end
+
+ defp truncatedb do
+ Ecto.Adapters.SQL.query!(Repo, "TRUNCATE config;")
+ Ecto.Adapters.SQL.query!(Repo, "ALTER SEQUENCE config_id_seq RESTART;")
+ end
end
diff --git a/lib/mix/tasks/pleroma/count_statuses.ex b/lib/mix/tasks/pleroma/count_statuses.ex
index 8761d8f17..c29ea8567 100644
--- a/lib/mix/tasks/pleroma/count_statuses.ex
+++ b/lib/mix/tasks/pleroma/count_statuses.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.CountStatuses do
diff --git a/lib/mix/tasks/pleroma/database.ex b/lib/mix/tasks/pleroma/database.ex
index a01c36ece..2403ed581 100644
--- a/lib/mix/tasks/pleroma/database.ex
+++ b/lib/mix/tasks/pleroma/database.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Database do
@@ -48,9 +48,15 @@ defmodule Mix.Tasks.Pleroma.Database do
def run(["update_users_following_followers_counts"]) do
start_pleroma()
- User
- |> Repo.all()
- |> Enum.each(&User.update_follower_count/1)
+ Repo.transaction(
+ fn ->
+ from(u in User, select: u)
+ |> Repo.stream()
+ |> Stream.each(&User.update_follower_count/1)
+ |> Stream.run()
+ end,
+ timeout: :infinity
+ )
end
def run(["prune_objects" | args]) do
@@ -161,4 +167,51 @@ defmodule Mix.Tasks.Pleroma.Database do
end)
|> Stream.run()
end
+
+ def run(["set_text_search_config", tsconfig]) do
+ start_pleroma()
+ %{rows: [[tsc]]} = Ecto.Adapters.SQL.query!(Pleroma.Repo, "SHOW default_text_search_config;")
+ shell_info("Current default_text_search_config: #{tsc}")
+
+ %{rows: [[db]]} = Ecto.Adapters.SQL.query!(Pleroma.Repo, "SELECT current_database();")
+ shell_info("Update default_text_search_config: #{tsconfig}")
+
+ %{messages: msg} =
+ Ecto.Adapters.SQL.query!(
+ Pleroma.Repo,
+ "ALTER DATABASE #{db} SET default_text_search_config = '#{tsconfig}';"
+ )
+
+ # non-exist config will not raise excpetion but only give >0 messages
+ if length(msg) > 0 do
+ shell_info("Error: #{inspect(msg, pretty: true)}")
+ else
+ rum_enabled = Pleroma.Config.get([:database, :rum_enabled])
+ shell_info("Recreate index, RUM: #{rum_enabled}")
+
+ # Note SQL below needs to be kept up-to-date with latest GIN or RUM index definition in future
+ if rum_enabled do
+ Ecto.Adapters.SQL.query!(
+ Pleroma.Repo,
+ "CREATE OR REPLACE FUNCTION objects_fts_update() RETURNS trigger AS $$ BEGIN
+ new.fts_content := to_tsvector(new.data->>'content');
+ RETURN new;
+ END
+ $$ LANGUAGE plpgsql"
+ )
+
+ shell_info("Refresh RUM index")
+ Ecto.Adapters.SQL.query!(Pleroma.Repo, "UPDATE objects SET updated_at = NOW();")
+ else
+ Ecto.Adapters.SQL.query!(Pleroma.Repo, "DROP INDEX IF EXISTS objects_fts;")
+
+ Ecto.Adapters.SQL.query!(
+ Pleroma.Repo,
+ "CREATE INDEX objects_fts ON objects USING gin(to_tsvector('#{tsconfig}', data->>'content')); "
+ )
+ end
+
+ shell_info('Done.')
+ end
+ end
end
diff --git a/lib/mix/tasks/pleroma/digest.ex b/lib/mix/tasks/pleroma/digest.ex
index cac148b88..f34fc839e 100644
--- a/lib/mix/tasks/pleroma/digest.ex
+++ b/lib/mix/tasks/pleroma/digest.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Digest do
diff --git a/lib/mix/tasks/pleroma/docs.ex b/lib/mix/tasks/pleroma/docs.ex
index ad5c37fc9..45cca1c74 100644
--- a/lib/mix/tasks/pleroma/docs.ex
+++ b/lib/mix/tasks/pleroma/docs.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Docs do
diff --git a/lib/mix/tasks/pleroma/ecto.ex b/lib/mix/tasks/pleroma/ecto.ex
index 3363cd45f..69564c61a 100644
--- a/lib/mix/tasks/pleroma/ecto.ex
+++ b/lib/mix/tasks/pleroma/ecto.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-onl
defmodule Mix.Tasks.Pleroma.Ecto do
diff --git a/lib/mix/tasks/pleroma/ecto/migrate.ex b/lib/mix/tasks/pleroma/ecto/migrate.ex
index e903bd171..8d9f44e1c 100644
--- a/lib/mix/tasks/pleroma/ecto/migrate.ex
+++ b/lib/mix/tasks/pleroma/ecto/migrate.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-onl
defmodule Mix.Tasks.Pleroma.Ecto.Migrate do
diff --git a/lib/mix/tasks/pleroma/ecto/rollback.ex b/lib/mix/tasks/pleroma/ecto/rollback.ex
index 3dba952cb..025ebaf19 100644
--- a/lib/mix/tasks/pleroma/ecto/rollback.ex
+++ b/lib/mix/tasks/pleroma/ecto/rollback.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-onl
defmodule Mix.Tasks.Pleroma.Ecto.Rollback do
@@ -20,7 +20,8 @@ defmodule Mix.Tasks.Pleroma.Ecto.Rollback do
start: :boolean,
quiet: :boolean,
log_sql: :boolean,
- migrations_path: :string
+ migrations_path: :string,
+ env: :string
]
@moduledoc """
@@ -59,7 +60,7 @@ defmodule Mix.Tasks.Pleroma.Ecto.Rollback do
level = Logger.level()
Logger.configure(level: :info)
- if Pleroma.Config.get(:env) == :test do
+ if opts[:env] == "test" do
Logger.info("Rollback succesfully")
else
{:ok, _, _} =
diff --git a/lib/mix/tasks/pleroma/email.ex b/lib/mix/tasks/pleroma/email.ex
index bc5facc09..4ce8c9b05 100644
--- a/lib/mix/tasks/pleroma/email.ex
+++ b/lib/mix/tasks/pleroma/email.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Email do
@@ -33,12 +33,12 @@ defmodule Mix.Tasks.Pleroma.Email do
Pleroma.User.Query.build(%{
local: true,
- deactivated: false,
- confirmation_pending: true,
+ is_active: true,
+ is_confirmed: false,
invisible: false
})
|> Pleroma.Repo.chunk_stream(500)
- |> Stream.each(&Pleroma.User.try_send_confirmation_email(&1))
+ |> Stream.each(&Pleroma.User.maybe_send_confirmation_email(&1))
|> Stream.run()
end
end
diff --git a/lib/mix/tasks/pleroma/emoji.ex b/lib/mix/tasks/pleroma/emoji.ex
index 1750373f9..9ad4a7467 100644
--- a/lib/mix/tasks/pleroma/emoji.ex
+++ b/lib/mix/tasks/pleroma/emoji.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Emoji do
diff --git a/lib/mix/tasks/pleroma/frontend.ex b/lib/mix/tasks/pleroma/frontend.ex
index cbce81ab9..8334e0049 100644
--- a/lib/mix/tasks/pleroma/frontend.ex
+++ b/lib/mix/tasks/pleroma/frontend.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Frontend do
@@ -17,8 +17,6 @@ defmodule Mix.Tasks.Pleroma.Frontend do
end
def run(["install", frontend | args]) do
- log_level = Logger.level()
- Logger.configure(level: :warn)
start_pleroma()
{options, [], []} =
@@ -33,109 +31,6 @@ defmodule Mix.Tasks.Pleroma.Frontend do
]
)
- instance_static_dir =
- with nil <- options[:static_dir] do
- Pleroma.Config.get!([:instance, :static_dir])
- end
-
- cmd_frontend_info = %{
- "name" => frontend,
- "ref" => options[:ref],
- "build_url" => options[:build_url],
- "build_dir" => options[:build_dir]
- }
-
- config_frontend_info = Pleroma.Config.get([:frontends, :available, frontend], %{})
-
- frontend_info =
- Map.merge(config_frontend_info, cmd_frontend_info, fn _key, config, cmd ->
- # This only overrides things that are actually set
- cmd || config
- end)
-
- ref = frontend_info["ref"]
-
- unless ref do
- raise "No ref given or configured"
- end
-
- dest =
- Path.join([
- instance_static_dir,
- "frontends",
- frontend,
- ref
- ])
-
- fe_label = "#{frontend} (#{ref})"
-
- tmp_dir = Path.join([instance_static_dir, "frontends", "tmp"])
-
- with {_, :ok} <-
- {:download_or_unzip, download_or_unzip(frontend_info, tmp_dir, options[:file])},
- shell_info("Installing #{fe_label} to #{dest}"),
- :ok <- install_frontend(frontend_info, tmp_dir, dest) do
- File.rm_rf!(tmp_dir)
- shell_info("Frontend #{fe_label} installed to #{dest}")
-
- Logger.configure(level: log_level)
- else
- {:download_or_unzip, _} ->
- shell_info("Could not download or unzip the frontend")
-
- _e ->
- shell_info("Could not install the frontend")
- end
- end
-
- defp download_or_unzip(frontend_info, temp_dir, file) do
- if file do
- with {:ok, zip} <- File.read(Path.expand(file)) do
- unzip(zip, temp_dir)
- end
- else
- download_build(frontend_info, temp_dir)
- end
- end
-
- def unzip(zip, dest) do
- with {:ok, unzipped} <- :zip.unzip(zip, [:memory]) do
- File.rm_rf!(dest)
- File.mkdir_p!(dest)
-
- Enum.each(unzipped, fn {filename, data} ->
- path = filename
-
- new_file_path = Path.join(dest, path)
-
- new_file_path
- |> Path.dirname()
- |> File.mkdir_p!()
-
- File.write!(new_file_path, data)
- end)
-
- :ok
- end
- end
-
- defp download_build(frontend_info, dest) do
- shell_info("Downloading pre-built bundle for #{frontend_info["name"]}")
- url = String.replace(frontend_info["build_url"], "${ref}", frontend_info["ref"])
-
- with {:ok, %{status: 200, body: zip_body}} <-
- Pleroma.HTTP.get(url, [], pool: :media, recv_timeout: 120_000) do
- unzip(zip_body, dest)
- else
- e -> {:error, e}
- end
- end
-
- defp install_frontend(frontend_info, source, dest) do
- from = frontend_info["build_dir"] || "dist"
- File.rm_rf!(dest)
- File.mkdir_p!(dest)
- File.cp_r!(Path.join([source, from]), dest)
- :ok
+ Pleroma.Frontend.install(frontend, options)
end
end
diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex
index ac8688424..da27a99d0 100644
--- a/lib/mix/tasks/pleroma/instance.ex
+++ b/lib/mix/tasks/pleroma/instance.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Instance do
@@ -161,12 +161,21 @@ defmodule Mix.Tasks.Pleroma.Instance do
)
|> Path.expand()
+ {strip_uploads_message, strip_uploads_default} =
+ if Pleroma.Utils.command_available?("exiftool") do
+ {"Do you want to strip location (GPS) data from uploaded images? This requires exiftool, it was detected as installed. (y/n)",
+ "y"}
+ else
+ {"Do you want to strip location (GPS) data from uploaded images? This requires exiftool, it was detected as not installed, please install it if you answer yes. (y/n)",
+ "n"}
+ end
+
strip_uploads =
get_option(
options,
:strip_uploads,
- "Do you want to strip location (GPS) data from uploaded images? (y/n)",
- "y"
+ strip_uploads_message,
+ strip_uploads_default
) === "y"
anonymize_uploads =
@@ -233,6 +242,13 @@ defmodule Mix.Tasks.Pleroma.Instance do
rum_enabled: rum_enabled
)
+ config_dir = Path.dirname(config_path)
+ psql_dir = Path.dirname(psql_path)
+
+ [config_dir, psql_dir, static_dir, uploads_dir]
+ |> Enum.reject(&File.exists?/1)
+ |> Enum.map(&File.mkdir_p!/1)
+
shell_info("Writing config to #{config_path}.")
File.write(config_path, result_config)
@@ -253,7 +269,7 @@ defmodule Mix.Tasks.Pleroma.Instance do
else
shell_error(
"The task would have overwritten the following files:\n" <>
- (Enum.map(paths, &"- #{&1}\n") |> Enum.join("")) <>
+ (Enum.map(will_overwrite, &"- #{&1}\n") |> Enum.join("")) <>
"Rerun with `--force` to overwrite them."
)
end
@@ -266,10 +282,6 @@ defmodule Mix.Tasks.Pleroma.Instance do
indexable: indexable
)
- unless File.exists?(static_dir) do
- File.mkdir_p!(static_dir)
- end
-
robots_txt_path = Path.join(static_dir, "robots.txt")
if File.exists?(robots_txt_path) do
diff --git a/lib/mix/tasks/pleroma/notification_settings.ex b/lib/mix/tasks/pleroma/notification_settings.ex
index f99275de1..e16866b6a 100644
--- a/lib/mix/tasks/pleroma/notification_settings.ex
+++ b/lib/mix/tasks/pleroma/notification_settings.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.NotificationSettings do
diff --git a/lib/mix/tasks/pleroma/openapi_spec.ex b/lib/mix/tasks/pleroma/openapi_spec.ex
new file mode 100644
index 000000000..8f719c58b
--- /dev/null
+++ b/lib/mix/tasks/pleroma/openapi_spec.ex
@@ -0,0 +1,8 @@
+defmodule Mix.Tasks.Pleroma.OpenapiSpec do
+ def run([path]) do
+ # Load Pleroma application to get version info
+ Application.load(:pleroma)
+ spec = Pleroma.Web.ApiSpec.spec(server_specific: false) |> Jason.encode!()
+ File.write(path, spec)
+ end
+end
diff --git a/lib/mix/tasks/pleroma/refresh_counter_cache.ex b/lib/mix/tasks/pleroma/refresh_counter_cache.ex
index efcbaa3b1..66eed8657 100644
--- a/lib/mix/tasks/pleroma/refresh_counter_cache.ex
+++ b/lib/mix/tasks/pleroma/refresh_counter_cache.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.RefreshCounterCache do
diff --git a/lib/mix/tasks/pleroma/relay.ex b/lib/mix/tasks/pleroma/relay.ex
index bb808ca47..01e6b4279 100644
--- a/lib/mix/tasks/pleroma/relay.ex
+++ b/lib/mix/tasks/pleroma/relay.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Relay do
diff --git a/lib/mix/tasks/pleroma/robots_txt.ex b/lib/mix/tasks/pleroma/robots_txt.ex
index 24f08180e..2ae430761 100644
--- a/lib/mix/tasks/pleroma/robots_txt.ex
+++ b/lib/mix/tasks/pleroma/robots_txt.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.RobotsTxt do
diff --git a/lib/mix/tasks/pleroma/uploads.ex b/lib/mix/tasks/pleroma/uploads.ex
index c47b7531e..333e9aa8e 100644
--- a/lib/mix/tasks/pleroma/uploads.ex
+++ b/lib/mix/tasks/pleroma/uploads.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Uploads do
diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex
index a8d251411..53d5fc6d9 100644
--- a/lib/mix/tasks/pleroma/user.ex
+++ b/lib/mix/tasks/pleroma/user.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.User do
@@ -60,7 +60,7 @@ defmodule Mix.Tasks.Pleroma.User do
- admin: #{if(admin?, do: "true", else: "false")}
""")
- proceed? = assume_yes? or shell_yes?("Continue?")
+ proceed? = assume_yes? or shell_prompt("Continue?", "n") in ~w(Yn Y y)
if proceed? do
start_pleroma()
@@ -74,7 +74,7 @@ defmodule Mix.Tasks.Pleroma.User do
bio: bio
}
- changeset = User.register_changeset(%User{}, params, need_confirmation: false)
+ changeset = User.register_changeset(%User{}, params, is_confirmed: true)
{:ok, _user} = User.register(changeset)
shell_info("User #{nickname} created")
@@ -107,21 +107,6 @@ defmodule Mix.Tasks.Pleroma.User do
end
end
- def run(["toggle_activated", nickname]) do
- start_pleroma()
-
- with %User{} = user <- User.get_cached_by_nickname(nickname) do
- {:ok, user} = User.deactivate(user, !user.deactivated)
-
- shell_info(
- "Activation status of #{nickname}: #{if(user.deactivated, do: "de", else: "")}activated"
- )
- else
- _ ->
- shell_error("No user #{nickname}")
- end
- end
-
def run(["reset_password", nickname]) do
start_pleroma()
@@ -156,20 +141,41 @@ defmodule Mix.Tasks.Pleroma.User do
end
end
+ def run(["activate", nickname]) do
+ start_pleroma()
+
+ with %User{} = user <- User.get_cached_by_nickname(nickname),
+ false <- user.is_active do
+ User.set_activation(user, true)
+ :timer.sleep(500)
+
+ shell_info("Successfully activated #{nickname}")
+ else
+ true ->
+ shell_info("User #{nickname} already activated")
+
+ _ ->
+ shell_error("No user #{nickname}")
+ end
+ end
+
def run(["deactivate", nickname]) do
start_pleroma()
- with %User{} = user <- User.get_cached_by_nickname(nickname) do
- shell_info("Deactivating #{user.nickname}")
- User.deactivate(user)
+ with %User{} = user <- User.get_cached_by_nickname(nickname),
+ true <- user.is_active do
+ User.set_activation(user, false)
:timer.sleep(500)
user = User.get_cached_by_id(user.id)
if Enum.empty?(Enum.filter(User.get_friends(user), & &1.local)) do
- shell_info("Successfully unsubscribed all local followers from #{user.nickname}")
+ shell_info("Successfully deactivated #{nickname} and unsubscribed all local followers")
end
else
+ false ->
+ shell_info("User #{nickname} already deactivated")
+
_ ->
shell_error("No user #{nickname}")
end
@@ -213,7 +219,7 @@ defmodule Mix.Tasks.Pleroma.User do
user =
case Keyword.get(options, :confirmed) do
nil -> user
- value -> set_confirmed(user, value)
+ value -> set_confirmation(user, value)
end
user =
@@ -345,13 +351,13 @@ defmodule Mix.Tasks.Pleroma.User do
end
end
- def run(["toggle_confirmed", nickname]) do
+ def run(["confirm", nickname]) do
start_pleroma()
with %User{} = user <- User.get_cached_by_nickname(nickname) do
- {:ok, user} = User.toggle_confirmation(user)
+ {:ok, user} = User.confirm(user)
- message = if user.confirmation_pending, do: "needs", else: "doesn't need"
+ message = if !user.is_confirmed, do: "needs", else: "doesn't need"
shell_info("#{nickname} #{message} confirmation.")
else
@@ -365,7 +371,7 @@ defmodule Mix.Tasks.Pleroma.User do
Pleroma.User.Query.build(%{
local: true,
- deactivated: false,
+ is_active: true,
is_moderator: false,
is_admin: false,
invisible: false
@@ -373,7 +379,7 @@ defmodule Mix.Tasks.Pleroma.User do
|> Pleroma.Repo.chunk_stream(500, :batches)
|> Stream.each(fn users ->
users
- |> Enum.each(fn user -> User.need_confirmation(user, false) end)
+ |> Enum.each(fn user -> User.set_confirmation(user, true) end)
end)
|> Stream.run()
end
@@ -383,7 +389,7 @@ defmodule Mix.Tasks.Pleroma.User do
Pleroma.User.Query.build(%{
local: true,
- deactivated: false,
+ is_active: true,
is_moderator: false,
is_admin: false,
invisible: false
@@ -391,7 +397,7 @@ defmodule Mix.Tasks.Pleroma.User do
|> Pleroma.Repo.chunk_stream(500, :batches)
|> Stream.each(fn users ->
users
- |> Enum.each(fn user -> User.need_confirmation(user, true) end)
+ |> Enum.each(fn user -> User.set_confirmation(user, false) end)
end)
|> Stream.run()
end
@@ -420,7 +426,7 @@ defmodule Mix.Tasks.Pleroma.User do
shell_info(
"#{user.nickname} moderator: #{user.is_moderator}, admin: #{user.is_admin}, locked: #{
user.is_locked
- }, deactivated: #{user.deactivated}"
+ }, is_active: #{user.is_active}"
)
end)
end)
@@ -454,10 +460,10 @@ defmodule Mix.Tasks.Pleroma.User do
user
end
- defp set_confirmed(user, value) do
- {:ok, user} = User.need_confirmation(user, !value)
+ defp set_confirmation(user, value) do
+ {:ok, user} = User.set_confirmation(user, value)
- shell_info("Confirmation pending status of #{user.nickname}: #{user.confirmation_pending}")
+ shell_info("Confirmation status of #{user.nickname}: #{user.is_confirmed}")
user
end
end
diff --git a/lib/phoenix/transports/web_socket/raw.ex b/lib/phoenix/transports/web_socket/raw.ex
index c3665bebe..8ed64eb16 100644
--- a/lib/phoenix/transports/web_socket/raw.ex
+++ b/lib/phoenix/transports/web_socket/raw.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Phoenix.Transports.WebSocket.Raw do
diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex
index 1dc777e3b..6542e684e 100644
--- a/lib/pleroma/activity.ex
+++ b/lib/pleroma/activity.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Activity do
@@ -14,6 +14,7 @@ defmodule Pleroma.Activity do
alias Pleroma.ReportNote
alias Pleroma.ThreadMute
alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ActivityPub
import Ecto.Changeset
import Ecto.Query
@@ -23,6 +24,8 @@ defmodule Pleroma.Activity do
@primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
+ @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
+
schema "activities" do
field(:data, :map)
field(:local, :boolean, default: true)
@@ -153,6 +156,18 @@ defmodule Pleroma.Activity do
def get_bookmark(_, _), do: nil
+ def get_report(activity_id) do
+ opts = %{
+ type: "Flag",
+ skip_preload: true,
+ preload_report_notes: true
+ }
+
+ ActivityPub.fetch_activities_query([], opts)
+ |> where(id: ^activity_id)
+ |> Repo.one()
+ end
+
def change(struct, params \\ %{}) do
struct
|> cast(params, [:data, :recipients])
@@ -181,6 +196,19 @@ defmodule Pleroma.Activity do
end
end
+ def get_by_id_with_user_actor(id) do
+ case FlakeId.flake_id?(id) do
+ true ->
+ Activity
+ |> where([a], a.id == ^id)
+ |> with_preloaded_user_actor()
+ |> Repo.one()
+
+ _ ->
+ nil
+ end
+ end
+
def get_by_id_with_object(id) do
Activity
|> where(id: ^id)
@@ -246,7 +274,7 @@ defmodule Pleroma.Activity do
defp get_in_reply_to_activity_from_object(_), do: nil
def get_in_reply_to_activity(%Activity{} = activity) do
- get_in_reply_to_activity_from_object(Object.normalize(activity))
+ get_in_reply_to_activity_from_object(Object.normalize(activity, fetch: false))
end
def normalize(obj) when is_map(obj), do: get_by_ap_id_with_object(obj["id"])
@@ -272,7 +300,7 @@ defmodule Pleroma.Activity do
defp purge_web_resp_cache(%Activity{} = activity) do
%{path: path} = URI.parse(activity.data["id"])
- Cachex.del(:web_resp_cache, path)
+ @cachex.del(:web_resp_cache, path)
activity
end
diff --git a/lib/pleroma/activity/ir/topics.ex b/lib/pleroma/activity/ir/topics.ex
index 9e65bedad..d94395fc1 100644
--- a/lib/pleroma/activity/ir/topics.ex
+++ b/lib/pleroma/activity/ir/topics.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Activity.Ir.Topics do
@@ -8,7 +8,7 @@ defmodule Pleroma.Activity.Ir.Topics do
def get_activity_topics(activity) do
activity
- |> Object.normalize()
+ |> Object.normalize(fetch: false)
|> generate_topics(activity)
|> List.flatten()
end
@@ -40,7 +40,8 @@ defmodule Pleroma.Activity.Ir.Topics do
end
defp item_creation_tags(tags, object, %{data: %{"type" => "Create"}} = activity) do
- tags ++ hashtags_to_topics(object) ++ attachment_topics(object, activity)
+ tags ++
+ remote_topics(activity) ++ hashtags_to_topics(object) ++ attachment_topics(object, activity)
end
defp item_creation_tags(tags, _, _) do
@@ -55,9 +56,19 @@ defmodule Pleroma.Activity.Ir.Topics do
defp hashtags_to_topics(_), do: []
+ defp remote_topics(%{local: true}), do: []
+
+ defp remote_topics(%{actor: actor}) when is_binary(actor),
+ do: ["public:remote:" <> URI.parse(actor).host]
+
+ defp remote_topics(_), do: []
+
defp attachment_topics(%{data: %{"attachment" => []}}, _act), do: []
defp attachment_topics(_object, %{local: true}), do: ["public:media", "public:local:media"]
+ defp attachment_topics(_object, %{actor: actor}) when is_binary(actor),
+ do: ["public:media", "public:remote:media:" <> URI.parse(actor).host]
+
defp attachment_topics(_object, _act), do: ["public:media"]
end
diff --git a/lib/pleroma/activity/queries.ex b/lib/pleroma/activity/queries.ex
index c99aae44b..a6b02a889 100644
--- a/lib/pleroma/activity/queries.ex
+++ b/lib/pleroma/activity/queries.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Activity.Queries do
diff --git a/lib/pleroma/activity/search.ex b/lib/pleroma/activity/search.ex
index 382c81118..ed898ba4f 100644
--- a/lib/pleroma/activity/search.ex
+++ b/lib/pleroma/activity/search.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 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
@@ -19,11 +19,18 @@ defmodule Pleroma.Activity.Search do
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
+
Activity
|> Activity.with_preloaded_object()
|> Activity.restrict_deactivated_users()
|> restrict_public()
- |> query_with(index_type, search_query)
+ |> query_with(index_type, search_query, search_function)
|> maybe_restrict_local(user)
|> maybe_restrict_author(author)
|> maybe_restrict_blocked(user)
@@ -53,22 +60,45 @@ defmodule Pleroma.Activity.Search do
)
end
- defp query_with(q, :gin, search_query) do
+ defp query_with(q, :gin, search_query, :plain) do
+ from([a, o] in q,
+ where:
+ fragment(
+ "to_tsvector(?->>'content') @@ plainto_tsquery(?)",
+ o.data,
+ ^search_query
+ )
+ )
+ end
+
+ defp query_with(q, :gin, search_query, :websearch) do
from([a, o] in q,
where:
fragment(
- "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
+ "to_tsvector(?->>'content') @@ websearch_to_tsquery(?)",
o.data,
^search_query
)
)
end
- defp query_with(q, :rum, search_query) do
+ 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
from([a, o] in q,
where:
fragment(
- "? @@ plainto_tsquery('english', ?)",
+ "? @@ websearch_to_tsquery(?)",
o.fts_content,
^search_query
),
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index 51e9dda3b..c853a2bb4 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Application do
@@ -14,7 +14,7 @@ defmodule Pleroma.Application do
@name Mix.Project.config()[:name]
@version Mix.Project.config()[:version]
@repository Mix.Project.config()[:source_url]
- @env Mix.env()
+ @mix_env Mix.env()
def name, do: @name
def version, do: @version
@@ -57,6 +57,7 @@ defmodule Pleroma.Application do
setup_instrumenters()
load_custom_modules()
Pleroma.Docs.JSON.compile()
+ limiters_setup()
adapter = Application.get_env(:tesla, :adapter)
@@ -91,25 +92,46 @@ defmodule Pleroma.Application do
Pleroma.Web.Plugs.RateLimiter.Supervisor
] ++
cachex_children() ++
- http_children(adapter, @env) ++
+ http_children(adapter, @mix_env) ++
[
Pleroma.Stats,
Pleroma.JobQueueMonitor,
{Majic.Pool, [name: Pleroma.MajicPool, pool_size: Config.get([:majic_pool, :size], 2)]},
- {Oban, Config.get(Oban)}
+ {Oban, Config.get(Oban)},
+ Pleroma.Web.Endpoint
] ++
- task_children(@env) ++
- dont_run_in_test(@env) ++
+ task_children(@mix_env) ++
+ dont_run_in_test(@mix_env) ++
chat_child(chat_enabled?()) ++
[
- Pleroma.Web.Endpoint,
Pleroma.Gopher.Server
]
# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Pleroma.Supervisor]
- Supervisor.start_link(children, opts)
+ 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)
end
def load_custom_modules do
@@ -123,7 +145,7 @@ defmodule Pleroma.Application do
raise "Invalid custom modules"
{:ok, modules, _warnings} ->
- if @env != :test do
+ if @mix_env != :test do
Enum.each(modules, fn mod ->
Logger.info("Custom module loaded: #{inspect(mod)}")
end)
@@ -168,7 +190,11 @@ defmodule Pleroma.Application do
build_cachex("web_resp", limit: 2500),
build_cachex("emoji_packs", expiration: emoji_packs_expiration(), limit: 10),
build_cachex("failed_proxy_url", limit: 2500),
- build_cachex("banned_urls", default_ttl: :timer.hours(24 * 30), limit: 5_000)
+ build_cachex("banned_urls", default_ttl: :timer.hours(24 * 30), limit: 5_000),
+ build_cachex("chat_message_id_idempotency_key",
+ expiration: chat_message_id_idempotency_key_expiration(),
+ limit: 500_000
+ )
]
end
@@ -178,6 +204,9 @@ defmodule Pleroma.Application do
defp idempotency_expiration,
do: expiration(default: :timer.seconds(6 * 60 * 60), interval: :timer.seconds(60))
+ defp chat_message_id_idempotency_key_expiration,
+ do: expiration(default: :timer.minutes(2), interval: :timer.seconds(60))
+
defp seconds_valid_interval,
do: :timer.seconds(Config.get!([Pleroma.Captcha, :seconds_valid]))
@@ -200,8 +229,7 @@ defmodule Pleroma.Application do
name: Pleroma.Web.Streamer.registry(),
keys: :duplicate,
partitions: System.schedulers_online()
- ]},
- Pleroma.Web.FedSockets.Supervisor
+ ]}
]
end
@@ -266,4 +294,19 @@ defmodule Pleroma.Application do
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]
+ |> Enum.each(fn module ->
+ mod_config = Keyword.get(config, module, [])
+
+ max_running = Keyword.get(mod_config, :max_running, 5)
+ max_waiting = Keyword.get(mod_config, :max_waiting, 5)
+
+ ConcurrentLimiter.new(module, max_running, max_waiting)
+ end)
+ end
end
diff --git a/lib/pleroma/application_requirements.ex b/lib/pleroma/application_requirements.ex
index b977257a3..6ef65b263 100644
--- a/lib/pleroma/application_requirements.ex
+++ b/lib/pleroma/application_requirements.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ApplicationRequirements do
@@ -24,6 +24,7 @@ defmodule Pleroma.ApplicationRequirements do
|> check_migrations_applied!()
|> check_welcome_message_config!()
|> check_rum!()
+ |> check_repo_pool_size!()
|> handle_result()
end
@@ -188,6 +189,30 @@ defmodule Pleroma.ApplicationRequirements do
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
+ Logger.error("""
+ !!!CONFIG WARNING!!!
+
+ The database pool size has been altered from the recommended value of 10.
+
+ Please revert or ensure your database is tuned appropriately and then set
+ `config :pleroma, :dangerzone, override_repo_pool_size: true`.
+
+ If you are experiencing database timeouts, please check the "Optimizing
+ your PostgreSQL performance" section in the documentation. If you still
+ encounter issues after that, please open an issue on the tracker.
+ """)
+
+ {:error, "Repo.pool_size different than recommended value."}
+ else
+ :ok
+ end
+ end
+
+ defp check_repo_pool_size!(result), do: result
+
defp check_filter(filter, command_required) do
filters = Config.get([Pleroma.Upload, :filters])
diff --git a/lib/pleroma/bbs/authenticator.ex b/lib/pleroma/bbs/authenticator.ex
index 83ebb756d..241fcb53c 100644
--- a/lib/pleroma/bbs/authenticator.ex
+++ b/lib/pleroma/bbs/authenticator.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.BBS.Authenticator do
diff --git a/lib/pleroma/bbs/handler.ex b/lib/pleroma/bbs/handler.ex
index cd523cf7d..4a2e255f7 100644
--- a/lib/pleroma/bbs/handler.ex
+++ b/lib/pleroma/bbs/handler.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.BBS.Handler do
diff --git a/lib/pleroma/bookmark.ex b/lib/pleroma/bookmark.ex
index e6ddbce1b..83cc8e7e1 100644
--- a/lib/pleroma/bookmark.ex
+++ b/lib/pleroma/bookmark.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Bookmark do
diff --git a/lib/pleroma/caching.ex b/lib/pleroma/caching.ex
new file mode 100644
index 000000000..02c18564d
--- /dev/null
+++ b/lib/pleroma/caching.ex
@@ -0,0 +1,19 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Caching do
+ @callback get!(Cachex.cache(), any()) :: any()
+ @callback get(Cachex.cache(), any()) :: {atom(), any()}
+ @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 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 exists?(Cachex.cache(), any()) :: {Cachex.status(), boolean()}
+ @callback execute!(Cachex.cache(), function()) :: any()
+ @callback get_and_update(Cachex.cache(), any(), function()) ::
+ {:commit | :ignore, any()}
+end
diff --git a/lib/pleroma/captcha.ex b/lib/pleroma/captcha.ex
index 6ab754b6f..bad7b3a66 100644
--- a/lib/pleroma/captcha.ex
+++ b/lib/pleroma/captcha.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Captcha do
@@ -7,6 +7,8 @@ defmodule Pleroma.Captcha do
alias Plug.Crypto.KeyGenerator
alias Plug.Crypto.MessageEncryptor
+ @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
+
@doc """
Ask the configured captcha service for a new captcha
"""
@@ -86,7 +88,7 @@ defmodule Pleroma.Captcha do
end
defp validate_usage(token) do
- if is_nil(Cachex.get!(:used_captcha_cache, token)) do
+ if is_nil(@cachex.get!(:used_captcha_cache, token)) do
:ok
else
{:error, :already_used}
@@ -95,7 +97,7 @@ defmodule Pleroma.Captcha do
defp mark_captcha_as_used(token) do
ttl = seconds_valid() |> :timer.seconds()
- Cachex.put(:used_captcha_cache, token, true, ttl: ttl)
+ @cachex.put(:used_captcha_cache, token, true, ttl: ttl)
end
defp method, do: Pleroma.Config.get!([__MODULE__, :method])
diff --git a/lib/pleroma/captcha/kocaptcha.ex b/lib/pleroma/captcha/kocaptcha.ex
index 201b55ab4..eac6dfa36 100644
--- a/lib/pleroma/captcha/kocaptcha.ex
+++ b/lib/pleroma/captcha/kocaptcha.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Captcha.Kocaptcha do
diff --git a/lib/pleroma/captcha/native.ex b/lib/pleroma/captcha/native.ex
index 8d604d2b2..2c6f64e66 100644
--- a/lib/pleroma/captcha/native.ex
+++ b/lib/pleroma/captcha/native.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Captcha.Native do
diff --git a/lib/pleroma/captcha/service.ex b/lib/pleroma/captcha/service.ex
index 959038cef..a430fafdc 100644
--- a/lib/pleroma/captcha/service.ex
+++ b/lib/pleroma/captcha/service.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Captcha.Service do
diff --git a/lib/pleroma/chat.ex b/lib/pleroma/chat.ex
index 28007cd9f..bacff24b5 100644
--- a/lib/pleroma/chat.ex
+++ b/lib/pleroma/chat.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Chat do
diff --git a/lib/pleroma/chat/message_reference.ex b/lib/pleroma/chat/message_reference.ex
index 131ae0186..89537d155 100644
--- a/lib/pleroma/chat/message_reference.ex
+++ b/lib/pleroma/chat/message_reference.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Chat.MessageReference do
diff --git a/lib/pleroma/clippy.ex b/lib/pleroma/clippy.ex
index ae96e6ad1..9c674e075 100644
--- a/lib/pleroma/clippy.ex
+++ b/lib/pleroma/clippy.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Clippy do
diff --git a/lib/pleroma/config.ex b/lib/pleroma/config.ex
index 97f877595..2e15a3719 100644
--- a/lib/pleroma/config.ex
+++ b/lib/pleroma/config.ex
@@ -1,16 +1,20 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Config do
+ @behaviour Pleroma.Config.Getting
defmodule Error do
defexception [:message]
end
+ @impl true
def get(key), do: get(key, nil)
+ @impl true
def get([key], default), do: get(key, default)
+ @impl true
def get([_ | _] = path, default) do
case fetch(path) do
{:ok, value} -> value
@@ -18,6 +22,7 @@ defmodule Pleroma.Config do
end
end
+ @impl true
def get(key, default) do
Application.get_env(:pleroma, key, default)
end
@@ -94,16 +99,4 @@ defmodule Pleroma.Config do
def oauth_consumer_strategies, do: get([:auth, :oauth_consumer_strategies], [])
def oauth_consumer_enabled?, do: oauth_consumer_strategies() != []
-
- def enforce_oauth_admin_scope_usage?, do: !!get([:auth, :enforce_oauth_admin_scope_usage])
-
- def oauth_admin_scopes(scopes) when is_list(scopes) do
- Enum.flat_map(
- scopes,
- fn scope ->
- ["admin:#{scope}"] ++
- if enforce_oauth_admin_scope_usage?(), do: [], else: [scope]
- end
- )
- end
end
diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex
index 59c6b0f58..24aa5993b 100644
--- a/lib/pleroma/config/deprecation_warnings.ex
+++ b/lib/pleroma/config/deprecation_warnings.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Config.DeprecationWarnings do
@@ -40,7 +40,8 @@ defmodule Pleroma.Config.DeprecationWarnings do
:ok <- check_welcome_message_config(),
:ok <- check_gun_pool_options(),
:ok <- check_activity_expiration_config(),
- :ok <- check_remote_ip_plug_name() do
+ :ok <- check_remote_ip_plug_name(),
+ :ok <- check_uploders_s3_public_endpoint() do
:ok
else
_ ->
@@ -193,4 +194,25 @@ defmodule Pleroma.Config.DeprecationWarnings do
warning_preface
)
end
+
+ @spec check_uploders_s3_public_endpoint() :: :ok | nil
+ def check_uploders_s3_public_endpoint do
+ s3_config = Pleroma.Config.get([Pleroma.Uploaders.S3])
+
+ use_old_config = Keyword.has_key?(s3_config, :public_endpoint)
+
+ if use_old_config do
+ Logger.error("""
+ !!!DEPRECATION WARNING!!!
+ Your config is using the old setting for controlling the URL of media uploaded to your S3 bucket.\n
+ Please make the following change at your earliest convenience.\n
+ \n* `config :pleroma, Pleroma.Uploaders.S3, public_endpoint` is now equal to:
+ \n* `config :pleroma, Pleroma.Upload, base_url`
+ """)
+
+ :error
+ else
+ :ok
+ end
+ end
end
diff --git a/lib/pleroma/config/getting.ex b/lib/pleroma/config/getting.ex
new file mode 100644
index 000000000..2cc9fe80b
--- /dev/null
+++ b/lib/pleroma/config/getting.ex
@@ -0,0 +1,8 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Config.Getting do
+ @callback get(any()) :: any()
+ @callback get(any(), any()) :: any()
+end
diff --git a/lib/pleroma/config/helpers.ex b/lib/pleroma/config/helpers.ex
index 3dce40ea0..9f26c3546 100644
--- a/lib/pleroma/config/helpers.ex
+++ b/lib/pleroma/config/helpers.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Config.Helpers do
diff --git a/lib/pleroma/config/holder.ex b/lib/pleroma/config/holder.ex
index a99fc0471..4d186a854 100644
--- a/lib/pleroma/config/holder.ex
+++ b/lib/pleroma/config/holder.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Config.Holder do
diff --git a/lib/pleroma/config/loader.ex b/lib/pleroma/config/loader.ex
index 64e7de6df..b64d06707 100644
--- a/lib/pleroma/config/loader.ex
+++ b/lib/pleroma/config/loader.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Config.Loader do
diff --git a/lib/pleroma/config/oban.ex b/lib/pleroma/config/oban.ex
index 8e0351d52..3e63bca40 100644
--- a/lib/pleroma/config/oban.ex
+++ b/lib/pleroma/config/oban.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Config.Oban do
diff --git a/lib/pleroma/config/transfer_task.ex b/lib/pleroma/config/transfer_task.ex
index a0d7b7d71..aad45aab8 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-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Config.TransferTask do
diff --git a/lib/pleroma/config_db.ex b/lib/pleroma/config_db.ex
index e5b7811aa..b874e0e37 100644
--- a/lib/pleroma/config_db.ex
+++ b/lib/pleroma/config_db.ex
@@ -1,12 +1,12 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ConfigDB do
use Ecto.Schema
import Ecto.Changeset
- import Ecto.Query, only: [select: 3]
+ import Ecto.Query, only: [select: 3, from: 2]
import Pleroma.Web.Gettext
alias __MODULE__
@@ -41,8 +41,18 @@ defmodule Pleroma.ConfigDB do
end)
end
+ @spec get_all_by_group(atom() | String.t()) :: [t()]
+ def get_all_by_group(group) do
+ from(c in ConfigDB, where: c.group == ^group) |> Repo.all()
+ end
+
+ @spec get_by_group_and_key(atom() | String.t(), atom() | String.t()) :: t() | nil
+ def get_by_group_and_key(group, key) do
+ get_by_params(%{group: group, key: key})
+ end
+
@spec get_by_params(map()) :: ConfigDB.t() | nil
- def get_by_params(params), do: Repo.get_by(ConfigDB, params)
+ def get_by_params(%{group: _, key: _} = params), do: Repo.get_by(ConfigDB, params)
@spec changeset(ConfigDB.t(), map()) :: Changeset.t()
def changeset(config, params \\ %{}) do
diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex
index 13eeaa96b..b24338cc6 100644
--- a/lib/pleroma/constants.ex
+++ b/lib/pleroma/constants.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Constants do
@@ -18,7 +18,8 @@ defmodule Pleroma.Constants do
"emoji",
"context_id",
"deleted_activity_id",
- "pleroma_internal"
+ "pleroma_internal",
+ "generator"
]
)
@@ -26,4 +27,6 @@ defmodule Pleroma.Constants do
do:
~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc embed.js embed.css)
)
+
+ def as_local_public, do: Pleroma.Web.base_url() <> "/#Public"
end
diff --git a/lib/pleroma/conversation.ex b/lib/pleroma/conversation.ex
index e76eb0087..828e27450 100644
--- a/lib/pleroma/conversation.ex
+++ b/lib/pleroma/conversation.ex
@@ -1,10 +1,11 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Conversation do
alias Pleroma.Conversation.Participation
alias Pleroma.Conversation.Participation.RecipientShip
+ alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
use Ecto.Schema
@@ -43,7 +44,7 @@ defmodule Pleroma.Conversation do
def maybe_create_recipientships(participation, activity) do
participation = Repo.preload(participation, :recipients)
- if participation.recipients |> Enum.empty?() do
+ if Enum.empty?(participation.recipients) do
recipients = User.get_all_by_ap_id(activity.recipients)
RecipientShip.create(recipients, participation)
end
@@ -58,21 +59,16 @@ defmodule Pleroma.Conversation do
def create_or_bump_for(activity, opts \\ []) do
with true <- Pleroma.Web.ActivityPub.Visibility.is_direct?(activity),
"Create" <- activity.data["type"],
- object <- Pleroma.Object.normalize(activity),
+ %Object{} = object <- Object.normalize(activity, fetch: false),
true <- object.data["type"] in ["Note", "Question"],
- ap_id when is_binary(ap_id) and byte_size(ap_id) > 0 <- object.data["context"] do
- {:ok, conversation} = create_for_ap_id(ap_id)
-
+ ap_id when is_binary(ap_id) and byte_size(ap_id) > 0 <- object.data["context"],
+ {:ok, conversation} <- create_for_ap_id(ap_id) do
users = User.get_users_from_set(activity.recipients, local_only: false)
participations =
Enum.map(users, fn user ->
invisible_conversation = Enum.any?(users, &User.blocks?(user, &1))
- unless invisible_conversation do
- User.increment_unread_conversation_count(conversation, user)
- end
-
opts = Keyword.put(opts, :invisible_conversation, invisible_conversation)
{:ok, participation} =
diff --git a/lib/pleroma/conversation/participation.ex b/lib/pleroma/conversation/participation.ex
index 8bc3e85d6..e0a3af28b 100644
--- a/lib/pleroma/conversation/participation.ex
+++ b/lib/pleroma/conversation/participation.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Conversation.Participation do
@@ -63,21 +63,10 @@ defmodule Pleroma.Conversation.Participation do
end
end
- def mark_as_read(participation) do
- __MODULE__
- |> where(id: ^participation.id)
- |> update(set: [read: true])
- |> select([p], p)
- |> Repo.update_all([])
- |> case do
- {1, [participation]} ->
- participation = Repo.preload(participation, :user)
- User.set_unread_conversation_count(participation.user)
- {:ok, participation}
-
- error ->
- error
- end
+ def mark_as_read(%__MODULE__{} = participation) do
+ participation
+ |> change(read: true)
+ |> Repo.update()
end
def mark_all_as_read(%User{local: true} = user, %User{} = target_user) do
@@ -93,7 +82,6 @@ defmodule Pleroma.Conversation.Participation do
|> update([p], set: [read: true])
|> Repo.update_all([])
- {:ok, user} = User.set_unread_conversation_count(user)
{:ok, user, []}
end
@@ -108,7 +96,6 @@ defmodule Pleroma.Conversation.Participation do
|> select([p], p)
|> Repo.update_all([])
- {:ok, user} = User.set_unread_conversation_count(user)
{:ok, user, participations}
end
@@ -220,6 +207,12 @@ defmodule Pleroma.Conversation.Participation do
{:ok, Repo.preload(participation, :recipients, force: true)}
end
+ @spec unread_count(User.t()) :: integer()
+ def unread_count(%User{id: user_id}) do
+ from(q in __MODULE__, where: q.user_id == ^user_id and q.read == false)
+ |> Repo.aggregate(:count, :id)
+ end
+
def unread_conversation_count_for_user(user) do
from(p in __MODULE__,
where: p.user_id == ^user.id,
@@ -227,4 +220,8 @@ defmodule Pleroma.Conversation.Participation do
select: %{count: count(p.id)}
)
end
+
+ def delete(%__MODULE__{} = participation) do
+ Repo.delete(participation)
+ end
end
diff --git a/lib/pleroma/conversation/participation/recipient_ship.ex b/lib/pleroma/conversation/participation/recipient_ship.ex
index de40bacac..094c1a176 100644
--- a/lib/pleroma/conversation/participation/recipient_ship.ex
+++ b/lib/pleroma/conversation/participation/recipient_ship.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Conversation.Participation.RecipientShip do
diff --git a/lib/pleroma/counter_cache.ex b/lib/pleroma/counter_cache.ex
index ebd1f603d..1e75d19ae 100644
--- a/lib/pleroma/counter_cache.ex
+++ b/lib/pleroma/counter_cache.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.CounterCache do
diff --git a/lib/pleroma/delivery.ex b/lib/pleroma/delivery.ex
index 0ded2855c..e8d536767 100644
--- a/lib/pleroma/delivery.ex
+++ b/lib/pleroma/delivery.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Delivery do
diff --git a/lib/pleroma/docs/generator.ex b/lib/pleroma/docs/generator.ex
index a70f83b73..e8a68fd41 100644
--- a/lib/pleroma/docs/generator.ex
+++ b/lib/pleroma/docs/generator.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Docs.Generator do
diff --git a/lib/pleroma/docs/json.ex b/lib/pleroma/docs/json.ex
index 13618b509..f22432ea4 100644
--- a/lib/pleroma/docs/json.ex
+++ b/lib/pleroma/docs/json.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Docs.JSON do
@@ -11,7 +11,11 @@ defmodule Pleroma.Docs.JSON do
@spec compile :: :ok
def compile do
- :persistent_term.put(@term, Pleroma.Docs.Generator.convert_to_strings(@raw_descriptions))
+ descriptions =
+ Pleroma.Web.ActivityPub.MRF.config_descriptions()
+ |> Enum.reduce(@raw_descriptions, fn description, acc -> [description | acc] end)
+
+ :persistent_term.put(@term, Pleroma.Docs.Generator.convert_to_strings(descriptions))
end
@spec compiled_descriptions :: Map.t()
diff --git a/lib/pleroma/docs/markdown.ex b/lib/pleroma/docs/markdown.ex
index eac0789a6..7e54e9d58 100644
--- a/lib/pleroma/docs/markdown.ex
+++ b/lib/pleroma/docs/markdown.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Docs.Markdown do
diff --git a/lib/pleroma/earmark_renderer.ex b/lib/pleroma/earmark_renderer.ex
index 6211a3b4a..31cae3c72 100644
--- a/lib/pleroma/earmark_renderer.ex
+++ b/lib/pleroma/earmark_renderer.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
#
# This file is derived from Earmark, under the following copyright:
diff --git a/lib/pleroma/ecto_enums.ex b/lib/pleroma/ecto_enums.ex
index 6fc47620c..f198cccb7 100644
--- a/lib/pleroma/ecto_enums.ex
+++ b/lib/pleroma/ecto_enums.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
import EctoEnum
diff --git a/lib/pleroma/ecto_type/activity_pub/object_validators/date_time.ex b/lib/pleroma/ecto_type/activity_pub/object_validators/date_time.ex
index d852c0abd..8552ae73d 100644
--- a/lib/pleroma/ecto_type/activity_pub/object_validators/date_time.ex
+++ b/lib/pleroma/ecto_type/activity_pub/object_validators/date_time.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.DateTime do
diff --git a/lib/pleroma/ecto_type/activity_pub/object_validators/emoji.ex b/lib/pleroma/ecto_type/activity_pub/object_validators/emoji.ex
index 4aacc5c88..96674e21f 100644
--- a/lib/pleroma/ecto_type/activity_pub/object_validators/emoji.ex
+++ b/lib/pleroma/ecto_type/activity_pub/object_validators/emoji.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.Emoji do
diff --git a/lib/pleroma/ecto_type/activity_pub/object_validators/object_id.ex b/lib/pleroma/ecto_type/activity_pub/object_validators/object_id.ex
index 8034235b0..45bd6070a 100644
--- a/lib/pleroma/ecto_type/activity_pub/object_validators/object_id.ex
+++ b/lib/pleroma/ecto_type/activity_pub/object_validators/object_id.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.ObjectID do
diff --git a/lib/pleroma/ecto_type/activity_pub/object_validators/recipients.ex b/lib/pleroma/ecto_type/activity_pub/object_validators/recipients.ex
index 205527a96..af4b0e527 100644
--- a/lib/pleroma/ecto_type/activity_pub/object_validators/recipients.ex
+++ b/lib/pleroma/ecto_type/activity_pub/object_validators/recipients.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.Recipients do
diff --git a/lib/pleroma/ecto_type/activity_pub/object_validators/safe_text.ex b/lib/pleroma/ecto_type/activity_pub/object_validators/safe_text.ex
index 7f0405c7b..d0f5f381f 100644
--- a/lib/pleroma/ecto_type/activity_pub/object_validators/safe_text.ex
+++ b/lib/pleroma/ecto_type/activity_pub/object_validators/safe_text.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.SafeText do
diff --git a/lib/pleroma/ecto_type/activity_pub/object_validators/uri.ex b/lib/pleroma/ecto_type/activity_pub/object_validators/uri.ex
index 2054c26be..f5b68648c 100644
--- a/lib/pleroma/ecto_type/activity_pub/object_validators/uri.ex
+++ b/lib/pleroma/ecto_type/activity_pub/object_validators/uri.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.Uri do
diff --git a/lib/pleroma/ecto_type/config/atom.ex b/lib/pleroma/ecto_type/config/atom.ex
index df565d432..3bf0bca5b 100644
--- a/lib/pleroma/ecto_type/config/atom.ex
+++ b/lib/pleroma/ecto_type/config/atom.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.EctoType.Config.Atom do
diff --git a/lib/pleroma/ecto_type/config/binary_value.ex b/lib/pleroma/ecto_type/config/binary_value.ex
index bbd2608c5..908220a65 100644
--- a/lib/pleroma/ecto_type/config/binary_value.ex
+++ b/lib/pleroma/ecto_type/config/binary_value.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.EctoType.Config.BinaryValue do
diff --git a/lib/pleroma/emails/admin_email.ex b/lib/pleroma/emails/admin_email.ex
index 423c294cb..5fe74e2f7 100644
--- a/lib/pleroma/emails/admin_email.ex
+++ b/lib/pleroma/emails/admin_email.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Emails.AdminEmail do
@@ -18,10 +18,6 @@ defmodule Pleroma.Emails.AdminEmail do
Keyword.get(instance_config(), :notify_email, instance_config()[:email])
end
- defp user_url(user) do
- Helpers.user_feed_url(Pleroma.Web.Endpoint, :feed_redirect, user.id)
- end
-
def test_email(mail_to \\ nil) do
html_body = """
<h3>Instance Test Email</h3>
@@ -72,8 +68,8 @@ defmodule Pleroma.Emails.AdminEmail do
end
html_body = """
- <p>Reported by: <a href="#{user_url(reporter)}">#{reporter.nickname}</a></p>
- <p>Reported Account: <a href="#{user_url(account)}">#{account.nickname}</a></p>
+ <p>Reported by: <a href="#{reporter.ap_id}">#{reporter.nickname}</a></p>
+ <p>Reported Account: <a href="#{account.ap_id}">#{account.nickname}</a></p>
#{comment_html}
#{statuses_html}
<p>
@@ -89,7 +85,7 @@ defmodule Pleroma.Emails.AdminEmail do
def new_unapproved_registration(to, account) do
html_body = """
- <p>New account for review: <a href="#{user_url(account)}">@#{account.nickname}</a></p>
+ <p>New account for review: <a href="#{account.ap_id}">@#{account.nickname}</a></p>
<blockquote>#{HTML.strip_tags(account.registration_reason)}</blockquote>
<a href="#{Pleroma.Web.base_url()}/pleroma/admin/#/users/#{account.id}/">Visit AdminFE</a>
"""
diff --git a/lib/pleroma/emails/mailer.ex b/lib/pleroma/emails/mailer.ex
index 5108c71c8..c68550bee 100644
--- a/lib/pleroma/emails/mailer.ex
+++ b/lib/pleroma/emails/mailer.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Emails.Mailer do
diff --git a/lib/pleroma/emails/new_users_digest_email.ex b/lib/pleroma/emails/new_users_digest_email.ex
index 348cbac9c..3552dedae 100644
--- a/lib/pleroma/emails/new_users_digest_email.ex
+++ b/lib/pleroma/emails/new_users_digest_email.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Emails.NewUsersDigestEmail do
diff --git a/lib/pleroma/emails/user_email.ex b/lib/pleroma/emails/user_email.ex
index 1d8c72ae9..52f3d419d 100644
--- a/lib/pleroma/emails/user_email.ex
+++ b/lib/pleroma/emails/user_email.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Emails.UserEmail do
@@ -81,9 +81,9 @@ defmodule Pleroma.Emails.UserEmail do
)
html_body = """
- <h3>Welcome to #{instance_name()}!</h3>
+ <h3>Thank you for registering on #{instance_name()}</h3>
<p>Email confirmation is required to activate the account.</p>
- <p>Click the following link to proceed: <a href="#{confirmation_url}">activate your account</a>.</p>
+ <p>Please click the following link to <a href="#{confirmation_url}">activate your account</a>.</p>
"""
new()
@@ -93,6 +93,33 @@ defmodule Pleroma.Emails.UserEmail do
|> html_body(html_body)
end
+ def approval_pending_email(user) do
+ html_body = """
+ <h3>Awaiting Approval</h3>
+ <p>Your account at #{instance_name()} is being reviewed by staff. You will receive another email once your account is approved.</p>
+ """
+
+ new()
+ |> to(recipient(user))
+ |> from(sender())
+ |> subject("Your account is awaiting approval")
+ |> html_body(html_body)
+ end
+
+ def successful_registration_email(user) do
+ html_body = """
+ <h3>Hello @#{user.nickname},</h3>
+ <p>Your account at #{instance_name()} has been registered successfully.</p>
+ <p>No further action is required to activate your account.</p>
+ """
+
+ new()
+ |> to(recipient(user))
+ |> from(sender())
+ |> subject("Account registered on #{instance_name()}")
+ |> html_body(html_body)
+ end
+
@doc """
Email used in digest email notifications
Includes Mentions and New Followers data
@@ -106,7 +133,7 @@ defmodule Pleroma.Emails.UserEmail do
notifications
|> Enum.filter(&(&1.activity.data["type"] == "Create"))
|> Enum.map(fn notification ->
- object = Pleroma.Object.normalize(notification.activity)
+ object = Pleroma.Object.normalize(notification.activity, fetch: false)
if not is_nil(object) do
object = update_in(object.data["content"], &format_links/1)
@@ -129,7 +156,7 @@ defmodule Pleroma.Emails.UserEmail do
if not is_nil(from) do
%{
data: notification,
- object: Pleroma.Object.normalize(notification.activity),
+ object: Pleroma.Object.normalize(notification.activity, fetch: false),
from: User.get_by_ap_id(notification.activity.actor)
}
end
@@ -151,7 +178,7 @@ defmodule Pleroma.Emails.UserEmail do
logo_path =
if is_nil(logo) do
- Path.join(:code.priv_dir(:pleroma), "static/static/logo.png")
+ Path.join(:code.priv_dir(:pleroma), "static/static/logo.svg")
else
Path.join(Config.get([:instance, :static_dir]), logo)
end
@@ -162,7 +189,7 @@ defmodule Pleroma.Emails.UserEmail do
|> subject("Your digest from #{instance_name()}")
|> put_layout(false)
|> render_body("digest.html", html_data)
- |> attachment(Swoosh.Attachment.new(logo_path, filename: "logo.png", type: :inline))
+ |> attachment(Swoosh.Attachment.new(logo_path, filename: "logo.svg", type: :inline))
end
end
@@ -189,4 +216,30 @@ defmodule Pleroma.Emails.UserEmail do
Router.Helpers.subscription_url(Endpoint, :unsubscribe, token)
end
+
+ def backup_is_ready_email(backup, admin_user_id \\ nil) do
+ %{user: user} = Pleroma.Repo.preload(backup, :user)
+ download_url = Pleroma.Web.PleromaAPI.BackupView.download_url(backup)
+
+ html_body =
+ if is_nil(admin_user_id) do
+ """
+ <p>You requested a full backup of your Pleroma account. It's ready for download:</p>
+ <p><a href="#{download_url}">#{download_url}</a></p>
+ """
+ else
+ admin = Pleroma.Repo.get(User, admin_user_id)
+
+ """
+ <p>Admin @#{admin.nickname} requested a full backup of your Pleroma account. It's ready for download:</p>
+ <p><a href="#{download_url}">#{download_url}</a></p>
+ """
+ end
+
+ new()
+ |> to(recipient(user))
+ |> from(sender())
+ |> subject("Your account archive is ready")
+ |> html_body(html_body)
+ end
end
diff --git a/lib/pleroma/emoji-data.txt b/lib/pleroma/emoji-data.txt
deleted file mode 100644
index 2fb5c3ff6..000000000
--- a/lib/pleroma/emoji-data.txt
+++ /dev/null
@@ -1,769 +0,0 @@
-# emoji-data.txt
-# Date: 2019-01-15, 12:10:05 GMT
-# © 2019 Unicode®, Inc.
-# Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries.
-# For terms of use, see http://www.unicode.org/terms_of_use.html
-#
-# Emoji Data for UTS #51
-# Version: 12.0
-#
-# For documentation and usage, see http://www.unicode.org/reports/tr51
-#
-# Format:
-# <codepoint(s)> ; <property> # <comments>
-# Note: there is no guarantee as to the structure of whitespace or comments
-#
-# Characters and sequences are listed in code point order. Users should be shown a more natural order.
-# See the CLDR collation order for Emoji.
-
-
-# ================================================
-
-# All omitted code points have Emoji=No
-# @missing: 0000..10FFFF ; Emoji ; No
-
-0023 ; Emoji # 1.1 [1] (#️) number sign
-002A ; Emoji # 1.1 [1] (*️) asterisk
-0030..0039 ; Emoji # 1.1 [10] (0️..9️) digit zero..digit nine
-00A9 ; Emoji # 1.1 [1] (©️) copyright
-00AE ; Emoji # 1.1 [1] (®️) registered
-203C ; Emoji # 1.1 [1] (‼️) double exclamation mark
-2049 ; Emoji # 3.0 [1] (⁉️) exclamation question mark
-2122 ; Emoji # 1.1 [1] (™️) trade mark
-2139 ; Emoji # 3.0 [1] (ℹ️) information
-2194..2199 ; Emoji # 1.1 [6] (↔️..↙️) left-right arrow..down-left arrow
-21A9..21AA ; Emoji # 1.1 [2] (↩️..↪️) right arrow curving left..left arrow curving right
-231A..231B ; Emoji # 1.1 [2] (⌚..⌛) watch..hourglass done
-2328 ; Emoji # 1.1 [1] (⌨️) keyboard
-23CF ; Emoji # 4.0 [1] (⏏️) eject button
-23E9..23F3 ; Emoji # 6.0 [11] (⏩..⏳) fast-forward button..hourglass not done
-23F8..23FA ; Emoji # 7.0 [3] (⏸️..⏺️) pause button..record button
-24C2 ; Emoji # 1.1 [1] (Ⓜ️) circled M
-25AA..25AB ; Emoji # 1.1 [2] (▪️..▫️) black small square..white small square
-25B6 ; Emoji # 1.1 [1] (▶️) play button
-25C0 ; Emoji # 1.1 [1] (◀️) reverse button
-25FB..25FE ; Emoji # 3.2 [4] (◻️..◾) white medium square..black medium-small square
-2600..2604 ; Emoji # 1.1 [5] (☀️..☄️) sun..comet
-260E ; Emoji # 1.1 [1] (☎️) telephone
-2611 ; Emoji # 1.1 [1] (☑️) check box with check
-2614..2615 ; Emoji # 4.0 [2] (☔..☕) umbrella with rain drops..hot beverage
-2618 ; Emoji # 4.1 [1] (☘️) shamrock
-261D ; Emoji # 1.1 [1] (☝️) index pointing up
-2620 ; Emoji # 1.1 [1] (☠️) skull and crossbones
-2622..2623 ; Emoji # 1.1 [2] (☢️..☣️) radioactive..biohazard
-2626 ; Emoji # 1.1 [1] (☦️) orthodox cross
-262A ; Emoji # 1.1 [1] (☪️) star and crescent
-262E..262F ; Emoji # 1.1 [2] (☮️..☯️) peace symbol..yin yang
-2638..263A ; Emoji # 1.1 [3] (☸️..☺️) wheel of dharma..smiling face
-2640 ; Emoji # 1.1 [1] (♀️) female sign
-2642 ; Emoji # 1.1 [1] (♂️) male sign
-2648..2653 ; Emoji # 1.1 [12] (♈..♓) Aries..Pisces
-265F..2660 ; Emoji # 1.1 [2] (♟️..♠️) chess pawn..spade suit
-2663 ; Emoji # 1.1 [1] (♣️) club suit
-2665..2666 ; Emoji # 1.1 [2] (♥️..♦️) heart suit..diamond suit
-2668 ; Emoji # 1.1 [1] (♨️) hot springs
-267B ; Emoji # 3.2 [1] (♻️) recycling symbol
-267E..267F ; Emoji # 4.1 [2] (♾️..♿) infinity..wheelchair symbol
-2692..2697 ; Emoji # 4.1 [6] (⚒️..⚗️) hammer and pick..alembic
-2699 ; Emoji # 4.1 [1] (⚙️) gear
-269B..269C ; Emoji # 4.1 [2] (⚛️..⚜️) atom symbol..fleur-de-lis
-26A0..26A1 ; Emoji # 4.0 [2] (⚠️..⚡) warning..high voltage
-26AA..26AB ; Emoji # 4.1 [2] (⚪..⚫) white circle..black circle
-26B0..26B1 ; Emoji # 4.1 [2] (⚰️..⚱️) coffin..funeral urn
-26BD..26BE ; Emoji # 5.2 [2] (⚽..⚾) soccer ball..baseball
-26C4..26C5 ; Emoji # 5.2 [2] (⛄..⛅) snowman without snow..sun behind cloud
-26C8 ; Emoji # 5.2 [1] (⛈️) cloud with lightning and rain
-26CE ; Emoji # 6.0 [1] (⛎) Ophiuchus
-26CF ; Emoji # 5.2 [1] (⛏️) pick
-26D1 ; Emoji # 5.2 [1] (⛑️) rescue worker’s helmet
-26D3..26D4 ; Emoji # 5.2 [2] (⛓️..⛔) chains..no entry
-26E9..26EA ; Emoji # 5.2 [2] (⛩️..⛪) shinto shrine..church
-26F0..26F5 ; Emoji # 5.2 [6] (⛰️..⛵) mountain..sailboat
-26F7..26FA ; Emoji # 5.2 [4] (⛷️..⛺) skier..tent
-26FD ; Emoji # 5.2 [1] (⛽) fuel pump
-2702 ; Emoji # 1.1 [1] (✂️) scissors
-2705 ; Emoji # 6.0 [1] (✅) check mark button
-2708..2709 ; Emoji # 1.1 [2] (✈️..✉️) airplane..envelope
-270A..270B ; Emoji # 6.0 [2] (✊..✋) raised fist..raised hand
-270C..270D ; Emoji # 1.1 [2] (✌️..✍️) victory hand..writing hand
-270F ; Emoji # 1.1 [1] (✏️) pencil
-2712 ; Emoji # 1.1 [1] (✒️) black nib
-2714 ; Emoji # 1.1 [1] (✔️) check mark
-2716 ; Emoji # 1.1 [1] (✖️) multiplication sign
-271D ; Emoji # 1.1 [1] (✝️) latin cross
-2721 ; Emoji # 1.1 [1] (✡️) star of David
-2728 ; Emoji # 6.0 [1] (✨) sparkles
-2733..2734 ; Emoji # 1.1 [2] (✳️..✴️) eight-spoked asterisk..eight-pointed star
-2744 ; Emoji # 1.1 [1] (❄️) snowflake
-2747 ; Emoji # 1.1 [1] (❇️) sparkle
-274C ; Emoji # 6.0 [1] (❌) cross mark
-274E ; Emoji # 6.0 [1] (❎) cross mark button
-2753..2755 ; Emoji # 6.0 [3] (❓..❕) question mark..white exclamation mark
-2757 ; Emoji # 5.2 [1] (❗) exclamation mark
-2763..2764 ; Emoji # 1.1 [2] (❣️..❤️) heart exclamation..red heart
-2795..2797 ; Emoji # 6.0 [3] (➕..➗) plus sign..division sign
-27A1 ; Emoji # 1.1 [1] (➡️) right arrow
-27B0 ; Emoji # 6.0 [1] (➰) curly loop
-27BF ; Emoji # 6.0 [1] (➿) double curly loop
-2934..2935 ; Emoji # 3.2 [2] (⤴️..⤵️) right arrow curving up..right arrow curving down
-2B05..2B07 ; Emoji # 4.0 [3] (⬅️..⬇️) left arrow..down arrow
-2B1B..2B1C ; Emoji # 5.1 [2] (⬛..⬜) black large square..white large square
-2B50 ; Emoji # 5.1 [1] (⭐) star
-2B55 ; Emoji # 5.2 [1] (⭕) hollow red circle
-3030 ; Emoji # 1.1 [1] (〰️) wavy dash
-303D ; Emoji # 3.2 [1] (〽️) part alternation mark
-3297 ; Emoji # 1.1 [1] (㊗️) Japanese “congratulations” button
-3299 ; Emoji # 1.1 [1] (㊙️) Japanese “secret” button
-1F004 ; Emoji # 5.1 [1] (🀄) mahjong red dragon
-1F0CF ; Emoji # 6.0 [1] (🃏) joker
-1F170..1F171 ; Emoji # 6.0 [2] (🅰️..🅱️) A button (blood type)..B button (blood type)
-1F17E ; Emoji # 6.0 [1] (🅾️) O button (blood type)
-1F17F ; Emoji # 5.2 [1] (🅿️) P button
-1F18E ; Emoji # 6.0 [1] (🆎) AB button (blood type)
-1F191..1F19A ; Emoji # 6.0 [10] (🆑..🆚) CL button..VS button
-1F1E6..1F1FF ; Emoji # 6.0 [26] (🇦..🇿) regional indicator symbol letter a..regional indicator symbol letter z
-1F201..1F202 ; Emoji # 6.0 [2] (🈁..🈂️) Japanese “here” button..Japanese “service charge” button
-1F21A ; Emoji # 5.2 [1] (🈚) Japanese “free of charge” button
-1F22F ; Emoji # 5.2 [1] (🈯) Japanese “reserved” button
-1F232..1F23A ; Emoji # 6.0 [9] (🈲..🈺) Japanese “prohibited” button..Japanese “open for business” button
-1F250..1F251 ; Emoji # 6.0 [2] (🉐..🉑) Japanese “bargain” button..Japanese “acceptable” button
-1F300..1F320 ; Emoji # 6.0 [33] (🌀..🌠) cyclone..shooting star
-1F321 ; Emoji # 7.0 [1] (🌡️) thermometer
-1F324..1F32C ; Emoji # 7.0 [9] (🌤️..🌬️) sun behind small cloud..wind face
-1F32D..1F32F ; Emoji # 8.0 [3] (🌭..🌯) hot dog..burrito
-1F330..1F335 ; Emoji # 6.0 [6] (🌰..🌵) chestnut..cactus
-1F336 ; Emoji # 7.0 [1] (🌶️) hot pepper
-1F337..1F37C ; Emoji # 6.0 [70] (🌷..🍼) tulip..baby bottle
-1F37D ; Emoji # 7.0 [1] (🍽️) fork and knife with plate
-1F37E..1F37F ; Emoji # 8.0 [2] (🍾..🍿) bottle with popping cork..popcorn
-1F380..1F393 ; Emoji # 6.0 [20] (🎀..🎓) ribbon..graduation cap
-1F396..1F397 ; Emoji # 7.0 [2] (🎖️..🎗️) military medal..reminder ribbon
-1F399..1F39B ; Emoji # 7.0 [3] (🎙️..🎛️) studio microphone..control knobs
-1F39E..1F39F ; Emoji # 7.0 [2] (🎞️..🎟️) film frames..admission tickets
-1F3A0..1F3C4 ; Emoji # 6.0 [37] (🎠..🏄) carousel horse..person surfing
-1F3C5 ; Emoji # 7.0 [1] (🏅) sports medal
-1F3C6..1F3CA ; Emoji # 6.0 [5] (🏆..🏊) trophy..person swimming
-1F3CB..1F3CE ; Emoji # 7.0 [4] (🏋️..🏎️) person lifting weights..racing car
-1F3CF..1F3D3 ; Emoji # 8.0 [5] (🏏..🏓) cricket game..ping pong
-1F3D4..1F3DF ; Emoji # 7.0 [12] (🏔️..🏟️) snow-capped mountain..stadium
-1F3E0..1F3F0 ; Emoji # 6.0 [17] (🏠..🏰) house..castle
-1F3F3..1F3F5 ; Emoji # 7.0 [3] (🏳️..🏵️) white flag..rosette
-1F3F7 ; Emoji # 7.0 [1] (🏷️) label
-1F3F8..1F3FF ; Emoji # 8.0 [8] (🏸..🏿) badminton..dark skin tone
-1F400..1F43E ; Emoji # 6.0 [63] (🐀..🐾) rat..paw prints
-1F43F ; Emoji # 7.0 [1] (🐿️) chipmunk
-1F440 ; Emoji # 6.0 [1] (👀) eyes
-1F441 ; Emoji # 7.0 [1] (👁️) eye
-1F442..1F4F7 ; Emoji # 6.0[182] (👂..📷) ear..camera
-1F4F8 ; Emoji # 7.0 [1] (📸) camera with flash
-1F4F9..1F4FC ; Emoji # 6.0 [4] (📹..📼) video camera..videocassette
-1F4FD ; Emoji # 7.0 [1] (📽️) film projector
-1F4FF ; Emoji # 8.0 [1] (📿) prayer beads
-1F500..1F53D ; Emoji # 6.0 [62] (🔀..🔽) shuffle tracks button..downwards button
-1F549..1F54A ; Emoji # 7.0 [2] (🕉️..🕊️) om..dove
-1F54B..1F54E ; Emoji # 8.0 [4] (🕋..🕎) kaaba..menorah
-1F550..1F567 ; Emoji # 6.0 [24] (🕐..🕧) one o’clock..twelve-thirty
-1F56F..1F570 ; Emoji # 7.0 [2] (🕯️..🕰️) candle..mantelpiece clock
-1F573..1F579 ; Emoji # 7.0 [7] (🕳️..🕹️) hole..joystick
-1F57A ; Emoji # 9.0 [1] (🕺) man dancing
-1F587 ; Emoji # 7.0 [1] (🖇️) linked paperclips
-1F58A..1F58D ; Emoji # 7.0 [4] (🖊️..🖍️) pen..crayon
-1F590 ; Emoji # 7.0 [1] (🖐️) hand with fingers splayed
-1F595..1F596 ; Emoji # 7.0 [2] (🖕..🖖) middle finger..vulcan salute
-1F5A4 ; Emoji # 9.0 [1] (🖤) black heart
-1F5A5 ; Emoji # 7.0 [1] (🖥️) desktop computer
-1F5A8 ; Emoji # 7.0 [1] (🖨️) printer
-1F5B1..1F5B2 ; Emoji # 7.0 [2] (🖱️..🖲️) computer mouse..trackball
-1F5BC ; Emoji # 7.0 [1] (🖼️) framed picture
-1F5C2..1F5C4 ; Emoji # 7.0 [3] (🗂️..🗄️) card index dividers..file cabinet
-1F5D1..1F5D3 ; Emoji # 7.0 [3] (🗑️..🗓️) wastebasket..spiral calendar
-1F5DC..1F5DE ; Emoji # 7.0 [3] (🗜️..🗞️) clamp..rolled-up newspaper
-1F5E1 ; Emoji # 7.0 [1] (🗡️) dagger
-1F5E3 ; Emoji # 7.0 [1] (🗣️) speaking head
-1F5E8 ; Emoji # 7.0 [1] (🗨️) left speech bubble
-1F5EF ; Emoji # 7.0 [1] (🗯️) right anger bubble
-1F5F3 ; Emoji # 7.0 [1] (🗳️) ballot box with ballot
-1F5FA ; Emoji # 7.0 [1] (🗺️) world map
-1F5FB..1F5FF ; Emoji # 6.0 [5] (🗻..🗿) mount fuji..moai
-1F600 ; Emoji # 6.1 [1] (😀) grinning face
-1F601..1F610 ; Emoji # 6.0 [16] (😁..😐) beaming face with smiling eyes..neutral face
-1F611 ; Emoji # 6.1 [1] (😑) expressionless face
-1F612..1F614 ; Emoji # 6.0 [3] (😒..😔) unamused face..pensive face
-1F615 ; Emoji # 6.1 [1] (😕) confused face
-1F616 ; Emoji # 6.0 [1] (😖) confounded face
-1F617 ; Emoji # 6.1 [1] (😗) kissing face
-1F618 ; Emoji # 6.0 [1] (😘) face blowing a kiss
-1F619 ; Emoji # 6.1 [1] (😙) kissing face with smiling eyes
-1F61A ; Emoji # 6.0 [1] (😚) kissing face with closed eyes
-1F61B ; Emoji # 6.1 [1] (😛) face with tongue
-1F61C..1F61E ; Emoji # 6.0 [3] (😜..😞) winking face with tongue..disappointed face
-1F61F ; Emoji # 6.1 [1] (😟) worried face
-1F620..1F625 ; Emoji # 6.0 [6] (😠..😥) angry face..sad but relieved face
-1F626..1F627 ; Emoji # 6.1 [2] (😦..😧) frowning face with open mouth..anguished face
-1F628..1F62B ; Emoji # 6.0 [4] (😨..😫) fearful face..tired face
-1F62C ; Emoji # 6.1 [1] (😬) grimacing face
-1F62D ; Emoji # 6.0 [1] (😭) loudly crying face
-1F62E..1F62F ; Emoji # 6.1 [2] (😮..😯) face with open mouth..hushed face
-1F630..1F633 ; Emoji # 6.0 [4] (😰..😳) anxious face with sweat..flushed face
-1F634 ; Emoji # 6.1 [1] (😴) sleeping face
-1F635..1F640 ; Emoji # 6.0 [12] (😵..🙀) dizzy face..weary cat
-1F641..1F642 ; Emoji # 7.0 [2] (🙁..🙂) slightly frowning face..slightly smiling face
-1F643..1F644 ; Emoji # 8.0 [2] (🙃..🙄) upside-down face..face with rolling eyes
-1F645..1F64F ; Emoji # 6.0 [11] (🙅..🙏) person gesturing NO..folded hands
-1F680..1F6C5 ; Emoji # 6.0 [70] (🚀..🛅) rocket..left luggage
-1F6CB..1F6CF ; Emoji # 7.0 [5] (🛋️..🛏️) couch and lamp..bed
-1F6D0 ; Emoji # 8.0 [1] (🛐) place of worship
-1F6D1..1F6D2 ; Emoji # 9.0 [2] (🛑..🛒) stop sign..shopping cart
-1F6D5 ; Emoji # 12.0 [1] (🛕) hindu temple
-1F6E0..1F6E5 ; Emoji # 7.0 [6] (🛠️..🛥️) hammer and wrench..motor boat
-1F6E9 ; Emoji # 7.0 [1] (🛩️) small airplane
-1F6EB..1F6EC ; Emoji # 7.0 [2] (🛫..🛬) airplane departure..airplane arrival
-1F6F0 ; Emoji # 7.0 [1] (🛰️) satellite
-1F6F3 ; Emoji # 7.0 [1] (🛳️) passenger ship
-1F6F4..1F6F6 ; Emoji # 9.0 [3] (🛴..🛶) kick scooter..canoe
-1F6F7..1F6F8 ; Emoji # 10.0 [2] (🛷..🛸) sled..flying saucer
-1F6F9 ; Emoji # 11.0 [1] (🛹) skateboard
-1F6FA ; Emoji # 12.0 [1] (🛺) auto rickshaw
-1F7E0..1F7EB ; Emoji # 12.0 [12] (🟠..🟫) orange circle..brown square
-1F90D..1F90F ; Emoji # 12.0 [3] (🤍..🤏) white heart..pinching hand
-1F910..1F918 ; Emoji # 8.0 [9] (🤐..🤘) zipper-mouth face..sign of the horns
-1F919..1F91E ; Emoji # 9.0 [6] (🤙..🤞) call me hand..crossed fingers
-1F91F ; Emoji # 10.0 [1] (🤟) love-you gesture
-1F920..1F927 ; Emoji # 9.0 [8] (🤠..🤧) cowboy hat face..sneezing face
-1F928..1F92F ; Emoji # 10.0 [8] (🤨..🤯) face with raised eyebrow..exploding head
-1F930 ; Emoji # 9.0 [1] (🤰) pregnant woman
-1F931..1F932 ; Emoji # 10.0 [2] (🤱..🤲) breast-feeding..palms up together
-1F933..1F93A ; Emoji # 9.0 [8] (🤳..🤺) selfie..person fencing
-1F93C..1F93E ; Emoji # 9.0 [3] (🤼..🤾) people wrestling..person playing handball
-1F93F ; Emoji # 12.0 [1] (🤿) diving mask
-1F940..1F945 ; Emoji # 9.0 [6] (🥀..🥅) wilted flower..goal net
-1F947..1F94B ; Emoji # 9.0 [5] (🥇..🥋) 1st place medal..martial arts uniform
-1F94C ; Emoji # 10.0 [1] (🥌) curling stone
-1F94D..1F94F ; Emoji # 11.0 [3] (🥍..🥏) lacrosse..flying disc
-1F950..1F95E ; Emoji # 9.0 [15] (🥐..🥞) croissant..pancakes
-1F95F..1F96B ; Emoji # 10.0 [13] (🥟..🥫) dumpling..canned food
-1F96C..1F970 ; Emoji # 11.0 [5] (🥬..🥰) leafy green..smiling face with hearts
-1F971 ; Emoji # 12.0 [1] (🥱) yawning face
-1F973..1F976 ; Emoji # 11.0 [4] (🥳..🥶) partying face..cold face
-1F97A ; Emoji # 11.0 [1] (🥺) pleading face
-1F97B ; Emoji # 12.0 [1] (🥻) sari
-1F97C..1F97F ; Emoji # 11.0 [4] (🥼..🥿) lab coat..flat shoe
-1F980..1F984 ; Emoji # 8.0 [5] (🦀..🦄) crab..unicorn
-1F985..1F991 ; Emoji # 9.0 [13] (🦅..🦑) eagle..squid
-1F992..1F997 ; Emoji # 10.0 [6] (🦒..🦗) giraffe..cricket
-1F998..1F9A2 ; Emoji # 11.0 [11] (🦘..🦢) kangaroo..swan
-1F9A5..1F9AA ; Emoji # 12.0 [6] (🦥..🦪) sloth..oyster
-1F9AE..1F9AF ; Emoji # 12.0 [2] (🦮..🦯) guide dog..probing cane
-1F9B0..1F9B9 ; Emoji # 11.0 [10] (🦰..🦹) red hair..supervillain
-1F9BA..1F9BF ; Emoji # 12.0 [6] (🦺..🦿) safety vest..mechanical leg
-1F9C0 ; Emoji # 8.0 [1] (🧀) cheese wedge
-1F9C1..1F9C2 ; Emoji # 11.0 [2] (🧁..🧂) cupcake..salt
-1F9C3..1F9CA ; Emoji # 12.0 [8] (🧃..🧊) beverage box..ice cube
-1F9CD..1F9CF ; Emoji # 12.0 [3] (🧍..🧏) person standing..deaf person
-1F9D0..1F9E6 ; Emoji # 10.0 [23] (🧐..🧦) face with monocle..socks
-1F9E7..1F9FF ; Emoji # 11.0 [25] (🧧..🧿) red envelope..nazar amulet
-1FA70..1FA73 ; Emoji # 12.0 [4] (🩰..🩳) ballet shoes..shorts
-1FA78..1FA7A ; Emoji # 12.0 [3] (🩸..🩺) drop of blood..stethoscope
-1FA80..1FA82 ; Emoji # 12.0 [3] (🪀..🪂) yo-yo..parachute
-1FA90..1FA95 ; Emoji # 12.0 [6] (🪐..🪕) ringed planet..banjo
-
-# Total elements: 1311
-
-# ================================================
-
-# All omitted code points have Emoji_Presentation=No
-# @missing: 0000..10FFFF ; Emoji_Presentation ; No
-
-231A..231B ; Emoji_Presentation # 1.1 [2] (⌚..⌛) watch..hourglass done
-23E9..23EC ; Emoji_Presentation # 6.0 [4] (⏩..⏬) fast-forward button..fast down button
-23F0 ; Emoji_Presentation # 6.0 [1] (⏰) alarm clock
-23F3 ; Emoji_Presentation # 6.0 [1] (⏳) hourglass not done
-25FD..25FE ; Emoji_Presentation # 3.2 [2] (◽..◾) white medium-small square..black medium-small square
-2614..2615 ; Emoji_Presentation # 4.0 [2] (☔..☕) umbrella with rain drops..hot beverage
-2648..2653 ; Emoji_Presentation # 1.1 [12] (♈..♓) Aries..Pisces
-267F ; Emoji_Presentation # 4.1 [1] (♿) wheelchair symbol
-2693 ; Emoji_Presentation # 4.1 [1] (⚓) anchor
-26A1 ; Emoji_Presentation # 4.0 [1] (⚡) high voltage
-26AA..26AB ; Emoji_Presentation # 4.1 [2] (⚪..⚫) white circle..black circle
-26BD..26BE ; Emoji_Presentation # 5.2 [2] (⚽..⚾) soccer ball..baseball
-26C4..26C5 ; Emoji_Presentation # 5.2 [2] (⛄..⛅) snowman without snow..sun behind cloud
-26CE ; Emoji_Presentation # 6.0 [1] (⛎) Ophiuchus
-26D4 ; Emoji_Presentation # 5.2 [1] (⛔) no entry
-26EA ; Emoji_Presentation # 5.2 [1] (⛪) church
-26F2..26F3 ; Emoji_Presentation # 5.2 [2] (⛲..⛳) fountain..flag in hole
-26F5 ; Emoji_Presentation # 5.2 [1] (⛵) sailboat
-26FA ; Emoji_Presentation # 5.2 [1] (⛺) tent
-26FD ; Emoji_Presentation # 5.2 [1] (⛽) fuel pump
-2705 ; Emoji_Presentation # 6.0 [1] (✅) check mark button
-270A..270B ; Emoji_Presentation # 6.0 [2] (✊..✋) raised fist..raised hand
-2728 ; Emoji_Presentation # 6.0 [1] (✨) sparkles
-274C ; Emoji_Presentation # 6.0 [1] (❌) cross mark
-274E ; Emoji_Presentation # 6.0 [1] (❎) cross mark button
-2753..2755 ; Emoji_Presentation # 6.0 [3] (❓..❕) question mark..white exclamation mark
-2757 ; Emoji_Presentation # 5.2 [1] (❗) exclamation mark
-2795..2797 ; Emoji_Presentation # 6.0 [3] (➕..➗) plus sign..division sign
-27B0 ; Emoji_Presentation # 6.0 [1] (➰) curly loop
-27BF ; Emoji_Presentation # 6.0 [1] (➿) double curly loop
-2B1B..2B1C ; Emoji_Presentation # 5.1 [2] (⬛..⬜) black large square..white large square
-2B50 ; Emoji_Presentation # 5.1 [1] (⭐) star
-2B55 ; Emoji_Presentation # 5.2 [1] (⭕) hollow red circle
-1F004 ; Emoji_Presentation # 5.1 [1] (🀄) mahjong red dragon
-1F0CF ; Emoji_Presentation # 6.0 [1] (🃏) joker
-1F18E ; Emoji_Presentation # 6.0 [1] (🆎) AB button (blood type)
-1F191..1F19A ; Emoji_Presentation # 6.0 [10] (🆑..🆚) CL button..VS button
-1F1E6..1F1FF ; Emoji_Presentation # 6.0 [26] (🇦..🇿) regional indicator symbol letter a..regional indicator symbol letter z
-1F201 ; Emoji_Presentation # 6.0 [1] (🈁) Japanese “here” button
-1F21A ; Emoji_Presentation # 5.2 [1] (🈚) Japanese “free of charge” button
-1F22F ; Emoji_Presentation # 5.2 [1] (🈯) Japanese “reserved” button
-1F232..1F236 ; Emoji_Presentation # 6.0 [5] (🈲..🈶) Japanese “prohibited” button..Japanese “not free of charge” button
-1F238..1F23A ; Emoji_Presentation # 6.0 [3] (🈸..🈺) Japanese “application” button..Japanese “open for business” button
-1F250..1F251 ; Emoji_Presentation # 6.0 [2] (🉐..🉑) Japanese “bargain” button..Japanese “acceptable” button
-1F300..1F320 ; Emoji_Presentation # 6.0 [33] (🌀..🌠) cyclone..shooting star
-1F32D..1F32F ; Emoji_Presentation # 8.0 [3] (🌭..🌯) hot dog..burrito
-1F330..1F335 ; Emoji_Presentation # 6.0 [6] (🌰..🌵) chestnut..cactus
-1F337..1F37C ; Emoji_Presentation # 6.0 [70] (🌷..🍼) tulip..baby bottle
-1F37E..1F37F ; Emoji_Presentation # 8.0 [2] (🍾..🍿) bottle with popping cork..popcorn
-1F380..1F393 ; Emoji_Presentation # 6.0 [20] (🎀..🎓) ribbon..graduation cap
-1F3A0..1F3C4 ; Emoji_Presentation # 6.0 [37] (🎠..🏄) carousel horse..person surfing
-1F3C5 ; Emoji_Presentation # 7.0 [1] (🏅) sports medal
-1F3C6..1F3CA ; Emoji_Presentation # 6.0 [5] (🏆..🏊) trophy..person swimming
-1F3CF..1F3D3 ; Emoji_Presentation # 8.0 [5] (🏏..🏓) cricket game..ping pong
-1F3E0..1F3F0 ; Emoji_Presentation # 6.0 [17] (🏠..🏰) house..castle
-1F3F4 ; Emoji_Presentation # 7.0 [1] (🏴) black flag
-1F3F8..1F3FF ; Emoji_Presentation # 8.0 [8] (🏸..🏿) badminton..dark skin tone
-1F400..1F43E ; Emoji_Presentation # 6.0 [63] (🐀..🐾) rat..paw prints
-1F440 ; Emoji_Presentation # 6.0 [1] (👀) eyes
-1F442..1F4F7 ; Emoji_Presentation # 6.0[182] (👂..📷) ear..camera
-1F4F8 ; Emoji_Presentation # 7.0 [1] (📸) camera with flash
-1F4F9..1F4FC ; Emoji_Presentation # 6.0 [4] (📹..📼) video camera..videocassette
-1F4FF ; Emoji_Presentation # 8.0 [1] (📿) prayer beads
-1F500..1F53D ; Emoji_Presentation # 6.0 [62] (🔀..🔽) shuffle tracks button..downwards button
-1F54B..1F54E ; Emoji_Presentation # 8.0 [4] (🕋..🕎) kaaba..menorah
-1F550..1F567 ; Emoji_Presentation # 6.0 [24] (🕐..🕧) one o’clock..twelve-thirty
-1F57A ; Emoji_Presentation # 9.0 [1] (🕺) man dancing
-1F595..1F596 ; Emoji_Presentation # 7.0 [2] (🖕..🖖) middle finger..vulcan salute
-1F5A4 ; Emoji_Presentation # 9.0 [1] (🖤) black heart
-1F5FB..1F5FF ; Emoji_Presentation # 6.0 [5] (🗻..🗿) mount fuji..moai
-1F600 ; Emoji_Presentation # 6.1 [1] (😀) grinning face
-1F601..1F610 ; Emoji_Presentation # 6.0 [16] (😁..😐) beaming face with smiling eyes..neutral face
-1F611 ; Emoji_Presentation # 6.1 [1] (😑) expressionless face
-1F612..1F614 ; Emoji_Presentation # 6.0 [3] (😒..😔) unamused face..pensive face
-1F615 ; Emoji_Presentation # 6.1 [1] (😕) confused face
-1F616 ; Emoji_Presentation # 6.0 [1] (😖) confounded face
-1F617 ; Emoji_Presentation # 6.1 [1] (😗) kissing face
-1F618 ; Emoji_Presentation # 6.0 [1] (😘) face blowing a kiss
-1F619 ; Emoji_Presentation # 6.1 [1] (😙) kissing face with smiling eyes
-1F61A ; Emoji_Presentation # 6.0 [1] (😚) kissing face with closed eyes
-1F61B ; Emoji_Presentation # 6.1 [1] (😛) face with tongue
-1F61C..1F61E ; Emoji_Presentation # 6.0 [3] (😜..😞) winking face with tongue..disappointed face
-1F61F ; Emoji_Presentation # 6.1 [1] (😟) worried face
-1F620..1F625 ; Emoji_Presentation # 6.0 [6] (😠..😥) angry face..sad but relieved face
-1F626..1F627 ; Emoji_Presentation # 6.1 [2] (😦..😧) frowning face with open mouth..anguished face
-1F628..1F62B ; Emoji_Presentation # 6.0 [4] (😨..😫) fearful face..tired face
-1F62C ; Emoji_Presentation # 6.1 [1] (😬) grimacing face
-1F62D ; Emoji_Presentation # 6.0 [1] (😭) loudly crying face
-1F62E..1F62F ; Emoji_Presentation # 6.1 [2] (😮..😯) face with open mouth..hushed face
-1F630..1F633 ; Emoji_Presentation # 6.0 [4] (😰..😳) anxious face with sweat..flushed face
-1F634 ; Emoji_Presentation # 6.1 [1] (😴) sleeping face
-1F635..1F640 ; Emoji_Presentation # 6.0 [12] (😵..🙀) dizzy face..weary cat
-1F641..1F642 ; Emoji_Presentation # 7.0 [2] (🙁..🙂) slightly frowning face..slightly smiling face
-1F643..1F644 ; Emoji_Presentation # 8.0 [2] (🙃..🙄) upside-down face..face with rolling eyes
-1F645..1F64F ; Emoji_Presentation # 6.0 [11] (🙅..🙏) person gesturing NO..folded hands
-1F680..1F6C5 ; Emoji_Presentation # 6.0 [70] (🚀..🛅) rocket..left luggage
-1F6CC ; Emoji_Presentation # 7.0 [1] (🛌) person in bed
-1F6D0 ; Emoji_Presentation # 8.0 [1] (🛐) place of worship
-1F6D1..1F6D2 ; Emoji_Presentation # 9.0 [2] (🛑..🛒) stop sign..shopping cart
-1F6D5 ; Emoji_Presentation # 12.0 [1] (🛕) hindu temple
-1F6EB..1F6EC ; Emoji_Presentation # 7.0 [2] (🛫..🛬) airplane departure..airplane arrival
-1F6F4..1F6F6 ; Emoji_Presentation # 9.0 [3] (🛴..🛶) kick scooter..canoe
-1F6F7..1F6F8 ; Emoji_Presentation # 10.0 [2] (🛷..🛸) sled..flying saucer
-1F6F9 ; Emoji_Presentation # 11.0 [1] (🛹) skateboard
-1F6FA ; Emoji_Presentation # 12.0 [1] (🛺) auto rickshaw
-1F7E0..1F7EB ; Emoji_Presentation # 12.0 [12] (🟠..🟫) orange circle..brown square
-1F90D..1F90F ; Emoji_Presentation # 12.0 [3] (🤍..🤏) white heart..pinching hand
-1F910..1F918 ; Emoji_Presentation # 8.0 [9] (🤐..🤘) zipper-mouth face..sign of the horns
-1F919..1F91E ; Emoji_Presentation # 9.0 [6] (🤙..🤞) call me hand..crossed fingers
-1F91F ; Emoji_Presentation # 10.0 [1] (🤟) love-you gesture
-1F920..1F927 ; Emoji_Presentation # 9.0 [8] (🤠..🤧) cowboy hat face..sneezing face
-1F928..1F92F ; Emoji_Presentation # 10.0 [8] (🤨..🤯) face with raised eyebrow..exploding head
-1F930 ; Emoji_Presentation # 9.0 [1] (🤰) pregnant woman
-1F931..1F932 ; Emoji_Presentation # 10.0 [2] (🤱..🤲) breast-feeding..palms up together
-1F933..1F93A ; Emoji_Presentation # 9.0 [8] (🤳..🤺) selfie..person fencing
-1F93C..1F93E ; Emoji_Presentation # 9.0 [3] (🤼..🤾) people wrestling..person playing handball
-1F93F ; Emoji_Presentation # 12.0 [1] (🤿) diving mask
-1F940..1F945 ; Emoji_Presentation # 9.0 [6] (🥀..🥅) wilted flower..goal net
-1F947..1F94B ; Emoji_Presentation # 9.0 [5] (🥇..🥋) 1st place medal..martial arts uniform
-1F94C ; Emoji_Presentation # 10.0 [1] (🥌) curling stone
-1F94D..1F94F ; Emoji_Presentation # 11.0 [3] (🥍..🥏) lacrosse..flying disc
-1F950..1F95E ; Emoji_Presentation # 9.0 [15] (🥐..🥞) croissant..pancakes
-1F95F..1F96B ; Emoji_Presentation # 10.0 [13] (🥟..🥫) dumpling..canned food
-1F96C..1F970 ; Emoji_Presentation # 11.0 [5] (🥬..🥰) leafy green..smiling face with hearts
-1F971 ; Emoji_Presentation # 12.0 [1] (🥱) yawning face
-1F973..1F976 ; Emoji_Presentation # 11.0 [4] (🥳..🥶) partying face..cold face
-1F97A ; Emoji_Presentation # 11.0 [1] (🥺) pleading face
-1F97B ; Emoji_Presentation # 12.0 [1] (🥻) sari
-1F97C..1F97F ; Emoji_Presentation # 11.0 [4] (🥼..🥿) lab coat..flat shoe
-1F980..1F984 ; Emoji_Presentation # 8.0 [5] (🦀..🦄) crab..unicorn
-1F985..1F991 ; Emoji_Presentation # 9.0 [13] (🦅..🦑) eagle..squid
-1F992..1F997 ; Emoji_Presentation # 10.0 [6] (🦒..🦗) giraffe..cricket
-1F998..1F9A2 ; Emoji_Presentation # 11.0 [11] (🦘..🦢) kangaroo..swan
-1F9A5..1F9AA ; Emoji_Presentation # 12.0 [6] (🦥..🦪) sloth..oyster
-1F9AE..1F9AF ; Emoji_Presentation # 12.0 [2] (🦮..🦯) guide dog..probing cane
-1F9B0..1F9B9 ; Emoji_Presentation # 11.0 [10] (🦰..🦹) red hair..supervillain
-1F9BA..1F9BF ; Emoji_Presentation # 12.0 [6] (🦺..🦿) safety vest..mechanical leg
-1F9C0 ; Emoji_Presentation # 8.0 [1] (🧀) cheese wedge
-1F9C1..1F9C2 ; Emoji_Presentation # 11.0 [2] (🧁..🧂) cupcake..salt
-1F9C3..1F9CA ; Emoji_Presentation # 12.0 [8] (🧃..🧊) beverage box..ice cube
-1F9CD..1F9CF ; Emoji_Presentation # 12.0 [3] (🧍..🧏) person standing..deaf person
-1F9D0..1F9E6 ; Emoji_Presentation # 10.0 [23] (🧐..🧦) face with monocle..socks
-1F9E7..1F9FF ; Emoji_Presentation # 11.0 [25] (🧧..🧿) red envelope..nazar amulet
-1FA70..1FA73 ; Emoji_Presentation # 12.0 [4] (🩰..🩳) ballet shoes..shorts
-1FA78..1FA7A ; Emoji_Presentation # 12.0 [3] (🩸..🩺) drop of blood..stethoscope
-1FA80..1FA82 ; Emoji_Presentation # 12.0 [3] (🪀..🪂) yo-yo..parachute
-1FA90..1FA95 ; Emoji_Presentation # 12.0 [6] (🪐..🪕) ringed planet..banjo
-
-# Total elements: 1093
-
-# ================================================
-
-# All omitted code points have Emoji_Modifier=No
-# @missing: 0000..10FFFF ; Emoji_Modifier ; No
-
-1F3FB..1F3FF ; Emoji_Modifier # 8.0 [5] (🏻..🏿) light skin tone..dark skin tone
-
-# Total elements: 5
-
-# ================================================
-
-# All omitted code points have Emoji_Modifier_Base=No
-# @missing: 0000..10FFFF ; Emoji_Modifier_Base ; No
-
-261D ; Emoji_Modifier_Base # 1.1 [1] (☝️) index pointing up
-26F9 ; Emoji_Modifier_Base # 5.2 [1] (⛹️) person bouncing ball
-270A..270B ; Emoji_Modifier_Base # 6.0 [2] (✊..✋) raised fist..raised hand
-270C..270D ; Emoji_Modifier_Base # 1.1 [2] (✌️..✍️) victory hand..writing hand
-1F385 ; Emoji_Modifier_Base # 6.0 [1] (🎅) Santa Claus
-1F3C2..1F3C4 ; Emoji_Modifier_Base # 6.0 [3] (🏂..🏄) snowboarder..person surfing
-1F3C7 ; Emoji_Modifier_Base # 6.0 [1] (🏇) horse racing
-1F3CA ; Emoji_Modifier_Base # 6.0 [1] (🏊) person swimming
-1F3CB..1F3CC ; Emoji_Modifier_Base # 7.0 [2] (🏋️..🏌️) person lifting weights..person golfing
-1F442..1F443 ; Emoji_Modifier_Base # 6.0 [2] (👂..👃) ear..nose
-1F446..1F450 ; Emoji_Modifier_Base # 6.0 [11] (👆..👐) backhand index pointing up..open hands
-1F466..1F478 ; Emoji_Modifier_Base # 6.0 [19] (👦..👸) boy..princess
-1F47C ; Emoji_Modifier_Base # 6.0 [1] (👼) baby angel
-1F481..1F483 ; Emoji_Modifier_Base # 6.0 [3] (💁..💃) person tipping hand..woman dancing
-1F485..1F487 ; Emoji_Modifier_Base # 6.0 [3] (💅..💇) nail polish..person getting haircut
-1F48F ; Emoji_Modifier_Base # 6.0 [1] (💏) kiss
-1F491 ; Emoji_Modifier_Base # 6.0 [1] (💑) couple with heart
-1F4AA ; Emoji_Modifier_Base # 6.0 [1] (💪) flexed biceps
-1F574..1F575 ; Emoji_Modifier_Base # 7.0 [2] (🕴️..🕵️) man in suit levitating..detective
-1F57A ; Emoji_Modifier_Base # 9.0 [1] (🕺) man dancing
-1F590 ; Emoji_Modifier_Base # 7.0 [1] (🖐️) hand with fingers splayed
-1F595..1F596 ; Emoji_Modifier_Base # 7.0 [2] (🖕..🖖) middle finger..vulcan salute
-1F645..1F647 ; Emoji_Modifier_Base # 6.0 [3] (🙅..🙇) person gesturing NO..person bowing
-1F64B..1F64F ; Emoji_Modifier_Base # 6.0 [5] (🙋..🙏) person raising hand..folded hands
-1F6A3 ; Emoji_Modifier_Base # 6.0 [1] (🚣) person rowing boat
-1F6B4..1F6B6 ; Emoji_Modifier_Base # 6.0 [3] (🚴..🚶) person biking..person walking
-1F6C0 ; Emoji_Modifier_Base # 6.0 [1] (🛀) person taking bath
-1F6CC ; Emoji_Modifier_Base # 7.0 [1] (🛌) person in bed
-1F90F ; Emoji_Modifier_Base # 12.0 [1] (🤏) pinching hand
-1F918 ; Emoji_Modifier_Base # 8.0 [1] (🤘) sign of the horns
-1F919..1F91E ; Emoji_Modifier_Base # 9.0 [6] (🤙..🤞) call me hand..crossed fingers
-1F91F ; Emoji_Modifier_Base # 10.0 [1] (🤟) love-you gesture
-1F926 ; Emoji_Modifier_Base # 9.0 [1] (🤦) person facepalming
-1F930 ; Emoji_Modifier_Base # 9.0 [1] (🤰) pregnant woman
-1F931..1F932 ; Emoji_Modifier_Base # 10.0 [2] (🤱..🤲) breast-feeding..palms up together
-1F933..1F939 ; Emoji_Modifier_Base # 9.0 [7] (🤳..🤹) selfie..person juggling
-1F93C..1F93E ; Emoji_Modifier_Base # 9.0 [3] (🤼..🤾) people wrestling..person playing handball
-1F9B5..1F9B6 ; Emoji_Modifier_Base # 11.0 [2] (🦵..🦶) leg..foot
-1F9B8..1F9B9 ; Emoji_Modifier_Base # 11.0 [2] (🦸..🦹) superhero..supervillain
-1F9BB ; Emoji_Modifier_Base # 12.0 [1] (🦻) ear with hearing aid
-1F9CD..1F9CF ; Emoji_Modifier_Base # 12.0 [3] (🧍..🧏) person standing..deaf person
-1F9D1..1F9DD ; Emoji_Modifier_Base # 10.0 [13] (🧑..🧝) person..elf
-
-# Total elements: 120
-
-# ================================================
-
-# All omitted code points have Emoji_Component=No
-# @missing: 0000..10FFFF ; Emoji_Component ; No
-
-0023 ; Emoji_Component # 1.1 [1] (#️) number sign
-002A ; Emoji_Component # 1.1 [1] (*️) asterisk
-0030..0039 ; Emoji_Component # 1.1 [10] (0️..9️) digit zero..digit nine
-200D ; Emoji_Component # 1.1 [1] (‍) zero width joiner
-20E3 ; Emoji_Component # 3.0 [1] (⃣) combining enclosing keycap
-FE0F ; Emoji_Component # 3.2 [1] () VARIATION SELECTOR-16
-1F1E6..1F1FF ; Emoji_Component # 6.0 [26] (🇦..🇿) regional indicator symbol letter a..regional indicator symbol letter z
-1F3FB..1F3FF ; Emoji_Component # 8.0 [5] (🏻..🏿) light skin tone..dark skin tone
-1F9B0..1F9B3 ; Emoji_Component # 11.0 [4] (🦰..🦳) red hair..white hair
-E0020..E007F ; Emoji_Component # 3.1 [96] (󠀠..󠁿) tag space..cancel tag
-
-# Total elements: 146
-
-# ================================================
-
-# All omitted code points have Extended_Pictographic=No
-# @missing: 0000..10FFFF ; Extended_Pictographic ; No
-
-00A9 ; Extended_Pictographic# 1.1 [1] (©️) copyright
-00AE ; Extended_Pictographic# 1.1 [1] (®️) registered
-203C ; Extended_Pictographic# 1.1 [1] (‼️) double exclamation mark
-2049 ; Extended_Pictographic# 3.0 [1] (⁉️) exclamation question mark
-2122 ; Extended_Pictographic# 1.1 [1] (™️) trade mark
-2139 ; Extended_Pictographic# 3.0 [1] (ℹ️) information
-2194..2199 ; Extended_Pictographic# 1.1 [6] (↔️..↙️) left-right arrow..down-left arrow
-21A9..21AA ; Extended_Pictographic# 1.1 [2] (↩️..↪️) right arrow curving left..left arrow curving right
-231A..231B ; Extended_Pictographic# 1.1 [2] (⌚..⌛) watch..hourglass done
-2328 ; Extended_Pictographic# 1.1 [1] (⌨️) keyboard
-2388 ; Extended_Pictographic# 3.0 [1] (⎈) HELM SYMBOL
-23CF ; Extended_Pictographic# 4.0 [1] (⏏️) eject button
-23E9..23F3 ; Extended_Pictographic# 6.0 [11] (⏩..⏳) fast-forward button..hourglass not done
-23F8..23FA ; Extended_Pictographic# 7.0 [3] (⏸️..⏺️) pause button..record button
-24C2 ; Extended_Pictographic# 1.1 [1] (Ⓜ️) circled M
-25AA..25AB ; Extended_Pictographic# 1.1 [2] (▪️..▫️) black small square..white small square
-25B6 ; Extended_Pictographic# 1.1 [1] (▶️) play button
-25C0 ; Extended_Pictographic# 1.1 [1] (◀️) reverse button
-25FB..25FE ; Extended_Pictographic# 3.2 [4] (◻️..◾) white medium square..black medium-small square
-2600..2605 ; Extended_Pictographic# 1.1 [6] (☀️..★) sun..BLACK STAR
-2607..2612 ; Extended_Pictographic# 1.1 [12] (☇..☒) LIGHTNING..BALLOT BOX WITH X
-2614..2615 ; Extended_Pictographic# 4.0 [2] (☔..☕) umbrella with rain drops..hot beverage
-2616..2617 ; Extended_Pictographic# 3.2 [2] (☖..☗) WHITE SHOGI PIECE..BLACK SHOGI PIECE
-2618 ; Extended_Pictographic# 4.1 [1] (☘️) shamrock
-2619 ; Extended_Pictographic# 3.0 [1] (☙) REVERSED ROTATED FLORAL HEART BULLET
-261A..266F ; Extended_Pictographic# 1.1 [86] (☚..♯) BLACK LEFT POINTING INDEX..MUSIC SHARP SIGN
-2670..2671 ; Extended_Pictographic# 3.0 [2] (♰..♱) WEST SYRIAC CROSS..EAST SYRIAC CROSS
-2672..267D ; Extended_Pictographic# 3.2 [12] (♲..♽) UNIVERSAL RECYCLING SYMBOL..PARTIALLY-RECYCLED PAPER SYMBOL
-267E..267F ; Extended_Pictographic# 4.1 [2] (♾️..♿) infinity..wheelchair symbol
-2680..2685 ; Extended_Pictographic# 3.2 [6] (⚀..⚅) DIE FACE-1..DIE FACE-6
-2690..2691 ; Extended_Pictographic# 4.0 [2] (⚐..⚑) WHITE FLAG..BLACK FLAG
-2692..269C ; Extended_Pictographic# 4.1 [11] (⚒️..⚜️) hammer and pick..fleur-de-lis
-269D ; Extended_Pictographic# 5.1 [1] (⚝) OUTLINED WHITE STAR
-269E..269F ; Extended_Pictographic# 5.2 [2] (⚞..⚟) THREE LINES CONVERGING RIGHT..THREE LINES CONVERGING LEFT
-26A0..26A1 ; Extended_Pictographic# 4.0 [2] (⚠️..⚡) warning..high voltage
-26A2..26B1 ; Extended_Pictographic# 4.1 [16] (⚢..⚱️) DOUBLED FEMALE SIGN..funeral urn
-26B2 ; Extended_Pictographic# 5.0 [1] (⚲) NEUTER
-26B3..26BC ; Extended_Pictographic# 5.1 [10] (⚳..⚼) CERES..SESQUIQUADRATE
-26BD..26BF ; Extended_Pictographic# 5.2 [3] (⚽..⚿) soccer ball..SQUARED KEY
-26C0..26C3 ; Extended_Pictographic# 5.1 [4] (⛀..⛃) WHITE DRAUGHTS MAN..BLACK DRAUGHTS KING
-26C4..26CD ; Extended_Pictographic# 5.2 [10] (⛄..⛍) snowman without snow..DISABLED CAR
-26CE ; Extended_Pictographic# 6.0 [1] (⛎) Ophiuchus
-26CF..26E1 ; Extended_Pictographic# 5.2 [19] (⛏️..⛡) pick..RESTRICTED LEFT ENTRY-2
-26E2 ; Extended_Pictographic# 6.0 [1] (⛢) ASTRONOMICAL SYMBOL FOR URANUS
-26E3 ; Extended_Pictographic# 5.2 [1] (⛣) HEAVY CIRCLE WITH STROKE AND TWO DOTS ABOVE
-26E4..26E7 ; Extended_Pictographic# 6.0 [4] (⛤..⛧) PENTAGRAM..INVERTED PENTAGRAM
-26E8..26FF ; Extended_Pictographic# 5.2 [24] (⛨..⛿) BLACK CROSS ON SHIELD..WHITE FLAG WITH HORIZONTAL MIDDLE BLACK STRIPE
-2700 ; Extended_Pictographic# 7.0 [1] (✀) BLACK SAFETY SCISSORS
-2701..2704 ; Extended_Pictographic# 1.1 [4] (✁..✄) UPPER BLADE SCISSORS..WHITE SCISSORS
-2705 ; Extended_Pictographic# 6.0 [1] (✅) check mark button
-2708..2709 ; Extended_Pictographic# 1.1 [2] (✈️..✉️) airplane..envelope
-270A..270B ; Extended_Pictographic# 6.0 [2] (✊..✋) raised fist..raised hand
-270C..2712 ; Extended_Pictographic# 1.1 [7] (✌️..✒️) victory hand..black nib
-2714 ; Extended_Pictographic# 1.1 [1] (✔️) check mark
-2716 ; Extended_Pictographic# 1.1 [1] (✖️) multiplication sign
-271D ; Extended_Pictographic# 1.1 [1] (✝️) latin cross
-2721 ; Extended_Pictographic# 1.1 [1] (✡️) star of David
-2728 ; Extended_Pictographic# 6.0 [1] (✨) sparkles
-2733..2734 ; Extended_Pictographic# 1.1 [2] (✳️..✴️) eight-spoked asterisk..eight-pointed star
-2744 ; Extended_Pictographic# 1.1 [1] (❄️) snowflake
-2747 ; Extended_Pictographic# 1.1 [1] (❇️) sparkle
-274C ; Extended_Pictographic# 6.0 [1] (❌) cross mark
-274E ; Extended_Pictographic# 6.0 [1] (❎) cross mark button
-2753..2755 ; Extended_Pictographic# 6.0 [3] (❓..❕) question mark..white exclamation mark
-2757 ; Extended_Pictographic# 5.2 [1] (❗) exclamation mark
-2763..2767 ; Extended_Pictographic# 1.1 [5] (❣️..❧) heart exclamation..ROTATED FLORAL HEART BULLET
-2795..2797 ; Extended_Pictographic# 6.0 [3] (➕..➗) plus sign..division sign
-27A1 ; Extended_Pictographic# 1.1 [1] (➡️) right arrow
-27B0 ; Extended_Pictographic# 6.0 [1] (➰) curly loop
-27BF ; Extended_Pictographic# 6.0 [1] (➿) double curly loop
-2934..2935 ; Extended_Pictographic# 3.2 [2] (⤴️..⤵️) right arrow curving up..right arrow curving down
-2B05..2B07 ; Extended_Pictographic# 4.0 [3] (⬅️..⬇️) left arrow..down arrow
-2B1B..2B1C ; Extended_Pictographic# 5.1 [2] (⬛..⬜) black large square..white large square
-2B50 ; Extended_Pictographic# 5.1 [1] (⭐) star
-2B55 ; Extended_Pictographic# 5.2 [1] (⭕) hollow red circle
-3030 ; Extended_Pictographic# 1.1 [1] (〰️) wavy dash
-303D ; Extended_Pictographic# 3.2 [1] (〽️) part alternation mark
-3297 ; Extended_Pictographic# 1.1 [1] (㊗️) Japanese “congratulations” button
-3299 ; Extended_Pictographic# 1.1 [1] (㊙️) Japanese “secret” button
-1F000..1F02B ; Extended_Pictographic# 5.1 [44] (🀀..🀫) MAHJONG TILE EAST WIND..MAHJONG TILE BACK
-1F02C..1F02F ; Extended_Pictographic# NA [4] (🀬..🀯) <reserved-1F02C>..<reserved-1F02F>
-1F030..1F093 ; Extended_Pictographic# 5.1[100] (🀰..🂓) DOMINO TILE HORIZONTAL BACK..DOMINO TILE VERTICAL-06-06
-1F094..1F09F ; Extended_Pictographic# NA [12] (🂔..🂟) <reserved-1F094>..<reserved-1F09F>
-1F0A0..1F0AE ; Extended_Pictographic# 6.0 [15] (🂠..🂮) PLAYING CARD BACK..PLAYING CARD KING OF SPADES
-1F0AF..1F0B0 ; Extended_Pictographic# NA [2] (🂯..🂰) <reserved-1F0AF>..<reserved-1F0B0>
-1F0B1..1F0BE ; Extended_Pictographic# 6.0 [14] (🂱..🂾) PLAYING CARD ACE OF HEARTS..PLAYING CARD KING OF HEARTS
-1F0BF ; Extended_Pictographic# 7.0 [1] (🂿) PLAYING CARD RED JOKER
-1F0C0 ; Extended_Pictographic# NA [1] (🃀) <reserved-1F0C0>
-1F0C1..1F0CF ; Extended_Pictographic# 6.0 [15] (🃁..🃏) PLAYING CARD ACE OF DIAMONDS..joker
-1F0D0 ; Extended_Pictographic# NA [1] (🃐) <reserved-1F0D0>
-1F0D1..1F0DF ; Extended_Pictographic# 6.0 [15] (🃑..🃟) PLAYING CARD ACE OF CLUBS..PLAYING CARD WHITE JOKER
-1F0E0..1F0F5 ; Extended_Pictographic# 7.0 [22] (🃠..🃵) PLAYING CARD FOOL..PLAYING CARD TRUMP-21
-1F0F6..1F0FF ; Extended_Pictographic# NA [10] (🃶..🃿) <reserved-1F0F6>..<reserved-1F0FF>
-1F10D..1F10F ; Extended_Pictographic# NA [3] (🄍..🄏) <reserved-1F10D>..<reserved-1F10F>
-1F12F ; Extended_Pictographic# 11.0 [1] (🄯) COPYLEFT SYMBOL
-1F16C ; Extended_Pictographic# 12.0 [1] (🅬) RAISED MR SIGN
-1F16D..1F16F ; Extended_Pictographic# NA [3] (🅭..🅯) <reserved-1F16D>..<reserved-1F16F>
-1F170..1F171 ; Extended_Pictographic# 6.0 [2] (🅰️..🅱️) A button (blood type)..B button (blood type)
-1F17E ; Extended_Pictographic# 6.0 [1] (🅾️) O button (blood type)
-1F17F ; Extended_Pictographic# 5.2 [1] (🅿️) P button
-1F18E ; Extended_Pictographic# 6.0 [1] (🆎) AB button (blood type)
-1F191..1F19A ; Extended_Pictographic# 6.0 [10] (🆑..🆚) CL button..VS button
-1F1AD..1F1E5 ; Extended_Pictographic# NA [57] (🆭..🇥) <reserved-1F1AD>..<reserved-1F1E5>
-1F201..1F202 ; Extended_Pictographic# 6.0 [2] (🈁..🈂️) Japanese “here” button..Japanese “service charge” button
-1F203..1F20F ; Extended_Pictographic# NA [13] (🈃..🈏) <reserved-1F203>..<reserved-1F20F>
-1F21A ; Extended_Pictographic# 5.2 [1] (🈚) Japanese “free of charge” button
-1F22F ; Extended_Pictographic# 5.2 [1] (🈯) Japanese “reserved” button
-1F232..1F23A ; Extended_Pictographic# 6.0 [9] (🈲..🈺) Japanese “prohibited” button..Japanese “open for business” button
-1F23C..1F23F ; Extended_Pictographic# NA [4] (🈼..🈿) <reserved-1F23C>..<reserved-1F23F>
-1F249..1F24F ; Extended_Pictographic# NA [7] (🉉..🉏) <reserved-1F249>..<reserved-1F24F>
-1F250..1F251 ; Extended_Pictographic# 6.0 [2] (🉐..🉑) Japanese “bargain” button..Japanese “acceptable” button
-1F252..1F25F ; Extended_Pictographic# NA [14] (🉒..🉟) <reserved-1F252>..<reserved-1F25F>
-1F260..1F265 ; Extended_Pictographic# 10.0 [6] (🉠..🉥) ROUNDED SYMBOL FOR FU..ROUNDED SYMBOL FOR CAI
-1F266..1F2FF ; Extended_Pictographic# NA[154] (🉦..🋿) <reserved-1F266>..<reserved-1F2FF>
-1F300..1F320 ; Extended_Pictographic# 6.0 [33] (🌀..🌠) cyclone..shooting star
-1F321..1F32C ; Extended_Pictographic# 7.0 [12] (🌡️..🌬️) thermometer..wind face
-1F32D..1F32F ; Extended_Pictographic# 8.0 [3] (🌭..🌯) hot dog..burrito
-1F330..1F335 ; Extended_Pictographic# 6.0 [6] (🌰..🌵) chestnut..cactus
-1F336 ; Extended_Pictographic# 7.0 [1] (🌶️) hot pepper
-1F337..1F37C ; Extended_Pictographic# 6.0 [70] (🌷..🍼) tulip..baby bottle
-1F37D ; Extended_Pictographic# 7.0 [1] (🍽️) fork and knife with plate
-1F37E..1F37F ; Extended_Pictographic# 8.0 [2] (🍾..🍿) bottle with popping cork..popcorn
-1F380..1F393 ; Extended_Pictographic# 6.0 [20] (🎀..🎓) ribbon..graduation cap
-1F394..1F39F ; Extended_Pictographic# 7.0 [12] (🎔..🎟️) HEART WITH TIP ON THE LEFT..admission tickets
-1F3A0..1F3C4 ; Extended_Pictographic# 6.0 [37] (🎠..🏄) carousel horse..person surfing
-1F3C5 ; Extended_Pictographic# 7.0 [1] (🏅) sports medal
-1F3C6..1F3CA ; Extended_Pictographic# 6.0 [5] (🏆..🏊) trophy..person swimming
-1F3CB..1F3CE ; Extended_Pictographic# 7.0 [4] (🏋️..🏎️) person lifting weights..racing car
-1F3CF..1F3D3 ; Extended_Pictographic# 8.0 [5] (🏏..🏓) cricket game..ping pong
-1F3D4..1F3DF ; Extended_Pictographic# 7.0 [12] (🏔️..🏟️) snow-capped mountain..stadium
-1F3E0..1F3F0 ; Extended_Pictographic# 6.0 [17] (🏠..🏰) house..castle
-1F3F1..1F3F7 ; Extended_Pictographic# 7.0 [7] (🏱..🏷️) WHITE PENNANT..label
-1F3F8..1F3FA ; Extended_Pictographic# 8.0 [3] (🏸..🏺) badminton..amphora
-1F400..1F43E ; Extended_Pictographic# 6.0 [63] (🐀..🐾) rat..paw prints
-1F43F ; Extended_Pictographic# 7.0 [1] (🐿️) chipmunk
-1F440 ; Extended_Pictographic# 6.0 [1] (👀) eyes
-1F441 ; Extended_Pictographic# 7.0 [1] (👁️) eye
-1F442..1F4F7 ; Extended_Pictographic# 6.0[182] (👂..📷) ear..camera
-1F4F8 ; Extended_Pictographic# 7.0 [1] (📸) camera with flash
-1F4F9..1F4FC ; Extended_Pictographic# 6.0 [4] (📹..📼) video camera..videocassette
-1F4FD..1F4FE ; Extended_Pictographic# 7.0 [2] (📽️..📾) film projector..PORTABLE STEREO
-1F4FF ; Extended_Pictographic# 8.0 [1] (📿) prayer beads
-1F500..1F53D ; Extended_Pictographic# 6.0 [62] (🔀..🔽) shuffle tracks button..downwards button
-1F546..1F54A ; Extended_Pictographic# 7.0 [5] (🕆..🕊️) WHITE LATIN CROSS..dove
-1F54B..1F54F ; Extended_Pictographic# 8.0 [5] (🕋..🕏) kaaba..BOWL OF HYGIEIA
-1F550..1F567 ; Extended_Pictographic# 6.0 [24] (🕐..🕧) one o’clock..twelve-thirty
-1F568..1F579 ; Extended_Pictographic# 7.0 [18] (🕨..🕹️) RIGHT SPEAKER..joystick
-1F57A ; Extended_Pictographic# 9.0 [1] (🕺) man dancing
-1F57B..1F5A3 ; Extended_Pictographic# 7.0 [41] (🕻..🖣) LEFT HAND TELEPHONE RECEIVER..BLACK DOWN POINTING BACKHAND INDEX
-1F5A4 ; Extended_Pictographic# 9.0 [1] (🖤) black heart
-1F5A5..1F5FA ; Extended_Pictographic# 7.0 [86] (🖥️..🗺️) desktop computer..world map
-1F5FB..1F5FF ; Extended_Pictographic# 6.0 [5] (🗻..🗿) mount fuji..moai
-1F600 ; Extended_Pictographic# 6.1 [1] (😀) grinning face
-1F601..1F610 ; Extended_Pictographic# 6.0 [16] (😁..😐) beaming face with smiling eyes..neutral face
-1F611 ; Extended_Pictographic# 6.1 [1] (😑) expressionless face
-1F612..1F614 ; Extended_Pictographic# 6.0 [3] (😒..😔) unamused face..pensive face
-1F615 ; Extended_Pictographic# 6.1 [1] (😕) confused face
-1F616 ; Extended_Pictographic# 6.0 [1] (😖) confounded face
-1F617 ; Extended_Pictographic# 6.1 [1] (😗) kissing face
-1F618 ; Extended_Pictographic# 6.0 [1] (😘) face blowing a kiss
-1F619 ; Extended_Pictographic# 6.1 [1] (😙) kissing face with smiling eyes
-1F61A ; Extended_Pictographic# 6.0 [1] (😚) kissing face with closed eyes
-1F61B ; Extended_Pictographic# 6.1 [1] (😛) face with tongue
-1F61C..1F61E ; Extended_Pictographic# 6.0 [3] (😜..😞) winking face with tongue..disappointed face
-1F61F ; Extended_Pictographic# 6.1 [1] (😟) worried face
-1F620..1F625 ; Extended_Pictographic# 6.0 [6] (😠..😥) angry face..sad but relieved face
-1F626..1F627 ; Extended_Pictographic# 6.1 [2] (😦..😧) frowning face with open mouth..anguished face
-1F628..1F62B ; Extended_Pictographic# 6.0 [4] (😨..😫) fearful face..tired face
-1F62C ; Extended_Pictographic# 6.1 [1] (😬) grimacing face
-1F62D ; Extended_Pictographic# 6.0 [1] (😭) loudly crying face
-1F62E..1F62F ; Extended_Pictographic# 6.1 [2] (😮..😯) face with open mouth..hushed face
-1F630..1F633 ; Extended_Pictographic# 6.0 [4] (😰..😳) anxious face with sweat..flushed face
-1F634 ; Extended_Pictographic# 6.1 [1] (😴) sleeping face
-1F635..1F640 ; Extended_Pictographic# 6.0 [12] (😵..🙀) dizzy face..weary cat
-1F641..1F642 ; Extended_Pictographic# 7.0 [2] (🙁..🙂) slightly frowning face..slightly smiling face
-1F643..1F644 ; Extended_Pictographic# 8.0 [2] (🙃..🙄) upside-down face..face with rolling eyes
-1F645..1F64F ; Extended_Pictographic# 6.0 [11] (🙅..🙏) person gesturing NO..folded hands
-1F680..1F6C5 ; Extended_Pictographic# 6.0 [70] (🚀..🛅) rocket..left luggage
-1F6C6..1F6CF ; Extended_Pictographic# 7.0 [10] (🛆..🛏️) TRIANGLE WITH ROUNDED CORNERS..bed
-1F6D0 ; Extended_Pictographic# 8.0 [1] (🛐) place of worship
-1F6D1..1F6D2 ; Extended_Pictographic# 9.0 [2] (🛑..🛒) stop sign..shopping cart
-1F6D3..1F6D4 ; Extended_Pictographic# 10.0 [2] (🛓..🛔) STUPA..PAGODA
-1F6D5 ; Extended_Pictographic# 12.0 [1] (🛕) hindu temple
-1F6D6..1F6DF ; Extended_Pictographic# NA [10] (🛖..🛟) <reserved-1F6D6>..<reserved-1F6DF>
-1F6E0..1F6EC ; Extended_Pictographic# 7.0 [13] (🛠️..🛬) hammer and wrench..airplane arrival
-1F6ED..1F6EF ; Extended_Pictographic# NA [3] (🛭..🛯) <reserved-1F6ED>..<reserved-1F6EF>
-1F6F0..1F6F3 ; Extended_Pictographic# 7.0 [4] (🛰️..🛳️) satellite..passenger ship
-1F6F4..1F6F6 ; Extended_Pictographic# 9.0 [3] (🛴..🛶) kick scooter..canoe
-1F6F7..1F6F8 ; Extended_Pictographic# 10.0 [2] (🛷..🛸) sled..flying saucer
-1F6F9 ; Extended_Pictographic# 11.0 [1] (🛹) skateboard
-1F6FA ; Extended_Pictographic# 12.0 [1] (🛺) auto rickshaw
-1F6FB..1F6FF ; Extended_Pictographic# NA [5] (🛻..🛿) <reserved-1F6FB>..<reserved-1F6FF>
-1F774..1F77F ; Extended_Pictographic# NA [12] (🝴..🝿) <reserved-1F774>..<reserved-1F77F>
-1F7D5..1F7D8 ; Extended_Pictographic# 11.0 [4] (🟕..🟘) CIRCLED TRIANGLE..NEGATIVE CIRCLED SQUARE
-1F7D9..1F7DF ; Extended_Pictographic# NA [7] (🟙..🟟) <reserved-1F7D9>..<reserved-1F7DF>
-1F7E0..1F7EB ; Extended_Pictographic# 12.0 [12] (🟠..🟫) orange circle..brown square
-1F7EC..1F7FF ; Extended_Pictographic# NA [20] (🟬..🟿) <reserved-1F7EC>..<reserved-1F7FF>
-1F80C..1F80F ; Extended_Pictographic# NA [4] (🠌..🠏) <reserved-1F80C>..<reserved-1F80F>
-1F848..1F84F ; Extended_Pictographic# NA [8] (🡈..🡏) <reserved-1F848>..<reserved-1F84F>
-1F85A..1F85F ; Extended_Pictographic# NA [6] (🡚..🡟) <reserved-1F85A>..<reserved-1F85F>
-1F888..1F88F ; Extended_Pictographic# NA [8] (🢈..🢏) <reserved-1F888>..<reserved-1F88F>
-1F8AE..1F8FF ; Extended_Pictographic# NA [82] (🢮..🣿) <reserved-1F8AE>..<reserved-1F8FF>
-1F90C ; Extended_Pictographic# NA [1] (🤌) <reserved-1F90C>
-1F90D..1F90F ; Extended_Pictographic# 12.0 [3] (🤍..🤏) white heart..pinching hand
-1F910..1F918 ; Extended_Pictographic# 8.0 [9] (🤐..🤘) zipper-mouth face..sign of the horns
-1F919..1F91E ; Extended_Pictographic# 9.0 [6] (🤙..🤞) call me hand..crossed fingers
-1F91F ; Extended_Pictographic# 10.0 [1] (🤟) love-you gesture
-1F920..1F927 ; Extended_Pictographic# 9.0 [8] (🤠..🤧) cowboy hat face..sneezing face
-1F928..1F92F ; Extended_Pictographic# 10.0 [8] (🤨..🤯) face with raised eyebrow..exploding head
-1F930 ; Extended_Pictographic# 9.0 [1] (🤰) pregnant woman
-1F931..1F932 ; Extended_Pictographic# 10.0 [2] (🤱..🤲) breast-feeding..palms up together
-1F933..1F93A ; Extended_Pictographic# 9.0 [8] (🤳..🤺) selfie..person fencing
-1F93C..1F93E ; Extended_Pictographic# 9.0 [3] (🤼..🤾) people wrestling..person playing handball
-1F93F ; Extended_Pictographic# 12.0 [1] (🤿) diving mask
-1F940..1F945 ; Extended_Pictographic# 9.0 [6] (🥀..🥅) wilted flower..goal net
-1F947..1F94B ; Extended_Pictographic# 9.0 [5] (🥇..🥋) 1st place medal..martial arts uniform
-1F94C ; Extended_Pictographic# 10.0 [1] (🥌) curling stone
-1F94D..1F94F ; Extended_Pictographic# 11.0 [3] (🥍..🥏) lacrosse..flying disc
-1F950..1F95E ; Extended_Pictographic# 9.0 [15] (🥐..🥞) croissant..pancakes
-1F95F..1F96B ; Extended_Pictographic# 10.0 [13] (🥟..🥫) dumpling..canned food
-1F96C..1F970 ; Extended_Pictographic# 11.0 [5] (🥬..🥰) leafy green..smiling face with hearts
-1F971 ; Extended_Pictographic# 12.0 [1] (🥱) yawning face
-1F972 ; Extended_Pictographic# NA [1] (🥲) <reserved-1F972>
-1F973..1F976 ; Extended_Pictographic# 11.0 [4] (🥳..🥶) partying face..cold face
-1F977..1F979 ; Extended_Pictographic# NA [3] (🥷..🥹) <reserved-1F977>..<reserved-1F979>
-1F97A ; Extended_Pictographic# 11.0 [1] (🥺) pleading face
-1F97B ; Extended_Pictographic# 12.0 [1] (🥻) sari
-1F97C..1F97F ; Extended_Pictographic# 11.0 [4] (🥼..🥿) lab coat..flat shoe
-1F980..1F984 ; Extended_Pictographic# 8.0 [5] (🦀..🦄) crab..unicorn
-1F985..1F991 ; Extended_Pictographic# 9.0 [13] (🦅..🦑) eagle..squid
-1F992..1F997 ; Extended_Pictographic# 10.0 [6] (🦒..🦗) giraffe..cricket
-1F998..1F9A2 ; Extended_Pictographic# 11.0 [11] (🦘..🦢) kangaroo..swan
-1F9A3..1F9A4 ; Extended_Pictographic# NA [2] (🦣..🦤) <reserved-1F9A3>..<reserved-1F9A4>
-1F9A5..1F9AA ; Extended_Pictographic# 12.0 [6] (🦥..🦪) sloth..oyster
-1F9AB..1F9AD ; Extended_Pictographic# NA [3] (🦫..🦭) <reserved-1F9AB>..<reserved-1F9AD>
-1F9AE..1F9AF ; Extended_Pictographic# 12.0 [2] (🦮..🦯) guide dog..probing cane
-1F9B0..1F9B9 ; Extended_Pictographic# 11.0 [10] (🦰..🦹) red hair..supervillain
-1F9BA..1F9BF ; Extended_Pictographic# 12.0 [6] (🦺..🦿) safety vest..mechanical leg
-1F9C0 ; Extended_Pictographic# 8.0 [1] (🧀) cheese wedge
-1F9C1..1F9C2 ; Extended_Pictographic# 11.0 [2] (🧁..🧂) cupcake..salt
-1F9C3..1F9CA ; Extended_Pictographic# 12.0 [8] (🧃..🧊) beverage box..ice cube
-1F9CB..1F9CC ; Extended_Pictographic# NA [2] (🧋..🧌) <reserved-1F9CB>..<reserved-1F9CC>
-1F9CD..1F9CF ; Extended_Pictographic# 12.0 [3] (🧍..🧏) person standing..deaf person
-1F9D0..1F9E6 ; Extended_Pictographic# 10.0 [23] (🧐..🧦) face with monocle..socks
-1F9E7..1F9FF ; Extended_Pictographic# 11.0 [25] (🧧..🧿) red envelope..nazar amulet
-1FA00..1FA53 ; Extended_Pictographic# 12.0 [84] (🨀..🩓) NEUTRAL CHESS KING..BLACK CHESS KNIGHT-BISHOP
-1FA54..1FA5F ; Extended_Pictographic# NA [12] (🩔..🩟) <reserved-1FA54>..<reserved-1FA5F>
-1FA60..1FA6D ; Extended_Pictographic# 11.0 [14] (🩠..🩭) XIANGQI RED GENERAL..XIANGQI BLACK SOLDIER
-1FA6E..1FA6F ; Extended_Pictographic# NA [2] (🩮..🩯) <reserved-1FA6E>..<reserved-1FA6F>
-1FA70..1FA73 ; Extended_Pictographic# 12.0 [4] (🩰..🩳) ballet shoes..shorts
-1FA74..1FA77 ; Extended_Pictographic# NA [4] (🩴..🩷) <reserved-1FA74>..<reserved-1FA77>
-1FA78..1FA7A ; Extended_Pictographic# 12.0 [3] (🩸..🩺) drop of blood..stethoscope
-1FA7B..1FA7F ; Extended_Pictographic# NA [5] (🩻..🩿) <reserved-1FA7B>..<reserved-1FA7F>
-1FA80..1FA82 ; Extended_Pictographic# 12.0 [3] (🪀..🪂) yo-yo..parachute
-1FA83..1FA8F ; Extended_Pictographic# NA [13] (🪃..🪏) <reserved-1FA83>..<reserved-1FA8F>
-1FA90..1FA95 ; Extended_Pictographic# 12.0 [6] (🪐..🪕) ringed planet..banjo
-1FA96..1FFFD ; Extended_Pictographic# NA[1384] (🪖..🿽) <reserved-1FA96>..<reserved-1FFFD>
-
-# Total elements: 3793
-
-#EOF
diff --git a/lib/pleroma/emoji-test.txt b/lib/pleroma/emoji-test.txt
new file mode 100644
index 000000000..d3c6d12bd
--- /dev/null
+++ b/lib/pleroma/emoji-test.txt
@@ -0,0 +1,4879 @@
+# emoji-test.txt
+# Date: 2020-09-12, 22:19:50 GMT
+# © 2020 Unicode®, Inc.
+# Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries.
+# For terms of use, see http://www.unicode.org/terms_of_use.html
+#
+# Emoji Keyboard/Display Test Data for UTS #51
+# Version: 13.1
+#
+# For documentation and usage, see http://www.unicode.org/reports/tr51
+#
+# This file provides data for testing which emoji forms should be in keyboards and which should also be displayed/processed.
+# Format: code points; status # emoji name
+# Code points — list of one or more hex code points, separated by spaces
+# Status
+# component — an Emoji_Component,
+# excluding Regional_Indicators, ASCII, and non-Emoji.
+# fully-qualified — a fully-qualified emoji (see ED-18 in UTS #51),
+# excluding Emoji_Component
+# minimally-qualified — a minimally-qualified emoji (see ED-18a in UTS #51)
+# unqualified — a unqualified emoji (See ED-19 in UTS #51)
+# Notes:
+# • This includes the emoji components that need emoji presentation (skin tone and hair)
+# when isolated, but omits the components that need not have an emoji
+# presentation when isolated.
+# • The RGI set is covered by the listed fully-qualified emoji.
+# • The listed minimally-qualified and unqualified cover all cases where an
+# element of the RGI set is missing one or more emoji presentation selectors.
+# • The file is in CLDR order, not codepoint order. This is recommended (but not required!) for keyboard palettes.
+# • The groups and subgroups are illustrative. See the Emoji Order chart for more information.
+
+
+# group: Smileys & Emotion
+
+# subgroup: face-smiling
+1F600 ; fully-qualified # 😀 E1.0 grinning face
+1F603 ; fully-qualified # 😃 E0.6 grinning face with big eyes
+1F604 ; fully-qualified # 😄 E0.6 grinning face with smiling eyes
+1F601 ; fully-qualified # 😁 E0.6 beaming face with smiling eyes
+1F606 ; fully-qualified # 😆 E0.6 grinning squinting face
+1F605 ; fully-qualified # 😅 E0.6 grinning face with sweat
+1F923 ; fully-qualified # 🤣 E3.0 rolling on the floor laughing
+1F602 ; fully-qualified # 😂 E0.6 face with tears of joy
+1F642 ; fully-qualified # 🙂 E1.0 slightly smiling face
+1F643 ; fully-qualified # 🙃 E1.0 upside-down face
+1F609 ; fully-qualified # 😉 E0.6 winking face
+1F60A ; fully-qualified # 😊 E0.6 smiling face with smiling eyes
+1F607 ; fully-qualified # 😇 E1.0 smiling face with halo
+
+# subgroup: face-affection
+1F970 ; fully-qualified # 🥰 E11.0 smiling face with hearts
+1F60D ; fully-qualified # 😍 E0.6 smiling face with heart-eyes
+1F929 ; fully-qualified # 🤩 E5.0 star-struck
+1F618 ; fully-qualified # 😘 E0.6 face blowing a kiss
+1F617 ; fully-qualified # 😗 E1.0 kissing face
+263A FE0F ; fully-qualified # ☺️ E0.6 smiling face
+263A ; unqualified # ☺ E0.6 smiling face
+1F61A ; fully-qualified # 😚 E0.6 kissing face with closed eyes
+1F619 ; fully-qualified # 😙 E1.0 kissing face with smiling eyes
+1F972 ; fully-qualified # 🥲 E13.0 smiling face with tear
+
+# subgroup: face-tongue
+1F60B ; fully-qualified # 😋 E0.6 face savoring food
+1F61B ; fully-qualified # 😛 E1.0 face with tongue
+1F61C ; fully-qualified # 😜 E0.6 winking face with tongue
+1F92A ; fully-qualified # 🤪 E5.0 zany face
+1F61D ; fully-qualified # 😝 E0.6 squinting face with tongue
+1F911 ; fully-qualified # 🤑 E1.0 money-mouth face
+
+# subgroup: face-hand
+1F917 ; fully-qualified # 🤗 E1.0 hugging face
+1F92D ; fully-qualified # 🤭 E5.0 face with hand over mouth
+1F92B ; fully-qualified # 🤫 E5.0 shushing face
+1F914 ; fully-qualified # 🤔 E1.0 thinking face
+
+# subgroup: face-neutral-skeptical
+1F910 ; fully-qualified # 🤐 E1.0 zipper-mouth face
+1F928 ; fully-qualified # 🤨 E5.0 face with raised eyebrow
+1F610 ; fully-qualified # 😐 E0.7 neutral face
+1F611 ; fully-qualified # 😑 E1.0 expressionless face
+1F636 ; fully-qualified # 😶 E1.0 face without mouth
+1F636 200D 1F32B FE0F ; fully-qualified # 😶‍🌫️ E13.1 face in clouds
+1F636 200D 1F32B ; minimally-qualified # 😶‍🌫 E13.1 face in clouds
+1F60F ; fully-qualified # 😏 E0.6 smirking face
+1F612 ; fully-qualified # 😒 E0.6 unamused face
+1F644 ; fully-qualified # 🙄 E1.0 face with rolling eyes
+1F62C ; fully-qualified # 😬 E1.0 grimacing face
+1F62E 200D 1F4A8 ; fully-qualified # 😮‍💨 E13.1 face exhaling
+1F925 ; fully-qualified # 🤥 E3.0 lying face
+
+# subgroup: face-sleepy
+1F60C ; fully-qualified # 😌 E0.6 relieved face
+1F614 ; fully-qualified # 😔 E0.6 pensive face
+1F62A ; fully-qualified # 😪 E0.6 sleepy face
+1F924 ; fully-qualified # 🤤 E3.0 drooling face
+1F634 ; fully-qualified # 😴 E1.0 sleeping face
+
+# subgroup: face-unwell
+1F637 ; fully-qualified # 😷 E0.6 face with medical mask
+1F912 ; fully-qualified # 🤒 E1.0 face with thermometer
+1F915 ; fully-qualified # 🤕 E1.0 face with head-bandage
+1F922 ; fully-qualified # 🤢 E3.0 nauseated face
+1F92E ; fully-qualified # 🤮 E5.0 face vomiting
+1F927 ; fully-qualified # 🤧 E3.0 sneezing face
+1F975 ; fully-qualified # 🥵 E11.0 hot face
+1F976 ; fully-qualified # 🥶 E11.0 cold face
+1F974 ; fully-qualified # 🥴 E11.0 woozy face
+1F635 ; fully-qualified # 😵 E0.6 knocked-out face
+1F635 200D 1F4AB ; fully-qualified # 😵‍💫 E13.1 face with spiral eyes
+1F92F ; fully-qualified # 🤯 E5.0 exploding head
+
+# subgroup: face-hat
+1F920 ; fully-qualified # 🤠 E3.0 cowboy hat face
+1F973 ; fully-qualified # 🥳 E11.0 partying face
+1F978 ; fully-qualified # 🥸 E13.0 disguised face
+
+# subgroup: face-glasses
+1F60E ; fully-qualified # 😎 E1.0 smiling face with sunglasses
+1F913 ; fully-qualified # 🤓 E1.0 nerd face
+1F9D0 ; fully-qualified # 🧐 E5.0 face with monocle
+
+# subgroup: face-concerned
+1F615 ; fully-qualified # 😕 E1.0 confused face
+1F61F ; fully-qualified # 😟 E1.0 worried face
+1F641 ; fully-qualified # 🙁 E1.0 slightly frowning face
+2639 FE0F ; fully-qualified # ☹️ E0.7 frowning face
+2639 ; unqualified # ☹ E0.7 frowning face
+1F62E ; fully-qualified # 😮 E1.0 face with open mouth
+1F62F ; fully-qualified # 😯 E1.0 hushed face
+1F632 ; fully-qualified # 😲 E0.6 astonished face
+1F633 ; fully-qualified # 😳 E0.6 flushed face
+1F97A ; fully-qualified # 🥺 E11.0 pleading face
+1F626 ; fully-qualified # 😦 E1.0 frowning face with open mouth
+1F627 ; fully-qualified # 😧 E1.0 anguished face
+1F628 ; fully-qualified # 😨 E0.6 fearful face
+1F630 ; fully-qualified # 😰 E0.6 anxious face with sweat
+1F625 ; fully-qualified # 😥 E0.6 sad but relieved face
+1F622 ; fully-qualified # 😢 E0.6 crying face
+1F62D ; fully-qualified # 😭 E0.6 loudly crying face
+1F631 ; fully-qualified # 😱 E0.6 face screaming in fear
+1F616 ; fully-qualified # 😖 E0.6 confounded face
+1F623 ; fully-qualified # 😣 E0.6 persevering face
+1F61E ; fully-qualified # 😞 E0.6 disappointed face
+1F613 ; fully-qualified # 😓 E0.6 downcast face with sweat
+1F629 ; fully-qualified # 😩 E0.6 weary face
+1F62B ; fully-qualified # 😫 E0.6 tired face
+1F971 ; fully-qualified # 🥱 E12.0 yawning face
+
+# subgroup: face-negative
+1F624 ; fully-qualified # 😤 E0.6 face with steam from nose
+1F621 ; fully-qualified # 😡 E0.6 pouting face
+1F620 ; fully-qualified # 😠 E0.6 angry face
+1F92C ; fully-qualified # 🤬 E5.0 face with symbols on mouth
+1F608 ; fully-qualified # 😈 E1.0 smiling face with horns
+1F47F ; fully-qualified # 👿 E0.6 angry face with horns
+1F480 ; fully-qualified # 💀 E0.6 skull
+2620 FE0F ; fully-qualified # ☠️ E1.0 skull and crossbones
+2620 ; unqualified # ☠ E1.0 skull and crossbones
+
+# subgroup: face-costume
+1F4A9 ; fully-qualified # 💩 E0.6 pile of poo
+1F921 ; fully-qualified # 🤡 E3.0 clown face
+1F479 ; fully-qualified # 👹 E0.6 ogre
+1F47A ; fully-qualified # 👺 E0.6 goblin
+1F47B ; fully-qualified # 👻 E0.6 ghost
+1F47D ; fully-qualified # 👽 E0.6 alien
+1F47E ; fully-qualified # 👾 E0.6 alien monster
+1F916 ; fully-qualified # 🤖 E1.0 robot
+
+# subgroup: cat-face
+1F63A ; fully-qualified # 😺 E0.6 grinning cat
+1F638 ; fully-qualified # 😸 E0.6 grinning cat with smiling eyes
+1F639 ; fully-qualified # 😹 E0.6 cat with tears of joy
+1F63B ; fully-qualified # 😻 E0.6 smiling cat with heart-eyes
+1F63C ; fully-qualified # 😼 E0.6 cat with wry smile
+1F63D ; fully-qualified # 😽 E0.6 kissing cat
+1F640 ; fully-qualified # 🙀 E0.6 weary cat
+1F63F ; fully-qualified # 😿 E0.6 crying cat
+1F63E ; fully-qualified # 😾 E0.6 pouting cat
+
+# subgroup: monkey-face
+1F648 ; fully-qualified # 🙈 E0.6 see-no-evil monkey
+1F649 ; fully-qualified # 🙉 E0.6 hear-no-evil monkey
+1F64A ; fully-qualified # 🙊 E0.6 speak-no-evil monkey
+
+# subgroup: emotion
+1F48B ; fully-qualified # 💋 E0.6 kiss mark
+1F48C ; fully-qualified # 💌 E0.6 love letter
+1F498 ; fully-qualified # 💘 E0.6 heart with arrow
+1F49D ; fully-qualified # 💝 E0.6 heart with ribbon
+1F496 ; fully-qualified # 💖 E0.6 sparkling heart
+1F497 ; fully-qualified # 💗 E0.6 growing heart
+1F493 ; fully-qualified # 💓 E0.6 beating heart
+1F49E ; fully-qualified # 💞 E0.6 revolving hearts
+1F495 ; fully-qualified # 💕 E0.6 two hearts
+1F49F ; fully-qualified # 💟 E0.6 heart decoration
+2763 FE0F ; fully-qualified # ❣️ E1.0 heart exclamation
+2763 ; unqualified # ❣ E1.0 heart exclamation
+1F494 ; fully-qualified # 💔 E0.6 broken heart
+2764 FE0F 200D 1F525 ; fully-qualified # ❤️‍🔥 E13.1 heart on fire
+2764 200D 1F525 ; unqualified # ❤‍🔥 E13.1 heart on fire
+2764 FE0F 200D 1FA79 ; fully-qualified # ❤️‍🩹 E13.1 mending heart
+2764 200D 1FA79 ; unqualified # ❤‍🩹 E13.1 mending heart
+2764 FE0F ; fully-qualified # ❤️ E0.6 red heart
+2764 ; unqualified # ❤ E0.6 red heart
+1F9E1 ; fully-qualified # 🧡 E5.0 orange heart
+1F49B ; fully-qualified # 💛 E0.6 yellow heart
+1F49A ; fully-qualified # 💚 E0.6 green heart
+1F499 ; fully-qualified # 💙 E0.6 blue heart
+1F49C ; fully-qualified # 💜 E0.6 purple heart
+1F90E ; fully-qualified # 🤎 E12.0 brown heart
+1F5A4 ; fully-qualified # 🖤 E3.0 black heart
+1F90D ; fully-qualified # 🤍 E12.0 white heart
+1F4AF ; fully-qualified # 💯 E0.6 hundred points
+1F4A2 ; fully-qualified # 💢 E0.6 anger symbol
+1F4A5 ; fully-qualified # 💥 E0.6 collision
+1F4AB ; fully-qualified # 💫 E0.6 dizzy
+1F4A6 ; fully-qualified # 💦 E0.6 sweat droplets
+1F4A8 ; fully-qualified # 💨 E0.6 dashing away
+1F573 FE0F ; fully-qualified # 🕳️ E0.7 hole
+1F573 ; unqualified # 🕳 E0.7 hole
+1F4A3 ; fully-qualified # 💣 E0.6 bomb
+1F4AC ; fully-qualified # 💬 E0.6 speech balloon
+1F441 FE0F 200D 1F5E8 FE0F ; fully-qualified # 👁️‍🗨️ E2.0 eye in speech bubble
+1F441 200D 1F5E8 FE0F ; unqualified # 👁‍🗨️ E2.0 eye in speech bubble
+1F441 FE0F 200D 1F5E8 ; unqualified # 👁️‍🗨 E2.0 eye in speech bubble
+1F441 200D 1F5E8 ; unqualified # 👁‍🗨 E2.0 eye in speech bubble
+1F5E8 FE0F ; fully-qualified # 🗨️ E2.0 left speech bubble
+1F5E8 ; unqualified # 🗨 E2.0 left speech bubble
+1F5EF FE0F ; fully-qualified # 🗯️ E0.7 right anger bubble
+1F5EF ; unqualified # 🗯 E0.7 right anger bubble
+1F4AD ; fully-qualified # 💭 E1.0 thought balloon
+1F4A4 ; fully-qualified # 💤 E0.6 zzz
+
+# Smileys & Emotion subtotal: 170
+# Smileys & Emotion subtotal: 170 w/o modifiers
+
+# group: People & Body
+
+# subgroup: hand-fingers-open
+1F44B ; fully-qualified # 👋 E0.6 waving hand
+1F44B 1F3FB ; fully-qualified # 👋🏻 E1.0 waving hand: light skin tone
+1F44B 1F3FC ; fully-qualified # 👋🏼 E1.0 waving hand: medium-light skin tone
+1F44B 1F3FD ; fully-qualified # 👋🏽 E1.0 waving hand: medium skin tone
+1F44B 1F3FE ; fully-qualified # 👋🏾 E1.0 waving hand: medium-dark skin tone
+1F44B 1F3FF ; fully-qualified # 👋🏿 E1.0 waving hand: dark skin tone
+1F91A ; fully-qualified # 🤚 E3.0 raised back of hand
+1F91A 1F3FB ; fully-qualified # 🤚🏻 E3.0 raised back of hand: light skin tone
+1F91A 1F3FC ; fully-qualified # 🤚🏼 E3.0 raised back of hand: medium-light skin tone
+1F91A 1F3FD ; fully-qualified # 🤚🏽 E3.0 raised back of hand: medium skin tone
+1F91A 1F3FE ; fully-qualified # 🤚🏾 E3.0 raised back of hand: medium-dark skin tone
+1F91A 1F3FF ; fully-qualified # 🤚🏿 E3.0 raised back of hand: dark skin tone
+1F590 FE0F ; fully-qualified # 🖐️ E0.7 hand with fingers splayed
+1F590 ; unqualified # 🖐 E0.7 hand with fingers splayed
+1F590 1F3FB ; fully-qualified # 🖐🏻 E1.0 hand with fingers splayed: light skin tone
+1F590 1F3FC ; fully-qualified # 🖐🏼 E1.0 hand with fingers splayed: medium-light skin tone
+1F590 1F3FD ; fully-qualified # 🖐🏽 E1.0 hand with fingers splayed: medium skin tone
+1F590 1F3FE ; fully-qualified # 🖐🏾 E1.0 hand with fingers splayed: medium-dark skin tone
+1F590 1F3FF ; fully-qualified # 🖐🏿 E1.0 hand with fingers splayed: dark skin tone
+270B ; fully-qualified # ✋ E0.6 raised hand
+270B 1F3FB ; fully-qualified # ✋🏻 E1.0 raised hand: light skin tone
+270B 1F3FC ; fully-qualified # ✋🏼 E1.0 raised hand: medium-light skin tone
+270B 1F3FD ; fully-qualified # ✋🏽 E1.0 raised hand: medium skin tone
+270B 1F3FE ; fully-qualified # ✋🏾 E1.0 raised hand: medium-dark skin tone
+270B 1F3FF ; fully-qualified # ✋🏿 E1.0 raised hand: dark skin tone
+1F596 ; fully-qualified # 🖖 E1.0 vulcan salute
+1F596 1F3FB ; fully-qualified # 🖖🏻 E1.0 vulcan salute: light skin tone
+1F596 1F3FC ; fully-qualified # 🖖🏼 E1.0 vulcan salute: medium-light skin tone
+1F596 1F3FD ; fully-qualified # 🖖🏽 E1.0 vulcan salute: medium skin tone
+1F596 1F3FE ; fully-qualified # 🖖🏾 E1.0 vulcan salute: medium-dark skin tone
+1F596 1F3FF ; fully-qualified # 🖖🏿 E1.0 vulcan salute: dark skin tone
+
+# subgroup: hand-fingers-partial
+1F44C ; fully-qualified # 👌 E0.6 OK hand
+1F44C 1F3FB ; fully-qualified # 👌🏻 E1.0 OK hand: light skin tone
+1F44C 1F3FC ; fully-qualified # 👌🏼 E1.0 OK hand: medium-light skin tone
+1F44C 1F3FD ; fully-qualified # 👌🏽 E1.0 OK hand: medium skin tone
+1F44C 1F3FE ; fully-qualified # 👌🏾 E1.0 OK hand: medium-dark skin tone
+1F44C 1F3FF ; fully-qualified # 👌🏿 E1.0 OK hand: dark skin tone
+1F90C ; fully-qualified # 🤌 E13.0 pinched fingers
+1F90C 1F3FB ; fully-qualified # 🤌🏻 E13.0 pinched fingers: light skin tone
+1F90C 1F3FC ; fully-qualified # 🤌🏼 E13.0 pinched fingers: medium-light skin tone
+1F90C 1F3FD ; fully-qualified # 🤌🏽 E13.0 pinched fingers: medium skin tone
+1F90C 1F3FE ; fully-qualified # 🤌🏾 E13.0 pinched fingers: medium-dark skin tone
+1F90C 1F3FF ; fully-qualified # 🤌🏿 E13.0 pinched fingers: dark skin tone
+1F90F ; fully-qualified # 🤏 E12.0 pinching hand
+1F90F 1F3FB ; fully-qualified # 🤏🏻 E12.0 pinching hand: light skin tone
+1F90F 1F3FC ; fully-qualified # 🤏🏼 E12.0 pinching hand: medium-light skin tone
+1F90F 1F3FD ; fully-qualified # 🤏🏽 E12.0 pinching hand: medium skin tone
+1F90F 1F3FE ; fully-qualified # 🤏🏾 E12.0 pinching hand: medium-dark skin tone
+1F90F 1F3FF ; fully-qualified # 🤏🏿 E12.0 pinching hand: dark skin tone
+270C FE0F ; fully-qualified # ✌️ E0.6 victory hand
+270C ; unqualified # ✌ E0.6 victory hand
+270C 1F3FB ; fully-qualified # ✌🏻 E1.0 victory hand: light skin tone
+270C 1F3FC ; fully-qualified # ✌🏼 E1.0 victory hand: medium-light skin tone
+270C 1F3FD ; fully-qualified # ✌🏽 E1.0 victory hand: medium skin tone
+270C 1F3FE ; fully-qualified # ✌🏾 E1.0 victory hand: medium-dark skin tone
+270C 1F3FF ; fully-qualified # ✌🏿 E1.0 victory hand: dark skin tone
+1F91E ; fully-qualified # 🤞 E3.0 crossed fingers
+1F91E 1F3FB ; fully-qualified # 🤞🏻 E3.0 crossed fingers: light skin tone
+1F91E 1F3FC ; fully-qualified # 🤞🏼 E3.0 crossed fingers: medium-light skin tone
+1F91E 1F3FD ; fully-qualified # 🤞🏽 E3.0 crossed fingers: medium skin tone
+1F91E 1F3FE ; fully-qualified # 🤞🏾 E3.0 crossed fingers: medium-dark skin tone
+1F91E 1F3FF ; fully-qualified # 🤞🏿 E3.0 crossed fingers: dark skin tone
+1F91F ; fully-qualified # 🤟 E5.0 love-you gesture
+1F91F 1F3FB ; fully-qualified # 🤟🏻 E5.0 love-you gesture: light skin tone
+1F91F 1F3FC ; fully-qualified # 🤟🏼 E5.0 love-you gesture: medium-light skin tone
+1F91F 1F3FD ; fully-qualified # 🤟🏽 E5.0 love-you gesture: medium skin tone
+1F91F 1F3FE ; fully-qualified # 🤟🏾 E5.0 love-you gesture: medium-dark skin tone
+1F91F 1F3FF ; fully-qualified # 🤟🏿 E5.0 love-you gesture: dark skin tone
+1F918 ; fully-qualified # 🤘 E1.0 sign of the horns
+1F918 1F3FB ; fully-qualified # 🤘🏻 E1.0 sign of the horns: light skin tone
+1F918 1F3FC ; fully-qualified # 🤘🏼 E1.0 sign of the horns: medium-light skin tone
+1F918 1F3FD ; fully-qualified # 🤘🏽 E1.0 sign of the horns: medium skin tone
+1F918 1F3FE ; fully-qualified # 🤘🏾 E1.0 sign of the horns: medium-dark skin tone
+1F918 1F3FF ; fully-qualified # 🤘🏿 E1.0 sign of the horns: dark skin tone
+1F919 ; fully-qualified # 🤙 E3.0 call me hand
+1F919 1F3FB ; fully-qualified # 🤙🏻 E3.0 call me hand: light skin tone
+1F919 1F3FC ; fully-qualified # 🤙🏼 E3.0 call me hand: medium-light skin tone
+1F919 1F3FD ; fully-qualified # 🤙🏽 E3.0 call me hand: medium skin tone
+1F919 1F3FE ; fully-qualified # 🤙🏾 E3.0 call me hand: medium-dark skin tone
+1F919 1F3FF ; fully-qualified # 🤙🏿 E3.0 call me hand: dark skin tone
+
+# subgroup: hand-single-finger
+1F448 ; fully-qualified # 👈 E0.6 backhand index pointing left
+1F448 1F3FB ; fully-qualified # 👈🏻 E1.0 backhand index pointing left: light skin tone
+1F448 1F3FC ; fully-qualified # 👈🏼 E1.0 backhand index pointing left: medium-light skin tone
+1F448 1F3FD ; fully-qualified # 👈🏽 E1.0 backhand index pointing left: medium skin tone
+1F448 1F3FE ; fully-qualified # 👈🏾 E1.0 backhand index pointing left: medium-dark skin tone
+1F448 1F3FF ; fully-qualified # 👈🏿 E1.0 backhand index pointing left: dark skin tone
+1F449 ; fully-qualified # 👉 E0.6 backhand index pointing right
+1F449 1F3FB ; fully-qualified # 👉🏻 E1.0 backhand index pointing right: light skin tone
+1F449 1F3FC ; fully-qualified # 👉🏼 E1.0 backhand index pointing right: medium-light skin tone
+1F449 1F3FD ; fully-qualified # 👉🏽 E1.0 backhand index pointing right: medium skin tone
+1F449 1F3FE ; fully-qualified # 👉🏾 E1.0 backhand index pointing right: medium-dark skin tone
+1F449 1F3FF ; fully-qualified # 👉🏿 E1.0 backhand index pointing right: dark skin tone
+1F446 ; fully-qualified # 👆 E0.6 backhand index pointing up
+1F446 1F3FB ; fully-qualified # 👆🏻 E1.0 backhand index pointing up: light skin tone
+1F446 1F3FC ; fully-qualified # 👆🏼 E1.0 backhand index pointing up: medium-light skin tone
+1F446 1F3FD ; fully-qualified # 👆🏽 E1.0 backhand index pointing up: medium skin tone
+1F446 1F3FE ; fully-qualified # 👆🏾 E1.0 backhand index pointing up: medium-dark skin tone
+1F446 1F3FF ; fully-qualified # 👆🏿 E1.0 backhand index pointing up: dark skin tone
+1F595 ; fully-qualified # 🖕 E1.0 middle finger
+1F595 1F3FB ; fully-qualified # 🖕🏻 E1.0 middle finger: light skin tone
+1F595 1F3FC ; fully-qualified # 🖕🏼 E1.0 middle finger: medium-light skin tone
+1F595 1F3FD ; fully-qualified # 🖕🏽 E1.0 middle finger: medium skin tone
+1F595 1F3FE ; fully-qualified # 🖕🏾 E1.0 middle finger: medium-dark skin tone
+1F595 1F3FF ; fully-qualified # 🖕🏿 E1.0 middle finger: dark skin tone
+1F447 ; fully-qualified # 👇 E0.6 backhand index pointing down
+1F447 1F3FB ; fully-qualified # 👇🏻 E1.0 backhand index pointing down: light skin tone
+1F447 1F3FC ; fully-qualified # 👇🏼 E1.0 backhand index pointing down: medium-light skin tone
+1F447 1F3FD ; fully-qualified # 👇🏽 E1.0 backhand index pointing down: medium skin tone
+1F447 1F3FE ; fully-qualified # 👇🏾 E1.0 backhand index pointing down: medium-dark skin tone
+1F447 1F3FF ; fully-qualified # 👇🏿 E1.0 backhand index pointing down: dark skin tone
+261D FE0F ; fully-qualified # ☝️ E0.6 index pointing up
+261D ; unqualified # ☝ E0.6 index pointing up
+261D 1F3FB ; fully-qualified # ☝🏻 E1.0 index pointing up: light skin tone
+261D 1F3FC ; fully-qualified # ☝🏼 E1.0 index pointing up: medium-light skin tone
+261D 1F3FD ; fully-qualified # ☝🏽 E1.0 index pointing up: medium skin tone
+261D 1F3FE ; fully-qualified # ☝🏾 E1.0 index pointing up: medium-dark skin tone
+261D 1F3FF ; fully-qualified # ☝🏿 E1.0 index pointing up: dark skin tone
+
+# subgroup: hand-fingers-closed
+1F44D ; fully-qualified # 👍 E0.6 thumbs up
+1F44D 1F3FB ; fully-qualified # 👍🏻 E1.0 thumbs up: light skin tone
+1F44D 1F3FC ; fully-qualified # 👍🏼 E1.0 thumbs up: medium-light skin tone
+1F44D 1F3FD ; fully-qualified # 👍🏽 E1.0 thumbs up: medium skin tone
+1F44D 1F3FE ; fully-qualified # 👍🏾 E1.0 thumbs up: medium-dark skin tone
+1F44D 1F3FF ; fully-qualified # 👍🏿 E1.0 thumbs up: dark skin tone
+1F44E ; fully-qualified # 👎 E0.6 thumbs down
+1F44E 1F3FB ; fully-qualified # 👎🏻 E1.0 thumbs down: light skin tone
+1F44E 1F3FC ; fully-qualified # 👎🏼 E1.0 thumbs down: medium-light skin tone
+1F44E 1F3FD ; fully-qualified # 👎🏽 E1.0 thumbs down: medium skin tone
+1F44E 1F3FE ; fully-qualified # 👎🏾 E1.0 thumbs down: medium-dark skin tone
+1F44E 1F3FF ; fully-qualified # 👎🏿 E1.0 thumbs down: dark skin tone
+270A ; fully-qualified # ✊ E0.6 raised fist
+270A 1F3FB ; fully-qualified # ✊🏻 E1.0 raised fist: light skin tone
+270A 1F3FC ; fully-qualified # ✊🏼 E1.0 raised fist: medium-light skin tone
+270A 1F3FD ; fully-qualified # ✊🏽 E1.0 raised fist: medium skin tone
+270A 1F3FE ; fully-qualified # ✊🏾 E1.0 raised fist: medium-dark skin tone
+270A 1F3FF ; fully-qualified # ✊🏿 E1.0 raised fist: dark skin tone
+1F44A ; fully-qualified # 👊 E0.6 oncoming fist
+1F44A 1F3FB ; fully-qualified # 👊🏻 E1.0 oncoming fist: light skin tone
+1F44A 1F3FC ; fully-qualified # 👊🏼 E1.0 oncoming fist: medium-light skin tone
+1F44A 1F3FD ; fully-qualified # 👊🏽 E1.0 oncoming fist: medium skin tone
+1F44A 1F3FE ; fully-qualified # 👊🏾 E1.0 oncoming fist: medium-dark skin tone
+1F44A 1F3FF ; fully-qualified # 👊🏿 E1.0 oncoming fist: dark skin tone
+1F91B ; fully-qualified # 🤛 E3.0 left-facing fist
+1F91B 1F3FB ; fully-qualified # 🤛🏻 E3.0 left-facing fist: light skin tone
+1F91B 1F3FC ; fully-qualified # 🤛🏼 E3.0 left-facing fist: medium-light skin tone
+1F91B 1F3FD ; fully-qualified # 🤛🏽 E3.0 left-facing fist: medium skin tone
+1F91B 1F3FE ; fully-qualified # 🤛🏾 E3.0 left-facing fist: medium-dark skin tone
+1F91B 1F3FF ; fully-qualified # 🤛🏿 E3.0 left-facing fist: dark skin tone
+1F91C ; fully-qualified # 🤜 E3.0 right-facing fist
+1F91C 1F3FB ; fully-qualified # 🤜🏻 E3.0 right-facing fist: light skin tone
+1F91C 1F3FC ; fully-qualified # 🤜🏼 E3.0 right-facing fist: medium-light skin tone
+1F91C 1F3FD ; fully-qualified # 🤜🏽 E3.0 right-facing fist: medium skin tone
+1F91C 1F3FE ; fully-qualified # 🤜🏾 E3.0 right-facing fist: medium-dark skin tone
+1F91C 1F3FF ; fully-qualified # 🤜🏿 E3.0 right-facing fist: dark skin tone
+
+# subgroup: hands
+1F44F ; fully-qualified # 👏 E0.6 clapping hands
+1F44F 1F3FB ; fully-qualified # 👏🏻 E1.0 clapping hands: light skin tone
+1F44F 1F3FC ; fully-qualified # 👏🏼 E1.0 clapping hands: medium-light skin tone
+1F44F 1F3FD ; fully-qualified # 👏🏽 E1.0 clapping hands: medium skin tone
+1F44F 1F3FE ; fully-qualified # 👏🏾 E1.0 clapping hands: medium-dark skin tone
+1F44F 1F3FF ; fully-qualified # 👏🏿 E1.0 clapping hands: dark skin tone
+1F64C ; fully-qualified # 🙌 E0.6 raising hands
+1F64C 1F3FB ; fully-qualified # 🙌🏻 E1.0 raising hands: light skin tone
+1F64C 1F3FC ; fully-qualified # 🙌🏼 E1.0 raising hands: medium-light skin tone
+1F64C 1F3FD ; fully-qualified # 🙌🏽 E1.0 raising hands: medium skin tone
+1F64C 1F3FE ; fully-qualified # 🙌🏾 E1.0 raising hands: medium-dark skin tone
+1F64C 1F3FF ; fully-qualified # 🙌🏿 E1.0 raising hands: dark skin tone
+1F450 ; fully-qualified # 👐 E0.6 open hands
+1F450 1F3FB ; fully-qualified # 👐🏻 E1.0 open hands: light skin tone
+1F450 1F3FC ; fully-qualified # 👐🏼 E1.0 open hands: medium-light skin tone
+1F450 1F3FD ; fully-qualified # 👐🏽 E1.0 open hands: medium skin tone
+1F450 1F3FE ; fully-qualified # 👐🏾 E1.0 open hands: medium-dark skin tone
+1F450 1F3FF ; fully-qualified # 👐🏿 E1.0 open hands: dark skin tone
+1F932 ; fully-qualified # 🤲 E5.0 palms up together
+1F932 1F3FB ; fully-qualified # 🤲🏻 E5.0 palms up together: light skin tone
+1F932 1F3FC ; fully-qualified # 🤲🏼 E5.0 palms up together: medium-light skin tone
+1F932 1F3FD ; fully-qualified # 🤲🏽 E5.0 palms up together: medium skin tone
+1F932 1F3FE ; fully-qualified # 🤲🏾 E5.0 palms up together: medium-dark skin tone
+1F932 1F3FF ; fully-qualified # 🤲🏿 E5.0 palms up together: dark skin tone
+1F91D ; fully-qualified # 🤝 E3.0 handshake
+1F64F ; fully-qualified # 🙏 E0.6 folded hands
+1F64F 1F3FB ; fully-qualified # 🙏🏻 E1.0 folded hands: light skin tone
+1F64F 1F3FC ; fully-qualified # 🙏🏼 E1.0 folded hands: medium-light skin tone
+1F64F 1F3FD ; fully-qualified # 🙏🏽 E1.0 folded hands: medium skin tone
+1F64F 1F3FE ; fully-qualified # 🙏🏾 E1.0 folded hands: medium-dark skin tone
+1F64F 1F3FF ; fully-qualified # 🙏🏿 E1.0 folded hands: dark skin tone
+
+# subgroup: hand-prop
+270D FE0F ; fully-qualified # ✍️ E0.7 writing hand
+270D ; unqualified # ✍ E0.7 writing hand
+270D 1F3FB ; fully-qualified # ✍🏻 E1.0 writing hand: light skin tone
+270D 1F3FC ; fully-qualified # ✍🏼 E1.0 writing hand: medium-light skin tone
+270D 1F3FD ; fully-qualified # ✍🏽 E1.0 writing hand: medium skin tone
+270D 1F3FE ; fully-qualified # ✍🏾 E1.0 writing hand: medium-dark skin tone
+270D 1F3FF ; fully-qualified # ✍🏿 E1.0 writing hand: dark skin tone
+1F485 ; fully-qualified # 💅 E0.6 nail polish
+1F485 1F3FB ; fully-qualified # 💅🏻 E1.0 nail polish: light skin tone
+1F485 1F3FC ; fully-qualified # 💅🏼 E1.0 nail polish: medium-light skin tone
+1F485 1F3FD ; fully-qualified # 💅🏽 E1.0 nail polish: medium skin tone
+1F485 1F3FE ; fully-qualified # 💅🏾 E1.0 nail polish: medium-dark skin tone
+1F485 1F3FF ; fully-qualified # 💅🏿 E1.0 nail polish: dark skin tone
+1F933 ; fully-qualified # 🤳 E3.0 selfie
+1F933 1F3FB ; fully-qualified # 🤳🏻 E3.0 selfie: light skin tone
+1F933 1F3FC ; fully-qualified # 🤳🏼 E3.0 selfie: medium-light skin tone
+1F933 1F3FD ; fully-qualified # 🤳🏽 E3.0 selfie: medium skin tone
+1F933 1F3FE ; fully-qualified # 🤳🏾 E3.0 selfie: medium-dark skin tone
+1F933 1F3FF ; fully-qualified # 🤳🏿 E3.0 selfie: dark skin tone
+
+# subgroup: body-parts
+1F4AA ; fully-qualified # 💪 E0.6 flexed biceps
+1F4AA 1F3FB ; fully-qualified # 💪🏻 E1.0 flexed biceps: light skin tone
+1F4AA 1F3FC ; fully-qualified # 💪🏼 E1.0 flexed biceps: medium-light skin tone
+1F4AA 1F3FD ; fully-qualified # 💪🏽 E1.0 flexed biceps: medium skin tone
+1F4AA 1F3FE ; fully-qualified # 💪🏾 E1.0 flexed biceps: medium-dark skin tone
+1F4AA 1F3FF ; fully-qualified # 💪🏿 E1.0 flexed biceps: dark skin tone
+1F9BE ; fully-qualified # 🦾 E12.0 mechanical arm
+1F9BF ; fully-qualified # 🦿 E12.0 mechanical leg
+1F9B5 ; fully-qualified # 🦵 E11.0 leg
+1F9B5 1F3FB ; fully-qualified # 🦵🏻 E11.0 leg: light skin tone
+1F9B5 1F3FC ; fully-qualified # 🦵🏼 E11.0 leg: medium-light skin tone
+1F9B5 1F3FD ; fully-qualified # 🦵🏽 E11.0 leg: medium skin tone
+1F9B5 1F3FE ; fully-qualified # 🦵🏾 E11.0 leg: medium-dark skin tone
+1F9B5 1F3FF ; fully-qualified # 🦵🏿 E11.0 leg: dark skin tone
+1F9B6 ; fully-qualified # 🦶 E11.0 foot
+1F9B6 1F3FB ; fully-qualified # 🦶🏻 E11.0 foot: light skin tone
+1F9B6 1F3FC ; fully-qualified # 🦶🏼 E11.0 foot: medium-light skin tone
+1F9B6 1F3FD ; fully-qualified # 🦶🏽 E11.0 foot: medium skin tone
+1F9B6 1F3FE ; fully-qualified # 🦶🏾 E11.0 foot: medium-dark skin tone
+1F9B6 1F3FF ; fully-qualified # 🦶🏿 E11.0 foot: dark skin tone
+1F442 ; fully-qualified # 👂 E0.6 ear
+1F442 1F3FB ; fully-qualified # 👂🏻 E1.0 ear: light skin tone
+1F442 1F3FC ; fully-qualified # 👂🏼 E1.0 ear: medium-light skin tone
+1F442 1F3FD ; fully-qualified # 👂🏽 E1.0 ear: medium skin tone
+1F442 1F3FE ; fully-qualified # 👂🏾 E1.0 ear: medium-dark skin tone
+1F442 1F3FF ; fully-qualified # 👂🏿 E1.0 ear: dark skin tone
+1F9BB ; fully-qualified # 🦻 E12.0 ear with hearing aid
+1F9BB 1F3FB ; fully-qualified # 🦻🏻 E12.0 ear with hearing aid: light skin tone
+1F9BB 1F3FC ; fully-qualified # 🦻🏼 E12.0 ear with hearing aid: medium-light skin tone
+1F9BB 1F3FD ; fully-qualified # 🦻🏽 E12.0 ear with hearing aid: medium skin tone
+1F9BB 1F3FE ; fully-qualified # 🦻🏾 E12.0 ear with hearing aid: medium-dark skin tone
+1F9BB 1F3FF ; fully-qualified # 🦻🏿 E12.0 ear with hearing aid: dark skin tone
+1F443 ; fully-qualified # 👃 E0.6 nose
+1F443 1F3FB ; fully-qualified # 👃🏻 E1.0 nose: light skin tone
+1F443 1F3FC ; fully-qualified # 👃🏼 E1.0 nose: medium-light skin tone
+1F443 1F3FD ; fully-qualified # 👃🏽 E1.0 nose: medium skin tone
+1F443 1F3FE ; fully-qualified # 👃🏾 E1.0 nose: medium-dark skin tone
+1F443 1F3FF ; fully-qualified # 👃🏿 E1.0 nose: dark skin tone
+1F9E0 ; fully-qualified # 🧠 E5.0 brain
+1FAC0 ; fully-qualified # 🫀 E13.0 anatomical heart
+1FAC1 ; fully-qualified # 🫁 E13.0 lungs
+1F9B7 ; fully-qualified # 🦷 E11.0 tooth
+1F9B4 ; fully-qualified # 🦴 E11.0 bone
+1F440 ; fully-qualified # 👀 E0.6 eyes
+1F441 FE0F ; fully-qualified # 👁️ E0.7 eye
+1F441 ; unqualified # 👁 E0.7 eye
+1F445 ; fully-qualified # 👅 E0.6 tongue
+1F444 ; fully-qualified # 👄 E0.6 mouth
+
+# subgroup: person
+1F476 ; fully-qualified # 👶 E0.6 baby
+1F476 1F3FB ; fully-qualified # 👶🏻 E1.0 baby: light skin tone
+1F476 1F3FC ; fully-qualified # 👶🏼 E1.0 baby: medium-light skin tone
+1F476 1F3FD ; fully-qualified # 👶🏽 E1.0 baby: medium skin tone
+1F476 1F3FE ; fully-qualified # 👶🏾 E1.0 baby: medium-dark skin tone
+1F476 1F3FF ; fully-qualified # 👶🏿 E1.0 baby: dark skin tone
+1F9D2 ; fully-qualified # 🧒 E5.0 child
+1F9D2 1F3FB ; fully-qualified # 🧒🏻 E5.0 child: light skin tone
+1F9D2 1F3FC ; fully-qualified # 🧒🏼 E5.0 child: medium-light skin tone
+1F9D2 1F3FD ; fully-qualified # 🧒🏽 E5.0 child: medium skin tone
+1F9D2 1F3FE ; fully-qualified # 🧒🏾 E5.0 child: medium-dark skin tone
+1F9D2 1F3FF ; fully-qualified # 🧒🏿 E5.0 child: dark skin tone
+1F466 ; fully-qualified # 👦 E0.6 boy
+1F466 1F3FB ; fully-qualified # 👦🏻 E1.0 boy: light skin tone
+1F466 1F3FC ; fully-qualified # 👦🏼 E1.0 boy: medium-light skin tone
+1F466 1F3FD ; fully-qualified # 👦🏽 E1.0 boy: medium skin tone
+1F466 1F3FE ; fully-qualified # 👦🏾 E1.0 boy: medium-dark skin tone
+1F466 1F3FF ; fully-qualified # 👦🏿 E1.0 boy: dark skin tone
+1F467 ; fully-qualified # 👧 E0.6 girl
+1F467 1F3FB ; fully-qualified # 👧🏻 E1.0 girl: light skin tone
+1F467 1F3FC ; fully-qualified # 👧🏼 E1.0 girl: medium-light skin tone
+1F467 1F3FD ; fully-qualified # 👧🏽 E1.0 girl: medium skin tone
+1F467 1F3FE ; fully-qualified # 👧🏾 E1.0 girl: medium-dark skin tone
+1F467 1F3FF ; fully-qualified # 👧🏿 E1.0 girl: dark skin tone
+1F9D1 ; fully-qualified # 🧑 E5.0 person
+1F9D1 1F3FB ; fully-qualified # 🧑🏻 E5.0 person: light skin tone
+1F9D1 1F3FC ; fully-qualified # 🧑🏼 E5.0 person: medium-light skin tone
+1F9D1 1F3FD ; fully-qualified # 🧑🏽 E5.0 person: medium skin tone
+1F9D1 1F3FE ; fully-qualified # 🧑🏾 E5.0 person: medium-dark skin tone
+1F9D1 1F3FF ; fully-qualified # 🧑🏿 E5.0 person: dark skin tone
+1F471 ; fully-qualified # 👱 E0.6 person: blond hair
+1F471 1F3FB ; fully-qualified # 👱🏻 E1.0 person: light skin tone, blond hair
+1F471 1F3FC ; fully-qualified # 👱🏼 E1.0 person: medium-light skin tone, blond hair
+1F471 1F3FD ; fully-qualified # 👱🏽 E1.0 person: medium skin tone, blond hair
+1F471 1F3FE ; fully-qualified # 👱🏾 E1.0 person: medium-dark skin tone, blond hair
+1F471 1F3FF ; fully-qualified # 👱🏿 E1.0 person: dark skin tone, blond hair
+1F468 ; fully-qualified # 👨 E0.6 man
+1F468 1F3FB ; fully-qualified # 👨🏻 E1.0 man: light skin tone
+1F468 1F3FC ; fully-qualified # 👨🏼 E1.0 man: medium-light skin tone
+1F468 1F3FD ; fully-qualified # 👨🏽 E1.0 man: medium skin tone
+1F468 1F3FE ; fully-qualified # 👨🏾 E1.0 man: medium-dark skin tone
+1F468 1F3FF ; fully-qualified # 👨🏿 E1.0 man: dark skin tone
+1F9D4 ; fully-qualified # 🧔 E5.0 person: beard
+1F9D4 1F3FB ; fully-qualified # 🧔🏻 E5.0 person: light skin tone, beard
+1F9D4 1F3FC ; fully-qualified # 🧔🏼 E5.0 person: medium-light skin tone, beard
+1F9D4 1F3FD ; fully-qualified # 🧔🏽 E5.0 person: medium skin tone, beard
+1F9D4 1F3FE ; fully-qualified # 🧔🏾 E5.0 person: medium-dark skin tone, beard
+1F9D4 1F3FF ; fully-qualified # 🧔🏿 E5.0 person: dark skin tone, beard
+1F9D4 200D 2642 FE0F ; fully-qualified # 🧔‍♂️ E13.1 man: beard
+1F9D4 200D 2642 ; minimally-qualified # 🧔‍♂ E13.1 man: beard
+1F9D4 1F3FB 200D 2642 FE0F ; fully-qualified # 🧔🏻‍♂️ E13.1 man: light skin tone, beard
+1F9D4 1F3FB 200D 2642 ; minimally-qualified # 🧔🏻‍♂ E13.1 man: light skin tone, beard
+1F9D4 1F3FC 200D 2642 FE0F ; fully-qualified # 🧔🏼‍♂️ E13.1 man: medium-light skin tone, beard
+1F9D4 1F3FC 200D 2642 ; minimally-qualified # 🧔🏼‍♂ E13.1 man: medium-light skin tone, beard
+1F9D4 1F3FD 200D 2642 FE0F ; fully-qualified # 🧔🏽‍♂️ E13.1 man: medium skin tone, beard
+1F9D4 1F3FD 200D 2642 ; minimally-qualified # 🧔🏽‍♂ E13.1 man: medium skin tone, beard
+1F9D4 1F3FE 200D 2642 FE0F ; fully-qualified # 🧔🏾‍♂️ E13.1 man: medium-dark skin tone, beard
+1F9D4 1F3FE 200D 2642 ; minimally-qualified # 🧔🏾‍♂ E13.1 man: medium-dark skin tone, beard
+1F9D4 1F3FF 200D 2642 FE0F ; fully-qualified # 🧔🏿‍♂️ E13.1 man: dark skin tone, beard
+1F9D4 1F3FF 200D 2642 ; minimally-qualified # 🧔🏿‍♂ E13.1 man: dark skin tone, beard
+1F9D4 200D 2640 FE0F ; fully-qualified # 🧔‍♀️ E13.1 woman: beard
+1F9D4 200D 2640 ; minimally-qualified # 🧔‍♀ E13.1 woman: beard
+1F9D4 1F3FB 200D 2640 FE0F ; fully-qualified # 🧔🏻‍♀️ E13.1 woman: light skin tone, beard
+1F9D4 1F3FB 200D 2640 ; minimally-qualified # 🧔🏻‍♀ E13.1 woman: light skin tone, beard
+1F9D4 1F3FC 200D 2640 FE0F ; fully-qualified # 🧔🏼‍♀️ E13.1 woman: medium-light skin tone, beard
+1F9D4 1F3FC 200D 2640 ; minimally-qualified # 🧔🏼‍♀ E13.1 woman: medium-light skin tone, beard
+1F9D4 1F3FD 200D 2640 FE0F ; fully-qualified # 🧔🏽‍♀️ E13.1 woman: medium skin tone, beard
+1F9D4 1F3FD 200D 2640 ; minimally-qualified # 🧔🏽‍♀ E13.1 woman: medium skin tone, beard
+1F9D4 1F3FE 200D 2640 FE0F ; fully-qualified # 🧔🏾‍♀️ E13.1 woman: medium-dark skin tone, beard
+1F9D4 1F3FE 200D 2640 ; minimally-qualified # 🧔🏾‍♀ E13.1 woman: medium-dark skin tone, beard
+1F9D4 1F3FF 200D 2640 FE0F ; fully-qualified # 🧔🏿‍♀️ E13.1 woman: dark skin tone, beard
+1F9D4 1F3FF 200D 2640 ; minimally-qualified # 🧔🏿‍♀ E13.1 woman: dark skin tone, beard
+1F468 200D 1F9B0 ; fully-qualified # 👨‍🦰 E11.0 man: red hair
+1F468 1F3FB 200D 1F9B0 ; fully-qualified # 👨🏻‍🦰 E11.0 man: light skin tone, red hair
+1F468 1F3FC 200D 1F9B0 ; fully-qualified # 👨🏼‍🦰 E11.0 man: medium-light skin tone, red hair
+1F468 1F3FD 200D 1F9B0 ; fully-qualified # 👨🏽‍🦰 E11.0 man: medium skin tone, red hair
+1F468 1F3FE 200D 1F9B0 ; fully-qualified # 👨🏾‍🦰 E11.0 man: medium-dark skin tone, red hair
+1F468 1F3FF 200D 1F9B0 ; fully-qualified # 👨🏿‍🦰 E11.0 man: dark skin tone, red hair
+1F468 200D 1F9B1 ; fully-qualified # 👨‍🦱 E11.0 man: curly hair
+1F468 1F3FB 200D 1F9B1 ; fully-qualified # 👨🏻‍🦱 E11.0 man: light skin tone, curly hair
+1F468 1F3FC 200D 1F9B1 ; fully-qualified # 👨🏼‍🦱 E11.0 man: medium-light skin tone, curly hair
+1F468 1F3FD 200D 1F9B1 ; fully-qualified # 👨🏽‍🦱 E11.0 man: medium skin tone, curly hair
+1F468 1F3FE 200D 1F9B1 ; fully-qualified # 👨🏾‍🦱 E11.0 man: medium-dark skin tone, curly hair
+1F468 1F3FF 200D 1F9B1 ; fully-qualified # 👨🏿‍🦱 E11.0 man: dark skin tone, curly hair
+1F468 200D 1F9B3 ; fully-qualified # 👨‍🦳 E11.0 man: white hair
+1F468 1F3FB 200D 1F9B3 ; fully-qualified # 👨🏻‍🦳 E11.0 man: light skin tone, white hair
+1F468 1F3FC 200D 1F9B3 ; fully-qualified # 👨🏼‍🦳 E11.0 man: medium-light skin tone, white hair
+1F468 1F3FD 200D 1F9B3 ; fully-qualified # 👨🏽‍🦳 E11.0 man: medium skin tone, white hair
+1F468 1F3FE 200D 1F9B3 ; fully-qualified # 👨🏾‍🦳 E11.0 man: medium-dark skin tone, white hair
+1F468 1F3FF 200D 1F9B3 ; fully-qualified # 👨🏿‍🦳 E11.0 man: dark skin tone, white hair
+1F468 200D 1F9B2 ; fully-qualified # 👨‍🦲 E11.0 man: bald
+1F468 1F3FB 200D 1F9B2 ; fully-qualified # 👨🏻‍🦲 E11.0 man: light skin tone, bald
+1F468 1F3FC 200D 1F9B2 ; fully-qualified # 👨🏼‍🦲 E11.0 man: medium-light skin tone, bald
+1F468 1F3FD 200D 1F9B2 ; fully-qualified # 👨🏽‍🦲 E11.0 man: medium skin tone, bald
+1F468 1F3FE 200D 1F9B2 ; fully-qualified # 👨🏾‍🦲 E11.0 man: medium-dark skin tone, bald
+1F468 1F3FF 200D 1F9B2 ; fully-qualified # 👨🏿‍🦲 E11.0 man: dark skin tone, bald
+1F469 ; fully-qualified # 👩 E0.6 woman
+1F469 1F3FB ; fully-qualified # 👩🏻 E1.0 woman: light skin tone
+1F469 1F3FC ; fully-qualified # 👩🏼 E1.0 woman: medium-light skin tone
+1F469 1F3FD ; fully-qualified # 👩🏽 E1.0 woman: medium skin tone
+1F469 1F3FE ; fully-qualified # 👩🏾 E1.0 woman: medium-dark skin tone
+1F469 1F3FF ; fully-qualified # 👩🏿 E1.0 woman: dark skin tone
+1F469 200D 1F9B0 ; fully-qualified # 👩‍🦰 E11.0 woman: red hair
+1F469 1F3FB 200D 1F9B0 ; fully-qualified # 👩🏻‍🦰 E11.0 woman: light skin tone, red hair
+1F469 1F3FC 200D 1F9B0 ; fully-qualified # 👩🏼‍🦰 E11.0 woman: medium-light skin tone, red hair
+1F469 1F3FD 200D 1F9B0 ; fully-qualified # 👩🏽‍🦰 E11.0 woman: medium skin tone, red hair
+1F469 1F3FE 200D 1F9B0 ; fully-qualified # 👩🏾‍🦰 E11.0 woman: medium-dark skin tone, red hair
+1F469 1F3FF 200D 1F9B0 ; fully-qualified # 👩🏿‍🦰 E11.0 woman: dark skin tone, red hair
+1F9D1 200D 1F9B0 ; fully-qualified # 🧑‍🦰 E12.1 person: red hair
+1F9D1 1F3FB 200D 1F9B0 ; fully-qualified # 🧑🏻‍🦰 E12.1 person: light skin tone, red hair
+1F9D1 1F3FC 200D 1F9B0 ; fully-qualified # 🧑🏼‍🦰 E12.1 person: medium-light skin tone, red hair
+1F9D1 1F3FD 200D 1F9B0 ; fully-qualified # 🧑🏽‍🦰 E12.1 person: medium skin tone, red hair
+1F9D1 1F3FE 200D 1F9B0 ; fully-qualified # 🧑🏾‍🦰 E12.1 person: medium-dark skin tone, red hair
+1F9D1 1F3FF 200D 1F9B0 ; fully-qualified # 🧑🏿‍🦰 E12.1 person: dark skin tone, red hair
+1F469 200D 1F9B1 ; fully-qualified # 👩‍🦱 E11.0 woman: curly hair
+1F469 1F3FB 200D 1F9B1 ; fully-qualified # 👩🏻‍🦱 E11.0 woman: light skin tone, curly hair
+1F469 1F3FC 200D 1F9B1 ; fully-qualified # 👩🏼‍🦱 E11.0 woman: medium-light skin tone, curly hair
+1F469 1F3FD 200D 1F9B1 ; fully-qualified # 👩🏽‍🦱 E11.0 woman: medium skin tone, curly hair
+1F469 1F3FE 200D 1F9B1 ; fully-qualified # 👩🏾‍🦱 E11.0 woman: medium-dark skin tone, curly hair
+1F469 1F3FF 200D 1F9B1 ; fully-qualified # 👩🏿‍🦱 E11.0 woman: dark skin tone, curly hair
+1F9D1 200D 1F9B1 ; fully-qualified # 🧑‍🦱 E12.1 person: curly hair
+1F9D1 1F3FB 200D 1F9B1 ; fully-qualified # 🧑🏻‍🦱 E12.1 person: light skin tone, curly hair
+1F9D1 1F3FC 200D 1F9B1 ; fully-qualified # 🧑🏼‍🦱 E12.1 person: medium-light skin tone, curly hair
+1F9D1 1F3FD 200D 1F9B1 ; fully-qualified # 🧑🏽‍🦱 E12.1 person: medium skin tone, curly hair
+1F9D1 1F3FE 200D 1F9B1 ; fully-qualified # 🧑🏾‍🦱 E12.1 person: medium-dark skin tone, curly hair
+1F9D1 1F3FF 200D 1F9B1 ; fully-qualified # 🧑🏿‍🦱 E12.1 person: dark skin tone, curly hair
+1F469 200D 1F9B3 ; fully-qualified # 👩‍🦳 E11.0 woman: white hair
+1F469 1F3FB 200D 1F9B3 ; fully-qualified # 👩🏻‍🦳 E11.0 woman: light skin tone, white hair
+1F469 1F3FC 200D 1F9B3 ; fully-qualified # 👩🏼‍🦳 E11.0 woman: medium-light skin tone, white hair
+1F469 1F3FD 200D 1F9B3 ; fully-qualified # 👩🏽‍🦳 E11.0 woman: medium skin tone, white hair
+1F469 1F3FE 200D 1F9B3 ; fully-qualified # 👩🏾‍🦳 E11.0 woman: medium-dark skin tone, white hair
+1F469 1F3FF 200D 1F9B3 ; fully-qualified # 👩🏿‍🦳 E11.0 woman: dark skin tone, white hair
+1F9D1 200D 1F9B3 ; fully-qualified # 🧑‍🦳 E12.1 person: white hair
+1F9D1 1F3FB 200D 1F9B3 ; fully-qualified # 🧑🏻‍🦳 E12.1 person: light skin tone, white hair
+1F9D1 1F3FC 200D 1F9B3 ; fully-qualified # 🧑🏼‍🦳 E12.1 person: medium-light skin tone, white hair
+1F9D1 1F3FD 200D 1F9B3 ; fully-qualified # 🧑🏽‍🦳 E12.1 person: medium skin tone, white hair
+1F9D1 1F3FE 200D 1F9B3 ; fully-qualified # 🧑🏾‍🦳 E12.1 person: medium-dark skin tone, white hair
+1F9D1 1F3FF 200D 1F9B3 ; fully-qualified # 🧑🏿‍🦳 E12.1 person: dark skin tone, white hair
+1F469 200D 1F9B2 ; fully-qualified # 👩‍🦲 E11.0 woman: bald
+1F469 1F3FB 200D 1F9B2 ; fully-qualified # 👩🏻‍🦲 E11.0 woman: light skin tone, bald
+1F469 1F3FC 200D 1F9B2 ; fully-qualified # 👩🏼‍🦲 E11.0 woman: medium-light skin tone, bald
+1F469 1F3FD 200D 1F9B2 ; fully-qualified # 👩🏽‍🦲 E11.0 woman: medium skin tone, bald
+1F469 1F3FE 200D 1F9B2 ; fully-qualified # 👩🏾‍🦲 E11.0 woman: medium-dark skin tone, bald
+1F469 1F3FF 200D 1F9B2 ; fully-qualified # 👩🏿‍🦲 E11.0 woman: dark skin tone, bald
+1F9D1 200D 1F9B2 ; fully-qualified # 🧑‍🦲 E12.1 person: bald
+1F9D1 1F3FB 200D 1F9B2 ; fully-qualified # 🧑🏻‍🦲 E12.1 person: light skin tone, bald
+1F9D1 1F3FC 200D 1F9B2 ; fully-qualified # 🧑🏼‍🦲 E12.1 person: medium-light skin tone, bald
+1F9D1 1F3FD 200D 1F9B2 ; fully-qualified # 🧑🏽‍🦲 E12.1 person: medium skin tone, bald
+1F9D1 1F3FE 200D 1F9B2 ; fully-qualified # 🧑🏾‍🦲 E12.1 person: medium-dark skin tone, bald
+1F9D1 1F3FF 200D 1F9B2 ; fully-qualified # 🧑🏿‍🦲 E12.1 person: dark skin tone, bald
+1F471 200D 2640 FE0F ; fully-qualified # 👱‍♀️ E4.0 woman: blond hair
+1F471 200D 2640 ; minimally-qualified # 👱‍♀ E4.0 woman: blond hair
+1F471 1F3FB 200D 2640 FE0F ; fully-qualified # 👱🏻‍♀️ E4.0 woman: light skin tone, blond hair
+1F471 1F3FB 200D 2640 ; minimally-qualified # 👱🏻‍♀ E4.0 woman: light skin tone, blond hair
+1F471 1F3FC 200D 2640 FE0F ; fully-qualified # 👱🏼‍♀️ E4.0 woman: medium-light skin tone, blond hair
+1F471 1F3FC 200D 2640 ; minimally-qualified # 👱🏼‍♀ E4.0 woman: medium-light skin tone, blond hair
+1F471 1F3FD 200D 2640 FE0F ; fully-qualified # 👱🏽‍♀️ E4.0 woman: medium skin tone, blond hair
+1F471 1F3FD 200D 2640 ; minimally-qualified # 👱🏽‍♀ E4.0 woman: medium skin tone, blond hair
+1F471 1F3FE 200D 2640 FE0F ; fully-qualified # 👱🏾‍♀️ E4.0 woman: medium-dark skin tone, blond hair
+1F471 1F3FE 200D 2640 ; minimally-qualified # 👱🏾‍♀ E4.0 woman: medium-dark skin tone, blond hair
+1F471 1F3FF 200D 2640 FE0F ; fully-qualified # 👱🏿‍♀️ E4.0 woman: dark skin tone, blond hair
+1F471 1F3FF 200D 2640 ; minimally-qualified # 👱🏿‍♀ E4.0 woman: dark skin tone, blond hair
+1F471 200D 2642 FE0F ; fully-qualified # 👱‍♂️ E4.0 man: blond hair
+1F471 200D 2642 ; minimally-qualified # 👱‍♂ E4.0 man: blond hair
+1F471 1F3FB 200D 2642 FE0F ; fully-qualified # 👱🏻‍♂️ E4.0 man: light skin tone, blond hair
+1F471 1F3FB 200D 2642 ; minimally-qualified # 👱🏻‍♂ E4.0 man: light skin tone, blond hair
+1F471 1F3FC 200D 2642 FE0F ; fully-qualified # 👱🏼‍♂️ E4.0 man: medium-light skin tone, blond hair
+1F471 1F3FC 200D 2642 ; minimally-qualified # 👱🏼‍♂ E4.0 man: medium-light skin tone, blond hair
+1F471 1F3FD 200D 2642 FE0F ; fully-qualified # 👱🏽‍♂️ E4.0 man: medium skin tone, blond hair
+1F471 1F3FD 200D 2642 ; minimally-qualified # 👱🏽‍♂ E4.0 man: medium skin tone, blond hair
+1F471 1F3FE 200D 2642 FE0F ; fully-qualified # 👱🏾‍♂️ E4.0 man: medium-dark skin tone, blond hair
+1F471 1F3FE 200D 2642 ; minimally-qualified # 👱🏾‍♂ E4.0 man: medium-dark skin tone, blond hair
+1F471 1F3FF 200D 2642 FE0F ; fully-qualified # 👱🏿‍♂️ E4.0 man: dark skin tone, blond hair
+1F471 1F3FF 200D 2642 ; minimally-qualified # 👱🏿‍♂ E4.0 man: dark skin tone, blond hair
+1F9D3 ; fully-qualified # 🧓 E5.0 older person
+1F9D3 1F3FB ; fully-qualified # 🧓🏻 E5.0 older person: light skin tone
+1F9D3 1F3FC ; fully-qualified # 🧓🏼 E5.0 older person: medium-light skin tone
+1F9D3 1F3FD ; fully-qualified # 🧓🏽 E5.0 older person: medium skin tone
+1F9D3 1F3FE ; fully-qualified # 🧓🏾 E5.0 older person: medium-dark skin tone
+1F9D3 1F3FF ; fully-qualified # 🧓🏿 E5.0 older person: dark skin tone
+1F474 ; fully-qualified # 👴 E0.6 old man
+1F474 1F3FB ; fully-qualified # 👴🏻 E1.0 old man: light skin tone
+1F474 1F3FC ; fully-qualified # 👴🏼 E1.0 old man: medium-light skin tone
+1F474 1F3FD ; fully-qualified # 👴🏽 E1.0 old man: medium skin tone
+1F474 1F3FE ; fully-qualified # 👴🏾 E1.0 old man: medium-dark skin tone
+1F474 1F3FF ; fully-qualified # 👴🏿 E1.0 old man: dark skin tone
+1F475 ; fully-qualified # 👵 E0.6 old woman
+1F475 1F3FB ; fully-qualified # 👵🏻 E1.0 old woman: light skin tone
+1F475 1F3FC ; fully-qualified # 👵🏼 E1.0 old woman: medium-light skin tone
+1F475 1F3FD ; fully-qualified # 👵🏽 E1.0 old woman: medium skin tone
+1F475 1F3FE ; fully-qualified # 👵🏾 E1.0 old woman: medium-dark skin tone
+1F475 1F3FF ; fully-qualified # 👵🏿 E1.0 old woman: dark skin tone
+
+# subgroup: person-gesture
+1F64D ; fully-qualified # 🙍 E0.6 person frowning
+1F64D 1F3FB ; fully-qualified # 🙍🏻 E1.0 person frowning: light skin tone
+1F64D 1F3FC ; fully-qualified # 🙍🏼 E1.0 person frowning: medium-light skin tone
+1F64D 1F3FD ; fully-qualified # 🙍🏽 E1.0 person frowning: medium skin tone
+1F64D 1F3FE ; fully-qualified # 🙍🏾 E1.0 person frowning: medium-dark skin tone
+1F64D 1F3FF ; fully-qualified # 🙍🏿 E1.0 person frowning: dark skin tone
+1F64D 200D 2642 FE0F ; fully-qualified # 🙍‍♂️ E4.0 man frowning
+1F64D 200D 2642 ; minimally-qualified # 🙍‍♂ E4.0 man frowning
+1F64D 1F3FB 200D 2642 FE0F ; fully-qualified # 🙍🏻‍♂️ E4.0 man frowning: light skin tone
+1F64D 1F3FB 200D 2642 ; minimally-qualified # 🙍🏻‍♂ E4.0 man frowning: light skin tone
+1F64D 1F3FC 200D 2642 FE0F ; fully-qualified # 🙍🏼‍♂️ E4.0 man frowning: medium-light skin tone
+1F64D 1F3FC 200D 2642 ; minimally-qualified # 🙍🏼‍♂ E4.0 man frowning: medium-light skin tone
+1F64D 1F3FD 200D 2642 FE0F ; fully-qualified # 🙍🏽‍♂️ E4.0 man frowning: medium skin tone
+1F64D 1F3FD 200D 2642 ; minimally-qualified # 🙍🏽‍♂ E4.0 man frowning: medium skin tone
+1F64D 1F3FE 200D 2642 FE0F ; fully-qualified # 🙍🏾‍♂️ E4.0 man frowning: medium-dark skin tone
+1F64D 1F3FE 200D 2642 ; minimally-qualified # 🙍🏾‍♂ E4.0 man frowning: medium-dark skin tone
+1F64D 1F3FF 200D 2642 FE0F ; fully-qualified # 🙍🏿‍♂️ E4.0 man frowning: dark skin tone
+1F64D 1F3FF 200D 2642 ; minimally-qualified # 🙍🏿‍♂ E4.0 man frowning: dark skin tone
+1F64D 200D 2640 FE0F ; fully-qualified # 🙍‍♀️ E4.0 woman frowning
+1F64D 200D 2640 ; minimally-qualified # 🙍‍♀ E4.0 woman frowning
+1F64D 1F3FB 200D 2640 FE0F ; fully-qualified # 🙍🏻‍♀️ E4.0 woman frowning: light skin tone
+1F64D 1F3FB 200D 2640 ; minimally-qualified # 🙍🏻‍♀ E4.0 woman frowning: light skin tone
+1F64D 1F3FC 200D 2640 FE0F ; fully-qualified # 🙍🏼‍♀️ E4.0 woman frowning: medium-light skin tone
+1F64D 1F3FC 200D 2640 ; minimally-qualified # 🙍🏼‍♀ E4.0 woman frowning: medium-light skin tone
+1F64D 1F3FD 200D 2640 FE0F ; fully-qualified # 🙍🏽‍♀️ E4.0 woman frowning: medium skin tone
+1F64D 1F3FD 200D 2640 ; minimally-qualified # 🙍🏽‍♀ E4.0 woman frowning: medium skin tone
+1F64D 1F3FE 200D 2640 FE0F ; fully-qualified # 🙍🏾‍♀️ E4.0 woman frowning: medium-dark skin tone
+1F64D 1F3FE 200D 2640 ; minimally-qualified # 🙍🏾‍♀ E4.0 woman frowning: medium-dark skin tone
+1F64D 1F3FF 200D 2640 FE0F ; fully-qualified # 🙍🏿‍♀️ E4.0 woman frowning: dark skin tone
+1F64D 1F3FF 200D 2640 ; minimally-qualified # 🙍🏿‍♀ E4.0 woman frowning: dark skin tone
+1F64E ; fully-qualified # 🙎 E0.6 person pouting
+1F64E 1F3FB ; fully-qualified # 🙎🏻 E1.0 person pouting: light skin tone
+1F64E 1F3FC ; fully-qualified # 🙎🏼 E1.0 person pouting: medium-light skin tone
+1F64E 1F3FD ; fully-qualified # 🙎🏽 E1.0 person pouting: medium skin tone
+1F64E 1F3FE ; fully-qualified # 🙎🏾 E1.0 person pouting: medium-dark skin tone
+1F64E 1F3FF ; fully-qualified # 🙎🏿 E1.0 person pouting: dark skin tone
+1F64E 200D 2642 FE0F ; fully-qualified # 🙎‍♂️ E4.0 man pouting
+1F64E 200D 2642 ; minimally-qualified # 🙎‍♂ E4.0 man pouting
+1F64E 1F3FB 200D 2642 FE0F ; fully-qualified # 🙎🏻‍♂️ E4.0 man pouting: light skin tone
+1F64E 1F3FB 200D 2642 ; minimally-qualified # 🙎🏻‍♂ E4.0 man pouting: light skin tone
+1F64E 1F3FC 200D 2642 FE0F ; fully-qualified # 🙎🏼‍♂️ E4.0 man pouting: medium-light skin tone
+1F64E 1F3FC 200D 2642 ; minimally-qualified # 🙎🏼‍♂ E4.0 man pouting: medium-light skin tone
+1F64E 1F3FD 200D 2642 FE0F ; fully-qualified # 🙎🏽‍♂️ E4.0 man pouting: medium skin tone
+1F64E 1F3FD 200D 2642 ; minimally-qualified # 🙎🏽‍♂ E4.0 man pouting: medium skin tone
+1F64E 1F3FE 200D 2642 FE0F ; fully-qualified # 🙎🏾‍♂️ E4.0 man pouting: medium-dark skin tone
+1F64E 1F3FE 200D 2642 ; minimally-qualified # 🙎🏾‍♂ E4.0 man pouting: medium-dark skin tone
+1F64E 1F3FF 200D 2642 FE0F ; fully-qualified # 🙎🏿‍♂️ E4.0 man pouting: dark skin tone
+1F64E 1F3FF 200D 2642 ; minimally-qualified # 🙎🏿‍♂ E4.0 man pouting: dark skin tone
+1F64E 200D 2640 FE0F ; fully-qualified # 🙎‍♀️ E4.0 woman pouting
+1F64E 200D 2640 ; minimally-qualified # 🙎‍♀ E4.0 woman pouting
+1F64E 1F3FB 200D 2640 FE0F ; fully-qualified # 🙎🏻‍♀️ E4.0 woman pouting: light skin tone
+1F64E 1F3FB 200D 2640 ; minimally-qualified # 🙎🏻‍♀ E4.0 woman pouting: light skin tone
+1F64E 1F3FC 200D 2640 FE0F ; fully-qualified # 🙎🏼‍♀️ E4.0 woman pouting: medium-light skin tone
+1F64E 1F3FC 200D 2640 ; minimally-qualified # 🙎🏼‍♀ E4.0 woman pouting: medium-light skin tone
+1F64E 1F3FD 200D 2640 FE0F ; fully-qualified # 🙎🏽‍♀️ E4.0 woman pouting: medium skin tone
+1F64E 1F3FD 200D 2640 ; minimally-qualified # 🙎🏽‍♀ E4.0 woman pouting: medium skin tone
+1F64E 1F3FE 200D 2640 FE0F ; fully-qualified # 🙎🏾‍♀️ E4.0 woman pouting: medium-dark skin tone
+1F64E 1F3FE 200D 2640 ; minimally-qualified # 🙎🏾‍♀ E4.0 woman pouting: medium-dark skin tone
+1F64E 1F3FF 200D 2640 FE0F ; fully-qualified # 🙎🏿‍♀️ E4.0 woman pouting: dark skin tone
+1F64E 1F3FF 200D 2640 ; minimally-qualified # 🙎🏿‍♀ E4.0 woman pouting: dark skin tone
+1F645 ; fully-qualified # 🙅 E0.6 person gesturing NO
+1F645 1F3FB ; fully-qualified # 🙅🏻 E1.0 person gesturing NO: light skin tone
+1F645 1F3FC ; fully-qualified # 🙅🏼 E1.0 person gesturing NO: medium-light skin tone
+1F645 1F3FD ; fully-qualified # 🙅🏽 E1.0 person gesturing NO: medium skin tone
+1F645 1F3FE ; fully-qualified # 🙅🏾 E1.0 person gesturing NO: medium-dark skin tone
+1F645 1F3FF ; fully-qualified # 🙅🏿 E1.0 person gesturing NO: dark skin tone
+1F645 200D 2642 FE0F ; fully-qualified # 🙅‍♂️ E4.0 man gesturing NO
+1F645 200D 2642 ; minimally-qualified # 🙅‍♂ E4.0 man gesturing NO
+1F645 1F3FB 200D 2642 FE0F ; fully-qualified # 🙅🏻‍♂️ E4.0 man gesturing NO: light skin tone
+1F645 1F3FB 200D 2642 ; minimally-qualified # 🙅🏻‍♂ E4.0 man gesturing NO: light skin tone
+1F645 1F3FC 200D 2642 FE0F ; fully-qualified # 🙅🏼‍♂️ E4.0 man gesturing NO: medium-light skin tone
+1F645 1F3FC 200D 2642 ; minimally-qualified # 🙅🏼‍♂ E4.0 man gesturing NO: medium-light skin tone
+1F645 1F3FD 200D 2642 FE0F ; fully-qualified # 🙅🏽‍♂️ E4.0 man gesturing NO: medium skin tone
+1F645 1F3FD 200D 2642 ; minimally-qualified # 🙅🏽‍♂ E4.0 man gesturing NO: medium skin tone
+1F645 1F3FE 200D 2642 FE0F ; fully-qualified # 🙅🏾‍♂️ E4.0 man gesturing NO: medium-dark skin tone
+1F645 1F3FE 200D 2642 ; minimally-qualified # 🙅🏾‍♂ E4.0 man gesturing NO: medium-dark skin tone
+1F645 1F3FF 200D 2642 FE0F ; fully-qualified # 🙅🏿‍♂️ E4.0 man gesturing NO: dark skin tone
+1F645 1F3FF 200D 2642 ; minimally-qualified # 🙅🏿‍♂ E4.0 man gesturing NO: dark skin tone
+1F645 200D 2640 FE0F ; fully-qualified # 🙅‍♀️ E4.0 woman gesturing NO
+1F645 200D 2640 ; minimally-qualified # 🙅‍♀ E4.0 woman gesturing NO
+1F645 1F3FB 200D 2640 FE0F ; fully-qualified # 🙅🏻‍♀️ E4.0 woman gesturing NO: light skin tone
+1F645 1F3FB 200D 2640 ; minimally-qualified # 🙅🏻‍♀ E4.0 woman gesturing NO: light skin tone
+1F645 1F3FC 200D 2640 FE0F ; fully-qualified # 🙅🏼‍♀️ E4.0 woman gesturing NO: medium-light skin tone
+1F645 1F3FC 200D 2640 ; minimally-qualified # 🙅🏼‍♀ E4.0 woman gesturing NO: medium-light skin tone
+1F645 1F3FD 200D 2640 FE0F ; fully-qualified # 🙅🏽‍♀️ E4.0 woman gesturing NO: medium skin tone
+1F645 1F3FD 200D 2640 ; minimally-qualified # 🙅🏽‍♀ E4.0 woman gesturing NO: medium skin tone
+1F645 1F3FE 200D 2640 FE0F ; fully-qualified # 🙅🏾‍♀️ E4.0 woman gesturing NO: medium-dark skin tone
+1F645 1F3FE 200D 2640 ; minimally-qualified # 🙅🏾‍♀ E4.0 woman gesturing NO: medium-dark skin tone
+1F645 1F3FF 200D 2640 FE0F ; fully-qualified # 🙅🏿‍♀️ E4.0 woman gesturing NO: dark skin tone
+1F645 1F3FF 200D 2640 ; minimally-qualified # 🙅🏿‍♀ E4.0 woman gesturing NO: dark skin tone
+1F646 ; fully-qualified # 🙆 E0.6 person gesturing OK
+1F646 1F3FB ; fully-qualified # 🙆🏻 E1.0 person gesturing OK: light skin tone
+1F646 1F3FC ; fully-qualified # 🙆🏼 E1.0 person gesturing OK: medium-light skin tone
+1F646 1F3FD ; fully-qualified # 🙆🏽 E1.0 person gesturing OK: medium skin tone
+1F646 1F3FE ; fully-qualified # 🙆🏾 E1.0 person gesturing OK: medium-dark skin tone
+1F646 1F3FF ; fully-qualified # 🙆🏿 E1.0 person gesturing OK: dark skin tone
+1F646 200D 2642 FE0F ; fully-qualified # 🙆‍♂️ E4.0 man gesturing OK
+1F646 200D 2642 ; minimally-qualified # 🙆‍♂ E4.0 man gesturing OK
+1F646 1F3FB 200D 2642 FE0F ; fully-qualified # 🙆🏻‍♂️ E4.0 man gesturing OK: light skin tone
+1F646 1F3FB 200D 2642 ; minimally-qualified # 🙆🏻‍♂ E4.0 man gesturing OK: light skin tone
+1F646 1F3FC 200D 2642 FE0F ; fully-qualified # 🙆🏼‍♂️ E4.0 man gesturing OK: medium-light skin tone
+1F646 1F3FC 200D 2642 ; minimally-qualified # 🙆🏼‍♂ E4.0 man gesturing OK: medium-light skin tone
+1F646 1F3FD 200D 2642 FE0F ; fully-qualified # 🙆🏽‍♂️ E4.0 man gesturing OK: medium skin tone
+1F646 1F3FD 200D 2642 ; minimally-qualified # 🙆🏽‍♂ E4.0 man gesturing OK: medium skin tone
+1F646 1F3FE 200D 2642 FE0F ; fully-qualified # 🙆🏾‍♂️ E4.0 man gesturing OK: medium-dark skin tone
+1F646 1F3FE 200D 2642 ; minimally-qualified # 🙆🏾‍♂ E4.0 man gesturing OK: medium-dark skin tone
+1F646 1F3FF 200D 2642 FE0F ; fully-qualified # 🙆🏿‍♂️ E4.0 man gesturing OK: dark skin tone
+1F646 1F3FF 200D 2642 ; minimally-qualified # 🙆🏿‍♂ E4.0 man gesturing OK: dark skin tone
+1F646 200D 2640 FE0F ; fully-qualified # 🙆‍♀️ E4.0 woman gesturing OK
+1F646 200D 2640 ; minimally-qualified # 🙆‍♀ E4.0 woman gesturing OK
+1F646 1F3FB 200D 2640 FE0F ; fully-qualified # 🙆🏻‍♀️ E4.0 woman gesturing OK: light skin tone
+1F646 1F3FB 200D 2640 ; minimally-qualified # 🙆🏻‍♀ E4.0 woman gesturing OK: light skin tone
+1F646 1F3FC 200D 2640 FE0F ; fully-qualified # 🙆🏼‍♀️ E4.0 woman gesturing OK: medium-light skin tone
+1F646 1F3FC 200D 2640 ; minimally-qualified # 🙆🏼‍♀ E4.0 woman gesturing OK: medium-light skin tone
+1F646 1F3FD 200D 2640 FE0F ; fully-qualified # 🙆🏽‍♀️ E4.0 woman gesturing OK: medium skin tone
+1F646 1F3FD 200D 2640 ; minimally-qualified # 🙆🏽‍♀ E4.0 woman gesturing OK: medium skin tone
+1F646 1F3FE 200D 2640 FE0F ; fully-qualified # 🙆🏾‍♀️ E4.0 woman gesturing OK: medium-dark skin tone
+1F646 1F3FE 200D 2640 ; minimally-qualified # 🙆🏾‍♀ E4.0 woman gesturing OK: medium-dark skin tone
+1F646 1F3FF 200D 2640 FE0F ; fully-qualified # 🙆🏿‍♀️ E4.0 woman gesturing OK: dark skin tone
+1F646 1F3FF 200D 2640 ; minimally-qualified # 🙆🏿‍♀ E4.0 woman gesturing OK: dark skin tone
+1F481 ; fully-qualified # 💁 E0.6 person tipping hand
+1F481 1F3FB ; fully-qualified # 💁🏻 E1.0 person tipping hand: light skin tone
+1F481 1F3FC ; fully-qualified # 💁🏼 E1.0 person tipping hand: medium-light skin tone
+1F481 1F3FD ; fully-qualified # 💁🏽 E1.0 person tipping hand: medium skin tone
+1F481 1F3FE ; fully-qualified # 💁🏾 E1.0 person tipping hand: medium-dark skin tone
+1F481 1F3FF ; fully-qualified # 💁🏿 E1.0 person tipping hand: dark skin tone
+1F481 200D 2642 FE0F ; fully-qualified # 💁‍♂️ E4.0 man tipping hand
+1F481 200D 2642 ; minimally-qualified # 💁‍♂ E4.0 man tipping hand
+1F481 1F3FB 200D 2642 FE0F ; fully-qualified # 💁🏻‍♂️ E4.0 man tipping hand: light skin tone
+1F481 1F3FB 200D 2642 ; minimally-qualified # 💁🏻‍♂ E4.0 man tipping hand: light skin tone
+1F481 1F3FC 200D 2642 FE0F ; fully-qualified # 💁🏼‍♂️ E4.0 man tipping hand: medium-light skin tone
+1F481 1F3FC 200D 2642 ; minimally-qualified # 💁🏼‍♂ E4.0 man tipping hand: medium-light skin tone
+1F481 1F3FD 200D 2642 FE0F ; fully-qualified # 💁🏽‍♂️ E4.0 man tipping hand: medium skin tone
+1F481 1F3FD 200D 2642 ; minimally-qualified # 💁🏽‍♂ E4.0 man tipping hand: medium skin tone
+1F481 1F3FE 200D 2642 FE0F ; fully-qualified # 💁🏾‍♂️ E4.0 man tipping hand: medium-dark skin tone
+1F481 1F3FE 200D 2642 ; minimally-qualified # 💁🏾‍♂ E4.0 man tipping hand: medium-dark skin tone
+1F481 1F3FF 200D 2642 FE0F ; fully-qualified # 💁🏿‍♂️ E4.0 man tipping hand: dark skin tone
+1F481 1F3FF 200D 2642 ; minimally-qualified # 💁🏿‍♂ E4.0 man tipping hand: dark skin tone
+1F481 200D 2640 FE0F ; fully-qualified # 💁‍♀️ E4.0 woman tipping hand
+1F481 200D 2640 ; minimally-qualified # 💁‍♀ E4.0 woman tipping hand
+1F481 1F3FB 200D 2640 FE0F ; fully-qualified # 💁🏻‍♀️ E4.0 woman tipping hand: light skin tone
+1F481 1F3FB 200D 2640 ; minimally-qualified # 💁🏻‍♀ E4.0 woman tipping hand: light skin tone
+1F481 1F3FC 200D 2640 FE0F ; fully-qualified # 💁🏼‍♀️ E4.0 woman tipping hand: medium-light skin tone
+1F481 1F3FC 200D 2640 ; minimally-qualified # 💁🏼‍♀ E4.0 woman tipping hand: medium-light skin tone
+1F481 1F3FD 200D 2640 FE0F ; fully-qualified # 💁🏽‍♀️ E4.0 woman tipping hand: medium skin tone
+1F481 1F3FD 200D 2640 ; minimally-qualified # 💁🏽‍♀ E4.0 woman tipping hand: medium skin tone
+1F481 1F3FE 200D 2640 FE0F ; fully-qualified # 💁🏾‍♀️ E4.0 woman tipping hand: medium-dark skin tone
+1F481 1F3FE 200D 2640 ; minimally-qualified # 💁🏾‍♀ E4.0 woman tipping hand: medium-dark skin tone
+1F481 1F3FF 200D 2640 FE0F ; fully-qualified # 💁🏿‍♀️ E4.0 woman tipping hand: dark skin tone
+1F481 1F3FF 200D 2640 ; minimally-qualified # 💁🏿‍♀ E4.0 woman tipping hand: dark skin tone
+1F64B ; fully-qualified # 🙋 E0.6 person raising hand
+1F64B 1F3FB ; fully-qualified # 🙋🏻 E1.0 person raising hand: light skin tone
+1F64B 1F3FC ; fully-qualified # 🙋🏼 E1.0 person raising hand: medium-light skin tone
+1F64B 1F3FD ; fully-qualified # 🙋🏽 E1.0 person raising hand: medium skin tone
+1F64B 1F3FE ; fully-qualified # 🙋🏾 E1.0 person raising hand: medium-dark skin tone
+1F64B 1F3FF ; fully-qualified # 🙋🏿 E1.0 person raising hand: dark skin tone
+1F64B 200D 2642 FE0F ; fully-qualified # 🙋‍♂️ E4.0 man raising hand
+1F64B 200D 2642 ; minimally-qualified # 🙋‍♂ E4.0 man raising hand
+1F64B 1F3FB 200D 2642 FE0F ; fully-qualified # 🙋🏻‍♂️ E4.0 man raising hand: light skin tone
+1F64B 1F3FB 200D 2642 ; minimally-qualified # 🙋🏻‍♂ E4.0 man raising hand: light skin tone
+1F64B 1F3FC 200D 2642 FE0F ; fully-qualified # 🙋🏼‍♂️ E4.0 man raising hand: medium-light skin tone
+1F64B 1F3FC 200D 2642 ; minimally-qualified # 🙋🏼‍♂ E4.0 man raising hand: medium-light skin tone
+1F64B 1F3FD 200D 2642 FE0F ; fully-qualified # 🙋🏽‍♂️ E4.0 man raising hand: medium skin tone
+1F64B 1F3FD 200D 2642 ; minimally-qualified # 🙋🏽‍♂ E4.0 man raising hand: medium skin tone
+1F64B 1F3FE 200D 2642 FE0F ; fully-qualified # 🙋🏾‍♂️ E4.0 man raising hand: medium-dark skin tone
+1F64B 1F3FE 200D 2642 ; minimally-qualified # 🙋🏾‍♂ E4.0 man raising hand: medium-dark skin tone
+1F64B 1F3FF 200D 2642 FE0F ; fully-qualified # 🙋🏿‍♂️ E4.0 man raising hand: dark skin tone
+1F64B 1F3FF 200D 2642 ; minimally-qualified # 🙋🏿‍♂ E4.0 man raising hand: dark skin tone
+1F64B 200D 2640 FE0F ; fully-qualified # 🙋‍♀️ E4.0 woman raising hand
+1F64B 200D 2640 ; minimally-qualified # 🙋‍♀ E4.0 woman raising hand
+1F64B 1F3FB 200D 2640 FE0F ; fully-qualified # 🙋🏻‍♀️ E4.0 woman raising hand: light skin tone
+1F64B 1F3FB 200D 2640 ; minimally-qualified # 🙋🏻‍♀ E4.0 woman raising hand: light skin tone
+1F64B 1F3FC 200D 2640 FE0F ; fully-qualified # 🙋🏼‍♀️ E4.0 woman raising hand: medium-light skin tone
+1F64B 1F3FC 200D 2640 ; minimally-qualified # 🙋🏼‍♀ E4.0 woman raising hand: medium-light skin tone
+1F64B 1F3FD 200D 2640 FE0F ; fully-qualified # 🙋🏽‍♀️ E4.0 woman raising hand: medium skin tone
+1F64B 1F3FD 200D 2640 ; minimally-qualified # 🙋🏽‍♀ E4.0 woman raising hand: medium skin tone
+1F64B 1F3FE 200D 2640 FE0F ; fully-qualified # 🙋🏾‍♀️ E4.0 woman raising hand: medium-dark skin tone
+1F64B 1F3FE 200D 2640 ; minimally-qualified # 🙋🏾‍♀ E4.0 woman raising hand: medium-dark skin tone
+1F64B 1F3FF 200D 2640 FE0F ; fully-qualified # 🙋🏿‍♀️ E4.0 woman raising hand: dark skin tone
+1F64B 1F3FF 200D 2640 ; minimally-qualified # 🙋🏿‍♀ E4.0 woman raising hand: dark skin tone
+1F9CF ; fully-qualified # 🧏 E12.0 deaf person
+1F9CF 1F3FB ; fully-qualified # 🧏🏻 E12.0 deaf person: light skin tone
+1F9CF 1F3FC ; fully-qualified # 🧏🏼 E12.0 deaf person: medium-light skin tone
+1F9CF 1F3FD ; fully-qualified # 🧏🏽 E12.0 deaf person: medium skin tone
+1F9CF 1F3FE ; fully-qualified # 🧏🏾 E12.0 deaf person: medium-dark skin tone
+1F9CF 1F3FF ; fully-qualified # 🧏🏿 E12.0 deaf person: dark skin tone
+1F9CF 200D 2642 FE0F ; fully-qualified # 🧏‍♂️ E12.0 deaf man
+1F9CF 200D 2642 ; minimally-qualified # 🧏‍♂ E12.0 deaf man
+1F9CF 1F3FB 200D 2642 FE0F ; fully-qualified # 🧏🏻‍♂️ E12.0 deaf man: light skin tone
+1F9CF 1F3FB 200D 2642 ; minimally-qualified # 🧏🏻‍♂ E12.0 deaf man: light skin tone
+1F9CF 1F3FC 200D 2642 FE0F ; fully-qualified # 🧏🏼‍♂️ E12.0 deaf man: medium-light skin tone
+1F9CF 1F3FC 200D 2642 ; minimally-qualified # 🧏🏼‍♂ E12.0 deaf man: medium-light skin tone
+1F9CF 1F3FD 200D 2642 FE0F ; fully-qualified # 🧏🏽‍♂️ E12.0 deaf man: medium skin tone
+1F9CF 1F3FD 200D 2642 ; minimally-qualified # 🧏🏽‍♂ E12.0 deaf man: medium skin tone
+1F9CF 1F3FE 200D 2642 FE0F ; fully-qualified # 🧏🏾‍♂️ E12.0 deaf man: medium-dark skin tone
+1F9CF 1F3FE 200D 2642 ; minimally-qualified # 🧏🏾‍♂ E12.0 deaf man: medium-dark skin tone
+1F9CF 1F3FF 200D 2642 FE0F ; fully-qualified # 🧏🏿‍♂️ E12.0 deaf man: dark skin tone
+1F9CF 1F3FF 200D 2642 ; minimally-qualified # 🧏🏿‍♂ E12.0 deaf man: dark skin tone
+1F9CF 200D 2640 FE0F ; fully-qualified # 🧏‍♀️ E12.0 deaf woman
+1F9CF 200D 2640 ; minimally-qualified # 🧏‍♀ E12.0 deaf woman
+1F9CF 1F3FB 200D 2640 FE0F ; fully-qualified # 🧏🏻‍♀️ E12.0 deaf woman: light skin tone
+1F9CF 1F3FB 200D 2640 ; minimally-qualified # 🧏🏻‍♀ E12.0 deaf woman: light skin tone
+1F9CF 1F3FC 200D 2640 FE0F ; fully-qualified # 🧏🏼‍♀️ E12.0 deaf woman: medium-light skin tone
+1F9CF 1F3FC 200D 2640 ; minimally-qualified # 🧏🏼‍♀ E12.0 deaf woman: medium-light skin tone
+1F9CF 1F3FD 200D 2640 FE0F ; fully-qualified # 🧏🏽‍♀️ E12.0 deaf woman: medium skin tone
+1F9CF 1F3FD 200D 2640 ; minimally-qualified # 🧏🏽‍♀ E12.0 deaf woman: medium skin tone
+1F9CF 1F3FE 200D 2640 FE0F ; fully-qualified # 🧏🏾‍♀️ E12.0 deaf woman: medium-dark skin tone
+1F9CF 1F3FE 200D 2640 ; minimally-qualified # 🧏🏾‍♀ E12.0 deaf woman: medium-dark skin tone
+1F9CF 1F3FF 200D 2640 FE0F ; fully-qualified # 🧏🏿‍♀️ E12.0 deaf woman: dark skin tone
+1F9CF 1F3FF 200D 2640 ; minimally-qualified # 🧏🏿‍♀ E12.0 deaf woman: dark skin tone
+1F647 ; fully-qualified # 🙇 E0.6 person bowing
+1F647 1F3FB ; fully-qualified # 🙇🏻 E1.0 person bowing: light skin tone
+1F647 1F3FC ; fully-qualified # 🙇🏼 E1.0 person bowing: medium-light skin tone
+1F647 1F3FD ; fully-qualified # 🙇🏽 E1.0 person bowing: medium skin tone
+1F647 1F3FE ; fully-qualified # 🙇🏾 E1.0 person bowing: medium-dark skin tone
+1F647 1F3FF ; fully-qualified # 🙇🏿 E1.0 person bowing: dark skin tone
+1F647 200D 2642 FE0F ; fully-qualified # 🙇‍♂️ E4.0 man bowing
+1F647 200D 2642 ; minimally-qualified # 🙇‍♂ E4.0 man bowing
+1F647 1F3FB 200D 2642 FE0F ; fully-qualified # 🙇🏻‍♂️ E4.0 man bowing: light skin tone
+1F647 1F3FB 200D 2642 ; minimally-qualified # 🙇🏻‍♂ E4.0 man bowing: light skin tone
+1F647 1F3FC 200D 2642 FE0F ; fully-qualified # 🙇🏼‍♂️ E4.0 man bowing: medium-light skin tone
+1F647 1F3FC 200D 2642 ; minimally-qualified # 🙇🏼‍♂ E4.0 man bowing: medium-light skin tone
+1F647 1F3FD 200D 2642 FE0F ; fully-qualified # 🙇🏽‍♂️ E4.0 man bowing: medium skin tone
+1F647 1F3FD 200D 2642 ; minimally-qualified # 🙇🏽‍♂ E4.0 man bowing: medium skin tone
+1F647 1F3FE 200D 2642 FE0F ; fully-qualified # 🙇🏾‍♂️ E4.0 man bowing: medium-dark skin tone
+1F647 1F3FE 200D 2642 ; minimally-qualified # 🙇🏾‍♂ E4.0 man bowing: medium-dark skin tone
+1F647 1F3FF 200D 2642 FE0F ; fully-qualified # 🙇🏿‍♂️ E4.0 man bowing: dark skin tone
+1F647 1F3FF 200D 2642 ; minimally-qualified # 🙇🏿‍♂ E4.0 man bowing: dark skin tone
+1F647 200D 2640 FE0F ; fully-qualified # 🙇‍♀️ E4.0 woman bowing
+1F647 200D 2640 ; minimally-qualified # 🙇‍♀ E4.0 woman bowing
+1F647 1F3FB 200D 2640 FE0F ; fully-qualified # 🙇🏻‍♀️ E4.0 woman bowing: light skin tone
+1F647 1F3FB 200D 2640 ; minimally-qualified # 🙇🏻‍♀ E4.0 woman bowing: light skin tone
+1F647 1F3FC 200D 2640 FE0F ; fully-qualified # 🙇🏼‍♀️ E4.0 woman bowing: medium-light skin tone
+1F647 1F3FC 200D 2640 ; minimally-qualified # 🙇🏼‍♀ E4.0 woman bowing: medium-light skin tone
+1F647 1F3FD 200D 2640 FE0F ; fully-qualified # 🙇🏽‍♀️ E4.0 woman bowing: medium skin tone
+1F647 1F3FD 200D 2640 ; minimally-qualified # 🙇🏽‍♀ E4.0 woman bowing: medium skin tone
+1F647 1F3FE 200D 2640 FE0F ; fully-qualified # 🙇🏾‍♀️ E4.0 woman bowing: medium-dark skin tone
+1F647 1F3FE 200D 2640 ; minimally-qualified # 🙇🏾‍♀ E4.0 woman bowing: medium-dark skin tone
+1F647 1F3FF 200D 2640 FE0F ; fully-qualified # 🙇🏿‍♀️ E4.0 woman bowing: dark skin tone
+1F647 1F3FF 200D 2640 ; minimally-qualified # 🙇🏿‍♀ E4.0 woman bowing: dark skin tone
+1F926 ; fully-qualified # 🤦 E3.0 person facepalming
+1F926 1F3FB ; fully-qualified # 🤦🏻 E3.0 person facepalming: light skin tone
+1F926 1F3FC ; fully-qualified # 🤦🏼 E3.0 person facepalming: medium-light skin tone
+1F926 1F3FD ; fully-qualified # 🤦🏽 E3.0 person facepalming: medium skin tone
+1F926 1F3FE ; fully-qualified # 🤦🏾 E3.0 person facepalming: medium-dark skin tone
+1F926 1F3FF ; fully-qualified # 🤦🏿 E3.0 person facepalming: dark skin tone
+1F926 200D 2642 FE0F ; fully-qualified # 🤦‍♂️ E4.0 man facepalming
+1F926 200D 2642 ; minimally-qualified # 🤦‍♂ E4.0 man facepalming
+1F926 1F3FB 200D 2642 FE0F ; fully-qualified # 🤦🏻‍♂️ E4.0 man facepalming: light skin tone
+1F926 1F3FB 200D 2642 ; minimally-qualified # 🤦🏻‍♂ E4.0 man facepalming: light skin tone
+1F926 1F3FC 200D 2642 FE0F ; fully-qualified # 🤦🏼‍♂️ E4.0 man facepalming: medium-light skin tone
+1F926 1F3FC 200D 2642 ; minimally-qualified # 🤦🏼‍♂ E4.0 man facepalming: medium-light skin tone
+1F926 1F3FD 200D 2642 FE0F ; fully-qualified # 🤦🏽‍♂️ E4.0 man facepalming: medium skin tone
+1F926 1F3FD 200D 2642 ; minimally-qualified # 🤦🏽‍♂ E4.0 man facepalming: medium skin tone
+1F926 1F3FE 200D 2642 FE0F ; fully-qualified # 🤦🏾‍♂️ E4.0 man facepalming: medium-dark skin tone
+1F926 1F3FE 200D 2642 ; minimally-qualified # 🤦🏾‍♂ E4.0 man facepalming: medium-dark skin tone
+1F926 1F3FF 200D 2642 FE0F ; fully-qualified # 🤦🏿‍♂️ E4.0 man facepalming: dark skin tone
+1F926 1F3FF 200D 2642 ; minimally-qualified # 🤦🏿‍♂ E4.0 man facepalming: dark skin tone
+1F926 200D 2640 FE0F ; fully-qualified # 🤦‍♀️ E4.0 woman facepalming
+1F926 200D 2640 ; minimally-qualified # 🤦‍♀ E4.0 woman facepalming
+1F926 1F3FB 200D 2640 FE0F ; fully-qualified # 🤦🏻‍♀️ E4.0 woman facepalming: light skin tone
+1F926 1F3FB 200D 2640 ; minimally-qualified # 🤦🏻‍♀ E4.0 woman facepalming: light skin tone
+1F926 1F3FC 200D 2640 FE0F ; fully-qualified # 🤦🏼‍♀️ E4.0 woman facepalming: medium-light skin tone
+1F926 1F3FC 200D 2640 ; minimally-qualified # 🤦🏼‍♀ E4.0 woman facepalming: medium-light skin tone
+1F926 1F3FD 200D 2640 FE0F ; fully-qualified # 🤦🏽‍♀️ E4.0 woman facepalming: medium skin tone
+1F926 1F3FD 200D 2640 ; minimally-qualified # 🤦🏽‍♀ E4.0 woman facepalming: medium skin tone
+1F926 1F3FE 200D 2640 FE0F ; fully-qualified # 🤦🏾‍♀️ E4.0 woman facepalming: medium-dark skin tone
+1F926 1F3FE 200D 2640 ; minimally-qualified # 🤦🏾‍♀ E4.0 woman facepalming: medium-dark skin tone
+1F926 1F3FF 200D 2640 FE0F ; fully-qualified # 🤦🏿‍♀️ E4.0 woman facepalming: dark skin tone
+1F926 1F3FF 200D 2640 ; minimally-qualified # 🤦🏿‍♀ E4.0 woman facepalming: dark skin tone
+1F937 ; fully-qualified # 🤷 E3.0 person shrugging
+1F937 1F3FB ; fully-qualified # 🤷🏻 E3.0 person shrugging: light skin tone
+1F937 1F3FC ; fully-qualified # 🤷🏼 E3.0 person shrugging: medium-light skin tone
+1F937 1F3FD ; fully-qualified # 🤷🏽 E3.0 person shrugging: medium skin tone
+1F937 1F3FE ; fully-qualified # 🤷🏾 E3.0 person shrugging: medium-dark skin tone
+1F937 1F3FF ; fully-qualified # 🤷🏿 E3.0 person shrugging: dark skin tone
+1F937 200D 2642 FE0F ; fully-qualified # 🤷‍♂️ E4.0 man shrugging
+1F937 200D 2642 ; minimally-qualified # 🤷‍♂ E4.0 man shrugging
+1F937 1F3FB 200D 2642 FE0F ; fully-qualified # 🤷🏻‍♂️ E4.0 man shrugging: light skin tone
+1F937 1F3FB 200D 2642 ; minimally-qualified # 🤷🏻‍♂ E4.0 man shrugging: light skin tone
+1F937 1F3FC 200D 2642 FE0F ; fully-qualified # 🤷🏼‍♂️ E4.0 man shrugging: medium-light skin tone
+1F937 1F3FC 200D 2642 ; minimally-qualified # 🤷🏼‍♂ E4.0 man shrugging: medium-light skin tone
+1F937 1F3FD 200D 2642 FE0F ; fully-qualified # 🤷🏽‍♂️ E4.0 man shrugging: medium skin tone
+1F937 1F3FD 200D 2642 ; minimally-qualified # 🤷🏽‍♂ E4.0 man shrugging: medium skin tone
+1F937 1F3FE 200D 2642 FE0F ; fully-qualified # 🤷🏾‍♂️ E4.0 man shrugging: medium-dark skin tone
+1F937 1F3FE 200D 2642 ; minimally-qualified # 🤷🏾‍♂ E4.0 man shrugging: medium-dark skin tone
+1F937 1F3FF 200D 2642 FE0F ; fully-qualified # 🤷🏿‍♂️ E4.0 man shrugging: dark skin tone
+1F937 1F3FF 200D 2642 ; minimally-qualified # 🤷🏿‍♂ E4.0 man shrugging: dark skin tone
+1F937 200D 2640 FE0F ; fully-qualified # 🤷‍♀️ E4.0 woman shrugging
+1F937 200D 2640 ; minimally-qualified # 🤷‍♀ E4.0 woman shrugging
+1F937 1F3FB 200D 2640 FE0F ; fully-qualified # 🤷🏻‍♀️ E4.0 woman shrugging: light skin tone
+1F937 1F3FB 200D 2640 ; minimally-qualified # 🤷🏻‍♀ E4.0 woman shrugging: light skin tone
+1F937 1F3FC 200D 2640 FE0F ; fully-qualified # 🤷🏼‍♀️ E4.0 woman shrugging: medium-light skin tone
+1F937 1F3FC 200D 2640 ; minimally-qualified # 🤷🏼‍♀ E4.0 woman shrugging: medium-light skin tone
+1F937 1F3FD 200D 2640 FE0F ; fully-qualified # 🤷🏽‍♀️ E4.0 woman shrugging: medium skin tone
+1F937 1F3FD 200D 2640 ; minimally-qualified # 🤷🏽‍♀ E4.0 woman shrugging: medium skin tone
+1F937 1F3FE 200D 2640 FE0F ; fully-qualified # 🤷🏾‍♀️ E4.0 woman shrugging: medium-dark skin tone
+1F937 1F3FE 200D 2640 ; minimally-qualified # 🤷🏾‍♀ E4.0 woman shrugging: medium-dark skin tone
+1F937 1F3FF 200D 2640 FE0F ; fully-qualified # 🤷🏿‍♀️ E4.0 woman shrugging: dark skin tone
+1F937 1F3FF 200D 2640 ; minimally-qualified # 🤷🏿‍♀ E4.0 woman shrugging: dark skin tone
+
+# subgroup: person-role
+1F9D1 200D 2695 FE0F ; fully-qualified # 🧑‍⚕️ E12.1 health worker
+1F9D1 200D 2695 ; minimally-qualified # 🧑‍⚕ E12.1 health worker
+1F9D1 1F3FB 200D 2695 FE0F ; fully-qualified # 🧑🏻‍⚕️ E12.1 health worker: light skin tone
+1F9D1 1F3FB 200D 2695 ; minimally-qualified # 🧑🏻‍⚕ E12.1 health worker: light skin tone
+1F9D1 1F3FC 200D 2695 FE0F ; fully-qualified # 🧑🏼‍⚕️ E12.1 health worker: medium-light skin tone
+1F9D1 1F3FC 200D 2695 ; minimally-qualified # 🧑🏼‍⚕ E12.1 health worker: medium-light skin tone
+1F9D1 1F3FD 200D 2695 FE0F ; fully-qualified # 🧑🏽‍⚕️ E12.1 health worker: medium skin tone
+1F9D1 1F3FD 200D 2695 ; minimally-qualified # 🧑🏽‍⚕ E12.1 health worker: medium skin tone
+1F9D1 1F3FE 200D 2695 FE0F ; fully-qualified # 🧑🏾‍⚕️ E12.1 health worker: medium-dark skin tone
+1F9D1 1F3FE 200D 2695 ; minimally-qualified # 🧑🏾‍⚕ E12.1 health worker: medium-dark skin tone
+1F9D1 1F3FF 200D 2695 FE0F ; fully-qualified # 🧑🏿‍⚕️ E12.1 health worker: dark skin tone
+1F9D1 1F3FF 200D 2695 ; minimally-qualified # 🧑🏿‍⚕ E12.1 health worker: dark skin tone
+1F468 200D 2695 FE0F ; fully-qualified # 👨‍⚕️ E4.0 man health worker
+1F468 200D 2695 ; minimally-qualified # 👨‍⚕ E4.0 man health worker
+1F468 1F3FB 200D 2695 FE0F ; fully-qualified # 👨🏻‍⚕️ E4.0 man health worker: light skin tone
+1F468 1F3FB 200D 2695 ; minimally-qualified # 👨🏻‍⚕ E4.0 man health worker: light skin tone
+1F468 1F3FC 200D 2695 FE0F ; fully-qualified # 👨🏼‍⚕️ E4.0 man health worker: medium-light skin tone
+1F468 1F3FC 200D 2695 ; minimally-qualified # 👨🏼‍⚕ E4.0 man health worker: medium-light skin tone
+1F468 1F3FD 200D 2695 FE0F ; fully-qualified # 👨🏽‍⚕️ E4.0 man health worker: medium skin tone
+1F468 1F3FD 200D 2695 ; minimally-qualified # 👨🏽‍⚕ E4.0 man health worker: medium skin tone
+1F468 1F3FE 200D 2695 FE0F ; fully-qualified # 👨🏾‍⚕️ E4.0 man health worker: medium-dark skin tone
+1F468 1F3FE 200D 2695 ; minimally-qualified # 👨🏾‍⚕ E4.0 man health worker: medium-dark skin tone
+1F468 1F3FF 200D 2695 FE0F ; fully-qualified # 👨🏿‍⚕️ E4.0 man health worker: dark skin tone
+1F468 1F3FF 200D 2695 ; minimally-qualified # 👨🏿‍⚕ E4.0 man health worker: dark skin tone
+1F469 200D 2695 FE0F ; fully-qualified # 👩‍⚕️ E4.0 woman health worker
+1F469 200D 2695 ; minimally-qualified # 👩‍⚕ E4.0 woman health worker
+1F469 1F3FB 200D 2695 FE0F ; fully-qualified # 👩🏻‍⚕️ E4.0 woman health worker: light skin tone
+1F469 1F3FB 200D 2695 ; minimally-qualified # 👩🏻‍⚕ E4.0 woman health worker: light skin tone
+1F469 1F3FC 200D 2695 FE0F ; fully-qualified # 👩🏼‍⚕️ E4.0 woman health worker: medium-light skin tone
+1F469 1F3FC 200D 2695 ; minimally-qualified # 👩🏼‍⚕ E4.0 woman health worker: medium-light skin tone
+1F469 1F3FD 200D 2695 FE0F ; fully-qualified # 👩🏽‍⚕️ E4.0 woman health worker: medium skin tone
+1F469 1F3FD 200D 2695 ; minimally-qualified # 👩🏽‍⚕ E4.0 woman health worker: medium skin tone
+1F469 1F3FE 200D 2695 FE0F ; fully-qualified # 👩🏾‍⚕️ E4.0 woman health worker: medium-dark skin tone
+1F469 1F3FE 200D 2695 ; minimally-qualified # 👩🏾‍⚕ E4.0 woman health worker: medium-dark skin tone
+1F469 1F3FF 200D 2695 FE0F ; fully-qualified # 👩🏿‍⚕️ E4.0 woman health worker: dark skin tone
+1F469 1F3FF 200D 2695 ; minimally-qualified # 👩🏿‍⚕ E4.0 woman health worker: dark skin tone
+1F9D1 200D 1F393 ; fully-qualified # 🧑‍🎓 E12.1 student
+1F9D1 1F3FB 200D 1F393 ; fully-qualified # 🧑🏻‍🎓 E12.1 student: light skin tone
+1F9D1 1F3FC 200D 1F393 ; fully-qualified # 🧑🏼‍🎓 E12.1 student: medium-light skin tone
+1F9D1 1F3FD 200D 1F393 ; fully-qualified # 🧑🏽‍🎓 E12.1 student: medium skin tone
+1F9D1 1F3FE 200D 1F393 ; fully-qualified # 🧑🏾‍🎓 E12.1 student: medium-dark skin tone
+1F9D1 1F3FF 200D 1F393 ; fully-qualified # 🧑🏿‍🎓 E12.1 student: dark skin tone
+1F468 200D 1F393 ; fully-qualified # 👨‍🎓 E4.0 man student
+1F468 1F3FB 200D 1F393 ; fully-qualified # 👨🏻‍🎓 E4.0 man student: light skin tone
+1F468 1F3FC 200D 1F393 ; fully-qualified # 👨🏼‍🎓 E4.0 man student: medium-light skin tone
+1F468 1F3FD 200D 1F393 ; fully-qualified # 👨🏽‍🎓 E4.0 man student: medium skin tone
+1F468 1F3FE 200D 1F393 ; fully-qualified # 👨🏾‍🎓 E4.0 man student: medium-dark skin tone
+1F468 1F3FF 200D 1F393 ; fully-qualified # 👨🏿‍🎓 E4.0 man student: dark skin tone
+1F469 200D 1F393 ; fully-qualified # 👩‍🎓 E4.0 woman student
+1F469 1F3FB 200D 1F393 ; fully-qualified # 👩🏻‍🎓 E4.0 woman student: light skin tone
+1F469 1F3FC 200D 1F393 ; fully-qualified # 👩🏼‍🎓 E4.0 woman student: medium-light skin tone
+1F469 1F3FD 200D 1F393 ; fully-qualified # 👩🏽‍🎓 E4.0 woman student: medium skin tone
+1F469 1F3FE 200D 1F393 ; fully-qualified # 👩🏾‍🎓 E4.0 woman student: medium-dark skin tone
+1F469 1F3FF 200D 1F393 ; fully-qualified # 👩🏿‍🎓 E4.0 woman student: dark skin tone
+1F9D1 200D 1F3EB ; fully-qualified # 🧑‍🏫 E12.1 teacher
+1F9D1 1F3FB 200D 1F3EB ; fully-qualified # 🧑🏻‍🏫 E12.1 teacher: light skin tone
+1F9D1 1F3FC 200D 1F3EB ; fully-qualified # 🧑🏼‍🏫 E12.1 teacher: medium-light skin tone
+1F9D1 1F3FD 200D 1F3EB ; fully-qualified # 🧑🏽‍🏫 E12.1 teacher: medium skin tone
+1F9D1 1F3FE 200D 1F3EB ; fully-qualified # 🧑🏾‍🏫 E12.1 teacher: medium-dark skin tone
+1F9D1 1F3FF 200D 1F3EB ; fully-qualified # 🧑🏿‍🏫 E12.1 teacher: dark skin tone
+1F468 200D 1F3EB ; fully-qualified # 👨‍🏫 E4.0 man teacher
+1F468 1F3FB 200D 1F3EB ; fully-qualified # 👨🏻‍🏫 E4.0 man teacher: light skin tone
+1F468 1F3FC 200D 1F3EB ; fully-qualified # 👨🏼‍🏫 E4.0 man teacher: medium-light skin tone
+1F468 1F3FD 200D 1F3EB ; fully-qualified # 👨🏽‍🏫 E4.0 man teacher: medium skin tone
+1F468 1F3FE 200D 1F3EB ; fully-qualified # 👨🏾‍🏫 E4.0 man teacher: medium-dark skin tone
+1F468 1F3FF 200D 1F3EB ; fully-qualified # 👨🏿‍🏫 E4.0 man teacher: dark skin tone
+1F469 200D 1F3EB ; fully-qualified # 👩‍🏫 E4.0 woman teacher
+1F469 1F3FB 200D 1F3EB ; fully-qualified # 👩🏻‍🏫 E4.0 woman teacher: light skin tone
+1F469 1F3FC 200D 1F3EB ; fully-qualified # 👩🏼‍🏫 E4.0 woman teacher: medium-light skin tone
+1F469 1F3FD 200D 1F3EB ; fully-qualified # 👩🏽‍🏫 E4.0 woman teacher: medium skin tone
+1F469 1F3FE 200D 1F3EB ; fully-qualified # 👩🏾‍🏫 E4.0 woman teacher: medium-dark skin tone
+1F469 1F3FF 200D 1F3EB ; fully-qualified # 👩🏿‍🏫 E4.0 woman teacher: dark skin tone
+1F9D1 200D 2696 FE0F ; fully-qualified # 🧑‍⚖️ E12.1 judge
+1F9D1 200D 2696 ; minimally-qualified # 🧑‍⚖ E12.1 judge
+1F9D1 1F3FB 200D 2696 FE0F ; fully-qualified # 🧑🏻‍⚖️ E12.1 judge: light skin tone
+1F9D1 1F3FB 200D 2696 ; minimally-qualified # 🧑🏻‍⚖ E12.1 judge: light skin tone
+1F9D1 1F3FC 200D 2696 FE0F ; fully-qualified # 🧑🏼‍⚖️ E12.1 judge: medium-light skin tone
+1F9D1 1F3FC 200D 2696 ; minimally-qualified # 🧑🏼‍⚖ E12.1 judge: medium-light skin tone
+1F9D1 1F3FD 200D 2696 FE0F ; fully-qualified # 🧑🏽‍⚖️ E12.1 judge: medium skin tone
+1F9D1 1F3FD 200D 2696 ; minimally-qualified # 🧑🏽‍⚖ E12.1 judge: medium skin tone
+1F9D1 1F3FE 200D 2696 FE0F ; fully-qualified # 🧑🏾‍⚖️ E12.1 judge: medium-dark skin tone
+1F9D1 1F3FE 200D 2696 ; minimally-qualified # 🧑🏾‍⚖ E12.1 judge: medium-dark skin tone
+1F9D1 1F3FF 200D 2696 FE0F ; fully-qualified # 🧑🏿‍⚖️ E12.1 judge: dark skin tone
+1F9D1 1F3FF 200D 2696 ; minimally-qualified # 🧑🏿‍⚖ E12.1 judge: dark skin tone
+1F468 200D 2696 FE0F ; fully-qualified # 👨‍⚖️ E4.0 man judge
+1F468 200D 2696 ; minimally-qualified # 👨‍⚖ E4.0 man judge
+1F468 1F3FB 200D 2696 FE0F ; fully-qualified # 👨🏻‍⚖️ E4.0 man judge: light skin tone
+1F468 1F3FB 200D 2696 ; minimally-qualified # 👨🏻‍⚖ E4.0 man judge: light skin tone
+1F468 1F3FC 200D 2696 FE0F ; fully-qualified # 👨🏼‍⚖️ E4.0 man judge: medium-light skin tone
+1F468 1F3FC 200D 2696 ; minimally-qualified # 👨🏼‍⚖ E4.0 man judge: medium-light skin tone
+1F468 1F3FD 200D 2696 FE0F ; fully-qualified # 👨🏽‍⚖️ E4.0 man judge: medium skin tone
+1F468 1F3FD 200D 2696 ; minimally-qualified # 👨🏽‍⚖ E4.0 man judge: medium skin tone
+1F468 1F3FE 200D 2696 FE0F ; fully-qualified # 👨🏾‍⚖️ E4.0 man judge: medium-dark skin tone
+1F468 1F3FE 200D 2696 ; minimally-qualified # 👨🏾‍⚖ E4.0 man judge: medium-dark skin tone
+1F468 1F3FF 200D 2696 FE0F ; fully-qualified # 👨🏿‍⚖️ E4.0 man judge: dark skin tone
+1F468 1F3FF 200D 2696 ; minimally-qualified # 👨🏿‍⚖ E4.0 man judge: dark skin tone
+1F469 200D 2696 FE0F ; fully-qualified # 👩‍⚖️ E4.0 woman judge
+1F469 200D 2696 ; minimally-qualified # 👩‍⚖ E4.0 woman judge
+1F469 1F3FB 200D 2696 FE0F ; fully-qualified # 👩🏻‍⚖️ E4.0 woman judge: light skin tone
+1F469 1F3FB 200D 2696 ; minimally-qualified # 👩🏻‍⚖ E4.0 woman judge: light skin tone
+1F469 1F3FC 200D 2696 FE0F ; fully-qualified # 👩🏼‍⚖️ E4.0 woman judge: medium-light skin tone
+1F469 1F3FC 200D 2696 ; minimally-qualified # 👩🏼‍⚖ E4.0 woman judge: medium-light skin tone
+1F469 1F3FD 200D 2696 FE0F ; fully-qualified # 👩🏽‍⚖️ E4.0 woman judge: medium skin tone
+1F469 1F3FD 200D 2696 ; minimally-qualified # 👩🏽‍⚖ E4.0 woman judge: medium skin tone
+1F469 1F3FE 200D 2696 FE0F ; fully-qualified # 👩🏾‍⚖️ E4.0 woman judge: medium-dark skin tone
+1F469 1F3FE 200D 2696 ; minimally-qualified # 👩🏾‍⚖ E4.0 woman judge: medium-dark skin tone
+1F469 1F3FF 200D 2696 FE0F ; fully-qualified # 👩🏿‍⚖️ E4.0 woman judge: dark skin tone
+1F469 1F3FF 200D 2696 ; minimally-qualified # 👩🏿‍⚖ E4.0 woman judge: dark skin tone
+1F9D1 200D 1F33E ; fully-qualified # 🧑‍🌾 E12.1 farmer
+1F9D1 1F3FB 200D 1F33E ; fully-qualified # 🧑🏻‍🌾 E12.1 farmer: light skin tone
+1F9D1 1F3FC 200D 1F33E ; fully-qualified # 🧑🏼‍🌾 E12.1 farmer: medium-light skin tone
+1F9D1 1F3FD 200D 1F33E ; fully-qualified # 🧑🏽‍🌾 E12.1 farmer: medium skin tone
+1F9D1 1F3FE 200D 1F33E ; fully-qualified # 🧑🏾‍🌾 E12.1 farmer: medium-dark skin tone
+1F9D1 1F3FF 200D 1F33E ; fully-qualified # 🧑🏿‍🌾 E12.1 farmer: dark skin tone
+1F468 200D 1F33E ; fully-qualified # 👨‍🌾 E4.0 man farmer
+1F468 1F3FB 200D 1F33E ; fully-qualified # 👨🏻‍🌾 E4.0 man farmer: light skin tone
+1F468 1F3FC 200D 1F33E ; fully-qualified # 👨🏼‍🌾 E4.0 man farmer: medium-light skin tone
+1F468 1F3FD 200D 1F33E ; fully-qualified # 👨🏽‍🌾 E4.0 man farmer: medium skin tone
+1F468 1F3FE 200D 1F33E ; fully-qualified # 👨🏾‍🌾 E4.0 man farmer: medium-dark skin tone
+1F468 1F3FF 200D 1F33E ; fully-qualified # 👨🏿‍🌾 E4.0 man farmer: dark skin tone
+1F469 200D 1F33E ; fully-qualified # 👩‍🌾 E4.0 woman farmer
+1F469 1F3FB 200D 1F33E ; fully-qualified # 👩🏻‍🌾 E4.0 woman farmer: light skin tone
+1F469 1F3FC 200D 1F33E ; fully-qualified # 👩🏼‍🌾 E4.0 woman farmer: medium-light skin tone
+1F469 1F3FD 200D 1F33E ; fully-qualified # 👩🏽‍🌾 E4.0 woman farmer: medium skin tone
+1F469 1F3FE 200D 1F33E ; fully-qualified # 👩🏾‍🌾 E4.0 woman farmer: medium-dark skin tone
+1F469 1F3FF 200D 1F33E ; fully-qualified # 👩🏿‍🌾 E4.0 woman farmer: dark skin tone
+1F9D1 200D 1F373 ; fully-qualified # 🧑‍🍳 E12.1 cook
+1F9D1 1F3FB 200D 1F373 ; fully-qualified # 🧑🏻‍🍳 E12.1 cook: light skin tone
+1F9D1 1F3FC 200D 1F373 ; fully-qualified # 🧑🏼‍🍳 E12.1 cook: medium-light skin tone
+1F9D1 1F3FD 200D 1F373 ; fully-qualified # 🧑🏽‍🍳 E12.1 cook: medium skin tone
+1F9D1 1F3FE 200D 1F373 ; fully-qualified # 🧑🏾‍🍳 E12.1 cook: medium-dark skin tone
+1F9D1 1F3FF 200D 1F373 ; fully-qualified # 🧑🏿‍🍳 E12.1 cook: dark skin tone
+1F468 200D 1F373 ; fully-qualified # 👨‍🍳 E4.0 man cook
+1F468 1F3FB 200D 1F373 ; fully-qualified # 👨🏻‍🍳 E4.0 man cook: light skin tone
+1F468 1F3FC 200D 1F373 ; fully-qualified # 👨🏼‍🍳 E4.0 man cook: medium-light skin tone
+1F468 1F3FD 200D 1F373 ; fully-qualified # 👨🏽‍🍳 E4.0 man cook: medium skin tone
+1F468 1F3FE 200D 1F373 ; fully-qualified # 👨🏾‍🍳 E4.0 man cook: medium-dark skin tone
+1F468 1F3FF 200D 1F373 ; fully-qualified # 👨🏿‍🍳 E4.0 man cook: dark skin tone
+1F469 200D 1F373 ; fully-qualified # 👩‍🍳 E4.0 woman cook
+1F469 1F3FB 200D 1F373 ; fully-qualified # 👩🏻‍🍳 E4.0 woman cook: light skin tone
+1F469 1F3FC 200D 1F373 ; fully-qualified # 👩🏼‍🍳 E4.0 woman cook: medium-light skin tone
+1F469 1F3FD 200D 1F373 ; fully-qualified # 👩🏽‍🍳 E4.0 woman cook: medium skin tone
+1F469 1F3FE 200D 1F373 ; fully-qualified # 👩🏾‍🍳 E4.0 woman cook: medium-dark skin tone
+1F469 1F3FF 200D 1F373 ; fully-qualified # 👩🏿‍🍳 E4.0 woman cook: dark skin tone
+1F9D1 200D 1F527 ; fully-qualified # 🧑‍🔧 E12.1 mechanic
+1F9D1 1F3FB 200D 1F527 ; fully-qualified # 🧑🏻‍🔧 E12.1 mechanic: light skin tone
+1F9D1 1F3FC 200D 1F527 ; fully-qualified # 🧑🏼‍🔧 E12.1 mechanic: medium-light skin tone
+1F9D1 1F3FD 200D 1F527 ; fully-qualified # 🧑🏽‍🔧 E12.1 mechanic: medium skin tone
+1F9D1 1F3FE 200D 1F527 ; fully-qualified # 🧑🏾‍🔧 E12.1 mechanic: medium-dark skin tone
+1F9D1 1F3FF 200D 1F527 ; fully-qualified # 🧑🏿‍🔧 E12.1 mechanic: dark skin tone
+1F468 200D 1F527 ; fully-qualified # 👨‍🔧 E4.0 man mechanic
+1F468 1F3FB 200D 1F527 ; fully-qualified # 👨🏻‍🔧 E4.0 man mechanic: light skin tone
+1F468 1F3FC 200D 1F527 ; fully-qualified # 👨🏼‍🔧 E4.0 man mechanic: medium-light skin tone
+1F468 1F3FD 200D 1F527 ; fully-qualified # 👨🏽‍🔧 E4.0 man mechanic: medium skin tone
+1F468 1F3FE 200D 1F527 ; fully-qualified # 👨🏾‍🔧 E4.0 man mechanic: medium-dark skin tone
+1F468 1F3FF 200D 1F527 ; fully-qualified # 👨🏿‍🔧 E4.0 man mechanic: dark skin tone
+1F469 200D 1F527 ; fully-qualified # 👩‍🔧 E4.0 woman mechanic
+1F469 1F3FB 200D 1F527 ; fully-qualified # 👩🏻‍🔧 E4.0 woman mechanic: light skin tone
+1F469 1F3FC 200D 1F527 ; fully-qualified # 👩🏼‍🔧 E4.0 woman mechanic: medium-light skin tone
+1F469 1F3FD 200D 1F527 ; fully-qualified # 👩🏽‍🔧 E4.0 woman mechanic: medium skin tone
+1F469 1F3FE 200D 1F527 ; fully-qualified # 👩🏾‍🔧 E4.0 woman mechanic: medium-dark skin tone
+1F469 1F3FF 200D 1F527 ; fully-qualified # 👩🏿‍🔧 E4.0 woman mechanic: dark skin tone
+1F9D1 200D 1F3ED ; fully-qualified # 🧑‍🏭 E12.1 factory worker
+1F9D1 1F3FB 200D 1F3ED ; fully-qualified # 🧑🏻‍🏭 E12.1 factory worker: light skin tone
+1F9D1 1F3FC 200D 1F3ED ; fully-qualified # 🧑🏼‍🏭 E12.1 factory worker: medium-light skin tone
+1F9D1 1F3FD 200D 1F3ED ; fully-qualified # 🧑🏽‍🏭 E12.1 factory worker: medium skin tone
+1F9D1 1F3FE 200D 1F3ED ; fully-qualified # 🧑🏾‍🏭 E12.1 factory worker: medium-dark skin tone
+1F9D1 1F3FF 200D 1F3ED ; fully-qualified # 🧑🏿‍🏭 E12.1 factory worker: dark skin tone
+1F468 200D 1F3ED ; fully-qualified # 👨‍🏭 E4.0 man factory worker
+1F468 1F3FB 200D 1F3ED ; fully-qualified # 👨🏻‍🏭 E4.0 man factory worker: light skin tone
+1F468 1F3FC 200D 1F3ED ; fully-qualified # 👨🏼‍🏭 E4.0 man factory worker: medium-light skin tone
+1F468 1F3FD 200D 1F3ED ; fully-qualified # 👨🏽‍🏭 E4.0 man factory worker: medium skin tone
+1F468 1F3FE 200D 1F3ED ; fully-qualified # 👨🏾‍🏭 E4.0 man factory worker: medium-dark skin tone
+1F468 1F3FF 200D 1F3ED ; fully-qualified # 👨🏿‍🏭 E4.0 man factory worker: dark skin tone
+1F469 200D 1F3ED ; fully-qualified # 👩‍🏭 E4.0 woman factory worker
+1F469 1F3FB 200D 1F3ED ; fully-qualified # 👩🏻‍🏭 E4.0 woman factory worker: light skin tone
+1F469 1F3FC 200D 1F3ED ; fully-qualified # 👩🏼‍🏭 E4.0 woman factory worker: medium-light skin tone
+1F469 1F3FD 200D 1F3ED ; fully-qualified # 👩🏽‍🏭 E4.0 woman factory worker: medium skin tone
+1F469 1F3FE 200D 1F3ED ; fully-qualified # 👩🏾‍🏭 E4.0 woman factory worker: medium-dark skin tone
+1F469 1F3FF 200D 1F3ED ; fully-qualified # 👩🏿‍🏭 E4.0 woman factory worker: dark skin tone
+1F9D1 200D 1F4BC ; fully-qualified # 🧑‍💼 E12.1 office worker
+1F9D1 1F3FB 200D 1F4BC ; fully-qualified # 🧑🏻‍💼 E12.1 office worker: light skin tone
+1F9D1 1F3FC 200D 1F4BC ; fully-qualified # 🧑🏼‍💼 E12.1 office worker: medium-light skin tone
+1F9D1 1F3FD 200D 1F4BC ; fully-qualified # 🧑🏽‍💼 E12.1 office worker: medium skin tone
+1F9D1 1F3FE 200D 1F4BC ; fully-qualified # 🧑🏾‍💼 E12.1 office worker: medium-dark skin tone
+1F9D1 1F3FF 200D 1F4BC ; fully-qualified # 🧑🏿‍💼 E12.1 office worker: dark skin tone
+1F468 200D 1F4BC ; fully-qualified # 👨‍💼 E4.0 man office worker
+1F468 1F3FB 200D 1F4BC ; fully-qualified # 👨🏻‍💼 E4.0 man office worker: light skin tone
+1F468 1F3FC 200D 1F4BC ; fully-qualified # 👨🏼‍💼 E4.0 man office worker: medium-light skin tone
+1F468 1F3FD 200D 1F4BC ; fully-qualified # 👨🏽‍💼 E4.0 man office worker: medium skin tone
+1F468 1F3FE 200D 1F4BC ; fully-qualified # 👨🏾‍💼 E4.0 man office worker: medium-dark skin tone
+1F468 1F3FF 200D 1F4BC ; fully-qualified # 👨🏿‍💼 E4.0 man office worker: dark skin tone
+1F469 200D 1F4BC ; fully-qualified # 👩‍💼 E4.0 woman office worker
+1F469 1F3FB 200D 1F4BC ; fully-qualified # 👩🏻‍💼 E4.0 woman office worker: light skin tone
+1F469 1F3FC 200D 1F4BC ; fully-qualified # 👩🏼‍💼 E4.0 woman office worker: medium-light skin tone
+1F469 1F3FD 200D 1F4BC ; fully-qualified # 👩🏽‍💼 E4.0 woman office worker: medium skin tone
+1F469 1F3FE 200D 1F4BC ; fully-qualified # 👩🏾‍💼 E4.0 woman office worker: medium-dark skin tone
+1F469 1F3FF 200D 1F4BC ; fully-qualified # 👩🏿‍💼 E4.0 woman office worker: dark skin tone
+1F9D1 200D 1F52C ; fully-qualified # 🧑‍🔬 E12.1 scientist
+1F9D1 1F3FB 200D 1F52C ; fully-qualified # 🧑🏻‍🔬 E12.1 scientist: light skin tone
+1F9D1 1F3FC 200D 1F52C ; fully-qualified # 🧑🏼‍🔬 E12.1 scientist: medium-light skin tone
+1F9D1 1F3FD 200D 1F52C ; fully-qualified # 🧑🏽‍🔬 E12.1 scientist: medium skin tone
+1F9D1 1F3FE 200D 1F52C ; fully-qualified # 🧑🏾‍🔬 E12.1 scientist: medium-dark skin tone
+1F9D1 1F3FF 200D 1F52C ; fully-qualified # 🧑🏿‍🔬 E12.1 scientist: dark skin tone
+1F468 200D 1F52C ; fully-qualified # 👨‍🔬 E4.0 man scientist
+1F468 1F3FB 200D 1F52C ; fully-qualified # 👨🏻‍🔬 E4.0 man scientist: light skin tone
+1F468 1F3FC 200D 1F52C ; fully-qualified # 👨🏼‍🔬 E4.0 man scientist: medium-light skin tone
+1F468 1F3FD 200D 1F52C ; fully-qualified # 👨🏽‍🔬 E4.0 man scientist: medium skin tone
+1F468 1F3FE 200D 1F52C ; fully-qualified # 👨🏾‍🔬 E4.0 man scientist: medium-dark skin tone
+1F468 1F3FF 200D 1F52C ; fully-qualified # 👨🏿‍🔬 E4.0 man scientist: dark skin tone
+1F469 200D 1F52C ; fully-qualified # 👩‍🔬 E4.0 woman scientist
+1F469 1F3FB 200D 1F52C ; fully-qualified # 👩🏻‍🔬 E4.0 woman scientist: light skin tone
+1F469 1F3FC 200D 1F52C ; fully-qualified # 👩🏼‍🔬 E4.0 woman scientist: medium-light skin tone
+1F469 1F3FD 200D 1F52C ; fully-qualified # 👩🏽‍🔬 E4.0 woman scientist: medium skin tone
+1F469 1F3FE 200D 1F52C ; fully-qualified # 👩🏾‍🔬 E4.0 woman scientist: medium-dark skin tone
+1F469 1F3FF 200D 1F52C ; fully-qualified # 👩🏿‍🔬 E4.0 woman scientist: dark skin tone
+1F9D1 200D 1F4BB ; fully-qualified # 🧑‍💻 E12.1 technologist
+1F9D1 1F3FB 200D 1F4BB ; fully-qualified # 🧑🏻‍💻 E12.1 technologist: light skin tone
+1F9D1 1F3FC 200D 1F4BB ; fully-qualified # 🧑🏼‍💻 E12.1 technologist: medium-light skin tone
+1F9D1 1F3FD 200D 1F4BB ; fully-qualified # 🧑🏽‍💻 E12.1 technologist: medium skin tone
+1F9D1 1F3FE 200D 1F4BB ; fully-qualified # 🧑🏾‍💻 E12.1 technologist: medium-dark skin tone
+1F9D1 1F3FF 200D 1F4BB ; fully-qualified # 🧑🏿‍💻 E12.1 technologist: dark skin tone
+1F468 200D 1F4BB ; fully-qualified # 👨‍💻 E4.0 man technologist
+1F468 1F3FB 200D 1F4BB ; fully-qualified # 👨🏻‍💻 E4.0 man technologist: light skin tone
+1F468 1F3FC 200D 1F4BB ; fully-qualified # 👨🏼‍💻 E4.0 man technologist: medium-light skin tone
+1F468 1F3FD 200D 1F4BB ; fully-qualified # 👨🏽‍💻 E4.0 man technologist: medium skin tone
+1F468 1F3FE 200D 1F4BB ; fully-qualified # 👨🏾‍💻 E4.0 man technologist: medium-dark skin tone
+1F468 1F3FF 200D 1F4BB ; fully-qualified # 👨🏿‍💻 E4.0 man technologist: dark skin tone
+1F469 200D 1F4BB ; fully-qualified # 👩‍💻 E4.0 woman technologist
+1F469 1F3FB 200D 1F4BB ; fully-qualified # 👩🏻‍💻 E4.0 woman technologist: light skin tone
+1F469 1F3FC 200D 1F4BB ; fully-qualified # 👩🏼‍💻 E4.0 woman technologist: medium-light skin tone
+1F469 1F3FD 200D 1F4BB ; fully-qualified # 👩🏽‍💻 E4.0 woman technologist: medium skin tone
+1F469 1F3FE 200D 1F4BB ; fully-qualified # 👩🏾‍💻 E4.0 woman technologist: medium-dark skin tone
+1F469 1F3FF 200D 1F4BB ; fully-qualified # 👩🏿‍💻 E4.0 woman technologist: dark skin tone
+1F9D1 200D 1F3A4 ; fully-qualified # 🧑‍🎤 E12.1 singer
+1F9D1 1F3FB 200D 1F3A4 ; fully-qualified # 🧑🏻‍🎤 E12.1 singer: light skin tone
+1F9D1 1F3FC 200D 1F3A4 ; fully-qualified # 🧑🏼‍🎤 E12.1 singer: medium-light skin tone
+1F9D1 1F3FD 200D 1F3A4 ; fully-qualified # 🧑🏽‍🎤 E12.1 singer: medium skin tone
+1F9D1 1F3FE 200D 1F3A4 ; fully-qualified # 🧑🏾‍🎤 E12.1 singer: medium-dark skin tone
+1F9D1 1F3FF 200D 1F3A4 ; fully-qualified # 🧑🏿‍🎤 E12.1 singer: dark skin tone
+1F468 200D 1F3A4 ; fully-qualified # 👨‍🎤 E4.0 man singer
+1F468 1F3FB 200D 1F3A4 ; fully-qualified # 👨🏻‍🎤 E4.0 man singer: light skin tone
+1F468 1F3FC 200D 1F3A4 ; fully-qualified # 👨🏼‍🎤 E4.0 man singer: medium-light skin tone
+1F468 1F3FD 200D 1F3A4 ; fully-qualified # 👨🏽‍🎤 E4.0 man singer: medium skin tone
+1F468 1F3FE 200D 1F3A4 ; fully-qualified # 👨🏾‍🎤 E4.0 man singer: medium-dark skin tone
+1F468 1F3FF 200D 1F3A4 ; fully-qualified # 👨🏿‍🎤 E4.0 man singer: dark skin tone
+1F469 200D 1F3A4 ; fully-qualified # 👩‍🎤 E4.0 woman singer
+1F469 1F3FB 200D 1F3A4 ; fully-qualified # 👩🏻‍🎤 E4.0 woman singer: light skin tone
+1F469 1F3FC 200D 1F3A4 ; fully-qualified # 👩🏼‍🎤 E4.0 woman singer: medium-light skin tone
+1F469 1F3FD 200D 1F3A4 ; fully-qualified # 👩🏽‍🎤 E4.0 woman singer: medium skin tone
+1F469 1F3FE 200D 1F3A4 ; fully-qualified # 👩🏾‍🎤 E4.0 woman singer: medium-dark skin tone
+1F469 1F3FF 200D 1F3A4 ; fully-qualified # 👩🏿‍🎤 E4.0 woman singer: dark skin tone
+1F9D1 200D 1F3A8 ; fully-qualified # 🧑‍🎨 E12.1 artist
+1F9D1 1F3FB 200D 1F3A8 ; fully-qualified # 🧑🏻‍🎨 E12.1 artist: light skin tone
+1F9D1 1F3FC 200D 1F3A8 ; fully-qualified # 🧑🏼‍🎨 E12.1 artist: medium-light skin tone
+1F9D1 1F3FD 200D 1F3A8 ; fully-qualified # 🧑🏽‍🎨 E12.1 artist: medium skin tone
+1F9D1 1F3FE 200D 1F3A8 ; fully-qualified # 🧑🏾‍🎨 E12.1 artist: medium-dark skin tone
+1F9D1 1F3FF 200D 1F3A8 ; fully-qualified # 🧑🏿‍🎨 E12.1 artist: dark skin tone
+1F468 200D 1F3A8 ; fully-qualified # 👨‍🎨 E4.0 man artist
+1F468 1F3FB 200D 1F3A8 ; fully-qualified # 👨🏻‍🎨 E4.0 man artist: light skin tone
+1F468 1F3FC 200D 1F3A8 ; fully-qualified # 👨🏼‍🎨 E4.0 man artist: medium-light skin tone
+1F468 1F3FD 200D 1F3A8 ; fully-qualified # 👨🏽‍🎨 E4.0 man artist: medium skin tone
+1F468 1F3FE 200D 1F3A8 ; fully-qualified # 👨🏾‍🎨 E4.0 man artist: medium-dark skin tone
+1F468 1F3FF 200D 1F3A8 ; fully-qualified # 👨🏿‍🎨 E4.0 man artist: dark skin tone
+1F469 200D 1F3A8 ; fully-qualified # 👩‍🎨 E4.0 woman artist
+1F469 1F3FB 200D 1F3A8 ; fully-qualified # 👩🏻‍🎨 E4.0 woman artist: light skin tone
+1F469 1F3FC 200D 1F3A8 ; fully-qualified # 👩🏼‍🎨 E4.0 woman artist: medium-light skin tone
+1F469 1F3FD 200D 1F3A8 ; fully-qualified # 👩🏽‍🎨 E4.0 woman artist: medium skin tone
+1F469 1F3FE 200D 1F3A8 ; fully-qualified # 👩🏾‍🎨 E4.0 woman artist: medium-dark skin tone
+1F469 1F3FF 200D 1F3A8 ; fully-qualified # 👩🏿‍🎨 E4.0 woman artist: dark skin tone
+1F9D1 200D 2708 FE0F ; fully-qualified # 🧑‍✈️ E12.1 pilot
+1F9D1 200D 2708 ; minimally-qualified # 🧑‍✈ E12.1 pilot
+1F9D1 1F3FB 200D 2708 FE0F ; fully-qualified # 🧑🏻‍✈️ E12.1 pilot: light skin tone
+1F9D1 1F3FB 200D 2708 ; minimally-qualified # 🧑🏻‍✈ E12.1 pilot: light skin tone
+1F9D1 1F3FC 200D 2708 FE0F ; fully-qualified # 🧑🏼‍✈️ E12.1 pilot: medium-light skin tone
+1F9D1 1F3FC 200D 2708 ; minimally-qualified # 🧑🏼‍✈ E12.1 pilot: medium-light skin tone
+1F9D1 1F3FD 200D 2708 FE0F ; fully-qualified # 🧑🏽‍✈️ E12.1 pilot: medium skin tone
+1F9D1 1F3FD 200D 2708 ; minimally-qualified # 🧑🏽‍✈ E12.1 pilot: medium skin tone
+1F9D1 1F3FE 200D 2708 FE0F ; fully-qualified # 🧑🏾‍✈️ E12.1 pilot: medium-dark skin tone
+1F9D1 1F3FE 200D 2708 ; minimally-qualified # 🧑🏾‍✈ E12.1 pilot: medium-dark skin tone
+1F9D1 1F3FF 200D 2708 FE0F ; fully-qualified # 🧑🏿‍✈️ E12.1 pilot: dark skin tone
+1F9D1 1F3FF 200D 2708 ; minimally-qualified # 🧑🏿‍✈ E12.1 pilot: dark skin tone
+1F468 200D 2708 FE0F ; fully-qualified # 👨‍✈️ E4.0 man pilot
+1F468 200D 2708 ; minimally-qualified # 👨‍✈ E4.0 man pilot
+1F468 1F3FB 200D 2708 FE0F ; fully-qualified # 👨🏻‍✈️ E4.0 man pilot: light skin tone
+1F468 1F3FB 200D 2708 ; minimally-qualified # 👨🏻‍✈ E4.0 man pilot: light skin tone
+1F468 1F3FC 200D 2708 FE0F ; fully-qualified # 👨🏼‍✈️ E4.0 man pilot: medium-light skin tone
+1F468 1F3FC 200D 2708 ; minimally-qualified # 👨🏼‍✈ E4.0 man pilot: medium-light skin tone
+1F468 1F3FD 200D 2708 FE0F ; fully-qualified # 👨🏽‍✈️ E4.0 man pilot: medium skin tone
+1F468 1F3FD 200D 2708 ; minimally-qualified # 👨🏽‍✈ E4.0 man pilot: medium skin tone
+1F468 1F3FE 200D 2708 FE0F ; fully-qualified # 👨🏾‍✈️ E4.0 man pilot: medium-dark skin tone
+1F468 1F3FE 200D 2708 ; minimally-qualified # 👨🏾‍✈ E4.0 man pilot: medium-dark skin tone
+1F468 1F3FF 200D 2708 FE0F ; fully-qualified # 👨🏿‍✈️ E4.0 man pilot: dark skin tone
+1F468 1F3FF 200D 2708 ; minimally-qualified # 👨🏿‍✈ E4.0 man pilot: dark skin tone
+1F469 200D 2708 FE0F ; fully-qualified # 👩‍✈️ E4.0 woman pilot
+1F469 200D 2708 ; minimally-qualified # 👩‍✈ E4.0 woman pilot
+1F469 1F3FB 200D 2708 FE0F ; fully-qualified # 👩🏻‍✈️ E4.0 woman pilot: light skin tone
+1F469 1F3FB 200D 2708 ; minimally-qualified # 👩🏻‍✈ E4.0 woman pilot: light skin tone
+1F469 1F3FC 200D 2708 FE0F ; fully-qualified # 👩🏼‍✈️ E4.0 woman pilot: medium-light skin tone
+1F469 1F3FC 200D 2708 ; minimally-qualified # 👩🏼‍✈ E4.0 woman pilot: medium-light skin tone
+1F469 1F3FD 200D 2708 FE0F ; fully-qualified # 👩🏽‍✈️ E4.0 woman pilot: medium skin tone
+1F469 1F3FD 200D 2708 ; minimally-qualified # 👩🏽‍✈ E4.0 woman pilot: medium skin tone
+1F469 1F3FE 200D 2708 FE0F ; fully-qualified # 👩🏾‍✈️ E4.0 woman pilot: medium-dark skin tone
+1F469 1F3FE 200D 2708 ; minimally-qualified # 👩🏾‍✈ E4.0 woman pilot: medium-dark skin tone
+1F469 1F3FF 200D 2708 FE0F ; fully-qualified # 👩🏿‍✈️ E4.0 woman pilot: dark skin tone
+1F469 1F3FF 200D 2708 ; minimally-qualified # 👩🏿‍✈ E4.0 woman pilot: dark skin tone
+1F9D1 200D 1F680 ; fully-qualified # 🧑‍🚀 E12.1 astronaut
+1F9D1 1F3FB 200D 1F680 ; fully-qualified # 🧑🏻‍🚀 E12.1 astronaut: light skin tone
+1F9D1 1F3FC 200D 1F680 ; fully-qualified # 🧑🏼‍🚀 E12.1 astronaut: medium-light skin tone
+1F9D1 1F3FD 200D 1F680 ; fully-qualified # 🧑🏽‍🚀 E12.1 astronaut: medium skin tone
+1F9D1 1F3FE 200D 1F680 ; fully-qualified # 🧑🏾‍🚀 E12.1 astronaut: medium-dark skin tone
+1F9D1 1F3FF 200D 1F680 ; fully-qualified # 🧑🏿‍🚀 E12.1 astronaut: dark skin tone
+1F468 200D 1F680 ; fully-qualified # 👨‍🚀 E4.0 man astronaut
+1F468 1F3FB 200D 1F680 ; fully-qualified # 👨🏻‍🚀 E4.0 man astronaut: light skin tone
+1F468 1F3FC 200D 1F680 ; fully-qualified # 👨🏼‍🚀 E4.0 man astronaut: medium-light skin tone
+1F468 1F3FD 200D 1F680 ; fully-qualified # 👨🏽‍🚀 E4.0 man astronaut: medium skin tone
+1F468 1F3FE 200D 1F680 ; fully-qualified # 👨🏾‍🚀 E4.0 man astronaut: medium-dark skin tone
+1F468 1F3FF 200D 1F680 ; fully-qualified # 👨🏿‍🚀 E4.0 man astronaut: dark skin tone
+1F469 200D 1F680 ; fully-qualified # 👩‍🚀 E4.0 woman astronaut
+1F469 1F3FB 200D 1F680 ; fully-qualified # 👩🏻‍🚀 E4.0 woman astronaut: light skin tone
+1F469 1F3FC 200D 1F680 ; fully-qualified # 👩🏼‍🚀 E4.0 woman astronaut: medium-light skin tone
+1F469 1F3FD 200D 1F680 ; fully-qualified # 👩🏽‍🚀 E4.0 woman astronaut: medium skin tone
+1F469 1F3FE 200D 1F680 ; fully-qualified # 👩🏾‍🚀 E4.0 woman astronaut: medium-dark skin tone
+1F469 1F3FF 200D 1F680 ; fully-qualified # 👩🏿‍🚀 E4.0 woman astronaut: dark skin tone
+1F9D1 200D 1F692 ; fully-qualified # 🧑‍🚒 E12.1 firefighter
+1F9D1 1F3FB 200D 1F692 ; fully-qualified # 🧑🏻‍🚒 E12.1 firefighter: light skin tone
+1F9D1 1F3FC 200D 1F692 ; fully-qualified # 🧑🏼‍🚒 E12.1 firefighter: medium-light skin tone
+1F9D1 1F3FD 200D 1F692 ; fully-qualified # 🧑🏽‍🚒 E12.1 firefighter: medium skin tone
+1F9D1 1F3FE 200D 1F692 ; fully-qualified # 🧑🏾‍🚒 E12.1 firefighter: medium-dark skin tone
+1F9D1 1F3FF 200D 1F692 ; fully-qualified # 🧑🏿‍🚒 E12.1 firefighter: dark skin tone
+1F468 200D 1F692 ; fully-qualified # 👨‍🚒 E4.0 man firefighter
+1F468 1F3FB 200D 1F692 ; fully-qualified # 👨🏻‍🚒 E4.0 man firefighter: light skin tone
+1F468 1F3FC 200D 1F692 ; fully-qualified # 👨🏼‍🚒 E4.0 man firefighter: medium-light skin tone
+1F468 1F3FD 200D 1F692 ; fully-qualified # 👨🏽‍🚒 E4.0 man firefighter: medium skin tone
+1F468 1F3FE 200D 1F692 ; fully-qualified # 👨🏾‍🚒 E4.0 man firefighter: medium-dark skin tone
+1F468 1F3FF 200D 1F692 ; fully-qualified # 👨🏿‍🚒 E4.0 man firefighter: dark skin tone
+1F469 200D 1F692 ; fully-qualified # 👩‍🚒 E4.0 woman firefighter
+1F469 1F3FB 200D 1F692 ; fully-qualified # 👩🏻‍🚒 E4.0 woman firefighter: light skin tone
+1F469 1F3FC 200D 1F692 ; fully-qualified # 👩🏼‍🚒 E4.0 woman firefighter: medium-light skin tone
+1F469 1F3FD 200D 1F692 ; fully-qualified # 👩🏽‍🚒 E4.0 woman firefighter: medium skin tone
+1F469 1F3FE 200D 1F692 ; fully-qualified # 👩🏾‍🚒 E4.0 woman firefighter: medium-dark skin tone
+1F469 1F3FF 200D 1F692 ; fully-qualified # 👩🏿‍🚒 E4.0 woman firefighter: dark skin tone
+1F46E ; fully-qualified # 👮 E0.6 police officer
+1F46E 1F3FB ; fully-qualified # 👮🏻 E1.0 police officer: light skin tone
+1F46E 1F3FC ; fully-qualified # 👮🏼 E1.0 police officer: medium-light skin tone
+1F46E 1F3FD ; fully-qualified # 👮🏽 E1.0 police officer: medium skin tone
+1F46E 1F3FE ; fully-qualified # 👮🏾 E1.0 police officer: medium-dark skin tone
+1F46E 1F3FF ; fully-qualified # 👮🏿 E1.0 police officer: dark skin tone
+1F46E 200D 2642 FE0F ; fully-qualified # 👮‍♂️ E4.0 man police officer
+1F46E 200D 2642 ; minimally-qualified # 👮‍♂ E4.0 man police officer
+1F46E 1F3FB 200D 2642 FE0F ; fully-qualified # 👮🏻‍♂️ E4.0 man police officer: light skin tone
+1F46E 1F3FB 200D 2642 ; minimally-qualified # 👮🏻‍♂ E4.0 man police officer: light skin tone
+1F46E 1F3FC 200D 2642 FE0F ; fully-qualified # 👮🏼‍♂️ E4.0 man police officer: medium-light skin tone
+1F46E 1F3FC 200D 2642 ; minimally-qualified # 👮🏼‍♂ E4.0 man police officer: medium-light skin tone
+1F46E 1F3FD 200D 2642 FE0F ; fully-qualified # 👮🏽‍♂️ E4.0 man police officer: medium skin tone
+1F46E 1F3FD 200D 2642 ; minimally-qualified # 👮🏽‍♂ E4.0 man police officer: medium skin tone
+1F46E 1F3FE 200D 2642 FE0F ; fully-qualified # 👮🏾‍♂️ E4.0 man police officer: medium-dark skin tone
+1F46E 1F3FE 200D 2642 ; minimally-qualified # 👮🏾‍♂ E4.0 man police officer: medium-dark skin tone
+1F46E 1F3FF 200D 2642 FE0F ; fully-qualified # 👮🏿‍♂️ E4.0 man police officer: dark skin tone
+1F46E 1F3FF 200D 2642 ; minimally-qualified # 👮🏿‍♂ E4.0 man police officer: dark skin tone
+1F46E 200D 2640 FE0F ; fully-qualified # 👮‍♀️ E4.0 woman police officer
+1F46E 200D 2640 ; minimally-qualified # 👮‍♀ E4.0 woman police officer
+1F46E 1F3FB 200D 2640 FE0F ; fully-qualified # 👮🏻‍♀️ E4.0 woman police officer: light skin tone
+1F46E 1F3FB 200D 2640 ; minimally-qualified # 👮🏻‍♀ E4.0 woman police officer: light skin tone
+1F46E 1F3FC 200D 2640 FE0F ; fully-qualified # 👮🏼‍♀️ E4.0 woman police officer: medium-light skin tone
+1F46E 1F3FC 200D 2640 ; minimally-qualified # 👮🏼‍♀ E4.0 woman police officer: medium-light skin tone
+1F46E 1F3FD 200D 2640 FE0F ; fully-qualified # 👮🏽‍♀️ E4.0 woman police officer: medium skin tone
+1F46E 1F3FD 200D 2640 ; minimally-qualified # 👮🏽‍♀ E4.0 woman police officer: medium skin tone
+1F46E 1F3FE 200D 2640 FE0F ; fully-qualified # 👮🏾‍♀️ E4.0 woman police officer: medium-dark skin tone
+1F46E 1F3FE 200D 2640 ; minimally-qualified # 👮🏾‍♀ E4.0 woman police officer: medium-dark skin tone
+1F46E 1F3FF 200D 2640 FE0F ; fully-qualified # 👮🏿‍♀️ E4.0 woman police officer: dark skin tone
+1F46E 1F3FF 200D 2640 ; minimally-qualified # 👮🏿‍♀ E4.0 woman police officer: dark skin tone
+1F575 FE0F ; fully-qualified # 🕵️ E0.7 detective
+1F575 ; unqualified # 🕵 E0.7 detective
+1F575 1F3FB ; fully-qualified # 🕵🏻 E2.0 detective: light skin tone
+1F575 1F3FC ; fully-qualified # 🕵🏼 E2.0 detective: medium-light skin tone
+1F575 1F3FD ; fully-qualified # 🕵🏽 E2.0 detective: medium skin tone
+1F575 1F3FE ; fully-qualified # 🕵🏾 E2.0 detective: medium-dark skin tone
+1F575 1F3FF ; fully-qualified # 🕵🏿 E2.0 detective: dark skin tone
+1F575 FE0F 200D 2642 FE0F ; fully-qualified # 🕵️‍♂️ E4.0 man detective
+1F575 200D 2642 FE0F ; unqualified # 🕵‍♂️ E4.0 man detective
+1F575 FE0F 200D 2642 ; unqualified # 🕵️‍♂ E4.0 man detective
+1F575 200D 2642 ; unqualified # 🕵‍♂ E4.0 man detective
+1F575 1F3FB 200D 2642 FE0F ; fully-qualified # 🕵🏻‍♂️ E4.0 man detective: light skin tone
+1F575 1F3FB 200D 2642 ; minimally-qualified # 🕵🏻‍♂ E4.0 man detective: light skin tone
+1F575 1F3FC 200D 2642 FE0F ; fully-qualified # 🕵🏼‍♂️ E4.0 man detective: medium-light skin tone
+1F575 1F3FC 200D 2642 ; minimally-qualified # 🕵🏼‍♂ E4.0 man detective: medium-light skin tone
+1F575 1F3FD 200D 2642 FE0F ; fully-qualified # 🕵🏽‍♂️ E4.0 man detective: medium skin tone
+1F575 1F3FD 200D 2642 ; minimally-qualified # 🕵🏽‍♂ E4.0 man detective: medium skin tone
+1F575 1F3FE 200D 2642 FE0F ; fully-qualified # 🕵🏾‍♂️ E4.0 man detective: medium-dark skin tone
+1F575 1F3FE 200D 2642 ; minimally-qualified # 🕵🏾‍♂ E4.0 man detective: medium-dark skin tone
+1F575 1F3FF 200D 2642 FE0F ; fully-qualified # 🕵🏿‍♂️ E4.0 man detective: dark skin tone
+1F575 1F3FF 200D 2642 ; minimally-qualified # 🕵🏿‍♂ E4.0 man detective: dark skin tone
+1F575 FE0F 200D 2640 FE0F ; fully-qualified # 🕵️‍♀️ E4.0 woman detective
+1F575 200D 2640 FE0F ; unqualified # 🕵‍♀️ E4.0 woman detective
+1F575 FE0F 200D 2640 ; unqualified # 🕵️‍♀ E4.0 woman detective
+1F575 200D 2640 ; unqualified # 🕵‍♀ E4.0 woman detective
+1F575 1F3FB 200D 2640 FE0F ; fully-qualified # 🕵🏻‍♀️ E4.0 woman detective: light skin tone
+1F575 1F3FB 200D 2640 ; minimally-qualified # 🕵🏻‍♀ E4.0 woman detective: light skin tone
+1F575 1F3FC 200D 2640 FE0F ; fully-qualified # 🕵🏼‍♀️ E4.0 woman detective: medium-light skin tone
+1F575 1F3FC 200D 2640 ; minimally-qualified # 🕵🏼‍♀ E4.0 woman detective: medium-light skin tone
+1F575 1F3FD 200D 2640 FE0F ; fully-qualified # 🕵🏽‍♀️ E4.0 woman detective: medium skin tone
+1F575 1F3FD 200D 2640 ; minimally-qualified # 🕵🏽‍♀ E4.0 woman detective: medium skin tone
+1F575 1F3FE 200D 2640 FE0F ; fully-qualified # 🕵🏾‍♀️ E4.0 woman detective: medium-dark skin tone
+1F575 1F3FE 200D 2640 ; minimally-qualified # 🕵🏾‍♀ E4.0 woman detective: medium-dark skin tone
+1F575 1F3FF 200D 2640 FE0F ; fully-qualified # 🕵🏿‍♀️ E4.0 woman detective: dark skin tone
+1F575 1F3FF 200D 2640 ; minimally-qualified # 🕵🏿‍♀ E4.0 woman detective: dark skin tone
+1F482 ; fully-qualified # 💂 E0.6 guard
+1F482 1F3FB ; fully-qualified # 💂🏻 E1.0 guard: light skin tone
+1F482 1F3FC ; fully-qualified # 💂🏼 E1.0 guard: medium-light skin tone
+1F482 1F3FD ; fully-qualified # 💂🏽 E1.0 guard: medium skin tone
+1F482 1F3FE ; fully-qualified # 💂🏾 E1.0 guard: medium-dark skin tone
+1F482 1F3FF ; fully-qualified # 💂🏿 E1.0 guard: dark skin tone
+1F482 200D 2642 FE0F ; fully-qualified # 💂‍♂️ E4.0 man guard
+1F482 200D 2642 ; minimally-qualified # 💂‍♂ E4.0 man guard
+1F482 1F3FB 200D 2642 FE0F ; fully-qualified # 💂🏻‍♂️ E4.0 man guard: light skin tone
+1F482 1F3FB 200D 2642 ; minimally-qualified # 💂🏻‍♂ E4.0 man guard: light skin tone
+1F482 1F3FC 200D 2642 FE0F ; fully-qualified # 💂🏼‍♂️ E4.0 man guard: medium-light skin tone
+1F482 1F3FC 200D 2642 ; minimally-qualified # 💂🏼‍♂ E4.0 man guard: medium-light skin tone
+1F482 1F3FD 200D 2642 FE0F ; fully-qualified # 💂🏽‍♂️ E4.0 man guard: medium skin tone
+1F482 1F3FD 200D 2642 ; minimally-qualified # 💂🏽‍♂ E4.0 man guard: medium skin tone
+1F482 1F3FE 200D 2642 FE0F ; fully-qualified # 💂🏾‍♂️ E4.0 man guard: medium-dark skin tone
+1F482 1F3FE 200D 2642 ; minimally-qualified # 💂🏾‍♂ E4.0 man guard: medium-dark skin tone
+1F482 1F3FF 200D 2642 FE0F ; fully-qualified # 💂🏿‍♂️ E4.0 man guard: dark skin tone
+1F482 1F3FF 200D 2642 ; minimally-qualified # 💂🏿‍♂ E4.0 man guard: dark skin tone
+1F482 200D 2640 FE0F ; fully-qualified # 💂‍♀️ E4.0 woman guard
+1F482 200D 2640 ; minimally-qualified # 💂‍♀ E4.0 woman guard
+1F482 1F3FB 200D 2640 FE0F ; fully-qualified # 💂🏻‍♀️ E4.0 woman guard: light skin tone
+1F482 1F3FB 200D 2640 ; minimally-qualified # 💂🏻‍♀ E4.0 woman guard: light skin tone
+1F482 1F3FC 200D 2640 FE0F ; fully-qualified # 💂🏼‍♀️ E4.0 woman guard: medium-light skin tone
+1F482 1F3FC 200D 2640 ; minimally-qualified # 💂🏼‍♀ E4.0 woman guard: medium-light skin tone
+1F482 1F3FD 200D 2640 FE0F ; fully-qualified # 💂🏽‍♀️ E4.0 woman guard: medium skin tone
+1F482 1F3FD 200D 2640 ; minimally-qualified # 💂🏽‍♀ E4.0 woman guard: medium skin tone
+1F482 1F3FE 200D 2640 FE0F ; fully-qualified # 💂🏾‍♀️ E4.0 woman guard: medium-dark skin tone
+1F482 1F3FE 200D 2640 ; minimally-qualified # 💂🏾‍♀ E4.0 woman guard: medium-dark skin tone
+1F482 1F3FF 200D 2640 FE0F ; fully-qualified # 💂🏿‍♀️ E4.0 woman guard: dark skin tone
+1F482 1F3FF 200D 2640 ; minimally-qualified # 💂🏿‍♀ E4.0 woman guard: dark skin tone
+1F977 ; fully-qualified # 🥷 E13.0 ninja
+1F977 1F3FB ; fully-qualified # 🥷🏻 E13.0 ninja: light skin tone
+1F977 1F3FC ; fully-qualified # 🥷🏼 E13.0 ninja: medium-light skin tone
+1F977 1F3FD ; fully-qualified # 🥷🏽 E13.0 ninja: medium skin tone
+1F977 1F3FE ; fully-qualified # 🥷🏾 E13.0 ninja: medium-dark skin tone
+1F977 1F3FF ; fully-qualified # 🥷🏿 E13.0 ninja: dark skin tone
+1F477 ; fully-qualified # 👷 E0.6 construction worker
+1F477 1F3FB ; fully-qualified # 👷🏻 E1.0 construction worker: light skin tone
+1F477 1F3FC ; fully-qualified # 👷🏼 E1.0 construction worker: medium-light skin tone
+1F477 1F3FD ; fully-qualified # 👷🏽 E1.0 construction worker: medium skin tone
+1F477 1F3FE ; fully-qualified # 👷🏾 E1.0 construction worker: medium-dark skin tone
+1F477 1F3FF ; fully-qualified # 👷🏿 E1.0 construction worker: dark skin tone
+1F477 200D 2642 FE0F ; fully-qualified # 👷‍♂️ E4.0 man construction worker
+1F477 200D 2642 ; minimally-qualified # 👷‍♂ E4.0 man construction worker
+1F477 1F3FB 200D 2642 FE0F ; fully-qualified # 👷🏻‍♂️ E4.0 man construction worker: light skin tone
+1F477 1F3FB 200D 2642 ; minimally-qualified # 👷🏻‍♂ E4.0 man construction worker: light skin tone
+1F477 1F3FC 200D 2642 FE0F ; fully-qualified # 👷🏼‍♂️ E4.0 man construction worker: medium-light skin tone
+1F477 1F3FC 200D 2642 ; minimally-qualified # 👷🏼‍♂ E4.0 man construction worker: medium-light skin tone
+1F477 1F3FD 200D 2642 FE0F ; fully-qualified # 👷🏽‍♂️ E4.0 man construction worker: medium skin tone
+1F477 1F3FD 200D 2642 ; minimally-qualified # 👷🏽‍♂ E4.0 man construction worker: medium skin tone
+1F477 1F3FE 200D 2642 FE0F ; fully-qualified # 👷🏾‍♂️ E4.0 man construction worker: medium-dark skin tone
+1F477 1F3FE 200D 2642 ; minimally-qualified # 👷🏾‍♂ E4.0 man construction worker: medium-dark skin tone
+1F477 1F3FF 200D 2642 FE0F ; fully-qualified # 👷🏿‍♂️ E4.0 man construction worker: dark skin tone
+1F477 1F3FF 200D 2642 ; minimally-qualified # 👷🏿‍♂ E4.0 man construction worker: dark skin tone
+1F477 200D 2640 FE0F ; fully-qualified # 👷‍♀️ E4.0 woman construction worker
+1F477 200D 2640 ; minimally-qualified # 👷‍♀ E4.0 woman construction worker
+1F477 1F3FB 200D 2640 FE0F ; fully-qualified # 👷🏻‍♀️ E4.0 woman construction worker: light skin tone
+1F477 1F3FB 200D 2640 ; minimally-qualified # 👷🏻‍♀ E4.0 woman construction worker: light skin tone
+1F477 1F3FC 200D 2640 FE0F ; fully-qualified # 👷🏼‍♀️ E4.0 woman construction worker: medium-light skin tone
+1F477 1F3FC 200D 2640 ; minimally-qualified # 👷🏼‍♀ E4.0 woman construction worker: medium-light skin tone
+1F477 1F3FD 200D 2640 FE0F ; fully-qualified # 👷🏽‍♀️ E4.0 woman construction worker: medium skin tone
+1F477 1F3FD 200D 2640 ; minimally-qualified # 👷🏽‍♀ E4.0 woman construction worker: medium skin tone
+1F477 1F3FE 200D 2640 FE0F ; fully-qualified # 👷🏾‍♀️ E4.0 woman construction worker: medium-dark skin tone
+1F477 1F3FE 200D 2640 ; minimally-qualified # 👷🏾‍♀ E4.0 woman construction worker: medium-dark skin tone
+1F477 1F3FF 200D 2640 FE0F ; fully-qualified # 👷🏿‍♀️ E4.0 woman construction worker: dark skin tone
+1F477 1F3FF 200D 2640 ; minimally-qualified # 👷🏿‍♀ E4.0 woman construction worker: dark skin tone
+1F934 ; fully-qualified # 🤴 E3.0 prince
+1F934 1F3FB ; fully-qualified # 🤴🏻 E3.0 prince: light skin tone
+1F934 1F3FC ; fully-qualified # 🤴🏼 E3.0 prince: medium-light skin tone
+1F934 1F3FD ; fully-qualified # 🤴🏽 E3.0 prince: medium skin tone
+1F934 1F3FE ; fully-qualified # 🤴🏾 E3.0 prince: medium-dark skin tone
+1F934 1F3FF ; fully-qualified # 🤴🏿 E3.0 prince: dark skin tone
+1F478 ; fully-qualified # 👸 E0.6 princess
+1F478 1F3FB ; fully-qualified # 👸🏻 E1.0 princess: light skin tone
+1F478 1F3FC ; fully-qualified # 👸🏼 E1.0 princess: medium-light skin tone
+1F478 1F3FD ; fully-qualified # 👸🏽 E1.0 princess: medium skin tone
+1F478 1F3FE ; fully-qualified # 👸🏾 E1.0 princess: medium-dark skin tone
+1F478 1F3FF ; fully-qualified # 👸🏿 E1.0 princess: dark skin tone
+1F473 ; fully-qualified # 👳 E0.6 person wearing turban
+1F473 1F3FB ; fully-qualified # 👳🏻 E1.0 person wearing turban: light skin tone
+1F473 1F3FC ; fully-qualified # 👳🏼 E1.0 person wearing turban: medium-light skin tone
+1F473 1F3FD ; fully-qualified # 👳🏽 E1.0 person wearing turban: medium skin tone
+1F473 1F3FE ; fully-qualified # 👳🏾 E1.0 person wearing turban: medium-dark skin tone
+1F473 1F3FF ; fully-qualified # 👳🏿 E1.0 person wearing turban: dark skin tone
+1F473 200D 2642 FE0F ; fully-qualified # 👳‍♂️ E4.0 man wearing turban
+1F473 200D 2642 ; minimally-qualified # 👳‍♂ E4.0 man wearing turban
+1F473 1F3FB 200D 2642 FE0F ; fully-qualified # 👳🏻‍♂️ E4.0 man wearing turban: light skin tone
+1F473 1F3FB 200D 2642 ; minimally-qualified # 👳🏻‍♂ E4.0 man wearing turban: light skin tone
+1F473 1F3FC 200D 2642 FE0F ; fully-qualified # 👳🏼‍♂️ E4.0 man wearing turban: medium-light skin tone
+1F473 1F3FC 200D 2642 ; minimally-qualified # 👳🏼‍♂ E4.0 man wearing turban: medium-light skin tone
+1F473 1F3FD 200D 2642 FE0F ; fully-qualified # 👳🏽‍♂️ E4.0 man wearing turban: medium skin tone
+1F473 1F3FD 200D 2642 ; minimally-qualified # 👳🏽‍♂ E4.0 man wearing turban: medium skin tone
+1F473 1F3FE 200D 2642 FE0F ; fully-qualified # 👳🏾‍♂️ E4.0 man wearing turban: medium-dark skin tone
+1F473 1F3FE 200D 2642 ; minimally-qualified # 👳🏾‍♂ E4.0 man wearing turban: medium-dark skin tone
+1F473 1F3FF 200D 2642 FE0F ; fully-qualified # 👳🏿‍♂️ E4.0 man wearing turban: dark skin tone
+1F473 1F3FF 200D 2642 ; minimally-qualified # 👳🏿‍♂ E4.0 man wearing turban: dark skin tone
+1F473 200D 2640 FE0F ; fully-qualified # 👳‍♀️ E4.0 woman wearing turban
+1F473 200D 2640 ; minimally-qualified # 👳‍♀ E4.0 woman wearing turban
+1F473 1F3FB 200D 2640 FE0F ; fully-qualified # 👳🏻‍♀️ E4.0 woman wearing turban: light skin tone
+1F473 1F3FB 200D 2640 ; minimally-qualified # 👳🏻‍♀ E4.0 woman wearing turban: light skin tone
+1F473 1F3FC 200D 2640 FE0F ; fully-qualified # 👳🏼‍♀️ E4.0 woman wearing turban: medium-light skin tone
+1F473 1F3FC 200D 2640 ; minimally-qualified # 👳🏼‍♀ E4.0 woman wearing turban: medium-light skin tone
+1F473 1F3FD 200D 2640 FE0F ; fully-qualified # 👳🏽‍♀️ E4.0 woman wearing turban: medium skin tone
+1F473 1F3FD 200D 2640 ; minimally-qualified # 👳🏽‍♀ E4.0 woman wearing turban: medium skin tone
+1F473 1F3FE 200D 2640 FE0F ; fully-qualified # 👳🏾‍♀️ E4.0 woman wearing turban: medium-dark skin tone
+1F473 1F3FE 200D 2640 ; minimally-qualified # 👳🏾‍♀ E4.0 woman wearing turban: medium-dark skin tone
+1F473 1F3FF 200D 2640 FE0F ; fully-qualified # 👳🏿‍♀️ E4.0 woman wearing turban: dark skin tone
+1F473 1F3FF 200D 2640 ; minimally-qualified # 👳🏿‍♀ E4.0 woman wearing turban: dark skin tone
+1F472 ; fully-qualified # 👲 E0.6 person with skullcap
+1F472 1F3FB ; fully-qualified # 👲🏻 E1.0 person with skullcap: light skin tone
+1F472 1F3FC ; fully-qualified # 👲🏼 E1.0 person with skullcap: medium-light skin tone
+1F472 1F3FD ; fully-qualified # 👲🏽 E1.0 person with skullcap: medium skin tone
+1F472 1F3FE ; fully-qualified # 👲🏾 E1.0 person with skullcap: medium-dark skin tone
+1F472 1F3FF ; fully-qualified # 👲🏿 E1.0 person with skullcap: dark skin tone
+1F9D5 ; fully-qualified # 🧕 E5.0 woman with headscarf
+1F9D5 1F3FB ; fully-qualified # 🧕🏻 E5.0 woman with headscarf: light skin tone
+1F9D5 1F3FC ; fully-qualified # 🧕🏼 E5.0 woman with headscarf: medium-light skin tone
+1F9D5 1F3FD ; fully-qualified # 🧕🏽 E5.0 woman with headscarf: medium skin tone
+1F9D5 1F3FE ; fully-qualified # 🧕🏾 E5.0 woman with headscarf: medium-dark skin tone
+1F9D5 1F3FF ; fully-qualified # 🧕🏿 E5.0 woman with headscarf: dark skin tone
+1F935 ; fully-qualified # 🤵 E3.0 person in tuxedo
+1F935 1F3FB ; fully-qualified # 🤵🏻 E3.0 person in tuxedo: light skin tone
+1F935 1F3FC ; fully-qualified # 🤵🏼 E3.0 person in tuxedo: medium-light skin tone
+1F935 1F3FD ; fully-qualified # 🤵🏽 E3.0 person in tuxedo: medium skin tone
+1F935 1F3FE ; fully-qualified # 🤵🏾 E3.0 person in tuxedo: medium-dark skin tone
+1F935 1F3FF ; fully-qualified # 🤵🏿 E3.0 person in tuxedo: dark skin tone
+1F935 200D 2642 FE0F ; fully-qualified # 🤵‍♂️ E13.0 man in tuxedo
+1F935 200D 2642 ; minimally-qualified # 🤵‍♂ E13.0 man in tuxedo
+1F935 1F3FB 200D 2642 FE0F ; fully-qualified # 🤵🏻‍♂️ E13.0 man in tuxedo: light skin tone
+1F935 1F3FB 200D 2642 ; minimally-qualified # 🤵🏻‍♂ E13.0 man in tuxedo: light skin tone
+1F935 1F3FC 200D 2642 FE0F ; fully-qualified # 🤵🏼‍♂️ E13.0 man in tuxedo: medium-light skin tone
+1F935 1F3FC 200D 2642 ; minimally-qualified # 🤵🏼‍♂ E13.0 man in tuxedo: medium-light skin tone
+1F935 1F3FD 200D 2642 FE0F ; fully-qualified # 🤵🏽‍♂️ E13.0 man in tuxedo: medium skin tone
+1F935 1F3FD 200D 2642 ; minimally-qualified # 🤵🏽‍♂ E13.0 man in tuxedo: medium skin tone
+1F935 1F3FE 200D 2642 FE0F ; fully-qualified # 🤵🏾‍♂️ E13.0 man in tuxedo: medium-dark skin tone
+1F935 1F3FE 200D 2642 ; minimally-qualified # 🤵🏾‍♂ E13.0 man in tuxedo: medium-dark skin tone
+1F935 1F3FF 200D 2642 FE0F ; fully-qualified # 🤵🏿‍♂️ E13.0 man in tuxedo: dark skin tone
+1F935 1F3FF 200D 2642 ; minimally-qualified # 🤵🏿‍♂ E13.0 man in tuxedo: dark skin tone
+1F935 200D 2640 FE0F ; fully-qualified # 🤵‍♀️ E13.0 woman in tuxedo
+1F935 200D 2640 ; minimally-qualified # 🤵‍♀ E13.0 woman in tuxedo
+1F935 1F3FB 200D 2640 FE0F ; fully-qualified # 🤵🏻‍♀️ E13.0 woman in tuxedo: light skin tone
+1F935 1F3FB 200D 2640 ; minimally-qualified # 🤵🏻‍♀ E13.0 woman in tuxedo: light skin tone
+1F935 1F3FC 200D 2640 FE0F ; fully-qualified # 🤵🏼‍♀️ E13.0 woman in tuxedo: medium-light skin tone
+1F935 1F3FC 200D 2640 ; minimally-qualified # 🤵🏼‍♀ E13.0 woman in tuxedo: medium-light skin tone
+1F935 1F3FD 200D 2640 FE0F ; fully-qualified # 🤵🏽‍♀️ E13.0 woman in tuxedo: medium skin tone
+1F935 1F3FD 200D 2640 ; minimally-qualified # 🤵🏽‍♀ E13.0 woman in tuxedo: medium skin tone
+1F935 1F3FE 200D 2640 FE0F ; fully-qualified # 🤵🏾‍♀️ E13.0 woman in tuxedo: medium-dark skin tone
+1F935 1F3FE 200D 2640 ; minimally-qualified # 🤵🏾‍♀ E13.0 woman in tuxedo: medium-dark skin tone
+1F935 1F3FF 200D 2640 FE0F ; fully-qualified # 🤵🏿‍♀️ E13.0 woman in tuxedo: dark skin tone
+1F935 1F3FF 200D 2640 ; minimally-qualified # 🤵🏿‍♀ E13.0 woman in tuxedo: dark skin tone
+1F470 ; fully-qualified # 👰 E0.6 person with veil
+1F470 1F3FB ; fully-qualified # 👰🏻 E1.0 person with veil: light skin tone
+1F470 1F3FC ; fully-qualified # 👰🏼 E1.0 person with veil: medium-light skin tone
+1F470 1F3FD ; fully-qualified # 👰🏽 E1.0 person with veil: medium skin tone
+1F470 1F3FE ; fully-qualified # 👰🏾 E1.0 person with veil: medium-dark skin tone
+1F470 1F3FF ; fully-qualified # 👰🏿 E1.0 person with veil: dark skin tone
+1F470 200D 2642 FE0F ; fully-qualified # 👰‍♂️ E13.0 man with veil
+1F470 200D 2642 ; minimally-qualified # 👰‍♂ E13.0 man with veil
+1F470 1F3FB 200D 2642 FE0F ; fully-qualified # 👰🏻‍♂️ E13.0 man with veil: light skin tone
+1F470 1F3FB 200D 2642 ; minimally-qualified # 👰🏻‍♂ E13.0 man with veil: light skin tone
+1F470 1F3FC 200D 2642 FE0F ; fully-qualified # 👰🏼‍♂️ E13.0 man with veil: medium-light skin tone
+1F470 1F3FC 200D 2642 ; minimally-qualified # 👰🏼‍♂ E13.0 man with veil: medium-light skin tone
+1F470 1F3FD 200D 2642 FE0F ; fully-qualified # 👰🏽‍♂️ E13.0 man with veil: medium skin tone
+1F470 1F3FD 200D 2642 ; minimally-qualified # 👰🏽‍♂ E13.0 man with veil: medium skin tone
+1F470 1F3FE 200D 2642 FE0F ; fully-qualified # 👰🏾‍♂️ E13.0 man with veil: medium-dark skin tone
+1F470 1F3FE 200D 2642 ; minimally-qualified # 👰🏾‍♂ E13.0 man with veil: medium-dark skin tone
+1F470 1F3FF 200D 2642 FE0F ; fully-qualified # 👰🏿‍♂️ E13.0 man with veil: dark skin tone
+1F470 1F3FF 200D 2642 ; minimally-qualified # 👰🏿‍♂ E13.0 man with veil: dark skin tone
+1F470 200D 2640 FE0F ; fully-qualified # 👰‍♀️ E13.0 woman with veil
+1F470 200D 2640 ; minimally-qualified # 👰‍♀ E13.0 woman with veil
+1F470 1F3FB 200D 2640 FE0F ; fully-qualified # 👰🏻‍♀️ E13.0 woman with veil: light skin tone
+1F470 1F3FB 200D 2640 ; minimally-qualified # 👰🏻‍♀ E13.0 woman with veil: light skin tone
+1F470 1F3FC 200D 2640 FE0F ; fully-qualified # 👰🏼‍♀️ E13.0 woman with veil: medium-light skin tone
+1F470 1F3FC 200D 2640 ; minimally-qualified # 👰🏼‍♀ E13.0 woman with veil: medium-light skin tone
+1F470 1F3FD 200D 2640 FE0F ; fully-qualified # 👰🏽‍♀️ E13.0 woman with veil: medium skin tone
+1F470 1F3FD 200D 2640 ; minimally-qualified # 👰🏽‍♀ E13.0 woman with veil: medium skin tone
+1F470 1F3FE 200D 2640 FE0F ; fully-qualified # 👰🏾‍♀️ E13.0 woman with veil: medium-dark skin tone
+1F470 1F3FE 200D 2640 ; minimally-qualified # 👰🏾‍♀ E13.0 woman with veil: medium-dark skin tone
+1F470 1F3FF 200D 2640 FE0F ; fully-qualified # 👰🏿‍♀️ E13.0 woman with veil: dark skin tone
+1F470 1F3FF 200D 2640 ; minimally-qualified # 👰🏿‍♀ E13.0 woman with veil: dark skin tone
+1F930 ; fully-qualified # 🤰 E3.0 pregnant woman
+1F930 1F3FB ; fully-qualified # 🤰🏻 E3.0 pregnant woman: light skin tone
+1F930 1F3FC ; fully-qualified # 🤰🏼 E3.0 pregnant woman: medium-light skin tone
+1F930 1F3FD ; fully-qualified # 🤰🏽 E3.0 pregnant woman: medium skin tone
+1F930 1F3FE ; fully-qualified # 🤰🏾 E3.0 pregnant woman: medium-dark skin tone
+1F930 1F3FF ; fully-qualified # 🤰🏿 E3.0 pregnant woman: dark skin tone
+1F931 ; fully-qualified # 🤱 E5.0 breast-feeding
+1F931 1F3FB ; fully-qualified # 🤱🏻 E5.0 breast-feeding: light skin tone
+1F931 1F3FC ; fully-qualified # 🤱🏼 E5.0 breast-feeding: medium-light skin tone
+1F931 1F3FD ; fully-qualified # 🤱🏽 E5.0 breast-feeding: medium skin tone
+1F931 1F3FE ; fully-qualified # 🤱🏾 E5.0 breast-feeding: medium-dark skin tone
+1F931 1F3FF ; fully-qualified # 🤱🏿 E5.0 breast-feeding: dark skin tone
+1F469 200D 1F37C ; fully-qualified # 👩‍🍼 E13.0 woman feeding baby
+1F469 1F3FB 200D 1F37C ; fully-qualified # 👩🏻‍🍼 E13.0 woman feeding baby: light skin tone
+1F469 1F3FC 200D 1F37C ; fully-qualified # 👩🏼‍🍼 E13.0 woman feeding baby: medium-light skin tone
+1F469 1F3FD 200D 1F37C ; fully-qualified # 👩🏽‍🍼 E13.0 woman feeding baby: medium skin tone
+1F469 1F3FE 200D 1F37C ; fully-qualified # 👩🏾‍🍼 E13.0 woman feeding baby: medium-dark skin tone
+1F469 1F3FF 200D 1F37C ; fully-qualified # 👩🏿‍🍼 E13.0 woman feeding baby: dark skin tone
+1F468 200D 1F37C ; fully-qualified # 👨‍🍼 E13.0 man feeding baby
+1F468 1F3FB 200D 1F37C ; fully-qualified # 👨🏻‍🍼 E13.0 man feeding baby: light skin tone
+1F468 1F3FC 200D 1F37C ; fully-qualified # 👨🏼‍🍼 E13.0 man feeding baby: medium-light skin tone
+1F468 1F3FD 200D 1F37C ; fully-qualified # 👨🏽‍🍼 E13.0 man feeding baby: medium skin tone
+1F468 1F3FE 200D 1F37C ; fully-qualified # 👨🏾‍🍼 E13.0 man feeding baby: medium-dark skin tone
+1F468 1F3FF 200D 1F37C ; fully-qualified # 👨🏿‍🍼 E13.0 man feeding baby: dark skin tone
+1F9D1 200D 1F37C ; fully-qualified # 🧑‍🍼 E13.0 person feeding baby
+1F9D1 1F3FB 200D 1F37C ; fully-qualified # 🧑🏻‍🍼 E13.0 person feeding baby: light skin tone
+1F9D1 1F3FC 200D 1F37C ; fully-qualified # 🧑🏼‍🍼 E13.0 person feeding baby: medium-light skin tone
+1F9D1 1F3FD 200D 1F37C ; fully-qualified # 🧑🏽‍🍼 E13.0 person feeding baby: medium skin tone
+1F9D1 1F3FE 200D 1F37C ; fully-qualified # 🧑🏾‍🍼 E13.0 person feeding baby: medium-dark skin tone
+1F9D1 1F3FF 200D 1F37C ; fully-qualified # 🧑🏿‍🍼 E13.0 person feeding baby: dark skin tone
+
+# subgroup: person-fantasy
+1F47C ; fully-qualified # 👼 E0.6 baby angel
+1F47C 1F3FB ; fully-qualified # 👼🏻 E1.0 baby angel: light skin tone
+1F47C 1F3FC ; fully-qualified # 👼🏼 E1.0 baby angel: medium-light skin tone
+1F47C 1F3FD ; fully-qualified # 👼🏽 E1.0 baby angel: medium skin tone
+1F47C 1F3FE ; fully-qualified # 👼🏾 E1.0 baby angel: medium-dark skin tone
+1F47C 1F3FF ; fully-qualified # 👼🏿 E1.0 baby angel: dark skin tone
+1F385 ; fully-qualified # 🎅 E0.6 Santa Claus
+1F385 1F3FB ; fully-qualified # 🎅🏻 E1.0 Santa Claus: light skin tone
+1F385 1F3FC ; fully-qualified # 🎅🏼 E1.0 Santa Claus: medium-light skin tone
+1F385 1F3FD ; fully-qualified # 🎅🏽 E1.0 Santa Claus: medium skin tone
+1F385 1F3FE ; fully-qualified # 🎅🏾 E1.0 Santa Claus: medium-dark skin tone
+1F385 1F3FF ; fully-qualified # 🎅🏿 E1.0 Santa Claus: dark skin tone
+1F936 ; fully-qualified # 🤶 E3.0 Mrs. Claus
+1F936 1F3FB ; fully-qualified # 🤶🏻 E3.0 Mrs. Claus: light skin tone
+1F936 1F3FC ; fully-qualified # 🤶🏼 E3.0 Mrs. Claus: medium-light skin tone
+1F936 1F3FD ; fully-qualified # 🤶🏽 E3.0 Mrs. Claus: medium skin tone
+1F936 1F3FE ; fully-qualified # 🤶🏾 E3.0 Mrs. Claus: medium-dark skin tone
+1F936 1F3FF ; fully-qualified # 🤶🏿 E3.0 Mrs. Claus: dark skin tone
+1F9D1 200D 1F384 ; fully-qualified # 🧑‍🎄 E13.0 mx claus
+1F9D1 1F3FB 200D 1F384 ; fully-qualified # 🧑🏻‍🎄 E13.0 mx claus: light skin tone
+1F9D1 1F3FC 200D 1F384 ; fully-qualified # 🧑🏼‍🎄 E13.0 mx claus: medium-light skin tone
+1F9D1 1F3FD 200D 1F384 ; fully-qualified # 🧑🏽‍🎄 E13.0 mx claus: medium skin tone
+1F9D1 1F3FE 200D 1F384 ; fully-qualified # 🧑🏾‍🎄 E13.0 mx claus: medium-dark skin tone
+1F9D1 1F3FF 200D 1F384 ; fully-qualified # 🧑🏿‍🎄 E13.0 mx claus: dark skin tone
+1F9B8 ; fully-qualified # 🦸 E11.0 superhero
+1F9B8 1F3FB ; fully-qualified # 🦸🏻 E11.0 superhero: light skin tone
+1F9B8 1F3FC ; fully-qualified # 🦸🏼 E11.0 superhero: medium-light skin tone
+1F9B8 1F3FD ; fully-qualified # 🦸🏽 E11.0 superhero: medium skin tone
+1F9B8 1F3FE ; fully-qualified # 🦸🏾 E11.0 superhero: medium-dark skin tone
+1F9B8 1F3FF ; fully-qualified # 🦸🏿 E11.0 superhero: dark skin tone
+1F9B8 200D 2642 FE0F ; fully-qualified # 🦸‍♂️ E11.0 man superhero
+1F9B8 200D 2642 ; minimally-qualified # 🦸‍♂ E11.0 man superhero
+1F9B8 1F3FB 200D 2642 FE0F ; fully-qualified # 🦸🏻‍♂️ E11.0 man superhero: light skin tone
+1F9B8 1F3FB 200D 2642 ; minimally-qualified # 🦸🏻‍♂ E11.0 man superhero: light skin tone
+1F9B8 1F3FC 200D 2642 FE0F ; fully-qualified # 🦸🏼‍♂️ E11.0 man superhero: medium-light skin tone
+1F9B8 1F3FC 200D 2642 ; minimally-qualified # 🦸🏼‍♂ E11.0 man superhero: medium-light skin tone
+1F9B8 1F3FD 200D 2642 FE0F ; fully-qualified # 🦸🏽‍♂️ E11.0 man superhero: medium skin tone
+1F9B8 1F3FD 200D 2642 ; minimally-qualified # 🦸🏽‍♂ E11.0 man superhero: medium skin tone
+1F9B8 1F3FE 200D 2642 FE0F ; fully-qualified # 🦸🏾‍♂️ E11.0 man superhero: medium-dark skin tone
+1F9B8 1F3FE 200D 2642 ; minimally-qualified # 🦸🏾‍♂ E11.0 man superhero: medium-dark skin tone
+1F9B8 1F3FF 200D 2642 FE0F ; fully-qualified # 🦸🏿‍♂️ E11.0 man superhero: dark skin tone
+1F9B8 1F3FF 200D 2642 ; minimally-qualified # 🦸🏿‍♂ E11.0 man superhero: dark skin tone
+1F9B8 200D 2640 FE0F ; fully-qualified # 🦸‍♀️ E11.0 woman superhero
+1F9B8 200D 2640 ; minimally-qualified # 🦸‍♀ E11.0 woman superhero
+1F9B8 1F3FB 200D 2640 FE0F ; fully-qualified # 🦸🏻‍♀️ E11.0 woman superhero: light skin tone
+1F9B8 1F3FB 200D 2640 ; minimally-qualified # 🦸🏻‍♀ E11.0 woman superhero: light skin tone
+1F9B8 1F3FC 200D 2640 FE0F ; fully-qualified # 🦸🏼‍♀️ E11.0 woman superhero: medium-light skin tone
+1F9B8 1F3FC 200D 2640 ; minimally-qualified # 🦸🏼‍♀ E11.0 woman superhero: medium-light skin tone
+1F9B8 1F3FD 200D 2640 FE0F ; fully-qualified # 🦸🏽‍♀️ E11.0 woman superhero: medium skin tone
+1F9B8 1F3FD 200D 2640 ; minimally-qualified # 🦸🏽‍♀ E11.0 woman superhero: medium skin tone
+1F9B8 1F3FE 200D 2640 FE0F ; fully-qualified # 🦸🏾‍♀️ E11.0 woman superhero: medium-dark skin tone
+1F9B8 1F3FE 200D 2640 ; minimally-qualified # 🦸🏾‍♀ E11.0 woman superhero: medium-dark skin tone
+1F9B8 1F3FF 200D 2640 FE0F ; fully-qualified # 🦸🏿‍♀️ E11.0 woman superhero: dark skin tone
+1F9B8 1F3FF 200D 2640 ; minimally-qualified # 🦸🏿‍♀ E11.0 woman superhero: dark skin tone
+1F9B9 ; fully-qualified # 🦹 E11.0 supervillain
+1F9B9 1F3FB ; fully-qualified # 🦹🏻 E11.0 supervillain: light skin tone
+1F9B9 1F3FC ; fully-qualified # 🦹🏼 E11.0 supervillain: medium-light skin tone
+1F9B9 1F3FD ; fully-qualified # 🦹🏽 E11.0 supervillain: medium skin tone
+1F9B9 1F3FE ; fully-qualified # 🦹🏾 E11.0 supervillain: medium-dark skin tone
+1F9B9 1F3FF ; fully-qualified # 🦹🏿 E11.0 supervillain: dark skin tone
+1F9B9 200D 2642 FE0F ; fully-qualified # 🦹‍♂️ E11.0 man supervillain
+1F9B9 200D 2642 ; minimally-qualified # 🦹‍♂ E11.0 man supervillain
+1F9B9 1F3FB 200D 2642 FE0F ; fully-qualified # 🦹🏻‍♂️ E11.0 man supervillain: light skin tone
+1F9B9 1F3FB 200D 2642 ; minimally-qualified # 🦹🏻‍♂ E11.0 man supervillain: light skin tone
+1F9B9 1F3FC 200D 2642 FE0F ; fully-qualified # 🦹🏼‍♂️ E11.0 man supervillain: medium-light skin tone
+1F9B9 1F3FC 200D 2642 ; minimally-qualified # 🦹🏼‍♂ E11.0 man supervillain: medium-light skin tone
+1F9B9 1F3FD 200D 2642 FE0F ; fully-qualified # 🦹🏽‍♂️ E11.0 man supervillain: medium skin tone
+1F9B9 1F3FD 200D 2642 ; minimally-qualified # 🦹🏽‍♂ E11.0 man supervillain: medium skin tone
+1F9B9 1F3FE 200D 2642 FE0F ; fully-qualified # 🦹🏾‍♂️ E11.0 man supervillain: medium-dark skin tone
+1F9B9 1F3FE 200D 2642 ; minimally-qualified # 🦹🏾‍♂ E11.0 man supervillain: medium-dark skin tone
+1F9B9 1F3FF 200D 2642 FE0F ; fully-qualified # 🦹🏿‍♂️ E11.0 man supervillain: dark skin tone
+1F9B9 1F3FF 200D 2642 ; minimally-qualified # 🦹🏿‍♂ E11.0 man supervillain: dark skin tone
+1F9B9 200D 2640 FE0F ; fully-qualified # 🦹‍♀️ E11.0 woman supervillain
+1F9B9 200D 2640 ; minimally-qualified # 🦹‍♀ E11.0 woman supervillain
+1F9B9 1F3FB 200D 2640 FE0F ; fully-qualified # 🦹🏻‍♀️ E11.0 woman supervillain: light skin tone
+1F9B9 1F3FB 200D 2640 ; minimally-qualified # 🦹🏻‍♀ E11.0 woman supervillain: light skin tone
+1F9B9 1F3FC 200D 2640 FE0F ; fully-qualified # 🦹🏼‍♀️ E11.0 woman supervillain: medium-light skin tone
+1F9B9 1F3FC 200D 2640 ; minimally-qualified # 🦹🏼‍♀ E11.0 woman supervillain: medium-light skin tone
+1F9B9 1F3FD 200D 2640 FE0F ; fully-qualified # 🦹🏽‍♀️ E11.0 woman supervillain: medium skin tone
+1F9B9 1F3FD 200D 2640 ; minimally-qualified # 🦹🏽‍♀ E11.0 woman supervillain: medium skin tone
+1F9B9 1F3FE 200D 2640 FE0F ; fully-qualified # 🦹🏾‍♀️ E11.0 woman supervillain: medium-dark skin tone
+1F9B9 1F3FE 200D 2640 ; minimally-qualified # 🦹🏾‍♀ E11.0 woman supervillain: medium-dark skin tone
+1F9B9 1F3FF 200D 2640 FE0F ; fully-qualified # 🦹🏿‍♀️ E11.0 woman supervillain: dark skin tone
+1F9B9 1F3FF 200D 2640 ; minimally-qualified # 🦹🏿‍♀ E11.0 woman supervillain: dark skin tone
+1F9D9 ; fully-qualified # 🧙 E5.0 mage
+1F9D9 1F3FB ; fully-qualified # 🧙🏻 E5.0 mage: light skin tone
+1F9D9 1F3FC ; fully-qualified # 🧙🏼 E5.0 mage: medium-light skin tone
+1F9D9 1F3FD ; fully-qualified # 🧙🏽 E5.0 mage: medium skin tone
+1F9D9 1F3FE ; fully-qualified # 🧙🏾 E5.0 mage: medium-dark skin tone
+1F9D9 1F3FF ; fully-qualified # 🧙🏿 E5.0 mage: dark skin tone
+1F9D9 200D 2642 FE0F ; fully-qualified # 🧙‍♂️ E5.0 man mage
+1F9D9 200D 2642 ; minimally-qualified # 🧙‍♂ E5.0 man mage
+1F9D9 1F3FB 200D 2642 FE0F ; fully-qualified # 🧙🏻‍♂️ E5.0 man mage: light skin tone
+1F9D9 1F3FB 200D 2642 ; minimally-qualified # 🧙🏻‍♂ E5.0 man mage: light skin tone
+1F9D9 1F3FC 200D 2642 FE0F ; fully-qualified # 🧙🏼‍♂️ E5.0 man mage: medium-light skin tone
+1F9D9 1F3FC 200D 2642 ; minimally-qualified # 🧙🏼‍♂ E5.0 man mage: medium-light skin tone
+1F9D9 1F3FD 200D 2642 FE0F ; fully-qualified # 🧙🏽‍♂️ E5.0 man mage: medium skin tone
+1F9D9 1F3FD 200D 2642 ; minimally-qualified # 🧙🏽‍♂ E5.0 man mage: medium skin tone
+1F9D9 1F3FE 200D 2642 FE0F ; fully-qualified # 🧙🏾‍♂️ E5.0 man mage: medium-dark skin tone
+1F9D9 1F3FE 200D 2642 ; minimally-qualified # 🧙🏾‍♂ E5.0 man mage: medium-dark skin tone
+1F9D9 1F3FF 200D 2642 FE0F ; fully-qualified # 🧙🏿‍♂️ E5.0 man mage: dark skin tone
+1F9D9 1F3FF 200D 2642 ; minimally-qualified # 🧙🏿‍♂ E5.0 man mage: dark skin tone
+1F9D9 200D 2640 FE0F ; fully-qualified # 🧙‍♀️ E5.0 woman mage
+1F9D9 200D 2640 ; minimally-qualified # 🧙‍♀ E5.0 woman mage
+1F9D9 1F3FB 200D 2640 FE0F ; fully-qualified # 🧙🏻‍♀️ E5.0 woman mage: light skin tone
+1F9D9 1F3FB 200D 2640 ; minimally-qualified # 🧙🏻‍♀ E5.0 woman mage: light skin tone
+1F9D9 1F3FC 200D 2640 FE0F ; fully-qualified # 🧙🏼‍♀️ E5.0 woman mage: medium-light skin tone
+1F9D9 1F3FC 200D 2640 ; minimally-qualified # 🧙🏼‍♀ E5.0 woman mage: medium-light skin tone
+1F9D9 1F3FD 200D 2640 FE0F ; fully-qualified # 🧙🏽‍♀️ E5.0 woman mage: medium skin tone
+1F9D9 1F3FD 200D 2640 ; minimally-qualified # 🧙🏽‍♀ E5.0 woman mage: medium skin tone
+1F9D9 1F3FE 200D 2640 FE0F ; fully-qualified # 🧙🏾‍♀️ E5.0 woman mage: medium-dark skin tone
+1F9D9 1F3FE 200D 2640 ; minimally-qualified # 🧙🏾‍♀ E5.0 woman mage: medium-dark skin tone
+1F9D9 1F3FF 200D 2640 FE0F ; fully-qualified # 🧙🏿‍♀️ E5.0 woman mage: dark skin tone
+1F9D9 1F3FF 200D 2640 ; minimally-qualified # 🧙🏿‍♀ E5.0 woman mage: dark skin tone
+1F9DA ; fully-qualified # 🧚 E5.0 fairy
+1F9DA 1F3FB ; fully-qualified # 🧚🏻 E5.0 fairy: light skin tone
+1F9DA 1F3FC ; fully-qualified # 🧚🏼 E5.0 fairy: medium-light skin tone
+1F9DA 1F3FD ; fully-qualified # 🧚🏽 E5.0 fairy: medium skin tone
+1F9DA 1F3FE ; fully-qualified # 🧚🏾 E5.0 fairy: medium-dark skin tone
+1F9DA 1F3FF ; fully-qualified # 🧚🏿 E5.0 fairy: dark skin tone
+1F9DA 200D 2642 FE0F ; fully-qualified # 🧚‍♂️ E5.0 man fairy
+1F9DA 200D 2642 ; minimally-qualified # 🧚‍♂ E5.0 man fairy
+1F9DA 1F3FB 200D 2642 FE0F ; fully-qualified # 🧚🏻‍♂️ E5.0 man fairy: light skin tone
+1F9DA 1F3FB 200D 2642 ; minimally-qualified # 🧚🏻‍♂ E5.0 man fairy: light skin tone
+1F9DA 1F3FC 200D 2642 FE0F ; fully-qualified # 🧚🏼‍♂️ E5.0 man fairy: medium-light skin tone
+1F9DA 1F3FC 200D 2642 ; minimally-qualified # 🧚🏼‍♂ E5.0 man fairy: medium-light skin tone
+1F9DA 1F3FD 200D 2642 FE0F ; fully-qualified # 🧚🏽‍♂️ E5.0 man fairy: medium skin tone
+1F9DA 1F3FD 200D 2642 ; minimally-qualified # 🧚🏽‍♂ E5.0 man fairy: medium skin tone
+1F9DA 1F3FE 200D 2642 FE0F ; fully-qualified # 🧚🏾‍♂️ E5.0 man fairy: medium-dark skin tone
+1F9DA 1F3FE 200D 2642 ; minimally-qualified # 🧚🏾‍♂ E5.0 man fairy: medium-dark skin tone
+1F9DA 1F3FF 200D 2642 FE0F ; fully-qualified # 🧚🏿‍♂️ E5.0 man fairy: dark skin tone
+1F9DA 1F3FF 200D 2642 ; minimally-qualified # 🧚🏿‍♂ E5.0 man fairy: dark skin tone
+1F9DA 200D 2640 FE0F ; fully-qualified # 🧚‍♀️ E5.0 woman fairy
+1F9DA 200D 2640 ; minimally-qualified # 🧚‍♀ E5.0 woman fairy
+1F9DA 1F3FB 200D 2640 FE0F ; fully-qualified # 🧚🏻‍♀️ E5.0 woman fairy: light skin tone
+1F9DA 1F3FB 200D 2640 ; minimally-qualified # 🧚🏻‍♀ E5.0 woman fairy: light skin tone
+1F9DA 1F3FC 200D 2640 FE0F ; fully-qualified # 🧚🏼‍♀️ E5.0 woman fairy: medium-light skin tone
+1F9DA 1F3FC 200D 2640 ; minimally-qualified # 🧚🏼‍♀ E5.0 woman fairy: medium-light skin tone
+1F9DA 1F3FD 200D 2640 FE0F ; fully-qualified # 🧚🏽‍♀️ E5.0 woman fairy: medium skin tone
+1F9DA 1F3FD 200D 2640 ; minimally-qualified # 🧚🏽‍♀ E5.0 woman fairy: medium skin tone
+1F9DA 1F3FE 200D 2640 FE0F ; fully-qualified # 🧚🏾‍♀️ E5.0 woman fairy: medium-dark skin tone
+1F9DA 1F3FE 200D 2640 ; minimally-qualified # 🧚🏾‍♀ E5.0 woman fairy: medium-dark skin tone
+1F9DA 1F3FF 200D 2640 FE0F ; fully-qualified # 🧚🏿‍♀️ E5.0 woman fairy: dark skin tone
+1F9DA 1F3FF 200D 2640 ; minimally-qualified # 🧚🏿‍♀ E5.0 woman fairy: dark skin tone
+1F9DB ; fully-qualified # 🧛 E5.0 vampire
+1F9DB 1F3FB ; fully-qualified # 🧛🏻 E5.0 vampire: light skin tone
+1F9DB 1F3FC ; fully-qualified # 🧛🏼 E5.0 vampire: medium-light skin tone
+1F9DB 1F3FD ; fully-qualified # 🧛🏽 E5.0 vampire: medium skin tone
+1F9DB 1F3FE ; fully-qualified # 🧛🏾 E5.0 vampire: medium-dark skin tone
+1F9DB 1F3FF ; fully-qualified # 🧛🏿 E5.0 vampire: dark skin tone
+1F9DB 200D 2642 FE0F ; fully-qualified # 🧛‍♂️ E5.0 man vampire
+1F9DB 200D 2642 ; minimally-qualified # 🧛‍♂ E5.0 man vampire
+1F9DB 1F3FB 200D 2642 FE0F ; fully-qualified # 🧛🏻‍♂️ E5.0 man vampire: light skin tone
+1F9DB 1F3FB 200D 2642 ; minimally-qualified # 🧛🏻‍♂ E5.0 man vampire: light skin tone
+1F9DB 1F3FC 200D 2642 FE0F ; fully-qualified # 🧛🏼‍♂️ E5.0 man vampire: medium-light skin tone
+1F9DB 1F3FC 200D 2642 ; minimally-qualified # 🧛🏼‍♂ E5.0 man vampire: medium-light skin tone
+1F9DB 1F3FD 200D 2642 FE0F ; fully-qualified # 🧛🏽‍♂️ E5.0 man vampire: medium skin tone
+1F9DB 1F3FD 200D 2642 ; minimally-qualified # 🧛🏽‍♂ E5.0 man vampire: medium skin tone
+1F9DB 1F3FE 200D 2642 FE0F ; fully-qualified # 🧛🏾‍♂️ E5.0 man vampire: medium-dark skin tone
+1F9DB 1F3FE 200D 2642 ; minimally-qualified # 🧛🏾‍♂ E5.0 man vampire: medium-dark skin tone
+1F9DB 1F3FF 200D 2642 FE0F ; fully-qualified # 🧛🏿‍♂️ E5.0 man vampire: dark skin tone
+1F9DB 1F3FF 200D 2642 ; minimally-qualified # 🧛🏿‍♂ E5.0 man vampire: dark skin tone
+1F9DB 200D 2640 FE0F ; fully-qualified # 🧛‍♀️ E5.0 woman vampire
+1F9DB 200D 2640 ; minimally-qualified # 🧛‍♀ E5.0 woman vampire
+1F9DB 1F3FB 200D 2640 FE0F ; fully-qualified # 🧛🏻‍♀️ E5.0 woman vampire: light skin tone
+1F9DB 1F3FB 200D 2640 ; minimally-qualified # 🧛🏻‍♀ E5.0 woman vampire: light skin tone
+1F9DB 1F3FC 200D 2640 FE0F ; fully-qualified # 🧛🏼‍♀️ E5.0 woman vampire: medium-light skin tone
+1F9DB 1F3FC 200D 2640 ; minimally-qualified # 🧛🏼‍♀ E5.0 woman vampire: medium-light skin tone
+1F9DB 1F3FD 200D 2640 FE0F ; fully-qualified # 🧛🏽‍♀️ E5.0 woman vampire: medium skin tone
+1F9DB 1F3FD 200D 2640 ; minimally-qualified # 🧛🏽‍♀ E5.0 woman vampire: medium skin tone
+1F9DB 1F3FE 200D 2640 FE0F ; fully-qualified # 🧛🏾‍♀️ E5.0 woman vampire: medium-dark skin tone
+1F9DB 1F3FE 200D 2640 ; minimally-qualified # 🧛🏾‍♀ E5.0 woman vampire: medium-dark skin tone
+1F9DB 1F3FF 200D 2640 FE0F ; fully-qualified # 🧛🏿‍♀️ E5.0 woman vampire: dark skin tone
+1F9DB 1F3FF 200D 2640 ; minimally-qualified # 🧛🏿‍♀ E5.0 woman vampire: dark skin tone
+1F9DC ; fully-qualified # 🧜 E5.0 merperson
+1F9DC 1F3FB ; fully-qualified # 🧜🏻 E5.0 merperson: light skin tone
+1F9DC 1F3FC ; fully-qualified # 🧜🏼 E5.0 merperson: medium-light skin tone
+1F9DC 1F3FD ; fully-qualified # 🧜🏽 E5.0 merperson: medium skin tone
+1F9DC 1F3FE ; fully-qualified # 🧜🏾 E5.0 merperson: medium-dark skin tone
+1F9DC 1F3FF ; fully-qualified # 🧜🏿 E5.0 merperson: dark skin tone
+1F9DC 200D 2642 FE0F ; fully-qualified # 🧜‍♂️ E5.0 merman
+1F9DC 200D 2642 ; minimally-qualified # 🧜‍♂ E5.0 merman
+1F9DC 1F3FB 200D 2642 FE0F ; fully-qualified # 🧜🏻‍♂️ E5.0 merman: light skin tone
+1F9DC 1F3FB 200D 2642 ; minimally-qualified # 🧜🏻‍♂ E5.0 merman: light skin tone
+1F9DC 1F3FC 200D 2642 FE0F ; fully-qualified # 🧜🏼‍♂️ E5.0 merman: medium-light skin tone
+1F9DC 1F3FC 200D 2642 ; minimally-qualified # 🧜🏼‍♂ E5.0 merman: medium-light skin tone
+1F9DC 1F3FD 200D 2642 FE0F ; fully-qualified # 🧜🏽‍♂️ E5.0 merman: medium skin tone
+1F9DC 1F3FD 200D 2642 ; minimally-qualified # 🧜🏽‍♂ E5.0 merman: medium skin tone
+1F9DC 1F3FE 200D 2642 FE0F ; fully-qualified # 🧜🏾‍♂️ E5.0 merman: medium-dark skin tone
+1F9DC 1F3FE 200D 2642 ; minimally-qualified # 🧜🏾‍♂ E5.0 merman: medium-dark skin tone
+1F9DC 1F3FF 200D 2642 FE0F ; fully-qualified # 🧜🏿‍♂️ E5.0 merman: dark skin tone
+1F9DC 1F3FF 200D 2642 ; minimally-qualified # 🧜🏿‍♂ E5.0 merman: dark skin tone
+1F9DC 200D 2640 FE0F ; fully-qualified # 🧜‍♀️ E5.0 mermaid
+1F9DC 200D 2640 ; minimally-qualified # 🧜‍♀ E5.0 mermaid
+1F9DC 1F3FB 200D 2640 FE0F ; fully-qualified # 🧜🏻‍♀️ E5.0 mermaid: light skin tone
+1F9DC 1F3FB 200D 2640 ; minimally-qualified # 🧜🏻‍♀ E5.0 mermaid: light skin tone
+1F9DC 1F3FC 200D 2640 FE0F ; fully-qualified # 🧜🏼‍♀️ E5.0 mermaid: medium-light skin tone
+1F9DC 1F3FC 200D 2640 ; minimally-qualified # 🧜🏼‍♀ E5.0 mermaid: medium-light skin tone
+1F9DC 1F3FD 200D 2640 FE0F ; fully-qualified # 🧜🏽‍♀️ E5.0 mermaid: medium skin tone
+1F9DC 1F3FD 200D 2640 ; minimally-qualified # 🧜🏽‍♀ E5.0 mermaid: medium skin tone
+1F9DC 1F3FE 200D 2640 FE0F ; fully-qualified # 🧜🏾‍♀️ E5.0 mermaid: medium-dark skin tone
+1F9DC 1F3FE 200D 2640 ; minimally-qualified # 🧜🏾‍♀ E5.0 mermaid: medium-dark skin tone
+1F9DC 1F3FF 200D 2640 FE0F ; fully-qualified # 🧜🏿‍♀️ E5.0 mermaid: dark skin tone
+1F9DC 1F3FF 200D 2640 ; minimally-qualified # 🧜🏿‍♀ E5.0 mermaid: dark skin tone
+1F9DD ; fully-qualified # 🧝 E5.0 elf
+1F9DD 1F3FB ; fully-qualified # 🧝🏻 E5.0 elf: light skin tone
+1F9DD 1F3FC ; fully-qualified # 🧝🏼 E5.0 elf: medium-light skin tone
+1F9DD 1F3FD ; fully-qualified # 🧝🏽 E5.0 elf: medium skin tone
+1F9DD 1F3FE ; fully-qualified # 🧝🏾 E5.0 elf: medium-dark skin tone
+1F9DD 1F3FF ; fully-qualified # 🧝🏿 E5.0 elf: dark skin tone
+1F9DD 200D 2642 FE0F ; fully-qualified # 🧝‍♂️ E5.0 man elf
+1F9DD 200D 2642 ; minimally-qualified # 🧝‍♂ E5.0 man elf
+1F9DD 1F3FB 200D 2642 FE0F ; fully-qualified # 🧝🏻‍♂️ E5.0 man elf: light skin tone
+1F9DD 1F3FB 200D 2642 ; minimally-qualified # 🧝🏻‍♂ E5.0 man elf: light skin tone
+1F9DD 1F3FC 200D 2642 FE0F ; fully-qualified # 🧝🏼‍♂️ E5.0 man elf: medium-light skin tone
+1F9DD 1F3FC 200D 2642 ; minimally-qualified # 🧝🏼‍♂ E5.0 man elf: medium-light skin tone
+1F9DD 1F3FD 200D 2642 FE0F ; fully-qualified # 🧝🏽‍♂️ E5.0 man elf: medium skin tone
+1F9DD 1F3FD 200D 2642 ; minimally-qualified # 🧝🏽‍♂ E5.0 man elf: medium skin tone
+1F9DD 1F3FE 200D 2642 FE0F ; fully-qualified # 🧝🏾‍♂️ E5.0 man elf: medium-dark skin tone
+1F9DD 1F3FE 200D 2642 ; minimally-qualified # 🧝🏾‍♂ E5.0 man elf: medium-dark skin tone
+1F9DD 1F3FF 200D 2642 FE0F ; fully-qualified # 🧝🏿‍♂️ E5.0 man elf: dark skin tone
+1F9DD 1F3FF 200D 2642 ; minimally-qualified # 🧝🏿‍♂ E5.0 man elf: dark skin tone
+1F9DD 200D 2640 FE0F ; fully-qualified # 🧝‍♀️ E5.0 woman elf
+1F9DD 200D 2640 ; minimally-qualified # 🧝‍♀ E5.0 woman elf
+1F9DD 1F3FB 200D 2640 FE0F ; fully-qualified # 🧝🏻‍♀️ E5.0 woman elf: light skin tone
+1F9DD 1F3FB 200D 2640 ; minimally-qualified # 🧝🏻‍♀ E5.0 woman elf: light skin tone
+1F9DD 1F3FC 200D 2640 FE0F ; fully-qualified # 🧝🏼‍♀️ E5.0 woman elf: medium-light skin tone
+1F9DD 1F3FC 200D 2640 ; minimally-qualified # 🧝🏼‍♀ E5.0 woman elf: medium-light skin tone
+1F9DD 1F3FD 200D 2640 FE0F ; fully-qualified # 🧝🏽‍♀️ E5.0 woman elf: medium skin tone
+1F9DD 1F3FD 200D 2640 ; minimally-qualified # 🧝🏽‍♀ E5.0 woman elf: medium skin tone
+1F9DD 1F3FE 200D 2640 FE0F ; fully-qualified # 🧝🏾‍♀️ E5.0 woman elf: medium-dark skin tone
+1F9DD 1F3FE 200D 2640 ; minimally-qualified # 🧝🏾‍♀ E5.0 woman elf: medium-dark skin tone
+1F9DD 1F3FF 200D 2640 FE0F ; fully-qualified # 🧝🏿‍♀️ E5.0 woman elf: dark skin tone
+1F9DD 1F3FF 200D 2640 ; minimally-qualified # 🧝🏿‍♀ E5.0 woman elf: dark skin tone
+1F9DE ; fully-qualified # 🧞 E5.0 genie
+1F9DE 200D 2642 FE0F ; fully-qualified # 🧞‍♂️ E5.0 man genie
+1F9DE 200D 2642 ; minimally-qualified # 🧞‍♂ E5.0 man genie
+1F9DE 200D 2640 FE0F ; fully-qualified # 🧞‍♀️ E5.0 woman genie
+1F9DE 200D 2640 ; minimally-qualified # 🧞‍♀ E5.0 woman genie
+1F9DF ; fully-qualified # 🧟 E5.0 zombie
+1F9DF 200D 2642 FE0F ; fully-qualified # 🧟‍♂️ E5.0 man zombie
+1F9DF 200D 2642 ; minimally-qualified # 🧟‍♂ E5.0 man zombie
+1F9DF 200D 2640 FE0F ; fully-qualified # 🧟‍♀️ E5.0 woman zombie
+1F9DF 200D 2640 ; minimally-qualified # 🧟‍♀ E5.0 woman zombie
+
+# subgroup: person-activity
+1F486 ; fully-qualified # 💆 E0.6 person getting massage
+1F486 1F3FB ; fully-qualified # 💆🏻 E1.0 person getting massage: light skin tone
+1F486 1F3FC ; fully-qualified # 💆🏼 E1.0 person getting massage: medium-light skin tone
+1F486 1F3FD ; fully-qualified # 💆🏽 E1.0 person getting massage: medium skin tone
+1F486 1F3FE ; fully-qualified # 💆🏾 E1.0 person getting massage: medium-dark skin tone
+1F486 1F3FF ; fully-qualified # 💆🏿 E1.0 person getting massage: dark skin tone
+1F486 200D 2642 FE0F ; fully-qualified # 💆‍♂️ E4.0 man getting massage
+1F486 200D 2642 ; minimally-qualified # 💆‍♂ E4.0 man getting massage
+1F486 1F3FB 200D 2642 FE0F ; fully-qualified # 💆🏻‍♂️ E4.0 man getting massage: light skin tone
+1F486 1F3FB 200D 2642 ; minimally-qualified # 💆🏻‍♂ E4.0 man getting massage: light skin tone
+1F486 1F3FC 200D 2642 FE0F ; fully-qualified # 💆🏼‍♂️ E4.0 man getting massage: medium-light skin tone
+1F486 1F3FC 200D 2642 ; minimally-qualified # 💆🏼‍♂ E4.0 man getting massage: medium-light skin tone
+1F486 1F3FD 200D 2642 FE0F ; fully-qualified # 💆🏽‍♂️ E4.0 man getting massage: medium skin tone
+1F486 1F3FD 200D 2642 ; minimally-qualified # 💆🏽‍♂ E4.0 man getting massage: medium skin tone
+1F486 1F3FE 200D 2642 FE0F ; fully-qualified # 💆🏾‍♂️ E4.0 man getting massage: medium-dark skin tone
+1F486 1F3FE 200D 2642 ; minimally-qualified # 💆🏾‍♂ E4.0 man getting massage: medium-dark skin tone
+1F486 1F3FF 200D 2642 FE0F ; fully-qualified # 💆🏿‍♂️ E4.0 man getting massage: dark skin tone
+1F486 1F3FF 200D 2642 ; minimally-qualified # 💆🏿‍♂ E4.0 man getting massage: dark skin tone
+1F486 200D 2640 FE0F ; fully-qualified # 💆‍♀️ E4.0 woman getting massage
+1F486 200D 2640 ; minimally-qualified # 💆‍♀ E4.0 woman getting massage
+1F486 1F3FB 200D 2640 FE0F ; fully-qualified # 💆🏻‍♀️ E4.0 woman getting massage: light skin tone
+1F486 1F3FB 200D 2640 ; minimally-qualified # 💆🏻‍♀ E4.0 woman getting massage: light skin tone
+1F486 1F3FC 200D 2640 FE0F ; fully-qualified # 💆🏼‍♀️ E4.0 woman getting massage: medium-light skin tone
+1F486 1F3FC 200D 2640 ; minimally-qualified # 💆🏼‍♀ E4.0 woman getting massage: medium-light skin tone
+1F486 1F3FD 200D 2640 FE0F ; fully-qualified # 💆🏽‍♀️ E4.0 woman getting massage: medium skin tone
+1F486 1F3FD 200D 2640 ; minimally-qualified # 💆🏽‍♀ E4.0 woman getting massage: medium skin tone
+1F486 1F3FE 200D 2640 FE0F ; fully-qualified # 💆🏾‍♀️ E4.0 woman getting massage: medium-dark skin tone
+1F486 1F3FE 200D 2640 ; minimally-qualified # 💆🏾‍♀ E4.0 woman getting massage: medium-dark skin tone
+1F486 1F3FF 200D 2640 FE0F ; fully-qualified # 💆🏿‍♀️ E4.0 woman getting massage: dark skin tone
+1F486 1F3FF 200D 2640 ; minimally-qualified # 💆🏿‍♀ E4.0 woman getting massage: dark skin tone
+1F487 ; fully-qualified # 💇 E0.6 person getting haircut
+1F487 1F3FB ; fully-qualified # 💇🏻 E1.0 person getting haircut: light skin tone
+1F487 1F3FC ; fully-qualified # 💇🏼 E1.0 person getting haircut: medium-light skin tone
+1F487 1F3FD ; fully-qualified # 💇🏽 E1.0 person getting haircut: medium skin tone
+1F487 1F3FE ; fully-qualified # 💇🏾 E1.0 person getting haircut: medium-dark skin tone
+1F487 1F3FF ; fully-qualified # 💇🏿 E1.0 person getting haircut: dark skin tone
+1F487 200D 2642 FE0F ; fully-qualified # 💇‍♂️ E4.0 man getting haircut
+1F487 200D 2642 ; minimally-qualified # 💇‍♂ E4.0 man getting haircut
+1F487 1F3FB 200D 2642 FE0F ; fully-qualified # 💇🏻‍♂️ E4.0 man getting haircut: light skin tone
+1F487 1F3FB 200D 2642 ; minimally-qualified # 💇🏻‍♂ E4.0 man getting haircut: light skin tone
+1F487 1F3FC 200D 2642 FE0F ; fully-qualified # 💇🏼‍♂️ E4.0 man getting haircut: medium-light skin tone
+1F487 1F3FC 200D 2642 ; minimally-qualified # 💇🏼‍♂ E4.0 man getting haircut: medium-light skin tone
+1F487 1F3FD 200D 2642 FE0F ; fully-qualified # 💇🏽‍♂️ E4.0 man getting haircut: medium skin tone
+1F487 1F3FD 200D 2642 ; minimally-qualified # 💇🏽‍♂ E4.0 man getting haircut: medium skin tone
+1F487 1F3FE 200D 2642 FE0F ; fully-qualified # 💇🏾‍♂️ E4.0 man getting haircut: medium-dark skin tone
+1F487 1F3FE 200D 2642 ; minimally-qualified # 💇🏾‍♂ E4.0 man getting haircut: medium-dark skin tone
+1F487 1F3FF 200D 2642 FE0F ; fully-qualified # 💇🏿‍♂️ E4.0 man getting haircut: dark skin tone
+1F487 1F3FF 200D 2642 ; minimally-qualified # 💇🏿‍♂ E4.0 man getting haircut: dark skin tone
+1F487 200D 2640 FE0F ; fully-qualified # 💇‍♀️ E4.0 woman getting haircut
+1F487 200D 2640 ; minimally-qualified # 💇‍♀ E4.0 woman getting haircut
+1F487 1F3FB 200D 2640 FE0F ; fully-qualified # 💇🏻‍♀️ E4.0 woman getting haircut: light skin tone
+1F487 1F3FB 200D 2640 ; minimally-qualified # 💇🏻‍♀ E4.0 woman getting haircut: light skin tone
+1F487 1F3FC 200D 2640 FE0F ; fully-qualified # 💇🏼‍♀️ E4.0 woman getting haircut: medium-light skin tone
+1F487 1F3FC 200D 2640 ; minimally-qualified # 💇🏼‍♀ E4.0 woman getting haircut: medium-light skin tone
+1F487 1F3FD 200D 2640 FE0F ; fully-qualified # 💇🏽‍♀️ E4.0 woman getting haircut: medium skin tone
+1F487 1F3FD 200D 2640 ; minimally-qualified # 💇🏽‍♀ E4.0 woman getting haircut: medium skin tone
+1F487 1F3FE 200D 2640 FE0F ; fully-qualified # 💇🏾‍♀️ E4.0 woman getting haircut: medium-dark skin tone
+1F487 1F3FE 200D 2640 ; minimally-qualified # 💇🏾‍♀ E4.0 woman getting haircut: medium-dark skin tone
+1F487 1F3FF 200D 2640 FE0F ; fully-qualified # 💇🏿‍♀️ E4.0 woman getting haircut: dark skin tone
+1F487 1F3FF 200D 2640 ; minimally-qualified # 💇🏿‍♀ E4.0 woman getting haircut: dark skin tone
+1F6B6 ; fully-qualified # 🚶 E0.6 person walking
+1F6B6 1F3FB ; fully-qualified # 🚶🏻 E1.0 person walking: light skin tone
+1F6B6 1F3FC ; fully-qualified # 🚶🏼 E1.0 person walking: medium-light skin tone
+1F6B6 1F3FD ; fully-qualified # 🚶🏽 E1.0 person walking: medium skin tone
+1F6B6 1F3FE ; fully-qualified # 🚶🏾 E1.0 person walking: medium-dark skin tone
+1F6B6 1F3FF ; fully-qualified # 🚶🏿 E1.0 person walking: dark skin tone
+1F6B6 200D 2642 FE0F ; fully-qualified # 🚶‍♂️ E4.0 man walking
+1F6B6 200D 2642 ; minimally-qualified # 🚶‍♂ E4.0 man walking
+1F6B6 1F3FB 200D 2642 FE0F ; fully-qualified # 🚶🏻‍♂️ E4.0 man walking: light skin tone
+1F6B6 1F3FB 200D 2642 ; minimally-qualified # 🚶🏻‍♂ E4.0 man walking: light skin tone
+1F6B6 1F3FC 200D 2642 FE0F ; fully-qualified # 🚶🏼‍♂️ E4.0 man walking: medium-light skin tone
+1F6B6 1F3FC 200D 2642 ; minimally-qualified # 🚶🏼‍♂ E4.0 man walking: medium-light skin tone
+1F6B6 1F3FD 200D 2642 FE0F ; fully-qualified # 🚶🏽‍♂️ E4.0 man walking: medium skin tone
+1F6B6 1F3FD 200D 2642 ; minimally-qualified # 🚶🏽‍♂ E4.0 man walking: medium skin tone
+1F6B6 1F3FE 200D 2642 FE0F ; fully-qualified # 🚶🏾‍♂️ E4.0 man walking: medium-dark skin tone
+1F6B6 1F3FE 200D 2642 ; minimally-qualified # 🚶🏾‍♂ E4.0 man walking: medium-dark skin tone
+1F6B6 1F3FF 200D 2642 FE0F ; fully-qualified # 🚶🏿‍♂️ E4.0 man walking: dark skin tone
+1F6B6 1F3FF 200D 2642 ; minimally-qualified # 🚶🏿‍♂ E4.0 man walking: dark skin tone
+1F6B6 200D 2640 FE0F ; fully-qualified # 🚶‍♀️ E4.0 woman walking
+1F6B6 200D 2640 ; minimally-qualified # 🚶‍♀ E4.0 woman walking
+1F6B6 1F3FB 200D 2640 FE0F ; fully-qualified # 🚶🏻‍♀️ E4.0 woman walking: light skin tone
+1F6B6 1F3FB 200D 2640 ; minimally-qualified # 🚶🏻‍♀ E4.0 woman walking: light skin tone
+1F6B6 1F3FC 200D 2640 FE0F ; fully-qualified # 🚶🏼‍♀️ E4.0 woman walking: medium-light skin tone
+1F6B6 1F3FC 200D 2640 ; minimally-qualified # 🚶🏼‍♀ E4.0 woman walking: medium-light skin tone
+1F6B6 1F3FD 200D 2640 FE0F ; fully-qualified # 🚶🏽‍♀️ E4.0 woman walking: medium skin tone
+1F6B6 1F3FD 200D 2640 ; minimally-qualified # 🚶🏽‍♀ E4.0 woman walking: medium skin tone
+1F6B6 1F3FE 200D 2640 FE0F ; fully-qualified # 🚶🏾‍♀️ E4.0 woman walking: medium-dark skin tone
+1F6B6 1F3FE 200D 2640 ; minimally-qualified # 🚶🏾‍♀ E4.0 woman walking: medium-dark skin tone
+1F6B6 1F3FF 200D 2640 FE0F ; fully-qualified # 🚶🏿‍♀️ E4.0 woman walking: dark skin tone
+1F6B6 1F3FF 200D 2640 ; minimally-qualified # 🚶🏿‍♀ E4.0 woman walking: dark skin tone
+1F9CD ; fully-qualified # 🧍 E12.0 person standing
+1F9CD 1F3FB ; fully-qualified # 🧍🏻 E12.0 person standing: light skin tone
+1F9CD 1F3FC ; fully-qualified # 🧍🏼 E12.0 person standing: medium-light skin tone
+1F9CD 1F3FD ; fully-qualified # 🧍🏽 E12.0 person standing: medium skin tone
+1F9CD 1F3FE ; fully-qualified # 🧍🏾 E12.0 person standing: medium-dark skin tone
+1F9CD 1F3FF ; fully-qualified # 🧍🏿 E12.0 person standing: dark skin tone
+1F9CD 200D 2642 FE0F ; fully-qualified # 🧍‍♂️ E12.0 man standing
+1F9CD 200D 2642 ; minimally-qualified # 🧍‍♂ E12.0 man standing
+1F9CD 1F3FB 200D 2642 FE0F ; fully-qualified # 🧍🏻‍♂️ E12.0 man standing: light skin tone
+1F9CD 1F3FB 200D 2642 ; minimally-qualified # 🧍🏻‍♂ E12.0 man standing: light skin tone
+1F9CD 1F3FC 200D 2642 FE0F ; fully-qualified # 🧍🏼‍♂️ E12.0 man standing: medium-light skin tone
+1F9CD 1F3FC 200D 2642 ; minimally-qualified # 🧍🏼‍♂ E12.0 man standing: medium-light skin tone
+1F9CD 1F3FD 200D 2642 FE0F ; fully-qualified # 🧍🏽‍♂️ E12.0 man standing: medium skin tone
+1F9CD 1F3FD 200D 2642 ; minimally-qualified # 🧍🏽‍♂ E12.0 man standing: medium skin tone
+1F9CD 1F3FE 200D 2642 FE0F ; fully-qualified # 🧍🏾‍♂️ E12.0 man standing: medium-dark skin tone
+1F9CD 1F3FE 200D 2642 ; minimally-qualified # 🧍🏾‍♂ E12.0 man standing: medium-dark skin tone
+1F9CD 1F3FF 200D 2642 FE0F ; fully-qualified # 🧍🏿‍♂️ E12.0 man standing: dark skin tone
+1F9CD 1F3FF 200D 2642 ; minimally-qualified # 🧍🏿‍♂ E12.0 man standing: dark skin tone
+1F9CD 200D 2640 FE0F ; fully-qualified # 🧍‍♀️ E12.0 woman standing
+1F9CD 200D 2640 ; minimally-qualified # 🧍‍♀ E12.0 woman standing
+1F9CD 1F3FB 200D 2640 FE0F ; fully-qualified # 🧍🏻‍♀️ E12.0 woman standing: light skin tone
+1F9CD 1F3FB 200D 2640 ; minimally-qualified # 🧍🏻‍♀ E12.0 woman standing: light skin tone
+1F9CD 1F3FC 200D 2640 FE0F ; fully-qualified # 🧍🏼‍♀️ E12.0 woman standing: medium-light skin tone
+1F9CD 1F3FC 200D 2640 ; minimally-qualified # 🧍🏼‍♀ E12.0 woman standing: medium-light skin tone
+1F9CD 1F3FD 200D 2640 FE0F ; fully-qualified # 🧍🏽‍♀️ E12.0 woman standing: medium skin tone
+1F9CD 1F3FD 200D 2640 ; minimally-qualified # 🧍🏽‍♀ E12.0 woman standing: medium skin tone
+1F9CD 1F3FE 200D 2640 FE0F ; fully-qualified # 🧍🏾‍♀️ E12.0 woman standing: medium-dark skin tone
+1F9CD 1F3FE 200D 2640 ; minimally-qualified # 🧍🏾‍♀ E12.0 woman standing: medium-dark skin tone
+1F9CD 1F3FF 200D 2640 FE0F ; fully-qualified # 🧍🏿‍♀️ E12.0 woman standing: dark skin tone
+1F9CD 1F3FF 200D 2640 ; minimally-qualified # 🧍🏿‍♀ E12.0 woman standing: dark skin tone
+1F9CE ; fully-qualified # 🧎 E12.0 person kneeling
+1F9CE 1F3FB ; fully-qualified # 🧎🏻 E12.0 person kneeling: light skin tone
+1F9CE 1F3FC ; fully-qualified # 🧎🏼 E12.0 person kneeling: medium-light skin tone
+1F9CE 1F3FD ; fully-qualified # 🧎🏽 E12.0 person kneeling: medium skin tone
+1F9CE 1F3FE ; fully-qualified # 🧎🏾 E12.0 person kneeling: medium-dark skin tone
+1F9CE 1F3FF ; fully-qualified # 🧎🏿 E12.0 person kneeling: dark skin tone
+1F9CE 200D 2642 FE0F ; fully-qualified # 🧎‍♂️ E12.0 man kneeling
+1F9CE 200D 2642 ; minimally-qualified # 🧎‍♂ E12.0 man kneeling
+1F9CE 1F3FB 200D 2642 FE0F ; fully-qualified # 🧎🏻‍♂️ E12.0 man kneeling: light skin tone
+1F9CE 1F3FB 200D 2642 ; minimally-qualified # 🧎🏻‍♂ E12.0 man kneeling: light skin tone
+1F9CE 1F3FC 200D 2642 FE0F ; fully-qualified # 🧎🏼‍♂️ E12.0 man kneeling: medium-light skin tone
+1F9CE 1F3FC 200D 2642 ; minimally-qualified # 🧎🏼‍♂ E12.0 man kneeling: medium-light skin tone
+1F9CE 1F3FD 200D 2642 FE0F ; fully-qualified # 🧎🏽‍♂️ E12.0 man kneeling: medium skin tone
+1F9CE 1F3FD 200D 2642 ; minimally-qualified # 🧎🏽‍♂ E12.0 man kneeling: medium skin tone
+1F9CE 1F3FE 200D 2642 FE0F ; fully-qualified # 🧎🏾‍♂️ E12.0 man kneeling: medium-dark skin tone
+1F9CE 1F3FE 200D 2642 ; minimally-qualified # 🧎🏾‍♂ E12.0 man kneeling: medium-dark skin tone
+1F9CE 1F3FF 200D 2642 FE0F ; fully-qualified # 🧎🏿‍♂️ E12.0 man kneeling: dark skin tone
+1F9CE 1F3FF 200D 2642 ; minimally-qualified # 🧎🏿‍♂ E12.0 man kneeling: dark skin tone
+1F9CE 200D 2640 FE0F ; fully-qualified # 🧎‍♀️ E12.0 woman kneeling
+1F9CE 200D 2640 ; minimally-qualified # 🧎‍♀ E12.0 woman kneeling
+1F9CE 1F3FB 200D 2640 FE0F ; fully-qualified # 🧎🏻‍♀️ E12.0 woman kneeling: light skin tone
+1F9CE 1F3FB 200D 2640 ; minimally-qualified # 🧎🏻‍♀ E12.0 woman kneeling: light skin tone
+1F9CE 1F3FC 200D 2640 FE0F ; fully-qualified # 🧎🏼‍♀️ E12.0 woman kneeling: medium-light skin tone
+1F9CE 1F3FC 200D 2640 ; minimally-qualified # 🧎🏼‍♀ E12.0 woman kneeling: medium-light skin tone
+1F9CE 1F3FD 200D 2640 FE0F ; fully-qualified # 🧎🏽‍♀️ E12.0 woman kneeling: medium skin tone
+1F9CE 1F3FD 200D 2640 ; minimally-qualified # 🧎🏽‍♀ E12.0 woman kneeling: medium skin tone
+1F9CE 1F3FE 200D 2640 FE0F ; fully-qualified # 🧎🏾‍♀️ E12.0 woman kneeling: medium-dark skin tone
+1F9CE 1F3FE 200D 2640 ; minimally-qualified # 🧎🏾‍♀ E12.0 woman kneeling: medium-dark skin tone
+1F9CE 1F3FF 200D 2640 FE0F ; fully-qualified # 🧎🏿‍♀️ E12.0 woman kneeling: dark skin tone
+1F9CE 1F3FF 200D 2640 ; minimally-qualified # 🧎🏿‍♀ E12.0 woman kneeling: dark skin tone
+1F9D1 200D 1F9AF ; fully-qualified # 🧑‍🦯 E12.1 person with white cane
+1F9D1 1F3FB 200D 1F9AF ; fully-qualified # 🧑🏻‍🦯 E12.1 person with white cane: light skin tone
+1F9D1 1F3FC 200D 1F9AF ; fully-qualified # 🧑🏼‍🦯 E12.1 person with white cane: medium-light skin tone
+1F9D1 1F3FD 200D 1F9AF ; fully-qualified # 🧑🏽‍🦯 E12.1 person with white cane: medium skin tone
+1F9D1 1F3FE 200D 1F9AF ; fully-qualified # 🧑🏾‍🦯 E12.1 person with white cane: medium-dark skin tone
+1F9D1 1F3FF 200D 1F9AF ; fully-qualified # 🧑🏿‍🦯 E12.1 person with white cane: dark skin tone
+1F468 200D 1F9AF ; fully-qualified # 👨‍🦯 E12.0 man with white cane
+1F468 1F3FB 200D 1F9AF ; fully-qualified # 👨🏻‍🦯 E12.0 man with white cane: light skin tone
+1F468 1F3FC 200D 1F9AF ; fully-qualified # 👨🏼‍🦯 E12.0 man with white cane: medium-light skin tone
+1F468 1F3FD 200D 1F9AF ; fully-qualified # 👨🏽‍🦯 E12.0 man with white cane: medium skin tone
+1F468 1F3FE 200D 1F9AF ; fully-qualified # 👨🏾‍🦯 E12.0 man with white cane: medium-dark skin tone
+1F468 1F3FF 200D 1F9AF ; fully-qualified # 👨🏿‍🦯 E12.0 man with white cane: dark skin tone
+1F469 200D 1F9AF ; fully-qualified # 👩‍🦯 E12.0 woman with white cane
+1F469 1F3FB 200D 1F9AF ; fully-qualified # 👩🏻‍🦯 E12.0 woman with white cane: light skin tone
+1F469 1F3FC 200D 1F9AF ; fully-qualified # 👩🏼‍🦯 E12.0 woman with white cane: medium-light skin tone
+1F469 1F3FD 200D 1F9AF ; fully-qualified # 👩🏽‍🦯 E12.0 woman with white cane: medium skin tone
+1F469 1F3FE 200D 1F9AF ; fully-qualified # 👩🏾‍🦯 E12.0 woman with white cane: medium-dark skin tone
+1F469 1F3FF 200D 1F9AF ; fully-qualified # 👩🏿‍🦯 E12.0 woman with white cane: dark skin tone
+1F9D1 200D 1F9BC ; fully-qualified # 🧑‍🦼 E12.1 person in motorized wheelchair
+1F9D1 1F3FB 200D 1F9BC ; fully-qualified # 🧑🏻‍🦼 E12.1 person in motorized wheelchair: light skin tone
+1F9D1 1F3FC 200D 1F9BC ; fully-qualified # 🧑🏼‍🦼 E12.1 person in motorized wheelchair: medium-light skin tone
+1F9D1 1F3FD 200D 1F9BC ; fully-qualified # 🧑🏽‍🦼 E12.1 person in motorized wheelchair: medium skin tone
+1F9D1 1F3FE 200D 1F9BC ; fully-qualified # 🧑🏾‍🦼 E12.1 person in motorized wheelchair: medium-dark skin tone
+1F9D1 1F3FF 200D 1F9BC ; fully-qualified # 🧑🏿‍🦼 E12.1 person in motorized wheelchair: dark skin tone
+1F468 200D 1F9BC ; fully-qualified # 👨‍🦼 E12.0 man in motorized wheelchair
+1F468 1F3FB 200D 1F9BC ; fully-qualified # 👨🏻‍🦼 E12.0 man in motorized wheelchair: light skin tone
+1F468 1F3FC 200D 1F9BC ; fully-qualified # 👨🏼‍🦼 E12.0 man in motorized wheelchair: medium-light skin tone
+1F468 1F3FD 200D 1F9BC ; fully-qualified # 👨🏽‍🦼 E12.0 man in motorized wheelchair: medium skin tone
+1F468 1F3FE 200D 1F9BC ; fully-qualified # 👨🏾‍🦼 E12.0 man in motorized wheelchair: medium-dark skin tone
+1F468 1F3FF 200D 1F9BC ; fully-qualified # 👨🏿‍🦼 E12.0 man in motorized wheelchair: dark skin tone
+1F469 200D 1F9BC ; fully-qualified # 👩‍🦼 E12.0 woman in motorized wheelchair
+1F469 1F3FB 200D 1F9BC ; fully-qualified # 👩🏻‍🦼 E12.0 woman in motorized wheelchair: light skin tone
+1F469 1F3FC 200D 1F9BC ; fully-qualified # 👩🏼‍🦼 E12.0 woman in motorized wheelchair: medium-light skin tone
+1F469 1F3FD 200D 1F9BC ; fully-qualified # 👩🏽‍🦼 E12.0 woman in motorized wheelchair: medium skin tone
+1F469 1F3FE 200D 1F9BC ; fully-qualified # 👩🏾‍🦼 E12.0 woman in motorized wheelchair: medium-dark skin tone
+1F469 1F3FF 200D 1F9BC ; fully-qualified # 👩🏿‍🦼 E12.0 woman in motorized wheelchair: dark skin tone
+1F9D1 200D 1F9BD ; fully-qualified # 🧑‍🦽 E12.1 person in manual wheelchair
+1F9D1 1F3FB 200D 1F9BD ; fully-qualified # 🧑🏻‍🦽 E12.1 person in manual wheelchair: light skin tone
+1F9D1 1F3FC 200D 1F9BD ; fully-qualified # 🧑🏼‍🦽 E12.1 person in manual wheelchair: medium-light skin tone
+1F9D1 1F3FD 200D 1F9BD ; fully-qualified # 🧑🏽‍🦽 E12.1 person in manual wheelchair: medium skin tone
+1F9D1 1F3FE 200D 1F9BD ; fully-qualified # 🧑🏾‍🦽 E12.1 person in manual wheelchair: medium-dark skin tone
+1F9D1 1F3FF 200D 1F9BD ; fully-qualified # 🧑🏿‍🦽 E12.1 person in manual wheelchair: dark skin tone
+1F468 200D 1F9BD ; fully-qualified # 👨‍🦽 E12.0 man in manual wheelchair
+1F468 1F3FB 200D 1F9BD ; fully-qualified # 👨🏻‍🦽 E12.0 man in manual wheelchair: light skin tone
+1F468 1F3FC 200D 1F9BD ; fully-qualified # 👨🏼‍🦽 E12.0 man in manual wheelchair: medium-light skin tone
+1F468 1F3FD 200D 1F9BD ; fully-qualified # 👨🏽‍🦽 E12.0 man in manual wheelchair: medium skin tone
+1F468 1F3FE 200D 1F9BD ; fully-qualified # 👨🏾‍🦽 E12.0 man in manual wheelchair: medium-dark skin tone
+1F468 1F3FF 200D 1F9BD ; fully-qualified # 👨🏿‍🦽 E12.0 man in manual wheelchair: dark skin tone
+1F469 200D 1F9BD ; fully-qualified # 👩‍🦽 E12.0 woman in manual wheelchair
+1F469 1F3FB 200D 1F9BD ; fully-qualified # 👩🏻‍🦽 E12.0 woman in manual wheelchair: light skin tone
+1F469 1F3FC 200D 1F9BD ; fully-qualified # 👩🏼‍🦽 E12.0 woman in manual wheelchair: medium-light skin tone
+1F469 1F3FD 200D 1F9BD ; fully-qualified # 👩🏽‍🦽 E12.0 woman in manual wheelchair: medium skin tone
+1F469 1F3FE 200D 1F9BD ; fully-qualified # 👩🏾‍🦽 E12.0 woman in manual wheelchair: medium-dark skin tone
+1F469 1F3FF 200D 1F9BD ; fully-qualified # 👩🏿‍🦽 E12.0 woman in manual wheelchair: dark skin tone
+1F3C3 ; fully-qualified # 🏃 E0.6 person running
+1F3C3 1F3FB ; fully-qualified # 🏃🏻 E1.0 person running: light skin tone
+1F3C3 1F3FC ; fully-qualified # 🏃🏼 E1.0 person running: medium-light skin tone
+1F3C3 1F3FD ; fully-qualified # 🏃🏽 E1.0 person running: medium skin tone
+1F3C3 1F3FE ; fully-qualified # 🏃🏾 E1.0 person running: medium-dark skin tone
+1F3C3 1F3FF ; fully-qualified # 🏃🏿 E1.0 person running: dark skin tone
+1F3C3 200D 2642 FE0F ; fully-qualified # 🏃‍♂️ E4.0 man running
+1F3C3 200D 2642 ; minimally-qualified # 🏃‍♂ E4.0 man running
+1F3C3 1F3FB 200D 2642 FE0F ; fully-qualified # 🏃🏻‍♂️ E4.0 man running: light skin tone
+1F3C3 1F3FB 200D 2642 ; minimally-qualified # 🏃🏻‍♂ E4.0 man running: light skin tone
+1F3C3 1F3FC 200D 2642 FE0F ; fully-qualified # 🏃🏼‍♂️ E4.0 man running: medium-light skin tone
+1F3C3 1F3FC 200D 2642 ; minimally-qualified # 🏃🏼‍♂ E4.0 man running: medium-light skin tone
+1F3C3 1F3FD 200D 2642 FE0F ; fully-qualified # 🏃🏽‍♂️ E4.0 man running: medium skin tone
+1F3C3 1F3FD 200D 2642 ; minimally-qualified # 🏃🏽‍♂ E4.0 man running: medium skin tone
+1F3C3 1F3FE 200D 2642 FE0F ; fully-qualified # 🏃🏾‍♂️ E4.0 man running: medium-dark skin tone
+1F3C3 1F3FE 200D 2642 ; minimally-qualified # 🏃🏾‍♂ E4.0 man running: medium-dark skin tone
+1F3C3 1F3FF 200D 2642 FE0F ; fully-qualified # 🏃🏿‍♂️ E4.0 man running: dark skin tone
+1F3C3 1F3FF 200D 2642 ; minimally-qualified # 🏃🏿‍♂ E4.0 man running: dark skin tone
+1F3C3 200D 2640 FE0F ; fully-qualified # 🏃‍♀️ E4.0 woman running
+1F3C3 200D 2640 ; minimally-qualified # 🏃‍♀ E4.0 woman running
+1F3C3 1F3FB 200D 2640 FE0F ; fully-qualified # 🏃🏻‍♀️ E4.0 woman running: light skin tone
+1F3C3 1F3FB 200D 2640 ; minimally-qualified # 🏃🏻‍♀ E4.0 woman running: light skin tone
+1F3C3 1F3FC 200D 2640 FE0F ; fully-qualified # 🏃🏼‍♀️ E4.0 woman running: medium-light skin tone
+1F3C3 1F3FC 200D 2640 ; minimally-qualified # 🏃🏼‍♀ E4.0 woman running: medium-light skin tone
+1F3C3 1F3FD 200D 2640 FE0F ; fully-qualified # 🏃🏽‍♀️ E4.0 woman running: medium skin tone
+1F3C3 1F3FD 200D 2640 ; minimally-qualified # 🏃🏽‍♀ E4.0 woman running: medium skin tone
+1F3C3 1F3FE 200D 2640 FE0F ; fully-qualified # 🏃🏾‍♀️ E4.0 woman running: medium-dark skin tone
+1F3C3 1F3FE 200D 2640 ; minimally-qualified # 🏃🏾‍♀ E4.0 woman running: medium-dark skin tone
+1F3C3 1F3FF 200D 2640 FE0F ; fully-qualified # 🏃🏿‍♀️ E4.0 woman running: dark skin tone
+1F3C3 1F3FF 200D 2640 ; minimally-qualified # 🏃🏿‍♀ E4.0 woman running: dark skin tone
+1F483 ; fully-qualified # 💃 E0.6 woman dancing
+1F483 1F3FB ; fully-qualified # 💃🏻 E1.0 woman dancing: light skin tone
+1F483 1F3FC ; fully-qualified # 💃🏼 E1.0 woman dancing: medium-light skin tone
+1F483 1F3FD ; fully-qualified # 💃🏽 E1.0 woman dancing: medium skin tone
+1F483 1F3FE ; fully-qualified # 💃🏾 E1.0 woman dancing: medium-dark skin tone
+1F483 1F3FF ; fully-qualified # 💃🏿 E1.0 woman dancing: dark skin tone
+1F57A ; fully-qualified # 🕺 E3.0 man dancing
+1F57A 1F3FB ; fully-qualified # 🕺🏻 E3.0 man dancing: light skin tone
+1F57A 1F3FC ; fully-qualified # 🕺🏼 E3.0 man dancing: medium-light skin tone
+1F57A 1F3FD ; fully-qualified # 🕺🏽 E3.0 man dancing: medium skin tone
+1F57A 1F3FE ; fully-qualified # 🕺🏾 E3.0 man dancing: medium-dark skin tone
+1F57A 1F3FF ; fully-qualified # 🕺🏿 E3.0 man dancing: dark skin tone
+1F574 FE0F ; fully-qualified # 🕴️ E0.7 person in suit levitating
+1F574 ; unqualified # 🕴 E0.7 person in suit levitating
+1F574 1F3FB ; fully-qualified # 🕴🏻 E4.0 person in suit levitating: light skin tone
+1F574 1F3FC ; fully-qualified # 🕴🏼 E4.0 person in suit levitating: medium-light skin tone
+1F574 1F3FD ; fully-qualified # 🕴🏽 E4.0 person in suit levitating: medium skin tone
+1F574 1F3FE ; fully-qualified # 🕴🏾 E4.0 person in suit levitating: medium-dark skin tone
+1F574 1F3FF ; fully-qualified # 🕴🏿 E4.0 person in suit levitating: dark skin tone
+1F46F ; fully-qualified # 👯 E0.6 people with bunny ears
+1F46F 200D 2642 FE0F ; fully-qualified # 👯‍♂️ E4.0 men with bunny ears
+1F46F 200D 2642 ; minimally-qualified # 👯‍♂ E4.0 men with bunny ears
+1F46F 200D 2640 FE0F ; fully-qualified # 👯‍♀️ E4.0 women with bunny ears
+1F46F 200D 2640 ; minimally-qualified # 👯‍♀ E4.0 women with bunny ears
+1F9D6 ; fully-qualified # 🧖 E5.0 person in steamy room
+1F9D6 1F3FB ; fully-qualified # 🧖🏻 E5.0 person in steamy room: light skin tone
+1F9D6 1F3FC ; fully-qualified # 🧖🏼 E5.0 person in steamy room: medium-light skin tone
+1F9D6 1F3FD ; fully-qualified # 🧖🏽 E5.0 person in steamy room: medium skin tone
+1F9D6 1F3FE ; fully-qualified # 🧖🏾 E5.0 person in steamy room: medium-dark skin tone
+1F9D6 1F3FF ; fully-qualified # 🧖🏿 E5.0 person in steamy room: dark skin tone
+1F9D6 200D 2642 FE0F ; fully-qualified # 🧖‍♂️ E5.0 man in steamy room
+1F9D6 200D 2642 ; minimally-qualified # 🧖‍♂ E5.0 man in steamy room
+1F9D6 1F3FB 200D 2642 FE0F ; fully-qualified # 🧖🏻‍♂️ E5.0 man in steamy room: light skin tone
+1F9D6 1F3FB 200D 2642 ; minimally-qualified # 🧖🏻‍♂ E5.0 man in steamy room: light skin tone
+1F9D6 1F3FC 200D 2642 FE0F ; fully-qualified # 🧖🏼‍♂️ E5.0 man in steamy room: medium-light skin tone
+1F9D6 1F3FC 200D 2642 ; minimally-qualified # 🧖🏼‍♂ E5.0 man in steamy room: medium-light skin tone
+1F9D6 1F3FD 200D 2642 FE0F ; fully-qualified # 🧖🏽‍♂️ E5.0 man in steamy room: medium skin tone
+1F9D6 1F3FD 200D 2642 ; minimally-qualified # 🧖🏽‍♂ E5.0 man in steamy room: medium skin tone
+1F9D6 1F3FE 200D 2642 FE0F ; fully-qualified # 🧖🏾‍♂️ E5.0 man in steamy room: medium-dark skin tone
+1F9D6 1F3FE 200D 2642 ; minimally-qualified # 🧖🏾‍♂ E5.0 man in steamy room: medium-dark skin tone
+1F9D6 1F3FF 200D 2642 FE0F ; fully-qualified # 🧖🏿‍♂️ E5.0 man in steamy room: dark skin tone
+1F9D6 1F3FF 200D 2642 ; minimally-qualified # 🧖🏿‍♂ E5.0 man in steamy room: dark skin tone
+1F9D6 200D 2640 FE0F ; fully-qualified # 🧖‍♀️ E5.0 woman in steamy room
+1F9D6 200D 2640 ; minimally-qualified # 🧖‍♀ E5.0 woman in steamy room
+1F9D6 1F3FB 200D 2640 FE0F ; fully-qualified # 🧖🏻‍♀️ E5.0 woman in steamy room: light skin tone
+1F9D6 1F3FB 200D 2640 ; minimally-qualified # 🧖🏻‍♀ E5.0 woman in steamy room: light skin tone
+1F9D6 1F3FC 200D 2640 FE0F ; fully-qualified # 🧖🏼‍♀️ E5.0 woman in steamy room: medium-light skin tone
+1F9D6 1F3FC 200D 2640 ; minimally-qualified # 🧖🏼‍♀ E5.0 woman in steamy room: medium-light skin tone
+1F9D6 1F3FD 200D 2640 FE0F ; fully-qualified # 🧖🏽‍♀️ E5.0 woman in steamy room: medium skin tone
+1F9D6 1F3FD 200D 2640 ; minimally-qualified # 🧖🏽‍♀ E5.0 woman in steamy room: medium skin tone
+1F9D6 1F3FE 200D 2640 FE0F ; fully-qualified # 🧖🏾‍♀️ E5.0 woman in steamy room: medium-dark skin tone
+1F9D6 1F3FE 200D 2640 ; minimally-qualified # 🧖🏾‍♀ E5.0 woman in steamy room: medium-dark skin tone
+1F9D6 1F3FF 200D 2640 FE0F ; fully-qualified # 🧖🏿‍♀️ E5.0 woman in steamy room: dark skin tone
+1F9D6 1F3FF 200D 2640 ; minimally-qualified # 🧖🏿‍♀ E5.0 woman in steamy room: dark skin tone
+1F9D7 ; fully-qualified # 🧗 E5.0 person climbing
+1F9D7 1F3FB ; fully-qualified # 🧗🏻 E5.0 person climbing: light skin tone
+1F9D7 1F3FC ; fully-qualified # 🧗🏼 E5.0 person climbing: medium-light skin tone
+1F9D7 1F3FD ; fully-qualified # 🧗🏽 E5.0 person climbing: medium skin tone
+1F9D7 1F3FE ; fully-qualified # 🧗🏾 E5.0 person climbing: medium-dark skin tone
+1F9D7 1F3FF ; fully-qualified # 🧗🏿 E5.0 person climbing: dark skin tone
+1F9D7 200D 2642 FE0F ; fully-qualified # 🧗‍♂️ E5.0 man climbing
+1F9D7 200D 2642 ; minimally-qualified # 🧗‍♂ E5.0 man climbing
+1F9D7 1F3FB 200D 2642 FE0F ; fully-qualified # 🧗🏻‍♂️ E5.0 man climbing: light skin tone
+1F9D7 1F3FB 200D 2642 ; minimally-qualified # 🧗🏻‍♂ E5.0 man climbing: light skin tone
+1F9D7 1F3FC 200D 2642 FE0F ; fully-qualified # 🧗🏼‍♂️ E5.0 man climbing: medium-light skin tone
+1F9D7 1F3FC 200D 2642 ; minimally-qualified # 🧗🏼‍♂ E5.0 man climbing: medium-light skin tone
+1F9D7 1F3FD 200D 2642 FE0F ; fully-qualified # 🧗🏽‍♂️ E5.0 man climbing: medium skin tone
+1F9D7 1F3FD 200D 2642 ; minimally-qualified # 🧗🏽‍♂ E5.0 man climbing: medium skin tone
+1F9D7 1F3FE 200D 2642 FE0F ; fully-qualified # 🧗🏾‍♂️ E5.0 man climbing: medium-dark skin tone
+1F9D7 1F3FE 200D 2642 ; minimally-qualified # 🧗🏾‍♂ E5.0 man climbing: medium-dark skin tone
+1F9D7 1F3FF 200D 2642 FE0F ; fully-qualified # 🧗🏿‍♂️ E5.0 man climbing: dark skin tone
+1F9D7 1F3FF 200D 2642 ; minimally-qualified # 🧗🏿‍♂ E5.0 man climbing: dark skin tone
+1F9D7 200D 2640 FE0F ; fully-qualified # 🧗‍♀️ E5.0 woman climbing
+1F9D7 200D 2640 ; minimally-qualified # 🧗‍♀ E5.0 woman climbing
+1F9D7 1F3FB 200D 2640 FE0F ; fully-qualified # 🧗🏻‍♀️ E5.0 woman climbing: light skin tone
+1F9D7 1F3FB 200D 2640 ; minimally-qualified # 🧗🏻‍♀ E5.0 woman climbing: light skin tone
+1F9D7 1F3FC 200D 2640 FE0F ; fully-qualified # 🧗🏼‍♀️ E5.0 woman climbing: medium-light skin tone
+1F9D7 1F3FC 200D 2640 ; minimally-qualified # 🧗🏼‍♀ E5.0 woman climbing: medium-light skin tone
+1F9D7 1F3FD 200D 2640 FE0F ; fully-qualified # 🧗🏽‍♀️ E5.0 woman climbing: medium skin tone
+1F9D7 1F3FD 200D 2640 ; minimally-qualified # 🧗🏽‍♀ E5.0 woman climbing: medium skin tone
+1F9D7 1F3FE 200D 2640 FE0F ; fully-qualified # 🧗🏾‍♀️ E5.0 woman climbing: medium-dark skin tone
+1F9D7 1F3FE 200D 2640 ; minimally-qualified # 🧗🏾‍♀ E5.0 woman climbing: medium-dark skin tone
+1F9D7 1F3FF 200D 2640 FE0F ; fully-qualified # 🧗🏿‍♀️ E5.0 woman climbing: dark skin tone
+1F9D7 1F3FF 200D 2640 ; minimally-qualified # 🧗🏿‍♀ E5.0 woman climbing: dark skin tone
+
+# subgroup: person-sport
+1F93A ; fully-qualified # 🤺 E3.0 person fencing
+1F3C7 ; fully-qualified # 🏇 E1.0 horse racing
+1F3C7 1F3FB ; fully-qualified # 🏇🏻 E1.0 horse racing: light skin tone
+1F3C7 1F3FC ; fully-qualified # 🏇🏼 E1.0 horse racing: medium-light skin tone
+1F3C7 1F3FD ; fully-qualified # 🏇🏽 E1.0 horse racing: medium skin tone
+1F3C7 1F3FE ; fully-qualified # 🏇🏾 E1.0 horse racing: medium-dark skin tone
+1F3C7 1F3FF ; fully-qualified # 🏇🏿 E1.0 horse racing: dark skin tone
+26F7 FE0F ; fully-qualified # ⛷️ E0.7 skier
+26F7 ; unqualified # ⛷ E0.7 skier
+1F3C2 ; fully-qualified # 🏂 E0.6 snowboarder
+1F3C2 1F3FB ; fully-qualified # 🏂🏻 E1.0 snowboarder: light skin tone
+1F3C2 1F3FC ; fully-qualified # 🏂🏼 E1.0 snowboarder: medium-light skin tone
+1F3C2 1F3FD ; fully-qualified # 🏂🏽 E1.0 snowboarder: medium skin tone
+1F3C2 1F3FE ; fully-qualified # 🏂🏾 E1.0 snowboarder: medium-dark skin tone
+1F3C2 1F3FF ; fully-qualified # 🏂🏿 E1.0 snowboarder: dark skin tone
+1F3CC FE0F ; fully-qualified # 🏌️ E0.7 person golfing
+1F3CC ; unqualified # 🏌 E0.7 person golfing
+1F3CC 1F3FB ; fully-qualified # 🏌🏻 E4.0 person golfing: light skin tone
+1F3CC 1F3FC ; fully-qualified # 🏌🏼 E4.0 person golfing: medium-light skin tone
+1F3CC 1F3FD ; fully-qualified # 🏌🏽 E4.0 person golfing: medium skin tone
+1F3CC 1F3FE ; fully-qualified # 🏌🏾 E4.0 person golfing: medium-dark skin tone
+1F3CC 1F3FF ; fully-qualified # 🏌🏿 E4.0 person golfing: dark skin tone
+1F3CC FE0F 200D 2642 FE0F ; fully-qualified # 🏌️‍♂️ E4.0 man golfing
+1F3CC 200D 2642 FE0F ; unqualified # 🏌‍♂️ E4.0 man golfing
+1F3CC FE0F 200D 2642 ; unqualified # 🏌️‍♂ E4.0 man golfing
+1F3CC 200D 2642 ; unqualified # 🏌‍♂ E4.0 man golfing
+1F3CC 1F3FB 200D 2642 FE0F ; fully-qualified # 🏌🏻‍♂️ E4.0 man golfing: light skin tone
+1F3CC 1F3FB 200D 2642 ; minimally-qualified # 🏌🏻‍♂ E4.0 man golfing: light skin tone
+1F3CC 1F3FC 200D 2642 FE0F ; fully-qualified # 🏌🏼‍♂️ E4.0 man golfing: medium-light skin tone
+1F3CC 1F3FC 200D 2642 ; minimally-qualified # 🏌🏼‍♂ E4.0 man golfing: medium-light skin tone
+1F3CC 1F3FD 200D 2642 FE0F ; fully-qualified # 🏌🏽‍♂️ E4.0 man golfing: medium skin tone
+1F3CC 1F3FD 200D 2642 ; minimally-qualified # 🏌🏽‍♂ E4.0 man golfing: medium skin tone
+1F3CC 1F3FE 200D 2642 FE0F ; fully-qualified # 🏌🏾‍♂️ E4.0 man golfing: medium-dark skin tone
+1F3CC 1F3FE 200D 2642 ; minimally-qualified # 🏌🏾‍♂ E4.0 man golfing: medium-dark skin tone
+1F3CC 1F3FF 200D 2642 FE0F ; fully-qualified # 🏌🏿‍♂️ E4.0 man golfing: dark skin tone
+1F3CC 1F3FF 200D 2642 ; minimally-qualified # 🏌🏿‍♂ E4.0 man golfing: dark skin tone
+1F3CC FE0F 200D 2640 FE0F ; fully-qualified # 🏌️‍♀️ E4.0 woman golfing
+1F3CC 200D 2640 FE0F ; unqualified # 🏌‍♀️ E4.0 woman golfing
+1F3CC FE0F 200D 2640 ; unqualified # 🏌️‍♀ E4.0 woman golfing
+1F3CC 200D 2640 ; unqualified # 🏌‍♀ E4.0 woman golfing
+1F3CC 1F3FB 200D 2640 FE0F ; fully-qualified # 🏌🏻‍♀️ E4.0 woman golfing: light skin tone
+1F3CC 1F3FB 200D 2640 ; minimally-qualified # 🏌🏻‍♀ E4.0 woman golfing: light skin tone
+1F3CC 1F3FC 200D 2640 FE0F ; fully-qualified # 🏌🏼‍♀️ E4.0 woman golfing: medium-light skin tone
+1F3CC 1F3FC 200D 2640 ; minimally-qualified # 🏌🏼‍♀ E4.0 woman golfing: medium-light skin tone
+1F3CC 1F3FD 200D 2640 FE0F ; fully-qualified # 🏌🏽‍♀️ E4.0 woman golfing: medium skin tone
+1F3CC 1F3FD 200D 2640 ; minimally-qualified # 🏌🏽‍♀ E4.0 woman golfing: medium skin tone
+1F3CC 1F3FE 200D 2640 FE0F ; fully-qualified # 🏌🏾‍♀️ E4.0 woman golfing: medium-dark skin tone
+1F3CC 1F3FE 200D 2640 ; minimally-qualified # 🏌🏾‍♀ E4.0 woman golfing: medium-dark skin tone
+1F3CC 1F3FF 200D 2640 FE0F ; fully-qualified # 🏌🏿‍♀️ E4.0 woman golfing: dark skin tone
+1F3CC 1F3FF 200D 2640 ; minimally-qualified # 🏌🏿‍♀ E4.0 woman golfing: dark skin tone
+1F3C4 ; fully-qualified # 🏄 E0.6 person surfing
+1F3C4 1F3FB ; fully-qualified # 🏄🏻 E1.0 person surfing: light skin tone
+1F3C4 1F3FC ; fully-qualified # 🏄🏼 E1.0 person surfing: medium-light skin tone
+1F3C4 1F3FD ; fully-qualified # 🏄🏽 E1.0 person surfing: medium skin tone
+1F3C4 1F3FE ; fully-qualified # 🏄🏾 E1.0 person surfing: medium-dark skin tone
+1F3C4 1F3FF ; fully-qualified # 🏄🏿 E1.0 person surfing: dark skin tone
+1F3C4 200D 2642 FE0F ; fully-qualified # 🏄‍♂️ E4.0 man surfing
+1F3C4 200D 2642 ; minimally-qualified # 🏄‍♂ E4.0 man surfing
+1F3C4 1F3FB 200D 2642 FE0F ; fully-qualified # 🏄🏻‍♂️ E4.0 man surfing: light skin tone
+1F3C4 1F3FB 200D 2642 ; minimally-qualified # 🏄🏻‍♂ E4.0 man surfing: light skin tone
+1F3C4 1F3FC 200D 2642 FE0F ; fully-qualified # 🏄🏼‍♂️ E4.0 man surfing: medium-light skin tone
+1F3C4 1F3FC 200D 2642 ; minimally-qualified # 🏄🏼‍♂ E4.0 man surfing: medium-light skin tone
+1F3C4 1F3FD 200D 2642 FE0F ; fully-qualified # 🏄🏽‍♂️ E4.0 man surfing: medium skin tone
+1F3C4 1F3FD 200D 2642 ; minimally-qualified # 🏄🏽‍♂ E4.0 man surfing: medium skin tone
+1F3C4 1F3FE 200D 2642 FE0F ; fully-qualified # 🏄🏾‍♂️ E4.0 man surfing: medium-dark skin tone
+1F3C4 1F3FE 200D 2642 ; minimally-qualified # 🏄🏾‍♂ E4.0 man surfing: medium-dark skin tone
+1F3C4 1F3FF 200D 2642 FE0F ; fully-qualified # 🏄🏿‍♂️ E4.0 man surfing: dark skin tone
+1F3C4 1F3FF 200D 2642 ; minimally-qualified # 🏄🏿‍♂ E4.0 man surfing: dark skin tone
+1F3C4 200D 2640 FE0F ; fully-qualified # 🏄‍♀️ E4.0 woman surfing
+1F3C4 200D 2640 ; minimally-qualified # 🏄‍♀ E4.0 woman surfing
+1F3C4 1F3FB 200D 2640 FE0F ; fully-qualified # 🏄🏻‍♀️ E4.0 woman surfing: light skin tone
+1F3C4 1F3FB 200D 2640 ; minimally-qualified # 🏄🏻‍♀ E4.0 woman surfing: light skin tone
+1F3C4 1F3FC 200D 2640 FE0F ; fully-qualified # 🏄🏼‍♀️ E4.0 woman surfing: medium-light skin tone
+1F3C4 1F3FC 200D 2640 ; minimally-qualified # 🏄🏼‍♀ E4.0 woman surfing: medium-light skin tone
+1F3C4 1F3FD 200D 2640 FE0F ; fully-qualified # 🏄🏽‍♀️ E4.0 woman surfing: medium skin tone
+1F3C4 1F3FD 200D 2640 ; minimally-qualified # 🏄🏽‍♀ E4.0 woman surfing: medium skin tone
+1F3C4 1F3FE 200D 2640 FE0F ; fully-qualified # 🏄🏾‍♀️ E4.0 woman surfing: medium-dark skin tone
+1F3C4 1F3FE 200D 2640 ; minimally-qualified # 🏄🏾‍♀ E4.0 woman surfing: medium-dark skin tone
+1F3C4 1F3FF 200D 2640 FE0F ; fully-qualified # 🏄🏿‍♀️ E4.0 woman surfing: dark skin tone
+1F3C4 1F3FF 200D 2640 ; minimally-qualified # 🏄🏿‍♀ E4.0 woman surfing: dark skin tone
+1F6A3 ; fully-qualified # 🚣 E1.0 person rowing boat
+1F6A3 1F3FB ; fully-qualified # 🚣🏻 E1.0 person rowing boat: light skin tone
+1F6A3 1F3FC ; fully-qualified # 🚣🏼 E1.0 person rowing boat: medium-light skin tone
+1F6A3 1F3FD ; fully-qualified # 🚣🏽 E1.0 person rowing boat: medium skin tone
+1F6A3 1F3FE ; fully-qualified # 🚣🏾 E1.0 person rowing boat: medium-dark skin tone
+1F6A3 1F3FF ; fully-qualified # 🚣🏿 E1.0 person rowing boat: dark skin tone
+1F6A3 200D 2642 FE0F ; fully-qualified # 🚣‍♂️ E4.0 man rowing boat
+1F6A3 200D 2642 ; minimally-qualified # 🚣‍♂ E4.0 man rowing boat
+1F6A3 1F3FB 200D 2642 FE0F ; fully-qualified # 🚣🏻‍♂️ E4.0 man rowing boat: light skin tone
+1F6A3 1F3FB 200D 2642 ; minimally-qualified # 🚣🏻‍♂ E4.0 man rowing boat: light skin tone
+1F6A3 1F3FC 200D 2642 FE0F ; fully-qualified # 🚣🏼‍♂️ E4.0 man rowing boat: medium-light skin tone
+1F6A3 1F3FC 200D 2642 ; minimally-qualified # 🚣🏼‍♂ E4.0 man rowing boat: medium-light skin tone
+1F6A3 1F3FD 200D 2642 FE0F ; fully-qualified # 🚣🏽‍♂️ E4.0 man rowing boat: medium skin tone
+1F6A3 1F3FD 200D 2642 ; minimally-qualified # 🚣🏽‍♂ E4.0 man rowing boat: medium skin tone
+1F6A3 1F3FE 200D 2642 FE0F ; fully-qualified # 🚣🏾‍♂️ E4.0 man rowing boat: medium-dark skin tone
+1F6A3 1F3FE 200D 2642 ; minimally-qualified # 🚣🏾‍♂ E4.0 man rowing boat: medium-dark skin tone
+1F6A3 1F3FF 200D 2642 FE0F ; fully-qualified # 🚣🏿‍♂️ E4.0 man rowing boat: dark skin tone
+1F6A3 1F3FF 200D 2642 ; minimally-qualified # 🚣🏿‍♂ E4.0 man rowing boat: dark skin tone
+1F6A3 200D 2640 FE0F ; fully-qualified # 🚣‍♀️ E4.0 woman rowing boat
+1F6A3 200D 2640 ; minimally-qualified # 🚣‍♀ E4.0 woman rowing boat
+1F6A3 1F3FB 200D 2640 FE0F ; fully-qualified # 🚣🏻‍♀️ E4.0 woman rowing boat: light skin tone
+1F6A3 1F3FB 200D 2640 ; minimally-qualified # 🚣🏻‍♀ E4.0 woman rowing boat: light skin tone
+1F6A3 1F3FC 200D 2640 FE0F ; fully-qualified # 🚣🏼‍♀️ E4.0 woman rowing boat: medium-light skin tone
+1F6A3 1F3FC 200D 2640 ; minimally-qualified # 🚣🏼‍♀ E4.0 woman rowing boat: medium-light skin tone
+1F6A3 1F3FD 200D 2640 FE0F ; fully-qualified # 🚣🏽‍♀️ E4.0 woman rowing boat: medium skin tone
+1F6A3 1F3FD 200D 2640 ; minimally-qualified # 🚣🏽‍♀ E4.0 woman rowing boat: medium skin tone
+1F6A3 1F3FE 200D 2640 FE0F ; fully-qualified # 🚣🏾‍♀️ E4.0 woman rowing boat: medium-dark skin tone
+1F6A3 1F3FE 200D 2640 ; minimally-qualified # 🚣🏾‍♀ E4.0 woman rowing boat: medium-dark skin tone
+1F6A3 1F3FF 200D 2640 FE0F ; fully-qualified # 🚣🏿‍♀️ E4.0 woman rowing boat: dark skin tone
+1F6A3 1F3FF 200D 2640 ; minimally-qualified # 🚣🏿‍♀ E4.0 woman rowing boat: dark skin tone
+1F3CA ; fully-qualified # 🏊 E0.6 person swimming
+1F3CA 1F3FB ; fully-qualified # 🏊🏻 E1.0 person swimming: light skin tone
+1F3CA 1F3FC ; fully-qualified # 🏊🏼 E1.0 person swimming: medium-light skin tone
+1F3CA 1F3FD ; fully-qualified # 🏊🏽 E1.0 person swimming: medium skin tone
+1F3CA 1F3FE ; fully-qualified # 🏊🏾 E1.0 person swimming: medium-dark skin tone
+1F3CA 1F3FF ; fully-qualified # 🏊🏿 E1.0 person swimming: dark skin tone
+1F3CA 200D 2642 FE0F ; fully-qualified # 🏊‍♂️ E4.0 man swimming
+1F3CA 200D 2642 ; minimally-qualified # 🏊‍♂ E4.0 man swimming
+1F3CA 1F3FB 200D 2642 FE0F ; fully-qualified # 🏊🏻‍♂️ E4.0 man swimming: light skin tone
+1F3CA 1F3FB 200D 2642 ; minimally-qualified # 🏊🏻‍♂ E4.0 man swimming: light skin tone
+1F3CA 1F3FC 200D 2642 FE0F ; fully-qualified # 🏊🏼‍♂️ E4.0 man swimming: medium-light skin tone
+1F3CA 1F3FC 200D 2642 ; minimally-qualified # 🏊🏼‍♂ E4.0 man swimming: medium-light skin tone
+1F3CA 1F3FD 200D 2642 FE0F ; fully-qualified # 🏊🏽‍♂️ E4.0 man swimming: medium skin tone
+1F3CA 1F3FD 200D 2642 ; minimally-qualified # 🏊🏽‍♂ E4.0 man swimming: medium skin tone
+1F3CA 1F3FE 200D 2642 FE0F ; fully-qualified # 🏊🏾‍♂️ E4.0 man swimming: medium-dark skin tone
+1F3CA 1F3FE 200D 2642 ; minimally-qualified # 🏊🏾‍♂ E4.0 man swimming: medium-dark skin tone
+1F3CA 1F3FF 200D 2642 FE0F ; fully-qualified # 🏊🏿‍♂️ E4.0 man swimming: dark skin tone
+1F3CA 1F3FF 200D 2642 ; minimally-qualified # 🏊🏿‍♂ E4.0 man swimming: dark skin tone
+1F3CA 200D 2640 FE0F ; fully-qualified # 🏊‍♀️ E4.0 woman swimming
+1F3CA 200D 2640 ; minimally-qualified # 🏊‍♀ E4.0 woman swimming
+1F3CA 1F3FB 200D 2640 FE0F ; fully-qualified # 🏊🏻‍♀️ E4.0 woman swimming: light skin tone
+1F3CA 1F3FB 200D 2640 ; minimally-qualified # 🏊🏻‍♀ E4.0 woman swimming: light skin tone
+1F3CA 1F3FC 200D 2640 FE0F ; fully-qualified # 🏊🏼‍♀️ E4.0 woman swimming: medium-light skin tone
+1F3CA 1F3FC 200D 2640 ; minimally-qualified # 🏊🏼‍♀ E4.0 woman swimming: medium-light skin tone
+1F3CA 1F3FD 200D 2640 FE0F ; fully-qualified # 🏊🏽‍♀️ E4.0 woman swimming: medium skin tone
+1F3CA 1F3FD 200D 2640 ; minimally-qualified # 🏊🏽‍♀ E4.0 woman swimming: medium skin tone
+1F3CA 1F3FE 200D 2640 FE0F ; fully-qualified # 🏊🏾‍♀️ E4.0 woman swimming: medium-dark skin tone
+1F3CA 1F3FE 200D 2640 ; minimally-qualified # 🏊🏾‍♀ E4.0 woman swimming: medium-dark skin tone
+1F3CA 1F3FF 200D 2640 FE0F ; fully-qualified # 🏊🏿‍♀️ E4.0 woman swimming: dark skin tone
+1F3CA 1F3FF 200D 2640 ; minimally-qualified # 🏊🏿‍♀ E4.0 woman swimming: dark skin tone
+26F9 FE0F ; fully-qualified # ⛹️ E0.7 person bouncing ball
+26F9 ; unqualified # ⛹ E0.7 person bouncing ball
+26F9 1F3FB ; fully-qualified # ⛹🏻 E2.0 person bouncing ball: light skin tone
+26F9 1F3FC ; fully-qualified # ⛹🏼 E2.0 person bouncing ball: medium-light skin tone
+26F9 1F3FD ; fully-qualified # ⛹🏽 E2.0 person bouncing ball: medium skin tone
+26F9 1F3FE ; fully-qualified # ⛹🏾 E2.0 person bouncing ball: medium-dark skin tone
+26F9 1F3FF ; fully-qualified # ⛹🏿 E2.0 person bouncing ball: dark skin tone
+26F9 FE0F 200D 2642 FE0F ; fully-qualified # ⛹️‍♂️ E4.0 man bouncing ball
+26F9 200D 2642 FE0F ; unqualified # ⛹‍♂️ E4.0 man bouncing ball
+26F9 FE0F 200D 2642 ; unqualified # ⛹️‍♂ E4.0 man bouncing ball
+26F9 200D 2642 ; unqualified # ⛹‍♂ E4.0 man bouncing ball
+26F9 1F3FB 200D 2642 FE0F ; fully-qualified # ⛹🏻‍♂️ E4.0 man bouncing ball: light skin tone
+26F9 1F3FB 200D 2642 ; minimally-qualified # ⛹🏻‍♂ E4.0 man bouncing ball: light skin tone
+26F9 1F3FC 200D 2642 FE0F ; fully-qualified # ⛹🏼‍♂️ E4.0 man bouncing ball: medium-light skin tone
+26F9 1F3FC 200D 2642 ; minimally-qualified # ⛹🏼‍♂ E4.0 man bouncing ball: medium-light skin tone
+26F9 1F3FD 200D 2642 FE0F ; fully-qualified # ⛹🏽‍♂️ E4.0 man bouncing ball: medium skin tone
+26F9 1F3FD 200D 2642 ; minimally-qualified # ⛹🏽‍♂ E4.0 man bouncing ball: medium skin tone
+26F9 1F3FE 200D 2642 FE0F ; fully-qualified # ⛹🏾‍♂️ E4.0 man bouncing ball: medium-dark skin tone
+26F9 1F3FE 200D 2642 ; minimally-qualified # ⛹🏾‍♂ E4.0 man bouncing ball: medium-dark skin tone
+26F9 1F3FF 200D 2642 FE0F ; fully-qualified # ⛹🏿‍♂️ E4.0 man bouncing ball: dark skin tone
+26F9 1F3FF 200D 2642 ; minimally-qualified # ⛹🏿‍♂ E4.0 man bouncing ball: dark skin tone
+26F9 FE0F 200D 2640 FE0F ; fully-qualified # ⛹️‍♀️ E4.0 woman bouncing ball
+26F9 200D 2640 FE0F ; unqualified # ⛹‍♀️ E4.0 woman bouncing ball
+26F9 FE0F 200D 2640 ; unqualified # ⛹️‍♀ E4.0 woman bouncing ball
+26F9 200D 2640 ; unqualified # ⛹‍♀ E4.0 woman bouncing ball
+26F9 1F3FB 200D 2640 FE0F ; fully-qualified # ⛹🏻‍♀️ E4.0 woman bouncing ball: light skin tone
+26F9 1F3FB 200D 2640 ; minimally-qualified # ⛹🏻‍♀ E4.0 woman bouncing ball: light skin tone
+26F9 1F3FC 200D 2640 FE0F ; fully-qualified # ⛹🏼‍♀️ E4.0 woman bouncing ball: medium-light skin tone
+26F9 1F3FC 200D 2640 ; minimally-qualified # ⛹🏼‍♀ E4.0 woman bouncing ball: medium-light skin tone
+26F9 1F3FD 200D 2640 FE0F ; fully-qualified # ⛹🏽‍♀️ E4.0 woman bouncing ball: medium skin tone
+26F9 1F3FD 200D 2640 ; minimally-qualified # ⛹🏽‍♀ E4.0 woman bouncing ball: medium skin tone
+26F9 1F3FE 200D 2640 FE0F ; fully-qualified # ⛹🏾‍♀️ E4.0 woman bouncing ball: medium-dark skin tone
+26F9 1F3FE 200D 2640 ; minimally-qualified # ⛹🏾‍♀ E4.0 woman bouncing ball: medium-dark skin tone
+26F9 1F3FF 200D 2640 FE0F ; fully-qualified # ⛹🏿‍♀️ E4.0 woman bouncing ball: dark skin tone
+26F9 1F3FF 200D 2640 ; minimally-qualified # ⛹🏿‍♀ E4.0 woman bouncing ball: dark skin tone
+1F3CB FE0F ; fully-qualified # 🏋️ E0.7 person lifting weights
+1F3CB ; unqualified # 🏋 E0.7 person lifting weights
+1F3CB 1F3FB ; fully-qualified # 🏋🏻 E2.0 person lifting weights: light skin tone
+1F3CB 1F3FC ; fully-qualified # 🏋🏼 E2.0 person lifting weights: medium-light skin tone
+1F3CB 1F3FD ; fully-qualified # 🏋🏽 E2.0 person lifting weights: medium skin tone
+1F3CB 1F3FE ; fully-qualified # 🏋🏾 E2.0 person lifting weights: medium-dark skin tone
+1F3CB 1F3FF ; fully-qualified # 🏋🏿 E2.0 person lifting weights: dark skin tone
+1F3CB FE0F 200D 2642 FE0F ; fully-qualified # 🏋️‍♂️ E4.0 man lifting weights
+1F3CB 200D 2642 FE0F ; unqualified # 🏋‍♂️ E4.0 man lifting weights
+1F3CB FE0F 200D 2642 ; unqualified # 🏋️‍♂ E4.0 man lifting weights
+1F3CB 200D 2642 ; unqualified # 🏋‍♂ E4.0 man lifting weights
+1F3CB 1F3FB 200D 2642 FE0F ; fully-qualified # 🏋🏻‍♂️ E4.0 man lifting weights: light skin tone
+1F3CB 1F3FB 200D 2642 ; minimally-qualified # 🏋🏻‍♂ E4.0 man lifting weights: light skin tone
+1F3CB 1F3FC 200D 2642 FE0F ; fully-qualified # 🏋🏼‍♂️ E4.0 man lifting weights: medium-light skin tone
+1F3CB 1F3FC 200D 2642 ; minimally-qualified # 🏋🏼‍♂ E4.0 man lifting weights: medium-light skin tone
+1F3CB 1F3FD 200D 2642 FE0F ; fully-qualified # 🏋🏽‍♂️ E4.0 man lifting weights: medium skin tone
+1F3CB 1F3FD 200D 2642 ; minimally-qualified # 🏋🏽‍♂ E4.0 man lifting weights: medium skin tone
+1F3CB 1F3FE 200D 2642 FE0F ; fully-qualified # 🏋🏾‍♂️ E4.0 man lifting weights: medium-dark skin tone
+1F3CB 1F3FE 200D 2642 ; minimally-qualified # 🏋🏾‍♂ E4.0 man lifting weights: medium-dark skin tone
+1F3CB 1F3FF 200D 2642 FE0F ; fully-qualified # 🏋🏿‍♂️ E4.0 man lifting weights: dark skin tone
+1F3CB 1F3FF 200D 2642 ; minimally-qualified # 🏋🏿‍♂ E4.0 man lifting weights: dark skin tone
+1F3CB FE0F 200D 2640 FE0F ; fully-qualified # 🏋️‍♀️ E4.0 woman lifting weights
+1F3CB 200D 2640 FE0F ; unqualified # 🏋‍♀️ E4.0 woman lifting weights
+1F3CB FE0F 200D 2640 ; unqualified # 🏋️‍♀ E4.0 woman lifting weights
+1F3CB 200D 2640 ; unqualified # 🏋‍♀ E4.0 woman lifting weights
+1F3CB 1F3FB 200D 2640 FE0F ; fully-qualified # 🏋🏻‍♀️ E4.0 woman lifting weights: light skin tone
+1F3CB 1F3FB 200D 2640 ; minimally-qualified # 🏋🏻‍♀ E4.0 woman lifting weights: light skin tone
+1F3CB 1F3FC 200D 2640 FE0F ; fully-qualified # 🏋🏼‍♀️ E4.0 woman lifting weights: medium-light skin tone
+1F3CB 1F3FC 200D 2640 ; minimally-qualified # 🏋🏼‍♀ E4.0 woman lifting weights: medium-light skin tone
+1F3CB 1F3FD 200D 2640 FE0F ; fully-qualified # 🏋🏽‍♀️ E4.0 woman lifting weights: medium skin tone
+1F3CB 1F3FD 200D 2640 ; minimally-qualified # 🏋🏽‍♀ E4.0 woman lifting weights: medium skin tone
+1F3CB 1F3FE 200D 2640 FE0F ; fully-qualified # 🏋🏾‍♀️ E4.0 woman lifting weights: medium-dark skin tone
+1F3CB 1F3FE 200D 2640 ; minimally-qualified # 🏋🏾‍♀ E4.0 woman lifting weights: medium-dark skin tone
+1F3CB 1F3FF 200D 2640 FE0F ; fully-qualified # 🏋🏿‍♀️ E4.0 woman lifting weights: dark skin tone
+1F3CB 1F3FF 200D 2640 ; minimally-qualified # 🏋🏿‍♀ E4.0 woman lifting weights: dark skin tone
+1F6B4 ; fully-qualified # 🚴 E1.0 person biking
+1F6B4 1F3FB ; fully-qualified # 🚴🏻 E1.0 person biking: light skin tone
+1F6B4 1F3FC ; fully-qualified # 🚴🏼 E1.0 person biking: medium-light skin tone
+1F6B4 1F3FD ; fully-qualified # 🚴🏽 E1.0 person biking: medium skin tone
+1F6B4 1F3FE ; fully-qualified # 🚴🏾 E1.0 person biking: medium-dark skin tone
+1F6B4 1F3FF ; fully-qualified # 🚴🏿 E1.0 person biking: dark skin tone
+1F6B4 200D 2642 FE0F ; fully-qualified # 🚴‍♂️ E4.0 man biking
+1F6B4 200D 2642 ; minimally-qualified # 🚴‍♂ E4.0 man biking
+1F6B4 1F3FB 200D 2642 FE0F ; fully-qualified # 🚴🏻‍♂️ E4.0 man biking: light skin tone
+1F6B4 1F3FB 200D 2642 ; minimally-qualified # 🚴🏻‍♂ E4.0 man biking: light skin tone
+1F6B4 1F3FC 200D 2642 FE0F ; fully-qualified # 🚴🏼‍♂️ E4.0 man biking: medium-light skin tone
+1F6B4 1F3FC 200D 2642 ; minimally-qualified # 🚴🏼‍♂ E4.0 man biking: medium-light skin tone
+1F6B4 1F3FD 200D 2642 FE0F ; fully-qualified # 🚴🏽‍♂️ E4.0 man biking: medium skin tone
+1F6B4 1F3FD 200D 2642 ; minimally-qualified # 🚴🏽‍♂ E4.0 man biking: medium skin tone
+1F6B4 1F3FE 200D 2642 FE0F ; fully-qualified # 🚴🏾‍♂️ E4.0 man biking: medium-dark skin tone
+1F6B4 1F3FE 200D 2642 ; minimally-qualified # 🚴🏾‍♂ E4.0 man biking: medium-dark skin tone
+1F6B4 1F3FF 200D 2642 FE0F ; fully-qualified # 🚴🏿‍♂️ E4.0 man biking: dark skin tone
+1F6B4 1F3FF 200D 2642 ; minimally-qualified # 🚴🏿‍♂ E4.0 man biking: dark skin tone
+1F6B4 200D 2640 FE0F ; fully-qualified # 🚴‍♀️ E4.0 woman biking
+1F6B4 200D 2640 ; minimally-qualified # 🚴‍♀ E4.0 woman biking
+1F6B4 1F3FB 200D 2640 FE0F ; fully-qualified # 🚴🏻‍♀️ E4.0 woman biking: light skin tone
+1F6B4 1F3FB 200D 2640 ; minimally-qualified # 🚴🏻‍♀ E4.0 woman biking: light skin tone
+1F6B4 1F3FC 200D 2640 FE0F ; fully-qualified # 🚴🏼‍♀️ E4.0 woman biking: medium-light skin tone
+1F6B4 1F3FC 200D 2640 ; minimally-qualified # 🚴🏼‍♀ E4.0 woman biking: medium-light skin tone
+1F6B4 1F3FD 200D 2640 FE0F ; fully-qualified # 🚴🏽‍♀️ E4.0 woman biking: medium skin tone
+1F6B4 1F3FD 200D 2640 ; minimally-qualified # 🚴🏽‍♀ E4.0 woman biking: medium skin tone
+1F6B4 1F3FE 200D 2640 FE0F ; fully-qualified # 🚴🏾‍♀️ E4.0 woman biking: medium-dark skin tone
+1F6B4 1F3FE 200D 2640 ; minimally-qualified # 🚴🏾‍♀ E4.0 woman biking: medium-dark skin tone
+1F6B4 1F3FF 200D 2640 FE0F ; fully-qualified # 🚴🏿‍♀️ E4.0 woman biking: dark skin tone
+1F6B4 1F3FF 200D 2640 ; minimally-qualified # 🚴🏿‍♀ E4.0 woman biking: dark skin tone
+1F6B5 ; fully-qualified # 🚵 E1.0 person mountain biking
+1F6B5 1F3FB ; fully-qualified # 🚵🏻 E1.0 person mountain biking: light skin tone
+1F6B5 1F3FC ; fully-qualified # 🚵🏼 E1.0 person mountain biking: medium-light skin tone
+1F6B5 1F3FD ; fully-qualified # 🚵🏽 E1.0 person mountain biking: medium skin tone
+1F6B5 1F3FE ; fully-qualified # 🚵🏾 E1.0 person mountain biking: medium-dark skin tone
+1F6B5 1F3FF ; fully-qualified # 🚵🏿 E1.0 person mountain biking: dark skin tone
+1F6B5 200D 2642 FE0F ; fully-qualified # 🚵‍♂️ E4.0 man mountain biking
+1F6B5 200D 2642 ; minimally-qualified # 🚵‍♂ E4.0 man mountain biking
+1F6B5 1F3FB 200D 2642 FE0F ; fully-qualified # 🚵🏻‍♂️ E4.0 man mountain biking: light skin tone
+1F6B5 1F3FB 200D 2642 ; minimally-qualified # 🚵🏻‍♂ E4.0 man mountain biking: light skin tone
+1F6B5 1F3FC 200D 2642 FE0F ; fully-qualified # 🚵🏼‍♂️ E4.0 man mountain biking: medium-light skin tone
+1F6B5 1F3FC 200D 2642 ; minimally-qualified # 🚵🏼‍♂ E4.0 man mountain biking: medium-light skin tone
+1F6B5 1F3FD 200D 2642 FE0F ; fully-qualified # 🚵🏽‍♂️ E4.0 man mountain biking: medium skin tone
+1F6B5 1F3FD 200D 2642 ; minimally-qualified # 🚵🏽‍♂ E4.0 man mountain biking: medium skin tone
+1F6B5 1F3FE 200D 2642 FE0F ; fully-qualified # 🚵🏾‍♂️ E4.0 man mountain biking: medium-dark skin tone
+1F6B5 1F3FE 200D 2642 ; minimally-qualified # 🚵🏾‍♂ E4.0 man mountain biking: medium-dark skin tone
+1F6B5 1F3FF 200D 2642 FE0F ; fully-qualified # 🚵🏿‍♂️ E4.0 man mountain biking: dark skin tone
+1F6B5 1F3FF 200D 2642 ; minimally-qualified # 🚵🏿‍♂ E4.0 man mountain biking: dark skin tone
+1F6B5 200D 2640 FE0F ; fully-qualified # 🚵‍♀️ E4.0 woman mountain biking
+1F6B5 200D 2640 ; minimally-qualified # 🚵‍♀ E4.0 woman mountain biking
+1F6B5 1F3FB 200D 2640 FE0F ; fully-qualified # 🚵🏻‍♀️ E4.0 woman mountain biking: light skin tone
+1F6B5 1F3FB 200D 2640 ; minimally-qualified # 🚵🏻‍♀ E4.0 woman mountain biking: light skin tone
+1F6B5 1F3FC 200D 2640 FE0F ; fully-qualified # 🚵🏼‍♀️ E4.0 woman mountain biking: medium-light skin tone
+1F6B5 1F3FC 200D 2640 ; minimally-qualified # 🚵🏼‍♀ E4.0 woman mountain biking: medium-light skin tone
+1F6B5 1F3FD 200D 2640 FE0F ; fully-qualified # 🚵🏽‍♀️ E4.0 woman mountain biking: medium skin tone
+1F6B5 1F3FD 200D 2640 ; minimally-qualified # 🚵🏽‍♀ E4.0 woman mountain biking: medium skin tone
+1F6B5 1F3FE 200D 2640 FE0F ; fully-qualified # 🚵🏾‍♀️ E4.0 woman mountain biking: medium-dark skin tone
+1F6B5 1F3FE 200D 2640 ; minimally-qualified # 🚵🏾‍♀ E4.0 woman mountain biking: medium-dark skin tone
+1F6B5 1F3FF 200D 2640 FE0F ; fully-qualified # 🚵🏿‍♀️ E4.0 woman mountain biking: dark skin tone
+1F6B5 1F3FF 200D 2640 ; minimally-qualified # 🚵🏿‍♀ E4.0 woman mountain biking: dark skin tone
+1F938 ; fully-qualified # 🤸 E3.0 person cartwheeling
+1F938 1F3FB ; fully-qualified # 🤸🏻 E3.0 person cartwheeling: light skin tone
+1F938 1F3FC ; fully-qualified # 🤸🏼 E3.0 person cartwheeling: medium-light skin tone
+1F938 1F3FD ; fully-qualified # 🤸🏽 E3.0 person cartwheeling: medium skin tone
+1F938 1F3FE ; fully-qualified # 🤸🏾 E3.0 person cartwheeling: medium-dark skin tone
+1F938 1F3FF ; fully-qualified # 🤸🏿 E3.0 person cartwheeling: dark skin tone
+1F938 200D 2642 FE0F ; fully-qualified # 🤸‍♂️ E4.0 man cartwheeling
+1F938 200D 2642 ; minimally-qualified # 🤸‍♂ E4.0 man cartwheeling
+1F938 1F3FB 200D 2642 FE0F ; fully-qualified # 🤸🏻‍♂️ E4.0 man cartwheeling: light skin tone
+1F938 1F3FB 200D 2642 ; minimally-qualified # 🤸🏻‍♂ E4.0 man cartwheeling: light skin tone
+1F938 1F3FC 200D 2642 FE0F ; fully-qualified # 🤸🏼‍♂️ E4.0 man cartwheeling: medium-light skin tone
+1F938 1F3FC 200D 2642 ; minimally-qualified # 🤸🏼‍♂ E4.0 man cartwheeling: medium-light skin tone
+1F938 1F3FD 200D 2642 FE0F ; fully-qualified # 🤸🏽‍♂️ E4.0 man cartwheeling: medium skin tone
+1F938 1F3FD 200D 2642 ; minimally-qualified # 🤸🏽‍♂ E4.0 man cartwheeling: medium skin tone
+1F938 1F3FE 200D 2642 FE0F ; fully-qualified # 🤸🏾‍♂️ E4.0 man cartwheeling: medium-dark skin tone
+1F938 1F3FE 200D 2642 ; minimally-qualified # 🤸🏾‍♂ E4.0 man cartwheeling: medium-dark skin tone
+1F938 1F3FF 200D 2642 FE0F ; fully-qualified # 🤸🏿‍♂️ E4.0 man cartwheeling: dark skin tone
+1F938 1F3FF 200D 2642 ; minimally-qualified # 🤸🏿‍♂ E4.0 man cartwheeling: dark skin tone
+1F938 200D 2640 FE0F ; fully-qualified # 🤸‍♀️ E4.0 woman cartwheeling
+1F938 200D 2640 ; minimally-qualified # 🤸‍♀ E4.0 woman cartwheeling
+1F938 1F3FB 200D 2640 FE0F ; fully-qualified # 🤸🏻‍♀️ E4.0 woman cartwheeling: light skin tone
+1F938 1F3FB 200D 2640 ; minimally-qualified # 🤸🏻‍♀ E4.0 woman cartwheeling: light skin tone
+1F938 1F3FC 200D 2640 FE0F ; fully-qualified # 🤸🏼‍♀️ E4.0 woman cartwheeling: medium-light skin tone
+1F938 1F3FC 200D 2640 ; minimally-qualified # 🤸🏼‍♀ E4.0 woman cartwheeling: medium-light skin tone
+1F938 1F3FD 200D 2640 FE0F ; fully-qualified # 🤸🏽‍♀️ E4.0 woman cartwheeling: medium skin tone
+1F938 1F3FD 200D 2640 ; minimally-qualified # 🤸🏽‍♀ E4.0 woman cartwheeling: medium skin tone
+1F938 1F3FE 200D 2640 FE0F ; fully-qualified # 🤸🏾‍♀️ E4.0 woman cartwheeling: medium-dark skin tone
+1F938 1F3FE 200D 2640 ; minimally-qualified # 🤸🏾‍♀ E4.0 woman cartwheeling: medium-dark skin tone
+1F938 1F3FF 200D 2640 FE0F ; fully-qualified # 🤸🏿‍♀️ E4.0 woman cartwheeling: dark skin tone
+1F938 1F3FF 200D 2640 ; minimally-qualified # 🤸🏿‍♀ E4.0 woman cartwheeling: dark skin tone
+1F93C ; fully-qualified # 🤼 E3.0 people wrestling
+1F93C 200D 2642 FE0F ; fully-qualified # 🤼‍♂️ E4.0 men wrestling
+1F93C 200D 2642 ; minimally-qualified # 🤼‍♂ E4.0 men wrestling
+1F93C 200D 2640 FE0F ; fully-qualified # 🤼‍♀️ E4.0 women wrestling
+1F93C 200D 2640 ; minimally-qualified # 🤼‍♀ E4.0 women wrestling
+1F93D ; fully-qualified # 🤽 E3.0 person playing water polo
+1F93D 1F3FB ; fully-qualified # 🤽🏻 E3.0 person playing water polo: light skin tone
+1F93D 1F3FC ; fully-qualified # 🤽🏼 E3.0 person playing water polo: medium-light skin tone
+1F93D 1F3FD ; fully-qualified # 🤽🏽 E3.0 person playing water polo: medium skin tone
+1F93D 1F3FE ; fully-qualified # 🤽🏾 E3.0 person playing water polo: medium-dark skin tone
+1F93D 1F3FF ; fully-qualified # 🤽🏿 E3.0 person playing water polo: dark skin tone
+1F93D 200D 2642 FE0F ; fully-qualified # 🤽‍♂️ E4.0 man playing water polo
+1F93D 200D 2642 ; minimally-qualified # 🤽‍♂ E4.0 man playing water polo
+1F93D 1F3FB 200D 2642 FE0F ; fully-qualified # 🤽🏻‍♂️ E4.0 man playing water polo: light skin tone
+1F93D 1F3FB 200D 2642 ; minimally-qualified # 🤽🏻‍♂ E4.0 man playing water polo: light skin tone
+1F93D 1F3FC 200D 2642 FE0F ; fully-qualified # 🤽🏼‍♂️ E4.0 man playing water polo: medium-light skin tone
+1F93D 1F3FC 200D 2642 ; minimally-qualified # 🤽🏼‍♂ E4.0 man playing water polo: medium-light skin tone
+1F93D 1F3FD 200D 2642 FE0F ; fully-qualified # 🤽🏽‍♂️ E4.0 man playing water polo: medium skin tone
+1F93D 1F3FD 200D 2642 ; minimally-qualified # 🤽🏽‍♂ E4.0 man playing water polo: medium skin tone
+1F93D 1F3FE 200D 2642 FE0F ; fully-qualified # 🤽🏾‍♂️ E4.0 man playing water polo: medium-dark skin tone
+1F93D 1F3FE 200D 2642 ; minimally-qualified # 🤽🏾‍♂ E4.0 man playing water polo: medium-dark skin tone
+1F93D 1F3FF 200D 2642 FE0F ; fully-qualified # 🤽🏿‍♂️ E4.0 man playing water polo: dark skin tone
+1F93D 1F3FF 200D 2642 ; minimally-qualified # 🤽🏿‍♂ E4.0 man playing water polo: dark skin tone
+1F93D 200D 2640 FE0F ; fully-qualified # 🤽‍♀️ E4.0 woman playing water polo
+1F93D 200D 2640 ; minimally-qualified # 🤽‍♀ E4.0 woman playing water polo
+1F93D 1F3FB 200D 2640 FE0F ; fully-qualified # 🤽🏻‍♀️ E4.0 woman playing water polo: light skin tone
+1F93D 1F3FB 200D 2640 ; minimally-qualified # 🤽🏻‍♀ E4.0 woman playing water polo: light skin tone
+1F93D 1F3FC 200D 2640 FE0F ; fully-qualified # 🤽🏼‍♀️ E4.0 woman playing water polo: medium-light skin tone
+1F93D 1F3FC 200D 2640 ; minimally-qualified # 🤽🏼‍♀ E4.0 woman playing water polo: medium-light skin tone
+1F93D 1F3FD 200D 2640 FE0F ; fully-qualified # 🤽🏽‍♀️ E4.0 woman playing water polo: medium skin tone
+1F93D 1F3FD 200D 2640 ; minimally-qualified # 🤽🏽‍♀ E4.0 woman playing water polo: medium skin tone
+1F93D 1F3FE 200D 2640 FE0F ; fully-qualified # 🤽🏾‍♀️ E4.0 woman playing water polo: medium-dark skin tone
+1F93D 1F3FE 200D 2640 ; minimally-qualified # 🤽🏾‍♀ E4.0 woman playing water polo: medium-dark skin tone
+1F93D 1F3FF 200D 2640 FE0F ; fully-qualified # 🤽🏿‍♀️ E4.0 woman playing water polo: dark skin tone
+1F93D 1F3FF 200D 2640 ; minimally-qualified # 🤽🏿‍♀ E4.0 woman playing water polo: dark skin tone
+1F93E ; fully-qualified # 🤾 E3.0 person playing handball
+1F93E 1F3FB ; fully-qualified # 🤾🏻 E3.0 person playing handball: light skin tone
+1F93E 1F3FC ; fully-qualified # 🤾🏼 E3.0 person playing handball: medium-light skin tone
+1F93E 1F3FD ; fully-qualified # 🤾🏽 E3.0 person playing handball: medium skin tone
+1F93E 1F3FE ; fully-qualified # 🤾🏾 E3.0 person playing handball: medium-dark skin tone
+1F93E 1F3FF ; fully-qualified # 🤾🏿 E3.0 person playing handball: dark skin tone
+1F93E 200D 2642 FE0F ; fully-qualified # 🤾‍♂️ E4.0 man playing handball
+1F93E 200D 2642 ; minimally-qualified # 🤾‍♂ E4.0 man playing handball
+1F93E 1F3FB 200D 2642 FE0F ; fully-qualified # 🤾🏻‍♂️ E4.0 man playing handball: light skin tone
+1F93E 1F3FB 200D 2642 ; minimally-qualified # 🤾🏻‍♂ E4.0 man playing handball: light skin tone
+1F93E 1F3FC 200D 2642 FE0F ; fully-qualified # 🤾🏼‍♂️ E4.0 man playing handball: medium-light skin tone
+1F93E 1F3FC 200D 2642 ; minimally-qualified # 🤾🏼‍♂ E4.0 man playing handball: medium-light skin tone
+1F93E 1F3FD 200D 2642 FE0F ; fully-qualified # 🤾🏽‍♂️ E4.0 man playing handball: medium skin tone
+1F93E 1F3FD 200D 2642 ; minimally-qualified # 🤾🏽‍♂ E4.0 man playing handball: medium skin tone
+1F93E 1F3FE 200D 2642 FE0F ; fully-qualified # 🤾🏾‍♂️ E4.0 man playing handball: medium-dark skin tone
+1F93E 1F3FE 200D 2642 ; minimally-qualified # 🤾🏾‍♂ E4.0 man playing handball: medium-dark skin tone
+1F93E 1F3FF 200D 2642 FE0F ; fully-qualified # 🤾🏿‍♂️ E4.0 man playing handball: dark skin tone
+1F93E 1F3FF 200D 2642 ; minimally-qualified # 🤾🏿‍♂ E4.0 man playing handball: dark skin tone
+1F93E 200D 2640 FE0F ; fully-qualified # 🤾‍♀️ E4.0 woman playing handball
+1F93E 200D 2640 ; minimally-qualified # 🤾‍♀ E4.0 woman playing handball
+1F93E 1F3FB 200D 2640 FE0F ; fully-qualified # 🤾🏻‍♀️ E4.0 woman playing handball: light skin tone
+1F93E 1F3FB 200D 2640 ; minimally-qualified # 🤾🏻‍♀ E4.0 woman playing handball: light skin tone
+1F93E 1F3FC 200D 2640 FE0F ; fully-qualified # 🤾🏼‍♀️ E4.0 woman playing handball: medium-light skin tone
+1F93E 1F3FC 200D 2640 ; minimally-qualified # 🤾🏼‍♀ E4.0 woman playing handball: medium-light skin tone
+1F93E 1F3FD 200D 2640 FE0F ; fully-qualified # 🤾🏽‍♀️ E4.0 woman playing handball: medium skin tone
+1F93E 1F3FD 200D 2640 ; minimally-qualified # 🤾🏽‍♀ E4.0 woman playing handball: medium skin tone
+1F93E 1F3FE 200D 2640 FE0F ; fully-qualified # 🤾🏾‍♀️ E4.0 woman playing handball: medium-dark skin tone
+1F93E 1F3FE 200D 2640 ; minimally-qualified # 🤾🏾‍♀ E4.0 woman playing handball: medium-dark skin tone
+1F93E 1F3FF 200D 2640 FE0F ; fully-qualified # 🤾🏿‍♀️ E4.0 woman playing handball: dark skin tone
+1F93E 1F3FF 200D 2640 ; minimally-qualified # 🤾🏿‍♀ E4.0 woman playing handball: dark skin tone
+1F939 ; fully-qualified # 🤹 E3.0 person juggling
+1F939 1F3FB ; fully-qualified # 🤹🏻 E3.0 person juggling: light skin tone
+1F939 1F3FC ; fully-qualified # 🤹🏼 E3.0 person juggling: medium-light skin tone
+1F939 1F3FD ; fully-qualified # 🤹🏽 E3.0 person juggling: medium skin tone
+1F939 1F3FE ; fully-qualified # 🤹🏾 E3.0 person juggling: medium-dark skin tone
+1F939 1F3FF ; fully-qualified # 🤹🏿 E3.0 person juggling: dark skin tone
+1F939 200D 2642 FE0F ; fully-qualified # 🤹‍♂️ E4.0 man juggling
+1F939 200D 2642 ; minimally-qualified # 🤹‍♂ E4.0 man juggling
+1F939 1F3FB 200D 2642 FE0F ; fully-qualified # 🤹🏻‍♂️ E4.0 man juggling: light skin tone
+1F939 1F3FB 200D 2642 ; minimally-qualified # 🤹🏻‍♂ E4.0 man juggling: light skin tone
+1F939 1F3FC 200D 2642 FE0F ; fully-qualified # 🤹🏼‍♂️ E4.0 man juggling: medium-light skin tone
+1F939 1F3FC 200D 2642 ; minimally-qualified # 🤹🏼‍♂ E4.0 man juggling: medium-light skin tone
+1F939 1F3FD 200D 2642 FE0F ; fully-qualified # 🤹🏽‍♂️ E4.0 man juggling: medium skin tone
+1F939 1F3FD 200D 2642 ; minimally-qualified # 🤹🏽‍♂ E4.0 man juggling: medium skin tone
+1F939 1F3FE 200D 2642 FE0F ; fully-qualified # 🤹🏾‍♂️ E4.0 man juggling: medium-dark skin tone
+1F939 1F3FE 200D 2642 ; minimally-qualified # 🤹🏾‍♂ E4.0 man juggling: medium-dark skin tone
+1F939 1F3FF 200D 2642 FE0F ; fully-qualified # 🤹🏿‍♂️ E4.0 man juggling: dark skin tone
+1F939 1F3FF 200D 2642 ; minimally-qualified # 🤹🏿‍♂ E4.0 man juggling: dark skin tone
+1F939 200D 2640 FE0F ; fully-qualified # 🤹‍♀️ E4.0 woman juggling
+1F939 200D 2640 ; minimally-qualified # 🤹‍♀ E4.0 woman juggling
+1F939 1F3FB 200D 2640 FE0F ; fully-qualified # 🤹🏻‍♀️ E4.0 woman juggling: light skin tone
+1F939 1F3FB 200D 2640 ; minimally-qualified # 🤹🏻‍♀ E4.0 woman juggling: light skin tone
+1F939 1F3FC 200D 2640 FE0F ; fully-qualified # 🤹🏼‍♀️ E4.0 woman juggling: medium-light skin tone
+1F939 1F3FC 200D 2640 ; minimally-qualified # 🤹🏼‍♀ E4.0 woman juggling: medium-light skin tone
+1F939 1F3FD 200D 2640 FE0F ; fully-qualified # 🤹🏽‍♀️ E4.0 woman juggling: medium skin tone
+1F939 1F3FD 200D 2640 ; minimally-qualified # 🤹🏽‍♀ E4.0 woman juggling: medium skin tone
+1F939 1F3FE 200D 2640 FE0F ; fully-qualified # 🤹🏾‍♀️ E4.0 woman juggling: medium-dark skin tone
+1F939 1F3FE 200D 2640 ; minimally-qualified # 🤹🏾‍♀ E4.0 woman juggling: medium-dark skin tone
+1F939 1F3FF 200D 2640 FE0F ; fully-qualified # 🤹🏿‍♀️ E4.0 woman juggling: dark skin tone
+1F939 1F3FF 200D 2640 ; minimally-qualified # 🤹🏿‍♀ E4.0 woman juggling: dark skin tone
+
+# subgroup: person-resting
+1F9D8 ; fully-qualified # 🧘 E5.0 person in lotus position
+1F9D8 1F3FB ; fully-qualified # 🧘🏻 E5.0 person in lotus position: light skin tone
+1F9D8 1F3FC ; fully-qualified # 🧘🏼 E5.0 person in lotus position: medium-light skin tone
+1F9D8 1F3FD ; fully-qualified # 🧘🏽 E5.0 person in lotus position: medium skin tone
+1F9D8 1F3FE ; fully-qualified # 🧘🏾 E5.0 person in lotus position: medium-dark skin tone
+1F9D8 1F3FF ; fully-qualified # 🧘🏿 E5.0 person in lotus position: dark skin tone
+1F9D8 200D 2642 FE0F ; fully-qualified # 🧘‍♂️ E5.0 man in lotus position
+1F9D8 200D 2642 ; minimally-qualified # 🧘‍♂ E5.0 man in lotus position
+1F9D8 1F3FB 200D 2642 FE0F ; fully-qualified # 🧘🏻‍♂️ E5.0 man in lotus position: light skin tone
+1F9D8 1F3FB 200D 2642 ; minimally-qualified # 🧘🏻‍♂ E5.0 man in lotus position: light skin tone
+1F9D8 1F3FC 200D 2642 FE0F ; fully-qualified # 🧘🏼‍♂️ E5.0 man in lotus position: medium-light skin tone
+1F9D8 1F3FC 200D 2642 ; minimally-qualified # 🧘🏼‍♂ E5.0 man in lotus position: medium-light skin tone
+1F9D8 1F3FD 200D 2642 FE0F ; fully-qualified # 🧘🏽‍♂️ E5.0 man in lotus position: medium skin tone
+1F9D8 1F3FD 200D 2642 ; minimally-qualified # 🧘🏽‍♂ E5.0 man in lotus position: medium skin tone
+1F9D8 1F3FE 200D 2642 FE0F ; fully-qualified # 🧘🏾‍♂️ E5.0 man in lotus position: medium-dark skin tone
+1F9D8 1F3FE 200D 2642 ; minimally-qualified # 🧘🏾‍♂ E5.0 man in lotus position: medium-dark skin tone
+1F9D8 1F3FF 200D 2642 FE0F ; fully-qualified # 🧘🏿‍♂️ E5.0 man in lotus position: dark skin tone
+1F9D8 1F3FF 200D 2642 ; minimally-qualified # 🧘🏿‍♂ E5.0 man in lotus position: dark skin tone
+1F9D8 200D 2640 FE0F ; fully-qualified # 🧘‍♀️ E5.0 woman in lotus position
+1F9D8 200D 2640 ; minimally-qualified # 🧘‍♀ E5.0 woman in lotus position
+1F9D8 1F3FB 200D 2640 FE0F ; fully-qualified # 🧘🏻‍♀️ E5.0 woman in lotus position: light skin tone
+1F9D8 1F3FB 200D 2640 ; minimally-qualified # 🧘🏻‍♀ E5.0 woman in lotus position: light skin tone
+1F9D8 1F3FC 200D 2640 FE0F ; fully-qualified # 🧘🏼‍♀️ E5.0 woman in lotus position: medium-light skin tone
+1F9D8 1F3FC 200D 2640 ; minimally-qualified # 🧘🏼‍♀ E5.0 woman in lotus position: medium-light skin tone
+1F9D8 1F3FD 200D 2640 FE0F ; fully-qualified # 🧘🏽‍♀️ E5.0 woman in lotus position: medium skin tone
+1F9D8 1F3FD 200D 2640 ; minimally-qualified # 🧘🏽‍♀ E5.0 woman in lotus position: medium skin tone
+1F9D8 1F3FE 200D 2640 FE0F ; fully-qualified # 🧘🏾‍♀️ E5.0 woman in lotus position: medium-dark skin tone
+1F9D8 1F3FE 200D 2640 ; minimally-qualified # 🧘🏾‍♀ E5.0 woman in lotus position: medium-dark skin tone
+1F9D8 1F3FF 200D 2640 FE0F ; fully-qualified # 🧘🏿‍♀️ E5.0 woman in lotus position: dark skin tone
+1F9D8 1F3FF 200D 2640 ; minimally-qualified # 🧘🏿‍♀ E5.0 woman in lotus position: dark skin tone
+1F6C0 ; fully-qualified # 🛀 E0.6 person taking bath
+1F6C0 1F3FB ; fully-qualified # 🛀🏻 E1.0 person taking bath: light skin tone
+1F6C0 1F3FC ; fully-qualified # 🛀🏼 E1.0 person taking bath: medium-light skin tone
+1F6C0 1F3FD ; fully-qualified # 🛀🏽 E1.0 person taking bath: medium skin tone
+1F6C0 1F3FE ; fully-qualified # 🛀🏾 E1.0 person taking bath: medium-dark skin tone
+1F6C0 1F3FF ; fully-qualified # 🛀🏿 E1.0 person taking bath: dark skin tone
+1F6CC ; fully-qualified # 🛌 E1.0 person in bed
+1F6CC 1F3FB ; fully-qualified # 🛌🏻 E4.0 person in bed: light skin tone
+1F6CC 1F3FC ; fully-qualified # 🛌🏼 E4.0 person in bed: medium-light skin tone
+1F6CC 1F3FD ; fully-qualified # 🛌🏽 E4.0 person in bed: medium skin tone
+1F6CC 1F3FE ; fully-qualified # 🛌🏾 E4.0 person in bed: medium-dark skin tone
+1F6CC 1F3FF ; fully-qualified # 🛌🏿 E4.0 person in bed: dark skin tone
+
+# subgroup: family
+1F9D1 200D 1F91D 200D 1F9D1 ; fully-qualified # 🧑‍🤝‍🧑 E12.0 people holding hands
+1F9D1 1F3FB 200D 1F91D 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏻‍🤝‍🧑🏻 E12.0 people holding hands: light skin tone
+1F9D1 1F3FB 200D 1F91D 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏻‍🤝‍🧑🏼 E12.1 people holding hands: light skin tone, medium-light skin tone
+1F9D1 1F3FB 200D 1F91D 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏻‍🤝‍🧑🏽 E12.1 people holding hands: light skin tone, medium skin tone
+1F9D1 1F3FB 200D 1F91D 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏻‍🤝‍🧑🏾 E12.1 people holding hands: light skin tone, medium-dark skin tone
+1F9D1 1F3FB 200D 1F91D 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏻‍🤝‍🧑🏿 E12.1 people holding hands: light skin tone, dark skin tone
+1F9D1 1F3FC 200D 1F91D 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏼‍🤝‍🧑🏻 E12.0 people holding hands: medium-light skin tone, light skin tone
+1F9D1 1F3FC 200D 1F91D 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏼‍🤝‍🧑🏼 E12.0 people holding hands: medium-light skin tone
+1F9D1 1F3FC 200D 1F91D 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏼‍🤝‍🧑🏽 E12.1 people holding hands: medium-light skin tone, medium skin tone
+1F9D1 1F3FC 200D 1F91D 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏼‍🤝‍🧑🏾 E12.1 people holding hands: medium-light skin tone, medium-dark skin tone
+1F9D1 1F3FC 200D 1F91D 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏼‍🤝‍🧑🏿 E12.1 people holding hands: medium-light skin tone, dark skin tone
+1F9D1 1F3FD 200D 1F91D 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏽‍🤝‍🧑🏻 E12.0 people holding hands: medium skin tone, light skin tone
+1F9D1 1F3FD 200D 1F91D 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏽‍🤝‍🧑🏼 E12.0 people holding hands: medium skin tone, medium-light skin tone
+1F9D1 1F3FD 200D 1F91D 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏽‍🤝‍🧑🏽 E12.0 people holding hands: medium skin tone
+1F9D1 1F3FD 200D 1F91D 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏽‍🤝‍🧑🏾 E12.1 people holding hands: medium skin tone, medium-dark skin tone
+1F9D1 1F3FD 200D 1F91D 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏽‍🤝‍🧑🏿 E12.1 people holding hands: medium skin tone, dark skin tone
+1F9D1 1F3FE 200D 1F91D 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏾‍🤝‍🧑🏻 E12.0 people holding hands: medium-dark skin tone, light skin tone
+1F9D1 1F3FE 200D 1F91D 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏾‍🤝‍🧑🏼 E12.0 people holding hands: medium-dark skin tone, medium-light skin tone
+1F9D1 1F3FE 200D 1F91D 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏾‍🤝‍🧑🏽 E12.0 people holding hands: medium-dark skin tone, medium skin tone
+1F9D1 1F3FE 200D 1F91D 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏾‍🤝‍🧑🏾 E12.0 people holding hands: medium-dark skin tone
+1F9D1 1F3FE 200D 1F91D 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏾‍🤝‍🧑🏿 E12.1 people holding hands: medium-dark skin tone, dark skin tone
+1F9D1 1F3FF 200D 1F91D 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏿‍🤝‍🧑🏻 E12.0 people holding hands: dark skin tone, light skin tone
+1F9D1 1F3FF 200D 1F91D 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏿‍🤝‍🧑🏼 E12.0 people holding hands: dark skin tone, medium-light skin tone
+1F9D1 1F3FF 200D 1F91D 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏿‍🤝‍🧑🏽 E12.0 people holding hands: dark skin tone, medium skin tone
+1F9D1 1F3FF 200D 1F91D 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏿‍🤝‍🧑🏾 E12.0 people holding hands: dark skin tone, medium-dark skin tone
+1F9D1 1F3FF 200D 1F91D 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏿‍🤝‍🧑🏿 E12.0 people holding hands: dark skin tone
+1F46D ; fully-qualified # 👭 E1.0 women holding hands
+1F46D 1F3FB ; fully-qualified # 👭🏻 E12.0 women holding hands: light skin tone
+1F469 1F3FB 200D 1F91D 200D 1F469 1F3FC ; fully-qualified # 👩🏻‍🤝‍👩🏼 E12.1 women holding hands: light skin tone, medium-light skin tone
+1F469 1F3FB 200D 1F91D 200D 1F469 1F3FD ; fully-qualified # 👩🏻‍🤝‍👩🏽 E12.1 women holding hands: light skin tone, medium skin tone
+1F469 1F3FB 200D 1F91D 200D 1F469 1F3FE ; fully-qualified # 👩🏻‍🤝‍👩🏾 E12.1 women holding hands: light skin tone, medium-dark skin tone
+1F469 1F3FB 200D 1F91D 200D 1F469 1F3FF ; fully-qualified # 👩🏻‍🤝‍👩🏿 E12.1 women holding hands: light skin tone, dark skin tone
+1F469 1F3FC 200D 1F91D 200D 1F469 1F3FB ; fully-qualified # 👩🏼‍🤝‍👩🏻 E12.0 women holding hands: medium-light skin tone, light skin tone
+1F46D 1F3FC ; fully-qualified # 👭🏼 E12.0 women holding hands: medium-light skin tone
+1F469 1F3FC 200D 1F91D 200D 1F469 1F3FD ; fully-qualified # 👩🏼‍🤝‍👩🏽 E12.1 women holding hands: medium-light skin tone, medium skin tone
+1F469 1F3FC 200D 1F91D 200D 1F469 1F3FE ; fully-qualified # 👩🏼‍🤝‍👩🏾 E12.1 women holding hands: medium-light skin tone, medium-dark skin tone
+1F469 1F3FC 200D 1F91D 200D 1F469 1F3FF ; fully-qualified # 👩🏼‍🤝‍👩🏿 E12.1 women holding hands: medium-light skin tone, dark skin tone
+1F469 1F3FD 200D 1F91D 200D 1F469 1F3FB ; fully-qualified # 👩🏽‍🤝‍👩🏻 E12.0 women holding hands: medium skin tone, light skin tone
+1F469 1F3FD 200D 1F91D 200D 1F469 1F3FC ; fully-qualified # 👩🏽‍🤝‍👩🏼 E12.0 women holding hands: medium skin tone, medium-light skin tone
+1F46D 1F3FD ; fully-qualified # 👭🏽 E12.0 women holding hands: medium skin tone
+1F469 1F3FD 200D 1F91D 200D 1F469 1F3FE ; fully-qualified # 👩🏽‍🤝‍👩🏾 E12.1 women holding hands: medium skin tone, medium-dark skin tone
+1F469 1F3FD 200D 1F91D 200D 1F469 1F3FF ; fully-qualified # 👩🏽‍🤝‍👩🏿 E12.1 women holding hands: medium skin tone, dark skin tone
+1F469 1F3FE 200D 1F91D 200D 1F469 1F3FB ; fully-qualified # 👩🏾‍🤝‍👩🏻 E12.0 women holding hands: medium-dark skin tone, light skin tone
+1F469 1F3FE 200D 1F91D 200D 1F469 1F3FC ; fully-qualified # 👩🏾‍🤝‍👩🏼 E12.0 women holding hands: medium-dark skin tone, medium-light skin tone
+1F469 1F3FE 200D 1F91D 200D 1F469 1F3FD ; fully-qualified # 👩🏾‍🤝‍👩🏽 E12.0 women holding hands: medium-dark skin tone, medium skin tone
+1F46D 1F3FE ; fully-qualified # 👭🏾 E12.0 women holding hands: medium-dark skin tone
+1F469 1F3FE 200D 1F91D 200D 1F469 1F3FF ; fully-qualified # 👩🏾‍🤝‍👩🏿 E12.1 women holding hands: medium-dark skin tone, dark skin tone
+1F469 1F3FF 200D 1F91D 200D 1F469 1F3FB ; fully-qualified # 👩🏿‍🤝‍👩🏻 E12.0 women holding hands: dark skin tone, light skin tone
+1F469 1F3FF 200D 1F91D 200D 1F469 1F3FC ; fully-qualified # 👩🏿‍🤝‍👩🏼 E12.0 women holding hands: dark skin tone, medium-light skin tone
+1F469 1F3FF 200D 1F91D 200D 1F469 1F3FD ; fully-qualified # 👩🏿‍🤝‍👩🏽 E12.0 women holding hands: dark skin tone, medium skin tone
+1F469 1F3FF 200D 1F91D 200D 1F469 1F3FE ; fully-qualified # 👩🏿‍🤝‍👩🏾 E12.0 women holding hands: dark skin tone, medium-dark skin tone
+1F46D 1F3FF ; fully-qualified # 👭🏿 E12.0 women holding hands: dark skin tone
+1F46B ; fully-qualified # 👫 E0.6 woman and man holding hands
+1F46B 1F3FB ; fully-qualified # 👫🏻 E12.0 woman and man holding hands: light skin tone
+1F469 1F3FB 200D 1F91D 200D 1F468 1F3FC ; fully-qualified # 👩🏻‍🤝‍👨🏼 E12.0 woman and man holding hands: light skin tone, medium-light skin tone
+1F469 1F3FB 200D 1F91D 200D 1F468 1F3FD ; fully-qualified # 👩🏻‍🤝‍👨🏽 E12.0 woman and man holding hands: light skin tone, medium skin tone
+1F469 1F3FB 200D 1F91D 200D 1F468 1F3FE ; fully-qualified # 👩🏻‍🤝‍👨🏾 E12.0 woman and man holding hands: light skin tone, medium-dark skin tone
+1F469 1F3FB 200D 1F91D 200D 1F468 1F3FF ; fully-qualified # 👩🏻‍🤝‍👨🏿 E12.0 woman and man holding hands: light skin tone, dark skin tone
+1F469 1F3FC 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👩🏼‍🤝‍👨🏻 E12.0 woman and man holding hands: medium-light skin tone, light skin tone
+1F46B 1F3FC ; fully-qualified # 👫🏼 E12.0 woman and man holding hands: medium-light skin tone
+1F469 1F3FC 200D 1F91D 200D 1F468 1F3FD ; fully-qualified # 👩🏼‍🤝‍👨🏽 E12.0 woman and man holding hands: medium-light skin tone, medium skin tone
+1F469 1F3FC 200D 1F91D 200D 1F468 1F3FE ; fully-qualified # 👩🏼‍🤝‍👨🏾 E12.0 woman and man holding hands: medium-light skin tone, medium-dark skin tone
+1F469 1F3FC 200D 1F91D 200D 1F468 1F3FF ; fully-qualified # 👩🏼‍🤝‍👨🏿 E12.0 woman and man holding hands: medium-light skin tone, dark skin tone
+1F469 1F3FD 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👩🏽‍🤝‍👨🏻 E12.0 woman and man holding hands: medium skin tone, light skin tone
+1F469 1F3FD 200D 1F91D 200D 1F468 1F3FC ; fully-qualified # 👩🏽‍🤝‍👨🏼 E12.0 woman and man holding hands: medium skin tone, medium-light skin tone
+1F46B 1F3FD ; fully-qualified # 👫🏽 E12.0 woman and man holding hands: medium skin tone
+1F469 1F3FD 200D 1F91D 200D 1F468 1F3FE ; fully-qualified # 👩🏽‍🤝‍👨🏾 E12.0 woman and man holding hands: medium skin tone, medium-dark skin tone
+1F469 1F3FD 200D 1F91D 200D 1F468 1F3FF ; fully-qualified # 👩🏽‍🤝‍👨🏿 E12.0 woman and man holding hands: medium skin tone, dark skin tone
+1F469 1F3FE 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👩🏾‍🤝‍👨🏻 E12.0 woman and man holding hands: medium-dark skin tone, light skin tone
+1F469 1F3FE 200D 1F91D 200D 1F468 1F3FC ; fully-qualified # 👩🏾‍🤝‍👨🏼 E12.0 woman and man holding hands: medium-dark skin tone, medium-light skin tone
+1F469 1F3FE 200D 1F91D 200D 1F468 1F3FD ; fully-qualified # 👩🏾‍🤝‍👨🏽 E12.0 woman and man holding hands: medium-dark skin tone, medium skin tone
+1F46B 1F3FE ; fully-qualified # 👫🏾 E12.0 woman and man holding hands: medium-dark skin tone
+1F469 1F3FE 200D 1F91D 200D 1F468 1F3FF ; fully-qualified # 👩🏾‍🤝‍👨🏿 E12.0 woman and man holding hands: medium-dark skin tone, dark skin tone
+1F469 1F3FF 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👩🏿‍🤝‍👨🏻 E12.0 woman and man holding hands: dark skin tone, light skin tone
+1F469 1F3FF 200D 1F91D 200D 1F468 1F3FC ; fully-qualified # 👩🏿‍🤝‍👨🏼 E12.0 woman and man holding hands: dark skin tone, medium-light skin tone
+1F469 1F3FF 200D 1F91D 200D 1F468 1F3FD ; fully-qualified # 👩🏿‍🤝‍👨🏽 E12.0 woman and man holding hands: dark skin tone, medium skin tone
+1F469 1F3FF 200D 1F91D 200D 1F468 1F3FE ; fully-qualified # 👩🏿‍🤝‍👨🏾 E12.0 woman and man holding hands: dark skin tone, medium-dark skin tone
+1F46B 1F3FF ; fully-qualified # 👫🏿 E12.0 woman and man holding hands: dark skin tone
+1F46C ; fully-qualified # 👬 E1.0 men holding hands
+1F46C 1F3FB ; fully-qualified # 👬🏻 E12.0 men holding hands: light skin tone
+1F468 1F3FB 200D 1F91D 200D 1F468 1F3FC ; fully-qualified # 👨🏻‍🤝‍👨🏼 E12.1 men holding hands: light skin tone, medium-light skin tone
+1F468 1F3FB 200D 1F91D 200D 1F468 1F3FD ; fully-qualified # 👨🏻‍🤝‍👨🏽 E12.1 men holding hands: light skin tone, medium skin tone
+1F468 1F3FB 200D 1F91D 200D 1F468 1F3FE ; fully-qualified # 👨🏻‍🤝‍👨🏾 E12.1 men holding hands: light skin tone, medium-dark skin tone
+1F468 1F3FB 200D 1F91D 200D 1F468 1F3FF ; fully-qualified # 👨🏻‍🤝‍👨🏿 E12.1 men holding hands: light skin tone, dark skin tone
+1F468 1F3FC 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👨🏼‍🤝‍👨🏻 E12.0 men holding hands: medium-light skin tone, light skin tone
+1F46C 1F3FC ; fully-qualified # 👬🏼 E12.0 men holding hands: medium-light skin tone
+1F468 1F3FC 200D 1F91D 200D 1F468 1F3FD ; fully-qualified # 👨🏼‍🤝‍👨🏽 E12.1 men holding hands: medium-light skin tone, medium skin tone
+1F468 1F3FC 200D 1F91D 200D 1F468 1F3FE ; fully-qualified # 👨🏼‍🤝‍👨🏾 E12.1 men holding hands: medium-light skin tone, medium-dark skin tone
+1F468 1F3FC 200D 1F91D 200D 1F468 1F3FF ; fully-qualified # 👨🏼‍🤝‍👨🏿 E12.1 men holding hands: medium-light skin tone, dark skin tone
+1F468 1F3FD 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👨🏽‍🤝‍👨🏻 E12.0 men holding hands: medium skin tone, light skin tone
+1F468 1F3FD 200D 1F91D 200D 1F468 1F3FC ; fully-qualified # 👨🏽‍🤝‍👨🏼 E12.0 men holding hands: medium skin tone, medium-light skin tone
+1F46C 1F3FD ; fully-qualified # 👬🏽 E12.0 men holding hands: medium skin tone
+1F468 1F3FD 200D 1F91D 200D 1F468 1F3FE ; fully-qualified # 👨🏽‍🤝‍👨🏾 E12.1 men holding hands: medium skin tone, medium-dark skin tone
+1F468 1F3FD 200D 1F91D 200D 1F468 1F3FF ; fully-qualified # 👨🏽‍🤝‍👨🏿 E12.1 men holding hands: medium skin tone, dark skin tone
+1F468 1F3FE 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👨🏾‍🤝‍👨🏻 E12.0 men holding hands: medium-dark skin tone, light skin tone
+1F468 1F3FE 200D 1F91D 200D 1F468 1F3FC ; fully-qualified # 👨🏾‍🤝‍👨🏼 E12.0 men holding hands: medium-dark skin tone, medium-light skin tone
+1F468 1F3FE 200D 1F91D 200D 1F468 1F3FD ; fully-qualified # 👨🏾‍🤝‍👨🏽 E12.0 men holding hands: medium-dark skin tone, medium skin tone
+1F46C 1F3FE ; fully-qualified # 👬🏾 E12.0 men holding hands: medium-dark skin tone
+1F468 1F3FE 200D 1F91D 200D 1F468 1F3FF ; fully-qualified # 👨🏾‍🤝‍👨🏿 E12.1 men holding hands: medium-dark skin tone, dark skin tone
+1F468 1F3FF 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👨🏿‍🤝‍👨🏻 E12.0 men holding hands: dark skin tone, light skin tone
+1F468 1F3FF 200D 1F91D 200D 1F468 1F3FC ; fully-qualified # 👨🏿‍🤝‍👨🏼 E12.0 men holding hands: dark skin tone, medium-light skin tone
+1F468 1F3FF 200D 1F91D 200D 1F468 1F3FD ; fully-qualified # 👨🏿‍🤝‍👨🏽 E12.0 men holding hands: dark skin tone, medium skin tone
+1F468 1F3FF 200D 1F91D 200D 1F468 1F3FE ; fully-qualified # 👨🏿‍🤝‍👨🏾 E12.0 men holding hands: dark skin tone, medium-dark skin tone
+1F46C 1F3FF ; fully-qualified # 👬🏿 E12.0 men holding hands: dark skin tone
+1F48F ; fully-qualified # 💏 E0.6 kiss
+1F48F 1F3FB ; fully-qualified # 💏🏻 E13.1 kiss: light skin tone
+1F48F 1F3FC ; fully-qualified # 💏🏼 E13.1 kiss: medium-light skin tone
+1F48F 1F3FD ; fully-qualified # 💏🏽 E13.1 kiss: medium skin tone
+1F48F 1F3FE ; fully-qualified # 💏🏾 E13.1 kiss: medium-dark skin tone
+1F48F 1F3FF ; fully-qualified # 💏🏿 E13.1 kiss: dark skin tone
+1F9D1 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏻‍❤️‍💋‍🧑🏼 E13.1 kiss: person, person, light skin tone, medium-light skin tone
+1F9D1 1F3FB 200D 2764 200D 1F48B 200D 1F9D1 1F3FC ; minimally-qualified # 🧑🏻‍❤‍💋‍🧑🏼 E13.1 kiss: person, person, light skin tone, medium-light skin tone
+1F9D1 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏻‍❤️‍💋‍🧑🏽 E13.1 kiss: person, person, light skin tone, medium skin tone
+1F9D1 1F3FB 200D 2764 200D 1F48B 200D 1F9D1 1F3FD ; minimally-qualified # 🧑🏻‍❤‍💋‍🧑🏽 E13.1 kiss: person, person, light skin tone, medium skin tone
+1F9D1 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏻‍❤️‍💋‍🧑🏾 E13.1 kiss: person, person, light skin tone, medium-dark skin tone
+1F9D1 1F3FB 200D 2764 200D 1F48B 200D 1F9D1 1F3FE ; minimally-qualified # 🧑🏻‍❤‍💋‍🧑🏾 E13.1 kiss: person, person, light skin tone, medium-dark skin tone
+1F9D1 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏻‍❤️‍💋‍🧑🏿 E13.1 kiss: person, person, light skin tone, dark skin tone
+1F9D1 1F3FB 200D 2764 200D 1F48B 200D 1F9D1 1F3FF ; minimally-qualified # 🧑🏻‍❤‍💋‍🧑🏿 E13.1 kiss: person, person, light skin tone, dark skin tone
+1F9D1 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏼‍❤️‍💋‍🧑🏻 E13.1 kiss: person, person, medium-light skin tone, light skin tone
+1F9D1 1F3FC 200D 2764 200D 1F48B 200D 1F9D1 1F3FB ; minimally-qualified # 🧑🏼‍❤‍💋‍🧑🏻 E13.1 kiss: person, person, medium-light skin tone, light skin tone
+1F9D1 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏼‍❤️‍💋‍🧑🏽 E13.1 kiss: person, person, medium-light skin tone, medium skin tone
+1F9D1 1F3FC 200D 2764 200D 1F48B 200D 1F9D1 1F3FD ; minimally-qualified # 🧑🏼‍❤‍💋‍🧑🏽 E13.1 kiss: person, person, medium-light skin tone, medium skin tone
+1F9D1 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏼‍❤️‍💋‍🧑🏾 E13.1 kiss: person, person, medium-light skin tone, medium-dark skin tone
+1F9D1 1F3FC 200D 2764 200D 1F48B 200D 1F9D1 1F3FE ; minimally-qualified # 🧑🏼‍❤‍💋‍🧑🏾 E13.1 kiss: person, person, medium-light skin tone, medium-dark skin tone
+1F9D1 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏼‍❤️‍💋‍🧑🏿 E13.1 kiss: person, person, medium-light skin tone, dark skin tone
+1F9D1 1F3FC 200D 2764 200D 1F48B 200D 1F9D1 1F3FF ; minimally-qualified # 🧑🏼‍❤‍💋‍🧑🏿 E13.1 kiss: person, person, medium-light skin tone, dark skin tone
+1F9D1 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏽‍❤️‍💋‍🧑🏻 E13.1 kiss: person, person, medium skin tone, light skin tone
+1F9D1 1F3FD 200D 2764 200D 1F48B 200D 1F9D1 1F3FB ; minimally-qualified # 🧑🏽‍❤‍💋‍🧑🏻 E13.1 kiss: person, person, medium skin tone, light skin tone
+1F9D1 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏽‍❤️‍💋‍🧑🏼 E13.1 kiss: person, person, medium skin tone, medium-light skin tone
+1F9D1 1F3FD 200D 2764 200D 1F48B 200D 1F9D1 1F3FC ; minimally-qualified # 🧑🏽‍❤‍💋‍🧑🏼 E13.1 kiss: person, person, medium skin tone, medium-light skin tone
+1F9D1 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏽‍❤️‍💋‍🧑🏾 E13.1 kiss: person, person, medium skin tone, medium-dark skin tone
+1F9D1 1F3FD 200D 2764 200D 1F48B 200D 1F9D1 1F3FE ; minimally-qualified # 🧑🏽‍❤‍💋‍🧑🏾 E13.1 kiss: person, person, medium skin tone, medium-dark skin tone
+1F9D1 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏽‍❤️‍💋‍🧑🏿 E13.1 kiss: person, person, medium skin tone, dark skin tone
+1F9D1 1F3FD 200D 2764 200D 1F48B 200D 1F9D1 1F3FF ; minimally-qualified # 🧑🏽‍❤‍💋‍🧑🏿 E13.1 kiss: person, person, medium skin tone, dark skin tone
+1F9D1 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏾‍❤️‍💋‍🧑🏻 E13.1 kiss: person, person, medium-dark skin tone, light skin tone
+1F9D1 1F3FE 200D 2764 200D 1F48B 200D 1F9D1 1F3FB ; minimally-qualified # 🧑🏾‍❤‍💋‍🧑🏻 E13.1 kiss: person, person, medium-dark skin tone, light skin tone
+1F9D1 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏾‍❤️‍💋‍🧑🏼 E13.1 kiss: person, person, medium-dark skin tone, medium-light skin tone
+1F9D1 1F3FE 200D 2764 200D 1F48B 200D 1F9D1 1F3FC ; minimally-qualified # 🧑🏾‍❤‍💋‍🧑🏼 E13.1 kiss: person, person, medium-dark skin tone, medium-light skin tone
+1F9D1 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏾‍❤️‍💋‍🧑🏽 E13.1 kiss: person, person, medium-dark skin tone, medium skin tone
+1F9D1 1F3FE 200D 2764 200D 1F48B 200D 1F9D1 1F3FD ; minimally-qualified # 🧑🏾‍❤‍💋‍🧑🏽 E13.1 kiss: person, person, medium-dark skin tone, medium skin tone
+1F9D1 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏾‍❤️‍💋‍🧑🏿 E13.1 kiss: person, person, medium-dark skin tone, dark skin tone
+1F9D1 1F3FE 200D 2764 200D 1F48B 200D 1F9D1 1F3FF ; minimally-qualified # 🧑🏾‍❤‍💋‍🧑🏿 E13.1 kiss: person, person, medium-dark skin tone, dark skin tone
+1F9D1 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏿‍❤️‍💋‍🧑🏻 E13.1 kiss: person, person, dark skin tone, light skin tone
+1F9D1 1F3FF 200D 2764 200D 1F48B 200D 1F9D1 1F3FB ; minimally-qualified # 🧑🏿‍❤‍💋‍🧑🏻 E13.1 kiss: person, person, dark skin tone, light skin tone
+1F9D1 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏿‍❤️‍💋‍🧑🏼 E13.1 kiss: person, person, dark skin tone, medium-light skin tone
+1F9D1 1F3FF 200D 2764 200D 1F48B 200D 1F9D1 1F3FC ; minimally-qualified # 🧑🏿‍❤‍💋‍🧑🏼 E13.1 kiss: person, person, dark skin tone, medium-light skin tone
+1F9D1 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏿‍❤️‍💋‍🧑🏽 E13.1 kiss: person, person, dark skin tone, medium skin tone
+1F9D1 1F3FF 200D 2764 200D 1F48B 200D 1F9D1 1F3FD ; minimally-qualified # 🧑🏿‍❤‍💋‍🧑🏽 E13.1 kiss: person, person, dark skin tone, medium skin tone
+1F9D1 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏿‍❤️‍💋‍🧑🏾 E13.1 kiss: person, person, dark skin tone, medium-dark skin tone
+1F9D1 1F3FF 200D 2764 200D 1F48B 200D 1F9D1 1F3FE ; minimally-qualified # 🧑🏿‍❤‍💋‍🧑🏾 E13.1 kiss: person, person, dark skin tone, medium-dark skin tone
+1F469 200D 2764 FE0F 200D 1F48B 200D 1F468 ; fully-qualified # 👩‍❤️‍💋‍👨 E2.0 kiss: woman, man
+1F469 200D 2764 200D 1F48B 200D 1F468 ; minimally-qualified # 👩‍❤‍💋‍👨 E2.0 kiss: woman, man
+1F469 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FB ; fully-qualified # 👩🏻‍❤️‍💋‍👨🏻 E13.1 kiss: woman, man, light skin tone
+1F469 1F3FB 200D 2764 200D 1F48B 200D 1F468 1F3FB ; minimally-qualified # 👩🏻‍❤‍💋‍👨🏻 E13.1 kiss: woman, man, light skin tone
+1F469 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FC ; fully-qualified # 👩🏻‍❤️‍💋‍👨🏼 E13.1 kiss: woman, man, light skin tone, medium-light skin tone
+1F469 1F3FB 200D 2764 200D 1F48B 200D 1F468 1F3FC ; minimally-qualified # 👩🏻‍❤‍💋‍👨🏼 E13.1 kiss: woman, man, light skin tone, medium-light skin tone
+1F469 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FD ; fully-qualified # 👩🏻‍❤️‍💋‍👨🏽 E13.1 kiss: woman, man, light skin tone, medium skin tone
+1F469 1F3FB 200D 2764 200D 1F48B 200D 1F468 1F3FD ; minimally-qualified # 👩🏻‍❤‍💋‍👨🏽 E13.1 kiss: woman, man, light skin tone, medium skin tone
+1F469 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FE ; fully-qualified # 👩🏻‍❤️‍💋‍👨🏾 E13.1 kiss: woman, man, light skin tone, medium-dark skin tone
+1F469 1F3FB 200D 2764 200D 1F48B 200D 1F468 1F3FE ; minimally-qualified # 👩🏻‍❤‍💋‍👨🏾 E13.1 kiss: woman, man, light skin tone, medium-dark skin tone
+1F469 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FF ; fully-qualified # 👩🏻‍❤️‍💋‍👨🏿 E13.1 kiss: woman, man, light skin tone, dark skin tone
+1F469 1F3FB 200D 2764 200D 1F48B 200D 1F468 1F3FF ; minimally-qualified # 👩🏻‍❤‍💋‍👨🏿 E13.1 kiss: woman, man, light skin tone, dark skin tone
+1F469 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FB ; fully-qualified # 👩🏼‍❤️‍💋‍👨🏻 E13.1 kiss: woman, man, medium-light skin tone, light skin tone
+1F469 1F3FC 200D 2764 200D 1F48B 200D 1F468 1F3FB ; minimally-qualified # 👩🏼‍❤‍💋‍👨🏻 E13.1 kiss: woman, man, medium-light skin tone, light skin tone
+1F469 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FC ; fully-qualified # 👩🏼‍❤️‍💋‍👨🏼 E13.1 kiss: woman, man, medium-light skin tone
+1F469 1F3FC 200D 2764 200D 1F48B 200D 1F468 1F3FC ; minimally-qualified # 👩🏼‍❤‍💋‍👨🏼 E13.1 kiss: woman, man, medium-light skin tone
+1F469 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FD ; fully-qualified # 👩🏼‍❤️‍💋‍👨🏽 E13.1 kiss: woman, man, medium-light skin tone, medium skin tone
+1F469 1F3FC 200D 2764 200D 1F48B 200D 1F468 1F3FD ; minimally-qualified # 👩🏼‍❤‍💋‍👨🏽 E13.1 kiss: woman, man, medium-light skin tone, medium skin tone
+1F469 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FE ; fully-qualified # 👩🏼‍❤️‍💋‍👨🏾 E13.1 kiss: woman, man, medium-light skin tone, medium-dark skin tone
+1F469 1F3FC 200D 2764 200D 1F48B 200D 1F468 1F3FE ; minimally-qualified # 👩🏼‍❤‍💋‍👨🏾 E13.1 kiss: woman, man, medium-light skin tone, medium-dark skin tone
+1F469 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FF ; fully-qualified # 👩🏼‍❤️‍💋‍👨🏿 E13.1 kiss: woman, man, medium-light skin tone, dark skin tone
+1F469 1F3FC 200D 2764 200D 1F48B 200D 1F468 1F3FF ; minimally-qualified # 👩🏼‍❤‍💋‍👨🏿 E13.1 kiss: woman, man, medium-light skin tone, dark skin tone
+1F469 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FB ; fully-qualified # 👩🏽‍❤️‍💋‍👨🏻 E13.1 kiss: woman, man, medium skin tone, light skin tone
+1F469 1F3FD 200D 2764 200D 1F48B 200D 1F468 1F3FB ; minimally-qualified # 👩🏽‍❤‍💋‍👨🏻 E13.1 kiss: woman, man, medium skin tone, light skin tone
+1F469 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FC ; fully-qualified # 👩🏽‍❤️‍💋‍👨🏼 E13.1 kiss: woman, man, medium skin tone, medium-light skin tone
+1F469 1F3FD 200D 2764 200D 1F48B 200D 1F468 1F3FC ; minimally-qualified # 👩🏽‍❤‍💋‍👨🏼 E13.1 kiss: woman, man, medium skin tone, medium-light skin tone
+1F469 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FD ; fully-qualified # 👩🏽‍❤️‍💋‍👨🏽 E13.1 kiss: woman, man, medium skin tone
+1F469 1F3FD 200D 2764 200D 1F48B 200D 1F468 1F3FD ; minimally-qualified # 👩🏽‍❤‍💋‍👨🏽 E13.1 kiss: woman, man, medium skin tone
+1F469 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FE ; fully-qualified # 👩🏽‍❤️‍💋‍👨🏾 E13.1 kiss: woman, man, medium skin tone, medium-dark skin tone
+1F469 1F3FD 200D 2764 200D 1F48B 200D 1F468 1F3FE ; minimally-qualified # 👩🏽‍❤‍💋‍👨🏾 E13.1 kiss: woman, man, medium skin tone, medium-dark skin tone
+1F469 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FF ; fully-qualified # 👩🏽‍❤️‍💋‍👨🏿 E13.1 kiss: woman, man, medium skin tone, dark skin tone
+1F469 1F3FD 200D 2764 200D 1F48B 200D 1F468 1F3FF ; minimally-qualified # 👩🏽‍❤‍💋‍👨🏿 E13.1 kiss: woman, man, medium skin tone, dark skin tone
+1F469 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FB ; fully-qualified # 👩🏾‍❤️‍💋‍👨🏻 E13.1 kiss: woman, man, medium-dark skin tone, light skin tone
+1F469 1F3FE 200D 2764 200D 1F48B 200D 1F468 1F3FB ; minimally-qualified # 👩🏾‍❤‍💋‍👨🏻 E13.1 kiss: woman, man, medium-dark skin tone, light skin tone
+1F469 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FC ; fully-qualified # 👩🏾‍❤️‍💋‍👨🏼 E13.1 kiss: woman, man, medium-dark skin tone, medium-light skin tone
+1F469 1F3FE 200D 2764 200D 1F48B 200D 1F468 1F3FC ; minimally-qualified # 👩🏾‍❤‍💋‍👨🏼 E13.1 kiss: woman, man, medium-dark skin tone, medium-light skin tone
+1F469 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FD ; fully-qualified # 👩🏾‍❤️‍💋‍👨🏽 E13.1 kiss: woman, man, medium-dark skin tone, medium skin tone
+1F469 1F3FE 200D 2764 200D 1F48B 200D 1F468 1F3FD ; minimally-qualified # 👩🏾‍❤‍💋‍👨🏽 E13.1 kiss: woman, man, medium-dark skin tone, medium skin tone
+1F469 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FE ; fully-qualified # 👩🏾‍❤️‍💋‍👨🏾 E13.1 kiss: woman, man, medium-dark skin tone
+1F469 1F3FE 200D 2764 200D 1F48B 200D 1F468 1F3FE ; minimally-qualified # 👩🏾‍❤‍💋‍👨🏾 E13.1 kiss: woman, man, medium-dark skin tone
+1F469 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FF ; fully-qualified # 👩🏾‍❤️‍💋‍👨🏿 E13.1 kiss: woman, man, medium-dark skin tone, dark skin tone
+1F469 1F3FE 200D 2764 200D 1F48B 200D 1F468 1F3FF ; minimally-qualified # 👩🏾‍❤‍💋‍👨🏿 E13.1 kiss: woman, man, medium-dark skin tone, dark skin tone
+1F469 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FB ; fully-qualified # 👩🏿‍❤️‍💋‍👨🏻 E13.1 kiss: woman, man, dark skin tone, light skin tone
+1F469 1F3FF 200D 2764 200D 1F48B 200D 1F468 1F3FB ; minimally-qualified # 👩🏿‍❤‍💋‍👨🏻 E13.1 kiss: woman, man, dark skin tone, light skin tone
+1F469 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FC ; fully-qualified # 👩🏿‍❤️‍💋‍👨🏼 E13.1 kiss: woman, man, dark skin tone, medium-light skin tone
+1F469 1F3FF 200D 2764 200D 1F48B 200D 1F468 1F3FC ; minimally-qualified # 👩🏿‍❤‍💋‍👨🏼 E13.1 kiss: woman, man, dark skin tone, medium-light skin tone
+1F469 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FD ; fully-qualified # 👩🏿‍❤️‍💋‍👨🏽 E13.1 kiss: woman, man, dark skin tone, medium skin tone
+1F469 1F3FF 200D 2764 200D 1F48B 200D 1F468 1F3FD ; minimally-qualified # 👩🏿‍❤‍💋‍👨🏽 E13.1 kiss: woman, man, dark skin tone, medium skin tone
+1F469 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FE ; fully-qualified # 👩🏿‍❤️‍💋‍👨🏾 E13.1 kiss: woman, man, dark skin tone, medium-dark skin tone
+1F469 1F3FF 200D 2764 200D 1F48B 200D 1F468 1F3FE ; minimally-qualified # 👩🏿‍❤‍💋‍👨🏾 E13.1 kiss: woman, man, dark skin tone, medium-dark skin tone
+1F469 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FF ; fully-qualified # 👩🏿‍❤️‍💋‍👨🏿 E13.1 kiss: woman, man, dark skin tone
+1F469 1F3FF 200D 2764 200D 1F48B 200D 1F468 1F3FF ; minimally-qualified # 👩🏿‍❤‍💋‍👨🏿 E13.1 kiss: woman, man, dark skin tone
+1F468 200D 2764 FE0F 200D 1F48B 200D 1F468 ; fully-qualified # 👨‍❤️‍💋‍👨 E2.0 kiss: man, man
+1F468 200D 2764 200D 1F48B 200D 1F468 ; minimally-qualified # 👨‍❤‍💋‍👨 E2.0 kiss: man, man
+1F468 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FB ; fully-qualified # 👨🏻‍❤️‍💋‍👨🏻 E13.1 kiss: man, man, light skin tone
+1F468 1F3FB 200D 2764 200D 1F48B 200D 1F468 1F3FB ; minimally-qualified # 👨🏻‍❤‍💋‍👨🏻 E13.1 kiss: man, man, light skin tone
+1F468 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FC ; fully-qualified # 👨🏻‍❤️‍💋‍👨🏼 E13.1 kiss: man, man, light skin tone, medium-light skin tone
+1F468 1F3FB 200D 2764 200D 1F48B 200D 1F468 1F3FC ; minimally-qualified # 👨🏻‍❤‍💋‍👨🏼 E13.1 kiss: man, man, light skin tone, medium-light skin tone
+1F468 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FD ; fully-qualified # 👨🏻‍❤️‍💋‍👨🏽 E13.1 kiss: man, man, light skin tone, medium skin tone
+1F468 1F3FB 200D 2764 200D 1F48B 200D 1F468 1F3FD ; minimally-qualified # 👨🏻‍❤‍💋‍👨🏽 E13.1 kiss: man, man, light skin tone, medium skin tone
+1F468 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FE ; fully-qualified # 👨🏻‍❤️‍💋‍👨🏾 E13.1 kiss: man, man, light skin tone, medium-dark skin tone
+1F468 1F3FB 200D 2764 200D 1F48B 200D 1F468 1F3FE ; minimally-qualified # 👨🏻‍❤‍💋‍👨🏾 E13.1 kiss: man, man, light skin tone, medium-dark skin tone
+1F468 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FF ; fully-qualified # 👨🏻‍❤️‍💋‍👨🏿 E13.1 kiss: man, man, light skin tone, dark skin tone
+1F468 1F3FB 200D 2764 200D 1F48B 200D 1F468 1F3FF ; minimally-qualified # 👨🏻‍❤‍💋‍👨🏿 E13.1 kiss: man, man, light skin tone, dark skin tone
+1F468 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FB ; fully-qualified # 👨🏼‍❤️‍💋‍👨🏻 E13.1 kiss: man, man, medium-light skin tone, light skin tone
+1F468 1F3FC 200D 2764 200D 1F48B 200D 1F468 1F3FB ; minimally-qualified # 👨🏼‍❤‍💋‍👨🏻 E13.1 kiss: man, man, medium-light skin tone, light skin tone
+1F468 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FC ; fully-qualified # 👨🏼‍❤️‍💋‍👨🏼 E13.1 kiss: man, man, medium-light skin tone
+1F468 1F3FC 200D 2764 200D 1F48B 200D 1F468 1F3FC ; minimally-qualified # 👨🏼‍❤‍💋‍👨🏼 E13.1 kiss: man, man, medium-light skin tone
+1F468 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FD ; fully-qualified # 👨🏼‍❤️‍💋‍👨🏽 E13.1 kiss: man, man, medium-light skin tone, medium skin tone
+1F468 1F3FC 200D 2764 200D 1F48B 200D 1F468 1F3FD ; minimally-qualified # 👨🏼‍❤‍💋‍👨🏽 E13.1 kiss: man, man, medium-light skin tone, medium skin tone
+1F468 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FE ; fully-qualified # 👨🏼‍❤️‍💋‍👨🏾 E13.1 kiss: man, man, medium-light skin tone, medium-dark skin tone
+1F468 1F3FC 200D 2764 200D 1F48B 200D 1F468 1F3FE ; minimally-qualified # 👨🏼‍❤‍💋‍👨🏾 E13.1 kiss: man, man, medium-light skin tone, medium-dark skin tone
+1F468 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FF ; fully-qualified # 👨🏼‍❤️‍💋‍👨🏿 E13.1 kiss: man, man, medium-light skin tone, dark skin tone
+1F468 1F3FC 200D 2764 200D 1F48B 200D 1F468 1F3FF ; minimally-qualified # 👨🏼‍❤‍💋‍👨🏿 E13.1 kiss: man, man, medium-light skin tone, dark skin tone
+1F468 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FB ; fully-qualified # 👨🏽‍❤️‍💋‍👨🏻 E13.1 kiss: man, man, medium skin tone, light skin tone
+1F468 1F3FD 200D 2764 200D 1F48B 200D 1F468 1F3FB ; minimally-qualified # 👨🏽‍❤‍💋‍👨🏻 E13.1 kiss: man, man, medium skin tone, light skin tone
+1F468 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FC ; fully-qualified # 👨🏽‍❤️‍💋‍👨🏼 E13.1 kiss: man, man, medium skin tone, medium-light skin tone
+1F468 1F3FD 200D 2764 200D 1F48B 200D 1F468 1F3FC ; minimally-qualified # 👨🏽‍❤‍💋‍👨🏼 E13.1 kiss: man, man, medium skin tone, medium-light skin tone
+1F468 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FD ; fully-qualified # 👨🏽‍❤️‍💋‍👨🏽 E13.1 kiss: man, man, medium skin tone
+1F468 1F3FD 200D 2764 200D 1F48B 200D 1F468 1F3FD ; minimally-qualified # 👨🏽‍❤‍💋‍👨🏽 E13.1 kiss: man, man, medium skin tone
+1F468 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FE ; fully-qualified # 👨🏽‍❤️‍💋‍👨🏾 E13.1 kiss: man, man, medium skin tone, medium-dark skin tone
+1F468 1F3FD 200D 2764 200D 1F48B 200D 1F468 1F3FE ; minimally-qualified # 👨🏽‍❤‍💋‍👨🏾 E13.1 kiss: man, man, medium skin tone, medium-dark skin tone
+1F468 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FF ; fully-qualified # 👨🏽‍❤️‍💋‍👨🏿 E13.1 kiss: man, man, medium skin tone, dark skin tone
+1F468 1F3FD 200D 2764 200D 1F48B 200D 1F468 1F3FF ; minimally-qualified # 👨🏽‍❤‍💋‍👨🏿 E13.1 kiss: man, man, medium skin tone, dark skin tone
+1F468 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FB ; fully-qualified # 👨🏾‍❤️‍💋‍👨🏻 E13.1 kiss: man, man, medium-dark skin tone, light skin tone
+1F468 1F3FE 200D 2764 200D 1F48B 200D 1F468 1F3FB ; minimally-qualified # 👨🏾‍❤‍💋‍👨🏻 E13.1 kiss: man, man, medium-dark skin tone, light skin tone
+1F468 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FC ; fully-qualified # 👨🏾‍❤️‍💋‍👨🏼 E13.1 kiss: man, man, medium-dark skin tone, medium-light skin tone
+1F468 1F3FE 200D 2764 200D 1F48B 200D 1F468 1F3FC ; minimally-qualified # 👨🏾‍❤‍💋‍👨🏼 E13.1 kiss: man, man, medium-dark skin tone, medium-light skin tone
+1F468 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FD ; fully-qualified # 👨🏾‍❤️‍💋‍👨🏽 E13.1 kiss: man, man, medium-dark skin tone, medium skin tone
+1F468 1F3FE 200D 2764 200D 1F48B 200D 1F468 1F3FD ; minimally-qualified # 👨🏾‍❤‍💋‍👨🏽 E13.1 kiss: man, man, medium-dark skin tone, medium skin tone
+1F468 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FE ; fully-qualified # 👨🏾‍❤️‍💋‍👨🏾 E13.1 kiss: man, man, medium-dark skin tone
+1F468 1F3FE 200D 2764 200D 1F48B 200D 1F468 1F3FE ; minimally-qualified # 👨🏾‍❤‍💋‍👨🏾 E13.1 kiss: man, man, medium-dark skin tone
+1F468 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FF ; fully-qualified # 👨🏾‍❤️‍💋‍👨🏿 E13.1 kiss: man, man, medium-dark skin tone, dark skin tone
+1F468 1F3FE 200D 2764 200D 1F48B 200D 1F468 1F3FF ; minimally-qualified # 👨🏾‍❤‍💋‍👨🏿 E13.1 kiss: man, man, medium-dark skin tone, dark skin tone
+1F468 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FB ; fully-qualified # 👨🏿‍❤️‍💋‍👨🏻 E13.1 kiss: man, man, dark skin tone, light skin tone
+1F468 1F3FF 200D 2764 200D 1F48B 200D 1F468 1F3FB ; minimally-qualified # 👨🏿‍❤‍💋‍👨🏻 E13.1 kiss: man, man, dark skin tone, light skin tone
+1F468 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FC ; fully-qualified # 👨🏿‍❤️‍💋‍👨🏼 E13.1 kiss: man, man, dark skin tone, medium-light skin tone
+1F468 1F3FF 200D 2764 200D 1F48B 200D 1F468 1F3FC ; minimally-qualified # 👨🏿‍❤‍💋‍👨🏼 E13.1 kiss: man, man, dark skin tone, medium-light skin tone
+1F468 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FD ; fully-qualified # 👨🏿‍❤️‍💋‍👨🏽 E13.1 kiss: man, man, dark skin tone, medium skin tone
+1F468 1F3FF 200D 2764 200D 1F48B 200D 1F468 1F3FD ; minimally-qualified # 👨🏿‍❤‍💋‍👨🏽 E13.1 kiss: man, man, dark skin tone, medium skin tone
+1F468 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FE ; fully-qualified # 👨🏿‍❤️‍💋‍👨🏾 E13.1 kiss: man, man, dark skin tone, medium-dark skin tone
+1F468 1F3FF 200D 2764 200D 1F48B 200D 1F468 1F3FE ; minimally-qualified # 👨🏿‍❤‍💋‍👨🏾 E13.1 kiss: man, man, dark skin tone, medium-dark skin tone
+1F468 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F468 1F3FF ; fully-qualified # 👨🏿‍❤️‍💋‍👨🏿 E13.1 kiss: man, man, dark skin tone
+1F468 1F3FF 200D 2764 200D 1F48B 200D 1F468 1F3FF ; minimally-qualified # 👨🏿‍❤‍💋‍👨🏿 E13.1 kiss: man, man, dark skin tone
+1F469 200D 2764 FE0F 200D 1F48B 200D 1F469 ; fully-qualified # 👩‍❤️‍💋‍👩 E2.0 kiss: woman, woman
+1F469 200D 2764 200D 1F48B 200D 1F469 ; minimally-qualified # 👩‍❤‍💋‍👩 E2.0 kiss: woman, woman
+1F469 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FB ; fully-qualified # 👩🏻‍❤️‍💋‍👩🏻 E13.1 kiss: woman, woman, light skin tone
+1F469 1F3FB 200D 2764 200D 1F48B 200D 1F469 1F3FB ; minimally-qualified # 👩🏻‍❤‍💋‍👩🏻 E13.1 kiss: woman, woman, light skin tone
+1F469 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FC ; fully-qualified # 👩🏻‍❤️‍💋‍👩🏼 E13.1 kiss: woman, woman, light skin tone, medium-light skin tone
+1F469 1F3FB 200D 2764 200D 1F48B 200D 1F469 1F3FC ; minimally-qualified # 👩🏻‍❤‍💋‍👩🏼 E13.1 kiss: woman, woman, light skin tone, medium-light skin tone
+1F469 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FD ; fully-qualified # 👩🏻‍❤️‍💋‍👩🏽 E13.1 kiss: woman, woman, light skin tone, medium skin tone
+1F469 1F3FB 200D 2764 200D 1F48B 200D 1F469 1F3FD ; minimally-qualified # 👩🏻‍❤‍💋‍👩🏽 E13.1 kiss: woman, woman, light skin tone, medium skin tone
+1F469 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FE ; fully-qualified # 👩🏻‍❤️‍💋‍👩🏾 E13.1 kiss: woman, woman, light skin tone, medium-dark skin tone
+1F469 1F3FB 200D 2764 200D 1F48B 200D 1F469 1F3FE ; minimally-qualified # 👩🏻‍❤‍💋‍👩🏾 E13.1 kiss: woman, woman, light skin tone, medium-dark skin tone
+1F469 1F3FB 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FF ; fully-qualified # 👩🏻‍❤️‍💋‍👩🏿 E13.1 kiss: woman, woman, light skin tone, dark skin tone
+1F469 1F3FB 200D 2764 200D 1F48B 200D 1F469 1F3FF ; minimally-qualified # 👩🏻‍❤‍💋‍👩🏿 E13.1 kiss: woman, woman, light skin tone, dark skin tone
+1F469 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FB ; fully-qualified # 👩🏼‍❤️‍💋‍👩🏻 E13.1 kiss: woman, woman, medium-light skin tone, light skin tone
+1F469 1F3FC 200D 2764 200D 1F48B 200D 1F469 1F3FB ; minimally-qualified # 👩🏼‍❤‍💋‍👩🏻 E13.1 kiss: woman, woman, medium-light skin tone, light skin tone
+1F469 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FC ; fully-qualified # 👩🏼‍❤️‍💋‍👩🏼 E13.1 kiss: woman, woman, medium-light skin tone
+1F469 1F3FC 200D 2764 200D 1F48B 200D 1F469 1F3FC ; minimally-qualified # 👩🏼‍❤‍💋‍👩🏼 E13.1 kiss: woman, woman, medium-light skin tone
+1F469 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FD ; fully-qualified # 👩🏼‍❤️‍💋‍👩🏽 E13.1 kiss: woman, woman, medium-light skin tone, medium skin tone
+1F469 1F3FC 200D 2764 200D 1F48B 200D 1F469 1F3FD ; minimally-qualified # 👩🏼‍❤‍💋‍👩🏽 E13.1 kiss: woman, woman, medium-light skin tone, medium skin tone
+1F469 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FE ; fully-qualified # 👩🏼‍❤️‍💋‍👩🏾 E13.1 kiss: woman, woman, medium-light skin tone, medium-dark skin tone
+1F469 1F3FC 200D 2764 200D 1F48B 200D 1F469 1F3FE ; minimally-qualified # 👩🏼‍❤‍💋‍👩🏾 E13.1 kiss: woman, woman, medium-light skin tone, medium-dark skin tone
+1F469 1F3FC 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FF ; fully-qualified # 👩🏼‍❤️‍💋‍👩🏿 E13.1 kiss: woman, woman, medium-light skin tone, dark skin tone
+1F469 1F3FC 200D 2764 200D 1F48B 200D 1F469 1F3FF ; minimally-qualified # 👩🏼‍❤‍💋‍👩🏿 E13.1 kiss: woman, woman, medium-light skin tone, dark skin tone
+1F469 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FB ; fully-qualified # 👩🏽‍❤️‍💋‍👩🏻 E13.1 kiss: woman, woman, medium skin tone, light skin tone
+1F469 1F3FD 200D 2764 200D 1F48B 200D 1F469 1F3FB ; minimally-qualified # 👩🏽‍❤‍💋‍👩🏻 E13.1 kiss: woman, woman, medium skin tone, light skin tone
+1F469 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FC ; fully-qualified # 👩🏽‍❤️‍💋‍👩🏼 E13.1 kiss: woman, woman, medium skin tone, medium-light skin tone
+1F469 1F3FD 200D 2764 200D 1F48B 200D 1F469 1F3FC ; minimally-qualified # 👩🏽‍❤‍💋‍👩🏼 E13.1 kiss: woman, woman, medium skin tone, medium-light skin tone
+1F469 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FD ; fully-qualified # 👩🏽‍❤️‍💋‍👩🏽 E13.1 kiss: woman, woman, medium skin tone
+1F469 1F3FD 200D 2764 200D 1F48B 200D 1F469 1F3FD ; minimally-qualified # 👩🏽‍❤‍💋‍👩🏽 E13.1 kiss: woman, woman, medium skin tone
+1F469 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FE ; fully-qualified # 👩🏽‍❤️‍💋‍👩🏾 E13.1 kiss: woman, woman, medium skin tone, medium-dark skin tone
+1F469 1F3FD 200D 2764 200D 1F48B 200D 1F469 1F3FE ; minimally-qualified # 👩🏽‍❤‍💋‍👩🏾 E13.1 kiss: woman, woman, medium skin tone, medium-dark skin tone
+1F469 1F3FD 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FF ; fully-qualified # 👩🏽‍❤️‍💋‍👩🏿 E13.1 kiss: woman, woman, medium skin tone, dark skin tone
+1F469 1F3FD 200D 2764 200D 1F48B 200D 1F469 1F3FF ; minimally-qualified # 👩🏽‍❤‍💋‍👩🏿 E13.1 kiss: woman, woman, medium skin tone, dark skin tone
+1F469 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FB ; fully-qualified # 👩🏾‍❤️‍💋‍👩🏻 E13.1 kiss: woman, woman, medium-dark skin tone, light skin tone
+1F469 1F3FE 200D 2764 200D 1F48B 200D 1F469 1F3FB ; minimally-qualified # 👩🏾‍❤‍💋‍👩🏻 E13.1 kiss: woman, woman, medium-dark skin tone, light skin tone
+1F469 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FC ; fully-qualified # 👩🏾‍❤️‍💋‍👩🏼 E13.1 kiss: woman, woman, medium-dark skin tone, medium-light skin tone
+1F469 1F3FE 200D 2764 200D 1F48B 200D 1F469 1F3FC ; minimally-qualified # 👩🏾‍❤‍💋‍👩🏼 E13.1 kiss: woman, woman, medium-dark skin tone, medium-light skin tone
+1F469 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FD ; fully-qualified # 👩🏾‍❤️‍💋‍👩🏽 E13.1 kiss: woman, woman, medium-dark skin tone, medium skin tone
+1F469 1F3FE 200D 2764 200D 1F48B 200D 1F469 1F3FD ; minimally-qualified # 👩🏾‍❤‍💋‍👩🏽 E13.1 kiss: woman, woman, medium-dark skin tone, medium skin tone
+1F469 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FE ; fully-qualified # 👩🏾‍❤️‍💋‍👩🏾 E13.1 kiss: woman, woman, medium-dark skin tone
+1F469 1F3FE 200D 2764 200D 1F48B 200D 1F469 1F3FE ; minimally-qualified # 👩🏾‍❤‍💋‍👩🏾 E13.1 kiss: woman, woman, medium-dark skin tone
+1F469 1F3FE 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FF ; fully-qualified # 👩🏾‍❤️‍💋‍👩🏿 E13.1 kiss: woman, woman, medium-dark skin tone, dark skin tone
+1F469 1F3FE 200D 2764 200D 1F48B 200D 1F469 1F3FF ; minimally-qualified # 👩🏾‍❤‍💋‍👩🏿 E13.1 kiss: woman, woman, medium-dark skin tone, dark skin tone
+1F469 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FB ; fully-qualified # 👩🏿‍❤️‍💋‍👩🏻 E13.1 kiss: woman, woman, dark skin tone, light skin tone
+1F469 1F3FF 200D 2764 200D 1F48B 200D 1F469 1F3FB ; minimally-qualified # 👩🏿‍❤‍💋‍👩🏻 E13.1 kiss: woman, woman, dark skin tone, light skin tone
+1F469 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FC ; fully-qualified # 👩🏿‍❤️‍💋‍👩🏼 E13.1 kiss: woman, woman, dark skin tone, medium-light skin tone
+1F469 1F3FF 200D 2764 200D 1F48B 200D 1F469 1F3FC ; minimally-qualified # 👩🏿‍❤‍💋‍👩🏼 E13.1 kiss: woman, woman, dark skin tone, medium-light skin tone
+1F469 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FD ; fully-qualified # 👩🏿‍❤️‍💋‍👩🏽 E13.1 kiss: woman, woman, dark skin tone, medium skin tone
+1F469 1F3FF 200D 2764 200D 1F48B 200D 1F469 1F3FD ; minimally-qualified # 👩🏿‍❤‍💋‍👩🏽 E13.1 kiss: woman, woman, dark skin tone, medium skin tone
+1F469 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FE ; fully-qualified # 👩🏿‍❤️‍💋‍👩🏾 E13.1 kiss: woman, woman, dark skin tone, medium-dark skin tone
+1F469 1F3FF 200D 2764 200D 1F48B 200D 1F469 1F3FE ; minimally-qualified # 👩🏿‍❤‍💋‍👩🏾 E13.1 kiss: woman, woman, dark skin tone, medium-dark skin tone
+1F469 1F3FF 200D 2764 FE0F 200D 1F48B 200D 1F469 1F3FF ; fully-qualified # 👩🏿‍❤️‍💋‍👩🏿 E13.1 kiss: woman, woman, dark skin tone
+1F469 1F3FF 200D 2764 200D 1F48B 200D 1F469 1F3FF ; minimally-qualified # 👩🏿‍❤‍💋‍👩🏿 E13.1 kiss: woman, woman, dark skin tone
+1F491 ; fully-qualified # 💑 E0.6 couple with heart
+1F491 1F3FB ; fully-qualified # 💑🏻 E13.1 couple with heart: light skin tone
+1F491 1F3FC ; fully-qualified # 💑🏼 E13.1 couple with heart: medium-light skin tone
+1F491 1F3FD ; fully-qualified # 💑🏽 E13.1 couple with heart: medium skin tone
+1F491 1F3FE ; fully-qualified # 💑🏾 E13.1 couple with heart: medium-dark skin tone
+1F491 1F3FF ; fully-qualified # 💑🏿 E13.1 couple with heart: dark skin tone
+1F9D1 1F3FB 200D 2764 FE0F 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏻‍❤️‍🧑🏼 E13.1 couple with heart: person, person, light skin tone, medium-light skin tone
+1F9D1 1F3FB 200D 2764 200D 1F9D1 1F3FC ; minimally-qualified # 🧑🏻‍❤‍🧑🏼 E13.1 couple with heart: person, person, light skin tone, medium-light skin tone
+1F9D1 1F3FB 200D 2764 FE0F 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏻‍❤️‍🧑🏽 E13.1 couple with heart: person, person, light skin tone, medium skin tone
+1F9D1 1F3FB 200D 2764 200D 1F9D1 1F3FD ; minimally-qualified # 🧑🏻‍❤‍🧑🏽 E13.1 couple with heart: person, person, light skin tone, medium skin tone
+1F9D1 1F3FB 200D 2764 FE0F 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏻‍❤️‍🧑🏾 E13.1 couple with heart: person, person, light skin tone, medium-dark skin tone
+1F9D1 1F3FB 200D 2764 200D 1F9D1 1F3FE ; minimally-qualified # 🧑🏻‍❤‍🧑🏾 E13.1 couple with heart: person, person, light skin tone, medium-dark skin tone
+1F9D1 1F3FB 200D 2764 FE0F 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏻‍❤️‍🧑🏿 E13.1 couple with heart: person, person, light skin tone, dark skin tone
+1F9D1 1F3FB 200D 2764 200D 1F9D1 1F3FF ; minimally-qualified # 🧑🏻‍❤‍🧑🏿 E13.1 couple with heart: person, person, light skin tone, dark skin tone
+1F9D1 1F3FC 200D 2764 FE0F 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏼‍❤️‍🧑🏻 E13.1 couple with heart: person, person, medium-light skin tone, light skin tone
+1F9D1 1F3FC 200D 2764 200D 1F9D1 1F3FB ; minimally-qualified # 🧑🏼‍❤‍🧑🏻 E13.1 couple with heart: person, person, medium-light skin tone, light skin tone
+1F9D1 1F3FC 200D 2764 FE0F 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏼‍❤️‍🧑🏽 E13.1 couple with heart: person, person, medium-light skin tone, medium skin tone
+1F9D1 1F3FC 200D 2764 200D 1F9D1 1F3FD ; minimally-qualified # 🧑🏼‍❤‍🧑🏽 E13.1 couple with heart: person, person, medium-light skin tone, medium skin tone
+1F9D1 1F3FC 200D 2764 FE0F 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏼‍❤️‍🧑🏾 E13.1 couple with heart: person, person, medium-light skin tone, medium-dark skin tone
+1F9D1 1F3FC 200D 2764 200D 1F9D1 1F3FE ; minimally-qualified # 🧑🏼‍❤‍🧑🏾 E13.1 couple with heart: person, person, medium-light skin tone, medium-dark skin tone
+1F9D1 1F3FC 200D 2764 FE0F 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏼‍❤️‍🧑🏿 E13.1 couple with heart: person, person, medium-light skin tone, dark skin tone
+1F9D1 1F3FC 200D 2764 200D 1F9D1 1F3FF ; minimally-qualified # 🧑🏼‍❤‍🧑🏿 E13.1 couple with heart: person, person, medium-light skin tone, dark skin tone
+1F9D1 1F3FD 200D 2764 FE0F 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏽‍❤️‍🧑🏻 E13.1 couple with heart: person, person, medium skin tone, light skin tone
+1F9D1 1F3FD 200D 2764 200D 1F9D1 1F3FB ; minimally-qualified # 🧑🏽‍❤‍🧑🏻 E13.1 couple with heart: person, person, medium skin tone, light skin tone
+1F9D1 1F3FD 200D 2764 FE0F 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏽‍❤️‍🧑🏼 E13.1 couple with heart: person, person, medium skin tone, medium-light skin tone
+1F9D1 1F3FD 200D 2764 200D 1F9D1 1F3FC ; minimally-qualified # 🧑🏽‍❤‍🧑🏼 E13.1 couple with heart: person, person, medium skin tone, medium-light skin tone
+1F9D1 1F3FD 200D 2764 FE0F 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏽‍❤️‍🧑🏾 E13.1 couple with heart: person, person, medium skin tone, medium-dark skin tone
+1F9D1 1F3FD 200D 2764 200D 1F9D1 1F3FE ; minimally-qualified # 🧑🏽‍❤‍🧑🏾 E13.1 couple with heart: person, person, medium skin tone, medium-dark skin tone
+1F9D1 1F3FD 200D 2764 FE0F 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏽‍❤️‍🧑🏿 E13.1 couple with heart: person, person, medium skin tone, dark skin tone
+1F9D1 1F3FD 200D 2764 200D 1F9D1 1F3FF ; minimally-qualified # 🧑🏽‍❤‍🧑🏿 E13.1 couple with heart: person, person, medium skin tone, dark skin tone
+1F9D1 1F3FE 200D 2764 FE0F 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏾‍❤️‍🧑🏻 E13.1 couple with heart: person, person, medium-dark skin tone, light skin tone
+1F9D1 1F3FE 200D 2764 200D 1F9D1 1F3FB ; minimally-qualified # 🧑🏾‍❤‍🧑🏻 E13.1 couple with heart: person, person, medium-dark skin tone, light skin tone
+1F9D1 1F3FE 200D 2764 FE0F 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏾‍❤️‍🧑🏼 E13.1 couple with heart: person, person, medium-dark skin tone, medium-light skin tone
+1F9D1 1F3FE 200D 2764 200D 1F9D1 1F3FC ; minimally-qualified # 🧑🏾‍❤‍🧑🏼 E13.1 couple with heart: person, person, medium-dark skin tone, medium-light skin tone
+1F9D1 1F3FE 200D 2764 FE0F 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏾‍❤️‍🧑🏽 E13.1 couple with heart: person, person, medium-dark skin tone, medium skin tone
+1F9D1 1F3FE 200D 2764 200D 1F9D1 1F3FD ; minimally-qualified # 🧑🏾‍❤‍🧑🏽 E13.1 couple with heart: person, person, medium-dark skin tone, medium skin tone
+1F9D1 1F3FE 200D 2764 FE0F 200D 1F9D1 1F3FF ; fully-qualified # 🧑🏾‍❤️‍🧑🏿 E13.1 couple with heart: person, person, medium-dark skin tone, dark skin tone
+1F9D1 1F3FE 200D 2764 200D 1F9D1 1F3FF ; minimally-qualified # 🧑🏾‍❤‍🧑🏿 E13.1 couple with heart: person, person, medium-dark skin tone, dark skin tone
+1F9D1 1F3FF 200D 2764 FE0F 200D 1F9D1 1F3FB ; fully-qualified # 🧑🏿‍❤️‍🧑🏻 E13.1 couple with heart: person, person, dark skin tone, light skin tone
+1F9D1 1F3FF 200D 2764 200D 1F9D1 1F3FB ; minimally-qualified # 🧑🏿‍❤‍🧑🏻 E13.1 couple with heart: person, person, dark skin tone, light skin tone
+1F9D1 1F3FF 200D 2764 FE0F 200D 1F9D1 1F3FC ; fully-qualified # 🧑🏿‍❤️‍🧑🏼 E13.1 couple with heart: person, person, dark skin tone, medium-light skin tone
+1F9D1 1F3FF 200D 2764 200D 1F9D1 1F3FC ; minimally-qualified # 🧑🏿‍❤‍🧑🏼 E13.1 couple with heart: person, person, dark skin tone, medium-light skin tone
+1F9D1 1F3FF 200D 2764 FE0F 200D 1F9D1 1F3FD ; fully-qualified # 🧑🏿‍❤️‍🧑🏽 E13.1 couple with heart: person, person, dark skin tone, medium skin tone
+1F9D1 1F3FF 200D 2764 200D 1F9D1 1F3FD ; minimally-qualified # 🧑🏿‍❤‍🧑🏽 E13.1 couple with heart: person, person, dark skin tone, medium skin tone
+1F9D1 1F3FF 200D 2764 FE0F 200D 1F9D1 1F3FE ; fully-qualified # 🧑🏿‍❤️‍🧑🏾 E13.1 couple with heart: person, person, dark skin tone, medium-dark skin tone
+1F9D1 1F3FF 200D 2764 200D 1F9D1 1F3FE ; minimally-qualified # 🧑🏿‍❤‍🧑🏾 E13.1 couple with heart: person, person, dark skin tone, medium-dark skin tone
+1F469 200D 2764 FE0F 200D 1F468 ; fully-qualified # 👩‍❤️‍👨 E2.0 couple with heart: woman, man
+1F469 200D 2764 200D 1F468 ; minimally-qualified # 👩‍❤‍👨 E2.0 couple with heart: woman, man
+1F469 1F3FB 200D 2764 FE0F 200D 1F468 1F3FB ; fully-qualified # 👩🏻‍❤️‍👨🏻 E13.1 couple with heart: woman, man, light skin tone
+1F469 1F3FB 200D 2764 200D 1F468 1F3FB ; minimally-qualified # 👩🏻‍❤‍👨🏻 E13.1 couple with heart: woman, man, light skin tone
+1F469 1F3FB 200D 2764 FE0F 200D 1F468 1F3FC ; fully-qualified # 👩🏻‍❤️‍👨🏼 E13.1 couple with heart: woman, man, light skin tone, medium-light skin tone
+1F469 1F3FB 200D 2764 200D 1F468 1F3FC ; minimally-qualified # 👩🏻‍❤‍👨🏼 E13.1 couple with heart: woman, man, light skin tone, medium-light skin tone
+1F469 1F3FB 200D 2764 FE0F 200D 1F468 1F3FD ; fully-qualified # 👩🏻‍❤️‍👨🏽 E13.1 couple with heart: woman, man, light skin tone, medium skin tone
+1F469 1F3FB 200D 2764 200D 1F468 1F3FD ; minimally-qualified # 👩🏻‍❤‍👨🏽 E13.1 couple with heart: woman, man, light skin tone, medium skin tone
+1F469 1F3FB 200D 2764 FE0F 200D 1F468 1F3FE ; fully-qualified # 👩🏻‍❤️‍👨🏾 E13.1 couple with heart: woman, man, light skin tone, medium-dark skin tone
+1F469 1F3FB 200D 2764 200D 1F468 1F3FE ; minimally-qualified # 👩🏻‍❤‍👨🏾 E13.1 couple with heart: woman, man, light skin tone, medium-dark skin tone
+1F469 1F3FB 200D 2764 FE0F 200D 1F468 1F3FF ; fully-qualified # 👩🏻‍❤️‍👨🏿 E13.1 couple with heart: woman, man, light skin tone, dark skin tone
+1F469 1F3FB 200D 2764 200D 1F468 1F3FF ; minimally-qualified # 👩🏻‍❤‍👨🏿 E13.1 couple with heart: woman, man, light skin tone, dark skin tone
+1F469 1F3FC 200D 2764 FE0F 200D 1F468 1F3FB ; fully-qualified # 👩🏼‍❤️‍👨🏻 E13.1 couple with heart: woman, man, medium-light skin tone, light skin tone
+1F469 1F3FC 200D 2764 200D 1F468 1F3FB ; minimally-qualified # 👩🏼‍❤‍👨🏻 E13.1 couple with heart: woman, man, medium-light skin tone, light skin tone
+1F469 1F3FC 200D 2764 FE0F 200D 1F468 1F3FC ; fully-qualified # 👩🏼‍❤️‍👨🏼 E13.1 couple with heart: woman, man, medium-light skin tone
+1F469 1F3FC 200D 2764 200D 1F468 1F3FC ; minimally-qualified # 👩🏼‍❤‍👨🏼 E13.1 couple with heart: woman, man, medium-light skin tone
+1F469 1F3FC 200D 2764 FE0F 200D 1F468 1F3FD ; fully-qualified # 👩🏼‍❤️‍👨🏽 E13.1 couple with heart: woman, man, medium-light skin tone, medium skin tone
+1F469 1F3FC 200D 2764 200D 1F468 1F3FD ; minimally-qualified # 👩🏼‍❤‍👨🏽 E13.1 couple with heart: woman, man, medium-light skin tone, medium skin tone
+1F469 1F3FC 200D 2764 FE0F 200D 1F468 1F3FE ; fully-qualified # 👩🏼‍❤️‍👨🏾 E13.1 couple with heart: woman, man, medium-light skin tone, medium-dark skin tone
+1F469 1F3FC 200D 2764 200D 1F468 1F3FE ; minimally-qualified # 👩🏼‍❤‍👨🏾 E13.1 couple with heart: woman, man, medium-light skin tone, medium-dark skin tone
+1F469 1F3FC 200D 2764 FE0F 200D 1F468 1F3FF ; fully-qualified # 👩🏼‍❤️‍👨🏿 E13.1 couple with heart: woman, man, medium-light skin tone, dark skin tone
+1F469 1F3FC 200D 2764 200D 1F468 1F3FF ; minimally-qualified # 👩🏼‍❤‍👨🏿 E13.1 couple with heart: woman, man, medium-light skin tone, dark skin tone
+1F469 1F3FD 200D 2764 FE0F 200D 1F468 1F3FB ; fully-qualified # 👩🏽‍❤️‍👨🏻 E13.1 couple with heart: woman, man, medium skin tone, light skin tone
+1F469 1F3FD 200D 2764 200D 1F468 1F3FB ; minimally-qualified # 👩🏽‍❤‍👨🏻 E13.1 couple with heart: woman, man, medium skin tone, light skin tone
+1F469 1F3FD 200D 2764 FE0F 200D 1F468 1F3FC ; fully-qualified # 👩🏽‍❤️‍👨🏼 E13.1 couple with heart: woman, man, medium skin tone, medium-light skin tone
+1F469 1F3FD 200D 2764 200D 1F468 1F3FC ; minimally-qualified # 👩🏽‍❤‍👨🏼 E13.1 couple with heart: woman, man, medium skin tone, medium-light skin tone
+1F469 1F3FD 200D 2764 FE0F 200D 1F468 1F3FD ; fully-qualified # 👩🏽‍❤️‍👨🏽 E13.1 couple with heart: woman, man, medium skin tone
+1F469 1F3FD 200D 2764 200D 1F468 1F3FD ; minimally-qualified # 👩🏽‍❤‍👨🏽 E13.1 couple with heart: woman, man, medium skin tone
+1F469 1F3FD 200D 2764 FE0F 200D 1F468 1F3FE ; fully-qualified # 👩🏽‍❤️‍👨🏾 E13.1 couple with heart: woman, man, medium skin tone, medium-dark skin tone
+1F469 1F3FD 200D 2764 200D 1F468 1F3FE ; minimally-qualified # 👩🏽‍❤‍👨🏾 E13.1 couple with heart: woman, man, medium skin tone, medium-dark skin tone
+1F469 1F3FD 200D 2764 FE0F 200D 1F468 1F3FF ; fully-qualified # 👩🏽‍❤️‍👨🏿 E13.1 couple with heart: woman, man, medium skin tone, dark skin tone
+1F469 1F3FD 200D 2764 200D 1F468 1F3FF ; minimally-qualified # 👩🏽‍❤‍👨🏿 E13.1 couple with heart: woman, man, medium skin tone, dark skin tone
+1F469 1F3FE 200D 2764 FE0F 200D 1F468 1F3FB ; fully-qualified # 👩🏾‍❤️‍👨🏻 E13.1 couple with heart: woman, man, medium-dark skin tone, light skin tone
+1F469 1F3FE 200D 2764 200D 1F468 1F3FB ; minimally-qualified # 👩🏾‍❤‍👨🏻 E13.1 couple with heart: woman, man, medium-dark skin tone, light skin tone
+1F469 1F3FE 200D 2764 FE0F 200D 1F468 1F3FC ; fully-qualified # 👩🏾‍❤️‍👨🏼 E13.1 couple with heart: woman, man, medium-dark skin tone, medium-light skin tone
+1F469 1F3FE 200D 2764 200D 1F468 1F3FC ; minimally-qualified # 👩🏾‍❤‍👨🏼 E13.1 couple with heart: woman, man, medium-dark skin tone, medium-light skin tone
+1F469 1F3FE 200D 2764 FE0F 200D 1F468 1F3FD ; fully-qualified # 👩🏾‍❤️‍👨🏽 E13.1 couple with heart: woman, man, medium-dark skin tone, medium skin tone
+1F469 1F3FE 200D 2764 200D 1F468 1F3FD ; minimally-qualified # 👩🏾‍❤‍👨🏽 E13.1 couple with heart: woman, man, medium-dark skin tone, medium skin tone
+1F469 1F3FE 200D 2764 FE0F 200D 1F468 1F3FE ; fully-qualified # 👩🏾‍❤️‍👨🏾 E13.1 couple with heart: woman, man, medium-dark skin tone
+1F469 1F3FE 200D 2764 200D 1F468 1F3FE ; minimally-qualified # 👩🏾‍❤‍👨🏾 E13.1 couple with heart: woman, man, medium-dark skin tone
+1F469 1F3FE 200D 2764 FE0F 200D 1F468 1F3FF ; fully-qualified # 👩🏾‍❤️‍👨🏿 E13.1 couple with heart: woman, man, medium-dark skin tone, dark skin tone
+1F469 1F3FE 200D 2764 200D 1F468 1F3FF ; minimally-qualified # 👩🏾‍❤‍👨🏿 E13.1 couple with heart: woman, man, medium-dark skin tone, dark skin tone
+1F469 1F3FF 200D 2764 FE0F 200D 1F468 1F3FB ; fully-qualified # 👩🏿‍❤️‍👨🏻 E13.1 couple with heart: woman, man, dark skin tone, light skin tone
+1F469 1F3FF 200D 2764 200D 1F468 1F3FB ; minimally-qualified # 👩🏿‍❤‍👨🏻 E13.1 couple with heart: woman, man, dark skin tone, light skin tone
+1F469 1F3FF 200D 2764 FE0F 200D 1F468 1F3FC ; fully-qualified # 👩🏿‍❤️‍👨🏼 E13.1 couple with heart: woman, man, dark skin tone, medium-light skin tone
+1F469 1F3FF 200D 2764 200D 1F468 1F3FC ; minimally-qualified # 👩🏿‍❤‍👨🏼 E13.1 couple with heart: woman, man, dark skin tone, medium-light skin tone
+1F469 1F3FF 200D 2764 FE0F 200D 1F468 1F3FD ; fully-qualified # 👩🏿‍❤️‍👨🏽 E13.1 couple with heart: woman, man, dark skin tone, medium skin tone
+1F469 1F3FF 200D 2764 200D 1F468 1F3FD ; minimally-qualified # 👩🏿‍❤‍👨🏽 E13.1 couple with heart: woman, man, dark skin tone, medium skin tone
+1F469 1F3FF 200D 2764 FE0F 200D 1F468 1F3FE ; fully-qualified # 👩🏿‍❤️‍👨🏾 E13.1 couple with heart: woman, man, dark skin tone, medium-dark skin tone
+1F469 1F3FF 200D 2764 200D 1F468 1F3FE ; minimally-qualified # 👩🏿‍❤‍👨🏾 E13.1 couple with heart: woman, man, dark skin tone, medium-dark skin tone
+1F469 1F3FF 200D 2764 FE0F 200D 1F468 1F3FF ; fully-qualified # 👩🏿‍❤️‍👨🏿 E13.1 couple with heart: woman, man, dark skin tone
+1F469 1F3FF 200D 2764 200D 1F468 1F3FF ; minimally-qualified # 👩🏿‍❤‍👨🏿 E13.1 couple with heart: woman, man, dark skin tone
+1F468 200D 2764 FE0F 200D 1F468 ; fully-qualified # 👨‍❤️‍👨 E2.0 couple with heart: man, man
+1F468 200D 2764 200D 1F468 ; minimally-qualified # 👨‍❤‍👨 E2.0 couple with heart: man, man
+1F468 1F3FB 200D 2764 FE0F 200D 1F468 1F3FB ; fully-qualified # 👨🏻‍❤️‍👨🏻 E13.1 couple with heart: man, man, light skin tone
+1F468 1F3FB 200D 2764 200D 1F468 1F3FB ; minimally-qualified # 👨🏻‍❤‍👨🏻 E13.1 couple with heart: man, man, light skin tone
+1F468 1F3FB 200D 2764 FE0F 200D 1F468 1F3FC ; fully-qualified # 👨🏻‍❤️‍👨🏼 E13.1 couple with heart: man, man, light skin tone, medium-light skin tone
+1F468 1F3FB 200D 2764 200D 1F468 1F3FC ; minimally-qualified # 👨🏻‍❤‍👨🏼 E13.1 couple with heart: man, man, light skin tone, medium-light skin tone
+1F468 1F3FB 200D 2764 FE0F 200D 1F468 1F3FD ; fully-qualified # 👨🏻‍❤️‍👨🏽 E13.1 couple with heart: man, man, light skin tone, medium skin tone
+1F468 1F3FB 200D 2764 200D 1F468 1F3FD ; minimally-qualified # 👨🏻‍❤‍👨🏽 E13.1 couple with heart: man, man, light skin tone, medium skin tone
+1F468 1F3FB 200D 2764 FE0F 200D 1F468 1F3FE ; fully-qualified # 👨🏻‍❤️‍👨🏾 E13.1 couple with heart: man, man, light skin tone, medium-dark skin tone
+1F468 1F3FB 200D 2764 200D 1F468 1F3FE ; minimally-qualified # 👨🏻‍❤‍👨🏾 E13.1 couple with heart: man, man, light skin tone, medium-dark skin tone
+1F468 1F3FB 200D 2764 FE0F 200D 1F468 1F3FF ; fully-qualified # 👨🏻‍❤️‍👨🏿 E13.1 couple with heart: man, man, light skin tone, dark skin tone
+1F468 1F3FB 200D 2764 200D 1F468 1F3FF ; minimally-qualified # 👨🏻‍❤‍👨🏿 E13.1 couple with heart: man, man, light skin tone, dark skin tone
+1F468 1F3FC 200D 2764 FE0F 200D 1F468 1F3FB ; fully-qualified # 👨🏼‍❤️‍👨🏻 E13.1 couple with heart: man, man, medium-light skin tone, light skin tone
+1F468 1F3FC 200D 2764 200D 1F468 1F3FB ; minimally-qualified # 👨🏼‍❤‍👨🏻 E13.1 couple with heart: man, man, medium-light skin tone, light skin tone
+1F468 1F3FC 200D 2764 FE0F 200D 1F468 1F3FC ; fully-qualified # 👨🏼‍❤️‍👨🏼 E13.1 couple with heart: man, man, medium-light skin tone
+1F468 1F3FC 200D 2764 200D 1F468 1F3FC ; minimally-qualified # 👨🏼‍❤‍👨🏼 E13.1 couple with heart: man, man, medium-light skin tone
+1F468 1F3FC 200D 2764 FE0F 200D 1F468 1F3FD ; fully-qualified # 👨🏼‍❤️‍👨🏽 E13.1 couple with heart: man, man, medium-light skin tone, medium skin tone
+1F468 1F3FC 200D 2764 200D 1F468 1F3FD ; minimally-qualified # 👨🏼‍❤‍👨🏽 E13.1 couple with heart: man, man, medium-light skin tone, medium skin tone
+1F468 1F3FC 200D 2764 FE0F 200D 1F468 1F3FE ; fully-qualified # 👨🏼‍❤️‍👨🏾 E13.1 couple with heart: man, man, medium-light skin tone, medium-dark skin tone
+1F468 1F3FC 200D 2764 200D 1F468 1F3FE ; minimally-qualified # 👨🏼‍❤‍👨🏾 E13.1 couple with heart: man, man, medium-light skin tone, medium-dark skin tone
+1F468 1F3FC 200D 2764 FE0F 200D 1F468 1F3FF ; fully-qualified # 👨🏼‍❤️‍👨🏿 E13.1 couple with heart: man, man, medium-light skin tone, dark skin tone
+1F468 1F3FC 200D 2764 200D 1F468 1F3FF ; minimally-qualified # 👨🏼‍❤‍👨🏿 E13.1 couple with heart: man, man, medium-light skin tone, dark skin tone
+1F468 1F3FD 200D 2764 FE0F 200D 1F468 1F3FB ; fully-qualified # 👨🏽‍❤️‍👨🏻 E13.1 couple with heart: man, man, medium skin tone, light skin tone
+1F468 1F3FD 200D 2764 200D 1F468 1F3FB ; minimally-qualified # 👨🏽‍❤‍👨🏻 E13.1 couple with heart: man, man, medium skin tone, light skin tone
+1F468 1F3FD 200D 2764 FE0F 200D 1F468 1F3FC ; fully-qualified # 👨🏽‍❤️‍👨🏼 E13.1 couple with heart: man, man, medium skin tone, medium-light skin tone
+1F468 1F3FD 200D 2764 200D 1F468 1F3FC ; minimally-qualified # 👨🏽‍❤‍👨🏼 E13.1 couple with heart: man, man, medium skin tone, medium-light skin tone
+1F468 1F3FD 200D 2764 FE0F 200D 1F468 1F3FD ; fully-qualified # 👨🏽‍❤️‍👨🏽 E13.1 couple with heart: man, man, medium skin tone
+1F468 1F3FD 200D 2764 200D 1F468 1F3FD ; minimally-qualified # 👨🏽‍❤‍👨🏽 E13.1 couple with heart: man, man, medium skin tone
+1F468 1F3FD 200D 2764 FE0F 200D 1F468 1F3FE ; fully-qualified # 👨🏽‍❤️‍👨🏾 E13.1 couple with heart: man, man, medium skin tone, medium-dark skin tone
+1F468 1F3FD 200D 2764 200D 1F468 1F3FE ; minimally-qualified # 👨🏽‍❤‍👨🏾 E13.1 couple with heart: man, man, medium skin tone, medium-dark skin tone
+1F468 1F3FD 200D 2764 FE0F 200D 1F468 1F3FF ; fully-qualified # 👨🏽‍❤️‍👨🏿 E13.1 couple with heart: man, man, medium skin tone, dark skin tone
+1F468 1F3FD 200D 2764 200D 1F468 1F3FF ; minimally-qualified # 👨🏽‍❤‍👨🏿 E13.1 couple with heart: man, man, medium skin tone, dark skin tone
+1F468 1F3FE 200D 2764 FE0F 200D 1F468 1F3FB ; fully-qualified # 👨🏾‍❤️‍👨🏻 E13.1 couple with heart: man, man, medium-dark skin tone, light skin tone
+1F468 1F3FE 200D 2764 200D 1F468 1F3FB ; minimally-qualified # 👨🏾‍❤‍👨🏻 E13.1 couple with heart: man, man, medium-dark skin tone, light skin tone
+1F468 1F3FE 200D 2764 FE0F 200D 1F468 1F3FC ; fully-qualified # 👨🏾‍❤️‍👨🏼 E13.1 couple with heart: man, man, medium-dark skin tone, medium-light skin tone
+1F468 1F3FE 200D 2764 200D 1F468 1F3FC ; minimally-qualified # 👨🏾‍❤‍👨🏼 E13.1 couple with heart: man, man, medium-dark skin tone, medium-light skin tone
+1F468 1F3FE 200D 2764 FE0F 200D 1F468 1F3FD ; fully-qualified # 👨🏾‍❤️‍👨🏽 E13.1 couple with heart: man, man, medium-dark skin tone, medium skin tone
+1F468 1F3FE 200D 2764 200D 1F468 1F3FD ; minimally-qualified # 👨🏾‍❤‍👨🏽 E13.1 couple with heart: man, man, medium-dark skin tone, medium skin tone
+1F468 1F3FE 200D 2764 FE0F 200D 1F468 1F3FE ; fully-qualified # 👨🏾‍❤️‍👨🏾 E13.1 couple with heart: man, man, medium-dark skin tone
+1F468 1F3FE 200D 2764 200D 1F468 1F3FE ; minimally-qualified # 👨🏾‍❤‍👨🏾 E13.1 couple with heart: man, man, medium-dark skin tone
+1F468 1F3FE 200D 2764 FE0F 200D 1F468 1F3FF ; fully-qualified # 👨🏾‍❤️‍👨🏿 E13.1 couple with heart: man, man, medium-dark skin tone, dark skin tone
+1F468 1F3FE 200D 2764 200D 1F468 1F3FF ; minimally-qualified # 👨🏾‍❤‍👨🏿 E13.1 couple with heart: man, man, medium-dark skin tone, dark skin tone
+1F468 1F3FF 200D 2764 FE0F 200D 1F468 1F3FB ; fully-qualified # 👨🏿‍❤️‍👨🏻 E13.1 couple with heart: man, man, dark skin tone, light skin tone
+1F468 1F3FF 200D 2764 200D 1F468 1F3FB ; minimally-qualified # 👨🏿‍❤‍👨🏻 E13.1 couple with heart: man, man, dark skin tone, light skin tone
+1F468 1F3FF 200D 2764 FE0F 200D 1F468 1F3FC ; fully-qualified # 👨🏿‍❤️‍👨🏼 E13.1 couple with heart: man, man, dark skin tone, medium-light skin tone
+1F468 1F3FF 200D 2764 200D 1F468 1F3FC ; minimally-qualified # 👨🏿‍❤‍👨🏼 E13.1 couple with heart: man, man, dark skin tone, medium-light skin tone
+1F468 1F3FF 200D 2764 FE0F 200D 1F468 1F3FD ; fully-qualified # 👨🏿‍❤️‍👨🏽 E13.1 couple with heart: man, man, dark skin tone, medium skin tone
+1F468 1F3FF 200D 2764 200D 1F468 1F3FD ; minimally-qualified # 👨🏿‍❤‍👨🏽 E13.1 couple with heart: man, man, dark skin tone, medium skin tone
+1F468 1F3FF 200D 2764 FE0F 200D 1F468 1F3FE ; fully-qualified # 👨🏿‍❤️‍👨🏾 E13.1 couple with heart: man, man, dark skin tone, medium-dark skin tone
+1F468 1F3FF 200D 2764 200D 1F468 1F3FE ; minimally-qualified # 👨🏿‍❤‍👨🏾 E13.1 couple with heart: man, man, dark skin tone, medium-dark skin tone
+1F468 1F3FF 200D 2764 FE0F 200D 1F468 1F3FF ; fully-qualified # 👨🏿‍❤️‍👨🏿 E13.1 couple with heart: man, man, dark skin tone
+1F468 1F3FF 200D 2764 200D 1F468 1F3FF ; minimally-qualified # 👨🏿‍❤‍👨🏿 E13.1 couple with heart: man, man, dark skin tone
+1F469 200D 2764 FE0F 200D 1F469 ; fully-qualified # 👩‍❤️‍👩 E2.0 couple with heart: woman, woman
+1F469 200D 2764 200D 1F469 ; minimally-qualified # 👩‍❤‍👩 E2.0 couple with heart: woman, woman
+1F469 1F3FB 200D 2764 FE0F 200D 1F469 1F3FB ; fully-qualified # 👩🏻‍❤️‍👩🏻 E13.1 couple with heart: woman, woman, light skin tone
+1F469 1F3FB 200D 2764 200D 1F469 1F3FB ; minimally-qualified # 👩🏻‍❤‍👩🏻 E13.1 couple with heart: woman, woman, light skin tone
+1F469 1F3FB 200D 2764 FE0F 200D 1F469 1F3FC ; fully-qualified # 👩🏻‍❤️‍👩🏼 E13.1 couple with heart: woman, woman, light skin tone, medium-light skin tone
+1F469 1F3FB 200D 2764 200D 1F469 1F3FC ; minimally-qualified # 👩🏻‍❤‍👩🏼 E13.1 couple with heart: woman, woman, light skin tone, medium-light skin tone
+1F469 1F3FB 200D 2764 FE0F 200D 1F469 1F3FD ; fully-qualified # 👩🏻‍❤️‍👩🏽 E13.1 couple with heart: woman, woman, light skin tone, medium skin tone
+1F469 1F3FB 200D 2764 200D 1F469 1F3FD ; minimally-qualified # 👩🏻‍❤‍👩🏽 E13.1 couple with heart: woman, woman, light skin tone, medium skin tone
+1F469 1F3FB 200D 2764 FE0F 200D 1F469 1F3FE ; fully-qualified # 👩🏻‍❤️‍👩🏾 E13.1 couple with heart: woman, woman, light skin tone, medium-dark skin tone
+1F469 1F3FB 200D 2764 200D 1F469 1F3FE ; minimally-qualified # 👩🏻‍❤‍👩🏾 E13.1 couple with heart: woman, woman, light skin tone, medium-dark skin tone
+1F469 1F3FB 200D 2764 FE0F 200D 1F469 1F3FF ; fully-qualified # 👩🏻‍❤️‍👩🏿 E13.1 couple with heart: woman, woman, light skin tone, dark skin tone
+1F469 1F3FB 200D 2764 200D 1F469 1F3FF ; minimally-qualified # 👩🏻‍❤‍👩🏿 E13.1 couple with heart: woman, woman, light skin tone, dark skin tone
+1F469 1F3FC 200D 2764 FE0F 200D 1F469 1F3FB ; fully-qualified # 👩🏼‍❤️‍👩🏻 E13.1 couple with heart: woman, woman, medium-light skin tone, light skin tone
+1F469 1F3FC 200D 2764 200D 1F469 1F3FB ; minimally-qualified # 👩🏼‍❤‍👩🏻 E13.1 couple with heart: woman, woman, medium-light skin tone, light skin tone
+1F469 1F3FC 200D 2764 FE0F 200D 1F469 1F3FC ; fully-qualified # 👩🏼‍❤️‍👩🏼 E13.1 couple with heart: woman, woman, medium-light skin tone
+1F469 1F3FC 200D 2764 200D 1F469 1F3FC ; minimally-qualified # 👩🏼‍❤‍👩🏼 E13.1 couple with heart: woman, woman, medium-light skin tone
+1F469 1F3FC 200D 2764 FE0F 200D 1F469 1F3FD ; fully-qualified # 👩🏼‍❤️‍👩🏽 E13.1 couple with heart: woman, woman, medium-light skin tone, medium skin tone
+1F469 1F3FC 200D 2764 200D 1F469 1F3FD ; minimally-qualified # 👩🏼‍❤‍👩🏽 E13.1 couple with heart: woman, woman, medium-light skin tone, medium skin tone
+1F469 1F3FC 200D 2764 FE0F 200D 1F469 1F3FE ; fully-qualified # 👩🏼‍❤️‍👩🏾 E13.1 couple with heart: woman, woman, medium-light skin tone, medium-dark skin tone
+1F469 1F3FC 200D 2764 200D 1F469 1F3FE ; minimally-qualified # 👩🏼‍❤‍👩🏾 E13.1 couple with heart: woman, woman, medium-light skin tone, medium-dark skin tone
+1F469 1F3FC 200D 2764 FE0F 200D 1F469 1F3FF ; fully-qualified # 👩🏼‍❤️‍👩🏿 E13.1 couple with heart: woman, woman, medium-light skin tone, dark skin tone
+1F469 1F3FC 200D 2764 200D 1F469 1F3FF ; minimally-qualified # 👩🏼‍❤‍👩🏿 E13.1 couple with heart: woman, woman, medium-light skin tone, dark skin tone
+1F469 1F3FD 200D 2764 FE0F 200D 1F469 1F3FB ; fully-qualified # 👩🏽‍❤️‍👩🏻 E13.1 couple with heart: woman, woman, medium skin tone, light skin tone
+1F469 1F3FD 200D 2764 200D 1F469 1F3FB ; minimally-qualified # 👩🏽‍❤‍👩🏻 E13.1 couple with heart: woman, woman, medium skin tone, light skin tone
+1F469 1F3FD 200D 2764 FE0F 200D 1F469 1F3FC ; fully-qualified # 👩🏽‍❤️‍👩🏼 E13.1 couple with heart: woman, woman, medium skin tone, medium-light skin tone
+1F469 1F3FD 200D 2764 200D 1F469 1F3FC ; minimally-qualified # 👩🏽‍❤‍👩🏼 E13.1 couple with heart: woman, woman, medium skin tone, medium-light skin tone
+1F469 1F3FD 200D 2764 FE0F 200D 1F469 1F3FD ; fully-qualified # 👩🏽‍❤️‍👩🏽 E13.1 couple with heart: woman, woman, medium skin tone
+1F469 1F3FD 200D 2764 200D 1F469 1F3FD ; minimally-qualified # 👩🏽‍❤‍👩🏽 E13.1 couple with heart: woman, woman, medium skin tone
+1F469 1F3FD 200D 2764 FE0F 200D 1F469 1F3FE ; fully-qualified # 👩🏽‍❤️‍👩🏾 E13.1 couple with heart: woman, woman, medium skin tone, medium-dark skin tone
+1F469 1F3FD 200D 2764 200D 1F469 1F3FE ; minimally-qualified # 👩🏽‍❤‍👩🏾 E13.1 couple with heart: woman, woman, medium skin tone, medium-dark skin tone
+1F469 1F3FD 200D 2764 FE0F 200D 1F469 1F3FF ; fully-qualified # 👩🏽‍❤️‍👩🏿 E13.1 couple with heart: woman, woman, medium skin tone, dark skin tone
+1F469 1F3FD 200D 2764 200D 1F469 1F3FF ; minimally-qualified # 👩🏽‍❤‍👩🏿 E13.1 couple with heart: woman, woman, medium skin tone, dark skin tone
+1F469 1F3FE 200D 2764 FE0F 200D 1F469 1F3FB ; fully-qualified # 👩🏾‍❤️‍👩🏻 E13.1 couple with heart: woman, woman, medium-dark skin tone, light skin tone
+1F469 1F3FE 200D 2764 200D 1F469 1F3FB ; minimally-qualified # 👩🏾‍❤‍👩🏻 E13.1 couple with heart: woman, woman, medium-dark skin tone, light skin tone
+1F469 1F3FE 200D 2764 FE0F 200D 1F469 1F3FC ; fully-qualified # 👩🏾‍❤️‍👩🏼 E13.1 couple with heart: woman, woman, medium-dark skin tone, medium-light skin tone
+1F469 1F3FE 200D 2764 200D 1F469 1F3FC ; minimally-qualified # 👩🏾‍❤‍👩🏼 E13.1 couple with heart: woman, woman, medium-dark skin tone, medium-light skin tone
+1F469 1F3FE 200D 2764 FE0F 200D 1F469 1F3FD ; fully-qualified # 👩🏾‍❤️‍👩🏽 E13.1 couple with heart: woman, woman, medium-dark skin tone, medium skin tone
+1F469 1F3FE 200D 2764 200D 1F469 1F3FD ; minimally-qualified # 👩🏾‍❤‍👩🏽 E13.1 couple with heart: woman, woman, medium-dark skin tone, medium skin tone
+1F469 1F3FE 200D 2764 FE0F 200D 1F469 1F3FE ; fully-qualified # 👩🏾‍❤️‍👩🏾 E13.1 couple with heart: woman, woman, medium-dark skin tone
+1F469 1F3FE 200D 2764 200D 1F469 1F3FE ; minimally-qualified # 👩🏾‍❤‍👩🏾 E13.1 couple with heart: woman, woman, medium-dark skin tone
+1F469 1F3FE 200D 2764 FE0F 200D 1F469 1F3FF ; fully-qualified # 👩🏾‍❤️‍👩🏿 E13.1 couple with heart: woman, woman, medium-dark skin tone, dark skin tone
+1F469 1F3FE 200D 2764 200D 1F469 1F3FF ; minimally-qualified # 👩🏾‍❤‍👩🏿 E13.1 couple with heart: woman, woman, medium-dark skin tone, dark skin tone
+1F469 1F3FF 200D 2764 FE0F 200D 1F469 1F3FB ; fully-qualified # 👩🏿‍❤️‍👩🏻 E13.1 couple with heart: woman, woman, dark skin tone, light skin tone
+1F469 1F3FF 200D 2764 200D 1F469 1F3FB ; minimally-qualified # 👩🏿‍❤‍👩🏻 E13.1 couple with heart: woman, woman, dark skin tone, light skin tone
+1F469 1F3FF 200D 2764 FE0F 200D 1F469 1F3FC ; fully-qualified # 👩🏿‍❤️‍👩🏼 E13.1 couple with heart: woman, woman, dark skin tone, medium-light skin tone
+1F469 1F3FF 200D 2764 200D 1F469 1F3FC ; minimally-qualified # 👩🏿‍❤‍👩🏼 E13.1 couple with heart: woman, woman, dark skin tone, medium-light skin tone
+1F469 1F3FF 200D 2764 FE0F 200D 1F469 1F3FD ; fully-qualified # 👩🏿‍❤️‍👩🏽 E13.1 couple with heart: woman, woman, dark skin tone, medium skin tone
+1F469 1F3FF 200D 2764 200D 1F469 1F3FD ; minimally-qualified # 👩🏿‍❤‍👩🏽 E13.1 couple with heart: woman, woman, dark skin tone, medium skin tone
+1F469 1F3FF 200D 2764 FE0F 200D 1F469 1F3FE ; fully-qualified # 👩🏿‍❤️‍👩🏾 E13.1 couple with heart: woman, woman, dark skin tone, medium-dark skin tone
+1F469 1F3FF 200D 2764 200D 1F469 1F3FE ; minimally-qualified # 👩🏿‍❤‍👩🏾 E13.1 couple with heart: woman, woman, dark skin tone, medium-dark skin tone
+1F469 1F3FF 200D 2764 FE0F 200D 1F469 1F3FF ; fully-qualified # 👩🏿‍❤️‍👩🏿 E13.1 couple with heart: woman, woman, dark skin tone
+1F469 1F3FF 200D 2764 200D 1F469 1F3FF ; minimally-qualified # 👩🏿‍❤‍👩🏿 E13.1 couple with heart: woman, woman, dark skin tone
+1F46A ; fully-qualified # 👪 E0.6 family
+1F468 200D 1F469 200D 1F466 ; fully-qualified # 👨‍👩‍👦 E2.0 family: man, woman, boy
+1F468 200D 1F469 200D 1F467 ; fully-qualified # 👨‍👩‍👧 E2.0 family: man, woman, girl
+1F468 200D 1F469 200D 1F467 200D 1F466 ; fully-qualified # 👨‍👩‍👧‍👦 E2.0 family: man, woman, girl, boy
+1F468 200D 1F469 200D 1F466 200D 1F466 ; fully-qualified # 👨‍👩‍👦‍👦 E2.0 family: man, woman, boy, boy
+1F468 200D 1F469 200D 1F467 200D 1F467 ; fully-qualified # 👨‍👩‍👧‍👧 E2.0 family: man, woman, girl, girl
+1F468 200D 1F468 200D 1F466 ; fully-qualified # 👨‍👨‍👦 E2.0 family: man, man, boy
+1F468 200D 1F468 200D 1F467 ; fully-qualified # 👨‍👨‍👧 E2.0 family: man, man, girl
+1F468 200D 1F468 200D 1F467 200D 1F466 ; fully-qualified # 👨‍👨‍👧‍👦 E2.0 family: man, man, girl, boy
+1F468 200D 1F468 200D 1F466 200D 1F466 ; fully-qualified # 👨‍👨‍👦‍👦 E2.0 family: man, man, boy, boy
+1F468 200D 1F468 200D 1F467 200D 1F467 ; fully-qualified # 👨‍👨‍👧‍👧 E2.0 family: man, man, girl, girl
+1F469 200D 1F469 200D 1F466 ; fully-qualified # 👩‍👩‍👦 E2.0 family: woman, woman, boy
+1F469 200D 1F469 200D 1F467 ; fully-qualified # 👩‍👩‍👧 E2.0 family: woman, woman, girl
+1F469 200D 1F469 200D 1F467 200D 1F466 ; fully-qualified # 👩‍👩‍👧‍👦 E2.0 family: woman, woman, girl, boy
+1F469 200D 1F469 200D 1F466 200D 1F466 ; fully-qualified # 👩‍👩‍👦‍👦 E2.0 family: woman, woman, boy, boy
+1F469 200D 1F469 200D 1F467 200D 1F467 ; fully-qualified # 👩‍👩‍👧‍👧 E2.0 family: woman, woman, girl, girl
+1F468 200D 1F466 ; fully-qualified # 👨‍👦 E4.0 family: man, boy
+1F468 200D 1F466 200D 1F466 ; fully-qualified # 👨‍👦‍👦 E4.0 family: man, boy, boy
+1F468 200D 1F467 ; fully-qualified # 👨‍👧 E4.0 family: man, girl
+1F468 200D 1F467 200D 1F466 ; fully-qualified # 👨‍👧‍👦 E4.0 family: man, girl, boy
+1F468 200D 1F467 200D 1F467 ; fully-qualified # 👨‍👧‍👧 E4.0 family: man, girl, girl
+1F469 200D 1F466 ; fully-qualified # 👩‍👦 E4.0 family: woman, boy
+1F469 200D 1F466 200D 1F466 ; fully-qualified # 👩‍👦‍👦 E4.0 family: woman, boy, boy
+1F469 200D 1F467 ; fully-qualified # 👩‍👧 E4.0 family: woman, girl
+1F469 200D 1F467 200D 1F466 ; fully-qualified # 👩‍👧‍👦 E4.0 family: woman, girl, boy
+1F469 200D 1F467 200D 1F467 ; fully-qualified # 👩‍👧‍👧 E4.0 family: woman, girl, girl
+
+# subgroup: person-symbol
+1F5E3 FE0F ; fully-qualified # 🗣️ E0.7 speaking head
+1F5E3 ; unqualified # 🗣 E0.7 speaking head
+1F464 ; fully-qualified # 👤 E0.6 bust in silhouette
+1F465 ; fully-qualified # 👥 E1.0 busts in silhouette
+1FAC2 ; fully-qualified # 🫂 E13.0 people hugging
+1F463 ; fully-qualified # 👣 E0.6 footprints
+
+# People & Body subtotal: 2899
+# People & Body subtotal: 494 w/o modifiers
+
+# group: Component
+
+# subgroup: skin-tone
+1F3FB ; component # 🏻 E1.0 light skin tone
+1F3FC ; component # 🏼 E1.0 medium-light skin tone
+1F3FD ; component # 🏽 E1.0 medium skin tone
+1F3FE ; component # 🏾 E1.0 medium-dark skin tone
+1F3FF ; component # 🏿 E1.0 dark skin tone
+
+# subgroup: hair-style
+1F9B0 ; component # 🦰 E11.0 red hair
+1F9B1 ; component # 🦱 E11.0 curly hair
+1F9B3 ; component # 🦳 E11.0 white hair
+1F9B2 ; component # 🦲 E11.0 bald
+
+# Component subtotal: 9
+# Component subtotal: 4 w/o modifiers
+
+# group: Animals & Nature
+
+# subgroup: animal-mammal
+1F435 ; fully-qualified # 🐵 E0.6 monkey face
+1F412 ; fully-qualified # 🐒 E0.6 monkey
+1F98D ; fully-qualified # 🦍 E3.0 gorilla
+1F9A7 ; fully-qualified # 🦧 E12.0 orangutan
+1F436 ; fully-qualified # 🐶 E0.6 dog face
+1F415 ; fully-qualified # 🐕 E0.7 dog
+1F9AE ; fully-qualified # 🦮 E12.0 guide dog
+1F415 200D 1F9BA ; fully-qualified # 🐕‍🦺 E12.0 service dog
+1F429 ; fully-qualified # 🐩 E0.6 poodle
+1F43A ; fully-qualified # 🐺 E0.6 wolf
+1F98A ; fully-qualified # 🦊 E3.0 fox
+1F99D ; fully-qualified # 🦝 E11.0 raccoon
+1F431 ; fully-qualified # 🐱 E0.6 cat face
+1F408 ; fully-qualified # 🐈 E0.7 cat
+1F408 200D 2B1B ; fully-qualified # 🐈‍⬛ E13.0 black cat
+1F981 ; fully-qualified # 🦁 E1.0 lion
+1F42F ; fully-qualified # 🐯 E0.6 tiger face
+1F405 ; fully-qualified # 🐅 E1.0 tiger
+1F406 ; fully-qualified # 🐆 E1.0 leopard
+1F434 ; fully-qualified # 🐴 E0.6 horse face
+1F40E ; fully-qualified # 🐎 E0.6 horse
+1F984 ; fully-qualified # 🦄 E1.0 unicorn
+1F993 ; fully-qualified # 🦓 E5.0 zebra
+1F98C ; fully-qualified # 🦌 E3.0 deer
+1F9AC ; fully-qualified # 🦬 E13.0 bison
+1F42E ; fully-qualified # 🐮 E0.6 cow face
+1F402 ; fully-qualified # 🐂 E1.0 ox
+1F403 ; fully-qualified # 🐃 E1.0 water buffalo
+1F404 ; fully-qualified # 🐄 E1.0 cow
+1F437 ; fully-qualified # 🐷 E0.6 pig face
+1F416 ; fully-qualified # 🐖 E1.0 pig
+1F417 ; fully-qualified # 🐗 E0.6 boar
+1F43D ; fully-qualified # 🐽 E0.6 pig nose
+1F40F ; fully-qualified # 🐏 E1.0 ram
+1F411 ; fully-qualified # 🐑 E0.6 ewe
+1F410 ; fully-qualified # 🐐 E1.0 goat
+1F42A ; fully-qualified # 🐪 E1.0 camel
+1F42B ; fully-qualified # 🐫 E0.6 two-hump camel
+1F999 ; fully-qualified # 🦙 E11.0 llama
+1F992 ; fully-qualified # 🦒 E5.0 giraffe
+1F418 ; fully-qualified # 🐘 E0.6 elephant
+1F9A3 ; fully-qualified # 🦣 E13.0 mammoth
+1F98F ; fully-qualified # 🦏 E3.0 rhinoceros
+1F99B ; fully-qualified # 🦛 E11.0 hippopotamus
+1F42D ; fully-qualified # 🐭 E0.6 mouse face
+1F401 ; fully-qualified # 🐁 E1.0 mouse
+1F400 ; fully-qualified # 🐀 E1.0 rat
+1F439 ; fully-qualified # 🐹 E0.6 hamster
+1F430 ; fully-qualified # 🐰 E0.6 rabbit face
+1F407 ; fully-qualified # 🐇 E1.0 rabbit
+1F43F FE0F ; fully-qualified # 🐿️ E0.7 chipmunk
+1F43F ; unqualified # 🐿 E0.7 chipmunk
+1F9AB ; fully-qualified # 🦫 E13.0 beaver
+1F994 ; fully-qualified # 🦔 E5.0 hedgehog
+1F987 ; fully-qualified # 🦇 E3.0 bat
+1F43B ; fully-qualified # 🐻 E0.6 bear
+1F43B 200D 2744 FE0F ; fully-qualified # 🐻‍❄️ E13.0 polar bear
+1F43B 200D 2744 ; minimally-qualified # 🐻‍❄ E13.0 polar bear
+1F428 ; fully-qualified # 🐨 E0.6 koala
+1F43C ; fully-qualified # 🐼 E0.6 panda
+1F9A5 ; fully-qualified # 🦥 E12.0 sloth
+1F9A6 ; fully-qualified # 🦦 E12.0 otter
+1F9A8 ; fully-qualified # 🦨 E12.0 skunk
+1F998 ; fully-qualified # 🦘 E11.0 kangaroo
+1F9A1 ; fully-qualified # 🦡 E11.0 badger
+1F43E ; fully-qualified # 🐾 E0.6 paw prints
+
+# subgroup: animal-bird
+1F983 ; fully-qualified # 🦃 E1.0 turkey
+1F414 ; fully-qualified # 🐔 E0.6 chicken
+1F413 ; fully-qualified # 🐓 E1.0 rooster
+1F423 ; fully-qualified # 🐣 E0.6 hatching chick
+1F424 ; fully-qualified # 🐤 E0.6 baby chick
+1F425 ; fully-qualified # 🐥 E0.6 front-facing baby chick
+1F426 ; fully-qualified # 🐦 E0.6 bird
+1F427 ; fully-qualified # 🐧 E0.6 penguin
+1F54A FE0F ; fully-qualified # 🕊️ E0.7 dove
+1F54A ; unqualified # 🕊 E0.7 dove
+1F985 ; fully-qualified # 🦅 E3.0 eagle
+1F986 ; fully-qualified # 🦆 E3.0 duck
+1F9A2 ; fully-qualified # 🦢 E11.0 swan
+1F989 ; fully-qualified # 🦉 E3.0 owl
+1F9A4 ; fully-qualified # 🦤 E13.0 dodo
+1FAB6 ; fully-qualified # 🪶 E13.0 feather
+1F9A9 ; fully-qualified # 🦩 E12.0 flamingo
+1F99A ; fully-qualified # 🦚 E11.0 peacock
+1F99C ; fully-qualified # 🦜 E11.0 parrot
+
+# subgroup: animal-amphibian
+1F438 ; fully-qualified # 🐸 E0.6 frog
+
+# subgroup: animal-reptile
+1F40A ; fully-qualified # 🐊 E1.0 crocodile
+1F422 ; fully-qualified # 🐢 E0.6 turtle
+1F98E ; fully-qualified # 🦎 E3.0 lizard
+1F40D ; fully-qualified # 🐍 E0.6 snake
+1F432 ; fully-qualified # 🐲 E0.6 dragon face
+1F409 ; fully-qualified # 🐉 E1.0 dragon
+1F995 ; fully-qualified # 🦕 E5.0 sauropod
+1F996 ; fully-qualified # 🦖 E5.0 T-Rex
+
+# subgroup: animal-marine
+1F433 ; fully-qualified # 🐳 E0.6 spouting whale
+1F40B ; fully-qualified # 🐋 E1.0 whale
+1F42C ; fully-qualified # 🐬 E0.6 dolphin
+1F9AD ; fully-qualified # 🦭 E13.0 seal
+1F41F ; fully-qualified # 🐟 E0.6 fish
+1F420 ; fully-qualified # 🐠 E0.6 tropical fish
+1F421 ; fully-qualified # 🐡 E0.6 blowfish
+1F988 ; fully-qualified # 🦈 E3.0 shark
+1F419 ; fully-qualified # 🐙 E0.6 octopus
+1F41A ; fully-qualified # 🐚 E0.6 spiral shell
+
+# subgroup: animal-bug
+1F40C ; fully-qualified # 🐌 E0.6 snail
+1F98B ; fully-qualified # 🦋 E3.0 butterfly
+1F41B ; fully-qualified # 🐛 E0.6 bug
+1F41C ; fully-qualified # 🐜 E0.6 ant
+1F41D ; fully-qualified # 🐝 E0.6 honeybee
+1FAB2 ; fully-qualified # 🪲 E13.0 beetle
+1F41E ; fully-qualified # 🐞 E0.6 lady beetle
+1F997 ; fully-qualified # 🦗 E5.0 cricket
+1FAB3 ; fully-qualified # 🪳 E13.0 cockroach
+1F577 FE0F ; fully-qualified # 🕷️ E0.7 spider
+1F577 ; unqualified # 🕷 E0.7 spider
+1F578 FE0F ; fully-qualified # 🕸️ E0.7 spider web
+1F578 ; unqualified # 🕸 E0.7 spider web
+1F982 ; fully-qualified # 🦂 E1.0 scorpion
+1F99F ; fully-qualified # 🦟 E11.0 mosquito
+1FAB0 ; fully-qualified # 🪰 E13.0 fly
+1FAB1 ; fully-qualified # 🪱 E13.0 worm
+1F9A0 ; fully-qualified # 🦠 E11.0 microbe
+
+# subgroup: plant-flower
+1F490 ; fully-qualified # 💐 E0.6 bouquet
+1F338 ; fully-qualified # 🌸 E0.6 cherry blossom
+1F4AE ; fully-qualified # 💮 E0.6 white flower
+1F3F5 FE0F ; fully-qualified # 🏵️ E0.7 rosette
+1F3F5 ; unqualified # 🏵 E0.7 rosette
+1F339 ; fully-qualified # 🌹 E0.6 rose
+1F940 ; fully-qualified # 🥀 E3.0 wilted flower
+1F33A ; fully-qualified # 🌺 E0.6 hibiscus
+1F33B ; fully-qualified # 🌻 E0.6 sunflower
+1F33C ; fully-qualified # 🌼 E0.6 blossom
+1F337 ; fully-qualified # 🌷 E0.6 tulip
+
+# subgroup: plant-other
+1F331 ; fully-qualified # 🌱 E0.6 seedling
+1FAB4 ; fully-qualified # 🪴 E13.0 potted plant
+1F332 ; fully-qualified # 🌲 E1.0 evergreen tree
+1F333 ; fully-qualified # 🌳 E1.0 deciduous tree
+1F334 ; fully-qualified # 🌴 E0.6 palm tree
+1F335 ; fully-qualified # 🌵 E0.6 cactus
+1F33E ; fully-qualified # 🌾 E0.6 sheaf of rice
+1F33F ; fully-qualified # 🌿 E0.6 herb
+2618 FE0F ; fully-qualified # ☘️ E1.0 shamrock
+2618 ; unqualified # ☘ E1.0 shamrock
+1F340 ; fully-qualified # 🍀 E0.6 four leaf clover
+1F341 ; fully-qualified # 🍁 E0.6 maple leaf
+1F342 ; fully-qualified # 🍂 E0.6 fallen leaf
+1F343 ; fully-qualified # 🍃 E0.6 leaf fluttering in wind
+
+# Animals & Nature subtotal: 147
+# Animals & Nature subtotal: 147 w/o modifiers
+
+# group: Food & Drink
+
+# subgroup: food-fruit
+1F347 ; fully-qualified # 🍇 E0.6 grapes
+1F348 ; fully-qualified # 🍈 E0.6 melon
+1F349 ; fully-qualified # 🍉 E0.6 watermelon
+1F34A ; fully-qualified # 🍊 E0.6 tangerine
+1F34B ; fully-qualified # 🍋 E1.0 lemon
+1F34C ; fully-qualified # 🍌 E0.6 banana
+1F34D ; fully-qualified # 🍍 E0.6 pineapple
+1F96D ; fully-qualified # 🥭 E11.0 mango
+1F34E ; fully-qualified # 🍎 E0.6 red apple
+1F34F ; fully-qualified # 🍏 E0.6 green apple
+1F350 ; fully-qualified # 🍐 E1.0 pear
+1F351 ; fully-qualified # 🍑 E0.6 peach
+1F352 ; fully-qualified # 🍒 E0.6 cherries
+1F353 ; fully-qualified # 🍓 E0.6 strawberry
+1FAD0 ; fully-qualified # 🫐 E13.0 blueberries
+1F95D ; fully-qualified # 🥝 E3.0 kiwi fruit
+1F345 ; fully-qualified # 🍅 E0.6 tomato
+1FAD2 ; fully-qualified # 🫒 E13.0 olive
+1F965 ; fully-qualified # 🥥 E5.0 coconut
+
+# subgroup: food-vegetable
+1F951 ; fully-qualified # 🥑 E3.0 avocado
+1F346 ; fully-qualified # 🍆 E0.6 eggplant
+1F954 ; fully-qualified # 🥔 E3.0 potato
+1F955 ; fully-qualified # 🥕 E3.0 carrot
+1F33D ; fully-qualified # 🌽 E0.6 ear of corn
+1F336 FE0F ; fully-qualified # 🌶️ E0.7 hot pepper
+1F336 ; unqualified # 🌶 E0.7 hot pepper
+1FAD1 ; fully-qualified # 🫑 E13.0 bell pepper
+1F952 ; fully-qualified # 🥒 E3.0 cucumber
+1F96C ; fully-qualified # 🥬 E11.0 leafy green
+1F966 ; fully-qualified # 🥦 E5.0 broccoli
+1F9C4 ; fully-qualified # 🧄 E12.0 garlic
+1F9C5 ; fully-qualified # 🧅 E12.0 onion
+1F344 ; fully-qualified # 🍄 E0.6 mushroom
+1F95C ; fully-qualified # 🥜 E3.0 peanuts
+1F330 ; fully-qualified # 🌰 E0.6 chestnut
+
+# subgroup: food-prepared
+1F35E ; fully-qualified # 🍞 E0.6 bread
+1F950 ; fully-qualified # 🥐 E3.0 croissant
+1F956 ; fully-qualified # 🥖 E3.0 baguette bread
+1FAD3 ; fully-qualified # 🫓 E13.0 flatbread
+1F968 ; fully-qualified # 🥨 E5.0 pretzel
+1F96F ; fully-qualified # 🥯 E11.0 bagel
+1F95E ; fully-qualified # 🥞 E3.0 pancakes
+1F9C7 ; fully-qualified # 🧇 E12.0 waffle
+1F9C0 ; fully-qualified # 🧀 E1.0 cheese wedge
+1F356 ; fully-qualified # 🍖 E0.6 meat on bone
+1F357 ; fully-qualified # 🍗 E0.6 poultry leg
+1F969 ; fully-qualified # 🥩 E5.0 cut of meat
+1F953 ; fully-qualified # 🥓 E3.0 bacon
+1F354 ; fully-qualified # 🍔 E0.6 hamburger
+1F35F ; fully-qualified # 🍟 E0.6 french fries
+1F355 ; fully-qualified # 🍕 E0.6 pizza
+1F32D ; fully-qualified # 🌭 E1.0 hot dog
+1F96A ; fully-qualified # 🥪 E5.0 sandwich
+1F32E ; fully-qualified # 🌮 E1.0 taco
+1F32F ; fully-qualified # 🌯 E1.0 burrito
+1FAD4 ; fully-qualified # 🫔 E13.0 tamale
+1F959 ; fully-qualified # 🥙 E3.0 stuffed flatbread
+1F9C6 ; fully-qualified # 🧆 E12.0 falafel
+1F95A ; fully-qualified # 🥚 E3.0 egg
+1F373 ; fully-qualified # 🍳 E0.6 cooking
+1F958 ; fully-qualified # 🥘 E3.0 shallow pan of food
+1F372 ; fully-qualified # 🍲 E0.6 pot of food
+1FAD5 ; fully-qualified # 🫕 E13.0 fondue
+1F963 ; fully-qualified # 🥣 E5.0 bowl with spoon
+1F957 ; fully-qualified # 🥗 E3.0 green salad
+1F37F ; fully-qualified # 🍿 E1.0 popcorn
+1F9C8 ; fully-qualified # 🧈 E12.0 butter
+1F9C2 ; fully-qualified # 🧂 E11.0 salt
+1F96B ; fully-qualified # 🥫 E5.0 canned food
+
+# subgroup: food-asian
+1F371 ; fully-qualified # 🍱 E0.6 bento box
+1F358 ; fully-qualified # 🍘 E0.6 rice cracker
+1F359 ; fully-qualified # 🍙 E0.6 rice ball
+1F35A ; fully-qualified # 🍚 E0.6 cooked rice
+1F35B ; fully-qualified # 🍛 E0.6 curry rice
+1F35C ; fully-qualified # 🍜 E0.6 steaming bowl
+1F35D ; fully-qualified # 🍝 E0.6 spaghetti
+1F360 ; fully-qualified # 🍠 E0.6 roasted sweet potato
+1F362 ; fully-qualified # 🍢 E0.6 oden
+1F363 ; fully-qualified # 🍣 E0.6 sushi
+1F364 ; fully-qualified # 🍤 E0.6 fried shrimp
+1F365 ; fully-qualified # 🍥 E0.6 fish cake with swirl
+1F96E ; fully-qualified # 🥮 E11.0 moon cake
+1F361 ; fully-qualified # 🍡 E0.6 dango
+1F95F ; fully-qualified # 🥟 E5.0 dumpling
+1F960 ; fully-qualified # 🥠 E5.0 fortune cookie
+1F961 ; fully-qualified # 🥡 E5.0 takeout box
+
+# subgroup: food-marine
+1F980 ; fully-qualified # 🦀 E1.0 crab
+1F99E ; fully-qualified # 🦞 E11.0 lobster
+1F990 ; fully-qualified # 🦐 E3.0 shrimp
+1F991 ; fully-qualified # 🦑 E3.0 squid
+1F9AA ; fully-qualified # 🦪 E12.0 oyster
+
+# subgroup: food-sweet
+1F366 ; fully-qualified # 🍦 E0.6 soft ice cream
+1F367 ; fully-qualified # 🍧 E0.6 shaved ice
+1F368 ; fully-qualified # 🍨 E0.6 ice cream
+1F369 ; fully-qualified # 🍩 E0.6 doughnut
+1F36A ; fully-qualified # 🍪 E0.6 cookie
+1F382 ; fully-qualified # 🎂 E0.6 birthday cake
+1F370 ; fully-qualified # 🍰 E0.6 shortcake
+1F9C1 ; fully-qualified # 🧁 E11.0 cupcake
+1F967 ; fully-qualified # 🥧 E5.0 pie
+1F36B ; fully-qualified # 🍫 E0.6 chocolate bar
+1F36C ; fully-qualified # 🍬 E0.6 candy
+1F36D ; fully-qualified # 🍭 E0.6 lollipop
+1F36E ; fully-qualified # 🍮 E0.6 custard
+1F36F ; fully-qualified # 🍯 E0.6 honey pot
+
+# subgroup: drink
+1F37C ; fully-qualified # 🍼 E1.0 baby bottle
+1F95B ; fully-qualified # 🥛 E3.0 glass of milk
+2615 ; fully-qualified # ☕ E0.6 hot beverage
+1FAD6 ; fully-qualified # 🫖 E13.0 teapot
+1F375 ; fully-qualified # 🍵 E0.6 teacup without handle
+1F376 ; fully-qualified # 🍶 E0.6 sake
+1F37E ; fully-qualified # 🍾 E1.0 bottle with popping cork
+1F377 ; fully-qualified # 🍷 E0.6 wine glass
+1F378 ; fully-qualified # 🍸 E0.6 cocktail glass
+1F379 ; fully-qualified # 🍹 E0.6 tropical drink
+1F37A ; fully-qualified # 🍺 E0.6 beer mug
+1F37B ; fully-qualified # 🍻 E0.6 clinking beer mugs
+1F942 ; fully-qualified # 🥂 E3.0 clinking glasses
+1F943 ; fully-qualified # 🥃 E3.0 tumbler glass
+1F964 ; fully-qualified # 🥤 E5.0 cup with straw
+1F9CB ; fully-qualified # 🧋 E13.0 bubble tea
+1F9C3 ; fully-qualified # 🧃 E12.0 beverage box
+1F9C9 ; fully-qualified # 🧉 E12.0 mate
+1F9CA ; fully-qualified # 🧊 E12.0 ice
+
+# subgroup: dishware
+1F962 ; fully-qualified # 🥢 E5.0 chopsticks
+1F37D FE0F ; fully-qualified # 🍽️ E0.7 fork and knife with plate
+1F37D ; unqualified # 🍽 E0.7 fork and knife with plate
+1F374 ; fully-qualified # 🍴 E0.6 fork and knife
+1F944 ; fully-qualified # 🥄 E3.0 spoon
+1F52A ; fully-qualified # 🔪 E0.6 kitchen knife
+1F3FA ; fully-qualified # 🏺 E1.0 amphora
+
+# Food & Drink subtotal: 131
+# Food & Drink subtotal: 131 w/o modifiers
+
+# group: Travel & Places
+
+# subgroup: place-map
+1F30D ; fully-qualified # 🌍 E0.7 globe showing Europe-Africa
+1F30E ; fully-qualified # 🌎 E0.7 globe showing Americas
+1F30F ; fully-qualified # 🌏 E0.6 globe showing Asia-Australia
+1F310 ; fully-qualified # 🌐 E1.0 globe with meridians
+1F5FA FE0F ; fully-qualified # 🗺️ E0.7 world map
+1F5FA ; unqualified # 🗺 E0.7 world map
+1F5FE ; fully-qualified # 🗾 E0.6 map of Japan
+1F9ED ; fully-qualified # 🧭 E11.0 compass
+
+# subgroup: place-geographic
+1F3D4 FE0F ; fully-qualified # 🏔️ E0.7 snow-capped mountain
+1F3D4 ; unqualified # 🏔 E0.7 snow-capped mountain
+26F0 FE0F ; fully-qualified # ⛰️ E0.7 mountain
+26F0 ; unqualified # ⛰ E0.7 mountain
+1F30B ; fully-qualified # 🌋 E0.6 volcano
+1F5FB ; fully-qualified # 🗻 E0.6 mount fuji
+1F3D5 FE0F ; fully-qualified # 🏕️ E0.7 camping
+1F3D5 ; unqualified # 🏕 E0.7 camping
+1F3D6 FE0F ; fully-qualified # 🏖️ E0.7 beach with umbrella
+1F3D6 ; unqualified # 🏖 E0.7 beach with umbrella
+1F3DC FE0F ; fully-qualified # 🏜️ E0.7 desert
+1F3DC ; unqualified # 🏜 E0.7 desert
+1F3DD FE0F ; fully-qualified # 🏝️ E0.7 desert island
+1F3DD ; unqualified # 🏝 E0.7 desert island
+1F3DE FE0F ; fully-qualified # 🏞️ E0.7 national park
+1F3DE ; unqualified # 🏞 E0.7 national park
+
+# subgroup: place-building
+1F3DF FE0F ; fully-qualified # 🏟️ E0.7 stadium
+1F3DF ; unqualified # 🏟 E0.7 stadium
+1F3DB FE0F ; fully-qualified # 🏛️ E0.7 classical building
+1F3DB ; unqualified # 🏛 E0.7 classical building
+1F3D7 FE0F ; fully-qualified # 🏗️ E0.7 building construction
+1F3D7 ; unqualified # 🏗 E0.7 building construction
+1F9F1 ; fully-qualified # 🧱 E11.0 brick
+1FAA8 ; fully-qualified # 🪨 E13.0 rock
+1FAB5 ; fully-qualified # 🪵 E13.0 wood
+1F6D6 ; fully-qualified # 🛖 E13.0 hut
+1F3D8 FE0F ; fully-qualified # 🏘️ E0.7 houses
+1F3D8 ; unqualified # 🏘 E0.7 houses
+1F3DA FE0F ; fully-qualified # 🏚️ E0.7 derelict house
+1F3DA ; unqualified # 🏚 E0.7 derelict house
+1F3E0 ; fully-qualified # 🏠 E0.6 house
+1F3E1 ; fully-qualified # 🏡 E0.6 house with garden
+1F3E2 ; fully-qualified # 🏢 E0.6 office building
+1F3E3 ; fully-qualified # 🏣 E0.6 Japanese post office
+1F3E4 ; fully-qualified # 🏤 E1.0 post office
+1F3E5 ; fully-qualified # 🏥 E0.6 hospital
+1F3E6 ; fully-qualified # 🏦 E0.6 bank
+1F3E8 ; fully-qualified # 🏨 E0.6 hotel
+1F3E9 ; fully-qualified # 🏩 E0.6 love hotel
+1F3EA ; fully-qualified # 🏪 E0.6 convenience store
+1F3EB ; fully-qualified # 🏫 E0.6 school
+1F3EC ; fully-qualified # 🏬 E0.6 department store
+1F3ED ; fully-qualified # 🏭 E0.6 factory
+1F3EF ; fully-qualified # 🏯 E0.6 Japanese castle
+1F3F0 ; fully-qualified # 🏰 E0.6 castle
+1F492 ; fully-qualified # 💒 E0.6 wedding
+1F5FC ; fully-qualified # 🗼 E0.6 Tokyo tower
+1F5FD ; fully-qualified # 🗽 E0.6 Statue of Liberty
+
+# subgroup: place-religious
+26EA ; fully-qualified # ⛪ E0.6 church
+1F54C ; fully-qualified # 🕌 E1.0 mosque
+1F6D5 ; fully-qualified # 🛕 E12.0 hindu temple
+1F54D ; fully-qualified # 🕍 E1.0 synagogue
+26E9 FE0F ; fully-qualified # ⛩️ E0.7 shinto shrine
+26E9 ; unqualified # ⛩ E0.7 shinto shrine
+1F54B ; fully-qualified # 🕋 E1.0 kaaba
+
+# subgroup: place-other
+26F2 ; fully-qualified # ⛲ E0.6 fountain
+26FA ; fully-qualified # ⛺ E0.6 tent
+1F301 ; fully-qualified # 🌁 E0.6 foggy
+1F303 ; fully-qualified # 🌃 E0.6 night with stars
+1F3D9 FE0F ; fully-qualified # 🏙️ E0.7 cityscape
+1F3D9 ; unqualified # 🏙 E0.7 cityscape
+1F304 ; fully-qualified # 🌄 E0.6 sunrise over mountains
+1F305 ; fully-qualified # 🌅 E0.6 sunrise
+1F306 ; fully-qualified # 🌆 E0.6 cityscape at dusk
+1F307 ; fully-qualified # 🌇 E0.6 sunset
+1F309 ; fully-qualified # 🌉 E0.6 bridge at night
+2668 FE0F ; fully-qualified # ♨️ E0.6 hot springs
+2668 ; unqualified # ♨ E0.6 hot springs
+1F3A0 ; fully-qualified # 🎠 E0.6 carousel horse
+1F3A1 ; fully-qualified # 🎡 E0.6 ferris wheel
+1F3A2 ; fully-qualified # 🎢 E0.6 roller coaster
+1F488 ; fully-qualified # 💈 E0.6 barber pole
+1F3AA ; fully-qualified # 🎪 E0.6 circus tent
+
+# subgroup: transport-ground
+1F682 ; fully-qualified # 🚂 E1.0 locomotive
+1F683 ; fully-qualified # 🚃 E0.6 railway car
+1F684 ; fully-qualified # 🚄 E0.6 high-speed train
+1F685 ; fully-qualified # 🚅 E0.6 bullet train
+1F686 ; fully-qualified # 🚆 E1.0 train
+1F687 ; fully-qualified # 🚇 E0.6 metro
+1F688 ; fully-qualified # 🚈 E1.0 light rail
+1F689 ; fully-qualified # 🚉 E0.6 station
+1F68A ; fully-qualified # 🚊 E1.0 tram
+1F69D ; fully-qualified # 🚝 E1.0 monorail
+1F69E ; fully-qualified # 🚞 E1.0 mountain railway
+1F68B ; fully-qualified # 🚋 E1.0 tram car
+1F68C ; fully-qualified # 🚌 E0.6 bus
+1F68D ; fully-qualified # 🚍 E0.7 oncoming bus
+1F68E ; fully-qualified # 🚎 E1.0 trolleybus
+1F690 ; fully-qualified # 🚐 E1.0 minibus
+1F691 ; fully-qualified # 🚑 E0.6 ambulance
+1F692 ; fully-qualified # 🚒 E0.6 fire engine
+1F693 ; fully-qualified # 🚓 E0.6 police car
+1F694 ; fully-qualified # 🚔 E0.7 oncoming police car
+1F695 ; fully-qualified # 🚕 E0.6 taxi
+1F696 ; fully-qualified # 🚖 E1.0 oncoming taxi
+1F697 ; fully-qualified # 🚗 E0.6 automobile
+1F698 ; fully-qualified # 🚘 E0.7 oncoming automobile
+1F699 ; fully-qualified # 🚙 E0.6 sport utility vehicle
+1F6FB ; fully-qualified # 🛻 E13.0 pickup truck
+1F69A ; fully-qualified # 🚚 E0.6 delivery truck
+1F69B ; fully-qualified # 🚛 E1.0 articulated lorry
+1F69C ; fully-qualified # 🚜 E1.0 tractor
+1F3CE FE0F ; fully-qualified # 🏎️ E0.7 racing car
+1F3CE ; unqualified # 🏎 E0.7 racing car
+1F3CD FE0F ; fully-qualified # 🏍️ E0.7 motorcycle
+1F3CD ; unqualified # 🏍 E0.7 motorcycle
+1F6F5 ; fully-qualified # 🛵 E3.0 motor scooter
+1F9BD ; fully-qualified # 🦽 E12.0 manual wheelchair
+1F9BC ; fully-qualified # 🦼 E12.0 motorized wheelchair
+1F6FA ; fully-qualified # 🛺 E12.0 auto rickshaw
+1F6B2 ; fully-qualified # 🚲 E0.6 bicycle
+1F6F4 ; fully-qualified # 🛴 E3.0 kick scooter
+1F6F9 ; fully-qualified # 🛹 E11.0 skateboard
+1F6FC ; fully-qualified # 🛼 E13.0 roller skate
+1F68F ; fully-qualified # 🚏 E0.6 bus stop
+1F6E3 FE0F ; fully-qualified # 🛣️ E0.7 motorway
+1F6E3 ; unqualified # 🛣 E0.7 motorway
+1F6E4 FE0F ; fully-qualified # 🛤️ E0.7 railway track
+1F6E4 ; unqualified # 🛤 E0.7 railway track
+1F6E2 FE0F ; fully-qualified # 🛢️ E0.7 oil drum
+1F6E2 ; unqualified # 🛢 E0.7 oil drum
+26FD ; fully-qualified # ⛽ E0.6 fuel pump
+1F6A8 ; fully-qualified # 🚨 E0.6 police car light
+1F6A5 ; fully-qualified # 🚥 E0.6 horizontal traffic light
+1F6A6 ; fully-qualified # 🚦 E1.0 vertical traffic light
+1F6D1 ; fully-qualified # 🛑 E3.0 stop sign
+1F6A7 ; fully-qualified # 🚧 E0.6 construction
+
+# subgroup: transport-water
+2693 ; fully-qualified # ⚓ E0.6 anchor
+26F5 ; fully-qualified # ⛵ E0.6 sailboat
+1F6F6 ; fully-qualified # 🛶 E3.0 canoe
+1F6A4 ; fully-qualified # 🚤 E0.6 speedboat
+1F6F3 FE0F ; fully-qualified # 🛳️ E0.7 passenger ship
+1F6F3 ; unqualified # 🛳 E0.7 passenger ship
+26F4 FE0F ; fully-qualified # ⛴️ E0.7 ferry
+26F4 ; unqualified # ⛴ E0.7 ferry
+1F6E5 FE0F ; fully-qualified # 🛥️ E0.7 motor boat
+1F6E5 ; unqualified # 🛥 E0.7 motor boat
+1F6A2 ; fully-qualified # 🚢 E0.6 ship
+
+# subgroup: transport-air
+2708 FE0F ; fully-qualified # ✈️ E0.6 airplane
+2708 ; unqualified # ✈ E0.6 airplane
+1F6E9 FE0F ; fully-qualified # 🛩️ E0.7 small airplane
+1F6E9 ; unqualified # 🛩 E0.7 small airplane
+1F6EB ; fully-qualified # 🛫 E1.0 airplane departure
+1F6EC ; fully-qualified # 🛬 E1.0 airplane arrival
+1FA82 ; fully-qualified # 🪂 E12.0 parachute
+1F4BA ; fully-qualified # 💺 E0.6 seat
+1F681 ; fully-qualified # 🚁 E1.0 helicopter
+1F69F ; fully-qualified # 🚟 E1.0 suspension railway
+1F6A0 ; fully-qualified # 🚠 E1.0 mountain cableway
+1F6A1 ; fully-qualified # 🚡 E1.0 aerial tramway
+1F6F0 FE0F ; fully-qualified # 🛰️ E0.7 satellite
+1F6F0 ; unqualified # 🛰 E0.7 satellite
+1F680 ; fully-qualified # 🚀 E0.6 rocket
+1F6F8 ; fully-qualified # 🛸 E5.0 flying saucer
+
+# subgroup: hotel
+1F6CE FE0F ; fully-qualified # 🛎️ E0.7 bellhop bell
+1F6CE ; unqualified # 🛎 E0.7 bellhop bell
+1F9F3 ; fully-qualified # 🧳 E11.0 luggage
+
+# subgroup: time
+231B ; fully-qualified # ⌛ E0.6 hourglass done
+23F3 ; fully-qualified # ⏳ E0.6 hourglass not done
+231A ; fully-qualified # ⌚ E0.6 watch
+23F0 ; fully-qualified # ⏰ E0.6 alarm clock
+23F1 FE0F ; fully-qualified # ⏱️ E1.0 stopwatch
+23F1 ; unqualified # ⏱ E1.0 stopwatch
+23F2 FE0F ; fully-qualified # ⏲️ E1.0 timer clock
+23F2 ; unqualified # ⏲ E1.0 timer clock
+1F570 FE0F ; fully-qualified # 🕰️ E0.7 mantelpiece clock
+1F570 ; unqualified # 🕰 E0.7 mantelpiece clock
+1F55B ; fully-qualified # 🕛 E0.6 twelve o’clock
+1F567 ; fully-qualified # 🕧 E0.7 twelve-thirty
+1F550 ; fully-qualified # 🕐 E0.6 one o’clock
+1F55C ; fully-qualified # 🕜 E0.7 one-thirty
+1F551 ; fully-qualified # 🕑 E0.6 two o’clock
+1F55D ; fully-qualified # 🕝 E0.7 two-thirty
+1F552 ; fully-qualified # 🕒 E0.6 three o’clock
+1F55E ; fully-qualified # 🕞 E0.7 three-thirty
+1F553 ; fully-qualified # 🕓 E0.6 four o’clock
+1F55F ; fully-qualified # 🕟 E0.7 four-thirty
+1F554 ; fully-qualified # 🕔 E0.6 five o’clock
+1F560 ; fully-qualified # 🕠 E0.7 five-thirty
+1F555 ; fully-qualified # 🕕 E0.6 six o’clock
+1F561 ; fully-qualified # 🕡 E0.7 six-thirty
+1F556 ; fully-qualified # 🕖 E0.6 seven o’clock
+1F562 ; fully-qualified # 🕢 E0.7 seven-thirty
+1F557 ; fully-qualified # 🕗 E0.6 eight o’clock
+1F563 ; fully-qualified # 🕣 E0.7 eight-thirty
+1F558 ; fully-qualified # 🕘 E0.6 nine o’clock
+1F564 ; fully-qualified # 🕤 E0.7 nine-thirty
+1F559 ; fully-qualified # 🕙 E0.6 ten o’clock
+1F565 ; fully-qualified # 🕥 E0.7 ten-thirty
+1F55A ; fully-qualified # 🕚 E0.6 eleven o’clock
+1F566 ; fully-qualified # 🕦 E0.7 eleven-thirty
+
+# subgroup: sky & weather
+1F311 ; fully-qualified # 🌑 E0.6 new moon
+1F312 ; fully-qualified # 🌒 E1.0 waxing crescent moon
+1F313 ; fully-qualified # 🌓 E0.6 first quarter moon
+1F314 ; fully-qualified # 🌔 E0.6 waxing gibbous moon
+1F315 ; fully-qualified # 🌕 E0.6 full moon
+1F316 ; fully-qualified # 🌖 E1.0 waning gibbous moon
+1F317 ; fully-qualified # 🌗 E1.0 last quarter moon
+1F318 ; fully-qualified # 🌘 E1.0 waning crescent moon
+1F319 ; fully-qualified # 🌙 E0.6 crescent moon
+1F31A ; fully-qualified # 🌚 E1.0 new moon face
+1F31B ; fully-qualified # 🌛 E0.6 first quarter moon face
+1F31C ; fully-qualified # 🌜 E0.7 last quarter moon face
+1F321 FE0F ; fully-qualified # 🌡️ E0.7 thermometer
+1F321 ; unqualified # 🌡 E0.7 thermometer
+2600 FE0F ; fully-qualified # ☀️ E0.6 sun
+2600 ; unqualified # ☀ E0.6 sun
+1F31D ; fully-qualified # 🌝 E1.0 full moon face
+1F31E ; fully-qualified # 🌞 E1.0 sun with face
+1FA90 ; fully-qualified # 🪐 E12.0 ringed planet
+2B50 ; fully-qualified # ⭐ E0.6 star
+1F31F ; fully-qualified # 🌟 E0.6 glowing star
+1F320 ; fully-qualified # 🌠 E0.6 shooting star
+1F30C ; fully-qualified # 🌌 E0.6 milky way
+2601 FE0F ; fully-qualified # ☁️ E0.6 cloud
+2601 ; unqualified # ☁ E0.6 cloud
+26C5 ; fully-qualified # ⛅ E0.6 sun behind cloud
+26C8 FE0F ; fully-qualified # ⛈️ E0.7 cloud with lightning and rain
+26C8 ; unqualified # ⛈ E0.7 cloud with lightning and rain
+1F324 FE0F ; fully-qualified # 🌤️ E0.7 sun behind small cloud
+1F324 ; unqualified # 🌤 E0.7 sun behind small cloud
+1F325 FE0F ; fully-qualified # 🌥️ E0.7 sun behind large cloud
+1F325 ; unqualified # 🌥 E0.7 sun behind large cloud
+1F326 FE0F ; fully-qualified # 🌦️ E0.7 sun behind rain cloud
+1F326 ; unqualified # 🌦 E0.7 sun behind rain cloud
+1F327 FE0F ; fully-qualified # 🌧️ E0.7 cloud with rain
+1F327 ; unqualified # 🌧 E0.7 cloud with rain
+1F328 FE0F ; fully-qualified # 🌨️ E0.7 cloud with snow
+1F328 ; unqualified # 🌨 E0.7 cloud with snow
+1F329 FE0F ; fully-qualified # 🌩️ E0.7 cloud with lightning
+1F329 ; unqualified # 🌩 E0.7 cloud with lightning
+1F32A FE0F ; fully-qualified # 🌪️ E0.7 tornado
+1F32A ; unqualified # 🌪 E0.7 tornado
+1F32B FE0F ; fully-qualified # 🌫️ E0.7 fog
+1F32B ; unqualified # 🌫 E0.7 fog
+1F32C FE0F ; fully-qualified # 🌬️ E0.7 wind face
+1F32C ; unqualified # 🌬 E0.7 wind face
+1F300 ; fully-qualified # 🌀 E0.6 cyclone
+1F308 ; fully-qualified # 🌈 E0.6 rainbow
+1F302 ; fully-qualified # 🌂 E0.6 closed umbrella
+2602 FE0F ; fully-qualified # ☂️ E0.7 umbrella
+2602 ; unqualified # ☂ E0.7 umbrella
+2614 ; fully-qualified # ☔ E0.6 umbrella with rain drops
+26F1 FE0F ; fully-qualified # ⛱️ E0.7 umbrella on ground
+26F1 ; unqualified # ⛱ E0.7 umbrella on ground
+26A1 ; fully-qualified # ⚡ E0.6 high voltage
+2744 FE0F ; fully-qualified # ❄️ E0.6 snowflake
+2744 ; unqualified # ❄ E0.6 snowflake
+2603 FE0F ; fully-qualified # ☃️ E0.7 snowman
+2603 ; unqualified # ☃ E0.7 snowman
+26C4 ; fully-qualified # ⛄ E0.6 snowman without snow
+2604 FE0F ; fully-qualified # ☄️ E1.0 comet
+2604 ; unqualified # ☄ E1.0 comet
+1F525 ; fully-qualified # 🔥 E0.6 fire
+1F4A7 ; fully-qualified # 💧 E0.6 droplet
+1F30A ; fully-qualified # 🌊 E0.6 water wave
+
+# Travel & Places subtotal: 264
+# Travel & Places subtotal: 264 w/o modifiers
+
+# group: Activities
+
+# subgroup: event
+1F383 ; fully-qualified # 🎃 E0.6 jack-o-lantern
+1F384 ; fully-qualified # 🎄 E0.6 Christmas tree
+1F386 ; fully-qualified # 🎆 E0.6 fireworks
+1F387 ; fully-qualified # 🎇 E0.6 sparkler
+1F9E8 ; fully-qualified # 🧨 E11.0 firecracker
+2728 ; fully-qualified # ✨ E0.6 sparkles
+1F388 ; fully-qualified # 🎈 E0.6 balloon
+1F389 ; fully-qualified # 🎉 E0.6 party popper
+1F38A ; fully-qualified # 🎊 E0.6 confetti ball
+1F38B ; fully-qualified # 🎋 E0.6 tanabata tree
+1F38D ; fully-qualified # 🎍 E0.6 pine decoration
+1F38E ; fully-qualified # 🎎 E0.6 Japanese dolls
+1F38F ; fully-qualified # 🎏 E0.6 carp streamer
+1F390 ; fully-qualified # 🎐 E0.6 wind chime
+1F391 ; fully-qualified # 🎑 E0.6 moon viewing ceremony
+1F9E7 ; fully-qualified # 🧧 E11.0 red envelope
+1F380 ; fully-qualified # 🎀 E0.6 ribbon
+1F381 ; fully-qualified # 🎁 E0.6 wrapped gift
+1F397 FE0F ; fully-qualified # 🎗️ E0.7 reminder ribbon
+1F397 ; unqualified # 🎗 E0.7 reminder ribbon
+1F39F FE0F ; fully-qualified # 🎟️ E0.7 admission tickets
+1F39F ; unqualified # 🎟 E0.7 admission tickets
+1F3AB ; fully-qualified # 🎫 E0.6 ticket
+
+# subgroup: award-medal
+1F396 FE0F ; fully-qualified # 🎖️ E0.7 military medal
+1F396 ; unqualified # 🎖 E0.7 military medal
+1F3C6 ; fully-qualified # 🏆 E0.6 trophy
+1F3C5 ; fully-qualified # 🏅 E1.0 sports medal
+1F947 ; fully-qualified # 🥇 E3.0 1st place medal
+1F948 ; fully-qualified # 🥈 E3.0 2nd place medal
+1F949 ; fully-qualified # 🥉 E3.0 3rd place medal
+
+# subgroup: sport
+26BD ; fully-qualified # ⚽ E0.6 soccer ball
+26BE ; fully-qualified # ⚾ E0.6 baseball
+1F94E ; fully-qualified # 🥎 E11.0 softball
+1F3C0 ; fully-qualified # 🏀 E0.6 basketball
+1F3D0 ; fully-qualified # 🏐 E1.0 volleyball
+1F3C8 ; fully-qualified # 🏈 E0.6 american football
+1F3C9 ; fully-qualified # 🏉 E1.0 rugby football
+1F3BE ; fully-qualified # 🎾 E0.6 tennis
+1F94F ; fully-qualified # 🥏 E11.0 flying disc
+1F3B3 ; fully-qualified # 🎳 E0.6 bowling
+1F3CF ; fully-qualified # 🏏 E1.0 cricket game
+1F3D1 ; fully-qualified # 🏑 E1.0 field hockey
+1F3D2 ; fully-qualified # 🏒 E1.0 ice hockey
+1F94D ; fully-qualified # 🥍 E11.0 lacrosse
+1F3D3 ; fully-qualified # 🏓 E1.0 ping pong
+1F3F8 ; fully-qualified # 🏸 E1.0 badminton
+1F94A ; fully-qualified # 🥊 E3.0 boxing glove
+1F94B ; fully-qualified # 🥋 E3.0 martial arts uniform
+1F945 ; fully-qualified # 🥅 E3.0 goal net
+26F3 ; fully-qualified # ⛳ E0.6 flag in hole
+26F8 FE0F ; fully-qualified # ⛸️ E0.7 ice skate
+26F8 ; unqualified # ⛸ E0.7 ice skate
+1F3A3 ; fully-qualified # 🎣 E0.6 fishing pole
+1F93F ; fully-qualified # 🤿 E12.0 diving mask
+1F3BD ; fully-qualified # 🎽 E0.6 running shirt
+1F3BF ; fully-qualified # 🎿 E0.6 skis
+1F6F7 ; fully-qualified # 🛷 E5.0 sled
+1F94C ; fully-qualified # 🥌 E5.0 curling stone
+
+# subgroup: game
+1F3AF ; fully-qualified # 🎯 E0.6 bullseye
+1FA80 ; fully-qualified # 🪀 E12.0 yo-yo
+1FA81 ; fully-qualified # 🪁 E12.0 kite
+1F3B1 ; fully-qualified # 🎱 E0.6 pool 8 ball
+1F52E ; fully-qualified # 🔮 E0.6 crystal ball
+1FA84 ; fully-qualified # 🪄 E13.0 magic wand
+1F9FF ; fully-qualified # 🧿 E11.0 nazar amulet
+1F3AE ; fully-qualified # 🎮 E0.6 video game
+1F579 FE0F ; fully-qualified # 🕹️ E0.7 joystick
+1F579 ; unqualified # 🕹 E0.7 joystick
+1F3B0 ; fully-qualified # 🎰 E0.6 slot machine
+1F3B2 ; fully-qualified # 🎲 E0.6 game die
+1F9E9 ; fully-qualified # 🧩 E11.0 puzzle piece
+1F9F8 ; fully-qualified # 🧸 E11.0 teddy bear
+1FA85 ; fully-qualified # 🪅 E13.0 piñata
+1FA86 ; fully-qualified # 🪆 E13.0 nesting dolls
+2660 FE0F ; fully-qualified # ♠️ E0.6 spade suit
+2660 ; unqualified # ♠ E0.6 spade suit
+2665 FE0F ; fully-qualified # ♥️ E0.6 heart suit
+2665 ; unqualified # ♥ E0.6 heart suit
+2666 FE0F ; fully-qualified # ♦️ E0.6 diamond suit
+2666 ; unqualified # ♦ E0.6 diamond suit
+2663 FE0F ; fully-qualified # ♣️ E0.6 club suit
+2663 ; unqualified # ♣ E0.6 club suit
+265F FE0F ; fully-qualified # ♟️ E11.0 chess pawn
+265F ; unqualified # ♟ E11.0 chess pawn
+1F0CF ; fully-qualified # 🃏 E0.6 joker
+1F004 ; fully-qualified # 🀄 E0.6 mahjong red dragon
+1F3B4 ; fully-qualified # 🎴 E0.6 flower playing cards
+
+# subgroup: arts & crafts
+1F3AD ; fully-qualified # 🎭 E0.6 performing arts
+1F5BC FE0F ; fully-qualified # 🖼️ E0.7 framed picture
+1F5BC ; unqualified # 🖼 E0.7 framed picture
+1F3A8 ; fully-qualified # 🎨 E0.6 artist palette
+1F9F5 ; fully-qualified # 🧵 E11.0 thread
+1FAA1 ; fully-qualified # 🪡 E13.0 sewing needle
+1F9F6 ; fully-qualified # 🧶 E11.0 yarn
+1FAA2 ; fully-qualified # 🪢 E13.0 knot
+
+# Activities subtotal: 95
+# Activities subtotal: 95 w/o modifiers
+
+# group: Objects
+
+# subgroup: clothing
+1F453 ; fully-qualified # 👓 E0.6 glasses
+1F576 FE0F ; fully-qualified # 🕶️ E0.7 sunglasses
+1F576 ; unqualified # 🕶 E0.7 sunglasses
+1F97D ; fully-qualified # 🥽 E11.0 goggles
+1F97C ; fully-qualified # 🥼 E11.0 lab coat
+1F9BA ; fully-qualified # 🦺 E12.0 safety vest
+1F454 ; fully-qualified # 👔 E0.6 necktie
+1F455 ; fully-qualified # 👕 E0.6 t-shirt
+1F456 ; fully-qualified # 👖 E0.6 jeans
+1F9E3 ; fully-qualified # 🧣 E5.0 scarf
+1F9E4 ; fully-qualified # 🧤 E5.0 gloves
+1F9E5 ; fully-qualified # 🧥 E5.0 coat
+1F9E6 ; fully-qualified # 🧦 E5.0 socks
+1F457 ; fully-qualified # 👗 E0.6 dress
+1F458 ; fully-qualified # 👘 E0.6 kimono
+1F97B ; fully-qualified # 🥻 E12.0 sari
+1FA71 ; fully-qualified # 🩱 E12.0 one-piece swimsuit
+1FA72 ; fully-qualified # 🩲 E12.0 briefs
+1FA73 ; fully-qualified # 🩳 E12.0 shorts
+1F459 ; fully-qualified # 👙 E0.6 bikini
+1F45A ; fully-qualified # 👚 E0.6 woman’s clothes
+1F45B ; fully-qualified # 👛 E0.6 purse
+1F45C ; fully-qualified # 👜 E0.6 handbag
+1F45D ; fully-qualified # 👝 E0.6 clutch bag
+1F6CD FE0F ; fully-qualified # 🛍️ E0.7 shopping bags
+1F6CD ; unqualified # 🛍 E0.7 shopping bags
+1F392 ; fully-qualified # 🎒 E0.6 backpack
+1FA74 ; fully-qualified # 🩴 E13.0 thong sandal
+1F45E ; fully-qualified # 👞 E0.6 man’s shoe
+1F45F ; fully-qualified # 👟 E0.6 running shoe
+1F97E ; fully-qualified # 🥾 E11.0 hiking boot
+1F97F ; fully-qualified # 🥿 E11.0 flat shoe
+1F460 ; fully-qualified # 👠 E0.6 high-heeled shoe
+1F461 ; fully-qualified # 👡 E0.6 woman’s sandal
+1FA70 ; fully-qualified # 🩰 E12.0 ballet shoes
+1F462 ; fully-qualified # 👢 E0.6 woman’s boot
+1F451 ; fully-qualified # 👑 E0.6 crown
+1F452 ; fully-qualified # 👒 E0.6 woman’s hat
+1F3A9 ; fully-qualified # 🎩 E0.6 top hat
+1F393 ; fully-qualified # 🎓 E0.6 graduation cap
+1F9E2 ; fully-qualified # 🧢 E5.0 billed cap
+1FA96 ; fully-qualified # 🪖 E13.0 military helmet
+26D1 FE0F ; fully-qualified # ⛑️ E0.7 rescue worker’s helmet
+26D1 ; unqualified # ⛑ E0.7 rescue worker’s helmet
+1F4FF ; fully-qualified # 📿 E1.0 prayer beads
+1F484 ; fully-qualified # 💄 E0.6 lipstick
+1F48D ; fully-qualified # 💍 E0.6 ring
+1F48E ; fully-qualified # 💎 E0.6 gem stone
+
+# subgroup: sound
+1F507 ; fully-qualified # 🔇 E1.0 muted speaker
+1F508 ; fully-qualified # 🔈 E0.7 speaker low volume
+1F509 ; fully-qualified # 🔉 E1.0 speaker medium volume
+1F50A ; fully-qualified # 🔊 E0.6 speaker high volume
+1F4E2 ; fully-qualified # 📢 E0.6 loudspeaker
+1F4E3 ; fully-qualified # 📣 E0.6 megaphone
+1F4EF ; fully-qualified # 📯 E1.0 postal horn
+1F514 ; fully-qualified # 🔔 E0.6 bell
+1F515 ; fully-qualified # 🔕 E1.0 bell with slash
+
+# subgroup: music
+1F3BC ; fully-qualified # 🎼 E0.6 musical score
+1F3B5 ; fully-qualified # 🎵 E0.6 musical note
+1F3B6 ; fully-qualified # 🎶 E0.6 musical notes
+1F399 FE0F ; fully-qualified # 🎙️ E0.7 studio microphone
+1F399 ; unqualified # 🎙 E0.7 studio microphone
+1F39A FE0F ; fully-qualified # 🎚️ E0.7 level slider
+1F39A ; unqualified # 🎚 E0.7 level slider
+1F39B FE0F ; fully-qualified # 🎛️ E0.7 control knobs
+1F39B ; unqualified # 🎛 E0.7 control knobs
+1F3A4 ; fully-qualified # 🎤 E0.6 microphone
+1F3A7 ; fully-qualified # 🎧 E0.6 headphone
+1F4FB ; fully-qualified # 📻 E0.6 radio
+
+# subgroup: musical-instrument
+1F3B7 ; fully-qualified # 🎷 E0.6 saxophone
+1FA97 ; fully-qualified # 🪗 E13.0 accordion
+1F3B8 ; fully-qualified # 🎸 E0.6 guitar
+1F3B9 ; fully-qualified # 🎹 E0.6 musical keyboard
+1F3BA ; fully-qualified # 🎺 E0.6 trumpet
+1F3BB ; fully-qualified # 🎻 E0.6 violin
+1FA95 ; fully-qualified # 🪕 E12.0 banjo
+1F941 ; fully-qualified # 🥁 E3.0 drum
+1FA98 ; fully-qualified # 🪘 E13.0 long drum
+
+# subgroup: phone
+1F4F1 ; fully-qualified # 📱 E0.6 mobile phone
+1F4F2 ; fully-qualified # 📲 E0.6 mobile phone with arrow
+260E FE0F ; fully-qualified # ☎️ E0.6 telephone
+260E ; unqualified # ☎ E0.6 telephone
+1F4DE ; fully-qualified # 📞 E0.6 telephone receiver
+1F4DF ; fully-qualified # 📟 E0.6 pager
+1F4E0 ; fully-qualified # 📠 E0.6 fax machine
+
+# subgroup: computer
+1F50B ; fully-qualified # 🔋 E0.6 battery
+1F50C ; fully-qualified # 🔌 E0.6 electric plug
+1F4BB ; fully-qualified # 💻 E0.6 laptop
+1F5A5 FE0F ; fully-qualified # 🖥️ E0.7 desktop computer
+1F5A5 ; unqualified # 🖥 E0.7 desktop computer
+1F5A8 FE0F ; fully-qualified # 🖨️ E0.7 printer
+1F5A8 ; unqualified # 🖨 E0.7 printer
+2328 FE0F ; fully-qualified # ⌨️ E1.0 keyboard
+2328 ; unqualified # ⌨ E1.0 keyboard
+1F5B1 FE0F ; fully-qualified # 🖱️ E0.7 computer mouse
+1F5B1 ; unqualified # 🖱 E0.7 computer mouse
+1F5B2 FE0F ; fully-qualified # 🖲️ E0.7 trackball
+1F5B2 ; unqualified # 🖲 E0.7 trackball
+1F4BD ; fully-qualified # 💽 E0.6 computer disk
+1F4BE ; fully-qualified # 💾 E0.6 floppy disk
+1F4BF ; fully-qualified # 💿 E0.6 optical disk
+1F4C0 ; fully-qualified # 📀 E0.6 dvd
+1F9EE ; fully-qualified # 🧮 E11.0 abacus
+
+# subgroup: light & video
+1F3A5 ; fully-qualified # 🎥 E0.6 movie camera
+1F39E FE0F ; fully-qualified # 🎞️ E0.7 film frames
+1F39E ; unqualified # 🎞 E0.7 film frames
+1F4FD FE0F ; fully-qualified # 📽️ E0.7 film projector
+1F4FD ; unqualified # 📽 E0.7 film projector
+1F3AC ; fully-qualified # 🎬 E0.6 clapper board
+1F4FA ; fully-qualified # 📺 E0.6 television
+1F4F7 ; fully-qualified # 📷 E0.6 camera
+1F4F8 ; fully-qualified # 📸 E1.0 camera with flash
+1F4F9 ; fully-qualified # 📹 E0.6 video camera
+1F4FC ; fully-qualified # 📼 E0.6 videocassette
+1F50D ; fully-qualified # 🔍 E0.6 magnifying glass tilted left
+1F50E ; fully-qualified # 🔎 E0.6 magnifying glass tilted right
+1F56F FE0F ; fully-qualified # 🕯️ E0.7 candle
+1F56F ; unqualified # 🕯 E0.7 candle
+1F4A1 ; fully-qualified # 💡 E0.6 light bulb
+1F526 ; fully-qualified # 🔦 E0.6 flashlight
+1F3EE ; fully-qualified # 🏮 E0.6 red paper lantern
+1FA94 ; fully-qualified # 🪔 E12.0 diya lamp
+
+# subgroup: book-paper
+1F4D4 ; fully-qualified # 📔 E0.6 notebook with decorative cover
+1F4D5 ; fully-qualified # 📕 E0.6 closed book
+1F4D6 ; fully-qualified # 📖 E0.6 open book
+1F4D7 ; fully-qualified # 📗 E0.6 green book
+1F4D8 ; fully-qualified # 📘 E0.6 blue book
+1F4D9 ; fully-qualified # 📙 E0.6 orange book
+1F4DA ; fully-qualified # 📚 E0.6 books
+1F4D3 ; fully-qualified # 📓 E0.6 notebook
+1F4D2 ; fully-qualified # 📒 E0.6 ledger
+1F4C3 ; fully-qualified # 📃 E0.6 page with curl
+1F4DC ; fully-qualified # 📜 E0.6 scroll
+1F4C4 ; fully-qualified # 📄 E0.6 page facing up
+1F4F0 ; fully-qualified # 📰 E0.6 newspaper
+1F5DE FE0F ; fully-qualified # 🗞️ E0.7 rolled-up newspaper
+1F5DE ; unqualified # 🗞 E0.7 rolled-up newspaper
+1F4D1 ; fully-qualified # 📑 E0.6 bookmark tabs
+1F516 ; fully-qualified # 🔖 E0.6 bookmark
+1F3F7 FE0F ; fully-qualified # 🏷️ E0.7 label
+1F3F7 ; unqualified # 🏷 E0.7 label
+
+# subgroup: money
+1F4B0 ; fully-qualified # 💰 E0.6 money bag
+1FA99 ; fully-qualified # 🪙 E13.0 coin
+1F4B4 ; fully-qualified # 💴 E0.6 yen banknote
+1F4B5 ; fully-qualified # 💵 E0.6 dollar banknote
+1F4B6 ; fully-qualified # 💶 E1.0 euro banknote
+1F4B7 ; fully-qualified # 💷 E1.0 pound banknote
+1F4B8 ; fully-qualified # 💸 E0.6 money with wings
+1F4B3 ; fully-qualified # 💳 E0.6 credit card
+1F9FE ; fully-qualified # 🧾 E11.0 receipt
+1F4B9 ; fully-qualified # 💹 E0.6 chart increasing with yen
+
+# subgroup: mail
+2709 FE0F ; fully-qualified # ✉️ E0.6 envelope
+2709 ; unqualified # ✉ E0.6 envelope
+1F4E7 ; fully-qualified # 📧 E0.6 e-mail
+1F4E8 ; fully-qualified # 📨 E0.6 incoming envelope
+1F4E9 ; fully-qualified # 📩 E0.6 envelope with arrow
+1F4E4 ; fully-qualified # 📤 E0.6 outbox tray
+1F4E5 ; fully-qualified # 📥 E0.6 inbox tray
+1F4E6 ; fully-qualified # 📦 E0.6 package
+1F4EB ; fully-qualified # 📫 E0.6 closed mailbox with raised flag
+1F4EA ; fully-qualified # 📪 E0.6 closed mailbox with lowered flag
+1F4EC ; fully-qualified # 📬 E0.7 open mailbox with raised flag
+1F4ED ; fully-qualified # 📭 E0.7 open mailbox with lowered flag
+1F4EE ; fully-qualified # 📮 E0.6 postbox
+1F5F3 FE0F ; fully-qualified # 🗳️ E0.7 ballot box with ballot
+1F5F3 ; unqualified # 🗳 E0.7 ballot box with ballot
+
+# subgroup: writing
+270F FE0F ; fully-qualified # ✏️ E0.6 pencil
+270F ; unqualified # ✏ E0.6 pencil
+2712 FE0F ; fully-qualified # ✒️ E0.6 black nib
+2712 ; unqualified # ✒ E0.6 black nib
+1F58B FE0F ; fully-qualified # 🖋️ E0.7 fountain pen
+1F58B ; unqualified # 🖋 E0.7 fountain pen
+1F58A FE0F ; fully-qualified # 🖊️ E0.7 pen
+1F58A ; unqualified # 🖊 E0.7 pen
+1F58C FE0F ; fully-qualified # 🖌️ E0.7 paintbrush
+1F58C ; unqualified # 🖌 E0.7 paintbrush
+1F58D FE0F ; fully-qualified # 🖍️ E0.7 crayon
+1F58D ; unqualified # 🖍 E0.7 crayon
+1F4DD ; fully-qualified # 📝 E0.6 memo
+
+# subgroup: office
+1F4BC ; fully-qualified # 💼 E0.6 briefcase
+1F4C1 ; fully-qualified # 📁 E0.6 file folder
+1F4C2 ; fully-qualified # 📂 E0.6 open file folder
+1F5C2 FE0F ; fully-qualified # 🗂️ E0.7 card index dividers
+1F5C2 ; unqualified # 🗂 E0.7 card index dividers
+1F4C5 ; fully-qualified # 📅 E0.6 calendar
+1F4C6 ; fully-qualified # 📆 E0.6 tear-off calendar
+1F5D2 FE0F ; fully-qualified # 🗒️ E0.7 spiral notepad
+1F5D2 ; unqualified # 🗒 E0.7 spiral notepad
+1F5D3 FE0F ; fully-qualified # 🗓️ E0.7 spiral calendar
+1F5D3 ; unqualified # 🗓 E0.7 spiral calendar
+1F4C7 ; fully-qualified # 📇 E0.6 card index
+1F4C8 ; fully-qualified # 📈 E0.6 chart increasing
+1F4C9 ; fully-qualified # 📉 E0.6 chart decreasing
+1F4CA ; fully-qualified # 📊 E0.6 bar chart
+1F4CB ; fully-qualified # 📋 E0.6 clipboard
+1F4CC ; fully-qualified # 📌 E0.6 pushpin
+1F4CD ; fully-qualified # 📍 E0.6 round pushpin
+1F4CE ; fully-qualified # 📎 E0.6 paperclip
+1F587 FE0F ; fully-qualified # 🖇️ E0.7 linked paperclips
+1F587 ; unqualified # 🖇 E0.7 linked paperclips
+1F4CF ; fully-qualified # 📏 E0.6 straight ruler
+1F4D0 ; fully-qualified # 📐 E0.6 triangular ruler
+2702 FE0F ; fully-qualified # ✂️ E0.6 scissors
+2702 ; unqualified # ✂ E0.6 scissors
+1F5C3 FE0F ; fully-qualified # 🗃️ E0.7 card file box
+1F5C3 ; unqualified # 🗃 E0.7 card file box
+1F5C4 FE0F ; fully-qualified # 🗄️ E0.7 file cabinet
+1F5C4 ; unqualified # 🗄 E0.7 file cabinet
+1F5D1 FE0F ; fully-qualified # 🗑️ E0.7 wastebasket
+1F5D1 ; unqualified # 🗑 E0.7 wastebasket
+
+# subgroup: lock
+1F512 ; fully-qualified # 🔒 E0.6 locked
+1F513 ; fully-qualified # 🔓 E0.6 unlocked
+1F50F ; fully-qualified # 🔏 E0.6 locked with pen
+1F510 ; fully-qualified # 🔐 E0.6 locked with key
+1F511 ; fully-qualified # 🔑 E0.6 key
+1F5DD FE0F ; fully-qualified # 🗝️ E0.7 old key
+1F5DD ; unqualified # 🗝 E0.7 old key
+
+# subgroup: tool
+1F528 ; fully-qualified # 🔨 E0.6 hammer
+1FA93 ; fully-qualified # 🪓 E12.0 axe
+26CF FE0F ; fully-qualified # ⛏️ E0.7 pick
+26CF ; unqualified # ⛏ E0.7 pick
+2692 FE0F ; fully-qualified # ⚒️ E1.0 hammer and pick
+2692 ; unqualified # ⚒ E1.0 hammer and pick
+1F6E0 FE0F ; fully-qualified # 🛠️ E0.7 hammer and wrench
+1F6E0 ; unqualified # 🛠 E0.7 hammer and wrench
+1F5E1 FE0F ; fully-qualified # 🗡️ E0.7 dagger
+1F5E1 ; unqualified # 🗡 E0.7 dagger
+2694 FE0F ; fully-qualified # ⚔️ E1.0 crossed swords
+2694 ; unqualified # ⚔ E1.0 crossed swords
+1F52B ; fully-qualified # 🔫 E0.6 water pistol
+1FA83 ; fully-qualified # 🪃 E13.0 boomerang
+1F3F9 ; fully-qualified # 🏹 E1.0 bow and arrow
+1F6E1 FE0F ; fully-qualified # 🛡️ E0.7 shield
+1F6E1 ; unqualified # 🛡 E0.7 shield
+1FA9A ; fully-qualified # 🪚 E13.0 carpentry saw
+1F527 ; fully-qualified # 🔧 E0.6 wrench
+1FA9B ; fully-qualified # 🪛 E13.0 screwdriver
+1F529 ; fully-qualified # 🔩 E0.6 nut and bolt
+2699 FE0F ; fully-qualified # ⚙️ E1.0 gear
+2699 ; unqualified # ⚙ E1.0 gear
+1F5DC FE0F ; fully-qualified # 🗜️ E0.7 clamp
+1F5DC ; unqualified # 🗜 E0.7 clamp
+2696 FE0F ; fully-qualified # ⚖️ E1.0 balance scale
+2696 ; unqualified # ⚖ E1.0 balance scale
+1F9AF ; fully-qualified # 🦯 E12.0 white cane
+1F517 ; fully-qualified # 🔗 E0.6 link
+26D3 FE0F ; fully-qualified # ⛓️ E0.7 chains
+26D3 ; unqualified # ⛓ E0.7 chains
+1FA9D ; fully-qualified # 🪝 E13.0 hook
+1F9F0 ; fully-qualified # 🧰 E11.0 toolbox
+1F9F2 ; fully-qualified # 🧲 E11.0 magnet
+1FA9C ; fully-qualified # 🪜 E13.0 ladder
+
+# subgroup: science
+2697 FE0F ; fully-qualified # ⚗️ E1.0 alembic
+2697 ; unqualified # ⚗ E1.0 alembic
+1F9EA ; fully-qualified # 🧪 E11.0 test tube
+1F9EB ; fully-qualified # 🧫 E11.0 petri dish
+1F9EC ; fully-qualified # 🧬 E11.0 dna
+1F52C ; fully-qualified # 🔬 E1.0 microscope
+1F52D ; fully-qualified # 🔭 E1.0 telescope
+1F4E1 ; fully-qualified # 📡 E0.6 satellite antenna
+
+# subgroup: medical
+1F489 ; fully-qualified # 💉 E0.6 syringe
+1FA78 ; fully-qualified # 🩸 E12.0 drop of blood
+1F48A ; fully-qualified # 💊 E0.6 pill
+1FA79 ; fully-qualified # 🩹 E12.0 adhesive bandage
+1FA7A ; fully-qualified # 🩺 E12.0 stethoscope
+
+# subgroup: household
+1F6AA ; fully-qualified # 🚪 E0.6 door
+1F6D7 ; fully-qualified # 🛗 E13.0 elevator
+1FA9E ; fully-qualified # 🪞 E13.0 mirror
+1FA9F ; fully-qualified # 🪟 E13.0 window
+1F6CF FE0F ; fully-qualified # 🛏️ E0.7 bed
+1F6CF ; unqualified # 🛏 E0.7 bed
+1F6CB FE0F ; fully-qualified # 🛋️ E0.7 couch and lamp
+1F6CB ; unqualified # 🛋 E0.7 couch and lamp
+1FA91 ; fully-qualified # 🪑 E12.0 chair
+1F6BD ; fully-qualified # 🚽 E0.6 toilet
+1FAA0 ; fully-qualified # 🪠 E13.0 plunger
+1F6BF ; fully-qualified # 🚿 E1.0 shower
+1F6C1 ; fully-qualified # 🛁 E1.0 bathtub
+1FAA4 ; fully-qualified # 🪤 E13.0 mouse trap
+1FA92 ; fully-qualified # 🪒 E12.0 razor
+1F9F4 ; fully-qualified # 🧴 E11.0 lotion bottle
+1F9F7 ; fully-qualified # 🧷 E11.0 safety pin
+1F9F9 ; fully-qualified # 🧹 E11.0 broom
+1F9FA ; fully-qualified # 🧺 E11.0 basket
+1F9FB ; fully-qualified # 🧻 E11.0 roll of paper
+1FAA3 ; fully-qualified # 🪣 E13.0 bucket
+1F9FC ; fully-qualified # 🧼 E11.0 soap
+1FAA5 ; fully-qualified # 🪥 E13.0 toothbrush
+1F9FD ; fully-qualified # 🧽 E11.0 sponge
+1F9EF ; fully-qualified # 🧯 E11.0 fire extinguisher
+1F6D2 ; fully-qualified # 🛒 E3.0 shopping cart
+
+# subgroup: other-object
+1F6AC ; fully-qualified # 🚬 E0.6 cigarette
+26B0 FE0F ; fully-qualified # ⚰️ E1.0 coffin
+26B0 ; unqualified # ⚰ E1.0 coffin
+1FAA6 ; fully-qualified # 🪦 E13.0 headstone
+26B1 FE0F ; fully-qualified # ⚱️ E1.0 funeral urn
+26B1 ; unqualified # ⚱ E1.0 funeral urn
+1F5FF ; fully-qualified # 🗿 E0.6 moai
+1FAA7 ; fully-qualified # 🪧 E13.0 placard
+
+# Objects subtotal: 299
+# Objects subtotal: 299 w/o modifiers
+
+# group: Symbols
+
+# subgroup: transport-sign
+1F3E7 ; fully-qualified # 🏧 E0.6 ATM sign
+1F6AE ; fully-qualified # 🚮 E1.0 litter in bin sign
+1F6B0 ; fully-qualified # 🚰 E1.0 potable water
+267F ; fully-qualified # ♿ E0.6 wheelchair symbol
+1F6B9 ; fully-qualified # 🚹 E0.6 men’s room
+1F6BA ; fully-qualified # 🚺 E0.6 women’s room
+1F6BB ; fully-qualified # 🚻 E0.6 restroom
+1F6BC ; fully-qualified # 🚼 E0.6 baby symbol
+1F6BE ; fully-qualified # 🚾 E0.6 water closet
+1F6C2 ; fully-qualified # 🛂 E1.0 passport control
+1F6C3 ; fully-qualified # 🛃 E1.0 customs
+1F6C4 ; fully-qualified # 🛄 E1.0 baggage claim
+1F6C5 ; fully-qualified # 🛅 E1.0 left luggage
+
+# subgroup: warning
+26A0 FE0F ; fully-qualified # ⚠️ E0.6 warning
+26A0 ; unqualified # ⚠ E0.6 warning
+1F6B8 ; fully-qualified # 🚸 E1.0 children crossing
+26D4 ; fully-qualified # ⛔ E0.6 no entry
+1F6AB ; fully-qualified # 🚫 E0.6 prohibited
+1F6B3 ; fully-qualified # 🚳 E1.0 no bicycles
+1F6AD ; fully-qualified # 🚭 E0.6 no smoking
+1F6AF ; fully-qualified # 🚯 E1.0 no littering
+1F6B1 ; fully-qualified # 🚱 E1.0 non-potable water
+1F6B7 ; fully-qualified # 🚷 E1.0 no pedestrians
+1F4F5 ; fully-qualified # 📵 E1.0 no mobile phones
+1F51E ; fully-qualified # 🔞 E0.6 no one under eighteen
+2622 FE0F ; fully-qualified # ☢️ E1.0 radioactive
+2622 ; unqualified # ☢ E1.0 radioactive
+2623 FE0F ; fully-qualified # ☣️ E1.0 biohazard
+2623 ; unqualified # ☣ E1.0 biohazard
+
+# subgroup: arrow
+2B06 FE0F ; fully-qualified # ⬆️ E0.6 up arrow
+2B06 ; unqualified # ⬆ E0.6 up arrow
+2197 FE0F ; fully-qualified # ↗️ E0.6 up-right arrow
+2197 ; unqualified # ↗ E0.6 up-right arrow
+27A1 FE0F ; fully-qualified # ➡️ E0.6 right arrow
+27A1 ; unqualified # ➡ E0.6 right arrow
+2198 FE0F ; fully-qualified # ↘️ E0.6 down-right arrow
+2198 ; unqualified # ↘ E0.6 down-right arrow
+2B07 FE0F ; fully-qualified # ⬇️ E0.6 down arrow
+2B07 ; unqualified # ⬇ E0.6 down arrow
+2199 FE0F ; fully-qualified # ↙️ E0.6 down-left arrow
+2199 ; unqualified # ↙ E0.6 down-left arrow
+2B05 FE0F ; fully-qualified # ⬅️ E0.6 left arrow
+2B05 ; unqualified # ⬅ E0.6 left arrow
+2196 FE0F ; fully-qualified # ↖️ E0.6 up-left arrow
+2196 ; unqualified # ↖ E0.6 up-left arrow
+2195 FE0F ; fully-qualified # ↕️ E0.6 up-down arrow
+2195 ; unqualified # ↕ E0.6 up-down arrow
+2194 FE0F ; fully-qualified # ↔️ E0.6 left-right arrow
+2194 ; unqualified # ↔ E0.6 left-right arrow
+21A9 FE0F ; fully-qualified # ↩️ E0.6 right arrow curving left
+21A9 ; unqualified # ↩ E0.6 right arrow curving left
+21AA FE0F ; fully-qualified # ↪️ E0.6 left arrow curving right
+21AA ; unqualified # ↪ E0.6 left arrow curving right
+2934 FE0F ; fully-qualified # ⤴️ E0.6 right arrow curving up
+2934 ; unqualified # ⤴ E0.6 right arrow curving up
+2935 FE0F ; fully-qualified # ⤵️ E0.6 right arrow curving down
+2935 ; unqualified # ⤵ E0.6 right arrow curving down
+1F503 ; fully-qualified # 🔃 E0.6 clockwise vertical arrows
+1F504 ; fully-qualified # 🔄 E1.0 counterclockwise arrows button
+1F519 ; fully-qualified # 🔙 E0.6 BACK arrow
+1F51A ; fully-qualified # 🔚 E0.6 END arrow
+1F51B ; fully-qualified # 🔛 E0.6 ON! arrow
+1F51C ; fully-qualified # 🔜 E0.6 SOON arrow
+1F51D ; fully-qualified # 🔝 E0.6 TOP arrow
+
+# subgroup: religion
+1F6D0 ; fully-qualified # 🛐 E1.0 place of worship
+269B FE0F ; fully-qualified # ⚛️ E1.0 atom symbol
+269B ; unqualified # ⚛ E1.0 atom symbol
+1F549 FE0F ; fully-qualified # 🕉️ E0.7 om
+1F549 ; unqualified # 🕉 E0.7 om
+2721 FE0F ; fully-qualified # ✡️ E0.7 star of David
+2721 ; unqualified # ✡ E0.7 star of David
+2638 FE0F ; fully-qualified # ☸️ E0.7 wheel of dharma
+2638 ; unqualified # ☸ E0.7 wheel of dharma
+262F FE0F ; fully-qualified # ☯️ E0.7 yin yang
+262F ; unqualified # ☯ E0.7 yin yang
+271D FE0F ; fully-qualified # ✝️ E0.7 latin cross
+271D ; unqualified # ✝ E0.7 latin cross
+2626 FE0F ; fully-qualified # ☦️ E1.0 orthodox cross
+2626 ; unqualified # ☦ E1.0 orthodox cross
+262A FE0F ; fully-qualified # ☪️ E0.7 star and crescent
+262A ; unqualified # ☪ E0.7 star and crescent
+262E FE0F ; fully-qualified # ☮️ E1.0 peace symbol
+262E ; unqualified # ☮ E1.0 peace symbol
+1F54E ; fully-qualified # 🕎 E1.0 menorah
+1F52F ; fully-qualified # 🔯 E0.6 dotted six-pointed star
+
+# subgroup: zodiac
+2648 ; fully-qualified # ♈ E0.6 Aries
+2649 ; fully-qualified # ♉ E0.6 Taurus
+264A ; fully-qualified # ♊ E0.6 Gemini
+264B ; fully-qualified # ♋ E0.6 Cancer
+264C ; fully-qualified # ♌ E0.6 Leo
+264D ; fully-qualified # ♍ E0.6 Virgo
+264E ; fully-qualified # ♎ E0.6 Libra
+264F ; fully-qualified # ♏ E0.6 Scorpio
+2650 ; fully-qualified # ♐ E0.6 Sagittarius
+2651 ; fully-qualified # ♑ E0.6 Capricorn
+2652 ; fully-qualified # ♒ E0.6 Aquarius
+2653 ; fully-qualified # ♓ E0.6 Pisces
+26CE ; fully-qualified # ⛎ E0.6 Ophiuchus
+
+# subgroup: av-symbol
+1F500 ; fully-qualified # 🔀 E1.0 shuffle tracks button
+1F501 ; fully-qualified # 🔁 E1.0 repeat button
+1F502 ; fully-qualified # 🔂 E1.0 repeat single button
+25B6 FE0F ; fully-qualified # ▶️ E0.6 play button
+25B6 ; unqualified # ▶ E0.6 play button
+23E9 ; fully-qualified # ⏩ E0.6 fast-forward button
+23ED FE0F ; fully-qualified # ⏭️ E0.7 next track button
+23ED ; unqualified # ⏭ E0.7 next track button
+23EF FE0F ; fully-qualified # ⏯️ E1.0 play or pause button
+23EF ; unqualified # ⏯ E1.0 play or pause button
+25C0 FE0F ; fully-qualified # ◀️ E0.6 reverse button
+25C0 ; unqualified # ◀ E0.6 reverse button
+23EA ; fully-qualified # ⏪ E0.6 fast reverse button
+23EE FE0F ; fully-qualified # ⏮️ E0.7 last track button
+23EE ; unqualified # ⏮ E0.7 last track button
+1F53C ; fully-qualified # 🔼 E0.6 upwards button
+23EB ; fully-qualified # ⏫ E0.6 fast up button
+1F53D ; fully-qualified # 🔽 E0.6 downwards button
+23EC ; fully-qualified # ⏬ E0.6 fast down button
+23F8 FE0F ; fully-qualified # ⏸️ E0.7 pause button
+23F8 ; unqualified # ⏸ E0.7 pause button
+23F9 FE0F ; fully-qualified # ⏹️ E0.7 stop button
+23F9 ; unqualified # ⏹ E0.7 stop button
+23FA FE0F ; fully-qualified # ⏺️ E0.7 record button
+23FA ; unqualified # ⏺ E0.7 record button
+23CF FE0F ; fully-qualified # ⏏️ E1.0 eject button
+23CF ; unqualified # ⏏ E1.0 eject button
+1F3A6 ; fully-qualified # 🎦 E0.6 cinema
+1F505 ; fully-qualified # 🔅 E1.0 dim button
+1F506 ; fully-qualified # 🔆 E1.0 bright button
+1F4F6 ; fully-qualified # 📶 E0.6 antenna bars
+1F4F3 ; fully-qualified # 📳 E0.6 vibration mode
+1F4F4 ; fully-qualified # 📴 E0.6 mobile phone off
+
+# subgroup: gender
+2640 FE0F ; fully-qualified # ♀️ E4.0 female sign
+2640 ; unqualified # ♀ E4.0 female sign
+2642 FE0F ; fully-qualified # ♂️ E4.0 male sign
+2642 ; unqualified # ♂ E4.0 male sign
+26A7 FE0F ; fully-qualified # ⚧️ E13.0 transgender symbol
+26A7 ; unqualified # ⚧ E13.0 transgender symbol
+
+# subgroup: math
+2716 FE0F ; fully-qualified # ✖️ E0.6 multiply
+2716 ; unqualified # ✖ E0.6 multiply
+2795 ; fully-qualified # ➕ E0.6 plus
+2796 ; fully-qualified # ➖ E0.6 minus
+2797 ; fully-qualified # ➗ E0.6 divide
+267E FE0F ; fully-qualified # ♾️ E11.0 infinity
+267E ; unqualified # ♾ E11.0 infinity
+
+# subgroup: punctuation
+203C FE0F ; fully-qualified # ‼️ E0.6 double exclamation mark
+203C ; unqualified # ‼ E0.6 double exclamation mark
+2049 FE0F ; fully-qualified # ⁉️ E0.6 exclamation question mark
+2049 ; unqualified # ⁉ E0.6 exclamation question mark
+2753 ; fully-qualified # ❓ E0.6 red question mark
+2754 ; fully-qualified # ❔ E0.6 white question mark
+2755 ; fully-qualified # ❕ E0.6 white exclamation mark
+2757 ; fully-qualified # ❗ E0.6 red exclamation mark
+3030 FE0F ; fully-qualified # 〰️ E0.6 wavy dash
+3030 ; unqualified # 〰 E0.6 wavy dash
+
+# subgroup: currency
+1F4B1 ; fully-qualified # 💱 E0.6 currency exchange
+1F4B2 ; fully-qualified # 💲 E0.6 heavy dollar sign
+
+# subgroup: other-symbol
+2695 FE0F ; fully-qualified # ⚕️ E4.0 medical symbol
+2695 ; unqualified # ⚕ E4.0 medical symbol
+267B FE0F ; fully-qualified # ♻️ E0.6 recycling symbol
+267B ; unqualified # ♻ E0.6 recycling symbol
+269C FE0F ; fully-qualified # ⚜️ E1.0 fleur-de-lis
+269C ; unqualified # ⚜ E1.0 fleur-de-lis
+1F531 ; fully-qualified # 🔱 E0.6 trident emblem
+1F4DB ; fully-qualified # 📛 E0.6 name badge
+1F530 ; fully-qualified # 🔰 E0.6 Japanese symbol for beginner
+2B55 ; fully-qualified # ⭕ E0.6 hollow red circle
+2705 ; fully-qualified # ✅ E0.6 check mark button
+2611 FE0F ; fully-qualified # ☑️ E0.6 check box with check
+2611 ; unqualified # ☑ E0.6 check box with check
+2714 FE0F ; fully-qualified # ✔️ E0.6 check mark
+2714 ; unqualified # ✔ E0.6 check mark
+274C ; fully-qualified # ❌ E0.6 cross mark
+274E ; fully-qualified # ❎ E0.6 cross mark button
+27B0 ; fully-qualified # ➰ E0.6 curly loop
+27BF ; fully-qualified # ➿ E1.0 double curly loop
+303D FE0F ; fully-qualified # 〽️ E0.6 part alternation mark
+303D ; unqualified # 〽 E0.6 part alternation mark
+2733 FE0F ; fully-qualified # ✳️ E0.6 eight-spoked asterisk
+2733 ; unqualified # ✳ E0.6 eight-spoked asterisk
+2734 FE0F ; fully-qualified # ✴️ E0.6 eight-pointed star
+2734 ; unqualified # ✴ E0.6 eight-pointed star
+2747 FE0F ; fully-qualified # ❇️ E0.6 sparkle
+2747 ; unqualified # ❇ E0.6 sparkle
+00A9 FE0F ; fully-qualified # ©️ E0.6 copyright
+00A9 ; unqualified # © E0.6 copyright
+00AE FE0F ; fully-qualified # ®️ E0.6 registered
+00AE ; unqualified # ® E0.6 registered
+2122 FE0F ; fully-qualified # ™️ E0.6 trade mark
+2122 ; unqualified # ™ E0.6 trade mark
+
+# subgroup: keycap
+0023 FE0F 20E3 ; fully-qualified # #️⃣ E0.6 keycap: #
+0023 20E3 ; unqualified # #⃣ E0.6 keycap: #
+002A FE0F 20E3 ; fully-qualified # *️⃣ E2.0 keycap: *
+002A 20E3 ; unqualified # *⃣ E2.0 keycap: *
+0030 FE0F 20E3 ; fully-qualified # 0️⃣ E0.6 keycap: 0
+0030 20E3 ; unqualified # 0⃣ E0.6 keycap: 0
+0031 FE0F 20E3 ; fully-qualified # 1️⃣ E0.6 keycap: 1
+0031 20E3 ; unqualified # 1⃣ E0.6 keycap: 1
+0032 FE0F 20E3 ; fully-qualified # 2️⃣ E0.6 keycap: 2
+0032 20E3 ; unqualified # 2⃣ E0.6 keycap: 2
+0033 FE0F 20E3 ; fully-qualified # 3️⃣ E0.6 keycap: 3
+0033 20E3 ; unqualified # 3⃣ E0.6 keycap: 3
+0034 FE0F 20E3 ; fully-qualified # 4️⃣ E0.6 keycap: 4
+0034 20E3 ; unqualified # 4⃣ E0.6 keycap: 4
+0035 FE0F 20E3 ; fully-qualified # 5️⃣ E0.6 keycap: 5
+0035 20E3 ; unqualified # 5⃣ E0.6 keycap: 5
+0036 FE0F 20E3 ; fully-qualified # 6️⃣ E0.6 keycap: 6
+0036 20E3 ; unqualified # 6⃣ E0.6 keycap: 6
+0037 FE0F 20E3 ; fully-qualified # 7️⃣ E0.6 keycap: 7
+0037 20E3 ; unqualified # 7⃣ E0.6 keycap: 7
+0038 FE0F 20E3 ; fully-qualified # 8️⃣ E0.6 keycap: 8
+0038 20E3 ; unqualified # 8⃣ E0.6 keycap: 8
+0039 FE0F 20E3 ; fully-qualified # 9️⃣ E0.6 keycap: 9
+0039 20E3 ; unqualified # 9⃣ E0.6 keycap: 9
+1F51F ; fully-qualified # 🔟 E0.6 keycap: 10
+
+# subgroup: alphanum
+1F520 ; fully-qualified # 🔠 E0.6 input latin uppercase
+1F521 ; fully-qualified # 🔡 E0.6 input latin lowercase
+1F522 ; fully-qualified # 🔢 E0.6 input numbers
+1F523 ; fully-qualified # 🔣 E0.6 input symbols
+1F524 ; fully-qualified # 🔤 E0.6 input latin letters
+1F170 FE0F ; fully-qualified # 🅰️ E0.6 A button (blood type)
+1F170 ; unqualified # 🅰 E0.6 A button (blood type)
+1F18E ; fully-qualified # 🆎 E0.6 AB button (blood type)
+1F171 FE0F ; fully-qualified # 🅱️ E0.6 B button (blood type)
+1F171 ; unqualified # 🅱 E0.6 B button (blood type)
+1F191 ; fully-qualified # 🆑 E0.6 CL button
+1F192 ; fully-qualified # 🆒 E0.6 COOL button
+1F193 ; fully-qualified # 🆓 E0.6 FREE button
+2139 FE0F ; fully-qualified # ℹ️ E0.6 information
+2139 ; unqualified # ℹ E0.6 information
+1F194 ; fully-qualified # 🆔 E0.6 ID button
+24C2 FE0F ; fully-qualified # Ⓜ️ E0.6 circled M
+24C2 ; unqualified # Ⓜ E0.6 circled M
+1F195 ; fully-qualified # 🆕 E0.6 NEW button
+1F196 ; fully-qualified # 🆖 E0.6 NG button
+1F17E FE0F ; fully-qualified # 🅾️ E0.6 O button (blood type)
+1F17E ; unqualified # 🅾 E0.6 O button (blood type)
+1F197 ; fully-qualified # 🆗 E0.6 OK button
+1F17F FE0F ; fully-qualified # 🅿️ E0.6 P button
+1F17F ; unqualified # 🅿 E0.6 P button
+1F198 ; fully-qualified # 🆘 E0.6 SOS button
+1F199 ; fully-qualified # 🆙 E0.6 UP! button
+1F19A ; fully-qualified # 🆚 E0.6 VS button
+1F201 ; fully-qualified # 🈁 E0.6 Japanese “here” button
+1F202 FE0F ; fully-qualified # 🈂️ E0.6 Japanese “service charge” button
+1F202 ; unqualified # 🈂 E0.6 Japanese “service charge” button
+1F237 FE0F ; fully-qualified # 🈷️ E0.6 Japanese “monthly amount” button
+1F237 ; unqualified # 🈷 E0.6 Japanese “monthly amount” button
+1F236 ; fully-qualified # 🈶 E0.6 Japanese “not free of charge” button
+1F22F ; fully-qualified # 🈯 E0.6 Japanese “reserved” button
+1F250 ; fully-qualified # 🉐 E0.6 Japanese “bargain” button
+1F239 ; fully-qualified # 🈹 E0.6 Japanese “discount” button
+1F21A ; fully-qualified # 🈚 E0.6 Japanese “free of charge” button
+1F232 ; fully-qualified # 🈲 E0.6 Japanese “prohibited” button
+1F251 ; fully-qualified # 🉑 E0.6 Japanese “acceptable” button
+1F238 ; fully-qualified # 🈸 E0.6 Japanese “application” button
+1F234 ; fully-qualified # 🈴 E0.6 Japanese “passing grade” button
+1F233 ; fully-qualified # 🈳 E0.6 Japanese “vacancy” button
+3297 FE0F ; fully-qualified # ㊗️ E0.6 Japanese “congratulations” button
+3297 ; unqualified # ㊗ E0.6 Japanese “congratulations” button
+3299 FE0F ; fully-qualified # ㊙️ E0.6 Japanese “secret” button
+3299 ; unqualified # ㊙ E0.6 Japanese “secret” button
+1F23A ; fully-qualified # 🈺 E0.6 Japanese “open for business” button
+1F235 ; fully-qualified # 🈵 E0.6 Japanese “no vacancy” button
+
+# subgroup: geometric
+1F534 ; fully-qualified # 🔴 E0.6 red circle
+1F7E0 ; fully-qualified # 🟠 E12.0 orange circle
+1F7E1 ; fully-qualified # 🟡 E12.0 yellow circle
+1F7E2 ; fully-qualified # 🟢 E12.0 green circle
+1F535 ; fully-qualified # 🔵 E0.6 blue circle
+1F7E3 ; fully-qualified # 🟣 E12.0 purple circle
+1F7E4 ; fully-qualified # 🟤 E12.0 brown circle
+26AB ; fully-qualified # ⚫ E0.6 black circle
+26AA ; fully-qualified # ⚪ E0.6 white circle
+1F7E5 ; fully-qualified # 🟥 E12.0 red square
+1F7E7 ; fully-qualified # 🟧 E12.0 orange square
+1F7E8 ; fully-qualified # 🟨 E12.0 yellow square
+1F7E9 ; fully-qualified # 🟩 E12.0 green square
+1F7E6 ; fully-qualified # 🟦 E12.0 blue square
+1F7EA ; fully-qualified # 🟪 E12.0 purple square
+1F7EB ; fully-qualified # 🟫 E12.0 brown square
+2B1B ; fully-qualified # ⬛ E0.6 black large square
+2B1C ; fully-qualified # ⬜ E0.6 white large square
+25FC FE0F ; fully-qualified # ◼️ E0.6 black medium square
+25FC ; unqualified # ◼ E0.6 black medium square
+25FB FE0F ; fully-qualified # ◻️ E0.6 white medium square
+25FB ; unqualified # ◻ E0.6 white medium square
+25FE ; fully-qualified # ◾ E0.6 black medium-small square
+25FD ; fully-qualified # ◽ E0.6 white medium-small square
+25AA FE0F ; fully-qualified # ▪️ E0.6 black small square
+25AA ; unqualified # ▪ E0.6 black small square
+25AB FE0F ; fully-qualified # ▫️ E0.6 white small square
+25AB ; unqualified # ▫ E0.6 white small square
+1F536 ; fully-qualified # 🔶 E0.6 large orange diamond
+1F537 ; fully-qualified # 🔷 E0.6 large blue diamond
+1F538 ; fully-qualified # 🔸 E0.6 small orange diamond
+1F539 ; fully-qualified # 🔹 E0.6 small blue diamond
+1F53A ; fully-qualified # 🔺 E0.6 red triangle pointed up
+1F53B ; fully-qualified # 🔻 E0.6 red triangle pointed down
+1F4A0 ; fully-qualified # 💠 E0.6 diamond with a dot
+1F518 ; fully-qualified # 🔘 E0.6 radio button
+1F533 ; fully-qualified # 🔳 E0.6 white square button
+1F532 ; fully-qualified # 🔲 E0.6 black square button
+
+# Symbols subtotal: 301
+# Symbols subtotal: 301 w/o modifiers
+
+# group: Flags
+
+# subgroup: flag
+1F3C1 ; fully-qualified # 🏁 E0.6 chequered flag
+1F6A9 ; fully-qualified # 🚩 E0.6 triangular flag
+1F38C ; fully-qualified # 🎌 E0.6 crossed flags
+1F3F4 ; fully-qualified # 🏴 E1.0 black flag
+1F3F3 FE0F ; fully-qualified # 🏳️ E0.7 white flag
+1F3F3 ; unqualified # 🏳 E0.7 white flag
+1F3F3 FE0F 200D 1F308 ; fully-qualified # 🏳️‍🌈 E4.0 rainbow flag
+1F3F3 200D 1F308 ; unqualified # 🏳‍🌈 E4.0 rainbow flag
+1F3F3 FE0F 200D 26A7 FE0F ; fully-qualified # 🏳️‍⚧️ E13.0 transgender flag
+1F3F3 200D 26A7 FE0F ; unqualified # 🏳‍⚧️ E13.0 transgender flag
+1F3F3 FE0F 200D 26A7 ; unqualified # 🏳️‍⚧ E13.0 transgender flag
+1F3F3 200D 26A7 ; unqualified # 🏳‍⚧ E13.0 transgender flag
+1F3F4 200D 2620 FE0F ; fully-qualified # 🏴‍☠️ E11.0 pirate flag
+1F3F4 200D 2620 ; minimally-qualified # 🏴‍☠ E11.0 pirate flag
+
+# subgroup: country-flag
+1F1E6 1F1E8 ; fully-qualified # 🇦🇨 E2.0 flag: Ascension Island
+1F1E6 1F1E9 ; fully-qualified # 🇦🇩 E2.0 flag: Andorra
+1F1E6 1F1EA ; fully-qualified # 🇦🇪 E2.0 flag: United Arab Emirates
+1F1E6 1F1EB ; fully-qualified # 🇦🇫 E2.0 flag: Afghanistan
+1F1E6 1F1EC ; fully-qualified # 🇦🇬 E2.0 flag: Antigua & Barbuda
+1F1E6 1F1EE ; fully-qualified # 🇦🇮 E2.0 flag: Anguilla
+1F1E6 1F1F1 ; fully-qualified # 🇦🇱 E2.0 flag: Albania
+1F1E6 1F1F2 ; fully-qualified # 🇦🇲 E2.0 flag: Armenia
+1F1E6 1F1F4 ; fully-qualified # 🇦🇴 E2.0 flag: Angola
+1F1E6 1F1F6 ; fully-qualified # 🇦🇶 E2.0 flag: Antarctica
+1F1E6 1F1F7 ; fully-qualified # 🇦🇷 E2.0 flag: Argentina
+1F1E6 1F1F8 ; fully-qualified # 🇦🇸 E2.0 flag: American Samoa
+1F1E6 1F1F9 ; fully-qualified # 🇦🇹 E2.0 flag: Austria
+1F1E6 1F1FA ; fully-qualified # 🇦🇺 E2.0 flag: Australia
+1F1E6 1F1FC ; fully-qualified # 🇦🇼 E2.0 flag: Aruba
+1F1E6 1F1FD ; fully-qualified # 🇦🇽 E2.0 flag: Åland Islands
+1F1E6 1F1FF ; fully-qualified # 🇦🇿 E2.0 flag: Azerbaijan
+1F1E7 1F1E6 ; fully-qualified # 🇧🇦 E2.0 flag: Bosnia & Herzegovina
+1F1E7 1F1E7 ; fully-qualified # 🇧🇧 E2.0 flag: Barbados
+1F1E7 1F1E9 ; fully-qualified # 🇧🇩 E2.0 flag: Bangladesh
+1F1E7 1F1EA ; fully-qualified # 🇧🇪 E2.0 flag: Belgium
+1F1E7 1F1EB ; fully-qualified # 🇧🇫 E2.0 flag: Burkina Faso
+1F1E7 1F1EC ; fully-qualified # 🇧🇬 E2.0 flag: Bulgaria
+1F1E7 1F1ED ; fully-qualified # 🇧🇭 E2.0 flag: Bahrain
+1F1E7 1F1EE ; fully-qualified # 🇧🇮 E2.0 flag: Burundi
+1F1E7 1F1EF ; fully-qualified # 🇧🇯 E2.0 flag: Benin
+1F1E7 1F1F1 ; fully-qualified # 🇧🇱 E2.0 flag: St. Barthélemy
+1F1E7 1F1F2 ; fully-qualified # 🇧🇲 E2.0 flag: Bermuda
+1F1E7 1F1F3 ; fully-qualified # 🇧🇳 E2.0 flag: Brunei
+1F1E7 1F1F4 ; fully-qualified # 🇧🇴 E2.0 flag: Bolivia
+1F1E7 1F1F6 ; fully-qualified # 🇧🇶 E2.0 flag: Caribbean Netherlands
+1F1E7 1F1F7 ; fully-qualified # 🇧🇷 E2.0 flag: Brazil
+1F1E7 1F1F8 ; fully-qualified # 🇧🇸 E2.0 flag: Bahamas
+1F1E7 1F1F9 ; fully-qualified # 🇧🇹 E2.0 flag: Bhutan
+1F1E7 1F1FB ; fully-qualified # 🇧🇻 E2.0 flag: Bouvet Island
+1F1E7 1F1FC ; fully-qualified # 🇧🇼 E2.0 flag: Botswana
+1F1E7 1F1FE ; fully-qualified # 🇧🇾 E2.0 flag: Belarus
+1F1E7 1F1FF ; fully-qualified # 🇧🇿 E2.0 flag: Belize
+1F1E8 1F1E6 ; fully-qualified # 🇨🇦 E2.0 flag: Canada
+1F1E8 1F1E8 ; fully-qualified # 🇨🇨 E2.0 flag: Cocos (Keeling) Islands
+1F1E8 1F1E9 ; fully-qualified # 🇨🇩 E2.0 flag: Congo - Kinshasa
+1F1E8 1F1EB ; fully-qualified # 🇨🇫 E2.0 flag: Central African Republic
+1F1E8 1F1EC ; fully-qualified # 🇨🇬 E2.0 flag: Congo - Brazzaville
+1F1E8 1F1ED ; fully-qualified # 🇨🇭 E2.0 flag: Switzerland
+1F1E8 1F1EE ; fully-qualified # 🇨🇮 E2.0 flag: Côte d’Ivoire
+1F1E8 1F1F0 ; fully-qualified # 🇨🇰 E2.0 flag: Cook Islands
+1F1E8 1F1F1 ; fully-qualified # 🇨🇱 E2.0 flag: Chile
+1F1E8 1F1F2 ; fully-qualified # 🇨🇲 E2.0 flag: Cameroon
+1F1E8 1F1F3 ; fully-qualified # 🇨🇳 E0.6 flag: China
+1F1E8 1F1F4 ; fully-qualified # 🇨🇴 E2.0 flag: Colombia
+1F1E8 1F1F5 ; fully-qualified # 🇨🇵 E2.0 flag: Clipperton Island
+1F1E8 1F1F7 ; fully-qualified # 🇨🇷 E2.0 flag: Costa Rica
+1F1E8 1F1FA ; fully-qualified # 🇨🇺 E2.0 flag: Cuba
+1F1E8 1F1FB ; fully-qualified # 🇨🇻 E2.0 flag: Cape Verde
+1F1E8 1F1FC ; fully-qualified # 🇨🇼 E2.0 flag: Curaçao
+1F1E8 1F1FD ; fully-qualified # 🇨🇽 E2.0 flag: Christmas Island
+1F1E8 1F1FE ; fully-qualified # 🇨🇾 E2.0 flag: Cyprus
+1F1E8 1F1FF ; fully-qualified # 🇨🇿 E2.0 flag: Czechia
+1F1E9 1F1EA ; fully-qualified # 🇩🇪 E0.6 flag: Germany
+1F1E9 1F1EC ; fully-qualified # 🇩🇬 E2.0 flag: Diego Garcia
+1F1E9 1F1EF ; fully-qualified # 🇩🇯 E2.0 flag: Djibouti
+1F1E9 1F1F0 ; fully-qualified # 🇩🇰 E2.0 flag: Denmark
+1F1E9 1F1F2 ; fully-qualified # 🇩🇲 E2.0 flag: Dominica
+1F1E9 1F1F4 ; fully-qualified # 🇩🇴 E2.0 flag: Dominican Republic
+1F1E9 1F1FF ; fully-qualified # 🇩🇿 E2.0 flag: Algeria
+1F1EA 1F1E6 ; fully-qualified # 🇪🇦 E2.0 flag: Ceuta & Melilla
+1F1EA 1F1E8 ; fully-qualified # 🇪🇨 E2.0 flag: Ecuador
+1F1EA 1F1EA ; fully-qualified # 🇪🇪 E2.0 flag: Estonia
+1F1EA 1F1EC ; fully-qualified # 🇪🇬 E2.0 flag: Egypt
+1F1EA 1F1ED ; fully-qualified # 🇪🇭 E2.0 flag: Western Sahara
+1F1EA 1F1F7 ; fully-qualified # 🇪🇷 E2.0 flag: Eritrea
+1F1EA 1F1F8 ; fully-qualified # 🇪🇸 E0.6 flag: Spain
+1F1EA 1F1F9 ; fully-qualified # 🇪🇹 E2.0 flag: Ethiopia
+1F1EA 1F1FA ; fully-qualified # 🇪🇺 E2.0 flag: European Union
+1F1EB 1F1EE ; fully-qualified # 🇫🇮 E2.0 flag: Finland
+1F1EB 1F1EF ; fully-qualified # 🇫🇯 E2.0 flag: Fiji
+1F1EB 1F1F0 ; fully-qualified # 🇫🇰 E2.0 flag: Falkland Islands
+1F1EB 1F1F2 ; fully-qualified # 🇫🇲 E2.0 flag: Micronesia
+1F1EB 1F1F4 ; fully-qualified # 🇫🇴 E2.0 flag: Faroe Islands
+1F1EB 1F1F7 ; fully-qualified # 🇫🇷 E0.6 flag: France
+1F1EC 1F1E6 ; fully-qualified # 🇬🇦 E2.0 flag: Gabon
+1F1EC 1F1E7 ; fully-qualified # 🇬🇧 E0.6 flag: United Kingdom
+1F1EC 1F1E9 ; fully-qualified # 🇬🇩 E2.0 flag: Grenada
+1F1EC 1F1EA ; fully-qualified # 🇬🇪 E2.0 flag: Georgia
+1F1EC 1F1EB ; fully-qualified # 🇬🇫 E2.0 flag: French Guiana
+1F1EC 1F1EC ; fully-qualified # 🇬🇬 E2.0 flag: Guernsey
+1F1EC 1F1ED ; fully-qualified # 🇬🇭 E2.0 flag: Ghana
+1F1EC 1F1EE ; fully-qualified # 🇬🇮 E2.0 flag: Gibraltar
+1F1EC 1F1F1 ; fully-qualified # 🇬🇱 E2.0 flag: Greenland
+1F1EC 1F1F2 ; fully-qualified # 🇬🇲 E2.0 flag: Gambia
+1F1EC 1F1F3 ; fully-qualified # 🇬🇳 E2.0 flag: Guinea
+1F1EC 1F1F5 ; fully-qualified # 🇬🇵 E2.0 flag: Guadeloupe
+1F1EC 1F1F6 ; fully-qualified # 🇬🇶 E2.0 flag: Equatorial Guinea
+1F1EC 1F1F7 ; fully-qualified # 🇬🇷 E2.0 flag: Greece
+1F1EC 1F1F8 ; fully-qualified # 🇬🇸 E2.0 flag: South Georgia & South Sandwich Islands
+1F1EC 1F1F9 ; fully-qualified # 🇬🇹 E2.0 flag: Guatemala
+1F1EC 1F1FA ; fully-qualified # 🇬🇺 E2.0 flag: Guam
+1F1EC 1F1FC ; fully-qualified # 🇬🇼 E2.0 flag: Guinea-Bissau
+1F1EC 1F1FE ; fully-qualified # 🇬🇾 E2.0 flag: Guyana
+1F1ED 1F1F0 ; fully-qualified # 🇭🇰 E2.0 flag: Hong Kong SAR China
+1F1ED 1F1F2 ; fully-qualified # 🇭🇲 E2.0 flag: Heard & McDonald Islands
+1F1ED 1F1F3 ; fully-qualified # 🇭🇳 E2.0 flag: Honduras
+1F1ED 1F1F7 ; fully-qualified # 🇭🇷 E2.0 flag: Croatia
+1F1ED 1F1F9 ; fully-qualified # 🇭🇹 E2.0 flag: Haiti
+1F1ED 1F1FA ; fully-qualified # 🇭🇺 E2.0 flag: Hungary
+1F1EE 1F1E8 ; fully-qualified # 🇮🇨 E2.0 flag: Canary Islands
+1F1EE 1F1E9 ; fully-qualified # 🇮🇩 E2.0 flag: Indonesia
+1F1EE 1F1EA ; fully-qualified # 🇮🇪 E2.0 flag: Ireland
+1F1EE 1F1F1 ; fully-qualified # 🇮🇱 E2.0 flag: Israel
+1F1EE 1F1F2 ; fully-qualified # 🇮🇲 E2.0 flag: Isle of Man
+1F1EE 1F1F3 ; fully-qualified # 🇮🇳 E2.0 flag: India
+1F1EE 1F1F4 ; fully-qualified # 🇮🇴 E2.0 flag: British Indian Ocean Territory
+1F1EE 1F1F6 ; fully-qualified # 🇮🇶 E2.0 flag: Iraq
+1F1EE 1F1F7 ; fully-qualified # 🇮🇷 E2.0 flag: Iran
+1F1EE 1F1F8 ; fully-qualified # 🇮🇸 E2.0 flag: Iceland
+1F1EE 1F1F9 ; fully-qualified # 🇮🇹 E0.6 flag: Italy
+1F1EF 1F1EA ; fully-qualified # 🇯🇪 E2.0 flag: Jersey
+1F1EF 1F1F2 ; fully-qualified # 🇯🇲 E2.0 flag: Jamaica
+1F1EF 1F1F4 ; fully-qualified # 🇯🇴 E2.0 flag: Jordan
+1F1EF 1F1F5 ; fully-qualified # 🇯🇵 E0.6 flag: Japan
+1F1F0 1F1EA ; fully-qualified # 🇰🇪 E2.0 flag: Kenya
+1F1F0 1F1EC ; fully-qualified # 🇰🇬 E2.0 flag: Kyrgyzstan
+1F1F0 1F1ED ; fully-qualified # 🇰🇭 E2.0 flag: Cambodia
+1F1F0 1F1EE ; fully-qualified # 🇰🇮 E2.0 flag: Kiribati
+1F1F0 1F1F2 ; fully-qualified # 🇰🇲 E2.0 flag: Comoros
+1F1F0 1F1F3 ; fully-qualified # 🇰🇳 E2.0 flag: St. Kitts & Nevis
+1F1F0 1F1F5 ; fully-qualified # 🇰🇵 E2.0 flag: North Korea
+1F1F0 1F1F7 ; fully-qualified # 🇰🇷 E0.6 flag: South Korea
+1F1F0 1F1FC ; fully-qualified # 🇰🇼 E2.0 flag: Kuwait
+1F1F0 1F1FE ; fully-qualified # 🇰🇾 E2.0 flag: Cayman Islands
+1F1F0 1F1FF ; fully-qualified # 🇰🇿 E2.0 flag: Kazakhstan
+1F1F1 1F1E6 ; fully-qualified # 🇱🇦 E2.0 flag: Laos
+1F1F1 1F1E7 ; fully-qualified # 🇱🇧 E2.0 flag: Lebanon
+1F1F1 1F1E8 ; fully-qualified # 🇱🇨 E2.0 flag: St. Lucia
+1F1F1 1F1EE ; fully-qualified # 🇱🇮 E2.0 flag: Liechtenstein
+1F1F1 1F1F0 ; fully-qualified # 🇱🇰 E2.0 flag: Sri Lanka
+1F1F1 1F1F7 ; fully-qualified # 🇱🇷 E2.0 flag: Liberia
+1F1F1 1F1F8 ; fully-qualified # 🇱🇸 E2.0 flag: Lesotho
+1F1F1 1F1F9 ; fully-qualified # 🇱🇹 E2.0 flag: Lithuania
+1F1F1 1F1FA ; fully-qualified # 🇱🇺 E2.0 flag: Luxembourg
+1F1F1 1F1FB ; fully-qualified # 🇱🇻 E2.0 flag: Latvia
+1F1F1 1F1FE ; fully-qualified # 🇱🇾 E2.0 flag: Libya
+1F1F2 1F1E6 ; fully-qualified # 🇲🇦 E2.0 flag: Morocco
+1F1F2 1F1E8 ; fully-qualified # 🇲🇨 E2.0 flag: Monaco
+1F1F2 1F1E9 ; fully-qualified # 🇲🇩 E2.0 flag: Moldova
+1F1F2 1F1EA ; fully-qualified # 🇲🇪 E2.0 flag: Montenegro
+1F1F2 1F1EB ; fully-qualified # 🇲🇫 E2.0 flag: St. Martin
+1F1F2 1F1EC ; fully-qualified # 🇲🇬 E2.0 flag: Madagascar
+1F1F2 1F1ED ; fully-qualified # 🇲🇭 E2.0 flag: Marshall Islands
+1F1F2 1F1F0 ; fully-qualified # 🇲🇰 E2.0 flag: North Macedonia
+1F1F2 1F1F1 ; fully-qualified # 🇲🇱 E2.0 flag: Mali
+1F1F2 1F1F2 ; fully-qualified # 🇲🇲 E2.0 flag: Myanmar (Burma)
+1F1F2 1F1F3 ; fully-qualified # 🇲🇳 E2.0 flag: Mongolia
+1F1F2 1F1F4 ; fully-qualified # 🇲🇴 E2.0 flag: Macao SAR China
+1F1F2 1F1F5 ; fully-qualified # 🇲🇵 E2.0 flag: Northern Mariana Islands
+1F1F2 1F1F6 ; fully-qualified # 🇲🇶 E2.0 flag: Martinique
+1F1F2 1F1F7 ; fully-qualified # 🇲🇷 E2.0 flag: Mauritania
+1F1F2 1F1F8 ; fully-qualified # 🇲🇸 E2.0 flag: Montserrat
+1F1F2 1F1F9 ; fully-qualified # 🇲🇹 E2.0 flag: Malta
+1F1F2 1F1FA ; fully-qualified # 🇲🇺 E2.0 flag: Mauritius
+1F1F2 1F1FB ; fully-qualified # 🇲🇻 E2.0 flag: Maldives
+1F1F2 1F1FC ; fully-qualified # 🇲🇼 E2.0 flag: Malawi
+1F1F2 1F1FD ; fully-qualified # 🇲🇽 E2.0 flag: Mexico
+1F1F2 1F1FE ; fully-qualified # 🇲🇾 E2.0 flag: Malaysia
+1F1F2 1F1FF ; fully-qualified # 🇲🇿 E2.0 flag: Mozambique
+1F1F3 1F1E6 ; fully-qualified # 🇳🇦 E2.0 flag: Namibia
+1F1F3 1F1E8 ; fully-qualified # 🇳🇨 E2.0 flag: New Caledonia
+1F1F3 1F1EA ; fully-qualified # 🇳🇪 E2.0 flag: Niger
+1F1F3 1F1EB ; fully-qualified # 🇳🇫 E2.0 flag: Norfolk Island
+1F1F3 1F1EC ; fully-qualified # 🇳🇬 E2.0 flag: Nigeria
+1F1F3 1F1EE ; fully-qualified # 🇳🇮 E2.0 flag: Nicaragua
+1F1F3 1F1F1 ; fully-qualified # 🇳🇱 E2.0 flag: Netherlands
+1F1F3 1F1F4 ; fully-qualified # 🇳🇴 E2.0 flag: Norway
+1F1F3 1F1F5 ; fully-qualified # 🇳🇵 E2.0 flag: Nepal
+1F1F3 1F1F7 ; fully-qualified # 🇳🇷 E2.0 flag: Nauru
+1F1F3 1F1FA ; fully-qualified # 🇳🇺 E2.0 flag: Niue
+1F1F3 1F1FF ; fully-qualified # 🇳🇿 E2.0 flag: New Zealand
+1F1F4 1F1F2 ; fully-qualified # 🇴🇲 E2.0 flag: Oman
+1F1F5 1F1E6 ; fully-qualified # 🇵🇦 E2.0 flag: Panama
+1F1F5 1F1EA ; fully-qualified # 🇵🇪 E2.0 flag: Peru
+1F1F5 1F1EB ; fully-qualified # 🇵🇫 E2.0 flag: French Polynesia
+1F1F5 1F1EC ; fully-qualified # 🇵🇬 E2.0 flag: Papua New Guinea
+1F1F5 1F1ED ; fully-qualified # 🇵🇭 E2.0 flag: Philippines
+1F1F5 1F1F0 ; fully-qualified # 🇵🇰 E2.0 flag: Pakistan
+1F1F5 1F1F1 ; fully-qualified # 🇵🇱 E2.0 flag: Poland
+1F1F5 1F1F2 ; fully-qualified # 🇵🇲 E2.0 flag: St. Pierre & Miquelon
+1F1F5 1F1F3 ; fully-qualified # 🇵🇳 E2.0 flag: Pitcairn Islands
+1F1F5 1F1F7 ; fully-qualified # 🇵🇷 E2.0 flag: Puerto Rico
+1F1F5 1F1F8 ; fully-qualified # 🇵🇸 E2.0 flag: Palestinian Territories
+1F1F5 1F1F9 ; fully-qualified # 🇵🇹 E2.0 flag: Portugal
+1F1F5 1F1FC ; fully-qualified # 🇵🇼 E2.0 flag: Palau
+1F1F5 1F1FE ; fully-qualified # 🇵🇾 E2.0 flag: Paraguay
+1F1F6 1F1E6 ; fully-qualified # 🇶🇦 E2.0 flag: Qatar
+1F1F7 1F1EA ; fully-qualified # 🇷🇪 E2.0 flag: Réunion
+1F1F7 1F1F4 ; fully-qualified # 🇷🇴 E2.0 flag: Romania
+1F1F7 1F1F8 ; fully-qualified # 🇷🇸 E2.0 flag: Serbia
+1F1F7 1F1FA ; fully-qualified # 🇷🇺 E0.6 flag: Russia
+1F1F7 1F1FC ; fully-qualified # 🇷🇼 E2.0 flag: Rwanda
+1F1F8 1F1E6 ; fully-qualified # 🇸🇦 E2.0 flag: Saudi Arabia
+1F1F8 1F1E7 ; fully-qualified # 🇸🇧 E2.0 flag: Solomon Islands
+1F1F8 1F1E8 ; fully-qualified # 🇸🇨 E2.0 flag: Seychelles
+1F1F8 1F1E9 ; fully-qualified # 🇸🇩 E2.0 flag: Sudan
+1F1F8 1F1EA ; fully-qualified # 🇸🇪 E2.0 flag: Sweden
+1F1F8 1F1EC ; fully-qualified # 🇸🇬 E2.0 flag: Singapore
+1F1F8 1F1ED ; fully-qualified # 🇸🇭 E2.0 flag: St. Helena
+1F1F8 1F1EE ; fully-qualified # 🇸🇮 E2.0 flag: Slovenia
+1F1F8 1F1EF ; fully-qualified # 🇸🇯 E2.0 flag: Svalbard & Jan Mayen
+1F1F8 1F1F0 ; fully-qualified # 🇸🇰 E2.0 flag: Slovakia
+1F1F8 1F1F1 ; fully-qualified # 🇸🇱 E2.0 flag: Sierra Leone
+1F1F8 1F1F2 ; fully-qualified # 🇸🇲 E2.0 flag: San Marino
+1F1F8 1F1F3 ; fully-qualified # 🇸🇳 E2.0 flag: Senegal
+1F1F8 1F1F4 ; fully-qualified # 🇸🇴 E2.0 flag: Somalia
+1F1F8 1F1F7 ; fully-qualified # 🇸🇷 E2.0 flag: Suriname
+1F1F8 1F1F8 ; fully-qualified # 🇸🇸 E2.0 flag: South Sudan
+1F1F8 1F1F9 ; fully-qualified # 🇸🇹 E2.0 flag: São Tomé & Príncipe
+1F1F8 1F1FB ; fully-qualified # 🇸🇻 E2.0 flag: El Salvador
+1F1F8 1F1FD ; fully-qualified # 🇸🇽 E2.0 flag: Sint Maarten
+1F1F8 1F1FE ; fully-qualified # 🇸🇾 E2.0 flag: Syria
+1F1F8 1F1FF ; fully-qualified # 🇸🇿 E2.0 flag: Eswatini
+1F1F9 1F1E6 ; fully-qualified # 🇹🇦 E2.0 flag: Tristan da Cunha
+1F1F9 1F1E8 ; fully-qualified # 🇹🇨 E2.0 flag: Turks & Caicos Islands
+1F1F9 1F1E9 ; fully-qualified # 🇹🇩 E2.0 flag: Chad
+1F1F9 1F1EB ; fully-qualified # 🇹🇫 E2.0 flag: French Southern Territories
+1F1F9 1F1EC ; fully-qualified # 🇹🇬 E2.0 flag: Togo
+1F1F9 1F1ED ; fully-qualified # 🇹🇭 E2.0 flag: Thailand
+1F1F9 1F1EF ; fully-qualified # 🇹🇯 E2.0 flag: Tajikistan
+1F1F9 1F1F0 ; fully-qualified # 🇹🇰 E2.0 flag: Tokelau
+1F1F9 1F1F1 ; fully-qualified # 🇹🇱 E2.0 flag: Timor-Leste
+1F1F9 1F1F2 ; fully-qualified # 🇹🇲 E2.0 flag: Turkmenistan
+1F1F9 1F1F3 ; fully-qualified # 🇹🇳 E2.0 flag: Tunisia
+1F1F9 1F1F4 ; fully-qualified # 🇹🇴 E2.0 flag: Tonga
+1F1F9 1F1F7 ; fully-qualified # 🇹🇷 E2.0 flag: Turkey
+1F1F9 1F1F9 ; fully-qualified # 🇹🇹 E2.0 flag: Trinidad & Tobago
+1F1F9 1F1FB ; fully-qualified # 🇹🇻 E2.0 flag: Tuvalu
+1F1F9 1F1FC ; fully-qualified # 🇹🇼 E2.0 flag: Taiwan
+1F1F9 1F1FF ; fully-qualified # 🇹🇿 E2.0 flag: Tanzania
+1F1FA 1F1E6 ; fully-qualified # 🇺🇦 E2.0 flag: Ukraine
+1F1FA 1F1EC ; fully-qualified # 🇺🇬 E2.0 flag: Uganda
+1F1FA 1F1F2 ; fully-qualified # 🇺🇲 E2.0 flag: U.S. Outlying Islands
+1F1FA 1F1F3 ; fully-qualified # 🇺🇳 E4.0 flag: United Nations
+1F1FA 1F1F8 ; fully-qualified # 🇺🇸 E0.6 flag: United States
+1F1FA 1F1FE ; fully-qualified # 🇺🇾 E2.0 flag: Uruguay
+1F1FA 1F1FF ; fully-qualified # 🇺🇿 E2.0 flag: Uzbekistan
+1F1FB 1F1E6 ; fully-qualified # 🇻🇦 E2.0 flag: Vatican City
+1F1FB 1F1E8 ; fully-qualified # 🇻🇨 E2.0 flag: St. Vincent & Grenadines
+1F1FB 1F1EA ; fully-qualified # 🇻🇪 E2.0 flag: Venezuela
+1F1FB 1F1EC ; fully-qualified # 🇻🇬 E2.0 flag: British Virgin Islands
+1F1FB 1F1EE ; fully-qualified # 🇻🇮 E2.0 flag: U.S. Virgin Islands
+1F1FB 1F1F3 ; fully-qualified # 🇻🇳 E2.0 flag: Vietnam
+1F1FB 1F1FA ; fully-qualified # 🇻🇺 E2.0 flag: Vanuatu
+1F1FC 1F1EB ; fully-qualified # 🇼🇫 E2.0 flag: Wallis & Futuna
+1F1FC 1F1F8 ; fully-qualified # 🇼🇸 E2.0 flag: Samoa
+1F1FD 1F1F0 ; fully-qualified # 🇽🇰 E2.0 flag: Kosovo
+1F1FE 1F1EA ; fully-qualified # 🇾🇪 E2.0 flag: Yemen
+1F1FE 1F1F9 ; fully-qualified # 🇾🇹 E2.0 flag: Mayotte
+1F1FF 1F1E6 ; fully-qualified # 🇿🇦 E2.0 flag: South Africa
+1F1FF 1F1F2 ; fully-qualified # 🇿🇲 E2.0 flag: Zambia
+1F1FF 1F1FC ; fully-qualified # 🇿🇼 E2.0 flag: Zimbabwe
+
+# subgroup: subdivision-flag
+1F3F4 E0067 E0062 E0065 E006E E0067 E007F ; fully-qualified # 🏴󠁧󠁢󠁥󠁮󠁧󠁿 E5.0 flag: England
+1F3F4 E0067 E0062 E0073 E0063 E0074 E007F ; fully-qualified # 🏴󠁧󠁢󠁳󠁣󠁴󠁿 E5.0 flag: Scotland
+1F3F4 E0067 E0062 E0077 E006C E0073 E007F ; fully-qualified # 🏴󠁧󠁢󠁷󠁬󠁳󠁿 E5.0 flag: Wales
+
+# Flags subtotal: 275
+# Flags subtotal: 275 w/o modifiers
+
+# Status Counts
+# fully-qualified : 3512
+# minimally-qualified : 817
+# unqualified : 252
+# component : 9
+
+#EOF
diff --git a/lib/pleroma/emoji.ex b/lib/pleroma/emoji.ex
index 04936155b..f077fe5b4 100644
--- a/lib/pleroma/emoji.ex
+++ b/lib/pleroma/emoji.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Emoji do
@@ -102,31 +102,36 @@ defmodule Pleroma.Emoji do
:ets.insert(@ets, emojis)
end
- @external_resource "lib/pleroma/emoji-data.txt"
+ @external_resource "lib/pleroma/emoji-test.txt"
+
+ regional_indicators =
+ Enum.map(127_462..127_487, fn codepoint ->
+ <<codepoint::utf8>>
+ end)
emojis =
@external_resource
|> File.read!()
|> String.split("\n")
- |> Enum.filter(fn line -> line != "" and not String.starts_with?(line, "#") end)
+ |> Enum.filter(fn line ->
+ line != "" and not String.starts_with?(line, "#") and
+ String.contains?(line, "fully-qualified")
+ end)
|> Enum.map(fn line ->
line
|> String.split(";", parts: 2)
|> hd()
|> String.trim()
- |> String.split("..")
- |> case do
- [number] ->
- <<String.to_integer(number, 16)::utf8>>
-
- [first, last] ->
- String.to_integer(first, 16)..String.to_integer(last, 16)
- |> Enum.map(&<<&1::utf8>>)
- end
+ |> String.split()
+ |> Enum.map(fn codepoint ->
+ <<String.to_integer(codepoint, 16)::utf8>>
+ end)
+ |> Enum.join()
end)
- |> List.flatten()
|> Enum.uniq()
+ emojis = emojis ++ regional_indicators
+
for emoji <- emojis do
def is_unicode_emoji?(unquote(emoji)), do: true
end
diff --git a/lib/pleroma/emoji/formatter.ex b/lib/pleroma/emoji/formatter.ex
index dc45b8a38..50150e951 100644
--- a/lib/pleroma/emoji/formatter.ex
+++ b/lib/pleroma/emoji/formatter.ex
@@ -1,10 +1,11 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Emoji.Formatter do
alias Pleroma.Emoji
alias Pleroma.HTML
+ alias Pleroma.Web
alias Pleroma.Web.MediaProxy
def emojify(text) do
@@ -43,7 +44,7 @@ defmodule Pleroma.Emoji.Formatter do
Emoji.get_all()
|> Enum.filter(fn {emoji, %Emoji{}} -> String.contains?(text, ":#{emoji}:") end)
|> Enum.reduce(%{}, fn {name, %Emoji{file: file}}, acc ->
- Map.put(acc, name, "#{Pleroma.Web.Endpoint.static_url()}#{file}")
+ Map.put(acc, name, to_string(URI.merge(Web.base_url(), file)))
end)
end
diff --git a/lib/pleroma/emoji/loader.ex b/lib/pleroma/emoji/loader.ex
index 03a6bca0b..67acd7069 100644
--- a/lib/pleroma/emoji/loader.ex
+++ b/lib/pleroma/emoji/loader.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Emoji.Loader do
@@ -15,6 +15,8 @@ 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())
@@ -77,10 +79,19 @@ defmodule Pleroma.Emoji.Loader do
# it should run even if there are no emoji packs
shortcode_globs = Config.get([:emoji, :shortcode_globs], [])
+ # for testing emoji.txt entries we do not want exposed in normal operation
+ test_emoji =
+ if @mix_env == :test do
+ load_from_file("test/config/emoji.txt", emoji_groups)
+ else
+ []
+ end
+
emojis_txt =
(load_from_file("config/emoji.txt", emoji_groups) ++
load_from_file("config/custom_emoji.txt", emoji_groups) ++
- load_from_globs(shortcode_globs, emoji_groups))
+ load_from_globs(shortcode_globs, emoji_groups) ++
+ test_emoji)
|> Enum.reject(fn value -> value == nil end)
Enum.map(emojis ++ emojis_txt, &prepare_emoji/1)
diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex
index ca58e5432..09bfcc868 100644
--- a/lib/pleroma/emoji/pack.ex
+++ b/lib/pleroma/emoji/pack.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Emoji.Pack do
@@ -20,16 +20,18 @@ defmodule Pleroma.Emoji.Pack do
name: String.t()
}
+ @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
+
alias Pleroma.Emoji
alias Pleroma.Emoji.Pack
+ alias Pleroma.Utils
@spec create(String.t()) :: {:ok, t()} | {:error, File.posix()} | {:error, :empty_values}
def create(name) do
with :ok <- validate_not_empty([name]),
dir <- Path.join(emoji_path(), name),
:ok <- File.mkdir(dir) do
- %__MODULE__{pack_file: Path.join(dir, "pack.json")}
- |> save_pack()
+ save_pack(%__MODULE__{pack_file: Path.join(dir, "pack.json")})
end
end
@@ -62,10 +64,9 @@ defmodule Pleroma.Emoji.Pack do
@spec delete(String.t()) ::
{:ok, [binary()]} | {:error, File.posix(), binary()} | {:error, :empty_values}
def delete(name) do
- with :ok <- validate_not_empty([name]) do
- emoji_path()
- |> Path.join(name)
- |> File.rm_rf()
+ with :ok <- validate_not_empty([name]),
+ pack_path <- Path.join(emoji_path(), name) do
+ File.rm_rf(pack_path)
end
end
@@ -94,7 +95,7 @@ defmodule Pleroma.Emoji.Pack do
def add_file(%Pack{} = pack, _, _, %Plug.Upload{content_type: "application/zip"} = file) do
with {:ok, zip_files} <- :zip.table(to_charlist(file.path)),
[_ | _] = emojies <- unpack_zip_emojies(zip_files),
- {:ok, tmp_dir} <- Pleroma.Utils.tmp_dir("emoji") do
+ {:ok, tmp_dir} <- Utils.tmp_dir("emoji") do
try do
{:ok, _emoji_files} =
:zip.unzip(
@@ -282,18 +283,21 @@ defmodule Pleroma.Emoji.Pack do
end
end
- @spec load_pack(String.t()) :: {:ok, t()} | {:error, :not_found}
+ @spec load_pack(String.t()) :: {:ok, t()} | {:error, :file.posix()}
def load_pack(name) do
pack_file = Path.join([emoji_path(), name, "pack.json"])
- if File.exists?(pack_file) do
+ with {:ok, _} <- File.stat(pack_file),
+ {:ok, pack_data} <- File.read(pack_file) do
pack =
- pack_file
- |> File.read!()
- |> from_json()
- |> Map.put(:pack_file, pack_file)
- |> Map.put(:path, Path.dirname(pack_file))
- |> Map.put(:name, name)
+ from_json(
+ pack_data,
+ %{
+ pack_file: pack_file,
+ path: Path.dirname(pack_file),
+ name: name
+ }
+ )
files_count =
pack.files
@@ -301,8 +305,6 @@ defmodule Pleroma.Emoji.Pack do
|> length()
{:ok, Map.put(pack, :files_count, files_count)}
- else
- {:error, :not_found}
end
end
@@ -415,7 +417,7 @@ defmodule Pleroma.Emoji.Pack do
ttl_per_file = Pleroma.Config.get!([:emoji, :shared_pack_cache_seconds_per_file])
overall_ttl = :timer.seconds(ttl_per_file * Enum.count(files))
- Cachex.put!(
+ @cachex.put(
:emoji_packs_cache,
pack.name,
# if pack.json MD5 changes, the cache is not valid anymore
@@ -434,10 +436,17 @@ defmodule Pleroma.Emoji.Pack do
end
end
- defp from_json(json) do
+ defp from_json(json, attrs) do
map = Jason.decode!(json)
- struct(__MODULE__, %{files: map["files"], pack: map["pack"]})
+ pack_attrs =
+ attrs
+ |> Map.merge(%{
+ files: map["files"],
+ pack: map["pack"]
+ })
+
+ struct(__MODULE__, pack_attrs)
end
defp validate_shareable_packs_available(uri) do
@@ -491,10 +500,10 @@ defmodule Pleroma.Emoji.Pack do
end
defp create_subdirs(file_path) do
- if String.contains?(file_path, "/") do
- file_path
- |> Path.dirname()
- |> File.mkdir_p!()
+ with true <- String.contains?(file_path, "/"),
+ path <- Path.dirname(file_path),
+ false <- File.exists?(path) do
+ File.mkdir_p!(path)
end
end
@@ -518,10 +527,15 @@ defmodule Pleroma.Emoji.Pack do
defp get_filename(pack, shortcode) do
with %{^shortcode => filename} when is_binary(filename) <- pack.files,
- true <- pack.path |> Path.join(filename) |> File.exists?() do
+ file_path <- Path.join(pack.path, filename),
+ {:ok, _} <- File.stat(file_path) do
{:ok, filename}
else
- _ -> {:error, :doesnt_exist}
+ {:error, _} = error ->
+ error
+
+ _ ->
+ {:error, :doesnt_exist}
end
end
@@ -606,7 +620,7 @@ defmodule Pleroma.Emoji.Pack do
defp fetch_archive(pack) do
hash = :crypto.hash(:md5, File.read!(pack.pack_file))
- case Cachex.get!(:emoji_packs_cache, pack.name) do
+ case @cachex.get!(:emoji_packs_cache, pack.name) do
%{hash: ^hash, pack_data: archive} -> archive
_ -> create_archive_and_cache(pack, hash)
end
diff --git a/lib/pleroma/filter.ex b/lib/pleroma/filter.ex
index 5d6df9530..82b9caf9b 100644
--- a/lib/pleroma/filter.ex
+++ b/lib/pleroma/filter.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Filter do
@@ -11,6 +11,9 @@ defmodule Pleroma.Filter do
alias Pleroma.Repo
alias Pleroma.User
+ @type t() :: %__MODULE__{}
+ @type format() :: :postgres | :re
+
schema "filters" do
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
field(:filter_id, :integer)
@@ -18,15 +21,16 @@ defmodule Pleroma.Filter do
field(:whole_word, :boolean, default: true)
field(:phrase, :string)
field(:context, {:array, :string})
- field(:expires_at, :utc_datetime)
+ field(:expires_at, :naive_datetime)
timestamps()
end
+ @spec get(integer() | String.t(), User.t()) :: t() | nil
def get(id, %{id: user_id} = _user) do
query =
from(
- f in Pleroma.Filter,
+ f in __MODULE__,
where: f.filter_id == ^id,
where: f.user_id == ^user_id
)
@@ -34,14 +38,17 @@ defmodule Pleroma.Filter do
Repo.one(query)
end
+ @spec get_active(Ecto.Query.t() | module()) :: Ecto.Query.t()
def get_active(query) do
from(f in query, where: is_nil(f.expires_at) or f.expires_at > ^NaiveDateTime.utc_now())
end
+ @spec get_irreversible(Ecto.Query.t()) :: Ecto.Query.t()
def get_irreversible(query) do
from(f in query, where: f.hide)
end
+ @spec get_filters(Ecto.Query.t() | module(), User.t()) :: [t()]
def get_filters(query \\ __MODULE__, %User{id: user_id}) do
query =
from(
@@ -53,7 +60,32 @@ defmodule Pleroma.Filter do
Repo.all(query)
end
- def create(%Pleroma.Filter{user_id: user_id, filter_id: nil} = filter) do
+ @spec create(map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
+ def create(attrs \\ %{}) do
+ Repo.transaction(fn -> create_with_expiration(attrs) end)
+ end
+
+ defp create_with_expiration(attrs) do
+ with {:ok, filter} <- do_create(attrs),
+ {:ok, _} <- maybe_add_expiration_job(filter) do
+ filter
+ else
+ {:error, error} -> Repo.rollback(error)
+ end
+ end
+
+ defp do_create(attrs) do
+ %__MODULE__{}
+ |> cast(attrs, [:phrase, :context, :hide, :expires_at, :whole_word, :user_id, :filter_id])
+ |> maybe_add_filter_id()
+ |> validate_required([:phrase, :context, :user_id, :filter_id])
+ |> maybe_add_expires_at(attrs)
+ |> Repo.insert()
+ end
+
+ defp maybe_add_filter_id(%{changes: %{filter_id: _}} = changeset), do: changeset
+
+ defp maybe_add_filter_id(%{changes: %{user_id: user_id}} = changeset) do
# If filter_id wasn't given, use the max filter_id for this user plus 1.
# XXX This could result in a race condition if a user tries to add two
# different filters for their account from two different clients at the
@@ -61,7 +93,7 @@ defmodule Pleroma.Filter do
max_id_query =
from(
- f in Pleroma.Filter,
+ f in __MODULE__,
where: f.user_id == ^user_id,
select: max(f.filter_id)
)
@@ -76,34 +108,92 @@ defmodule Pleroma.Filter do
max_id + 1
end
- filter
- |> Map.put(:filter_id, filter_id)
- |> Repo.insert()
+ change(changeset, filter_id: filter_id)
+ end
+
+ # don't override expires_at, if passed expires_at and expires_in
+ defp maybe_add_expires_at(%{changes: %{expires_at: %NaiveDateTime{} = _}} = changeset, _) do
+ changeset
end
- def create(%Pleroma.Filter{} = filter) do
- Repo.insert(filter)
+ defp maybe_add_expires_at(changeset, %{expires_in: expires_in})
+ when is_integer(expires_in) and expires_in > 0 do
+ expires_at =
+ NaiveDateTime.utc_now()
+ |> NaiveDateTime.add(expires_in)
+ |> NaiveDateTime.truncate(:second)
+
+ change(changeset, expires_at: expires_at)
end
- def delete(%Pleroma.Filter{id: filter_key} = filter) when is_number(filter_key) do
- Repo.delete(filter)
+ defp maybe_add_expires_at(changeset, %{expires_in: nil}) do
+ change(changeset, expires_at: nil)
end
- def delete(%Pleroma.Filter{id: filter_key} = filter) when is_nil(filter_key) do
- %Pleroma.Filter{id: id} = get(filter.filter_id, %{id: filter.user_id})
+ defp maybe_add_expires_at(changeset, _), do: changeset
- filter
- |> Map.put(:id, id)
- |> Repo.delete()
+ defp maybe_add_expiration_job(%{expires_at: %NaiveDateTime{} = expires_at} = filter) do
+ Pleroma.Workers.PurgeExpiredFilter.enqueue(%{
+ filter_id: filter.id,
+ expires_at: DateTime.from_naive!(expires_at, "Etc/UTC")
+ })
end
- def update(%Pleroma.Filter{} = filter, params) do
+ defp maybe_add_expiration_job(_), do: {:ok, nil}
+
+ @spec delete(t()) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
+ def delete(%__MODULE__{} = filter) do
+ Repo.transaction(fn -> delete_with_expiration(filter) end)
+ end
+
+ defp delete_with_expiration(filter) do
+ with {:ok, _} <- maybe_delete_old_expiration_job(filter, nil),
+ {:ok, filter} <- Repo.delete(filter) do
+ filter
+ else
+ {:error, error} -> Repo.rollback(error)
+ end
+ end
+
+ @spec update(t(), map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
+ def update(%__MODULE__{} = filter, params) do
+ Repo.transaction(fn -> update_with_expiration(filter, params) end)
+ end
+
+ defp update_with_expiration(filter, params) do
+ with {:ok, updated} <- do_update(filter, params),
+ {:ok, _} <- maybe_delete_old_expiration_job(filter, updated),
+ {:ok, _} <-
+ maybe_add_expiration_job(updated) do
+ updated
+ else
+ {:error, error} -> Repo.rollback(error)
+ end
+ end
+
+ defp do_update(filter, params) do
filter
|> cast(params, [:phrase, :context, :hide, :expires_at, :whole_word])
|> validate_required([:phrase, :context])
+ |> maybe_add_expires_at(params)
|> Repo.update()
end
+ defp maybe_delete_old_expiration_job(%{expires_at: nil}, _), do: {:ok, nil}
+
+ defp maybe_delete_old_expiration_job(%{expires_at: expires_at}, %{expires_at: expires_at}) do
+ {:ok, nil}
+ end
+
+ defp maybe_delete_old_expiration_job(%{id: id}, _) do
+ with %Oban.Job{} = job <- Pleroma.Workers.PurgeExpiredFilter.get_expiration(id) do
+ Repo.delete(job)
+ else
+ nil -> {:ok, nil}
+ end
+ end
+
+ @spec compose_regex(User.t() | [t()], format()) :: String.t() | Regex.t() | nil
def compose_regex(user_or_filters, format \\ :postgres)
def compose_regex(%User{} = user, format) do
diff --git a/lib/pleroma/following_relationship.ex b/lib/pleroma/following_relationship.ex
index 2039a259d..a0c7e6e39 100644
--- a/lib/pleroma/following_relationship.ex
+++ b/lib/pleroma/following_relationship.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.FollowingRelationship do
@@ -62,23 +62,47 @@ defmodule Pleroma.FollowingRelationship do
follow(follower, following, state)
following_relationship ->
- following_relationship
- |> cast(%{state: state}, [:state])
- |> validate_required([:state])
- |> Repo.update()
+ with {:ok, _following_relationship} <-
+ following_relationship
+ |> cast(%{state: state}, [:state])
+ |> validate_required([:state])
+ |> Repo.update() do
+ after_update(state, follower, following)
+ end
end
end
def follow(%User{} = follower, %User{} = following, state \\ :follow_accept) do
- %__MODULE__{}
- |> changeset(%{follower: follower, following: following, state: state})
- |> Repo.insert(on_conflict: :nothing)
+ with {:ok, _following_relationship} <-
+ %__MODULE__{}
+ |> changeset(%{follower: follower, following: following, state: state})
+ |> Repo.insert(on_conflict: :nothing) do
+ after_update(state, follower, following)
+ end
end
def unfollow(%User{} = follower, %User{} = following) do
case get(follower, following) do
- %__MODULE__{} = following_relationship -> Repo.delete(following_relationship)
- _ -> {:ok, nil}
+ %__MODULE__{} = following_relationship ->
+ with {:ok, _following_relationship} <- Repo.delete(following_relationship) do
+ after_update(:unfollow, follower, following)
+ end
+
+ _ ->
+ {:ok, nil}
+ end
+ end
+
+ defp after_update(state, %User{} = follower, %User{} = following) do
+ with {:ok, following} <- User.update_follower_count(following),
+ {:ok, follower} <- User.update_following_count(follower) do
+ Pleroma.Web.Streamer.stream("follow_relationship", %{
+ state: state,
+ following: following,
+ follower: follower
+ })
+
+ {:ok, follower, following}
end
end
@@ -128,7 +152,7 @@ defmodule Pleroma.FollowingRelationship do
|> join(:inner, [r], f in assoc(r, :follower))
|> where([r], r.state == ^:follow_pending)
|> where([r], r.following_id == ^id)
- |> where([r, f], f.deactivated != true)
+ |> where([r, f], f.is_active == true)
|> select([r, f], f)
|> Repo.all()
end
diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex
index 0c450eae4..7a08e48a9 100644
--- a/lib/pleroma/formatter.ex
+++ b/lib/pleroma/formatter.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Formatter do
diff --git a/lib/pleroma/frontend.ex b/lib/pleroma/frontend.ex
new file mode 100644
index 000000000..34b7befb8
--- /dev/null
+++ b/lib/pleroma/frontend.ex
@@ -0,0 +1,110 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Frontend do
+ alias Pleroma.Config
+
+ require Logger
+
+ def install(name, opts \\ []) do
+ frontend_info = %{
+ "ref" => opts[:ref],
+ "build_url" => opts[:build_url],
+ "build_dir" => opts[:build_dir]
+ }
+
+ frontend_info =
+ [:frontends, :available, name]
+ |> Config.get(%{})
+ |> Map.merge(frontend_info, fn _key, config, cmd ->
+ # This only overrides things that are actually set
+ cmd || config
+ end)
+
+ ref = frontend_info["ref"]
+
+ unless ref do
+ raise "No ref given or configured"
+ end
+
+ dest = Path.join([dir(), name, ref])
+
+ label = "#{name} (#{ref})"
+ tmp_dir = Path.join(dir(), "tmp")
+
+ with {_, :ok} <-
+ {:download_or_unzip, download_or_unzip(frontend_info, tmp_dir, opts[:file])},
+ Logger.info("Installing #{label} to #{dest}"),
+ :ok <- install_frontend(frontend_info, tmp_dir, dest) do
+ File.rm_rf!(tmp_dir)
+ Logger.info("Frontend #{label} installed to #{dest}")
+ else
+ {:download_or_unzip, _} ->
+ Logger.info("Could not download or unzip the frontend")
+ {:error, "Could not download or unzip the frontend"}
+
+ _e ->
+ Logger.info("Could not install the frontend")
+ {:error, "Could not install the frontend"}
+ end
+ end
+
+ def dir(opts \\ []) do
+ if is_nil(opts[:static_dir]) do
+ Pleroma.Config.get!([:instance, :static_dir])
+ else
+ opts[:static_dir]
+ end
+ |> Path.join("frontends")
+ end
+
+ defp download_or_unzip(frontend_info, temp_dir, nil),
+ do: download_build(frontend_info, temp_dir)
+
+ defp download_or_unzip(_frontend_info, temp_dir, file) do
+ with {:ok, zip} <- File.read(Path.expand(file)) do
+ unzip(zip, temp_dir)
+ end
+ end
+
+ def unzip(zip, dest) do
+ with {:ok, unzipped} <- :zip.unzip(zip, [:memory]) do
+ File.rm_rf!(dest)
+ File.mkdir_p!(dest)
+
+ Enum.each(unzipped, fn {filename, data} ->
+ path = filename
+
+ new_file_path = Path.join(dest, path)
+
+ new_file_path
+ |> Path.dirname()
+ |> File.mkdir_p!()
+
+ File.write!(new_file_path, data)
+ end)
+ end
+ end
+
+ defp download_build(frontend_info, dest) do
+ Logger.info("Downloading pre-built bundle for #{frontend_info["name"]}")
+ url = String.replace(frontend_info["build_url"], "${ref}", frontend_info["ref"])
+
+ with {:ok, %{status: 200, body: zip_body}} <-
+ Pleroma.HTTP.get(url, [], pool: :media, recv_timeout: 120_000) do
+ unzip(zip_body, dest)
+ else
+ {:error, e} -> {:error, e}
+ e -> {:error, e}
+ end
+ end
+
+ defp install_frontend(frontend_info, source, dest) do
+ from = frontend_info["build_dir"] || "dist"
+ File.rm_rf!(dest)
+ File.mkdir_p!(dest)
+ File.cp_r!(Path.join([source, from]), dest)
+ :ok
+ end
+end
diff --git a/lib/pleroma/gopher/server.ex b/lib/pleroma/gopher/server.ex
index e9f54c4c0..1b85c49f5 100644
--- a/lib/pleroma/gopher/server.ex
+++ b/lib/pleroma/gopher/server.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Gopher.Server do
@@ -76,7 +76,7 @@ defmodule Pleroma.Gopher.Server.ProtocolHandler do
|> Enum.map(fn activity ->
user = User.get_cached_by_ap_id(activity.data["actor"])
- object = Object.normalize(activity)
+ object = Object.normalize(activity, fetch: false)
like_count = object.data["like_count"] || 0
announcement_count = object.data["announcement_count"] || 0
diff --git a/lib/pleroma/gun.ex b/lib/pleroma/gun.ex
index 4043e4880..f9c828fac 100644
--- a/lib/pleroma/gun.ex
+++ b/lib/pleroma/gun.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Gun do
diff --git a/lib/pleroma/gun/api.ex b/lib/pleroma/gun/api.ex
index 09be74392..24d542781 100644
--- a/lib/pleroma/gun/api.ex
+++ b/lib/pleroma/gun/api.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Gun.API do
diff --git a/lib/pleroma/gun/conn.ex b/lib/pleroma/gun/conn.ex
index 477e19c6e..a56625699 100644
--- a/lib/pleroma/gun/conn.ex
+++ b/lib/pleroma/gun/conn.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Gun.Conn do
diff --git a/lib/pleroma/gun/connection_pool.ex b/lib/pleroma/gun/connection_pool.ex
index e322f192a..f9fd77ade 100644
--- a/lib/pleroma/gun/connection_pool.ex
+++ b/lib/pleroma/gun/connection_pool.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Gun.ConnectionPool do
diff --git a/lib/pleroma/gun/connection_pool/reclaimer.ex b/lib/pleroma/gun/connection_pool/reclaimer.ex
index 241e8b04f..c37b62bf2 100644
--- a/lib/pleroma/gun/connection_pool/reclaimer.ex
+++ b/lib/pleroma/gun/connection_pool/reclaimer.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Gun.ConnectionPool.Reclaimer do
diff --git a/lib/pleroma/gun/connection_pool/worker.ex b/lib/pleroma/gun/connection_pool/worker.ex
index b71816bed..02bfff274 100644
--- a/lib/pleroma/gun/connection_pool/worker.ex
+++ b/lib/pleroma/gun/connection_pool/worker.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Gun.ConnectionPool.Worker do
diff --git a/lib/pleroma/gun/connection_pool/worker_supervisor.ex b/lib/pleroma/gun/connection_pool/worker_supervisor.ex
index 4c23bcbd9..016b675f4 100644
--- a/lib/pleroma/gun/connection_pool/worker_supervisor.ex
+++ b/lib/pleroma/gun/connection_pool/worker_supervisor.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Gun.ConnectionPool.WorkerSupervisor do
diff --git a/lib/pleroma/healthcheck.ex b/lib/pleroma/healthcheck.ex
index 92ce83cb7..c905bba3f 100644
--- a/lib/pleroma/healthcheck.ex
+++ b/lib/pleroma/healthcheck.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Healthcheck do
diff --git a/lib/pleroma/helpers/auth_helper.ex b/lib/pleroma/helpers/auth_helper.ex
new file mode 100644
index 000000000..13e4c8158
--- /dev/null
+++ b/lib/pleroma/helpers/auth_helper.ex
@@ -0,0 +1,46 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Helpers.AuthHelper do
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+ alias Plug.Conn
+
+ import Plug.Conn
+
+ @oauth_token_session_key :oauth_token
+
+ @doc """
+ Skips OAuth permissions (scopes) checks, assigns nil `:token`.
+ Intended to be used with explicit authentication and only when OAuth token cannot be determined.
+ """
+ def skip_oauth(conn) do
+ conn
+ |> assign(:token, nil)
+ |> OAuthScopesPlug.skip_plug()
+ end
+
+ @doc "Drops authentication info from connection"
+ def drop_auth_info(conn) do
+ # To simplify debugging, setting a private variable on `conn` if auth info is dropped
+ conn
+ |> assign(:user, nil)
+ |> assign(:token, nil)
+ |> put_private(:authentication_ignored, true)
+ end
+
+ @doc "Gets OAuth token string from session"
+ def get_session_token(%Conn{} = conn) do
+ get_session(conn, @oauth_token_session_key)
+ end
+
+ @doc "Updates OAuth token string in session"
+ def put_session_token(%Conn{} = conn, token) when is_binary(token) do
+ put_session(conn, @oauth_token_session_key, token)
+ end
+
+ @doc "Deletes OAuth token string from session"
+ def delete_session_token(%Conn{} = conn) do
+ delete_session(conn, @oauth_token_session_key)
+ end
+end
diff --git a/lib/pleroma/helpers/inet_helper.ex b/lib/pleroma/helpers/inet_helper.ex
index 126f82381..5acdfaed0 100644
--- a/lib/pleroma/helpers/inet_helper.ex
+++ b/lib/pleroma/helpers/inet_helper.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Helpers.InetHelper do
diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex
index 6b799173e..738adfcaa 100644
--- a/lib/pleroma/helpers/media_helper.ex
+++ b/lib/pleroma/helpers/media_helper.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Helpers.MediaHelper do
diff --git a/lib/pleroma/helpers/qt_fast_start.ex b/lib/pleroma/helpers/qt_fast_start.ex
index bb93224b5..c4d11b9dd 100644
--- a/lib/pleroma/helpers/qt_fast_start.ex
+++ b/lib/pleroma/helpers/qt_fast_start.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Helpers.QtFastStart do
diff --git a/lib/pleroma/helpers/uri_helper.ex b/lib/pleroma/helpers/uri_helper.ex
index f1301f055..8f6a664ad 100644
--- a/lib/pleroma/helpers/uri_helper.ex
+++ b/lib/pleroma/helpers/uri_helper.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Helpers.UriHelper do
diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex
index 43e9145be..2dfdca693 100644
--- a/lib/pleroma/html.ex
+++ b/lib/pleroma/html.ex
@@ -1,11 +1,13 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
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")
@@ -56,8 +58,8 @@ defmodule Pleroma.HTML do
) do
key = "#{key}#{generate_scrubber_signature(scrubbers)}|#{activity.id}"
- Cachex.fetch!(:scrubber_cache, key, fn _key ->
- object = Pleroma.Object.normalize(activity)
+ @cachex.fetch!(:scrubber_cache, key, fn _key ->
+ object = Pleroma.Object.normalize(activity, fetch: false)
ensure_scrubbed_html(content, scrubbers, object.data["fake"] || false, callback)
end)
end
@@ -105,7 +107,7 @@ defmodule Pleroma.HTML do
unless object.data["fake"] do
key = "URL|#{object.id}"
- Cachex.fetch!(:scrubber_cache, key, fn _key ->
+ @cachex.fetch!(:scrubber_cache, key, fn _key ->
{:commit, {:ok, extract_first_external_url(content)}}
end)
else
diff --git a/lib/pleroma/http.ex b/lib/pleroma/http.ex
index 052597191..07b3ab0ae 100644
--- a/lib/pleroma/http.ex
+++ b/lib/pleroma/http.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.HTTP do
diff --git a/lib/pleroma/http/adapter_helper.ex b/lib/pleroma/http/adapter_helper.ex
index 08b51578a..c667afd25 100644
--- a/lib/pleroma/http/adapter_helper.ex
+++ b/lib/pleroma/http/adapter_helper.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.HTTP.AdapterHelper do
diff --git a/lib/pleroma/http/adapter_helper/default.ex b/lib/pleroma/http/adapter_helper/default.ex
index 8567a616b..a1614b9c5 100644
--- a/lib/pleroma/http/adapter_helper/default.ex
+++ b/lib/pleroma/http/adapter_helper/default.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.HTTP.AdapterHelper.Default do
diff --git a/lib/pleroma/http/adapter_helper/gun.ex b/lib/pleroma/http/adapter_helper/gun.ex
index 1dbb71362..82c7fd654 100644
--- a/lib/pleroma/http/adapter_helper/gun.ex
+++ b/lib/pleroma/http/adapter_helper/gun.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.HTTP.AdapterHelper.Gun do
diff --git a/lib/pleroma/http/adapter_helper/hackney.ex b/lib/pleroma/http/adapter_helper/hackney.ex
index ff60513fd..fe3f91a72 100644
--- a/lib/pleroma/http/adapter_helper/hackney.ex
+++ b/lib/pleroma/http/adapter_helper/hackney.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.HTTP.AdapterHelper.Hackney do
diff --git a/lib/pleroma/http/ex_aws.ex b/lib/pleroma/http/ex_aws.ex
index 5cac3532f..283590b18 100644
--- a/lib/pleroma/http/ex_aws.ex
+++ b/lib/pleroma/http/ex_aws.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.HTTP.ExAws do
diff --git a/lib/pleroma/http/request.ex b/lib/pleroma/http/request.ex
index 761bd6ccf..d906024de 100644
--- a/lib/pleroma/http/request.ex
+++ b/lib/pleroma/http/request.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.HTTP.Request do
diff --git a/lib/pleroma/http/request_builder.ex b/lib/pleroma/http/request_builder.ex
index 8a44a001d..631c927af 100644
--- a/lib/pleroma/http/request_builder.ex
+++ b/lib/pleroma/http/request_builder.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.HTTP.RequestBuilder do
diff --git a/lib/pleroma/http/tzdata.ex b/lib/pleroma/http/tzdata.ex
index 09cfdadf7..77e1b537e 100644
--- a/lib/pleroma/http/tzdata.ex
+++ b/lib/pleroma/http/tzdata.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.HTTP.Tzdata do
diff --git a/lib/pleroma/http/web_push.ex b/lib/pleroma/http/web_push.ex
index 78148a12e..51f72fbf8 100644
--- a/lib/pleroma/http/web_push.ex
+++ b/lib/pleroma/http/web_push.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.HTTP.WebPush do
diff --git a/lib/pleroma/instances.ex b/lib/pleroma/instances.ex
index 557e8decf..80addcc52 100644
--- a/lib/pleroma/instances.ex
+++ b/lib/pleroma/instances.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Instances do
@@ -11,6 +11,7 @@ defmodule Pleroma.Instances do
defdelegate reachable?(url_or_host), to: @adapter
defdelegate set_reachable(url_or_host), to: @adapter
defdelegate set_unreachable(url_or_host, unreachable_since \\ nil), to: @adapter
+ defdelegate get_consistently_unreachable(), to: @adapter
def set_consistently_unreachable(url_or_host),
do: set_unreachable(url_or_host, reachability_datetime_threshold())
diff --git a/lib/pleroma/instances/instance.ex b/lib/pleroma/instances/instance.ex
index f0f601469..4d0e8034d 100644
--- a/lib/pleroma/instances/instance.ex
+++ b/lib/pleroma/instances/instance.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Instances.Instance do
@@ -77,7 +77,7 @@ defmodule Pleroma.Instances.Instance do
)
end
- def reachable?(_), do: true
+ 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),
@@ -119,6 +119,17 @@ defmodule Pleroma.Instances.Instance do
def set_unreachable(_, _), do: {:error, nil}
+ def get_consistently_unreachable do
+ reachability_datetime_threshold = Instances.reachability_datetime_threshold()
+
+ from(i in Instance,
+ where: ^reachability_datetime_threshold > i.unreachable_since,
+ order_by: i.unreachable_since,
+ select: {i.host, i.unreachable_since}
+ )
+ |> Repo.all()
+ end
+
defp parse_datetime(datetime) when is_binary(datetime) do
NaiveDateTime.from_iso8601(datetime)
end
@@ -155,7 +166,8 @@ defmodule Pleroma.Instances.Instance do
defp scrape_favicon(%URI{} = instance_uri) do
try do
- with {:ok, %Tesla.Env{body: html}} <-
+ with {_, true} <- {:reachable, reachable?(instance_uri.host)},
+ {:ok, %Tesla.Env{body: html}} <-
Pleroma.HTTP.get(to_string(instance_uri), [{"accept", "text/html"}], pool: :media),
{_, [favicon_rel | _]} when is_binary(favicon_rel) <-
{:parse,
@@ -164,7 +176,15 @@ defmodule Pleroma.Instances.Instance do
{:merge, URI.merge(instance_uri, favicon_rel) |> to_string()} do
favicon
else
- _ -> nil
+ {:reachable, false} ->
+ Logger.debug(
+ "Instance.scrape_favicon(\"#{to_string(instance_uri)}\") ignored unreachable host"
+ )
+
+ nil
+
+ _ ->
+ nil
end
rescue
e ->
diff --git a/lib/pleroma/job_queue_monitor.ex b/lib/pleroma/job_queue_monitor.ex
index c255a61ec..b5f124923 100644
--- a/lib/pleroma/job_queue_monitor.ex
+++ b/lib/pleroma/job_queue_monitor.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.JobQueueMonitor do
diff --git a/lib/pleroma/jwt.ex b/lib/pleroma/jwt.ex
index faeb77781..c75c44bd1 100644
--- a/lib/pleroma/jwt.ex
+++ b/lib/pleroma/jwt.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.JWT do
diff --git a/lib/pleroma/keys.ex b/lib/pleroma/keys.ex
index c9af79f00..413861b15 100644
--- a/lib/pleroma/keys.ex
+++ b/lib/pleroma/keys.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Keys do
diff --git a/lib/pleroma/list.ex b/lib/pleroma/list.ex
index 89aa7b5d4..fe5721c34 100644
--- a/lib/pleroma/list.ex
+++ b/lib/pleroma/list.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.List do
@@ -113,11 +113,15 @@ defmodule Pleroma.List do
end
end
- def follow(%Pleroma.List{following: following} = list, %User{} = followed) do
+ def follow(%Pleroma.List{id: id}, %User{} = followed) do
+ list = Repo.get(Pleroma.List, id)
+ %{following: following} = list
update_follows(list, %{following: Enum.uniq([followed.follower_address | following])})
end
- def unfollow(%Pleroma.List{following: following} = list, %User{} = unfollowed) do
+ def unfollow(%Pleroma.List{id: id}, %User{} = unfollowed) do
+ list = Repo.get(Pleroma.List, id)
+ %{following: following} = list
update_follows(list, %{following: List.delete(following, unfollowed.follower_address)})
end
diff --git a/lib/pleroma/logging.ex b/lib/pleroma/logging.ex
new file mode 100644
index 000000000..11e1c3bed
--- /dev/null
+++ b/lib/pleroma/logging.ex
@@ -0,0 +1,7 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Logging do
+ @callback error(String.t()) :: any()
+end
diff --git a/lib/pleroma/maintenance.ex b/lib/pleroma/maintenance.ex
index 326c17825..f1058b68a 100644
--- a/lib/pleroma/maintenance.ex
+++ b/lib/pleroma/maintenance.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Maintenance do
diff --git a/lib/pleroma/maps.ex b/lib/pleroma/maps.ex
index ab2e32e2f..0d2e94248 100644
--- a/lib/pleroma/maps.ex
+++ b/lib/pleroma/maps.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Maps do
diff --git a/lib/pleroma/marker.ex b/lib/pleroma/marker.ex
index 4d82860f5..9909de161 100644
--- a/lib/pleroma/marker.ex
+++ b/lib/pleroma/marker.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Marker do
diff --git a/lib/pleroma/mfa.ex b/lib/pleroma/mfa.ex
index 01b743f4f..02dce7d49 100644
--- a/lib/pleroma/mfa.ex
+++ b/lib/pleroma/mfa.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.MFA do
@@ -71,7 +71,7 @@ defmodule Pleroma.MFA do
@spec generate_backup_codes(User.t()) :: {:ok, list(binary)} | {:error, String.t()}
def generate_backup_codes(%User{} = user) do
with codes <- BackupCodes.generate(),
- hashed_codes <- Enum.map(codes, &Pbkdf2.hash_pwd_salt/1),
+ hashed_codes <- Enum.map(codes, &Pleroma.Password.Pbkdf2.hash_pwd_salt/1),
changeset <- Changeset.cast_backup_codes(user, hashed_codes),
{:ok, _} <- User.update_and_set_cache(changeset) do
{:ok, codes}
diff --git a/lib/pleroma/mfa/backup_codes.ex b/lib/pleroma/mfa/backup_codes.ex
index 9875310ff..a7a1fba2e 100644
--- a/lib/pleroma/mfa/backup_codes.ex
+++ b/lib/pleroma/mfa/backup_codes.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.MFA.BackupCodes do
diff --git a/lib/pleroma/mfa/changeset.ex b/lib/pleroma/mfa/changeset.ex
index 77c4fa202..2d46cdf73 100644
--- a/lib/pleroma/mfa/changeset.ex
+++ b/lib/pleroma/mfa/changeset.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.MFA.Changeset do
diff --git a/lib/pleroma/mfa/settings.ex b/lib/pleroma/mfa/settings.ex
index de6e2228f..94fbff635 100644
--- a/lib/pleroma/mfa/settings.ex
+++ b/lib/pleroma/mfa/settings.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.MFA.Settings do
diff --git a/lib/pleroma/mfa/token.ex b/lib/pleroma/mfa/token.ex
index 69b64c0e8..76573182a 100644
--- a/lib/pleroma/mfa/token.ex
+++ b/lib/pleroma/mfa/token.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.MFA.Token do
diff --git a/lib/pleroma/mfa/totp.ex b/lib/pleroma/mfa/totp.ex
index d2ea2b3aa..f33e3a379 100644
--- a/lib/pleroma/mfa/totp.ex
+++ b/lib/pleroma/mfa/totp.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.MFA.TOTP do
diff --git a/lib/pleroma/migration_helper/notification_backfill.ex b/lib/pleroma/migration_helper/notification_backfill.ex
index 24f4733fe..62b710f82 100644
--- a/lib/pleroma/migration_helper/notification_backfill.ex
+++ b/lib/pleroma/migration_helper/notification_backfill.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.MigrationHelper.NotificationBackfill do
diff --git a/lib/pleroma/moderation_log.ex b/lib/pleroma/moderation_log.ex
index 38a863443..1849cacc8 100644
--- a/lib/pleroma/moderation_log.ex
+++ b/lib/pleroma/moderation_log.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ModerationLog do
@@ -12,6 +12,26 @@ defmodule Pleroma.ModerationLog do
import Ecto.Query
+ @type t :: %__MODULE__{}
+ @type log_subject :: Activity.t() | User.t() | list(User.t())
+ @type log_params :: %{
+ required(:actor) => User.t(),
+ required(:action) => String.t(),
+ optional(:subject) => log_subject(),
+ optional(:subject_actor) => User.t(),
+ optional(:subject_id) => String.t(),
+ optional(:subjects) => list(User.t()),
+ optional(:permission) => String.t(),
+ optional(:text) => String.t(),
+ optional(:sensitive) => String.t(),
+ optional(:visibility) => String.t(),
+ optional(:followed) => User.t(),
+ optional(:follower) => User.t(),
+ optional(:nicknames) => list(String.t()),
+ optional(:tags) => list(String.t()),
+ optional(:target) => String.t()
+ }
+
schema "moderation_log" do
field(:data, :map)
@@ -90,203 +110,105 @@ defmodule Pleroma.ModerationLog do
parsed_datetime
end
- @spec insert_log(%{actor: User, subject: [User], action: String.t(), permission: String.t()}) ::
- {:ok, ModerationLog} | {:error, any}
- def insert_log(%{
- actor: %User{} = actor,
- subject: subjects,
- action: action,
- permission: permission
- }) do
- %ModerationLog{
- data: %{
- "actor" => user_to_map(actor),
- "subject" => user_to_map(subjects),
- "action" => action,
- "permission" => permission,
- "message" => ""
- }
+ defp prepare_log_data(%{actor: actor, action: action} = attrs) do
+ %{
+ "actor" => user_to_map(actor),
+ "action" => action,
+ "message" => ""
}
- |> insert_log_entry_with_message()
- end
+ |> Pleroma.Maps.put_if_present("subject_actor", user_to_map(attrs[:subject_actor]))
+ end
+
+ defp prepare_log_data(attrs), do: attrs
+
+ @spec insert_log(log_params()) :: {:ok, ModerationLog} | {:error, any}
+ def insert_log(%{actor: %User{}, subject: subjects, permission: permission} = attrs) do
+ data =
+ attrs
+ |> prepare_log_data
+ |> Map.merge(%{"subject" => user_to_map(subjects), "permission" => permission})
+
+ insert_log_entry_with_message(%ModerationLog{data: data})
+ end
+
+ def insert_log(%{actor: %User{}, action: action, subject: %Activity{} = subject} = attrs)
+ when action in ["report_note_delete", "report_update", "report_note"] do
+ data =
+ attrs
+ |> prepare_log_data
+ |> Pleroma.Maps.put_if_present("text", attrs[:text])
+ |> Map.merge(%{"subject" => report_to_map(subject)})
+
+ insert_log_entry_with_message(%ModerationLog{data: data})
+ end
+
+ def insert_log(
+ %{
+ actor: %User{},
+ action: action,
+ subject: %Activity{} = subject,
+ sensitive: sensitive,
+ visibility: visibility
+ } = attrs
+ )
+ when action == "status_update" do
+ data =
+ attrs
+ |> prepare_log_data
+ |> Map.merge(%{
+ "subject" => status_to_map(subject),
+ "sensitive" => sensitive,
+ "visibility" => visibility
+ })
- @spec insert_log(%{actor: User, subject: User, action: String.t()}) ::
- {:ok, ModerationLog} | {:error, any}
- def insert_log(%{
- actor: %User{} = actor,
- action: "report_update",
- subject: %Activity{data: %{"type" => "Flag"}} = subject
- }) do
- %ModerationLog{
- data: %{
- "actor" => user_to_map(actor),
- "action" => "report_update",
- "subject" => report_to_map(subject),
- "message" => ""
- }
- }
- |> insert_log_entry_with_message()
+ insert_log_entry_with_message(%ModerationLog{data: data})
end
- @spec insert_log(%{actor: User, subject: Activity, action: String.t(), text: String.t()}) ::
- {:ok, ModerationLog} | {:error, any}
- def insert_log(%{
- actor: %User{} = actor,
- action: "report_note",
- subject: %Activity{} = subject,
- text: text
- }) do
- %ModerationLog{
- data: %{
- "actor" => user_to_map(actor),
- "action" => "report_note",
- "subject" => report_to_map(subject),
- "text" => text
- }
- }
- |> insert_log_entry_with_message()
- end
+ def insert_log(%{actor: %User{}, action: action, subject_id: subject_id} = attrs)
+ when action == "status_delete" do
+ data =
+ attrs
+ |> prepare_log_data
+ |> Map.merge(%{"subject_id" => subject_id})
- @spec insert_log(%{actor: User, subject: Activity, action: String.t(), text: String.t()}) ::
- {:ok, ModerationLog} | {:error, any}
- def insert_log(%{
- actor: %User{} = actor,
- action: "report_note_delete",
- subject: %Activity{} = subject,
- text: text
- }) do
- %ModerationLog{
- data: %{
- "actor" => user_to_map(actor),
- "action" => "report_note_delete",
- "subject" => report_to_map(subject),
- "text" => text
- }
- }
- |> insert_log_entry_with_message()
- end
-
- @spec insert_log(%{
- actor: User,
- subject: Activity,
- action: String.t(),
- sensitive: String.t(),
- visibility: String.t()
- }) :: {:ok, ModerationLog} | {:error, any}
- def insert_log(%{
- actor: %User{} = actor,
- action: "status_update",
- subject: %Activity{} = subject,
- sensitive: sensitive,
- visibility: visibility
- }) do
- %ModerationLog{
- data: %{
- "actor" => user_to_map(actor),
- "action" => "status_update",
- "subject" => status_to_map(subject),
- "sensitive" => sensitive,
- "visibility" => visibility,
- "message" => ""
- }
- }
- |> insert_log_entry_with_message()
+ insert_log_entry_with_message(%ModerationLog{data: data})
end
- @spec insert_log(%{actor: User, action: String.t(), subject_id: String.t()}) ::
- {:ok, ModerationLog} | {:error, any}
- def insert_log(%{
- actor: %User{} = actor,
- action: "status_delete",
- subject_id: subject_id
- }) do
- %ModerationLog{
- data: %{
- "actor" => user_to_map(actor),
- "action" => "status_delete",
- "subject_id" => subject_id,
- "message" => ""
- }
- }
- |> insert_log_entry_with_message()
- end
+ def insert_log(%{actor: %User{}, subject: subject, action: _action} = attrs) do
+ data =
+ attrs
+ |> prepare_log_data
+ |> Map.merge(%{"subject" => user_to_map(subject)})
- @spec insert_log(%{actor: User, subject: User, action: String.t()}) ::
- {:ok, ModerationLog} | {:error, any}
- def insert_log(%{actor: %User{} = actor, subject: subject, action: action}) do
- %ModerationLog{
- data: %{
- "actor" => user_to_map(actor),
- "action" => action,
- "subject" => user_to_map(subject),
- "message" => ""
- }
- }
- |> insert_log_entry_with_message()
+ insert_log_entry_with_message(%ModerationLog{data: data})
end
- @spec insert_log(%{actor: User, subjects: [User], action: String.t()}) ::
- {:ok, ModerationLog} | {:error, any}
- def insert_log(%{actor: %User{} = actor, subjects: subjects, action: action}) do
- subjects = Enum.map(subjects, &user_to_map/1)
+ def insert_log(%{actor: %User{}, subjects: subjects, action: _action} = attrs) do
+ data =
+ attrs
+ |> prepare_log_data
+ |> Map.merge(%{"subjects" => user_to_map(subjects)})
- %ModerationLog{
- data: %{
- "actor" => user_to_map(actor),
- "action" => action,
- "subjects" => subjects,
- "message" => ""
- }
- }
- |> insert_log_entry_with_message()
+ insert_log_entry_with_message(%ModerationLog{data: data})
end
- @spec insert_log(%{actor: User, action: String.t(), followed: User, follower: User}) ::
- {:ok, ModerationLog} | {:error, any}
- def insert_log(%{
- actor: %User{} = actor,
- followed: %User{} = followed,
- follower: %User{} = follower,
- action: "follow"
- }) do
- %ModerationLog{
- data: %{
- "actor" => user_to_map(actor),
- "action" => "follow",
- "followed" => user_to_map(followed),
- "follower" => user_to_map(follower),
- "message" => ""
- }
- }
- |> insert_log_entry_with_message()
- end
+ def insert_log(
+ %{
+ actor: %User{},
+ followed: %User{} = followed,
+ follower: %User{} = follower,
+ action: action
+ } = attrs
+ )
+ when action in ["unfollow", "follow"] do
+ data =
+ attrs
+ |> prepare_log_data
+ |> Map.merge(%{"followed" => user_to_map(followed), "follower" => user_to_map(follower)})
- @spec insert_log(%{actor: User, action: String.t(), followed: User, follower: User}) ::
- {:ok, ModerationLog} | {:error, any}
- def insert_log(%{
- actor: %User{} = actor,
- followed: %User{} = followed,
- follower: %User{} = follower,
- action: "unfollow"
- }) do
- %ModerationLog{
- data: %{
- "actor" => user_to_map(actor),
- "action" => "unfollow",
- "followed" => user_to_map(followed),
- "follower" => user_to_map(follower),
- "message" => ""
- }
- }
- |> insert_log_entry_with_message()
+ insert_log_entry_with_message(%ModerationLog{data: data})
end
- @spec insert_log(%{
- actor: User,
- action: String.t(),
- nicknames: [String.t()],
- tags: [String.t()]
- }) :: {:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
nicknames: nicknames,
@@ -305,27 +227,16 @@ defmodule Pleroma.ModerationLog do
|> insert_log_entry_with_message()
end
- @spec insert_log(%{actor: User, action: String.t(), target: String.t()}) ::
- {:ok, ModerationLog} | {:error, any}
- def insert_log(%{
- actor: %User{} = actor,
- action: action,
- target: target
- })
+ def insert_log(%{actor: %User{}, action: action, target: target} = attrs)
when action in ["relay_follow", "relay_unfollow"] do
- %ModerationLog{
- data: %{
- "actor" => user_to_map(actor),
- "action" => action,
- "target" => target,
- "message" => ""
- }
- }
- |> insert_log_entry_with_message()
+ data =
+ attrs
+ |> prepare_log_data
+ |> Map.merge(%{"target" => target})
+
+ insert_log_entry_with_message(%ModerationLog{data: data})
end
- @spec insert_log(%{actor: User, action: String.t(), subject_id: String.t()}) ::
- {:ok, ModerationLog} | {:error, any}
def insert_log(%{actor: %User{} = actor, action: "chat_message_delete", subject_id: subject_id}) do
%ModerationLog{
data: %{
@@ -345,32 +256,27 @@ defmodule Pleroma.ModerationLog do
end
defp user_to_map(users) when is_list(users) do
- users |> Enum.map(&user_to_map/1)
+ Enum.map(users, &user_to_map/1)
end
defp user_to_map(%User{} = user) do
user
- |> Map.from_struct()
|> Map.take([:id, :nickname])
|> Map.new(fn {k, v} -> {Atom.to_string(k), v} end)
|> Map.put("type", "user")
end
+ defp user_to_map(_), do: nil
+
defp report_to_map(%Activity{} = report) do
- %{
- "type" => "report",
- "id" => report.id,
- "state" => report.data["state"]
- }
+ %{"type" => "report", "id" => report.id, "state" => report.data["state"]}
end
defp status_to_map(%Activity{} = status) do
- %{
- "type" => "status",
- "id" => status.id
- }
+ %{"type" => "status", "id" => status.id}
end
+ @spec get_log_entry_message(ModerationLog.t()) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@@ -382,7 +288,6 @@ defmodule Pleroma.ModerationLog do
"@#{actor_nickname} made @#{follower_nickname} #{action} @#{followed_nickname}"
end
- @spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@@ -393,7 +298,6 @@ defmodule Pleroma.ModerationLog do
"@#{actor_nickname} deleted users: #{users_to_nicknames_string(subjects)}"
end
- @spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@@ -404,7 +308,6 @@ defmodule Pleroma.ModerationLog do
"@#{actor_nickname} created users: #{users_to_nicknames_string(subjects)}"
end
- @spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@@ -415,7 +318,6 @@ defmodule Pleroma.ModerationLog do
"@#{actor_nickname} activated users: #{users_to_nicknames_string(users)}"
end
- @spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@@ -426,7 +328,6 @@ defmodule Pleroma.ModerationLog do
"@#{actor_nickname} deactivated users: #{users_to_nicknames_string(users)}"
end
- @spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@@ -437,7 +338,6 @@ defmodule Pleroma.ModerationLog do
"@#{actor_nickname} approved users: #{users_to_nicknames_string(users)}"
end
- @spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@@ -451,7 +351,6 @@ defmodule Pleroma.ModerationLog do
"@#{actor_nickname} added tags: #{tags_string} to users: #{nicknames_to_string(nicknames)}"
end
- @spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@@ -465,7 +364,6 @@ defmodule Pleroma.ModerationLog do
"@#{actor_nickname} removed tags: #{tags_string} from users: #{nicknames_to_string(nicknames)}"
end
- @spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@@ -477,7 +375,6 @@ defmodule Pleroma.ModerationLog do
"@#{actor_nickname} made #{users_to_nicknames_string(users)} #{permission}"
end
- @spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@@ -489,7 +386,6 @@ defmodule Pleroma.ModerationLog do
"@#{actor_nickname} revoked #{permission} role from #{users_to_nicknames_string(users)}"
end
- @spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@@ -500,7 +396,6 @@ defmodule Pleroma.ModerationLog do
"@#{actor_nickname} followed relay: #{target}"
end
- @spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@@ -511,42 +406,48 @@ defmodule Pleroma.ModerationLog do
"@#{actor_nickname} unfollowed relay: #{target}"
end
- @spec get_log_entry_message(ModerationLog) :: String.t()
- def get_log_entry_message(%ModerationLog{
- data: %{
- "actor" => %{"nickname" => actor_nickname},
- "action" => "report_update",
- "subject" => %{"id" => subject_id, "state" => state, "type" => "report"}
- }
- }) do
- "@#{actor_nickname} updated report ##{subject_id} with '#{state}' state"
- end
-
- @spec get_log_entry_message(ModerationLog) :: String.t()
- def get_log_entry_message(%ModerationLog{
- data: %{
- "actor" => %{"nickname" => actor_nickname},
- "action" => "report_note",
- "subject" => %{"id" => subject_id, "type" => "report"},
- "text" => text
- }
- }) do
- "@#{actor_nickname} added note '#{text}' to report ##{subject_id}"
- end
-
- @spec get_log_entry_message(ModerationLog) :: String.t()
- def get_log_entry_message(%ModerationLog{
- data: %{
- "actor" => %{"nickname" => actor_nickname},
- "action" => "report_note_delete",
- "subject" => %{"id" => subject_id, "type" => "report"},
- "text" => text
- }
- }) do
- "@#{actor_nickname} deleted note '#{text}' from report ##{subject_id}"
+ def get_log_entry_message(
+ %ModerationLog{
+ data: %{
+ "actor" => %{"nickname" => actor_nickname},
+ "action" => "report_update",
+ "subject" => %{"id" => subject_id, "state" => state, "type" => "report"}
+ }
+ } = log
+ ) do
+ "@#{actor_nickname} updated report ##{subject_id}" <>
+ subject_actor_nickname(log, " (on user ", ")") <>
+ " with '#{state}' state"
+ end
+
+ def get_log_entry_message(
+ %ModerationLog{
+ data: %{
+ "actor" => %{"nickname" => actor_nickname},
+ "action" => "report_note",
+ "subject" => %{"id" => subject_id, "type" => "report"},
+ "text" => text
+ }
+ } = log
+ ) do
+ "@#{actor_nickname} added note '#{text}' to report ##{subject_id}" <>
+ subject_actor_nickname(log, " on user ")
+ end
+
+ def get_log_entry_message(
+ %ModerationLog{
+ data: %{
+ "actor" => %{"nickname" => actor_nickname},
+ "action" => "report_note_delete",
+ "subject" => %{"id" => subject_id, "type" => "report"},
+ "text" => text
+ }
+ } = log
+ ) do
+ "@#{actor_nickname} deleted note '#{text}' from report ##{subject_id}" <>
+ subject_actor_nickname(log, " on user ")
end
- @spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@@ -559,7 +460,6 @@ defmodule Pleroma.ModerationLog do
"@#{actor_nickname} updated status ##{subject_id}, set visibility: '#{visibility}'"
end
- @spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@@ -572,7 +472,6 @@ defmodule Pleroma.ModerationLog do
"@#{actor_nickname} updated status ##{subject_id}, set sensitive: '#{sensitive}'"
end
- @spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@@ -587,7 +486,6 @@ defmodule Pleroma.ModerationLog do
}'"
end
- @spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@@ -598,7 +496,6 @@ defmodule Pleroma.ModerationLog do
"@#{actor_nickname} deleted status ##{subject_id}"
end
- @spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@@ -609,7 +506,6 @@ defmodule Pleroma.ModerationLog do
"@#{actor_nickname} forced password reset for users: #{users_to_nicknames_string(subjects)}"
end
- @spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@@ -620,7 +516,6 @@ defmodule Pleroma.ModerationLog do
"@#{actor_nickname} confirmed email for users: #{users_to_nicknames_string(subjects)}"
end
- @spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@@ -633,7 +528,6 @@ defmodule Pleroma.ModerationLog do
}"
end
- @spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@@ -644,7 +538,6 @@ defmodule Pleroma.ModerationLog do
"@#{actor_nickname} updated users: #{users_to_nicknames_string(subjects)}"
end
- @spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
@@ -655,6 +548,16 @@ defmodule Pleroma.ModerationLog do
"@#{actor_nickname} deleted chat message ##{subject_id}"
end
+ def get_log_entry_message(%ModerationLog{
+ data: %{
+ "actor" => %{"nickname" => actor_nickname},
+ "action" => "create_backup",
+ "subject" => %{"nickname" => user_nickname}
+ }
+ }) do
+ "@#{actor_nickname} requested account backup for @#{user_nickname}"
+ end
+
defp nicknames_to_string(nicknames) do
nicknames
|> Enum.map(&"@#{&1}")
@@ -666,4 +569,16 @@ defmodule Pleroma.ModerationLog do
|> Enum.map(&"@#{&1["nickname"]}")
|> Enum.join(", ")
end
+
+ defp subject_actor_nickname(%ModerationLog{data: data}, prefix_msg, postfix_msg \\ "") do
+ case data do
+ %{"subject_actor" => %{"nickname" => subject_actor}} ->
+ [prefix_msg, "@#{subject_actor}", postfix_msg]
+ |> Enum.reject(&(&1 == ""))
+ |> Enum.join()
+
+ _ ->
+ ""
+ end
+ end
end
diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex
index 8868a910e..7efbdc49a 100644
--- a/lib/pleroma/notification.ex
+++ b/lib/pleroma/notification.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Notification do
@@ -70,6 +70,7 @@ defmodule Pleroma.Notification do
move
pleroma:chat_mention
pleroma:emoji_reaction
+ pleroma:report
reblog
}
@@ -111,13 +112,6 @@ defmodule Pleroma.Notification do
Notification
|> where(user_id: ^user.id)
- |> where(
- [n, a],
- fragment(
- "? not in (SELECT ap_id FROM users WHERE deactivated = 'true')",
- a.actor
- )
- )
|> join(:inner, [n], activity in assoc(n, :activity))
|> join(:left, [n, a], object in Object,
on:
@@ -128,7 +122,9 @@ defmodule Pleroma.Notification do
a.data
)
)
+ |> join(:inner, [_n, a], u in User, on: u.ap_id == a.actor, as: :user_actor)
|> preload([n, a, o], activity: {a, object: o})
+ |> where([user_actor: user_actor], user_actor.is_active)
|> exclude_notification_muted(user, exclude_notification_muted_opts)
|> exclude_blocked(user, exclude_blocked_opts)
|> exclude_filtered(user)
@@ -155,9 +151,10 @@ defmodule Pleroma.Notification do
query
|> where([n, a], a.actor 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)
+ on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data),
+ as: :thread_mute
)
- |> where([n, a, o, tm], is_nil(tm.user_id))
+ |> where([thread_mute: thread_mute], is_nil(thread_mute.user_id))
end
defp exclude_filtered(query, user) do
@@ -357,7 +354,7 @@ defmodule Pleroma.Notification do
def create_notifications(activity, options \\ [])
def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity, options) do
- object = Object.normalize(activity, false)
+ object = Object.normalize(activity, fetch: false)
if object && object.data["type"] == "Answer" do
{:ok, []}
@@ -367,7 +364,7 @@ defmodule Pleroma.Notification do
end
def create_notifications(%Activity{data: %{"type" => type}} = activity, options)
- when type in ["Follow", "Like", "Announce", "Move", "EmojiReact"] do
+ when type in ["Follow", "Like", "Announce", "Move", "EmojiReact", "Flag"] do
do_create_notifications(activity, options)
end
@@ -410,6 +407,9 @@ defmodule Pleroma.Notification do
"EmojiReact" ->
"pleroma:emoji_reaction"
+ "Flag" ->
+ "pleroma:report"
+
# Compatibility with old reactions
"EmojiReaction" ->
"pleroma:emoji_reaction"
@@ -467,7 +467,7 @@ defmodule Pleroma.Notification do
def get_notified_from_activity(activity, local_only \\ true)
def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only)
- when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReact"] do
+ when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReact", "Flag"] do
potential_receiver_ap_ids = get_potential_receiver_ap_ids(activity)
potential_receivers =
@@ -503,6 +503,10 @@ defmodule Pleroma.Notification do
[object_id]
end
+ def get_potential_receiver_ap_ids(%{data: %{"type" => "Flag", "actor" => actor}}) do
+ (User.all_superusers() |> Enum.map(fn user -> user.ap_id end)) -- [actor]
+ end
+
def get_potential_receiver_ap_ids(activity) do
[]
|> Utils.maybe_notify_to_recipients(activity)
@@ -617,7 +621,7 @@ defmodule Pleroma.Notification do
def skip?(:filtered, %{data: %{"type" => type}}, _) when type in ["Follow", "Move"], do: false
def skip?(:filtered, activity, user) do
- object = Object.normalize(activity)
+ object = Object.normalize(activity, fetch: false)
cond do
is_nil(object) ->
diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex
index 052ad413b..aaf123840 100644
--- a/lib/pleroma/object.ex
+++ b/lib/pleroma/object.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Object do
@@ -23,6 +23,8 @@ defmodule Pleroma.Object do
@derive {Jason.Encoder, only: [:data]}
+ @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
+
schema "objects" do
field(:data, :map)
@@ -106,39 +108,42 @@ defmodule Pleroma.Object do
Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}")
end
- def normalize(_, fetch_remote \\ true, options \\ [])
+ def normalize(_, options \\ [fetch: false])
# If we pass an Activity to Object.normalize(), we can try to use the preloaded object.
# Use this whenever possible, especially when walking graphs in an O(N) loop!
- def normalize(%Object{} = object, _, _), do: object
- def normalize(%Activity{object: %Object{} = object}, _, _), do: object
+ def normalize(%Object{} = object, _), do: object
+ def normalize(%Activity{object: %Object{} = object}, _), do: object
# A hack for fake activities
- def normalize(%Activity{data: %{"object" => %{"fake" => true} = data}}, _, _) do
+ def normalize(%Activity{data: %{"object" => %{"fake" => true} = data}}, _) do
%Object{id: "pleroma:fake_object_id", data: data}
end
# No preloaded object
- def normalize(%Activity{data: %{"object" => %{"id" => ap_id}}}, fetch_remote, _) do
+ def normalize(%Activity{data: %{"object" => %{"id" => ap_id}}}, options) do
warn_on_no_object_preloaded(ap_id)
- normalize(ap_id, fetch_remote)
+ normalize(ap_id, options)
end
# No preloaded object
- def normalize(%Activity{data: %{"object" => ap_id}}, fetch_remote, _) do
+ def normalize(%Activity{data: %{"object" => ap_id}}, options) do
warn_on_no_object_preloaded(ap_id)
- normalize(ap_id, fetch_remote)
+ normalize(ap_id, options)
end
# Old way, try fetching the object through cache.
- def normalize(%{"id" => ap_id}, fetch_remote, _), do: normalize(ap_id, fetch_remote)
- def normalize(ap_id, false, _) when is_binary(ap_id), do: get_cached_by_ap_id(ap_id)
+ def normalize(%{"id" => ap_id}, options), do: normalize(ap_id, options)
- def normalize(ap_id, true, options) when is_binary(ap_id) do
- Fetcher.fetch_object_from_id!(ap_id, options)
+ def normalize(ap_id, options) when is_binary(ap_id) do
+ if Keyword.get(options, :fetch) do
+ Fetcher.fetch_object_from_id!(ap_id, options)
+ else
+ get_cached_by_ap_id(ap_id)
+ end
end
- def normalize(_, _, _), do: nil
+ def normalize(_, _), do: nil
# Owned objects can only be accessed by their owner
def authorize_access(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}) do
@@ -156,9 +161,9 @@ defmodule Pleroma.Object do
def get_cached_by_ap_id(ap_id) do
key = "object:#{ap_id}"
- with {:ok, nil} <- Cachex.get(:object_cache, key),
+ with {:ok, nil} <- @cachex.get(:object_cache, key),
object when not is_nil(object) <- get_by_ap_id(ap_id),
- {:ok, true} <- Cachex.put(:object_cache, key, object) do
+ {:ok, true} <- @cachex.put(:object_cache, key, object) do
object
else
{:ok, object} -> object
@@ -216,13 +221,13 @@ defmodule Pleroma.Object do
end
def invalid_object_cache(%Object{data: %{"id" => id}}) do
- with {:ok, true} <- Cachex.del(:object_cache, "object:#{id}") do
- Cachex.del(:web_resp_cache, URI.parse(id).path)
+ with {:ok, true} <- @cachex.del(:object_cache, "object:#{id}") do
+ @cachex.del(:web_resp_cache, URI.parse(id).path)
end
end
def set_cache(%Object{data: %{"id" => ap_id}} = object) do
- Cachex.put(:object_cache, "object:#{ap_id}", object)
+ @cachex.put(:object_cache, "object:#{ap_id}", object)
{:ok, object}
end
@@ -283,7 +288,7 @@ defmodule Pleroma.Object do
end
def increase_vote_count(ap_id, name, actor) do
- with %Object{} = object <- Object.normalize(ap_id),
+ with %Object{} = object <- Object.normalize(ap_id, fetch: false),
"Question" <- object.data["type"] do
key = if poll_is_multiple?(object), do: "anyOf", else: "oneOf"
@@ -324,7 +329,7 @@ defmodule Pleroma.Object do
end
def replies(object, opts \\ []) do
- object = Object.normalize(object)
+ object = Object.normalize(object, fetch: false)
query =
Object
diff --git a/lib/pleroma/object/containment.ex b/lib/pleroma/object/containment.ex
index 29cb3d672..fb0398f92 100644
--- a/lib/pleroma/object/containment.ex
+++ b/lib/pleroma/object/containment.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Object.Containment do
diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex
index ae4301738..bcccf1c4c 100644
--- a/lib/pleroma/object/fetcher.ex
+++ b/lib/pleroma/object/fetcher.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Object.Fetcher do
@@ -12,7 +12,6 @@ defmodule Pleroma.Object.Fetcher do
alias Pleroma.Web.ActivityPub.ObjectValidator
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.Federator
- alias Pleroma.Web.FedSockets
require Logger
require Pleroma.Constants
@@ -84,13 +83,13 @@ defmodule Pleroma.Object.Fetcher do
with {_, nil} <- {:fetch_object, Object.get_cached_by_ap_id(id)},
{_, true} <- {:allowed_depth, Federator.allowed_thread_distance?(options[:depth])},
{_, {:ok, data}} <- {:fetch, fetch_and_contain_remote_object_from_id(id)},
- {_, nil} <- {:normalize, Object.normalize(data, false)},
+ {_, nil} <- {:normalize, Object.normalize(data, fetch: false)},
params <- prepare_activity_params(data),
{_, :ok} <- {:containment, Containment.contain_origin(id, params)},
{_, {:ok, activity}} <-
{:transmogrifier, Transmogrifier.handle_incoming(params, options)},
{_, _data, %Object{} = object} <-
- {:object, data, Object.normalize(activity, false)} do
+ {:object, data, Object.normalize(activity, fetch: false)} do
{:ok, object}
else
{:allowed_depth, false} ->
@@ -183,16 +182,16 @@ defmodule Pleroma.Object.Fetcher do
end
end
- def fetch_and_contain_remote_object_from_id(prm, opts \\ [])
+ def fetch_and_contain_remote_object_from_id(id)
- def fetch_and_contain_remote_object_from_id(%{"id" => id}, opts),
- do: fetch_and_contain_remote_object_from_id(id, opts)
+ def fetch_and_contain_remote_object_from_id(%{"id" => id}),
+ do: fetch_and_contain_remote_object_from_id(id)
- def fetch_and_contain_remote_object_from_id(id, opts) when is_binary(id) do
+ def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do
Logger.debug("Fetching object #{id} via AP")
with {:scheme, true} <- {:scheme, String.starts_with?(id, "http")},
- {:ok, body} <- get_object(id, opts),
+ {:ok, body} <- get_object(id),
{:ok, data} <- safe_json_decode(body),
:ok <- Containment.contain_origin_from_id(id, data) do
{:ok, data}
@@ -208,22 +207,10 @@ defmodule Pleroma.Object.Fetcher do
end
end
- def fetch_and_contain_remote_object_from_id(_id, _opts),
+ def fetch_and_contain_remote_object_from_id(_id),
do: {:error, "id must be a string"}
- defp get_object(id, opts) do
- with false <- Keyword.get(opts, :force_http, false),
- {:ok, fedsocket} <- FedSockets.get_or_create_fed_socket(id) do
- Logger.debug("fetching via fedsocket - #{inspect(id)}")
- FedSockets.fetch(fedsocket, id)
- else
- _other ->
- Logger.debug("fetching via http - #{inspect(id)}")
- get_object_http(id)
- end
- end
-
- defp get_object_http(id) do
+ defp get_object(id) do
date = Pleroma.Signature.signed_date()
headers =
diff --git a/lib/pleroma/object_tombstone.ex b/lib/pleroma/object_tombstone.ex
index e26f44057..a42d2d9a0 100644
--- a/lib/pleroma/object_tombstone.ex
+++ b/lib/pleroma/object_tombstone.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ObjectTombstone do
diff --git a/lib/pleroma/otp_version.ex b/lib/pleroma/otp_version.ex
index 114d0054f..a5ac1b072 100644
--- a/lib/pleroma/otp_version.ex
+++ b/lib/pleroma/otp_version.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.OTPVersion do
diff --git a/lib/pleroma/pagination.ex b/lib/pleroma/pagination.ex
index 9a3795769..0d24e1010 100644
--- a/lib/pleroma/pagination.ex
+++ b/lib/pleroma/pagination.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Pagination do
diff --git a/lib/pleroma/password/pbkdf2.ex b/lib/pleroma/password/pbkdf2.ex
new file mode 100644
index 000000000..2fd5f4491
--- /dev/null
+++ b/lib/pleroma/password/pbkdf2.ex
@@ -0,0 +1,55 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Password.Pbkdf2 do
+ @moduledoc """
+ This module implements Pbkdf2 passwords in terms of Plug.Crypto.
+ """
+
+ alias Plug.Crypto.KeyGenerator
+
+ def decode64(str) do
+ str
+ |> String.replace(".", "+")
+ |> Base.decode64!(padding: false)
+ end
+
+ def encode64(bin) do
+ bin
+ |> Base.encode64(padding: false)
+ |> String.replace("+", ".")
+ end
+
+ def verify_pass(password, hash) do
+ ["pbkdf2-" <> digest, iterations, salt, hash] = String.split(hash, "$", trim: true)
+
+ salt = decode64(salt)
+
+ iterations = String.to_integer(iterations)
+
+ digest = String.to_atom(digest)
+
+ binary_hash =
+ KeyGenerator.generate(password, salt, digest: digest, iterations: iterations, length: 64)
+
+ encode64(binary_hash) == hash
+ end
+
+ def hash_pwd_salt(password, opts \\ []) do
+ salt =
+ Keyword.get_lazy(opts, :salt, fn ->
+ :crypto.strong_rand_bytes(16)
+ end)
+
+ digest = Keyword.get(opts, :digest, :sha512)
+
+ iterations =
+ Keyword.get(opts, :iterations, Pleroma.Config.get([:password, :iterations], 160_000))
+
+ binary_hash =
+ KeyGenerator.generate(password, salt, digest: digest, iterations: iterations, length: 64)
+
+ "$pbkdf2-#{digest}$#{iterations}$#{encode64(salt)}$#{encode64(binary_hash)}"
+ end
+end
diff --git a/lib/pleroma/password_reset_token.ex b/lib/pleroma/password_reset_token.ex
index 787bd4781..edc8ed6a0 100644
--- a/lib/pleroma/password_reset_token.ex
+++ b/lib/pleroma/password_reset_token.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.PasswordResetToken do
@@ -40,6 +40,7 @@ defmodule Pleroma.PasswordResetToken do
@spec reset_password(binary(), map()) :: {:ok, User.t()} | {:error, binary()}
def reset_password(token, data) do
with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}),
+ false <- expired?(token),
%User{} = user <- User.get_cached_by_id(token.user_id),
{:ok, _user} <- User.reset_password(user, data),
{:ok, token} <- Repo.update(used_changeset(token)) do
@@ -48,4 +49,14 @@ defmodule Pleroma.PasswordResetToken do
_e -> {:error, token}
end
end
+
+ def expired?(%__MODULE__{inserted_at: inserted_at}) do
+ validity = Pleroma.Config.get([:instance, :password_reset_token_validity], 0)
+
+ now = NaiveDateTime.utc_now()
+
+ difference = NaiveDateTime.diff(now, inserted_at)
+
+ difference > validity
+ end
end
diff --git a/lib/pleroma/registration.ex b/lib/pleroma/registration.ex
index 9163040b4..7b49618e1 100644
--- a/lib/pleroma/registration.ex
+++ b/lib/pleroma/registration.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Registration do
diff --git a/lib/pleroma/release_tasks.ex b/lib/pleroma/release_tasks.ex
index 02dd6c325..1e06aafe4 100644
--- a/lib/pleroma/release_tasks.ex
+++ b/lib/pleroma/release_tasks.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ReleaseTasks do
diff --git a/lib/pleroma/repo.ex b/lib/pleroma/repo.ex
index 4524bd5e2..4556352d0 100644
--- a/lib/pleroma/repo.ex
+++ b/lib/pleroma/repo.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Repo do
diff --git a/lib/pleroma/report_note.ex b/lib/pleroma/report_note.ex
index a239bd361..f8bab1548 100644
--- a/lib/pleroma/report_note.ex
+++ b/lib/pleroma/report_note.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ReportNote do
diff --git a/lib/pleroma/reverse_proxy.ex b/lib/pleroma/reverse_proxy.ex
index 8ae1157df..406f7e2b8 100644
--- a/lib/pleroma/reverse_proxy.ex
+++ b/lib/pleroma/reverse_proxy.ex
@@ -1,10 +1,10 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ReverseProxy do
@range_headers ~w(range if-range)
- @keep_req_headers ~w(accept user-agent accept-encoding cache-control if-modified-since) ++
+ @keep_req_headers ~w(accept accept-encoding cache-control if-modified-since) ++
~w(if-unmodified-since if-none-match) ++ @range_headers
@resp_cache_headers ~w(etag date last-modified)
@keep_resp_headers @resp_cache_headers ++
@@ -17,6 +17,8 @@ defmodule Pleroma.ReverseProxy do
@failed_request_ttl :timer.seconds(60)
@methods ~w(GET HEAD)
+ @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
+
def max_read_duration_default, do: @max_read_duration
def default_cache_control_header, do: @default_cache_control_header
@@ -55,9 +57,6 @@ defmodule Pleroma.ReverseProxy do
* `false` will add `content-disposition: attachment` to any request,
* a list of whitelisted content types
- * `keep_user_agent` will forward the client's user-agent to the upstream. This may be useful if the upstream is
- doing content transformation (encoding, …) depending on the request.
-
* `req_headers`, `resp_headers` additional headers.
* `http`: options for [hackney](https://github.com/benoitc/hackney) or [gun](https://github.com/ninenines/gun).
@@ -82,8 +81,7 @@ defmodule Pleroma.ReverseProxy do
import Plug.Conn
@type option() ::
- {:keep_user_agent, boolean}
- | {:max_read_duration, :timer.time() | :infinity}
+ {:max_read_duration, :timer.time() | :infinity}
| {:max_body_length, non_neg_integer() | :infinity}
| {:failed_request_ttl, :timer.time() | :infinity}
| {:http, []}
@@ -107,7 +105,7 @@ defmodule Pleroma.ReverseProxy do
opts
end
- with {:ok, nil} <- Cachex.get(:failed_proxy_url_cache, url),
+ with {:ok, nil} <- @cachex.get(:failed_proxy_url_cache, url),
{:ok, code, headers, client} <- request(method, url, req_headers, client_opts),
:ok <-
header_length_constraint(
@@ -289,17 +287,13 @@ defmodule Pleroma.ReverseProxy do
end
end
- defp build_req_user_agent_header(headers, opts) do
- if Keyword.get(opts, :keep_user_agent, false) do
- List.keystore(
- headers,
- "user-agent",
- 0,
- {"user-agent", Pleroma.Application.user_agent()}
- )
- else
- headers
- end
+ defp build_req_user_agent_header(headers, _opts) do
+ List.keystore(
+ headers,
+ "user-agent",
+ 0,
+ {"user-agent", Pleroma.Application.user_agent()}
+ )
end
defp build_resp_headers(headers, opts) do
@@ -427,6 +421,6 @@ defmodule Pleroma.ReverseProxy do
nil
end
- Cachex.put(:failed_proxy_url_cache, url, true, ttl: ttl)
+ @cachex.put(:failed_proxy_url_cache, url, true, ttl: ttl)
end
end
diff --git a/lib/pleroma/reverse_proxy/client.ex b/lib/pleroma/reverse_proxy/client.ex
index 0d13ff174..8698fa2e1 100644
--- a/lib/pleroma/reverse_proxy/client.ex
+++ b/lib/pleroma/reverse_proxy/client.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ReverseProxy.Client do
diff --git a/lib/pleroma/reverse_proxy/client/hackney.ex b/lib/pleroma/reverse_proxy/client/hackney.ex
index ad988fac3..dba946308 100644
--- a/lib/pleroma/reverse_proxy/client/hackney.ex
+++ b/lib/pleroma/reverse_proxy/client/hackney.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ReverseProxy.Client.Hackney do
diff --git a/lib/pleroma/reverse_proxy/client/tesla.ex b/lib/pleroma/reverse_proxy/client/tesla.ex
index 4b118eec2..36a0a2060 100644
--- a/lib/pleroma/reverse_proxy/client/tesla.ex
+++ b/lib/pleroma/reverse_proxy/client/tesla.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ReverseProxy.Client.Tesla do
diff --git a/lib/pleroma/scheduled_activity.ex b/lib/pleroma/scheduled_activity.ex
index 0937cb7db..2b156341f 100644
--- a/lib/pleroma/scheduled_activity.ex
+++ b/lib/pleroma/scheduled_activity.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ScheduledActivity do
diff --git a/lib/pleroma/signature.ex b/lib/pleroma/signature.ex
index e388993b7..43ab569a4 100644
--- a/lib/pleroma/signature.ex
+++ b/lib/pleroma/signature.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Signature do
@@ -39,7 +39,7 @@ defmodule Pleroma.Signature do
def fetch_public_key(conn) do
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
{:ok, actor_id} <- key_id_to_actor_id(kid),
- {:ok, public_key} <- User.get_public_key_for_ap_id(actor_id, force_http: true) do
+ {:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
{:ok, public_key}
else
e ->
@@ -50,8 +50,8 @@ defmodule Pleroma.Signature do
def refetch_public_key(conn) do
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
{:ok, actor_id} <- key_id_to_actor_id(kid),
- {:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id, force_http: true),
- {:ok, public_key} <- User.get_public_key_for_ap_id(actor_id, force_http: true) do
+ {: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}
else
e ->
diff --git a/lib/pleroma/stats.ex b/lib/pleroma/stats.ex
index e5c9c668b..3e3f24c2c 100644
--- a/lib/pleroma/stats.ex
+++ b/lib/pleroma/stats.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Stats do
@@ -23,8 +23,11 @@ defmodule Pleroma.Stats do
@impl true
def init(_args) do
- if Pleroma.Config.get(:env) == :test, do: :ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo)
- {:ok, nil, {:continue, :calculate_stats}}
+ if Pleroma.Config.get(:env) != :test do
+ {:ok, nil, {:continue, :calculate_stats}}
+ else
+ {:ok, calculate_stat_data()}
+ end
end
@doc "Performs update stats"
@@ -32,11 +35,6 @@ defmodule Pleroma.Stats do
GenServer.call(__MODULE__, :force_update)
end
- @doc "Performs collect stats"
- def do_collect do
- GenServer.cast(__MODULE__, :run_update)
- end
-
@doc "Returns stats data"
@spec get_stats() :: %{
domain_count: non_neg_integer(),
@@ -81,7 +79,7 @@ defmodule Pleroma.Stats do
users_query =
from(u in User,
- where: u.deactivated != true,
+ where: u.is_active == true,
where: u.local == true,
where: not is_nil(u.nickname),
where: not u.invisible
@@ -111,7 +109,11 @@ defmodule Pleroma.Stats do
@impl true
def handle_continue(:calculate_stats, _) do
stats = calculate_stat_data()
- Process.send_after(self(), :run_update, @interval)
+
+ unless Pleroma.Config.get(:env) == :test do
+ Process.send_after(self(), :run_update, @interval)
+ end
+
{:noreply, stats}
end
@@ -127,13 +129,6 @@ defmodule Pleroma.Stats do
end
@impl true
- def handle_cast(:run_update, _state) do
- new_stats = calculate_stat_data()
-
- {:noreply, new_stats}
- end
-
- @impl true
def handle_info(:run_update, _) do
new_stats = calculate_stat_data()
Process.send_after(self(), :run_update, @interval)
diff --git a/lib/pleroma/telemetry/logger.ex b/lib/pleroma/telemetry/logger.ex
index 003079cf3..44d2f48dc 100644
--- a/lib/pleroma/telemetry/logger.ex
+++ b/lib/pleroma/telemetry/logger.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Telemetry.Logger do
diff --git a/lib/pleroma/tesla/middleware/connection_pool.ex b/lib/pleroma/tesla/middleware/connection_pool.ex
index 056e736ce..906706d39 100644
--- a/lib/pleroma/tesla/middleware/connection_pool.ex
+++ b/lib/pleroma/tesla/middleware/connection_pool.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Tesla.Middleware.ConnectionPool do
diff --git a/lib/pleroma/tests/auth_test_controller.ex b/lib/pleroma/tests/auth_test_controller.ex
index b30d83567..ddf3fea4f 100644
--- a/lib/pleroma/tests/auth_test_controller.ex
+++ b/lib/pleroma/tests/auth_test_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
# A test controller reachable only in :test env.
diff --git a/lib/pleroma/thread_mute.ex b/lib/pleroma/thread_mute.ex
index be01d541d..5d06cf030 100644
--- a/lib/pleroma/thread_mute.ex
+++ b/lib/pleroma/thread_mute.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ThreadMute do
diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex
index db2cc1dae..654711351 100644
--- a/lib/pleroma/upload.ex
+++ b/lib/pleroma/upload.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Upload do
@@ -31,6 +31,7 @@ defmodule Pleroma.Upload do
"""
alias Ecto.UUID
+ alias Pleroma.Config
require Logger
@type source ::
@@ -130,12 +131,7 @@ defmodule Pleroma.Upload do
uploader: Keyword.get(opts, :uploader, Pleroma.Config.get([__MODULE__, :uploader])),
filters: Keyword.get(opts, :filters, Pleroma.Config.get([__MODULE__, :filters])),
description: Keyword.get(opts, :description),
- base_url:
- Keyword.get(
- opts,
- :base_url,
- Pleroma.Config.get([__MODULE__, :base_url], Pleroma.Web.base_url())
- )
+ base_url: base_url()
}
end
@@ -216,16 +212,46 @@ defmodule Pleroma.Upload do
""
end
- prefix =
- if is_nil(Pleroma.Config.get([__MODULE__, :base_url])) do
- "media"
- else
- ""
- end
-
- [base_url, prefix, path]
+ [base_url, path]
|> Path.join()
end
defp url_from_spec(_upload, _base_url, {:url, url}), do: url
+
+ 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])
+
+ case uploader do
+ Pleroma.Uploaders.Local ->
+ upload_base_url || Pleroma.Web.base_url() <> "/media/"
+
+ 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_with_namespace =
+ cond do
+ !is_nil(truncated_namespace) ->
+ truncated_namespace
+
+ !is_nil(namespace) ->
+ namespace <> ":" <> bucket
+
+ true ->
+ bucket
+ end
+
+ if public_endpoint do
+ Path.join([public_endpoint, bucket_with_namespace])
+ else
+ Path.join([upload_base_url, bucket_with_namespace])
+ end
+
+ _ ->
+ public_endpoint || upload_base_url || Pleroma.Web.base_url() <> "/media/"
+ end
+ end
end
diff --git a/lib/pleroma/upload/filter.ex b/lib/pleroma/upload/filter.ex
index 661135634..c677d4b9f 100644
--- a/lib/pleroma/upload/filter.ex
+++ b/lib/pleroma/upload/filter.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Upload.Filter do
diff --git a/lib/pleroma/upload/filter/anonymize_filename.ex b/lib/pleroma/upload/filter/anonymize_filename.ex
index fc456e4f4..7e62b3d13 100644
--- a/lib/pleroma/upload/filter/anonymize_filename.ex
+++ b/lib/pleroma/upload/filter/anonymize_filename.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Upload.Filter.AnonymizeFilename do
diff --git a/lib/pleroma/upload/filter/dedupe.ex b/lib/pleroma/upload/filter/dedupe.ex
index 86cbc8996..2bf581b05 100644
--- a/lib/pleroma/upload/filter/dedupe.ex
+++ b/lib/pleroma/upload/filter/dedupe.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Upload.Filter.Dedupe do
diff --git a/lib/pleroma/upload/filter/exiftool.ex b/lib/pleroma/upload/filter/exiftool.ex
index 1fd0cfdaa..a2bfbbf61 100644
--- a/lib/pleroma/upload/filter/exiftool.ex
+++ b/lib/pleroma/upload/filter/exiftool.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Upload.Filter.Exiftool do
@@ -11,7 +11,8 @@ defmodule Pleroma.Upload.Filter.Exiftool do
@spec filter(Pleroma.Upload.t()) :: {:ok, any()} | {:error, String.t()}
- # webp is not compatible with exiftool at this time
+ # 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}
def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do
@@ -21,8 +22,8 @@ defmodule Pleroma.Upload.Filter.Exiftool do
{error, 1} -> {:error, error}
end
rescue
- _e in ErlangError ->
- {:error, "exiftool command not found"}
+ e in ErlangError ->
+ {:error, "#{__MODULE__}: #{inspect(e)}"}
end
end
diff --git a/lib/pleroma/upload/filter/mogrifun.ex b/lib/pleroma/upload/filter/mogrifun.ex
index 363e5cf0f..01126aaeb 100644
--- a/lib/pleroma/upload/filter/mogrifun.ex
+++ b/lib/pleroma/upload/filter/mogrifun.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Upload.Filter.Mogrifun do
@@ -44,8 +44,8 @@ defmodule Pleroma.Upload.Filter.Mogrifun do
Filter.Mogrify.do_filter(file, [Enum.random(@filters)])
{:ok, :filtered}
rescue
- _e in ErlangError ->
- {:error, "mogrify command not found"}
+ e in ErlangError ->
+ {:error, "#{__MODULE__}: #{inspect(e)}"}
end
end
diff --git a/lib/pleroma/upload/filter/mogrify.ex b/lib/pleroma/upload/filter/mogrify.ex
index 71968fd9c..f27aefc22 100644
--- a/lib/pleroma/upload/filter/mogrify.ex
+++ b/lib/pleroma/upload/filter/mogrify.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Upload.Filter.Mogrify do
@@ -14,8 +14,8 @@ defmodule Pleroma.Upload.Filter.Mogrify do
do_filter(file, Pleroma.Config.get!([__MODULE__, :args]))
{:ok, :filtered}
rescue
- _e in ErlangError ->
- {:error, "mogrify command not found"}
+ e in ErlangError ->
+ {:error, "#{__MODULE__}: #{inspect(e)}"}
end
end
diff --git a/lib/pleroma/uploaders/local.ex b/lib/pleroma/uploaders/local.ex
index 10b3069f4..0e1ba4b90 100644
--- a/lib/pleroma/uploaders/local.ex
+++ b/lib/pleroma/uploaders/local.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Uploaders.Local do
diff --git a/lib/pleroma/uploaders/s3.ex b/lib/pleroma/uploaders/s3.ex
index 6dbef9085..d85c8cb2f 100644
--- a/lib/pleroma/uploaders/s3.ex
+++ b/lib/pleroma/uploaders/s3.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Uploaders.S3 do
@@ -12,26 +12,10 @@ defmodule Pleroma.Uploaders.S3 do
# links with less strict filenames
@impl true
def get_file(file) do
- config = Config.get([__MODULE__])
- bucket = Keyword.fetch!(config, :bucket)
-
- bucket_with_namespace =
- cond do
- truncated_namespace = Keyword.get(config, :truncated_namespace) ->
- truncated_namespace
-
- namespace = Keyword.get(config, :bucket_namespace) ->
- namespace <> ":" <> bucket
-
- true ->
- bucket
- end
-
{:ok,
{:url,
Path.join([
- Keyword.fetch!(config, :public_endpoint),
- bucket_with_namespace,
+ Pleroma.Upload.base_url(),
strict_encode(URI.decode(file))
])}}
end
diff --git a/lib/pleroma/uploaders/uploader.ex b/lib/pleroma/uploaders/uploader.ex
index 6249eceb1..0be878ca2 100644
--- a/lib/pleroma/uploaders/uploader.ex
+++ b/lib/pleroma/uploaders/uploader.ex
@@ -1,10 +1,12 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
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.
"""
@@ -74,7 +76,7 @@ defmodule Pleroma.Uploaders.Uploader do
end
defp callback_timeout do
- case Mix.env() do
+ case @mix_env do
:test -> 1_000
_ -> 30_000
end
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 0545b7445..9942617d8 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.User do
@@ -81,6 +81,8 @@ defmodule Pleroma.User do
]
]
+ @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
+
schema "users" do
field(:bio, :string, default: "")
field(:raw_bio, :string)
@@ -108,14 +110,14 @@ defmodule Pleroma.User do
field(:follower_count, :integer, default: 0)
field(:following_count, :integer, default: 0)
field(:is_locked, :boolean, default: false)
- field(:confirmation_pending, :boolean, default: false)
+ field(:is_confirmed, :boolean, default: true)
field(:password_reset_pending, :boolean, default: false)
- field(:approval_pending, :boolean, default: false)
+ field(:is_approved, :boolean, default: true)
field(:registration_reason, :string, default: nil)
field(:confirmation_token, :string, default: nil)
field(:default_scope, :string, default: "public")
field(:domain_blocks, {:array, :string}, default: [])
- field(:deactivated, :boolean, default: false)
+ field(:is_active, :boolean, default: true)
field(:no_rich_text, :boolean, default: false)
field(:ap_enabled, :boolean, default: false)
field(:is_moderator, :boolean, default: false)
@@ -128,7 +130,6 @@ defmodule Pleroma.User do
field(:hide_followers, :boolean, default: false)
field(:hide_follows, :boolean, default: false)
field(:hide_favorites, :boolean, default: true)
- field(:unread_conversation_count, :integer, default: 0)
field(:pinned_activities, {:array, :string}, default: [])
field(:email_notifications, :map, default: %{"digest" => false})
field(:mascot, :map, default: nil)
@@ -136,15 +137,17 @@ defmodule Pleroma.User do
field(:pleroma_settings_store, :map, default: %{})
field(:fields, {:array, :map}, default: [])
field(:raw_fields, {:array, :map}, default: [])
- field(:discoverable, :boolean, default: false)
+ field(:is_discoverable, :boolean, default: false)
field(:invisible, :boolean, default: false)
field(:allow_following_move, :boolean, default: true)
field(:skip_thread_containment, :boolean, default: false)
field(:actor_type, :string, default: "Person")
- field(:also_known_as, {:array, :string}, default: [])
+ field(:also_known_as, {:array, ObjectValidators.ObjectID}, default: [])
field(:inbox, :string)
field(:shared_inbox, :string)
field(:accepts_chat_messages, :boolean, default: nil)
+ field(:last_active_at, :naive_datetime)
+ field(:disclose_client, :boolean, default: true)
embeds_one(
:notification_settings,
@@ -216,7 +219,8 @@ defmodule Pleroma.User do
target_users_query = assoc(user, unquote(outgoing_relation_target))
if restrict_deactivated? do
- restrict_deactivated(target_users_query)
+ target_users_query
+ |> User.Query.build(%{deactivated: false})
else
target_users_query
end
@@ -246,6 +250,18 @@ defmodule Pleroma.User do
end
end
+ def cached_blocked_users_ap_ids(user) do
+ @cachex.fetch!(:user_cache, "blocked_users_ap_ids:#{user.ap_id}", fn _ ->
+ blocked_users_ap_ids(user)
+ end)
+ end
+
+ def cached_muted_users_ap_ids(user) do
+ @cachex.fetch!(:user_cache, "muted_users_ap_ids:#{user.ap_id}", fn _ ->
+ muted_users_ap_ids(user)
+ end)
+ end
+
defdelegate following_count(user), to: FollowingRelationship
defdelegate following(user), to: FollowingRelationship
defdelegate following?(follower, followed), to: FollowingRelationship
@@ -273,18 +289,10 @@ defmodule Pleroma.User do
@doc "Returns status account"
@spec account_status(User.t()) :: account_status()
- def account_status(%User{deactivated: true}), do: :deactivated
+ def account_status(%User{is_active: false}), do: :deactivated
def account_status(%User{password_reset_pending: true}), do: :password_reset_pending
- def account_status(%User{local: true, approval_pending: true}), do: :approval_pending
-
- def account_status(%User{local: true, confirmation_pending: true}) do
- if Config.get([:instance, :account_activation_required]) do
- :confirmation_pending
- else
- :active
- end
- end
-
+ def account_status(%User{local: true, is_approved: false}), do: :approval_pending
+ def account_status(%User{local: true, is_confirmed: false}), do: :confirmation_pending
def account_status(%User{}), do: :active
@spec visible_for(User.t(), User.t() | nil) ::
@@ -373,11 +381,6 @@ defmodule Pleroma.User do
def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
- @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
- def restrict_deactivated(query) do
- from(u in query, where: u.deactivated != ^true)
- end
-
defp truncate_fields_param(params) do
if Map.has_key?(params, :fields) do
Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
@@ -447,7 +450,7 @@ defmodule Pleroma.User do
:follower_count,
:fields,
:following_count,
- :discoverable,
+ :is_discoverable,
:invisible,
:actor_type,
:also_known_as,
@@ -502,16 +505,17 @@ defmodule Pleroma.User do
:hide_follows_count,
:hide_favorites,
:allow_following_move,
+ :also_known_as,
:background,
:show_role,
:skip_thread_containment,
:fields,
:raw_fields,
:pleroma_settings_store,
- :discoverable,
+ :is_discoverable,
:actor_type,
- :also_known_as,
- :accepts_chat_messages
+ :accepts_chat_messages,
+ :disclose_client
]
)
|> unique_constraint(:nickname)
@@ -691,23 +695,23 @@ defmodule Pleroma.User do
reason_limit = Config.get([:instance, :registration_reason_length], 500)
params = Map.put_new(params, :accepts_chat_messages, true)
- need_confirmation? =
- if is_nil(opts[:need_confirmation]) do
- Config.get([:instance, :account_activation_required])
+ confirmed? =
+ if is_nil(opts[:confirmed]) do
+ !Config.get([:instance, :account_activation_required])
else
- opts[:need_confirmation]
+ opts[:confirmed]
end
- need_approval? =
- if is_nil(opts[:need_approval]) do
- Config.get([:instance, :account_approval_required])
+ approved? =
+ if is_nil(opts[:approved]) do
+ !Config.get([:instance, :account_approval_required])
else
- opts[:need_approval]
+ opts[:approved]
end
struct
- |> confirmation_changeset(need_confirmation: need_confirmation?)
- |> approval_changeset(need_approval: need_approval?)
+ |> confirmation_changeset(set_confirmation: confirmed?)
+ |> approval_changeset(set_approval: approved?)
|> cast(params, [
:bio,
:raw_bio,
@@ -772,12 +776,22 @@ defmodule Pleroma.User do
candidates = Config.get([:instance, :autofollowed_nicknames])
autofollowed_users =
- User.Query.build(%{nickname: candidates, local: true, deactivated: false})
+ User.Query.build(%{nickname: candidates, local: true, is_active: true})
|> Repo.all()
follow_all(user, autofollowed_users)
end
+ defp autofollowing_users(user) do
+ candidates = Config.get([:instance, :autofollowing_nicknames])
+
+ User.Query.build(%{nickname: candidates, local: true, deactivated: false})
+ |> Repo.all()
+ |> Enum.each(&follow(&1, user, :follow_accept))
+
+ {:ok, :success}
+ end
+
@doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
def register(%Ecto.Changeset{} = changeset) do
with {:ok, user} <- Repo.insert(changeset) do
@@ -785,18 +799,52 @@ defmodule Pleroma.User do
end
end
- def post_register_action(%User{} = user) do
+ def post_register_action(%User{is_confirmed: false} = user) do
+ with {:ok, _} <- maybe_send_confirmation_email(user) do
+ {:ok, user}
+ end
+ end
+
+ def post_register_action(%User{is_approved: false} = user) do
+ with {:ok, _} <- send_user_approval_email(user),
+ {:ok, _} <- send_admin_approval_emails(user) do
+ {:ok, user}
+ end
+ end
+
+ def post_register_action(%User{is_approved: true, is_confirmed: true} = user) do
with {:ok, user} <- autofollow_users(user),
+ {:ok, _} <- autofollowing_users(user),
{:ok, user} <- set_cache(user),
- {:ok, _} <- send_welcome_email(user),
- {:ok, _} <- send_welcome_message(user),
- {:ok, _} <- send_welcome_chat_message(user),
- {:ok, _} <- try_send_confirmation_email(user) do
+ {:ok, _} <- maybe_send_registration_email(user),
+ {:ok, _} <- maybe_send_welcome_email(user),
+ {:ok, _} <- maybe_send_welcome_message(user),
+ {:ok, _} <- maybe_send_welcome_chat_message(user) do
{:ok, user}
end
end
- def send_welcome_message(user) do
+ defp send_user_approval_email(user) do
+ user
+ |> Pleroma.Emails.UserEmail.approval_pending_email()
+ |> Pleroma.Emails.Mailer.deliver_async()
+
+ {:ok, :enqueued}
+ end
+
+ defp send_admin_approval_emails(user) do
+ all_superusers()
+ |> Enum.filter(fn user -> not is_nil(user.email) end)
+ |> Enum.each(fn superuser ->
+ superuser
+ |> Pleroma.Emails.AdminEmail.new_unapproved_registration(user)
+ |> Pleroma.Emails.Mailer.deliver_async()
+ end)
+
+ {:ok, :enqueued}
+ end
+
+ defp maybe_send_welcome_message(user) do
if User.WelcomeMessage.enabled?() do
User.WelcomeMessage.post_message(user)
{:ok, :enqueued}
@@ -805,7 +853,7 @@ defmodule Pleroma.User do
end
end
- def send_welcome_chat_message(user) do
+ defp maybe_send_welcome_chat_message(user) do
if User.WelcomeChatMessage.enabled?() do
User.WelcomeChatMessage.post_message(user)
{:ok, :enqueued}
@@ -814,7 +862,7 @@ defmodule Pleroma.User do
end
end
- def send_welcome_email(%User{email: email} = user) when is_binary(email) do
+ defp maybe_send_welcome_email(%User{email: email} = user) when is_binary(email) do
if User.WelcomeEmail.enabled?() do
User.WelcomeEmail.send_email(user)
{:ok, :enqueued}
@@ -823,10 +871,10 @@ defmodule Pleroma.User do
end
end
- def send_welcome_email(_), do: {:ok, :noop}
+ defp maybe_send_welcome_email(_), do: {:ok, :noop}
- @spec try_send_confirmation_email(User.t()) :: {:ok, :enqueued | :noop}
- def try_send_confirmation_email(%User{confirmation_pending: true, email: email} = user)
+ @spec maybe_send_confirmation_email(User.t()) :: {:ok, :enqueued | :noop}
+ def maybe_send_confirmation_email(%User{is_confirmed: false, email: email} = user)
when is_binary(email) do
if Config.get([:instance, :account_activation_required]) do
send_confirmation_email(user)
@@ -836,7 +884,7 @@ defmodule Pleroma.User do
end
end
- def try_send_confirmation_email(_), do: {:ok, :noop}
+ def maybe_send_confirmation_email(_), do: {:ok, :noop}
@spec send_confirmation_email(Uset.t()) :: User.t()
def send_confirmation_email(%User{} = user) do
@@ -847,6 +895,24 @@ defmodule Pleroma.User do
user
end
+ @spec maybe_send_registration_email(User.t()) :: {:ok, :enqueued | :noop}
+ defp maybe_send_registration_email(%User{email: email} = user) when is_binary(email) do
+ with false <- User.WelcomeEmail.enabled?(),
+ false <- Config.get([:instance, :account_activation_required], false),
+ false <- Config.get([:instance, :account_approval_required], false) do
+ user
+ |> Pleroma.Emails.UserEmail.successful_registration_email()
+ |> Pleroma.Emails.Mailer.deliver_async()
+
+ {:ok, :enqueued}
+ else
+ _ ->
+ {:ok, :noop}
+ end
+ end
+
+ defp maybe_send_registration_email(_), do: {:ok, :noop}
+
def needs_update?(%User{local: true}), do: false
def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
@@ -872,7 +938,7 @@ defmodule Pleroma.User do
if not ap_enabled?(followed) do
follow(follower, followed)
else
- {:ok, follower}
+ {:ok, follower, followed}
end
end
@@ -890,7 +956,7 @@ defmodule Pleroma.User do
deny_follow_blocked = Config.get([:user, :deny_follow_blocked])
cond do
- followed.deactivated ->
+ not followed.is_active ->
{:error, "Could not follow user: #{followed.nickname} is deactivated."}
deny_follow_blocked and blocks?(followed, follower) ->
@@ -898,11 +964,6 @@ defmodule Pleroma.User do
true ->
FollowingRelationship.follow(follower, followed, state)
-
- {:ok, _} = update_follower_count(followed)
-
- follower
- |> update_following_count()
end
end
@@ -926,11 +987,6 @@ defmodule Pleroma.User do
case get_follow_state(follower, followed) do
state when state in [:follow_pending, :follow_accept] ->
FollowingRelationship.unfollow(follower, followed)
- {:ok, followed} = update_follower_count(followed)
-
- {:ok, follower} = update_following_count(follower)
-
- {:ok, follower, followed}
nil ->
{:error, "Not subscribed!"}
@@ -1004,9 +1060,9 @@ defmodule Pleroma.User do
def set_cache({:error, err}), do: {:error, err}
def set_cache(%User{} = user) do
- Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
- Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
- Cachex.put(:user_cache, "friends_ap_ids:#{user.nickname}", get_user_friends_ap_ids(user))
+ @cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
+ @cachex.put(:user_cache, "nickname:#{user.nickname}", user)
+ @cachex.put(:user_cache, "friends_ap_ids:#{user.nickname}", get_user_friends_ap_ids(user))
{:ok, user}
end
@@ -1029,24 +1085,26 @@ defmodule Pleroma.User do
@spec get_cached_user_friends_ap_ids(User.t()) :: [String.t()]
def get_cached_user_friends_ap_ids(user) do
- Cachex.fetch!(:user_cache, "friends_ap_ids:#{user.ap_id}", fn _ ->
+ @cachex.fetch!(:user_cache, "friends_ap_ids:#{user.ap_id}", fn _ ->
get_user_friends_ap_ids(user)
end)
end
def invalidate_cache(user) do
- Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
- Cachex.del(:user_cache, "nickname:#{user.nickname}")
- Cachex.del(:user_cache, "friends_ap_ids:#{user.ap_id}")
+ @cachex.del(:user_cache, "ap_id:#{user.ap_id}")
+ @cachex.del(:user_cache, "nickname:#{user.nickname}")
+ @cachex.del(:user_cache, "friends_ap_ids:#{user.ap_id}")
+ @cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
+ @cachex.del(:user_cache, "muted_users_ap_ids:#{user.ap_id}")
end
@spec get_cached_by_ap_id(String.t()) :: User.t() | nil
def get_cached_by_ap_id(ap_id) do
key = "ap_id:#{ap_id}"
- with {:ok, nil} <- Cachex.get(:user_cache, key),
+ with {:ok, nil} <- @cachex.get(:user_cache, key),
user when not is_nil(user) <- get_by_ap_id(ap_id),
- {:ok, true} <- Cachex.put(:user_cache, key, user) do
+ {:ok, true} <- @cachex.put(:user_cache, key, user) do
user
else
{:ok, user} -> user
@@ -1058,11 +1116,11 @@ defmodule Pleroma.User do
key = "id:#{id}"
ap_id =
- Cachex.fetch!(:user_cache, key, fn _ ->
+ @cachex.fetch!(:user_cache, key, fn _ ->
user = get_by_id(id)
if user do
- Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
+ @cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
{:commit, user.ap_id}
else
{:ignore, ""}
@@ -1075,7 +1133,7 @@ defmodule Pleroma.User do
def get_cached_by_nickname(nickname) do
key = "nickname:#{nickname}"
- Cachex.fetch!(:user_cache, key, fn ->
+ @cachex.fetch!(:user_cache, key, fn _ ->
case get_or_fetch_by_nickname(nickname) do
{:ok, user} -> {:commit, user}
{:error, _error} -> {:ignore, nil}
@@ -1133,7 +1191,7 @@ defmodule Pleroma.User do
@spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
def get_followers_query(%User{} = user, nil) do
- User.Query.build(%{followers: user, deactivated: false})
+ User.Query.build(%{followers: user, is_active: true})
end
def get_followers_query(%User{} = user, page) do
@@ -1306,51 +1364,10 @@ defmodule Pleroma.User do
|> update_and_set_cache()
end
- def set_unread_conversation_count(%User{local: true} = user) do
- unread_query = Participation.unread_conversation_count_for_user(user)
-
- User
- |> join(:inner, [u], p in subquery(unread_query))
- |> update([u, p],
- set: [unread_conversation_count: p.count]
- )
- |> where([u], u.id == ^user.id)
- |> select([u], u)
- |> Repo.update_all([])
- |> case do
- {1, [user]} -> set_cache(user)
- _ -> {:error, user}
- end
- end
-
- def set_unread_conversation_count(user), do: {:ok, user}
-
- def increment_unread_conversation_count(conversation, %User{local: true} = user) do
- unread_query =
- Participation.unread_conversation_count_for_user(user)
- |> where([p], p.conversation_id == ^conversation.id)
-
- User
- |> join(:inner, [u], p in subquery(unread_query))
- |> update([u, p],
- inc: [unread_conversation_count: 1]
- )
- |> where([u], u.id == ^user.id)
- |> where([u, p], p.count == 0)
- |> select([u], u)
- |> Repo.update_all([])
- |> case do
- {1, [user]} -> set_cache(user)
- _ -> {:error, user}
- end
- end
-
- def increment_unread_conversation_count(_, user), do: {:ok, user}
-
@spec get_users_from_set([String.t()], keyword()) :: [User.t()]
def get_users_from_set(ap_ids, opts \\ []) do
local_only = Keyword.get(opts, :local_only, true)
- criteria = %{ap_id: ap_ids, deactivated: false}
+ criteria = %{ap_id: ap_ids, is_active: true}
criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
User.Query.build(criteria)
@@ -1361,20 +1378,57 @@ defmodule Pleroma.User do
def get_recipients_from_activity(%Activity{recipients: to, actor: actor}) do
to = [actor | to]
- query = User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
+ query = User.Query.build(%{recipients_from_activity: to, local: true, is_active: true})
query
|> Repo.all()
end
- @spec mute(User.t(), User.t(), boolean()) ::
+ @spec mute(User.t(), User.t(), map()) ::
{:ok, list(UserRelationship.t())} | {:error, String.t()}
- def mute(%User{} = muter, %User{} = mutee, notifications? \\ true) do
- add_to_mutes(muter, mutee, notifications?)
+ def mute(%User{} = muter, %User{} = mutee, params \\ %{}) do
+ notifications? = Map.get(params, :notifications, true)
+ expires_in = Map.get(params, :expires_in, 0)
+
+ with {:ok, user_mute} <- UserRelationship.create_mute(muter, mutee),
+ {:ok, user_notification_mute} <-
+ (notifications? && UserRelationship.create_notification_mute(muter, mutee)) ||
+ {:ok, nil} do
+ if expires_in > 0 do
+ Pleroma.Workers.MuteExpireWorker.enqueue(
+ "unmute_user",
+ %{"muter_id" => muter.id, "mutee_id" => mutee.id},
+ schedule_in: expires_in
+ )
+ end
+
+ @cachex.del(:user_cache, "muted_users_ap_ids:#{muter.ap_id}")
+
+ {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
+ end
end
def unmute(%User{} = muter, %User{} = mutee) do
- remove_from_mutes(muter, mutee)
+ with {:ok, user_mute} <- UserRelationship.delete_mute(muter, mutee),
+ {:ok, user_notification_mute} <-
+ UserRelationship.delete_notification_mute(muter, mutee) do
+ @cachex.del(:user_cache, "muted_users_ap_ids:#{muter.ap_id}")
+ {:ok, [user_mute, user_notification_mute]}
+ end
+ end
+
+ def unmute(muter_id, mutee_id) do
+ with {:muter, %User{} = muter} <- {:muter, User.get_by_id(muter_id)},
+ {:mutee, %User{} = mutee} <- {:mutee, User.get_by_id(mutee_id)} do
+ unmute(muter, mutee)
+ else
+ {who, result} = error ->
+ Logger.warn(
+ "User.unmute/2 failed. #{who}: #{result}, muter_id: #{muter_id}, mutee_id: #{mutee_id}"
+ )
+
+ {:error, error}
+ end
end
def subscribe(%User{} = subscriber, %User{} = target) do
@@ -1543,19 +1597,19 @@ defmodule Pleroma.User do
defp maybe_filter_on_ap_id(query, _ap_ids), do: query
- def deactivate_async(user, status \\ true) do
- BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
+ def set_activation_async(user, status \\ true) do
+ BackgroundWorker.enqueue("user_activation", %{"user_id" => user.id, "status" => status})
end
- def deactivate(user, status \\ true)
-
- def deactivate(users, status) when is_list(users) do
+ @spec set_activation([User.t()], boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
+ def set_activation(users, status) when is_list(users) do
Repo.transaction(fn ->
- for user <- users, do: deactivate(user, status)
+ for user <- users, do: set_activation(user, status)
end)
end
- def deactivate(%User{} = user, status) do
+ @spec set_activation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
+ def set_activation(%User{} = user, status) do
with {:ok, user} <- set_activation_status(user, status) do
user
|> get_followers()
@@ -1580,11 +1634,34 @@ defmodule Pleroma.User do
end)
end
- def approve(%User{} = user) do
- change(user, approval_pending: false)
- |> update_and_set_cache()
+ def approve(%User{is_approved: false} = user) do
+ with chg <- change(user, is_approved: true),
+ {:ok, user} <- update_and_set_cache(chg) do
+ post_register_action(user)
+ {:ok, user}
+ end
+ end
+
+ def approve(%User{} = user), do: {:ok, user}
+
+ def confirm(users) when is_list(users) do
+ Repo.transaction(fn ->
+ Enum.map(users, fn user ->
+ with {:ok, user} <- confirm(user), do: user
+ end)
+ end)
+ end
+
+ def confirm(%User{is_confirmed: false} = user) do
+ with chg <- confirmation_changeset(user, set_confirmation: true),
+ {:ok, user} <- update_and_set_cache(chg) do
+ post_register_action(user)
+ {:ok, user}
+ end
end
+ def confirm(%User{} = user), do: {:ok, user}
+
def update_notification_settings(%User{} = user, settings) do
user
|> cast(%{notification_settings: settings}, [])
@@ -1615,13 +1692,13 @@ defmodule Pleroma.User do
follower_count: 0,
following_count: 0,
is_locked: false,
- confirmation_pending: false,
+ is_confirmed: true,
password_reset_pending: false,
- approval_pending: false,
+ is_approved: true,
registration_reason: nil,
confirmation_token: nil,
domain_blocks: [],
- deactivated: true,
+ is_active: false,
ap_enabled: false,
is_moderator: false,
is_admin: false,
@@ -1631,7 +1708,7 @@ defmodule Pleroma.User do
pleroma_settings_store: %{},
fields: [],
raw_fields: [],
- discoverable: false,
+ is_discoverable: false,
also_known_as: []
})
end
@@ -1695,7 +1772,7 @@ defmodule Pleroma.User do
delete_or_deactivate(user)
end
- def perform(:deactivate_async, user, status), do: deactivate(user, status)
+ def perform(:set_activation_async, user, status), do: set_activation(user, status)
@spec external_users_query() :: Ecto.Query.t()
def external_users_query do
@@ -1781,12 +1858,12 @@ defmodule Pleroma.User do
def html_filter_policy(_), do: Config.get([:markup, :scrub_policy])
- def fetch_by_ap_id(ap_id, opts \\ []), do: ActivityPub.make_user_from_ap_id(ap_id, opts)
+ def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
- def get_or_fetch_by_ap_id(ap_id, opts \\ []) do
+ 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, opts)
+ maybe_fetched_user = needs_update?(cached_user) && fetch_by_ap_id(ap_id)
case {cached_user, maybe_fetched_user} do
{_, {:ok, %User{} = user}} ->
@@ -1859,8 +1936,8 @@ defmodule Pleroma.User do
def public_key(_), do: {:error, "key not found"}
- def get_public_key_for_ap_id(ap_id, opts \\ []) do
- with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id, opts),
+ def get_public_key_for_ap_id(ap_id) do
+ with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
{:ok, public_key} <- public_key(user) do
{:ok, public_key}
else
@@ -1975,6 +2052,15 @@ defmodule Pleroma.User do
|> hd()
end
+ def full_nickname(%User{} = user) do
+ if String.contains?(user.nickname, "@") do
+ user.nickname
+ else
+ %{host: host} = URI.parse(user.ap_id)
+ user.nickname <> "@" <> host
+ end
+ end
+
def full_nickname(nickname_or_mention),
do: String.trim_leading(nickname_or_mention, "@")
@@ -1989,7 +2075,7 @@ defmodule Pleroma.User do
@spec all_superusers() :: [User.t()]
def all_superusers do
- User.Query.build(%{super_users: true, local: true, deactivated: false})
+ User.Query.build(%{super_users: true, local: true, is_active: true})
|> Repo.all()
end
@@ -2030,7 +2116,7 @@ defmodule Pleroma.User do
left_join: a in Pleroma.Activity,
on: u.ap_id == a.actor,
where: not is_nil(u.nickname),
- where: u.deactivated != ^true,
+ where: u.is_active == ^true,
where: u.id not in ^has_read_notifications,
group_by: u.id,
having:
@@ -2071,22 +2157,10 @@ defmodule Pleroma.User do
updated_user
end
- @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
- def toggle_confirmation(%User{} = user) do
+ @spec set_confirmation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
+ def set_confirmation(%User{} = user, bool) do
user
- |> confirmation_changeset(need_confirmation: !user.confirmation_pending)
- |> update_and_set_cache()
- end
-
- @spec toggle_confirmation([User.t()]) :: [{:ok, User.t()} | {:error, Changeset.t()}]
- def toggle_confirmation(users) do
- Enum.map(users, &toggle_confirmation/1)
- end
-
- @spec need_confirmation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
- def need_confirmation(%User{} = user, bool) do
- user
- |> confirmation_changeset(need_confirmation: bool)
+ |> confirmation_changeset(set_confirmation: bool)
|> update_and_set_cache()
end
@@ -2132,7 +2206,7 @@ defmodule Pleroma.User do
defp put_password_hash(
%Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
) do
- change(changeset, password_hash: Pbkdf2.hash_pwd_salt(password))
+ change(changeset, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password))
end
defp put_password_hash(changeset), do: changeset
@@ -2163,9 +2237,9 @@ defmodule Pleroma.User do
end
# Internal function; public one is `deactivate/2`
- defp set_activation_status(user, deactivated) do
+ defp set_activation_status(user, status) do
user
- |> cast(%{deactivated: deactivated}, [:deactivated])
+ |> cast(%{is_active: status}, [:is_active])
|> update_and_set_cache()
end
@@ -2254,27 +2328,26 @@ defmodule Pleroma.User do
end
@spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
- def confirmation_changeset(user, need_confirmation: need_confirmation?) do
+ def confirmation_changeset(user, set_confirmation: confirmed?) do
params =
- if need_confirmation? do
+ if confirmed? do
%{
- confirmation_pending: true,
- confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
+ is_confirmed: true,
+ confirmation_token: nil
}
else
%{
- confirmation_pending: false,
- confirmation_token: nil
+ is_confirmed: false,
+ confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
}
end
- cast(user, params, [:confirmation_pending, :confirmation_token])
+ cast(user, params, [:is_confirmed, :confirmation_token])
end
@spec approval_changeset(User.t(), keyword()) :: Changeset.t()
- def approval_changeset(user, need_approval: need_approval?) do
- params = if need_approval?, do: %{approval_pending: true}, else: %{approval_pending: false}
- cast(user, params, [:approval_pending])
+ def approval_changeset(user, set_approval: approved?) do
+ cast(user, %{is_approved: approved?}, [:is_approved])
end
def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
@@ -2354,29 +2427,18 @@ defmodule Pleroma.User do
@spec add_to_block(User.t(), User.t()) ::
{:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
defp add_to_block(%User{} = user, %User{} = blocked) do
- UserRelationship.create_block(user, blocked)
+ with {:ok, relationship} <- UserRelationship.create_block(user, blocked) do
+ @cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
+ {:ok, relationship}
+ end
end
@spec add_to_block(User.t(), User.t()) ::
{:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
defp remove_from_block(%User{} = user, %User{} = blocked) do
- UserRelationship.delete_block(user, blocked)
- end
-
- defp add_to_mutes(%User{} = user, %User{} = muted_user, notifications?) do
- with {:ok, user_mute} <- UserRelationship.create_mute(user, muted_user),
- {:ok, user_notification_mute} <-
- (notifications? && UserRelationship.create_notification_mute(user, muted_user)) ||
- {:ok, nil} do
- {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
- end
- end
-
- defp remove_from_mutes(user, %User{} = muted_user) do
- with {:ok, user_mute} <- UserRelationship.delete_mute(user, muted_user),
- {:ok, user_notification_mute} <-
- UserRelationship.delete_notification_mute(user, muted_user) do
- {:ok, [user_mute, user_notification_mute]}
+ with {:ok, relationship} <- UserRelationship.delete_block(user, blocked) do
+ @cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
+ {:ok, relationship}
end
end
@@ -2409,4 +2471,23 @@ defmodule Pleroma.User do
|> Map.put(:bio, HTML.filter_tags(user.bio, filter))
|> Map.put(:fields, fields)
end
+
+ def get_host(%User{ap_id: ap_id} = _user) do
+ URI.parse(ap_id).host
+ end
+
+ def update_last_active_at(%__MODULE__{local: true} = user) do
+ user
+ |> cast(%{last_active_at: NaiveDateTime.utc_now()}, [:last_active_at])
+ |> update_and_set_cache()
+ end
+
+ def active_user_count(weeks \\ 4) do
+ active_after = Timex.shift(NaiveDateTime.utc_now(), weeks: -weeks)
+
+ __MODULE__
+ |> where([u], u.last_active_at >= ^active_after)
+ |> where([u], u.local == true)
+ |> Repo.aggregate(:count)
+ end
end
diff --git a/lib/pleroma/user/backup.ex b/lib/pleroma/user/backup.ex
new file mode 100644
index 000000000..cba94248f
--- /dev/null
+++ b/lib/pleroma/user/backup.ex
@@ -0,0 +1,258 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.User.Backup do
+ use Ecto.Schema
+
+ import Ecto.Changeset
+ import Ecto.Query
+ import Pleroma.Web.Gettext
+
+ require Pleroma.Constants
+
+ alias Pleroma.Activity
+ alias Pleroma.Bookmark
+ alias Pleroma.Repo
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Transmogrifier
+ alias Pleroma.Web.ActivityPub.UserView
+ alias Pleroma.Workers.BackupWorker
+
+ schema "backups" do
+ field(:content_type, :string)
+ field(:file_name, :string)
+ field(:file_size, :integer, default: 0)
+ field(:processed, :boolean, default: false)
+
+ belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
+
+ timestamps()
+ end
+
+ def create(user, admin_id \\ nil) do
+ with :ok <- validate_email_enabled(),
+ :ok <- validate_user_email(user),
+ :ok <- validate_limit(user, admin_id),
+ {:ok, backup} <- user |> new() |> Repo.insert() do
+ BackupWorker.process(backup, admin_id)
+ end
+ end
+
+ def new(user) do
+ rand_str = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false)
+ datetime = Calendar.NaiveDateTime.Format.iso8601_basic(NaiveDateTime.utc_now())
+ name = "archive-#{user.nickname}-#{datetime}-#{rand_str}.zip"
+
+ %__MODULE__{
+ user_id: user.id,
+ content_type: "application/zip",
+ file_name: name
+ }
+ end
+
+ def delete(backup) do
+ uploader = Pleroma.Config.get([Pleroma.Upload, :uploader])
+
+ with :ok <- uploader.delete_file(Path.join("backups", backup.file_name)) do
+ Repo.delete(backup)
+ end
+ end
+
+ defp validate_limit(_user, admin_id) when is_binary(admin_id), do: :ok
+
+ defp validate_limit(user, nil) do
+ case get_last(user.id) do
+ %__MODULE__{inserted_at: inserted_at} ->
+ days = Pleroma.Config.get([__MODULE__, :limit_days])
+ diff = Timex.diff(NaiveDateTime.utc_now(), inserted_at, :days)
+
+ if diff > days do
+ :ok
+ else
+ {:error,
+ dngettext(
+ "errors",
+ "Last export was less than a day ago",
+ "Last export was less than %{days} days ago",
+ days,
+ days: days
+ )}
+ end
+
+ nil ->
+ :ok
+ end
+ end
+
+ defp validate_email_enabled do
+ if Pleroma.Config.get([Pleroma.Emails.Mailer, :enabled]) do
+ :ok
+ else
+ {:error, dgettext("errors", "Backups require enabled email")}
+ end
+ end
+
+ defp validate_user_email(%User{email: nil}) do
+ {:error, dgettext("errors", "Email is required")}
+ end
+
+ defp validate_user_email(%User{email: email}) when is_binary(email), do: :ok
+
+ def get_last(user_id) do
+ __MODULE__
+ |> where(user_id: ^user_id)
+ |> order_by(desc: :id)
+ |> limit(1)
+ |> Repo.one()
+ end
+
+ def list(%User{id: user_id}) do
+ __MODULE__
+ |> where(user_id: ^user_id)
+ |> order_by(desc: :id)
+ |> Repo.all()
+ end
+
+ def remove_outdated(%__MODULE__{id: latest_id, user_id: user_id}) do
+ __MODULE__
+ |> where(user_id: ^user_id)
+ |> where([b], b.id != ^latest_id)
+ |> Repo.all()
+ |> Enum.each(&BackupWorker.delete/1)
+ end
+
+ def get(id), do: Repo.get(__MODULE__, id)
+
+ def process(%__MODULE__{} = backup) do
+ with {:ok, zip_file} <- export(backup),
+ {:ok, %{size: size}} <- File.stat(zip_file),
+ {:ok, _upload} <- upload(backup, zip_file) do
+ backup
+ |> cast(%{file_size: size, processed: true}, [:file_size, :processed])
+ |> Repo.update()
+ end
+ end
+
+ @files ['actor.json', 'outbox.json', 'likes.json', 'bookmarks.json']
+ def export(%__MODULE__{} = backup) do
+ backup = Repo.preload(backup, :user)
+ name = String.trim_trailing(backup.file_name, ".zip")
+ dir = dir(name)
+
+ with :ok <- File.mkdir(dir),
+ :ok <- actor(dir, backup.user),
+ :ok <- statuses(dir, backup.user),
+ :ok <- likes(dir, backup.user),
+ :ok <- bookmarks(dir, backup.user),
+ {:ok, zip_path} <- :zip.create(String.to_charlist(dir <> ".zip"), @files, cwd: dir),
+ {:ok, _} <- File.rm_rf(dir) do
+ {:ok, to_string(zip_path)}
+ end
+ end
+
+ def dir(name) do
+ dir = Pleroma.Config.get([__MODULE__, :dir]) || System.tmp_dir!()
+ Path.join(dir, name)
+ end
+
+ def upload(%__MODULE__{} = backup, zip_path) do
+ uploader = Pleroma.Config.get([Pleroma.Upload, :uploader])
+
+ upload = %Pleroma.Upload{
+ name: backup.file_name,
+ tempfile: zip_path,
+ content_type: backup.content_type,
+ path: Path.join("backups", backup.file_name)
+ }
+
+ with {:ok, _} <- Pleroma.Uploaders.Uploader.put_file(uploader, upload),
+ :ok <- File.rm(zip_path) do
+ {:ok, upload}
+ end
+ end
+
+ defp actor(dir, user) do
+ with {:ok, json} <-
+ UserView.render("user.json", %{user: user})
+ |> Map.merge(%{"likes" => "likes.json", "bookmarks" => "bookmarks.json"})
+ |> Jason.encode() do
+ File.write(Path.join(dir, "actor.json"), json)
+ end
+ end
+
+ defp write_header(file, name) do
+ IO.write(
+ file,
+ """
+ {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "id": "#{name}.json",
+ "type": "OrderedCollection",
+ "orderedItems": [
+
+ """
+ )
+ end
+
+ defp write(query, dir, name, fun) do
+ path = Path.join(dir, "#{name}.json")
+
+ with {:ok, file} <- File.open(path, [:write, :utf8]),
+ :ok <- write_header(file, name) do
+ total =
+ query
+ |> Pleroma.Repo.chunk_stream(100)
+ |> Enum.reduce(0, fn i, acc ->
+ with {:ok, data} <- fun.(i),
+ {:ok, str} <- Jason.encode(data),
+ :ok <- IO.write(file, str <> ",\n") do
+ acc + 1
+ else
+ _ -> acc
+ end
+ end)
+
+ with :ok <- :file.pwrite(file, {:eof, -2}, "\n],\n \"totalItems\": #{total}}") do
+ File.close(file)
+ end
+ end
+ end
+
+ defp bookmarks(dir, %{id: user_id} = _user) do
+ Bookmark
+ |> where(user_id: ^user_id)
+ |> join(:inner, [b], activity in assoc(b, :activity))
+ |> select([b, a], %{id: b.id, object: fragment("(?)->>'object'", a.data)})
+ |> write(dir, "bookmarks", fn a -> {:ok, a.object} end)
+ end
+
+ defp likes(dir, user) do
+ user.ap_id
+ |> Activity.Queries.by_actor()
+ |> Activity.Queries.by_type("Like")
+ |> select([like], %{id: like.id, object: fragment("(?)->>'object'", like.data)})
+ |> write(dir, "likes", fn a -> {:ok, a.object} end)
+ end
+
+ defp statuses(dir, user) do
+ opts =
+ %{}
+ |> Map.put(:type, ["Create", "Announce"])
+ |> Map.put(:actor_id, user.ap_id)
+
+ [
+ [Pleroma.Constants.as_public(), user.ap_id],
+ User.following(user),
+ Pleroma.List.memberships(user)
+ ]
+ |> Enum.concat()
+ |> ActivityPub.fetch_activities_query(opts)
+ |> write(dir, "outbox", fn a ->
+ with {:ok, activity} <- Transmogrifier.prepare_outgoing(a.data) do
+ {:ok, Map.delete(activity, "@context")}
+ end
+ end)
+ end
+end
diff --git a/lib/pleroma/user/import.ex b/lib/pleroma/user/import.ex
index e458021c8..60cd18041 100644
--- a/lib/pleroma/user/import.ex
+++ b/lib/pleroma/user/import.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.User.Import do
@@ -45,7 +45,7 @@ defmodule Pleroma.User.Import do
identifiers,
fn identifier ->
with {:ok, %User{} = followed} <- User.get_or_fetch(identifier),
- {:ok, follower} <- User.maybe_direct_follow(follower, followed),
+ {:ok, follower, followed} <- User.maybe_direct_follow(follower, followed),
{:ok, _, _, _} <- CommonAPI.follow(follower, followed) do
followed
else
diff --git a/lib/pleroma/user/notification_setting.ex b/lib/pleroma/user/notification_setting.ex
index 7d9e8a000..a7cd61499 100644
--- a/lib/pleroma/user/notification_setting.ex
+++ b/lib/pleroma/user/notification_setting.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.User.NotificationSetting do
diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex
index 2440bf890..fa46545da 100644
--- a/lib/pleroma/user/query.ex
+++ b/lib/pleroma/user/query.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.User.Query do
@@ -43,6 +43,7 @@ defmodule Pleroma.User.Query do
active: boolean(),
deactivated: boolean(),
need_approval: boolean(),
+ unconfirmed: boolean(),
is_admin: boolean(),
is_moderator: boolean(),
super_users: boolean(),
@@ -55,7 +56,8 @@ defmodule Pleroma.User.Query do
ap_id: [String.t()],
order_by: term(),
select: term(),
- limit: pos_integer()
+ limit: pos_integer(),
+ actor_types: [String.t()]
}
| map()
@@ -114,6 +116,10 @@ defmodule Pleroma.User.Query do
where(query, [u], u.is_admin == ^bool)
end
+ defp compose_query({:actor_types, actor_types}, query) when is_list(actor_types) do
+ where(query, [u], u.actor_type in ^actor_types)
+ end
+
defp compose_query({:is_moderator, bool}, query) do
where(query, [u], u.is_moderator == ^bool)
end
@@ -131,8 +137,9 @@ defmodule Pleroma.User.Query do
defp compose_query({:external, _}, query), do: location_query(query, false)
defp compose_query({:active, _}, query) do
- User.restrict_deactivated(query)
- |> where([u], u.approval_pending == false)
+ where(query, [u], u.is_active == true)
+ |> where([u], u.is_approved == true)
+ |> where([u], u.is_confirmed == true)
end
defp compose_query({:legacy_active, _}, query) do
@@ -141,19 +148,23 @@ defmodule Pleroma.User.Query do
end
defp compose_query({:deactivated, false}, query) do
- User.restrict_deactivated(query)
+ where(query, [u], u.is_active == true)
end
defp compose_query({:deactivated, true}, query) do
- where(query, [u], u.deactivated == ^true)
+ where(query, [u], u.is_active == false)
end
defp compose_query({:confirmation_pending, bool}, query) do
- where(query, [u], u.confirmation_pending == ^bool)
+ where(query, [u], u.is_confirmed != ^bool)
end
defp compose_query({:need_approval, _}, query) do
- where(query, [u], u.approval_pending)
+ where(query, [u], u.is_approved == false)
+ end
+
+ defp compose_query({:unconfirmed, _}, query) do
+ where(query, [u], u.is_confirmed == false)
end
defp compose_query({:followers, %User{id: id}}, query) do
diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex
index b54111090..a4f6abca2 100644
--- a/lib/pleroma/user/search.ex
+++ b/lib/pleroma/user/search.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.User.Search do
@@ -85,7 +85,6 @@ defmodule Pleroma.User.Search do
|> base_query(following)
|> filter_blocked_user(for_user)
|> filter_invisible_users()
- |> filter_non_discoverable_users()
|> filter_internal_users()
|> filter_blocked_domains(for_user)
|> fts_search(query_string)
@@ -163,12 +162,6 @@ defmodule Pleroma.User.Search do
from(q in query, where: q.invisible == false)
end
- defp filter_non_discoverable_users(query) do
- # Note: commented out — can't do it with users being non-discoverable by default
- # from(q in query, where: q.is_discoverable == true)
- query
- end
-
defp filter_internal_users(query) do
from(q in query, where: q.actor_type != "Application")
end
diff --git a/lib/pleroma/user/welcome_chat_message.ex b/lib/pleroma/user/welcome_chat_message.ex
index 3e7d1f424..0d6690e34 100644
--- a/lib/pleroma/user/welcome_chat_message.ex
+++ b/lib/pleroma/user/welcome_chat_message.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.User.WelcomeChatMessage do
diff --git a/lib/pleroma/user/welcome_email.ex b/lib/pleroma/user/welcome_email.ex
index 5322000d4..295c1acc7 100644
--- a/lib/pleroma/user/welcome_email.ex
+++ b/lib/pleroma/user/welcome_email.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.User.WelcomeEmail do
diff --git a/lib/pleroma/user/welcome_message.ex b/lib/pleroma/user/welcome_message.ex
index 86e1c0678..2cff05549 100644
--- a/lib/pleroma/user/welcome_message.ex
+++ b/lib/pleroma/user/welcome_message.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.User.WelcomeMessage do
diff --git a/lib/pleroma/user_invite_token.ex b/lib/pleroma/user_invite_token.ex
index a08ba99f1..4cff1c515 100644
--- a/lib/pleroma/user_invite_token.ex
+++ b/lib/pleroma/user_invite_token.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.UserInviteToken do
diff --git a/lib/pleroma/user_relationship.ex b/lib/pleroma/user_relationship.ex
index 6dfdd2860..a467e9b65 100644
--- a/lib/pleroma/user_relationship.ex
+++ b/lib/pleroma/user_relationship.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.UserRelationship do
diff --git a/lib/pleroma/utils.ex b/lib/pleroma/utils.ex
index e95766223..bc0c95332 100644
--- a/lib/pleroma/utils.ex
+++ b/lib/pleroma/utils.ex
@@ -1,8 +1,16 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Utils do
+ @posix_error_codes ~w(
+ eacces eagain ebadf ebadmsg ebusy edeadlk edeadlock edquot eexist efault
+ efbig eftype eintr einval eio eisdir eloop emfile emlink emultihop
+ enametoolong enfile enobufs enodev enolck enolink enoent enomem enospc
+ enosr enostr enosys enotblk enotdir enotsup enxio eopnotsupp eoverflow
+ eperm epipe erange erofs espipe esrch estale etxtbsy exdev
+ )a
+
def compile_dir(dir) when is_binary(dir) do
dir
|> File.ls!()
@@ -22,7 +30,10 @@ defmodule Pleroma.Utils do
"""
@spec command_available?(String.t()) :: boolean()
def command_available?(command) do
- match?({_output, 0}, System.cmd("sh", ["-c", "command -v #{command}"]))
+ case :os.find_executable(String.to_charlist(command)) do
+ false -> false
+ _ -> true
+ end
end
@doc "creates the uniq temporary directory"
@@ -44,4 +55,12 @@ defmodule Pleroma.Utils do
error -> error
end
end
+
+ @spec posix_error_message(atom()) :: binary()
+ def posix_error_message(code) when code in @posix_error_codes do
+ error_message = Gettext.dgettext(Pleroma.Web.Gettext, "posix_errors", "#{code}")
+ "(POSIX error: #{error_message})"
+ end
+
+ def posix_error_message(_), do: ""
end
diff --git a/lib/pleroma/web.ex b/lib/pleroma/web.ex
index 6ed19d3dd..8630f244b 100644
--- a/lib/pleroma/web.ex
+++ b/lib/pleroma/web.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web do
@@ -20,6 +20,7 @@ defmodule Pleroma.Web do
below.
"""
+ alias Pleroma.Helpers.AuthHelper
alias Pleroma.Web.Plugs.EnsureAuthenticatedPlug
alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Web.Plugs.ExpectAuthenticatedCheckPlug
@@ -62,7 +63,8 @@ defmodule Pleroma.Web do
# Executed just before actual controller action, invokes before-action hooks (callbacks)
defp action(conn, params) do
- with %{halted: false} = conn <- maybe_drop_authentication_if_oauth_check_ignored(conn),
+ with %{halted: false} = conn <-
+ maybe_drop_authentication_if_oauth_check_ignored(conn),
%{halted: false} = conn <- maybe_perform_public_or_authenticated_check(conn),
%{halted: false} = conn <- maybe_perform_authenticated_check(conn),
%{halted: false} = conn <- maybe_halt_on_missing_oauth_scopes_check(conn) do
@@ -75,7 +77,7 @@ defmodule Pleroma.Web do
defp maybe_drop_authentication_if_oauth_check_ignored(conn) do
if PlugHelper.plug_called?(conn, ExpectPublicOrAuthenticatedCheckPlug) and
not PlugHelper.plug_called_or_skipped?(conn, OAuthScopesPlug) do
- OAuthScopesPlug.drop_auth_info(conn)
+ AuthHelper.drop_auth_info(conn)
else
conn
end
@@ -231,4 +233,16 @@ defmodule Pleroma.Web do
def base_url do
Pleroma.Web.Endpoint.url()
end
+
+ # TODO: Change to Phoenix.Router.routes/1 for Phoenix 1.6.0+
+ def get_api_routes do
+ Pleroma.Web.Router.__routes__()
+ |> Enum.reject(fn r -> r.plug == Pleroma.Web.Fallback.RedirectController end)
+ |> Enum.map(fn r ->
+ r.path
+ |> String.split("/", trim: true)
+ |> List.first()
+ end)
+ |> Enum.uniq()
+ end
end
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 99c729473..5b45e2ca1 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ActivityPub do
@@ -32,6 +32,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
require Logger
require Pleroma.Constants
+ @behaviour Pleroma.Web.ActivityPub.ActivityPub.Persisting
+ @behaviour Pleroma.Web.ActivityPub.ActivityPub.Streaming
+
defp get_recipients(%{"type" => "Create"} = data) do
to = Map.get(data, "to", [])
cc = Map.get(data, "cc", [])
@@ -53,7 +56,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp check_actor_is_active(actor) when is_binary(actor) do
case User.get_cached_by_ap_id(actor) do
- %User{deactivated: deactivated} -> not deactivated
+ %User{is_active: true} -> true
_ -> false
end
end
@@ -85,13 +88,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp increase_replies_count_if_reply(_create_data), do: :noop
@object_types ~w[ChatMessage Question Answer Audio Video Event Article]
- @spec persist(map(), keyword()) :: {:ok, Activity.t() | Object.t()}
+ @impl true
def persist(%{"type" => type} = object, meta) when type in @object_types do
with {:ok, object} <- Object.create(object) do
{:ok, object, meta}
end
end
+ @impl true
def persist(object, meta) do
with local <- Keyword.fetch!(meta, :local),
{recipients, _, _} <- get_recipients(object),
@@ -123,7 +127,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
# Splice in the child object if we have one.
activity = Maps.put_if_present(activity, :object, object)
- BackgroundWorker.enqueue("fetch_data_for_activity", %{"activity_id" => activity.id})
+ ConcurrentLimiter.limit(Pleroma.Web.RichMedia.Helpers, fn ->
+ Task.start(fn -> Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) end)
+ end)
{:ok, activity}
else
@@ -219,6 +225,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
Streamer.stream("participation", participations)
end
+ @impl true
def stream_out_participations(%Object{data: %{"context" => context}}, user) do
with %Conversation{} = conversation <- Conversation.get_for_ap_id(context) do
conversation = Repo.preload(conversation, :participations)
@@ -235,8 +242,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
+ @impl true
def stream_out_participations(_, _), do: :noop
+ @impl true
def stream_out(%Activity{data: %{"type" => data_type}} = activity)
when data_type in ["Create", "Announce", "Delete"] do
activity
@@ -244,6 +253,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> Streamer.stream(activity)
end
+ @impl true
def stream_out(_activity) do
:noop
end
@@ -367,6 +377,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
:ok <-
maybe_federate(stripped_activity) do
User.all_superusers()
+ |> Enum.filter(fn user -> user.ap_id != actor end)
|> Enum.filter(fn user -> not is_nil(user.email) end)
|> Enum.each(fn superuser ->
superuser
@@ -581,7 +592,21 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> Enum.reverse()
end
- def fetch_user_activities(user, reading_user, params \\ %{}) do
+ def fetch_user_activities(user, reading_user, params \\ %{})
+
+ def fetch_user_activities(user, reading_user, %{total: true} = params) do
+ result = fetch_activities_for_user(user, reading_user, params)
+
+ Keyword.put(result, :items, Enum.reverse(result[:items]))
+ end
+
+ def fetch_user_activities(user, reading_user, params) do
+ user
+ |> fetch_activities_for_user(reading_user, params)
+ |> Enum.reverse()
+ end
+
+ defp fetch_activities_for_user(user, reading_user, params) do
params =
params
|> Map.put(:type, ["Create", "Announce"])
@@ -598,16 +623,28 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> Map.put(:muting_user, reading_user)
end
+ pagination_type = Map.get(params, :pagination_type) || :keyset
+
%{
godmode: params[:godmode],
reading_user: reading_user
}
|> user_activities_recipients()
- |> fetch_activities(params)
- |> Enum.reverse()
+ |> fetch_activities(params, pagination_type)
+ end
+
+ def fetch_statuses(reading_user, %{total: true} = params) do
+ result = fetch_activities_for_reading_user(reading_user, params)
+ Keyword.put(result, :items, Enum.reverse(result[:items]))
end
def fetch_statuses(reading_user, params) do
+ reading_user
+ |> fetch_activities_for_reading_user(params)
+ |> Enum.reverse()
+ end
+
+ defp fetch_activities_for_reading_user(reading_user, params) do
params = Map.put(params, :type, ["Create", "Announce"])
%{
@@ -616,7 +653,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
}
|> user_activities_recipients()
|> fetch_activities(params, :offset)
- |> Enum.reverse()
end
defp user_activities_recipients(%{godmode: true}), do: []
@@ -723,6 +759,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_local(query, _), do: query
+ defp restrict_remote(query, %{remote: true}) do
+ from(activity in query, where: activity.local == false)
+ end
+
+ defp restrict_remote(query, _), do: query
+
defp restrict_actor(query, %{actor_id: actor_id}) do
from(activity in query, where: activity.actor == ^actor_id)
end
@@ -836,7 +878,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
query =
from([activity] in query,
where: fragment("not (? = ANY(?))", activity.actor, ^mutes),
- where: fragment("not (?->'to' \\?| ?)", activity.data, ^mutes)
+ where:
+ fragment(
+ "not (?->'to' \\?| ?) or ? = ?",
+ activity.data,
+ ^mutes,
+ activity.actor,
+ ^user.ap_id
+ )
)
unless opts[:skip_preload] do
@@ -939,16 +988,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_muted_reblogs(query, _), do: query
- defp restrict_instance(query, %{instance: instance}) do
- users =
- from(
- u in User,
- select: u.ap_id,
- where: fragment("? LIKE ?", u.nickname, ^"%@#{instance}")
- )
- |> Repo.all()
-
- from(activity in query, where: activity.actor in ^users)
+ defp restrict_instance(query, %{instance: instance}) when is_binary(instance) do
+ from(
+ activity in query,
+ where: fragment("split_part(actor::text, '/'::text, 3) = ?", ^instance)
+ )
end
defp restrict_instance(query, _), do: query
@@ -1097,6 +1141,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> restrict_tag_all(opts)
|> restrict_since(opts)
|> restrict_local(opts)
+ |> restrict_remote(opts)
|> restrict_actor(opts)
|> restrict_type(opts)
|> restrict_state(opts)
@@ -1241,7 +1286,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
capabilities = data["capabilities"] || %{}
accepts_chat_messages = capabilities["acceptsChatMessages"]
data = Transmogrifier.maybe_fix_user_object(data)
- discoverable = data["discoverable"] || false
+ is_discoverable = data["discoverable"] || false
invisible = data["invisible"] || false
actor_type = data["type"] || "Person"
@@ -1267,7 +1312,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
fields: fields,
emoji: emojis,
is_locked: is_locked,
- discoverable: discoverable,
+ is_discoverable: is_discoverable,
invisible: invisible,
avatar: avatar,
name: data["name"],
@@ -1296,12 +1341,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
def fetch_follow_information_for_user(user) do
with {:ok, following_data} <-
- Fetcher.fetch_and_contain_remote_object_from_id(user.following_address,
- force_http: true
- ),
+ Fetcher.fetch_and_contain_remote_object_from_id(user.following_address),
{:ok, hide_follows} <- collection_private(following_data),
{:ok, followers_data} <-
- Fetcher.fetch_and_contain_remote_object_from_id(user.follower_address, force_http: true),
+ Fetcher.fetch_and_contain_remote_object_from_id(user.follower_address),
{:ok, hide_followers} <- collection_private(followers_data) do
{:ok,
%{
@@ -1375,8 +1418,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
- def fetch_and_prepare_user_from_ap_id(ap_id, opts \\ []) do
- with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id, opts),
+ def fetch_and_prepare_user_from_ap_id(ap_id) do
+ with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id),
{:ok, data} <- user_data_from_user_object(data) do
{:ok, maybe_update_follow_information(data)}
else
@@ -1419,13 +1462,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
- def make_user_from_ap_id(ap_id, opts \\ []) do
+ def make_user_from_ap_id(ap_id) do
user = User.get_cached_by_ap_id(ap_id)
if user && !User.ap_enabled?(user) do
Transmogrifier.upgrade_user_from_ap_id(ap_id)
else
- with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id, opts) do
+ with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id) do
if user do
user
|> User.remote_user_changeset(data)
diff --git a/lib/pleroma/web/activity_pub/activity_pub/persisting.ex b/lib/pleroma/web/activity_pub/activity_pub/persisting.ex
new file mode 100644
index 000000000..5ec8b7bab
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/activity_pub/persisting.ex
@@ -0,0 +1,7 @@
+# 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.ActivityPub.Persisting do
+ @callback persist(map(), keyword()) :: {:ok, Activity.t() | Object.t()}
+end
diff --git a/lib/pleroma/web/activity_pub/activity_pub/streaming.ex b/lib/pleroma/web/activity_pub/activity_pub/streaming.ex
new file mode 100644
index 000000000..983168bff
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/activity_pub/streaming.ex
@@ -0,0 +1,12 @@
+# 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.ActivityPub.Streaming do
+ alias Pleroma.Activity
+ alias Pleroma.Object
+ alias Pleroma.User
+
+ @callback stream_out(Activity.t()) :: any()
+ @callback stream_out_participations(Object.t(), User.t()) :: any()
+end
diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
index 44f09be75..9d3dcc7f9 100644
--- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ActivityPubController do
@@ -79,10 +79,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
end
end
- def object(conn, _) do
+ def object(%{assigns: assigns} = conn, _) do
with ap_id <- Endpoint.url() <> conn.request_path,
%Object{} = object <- Object.get_cached_by_ap_id(ap_id),
- {_, true} <- {:public?, Visibility.is_public?(object)} do
+ user <- Map.get(assigns, :user, nil),
+ {_, true} <- {:visible?, Visibility.visible_for_user?(object, user)} do
conn
|> assign(:tracking_fun_data, object.id)
|> set_cache_ttl_for(object)
@@ -90,8 +91,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|> put_view(ObjectView)
|> render("object.json", object: object)
else
- {:public?, false} ->
- {:error, :not_found}
+ {:visible?, false} -> {:error, :not_found}
+ nil -> {:error, :not_found}
end
end
@@ -105,10 +106,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
conn
end
- def activity(conn, _params) do
+ def activity(%{assigns: assigns} = conn, _) do
with ap_id <- Endpoint.url() <> conn.request_path,
%Activity{} = activity <- Activity.normalize(ap_id),
- {_, true} <- {:public?, Visibility.is_public?(activity)} do
+ {_, true} <- {:local?, activity.local},
+ user <- Map.get(assigns, :user, nil),
+ {_, true} <- {:visible?, Visibility.visible_for_user?(activity, user)} do
conn
|> maybe_set_tracking_data(activity)
|> set_cache_ttl_for(activity)
@@ -116,13 +119,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|> put_view(ObjectView)
|> render("object.json", object: activity)
else
- {:public?, false} -> {:error, :not_found}
+ {:visible?, false} -> {:error, :not_found}
+ {:local?, false} -> {:error, :not_found}
nil -> {:error, :not_found}
end
end
defp maybe_set_tracking_data(conn, %Activity{data: %{"type" => "Create"}} = activity) do
- object_id = Object.normalize(activity).id
+ object_id = Object.normalize(activity, fetch: false).id
assign(conn, :tracking_fun_data, object_id)
end
@@ -428,7 +432,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
end
defp handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do
- with %Object{} = object <- Object.normalize(params["object"]),
+ with %Object{} = object <- Object.normalize(params["object"], fetch: false),
true <- user.is_moderator || user.ap_id == object.data["actor"],
{:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
{:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
@@ -439,7 +443,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
end
defp handle_user_activity(%User{} = user, %{"type" => "Like"} = params) do
- with %Object{} = object <- Object.normalize(params["object"]),
+ with %Object{} = object <- Object.normalize(params["object"], fetch: false),
{_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
{_, {:ok, %Activity{} = activity, _meta}} <-
{:common_pipeline,
@@ -525,19 +529,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
{new_user, for_user}
end
- @doc """
- Endpoint based on <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>
-
- Parameters:
- - (required) `file`: data of the media
- - (optionnal) `description`: description of the media, intended for accessibility
-
- Response:
- - HTTP Code: 201 Created
- - HTTP Body: ActivityPub object to be inserted into another's `attachment` field
-
- Note: Will not point to a URL with a `Location` header because no standalone Activity has been created.
- """
def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
with {:ok, object} <-
ActivityPub.upload(
diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex
index 298aff6b7..f56bfc600 100644
--- a/lib/pleroma/web/activity_pub/builder.ex
+++ b/lib/pleroma/web/activity_pub/builder.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Builder do
@@ -80,7 +80,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do
@spec delete(User.t(), String.t()) :: {:ok, map(), keyword()}
def delete(actor, object_id) do
- object = Object.normalize(object_id, false)
+ object = Object.normalize(object_id, fetch: false)
user = !object && User.get_cached_by_ap_id(object_id)
@@ -222,6 +222,9 @@ defmodule Pleroma.Web.ActivityPub.Builder do
actor.ap_id == Relay.ap_id() ->
[actor.follower_address]
+ public? and Visibility.is_local_public?(object) ->
+ [actor.follower_address, object.data["actor"], Pleroma.Constants.as_local_public()]
+
public? ->
[actor.follower_address, object.data["actor"], Pleroma.Constants.as_public()]
diff --git a/lib/pleroma/web/activity_pub/internal_fetch_actor.ex b/lib/pleroma/web/activity_pub/internal_fetch_actor.ex
index c80272b8f..ca76071e5 100644
--- a/lib/pleroma/web/activity_pub/internal_fetch_actor.ex
+++ b/lib/pleroma/web/activity_pub/internal_fetch_actor.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.InternalFetchActor do
diff --git a/lib/pleroma/web/activity_pub/mrf.ex b/lib/pleroma/web/activity_pub/mrf.ex
index 5e5361082..ef5a09a93 100644
--- a/lib/pleroma/web/activity_pub/mrf.ex
+++ b/lib/pleroma/web/activity_pub/mrf.ex
@@ -1,9 +1,66 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF do
+ require Logger
+
+ @behaviour Pleroma.Web.ActivityPub.MRF.PipelineFiltering
+
+ @mrf_config_descriptions [
+ %{
+ group: :pleroma,
+ key: :mrf,
+ tab: :mrf,
+ label: "MRF",
+ type: :group,
+ description: "General MRF settings",
+ children: [
+ %{
+ key: :policies,
+ type: [:module, {:list, :module}],
+ description:
+ "A list of MRF policies enabled. Module names are shortened (removed leading `Pleroma.Web.ActivityPub.MRF.` part), but on adding custom module you need to use full name.",
+ suggestions: {:list_behaviour_implementations, Pleroma.Web.ActivityPub.MRF}
+ },
+ %{
+ key: :transparency,
+ label: "MRF transparency",
+ type: :boolean,
+ description:
+ "Make the content of your Message Rewrite Facility settings public (via nodeinfo)"
+ },
+ %{
+ key: :transparency_exclusions,
+ label: "MRF transparency exclusions",
+ type: {:list, :string},
+ description:
+ "Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.",
+ suggestions: [
+ "exclusion.com"
+ ]
+ }
+ ]
+ }
+ ]
+
+ @default_description %{
+ label: "",
+ description: ""
+ }
+
+ @required_description_keys [:key, :related_policy]
+
@callback filter(Map.t()) :: {:ok | :reject, Map.t()}
+ @callback describe() :: {:ok | :error, Map.t()}
+ @callback config_description() :: %{
+ optional(:children) => [map()],
+ key: atom(),
+ related_policy: String.t(),
+ label: String.t(),
+ description: String.t()
+ }
+ @optional_callbacks config_description: 0
def filter(policies, %{} = message) do
policies
@@ -15,6 +72,7 @@ defmodule Pleroma.Web.ActivityPub.MRF do
def filter(%{} = object), do: get_policies() |> filter(object)
+ @impl true
def pipeline_filter(%{} = message, meta) do
object = meta[:object_data]
ap_id = message["object"]
@@ -51,8 +109,6 @@ defmodule Pleroma.Web.ActivityPub.MRF do
Enum.any?(domains, fn domain -> Regex.match?(domain, host) end)
end
- @callback describe() :: {:ok | :error, Map.t()}
-
def describe(policies) do
{:ok, policy_configs} =
policies
@@ -82,4 +138,41 @@ defmodule Pleroma.Web.ActivityPub.MRF do
end
def describe, do: get_policies() |> describe()
+
+ def config_descriptions do
+ Pleroma.Web.ActivityPub.MRF
+ |> Pleroma.Docs.Generator.list_behaviour_implementations()
+ |> config_descriptions()
+ end
+
+ def config_descriptions(policies) do
+ Enum.reduce(policies, @mrf_config_descriptions, fn policy, acc ->
+ if function_exported?(policy, :config_description, 0) do
+ description =
+ @default_description
+ |> Map.merge(policy.config_description)
+ |> Map.put(:group, :pleroma)
+ |> Map.put(:tab, :mrf)
+ |> Map.put(:type, :group)
+
+ if Enum.all?(@required_description_keys, &Map.has_key?(description, &1)) do
+ [description | acc]
+ else
+ Logger.warn(
+ "#{policy} config description doesn't have one or all required keys #{
+ inspect(@required_description_keys)
+ }"
+ )
+
+ acc
+ end
+ else
+ Logger.debug(
+ "#{policy} is excluded from config descriptions, because does not implement `config_description/0` method."
+ )
+
+ acc
+ end
+ end)
+ end
end
diff --git a/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex b/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex
index bee47b4ed..fc347236e 100644
--- a/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy do
@@ -40,4 +40,22 @@ defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy do
_ -> Map.put(activity, "expires_at", expires_at)
end
end
+
+ @impl true
+ def config_description do
+ %{
+ key: :mrf_activity_expiration,
+ related_policy: "Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy",
+ label: "MRF Activity Expiration Policy",
+ description: "Adds automatic expiration to all local activities",
+ children: [
+ %{
+ key: :days,
+ type: :integer,
+ description: "Default global expiration time for all local activities (in days)",
+ suggestions: [90, 365]
+ }
+ ]
+ }
+ end
end
diff --git a/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex
index b96388489..b8bfdc3ce 100644
--- a/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy do
diff --git a/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex
index b22464111..40b19c3ab 100644
--- a/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy do
diff --git a/lib/pleroma/web/activity_pub/mrf/drop_policy.ex b/lib/pleroma/web/activity_pub/mrf/drop_policy.ex
index 5ab9844ff..378175205 100644
--- a/lib/pleroma/web/activity_pub/mrf/drop_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/drop_policy.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.DropPolicy do
diff --git a/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex b/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex
index 3bf70b894..2d3a10889 100644
--- a/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex
+++ b/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do
@@ -31,7 +31,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do
when is_map(child_object) do
child =
child_object["inReplyTo"]
- |> Object.normalize(child_object["inReplyTo"])
+ |> Object.normalize(fetch: false)
|> filter_by_summary(child_object)
object = Map.put(object, "object", child)
diff --git a/lib/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy.ex b/lib/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy.ex
index ea9c3d3f5..51dbb1ad4 100644
--- a/lib/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy do
diff --git a/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex b/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex
index 9ba07b4e3..768a669f3 100644
--- a/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do
@@ -97,4 +97,31 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do
@impl true
def describe,
do: {:ok, %{mrf_hellthread: Pleroma.Config.get(:mrf_hellthread) |> Enum.into(%{})}}
+
+ @impl true
+ def config_description do
+ %{
+ key: :mrf_hellthread,
+ related_policy: "Pleroma.Web.ActivityPub.MRF.HellthreadPolicy",
+ label: "MRF Hellthread",
+ description: "Block messages with excessive user mentions",
+ children: [
+ %{
+ key: :delist_threshold,
+ type: :integer,
+ description:
+ "Number of mentioned users after which the message gets removed from timelines and" <>
+ "disables notifications. Set to 0 to disable.",
+ suggestions: [10]
+ },
+ %{
+ key: :reject_threshold,
+ type: :integer,
+ description:
+ "Number of mentioned users after which the messaged gets rejected. Set to 0 to disable.",
+ suggestions: [20]
+ }
+ ]
+ }
+ end
end
diff --git a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex
index db66cfa3e..f91b51bcf 100644
--- a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
@@ -126,4 +126,46 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
{:ok, %{mrf_keyword: mrf_keyword}}
end
+
+ @impl true
+ def config_description do
+ %{
+ key: :mrf_keyword,
+ related_policy: "Pleroma.Web.ActivityPub.MRF.KeywordPolicy",
+ label: "MRF Keyword",
+ description:
+ "Reject or Word-Replace messages matching a keyword or [Regex](https://hexdocs.pm/elixir/Regex.html).",
+ children: [
+ %{
+ key: :reject,
+ type: {:list, :string},
+ description: """
+ A list of patterns which result in message being rejected.
+
+ Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
+ """,
+ suggestions: ["foo", ~r/foo/iu]
+ },
+ %{
+ key: :federated_timeline_removal,
+ type: {:list, :string},
+ description: """
+ A list of patterns which result in message being removed from federated timelines (a.k.a unlisted).
+
+ Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
+ """,
+ suggestions: ["foo", ~r/foo/iu]
+ },
+ %{
+ key: :replace,
+ type: {:list, :tuple},
+ description: """
+ **Pattern**: a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
+
+ **Replacement**: a string. Leaving the field empty is permitted.
+ """
+ }
+ ]
+ }
+ end
end
diff --git a/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex b/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex
index 0fb05d3c4..8dbf44071 100644
--- a/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do
@@ -8,7 +8,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do
alias Pleroma.HTTP
alias Pleroma.Web.MediaProxy
- alias Pleroma.Workers.BackgroundWorker
require Logger
@@ -17,7 +16,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do
recv_timeout: 10_000
]
- def perform(:prefetch, url) do
+ defp prefetch(url) do
# Fetching only proxiable resources
if MediaProxy.enabled?() and MediaProxy.url_proxiable?(url) do
# If preview proxy is enabled, it'll also hit media proxy (so we're caching both requests)
@@ -25,17 +24,25 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do
Logger.debug("Prefetching #{inspect(url)} as #{inspect(prefetch_url)}")
- HTTP.get(prefetch_url, [], @adapter_options)
+ if Pleroma.Config.get(:env) == :test do
+ fetch(prefetch_url)
+ else
+ ConcurrentLimiter.limit(__MODULE__, fn ->
+ Task.start(fn -> fetch(prefetch_url) end)
+ end)
+ end
end
end
- def perform(:preload, %{"object" => %{"attachment" => attachments}} = _message) do
+ defp fetch(url), do: HTTP.get(url, [], @adapter_options)
+
+ defp preload(%{"object" => %{"attachment" => attachments}} = _message) do
Enum.each(attachments, fn
%{"url" => url} when is_list(url) ->
url
|> Enum.each(fn
%{"href" => href} ->
- BackgroundWorker.enqueue("media_proxy_prefetch", %{"url" => href})
+ prefetch(href)
x ->
Logger.debug("Unhandled attachment URL object #{inspect(x)}")
@@ -51,7 +58,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do
%{"type" => "Create", "object" => %{"attachment" => attachments} = _object} = message
)
when is_list(attachments) and length(attachments) > 0 do
- BackgroundWorker.enqueue("media_proxy_preload", %{"message" => message})
+ preload(message)
{:ok, message}
end
diff --git a/lib/pleroma/web/activity_pub/mrf/mention_policy.ex b/lib/pleroma/web/activity_pub/mrf/mention_policy.ex
index 7910ca131..877277d4f 100644
--- a/lib/pleroma/web/activity_pub/mrf/mention_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/mention_policy.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.MentionPolicy do
@@ -25,4 +25,22 @@ defmodule Pleroma.Web.ActivityPub.MRF.MentionPolicy do
@impl true
def describe, do: {:ok, %{}}
+
+ @impl true
+ def config_description do
+ %{
+ key: :mrf_mention,
+ related_policy: "Pleroma.Web.ActivityPub.MRF.MentionPolicy",
+ label: "MRF Mention",
+ description: "Block messages which mention a specific user",
+ children: [
+ %{
+ key: :actors,
+ type: {:list, :string},
+ description: "A list of actors for which any post mentioning them will be dropped",
+ suggestions: ["actor1", "actor2"]
+ }
+ ]
+ }
+ end
end
diff --git a/lib/pleroma/web/activity_pub/mrf/no_empty_policy.ex b/lib/pleroma/web/activity_pub/mrf/no_empty_policy.ex
new file mode 100644
index 000000000..32bb1b645
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/mrf/no_empty_policy.ex
@@ -0,0 +1,61 @@
+# 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.NoEmptyPolicy do
+ @moduledoc "Filter local activities which have no content"
+ @behaviour Pleroma.Web.ActivityPub.MRF
+
+ alias Pleroma.Web
+
+ @impl true
+ def filter(%{"actor" => actor} = object) do
+ with true <- is_local?(actor),
+ true <- is_note?(object),
+ false <- has_attachment?(object),
+ true <- only_mentions?(object) do
+ {:reject, "[NoEmptyPolicy]"}
+ else
+ _ ->
+ {:ok, object}
+ end
+ end
+
+ def filter(object), do: {:ok, object}
+
+ defp is_local?(actor) do
+ if actor |> String.starts_with?("#{Web.base_url()}") do
+ true
+ else
+ false
+ end
+ end
+
+ defp has_attachment?(%{
+ "type" => "Create",
+ "object" => %{"type" => "Note", "attachment" => attachments}
+ })
+ when length(attachments) > 0,
+ do: true
+
+ defp has_attachment?(_), do: false
+
+ defp only_mentions?(%{"type" => "Create", "object" => %{"type" => "Note", "source" => source}}) do
+ non_mentions =
+ source |> String.split() |> Enum.filter(&(not String.starts_with?(&1, "@"))) |> length
+
+ if non_mentions > 0 do
+ false
+ else
+ true
+ end
+ end
+
+ defp only_mentions?(_), do: false
+
+ defp is_note?(%{"type" => "Create", "object" => %{"type" => "Note"}}), do: true
+ defp is_note?(_), do: false
+
+ @impl true
+ def describe, do: {:ok, %{}}
+end
diff --git a/lib/pleroma/web/activity_pub/mrf/no_op_policy.ex b/lib/pleroma/web/activity_pub/mrf/no_op_policy.ex
index cc2ac9d08..2ebc0674d 100644
--- a/lib/pleroma/web/activity_pub/mrf/no_op_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/no_op_policy.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.NoOpPolicy do
diff --git a/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex b/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex
index fc3475048..b658d7d41 100644
--- a/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicy do
diff --git a/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex b/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex
index 7abae37ae..2ad3fde0b 100644
--- a/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex
+++ b/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkup do
@@ -8,6 +8,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkup do
@behaviour Pleroma.Web.ActivityPub.MRF
+ @impl true
def filter(%{"type" => "Create", "object" => child_object} = object) do
scrub_policy = Pleroma.Config.get([:mrf_normalize_markup, :scrub_policy])
@@ -22,5 +23,23 @@ defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkup do
def filter(object), do: {:ok, object}
+ @impl true
def describe, do: {:ok, %{}}
+
+ @impl true
+ def config_description do
+ %{
+ key: :mrf_normalize_markup,
+ related_policy: "Pleroma.Web.ActivityPub.MRF.NormalizeMarkup",
+ label: "MRF Normalize Markup",
+ description: "MRF NormalizeMarkup settings. Scrub configured hypertext markup.",
+ children: [
+ %{
+ key: :scrub_policy,
+ type: :module,
+ suggestions: [Pleroma.HTML.Scrubber.Default]
+ }
+ ]
+ }
+ end
end
diff --git a/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex b/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex
index d45d2d7e3..aac24c0ec 100644
--- a/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
@@ -106,4 +106,32 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
{:ok, %{mrf_object_age: mrf_object_age}}
end
+
+ @impl true
+ def config_description do
+ %{
+ key: :mrf_object_age,
+ related_policy: "Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy",
+ label: "MRF Object Age",
+ description:
+ "Rejects or delists posts based on their timestamp deviance from your server's clock.",
+ children: [
+ %{
+ key: :threshold,
+ type: :integer,
+ description: "Required age (in seconds) of a post before actions are taken.",
+ suggestions: [172_800]
+ },
+ %{
+ key: :actions,
+ type: {:list, :atom},
+ description:
+ "A list of actions to apply to the post. `:delist` removes the post from public timelines; " <>
+ "`:strip_followers` removes followers from the ActivityPub recipient list ensuring they won't be delivered to home timelines; " <>
+ "`:reject` rejects the message entirely",
+ suggestions: [:delist, :strip_followers, :reject]
+ }
+ ]
+ }
+ end
end
diff --git a/lib/pleroma/web/activity_pub/mrf/pipeline_filtering.ex b/lib/pleroma/web/activity_pub/mrf/pipeline_filtering.ex
new file mode 100644
index 000000000..be95e38ec
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/mrf/pipeline_filtering.ex
@@ -0,0 +1,7 @@
+# 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.PipelineFiltering do
+ @callback pipeline_filter(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
+end
diff --git a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex
index 0b9ed2224..47a43c6a2 100644
--- a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex
+++ b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do
@@ -48,4 +48,27 @@ defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do
@impl true
def describe,
do: {:ok, %{mrf_rejectnonpublic: Config.get(:mrf_rejectnonpublic) |> Enum.into(%{})}}
+
+ @impl true
+ def config_description do
+ %{
+ key: :mrf_rejectnonpublic,
+ related_policy: "Pleroma.Web.ActivityPub.MRF.RejectNonPublic",
+ description: "RejectNonPublic drops posts with non-public visibility settings.",
+ label: "MRF Reject Non Public",
+ children: [
+ %{
+ key: :allow_followersonly,
+ label: "Allow followers-only",
+ type: :boolean,
+ description: "Whether to allow followers-only posts"
+ },
+ %{
+ key: :allow_direct,
+ type: :boolean,
+ description: "Whether to allow direct messages"
+ }
+ ]
+ }
+ end
end
diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
index 161177727..bb3838d2c 100644
--- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
@@ -244,4 +244,78 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
{:ok, %{mrf_simple: mrf_simple}}
end
+
+ @impl true
+ def config_description do
+ %{
+ key: :mrf_simple,
+ related_policy: "Pleroma.Web.ActivityPub.MRF.SimplePolicy",
+ label: "MRF Simple",
+ description: "Simple ingress policies",
+ children: [
+ %{
+ key: :media_removal,
+ type: {:list, :string},
+ description: "List of instances to strip media attachments from",
+ suggestions: ["example.com", "*.example.com"]
+ },
+ %{
+ key: :media_nsfw,
+ label: "Media NSFW",
+ type: {:list, :string},
+ description: "List of instances to tag all media as NSFW (sensitive) from",
+ suggestions: ["example.com", "*.example.com"]
+ },
+ %{
+ key: :federated_timeline_removal,
+ type: {:list, :string},
+ description:
+ "List of instances to remove from the Federated (aka The Whole Known Network) Timeline",
+ suggestions: ["example.com", "*.example.com"]
+ },
+ %{
+ key: :reject,
+ type: {:list, :string},
+ description: "List of instances to reject activities from (except deletes)",
+ suggestions: ["example.com", "*.example.com"]
+ },
+ %{
+ key: :accept,
+ type: {:list, :string},
+ description: "List of instances to only accept activities from (except deletes)",
+ suggestions: ["example.com", "*.example.com"]
+ },
+ %{
+ key: :followers_only,
+ type: {:list, :string},
+ description: "Force posts from the given instances to be visible by followers only",
+ suggestions: ["example.com", "*.example.com"]
+ },
+ %{
+ key: :report_removal,
+ type: {:list, :string},
+ description: "List of instances to reject reports from",
+ suggestions: ["example.com", "*.example.com"]
+ },
+ %{
+ key: :avatar_removal,
+ type: {:list, :string},
+ description: "List of instances to strip avatars from",
+ suggestions: ["example.com", "*.example.com"]
+ },
+ %{
+ key: :banner_removal,
+ type: {:list, :string},
+ description: "List of instances to strip banners from",
+ suggestions: ["example.com", "*.example.com"]
+ },
+ %{
+ key: :reject_deletes,
+ type: {:list, :string},
+ description: "List of instances to reject deletions from",
+ suggestions: ["example.com", "*.example.com"]
+ }
+ ]
+ }
+ end
end
diff --git a/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex b/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex
index 788f21261..4c5e33619 100644
--- a/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
diff --git a/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex b/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex
index 048052da6..86965d47b 100644
--- a/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.SubchainPolicy do
@@ -39,4 +39,28 @@ defmodule Pleroma.Web.ActivityPub.MRF.SubchainPolicy do
@impl true
def describe, do: {:ok, %{}}
+
+ @impl true
+ def config_description do
+ %{
+ key: :mrf_subchain,
+ related_policy: "Pleroma.Web.ActivityPub.MRF.SubchainPolicy",
+ label: "MRF Subchain",
+ description:
+ "This policy processes messages through an alternate pipeline when a given message matches certain criteria." <>
+ " All criteria are configured as a map of regular expressions to lists of policy modules.",
+ children: [
+ %{
+ key: :match_actor,
+ type: {:map, {:list, :string}},
+ description: "Matches a series of regular expressions against the actor field",
+ suggestions: [
+ %{
+ ~r/https:\/\/example.com/s => [Pleroma.Web.ActivityPub.MRF.DropPolicy]
+ }
+ ]
+ }
+ ]
+ }
+ end
end
diff --git a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex
index febabda08..5739cee63 100644
--- a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do
diff --git a/lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex b/lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex
index 1a28f2ba2..65b371bf3 100644
--- a/lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy do
@@ -41,4 +41,25 @@ defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy do
{:ok, %{mrf_user_allowlist: mrf_user_allowlist}}
end
+
+ # TODO: change way of getting settings on `lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex:18` to use `hosts` subkey
+ # @impl true
+ # def config_description do
+ # %{
+ # key: :mrf_user_allowlist,
+ # related_policy: "Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy",
+ # description: "Accept-list of users from specified instances",
+ # children: [
+ # %{
+ # key: :hosts,
+ # type: :map,
+ # description:
+ # "The keys in this section are the domain names that the policy should apply to." <>
+ # " Each key should be assigned a list of users that should be allowed " <>
+ # "through by their ActivityPub ID",
+ # suggestions: [%{"example.org" => ["https://example.org/users/admin"]}]
+ # }
+ # ]
+ # }
+ # end
end
diff --git a/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex b/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex
index a6c545570..ce559a239 100644
--- a/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicy do
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicy do
@behaviour Pleroma.Web.ActivityPub.MRF
+ @impl true
def filter(%{"type" => "Undo", "object" => child_message} = message) do
with {:ok, _} <- filter(child_message) do
{:ok, message}
@@ -36,6 +37,33 @@ defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicy do
def filter(message), do: {:ok, message}
+ @impl true
def describe,
do: {:ok, %{mrf_vocabulary: Pleroma.Config.get(:mrf_vocabulary) |> Enum.into(%{})}}
+
+ @impl true
+ def config_description do
+ %{
+ key: :mrf_vocabulary,
+ related_policy: "Pleroma.Web.ActivityPub.MRF.VocabularyPolicy",
+ label: "MRF Vocabulary",
+ description: "Filter messages which belong to certain activity vocabularies",
+ children: [
+ %{
+ key: :accept,
+ type: {:list, :string},
+ description:
+ "A list of ActivityStreams terms to accept. If empty, all supported messages are accepted.",
+ suggestions: ["Create", "Follow", "Mention", "Announce", "Like"]
+ },
+ %{
+ key: :reject,
+ type: {:list, :string},
+ description:
+ "A list of ActivityStreams terms to reject. If empty, no messages are rejected.",
+ suggestions: ["Create", "Follow", "Mention", "Announce", "Like"]
+ }
+ ]
+ }
+ end
end
diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex
index bd0a2a8dc..297c19cc0 100644
--- a/lib/pleroma/web/activity_pub/object_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validator.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidator do
@@ -9,6 +9,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
the system.
"""
+ @behaviour Pleroma.Web.ActivityPub.ObjectValidator.Validating
+
alias Pleroma.Activity
alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Object
@@ -32,7 +34,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator
- @spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
+ @impl true
def validate(object, meta)
def validate(%{"type" => type} = object, meta)
@@ -286,7 +288,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
def fetch_actor_and_object(object) do
fetch_actor(object)
- Object.normalize(object["object"], true)
+ Object.normalize(object["object"], fetch: true)
:ok
end
end
diff --git a/lib/pleroma/web/activity_pub/object_validator/validating.ex b/lib/pleroma/web/activity_pub/object_validator/validating.ex
new file mode 100644
index 000000000..28e8d2498
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validator/validating.ex
@@ -0,0 +1,7 @@
+# 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.ObjectValidator.Validating do
+ @callback validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
+end
diff --git a/lib/pleroma/web/activity_pub/object_validators/accept_reject_validator.ex b/lib/pleroma/web/activity_pub/object_validators/accept_reject_validator.ex
index 179beda58..d31e780c3 100644
--- a/lib/pleroma/web/activity_pub/object_validators/accept_reject_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/accept_reject_validator.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.AcceptRejectValidator do
diff --git a/lib/pleroma/web/activity_pub/object_validators/announce_validator.ex b/lib/pleroma/web/activity_pub/object_validators/announce_validator.ex
index 6f757f49c..b08a33e68 100644
--- a/lib/pleroma/web/activity_pub/object_validators/announce_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/announce_validator.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator do
@@ -67,7 +67,12 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator do
%Object{} = object <- Object.get_cached_by_ap_id(object),
false <- Visibility.is_public?(object) do
same_actor = object.data["actor"] == actor.ap_id
- is_public = Pleroma.Constants.as_public() in (get_field(cng, :to) ++ get_field(cng, :cc))
+ recipients = get_field(cng, :to) ++ get_field(cng, :cc)
+ local_public = Pleroma.Constants.as_local_public()
+
+ is_public =
+ Enum.member?(recipients, Pleroma.Constants.as_public()) or
+ Enum.member?(recipients, local_public)
cond do
same_actor && is_public ->
diff --git a/lib/pleroma/web/activity_pub/object_validators/answer_validator.ex b/lib/pleroma/web/activity_pub/object_validators/answer_validator.ex
index b9fbaf4f6..15e4413cd 100644
--- a/lib/pleroma/web/activity_pub/object_validators/answer_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/answer_validator.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnswerValidator do
diff --git a/lib/pleroma/web/activity_pub/object_validators/article_note_validator.ex b/lib/pleroma/web/activity_pub/object_validators/article_note_validator.ex
index 5b7dad517..b0388ef3b 100644
--- a/lib/pleroma/web/activity_pub/object_validators/article_note_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/article_note_validator.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidator do
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 df102a134..3175427ad 100644
--- a/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do
@@ -15,6 +15,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do
field(:type, :string)
field(:mediaType, :string, default: "application/octet-stream")
field(:name, :string)
+ field(:blurhash, :string)
embeds_many :url, UrlObjectValidator, primary_key: false do
field(:type, :string)
@@ -41,7 +42,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do
|> fix_url()
struct
- |> cast(data, [:type, :mediaType, :name])
+ |> cast(data, [:type, :mediaType, :name, :blurhash])
|> cast_embed(:url, with: &url_changeset/2)
|> validate_inclusion(:type, ~w[Link Document Audio Image Video])
|> validate_required([:type, :mediaType, :url])
diff --git a/lib/pleroma/web/activity_pub/object_validators/audio_video_validator.ex b/lib/pleroma/web/activity_pub/object_validators/audio_video_validator.ex
index 16973e5db..4a96fef52 100644
--- a/lib/pleroma/web/activity_pub/object_validators/audio_video_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/audio_video_validator.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator do
@@ -70,19 +70,33 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator do
|> changeset(data)
end
- defp fix_url(%{"url" => url} = data) when is_list(url) do
- attachment =
- Enum.find(url, fn x ->
- mime_type = x["mimeType"] || x["mediaType"] || ""
+ defp find_attachment(url) do
+ mpeg_url =
+ Enum.find(url, fn
+ %{"mediaType" => mime_type, "tag" => tags} when is_list(tags) ->
+ mime_type == "application/x-mpegURL"
- is_map(x) and String.starts_with?(mime_type, ["video/", "audio/"])
+ _ ->
+ false
end)
- link_element =
- Enum.find(url, fn x ->
- mime_type = x["mimeType"] || x["mediaType"] || ""
+ url
+ |> Enum.concat(mpeg_url["tag"] || [])
+ |> Enum.find(fn
+ %{"mediaType" => mime_type} -> String.starts_with?(mime_type, ["video/", "audio/"])
+ %{"mimeType" => mime_type} -> String.starts_with?(mime_type, ["video/", "audio/"])
+ _ -> false
+ end)
+ end
- is_map(x) and mime_type == "text/html"
+ defp fix_url(%{"url" => url} = data) when is_list(url) do
+ attachment = find_attachment(url)
+
+ link_element =
+ Enum.find(url, fn
+ %{"mediaType" => "text/html"} -> true
+ %{"mimeType" => "text/html"} -> true
+ _ -> false
end)
data
diff --git a/lib/pleroma/web/activity_pub/object_validators/block_validator.ex b/lib/pleroma/web/activity_pub/object_validators/block_validator.ex
index 1dde77198..c5f77bb76 100644
--- a/lib/pleroma/web/activity_pub/object_validators/block_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/block_validator.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator do
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 6acd4a771..1189778f2 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
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator do
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 b3638cfc7..5f2c633bc 100644
--- a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
diff --git a/lib/pleroma/web/activity_pub/object_validators/common_validations.ex b/lib/pleroma/web/activity_pub/object_validators/common_validations.ex
index 603d87b8e..093549a45 100644
--- a/lib/pleroma/web/activity_pub/object_validators/common_validations.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/common_validations.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
@@ -35,7 +35,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
cng
|> validate_change(field_name, fn field_name, actor ->
case User.get_cached_by_ap_id(actor) do
- %User{deactivated: true} ->
+ %User{is_active: false} ->
[{field_name, "user is deactivated"}]
%User{} ->
diff --git a/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex
index 7269f9ff0..8384c16a7 100644
--- a/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
# NOTES
diff --git a/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex
index 422ee07be..bf56a918c 100644
--- a/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
# Code based on CreateChatMessageValidator
diff --git a/lib/pleroma/web/activity_pub/object_validators/create_note_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_note_validator.ex
index 9b9743c4a..a85a0298c 100644
--- a/lib/pleroma/web/activity_pub/object_validators/create_note_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/create_note_validator.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateNoteValidator do
diff --git a/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex b/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex
index 2634e8d4d..fc1a79a72 100644
--- a/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator do
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 336c92d35..1906e597e 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
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
diff --git a/lib/pleroma/web/activity_pub/object_validators/event_validator.ex b/lib/pleroma/web/activity_pub/object_validators/event_validator.ex
index 0b4c99dc0..2e26726f8 100644
--- a/lib/pleroma/web/activity_pub/object_validators/event_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/event_validator.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do
diff --git a/lib/pleroma/web/activity_pub/object_validators/follow_validator.ex b/lib/pleroma/web/activity_pub/object_validators/follow_validator.ex
index ca2724616..6e428bacc 100644
--- a/lib/pleroma/web/activity_pub/object_validators/follow_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/follow_validator.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.FollowValidator do
diff --git a/lib/pleroma/web/activity_pub/object_validators/like_validator.ex b/lib/pleroma/web/activity_pub/object_validators/like_validator.ex
index 493e4c247..30c40b238 100644
--- a/lib/pleroma/web/activity_pub/object_validators/like_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/like_validator.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator do
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 478b3b5cf..ddcd1be7c 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
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionOptionsValidator 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 9310485dc..6b746c997 100644
--- a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do
diff --git a/lib/pleroma/web/activity_pub/object_validators/undo_validator.ex b/lib/pleroma/web/activity_pub/object_validators/undo_validator.ex
index 8cae94467..783a79ddb 100644
--- a/lib/pleroma/web/activity_pub/object_validators/undo_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/undo_validator.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator do
diff --git a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex
index b4ba5ede0..a66d41400 100644
--- a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator do
diff --git a/lib/pleroma/web/activity_pub/pipeline.ex b/lib/pleroma/web/activity_pub/pipeline.ex
index 2db86f116..195596f94 100644
--- a/lib/pleroma/web/activity_pub/pipeline.ex
+++ b/lib/pleroma/web/activity_pub/pipeline.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Pipeline do
@@ -11,14 +11,22 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do
alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.ActivityPub.ObjectValidator
alias Pleroma.Web.ActivityPub.SideEffects
+ alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Federator
+ @side_effects Config.get([:pipeline, :side_effects], SideEffects)
+ @federator Config.get([:pipeline, :federator], Federator)
+ @object_validator Config.get([:pipeline, :object_validator], ObjectValidator)
+ @mrf Config.get([:pipeline, :mrf], MRF)
+ @activity_pub Config.get([:pipeline, :activity_pub], ActivityPub)
+ @config Config.get([:pipeline, :config], Config)
+
@spec common_pipeline(map(), keyword()) ::
{:ok, Activity.t() | Object.t(), keyword()} | {:error, any()}
def common_pipeline(object, meta) do
case Repo.transaction(fn -> do_common_pipeline(object, meta) end) do
{:ok, {:ok, activity, meta}} ->
- SideEffects.handle_after_transaction(meta)
+ @side_effects.handle_after_transaction(meta)
{:ok, activity, meta}
{:ok, value} ->
@@ -34,13 +42,13 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do
def do_common_pipeline(object, meta) do
with {_, {:ok, validated_object, meta}} <-
- {:validate_object, ObjectValidator.validate(object, meta)},
+ {:validate_object, @object_validator.validate(object, meta)},
{_, {:ok, mrfd_object, meta}} <-
- {:mrf_object, MRF.pipeline_filter(validated_object, meta)},
+ {:mrf_object, @mrf.pipeline_filter(validated_object, meta)},
{_, {:ok, activity, meta}} <-
- {:persist_object, ActivityPub.persist(mrfd_object, meta)},
+ {:persist_object, @activity_pub.persist(mrfd_object, meta)},
{_, {:ok, activity, meta}} <-
- {:execute_side_effects, SideEffects.handle(activity, meta)},
+ {:execute_side_effects, @side_effects.handle(activity, meta)},
{_, {:ok, _}} <- {:federation, maybe_federate(activity, meta)} do
{:ok, activity, meta}
else
@@ -53,9 +61,9 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do
defp maybe_federate(%Activity{} = activity, meta) do
with {:ok, local} <- Keyword.fetch(meta, :local) do
- do_not_federate = meta[:do_not_federate] || !Config.get([:instance, :federating])
+ do_not_federate = meta[:do_not_federate] || !@config.get([:instance, :federating])
- if !do_not_federate && local do
+ if !do_not_federate and local and not Visibility.is_local_public?(activity) do
activity =
if object = Keyword.get(meta, :object_data) do
%{activity | data: Map.put(activity.data, "object", object)}
@@ -63,7 +71,7 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do
activity
end
- Federator.publish(activity)
+ @federator.publish(activity)
{:ok, :federated}
else
{:ok, :not_federated}
diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex
index a2930c1cd..b12b2fc24 100644
--- a/lib/pleroma/web/activity_pub/publisher.ex
+++ b/lib/pleroma/web/activity_pub/publisher.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Publisher do
@@ -13,7 +13,6 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Transmogrifier
- alias Pleroma.Web.FedSockets
require Pleroma.Constants
@@ -50,28 +49,6 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
"""
def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = params) do
Logger.debug("Federating #{id} to #{inbox}")
-
- case FedSockets.get_or_create_fed_socket(inbox) do
- {:ok, fedsocket} ->
- Logger.debug("publishing via fedsockets - #{inspect(inbox)}")
- FedSockets.publish(fedsocket, json)
-
- _ ->
- Logger.debug("publishing via http - #{inspect(inbox)}")
- http_publish(inbox, actor, json, params)
- 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 http_publish(inbox, actor, json, params) do
uri = %{path: path} = URI.parse(inbox)
digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
@@ -110,6 +87,15 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
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
@@ -143,7 +129,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
fetchers =
with %Activity{data: %{"type" => "Delete"}} <- activity,
- %Object{id: object_id} <- Object.normalize(activity),
+ %Object{id: object_id} <- Object.normalize(activity, fetch: false),
fetchers <- User.get_delivered_users_by_object_id(object_id),
_ <- Delivery.delete_all_by_object_id(object_id) do
fetchers
diff --git a/lib/pleroma/web/activity_pub/relay.ex b/lib/pleroma/web/activity_pub/relay.ex
index 6606e1780..6d60a074f 100644
--- a/lib/pleroma/web/activity_pub/relay.ex
+++ b/lib/pleroma/web/activity_pub/relay.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Relay do
diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex
index 0fff5faf2..0b9a9f0c5 100644
--- a/lib/pleroma/web/activity_pub/side_effects.ex
+++ b/lib/pleroma/web/activity_pub/side_effects.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.SideEffects do
@@ -24,15 +24,22 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Push
alias Pleroma.Web.Streamer
- alias Pleroma.Workers.BackgroundWorker
require Logger
+ @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
+ @ap_streamer Pleroma.Config.get([:side_effects, :ap_streamer], ActivityPub)
+ @logger Pleroma.Config.get([:side_effects, :logger], Logger)
+
+ @behaviour Pleroma.Web.ActivityPub.SideEffects.Handling
+
+ @impl true
def handle(object, meta \\ [])
# Task this handles
# - Follows
# - Sends a notification
+ @impl true
def handle(
%{
data: %{
@@ -48,10 +55,9 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
%User{} = followed <- User.get_cached_by_ap_id(actor),
%User{} = follower <- User.get_cached_by_ap_id(follower_id),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
- {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept) do
+ {:ok, _follower, followed} <-
+ FollowingRelationship.update(follower, followed, :follow_accept) do
Notification.update_notification_type(followed, follow_activity)
- User.update_follower_count(followed)
- User.update_following_count(follower)
end
{:ok, object, meta}
@@ -61,6 +67,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
# - Rejects all existing follow activities for this person
# - Updates the follow state
# - Dismisses notification
+ @impl true
def handle(
%{
data: %{
@@ -87,6 +94,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
# - Follows if possible
# - Sends a notification
# - Generates accept or reject if appropriate
+ @impl true
def handle(
%{
data: %{
@@ -100,7 +108,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
) do
with %User{} = follower <- User.get_cached_by_ap_id(following_user),
%User{} = followed <- User.get_cached_by_ap_id(followed_user),
- {_, {:ok, _}, _, _} <-
+ {_, {:ok, _, _}, _, _} <-
{:following, User.follow(follower, followed, :follow_pending), follower, followed} do
if followed.local && !followed.is_locked do
{:ok, accept_data, _} = Builder.accept(followed, object)
@@ -128,6 +136,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
# Tasks this handles:
# - Unfollow and block
+ @impl true
def handle(
%{data: %{"type" => "Block", "object" => blocked_user, "actor" => blocking_user}} =
object,
@@ -146,6 +155,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
#
# For a local user, we also get a changeset with the full information, so we
# can update non-federating, non-activitypub settings as well.
+ @impl true
def handle(%{data: %{"type" => "Update", "object" => updated_object}} = object, meta) do
if changeset = Keyword.get(meta, :user_update_changeset) do
changeset
@@ -164,6 +174,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
# Tasks this handles:
# - Add like to object
# - Set up notification
+ @impl true
def handle(%{data: %{"type" => "Like"}} = object, meta) do
liked_object = Object.get_by_ap_id(object.data["object"])
Utils.add_like_to_object(object, liked_object)
@@ -181,17 +192,20 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
# - Increase replies count
# - Set up ActivityExpiration
# - Set up notifications
+ @impl true
def handle(%{data: %{"type" => "Create"}} = activity, meta) do
with {:ok, object, meta} <- handle_object_creation(meta[:object_data], meta),
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
{:ok, notifications} = Notification.create_notifications(activity, do_send: false)
{:ok, _user} = ActivityPub.increase_note_count_if_public(user, object)
- if in_reply_to = object.data["inReplyTo"] do
+ if in_reply_to = object.data["inReplyTo"] && object.data["type"] != "Answer" do
Object.increase_replies_count(in_reply_to)
end
- BackgroundWorker.enqueue("fetch_data_for_activity", %{"activity_id" => activity.id})
+ ConcurrentLimiter.limit(Pleroma.Web.RichMedia.Helpers, fn ->
+ Task.start(fn -> Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) end)
+ end)
meta =
meta
@@ -207,6 +221,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
# - Add announce to object
# - Set up notification
# - Stream out the announce
+ @impl true
def handle(%{data: %{"type" => "Announce"}} = object, meta) do
announced_object = Object.get_by_ap_id(object.data["object"])
user = User.get_cached_by_ap_id(object.data["actor"])
@@ -224,6 +239,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
{:ok, object, meta}
end
+ @impl true
def handle(%{data: %{"type" => "Undo", "object" => undone_object}} = object, meta) do
with undone_object <- Activity.get_by_ap_id(undone_object),
:ok <- handle_undoing(undone_object) do
@@ -234,6 +250,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
# Tasks this handles:
# - Add reaction to object
# - Set up notification
+ @impl true
def handle(%{data: %{"type" => "EmojiReact"}} = object, meta) do
reacted_object = Object.get_by_ap_id(object.data["object"])
Utils.add_emoji_reaction_to_object(object, reacted_object)
@@ -250,9 +267,10 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
# - Reduce the user note count
# - Reduce the reply count
# - Stream out the activity
+ @impl true
def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object, meta) do
deleted_object =
- Object.normalize(deleted_object, false) ||
+ Object.normalize(deleted_object, fetch: false) ||
User.get_cached_by_ap_id(deleted_object)
result =
@@ -271,12 +289,12 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
MessageReference.delete_for_object(deleted_object)
- ActivityPub.stream_out(object)
- ActivityPub.stream_out_participations(deleted_object, user)
+ @ap_streamer.stream_out(object)
+ @ap_streamer.stream_out_participations(deleted_object, user)
:ok
else
{:actor, _} ->
- Logger.error("The object doesn't have an actor: #{inspect(deleted_object)}")
+ @logger.error("The object doesn't have an actor: #{inspect(deleted_object)}")
:no_object_actor
end
@@ -295,6 +313,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
end
# Nothing to do
+ @impl true
def handle(object, meta) do
{:ok, object, meta}
end
@@ -312,6 +331,12 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
{:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id)
{:ok, cm_ref} = MessageReference.create(chat, object, user.ap_id != actor.ap_id)
+ @cachex.put(
+ :chat_message_id_idempotency_key_cache,
+ cm_ref.id,
+ meta[:idempotency_key]
+ )
+
{
["user", "user:pleroma_chat"],
{user, %{cm_ref | chat: chat, object: object}}
@@ -433,6 +458,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|> Keyword.put(:notifications, notifications ++ existing)
end
+ @impl true
def handle_after_transaction(meta) do
meta
|> send_notifications()
diff --git a/lib/pleroma/web/activity_pub/side_effects/handling.ex b/lib/pleroma/web/activity_pub/side_effects/handling.ex
new file mode 100644
index 000000000..a82305155
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/side_effects/handling.ex
@@ -0,0 +1,8 @@
+# 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.SideEffects.Handling do
+ @callback handle(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
+ @callback handle_after_transaction(map()) :: map()
+end
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 39c8f7e39..4d9a5617e 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Transmogrifier do
@@ -252,6 +252,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
}
|> Maps.put_if_present("mediaType", media_type)
|> Maps.put_if_present("name", data["name"])
+ |> Maps.put_if_present("blurhash", data["blurhash"])
else
nil
end
@@ -652,7 +653,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
@spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
def get_obj_helper(id, options \\ []) do
- case Object.normalize(id, true, options) do
+ options = Keyword.put(options, :fetch, true)
+
+ case Object.normalize(id, options) do
%Object{} = object -> {:ok, object}
_ -> nil
end
@@ -671,7 +674,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
"actor" => attributed_to,
"object" => data
}) do
- {:ok, Object.normalize(activity)}
+ {:ok, Object.normalize(activity, fetch: false)}
else
_ -> get_obj_helper(object_id)
end
@@ -762,7 +765,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
when activity_type in ["Create", "Listen"] do
object =
object_id
- |> Object.normalize()
+ |> Object.normalize(fetch: false)
|> Map.get(:data)
|> prepare_object
@@ -778,7 +781,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do
object =
object_id
- |> Object.normalize()
+ |> Object.normalize(fetch: false)
data =
if Visibility.is_private?(object) && object.data["actor"] == ap_id do
@@ -918,7 +921,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
defp build_emoji_tag({name, url}) do
%{
- "icon" => %{"url" => url, "type" => "Image"},
+ "icon" => %{"url" => "#{URI.encode(url)}", "type" => "Image"},
"name" => ":" <> name <> ":",
"type" => "Emoji",
"updated" => "1970-01-01T00:00:00Z",
@@ -1007,7 +1010,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def upgrade_user_from_ap_id(ap_id) do
with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
- {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id, force_http: true),
+ {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
{:ok, user} <- update_user(user, data) do
TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
{:ok, user}
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index d580c02a9..a4dc469dc 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Utils do
@@ -175,7 +175,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do
outgoing_blocks = Config.get([:activitypub, :outgoing_blocks])
with true <- Config.get!([:instance, :federating]),
- true <- type != "Block" || outgoing_blocks do
+ true <- type != "Block" || outgoing_blocks,
+ false <- Visibility.is_local_public?(activity) do
Pleroma.Web.Federator.publish(activity)
end
diff --git a/lib/pleroma/web/activity_pub/views/object_view.ex b/lib/pleroma/web/activity_pub/views/object_view.ex
index e555e9999..8a3e4d77b 100644
--- a/lib/pleroma/web/activity_pub/views/object_view.ex
+++ b/lib/pleroma/web/activity_pub/views/object_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectView do
@@ -18,7 +18,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do
def render("object.json", %{object: %Activity{data: %{"type" => activity_type}} = activity})
when activity_type in ["Create", "Listen"] do
base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header()
- object = Object.normalize(activity)
+ object = Object.normalize(activity, fetch: false)
additional =
Transmogrifier.prepare_object(activity.data)
@@ -29,7 +29,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do
def render("object.json", %{object: %Activity{} = activity}) do
base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header()
- object = Object.normalize(activity)
+ object = Object.normalize(activity, fetch: false)
additional =
Transmogrifier.prepare_object(activity.data)
diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex
index c6dee61db..8adc9878a 100644
--- a/lib/pleroma/web/activity_pub/views/user_view.ex
+++ b/lib/pleroma/web/activity_pub/views/user_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.UserView do
@@ -110,8 +110,10 @@ defmodule Pleroma.Web.ActivityPub.UserView do
"endpoints" => endpoints,
"attachment" => fields,
"tag" => emoji_tags,
- "discoverable" => user.discoverable,
- "capabilities" => capabilities
+ # Note: key name is indeed "discoverable" (not an error)
+ "discoverable" => user.is_discoverable,
+ "capabilities" => capabilities,
+ "alsoKnownAs" => user.also_known_as
}
|> 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 76bd54a42..00234c0b0 100644
--- a/lib/pleroma/web/activity_pub/visibility.ex
+++ b/lib/pleroma/web/activity_pub/visibility.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Visibility do
@@ -17,7 +17,19 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
def is_public?(%Activity{data: %{"type" => "Move"}}), do: true
def is_public?(%Activity{data: data}), do: is_public?(data)
def is_public?(%{"directMessage" => true}), do: false
- def is_public?(data), do: Utils.label_in_message?(Pleroma.Constants.as_public(), data)
+
+ def is_public?(data) do
+ Utils.label_in_message?(Pleroma.Constants.as_public(), data) or
+ Utils.label_in_message?(Pleroma.Constants.as_local_public(), data)
+ end
+
+ def is_local_public?(%Object{data: data}), do: is_local_public?(data)
+ def is_local_public?(%Activity{data: data}), do: is_local_public?(data)
+
+ def is_local_public?(data) do
+ Utils.label_in_message?(Pleroma.Constants.as_local_public(), data) and
+ not Utils.label_in_message?(Pleroma.Constants.as_public(), data)
+ end
def is_private?(activity) do
with false <- is_public?(activity),
@@ -44,11 +56,10 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
def is_list?(%{data: %{"listMessage" => _}}), do: true
def is_list?(_), do: false
- @spec visible_for_user?(Activity.t() | nil, User.t() | nil) :: boolean()
+ @spec visible_for_user?(Object.t() | Activity.t() | nil, User.t() | nil) :: boolean()
def visible_for_user?(%Activity{actor: ap_id}, %User{ap_id: ap_id}), do: true
-
+ def visible_for_user?(%Object{data: %{"actor" => ap_id}}, %User{ap_id: ap_id}), do: true
def visible_for_user?(nil, _), do: false
-
def visible_for_user?(%Activity{data: %{"listMessage" => _}}, nil), do: false
def visible_for_user?(
@@ -61,16 +72,18 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
|> Pleroma.List.member?(user)
end
- def visible_for_user?(%Activity{} = activity, nil) do
- if restrict_unauthenticated_access?(activity),
+ def visible_for_user?(%{__struct__: module} = message, nil)
+ when module in [Activity, Object] do
+ if restrict_unauthenticated_access?(message),
do: false,
- else: is_public?(activity)
+ else: is_public?(message) and not is_local_public?(message)
end
- def visible_for_user?(%Activity{} = activity, user) do
+ def visible_for_user?(%{__struct__: module} = message, user)
+ when module in [Activity, Object] do
x = [user.ap_id | User.following(user)]
- y = [activity.actor] ++ activity.data["to"] ++ (activity.data["cc"] || [])
- is_public?(activity) || Enum.any?(x, &(&1 in y))
+ y = [message.data["actor"]] ++ message.data["to"] ++ (message.data["cc"] || [])
+ is_public?(message) || Enum.any?(x, &(&1 in y))
end
def entire_thread_visible_for_user?(%Activity{} = activity, %User{} = user) do
@@ -114,6 +127,9 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
Pleroma.Constants.as_public() in cc ->
"unlisted"
+ Pleroma.Constants.as_local_public() in to ->
+ "local"
+
# this should use the sql for the object's activity
Enum.any?(to, &String.contains?(&1, "/followers")) ->
"private"
diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
index bdd3e195d..839ac1a8d 100644
--- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
@@ -1,11 +1,12 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.AdminAPIController do
use Pleroma.Web, :controller
- import Pleroma.Web.ControllerHelper, only: [json_response: 3]
+ import Pleroma.Web.ControllerHelper,
+ only: [json_response: 3, fetch_integer_param: 3]
alias Pleroma.Config
alias Pleroma.MFA
@@ -13,12 +14,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
alias Pleroma.Stats
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
- alias Pleroma.Web.ActivityPub.Builder
- alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.AdminAPI
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.AdminAPI.ModerationLogView
- alias Pleroma.Web.AdminAPI.Search
alias Pleroma.Web.Endpoint
alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.Router
@@ -27,22 +25,16 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
plug(
OAuthScopesPlug,
- %{scopes: ["read:accounts"], admin: true}
- when action in [:list_users, :user_show, :right_get, :show_user_credentials]
+ %{scopes: ["admin:read:accounts"]}
+ when action in [:right_get, :show_user_credentials, :create_backup]
)
plug(
OAuthScopesPlug,
- %{scopes: ["write:accounts"], admin: true}
+ %{scopes: ["admin:write:accounts"]}
when action in [
:get_password_reset,
:force_password_reset,
- :user_delete,
- :users_create,
- :user_toggle_activation,
- :user_activate,
- :user_deactivate,
- :user_approve,
:tag_users,
:untag_users,
:right_add,
@@ -56,25 +48,19 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
plug(
OAuthScopesPlug,
- %{scopes: ["write:follows"], admin: true}
- when action in [:user_follow, :user_unfollow]
- )
-
- plug(
- OAuthScopesPlug,
- %{scopes: ["read:statuses"], admin: true}
+ %{scopes: ["admin:read:statuses"]}
when action in [:list_user_statuses, :list_instance_statuses]
)
plug(
OAuthScopesPlug,
- %{scopes: ["read:chats"], admin: true}
+ %{scopes: ["admin:read:chats"]}
when action in [:list_user_chats]
)
plug(
OAuthScopesPlug,
- %{scopes: ["read"], admin: true}
+ %{scopes: ["admin:read"]}
when action in [
:list_log,
:stats,
@@ -84,7 +70,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
plug(
OAuthScopesPlug,
- %{scopes: ["write"], admin: true}
+ %{scopes: ["admin:write"]}
when action in [
:restart,
:resend_confirmation_email,
@@ -95,147 +81,22 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
action_fallback(AdminAPI.FallbackController)
- def user_delete(conn, %{"nickname" => nickname}) do
- user_delete(conn, %{"nicknames" => [nickname]})
- end
-
- def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
- users =
- nicknames
- |> Enum.map(&User.get_cached_by_nickname/1)
-
- users
- |> Enum.each(fn user ->
- {:ok, delete_data, _} = Builder.delete(admin, user.ap_id)
- Pipeline.common_pipeline(delete_data, local: true)
- end)
-
- ModerationLog.insert_log(%{
- actor: admin,
- subject: users,
- action: "delete"
- })
-
- json(conn, nicknames)
- end
-
- def user_follow(%{assigns: %{user: admin}} = conn, %{
- "follower" => follower_nick,
- "followed" => followed_nick
- }) do
- with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
- %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
- User.follow(follower, followed)
-
- ModerationLog.insert_log(%{
- actor: admin,
- followed: followed,
- follower: follower,
- action: "follow"
- })
- end
-
- json(conn, "ok")
- end
-
- def user_unfollow(%{assigns: %{user: admin}} = conn, %{
- "follower" => follower_nick,
- "followed" => followed_nick
- }) do
- with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
- %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
- User.unfollow(follower, followed)
-
- ModerationLog.insert_log(%{
- actor: admin,
- followed: followed,
- follower: follower,
- action: "unfollow"
- })
- end
-
- json(conn, "ok")
- end
-
- def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
- changesets =
- Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} ->
- user_data = %{
- nickname: nickname,
- name: nickname,
- email: email,
- password: password,
- password_confirmation: password,
- bio: "."
- }
-
- User.register_changeset(%User{}, user_data, need_confirmation: false)
- end)
- |> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
- Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
- end)
-
- case Pleroma.Repo.transaction(changesets) do
- {:ok, users} ->
- res =
- users
- |> Map.values()
- |> Enum.map(fn user ->
- {:ok, user} = User.post_register_action(user)
-
- user
- end)
- |> Enum.map(&AccountView.render("created.json", %{user: &1}))
-
- ModerationLog.insert_log(%{
- actor: admin,
- subjects: Map.values(users),
- action: "create"
- })
-
- json(conn, res)
-
- {:error, id, changeset, _} ->
- res =
- Enum.map(changesets.operations, fn
- {current_id, {:changeset, _current_changeset, _}} when current_id == id ->
- AccountView.render("create-error.json", %{changeset: changeset})
-
- {_, {:changeset, current_changeset, _}} ->
- AccountView.render("create-error.json", %{changeset: current_changeset})
- end)
-
- conn
- |> put_status(:conflict)
- |> json(res)
- end
- end
-
- def user_show(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
- with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do
- conn
- |> put_view(AccountView)
- |> render("show.json", %{user: user})
- else
- _ -> {:error, :not_found}
- end
- end
-
def list_instance_statuses(conn, %{"instance" => instance} = params) do
with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
{page, page_size} = page_params(params)
- activities =
+ result =
ActivityPub.fetch_statuses(nil, %{
instance: instance,
limit: page_size,
offset: (page - 1) * page_size,
- exclude_reblogs: not with_reblogs
+ exclude_reblogs: not with_reblogs,
+ total: true
})
conn
|> put_view(AdminAPI.StatusView)
- |> render("index.json", %{activities: activities, as: :activity})
+ |> render("index.json", %{total: result[:total], activities: result[:items], as: :activity})
end
def list_user_statuses(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname} = params) do
@@ -243,18 +104,21 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
godmode = params["godmode"] == "true" || params["godmode"] == true
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do
- {_, page_size} = page_params(params)
+ {page, page_size} = page_params(params)
- activities =
+ result =
ActivityPub.fetch_user_activities(user, nil, %{
limit: page_size,
+ offset: (page - 1) * page_size,
godmode: godmode,
- exclude_reblogs: not with_reblogs
+ exclude_reblogs: not with_reblogs,
+ pagination_type: :offset,
+ total: true
})
conn
|> put_view(AdminAPI.StatusView)
- |> render("index.json", %{activities: activities, as: :activity})
+ |> render("index.json", %{total: result[:total], activities: result[:items], as: :activity})
else
_ -> {:error, :not_found}
end
@@ -274,69 +138,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
end
end
- def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
- user = User.get_cached_by_nickname(nickname)
-
- {:ok, updated_user} = User.deactivate(user, !user.deactivated)
-
- action = if user.deactivated, do: "activate", else: "deactivate"
-
- ModerationLog.insert_log(%{
- actor: admin,
- subject: [user],
- action: action
- })
-
- conn
- |> put_view(AccountView)
- |> render("show.json", %{user: updated_user})
- end
-
- def user_activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
- users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
- {:ok, updated_users} = User.deactivate(users, false)
-
- ModerationLog.insert_log(%{
- actor: admin,
- subject: users,
- action: "activate"
- })
-
- conn
- |> put_view(AccountView)
- |> render("index.json", %{users: Keyword.values(updated_users)})
- end
-
- def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
- users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
- {:ok, updated_users} = User.deactivate(users, true)
-
- ModerationLog.insert_log(%{
- actor: admin,
- subject: users,
- action: "deactivate"
- })
-
- conn
- |> put_view(AccountView)
- |> render("index.json", %{users: Keyword.values(updated_users)})
- end
-
- def user_approve(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
- users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
- {:ok, updated_users} = User.approve(users)
-
- ModerationLog.insert_log(%{
- actor: admin,
- subject: users,
- action: "approve"
- })
-
- conn
- |> put_view(AccountView)
- |> render("index.json", %{users: updated_users})
- end
-
def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
with {:ok, _} <- User.tag(nicknames, tags) do
ModerationLog.insert_log(%{
@@ -363,43 +164,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
end
end
- def list_users(conn, params) do
- {page, page_size} = page_params(params)
- filters = maybe_parse_filters(params["filters"])
-
- search_params = %{
- query: params["query"],
- page: page,
- page_size: page_size,
- tags: params["tags"],
- name: params["name"],
- email: params["email"]
- }
-
- with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)) do
- json(
- conn,
- AccountView.render("index.json",
- users: users,
- count: count,
- page_size: page_size
- )
- )
- end
- end
-
- @filters ~w(local external active deactivated need_approval is_admin is_moderator)
-
- @spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
- defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
-
- defp maybe_parse_filters(filters) do
- filters
- |> String.split(",")
- |> Enum.filter(&Enum.member?(@filters, &1))
- |> Map.new(&{String.to_existing_atom(&1), true})
- end
-
def right_add_multiple(%{assigns: %{user: admin}} = conn, %{
"permission_group" => permission_group,
"nicknames" => nicknames
@@ -642,7 +406,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
if Config.get(:configurable_from_database) do
:ok
else
- {:error, "To use this endpoint you need to enable configuration from database."}
+ {:error, "You must enable configurable_from_database in your config file."}
end
end
@@ -655,7 +419,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
- User.toggle_confirmation(users)
+ User.confirm(users)
ModerationLog.insert_log(%{actor: admin, subject: users, action: "confirm_email"})
@@ -681,25 +445,19 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
json(conn, %{"status_visibility" => counters})
end
- defp page_params(params) do
- {get_page(params["page"]), get_page_size(params["page_size"])}
- end
+ def create_backup(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
+ with %User{} = user <- User.get_by_nickname(nickname),
+ {:ok, _} <- Pleroma.User.Backup.create(user, admin.id) do
+ ModerationLog.insert_log(%{actor: admin, subject: user, action: "create_backup"})
- defp get_page(page_string) when is_nil(page_string), do: 1
-
- defp get_page(page_string) do
- case Integer.parse(page_string) do
- {page, _} -> page
- :error -> 1
+ json(conn, "")
end
end
- defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
-
- defp get_page_size(page_size_string) do
- case Integer.parse(page_size_string) do
- {page_size, _} -> page_size
- :error -> @users_page_size
- end
+ defp page_params(params) do
+ {
+ fetch_integer_param(params, "page", 1),
+ fetch_integer_param(params, "page_size", @users_page_size)
+ }
end
end
diff --git a/lib/pleroma/web/admin_api/controllers/chat_controller.ex b/lib/pleroma/web/admin_api/controllers/chat_controller.ex
index af8ff8292..ff20c8604 100644
--- a/lib/pleroma/web/admin_api/controllers/chat_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/chat_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.ChatController do
@@ -21,12 +21,12 @@ defmodule Pleroma.Web.AdminAPI.ChatController do
plug(
OAuthScopesPlug,
- %{scopes: ["read:chats"], admin: true} when action in [:show, :messages]
+ %{scopes: ["admin:read:chats"]} when action in [:show, :messages]
)
plug(
OAuthScopesPlug,
- %{scopes: ["write:chats"], admin: true} when action in [:delete_message]
+ %{scopes: ["admin:write:chats"]} when action in [:delete_message]
)
action_fallback(Pleroma.Web.AdminAPI.FallbackController)
diff --git a/lib/pleroma/web/admin_api/controllers/config_controller.ex b/lib/pleroma/web/admin_api/controllers/config_controller.ex
index 5d155af3d..a718d7b8d 100644
--- a/lib/pleroma/web/admin_api/controllers/config_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/config_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.ConfigController do
@@ -10,11 +10,11 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
alias Pleroma.Web.Plugs.OAuthScopesPlug
plug(Pleroma.Web.ApiSpec.CastAndValidate)
- plug(OAuthScopesPlug, %{scopes: ["write"], admin: true} when action == :update)
+ plug(OAuthScopesPlug, %{scopes: ["admin:write"]} when action == :update)
plug(
OAuthScopesPlug,
- %{scopes: ["read"], admin: true}
+ %{scopes: ["admin:read"]}
when action in [:show, :descriptions]
)
@@ -122,7 +122,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
if Config.get(:configurable_from_database) do
:ok
else
- {:error, "To use this endpoint you need to enable configuration from database."}
+ {:error, "You must enable configurable_from_database in your config file."}
end
end
diff --git a/lib/pleroma/web/admin_api/controllers/fallback_controller.ex b/lib/pleroma/web/admin_api/controllers/fallback_controller.ex
index 34d90db07..45d8815b5 100644
--- a/lib/pleroma/web/admin_api/controllers/fallback_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/fallback_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.FallbackController do
diff --git a/lib/pleroma/web/admin_api/controllers/frontend_controller.ex b/lib/pleroma/web/admin_api/controllers/frontend_controller.ex
new file mode 100644
index 000000000..722f51bd2
--- /dev/null
+++ b/lib/pleroma/web/admin_api/controllers/frontend_controller.ex
@@ -0,0 +1,40 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.AdminAPI.FrontendController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Config
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(OAuthScopesPlug, %{scopes: ["admin:write"]} when action == :install)
+ plug(OAuthScopesPlug, %{scopes: ["admin:read"]} when action == :index)
+ action_fallback(Pleroma.Web.AdminAPI.FallbackController)
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.FrontendOperation
+
+ def index(conn, _params) do
+ installed = installed()
+
+ frontends =
+ [:frontends, :available]
+ |> Config.get([])
+ |> Enum.map(fn {name, desc} ->
+ Map.put(desc, "installed", name in installed)
+ end)
+
+ render(conn, "index.json", frontends: frontends)
+ end
+
+ def install(%{body_params: params} = conn, _params) do
+ with :ok <- Pleroma.Frontend.install(params.name, Map.delete(params, :name)) do
+ index(conn, %{})
+ end
+ end
+
+ defp installed do
+ File.ls!(Pleroma.Frontend.dir())
+ end
+end
diff --git a/lib/pleroma/web/admin_api/controllers/instance_document_controller.ex b/lib/pleroma/web/admin_api/controllers/instance_document_controller.ex
index 37dbfeb72..a55857a0e 100644
--- a/lib/pleroma/web/admin_api/controllers/instance_document_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/instance_document_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.InstanceDocumentController do
@@ -15,8 +15,8 @@ defmodule Pleroma.Web.AdminAPI.InstanceDocumentController do
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.InstanceDocumentOperation
- plug(OAuthScopesPlug, %{scopes: ["read"], admin: true} when action == :show)
- plug(OAuthScopesPlug, %{scopes: ["write"], admin: true} when action in [:update, :delete])
+ 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
with {:ok, url} <- InstanceDocument.get(document_name),
diff --git a/lib/pleroma/web/admin_api/controllers/invite_controller.ex b/lib/pleroma/web/admin_api/controllers/invite_controller.ex
index 6a9b4038a..727ebd846 100644
--- a/lib/pleroma/web/admin_api/controllers/invite_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/invite_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.InviteController do
@@ -14,11 +14,11 @@ defmodule Pleroma.Web.AdminAPI.InviteController do
require Logger
plug(Pleroma.Web.ApiSpec.CastAndValidate)
- plug(OAuthScopesPlug, %{scopes: ["read:invites"], admin: true} when action == :index)
+ plug(OAuthScopesPlug, %{scopes: ["admin:read:invites"]} when action == :index)
plug(
OAuthScopesPlug,
- %{scopes: ["write:invites"], admin: true} when action in [:create, :revoke, :email]
+ %{scopes: ["admin:write:invites"]} when action in [:create, :revoke, :email]
)
action_fallback(Pleroma.Web.AdminAPI.FallbackController)
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 6d92e9f7f..a6d7aaf54 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
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.MediaProxyCacheController do
@@ -9,16 +9,18 @@ defmodule Pleroma.Web.AdminAPI.MediaProxyCacheController do
alias Pleroma.Web.MediaProxy
alias Pleroma.Web.Plugs.OAuthScopesPlug
+ @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
+
plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(
OAuthScopesPlug,
- %{scopes: ["read:media_proxy_caches"], admin: true} when action in [:index]
+ %{scopes: ["admin:read:media_proxy_caches"]} when action in [:index]
)
plug(
OAuthScopesPlug,
- %{scopes: ["write:media_proxy_caches"], admin: true} when action in [:purge, :delete]
+ %{scopes: ["admin:write:media_proxy_caches"]} when action in [:purge, :delete]
)
action_fallback(Pleroma.Web.AdminAPI.FallbackController)
@@ -38,7 +40,7 @@ defmodule Pleroma.Web.AdminAPI.MediaProxyCacheController do
defp fetch_entries(params) do
MediaProxy.cache_table()
- |> Cachex.stream!(Cachex.Query.create(true, :key))
+ |> @cachex.stream!(Cachex.Query.create(true, :key))
|> filter_entries(params[:query])
end
diff --git a/lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex b/lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex
index 116a05a4d..005fe67e2 100644
--- a/lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.OAuthAppController do
@@ -17,7 +17,7 @@ defmodule Pleroma.Web.AdminAPI.OAuthAppController do
plug(
OAuthScopesPlug,
- %{scopes: ["write"], admin: true}
+ %{scopes: ["admin:write"]}
when action in [:create, :index, :update, :delete]
)
diff --git a/lib/pleroma/web/admin_api/controllers/relay_controller.ex b/lib/pleroma/web/admin_api/controllers/relay_controller.ex
index 611388447..c6bd43fea 100644
--- a/lib/pleroma/web/admin_api/controllers/relay_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/relay_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.RelayController do
@@ -15,11 +15,11 @@ defmodule Pleroma.Web.AdminAPI.RelayController do
plug(
OAuthScopesPlug,
- %{scopes: ["write:follows"], admin: true}
+ %{scopes: ["admin:write:follows"]}
when action in [:follow, :unfollow]
)
- plug(OAuthScopesPlug, %{scopes: ["read"], admin: true} when action == :index)
+ plug(OAuthScopesPlug, %{scopes: ["admin:read"]} when action == :index)
action_fallback(Pleroma.Web.AdminAPI.FallbackController)
diff --git a/lib/pleroma/web/admin_api/controllers/report_controller.ex b/lib/pleroma/web/admin_api/controllers/report_controller.ex
index 86da93893..d4a4935ee 100644
--- a/lib/pleroma/web/admin_api/controllers/report_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/report_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.ReportController do
@@ -19,11 +19,11 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
require Logger
plug(Pleroma.Web.ApiSpec.CastAndValidate)
- plug(OAuthScopesPlug, %{scopes: ["read:reports"], admin: true} when action in [:index, :show])
+ plug(OAuthScopesPlug, %{scopes: ["admin:read:reports"]} when action in [:index, :show])
plug(
OAuthScopesPlug,
- %{scopes: ["write:reports"], admin: true}
+ %{scopes: ["admin:write:reports"]}
when action in [:update, :notes_create, :notes_delete]
)
@@ -38,7 +38,7 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
end
def show(conn, %{id: id}) do
- with %Activity{} = report <- Activity.get_by_id(id) do
+ with %Activity{} = report <- Activity.get_report(id) do
render(conn, "show.json", Report.extract_report_info(report))
else
_ -> {:error, :not_found}
@@ -50,10 +50,13 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
Enum.map(reports, fn report ->
case CommonAPI.update_report_state(report.id, report.state) do
{:ok, activity} ->
+ report = Activity.get_by_id_with_user_actor(activity.id)
+
ModerationLog.insert_log(%{
action: "report_update",
actor: admin,
- subject: activity
+ subject: activity,
+ subject_actor: report.user_actor
})
activity
@@ -73,11 +76,13 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
def notes_create(%{assigns: %{user: user}, body_params: %{content: content}} = conn, %{
id: report_id
}) do
- with {:ok, _} <- ReportNote.create(user.id, report_id, content) do
+ with {:ok, _} <- ReportNote.create(user.id, report_id, content),
+ report <- Activity.get_by_id_with_user_actor(report_id) do
ModerationLog.insert_log(%{
action: "report_note",
actor: user,
- subject: Activity.get_by_id(report_id),
+ subject: report,
+ subject_actor: report.user_actor,
text: content
})
@@ -91,11 +96,13 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
id: note_id,
report_id: report_id
}) do
- with {:ok, note} <- ReportNote.destroy(note_id) do
+ with {:ok, note} <- ReportNote.destroy(note_id),
+ report <- Activity.get_by_id_with_user_actor(report_id) do
ModerationLog.insert_log(%{
action: "report_note_delete",
actor: user,
- subject: Activity.get_by_id(report_id),
+ subject: report,
+ subject_actor: report.user_actor,
text: note.content
})
diff --git a/lib/pleroma/web/admin_api/controllers/status_controller.ex b/lib/pleroma/web/admin_api/controllers/status_controller.ex
index 2bb437cfe..7058def82 100644
--- a/lib/pleroma/web/admin_api/controllers/status_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/status_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.StatusController do
@@ -15,11 +15,11 @@ defmodule Pleroma.Web.AdminAPI.StatusController do
require Logger
plug(Pleroma.Web.ApiSpec.CastAndValidate)
- plug(OAuthScopesPlug, %{scopes: ["read:statuses"], admin: true} when action in [:index, :show])
+ plug(OAuthScopesPlug, %{scopes: ["admin:read:statuses"]} when action in [:index, :show])
plug(
OAuthScopesPlug,
- %{scopes: ["write:statuses"], admin: true} when action in [:update, :delete]
+ %{scopes: ["admin:write:statuses"]} when action in [:update, :delete]
)
action_fallback(Pleroma.Web.AdminAPI.FallbackController)
diff --git a/lib/pleroma/web/admin_api/controllers/user_controller.ex b/lib/pleroma/web/admin_api/controllers/user_controller.ex
new file mode 100644
index 000000000..65bc63cb9
--- /dev/null
+++ b/lib/pleroma/web/admin_api/controllers/user_controller.ex
@@ -0,0 +1,281 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.AdminAPI.UserController do
+ use Pleroma.Web, :controller
+
+ import Pleroma.Web.ControllerHelper,
+ only: [fetch_integer_param: 3]
+
+ alias Pleroma.ModerationLog
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.Builder
+ alias Pleroma.Web.ActivityPub.Pipeline
+ alias Pleroma.Web.AdminAPI
+ alias Pleroma.Web.AdminAPI.AccountView
+ alias Pleroma.Web.AdminAPI.Search
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+
+ @users_page_size 50
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["admin:read:accounts"]}
+ when action in [:list, :show]
+ )
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["admin:write:accounts"]}
+ when action in [
+ :delete,
+ :create,
+ :toggle_activation,
+ :activate,
+ :deactivate,
+ :approve
+ ]
+ )
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["admin:write:follows"]}
+ when action in [:follow, :unfollow]
+ )
+
+ action_fallback(AdminAPI.FallbackController)
+
+ def delete(conn, %{"nickname" => nickname}) do
+ delete(conn, %{"nicknames" => [nickname]})
+ end
+
+ def delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
+ users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
+
+ Enum.each(users, fn user ->
+ {:ok, delete_data, _} = Builder.delete(admin, user.ap_id)
+ Pipeline.common_pipeline(delete_data, local: true)
+ end)
+
+ ModerationLog.insert_log(%{
+ actor: admin,
+ subject: users,
+ action: "delete"
+ })
+
+ json(conn, nicknames)
+ end
+
+ def follow(%{assigns: %{user: admin}} = conn, %{
+ "follower" => follower_nick,
+ "followed" => followed_nick
+ }) do
+ with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
+ %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
+ User.follow(follower, followed)
+
+ ModerationLog.insert_log(%{
+ actor: admin,
+ followed: followed,
+ follower: follower,
+ action: "follow"
+ })
+ end
+
+ json(conn, "ok")
+ end
+
+ def unfollow(%{assigns: %{user: admin}} = conn, %{
+ "follower" => follower_nick,
+ "followed" => followed_nick
+ }) do
+ with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
+ %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
+ User.unfollow(follower, followed)
+
+ ModerationLog.insert_log(%{
+ actor: admin,
+ followed: followed,
+ follower: follower,
+ action: "unfollow"
+ })
+ end
+
+ json(conn, "ok")
+ end
+
+ def create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
+ changesets =
+ Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} ->
+ user_data = %{
+ nickname: nickname,
+ name: nickname,
+ email: email,
+ password: password,
+ password_confirmation: password,
+ bio: "."
+ }
+
+ User.register_changeset(%User{}, user_data, need_confirmation: false)
+ end)
+ |> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
+ Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
+ end)
+
+ case Pleroma.Repo.transaction(changesets) do
+ {:ok, users} ->
+ res =
+ users
+ |> Map.values()
+ |> Enum.map(fn user ->
+ {:ok, user} = User.post_register_action(user)
+
+ user
+ end)
+ |> Enum.map(&AccountView.render("created.json", %{user: &1}))
+
+ ModerationLog.insert_log(%{
+ actor: admin,
+ subjects: Map.values(users),
+ action: "create"
+ })
+
+ json(conn, res)
+
+ {:error, id, changeset, _} ->
+ res =
+ Enum.map(changesets.operations, fn
+ {current_id, {:changeset, _current_changeset, _}} when current_id == id ->
+ AccountView.render("create-error.json", %{changeset: changeset})
+
+ {_, {:changeset, current_changeset, _}} ->
+ AccountView.render("create-error.json", %{changeset: current_changeset})
+ end)
+
+ conn
+ |> put_status(:conflict)
+ |> json(res)
+ end
+ end
+
+ def show(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
+ with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do
+ conn
+ |> put_view(AccountView)
+ |> render("show.json", %{user: user})
+ else
+ _ -> {:error, :not_found}
+ end
+ end
+
+ def toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
+ user = User.get_cached_by_nickname(nickname)
+
+ {:ok, updated_user} = User.set_activation(user, !user.is_active)
+
+ action = if !user.is_active, do: "activate", else: "deactivate"
+
+ ModerationLog.insert_log(%{
+ actor: admin,
+ subject: [user],
+ action: action
+ })
+
+ conn
+ |> put_view(AccountView)
+ |> render("show.json", %{user: updated_user})
+ end
+
+ def activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
+ users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
+ {:ok, updated_users} = User.set_activation(users, true)
+
+ ModerationLog.insert_log(%{
+ actor: admin,
+ subject: users,
+ action: "activate"
+ })
+
+ conn
+ |> put_view(AccountView)
+ |> render("index.json", %{users: Keyword.values(updated_users)})
+ end
+
+ def deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
+ users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
+ {:ok, updated_users} = User.set_activation(users, false)
+
+ ModerationLog.insert_log(%{
+ actor: admin,
+ subject: users,
+ action: "deactivate"
+ })
+
+ conn
+ |> put_view(AccountView)
+ |> render("index.json", %{users: Keyword.values(updated_users)})
+ end
+
+ def approve(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
+ users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
+ {:ok, updated_users} = User.approve(users)
+
+ ModerationLog.insert_log(%{
+ actor: admin,
+ subject: users,
+ action: "approve"
+ })
+
+ conn
+ |> put_view(AccountView)
+ |> render("index.json", %{users: updated_users})
+ end
+
+ def list(conn, params) do
+ {page, page_size} = page_params(params)
+ filters = maybe_parse_filters(params["filters"])
+
+ search_params =
+ %{
+ query: params["query"],
+ page: page,
+ page_size: page_size,
+ tags: params["tags"],
+ name: params["name"],
+ email: params["email"],
+ actor_types: params["actor_types"]
+ }
+ |> Map.merge(filters)
+
+ with {:ok, users, count} <- Search.user(search_params) do
+ json(
+ conn,
+ AccountView.render("index.json",
+ users: users,
+ count: count,
+ page_size: page_size
+ )
+ )
+ end
+ end
+
+ @filters ~w(local external active deactivated need_approval unconfirmed is_admin is_moderator)
+
+ @spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
+ defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
+
+ defp maybe_parse_filters(filters) do
+ filters
+ |> String.split(",")
+ |> Enum.filter(&Enum.member?(@filters, &1))
+ |> Map.new(&{String.to_existing_atom(&1), true})
+ end
+
+ defp page_params(params) do
+ {
+ fetch_integer_param(params, "page", 1),
+ fetch_integer_param(params, "page_size", @users_page_size)
+ }
+ end
+end
diff --git a/lib/pleroma/web/admin_api/report.ex b/lib/pleroma/web/admin_api/report.ex
index 8660d6520..259068f04 100644
--- a/lib/pleroma/web/admin_api/report.ex
+++ b/lib/pleroma/web/admin_api/report.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.Report do
diff --git a/lib/pleroma/web/admin_api/search.ex b/lib/pleroma/web/admin_api/search.ex
index 0bfb8f022..eeeebdf4e 100644
--- a/lib/pleroma/web/admin_api/search.ex
+++ b/lib/pleroma/web/admin_api/search.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.Search do
diff --git a/lib/pleroma/web/admin_api/views/account_view.ex b/lib/pleroma/web/admin_api/views/account_view.ex
index bda7ea19c..d7c63d385 100644
--- a/lib/pleroma/web/admin_api/views/account_view.ex
+++ b/lib/pleroma/web/admin_api/views/account_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.AccountView do
@@ -52,7 +52,7 @@ defmodule Pleroma.Web.AdminAPI.AccountView do
:skip_thread_containment,
:pleroma_settings_store,
:raw_fields,
- :discoverable,
+ :is_discoverable,
:actor_type
])
|> Map.merge(%{
@@ -69,15 +69,16 @@ defmodule Pleroma.Web.AdminAPI.AccountView do
%{
"id" => user.id,
+ "email" => user.email,
"avatar" => avatar,
"nickname" => user.nickname,
"display_name" => display_name,
- "deactivated" => user.deactivated,
+ "is_active" => user.is_active,
"local" => user.local,
"roles" => User.roles(user),
"tags" => user.tags || [],
- "confirmation_pending" => user.confirmation_pending,
- "approval_pending" => user.approval_pending,
+ "is_confirmed" => user.is_confirmed,
+ "is_approved" => user.is_approved,
"url" => user.uri || user.ap_id,
"registration_reason" => user.registration_reason,
"actor_type" => user.actor_type
diff --git a/lib/pleroma/web/admin_api/views/chat_view.ex b/lib/pleroma/web/admin_api/views/chat_view.ex
index 847df1423..2a2015ad1 100644
--- a/lib/pleroma/web/admin_api/views/chat_view.ex
+++ b/lib/pleroma/web/admin_api/views/chat_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.ChatView do
diff --git a/lib/pleroma/web/admin_api/views/config_view.ex b/lib/pleroma/web/admin_api/views/config_view.ex
index d2d8b5907..d29b4963d 100644
--- a/lib/pleroma/web/admin_api/views/config_view.ex
+++ b/lib/pleroma/web/admin_api/views/config_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.ConfigView do
diff --git a/lib/pleroma/web/admin_api/views/frontend_view.ex b/lib/pleroma/web/admin_api/views/frontend_view.ex
new file mode 100644
index 000000000..a3933a57d
--- /dev/null
+++ b/lib/pleroma/web/admin_api/views/frontend_view.ex
@@ -0,0 +1,21 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.AdminAPI.FrontendView do
+ use Pleroma.Web, :view
+
+ def render("index.json", %{frontends: frontends}) do
+ render_many(frontends, __MODULE__, "show.json")
+ end
+
+ def render("show.json", %{frontend: frontend}) do
+ %{
+ name: frontend["name"],
+ git: frontend["git"],
+ build_url: frontend["build_url"],
+ ref: frontend["ref"],
+ installed: frontend["installed"]
+ }
+ end
+end
diff --git a/lib/pleroma/web/admin_api/views/invite_view.ex b/lib/pleroma/web/admin_api/views/invite_view.ex
index f93cb6916..c7e307bda 100644
--- a/lib/pleroma/web/admin_api/views/invite_view.ex
+++ b/lib/pleroma/web/admin_api/views/invite_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.InviteView do
diff --git a/lib/pleroma/web/admin_api/views/media_proxy_cache_view.ex b/lib/pleroma/web/admin_api/views/media_proxy_cache_view.ex
index a803bda0b..1ec123048 100644
--- a/lib/pleroma/web/admin_api/views/media_proxy_cache_view.ex
+++ b/lib/pleroma/web/admin_api/views/media_proxy_cache_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.MediaProxyCacheView do
diff --git a/lib/pleroma/web/admin_api/views/moderation_log_view.ex b/lib/pleroma/web/admin_api/views/moderation_log_view.ex
index 112f9e0e1..b3a9efff3 100644
--- a/lib/pleroma/web/admin_api/views/moderation_log_view.ex
+++ b/lib/pleroma/web/admin_api/views/moderation_log_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.ModerationLogView do
@@ -21,6 +21,7 @@ defmodule Pleroma.Web.AdminAPI.ModerationLogView do
|> DateTime.to_unix()
%{
+ id: log_entry.id,
data: log_entry.data,
time: time,
message: ModerationLog.get_log_entry_message(log_entry)
diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex
index 535556370..1c67b2458 100644
--- a/lib/pleroma/web/admin_api/views/report_view.ex
+++ b/lib/pleroma/web/admin_api/views/report_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.ReportView do
@@ -19,8 +19,7 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
reports:
reports[:items]
|> Enum.map(&Report.extract_report_info/1)
- |> Enum.map(&render(__MODULE__, "show.json", &1))
- |> Enum.reverse(),
+ |> Enum.map(&render(__MODULE__, "show.json", &1)),
total: reports[:total]
}
end
diff --git a/lib/pleroma/web/admin_api/views/status_view.ex b/lib/pleroma/web/admin_api/views/status_view.ex
index 6042a22b6..48d639b41 100644
--- a/lib/pleroma/web/admin_api/views/status_view.ex
+++ b/lib/pleroma/web/admin_api/views/status_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.StatusView do
@@ -13,6 +13,10 @@ defmodule Pleroma.Web.AdminAPI.StatusView do
defdelegate merge_account_views(user), to: AdminAPI.AccountView
+ def render("index.json", %{total: total} = opts) do
+ %{total: total, activities: safe_render_many(opts.activities, __MODULE__, "show.json", opts)}
+ end
+
def render("index.json", opts) do
safe_render_many(opts.activities, __MODULE__, "show.json", opts)
end
diff --git a/lib/pleroma/web/api_spec.ex b/lib/pleroma/web/api_spec.ex
index 93a5273e3..adc8762dc 100644
--- a/lib/pleroma/web/api_spec.ex
+++ b/lib/pleroma/web/api_spec.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec do
@@ -11,10 +11,10 @@ defmodule Pleroma.Web.ApiSpec do
@behaviour OpenApi
@impl OpenApi
- def spec do
+ def spec(opts \\ []) do
%OpenApi{
servers:
- if Phoenix.Endpoint.server?(:pleroma, Endpoint) do
+ if opts[:server_specific] do
[
# Populate the Server info from a phoenix endpoint
OpenApiSpex.Server.from_endpoint(Endpoint)
@@ -23,9 +23,26 @@ defmodule Pleroma.Web.ApiSpec do
[]
end,
info: %OpenApiSpex.Info{
- title: "Pleroma",
- description: Application.spec(:pleroma, :description) |> to_string(),
- version: Application.spec(:pleroma, :vsn) |> to_string()
+ title: "Pleroma API",
+ description: """
+ This is documentation for client Pleroma API. Most of the endpoints and entities come
+ from Mastodon API and have custom extensions on top.
+
+ While this document aims to be a complete guide to the client API Pleroma exposes,
+ the details are still being worked out. Some endpoints may have incomplete or poorly worded documentation.
+ You might want to check the following resources if something is not clear:
+ - [Legacy Pleroma-specific endpoint documentation](https://docs-develop.pleroma.social/backend/development/API/pleroma_api/)
+ - [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!
+ """,
+ # Strip environment from the version
+ version: Application.spec(:pleroma, :vsn) |> to_string() |> String.replace(~r/\+.*$/, ""),
+ extensions: %{
+ # Logo path should be picked so that the path exists both on Pleroma instances and on api.pleroma.social
+ "x-logo": %{"url" => "/static/logo.svg", "altText" => "Pleroma logo"}
+ }
},
# populate the paths from a phoenix router
paths: OpenApiSpex.Paths.from_router(Router),
@@ -45,15 +62,73 @@ defmodule Pleroma.Web.ApiSpec do
authorizationUrl: "/oauth/authorize",
tokenUrl: "/oauth/token",
scopes: %{
- "read" => "read",
- "write" => "write",
- "follow" => "follow",
- "push" => "push"
+ # TODO: Document granular scopes
+ "read" => "Read everything",
+ "write" => "Write everything",
+ "follow" => "Manage relationships",
+ "push" => "Web Push API subscriptions"
}
}
}
}
}
+ },
+ extensions: %{
+ # Redoc-specific extension, every time a new tag is added it should be reflected here,
+ # otherwise it won't be shown.
+ "x-tagGroups": [
+ %{
+ "name" => "Accounts",
+ "tags" => ["Account actions", "Retrieve account information", "Scrobbles"]
+ },
+ %{
+ "name" => "Administration",
+ "tags" => [
+ "Chat administration",
+ "Emoji pack administration",
+ "Frontend managment",
+ "Instance configuration",
+ "Instance documents",
+ "Invites",
+ "MediaProxy cache",
+ "OAuth application managment",
+ "Report managment",
+ "Relays",
+ "Status administration"
+ ]
+ },
+ %{"name" => "Applications", "tags" => ["Applications", "Push subscriptions"]},
+ %{
+ "name" => "Current account",
+ "tags" => [
+ "Account credentials",
+ "Backups",
+ "Blocks and mutes",
+ "Data import",
+ "Domain blocks",
+ "Follow requests",
+ "Mascot",
+ "Markers",
+ "Notifications"
+ ]
+ },
+ %{"name" => "Instance", "tags" => ["Custom emojis"]},
+ %{"name" => "Messaging", "tags" => ["Chats", "Conversations"]},
+ %{
+ "name" => "Statuses",
+ "tags" => [
+ "Emoji reactions",
+ "Lists",
+ "Polls",
+ "Timelines",
+ "Retrieve status information",
+ "Scheduled statuses",
+ "Search",
+ "Status actions"
+ ]
+ },
+ %{"name" => "Miscellaneous", "tags" => ["Emoji packs", "Reports", "Suggestions"]}
+ ]
}
}
# discover request/response schemas from path specs
diff --git a/lib/pleroma/web/api_spec/cast_and_validate.ex b/lib/pleroma/web/api_spec/cast_and_validate.ex
index 6d1a7ebbc..a3da856ff 100644
--- a/lib/pleroma/web/api_spec/cast_and_validate.ex
+++ b/lib/pleroma/web/api_spec/cast_and_validate.ex
@@ -1,6 +1,6 @@
# Pleroma: A lightweight social networking server
# Copyright © 2019-2020 Moxley Stratton, Mike Buhot <https://github.com/open-api-spex/open_api_spex>, MPL-2.0
-# Copyright © 2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.CastAndValidate do
diff --git a/lib/pleroma/web/api_spec/helpers.ex b/lib/pleroma/web/api_spec/helpers.ex
index 34de2ed57..6f67339e6 100644
--- a/lib/pleroma/web/api_spec/helpers.ex
+++ b/lib/pleroma/web/api_spec/helpers.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Helpers do
@@ -63,7 +63,7 @@ defmodule Pleroma.Web.ApiSpec.Helpers do
:with_relationships,
:query,
BooleanLike,
- "Embed relationships into accounts."
+ "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 d90ddb787..54e5ebc76 100644
--- a/lib/pleroma/web/api_spec/operations/account_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/account_operation.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.AccountOperation do
@@ -26,7 +26,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
@spec create_operation() :: Operation.t()
def create_operation do
%Operation{
- tags: ["accounts"],
+ tags: ["Account credentials"],
summary: "Register an account",
description:
"Creates a user and account records. Returns an account access token for the app that initiated the request. The app should save this token for later, and should wait for the user to confirm their account by clicking a link in their email inbox.",
@@ -43,7 +43,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
def verify_credentials_operation do
%Operation{
- tags: ["accounts"],
+ tags: ["Account credentials"],
description: "Test to make sure that the user token works.",
summary: "Verify account credentials",
operationId: "AccountController.verify_credentials",
@@ -56,7 +56,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
def update_credentials_operation do
%Operation{
- tags: ["accounts"],
+ tags: ["Account credentials"],
summary: "Update account credentials",
description: "Update the user's display and preferences.",
operationId: "AccountController.update_credentials",
@@ -71,8 +71,8 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
def relationships_operation do
%Operation{
- tags: ["accounts"],
- summary: "Check relationships to other accounts",
+ tags: ["Retrieve account information"],
+ summary: "Relationship with current account",
operationId: "AccountController.relationships",
description: "Find out whether a given account is followed, blocked, muted, etc.",
security: [%{"oAuth" => ["read:follows"]}],
@@ -95,11 +95,14 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
def show_operation do
%Operation{
- tags: ["accounts"],
+ tags: ["Retrieve account information"],
summary: "Account",
operationId: "AccountController.show",
description: "View information about a profile.",
- parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
+ parameters: [
+ %Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
+ with_relationships_param()
+ ],
responses: %{
200 => Operation.response("Account", "application/json", Account),
401 => Operation.response("Error", "application/json", ApiError),
@@ -110,8 +113,8 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
def statuses_operation do
%Operation{
- tags: ["accounts"],
summary: "Statuses",
+ tags: ["Retrieve account information"],
operationId: "AccountController.statuses",
description:
"Statuses posted to the given account. Public (for public statuses only), or user token + `read:statuses` (for private statuses the user is authorized to see)",
@@ -130,7 +133,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
:with_muted,
:query,
BooleanLike,
- "Include statuses from muted acccounts."
+ "Include statuses from muted accounts."
),
Operation.parameter(:exclude_reblogs, :query, BooleanLike, "Exclude reblogs"),
Operation.parameter(:exclude_replies, :query, BooleanLike, "Exclude replies"),
@@ -139,6 +142,12 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
:query,
%Schema{type: :array, items: VisibilityScope},
"Exclude visibilities"
+ ),
+ Operation.parameter(
+ :with_muted,
+ :query,
+ BooleanLike,
+ "Include reactions from muted accounts."
)
] ++ pagination_params(),
responses: %{
@@ -151,7 +160,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
def followers_operation do
%Operation{
- tags: ["accounts"],
+ tags: ["Retrieve account information"],
summary: "Followers",
operationId: "AccountController.followers",
security: [%{"oAuth" => ["read:accounts"]}],
@@ -170,7 +179,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
def following_operation do
%Operation{
- tags: ["accounts"],
+ tags: ["Retrieve account information"],
summary: "Following",
operationId: "AccountController.following",
security: [%{"oAuth" => ["read:accounts"]}],
@@ -187,7 +196,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
def lists_operation do
%Operation{
- tags: ["accounts"],
+ tags: ["Retrieve account information"],
summary: "Lists containing this account",
operationId: "AccountController.lists",
security: [%{"oAuth" => ["read:lists"]}],
@@ -199,7 +208,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
def follow_operation do
%Operation{
- tags: ["accounts"],
+ tags: ["Account actions"],
summary: "Follow",
operationId: "AccountController.follow",
security: [%{"oAuth" => ["follow", "write:follows"]}],
@@ -232,7 +241,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
def unfollow_operation do
%Operation{
- tags: ["accounts"],
+ tags: ["Account actions"],
summary: "Unfollow",
operationId: "AccountController.unfollow",
security: [%{"oAuth" => ["follow", "write:follows"]}],
@@ -248,7 +257,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
def mute_operation do
%Operation{
- tags: ["accounts"],
+ tags: ["Account actions"],
summary: "Mute",
operationId: "AccountController.mute",
security: [%{"oAuth" => ["follow", "write:mutes"]}],
@@ -262,6 +271,12 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
:query,
%Schema{allOf: [BooleanLike], default: true},
"Mute notifications in addition to statuses? Defaults to `true`."
+ ),
+ Operation.parameter(
+ :expires_in,
+ :query,
+ %Schema{type: :integer, default: 0},
+ "Expire the mute in `expires_in` seconds. Default 0 for infinity"
)
],
responses: %{
@@ -272,7 +287,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
def unmute_operation do
%Operation{
- tags: ["accounts"],
+ tags: ["Account actions"],
summary: "Unmute",
operationId: "AccountController.unmute",
security: [%{"oAuth" => ["follow", "write:mutes"]}],
@@ -286,7 +301,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
def block_operation do
%Operation{
- tags: ["accounts"],
+ tags: ["Account actions"],
summary: "Block",
operationId: "AccountController.block",
security: [%{"oAuth" => ["follow", "write:blocks"]}],
@@ -301,7 +316,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
def unblock_operation do
%Operation{
- tags: ["accounts"],
+ tags: ["Account actions"],
summary: "Unblock",
operationId: "AccountController.unblock",
security: [%{"oAuth" => ["follow", "write:blocks"]}],
@@ -315,7 +330,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
def follow_by_uri_operation do
%Operation{
- tags: ["accounts"],
+ tags: ["Account actions"],
summary: "Follow by URI",
operationId: "AccountController.follows",
security: [%{"oAuth" => ["follow", "write:follows"]}],
@@ -330,11 +345,12 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
def mutes_operation do
%Operation{
- tags: ["accounts"],
- summary: "Muted accounts",
+ tags: ["Blocks and mutes"],
+ summary: "Retrieve list of mutes",
operationId: "AccountController.mutes",
description: "Accounts the user has muted.",
security: [%{"oAuth" => ["follow", "read:mutes"]}],
+ parameters: [with_relationships_param() | pagination_params()],
responses: %{
200 => Operation.response("Accounts", "application/json", array_of_accounts())
}
@@ -343,11 +359,12 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
def blocks_operation do
%Operation{
- tags: ["accounts"],
- summary: "Blocked users",
+ tags: ["Blocks and mutes"],
+ summary: "Retrieve list of blocks",
operationId: "AccountController.blocks",
description: "View your blocks. See also accounts/:id/{block,unblock}",
security: [%{"oAuth" => ["read:blocks"]}],
+ parameters: pagination_params(),
responses: %{
200 => Operation.response("Accounts", "application/json", array_of_accounts())
}
@@ -356,7 +373,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
def endorsements_operation do
%Operation{
- tags: ["accounts"],
+ tags: ["Retrieve account information"],
summary: "Endorsements",
operationId: "AccountController.endorsements",
description: "Not implemented",
@@ -369,7 +386,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
def identity_proofs_operation do
%Operation{
- tags: ["accounts"],
+ tags: ["Retrieve account information"],
summary: "Identity proofs",
operationId: "AccountController.identity_proofs",
# Validators complains about unused path params otherwise
@@ -600,6 +617,12 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
nullable: true,
description: "Allows automatically follow moved following accounts"
},
+ also_known_as: %Schema{
+ type: :array,
+ items: %Schema{type: :string},
+ nullable: true,
+ description: "List of alternate ActivityPub IDs"
+ },
pleroma_background_image: %Schema{
type: :string,
nullable: true,
@@ -610,7 +633,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
allOf: [BooleanLike],
nullable: true,
description:
- "Discovery of this account in search results and other services is allowed."
+ "Discovery (listing, indexing) of this account by external services (search bots etc.) is allowed."
},
actor_type: ActorType
},
@@ -630,6 +653,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
pleroma_settings_store: %{"pleroma-fe" => %{"key" => "val"}},
skip_thread_containment: false,
allow_following_move: false,
+ also_known_as: ["https://foo.bar/users/foo"],
discoverable: false,
actor_type: "Person"
}
@@ -721,10 +745,17 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
nullable: true,
description: "Mute notifications in addition to statuses? Defaults to true.",
default: true
+ },
+ expires_in: %Schema{
+ type: :integer,
+ nullable: true,
+ description: "Expire the mute in `expires_in` seconds. Default 0 for infinity",
+ default: 0
}
},
example: %{
- "notifications" => true
+ "notifications" => true,
+ "expires_in" => 86_400
}
}
end
diff --git a/lib/pleroma/web/api_spec/operations/admin/chat_operation.ex b/lib/pleroma/web/api_spec/operations/admin/chat_operation.ex
index d3e5dfc1c..57906445e 100644
--- a/lib/pleroma/web/api_spec/operations/admin/chat_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/admin/chat_operation.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Admin.ChatOperation do
@@ -16,7 +16,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ChatOperation do
def delete_message_operation do
%Operation{
- tags: ["admin", "chat"],
+ tags: ["Chat administration"],
summary: "Delete an individual chat message",
operationId: "AdminAPI.ChatController.delete_message",
parameters: [
@@ -33,7 +33,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ChatOperation do
},
security: [
%{
- "oAuth" => ["write:chats"]
+ "oAuth" => ["admin:write:chats"]
}
]
}
@@ -41,8 +41,8 @@ defmodule Pleroma.Web.ApiSpec.Admin.ChatOperation do
def messages_operation do
%Operation{
- tags: ["admin", "chat"],
- summary: "Get the most recent messages of the chat",
+ tags: ["Chat administration"],
+ summary: "Get chat's messages",
operationId: "AdminAPI.ChatController.messages",
parameters:
[Operation.parameter(:id, :path, :string, "The ID of the Chat")] ++
@@ -57,7 +57,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ChatOperation do
},
security: [
%{
- "oAuth" => ["read:chats"]
+ "oAuth" => ["admin:read:chats"]
}
]
}
@@ -65,7 +65,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ChatOperation do
def show_operation do
%Operation{
- tags: ["chat"],
+ tags: ["Chat administration"],
summary: "Create a chat",
operationId: "AdminAPI.ChatController.show",
parameters: [
@@ -88,7 +88,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ChatOperation do
},
security: [
%{
- "oAuth" => ["read"]
+ "oAuth" => ["admin:read"]
}
]
}
diff --git a/lib/pleroma/web/api_spec/operations/admin/config_operation.ex b/lib/pleroma/web/api_spec/operations/admin/config_operation.ex
index 3a8380797..30c3433b7 100644
--- a/lib/pleroma/web/api_spec/operations/admin/config_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/admin/config_operation.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Admin.ConfigOperation do
@@ -16,8 +16,8 @@ defmodule Pleroma.Web.ApiSpec.Admin.ConfigOperation do
def show_operation do
%Operation{
- tags: ["Admin", "Config"],
- summary: "Get list of merged default settings with saved in database",
+ tags: ["Instance configuration"],
+ summary: "Retrieve instance configuration",
operationId: "AdminAPI.ConfigController.show",
parameters: [
Operation.parameter(
@@ -28,7 +28,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ConfigOperation do
)
| admin_api_params()
],
- security: [%{"oAuth" => ["read"]}],
+ security: [%{"oAuth" => ["admin:read"]}],
responses: %{
200 => Operation.response("Config", "application/json", config_response()),
400 => Operation.response("Bad Request", "application/json", ApiError)
@@ -38,10 +38,10 @@ defmodule Pleroma.Web.ApiSpec.Admin.ConfigOperation do
def update_operation do
%Operation{
- tags: ["Admin", "Config"],
- summary: "Update config settings",
+ tags: ["Instance configuration"],
+ summary: "Update instance configuration",
operationId: "AdminAPI.ConfigController.update",
- security: [%{"oAuth" => ["write"]}],
+ security: [%{"oAuth" => ["admin:write"]}],
parameters: admin_api_params(),
requestBody:
request_body("Parameters", %Schema{
@@ -71,10 +71,10 @@ defmodule Pleroma.Web.ApiSpec.Admin.ConfigOperation do
def descriptions_operation do
%Operation{
- tags: ["Admin", "Config"],
- summary: "Get JSON with config descriptions.",
+ tags: ["Instance configuration"],
+ summary: "Retrieve config description",
operationId: "AdminAPI.ConfigController.descriptions",
- security: [%{"oAuth" => ["read"]}],
+ security: [%{"oAuth" => ["admin:read"]}],
parameters: admin_api_params(),
responses: %{
200 =>
diff --git a/lib/pleroma/web/api_spec/operations/admin/frontend_operation.ex b/lib/pleroma/web/api_spec/operations/admin/frontend_operation.ex
new file mode 100644
index 000000000..566f1eeb1
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/admin/frontend_operation.ex
@@ -0,0 +1,85 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Admin.FrontendOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+
+ import Pleroma.Web.ApiSpec.Helpers
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def index_operation do
+ %Operation{
+ tags: ["Frontend managment"],
+ summary: "Retrieve a list of available frontends",
+ operationId: "AdminAPI.FrontendController.index",
+ security: [%{"oAuth" => ["admin:read"]}],
+ responses: %{
+ 200 => Operation.response("Response", "application/json", list_of_frontends()),
+ 403 => Operation.response("Forbidden", "application/json", ApiError)
+ }
+ }
+ end
+
+ def install_operation do
+ %Operation{
+ tags: ["Frontend managment"],
+ summary: "Install a frontend",
+ operationId: "AdminAPI.FrontendController.install",
+ security: [%{"oAuth" => ["admin:read"]}],
+ requestBody: request_body("Parameters", install_request(), required: true),
+ responses: %{
+ 200 => Operation.response("Response", "application/json", list_of_frontends()),
+ 403 => Operation.response("Forbidden", "application/json", ApiError),
+ 400 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp list_of_frontends do
+ %Schema{
+ type: :array,
+ items: %Schema{
+ type: :object,
+ properties: %{
+ name: %Schema{type: :string},
+ git: %Schema{type: :string, format: :uri, nullable: true},
+ build_url: %Schema{type: :string, format: :uri, nullable: true},
+ ref: %Schema{type: :string},
+ installed: %Schema{type: :boolean}
+ }
+ }
+ }
+ end
+
+ defp install_request do
+ %Schema{
+ title: "FrontendInstallRequest",
+ type: :object,
+ required: [:name],
+ properties: %{
+ name: %Schema{
+ type: :string
+ },
+ ref: %Schema{
+ type: :string
+ },
+ file: %Schema{
+ type: :string
+ },
+ build_url: %Schema{
+ type: :string
+ },
+ build_dir: %Schema{
+ type: :string
+ }
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/admin/instance_document_operation.ex b/lib/pleroma/web/api_spec/operations/admin/instance_document_operation.ex
index a120ff4e8..79ceae970 100644
--- a/lib/pleroma/web/api_spec/operations/admin/instance_document_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/admin/instance_document_operation.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Admin.InstanceDocumentOperation do
@@ -15,10 +15,10 @@ defmodule Pleroma.Web.ApiSpec.Admin.InstanceDocumentOperation do
def show_operation do
%Operation{
- tags: ["Admin", "InstanceDocument"],
- summary: "Get the instance document",
+ tags: ["Instance documents"],
+ summary: "Retrieve an instance document",
operationId: "AdminAPI.InstanceDocumentController.show",
- security: [%{"oAuth" => ["read"]}],
+ security: [%{"oAuth" => ["admin:read"]}],
parameters: [
Operation.parameter(:name, :path, %Schema{type: :string}, "The document name",
required: true
@@ -36,10 +36,10 @@ defmodule Pleroma.Web.ApiSpec.Admin.InstanceDocumentOperation do
def update_operation do
%Operation{
- tags: ["Admin", "InstanceDocument"],
- summary: "Update the instance document",
+ tags: ["Instance documents"],
+ summary: "Update an instance document",
operationId: "AdminAPI.InstanceDocumentController.update",
- security: [%{"oAuth" => ["write"]}],
+ security: [%{"oAuth" => ["admin:write"]}],
requestBody: Helpers.request_body("Parameters", update_request()),
parameters: [
Operation.parameter(:name, :path, %Schema{type: :string}, "The document name",
@@ -74,10 +74,10 @@ defmodule Pleroma.Web.ApiSpec.Admin.InstanceDocumentOperation do
def delete_operation do
%Operation{
- tags: ["Admin", "InstanceDocument"],
- summary: "Get the instance document",
+ tags: ["Instance documents"],
+ summary: "Delete an instance document",
operationId: "AdminAPI.InstanceDocumentController.delete",
- security: [%{"oAuth" => ["write"]}],
+ security: [%{"oAuth" => ["admin:write"]}],
parameters: [
Operation.parameter(:name, :path, %Schema{type: :string}, "The document name",
required: true
diff --git a/lib/pleroma/web/api_spec/operations/admin/invite_operation.ex b/lib/pleroma/web/api_spec/operations/admin/invite_operation.ex
index 801024d75..704f082ba 100644
--- a/lib/pleroma/web/api_spec/operations/admin/invite_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/admin/invite_operation.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Admin.InviteOperation do
@@ -16,10 +16,10 @@ defmodule Pleroma.Web.ApiSpec.Admin.InviteOperation do
def index_operation do
%Operation{
- tags: ["Admin", "Invites"],
+ tags: ["Invites"],
summary: "Get a list of generated invites",
operationId: "AdminAPI.InviteController.index",
- security: [%{"oAuth" => ["read:invites"]}],
+ security: [%{"oAuth" => ["admin:read:invites"]}],
parameters: admin_api_params(),
responses: %{
200 =>
@@ -48,10 +48,10 @@ defmodule Pleroma.Web.ApiSpec.Admin.InviteOperation do
def create_operation do
%Operation{
- tags: ["Admin", "Invites"],
+ tags: ["Invites"],
summary: "Create an account registration invite token",
operationId: "AdminAPI.InviteController.create",
- security: [%{"oAuth" => ["write:invites"]}],
+ security: [%{"oAuth" => ["admin:write:invites"]}],
parameters: admin_api_params(),
requestBody:
request_body("Parameters", %Schema{
@@ -69,10 +69,10 @@ defmodule Pleroma.Web.ApiSpec.Admin.InviteOperation do
def revoke_operation do
%Operation{
- tags: ["Admin", "Invites"],
+ tags: ["Invites"],
summary: "Revoke invite by token",
operationId: "AdminAPI.InviteController.revoke",
- security: [%{"oAuth" => ["write:invites"]}],
+ security: [%{"oAuth" => ["admin:write:invites"]}],
parameters: admin_api_params(),
requestBody:
request_body(
@@ -96,10 +96,10 @@ defmodule Pleroma.Web.ApiSpec.Admin.InviteOperation do
def email_operation do
%Operation{
- tags: ["Admin", "Invites"],
+ tags: ["Invites"],
summary: "Sends registration invite via email",
operationId: "AdminAPI.InviteController.email",
- security: [%{"oAuth" => ["write:invites"]}],
+ security: [%{"oAuth" => ["admin:write:invites"]}],
parameters: admin_api_params(),
requestBody:
request_body(
diff --git a/lib/pleroma/web/api_spec/operations/admin/media_proxy_cache_operation.ex b/lib/pleroma/web/api_spec/operations/admin/media_proxy_cache_operation.ex
index ab45d6633..8f85ebf2d 100644
--- a/lib/pleroma/web/api_spec/operations/admin/media_proxy_cache_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/admin/media_proxy_cache_operation.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Admin.MediaProxyCacheOperation do
@@ -16,10 +16,10 @@ defmodule Pleroma.Web.ApiSpec.Admin.MediaProxyCacheOperation do
def index_operation do
%Operation{
- tags: ["Admin", "MediaProxyCache"],
- summary: "Fetch a paginated list of all banned MediaProxy URLs in Cachex",
+ tags: ["MediaProxy cache"],
+ summary: "Retrieve a list of banned MediaProxy URLs",
operationId: "AdminAPI.MediaProxyCacheController.index",
- security: [%{"oAuth" => ["read:media_proxy_caches"]}],
+ security: [%{"oAuth" => ["admin:read:media_proxy_caches"]}],
parameters: [
Operation.parameter(
:query,
@@ -44,7 +44,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.MediaProxyCacheOperation do
responses: %{
200 =>
Operation.response(
- "Array of banned MediaProxy URLs in Cachex",
+ "Array of MediaProxy URLs",
"application/json",
%Schema{
type: :object,
@@ -68,10 +68,10 @@ defmodule Pleroma.Web.ApiSpec.Admin.MediaProxyCacheOperation do
def delete_operation do
%Operation{
- tags: ["Admin", "MediaProxyCache"],
- summary: "Remove a banned MediaProxy URL from Cachex",
+ tags: ["MediaProxy cache"],
+ summary: "Remove a banned MediaProxy URL",
operationId: "AdminAPI.MediaProxyCacheController.delete",
- security: [%{"oAuth" => ["write:media_proxy_caches"]}],
+ security: [%{"oAuth" => ["admin:write:media_proxy_caches"]}],
parameters: admin_api_params(),
requestBody:
request_body(
@@ -94,10 +94,10 @@ defmodule Pleroma.Web.ApiSpec.Admin.MediaProxyCacheOperation do
def purge_operation do
%Operation{
- tags: ["Admin", "MediaProxyCache"],
- summary: "Purge and optionally ban a MediaProxy URL",
+ tags: ["MediaProxy cache"],
+ summary: "Purge a URL from MediaProxy cache and optionally ban it",
operationId: "AdminAPI.MediaProxyCacheController.purge",
- security: [%{"oAuth" => ["write:media_proxy_caches"]}],
+ security: [%{"oAuth" => ["admin:write:media_proxy_caches"]}],
parameters: admin_api_params(),
requestBody:
request_body(
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 a75f3e622..35b029b19 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
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do
@@ -16,10 +16,10 @@ defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do
def index_operation do
%Operation{
- summary: "List OAuth apps",
- tags: ["Admin", "oAuth Apps"],
+ summary: "Retrieve a list of OAuth applications",
+ tags: ["OAuth application managment"],
operationId: "AdminAPI.OAuthAppController.index",
- security: [%{"oAuth" => ["write"]}],
+ security: [%{"oAuth" => ["admin:write"]}],
parameters: [
Operation.parameter(:name, :query, %Schema{type: :string}, "App name"),
Operation.parameter(:client_id, :query, %Schema{type: :string}, "Client ID"),
@@ -69,12 +69,12 @@ defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do
def create_operation do
%Operation{
- tags: ["Admin", "oAuth Apps"],
- summary: "Create OAuth App",
+ tags: ["OAuth application managment"],
+ summary: "Create an OAuth application",
operationId: "AdminAPI.OAuthAppController.create",
requestBody: request_body("Parameters", create_request()),
parameters: admin_api_params(),
- security: [%{"oAuth" => ["write"]}],
+ security: [%{"oAuth" => ["admin:write"]}],
responses: %{
200 => Operation.response("App", "application/json", oauth_app()),
400 => Operation.response("Bad Request", "application/json", ApiError)
@@ -84,11 +84,11 @@ defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do
def update_operation do
%Operation{
- tags: ["Admin", "oAuth Apps"],
- summary: "Update OAuth App",
+ tags: ["OAuth application managment"],
+ summary: "Update OAuth application",
operationId: "AdminAPI.OAuthAppController.update",
parameters: [id_param() | admin_api_params()],
- security: [%{"oAuth" => ["write"]}],
+ security: [%{"oAuth" => ["admin:write"]}],
requestBody: request_body("Parameters", update_request()),
responses: %{
200 => Operation.response("App", "application/json", oauth_app()),
@@ -102,11 +102,11 @@ defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do
def delete_operation do
%Operation{
- tags: ["Admin", "oAuth Apps"],
- summary: "Delete OAuth App",
+ tags: ["OAuth application managment"],
+ summary: "Delete OAuth application",
operationId: "AdminAPI.OAuthAppController.delete",
parameters: [id_param() | admin_api_params()],
- security: [%{"oAuth" => ["write"]}],
+ security: [%{"oAuth" => ["admin:write"]}],
responses: %{
204 => no_content_response(),
400 => no_content_response()
diff --git a/lib/pleroma/web/api_spec/operations/admin/relay_operation.ex b/lib/pleroma/web/api_spec/operations/admin/relay_operation.ex
index f754bb9f5..c55c84fee 100644
--- a/lib/pleroma/web/api_spec/operations/admin/relay_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/admin/relay_operation.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Admin.RelayOperation do
@@ -15,10 +15,10 @@ defmodule Pleroma.Web.ApiSpec.Admin.RelayOperation do
def index_operation do
%Operation{
- tags: ["Admin", "Relays"],
- summary: "List Relays",
+ tags: ["Relays"],
+ summary: "Retrieve a list of relays",
operationId: "AdminAPI.RelayController.index",
- security: [%{"oAuth" => ["read"]}],
+ security: [%{"oAuth" => ["admin:read"]}],
parameters: admin_api_params(),
responses: %{
200 =>
@@ -37,10 +37,10 @@ defmodule Pleroma.Web.ApiSpec.Admin.RelayOperation do
def follow_operation do
%Operation{
- tags: ["Admin", "Relays"],
- summary: "Follow a Relay",
+ tags: ["Relays"],
+ summary: "Follow a relay",
operationId: "AdminAPI.RelayController.follow",
- security: [%{"oAuth" => ["write:follows"]}],
+ security: [%{"oAuth" => ["admin:write:follows"]}],
parameters: admin_api_params(),
requestBody: request_body("Parameters", relay_url()),
responses: %{
@@ -51,10 +51,10 @@ defmodule Pleroma.Web.ApiSpec.Admin.RelayOperation do
def unfollow_operation do
%Operation{
- tags: ["Admin", "Relays"],
- summary: "Unfollow a Relay",
+ tags: ["Relays"],
+ summary: "Unfollow a relay",
operationId: "AdminAPI.RelayController.unfollow",
- security: [%{"oAuth" => ["write:follows"]}],
+ security: [%{"oAuth" => ["admin:write:follows"]}],
parameters: admin_api_params(),
requestBody: request_body("Parameters", relay_unfollow()),
responses: %{
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 3bb7ec49e..8d7577505 100644
--- a/lib/pleroma/web/api_spec/operations/admin/report_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/admin/report_operation.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
@@ -19,10 +19,10 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
def index_operation do
%Operation{
- tags: ["Admin", "Reports"],
- summary: "Get a list of reports",
+ tags: ["Report managment"],
+ summary: "Retrieve a list of reports",
operationId: "AdminAPI.ReportController.index",
- security: [%{"oAuth" => ["read:reports"]}],
+ security: [%{"oAuth" => ["admin:read:reports"]}],
parameters: [
Operation.parameter(
:state,
@@ -69,11 +69,11 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
def show_operation do
%Operation{
- tags: ["Admin", "Reports"],
- summary: "Get an individual report",
+ tags: ["Report managment"],
+ summary: "Retrieve a report",
operationId: "AdminAPI.ReportController.show",
parameters: [id_param() | admin_api_params()],
- security: [%{"oAuth" => ["read:reports"]}],
+ security: [%{"oAuth" => ["admin:read:reports"]}],
responses: %{
200 => Operation.response("Report", "application/json", report()),
404 => Operation.response("Not Found", "application/json", ApiError)
@@ -83,10 +83,10 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
def update_operation do
%Operation{
- tags: ["Admin", "Reports"],
- summary: "Change the state of one or multiple reports",
+ tags: ["Report managment"],
+ summary: "Change state of specified reports",
operationId: "AdminAPI.ReportController.update",
- security: [%{"oAuth" => ["write:reports"]}],
+ security: [%{"oAuth" => ["admin:write:reports"]}],
parameters: admin_api_params(),
requestBody: request_body("Parameters", update_request(), required: true),
responses: %{
@@ -99,8 +99,8 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
def notes_create_operation do
%Operation{
- tags: ["Admin", "Reports"],
- summary: "Create report note",
+ tags: ["Report managment"],
+ summary: "Add a note to the report",
operationId: "AdminAPI.ReportController.notes_create",
parameters: [id_param() | admin_api_params()],
requestBody:
@@ -110,7 +110,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
content: %Schema{type: :string, description: "The message"}
}
}),
- security: [%{"oAuth" => ["write:reports"]}],
+ security: [%{"oAuth" => ["admin:write:reports"]}],
responses: %{
204 => no_content_response(),
404 => Operation.response("Not Found", "application/json", ApiError)
@@ -120,15 +120,15 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
def notes_delete_operation do
%Operation{
- tags: ["Admin", "Reports"],
- summary: "Delete report note",
+ tags: ["Report managment"],
+ summary: "Delete note attached to the report",
operationId: "AdminAPI.ReportController.notes_delete",
parameters: [
Operation.parameter(:report_id, :path, :string, "Report ID"),
Operation.parameter(:id, :path, :string, "Note ID")
| admin_api_params()
],
- security: [%{"oAuth" => ["write:reports"]}],
+ security: [%{"oAuth" => ["admin:write:reports"]}],
responses: %{
204 => no_content_response(),
404 => Operation.response("Not Found", "application/json", ApiError)
@@ -136,11 +136,11 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
}
end
- defp report_state do
+ def report_state do
%Schema{type: :string, enum: ["open", "closed", "resolved"]}
end
- defp id_param do
+ def id_param do
Operation.parameter(:id, :path, FlakeID, "Report ID",
example: "9umDrYheeY451cQnEe",
required: true
@@ -182,7 +182,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
properties:
Map.merge(Account.schema().properties, %{
nickname: %Schema{type: :string},
- deactivated: %Schema{type: :boolean},
+ is_active: %Schema{type: :boolean},
local: %Schema{type: :boolean},
roles: %Schema{
type: :object,
@@ -191,7 +191,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
moderator: %Schema{type: :boolean}
}
},
- confirmation_pending: %Schema{type: :boolean}
+ is_confirmed: %Schema{type: :boolean}
})
}
end
diff --git a/lib/pleroma/web/api_spec/operations/admin/status_operation.ex b/lib/pleroma/web/api_spec/operations/admin/status_operation.ex
index c105838a4..d25ab5247 100644
--- a/lib/pleroma/web/api_spec/operations/admin/status_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/admin/status_operation.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Admin.StatusOperation do
@@ -21,9 +21,10 @@ defmodule Pleroma.Web.ApiSpec.Admin.StatusOperation do
def index_operation do
%Operation{
- tags: ["Admin", "Statuses"],
+ tags: ["Status administration"],
operationId: "AdminAPI.StatusController.index",
- security: [%{"oAuth" => ["read:statuses"]}],
+ summary: "Get all statuses",
+ security: [%{"oAuth" => ["admin:read:statuses"]}],
parameters: [
Operation.parameter(
:godmode,
@@ -69,11 +70,11 @@ defmodule Pleroma.Web.ApiSpec.Admin.StatusOperation do
def show_operation do
%Operation{
- tags: ["Admin", "Statuses"],
- summary: "Show Status",
+ tags: ["Status adminitration)"],
+ summary: "Get status",
operationId: "AdminAPI.StatusController.show",
parameters: [id_param() | admin_api_params()],
- security: [%{"oAuth" => ["read:statuses"]}],
+ security: [%{"oAuth" => ["admin:read:statuses"]}],
responses: %{
200 => Operation.response("Status", "application/json", status()),
404 => Operation.response("Not Found", "application/json", ApiError)
@@ -83,11 +84,11 @@ defmodule Pleroma.Web.ApiSpec.Admin.StatusOperation do
def update_operation do
%Operation{
- tags: ["Admin", "Statuses"],
- summary: "Change the scope of an individual reported status",
+ tags: ["Status adminitration)"],
+ summary: "Change the scope of a status",
operationId: "AdminAPI.StatusController.update",
parameters: [id_param() | admin_api_params()],
- security: [%{"oAuth" => ["write:statuses"]}],
+ security: [%{"oAuth" => ["admin:write:statuses"]}],
requestBody: request_body("Parameters", update_request(), required: true),
responses: %{
200 => Operation.response("Status", "application/json", Status),
@@ -98,11 +99,11 @@ defmodule Pleroma.Web.ApiSpec.Admin.StatusOperation do
def delete_operation do
%Operation{
- tags: ["Admin", "Statuses"],
- summary: "Delete an individual reported status",
+ tags: ["Status adminitration)"],
+ summary: "Delete status",
operationId: "AdminAPI.StatusController.delete",
parameters: [id_param() | admin_api_params()],
- security: [%{"oAuth" => ["write:statuses"]}],
+ security: [%{"oAuth" => ["admin:write:statuses"]}],
responses: %{
200 => empty_object_response(),
404 => Operation.response("Not Found", "application/json", ApiError)
@@ -132,7 +133,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.StatusOperation do
avatar: %Schema{type: :string},
nickname: %Schema{type: :string},
display_name: %Schema{type: :string},
- deactivated: %Schema{type: :boolean},
+ is_active: %Schema{type: :boolean},
local: %Schema{type: :boolean},
roles: %Schema{
type: :object,
@@ -142,7 +143,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.StatusOperation do
}
},
tags: %Schema{type: :string},
- confirmation_pending: %Schema{type: :string}
+ is_confirmed: %Schema{type: :string}
}
}
end
diff --git a/lib/pleroma/web/api_spec/operations/app_operation.ex b/lib/pleroma/web/api_spec/operations/app_operation.ex
index ae01cbbec..dfb1c7170 100644
--- a/lib/pleroma/web/api_spec/operations/app_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/app_operation.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.AppOperation do
@@ -16,7 +16,7 @@ defmodule Pleroma.Web.ApiSpec.AppOperation do
@spec create_operation() :: Operation.t()
def create_operation do
%Operation{
- tags: ["apps"],
+ tags: ["Applications"],
summary: "Create an application",
description: "Create a new application to obtain OAuth2 credentials",
operationId: "AppController.create",
@@ -45,8 +45,8 @@ defmodule Pleroma.Web.ApiSpec.AppOperation do
def verify_credentials_operation do
%Operation{
- tags: ["apps"],
- summary: "Verify your app works",
+ tags: ["Applications"],
+ summary: "Verify the application works",
description: "Confirm that the app's OAuth2 credentials work.",
operationId: "AppController.verify_credentials",
security: [%{"oAuth" => ["read"]}],
diff --git a/lib/pleroma/web/api_spec/operations/chat_operation.ex b/lib/pleroma/web/api_spec/operations/chat_operation.ex
index 0dcfdb354..23cb66392 100644
--- a/lib/pleroma/web/api_spec/operations/chat_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/chat_operation.ex
@@ -1,11 +1,12 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.ChatOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.ApiError
+ alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
alias Pleroma.Web.ApiSpec.Schemas.Chat
alias Pleroma.Web.ApiSpec.Schemas.ChatMessage
@@ -19,7 +20,7 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
def mark_as_read_operation do
%Operation{
- tags: ["chat"],
+ tags: ["Chats"],
summary: "Mark all messages in the chat as read",
operationId: "ChatController.mark_as_read",
parameters: [Operation.parameter(:id, :path, :string, "The ID of the Chat")],
@@ -42,8 +43,8 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
def mark_message_as_read_operation do
%Operation{
- tags: ["chat"],
- summary: "Mark one message in the chat as read",
+ tags: ["Chats"],
+ summary: "Mark a message as read",
operationId: "ChatController.mark_message_as_read",
parameters: [
Operation.parameter(:id, :path, :string, "The ID of the Chat"),
@@ -67,8 +68,8 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
def show_operation do
%Operation{
- tags: ["chat"],
- summary: "Create a chat",
+ tags: ["Chats"],
+ summary: "Retrieve a chat",
operationId: "ChatController.show",
parameters: [
Operation.parameter(
@@ -98,7 +99,7 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
def create_operation do
%Operation{
- tags: ["chat"],
+ tags: ["Chats"],
summary: "Create a chat",
operationId: "ChatController.create",
parameters: [
@@ -129,10 +130,35 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
def index_operation do
%Operation{
- tags: ["chat"],
- summary: "Get a list of chats that you participated in",
+ tags: ["Chats"],
+ summary: "Retrieve list of chats (unpaginated)",
+ deprecated: true,
+ description:
+ "Deprecated due to no support for pagination. Using [/api/v2/pleroma/chats](#operation/ChatController.index2) instead is recommended.",
operationId: "ChatController.index",
- parameters: pagination_params(),
+ parameters: [
+ Operation.parameter(:with_muted, :query, BooleanLike, "Include chats from muted users")
+ ],
+ responses: %{
+ 200 => Operation.response("The chats of the user", "application/json", chats_response())
+ },
+ security: [
+ %{
+ "oAuth" => ["read:chats"]
+ }
+ ]
+ }
+ end
+
+ def index2_operation do
+ %Operation{
+ tags: ["Chats"],
+ summary: "Retrieve list of chats",
+ operationId: "ChatController.index2",
+ parameters: [
+ Operation.parameter(:with_muted, :query, BooleanLike, "Include chats from muted users")
+ | pagination_params()
+ ],
responses: %{
200 => Operation.response("The chats of the user", "application/json", chats_response())
},
@@ -146,8 +172,8 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
def messages_operation do
%Operation{
- tags: ["chat"],
- summary: "Get the most recent messages of the chat",
+ tags: ["Chats"],
+ summary: "Retrieve chat's messages",
operationId: "ChatController.messages",
parameters:
[Operation.parameter(:id, :path, :string, "The ID of the Chat")] ++
@@ -171,7 +197,7 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
def post_chat_message_operation do
%Operation{
- tags: ["chat"],
+ tags: ["Chats"],
summary: "Post a message to the chat",
operationId: "ChatController.post_chat_message",
parameters: [
@@ -198,8 +224,8 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
def delete_message_operation do
%Operation{
- tags: ["chat"],
- summary: "delete_message",
+ tags: ["Chats"],
+ summary: "Delete message",
operationId: "ChatController.delete_message",
parameters: [
Operation.parameter(:id, :path, :string, "The ID of the Chat"),
@@ -232,7 +258,7 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
"account" => %{
"pleroma" => %{
"is_admin" => false,
- "confirmation_pending" => false,
+ "is_confirmed" => true,
"hide_followers_count" => false,
"is_moderator" => false,
"hide_favorites" => true,
diff --git a/lib/pleroma/web/api_spec/operations/conversation_operation.ex b/lib/pleroma/web/api_spec/operations/conversation_operation.ex
index 475468893..17ed1af5e 100644
--- a/lib/pleroma/web/api_spec/operations/conversation_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/conversation_operation.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.ConversationOperation do
@@ -18,7 +18,7 @@ defmodule Pleroma.Web.ApiSpec.ConversationOperation do
def index_operation do
%Operation{
tags: ["Conversations"],
- summary: "Show conversation",
+ summary: "List of conversations",
security: [%{"oAuth" => ["read:statuses"]}],
operationId: "ConversationController.index",
parameters: [
@@ -44,18 +44,33 @@ defmodule Pleroma.Web.ApiSpec.ConversationOperation do
def mark_as_read_operation do
%Operation{
tags: ["Conversations"],
- summary: "Mark as read",
+ summary: "Mark conversation as read",
operationId: "ConversationController.mark_as_read",
- parameters: [
- Operation.parameter(:id, :path, :string, "Conversation ID",
- example: "123",
- required: true
- )
- ],
+ parameters: [id_param()],
security: [%{"oAuth" => ["write:conversations"]}],
responses: %{
200 => Operation.response("Conversation", "application/json", Conversation)
}
}
end
+
+ def delete_operation do
+ %Operation{
+ tags: ["Conversations"],
+ summary: "Remove conversation",
+ operationId: "ConversationController.delete",
+ parameters: [id_param()],
+ security: [%{"oAuth" => ["write:conversations"]}],
+ responses: %{
+ 200 => empty_object_response()
+ }
+ }
+ end
+
+ def id_param do
+ Operation.parameter(:id, :path, :string, "Conversation ID",
+ example: "123",
+ required: true
+ )
+ end
end
diff --git a/lib/pleroma/web/api_spec/operations/custom_emoji_operation.ex b/lib/pleroma/web/api_spec/operations/custom_emoji_operation.ex
index 5ff263ceb..98da1a6de 100644
--- a/lib/pleroma/web/api_spec/operations/custom_emoji_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/custom_emoji_operation.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.CustomEmojiOperation do
@@ -14,8 +14,8 @@ defmodule Pleroma.Web.ApiSpec.CustomEmojiOperation do
def index_operation do
%Operation{
- tags: ["custom_emojis"],
- summary: "List custom custom emojis",
+ tags: ["Custom emojis"],
+ summary: "Retrieve a list of custom emojis",
description: "Returns custom emojis that are available on the server.",
operationId: "CustomEmojiController.index",
responses: %{
diff --git a/lib/pleroma/web/api_spec/operations/domain_block_operation.ex b/lib/pleroma/web/api_spec/operations/domain_block_operation.ex
index 1e0da8209..f124e7fe5 100644
--- a/lib/pleroma/web/api_spec/operations/domain_block_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/domain_block_operation.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.DomainBlockOperation do
@@ -14,9 +14,8 @@ defmodule Pleroma.Web.ApiSpec.DomainBlockOperation do
def index_operation do
%Operation{
- tags: ["domain_blocks"],
- summary: "Fetch domain blocks",
- description: "View domains the user has blocked.",
+ tags: ["Domain blocks"],
+ summary: "Retrieve a list of blocked domains",
security: [%{"oAuth" => ["follow", "read:blocks"]}],
operationId: "DomainBlockController.index",
responses: %{
@@ -34,7 +33,7 @@ defmodule Pleroma.Web.ApiSpec.DomainBlockOperation do
# Supporting domain query parameter is deprecated in Mastodon API
def create_operation do
%Operation{
- tags: ["domain_blocks"],
+ tags: ["Domain blocks"],
summary: "Block a domain",
description: """
Block a domain to:
@@ -55,7 +54,7 @@ defmodule Pleroma.Web.ApiSpec.DomainBlockOperation do
# Supporting domain query parameter is deprecated in Mastodon API
def delete_operation do
%Operation{
- tags: ["domain_blocks"],
+ tags: ["Domain blocks"],
summary: "Unblock a domain",
description: "Remove a domain block, if it exists in the user's array of blocked domains.",
operationId: "DomainBlockController.delete",
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 745d41f88..a7b306a30 100644
--- a/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.EmojiReactionOperation do
@@ -17,13 +17,19 @@ defmodule Pleroma.Web.ApiSpec.EmojiReactionOperation do
def index_operation do
%Operation{
- tags: ["Emoji Reactions"],
+ tags: ["Emoji reactions"],
summary:
"Get an object of emoji to account mappings with accounts that reacted to the post",
parameters: [
Operation.parameter(:id, :path, FlakeID, "Status ID", required: true),
Operation.parameter(:emoji, :path, :string, "Filter by a single unicode emoji",
required: nil
+ ),
+ Operation.parameter(
+ :with_muted,
+ :query,
+ :boolean,
+ "Include reactions from muted acccounts."
)
],
security: [%{"oAuth" => ["read:statuses"]}],
@@ -36,7 +42,7 @@ defmodule Pleroma.Web.ApiSpec.EmojiReactionOperation do
def create_operation do
%Operation{
- tags: ["Emoji Reactions"],
+ tags: ["Emoji reactions"],
summary: "React to a post with a unicode emoji",
parameters: [
Operation.parameter(:id, :path, FlakeID, "Status ID", required: true),
@@ -55,7 +61,7 @@ defmodule Pleroma.Web.ApiSpec.EmojiReactionOperation do
def delete_operation do
%Operation{
- tags: ["Emoji Reactions"],
+ tags: ["Emoji reactions"],
summary: "Remove a reaction to a post with a unicode emoji",
parameters: [
Operation.parameter(:id, :path, FlakeID, "Status ID", required: true),
@@ -72,7 +78,7 @@ defmodule Pleroma.Web.ApiSpec.EmojiReactionOperation do
end
defp array_of_reactions_response do
- Operation.response("Array of Emoji Reactions", "application/json", %Schema{
+ Operation.response("Array of Emoji reactions", "application/json", %Schema{
type: :array,
items: emoji_reaction(),
example: [emoji_reaction().example]
diff --git a/lib/pleroma/web/api_spec/operations/filter_operation.ex b/lib/pleroma/web/api_spec/operations/filter_operation.ex
index 31e576f99..5102921bc 100644
--- a/lib/pleroma/web/api_spec/operations/filter_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/filter_operation.ex
@@ -1,11 +1,12 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.FilterOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Helpers
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
def open_api_operation(action) do
@@ -15,57 +16,64 @@ defmodule Pleroma.Web.ApiSpec.FilterOperation do
def index_operation do
%Operation{
- tags: ["apps"],
- summary: "View all filters",
+ tags: ["Filters"],
+ summary: "All filters",
operationId: "FilterController.index",
security: [%{"oAuth" => ["read:filters"]}],
responses: %{
- 200 => Operation.response("Filters", "application/json", array_of_filters())
+ 200 => Operation.response("Filters", "application/json", array_of_filters()),
+ 403 => Operation.response("Error", "application/json", ApiError)
}
}
end
def create_operation do
%Operation{
- tags: ["apps"],
+ tags: ["Filters"],
summary: "Create a filter",
operationId: "FilterController.create",
requestBody: Helpers.request_body("Parameters", create_request(), required: true),
security: [%{"oAuth" => ["write:filters"]}],
- responses: %{200 => Operation.response("Filter", "application/json", filter())}
+ responses: %{
+ 200 => Operation.response("Filter", "application/json", filter()),
+ 403 => Operation.response("Error", "application/json", ApiError)
+ }
}
end
def show_operation do
%Operation{
- tags: ["apps"],
- summary: "View all filters",
+ tags: ["Filters"],
+ summary: "Filter",
parameters: [id_param()],
operationId: "FilterController.show",
security: [%{"oAuth" => ["read:filters"]}],
responses: %{
- 200 => Operation.response("Filter", "application/json", filter())
+ 200 => Operation.response("Filter", "application/json", filter()),
+ 403 => Operation.response("Error", "application/json", ApiError),
+ 404 => Operation.response("Error", "application/json", ApiError)
}
}
end
def update_operation do
%Operation{
- tags: ["apps"],
+ tags: ["Filters"],
summary: "Update a filter",
parameters: [id_param()],
operationId: "FilterController.update",
requestBody: Helpers.request_body("Parameters", update_request(), required: true),
security: [%{"oAuth" => ["write:filters"]}],
responses: %{
- 200 => Operation.response("Filter", "application/json", filter())
+ 200 => Operation.response("Filter", "application/json", filter()),
+ 403 => Operation.response("Error", "application/json", ApiError)
}
}
end
def delete_operation do
%Operation{
- tags: ["apps"],
+ tags: ["Filters"],
summary: "Remove a filter",
parameters: [id_param()],
operationId: "FilterController.delete",
@@ -75,7 +83,8 @@ defmodule Pleroma.Web.ApiSpec.FilterOperation do
Operation.response("Filter", "application/json", %Schema{
type: :object,
description: "Empty object"
- })
+ }),
+ 403 => Operation.response("Error", "application/json", ApiError)
}
}
end
@@ -210,15 +219,13 @@ defmodule Pleroma.Web.ApiSpec.FilterOperation do
nullable: true,
description: "Consider word boundaries?",
default: true
+ },
+ expires_in: %Schema{
+ nullable: true,
+ type: :integer,
+ description:
+ "Number of seconds from now the filter should expire. Otherwise, null for a filter that doesn't expire."
}
- # TODO: probably should implement filter expiration
- # expires_in: %Schema{
- # type: :string,
- # format: :"date-time",
- # description:
- # "ISO 8601 Datetime for when the filter expires. Otherwise,
- # null for a filter that doesn't expire."
- # }
},
required: [:phrase, :context],
example: %{
diff --git a/lib/pleroma/web/api_spec/operations/follow_request_operation.ex b/lib/pleroma/web/api_spec/operations/follow_request_operation.ex
index ac4aee6da..784019699 100644
--- a/lib/pleroma/web/api_spec/operations/follow_request_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/follow_request_operation.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.FollowRequestOperation do
@@ -15,8 +15,8 @@ defmodule Pleroma.Web.ApiSpec.FollowRequestOperation do
def index_operation do
%Operation{
- tags: ["Follow Requests"],
- summary: "Pending Follows",
+ tags: ["Follow requests"],
+ summary: "Retrieve follow requests",
security: [%{"oAuth" => ["read:follows", "follow"]}],
operationId: "FollowRequestController.index",
responses: %{
@@ -32,8 +32,8 @@ defmodule Pleroma.Web.ApiSpec.FollowRequestOperation do
def authorize_operation do
%Operation{
- tags: ["Follow Requests"],
- summary: "Accept Follow",
+ tags: ["Follow requests"],
+ summary: "Accept follow request",
operationId: "FollowRequestController.authorize",
parameters: [id_param()],
security: [%{"oAuth" => ["follow", "write:follows"]}],
@@ -45,8 +45,8 @@ defmodule Pleroma.Web.ApiSpec.FollowRequestOperation do
def reject_operation do
%Operation{
- tags: ["Follow Requests"],
- summary: "Reject Follow",
+ tags: ["Follow requests"],
+ summary: "Reject follow request",
operationId: "FollowRequestController.reject",
parameters: [id_param()],
security: [%{"oAuth" => ["follow", "write:follows"]}],
diff --git a/lib/pleroma/web/api_spec/operations/instance_operation.ex b/lib/pleroma/web/api_spec/operations/instance_operation.ex
index bf39ae643..9384acc32 100644
--- a/lib/pleroma/web/api_spec/operations/instance_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/instance_operation.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.InstanceOperation do
@@ -14,7 +14,7 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
def show_operation do
%Operation{
tags: ["Instance"],
- summary: "Fetch instance",
+ summary: "Retrieve instance information",
description: "Information about the server",
operationId: "InstanceController.show",
responses: %{
@@ -26,7 +26,7 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
def peers_operation do
%Operation{
tags: ["Instance"],
- summary: "List of known hosts",
+ summary: "Retrieve list of known instances",
operationId: "InstanceController.peers",
responses: %{
200 => Operation.response("Array of domains", "application/json", array_of_domains())
diff --git a/lib/pleroma/web/api_spec/operations/list_operation.ex b/lib/pleroma/web/api_spec/operations/list_operation.ex
index f6e73968a..8a6e92b99 100644
--- a/lib/pleroma/web/api_spec/operations/list_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/list_operation.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.ListOperation do
@@ -20,7 +20,7 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do
def index_operation do
%Operation{
tags: ["Lists"],
- summary: "Show user's lists",
+ summary: "Retrieve a list of lists",
description: "Fetch all lists that the user owns",
security: [%{"oAuth" => ["read:lists"]}],
operationId: "ListController.index",
@@ -33,7 +33,7 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do
def create_operation do
%Operation{
tags: ["Lists"],
- summary: "Create a list",
+ summary: "Create a list",
description: "Fetch the list with the given ID. Used for verifying the title of a list.",
operationId: "ListController.create",
requestBody: create_update_request(),
@@ -49,7 +49,7 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do
def show_operation do
%Operation{
tags: ["Lists"],
- summary: "Show a single list",
+ summary: "Retrieve a list",
description: "Fetch the list with the given ID. Used for verifying the title of a list.",
operationId: "ListController.show",
parameters: [id_param()],
@@ -93,7 +93,7 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do
def list_accounts_operation do
%Operation{
tags: ["Lists"],
- summary: "View accounts in list",
+ summary: "Retrieve accounts in list",
operationId: "ListController.list_accounts",
parameters: [id_param()],
security: [%{"oAuth" => ["read:lists"]}],
diff --git a/lib/pleroma/web/api_spec/operations/marker_operation.ex b/lib/pleroma/web/api_spec/operations/marker_operation.ex
index 714ef1f99..c5ff5984b 100644
--- a/lib/pleroma/web/api_spec/operations/marker_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/marker_operation.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.MarkerOperation do
diff --git a/lib/pleroma/web/api_spec/operations/media_operation.ex b/lib/pleroma/web/api_spec/operations/media_operation.ex
index d9c3c42db..85aa14869 100644
--- a/lib/pleroma/web/api_spec/operations/media_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/media_operation.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.MediaOperation do
@@ -16,7 +16,7 @@ defmodule Pleroma.Web.ApiSpec.MediaOperation do
def create_operation do
%Operation{
- tags: ["media"],
+ tags: ["Media attachments"],
summary: "Upload media as attachment",
description: "Creates an attachment to be used with a new status.",
operationId: "MediaController.create",
@@ -56,8 +56,8 @@ defmodule Pleroma.Web.ApiSpec.MediaOperation do
def update_operation do
%Operation{
- tags: ["media"],
- summary: "Upload media as attachment",
+ tags: ["Media attachments"],
+ summary: "Update attachment",
description: "Creates an attachment to be used with a new status.",
operationId: "MediaController.update",
security: [%{"oAuth" => ["write:media"]}],
@@ -97,8 +97,8 @@ defmodule Pleroma.Web.ApiSpec.MediaOperation do
def show_operation do
%Operation{
- tags: ["media"],
- summary: "Show Uploaded media attachment",
+ tags: ["Media attachments"],
+ summary: "Attachment",
operationId: "MediaController.show",
parameters: [id_param()],
security: [%{"oAuth" => ["read:media"]}],
@@ -112,8 +112,8 @@ defmodule Pleroma.Web.ApiSpec.MediaOperation do
def create2_operation do
%Operation{
- tags: ["media"],
- summary: "Upload media as attachment",
+ tags: ["Media attachments"],
+ summary: "Upload media as attachment (v2)",
description: "Creates an attachment to be used with a new status.",
operationId: "MediaController.create2",
security: [%{"oAuth" => ["write:media"]}],
diff --git a/lib/pleroma/web/api_spec/operations/notification_operation.ex b/lib/pleroma/web/api_spec/operations/notification_operation.ex
index f09be64cb..ec88eabe1 100644
--- a/lib/pleroma/web/api_spec/operations/notification_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/notification_operation.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.NotificationOperation do
@@ -22,7 +22,7 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
def index_operation do
%Operation{
tags: ["Notifications"],
- summary: "Get all notifications",
+ summary: "Retrieve a list of notifications",
description:
"Notifications concerning the user. This API returns Link headers containing links to the next/previous page. However, the links can also be constructed dynamically using query params and `id` values.",
operationId: "NotificationController.index",
@@ -74,7 +74,7 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
def show_operation do
%Operation{
tags: ["Notifications"],
- summary: "Get a single notification",
+ summary: "Retrieve a notification",
description: "View information about a notification with a given ID.",
operationId: "NotificationController.show",
security: [%{"oAuth" => ["read:notifications"]}],
@@ -99,7 +99,7 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
def dismiss_operation do
%Operation{
tags: ["Notifications"],
- summary: "Dismiss a single notification",
+ summary: "Dismiss a notification",
description: "Clear a single notification from the server.",
operationId: "NotificationController.dismiss",
parameters: [id_param()],
@@ -193,6 +193,7 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
"mention",
"pleroma:emoji_reaction",
"pleroma:chat_mention",
+ "pleroma:report",
"move",
"follow_request"
],
@@ -206,6 +207,8 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
- `poll` - A poll you have voted in or created has ended
- `move` - Someone moved their account
- `pleroma:emoji_reaction` - Someone reacted with emoji to your status
+ - `pleroma:chat_mention` - Someone mentioned you in a chat message
+ - `pleroma:report` - Someone was reported
"""
}
end
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex
index 97836b2eb..ad49f6426 100644
--- a/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do
@@ -18,8 +18,9 @@ defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do
def confirmation_resend_operation do
%Operation{
- tags: ["Accounts"],
- summary: "Resend confirmation email. Expects `email` or `nickname`",
+ tags: ["Account credentials"],
+ summary: "Resend confirmation email",
+ description: "Expects `email` or `nickname`.",
operationId: "PleromaAPI.AccountController.confirmation_resend",
parameters: [
Operation.parameter(:email, :query, :string, "Email of that needs to be verified",
@@ -41,8 +42,10 @@ defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do
def favourites_operation do
%Operation{
- tags: ["Accounts"],
- summary: "Returns favorites timeline of any user",
+ tags: ["Retrieve account information"],
+ summary: "Favorites",
+ description:
+ "Only returns data if the user has opted into sharing it. See `hide_favorites` in [Update account credentials](#operation/AccountController.update_credentials).",
operationId: "PleromaAPI.AccountController.favourites",
parameters: [id_param() | pagination_params()],
security: [%{"oAuth" => ["read:favourites"]}],
@@ -61,8 +64,9 @@ defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do
def subscribe_operation do
%Operation{
- tags: ["Accounts"],
- summary: "Subscribe to receive notifications for all statuses posted by a user",
+ tags: ["Account actions"],
+ summary: "Subscribe",
+ description: "Receive notifications for all statuses posted by the account.",
operationId: "PleromaAPI.AccountController.subscribe",
parameters: [id_param()],
security: [%{"oAuth" => ["follow", "write:follows"]}],
@@ -75,8 +79,9 @@ defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do
def unsubscribe_operation do
%Operation{
- tags: ["Accounts"],
- summary: "Unsubscribe to stop receiving notifications from user statuses",
+ tags: ["Account actions"],
+ summary: "Unsubscribe",
+ description: "Stop receiving notifications for all statuses posted by the account.",
operationId: "PleromaAPI.AccountController.unsubscribe",
parameters: [id_param()],
security: [%{"oAuth" => ["follow", "write:follows"]}],
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex
new file mode 100644
index 000000000..c78e9780f
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex
@@ -0,0 +1,79 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.PleromaBackupOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def index_operation do
+ %Operation{
+ tags: ["Backups"],
+ summary: "List backups",
+ security: [%{"oAuth" => ["read:account"]}],
+ operationId: "PleromaAPI.BackupController.index",
+ responses: %{
+ 200 =>
+ Operation.response(
+ "An array of backups",
+ "application/json",
+ %Schema{
+ type: :array,
+ items: backup()
+ }
+ ),
+ 400 => Operation.response("Bad Request", "application/json", ApiError)
+ }
+ }
+ end
+
+ def create_operation do
+ %Operation{
+ tags: ["Backups"],
+ summary: "Create a backup",
+ security: [%{"oAuth" => ["read:account"]}],
+ operationId: "PleromaAPI.BackupController.create",
+ responses: %{
+ 200 =>
+ Operation.response(
+ "An array of backups",
+ "application/json",
+ %Schema{
+ type: :array,
+ items: backup()
+ }
+ ),
+ 400 => Operation.response("Bad Request", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp backup do
+ %Schema{
+ title: "Backup",
+ description: "Response schema for a backup",
+ type: :object,
+ properties: %{
+ inserted_at: %Schema{type: :string, format: :"date-time"},
+ content_type: %Schema{type: :string},
+ file_name: %Schema{type: :string},
+ file_size: %Schema{type: :integer},
+ processed: %Schema{type: :boolean}
+ },
+ example: %{
+ "content_type" => "application/zip",
+ "file_name" =>
+ "https://cofe.fe:4000/media/backups/archive-foobar-20200908T164207-Yr7vuT5Wycv-sN3kSN2iJ0k-9pMo60j9qmvRCdDqIew.zip",
+ "file_size" => 4105,
+ "inserted_at" => "2020-09-08T16:42:07.000Z",
+ "processed" => true
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_conversation_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_conversation_operation.ex
index e885eab20..12fb8ed36 100644
--- a/lib/pleroma/web/api_spec/operations/pleroma_conversation_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/pleroma_conversation_operation.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.PleromaConversationOperation do
@@ -19,7 +19,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaConversationOperation do
def show_operation do
%Operation{
tags: ["Conversations"],
- summary: "The conversation with the given ID",
+ summary: "Conversation",
parameters: [
Operation.parameter(:id, :path, :string, "Conversation ID",
example: "123",
@@ -37,7 +37,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaConversationOperation do
def statuses_operation do
%Operation{
tags: ["Conversations"],
- summary: "Timeline for a given conversation",
+ summary: "Timeline for conversation",
parameters: [
Operation.parameter(:id, :path, :string, "Conversation ID",
example: "123",
@@ -61,7 +61,8 @@ defmodule Pleroma.Web.ApiSpec.PleromaConversationOperation do
def update_operation do
%Operation{
tags: ["Conversations"],
- summary: "Update a conversation. Used to change the set of recipients.",
+ summary: "Update conversation",
+ description: "Change set of recipients for the conversation.",
parameters: [
Operation.parameter(:id, :path, :string, "Conversation ID",
example: "123",
@@ -86,7 +87,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaConversationOperation do
def mark_as_read_operation do
%Operation{
tags: ["Conversations"],
- summary: "Marks all user's conversations as read",
+ summary: "Marks all conversations as read",
security: [%{"oAuth" => ["write:conversations"]}],
operationId: "PleromaAPI.ConversationController.mark_as_read",
responses: %{
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_emoji_file_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_emoji_file_operation.ex
index a56641426..8c76096b5 100644
--- a/lib/pleroma/web/api_spec/operations/pleroma_emoji_file_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/pleroma_emoji_file_operation.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.PleromaEmojiFileOperation do
@@ -16,10 +16,10 @@ defmodule Pleroma.Web.ApiSpec.PleromaEmojiFileOperation do
def create_operation do
%Operation{
- tags: ["Emoji Packs"],
+ tags: ["Emoji pack administration"],
summary: "Add new file to the pack",
operationId: "PleromaAPI.EmojiPackController.add_file",
- security: [%{"oAuth" => ["write"]}],
+ security: [%{"oAuth" => ["admin:write"]}],
requestBody: request_body("Parameters", create_request(), required: true),
parameters: [name_param()],
responses: %{
@@ -27,7 +27,8 @@ defmodule Pleroma.Web.ApiSpec.PleromaEmojiFileOperation do
422 => Operation.response("Unprocessable Entity", "application/json", ApiError),
404 => Operation.response("Not Found", "application/json", ApiError),
400 => Operation.response("Bad Request", "application/json", ApiError),
- 409 => Operation.response("Conflict", "application/json", ApiError)
+ 409 => Operation.response("Conflict", "application/json", ApiError),
+ 500 => Operation.response("Error", "application/json", ApiError)
}
}
end
@@ -61,10 +62,10 @@ defmodule Pleroma.Web.ApiSpec.PleromaEmojiFileOperation do
def update_operation do
%Operation{
- tags: ["Emoji Packs"],
+ tags: ["Emoji pack administration"],
summary: "Add new file to the pack",
operationId: "PleromaAPI.EmojiPackController.update_file",
- security: [%{"oAuth" => ["write"]}],
+ security: [%{"oAuth" => ["admin:write"]}],
requestBody: request_body("Parameters", update_request(), required: true),
parameters: [name_param()],
responses: %{
@@ -105,10 +106,10 @@ defmodule Pleroma.Web.ApiSpec.PleromaEmojiFileOperation do
def delete_operation do
%Operation{
- tags: ["Emoji Packs"],
+ tags: ["Emoji pack administration"],
summary: "Delete emoji file from pack",
operationId: "PleromaAPI.EmojiPackController.delete_file",
- security: [%{"oAuth" => ["write"]}],
+ security: [%{"oAuth" => ["admin:write"]}],
parameters: [
name_param(),
Operation.parameter(:shortcode, :query, :string, "File shortcode",
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex
index 79f52dcb3..49247d9b6 100644
--- a/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.PleromaEmojiPackOperation do
@@ -16,9 +16,9 @@ defmodule Pleroma.Web.ApiSpec.PleromaEmojiPackOperation do
def remote_operation do
%Operation{
- tags: ["Emoji Packs"],
+ tags: ["Emoji pack administration"],
summary: "Make request to another instance for emoji packs list",
- security: [%{"oAuth" => ["write"]}],
+ security: [%{"oAuth" => ["admin:write"]}],
parameters: [
url_param(),
Operation.parameter(
@@ -44,7 +44,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaEmojiPackOperation do
def index_operation do
%Operation{
- tags: ["Emoji Packs"],
+ tags: ["Emoji packs"],
summary: "Lists local custom emoji packs",
operationId: "PleromaAPI.EmojiPackController.index",
parameters: [
@@ -69,7 +69,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaEmojiPackOperation do
def show_operation do
%Operation{
- tags: ["Emoji Packs"],
+ tags: ["Emoji packs"],
summary: "Show emoji pack",
operationId: "PleromaAPI.EmojiPackController.show",
parameters: [
@@ -97,7 +97,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaEmojiPackOperation do
def archive_operation do
%Operation{
- tags: ["Emoji Packs"],
+ tags: ["Emoji packs"],
summary: "Requests a local pack archive from the instance",
operationId: "PleromaAPI.EmojiPackController.archive",
parameters: [name_param()],
@@ -115,10 +115,10 @@ defmodule Pleroma.Web.ApiSpec.PleromaEmojiPackOperation do
def download_operation do
%Operation{
- tags: ["Emoji Packs"],
+ tags: ["Emoji pack administration"],
summary: "Download pack from another instance",
operationId: "PleromaAPI.EmojiPackController.download",
- security: [%{"oAuth" => ["write"]}],
+ security: [%{"oAuth" => ["admin:write"]}],
requestBody: request_body("Parameters", download_request(), required: true),
responses: %{
200 => ok_response(),
@@ -145,10 +145,10 @@ defmodule Pleroma.Web.ApiSpec.PleromaEmojiPackOperation do
def create_operation do
%Operation{
- tags: ["Emoji Packs"],
+ tags: ["Emoji pack administration"],
summary: "Create an empty pack",
operationId: "PleromaAPI.EmojiPackController.create",
- security: [%{"oAuth" => ["write"]}],
+ security: [%{"oAuth" => ["admin:write"]}],
parameters: [name_param()],
responses: %{
200 => ok_response(),
@@ -161,40 +161,42 @@ defmodule Pleroma.Web.ApiSpec.PleromaEmojiPackOperation do
def delete_operation do
%Operation{
- tags: ["Emoji Packs"],
+ tags: ["Emoji pack administration"],
summary: "Delete a custom emoji pack",
operationId: "PleromaAPI.EmojiPackController.delete",
- security: [%{"oAuth" => ["write"]}],
+ security: [%{"oAuth" => ["admin:write"]}],
parameters: [name_param()],
responses: %{
200 => ok_response(),
400 => Operation.response("Bad Request", "application/json", ApiError),
- 404 => Operation.response("Not Found", "application/json", ApiError)
+ 404 => Operation.response("Not Found", "application/json", ApiError),
+ 500 => Operation.response("Error", "application/json", ApiError)
}
}
end
def update_operation do
%Operation{
- tags: ["Emoji Packs"],
+ tags: ["Emoji pack administration"],
summary: "Updates (replaces) pack metadata",
operationId: "PleromaAPI.EmojiPackController.update",
- security: [%{"oAuth" => ["write"]}],
+ security: [%{"oAuth" => ["admin:write"]}],
requestBody: request_body("Parameters", update_request(), required: true),
parameters: [name_param()],
responses: %{
200 => Operation.response("Metadata", "application/json", metadata()),
- 400 => Operation.response("Bad Request", "application/json", ApiError)
+ 400 => Operation.response("Bad Request", "application/json", ApiError),
+ 500 => Operation.response("Error", "application/json", ApiError)
}
}
end
def import_from_filesystem_operation do
%Operation{
- tags: ["Emoji Packs"],
+ tags: ["Emoji pack administration"],
summary: "Imports packs from filesystem",
operationId: "PleromaAPI.EmojiPackController.import",
- security: [%{"oAuth" => ["write"]}],
+ security: [%{"oAuth" => ["admin:write"]}],
responses: %{
200 =>
Operation.response("Array of imported pack names", "application/json", %Schema{
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_instances_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_instances_operation.ex
new file mode 100644
index 000000000..612113147
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/pleroma_instances_operation.ex
@@ -0,0 +1,40 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.PleromaInstancesOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def show_operation do
+ %Operation{
+ tags: ["Instance"],
+ summary: "Retrieve federation status",
+ description: "Information about instances deemed unreachable by the server",
+ operationId: "PleromaInstances.show",
+ responses: %{
+ 200 => Operation.response("PleromaInstances", "application/json", pleroma_instances())
+ }
+ }
+ end
+
+ def pleroma_instances do
+ %Schema{
+ type: :object,
+ properties: %{
+ unreachable: %Schema{
+ type: :object,
+ properties: %{hostname: %Schema{type: :string, format: :"date-time"}}
+ }
+ },
+ example: %{
+ "unreachable" => %{"consistently-unreachable.name" => "2020-10-14 22:07:58.216473"}
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_mascot_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_mascot_operation.ex
index 8c5f37ea6..6191cb97d 100644
--- a/lib/pleroma/web/api_spec/operations/pleroma_mascot_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/pleroma_mascot_operation.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.PleromaMascotOperation do
@@ -17,7 +17,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaMascotOperation do
def show_operation do
%Operation{
tags: ["Mascot"],
- summary: "Gets user mascot image",
+ summary: "Retrieve mascot",
security: [%{"oAuth" => ["read:accounts"]}],
operationId: "PleromaAPI.MascotController.show",
responses: %{
@@ -29,7 +29,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaMascotOperation do
def update_operation do
%Operation{
tags: ["Mascot"],
- summary: "Set/clear user avatar image",
+ summary: "Set or clear mascot",
description:
"Behaves exactly the same as `POST /api/v1/upload`. Can only accept images - any attempt to upload non-image files will be met with `HTTP 415 Unsupported Media Type`.",
operationId: "PleromaAPI.MascotController.update",
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 b0c8db863..1dda39240 100644
--- a/lib/pleroma/web/api_spec/operations/pleroma_notification_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/pleroma_notification_operation.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.PleromaNotificationOperation do
@@ -18,7 +18,8 @@ defmodule Pleroma.Web.ApiSpec.PleromaNotificationOperation do
def mark_as_read_operation do
%Operation{
tags: ["Notifications"],
- summary: "Mark notifications as read. Query parameters are mutually exclusive.",
+ summary: "Mark notifications as read",
+ description: "Query parameters are mutually exclusive.",
requestBody:
request_body("Parameters", %Schema{
type: :object,
@@ -32,7 +33,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaNotificationOperation do
responses: %{
200 =>
Operation.response(
- "A Notification or array of Motifications",
+ "A Notification or array of Notifications",
"application/json",
%Schema{
anyOf: [
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_report_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_report_operation.ex
new file mode 100644
index 000000000..ee8870dc2
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/pleroma_report_operation.ex
@@ -0,0 +1,97 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.PleromaReportOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Admin.ReportOperation
+ alias Pleroma.Web.ApiSpec.Schemas.Account
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+ alias Pleroma.Web.ApiSpec.Schemas.FlakeID
+ alias Pleroma.Web.ApiSpec.Schemas.Status
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def index_operation do
+ %Operation{
+ tags: ["Reports"],
+ summary: "Get a list of your own reports",
+ operationId: "PleromaAPI.ReportController.index",
+ security: [%{"oAuth" => ["read:reports"]}],
+ parameters: [
+ Operation.parameter(
+ :state,
+ :query,
+ ReportOperation.report_state(),
+ "Filter by report state"
+ ),
+ Operation.parameter(
+ :limit,
+ :query,
+ %Schema{type: :integer},
+ "The number of records to retrieve"
+ ),
+ Operation.parameter(
+ :page,
+ :query,
+ %Schema{type: :integer, default: 1},
+ "Page number"
+ ),
+ Operation.parameter(
+ :page_size,
+ :query,
+ %Schema{type: :integer, default: 50},
+ "Number number of log entries per page"
+ )
+ ],
+ responses: %{
+ 200 =>
+ Operation.response("Response", "application/json", %Schema{
+ type: :object,
+ properties: %{
+ total: %Schema{type: :integer},
+ reports: %Schema{
+ type: :array,
+ items: report()
+ }
+ }
+ }),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def show_operation do
+ %Operation{
+ tags: ["Reports"],
+ summary: "Get an individual report",
+ operationId: "PleromaAPI.ReportController.show",
+ parameters: [ReportOperation.id_param()],
+ security: [%{"oAuth" => ["read:reports"]}],
+ responses: %{
+ 200 => Operation.response("Report", "application/json", report()),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ # Copied from ReportOperation.report with removing notes
+ defp report do
+ %Schema{
+ type: :object,
+ properties: %{
+ id: FlakeID,
+ state: ReportOperation.report_state(),
+ account: Account,
+ actor: Account,
+ content: %Schema{type: :string},
+ created_at: %Schema{type: :string, format: :"date-time"},
+ statuses: %Schema{type: :array, items: Status}
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex
index 85a22aa0b..6a909fc85 100644
--- a/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do
diff --git a/lib/pleroma/web/api_spec/operations/poll_operation.ex b/lib/pleroma/web/api_spec/operations/poll_operation.ex
index e15c7dc95..0d1c8d099 100644
--- a/lib/pleroma/web/api_spec/operations/poll_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/poll_operation.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.PollOperation do
diff --git a/lib/pleroma/web/api_spec/operations/report_operation.ex b/lib/pleroma/web/api_spec/operations/report_operation.ex
index b9b4c4f79..b744efa60 100644
--- a/lib/pleroma/web/api_spec/operations/report_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/report_operation.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.ReportOperation do
@@ -16,7 +16,7 @@ defmodule Pleroma.Web.ApiSpec.ReportOperation do
def create_operation do
%Operation{
- tags: ["reports"],
+ tags: ["Reports"],
summary: "File a report",
description: "Report problematic users to your moderators",
operationId: "ReportController.create",
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 fe675a923..b9c5b35c1 100644
--- a/lib/pleroma/web/api_spec/operations/scheduled_activity_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/scheduled_activity_operation.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.ScheduledActivityOperation do
@@ -18,7 +18,7 @@ defmodule Pleroma.Web.ApiSpec.ScheduledActivityOperation do
def index_operation do
%Operation{
- tags: ["Scheduled Statuses"],
+ tags: ["Scheduled statuses"],
summary: "View scheduled statuses",
security: [%{"oAuth" => ["read:statuses"]}],
parameters: pagination_params(),
@@ -35,7 +35,7 @@ defmodule Pleroma.Web.ApiSpec.ScheduledActivityOperation do
def show_operation do
%Operation{
- tags: ["Scheduled Statuses"],
+ tags: ["Scheduled statuses"],
summary: "View a single scheduled status",
security: [%{"oAuth" => ["read:statuses"]}],
parameters: [id_param()],
@@ -49,7 +49,7 @@ defmodule Pleroma.Web.ApiSpec.ScheduledActivityOperation do
def update_operation do
%Operation{
- tags: ["Scheduled Statuses"],
+ tags: ["Scheduled statuses"],
summary: "Schedule a status",
operationId: "ScheduledActivity.update",
security: [%{"oAuth" => ["write:statuses"]}],
@@ -75,7 +75,7 @@ defmodule Pleroma.Web.ApiSpec.ScheduledActivityOperation do
def delete_operation do
%Operation{
- tags: ["Scheduled Statuses"],
+ tags: ["Scheduled statuses"],
summary: "Cancel a scheduled status",
security: [%{"oAuth" => ["write:statuses"]}],
parameters: [id_param()],
diff --git a/lib/pleroma/web/api_spec/operations/search_operation.ex b/lib/pleroma/web/api_spec/operations/search_operation.ex
index 169c36d87..ff4fd0027 100644
--- a/lib/pleroma/web/api_spec/operations/search_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/search_operation.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.SearchOperation do
diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex
index d7ebde6f6..40edc747d 100644
--- a/lib/pleroma/web/api_spec/operations/status_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/status_operation.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.StatusOperation do
@@ -22,8 +22,8 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
def index_operation do
%Operation{
- tags: ["Statuses"],
- summary: "Get multiple statuses by IDs",
+ tags: ["Retrieve status information"],
+ summary: "Multiple statuses",
security: [%{"oAuth" => ["read:statuses"]}],
parameters: [
Operation.parameter(
@@ -31,6 +31,12 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
:query,
%Schema{type: :array, items: FlakeID},
"Array of status IDs"
+ ),
+ Operation.parameter(
+ :with_muted,
+ :query,
+ BooleanLike,
+ "Include reactions from muted acccounts."
)
],
operationId: "StatusController.index",
@@ -42,7 +48,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
def create_operation do
%Operation{
- tags: ["Statuses"],
+ tags: ["Status actions"],
summary: "Publish new status",
security: [%{"oAuth" => ["write:statuses"]}],
description: "Post a new status",
@@ -62,12 +68,20 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
def show_operation do
%Operation{
- tags: ["Statuses"],
- summary: "View specific status",
+ tags: ["Retrieve status information"],
+ summary: "Status",
description: "View information about a status",
operationId: "StatusController.show",
security: [%{"oAuth" => ["read:statuses"]}],
- parameters: [id_param()],
+ parameters: [
+ id_param(),
+ Operation.parameter(
+ :with_muted,
+ :query,
+ BooleanLike,
+ "Include reactions from muted acccounts."
+ )
+ ],
responses: %{
200 => status_response(),
404 => Operation.response("Not Found", "application/json", ApiError)
@@ -77,8 +91,8 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
def delete_operation do
%Operation{
- tags: ["Statuses"],
- summary: "Delete status",
+ tags: ["Status actions"],
+ summary: "Delete",
security: [%{"oAuth" => ["write:statuses"]}],
description: "Delete one of your own statuses",
operationId: "StatusController.delete",
@@ -93,8 +107,8 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
def reblog_operation do
%Operation{
- tags: ["Statuses"],
- summary: "Boost",
+ tags: ["Status actions"],
+ summary: "Reblog",
security: [%{"oAuth" => ["write:statuses"]}],
description: "Share a status",
operationId: "StatusController.reblog",
@@ -103,7 +117,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
request_body("Parameters", %Schema{
type: :object,
properties: %{
- visibility: %Schema{allOf: [VisibilityScope], default: "public"}
+ visibility: %Schema{allOf: [VisibilityScope]}
}
}),
responses: %{
@@ -115,8 +129,8 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
def unreblog_operation do
%Operation{
- tags: ["Statuses"],
- summary: "Undo boost",
+ tags: ["Status actions"],
+ summary: "Undo reblog",
security: [%{"oAuth" => ["write:statuses"]}],
description: "Undo a reshare of a status",
operationId: "StatusController.unreblog",
@@ -130,7 +144,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
def favourite_operation do
%Operation{
- tags: ["Statuses"],
+ tags: ["Status actions"],
summary: "Favourite",
security: [%{"oAuth" => ["write:favourites"]}],
description: "Add a status to your favourites list",
@@ -145,7 +159,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
def unfavourite_operation do
%Operation{
- tags: ["Statuses"],
+ tags: ["Status actions"],
summary: "Undo favourite",
security: [%{"oAuth" => ["write:favourites"]}],
description: "Remove a status from your favourites list",
@@ -160,7 +174,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
def pin_operation do
%Operation{
- tags: ["Statuses"],
+ tags: ["Status actions"],
summary: "Pin to profile",
security: [%{"oAuth" => ["write:accounts"]}],
description: "Feature one of your own public statuses at the top of your profile",
@@ -175,8 +189,8 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
def unpin_operation do
%Operation{
- tags: ["Statuses"],
- summary: "Unpin to profile",
+ tags: ["Status actions"],
+ summary: "Unpin from profile",
security: [%{"oAuth" => ["write:accounts"]}],
description: "Unfeature a status from the top of your profile",
operationId: "StatusController.unpin",
@@ -190,7 +204,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
def bookmark_operation do
%Operation{
- tags: ["Statuses"],
+ tags: ["Status actions"],
summary: "Bookmark",
security: [%{"oAuth" => ["write:bookmarks"]}],
description: "Privately bookmark a status",
@@ -204,7 +218,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
def unbookmark_operation do
%Operation{
- tags: ["Statuses"],
+ tags: ["Status actions"],
summary: "Undo bookmark",
security: [%{"oAuth" => ["write:bookmarks"]}],
description: "Remove a status from your private bookmarks",
@@ -218,12 +232,32 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
def mute_conversation_operation do
%Operation{
- tags: ["Statuses"],
+ tags: ["Status actions"],
summary: "Mute conversation",
security: [%{"oAuth" => ["write:mutes"]}],
description: "Do not receive notifications for the thread that this status is part of.",
operationId: "StatusController.mute_conversation",
- parameters: [id_param()],
+ requestBody:
+ request_body("Parameters", %Schema{
+ type: :object,
+ properties: %{
+ expires_in: %Schema{
+ type: :integer,
+ nullable: true,
+ description: "Expire the mute in `expires_in` seconds. Default 0 for infinity",
+ default: 0
+ }
+ }
+ }),
+ parameters: [
+ id_param(),
+ Operation.parameter(
+ :expires_in,
+ :query,
+ %Schema{type: :integer, default: 0},
+ "Expire the mute in `expires_in` seconds. Default 0 for infinity"
+ )
+ ],
responses: %{
200 => status_response(),
400 => Operation.response("Error", "application/json", ApiError)
@@ -233,7 +267,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
def unmute_conversation_operation do
%Operation{
- tags: ["Statuses"],
+ tags: ["Status actions"],
summary: "Unmute conversation",
security: [%{"oAuth" => ["write:mutes"]}],
description:
@@ -249,7 +283,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
def card_operation do
%Operation{
- tags: ["Statuses"],
+ tags: ["Retrieve status information"],
deprecated: true,
summary: "Preview card",
description: "Deprecated in favor of card property inlined on Status entity",
@@ -277,7 +311,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
def favourited_by_operation do
%Operation{
- tags: ["Statuses"],
+ tags: ["Retrieve status information"],
summary: "Favourited by",
description: "View who favourited a given status",
operationId: "StatusController.favourited_by",
@@ -297,9 +331,9 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
def reblogged_by_operation do
%Operation{
- tags: ["Statuses"],
- summary: "Boosted by",
- description: "View who boosted a given status",
+ tags: ["Retrieve status information"],
+ summary: "Reblogged by",
+ description: "View who reblogged a given status",
operationId: "StatusController.reblogged_by",
security: [%{"oAuth" => ["read:accounts"]}],
parameters: [id_param()],
@@ -317,7 +351,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
def context_operation do
%Operation{
- tags: ["Statuses"],
+ tags: ["Retrieve status information"],
summary: "Parent and child statuses",
description: "View statuses above and below this status in the thread",
operationId: "StatusController.context",
@@ -331,7 +365,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
def favourites_operation do
%Operation{
- tags: ["Statuses"],
+ tags: ["Timelines"],
summary: "Favourited statuses",
description:
"Statuses the user has favourited. Please note that you have to use the link headers to paginate this. You can not build the query parameters yourself.",
@@ -346,7 +380,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
def bookmarks_operation do
%Operation{
- tags: ["Statuses"],
+ tags: ["Timelines"],
summary: "Bookmarked statuses",
description: "Statuses the user has bookmarked",
operationId: "StatusController.bookmarks",
@@ -379,34 +413,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
items: %Schema{type: :string},
description: "Array of Attachment ids to be attached as media."
},
- poll: %Schema{
- nullable: true,
- type: :object,
- required: [:options],
- properties: %{
- options: %Schema{
- type: :array,
- items: %Schema{type: :string},
- description: "Array of possible answers. Must be provided with `poll[expires_in]`."
- },
- expires_in: %Schema{
- type: :integer,
- nullable: true,
- description:
- "Duration the poll should be open, in seconds. Must be provided with `poll[options]`"
- },
- multiple: %Schema{
- allOf: [BooleanLike],
- nullable: true,
- description: "Allow multiple choices?"
- },
- hide_totals: %Schema{
- allOf: [BooleanLike],
- nullable: true,
- description: "Hide vote counts until the poll ends?"
- }
- }
- },
+ poll: poll_params(),
in_reply_to_id: %Schema{
nullable: true,
allOf: [FlakeID],
@@ -488,6 +495,37 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
}
end
+ def poll_params do
+ %Schema{
+ nullable: true,
+ type: :object,
+ required: [:options, :expires_in],
+ properties: %{
+ options: %Schema{
+ type: :array,
+ items: %Schema{type: :string},
+ description: "Array of possible answers. Must be provided with `poll[expires_in]`."
+ },
+ expires_in: %Schema{
+ type: :integer,
+ nullable: true,
+ description:
+ "Duration the poll should be open, in seconds. Must be provided with `poll[options]`"
+ },
+ multiple: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Allow multiple choices?"
+ },
+ hide_totals: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Hide vote counts until the poll ends?"
+ }
+ }
+ }
+ end
+
def id_param do
Operation.parameter(:id, :path, FlakeID, "Status ID",
example: "9umDrYheeY451cQnEe",
diff --git a/lib/pleroma/web/api_spec/operations/subscription_operation.ex b/lib/pleroma/web/api_spec/operations/subscription_operation.ex
index 775dd795d..60a7fb3b0 100644
--- a/lib/pleroma/web/api_spec/operations/subscription_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/subscription_operation.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.SubscriptionOperation do
@@ -17,7 +17,7 @@ defmodule Pleroma.Web.ApiSpec.SubscriptionOperation do
def create_operation do
%Operation{
- tags: ["Push Subscriptions"],
+ tags: ["Push subscriptions"],
summary: "Subscribe to push notifications",
description:
"Add a Web Push API subscription to receive notifications. Each access token can have one push subscription. If you create a new subscription, the old subscription is deleted.",
@@ -25,7 +25,7 @@ defmodule Pleroma.Web.ApiSpec.SubscriptionOperation do
security: [%{"oAuth" => ["push"]}],
requestBody: Helpers.request_body("Parameters", create_request(), required: true),
responses: %{
- 200 => Operation.response("Push Subscription", "application/json", PushSubscription),
+ 200 => Operation.response("Push subscription", "application/json", PushSubscription),
400 => Operation.response("Error", "application/json", ApiError),
403 => Operation.response("Error", "application/json", ApiError)
}
@@ -34,13 +34,13 @@ defmodule Pleroma.Web.ApiSpec.SubscriptionOperation do
def show_operation do
%Operation{
- tags: ["Push Subscriptions"],
+ tags: ["Push subscriptions"],
summary: "Get current subscription",
description: "View the PushSubscription currently associated with this access token.",
operationId: "SubscriptionController.show",
security: [%{"oAuth" => ["push"]}],
responses: %{
- 200 => Operation.response("Push Subscription", "application/json", PushSubscription),
+ 200 => Operation.response("Push subscription", "application/json", PushSubscription),
403 => Operation.response("Error", "application/json", ApiError),
404 => Operation.response("Error", "application/json", ApiError)
}
@@ -49,7 +49,7 @@ defmodule Pleroma.Web.ApiSpec.SubscriptionOperation do
def update_operation do
%Operation{
- tags: ["Push Subscriptions"],
+ tags: ["Push subscriptions"],
summary: "Change types of notifications",
description:
"Updates the current push subscription. Only the data part can be updated. To change fundamentals, a new subscription must be created instead.",
@@ -57,7 +57,7 @@ defmodule Pleroma.Web.ApiSpec.SubscriptionOperation do
security: [%{"oAuth" => ["push"]}],
requestBody: Helpers.request_body("Parameters", update_request(), required: true),
responses: %{
- 200 => Operation.response("Push Subscription", "application/json", PushSubscription),
+ 200 => Operation.response("Push subscription", "application/json", PushSubscription),
403 => Operation.response("Error", "application/json", ApiError)
}
}
@@ -65,7 +65,7 @@ defmodule Pleroma.Web.ApiSpec.SubscriptionOperation do
def delete_operation do
%Operation{
- tags: ["Push Subscriptions"],
+ tags: ["Push subscriptions"],
summary: "Remove current subscription",
description: "Removes the current Web Push API subscription.",
operationId: "SubscriptionController.delete",
@@ -146,6 +146,11 @@ defmodule Pleroma.Web.ApiSpec.SubscriptionOperation do
allOf: [BooleanLike],
nullable: true,
description: "Receive chat notifications?"
+ },
+ "pleroma:emoji_reaction": %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Receive emoji reaction notifications?"
}
}
}
@@ -210,6 +215,16 @@ defmodule Pleroma.Web.ApiSpec.SubscriptionOperation do
allOf: [BooleanLike],
nullable: true,
description: "Receive poll notifications?"
+ },
+ "pleroma:chat_mention": %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Receive chat notifications?"
+ },
+ "pleroma:emoji_reaction": %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Receive emoji reaction notifications?"
}
}
}
diff --git a/lib/pleroma/web/api_spec/operations/timeline_operation.ex b/lib/pleroma/web/api_spec/operations/timeline_operation.ex
index 8e19bace7..cae18c758 100644
--- a/lib/pleroma/web/api_spec/operations/timeline_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/timeline_operation.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.TimelineOperation do
@@ -25,6 +25,8 @@ defmodule Pleroma.Web.ApiSpec.TimelineOperation do
security: [%{"oAuth" => ["read:statuses"]}],
parameters: [
local_param(),
+ remote_param(),
+ only_media_param(),
with_muted_param(),
exclude_visibilities_param(),
reply_visibility_param() | pagination_params()
@@ -41,8 +43,7 @@ defmodule Pleroma.Web.ApiSpec.TimelineOperation do
tags: ["Timelines"],
summary: "Direct timeline",
description:
- "View statuses with a “direct” privacy, from your account or in your notifications",
- deprecated: true,
+ "View statuses with a “direct” scope addressed to the account. Using this endpoint is discouraged, please use [conversations](#tag/Conversations) or [chats](#tag/Chats).",
parameters: [with_muted_param() | pagination_params()],
security: [%{"oAuth" => ["read:statuses"]}],
operationId: "TimelineController.direct",
@@ -59,7 +60,9 @@ defmodule Pleroma.Web.ApiSpec.TimelineOperation do
security: [%{"oAuth" => ["read:statuses"]}],
parameters: [
local_param(),
+ instance_param(),
only_media_param(),
+ remote_param(),
with_muted_param(),
exclude_visibilities_param(),
reply_visibility_param() | pagination_params()
@@ -106,6 +109,7 @@ defmodule Pleroma.Web.ApiSpec.TimelineOperation do
),
local_param(),
only_media_param(),
+ remote_param(),
with_muted_param(),
exclude_visibilities_param() | pagination_params()
],
@@ -131,6 +135,9 @@ defmodule Pleroma.Web.ApiSpec.TimelineOperation do
required: true
),
with_muted_param(),
+ local_param(),
+ remote_param(),
+ only_media_param(),
exclude_visibilities_param() | pagination_params()
],
operationId: "TimelineController.list",
@@ -158,8 +165,17 @@ defmodule Pleroma.Web.ApiSpec.TimelineOperation do
)
end
+ defp instance_param do
+ Operation.parameter(
+ :instance,
+ :query,
+ %Schema{type: :string},
+ "Show only statuses from the given domain"
+ )
+ end
+
defp with_muted_param do
- Operation.parameter(:with_muted, :query, BooleanLike, "Includeactivities by muted users")
+ Operation.parameter(:with_muted, :query, BooleanLike, "Include activities by muted users")
end
defp exclude_visibilities_param do
@@ -188,4 +204,13 @@ defmodule Pleroma.Web.ApiSpec.TimelineOperation do
"Show only statuses with media attached?"
)
end
+
+ defp remote_param do
+ Operation.parameter(
+ :remote,
+ :query,
+ %Schema{allOf: [BooleanLike], default: false},
+ "Show only remote statuses?"
+ )
+ end
end
diff --git a/lib/pleroma/web/api_spec/operations/user_import_operation.ex b/lib/pleroma/web/api_spec/operations/user_import_operation.ex
index a50314fb7..6292e2004 100644
--- a/lib/pleroma/web/api_spec/operations/user_import_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/user_import_operation.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.UserImportOperation do
@@ -17,8 +17,8 @@ defmodule Pleroma.Web.ApiSpec.UserImportOperation do
def follow_operation do
%Operation{
- tags: ["follow_import"],
- summary: "Imports your follows.",
+ tags: ["Data import"],
+ summary: "Import follows",
operationId: "UserImportController.follow",
requestBody: request_body("Parameters", import_request(), required: true),
responses: %{
@@ -31,8 +31,8 @@ defmodule Pleroma.Web.ApiSpec.UserImportOperation do
def blocks_operation do
%Operation{
- tags: ["blocks_import"],
- summary: "Imports your blocks.",
+ tags: ["Data import"],
+ summary: "Import blocks",
operationId: "UserImportController.blocks",
requestBody: request_body("Parameters", import_request(), required: true),
responses: %{
@@ -45,8 +45,8 @@ defmodule Pleroma.Web.ApiSpec.UserImportOperation do
def mutes_operation do
%Operation{
- tags: ["mutes_import"],
- summary: "Imports your mutes.",
+ tags: ["Data import"],
+ summary: "Import mutes",
operationId: "UserImportController.mutes",
requestBody: request_body("Parameters", import_request(), required: true),
responses: %{
diff --git a/lib/pleroma/web/api_spec/render_error.ex b/lib/pleroma/web/api_spec/render_error.ex
index d476b8ef3..e501a6be4 100644
--- a/lib/pleroma/web/api_spec/render_error.ex
+++ b/lib/pleroma/web/api_spec/render_error.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.RenderError do
diff --git a/lib/pleroma/web/api_spec/schemas/account.ex b/lib/pleroma/web/api_spec/schemas/account.ex
index ca79f0747..bd7143ab9 100644
--- a/lib/pleroma/web/api_spec/schemas/account.ex
+++ b/lib/pleroma/web/api_spec/schemas/account.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.Account do
@@ -40,13 +40,15 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
pleroma: %Schema{
type: :object,
properties: %{
+ ap_id: %Schema{type: :string},
+ also_known_as: %Schema{type: :array, items: %Schema{type: :string}},
allow_following_move: %Schema{
type: :boolean,
description: "whether the user allows automatically follow moved following accounts"
},
background_image: %Schema{type: :string, nullable: true, format: :uri},
chat_token: %Schema{type: :string},
- confirmation_pending: %Schema{
+ is_confirmed: %Schema{
type: :boolean,
description:
"whether the user account is waiting on email confirmation to be activated"
@@ -94,7 +96,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
hide_notification_contents: %Schema{type: :boolean}
}
},
- relationship: AccountRelationship,
+ relationship: %Schema{allOf: [AccountRelationship], nullable: true},
settings_store: %Schema{
type: :object,
description:
@@ -127,7 +129,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
discoverable: %Schema{
type: :boolean,
description:
- "whether the user allows discovery of the account in search results and other services."
+ "whether the user allows indexing / listing of the account by external services (search engines etc.)."
},
no_rich_text: %Schema{
type: :boolean,
@@ -164,7 +166,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
"pleroma" => %{
"allow_following_move" => true,
"background_image" => nil,
- "confirmation_pending" => true,
+ "is_confirmed" => false,
"hide_favorites" => true,
"hide_followers" => false,
"hide_followers_count" => false,
diff --git a/lib/pleroma/web/api_spec/schemas/account_field.ex b/lib/pleroma/web/api_spec/schemas/account_field.ex
index fa97073a0..7c4f94001 100644
--- a/lib/pleroma/web/api_spec/schemas/account_field.ex
+++ b/lib/pleroma/web/api_spec/schemas/account_field.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.AccountField do
diff --git a/lib/pleroma/web/api_spec/schemas/account_relationship.ex b/lib/pleroma/web/api_spec/schemas/account_relationship.ex
index 8b982669e..16b73ebb4 100644
--- a/lib/pleroma/web/api_spec/schemas/account_relationship.ex
+++ b/lib/pleroma/web/api_spec/schemas/account_relationship.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.AccountRelationship do
@@ -10,7 +10,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.AccountRelationship do
OpenApiSpex.schema(%{
title: "AccountRelationship",
- description: "Response schema for relationship",
+ description: "Relationship between current account and requested account",
type: :object,
properties: %{
blocked_by: %Schema{type: :boolean},
diff --git a/lib/pleroma/web/api_spec/schemas/actor_type.ex b/lib/pleroma/web/api_spec/schemas/actor_type.ex
index ac9b46678..1336640a1 100644
--- a/lib/pleroma/web/api_spec/schemas/actor_type.ex
+++ b/lib/pleroma/web/api_spec/schemas/actor_type.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.ActorType do
diff --git a/lib/pleroma/web/api_spec/schemas/api_error.ex b/lib/pleroma/web/api_spec/schemas/api_error.ex
index 5815df94c..0d6d0b75c 100644
--- a/lib/pleroma/web/api_spec/schemas/api_error.ex
+++ b/lib/pleroma/web/api_spec/schemas/api_error.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.ApiError do
diff --git a/lib/pleroma/web/api_spec/schemas/attachment.ex b/lib/pleroma/web/api_spec/schemas/attachment.ex
index c6edf6d36..ca3659c93 100644
--- a/lib/pleroma/web/api_spec/schemas/attachment.ex
+++ b/lib/pleroma/web/api_spec/schemas/attachment.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.Attachment do
diff --git a/lib/pleroma/web/api_spec/schemas/boolean_like.ex b/lib/pleroma/web/api_spec/schemas/boolean_like.ex
index f3bfb74da..eb001c5bb 100644
--- a/lib/pleroma/web/api_spec/schemas/boolean_like.ex
+++ b/lib/pleroma/web/api_spec/schemas/boolean_like.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.BooleanLike do
diff --git a/lib/pleroma/web/api_spec/schemas/chat.ex b/lib/pleroma/web/api_spec/schemas/chat.ex
index 65f908e33..4afed910d 100644
--- a/lib/pleroma/web/api_spec/schemas/chat.ex
+++ b/lib/pleroma/web/api_spec/schemas/chat.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.Chat do
@@ -23,7 +23,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Chat do
"account" => %{
"pleroma" => %{
"is_admin" => false,
- "confirmation_pending" => false,
+ "is_confirmed" => true,
"hide_followers_count" => false,
"is_moderator" => false,
"hide_favorites" => true,
diff --git a/lib/pleroma/web/api_spec/schemas/chat_message.ex b/lib/pleroma/web/api_spec/schemas/chat_message.ex
index 9d2799618..348fe95f8 100644
--- a/lib/pleroma/web/api_spec/schemas/chat_message.ex
+++ b/lib/pleroma/web/api_spec/schemas/chat_message.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessage do
@@ -52,7 +52,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessage do
title: %Schema{type: :string, description: "Title of linked resource"},
description: %Schema{type: :string, description: "Description of preview"}
}
- }
+ },
+ unread: %Schema{type: :boolean, description: "Whether a message has been marked as read."}
},
example: %{
"account_id" => "someflakeid",
@@ -69,7 +70,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessage do
}
],
"id" => "14",
- "attachment" => nil
+ "attachment" => nil,
+ "unread" => false
}
})
end
diff --git a/lib/pleroma/web/api_spec/schemas/conversation.ex b/lib/pleroma/web/api_spec/schemas/conversation.ex
index d8ff5ba26..7c609965f 100644
--- a/lib/pleroma/web/api_spec/schemas/conversation.ex
+++ b/lib/pleroma/web/api_spec/schemas/conversation.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.Conversation do
diff --git a/lib/pleroma/web/api_spec/schemas/emoji.ex b/lib/pleroma/web/api_spec/schemas/emoji.ex
index 26f35e648..ceb3c7186 100644
--- a/lib/pleroma/web/api_spec/schemas/emoji.ex
+++ b/lib/pleroma/web/api_spec/schemas/emoji.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.Emoji do
diff --git a/lib/pleroma/web/api_spec/schemas/flake_id.ex b/lib/pleroma/web/api_spec/schemas/flake_id.ex
index 3b5f6477a..45314d53a 100644
--- a/lib/pleroma/web/api_spec/schemas/flake_id.ex
+++ b/lib/pleroma/web/api_spec/schemas/flake_id.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.FlakeID do
diff --git a/lib/pleroma/web/api_spec/schemas/list.ex b/lib/pleroma/web/api_spec/schemas/list.ex
index b7d1685c9..90f5ec987 100644
--- a/lib/pleroma/web/api_spec/schemas/list.ex
+++ b/lib/pleroma/web/api_spec/schemas/list.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.List do
diff --git a/lib/pleroma/web/api_spec/schemas/poll.ex b/lib/pleroma/web/api_spec/schemas/poll.ex
index c62096db0..943ad8bd4 100644
--- a/lib/pleroma/web/api_spec/schemas/poll.ex
+++ b/lib/pleroma/web/api_spec/schemas/poll.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.Poll do
@@ -28,8 +28,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Poll do
},
votes_count: %Schema{
type: :integer,
- nullable: true,
- description: "How many votes have been received. Number, or null if `multiple` is false."
+ description: "How many votes have been received. Number."
+ },
+ voters_count: %Schema{
+ type: :integer,
+ description: "How many unique accounts have voted. Number."
},
voted: %Schema{
type: :boolean,
@@ -61,7 +64,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Poll do
expired: true,
multiple: false,
votes_count: 10,
- voters_count: nil,
+ voters_count: 10,
voted: true,
own_votes: [
1
diff --git a/lib/pleroma/web/api_spec/schemas/push_subscription.ex b/lib/pleroma/web/api_spec/schemas/push_subscription.ex
index cc91b95b8..20fe9f304 100644
--- a/lib/pleroma/web/api_spec/schemas/push_subscription.ex
+++ b/lib/pleroma/web/api_spec/schemas/push_subscription.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.PushSubscription do
diff --git a/lib/pleroma/web/api_spec/schemas/scheduled_status.ex b/lib/pleroma/web/api_spec/schemas/scheduled_status.ex
index addefa9d3..607586e32 100644
--- a/lib/pleroma/web/api_spec/schemas/scheduled_status.ex
+++ b/lib/pleroma/web/api_spec/schemas/scheduled_status.ex
@@ -1,12 +1,12 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.ScheduledStatus do
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.Attachment
- alias Pleroma.Web.ApiSpec.Schemas.Poll
alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
+ alias Pleroma.Web.ApiSpec.StatusOperation
require OpenApiSpex
@@ -29,8 +29,9 @@ defmodule Pleroma.Web.ApiSpec.Schemas.ScheduledStatus do
spoiler_text: %Schema{type: :string, nullable: true},
visibility: %Schema{allOf: [VisibilityScope], nullable: true},
scheduled_at: %Schema{type: :string, format: :"date-time", nullable: true},
- poll: %Schema{allOf: [Poll], nullable: true},
- in_reply_to_id: %Schema{type: :string, nullable: true}
+ poll: StatusOperation.poll_params(),
+ in_reply_to_id: %Schema{type: :string, nullable: true},
+ expires_in: %Schema{type: :integer, nullable: true}
}
}
},
@@ -46,7 +47,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.ScheduledStatus do
scheduled_at: nil,
poll: nil,
idempotency: nil,
- in_reply_to_id: nil
+ in_reply_to_id: nil,
+ expires_in: nil
},
media_attachments: [Attachment.schema().example]
}
diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex
index e6890df2d..42fa98718 100644
--- a/lib/pleroma/web/api_spec/schemas/status.ex
+++ b/lib/pleroma/web/api_spec/schemas/status.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.Status do
@@ -23,9 +23,10 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
application: %Schema{
description: "The application used to post this status",
type: :object,
+ nullable: true,
properties: %{
name: %Schema{type: :string},
- website: %Schema{type: :string, nullable: true, format: :uri}
+ website: %Schema{type: :string, format: :uri}
}
},
bookmarked: %Schema{type: :boolean, description: "Have you bookmarked this status?"},
@@ -256,7 +257,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
"note" => "Tester Number 6",
"pleroma" => %{
"background_image" => nil,
- "confirmation_pending" => false,
+ "is_confirmed" => true,
"hide_favorites" => true,
"hide_followers" => false,
"hide_followers_count" => false,
@@ -291,7 +292,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
"url" => "http://localhost:4001/users/nick6",
"username" => "nick6"
},
- "application" => %{"name" => "Web", "website" => nil},
+ "application" => nil,
"bookmarked" => false,
"card" => nil,
"content" => "foobar",
diff --git a/lib/pleroma/web/api_spec/schemas/tag.ex b/lib/pleroma/web/api_spec/schemas/tag.ex
index e693fb83e..657b675e5 100644
--- a/lib/pleroma/web/api_spec/schemas/tag.ex
+++ b/lib/pleroma/web/api_spec/schemas/tag.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.Tag do
diff --git a/lib/pleroma/web/api_spec/schemas/visibility_scope.ex b/lib/pleroma/web/api_spec/schemas/visibility_scope.ex
index 831734e27..25a08a0b2 100644
--- a/lib/pleroma/web/api_spec/schemas/visibility_scope.ex
+++ b/lib/pleroma/web/api_spec/schemas/visibility_scope.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.VisibilityScope do
@@ -9,6 +9,6 @@ defmodule Pleroma.Web.ApiSpec.Schemas.VisibilityScope do
title: "VisibilityScope",
description: "Status visibility",
type: :string,
- enum: ["public", "unlisted", "private", "direct", "list"]
+ enum: ["public", "unlisted", "local", "private", "direct", "list"]
})
end
diff --git a/lib/pleroma/web/auth/authenticator.ex b/lib/pleroma/web/auth/authenticator.ex
index b4db312fb..84741ee11 100644
--- a/lib/pleroma/web/auth/authenticator.ex
+++ b/lib/pleroma/web/auth/authenticator.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Auth.Authenticator do
diff --git a/lib/pleroma/web/auth/ldap_authenticator.ex b/lib/pleroma/web/auth/ldap_authenticator.ex
index 402ab428b..17e08a2a6 100644
--- a/lib/pleroma/web/auth/ldap_authenticator.ex
+++ b/lib/pleroma/web/auth/ldap_authenticator.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Auth.LDAPAuthenticator do
diff --git a/lib/pleroma/web/auth/pleroma_authenticator.ex b/lib/pleroma/web/auth/pleroma_authenticator.ex
index d6d2a8d06..401f23c9f 100644
--- a/lib/pleroma/web/auth/pleroma_authenticator.ex
+++ b/lib/pleroma/web/auth/pleroma_authenticator.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Auth.PleromaAuthenticator do
@@ -84,7 +84,7 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do
password_confirmation: random_password
},
external: true,
- need_confirmation: false
+ confirmed: true
)
|> Repo.insert(),
{:ok, _} <-
diff --git a/lib/pleroma/web/auth/totp_authenticator.ex b/lib/pleroma/web/auth/totp_authenticator.ex
index edc9871ea..5947cd8c9 100644
--- a/lib/pleroma/web/auth/totp_authenticator.ex
+++ b/lib/pleroma/web/auth/totp_authenticator.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Auth.TOTPAuthenticator do
diff --git a/lib/pleroma/web/channels/user_socket.ex b/lib/pleroma/web/channels/user_socket.ex
index 306ef1916..1c09b6768 100644
--- a/lib/pleroma/web/channels/user_socket.ex
+++ b/lib/pleroma/web/channels/user_socket.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.UserSocket do
diff --git a/lib/pleroma/web/chat_channel.ex b/lib/pleroma/web/chat_channel.ex
index 3b1469c19..4008129e9 100644
--- a/lib/pleroma/web/chat_channel.ex
+++ b/lib/pleroma/web/chat_channel.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ChatChannel do
diff --git a/lib/pleroma/web/common_api.ex b/lib/pleroma/web/common_api.ex
index 60a50b027..b003e30c7 100644
--- a/lib/pleroma/web/common_api.ex
+++ b/lib/pleroma/web/common_api.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.CommonAPI do
@@ -15,6 +15,7 @@ defmodule Pleroma.Web.CommonAPI do
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
+ alias Pleroma.Web.CommonAPI.ActivityDraft
import Pleroma.Web.Gettext
import Pleroma.Web.CommonAPI.Utils
@@ -45,7 +46,8 @@ defmodule Pleroma.Web.CommonAPI do
{_, {:ok, %Activity{} = activity, _meta}} <-
{:common_pipeline,
Pipeline.common_pipeline(create_activity_data,
- local: true
+ local: true,
+ idempotency_key: opts[:idempotency_key]
)} do
{:ok, activity}
else
@@ -140,7 +142,7 @@ defmodule Pleroma.Web.CommonAPI do
with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(activity_id)},
{_, %Object{} = object, _} <-
- {:find_object, Object.normalize(activity, false), activity},
+ {:find_object, Object.normalize(activity, fetch: false), activity},
true <- User.superuser?(user) || user.ap_id == object.data["actor"],
{:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
{:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
@@ -171,7 +173,7 @@ defmodule Pleroma.Web.CommonAPI do
def repeat(id, user, params \\ %{}) do
with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
- object = %Object{} <- Object.normalize(activity, false),
+ object = %Object{} <- Object.normalize(activity, fetch: false),
{_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
public = public_announce?(object, params),
{:ok, announce, _} <- Builder.announce(user, object, public: public),
@@ -189,7 +191,7 @@ defmodule Pleroma.Web.CommonAPI do
def unrepeat(id, user) do
with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(id)},
- %Object{} = note <- Object.normalize(activity, false),
+ %Object{} = note <- Object.normalize(activity, fetch: false),
%Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
{:ok, undo, _} <- Builder.undo(user, announce),
{:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
@@ -251,7 +253,7 @@ defmodule Pleroma.Web.CommonAPI do
def unfavorite(id, user) do
with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(id)},
- %Object{} = note <- Object.normalize(activity, false),
+ %Object{} = note <- Object.normalize(activity, fetch: false),
%Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
{:ok, undo, _} <- Builder.undo(user, like),
{:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
@@ -264,7 +266,7 @@ defmodule Pleroma.Web.CommonAPI do
def react_with_emoji(id, user, emoji) do
with %Activity{} = activity <- Activity.get_by_id(id),
- object <- Object.normalize(activity),
+ object <- Object.normalize(activity, fetch: false),
{:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
{:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
{:ok, activity}
@@ -357,7 +359,7 @@ defmodule Pleroma.Web.CommonAPI do
def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
def get_visibility(%{visibility: visibility}, in_reply_to, _)
- when visibility in ~w{public unlisted private direct},
+ when visibility in ~w{public local unlisted private direct},
do: {visibility, get_replied_to_visibility(in_reply_to)}
def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
@@ -375,7 +377,7 @@ defmodule Pleroma.Web.CommonAPI do
def get_replied_to_visibility(nil), do: nil
def get_replied_to_visibility(activity) do
- with %Object{} = object <- Object.normalize(activity) do
+ with %Object{} = object <- Object.normalize(activity, fetch: false) do
Visibility.get_visibility(object)
end
end
@@ -398,31 +400,13 @@ defmodule Pleroma.Web.CommonAPI do
end
def listen(user, data) do
- visibility = Map.get(data, :visibility, "public")
-
- with {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
- listen_data <-
- data
- |> Map.take([:album, :artist, :title, :length])
- |> Map.new(fn {key, value} -> {to_string(key), value} end)
- |> Map.put("type", "Audio")
- |> Map.put("to", to)
- |> Map.put("cc", cc)
- |> Map.put("actor", user.ap_id),
- {:ok, activity} <-
- ActivityPub.listen(%{
- actor: user,
- to: to,
- object: listen_data,
- context: Utils.generate_context_id(),
- additional: %{"cc" => cc}
- }) do
- {:ok, activity}
+ with {:ok, draft} <- ActivityDraft.listen(user, data) do
+ ActivityPub.listen(draft.changes)
end
end
def post(user, %{status: _} = data) do
- with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
+ with {:ok, draft} <- ActivityDraft.create(user, data) do
ActivityPub.create(draft.changes, draft.preview?)
end
end
@@ -453,20 +437,46 @@ defmodule Pleroma.Web.CommonAPI do
end
end
- def add_mute(user, activity) do
+ def add_mute(user, activity, params \\ %{}) do
+ expires_in = Map.get(params, :expires_in, 0)
+
with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
_ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do
+ if expires_in > 0 do
+ Pleroma.Workers.MuteExpireWorker.enqueue(
+ "unmute_conversation",
+ %{"user_id" => user.id, "activity_id" => activity.id},
+ schedule_in: expires_in
+ )
+ end
+
{:ok, activity}
else
{:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
end
end
- def remove_mute(user, activity) do
+ def remove_mute(%User{} = user, %Activity{} = activity) do
ThreadMute.remove_mute(user.id, activity.data["context"])
{:ok, activity}
end
+ def remove_mute(user_id, activity_id) do
+ with {:user, %User{} = user} <- {:user, User.get_by_id(user_id)},
+ {:activity, %Activity{} = activity} <- {:activity, Activity.get_by_id(activity_id)} do
+ remove_mute(user, activity)
+ else
+ {what, result} = error ->
+ Logger.warn(
+ "CommonAPI.remove_mute/2 failed. #{what}: #{result}, user_id: #{user_id}, activity_id: #{
+ activity_id
+ }"
+ )
+
+ {:error, error}
+ end
+ end
+
def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
when is_binary(context) do
ThreadMute.exists?(user_id, context)
diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex
index 548f76609..73f1b0931 100644
--- a/lib/pleroma/web/common_api/activity_draft.ex
+++ b/lib/pleroma/web/common_api/activity_draft.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.CommonAPI.ActivityDraft do
@@ -22,7 +22,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
in_reply_to_conversation: nil,
visibility: nil,
expires_at: nil,
- poll: nil,
+ extra: nil,
emoji: %{},
content_html: nil,
mentions: [],
@@ -35,9 +35,14 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
preview?: false,
changes: %{}
- def create(user, params) do
+ def new(user, params) do
%__MODULE__{user: user}
|> put_params(params)
+ end
+
+ def create(user, params) do
+ user
+ |> new(params)
|> status()
|> summary()
|> with_valid(&attachments/1)
@@ -57,6 +62,30 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|> validate()
end
+ def listen(user, params) do
+ user
+ |> new(params)
+ |> visibility()
+ |> to_and_cc()
+ |> context()
+ |> listen_object()
+ |> with_valid(&changes/1)
+ |> validate()
+ end
+
+ defp listen_object(draft) do
+ object =
+ draft.params
+ |> Map.take([:album, :artist, :title, :length])
+ |> Map.new(fn {key, value} -> {to_string(key), value} end)
+ |> Map.put("type", "Audio")
+ |> Map.put("to", draft.to)
+ |> Map.put("cc", draft.cc)
+ |> Map.put("actor", draft.user.ap_id)
+
+ %__MODULE__{draft | object: object}
+ end
+
defp put_params(draft, params) do
params = Map.put_new(params, :in_reply_to_status_id, params[:in_reply_to_id])
%__MODULE__{draft | params: params}
@@ -121,7 +150,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
defp poll(draft) do
case Utils.make_poll_data(draft.params) do
{:ok, {poll, poll_emoji}} ->
- %__MODULE__{draft | poll: poll, emoji: Map.merge(draft.emoji, poll_emoji)}
+ %__MODULE__{draft | extra: poll, emoji: Map.merge(draft.emoji, poll_emoji)}
{:error, message} ->
add_error(draft, message)
@@ -129,32 +158,18 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
end
defp content(draft) do
- {content_html, mentions, tags} =
- Utils.make_content_html(
- draft.status,
- draft.attachments,
- draft.params,
- draft.visibility
- )
-
- %__MODULE__{draft | content_html: content_html, mentions: mentions, tags: tags}
- end
+ {content_html, mentioned_users, tags} = Utils.make_content_html(draft)
- defp to_and_cc(draft) do
- addressed_users =
- draft.mentions
+ mentions =
+ mentioned_users
|> Enum.map(fn {_, mentioned_user} -> mentioned_user.ap_id end)
|> Utils.get_addressed_users(draft.params[:to])
- {to, cc} =
- Utils.get_to_and_cc(
- draft.user,
- addressed_users,
- draft.in_reply_to,
- draft.visibility,
- draft.in_reply_to_conversation
- )
+ %__MODULE__{draft | content_html: content_html, mentions: mentions, tags: tags}
+ end
+ defp to_and_cc(draft) do
+ {to, cc} = Utils.get_to_and_cc(draft)
%__MODULE__{draft | to: to, cc: cc}
end
@@ -172,21 +187,10 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
emoji = Map.merge(Pleroma.Emoji.Formatter.get_emoji_map(draft.full_payload), draft.emoji)
object =
- Utils.make_note_data(
- draft.user.ap_id,
- draft.to,
- draft.context,
- draft.content_html,
- draft.attachments,
- draft.in_reply_to,
- draft.tags,
- draft.summary,
- draft.cc,
- draft.sensitive,
- draft.poll
- )
+ Utils.make_note_data(draft)
|> Map.put("emoji", emoji)
|> Map.put("source", draft.status)
+ |> Map.put("generator", draft.params[:generator])
%__MODULE__{draft | object: object}
end
diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index 3b71adf0e..9587dfa25 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.CommonAPI.Utils do
@@ -16,6 +16,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
+ alias Pleroma.Web.CommonAPI.ActivityDraft
alias Pleroma.Web.MediaProxy
alias Pleroma.Web.Plugs.AuthenticationPlug
@@ -50,67 +51,62 @@ defmodule Pleroma.Web.CommonAPI.Utils do
{_, descs} = Jason.decode(descs_str)
Enum.map(ids, fn media_id ->
- case Repo.get(Object, media_id) do
- %Object{data: data} ->
- Map.put(data, "name", descs[media_id])
-
- _ ->
- nil
+ with %Object{data: data} <- Repo.get(Object, media_id) do
+ Map.put(data, "name", descs[media_id])
end
end)
|> Enum.reject(&is_nil/1)
end
- @spec get_to_and_cc(
- User.t(),
- list(String.t()),
- Activity.t() | nil,
- String.t(),
- Participation.t() | nil
- ) :: {list(String.t()), list(String.t())}
+ @spec get_to_and_cc(ActivityDraft.t()) :: {list(String.t()), list(String.t())}
- def get_to_and_cc(_, _, _, _, %Participation{} = participation) do
+ def get_to_and_cc(%{in_reply_to_conversation: %Participation{} = participation}) do
participation = Repo.preload(participation, :recipients)
{Enum.map(participation.recipients, & &1.ap_id), []}
end
- def get_to_and_cc(user, mentioned_users, inReplyTo, "public", _) do
- to = [Pleroma.Constants.as_public() | mentioned_users]
- cc = [user.follower_address]
+ def get_to_and_cc(%{visibility: visibility} = draft) when visibility in ["public", "local"] do
+ to =
+ case visibility do
+ "public" -> [Pleroma.Constants.as_public() | draft.mentions]
+ "local" -> [Pleroma.Constants.as_local_public() | draft.mentions]
+ end
+
+ cc = [draft.user.follower_address]
- if inReplyTo do
- {Enum.uniq([inReplyTo.data["actor"] | to]), cc}
+ if draft.in_reply_to do
+ {Enum.uniq([draft.in_reply_to.data["actor"] | to]), cc}
else
{to, cc}
end
end
- def get_to_and_cc(user, mentioned_users, inReplyTo, "unlisted", _) do
- to = [user.follower_address | mentioned_users]
+ def get_to_and_cc(%{visibility: "unlisted"} = draft) do
+ to = [draft.user.follower_address | draft.mentions]
cc = [Pleroma.Constants.as_public()]
- if inReplyTo do
- {Enum.uniq([inReplyTo.data["actor"] | to]), cc}
+ if draft.in_reply_to do
+ {Enum.uniq([draft.in_reply_to.data["actor"] | to]), cc}
else
{to, cc}
end
end
- def get_to_and_cc(user, mentioned_users, inReplyTo, "private", _) do
- {to, cc} = get_to_and_cc(user, mentioned_users, inReplyTo, "direct", nil)
- {[user.follower_address | to], cc}
+ def get_to_and_cc(%{visibility: "private"} = draft) do
+ {to, cc} = get_to_and_cc(struct(draft, visibility: "direct"))
+ {[draft.user.follower_address | to], cc}
end
- def get_to_and_cc(_user, mentioned_users, inReplyTo, "direct", _) do
+ def get_to_and_cc(%{visibility: "direct"} = draft) do
# If the OP is a DM already, add the implicit actor.
- if inReplyTo && Visibility.is_direct?(inReplyTo) do
- {Enum.uniq([inReplyTo.data["actor"] | mentioned_users]), []}
+ if draft.in_reply_to && Visibility.is_direct?(draft.in_reply_to) do
+ {Enum.uniq([draft.in_reply_to.data["actor"] | draft.mentions]), []}
else
- {mentioned_users, []}
+ {draft.mentions, []}
end
end
- def get_to_and_cc(_user, mentions, _inReplyTo, {:list, _}, _), do: {mentions, []}
+ def get_to_and_cc(%{visibility: {:list, _}, mentions: mentions}), do: {mentions, []}
def get_addressed_users(_, to) when is_list(to) do
User.get_ap_ids_by_nicknames(to)
@@ -203,30 +199,25 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
end
- def make_content_html(
- status,
- attachments,
- data,
- visibility
- ) do
+ def make_content_html(%ActivityDraft{} = draft) do
attachment_links =
- data
+ draft.params
|> Map.get("attachment_links", Config.get([:instance, :attachment_links]))
|> truthy_param?()
- content_type = get_content_type(data[:content_type])
+ content_type = get_content_type(draft.params[:content_type])
options =
- if visibility == "direct" && Config.get([:instance, :safe_dm_mentions]) do
+ if draft.visibility == "direct" && Config.get([:instance, :safe_dm_mentions]) do
[safe_mention: true]
else
[]
end
- status
+ draft.status
|> format_input(content_type, options)
- |> maybe_add_attachments(attachments, attachment_links)
- |> maybe_add_nsfw_tag(data)
+ |> maybe_add_attachments(draft.attachments, attachment_links)
+ |> maybe_add_nsfw_tag(draft.params)
end
defp get_content_type(content_type) do
@@ -308,39 +299,27 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|> Formatter.html_escape("text/html")
end
- def make_note_data(
- actor,
- to,
- context,
- content_html,
- attachments,
- in_reply_to,
- tags,
- summary \\ nil,
- cc \\ [],
- sensitive \\ false,
- extra_params \\ %{}
- ) do
+ def make_note_data(%ActivityDraft{} = draft) do
%{
"type" => "Note",
- "to" => to,
- "cc" => cc,
- "content" => content_html,
- "summary" => summary,
- "sensitive" => truthy_param?(sensitive),
- "context" => context,
- "attachment" => attachments,
- "actor" => actor,
- "tag" => Keyword.values(tags) |> Enum.uniq()
+ "to" => draft.to,
+ "cc" => draft.cc,
+ "content" => draft.content_html,
+ "summary" => draft.summary,
+ "sensitive" => draft.sensitive,
+ "context" => draft.context,
+ "attachment" => draft.attachments,
+ "actor" => draft.user.ap_id,
+ "tag" => Keyword.values(draft.tags) |> Enum.uniq()
}
- |> add_in_reply_to(in_reply_to)
- |> Map.merge(extra_params)
+ |> add_in_reply_to(draft.in_reply_to)
+ |> Map.merge(draft.extra)
end
defp add_in_reply_to(object, nil), do: object
defp add_in_reply_to(object, in_reply_to) do
- with %Object{} = in_reply_to_object <- Object.normalize(in_reply_to) do
+ with %Object{} = in_reply_to_object <- Object.normalize(in_reply_to, fetch: false) do
Map.put(object, "inReplyTo", in_reply_to_object.data["id"])
else
_ -> object
@@ -420,7 +399,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
%Activity{data: %{"to" => _to, "type" => type} = data} = activity
)
when type == "Create" do
- object = Object.normalize(activity, false)
+ object = Object.normalize(activity, fetch: false)
object_data =
cond do
diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex
index 69188a882..61d65e7a3 100644
--- a/lib/pleroma/web/controller_helper.ex
+++ b/lib/pleroma/web/controller_helper.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ControllerHelper do
@@ -67,7 +67,7 @@ defmodule Pleroma.Web.ControllerHelper do
defp build_pagination_fields(conn, min_id, max_id, extra_params) do
params =
conn.params
- |> Map.drop(Map.keys(conn.path_params))
+ |> Map.drop(Map.keys(conn.path_params) |> Enum.map(&String.to_existing_atom/1))
|> Map.merge(extra_params)
|> Map.drop(@id_keys)
diff --git a/lib/pleroma/web/embed_controller.ex b/lib/pleroma/web/embed_controller.ex
index f6b8a5ee1..c7912bb1f 100644
--- a/lib/pleroma/web/embed_controller.ex
+++ b/lib/pleroma/web/embed_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.EmbedController do
@@ -31,7 +31,7 @@ defmodule Pleroma.Web.EmbedController do
end
defp get_counts(%Activity{} = activity) do
- %Object{data: data} = Object.normalize(activity)
+ %Object{data: data} = Object.normalize(activity, fetch: false)
%{
likes: Map.get(data, "like_count", 0),
diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex
index f26542e88..8e274de88 100644
--- a/lib/pleroma/web/endpoint.ex
+++ b/lib/pleroma/web/endpoint.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Endpoint do
@@ -23,6 +23,18 @@ defmodule Pleroma.Web.Endpoint do
# InstanceStatic needs to be before Plug.Static to be able to override shipped-static files
# If you're adding new paths to `only:` you'll need to configure them in InstanceStatic as well
# Cache-control headers are duplicated in case we turn off etags in the future
+ plug(
+ Pleroma.Web.Plugs.InstanceStatic,
+ at: "/",
+ from: :pleroma,
+ only: ["emoji", "images"],
+ gzip: true,
+ cache_control_for_etags: "public, max-age=1209600",
+ headers: %{
+ "cache-control" => "public, max-age=1209600"
+ }
+ )
+
plug(Pleroma.Web.Plugs.InstanceStatic,
at: "/",
gzip: true,
diff --git a/lib/pleroma/web/fallback/legacy_pleroma_api_rerouter_plug.ex b/lib/pleroma/web/fallback/legacy_pleroma_api_rerouter_plug.ex
new file mode 100644
index 000000000..f86d6b52b
--- /dev/null
+++ b/lib/pleroma/web/fallback/legacy_pleroma_api_rerouter_plug.ex
@@ -0,0 +1,26 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Fallback.LegacyPleromaApiRerouterPlug do
+ alias Pleroma.Web.Endpoint
+ alias Pleroma.Web.Fallback.RedirectController
+
+ def init(opts), do: opts
+
+ def call(%{path_info: ["api", "pleroma" | path_info_rest]} = conn, _opts) do
+ new_path_info = ["api", "v1", "pleroma" | path_info_rest]
+ new_request_path = Enum.join(new_path_info, "/")
+
+ conn
+ |> Map.merge(%{
+ path_info: new_path_info,
+ request_path: new_request_path
+ })
+ |> Endpoint.call(conn.params)
+ end
+
+ def call(conn, _opts) do
+ RedirectController.api_not_implemented(conn, %{})
+ end
+end
diff --git a/lib/pleroma/web/fallback/redirect_controller.ex b/lib/pleroma/web/fallback/redirect_controller.ex
index 6f759d559..5fca290e5 100644
--- a/lib/pleroma/web/fallback/redirect_controller.ex
+++ b/lib/pleroma/web/fallback/redirect_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Fallback.RedirectController do
@@ -37,10 +37,11 @@ defmodule Pleroma.Web.Fallback.RedirectController do
tags = build_tags(conn, params)
preloads = preload_data(conn, params)
+ title = "<title>#{Pleroma.Config.get([:instance, :name])}</title>"
response =
index_content
- |> String.replace("<!--server-generated-meta-->", tags <> preloads)
+ |> String.replace("<!--server-generated-meta-->", tags <> preloads <> title)
conn
|> put_resp_content_type("text/html")
@@ -54,10 +55,11 @@ defmodule 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)
+ |> String.replace("<!--server-generated-meta-->", preloads <> title)
conn
|> put_resp_content_type("text/html")
diff --git a/lib/pleroma/web/fed_sockets.ex b/lib/pleroma/web/fed_sockets.ex
deleted file mode 100644
index 1fd5899c8..000000000
--- a/lib/pleroma/web/fed_sockets.ex
+++ /dev/null
@@ -1,185 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.FedSockets do
- @moduledoc """
- This documents the FedSockets framework. A framework for federating
- ActivityPub objects between servers via persistant WebSocket connections.
-
- FedSockets allow servers to authenticate on first contact and maintain that
- connection, eliminating the need to authenticate every time data needs to be shared.
-
- ## Protocol
- FedSockets currently support 2 types of data transfer:
- * `publish` method which doesn't require a response
- * `fetch` method requires a response be sent
-
- ### Publish
- The publish operation sends a json encoded map of the shape:
- %{action: :publish, data: json}
- and accepts (but does not require) a reply of form:
- %{"action" => "publish_reply"}
-
- The outgoing params represent
- * data: ActivityPub object encoded into json
-
-
- ### Fetch
- The fetch operation sends a json encoded map of the shape:
- %{action: :fetch, data: id, uuid: fetch_uuid}
- and requires a reply of form:
- %{"action" => "fetch_reply", "uuid" => uuid, "data" => data}
-
- The outgoing params represent
- * id: an ActivityPub object URI
- * uuid: a unique uuid generated by the sender
-
- The reply params represent
- * data: an ActivityPub object encoded into json
- * uuid: the uuid sent along with the fetch request
-
- ## Examples
- Clients of FedSocket transfers shouldn't need to use any of the functions outside of this module.
-
- A typical publish operation can be performed through the following code, and a fetch operation in a similar manner.
-
- case FedSockets.get_or_create_fed_socket(inbox) do
- {:ok, fedsocket} ->
- FedSockets.publish(fedsocket, json)
-
- _ ->
- alternative_publish(inbox, actor, json, params)
- end
-
- ## Configuration
- FedSockets have the following config settings
-
- config :pleroma, :fed_sockets,
- enabled: true,
- ping_interval: :timer.seconds(15),
- connection_duration: :timer.hours(1),
- rejection_duration: :timer.hours(1),
- fed_socket_fetches: [
- default: 12_000,
- interval: 3_000,
- lazy: false
- ]
- * enabled - turn FedSockets on or off with this flag. Can be toggled at runtime.
- * connection_duration - How long a FedSocket can sit idle before it's culled.
- * rejection_duration - After failing to make a FedSocket connection a host will be excluded
- from further connections for this amount of time
- * fed_socket_fetches - Use these parameters to pass options to the Cachex queue backing the FetchRegistry
- * fed_socket_rejections - Use these parameters to pass options to the Cachex queue backing the FedRegistry
-
- Cachex options are
- * default: the minimum amount of time a fetch can wait before it times out.
- * interval: the interval between checks for timed out entries. This plus the default represent the maximum time allowed
- * lazy: leave at false for consistant and fast lookups, set to true for stricter timeout enforcement
-
- """
- require Logger
-
- alias Pleroma.Web.FedSockets.FedRegistry
- alias Pleroma.Web.FedSockets.FedSocket
- alias Pleroma.Web.FedSockets.SocketInfo
-
- @doc """
- returns a FedSocket for the given origin. Will reuse an existing one or create a new one.
-
- address is expected to be a fully formed URL such as:
- "http://www.example.com" or "http://www.example.com:8080"
-
- It can and usually does include additional path parameters,
- but these are ignored as the FedSockets are organized by host and port info alone.
- """
- def get_or_create_fed_socket(address) do
- with {:cache, {:error, :missing}} <- {:cache, get_fed_socket(address)},
- {:connect, {:ok, _pid}} <- {:connect, FedSocket.connect_to_host(address)},
- {:cache, {:ok, fed_socket}} <- {:cache, get_fed_socket(address)} do
- Logger.debug("fedsocket created for - #{inspect(address)}")
- {:ok, fed_socket}
- else
- {:cache, {:ok, socket}} ->
- Logger.debug("fedsocket found in cache - #{inspect(address)}")
- {:ok, socket}
-
- {:cache, {:error, :rejected} = e} ->
- e
-
- {:connect, {:error, _host}} ->
- Logger.debug("set host rejected for - #{inspect(address)}")
- FedRegistry.set_host_rejected(address)
- {:error, :rejected}
-
- {_, {:error, :disabled}} ->
- {:error, :disabled}
-
- {_, {:error, reason}} ->
- Logger.warn("get_or_create_fed_socket error - #{inspect(reason)}")
- {:error, reason}
- end
- end
-
- @doc """
- returns a FedSocket for the given origin. Will not create a new FedSocket if one does not exist.
-
- address is expected to be a fully formed URL such as:
- "http://www.example.com" or "http://www.example.com:8080"
- """
- def get_fed_socket(address) do
- origin = SocketInfo.origin(address)
-
- with {:config, true} <- {:config, Pleroma.Config.get([:fed_sockets, :enabled], false)},
- {:ok, socket} <- FedRegistry.get_fed_socket(origin) do
- {:ok, socket}
- else
- {:config, _} ->
- {:error, :disabled}
-
- {:error, :rejected} ->
- Logger.debug("FedSocket previously rejected - #{inspect(origin)}")
- {:error, :rejected}
-
- {:error, reason} ->
- {:error, reason}
- end
- end
-
- @doc """
- Sends the supplied data via the publish protocol.
- It will not block waiting for a reply.
- Returns :ok but this is not an indication of a successful transfer.
-
- the data is expected to be JSON encoded binary data.
- """
- def publish(%SocketInfo{} = fed_socket, json) do
- FedSocket.publish(fed_socket, json)
- end
-
- @doc """
- Sends the supplied data via the fetch protocol.
- It will block waiting for a reply or timeout.
-
- Returns {:ok, object} where object is the requested object (or nil)
- {:error, :timeout} in the event the message was not responded to
-
- the id is expected to be the URI of an ActivityPub object.
- """
- def fetch(%SocketInfo{} = fed_socket, id) do
- FedSocket.fetch(fed_socket, id)
- end
-
- @doc """
- Disconnect all and restart FedSockets.
- This is mainly used in development and testing but could be useful in production.
- """
- def reset do
- FedRegistry
- |> Process.whereis()
- |> Process.exit(:testing)
- end
-
- def uri_for_origin(origin),
- do: "ws://#{origin}/api/fedsocket/v1"
-end
diff --git a/lib/pleroma/web/fed_sockets/fed_registry.ex b/lib/pleroma/web/fed_sockets/fed_registry.ex
deleted file mode 100644
index e00ea69c0..000000000
--- a/lib/pleroma/web/fed_sockets/fed_registry.ex
+++ /dev/null
@@ -1,185 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.FedSockets.FedRegistry do
- @moduledoc """
- The FedRegistry stores the active FedSockets for quick retrieval.
-
- The storage and retrieval portion of the FedRegistry is done in process through
- elixir's `Registry` module for speed and its ability to monitor for terminated processes.
-
- Dropped connections will be caught by `Registry` and deleted. Since the next
- message will initiate a new connection there is no reason to try and reconnect at that point.
-
- Normally outside modules should have no need to call or use the FedRegistry themselves.
- """
-
- alias Pleroma.Web.FedSockets.FedSocket
- alias Pleroma.Web.FedSockets.SocketInfo
-
- require Logger
-
- @default_rejection_duration 15 * 60 * 1000
- @rejections :fed_socket_rejections
-
- @doc """
- Retrieves a FedSocket from the Registry given it's origin.
-
- The origin is expected to be a string identifying the endpoint "example.com" or "example2.com:8080"
-
- Will return:
- * {:ok, fed_socket} for working FedSockets
- * {:error, :rejected} for origins that have been tried and refused within the rejection duration interval
- * {:error, some_reason} usually :missing for unknown origins
- """
- def get_fed_socket(origin) do
- case get_registry_data(origin) do
- {:error, reason} ->
- {:error, reason}
-
- {:ok, %{state: :connected} = socket_info} ->
- {:ok, socket_info}
- end
- end
-
- @doc """
- Adds a connected FedSocket to the Registry.
-
- Always returns {:ok, fed_socket}
- """
- def add_fed_socket(origin, pid \\ nil) do
- origin
- |> SocketInfo.build(pid)
- |> SocketInfo.connect()
- |> add_socket_info
- end
-
- defp add_socket_info(%{origin: origin, state: :connected} = socket_info) do
- case Registry.register(FedSockets.Registry, origin, socket_info) do
- {:ok, _owner} ->
- clear_prior_rejection(origin)
- Logger.debug("fedsocket added: #{inspect(origin)}")
-
- {:ok, socket_info}
-
- {:error, {:already_registered, _pid}} ->
- FedSocket.close(socket_info)
- existing_socket_info = Registry.lookup(FedSockets.Registry, origin)
-
- {:ok, existing_socket_info}
-
- _ ->
- {:error, :error_adding_socket}
- end
- end
-
- @doc """
- Mark this origin as having rejected a connection attempt.
- This will keep it from getting additional connection attempts
- for a period of time specified in the config.
-
- Always returns {:ok, new_reg_data}
- """
- def set_host_rejected(uri) do
- new_reg_data =
- uri
- |> SocketInfo.origin()
- |> get_or_create_registry_data()
- |> set_to_rejected()
- |> save_registry_data()
-
- {:ok, new_reg_data}
- end
-
- @doc """
- Retrieves the FedRegistryData from the Registry given it's origin.
-
- The origin is expected to be a string identifying the endpoint "example.com" or "example2.com:8080"
-
- Will return:
- * {:ok, fed_registry_data} for known origins
- * {:error, :missing} for uniknown origins
- * {:error, :cache_error} indicating some low level runtime issues
- """
- def get_registry_data(origin) do
- case Registry.lookup(FedSockets.Registry, origin) do
- [] ->
- if is_rejected?(origin) do
- Logger.debug("previously rejected fedsocket requested")
- {:error, :rejected}
- else
- {:error, :missing}
- end
-
- [{_pid, %{state: :connected} = socket_info}] ->
- {:ok, socket_info}
-
- _ ->
- {:error, :cache_error}
- end
- end
-
- @doc """
- Retrieves a map of all sockets from the Registry. The keys are the origins and the values are the corresponding SocketInfo
- """
- def list_all do
- (list_all_connected() ++ list_all_rejected())
- |> Enum.into(%{})
- end
-
- defp list_all_connected do
- FedSockets.Registry
- |> Registry.select([{{:"$1", :_, :"$3"}, [], [{{:"$1", :"$3"}}]}])
- end
-
- defp list_all_rejected do
- {:ok, keys} = Cachex.keys(@rejections)
-
- {:ok, registry_data} =
- Cachex.execute(@rejections, fn worker ->
- Enum.map(keys, fn k -> {k, Cachex.get!(worker, k)} end)
- end)
-
- registry_data
- end
-
- defp clear_prior_rejection(origin),
- do: Cachex.del(@rejections, origin)
-
- defp is_rejected?(origin) do
- case Cachex.get(@rejections, origin) do
- {:ok, nil} ->
- false
-
- {:ok, _} ->
- true
- end
- end
-
- defp get_or_create_registry_data(origin) do
- case get_registry_data(origin) do
- {:error, :missing} ->
- %SocketInfo{origin: origin}
-
- {:ok, socket_info} ->
- socket_info
- end
- end
-
- defp save_registry_data(%SocketInfo{origin: origin, state: :connected} = socket_info) do
- {:ok, true} = Registry.update_value(FedSockets.Registry, origin, fn _ -> socket_info end)
- socket_info
- end
-
- defp save_registry_data(%SocketInfo{origin: origin, state: :rejected} = socket_info) do
- rejection_expiration =
- Pleroma.Config.get([:fed_sockets, :rejection_duration], @default_rejection_duration)
-
- {:ok, true} = Cachex.put(@rejections, origin, socket_info, ttl: rejection_expiration)
- socket_info
- end
-
- defp set_to_rejected(%SocketInfo{} = socket_info),
- do: %SocketInfo{socket_info | state: :rejected}
-end
diff --git a/lib/pleroma/web/fed_sockets/fed_socket.ex b/lib/pleroma/web/fed_sockets/fed_socket.ex
deleted file mode 100644
index 98d64e65a..000000000
--- a/lib/pleroma/web/fed_sockets/fed_socket.ex
+++ /dev/null
@@ -1,137 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.FedSockets.FedSocket do
- @moduledoc """
- The FedSocket module abstracts the actions to be taken taken on connections regardless of
- whether the connection started as inbound or outbound.
-
-
- Normally outside modules will have no need to call the FedSocket module directly.
- """
-
- alias Pleroma.Object
- alias Pleroma.Object.Containment
- alias Pleroma.User
- alias Pleroma.Web.ActivityPub.ObjectView
- alias Pleroma.Web.ActivityPub.UserView
- alias Pleroma.Web.ActivityPub.Visibility
- alias Pleroma.Web.FedSockets.FetchRegistry
- alias Pleroma.Web.FedSockets.IngesterWorker
- alias Pleroma.Web.FedSockets.OutgoingHandler
- alias Pleroma.Web.FedSockets.SocketInfo
-
- require Logger
-
- @shake "61dd18f7-f1e6-49a4-939a-a749fcdc1103"
-
- def connect_to_host(uri) do
- case OutgoingHandler.start_link(uri) do
- {:ok, pid} ->
- {:ok, pid}
-
- error ->
- {:error, error}
- end
- end
-
- def close(%SocketInfo{pid: socket_pid}),
- do: Process.send(socket_pid, :close, [])
-
- def publish(%SocketInfo{pid: socket_pid}, json) do
- %{action: :publish, data: json}
- |> Jason.encode!()
- |> send_packet(socket_pid)
- end
-
- def fetch(%SocketInfo{pid: socket_pid}, id) do
- fetch_uuid = FetchRegistry.register_fetch(id)
-
- %{action: :fetch, data: id, uuid: fetch_uuid}
- |> Jason.encode!()
- |> send_packet(socket_pid)
-
- wait_for_fetch_to_return(fetch_uuid, 0)
- end
-
- def receive_package(%SocketInfo{} = fed_socket, json) do
- json
- |> Jason.decode!()
- |> process_package(fed_socket)
- end
-
- defp wait_for_fetch_to_return(uuid, cntr) do
- case FetchRegistry.check_fetch(uuid) do
- {:error, :waiting} ->
- Process.sleep(:math.pow(cntr, 3) |> Kernel.trunc())
- wait_for_fetch_to_return(uuid, cntr + 1)
-
- {:error, :missing} ->
- Logger.error("FedSocket fetch timed out - #{inspect(uuid)}")
- {:error, :timeout}
-
- {:ok, _fr} ->
- FetchRegistry.pop_fetch(uuid)
- end
- end
-
- defp process_package(%{"action" => "publish", "data" => data}, %{origin: origin} = _fed_socket) do
- if Containment.contain_origin(origin, data) do
- IngesterWorker.enqueue("ingest", %{"object" => data})
- end
-
- {:reply, %{"action" => "publish_reply", "status" => "processed"}}
- end
-
- defp process_package(%{"action" => "fetch_reply", "uuid" => uuid, "data" => data}, _fed_socket) do
- FetchRegistry.register_fetch_received(uuid, data)
- {:noreply, nil}
- end
-
- defp process_package(%{"action" => "fetch", "uuid" => uuid, "data" => ap_id}, _fed_socket) do
- {:ok, data} = render_fetched_data(ap_id, uuid)
- {:reply, data}
- end
-
- defp process_package(%{"action" => "publish_reply"}, _fed_socket) do
- {:noreply, nil}
- end
-
- defp process_package(other, _fed_socket) do
- Logger.warn("unknown json packages received #{inspect(other)}")
- {:noreply, nil}
- end
-
- defp render_fetched_data(ap_id, uuid) do
- {:ok,
- %{
- "action" => "fetch_reply",
- "status" => "processed",
- "uuid" => uuid,
- "data" => represent_item(ap_id)
- }}
- end
-
- defp represent_item(ap_id) do
- case User.get_by_ap_id(ap_id) do
- nil ->
- object = Object.get_cached_by_ap_id(ap_id)
-
- if Visibility.is_public?(object) do
- Phoenix.View.render_to_string(ObjectView, "object.json", object: object)
- else
- nil
- end
-
- user ->
- Phoenix.View.render_to_string(UserView, "user.json", user: user)
- end
- end
-
- defp send_packet(data, socket_pid) do
- Process.send(socket_pid, {:send, data}, [])
- end
-
- def shake, do: @shake
-end
diff --git a/lib/pleroma/web/fed_sockets/fetch_registry.ex b/lib/pleroma/web/fed_sockets/fetch_registry.ex
deleted file mode 100644
index 7897f0fc6..000000000
--- a/lib/pleroma/web/fed_sockets/fetch_registry.ex
+++ /dev/null
@@ -1,151 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.FedSockets.FetchRegistry do
- @moduledoc """
- The FetchRegistry acts as a broker for fetch requests and return values.
- This allows calling processes to block while waiting for a reply.
- It doesn't impose it's own process instead using `Cachex` to handle fetches in process, allowing
- multi threaded processes to avoid bottlenecking.
-
- Normally outside modules will have no need to call or use the FetchRegistry themselves.
-
- The `Cachex` parameters can be controlled from the config. Since exact timeout intervals
- aren't necessary the following settings are used by default:
-
- config :pleroma, :fed_sockets,
- fed_socket_fetches: [
- default: 12_000,
- interval: 3_000,
- lazy: false
- ]
-
- """
-
- defmodule FetchRegistryData do
- defstruct uuid: nil,
- sent_json: nil,
- received_json: nil,
- sent_at: nil,
- received_at: nil
- end
-
- alias Ecto.UUID
-
- require Logger
-
- @fetches :fed_socket_fetches
-
- @doc """
- Registers a json request wth the FetchRegistry and returns the identifying UUID.
- """
- def register_fetch(json) do
- %FetchRegistryData{uuid: uuid} =
- json
- |> new_registry_data
- |> save_registry_data
-
- uuid
- end
-
- @doc """
- Reports on the status of a Fetch given the identifying UUID.
-
- Will return
- * {:ok, fetched_object} if a fetch has completed
- * {:error, :waiting} if a fetch is still pending
- * {:error, other_error} usually :missing to indicate a fetch that has timed out
- """
- def check_fetch(uuid) do
- case get_registry_data(uuid) do
- {:ok, %FetchRegistryData{received_at: nil}} ->
- {:error, :waiting}
-
- {:ok, %FetchRegistryData{} = reg_data} ->
- {:ok, reg_data}
-
- e ->
- e
- end
- end
-
- @doc """
- Retrieves the response to a fetch given the identifying UUID.
- The completed fetch will be deleted from the FetchRegistry
-
- Will return
- * {:ok, fetched_object} if a fetch has completed
- * {:error, :waiting} if a fetch is still pending
- * {:error, other_error} usually :missing to indicate a fetch that has timed out
- """
- def pop_fetch(uuid) do
- case check_fetch(uuid) do
- {:ok, %FetchRegistryData{received_json: received_json}} ->
- delete_registry_data(uuid)
- {:ok, received_json}
-
- e ->
- e
- end
- end
-
- @doc """
- This is called to register a fetch has returned.
- It expects the result data along with the UUID that was sent in the request
-
- Will return the fetched object or :error
- """
- def register_fetch_received(uuid, data) do
- case get_registry_data(uuid) do
- {:ok, %FetchRegistryData{received_at: nil} = reg_data} ->
- reg_data
- |> set_fetch_received(data)
- |> save_registry_data()
-
- {:ok, %FetchRegistryData{} = reg_data} ->
- Logger.warn("tried to add fetched data twice - #{uuid}")
- reg_data
-
- {:error, _} ->
- Logger.warn("Error adding fetch to registry - #{uuid}")
- :error
- end
- end
-
- defp new_registry_data(json) do
- %FetchRegistryData{
- uuid: UUID.generate(),
- sent_json: json,
- sent_at: :erlang.monotonic_time(:millisecond)
- }
- end
-
- defp get_registry_data(origin) do
- case Cachex.get(@fetches, origin) do
- {:ok, nil} ->
- {:error, :missing}
-
- {:ok, reg_data} ->
- {:ok, reg_data}
-
- _ ->
- {:error, :cache_error}
- end
- end
-
- defp set_fetch_received(%FetchRegistryData{} = reg_data, data),
- do: %FetchRegistryData{
- reg_data
- | received_at: :erlang.monotonic_time(:millisecond),
- received_json: data
- }
-
- defp save_registry_data(%FetchRegistryData{uuid: uuid} = reg_data) do
- {:ok, true} = Cachex.put(@fetches, uuid, reg_data)
- reg_data
- end
-
- defp delete_registry_data(origin),
- do: {:ok, true} = Cachex.del(@fetches, origin)
-end
diff --git a/lib/pleroma/web/fed_sockets/incoming_handler.ex b/lib/pleroma/web/fed_sockets/incoming_handler.ex
deleted file mode 100644
index 49d0d9d84..000000000
--- a/lib/pleroma/web/fed_sockets/incoming_handler.ex
+++ /dev/null
@@ -1,88 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.FedSockets.IncomingHandler do
- require Logger
-
- alias Pleroma.Web.FedSockets.FedRegistry
- alias Pleroma.Web.FedSockets.FedSocket
- alias Pleroma.Web.FedSockets.SocketInfo
-
- import HTTPSignatures, only: [validate_conn: 1, split_signature: 1]
-
- @behaviour :cowboy_websocket
-
- def init(req, state) do
- shake = FedSocket.shake()
-
- with true <- Pleroma.Config.get([:fed_sockets, :enabled]),
- sec_protocol <- :cowboy_req.header("sec-websocket-protocol", req, nil),
- headers = %{"(request-target)" => ^shake} <- :cowboy_req.headers(req),
- true <- validate_conn(%{req_headers: headers}),
- %{"keyId" => origin} <- split_signature(headers["signature"]) do
- req =
- if is_nil(sec_protocol) do
- req
- else
- :cowboy_req.set_resp_header("sec-websocket-protocol", sec_protocol, req)
- end
-
- {:cowboy_websocket, req, %{origin: origin}, %{}}
- else
- _ ->
- {:ok, req, state}
- end
- end
-
- def websocket_init(%{origin: origin}) do
- case FedRegistry.add_fed_socket(origin) do
- {:ok, socket_info} ->
- {:ok, socket_info}
-
- e ->
- Logger.error("FedSocket websocket_init failed - #{inspect(e)}")
- {:error, inspect(e)}
- end
- end
-
- # Use the ping to check if the connection should be expired
- def websocket_handle(:ping, socket_info) do
- if SocketInfo.expired?(socket_info) do
- {:stop, socket_info}
- else
- {:ok, socket_info, :hibernate}
- end
- end
-
- def websocket_handle({:text, data}, socket_info) do
- socket_info = SocketInfo.touch(socket_info)
-
- case FedSocket.receive_package(socket_info, data) do
- {:noreply, _} ->
- {:ok, socket_info}
-
- {:reply, reply} ->
- {:reply, {:text, Jason.encode!(reply)}, socket_info}
-
- {:error, reason} ->
- Logger.error("incoming error - receive_package: #{inspect(reason)}")
- {:ok, socket_info}
- end
- end
-
- def websocket_info({:send, message}, socket_info) do
- socket_info = SocketInfo.touch(socket_info)
-
- {:reply, {:text, message}, socket_info}
- end
-
- def websocket_info(:close, state) do
- {:stop, state}
- end
-
- def websocket_info(message, state) do
- Logger.debug("#{__MODULE__} unknown message #{inspect(message)}")
- {:ok, state}
- end
-end
diff --git a/lib/pleroma/web/fed_sockets/ingester_worker.ex b/lib/pleroma/web/fed_sockets/ingester_worker.ex
deleted file mode 100644
index 325f2a4ab..000000000
--- a/lib/pleroma/web/fed_sockets/ingester_worker.ex
+++ /dev/null
@@ -1,33 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.FedSockets.IngesterWorker do
- use Pleroma.Workers.WorkerHelper, queue: "ingestion_queue"
- require Logger
-
- alias Pleroma.Web.Federator
-
- @impl Oban.Worker
- def perform(%Job{args: %{"op" => "ingest", "object" => ingestee}}) do
- try do
- ingestee
- |> Jason.decode!()
- |> do_ingestion()
- rescue
- e ->
- Logger.error("IngesterWorker error - #{inspect(e)}")
- e
- end
- end
-
- defp do_ingestion(params) do
- case Federator.incoming_ap_doc(params) do
- {:error, reason} ->
- {:error, reason}
-
- {:ok, object} ->
- {:ok, object}
- end
- end
-end
diff --git a/lib/pleroma/web/fed_sockets/outgoing_handler.ex b/lib/pleroma/web/fed_sockets/outgoing_handler.ex
deleted file mode 100644
index e235a7c43..000000000
--- a/lib/pleroma/web/fed_sockets/outgoing_handler.ex
+++ /dev/null
@@ -1,151 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.FedSockets.OutgoingHandler do
- use GenServer
-
- require Logger
-
- alias Pleroma.Application
- alias Pleroma.Web.ActivityPub.InternalFetchActor
- alias Pleroma.Web.FedSockets
- alias Pleroma.Web.FedSockets.FedRegistry
- alias Pleroma.Web.FedSockets.FedSocket
- alias Pleroma.Web.FedSockets.SocketInfo
-
- def start_link(uri) do
- GenServer.start_link(__MODULE__, %{uri: uri})
- end
-
- def init(%{uri: uri}) do
- case initiate_connection(uri) do
- {:ok, ws_origin, conn_pid} ->
- FedRegistry.add_fed_socket(ws_origin, conn_pid)
-
- {:error, reason} ->
- Logger.debug("Outgoing connection failed - #{inspect(reason)}")
- :ignore
- end
- end
-
- def handle_info({:gun_ws, conn_pid, _ref, {:text, data}}, socket_info) do
- socket_info = SocketInfo.touch(socket_info)
-
- case FedSocket.receive_package(socket_info, data) do
- {:noreply, _} ->
- {:noreply, socket_info}
-
- {:reply, reply} ->
- :gun.ws_send(conn_pid, {:text, Jason.encode!(reply)})
- {:noreply, socket_info}
-
- {:error, reason} ->
- Logger.error("incoming error - receive_package: #{inspect(reason)}")
- {:noreply, socket_info}
- end
- end
-
- def handle_info(:close, state) do
- Logger.debug("Sending close frame !!!!!!!")
- {:close, state}
- end
-
- def handle_info({:gun_down, _pid, _prot, :closed, _}, state) do
- {:stop, :normal, state}
- end
-
- def handle_info({:send, data}, %{conn_pid: conn_pid} = socket_info) do
- socket_info = SocketInfo.touch(socket_info)
- :gun.ws_send(conn_pid, {:text, data})
- {:noreply, socket_info}
- end
-
- def handle_info({:gun_ws, _, _, :pong}, state) do
- {:noreply, state, :hibernate}
- end
-
- def handle_info(msg, state) do
- Logger.debug("#{__MODULE__} unhandled event #{inspect(msg)}")
- {:noreply, state}
- end
-
- def terminate(reason, state) do
- Logger.debug(
- "#{__MODULE__} terminating outgoing connection for #{inspect(state)} for #{inspect(reason)}"
- )
-
- {:ok, state}
- end
-
- def initiate_connection(uri) do
- ws_uri =
- uri
- |> SocketInfo.origin()
- |> FedSockets.uri_for_origin()
-
- %{host: host, port: port, path: path} = URI.parse(ws_uri)
-
- with {:ok, conn_pid} <- :gun.open(to_charlist(host), port, %{protocols: [:http]}),
- {:ok, _} <- :gun.await_up(conn_pid),
- reference <-
- :gun.get(conn_pid, to_charlist(path), [
- {'user-agent', to_charlist(Application.user_agent())}
- ]),
- {:response, :fin, 204, _} <- :gun.await(conn_pid, reference),
- headers <- build_headers(uri),
- ref <- :gun.ws_upgrade(conn_pid, to_charlist(path), headers, %{silence_pings: false}) do
- receive do
- {:gun_upgrade, ^conn_pid, ^ref, [<<"websocket">>], _} ->
- {:ok, ws_uri, conn_pid}
- after
- 15_000 ->
- Logger.debug("Fedsocket timeout connecting to #{inspect(uri)}")
- {:error, :timeout}
- end
- else
- {:response, :nofin, 404, _} ->
- {:error, :fedsockets_not_supported}
-
- e ->
- Logger.debug("Fedsocket error connecting to #{inspect(uri)}")
- {:error, e}
- end
- end
-
- defp build_headers(uri) do
- host_for_sig = uri |> URI.parse() |> host_signature()
-
- shake = FedSocket.shake()
- digest = "SHA-256=" <> (:crypto.hash(:sha256, shake) |> Base.encode64())
- date = Pleroma.Signature.signed_date()
- shake_size = byte_size(shake)
-
- signature_opts = %{
- "(request-target)": shake,
- "content-length": to_charlist("#{shake_size}"),
- date: date,
- digest: digest,
- host: host_for_sig
- }
-
- signature = Pleroma.Signature.sign(InternalFetchActor.get_actor(), signature_opts)
-
- [
- {'signature', to_charlist(signature)},
- {'date', date},
- {'digest', to_charlist(digest)},
- {'content-length', to_charlist("#{shake_size}")},
- {to_charlist("(request-target)"), to_charlist(shake)},
- {'user-agent', to_charlist(Application.user_agent())}
- ]
- end
-
- defp host_signature(%{host: host, scheme: scheme, port: port}) do
- if port == URI.default_port(scheme) do
- host
- else
- "#{host}:#{port}"
- end
- end
-end
diff --git a/lib/pleroma/web/fed_sockets/socket_info.ex b/lib/pleroma/web/fed_sockets/socket_info.ex
deleted file mode 100644
index d6fdffe1a..000000000
--- a/lib/pleroma/web/fed_sockets/socket_info.ex
+++ /dev/null
@@ -1,52 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.FedSockets.SocketInfo do
- defstruct origin: nil,
- pid: nil,
- conn_pid: nil,
- state: :default,
- connected_until: nil
-
- alias Pleroma.Web.FedSockets.SocketInfo
- @default_connection_duration 15 * 60 * 1000
-
- def build(uri, conn_pid \\ nil) do
- uri
- |> build_origin()
- |> build_pids(conn_pid)
- |> touch()
- end
-
- def touch(%SocketInfo{} = socket_info),
- do: %{socket_info | connected_until: new_ttl()}
-
- def connect(%SocketInfo{} = socket_info),
- do: %{socket_info | state: :connected}
-
- def expired?(%{connected_until: connected_until}),
- do: connected_until < :erlang.monotonic_time(:millisecond)
-
- def origin(uri),
- do: build_origin(uri).origin
-
- defp build_pids(socket_info, conn_pid),
- do: struct(socket_info, pid: self(), conn_pid: conn_pid)
-
- defp build_origin(uri) when is_binary(uri),
- do: uri |> URI.parse() |> build_origin
-
- defp build_origin(%{host: host, port: nil, scheme: scheme}),
- do: build_origin(%{host: host, port: URI.default_port(scheme)})
-
- defp build_origin(%{host: host, port: port}),
- do: %SocketInfo{origin: "#{host}:#{port}"}
-
- defp new_ttl do
- connection_duration =
- Pleroma.Config.get([:fed_sockets, :connection_duration], @default_connection_duration)
-
- :erlang.monotonic_time(:millisecond) + connection_duration
- end
-end
diff --git a/lib/pleroma/web/fed_sockets/supervisor.ex b/lib/pleroma/web/fed_sockets/supervisor.ex
deleted file mode 100644
index a5f4bebfb..000000000
--- a/lib/pleroma/web/fed_sockets/supervisor.ex
+++ /dev/null
@@ -1,59 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.FedSockets.Supervisor do
- use Supervisor
- import Cachex.Spec
-
- def start_link(opts) do
- Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
- end
-
- def init(args) do
- children = [
- build_cache(:fed_socket_fetches, args),
- build_cache(:fed_socket_rejections, args),
- {Registry, keys: :unique, name: FedSockets.Registry, meta: [rejected: %{}]}
- ]
-
- opts = [strategy: :one_for_all, name: Pleroma.Web.Streamer.Supervisor]
- Supervisor.init(children, opts)
- end
-
- defp build_cache(name, args) do
- opts = get_opts(name, args)
-
- %{
- id: String.to_atom("#{name}_cache"),
- start: {Cachex, :start_link, [name, opts]},
- type: :worker
- }
- end
-
- defp get_opts(cache_name, args)
- when cache_name in [:fed_socket_fetches, :fed_socket_rejections] do
- default = get_opts_or_config(args, cache_name, :default, 15_000)
- interval = get_opts_or_config(args, cache_name, :interval, 3_000)
- lazy = get_opts_or_config(args, cache_name, :lazy, false)
-
- [expiration: expiration(default: default, interval: interval, lazy: lazy)]
- end
-
- defp get_opts(name, args) do
- Keyword.get(args, name, [])
- end
-
- defp get_opts_or_config(args, name, key, default) do
- args
- |> Keyword.get(name, [])
- |> Keyword.get(key)
- |> case do
- nil ->
- Pleroma.Config.get([:fed_sockets, name, key], default)
-
- value ->
- value
- end
- end
-end
diff --git a/lib/pleroma/web/federator.ex b/lib/pleroma/web/federator.ex
index 130654145..f5ef76d32 100644
--- a/lib/pleroma/web/federator.ex
+++ b/lib/pleroma/web/federator.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Federator do
@@ -15,6 +15,8 @@ defmodule Pleroma.Web.Federator do
require Logger
+ @behaviour Pleroma.Web.Federator.Publishing
+
@doc """
Returns `true` if the distance to target object does not exceed max configured value.
Serves to prevent fetching of very long threads, especially useful on smaller instances.
@@ -39,10 +41,12 @@ defmodule Pleroma.Web.Federator do
ReceiverWorker.enqueue("incoming_ap_doc", %{"params" => params})
end
+ @impl true
def publish(%{id: "pleroma:fakeid"} = activity) do
perform(:publish, activity)
end
+ @impl true
def publish(activity) do
PublisherWorker.enqueue("publish", %{"activity_id" => activity.id})
end
diff --git a/lib/pleroma/web/federator/publisher.ex b/lib/pleroma/web/federator/publisher.ex
index ad0201361..b7ee56803 100644
--- a/lib/pleroma/web/federator/publisher.ex
+++ b/lib/pleroma/web/federator/publisher.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Federator.Publisher do
diff --git a/lib/pleroma/web/federator/publishing.ex b/lib/pleroma/web/federator/publishing.ex
new file mode 100644
index 000000000..fe7805be9
--- /dev/null
+++ b/lib/pleroma/web/federator/publishing.ex
@@ -0,0 +1,7 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Federator.Publishing do
+ @callback publish(map()) :: any()
+end
diff --git a/lib/pleroma/web/feed/feed_view.ex b/lib/pleroma/web/feed/feed_view.ex
index 1ae03e7e2..df97d2f46 100644
--- a/lib/pleroma/web/feed/feed_view.ex
+++ b/lib/pleroma/web/feed/feed_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Feed.FeedView do
@@ -23,7 +23,7 @@ defmodule Pleroma.Web.Feed.FeedView do
def pub_date(%DateTime{} = date), do: Timex.format!(date, "{RFC822}")
def prepare_activity(activity, opts \\ []) do
- object = Object.normalize(activity)
+ object = Object.normalize(activity, fetch: false)
actor =
if opts[:actor] do
@@ -51,7 +51,7 @@ defmodule Pleroma.Web.Feed.FeedView do
def feed_logo do
case Pleroma.Config.get([:feed, :logo]) do
nil ->
- "#{Pleroma.Web.base_url()}/static/logo.png"
+ "#{Pleroma.Web.base_url()}/static/logo.svg"
logo ->
"#{Pleroma.Web.base_url()}#{logo}"
@@ -83,7 +83,7 @@ defmodule Pleroma.Web.Feed.FeedView do
def activity_content(_), do: ""
- def activity_context(activity), do: activity.data["context"]
+ def activity_context(activity), do: escape(activity.data["context"])
def attachment_href(attachment) do
attachment["url"]
diff --git a/lib/pleroma/web/feed/tag_controller.ex b/lib/pleroma/web/feed/tag_controller.ex
index 218cdbdf3..ef9293a55 100644
--- a/lib/pleroma/web/feed/tag_controller.ex
+++ b/lib/pleroma/web/feed/tag_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Feed.TagController do
diff --git a/lib/pleroma/web/feed/user_controller.ex b/lib/pleroma/web/feed/user_controller.ex
index a5013d2c0..58d35da1e 100644
--- a/lib/pleroma/web/feed/user_controller.ex
+++ b/lib/pleroma/web/feed/user_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Feed.UserController do
diff --git a/lib/pleroma/web/gettext.ex b/lib/pleroma/web/gettext.ex
index 0adf428ec..c0ca4d0e9 100644
--- a/lib/pleroma/web/gettext.ex
+++ b/lib/pleroma/web/gettext.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Gettext do
diff --git a/lib/pleroma/web/instance_document.ex b/lib/pleroma/web/instance_document.ex
index df5caebf0..a33bf605b 100644
--- a/lib/pleroma/web/instance_document.ex
+++ b/lib/pleroma/web/instance_document.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.InstanceDocument do
diff --git a/lib/pleroma/web/mailer/subscription_controller.ex b/lib/pleroma/web/mailer/subscription_controller.ex
index ace44afd1..f89abe46a 100644
--- a/lib/pleroma/web/mailer/subscription_controller.ex
+++ b/lib/pleroma/web/mailer/subscription_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Mailer.SubscriptionController do
diff --git a/lib/pleroma/web/masto_fe_controller.ex b/lib/pleroma/web/masto_fe_controller.ex
index 08f92d55f..e788ab37a 100644
--- a/lib/pleroma/web/masto_fe_controller.ex
+++ b/lib/pleroma/web/masto_fe_controller.ex
@@ -1,11 +1,13 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastoFEController do
use Pleroma.Web, :controller
alias Pleroma.User
+ alias Pleroma.Web.MastodonAPI.AuthController
+ alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Web.Plugs.OAuthScopesPlug
@@ -26,27 +28,27 @@ defmodule Pleroma.Web.MastoFEController do
)
@doc "GET /web/*path"
- def index(%{assigns: %{user: user, token: token}} = conn, _params)
- when not is_nil(user) and not is_nil(token) do
- conn
- |> put_layout(false)
- |> render("index.html",
- token: token.token,
- user: user,
- custom_emojis: Pleroma.Emoji.get_all()
- )
- end
-
def index(conn, _params) do
- conn
- |> put_session(:return_to, conn.request_path)
- |> redirect(to: "/web/login")
+ with %{assigns: %{user: %User{} = user, token: %Token{app_id: token_app_id} = token}} <- conn,
+ {:ok, %{id: ^token_app_id}} <- AuthController.local_mastofe_app() do
+ conn
+ |> put_layout(false)
+ |> render("index.html",
+ token: token.token,
+ user: user,
+ custom_emojis: Pleroma.Emoji.get_all()
+ )
+ else
+ _ ->
+ conn
+ |> put_session(:return_to, conn.request_path)
+ |> redirect(to: "/web/login")
+ end
end
@doc "GET /web/manifest.json"
def manifest(conn, _params) do
- conn
- |> render("manifest.json")
+ render(conn, "manifest.json")
end
@doc "PUT /api/web/settings: Backend-obscure settings blob for MastoFE, don't parse/reuse elsewhere"
diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
index 97858a93c..7a1e99044 100644
--- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.AccountController do
@@ -25,7 +25,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
alias Pleroma.Web.MastodonAPI.MastodonAPIController
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.OAuth.OAuthController
- alias Pleroma.Web.OAuth.OAuthView
alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.Plugs.RateLimiter
@@ -103,7 +102,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
{:ok, user} <- TwitterAPI.register_user(params),
{_, {:ok, token}} <-
{:login, OAuthController.login(user, app, app.scopes)} do
- json(conn, OAuthView.render("token.json", %{user: user, token: token}))
+ OAuthController.after_token_exchange(conn, %{user: user, token: token})
else
{:login, {:account_status, :confirmation_pending}} ->
json_response(conn, :ok, %{
@@ -185,7 +184,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
:show_role,
:skip_thread_containment,
:allow_following_move,
- :discoverable,
+ :also_known_as,
:accepts_chat_messages
]
|> Enum.reduce(%{}, fn key, acc ->
@@ -209,7 +208,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
if bot, do: {:ok, "Service"}, else: {:ok, "Person"}
end)
|> Maps.put_if_present(:actor_type, params[:actor_type])
+ |> Maps.put_if_present(:also_known_as, params[:also_known_as])
+ # Note: param name is indeed :locked (not an error)
|> Maps.put_if_present(:is_locked, params[:locked])
+ # Note: param name is indeed :discoverable (not an error)
+ |> Maps.put_if_present(:is_discoverable, params[:discoverable])
# What happens here:
#
@@ -266,10 +269,14 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
@doc "GET /api/v1/accounts/:id"
- def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
+ def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id} = 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", user: user, for: for_user)
+ render(conn, "show.json",
+ user: user,
+ for: for_user,
+ embed_relationships: embed_relationships?(params)
+ )
else
error -> user_visibility_error(conn, error)
end
@@ -292,7 +299,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|> render("index.json",
activities: activities,
for: reading_user,
- as: :activity
+ as: :activity,
+ with_muted: Map.get(params, :with_muted, false)
)
else
error -> user_visibility_error(conn, error)
@@ -394,7 +402,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
@doc "POST /api/v1/accounts/:id/mute"
def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
- with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
+ with {:ok, _user_relationships} <- User.mute(muter, muted, params) do
render(conn, "relationship.json", user: muter, target: muted)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
@@ -442,15 +450,32 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
@doc "GET /api/v1/mutes"
- def mutes(%{assigns: %{user: user}} = conn, _) do
- users = User.muted_users(user, _restrict_deactivated = true)
- render(conn, "index.json", users: users, for: user, as: :user)
+ def mutes(%{assigns: %{user: user}} = conn, params) do
+ users =
+ user
+ |> User.muted_users_relation(_restrict_deactivated = true)
+ |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true))
+
+ conn
+ |> add_link_headers(users)
+ |> render("index.json",
+ users: users,
+ for: user,
+ as: :user,
+ embed_relationships: embed_relationships?(params)
+ )
end
@doc "GET /api/v1/blocks"
- def blocks(%{assigns: %{user: user}} = conn, _) do
- users = User.blocked_users(user, _restrict_deactivated = true)
- render(conn, "index.json", users: users, for: user, as: :user)
+ def blocks(%{assigns: %{user: user}} = conn, params) do
+ users =
+ user
+ |> User.blocked_users_relation(_restrict_deactivated = true)
+ |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true))
+
+ conn
+ |> add_link_headers(users)
+ |> render("index.json", users: users, for: user, as: :user)
end
@doc "GET /api/v1/endorsements"
diff --git a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex
index 143dcf80c..dd3b39c77 100644
--- a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex
@@ -1,8 +1,13 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.AppController do
+ @moduledoc """
+ Controller for supporting app-related actions.
+ If authentication is an option, app tokens (user-unbound) must be supported.
+ """
+
use Pleroma.Web, :controller
alias Pleroma.Repo
@@ -17,11 +22,9 @@ defmodule Pleroma.Web.MastodonAPI.AppController do
plug(
:skip_plug,
[OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug]
- when action == :create
+ when action in [:create, :verify_credentials]
)
- plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :verify_credentials)
-
plug(Pleroma.Web.ApiSpec.CastAndValidate)
@local_mastodon_name "Mastodon-Local"
@@ -44,10 +47,13 @@ defmodule Pleroma.Web.MastodonAPI.AppController do
end
end
- @doc "GET /api/v1/apps/verify_credentials"
- def verify_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
- with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
- render(conn, "short.json", app: app)
+ @doc """
+ GET /api/v1/apps/verify_credentials
+ Gets compact non-secret representation of the app. Supports app tokens and user tokens.
+ """
+ def verify_credentials(%{assigns: %{token: %Token{} = token}} = conn, _) do
+ with %{app: %App{} = app} <- Repo.preload(token, :app) do
+ render(conn, "compact_non_secret.json", app: app)
end
end
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
index 9cc3984d0..eb6639fc5 100644
--- a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.AuthController do
@@ -7,10 +7,13 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
+ alias Pleroma.Helpers.AuthHelper
+ alias Pleroma.Helpers.UriHelper
alias Pleroma.User
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.Token
+ alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken
alias Pleroma.Web.TwitterAPI.TwitterAPI
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
@@ -20,24 +23,35 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
@local_mastodon_name "Mastodon-Local"
@doc "GET /web/login"
- def login(%{assigns: %{user: %User{}}} = conn, _params) do
- redirect(conn, to: local_mastodon_root_path(conn))
- end
-
- # Local Mastodon FE login init action
- def login(conn, %{"code" => auth_token}) do
- with {:ok, app} <- get_or_make_app(),
+ # Local Mastodon FE login callback action
+ def login(conn, %{"code" => auth_token} = params) do
+ with {:ok, app} <- local_mastofe_app(),
{:ok, auth} <- Authorization.get_by_token(app, auth_token),
- {:ok, token} <- Token.exchange_token(app, auth) do
+ {:ok, oauth_token} <- Token.exchange_token(app, auth) do
+ redirect_to =
+ conn
+ |> local_mastodon_post_login_path()
+ |> UriHelper.modify_uri_params(%{"access_token" => oauth_token.token})
+
conn
- |> put_session(:oauth_token, token.token)
- |> redirect(to: local_mastodon_root_path(conn))
+ |> AuthHelper.put_session_token(oauth_token.token)
+ |> redirect(to: redirect_to)
+ else
+ _ -> redirect_to_oauth_form(conn, params)
+ end
+ end
+
+ def login(conn, params) do
+ with %{assigns: %{user: %User{}, token: %Token{app_id: app_id}}} <- conn,
+ {:ok, %{id: ^app_id}} <- local_mastofe_app() do
+ redirect(conn, to: local_mastodon_post_login_path(conn))
+ else
+ _ -> redirect_to_oauth_form(conn, params)
end
end
- # Local Mastodon FE callback action
- def login(conn, _) do
- with {:ok, app} <- get_or_make_app() do
+ defp redirect_to_oauth_form(conn, _params) do
+ with {:ok, app} <- local_mastofe_app() do
path =
o_auth_path(conn, :authorize,
response_type: "code",
@@ -52,9 +66,16 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
@doc "DELETE /auth/sign_out"
def logout(conn, _) do
- conn
- |> clear_session
- |> redirect(to: "/")
+ conn =
+ with %{assigns: %{token: %Token{} = oauth_token}} <- conn,
+ session_token = AuthHelper.get_session_token(conn),
+ {:ok, %Token{token: ^session_token}} <- RevokeToken.revoke(oauth_token) do
+ AuthHelper.delete_session_token(conn)
+ else
+ _ -> conn
+ end
+
+ redirect(conn, to: "/")
end
@doc "POST /auth/password"
@@ -66,7 +87,7 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
json_response(conn, :no_content, "")
end
- defp local_mastodon_root_path(conn) do
+ defp local_mastodon_post_login_path(conn) do
case get_session(conn, :return_to) do
nil ->
masto_fe_path(conn, :index, ["getting-started"])
@@ -77,9 +98,11 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
end
end
- @spec get_or_make_app() :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
- defp get_or_make_app do
- %{client_name: @local_mastodon_name, redirect_uris: "."}
- |> App.get_or_make(["read", "write", "follow", "push", "admin"])
+ @spec local_mastofe_app() :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
+ def local_mastofe_app do
+ App.get_or_make(
+ %{client_name: @local_mastodon_name, redirect_uris: "."},
+ ["read", "write", "follow", "push", "admin"]
+ )
end
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex
index 61347d8db..f2a0949e8 100644
--- a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ConversationController do
@@ -36,4 +36,13 @@ defmodule Pleroma.Web.MastodonAPI.ConversationController do
render(conn, "participation.json", participation: participation, for: user)
end
end
+
+ @doc "DELETE /api/v1/conversations/:id"
+ def delete(%{assigns: %{user: user}} = conn, %{id: participation_id}) do
+ with %Participation{} = participation <-
+ Repo.get_by(Participation, id: participation_id, user_id: user.id),
+ {:ok, _} <- Participation.delete(participation) do
+ json(conn, %{})
+ end
+ end
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex b/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex
index 872cb1f4d..d7e18dc92 100644
--- a/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.CustomEmojiController do
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 503bd7d5f..30300307d 100644
--- a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.DomainBlockController do
diff --git a/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex b/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex
index 8af557b61..d25f84837 100644
--- a/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.FallbackController do
diff --git a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
index c71a34b15..9b1ae809d 100644
--- a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.FilterController do
@@ -20,6 +20,8 @@ defmodule Pleroma.Web.MastodonAPI.FilterController do
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.FilterOperation
+ action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+
@doc "GET /api/v1/filters"
def index(%{assigns: %{user: user}} = conn, _) do
filters = Filter.get_filters(user)
@@ -29,25 +31,23 @@ defmodule Pleroma.Web.MastodonAPI.FilterController do
@doc "POST /api/v1/filters"
def create(%{assigns: %{user: user}, body_params: params} = conn, _) do
- query = %Filter{
- user_id: user.id,
- phrase: params.phrase,
- context: params.context,
- hide: params.irreversible,
- whole_word: params.whole_word
- # TODO: support `expires_in` parameter (as in Mastodon API)
- }
-
- {:ok, response} = Filter.create(query)
-
- render(conn, "show.json", filter: response)
+ with {:ok, response} <-
+ params
+ |> Map.put(:user_id, user.id)
+ |> Map.put(:hide, params[:irreversible])
+ |> Map.delete(:irreversible)
+ |> Filter.create() do
+ render(conn, "show.json", filter: response)
+ end
end
@doc "GET /api/v1/filters/:id"
def show(%{assigns: %{user: user}} = conn, %{id: filter_id}) do
- filter = Filter.get(filter_id, user)
-
- render(conn, "show.json", filter: filter)
+ with %Filter{} = filter <- Filter.get(filter_id, user) do
+ render(conn, "show.json", filter: filter)
+ else
+ nil -> {:error, :not_found}
+ end
end
@doc "PUT /api/v1/filters/:id"
@@ -56,28 +56,31 @@ defmodule Pleroma.Web.MastodonAPI.FilterController do
%{id: filter_id}
) do
params =
- params
- |> Map.delete(:irreversible)
- |> Map.put(:hide, params[:irreversible])
- |> Enum.reject(fn {_key, value} -> is_nil(value) end)
- |> Map.new()
-
- # TODO: support `expires_in` parameter (as in Mastodon API)
+ if is_boolean(params[:irreversible]) do
+ params
+ |> Map.put(:hide, params[:irreversible])
+ |> Map.delete(:irreversible)
+ else
+ params
+ end
with %Filter{} = filter <- Filter.get(filter_id, user),
{:ok, %Filter{} = filter} <- Filter.update(filter, params) do
render(conn, "show.json", filter: filter)
+ else
+ nil -> {:error, :not_found}
+ error -> error
end
end
@doc "DELETE /api/v1/filters/:id"
def delete(%{assigns: %{user: user}} = conn, %{id: filter_id}) do
- query = %Filter{
- user_id: user.id,
- filter_id: filter_id
- }
-
- {:ok, _} = Filter.delete(query)
- json(conn, %{})
+ with %Filter{} = filter <- Filter.get(filter_id, user),
+ {:ok, _} <- Filter.delete(filter) do
+ json(conn, %{})
+ else
+ nil -> {:error, :not_found}
+ error -> error
+ end
end
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 f8cd7fa9f..63d0e2c35 100644
--- a/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.FollowRequestController do
diff --git a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex
index 07a32491a..267d0f03b 100644
--- a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.InstanceController do
diff --git a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex
index f6b51bf02..b7b41f449 100644
--- a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ListController do
diff --git a/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex b/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex
index 0628b2b49..c745f3493 100644
--- a/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.MarkerController do
diff --git a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
index 9cf682c7b..a1bcc91d9 100644
--- a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
diff --git a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex
index 161193134..d6949ed80 100644
--- a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.MediaController do
diff --git a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
index c3c8606f2..647ba661e 100644
--- a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.NotificationController do
diff --git a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex
index 3dcd1c44f..f44ff997d 100644
--- a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.PollController do
@@ -26,6 +26,8 @@ defmodule Pleroma.Web.MastodonAPI.PollController do
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PollOperation
+ @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
+
@doc "GET /api/v1/polls/:id"
def show(%{assigns: %{user: user}} = conn, %{id: id}) do
with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
@@ -55,7 +57,7 @@ defmodule Pleroma.Web.MastodonAPI.PollController do
defp get_cached_vote_or_vote(user, object, choices) do
idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
- Cachex.fetch!(:idempotency_cache, idempotency_key, fn ->
+ @cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
case CommonAPI.vote(user, object, choices) do
{:error, _message} = res -> {:ignore, res}
res -> {:commit, res}
diff --git a/lib/pleroma/web/mastodon_api/controllers/report_controller.ex b/lib/pleroma/web/mastodon_api/controllers/report_controller.ex
index 156544f40..03d9a4f4f 100644
--- a/lib/pleroma/web/mastodon_api/controllers/report_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/report_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ReportController do
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 322a46497..3b7a0c788 100644
--- a/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ScheduledActivityController do
diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
index 0043c3a56..af93e453d 100644
--- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.SearchController do
diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
index 6848adace..d1a58d5e1 100644
--- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.StatusController do
@@ -21,6 +21,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.ScheduledActivityView
+ # alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.Plugs.RateLimiter
@@ -109,7 +110,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
`ids` query param is required
"""
- def index(%{assigns: %{user: user}} = conn, %{ids: ids} = _params) do
+ def index(%{assigns: %{user: user}} = conn, %{ids: ids} = params) do
limit = 100
activities =
@@ -121,7 +122,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
render(conn, "index.json",
activities: activities,
for: user,
- as: :activity
+ as: :activity,
+ with_muted: Map.get(params, :with_muted, false)
)
end
@@ -137,7 +139,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
_
)
when not is_nil(scheduled_at) do
- params = Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
+ params =
+ Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
+ |> put_application(conn)
attrs = %{
params: Map.new(params, fn {key, value} -> {to_string(key), value} end),
@@ -161,7 +165,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
# Creates a regular status
def create(%{assigns: %{user: user}, body_params: %{status: _} = params} = conn, _) do
- params = Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
+ params =
+ Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
+ |> put_application(conn)
with {:ok, activity} <- CommonAPI.post(user, params) do
try_render(conn, "show.json",
@@ -189,13 +195,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "GET /api/v1/statuses/:id"
- def show(%{assigns: %{user: user}} = conn, %{id: id}) do
+ def show(%{assigns: %{user: user}} = conn, %{id: id} = params) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
true <- Visibility.visible_for_user?(activity, user) do
try_render(conn, "show.json",
activity: activity,
for: user,
- with_direct_conversation_id: true
+ with_direct_conversation_id: true,
+ with_muted: Map.get(params, :with_muted, false)
)
else
_ -> {:error, :not_found}
@@ -284,9 +291,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "POST /api/v1/statuses/:id/mute"
- def mute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
+ def mute_conversation(%{assigns: %{user: user}, body_params: params} = conn, %{id: id}) do
with %Activity{} = activity <- Activity.get_by_id(id),
- {:ok, activity} <- CommonAPI.add_mute(user, activity) do
+ {:ok, activity} <- CommonAPI.add_mute(user, activity, params) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end
@@ -316,7 +323,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
with true <- Pleroma.Config.get([:instance, :show_reactions]),
%Activity{} = activity <- Activity.get_by_id_with_object(id),
{:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
- %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
+ %Object{data: %{"likes" => likes}} <- Object.normalize(activity, fetch: false) do
users =
User
|> Ecto.Query.where([u], u.ap_id in ^likes)
@@ -337,7 +344,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController 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}} <-
- Object.normalize(activity) do
+ Object.normalize(activity, fetch: false) do
announces =
"Announce"
|> Activity.Queries.by_type()
@@ -412,4 +419,17 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
as: :activity
)
end
+
+ # Deactivated for 2.3.0
+ # defp put_application(params,
+ # %{assigns: %{token: %Token{user: %User{} = user} = token}} = _conn) do
+ # if user.disclose_client do
+ # %{client_name: client_name, website: website} = Repo.preload(token, :app).app
+ # Map.put(params, :generator, %{type: "Application", name: client_name, url: website})
+ # else
+ # Map.put(params, :generator, nil)
+ # end
+ # end
+
+ defp put_application(params, _), do: Map.put(params, :generator, nil)
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex b/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex
index 20138908c..fcb3d4829 100644
--- a/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.SubscriptionController do
diff --git a/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex b/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex
index 5765271cf..01e122dd9 100644
--- a/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.SuggestionController do
diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
index 7a5c80e01..cef299aa4 100644
--- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.TimelineController do
@@ -51,6 +51,8 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|> Map.put(:reply_filtering_user, user)
|> Map.put(:announce_filtering_user, user)
|> Map.put(:user, user)
+ |> Map.put(:local_only, params[:local])
+ |> Map.delete(:local)
activities =
[user.ap_id | User.following(user)]
@@ -62,7 +64,8 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|> render("index.json",
activities: activities,
for: user,
- as: :activity
+ as: :activity,
+ with_muted: Map.get(params, :with_muted, false)
)
end
@@ -111,6 +114,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|> Map.put(:blocking_user, user)
|> Map.put(:muting_user, user)
|> Map.put(:reply_filtering_user, user)
+ |> Map.put(:instance, params[:instance])
|> ActivityPub.fetch_public_activities()
conn
@@ -118,7 +122,8 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|> render("index.json",
activities: activities,
for: user,
- as: :activity
+ as: :activity,
+ with_muted: Map.get(params, :with_muted, false)
)
end
end
@@ -172,7 +177,8 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|> render("index.json",
activities: activities,
for: user,
- as: :activity
+ as: :activity,
+ with_muted: Map.get(params, :with_muted, false)
)
end
end
@@ -186,6 +192,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|> Map.put(:blocking_user, user)
|> Map.put(:user, user)
|> Map.put(:muting_user, user)
+ |> Map.put(:local_only, params[:local])
# we must filter the following list for the user to avoid leaking statuses the user
# does not actually have permission to see (for more info, peruse security issue #270).
@@ -201,7 +208,8 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
render(conn, "index.json",
activities: activities,
for: user,
- as: :activity
+ as: :activity,
+ with_muted: Map.get(params, :with_muted, false)
)
else
_e -> render_error(conn, :forbidden, "Error.")
diff --git a/lib/pleroma/web/mastodon_api/mastodon_api.ex b/lib/pleroma/web/mastodon_api/mastodon_api.ex
index 694bf5ca8..71479550e 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index d54cae732..ac25aefdd 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-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.AccountView do
@@ -187,18 +187,14 @@ 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 do
- user.following_count || 0
- else
- 0
- end
+ if !user.hide_follows_count or !user.hide_follows or opts[:for] == user,
+ do: user.following_count,
+ else: 0
followers_count =
- if !user.hide_followers_count or !user.hide_followers or opts[:for] == user do
- user.follower_count || 0
- else
- 0
- end
+ if !user.hide_followers_count or !user.hide_followers or opts[:for] == user,
+ do: user.follower_count,
+ else: 0
bot = user.actor_type == "Service"
@@ -261,15 +257,18 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
sensitive: false,
fields: user.raw_fields,
pleroma: %{
- discoverable: user.discoverable,
+ discoverable: user.is_discoverable,
actor_type: user.actor_type
}
},
- # Pleroma extension
+ # Pleroma extensions
+ # Note: it's insecure to output :email but fully-qualified nickname may serve as safe stub
+ fqn: User.full_nickname(user),
pleroma: %{
ap_id: user.ap_id,
- confirmation_pending: user.confirmation_pending,
+ also_known_as: user.also_known_as,
+ is_confirmed: user.is_confirmed,
tags: user.tags,
hide_followers_count: user.hide_followers_count,
hide_follows_count: user.hide_follows_count,
@@ -379,7 +378,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
defp maybe_put_allow_following_move(data, _, _), do: data
defp maybe_put_activation_status(data, user, %User{is_admin: true}) do
- Kernel.put_in(data, [:pleroma, :deactivated], user.deactivated)
+ Kernel.put_in(data, [:pleroma, :deactivated], !user.is_active)
end
defp maybe_put_activation_status(data, _, _), do: data
@@ -388,7 +387,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
data
|> Kernel.put_in(
[:pleroma, :unread_conversation_count],
- user.unread_conversation_count
+ Pleroma.Conversation.Participation.unread_count(user)
)
end
diff --git a/lib/pleroma/web/mastodon_api/views/app_view.ex b/lib/pleroma/web/mastodon_api/views/app_view.ex
index e44272c6f..c406b5a27 100644
--- a/lib/pleroma/web/mastodon_api/views/app_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/app_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.AppView do
@@ -34,10 +34,10 @@ defmodule Pleroma.Web.MastodonAPI.AppView do
|> with_vapid_key()
end
- def render("short.json", %{app: %App{website: webiste, client_name: name}}) do
+ def render("compact_non_secret.json", %{app: %App{website: website, client_name: name}}) do
%{
name: name,
- website: webiste
+ website: website
}
|> with_vapid_key()
end
diff --git a/lib/pleroma/web/mastodon_api/views/conversation_view.ex b/lib/pleroma/web/mastodon_api/views/conversation_view.ex
index a91994915..46b63b54b 100644
--- a/lib/pleroma/web/mastodon_api/views/conversation_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/conversation_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ConversationView do
@@ -33,8 +33,15 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do
end
activity = Activity.get_by_id_with_object(last_activity_id)
- # Conversations return all users except the current user.
- users = Enum.reject(participation.recipients, &(&1.id == user.id))
+
+ # Conversations return all users except the current user,
+ # except when the current user is the only participant
+ users =
+ if length(participation.recipients) > 1 do
+ Enum.reject(participation.recipients, &(&1.id == user.id))
+ else
+ participation.recipients
+ end
%{
id: participation.id |> to_string(),
@@ -43,7 +50,8 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do
last_status:
render(StatusView, "show.json",
activity: activity,
- direct_conversation_id: participation.id
+ direct_conversation_id: participation.id,
+ for: user
)
}
end
diff --git a/lib/pleroma/web/mastodon_api/views/custom_emoji_view.ex b/lib/pleroma/web/mastodon_api/views/custom_emoji_view.ex
index 47a242b8e..40e314164 100644
--- a/lib/pleroma/web/mastodon_api/views/custom_emoji_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/custom_emoji_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.CustomEmojiView do
diff --git a/lib/pleroma/web/mastodon_api/views/filter_view.ex b/lib/pleroma/web/mastodon_api/views/filter_view.ex
index c37f624e0..8e8798c1e 100644
--- a/lib/pleroma/web/mastodon_api/views/filter_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/filter_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.FilterView do
diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex
index ea2d3aa9c..73205fb6d 100644
--- a/lib/pleroma/web/mastodon_api/views/instance_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.InstanceView do
@@ -23,7 +23,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
streaming_api: Pleroma.Web.Endpoint.websocket_url()
},
stats: Pleroma.Stats.get_stats(),
- thumbnail: Keyword.get(instance, :instance_thumbnail),
+ thumbnail: Pleroma.Web.base_url() <> Keyword.get(instance, :instance_thumbnail),
languages: ["en"],
registrations: Keyword.get(instance, :registrations_open),
approval_required: Keyword.get(instance, :account_approval_required),
@@ -34,7 +34,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
avatar_upload_limit: Keyword.get(instance, :avatar_upload_limit),
background_upload_limit: Keyword.get(instance, :background_upload_limit),
banner_upload_limit: Keyword.get(instance, :banner_upload_limit),
- background_image: Keyword.get(instance, :background_image),
+ background_image: Pleroma.Web.base_url() <> Keyword.get(instance, :background_image),
chat_limit: Keyword.get(instance, :chat_limit),
description_limit: Keyword.get(instance, :description_limit),
pleroma: %{
@@ -45,6 +45,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
fields_limits: fields_limits(),
post_formats: Config.get([:instance, :allowed_post_formats])
},
+ stats: %{mau: Pleroma.User.active_user_count()},
vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
}
}
diff --git a/lib/pleroma/web/mastodon_api/views/list_view.ex b/lib/pleroma/web/mastodon_api/views/list_view.ex
index 580596b64..931e77769 100644
--- a/lib/pleroma/web/mastodon_api/views/list_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/list_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ListView do
diff --git a/lib/pleroma/web/mastodon_api/views/marker_view.ex b/lib/pleroma/web/mastodon_api/views/marker_view.ex
index 21d535d54..0c1880935 100644
--- a/lib/pleroma/web/mastodon_api/views/marker_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/marker_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.MarkerView do
diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex
index c97e6d32f..df9bedfed 100644
--- a/lib/pleroma/web/mastodon_api/views/notification_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.NotificationView do
@@ -11,6 +11,8 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.UserRelationship
+ alias Pleroma.Web.AdminAPI.Report
+ alias Pleroma.Web.AdminAPI.ReportView
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.NotificationView
@@ -118,17 +120,26 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
"pleroma:chat_mention" ->
put_chat_message(response, activity, reading_user, status_render_opts)
+ "pleroma:report" ->
+ put_report(response, activity)
+
type when type in ["follow", "follow_request"] ->
response
end
end
+ defp put_report(response, activity) do
+ report_render = ReportView.render("show.json", Report.extract_report_info(activity))
+
+ Map.put(response, :report, report_render)
+ end
+
defp put_emoji(response, activity) do
Map.put(response, :emoji, activity.data["content"])
end
defp put_chat_message(response, activity, reading_user, opts) do
- object = Object.normalize(activity)
+ object = Object.normalize(activity, fetch: false)
author = User.get_cached_by_ap_id(object.data["actor"])
chat = Pleroma.Chat.get(reading_user.id, author.ap_id)
cm_ref = MessageReference.for_chat_and_object(chat, object)
diff --git a/lib/pleroma/web/mastodon_api/views/poll_view.ex b/lib/pleroma/web/mastodon_api/views/poll_view.ex
index 1208dc9a0..71bc8b949 100644
--- a/lib/pleroma/web/mastodon_api/views/poll_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/poll_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.PollView do
@@ -11,7 +11,7 @@ defmodule Pleroma.Web.MastodonAPI.PollView do
{end_time, expired} = end_time_and_expired(object)
{options, votes_count} = options_and_votes_count(options)
- %{
+ poll = %{
# Mastodon uses separate ids for polls, but an object can't have
# more than one poll embedded so object id is fine
id: to_string(object.id),
@@ -19,11 +19,18 @@ defmodule Pleroma.Web.MastodonAPI.PollView do
expired: expired,
multiple: multiple,
votes_count: votes_count,
- voters_count: (multiple || nil) && voters_count(object),
+ voters_count: voters_count(object),
options: options,
- voted: voted?(params),
emojis: Pleroma.Web.MastodonAPI.StatusView.build_emojis(object.data["emoji"])
}
+
+ if params[:for] do
+ # when unauthenticated Mastodon doesn't include `voted` & `own_votes` keys in response
+ {voted, own_votes} = voted_and_own_votes(params, options)
+ Map.merge(poll, %{voted: voted, own_votes: own_votes})
+ else
+ poll
+ end
end
def render("show.json", %{object: object} = params) do
@@ -67,12 +74,29 @@ defmodule Pleroma.Web.MastodonAPI.PollView do
defp voters_count(_), do: 0
- defp voted?(%{object: object} = opts) do
- if opts[:for] do
- existing_votes = Pleroma.Web.ActivityPub.Utils.get_existing_votes(opts[:for].ap_id, object)
- existing_votes != [] or opts[:for].ap_id == object.data["actor"]
+ defp voted_and_own_votes(%{object: object} = params, options) do
+ if params[:for] do
+ existing_votes =
+ Pleroma.Web.ActivityPub.Utils.get_existing_votes(params[:for].ap_id, object)
+
+ voted = existing_votes != [] or params[:for].ap_id == object.data["actor"]
+
+ own_votes =
+ if voted do
+ titles = Enum.map(options, & &1[:title])
+
+ Enum.reduce(existing_votes, [], fn vote, acc ->
+ data = vote |> Map.get(:object) |> Map.get(:data)
+ index = Enum.find_index(titles, &(&1 == data["name"]))
+ [index | acc]
+ end)
+ else
+ []
+ end
+
+ {voted, own_votes}
else
- false
+ {false, []}
end
end
end
diff --git a/lib/pleroma/web/mastodon_api/views/report_view.ex b/lib/pleroma/web/mastodon_api/views/report_view.ex
index 98cb581ef..0ff347ade 100644
--- a/lib/pleroma/web/mastodon_api/views/report_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/report_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ReportView do
diff --git a/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex b/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex
index 5b896bf3b..453221f41 100644
--- a/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ScheduledActivityView do
@@ -37,7 +37,8 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityView do
visibility: params["visibility"],
scheduled_at: params["scheduled_at"],
poll: params["poll"],
- in_reply_to_id: params["in_reply_to_id"]
+ in_reply_to_id: params["in_reply_to_id"],
+ expires_in: params["expires_in"]
}
|> Pleroma.Maps.put_if_present(:media_ids, params["media_ids"])
end
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index 435bcde15..bac897a57 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.StatusView do
@@ -19,6 +19,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
alias Pleroma.Web.MastodonAPI.PollView
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.MediaProxy
+ alias Pleroma.Web.PleromaAPI.EmojiReactionController
import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1, visible_for_user?: 2]
@@ -40,7 +41,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
activities
|> Enum.map(fn
%{data: %{"type" => "Create"}} = activity ->
- object = Object.normalize(activity)
+ object = Object.normalize(activity, fetch: false)
object && object.data["inReplyTo"] != "" && object.data["inReplyTo"]
_ ->
@@ -50,7 +51,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|> Activity.create_by_object_ap_id_with_object()
|> Repo.all()
|> Enum.reduce(%{}, fn activity, acc ->
- object = Object.normalize(activity)
+ object = Object.normalize(activity, fetch: false)
if object, do: Map.put(acc, object.data["id"], activity), else: acc
end)
end
@@ -64,7 +65,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
defp get_context_id(_), do: nil
defp reblogged?(activity, user) do
- object = Object.normalize(activity) || %{}
+ object = Object.normalize(activity, fetch: false) || %{}
present?(user && user.ap_id in (object.data["announcements"] || []))
end
@@ -83,7 +84,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
parent_activities =
activities
|> Enum.filter(&(&1.data["type"] == "Announce" && &1.data["object"]))
- |> Enum.map(&Object.normalize(&1).data["id"])
+ |> Enum.map(&Object.normalize(&1, fetch: false).data["id"])
|> Activity.create_by_object_ap_id()
|> Activity.with_preloaded_object(:left)
|> Activity.with_preloaded_bookmark(reading_user)
@@ -123,7 +124,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
) do
user = CommonAPI.get_user(activity.data["actor"])
created_at = Utils.to_masto_date(activity.data["published"])
- activity_object = Object.normalize(activity)
+ activity_object = Object.normalize(activity, fetch: false)
reblogged_parent_activity =
if opts[:parent_activities] do
@@ -179,10 +180,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
media_attachments: reblogged[:media_attachments] || [],
mentions: mentions,
tags: reblogged[:tags] || [],
- application: %{
- name: "Web",
- website: nil
- },
+ application: build_application(activity_object.data["generator"]),
language: nil,
emojis: [],
pleroma: %{
@@ -192,7 +190,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
end
def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
- object = Object.normalize(activity)
+ object = Object.normalize(activity, fetch: false)
user = CommonAPI.get_user(activity.data["actor"])
user_follower_address = user.follower_address
@@ -294,21 +292,16 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
end
emoji_reactions =
- with %{data: %{"reactions" => emoji_reactions}} <- object do
- Enum.map(emoji_reactions, fn
- [emoji, users] when is_list(users) ->
- build_emoji_map(emoji, users, opts[:for])
-
- {emoji, users} when is_list(users) ->
- build_emoji_map(emoji, users, opts[:for])
-
- _ ->
- nil
- end)
- |> Enum.reject(&is_nil/1)
- else
- _ -> []
- end
+ object.data
+ |> Map.get("reactions", [])
+ |> EmojiReactionController.filter_allowed_users(
+ opts[:for],
+ Map.get(opts, :with_muted, false)
+ )
+ |> Stream.map(fn {emoji, users} ->
+ build_emoji_map(emoji, users, opts[:for])
+ end)
+ |> Enum.to_list()
# Status muted state (would do 1 request per status unless user mutes are preloaded)
muted =
@@ -352,10 +345,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
poll: render(PollView, "show.json", object: object, for: opts[:for]),
mentions: mentions,
tags: build_tags(tags),
- application: %{
- name: "Web",
- website: nil
- },
+ application: build_application(object.data["generator"]),
language: nil,
emojis: build_emojis(object.data["emoji"]),
pleroma: %{
@@ -435,7 +425,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
text_url: href,
type: type,
description: attachment["name"],
- pleroma: %{mime_type: media_type}
+ pleroma: %{mime_type: media_type},
+ blurhash: attachment["blurhash"]
}
end
@@ -454,7 +445,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
end
def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
- object = Object.normalize(activity)
+ object = Object.normalize(activity, fetch: false)
with nil <- replied_to_activities[object.data["inReplyTo"]] do
# If user didn't participate in the thread
@@ -463,7 +454,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
end
def get_reply_to(%{data: %{"object" => _object}} = activity, _) do
- object = Object.normalize(activity)
+ object = Object.normalize(activity, fetch: false)
if object.data["inReplyTo"] && object.data["inReplyTo"] != "" do
Activity.get_create_by_object_ap_id(object.data["inReplyTo"])
@@ -494,7 +485,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
def build_tags(object_tags) when is_list(object_tags) do
object_tags
|> Enum.filter(&is_binary/1)
- |> Enum.map(&%{name: &1, url: "/tag/#{URI.encode(&1)}"})
+ |> Enum.map(&%{name: &1, url: "#{Pleroma.Web.base_url()}/tag/#{URI.encode(&1)}"})
end
def build_tags(_), do: []
@@ -543,4 +534,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
me: !!(current_user && current_user.ap_id in users)
}
end
+
+ @spec build_application(map() | nil) :: map() | nil
+ defp build_application(%{type: _type, name: name, url: url}), do: %{name: name, website: url}
+ defp build_application(_), do: nil
end
diff --git a/lib/pleroma/web/mastodon_api/views/subscription_view.ex b/lib/pleroma/web/mastodon_api/views/subscription_view.ex
index 7c67cc924..a07d23512 100644
--- a/lib/pleroma/web/mastodon_api/views/subscription_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/subscription_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.SubscriptionView do
diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex
index 439cdd716..0d1faffbd 100644
--- a/lib/pleroma/web/mastodon_api/websocket_handler.ex
+++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
diff --git a/lib/pleroma/web/media_proxy.ex b/lib/pleroma/web/media_proxy.ex
index 8656b8cad..27f337138 100644
--- a/lib/pleroma/web/media_proxy.ex
+++ b/lib/pleroma/web/media_proxy.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MediaProxy do
@@ -12,29 +12,31 @@ defmodule Pleroma.Web.MediaProxy do
@base64_opts [padding: false]
@cache_table :banned_urls_cache
+ @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
+
def cache_table, do: @cache_table
@spec in_banned_urls(String.t()) :: boolean()
- def in_banned_urls(url), do: elem(Cachex.exists?(@cache_table, url(url)), 1)
+ def in_banned_urls(url), do: elem(@cachex.exists?(@cache_table, url(url)), 1)
def remove_from_banned_urls(urls) when is_list(urls) do
- Cachex.execute!(@cache_table, fn cache ->
- Enum.each(Invalidation.prepare_urls(urls), &Cachex.del(cache, &1))
+ @cachex.execute!(@cache_table, fn cache ->
+ Enum.each(Invalidation.prepare_urls(urls), &@cachex.del(cache, &1))
end)
end
def remove_from_banned_urls(url) when is_binary(url) do
- Cachex.del(@cache_table, url(url))
+ @cachex.del(@cache_table, url(url))
end
def put_in_banned_urls(urls) when is_list(urls) do
- Cachex.execute!(@cache_table, fn cache ->
- Enum.each(Invalidation.prepare_urls(urls), &Cachex.put(cache, &1, true))
+ @cachex.execute!(@cache_table, fn cache ->
+ Enum.each(Invalidation.prepare_urls(urls), &@cachex.put(cache, &1, true))
end)
end
def put_in_banned_urls(url) when is_binary(url) do
- Cachex.put(@cache_table, url(url), true)
+ @cachex.put(@cache_table, url(url), true)
end
def url(url) when is_nil(url) or url == "", do: nil
@@ -67,7 +69,7 @@ defmodule Pleroma.Web.MediaProxy do
# non-local non-whitelisted URLs through it and be sure that body size constraint is preserved.
def preview_enabled?, do: enabled?() and !!Config.get([:media_preview_proxy, :enabled])
- def local?(url), do: String.starts_with?(url, Pleroma.Web.base_url())
+ def local?(url), do: String.starts_with?(url, Web.base_url())
def whitelisted?(url) do
%{host: domain} = URI.parse(url)
@@ -75,17 +77,10 @@ defmodule Pleroma.Web.MediaProxy do
mediaproxy_whitelist_domains =
[:media_proxy, :whitelist]
|> Config.get()
+ |> Kernel.++(["#{Upload.base_url()}"])
|> Enum.map(&maybe_get_domain_from_url/1)
- whitelist_domains =
- if base_url = Config.get([Upload, :base_url]) do
- %{host: base_domain} = URI.parse(base_url)
- [base_domain | mediaproxy_whitelist_domains]
- else
- mediaproxy_whitelist_domains
- end
-
- domain in whitelist_domains
+ domain in mediaproxy_whitelist_domains
end
defp maybe_get_domain_from_url("http" <> _ = url) do
diff --git a/lib/pleroma/web/media_proxy/invalidation.ex b/lib/pleroma/web/media_proxy/invalidation.ex
index 4f4340478..cb2db5ce9 100644
--- a/lib/pleroma/web/media_proxy/invalidation.ex
+++ b/lib/pleroma/web/media_proxy/invalidation.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MediaProxy.Invalidation do
diff --git a/lib/pleroma/web/media_proxy/invalidation/http.ex b/lib/pleroma/web/media_proxy/invalidation/http.ex
index 0b0cde68c..0b2a45518 100644
--- a/lib/pleroma/web/media_proxy/invalidation/http.ex
+++ b/lib/pleroma/web/media_proxy/invalidation/http.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MediaProxy.Invalidation.Http do
diff --git a/lib/pleroma/web/media_proxy/invalidation/script.ex b/lib/pleroma/web/media_proxy/invalidation/script.ex
index d32ffc50b..87a21166c 100644
--- a/lib/pleroma/web/media_proxy/invalidation/script.ex
+++ b/lib/pleroma/web/media_proxy/invalidation/script.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MediaProxy.Invalidation.Script do
@@ -13,6 +13,7 @@ defmodule Pleroma.Web.MediaProxy.Invalidation.Script do
def purge(urls, opts \\ []) do
args =
urls
+ |> maybe_format_urls(Keyword.get(opts, :url_format))
|> List.wrap()
|> Enum.uniq()
|> Enum.join(" ")
@@ -40,4 +41,22 @@ defmodule Pleroma.Web.MediaProxy.Invalidation.Script do
Logger.error("Error while cache purge: #{inspect(error)}")
{:error, inspect(error)}
end
+
+ def maybe_format_urls(urls, :htcacheclean) do
+ urls
+ |> Enum.map(fn url ->
+ uri = URI.parse(url)
+
+ query =
+ if !is_nil(uri.query) do
+ "?" <> uri.query
+ else
+ "?"
+ end
+
+ uri.scheme <> "://" <> uri.host <> ":#{inspect(uri.port)}" <> uri.path <> query
+ end)
+ end
+
+ def maybe_format_urls(urls, _), do: urls
end
diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex
index 90651ed9b..c74eaaf93 100644
--- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex
+++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MediaProxy.MediaProxyController do
diff --git a/lib/pleroma/web/metadata.ex b/lib/pleroma/web/metadata.ex
index 0f2d8d1e7..46ef00c08 100644
--- a/lib/pleroma/web/metadata.ex
+++ b/lib/pleroma/web/metadata.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Metadata do
diff --git a/lib/pleroma/web/metadata/player_view.ex b/lib/pleroma/web/metadata/player_view.ex
index 5a918532a..9be5e433d 100644
--- a/lib/pleroma/web/metadata/player_view.ex
+++ b/lib/pleroma/web/metadata/player_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Metadata.PlayerView do
diff --git a/lib/pleroma/web/metadata/providers/feed.ex b/lib/pleroma/web/metadata/providers/feed.ex
index bd1459a17..d0ab5c19e 100644
--- a/lib/pleroma/web/metadata/providers/feed.ex
+++ b/lib/pleroma/web/metadata/providers/feed.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Metadata.Providers.Feed do
diff --git a/lib/pleroma/web/metadata/providers/open_graph.ex b/lib/pleroma/web/metadata/providers/open_graph.ex
index bb1b23208..1687b2634 100644
--- a/lib/pleroma/web/metadata/providers/open_graph.ex
+++ b/lib/pleroma/web/metadata/providers/open_graph.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Metadata.Providers.OpenGraph do
diff --git a/lib/pleroma/web/metadata/providers/provider.ex b/lib/pleroma/web/metadata/providers/provider.ex
index 767288f9c..c91d87c6d 100644
--- a/lib/pleroma/web/metadata/providers/provider.ex
+++ b/lib/pleroma/web/metadata/providers/provider.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Metadata.Providers.Provider do
diff --git a/lib/pleroma/web/metadata/providers/rel_me.ex b/lib/pleroma/web/metadata/providers/rel_me.ex
index 8905c9c72..f013def51 100644
--- a/lib/pleroma/web/metadata/providers/rel_me.ex
+++ b/lib/pleroma/web/metadata/providers/rel_me.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Metadata.Providers.RelMe do
diff --git a/lib/pleroma/web/metadata/providers/restrict_indexing.ex b/lib/pleroma/web/metadata/providers/restrict_indexing.ex
index a1dcb6e15..aa6511610 100644
--- a/lib/pleroma/web/metadata/providers/restrict_indexing.ex
+++ b/lib/pleroma/web/metadata/providers/restrict_indexing.ex
@@ -1,16 +1,16 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Metadata.Providers.RestrictIndexing do
@behaviour Pleroma.Web.Metadata.Providers.Provider
@moduledoc """
- Restricts indexing of remote users.
+ Restricts indexing of remote and/or non-discoverable users.
"""
@impl true
- def build_tags(%{user: %{local: true, discoverable: true}}), do: []
+ def build_tags(%{user: %{local: true, is_discoverable: true}}), do: []
def build_tags(_) do
[
diff --git a/lib/pleroma/web/metadata/providers/twitter_card.ex b/lib/pleroma/web/metadata/providers/twitter_card.ex
index df34b033f..58fc05cf9 100644
--- a/lib/pleroma/web/metadata/providers/twitter_card.ex
+++ b/lib/pleroma/web/metadata/providers/twitter_card.ex
@@ -1,6 +1,6 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Metadata.Providers.TwitterCard do
diff --git a/lib/pleroma/web/metadata/utils.ex b/lib/pleroma/web/metadata/utils.ex
index 8a206e019..de7195435 100644
--- a/lib/pleroma/web/metadata/utils.ex
+++ b/lib/pleroma/web/metadata/utils.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Metadata.Utils do
diff --git a/lib/pleroma/web/mongoose_im/mongoose_im_controller.ex b/lib/pleroma/web/mongoose_im/mongoose_im_controller.ex
index 2a5c7c356..6ace3e0b5 100644
--- a/lib/pleroma/web/mongoose_im/mongoose_im_controller.ex
+++ b/lib/pleroma/web/mongoose_im/mongoose_im_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MongooseIM.MongooseIMController do
@@ -14,7 +14,7 @@ defmodule Pleroma.Web.MongooseIM.MongooseIMController do
plug(RateLimiter, [name: :authentication, params: ["user"]] when action == :check_password)
def user_exists(conn, %{"user" => username}) do
- with %User{} <- Repo.get_by(User, nickname: username, local: true, deactivated: false) do
+ with %User{} <- Repo.get_by(User, nickname: username, local: true, is_active: true) do
conn
|> json(true)
else
@@ -26,7 +26,7 @@ defmodule Pleroma.Web.MongooseIM.MongooseIMController do
end
def check_password(conn, %{"user" => username, "pass" => password}) do
- with %User{password_hash: password_hash, deactivated: false} <-
+ with %User{password_hash: password_hash, is_active: true} <-
Repo.get_by(User, nickname: username, local: true),
true <- AuthenticationPlug.checkpw(password, password_hash) do
conn
diff --git a/lib/pleroma/web/nodeinfo/nodeinfo.ex b/lib/pleroma/web/nodeinfo/nodeinfo.ex
index 47fa46376..6a0112d2a 100644
--- a/lib/pleroma/web/nodeinfo/nodeinfo.ex
+++ b/lib/pleroma/web/nodeinfo/nodeinfo.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Nodeinfo.Nodeinfo do
diff --git a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
index 8c7a9e565..bca94d236 100644
--- a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
+++ b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
diff --git a/lib/pleroma/web/o_auth.ex b/lib/pleroma/web/o_auth.ex
index 2f1b8708d..3bc1a6ad4 100644
--- a/lib/pleroma/web/o_auth.ex
+++ b/lib/pleroma/web/o_auth.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth do
diff --git a/lib/pleroma/web/o_auth/app.ex b/lib/pleroma/web/o_auth/app.ex
index df99472e1..382750010 100644
--- a/lib/pleroma/web/o_auth/app.ex
+++ b/lib/pleroma/web/o_auth/app.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.App do
diff --git a/lib/pleroma/web/o_auth/authorization.ex b/lib/pleroma/web/o_auth/authorization.ex
index 268ee5b63..e0ecb0f4f 100644
--- a/lib/pleroma/web/o_auth/authorization.ex
+++ b/lib/pleroma/web/o_auth/authorization.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.Authorization do
@@ -9,6 +9,7 @@ defmodule Pleroma.Web.OAuth.Authorization do
alias Pleroma.User
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Authorization
+ alias Pleroma.Web.OAuth.Token
import Ecto.Changeset
import Ecto.Query
@@ -53,7 +54,8 @@ defmodule Pleroma.Web.OAuth.Authorization do
end
defp add_lifetime(changeset) do
- put_change(changeset, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10))
+ lifespan = Token.lifespan()
+ put_change(changeset, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), lifespan))
end
@spec use_changeset(Authtorizatiton.t(), map()) :: Changeset.t()
diff --git a/lib/pleroma/web/o_auth/fallback_controller.ex b/lib/pleroma/web/o_auth/fallback_controller.ex
index a89ced886..df68cbfc1 100644
--- a/lib/pleroma/web/o_auth/fallback_controller.ex
+++ b/lib/pleroma/web/o_auth/fallback_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.FallbackController do
diff --git a/lib/pleroma/web/o_auth/mfa_controller.ex b/lib/pleroma/web/o_auth/mfa_controller.ex
index f102c93e7..b38b00213 100644
--- a/lib/pleroma/web/o_auth/mfa_controller.ex
+++ b/lib/pleroma/web/o_auth/mfa_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.MFAController do
@@ -13,7 +13,6 @@ defmodule Pleroma.Web.OAuth.MFAController do
alias Pleroma.Web.Auth.TOTPAuthenticator
alias Pleroma.Web.OAuth.MFAView, as: View
alias Pleroma.Web.OAuth.OAuthController
- alias Pleroma.Web.OAuth.OAuthView
alias Pleroma.Web.OAuth.Token
plug(:fetch_session when action in [:show, :verify])
@@ -75,7 +74,7 @@ defmodule Pleroma.Web.OAuth.MFAController do
{:ok, %{user: user, authorization: auth}} <- MFA.Token.validate(mfa_token),
{:ok, _} <- validates_challenge(user, params),
{:ok, token} <- Token.exchange_token(app, auth) do
- json(conn, OAuthView.render("token.json", %{user: user, token: token}))
+ OAuthController.after_token_exchange(conn, %{user: user, token: token})
else
_error ->
conn
diff --git a/lib/pleroma/web/o_auth/mfa_view.ex b/lib/pleroma/web/o_auth/mfa_view.ex
index 5d87db268..3d473f29c 100644
--- a/lib/pleroma/web/o_auth/mfa_view.ex
+++ b/lib/pleroma/web/o_auth/mfa_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.MFAView do
diff --git a/lib/pleroma/web/o_auth/o_auth_controller.ex b/lib/pleroma/web/o_auth/o_auth_controller.ex
index d2f9d1ceb..215d97b3a 100644
--- a/lib/pleroma/web/o_auth/o_auth_controller.ex
+++ b/lib/pleroma/web/o_auth/o_auth_controller.ex
@@ -1,10 +1,11 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.OAuthController do
use Pleroma.Web, :controller
+ alias Pleroma.Helpers.AuthHelper
alias Pleroma.Helpers.UriHelper
alias Pleroma.Maps
alias Pleroma.MFA
@@ -79,6 +80,13 @@ defmodule Pleroma.Web.OAuth.OAuthController do
available_scopes = (app && app.scopes) || []
scopes = Scopes.fetch_scopes(params, available_scopes)
+ user =
+ with %{assigns: %{user: %User{} = user}} <- conn do
+ user
+ else
+ _ -> nil
+ end
+
scopes =
if scopes == [] do
available_scopes
@@ -88,6 +96,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do
# Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template
render(conn, Authenticator.auth_template(), %{
+ user: user,
+ app: app && Map.delete(app, :client_secret),
response_type: params["response_type"],
client_id: params["client_id"],
available_scopes: available_scopes,
@@ -131,11 +141,13 @@ defmodule Pleroma.Web.OAuth.OAuthController do
end
end
- def create_authorization(
- %Plug.Conn{} = conn,
- %{"authorization" => _} = params,
- opts \\ []
- ) do
+ def create_authorization(_, _, opts \\ [])
+
+ def create_authorization(%Plug.Conn{assigns: %{user: %User{} = user}} = conn, params, []) do
+ create_authorization(conn, params, user: user)
+ end
+
+ def create_authorization(%Plug.Conn{} = conn, %{"authorization" => _} = params, opts) do
with {:ok, auth, user} <- do_create_authorization(conn, params, opts[:user]),
{:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)} do
after_create_authorization(conn, auth, params)
@@ -248,7 +260,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
with {:ok, app} <- Token.Utils.fetch_app(conn),
{:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token),
{:ok, token} <- RefreshToken.grant(token) do
- json(conn, OAuthView.render("token.json", %{user: user, token: token}))
+ after_token_exchange(conn, %{user: user, token: token})
else
_error -> render_invalid_credentials_error(conn)
end
@@ -260,7 +272,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
{:ok, auth} <- Authorization.get_by_token(app, fixed_token),
%User{} = user <- User.get_cached_by_id(auth.user_id),
{:ok, token} <- Token.exchange_token(app, auth) do
- json(conn, OAuthView.render("token.json", %{user: user, token: token}))
+ after_token_exchange(conn, %{user: user, token: token})
else
error ->
handle_token_exchange_error(conn, error)
@@ -275,7 +287,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
{:ok, app} <- Token.Utils.fetch_app(conn),
requested_scopes <- Scopes.fetch_scopes(params, app.scopes),
{:ok, token} <- login(user, app, requested_scopes) do
- json(conn, OAuthView.render("token.json", %{user: user, token: token}))
+ after_token_exchange(conn, %{user: user, token: token})
else
error ->
handle_token_exchange_error(conn, error)
@@ -298,7 +310,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
with {:ok, app} <- Token.Utils.fetch_app(conn),
{:ok, auth} <- Authorization.create_authorization(app, %User{}),
{:ok, token} <- Token.exchange_token(app, auth) do
- json(conn, OAuthView.render("token.json", %{token: token}))
+ after_token_exchange(conn, %{token: token})
else
_error ->
handle_token_exchange_error(conn, :invalid_credentails)
@@ -308,6 +320,12 @@ defmodule Pleroma.Web.OAuth.OAuthController do
# Bad request
def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
+ def after_token_exchange(%Plug.Conn{} = conn, %{token: token} = view_params) do
+ conn
+ |> AuthHelper.put_session_token(token.token)
+ |> json(OAuthView.render("token.json", view_params))
+ end
+
defp handle_token_exchange_error(%Plug.Conn{} = conn, {:mfa_required, user, auth, _}) do
conn
|> put_status(:forbidden)
@@ -361,9 +379,17 @@ defmodule Pleroma.Web.OAuth.OAuthController do
render_invalid_credentials_error(conn)
end
- def token_revoke(%Plug.Conn{} = conn, %{"token" => _token} = params) do
- with {:ok, app} <- Token.Utils.fetch_app(conn),
- {:ok, _token} <- RevokeToken.revoke(app, params) do
+ def token_revoke(%Plug.Conn{} = conn, %{"token" => token}) do
+ with {:ok, %Token{} = oauth_token} <- Token.get_by_token(token),
+ {:ok, oauth_token} <- RevokeToken.revoke(oauth_token) do
+ conn =
+ with session_token = AuthHelper.get_session_token(conn),
+ %Token{token: ^session_token} <- oauth_token do
+ AuthHelper.delete_session_token(conn)
+ else
+ _ -> conn
+ end
+
json(conn, %{})
else
_error ->
diff --git a/lib/pleroma/web/o_auth/o_auth_view.ex b/lib/pleroma/web/o_auth/o_auth_view.ex
index f55247ebd..281bbcc3c 100644
--- a/lib/pleroma/web/o_auth/o_auth_view.ex
+++ b/lib/pleroma/web/o_auth/o_auth_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.OAuthView do
@@ -13,7 +13,7 @@ defmodule Pleroma.Web.OAuth.OAuthView do
token_type: "Bearer",
access_token: token.token,
refresh_token: token.refresh_token,
- expires_in: expires_in(),
+ expires_in: NaiveDateTime.diff(token.valid_until, NaiveDateTime.utc_now()),
scope: Enum.join(token.scopes, " "),
created_at: Utils.format_created_at(token)
}
@@ -25,6 +25,4 @@ defmodule Pleroma.Web.OAuth.OAuthView do
response
end
end
-
- defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600)
end
diff --git a/lib/pleroma/web/o_auth/scopes.ex b/lib/pleroma/web/o_auth/scopes.ex
index 90b9a0471..ada43eae9 100644
--- a/lib/pleroma/web/o_auth/scopes.ex
+++ b/lib/pleroma/web/o_auth/scopes.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.Scopes do
diff --git a/lib/pleroma/web/o_auth/token.ex b/lib/pleroma/web/o_auth/token.ex
index de37998f2..9d69e9db4 100644
--- a/lib/pleroma/web/o_auth/token.ex
+++ b/lib/pleroma/web/o_auth/token.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.Token do
@@ -27,6 +27,18 @@ defmodule Pleroma.Web.OAuth.Token do
timestamps()
end
+ def lifespan do
+ Pleroma.Config.get!([:oauth2, :token_expires_in])
+ end
+
+ @doc "Gets token by unique access token"
+ @spec get_by_token(String.t()) :: {:ok, t()} | {:error, :not_found}
+ def get_by_token(token) do
+ token
+ |> Query.get_by_token()
+ |> Repo.find_resource()
+ end
+
@doc "Gets token for app by access token"
@spec get_by_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found}
def get_by_token(%App{id: app_id} = _app, token) do
@@ -75,11 +87,11 @@ defmodule Pleroma.Web.OAuth.Token do
end
defp put_valid_until(changeset, attrs) do
- expires_in =
- Map.get(attrs, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), expires_in()))
+ valid_until =
+ Map.get(attrs, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), lifespan()))
changeset
- |> change(%{valid_until: expires_in})
+ |> change(%{valid_until: valid_until})
|> validate_required([:valid_until])
end
@@ -130,6 +142,4 @@ defmodule Pleroma.Web.OAuth.Token do
end
def is_expired?(_), do: false
-
- defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600)
end
diff --git a/lib/pleroma/web/o_auth/token/query.ex b/lib/pleroma/web/o_auth/token/query.ex
index fd6d9b112..d16a759d8 100644
--- a/lib/pleroma/web/o_auth/token/query.ex
+++ b/lib/pleroma/web/o_auth/token/query.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.Token.Query do
diff --git a/lib/pleroma/web/o_auth/token/strategy/refresh_token.ex b/lib/pleroma/web/o_auth/token/strategy/refresh_token.ex
index 625b0fde2..f5a0ed272 100644
--- a/lib/pleroma/web/o_auth/token/strategy/refresh_token.ex
+++ b/lib/pleroma/web/o_auth/token/strategy/refresh_token.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.Token.Strategy.RefreshToken do
diff --git a/lib/pleroma/web/o_auth/token/strategy/revoke.ex b/lib/pleroma/web/o_auth/token/strategy/revoke.ex
index 069c1ee21..8d6572704 100644
--- a/lib/pleroma/web/o_auth/token/strategy/revoke.ex
+++ b/lib/pleroma/web/o_auth/token/strategy/revoke.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.Token.Strategy.Revoke do
diff --git a/lib/pleroma/web/o_auth/token/utils.ex b/lib/pleroma/web/o_auth/token/utils.ex
index 43aeab6b0..b572dc9cf 100644
--- a/lib/pleroma/web/o_auth/token/utils.ex
+++ b/lib/pleroma/web/o_auth/token/utils.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.Token.Utils do
diff --git a/lib/pleroma/web/o_status/o_status_controller.ex b/lib/pleroma/web/o_status/o_status_controller.ex
index 668ae0ea4..da3264149 100644
--- a/lib/pleroma/web/o_status/o_status_controller.ex
+++ b/lib/pleroma/web/o_status/o_status_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OStatus.OStatusController do
@@ -73,15 +73,11 @@ defmodule Pleroma.Web.OStatus.OStatusController do
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
cond do
format in ["json", "activity+json"] ->
- if activity.local do
- %{data: %{"id" => redirect_url}} = Object.normalize(activity)
- redirect(conn, external: redirect_url)
- else
- {:error, :not_found}
- end
+ %{data: %{"id" => redirect_url}} = Object.normalize(activity, fetch: false)
+ redirect(conn, external: redirect_url)
activity.data["type"] == "Create" ->
- %Object{} = object = Object.normalize(activity)
+ %Object{} = object = Object.normalize(activity, fetch: false)
RedirectController.redirector_with_meta(
conn,
@@ -112,7 +108,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do
with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id_with_object(id),
true <- Visibility.is_public?(activity),
{_, true} <- {:visible?, Visibility.visible_for_user?(activity, _reading_user = nil)},
- %Object{} = object <- Object.normalize(activity),
+ %Object{} = object <- Object.normalize(activity, fetch: false),
%{data: %{"attachment" => [%{"url" => [url | _]} | _]}} <- object,
true <- String.starts_with?(url["mediaType"], ["audio", "video"]) do
conn
diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
index 30cf83567..165afd3b4 100644
--- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.AccountController do
@@ -56,7 +56,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
nickname_or_email = params[:email] || params[:nickname]
with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
- {:ok, _} <- User.try_send_confirmation_email(user) do
+ {:ok, _} <- User.maybe_send_confirmation_email(user) do
json_response(conn, :no_content, "")
end
end
diff --git a/lib/pleroma/web/pleroma_api/controllers/backup_controller.ex b/lib/pleroma/web/pleroma_api/controllers/backup_controller.ex
new file mode 100644
index 000000000..315657e9c
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/backup_controller.ex
@@ -0,0 +1,28 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.BackupController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.User.Backup
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+
+ action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+ plug(OAuthScopesPlug, %{scopes: ["read:accounts"]} when action in [:index, :create])
+ plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaBackupOperation
+
+ def index(%{assigns: %{user: user}} = conn, _params) do
+ backups = Backup.list(user)
+ render(conn, "index.json", backups: backups)
+ end
+
+ def create(%{assigns: %{user: user}} = conn, _params) do
+ with {:ok, _} <- Backup.create(user) do
+ backups = Backup.list(user)
+ render(conn, "index.json", backups: backups)
+ end
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex
index 6357148d0..4adc685fe 100644
--- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.ChatController do
use Pleroma.Web, :controller
@@ -15,7 +15,6 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
alias Pleroma.User
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
- alias Pleroma.Web.PleromaAPI.ChatView
alias Pleroma.Web.Plugs.OAuthScopesPlug
import Ecto.Query
@@ -36,7 +35,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
plug(
OAuthScopesPlug,
- %{scopes: ["read:chats"]} when action in [:messages, :index, :show]
+ %{scopes: ["read:chats"]} when action in [:messages, :index, :index2, :show]
)
plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
@@ -80,9 +79,10 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
%User{} = recipient <- User.get_cached_by_ap_id(chat.recipient),
{:ok, activity} <-
CommonAPI.post_chat_message(user, recipient, params[:content],
- media_id: params[:media_id]
+ media_id: params[:media_id],
+ idempotency_key: idempotency_key(conn)
),
- message <- Object.normalize(activity, false),
+ message <- Object.normalize(activity, fetch: false),
cm_ref <- MessageReference.for_chat_and_object(chat, message) do
conn
|> put_view(MessageReferenceView)
@@ -120,9 +120,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
) do
with {:ok, chat} <- Chat.get_by_user_and_id(user, id),
{_n, _} <- MessageReference.set_all_seen_for_chat(chat, last_read_id) do
- conn
- |> put_view(ChatView)
- |> render("show.json", chat: chat)
+ render(conn, "show.json", chat: chat)
end
end
@@ -140,33 +138,49 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
end
end
- def index(%{assigns: %{user: %{id: user_id} = user}} = conn, _params) do
- blocked_ap_ids = User.blocked_users_ap_ids(user)
-
+ def index(%{assigns: %{user: user}} = conn, params) do
chats =
- Chat.for_user_query(user_id)
- |> where([c], c.recipient not in ^blocked_ap_ids)
+ index_query(user, params)
|> Repo.all()
- conn
- |> put_view(ChatView)
- |> render("index.json", chats: chats)
+ render(conn, "index.json", chats: chats)
+ end
+
+ def index2(%{assigns: %{user: user}} = conn, params) do
+ chats =
+ index_query(user, params)
+ |> Pagination.fetch_paginated(params)
+
+ render(conn, "index.json", chats: chats)
+ end
+
+ defp index_query(%{id: user_id} = user, params) do
+ exclude_users =
+ User.cached_blocked_users_ap_ids(user) ++
+ if params[:with_muted], do: [], else: User.cached_muted_users_ap_ids(user)
+
+ user_id
+ |> Chat.for_user_query()
+ |> where([c], c.recipient not in ^exclude_users)
end
def create(%{assigns: %{user: user}} = conn, %{id: id}) do
with %User{ap_id: recipient} <- User.get_cached_by_id(id),
{:ok, %Chat{} = chat} <- Chat.get_or_create(user.id, recipient) do
- conn
- |> put_view(ChatView)
- |> render("show.json", chat: chat)
+ render(conn, "show.json", chat: chat)
end
end
def show(%{assigns: %{user: user}} = conn, %{id: id}) do
with {:ok, chat} <- Chat.get_by_user_and_id(user, id) do
- conn
- |> put_view(ChatView)
- |> render("show.json", chat: chat)
+ render(conn, "show.json", chat: chat)
+ end
+ end
+
+ defp idempotency_key(conn) do
+ case get_req_header(conn, "idempotency-key") do
+ [key] -> key
+ _ -> nil
end
end
end
diff --git a/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex b/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex
index df52b7566..d285e4907 100644
--- a/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.ConversationController do
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 428c97de6..204e81311 100644
--- a/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.EmojiFileController do
@@ -12,7 +12,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiFileController do
plug(
Pleroma.Web.Plugs.OAuthScopesPlug,
- %{scopes: ["write"], admin: true}
+ %{scopes: ["admin:write"]}
when action in [
:create,
:update,
@@ -42,7 +42,10 @@ defmodule Pleroma.Web.PleromaAPI.EmojiFileController do
|> json(%{error: "pack name, shortcode or filename cannot be empty"})
{:error, _} = error ->
- handle_error(conn, error, %{pack_name: pack_name})
+ handle_error(conn, error, %{
+ pack_name: pack_name,
+ message: "Unexpected error occurred while adding file to pack."
+ })
end
end
@@ -69,7 +72,11 @@ defmodule Pleroma.Web.PleromaAPI.EmojiFileController do
|> json(%{error: "new_shortcode or new_filename cannot be empty"})
{:error, _} = error ->
- handle_error(conn, error, %{pack_name: pack_name, code: shortcode})
+ handle_error(conn, error, %{
+ pack_name: pack_name,
+ code: shortcode,
+ message: "Unexpected error occurred while updating."
+ })
end
end
@@ -84,7 +91,11 @@ defmodule Pleroma.Web.PleromaAPI.EmojiFileController do
|> json(%{error: "pack name or shortcode cannot be empty"})
{:error, _} = error ->
- handle_error(conn, error, %{pack_name: pack_name, code: shortcode})
+ handle_error(conn, error, %{
+ pack_name: pack_name,
+ code: shortcode,
+ message: "Unexpected error occurred while deleting emoji file."
+ })
end
end
@@ -94,18 +105,24 @@ defmodule Pleroma.Web.PleromaAPI.EmojiFileController do
|> json(%{error: "Emoji \"#{emoji_code}\" does not exist"})
end
- defp handle_error(conn, {:error, :not_found}, %{pack_name: pack_name}) do
+ defp handle_error(conn, {:error, :enoent}, %{pack_name: pack_name}) do
conn
|> put_status(:not_found)
|> json(%{error: "pack \"#{pack_name}\" is not found"})
end
- defp handle_error(conn, {:error, _}, _) do
- render_error(
- conn,
- :internal_server_error,
- "Unexpected error occurred while adding file to pack."
- )
+ defp handle_error(conn, {:error, error}, opts) do
+ message =
+ [
+ Map.get(opts, :message, "Unexpected error occurred."),
+ Pleroma.Utils.posix_error_message(error)
+ ]
+ |> Enum.join(" ")
+ |> String.trim()
+
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{error: message})
end
defp get_filename(%Plug.Upload{filename: filename}), do: filename
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 a9accc5af..d0f677d3c 100644
--- a/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
@@ -11,7 +11,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
plug(
Pleroma.Web.Plugs.OAuthScopesPlug,
- %{scopes: ["write"], admin: true}
+ %{scopes: ["admin:write"]}
when action in [
:import_from_filesystem,
:remote,
@@ -71,7 +71,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
with {:ok, pack} <- Pack.show(name: name, page: page, page_size: page_size) do
json(conn, pack)
else
- {:error, :not_found} ->
+ {:error, :enoent} ->
conn
|> put_status(:not_found)
|> json(%{error: "Pack #{name} does not exist"})
@@ -80,6 +80,17 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
conn
|> put_status(:bad_request)
|> json(%{error: "pack name cannot be empty"})
+
+ {:error, error} ->
+ error_message =
+ add_posix_error(
+ "Failed to get the contents of the `#{name}` pack.",
+ error
+ )
+
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{error: error_message})
end
end
@@ -95,7 +106,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
"Pack #{name} cannot be downloaded from this instance, either pack sharing was disabled for this pack or some files are missing"
})
- {:error, :not_found} ->
+ {:error, :enoent} ->
conn
|> put_status(:not_found)
|> json(%{error: "Pack #{name} does not exist"})
@@ -116,10 +127,10 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
|> put_status(:internal_server_error)
|> json(%{error: "SHA256 for the pack doesn't match the one sent by the server"})
- {:error, e} ->
+ {:error, error} ->
conn
|> put_status(:internal_server_error)
- |> json(%{error: e})
+ |> json(%{error: error})
end
end
@@ -139,12 +150,16 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
|> put_status(:bad_request)
|> json(%{error: "pack name cannot be empty"})
- {:error, _} ->
- render_error(
- conn,
- :internal_server_error,
- "Unexpected error occurred while creating pack."
- )
+ {:error, error} ->
+ error_message =
+ add_posix_error(
+ "Unexpected error occurred while creating pack.",
+ error
+ )
+
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{error: error_message})
end
end
@@ -164,10 +179,12 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
|> put_status(:bad_request)
|> json(%{error: "pack name cannot be empty"})
- {:error, _, _} ->
+ {:error, error, _} ->
+ error_message = add_posix_error("Couldn't delete the `#{name}` pack", error)
+
conn
|> put_status(:internal_server_error)
- |> json(%{error: "Couldn't delete the pack #{name}"})
+ |> json(%{error: error_message})
end
end
@@ -180,12 +197,16 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
|> put_status(:bad_request)
|> json(%{error: "The fallback archive does not have all files specified in pack.json"})
- {:error, _} ->
- render_error(
- conn,
- :internal_server_error,
- "Unexpected error occurred while updating pack metadata."
- )
+ {:error, error} ->
+ error_message =
+ add_posix_error(
+ "Unexpected error occurred while updating pack metadata.",
+ error
+ )
+
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{error: error_message})
end
end
@@ -204,4 +225,10 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
|> json(%{error: "Error accessing emoji pack directory"})
end
end
+
+ defp add_posix_error(msg, error) do
+ [msg, Pleroma.Utils.posix_error_message(error)]
+ |> Enum.join(" ")
+ |> String.trim()
+ end
end
diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex
index ae199a50f..da5f2474f 100644
--- a/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do
alias Pleroma.Activity
alias Pleroma.Object
+ alias Pleroma.User
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.Plugs.OAuthScopesPlug
@@ -28,14 +29,43 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do
with true <- Pleroma.Config.get([:instance, :show_reactions]),
%Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
%Object{data: %{"reactions" => reactions}} when is_list(reactions) <-
- Object.normalize(activity) do
- reactions = filter(reactions, params)
+ Object.normalize(activity, fetch: false) do
+ reactions =
+ reactions
+ |> filter(params)
+ |> filter_allowed_users(user, Map.get(params, :with_muted, false))
+
render(conn, "index.json", emoji_reactions: reactions, user: user)
else
_e -> json(conn, [])
end
end
+ def filter_allowed_users(reactions, user, with_muted) do
+ exclude_ap_ids =
+ if is_nil(user) do
+ []
+ else
+ User.cached_blocked_users_ap_ids(user) ++
+ if not with_muted, do: User.cached_muted_users_ap_ids(user), else: []
+ end
+
+ filter_emoji = fn emoji, users ->
+ case Enum.reject(users, &(&1 in exclude_ap_ids)) do
+ [] -> nil
+ users -> {emoji, users}
+ end
+ end
+
+ reactions
+ |> Stream.map(fn
+ [emoji, users] when is_list(users) -> filter_emoji.(emoji, users)
+ {emoji, users} when is_list(users) -> filter_emoji.(emoji, users)
+ _ -> nil
+ end)
+ |> Stream.reject(&is_nil/1)
+ end
+
defp filter(reactions, %{emoji: emoji}) when is_binary(emoji) do
Enum.filter(reactions, fn [e, _] -> e == emoji end)
end
diff --git a/lib/pleroma/web/pleroma_api/controllers/instances_controller.ex b/lib/pleroma/web/pleroma_api/controllers/instances_controller.ex
new file mode 100644
index 000000000..01424c6ba
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/instances_controller.ex
@@ -0,0 +1,21 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.InstancesController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Instances
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaInstancesOperation
+
+ def show(conn, _params) do
+ unreachable =
+ Instances.get_consistently_unreachable()
+ |> Map.new(fn {host, date} -> {host, to_string(date)} end)
+
+ json(conn, %{"unreachable" => unreachable})
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex
index 15210f1e6..429ef5112 100644
--- a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.MascotController do
diff --git a/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex b/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex
index fa32aaa84..257bcd550 100644
--- a/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.NotificationController do
diff --git a/lib/pleroma/web/pleroma_api/controllers/report_controller.ex b/lib/pleroma/web/pleroma_api/controllers/report_controller.ex
new file mode 100644
index 000000000..d93d7570a
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/report_controller.ex
@@ -0,0 +1,46 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.ReportController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Activity
+ alias Pleroma.Web.ActivityPub.Utils
+ alias Pleroma.Web.AdminAPI.Report
+
+ action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.Plugs.OAuthScopesPlug, %{scopes: ["read:reports"]})
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaReportOperation
+
+ @doc "GET /api/v0/pleroma/reports"
+ def index(%{assigns: %{user: user}, body_params: params} = conn, _) do
+ params =
+ params
+ |> Map.put(:actor_id, user.ap_id)
+
+ reports = Utils.get_reports(params, Map.get(params, :page, 1), Map.get(params, :size, 20))
+
+ render(conn, "index.json", %{reports: reports, for: user})
+ end
+
+ @doc "GET /api/v0/pleroma/reports/:id"
+ def show(%{assigns: %{user: user}} = conn, %{id: id}) do
+ with %Activity{} = report <- Activity.get_report(id),
+ true <- report.actor == user.ap_id,
+ %{} = report_info <- Report.extract_report_info(report) do
+ render(conn, "show.json", Map.put(report_info, :for, user))
+ else
+ false ->
+ {:error, :not_found}
+
+ nil ->
+ {:error, :not_found}
+
+ e ->
+ {:error, inspect(e)}
+ end
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex
index 632d65434..ca26d80ef 100644
--- a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.ScrobbleController do
diff --git a/lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex b/lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex
index eba452300..3940ad581 100644
--- a/lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.TwoFactorAuthenticationController do
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 7f089af1c..6d9a11fb6 100644
--- a/lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.UserImportController do
diff --git a/lib/pleroma/web/pleroma_api/views/backup_view.ex b/lib/pleroma/web/pleroma_api/views/backup_view.ex
new file mode 100644
index 000000000..944600c86
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/views/backup_view.ex
@@ -0,0 +1,29 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.BackupView do
+ use Pleroma.Web, :view
+
+ alias Pleroma.User.Backup
+ alias Pleroma.Web.CommonAPI.Utils
+
+ def render("show.json", %{backup: %Backup{} = backup}) do
+ %{
+ id: backup.id,
+ content_type: backup.content_type,
+ url: download_url(backup),
+ file_size: backup.file_size,
+ processed: backup.processed,
+ inserted_at: Utils.to_masto_date(backup.inserted_at)
+ }
+ end
+
+ def render("index.json", %{backups: backups}) do
+ render_many(backups, __MODULE__, "show.json")
+ end
+
+ def download_url(%Backup{file_name: file_name}) do
+ Pleroma.Upload.base_url() <> "/backups/" <> file_name
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/views/chat/message_reference_view.ex b/lib/pleroma/web/pleroma_api/views/chat/message_reference_view.ex
index d4e08b50d..2e4355992 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
@@ -1,14 +1,17 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.Chat.MessageReferenceView do
use Pleroma.Web, :view
+ alias Pleroma.Maps
alias Pleroma.User
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.StatusView
+ @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
+
def render(
"show.json",
%{
@@ -37,6 +40,7 @@ defmodule Pleroma.Web.PleromaAPI.Chat.MessageReferenceView do
Pleroma.Web.RichMedia.Helpers.fetch_data_for_object(object)
)
}
+ |> put_idempotency_key()
end
def render("index.json", opts) do
@@ -47,4 +51,13 @@ defmodule Pleroma.Web.PleromaAPI.Chat.MessageReferenceView do
Map.put(opts, :as, :chat_message_reference)
)
end
+
+ defp put_idempotency_key(data) do
+ with {:ok, idempotency_key} <- @cachex.get(:chat_message_id_idempotency_key_cache, data.id) do
+ data
+ |> Maps.put_if_present(:idempotency_key, idempotency_key)
+ else
+ _ -> data
+ end
+ end
end
diff --git a/lib/pleroma/web/pleroma_api/views/chat_view.ex b/lib/pleroma/web/pleroma_api/views/chat_view.ex
index 04dc20d51..3794818a7 100644
--- a/lib/pleroma/web/pleroma_api/views/chat_view.ex
+++ b/lib/pleroma/web/pleroma_api/views/chat_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.ChatView do
diff --git a/lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex b/lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex
index e0f98b50a..c94527e6d 100644
--- a/lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex
+++ b/lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.EmojiReactionView do
@@ -11,7 +11,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionView do
render_many(emoji_reactions, __MODULE__, "show.json", opts)
end
- def render("show.json", %{emoji_reaction: [emoji, user_ap_ids], user: user}) do
+ def render("show.json", %{emoji_reaction: {emoji, user_ap_ids}, user: user}) do
users = fetch_users(user_ap_ids)
%{
@@ -26,7 +26,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionView do
user_ap_ids
|> Enum.map(&Pleroma.User.get_cached_by_ap_id/1)
|> Enum.filter(fn
- %{deactivated: false} -> true
+ %{is_active: true} -> true
_ -> false
end)
end
diff --git a/lib/pleroma/web/pleroma_api/views/report_view.ex b/lib/pleroma/web/pleroma_api/views/report_view.ex
new file mode 100644
index 000000000..a0b3f085c
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/views/report_view.ex
@@ -0,0 +1,55 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.ReportView do
+ use Pleroma.Web, :view
+
+ alias Pleroma.HTML
+ alias Pleroma.Web.AdminAPI.Report
+ alias Pleroma.Web.CommonAPI.Utils
+ alias Pleroma.Web.MastodonAPI.AccountView
+ alias Pleroma.Web.MastodonAPI.StatusView
+
+ def render("index.json", %{reports: reports, for: for_user}) do
+ %{
+ reports:
+ reports[:items]
+ |> Enum.map(&Report.extract_report_info/1)
+ |> Enum.map(&render(__MODULE__, "show.json", Map.put(&1, :for, for_user))),
+ total: reports[:total]
+ }
+ end
+
+ def render("show.json", %{
+ report: report,
+ user: actor,
+ account: account,
+ statuses: statuses,
+ for: for_user
+ }) do
+ created_at = Utils.to_masto_date(report.data["published"])
+
+ content =
+ unless is_nil(report.data["content"]) do
+ HTML.filter_tags(report.data["content"])
+ else
+ nil
+ end
+
+ %{
+ id: report.id,
+ account: AccountView.render("show.json", %{user: account, for: for_user}),
+ actor: AccountView.render("show.json", %{user: actor, for: for_user}),
+ content: content,
+ created_at: created_at,
+ statuses:
+ StatusView.render("index.json", %{
+ activities: statuses,
+ as: :activity,
+ for: for_user
+ }),
+ state: report.data["state"]
+ }
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/views/scrobble_view.ex b/lib/pleroma/web/pleroma_api/views/scrobble_view.ex
index 95bd4c368..2bc069529 100644
--- a/lib/pleroma/web/pleroma_api/views/scrobble_view.ex
+++ b/lib/pleroma/web/pleroma_api/views/scrobble_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.ScrobbleView do
@@ -15,7 +15,7 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleView do
alias Pleroma.Web.MastodonAPI.AccountView
def render("show.json", %{activity: %Activity{data: %{"type" => "Listen"}} = activity} = opts) do
- object = Object.normalize(activity)
+ object = Object.normalize(activity, fetch: false)
user = CommonAPI.get_user(activity.data["actor"])
created_at = Utils.to_masto_date(activity.data["published"])
diff --git a/lib/pleroma/web/plug.ex b/lib/pleroma/web/plug.ex
index 840b35072..dffad3a06 100644
--- a/lib/pleroma/web/plug.ex
+++ b/lib/pleroma/web/plug.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plug do
diff --git a/lib/pleroma/web/plugs/admin_secret_authentication_plug.ex b/lib/pleroma/web/plugs/admin_secret_authentication_plug.ex
index d7d4e4092..976e5cd92 100644
--- a/lib/pleroma/web/plugs/admin_secret_authentication_plug.ex
+++ b/lib/pleroma/web/plugs/admin_secret_authentication_plug.ex
@@ -1,25 +1,18 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.AdminSecretAuthenticationPlug do
import Plug.Conn
+ alias Pleroma.Helpers.AuthHelper
alias Pleroma.User
- alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.Plugs.RateLimiter
def init(options) do
options
end
- def secret_token do
- case Pleroma.Config.get(:admin_token) do
- blank when blank in [nil, ""] -> nil
- token -> token
- end
- end
-
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
def call(conn, _) do
@@ -30,7 +23,7 @@ defmodule Pleroma.Web.Plugs.AdminSecretAuthenticationPlug do
end
end
- def authenticate(%{params: %{"admin_token" => admin_token}} = conn) do
+ defp authenticate(%{params: %{"admin_token" => admin_token}} = conn) do
if admin_token == secret_token() do
assign_admin_user(conn)
else
@@ -38,7 +31,7 @@ defmodule Pleroma.Web.Plugs.AdminSecretAuthenticationPlug do
end
end
- def authenticate(conn) do
+ defp authenticate(conn) do
token = secret_token()
case get_req_header(conn, "x-admin-token") do
@@ -48,10 +41,17 @@ defmodule Pleroma.Web.Plugs.AdminSecretAuthenticationPlug do
end
end
+ defp secret_token do
+ case Pleroma.Config.get(:admin_token) do
+ blank when blank in [nil, ""] -> nil
+ token -> token
+ end
+ end
+
defp assign_admin_user(conn) do
conn
|> assign(:user, %User{is_admin: true})
- |> OAuthScopesPlug.skip_plug()
+ |> AuthHelper.skip_oauth()
end
defp handle_bad_token(conn) do
diff --git a/lib/pleroma/web/plugs/authentication_plug.ex b/lib/pleroma/web/plugs/authentication_plug.ex
index e2a8b1b69..8d58169cf 100644
--- a/lib/pleroma/web/plugs/authentication_plug.ex
+++ b/lib/pleroma/web/plugs/authentication_plug.ex
@@ -1,8 +1,11 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.AuthenticationPlug do
+ @moduledoc "Password authentication plug."
+
+ alias Pleroma.Helpers.AuthHelper
alias Pleroma.User
import Plug.Conn
@@ -11,6 +14,30 @@ defmodule Pleroma.Web.Plugs.AuthenticationPlug do
def init(options), do: options
+ def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
+
+ def call(
+ %{
+ assigns: %{
+ auth_user: %{password_hash: password_hash} = auth_user,
+ auth_credentials: %{password: password}
+ }
+ } = conn,
+ _
+ ) do
+ if checkpw(password, password_hash) do
+ {:ok, auth_user} = maybe_update_password(auth_user, password)
+
+ conn
+ |> assign(:user, auth_user)
+ |> AuthHelper.skip_oauth()
+ else
+ conn
+ end
+ end
+
+ def call(conn, _), do: conn
+
def checkpw(password, "$6" <> _ = password_hash) do
:crypt.crypt(password, password_hash) == password_hash
end
@@ -21,7 +48,7 @@ defmodule Pleroma.Web.Plugs.AuthenticationPlug do
end
def checkpw(password, "$pbkdf2" <> _ = password_hash) do
- Pbkdf2.verify_pass(password, password_hash)
+ Pleroma.Password.Pbkdf2.verify_pass(password, password_hash)
end
def checkpw(_password, _password_hash) do
@@ -40,40 +67,6 @@ defmodule Pleroma.Web.Plugs.AuthenticationPlug do
def maybe_update_password(user, _), do: {:ok, user}
defp do_update_password(user, password) do
- user
- |> User.password_update_changeset(%{
- "password" => password,
- "password_confirmation" => password
- })
- |> Pleroma.Repo.update()
+ User.reset_password(user, %{password: password, password_confirmation: password})
end
-
- def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
-
- def call(
- %{
- assigns: %{
- auth_user: %{password_hash: password_hash} = auth_user,
- auth_credentials: %{password: password}
- }
- } = conn,
- _
- ) do
- if checkpw(password, password_hash) do
- {:ok, auth_user} = maybe_update_password(auth_user, password)
-
- conn
- |> assign(:user, auth_user)
- |> Pleroma.Web.Plugs.OAuthScopesPlug.skip_plug()
- else
- conn
- end
- end
-
- def call(%{assigns: %{auth_credentials: %{password: _}}} = conn, _) do
- Pbkdf2.no_user_verify()
- conn
- end
-
- def call(conn, _), do: conn
end
diff --git a/lib/pleroma/web/plugs/basic_auth_decoder_plug.ex b/lib/pleroma/web/plugs/basic_auth_decoder_plug.ex
index 4dadfb000..397f26de5 100644
--- a/lib/pleroma/web/plugs/basic_auth_decoder_plug.ex
+++ b/lib/pleroma/web/plugs/basic_auth_decoder_plug.ex
@@ -1,8 +1,14 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.BasicAuthDecoderPlug do
+ @moduledoc """
+ Decodes HTTP Basic Auth information and assigns `:auth_credentials`.
+
+ NOTE: no checks are performed at this step, auth_credentials/username could be easily faked.
+ """
+
import Plug.Conn
def init(options) do
diff --git a/lib/pleroma/web/plugs/cache.ex b/lib/pleroma/web/plugs/cache.ex
index 6de01804a..111854859 100644
--- a/lib/pleroma/web/plugs/cache.ex
+++ b/lib/pleroma/web/plugs/cache.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.Cache do
@@ -41,6 +41,8 @@ defmodule Pleroma.Web.Plugs.Cache do
@defaults %{ttl: nil, query_params: true}
+ @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
+
@impl true
def init([]), do: @defaults
@@ -53,7 +55,7 @@ defmodule Pleroma.Web.Plugs.Cache do
def call(%{method: "GET"} = conn, opts) do
key = cache_key(conn, opts)
- case Cachex.get(:web_resp_cache, key) do
+ case @cachex.get(:web_resp_cache, key) do
{:ok, nil} ->
cache_resp(conn, opts)
@@ -97,11 +99,11 @@ defmodule Pleroma.Web.Plugs.Cache do
conn =
unless opts[:tracking_fun] do
- Cachex.put(:web_resp_cache, key, {content_type, body}, ttl: ttl)
+ @cachex.put(:web_resp_cache, key, {content_type, body}, ttl: ttl)
conn
else
tracking_fun_data = Map.get(conn.assigns, :tracking_fun_data, nil)
- Cachex.put(:web_resp_cache, key, {content_type, body, tracking_fun_data}, ttl: ttl)
+ @cachex.put(:web_resp_cache, key, {content_type, body, tracking_fun_data}, ttl: ttl)
opts.tracking_fun.(conn, tracking_fun_data)
end
diff --git a/lib/pleroma/web/plugs/digest_plug.ex b/lib/pleroma/web/plugs/digest_plug.ex
index b521b3073..d72f8073c 100644
--- a/lib/pleroma/web/plugs/digest_plug.ex
+++ b/lib/pleroma/web/plugs/digest_plug.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.DigestPlug do
@@ -7,8 +7,22 @@ defmodule Pleroma.Web.Plugs.DigestPlug do
require Logger
def read_body(conn, opts) do
+ digest_algorithm =
+ with [digest_header] <- Conn.get_req_header(conn, "digest") do
+ digest_header
+ |> String.split("=", parts: 2)
+ |> List.first()
+ else
+ _ -> "SHA-256"
+ end
+
+ unless String.downcase(digest_algorithm) == "sha-256" do
+ raise ArgumentError,
+ message: "invalid value for digest algorithm, got: #{digest_algorithm}"
+ end
+
{:ok, body, conn} = Conn.read_body(conn, opts)
- digest = "SHA-256=" <> (:crypto.hash(:sha256, body) |> Base.encode64())
- {:ok, body, Conn.assign(conn, :digest, digest)}
+ encoded_digest = :crypto.hash(:sha256, body) |> Base.encode64()
+ {:ok, body, Conn.assign(conn, :digest, "#{digest_algorithm}=#{encoded_digest}")}
end
end
diff --git a/lib/pleroma/web/plugs/ensure_authenticated_plug.ex b/lib/pleroma/web/plugs/ensure_authenticated_plug.ex
index ea2af6881..31e7410d6 100644
--- a/lib/pleroma/web/plugs/ensure_authenticated_plug.ex
+++ b/lib/pleroma/web/plugs/ensure_authenticated_plug.ex
@@ -1,8 +1,12 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.EnsureAuthenticatedPlug do
+ @moduledoc """
+ Ensures _user_ authentication (app-bound user-unbound tokens are not accepted).
+ """
+
import Plug.Conn
import Pleroma.Web.TranslationHelpers
diff --git a/lib/pleroma/web/plugs/ensure_public_or_authenticated_plug.ex b/lib/pleroma/web/plugs/ensure_public_or_authenticated_plug.ex
index 3bebdac6d..8a8532f41 100644
--- a/lib/pleroma/web/plugs/ensure_public_or_authenticated_plug.ex
+++ b/lib/pleroma/web/plugs/ensure_public_or_authenticated_plug.ex
@@ -1,8 +1,13 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug do
+ @moduledoc """
+ Ensures instance publicity or _user_ authentication
+ (app-bound user-unbound tokens are accepted only if the instance is public).
+ """
+
import Pleroma.Web.TranslationHelpers
import Plug.Conn
diff --git a/lib/pleroma/web/plugs/ensure_user_key_plug.ex b/lib/pleroma/web/plugs/ensure_user_key_plug.ex
deleted file mode 100644
index 70d3091f0..000000000
--- a/lib/pleroma/web/plugs/ensure_user_key_plug.ex
+++ /dev/null
@@ -1,18 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.Plugs.EnsureUserKeyPlug do
- import Plug.Conn
-
- def init(opts) do
- opts
- end
-
- def call(%{assigns: %{user: _}} = conn, _), do: conn
-
- def call(conn, _) do
- conn
- |> assign(:user, nil)
- end
-end
diff --git a/lib/pleroma/web/plugs/ensure_user_token_assigns_plug.ex b/lib/pleroma/web/plugs/ensure_user_token_assigns_plug.ex
new file mode 100644
index 000000000..534b0cff1
--- /dev/null
+++ b/lib/pleroma/web/plugs/ensure_user_token_assigns_plug.ex
@@ -0,0 +1,41 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug do
+ import Plug.Conn
+
+ alias Pleroma.Helpers.AuthHelper
+ alias Pleroma.User
+ alias Pleroma.Web.OAuth.Token
+
+ @moduledoc "Ensures presence and consistency of :user and :token assigns."
+
+ def init(opts) do
+ opts
+ end
+
+ def call(%{assigns: %{user: %User{id: user_id}} = assigns} = conn, _) do
+ with %Token{user_id: ^user_id} <- assigns[:token] do
+ conn
+ else
+ %Token{} ->
+ # A safety net for abnormal (unexpected) scenario: :token belongs to another user
+ AuthHelper.drop_auth_info(conn)
+
+ _ ->
+ assign(conn, :token, nil)
+ end
+ end
+
+ # App-bound token case (obtained with client_id and client_secret)
+ def call(%{assigns: %{token: %Token{user_id: nil}}} = conn, _) do
+ assign(conn, :user, nil)
+ end
+
+ def call(conn, _) do
+ conn
+ |> assign(:user, nil)
+ |> assign(:token, nil)
+ end
+end
diff --git a/lib/pleroma/web/plugs/expect_authenticated_check_plug.ex b/lib/pleroma/web/plugs/expect_authenticated_check_plug.ex
index 0925ded4d..f09cffe95 100644
--- a/lib/pleroma/web/plugs/expect_authenticated_check_plug.ex
+++ b/lib/pleroma/web/plugs/expect_authenticated_check_plug.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.ExpectAuthenticatedCheckPlug do
diff --git a/lib/pleroma/web/plugs/expect_public_or_authenticated_check_plug.ex b/lib/pleroma/web/plugs/expect_public_or_authenticated_check_plug.ex
index ace512a78..e227d5150 100644
--- a/lib/pleroma/web/plugs/expect_public_or_authenticated_check_plug.ex
+++ b/lib/pleroma/web/plugs/expect_public_or_authenticated_check_plug.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.ExpectPublicOrAuthenticatedCheckPlug do
diff --git a/lib/pleroma/web/plugs/federating_plug.ex b/lib/pleroma/web/plugs/federating_plug.ex
index 3c90a7644..eeef7e45b 100644
--- a/lib/pleroma/web/plugs/federating_plug.ex
+++ b/lib/pleroma/web/plugs/federating_plug.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.FederatingPlug do
diff --git a/lib/pleroma/web/plugs/frontend_static.ex b/lib/pleroma/web/plugs/frontend_static.ex
index 1b0b36813..eb385e94d 100644
--- a/lib/pleroma/web/plugs/frontend_static.ex
+++ b/lib/pleroma/web/plugs/frontend_static.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.FrontendStatic do
@@ -10,6 +10,8 @@ defmodule Pleroma.Web.Plugs.FrontendStatic do
"""
@behaviour Plug
+ @api_routes Pleroma.Web.get_api_routes()
+
def file_path(path, frontend_type \\ :primary) do
if configuration = Pleroma.Config.get([:frontends, frontend_type]) do
instance_static_path = Pleroma.Config.get([:instance, :static_dir], "instance/static")
@@ -34,7 +36,8 @@ defmodule Pleroma.Web.Plugs.FrontendStatic do
end
def call(conn, opts) do
- with false <- invalid_path?(conn.path_info),
+ with false <- api_route?(conn.path_info),
+ false <- invalid_path?(conn.path_info),
frontend_type <- Map.get(opts, :frontend_type, :primary),
path when not is_nil(path) <- file_path("", frontend_type) do
call_static(conn, opts, path)
@@ -52,6 +55,10 @@ defmodule Pleroma.Web.Plugs.FrontendStatic do
defp invalid_path?([h | t], match), do: String.contains?(h, match) or invalid_path?(t)
defp invalid_path?([], _match), do: false
+ defp api_route?([h | _]) when h in @api_routes, do: true
+ defp api_route?([_ | t]), do: api_route?(t)
+ defp api_route?([]), do: false
+
defp call_static(conn, opts, from) do
opts = Map.put(opts, :from, from)
Plug.Static.call(conn, opts)
diff --git a/lib/pleroma/web/plugs/http_security_plug.ex b/lib/pleroma/web/plugs/http_security_plug.ex
index 45aaf188e..0025b042a 100644
--- a/lib/pleroma/web/plugs/http_security_plug.ex
+++ b/lib/pleroma/web/plugs/http_security_plug.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.HTTPSecurityPlug do
@@ -20,9 +20,26 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlug do
end
end
- defp headers do
+ def primary_frontend do
+ with %{"name" => frontend} <- Config.get([:frontends, :primary]),
+ available <- Config.get([:frontends, :available]),
+ %{} = primary_frontend <- Map.get(available, frontend) do
+ {:ok, primary_frontend}
+ end
+ end
+
+ def custom_http_frontend_headers do
+ with {:ok, %{"custom-http-headers" => custom_headers}} <- primary_frontend() do
+ custom_headers
+ else
+ _ -> []
+ end
+ end
+
+ def headers do
referrer_policy = Config.get([:http_security, :referrer_policy])
report_uri = Config.get([:http_security, :report_uri])
+ custom_http_frontend_headers = custom_http_frontend_headers()
headers = [
{"x-xss-protection", "1; mode=block"},
@@ -34,6 +51,13 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlug do
{"content-security-policy", csp_string()}
]
+ headers =
+ if custom_http_frontend_headers do
+ custom_http_frontend_headers ++ headers
+ else
+ headers
+ end
+
if report_uri do
report_group = %{
"group" => "csp-endpoint",
diff --git a/lib/pleroma/web/plugs/http_signature_plug.ex b/lib/pleroma/web/plugs/http_signature_plug.ex
index 036e2a773..0f7550516 100644
--- a/lib/pleroma/web/plugs/http_signature_plug.ex
+++ b/lib/pleroma/web/plugs/http_signature_plug.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
diff --git a/lib/pleroma/web/plugs/idempotency_plug.ex b/lib/pleroma/web/plugs/idempotency_plug.ex
index 254a790b0..9ac8f3647 100644
--- a/lib/pleroma/web/plugs/idempotency_plug.ex
+++ b/lib/pleroma/web/plugs/idempotency_plug.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.IdempotencyPlug do
@@ -8,6 +8,8 @@ defmodule Pleroma.Web.Plugs.IdempotencyPlug do
@behaviour Plug
+ @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
+
@impl true
def init(opts), do: opts
@@ -25,7 +27,7 @@ defmodule Pleroma.Web.Plugs.IdempotencyPlug do
def call(conn, _), do: conn
def process_request(conn, key) do
- case Cachex.get(:idempotency_cache, key) do
+ case @cachex.get(:idempotency_cache, key) do
{:ok, nil} ->
cache_resposnse(conn, key)
@@ -43,7 +45,7 @@ defmodule Pleroma.Web.Plugs.IdempotencyPlug do
content_type = get_content_type(conn)
record = {request_id, content_type, conn.status, conn.resp_body}
- {:ok, _} = Cachex.put(:idempotency_cache, key, record)
+ {:ok, _} = @cachex.put(:idempotency_cache, key, record)
conn
|> put_resp_header("idempotency-key", key)
diff --git a/lib/pleroma/web/plugs/instance_static.ex b/lib/pleroma/web/plugs/instance_static.ex
index 54b9175df..723b25679 100644
--- a/lib/pleroma/web/plugs/instance_static.ex
+++ b/lib/pleroma/web/plugs/instance_static.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.InstanceStatic do
diff --git a/lib/pleroma/web/plugs/legacy_authentication_plug.ex b/lib/pleroma/web/plugs/legacy_authentication_plug.ex
deleted file mode 100644
index 2a54d0b59..000000000
--- a/lib/pleroma/web/plugs/legacy_authentication_plug.ex
+++ /dev/null
@@ -1,41 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.Plugs.LegacyAuthenticationPlug do
- import Plug.Conn
-
- alias Pleroma.User
-
- def init(options) do
- options
- end
-
- def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
-
- def call(
- %{
- assigns: %{
- auth_user: %{password_hash: "$6$" <> _ = password_hash} = auth_user,
- auth_credentials: %{password: password}
- }
- } = conn,
- _
- ) do
- with ^password_hash <- :crypt.crypt(password, password_hash),
- {:ok, user} <-
- User.reset_password(auth_user, %{password: password, password_confirmation: password}) do
- conn
- |> assign(:auth_user, user)
- |> assign(:user, user)
- |> Pleroma.Web.Plugs.OAuthScopesPlug.skip_plug()
- else
- _ ->
- conn
- end
- end
-
- def call(conn, _) do
- conn
- end
-end
diff --git a/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex b/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex
index f44d4dee5..58cb0316a 100644
--- a/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex
+++ b/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex
@@ -1,8 +1,9 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do
+ alias Pleroma.Helpers.AuthHelper
alias Pleroma.Signature
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Utils
@@ -12,34 +13,16 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do
def init(options), do: options
- defp key_id_from_conn(conn) do
- with %{"keyId" => key_id} <- HTTPSignatures.signature_for_conn(conn),
- {:ok, ap_id} <- Signature.key_id_to_actor_id(key_id) do
- ap_id
- else
- _ ->
- nil
- end
- end
-
- defp user_from_key_id(conn) do
- with key_actor_id when is_binary(key_actor_id) <- key_id_from_conn(conn),
- {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(key_actor_id) do
- user
- else
- _ ->
- nil
- end
- end
-
- def call(%{assigns: %{user: _}} = conn, _opts), do: conn
+ def call(%{assigns: %{user: %User{}}} = conn, _opts), do: conn
# if this has payload make sure it is signed by the same actor that made it
def call(%{assigns: %{valid_signature: true}, params: %{"actor" => actor}} = conn, _opts) do
with actor_id <- Utils.get_ap_id(actor),
{:user, %User{} = user} <- {:user, user_from_key_id(conn)},
{:user_match, true} <- {:user_match, user.ap_id == actor_id} do
- assign(conn, :user, user)
+ conn
+ |> assign(:user, user)
+ |> AuthHelper.skip_oauth()
else
{:user_match, false} ->
Logger.debug("Failed to map identity from signature (payload actor mismatch)")
@@ -57,7 +40,9 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do
# no payload, probably a signed fetch
def call(%{assigns: %{valid_signature: true}} = conn, _opts) do
with %User{} = user <- user_from_key_id(conn) do
- assign(conn, :user, user)
+ conn
+ |> assign(:user, user)
+ |> AuthHelper.skip_oauth()
else
_ ->
Logger.debug("Failed to map identity from signature (no payload actor mismatch)")
@@ -68,4 +53,24 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do
# no signature at all
def call(conn, _opts), do: conn
+
+ defp key_id_from_conn(conn) do
+ with %{"keyId" => key_id} <- HTTPSignatures.signature_for_conn(conn),
+ {:ok, ap_id} <- Signature.key_id_to_actor_id(key_id) do
+ ap_id
+ else
+ _ ->
+ nil
+ end
+ end
+
+ defp user_from_key_id(conn) do
+ with key_actor_id when is_binary(key_actor_id) <- key_id_from_conn(conn),
+ {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(key_actor_id) do
+ user
+ else
+ _ ->
+ nil
+ end
+ end
end
diff --git a/lib/pleroma/web/plugs/o_auth_plug.ex b/lib/pleroma/web/plugs/o_auth_plug.ex
index c7b58d90f..5e06ac3f6 100644
--- a/lib/pleroma/web/plugs/o_auth_plug.ex
+++ b/lib/pleroma/web/plugs/o_auth_plug.ex
@@ -1,11 +1,14 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.OAuthPlug do
+ @moduledoc "Performs OAuth authentication by token from params / headers / cookies."
+
import Plug.Conn
import Ecto.Query
+ alias Pleroma.Helpers.AuthHelper
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.OAuth.App
@@ -17,45 +20,26 @@ defmodule Pleroma.Web.Plugs.OAuthPlug do
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
- def call(%{params: %{"access_token" => access_token}} = conn, _) do
- with {:ok, user, token_record} <- fetch_user_and_token(access_token) do
- conn
- |> assign(:token, token_record)
- |> assign(:user, user)
- else
- _ ->
- # token found, but maybe only with app
- with {:ok, app, token_record} <- fetch_app_and_token(access_token) do
- conn
- |> assign(:token, token_record)
- |> assign(:app, app)
- else
- _ -> conn
- end
- end
- end
-
def call(conn, _) do
- case fetch_token_str(conn) do
- {:ok, token} ->
- with {:ok, user, token_record} <- fetch_user_and_token(token) do
- conn
- |> assign(:token, token_record)
- |> assign(:user, user)
- else
- _ ->
- # token found, but maybe only with app
- with {:ok, app, token_record} <- fetch_app_and_token(token) do
- conn
- |> assign(:token, token_record)
- |> assign(:app, app)
- else
- _ -> conn
- end
- end
-
- _ ->
+ 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
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
+ conn
+ |> assign(:token, app_token)
+ |> assign(:app, app)
+ else
+ _ -> conn
+ end
+ end
+ else
+ _ -> conn
end
end
@@ -70,7 +54,6 @@ defmodule Pleroma.Web.Plugs.OAuthPlug do
preload: [user: user]
)
- # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
with %Token{user: user} = token_record <- Repo.one(query) do
{:ok, user, token_record}
end
@@ -86,29 +69,23 @@ defmodule Pleroma.Web.Plugs.OAuthPlug do
end
end
- # Gets token from session by :oauth_token key
+ # Gets token string from conn (in params / headers / session)
#
- @spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()}
- defp fetch_token_from_session(conn) do
- case get_session(conn, :oauth_token) do
- nil -> :no_token_found
- token -> {:ok, token}
- end
+ @spec fetch_token_str(Plug.Conn.t() | list(String.t())) :: :no_token_found | {:ok, String.t()}
+ defp fetch_token_str(%Plug.Conn{params: %{"access_token" => access_token}} = _conn) do
+ {:ok, access_token}
end
- # Gets token from headers
- #
- @spec fetch_token_str(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()}
defp fetch_token_str(%Plug.Conn{} = conn) do
headers = get_req_header(conn, "authorization")
- with :no_token_found <- fetch_token_str(headers),
- do: fetch_token_from_session(conn)
+ with {:ok, token} <- fetch_token_str(headers) do
+ {:ok, token}
+ else
+ _ -> fetch_token_from_session(conn)
+ end
end
- @spec fetch_token_str(Keyword.t()) :: :no_token_found | {:ok, String.t()}
- defp fetch_token_str([]), do: :no_token_found
-
defp fetch_token_str([token | tail]) do
trimmed_token = String.trim(token)
@@ -117,4 +94,14 @@ defmodule Pleroma.Web.Plugs.OAuthPlug do
_ -> fetch_token_str(tail)
end
end
+
+ defp fetch_token_str([]), do: :no_token_found
+
+ @spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()}
+ defp fetch_token_from_session(conn) do
+ case AuthHelper.get_session_token(conn) do
+ nil -> :no_token_found
+ token -> {:ok, token}
+ end
+ end
end
diff --git a/lib/pleroma/web/plugs/o_auth_scopes_plug.ex b/lib/pleroma/web/plugs/o_auth_scopes_plug.ex
index cfc30837c..f017c8bc7 100644
--- a/lib/pleroma/web/plugs/o_auth_scopes_plug.ex
+++ b/lib/pleroma/web/plugs/o_auth_scopes_plug.ex
@@ -1,12 +1,12 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.OAuthScopesPlug do
import Plug.Conn
import Pleroma.Web.Gettext
- alias Pleroma.Config
+ alias Pleroma.Helpers.AuthHelper
use Pleroma.Web, :plug
@@ -17,7 +17,6 @@ defmodule Pleroma.Web.Plugs.OAuthScopesPlug do
op = options[:op] || :|
token = assigns[:token]
- scopes = transform_scopes(scopes, options)
matched_scopes = (token && filter_descendants(scopes, token.scopes)) || []
cond do
@@ -28,7 +27,7 @@ defmodule Pleroma.Web.Plugs.OAuthScopesPlug do
conn
options[:fallback] == :proceed_unauthenticated ->
- drop_auth_info(conn)
+ AuthHelper.drop_auth_info(conn)
true ->
missing_scopes = scopes -- matched_scopes
@@ -44,15 +43,6 @@ defmodule Pleroma.Web.Plugs.OAuthScopesPlug do
end
end
- @doc "Drops authentication info from connection"
- def drop_auth_info(conn) do
- # To simplify debugging, setting a private variable on `conn` if auth info is dropped
- conn
- |> put_private(:authentication_ignored, true)
- |> assign(:user, nil)
- |> assign(:token, nil)
- end
-
@doc "Keeps those of `scopes` which are descendants of `supported_scopes`"
def filter_descendants(scopes, supported_scopes) do
Enum.filter(
@@ -65,13 +55,4 @@ defmodule Pleroma.Web.Plugs.OAuthScopesPlug do
end
)
end
-
- @doc "Transforms scopes by applying supported options (e.g. :admin)"
- def transform_scopes(scopes, options) do
- if options[:admin] do
- Config.oauth_admin_scopes(scopes)
- else
- scopes
- end
- end
end
diff --git a/lib/pleroma/web/plugs/plug_helper.ex b/lib/pleroma/web/plugs/plug_helper.ex
index b314e7596..d73021bf7 100644
--- a/lib/pleroma/web/plugs/plug_helper.ex
+++ b/lib/pleroma/web/plugs/plug_helper.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.PlugHelper do
diff --git a/lib/pleroma/web/plugs/rate_limiter.ex b/lib/pleroma/web/plugs/rate_limiter.ex
index a589610d1..5bebe0ad5 100644
--- a/lib/pleroma/web/plugs/rate_limiter.ex
+++ b/lib/pleroma/web/plugs/rate_limiter.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.RateLimiter do
@@ -72,6 +72,8 @@ defmodule Pleroma.Web.Plugs.RateLimiter do
require Logger
+ @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
+
@doc false
def init(plug_opts) do
plug_opts
@@ -124,7 +126,7 @@ defmodule Pleroma.Web.Plugs.RateLimiter do
key_name = make_key_name(action_settings)
limit = get_limits(action_settings)
- case Cachex.get(bucket_name, key_name) do
+ case @cachex.get(bucket_name, key_name) do
{:error, :no_cache} ->
@inspect_bucket_not_found
@@ -157,7 +159,7 @@ defmodule Pleroma.Web.Plugs.RateLimiter do
key_name = make_key_name(action_settings)
limit = get_limits(action_settings)
- case Cachex.get_and_update(bucket_name, key_name, &increment_value(&1, limit)) do
+ case @cachex.get_and_update(bucket_name, key_name, &increment_value(&1, limit)) do
{:commit, value} ->
{:ok, value}
diff --git a/lib/pleroma/web/plugs/rate_limiter/limiter_supervisor.ex b/lib/pleroma/web/plugs/rate_limiter/limiter_supervisor.ex
index 5642bb205..3db59bf17 100644
--- a/lib/pleroma/web/plugs/rate_limiter/limiter_supervisor.ex
+++ b/lib/pleroma/web/plugs/rate_limiter/limiter_supervisor.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.RateLimiter.LimiterSupervisor do
diff --git a/lib/pleroma/web/plugs/rate_limiter/supervisor.ex b/lib/pleroma/web/plugs/rate_limiter/supervisor.ex
index a1c84063d..0dc2aa71b 100644
--- a/lib/pleroma/web/plugs/rate_limiter/supervisor.ex
+++ b/lib/pleroma/web/plugs/rate_limiter/supervisor.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.RateLimiter.Supervisor do
diff --git a/lib/pleroma/web/plugs/remote_ip.ex b/lib/pleroma/web/plugs/remote_ip.ex
index 401e2cbfa..4d7daca56 100644
--- a/lib/pleroma/web/plugs/remote_ip.ex
+++ b/lib/pleroma/web/plugs/remote_ip.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.RemoteIp do
diff --git a/lib/pleroma/web/plugs/session_authentication_plug.ex b/lib/pleroma/web/plugs/session_authentication_plug.ex
deleted file mode 100644
index 6e176d553..000000000
--- a/lib/pleroma/web/plugs/session_authentication_plug.ex
+++ /dev/null
@@ -1,21 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.Plugs.SessionAuthenticationPlug do
- import Plug.Conn
-
- def init(options) do
- options
- end
-
- def call(conn, _) do
- with saved_user_id <- get_session(conn, :user_id),
- %{auth_user: %{id: ^saved_user_id}} <- conn.assigns do
- conn
- |> assign(:user, conn.assigns.auth_user)
- else
- _ -> conn
- end
- end
-end
diff --git a/lib/pleroma/web/plugs/set_format_plug.ex b/lib/pleroma/web/plugs/set_format_plug.ex
index c16d2f81d..7ef88f305 100644
--- a/lib/pleroma/web/plugs/set_format_plug.ex
+++ b/lib/pleroma/web/plugs/set_format_plug.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.SetFormatPlug do
diff --git a/lib/pleroma/web/plugs/set_locale_plug.ex b/lib/pleroma/web/plugs/set_locale_plug.ex
index d9d24b93f..d77191cff 100644
--- a/lib/pleroma/web/plugs/set_locale_plug.ex
+++ b/lib/pleroma/web/plugs/set_locale_plug.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
# NOTE: this module is based on https://github.com/smeevil/set_locale
diff --git a/lib/pleroma/web/plugs/set_user_session_id_plug.ex b/lib/pleroma/web/plugs/set_user_session_id_plug.ex
index e520159e4..a1cfa0915 100644
--- a/lib/pleroma/web/plugs/set_user_session_id_plug.ex
+++ b/lib/pleroma/web/plugs/set_user_session_id_plug.ex
@@ -1,18 +1,17 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.SetUserSessionIdPlug do
- import Plug.Conn
- alias Pleroma.User
+ alias Pleroma.Helpers.AuthHelper
+ alias Pleroma.Web.OAuth.Token
def init(opts) do
opts
end
- def call(%{assigns: %{user: %User{id: id}}} = conn, _) do
- conn
- |> put_session(:user_id, id)
+ def call(%{assigns: %{token: %Token{} = oauth_token}} = conn, _) do
+ AuthHelper.put_session_token(conn, oauth_token.token)
end
def call(conn, _), do: conn
diff --git a/lib/pleroma/web/plugs/static_fe_plug.ex b/lib/pleroma/web/plugs/static_fe_plug.ex
index 658a1052e..9ba9dc5ff 100644
--- a/lib/pleroma/web/plugs/static_fe_plug.ex
+++ b/lib/pleroma/web/plugs/static_fe_plug.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.StaticFEPlug do
diff --git a/lib/pleroma/web/plugs/trailing_format_plug.ex b/lib/pleroma/web/plugs/trailing_format_plug.ex
index e3f57c14a..c5069ae0e 100644
--- a/lib/pleroma/web/plugs/trailing_format_plug.ex
+++ b/lib/pleroma/web/plugs/trailing_format_plug.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.TrailingFormatPlug do
diff --git a/lib/pleroma/web/plugs/uploaded_media.ex b/lib/pleroma/web/plugs/uploaded_media.ex
index 402a8bb34..2378e98d2 100644
--- a/lib/pleroma/web/plugs/uploaded_media.ex
+++ b/lib/pleroma/web/plugs/uploaded_media.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.UploadedMedia do
@@ -62,7 +62,7 @@ defmodule Pleroma.Web.Plugs.UploadedMedia do
def call(conn, _opts), do: conn
defp media_is_banned(%{request_path: path} = _conn, {:static_dir, _}) do
- MediaProxy.in_banned_urls(Pleroma.Web.base_url() <> path)
+ MediaProxy.in_banned_urls(Pleroma.Upload.base_url() <> path)
end
defp media_is_banned(_, {:url, url}), do: MediaProxy.in_banned_urls(url)
@@ -87,8 +87,15 @@ defmodule Pleroma.Web.Plugs.UploadedMedia do
end
defp get_media(conn, {:url, url}, true, _) do
+ proxy_opts = [
+ http: [
+ follow_redirect: true,
+ pool: :upload
+ ]
+ ]
+
conn
- |> Pleroma.ReverseProxy.call(url, Pleroma.Config.get([Pleroma.Upload, :proxy_opts], []))
+ |> Pleroma.ReverseProxy.call(url, proxy_opts)
end
defp get_media(conn, {:url, url}, _, _) do
diff --git a/lib/pleroma/web/plugs/user_enabled_plug.ex b/lib/pleroma/web/plugs/user_enabled_plug.ex
index fa28ee48b..1142a8dbc 100644
--- a/lib/pleroma/web/plugs/user_enabled_plug.ex
+++ b/lib/pleroma/web/plugs/user_enabled_plug.ex
@@ -1,9 +1,9 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.UserEnabledPlug do
- import Plug.Conn
+ alias Pleroma.Helpers.AuthHelper
alias Pleroma.User
def init(options) do
@@ -11,9 +11,10 @@ defmodule Pleroma.Web.Plugs.UserEnabledPlug do
end
def call(%{assigns: %{user: %User{} = user}} = conn, _) do
- case User.account_status(user) do
- :active -> conn
- _ -> assign(conn, :user, nil)
+ if User.account_status(user) == :active do
+ conn
+ else
+ AuthHelper.drop_auth_info(conn)
end
end
diff --git a/lib/pleroma/web/plugs/user_fetcher_plug.ex b/lib/pleroma/web/plugs/user_fetcher_plug.ex
index 4039600da..707df9bfd 100644
--- a/lib/pleroma/web/plugs/user_fetcher_plug.ex
+++ b/lib/pleroma/web/plugs/user_fetcher_plug.ex
@@ -1,8 +1,14 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.UserFetcherPlug do
+ @moduledoc """
+ Assigns `:auth_user` basing on `:auth_credentials`.
+
+ NOTE: no checks are performed at this step, auth_credentials/username could be easily faked.
+ """
+
alias Pleroma.User
import Plug.Conn
diff --git a/lib/pleroma/web/plugs/user_is_admin_plug.ex b/lib/pleroma/web/plugs/user_is_admin_plug.ex
index 531c965f0..7649912ba 100644
--- a/lib/pleroma/web/plugs/user_is_admin_plug.ex
+++ b/lib/pleroma/web/plugs/user_is_admin_plug.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.UserIsAdminPlug do
diff --git a/lib/pleroma/web/plugs/user_tracking_plug.ex b/lib/pleroma/web/plugs/user_tracking_plug.ex
new file mode 100644
index 000000000..c9a988f00
--- /dev/null
+++ b/lib/pleroma/web/plugs/user_tracking_plug.ex
@@ -0,0 +1,30 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.UserTrackingPlug do
+ alias Pleroma.User
+
+ import Plug.Conn, only: [assign: 3]
+
+ @update_interval :timer.hours(24)
+
+ def init(opts), do: opts
+
+ def call(%{assigns: %{user: %User{id: id} = user}} = conn, _) when not is_nil(id) do
+ with true <- needs_update?(user),
+ {:ok, user} <- User.update_last_active_at(user) do
+ assign(conn, :user, user)
+ else
+ _ -> conn
+ end
+ end
+
+ def call(conn, _), do: conn
+
+ defp needs_update?(%User{last_active_at: nil}), do: true
+
+ defp needs_update?(%User{last_active_at: last_active_at}) do
+ NaiveDateTime.diff(NaiveDateTime.utc_now(), last_active_at, :millisecond) >= @update_interval
+ end
+end
diff --git a/lib/pleroma/web/preload.ex b/lib/pleroma/web/preload.ex
index 90e454468..e8588bcc9 100644
--- a/lib/pleroma/web/preload.ex
+++ b/lib/pleroma/web/preload.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Preload do
diff --git a/lib/pleroma/web/preload/providers/instance.ex b/lib/pleroma/web/preload/providers/instance.ex
index a549bb1eb..eb0254c74 100644
--- a/lib/pleroma/web/preload/providers/instance.ex
+++ b/lib/pleroma/web/preload/providers/instance.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Preload.Providers.Instance do
diff --git a/lib/pleroma/web/preload/providers/provider.ex b/lib/pleroma/web/preload/providers/provider.ex
index 7ef595a34..60f304f2c 100644
--- a/lib/pleroma/web/preload/providers/provider.ex
+++ b/lib/pleroma/web/preload/providers/provider.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Preload.Providers.Provider do
diff --git a/lib/pleroma/web/preload/providers/timelines.ex b/lib/pleroma/web/preload/providers/timelines.ex
index b279a865d..c1704ccdc 100644
--- a/lib/pleroma/web/preload/providers/timelines.ex
+++ b/lib/pleroma/web/preload/providers/timelines.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Preload.Providers.Timelines do
diff --git a/lib/pleroma/web/preload/providers/user.ex b/lib/pleroma/web/preload/providers/user.ex
index b3d2e9b8d..504f79ba0 100644
--- a/lib/pleroma/web/preload/providers/user.ex
+++ b/lib/pleroma/web/preload/providers/user.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Preload.Providers.User do
diff --git a/lib/pleroma/web/push.ex b/lib/pleroma/web/push.ex
index b80a6438d..154dae614 100644
--- a/lib/pleroma/web/push.ex
+++ b/lib/pleroma/web/push.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Push do
diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex
index da535aa68..83cbdc870 100644
--- a/lib/pleroma/web/push/impl.ex
+++ b/lib/pleroma/web/push/impl.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Push.Impl do
@@ -16,7 +16,7 @@ defmodule Pleroma.Web.Push.Impl do
require Logger
import Ecto.Query
- @types ["Create", "Follow", "Announce", "Like", "Move"]
+ @types ["Create", "Follow", "Announce", "Like", "Move", "EmojiReact"]
@doc "Performs sending notifications for user subscriptions"
@spec perform(Notification.t()) :: list(any) | :error | {:error, :unknown_type}
@@ -32,7 +32,7 @@ defmodule Pleroma.Web.Push.Impl do
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, false)
+ object = Object.normalize(activity, fetch: false)
user = User.get_cached_by_id(user_id)
direct_conversation_id = Activity.direct_conversation_id(activity, user)
@@ -150,6 +150,15 @@ defmodule Pleroma.Web.Push.Impl do
end
def format_body(
+ %{activity: %{data: %{"type" => "EmojiReact", "content" => content}}},
+ actor,
+ _object,
+ _mastodon_type
+ ) do
+ "@#{actor.nickname} reacted with #{content}"
+ end
+
+ def format_body(
%{activity: %{data: %{"type" => type}}} = notification,
actor,
_object,
@@ -179,6 +188,7 @@ defmodule Pleroma.Web.Push.Impl do
"reblog" -> "New Repeat"
"favourite" -> "New Favorite"
"pleroma:chat_mention" -> "New Chat Message"
+ "pleroma:emoji_reaction" -> "New Reaction"
type -> "New #{String.capitalize(type || "event")}"
end
end
diff --git a/lib/pleroma/web/push/subscription.ex b/lib/pleroma/web/push/subscription.ex
index 5b5aa0d59..4f6c9bc9f 100644
--- a/lib/pleroma/web/push/subscription.ex
+++ b/lib/pleroma/web/push/subscription.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Push.Subscription do
@@ -25,7 +25,8 @@ defmodule Pleroma.Web.Push.Subscription do
timestamps()
end
- @supported_alert_types ~w[follow favourite mention reblog pleroma:chat_mention]a
+ # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
+ @supported_alert_types ~w[follow favourite mention reblog pleroma:chat_mention pleroma:emoji_reaction]a
defp alerts(%{data: %{alerts: alerts}}) do
alerts = Map.take(alerts, @supported_alert_types)
diff --git a/lib/pleroma/web/rel_me.ex b/lib/pleroma/web/rel_me.ex
index 28f75b18d..7e745d07e 100644
--- a/lib/pleroma/web/rel_me.ex
+++ b/lib/pleroma/web/rel_me.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RelMe do
@@ -12,8 +12,9 @@ defmodule Pleroma.Web.RelMe do
if Pleroma.Config.get(:env) == :test do
def parse(url) when is_binary(url), do: parse_url(url)
else
+ @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
def parse(url) when is_binary(url) do
- Cachex.fetch!(:rel_me_cache, url, fn _ ->
+ @cachex.fetch!(:rel_me_cache, url, fn _ ->
{:commit, parse_url(url)}
end)
rescue
diff --git a/lib/pleroma/web/rich_media/helpers.ex b/lib/pleroma/web/rich_media/helpers.ex
index d67b594b5..566fc8c8a 100644
--- a/lib/pleroma/web/rich_media/helpers.ex
+++ b/lib/pleroma/web/rich_media/helpers.ex
@@ -69,7 +69,7 @@ defmodule Pleroma.Web.RichMedia.Helpers do
def fetch_data_for_activity(%Activity{data: %{"type" => "Create"}} = activity) do
with true <- Config.get([:rich_media, :enabled]),
- %Object{} = object <- Object.normalize(activity) do
+ %Object{} = object <- Object.normalize(activity, fetch: false) do
fetch_data_for_object(object)
else
_ -> %{}
@@ -78,11 +78,6 @@ defmodule Pleroma.Web.RichMedia.Helpers do
def fetch_data_for_activity(_), do: %{}
- def perform(:fetch, %Activity{} = activity) do
- fetch_data_for_activity(activity)
- :ok
- end
-
def rich_media_get(url) do
headers = [{"user-agent", Pleroma.Application.user_agent() <> "; Bot"}]
diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex
index c70d2fdba..d6b54943b 100644
--- a/lib/pleroma/web/rich_media/parser.ex
+++ b/lib/pleroma/web/rich_media/parser.ex
@@ -1,10 +1,12 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Parser do
require Logger
+ @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
+
defp parsers do
Pleroma.Config.get([:rich_media, :parsers])
end
@@ -24,7 +26,7 @@ defmodule Pleroma.Web.RichMedia.Parser do
end
defp get_cached_or_parse(url) do
- case Cachex.fetch(:rich_media_cache, url, fn ->
+ case @cachex.fetch(:rich_media_cache, url, fn ->
case parse_url(url) do
{:ok, _} = res ->
{:commit, res}
@@ -64,7 +66,7 @@ defmodule Pleroma.Web.RichMedia.Parser do
defp set_error_ttl(url, _reason) do
ttl = Pleroma.Config.get([:rich_media, :failure_backoff], 60_000)
- Cachex.expire(:rich_media_cache, url, ttl)
+ @cachex.expire(:rich_media_cache, url, ttl)
:ok
end
@@ -106,7 +108,7 @@ defmodule Pleroma.Web.RichMedia.Parser do
{:ok, ttl} when is_number(ttl) ->
ttl = ttl * 1000
- case Cachex.expire_at(:rich_media_cache, url, ttl) do
+ case @cachex.expire_at(:rich_media_cache, url, ttl) do
{:ok, true} -> {:ok, ttl}
{:ok, false} -> {:error, :no_key}
end
diff --git a/lib/pleroma/web/rich_media/parser/ttl.ex b/lib/pleroma/web/rich_media/parser/ttl.ex
index 8353f0fff..0b7f14fb2 100644
--- a/lib/pleroma/web/rich_media/parser/ttl.ex
+++ b/lib/pleroma/web/rich_media/parser/ttl.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Parser.TTL do
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 fc4ef79c0..c7eb267f3 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
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl do
diff --git a/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex b/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex
index 3d577e254..31c3d1e33 100644
--- a/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex
+++ b/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Parsers.MetaTagsParser do
diff --git a/lib/pleroma/web/rich_media/parsers/o_embed.ex b/lib/pleroma/web/rich_media/parsers/o_embed.ex
index 1fe6729c3..09eabec56 100644
--- a/lib/pleroma/web/rich_media/parsers/o_embed.ex
+++ b/lib/pleroma/web/rich_media/parsers/o_embed.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Parsers.OEmbed do
diff --git a/lib/pleroma/web/rich_media/parsers/ogp.ex b/lib/pleroma/web/rich_media/parsers/ogp.ex
index b3b3b059c..d0edf1c88 100644
--- a/lib/pleroma/web/rich_media/parsers/ogp.ex
+++ b/lib/pleroma/web/rich_media/parsers/ogp.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Parsers.OGP do
diff --git a/lib/pleroma/web/rich_media/parsers/twitter_card.ex b/lib/pleroma/web/rich_media/parsers/twitter_card.ex
index 4a04865d2..0adf84159 100644
--- a/lib/pleroma/web/rich_media/parsers/twitter_card.ex
+++ b/lib/pleroma/web/rich_media/parsers/twitter_card.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Parsers.TwitterCard do
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 07a574f35..72ad14f05 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Router do
@@ -34,13 +34,16 @@ defmodule Pleroma.Web.Router do
plug(:fetch_session)
plug(Pleroma.Web.Plugs.OAuthPlug)
plug(Pleroma.Web.Plugs.UserEnabledPlug)
+ plug(Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug)
end
- pipeline :expect_authentication do
+ # Note: expects _user_ authentication (user-unbound app-bound tokens don't qualify)
+ pipeline :expect_user_authentication do
plug(Pleroma.Web.Plugs.ExpectAuthenticatedCheckPlug)
end
- pipeline :expect_public_instance_or_authentication do
+ # Note: expects public instance or _user_ authentication (user-unbound tokens don't qualify)
+ pipeline :expect_public_instance_or_user_authentication do
plug(Pleroma.Web.Plugs.ExpectPublicOrAuthenticatedCheckPlug)
end
@@ -48,15 +51,14 @@ defmodule Pleroma.Web.Router do
plug(Pleroma.Web.Plugs.OAuthPlug)
plug(Pleroma.Web.Plugs.BasicAuthDecoderPlug)
plug(Pleroma.Web.Plugs.UserFetcherPlug)
- plug(Pleroma.Web.Plugs.SessionAuthenticationPlug)
- plug(Pleroma.Web.Plugs.LegacyAuthenticationPlug)
plug(Pleroma.Web.Plugs.AuthenticationPlug)
end
pipeline :after_auth do
plug(Pleroma.Web.Plugs.UserEnabledPlug)
plug(Pleroma.Web.Plugs.SetUserSessionIdPlug)
- plug(Pleroma.Web.Plugs.EnsureUserKeyPlug)
+ plug(Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug)
+ plug(Pleroma.Web.Plugs.UserTrackingPlug)
end
pipeline :base_api do
@@ -66,23 +68,30 @@ defmodule Pleroma.Web.Router do
plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
end
- pipeline :api do
- plug(:expect_public_instance_or_authentication)
+ pipeline :no_auth_or_privacy_expectations_api do
plug(:base_api)
plug(:after_auth)
plug(Pleroma.Web.Plugs.IdempotencyPlug)
end
+ # Pipeline for app-related endpoints (no user auth checks — app-bound tokens must be supported)
+ pipeline :app_api do
+ plug(:no_auth_or_privacy_expectations_api)
+ end
+
+ pipeline :api do
+ plug(:expect_public_instance_or_user_authentication)
+ plug(:no_auth_or_privacy_expectations_api)
+ end
+
pipeline :authenticated_api do
- plug(:expect_authentication)
- plug(:base_api)
- plug(:after_auth)
+ plug(:expect_user_authentication)
+ plug(:no_auth_or_privacy_expectations_api)
plug(Pleroma.Web.Plugs.EnsureAuthenticatedPlug)
- plug(Pleroma.Web.Plugs.IdempotencyPlug)
end
pipeline :admin_api do
- plug(:expect_authentication)
+ plug(:expect_user_authentication)
plug(:base_api)
plug(Pleroma.Web.Plugs.AdminSecretAuthenticationPlug)
plug(:after_auth)
@@ -100,7 +109,7 @@ defmodule Pleroma.Web.Router do
pipeline :pleroma_html do
plug(:browser)
plug(:authenticate)
- plug(Pleroma.Web.Plugs.EnsureUserKeyPlug)
+ plug(Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug)
end
pipeline :well_known do
@@ -131,7 +140,7 @@ defmodule Pleroma.Web.Router do
plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug)
end
- scope "/api/pleroma", Pleroma.Web.TwitterAPI do
+ scope "/api/v1/pleroma", Pleroma.Web.TwitterAPI do
pipe_through(:pleroma_api)
get("/password_reset/:token", PasswordController, :reset, as: :reset_password)
@@ -141,24 +150,15 @@ defmodule Pleroma.Web.Router do
get("/healthcheck", UtilController, :healthcheck)
end
- scope "/api/pleroma", Pleroma.Web do
+ scope "/api/v1/pleroma", Pleroma.Web do
pipe_through(:pleroma_api)
post("/uploader_callback/:upload_path", UploaderController, :callback)
end
- scope "/api/pleroma/admin", Pleroma.Web.AdminAPI do
+ scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:admin_api)
- post("/users/follow", AdminAPIController, :user_follow)
- post("/users/unfollow", AdminAPIController, :user_unfollow)
-
put("/users/disable_mfa", AdminAPIController, :disable_mfa)
- delete("/users", AdminAPIController, :user_delete)
- post("/users", AdminAPIController, :users_create)
- patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation)
- patch("/users/activate", AdminAPIController, :user_activate)
- patch("/users/deactivate", AdminAPIController, :user_deactivate)
- patch("/users/approve", AdminAPIController, :user_approve)
put("/users/tag", AdminAPIController, :tag_users)
delete("/users/tag", AdminAPIController, :untag_users)
@@ -181,6 +181,15 @@ defmodule Pleroma.Web.Router do
:right_delete_multiple
)
+ post("/users/follow", UserController, :follow)
+ post("/users/unfollow", UserController, :unfollow)
+ delete("/users", UserController, :delete)
+ post("/users", UserController, :create)
+ patch("/users/:nickname/toggle_activation", UserController, :toggle_activation)
+ patch("/users/activate", UserController, :activate)
+ patch("/users/deactivate", UserController, :deactivate)
+ patch("/users/approve", UserController, :approve)
+
get("/relay", RelayController, :index)
post("/relay", RelayController, :follow)
delete("/relay", RelayController, :unfollow)
@@ -195,8 +204,8 @@ defmodule Pleroma.Web.Router do
get("/users/:nickname/credentials", AdminAPIController, :show_user_credentials)
patch("/users/:nickname/credentials", AdminAPIController, :update_user_credentials)
- get("/users", AdminAPIController, :list_users)
- get("/users/:nickname", AdminAPIController, :user_show)
+ get("/users", UserController, :list)
+ get("/users/:nickname", UserController, :show)
get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses)
get("/users/:nickname/chats", AdminAPIController, :list_user_chats)
@@ -243,9 +252,14 @@ defmodule Pleroma.Web.Router do
get("/chats/:id", ChatController, :show)
get("/chats/:id/messages", ChatController, :messages)
delete("/chats/:id/messages/:message_id", ChatController, :delete_message)
+
+ get("/frontends", FrontendController, :index)
+ post("/frontends/install", FrontendController, :install)
+
+ post("/backups", AdminAPIController, :create_backup)
end
- scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do
+ scope "/api/v1/pleroma/emoji", Pleroma.Web.PleromaAPI do
scope "/pack" do
pipe_through(:admin_api)
@@ -287,7 +301,6 @@ defmodule Pleroma.Web.Router do
post("/main/ostatus", UtilController, :remote_subscribe)
get("/ostatus_subscribe", RemoteFollowController, :follow)
-
post("/ostatus_subscribe", RemoteFollowController, :do_follow)
end
@@ -316,19 +329,27 @@ defmodule Pleroma.Web.Router do
end
scope "/oauth", Pleroma.Web.OAuth do
+ # Note: use /api/v1/accounts/verify_credentials for userinfo of signed-in user
+
+ get("/registration_details", OAuthController, :registration_details)
+
+ post("/mfa/verify", MFAController, :verify, as: :mfa_verify)
+ get("/mfa", MFAController, :show)
+
scope [] do
pipe_through(:oauth)
+
get("/authorize", OAuthController, :authorize)
+ post("/authorize", OAuthController, :create_authorization)
end
- post("/authorize", OAuthController, :create_authorization)
- post("/token", OAuthController, :token_exchange)
- post("/revoke", OAuthController, :token_revoke)
- get("/registration_details", OAuthController, :registration_details)
+ scope [] do
+ pipe_through(:fetch_session)
- post("/mfa/challenge", MFAController, :challenge)
- post("/mfa/verify", MFAController, :verify, as: :mfa_verify)
- get("/mfa", MFAController, :show)
+ post("/token", OAuthController, :token_exchange)
+ post("/revoke", OAuthController, :token_revoke)
+ post("/mfa/challenge", MFAController, :challenge)
+ end
scope [] do
pipe_through(:browser)
@@ -347,6 +368,12 @@ defmodule Pleroma.Web.Router do
get("/statuses/:id/reactions", EmojiReactionController, :index)
end
+ scope "/api/v0/pleroma", Pleroma.Web.PleromaAPI do
+ pipe_through(:authenticated_api)
+ get("/reports", ReportController, :index)
+ get("/reports/:id", ReportController, :show)
+ end
+
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
scope [] do
pipe_through(:authenticated_api)
@@ -373,6 +400,9 @@ defmodule Pleroma.Web.Router do
put("/mascot", MascotController, :update)
post("/scrobble", ScrobbleController, :create)
+
+ get("/backups", BackupController, :index)
+ post("/backups", BackupController, :create)
end
scope [] do
@@ -393,6 +423,14 @@ 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
+ scope [] do
+ pipe_through(:authenticated_api)
+ get("/chats", ChatController, :index2)
+ end
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
@@ -416,10 +454,9 @@ defmodule Pleroma.Web.Router do
post("/accounts/:id/mute", AccountController, :mute)
post("/accounts/:id/unmute", AccountController, :unmute)
- get("/apps/verify_credentials", AppController, :verify_credentials)
-
get("/conversations", ConversationController, :index)
post("/conversations/:id/read", ConversationController, :mark_as_read)
+ delete("/conversations/:id", ConversationController, :delete)
get("/domain_blocks", DomainBlockController, :index)
post("/domain_blocks", DomainBlockController, :create)
@@ -509,6 +546,13 @@ defmodule Pleroma.Web.Router do
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
+ pipe_through(:app_api)
+
+ post("/apps", AppController, :create)
+ get("/apps/verify_credentials", AppController, :verify_credentials)
+ end
+
+ scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:api)
get("/accounts/search", SearchController, :account_search)
@@ -524,8 +568,6 @@ defmodule Pleroma.Web.Router do
get("/instance", InstanceController, :show)
get("/instance/peers", InstanceController, :peers)
- post("/apps", AppController, :create)
-
get("/statuses", StatusController, :index)
get("/statuses/:id", StatusController, :show)
get("/statuses/:id/context", StatusController, :context)
@@ -773,6 +815,7 @@ defmodule Pleroma.Web.Router do
scope "/", Pleroma.Web.Fallback do
get("/registration/:token", RedirectController, :registration_page)
get("/:maybe_nickname_or_id", RedirectController, :redirector_with_meta)
+ match(:*, "/api/pleroma*path", LegacyPleromaApiRerouterPlug, [])
get("/api*path", RedirectController, :api_not_implemented)
get("/*path", RedirectController, :redirector_with_preload)
diff --git a/lib/pleroma/web/static_fe/static_fe_controller.ex b/lib/pleroma/web/static_fe/static_fe_controller.ex
index bdec0897a..fe485d10d 100644
--- a/lib/pleroma/web/static_fe/static_fe_controller.ex
+++ b/lib/pleroma/web/static_fe/static_fe_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.StaticFE.StaticFEController do
@@ -122,7 +122,7 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do
end
defp get_counts(%Activity{} = activity) do
- %Object{data: data} = Object.normalize(activity)
+ %Object{data: data} = Object.normalize(activity, fetch: false)
%{
likes: data["like_count"] || 0,
diff --git a/lib/pleroma/web/static_fe/static_fe_view.ex b/lib/pleroma/web/static_fe/static_fe_view.ex
index b3d1d1ec8..c04715337 100644
--- a/lib/pleroma/web/static_fe/static_fe_view.ex
+++ b/lib/pleroma/web/static_fe/static_fe_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.StaticFE.StaticFEView do
diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex
index d618dfe54..fc3bbb130 100644
--- a/lib/pleroma/web/streamer.ex
+++ b/lib/pleroma/web/streamer.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Streamer do
@@ -36,9 +36,8 @@ defmodule Pleroma.Web.Streamer do
) ::
{:ok, topic :: String.t()} | {:error, :bad_topic} | {:error, :unauthorized}
def get_topic_and_add_socket(stream, user, oauth_token, params \\ %{}) do
- case get_topic(stream, user, oauth_token, params) do
- {:ok, topic} -> add_socket(topic, user)
- error -> error
+ with {:ok, topic} <- get_topic(stream, user, oauth_token, params) do
+ add_socket(topic, user)
end
end
@@ -57,14 +56,23 @@ defmodule Pleroma.Web.Streamer do
{:ok, "hashtag:" <> tag}
end
+ # Allow remote instance streams.
+ def get_topic("public:remote", _user, _oauth_token, %{"instance" => instance} = _params) do
+ {:ok, "public:remote:" <> instance}
+ end
+
+ def get_topic("public:remote:media", _user, _oauth_token, %{"instance" => instance} = _params) do
+ {:ok, "public:remote:media:" <> instance}
+ end
+
# Expand user streams.
def get_topic(
stream,
%User{id: user_id} = user,
- %Token{user_id: token_user_id} = oauth_token,
+ %Token{user_id: user_id} = oauth_token,
_params
)
- when stream in @user_streams and user_id == token_user_id do
+ when stream in @user_streams do
# Note: "read" works for all user streams (not mentioning it since it's an ancestor scope)
required_scopes =
if stream == "user:notification" do
@@ -88,10 +96,9 @@ defmodule Pleroma.Web.Streamer do
def get_topic(
"list",
%User{id: user_id} = user,
- %Token{user_id: token_user_id} = oauth_token,
+ %Token{user_id: user_id} = oauth_token,
%{"list" => id}
- )
- when user_id == token_user_id do
+ ) do
cond do
OAuthScopesPlug.filter_descendants(["read", "read:lists"], oauth_token.scopes) == [] ->
{:error, :unauthorized}
@@ -128,16 +135,10 @@ defmodule Pleroma.Web.Streamer do
def stream(topics, items) do
if should_env_send?() do
- List.wrap(topics)
- |> Enum.each(fn topic ->
- List.wrap(items)
- |> Enum.each(fn item ->
- spawn(fn -> do_stream(topic, item) end)
- end)
- end)
+ for topic <- List.wrap(topics), item <- List.wrap(items) do
+ spawn(fn -> do_stream(topic, item) end)
+ end
end
-
- :ok
end
def filtered_by_user?(user, item, streamed_type \\ :activity)
@@ -150,9 +151,8 @@ defmodule Pleroma.Web.Streamer do
recipients = MapSet.new(item.recipients)
domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
- with parent <- Object.normalize(item) || item,
- true <-
- Enum.all?([blocked_ap_ids, muted_ap_ids], &(item.actor not in &1)),
+ with parent <- Object.normalize(item, fetch: false) || item,
+ true <- Enum.all?([blocked_ap_ids, muted_ap_ids], &(item.actor not in &1)),
true <- item.data["type"] != "Announce" || item.actor not in reblog_muted_ap_ids,
true <-
!(streamed_type == :activity && item.data["type"] == "Announce" &&
@@ -186,6 +186,19 @@ defmodule Pleroma.Web.Streamer do
end)
end
+ defp do_stream("follow_relationship", item) do
+ text = StreamerView.render("follow_relationships_update.json", item)
+ user_topic = "user:#{item.follower.id}"
+
+ Logger.debug("Trying to push follow relationship update to #{user_topic}\n\n")
+
+ Registry.dispatch(@registry, user_topic, fn list ->
+ Enum.each(list, fn {pid, _auth} ->
+ send(pid, {:text, text})
+ end)
+ end)
+ end
+
defp do_stream("participation", participation) do
user_topic = "direct:#{participation.user_id}"
Logger.debug("Trying to push a conversation participation to #{user_topic}\n\n")
diff --git a/lib/pleroma/web/templates/email/digest.html.eex b/lib/pleroma/web/templates/email/digest.html.eex
index 860df5f9c..60eceff22 100644
--- a/lib/pleroma/web/templates/email/digest.html.eex
+++ b/lib/pleroma/web/templates/email/digest.html.eex
@@ -126,7 +126,7 @@
<div align="center" class="img-container center"
style="padding-right: 0px;padding-left: 0px;">
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr style="line-height:0px"><td style="padding-right: 0px;padding-left: 0px;" align="center"><![endif]--><img
- align="center" alt="Image" border="0" class="center" src="cid:logo.png"
+ align="center" alt="Image" border="0" class="center" src="cid:logo.svg"
style="text-decoration: none; -ms-interpolation-mode: bicubic; border: 0; height: 80px; width: auto; max-height: 80px; display: block;"
title="Image" height="80" />
<!--[if mso]></td></tr></table><![endif]-->
diff --git a/lib/pleroma/web/templates/embed/show.html.eex b/lib/pleroma/web/templates/embed/show.html.eex
index 05a3f0ee3..092b52b70 100644
--- a/lib/pleroma/web/templates/embed/show.html.eex
+++ b/lib/pleroma/web/templates/embed/show.html.eex
@@ -6,7 +6,7 @@
</div>
<span class="display-name" style="padding-left: 0.5em;">
<bdi><%= raw (@author.name |> Formatter.emojify(@author.emoji)) %></bdi>
- <span class="nickname"><%= full_nickname(@author) %></span>
+ <span class="nickname">@<%= full_nickname(@author) %></span>
</span>
</a>
</div>
diff --git a/lib/pleroma/web/templates/feed/feed/_activity.atom.eex b/lib/pleroma/web/templates/feed/feed/_activity.atom.eex
index 78350f2aa..3fd150c4e 100644
--- a/lib/pleroma/web/templates/feed/feed/_activity.atom.eex
+++ b/lib/pleroma/web/templates/feed/feed/_activity.atom.eex
@@ -12,7 +12,7 @@
<link href="<%= activity_context(@activity) %>" rel="ostatus:conversation"/>
<%= if @data["summary"] do %>
- <summary><%= @data["summary"] %></summary>
+ <summary><%= escape(@data["summary"]) %></summary>
<% end %>
<%= if @activity.local do %>
diff --git a/lib/pleroma/web/templates/feed/feed/_activity.rss.eex b/lib/pleroma/web/templates/feed/feed/_activity.rss.eex
index a304a16af..947bbb099 100644
--- a/lib/pleroma/web/templates/feed/feed/_activity.rss.eex
+++ b/lib/pleroma/web/templates/feed/feed/_activity.rss.eex
@@ -9,10 +9,9 @@
<ostatus:conversation ref="<%= activity_context(@activity) %>">
<%= activity_context(@activity) %>
</ostatus:conversation>
- <link rel="ostatus:conversation"><%= activity_context(@activity) %></link>
<%= if @data["summary"] do %>
- <description><%= @data["summary"] %></description>
+ <description><%= escape(@data["summary"]) %></description>
<% end %>
<%= if @activity.local do %>
@@ -21,6 +20,8 @@
<link><%= @data["external_url"] %></link>
<% end %>
+ <link rel="ostatus:conversation"><%= activity_context(@activity) %></link>
+
<%= for tag <- @data["tag"] || [] do %>
<category term="<%= tag %>"></category>
<% end %>
diff --git a/lib/pleroma/web/templates/layout/app.html.eex b/lib/pleroma/web/templates/layout/app.html.eex
index 3f28f1920..1ede59fd8 100644
--- a/lib/pleroma/web/templates/layout/app.html.eex
+++ b/lib/pleroma/web/templates/layout/app.html.eex
@@ -1,233 +1,19 @@
<!DOCTYPE html>
-<html>
+<html lang="en">
<head>
- <meta charset="utf-8" />
- <meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui" />
- <title>
- <%= Pleroma.Config.get([:instance, :name]) %>
- </title>
- <style>
- body {
- background-color: #121a24;
- font-family: sans-serif;
- color: #b9b9ba;
- text-align: center;
- }
-
- .container {
- max-width: 420px;
- padding: 20px;
- background-color: #182230;
- border-radius: 4px;
- margin: auto;
- margin-top: 10vh;
- box-shadow: 0 1px 4px 0px rgba(0, 0, 0, 0.5);
- }
-
- h1 {
- margin: 0;
- font-size: 24px;
- }
-
- h2 {
- color: #b9b9ba;
- font-weight: normal;
- font-size: 18px;
- margin-bottom: 20px;
- }
-
- a {
- color: #d8a070;
- text-decoration: none;
- }
-
- form {
- width: 100%;
- }
-
- .input {
- text-align: left;
- color: #89898a;
- display: flex;
- flex-direction: column;
- }
-
- input {
- box-sizing: content-box;
- padding: 10px;
- margin-top: 5px;
- margin-bottom: 10px;
- background-color: #121a24;
- color: #b9b9ba;
- border: 0;
- transition-property: border-bottom;
- transition-duration: 0.35s;
- border-bottom: 2px solid #2a384a;
- font-size: 14px;
- }
-
- .scopes-input {
- display: flex;
- flex-direction: column;
- margin-top: 1em;
- text-align: left;
- color: #89898a;
- }
-
- .scopes-input label:first-child {
- height: 2em;
- }
-
- .scopes {
- display: flex;
- flex-wrap: wrap;
- text-align: left;
- color: #b9b9ba;
- }
-
- .scope {
- display: flex;
- flex-basis: 100%;
- height: 2em;
- align-items: center;
- }
-
- .scope:before {
- color: #b9b9ba;
- content: "✔\fe0e";
- margin-left: 1em;
- margin-right: 1em;
- }
-
- [type="checkbox"] + label {
- display: none;
- cursor: pointer;
- margin: 0.5em;
- }
-
- [type="checkbox"] {
- display: none;
- }
-
- [type="checkbox"] + label:before {
- cursor: pointer;
- display: inline-block;
- color: white;
- background-color: #121a24;
- border: 4px solid #121a24;
- box-shadow: 0px 0px 1px 0 #d8a070;
- box-sizing: border-box;
- width: 1.2em;
- height: 1.2em;
- margin-right: 1.0em;
- content: "";
- transition-property: background-color;
- transition-duration: 0.35s;
- color: #121a24;
- margin-bottom: -0.2em;
- border-radius: 2px;
- }
-
- [type="checkbox"]:checked + label:before {
- background-color: #d8a070;
- }
-
- input:focus {
- outline: none;
- border-bottom: 2px solid #d8a070;
- }
-
- button {
- box-sizing: border-box;
- width: 100%;
- background-color: #1c2a3a;
- color: #b9b9ba;
- border-radius: 4px;
- border: none;
- padding: 10px;
- margin-top: 20px;
- margin-bottom: 20px;
- text-transform: uppercase;
- font-size: 16px;
- box-shadow: 0px 0px 2px 0px black,
- 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset,
- 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
- }
-
- button:hover {
- cursor: pointer;
- box-shadow: 0px 0px 0px 1px #d8a070,
- 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset,
- 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
- }
-
- .alert-danger {
- box-sizing: border-box;
- width: 100%;
- background-color: #931014;
- border: 1px solid #a06060;
- border-radius: 4px;
- padding: 10px;
- margin-top: 20px;
- font-weight: 500;
- font-size: 16px;
- }
-
- .alert-info {
- box-sizing: border-box;
- width: 100%;
- border-radius: 4px;
- border: 1px solid #7d796a;
- padding: 10px;
- margin-top: 20px;
- font-weight: 500;
- font-size: 16px;
- }
-
- @media all and (max-width: 440px) {
- .container {
- margin-top: 0
- }
-
- .scope {
- flex-basis: 0%;
- }
-
- .scope:before {
- content: "";
- margin-left: 0em;
- margin-right: 1em;
- }
-
- .scope:first-child:before {
- margin-left: 1em;
- content: "✔\fe0e";
- }
-
- .scope:after {
- content: ",";
- }
-
- .scope:last-child:after {
- content: "";
- }
- }
- .form-row {
- display: flex;
- }
- .form-row > label {
- text-align: left;
- line-height: 47px;
- flex: 1;
- }
- .form-row > input {
- flex: 2;
- }
- </style>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui">
+ <title><%= Pleroma.Config.get([:instance, :name]) %></title>
+ <link rel="stylesheet" href="/instance/static.css">
</head>
<body>
+ <div class="instance-header">
+ <a class="instance-header__content" href="/">
+ <img class="instance-header__thumbnail" src="<%= Pleroma.Config.get([:instance, :instance_thumbnail]) %>">
+ <h1 class="instance-header__title"><%= Pleroma.Config.get([:instance, :name]) %></h1>
+ </a>
+ </div>
<div class="container">
- <h1><%= Pleroma.Config.get([:instance, :name]) %></h1>
<%= @inner_content %>
</div>
</body>
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 b17142ff8..1a85818ec 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
@@ -5,32 +5,55 @@
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
<% end %>
-<h2>OAuth Authorization</h2>
<%= form_for @conn, o_auth_path(@conn, :authorize), [as: "authorization"], fn f -> %>
-<%= if @params["registration"] in ["true", true] do %>
- <h3>This is the first time you visit! Please enter your Pleroma handle.</h3>
- <p>Choose carefully! You won't be able to change this later. You will be able to change your display name, though.</p>
- <div class="input">
- <%= label f, :nickname, "Pleroma Handle" %>
- <%= text_input f, :nickname, placeholder: "lain" %>
+<%= if @user do %>
+ <div class="account-header">
+ <div class="account-header__banner" style="background-image: url('<%= Pleroma.User.banner_url(@user) %>')"></div>
+ <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>
</div>
- <%= hidden_input f, :name, value: @params["name"] %>
- <%= hidden_input f, :password, value: @params["password"] %>
- <br>
-<% else %>
- <div class="input">
- <%= label f, :name, "Username" %>
- <%= text_input f, :name %>
- </div>
- <div class="input">
- <%= label f, :password, "Password" %>
- <%= password_input f, :password %>
- </div>
- <%= submit "Log In" %>
- <%= render @view_module, "_scopes.html", Map.merge(assigns, %{form: f}) %>
<% end %>
+<div class="container__content">
+ <%= if @app do %>
+ <p>Application <strong><%= @app.client_name %></strong> is requesting access to your account.</p>
+ <%= render @view_module, "_scopes.html", Map.merge(assigns, %{form: f}) %>
+ <% end %>
+
+ <%= if @user do %>
+ <div class="actions">
+ <a class="button button--cancel" href="/">Cancel</a>
+ <%= submit "Approve", class: "button--approve" %>
+ </div>
+ <% else %>
+ <%= if @params["registration"] in ["true", true] do %>
+ <h3>This is the first time you visit! Please enter your Pleroma handle.</h3>
+ <p>Choose carefully! You won't be able to change this later. You will be able to change your display name, though.</p>
+ <div class="input">
+ <%= label f, :nickname, "Pleroma Handle" %>
+ <%= text_input f, :nickname, placeholder: "lain" %>
+ </div>
+ <%= hidden_input f, :name, value: @params["name"] %>
+ <%= hidden_input f, :password, value: @params["password"] %>
+ <br>
+ <% else %>
+ <div class="input">
+ <%= label f, :name, "Username" %>
+ <%= text_input f, :name %>
+ </div>
+ <div class="input">
+ <%= label f, :password, "Password" %>
+ <%= password_input f, :password %>
+ </div>
+ <%= submit "Log In" %>
+ <% end %>
+ <% end %>
+</div>
+
<%= hidden_input f, :client_id, value: @client_id %>
<%= hidden_input f, :response_type, value: @response_type %>
<%= hidden_input f, :redirect_uri, value: @redirect_uri %>
@@ -40,4 +63,3 @@
<%= if Pleroma.Config.oauth_consumer_enabled?() do %>
<%= render @view_module, Pleroma.Web.Auth.Authenticator.oauth_consumer_template(), assigns %>
<% end %>
-
diff --git a/lib/pleroma/web/translation_helpers.ex b/lib/pleroma/web/translation_helpers.ex
index 7f78ce1b9..0fe31d189 100644
--- a/lib/pleroma/web/translation_helpers.ex
+++ b/lib/pleroma/web/translation_helpers.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TranslationHelpers do
diff --git a/lib/pleroma/web/twitter_api/controller.ex b/lib/pleroma/web/twitter_api/controller.ex
index f42dba442..077bfa70d 100644
--- a/lib/pleroma/web/twitter_api/controller.ex
+++ b/lib/pleroma/web/twitter_api/controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.Controller do
@@ -30,11 +30,8 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
def confirm_email(conn, %{"user_id" => uid, "token" => token}) do
with %User{} = user <- User.get_cached_by_id(uid),
- true <- user.local and user.confirmation_pending and user.confirmation_token == token,
- {:ok, _} <-
- user
- |> User.confirmation_changeset(need_confirmation: false)
- |> User.update_and_set_cache() do
+ true <- user.local and !user.is_confirmed and user.confirmation_token == token,
+ {:ok, _} <- User.confirm(user) do
redirect(conn, to: "/")
end
end
diff --git a/lib/pleroma/web/twitter_api/controllers/password_controller.ex b/lib/pleroma/web/twitter_api/controllers/password_controller.ex
index 800ab8954..bc04a4d49 100644
--- a/lib/pleroma/web/twitter_api/controllers/password_controller.ex
+++ b/lib/pleroma/web/twitter_api/controllers/password_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.PasswordController do
@@ -17,6 +17,7 @@ defmodule Pleroma.Web.TwitterAPI.PasswordController do
def reset(conn, %{"token" => token}) do
with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}),
+ false <- PasswordResetToken.expired?(token),
%User{} = user <- User.get_cached_by_id(token.user_id) do
render(conn, "reset.html", %{
token: token,
diff --git a/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex b/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex
index 4480a4922..6ca02fbd7 100644
--- a/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex
+++ b/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do
diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
index 9ead0d626..940a645bb 100644
--- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex
+++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.UtilController do
@@ -150,7 +150,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
def disable_account(%{assigns: %{user: user}} = conn, params) do
case CommonAPI.Utils.confirm_current_password(user, params["password"]) do
{:ok, user} ->
- User.deactivate_async(user)
+ User.set_activation_async(user, false)
json(conn, %{status: "success"})
{:error, msg} ->
diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex
index 5d7948507..76ca82d20 100644
--- a/lib/pleroma/web/twitter_api/twitter_api.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
@@ -45,7 +45,6 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
case User.register(changeset) do
{:ok, user} ->
- maybe_notify_admins(user)
{:ok, user}
{:error, changeset} ->
@@ -58,21 +57,9 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
end
end
- defp maybe_notify_admins(%User{} = account) do
- if Pleroma.Config.get([:instance, :account_approval_required]) do
- User.all_superusers()
- |> Enum.filter(fn user -> not is_nil(user.email) end)
- |> Enum.each(fn superuser ->
- superuser
- |> Pleroma.Emails.AdminEmail.new_unapproved_registration(account)
- |> Pleroma.Emails.Mailer.deliver_async()
- end)
- end
- end
-
def password_reset(nickname_or_email) do
with true <- is_binary(nickname_or_email),
- %User{local: true, email: email, deactivated: false} = user when is_binary(email) <-
+ %User{local: true, email: email, is_active: true} = user when is_binary(email) <-
User.get_by_nickname_or_email(nickname_or_email),
{:ok, token_record} <- Pleroma.PasswordResetToken.create_token(user) do
user
diff --git a/lib/pleroma/web/twitter_api/views/password_view.ex b/lib/pleroma/web/twitter_api/views/password_view.ex
index 41462e4af..a9bb95a2c 100644
--- a/lib/pleroma/web/twitter_api/views/password_view.ex
+++ b/lib/pleroma/web/twitter_api/views/password_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.PasswordView do
diff --git a/lib/pleroma/web/twitter_api/views/remote_follow_view.ex b/lib/pleroma/web/twitter_api/views/remote_follow_view.ex
index c05c7821c..ac3f15eec 100644
--- a/lib/pleroma/web/twitter_api/views/remote_follow_view.ex
+++ b/lib/pleroma/web/twitter_api/views/remote_follow_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.RemoteFollowView do
diff --git a/lib/pleroma/web/twitter_api/views/token_view.ex b/lib/pleroma/web/twitter_api/views/token_view.ex
index c36303625..99884e714 100644
--- a/lib/pleroma/web/twitter_api/views/token_view.ex
+++ b/lib/pleroma/web/twitter_api/views/token_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.TokenView do
diff --git a/lib/pleroma/web/twitter_api/views/util_view.ex b/lib/pleroma/web/twitter_api/views/util_view.ex
index 98eea1d18..9b13c09b3 100644
--- a/lib/pleroma/web/twitter_api/views/util_view.ex
+++ b/lib/pleroma/web/twitter_api/views/util_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.UtilView do
diff --git a/lib/pleroma/web/uploader_controller.ex b/lib/pleroma/web/uploader_controller.ex
index 6533f1c0e..0d42c7ec3 100644
--- a/lib/pleroma/web/uploader_controller.ex
+++ b/lib/pleroma/web/uploader_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.UploaderController do
diff --git a/lib/pleroma/web/views/email_view.ex b/lib/pleroma/web/views/email_view.ex
index bcdee6571..f7659b994 100644
--- a/lib/pleroma/web/views/email_view.ex
+++ b/lib/pleroma/web/views/email_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.EmailView do
diff --git a/lib/pleroma/web/views/embed_view.ex b/lib/pleroma/web/views/embed_view.ex
index 5f50bd155..81e196730 100644
--- a/lib/pleroma/web/views/embed_view.ex
+++ b/lib/pleroma/web/views/embed_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.EmbedView do
@@ -17,6 +17,8 @@ defmodule Pleroma.Web.EmbedView do
use Phoenix.HTML
+ defdelegate full_nickname(user), to: User
+
@media_types ["image", "audio", "video"]
defp fetch_media_type(%{"mediaType" => mediaType}) do
@@ -30,11 +32,6 @@ defmodule Pleroma.Web.EmbedView do
)
end
- defp full_nickname(user) do
- %{host: host} = URI.parse(user.ap_id)
- "@" <> user.nickname <> "@" <> host
- end
-
defp status_title(%Activity{object: %Object{data: %{"name" => name}}}) when is_binary(name),
do: name
diff --git a/lib/pleroma/web/views/error_helpers.ex b/lib/pleroma/web/views/error_helpers.ex
index df657a343..d282c04b7 100644
--- a/lib/pleroma/web/views/error_helpers.ex
+++ b/lib/pleroma/web/views/error_helpers.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ErrorHelpers do
diff --git a/lib/pleroma/web/views/error_view.ex b/lib/pleroma/web/views/error_view.ex
index e68d55e08..c9715dc4b 100644
--- a/lib/pleroma/web/views/error_view.ex
+++ b/lib/pleroma/web/views/error_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ErrorView do
diff --git a/lib/pleroma/web/views/layout_view.ex b/lib/pleroma/web/views/layout_view.ex
index 3e49c6549..c2da10f04 100644
--- a/lib/pleroma/web/views/layout_view.ex
+++ b/lib/pleroma/web/views/layout_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.LayoutView do
diff --git a/lib/pleroma/web/views/mailer/subscription_view.ex b/lib/pleroma/web/views/mailer/subscription_view.ex
index 4562a9d6c..1dc80987b 100644
--- a/lib/pleroma/web/views/mailer/subscription_view.ex
+++ b/lib/pleroma/web/views/mailer/subscription_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Mailer.SubscriptionView do
diff --git a/lib/pleroma/web/views/masto_fe_view.ex b/lib/pleroma/web/views/masto_fe_view.ex
index b1669d198..b9055cb7f 100644
--- a/lib/pleroma/web/views/masto_fe_view.ex
+++ b/lib/pleroma/web/views/masto_fe_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastoFEView do
diff --git a/lib/pleroma/web/views/streamer_view.ex b/lib/pleroma/web/views/streamer_view.ex
index 476a33245..7706035e9 100644
--- a/lib/pleroma/web/views/streamer_view.ex
+++ b/lib/pleroma/web/views/streamer_view.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.StreamerView do
@@ -74,6 +74,28 @@ defmodule Pleroma.Web.StreamerView do
|> Jason.encode!()
end
+ def render("follow_relationships_update.json", item) do
+ %{
+ event: "pleroma:follow_relationships_update",
+ payload:
+ %{
+ state: item.state,
+ follower: %{
+ id: item.follower.id,
+ follower_count: item.follower.follower_count,
+ following_count: item.follower.following_count
+ },
+ following: %{
+ id: item.following.id,
+ follower_count: item.following.follower_count,
+ following_count: item.following.following_count
+ }
+ }
+ |> Jason.encode!()
+ }
+ |> Jason.encode!()
+ end
+
def render("conversation.json", %Participation{} = participation) do
%{
event: "conversation",
diff --git a/lib/pleroma/web/web_finger.ex b/lib/pleroma/web/web_finger.ex
index 6629f5356..15002b29f 100644
--- a/lib/pleroma/web/web_finger.ex
+++ b/lib/pleroma/web/web_finger.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.WebFinger do
@@ -58,12 +58,16 @@ defmodule Pleroma.Web.WebFinger do
] ++ Publisher.gather_webfinger_links(user)
end
+ defp gather_aliases(%User{} = user) do
+ [user.ap_id | user.also_known_as]
+ end
+
def represent_user(user, "JSON") do
{:ok, user} = User.ensure_keys_present(user)
%{
"subject" => "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}",
- "aliases" => [user.ap_id],
+ "aliases" => gather_aliases(user),
"links" => gather_links(user)
}
end
@@ -71,6 +75,11 @@ defmodule Pleroma.Web.WebFinger do
def represent_user(user, "XML") do
{:ok, user} = User.ensure_keys_present(user)
+ aliases =
+ user
+ |> gather_aliases()
+ |> Enum.map(&{:Alias, &1})
+
links =
gather_links(user)
|> Enum.map(fn link -> {:Link, link} end)
@@ -79,9 +88,8 @@ defmodule Pleroma.Web.WebFinger do
:XRD,
%{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"},
[
- {:Subject, "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}"},
- {:Alias, user.ap_id}
- ] ++ links
+ {:Subject, "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}"}
+ ] ++ aliases ++ links
}
|> XmlBuilder.to_doc()
end
@@ -116,6 +124,9 @@ defmodule Pleroma.Web.WebFinger do
{"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", "self"} ->
Map.put(data, "ap_id", link["href"])
+ {nil, "http://ostatus.org/schema/1.0/subscribe"} ->
+ Map.put(data, "subscribe_address", link["template"])
+
_ ->
Logger.debug("Unhandled type: #{inspect(link["type"])}")
data
diff --git a/lib/pleroma/web/web_finger/web_finger_controller.ex b/lib/pleroma/web/web_finger/web_finger_controller.ex
index 9f0938fc0..7944c50ad 100644
--- a/lib/pleroma/web/web_finger/web_finger_controller.ex
+++ b/lib/pleroma/web/web_finger/web_finger_controller.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.WebFinger.WebFingerController do
diff --git a/lib/pleroma/web/xml.ex b/lib/pleroma/web/xml.ex
index c69a86a1e..2b34611ac 100644
--- a/lib/pleroma/web/xml.ex
+++ b/lib/pleroma/web/xml.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.XML do
diff --git a/lib/pleroma/workers/attachments_cleanup_worker.ex b/lib/pleroma/workers/attachments_cleanup_worker.ex
index 58226b395..f5090dae7 100644
--- a/lib/pleroma/workers/attachments_cleanup_worker.ex
+++ b/lib/pleroma/workers/attachments_cleanup_worker.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.AttachmentsCleanupWorker do
@@ -17,12 +17,14 @@ defmodule Pleroma.Workers.AttachmentsCleanupWorker do
"object" => %{"data" => %{"attachment" => [_ | _] = attachments, "actor" => actor}}
}
}) do
- attachments
- |> Enum.flat_map(fn item -> Enum.map(item["url"], & &1["href"]) end)
- |> fetch_objects
- |> prepare_objects(actor, Enum.map(attachments, & &1["name"]))
- |> filter_objects
- |> do_clean
+ if Pleroma.Config.get([:instance, :cleanup_attachments], false) do
+ attachments
+ |> Enum.flat_map(fn item -> Enum.map(item["url"], & &1["href"]) end)
+ |> fetch_objects
+ |> prepare_objects(actor, Enum.map(attachments, & &1["name"]))
+ |> filter_objects
+ |> do_clean
+ end
{:ok, :success}
end
@@ -32,21 +34,15 @@ defmodule Pleroma.Workers.AttachmentsCleanupWorker do
defp do_clean({object_ids, attachment_urls}) do
uploader = Pleroma.Config.get([Pleroma.Upload, :uploader])
- prefix =
- case Pleroma.Config.get([Pleroma.Upload, :base_url]) do
- nil -> "media"
- _ -> ""
- end
-
base_url =
String.trim_trailing(
- Pleroma.Config.get([Pleroma.Upload, :base_url], Pleroma.Web.base_url()),
+ Pleroma.Upload.base_url(),
"/"
)
Enum.each(attachment_urls, fn href ->
href
- |> String.trim_leading("#{base_url}/#{prefix}")
+ |> String.trim_leading("#{base_url}")
|> uploader.delete_file()
end)
diff --git a/lib/pleroma/workers/background_worker.ex b/lib/pleroma/workers/background_worker.ex
index 55b5a13d9..1e28384cb 100644
--- a/lib/pleroma/workers/background_worker.ex
+++ b/lib/pleroma/workers/background_worker.ex
@@ -1,19 +1,17 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.BackgroundWorker do
- alias Pleroma.Activity
alias Pleroma.User
- alias Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy
use Pleroma.Workers.WorkerHelper, queue: "background"
@impl Oban.Worker
- def perform(%Job{args: %{"op" => "deactivate_user", "user_id" => user_id, "status" => status}}) do
+ def perform(%Job{args: %{"op" => "user_activation", "user_id" => user_id, "status" => status}}) do
user = User.get_cached_by_id(user_id)
- User.perform(:deactivate_async, user, status)
+ User.perform(:set_activation_async, user, status)
end
def perform(%Job{args: %{"op" => "delete_user", "user_id" => user_id}}) do
@@ -32,19 +30,6 @@ defmodule Pleroma.Workers.BackgroundWorker do
{:ok, User.Import.perform(String.to_atom(op), user, identifiers)}
end
- def perform(%Job{args: %{"op" => "media_proxy_preload", "message" => message}}) do
- MediaProxyWarmingPolicy.perform(:preload, message)
- end
-
- def perform(%Job{args: %{"op" => "media_proxy_prefetch", "url" => url}}) do
- MediaProxyWarmingPolicy.perform(:prefetch, url)
- end
-
- def perform(%Job{args: %{"op" => "fetch_data_for_activity", "activity_id" => activity_id}}) do
- activity = Activity.get_by_id(activity_id)
- Pleroma.Web.RichMedia.Helpers.perform(:fetch, activity)
- end
-
def perform(%Job{
args: %{"op" => "move_following", "origin_id" => origin_id, "target_id" => target_id}
}) do
diff --git a/lib/pleroma/workers/backup_worker.ex b/lib/pleroma/workers/backup_worker.ex
new file mode 100644
index 000000000..9b763b04b
--- /dev/null
+++ b/lib/pleroma/workers/backup_worker.ex
@@ -0,0 +1,54 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Workers.BackupWorker do
+ use Oban.Worker, queue: :backup, max_attempts: 1
+
+ alias Oban.Job
+ alias Pleroma.User.Backup
+
+ def process(backup, admin_user_id \\ nil) do
+ %{"op" => "process", "backup_id" => backup.id, "admin_user_id" => admin_user_id}
+ |> new()
+ |> Oban.insert()
+ end
+
+ def schedule_deletion(backup) do
+ days = Pleroma.Config.get([Backup, :purge_after_days])
+ time = 60 * 60 * 24 * days
+ scheduled_at = Calendar.NaiveDateTime.add!(backup.inserted_at, time)
+
+ %{"op" => "delete", "backup_id" => backup.id}
+ |> new(scheduled_at: scheduled_at)
+ |> Oban.insert()
+ end
+
+ def delete(backup) do
+ %{"op" => "delete", "backup_id" => backup.id}
+ |> new()
+ |> Oban.insert()
+ end
+
+ def perform(%Job{
+ args: %{"op" => "process", "backup_id" => backup_id, "admin_user_id" => admin_user_id}
+ }) do
+ with {:ok, %Backup{} = backup} <-
+ backup_id |> Backup.get() |> Backup.process(),
+ {:ok, _job} <- schedule_deletion(backup),
+ :ok <- Backup.remove_outdated(backup),
+ {:ok, _} <-
+ backup
+ |> Pleroma.Emails.UserEmail.backup_is_ready_email(admin_user_id)
+ |> Pleroma.Emails.Mailer.deliver() do
+ {:ok, backup}
+ end
+ end
+
+ def perform(%Job{args: %{"op" => "delete", "backup_id" => backup_id}}) do
+ case Backup.get(backup_id) do
+ %Backup{} = backup -> Backup.delete(backup)
+ nil -> :ok
+ end
+ end
+end
diff --git a/lib/pleroma/workers/cron/digest_emails_worker.ex b/lib/pleroma/workers/cron/digest_emails_worker.ex
index 0c56f00fb..83dc75d60 100644
--- a/lib/pleroma/workers/cron/digest_emails_worker.ex
+++ b/lib/pleroma/workers/cron/digest_emails_worker.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.Cron.DigestEmailsWorker do
diff --git a/lib/pleroma/workers/cron/new_users_digest_worker.ex b/lib/pleroma/workers/cron/new_users_digest_worker.ex
index 8bbaed83d..9dfd92228 100644
--- a/lib/pleroma/workers/cron/new_users_digest_worker.ex
+++ b/lib/pleroma/workers/cron/new_users_digest_worker.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.Cron.NewUsersDigestWorker do
diff --git a/lib/pleroma/workers/mailer_worker.ex b/lib/pleroma/workers/mailer_worker.ex
index 32273cfa5..592230e7a 100644
--- a/lib/pleroma/workers/mailer_worker.ex
+++ b/lib/pleroma/workers/mailer_worker.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.MailerWorker do
diff --git a/lib/pleroma/workers/mute_expire_worker.ex b/lib/pleroma/workers/mute_expire_worker.ex
new file mode 100644
index 000000000..8da903e76
--- /dev/null
+++ b/lib/pleroma/workers/mute_expire_worker.ex
@@ -0,0 +1,20 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Workers.MuteExpireWorker do
+ use Pleroma.Workers.WorkerHelper, queue: "mute_expire"
+
+ @impl Oban.Worker
+ def perform(%Job{args: %{"op" => "unmute_user", "muter_id" => muter_id, "mutee_id" => mutee_id}}) do
+ Pleroma.User.unmute(muter_id, mutee_id)
+ :ok
+ end
+
+ def perform(%Job{
+ args: %{"op" => "unmute_conversation", "user_id" => user_id, "activity_id" => activity_id}
+ }) do
+ Pleroma.Web.CommonAPI.remove_mute(user_id, activity_id)
+ :ok
+ end
+end
diff --git a/lib/pleroma/workers/publisher_worker.ex b/lib/pleroma/workers/publisher_worker.ex
index e739c3cd0..6209715b3 100644
--- a/lib/pleroma/workers/publisher_worker.ex
+++ b/lib/pleroma/workers/publisher_worker.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.PublisherWorker do
diff --git a/lib/pleroma/workers/purge_expired_activity.ex b/lib/pleroma/workers/purge_expired_activity.ex
index c168890a2..027171c1e 100644
--- a/lib/pleroma/workers/purge_expired_activity.ex
+++ b/lib/pleroma/workers/purge_expired_activity.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.PurgeExpiredActivity do
@@ -7,7 +7,7 @@ defmodule Pleroma.Workers.PurgeExpiredActivity do
Worker which purges expired activity.
"""
- use Oban.Worker, queue: :activity_expiration, max_attempts: 1
+ use Oban.Worker, queue: :activity_expiration, max_attempts: 1, unique: [period: :infinity]
import Ecto.Query
diff --git a/lib/pleroma/workers/purge_expired_filter.ex b/lib/pleroma/workers/purge_expired_filter.ex
new file mode 100644
index 000000000..4740d52e9
--- /dev/null
+++ b/lib/pleroma/workers/purge_expired_filter.ex
@@ -0,0 +1,43 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Workers.PurgeExpiredFilter do
+ @moduledoc """
+ Worker which purges expired filters
+ """
+
+ use Oban.Worker, queue: :filter_expiration, max_attempts: 1, unique: [period: :infinity]
+
+ import Ecto.Query
+
+ alias Oban.Job
+ alias Pleroma.Repo
+
+ @spec enqueue(%{filter_id: integer(), expires_at: DateTime.t()}) ::
+ {:ok, Job.t()} | {:error, Ecto.Changeset.t()}
+ def enqueue(args) do
+ {scheduled_at, args} = Map.pop(args, :expires_at)
+
+ args
+ |> new(scheduled_at: scheduled_at)
+ |> Oban.insert()
+ end
+
+ @impl true
+ def perform(%Job{args: %{"filter_id" => id}}) do
+ Pleroma.Filter
+ |> Repo.get(id)
+ |> Repo.delete()
+ end
+
+ @spec get_expiration(pos_integer()) :: Job.t() | nil
+ def get_expiration(id) do
+ from(j in Job,
+ where: j.state == "scheduled",
+ where: j.queue == "filter_expiration",
+ where: fragment("?->'filter_id' = ?", j.args, ^id)
+ )
+ |> Repo.one()
+ end
+end
diff --git a/lib/pleroma/workers/purge_expired_token.ex b/lib/pleroma/workers/purge_expired_token.ex
index a81e0cd28..cfdf5c6dc 100644
--- a/lib/pleroma/workers/purge_expired_token.ex
+++ b/lib/pleroma/workers/purge_expired_token.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.PurgeExpiredToken do
diff --git a/lib/pleroma/workers/receiver_worker.ex b/lib/pleroma/workers/receiver_worker.ex
index 1b97af1a8..69125dcd0 100644
--- a/lib/pleroma/workers/receiver_worker.ex
+++ b/lib/pleroma/workers/receiver_worker.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.ReceiverWorker do
diff --git a/lib/pleroma/workers/remote_fetcher_worker.ex b/lib/pleroma/workers/remote_fetcher_worker.ex
index 27e2e3386..ad4d785a1 100644
--- a/lib/pleroma/workers/remote_fetcher_worker.ex
+++ b/lib/pleroma/workers/remote_fetcher_worker.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.RemoteFetcherWorker do
diff --git a/lib/pleroma/workers/scheduled_activity_worker.ex b/lib/pleroma/workers/scheduled_activity_worker.ex
index dd9986fe4..a4ab9928d 100644
--- a/lib/pleroma/workers/scheduled_activity_worker.ex
+++ b/lib/pleroma/workers/scheduled_activity_worker.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.ScheduledActivityWorker do
@@ -9,38 +9,50 @@ defmodule Pleroma.Workers.ScheduledActivityWorker do
use Pleroma.Workers.WorkerHelper, queue: "scheduled_activities"
- alias Pleroma.Config
+ alias Pleroma.Repo
alias Pleroma.ScheduledActivity
alias Pleroma.User
- alias Pleroma.Web.CommonAPI
require Logger
@impl Oban.Worker
def perform(%Job{args: %{"activity_id" => activity_id}}) do
- if Config.get([ScheduledActivity, :enabled]) do
- case Pleroma.Repo.get(ScheduledActivity, activity_id) do
- %ScheduledActivity{} = scheduled_activity ->
- post_activity(scheduled_activity)
-
- _ ->
- Logger.error("#{__MODULE__} Couldn't find scheduled activity: #{activity_id}")
- end
+ with %ScheduledActivity{} = scheduled_activity <- find_scheduled_activity(activity_id),
+ %User{} = user <- find_user(scheduled_activity.user_id) do
+ params = atomize_keys(scheduled_activity.params)
+
+ Repo.transaction(fn ->
+ {:ok, activity} = Pleroma.Web.CommonAPI.post(user, params)
+ {:ok, _} = ScheduledActivity.delete(scheduled_activity)
+ activity
+ end)
+ else
+ {:error, :scheduled_activity_not_found} = error ->
+ Logger.error("#{__MODULE__} Couldn't find scheduled activity: #{activity_id}")
+ error
+
+ {:error, :user_not_found} = error ->
+ Logger.error("#{__MODULE__} Couldn't find user for scheduled activity: #{activity_id}")
+ error
end
end
- defp post_activity(%ScheduledActivity{user_id: user_id, params: params} = scheduled_activity) do
- params = Map.new(params, fn {key, value} -> {String.to_existing_atom(key), value} end)
+ defp find_scheduled_activity(id) do
+ with nil <- Repo.get(ScheduledActivity, id) do
+ {:error, :scheduled_activity_not_found}
+ end
+ end
- with {:delete, {:ok, _}} <- {:delete, ScheduledActivity.delete(scheduled_activity)},
- {:user, %User{} = user} <- {:user, User.get_cached_by_id(user_id)},
- {:post, {:ok, _}} <- {:post, CommonAPI.post(user, params)} do
- :ok
- else
- error ->
- Logger.error(
- "#{__MODULE__} Couldn't create a status from the scheduled activity: #{inspect(error)}"
- )
+ defp find_user(id) do
+ with nil <- User.get_cached_by_id(id) do
+ {:error, :user_not_found}
end
end
+
+ defp atomize_keys(map) do
+ Map.new(map, fn
+ {key, value} when is_map(value) -> {String.to_existing_atom(key), atomize_keys(value)}
+ {key, value} -> {String.to_existing_atom(key), value}
+ end)
+ end
end
diff --git a/lib/pleroma/workers/transmogrifier_worker.ex b/lib/pleroma/workers/transmogrifier_worker.ex
index 15f36375c..b39c1ea62 100644
--- a/lib/pleroma/workers/transmogrifier_worker.ex
+++ b/lib/pleroma/workers/transmogrifier_worker.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.TransmogrifierWorker do
diff --git a/lib/pleroma/workers/web_pusher_worker.ex b/lib/pleroma/workers/web_pusher_worker.ex
index 0cfdc6a6f..8fc2aff26 100644
--- a/lib/pleroma/workers/web_pusher_worker.ex
+++ b/lib/pleroma/workers/web_pusher_worker.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.WebPusherWorker do
diff --git a/lib/pleroma/workers/worker_helper.ex b/lib/pleroma/workers/worker_helper.ex
index 7d1289be2..4befbeb3b 100644
--- a/lib/pleroma/workers/worker_helper.ex
+++ b/lib/pleroma/workers/worker_helper.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.WorkerHelper do
diff --git a/lib/pleroma/xml_builder.ex b/lib/pleroma/xml_builder.ex
index 33b63a71f..922d3f6ee 100644
--- a/lib/pleroma/xml_builder.ex
+++ b/lib/pleroma/xml_builder.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.XmlBuilder do