From 9d4f18b984d6699bdf96e5f5963edfe80063426c Mon Sep 17 00:00:00 2001 From: Fire Demon Date: Sat, 27 Jun 2020 14:22:30 -0500 Subject: Monsterfork v2 Kaiju Commit 2020.06.27.1 - 2020.09.05.5 --- .rubocop.yml | 42 ++ Gemfile | 6 + Gemfile.lock | 21 + app/controllers/about_controller.rb | 17 +- app/controllers/accounts_controller.rb | 84 ++-- app/controllers/activitypub/claims_controller.rb | 2 +- app/controllers/activitypub/inboxes_controller.rb | 2 +- app/controllers/activitypub/outboxes_controller.rb | 39 +- app/controllers/activitypub/replies_controller.rb | 3 +- app/controllers/admin/domain_allows_controller.rb | 2 +- .../admin/pending_accounts_controller.rb | 6 +- app/controllers/admin/tags_controller.rb | 2 +- app/controllers/api/base_controller.rb | 2 +- .../api/v1/accounts/credentials_controller.rb | 4 +- .../api/v1/accounts/statuses_controller.rb | 77 ++-- app/controllers/api/v1/accounts_controller.rb | 2 +- .../api/v1/admin/domain_allows_controller.rb | 54 +++ .../api/v1/admin/domain_blocks_controller.rb | 54 +++ .../api/v1/domain_permissions_controller.rb | 81 ++++ .../api/v1/instances/activity_controller.rb | 4 +- .../api/v1/instances/peers_controller.rb | 4 +- app/controllers/api/v1/instances_controller.rb | 2 +- app/controllers/api/v1/polls/votes_controller.rb | 1 + app/controllers/api/v1/polls_controller.rb | 1 + .../api/v1/statuses/hides_controller.rb | 28 ++ .../api/v1/statuses/mutes_controller.rb | 4 +- app/controllers/api/v1/statuses/pins_controller.rb | 2 +- .../api/v1/statuses/publishing_controller.rb | 26 ++ app/controllers/api/v1/statuses_controller.rb | 100 ++++- .../api/v1/timelines/public_controller.rb | 3 +- app/controllers/application_controller.rb | 46 ++- app/controllers/auth/registrations_controller.rb | 8 +- app/controllers/concerns/account_owned_concern.rb | 2 +- app/controllers/home_controller.rb | 2 +- app/controllers/media_controller.rb | 5 +- app/controllers/media_proxy_controller.rb | 3 +- app/controllers/remote_interaction_controller.rb | 4 +- .../settings/preferences/filters_controller.rb | 9 + .../settings/preferences/publishing_controller.rb | 9 + app/controllers/settings/preferences_controller.rb | 16 + app/controllers/settings/profiles_controller.rb | 4 +- app/controllers/statuses_controller.rb | 14 +- app/controllers/tags_controller.rb | 10 +- app/controllers/user_profile_css_controller.rb | 24 ++ app/controllers/user_webapp_css_controller.rb | 73 ++++ app/helpers/domain_control_helper.rb | 2 +- app/helpers/img_proxy_helper.rb | 128 ++++++ app/helpers/jsonld_helper.rb | 27 +- app/helpers/settings_helper.rb | 1 + app/javascript/flavours/glitch/actions/accounts.js | 4 +- app/javascript/flavours/glitch/actions/compose.js | 5 +- .../flavours/glitch/actions/importer/normalizer.js | 9 +- app/javascript/flavours/glitch/actions/mutes.js | 7 + app/javascript/flavours/glitch/actions/statuses.js | 74 +++- .../flavours/glitch/actions/streaming.js | 5 + .../flavours/glitch/actions/timelines.js | 13 +- .../flavours/glitch/components/media_gallery.js | 61 +++ .../flavours/glitch/components/status.js | 8 +- .../glitch/components/status_action_bar.js | 25 +- .../flavours/glitch/components/status_content.js | 246 +++++++++++- .../flavours/glitch/containers/status_container.js | 16 +- .../features/account/components/action_bar.js | 25 +- .../features/account_timeline/components/header.js | 8 +- .../glitch/features/account_timeline/index.js | 22 +- .../features/compose/components/compose_form.js | 16 + .../features/compose/components/publisher.js | 18 +- .../compose/containers/compose_form_container.js | 5 + .../features/status/components/action_bar.js | 17 + .../features/status/components/detailed_status.js | 53 +-- .../status/containers/detailed_status_container.js | 16 + .../flavours/glitch/features/status/index.js | 20 +- .../glitch/features/ui/components/link_footer.js | 1 + .../glitch/features/ui/components/mute_modal.js | 27 +- .../glitch/features/ui/components/report_modal.js | 6 +- .../flavours/glitch/features/ui/index.js | 6 +- app/javascript/flavours/glitch/locales/en-MP.js | 4 + app/javascript/flavours/glitch/reducers/compose.js | 14 +- .../flavours/glitch/reducers/local_settings.js | 16 +- app/javascript/flavours/glitch/reducers/mutes.js | 5 + .../flavours/glitch/reducers/statuses.js | 3 + app/javascript/flavours/glitch/selectors/index.js | 5 + .../flavours/glitch/styles/components/modal.scss | 2 +- .../flavours/glitch/styles/containers.scss | 15 +- app/javascript/flavours/glitch/styles/index.scss | 3 + .../flavours/glitch/styles/monsterfork/about.scss | 9 + .../styles/monsterfork/components/composer.scss | 11 + .../styles/monsterfork/components/formatting.scss | 175 ++++++++ .../styles/monsterfork/components/index.scss | 3 + .../styles/monsterfork/components/status.scss | 243 ++++++++++++ .../flavours/glitch/styles/monsterfork/index.scss | 2 + .../flavours/glitch/styles/nightshade.scss | 3 + .../flavours/glitch/styles/nightshade/diff.scss | 440 +++++++++++++++++++++ .../glitch/styles/nightshade/variables.scss | 41 ++ .../flavours/glitch/styles/variables.scss | 8 +- app/javascript/flavours/glitch/styles/widgets.scss | 1 - app/javascript/fonts/opensans/LICENSE.txt | 202 ++++++++++ app/javascript/fonts/opensans/OpenSans-Bold.ttf | Bin 0 -> 104120 bytes app/javascript/fonts/opensans/OpenSans-Bold.woff2 | Bin 0 -> 46296 bytes .../fonts/opensans/OpenSans-BoldItalic.ttf | Bin 0 -> 92628 bytes .../fonts/opensans/OpenSans-BoldItalic.woff2 | Bin 0 -> 42116 bytes .../fonts/opensans/OpenSans-ExtraBold.ttf | Bin 0 -> 102076 bytes .../fonts/opensans/OpenSans-ExtraBold.woff2 | Bin 0 -> 46028 bytes .../fonts/opensans/OpenSans-ExtraBoldItalic.ttf | Bin 0 -> 92772 bytes .../fonts/opensans/OpenSans-ExtraBoldItalic.woff2 | Bin 0 -> 42180 bytes app/javascript/fonts/opensans/OpenSans-Italic.ttf | Bin 0 -> 92240 bytes .../fonts/opensans/OpenSans-Italic.woff2 | Bin 0 -> 42456 bytes app/javascript/fonts/opensans/OpenSans-Light.ttf | Bin 0 -> 101696 bytes app/javascript/fonts/opensans/OpenSans-Light.woff2 | Bin 0 -> 45632 bytes .../fonts/opensans/OpenSans-LightItalic.ttf | Bin 0 -> 92488 bytes .../fonts/opensans/OpenSans-LightItalic.woff2 | Bin 0 -> 41908 bytes app/javascript/fonts/opensans/OpenSans-Regular.ttf | Bin 0 -> 96932 bytes .../fonts/opensans/OpenSans-Regular.woff2 | Bin 0 -> 44504 bytes .../fonts/opensans/OpenSans-SemiBold.ttf | Bin 0 -> 100820 bytes .../fonts/opensans/OpenSans-SemiBold.woff2 | Bin 0 -> 46376 bytes .../fonts/opensans/OpenSans-SemiBoldItalic.ttf | Bin 0 -> 92180 bytes .../fonts/opensans/OpenSans-SemiBoldItalic.woff2 | Bin 0 -> 43340 bytes app/javascript/locales/locale-data/en-MP.js | 8 + .../mastodon/actions/importer/normalizer.js | 5 +- app/javascript/mastodon/actions/streaming.js | 5 + .../mastodon/components/status_action_bar.js | 6 +- .../mastodon/components/status_content.js | 20 + .../features/compose/components/poll_form.js | 4 +- app/javascript/mastodon/locales/en-MP.json | 176 +++++++++ .../mastodon/locales/locale-data/en-MP.js | 8 + .../mastodon/locales/whitelist_en-MP.json | 2 + app/javascript/mastodon/reducers/compose.js | 4 +- app/javascript/skins/glitch/nightshade/common.scss | 1 + app/javascript/skins/glitch/nightshade/names.yml | 5 + app/javascript/styles/fonts/montserrat.scss | 4 +- app/javascript/styles/fonts/opensans.scss | 134 +++++++ app/javascript/styles/fonts/roboto-mono.scss | 2 +- app/javascript/styles/fonts/roboto.scss | 8 +- app/javascript/styles/mailer.scss | 2 + app/javascript/styles/mastodon/variables.scss | 6 +- app/lib/activitypub/activity.rb | 10 +- app/lib/activitypub/activity/add.rb | 2 +- app/lib/activitypub/activity/announce.rb | 16 +- app/lib/activitypub/activity/create.rb | 168 +++++++- app/lib/activitypub/activity/delete.rb | 7 +- app/lib/activitypub/activity/update.rb | 4 + app/lib/activitypub/adapter.rb | 14 +- app/lib/activitypub/case_transform.rb | 4 +- app/lib/activitypub/tag_manager.rb | 30 +- app/lib/command_tag/command/account_tools.rb | 37 ++ app/lib/command_tag/command/footer_tools.rb | 50 +++ app/lib/command_tag/command/hello_world.rb | 11 + app/lib/command_tag/command/parent_status_tools.rb | 80 ++++ app/lib/command_tag/command/status_tools.rb | 239 +++++++++++ app/lib/command_tag/command/text_tools.rb | 58 +++ app/lib/command_tag/command/variables.rb | 40 ++ app/lib/command_tag/commands.rb | 11 + app/lib/command_tag/processor.rb | 335 ++++++++++++++++ app/lib/feed_manager.rb | 127 +++++- app/lib/formatter.rb | 113 ++++-- app/lib/img_tag_handler.rb | 30 ++ app/lib/rss/serializer.rb | 1 + app/lib/rss_builder.rb | 6 + app/lib/sanitize_config.rb | 29 +- app/lib/status_filter.rb | 10 +- app/lib/user_settings_decorator.rb | 94 ++++- app/models/account.rb | 45 +++ app/models/account_domain_permission.rb | 70 ++++ app/models/account_metadata.rb | 52 +++ app/models/collection_item.rb | 21 + app/models/collection_page.rb | 17 + app/models/concerns/account_associations.rb | 18 + app/models/concerns/account_interactions.rb | 39 +- app/models/concerns/status_threading_concern.rb | 2 +- app/models/conversation.rb | 5 + app/models/conversation_mute.rb | 5 +- app/models/domain_allow.rb | 2 + app/models/domain_block.rb | 1 + app/models/follow_request.rb | 5 +- app/models/form/admin_settings.rb | 4 + app/models/inline_media_attachment.rb | 20 + app/models/invite.rb | 2 +- app/models/media_attachment.rb | 32 +- app/models/mute.rb | 1 + app/models/queued_boost.rb | 15 + app/models/status.rb | 308 +++++++++++++-- app/models/status_domain_permission.rb | 69 ++++ app/models/status_mute.rb | 20 + app/models/user.rb | 36 +- app/policies/account_domain_permission_policy.rb | 17 + app/policies/status_policy.rb | 66 +++- app/presenters/activitypub/activity_presenter.rb | 14 +- app/presenters/status_relationships_presenter.rb | 8 + app/serializers/activitypub/actor_serializer.rb | 12 + app/serializers/activitypub/note_serializer.rb | 63 ++- app/serializers/activitypub/outbox_serializer.rb | 2 +- .../activitypub/undo_announce_serializer.rb | 2 +- app/serializers/nodeinfo/serializer.rb | 19 +- .../rest/account_domain_permission_serializer.rb | 9 + app/serializers/rest/account_serializer.rb | 2 + app/serializers/rest/instance_serializer.rb | 20 +- app/serializers/rest/mute_serializer.rb | 6 +- app/serializers/rest/preferences_serializer.rb | 6 + .../rest/status_domain_permission_serializer.rb | 10 + app/serializers/rest/status_serializer.rb | 70 +++- .../activitypub/fetch_collection_items_service.rb | 167 ++++++++ .../fetch_featured_collection_service.rb | 5 +- app/services/activitypub/fetch_replies_service.rb | 57 +-- .../activitypub/process_account_service.rb | 15 +- .../process_collection_items_service.rb | 30 ++ app/services/after_block_service.rb | 14 + app/services/block_service.rb | 8 +- app/services/concerns/payloadable.rb | 2 +- app/services/fan_out_on_write_service.rb | 43 +- app/services/fetch_remote_status_service.rb | 12 +- app/services/fetch_resource_service.rb | 16 +- app/services/keys/query_service.rb | 2 +- app/services/mute_conversation_service.rb | 10 + app/services/mute_service.rb | 4 +- app/services/mute_status_service.rb | 10 + app/services/notify_service.rb | 7 +- app/services/post_status_service.rb | 82 +++- app/services/precompute_feed_service.rb | 1 + app/services/process_command_tags_service.rb | 10 + app/services/process_hashtags_service.rb | 10 +- app/services/process_mentions_service.rb | 59 +-- app/services/publish_status_service.rb | 45 +++ app/services/reblog_service.rb | 4 +- app/services/remove_hashtags_service.rb | 21 + app/services/remove_media_attachments_service.rb | 11 + app/services/remove_status_service.rb | 34 +- app/services/resolve_mentions_service.rb | 61 +++ app/services/resolve_url_service.rb | 4 +- app/services/revoke_status_service.rb | 104 +++++ app/services/search_service.rb | 2 +- app/services/unfollow_service.rb | 5 +- app/services/update_status_service.rb | 161 ++++++++ app/validators/poll_validator.rb | 8 +- app/views/about/_domain_allows.html.haml | 12 + app/views/about/_registration.html.haml | 5 +- app/views/about/more.html.haml | 10 +- app/views/about/show.html.haml | 171 ++++---- app/views/accounts/_header.html.haml | 5 +- app/views/accounts/show.html.haml | 10 +- app/views/admin/accounts/index.html.haml | 2 +- app/views/admin/domain_allows/new.html.haml | 1 + app/views/admin/instances/index.html.haml | 10 +- app/views/admin/instances/show.html.haml | 7 +- app/views/admin/pending_accounts/index.html.haml | 2 +- app/views/admin/settings/edit.html.haml | 47 +-- app/views/auth/registrations/new.html.haml | 5 +- app/views/layouts/application.html.haml | 5 + app/views/layouts/public.html.haml | 7 +- .../settings/preferences/appearance/show.html.haml | 29 ++ .../settings/preferences/filters/show.html.haml | 22 ++ .../settings/preferences/other/show.html.haml | 5 - .../settings/preferences/publishing/show.html.haml | 23 ++ app/views/settings/profiles/show.html.haml | 29 +- app/views/statuses/_detailed_status.html.haml | 21 +- app/views/statuses/_simple_status.html.haml | 25 +- app/views/statuses/_status.html.haml | 9 +- app/views/statuses/show.html.haml | 4 + app/workers/activitypub/distribution_worker.rb | 13 +- .../process_collection_items_for_account_worker.rb | 20 + .../activitypub/process_collection_items_worker.rb | 27 ++ .../activitypub/reply_distribution_worker.rb | 7 +- app/workers/activitypub/sync_account_worker.rb | 57 +++ app/workers/distribution_worker.rb | 5 +- app/workers/fetch_reply_worker.rb | 9 +- app/workers/link_crawl_worker.rb | 3 +- app/workers/move_worker.rb | 3 + app/workers/mute_conversation_worker.rb | 11 + app/workers/publish_scheduled_status_worker.rb | 2 + app/workers/redownload_media_worker.rb | 19 +- app/workers/remove_media_attachments_worker.rb | 11 + app/workers/reset_account_worker.rb | 16 + app/workers/revoke_status_worker.rb | 11 + app/workers/scheduler/ambassador_scheduler.rb | 56 +++ .../scheduler/database_cleanup_scheduler.rb | 14 + app/workers/scheduler/publish_status_scheduler.rb | 11 + app/workers/scheduler/status_cleanup_scheduler.rb | 13 + app/workers/scheduler/user_cleanup_scheduler.rb | 5 + app/workers/softblock_worker.rb | 16 + app/workers/thread_resolve_worker.rb | 7 +- config/application.rb | 6 +- config/environments/development.rb | 2 +- config/environments/production.rb | 2 +- config/environments/test.rb | 4 +- config/initializers/2_whitelist_mode.rb | 2 +- config/initializers/doorkeeper.rb | 9 + config/initializers/inflections.rb | 2 + config/initializers/locale.rb | 1 + config/initializers/sidekiq.rb | 4 +- config/locales/en-MP.yml | 164 ++++++++ config/locales/simple_form.en-MP.yml | 80 ++++ config/navigation.rb | 4 +- config/routes.rb | 32 +- config/settings.yml | 2 + config/sidekiq.yml | 12 + .../20200628105849_add_hidden_to_domain_allows.rb | 7 + .../20200630222227_add_edited_to_statuses.rb | 10 + ...00630222517_backfill_default_statuses_edited.rb | 14 + ...032702_add_conversation_id_index_to_statuses.rb | 7 + ...171939_add_not_null_to_monsterfork_additions.rb | 11 + .../20200717014609_add_nest_level_to_statuses.rb | 7 + ...18011317_add_require_dereference_to_accounts.rb | 7 + .../20200719024610_add_show_replies_to_accounts.rb | 7 + ...20200719033609_add_show_unlisted_to_accounts.rb | 7 + .../20200719114344_add_timelines_only_to_mute.rb | 7 + .../20200719181947_add_published_to_statuses.rb | 7 + ...0719184152_add_unpublished_index_to_statuses.rb | 7 + ...200720211530_add_hidden_to_conversation_mute.rb | 7 + db/migrate/20200720212317_create_status_mutes.rb | 10 + ...it_visibility_of_replies_to_private_statuses.rb | 13 + ...00721195456_add_index_on_statuses_visibility.rb | 7 + ...200721202723_add_account_id_to_conversations.rb | 9 + ...1212401_backfill_account_id_on_conversations.rb | 15 + .../20200721221427_add_public_to_conversations.rb | 7 + ...00721221659_backfill_conversation_visibility.rb | 15 + db/migrate/20200723225552_add_title_to_statuses.rb | 5 + ...200724035808_add_inline_to_media_attachments.rb | 7 + ...200724045955_create_inline_media_attachments.rb | 12 + ...00725071818_create_status_domain_permissions.rb | 13 + ...0725080000_create_account_domain_permissions.rb | 13 + .../20200726094737_add_semiprivate_to_statuses.rb | 7 + ...00726095058_backfill_semiprivate_on_statuses.rb | 14 + ...20200728135753_add_original_text_to_statuses.rb | 5 + .../20200728171900_add_private_to_accounts.rb | 7 + .../20200728173757_add_require_auth_to_accounts.rb | 7 + .../20200731064236_create_account_metadata.rb | 10 + .../20200731135033_backfill_account_metadata.rb | 11 + .../20200731163700_create_destructing_statuses.rb | 11 + db/migrate/20200731205913_create_queued_boosts.rb | 10 + .../20200731211100_create_publishing_delays.rb | 10 + ...0801210543_add_accounts_to_publishing_delays.rb | 9 + ...1220000_add_accounts_to_destructing_statuses.rb | 9 + db/migrate/20200811024642_update_status_indexes.rb | 23 ++ .../20200816200108_add_root_to_conversations.rb | 7 + ...0200816200239_backfill_root_to_conversations.rb | 19 + ...20200817003033_add_defaults_to_conversations.rb | 8 + ...20200817003653_status_mute_account_id_bigint.rb | 7 + .../20200817225525_add_footer_to_statuses.rb | 5 + ...0200818040629_add_last_synced_at_to_accounts.rb | 5 + .../20200818160057_create_collection_items.rb | 12 + .../20200818160106_create_collection_pages.rb | 13 + ...200821051721_add_retries_to_collection_items.rb | 5 + ...4516_remove_public_column_from_conversations.rb | 7 + .../20200823002835_unlink_blocked_replies.rb | 28 ++ ...00826125821_add_username_and_nospam_to_users.rb | 6 + ...527_add_sticky_to_account_domain_permissions.rb | 7 + .../20200901183004_backfill_user_username.rb | 11 + .../20200904002209_add_expires_at_to_statuses.rb | 8 + .../20200904004330_add_publish_at_to_statuses.rb | 8 + db/migrate/20200904005553_drop_publishing_delay.rb | 5 + .../20200904005706_drop_destructing_status.rb | 5 + ...184045_add_originally_local_only_to_statuses.rb | 7 + ...0200904184155_backfill_originally_local_only.rb | 14 + ...4200803_backfill_default_false_to_local_only.rb | 13 + ...200904201028_add_default_false_to_local_only.rb | 7 + db/schema.rb | 124 +++++- lib/mastodon/snowflake.rb | 13 +- lib/mastodon/version.rb | 41 +- lib/tasks/monsterfork.rake | 22 ++ monsterfork.code-workspace | 11 + package.json | 3 +- public/registration.js | 54 +++ yarn.lock | 45 ++- 361 files changed, 8250 insertions(+), 807 deletions(-) create mode 100644 app/controllers/api/v1/admin/domain_allows_controller.rb create mode 100644 app/controllers/api/v1/admin/domain_blocks_controller.rb create mode 100644 app/controllers/api/v1/domain_permissions_controller.rb create mode 100644 app/controllers/api/v1/statuses/hides_controller.rb create mode 100644 app/controllers/api/v1/statuses/publishing_controller.rb create mode 100644 app/controllers/settings/preferences/filters_controller.rb create mode 100644 app/controllers/settings/preferences/publishing_controller.rb create mode 100644 app/controllers/user_profile_css_controller.rb create mode 100644 app/controllers/user_webapp_css_controller.rb create mode 100644 app/helpers/img_proxy_helper.rb create mode 100644 app/javascript/flavours/glitch/locales/en-MP.js create mode 100644 app/javascript/flavours/glitch/styles/monsterfork/about.scss create mode 100644 app/javascript/flavours/glitch/styles/monsterfork/components/composer.scss create mode 100644 app/javascript/flavours/glitch/styles/monsterfork/components/formatting.scss create mode 100644 app/javascript/flavours/glitch/styles/monsterfork/components/index.scss create mode 100644 app/javascript/flavours/glitch/styles/monsterfork/components/status.scss create mode 100644 app/javascript/flavours/glitch/styles/monsterfork/index.scss create mode 100644 app/javascript/flavours/glitch/styles/nightshade.scss create mode 100644 app/javascript/flavours/glitch/styles/nightshade/diff.scss create mode 100644 app/javascript/flavours/glitch/styles/nightshade/variables.scss create mode 100644 app/javascript/fonts/opensans/LICENSE.txt create mode 100644 app/javascript/fonts/opensans/OpenSans-Bold.ttf create mode 100644 app/javascript/fonts/opensans/OpenSans-Bold.woff2 create mode 100644 app/javascript/fonts/opensans/OpenSans-BoldItalic.ttf create mode 100644 app/javascript/fonts/opensans/OpenSans-BoldItalic.woff2 create mode 100644 app/javascript/fonts/opensans/OpenSans-ExtraBold.ttf create mode 100644 app/javascript/fonts/opensans/OpenSans-ExtraBold.woff2 create mode 100644 app/javascript/fonts/opensans/OpenSans-ExtraBoldItalic.ttf create mode 100644 app/javascript/fonts/opensans/OpenSans-ExtraBoldItalic.woff2 create mode 100644 app/javascript/fonts/opensans/OpenSans-Italic.ttf create mode 100644 app/javascript/fonts/opensans/OpenSans-Italic.woff2 create mode 100644 app/javascript/fonts/opensans/OpenSans-Light.ttf create mode 100644 app/javascript/fonts/opensans/OpenSans-Light.woff2 create mode 100644 app/javascript/fonts/opensans/OpenSans-LightItalic.ttf create mode 100644 app/javascript/fonts/opensans/OpenSans-LightItalic.woff2 create mode 100644 app/javascript/fonts/opensans/OpenSans-Regular.ttf create mode 100644 app/javascript/fonts/opensans/OpenSans-Regular.woff2 create mode 100644 app/javascript/fonts/opensans/OpenSans-SemiBold.ttf create mode 100644 app/javascript/fonts/opensans/OpenSans-SemiBold.woff2 create mode 100644 app/javascript/fonts/opensans/OpenSans-SemiBoldItalic.ttf create mode 100644 app/javascript/fonts/opensans/OpenSans-SemiBoldItalic.woff2 create mode 100644 app/javascript/locales/locale-data/en-MP.js create mode 100644 app/javascript/mastodon/locales/en-MP.json create mode 100644 app/javascript/mastodon/locales/locale-data/en-MP.js create mode 100644 app/javascript/mastodon/locales/whitelist_en-MP.json create mode 100644 app/javascript/skins/glitch/nightshade/common.scss create mode 100644 app/javascript/skins/glitch/nightshade/names.yml create mode 100644 app/javascript/styles/fonts/opensans.scss create mode 100644 app/lib/command_tag/command/account_tools.rb create mode 100644 app/lib/command_tag/command/footer_tools.rb create mode 100644 app/lib/command_tag/command/hello_world.rb create mode 100644 app/lib/command_tag/command/parent_status_tools.rb create mode 100644 app/lib/command_tag/command/status_tools.rb create mode 100644 app/lib/command_tag/command/text_tools.rb create mode 100644 app/lib/command_tag/command/variables.rb create mode 100644 app/lib/command_tag/commands.rb create mode 100644 app/lib/command_tag/processor.rb create mode 100644 app/lib/img_tag_handler.rb create mode 100644 app/models/account_domain_permission.rb create mode 100644 app/models/account_metadata.rb create mode 100644 app/models/collection_item.rb create mode 100644 app/models/collection_page.rb create mode 100644 app/models/inline_media_attachment.rb create mode 100644 app/models/queued_boost.rb create mode 100644 app/models/status_domain_permission.rb create mode 100644 app/models/status_mute.rb create mode 100644 app/policies/account_domain_permission_policy.rb create mode 100644 app/serializers/rest/account_domain_permission_serializer.rb create mode 100644 app/serializers/rest/status_domain_permission_serializer.rb create mode 100644 app/services/activitypub/fetch_collection_items_service.rb create mode 100644 app/services/activitypub/process_collection_items_service.rb create mode 100644 app/services/mute_conversation_service.rb create mode 100644 app/services/mute_status_service.rb create mode 100644 app/services/process_command_tags_service.rb create mode 100644 app/services/publish_status_service.rb create mode 100644 app/services/remove_hashtags_service.rb create mode 100644 app/services/remove_media_attachments_service.rb create mode 100644 app/services/resolve_mentions_service.rb create mode 100644 app/services/revoke_status_service.rb create mode 100644 app/services/update_status_service.rb create mode 100644 app/views/about/_domain_allows.html.haml create mode 100644 app/views/settings/preferences/filters/show.html.haml create mode 100644 app/views/settings/preferences/publishing/show.html.haml create mode 100644 app/workers/activitypub/process_collection_items_for_account_worker.rb create mode 100644 app/workers/activitypub/process_collection_items_worker.rb create mode 100644 app/workers/activitypub/sync_account_worker.rb create mode 100644 app/workers/mute_conversation_worker.rb create mode 100644 app/workers/remove_media_attachments_worker.rb create mode 100644 app/workers/reset_account_worker.rb create mode 100644 app/workers/revoke_status_worker.rb create mode 100644 app/workers/scheduler/ambassador_scheduler.rb create mode 100644 app/workers/scheduler/database_cleanup_scheduler.rb create mode 100644 app/workers/scheduler/publish_status_scheduler.rb create mode 100644 app/workers/scheduler/status_cleanup_scheduler.rb create mode 100644 app/workers/softblock_worker.rb create mode 100644 config/locales/en-MP.yml create mode 100644 config/locales/simple_form.en-MP.yml create mode 100644 db/migrate/20200628105849_add_hidden_to_domain_allows.rb create mode 100644 db/migrate/20200630222227_add_edited_to_statuses.rb create mode 100644 db/migrate/20200630222517_backfill_default_statuses_edited.rb create mode 100644 db/migrate/20200702032702_add_conversation_id_index_to_statuses.rb create mode 100644 db/migrate/20200706171939_add_not_null_to_monsterfork_additions.rb create mode 100644 db/migrate/20200717014609_add_nest_level_to_statuses.rb create mode 100644 db/migrate/20200718011317_add_require_dereference_to_accounts.rb create mode 100644 db/migrate/20200719024610_add_show_replies_to_accounts.rb create mode 100644 db/migrate/20200719033609_add_show_unlisted_to_accounts.rb create mode 100644 db/migrate/20200719114344_add_timelines_only_to_mute.rb create mode 100644 db/migrate/20200719181947_add_published_to_statuses.rb create mode 100644 db/migrate/20200719184152_add_unpublished_index_to_statuses.rb create mode 100644 db/migrate/20200720211530_add_hidden_to_conversation_mute.rb create mode 100644 db/migrate/20200720212317_create_status_mutes.rb create mode 100644 db/migrate/20200721184347_limit_visibility_of_replies_to_private_statuses.rb create mode 100644 db/migrate/20200721195456_add_index_on_statuses_visibility.rb create mode 100644 db/migrate/20200721202723_add_account_id_to_conversations.rb create mode 100644 db/migrate/20200721212401_backfill_account_id_on_conversations.rb create mode 100644 db/migrate/20200721221427_add_public_to_conversations.rb create mode 100644 db/migrate/20200721221659_backfill_conversation_visibility.rb create mode 100644 db/migrate/20200723225552_add_title_to_statuses.rb create mode 100644 db/migrate/20200724035808_add_inline_to_media_attachments.rb create mode 100644 db/migrate/20200724045955_create_inline_media_attachments.rb create mode 100644 db/migrate/20200725071818_create_status_domain_permissions.rb create mode 100644 db/migrate/20200725080000_create_account_domain_permissions.rb create mode 100644 db/migrate/20200726094737_add_semiprivate_to_statuses.rb create mode 100644 db/migrate/20200726095058_backfill_semiprivate_on_statuses.rb create mode 100644 db/migrate/20200728135753_add_original_text_to_statuses.rb create mode 100644 db/migrate/20200728171900_add_private_to_accounts.rb create mode 100644 db/migrate/20200728173757_add_require_auth_to_accounts.rb create mode 100644 db/migrate/20200731064236_create_account_metadata.rb create mode 100644 db/migrate/20200731135033_backfill_account_metadata.rb create mode 100644 db/migrate/20200731163700_create_destructing_statuses.rb create mode 100644 db/migrate/20200731205913_create_queued_boosts.rb create mode 100644 db/migrate/20200731211100_create_publishing_delays.rb create mode 100644 db/migrate/20200801210543_add_accounts_to_publishing_delays.rb create mode 100644 db/migrate/20200801220000_add_accounts_to_destructing_statuses.rb create mode 100644 db/migrate/20200811024642_update_status_indexes.rb create mode 100644 db/migrate/20200816200108_add_root_to_conversations.rb create mode 100644 db/migrate/20200816200239_backfill_root_to_conversations.rb create mode 100644 db/migrate/20200817003033_add_defaults_to_conversations.rb create mode 100644 db/migrate/20200817003653_status_mute_account_id_bigint.rb create mode 100644 db/migrate/20200817225525_add_footer_to_statuses.rb create mode 100644 db/migrate/20200818040629_add_last_synced_at_to_accounts.rb create mode 100644 db/migrate/20200818160057_create_collection_items.rb create mode 100644 db/migrate/20200818160106_create_collection_pages.rb create mode 100644 db/migrate/20200821051721_add_retries_to_collection_items.rb create mode 100644 db/migrate/20200822054516_remove_public_column_from_conversations.rb create mode 100644 db/migrate/20200823002835_unlink_blocked_replies.rb create mode 100644 db/migrate/20200826125821_add_username_and_nospam_to_users.rb create mode 100644 db/migrate/20200901035527_add_sticky_to_account_domain_permissions.rb create mode 100644 db/migrate/20200901183004_backfill_user_username.rb create mode 100644 db/migrate/20200904002209_add_expires_at_to_statuses.rb create mode 100644 db/migrate/20200904004330_add_publish_at_to_statuses.rb create mode 100644 db/migrate/20200904005553_drop_publishing_delay.rb create mode 100644 db/migrate/20200904005706_drop_destructing_status.rb create mode 100644 db/migrate/20200904184045_add_originally_local_only_to_statuses.rb create mode 100644 db/migrate/20200904184155_backfill_originally_local_only.rb create mode 100644 db/migrate/20200904200803_backfill_default_false_to_local_only.rb create mode 100644 db/migrate/20200904201028_add_default_false_to_local_only.rb create mode 100644 lib/tasks/monsterfork.rake create mode 100644 monsterfork.code-workspace create mode 100644 public/registration.js diff --git a/.rubocop.yml b/.rubocop.yml index 14728bf0e..74651358c 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -299,3 +299,45 @@ Style/TrailingCommaInHashLiteral: Style/UnpackFirst: Enabled: false + +Layout/EmptyLinesAroundAttributeAccessor: + Enabled: true + +Layout/SpaceAroundMethodCallOperator: + Enabled: true + +Lint/DeprecatedOpenSSLConstant: + Enabled: true + +Lint/MixedRegexpCaptureTypes: + Enabled: true + +Lint/RaiseException: + Enabled: true + +Lint/StructNewOverride: + Enabled: true + +Style/ExponentialNotation: + Enabled: true + +Style/HashEachMethods: + Enabled: true + +Style/HashTransformKeys: + Enabled: true + +Style/HashTransformValues: + Enabled: true + +Style/RedundantFetchBlock: + Enabled: true + +Style/RedundantRegexpCharacterClass: + Enabled: true + +Style/RedundantRegexpEscape: + Enabled: true + +Style/SlicingWithRange: + Enabled: true diff --git a/Gemfile b/Gemfile index 6e0ece406..cab8eae87 100644 --- a/Gemfile +++ b/Gemfile @@ -162,3 +162,9 @@ end gem 'concurrent-ruby', require: false gem 'connection_pool', require: false + +gem "reek", "~> 6.0", :group => :development + +gem "w3c_validators", "~> 1.3" + +gem "activerecord-import", "~> 1.0" diff --git a/Gemfile.lock b/Gemfile.lock index e72a3be84..15973bdec 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -54,6 +54,8 @@ GEM activemodel (= 5.2.4.3) activesupport (= 5.2.4.3) arel (>= 9.0) + activerecord-import (1.0.6) + activerecord (>= 3.2) activestorage (5.2.4.3) actionpack (= 5.2.4.3) activerecord (= 5.2.4.3) @@ -319,6 +321,7 @@ GEM activerecord kaminari-core (= 1.2.1) kaminari-core (1.2.1) + kwalify (0.7.2) launchy (2.5.0) addressable (~> 2.7) letter_opener (1.7.0) @@ -426,6 +429,7 @@ GEM pry (~> 0.13.0) pry-rails (0.3.9) pry (>= 0.10.4) + psych (3.1.0) public_suffix (4.0.5) puma (4.3.5) nio4r (~> 2.0) @@ -501,6 +505,11 @@ GEM redis-store (>= 1.2, < 2) redis-store (1.9.0) redis (>= 4, < 5) + reek (6.0.1) + kwalify (~> 0.7.0) + parser (>= 2.5.0.0, < 2.8, != 2.5.1.1) + psych (~> 3.1.0) + rainbow (>= 2.0, < 4.0) regexp_parser (1.7.1) request_store (1.5.0) rack (>= 1.4) @@ -642,6 +651,9 @@ GEM unf_ext (0.0.7.7) unicode-display_width (1.7.0) uniform_notifier (1.13.0) + w3c_validators (1.3.5) + json (>= 1.8) + nokogiri (~> 1.6) warden (1.2.8) rack (>= 2.0.6) webauthn (3.0.0.alpha1) @@ -679,6 +691,7 @@ PLATFORMS DEPENDENCIES active_model_serializers (~> 0.10) active_record_query_trace (~> 1.7) + activerecord-import (~> 1.0) addressable (~> 2.7) annotate (~> 3.1) aws-sdk-s3 (~> 1.79) @@ -776,6 +789,7 @@ DEPENDENCIES redis (~> 4.2) redis-namespace (~> 1.8) redis-rails (~> 5.0) + reek (~> 6.0) rqrcode (~> 1.1) rspec-rails (~> 4.0) rspec-sidekiq (~> 3.1) @@ -802,7 +816,14 @@ DEPENDENCIES tty-prompt (~> 0.22) twitter-text (~> 1.14) tzinfo-data (~> 1.2020) + w3c_validators (~> 1.3) webauthn (~> 3.0.0.alpha1) webmock (~> 3.8) webpacker (~> 5.2) webpush + +RUBY VERSION + ruby 2.6.6p146 + +BUNDLED WITH + 2.1.4 diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index 5d5db937c..bf3d3ff42 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -4,16 +4,14 @@ class AboutController < ApplicationController before_action :set_pack layout 'public' - before_action :require_open_federation!, only: [:show, :more] + #before_action :require_open_federation!, only: [:show, :more] before_action :set_body_classes, only: :show before_action :set_instance_presenter before_action :set_expires_in, only: [:show, :more, :terms] skip_before_action :require_functional!, only: [:more, :terms] - def show; end - - def more + def show flash.now[:notice] = I18n.t('about.instance_actor_flash') if params[:instance_actor] toc_generator = TOCGenerator.new(@instance_presenter.site_extended_description) @@ -21,10 +19,15 @@ class AboutController < ApplicationController @contents = toc_generator.html @table_of_contents = toc_generator.toc @blocks = DomainBlock.with_user_facing_limitations.by_severity if display_blocks? + @allows = DomainAllow.where(hidden: false) if display_allows? end + alias more show + def terms; end + helper_method :display_allows? + helper_method :display_blocks? helper_method :display_blocks_rationale? helper_method :public_fetch_mode? @@ -66,4 +69,10 @@ class AboutController < ApplicationController def set_expires_in expires_in 0, public: true end + + # Monsterfork additions + + def display_allows? + Setting.show_domain_allows == 'all' || (Setting.show_domain_allows == 'users' && user_signed_in?) + end end diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 54106933c..232a5fc71 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -10,20 +10,24 @@ class AccountsController < ApplicationController before_action :set_cache_headers before_action :set_body_classes + before_action :require_authenticated!, if: -> { @account.require_auth? || @account.private? } + skip_around_action :set_locale, if: -> { [:json, :rss].include?(request.format&.to_sym) } - skip_before_action :require_functional!, unless: :whitelist_mode? + skip_before_action :require_functional! # , unless: :whitelist_mode? def show + @without_unlisted = !@account.show_unlisted? + respond_to do |format| format.html do use_pack 'public' - expires_in 0, public: true unless user_signed_in? + expires_in 0, public: true unless user_signed_in? || signed_request_account.present? @pinned_statuses = [] - @endorsed_accounts = @account.endorsed_accounts.to_a.sample(4) - @featured_hashtags = @account.featured_tags.order(statuses_count: :desc) + @endorsed_accounts = unauthorized? ? [] : @account.endorsed_accounts.to_a.sample(4) + @featured_hashtags = unauthorized? ? [] : @account.featured_tags.order(statuses_count: :desc) - if current_account && @account.blocking?(current_account) + if unauthorized? @statuses = [] return end @@ -39,16 +43,19 @@ class AccountsController < ApplicationController end format.rss do - expires_in 1.minute, public: true + return forbidden if unauthorized? + + expires_in 1.minute, public: !current_account? - limit = params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE + @without_unlisted = true + limit = params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE @statuses = filtered_statuses.without_reblogs.limit(limit) @statuses = cache_collection(@statuses, Status) render xml: RSS::AccountSerializer.render(@account, @statuses, params[:tag]) end format.json do - expires_in 3.minutes, public: !(authorized_fetch_mode? && signed_request_account.present?) + expires_in 3.minutes, public: !current_account? render_with_cache json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter, fields: restrict_fields_to end end @@ -61,19 +68,28 @@ class AccountsController < ApplicationController end def show_pinned_statuses? - [replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none? + [threads_requested?, replies_requested?, reblogs_requested?, mentions_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none? end def filtered_statuses + return mentions_scope if mentions_requested? + default_statuses.tap do |statuses| - statuses.merge!(hashtag_scope) if tag_requested? statuses.merge!(only_media_scope) if media_requested? - statuses.merge!(no_replies_scope) unless replies_requested? end end def default_statuses - @account.statuses.not_local_only.where(visibility: [:public, :unlisted]) + @account.statuses.permitted_for( + @account, + current_account, + include_semiprivate: true, + include_reblogs: !(threads_requested? || replies_requested?), + only_reblogs: reblogs_requested?, + include_replies: replies_requested?, + tag: tag_requested? ? params[:tag] : nil, + public: @without_unlisted + ) end def only_media_scope @@ -84,18 +100,10 @@ class AccountsController < ApplicationController @account.media_attachments.attached.reorder(nil).select(:status_id).distinct end - def no_replies_scope - Status.without_replies - end - - def hashtag_scope - tag = Tag.find_normalized(params[:tag]) + def mentions_scope + return Status.none unless current_account? - if tag - Status.tagged_with(tag.id) - else - Status.none - end + Status.mentions_between(@account, current_account) end def username_param @@ -123,8 +131,14 @@ class AccountsController < ApplicationController short_account_tag_url(@account, params[:tag], max_id: max_id, min_id: min_id) elsif media_requested? short_account_media_url(@account, max_id: max_id, min_id: min_id) + elsif threads_requested? + short_account_threads_url(@account, max_id: max_id, min_id: min_id) elsif replies_requested? short_account_with_replies_url(@account, max_id: max_id, min_id: min_id) + elsif reblogs_requested? + short_account_reblogs_url(@account, max_id: max_id, min_id: min_id) + elsif mentions_requested? + short_account_mentions_url(@account, max_id: max_id, min_id: min_id) else short_account_url(@account, max_id: max_id, min_id: min_id) end @@ -134,7 +148,13 @@ class AccountsController < ApplicationController request.path.split('.').first.ends_with?('/media') && !tag_requested? end + def threads_requested? + request.path.split('.').first.ends_with?('/threads') && !tag_requested? + end + def replies_requested? + return false unless current_account&.id == @account.id || @account.show_replies? + request.path.split('.').first.ends_with?('/with_replies') && !tag_requested? end @@ -151,15 +171,31 @@ class AccountsController < ApplicationController ) end + def reblogs_requested? + request.path.split('.').first.ends_with?('/reblogs') && !tag_requested? + end + + def mentions_requested? + request.path.split('.').first.ends_with?('/mentions') && !tag_requested? + end + def params_slice(*keys) params.slice(*keys).permit(*keys) end def restrict_fields_to - if signed_request_account.present? || public_fetch_mode? + if current_account&.id == @account.id || (signed_request_account.present? && !blocked?) # Return all fields else %i(id type preferred_username inbox public_key endpoints) end end + + def blocked? + @blocked ||= current_account && @account.blocking?(current_account) + end + + def unauthorized? + @unauthorized ||= blocked? || (@account.private? && !following?(@account)) + end end diff --git a/app/controllers/activitypub/claims_controller.rb b/app/controllers/activitypub/claims_controller.rb index 08ad952df..5009a9f05 100644 --- a/app/controllers/activitypub/claims_controller.rb +++ b/app/controllers/activitypub/claims_controller.rb @@ -4,7 +4,7 @@ class ActivityPub::ClaimsController < ActivityPub::BaseController include SignatureVerification include AccountOwnedConcern - skip_before_action :authenticate_user! + #skip_before_action :authenticate_user! before_action :require_signature! before_action :set_claim_result diff --git a/app/controllers/activitypub/inboxes_controller.rb b/app/controllers/activitypub/inboxes_controller.rb index 0a561e7f0..3e67f3909 100644 --- a/app/controllers/activitypub/inboxes_controller.rb +++ b/app/controllers/activitypub/inboxes_controller.rb @@ -7,7 +7,7 @@ class ActivityPub::InboxesController < ActivityPub::BaseController before_action :skip_unknown_actor_delete before_action :require_signature! - skip_before_action :authenticate_user! + #skip_before_action :authenticate_user! def create upgrade_account diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb index e066860bf..51945656f 100644 --- a/app/controllers/activitypub/outboxes_controller.rb +++ b/app/controllers/activitypub/outboxes_controller.rb @@ -10,9 +10,12 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController before_action :set_statuses before_action :set_cache_headers + before_action :require_authenticated!, if: -> { @account.require_auth? } + before_action -> { require_following!(@account) }, if: -> { @account.private? } + def show - expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode? && !(signed_request_account.present? && page_requested?)) - render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' + expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode? && !(current_account.present? && page_requested?)) + render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json', target_domain: current_account&.domain end private @@ -31,7 +34,6 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController ActivityPub::CollectionPresenter.new( id: account_outbox_url(@account), type: :ordered, - size: @account.statuses_count, first: outbox_url(page: true), last: outbox_url(page: true, min_id: 0) ) @@ -54,12 +56,39 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController account_outbox_url(@account, page: true, min_id: @statuses.first.id) unless @statuses.empty? end + def permitted_account_statuses + @account.statuses.permitted_for( + @account, + current_account, + include_replies: true, + include_reblogs: true, + public: !(owner? || follower?), + include_semiprivate: owner? || mutual_follower?, + exclude_local_only: true + ) + end + + def owner? + return @owner if defined?(@owner) + + @owner = @account.id == current_account&.id + @owner ||= @account.moved_to_account_id == current_account&.id if @account.moved_to_account_id.present? + @owner + end + + def follower? + @following ||= current_account&.following?(@account) + end + + def mutual_follower? + follower? && @account.following?(current_account) + end + def set_statuses return unless page_requested? - @statuses = @account.statuses.permitted_for(@account, signed_request_account) @statuses = cache_collection_paginated_by_id( - @statuses, + permitted_account_statuses, Status, LIMIT, params_slice(:max_id, :min_id, :since_id) diff --git a/app/controllers/activitypub/replies_controller.rb b/app/controllers/activitypub/replies_controller.rb index 43bf4e657..4d553fc07 100644 --- a/app/controllers/activitypub/replies_controller.rb +++ b/app/controllers/activitypub/replies_controller.rb @@ -14,7 +14,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController def index expires_in 0, public: public_fetch_mode? - render json: replies_collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json', skip_activities: true + render json: replies_collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json', skip_activities: true, target_domain: current_account&.domain end private @@ -33,6 +33,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController def set_replies @replies = only_other_accounts? ? Status.where.not(account_id: @account.id) : @account.statuses @replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted]) + @replies = @replies.without_semiprivate unless authenticated_or_following?(@account) @replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id]) end diff --git a/app/controllers/admin/domain_allows_controller.rb b/app/controllers/admin/domain_allows_controller.rb index 31be1978b..95d9a31fb 100644 --- a/app/controllers/admin/domain_allows_controller.rb +++ b/app/controllers/admin/domain_allows_controller.rb @@ -35,6 +35,6 @@ class Admin::DomainAllowsController < Admin::BaseController end def resource_params - params.require(:domain_allow).permit(:domain) + params.require(:domain_allow).permit(:domain, :hidden) end end diff --git a/app/controllers/admin/pending_accounts_controller.rb b/app/controllers/admin/pending_accounts_controller.rb index b62a9bc84..8a9a51d84 100644 --- a/app/controllers/admin/pending_accounts_controller.rb +++ b/app/controllers/admin/pending_accounts_controller.rb @@ -18,19 +18,19 @@ module Admin end def approve_all - Form::AccountBatch.new(current_account: current_account, account_ids: User.pending.pluck(:account_id), action: 'approve').save + Form::AccountBatch.new(current_account: current_account, account_ids: User.pending.confirmed.pluck(:account_id), action: 'approve').save redirect_to admin_pending_accounts_path(current_params) end def reject_all - Form::AccountBatch.new(current_account: current_account, account_ids: User.pending.pluck(:account_id), action: 'reject').save + Form::AccountBatch.new(current_account: current_account, account_ids: User.pending.confirmed.pluck(:account_id), action: 'reject').save redirect_to admin_pending_accounts_path(current_params) end private def set_accounts - @accounts = Account.joins(:user).merge(User.pending.recent).includes(user: :invite_request).page(params[:page]) + @accounts = Account.joins(:user).merge(User.pending.confirmed.recent).includes(user: :invite_request).page(params[:page]) end def form_account_batch_params diff --git a/app/controllers/admin/tags_controller.rb b/app/controllers/admin/tags_controller.rb index 59df4470e..8abe19626 100644 --- a/app/controllers/admin/tags_controller.rb +++ b/app/controllers/admin/tags_controller.rb @@ -54,7 +54,7 @@ module Admin def set_usage_by_domain @usage_by_domain = @tag.statuses - .with_public_visibility + .distributable .excluding_silenced_accounts .where(Status.arel_table[:id].gteq(Mastodon::Snowflake.id_at(Time.now.utc.beginning_of_day))) .joins(:account) diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index 467225547..ac49a4dca 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -7,7 +7,7 @@ class Api::BaseController < ApplicationController include RateLimitHeaders skip_before_action :store_current_location - skip_before_action :require_functional!, unless: :whitelist_mode? + skip_before_action :require_functional! #, unless: :whitelist_mode? before_action :require_authenticated_user!, if: :disallow_unauthenticated_api_access? before_action :set_cache_headers diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb index 64b5cb747..3c8187a99 100644 --- a/app/controllers/api/v1/accounts/credentials_controller.rb +++ b/app/controllers/api/v1/accounts/credentials_controller.rb @@ -21,7 +21,9 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController private def account_params - params.permit(:display_name, :note, :avatar, :header, :locked, :bot, :discoverable, fields_attributes: [:name, :value]) + params.permit(:display_name, :note, :avatar, :header, :locked, :bot, :discoverable, + :require_dereference, :show_replies, :show_unlisted, + fields_attributes: [:name, :value]) end def user_settings_params diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb index 85a9133e3..d7e973e31 100644 --- a/app/controllers/api/v1/accounts/statuses_controller.rb +++ b/app/controllers/api/v1/accounts/statuses_controller.rb @@ -8,7 +8,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController def index @statuses = load_statuses - render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) + render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_account&.id) end private @@ -17,17 +17,17 @@ class Api::V1::Accounts::StatusesController < Api::BaseController @account = Account.find(params[:account_id]) end + def owner? + @account.id == current_account&.id + end + def load_statuses cached_account_statuses end def cached_account_statuses statuses = truthy_param?(:pinned) ? pinned_scope : permitted_account_statuses - statuses.merge!(only_media_scope) if truthy_param?(:only_media) - statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies) - statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs) - statuses.merge!(hashtag_scope) if params[:tagged].present? cache_collection_paginated_by_id( statuses, @@ -38,39 +38,66 @@ class Api::V1::Accounts::StatusesController < Api::BaseController end def permitted_account_statuses - @account.statuses.permitted_for(@account, current_account) + return mentions_scope if truthy_param?(:mentions) + return Status.none if unauthorized? + + @account.statuses.permitted_for( + @account, + current_account, + include_semiprivate: true, + include_reblogs: include_reblogs?, + include_replies: include_replies?, + only_reblogs: only_reblogs?, + only_replies: only_replies?, + include_unpublished: owner?, + tag: params[:tagged] + ) end def only_media_scope Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id) end - def pinned_scope - return Status.none if @account.blocking?(current_account) + def unauthorized? + (@account.private && !following?(@account)) || (@account.require_auth && !current_account?) + end - @account.pinned_statuses + def include_reblogs? + params[:include_reblogs].present? ? truthy_param?(:include_reblogs) : !truthy_param?(:exclude_reblogs) + end + + def include_replies? + return false unless owner? || @account.show_replies? + + params[:include_replies].present? ? truthy_param?(:include_replies) : !truthy_param?(:exclude_replies) end - def no_replies_scope - Status.without_replies + def only_reblogs? + truthy_param?(:only_reblogs).presence || false end - def no_reblogs_scope - Status.without_reblogs + def only_replies? + return false unless owner? || @account.show_replies? + + truthy_param?(:only_replies).presence || false end - def hashtag_scope - tag = Tag.find_normalized(params[:tagged]) + def mentions_scope + return Status.none unless current_account? + + Status.mentions_between(@account, current_account) + end - if tag - Status.tagged_with(tag.id) - else - Status.none - end + def pinned_scope + return Status.none if @account.blocking?(current_account) + + @account.pinned_statuses end def pagination_params(core_params) - params.slice(:limit, :only_media, :exclude_replies).permit(:limit, :only_media, :exclude_replies).merge(core_params) + params.slice(:limit, :only_media, :include_replies, :exclude_replies, :only_replies, :include_reblogs, :exclude_reblogs, :only_relogs, :mentions) + .permit(:limit, :only_media, :include_replies, :exclude_replies, :only_replies, :include_reblogs, :exclude_reblogs, :only_relogs, :mentions) + .merge(core_params) end def insert_pagination_headers @@ -78,15 +105,11 @@ class Api::V1::Accounts::StatusesController < Api::BaseController end def next_path - if records_continue? - api_v1_account_statuses_url pagination_params(max_id: pagination_max_id) - end + api_v1_account_statuses_url pagination_params(max_id: pagination_max_id) if records_continue? end def prev_path - unless @statuses.empty? - api_v1_account_statuses_url pagination_params(min_id: pagination_since_id) - end + api_v1_account_statuses_url pagination_params(min_id: pagination_since_id) unless @statuses.empty? end def records_continue? diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 0080faf33..e9f848ac9 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -44,7 +44,7 @@ class Api::V1::AccountsController < Api::BaseController end def mute - MuteService.new.call(current_user.account, @account, notifications: truthy_param?(:notifications)) + MuteService.new.call(current_user.account, @account, notifications: truthy_param?(:notifications), timelines_only: truthy_param?(:timelines_only)) render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships end diff --git a/app/controllers/api/v1/admin/domain_allows_controller.rb b/app/controllers/api/v1/admin/domain_allows_controller.rb new file mode 100644 index 000000000..1b150d480 --- /dev/null +++ b/app/controllers/api/v1/admin/domain_allows_controller.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +class Api::V1::Admin::DomainAllowsController < Api::BaseController + include Authorization + + LIMIT = 100 + + before_action -> { doorkeeper_authorize! :'admin:read', :'admin:read:domain_allows' }, only: :show + before_action :require_staff! + after_action :insert_pagination_headers, only: :show + + def show + @allows = load_domain_allows + render json: @allows + end + + private + + def load_domain_allows + DomainAllow.paginate_by_max_id( + limit_param(LIMIT), + params[:max_id], + params[:since_id] + ) + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def next_path + api_v1_admin_domain_allows_url pagination_params(max_id: pagination_max_id) if records_continue? + end + + def prev_path + api_v1_admin_domain_allows_url pagination_params(since_id: pagination_since_id) unless @allows.empty? + end + + def pagination_max_id + @allows.last.id + end + + def pagination_since_id + @allows.first.id + end + + def records_continue? + @allows.size == limit_param(LIMIT) + end + + def pagination_params(core_params) + params.slice(:limit).permit(:limit).merge(core_params) + end +end diff --git a/app/controllers/api/v1/admin/domain_blocks_controller.rb b/app/controllers/api/v1/admin/domain_blocks_controller.rb new file mode 100644 index 000000000..c0ce0da25 --- /dev/null +++ b/app/controllers/api/v1/admin/domain_blocks_controller.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +class Api::V1::Admin::DomainBlocksController < Api::BaseController + include Authorization + + LIMIT = 100 + + before_action -> { doorkeeper_authorize! :'admin:read', :'admin:read:domain_blocks' }, only: :show + before_action :require_staff! + after_action :insert_pagination_headers, only: :show + + def show + @blocks = load_domain_blocks + render json: @blocks + end + + private + + def load_domain_blocks + DomainBlock.paginate_by_max_id( + limit_param(LIMIT), + params[:max_id], + params[:since_id] + ) + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def next_path + api_v1_admin_domain_blocks_url pagination_params(max_id: pagination_max_id) if records_continue? + end + + def prev_path + api_v1_admin_domain_blocks_url pagination_params(since_id: pagination_since_id) unless @blocks.empty? + end + + def pagination_max_id + @blocks.last.id + end + + def pagination_since_id + @blocks.first.id + end + + def records_continue? + @blocks.size == limit_param(LIMIT) + end + + def pagination_params(core_params) + params.slice(:limit).permit(:limit).merge(core_params) + end +end diff --git a/app/controllers/api/v1/domain_permissions_controller.rb b/app/controllers/api/v1/domain_permissions_controller.rb new file mode 100644 index 000000000..1b0e37135 --- /dev/null +++ b/app/controllers/api/v1/domain_permissions_controller.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +class Api::V1::DomainPermissionsController < Api::BaseController + before_action -> { doorkeeper_authorize! :read, :'read:domain_permissions', :'read:domain_permissions:account' }, only: :show + before_action -> { doorkeeper_authorize! :write, :'write:domain_permissions', :'write:domain_permissions:account' }, only: [:create, :update, :destroy] + before_action :require_user! + before_action :set_permission, except: [:show, :create] + after_action :insert_pagination_headers + + LIMIT = 100 + + def show + @permissions = load_account_domain_permissions + render json: @permissions, each_serializer: REST::AccountDomainPermissionSerializer + end + + def create + @permission = current_account.domain_permissions.create!(domain_permission_params) + render json: @permission, serializer: REST::AccountDomainPermissionSerializer + end + + def update + @permission.update!(domain_permission_params) + render json: @permission, serializer: REST::AccountDomainPermissionSerializer + end + + def destroy + @permission.destroy! + render_empty + end + + private + + def load_account_domain_permissions + account_domain_permissions.paginate_by_max_id( + limit_param(LIMIT), + params[:max_id], + params[:since_id] + ) + end + + def set_permission + @permission = current_account.domain_permissions.find(params[:id]) + end + + def account_domain_permissions + current_account.domain_permissions + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def next_path + api_v1_domain_permissions_url pagination_params(max_id: pagination_max_id) if records_continue? + end + + def prev_path + api_v1_domain_permissions_url pagination_params(since_id: pagination_since_id) unless @permissions.empty? + end + + def pagination_max_id + @permissions.last.id + end + + def pagination_since_id + @permissions.first.id + end + + def records_continue? + @permissions.size == limit_param(LIMIT) + end + + def pagination_params(core_params) + params.slice(:limit).permit(:limit).merge(core_params) + end + + def domain_permission_params + params.permit(:domain, :visibility) + end +end diff --git a/app/controllers/api/v1/instances/activity_controller.rb b/app/controllers/api/v1/instances/activity_controller.rb index 4f6b4bcbf..f2ac902e1 100644 --- a/app/controllers/api/v1/instances/activity_controller.rb +++ b/app/controllers/api/v1/instances/activity_controller.rb @@ -4,7 +4,7 @@ class Api::V1::Instances::ActivityController < Api::BaseController before_action :require_enabled_api! skip_before_action :set_cache_headers - skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + skip_before_action :require_authenticated_user! #, unless: :whitelist_mode? def show expires_in 1.day, public: true @@ -33,6 +33,6 @@ class Api::V1::Instances::ActivityController < Api::BaseController end def require_enabled_api! - head 404 unless Setting.activity_api_enabled && !whitelist_mode? + head 404 unless Setting.activity_api_enabled #&& !whitelist_mode? end end diff --git a/app/controllers/api/v1/instances/peers_controller.rb b/app/controllers/api/v1/instances/peers_controller.rb index 9fa440935..d30ef1fe9 100644 --- a/app/controllers/api/v1/instances/peers_controller.rb +++ b/app/controllers/api/v1/instances/peers_controller.rb @@ -4,7 +4,7 @@ class Api::V1::Instances::PeersController < Api::BaseController before_action :require_enabled_api! skip_before_action :set_cache_headers - skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + skip_before_action :require_authenticated_user! #, unless: :whitelist_mode? def index expires_in 1.day, public: true @@ -14,6 +14,6 @@ class Api::V1::Instances::PeersController < Api::BaseController private def require_enabled_api! - head 404 unless Setting.peers_api_enabled && !whitelist_mode? + head 404 unless Setting.peers_api_enabled #&& !whitelist_mode? end end diff --git a/app/controllers/api/v1/instances_controller.rb b/app/controllers/api/v1/instances_controller.rb index 5b5058a7b..844bab68a 100644 --- a/app/controllers/api/v1/instances_controller.rb +++ b/app/controllers/api/v1/instances_controller.rb @@ -2,7 +2,7 @@ class Api::V1::InstancesController < Api::BaseController skip_before_action :set_cache_headers - skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + skip_before_action :require_authenticated_user! #, unless: :whitelist_mode? def show expires_in 3.minutes, public: true diff --git a/app/controllers/api/v1/polls/votes_controller.rb b/app/controllers/api/v1/polls/votes_controller.rb index 513b937ef..91ca96ef0 100644 --- a/app/controllers/api/v1/polls/votes_controller.rb +++ b/app/controllers/api/v1/polls/votes_controller.rb @@ -17,6 +17,7 @@ class Api::V1::Polls::VotesController < Api::BaseController def set_poll @poll = Poll.attached.find(params[:poll_id]) authorize @poll.status, :show? + authorize @poll.status.reblog, :show? if @poll.status.reblog? rescue Mastodon::NotPermittedError not_found end diff --git a/app/controllers/api/v1/polls_controller.rb b/app/controllers/api/v1/polls_controller.rb index 6435e9f0d..75f5a9f08 100644 --- a/app/controllers/api/v1/polls_controller.rb +++ b/app/controllers/api/v1/polls_controller.rb @@ -16,6 +16,7 @@ class Api::V1::PollsController < Api::BaseController def set_poll @poll = Poll.attached.find(params[:id]) authorize @poll.status, :show? + authorize @poll.status.reblog, :show? if @poll.status.reblog? rescue Mastodon::NotPermittedError not_found end diff --git a/app/controllers/api/v1/statuses/hides_controller.rb b/app/controllers/api/v1/statuses/hides_controller.rb new file mode 100644 index 000000000..8c5457c82 --- /dev/null +++ b/app/controllers/api/v1/statuses/hides_controller.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class Api::V1::Statuses::HidesController < Api::BaseController + include Authorization + + before_action -> { doorkeeper_authorize! :write, :'write:mutes' } + before_action :require_user! + before_action :set_status + + def create + MuteStatusService.new.call(current_account, @status) + render json: @status, serializer: REST::StatusSerializer + end + + def destroy + current_account.unmute_status!(@status) + render json: @status, serializer: REST::StatusSerializer + end + + private + + def set_status + @status = Status.find(params[:status_id]) + authorize @status, :show? + rescue Mastodon::NotPermittedError + not_found + end +end diff --git a/app/controllers/api/v1/statuses/mutes_controller.rb b/app/controllers/api/v1/statuses/mutes_controller.rb index 87071a2b9..73d9df734 100644 --- a/app/controllers/api/v1/statuses/mutes_controller.rb +++ b/app/controllers/api/v1/statuses/mutes_controller.rb @@ -9,12 +9,14 @@ class Api::V1::Statuses::MutesController < Api::BaseController before_action :set_conversation def create - current_account.mute_conversation!(@conversation) + MuteConversationService.new.call(current_account, @status.conversation, hidden: truthy_param?(:hide)) @mutes_map = { @conversation.id => true } render json: @status, serializer: REST::StatusSerializer end + alias update create + def destroy current_account.unmute_conversation!(@conversation) @mutes_map = { @conversation.id => false } diff --git a/app/controllers/api/v1/statuses/pins_controller.rb b/app/controllers/api/v1/statuses/pins_controller.rb index 51b1621b6..187b6145c 100644 --- a/app/controllers/api/v1/statuses/pins_controller.rb +++ b/app/controllers/api/v1/statuses/pins_controller.rb @@ -9,7 +9,7 @@ class Api::V1::Statuses::PinsController < Api::BaseController def create StatusPin.create!(account: current_account, status: @status) - distribute_add_activity! + distribute_add_activity! unless @status.semiprivate? render json: @status, serializer: REST::StatusSerializer end diff --git a/app/controllers/api/v1/statuses/publishing_controller.rb b/app/controllers/api/v1/statuses/publishing_controller.rb new file mode 100644 index 000000000..97c052e22 --- /dev/null +++ b/app/controllers/api/v1/statuses/publishing_controller.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class Api::V1::Statuses::PublishingController < Api::BaseController + include Authorization + + before_action -> { doorkeeper_authorize! :write, :'write:statuses:publish' } + before_action :require_user! + before_action :set_status + + def create + PublishStatusService.new.call(@status) + + render json: @status, + serializer: (@status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer), + source_requested: truthy_param?(:source) + end + + private + + def set_status + @status = Status.unpublished.find(params[:status_id]) + authorize @status, :destroy? + rescue Mastodon::NotPermittedError + not_found + end +end diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index c8529318f..cbd232a50 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -19,7 +19,7 @@ class Api::V1::StatusesController < Api::BaseController def show @status = cache_collection([@status], Status).first - render json: @status, serializer: REST::StatusSerializer + render json: @status, serializer: REST::StatusSerializer, source_requested: truthy_param?(:source) end def context @@ -31,7 +31,7 @@ class Api::V1::StatusesController < Api::BaseController @context = Context.new(ancestors: loaded_ancestors, descendants: loaded_descendants) statuses = [@status] + @context.ancestors + @context.descendants - render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id) + render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id), current_account_id: current_user&.account_id end def create @@ -41,24 +41,80 @@ class Api::V1::StatusesController < Api::BaseController media_ids: status_params[:media_ids], sensitive: status_params[:sensitive], spoiler_text: status_params[:spoiler_text], + title: status_params[:title], + footer: status_params[:footer], + notify: status_params[:notify], + publish: status_params[:publish], visibility: status_params[:visibility], scheduled_at: status_params[:scheduled_at], application: doorkeeper_token.application, poll: status_params[:poll], content_type: status_params[:content_type], + tags: parse_tags_param(status_params[:tags]), + mentions: parse_mentions_param(status_params[:mentions]), idempotency: request.headers['Idempotency-Key'], - with_rate_limit: true) + with_rate_limit: true, + expires_at: status_params[:expires_at], + publish_at: status_params[:publish_at]) - render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer + render json: @status, + serializer: (@status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer), + source_requested: truthy_param?(:source) + end + + def update + @status = Status.where(account_id: current_user.account).find(params[:id]) + authorize @status, :destroy? + + @status = PostStatusService.new.call(current_user.account, + text: status_params[:status], + thread: @thread, + media_ids: status_params[:media_ids], + sensitive: status_params[:sensitive], + spoiler_text: status_params[:spoiler_text], + title: status_params[:title], + footer: status_params[:footer], + notify: status_params[:notify], + publish: status_params[:publish], + visibility: status_params[:visibility], + scheduled_at: status_params[:scheduled_at], + application: doorkeeper_token.application, + poll: status_params[:poll], + content_type: status_params[:content_type], + status: @status, + tags: parse_tags_param(status_params[:tags]), + mentions: parse_mentions_param(status_params[:mentions]), + idempotency: request.headers['Idempotency-Key'], + with_rate_limit: true, + expires_at: status_params[:expires_at], + publish_at: status_params[:publish_at]) + + render json: @status, + serializer: (@status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer), + source_requested: truthy_param?(:source) end def destroy @status = Status.where(account_id: current_user.account).find(params[:id]) authorize @status, :destroy? - @status.discard - RemovalWorker.perform_async(@status.id, redraft: true) - @status.account.statuses_count = @status.account.statuses_count - 1 + if !(current_user.setting_unpublish_on_delete && @status.published?) || truthy_param?(:redraft) + @status.discard + RemovalWorker.perform_async(@status.id, redraft: true) + @status.account.statuses_count = @status.account.statuses_count - 1 + else + RemovalWorker.perform_async(@status.id, redraft: true, unpublish: true) + tag_script = "#!redraft #{@status.id}\n" + @status.text = "#{tag_script}#{@status.text.sub(/^\s*#!redraft \d+\n/, '')}" + @status.original_text = "#{tag_script}#{@status.original_text.sub(/^\s*#!redraft \d+\n/, '')}" + end + + @status.local_only = @status.originally_local_only? + unless @status.original_text.match?(/^\s*#!\s*federate\b/i) + tag_script = "#!federate #{@status.originally_local_only? ? 'off' : 'on'}\n" + @status.text.prepend(tag_script) + @status.original_text.prepend(tag_script) + end render json: @status, serializer: REST::StatusSerializer, source_requested: true end @@ -84,9 +140,17 @@ class Api::V1::StatusesController < Api::BaseController :in_reply_to_id, :sensitive, :spoiler_text, + :title, + :footer, + :notify, + :publish, :visibility, :scheduled_at, :content_type, + :expires_at, + :publish_at, + tags: [], + mentions: [], media_ids: [], poll: [ :multiple, @@ -100,4 +164,26 @@ class Api::V1::StatusesController < Api::BaseController def pagination_params(core_params) params.slice(:limit).permit(:limit).merge(core_params) end + + def parse_tags_param(tags_param) + return if tags_param.blank? + + tags_param.select { |value| value.respond_to?(:to_str) && value.present? } + end + + def parse_mentions_param(mentions_param) + return if mentions_param.blank? + + mentions_param.map do |value| + next if value.blank? + + value = value.split('@', 3) if value.respond_to?(:to_str) + next unless value.is_a?(Enumerable) + + mentioned_account = Account.find_by(username: value[0], domain: value[1]) + next if mentioned_account.nil? || mentioned_account.suspended? + + mentioned_account + end + end end diff --git a/app/controllers/api/v1/timelines/public_controller.rb b/app/controllers/api/v1/timelines/public_controller.rb index 52b5cb323..b53f7750f 100644 --- a/app/controllers/api/v1/timelines/public_controller.rb +++ b/app/controllers/api/v1/timelines/public_controller.rb @@ -41,7 +41,8 @@ class Api::V1::Timelines::PublicController < Api::BaseController end def public_timeline_statuses - Status.as_public_timeline(current_account, truthy_param?(:remote) ? :remote : truthy_param?(:local)) + local = truthy_param?(:local) ? true : :local_reblogs + Status.as_public_timeline(current_account, truthy_param?(:remote) ? :remote : local) end def insert_pagination_headers diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index e996c2217..8154924b9 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -12,6 +12,7 @@ class ApplicationController < ActionController::Base include SessionTrackingConcern include CacheConcern include DomainControlHelper + include SignatureVerification helper_method :current_account helper_method :current_session @@ -48,7 +49,7 @@ class ApplicationController < ActionController::Base end def authorized_fetch_mode? - ENV['AUTHORIZED_FETCH'] == 'true' || Rails.configuration.x.whitelist_mode + !(Rails.env.development? || Rails.env.test?) end def public_fetch_mode? @@ -68,7 +69,29 @@ class ApplicationController < ActionController::Base end def require_functional! - redirect_to edit_user_registration_path unless current_user.functional? + redirect_to edit_user_registration_path unless current_user&.functional? + end + + def require_authenticated! + return if current_account? + + respond_to do |format| + format.any { redirect_to edit_user_registration_path } + format.json { forbidden } + end + end + + def require_known!(account) + return if authenticated_or_following?(account) + + respond_to do |format| + format.any { redirect_to edit_user_registration_path } + format.json { forbidden } + end + end + + def require_following!(account) + forbidden unless following?(account) end def after_sign_out_path_for(_resource_or_scope) @@ -197,7 +220,7 @@ class ApplicationController < ActionController::Base def current_account return @current_account if defined?(@current_account) - @current_account = current_user&.account + @current_account = current_user&.account.presence || signed_request_account end def current_session @@ -225,4 +248,21 @@ class ApplicationController < ActionController::Base format.json { render json: { error: Rack::Utils::HTTP_STATUS_CODES[code] }, status: code } end end + + def following?(account) + return if account.blank? + + @account_following ||= {} + return @account_following[account.id] if @account_following[account.id].present? + + @account_following[account.id] = current_account.present? && (current_account.id == account.id || current_account.following?(account)) + end + + def authenticated_or_following?(account) + current_user&.account.present? || following?(account) + end + + def current_account? + current_account.present? + end end diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index 96d973394..55975b274 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -35,6 +35,12 @@ class Auth::RegistrationsController < Devise::RegistrationsController end end + def create + super do |resource| + return redirect_to root_path if resource.destroyed? + end + end + protected def update_resource(resource, params) @@ -55,7 +61,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController def configure_sign_up_params devise_parameter_sanitizer.permit(:sign_up) do |u| - u.permit({ account_attributes: [:username], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code, :agreement) + u.permit({ account_attributes: [:username], invite_request_attributes: [:text] }, :username, :email, :password, :password_confirmation, :kobold, :invite_code, :agreement) end end diff --git a/app/controllers/concerns/account_owned_concern.rb b/app/controllers/concerns/account_owned_concern.rb index 460f71f65..65168efff 100644 --- a/app/controllers/concerns/account_owned_concern.rb +++ b/app/controllers/concerns/account_owned_concern.rb @@ -4,7 +4,7 @@ module AccountOwnedConcern extend ActiveSupport::Concern included do - before_action :authenticate_user!, if: -> { whitelist_mode? && request.format != :json } + #before_action :authenticate_user!, if: -> { whitelist_mode? && request.format != :json } before_action :set_account, if: :account_required? before_action :check_account_approval, if: :account_required? before_action :check_account_suspension, if: :account_required? diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index c9b840881..d15adbf62 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -47,7 +47,7 @@ class HomeController < ApplicationController end def default_redirect_path - if request.path.start_with?('/web') || whitelist_mode? + if request.path.start_with?('/web') #|| whitelist_mode? new_user_session_path elsif single_user_mode? short_account_path(Account.local.without_suspended.where('id > 0').first) diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb index 772fc42cb..db8ccd173 100644 --- a/app/controllers/media_controller.rb +++ b/app/controllers/media_controller.rb @@ -4,9 +4,9 @@ class MediaController < ApplicationController include Authorization skip_before_action :store_current_location - skip_before_action :require_functional!, unless: :whitelist_mode? + skip_before_action :require_functional! #, unless: :whitelist_mode? - before_action :authenticate_user!, if: :whitelist_mode? + #before_action :authenticate_user!, if: :whitelist_mode? before_action :set_media_attachment before_action :verify_permitted_status! before_action :check_playable, only: :player @@ -33,6 +33,7 @@ class MediaController < ApplicationController def verify_permitted_status! authorize @media_attachment.status, :show? + authorize @media_attachment.status.reblog, :show? if @media_attachment.status.reblog? rescue Mastodon::NotPermittedError not_found end diff --git a/app/controllers/media_proxy_controller.rb b/app/controllers/media_proxy_controller.rb index 0b1d09de9..ee7568a33 100644 --- a/app/controllers/media_proxy_controller.rb +++ b/app/controllers/media_proxy_controller.rb @@ -7,7 +7,7 @@ class MediaProxyController < ApplicationController skip_before_action :store_current_location skip_before_action :require_functional! - before_action :authenticate_user!, if: :whitelist_mode? + #before_action :authenticate_user!, if: :whitelist_mode? rescue_from ActiveRecord::RecordInvalid, with: :not_found rescue_from Mastodon::UnexpectedResponseError, with: :not_found @@ -19,6 +19,7 @@ class MediaProxyController < ApplicationController if lock.acquired? @media_attachment = MediaAttachment.remote.attached.find(params[:id]) authorize @media_attachment.status, :show? + authorize @media_attachment.status.reblog, :show? if @media_attachment.status.reblog? redownload! if @media_attachment.needs_redownload? && !reject_media? else raise Mastodon::RaceConditionError diff --git a/app/controllers/remote_interaction_controller.rb b/app/controllers/remote_interaction_controller.rb index a277bfa10..5ead3aaa0 100644 --- a/app/controllers/remote_interaction_controller.rb +++ b/app/controllers/remote_interaction_controller.rb @@ -5,13 +5,13 @@ class RemoteInteractionController < ApplicationController layout 'modal' - before_action :authenticate_user!, if: :whitelist_mode? + #before_action :authenticate_user!, if: :whitelist_mode? before_action :set_interaction_type before_action :set_status before_action :set_body_classes before_action :set_pack - skip_before_action :require_functional!, unless: :whitelist_mode? + skip_before_action :require_functional! #, unless: :whitelist_mode? def new @remote_follow = RemoteFollow.new(session_params) diff --git a/app/controllers/settings/preferences/filters_controller.rb b/app/controllers/settings/preferences/filters_controller.rb new file mode 100644 index 000000000..c58a698ef --- /dev/null +++ b/app/controllers/settings/preferences/filters_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Settings::Preferences::FiltersController < Settings::PreferencesController + private + + def after_update_redirect_path + settings_preferences_filters_path + end +end diff --git a/app/controllers/settings/preferences/publishing_controller.rb b/app/controllers/settings/preferences/publishing_controller.rb new file mode 100644 index 000000000..5b298d94d --- /dev/null +++ b/app/controllers/settings/preferences/publishing_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Settings::Preferences::PublishingController < Settings::PreferencesController + private + + def after_update_redirect_path + settings_preferences_publishing_path + end +end diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index 75c3e2495..ddbf89665 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -11,6 +11,7 @@ class Settings::PreferencesController < Settings::BaseController user_settings.update(user_settings_params.to_h) if current_user.update(user_params) + Rails.cache.delete("filter_settings:#{current_user.account_id}") I18n.locale = current_user.locale redirect_to after_update_redirect_path, notice: I18n.t('generic.changes_saved_msg') else @@ -61,6 +62,21 @@ class Settings::PreferencesController < Settings::BaseController :setting_use_pending_items, :setting_trends, :setting_crop_images, + :setting_manual_publish, + :setting_style_dashed_nest, + :setting_style_underline_a, + :setting_style_css_profile, + :setting_style_css_webapp, + :setting_style_wide_media, + :setting_publish_in, + :setting_unpublish_in, + :setting_unpublish_delete, + :setting_boost_every, + :setting_boost_jitter, + :setting_boost_random, + :setting_filter_to_unknown, + :setting_filter_from_unknown, + :setting_unpublish_on_delete, notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account trending_tag), interactions: %i(must_be_follower must_be_following must_be_following_dm) ) diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb index 19a7ce157..8c4efa21d 100644 --- a/app/controllers/settings/profiles_controller.rb +++ b/app/controllers/settings/profiles_controller.rb @@ -23,7 +23,9 @@ class Settings::ProfilesController < Settings::BaseController private def account_params - params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, :bot, :discoverable, fields_attributes: [:name, :value]) + params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, :bot, :discoverable, + :require_dereference, :show_replies, :show_unlisted, :private, :require_auth, + fields_attributes: [:name, :value]) end def set_account diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index a6ab8828f..6f8e74414 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -8,7 +8,9 @@ class StatusesController < ApplicationController layout 'public' - before_action :require_signature!, only: :show, if: -> { request.format == :json && authorized_fetch_mode? } + before_action :require_signature!, only: :show, if: -> { request.format == :json && authorized_fetch_mode? && current_user&.account_id != @account.id } + before_action :require_authenticated!, if: -> { @account.require_auth? } + before_action -> { require_following!(@account) }, if: -> { request.format != :json && @account.private? } before_action :set_status before_action :set_instance_presenter before_action :set_link_headers @@ -19,7 +21,7 @@ class StatusesController < ApplicationController before_action :set_autoplay, only: :embed skip_around_action :set_locale, if: -> { request.format == :json } - skip_before_action :require_functional!, only: [:show, :embed], unless: :whitelist_mode? + skip_before_action :require_functional!, only: [:show, :embed] # , unless: :whitelist_mode? content_security_policy only: :embed do |p| p.frame_ancestors(false) @@ -37,14 +39,18 @@ class StatusesController < ApplicationController format.json do expires_in 3.minutes, public: @status.distributable? && public_fetch_mode? - render_with_cache json: @status, content_type: 'application/activity+json', serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter + render_with_cache json: @status, content_type: 'application/activity+json', serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter, target_domain: current_account&.domain end end end def activity expires_in 3.minutes, public: @status.distributable? && public_fetch_mode? - render_with_cache json: ActivityPub::ActivityPresenter.from_status(@status), content_type: 'application/activity+json', serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter + render_with_cache json: ActivityPub::ActivityPresenter.from_status(@status), + content_type: 'application/activity+json', + serializer: ActivityPub::ActivitySerializer, + adapter: ActivityPub::Adapter, + target_domain: current_account&.domain end def embed diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 69db89eb3..368419ef5 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -9,13 +9,13 @@ class TagsController < ApplicationController layout 'public' before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? } - before_action :authenticate_user!, if: :whitelist_mode? + # before_action :authenticate_user!, if: :whitelist_mode? before_action :set_tag before_action :set_local before_action :set_body_classes before_action :set_instance_presenter - skip_before_action :require_functional!, unless: :whitelist_mode? + skip_before_action :require_functional! # , unless: :whitelist_mode? def show respond_to do |format| @@ -37,10 +37,12 @@ class TagsController < ApplicationController format.json do expires_in 3.minutes, public: public_fetch_mode? - @statuses = HashtagQueryService.new.call(@tag, filter_params, current_account, @local).paginate_by_max_id(PAGE_SIZE, params[:max_id]) + @statuses = HashtagQueryService.new.call(@tag, filter_params, current_account, @local) + @statuses = @statuses.without_semiprivate unless authenticated_or_following?(@account) + @statuses = @statuses.paginate_by_max_id(PAGE_SIZE, params[:max_id]) @statuses = cache_collection(@statuses, Status) - render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' + render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json', target_domain: current_account&.domain end end end diff --git a/app/controllers/user_profile_css_controller.rb b/app/controllers/user_profile_css_controller.rb new file mode 100644 index 000000000..0a0588e88 --- /dev/null +++ b/app/controllers/user_profile_css_controller.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class UserProfileCssController < ApplicationController + skip_before_action :store_current_location + skip_before_action :require_functional! + + before_action :set_cache_headers + before_action :set_account + + def show + expires_in 3.minutes, public: true + render plain: css, content_type: 'text/css' + end + + private + + def css + @account.user&.setting_style_css_profile_errors.blank? ? (@account.user&.setting_style_css_profile || '') : '' + end + + def set_account + @account = Account.find(params[:id]) + end +end diff --git a/app/controllers/user_webapp_css_controller.rb b/app/controllers/user_webapp_css_controller.rb new file mode 100644 index 000000000..b2baa2843 --- /dev/null +++ b/app/controllers/user_webapp_css_controller.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +class UserWebappCssController < ApplicationController + skip_before_action :store_current_location + skip_before_action :require_functional! + + before_action :set_cache_headers + before_action :set_account + + def show + expires_in 3.minutes, public: false + render plain: css, content_type: 'text/css' + end + + private + + def css_dashed_nest + return unless @account.user&.setting_style_dashed_nest + + %( + div[data-nest-level] + { border-style: dashed; } + ) + end + + def css_underline_a + return unless @account.user&.setting_style_underline_a + + %( + .status__content__text a, + .reply-indicator__content a, + .composer--reply > .content a, + .account__header__content a + { text-decoration: underline; } + + .status__content__text a:hover, + .reply-indicator__content a:hover, + .composer--reply > .content a:hover, + .account__header__content a:hover + { text-decoration: none; } + ) + end + + def css_wide_media + return unless @account.user&.setting_style_wide_media + + %( + .media-gallery + { height: auto !important; } + + .media-gallery__item + { width: 100% !important; } + + .spoiler-button + .media-gallery__item + { height: 5em !important; } + + .spoiler-button--minified + .media-gallery__item + { height: 280px !important; } + ) + end + + def css_webapp + @account.user&.setting_style_css_webapp_errors.blank? ? (@account.user&.setting_style_css_webapp || '') : '' + end + + def css + "#{css_dashed_nest}\n#{css_underline_a}\n#{css_wide_media}\n#{css_webapp}".squish + end + + def set_account + @account = Account.find(params[:id]) + end +end diff --git a/app/helpers/domain_control_helper.rb b/app/helpers/domain_control_helper.rb index ac60cad29..765ffa536 100644 --- a/app/helpers/domain_control_helper.rb +++ b/app/helpers/domain_control_helper.rb @@ -20,6 +20,6 @@ module DomainControlHelper end def whitelist_mode? - Rails.configuration.x.whitelist_mode + !(Rails.env.development? || Rails.env.test?) end end diff --git a/app/helpers/img_proxy_helper.rb b/app/helpers/img_proxy_helper.rb new file mode 100644 index 000000000..5ea4bcd93 --- /dev/null +++ b/app/helpers/img_proxy_helper.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +# .~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~. # +################### Cthulhu Code! ################### +# `~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~` # +# - Has a high complexity level and needs tests. # +# - Makes many assumptions the environment it's included into. # +# - Incurs a high performance penalty. # +# # +############################################################################### + +module ImgProxyHelper + def process_inline_images! + raise NameError('@status must be defined by the instance this method is being called from.') unless defined?(@status) + return if @status.text&.strip.blank? || @status.content_type == 'text/plain' + + replace_markdown_images_with_html! + + handler = ImgTagHandler.new + Ox.sax_parse(handler, StringIO.new(@status.text, 'r')) + return if handler.srcs.blank? + + @skip_download_from = { @status.account.domain => DomainBlock.reject_media?(@status.account.domain) } + @redownload_attachment_ids = Set[] + + handler.srcs.each do |src| + alt = handler.alts[src] + normalized_src_parts = begin + Addressable::URI.parse(src&.strip).normalize + rescue Addressable::URI::InvalidURIError + nil + end + normalized_src = normalized_src_parts.to_s + + next replace_text!(src) if normalized_src.blank? || skip_download_from?(normalized_src_parts.host) + + file_name = normalized_src_parts.path.split('/').last + media_attachment = find_media_attachment(normalized_src, file_name) + + if media_attachment.present? + media_attachment.update(description: alt) if alt_more_descriptive?(alt, media_attachment.description) + elsif normalized_src_parts.scheme.blank? || !file_name.match?(/\S\.\w{3,}/) + next replace_text!(src) + else + media_attachment = create_media_attachment!(normalized_src, alt) + end + + next replace_text!(src) if media_attachment.blank? || media_attachment.destroyed? + + if media_attachment.needs_redownload? + replace_text!(src, "#{media_attachment.file.url(:small)}##{media_attachment.id}") + else + replace_text!(src, media_attachment.file.url(:small)) + end + end + end + + private + + def skip_download_from?(domain) + return true if @skip_download_from[@status.account.domain] + return @skip_download_from[domain] if @skip_download_from[domain] + + @skip_download_from[domain] = DomainBlock.reject_media?(domain) + end + + def unsupported_media_type?(mime_type) + mime_type.present? && !MediaAttachment.supported_mime_types.include?(mime_type) + end + + def html_entities + @html_entities ||= HTMLEntities.new + end + + def replace_markdown_images_with_html! + return unless @status.content_type == 'text/markdown' + + @status.text.gsub!(/!\[(\S+)\]\(\s*(\S+)\s*\)/) do + begin + alt = html_entities.encode(Regexp.last_match(1).strip) + url = Addressable::URI.parse(Regexp.last_match(2)).normalize.to_s + "\"#{alt}\"" + rescue Addressable::URI::InvalidURIError + '' + end + end + end + + def replace_text!(text, replacement = '') + @status.text.gsub!(text, replacement) + end + + def alt_more_descriptive?(alt, description) + return false unless alt.present? && description != alt + return true if description.blank? || alt.split(/[\s\n\r]+/).count > description.split(/[\s\n\r]+/).count + end + + def find_media_attachment(src, file_name) + media_attachment = src.start_with?('http') ? MediaAttachment.find_by(account: @account, remote_url: src, inline: true) : nil + return media_attachment if media_attachment.present? + + MediaAttachment.where(account: @status.account, file_file_name: file_name, inline: true) + .find { |m| [m.file.url(:small), m.file.url(:original)].include?(src) || m.status_id == @status.id } + end + + def create_media_attachment!(src, alt) + media_attachment = MediaAttachment.create!(account: @status.account, remote_url: src, description: alt, focus: nil, inline: true) + media_attachment = process_media_attachment!(media_attachment) + return if media_attachment.destroyed? + + @status.inlined_attachments.first_or_create!(media_attachment: media_attachment) + media_attachment + end + + def process_media_attachment!(media_attachment) + media_attachment.download_file! + media_attachment.download_thumbnail! + media_attachment.save! + media_attachment.destroy! if unsupported_media_type?(media_attachment.file.content_type) + media_attachment + rescue Mastodon::UnexpectedResponseError, HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError + return if @redownload_attachment_ids.include?(media_attachment.id) + + RedownloadMediaWorker.perform_in(rand(30..60).seconds, media_attachment.id) + @redownload_attachment_ids << media_attachment.id + media_attachment + end +end diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb index 1c473efa3..b93284637 100644 --- a/app/helpers/jsonld_helper.rb +++ b/app/helpers/jsonld_helper.rb @@ -76,9 +76,31 @@ module JsonLdHelper json.present? && json['id'] == uri ? json : nil end + def uri_allowed?(uri) + host = Addressable::URI.parse(uri)&.normalized_host + Rails.cache.fetch("fetch_resource:#{host}", expires_in: 1.hour) { DomainAllow.allowed?(host) } + rescue Addressable::URI::InvalidURIError + false + end + def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false) + return unless uri_allowed?(uri) + on_behalf_of ||= Account.representative + skip_retry = on_behalf_of.id == -99 || Rails.env.development? + begin + fetch_body(uri, on_behalf_of, !skip_retry || raise_on_temporary_error) + rescue Mastodon::UnexpectedResponseError + raise if skip_retry + + fetch_body(uri, Account.representative, raise_on_temporary_error) + end + rescue Addressable::URI::InvalidURIError + nil + end + + def fetch_body(uri, on_behalf_of, raise_on_temporary_error = false) build_request(uri, on_behalf_of).perform do |response| raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error @@ -87,6 +109,9 @@ module JsonLdHelper end def body_to_json(body, compare_id: nil) + body.strip! if body.is_a?(String) + return if body.blank? + json = body.is_a?(String) ? Oj.load(body, mode: :strict) : body return if compare_id.present? && json['id'] != compare_id @@ -114,7 +139,7 @@ module JsonLdHelper def build_request(uri, on_behalf_of = nil) Request.new(:get, uri).tap do |request| - request.on_behalf_of(on_behalf_of) if on_behalf_of + request.on_behalf_of(on_behalf_of) unless Rails.env.development? || on_behalf_of.blank? request.add_headers('Accept' => 'application/activity+json, application/ld+json') end end diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 87718dc05..bb98a71a5 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -2,6 +2,7 @@ module SettingsHelper HUMAN_LOCALES = { + 'en-MP': 'English (Monsterpit)', ar: 'العربية', ast: 'Asturianu', bg: 'Български', diff --git a/app/javascript/flavours/glitch/actions/accounts.js b/app/javascript/flavours/glitch/actions/accounts.js index e1012a80b..32e533bd0 100644 --- a/app/javascript/flavours/glitch/actions/accounts.js +++ b/app/javascript/flavours/glitch/actions/accounts.js @@ -264,11 +264,11 @@ export function unblockAccountFail(error) { }; -export function muteAccount(id, notifications) { +export function muteAccount(id, notifications, timelinesOnly) { return (dispatch, getState) => { dispatch(muteAccountRequest(id)); - api(getState).post(`/api/v1/accounts/${id}/mute`, { notifications }).then(response => { + api(getState).post(`/api/v1/accounts/${id}/mute`, { notifications, timelinesOnly }).then(response => { // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers dispatch(muteAccountSuccess(response.data, getState().get('statuses'))); }).catch(error => { diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index f83738093..4c2cca9eb 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -147,6 +147,9 @@ export function submitCompose(routerHistory) { let media = getState().getIn(['compose', 'media_attachments']); const spoilers = getState().getIn(['compose', 'spoiler']) || getState().getIn(['local_settings', 'always_show_spoilers_field']); let spoilerText = spoilers ? getState().getIn(['compose', 'spoiler_text'], '') : ''; + const id = getState().getIn(['compose', 'id'], null); + const submit_url = id ? `/api/v1/statuses/${id}` : '/api/v1/statuses'; + const submit_action = (res, body, config) => id ? api(getState).put(res, body, config) : api(getState).post(res, body, config); if ((!status || !status.length) && media.size === 0) { return; @@ -156,7 +159,7 @@ export function submitCompose(routerHistory) { if (getState().getIn(['compose', 'advanced_options', 'do_not_federate'])) { status = status + ' 👁️'; } - api(getState).post('/api/v1/statuses', { + submit_action(submit_url, { status, content_type: getState().getIn(['compose', 'content_type']), in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null), diff --git a/app/javascript/flavours/glitch/actions/importer/normalizer.js b/app/javascript/flavours/glitch/actions/importer/normalizer.js index 05955963c..729c8d700 100644 --- a/app/javascript/flavours/glitch/actions/importer/normalizer.js +++ b/app/javascript/flavours/glitch/actions/importer/normalizer.js @@ -12,7 +12,7 @@ const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => { export function searchTextFromRawStatus (status) { const spoilerText = status.spoiler_text || ''; - const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(//g, '\n').replace(/<\/p>

/g, '\n\n'); + const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).concat(status.tags ? status.tags.map(tag => tag.name) : []).join('\n\n').replace(//g, '\n').replace(/<\/p>

/g, '\n\n'); return domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; } @@ -53,11 +53,15 @@ export function normalizeStatus(status, normalOldStatus) { normalStatus.poll = status.poll.id; } + const oldUpdatedAt = normalOldStatus ? normalOldStatus.updated_at || normalOldStatus.created_at : null; + const newUpdatedAt = normalStatus ? normalStatus.updated_at || normalStatus.created_at : null; + // Only calculate these values when status first encountered // Otherwise keep the ones already in the reducer - if (normalOldStatus) { + if (normalOldStatus && oldUpdatedAt === newUpdatedAt) { normalStatus.search_index = normalOldStatus.get('search_index'); normalStatus.contentHtml = normalOldStatus.get('contentHtml'); + normalStatus.articleHtml = normalOldStatus.get('articleHtml'); normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml'); } else { const spoilerText = normalStatus.spoiler_text || ''; @@ -66,6 +70,7 @@ export function normalizeStatus(status, normalOldStatus) { normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; normalStatus.contentHtml = emojify(normalStatus.content, emojiMap); + normalStatus.articleHtml = normalStatus.article_content ? emojify(normalStatus.article_content, emojiMap) : normalStatus.contentHtml; normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap); } diff --git a/app/javascript/flavours/glitch/actions/mutes.js b/app/javascript/flavours/glitch/actions/mutes.js index 927fc7415..645261627 100644 --- a/app/javascript/flavours/glitch/actions/mutes.js +++ b/app/javascript/flavours/glitch/actions/mutes.js @@ -13,6 +13,7 @@ export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL'; export const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL'; export const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS'; +export const MUTES_TOGGLE_TIMELINES_ONLY = 'MUTES_TOGGLE_TIMELINES_ONLY'; export function fetchMutes() { return (dispatch, getState) => { @@ -104,3 +105,9 @@ export function toggleHideNotifications() { dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS }); }; } + +export function toggleTimelinesOnly() { + return dispatch => { + dispatch({ type: MUTES_TOGGLE_TIMELINES_ONLY }); + }; +} diff --git a/app/javascript/flavours/glitch/actions/statuses.js b/app/javascript/flavours/glitch/actions/statuses.js index 4d2bda78b..018641fc7 100644 --- a/app/javascript/flavours/glitch/actions/statuses.js +++ b/app/javascript/flavours/glitch/actions/statuses.js @@ -12,6 +12,10 @@ export const STATUS_DELETE_REQUEST = 'STATUS_DELETE_REQUEST'; export const STATUS_DELETE_SUCCESS = 'STATUS_DELETE_SUCCESS'; export const STATUS_DELETE_FAIL = 'STATUS_DELETE_FAIL'; +export const STATUS_PUBLISH_REQUEST = 'STATUS_PUBLISH_REQUEST'; +export const STATUS_PUBLISH_SUCCESS = 'STATUS_PUBLISH_SUCCESS'; +export const STATUS_PUBLISH_FAIL = 'STATUS_PUBLISH_FAIL'; + export const CONTEXT_FETCH_REQUEST = 'CONTEXT_FETCH_REQUEST'; export const CONTEXT_FETCH_SUCCESS = 'CONTEXT_FETCH_SUCCESS'; export const CONTEXT_FETCH_FAIL = 'CONTEXT_FETCH_FAIL'; @@ -34,9 +38,9 @@ export function fetchStatusRequest(id, skipLoading) { }; }; -export function fetchStatus(id) { +export function fetchStatus(id, skipLoading = null) { return (dispatch, getState) => { - const skipLoading = getState().getIn(['statuses', id], null) !== null; + skipLoading = skipLoading === null ? getState().getIn(['statuses', id], null) !== null : skipLoading; dispatch(fetchContext(id)); @@ -55,6 +59,59 @@ export function fetchStatus(id) { }; }; +export function editStatus(status, routerHistory) { + return (dispatch, getState) => { + const id = status.get('id'); + + dispatch(fetchContext(id)); + dispatch(fetchStatusRequest(id, false)); + + api(getState).get(`/api/v1/statuses/${id}`, { params: { source: 1 } }).then(response => { + dispatch(importFetchedStatus(response.data)); + dispatch(fetchStatusSuccess(false)); + dispatch(redraft(status, response.data.text, response.data.content_type, true)); + ensureComposeIsVisible(getState, routerHistory); + }).catch(error => { + dispatch(fetchStatusFail(id, error, false)); + }); + }; +}; + +export function publishStatus(id) { + return (dispatch, getState) => { + dispatch(publishStatusRequest(id)); + + api(getState).post(`/api/v1/statuses/${id}/publish`).then(() => { + dispatch(publishStatusSuccess(id)); + dispatch(fetchStatus(id, false)); + }).catch(error => { + dispatch(publishStatusFail(id, error)); + }); + }; +}; + +export function publishStatusRequest(id) { + return { + type: STATUS_PUBLISH_REQUEST, + id: id, + }; +}; + +export function publishStatusSuccess(id) { + return { + type: STATUS_PUBLISH_SUCCESS, + id: id, + }; +}; + +export function publishStatusFail(id, error) { + return { + type: STATUS_PUBLISH_FAIL, + id: id, + error: error, + }; +}; + export function fetchStatusSuccess(skipLoading) { return { type: STATUS_FETCH_SUCCESS, @@ -72,12 +129,13 @@ export function fetchStatusFail(id, error, skipLoading) { }; }; -export function redraft(status, raw_text, content_type) { +export function redraft(status, raw_text, content_type, inplace = false) { return { type: REDRAFT, status, raw_text, content_type, + inplace, }; }; @@ -91,7 +149,7 @@ export function deleteStatus(id, routerHistory, withRedraft = false) { dispatch(deleteStatusRequest(id)); - api(getState).delete(`/api/v1/statuses/${id}`).then(response => { + api(getState).delete(`/api/v1/statuses/${id}`, { params: { redraft: withRedraft?1:0 } } ).then(response => { dispatch(deleteStatusSuccess(id)); dispatch(deleteFromTimelines(id)); @@ -172,12 +230,16 @@ export function fetchContextFail(id, error) { }; }; -export function muteStatus(id) { +export function muteStatus(id, hide = false) { return (dispatch, getState) => { dispatch(muteStatusRequest(id)); - api(getState).post(`/api/v1/statuses/${id}/mute`).then(() => { + api(getState).post(`/api/v1/statuses/${id}/mute`, { params: { hide: hide?1:0 } }).then(() => { dispatch(muteStatusSuccess(id)); + + if (hide) { + dispatch(deleteFromTimelines(id)); + } }).catch(error => { dispatch(muteStatusFail(id, error)); }); diff --git a/app/javascript/flavours/glitch/actions/streaming.js b/app/javascript/flavours/glitch/actions/streaming.js index 35db5dcc9..295896e55 100644 --- a/app/javascript/flavours/glitch/actions/streaming.js +++ b/app/javascript/flavours/glitch/actions/streaming.js @@ -18,6 +18,7 @@ import { } from './announcements'; import { fetchFilters } from './filters'; import { getLocale } from 'mastodon/locales'; +import { resetCompose } from 'flavours/glitch/actions/compose'; const { messages } = getLocale(); @@ -96,6 +97,10 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti case 'announcement.delete': dispatch(deleteAnnouncement(data.payload)); break; + case 'refresh': + dispatch(resetCompose()); + window.location.reload(); + break; } }, }; diff --git a/app/javascript/flavours/glitch/actions/timelines.js b/app/javascript/flavours/glitch/actions/timelines.js index b19666e62..bd79d64f5 100644 --- a/app/javascript/flavours/glitch/actions/timelines.js +++ b/app/javascript/flavours/glitch/actions/timelines.js @@ -133,7 +133,18 @@ export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => ex export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, allow_local_only: !!allowLocalOnly, max_id: maxId, only_media: !!onlyMedia }, done); export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done); export const expandDirectTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done); -export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId }); +export const expandAccountTimeline = (accountId, { maxId, filter } = {}) => { + const path = filter ? filter : ''; + const params = { + include_replies: filter === ':replies', + include_reblogs: filter === ':reblogs', + only_reblogs: filter === ':reblogs', + mentions: filter === ':mentions', + max_id: maxId, + }; + + return expandTimeline(`account:${accountId}${path}`, `/api/v1/accounts/${accountId}/statuses`, params); +}; export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true }); export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 }); export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done); diff --git a/app/javascript/flavours/glitch/components/media_gallery.js b/app/javascript/flavours/glitch/components/media_gallery.js index 96042f07a..1ab9a6adb 100644 --- a/app/javascript/flavours/glitch/components/media_gallery.js +++ b/app/javascript/flavours/glitch/components/media_gallery.js @@ -384,6 +384,66 @@ class MediaGallery extends React.PureComponent { ); } + let parts = {}; + + media.map( + (attachment, i) => { + if (attachment.get('description')) { + if (attachment.get('description') in parts) { + parts[attachment.get('description')].push([i, attachment.get('url'), attachment.get('id')]); + } else { + parts[attachment.get('description')] = [[i, attachment.get('url'), attachment.get('id')]]; + } + } + }, + ); + + let descriptions = Object.entries(parts).map( + part => { + const [desc, idx] = part; + if (idx.length === 1) { + const url = idx[0][1]; + return ( +

+ + + + + + {desc} +

+ ); + } else if (idx.length !== 0) { + const indexes = ( + + { + idx.map((i, c) => { + const url = i[1]; + return ({c === 0 ? ' ' : ', '}#{1+i[0]}); + }) + } + + ); + return ( +

+ + + + {desc} +

+ ); + } else { + return null; + } + }, + ); + + let description_wrapper = visible && ( +
+ {descriptions} +
+ ); + return (
@@ -396,6 +456,7 @@ class MediaGallery extends React.PureComponent {
{children} + {description_wrapper}
); } diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index 4e628a420..69f93a2f1 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -73,6 +73,8 @@ class Status extends ImmutablePureComponent { onReblog: PropTypes.func, onBookmark: PropTypes.func, onDelete: PropTypes.func, + onEdit: PropTypes.func, + onPublish: PropTypes.func, onDirect: PropTypes.func, onMention: PropTypes.func, onPin: PropTypes.func, @@ -368,7 +370,7 @@ class Status extends ImmutablePureComponent { } handleExpandedToggle = () => { - if (this.props.status.get('spoiler_text')) { + if (this.props.status.get('spoiler_text') || this.props.status.get('reblogSpoilerHtml')) { this.setExpansion(!this.state.isExpanded); } }; @@ -672,6 +674,9 @@ class Status extends ImmutablePureComponent { // Users can use those for theming, hiding avatars etc via UserStyle const selectorAttribs = { 'data-status-by': `@${status.getIn(['account', 'acct'])}`, + 'data-nest-level': status.get('nest_level'), + 'data-nest-deep': status.get('nest_level') >= 15, + 'data-local-only': !!status.get('local_only'), }; if (prepend && account) { @@ -692,6 +697,7 @@ class Status extends ImmutablePureComponent { const computedClass = classNames('status', `status-${status.get('visibility')}`, { collapsed: isCollapsed, + unpublished: status.get('published') === false, 'has-background': isCollapsed && background, 'status__wrapper-reply': !!status.get('in_reply_to_id'), read: unread === false, diff --git a/app/javascript/flavours/glitch/components/status_action_bar.js b/app/javascript/flavours/glitch/components/status_action_bar.js index cfb03c21b..0822239f5 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.js +++ b/app/javascript/flavours/glitch/components/status_action_bar.js @@ -13,6 +13,8 @@ import classNames from 'classnames'; const messages = defineMessages({ delete: { id: 'status.delete', defaultMessage: 'Delete' }, redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' }, + edit: { id: 'status.edit', defaultMessage: 'Edit' }, + publish: { id: 'status.publish', defaultMessage: 'Publish' }, direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' }, mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' }, mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, @@ -63,6 +65,8 @@ class StatusActionBar extends ImmutablePureComponent { onFavourite: PropTypes.func, onReblog: PropTypes.func, onDelete: PropTypes.func, + onEdit: PropTypes.func, + onPublish: PropTypes.func, onDirect: PropTypes.func, onMention: PropTypes.func, onMute: PropTypes.func, @@ -125,7 +129,7 @@ class StatusActionBar extends ImmutablePureComponent { _openInteractionDialog = type => { window.open(`/interact/${this.props.status.get('id')}?type=${type}`, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes'); - } + } handleDeleteClick = () => { this.props.onDelete(this.props.status, this.context.router.history); @@ -135,6 +139,14 @@ class StatusActionBar extends ImmutablePureComponent { this.props.onDelete(this.props.status, this.context.router.history, true); } + handleEditClick = () => { + this.props.onEdit(this.props.status, this.context.router.history); + } + + handlePublishClick = () => { + this.props.onPublish(this.props.status); + } + handlePinClick = () => { this.props.onPin(this.props.status); } @@ -221,10 +233,8 @@ class StatusActionBar extends ImmutablePureComponent { menu.push(null); - if (status.getIn(['account', 'id']) === me || withDismiss) { - menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick }); - menu.push(null); - } + menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick }); + menu.push(null); if (status.getIn(['account', 'id']) === me) { if (publicStatus) { @@ -233,6 +243,11 @@ class StatusActionBar extends ImmutablePureComponent { menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick }); + menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick }); + + if (status.get('published') === false) { + menu.push({ text: intl.formatMessage(messages.publish), action: this.handlePublishClick }); + } } else { menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick }); diff --git a/app/javascript/flavours/glitch/components/status_content.js b/app/javascript/flavours/glitch/components/status_content.js index a39f747b8..a4546edfd 100644 --- a/app/javascript/flavours/glitch/components/status_content.js +++ b/app/javascript/flavours/glitch/components/status_content.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import { isRtl } from 'flavours/glitch/util/rtl'; import { FormattedMessage } from 'react-intl'; import Permalink from './permalink'; +import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp'; import classnames from 'classnames'; import Icon from 'flavours/glitch/components/icon'; import { autoPlayGif } from 'flavours/glitch/util/initial_state'; @@ -13,7 +14,7 @@ const textMatchesTarget = (text, origin, host) => { return (text === origin || text === host || text.startsWith(origin + '/') || text.startsWith(host + '/') || 'www.' + text === host || ('www.' + text).startsWith(host + '/')); -} +}; const isLinkMisleading = (link) => { let linkTextParts = []; @@ -77,11 +78,13 @@ export default class StatusContent extends React.PureComponent { onUpdate: PropTypes.func, tagLinks: PropTypes.bool, rewriteMentions: PropTypes.string, + article: PropTypes.bool, }; static defaultProps = { tagLinks: true, rewriteMentions: 'no', + article: false, }; state = { @@ -231,7 +234,7 @@ export default class StatusContent extends React.PureComponent { let element = e.target; while (element) { - if (['button', 'video', 'a', 'label', 'canvas'].includes(element.localName)) { + if (['button', 'video', 'a', 'label', 'canvas', 'details', 'summary'].includes(element.localName)) { return; } element = element.parentNode; @@ -271,23 +274,213 @@ export default class StatusContent extends React.PureComponent { disabled, tagLinks, rewriteMentions, + article, } = this.props; const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden; - const content = { __html: status.get('contentHtml') }; - const spoilerContent = { __html: status.get('spoilerHtml') }; + const edited = (status.get('edited') === 0) ? null : ( +
+ + , + }} + /> +
+ ); + + const unpublished = (status.get('published') === false) && ( +
+ + +
+ ); + + const local_only = (status.get('local_only') === true) && ( +
+ + +
+ ); + + const quiet = (status.get('notify') === false) && ( +
+ + +
+ ); + + const article_content = status.get('article') && ( +
+ + + + +
+ ); + + const publish_at = status.get('publish_at') && ( +
+ + , + }} + /> +
+ ); + + const expires_at = !unpublished && status.get('expires_at') && ( +
+ + , + }} + /> +
+ ); + + const status_notice_wrapper = ( +
+ {unpublished} + {publish_at} + {expires_at} + {quiet} + {edited} + {local_only} + {article_content} +
+ ); + + const permissions_present = status.get('domain_permissions') && status.get('domain_permissions').size > 0; + + const status_permission_items = permissions_present && status.get('domain_permissions').map((permission) => ( +
  • + + {permission.get('domain')}, + visibility: {permission.get('visibility')}, + }} + /> +
  • + )); + + const permissions = status_permission_items && ( +
    + + + + +
      + {status_permission_items} +
    +
    + ); + + const tag_items = (status.get('tags') && status.get('tags').size > 0) && status.get('tags').map(hashtag => + ( +
  • + + + {hashtag.get('name')} + +
  • + )); + + const tags = tag_items && ( +
    + + + + +
      + {tag_items} +
    +
    + ); + + const footers = ( +
    + {permissions} + {tags} +
    + ); + + const reblog_spoiler_html = status.get('reblogSpoilerPresent') && { __html: status.get('reblogSpoilerHtml') }; + const reblog_spoiler = reblog_spoiler_html && ( +
    + + +
    + ); + + const spoiler_html = status.get('spoiler_text').length > 0 && { __html: status.get('spoilerHtml') }; + const spoiler = spoiler_html && ( +
    + + +
    + ); + + const spoiler_present = status.get('spoiler_text').length > 0 || status.get('reblogSpoilerPresent'); + const content = { __html: article ? status.get('articleHtml') : status.get('contentHtml') }; const directionStyle = { direction: 'ltr' }; const classNames = classnames('status__content', { 'status__content--with-action': parseClick && !disabled, - 'status__content--with-spoiler': status.get('spoiler_text').length > 0, + 'status__content--with-spoiler': spoiler_present, }); if (isRtl(status.get('search_index'))) { directionStyle.direction = 'rtl'; } - if (status.get('spoiler_text').length > 0) { + if (spoiler_present) { let mentionsPlaceholder = ''; const mentionLinks = status.get('mentions').map(item => ( @@ -302,11 +495,19 @@ export default class StatusContent extends React.PureComponent { )).reduce((aggregate, item) => [...aggregate, item, ' '], []); const toggleText = hidden ? [ - , + article ? ( + + ) : ( + + ), mediaIcon ? ( - + {reblog_spoiler} + {spoiler} +
    + +
    + {mentionsPlaceholder} @@ -354,6 +558,8 @@ export default class StatusContent extends React.PureComponent { {media} + {footers} + ); } else if (parseClick) { @@ -366,6 +572,7 @@ export default class StatusContent extends React.PureComponent { tabIndex='0' ref={this.setRef} > + {status_notice_wrapper}
    {media} + {footers}
    ); } else { @@ -384,8 +592,10 @@ export default class StatusContent extends React.PureComponent { tabIndex='0' ref={this.setRef} > + {status_notice_wrapper}
    {media} + {footers}
    ); } diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js index 2cbe3d094..bccaba92d 100644 --- a/app/javascript/flavours/glitch/containers/status_container.js +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -17,7 +17,7 @@ import { pin, unpin, } from 'flavours/glitch/actions/interactions'; -import { muteStatus, unmuteStatus, deleteStatus } from 'flavours/glitch/actions/statuses'; +import { muteStatus, unmuteStatus, deleteStatus, editStatus, publishStatus } from 'flavours/glitch/actions/statuses'; import { initMuteModal } from 'flavours/glitch/actions/mutes'; import { initBlockModal } from 'flavours/glitch/actions/blocks'; import { initReport } from 'flavours/glitch/actions/reports'; @@ -38,6 +38,8 @@ const messages = defineMessages({ redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.' }, replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, + publishConfirm: { id: 'confirmations.publish.confirm', defaultMessage: 'Publish' }, + publishMessage: { id: 'confirmations.publish.message', defaultMessage: 'Are you ready to publish your post?' }, unfilterConfirm: { id: 'confirmations.unfilter.confirm', defaultMessage: 'Show' }, author: { id: 'confirmations.unfilter.author', defaultMessage: 'Author' }, matchingFilters: { id: 'confirmations.unfilter.filters', defaultMessage: 'Matching {count, plural, one {filter} other {filters}}' }, @@ -166,6 +168,18 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ } }, + onEdit (status, history) { + dispatch(editStatus(status, history)); + }, + + onPublish (status) { + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.publishMessage), + confirm: intl.formatMessage(messages.publishConfirm), + onConfirm: () => dispatch(publishStatus(status.get('id'))), + })); + }, + onDirect (account, router) { dispatch(directCompose(account, router)); }, diff --git a/app/javascript/flavours/glitch/features/account/components/action_bar.js b/app/javascript/flavours/glitch/features/account/components/action_bar.js index 6576bff8e..2f5a943fd 100644 --- a/app/javascript/flavours/glitch/features/account/components/action_bar.js +++ b/app/javascript/flavours/glitch/features/account/components/action_bar.js @@ -31,14 +31,20 @@ class ActionBar extends React.PureComponent { if (account.get('acct') !== account.get('username')) { extraInfo = (
    - - {' '} - - - +

    + +

    +

    + + + +

    ); } @@ -51,17 +57,14 @@ class ActionBar extends React.PureComponent {
    - - - { account.get('followers_count') < 0 ? '-' : }
    diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/header.js b/app/javascript/flavours/glitch/features/account_timeline/components/header.js index 8195735a1..591f8dffc 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/components/header.js +++ b/app/javascript/flavours/glitch/features/account_timeline/components/header.js @@ -7,6 +7,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { FormattedMessage } from 'react-intl'; import { NavLink } from 'react-router-dom'; import MovedNote from './moved_note'; +import { me } from 'flavours/glitch/util/initial_state'; export default class Header extends ImmutablePureComponent { @@ -123,9 +124,12 @@ export default class Header extends ImmutablePureComponent { {!hideTabs && (
    - - + + { (account.get('id') === me || account.get('show_replies')) && + () } + { (account.get('id') !== me) && () } +
    )} diff --git a/app/javascript/flavours/glitch/features/account_timeline/index.js b/app/javascript/flavours/glitch/features/account_timeline/index.js index 5558ba2a3..66bf55ec4 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/index.js +++ b/app/javascript/flavours/glitch/features/account_timeline/index.js @@ -17,15 +17,15 @@ import { fetchAccountIdentityProofs } from '../../actions/identity_proofs'; import MissingIndicator from 'flavours/glitch/components/missing_indicator'; import TimelineHint from 'flavours/glitch/components/timeline_hint'; -const mapStateToProps = (state, { params: { accountId }, withReplies = false }) => { - const path = withReplies ? `${accountId}:with_replies` : accountId; +const mapStateToProps = (state, { params: { accountId }, filter = '' }) => { + const path = `${accountId}${filter}`; return { remote: !!(state.getIn(['accounts', accountId, 'acct']) !== state.getIn(['accounts', accountId, 'username'])), remoteUrl: state.getIn(['accounts', accountId, 'url']), isAccount: !!state.getIn(['accounts', accountId]), statusIds: state.getIn(['timelines', `account:${path}`, 'items'], ImmutableList()), - featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], ImmutableList()), + featuredStatusIds: !filter ? state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], ImmutableList()) : ImmutableList(), isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']), hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']), }; @@ -49,7 +49,7 @@ class AccountTimeline extends ImmutablePureComponent { featuredStatusIds: ImmutablePropTypes.list, isLoading: PropTypes.bool, hasMore: PropTypes.bool, - withReplies: PropTypes.bool, + filter: PropTypes.string, isAccount: PropTypes.bool, remote: PropTypes.bool, remoteUrl: PropTypes.string, @@ -57,24 +57,24 @@ class AccountTimeline extends ImmutablePureComponent { }; componentWillMount () { - const { params: { accountId }, withReplies } = this.props; + const { params: { accountId }, filter } = this.props; this.props.dispatch(fetchAccount(accountId)); this.props.dispatch(fetchAccountIdentityProofs(accountId)); - if (!withReplies) { + if (!filter) { this.props.dispatch(expandAccountFeaturedTimeline(accountId)); } - this.props.dispatch(expandAccountTimeline(accountId, { withReplies })); + this.props.dispatch(expandAccountTimeline(accountId, { filter })); } componentWillReceiveProps (nextProps) { - if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) { + if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.filter !== this.props.filter) { this.props.dispatch(fetchAccount(nextProps.params.accountId)); this.props.dispatch(fetchAccountIdentityProofs(nextProps.params.accountId)); - if (!nextProps.withReplies) { + if (!nextProps.filter) { this.props.dispatch(expandAccountFeaturedTimeline(nextProps.params.accountId)); } - this.props.dispatch(expandAccountTimeline(nextProps.params.accountId, { withReplies: nextProps.params.withReplies })); + this.props.dispatch(expandAccountTimeline(nextProps.params.accountId, { filter: nextProps.params.filter })); } } @@ -83,7 +83,7 @@ class AccountTimeline extends ImmutablePureComponent { } handleLoadMore = maxId => { - this.props.dispatch(expandAccountTimeline(this.props.params.accountId, { maxId, withReplies: this.props.withReplies })); + this.props.dispatch(expandAccountTimeline(this.props.params.accountId, { maxId, filter: this.props.filter })); } setRef = c => { diff --git a/app/javascript/flavours/glitch/features/compose/components/compose_form.js b/app/javascript/flavours/glitch/features/compose/components/compose_form.js index a7cb95222..1c05fdafc 100644 --- a/app/javascript/flavours/glitch/features/compose/components/compose_form.js +++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.js @@ -71,10 +71,12 @@ class ComposeForm extends ImmutablePureComponent { onChangeVisibility: PropTypes.func, onPaste: PropTypes.func, onMediaDescriptionConfirm: PropTypes.func, + clearTimeout: PropTypes.bool, }; static defaultProps = { showSearch: false, + clearTimeout: null, }; handleChange = (e) => { @@ -149,6 +151,17 @@ class ComposeForm extends ImmutablePureComponent { this.handleSubmit(sideArm === 'none' ? null : sideArm); } + handleClearAll = () => { + if(!this.clearTimeout || this.clearTimeout === null) { + this.clearTimeout = window.setTimeout(() => { + this.clearTimeout = null; + }, 500); + } else { + this.clearTimeout = null; + this.props.onClearAll(); + } + } + // Selects a suggestion from the autofill. onSuggestionSelected = (tokenStart, token, value) => { this.props.onSuggestionSelected(tokenStart, token, value, ['text']); @@ -256,6 +269,7 @@ class ComposeForm extends ImmutablePureComponent { handleSecondarySubmit, handleSelect, handleSubmit, + handleClearAll, handleRefTextarea, } = this; const { @@ -281,6 +295,7 @@ class ComposeForm extends ImmutablePureComponent { suggestions, text, spoilersAlwaysOn, + clearTimeout, } = this.props; let disabledButton = isSubmitting || isUploading || isChangingUpload || (!text.trim().length && !anyMedia); @@ -356,6 +371,7 @@ class ComposeForm extends ImmutablePureComponent { disabled={disabledButton} onSecondarySubmit={handleSecondarySubmit} onSubmit={handleSubmit} + onClearAll={handleClearAll} privacy={privacy} sideArm={sideArm} /> diff --git a/app/javascript/flavours/glitch/features/compose/components/publisher.js b/app/javascript/flavours/glitch/features/compose/components/publisher.js index 97890f40d..e5a3d023f 100644 --- a/app/javascript/flavours/glitch/features/compose/components/publisher.js +++ b/app/javascript/flavours/glitch/features/compose/components/publisher.js @@ -23,6 +23,10 @@ const messages = defineMessages({ defaultMessage: '{publish}!', id: 'compose_form.publish_loud', }, + clear: { + defaultMessage: 'Double-click to clear', + id: 'compose_form.clear', + }, }); export default @injectIntl @@ -34,6 +38,7 @@ class Publisher extends ImmutablePureComponent { intl: PropTypes.object.isRequired, onSecondarySubmit: PropTypes.func, onSubmit: PropTypes.func, + onClearAll: PropTypes.func, privacy: PropTypes.oneOf(['direct', 'private', 'unlisted', 'public']), sideArm: PropTypes.oneOf(['none', 'direct', 'private', 'unlisted', 'public']), }; @@ -43,7 +48,7 @@ class Publisher extends ImmutablePureComponent { }; render () { - const { countText, disabled, intl, onSecondarySubmit, privacy, sideArm } = this.props; + const { countText, disabled, intl, onClearAll, onSecondarySubmit, privacy, sideArm } = this.props; const diff = maxChars - length(countText || ''); const computedClass = classNames('composer--publisher', { @@ -53,6 +58,17 @@ class Publisher extends ImmutablePureComponent { return (
    +

    + {edited} {mentionsPlaceholder}
    @@ -244,6 +260,8 @@ export default class StatusContent extends React.PureComponent { } else if (this.props.onClick) { const output = [
    + {edited} +
    {!!status.get('poll') && } @@ -260,6 +278,8 @@ export default class StatusContent extends React.PureComponent { } else { return (
    + {edited} +
    {!!status.get('poll') && } diff --git a/app/javascript/mastodon/features/compose/components/poll_form.js b/app/javascript/mastodon/features/compose/components/poll_form.js index 88894ae59..72ffeff09 100644 --- a/app/javascript/mastodon/features/compose/components/poll_form.js +++ b/app/javascript/mastodon/features/compose/components/poll_form.js @@ -89,7 +89,7 @@ class Option extends React.PureComponent {
    - + {/* eslint-disable-next-line jsx-a11y/no-onchange */}