Repository: florence-social/mastodon-fork Branch: main Commit: 147c3cfcbb35 Files: 2374 Total size: 7.4 MB Directory structure: gitextract_pog1r_hd/ ├── .buildpacks ├── .circleci/ │ └── config.yml ├── .codeclimate.yml ├── .dependabot/ │ └── config.yml ├── .dockerignore ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .foreman ├── .gitattributes ├── .gitignore ├── .haml-lint.yml ├── .nanoignore ├── .nvmrc ├── .profile ├── .rspec ├── .rubocop.yml ├── .ruby-version ├── .sass-lint.yml ├── .slugignore ├── .yarnclean ├── AUTHORS.md ├── Aptfile ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Capfile ├── Dockerfile ├── Gemfile ├── LICENSE ├── Procfile ├── Procfile.dev ├── README.md ├── Rakefile ├── Vagrantfile ├── app/ │ ├── chewy/ │ │ └── statuses_index.rb │ ├── controllers/ │ │ ├── about_controller.rb │ │ ├── account_follow_controller.rb │ │ ├── account_unfollow_controller.rb │ │ ├── accounts_controller.rb │ │ ├── activitypub/ │ │ │ ├── collections_controller.rb │ │ │ ├── inboxes_controller.rb │ │ │ └── outboxes_controller.rb │ │ ├── admin/ │ │ │ ├── account_actions_controller.rb │ │ │ ├── account_moderation_notes_controller.rb │ │ │ ├── accounts_controller.rb │ │ │ ├── action_logs_controller.rb │ │ │ ├── base_controller.rb │ │ │ ├── change_emails_controller.rb │ │ │ ├── confirmations_controller.rb │ │ │ ├── custom_emojis_controller.rb │ │ │ ├── dashboard_controller.rb │ │ │ ├── domain_blocks_controller.rb │ │ │ ├── email_domain_blocks_controller.rb │ │ │ ├── followers_controller.rb │ │ │ ├── instances_controller.rb │ │ │ ├── invites_controller.rb │ │ │ ├── pending_accounts_controller.rb │ │ │ ├── relays_controller.rb │ │ │ ├── report_notes_controller.rb │ │ │ ├── reported_statuses_controller.rb │ │ │ ├── reports_controller.rb │ │ │ ├── resets_controller.rb │ │ │ ├── roles_controller.rb │ │ │ ├── settings_controller.rb │ │ │ ├── statuses_controller.rb │ │ │ ├── subscriptions_controller.rb │ │ │ ├── tags_controller.rb │ │ │ ├── two_factor_authentications_controller.rb │ │ │ └── warning_presets_controller.rb │ │ ├── api/ │ │ │ ├── base_controller.rb │ │ │ ├── oembed_controller.rb │ │ │ ├── proofs_controller.rb │ │ │ ├── push_controller.rb │ │ │ ├── salmon_controller.rb │ │ │ ├── subscriptions_controller.rb │ │ │ ├── v1/ │ │ │ │ ├── accounts/ │ │ │ │ │ ├── credentials_controller.rb │ │ │ │ │ ├── follower_accounts_controller.rb │ │ │ │ │ ├── following_accounts_controller.rb │ │ │ │ │ ├── identity_proofs_controller.rb │ │ │ │ │ ├── lists_controller.rb │ │ │ │ │ ├── pins_controller.rb │ │ │ │ │ ├── relationships_controller.rb │ │ │ │ │ ├── search_controller.rb │ │ │ │ │ └── statuses_controller.rb │ │ │ │ ├── accounts_controller.rb │ │ │ │ ├── apps/ │ │ │ │ │ └── credentials_controller.rb │ │ │ │ ├── apps_controller.rb │ │ │ │ ├── blocks_controller.rb │ │ │ │ ├── conversations_controller.rb │ │ │ │ ├── custom_emojis_controller.rb │ │ │ │ ├── domain_blocks_controller.rb │ │ │ │ ├── endorsements_controller.rb │ │ │ │ ├── favourites_controller.rb │ │ │ │ ├── filters_controller.rb │ │ │ │ ├── follow_requests_controller.rb │ │ │ │ ├── follows_controller.rb │ │ │ │ ├── instances/ │ │ │ │ │ ├── activity_controller.rb │ │ │ │ │ └── peers_controller.rb │ │ │ │ ├── instances_controller.rb │ │ │ │ ├── lists/ │ │ │ │ │ └── accounts_controller.rb │ │ │ │ ├── lists_controller.rb │ │ │ │ ├── media_controller.rb │ │ │ │ ├── mutes_controller.rb │ │ │ │ ├── notifications_controller.rb │ │ │ │ ├── polls/ │ │ │ │ │ └── votes_controller.rb │ │ │ │ ├── polls_controller.rb │ │ │ │ ├── preferences_controller.rb │ │ │ │ ├── push/ │ │ │ │ │ └── subscriptions_controller.rb │ │ │ │ ├── reports_controller.rb │ │ │ │ ├── scheduled_statuses_controller.rb │ │ │ │ ├── search_controller.rb │ │ │ │ ├── statuses/ │ │ │ │ │ ├── favourited_by_accounts_controller.rb │ │ │ │ │ ├── favourites_controller.rb │ │ │ │ │ ├── mutes_controller.rb │ │ │ │ │ ├── pins_controller.rb │ │ │ │ │ ├── reblogged_by_accounts_controller.rb │ │ │ │ │ └── reblogs_controller.rb │ │ │ │ ├── statuses_controller.rb │ │ │ │ ├── streaming_controller.rb │ │ │ │ ├── suggestions_controller.rb │ │ │ │ └── timelines/ │ │ │ │ ├── direct_controller.rb │ │ │ │ ├── home_controller.rb │ │ │ │ ├── list_controller.rb │ │ │ │ ├── public_controller.rb │ │ │ │ └── tag_controller.rb │ │ │ ├── v2/ │ │ │ │ └── search_controller.rb │ │ │ └── web/ │ │ │ ├── base_controller.rb │ │ │ ├── embeds_controller.rb │ │ │ ├── push_subscriptions_controller.rb │ │ │ └── settings_controller.rb │ │ ├── application_controller.rb │ │ ├── auth/ │ │ │ ├── confirmations_controller.rb │ │ │ ├── omniauth_callbacks_controller.rb │ │ │ ├── passwords_controller.rb │ │ │ ├── registrations_controller.rb │ │ │ └── sessions_controller.rb │ │ ├── authorize_interactions_controller.rb │ │ ├── concerns/ │ │ │ ├── account_controller_concern.rb │ │ │ ├── accountable_concern.rb │ │ │ ├── authorization.rb │ │ │ ├── export_controller_concern.rb │ │ │ ├── localized.rb │ │ │ ├── obfuscate_filename.rb │ │ │ ├── rate_limit_headers.rb │ │ │ ├── session_tracking_concern.rb │ │ │ ├── signature_authentication.rb │ │ │ ├── signature_verification.rb │ │ │ └── user_tracking_concern.rb │ │ ├── custom_css_controller.rb │ │ ├── directories_controller.rb │ │ ├── emojis_controller.rb │ │ ├── filters_controller.rb │ │ ├── follower_accounts_controller.rb │ │ ├── following_accounts_controller.rb │ │ ├── home_controller.rb │ │ ├── intents_controller.rb │ │ ├── invites_controller.rb │ │ ├── manifests_controller.rb │ │ ├── media_controller.rb │ │ ├── media_proxy_controller.rb │ │ ├── oauth/ │ │ │ ├── authorizations_controller.rb │ │ │ ├── authorized_applications_controller.rb │ │ │ └── tokens_controller.rb │ │ ├── public_timelines_controller.rb │ │ ├── relationships_controller.rb │ │ ├── remote_follow_controller.rb │ │ ├── remote_interaction_controller.rb │ │ ├── remote_unfollows_controller.rb │ │ ├── settings/ │ │ │ ├── applications_controller.rb │ │ │ ├── base_controller.rb │ │ │ ├── deletes_controller.rb │ │ │ ├── exports/ │ │ │ │ ├── blocked_accounts_controller.rb │ │ │ │ ├── blocked_domains_controller.rb │ │ │ │ ├── following_accounts_controller.rb │ │ │ │ ├── lists_controller.rb │ │ │ │ └── muted_accounts_controller.rb │ │ │ ├── exports_controller.rb │ │ │ ├── featured_tags_controller.rb │ │ │ ├── identity_proofs_controller.rb │ │ │ ├── imports_controller.rb │ │ │ ├── migrations_controller.rb │ │ │ ├── preferences/ │ │ │ │ ├── appearance_controller.rb │ │ │ │ ├── notifications_controller.rb │ │ │ │ └── other_controller.rb │ │ │ ├── preferences_controller.rb │ │ │ ├── profiles_controller.rb │ │ │ ├── sessions_controller.rb │ │ │ ├── two_factor_authentication/ │ │ │ │ ├── confirmations_controller.rb │ │ │ │ └── recovery_codes_controller.rb │ │ │ └── two_factor_authentications_controller.rb │ │ ├── shares_controller.rb │ │ ├── statuses_controller.rb │ │ ├── stream_entries_controller.rb │ │ ├── tags_controller.rb │ │ └── well_known/ │ │ ├── host_meta_controller.rb │ │ ├── keybase_proof_config_controller.rb │ │ └── webfinger_controller.rb │ ├── helpers/ │ │ ├── admin/ │ │ │ ├── account_moderation_notes_helper.rb │ │ │ ├── action_logs_helper.rb │ │ │ ├── dashboard_helper.rb │ │ │ └── filter_helper.rb │ │ ├── application_helper.rb │ │ ├── flashes_helper.rb │ │ ├── home_helper.rb │ │ ├── instance_helper.rb │ │ ├── jsonld_helper.rb │ │ ├── routing_helper.rb │ │ ├── settings_helper.rb │ │ └── stream_entries_helper.rb │ ├── javascript/ │ │ ├── mastodon/ │ │ │ ├── actions/ │ │ │ │ ├── accounts.js │ │ │ │ ├── alerts.js │ │ │ │ ├── blocks.js │ │ │ │ ├── bundles.js │ │ │ │ ├── columns.js │ │ │ │ ├── compose.js │ │ │ │ ├── conversations.js │ │ │ │ ├── custom_emojis.js │ │ │ │ ├── domain_blocks.js │ │ │ │ ├── dropdown_menu.js │ │ │ │ ├── emojis.js │ │ │ │ ├── favourites.js │ │ │ │ ├── filters.js │ │ │ │ ├── height_cache.js │ │ │ │ ├── identity_proofs.js │ │ │ │ ├── importer/ │ │ │ │ │ ├── index.js │ │ │ │ │ └── normalizer.js │ │ │ │ ├── interactions.js │ │ │ │ ├── lists.js │ │ │ │ ├── modal.js │ │ │ │ ├── mutes.js │ │ │ │ ├── notifications.js │ │ │ │ ├── onboarding.js │ │ │ │ ├── pin_statuses.js │ │ │ │ ├── polls.js │ │ │ │ ├── push_notifications/ │ │ │ │ │ ├── index.js │ │ │ │ │ ├── registerer.js │ │ │ │ │ └── setter.js │ │ │ │ ├── reports.js │ │ │ │ ├── search.js │ │ │ │ ├── settings.js │ │ │ │ ├── statuses.js │ │ │ │ ├── store.js │ │ │ │ ├── streaming.js │ │ │ │ ├── suggestions.js │ │ │ │ └── timelines.js │ │ │ ├── api.js │ │ │ ├── base_polyfills.js │ │ │ ├── common.js │ │ │ ├── compare_id.js │ │ │ ├── components/ │ │ │ │ ├── __tests__/ │ │ │ │ │ ├── __snapshots__/ │ │ │ │ │ │ ├── autosuggest_emoji-test.js.snap │ │ │ │ │ │ ├── avatar-test.js.snap │ │ │ │ │ │ ├── avatar_overlay-test.js.snap │ │ │ │ │ │ ├── button-test.js.snap │ │ │ │ │ │ └── display_name-test.js.snap │ │ │ │ │ ├── autosuggest_emoji-test.js │ │ │ │ │ ├── avatar-test.js │ │ │ │ │ ├── avatar_overlay-test.js │ │ │ │ │ ├── button-test.js │ │ │ │ │ └── display_name-test.js │ │ │ │ ├── account.js │ │ │ │ ├── attachment_list.js │ │ │ │ ├── autosuggest_emoji.js │ │ │ │ ├── autosuggest_input.js │ │ │ │ ├── autosuggest_textarea.js │ │ │ │ ├── avatar.js │ │ │ │ ├── avatar_composite.js │ │ │ │ ├── avatar_overlay.js │ │ │ │ ├── button.js │ │ │ │ ├── column.js │ │ │ │ ├── column_back_button.js │ │ │ │ ├── column_back_button_slim.js │ │ │ │ ├── column_header.js │ │ │ │ ├── display_name.js │ │ │ │ ├── domain.js │ │ │ │ ├── dropdown_menu.js │ │ │ │ ├── error_boundary.js │ │ │ │ ├── extended_video_player.js │ │ │ │ ├── hashtag.js │ │ │ │ ├── icon.js │ │ │ │ ├── icon_button.js │ │ │ │ ├── icon_with_badge.js │ │ │ │ ├── intersection_observer_article.js │ │ │ │ ├── load_gap.js │ │ │ │ ├── load_more.js │ │ │ │ ├── loading_indicator.js │ │ │ │ ├── media_gallery.js │ │ │ │ ├── missing_indicator.js │ │ │ │ ├── modal_root.js │ │ │ │ ├── permalink.js │ │ │ │ ├── poll.js │ │ │ │ ├── relative_timestamp.js │ │ │ │ ├── scrollable_list.js │ │ │ │ ├── setting_text.js │ │ │ │ ├── status.js │ │ │ │ ├── status_action_bar.js │ │ │ │ ├── status_content.js │ │ │ │ └── status_list.js │ │ │ ├── containers/ │ │ │ │ ├── account_container.js │ │ │ │ ├── compose_container.js │ │ │ │ ├── domain_container.js │ │ │ │ ├── dropdown_menu_container.js │ │ │ │ ├── intersection_observer_article_container.js │ │ │ │ ├── mastodon.js │ │ │ │ ├── media_container.js │ │ │ │ ├── poll_container.js │ │ │ │ ├── status_container.js │ │ │ │ └── timeline_container.js │ │ │ ├── extra_polyfills.js │ │ │ ├── features/ │ │ │ │ ├── account/ │ │ │ │ │ └── components/ │ │ │ │ │ └── header.js │ │ │ │ ├── account_gallery/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ └── media_item.js │ │ │ │ │ └── index.js │ │ │ │ ├── account_timeline/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── header.js │ │ │ │ │ │ └── moved_note.js │ │ │ │ │ ├── containers/ │ │ │ │ │ │ └── header_container.js │ │ │ │ │ └── index.js │ │ │ │ ├── blocks/ │ │ │ │ │ └── index.js │ │ │ │ ├── community_timeline/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ └── column_settings.js │ │ │ │ │ ├── containers/ │ │ │ │ │ │ └── column_settings_container.js │ │ │ │ │ └── index.js │ │ │ │ ├── compose/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── action_bar.js │ │ │ │ │ │ ├── autosuggest_account.js │ │ │ │ │ │ ├── character_counter.js │ │ │ │ │ │ ├── compose_form.js │ │ │ │ │ │ ├── emoji_picker_dropdown.js │ │ │ │ │ │ ├── navigation_bar.js │ │ │ │ │ │ ├── poll_button.js │ │ │ │ │ │ ├── poll_form.js │ │ │ │ │ │ ├── privacy_dropdown.js │ │ │ │ │ │ ├── reply_indicator.js │ │ │ │ │ │ ├── search.js │ │ │ │ │ │ ├── search_results.js │ │ │ │ │ │ ├── text_icon_button.js │ │ │ │ │ │ ├── upload.js │ │ │ │ │ │ ├── upload_button.js │ │ │ │ │ │ ├── upload_form.js │ │ │ │ │ │ ├── upload_progress.js │ │ │ │ │ │ └── warning.js │ │ │ │ │ ├── containers/ │ │ │ │ │ │ ├── autosuggest_account_container.js │ │ │ │ │ │ ├── compose_form_container.js │ │ │ │ │ │ ├── emoji_picker_dropdown_container.js │ │ │ │ │ │ ├── navigation_container.js │ │ │ │ │ │ ├── poll_button_container.js │ │ │ │ │ │ ├── poll_form_container.js │ │ │ │ │ │ ├── privacy_dropdown_container.js │ │ │ │ │ │ ├── reply_indicator_container.js │ │ │ │ │ │ ├── search_container.js │ │ │ │ │ │ ├── search_results_container.js │ │ │ │ │ │ ├── sensitive_button_container.js │ │ │ │ │ │ ├── spoiler_button_container.js │ │ │ │ │ │ ├── upload_button_container.js │ │ │ │ │ │ ├── upload_container.js │ │ │ │ │ │ ├── upload_form_container.js │ │ │ │ │ │ ├── upload_progress_container.js │ │ │ │ │ │ └── warning_container.js │ │ │ │ │ ├── index.js │ │ │ │ │ └── util/ │ │ │ │ │ ├── counter.js │ │ │ │ │ └── url_regex.js │ │ │ │ ├── direct_timeline/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── conversation.js │ │ │ │ │ │ └── conversations_list.js │ │ │ │ │ ├── containers/ │ │ │ │ │ │ ├── conversation_container.js │ │ │ │ │ │ └── conversations_list_container.js │ │ │ │ │ └── index.js │ │ │ │ ├── domain_blocks/ │ │ │ │ │ └── index.js │ │ │ │ ├── emoji/ │ │ │ │ │ ├── __tests__/ │ │ │ │ │ │ ├── emoji-test.js │ │ │ │ │ │ └── emoji_index-test.js │ │ │ │ │ ├── emoji.js │ │ │ │ │ ├── emoji_compressed.js │ │ │ │ │ ├── emoji_map.json │ │ │ │ │ ├── emoji_mart_data_light.js │ │ │ │ │ ├── emoji_mart_search_light.js │ │ │ │ │ ├── emoji_picker.js │ │ │ │ │ ├── emoji_unicode_mapping_light.js │ │ │ │ │ ├── emoji_utils.js │ │ │ │ │ ├── unicode_to_filename.js │ │ │ │ │ └── unicode_to_unified_name.js │ │ │ │ ├── favourited_statuses/ │ │ │ │ │ └── index.js │ │ │ │ ├── favourites/ │ │ │ │ │ └── index.js │ │ │ │ ├── follow_requests/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ └── account_authorize.js │ │ │ │ │ ├── containers/ │ │ │ │ │ │ └── account_authorize_container.js │ │ │ │ │ └── index.js │ │ │ │ ├── followers/ │ │ │ │ │ └── index.js │ │ │ │ ├── following/ │ │ │ │ │ └── index.js │ │ │ │ ├── generic_not_found/ │ │ │ │ │ └── index.js │ │ │ │ ├── getting_started/ │ │ │ │ │ └── index.js │ │ │ │ ├── hashtag_timeline/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ └── column_settings.js │ │ │ │ │ ├── containers/ │ │ │ │ │ │ └── column_settings_container.js │ │ │ │ │ └── index.js │ │ │ │ ├── home_timeline/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ └── column_settings.js │ │ │ │ │ ├── containers/ │ │ │ │ │ │ └── column_settings_container.js │ │ │ │ │ └── index.js │ │ │ │ ├── introduction/ │ │ │ │ │ └── index.js │ │ │ │ ├── keyboard_shortcuts/ │ │ │ │ │ └── index.js │ │ │ │ ├── list_adder/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── account.js │ │ │ │ │ │ └── list.js │ │ │ │ │ └── index.js │ │ │ │ ├── list_editor/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── account.js │ │ │ │ │ │ ├── edit_list_form.js │ │ │ │ │ │ └── search.js │ │ │ │ │ └── index.js │ │ │ │ ├── list_timeline/ │ │ │ │ │ └── index.js │ │ │ │ ├── lists/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ └── new_list_form.js │ │ │ │ │ └── index.js │ │ │ │ ├── mutes/ │ │ │ │ │ └── index.js │ │ │ │ ├── notifications/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── clear_column_button.js │ │ │ │ │ │ ├── column_settings.js │ │ │ │ │ │ ├── filter_bar.js │ │ │ │ │ │ ├── notification.js │ │ │ │ │ │ └── setting_toggle.js │ │ │ │ │ ├── containers/ │ │ │ │ │ │ ├── column_settings_container.js │ │ │ │ │ │ ├── filter_bar_container.js │ │ │ │ │ │ └── notification_container.js │ │ │ │ │ └── index.js │ │ │ │ ├── pinned_statuses/ │ │ │ │ │ └── index.js │ │ │ │ ├── public_timeline/ │ │ │ │ │ ├── containers/ │ │ │ │ │ │ └── column_settings_container.js │ │ │ │ │ └── index.js │ │ │ │ ├── reblogs/ │ │ │ │ │ └── index.js │ │ │ │ ├── report/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ └── status_check_box.js │ │ │ │ │ └── containers/ │ │ │ │ │ └── status_check_box_container.js │ │ │ │ ├── search/ │ │ │ │ │ └── index.js │ │ │ │ ├── standalone/ │ │ │ │ │ ├── compose/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── hashtag_timeline/ │ │ │ │ │ │ └── index.js │ │ │ │ │ └── public_timeline/ │ │ │ │ │ └── index.js │ │ │ │ ├── status/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── action_bar.js │ │ │ │ │ │ ├── card.js │ │ │ │ │ │ └── detailed_status.js │ │ │ │ │ ├── containers/ │ │ │ │ │ │ └── detailed_status_container.js │ │ │ │ │ └── index.js │ │ │ │ ├── ui/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── __tests__/ │ │ │ │ │ │ │ └── column-test.js │ │ │ │ │ │ ├── actions_modal.js │ │ │ │ │ │ ├── boost_modal.js │ │ │ │ │ │ ├── bundle.js │ │ │ │ │ │ ├── bundle_column_error.js │ │ │ │ │ │ ├── bundle_modal_error.js │ │ │ │ │ │ ├── column.js │ │ │ │ │ │ ├── column_header.js │ │ │ │ │ │ ├── column_link.js │ │ │ │ │ │ ├── column_loading.js │ │ │ │ │ │ ├── column_subheading.js │ │ │ │ │ │ ├── columns_area.js │ │ │ │ │ │ ├── compose_panel.js │ │ │ │ │ │ ├── confirmation_modal.js │ │ │ │ │ │ ├── drawer_loading.js │ │ │ │ │ │ ├── embed_modal.js │ │ │ │ │ │ ├── focal_point_modal.js │ │ │ │ │ │ ├── follow_requests_nav_link.js │ │ │ │ │ │ ├── image_loader.js │ │ │ │ │ │ ├── link_footer.js │ │ │ │ │ │ ├── list_panel.js │ │ │ │ │ │ ├── media_modal.js │ │ │ │ │ │ ├── modal_loading.js │ │ │ │ │ │ ├── modal_root.js │ │ │ │ │ │ ├── mute_modal.js │ │ │ │ │ │ ├── navigation_panel.js │ │ │ │ │ │ ├── notifications_counter_icon.js │ │ │ │ │ │ ├── report_modal.js │ │ │ │ │ │ ├── tabs_bar.js │ │ │ │ │ │ ├── upload_area.js │ │ │ │ │ │ ├── video_modal.js │ │ │ │ │ │ └── zoomable_image.js │ │ │ │ │ ├── containers/ │ │ │ │ │ │ ├── bundle_container.js │ │ │ │ │ │ ├── columns_area_container.js │ │ │ │ │ │ ├── loading_bar_container.js │ │ │ │ │ │ ├── modal_container.js │ │ │ │ │ │ ├── notifications_container.js │ │ │ │ │ │ └── status_list_container.js │ │ │ │ │ ├── index.js │ │ │ │ │ └── util/ │ │ │ │ │ ├── async-components.js │ │ │ │ │ ├── fullscreen.js │ │ │ │ │ ├── get_rect_from_entry.js │ │ │ │ │ ├── intersection_observer_wrapper.js │ │ │ │ │ ├── optional_motion.js │ │ │ │ │ ├── react_router_helpers.js │ │ │ │ │ ├── reduced_motion.js │ │ │ │ │ └── schedule_idle_task.js │ │ │ │ └── video/ │ │ │ │ └── index.js │ │ │ ├── initial_state.js │ │ │ ├── is_mobile.js │ │ │ ├── load_polyfills.js │ │ │ ├── locales/ │ │ │ │ ├── ar.json │ │ │ │ ├── ast.json │ │ │ │ ├── bg.json │ │ │ │ ├── bn.json │ │ │ │ ├── ca.json │ │ │ │ ├── co.json │ │ │ │ ├── cs.json │ │ │ │ ├── cy.json │ │ │ │ ├── da.json │ │ │ │ ├── de.json │ │ │ │ ├── defaultMessages.json │ │ │ │ ├── el.json │ │ │ │ ├── en.json │ │ │ │ ├── eo.json │ │ │ │ ├── es.json │ │ │ │ ├── eu.json │ │ │ │ ├── fa.json │ │ │ │ ├── fi.json │ │ │ │ ├── fr.json │ │ │ │ ├── gl.json │ │ │ │ ├── he.json │ │ │ │ ├── hi.json │ │ │ │ ├── hr.json │ │ │ │ ├── hu.json │ │ │ │ ├── hy.json │ │ │ │ ├── id.json │ │ │ │ ├── index.js │ │ │ │ ├── io.json │ │ │ │ ├── it.json │ │ │ │ ├── ja.json │ │ │ │ ├── ka.json │ │ │ │ ├── kk.json │ │ │ │ ├── ko.json │ │ │ │ ├── locale-data/ │ │ │ │ │ ├── README.md │ │ │ │ │ ├── co.js │ │ │ │ │ └── oc.js │ │ │ │ ├── lt.json │ │ │ │ ├── lv.json │ │ │ │ ├── ms.json │ │ │ │ ├── nl.json │ │ │ │ ├── no.json │ │ │ │ ├── oc.json │ │ │ │ ├── pl.json │ │ │ │ ├── pt-BR.json │ │ │ │ ├── pt.json │ │ │ │ ├── ro.json │ │ │ │ ├── ru.json │ │ │ │ ├── sk.json │ │ │ │ ├── sl.json │ │ │ │ ├── sq.json │ │ │ │ ├── sr-Latn.json │ │ │ │ ├── sr.json │ │ │ │ ├── sv.json │ │ │ │ ├── ta.json │ │ │ │ ├── te.json │ │ │ │ ├── th.json │ │ │ │ ├── tr.json │ │ │ │ ├── uk.json │ │ │ │ ├── whitelist_ar.json │ │ │ │ ├── whitelist_ast.json │ │ │ │ ├── whitelist_bg.json │ │ │ │ ├── whitelist_bn.json │ │ │ │ ├── whitelist_ca.json │ │ │ │ ├── whitelist_co.json │ │ │ │ ├── whitelist_cs.json │ │ │ │ ├── whitelist_cy.json │ │ │ │ ├── whitelist_da.json │ │ │ │ ├── whitelist_de.json │ │ │ │ ├── whitelist_el.json │ │ │ │ ├── whitelist_en.json │ │ │ │ ├── whitelist_eo.json │ │ │ │ ├── whitelist_es.json │ │ │ │ ├── whitelist_eu.json │ │ │ │ ├── whitelist_fa.json │ │ │ │ ├── whitelist_fi.json │ │ │ │ ├── whitelist_fr.json │ │ │ │ ├── whitelist_gl.json │ │ │ │ ├── whitelist_he.json │ │ │ │ ├── whitelist_hi.json │ │ │ │ ├── whitelist_hr.json │ │ │ │ ├── whitelist_hu.json │ │ │ │ ├── whitelist_hy.json │ │ │ │ ├── whitelist_id.json │ │ │ │ ├── whitelist_io.json │ │ │ │ ├── whitelist_it.json │ │ │ │ ├── whitelist_ja.json │ │ │ │ ├── whitelist_ka.json │ │ │ │ ├── whitelist_kk.json │ │ │ │ ├── whitelist_ko.json │ │ │ │ ├── whitelist_lt.json │ │ │ │ ├── whitelist_lv.json │ │ │ │ ├── whitelist_ms.json │ │ │ │ ├── whitelist_nl.json │ │ │ │ ├── whitelist_no.json │ │ │ │ ├── whitelist_oc.json │ │ │ │ ├── whitelist_pl.json │ │ │ │ ├── whitelist_pt-BR.json │ │ │ │ ├── whitelist_pt.json │ │ │ │ ├── whitelist_ro.json │ │ │ │ ├── whitelist_ru.json │ │ │ │ ├── whitelist_sk.json │ │ │ │ ├── whitelist_sl.json │ │ │ │ ├── whitelist_sq.json │ │ │ │ ├── whitelist_sr-Latn.json │ │ │ │ ├── whitelist_sr.json │ │ │ │ ├── whitelist_sv.json │ │ │ │ ├── whitelist_ta.json │ │ │ │ ├── whitelist_te.json │ │ │ │ ├── whitelist_th.json │ │ │ │ ├── whitelist_tr.json │ │ │ │ ├── whitelist_uk.json │ │ │ │ ├── whitelist_zh-CN.json │ │ │ │ ├── whitelist_zh-HK.json │ │ │ │ ├── whitelist_zh-TW.json │ │ │ │ ├── zh-CN.json │ │ │ │ ├── zh-HK.json │ │ │ │ └── zh-TW.json │ │ │ ├── main.js │ │ │ ├── middleware/ │ │ │ │ ├── errors.js │ │ │ │ ├── loading_bar.js │ │ │ │ └── sounds.js │ │ │ ├── performance.js │ │ │ ├── ready.js │ │ │ ├── reducers/ │ │ │ │ ├── accounts.js │ │ │ │ ├── accounts_counters.js │ │ │ │ ├── alerts.js │ │ │ │ ├── compose.js │ │ │ │ ├── contexts.js │ │ │ │ ├── conversations.js │ │ │ │ ├── custom_emojis.js │ │ │ │ ├── domain_lists.js │ │ │ │ ├── dropdown_menu.js │ │ │ │ ├── filters.js │ │ │ │ ├── height_cache.js │ │ │ │ ├── identity_proofs.js │ │ │ │ ├── index.js │ │ │ │ ├── list_adder.js │ │ │ │ ├── list_editor.js │ │ │ │ ├── lists.js │ │ │ │ ├── media_attachments.js │ │ │ │ ├── meta.js │ │ │ │ ├── modal.js │ │ │ │ ├── mutes.js │ │ │ │ ├── notifications.js │ │ │ │ ├── polls.js │ │ │ │ ├── push_notifications.js │ │ │ │ ├── relationships.js │ │ │ │ ├── reports.js │ │ │ │ ├── search.js │ │ │ │ ├── settings.js │ │ │ │ ├── status_lists.js │ │ │ │ ├── statuses.js │ │ │ │ ├── suggestions.js │ │ │ │ ├── timelines.js │ │ │ │ └── user_lists.js │ │ │ ├── rtl.js │ │ │ ├── scroll.js │ │ │ ├── selectors/ │ │ │ │ └── index.js │ │ │ ├── service_worker/ │ │ │ │ ├── entry.js │ │ │ │ ├── web_push_locales.js │ │ │ │ └── web_push_notifications.js │ │ │ ├── settings.js │ │ │ ├── storage/ │ │ │ │ ├── db.js │ │ │ │ └── modifier.js │ │ │ ├── store/ │ │ │ │ └── configureStore.js │ │ │ ├── stream.js │ │ │ ├── test_setup.js │ │ │ ├── utils/ │ │ │ │ ├── __tests__/ │ │ │ │ │ ├── base64-test.js │ │ │ │ │ └── html-test.js │ │ │ │ ├── base64.js │ │ │ │ ├── html.js │ │ │ │ ├── numbers.js │ │ │ │ └── resize_image.js │ │ │ └── uuid.js │ │ ├── packs/ │ │ │ ├── about.js │ │ │ ├── admin.js │ │ │ ├── application.js │ │ │ ├── error.js │ │ │ ├── mailer.js │ │ │ ├── public.js │ │ │ └── share.js │ │ └── styles/ │ │ ├── application.scss │ │ ├── contrast/ │ │ │ ├── diff.scss │ │ │ └── variables.scss │ │ ├── contrast.scss │ │ ├── fonts/ │ │ │ ├── montserrat.scss │ │ │ ├── roboto-mono.scss │ │ │ └── roboto.scss │ │ ├── mailer.scss │ │ ├── mastodon/ │ │ │ ├── _mixins.scss │ │ │ ├── about.scss │ │ │ ├── accessibility.scss │ │ │ ├── accounts.scss │ │ │ ├── admin.scss │ │ │ ├── basics.scss │ │ │ ├── boost.scss │ │ │ ├── compact_header.scss │ │ │ ├── components.scss │ │ │ ├── containers.scss │ │ │ ├── dashboard.scss │ │ │ ├── emoji_picker.scss │ │ │ ├── footer.scss │ │ │ ├── forms.scss │ │ │ ├── introduction.scss │ │ │ ├── lists.scss │ │ │ ├── modal.scss │ │ │ ├── polls.scss │ │ │ ├── reset.scss │ │ │ ├── rtl.scss │ │ │ ├── stream_entries.scss │ │ │ ├── tables.scss │ │ │ ├── variables.scss │ │ │ └── widgets.scss │ │ ├── mastodon-light/ │ │ │ ├── diff.scss │ │ │ └── variables.scss │ │ └── mastodon-light.scss │ ├── lib/ │ │ ├── activity_tracker.rb │ │ ├── activitypub/ │ │ │ ├── activity/ │ │ │ │ ├── accept.rb │ │ │ │ ├── add.rb │ │ │ │ ├── announce.rb │ │ │ │ ├── block.rb │ │ │ │ ├── create.rb │ │ │ │ ├── delete.rb │ │ │ │ ├── flag.rb │ │ │ │ ├── follow.rb │ │ │ │ ├── like.rb │ │ │ │ ├── move.rb │ │ │ │ ├── reject.rb │ │ │ │ ├── remove.rb │ │ │ │ ├── undo.rb │ │ │ │ └── update.rb │ │ │ ├── activity.rb │ │ │ ├── adapter.rb │ │ │ ├── case_transform.rb │ │ │ ├── linked_data_signature.rb │ │ │ ├── serializer.rb │ │ │ └── tag_manager.rb │ │ ├── application_extension.rb │ │ ├── delivery_failure_tracker.rb │ │ ├── entity_cache.rb │ │ ├── exceptions.rb │ │ ├── extractor.rb │ │ ├── fast_geometry_parser.rb │ │ ├── feed_manager.rb │ │ ├── formatter.rb │ │ ├── hash_object.rb │ │ ├── inline_renderer.rb │ │ ├── language_detector.rb │ │ ├── ostatus/ │ │ │ ├── activity/ │ │ │ │ ├── base.rb │ │ │ │ ├── creation.rb │ │ │ │ ├── deletion.rb │ │ │ │ ├── general.rb │ │ │ │ ├── post.rb │ │ │ │ ├── remote.rb │ │ │ │ └── share.rb │ │ │ ├── atom_serializer.rb │ │ │ └── tag_manager.rb │ │ ├── potential_friendship_tracker.rb │ │ ├── proof_provider/ │ │ │ ├── keybase/ │ │ │ │ ├── badge.rb │ │ │ │ ├── config_serializer.rb │ │ │ │ ├── serializer.rb │ │ │ │ ├── verifier.rb │ │ │ │ └── worker.rb │ │ │ └── keybase.rb │ │ ├── proof_provider.rb │ │ ├── request.rb │ │ ├── rss_builder.rb │ │ ├── sanitize_config.rb │ │ ├── settings/ │ │ │ ├── extend.rb │ │ │ └── scoped_settings.rb │ │ ├── sidekiq_error_handler.rb │ │ ├── status_filter.rb │ │ ├── status_finder.rb │ │ ├── tag_manager.rb │ │ ├── themes.rb │ │ ├── user_settings_decorator.rb │ │ └── webfinger_resource.rb │ ├── mailers/ │ │ ├── admin_mailer.rb │ │ ├── application_mailer.rb │ │ ├── notification_mailer.rb │ │ └── user_mailer.rb │ ├── models/ │ │ ├── account.rb │ │ ├── account_conversation.rb │ │ ├── account_domain_block.rb │ │ ├── account_filter.rb │ │ ├── account_identity_proof.rb │ │ ├── account_moderation_note.rb │ │ ├── account_pin.rb │ │ ├── account_stat.rb │ │ ├── account_tag_stat.rb │ │ ├── account_warning.rb │ │ ├── account_warning_preset.rb │ │ ├── admin/ │ │ │ ├── account_action.rb │ │ │ └── action_log.rb │ │ ├── admin.rb │ │ ├── application_record.rb │ │ ├── backup.rb │ │ ├── block.rb │ │ ├── concerns/ │ │ │ ├── account_associations.rb │ │ │ ├── account_avatar.rb │ │ │ ├── account_counters.rb │ │ │ ├── account_finder_concern.rb │ │ │ ├── account_header.rb │ │ │ ├── account_interactions.rb │ │ │ ├── attachmentable.rb │ │ │ ├── cacheable.rb │ │ │ ├── domain_normalizable.rb │ │ │ ├── expireable.rb │ │ │ ├── ldap_authenticable.rb │ │ │ ├── omniauthable.rb │ │ │ ├── paginable.rb │ │ │ ├── pam_authenticable.rb │ │ │ ├── redisable.rb │ │ │ ├── relationship_cacheable.rb │ │ │ ├── remotable.rb │ │ │ ├── status_threading_concern.rb │ │ │ ├── streamable.rb │ │ │ └── user_roles.rb │ │ ├── context.rb │ │ ├── conversation.rb │ │ ├── conversation_mute.rb │ │ ├── custom_emoji.rb │ │ ├── custom_emoji_filter.rb │ │ ├── custom_filter.rb │ │ ├── domain_block.rb │ │ ├── email_domain_block.rb │ │ ├── export.rb │ │ ├── favourite.rb │ │ ├── featured_tag.rb │ │ ├── feed.rb │ │ ├── follow.rb │ │ ├── follow_request.rb │ │ ├── form/ │ │ │ ├── account_batch.rb │ │ │ ├── admin_settings.rb │ │ │ ├── delete_confirmation.rb │ │ │ ├── migration.rb │ │ │ ├── status_batch.rb │ │ │ └── two_factor_confirmation.rb │ │ ├── home_feed.rb │ │ ├── identity.rb │ │ ├── import.rb │ │ ├── instance.rb │ │ ├── instance_filter.rb │ │ ├── invite.rb │ │ ├── invite_filter.rb │ │ ├── list.rb │ │ ├── list_account.rb │ │ ├── list_feed.rb │ │ ├── media_attachment.rb │ │ ├── mention.rb │ │ ├── mute.rb │ │ ├── notification.rb │ │ ├── poll.rb │ │ ├── poll_vote.rb │ │ ├── preview_card.rb │ │ ├── relay.rb │ │ ├── remote_follow.rb │ │ ├── remote_profile.rb │ │ ├── report.rb │ │ ├── report_filter.rb │ │ ├── report_note.rb │ │ ├── scheduled_status.rb │ │ ├── search.rb │ │ ├── session_activation.rb │ │ ├── setting.rb │ │ ├── site_upload.rb │ │ ├── status.rb │ │ ├── status_pin.rb │ │ ├── status_stat.rb │ │ ├── stream_entry.rb │ │ ├── subscription.rb │ │ ├── tag.rb │ │ ├── tombstone.rb │ │ ├── trending_tags.rb │ │ ├── user.rb │ │ ├── user_invite_request.rb │ │ ├── web/ │ │ │ ├── push_subscription.rb │ │ │ └── setting.rb │ │ └── web.rb │ ├── policies/ │ │ ├── account_moderation_note_policy.rb │ │ ├── account_policy.rb │ │ ├── account_warning_preset_policy.rb │ │ ├── application_policy.rb │ │ ├── backup_policy.rb │ │ ├── custom_emoji_policy.rb │ │ ├── domain_block_policy.rb │ │ ├── email_domain_block_policy.rb │ │ ├── instance_policy.rb │ │ ├── invite_policy.rb │ │ ├── poll_policy.rb │ │ ├── relay_policy.rb │ │ ├── report_note_policy.rb │ │ ├── report_policy.rb │ │ ├── settings_policy.rb │ │ ├── status_policy.rb │ │ ├── subscription_policy.rb │ │ ├── tag_policy.rb │ │ └── user_policy.rb │ ├── presenters/ │ │ ├── account_relationships_presenter.rb │ │ ├── activitypub/ │ │ │ └── collection_presenter.rb │ │ ├── initial_state_presenter.rb │ │ ├── instance_presenter.rb │ │ └── status_relationships_presenter.rb │ ├── serializers/ │ │ ├── activitypub/ │ │ │ ├── accept_follow_serializer.rb │ │ │ ├── activity_serializer.rb │ │ │ ├── actor_serializer.rb │ │ │ ├── add_serializer.rb │ │ │ ├── block_serializer.rb │ │ │ ├── collection_serializer.rb │ │ │ ├── delete_actor_serializer.rb │ │ │ ├── delete_serializer.rb │ │ │ ├── emoji_serializer.rb │ │ │ ├── flag_serializer.rb │ │ │ ├── follow_serializer.rb │ │ │ ├── image_serializer.rb │ │ │ ├── like_serializer.rb │ │ │ ├── note_serializer.rb │ │ │ ├── outbox_serializer.rb │ │ │ ├── public_key_serializer.rb │ │ │ ├── reject_follow_serializer.rb │ │ │ ├── remove_serializer.rb │ │ │ ├── undo_announce_serializer.rb │ │ │ ├── undo_block_serializer.rb │ │ │ ├── undo_follow_serializer.rb │ │ │ ├── undo_like_serializer.rb │ │ │ ├── update_poll_serializer.rb │ │ │ ├── update_serializer.rb │ │ │ └── vote_serializer.rb │ │ ├── initial_state_serializer.rb │ │ ├── manifest_serializer.rb │ │ ├── oembed_serializer.rb │ │ ├── rest/ │ │ │ ├── account_serializer.rb │ │ │ ├── application_serializer.rb │ │ │ ├── context_serializer.rb │ │ │ ├── conversation_serializer.rb │ │ │ ├── credential_account_serializer.rb │ │ │ ├── custom_emoji_serializer.rb │ │ │ ├── filter_serializer.rb │ │ │ ├── identity_proof_serializer.rb │ │ │ ├── instance_serializer.rb │ │ │ ├── list_serializer.rb │ │ │ ├── media_attachment_serializer.rb │ │ │ ├── notification_serializer.rb │ │ │ ├── poll_serializer.rb │ │ │ ├── preferences_serializer.rb │ │ │ ├── preview_card_serializer.rb │ │ │ ├── relationship_serializer.rb │ │ │ ├── report_serializer.rb │ │ │ ├── scheduled_status_serializer.rb │ │ │ ├── search_serializer.rb │ │ │ ├── status_serializer.rb │ │ │ ├── tag_serializer.rb │ │ │ ├── v2/ │ │ │ │ └── search_serializer.rb │ │ │ └── web_push_subscription_serializer.rb │ │ ├── rss/ │ │ │ ├── account_serializer.rb │ │ │ └── tag_serializer.rb │ │ ├── web/ │ │ │ └── notification_serializer.rb │ │ └── webfinger_serializer.rb │ ├── services/ │ │ ├── account_search_service.rb │ │ ├── activitypub/ │ │ │ ├── fetch_featured_collection_service.rb │ │ │ ├── fetch_remote_account_service.rb │ │ │ ├── fetch_remote_key_service.rb │ │ │ ├── fetch_remote_poll_service.rb │ │ │ ├── fetch_remote_status_service.rb │ │ │ ├── fetch_replies_service.rb │ │ │ ├── process_account_service.rb │ │ │ ├── process_collection_service.rb │ │ │ └── process_poll_service.rb │ │ ├── after_block_domain_from_account_service.rb │ │ ├── after_block_service.rb │ │ ├── app_sign_up_service.rb │ │ ├── authorize_follow_service.rb │ │ ├── backup_service.rb │ │ ├── base_service.rb │ │ ├── batched_remove_status_service.rb │ │ ├── block_domain_service.rb │ │ ├── block_service.rb │ │ ├── bootstrap_timeline_service.rb │ │ ├── concerns/ │ │ │ ├── author_extractor.rb │ │ │ ├── payloadable.rb │ │ │ └── stream_entry_renderer.rb │ │ ├── fan_out_on_write_service.rb │ │ ├── favourite_service.rb │ │ ├── fetch_atom_service.rb │ │ ├── fetch_link_card_service.rb │ │ ├── fetch_oembed_service.rb │ │ ├── fetch_remote_account_service.rb │ │ ├── fetch_remote_status_service.rb │ │ ├── follow_service.rb │ │ ├── hashtag_query_service.rb │ │ ├── import_service.rb │ │ ├── mute_service.rb │ │ ├── notify_service.rb │ │ ├── post_status_service.rb │ │ ├── precompute_feed_service.rb │ │ ├── process_feed_service.rb │ │ ├── process_hashtags_service.rb │ │ ├── process_interaction_service.rb │ │ ├── process_mentions_service.rb │ │ ├── pubsubhubbub/ │ │ │ ├── subscribe_service.rb │ │ │ └── unsubscribe_service.rb │ │ ├── reblog_service.rb │ │ ├── reject_follow_service.rb │ │ ├── remove_status_service.rb │ │ ├── report_service.rb │ │ ├── resolve_account_service.rb │ │ ├── resolve_url_service.rb │ │ ├── search_service.rb │ │ ├── send_interaction_service.rb │ │ ├── subscribe_service.rb │ │ ├── suspend_account_service.rb │ │ ├── unblock_domain_service.rb │ │ ├── unblock_service.rb │ │ ├── unfavourite_service.rb │ │ ├── unfollow_service.rb │ │ ├── unmute_service.rb │ │ ├── unsubscribe_service.rb │ │ ├── update_account_service.rb │ │ ├── update_remote_profile_service.rb │ │ ├── verify_link_service.rb │ │ ├── verify_salmon_service.rb │ │ └── vote_service.rb │ ├── validators/ │ │ ├── blacklisted_email_validator.rb │ │ ├── disallowed_hashtags_validator.rb │ │ ├── email_mx_validator.rb │ │ ├── existing_username_validator.rb │ │ ├── follow_limit_validator.rb │ │ ├── html_validator.rb │ │ ├── note_length_validator.rb │ │ ├── poll_validator.rb │ │ ├── status_length_validator.rb │ │ ├── status_pin_validator.rb │ │ ├── unique_username_validator.rb │ │ ├── unreserved_username_validator.rb │ │ ├── url_validator.rb │ │ └── vote_validator.rb │ ├── views/ │ │ ├── about/ │ │ │ ├── _login.html.haml │ │ │ ├── _registration.html.haml │ │ │ ├── more.html.haml │ │ │ ├── show.html.haml │ │ │ └── terms.html.haml │ │ ├── accounts/ │ │ │ ├── _bio.html.haml │ │ │ ├── _header.html.haml │ │ │ ├── _moved.html.haml │ │ │ ├── _og.html.haml │ │ │ └── show.html.haml │ │ ├── admin/ │ │ │ ├── account_actions/ │ │ │ │ └── new.html.haml │ │ │ ├── account_moderation_notes/ │ │ │ │ └── _account_moderation_note.html.haml │ │ │ ├── account_warnings/ │ │ │ │ └── _account_warning.html.haml │ │ │ ├── accounts/ │ │ │ │ ├── _account.html.haml │ │ │ │ ├── index.html.haml │ │ │ │ └── show.html.haml │ │ │ ├── action_logs/ │ │ │ │ ├── _action_log.html.haml │ │ │ │ └── index.html.haml │ │ │ ├── change_emails/ │ │ │ │ └── show.html.haml │ │ │ ├── custom_emojis/ │ │ │ │ ├── _custom_emoji.html.haml │ │ │ │ ├── index.html.haml │ │ │ │ └── new.html.haml │ │ │ ├── dashboard/ │ │ │ │ └── index.html.haml │ │ │ ├── domain_blocks/ │ │ │ │ ├── new.html.haml │ │ │ │ └── show.html.haml │ │ │ ├── email_domain_blocks/ │ │ │ │ ├── _email_domain_block.html.haml │ │ │ │ ├── index.html.haml │ │ │ │ └── new.html.haml │ │ │ ├── followers/ │ │ │ │ └── index.html.haml │ │ │ ├── instances/ │ │ │ │ ├── index.html.haml │ │ │ │ └── show.html.haml │ │ │ ├── invites/ │ │ │ │ ├── _invite.html.haml │ │ │ │ └── index.html.haml │ │ │ ├── pending_accounts/ │ │ │ │ ├── _account.html.haml │ │ │ │ └── index.html.haml │ │ │ ├── relays/ │ │ │ │ ├── _relay.html.haml │ │ │ │ ├── index.html.haml │ │ │ │ └── new.html.haml │ │ │ ├── report_notes/ │ │ │ │ └── _report_note.html.haml │ │ │ ├── reports/ │ │ │ │ ├── _action_log.html.haml │ │ │ │ ├── _status.html.haml │ │ │ │ ├── index.html.haml │ │ │ │ └── show.html.haml │ │ │ ├── settings/ │ │ │ │ └── edit.html.haml │ │ │ ├── statuses/ │ │ │ │ ├── index.html.haml │ │ │ │ └── show.html.haml │ │ │ ├── subscriptions/ │ │ │ │ ├── _subscription.html.haml │ │ │ │ └── index.html.haml │ │ │ ├── tags/ │ │ │ │ ├── _tag.html.haml │ │ │ │ └── index.html.haml │ │ │ └── warning_presets/ │ │ │ ├── edit.html.haml │ │ │ └── index.html.haml │ │ ├── admin_mailer/ │ │ │ ├── new_pending_account.text.erb │ │ │ └── new_report.text.erb │ │ ├── application/ │ │ │ ├── _card.html.haml │ │ │ ├── _flashes.html.haml │ │ │ └── _sidebar.html.haml │ │ ├── auth/ │ │ │ ├── confirmations/ │ │ │ │ ├── finish_signup.html.haml │ │ │ │ └── new.html.haml │ │ │ ├── passwords/ │ │ │ │ ├── edit.html.haml │ │ │ │ └── new.html.haml │ │ │ ├── registrations/ │ │ │ │ ├── _sessions.html.haml │ │ │ │ ├── edit.html.haml │ │ │ │ └── new.html.haml │ │ │ ├── sessions/ │ │ │ │ ├── new.html.haml │ │ │ │ └── two_factor.html.haml │ │ │ └── shared/ │ │ │ └── _links.html.haml │ │ ├── authorize_interactions/ │ │ │ ├── _post_follow_actions.html.haml │ │ │ ├── error.html.haml │ │ │ ├── show.html.haml │ │ │ └── success.html.haml │ │ ├── directories/ │ │ │ └── index.html.haml │ │ ├── errors/ │ │ │ ├── 403.html.haml │ │ │ ├── 404.html.haml │ │ │ ├── 410.html.haml │ │ │ ├── 422.html.haml │ │ │ └── 500.html.haml │ │ ├── filters/ │ │ │ ├── _fields.html.haml │ │ │ ├── edit.html.haml │ │ │ ├── index.html.haml │ │ │ └── new.html.haml │ │ ├── follower_accounts/ │ │ │ └── index.html.haml │ │ ├── following_accounts/ │ │ │ └── index.html.haml │ │ ├── home/ │ │ │ └── index.html.haml │ │ ├── invites/ │ │ │ ├── _form.html.haml │ │ │ ├── _invite.html.haml │ │ │ └── index.html.haml │ │ ├── kaminari/ │ │ │ ├── _next_page.html.haml │ │ │ ├── _paginator.html.haml │ │ │ └── _prev_page.html.haml │ │ ├── layouts/ │ │ │ ├── admin.html.haml │ │ │ ├── application.html.haml │ │ │ ├── auth.html.haml │ │ │ ├── embedded.html.haml │ │ │ ├── error.html.haml │ │ │ ├── mailer.html.haml │ │ │ ├── mailer.text.erb │ │ │ ├── modal.html.haml │ │ │ ├── plain_mailer.html.haml │ │ │ └── public.html.haml │ │ ├── media/ │ │ │ └── player.html.haml │ │ ├── notification_mailer/ │ │ │ ├── _status.html.haml │ │ │ ├── _status.text.erb │ │ │ ├── digest.html.haml │ │ │ ├── digest.text.erb │ │ │ ├── favourite.html.haml │ │ │ ├── favourite.text.erb │ │ │ ├── follow.html.haml │ │ │ ├── follow.text.erb │ │ │ ├── follow_request.html.haml │ │ │ ├── follow_request.text.erb │ │ │ ├── mention.html.haml │ │ │ ├── mention.text.erb │ │ │ ├── reblog.html.haml │ │ │ └── reblog.text.erb │ │ ├── oauth/ │ │ │ ├── authorizations/ │ │ │ │ ├── error.html.haml │ │ │ │ ├── new.html.haml │ │ │ │ └── show.html.haml │ │ │ └── authorized_applications/ │ │ │ └── index.html.haml │ │ ├── public_timelines/ │ │ │ └── show.html.haml │ │ ├── relationships/ │ │ │ ├── _account.html.haml │ │ │ └── show.html.haml │ │ ├── remote_follow/ │ │ │ └── new.html.haml │ │ ├── remote_interaction/ │ │ │ └── new.html.haml │ │ ├── remote_unfollows/ │ │ │ ├── _card.html.haml │ │ │ ├── _post_follow_actions.html.haml │ │ │ ├── error.html.haml │ │ │ └── success.html.haml │ │ ├── settings/ │ │ │ ├── applications/ │ │ │ │ ├── _fields.html.haml │ │ │ │ ├── index.html.haml │ │ │ │ ├── new.html.haml │ │ │ │ └── show.html.haml │ │ │ ├── deletes/ │ │ │ │ └── show.html.haml │ │ │ ├── exports/ │ │ │ │ └── show.html.haml │ │ │ ├── featured_tags/ │ │ │ │ └── index.html.haml │ │ │ ├── identity_proofs/ │ │ │ │ ├── _proof.html.haml │ │ │ │ ├── index.html.haml │ │ │ │ └── new.html.haml │ │ │ ├── imports/ │ │ │ │ └── show.html.haml │ │ │ ├── migrations/ │ │ │ │ └── show.html.haml │ │ │ ├── preferences/ │ │ │ │ ├── appearance/ │ │ │ │ │ └── show.html.haml │ │ │ │ ├── notifications/ │ │ │ │ │ └── show.html.haml │ │ │ │ └── other/ │ │ │ │ └── show.html.haml │ │ │ ├── profiles/ │ │ │ │ └── show.html.haml │ │ │ ├── shared/ │ │ │ │ └── _links.html.haml │ │ │ ├── two_factor_authentication/ │ │ │ │ ├── confirmations/ │ │ │ │ │ └── new.html.haml │ │ │ │ └── recovery_codes/ │ │ │ │ └── index.html.haml │ │ │ └── two_factor_authentications/ │ │ │ └── show.html.haml │ │ ├── shared/ │ │ │ ├── _error_messages.html.haml │ │ │ └── _og.html.haml │ │ ├── shares/ │ │ │ └── show.html.haml │ │ ├── stream_entries/ │ │ │ ├── _attachment_list.html.haml │ │ │ ├── _detailed_status.html.haml │ │ │ ├── _og_description.html.haml │ │ │ ├── _og_image.html.haml │ │ │ ├── _poll.html.haml │ │ │ ├── _simple_status.html.haml │ │ │ ├── _status.html.haml │ │ │ ├── embed.html.haml │ │ │ └── show.html.haml │ │ ├── tags/ │ │ │ ├── _og.html.haml │ │ │ └── show.html.haml │ │ ├── user_mailer/ │ │ │ ├── backup_ready.html.haml │ │ │ ├── backup_ready.text.erb │ │ │ ├── confirmation_instructions.html.haml │ │ │ ├── confirmation_instructions.text.erb │ │ │ ├── email_changed.html.haml │ │ │ ├── email_changed.text.erb │ │ │ ├── password_change.html.haml │ │ │ ├── password_change.text.erb │ │ │ ├── reconfirmation_instructions.html.haml │ │ │ ├── reconfirmation_instructions.text.erb │ │ │ ├── reset_password_instructions.html.haml │ │ │ ├── reset_password_instructions.text.erb │ │ │ ├── warning.html.haml │ │ │ ├── warning.text.erb │ │ │ ├── welcome.html.haml │ │ │ └── welcome.text.erb │ │ └── well_known/ │ │ ├── host_meta/ │ │ │ └── show.xml.ruby │ │ └── webfinger/ │ │ └── show.xml.ruby │ └── workers/ │ ├── activitypub/ │ │ ├── delivery_worker.rb │ │ ├── distribute_poll_update_worker.rb │ │ ├── distribution_worker.rb │ │ ├── fetch_replies_worker.rb │ │ ├── low_priority_delivery_worker.rb │ │ ├── post_upgrade_worker.rb │ │ ├── processing_worker.rb │ │ ├── raw_distribution_worker.rb │ │ ├── reply_distribution_worker.rb │ │ ├── synchronize_featured_collection_worker.rb │ │ └── update_distribution_worker.rb │ ├── admin/ │ │ └── suspension_worker.rb │ ├── after_account_domain_block_worker.rb │ ├── after_remote_follow_request_worker.rb │ ├── after_remote_follow_worker.rb │ ├── authorize_follow_worker.rb │ ├── backup_worker.rb │ ├── block_worker.rb │ ├── bootstrap_timeline_worker.rb │ ├── concerns/ │ │ └── exponential_backoff.rb │ ├── digest_mailer_worker.rb │ ├── distribution_worker.rb │ ├── domain_block_worker.rb │ ├── feed_insert_worker.rb │ ├── fetch_reply_worker.rb │ ├── import/ │ │ └── relationship_worker.rb │ ├── import_worker.rb │ ├── link_crawl_worker.rb │ ├── local_notification_worker.rb │ ├── maintenance/ │ │ ├── destroy_media_worker.rb │ │ ├── redownload_account_media_worker.rb │ │ └── uncache_media_worker.rb │ ├── merge_worker.rb │ ├── mute_worker.rb │ ├── notification_worker.rb │ ├── poll_expiration_notify_worker.rb │ ├── processing_worker.rb │ ├── publish_scheduled_status_worker.rb │ ├── pubsubhubbub/ │ │ ├── confirmation_worker.rb │ │ ├── delivery_worker.rb │ │ ├── distribution_worker.rb │ │ ├── raw_distribution_worker.rb │ │ ├── subscribe_worker.rb │ │ └── unsubscribe_worker.rb │ ├── push_conversation_worker.rb │ ├── push_update_worker.rb │ ├── refollow_worker.rb │ ├── regeneration_worker.rb │ ├── remote_profile_update_worker.rb │ ├── removal_worker.rb │ ├── resolve_account_worker.rb │ ├── salmon_worker.rb │ ├── scheduler/ │ │ ├── backup_cleanup_scheduler.rb │ │ ├── doorkeeper_cleanup_scheduler.rb │ │ ├── email_scheduler.rb │ │ ├── feed_cleanup_scheduler.rb │ │ ├── ip_cleanup_scheduler.rb │ │ ├── media_cleanup_scheduler.rb │ │ ├── pghero_scheduler.rb │ │ ├── scheduled_statuses_scheduler.rb │ │ ├── subscriptions_cleanup_scheduler.rb │ │ ├── subscriptions_scheduler.rb │ │ └── user_cleanup_scheduler.rb │ ├── thread_resolve_worker.rb │ ├── unfavourite_worker.rb │ ├── unfollow_follow_worker.rb │ ├── unmerge_worker.rb │ ├── verify_account_links_worker.rb │ └── web/ │ └── push_notification_worker.rb ├── app.json ├── babel.config.js ├── bin/ │ ├── bundle │ ├── rails │ ├── rake │ ├── retry │ ├── rspec │ ├── setup │ ├── tootctl │ ├── update │ ├── webpack │ ├── webpack-dev-server │ └── yarn ├── boxfile.yml ├── config/ │ ├── application.rb │ ├── boot.rb │ ├── brakeman.ignore │ ├── database.yml │ ├── deploy.rb │ ├── environment.rb │ ├── environments/ │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ ├── i18n-tasks.yml │ ├── initializers/ │ │ ├── 0_post_deployment_migrations.rb │ │ ├── 1_hosts.rb │ │ ├── active_model_serializers.rb │ │ ├── application_controller_renderer.rb │ │ ├── assets.rb │ │ ├── backtrace_silencers.rb │ │ ├── blacklists.rb │ │ ├── chewy.rb │ │ ├── content_security_policy.rb │ │ ├── cookies_serializer.rb │ │ ├── cors.rb │ │ ├── delivery_job.rb │ │ ├── devise.rb │ │ ├── doorkeeper.rb │ │ ├── fast_blank.rb │ │ ├── ffmpeg.rb │ │ ├── filter_parameter_logging.rb │ │ ├── http_client_proxy.rb │ │ ├── httplog.rb │ │ ├── inflections.rb │ │ ├── instrumentation.rb │ │ ├── json_ld.rb │ │ ├── kaminari_config.rb │ │ ├── mime_types.rb │ │ ├── oj.rb │ │ ├── omniauth.rb │ │ ├── open_uri_redirection.rb │ │ ├── pagination.rb │ │ ├── paperclip.rb │ │ ├── premailer_rails.rb │ │ ├── rack_attack.rb │ │ ├── rack_attack_logging.rb │ │ ├── redis.rb │ │ ├── session_activations.rb │ │ ├── session_store.rb │ │ ├── sidekiq.rb │ │ ├── simple_form.rb │ │ ├── single_user_mode.rb │ │ ├── statsd.rb │ │ ├── stoplight.rb │ │ ├── strong_migrations.rb │ │ ├── suppress_csrf_warnings.rb │ │ ├── trusted_proxies.rb │ │ ├── twitter_regex.rb │ │ ├── vapid.rb │ │ └── wrap_parameters.rb │ ├── locales/ │ │ ├── activerecord.ar.yml │ │ ├── activerecord.ast.yml │ │ ├── activerecord.bg.yml │ │ ├── activerecord.bn.yml │ │ ├── activerecord.ca.yml │ │ ├── activerecord.co.yml │ │ ├── activerecord.cs.yml │ │ ├── activerecord.cy.yml │ │ ├── activerecord.da.yml │ │ ├── activerecord.de.yml │ │ ├── activerecord.el.yml │ │ ├── activerecord.en.yml │ │ ├── activerecord.eo.yml │ │ ├── activerecord.es.yml │ │ ├── activerecord.eu.yml │ │ ├── activerecord.fa.yml │ │ ├── activerecord.fi.yml │ │ ├── activerecord.fr.yml │ │ ├── activerecord.gl.yml │ │ ├── activerecord.he.yml │ │ ├── activerecord.hr.yml │ │ ├── activerecord.hu.yml │ │ ├── activerecord.hy.yml │ │ ├── activerecord.id.yml │ │ ├── activerecord.io.yml │ │ ├── activerecord.it.yml │ │ ├── activerecord.ja.yml │ │ ├── activerecord.ka.yml │ │ ├── activerecord.kk.yml │ │ ├── activerecord.ko.yml │ │ ├── activerecord.lt.yml │ │ ├── activerecord.lv.yml │ │ ├── activerecord.ms.yml │ │ ├── activerecord.nl.yml │ │ ├── activerecord.no.yml │ │ ├── activerecord.oc.yml │ │ ├── activerecord.pl.yml │ │ ├── activerecord.pt-BR.yml │ │ ├── activerecord.pt.yml │ │ ├── activerecord.ro.yml │ │ ├── activerecord.ru.yml │ │ ├── activerecord.sk.yml │ │ ├── activerecord.sl.yml │ │ ├── activerecord.sq.yml │ │ ├── activerecord.sr-Latn.yml │ │ ├── activerecord.sr.yml │ │ ├── activerecord.sv.yml │ │ ├── activerecord.ta.yml │ │ ├── activerecord.te.yml │ │ ├── activerecord.th.yml │ │ ├── activerecord.tr.yml │ │ ├── activerecord.uk.yml │ │ ├── activerecord.zh-CN.yml │ │ ├── activerecord.zh-HK.yml │ │ ├── activerecord.zh-TW.yml │ │ ├── activerecord.zh_Hant.yml │ │ ├── ar.yml │ │ ├── ast.yml │ │ ├── bg.yml │ │ ├── bn.yml │ │ ├── ca.yml │ │ ├── co.yml │ │ ├── cs.yml │ │ ├── cy.yml │ │ ├── da.yml │ │ ├── de.yml │ │ ├── devise.ar.yml │ │ ├── devise.ast.yml │ │ ├── devise.bg.yml │ │ ├── devise.bn.yml │ │ ├── devise.ca.yml │ │ ├── devise.co.yml │ │ ├── devise.cs.yml │ │ ├── devise.cy.yml │ │ ├── devise.da.yml │ │ ├── devise.de.yml │ │ ├── devise.el.yml │ │ ├── devise.en.yml │ │ ├── devise.eo.yml │ │ ├── devise.es.yml │ │ ├── devise.eu.yml │ │ ├── devise.fa.yml │ │ ├── devise.fi.yml │ │ ├── devise.fr.yml │ │ ├── devise.gl.yml │ │ ├── devise.he.yml │ │ ├── devise.hr.yml │ │ ├── devise.hu.yml │ │ ├── devise.hy.yml │ │ ├── devise.id.yml │ │ ├── devise.io.yml │ │ ├── devise.it.yml │ │ ├── devise.ja.yml │ │ ├── devise.ka.yml │ │ ├── devise.kk.yml │ │ ├── devise.ko.yml │ │ ├── devise.lt.yml │ │ ├── devise.lv.yml │ │ ├── devise.ms.yml │ │ ├── devise.nl.yml │ │ ├── devise.no.yml │ │ ├── devise.oc.yml │ │ ├── devise.pl.yml │ │ ├── devise.pt-BR.yml │ │ ├── devise.pt.yml │ │ ├── devise.ro.yml │ │ ├── devise.ru.yml │ │ ├── devise.sk.yml │ │ ├── devise.sl.yml │ │ ├── devise.sq.yml │ │ ├── devise.sr-Latn.yml │ │ ├── devise.sr.yml │ │ ├── devise.sv.yml │ │ ├── devise.ta.yml │ │ ├── devise.te.yml │ │ ├── devise.th.yml │ │ ├── devise.tr.yml │ │ ├── devise.uk.yml │ │ ├── devise.zh-CN.yml │ │ ├── devise.zh-HK.yml │ │ ├── devise.zh-TW.yml │ │ ├── doorkeeper.ar.yml │ │ ├── doorkeeper.ast.yml │ │ ├── doorkeeper.bg.yml │ │ ├── doorkeeper.bn.yml │ │ ├── doorkeeper.ca.yml │ │ ├── doorkeeper.co.yml │ │ ├── doorkeeper.cs.yml │ │ ├── doorkeeper.cy.yml │ │ ├── doorkeeper.da.yml │ │ ├── doorkeeper.de.yml │ │ ├── doorkeeper.el.yml │ │ ├── doorkeeper.en.yml │ │ ├── doorkeeper.eo.yml │ │ ├── doorkeeper.es.yml │ │ ├── doorkeeper.eu.yml │ │ ├── doorkeeper.fa.yml │ │ ├── doorkeeper.fi.yml │ │ ├── doorkeeper.fr.yml │ │ ├── doorkeeper.gl.yml │ │ ├── doorkeeper.he.yml │ │ ├── doorkeeper.hr.yml │ │ ├── doorkeeper.hu.yml │ │ ├── doorkeeper.hy.yml │ │ ├── doorkeeper.id.yml │ │ ├── doorkeeper.io.yml │ │ ├── doorkeeper.it.yml │ │ ├── doorkeeper.ja.yml │ │ ├── doorkeeper.ka.yml │ │ ├── doorkeeper.kk.yml │ │ ├── doorkeeper.ko.yml │ │ ├── doorkeeper.lt.yml │ │ ├── doorkeeper.lv.yml │ │ ├── doorkeeper.ms.yml │ │ ├── doorkeeper.nl.yml │ │ ├── doorkeeper.no.yml │ │ ├── doorkeeper.oc.yml │ │ ├── doorkeeper.pl.yml │ │ ├── doorkeeper.pt-BR.yml │ │ ├── doorkeeper.pt.yml │ │ ├── doorkeeper.ro.yml │ │ ├── doorkeeper.ru.yml │ │ ├── doorkeeper.sk.yml │ │ ├── doorkeeper.sl.yml │ │ ├── doorkeeper.sq.yml │ │ ├── doorkeeper.sr-Latn.yml │ │ ├── doorkeeper.sr.yml │ │ ├── doorkeeper.sv.yml │ │ ├── doorkeeper.ta.yml │ │ ├── doorkeeper.te.yml │ │ ├── doorkeeper.th.yml │ │ ├── doorkeeper.tr.yml │ │ ├── doorkeeper.uk.yml │ │ ├── doorkeeper.zh-CN.yml │ │ ├── doorkeeper.zh-HK.yml │ │ ├── doorkeeper.zh-TW.yml │ │ ├── el.yml │ │ ├── en.yml │ │ ├── en_GB.yml │ │ ├── eo.yml │ │ ├── es.yml │ │ ├── eu.yml │ │ ├── fa.yml │ │ ├── fi.yml │ │ ├── fr.yml │ │ ├── ga.yml │ │ ├── gl.yml │ │ ├── he.yml │ │ ├── hi.yml │ │ ├── hr.yml │ │ ├── hu.yml │ │ ├── hy.yml │ │ ├── id.yml │ │ ├── io.yml │ │ ├── it.yml │ │ ├── ja.yml │ │ ├── ka.yml │ │ ├── kk.yml │ │ ├── ko.yml │ │ ├── lt.yml │ │ ├── lv.yml │ │ ├── ms.yml │ │ ├── nl.yml │ │ ├── no.yml │ │ ├── oc.yml │ │ ├── pl.yml │ │ ├── pt-BR.yml │ │ ├── pt.yml │ │ ├── ro.yml │ │ ├── ru.yml │ │ ├── simple_form.ar.yml │ │ ├── simple_form.ast.yml │ │ ├── simple_form.bg.yml │ │ ├── simple_form.bn.yml │ │ ├── simple_form.ca.yml │ │ ├── simple_form.co.yml │ │ ├── simple_form.cs.yml │ │ ├── simple_form.cy.yml │ │ ├── simple_form.da.yml │ │ ├── simple_form.de.yml │ │ ├── simple_form.el.yml │ │ ├── simple_form.en.yml │ │ ├── simple_form.en_GB.yml │ │ ├── simple_form.eo.yml │ │ ├── simple_form.es.yml │ │ ├── simple_form.eu.yml │ │ ├── simple_form.fa.yml │ │ ├── simple_form.fi.yml │ │ ├── simple_form.fr.yml │ │ ├── simple_form.gl.yml │ │ ├── simple_form.he.yml │ │ ├── simple_form.hr.yml │ │ ├── simple_form.hu.yml │ │ ├── simple_form.hy.yml │ │ ├── simple_form.id.yml │ │ ├── simple_form.io.yml │ │ ├── simple_form.it.yml │ │ ├── simple_form.ja.yml │ │ ├── simple_form.ka.yml │ │ ├── simple_form.kk.yml │ │ ├── simple_form.ko.yml │ │ ├── simple_form.lt.yml │ │ ├── simple_form.lv.yml │ │ ├── simple_form.ms.yml │ │ ├── simple_form.nl.yml │ │ ├── simple_form.no.yml │ │ ├── simple_form.oc.yml │ │ ├── simple_form.pl.yml │ │ ├── simple_form.pt-BR.yml │ │ ├── simple_form.pt.yml │ │ ├── simple_form.ro.yml │ │ ├── simple_form.ru.yml │ │ ├── simple_form.sk.yml │ │ ├── simple_form.sl.yml │ │ ├── simple_form.sq.yml │ │ ├── simple_form.sr-Latn.yml │ │ ├── simple_form.sr.yml │ │ ├── simple_form.sv.yml │ │ ├── simple_form.ta.yml │ │ ├── simple_form.te.yml │ │ ├── simple_form.th.yml │ │ ├── simple_form.tr.yml │ │ ├── simple_form.uk.yml │ │ ├── simple_form.zh-CN.yml │ │ ├── simple_form.zh-HK.yml │ │ ├── simple_form.zh-TW.yml │ │ ├── sk.yml │ │ ├── sl.yml │ │ ├── sq.yml │ │ ├── sr-Latn.rb │ │ ├── sr-Latn.yml │ │ ├── sr.rb │ │ ├── sr.yml │ │ ├── sv.yml │ │ ├── ta.yml │ │ ├── te.yml │ │ ├── th.yml │ │ ├── tr.yml │ │ ├── uk.yml │ │ ├── zh-CN.yml │ │ ├── zh-HK.yml │ │ └── zh-TW.yml │ ├── navigation.rb │ ├── puma.rb │ ├── routes.rb │ ├── secrets.yml │ ├── settings.yml │ ├── sidekiq.yml │ ├── themes.yml │ ├── webpack/ │ │ ├── configuration.js │ │ ├── development.js │ │ ├── generateLocalePacks.js │ │ ├── production.js │ │ ├── rules/ │ │ │ ├── babel.js │ │ │ ├── css.js │ │ │ ├── file.js │ │ │ ├── index.js │ │ │ ├── mark.js │ │ │ └── node_modules.js │ │ ├── shared.js │ │ ├── test.js │ │ └── translationRunner.js │ └── webpacker.yml ├── config.ru ├── db/ │ ├── migrate/ │ │ ├── 20160220174730_create_accounts.rb │ │ ├── 20160220211917_create_statuses.rb │ │ ├── 20160221003140_create_users.rb │ │ ├── 20160221003621_create_follows.rb │ │ ├── 20160222122600_create_stream_entries.rb │ │ ├── 20160222143943_add_profile_fields_to_accounts.rb │ │ ├── 20160223162837_add_metadata_to_statuses.rb │ │ ├── 20160223164502_make_uris_nullable_in_statuses.rb │ │ ├── 20160223165723_add_url_to_statuses.rb │ │ ├── 20160223165855_add_url_to_accounts.rb │ │ ├── 20160223171800_create_favourites.rb │ │ ├── 20160224223247_create_mentions.rb │ │ ├── 20160227230233_add_attachment_avatar_to_accounts.rb │ │ ├── 20160305115639_add_devise_to_users.rb │ │ ├── 20160306172223_create_doorkeeper_tables.rb │ │ ├── 20160312193225_add_attachment_header_to_accounts.rb │ │ ├── 20160314164231_add_owner_to_application.rb │ │ ├── 20160316103650_add_missing_indices.rb │ │ ├── 20160322193748_add_avatar_remote_url_to_accounts.rb │ │ ├── 20160325130944_add_admin_to_users.rb │ │ ├── 20160826155805_add_superapp_to_oauth_applications.rb │ │ ├── 20160905150353_create_media_attachments.rb │ │ ├── 20160919221059_add_subscription_expires_at_to_accounts.rb │ │ ├── 20160920003904_remove_verify_token_from_accounts.rb │ │ ├── 20160926213048_remove_owner_from_application.rb │ │ ├── 20161003142332_add_confirmable_to_users.rb │ │ ├── 20161003145426_create_blocks.rb │ │ ├── 20161006213403_rails_settings_migration.rb │ │ ├── 20161009120834_create_domain_blocks.rb │ │ ├── 20161027172456_add_silenced_to_accounts.rb │ │ ├── 20161104173623_create_tags.rb │ │ ├── 20161105130633_create_statuses_tags_join_table.rb │ │ ├── 20161116162355_add_locale_to_users.rb │ │ ├── 20161119211120_create_notifications.rb │ │ ├── 20161122163057_remove_unneeded_indexes.rb │ │ ├── 20161123093447_add_sensitive_to_statuses.rb │ │ ├── 20161128103007_create_subscriptions.rb │ │ ├── 20161130142058_add_last_successful_delivery_at_to_subscriptions.rb │ │ ├── 20161130185319_add_visibility_to_statuses.rb │ │ ├── 20161202132159_add_in_reply_to_account_id_to_statuses.rb │ │ ├── 20161203164520_add_from_account_id_to_notifications.rb │ │ ├── 20161205214545_add_suspended_to_accounts.rb │ │ ├── 20161221152630_add_hidden_to_stream_entries.rb │ │ ├── 20161222201034_add_locked_to_accounts.rb │ │ ├── 20161222204147_create_follow_requests.rb │ │ ├── 20170105224407_add_shortcode_to_media_attachments.rb │ │ ├── 20170109120109_create_web_settings.rb │ │ ├── 20170112154826_migrate_settings.rb │ │ ├── 20170114194937_add_application_to_statuses.rb │ │ ├── 20170114203041_add_website_to_oauth_application.rb │ │ ├── 20170119214911_create_preview_cards.rb │ │ ├── 20170123162658_add_severity_to_domain_blocks.rb │ │ ├── 20170123203248_add_reject_media_to_domain_blocks.rb │ │ ├── 20170125145934_add_spoiler_text_to_statuses.rb │ │ ├── 20170127165745_add_devise_two_factor_to_users.rb │ │ ├── 20170129000348_create_devices.rb │ │ ├── 20170205175257_remove_devices.rb │ │ ├── 20170209184350_add_reply_to_statuses.rb │ │ ├── 20170214110202_create_reports.rb │ │ ├── 20170217012631_add_reblog_of_id_foreign_key_to_statuses.rb │ │ ├── 20170301222600_create_mutes.rb │ │ ├── 20170303212857_add_last_emailed_at_to_users.rb │ │ ├── 20170304202101_add_type_to_media_attachments.rb │ │ ├── 20170317193015_add_search_index_to_accounts.rb │ │ ├── 20170318214217_add_header_remote_url_to_accounts.rb │ │ ├── 20170322021028_add_lowercase_index_to_accounts.rb │ │ ├── 20170322143850_change_primary_key_to_bigint_on_statuses.rb │ │ ├── 20170322162804_add_search_index_to_tags.rb │ │ ├── 20170330021336_add_counter_caches.rb │ │ ├── 20170330163835_create_imports.rb │ │ ├── 20170330164118_add_attachment_data_to_imports.rb │ │ ├── 20170403172249_add_action_taken_by_account_id_to_reports.rb │ │ ├── 20170405112956_add_index_on_mentions_status_id.rb │ │ ├── 20170406215816_add_notifications_and_favourites_indices.rb │ │ ├── 20170409170753_add_last_webfingered_at_to_accounts.rb │ │ ├── 20170414080609_add_devise_two_factor_backupable_to_users.rb │ │ ├── 20170414132105_add_language_to_statuses.rb │ │ ├── 20170418160728_add_indexes_to_reports_for_accounts.rb │ │ ├── 20170423005413_add_allowed_languages_to_user.rb │ │ ├── 20170424003227_create_account_domain_blocks.rb │ │ ├── 20170424112722_add_status_id_index_to_statuses_tags.rb │ │ ├── 20170425131920_add_media_attachment_meta.rb │ │ ├── 20170425202925_add_oembed_to_preview_cards.rb │ │ ├── 20170427011934_re_add_owner_to_application.rb │ │ ├── 20170506235850_create_conversations.rb │ │ ├── 20170507000211_add_conversation_id_to_statuses.rb │ │ ├── 20170507141759_optimize_index_subscriptions.rb │ │ ├── 20170508230434_create_conversation_mutes.rb │ │ ├── 20170516072309_add_index_accounts_on_uri.rb │ │ ├── 20170520145338_change_language_filter_to_opt_out.rb │ │ ├── 20170601210557_add_index_on_media_attachments_account_id.rb │ │ ├── 20170604144747_add_foreign_keys_for_accounts.rb │ │ ├── 20170606113804_change_tag_search_index_to_btree.rb │ │ ├── 20170609145826_remove_default_language_from_statuses.rb │ │ ├── 20170610000000_add_statuses_index_on_account_id_id.rb │ │ ├── 20170623152212_create_session_activations.rb │ │ ├── 20170624134742_add_description_to_session_activations.rb │ │ ├── 20170625140443_add_access_token_id_to_session_activations.rb │ │ ├── 20170711225116_fix_null_booleans.rb │ │ ├── 20170713112503_make_tag_search_case_insensitive.rb │ │ ├── 20170713175513_create_web_push_subscriptions.rb │ │ ├── 20170713190709_add_web_push_subscription_to_session_activations.rb │ │ ├── 20170714184731_add_domain_to_subscriptions.rb │ │ ├── 20170716191202_add_hide_notifications_to_mute.rb │ │ ├── 20170718211102_add_activitypub_to_accounts.rb │ │ ├── 20170720000000_add_index_favourites_on_account_id_and_id.rb │ │ ├── 20170823162448_create_status_pins.rb │ │ ├── 20170824103029_add_timestamps_to_status_pins.rb │ │ ├── 20170829215220_remove_status_pins_account_index.rb │ │ ├── 20170901141119_truncate_preview_cards.rb │ │ ├── 20170901142658_create_join_table_preview_cards_statuses.rb │ │ ├── 20170905044538_add_index_id_account_id_activity_type_on_notifications.rb │ │ ├── 20170905165803_add_local_to_statuses.rb │ │ ├── 20170913000752_create_site_uploads.rb │ │ ├── 20170917153509_create_custom_emojis.rb │ │ ├── 20170918125918_ids_to_bigints.rb │ │ ├── 20170920024819_status_ids_to_timestamp_ids.rb │ │ ├── 20170920032311_fix_reblogs_in_feeds.rb │ │ ├── 20170924022025_ids_to_bigints2.rb │ │ ├── 20170927215609_add_description_to_media_attachments.rb │ │ ├── 20170928082043_create_email_domain_blocks.rb │ │ ├── 20171005102658_create_account_moderation_notes.rb │ │ ├── 20171005171936_add_disabled_to_custom_emojis.rb │ │ ├── 20171006142024_add_uri_to_custom_emojis.rb │ │ ├── 20171010023049_add_foreign_key_to_account_moderation_notes.rb │ │ ├── 20171010025614_change_accounts_nonnullable_in_account_moderation_notes.rb │ │ ├── 20171020084748_add_visible_in_picker_to_custom_emoji.rb │ │ ├── 20171028221157_add_reblogs_to_follows.rb │ │ ├── 20171107143332_add_memorial_to_accounts.rb │ │ ├── 20171107143624_add_disabled_to_users.rb │ │ ├── 20171109012327_add_moderator_to_accounts.rb │ │ ├── 20171114080328_add_index_domain_to_email_domain_blocks.rb │ │ ├── 20171114231651_create_lists.rb │ │ ├── 20171116161857_create_list_accounts.rb │ │ ├── 20171118012443_add_moved_to_account_id_to_accounts.rb │ │ ├── 20171119172437_create_admin_action_logs.rb │ │ ├── 20171122120436_add_index_account_and_reblog_of_id_to_statuses.rb │ │ ├── 20171125024930_create_invites.rb │ │ ├── 20171125031751_add_invite_id_to_users.rb │ │ ├── 20171125185353_add_index_reblog_of_id_and_account_to_statuses.rb │ │ ├── 20171125190735_remove_old_reblog_index_on_statuses.rb │ │ ├── 20171129172043_add_index_on_stream_entries.rb │ │ ├── 20171130000000_add_embed_url_to_preview_cards.rb │ │ ├── 20171201000000_change_account_id_nonnullable_in_lists.rb │ │ ├── 20171212195226_remove_duplicate_indexes_in_lists.rb │ │ ├── 20171226094803_more_faster_index_on_notifications.rb │ │ ├── 20180106000232_add_index_on_statuses_for_api_v1_accounts_account_id_statuses.rb │ │ ├── 20180109143959_add_remember_token_to_users.rb │ │ ├── 20180204034416_create_identities.rb │ │ ├── 20180206000000_change_user_id_nonnullable.rb │ │ ├── 20180211015820_create_backups.rb │ │ ├── 20180304013859_add_featured_collection_url_to_accounts.rb │ │ ├── 20180310000000_change_columns_in_notifications_nonnullable.rb │ │ ├── 20180402031200_add_assigned_account_id_to_reports.rb │ │ ├── 20180402040909_create_report_notes.rb │ │ ├── 20180410204633_add_fields_to_accounts.rb │ │ ├── 20180416210259_add_uri_to_relationships.rb │ │ ├── 20180506221944_add_actor_type_to_accounts.rb │ │ ├── 20180510214435_add_access_token_id_to_web_push_subscriptions.rb │ │ ├── 20180510230049_migrate_web_push_subscriptions.rb │ │ ├── 20180514130000_improve_index_on_statuses_for_api_v1_accounts_account_id_statuses.rb │ │ ├── 20180514140000_revert_index_change_on_statuses_for_api_v1_accounts_account_id_statuses.rb │ │ ├── 20180528141303_fix_accounts_unique_index.rb │ │ ├── 20180608213548_reject_following_blocked_users.rb │ │ ├── 20180609104432_migrate_web_push_subscriptions2.rb │ │ ├── 20180615122121_add_autofollow_to_invites.rb │ │ ├── 20180616192031_add_chosen_languages_to_users.rb │ │ ├── 20180617162849_remove_unused_indexes.rb │ │ ├── 20180628181026_create_custom_filters.rb │ │ ├── 20180707154237_add_whole_word_to_custom_filter.rb │ │ ├── 20180711152640_create_relays.rb │ │ ├── 20180808175627_create_account_pins.rb │ │ ├── 20180812123222_change_relays_enabled.rb │ │ ├── 20180812162710_create_status_stats.rb │ │ ├── 20180812173710_copy_status_stats.rb │ │ ├── 20180814171349_add_confidential_to_doorkeeper_application.rb │ │ ├── 20180820232245_add_foreign_key_indices.rb │ │ ├── 20180929222014_create_account_conversations.rb │ │ ├── 20181007025445_create_pghero_space_stats.rb │ │ ├── 20181010141500_add_silent_to_mentions.rb │ │ ├── 20181017170937_add_reject_reports_to_domain_blocks.rb │ │ ├── 20181018205649_add_unread_to_account_conversations.rb │ │ ├── 20181024224956_migrate_account_conversations.rb │ │ ├── 20181026034033_remove_faux_remote_account_duplicates.rb │ │ ├── 20181116165755_create_account_stats.rb │ │ ├── 20181116173541_copy_account_stats.rb │ │ ├── 20181127130500_identity_id_to_bigint.rb │ │ ├── 20181203003808_create_accounts_tags_join_table.rb │ │ ├── 20181203021853_add_discoverable_to_accounts.rb │ │ ├── 20181204193439_add_last_status_at_to_account_stats.rb │ │ ├── 20181204215309_create_account_tag_stats.rb │ │ ├── 20181207011115_downcase_custom_emoji_domains.rb │ │ ├── 20181213184704_create_account_warnings.rb │ │ ├── 20181213185533_create_account_warning_presets.rb │ │ ├── 20181219235220_add_created_by_application_id_to_users.rb │ │ ├── 20181226021420_add_also_known_as_to_accounts.rb │ │ ├── 20190103124649_create_scheduled_statuses.rb │ │ ├── 20190103124754_add_scheduled_status_id_to_media_attachments.rb │ │ ├── 20190117114553_create_tombstones.rb │ │ ├── 20190201012802_add_overwrite_to_imports.rb │ │ ├── 20190203180359_create_featured_tags.rb │ │ ├── 20190225031541_create_polls.rb │ │ ├── 20190225031625_create_poll_votes.rb │ │ ├── 20190226003449_add_poll_id_to_statuses.rb │ │ ├── 20190304152020_add_uri_to_poll_votes.rb │ │ ├── 20190306145741_add_lock_version_to_polls.rb │ │ ├── 20190307234537_add_approved_to_users.rb │ │ ├── 20190314181829_migrate_open_registrations_setting.rb │ │ ├── 20190316190352_create_account_identity_proofs.rb │ │ ├── 20190317135723_add_uri_to_reports.rb │ │ ├── 20190409054914_create_user_invite_requests.rb │ │ ├── 20190420025523_add_blurhash_to_media_attachments.rb │ │ ├── 20190509164208_add_by_moderator_to_tombstone.rb │ │ ├── 20190511134027_add_silenced_at_suspended_at_to_accounts.rb │ │ └── 20190529143559_preserve_old_layout_for_existing_users.rb │ ├── post_migrate/ │ │ ├── .gitkeep │ │ ├── 20180813113448_copy_status_stats_cleanup.rb │ │ ├── 20181116184611_copy_account_stats_cleanup.rb │ │ ├── 20190511152737_remove_suspended_silenced_account_fields.rb │ │ └── 20190519130537_remove_boosts_widening_audience.rb │ ├── schema.rb │ └── seeds.rb ├── dist/ │ ├── mastodon-sidekiq.service │ ├── mastodon-streaming.service │ ├── mastodon-web.service │ └── nginx.conf ├── docker-compose.yml ├── lib/ │ ├── assets/ │ │ └── .keep │ ├── cli.rb │ ├── devise/ │ │ └── ldap_authenticatable.rb │ ├── florence/ │ │ └── version.rb │ ├── generators/ │ │ └── post_deployment_migration_generator.rb │ ├── json_ld/ │ │ └── security.rb │ ├── mastodon/ │ │ ├── accounts_cli.rb │ │ ├── cache_cli.rb │ │ ├── cli_helper.rb │ │ ├── domains_cli.rb │ │ ├── emoji_cli.rb │ │ ├── feeds_cli.rb │ │ ├── media_cli.rb │ │ ├── migration_helpers.rb │ │ ├── premailer_webpack_strategy.rb │ │ ├── redis_config.rb │ │ ├── search_cli.rb │ │ ├── settings_cli.rb │ │ ├── snowflake.rb │ │ ├── statuses_cli.rb │ │ └── version.rb │ ├── paperclip/ │ │ ├── blurhash_transcoder.rb │ │ ├── gif_transcoder.rb │ │ ├── lazy_thumbnail.rb │ │ └── video_transcoder.rb │ ├── tasks/ │ │ ├── assets.rake │ │ ├── auto_annotate_models.rake │ │ ├── db.rake │ │ ├── emojis.rake │ │ ├── mastodon.rake │ │ ├── repo.rake │ │ └── statistics.rake │ └── templates/ │ ├── haml/ │ │ └── scaffold/ │ │ └── _form.html.haml │ └── rails/ │ └── post_deployment_migration/ │ └── migration.rb ├── log/ │ └── .keep ├── nanobox/ │ ├── nginx-local.conf │ ├── nginx-stream.conf.erb │ └── nginx-web.conf.erb ├── package.json ├── postcss.config.js ├── priv-config ├── public/ │ ├── browserconfig.xml │ ├── embed.js │ ├── robots.txt │ └── sounds/ │ └── boop.ogg ├── scalingo.json ├── spec/ │ ├── controllers/ │ │ ├── about_controller_spec.rb │ │ ├── account_follow_controller_spec.rb │ │ ├── account_unfollow_controller_spec.rb │ │ ├── accounts_controller_spec.rb │ │ ├── activitypub/ │ │ │ ├── collections_controller_spec.rb │ │ │ ├── inboxes_controller_spec.rb │ │ │ └── outboxes_controller_spec.rb │ │ ├── admin/ │ │ │ ├── account_moderation_notes_controller_spec.rb │ │ │ ├── accounts_controller_spec.rb │ │ │ ├── action_logs_controller_spec.rb │ │ │ ├── base_controller_spec.rb │ │ │ ├── change_email_controller_spec.rb │ │ │ ├── confirmations_controller_spec.rb │ │ │ ├── custom_emojis_controller_spec.rb │ │ │ ├── dashboard_controller_spec.rb │ │ │ ├── domain_blocks_controller_spec.rb │ │ │ ├── email_domain_blocks_controller_spec.rb │ │ │ ├── instances_controller_spec.rb │ │ │ ├── invites_controller_spec.rb │ │ │ ├── report_notes_controller_spec.rb │ │ │ ├── reported_statuses_controller_spec.rb │ │ │ ├── reports_controller_spec.rb │ │ │ ├── resets_controller_spec.rb │ │ │ ├── roles_controller_spec.rb │ │ │ ├── settings_controller_spec.rb │ │ │ ├── statuses_controller_spec.rb │ │ │ ├── subscriptions_controller_spec.rb │ │ │ ├── tags_controller_spec.rb │ │ │ └── two_factor_authentications_controller_spec.rb │ │ ├── api/ │ │ │ ├── base_controller_spec.rb │ │ │ ├── oembed_controller_spec.rb │ │ │ ├── proofs_controller_spec.rb │ │ │ ├── push_controller_spec.rb │ │ │ ├── salmon_controller_spec.rb │ │ │ ├── subscriptions_controller_spec.rb │ │ │ ├── v1/ │ │ │ │ ├── accounts/ │ │ │ │ │ ├── credentials_controller_spec.rb │ │ │ │ │ ├── follower_accounts_controller_spec.rb │ │ │ │ │ ├── following_accounts_controller_spec.rb │ │ │ │ │ ├── lists_controller_spec.rb │ │ │ │ │ ├── pins_controller_spec.rb │ │ │ │ │ ├── relationships_controller_spec.rb │ │ │ │ │ ├── search_controller_spec.rb │ │ │ │ │ └── statuses_controller_spec.rb │ │ │ │ ├── accounts_controller_spec.rb │ │ │ │ ├── apps/ │ │ │ │ │ └── credentials_controller_spec.rb │ │ │ │ ├── apps_controller_spec.rb │ │ │ │ ├── blocks_controller_spec.rb │ │ │ │ ├── conversations_controller_spec.rb │ │ │ │ ├── custom_emojis_controller_spec.rb │ │ │ │ ├── domain_blocks_controller_spec.rb │ │ │ │ ├── endorsements_controller_spec.rb │ │ │ │ ├── favourites_controller_spec.rb │ │ │ │ ├── filters_controller_spec.rb │ │ │ │ ├── follow_requests_controller_spec.rb │ │ │ │ ├── follows_controller_spec.rb │ │ │ │ ├── instances/ │ │ │ │ │ ├── activity_controller_spec.rb │ │ │ │ │ └── peers_controller_spec.rb │ │ │ │ ├── instances_controller_spec.rb │ │ │ │ ├── lists/ │ │ │ │ │ └── accounts_controller_spec.rb │ │ │ │ ├── lists_controller_spec.rb │ │ │ │ ├── media_controller_spec.rb │ │ │ │ ├── mutes_controller_spec.rb │ │ │ │ ├── notifications_controller_spec.rb │ │ │ │ ├── polls/ │ │ │ │ │ └── votes_controller_spec.rb │ │ │ │ ├── polls_controller_spec.rb │ │ │ │ ├── push/ │ │ │ │ │ └── subscriptions_controller_spec.rb │ │ │ │ ├── reports_controller_spec.rb │ │ │ │ ├── search_controller_spec.rb │ │ │ │ ├── statuses/ │ │ │ │ │ ├── favourited_by_accounts_controller_spec.rb │ │ │ │ │ ├── favourites_controller_spec.rb │ │ │ │ │ ├── mutes_controller_spec.rb │ │ │ │ │ ├── pins_controller_spec.rb │ │ │ │ │ ├── reblogged_by_accounts_controller_spec.rb │ │ │ │ │ └── reblogs_controller_spec.rb │ │ │ │ ├── statuses_controller_spec.rb │ │ │ │ ├── streaming_controller_spec.rb │ │ │ │ ├── suggestions_controller_spec.rb │ │ │ │ └── timelines/ │ │ │ │ ├── direct_controller_spec.rb │ │ │ │ ├── home_controller_spec.rb │ │ │ │ ├── list_controller_spec.rb │ │ │ │ ├── public_controller_spec.rb │ │ │ │ └── tag_controller_spec.rb │ │ │ ├── v2/ │ │ │ │ └── search_controller_spec.rb │ │ │ └── web/ │ │ │ ├── embeds_controller_spec.rb │ │ │ ├── push_subscriptions_controller_spec.rb │ │ │ └── settings_controller_spec.rb │ │ ├── application_controller_spec.rb │ │ ├── auth/ │ │ │ ├── confirmations_controller_spec.rb │ │ │ ├── passwords_controller_spec.rb │ │ │ ├── registrations_controller_spec.rb │ │ │ └── sessions_controller_spec.rb │ │ ├── authorize_interactions_controller_spec.rb │ │ ├── concerns/ │ │ │ ├── account_controller_concern_spec.rb │ │ │ ├── accountable_concern_spec.rb │ │ │ ├── export_controller_concern_spec.rb │ │ │ ├── localized_spec.rb │ │ │ ├── obfuscate_filename_spec.rb │ │ │ ├── rate_limit_headers_spec.rb │ │ │ ├── signature_verification_spec.rb │ │ │ └── user_tracking_concern_spec.rb │ │ ├── emojis_controller_spec.rb │ │ ├── follower_accounts_controller_spec.rb │ │ ├── following_accounts_controller_spec.rb │ │ ├── home_controller_spec.rb │ │ ├── intents_controller_spec.rb │ │ ├── invites_controller_spec.rb │ │ ├── manifests_controller_spec.rb │ │ ├── media_controller_spec.rb │ │ ├── oauth/ │ │ │ ├── authorizations_controller_spec.rb │ │ │ ├── authorized_applications_controller_spec.rb │ │ │ └── tokens_controller_spec.rb │ │ ├── relationships_controller_spec.rb │ │ ├── remote_follow_controller_spec.rb │ │ ├── remote_interaction_controller_spec.rb │ │ ├── remote_unfollows_controller_spec.rb │ │ ├── settings/ │ │ │ ├── applications_controller_spec.rb │ │ │ ├── deletes_controller_spec.rb │ │ │ ├── exports/ │ │ │ │ ├── blocked_accounts_controller_spec.rb │ │ │ │ ├── following_accounts_controller_spec.rb │ │ │ │ └── muted_accounts_controller_spec.rb │ │ │ ├── exports_controller_spec.rb │ │ │ ├── identity_proofs_controller_spec.rb │ │ │ ├── imports_controller_spec.rb │ │ │ ├── migrations_controller_spec.rb │ │ │ ├── preferences/ │ │ │ │ ├── notifications_controller_spec.rb │ │ │ │ └── other_controller_spec.rb │ │ │ ├── profiles_controller_spec.rb │ │ │ ├── sessions_controller_spec.rb │ │ │ ├── two_factor_authentication/ │ │ │ │ ├── confirmations_controller_spec.rb │ │ │ │ └── recovery_codes_controller_spec.rb │ │ │ └── two_factor_authentications_controller_spec.rb │ │ ├── shares_controller_spec.rb │ │ ├── statuses_controller_spec.rb │ │ ├── stream_entries_controller_spec.rb │ │ ├── tags_controller_spec.rb │ │ └── well_known/ │ │ ├── host_meta_controller_spec.rb │ │ ├── keybase_proof_config_controller_spec.rb │ │ └── webfinger_controller_spec.rb │ ├── fabricators/ │ │ ├── access_token_fabricator.rb │ │ ├── accessible_access_token_fabricator.rb │ │ ├── account_domain_block_fabricator.rb │ │ ├── account_fabricator.rb │ │ ├── account_identity_proof_fabricator.rb │ │ ├── account_moderation_note_fabricator.rb │ │ ├── account_pin_fabricator.rb │ │ ├── account_stat_fabricator.rb │ │ ├── account_tag_stat_fabricator.rb │ │ ├── account_warning_fabricator.rb │ │ ├── account_warning_preset_fabricator.rb │ │ ├── admin_action_log_fabricator.rb │ │ ├── application_fabricator.rb │ │ ├── assets/ │ │ │ └── TEAPOT │ │ ├── backup_fabricator.rb │ │ ├── block_fabricator.rb │ │ ├── conversation_account_fabricator.rb │ │ ├── conversation_fabricator.rb │ │ ├── conversation_mute_fabricator.rb │ │ ├── custom_emoji_fabricator.rb │ │ ├── custom_filter_fabricator.rb │ │ ├── domain_block_fabricator.rb │ │ ├── email_domain_block_fabricator.rb │ │ ├── favourite_fabricator.rb │ │ ├── featured_tag_fabricator.rb │ │ ├── follow_fabricator.rb │ │ ├── follow_request_fabricator.rb │ │ ├── identity_fabricator.rb │ │ ├── import_fabricator.rb │ │ ├── invite_fabricator.rb │ │ ├── list_account_fabricator.rb │ │ ├── list_fabricator.rb │ │ ├── media_attachment_fabricator.rb │ │ ├── mention_fabricator.rb │ │ ├── mute_fabricator.rb │ │ ├── notification_fabricator.rb │ │ ├── poll_fabricator.rb │ │ ├── poll_vote_fabricator.rb │ │ ├── relay_fabricator.rb │ │ ├── report_fabricator.rb │ │ ├── report_note_fabricator.rb │ │ ├── scheduled_status_fabricator.rb │ │ ├── session_activation_fabricator.rb │ │ ├── setting_fabricator.rb │ │ ├── site_upload_fabricator.rb │ │ ├── status_fabricator.rb │ │ ├── status_pin_fabricator.rb │ │ ├── status_stat_fabricator.rb │ │ ├── stream_entry_fabricator.rb │ │ ├── subscription_fabricator.rb │ │ ├── tag_fabricator.rb │ │ ├── user_fabricator.rb │ │ ├── user_invite_request_fabricator.rb │ │ ├── web_push_subscription_fabricator.rb │ │ └── web_setting_fabricator.rb │ ├── features/ │ │ └── log_in_spec.rb │ ├── fixtures/ │ │ ├── files/ │ │ │ ├── attachment.webm │ │ │ ├── imports.txt │ │ │ ├── mute-imports.txt │ │ │ ├── new-following-imports.txt │ │ │ └── new-mute-imports.txt │ │ ├── push/ │ │ │ ├── feed.atom │ │ │ └── reblog.atom │ │ ├── requests/ │ │ │ ├── .host-meta.txt │ │ │ ├── activitypub-actor-individual.txt │ │ │ ├── activitypub-actor-noinbox.txt │ │ │ ├── activitypub-actor.txt │ │ │ ├── activitypub-feed.txt │ │ │ ├── activitypub-webfinger.txt │ │ │ ├── attachment1.txt │ │ │ ├── attachment2.txt │ │ │ ├── avatar.txt │ │ │ ├── feed.txt │ │ │ ├── idn.txt │ │ │ ├── json-ld.activitystreams.txt │ │ │ ├── json-ld.identity.txt │ │ │ ├── json-ld.security.txt │ │ │ ├── koi8-r.txt │ │ │ ├── localdomain-feed.txt │ │ │ ├── localdomain-hostmeta.txt │ │ │ ├── localdomain-webfinger.txt │ │ │ ├── oembed_invalid_xml.html │ │ │ ├── oembed_json.html │ │ │ ├── oembed_json_empty.html │ │ │ ├── oembed_json_xml.html │ │ │ ├── oembed_undiscoverable.html │ │ │ ├── oembed_xml.html │ │ │ ├── redirected.host-meta.txt │ │ │ ├── sjis.txt │ │ │ ├── sjis_with_wrong_charset.txt │ │ │ ├── webfinger-hacker1.txt │ │ │ ├── webfinger-hacker2.txt │ │ │ ├── webfinger-hacker3.txt │ │ │ ├── webfinger.txt │ │ │ └── windows-1251.txt │ │ ├── salmon/ │ │ │ └── mention.xml │ │ └── xml/ │ │ └── mastodon.atom │ ├── helpers/ │ │ ├── admin/ │ │ │ ├── account_moderation_notes_helper_spec.rb │ │ │ ├── action_log_helper_spec.rb │ │ │ └── filter_helper_spec.rb │ │ ├── application_helper_spec.rb │ │ ├── flashes_helper_spec.rb │ │ ├── home_helper_spec.rb │ │ ├── instance_helper_spec.rb │ │ ├── jsonld_helper_spec.rb │ │ ├── routing_helper_spec.rb │ │ ├── settings_helper_spec.rb │ │ └── stream_entries_helper_spec.rb │ ├── lib/ │ │ ├── activitypub/ │ │ │ ├── activity/ │ │ │ │ ├── accept_spec.rb │ │ │ │ ├── add_spec.rb │ │ │ │ ├── announce_spec.rb │ │ │ │ ├── block_spec.rb │ │ │ │ ├── create_spec.rb │ │ │ │ ├── delete_spec.rb │ │ │ │ ├── flag_spec.rb │ │ │ │ ├── follow_spec.rb │ │ │ │ ├── like_spec.rb │ │ │ │ ├── move_spec.rb │ │ │ │ ├── reject_spec.rb │ │ │ │ ├── remove_spec.rb │ │ │ │ ├── undo_spec.rb │ │ │ │ └── update_spec.rb │ │ │ ├── adapter_spec.rb │ │ │ ├── linked_data_signature_spec.rb │ │ │ └── tag_manager_spec.rb │ │ ├── delivery_failure_tracker_spec.rb │ │ ├── extractor_spec.rb │ │ ├── feed_manager_spec.rb │ │ ├── formatter_spec.rb │ │ ├── hash_object_spec.rb │ │ ├── language_detector_spec.rb │ │ ├── ostatus/ │ │ │ ├── atom_serializer_spec.rb │ │ │ └── tag_manager_spec.rb │ │ ├── proof_provider/ │ │ │ └── keybase/ │ │ │ └── verifier_spec.rb │ │ ├── request_spec.rb │ │ ├── settings/ │ │ │ ├── extend_spec.rb │ │ │ └── scoped_settings_spec.rb │ │ ├── status_filter_spec.rb │ │ ├── status_finder_spec.rb │ │ ├── tag_manager_spec.rb │ │ ├── user_settings_decorator_spec.rb │ │ └── webfinger_resource_spec.rb │ ├── mailers/ │ │ ├── admin_mailer_spec.rb │ │ ├── notification_mailer_spec.rb │ │ ├── previews/ │ │ │ ├── admin_mailer_preview.rb │ │ │ ├── notification_mailer_preview.rb │ │ │ └── user_mailer_preview.rb │ │ └── user_mailer_spec.rb │ ├── models/ │ │ ├── account_conversation_spec.rb │ │ ├── account_domain_block_spec.rb │ │ ├── account_filter_spec.rb │ │ ├── account_moderation_note_spec.rb │ │ ├── account_spec.rb │ │ ├── account_stat_spec.rb │ │ ├── account_tag_stat_spec.rb │ │ ├── admin/ │ │ │ ├── account_action_spec.rb │ │ │ └── action_log_spec.rb │ │ ├── backup_spec.rb │ │ ├── block_spec.rb │ │ ├── concerns/ │ │ │ ├── account_finder_concern_spec.rb │ │ │ ├── account_interactions_spec.rb │ │ │ ├── remotable_spec.rb │ │ │ ├── status_threading_concern_spec.rb │ │ │ └── streamable_spec.rb │ │ ├── conversation_mute_spec.rb │ │ ├── conversation_spec.rb │ │ ├── custom_emoji_filter_spec.rb │ │ ├── custom_emoji_spec.rb │ │ ├── custom_filter_spec.rb │ │ ├── domain_block_spec.rb │ │ ├── email_domain_block_spec.rb │ │ ├── export_spec.rb │ │ ├── favourite_spec.rb │ │ ├── featured_tag_spec.rb │ │ ├── follow_request_spec.rb │ │ ├── follow_spec.rb │ │ ├── form/ │ │ │ └── status_batch_spec.rb │ │ ├── home_feed_spec.rb │ │ ├── identity_spec.rb │ │ ├── import_spec.rb │ │ ├── invite_spec.rb │ │ ├── list_account_spec.rb │ │ ├── list_spec.rb │ │ ├── media_attachment_spec.rb │ │ ├── mention_spec.rb │ │ ├── mute_spec.rb │ │ ├── notification_spec.rb │ │ ├── poll_spec.rb │ │ ├── poll_vote_spec.rb │ │ ├── preview_card_spec.rb │ │ ├── relay_spec.rb │ │ ├── remote_follow_spec.rb │ │ ├── remote_profile_spec.rb │ │ ├── report_filter_spec.rb │ │ ├── report_spec.rb │ │ ├── scheduled_status_spec.rb │ │ ├── session_activation_spec.rb │ │ ├── setting_spec.rb │ │ ├── site_upload_spec.rb │ │ ├── status_pin_spec.rb │ │ ├── status_spec.rb │ │ ├── status_stat_spec.rb │ │ ├── stream_entry_spec.rb │ │ ├── subscription_spec.rb │ │ ├── tag_spec.rb │ │ ├── user_invite_request_spec.rb │ │ ├── user_spec.rb │ │ └── web/ │ │ ├── push_subscription_spec.rb │ │ └── setting_spec.rb │ ├── policies/ │ │ ├── account_moderation_note_policy_spec.rb │ │ ├── account_policy_spec.rb │ │ ├── backup_policy_spec.rb │ │ ├── custom_emoji_policy_spec.rb │ │ ├── domain_block_policy_spec.rb │ │ ├── email_domain_block_policy_spec.rb │ │ ├── instance_policy_spec.rb │ │ ├── invite_policy_spec.rb │ │ ├── relay_policy_spec.rb │ │ ├── report_note_policy_spec.rb │ │ ├── report_policy_spec.rb │ │ ├── settings_policy_spec.rb │ │ ├── status_policy_spec.rb │ │ ├── subscription_policy_spec.rb │ │ ├── tag_policy_spec.rb │ │ └── user_policy_spec.rb │ ├── presenters/ │ │ ├── account_relationships_presenter_spec.rb │ │ └── instance_presenter_spec.rb │ ├── rails_helper.rb │ ├── requests/ │ │ ├── account_show_page_spec.rb │ │ ├── catch_all_route_request_spec.rb │ │ ├── host_meta_request_spec.rb │ │ ├── link_headers_spec.rb │ │ ├── localization_spec.rb │ │ └── webfinger_request_spec.rb │ ├── routing/ │ │ ├── accounts_routing_spec.rb │ │ ├── api_routing_spec.rb │ │ └── well_known_routes_spec.rb │ ├── serializers/ │ │ └── activitypub/ │ │ └── note_spec.rb │ ├── services/ │ │ ├── account_search_service_spec.rb │ │ ├── activitypub/ │ │ │ ├── fetch_remote_account_service_spec.rb │ │ │ ├── fetch_remote_status_service_spec.rb │ │ │ ├── fetch_replies_service_spec.rb │ │ │ ├── process_account_service_spec.rb │ │ │ └── process_collection_service_spec.rb │ │ ├── after_block_domain_from_account_service_spec.rb │ │ ├── after_block_service_spec.rb │ │ ├── app_sign_up_service_spec.rb │ │ ├── authorize_follow_service_spec.rb │ │ ├── batched_remove_status_service_spec.rb │ │ ├── block_domain_service_spec.rb │ │ ├── block_service_spec.rb │ │ ├── bootstrap_timeline_service_spec.rb │ │ ├── fan_out_on_write_service_spec.rb │ │ ├── favourite_service_spec.rb │ │ ├── fetch_atom_service_spec.rb │ │ ├── fetch_link_card_service_spec.rb │ │ ├── fetch_oembed_service_spec.rb │ │ ├── fetch_remote_account_service_spec.rb │ │ ├── fetch_remote_status_service_spec.rb │ │ ├── follow_service_spec.rb │ │ ├── hashtag_query_service_spec.rb │ │ ├── import_service_spec.rb │ │ ├── mute_service_spec.rb │ │ ├── notify_service_spec.rb │ │ ├── post_status_service_spec.rb │ │ ├── precompute_feed_service_spec.rb │ │ ├── process_feed_service_spec.rb │ │ ├── process_interaction_service_spec.rb │ │ ├── process_mentions_service_spec.rb │ │ ├── pubsubhubbub/ │ │ │ ├── subscribe_service_spec.rb │ │ │ └── unsubscribe_service_spec.rb │ │ ├── reblog_service_spec.rb │ │ ├── reject_follow_service_spec.rb │ │ ├── remove_status_service_spec.rb │ │ ├── report_service_spec.rb │ │ ├── resolve_account_service_spec.rb │ │ ├── resolve_url_service_spec.rb │ │ ├── search_service_spec.rb │ │ ├── send_interaction_service_spec.rb │ │ ├── subscribe_service_spec.rb │ │ ├── suspend_account_service_spec.rb │ │ ├── unblock_domain_service_spec.rb │ │ ├── unblock_service_spec.rb │ │ ├── unfollow_service_spec.rb │ │ ├── unmute_service_spec.rb │ │ ├── unsubscribe_service_spec.rb │ │ ├── update_remote_profile_service_spec.rb │ │ └── verify_link_service_spec.rb │ ├── spec_helper.rb │ ├── support/ │ │ ├── examples/ │ │ │ ├── lib/ │ │ │ │ └── settings/ │ │ │ │ ├── scoped_settings.rb │ │ │ │ └── settings_extended.rb │ │ │ └── models/ │ │ │ └── concerns/ │ │ │ └── account_avatar.rb │ │ └── matchers/ │ │ └── model/ │ │ └── model_have_error_on_field.rb │ ├── validators/ │ │ ├── blacklisted_email_validator_spec.rb │ │ ├── disallowed_hashtags_validator_spec.rb │ │ ├── email_mx_validator_spec.rb │ │ ├── follow_limit_validator_spec.rb │ │ ├── poll_validator_spec.rb │ │ ├── status_length_validator_spec.rb │ │ ├── status_pin_validator_spec.rb │ │ ├── unique_username_validator_spec.rb │ │ ├── unreserved_username_validator_spec.rb │ │ └── url_validator_spec.rb │ ├── views/ │ │ ├── about/ │ │ │ └── show.html.haml_spec.rb │ │ └── stream_entries/ │ │ └── show.html.haml_spec.rb │ └── workers/ │ ├── activitypub/ │ │ ├── delivery_worker_spec.rb │ │ ├── distribution_worker_spec.rb │ │ ├── fetch_replies_worker_spec.rb │ │ ├── processing_worker_spec.rb │ │ └── update_distribution_worker_spec.rb │ ├── after_remote_follow_request_worker_spec.rb │ ├── after_remote_follow_worker_spec.rb │ ├── digest_mailer_worker_spec.rb │ ├── domain_block_worker_spec.rb │ ├── feed_insert_worker_spec.rb │ ├── publish_scheduled_status_worker_spec.rb │ ├── pubsubhubbub/ │ │ ├── confirmation_worker_spec.rb │ │ ├── delivery_worker_spec.rb │ │ └── distribution_worker_spec.rb │ ├── regeneration_worker_spec.rb │ └── scheduler/ │ ├── feed_cleanup_scheduler_spec.rb │ ├── media_cleanup_scheduler_spec.rb │ └── subscriptions_scheduler_spec.rb ├── streaming/ │ └── index.js └── vendor/ └── .keep ================================================ FILE CONTENTS ================================================ ================================================ FILE: .buildpacks ================================================ https://github.com/heroku/heroku-buildpack-apt https://github.com/Scalingo/ffmpeg-buildpack https://github.com/Scalingo/nodejs-buildpack https://github.com/Scalingo/ruby-buildpack ================================================ FILE: .circleci/config.yml ================================================ version: 2 aliases: - &defaults docker: - image: circleci/ruby:2.6.0-stretch-node environment: &ruby_environment BUNDLE_APP_CONFIG: ./.bundle/ DB_HOST: localhost DB_USER: root RAILS_ENV: test PARALLEL_TEST_PROCESSORS: 4 ALLOW_NOPAM: true CONTINUOUS_INTEGRATION: true DISABLE_SIMPLECOV: true PAM_ENABLED: true PAM_DEFAULT_SERVICE: pam_test PAM_CONTROLLED_SERVICE: pam_test_controlled working_directory: ~/projects/mastodon/ - &attach_workspace attach_workspace: at: ~/projects/ - &persist_to_workspace persist_to_workspace: root: ~/projects/ paths: - ./mastodon/ - &restore_ruby_dependencies restore_cache: keys: - v2-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }} - v2-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}- - v2-ruby-dependencies- - &install_steps steps: - checkout - *attach_workspace - restore_cache: keys: - v1-node-dependencies-{{ checksum "yarn.lock" }} - v1-node-dependencies- - run: yarn install --frozen-lockfile - save_cache: key: v1-node-dependencies-{{ checksum "yarn.lock" }} paths: - ./node_modules/ - *persist_to_workspace - &install_system_dependencies run: name: Install system dependencies command: | sudo apt-get update sudo apt-get install -y libicu-dev libidn11-dev libprotobuf-dev protobuf-compiler - &install_ruby_dependencies steps: - *attach_workspace - *install_system_dependencies - run: ruby -e 'puts RUBY_VERSION' | tee /tmp/.ruby-version - *restore_ruby_dependencies - run: bundle install --clean --jobs 16 --path ./vendor/bundle/ --retry 3 --with pam_authentication --without development production && bundle clean - save_cache: key: v2-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }} paths: - ./.bundle/ - ./vendor/bundle/ - persist_to_workspace: root: ~/projects/ paths: - ./mastodon/.bundle/ - ./mastodon/vendor/bundle/ - &test_steps steps: - *attach_workspace - *install_system_dependencies - run: sudo apt-get install -y ffmpeg - run: name: Prepare Tests command: ./bin/rails parallel:create parallel:load_schema parallel:prepare - run: name: Run Tests command: ./bin/retry bundle exec parallel_test ./spec/ --group-by filesize --type rspec jobs: install: <<: *defaults <<: *install_steps install-ruby2.6: <<: *defaults <<: *install_ruby_dependencies install-ruby2.5: <<: *defaults docker: - image: circleci/ruby:2.5.3-stretch-node environment: *ruby_environment <<: *install_ruby_dependencies install-ruby2.4: <<: *defaults docker: - image: circleci/ruby:2.4.5-stretch-node environment: *ruby_environment <<: *install_ruby_dependencies build: <<: *defaults steps: - *attach_workspace - *install_system_dependencies - run: ./bin/rails assets:precompile - persist_to_workspace: root: ~/projects/ paths: - ./mastodon/public/assets - ./mastodon/public/packs-test/ test-ruby2.6: <<: *defaults docker: - image: circleci/ruby:2.6.0-stretch-node environment: *ruby_environment - image: circleci/postgres:10.6-alpine environment: POSTGRES_USER: root - image: circleci/redis:5.0.3-alpine3.8 <<: *test_steps test-ruby2.5: <<: *defaults docker: - image: circleci/ruby:2.5.3-stretch-node environment: *ruby_environment - image: circleci/postgres:10.6-alpine environment: POSTGRES_USER: root - image: circleci/redis:4.0.12-alpine <<: *test_steps test-ruby2.4: <<: *defaults docker: - image: circleci/ruby:2.4.5-stretch-node environment: *ruby_environment - image: circleci/postgres:10.6-alpine environment: POSTGRES_USER: root - image: circleci/redis:4.0.12-alpine <<: *test_steps test-webui: <<: *defaults docker: - image: circleci/node:8.15.0-stretch steps: - *attach_workspace - run: ./bin/retry yarn test:jest check-i18n: <<: *defaults steps: - *attach_workspace - run: bundle exec i18n-tasks check-normalized - run: bundle exec i18n-tasks unused -l en - run: bundle exec i18n-tasks check-consistent-interpolations workflows: version: 2 build-and-test: jobs: - install - install-ruby2.6: requires: - install - install-ruby2.5: requires: - install - install-ruby2.6 - install-ruby2.4: requires: - install - install-ruby2.6 - build: requires: - install-ruby2.6 - test-ruby2.6: requires: - install-ruby2.6 - build - test-ruby2.5: requires: - install-ruby2.5 - build - test-ruby2.4: requires: - install-ruby2.4 - build - test-webui: requires: - install - check-i18n: requires: - install-ruby2.6 ================================================ FILE: .codeclimate.yml ================================================ version: "2" checks: argument-count: enabled: false complex-logic: enabled: false file-lines: enabled: false method-complexity: enabled: false method-count: enabled: false method-lines: enabled: false nested-control-flow: enabled: false return-statements: enabled: false similar-code: enabled: false identical-code: enabled: false plugins: brakeman: enabled: true bundler-audit: enabled: true eslint: enabled: true channel: eslint-5 rubocop: enabled: true channel: rubocop-0-71 sass-lint: enabled: true exclude_patterns: - spec/ - vendor/asset ================================================ FILE: .dependabot/config.yml ================================================ version: 1 update_configs: - package_manager: "ruby:bundler" directory: "/" update_schedule: "weekly" - package_manager: "javascript" directory: "/" update_schedule: "weekly" ================================================ FILE: .dockerignore ================================================ .bundle .env .env.* public/system public/assets public/packs node_modules neo4j vendor/bundle .DS_Store *.swp *~ postgres redis elasticsearch ================================================ FILE: .editorconfig ================================================ # EditorConfig is awesome: http://EditorConfig.org # top-most EditorConfig file root = true # Unix-style newlines with a newline ending every file [*] end_of_line = lf insert_final_newline = true charset = utf-8 indent_style = space indent_size = 2 ================================================ FILE: .eslintignore ================================================ /build/** /coverage/** /db/** /lib/** /log/** /node_modules/** /nonobox/** /public/** !/public/embed.js /spec/** /tmp/** /vendor/** !.eslintrc.js ================================================ FILE: .eslintrc.js ================================================ module.exports = { root: true, env: { browser: true, node: true, es6: true, jest: true, }, globals: { ATTACHMENT_HOST: false, }, parser: 'babel-eslint', plugins: [ 'react', 'jsx-a11y', 'import', 'promise', ], parserOptions: { sourceType: 'module', ecmaFeatures: { experimentalObjectRestSpread: true, jsx: true, }, ecmaVersion: 2018, }, settings: { react: { version: 'detect', }, 'import/extensions': [ '.js', ], 'import/ignore': [ 'node_modules', '\\.(css|scss|json)$', ], 'import/resolver': { node: { paths: ['app/javascript'], }, }, }, rules: { 'brace-style': 'warn', 'comma-dangle': ['error', 'always-multiline'], 'comma-spacing': [ 'warn', { before: false, after: true, }, ], 'comma-style': ['warn', 'last'], 'consistent-return': 'error', 'dot-notation': 'error', eqeqeq: 'error', indent: ['warn', 2], 'jsx-quotes': ['error', 'prefer-single'], 'no-catch-shadow': 'error', 'no-cond-assign': 'error', 'no-console': [ 'warn', { allow: [ 'error', 'warn', ], }, ], 'no-fallthrough': 'error', 'no-irregular-whitespace': 'error', 'no-mixed-spaces-and-tabs': 'warn', 'no-nested-ternary': 'warn', 'no-trailing-spaces': 'warn', 'no-undef': 'error', 'no-unreachable': 'error', 'no-unused-expressions': 'error', 'no-unused-vars': [ 'error', { vars: 'all', args: 'after-used', ignoreRestSiblings: true, }, ], 'object-curly-spacing': ['error', 'always'], 'padded-blocks': [ 'error', { classes: 'always', }, ], quotes: ['error', 'single'], semi: 'error', strict: 'off', 'valid-typeof': 'error', 'react/jsx-boolean-value': 'error', 'react/jsx-closing-bracket-location': ['error', 'line-aligned'], 'react/jsx-curly-spacing': 'error', 'react/jsx-equals-spacing': 'error', 'react/jsx-first-prop-new-line': ['error', 'multiline-multiprop'], 'react/jsx-indent': ['error', 2], 'react/jsx-no-bind': 'error', 'react/jsx-no-duplicate-props': 'error', 'react/jsx-no-undef': 'error', 'react/jsx-tag-spacing': 'error', 'react/jsx-uses-react': 'error', 'react/jsx-uses-vars': 'error', 'react/jsx-wrap-multilines': 'error', 'react/no-multi-comp': 'off', 'react/no-string-refs': 'error', 'react/prop-types': 'error', 'react/self-closing-comp': 'error', 'jsx-a11y/accessible-emoji': 'warn', 'jsx-a11y/alt-text': 'warn', 'jsx-a11y/anchor-has-content': 'warn', 'jsx-a11y/anchor-is-valid': [ 'warn', { components: [ 'Link', 'NavLink', ], specialLink: [ 'to', ], aspect: [ 'noHref', 'invalidHref', 'preferButton', ], }, ], 'jsx-a11y/aria-activedescendant-has-tabindex': 'warn', 'jsx-a11y/aria-props': 'warn', 'jsx-a11y/aria-proptypes': 'warn', 'jsx-a11y/aria-role': 'warn', 'jsx-a11y/aria-unsupported-elements': 'warn', 'jsx-a11y/heading-has-content': 'warn', 'jsx-a11y/html-has-lang': 'warn', 'jsx-a11y/iframe-has-title': 'warn', 'jsx-a11y/img-redundant-alt': 'warn', 'jsx-a11y/interactive-supports-focus': 'warn', 'jsx-a11y/label-has-for': 'off', 'jsx-a11y/mouse-events-have-key-events': 'warn', 'jsx-a11y/no-access-key': 'warn', 'jsx-a11y/no-distracting-elements': 'warn', 'jsx-a11y/no-noninteractive-element-interactions': [ 'warn', { handlers: [ 'onClick', ], }, ], 'jsx-a11y/no-onchange': 'warn', 'jsx-a11y/no-redundant-roles': 'warn', 'jsx-a11y/no-static-element-interactions': [ 'warn', { handlers: [ 'onClick', ], }, ], 'jsx-a11y/role-has-required-aria-props': 'warn', 'jsx-a11y/role-supports-aria-props': 'off', 'jsx-a11y/scope': 'warn', 'jsx-a11y/tabindex-no-positive': 'warn', 'import/extensions': [ 'error', 'always', { js: 'never', }, ], 'import/newline-after-import': 'error', 'import/no-extraneous-dependencies': [ 'error', { devDependencies: [ 'config/webpack/**', 'app/javascript/mastodon/test_setup.js', 'app/javascript/**/__tests__/**', ], }, ], 'import/no-unresolved': 'error', 'import/no-webpack-loader-syntax': 'error', 'promise/catch-or-return': 'error', }, }; ================================================ FILE: .foreman ================================================ procfile: Procfile.dev ================================================ FILE: .gitattributes ================================================ * text=auto eol=lf *.eot -text *.gif -text *.gz -text *.ico -text *.jpg -text *.mp3 -text *.ogg -text *.png -text *.ttf -text *.webm -text *.woff -text *.woff2 -text spec/fixtures/requests/** -text !eol ================================================ FILE: .gitignore ================================================ # See https://help.github.com/articles/ignoring-files for more about ignoring files. # # If you find yourself ignoring temporary files generated by your text editor # or operating system, you probably want to add a global ignore instead: # git config --global core.excludesfile '~/.gitignore_global' # Ignore bundler config and downloaded libraries. /.bundle /vendor/bundle # Ignore the default SQLite database. /db/*.sqlite3 /db/*.sqlite3-journal # Ignore all logfiles and tempfiles. /log/* !/log/.keep /tmp coverage public/system public/assets public/packs public/packs-test .env .env.production node_modules/ build/ # Ignore Vagrant files .vagrant/ # Ignore Capistrano customizations config/deploy/* # Ignore IDE files .vscode/ .idea/ # Ignore postgres + redis + elasticsearch volume optionally created by docker-compose postgres redis elasticsearch # Ignore Apple files .DS_Store # Ignore vim files *~ *.swp # Ignore npm debug log npm-debug.log # Ignore yarn log files yarn-error.log yarn-debug.log # Ignore Docker option files docker-compose.override.yml ================================================ FILE: .haml-lint.yml ================================================ # Whether to ignore frontmatter at the beginning of HAML documents for # frameworks such as Jekyll/Middleman skip_frontmatter: false exclude: - 'vendor/**/*' - 'spec/**/*' - 'lib/templates/**/*' - 'app/views/kaminari/**/*' linters: AltText: enabled: false ClassAttributeWithStaticValue: enabled: true ClassesBeforeIds: enabled: true ConsecutiveComments: enabled: true ConsecutiveSilentScripts: enabled: true max_consecutive: 2 EmptyObjectReference: enabled: true EmptyScript: enabled: true FinalNewline: enabled: true present: true HtmlAttributes: enabled: true ImplicitDiv: enabled: true LeadingCommentSpace: enabled: true LineLength: enabled: false max: 80 MultilinePipe: enabled: true MultilineScript: enabled: true ObjectReferenceAttributes: enabled: true RuboCop: enabled: true # These cops are incredibly noisy when it comes to HAML templates, so we # ignore them. ignored_cops: - Lint/BlockAlignment - Lint/EndAlignment - Lint/Void - Metrics/BlockLength - Metrics/LineLength - Style/AlignParameters - Style/BlockNesting - Style/ElseAlignment - Style/EndOfLine - Style/FileName - Style/FinalNewline - Style/FrozenStringLiteralComment - Style/IfUnlessModifier - Style/IndentationWidth - Style/Next - Style/TrailingBlankLines - Style/TrailingWhitespace - Style/WhileUntilModifier RubyComments: enabled: true SpaceBeforeScript: enabled: true SpaceInsideHashAttributes: enabled: true style: space Indentation: enabled: true character: space # or tab TagName: enabled: true TrailingWhitespace: enabled: true UnnecessaryInterpolation: enabled: true UnnecessaryStringOutput: enabled: true ================================================ FILE: .nanoignore ================================================ .DS_Store .git/ .gitignore .bundle/ .cache/ config/deploy/* coverage docs/ .env log/*.log neo4j/ node_modules/ public/assets/ public/system/ spec/ tmp/ .vagrant/ vendor/bundle/ ================================================ FILE: .nvmrc ================================================ 8 ================================================ FILE: .profile ================================================ LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/app/.apt/lib/x86_64-linux-gnu:/app/.apt/usr/lib/x86_64-linux-gnu/mesa:/app/.apt/usr/lib/x86_64-linux-gnu/pulseaudio ================================================ FILE: .rspec ================================================ --color --require spec_helper --format Fuubar ================================================ FILE: .rubocop.yml ================================================ require: - rubocop-rails AllCops: TargetRubyVersion: 2.3 Exclude: - 'spec/**/*' - 'db/**/*' - 'app/views/**/*' - 'config/**/*' - 'bin/*' - 'Rakefile' - 'node_modules/**/*' - 'Vagrantfile' - 'vendor/**/*' - 'lib/json_ld/*' - 'lib/templates/**/*' Bundler/OrderedGems: Enabled: false Layout/AccessModifierIndentation: EnforcedStyle: indent Layout/EmptyLineAfterMagicComment: Enabled: false Layout/SpaceInsideHashLiteralBraces: EnforcedStyle: space Metrics/AbcSize: Max: 100 Metrics/BlockLength: Max: 35 Exclude: - 'lib/tasks/**/*' Metrics/BlockNesting: Max: 3 Metrics/ClassLength: CountComments: false Max: 300 Metrics/CyclomaticComplexity: Max: 25 Metrics/LineLength: AllowURI: true Enabled: false Metrics/MethodLength: CountComments: false Max: 55 Metrics/ModuleLength: CountComments: false Max: 200 Metrics/ParameterLists: Max: 5 CountKeywordArgs: true Metrics/PerceivedComplexity: Max: 20 Naming/MemoizedInstanceVariableName: Enabled: false Rails: Enabled: true Rails/HasAndBelongsToMany: Enabled: false Rails/SkipsModelValidations: Enabled: false Rails/HttpStatus: Enabled: false Rails/Exit: Exclude: - 'lib/mastodon/*' - 'lib/cli.rb' Rails/HelperInstanceVariable: Enabled: false Style/ClassAndModuleChildren: Enabled: false Style/CollectionMethods: Enabled: true PreferredMethods: find_all: 'select' Style/Documentation: Enabled: false Style/DoubleNegation: Enabled: true Style/FrozenStringLiteralComment: Enabled: true Style/GuardClause: Enabled: false Style/Lambda: Enabled: false Style/PercentLiteralDelimiters: PreferredDelimiters: '%i': '()' '%w': '()' Style/PerlBackrefs: AutoCorrect: false Style/RegexpLiteral: Enabled: false Style/SymbolArray: Enabled: false Style/TrailingCommaInArrayLiteral: EnforcedStyleForMultiline: 'comma' Style/TrailingCommaInHashLiteral: EnforcedStyleForMultiline: 'comma' ================================================ FILE: .ruby-version ================================================ 2.6.1 ================================================ FILE: .sass-lint.yml ================================================ # Linter Documentation: # https://github.com/sasstools/sass-lint/tree/v1.13.1/docs/options files: include: app/javascript/styles/**/*.scss ignore: - app/javascript/styles/mastodon/reset.scss rules: # Disallows no-color-literals: 0 no-css-comments: 0 no-duplicate-properties: 0 no-ids: 0 no-important: 0 no-mergeable-selectors: 0 no-misspelled-properties: 0 no-qualifying-elements: 0 no-transition-all: 0 no-vendor-prefixes: 0 # Nesting force-element-nesting: 0 force-attribute-nesting: 0 force-pseudo-nesting: 0 # Name Formats class-name-format: 0 leading-zero: 0 # Style Guide attribute-quotes: 0 hex-length: 0 indentation: 0 nesting-depth: 0 property-sort-order: 0 quotes: 0 ================================================ FILE: .slugignore ================================================ node_modules/ .cache/ docs/ spec/ ================================================ FILE: .yarnclean ================================================ # test directories __tests__ test tests powered-test # asset directories docs doc website images # assets # examples example examples # code coverage directories coverage .nyc_output # build scripts Makefile Gulpfile.js Gruntfile.js # configs .tern-project .gitattributes .editorconfig .*ignore .eslintrc .jshintrc .flowconfig .documentup.json .yarn-metadata.json .*.yml *.yml # misc *.gz *.md # for specific ignore !.svgo.yml !sass-lint/**/*.yml ================================================ FILE: AUTHORS.md ================================================ Authors ======= Mastodon is available on [GitHub](https://github.com/tootsuite/mastodon) and provided thanks to the work of the following contributors: * [Gargron](https://github.com/Gargron) * [ykzts](https://github.com/ykzts) * [ThibG](https://github.com/ThibG) * [akihikodaki](https://github.com/akihikodaki) * [mjankowski](https://github.com/mjankowski) * [dependabot[bot]](https://github.com/apps/dependabot) * [unarist](https://github.com/unarist) * [m4sk1n](https://github.com/m4sk1n) * [yiskah](https://github.com/yiskah) * [nolanlawson](https://github.com/nolanlawson) * [ysksn](https://github.com/ysksn) * [sorin-davidoi](https://github.com/sorin-davidoi) * [abcang](https://github.com/abcang) * [lynlynlynx](https://github.com/lynlynlynx) * [mayaeh](https://github.com/mayaeh) * [renatolond](https://github.com/renatolond) * [alpaca-tc](https://github.com/alpaca-tc) * [nclm](https://github.com/nclm) * [ineffyble](https://github.com/ineffyble) * [jeroenpraat](https://github.com/jeroenpraat) * [blackle](https://github.com/blackle) * [Quent-in](https://github.com/Quent-in) * [JantsoP](https://github.com/JantsoP) * [Kjwon15](https://github.com/Kjwon15) * [mabkenar](https://github.com/mabkenar) * [nullkal](https://github.com/nullkal) * [yookoala](https://github.com/yookoala) * [shuheiktgw](https://github.com/shuheiktgw) * [ashfurrow](https://github.com/ashfurrow) * [zunda](https://github.com/zunda) * [Quenty31](https://github.com/Quenty31) * [eramdam](https://github.com/eramdam) * [takayamaki](https://github.com/takayamaki) * [masarakki](https://github.com/masarakki) * [ticky](https://github.com/ticky) * [danhunsaker](https://github.com/danhunsaker) * [ThisIsMissEm](https://github.com/ThisIsMissEm) * [hcmiya](https://github.com/hcmiya) * [stephenburgess8](https://github.com/stephenburgess8) * [Wonderfall](https://github.com/Wonderfall) * [matteoaquila](https://github.com/matteoaquila) * [yukimochi](https://github.com/yukimochi) * [rkarabut](https://github.com/rkarabut) * [Artoria2e5](https://github.com/Artoria2e5) * [nightpool](https://github.com/nightpool) * [marrus-sh](https://github.com/marrus-sh) * [krainboltgreene](https://github.com/krainboltgreene) * [pfigel](https://github.com/pfigel) * [Aldarone](https://github.com/Aldarone) * [BoFFire](https://github.com/BoFFire) * [clworld](https://github.com/clworld) * [dracos](https://github.com/dracos) * [SerCom_KC](mailto:sercom-kc@users.noreply.github.com) * [Sylvhem](https://github.com/Sylvhem) * [MasterGroosha](https://github.com/MasterGroosha) * [JeanGauthier](https://github.com/JeanGauthier) * [kschaper](https://github.com/kschaper) * [MaciekBaron](https://github.com/MaciekBaron) * [MitarashiDango](mailto:mitarashidango@users.noreply.github.com) * [beatrix-bitrot](https://github.com/beatrix-bitrot) * [Aditoo17](https://github.com/Aditoo17) * [adbelle](https://github.com/adbelle) * [evanminto](https://github.com/evanminto) * [MightyPork](https://github.com/MightyPork) * [yhirano55](https://github.com/yhirano55) * [rinsuki](https://github.com/rinsuki) * [camponez](https://github.com/camponez) * [hinaloe](https://github.com/hinaloe) * [SerCom-KC](https://github.com/SerCom-KC) * [aschmitz](https://github.com/aschmitz) * [devkral](https://github.com/devkral) * [fpiesche](https://github.com/fpiesche) * [gandaro](https://github.com/gandaro) * [johnsudaar](https://github.com/johnsudaar) * [trebmuh](https://github.com/trebmuh) * [Rakib Hasan](mailto:rmhasan@gmail.com) * [ashleyhull-versent](https://github.com/ashleyhull-versent) * [lindwurm](https://github.com/lindwurm) * [victorhck](mailto:victorhck@geeko.site) * [voidsatisfaction](https://github.com/voidsatisfaction) * [hikari-no-yume](https://github.com/hikari-no-yume) * [angristan](https://github.com/angristan) * [seefood](https://github.com/seefood) * [jackjennings](https://github.com/jackjennings) * [spla](mailto:spla@mastodont.cat) * [expenses](https://github.com/expenses) * [walf443](https://github.com/walf443) * [JoelQ](https://github.com/JoelQ) * [mistydemeo](https://github.com/mistydemeo) * [dunn](https://github.com/dunn) * [xqus](https://github.com/xqus) * [hugogameiro](https://github.com/hugogameiro) * [ariasuni](https://github.com/ariasuni) * [pfm-eyesightjp](https://github.com/pfm-eyesightjp) * [fakenine](https://github.com/fakenine) * [tsuwatch](https://github.com/tsuwatch) * [victorhck](https://github.com/victorhck) * [kedamaDQ](https://github.com/kedamaDQ) * [puckipedia](https://github.com/puckipedia) * [trwnh](https://github.com/trwnh) * [fvh-P](https://github.com/fvh-P) * [Anna e só](mailto:contraexemplos@gmail.com) * [BenLubar](https://github.com/BenLubar) * [kazu9su](https://github.com/kazu9su) * [Komic](https://github.com/Komic) * [lmorchard](https://github.com/lmorchard) * [diomed](https://github.com/diomed) * [Neetshin](mailto:neetshin@neetsh.in) * [rainyday](https://github.com/rainyday) * [ProgVal](https://github.com/ProgVal) * [valentin2105](https://github.com/valentin2105) * [yuntan](https://github.com/yuntan) * [goofy-bz](mailto:goofy@babelzilla.org) * [kadiix](https://github.com/kadiix) * [kodacs](https://github.com/kodacs) * [JMendyk](https://github.com/JMendyk) * [KScl](https://github.com/KScl) * [sterdev](https://github.com/sterdev) * [TheKinrar](https://github.com/TheKinrar) * [AA4ch1](https://github.com/AA4ch1) * [alexgleason](https://github.com/alexgleason) * [cpytel](https://github.com/cpytel) * [northerner](https://github.com/northerner) * [fhemberger](https://github.com/fhemberger) * [greysteil](https://github.com/greysteil) * [hensmith](https://github.com/hensmith) * [d6rkaiz](https://github.com/d6rkaiz) * [Reverite](https://github.com/Reverite) * [JohnD28](https://github.com/JohnD28) * [znz](https://github.com/znz) * [marek-lach](https://github.com/marek-lach) * [Naouak](https://github.com/Naouak) * [pawelngei](https://github.com/pawelngei) * [rtucker](https://github.com/rtucker) * [reneklacan](https://github.com/reneklacan) * [ekiru](https://github.com/ekiru) * [noellabo](https://github.com/noellabo) * [tcitworld](https://github.com/tcitworld) * [geta6](https://github.com/geta6) * [happycoloredbanana](https://github.com/happycoloredbanana) * [leopku](https://github.com/leopku) * [SansPseudoFix](https://github.com/SansPseudoFix) * [tomfhowe](https://github.com/tomfhowe) * [noraworld](https://github.com/noraworld) * [theboss](https://github.com/theboss) * [178inaba](https://github.com/178inaba) * [alyssais](https://github.com/alyssais) * [hiphref](https://github.com/hiphref) * [stalker314314](https://github.com/stalker314314) * [huertanix](https://github.com/huertanix) * [genesixx](https://github.com/genesixx) * [halkeye](https://github.com/halkeye) * [treby](https://github.com/treby) * [jpdevries](https://github.com/jpdevries) * [gdpelican](https://github.com/gdpelican) * [kmichl](https://github.com/kmichl) * [Kurtis Rainbolt-Greene](mailto:me@kurtisrainboltgreene.name) * [saper](https://github.com/saper) * [nevillepark](https://github.com/nevillepark) * [ornithocoder](https://github.com/ornithocoder) * [pierreozoux](https://github.com/pierreozoux) * [qguv](https://github.com/qguv) * [Ram Lmn](mailto:ramlmn@users.noreply.github.com) * [sascha-sl](https://github.com/sascha-sl) * [harukasan](https://github.com/harukasan) * [stamak](https://github.com/stamak) * [Technowix](mailto:technowix@users.noreply.github.com) * [Zoeille](https://github.com/Zoeille) * [Thor Harald Johansen](mailto:thj@thj.no) * [0x70b1a5](https://github.com/0x70b1a5) * [gled-rs](https://github.com/gled-rs) * [Valentin_NC](mailto:valentin.ouvrard@nautile.sarl) * [R0ckweb](https://github.com/R0ckweb) * [caasi](https://github.com/caasi) * [chr-1x](https://github.com/chr-1x) * [esetomo](https://github.com/esetomo) * [foxiehkins](https://github.com/foxiehkins) * [hoodie](mailto:hoodiekitten@outlook.com) * [luzi82](https://github.com/luzi82) * [duxovni](https://github.com/duxovni) * [tmm576](https://github.com/tmm576) * [unsmell](https://github.com/unsmell) * [valerauko](https://github.com/valerauko) * [chriswmartin](https://github.com/chriswmartin) * [vahnj](https://github.com/vahnj) * [ikuradon](https://github.com/ikuradon) * [AndreLewin](https://github.com/AndreLewin) * [0xflotus](https://github.com/0xflotus) * [redtachyons](https://github.com/redtachyons) * [thurloat](https://github.com/thurloat) * [aaribaud](https://github.com/aaribaud) * [pointlessone](https://github.com/pointlessone) * [Andrew](mailto:andrewlchronister@gmail.com) * [estuans](https://github.com/estuans) * [dissolve](https://github.com/dissolve) * [PurpleBooth](https://github.com/PurpleBooth) * [bradurani](https://github.com/bradurani) * [wavebeem](https://github.com/wavebeem) * [bruwalfas](https://github.com/bruwalfas) * [foxsan48](https://github.com/foxsan48) * [wchristian](https://github.com/wchristian) * [muffinista](https://github.com/muffinista) * [cdutson](https://github.com/cdutson) * [farlistener](https://github.com/farlistener) * [DavidLibeau](https://github.com/DavidLibeau) * [ddevault](https://github.com/ddevault) * [Fjoerfoks](https://github.com/Fjoerfoks) * [fmauNeko](https://github.com/fmauNeko) * [gloaec](https://github.com/gloaec) * [Gomasy](https://github.com/Gomasy) * [unstabler](https://github.com/unstabler) * [potato4d](https://github.com/potato4d) * [h-izumi](https://github.com/h-izumi) * [ErikXXon](https://github.com/ErikXXon) * [ian-kelling](https://github.com/ian-kelling) * [immae](https://github.com/immae) * [J0WI](https://github.com/J0WI) * [foozmeat](https://github.com/foozmeat) * [jasonrhodes](https://github.com/jasonrhodes) * [Jason Snell](mailto:jason@newrelic.com) * [jviide](https://github.com/jviide) * [YuleZ](https://github.com/YuleZ) * [crakaC](https://github.com/crakaC) * [tkbky](https://github.com/tkbky) * [Kaylee](mailto:kaylee@codethat.sucks) * [Kazhnuz](https://github.com/Kazhnuz) * [connyduck](https://github.com/connyduck) * [Lindsey Bieda](mailto:lindseyb@users.noreply.github.com) * [Lorenz Diener](mailto:halcyon@icosahedron.website) * [alimony](https://github.com/alimony) * [mig5](https://github.com/mig5) * [moritzheiber](https://github.com/moritzheiber) * [ndarville](https://github.com/ndarville) * [Abzol](https://github.com/Abzol) * [pwoolcoc](https://github.com/pwoolcoc) * [xPaw](https://github.com/xPaw) * [petzah](https://github.com/petzah) * [ignisf](https://github.com/ignisf) * [raymestalez](https://github.com/raymestalez) * [remram44](https://github.com/remram44) * [sts10](https://github.com/sts10) * [u1-liquid](https://github.com/u1-liquid) * [sim6](https://github.com/sim6) * [Sir-Boops](https://github.com/Sir-Boops) * [stemid](https://github.com/stemid) * [sumdog](https://github.com/sumdog) * [ThomasLeister](https://github.com/ThomasLeister) * [mcat-ee](https://github.com/mcat-ee) * [tototoshi](https://github.com/tototoshi) * [TrashMacNugget](https://github.com/TrashMacNugget) * [VirtuBox](https://github.com/VirtuBox) * [Vladyslav](mailto:vaden@tuta.io) * [kaniini](https://github.com/kaniini) * [vayan](https://github.com/vayan) * [yannicka](https://github.com/yannicka) * [ikasoumen](https://github.com/ikasoumen) * [zacanger](https://github.com/zacanger) * [amazedkoumei](https://github.com/amazedkoumei) * [anon5r](https://github.com/anon5r) * [aus-social](https://github.com/aus-social) * [imbsky](https://github.com/imbsky) * [bsky](mailto:me@imbsky.net) * [codl](https://github.com/codl) * [cpsdqs](https://github.com/cpsdqs) * [barzamin](https://github.com/barzamin) * [fhalna](https://github.com/fhalna) * [haoyayoi](https://github.com/haoyayoi) * [ik11235](https://github.com/ik11235) * [kawax](https://github.com/kawax) * [007lva](https://github.com/007lva) * [mbajur](https://github.com/mbajur) * [matsurai25](https://github.com/matsurai25) * [mecab](https://github.com/mecab) * [nicobz25](https://github.com/nicobz25) * [oliverkeeble](https://github.com/oliverkeeble) * [pinfort](https://github.com/pinfort) * [rbaumert](https://github.com/rbaumert) * [rhoio](https://github.com/rhoio) * [usagi-f](https://github.com/usagi-f) * [vidarlee](https://github.com/vidarlee) * [vjackson725](https://github.com/vjackson725) * [wxcafe](https://github.com/wxcafe) * [新都心(Neet Shin)](mailto:nucx@dio-vox.com) * [cygnan](https://github.com/cygnan) * [Awea](https://github.com/Awea) * [halcy](https://github.com/halcy) * [naaaaaaaaaaaf](https://github.com/naaaaaaaaaaaf) * [8398a7](https://github.com/8398a7) * [857b](https://github.com/857b) * [insom](https://github.com/insom) * [tachyons](https://github.com/tachyons) * [acid-chicken](https://github.com/acid-chicken) * [Esteth](https://github.com/Esteth) * [unascribed](https://github.com/unascribed) * [Aguay-val](https://github.com/Aguay-val) * [Akihiko Odaki](mailto:nekomanma@pixiv.co.jp) * [knu](https://github.com/knu) * [h3poteto](https://github.com/h3poteto) * [unleashed](https://github.com/unleashed) * [alxrcs](https://github.com/alxrcs) * [console-cowboy](https://github.com/console-cowboy) * [Alkarex](https://github.com/Alkarex) * [a2](https://github.com/a2) * [0xa](https://github.com/0xa) * [palindromordnilap](https://github.com/palindromordnilap) * [virtualpain](https://github.com/virtualpain) * [sapphirus](https://github.com/sapphirus) * [amandavisconti](https://github.com/amandavisconti) * [ameliavoncat](https://github.com/ameliavoncat) * [ilpianista](https://github.com/ilpianista) * [Andreas Drop](mailto:andy@remline.de) * [andi1984](https://github.com/andi1984) * [schas002](https://github.com/schas002) * [contraexemplo](https://github.com/contraexemplo) * [abackstrom](https://github.com/abackstrom) * [armandfardeau](https://github.com/armandfardeau) * [jumbosushi](https://github.com/jumbosushi) * [aurelien-reeves](https://github.com/aurelien-reeves) * [ayumin](https://github.com/ayumin) * [BaptisteGelez](https://github.com/BaptisteGelez) * [bzg](https://github.com/bzg) * [benediktg](https://github.com/benediktg) * [blakebarnett](https://github.com/blakebarnett) * [bradj](https://github.com/bradj) * [brycied00d](https://github.com/brycied00d) * [carlosjs23](https://github.com/carlosjs23) * [cgxxx](https://github.com/cgxxx) * [kibitan](https://github.com/kibitan) * [chrisheninger](https://github.com/chrisheninger) * [chris-martin](https://github.com/chris-martin) * [DoubleMalt](https://github.com/DoubleMalt) * [Moosh-be](https://github.com/Moosh-be) * [Motoma](https://github.com/Motoma) * [chriswk](https://github.com/chriswk) * [csu](https://github.com/csu) * [clarfon](https://github.com/clarfon) * [kklleemm](https://github.com/kklleemm) * [colindean](https://github.com/colindean) * [dachinat](https://github.com/dachinat) * [multiple-creatures](https://github.com/multiple-creatures) * [watilde](https://github.com/watilde) * [daprice](https://github.com/daprice) * [dar5hak](https://github.com/dar5hak) * [kant](https://github.com/kant) * [maxolasersquad](https://github.com/maxolasersquad) * [singingwolfboy](https://github.com/singingwolfboy) * [davidcelis](https://github.com/davidcelis) * [davefp](https://github.com/davefp) * [yipdw](https://github.com/yipdw) * [debanshuk](https://github.com/debanshuk) * [Derek Lewis](mailto:derekcecillewis@gmail.com) * [dblandin](https://github.com/dblandin) * [Drew Gates](mailto:aranaur@users.noreply.github.com) * [dtschust](https://github.com/dtschust) * [Dryusdan](https://github.com/Dryusdan) * [eai04191](https://github.com/eai04191) * [d3vgru](https://github.com/d3vgru) * [Elizafox](https://github.com/Elizafox) * [enewhuis](https://github.com/enewhuis) * [ericblade](https://github.com/ericblade) * [mikoim](https://github.com/mikoim) * [espenronnevik](https://github.com/espenronnevik) * [Finariel](https://github.com/Finariel) * [siuying](https://github.com/siuying) * [zoc](https://github.com/zoc) * [fwenzel](https://github.com/fwenzel) * [GenbuHase](https://github.com/GenbuHase) * [hattori6789](https://github.com/hattori6789) * [algernon](https://github.com/algernon) * [Fastbyte01](https://github.com/Fastbyte01) * [myfreeweb](https://github.com/myfreeweb) * [gfaivre](https://github.com/gfaivre) * [Fiaxhs](https://github.com/Fiaxhs) * [reedcourty](https://github.com/reedcourty) * [anneau](https://github.com/anneau) * [lanodan](https://github.com/lanodan) * [Harmon758](https://github.com/Harmon758) * [HellPie](https://github.com/HellPie) * [Habu-Kagumba](https://github.com/Habu-Kagumba) * [suzukaze](https://github.com/suzukaze) * [Hiromi-Kai](https://github.com/Hiromi-Kai) * [hishamhm](https://github.com/hishamhm) * [musashino205](https://github.com/musashino205) * [iwaim](https://github.com/iwaim) * [valrus](https://github.com/valrus) * [IMcD23](https://github.com/IMcD23) * [yi0713](https://github.com/yi0713) * [iblech](https://github.com/iblech) * [usbsnowcrash](https://github.com/usbsnowcrash) * [jack-michaud](https://github.com/jack-michaud) * [Floppy](https://github.com/Floppy) * [loomchild](https://github.com/loomchild) * [jenkr55](https://github.com/jenkr55) * [press5](https://github.com/press5) * [TrollDecker](https://github.com/TrollDecker) * [jmontane](https://github.com/jmontane) * [jonathanklee](https://github.com/jonathanklee) * [jguerder](https://github.com/jguerder) * [Jehops](https://github.com/Jehops) * [joshuap](https://github.com/joshuap) * [Tiwy57](https://github.com/Tiwy57) * [xuv](https://github.com/xuv) * [June Sallou](mailto:jnsll@users.noreply.github.com) * [j0k3r](https://github.com/j0k3r) * [KEINOS](https://github.com/KEINOS) * [futoase](https://github.com/futoase) * [Pneumaticat](https://github.com/Pneumaticat) * [Kit Redgrave](mailto:qwertyitis@gmail.com) * [Knut Erik](mailto:abjectio@users.noreply.github.com) * [mkody](https://github.com/mkody) * [k0ta0uchi](https://github.com/k0ta0uchi) * [KrzysiekJ](https://github.com/KrzysiekJ) * [leowzukw](https://github.com/leowzukw) * [Tak](https://github.com/Tak) * [cacheflow](https://github.com/cacheflow) * [ldidry](https://github.com/ldidry) * [jemus42](https://github.com/jemus42) * [lfuelling](https://github.com/lfuelling) * [Grabacr07](https://github.com/Grabacr07) * [mistermantas](https://github.com/mistermantas) * [mareklach](https://github.com/mareklach) * [wirehack7](https://github.com/wirehack7) * [martymcguire](https://github.com/martymcguire) * [marvinkopf](https://github.com/marvinkopf) * [otsune](https://github.com/otsune) * [mbugowski](https://github.com/mbugowski) * [Mathias B](mailto:10813340+mathias-b@users.noreply.github.com) * [matt-auckland](https://github.com/matt-auckland) * [webroo](https://github.com/webroo) * [matthiasbeyer](https://github.com/matthiasbeyer) * [mattjmattj](https://github.com/mattjmattj) * [mtparet](https://github.com/mtparet) * [maximeborges](https://github.com/maximeborges) * [minacle](https://github.com/minacle) * [michaeljdeeb](https://github.com/michaeljdeeb) * [Themimitoof](https://github.com/Themimitoof) * [cyweo](https://github.com/cyweo) * [Midgard](mailto:m1dgard@users.noreply.github.com) * [mike-burns](https://github.com/mike-burns) * [verymilan](https://github.com/verymilan) * [milmazz](https://github.com/milmazz) * [premist](https://github.com/premist) * [Mnkai](https://github.com/Mnkai) * [mitchhentges](https://github.com/mitchhentges) * [mouse-reeve](https://github.com/mouse-reeve) * [Mozinet-fr](https://github.com/Mozinet-fr) * [lae](https://github.com/lae) * [nosada](https://github.com/nosada) * [Nanamachi](https://github.com/Nanamachi) * [orinthe](https://github.com/orinthe) * [NecroTechno](https://github.com/NecroTechno) * [Dar13](https://github.com/Dar13) * [ngerakines](https://github.com/ngerakines) * [vonneudeck](https://github.com/vonneudeck) * [Ninetailed](https://github.com/Ninetailed) * [k24](https://github.com/k24) * [noiob](https://github.com/noiob) * [kwaio](https://github.com/kwaio) * [norayr](https://github.com/norayr) * [joyeusenoelle](https://github.com/joyeusenoelle) * [OlivierNicole](https://github.com/OlivierNicole) * [noppa](https://github.com/noppa) * [Otakan951](https://github.com/Otakan951) * [fahy](https://github.com/fahy) * [PatrickRWells](mailto:32802366+patrickrwells@users.noreply.github.com) * [Paul](mailto:naydex.mc+github@gmail.com) * [Pete Keen](mailto:pete@petekeen.net) * [Pierre-Morgan Gate](mailto:pgate@users.noreply.github.com) * [Ratmir Karabut](mailto:rkarabut@sfmodern.ru) * [Reto Kromer](mailto:retokromer@users.noreply.github.com) * [Rey Tucker](mailto:git@reytucker.us) * [Rob Watson](mailto:rfwatson@users.noreply.github.com) * [Ryan Freebern](mailto:ryan@freebern.org) * [Ryan Wade](mailto:ryan.wade@protonmail.com) * [Ryo Kajiwara](mailto:kfe-fecn6.prussian@s01.info) * [S.H](mailto:gamelinks007@gmail.com) * [Sadiq Saif](mailto:staticsafe@users.noreply.github.com) * [Sam Hewitt](mailto:hewittsamuel@gmail.com) * [Satoshi KOJIMA](mailto:skoji@mac.com) * [ScienJus](mailto:i@scienjus.com) * [Scott Larkin](mailto:scott@codeclimate.com) * [Sebastian Hübner](mailto:imolein@users.noreply.github.com) * [Sebastian Morr](mailto:sebastian@morr.cc) * [Sergei Č](mailto:noiwex1911@gmail.com) * [Setuu](mailto:yuki764setuu@gmail.com) * [Shaun Gillies](mailto:me@shaungillies.net) * [Shin Adachi](mailto:shn@glucose.jp) * [Shin Kojima](mailto:shin@kojima.org) * [Sho Kusano](mailto:rosylilly@aduca.org) * [Shouko Yu](mailto:imshouko@gmail.com) * [Sina Mashek](mailto:sina@mashek.xyz) * [Soshi Kato](mailto:mail@sossii.com) * [Spanky](mailto:2788886+spankyworks@users.noreply.github.com) * [Stanislas](mailto:angristan@pm.me) * [StefOfficiel](mailto:pichard.stephane@free.fr) * [Steven Tappert](mailto:admin@dark-it.net) * [Svetlozar Todorov](mailto:svetlik@users.noreply.github.com) * [Sébastien Santoro](mailto:dereckson@espace-win.org) * [Tad Thorley](mailto:phaedryx@users.noreply.github.com) * [Takayoshi Nishida](mailto:takayoshi.nishida@gmail.com) * [Takayuki KUSANO](mailto:github@tkusano.jp) * [TakesxiSximada](mailto:takesxi.sximada@gmail.com) * [TheInventrix](mailto:theinventrix@users.noreply.github.com) * [Thomas Alberola](mailto:thomas@needacoffee.fr) * [Toby Deshane](mailto:fortyseven@users.noreply.github.com) * [Toby Pinder](mailto:gigitrix@gmail.com) * [Tomonori Murakami](mailto:crosslife777@gmail.com) * [TomoyaShibata](mailto:wind.of.hometown@gmail.com) * [Treyssat-Vincent Nino](mailto:treyssatvincent@users.noreply.github.com) * [Udo Kramer](mailto:optik@fluffel.io) * [Una](mailto:una@unascribed.com) * [Ushitora Anqou](mailto:ushitora_anqou@yahoo.co.jp) * [Valentin Lorentz](mailto:progval+git@progval.net) * [Vladimir Mincev](mailto:vladimir@canicinteractive.com) * [Waldir Pimenta](mailto:waldyrious@gmail.com) * [Wesley Ellis](mailto:tahnok@gmail.com) * [Wiktor](mailto:wiktor@metacode.biz) * [Wonderfall](mailto:wonderfall@schrodinger.io) * [YDrogen](mailto:ydrogen45@gmail.com) * [YMHuang](mailto:ymhuang@fmbase.tw) * [YOSHIOKA Eiichiro](mailto:yoshioka.eiichiro@gmail.com) * [YOU](mailto:stackexchange.you@gmail.com) * [YaQ](mailto:i_k_o_m_a_7@yahoo.co.jp) * [Yanaken](mailto:yanakend@gmail.com) * [Yann Klis](mailto:yann.klis@gmail.com) * [Yeechan Lu](mailto:wz.bluesnow@gmail.com) * [Yusuke Abe](mailto:moonset20@gmail.com) * [Zachary Spector](mailto:logicaldash@gmail.com) * [ZiiX](mailto:ziix@users.noreply.github.com) * [asria-jp](mailto:is@alicematic.com) * [ava](mailto:vladooku@users.noreply.github.com) * [benklop](mailto:benklop@gmail.com) * [bsky](mailto:git@imbsky.net) * [caesarologia](mailto:lopesgemelli.1@gmail.com) * [cbayerlein](mailto:c.bayerlein@gmail.com) * [chrolis](mailto:chrolis@users.noreply.github.com) * [cormo](mailto:cormorant2+github@gmail.com) * [d0p1](mailto:dopi-sama@hush.com) * [evilny0](mailto:evilny0@moomoocamp.net) * [febrezo](mailto:felixbrezo@gmail.com) * [fsubal](mailto:fsubal@users.noreply.github.com) * [fusshi-](mailto:dikky1218@users.noreply.github.com) * [gentaro](mailto:gentaroooo@gmail.com) * [gol-cha](mailto:info@mevo.xyz) * [hakoai](mailto:hk--76@qa2.so-net.ne.jp) * [haosbvnker](mailto:github@chaosbunker.com) * [isati](mailto:phil@juchnowi.cz) * [jacob](mailto:jacobherringtondeveloper@gmail.com) * [jenn kaplan](mailto:me@jkap.io) * [jirayudech](mailto:jirayudech@gmail.com) * [jomo](mailto:github@jomo.tv) * [jooops](mailto:joops@autistici.org) * [jukper](mailto:jukkaperanto@gmail.com) * [jumoru](mailto:jumoru@mailbox.org) * [karlyeurl](mailto:karl.yeurl@gmail.com) * [kedama](mailto:32974885+kedamadq@users.noreply.github.com) * [kodai](mailto:shirafuta.kodai@gmail.com) * [koyu](mailto:me@koyu.space) * [kuro5hin](mailto:rusty@kuro5hin.org) * [luzpaz](mailto:luzpaz@users.noreply.github.com) * [maxypy](mailto:maxime@mpigou.fr) * [mhe](mailto:mail@marcus-herrmann.com) * [mike castleman](mailto:m@mlcastle.net) * [mimikun](mailto:dzdzble_effort_311@outlook.jp) * [mohemohe](mailto:mohemohe@users.noreply.github.com) * [mshrtkch](mailto:mshrtkch@users.noreply.github.com) * [muan](mailto:muan@github.com) * [namelessGonbai](mailto:43787036+namelessgonbai@users.noreply.github.com) * [neetshin](mailto:neetshin@neetsh.in) * [rch850](mailto:rich850@gmail.com) * [roikale](mailto:roikale@users.noreply.github.com) * [rysiekpl](mailto:rysiek@hackerspace.pl) * [saturday06](mailto:dyob@lunaport.net) * [scriptjunkie](mailto:scriptjunkie@scriptjunkie.us) * [seekr](mailto:mario.drs@gmail.com) * [sundevour](mailto:31990469+sundevour@users.noreply.github.com) * [syui](mailto:syui@users.noreply.github.com) * [tackeyy](mailto:mailto.takita.yusuke@gmail.com) * [tateisu](mailto:tateisu@gmail.com) * [tmyt](mailto:shigure@refy.net) * [trevDev()](mailto:trev@trevdev.ca) * [utam0k](mailto:k0ma@utam0k.jp) * [vpzomtrrfrt](mailto:vpzomtrrfrt@gmail.com) * [walfie](mailto:walfington@gmail.com) * [y-temp4](mailto:y.temp4@gmail.com) * [ymmtmdk](mailto:ymmtmdk@gmail.com) * [yoshipc](mailto:yoooo@yoshipc.net) * [Özcan Zafer AYAN](mailto:ozcanzaferayan@gmail.com) * [ばん](mailto:detteiu0321@gmail.com) * [みたらしだんご](mailto:mitarashidango@users.noreply.github.com) * [りんすき](mailto:6533808+rinsuki@users.noreply.github.com) * [ヨイツの賢狼ホロ | 3rd style](mailto:horo@yoitsu.moe) * [猫吸血鬼ディフリス / 猫ロキP](mailto:deflis@gmail.com) * [艮 鮟鱇](mailto:ushitora_anqou@yahoo.co.jp) * [西小倉宏信](mailto:nishiko@mindia.jp) * [雨宮美羽](mailto:k737566@gmail.com) This document is provided for informational purposes only. Since it is only updated once per release, the version you are looking at may be currently out of date. To see the full list of contributors, consider looking at the [git history](https://github.com/tootsuite/mastodon/graphs/contributors) instead. ## Translators Following people have contributed to translation of Mastodon: - **Albanian** - Besnik Bleta - Aditoo - **Arabic** - ButterflyOfFire - Aditoo - Amrz0 - **Asturian** - ButterflyOfFire - Enol P. - Aditoo - **Basque** - Osoitz - Aditoo - Aitzol - ButterflyOfFire - Peru Iparragirre - Gorka Azkarate - **Bengali** - dxwc - **Bulgarian** - ButterflyOfFire - Aditoo - **Catalan** - spla - Aditoo - ButterflyOfFire - Joan Montané - Jose Luis - **Chinese (Hong Kong)** - ButterflyOfFire - Luzi Leung - Aditoo - **Chinese (Simplified)** - Allen Zhong - ButterflyOfFire - SerCom_KC - martialarts - Kaitian Xie - Aditoo - pan93412 - **Chinese (Traditional)** - Aditoo - ButterflyOfFire - James58899 - pan93412 - S1ttidoe477 - SHA265 - Jeff Huang - **Corsican** - Alix D. R. - Aditoo - ButterflyOfFire - **Croatian** - ButterflyOfFire - Aditoo - **Czech** - Aditoo - Marek Ľach - ButterflyOfFire - **Danish** - Einhjeriar - Rasmus Sæderup - Aditoo - ButterflyOfFire - **Dutch** - Albakham - ButterflyOfFire - jeroenpraat - rscmbbng - Aditoo - Jelv - **English** - ButterflyOfFire - Renato "Lond" Cerqueira - **English (United Kingdom)** - Albakham - **Esperanto** - Aditoo - ButterflyOfFire - Becci Cat - Jeong Arm - Mélanie Chauvel - Vanege - Martin Bodin - tuxayo/Victor Grousset - **Finnish** - ButterflyOfFire - Mikko Poussu - Taru Luojola - S Heija - Aditoo - Jonne Arjoranta - **French** - Albakham - Alix D. R. - ButterflyOfFire - codl - Leia - Alda Marteau-Hardi - Mélanie Chauvel - Paul Marques Mota - azenet - Olivier Humbert - Aditoo - Jonathan Chan - Letiteuf55 - Baptiste Jonglez - goofy-mdn - Jean-Baptiste Holcroft - Technowix - Martin Bodin - Théodore - Thibaut Girka - Franck Paul - Sylvhem - **Galician** - ButterflyOfFire - Xose M. - Aditoo - manequim - **Georgian** - ButterflyOfFire - Aditoo - **German** - Aditoo - ButterflyOfFire - Daniel - averageunicorn - Koyu Berteon - larsreineke - koyu - Austin Jones - lilo - Benedikt Geißler - ePirat - Eugen Rochko - Weblate Admin - Patrick Figel - **Greek** - Dimitris Maroulidis - Antonis - Aditoo - ButterflyOfFire - Konstantinos Grevenitis - **Hebrew** - ButterflyOfFire - Aditoo - Ira - Yaron Shahrabani - **Hungarian** - ButterflyOfFire - Adam Paszternak - Aditoo - Tibike Miklós - **Ido** - ButterflyOfFire - Aditoo - **Indonesian** - afachri - ButterflyOfFire - Dito Kurnia Pratama - Eirworks - Aditoo - Alfiana Sibuea - se7entime - **Irish** - Albakham - Kevin Houlihan - **Italian** - Alessandro Levati - Albakham - ButterflyOfFire - Marcin Mikołajczak - Aditoo - Giuseppe Pignataro - Stefano - **Japanese** - Hinaloe - 小鳥遊まりあ - mayaeh - osapon - 森の子リスのミーコの大冒険 - Kumasun Morino - Yamagishi Kazutoshi - Aditoo - ButterflyOfFire - Jeong Arm - unarist - **Kazakh** - arshat - Aditoo - **Korean** - Aditoo - Jeong Arm - ButterflyOfFire - Minori Hiraoka - Yamagishi Kazutoshi - **Lithuanian** - Sarunas Medeikis - **Malay** - Muhammad Nur Hidayat (MNH48) - Aditoo - ButterflyOfFire - **Norwegian (old code)** - ButterflyOfFire - Espen Rønnevik - Aditoo - Tale - **Occitan** - Aditoo - ButterflyOfFire - Quenti2 - Quentí - Maxenç - **Persian** - Masoud Abkenar - Aditoo - ButterflyOfFire - **Polish** - Aditoo - Albakham - ButterflyOfFire - Stasiek Michalski - Marcin Mikołajczak - Jakub Mendyk - Marek Ľach - krkk - **Portuguese** - Albakham - João Pinheiro - manequim - Aditoo - ButterflyOfFire - Hugo Gameiro - **Portuguese (Brazil)** - Aditoo - Albakham - Anna e só - Renato "Lond" Cerqueira - André Andrade - ButterflyOfFire - **Romanian** - adrianbblk - ButterflyOfFire - Aditoo - **Russian** - Albakham - ButterflyOfFire - Evgeny Petrov - Aditoo - Павел Гастелло - Andrew Zyabin - Yaron Shahrabani - **Serbian** - Branko Kokanovic - Burekz Finezt - Aditoo - ButterflyOfFire - **Serbian (latin)** - ButterflyOfFire - Aditoo - **Slovak** - Aditoo - ButterflyOfFire - Ivan Pleva - Marek Ľach - Peter - **Slovenian** - Kristijan Tkalec - Aditoo - ButterflyOfFire - **Spanish** - Albakham - ButterflyOfFire - Carlos Mondragon - Antón López - Max Winkler - Pablo de la Concepción Sanz - Sergio Soriano - Angeles Broullón - Lothar Wolf - Aditoo - David Charte - Emmanuel - **Swedish** - ButterflyOfFire - Isak Holmström - Shellkr - Aditoo - Elias Mårtenson - Stefan Midjich - Tim Stahel - Jonas Hultén - **Telugu** - avndp - Ranjith Tellakula - Aditoo - ButterflyOfFire - Joseph Nuthalapati - **Thai** - ButterflyOfFire - parnikkapore - Thai Localization - Aditoo - **Turkish** - Ali Demirtas - ButterflyOfFire - Aditoo - **Ukrainian** - alexcleac - ButterflyOfFire - Aditoo - Ivan Verchenko - **Welsh** - carl morris - Jaz-Michael King - Owain Rhys Lewis - Rhoslyn Prys - Aditoo - ButterflyOfFire - Renato "Lond" Cerqueira - Albakham - Kevin Beynon - **Armenian** - Aditoo - ButterflyOfFire - **Latvian** - Aditoo - ButterflyOfFire - Maigonis - **Tamil** - Aditoo - ButterflyOfFire - Prasanna Venkadesh ================================================ FILE: Aptfile ================================================ ffmpeg libicu[0-9][0-9] libicu-dev libidn11 libidn11-dev libpq-dev libprotobuf-dev libssl-dev libxdamage1 libxfixes3 protobuf-compiler zlib1g-dev libcairo2 libcroco3 libdatrie1 libgdk-pixbuf2.0-0 libgraphite2-3 libharfbuzz0b libpango-1.0-0 libpangocairo-1.0-0 libpangoft2-1.0-0 libpixman-1-0 librsvg2-2 libthai-data libthai0 libvpx5 libxcb-render0 libxcb-shm0 libxrender1 ================================================ FILE: CHANGELOG.md ================================================ # Changelog This changelog will only include differences from upstream Mastodon. [You can find the upstream changelog here.](https://github.com/tootsuite/mastodon/blob/master/CHANGELOG.md) Please note that this project doesn't follow semantic versioning— for details please have a look at our [README file]. [README file]: ./README.md ## Pre-Release 0.1.2 [2019-08-18 / v0.0.1.2] This release fixes a bug from 0.1.1 before the 0.2.0 release. It doesn't add any upstream changes, and is still based off of [Mastodon 2.9.0] plus the commits up to [65efe892cf]. ### Fixed * Toot and biography lengths are offered as integers in the API instead of strings * Thanks to [mthld] and [DagAgren] for finding the bug * Thanks to [1011X] for the fix [mthld]: https://github.com/mthld [DagAgren]: https://github.com/DagAgren ## Pre-Release 0.1.1 [2019-07-28 / v0.0.1.1] This release fixes a few bugs from 0.1.0 before the 0.2.0 release. It doesn't add any upstream changes, and is still based off of [Mastodon 2.9.0] plus the commits up to [65efe892cf]. ### Fixed * Toot and biography lengths are now on /api/v1/instance API, for better app integration * Thanks to [ElliotBerriot] for testing 0.1.0 and finding the bug * Thanks to [1011X] for the fix * Toot and biography lengths can actually be saved in the admin UI * Thanks to [ElliotBerriot] for testing 0.1.0 and finding the bug * Thanks to [clarfon] for the fix * All of the code has been fixed to include the proper project URL and docker image * Thanks to [ElliotBerriot] for debugging 0.1.0 and finding the errors with the Docker files * Thanks to [clarfon] for the fix * Broadcasted version has been reverted to the Mastodon version, so that mastodon.py and other libraries work until we find a better solution * Thanks to [Frinkel] for testing 0.1.0 and finding the bug * Thanks to [halcy] for providing insight into how mastodon.py works * Thanks to [1011X] for the fix * Blocking entire instance domains now gives a less judgmental message in the English locale * Thanks to [TrechNex] for the fix * Thanks to [mal0ki] and [clarfon] for reviewing the wording * Florence-specific settings have been translated into Dutch * Thanks to [rscmbbng] for the translations * The [lastest typo] in the README was corrected * Thanks to [ciderpunx] for the fix * A few dependencies were updated to fix various security issues * Prototype pollution vulnerabilities were fixed for handlebars and lodash * Thanks to [1011X] for future-proofing CVE-2015-9284 (OAuth vulnerability) [lastest typo]: https://github.com/florence-social/mastodon-fork/pull/106/files [ciderpunx]: https://github.com/ciderpunx [ElliotBerriot]: https://github.com/ElliotBerriot [Frinkel]: https://github.com/Frinkel [halcy]: https://github.com/halcy [mal0ki]: https://github.com/mal0ki [rscmbbng]: https://github.com/rscmbbng [TrechNex]: https://github.com/TrechNex ### Special Thanks * Thank you to everyone who jumped on the Florence train and set up their own instances! We'll try and compile a list of Florence Mastodon instances some time before the next release. * Thank you to @TrechNex for helping set up [installation instructions] on his website! [installation instructions]: https://bobbymoss.com/index.html#install-florence-prerelease ## Pre-Release 0.1.0 [2019-06-18 / v0.0.1.0] This release is based off of [Mastodon 2.9.0] plus the commits up to [65efe892cf]. [Mastodon 2.9.0]: https://github.com/tootsuite/mastodon/blob/v2.9.0/CHANGELOG.md [65efe892cf]: https://github.com/tootsuite/mastodon/compare/c9eeb2e832b5b36a86028bbec7a353c32be510a7..65efe892cf56cd4f998de885bccc36e9231d8144 ### Added * Toot length can now be configured by an admin; default is 500 * Thanks to glitch.social and many other forks for the original code * Thanks to [usbsnowcrash] for the Florence-specific code * Thanks to [m4sk1n] for the Polish translation * Thanks to [clarfon] and [Feufochmar] for the French translation * Thanks to [1011X] and [skrlet13] for the Spanish translation * Biography length can now be configured by an admin; default is 500 * Thanks to glitch.social and many other forks for the original code * Thanks to [usbsnowcrash] for the Florence-specific code * Thanks to [clarfon] and [Feufochmar] for the French translation * Thanks to [1011X] and [skrlet13] for the Spanish translation * Users can choose whether to receive DMs on the home timeline in their settings * Thanks to glitch.social and many other forks for the original code * Thanks to [usbsnowcrash] for the Florence-specific code * Thanks to [clarfon] and [Feufochmar] for the French translation * Thanks to [1011X] and [skrlet13] for the Spanish translation * Spanish translations were updated to be more formal * Thanks to [1011X] for the inital changes * Thanks to [skrlet13] for offering feedback and further changes [1011X]: https://github.com/1011X [clarfon]: https://github.com/clarfon [Feufochmar]: https://github.com/Feufochmar [m4sk1n]: https://github.com/m4sk1n [skrlet13]: https://github.com/skrlet13 [usbsnowcrash]: https://github.com/usbsnowcrash ### Special Thanks * Thank you to @1011x, @jhaye, @lightdark, @maloki, @skrlet13, @melody, @stolas, @hak, @mecaka: those who've been helping with governance, offering advice, and/or been working on this for the past few months. * Thank you to @woozle for hosting both the Wiki and Mattermost for us on their servers. * Thank you to all the forkers out there who are providing us both with inspiration, actual code, and conversation about how we can make the Fediverse a little bit better. * Thank you to everyone that has been cheerleading us for the past year, helped us have the courage to "Fork Off" from Mastodon, and also understood that we are working towards different objectives. That is after all why you all joined us in the first place! * Thank you to @mecaka who joined the team while @maloki was getting diagnosed, who helped push through that time and bring us to where we are now. * An additional thank you to those that joined the Mattermost server to keep the conversation alive with us after we moved on from Discord! ================================================ FILE: CODE_OF_CONDUCT.md ================================================ ### Table of Contents - [Code of Conduct](#code-of-conduct) - [Positive participation and communication](#positive-participation-and-communication) - [Prohibited Behavior](#prohibited-behavior) - [Discrimination](#discrimination) - [Harassment and privacy violation](#harassment-and-privacy-violation) - [Harmful and/or adult content](#harmful-andor-adult-content) - [Where does this CoC apply and who does this CoC apply to?](#where-does-this-coc-apply-and-who-does-this-coc-apply-to) - [What if someone violates this Code of Conduct while outside of Florence instances, or in a space or medium to which the Code does not apply?](#what-if-someone-violates-this-code-of-conduct-while-outside-of-florence-instances-or-in-a-space-or-medium-to-which-the-code-does-not-apply) - [When Something Happens](#when-something-happens) - [Example](#example) - [How to make a Code Of Conduct Report](#how-to-make-a-code-of-conduct-report) - [Information to include in your report](#information-to-include-in-your-report) - [Code of Conduct Reporting Guide](#code-of-conduct-reporting-guide) - [What happens after you file a report?](#what-happens-after-you-file-a-report) - [Example](#example-1) - [What if your report concerns a possible violation by a moderator?](#what-if-your-report-concerns-a-possible-violation-by-a-moderator) - [How CoC Reports are handled by moderators](#how-coc-reports-are-handled-by-moderators) - [Accountability for board members and moderators](#accountability-for-board-members-and-moderators) - [Definition of Terms](#definition-of-terms) - [Reference Codes of Conduct](#reference-codes-of-conduct) # Code of Conduct We strive to reduce barriers to access of our organization, our software and our community, as well as to dismantle any ableist assumptions and practices in our process. We recognize that there is no one solution for universal accessibility, and strive to provide alternatives and choices whenever possible. The stakeholders of this project pledge to institute and abide by a consistent, transparent and accountable human-centered resolution process with a low bar to entry that allows every stakeholder a voice on decisions to do with structural changes and features of both the project's organization and software protocol. If you support an idea, we would like you to help implement it. If you reject an idea, we ask you to help find alternatives. ## Positive participation and communication Florence wants to encourage positive participation and communication, in order to make people feel welcome. Positive communication doesn't mean tone policing or that everyone always has to be positive about everything. It means that we want people to feel safe when bringing up their concerns, and create a space where people can have the difficult conversations. To attain this together, we want to: * Maintain respectful and effective communication. * Use welcoming and inclusive language. * Show kindness and respect towards others. * Encourage and promote the ideas of others. * Be respectful of differing viewpoints and experiences. * Assume good faith. * Understand the challenges in online discussion. * Give others the chance to improve. * Keep criticism constructive. * Take criticism constructively. * Respect others’ privacy. * Avoid sexualized language without consent of all parties involved. * Don’t resort to personal attacks or condescension. * No hate speech or similar. * Promote and teach respectful, effective communication. * Welcome contributions to the project in all the forms they take. ## Prohibited Behavior There is also going to be behaviour that will be more strictly prohibited. ### Discrimination * Do not participate in discrimination or comments promoting or reinforcing the existing systems of oppression of any groups or people based on gender, gender expression, race, ethnicity, nationality, sexuality, religion, disability, mental illness, neurodivergence, personal appearance, physical appearance, body size, age, or class. * Do not claim “reverse-isms,” e.g. “reverse racism” * Do not participate in xenophobia or violent nationalism. ### Harassment and privacy violation * Do not contribute to behaviour intended to stalk, harass, or intimidate other users. * Do not continue to engage with a user that has specifically asked you to stop, regardless of whether they have blocked or muted you. Do not ask others to engage with them on your behalf. * Do not participate in aggregating, posting, and/or disseminating a person’s demographic, personal, or private data without their express permission. * Do not post or circulate disseminate a person’s posts, including screen captures or any other content, without their express permission, unless to protect others from the bad behaviour displayed. * Do not post or circulate libel, slander, or other known disinformation. ### Harmful and/or adult content * Do not distribute sexual or violent imagery without a Content Warning. * Do not distribute any sexualized depictions of minors, in any way. (This includes drawings and 3D renders.) ## Where does this CoC apply and who does this CoC apply to? This code of conduct applies to all official public discussion forums related to this project, including but not limited to public chat, forums, and comments on code. This code applies to any member of the community participating in discussions, contributing to the project, or otherwise discussing the project publicly. It also would apply in the future main Florence instance. Members of the instance must agree to the CoC. ## What if someone violates this Code of Conduct while outside of Florence instances, or in a space or medium to which the Code does not apply? Use your judgement, but always feel free to let us know -- we’d rather know more than less. If the person who violates the Code of Conduct intersects with the Florence community in any way, we encourage you to make a report, even if the offending behavior itself was outside of our space. ## When Something Happens If you see behavior that is not aligned with this Code of Conduct, here's how you can handle it. Consider the situation and if it might be useful to screenshot messages in case the messages in question are deleted. The person may not be completely aware of the Code of Conduct or specific items within it. **If you consider it safe** and you are comfortable in doing so you can start by letting them know that their actions weren't appropriate and point them to specific parts of the Code of Conduct. One suggestion is to send a private message first, because this can avoid potentially embarrassing someone. Politely and patiently let the person know that their behavior isn’t in line with the Code of Conduct. Share the code of conduct with them and refer to specific parts which you think their behavior is not in line with. You may ask them to adjust their behavior or possibly edit or delete a message. **If you don’t feel safe or comfortable** telling that person, or they refuse to change their behavior or delete text which is not in line with the CoC, it may be time to report the incident. See how to report an incident in the following section. You may also directly contact admins or moderators, especially if the behavior requires immediate attention ### Example Mary calls John an ableist slur in a discussion chat because he made a mistake. Ana tells Mary via DM that she shouldn't do that because it reinforces the oppression of disabled or neurodivergent people, but Mary calls Ana the same slur, and says that she shouldn't take this so seriously. Then, Ana proceeds to report Mary to a moderator. ## How to make a Code Of Conduct Report If you would like to report a Code of Conduct violation contact our moderators via email at report@florencesoc.org. Regardless of outcome, they (or committee) will respond in private to first acknowledge your report within three days and later with an explanation of the actions taken within a time frame of 14 days. Please include any relevant details, links, screenshots, context, or other information that may be used to better understand and resolve the situation. ### Information to include in your report * Contact information for the reporter including name, email and username. * You may report incidents anonymously if you are uncomfortable providing your contact information. However, this may hamper the investigation. * The names of all people directly involved in the incident, including relevant nicknames or pseudonyms. * Include witness names if possible. * Time and forum/location where the incident occurred. Be specific. * Details about what happened. Note any supporting materials, such as message screencaps, IRC logs, or emails. * Additional context for the situation, if appropriate. * Whether or not the incident is ongoing. * Any additional information that is relevant to investigating and resolving the incident. ## Code of Conduct Reporting Guide Most people will feel more comfortable reporting the code of conduct violation rather than directly confronting it, and we encourage those people to email report@florencesoc.org. **All reports will be kept confidential.** In some cases we may determine that a public statement will need to be made in order to inform the broader community. If this need arises, the identities of all parties involved will remain confidential unless those individuals instruct us otherwise. **If you believe anyone is in physical danger, please notify appropriate law enforcement first.** If you are unsure what law enforcement agency is appropriate, please include this in your report and we will attempt to notify them. We encourage you to report incidents, even if you are unsure whether the incident is a violation, or whether the space where it happened is covered by this Code of Conduct. We would much rather have a few extra reports where we decide to take no action, than miss a report of an actual violation. We do not look negatively on you if we find the incident is not a violation. Knowing about incidents that are not violations, or happen outside our spaces, can help us to improve the Code of Conduct and the processes surrounding it. ## What happens after you file a report? You will receive an email from the moderators acknowledging receipt. The current Working Group members are [@1011X@mastodon.social](https://mastodon.social/@1011X), [@jhaye@social.libre.fi](https://social.libre.fi/jhaye), and [@skrlet13@chile.masto.host](https://chile.masto.host/@skrlet13). The moderators will meet to review the incident and determine: * What happened based on the perspective of all involved. * Whether this event constitutes a code of conduct violation. * Who the bad actor was. * Whether this is an ongoing situation, or if there is a threat to anyone's physical safety. We promise to acknowledge receipt within 72 hours (and will aim for a faster response). If this is determined to be an ongoing incident or a threat to physical safety, the working groups' immediate priority will be to protect everyone involved. This means we may delay an "official" response until we believe that the situation has ended and that everyone is physically safe. Once the moderators have a complete account of the events, they will make a decision as to how to respond. Responses may include: * Nothing (if we determine no violation occurred). * A private gentle reminder of the code of conduct. * A public gentle reminder of the code of conduct. * A private reprimand from the working group to the individual(s) involved. * A public reprimand. * An imposed vacation (i.e. asking someone to "take a week off" from a space). * A temporary (for a specified time) or indefinite ban from some or all Florence Work Spaces. We'll respond within one week to the person who filed the report with either a resolution or an explanation of why the situation is not yet resolved. Once we've determined our final action, we'll contact the original reporter to let them know what action (if any) we'll be taking. We'll take into account feedback from the reporter on the appropriateness of our response, but we don't guarantee we'll act on it. ### Example Ana reports Mary. She explains the situation and gives the links and screenshots as proof to the moderator. The moderator examines the case and decides to give Mary a warning. Mary is told this and she will no longer be able to participate if she does that again. Ana is notified of the actions taken by the moderator. ## What if your report concerns a possible violation by a moderator? The entire code of conduct working group will see all incident reports sent to report@florencesoc.org. Anyone directly involved in the incident will be immediately recused and will not participate in any discussions of the incident or its resolution. * If you are uncomfortable submitting a report that will be seen by a person involved in the incident, you can instead send the report directly to the other moderators for resolution. ## How CoC Reports are handled by moderators When a Code of Conduct report is made, moderators should take the issue seriously. If it’s not possible to deal with the report immediately, moderators should send a response saying that the report was received and is being looked into. Moderators will take all reports seriously and prioritize the well-being and comfort of the person making the report recipients of the violation over the comfort of the violator. As soon as available, a moderator will join, identify themselves, and take further action. You should offer people the kind of support they ask for. If you aren't sure what you're offering will be helpful, ask if that type of thing would be useful. ## Accountability for board members and moderators Board members, moderators, and other community members who contributed to running this project will be held to the same code of conduct. Reports against those in positions of power will be taken seriously and handled with respect, prioritizing the safety and well-being of anyone who makes such a report. ## Definition of Terms We acknowledge that systemic (structural) oppression works on a society-wide level, enacted by laws and policies, institutions, and culture. It is reflected in and reinforced by individuals’ discriminatory beliefs and actions. As such, we expect people’s commitment to ending oppression to extend to their own behaviour. * **Ableism**: Discrimination against disabled, ill, and/or neurodivergent people. * **Disability**: A physical or mental condition that limits a person's abilities to interact with their environment. * **Gender**: Social construct tied to the perception of social and cultural roles. The most common are male and female. * **Gender expression**: Includes personal behavior, mannerisms, interests, and appearance associated with gender in a particular cultural context. * **Harassment**: Behaviour towards a person that causes mental or emotional suffering, which includes repeated unwanted contacts without a reasonable purpose, insults, threats, touching, or offensive language. * **Hate speech (includes Holocaust denial or Nazi symbolism)**: Speech that oppresses, dehumanizes, and endangers marginalized people or anyone perceived to have a particular marginalization. * **Homophobia**: Discrimination against gay people and people who are thought to be gay, incl. the use of homophobic terms where they cause harm. * **Inclusivity**: Attitude that recognises and considers people's differences in order to create a welcoming environment without requiring minority assimilation to the majority. * **Marginalization**: A social phenomenon by which a minority or sub-group is excluded, and their needs or desires ignored. * **Mental illness**: Behavioral or mental pattern that causes significant distress or impairment of personal functioning. * **Microaggressions**: brief and commonplace daily verbal, behavioural, or environmental indignities, whether intentional or unintentional, that communicate hostile, derogatory, or negative prejudicial slights and insults toward any group, particularly culturally marginalized groups. * **Neurodivergence**: Behavioral or mental pattern that doesn't cause distress but affects the way the person interacts with the environment. * **Race**: Social construct tied to phenotypical and social characteristics of a group of people. * **Racism**: Discrimination against people of color, including but not limited to microaggressions, tone policing and slurs. * **Reverse-isms** (like “reverse racism”): Claims by people privileged on an axis that they are somehow oppressed by people marginalized on the same axis. * **Sexism**: Discrimination against women and non-binary people. Including derogatory language about women or people perceived as women. * **Sexual orientation**: The pattern of attraction to 0 or more genders. * **Transphobia**: Discrimination against trans people. This includes arguing someone’s gender, policing their presentation, and malicious misgendering. * **Tone Policing**: Attempts to dismiss marginalized people's perspectives by claiming their tone, rather than the content, is unappealing. ## Reference Codes of Conduct * http://open-zfs.org/wiki/Reporting_Guide * https://wiki.snowdrift.coop/community/conduct * https://wealljs.org/code-of-conduct * https://us.pycon.org/2018/about/code-of-conduct/ * https://www.coc-handbook.com/ * https://github.com/ayojs/ayo/blob/latest/CODE_OF_CONDUCT.md * https://www.freebsd.org/internal/code-of-conduct.html * http://geekfeminism.wikia.com/wiki/Code_of_conduct_evaluations * https://www.rust-lang.org/en-US/conduct.html * https://docs.google.com/document/d/1V2-YNCRW-Ya2tFzapwLMPMbqWAJZNw4qE40cIWqZti8/edit# (NOVA DSA Socialist Meetings: Participation Guide) This ones nice because it's specifically designed for meetings run with a similar format to ours, though theirs take place offline. Creative Commons License
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License. ================================================ FILE: CONTRIBUTING.md ================================================ Contributing ============ Thank you for considering contributing to Florence Mastodon You can contribute in the following ways: - Finding and reporting bugs - Translating the Mastodon interface into various languages - Contributing code to Mastodon by fixing bugs or implementing features - Improving the documentation - Joining the conversation on our [Chat] - Participate in OutReach or other activities - Proof-read - And much more If you want to contribute with monetary support you can do so via our [Open Collective](https://opencollective.com/florence-social) (which isn't publicly launched yet) ## Adding an Issue for Bug Report or Feature Request Bug reports and feature suggestions can be submitted to [GitHub Issues](https://github.com/florence-social/mastodon-fork/issues). Do not worry about submitting duplicates, but please make a cursory search to see if any similar reports or request have already been resolved or rejected in the past using the search function. If you find a bug or feature request which matches yours you can join the conversation by adding your commentary to that issue. Try to describe your issue, the use-case, and additional considerations when submitting a feature request. ## Translations We will be using Weblate for translations in the near future, but we're waiting for a free account for Open Source Projects via Weblate themselves, in the meanwhile you can submit PRs with translations, or wait. We will update this document as soon as Weblate is available for translations, and will make public announcements elsewhere. ## Documentation Documentation work will be added soon, and in the meanwhile you can join us in our [Chat] for documentation. [Chat]: https://chat.florencesoc.org/signup_user_complete/?id=2a7237f68937b2c4a99ca25c156e6915 ================================================ FILE: Capfile ================================================ # frozen_string_literal: true require 'capistrano/setup' require 'capistrano/deploy' require 'capistrano/scm/git' install_plugin Capistrano::SCM::Git require 'capistrano/rbenv' require 'capistrano/bundler' require 'capistrano/yarn' require 'capistrano/rails/assets' require 'capistrano/rails/migrations' Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r } ================================================ FILE: Dockerfile ================================================ FROM ubuntu:18.04 as build-dep # Use bash for the shell SHELL ["bash", "-c"] # Install Node ENV NODE_VER="8.15.0" RUN echo "Etc/UTC" > /etc/localtime && \ apt update && \ apt -y install wget make gcc g++ python && \ cd ~ && \ wget https://nodejs.org/download/release/v$NODE_VER/node-v$NODE_VER.tar.gz && \ tar xf node-v$NODE_VER.tar.gz && \ cd node-v$NODE_VER && \ ./configure --prefix=/opt/node && \ make -j$(nproc) > /dev/null && \ make install # Install jemalloc ENV JE_VER="5.1.0" RUN apt update && \ apt -y install autoconf && \ cd ~ && \ wget https://github.com/jemalloc/jemalloc/archive/$JE_VER.tar.gz && \ tar xf $JE_VER.tar.gz && \ cd jemalloc-$JE_VER && \ ./autogen.sh && \ ./configure --prefix=/opt/jemalloc && \ make -j$(nproc) > /dev/null && \ make install_bin install_include install_lib # Install ruby ENV RUBY_VER="2.6.1" ENV CPPFLAGS="-I/opt/jemalloc/include" ENV LDFLAGS="-L/opt/jemalloc/lib/" RUN apt update && \ apt -y install build-essential \ bison libyaml-dev libgdbm-dev libreadline-dev \ libncurses5-dev libffi-dev zlib1g-dev libssl-dev && \ cd ~ && \ wget https://cache.ruby-lang.org/pub/ruby/${RUBY_VER%.*}/ruby-$RUBY_VER.tar.gz && \ tar xf ruby-$RUBY_VER.tar.gz && \ cd ruby-$RUBY_VER && \ ./configure --prefix=/opt/ruby \ --with-jemalloc \ --with-shared \ --disable-install-doc && \ ln -s /opt/jemalloc/lib/* /usr/lib/ && \ make -j$(nproc) > /dev/null && \ make install ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin" RUN npm install -g yarn && \ gem install bundler && \ apt update && \ apt -y install git libicu-dev libidn11-dev \ libpq-dev libprotobuf-dev protobuf-compiler COPY Gemfile* package.json yarn.lock /opt/mastodon/ RUN cd /opt/mastodon && \ bundle install -j$(nproc) --deployment --without development test && \ yarn install --pure-lockfile FROM ubuntu:18.04 # Copy over all the langs needed for runtime COPY --from=build-dep /opt/node /opt/node COPY --from=build-dep /opt/ruby /opt/ruby COPY --from=build-dep /opt/jemalloc /opt/jemalloc # Add more PATHs to the PATH ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin:/opt/mastodon/bin" # Create the mastodon user ARG UID=991 ARG GID=991 RUN apt update && \ echo "Etc/UTC" > /etc/localtime && \ ln -s /opt/jemalloc/lib/* /usr/lib/ && \ apt install -y whois wget && \ addgroup --gid $GID mastodon && \ useradd -m -u $UID -g $GID -d /opt/mastodon mastodon && \ echo "mastodon:`head /dev/urandom | tr -dc A-Za-z0-9 | head -c 24 | mkpasswd -s -m sha-256`" | chpasswd # Install mastodon runtime deps RUN apt -y --no-install-recommends install \ libssl1.1 libpq5 imagemagick ffmpeg \ libicu60 libprotobuf10 libidn11 libyaml-0-2 \ file ca-certificates tzdata libreadline7 && \ apt -y install gcc && \ ln -s /opt/mastodon /mastodon && \ gem install bundler && \ rm -rf /var/cache && \ rm -rf /var/lib/apt/lists/* # Add tini ENV TINI_VERSION="0.18.0" ENV TINI_SUM="12d20136605531b09a2c2dac02ccee85e1b874eb322ef6baf7561cd93f93c855" ADD https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini /tini RUN echo "$TINI_SUM tini" | sha256sum -c - RUN chmod +x /tini # Copy over mastodon source, and dependencies from building, and set permissions COPY --chown=mastodon:mastodon . /opt/mastodon COPY --from=build-dep --chown=mastodon:mastodon /opt/mastodon /opt/mastodon # Run mastodon services in prod mode ENV RAILS_ENV="production" ENV NODE_ENV="production" # Tell rails to serve static files ENV RAILS_SERVE_STATIC_FILES="true" # Set the run user USER mastodon # Precompile assets RUN cd ~ && \ OTP_SECRET=precompile_placeholder SECRET_KEY_BASE=precompile_placeholder rails assets:precompile && \ yarn cache clean # Set the work dir and the container entry point WORKDIR /opt/mastodon ENTRYPOINT ["/tini", "--"] ================================================ FILE: Gemfile ================================================ # frozen_string_literal: true source 'https://rubygems.org' ruby '>= 2.4.0', '< 2.7.0' gem 'pkg-config', '~> 1.3' gem 'puma', '~> 3.12' gem 'rails', '~> 5.2.3' gem 'thor', '~> 0.20' gem 'hamlit-rails', '~> 0.2' gem 'pg', '~> 1.1' gem 'makara', '~> 0.4' gem 'pghero', '~> 2.2' gem 'dotenv-rails', '~> 2.7' gem 'aws-sdk-s3', '~> 1.41', require: false gem 'fog-core', '<= 2.1.0' gem 'fog-openstack', '~> 0.3', require: false gem 'paperclip', '~> 6.0' gem 'paperclip-av-transcoder', '~> 0.6' gem 'streamio-ffmpeg', '~> 3.0' gem 'blurhash', '~> 0.1' gem 'active_model_serializers', '~> 0.10' gem 'addressable', '~> 2.6' gem 'bootsnap', '~> 1.4', require: false gem 'browser' gem 'charlock_holmes', '~> 0.7.6' gem 'iso-639' gem 'chewy', '~> 5.0' gem 'cld3', '~> 3.2.4' gem 'devise', '~> 4.7' gem 'devise-two-factor', '~> 3.0' group :pam_authentication, optional: true do gem 'devise_pam_authenticatable2', '~> 9.2' end gem 'net-ldap', '~> 0.10' gem 'omniauth-cas', '~> 1.1' gem 'omniauth-rails_csrf_protection', '~> 0.1' gem 'omniauth-saml', '~> 1.10' gem 'omniauth', '~> 1.9' gem 'doorkeeper', '~> 5.1' gem 'fast_blank', '~> 1.0' gem 'fastimage' gem 'goldfinger', '~> 2.1' gem 'hiredis', '~> 0.6' gem 'redis-namespace', '~> 1.5' gem 'htmlentities', '~> 4.3' gem 'http', '~> 3.3' gem 'http_accept_language', '~> 2.1' gem 'http_parser.rb', '~> 0.6', git: 'https://github.com/tmm1/http_parser.rb', ref: '54b17ba8c7d8d20a16dfc65d1775241833219cf2' gem 'httplog', '~> 1.3' gem 'idn-ruby', require: 'idn' gem 'kaminari', '~> 1.1' gem 'link_header', '~> 0.0' gem 'mime-types', '~> 3.2', require: 'mime/types/columnar' gem 'nokogiri', '~> 1.10' gem 'nsa', '~> 0.2' gem 'oj', '~> 3.7' gem 'ostatus2', '~> 2.0' gem 'ox', '~> 2.10' gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c' gem 'pundit', '~> 2.0' gem 'premailer-rails' gem 'rack-attack', '~> 6.0' gem 'rack-cors', '~> 1.0', require: 'rack/cors' gem 'rails-i18n', '~> 5.1' gem 'rails-settings-cached', '~> 0.6' gem 'redis', '~> 4.1', require: ['redis', 'redis/connection/hiredis'] gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' gem 'rqrcode', '~> 0.10' gem 'sanitize', '~> 5.0' gem 'sidekiq', '~> 5.2' gem 'sidekiq-scheduler', '~> 3.0' gem 'sidekiq-unique-jobs', '~> 6.0' gem 'sidekiq-bulk', '~>0.2.0' gem 'simple-navigation', '~> 4.0' gem 'simple_form', '~> 4.1' gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie' gem 'stoplight', '~> 2.1.3' gem 'strong_migrations', '~> 0.4' gem 'tty-command', '~> 0.8', require: false gem 'tty-prompt', '~> 0.19', require: false gem 'twitter-text', '~> 1.14' gem 'tzinfo-data', '~> 1.2019' gem 'webpacker', '~> 4.0' gem 'webpush' gem 'json-ld', '~> 3.0' gem 'json-ld-preloaded', '~> 3.0' gem 'rdf-normalize', '~> 0.3' group :development, :test do gem 'fabrication', '~> 2.20' gem 'fuubar', '~> 2.4' gem 'i18n-tasks', '~> 0.9', require: false gem 'pry-byebug', '~> 3.7' gem 'pry-rails', '~> 0.3' gem 'rspec-rails', '~> 3.8' end group :production, :test do gem 'private_address_check', '~> 0.5' end group :test do gem 'capybara', '~> 3.22' gem 'climate_control', '~> 0.2' gem 'faker', '~> 1.9' gem 'microformats', '~> 4.1' gem 'rails-controller-testing', '~> 1.0' gem 'rspec-sidekiq', '~> 3.0' gem 'simplecov', '~> 0.16', require: false gem 'webmock', '~> 3.5' gem 'parallel_tests', '~> 2.29' end group :development do gem 'active_record_query_trace', '~> 1.6' gem 'annotate', '~> 2.7' gem 'better_errors', '~> 2.5' gem 'binding_of_caller', '~> 0.7' gem 'bullet', '~> 6.0' gem 'letter_opener', '~> 1.7' gem 'letter_opener_web', '~> 1.3' gem 'memory_profiler' gem 'rubocop', '~> 0.71', require: false gem 'rubocop-rails', '~> 2.0', require: false gem 'brakeman', '~> 4.5', require: false gem 'bundler-audit', '~> 0.6', require: false gem 'capistrano', '~> 3.11' gem 'capistrano-rails', '~> 1.4' gem 'capistrano-rbenv', '~> 2.1' gem 'capistrano-yarn', '~> 2.0' gem 'derailed_benchmarks' gem 'stackprof' end group :production do gem 'lograge', '~> 0.11' gem 'redis-rails', '~> 5.0' end gem 'concurrent-ruby', require: false ================================================ FILE: LICENSE ================================================ GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Remote Network Interaction; Use with the GNU General Public License. Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . ================================================ FILE: Procfile ================================================ web: bundle exec puma -C config/puma.rb worker: bundle exec sidekiq ================================================ FILE: Procfile.dev ================================================ web: env PORT=3000 bundle exec puma -C config/puma.rb sidekiq: env PORT=3000 bundle exec sidekiq stream: env PORT=4000 yarn run start webpack: ./bin/webpack-dev-server --listen-host 0.0.0.0 ================================================ FILE: README.md ================================================ Florence Mastodon ================= Mastodon is a **free, open-source social network server** based on ActivityPub. This is *not* the official version of Mastodon; this is a separate version (i.e. a fork) maintained by Florence. For more information on Mastodon, you can see the [official website] and the [upstream repo]. [official website]: https://joinmastodon.org [upstream repo]: https://github.com/tootsuite/mastodon This version of Mastodon will include much-wanted changes by the community that are not included in the upstream version of Mastodon. Migrating from the latest stable release of Mastodon to Florence's Mastodon will always be possible, to ensure that everyone can benefit from these changes. ## Versioning Florence Mastodon uses a four-numbered versioning system, loosely based upon [semantic versioning]. The four numbers are: * **Compatibility**: Increased when federation, app compatibility, etc. are changed in a non-compatibile way. * **Feel**: Increased when user experience is changed strongly enough to feel different, i.e. more than just small new features. * **Features**: Increased when new features are added. Reset to zero when **feel** version is bumped. * **Hotfixes**: Increased when fixes are substantial enough to release a new version without any new features. Reset to zero when **feature** version is bumped. For now, because this versioning system hasn't been strongly adopted, releases will be annotated as **Pre-Release x.y.z**, which is equivalent to version 0.x.y.z. [semantic versioning]: https://semver.org ## Release timeline Pre-release 0.1.0 is mostly equivalent to Mastodon 2.9.0, with some extra changes added in. Right now, the goal before pre-release 1.0.0 is to incorporate existing, already-developed changes into the fork so that people have a central version to upgrade to. Once we've finally gotten the software to the point where we like it, we will release the first official release, which will be named something special. Stay tuned! ## License Copyright (C) 2016-2019 Florence, Eugen Rochko, and many other Mastodon contributors; see [AUTHORS.md](AUTHORS.md). This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . ================================================ FILE: Rakefile ================================================ # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. require File.expand_path('../config/application', __FILE__) Rails.application.load_tasks ================================================ FILE: Vagrantfile ================================================ # -*- mode: ruby -*- # vi: set ft=ruby : ENV["PORT"] ||= "3000" $provision = < 中国域名网站

网址大全

中文域名简介

“中国域名”是中文域名的一种,特指以“中国”为后缀的中文域名,是我国域名体系和全球互联网域名体系的重要组成部分。“中国”是在全球互联网上代表中国的中文顶级域名,于2010年7月正式纳入全球互联网域名体系,全球互联网域名体系,全球网民可通过联网计算机在世界任何国家和地区实现无障碍访问。“中国”域名在使用上和 .CN,相似属于互联网上的基础服务,基于域名可以提供WWW.EMAIL FTP等应用服务。

================================================ FILE: spec/fixtures/requests/json-ld.activitystreams.txt ================================================ HTTP/1.1 200 OK Date: Tue, 01 May 2018 23:25:57 GMT Content-Location: activitystreams.jsonld Vary: negotiate,accept TCN: choice Last-Modified: Mon, 16 Apr 2018 00:28:23 GMT ETag: "1eb0-569ec4caa97c0;d3-540ee27e0eec0" Accept-Ranges: bytes Content-Length: 7856 Cache-Control: max-age=21600 Expires: Wed, 02 May 2018 05:25:57 GMT P3P: policyref="http://www.w3.org/2014/08/p3p.xml" Access-Control-Allow-Origin: * Content-Type: application/ld+json Strict-Transport-Security: max-age=15552000; includeSubdomains; preload Content-Security-Policy: upgrade-insecure-requests { "@context": { "@vocab": "_:", "xsd": "http://www.w3.org/2001/XMLSchema#", "as": "https://www.w3.org/ns/activitystreams#", "ldp": "http://www.w3.org/ns/ldp#", "id": "@id", "type": "@type", "Accept": "as:Accept", "Activity": "as:Activity", "IntransitiveActivity": "as:IntransitiveActivity", "Add": "as:Add", "Announce": "as:Announce", "Application": "as:Application", "Arrive": "as:Arrive", "Article": "as:Article", "Audio": "as:Audio", "Block": "as:Block", "Collection": "as:Collection", "CollectionPage": "as:CollectionPage", "Relationship": "as:Relationship", "Create": "as:Create", "Delete": "as:Delete", "Dislike": "as:Dislike", "Document": "as:Document", "Event": "as:Event", "Follow": "as:Follow", "Flag": "as:Flag", "Group": "as:Group", "Ignore": "as:Ignore", "Image": "as:Image", "Invite": "as:Invite", "Join": "as:Join", "Leave": "as:Leave", "Like": "as:Like", "Link": "as:Link", "Mention": "as:Mention", "Note": "as:Note", "Object": "as:Object", "Offer": "as:Offer", "OrderedCollection": "as:OrderedCollection", "OrderedCollectionPage": "as:OrderedCollectionPage", "Organization": "as:Organization", "Page": "as:Page", "Person": "as:Person", "Place": "as:Place", "Profile": "as:Profile", "Question": "as:Question", "Reject": "as:Reject", "Remove": "as:Remove", "Service": "as:Service", "TentativeAccept": "as:TentativeAccept", "TentativeReject": "as:TentativeReject", "Tombstone": "as:Tombstone", "Undo": "as:Undo", "Update": "as:Update", "Video": "as:Video", "View": "as:View", "Listen": "as:Listen", "Read": "as:Read", "Move": "as:Move", "Travel": "as:Travel", "IsFollowing": "as:IsFollowing", "IsFollowedBy": "as:IsFollowedBy", "IsContact": "as:IsContact", "IsMember": "as:IsMember", "subject": { "@id": "as:subject", "@type": "@id" }, "relationship": { "@id": "as:relationship", "@type": "@id" }, "actor": { "@id": "as:actor", "@type": "@id" }, "attributedTo": { "@id": "as:attributedTo", "@type": "@id" }, "attachment": { "@id": "as:attachment", "@type": "@id" }, "bcc": { "@id": "as:bcc", "@type": "@id" }, "bto": { "@id": "as:bto", "@type": "@id" }, "cc": { "@id": "as:cc", "@type": "@id" }, "context": { "@id": "as:context", "@type": "@id" }, "current": { "@id": "as:current", "@type": "@id" }, "first": { "@id": "as:first", "@type": "@id" }, "generator": { "@id": "as:generator", "@type": "@id" }, "icon": { "@id": "as:icon", "@type": "@id" }, "image": { "@id": "as:image", "@type": "@id" }, "inReplyTo": { "@id": "as:inReplyTo", "@type": "@id" }, "items": { "@id": "as:items", "@type": "@id" }, "instrument": { "@id": "as:instrument", "@type": "@id" }, "orderedItems": { "@id": "as:items", "@type": "@id", "@container": "@list" }, "last": { "@id": "as:last", "@type": "@id" }, "location": { "@id": "as:location", "@type": "@id" }, "next": { "@id": "as:next", "@type": "@id" }, "object": { "@id": "as:object", "@type": "@id" }, "oneOf": { "@id": "as:oneOf", "@type": "@id" }, "anyOf": { "@id": "as:anyOf", "@type": "@id" }, "closed": { "@id": "as:closed", "@type": "xsd:dateTime" }, "origin": { "@id": "as:origin", "@type": "@id" }, "accuracy": { "@id": "as:accuracy", "@type": "xsd:float" }, "prev": { "@id": "as:prev", "@type": "@id" }, "preview": { "@id": "as:preview", "@type": "@id" }, "replies": { "@id": "as:replies", "@type": "@id" }, "result": { "@id": "as:result", "@type": "@id" }, "audience": { "@id": "as:audience", "@type": "@id" }, "partOf": { "@id": "as:partOf", "@type": "@id" }, "tag": { "@id": "as:tag", "@type": "@id" }, "target": { "@id": "as:target", "@type": "@id" }, "to": { "@id": "as:to", "@type": "@id" }, "url": { "@id": "as:url", "@type": "@id" }, "altitude": { "@id": "as:altitude", "@type": "xsd:float" }, "content": "as:content", "contentMap": { "@id": "as:content", "@container": "@language" }, "name": "as:name", "nameMap": { "@id": "as:name", "@container": "@language" }, "duration": { "@id": "as:duration", "@type": "xsd:duration" }, "endTime": { "@id": "as:endTime", "@type": "xsd:dateTime" }, "height": { "@id": "as:height", "@type": "xsd:nonNegativeInteger" }, "href": { "@id": "as:href", "@type": "@id" }, "hreflang": "as:hreflang", "latitude": { "@id": "as:latitude", "@type": "xsd:float" }, "longitude": { "@id": "as:longitude", "@type": "xsd:float" }, "mediaType": "as:mediaType", "published": { "@id": "as:published", "@type": "xsd:dateTime" }, "radius": { "@id": "as:radius", "@type": "xsd:float" }, "rel": "as:rel", "startIndex": { "@id": "as:startIndex", "@type": "xsd:nonNegativeInteger" }, "startTime": { "@id": "as:startTime", "@type": "xsd:dateTime" }, "summary": "as:summary", "summaryMap": { "@id": "as:summary", "@container": "@language" }, "totalItems": { "@id": "as:totalItems", "@type": "xsd:nonNegativeInteger" }, "units": "as:units", "updated": { "@id": "as:updated", "@type": "xsd:dateTime" }, "width": { "@id": "as:width", "@type": "xsd:nonNegativeInteger" }, "describes": { "@id": "as:describes", "@type": "@id" }, "formerType": { "@id": "as:formerType", "@type": "@id" }, "deleted": { "@id": "as:deleted", "@type": "xsd:dateTime" }, "inbox": { "@id": "ldp:inbox", "@type": "@id" }, "outbox": { "@id": "as:outbox", "@type": "@id" }, "following": { "@id": "as:following", "@type": "@id" }, "followers": { "@id": "as:followers", "@type": "@id" }, "streams": { "@id": "as:streams", "@type": "@id" }, "preferredUsername": "as:preferredUsername", "endpoints": { "@id": "as:endpoints", "@type": "@id" }, "uploadMedia": { "@id": "as:uploadMedia", "@type": "@id" }, "proxyUrl": { "@id": "as:proxyUrl", "@type": "@id" }, "liked": { "@id": "as:liked", "@type": "@id" }, "oauthAuthorizationEndpoint": { "@id": "as:oauthAuthorizationEndpoint", "@type": "@id" }, "oauthTokenEndpoint": { "@id": "as:oauthTokenEndpoint", "@type": "@id" }, "provideClientKey": { "@id": "as:provideClientKey", "@type": "@id" }, "signClientKey": { "@id": "as:signClientKey", "@type": "@id" }, "sharedInbox": { "@id": "as:sharedInbox", "@type": "@id" }, "Public": { "@id": "as:Public", "@type": "@id" }, "source": "as:source", "likes": { "@id": "as:likes", "@type": "@id" }, "shares": { "@id": "as:shares", "@type": "@id" } } } ================================================ FILE: spec/fixtures/requests/json-ld.identity.txt ================================================ HTTP/1.1 200 OK Accept-Ranges: bytes Access-Control-Allow-Headers: DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Accept-Encoding Access-Control-Allow-Origin: * Content-Type: application/ld+json Date: Tue, 01 May 2018 23:28:21 GMT Etag: "e26-547a6fc75b04a-gzip" Last-Modified: Fri, 03 Feb 2017 21:30:09 GMT Server: Apache/2.4.7 (Ubuntu) Vary: Accept-Encoding Transfer-Encoding: chunked { "@context": { "id": "@id", "type": "@type", "cred": "https://w3id.org/credentials#", "dc": "http://purl.org/dc/terms/", "identity": "https://w3id.org/identity#", "perm": "https://w3id.org/permissions#", "ps": "https://w3id.org/payswarm#", "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", "rdfs": "http://www.w3.org/2000/01/rdf-schema#", "sec": "https://w3id.org/security#", "schema": "http://schema.org/", "xsd": "http://www.w3.org/2001/XMLSchema#", "Group": "https://www.w3.org/ns/activitystreams#Group", "claim": {"@id": "cred:claim", "@type": "@id"}, "credential": {"@id": "cred:credential", "@type": "@id"}, "issued": {"@id": "cred:issued", "@type": "xsd:dateTime"}, "issuer": {"@id": "cred:issuer", "@type": "@id"}, "recipient": {"@id": "cred:recipient", "@type": "@id"}, "Credential": "cred:Credential", "CryptographicKeyCredential": "cred:CryptographicKeyCredential", "about": {"@id": "schema:about", "@type": "@id"}, "address": {"@id": "schema:address", "@type": "@id"}, "addressCountry": "schema:addressCountry", "addressLocality": "schema:addressLocality", "addressRegion": "schema:addressRegion", "comment": "rdfs:comment", "created": {"@id": "dc:created", "@type": "xsd:dateTime"}, "creator": {"@id": "dc:creator", "@type": "@id"}, "description": "schema:description", "email": "schema:email", "familyName": "schema:familyName", "givenName": "schema:givenName", "image": {"@id": "schema:image", "@type": "@id"}, "label": "rdfs:label", "name": "schema:name", "postalCode": "schema:postalCode", "streetAddress": "schema:streetAddress", "title": "dc:title", "url": {"@id": "schema:url", "@type": "@id"}, "Person": "schema:Person", "PostalAddress": "schema:PostalAddress", "Organization": "schema:Organization", "identityService": {"@id": "identity:identityService", "@type": "@id"}, "idp": {"@id": "identity:idp", "@type": "@id"}, "Identity": "identity:Identity", "paymentProcessor": "ps:processor", "preferences": {"@id": "ps:preferences", "@type": "@vocab"}, "cipherAlgorithm": "sec:cipherAlgorithm", "cipherData": "sec:cipherData", "cipherKey": "sec:cipherKey", "digestAlgorithm": "sec:digestAlgorithm", "digestValue": "sec:digestValue", "domain": "sec:domain", "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"}, "initializationVector": "sec:initializationVector", "member": {"@id": "schema:member", "@type": "@id"}, "memberOf": {"@id": "schema:memberOf", "@type": "@id"}, "nonce": "sec:nonce", "normalizationAlgorithm": "sec:normalizationAlgorithm", "owner": {"@id": "sec:owner", "@type": "@id"}, "password": "sec:password", "privateKey": {"@id": "sec:privateKey", "@type": "@id"}, "privateKeyPem": "sec:privateKeyPem", "publicKey": {"@id": "sec:publicKey", "@type": "@id"}, "publicKeyPem": "sec:publicKeyPem", "publicKeyService": {"@id": "sec:publicKeyService", "@type": "@id"}, "revoked": {"@id": "sec:revoked", "@type": "xsd:dateTime"}, "signature": "sec:signature", "signatureAlgorithm": "sec:signatureAlgorithm", "signatureValue": "sec:signatureValue", "CryptographicKey": "sec:Key", "EncryptedMessage": "sec:EncryptedMessage", "GraphSignature2012": "sec:GraphSignature2012", "LinkedDataSignature2015": "sec:LinkedDataSignature2015", "accessControl": {"@id": "perm:accessControl", "@type": "@id"}, "writePermission": {"@id": "perm:writePermission", "@type": "@id"} } } ================================================ FILE: spec/fixtures/requests/json-ld.security.txt ================================================ HTTP/1.1 200 OK Accept-Ranges: bytes Access-Control-Allow-Headers: DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Accept-Encoding Access-Control-Allow-Origin: * Content-Type: application/ld+json Date: Wed, 02 May 2018 16:25:32 GMT Etag: "7e3-5651ec0f7c5ed-gzip" Last-Modified: Tue, 13 Feb 2018 21:34:04 GMT Server: Apache/2.4.7 (Ubuntu) Vary: Accept-Encoding Content-Length: 2019 { "@context": { "id": "@id", "type": "@type", "dc": "http://purl.org/dc/terms/", "sec": "https://w3id.org/security#", "xsd": "http://www.w3.org/2001/XMLSchema#", "EcdsaKoblitzSignature2016": "sec:EcdsaKoblitzSignature2016", "Ed25519Signature2018": "sec:Ed25519Signature2018", "EncryptedMessage": "sec:EncryptedMessage", "GraphSignature2012": "sec:GraphSignature2012", "LinkedDataSignature2015": "sec:LinkedDataSignature2015", "LinkedDataSignature2016": "sec:LinkedDataSignature2016", "CryptographicKey": "sec:Key", "authenticationTag": "sec:authenticationTag", "canonicalizationAlgorithm": "sec:canonicalizationAlgorithm", "cipherAlgorithm": "sec:cipherAlgorithm", "cipherData": "sec:cipherData", "cipherKey": "sec:cipherKey", "created": {"@id": "dc:created", "@type": "xsd:dateTime"}, "creator": {"@id": "dc:creator", "@type": "@id"}, "digestAlgorithm": "sec:digestAlgorithm", "digestValue": "sec:digestValue", "domain": "sec:domain", "encryptionKey": "sec:encryptionKey", "expiration": {"@id": "sec:expiration", "@type": "xsd:dateTime"}, "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"}, "initializationVector": "sec:initializationVector", "iterationCount": "sec:iterationCount", "nonce": "sec:nonce", "normalizationAlgorithm": "sec:normalizationAlgorithm", "owner": {"@id": "sec:owner", "@type": "@id"}, "password": "sec:password", "privateKey": {"@id": "sec:privateKey", "@type": "@id"}, "privateKeyPem": "sec:privateKeyPem", "publicKey": {"@id": "sec:publicKey", "@type": "@id"}, "publicKeyBase58": "sec:publicKeyBase58", "publicKeyPem": "sec:publicKeyPem", "publicKeyService": {"@id": "sec:publicKeyService", "@type": "@id"}, "revoked": {"@id": "sec:revoked", "@type": "xsd:dateTime"}, "salt": "sec:salt", "signature": "sec:signature", "signatureAlgorithm": "sec:signingAlgorithm", "signatureValue": "sec:signatureValue" } } ================================================ FILE: spec/fixtures/requests/koi8-r.txt ================================================ HTTP/1.1 200 OK Server: nginx/1.11.10 Date: Tue, 04 Jul 2017 16:43:39 GMT Content-Type: text/html Content-Length: 273 Connection: keep-alive Last-Modified: Tue, 04 Jul 2017 16:41:34 GMT Accept-Ranges: bytes XVI . .

XVI . .


================================================ FILE: spec/fixtures/requests/localdomain-feed.txt ================================================ HTTP/1.1 200 OK Date: Thu, 20 Apr 2017 07:36:08 GMT Content-Type: application/atom+xml; charset=utf-8 Transfer-Encoding: chunked Connection: keep-alive Server: Mastodon X-Frame-Options: DENY X-Content-Type-Options: nosniff X-XSS-Protection: 1; mode=block Link: ; rel="lrdd"; type="application/xrd+xml", ; rel="alternate"; type="application/atom+xml" Vary: Accept-Encoding ETag: W/"1fa54baac599205a1e54c136dea2b671" Cache-Control: max-age=0, private, must-revalidate Set-Cookie: _mastodon_session=Vk5XbERyQ0NscjJhdEw1eVEyY3JwQTlBVThObUJ1N3NFcVlJaCtpNU5FSmZlTzFIZ2FqSzhVY1lneFlLQ1haNkh1SDM5L0FSdnFLTGwwTnhJMy9qWWI5aWRnM1NOU1NLTmtuamR5cG5Ebm8vekFNL20ydGkxYXFXU2FwVTF1NnctLXdxdFhNVFA2VmlFVm5BY25QU2N1clE9PQ%3D%3D--47e86fed56f94d3998bfc3837af8de93ec8c104e; path=/; secure; HttpOnly X-Request-Id: 071ec889-04fb-4efa-b55e-81eb90772b50 X-Runtime: 1.173933 Strict-Transport-Security: max-age=31536000; includeSubDomains https://webdomain.com/users/foo.atom foo foo 2017-04-08T15:38:58Z https://quitter.no/avatar/7477-300-20160211190340.png https://webdomain.com/users/foo http://activitystrea.ms/schema/1.0/person https://webdomain.com/users/foo foo foo@localdomain.com foo foo foo foo public tag:localdomain.com,2017-04-19:objectId=12774:objectType=Status 2017-04-19T22:28:01Z 2017-04-19T22:28:01Z New status by foo http://activitystrea.ms/schema/1.0/comment http://activitystrea.ms/schema/1.0/post <p>Meh, ça foire l&apos;attribution des boosts.<br />Faudra que je corrige ça…</p> unlisted ================================================ FILE: spec/fixtures/requests/localdomain-hostmeta.txt ================================================ HTTP/1.1 200 OK Server: nginx/1.6.2 Date: Sun, 20 Mar 2016 11:11:00 GMT Content-Type: application/xrd+xml Transfer-Encoding: chunked Connection: keep-alive Access-Control-Allow-Origin: * Vary: Accept-Encoding,Cookie Strict-Transport-Security: max-age=31536000; includeSubdomains; ================================================ FILE: spec/fixtures/requests/localdomain-webfinger.txt ================================================ HTTP/1.1 200 OK Server: nginx/1.6.2 Date: Sun, 20 Mar 2016 11:11:00 GMT Content-Type: application/xrd+xml Transfer-Encoding: chunked Connection: keep-alive Access-Control-Allow-Origin: * Vary: Accept-Encoding,Cookie Strict-Transport-Security: max-age=31536000; includeSubdomains; acct:foo@localdomain.com https://webdomain.com/@foo ================================================ FILE: spec/fixtures/requests/oembed_invalid_xml.html ================================================ ================================================ FILE: spec/fixtures/requests/oembed_json.html ================================================ ================================================ FILE: spec/fixtures/requests/oembed_json_empty.html ================================================ ================================================ FILE: spec/fixtures/requests/oembed_json_xml.html ================================================ ================================================ FILE: spec/fixtures/requests/oembed_undiscoverable.html ================================================ ================================================ FILE: spec/fixtures/requests/oembed_xml.html ================================================ ================================================ FILE: spec/fixtures/requests/redirected.host-meta.txt ================================================ HTTP/1.1 200 OK Server: nginx/1.6.2 Date: Sun, 20 Mar 2016 11:11:00 GMT Content-Type: application/xrd+xml Transfer-Encoding: chunked Connection: keep-alive Access-Control-Allow-Origin: * Vary: Accept-Encoding,Cookie Strict-Transport-Security: max-age=31536000; includeSubdomains; ================================================ FILE: spec/fixtures/requests/sjis.txt ================================================ HTTP/1.1 200 OK Server: nginx/1.11.10 Date: Tue, 04 Jul 2017 16:43:39 GMT Content-Type: text/html Content-Length: 273 Connection: keep-alive Last-Modified: Tue, 04 Jul 2017 16:41:34 GMT Accept-Ranges: bytes SJIS̃y[W

N܂ĂLOlĂ̂̎łłBԂɈӖ҂͐ǂȔ܂܂ł\グ邽ɂ͎QlA邽Aɂ܂ȂB炢Ȃ̂͂ǂ㌎ł邾BĉcɔRKɉ]ł͂͂Ȃw}ƂoȂȂāA͎͉̐̂A{炩Av̂̂̂‚ɂ]ƌ΂manɂ֎Q悤ɓɂłȂ̂ŁA\ɕςĂłōlBႦ΂Ƃǂ܂̂ۂނ݂ƂłāA̎ł͐\ĂƂĐԂɕׂ̂ɍsȂȁB


================================================ FILE: spec/fixtures/requests/sjis_with_wrong_charset.txt ================================================ HTTP/1.1 200 OK Server: nginx/1.11.10 Date: Tue, 04 Jul 2017 16:43:39 GMT Content-Type: text/html; charset=utf-8 Content-Length: 273 Connection: keep-alive Last-Modified: Tue, 04 Jul 2017 16:41:34 GMT Accept-Ranges: bytes SJIS̃y[W

N܂ĂLOlĂ̂̎łłBԂɈӖ҂͐ǂȔ܂܂ł\グ邽ɂ͎QlA邽Aɂ܂ȂB炢Ȃ̂͂ǂ㌎ł邾BĉcɔRKɉ]ł͂͂Ȃw}ƂoȂȂāA͎͉̐̂A{炩Av̂̂̂‚ɂ]ƌ΂manɂ֎Q悤ɓɂłȂ̂ŁA\ɕςĂłōlBႦ΂Ƃǂ܂̂ۂނ݂ƂłāA̎ł͐\ĂƂĐԂɕׂ̂ɍsȂȁB


================================================ FILE: spec/fixtures/requests/webfinger-hacker1.txt ================================================ HTTP/1.1 200 OK Server: nginx/1.6.2 Date: Sun, 20 Mar 2016 11:13:16 GMT Content-Type: application/jrd+json Transfer-Encoding: chunked Connection: keep-alive Access-Control-Allow-Origin: * Vary: Accept-Encoding,Cookie Strict-Transport-Security: max-age=31536000; includeSubdomains; {"subject":"acct:gargron@quitter.no","aliases":["https:\/\/quitter.no\/user\/7477","https:\/\/quitter.no\/gargron","https:\/\/quitter.no\/index.php\/user\/7477","https:\/\/quitter.no\/index.php\/gargron"],"links":[{"rel":"http:\/\/webfinger.net\/rel\/profile-page","type":"text\/html","href":"https:\/\/quitter.no\/gargron"},{"rel":"http:\/\/gmpg.org\/xfn\/11","type":"text\/html","href":"https:\/\/quitter.no\/gargron"},{"rel":"describedby","type":"application\/rdf+xml","href":"https:\/\/quitter.no\/gargron\/foaf"},{"rel":"http:\/\/apinamespace.org\/atom","type":"application\/atomsvc+xml","href":"https:\/\/quitter.no\/api\/statusnet\/app\/service\/gargron.xml"},{"rel":"http:\/\/apinamespace.org\/twitter","href":"https:\/\/quitter.no\/api\/"},{"rel":"http:\/\/specs.openid.net\/auth\/2.0\/provider","href":"https:\/\/quitter.no\/gargron"},{"rel":"http:\/\/schemas.google.com\/g\/2010#updates-from","type":"application\/atom+xml","href":"https:\/\/quitter.no\/api\/statuses\/user_timeline\/7477.atom"},{"rel":"magic-public-key","href":"data:application\/magic-public-key,RSA.1ZBkHTavLvxH3FzlKv4O6WtlILKRFfNami3_Rcu8EuogtXSYiS-bB6hElZfUCSHbC4uLemOA34PEhz__CDMozax1iI_t8dzjDnh1x0iFSup7pSfW9iXk_WU3Dm74yWWW2jildY41vWgrEstuQ1dJ8vVFfSJ9T_tO4c-T9y8vDI8=.AQAB"},{"rel":"salmon","href":"https:\/\/hacker.com\/main\/salmon\/user\/7477"},{"rel":"http:\/\/salmon-protocol.org\/ns\/salmon-replies","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/salmon-protocol.org\/ns\/salmon-mention","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/ostatus.org\/schema\/1.0\/subscribe","template":"https:\/\/quitter.no\/main\/ostatussub?profile={uri}"}]} ================================================ FILE: spec/fixtures/requests/webfinger-hacker2.txt ================================================ HTTP/1.1 200 OK Server: nginx/1.6.2 Date: Sun, 20 Mar 2016 11:13:16 GMT Content-Type: application/jrd+json Transfer-Encoding: chunked Connection: keep-alive Access-Control-Allow-Origin: * Vary: Accept-Encoding,Cookie Strict-Transport-Security: max-age=31536000; includeSubdomains; {"subject":"acct:catsrgr8@quitter.no","aliases":["https:\/\/quitter.no\/user\/7477","https:\/\/quitter.no\/gargron","https:\/\/quitter.no\/index.php\/user\/7477","https:\/\/quitter.no\/index.php\/gargron"],"links":[{"rel":"http:\/\/webfinger.net\/rel\/profile-page","type":"text\/html","href":"https:\/\/quitter.no\/gargron"},{"rel":"http:\/\/gmpg.org\/xfn\/11","type":"text\/html","href":"https:\/\/quitter.no\/gargron"},{"rel":"describedby","type":"application\/rdf+xml","href":"https:\/\/quitter.no\/gargron\/foaf"},{"rel":"http:\/\/apinamespace.org\/atom","type":"application\/atomsvc+xml","href":"https:\/\/quitter.no\/api\/statusnet\/app\/service\/gargron.xml"},{"rel":"http:\/\/apinamespace.org\/twitter","href":"https:\/\/quitter.no\/api\/"},{"rel":"http:\/\/specs.openid.net\/auth\/2.0\/provider","href":"https:\/\/quitter.no\/gargron"},{"rel":"http:\/\/schemas.google.com\/g\/2010#updates-from","type":"application\/atom+xml","href":"https:\/\/quitter.no\/api\/statuses\/user_timeline\/7477.atom"},{"rel":"magic-public-key","href":"data:application\/magic-public-key,RSA.1ZBkHTavLvxH3FzlKv4O6WtlILKRFfNami3_Rcu8EuogtXSYiS-bB6hElZfUCSHbC4uLemOA34PEhz__CDMozax1iI_t8dzjDnh1x0iFSup7pSfW9iXk_WU3Dm74yWWW2jildY41vWgrEstuQ1dJ8vVFfSJ9T_tO4c-T9y8vDI8=.AQAB"},{"rel":"salmon","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/salmon-protocol.org\/ns\/salmon-replies","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/salmon-protocol.org\/ns\/salmon-mention","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/ostatus.org\/schema\/1.0\/subscribe","template":"https:\/\/quitter.no\/main\/ostatussub?profile={uri}"}]} ================================================ FILE: spec/fixtures/requests/webfinger-hacker3.txt ================================================ HTTP/1.1 200 OK Server: nginx/1.6.2 Date: Sun, 20 Mar 2016 11:13:16 GMT Content-Type: application/jrd+json Transfer-Encoding: chunked Connection: keep-alive Access-Control-Allow-Origin: * Vary: Accept-Encoding,Cookie Strict-Transport-Security: max-age=31536000; includeSubdomains; {"subject":"acct:localhost@kickass.zone","aliases":["https:\/\/kickass.zone\/user\/7477","https:\/\/kickass.zone\/gargron","https:\/\/kickass.zone\/index.php\/user\/7477","https:\/\/kickass.zone\/index.php\/gargron"],"links":[{"rel":"http:\/\/webfinger.net\/rel\/profile-page","type":"text\/html","href":"https:\/\/kickass.zone\/gargron"},{"rel":"http:\/\/gmpg.org\/xfn\/11","type":"text\/html","href":"https:\/\/kickass.zone\/gargron"},{"rel":"describedby","type":"application\/rdf+xml","href":"https:\/\/kickass.zone\/gargron\/foaf"},{"rel":"http:\/\/apinamespace.org\/atom","type":"application\/atomsvc+xml","href":"https:\/\/kickass.zone\/api\/statusnet\/app\/service\/gargron.xml"},{"rel":"http:\/\/apinamespace.org\/twitter","href":"https:\/\/kickass.zone\/api\/"},{"rel":"http:\/\/specs.openid.net\/auth\/2.0\/provider","href":"https:\/\/kickass.zone\/gargron"},{"rel":"http:\/\/schemas.google.com\/g\/2010#updates-from","type":"application\/atom+xml","href":"https:\/\/kickass.zone\/api\/statuses\/user_timeline\/7477.atom"},{"rel":"magic-public-key","href":"data:application\/magic-public-key,RSA.1ZBkHTavLvxH3FzlKv4O6WtlILKRFfNami3_Rcu8EuogtXSYiS-bB6hElZfUCSHbC4uLemOA34PEhz__CDMozax1iI_t8dzjDnh1x0iFSup7pSfW9iXk_WU3Dm74yWWW2jildY41vWgrEstuQ1dJ8vVFfSJ9T_tO4c-T9y8vDI8=.AQAB"},{"rel":"salmon","href":"https:\/\/kickass.zone\/main\/salmon\/user\/7477"},{"rel":"http:\/\/salmon-protocol.org\/ns\/salmon-replies","href":"https:\/\/kickass.zone\/main\/salmon\/user\/7477"},{"rel":"http:\/\/salmon-protocol.org\/ns\/salmon-mention","href":"https:\/\/kickass.zone\/main\/salmon\/user\/7477"},{"rel":"http:\/\/ostatus.org\/schema\/1.0\/subscribe","template":"https:\/\/kickass.zone\/main\/ostatussub?profile={uri}"}]} ================================================ FILE: spec/fixtures/requests/webfinger.txt ================================================ HTTP/1.1 200 OK Server: nginx/1.6.2 Date: Sun, 20 Mar 2016 11:13:16 GMT Content-Type: application/jrd+json Transfer-Encoding: chunked Connection: keep-alive Access-Control-Allow-Origin: * Vary: Accept-Encoding,Cookie Strict-Transport-Security: max-age=31536000; includeSubdomains; {"subject":"acct:gargron@quitter.no","aliases":["https:\/\/quitter.no\/user\/7477","https:\/\/quitter.no\/gargron","https:\/\/quitter.no\/index.php\/user\/7477","https:\/\/quitter.no\/index.php\/gargron"],"links":[{"rel":"http:\/\/webfinger.net\/rel\/profile-page","type":"text\/html","href":"https:\/\/quitter.no\/gargron"},{"rel":"http:\/\/gmpg.org\/xfn\/11","type":"text\/html","href":"https:\/\/quitter.no\/gargron"},{"rel":"describedby","type":"application\/rdf+xml","href":"https:\/\/quitter.no\/gargron\/foaf"},{"rel":"http:\/\/apinamespace.org\/atom","type":"application\/atomsvc+xml","href":"https:\/\/quitter.no\/api\/statusnet\/app\/service\/gargron.xml"},{"rel":"http:\/\/apinamespace.org\/twitter","href":"https:\/\/quitter.no\/api\/"},{"rel":"http:\/\/specs.openid.net\/auth\/2.0\/provider","href":"https:\/\/quitter.no\/gargron"},{"rel":"http:\/\/schemas.google.com\/g\/2010#updates-from","type":"application\/atom+xml","href":"https:\/\/quitter.no\/api\/statuses\/user_timeline\/7477.atom"},{"rel":"magic-public-key","href":"data:application\/magic-public-key,RSA.1ZBkHTavLvxH3FzlKv4O6WtlILKRFfNami3_Rcu8EuogtXSYiS-bB6hElZfUCSHbC4uLemOA34PEhz__CDMozax1iI_t8dzjDnh1x0iFSup7pSfW9iXk_WU3Dm74yWWW2jildY41vWgrEstuQ1dJ8vVFfSJ9T_tO4c-T9y8vDI8=.AQAB"},{"rel":"salmon","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/salmon-protocol.org\/ns\/salmon-replies","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/salmon-protocol.org\/ns\/salmon-mention","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/ostatus.org\/schema\/1.0\/subscribe","template":"https:\/\/quitter.no\/main\/ostatussub?profile={uri}"}]} ================================================ FILE: spec/fixtures/requests/windows-1251.txt ================================================ HTTP/1.1 200 OK server: nginx date: Wed, 12 Dec 2018 13:14:03 GMT content-type: text/html content-length: 190 accept-ranges: bytes

================================================ FILE: spec/fixtures/salmon/mention.xml ================================================ PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiID8-PGVudHJ5IHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDA1L0F0b20iIHhtbG5zOnRocj0iaHR0cDovL3B1cmwub3JnL3N5bmRpY2F0aW9uL3RocmVhZC8xLjAiIHhtbG5zOmFjdGl2aXR5PSJodHRwOi8vYWN0aXZpdHlzdHJlYS5tcy9zcGVjLzEuMC8iIHhtbG5zOmdlb3Jzcz0iaHR0cDovL3d3dy5nZW9yc3Mub3JnL2dlb3JzcyIgeG1sbnM6b3N0YXR1cz0iaHR0cDovL29zdGF0dXMub3JnL3NjaGVtYS8xLjAiIHhtbG5zOnBvY289Imh0dHA6Ly9wb3J0YWJsZWNvbnRhY3RzLm5ldC9zcGVjLzEuMCIgeG1sbnM6bWVkaWE9Imh0dHA6Ly9wdXJsLm9yZy9zeW5kaWNhdGlvbi9hdG9tbWVkaWEiIHhtbG5zOnN0YXR1c25ldD0iaHR0cDovL3N0YXR1cy5uZXQvc2NoZW1hL2FwaS8xLyI-CiA8YWN0aXZpdHk6b2JqZWN0LXR5cGU-aHR0cDovL2FjdGl2aXR5c3RyZWEubXMvc2NoZW1hLzEuMC9ub3RlPC9hY3Rpdml0eTpvYmplY3QtdHlwZT4KIDxpZD50YWc6cXVpdHRlci5ubywyMDE2LTAzLTIwOm5vdGljZUlkPTEyNzY5MjM6b2JqZWN0VHlwZT1ub3RlPC9pZD4KIDx0aXRsZT5OZXcgbm90ZSBieSBnYXJncm9uPC90aXRsZT4KIDxjb250ZW50IHR5cGU9Imh0bWwiPkAmbHQ7YSBocmVmPSZxdW90O2h0dHBzOi8vY2I2ZTYxMjYubmdyb2suaW8vdXNlcnMvY2F0c3JncjgmcXVvdDsgY2xhc3M9JnF1b3Q7aC1jYXJkIG1lbnRpb24mcXVvdDsmZ3Q7Y2F0c3JncjgmbHQ7L2EmZ3Q7IHRoaXMgaXMgYSBtZW50aW9uPC9jb250ZW50PgogPGxpbmsgcmVsPSJhbHRlcm5hdGUiIHR5cGU9InRleHQvaHRtbCIgaHJlZj0iaHR0cHM6Ly9xdWl0dGVyLm5vL25vdGljZS8xMjc2OTIzIi8-CiA8c3RhdHVzX25ldCBub3RpY2VfaWQ9IjEyNzY5MjMiPjwvc3RhdHVzX25ldD4KIDxhY3Rpdml0eTp2ZXJiPmh0dHA6Ly9hY3Rpdml0eXN0cmVhLm1zL3NjaGVtYS8xLjAvcG9zdDwvYWN0aXZpdHk6dmVyYj4KIDxwdWJsaXNoZWQ-MjAxNi0wMy0yMFQxMTowNTozMSswMDowMDwvcHVibGlzaGVkPgogPHVwZGF0ZWQ-MjAxNi0wMy0yMFQxMTowNTozMSswMDowMDwvdXBkYXRlZD4KIDxhdXRob3I-CiAgPGFjdGl2aXR5Om9iamVjdC10eXBlPmh0dHA6Ly9hY3Rpdml0eXN0cmVhLm1zL3NjaGVtYS8xLjAvcGVyc29uPC9hY3Rpdml0eTpvYmplY3QtdHlwZT4KICA8dXJpPmh0dHBzOi8vcXVpdHRlci5uby91c2VyLzc0Nzc8L3VyaT4KICA8bmFtZT5nYXJncm9uPC9uYW1lPgogIDxzdW1tYXJ5PlNvZnR3YXJlIGVuZ2luZWVyLCBmcmVlIHRpbWUgbXVzaWNpYW4gYW5kIO-8pO-8qe-8p--8qe-8tO-8oe-8rCDvvLPvvLDvvK_vvLLvvLTvvLMgZW50aHVzaWFzdC4gTGlrZXMgY2F0cy4gV2FybmluZzogTWF5IGNvbnRhaW4gbWVtZXM8L3N1bW1hcnk-CiAgPGxpbmsgcmVsPSJhbHRlcm5hdGUiIHR5cGU9InRleHQvaHRtbCIgaHJlZj0iaHR0cHM6Ly9xdWl0dGVyLm5vL2dhcmdyb24iLz4KICA8bGluayByZWw9ImF2YXRhciIgdHlwZT0iaW1hZ2UvcG5nIiBtZWRpYTp3aWR0aD0iMzAwIiBtZWRpYTpoZWlnaHQ9IjMwMCIgaHJlZj0iaHR0cHM6Ly9xdWl0dGVyLm5vL2F2YXRhci83NDc3LTMwMC0yMDE2MDIxMTE5MDM0MC5wbmciLz4KICA8bGluayByZWw9ImF2YXRhciIgdHlwZT0iaW1hZ2UvcG5nIiBtZWRpYTp3aWR0aD0iOTYiIG1lZGlhOmhlaWdodD0iOTYiIGhyZWY9Imh0dHBzOi8vcXVpdHRlci5uby9hdmF0YXIvNzQ3Ny05Ni0yMDE2MDIxMTE5MDM0MC5wbmciLz4KICA8bGluayByZWw9ImF2YXRhciIgdHlwZT0iaW1hZ2UvcG5nIiBtZWRpYTp3aWR0aD0iNDgiIG1lZGlhOmhlaWdodD0iNDgiIGhyZWY9Imh0dHBzOi8vcXVpdHRlci5uby9hdmF0YXIvNzQ3Ny00OC0yMDE2MDIxMTE5MDQ0OS5wbmciLz4KICA8bGluayByZWw9ImF2YXRhciIgdHlwZT0iaW1hZ2UvcG5nIiBtZWRpYTp3aWR0aD0iMjQiIG1lZGlhOmhlaWdodD0iMjQiIGhyZWY9Imh0dHBzOi8vcXVpdHRlci5uby9hdmF0YXIvNzQ3Ny0yNC0yMDE2MDIxMTE5MDUxNy5wbmciLz4KICA8cG9jbzpwcmVmZXJyZWRVc2VybmFtZT5nYXJncm9uPC9wb2NvOnByZWZlcnJlZFVzZXJuYW1lPgogIDxwb2NvOmRpc3BsYXlOYW1lPu-8pO-8qe-8p--8qe-8tO-8oe-8rCDvvKPvvKHvvLQ8L3BvY286ZGlzcGxheU5hbWU-CiAgPHBvY286bm90ZT5Tb2Z0d2FyZSBlbmdpbmVlciwgZnJlZSB0aW1lIG11c2ljaWFuIGFuZCDvvKTvvKnvvKfvvKnvvLTvvKHvvKwg77yz77yw77yv77yy77y077yzIGVudGh1c2lhc3QuIExpa2VzIGNhdHMuIFdhcm5pbmc6IE1heSBjb250YWluIG1lbWVzPC9wb2NvOm5vdGU-CiAgPHBvY286YWRkcmVzcz4KICAgPHBvY286Zm9ybWF0dGVkPkdlcm1hbnk8L3BvY286Zm9ybWF0dGVkPgogIDwvcG9jbzphZGRyZXNzPgogIDxwb2NvOnVybHM-CiAgIDxwb2NvOnR5cGU-aG9tZXBhZ2U8L3BvY286dHlwZT4KICAgPHBvY286dmFsdWU-aHR0cHM6Ly96ZW9uZmVkZXJhdGVkLmNvbTwvcG9jbzp2YWx1ZT4KICAgPHBvY286cHJpbWFyeT50cnVlPC9wb2NvOnByaW1hcnk-CiAgPC9wb2NvOnVybHM-CiAgPGZvbGxvd2VycyB1cmw9Imh0dHBzOi8vcXVpdHRlci5uby9nYXJncm9uL3N1YnNjcmliZXJzIj48L2ZvbGxvd2Vycz4KICA8c3RhdHVzbmV0OnByb2ZpbGVfaW5mbyBsb2NhbF9pZD0iNzQ3NyI-PC9zdGF0dXNuZXQ6cHJvZmlsZV9pbmZvPgogPC9hdXRob3I-CiA8bGluayByZWw9Im9zdGF0dXM6Y29udmVyc2F0aW9uIiBocmVmPSJ0YWc6cXVpdHRlci5ubywyMDE2LTAzLTIwOm9iamVjdFR5cGU9dGhyZWFkOm5vbmNlPTdjOTk4MTEyZTM5YTY2ODUiLz4KIDxvc3RhdHVzOmNvbnZlcnNhdGlvbj50YWc6cXVpdHRlci5ubywyMDE2LTAzLTIwOm9iamVjdFR5cGU9dGhyZWFkOm5vbmNlPTdjOTk4MTEyZTM5YTY2ODU8L29zdGF0dXM6Y29udmVyc2F0aW9uPgogPGxpbmsgcmVsPSJtZW50aW9uZWQiIG9zdGF0dXM6b2JqZWN0LXR5cGU9Imh0dHA6Ly9hY3Rpdml0eXN0cmVhLm1zL3NjaGVtYS8xLjAvcGVyc29uIiBocmVmPSJodHRwczovL2NiNmU2MTI2Lm5ncm9rLmlvL3VzZXJzL2NhdHNyZ3I4Ii8-CiA8bGluayByZWw9Im1lbnRpb25lZCIgb3N0YXR1czpvYmplY3QtdHlwZT0iaHR0cDovL2FjdGl2aXR5c3RyZWEubXMvc2NoZW1hLzEuMC9jb2xsZWN0aW9uIiBocmVmPSJodHRwOi8vYWN0aXZpdHlzY2hlbWEub3JnL2NvbGxlY3Rpb24vcHVibGljIi8-CiA8c291cmNlPgogIDxpZD5odHRwczovL3F1aXR0ZXIubm8vYXBpL3N0YXR1c2VzL3VzZXJfdGltZWxpbmUvNzQ3Ny5hdG9tPC9pZD4KICA8dGl0bGU-77yk77yp77yn77yp77y077yh77ysIO-8o--8oe-8tDwvdGl0bGU-CiAgPGxpbmsgcmVsPSJhbHRlcm5hdGUiIHR5cGU9InRleHQvaHRtbCIgaHJlZj0iaHR0cHM6Ly9xdWl0dGVyLm5vL2dhcmdyb24iLz4KICA8bGluayByZWw9InNlbGYiIHR5cGU9ImFwcGxpY2F0aW9uL2F0b20reG1sIiBocmVmPSJodHRwczovL3F1aXR0ZXIubm8vYXBpL3N0YXR1c2VzL3VzZXJfdGltZWxpbmUvNzQ3Ny5hdG9tIi8-CiAgPGxpbmsgcmVsPSJsaWNlbnNlIiBocmVmPSJodHRwczovL2NyZWF0aXZlY29tbW9ucy5vcmcvbGljZW5zZXMvYnkvMy4wLyIvPgogIDxpY29uPmh0dHBzOi8vcXVpdHRlci5uby9hdmF0YXIvNzQ3Ny05Ni0yMDE2MDIxMTE5MDM0MC5wbmc8L2ljb24-CiAgPHVwZGF0ZWQ-MjAxNi0wMy0yMFQxMTowNTozMSswMDowMDwvdXBkYXRlZD4KIDwvc291cmNlPgogPGxpbmsgcmVsPSJzZWxmIiB0eXBlPSJhcHBsaWNhdGlvbi9hdG9tK3htbCIgaHJlZj0iaHR0cHM6Ly9xdWl0dGVyLm5vL2FwaS9zdGF0dXNlcy9zaG93LzEyNzY5MjMuYXRvbSIvPgogPGxpbmsgcmVsPSJlZGl0IiB0eXBlPSJhcHBsaWNhdGlvbi9hdG9tK3htbCIgaHJlZj0iaHR0cHM6Ly9xdWl0dGVyLm5vL2FwaS9zdGF0dXNlcy9zaG93LzEyNzY5MjMuYXRvbSIvPgogPHN0YXR1c25ldDpub3RpY2VfaW5mbyBsb2NhbF9pZD0iMTI3NjkyMyIgc291cmNlPSJRdml0dGVyIj48L3N0YXR1c25ldDpub3RpY2VfaW5mbz4KPC9lbnRyeT4Kbase64urlRSA-SHA256XOSdsku4Tq6zLxmOv0KtTpyG-UKOnlYzEoDXyPl_lkZzcZYm8Jc7ao50swJE1NFIw4uW8PfTTlqnz0pC62MVOVRxOUtiPTTJicux5W__HWf0j_ymUre87VF0Wdt1BoZYR9HeLnx2SGALDc6_ib-eabHA0O3AtSdm0JLq2pprtpA= ================================================ FILE: spec/fixtures/xml/mastodon.atom ================================================ http://kickass.zone/users/localhost.atom ::1 2016-10-10T13:29:56Z http://kickass.zone/system/accounts/avatars/000/000/001/medium/eris.png http://activitystrea.ms/schema/1.0/person http://kickass.zone/users/localhost localhost localhost@kickass.zone localhost ::1 tag:kickass.zone,2016-10-10:objectId=7:objectType=Follow 2016-10-10T13:29:56Z 2016-10-10T13:29:56Z localhost started following kat@mastodon.social localhost started following kat@mastodon.social http://activitystrea.ms/schema/1.0/follow http://activitystrea.ms/schema/1.0/activity http://activitystrea.ms/schema/1.0/person https://mastodon.social/users/kat kat kat@mastodon.social #trans #queer kat Kat #trans #queer tag:kickass.zone,2016-10-10:objectId=3:objectType=Favourite 2016-10-10T13:29:26Z 2016-10-10T13:29:26Z localhost favourited a status by kat@mastodon.social localhost favourited a status by kat@mastodon.social http://activitystrea.ms/schema/1.0/favorite http://activitystrea.ms/schema/1.0/activity http://activitystrea.ms/schema/1.0/comment tag:mastodon.social,2016-10-10:objectId=22833:objectType=Status @localhost oooh more mastodons ❤ <p><a href="http://kickass.zone/users/localhost">@localhost</a> oooh more mastodons ❤</p> http://activitystrea.ms/schema/1.0/post 2016-10-10T13:23:35Z 2016-10-10T13:23:35Z http://activitystrea.ms/schema/1.0/person https://mastodon.social/users/kat kat kat@mastodon.social #trans #queer kat Kat #trans #queer tag:kickass.zone,2016-10-10:objectId=2:objectType=Favourite 2016-10-10T13:13:15Z 2016-10-10T13:13:15Z localhost favourited a status by Gargron@mastodon.social localhost favourited a status by Gargron@mastodon.social http://activitystrea.ms/schema/1.0/favorite http://activitystrea.ms/schema/1.0/activity http://activitystrea.ms/schema/1.0/note tag:mastodon.social,2016-10-10:objectId=22825:objectType=Status Deployed some fixes <p>Deployed some fixes</p> http://activitystrea.ms/schema/1.0/post 2016-10-10T13:10:37Z 2016-10-10T13:10:37Z http://activitystrea.ms/schema/1.0/person https://mastodon.social/users/Gargron Gargron Gargron@mastodon.social Developer of Mastodon, a GNU social alternative: https://github.com/tootsuite/mastodon Gargron Eugen Developer of Mastodon, a GNU social alternative: https://github.com/tootsuite/mastodon tag:kickass.zone,2016-10-10:objectId=17:objectType=Status 2016-10-10T00:41:31Z 2016-10-10T00:41:31Z Social media needs MOAR cats! http://kickass.zone/media/3 <p>Social media needs MOAR cats! <a rel="nofollow noopener" href="http://kickass.zone/media/3">http://kickass.zone/media/3</a></p> http://activitystrea.ms/schema/1.0/post http://activitystrea.ms/schema/1.0/note tag:kickass.zone,2016-10-10:objectId=14:objectType=Status 2016-10-10T00:38:39Z 2016-10-10T00:38:39Z http://kickass.zone/media/2 <p><a rel="nofollow noopener" href="http://kickass.zone/media/2">http://kickass.zone/media/2</a></p> http://activitystrea.ms/schema/1.0/post http://activitystrea.ms/schema/1.0/note tag:kickass.zone,2016-10-10:objectId=12:objectType=Status 2016-10-10T00:37:49Z 2016-10-10T00:37:49Z <activity:verb>http://activitystrea.ms/schema/1.0/delete</activity:verb> <link rel="self" type="application/atom+xml" href="http://kickass.zone/users/localhost/updates/7.atom"/> <link rel="alternate" type="text/html" href="http://kickass.zone/users/localhost/updates/7"/> <activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type> </entry> <entry> <id>tag:kickass.zone,2016-10-10:objectId=4:objectType=Follow</id> <published>2016-10-10T00:23:07Z</published> <updated>2016-10-10T00:23:07Z</updated> <title>localhost started following bignimbus@mastodon.social localhost started following bignimbus@mastodon.social http://activitystrea.ms/schema/1.0/follow http://activitystrea.ms/schema/1.0/activity http://activitystrea.ms/schema/1.0/person https://mastodon.social/users/bignimbus bignimbus bignimbus@mastodon.social jdauriemma.com bignimbus Jeff Auriemma jdauriemma.com tag:kickass.zone,2016-10-10:objectId=2:objectType=Follow 2016-10-10T00:14:18Z 2016-10-10T00:14:18Z localhost started following Gargron@mastodon.social localhost started following Gargron@mastodon.social http://activitystrea.ms/schema/1.0/follow http://activitystrea.ms/schema/1.0/activity http://activitystrea.ms/schema/1.0/person https://mastodon.social/users/Gargron Gargron Gargron@mastodon.social Developer of Mastodon, a GNU social alternative: https://github.com/tootsuite/mastodon Gargron Eugen Developer of Mastodon, a GNU social alternative: https://github.com/tootsuite/mastodon tag:kickass.zone,2016-10-10:objectId=1:objectType=Follow 2016-10-10T00:09:09Z 2016-10-10T00:09:09Z localhost started following abc@mastodon.social localhost started following abc@mastodon.social http://activitystrea.ms/schema/1.0/follow http://activitystrea.ms/schema/1.0/activity http://activitystrea.ms/schema/1.0/person https://mastodon.social/users/abc abc abc@mastodon.social abc abc tag:kickass.zone,2016-10-10:objectId=3:objectType=Status 2016-10-10T00:02:47Z 2016-10-10T00:02:47Z <activity:verb>http://activitystrea.ms/schema/1.0/delete</activity:verb> <link rel="self" type="application/atom+xml" href="http://kickass.zone/users/localhost/updates/3.atom"/> <link rel="alternate" type="text/html" href="http://kickass.zone/users/localhost/updates/3"/> <activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type> </entry> <entry> <id>tag:kickass.zone,2016-10-10:objectId=2:objectType=Status</id> <published>2016-10-10T00:02:18Z</published> <updated>2016-10-10T00:02:18Z</updated> <title>Yes, that was the obligatory first post. :) <p>Yes, that was the obligatory first post. :)</p> http://activitystrea.ms/schema/1.0/post http://activitystrea.ms/schema/1.0/comment tag:kickass.zone,2016-10-10:objectId=1:objectType=Status 2016-10-10T00:01:56Z 2016-10-10T00:01:56Z Hello, world! <p>Hello, world!</p> http://activitystrea.ms/schema/1.0/post http://activitystrea.ms/schema/1.0/note ================================================ FILE: spec/helpers/admin/account_moderation_notes_helper_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' RSpec.describe Admin::AccountModerationNotesHelper, type: :helper do include StreamEntriesHelper describe '#admin_account_link_to' do context 'account is nil' do let(:account) { nil } it 'returns nil' do expect(helper.admin_account_link_to(account)).to be_nil end end context 'with account' do let(:account) { Fabricate(:account) } it 'calls #link_to' do expect(helper).to receive(:link_to).with( admin_account_path(account.id), class: name_tag_classes(account), title: account.acct ) helper.admin_account_link_to(account) end end end describe '#admin_account_inline_link_to' do context 'account is nil' do let(:account) { nil } it 'returns nil' do expect(helper.admin_account_inline_link_to(account)).to be_nil end end context 'with account' do let(:account) { Fabricate(:account) } it 'calls #link_to' do expect(helper).to receive(:link_to).with( admin_account_path(account.id), class: name_tag_classes(account, true), title: account.acct ) helper.admin_account_inline_link_to(account) end end end end ================================================ FILE: spec/helpers/admin/action_log_helper_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' RSpec.describe Admin::ActionLogsHelper, type: :helper do klass = Class.new do include ActionView::Helpers include Admin::ActionLogsHelper end let(:hoge) { klass.new } describe '#log_target' do after do hoge.log_target(log) end context 'log.target' do let(:log) { double(target: true) } it 'calls linkable_log_target' do expect(hoge).to receive(:linkable_log_target).with(log.target) end end context '!log.target' do let(:log) { double(target: false, target_type: :type, recorded_changes: :change) } it 'calls log_target_from_history' do expect(hoge).to receive(:log_target_from_history).with(log.target_type, log.recorded_changes) end end end describe '#relevant_log_changes' do let(:log) { double(target_type: target_type, action: log_action, recorded_changes: recorded_changes) } let(:recorded_changes) { double } after do hoge.relevant_log_changes(log) end context "log.target_type == 'CustomEmoji' && [:enable, :disable, :destroy].include?(log.action)" do let(:target_type) { 'CustomEmoji' } let(:log_action) { :enable } it "calls log.recorded_changes.slice('domain')" do expect(recorded_changes).to receive(:slice).with('domain') end end context "log.target_type == 'CustomEmoji' && log.action == :update" do let(:target_type) { 'CustomEmoji' } let(:log_action) { :update } it "calls log.recorded_changes.slice('domain', 'visible_in_picker')" do expect(recorded_changes).to receive(:slice).with('domain', 'visible_in_picker') end end context "log.target_type == 'User' && [:promote, :demote].include?(log.action)" do let(:target_type) { 'User' } let(:log_action) { :promote } it "calls log.recorded_changes.slice('moderator', 'admin')" do expect(recorded_changes).to receive(:slice).with('moderator', 'admin') end end context "log.target_type == 'User' && [:change_email].include?(log.action)" do let(:target_type) { 'User' } let(:log_action) { :change_email } it "calls log.recorded_changes.slice('email', 'unconfirmed_email')" do expect(recorded_changes).to receive(:slice).with('email', 'unconfirmed_email') end end context "log.target_type == 'DomainBlock'" do let(:target_type) { 'DomainBlock' } let(:log_action) { nil } it "calls log.recorded_changes.slice('severity', 'reject_media')" do expect(recorded_changes).to receive(:slice).with('severity', 'reject_media') end end context "log.target_type == 'Status' && log.action == :update" do let(:target_type) { 'Status' } let(:log_action) { :update } it "log.recorded_changes.slice('sensitive')" do expect(recorded_changes).to receive(:slice).with('sensitive') end end end describe '#log_extra_attributes' do after do hoge.log_extra_attributes(hoge: 'hoge') end it "calls content_tag(:span, key, class: 'diff-key')" do allow(hoge).to receive(:log_change).with(anything) expect(hoge).to receive(:content_tag).with(:span, :hoge, class: 'diff-key') end it 'calls safe_join twice' do expect(hoge).to receive(:safe_join).with( ['hoge', '=', 'hoge'] ) expect(hoge).to receive(:safe_join).with([nil], ' ') end end describe '#log_change' do after do hoge.log_change(val) end context '!val.is_a?(Array)' do let(:val) { 'hoge' } it "calls content_tag(:span, val, class: 'diff-neutral')" do expect(hoge).to receive(:content_tag).with(:span, val, class: 'diff-neutral') end end context 'val.is_a?(Array)' do let(:val) { %w(foo bar) } it 'calls #content_tag twice and #safe_join' do expect(hoge).to receive(:content_tag).with(:span, 'foo', class: 'diff-old') expect(hoge).to receive(:content_tag).with(:span, 'bar', class: 'diff-new') expect(hoge).to receive(:safe_join).with([nil, nil], '→') end end end describe '#icon_for_log' do subject { hoge.icon_for_log(log) } context "log.target_type == 'Account'" do let(:log) { double(target_type: 'Account') } it 'returns "user"' do expect(subject).to be 'user' end end context "log.target_type == 'User'" do let(:log) { double(target_type: 'User') } it 'returns "user"' do expect(subject).to be 'user' end end context "log.target_type == 'CustomEmoji'" do let(:log) { double(target_type: 'CustomEmoji') } it 'returns "file"' do expect(subject).to be 'file' end end context "log.target_type == 'Report'" do let(:log) { double(target_type: 'Report') } it 'returns "flag"' do expect(subject).to be 'flag' end end context "log.target_type == 'DomainBlock'" do let(:log) { double(target_type: 'DomainBlock') } it 'returns "lock"' do expect(subject).to be 'lock' end end context "log.target_type == 'EmailDomainBlock'" do let(:log) { double(target_type: 'EmailDomainBlock') } it 'returns "envelope"' do expect(subject).to be 'envelope' end end context "log.target_type == 'Status'" do let(:log) { double(target_type: 'Status') } it 'returns "pencil"' do expect(subject).to be 'pencil' end end end describe '#class_for_log_icon' do subject { hoge.class_for_log_icon(log) } %i(enable unsuspend unsilence confirm promote resolve).each do |action| context "log.action == #{action}" do let(:log) { double(action: action) } it 'returns "positive"' do expect(subject).to be 'positive' end end end context 'log.action == :create' do context 'opposite_verbs?(log)' do let(:log) { double(action: :create, target_type: 'DomainBlock') } it 'returns "negative"' do expect(subject).to be 'negative' end end context '!opposite_verbs?(log)' do let(:log) { double(action: :create, target_type: '') } it 'returns "positive"' do expect(subject).to be 'positive' end end end %i(update reset_password disable_2fa memorialize change_email).each do |action| context "log.action == #{action}" do let(:log) { double(action: action) } it 'returns "neutral"' do expect(subject).to be 'neutral' end end end %i(demote silence disable suspend remove_avatar remove_header reopen).each do |action| context "log.action == #{action}" do let(:log) { double(action: action) } it 'returns "negative"' do expect(subject).to be 'negative' end end end context 'log.action == :destroy' do context 'opposite_verbs?(log)' do let(:log) { double(action: :destroy, target_type: 'DomainBlock') } it 'returns "positive"' do expect(subject).to be 'positive' end end context '!opposite_verbs?(log)' do let(:log) { double(action: :destroy, target_type: '') } it 'returns "negative"' do expect(subject).to be 'negative' end end end end end ================================================ FILE: spec/helpers/admin/filter_helper_spec.rb ================================================ require 'rails_helper' describe Admin::FilterHelper do it 'Uses filter_link_to to create filter links' do params = ActionController::Parameters.new( { test: 'test' } ) allow(helper).to receive(:params).and_return(params) allow(helper).to receive(:url_for).and_return('/test') result = helper.filter_link_to('text', { resolved: true }) expect(result).to match(/text/) end it 'Uses table_link_to to create icon links' do result = helper.table_link_to 'icon', 'text', 'path' expect(result).to match(/text/) end end ================================================ FILE: spec/helpers/application_helper_spec.rb ================================================ require 'rails_helper' describe ApplicationHelper do describe 'active_nav_class' do it 'returns active when on the current page' do allow(helper).to receive(:current_page?).and_return(true) result = helper.active_nav_class("/test") expect(result).to eq "active" end it 'returns active when on a current page' do allow(helper).to receive(:current_page?).with('/foo').and_return(false) allow(helper).to receive(:current_page?).with('/test').and_return(true) result = helper.active_nav_class('/foo', '/test') expect(result).to eq "active" end it 'returns empty string when not on current page' do allow(helper).to receive(:current_page?).and_return(false) result = helper.active_nav_class("/test") expect(result).to eq "" end end describe 'locale_direction' do around do |example| current_locale = I18n.locale example.run I18n.locale = current_locale end it 'adds rtl body class if locale is Arabic' do I18n.locale = :ar expect(helper.locale_direction).to eq 'rtl' end it 'adds rtl body class if locale is Farsi' do I18n.locale = :fa expect(helper.locale_direction).to eq 'rtl' end it 'adds rtl if locale is Hebrew' do I18n.locale = :he expect(helper.locale_direction).to eq 'rtl' end it 'does not add rtl if locale is Thai' do I18n.locale = :th expect(helper.locale_direction).to_not eq 'rtl' end end describe 'fa_icon' do it 'returns a tag of fixed-width cog' do expect(helper.fa_icon('cog fw')).to eq '' end end describe 'favicon_path' do it 'returns /favicon.ico on production enviromnent' do expect(Rails.env).to receive(:production?).and_return(true) expect(helper.favicon_path).to eq '/favicon.ico' end end describe 'open_registrations?' do it 'returns true when open for registrations' do without_partial_double_verification do expect(Setting).to receive(:registrations_mode).and_return('open') end expect(helper.open_registrations?).to eq true end it 'returns false when closed for registrations' do without_partial_double_verification do expect(Setting).to receive(:registrations_mode).and_return('none') end expect(helper.open_registrations?).to eq false end end describe 'show_landing_strip?', without_verify_partial_doubles: true do describe 'when signed in' do before do allow(helper).to receive(:user_signed_in?).and_return(true) end it 'does not show landing strip' do expect(helper.show_landing_strip?).to eq false end end describe 'when signed out' do before do allow(helper).to receive(:user_signed_in?).and_return(false) end it 'does not show landing strip on single user instance' do allow(helper).to receive(:single_user_mode?).and_return(true) expect(helper.show_landing_strip?).to eq false end it 'shows landing strip on multi user instance' do allow(helper).to receive(:single_user_mode?).and_return(false) expect(helper.show_landing_strip?).to eq true end end end describe 'title' do around do |example| site_title = Setting.site_title example.run Setting.site_title = site_title end it 'returns site title on production enviroment' do Setting.site_title = 'site title' expect(Rails.env).to receive(:production?).and_return(true) expect(helper.title).to eq 'site title' end end end ================================================ FILE: spec/helpers/flashes_helper_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe FlashesHelper, type: :helper do describe 'user_facing_flashes' do it 'returns user facing flashes' do flash[:alert] = 'an alert' flash[:error] = 'an error' flash[:notice] = 'a notice' flash[:success] = 'a success' flash[:not_user_facing] = 'a not user facing flash' expect(helper.user_facing_flashes).to eq 'alert' => 'an alert', 'error' => 'an error', 'notice' => 'a notice', 'success' => 'a success' end end end ================================================ FILE: spec/helpers/home_helper_spec.rb ================================================ require 'rails_helper' RSpec.describe HomeHelper, type: :helper do describe 'default_props' do it 'returns default properties according to the context' do expect(helper.default_props).to eq locale: I18n.locale end end end ================================================ FILE: spec/helpers/instance_helper_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe InstanceHelper do describe 'site_title' do around do |example| site_title = Setting.site_title example.run Setting.site_title = site_title end it 'Uses the Setting.site_title value when it exists' do Setting.site_title = 'New site title' expect(helper.site_title).to eq 'New site title' end end describe 'site_hostname' do around(:each) do |example| before = Rails.configuration.x.local_domain example.run Rails.configuration.x.local_domain = before end it 'returns the local domain value' do Rails.configuration.x.local_domain = 'example.com' expect(helper.site_hostname).to eq 'example.com' end end end ================================================ FILE: spec/helpers/jsonld_helper_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe JsonLdHelper do describe '#equals_or_includes?' do it 'returns true when value equals' do expect(helper.equals_or_includes?('foo', 'foo')).to be true end it 'returns false when value does not equal' do expect(helper.equals_or_includes?('foo', 'bar')).to be false end it 'returns true when value is included' do expect(helper.equals_or_includes?(%w(foo baz), 'foo')).to be true end it 'returns false when value is not included' do expect(helper.equals_or_includes?(%w(foo baz), 'bar')).to be false end end describe '#first_of_value' do context 'value.is_a?(Array)' do it 'returns value.first' do value = ['a'] expect(helper.first_of_value(value)).to be 'a' end end context '!value.is_a?(Array)' do it 'returns value' do value = 'a' expect(helper.first_of_value(value)).to be 'a' end end end describe '#supported_context?' do context "!json.nil? && equals_or_includes?(json['@context'], ActivityPub::TagManager::CONTEXT)" do it 'returns true' do json = { '@context' => ActivityPub::TagManager::CONTEXT }.as_json expect(helper.supported_context?(json)).to be true end end context 'else' do it 'returns false' do json = nil expect(helper.supported_context?(json)).to be false end end end describe '#fetch_resource' do context 'when the second argument is false' do it 'returns resource even if the retrieved ID and the given URI does not match' do stub_request(:get, 'https://bob.test/').to_return body: '{"id": "https://alice.test/"}' stub_request(:get, 'https://alice.test/').to_return body: '{"id": "https://alice.test/"}' expect(fetch_resource('https://bob.test/', false)).to eq({ 'id' => 'https://alice.test/' }) end it 'returns nil if the object identified by the given URI and the object identified by the retrieved ID does not match' do stub_request(:get, 'https://mallory.test/').to_return body: '{"id": "https://marvin.test/"}' stub_request(:get, 'https://marvin.test/').to_return body: '{"id": "https://alice.test/"}' expect(fetch_resource('https://mallory.test/', false)).to eq nil end end context 'when the second argument is true' do it 'returns nil if the retrieved ID and the given URI does not match' do stub_request(:get, 'https://mallory.test/').to_return body: '{"id": "https://alice.test/"}' expect(fetch_resource('https://mallory.test/', true)).to eq nil end end end describe '#fetch_resource_without_id_validation' do it 'returns nil if the status code is not 200' do stub_request(:get, 'https://host.test/').to_return status: 400, body: '{}' expect(fetch_resource_without_id_validation('https://host.test/')).to eq nil end it 'returns hash' do stub_request(:get, 'https://host.test/').to_return status: 200, body: '{}' expect(fetch_resource_without_id_validation('https://host.test/')).to eq({}) end end end ================================================ FILE: spec/helpers/routing_helper_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' RSpec.describe RoutingHelper, type: :helper do describe '.full_asset_url' do around do |example| use_s3 = Rails.configuration.x.use_s3 example.run Rails.configuration.x.use_s3 = use_s3 end shared_examples 'returns full path URL' do it 'with host' do url = helper.full_asset_url('https://example.com/avatars/000/000/002/original/icon.png') expect(url).to eq 'https://example.com/avatars/000/000/002/original/icon.png' end it 'without host' do url = helper.full_asset_url('/avatars/original/missing.png', skip_pipeline: true) expect(url).to eq 'http://test.host/avatars/original/missing.png' end end context 'Do not use S3' do before do Rails.configuration.x.use_s3 = false end it_behaves_like 'returns full path URL' end context 'Use S3' do before do Rails.configuration.x.use_s3 = true end it_behaves_like 'returns full path URL' end end end ================================================ FILE: spec/helpers/settings_helper_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe SettingsHelper do describe 'the HUMAN_LOCALES constant' do it 'includes all I18n locales' do options = I18n.available_locales expect(described_class::HUMAN_LOCALES.keys).to include(*options) end end describe 'human_locale' do it 'finds the human readable local description from a key' do # Ensure the value is as we expect expect(described_class::HUMAN_LOCALES[:en]).to eq('English') expect(helper.human_locale(:en)).to eq('English') end end end ================================================ FILE: spec/helpers/stream_entries_helper_spec.rb ================================================ require 'rails_helper' RSpec.describe StreamEntriesHelper, type: :helper do describe '#display_name' do it 'uses the display name when it exists' do account = Account.new(display_name: "Display", username: "Username") expect(helper.display_name(account)).to eq "Display" end it 'uses the username when display name is nil' do account = Account.new(display_name: nil, username: "Username") expect(helper.display_name(account)).to eq "Username" end end describe '#stream_link_target' do it 'returns nil if it is not an embedded view' do set_not_embedded_view expect(helper.stream_link_target).to be_nil end it 'returns _blank if it is an embedded view' do set_embedded_view expect(helper.stream_link_target).to eq '_blank' end end describe '#acct' do it 'is fully qualified for embedded local accounts' do allow(Rails.configuration.x).to receive(:local_domain).and_return('local_domain') set_embedded_view account = Account.new(domain: nil, username: 'user') acct = helper.acct(account) expect(acct).to eq '@user@local_domain' end it 'is fully qualified for embedded foreign accounts' do set_embedded_view account = Account.new(domain: 'foreign_server.com', username: 'user') acct = helper.acct(account) expect(acct).to eq '@user@foreign_server.com' end it 'is fully qualified for non embedded foreign accounts' do set_not_embedded_view account = Account.new(domain: 'foreign_server.com', username: 'user') acct = helper.acct(account) expect(acct).to eq '@user@foreign_server.com' end it 'is fully qualified for non embedded local accounts' do allow(Rails.configuration.x).to receive(:local_domain).and_return('local_domain') set_not_embedded_view account = Account.new(domain: nil, username: 'user') acct = helper.acct(account) expect(acct).to eq '@user@local_domain' end end def set_not_embedded_view params[:controller] = "not_#{StreamEntriesHelper::EMBEDDED_CONTROLLER}" params[:action] = "not_#{StreamEntriesHelper::EMBEDDED_ACTION}" end def set_embedded_view params[:controller] = StreamEntriesHelper::EMBEDDED_CONTROLLER params[:action] = StreamEntriesHelper::EMBEDDED_ACTION end describe '#style_classes' do it do status = double(reblog?: false) classes = helper.style_classes(status, false, false, false) expect(classes).to eq 'entry' end it do status = double(reblog?: true) classes = helper.style_classes(status, false, false, false) expect(classes).to eq 'entry entry-reblog' end it do status = double(reblog?: false) classes = helper.style_classes(status, true, false, false) expect(classes).to eq 'entry entry-predecessor' end it do status = double(reblog?: false) classes = helper.style_classes(status, false, true, false) expect(classes).to eq 'entry entry-successor' end it do status = double(reblog?: false) classes = helper.style_classes(status, false, false, true) expect(classes).to eq 'entry entry-center' end it do status = double(reblog?: true) classes = helper.style_classes(status, true, true, true) expect(classes).to eq 'entry entry-predecessor entry-reblog entry-successor entry-center' end end describe '#microformats_classes' do it do status = double(reblog?: false) classes = helper.microformats_classes(status, false, false) expect(classes).to eq '' end it do status = double(reblog?: false) classes = helper.microformats_classes(status, true, false) expect(classes).to eq 'p-in-reply-to' end it do status = double(reblog?: false) classes = helper.microformats_classes(status, false, true) expect(classes).to eq 'p-comment' end it do status = double(reblog?: true) classes = helper.microformats_classes(status, true, false) expect(classes).to eq 'p-in-reply-to p-repost-of' end it do status = double(reblog?: true) classes = helper.microformats_classes(status, true, true) expect(classes).to eq 'p-in-reply-to p-repost-of p-comment' end end describe '#microformats_h_class' do it do status = double(reblog?: false) css_class = helper.microformats_h_class(status, false, false, false) expect(css_class).to eq 'h-entry' end it do status = double(reblog?: true) css_class = helper.microformats_h_class(status, false, false, false) expect(css_class).to eq 'h-cite' end it do status = double(reblog?: false) css_class = helper.microformats_h_class(status, true, false, false) expect(css_class).to eq 'h-cite' end it do status = double(reblog?: false) css_class = helper.microformats_h_class(status, false, true, false) expect(css_class).to eq 'h-cite' end it do status = double(reblog?: false) css_class = helper.microformats_h_class(status, false, false, true) expect(css_class).to eq '' end it do status = double(reblog?: true) css_class = helper.microformats_h_class(status, true, true, true) expect(css_class).to eq 'h-cite' end end describe '#rtl?' do it 'is false if text is empty' do expect(helper).not_to be_rtl '' end it 'is false if there are no right to left characters' do expect(helper).not_to be_rtl 'hello world' end it 'is false if right to left characters are fewer than 1/3 of total text' do expect(helper).not_to be_rtl 'hello ݟ world' end it 'is true if right to left characters are greater than 1/3 of total text' do expect(helper).to be_rtl 'aaݟaaݟ' end end end ================================================ FILE: spec/lib/activitypub/activity/accept_spec.rb ================================================ require 'rails_helper' RSpec.describe ActivityPub::Activity::Accept do let(:sender) { Fabricate(:account) } let(:recipient) { Fabricate(:account) } let(:json) do { '@context': 'https://www.w3.org/ns/activitystreams', id: 'foo', type: 'Accept', actor: ActivityPub::TagManager.instance.uri_for(sender), object: { id: 'bar', type: 'Follow', actor: ActivityPub::TagManager.instance.uri_for(recipient), object: ActivityPub::TagManager.instance.uri_for(sender), }, }.with_indifferent_access end describe '#perform' do subject { described_class.new(json, sender) } before do Fabricate(:follow_request, account: recipient, target_account: sender) subject.perform end it 'creates a follow relationship' do expect(recipient.following?(sender)).to be true end it 'removes the follow request' do expect(recipient.requested?(sender)).to be false end end context 'given a relay' do let!(:relay) { Fabricate(:relay, state: :pending, follow_activity_id: 'https://abc-123/456') } let(:json) do { '@context': 'https://www.w3.org/ns/activitystreams', id: 'foo', type: 'Accept', actor: ActivityPub::TagManager.instance.uri_for(sender), object: { id: 'https://abc-123/456', type: 'Follow', actor: ActivityPub::TagManager.instance.uri_for(recipient), object: ActivityPub::TagManager.instance.uri_for(sender), }, }.with_indifferent_access end subject { described_class.new(json, sender) } it 'marks the relay as accepted' do subject.perform expect(relay.reload.accepted?).to be true end end end ================================================ FILE: spec/lib/activitypub/activity/add_spec.rb ================================================ require 'rails_helper' RSpec.describe ActivityPub::Activity::Add do let(:sender) { Fabricate(:account, featured_collection_url: 'https://example.com/featured') } let(:status) { Fabricate(:status, account: sender) } let(:json) do { '@context': 'https://www.w3.org/ns/activitystreams', id: 'foo', type: 'Add', actor: ActivityPub::TagManager.instance.uri_for(sender), object: ActivityPub::TagManager.instance.uri_for(status), target: sender.featured_collection_url, }.with_indifferent_access end describe '#perform' do subject { described_class.new(json, sender) } it 'creates a pin' do subject.perform expect(sender.pinned?(status)).to be true end context 'when status was not known before' do let(:json) do { '@context': 'https://www.w3.org/ns/activitystreams', id: 'foo', type: 'Add', actor: ActivityPub::TagManager.instance.uri_for(sender), object: 'https://example.com/unknown', target: sender.featured_collection_url, }.with_indifferent_access end before do stub_request(:get, 'https://example.com/unknown').to_return(status: 410) end it 'fetches the status' do subject.perform expect(a_request(:get, 'https://example.com/unknown')).to have_been_made.at_least_once end end end end ================================================ FILE: spec/lib/activitypub/activity/announce_spec.rb ================================================ require 'rails_helper' RSpec.describe ActivityPub::Activity::Announce do let(:sender) { Fabricate(:account, followers_url: 'http://example.com/followers', uri: 'https://example.com/actor') } let(:recipient) { Fabricate(:account) } let(:status) { Fabricate(:status, account: recipient) } let(:json) do { '@context': 'https://www.w3.org/ns/activitystreams', id: 'foo', type: 'Announce', actor: 'https://example.com/actor', object: object_json, to: 'http://example.com/followers', }.with_indifferent_access end let(:unknown_object_json) do { '@context': 'https://www.w3.org/ns/activitystreams', id: 'https://example.com/actor/hello-world', type: 'Note', attributedTo: 'https://example.com/actor', content: 'Hello world', to: 'http://example.com/followers', } end subject { described_class.new(json, sender) } describe '#perform' do context 'when sender is followed by a local account' do before do Fabricate(:account).follow!(sender) stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json)) subject.perform end context 'a known status' do let(:object_json) do ActivityPub::TagManager.instance.uri_for(status) end it 'creates a reblog by sender of status' do expect(sender.reblogged?(status)).to be true end end context 'an unknown status' do let(:object_json) { 'https://example.com/actor/hello-world' } it 'creates a reblog by sender of status' do reblog = sender.statuses.first expect(reblog).to_not be_nil expect(reblog.reblog.text).to eq 'Hello world' end end context 'self-boost of a previously unknown status with correct attributedTo' do let(:object_json) do { id: 'https://example.com/actor#bar', type: 'Note', content: 'Lorem ipsum', attributedTo: 'https://example.com/actor', to: 'http://example.com/followers', } end it 'creates a reblog by sender of status' do expect(sender.reblogged?(sender.statuses.first)).to be true end end end context 'when the status belongs to a local user' do before do subject.perform end let(:object_json) do ActivityPub::TagManager.instance.uri_for(status) end it 'creates a reblog by sender of status' do expect(sender.reblogged?(status)).to be true end end context 'when the sender is relayed' do let!(:relay_account) { Fabricate(:account, inbox_url: 'https://relay.example.com/inbox') } let!(:relay) { Fabricate(:relay, inbox_url: 'https://relay.example.com/inbox') } subject { described_class.new(json, sender, relayed_through_account: relay_account) } context 'and the relay is enabled' do before do relay.update(state: :accepted) subject.perform end let(:object_json) do { id: 'https://example.com/actor#bar', type: 'Note', content: 'Lorem ipsum', to: 'http://example.com/followers', attributedTo: 'https://example.com/actor', } end it 'creates a reblog by sender of status' do expect(sender.statuses.count).to eq 2 end end context 'and the relay is disabled' do before do subject.perform end let(:object_json) do { id: 'https://example.com/actor#bar', type: 'Note', content: 'Lorem ipsum', to: 'http://example.com/followers', attributedTo: 'https://example.com/actor', } end it 'does not create anything' do expect(sender.statuses.count).to eq 0 end end end context 'when the sender has no relevance to local activity' do before do subject.perform end let(:object_json) do { id: 'https://example.com/actor#bar', type: 'Note', content: 'Lorem ipsum', to: 'http://example.com/followers', attributedTo: 'https://example.com/actor', } end it 'does not create anything' do expect(sender.statuses.count).to eq 0 end end end end ================================================ FILE: spec/lib/activitypub/activity/block_spec.rb ================================================ require 'rails_helper' RSpec.describe ActivityPub::Activity::Block do let(:sender) { Fabricate(:account) } let(:recipient) { Fabricate(:account) } let(:json) do { '@context': 'https://www.w3.org/ns/activitystreams', id: 'foo', type: 'Block', actor: ActivityPub::TagManager.instance.uri_for(sender), object: ActivityPub::TagManager.instance.uri_for(recipient), }.with_indifferent_access end context 'when the recipient does not follow the sender' do describe '#perform' do subject { described_class.new(json, sender) } before do subject.perform end it 'creates a block from sender to recipient' do expect(sender.blocking?(recipient)).to be true end end end context 'when the recipient follows the sender' do before do recipient.follow!(sender) end describe '#perform' do subject { described_class.new(json, sender) } before do subject.perform end it 'creates a block from sender to recipient' do expect(sender.blocking?(recipient)).to be true end it 'ensures recipient is not following sender' do expect(recipient.following?(sender)).to be false end end end context 'when a matching undo has been received first' do let(:undo_json) do { '@context': 'https://www.w3.org/ns/activitystreams', id: 'bar', type: 'Undo', actor: ActivityPub::TagManager.instance.uri_for(sender), object: json, }.with_indifferent_access end before do recipient.follow!(sender) ActivityPub::Activity::Undo.new(undo_json, sender).perform end describe '#perform' do subject { described_class.new(json, sender) } before do subject.perform end it 'does not create a block from sender to recipient' do expect(sender.blocking?(recipient)).to be false end it 'ensures recipient is not following sender' do expect(recipient.following?(sender)).to be false end end end end ================================================ FILE: spec/lib/activitypub/activity/create_spec.rb ================================================ require 'rails_helper' RSpec.describe ActivityPub::Activity::Create do let(:sender) { Fabricate(:account, followers_url: 'http://example.com/followers', domain: 'example.com', uri: 'https://example.com/actor') } let(:json) do { '@context': 'https://www.w3.org/ns/activitystreams', id: [ActivityPub::TagManager.instance.uri_for(sender), '#foo'].join, type: 'Create', actor: ActivityPub::TagManager.instance.uri_for(sender), object: object_json, }.with_indifferent_access end before do sender.update(uri: ActivityPub::TagManager.instance.uri_for(sender)) stub_request(:get, 'http://example.com/attachment.png').to_return(request_fixture('avatar.txt')) stub_request(:get, 'http://example.com/emoji.png').to_return(body: attachment_fixture('emojo.png')) end describe '#perform' do context 'when fetching' do subject { described_class.new(json, sender) } before do subject.perform end context 'unknown object type' do let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, type: 'Banana', content: 'Lorem ipsum', } end it 'does not create a status' do expect(sender.statuses.count).to be_zero end end context 'standalone' do let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, type: 'Note', content: 'Lorem ipsum', } end it 'creates status' do status = sender.statuses.first expect(status).to_not be_nil expect(status.text).to eq 'Lorem ipsum' end it 'missing to/cc defaults to direct privacy' do status = sender.statuses.first expect(status).to_not be_nil expect(status.visibility).to eq 'direct' end end context 'public' do let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, type: 'Note', content: 'Lorem ipsum', to: 'https://www.w3.org/ns/activitystreams#Public', } end it 'creates status' do status = sender.statuses.first expect(status).to_not be_nil expect(status.visibility).to eq 'public' end end context 'unlisted' do let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, type: 'Note', content: 'Lorem ipsum', cc: 'https://www.w3.org/ns/activitystreams#Public', } end it 'creates status' do status = sender.statuses.first expect(status).to_not be_nil expect(status.visibility).to eq 'unlisted' end end context 'private' do let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, type: 'Note', content: 'Lorem ipsum', to: 'http://example.com/followers', } end it 'creates status' do status = sender.statuses.first expect(status).to_not be_nil expect(status.visibility).to eq 'private' end end context 'limited' do let(:recipient) { Fabricate(:account) } let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, type: 'Note', content: 'Lorem ipsum', to: ActivityPub::TagManager.instance.uri_for(recipient), } end it 'creates status' do status = sender.statuses.first expect(status).to_not be_nil expect(status.visibility).to eq 'limited' end it 'creates silent mention' do status = sender.statuses.first expect(status.mentions.first).to be_silent end end context 'direct' do let(:recipient) { Fabricate(:account) } let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, type: 'Note', content: 'Lorem ipsum', to: ActivityPub::TagManager.instance.uri_for(recipient), tag: { type: 'Mention', href: ActivityPub::TagManager.instance.uri_for(recipient), }, } end it 'creates status' do status = sender.statuses.first expect(status).to_not be_nil expect(status.visibility).to eq 'direct' end end context 'as a reply' do let(:original_status) { Fabricate(:status) } let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, type: 'Note', content: 'Lorem ipsum', inReplyTo: ActivityPub::TagManager.instance.uri_for(original_status), } end it 'creates status' do status = sender.statuses.first expect(status).to_not be_nil expect(status.thread).to eq original_status expect(status.reply?).to be true expect(status.in_reply_to_account).to eq original_status.account expect(status.conversation).to eq original_status.conversation end end context 'with mentions' do let(:recipient) { Fabricate(:account) } let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, type: 'Note', content: 'Lorem ipsum', tag: [ { type: 'Mention', href: ActivityPub::TagManager.instance.uri_for(recipient), }, ], } end it 'creates status' do status = sender.statuses.first expect(status).to_not be_nil expect(status.mentions.map(&:account)).to include(recipient) end end context 'with mentions missing href' do let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, type: 'Note', content: 'Lorem ipsum', tag: [ { type: 'Mention', }, ], } end it 'creates status' do status = sender.statuses.first expect(status).to_not be_nil end end context 'with media attachments' do let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, type: 'Note', content: 'Lorem ipsum', attachment: [ { type: 'Document', mediaType: 'image/png', url: 'http://example.com/attachment.png', }, ], } end it 'creates status' do status = sender.statuses.first expect(status).to_not be_nil expect(status.media_attachments.map(&:remote_url)).to include('http://example.com/attachment.png') end end context 'with media attachments with focal points' do let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, type: 'Note', content: 'Lorem ipsum', attachment: [ { type: 'Document', mediaType: 'image/png', url: 'http://example.com/attachment.png', focalPoint: [0.5, -0.7], }, ], } end it 'creates status' do status = sender.statuses.first expect(status).to_not be_nil expect(status.media_attachments.map(&:focus)).to include('0.5,-0.7') end end context 'with media attachments missing url' do let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, type: 'Note', content: 'Lorem ipsum', attachment: [ { type: 'Document', mediaType: 'image/png', }, ], } end it 'creates status' do status = sender.statuses.first expect(status).to_not be_nil end end context 'with hashtags' do let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, type: 'Note', content: 'Lorem ipsum', tag: [ { type: 'Hashtag', href: 'http://example.com/blah', name: '#test', }, ], } end it 'creates status' do status = sender.statuses.first expect(status).to_not be_nil expect(status.tags.map(&:name)).to include('test') end end context 'with hashtags missing name' do let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, type: 'Note', content: 'Lorem ipsum', tag: [ { type: 'Hashtag', href: 'http://example.com/blah', }, ], } end it 'creates status' do status = sender.statuses.first expect(status).to_not be_nil end end context 'with emojis' do let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, type: 'Note', content: 'Lorem ipsum :tinking:', tag: [ { type: 'Emoji', icon: { url: 'http://example.com/emoji.png', }, name: 'tinking', }, ], } end it 'creates status' do status = sender.statuses.first expect(status).to_not be_nil expect(status.emojis.map(&:shortcode)).to include('tinking') end end context 'with emojis missing name' do let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, type: 'Note', content: 'Lorem ipsum :tinking:', tag: [ { type: 'Emoji', icon: { url: 'http://example.com/emoji.png', }, }, ], } end it 'creates status' do status = sender.statuses.first expect(status).to_not be_nil end end context 'with emojis missing icon' do let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, type: 'Note', content: 'Lorem ipsum :tinking:', tag: [ { type: 'Emoji', name: 'tinking', }, ], } end it 'creates status' do status = sender.statuses.first expect(status).to_not be_nil end end context 'with poll' do let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, type: 'Question', content: 'Which color was the submarine?', oneOf: [ { name: 'Yellow', replies: { type: 'Collection', totalItems: 10, }, }, { name: 'Blue', replies: { type: 'Collection', totalItems: 3, } }, ], } end it 'creates status' do status = sender.statuses.first expect(status).to_not be_nil expect(status.poll).to_not be_nil end it 'creates a poll' do poll = sender.polls.first expect(poll).to_not be_nil expect(poll.status).to_not be_nil expect(poll.options).to eq %w(Yellow Blue) expect(poll.cached_tallies).to eq [10, 3] end end context 'when a vote to a local poll' do let(:poll) { Fabricate(:poll, options: %w(Yellow Blue)) } let!(:local_status) { Fabricate(:status, poll: poll) } let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, type: 'Note', name: 'Yellow', inReplyTo: ActivityPub::TagManager.instance.uri_for(local_status) } end it 'adds a vote to the poll with correct uri' do vote = poll.votes.first expect(vote).to_not be_nil expect(vote.uri).to eq object_json[:id] expect(poll.reload.cached_tallies).to eq [1, 0] end end context 'when a vote to an expired local poll' do let(:poll) do poll = Fabricate.build(:poll, options: %w(Yellow Blue), expires_at: 1.day.ago) poll.save(validate: false) poll end let!(:local_status) { Fabricate(:status, poll: poll) } let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, type: 'Note', name: 'Yellow', inReplyTo: ActivityPub::TagManager.instance.uri_for(local_status) } end it 'does not add a vote to the poll' do expect(poll.votes.first).to be_nil end end end context 'when sender is followed by local users' do subject { described_class.new(json, sender, delivery: true) } before do Fabricate(:account).follow!(sender) subject.perform end let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, type: 'Note', content: 'Lorem ipsum', } end it 'creates status' do status = sender.statuses.first expect(status).to_not be_nil expect(status.text).to eq 'Lorem ipsum' end end context 'when sender replies to local status' do let!(:local_status) { Fabricate(:status) } subject { described_class.new(json, sender, delivery: true) } before do subject.perform end let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, type: 'Note', content: 'Lorem ipsum', inReplyTo: ActivityPub::TagManager.instance.uri_for(local_status), } end it 'creates status' do status = sender.statuses.first expect(status).to_not be_nil expect(status.text).to eq 'Lorem ipsum' end end context 'when sender targets a local user' do let!(:local_account) { Fabricate(:account) } subject { described_class.new(json, sender, delivery: true) } before do subject.perform end let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, type: 'Note', content: 'Lorem ipsum', to: ActivityPub::TagManager.instance.uri_for(local_account), } end it 'creates status' do status = sender.statuses.first expect(status).to_not be_nil expect(status.text).to eq 'Lorem ipsum' end end context 'when sender cc\'s a local user' do let!(:local_account) { Fabricate(:account) } subject { described_class.new(json, sender, delivery: true) } before do subject.perform end let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, type: 'Note', content: 'Lorem ipsum', cc: ActivityPub::TagManager.instance.uri_for(local_account), } end it 'creates status' do status = sender.statuses.first expect(status).to_not be_nil expect(status.text).to eq 'Lorem ipsum' end end context 'when the sender has no relevance to local activity' do subject { described_class.new(json, sender, delivery: true) } before do subject.perform end let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, type: 'Note', content: 'Lorem ipsum', } end it 'does not create anything' do expect(sender.statuses.count).to eq 0 end end end end ================================================ FILE: spec/lib/activitypub/activity/delete_spec.rb ================================================ require 'rails_helper' RSpec.describe ActivityPub::Activity::Delete do let(:sender) { Fabricate(:account, domain: 'example.com') } let(:status) { Fabricate(:status, account: sender, uri: 'foobar') } let(:json) do { '@context': 'https://www.w3.org/ns/activitystreams', id: 'foo', type: 'Delete', actor: ActivityPub::TagManager.instance.uri_for(sender), object: ActivityPub::TagManager.instance.uri_for(status), signature: 'foo', }.with_indifferent_access end describe '#perform' do subject { described_class.new(json, sender) } before do subject.perform end it 'deletes sender\'s status' do expect(Status.find_by(id: status.id)).to be_nil end end context 'when the status has been reblogged' do describe '#perform' do subject { described_class.new(json, sender) } let!(:reblogger) { Fabricate(:account) } let!(:follower) { Fabricate(:account, username: 'follower', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') } let!(:reblog) { Fabricate(:status, account: reblogger, reblog: status) } before do stub_request(:post, 'http://example.com/inbox').to_return(status: 200) follower.follow!(reblogger) subject.perform end it 'deletes sender\'s status' do expect(Status.find_by(id: status.id)).to be_nil end it 'sends delete activity to followers of rebloggers' do expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once end end end end ================================================ FILE: spec/lib/activitypub/activity/flag_spec.rb ================================================ require 'rails_helper' RSpec.describe ActivityPub::Activity::Flag do let(:sender) { Fabricate(:account, domain: 'example.com', uri: 'http://example.com/account') } let(:flagged) { Fabricate(:account) } let(:status) { Fabricate(:status, account: flagged, uri: 'foobar') } let(:flag_id) { nil } let(:json) do { '@context': 'https://www.w3.org/ns/activitystreams', id: flag_id, type: 'Flag', content: 'Boo!!', actor: ActivityPub::TagManager.instance.uri_for(sender), object: [ ActivityPub::TagManager.instance.uri_for(flagged), ActivityPub::TagManager.instance.uri_for(status), ], }.with_indifferent_access end describe '#perform' do subject { described_class.new(json, sender) } before do subject.perform end it 'creates a report' do report = Report.find_by(account: sender, target_account: flagged) expect(report).to_not be_nil expect(report.comment).to eq 'Boo!!' expect(report.status_ids).to eq [status.id] end end describe '#perform with a defined uri' do subject { described_class.new(json, sender) } let (:flag_id) { 'http://example.com/reports/1' } before do subject.perform end it 'creates a report' do report = Report.find_by(account: sender, target_account: flagged) expect(report).to_not be_nil expect(report.comment).to eq 'Boo!!' expect(report.status_ids).to eq [status.id] expect(report.uri).to eq flag_id end end end ================================================ FILE: spec/lib/activitypub/activity/follow_spec.rb ================================================ require 'rails_helper' RSpec.describe ActivityPub::Activity::Follow do let(:sender) { Fabricate(:account) } let(:recipient) { Fabricate(:account) } let(:json) do { '@context': 'https://www.w3.org/ns/activitystreams', id: 'foo', type: 'Follow', actor: ActivityPub::TagManager.instance.uri_for(sender), object: ActivityPub::TagManager.instance.uri_for(recipient), }.with_indifferent_access end describe '#perform' do subject { described_class.new(json, sender) } context 'unlocked account' do before do subject.perform end it 'creates a follow from sender to recipient' do expect(sender.following?(recipient)).to be true end it 'does not create a follow request' do expect(sender.requested?(recipient)).to be false end end context 'locked account' do before do recipient.update(locked: true) subject.perform end it 'does not create a follow from sender to recipient' do expect(sender.following?(recipient)).to be false end it 'creates a follow request' do expect(sender.requested?(recipient)).to be true end end end end ================================================ FILE: spec/lib/activitypub/activity/like_spec.rb ================================================ require 'rails_helper' RSpec.describe ActivityPub::Activity::Like do let(:sender) { Fabricate(:account) } let(:recipient) { Fabricate(:account) } let(:status) { Fabricate(:status, account: recipient) } let(:json) do { '@context': 'https://www.w3.org/ns/activitystreams', id: 'foo', type: 'Like', actor: ActivityPub::TagManager.instance.uri_for(sender), object: ActivityPub::TagManager.instance.uri_for(status), }.with_indifferent_access end describe '#perform' do subject { described_class.new(json, sender) } before do subject.perform end it 'creates a favourite from sender to status' do expect(sender.favourited?(status)).to be true end end end ================================================ FILE: spec/lib/activitypub/activity/move_spec.rb ================================================ require 'rails_helper' RSpec.describe ActivityPub::Activity::Move do let(:follower) { Fabricate(:account) } let(:old_account) { Fabricate(:account) } let(:new_account) { Fabricate(:account) } before do follower.follow!(old_account) old_account.update!(uri: 'https://example.org/alice', domain: 'example.org', protocol: :activitypub, inbox_url: 'https://example.org/inbox') new_account.update!(uri: 'https://example.com/alice', domain: 'example.com', protocol: :activitypub, inbox_url: 'https://example.com/inbox', also_known_as: [old_account.uri]) stub_request(:post, 'https://example.org/inbox').to_return(status: 200) stub_request(:post, 'https://example.com/inbox').to_return(status: 200) service_stub = double allow(ActivityPub::FetchRemoteAccountService).to receive(:new).and_return(service_stub) allow(service_stub).to receive(:call).and_return(new_account) end let(:json) do { '@context': 'https://www.w3.org/ns/activitystreams', id: 'foo', type: 'Move', actor: old_account.uri, object: old_account.uri, target: new_account.uri, }.with_indifferent_access end describe '#perform' do subject { described_class.new(json, old_account) } before do subject.perform end it 'sets moved account on old account' do expect(old_account.reload.moved_to_account_id).to eq new_account.id end it 'makes followers unfollow old account' do expect(follower.following?(old_account)).to be false end it 'makes followers follow-request the new account' do expect(follower.requested?(new_account)).to be true end end end ================================================ FILE: spec/lib/activitypub/activity/reject_spec.rb ================================================ require 'rails_helper' RSpec.describe ActivityPub::Activity::Reject do let(:sender) { Fabricate(:account) } let(:recipient) { Fabricate(:account) } let(:json) do { '@context': 'https://www.w3.org/ns/activitystreams', id: 'foo', type: 'Reject', actor: ActivityPub::TagManager.instance.uri_for(sender), object: { id: 'bar', type: 'Follow', actor: ActivityPub::TagManager.instance.uri_for(recipient), object: ActivityPub::TagManager.instance.uri_for(sender), }, }.with_indifferent_access end describe '#perform' do subject { described_class.new(json, sender) } before do Fabricate(:follow_request, account: recipient, target_account: sender) subject.perform end it 'does not create a follow relationship' do expect(recipient.following?(sender)).to be false end it 'removes the follow request' do expect(recipient.requested?(sender)).to be false end end context 'given a relay' do let!(:relay) { Fabricate(:relay, state: :pending, follow_activity_id: 'https://abc-123/456') } let(:json) do { '@context': 'https://www.w3.org/ns/activitystreams', id: 'foo', type: 'Reject', actor: ActivityPub::TagManager.instance.uri_for(sender), object: { id: 'https://abc-123/456', type: 'Follow', actor: ActivityPub::TagManager.instance.uri_for(recipient), object: ActivityPub::TagManager.instance.uri_for(sender), }, }.with_indifferent_access end subject { described_class.new(json, sender) } it 'marks the relay as rejected' do subject.perform expect(relay.reload.rejected?).to be true end end end ================================================ FILE: spec/lib/activitypub/activity/remove_spec.rb ================================================ require 'rails_helper' RSpec.describe ActivityPub::Activity::Remove do let(:sender) { Fabricate(:account, featured_collection_url: 'https://example.com/featured') } let(:status) { Fabricate(:status, account: sender) } let(:json) do { '@context': 'https://www.w3.org/ns/activitystreams', id: 'foo', type: 'Add', actor: ActivityPub::TagManager.instance.uri_for(sender), object: ActivityPub::TagManager.instance.uri_for(status), target: sender.featured_collection_url, }.with_indifferent_access end describe '#perform' do subject { described_class.new(json, sender) } before do StatusPin.create!(account: sender, status: status) subject.perform end it 'removes a pin' do expect(sender.pinned?(status)).to be false end end end ================================================ FILE: spec/lib/activitypub/activity/undo_spec.rb ================================================ require 'rails_helper' RSpec.describe ActivityPub::Activity::Undo do let(:sender) { Fabricate(:account, domain: 'example.com') } let(:json) do { '@context': 'https://www.w3.org/ns/activitystreams', id: 'foo', type: 'Undo', actor: ActivityPub::TagManager.instance.uri_for(sender), object: object_json, }.with_indifferent_access end subject { described_class.new(json, sender) } describe '#perform' do context 'with Announce' do let(:status) { Fabricate(:status) } let(:object_json) do { id: 'bar', type: 'Announce', actor: ActivityPub::TagManager.instance.uri_for(sender), object: ActivityPub::TagManager.instance.uri_for(status), atomUri: 'barbar', } end context do before do Fabricate(:status, reblog: status, account: sender, uri: 'bar') end it 'deletes the reblog' do subject.perform expect(sender.reblogged?(status)).to be false end end context 'with atomUri' do before do Fabricate(:status, reblog: status, account: sender, uri: 'barbar') end it 'deletes the reblog by atomUri' do subject.perform expect(sender.reblogged?(status)).to be false end end end context 'with Accept' do let(:recipient) { Fabricate(:account) } let(:object_json) do { id: 'bar', type: 'Accept', actor: ActivityPub::TagManager.instance.uri_for(sender), object: 'follow-to-revoke', } end before do recipient.follow!(sender, uri: 'follow-to-revoke') end it 'deletes follow from recipient to sender' do subject.perform expect(recipient.following?(sender)).to be false end it 'creates a follow request from recipient to sender' do subject.perform expect(recipient.requested?(sender)).to be true end end context 'with Block' do let(:recipient) { Fabricate(:account) } let(:object_json) do { id: 'bar', type: 'Block', actor: ActivityPub::TagManager.instance.uri_for(sender), object: ActivityPub::TagManager.instance.uri_for(recipient), } end before do sender.block!(recipient) end it 'deletes block from sender to recipient' do subject.perform expect(sender.blocking?(recipient)).to be false end end context 'with Follow' do let(:recipient) { Fabricate(:account) } let(:object_json) do { id: 'bar', type: 'Follow', actor: ActivityPub::TagManager.instance.uri_for(sender), object: ActivityPub::TagManager.instance.uri_for(recipient), } end before do sender.follow!(recipient) end it 'deletes follow from sender to recipient' do subject.perform expect(sender.following?(recipient)).to be false end end context 'with Like' do let(:status) { Fabricate(:status) } let(:object_json) do { id: 'bar', type: 'Like', actor: ActivityPub::TagManager.instance.uri_for(sender), object: ActivityPub::TagManager.instance.uri_for(status), } end before do Fabricate(:favourite, account: sender, status: status) end it 'deletes favourite from sender to status' do subject.perform expect(sender.favourited?(status)).to be false end end end end ================================================ FILE: spec/lib/activitypub/activity/update_spec.rb ================================================ require 'rails_helper' RSpec.describe ActivityPub::Activity::Update do let!(:sender) { Fabricate(:account) } before do stub_request(:get, actor_json[:outbox]).to_return(status: 404) stub_request(:get, actor_json[:followers]).to_return(status: 404) stub_request(:get, actor_json[:following]).to_return(status: 404) stub_request(:get, actor_json[:featured]).to_return(status: 404) sender.update!(uri: ActivityPub::TagManager.instance.uri_for(sender)) end let(:modified_sender) do sender.dup.tap do |modified_sender| modified_sender.display_name = 'Totally modified now' end end let(:actor_json) do ActiveModelSerializers::SerializableResource.new(modified_sender, serializer: ActivityPub::ActorSerializer, key_transform: :camel_lower).as_json end let(:json) do { '@context': 'https://www.w3.org/ns/activitystreams', id: 'foo', type: 'Update', actor: ActivityPub::TagManager.instance.uri_for(sender), object: actor_json, }.with_indifferent_access end describe '#perform' do subject { described_class.new(json, sender) } before do subject.perform end it 'updates profile' do expect(sender.reload.display_name).to eq 'Totally modified now' end end end ================================================ FILE: spec/lib/activitypub/adapter_spec.rb ================================================ require 'rails_helper' RSpec.describe ActivityPub::Adapter do class TestObject < ActiveModelSerializers::Model attributes :foo end class TestWithBasicContextSerializer < ActivityPub::Serializer attributes :foo end class TestWithNamedContextSerializer < ActivityPub::Serializer context :security attributes :foo end class TestWithNestedNamedContextSerializer < ActivityPub::Serializer attributes :foo has_one :virtual_object, key: :baz, serializer: TestWithNamedContextSerializer def virtual_object object end end class TestWithContextExtensionSerializer < ActivityPub::Serializer context_extensions :sensitive attributes :foo end class TestWithNestedContextExtensionSerializer < ActivityPub::Serializer context_extensions :manually_approves_followers attributes :foo has_one :virtual_object, key: :baz, serializer: TestWithContextExtensionSerializer def virtual_object object end end describe '#serializable_hash' do let(:serializer_class) {} subject { ActiveModelSerializers::SerializableResource.new(TestObject.new(foo: 'bar'), serializer: serializer_class, adapter: described_class).as_json } context 'when serializer defines no context' do let(:serializer_class) { TestWithBasicContextSerializer } it 'renders a basic @context' do expect(subject).to include({ '@context' => 'https://www.w3.org/ns/activitystreams' }) end end context 'when serializer defines a named context' do let(:serializer_class) { TestWithNamedContextSerializer } it 'renders a @context with both items' do expect(subject).to include({ '@context' => ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'] }) end end context 'when serializer has children that define a named context' do let(:serializer_class) { TestWithNestedNamedContextSerializer } it 'renders a @context with both items' do expect(subject).to include({ '@context' => ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'] }) end end context 'when serializer defines context extensions' do let(:serializer_class) { TestWithContextExtensionSerializer } it 'renders a @context with the extension' do expect(subject).to include({ '@context' => ['https://www.w3.org/ns/activitystreams', { 'sensitive' => 'as:sensitive' }] }) end end context 'when serializer has children that define context extensions' do let(:serializer_class) { TestWithNestedContextExtensionSerializer } it 'renders a @context with both extensions' do expect(subject).to include({ '@context' => ['https://www.w3.org/ns/activitystreams', { 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers', 'sensitive' => 'as:sensitive' }] }) end end end end ================================================ FILE: spec/lib/activitypub/linked_data_signature_spec.rb ================================================ require 'rails_helper' RSpec.describe ActivityPub::LinkedDataSignature do include JsonLdHelper let!(:sender) { Fabricate(:account, uri: 'http://example.com/alice') } let(:raw_json) do { '@context' => 'https://www.w3.org/ns/activitystreams', 'id' => 'http://example.com/hello-world', } end let(:json) { raw_json.merge('signature' => signature) } subject { described_class.new(json) } before do stub_jsonld_contexts! end describe '#verify_account!' do context 'when signature matches' do let(:raw_signature) do { 'creator' => 'http://example.com/alice', 'created' => '2017-09-23T20:21:34Z', } end let(:signature) { raw_signature.merge('type' => 'RsaSignature2017', 'signatureValue' => sign(sender, raw_signature, raw_json)) } it 'returns creator' do expect(subject.verify_account!).to eq sender end end context 'when signature is missing' do let(:signature) { nil } it 'returns nil' do expect(subject.verify_account!).to be_nil end end context 'when signature is tampered' do let(:raw_signature) do { 'creator' => 'http://example.com/alice', 'created' => '2017-09-23T20:21:34Z', } end let(:signature) { raw_signature.merge('type' => 'RsaSignature2017', 'signatureValue' => 's69F3mfddd99dGjmvjdjjs81e12jn121Gkm1') } it 'returns nil' do expect(subject.verify_account!).to be_nil end end end describe '#sign!' do subject { described_class.new(raw_json).sign!(sender) } it 'returns a hash' do expect(subject).to be_a Hash end it 'contains signature' do expect(subject['signature']).to be_a Hash expect(subject['signature']['signatureValue']).to be_present end it 'can be verified again' do expect(described_class.new(subject).verify_account!).to eq sender end end def sign(from_account, options, document) options_hash = Digest::SHA256.hexdigest(canonicalize(options.merge('@context' => ActivityPub::LinkedDataSignature::CONTEXT))) document_hash = Digest::SHA256.hexdigest(canonicalize(document)) to_be_verified = options_hash + document_hash Base64.strict_encode64(from_account.keypair.sign(OpenSSL::Digest::SHA256.new, to_be_verified)) end end ================================================ FILE: spec/lib/activitypub/tag_manager_spec.rb ================================================ require 'rails_helper' RSpec.describe ActivityPub::TagManager do include RoutingHelper subject { described_class.instance } describe '#url_for' do it 'returns a string' do account = Fabricate(:account) expect(subject.url_for(account)).to be_a String end end describe '#uri_for' do it 'returns a string' do account = Fabricate(:account) expect(subject.uri_for(account)).to be_a String end end describe '#to' do it 'returns public collection for public status' do status = Fabricate(:status, visibility: :public) expect(subject.to(status)).to eq ['https://www.w3.org/ns/activitystreams#Public'] end it 'returns followers collection for unlisted status' do status = Fabricate(:status, visibility: :unlisted) expect(subject.to(status)).to eq [account_followers_url(status.account)] end it 'returns followers collection for private status' do status = Fabricate(:status, visibility: :private) expect(subject.to(status)).to eq [account_followers_url(status.account)] end it 'returns URIs of mentions for direct status' do status = Fabricate(:status, visibility: :direct) mentioned = Fabricate(:account) status.mentions.create(account: mentioned) expect(subject.to(status)).to eq [subject.uri_for(mentioned)] end it "returns URIs of mentions for direct silenced author's status only if they are followers or requesting to be" do bob = Fabricate(:account, username: 'bob') alice = Fabricate(:account, username: 'alice') foo = Fabricate(:account) author = Fabricate(:account, username: 'author', silenced: true) status = Fabricate(:status, visibility: :direct, account: author) bob.follow!(author) FollowRequest.create!(account: foo, target_account: author) status.mentions.create(account: alice) status.mentions.create(account: bob) status.mentions.create(account: foo) expect(subject.to(status)).to include(subject.uri_for(bob)) expect(subject.to(status)).to include(subject.uri_for(foo)) expect(subject.to(status)).to_not include(subject.uri_for(alice)) end end describe '#cc' do it 'returns followers collection for public status' do status = Fabricate(:status, visibility: :public) expect(subject.cc(status)).to eq [account_followers_url(status.account)] end it 'returns public collection for unlisted status' do status = Fabricate(:status, visibility: :unlisted) expect(subject.cc(status)).to eq ['https://www.w3.org/ns/activitystreams#Public'] end it 'returns empty array for private status' do status = Fabricate(:status, visibility: :private) expect(subject.cc(status)).to eq [] end it 'returns empty array for direct status' do status = Fabricate(:status, visibility: :direct) expect(subject.cc(status)).to eq [] end it 'returns URIs of mentions for non-direct status' do status = Fabricate(:status, visibility: :public) mentioned = Fabricate(:account) status.mentions.create(account: mentioned) expect(subject.cc(status)).to include(subject.uri_for(mentioned)) end it "returns URIs of mentions for silenced author's non-direct status only if they are followers or requesting to be" do bob = Fabricate(:account, username: 'bob') alice = Fabricate(:account, username: 'alice') foo = Fabricate(:account) author = Fabricate(:account, username: 'author', silenced: true) status = Fabricate(:status, visibility: :public, account: author) bob.follow!(author) FollowRequest.create!(account: foo, target_account: author) status.mentions.create(account: alice) status.mentions.create(account: bob) status.mentions.create(account: foo) expect(subject.cc(status)).to include(subject.uri_for(bob)) expect(subject.cc(status)).to include(subject.uri_for(foo)) expect(subject.cc(status)).to_not include(subject.uri_for(alice)) end end describe '#local_uri?' do it 'returns false for non-local URI' do expect(subject.local_uri?('http://example.com/123')).to be false end it 'returns true for local URIs' do account = Fabricate(:account) expect(subject.local_uri?(subject.uri_for(account))).to be true end end describe '#uri_to_local_id' do it 'returns the local ID' do account = Fabricate(:account) expect(subject.uri_to_local_id(subject.uri_for(account), :username)).to eq account.username end end describe '#uri_to_resource' do it 'returns the local account' do account = Fabricate(:account) expect(subject.uri_to_resource(subject.uri_for(account), Account)).to eq account end it 'returns the remote account by matching URI without fragment part' do account = Fabricate(:account, uri: 'https://example.com/123') expect(subject.uri_to_resource('https://example.com/123#456', Account)).to eq account end it 'returns the local status for ActivityPub URI' do status = Fabricate(:status) expect(subject.uri_to_resource(subject.uri_for(status), Status)).to eq status end it 'returns the local status for OStatus tag: URI' do status = Fabricate(:status) expect(subject.uri_to_resource(OStatus::TagManager.instance.uri_for(status), Status)).to eq status end it 'returns the local status for OStatus StreamEntry URL' do status = Fabricate(:status) stream_entry_url = account_stream_entry_url(status.account, status.stream_entry) expect(subject.uri_to_resource(stream_entry_url, Status)).to eq status end it 'returns the remote status by matching URI without fragment part' do status = Fabricate(:status, uri: 'https://example.com/123') expect(subject.uri_to_resource('https://example.com/123#456', Status)).to eq status end end end ================================================ FILE: spec/lib/delivery_failure_tracker_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe DeliveryFailureTracker do subject { described_class.new('http://example.com/inbox') } describe '#track_success!' do before do subject.track_failure! subject.track_success! end it 'marks URL as available again' do expect(described_class.available?('http://example.com/inbox')).to be true end it 'resets days to 0' do expect(subject.days).to be_zero end end describe '#track_failure!' do it 'marks URL as unavailable after 7 days of being called' do 6.times { |i| Redis.current.sadd('exhausted_deliveries:http://example.com/inbox', i) } subject.track_failure! expect(subject.days).to eq 7 expect(described_class.unavailable?('http://example.com/inbox')).to be true end it 'repeated calls on the same day do not count' do subject.track_failure! subject.track_failure! expect(subject.days).to eq 1 end end describe '.filter' do before do Redis.current.sadd('unavailable_inboxes', 'http://example.com/unavailable/inbox') end it 'removes URLs that are unavailable' do result = described_class.filter(['http://example.com/good/inbox', 'http://example.com/unavailable/inbox']) expect(result).to include('http://example.com/good/inbox') expect(result).to_not include('http://example.com/unavailable/inbox') end end describe '.track_inverse_success!' do let(:from_account) { Fabricate(:account, inbox_url: 'http://example.com/inbox', shared_inbox_url: 'http://example.com/shared/inbox') } before do Redis.current.sadd('unavailable_inboxes', 'http://example.com/inbox') Redis.current.sadd('unavailable_inboxes', 'http://example.com/shared/inbox') described_class.track_inverse_success!(from_account) end it 'marks inbox URL as available again' do expect(described_class.available?('http://example.com/inbox')).to be true end it 'marks shared inbox URL as available again' do expect(described_class.available?('http://example.com/shared/inbox')).to be true end end end ================================================ FILE: spec/lib/extractor_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe Extractor do describe 'extract_mentions_or_lists_with_indices' do it 'returns an empty array if the given string does not have at signs' do text = 'a string without at signs' extracted = Extractor.extract_mentions_or_lists_with_indices(text) expect(extracted).to eq [] end it 'does not extract mentions which ends with particular characters' do text = '@screen_name@' extracted = Extractor.extract_mentions_or_lists_with_indices(text) expect(extracted).to eq [] end it 'returns mentions as an array' do text = '@screen_name' extracted = Extractor.extract_mentions_or_lists_with_indices(text) expect(extracted).to eq [ { screen_name: 'screen_name', indices: [ 0, 12 ] } ] end it 'yields mentions if a block is given' do text = '@screen_name' Extractor.extract_mentions_or_lists_with_indices(text) do |screen_name, start_position, end_position| expect(screen_name).to eq 'screen_name' expect(start_position).to eq 0 expect(end_position).to eq 12 end end end describe 'extract_hashtags_with_indices' do it 'returns an empty array if it does not have #' do text = 'a string without hash sign' extracted = Extractor.extract_hashtags_with_indices(text) expect(extracted).to eq [] end it 'does not exclude normal hash text before ://' do text = '#hashtag://' extracted = Extractor.extract_hashtags_with_indices(text) expect(extracted).to eq [ { hashtag: 'hashtag', indices: [ 0, 8 ] } ] end it 'excludes http://' do text = '#hashtaghttp://' extracted = Extractor.extract_hashtags_with_indices(text) expect(extracted).to eq [ { hashtag: 'hashtag', indices: [ 0, 8 ] } ] end it 'excludes https://' do text = '#hashtaghttps://' extracted = Extractor.extract_hashtags_with_indices(text) expect(extracted).to eq [ { hashtag: 'hashtag', indices: [ 0, 8 ] } ] end it 'yields hashtags if a block is given' do text = '#hashtag' Extractor.extract_hashtags_with_indices(text) do |hashtag, start_position, end_position| expect(hashtag).to eq 'hashtag' expect(start_position).to eq 0 expect(end_position).to eq 8 end end end describe 'extract_cashtags_with_indices' do it 'returns []' do text = '$cashtag' extracted = Extractor.extract_cashtags_with_indices(text) expect(extracted).to eq [] end end end ================================================ FILE: spec/lib/feed_manager_spec.rb ================================================ require 'rails_helper' RSpec.describe FeedManager do before do |example| unless example.metadata[:skip_stub] stub_const 'FeedManager::MAX_ITEMS', 10 stub_const 'FeedManager::REBLOG_FALLOFF', 4 end end it 'tracks at least as many statuses as reblogs', skip_stub: true do expect(FeedManager::REBLOG_FALLOFF).to be <= FeedManager::MAX_ITEMS end describe '#key' do subject { FeedManager.instance.key(:home, 1) } it 'returns a string' do expect(subject).to be_a String end end describe '#filter?' do let(:alice) { Fabricate(:account, username: 'alice') } let(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com') } let(:jeff) { Fabricate(:account, username: 'jeff') } context 'for home feed' do it 'returns false for followee\'s status' do status = Fabricate(:status, text: 'Hello world', account: alice) bob.follow!(alice) expect(FeedManager.instance.filter?(:home, status, bob.id)).to be false end it 'returns false for reblog by followee' do status = Fabricate(:status, text: 'Hello world', account: jeff) reblog = Fabricate(:status, reblog: status, account: alice) bob.follow!(alice) expect(FeedManager.instance.filter?(:home, reblog, bob.id)).to be false end it 'returns true for reblog by followee of blocked account' do status = Fabricate(:status, text: 'Hello world', account: jeff) reblog = Fabricate(:status, reblog: status, account: alice) bob.follow!(alice) bob.block!(jeff) expect(FeedManager.instance.filter?(:home, reblog, bob.id)).to be true end it 'returns true for reblog by followee of muted account' do status = Fabricate(:status, text: 'Hello world', account: jeff) reblog = Fabricate(:status, reblog: status, account: alice) bob.follow!(alice) bob.mute!(jeff) expect(FeedManager.instance.filter?(:home, reblog, bob.id)).to be true end it 'returns true for reblog by followee of someone who is blocking recipient' do status = Fabricate(:status, text: 'Hello world', account: jeff) reblog = Fabricate(:status, reblog: status, account: alice) bob.follow!(alice) jeff.block!(bob) expect(FeedManager.instance.filter?(:home, reblog, bob.id)).to be true end it 'returns true for reblog from account with reblogs disabled' do status = Fabricate(:status, text: 'Hello world', account: jeff) reblog = Fabricate(:status, reblog: status, account: alice) bob.follow!(alice, reblogs: false) expect(FeedManager.instance.filter?(:home, reblog, bob.id)).to be true end it 'returns false for reply by followee to another followee' do status = Fabricate(:status, text: 'Hello world', account: jeff) reply = Fabricate(:status, text: 'Nay', thread: status, account: alice) bob.follow!(alice) bob.follow!(jeff) expect(FeedManager.instance.filter?(:home, reply, bob.id)).to be false end it 'returns false for reply by followee to recipient' do status = Fabricate(:status, text: 'Hello world', account: bob) reply = Fabricate(:status, text: 'Nay', thread: status, account: alice) bob.follow!(alice) expect(FeedManager.instance.filter?(:home, reply, bob.id)).to be false end it 'returns false for reply by followee to self' do status = Fabricate(:status, text: 'Hello world', account: alice) reply = Fabricate(:status, text: 'Nay', thread: status, account: alice) bob.follow!(alice) expect(FeedManager.instance.filter?(:home, reply, bob.id)).to be false end it 'returns true for reply by followee to non-followed account' do status = Fabricate(:status, text: 'Hello world', account: jeff) reply = Fabricate(:status, text: 'Nay', thread: status, account: alice) bob.follow!(alice) expect(FeedManager.instance.filter?(:home, reply, bob.id)).to be true end it 'returns true for the second reply by followee to a non-federated status' do reply = Fabricate(:status, text: 'Reply 1', reply: true, account: alice) second_reply = Fabricate(:status, text: 'Reply 2', thread: reply, account: alice) bob.follow!(alice) expect(FeedManager.instance.filter?(:home, second_reply, bob.id)).to be true end it 'returns false for status by followee mentioning another account' do bob.follow!(alice) status = PostStatusService.new.call(alice, text: 'Hey @jeff') expect(FeedManager.instance.filter?(:home, status, bob.id)).to be false end it 'returns true for status by followee mentioning blocked account' do bob.block!(jeff) bob.follow!(alice) status = PostStatusService.new.call(alice, text: 'Hey @jeff') expect(FeedManager.instance.filter?(:home, status, bob.id)).to be true end it 'returns true for reblog of a personally blocked domain' do alice.block_domain!('example.com') alice.follow!(jeff) status = Fabricate(:status, text: 'Hello world', account: bob) reblog = Fabricate(:status, reblog: status, account: jeff) expect(FeedManager.instance.filter?(:home, reblog, alice.id)).to be true end context 'for irreversibly muted phrases' do it 'considers word boundaries when matching' do alice.custom_filters.create!(phrase: 'bob', context: %w(home), irreversible: true) alice.follow!(jeff) status = Fabricate(:status, text: 'bobcats', account: jeff) expect(FeedManager.instance.filter?(:home, status, alice.id)).to be_falsy end it 'returns true if phrase is contained' do alice.custom_filters.create!(phrase: 'farts', context: %w(home public), irreversible: true) alice.custom_filters.create!(phrase: 'pop tarts', context: %w(home), irreversible: true) alice.follow!(jeff) status = Fabricate(:status, text: 'i sure like POP TARts', account: jeff) expect(FeedManager.instance.filter?(:home, status, alice.id)).to be true end it 'matches substrings if whole_word is false' do alice.custom_filters.create!(phrase: 'take', context: %w(home), whole_word: false, irreversible: true) alice.follow!(jeff) status = Fabricate(:status, text: 'shiitake', account: jeff) expect(FeedManager.instance.filter?(:home, status, alice.id)).to be true end end end context 'for mentions feed' do it 'returns true for status that mentions blocked account' do bob.block!(jeff) status = PostStatusService.new.call(alice, text: 'Hey @jeff') expect(FeedManager.instance.filter?(:mentions, status, bob.id)).to be true end it 'returns true for status that replies to a blocked account' do status = Fabricate(:status, text: 'Hello world', account: jeff) reply = Fabricate(:status, text: 'Nay', thread: status, account: alice) bob.block!(jeff) expect(FeedManager.instance.filter?(:mentions, reply, bob.id)).to be true end it 'returns true for status by silenced account who recipient is not following' do status = Fabricate(:status, text: 'Hello world', account: alice) alice.silence! expect(FeedManager.instance.filter?(:mentions, status, bob.id)).to be true end it 'returns false for status by followed silenced account' do status = Fabricate(:status, text: 'Hello world', account: alice) alice.silence! bob.follow!(alice) expect(FeedManager.instance.filter?(:mentions, status, bob.id)).to be false end end end describe '#push_to_home' do it 'trims timelines if they will have more than FeedManager::MAX_ITEMS' do account = Fabricate(:account) status = Fabricate(:status) members = FeedManager::MAX_ITEMS.times.map { |count| [count, count] } Redis.current.zadd("feed:home:#{account.id}", members) FeedManager.instance.push_to_home(account, status) expect(Redis.current.zcard("feed:home:#{account.id}")).to eq FeedManager::MAX_ITEMS end context 'reblogs' do it 'saves reblogs of unseen statuses' do account = Fabricate(:account) reblogged = Fabricate(:status) reblog = Fabricate(:status, reblog: reblogged) expect(FeedManager.instance.push_to_home(account, reblog)).to be true end it 'does not save a new reblog of a recent status' do account = Fabricate(:account) reblogged = Fabricate(:status) reblog = Fabricate(:status, reblog: reblogged) FeedManager.instance.push_to_home(account, reblogged) expect(FeedManager.instance.push_to_home(account, reblog)).to be false end it 'saves a new reblog of an old status' do account = Fabricate(:account) reblogged = Fabricate(:status) reblog = Fabricate(:status, reblog: reblogged) FeedManager.instance.push_to_home(account, reblogged) # Fill the feed with intervening statuses FeedManager::REBLOG_FALLOFF.times do FeedManager.instance.push_to_home(account, Fabricate(:status)) end expect(FeedManager.instance.push_to_home(account, reblog)).to be true end it 'does not save a new reblog of a recently-reblogged status' do account = Fabricate(:account) reblogged = Fabricate(:status) reblogs = 2.times.map { Fabricate(:status, reblog: reblogged) } # The first reblog will be accepted FeedManager.instance.push_to_home(account, reblogs.first) # The second reblog should be ignored expect(FeedManager.instance.push_to_home(account, reblogs.last)).to be false end it 'does not save a new reblog of a multiply-reblogged-then-unreblogged status' do account = Fabricate(:account) reblogged = Fabricate(:status) reblogs = 3.times.map { Fabricate(:status, reblog: reblogged) } # Accept the reblogs FeedManager.instance.push_to_home(account, reblogs[0]) FeedManager.instance.push_to_home(account, reblogs[1]) # Unreblog the first one FeedManager.instance.unpush_from_home(account, reblogs[0]) # The last reblog should still be ignored expect(FeedManager.instance.push_to_home(account, reblogs.last)).to be false end it 'saves a new reblog of a long-ago-reblogged status' do account = Fabricate(:account) reblogged = Fabricate(:status) reblogs = 2.times.map { Fabricate(:status, reblog: reblogged) } # The first reblog will be accepted FeedManager.instance.push_to_home(account, reblogs.first) # Fill the feed with intervening statuses FeedManager::REBLOG_FALLOFF.times do FeedManager.instance.push_to_home(account, Fabricate(:status)) end # The second reblog should also be accepted expect(FeedManager.instance.push_to_home(account, reblogs.last)).to be true end end it "does not push when the given status's reblog is already inserted" do account = Fabricate(:account) reblog = Fabricate(:status) status = Fabricate(:status, reblog: reblog) FeedManager.instance.push_to_home(account, status) expect(FeedManager.instance.push_to_home(account, reblog)).to eq false end end describe '#push_to_list' do it "does not push when the given status's reblog is already inserted" do list = Fabricate(:list) reblog = Fabricate(:status) status = Fabricate(:status, reblog: reblog) FeedManager.instance.push_to_list(list, status) expect(FeedManager.instance.push_to_list(list, reblog)).to eq false end end describe '#merge_into_timeline' do it "does not push source account's statuses whose reblogs are already inserted" do account = Fabricate(:account, id: 0) reblog = Fabricate(:status) status = Fabricate(:status, reblog: reblog) FeedManager.instance.push_to_home(account, status) FeedManager.instance.merge_into_timeline(account, reblog.account) expect(Redis.current.zscore("feed:home:0", reblog.id)).to eq nil end it "does not push direct messages into home timeline if the user has home_dms false" do status = Fabricate(:status, visibility: :direct) mentioned = Fabricate(:account, id: 1) mentioned.user = Fabricate(:user) mentioned.user.settings.home_dms = false status.mentions.create(account: mentioned) FeedManager.instance.merge_into_timeline(status.account, mentioned) expect(Redis.current.zscore("feed:home:1",status.id)).to be_nil end it "pushes direct messages into home timeline if the user has home_dms true" do status = Fabricate(:status, visibility: :direct) mentioned = Fabricate(:account, id: 1) mentioned.user = Fabricate(:user) mentioned.user.settings.home_dms = true status.mentions.create(account: mentioned) FeedManager.instance.merge_into_timeline(status.account, mentioned) expect(Redis.current.zscore("feed:home:1",status.id)).to_not be_nil end end describe '#trim' do let(:receiver) { Fabricate(:account) } it 'cleans up reblog tracking keys' do reblogged = Fabricate(:status) status = Fabricate(:status, reblog: reblogged) another_status = Fabricate(:status, reblog: reblogged) reblogs_key = FeedManager.instance.key('home', receiver.id, 'reblogs') reblog_set_key = FeedManager.instance.key('home', receiver.id, "reblogs:#{reblogged.id}") FeedManager.instance.push_to_home(receiver, status) FeedManager.instance.push_to_home(receiver, another_status) # We should have a tracking set and an entry in reblogs. expect(Redis.current.exists(reblog_set_key)).to be true expect(Redis.current.zrange(reblogs_key, 0, -1)).to eq [reblogged.id.to_s] # Push everything off the end of the feed. FeedManager::MAX_ITEMS.times do FeedManager.instance.push_to_home(receiver, Fabricate(:status)) end # `trim` should be called automatically, but do it anyway, as # we're testing `trim`, not side effects of `push`. FeedManager.instance.trim('home', receiver.id) # We should not have any reblog tracking data. expect(Redis.current.exists(reblog_set_key)).to be false expect(Redis.current.zrange(reblogs_key, 0, -1)).to be_empty end end describe '#unpush' do let(:receiver) { Fabricate(:account) } it 'leaves a reblogged status if original was on feed' do reblogged = Fabricate(:status) status = Fabricate(:status, reblog: reblogged) FeedManager.instance.push_to_home(receiver, reblogged) FeedManager::REBLOG_FALLOFF.times { FeedManager.instance.push_to_home(receiver, Fabricate(:status)) } FeedManager.instance.push_to_home(receiver, status) # The reblogging status should show up under normal conditions. expect(Redis.current.zrange("feed:home:#{receiver.id}", 0, -1)).to include(status.id.to_s) FeedManager.instance.unpush_from_home(receiver, status) # Restore original status expect(Redis.current.zrange("feed:home:#{receiver.id}", 0, -1)).to_not include(status.id.to_s) expect(Redis.current.zrange("feed:home:#{receiver.id}", 0, -1)).to include(reblogged.id.to_s) end it 'removes a reblogged status if it was only reblogged once' do reblogged = Fabricate(:status) status = Fabricate(:status, reblog: reblogged) FeedManager.instance.push_to_home(receiver, status) # The reblogging status should show up under normal conditions. expect(Redis.current.zrange("feed:home:#{receiver.id}", 0, -1)).to eq [status.id.to_s] FeedManager.instance.unpush_from_home(receiver, status) expect(Redis.current.zrange("feed:home:#{receiver.id}", 0, -1)).to be_empty end it 'leaves a multiply-reblogged status if another reblog was in feed' do reblogged = Fabricate(:status) reblogs = 3.times.map { Fabricate(:status, reblog: reblogged) } reblogs.each do |reblog| FeedManager.instance.push_to_home(receiver, reblog) end # The reblogging status should show up under normal conditions. expect(Redis.current.zrange("feed:home:#{receiver.id}", 0, -1)).to eq [reblogs.first.id.to_s] reblogs[0...-1].each do |reblog| FeedManager.instance.unpush_from_home(receiver, reblog) end expect(Redis.current.zrange("feed:home:#{receiver.id}", 0, -1)).to eq [reblogs.last.id.to_s] end it 'sends push updates' do status = Fabricate(:status) FeedManager.instance.push_to_home(receiver, status) allow(Redis.current).to receive_messages(publish: nil) FeedManager.instance.unpush_from_home(receiver, status) deletion = Oj.dump(event: :delete, payload: status.id.to_s) expect(Redis.current).to have_received(:publish).with("timeline:#{receiver.id}", deletion) end end end ================================================ FILE: spec/lib/formatter_spec.rb ================================================ require 'rails_helper' RSpec.describe Formatter do let(:local_account) { Fabricate(:account, domain: nil, username: 'alice') } let(:remote_account) { Fabricate(:account, domain: 'remote.test', username: 'bob', url: 'https://remote.test/') } shared_examples 'encode and link URLs' do context 'given a stand-alone medium URL' do let(:text) { 'https://hackernoon.com/the-power-to-build-communities-a-response-to-mark-zuckerberg-3f2cac9148a4' } it 'matches the full URL' do is_expected.to include 'href="https://hackernoon.com/the-power-to-build-communities-a-response-to-mark-zuckerberg-3f2cac9148a4"' end end context 'given a stand-alone google URL' do let(:text) { 'http://google.com' } it 'matches the full URL' do is_expected.to include 'href="http://google.com"' end end context 'given a stand-alone IDN URL' do let(:text) { 'https://nic.みんな/' } it 'matches the full URL' do is_expected.to include 'href="https://nic.みんな/"' end it 'has display URL' do is_expected.to include 'nic.みんな/' end end context 'given a URL with a trailing period' do let(:text) { 'http://www.mcmansionhell.com/post/156408871451/50-states-of-mcmansion-hell-scottsdale-arizona. ' } it 'matches the full URL but not the period' do is_expected.to include 'href="http://www.mcmansionhell.com/post/156408871451/50-states-of-mcmansion-hell-scottsdale-arizona"' end end context 'given a URL enclosed with parentheses' do let(:text) { '(http://google.com/)' } it 'matches the full URL but not the parentheses' do is_expected.to include 'href="http://google.com/"' end end context 'given a URL with a trailing exclamation point' do let(:text) { 'http://www.google.com!' } it 'matches the full URL but not the exclamation point' do is_expected.to include 'href="http://www.google.com"' end end context 'given a URL with a trailing single quote' do let(:text) { "http://www.google.com'" } it 'matches the full URL but not the single quote' do is_expected.to include 'href="http://www.google.com"' end end context 'given a URL with a trailing angle bracket' do let(:text) { 'http://www.google.com>' } it 'matches the full URL but not the angle bracket' do is_expected.to include 'href="http://www.google.com"' end end context 'given a URL with a query string' do context 'with escaped unicode character' do let(:text) { 'https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&q=autolink' } it 'matches the full URL' do is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&q=autolink"' end end context 'with unicode character' do let(:text) { 'https://www.ruby-toolbox.com/search?utf8=✓&q=autolink' } it 'matches the full URL' do is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=✓&q=autolink"' end end context 'with unicode character at the end' do let(:text) { 'https://www.ruby-toolbox.com/search?utf8=✓' } it 'matches the full URL' do is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=✓"' end end context 'with escaped and not escaped unicode characters' do let(:text) { 'https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&utf81=✓&q=autolink' } it 'preserves escaped unicode characters' do is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&utf81=✓&q=autolink"' end end end context 'given a URL with parentheses in it' do let(:text) { 'https://en.wikipedia.org/wiki/Diaspora_(software)' } it 'matches the full URL' do is_expected.to include 'href="https://en.wikipedia.org/wiki/Diaspora_(software)"' end end context 'given a URL in quotation marks' do let(:text) { '"https://example.com/"' } it 'does not match the quotation marks' do is_expected.to include 'href="https://example.com/"' end end context 'given a URL in angle brackets' do let(:text) { '' } it 'does not match the angle brackets' do is_expected.to include 'href="https://example.com/"' end end context 'given a URL with Japanese path string' do let(:text) { 'https://ja.wikipedia.org/wiki/日本' } it 'matches the full URL' do is_expected.to include 'href="https://ja.wikipedia.org/wiki/日本"' end end context 'given a URL with Korean path string' do let(:text) { 'https://ko.wikipedia.org/wiki/대한민국' } it 'matches the full URL' do is_expected.to include 'href="https://ko.wikipedia.org/wiki/대한민국"' end end context 'given a URL with a full-width space' do let(:text) { 'https://example.com/ abc123' } it 'does not match the full-width space' do is_expected.to include 'href="https://example.com/"' end end context 'given a URL in Japanese quotation marks' do let(:text) { '「[https://example.org/」' } it 'does not match the quotation marks' do is_expected.to include 'href="https://example.org/"' end end context 'given a URL with Simplified Chinese path string' do let(:text) { 'https://baike.baidu.com/item/中华人民共和国' } it 'matches the full URL' do is_expected.to include 'href="https://baike.baidu.com/item/中华人民共和国"' end end context 'given a URL with Traditional Chinese path string' do let(:text) { 'https://zh.wikipedia.org/wiki/臺灣' } it 'matches the full URL' do is_expected.to include 'href="https://zh.wikipedia.org/wiki/臺灣"' end end context 'given a URL containing unsafe code (XSS attack, visible part)' do let(:text) { %q{http://example.com/bb} } it 'does not include the HTML in the URL' do is_expected.to include '"http://example.com/b"' end it 'escapes the HTML' do is_expected.to include '<del>b</del>' end end context 'given a URL containing unsafe code (XSS attack, invisible part)' do let(:text) { %q{http://example.com/blahblahblahblah/a} } it 'does not include the HTML in the URL' do is_expected.to include '"http://example.com/blahblahblahblah/a"' end it 'escapes the HTML' do is_expected.to include '<script>alert("Hello")</script>' end end context 'given text containing HTML code (script tag)' do let(:text) { '' } it 'escapes the HTML' do is_expected.to include '

<script>alert("Hello")</script>

' end end context 'given text containing HTML (XSS attack)' do let(:text) { %q{} } it 'escapes the HTML' do is_expected.to include '

<img src="javascript:alert('XSS');">

' end end context 'given an invalid URL' do let(:text) { 'http://www\.google\.com' } it 'outputs the raw URL' do is_expected.to eq '

http://www\.google\.com

' end end context 'given text containing a hashtag' do let(:text) { '#hashtag' } it 'creates a hashtag link' do is_expected.to include '/tags/hashtag" class="mention hashtag" rel="tag">#hashtag' end end context 'given text containing a hashtag with Unicode chars' do let(:text) { '#hashtagタグ' } it 'creates a hashtag link' do is_expected.to include '/tags/hashtag%E3%82%BF%E3%82%B0" class="mention hashtag" rel="tag">#hashtagタグ' end end end describe '#format_spoiler' do subject { Formatter.instance.format_spoiler(status) } context 'given a post containing plain text' do let(:status) { Fabricate(:status, text: 'text', spoiler_text: 'Secret!', uri: nil) } it 'Returns the spoiler text' do is_expected.to eq 'Secret!' end end context 'given a post with an emoji shortcode at the start' do let!(:emoji) { Fabricate(:custom_emoji) } let(:status) { Fabricate(:status, text: 'text', spoiler_text: ':coolcat: Secret!', uri: nil) } let(:text) { ':coolcat: Beep boop' } it 'converts the shortcode to an image tag' do is_expected.to match(/:coolcat:@alice Hello world' end end context 'given a post containing plain text' do let(:status) { Fabricate(:status, text: 'text', uri: nil) } it 'paragraphizes the text' do is_expected.to eq '

text

' end end context 'given a post containing line feeds' do let(:status) { Fabricate(:status, text: "line\nfeed", uri: nil) } it 'removes line feeds' do is_expected.not_to include "\n" end end context 'given a post containing linkable mentions' do let(:status) { Fabricate(:status, mentions: [ Fabricate(:mention, account: local_account) ], text: '@alice') } it 'creates a mention link' do is_expected.to include '@alice' end end context 'given a post containing unlinkable mentions' do let(:status) { Fabricate(:status, text: '@alice', uri: nil) } it 'does not create a mention link' do is_expected.to include '@alice' end end context do subject do status = Fabricate(:status, text: text, uri: nil) Formatter.instance.format(status) end include_examples 'encode and link URLs' end context 'given a post with custom_emojify option' do let!(:emoji) { Fabricate(:custom_emoji) } let(:status) { Fabricate(:status, account: local_account, text: text) } subject { Formatter.instance.format(status, custom_emojify: true) } context 'given a post with an emoji shortcode at the start' do let(:text) { ':coolcat: Beep boop' } it 'converts the shortcode to an image tag' do is_expected.to match(/

:coolcat::coolcat: Beep boop
' } it 'converts the shortcode to an image tag' do is_expected.to match(/

:coolcat:Beep :coolcat: boop

' } it 'converts the shortcode to an image tag' do is_expected.to match(/Beep :coolcat::coolcat::coolcat:

' } it 'does not touch the shortcodes' do is_expected.to match(/

:coolcat::coolcat:<\/p>/) end end context 'given a post with an emoji shortcode at the end' do let(:text) { '

Beep boop
:coolcat:

' } it 'converts the shortcode to an image tag' do is_expected.to match(/
:coolcat:alert("Hello")' } it 'strips the scripts' do is_expected.to_not include '' end end context 'given a post containing malicious classes' do let(:text) { 'Show more' } it 'strips the malicious classes' do is_expected.to_not include 'status__content__spoiler-link' end end end describe '#plaintext' do subject { Formatter.instance.plaintext(status) } context 'given a post with local status' do let(:status) { Fabricate(:status, text: '

a text by a nerd who uses an HTML tag in text

', uri: nil) } it 'returns the raw text' do is_expected.to eq '

a text by a nerd who uses an HTML tag in text

' end end context 'given a post with remote status' do let(:status) { Fabricate(:status, account: remote_account, text: '') } it 'returns tag-stripped text' do is_expected.to eq '' end end end describe '#simplified_format' do subject { Formatter.instance.simplified_format(account) } context 'given a post with local status' do let(:account) { Fabricate(:account, domain: nil, note: text) } context 'given a post containing linkable mentions for local accounts' do let(:text) { '@alice' } before { local_account } it 'creates a mention link' do is_expected.to eq '

@alice

' end end context 'given a post containing linkable mentions for remote accounts' do let(:text) { '@bob@remote.test' } before { remote_account } it 'creates a mention link' do is_expected.to eq '

@bob

' end end context 'given a post containing unlinkable mentions' do let(:text) { '@alice' } it 'does not create a mention link' do is_expected.to eq '

@alice

' end end context 'given a post with custom_emojify option' do let!(:emoji) { Fabricate(:custom_emoji) } before { account.note = text } subject { Formatter.instance.simplified_format(account, custom_emojify: true) } context 'given a post with an emoji shortcode at the start' do let(:text) { ':coolcat: Beep boop' } it 'converts the shortcode to an image tag' do is_expected.to match(/

:coolcat:alert("Hello")' } let(:account) { Fabricate(:account, domain: 'remote', note: text) } it 'reformats' do is_expected.to_not include '' end context 'with custom_emojify option' do let!(:emoji) { Fabricate(:custom_emoji, domain: remote_account.domain) } before { remote_account.note = text } subject { Formatter.instance.simplified_format(remote_account, custom_emojify: true) } context 'given a post with an emoji shortcode at the start' do let(:text) { '

:coolcat: Beep boop
' } it 'converts shortcode to image tag' do is_expected.to match(/

:coolcat:Beep :coolcat: boop

' } it 'converts shortcode to image tag' do is_expected.to match(/Beep :coolcat::coolcat::coolcat:

' } it 'does not touch the shortcodes' do is_expected.to match(/

:coolcat::coolcat:<\/p>/) end end context 'given a post with an emoji shortcode at the end' do let(:text) { '

Beep boop
:coolcat:

' } it 'converts shortcode to image tag' do is_expected.to match(/
:coolcat:alert("Hello")' } subject { Formatter.instance.sanitize(html, Sanitize::Config::MASTODON_STRICT) } it 'sanitizes' do is_expected.to eq '' end end end ================================================ FILE: spec/lib/hash_object_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe HashObject do it 'has methods corresponding to hash properties' do expect(HashObject.new(key: 'value').key).to eq 'value' end end ================================================ FILE: spec/lib/language_detector_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe LanguageDetector do describe 'prepare_text' do it 'returns unmodified string without special cases' do string = 'just a regular string' result = described_class.instance.send(:prepare_text, string) expect(result).to eq string end it 'collapses spacing in strings' do string = 'The formatting in this is very odd' result = described_class.instance.send(:prepare_text, string) expect(result).to eq 'The formatting in this is very odd' end it 'strips usernames from strings before detection' do string = '@username Yeah, very surreal...! also @friend' result = described_class.instance.send(:prepare_text, string) expect(result).to eq 'Yeah, very surreal...! also' end it 'strips URLs from strings before detection' do string = 'Our website is https://example.com and also http://localhost.dev' result = described_class.instance.send(:prepare_text, string) expect(result).to eq 'Our website is and also' end it 'strips #hashtags from strings before detection' do string = 'Hey look at all the #animals and #fish' result = described_class.instance.send(:prepare_text, string) expect(result).to eq 'Hey look at all the and' end end describe 'detect' do let(:account_without_user_locale) { Fabricate(:user, locale: nil).account } let(:account_remote) { Fabricate(:account, domain: 'joinmastodon.org') } it 'detects english language for basic strings' do strings = [ "Hello and welcome to mastodon how are you today?", "I'd rather not!", "a lot of people just want to feel righteous all the time and that's all that matters", ] strings.each do |string| result = described_class.instance.detect(string, account_without_user_locale) expect(result).to eq(:en), string end end it 'detects spanish language' do string = 'Obtener un Hola y bienvenidos a Mastodon. Obtener un Hola y bienvenidos a Mastodon. Obtener un Hola y bienvenidos a Mastodon. Obtener un Hola y bienvenidos a Mastodon' result = described_class.instance.detect(string, account_without_user_locale) expect(result).to eq :es end describe 'when language can\'t be detected' do it 'uses nil when sent an empty document' do result = described_class.instance.detect('', account_without_user_locale) expect(result).to eq nil end describe 'because of a URL' do it 'uses nil when sent just a URL' do string = 'http://example.com/media/2kFTgOJLXhQf0g2nKB4' cld_result = CLD3::NNetLanguageIdentifier.new(0, 2048).find_language(string) expect(cld_result).not_to eq :en result = described_class.instance.detect(string, account_without_user_locale) expect(result).to eq nil end end describe 'with an account' do it 'uses the account locale when present' do account = double(user_locale: 'fr') result = described_class.instance.detect('', account) expect(result).to eq nil end it 'uses nil when account is present but has no locale' do result = described_class.instance.detect('', account_without_user_locale) expect(result).to eq nil end end describe 'with an `en` default locale' do it 'uses nil for undetectable string' do result = described_class.instance.detect('', account_without_user_locale) expect(result).to eq nil end end describe 'remote user' do it 'detects Korean language' do string = '안녕하세요' result = described_class.instance.detect(string, account_remote) expect(result).to eq :ko end end describe 'with a non-`en` default locale' do around(:each) do |example| before = I18n.default_locale I18n.default_locale = :ja example.run I18n.default_locale = before end it 'uses nil for undetectable string' do string = '' result = described_class.instance.detect(string, account_without_user_locale) expect(result).to eq nil end end end end end ================================================ FILE: spec/lib/ostatus/atom_serializer_spec.rb ================================================ require 'rails_helper' RSpec.describe OStatus::AtomSerializer do shared_examples 'follow request salmon' do it 'appends author element with account' do account = Fabricate(:account, domain: nil, username: 'username') follow_request = Fabricate(:follow_request, account: account) follow_request_salmon = serialize(follow_request) expect(follow_request_salmon.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/username' end it 'appends activity:object-type element with activity type' do follow_request = Fabricate(:follow_request) follow_request_salmon = serialize(follow_request) object_type = follow_request_salmon.nodes.find { |node| node.name == 'activity:object-type' } expect(object_type.text).to eq OStatus::TagManager::TYPES[:activity] end it 'appends activity:verb element with request_friend type' do follow_request = Fabricate(:follow_request) follow_request_salmon = serialize(follow_request) verb = follow_request_salmon.nodes.find { |node| node.name == 'activity:verb' } expect(verb.text).to eq OStatus::TagManager::VERBS[:request_friend] end it 'appends activity:object with target account' do target_account = Fabricate(:account, domain: 'domain.test', uri: 'https://domain.test/id') follow_request = Fabricate(:follow_request, target_account: target_account) follow_request_salmon = serialize(follow_request) object = follow_request_salmon.nodes.find { |node| node.name == 'activity:object' } expect(object.id.text).to eq 'https://domain.test/id' end end shared_examples 'namespaces' do it 'adds namespaces' do element = serialize expect(element['xmlns']).to eq OStatus::TagManager::XMLNS expect(element['xmlns:thr']).to eq OStatus::TagManager::THR_XMLNS expect(element['xmlns:activity']).to eq OStatus::TagManager::AS_XMLNS expect(element['xmlns:poco']).to eq OStatus::TagManager::POCO_XMLNS expect(element['xmlns:media']).to eq OStatus::TagManager::MEDIA_XMLNS expect(element['xmlns:ostatus']).to eq OStatus::TagManager::OS_XMLNS expect(element['xmlns:mastodon']).to eq OStatus::TagManager::MTDN_XMLNS end end shared_examples 'no namespaces' do it 'does not add namespaces' do expect(serialize['xmlns']).to eq nil end end shared_examples 'status attributes' do it 'appends summary element with spoiler text if present' do status = Fabricate(:status, language: :ca, spoiler_text: 'spoiler text') element = serialize(status) summary = element.summary expect(summary['xml:lang']).to eq 'ca' expect(summary.text).to eq 'spoiler text' end it 'does not append summary element with spoiler text if not present' do status = Fabricate(:status, spoiler_text: '') element = serialize(status) element.nodes.each { |node| expect(node.name).not_to eq 'summary' } end it 'appends content element with formatted status' do status = Fabricate(:status, language: :ca, text: 'text') element = serialize(status) content = element.content expect(content[:type]).to eq 'html' expect(content['xml:lang']).to eq 'ca' expect(content.text).to eq '

text

' end it 'appends link elements for mentioned accounts' do account = Fabricate(:account, username: 'username') status = Fabricate(:status) Fabricate(:mention, account: account, status: status) element = serialize(status) mentioned = element.nodes.find do |node| node.name == 'link' && node[:rel] == 'mentioned' && node['ostatus:object-type'] == OStatus::TagManager::TYPES[:person] end expect(mentioned[:href]).to eq 'https://cb6e6126.ngrok.io/users/username' end it 'appends link elements for emojis' do Fabricate(:custom_emoji) status = Fabricate(:status, text: ':coolcat:') element = serialize(status) emoji = element.nodes.find { |node| node.name == 'link' && node[:rel] == 'emoji' } expect(emoji[:name]).to eq 'coolcat' expect(emoji[:href]).to_not be_blank end end describe 'render' do it 'returns XML with emojis' do element = Ox::Element.new('tag') element << '💩' xml = OStatus::AtomSerializer.render(element) expect(xml).to eq "\n💩\n" end it 'returns XML, stripping invalid characters like \b and \v' do element = Ox::Element.new('tag') element << "im l33t\b haxo\b\vr" xml = OStatus::AtomSerializer.render(element) expect(xml).to eq "\nim l33t haxor\n" end end describe '#author' do context 'when note is present' do it 'appends poco:note element with note for local account' do account = Fabricate(:account, domain: nil, note: '

note

') author = OStatus::AtomSerializer.new.author(account) note = author.nodes.find { |node| node.name == 'poco:note' } expect(note.text).to eq '

note

' end it 'appends poco:note element with tags-stripped note for remote account' do account = Fabricate(:account, domain: 'remote', note: '

note

') author = OStatus::AtomSerializer.new.author(account) note = author.nodes.find { |node| node.name == 'poco:note' } expect(note.text).to eq 'note' end it 'appends summary element with type attribute and simplified note if present' do account = Fabricate(:account, note: 'note') author = OStatus::AtomSerializer.new.author(account) expect(author.summary.text).to eq '

note

' expect(author.summary[:type]).to eq 'html' end end context 'when note is not present' do it 'does not append poco:note element' do account = Fabricate(:account, note: '') author = OStatus::AtomSerializer.new.author(account) author.nodes.each { |node| expect(node.name).not_to eq 'poco:note' } end it 'does not append summary element' do account = Fabricate(:account, note: '') author = OStatus::AtomSerializer.new.author(account) author.nodes.each { |node| expect(node.name).not_to eq 'summary' } end end it 'returns author element' do account = Fabricate(:account) author = OStatus::AtomSerializer.new.author(account) expect(author.name).to eq 'author' end it 'appends activity:object-type element with person type' do account = Fabricate(:account, domain: nil, username: 'username') author = OStatus::AtomSerializer.new.author(account) object_type = author.nodes.find { |node| node.name == 'activity:object-type' } expect(object_type.text).to eq OStatus::TagManager::TYPES[:person] end it 'appends email element with username and domain for local account' do account = Fabricate(:account, username: 'username') author = OStatus::AtomSerializer.new.author(account) expect(author.email.text).to eq 'username@cb6e6126.ngrok.io' end it 'appends email element with username and domain for remote user' do account = Fabricate(:account, domain: 'domain', username: 'username') author = OStatus::AtomSerializer.new.author(account) expect(author.email.text).to eq 'username@domain' end it 'appends link element for an alternative' do account = Fabricate(:account, domain: nil, username: 'username') author = OStatus::AtomSerializer.new.author(account) link = author.nodes.find { |node| node.name == 'link' && node[:rel] == 'alternate' && node[:type] == 'text/html' } expect(link[:type]).to eq 'text/html' expect(link[:rel]).to eq 'alternate' expect(link[:href]).to eq 'https://cb6e6126.ngrok.io/@username' end it 'has link element for avatar if present' do account = Fabricate(:account, avatar: attachment_fixture('avatar.gif')) author = OStatus::AtomSerializer.new.author(account) link = author.nodes.find { |node| node.name == 'link' && node[:rel] == 'avatar' } expect(link[:type]).to eq 'image/gif' expect(link['media:width']).to eq '120' expect(link['media:height']).to eq '120' expect(link[:href]).to match /^https:\/\/cb6e6126.ngrok.io\/system\/accounts\/avatars\/.+\/original\/avatar.gif/ end it 'does not have link element for avatar if not present' do account = Fabricate(:account, avatar: nil) author = OStatus::AtomSerializer.new.author(account) author.nodes.each do |node| expect(node[:rel]).not_to eq 'avatar' if node.name == 'link' end end it 'appends link element for header if present' do account = Fabricate(:account, header: attachment_fixture('avatar.gif')) author = OStatus::AtomSerializer.new.author(account) link = author.nodes.find { |node| node.name == 'link' && node[:rel] == 'header' } expect(link[:type]).to eq 'image/gif' expect(link['media:width']).to eq '700' expect(link['media:height']).to eq '335' expect(link[:href]).to match /^https:\/\/cb6e6126.ngrok.io\/system\/accounts\/headers\/.+\/original\/avatar.gif/ end it 'does not append link element for header if not present' do account = Fabricate(:account, header: nil) author = OStatus::AtomSerializer.new.author(account) author.nodes.each do |node| expect(node[:rel]).not_to eq 'header' if node.name == 'link' end end it 'appends poco:displayName element with display name if present' do account = Fabricate(:account, display_name: 'display name') author = OStatus::AtomSerializer.new.author(account) display_name = author.nodes.find { |node| node.name == 'poco:displayName' } expect(display_name.text).to eq 'display name' end it 'does not append poco:displayName element with display name if not present' do account = Fabricate(:account, display_name: '') author = OStatus::AtomSerializer.new.author(account) author.nodes.each { |node| expect(node.name).not_to eq 'poco:displayName' } end it "appends mastodon:scope element with 'private' if locked" do account = Fabricate(:account, locked: true) author = OStatus::AtomSerializer.new.author(account) scope = author.nodes.find { |node| node.name == 'mastodon:scope' } expect(scope.text).to eq 'private' end it "appends mastodon:scope element with 'public' if unlocked" do account = Fabricate(:account, locked: false) author = OStatus::AtomSerializer.new.author(account) scope = author.nodes.find { |node| node.name == 'mastodon:scope' } expect(scope.text).to eq 'public' end it 'includes URI' do account = Fabricate(:account, domain: nil, username: 'username') author = OStatus::AtomSerializer.new.author(account) expect(author.id.text).to eq 'https://cb6e6126.ngrok.io/users/username' expect(author.uri.text).to eq 'https://cb6e6126.ngrok.io/users/username' end it 'includes username' do account = Fabricate(:account, username: 'username') author = OStatus::AtomSerializer.new.author(account) name = author.nodes.find { |node| node.name == 'name' } username = author.nodes.find { |node| node.name == 'poco:preferredUsername' } expect(name.text).to eq 'username' expect(username.text).to eq 'username' end end describe '#entry' do shared_examples 'not root' do include_examples 'no namespaces' do def serialize subject end end it 'does not append author element' do subject.nodes.each { |node| expect(node.name).not_to eq 'author' } end end context 'it is root' do include_examples 'namespaces' do def serialize stream_entry = Fabricate(:stream_entry) OStatus::AtomSerializer.new.entry(stream_entry, true) end end it 'appends author element' do account = Fabricate(:account, username: 'username') status = Fabricate(:status, account: account) entry = OStatus::AtomSerializer.new.entry(status.stream_entry, true) expect(entry.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/username' end end context 'if status is present' do include_examples 'status attributes' do def serialize(status) OStatus::AtomSerializer.new.entry(status.stream_entry, true) end end it 'appends link element for the public collection if status is publicly visible' do status = Fabricate(:status, visibility: :public) entry = OStatus::AtomSerializer.new.entry(status.stream_entry) mentioned_person = entry.nodes.find do |node| node.name == 'link' && node[:rel] == 'mentioned' && node['ostatus:object-type'] == OStatus::TagManager::TYPES[:collection] end expect(mentioned_person[:href]).to eq OStatus::TagManager::COLLECTIONS[:public] end it 'does not append link element for the public collection if status is not publicly visible' do status = Fabricate(:status, visibility: :private) entry = OStatus::AtomSerializer.new.entry(status.stream_entry) entry.nodes.each do |node| if node.name == 'link' && node[:rel] == 'mentioned' && node['ostatus:object-type'] == OStatus::TagManager::TYPES[:collection] expect(mentioned_collection[:href]).not_to eq OStatus::TagManager::COLLECTIONS[:public] end end end it 'appends category elements for tags' do tag = Fabricate(:tag, name: 'tag') status = Fabricate(:status, tags: [ tag ]) entry = OStatus::AtomSerializer.new.entry(status.stream_entry) expect(entry.category[:term]).to eq 'tag' end it 'appends link elements for media attachments' do file = attachment_fixture('attachment.jpg') media_attachment = Fabricate(:media_attachment, file: file) status = Fabricate(:status, media_attachments: [ media_attachment ]) entry = OStatus::AtomSerializer.new.entry(status.stream_entry) enclosure = entry.nodes.find { |node| node.name == 'link' && node[:rel] == 'enclosure' } expect(enclosure[:type]).to eq 'image/jpeg' expect(enclosure[:href]).to match /^https:\/\/cb6e6126.ngrok.io\/system\/media_attachments\/files\/.+\/original\/attachment.jpg$/ end it 'appends mastodon:scope element with visibility' do status = Fabricate(:status, visibility: :public) entry = OStatus::AtomSerializer.new.entry(status.stream_entry) scope = entry.nodes.find { |node| node.name == 'mastodon:scope' } expect(scope.text).to eq 'public' end it 'returns element whose rendered view triggers creation when processed' do remote_account = Account.create!(username: 'username') remote_status = Fabricate(:status, account: remote_account, created_at: '2000-01-01T00:00:00Z') entry = OStatus::AtomSerializer.new.entry(remote_status.stream_entry, true) entry.nodes.delete_if { |node| node[:type] == 'application/activity+json' } # Remove ActivityPub link to simplify test xml = OStatus::AtomSerializer.render(entry).gsub('cb6e6126.ngrok.io', 'remote.test') remote_status.destroy! remote_account.destroy! account = Account.create!( domain: 'remote.test', username: 'username', last_webfingered_at: Time.now.utc ) ProcessFeedService.new.call(xml, account) expect(Status.find_by(uri: "https://remote.test/users/#{remote_status.account.to_param}/statuses/#{remote_status.id}")).to be_instance_of Status end end context 'if status is not present' do it 'appends content element saying status is deleted' do status = Fabricate(:status) status.destroy! entry = OStatus::AtomSerializer.new.entry(status.stream_entry) expect(entry.content.text).to eq 'Deleted status' end it 'appends title element saying the status is deleted' do account = Fabricate(:account, username: 'username') status = Fabricate(:status, account: account) status.destroy! entry = OStatus::AtomSerializer.new.entry(status.stream_entry) expect(entry.title.text).to eq 'username deleted status' end end context 'it is not root' do let(:stream_entry) { Fabricate(:stream_entry) } subject { OStatus::AtomSerializer.new.entry(stream_entry, false) } include_examples 'not root' end context 'without root parameter' do let(:stream_entry) { Fabricate(:stream_entry) } subject { OStatus::AtomSerializer.new.entry(stream_entry) } include_examples 'not root' end it 'returns entry element' do stream_entry = Fabricate(:stream_entry) entry = OStatus::AtomSerializer.new.entry(stream_entry) expect(entry.name).to eq 'entry' end it 'appends id element with unique tag' do status = Fabricate(:status, reblog_of_id: nil, created_at: '2000-01-01T00:00:00Z') entry = OStatus::AtomSerializer.new.entry(status.stream_entry) expect(entry.id.text).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}" end it 'appends published element with created date' do stream_entry = Fabricate(:stream_entry, created_at: '2000-01-01T00:00:00Z') entry = OStatus::AtomSerializer.new.entry(stream_entry) expect(entry.published.text).to eq '2000-01-01T00:00:00Z' end it 'appends updated element with updated date' do stream_entry = Fabricate(:stream_entry, updated_at: '2000-01-01T00:00:00Z') entry = OStatus::AtomSerializer.new.entry(stream_entry) expect(entry.updated.text).to eq '2000-01-01T00:00:00Z' end it 'appends title element with status title' do account = Fabricate(:account, username: 'username') status = Fabricate(:status, account: account, reblog_of_id: nil) entry = OStatus::AtomSerializer.new.entry(status.stream_entry) expect(entry.title.text).to eq 'New status by username' end it 'appends activity:object-type element with object type' do status = Fabricate(:status) entry = OStatus::AtomSerializer.new.entry(status.stream_entry) object_type = entry.nodes.find { |node| node.name == 'activity:object-type' } expect(object_type.text).to eq OStatus::TagManager::TYPES[:note] end it 'appends activity:verb element with object type' do status = Fabricate(:status) entry = OStatus::AtomSerializer.new.entry(status.stream_entry) object_type = entry.nodes.find { |node| node.name == 'activity:verb' } expect(object_type.text).to eq OStatus::TagManager::VERBS[:post] end it 'appends activity:object element with target if present' do reblogged = Fabricate(:status, created_at: '2000-01-01T00:00:00Z') reblog = Fabricate(:status, reblog: reblogged) entry = OStatus::AtomSerializer.new.entry(reblog.stream_entry) object = entry.nodes.find { |node| node.name == 'activity:object' } expect(object.id.text).to eq "https://cb6e6126.ngrok.io/users/#{reblogged.account.to_param}/statuses/#{reblogged.id}" end it 'does not append activity:object element if target is not present' do status = Fabricate(:status, reblog_of_id: nil) entry = OStatus::AtomSerializer.new.entry(status.stream_entry) entry.nodes.each { |node| expect(node.name).not_to eq 'activity:object' } end it 'appends link element for an alternative' do account = Fabricate(:account, username: 'username') status = Fabricate(:status, account: account) entry = OStatus::AtomSerializer.new.entry(status.stream_entry) link = entry.nodes.find { |node| node.name == 'link' && node[:rel] == 'alternate' && node[:type] == 'text/html' } expect(link[:type]).to eq 'text/html' expect(link[:href]).to eq "https://cb6e6126.ngrok.io/@username/#{status.id}" end it 'appends link element for itself' do account = Fabricate(:account, username: 'username') status = Fabricate(:status, account: account) entry = OStatus::AtomSerializer.new.entry(status.stream_entry) link = entry.nodes.find { |node| node.name == 'link' && node[:rel] == 'self' } expect(link[:type]).to eq 'application/atom+xml' expect(link[:href]).to eq "https://cb6e6126.ngrok.io/users/username/updates/#{status.stream_entry.id}.atom" end it 'appends thr:in-reply-to element if threaded' do in_reply_to_status = Fabricate(:status, created_at: '2000-01-01T00:00:00Z', reblog_of_id: nil) reply_status = Fabricate(:status, in_reply_to_id: in_reply_to_status.id) entry = OStatus::AtomSerializer.new.entry(reply_status.stream_entry) in_reply_to = entry.nodes.find { |node| node.name == 'thr:in-reply-to' } expect(in_reply_to[:ref]).to eq "https://cb6e6126.ngrok.io/users/#{in_reply_to_status.account.to_param}/statuses/#{in_reply_to_status.id}" end it 'does not append thr:in-reply-to element if not threaded' do status = Fabricate(:status) entry = OStatus::AtomSerializer.new.entry(status.stream_entry) entry.nodes.each { |node| expect(node.name).not_to eq 'thr:in-reply-to' } end it 'appends ostatus:conversation if conversation id is present' do status = Fabricate(:status) status.conversation.update!(created_at: '2000-01-01T00:00:00Z') entry = OStatus::AtomSerializer.new.entry(status.stream_entry) conversation = entry.nodes.find { |node| node.name == 'ostatus:conversation' } expect(conversation[:ref]).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{status.conversation_id}:objectType=Conversation" end it 'does not append ostatus:conversation if conversation id is not present' do status = Fabricate.build(:status, conversation_id: nil) status.save!(validate: false) entry = OStatus::AtomSerializer.new.entry(status.stream_entry) entry.nodes.each { |node| expect(node.name).not_to eq 'ostatus:conversation' } end end describe '#feed' do include_examples 'namespaces' do def serialize account = Fabricate(:account) OStatus::AtomSerializer.new.feed(account, []) end end it 'returns feed element' do account = Fabricate(:account) feed = OStatus::AtomSerializer.new.feed(account, []) expect(feed.name).to eq 'feed' end it 'appends id element with account Atom URL' do account = Fabricate(:account, username: 'username') feed = OStatus::AtomSerializer.new.feed(account, []) expect(feed.id.text).to eq 'https://cb6e6126.ngrok.io/users/username.atom' end it 'appends title element with account display name if present' do account = Fabricate(:account, display_name: 'display name') feed = OStatus::AtomSerializer.new.feed(account, []) expect(feed.title.text).to eq 'display name' end it 'does not append title element with account username if account display name is not present' do account = Fabricate(:account, display_name: '', username: 'username') feed = OStatus::AtomSerializer.new.feed(account, []) expect(feed.title.text).to eq 'username' end it 'appends subtitle element with account note' do account = Fabricate(:account, note: 'note') feed = OStatus::AtomSerializer.new.feed(account, []) expect(feed.subtitle.text).to eq 'note' end it 'appends updated element with date account got updated' do account = Fabricate(:account, updated_at: '2000-01-01T00:00:00Z') feed = OStatus::AtomSerializer.new.feed(account, []) expect(feed.updated.text).to eq '2000-01-01T00:00:00Z' end it 'appends logo element with full asset URL for original account avatar' do account = Fabricate(:account, avatar: attachment_fixture('avatar.gif')) feed = OStatus::AtomSerializer.new.feed(account, []) expect(feed.logo.text).to match /^https:\/\/cb6e6126.ngrok.io\/system\/accounts\/avatars\/.+\/original\/avatar.gif/ end it 'appends author element' do account = Fabricate(:account, username: 'username') feed = OStatus::AtomSerializer.new.feed(account, []) expect(feed.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/username' end it 'appends link element for an alternative' do account = Fabricate(:account, username: 'username') feed = OStatus::AtomSerializer.new.feed(account, []) link = feed.nodes.find { |node| node.name == 'link' && node[:rel] == 'alternate' && node[:type] == 'text/html' } expect(link[:type]).to eq 'text/html' expect(link[:href]).to eq 'https://cb6e6126.ngrok.io/@username' end it 'appends link element for itself' do account = Fabricate(:account, username: 'username') feed = OStatus::AtomSerializer.new.feed(account, []) link = feed.nodes.find { |node| node.name == 'link' && node[:rel] == 'self' } expect(link[:type]).to eq 'application/atom+xml' expect(link[:href]).to eq 'https://cb6e6126.ngrok.io/users/username.atom' end it 'appends link element for the next if it has 20 stream entries' do account = Fabricate(:account, username: 'username') stream_entry = Fabricate(:stream_entry) feed = OStatus::AtomSerializer.new.feed(account, Array.new(20, stream_entry)) link = feed.nodes.find { |node| node.name == 'link' && node[:rel] == 'next' } expect(link[:type]).to eq 'application/atom+xml' expect(link[:href]).to eq "https://cb6e6126.ngrok.io/users/username.atom?max_id=#{stream_entry.id}" end it 'does not append link element for the next if it does not have 20 stream entries' do account = Fabricate(:account, username: 'username') feed = OStatus::AtomSerializer.new.feed(account, []) feed.nodes.each do |node| expect(node[:rel]).not_to eq 'next' if node.name == 'link' end end it 'appends link element for hub' do account = Fabricate(:account, username: 'username') feed = OStatus::AtomSerializer.new.feed(account, []) link = feed.nodes.find { |node| node.name == 'link' && node[:rel] == 'hub' } expect(link[:href]).to eq 'https://cb6e6126.ngrok.io/api/push' end it 'appends link element for Salmon' do account = Fabricate(:account, username: 'username') feed = OStatus::AtomSerializer.new.feed(account, []) link = feed.nodes.find { |node| node.name == 'link' && node[:rel] == 'salmon' } expect(link[:href]).to start_with 'https://cb6e6126.ngrok.io/api/salmon/' end it 'appends stream entries' do account = Fabricate(:account, username: 'username') status = Fabricate(:status, account: account) feed = OStatus::AtomSerializer.new.feed(account, [status.stream_entry]) expect(feed.entry.title.text).to eq 'New status by username' end end describe '#block_salmon' do include_examples 'namespaces' do def serialize block = Fabricate(:block) OStatus::AtomSerializer.new.block_salmon(block) end end it 'returns entry element' do block = Fabricate(:block) block_salmon = OStatus::AtomSerializer.new.block_salmon(block) expect(block_salmon.name).to eq 'entry' end it 'appends id element with unique tag' do block = Fabricate(:block) time_before = Time.zone.now block_salmon = OStatus::AtomSerializer.new.block_salmon(block) time_after = Time.zone.now expect(block_salmon.id.text).to( eq(OStatus::TagManager.instance.unique_tag(time_before.utc, block.id, 'Block')) .or(eq(OStatus::TagManager.instance.unique_tag(time_after.utc, block.id, 'Block'))) ) end it 'appends title element with description' do account = Fabricate(:account, domain: nil, username: 'account') target_account = Fabricate(:account, domain: 'remote', username: 'target_account') block = Fabricate(:block, account: account, target_account: target_account) block_salmon = OStatus::AtomSerializer.new.block_salmon(block) expect(block_salmon.title.text).to eq 'account no longer wishes to interact with target_account@remote' end it 'appends author element with account' do account = Fabricate(:account, domain: nil, username: 'account') block = Fabricate(:block, account: account) block_salmon = OStatus::AtomSerializer.new.block_salmon(block) expect(block_salmon.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/account' end it 'appends activity:object-type element with activity type' do block = Fabricate(:block) block_salmon = OStatus::AtomSerializer.new.block_salmon(block) object_type = block_salmon.nodes.find { |node| node.name == 'activity:object-type' } expect(object_type.text).to eq OStatus::TagManager::TYPES[:activity] end it 'appends activity:verb element with block' do block = Fabricate(:block) block_salmon = OStatus::AtomSerializer.new.block_salmon(block) verb = block_salmon.nodes.find { |node| node.name == 'activity:verb' } expect(verb.text).to eq OStatus::TagManager::VERBS[:block] end it 'appends activity:object element with target account' do target_account = Fabricate(:account, domain: 'domain.test', uri: 'https://domain.test/id') block = Fabricate(:block, target_account: target_account) block_salmon = OStatus::AtomSerializer.new.block_salmon(block) object = block_salmon.nodes.find { |node| node.name == 'activity:object' } expect(object.id.text).to eq 'https://domain.test/id' end it 'returns element whose rendered view triggers block when processed' do block = Fabricate(:block) block_salmon = OStatus::AtomSerializer.new.block_salmon(block) xml = OStatus::AtomSerializer.render(block_salmon) envelope = OStatus2::Salmon.new.pack(xml, block.account.keypair) block.destroy! ProcessInteractionService.new.call(envelope, block.target_account) expect(block.account.blocking?(block.target_account)).to be true end end describe '#unblock_salmon' do include_examples 'namespaces' do def serialize block = Fabricate(:block) OStatus::AtomSerializer.new.unblock_salmon(block) end end it 'returns entry element' do block = Fabricate(:block) unblock_salmon = OStatus::AtomSerializer.new.unblock_salmon(block) expect(unblock_salmon.name).to eq 'entry' end it 'appends id element with unique tag' do block = Fabricate(:block) time_before = Time.zone.now unblock_salmon = OStatus::AtomSerializer.new.unblock_salmon(block) time_after = Time.zone.now expect(unblock_salmon.id.text).to( eq(OStatus::TagManager.instance.unique_tag(time_before.utc, block.id, 'Block')) .or(eq(OStatus::TagManager.instance.unique_tag(time_after.utc, block.id, 'Block'))) ) end it 'appends title element with description' do account = Fabricate(:account, domain: nil, username: 'account') target_account = Fabricate(:account, domain: 'remote', username: 'target_account') block = Fabricate(:block, account: account, target_account: target_account) unblock_salmon = OStatus::AtomSerializer.new.unblock_salmon(block) expect(unblock_salmon.title.text).to eq 'account no longer blocks target_account@remote' end it 'appends author element with account' do account = Fabricate(:account, domain: nil, username: 'account') block = Fabricate(:block, account: account) unblock_salmon = OStatus::AtomSerializer.new.unblock_salmon(block) expect(unblock_salmon.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/account' end it 'appends activity:object-type element with activity type' do block = Fabricate(:block) unblock_salmon = OStatus::AtomSerializer.new.unblock_salmon(block) object_type = unblock_salmon.nodes.find { |node| node.name == 'activity:object-type' } expect(object_type.text).to eq OStatus::TagManager::TYPES[:activity] end it 'appends activity:verb element with block' do block = Fabricate(:block) unblock_salmon = OStatus::AtomSerializer.new.unblock_salmon(block) verb = unblock_salmon.nodes.find { |node| node.name == 'activity:verb' } expect(verb.text).to eq OStatus::TagManager::VERBS[:unblock] end it 'appends activity:object element with target account' do target_account = Fabricate(:account, domain: 'domain.test', uri: 'https://domain.test/id') block = Fabricate(:block, target_account: target_account) unblock_salmon = OStatus::AtomSerializer.new.unblock_salmon(block) object = unblock_salmon.nodes.find { |node| node.name == 'activity:object' } expect(object.id.text).to eq 'https://domain.test/id' end it 'returns element whose rendered view triggers block when processed' do block = Fabricate(:block) unblock_salmon = OStatus::AtomSerializer.new.unblock_salmon(block) xml = OStatus::AtomSerializer.render(unblock_salmon) envelope = OStatus2::Salmon.new.pack(xml, block.account.keypair) ProcessInteractionService.new.call(envelope, block.target_account) expect { block.reload }.to raise_error ActiveRecord::RecordNotFound end end describe '#favourite_salmon' do include_examples 'namespaces' do def serialize favourite = Fabricate(:favourite) OStatus::AtomSerializer.new.favourite_salmon(favourite) end end it 'returns entry element' do favourite = Fabricate(:favourite) favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite) expect(favourite_salmon.name).to eq 'entry' end it 'appends id element with unique tag' do favourite = Fabricate(:favourite, created_at: '2000-01-01T00:00:00Z') favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite) expect(favourite_salmon.id.text).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{favourite.id}:objectType=Favourite" end it 'appends author element with account' do account = Fabricate(:account, domain: nil, username: 'username') favourite = Fabricate(:favourite, account: account) favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite) expect(favourite_salmon.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/username' end it 'appends activity:object-type element with activity type' do favourite = Fabricate(:favourite) favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite) object_type = favourite_salmon.nodes.find { |node| node.name == 'activity:object-type' } expect(object_type.text).to eq 'http://activitystrea.ms/schema/1.0/activity' end it 'appends activity:verb element with favorite' do favourite = Fabricate(:favourite) favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite) verb = favourite_salmon.nodes.find { |node| node.name == 'activity:verb' } expect(verb.text).to eq OStatus::TagManager::VERBS[:favorite] end it 'appends activity:object element with status' do status = Fabricate(:status, created_at: '2000-01-01T00:00:00Z') favourite = Fabricate(:favourite, status: status) favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite) object = favourite_salmon.nodes.find { |node| node.name == 'activity:object' } expect(object.id.text).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}" end it 'appends thr:in-reply-to element for status' do status_account = Fabricate(:account, username: 'username') status = Fabricate(:status, account: status_account, created_at: '2000-01-01T00:00:00Z') favourite = Fabricate(:favourite, status: status) favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite) in_reply_to = favourite_salmon.nodes.find { |node| node.name == 'thr:in-reply-to' } expect(in_reply_to.ref).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}" expect(in_reply_to.href).to eq "https://cb6e6126.ngrok.io/@username/#{status.id}" end it 'includes description' do account = Fabricate(:account, domain: nil, username: 'account') status_account = Fabricate(:account, domain: 'remote', username: 'status_account') status = Fabricate(:status, account: status_account) favourite = Fabricate(:favourite, account: account, status: status) favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite) expect(favourite_salmon.title.text).to eq 'account favourited a status by status_account@remote' expect(favourite_salmon.content.text).to eq 'account favourited a status by status_account@remote' end it 'returns element whose rendered view triggers favourite when processed' do favourite = Fabricate(:favourite) favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite) xml = OStatus::AtomSerializer.render(favourite_salmon) envelope = OStatus2::Salmon.new.pack(xml, favourite.account.keypair) favourite.destroy! ProcessInteractionService.new.call(envelope, favourite.status.account) expect(favourite.account.favourited?(favourite.status)).to be true end end describe '#unfavourite_salmon' do include_examples 'namespaces' do def serialize favourite = Fabricate(:favourite) OStatus::AtomSerializer.new.favourite_salmon(favourite) end end it 'returns entry element' do favourite = Fabricate(:favourite) unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite) expect(unfavourite_salmon.name).to eq 'entry' end it 'appends id element with unique tag' do favourite = Fabricate(:favourite) time_before = Time.zone.now unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite) time_after = Time.zone.now expect(unfavourite_salmon.id.text).to( eq(OStatus::TagManager.instance.unique_tag(time_before.utc, favourite.id, 'Favourite')) .or(eq(OStatus::TagManager.instance.unique_tag(time_after.utc, favourite.id, 'Favourite'))) ) end it 'appends author element with account' do account = Fabricate(:account, domain: nil, username: 'username') favourite = Fabricate(:favourite, account: account) unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite) expect(unfavourite_salmon.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/username' end it 'appends activity:object-type element with activity type' do favourite = Fabricate(:favourite) unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite) object_type = unfavourite_salmon.nodes.find { |node| node.name == 'activity:object-type' } expect(object_type.text).to eq 'http://activitystrea.ms/schema/1.0/activity' end it 'appends activity:verb element with favorite' do favourite = Fabricate(:favourite) unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite) verb = unfavourite_salmon.nodes.find { |node| node.name == 'activity:verb' } expect(verb.text).to eq OStatus::TagManager::VERBS[:unfavorite] end it 'appends activity:object element with status' do status = Fabricate(:status, created_at: '2000-01-01T00:00:00Z') favourite = Fabricate(:favourite, status: status) unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite) object = unfavourite_salmon.nodes.find { |node| node.name == 'activity:object' } expect(object.id.text).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}" end it 'appends thr:in-reply-to element for status' do status_account = Fabricate(:account, username: 'username') status = Fabricate(:status, account: status_account, created_at: '2000-01-01T00:00:00Z') favourite = Fabricate(:favourite, status: status) unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite) in_reply_to = unfavourite_salmon.nodes.find { |node| node.name == 'thr:in-reply-to' } expect(in_reply_to.ref).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}" expect(in_reply_to.href).to eq "https://cb6e6126.ngrok.io/@username/#{status.id}" end it 'includes description' do account = Fabricate(:account, domain: nil, username: 'account') status_account = Fabricate(:account, domain: 'remote', username: 'status_account') status = Fabricate(:status, account: status_account) favourite = Fabricate(:favourite, account: account, status: status) unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite) expect(unfavourite_salmon.title.text).to eq 'account no longer favourites a status by status_account@remote' expect(unfavourite_salmon.content.text).to eq 'account no longer favourites a status by status_account@remote' end it 'returns element whose rendered view triggers unfavourite when processed' do favourite = Fabricate(:favourite) unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite) xml = OStatus::AtomSerializer.render(unfavourite_salmon) envelope = OStatus2::Salmon.new.pack(xml, favourite.account.keypair) ProcessInteractionService.new.call(envelope, favourite.status.account) expect { favourite.reload }.to raise_error ActiveRecord::RecordNotFound end end describe '#follow_salmon' do include_examples 'namespaces' do def serialize follow = Fabricate(:follow) OStatus::AtomSerializer.new.follow_salmon(follow) end end it 'returns entry element' do follow = Fabricate(:follow) follow_salmon = OStatus::AtomSerializer.new.follow_salmon(follow) expect(follow_salmon.name).to eq 'entry' end it 'appends id element with unique tag' do follow = Fabricate(:follow, created_at: '2000-01-01T00:00:00Z') follow_salmon = OStatus::AtomSerializer.new.follow_salmon(follow) expect(follow_salmon.id.text).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{follow.id}:objectType=Follow" end it 'appends author element with account' do account = Fabricate(:account, domain: nil, username: 'username') follow = Fabricate(:follow, account: account) follow_salmon = OStatus::AtomSerializer.new.follow_salmon(follow) expect(follow_salmon.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/username' end it 'appends activity:object-type element with activity type' do follow = Fabricate(:follow) follow_salmon = OStatus::AtomSerializer.new.follow_salmon(follow) object_type = follow_salmon.nodes.find { |node| node.name == 'activity:object-type' } expect(object_type.text).to eq OStatus::TagManager::TYPES[:activity] end it 'appends activity:verb element with follow' do follow = Fabricate(:follow) follow_salmon = OStatus::AtomSerializer.new.follow_salmon(follow) verb = follow_salmon.nodes.find { |node| node.name == 'activity:verb' } expect(verb.text).to eq OStatus::TagManager::VERBS[:follow] end it 'appends activity:object element with target account' do target_account = Fabricate(:account, domain: 'domain.test', uri: 'https://domain.test/id') follow = Fabricate(:follow, target_account: target_account) follow_salmon = OStatus::AtomSerializer.new.follow_salmon(follow) object = follow_salmon.nodes.find { |node| node.name == 'activity:object' } expect(object.id.text).to eq 'https://domain.test/id' end it 'includes description' do account = Fabricate(:account, domain: nil, username: 'account') target_account = Fabricate(:account, domain: 'remote', username: 'target_account') follow = Fabricate(:follow, account: account, target_account: target_account) follow_salmon = OStatus::AtomSerializer.new.follow_salmon(follow) expect(follow_salmon.title.text).to eq 'account started following target_account@remote' expect(follow_salmon.content.text).to eq 'account started following target_account@remote' end it 'returns element whose rendered view triggers follow when processed' do follow = Fabricate(:follow) follow_salmon = OStatus::AtomSerializer.new.follow_salmon(follow) xml = OStatus::AtomSerializer.render(follow_salmon) follow.destroy! envelope = OStatus2::Salmon.new.pack(xml, follow.account.keypair) ProcessInteractionService.new.call(envelope, follow.target_account) expect(follow.account.following?(follow.target_account)).to be true end end describe '#unfollow_salmon' do include_examples 'namespaces' do def serialize follow = Fabricate(:follow) follow.destroy! OStatus::AtomSerializer.new.unfollow_salmon(follow) end end it 'returns entry element' do follow = Fabricate(:follow) follow.destroy! unfollow_salmon = OStatus::AtomSerializer.new.unfollow_salmon(follow) expect(unfollow_salmon.name).to eq 'entry' end it 'appends id element with unique tag' do follow = Fabricate(:follow) follow.destroy! time_before = Time.zone.now unfollow_salmon = OStatus::AtomSerializer.new.unfollow_salmon(follow) time_after = Time.zone.now expect(unfollow_salmon.id.text).to( eq(OStatus::TagManager.instance.unique_tag(time_before.utc, follow.id, 'Follow')) .or(eq(OStatus::TagManager.instance.unique_tag(time_after.utc, follow.id, 'Follow'))) ) end it 'appends title element with description' do account = Fabricate(:account, domain: nil, username: 'account') target_account = Fabricate(:account, domain: 'remote', username: 'target_account') follow = Fabricate(:follow, account: account, target_account: target_account) follow.destroy! unfollow_salmon = OStatus::AtomSerializer.new.unfollow_salmon(follow) expect(unfollow_salmon.title.text).to eq 'account is no longer following target_account@remote' end it 'appends content element with description' do account = Fabricate(:account, domain: nil, username: 'account') target_account = Fabricate(:account, domain: 'remote', username: 'target_account') follow = Fabricate(:follow, account: account, target_account: target_account) follow.destroy! unfollow_salmon = OStatus::AtomSerializer.new.unfollow_salmon(follow) expect(unfollow_salmon.content.text).to eq 'account is no longer following target_account@remote' end it 'appends author element with account' do account = Fabricate(:account, domain: nil, username: 'username') follow = Fabricate(:follow, account: account) follow.destroy! unfollow_salmon = OStatus::AtomSerializer.new.unfollow_salmon(follow) expect(unfollow_salmon.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/username' end it 'appends activity:object-type element with activity type' do follow = Fabricate(:follow) follow.destroy! unfollow_salmon = OStatus::AtomSerializer.new.unfollow_salmon(follow) object_type = unfollow_salmon.nodes.find { |node| node.name == 'activity:object-type' } expect(object_type.text).to eq OStatus::TagManager::TYPES[:activity] end it 'appends activity:verb element with follow' do follow = Fabricate(:follow) follow.destroy! unfollow_salmon = OStatus::AtomSerializer.new.unfollow_salmon(follow) verb = unfollow_salmon.nodes.find { |node| node.name == 'activity:verb' } expect(verb.text).to eq OStatus::TagManager::VERBS[:unfollow] end it 'appends activity:object element with target account' do target_account = Fabricate(:account, domain: 'domain.test', uri: 'https://domain.test/id') follow = Fabricate(:follow, target_account: target_account) follow.destroy! unfollow_salmon = OStatus::AtomSerializer.new.unfollow_salmon(follow) object = unfollow_salmon.nodes.find { |node| node.name == 'activity:object' } expect(object.id.text).to eq 'https://domain.test/id' end it 'returns element whose rendered view triggers unfollow when processed' do follow = Fabricate(:follow) follow.destroy! unfollow_salmon = OStatus::AtomSerializer.new.unfollow_salmon(follow) xml = OStatus::AtomSerializer.render(unfollow_salmon) follow.account.follow!(follow.target_account) envelope = OStatus2::Salmon.new.pack(xml, follow.account.keypair) ProcessInteractionService.new.call(envelope, follow.target_account) expect(follow.account.following?(follow.target_account)).to be false end end describe '#follow_request_salmon' do include_examples 'namespaces' do def serialize follow_request = Fabricate(:follow_request) OStatus::AtomSerializer.new.follow_request_salmon(follow_request) end end context do def serialize(follow_request) OStatus::AtomSerializer.new.follow_request_salmon(follow_request) end it_behaves_like 'follow request salmon' it 'appends id element with unique tag' do follow_request = Fabricate(:follow_request, created_at: '2000-01-01T00:00:00Z') follow_request_salmon = serialize(follow_request) expect(follow_request_salmon.id.text).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{follow_request.id}:objectType=FollowRequest" end it 'appends title element with description' do account = Fabricate(:account, domain: nil, username: 'account') target_account = Fabricate(:account, domain: 'remote', username: 'target_account') follow_request = Fabricate(:follow_request, account: account, target_account: target_account) follow_request_salmon = serialize(follow_request) expect(follow_request_salmon.title.text).to eq 'account requested to follow target_account@remote' end it 'returns element whose rendered view triggers follow request when processed' do follow_request = Fabricate(:follow_request) follow_request_salmon = serialize(follow_request) xml = OStatus::AtomSerializer.render(follow_request_salmon) envelope = OStatus2::Salmon.new.pack(xml, follow_request.account.keypair) follow_request.destroy! ProcessInteractionService.new.call(envelope, follow_request.target_account) expect(follow_request.account.requested?(follow_request.target_account)).to eq true end end end describe '#authorize_follow_request_salmon' do include_examples 'namespaces' do def serialize follow_request = Fabricate(:follow_request) OStatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request) end end it_behaves_like 'follow request salmon' do def serialize(follow_request) authorize_follow_request_salmon = OStatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request) authorize_follow_request_salmon.nodes.find { |node| node.name == 'activity:object' } end end it 'appends id element with unique tag' do follow_request = Fabricate(:follow_request) time_before = Time.zone.now authorize_follow_request_salmon = OStatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request) time_after = Time.zone.now expect(authorize_follow_request_salmon.id.text).to( eq(OStatus::TagManager.instance.unique_tag(time_before.utc, follow_request.id, 'FollowRequest')) .or(eq(OStatus::TagManager.instance.unique_tag(time_after.utc, follow_request.id, 'FollowRequest'))) ) end it 'appends title element with description' do account = Fabricate(:account, domain: 'remote', username: 'account') target_account = Fabricate(:account, domain: nil, username: 'target_account') follow_request = Fabricate(:follow_request, account: account, target_account: target_account) authorize_follow_request_salmon = OStatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request) expect(authorize_follow_request_salmon.title.text).to eq 'target_account authorizes follow request by account@remote' end it 'appends activity:object-type element with activity type' do follow_request = Fabricate(:follow_request) authorize_follow_request_salmon = OStatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request) object_type = authorize_follow_request_salmon.nodes.find { |node| node.name == 'activity:object-type' } expect(object_type.text).to eq OStatus::TagManager::TYPES[:activity] end it 'appends activity:verb element with authorize' do follow_request = Fabricate(:follow_request) authorize_follow_request_salmon = OStatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request) verb = authorize_follow_request_salmon.nodes.find { |node| node.name == 'activity:verb' } expect(verb.text).to eq OStatus::TagManager::VERBS[:authorize] end it 'returns element whose rendered view creates follow from follow request when processed' do follow_request = Fabricate(:follow_request) authorize_follow_request_salmon = OStatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request) xml = OStatus::AtomSerializer.render(authorize_follow_request_salmon) envelope = OStatus2::Salmon.new.pack(xml, follow_request.target_account.keypair) ProcessInteractionService.new.call(envelope, follow_request.account) expect(follow_request.account.following?(follow_request.target_account)).to eq true expect { follow_request.reload }.to raise_error ActiveRecord::RecordNotFound end end describe '#reject_follow_request_salmon' do include_examples 'namespaces' do def serialize follow_request = Fabricate(:follow_request) OStatus::AtomSerializer.new.reject_follow_request_salmon(follow_request) end end it_behaves_like 'follow request salmon' do def serialize(follow_request) reject_follow_request_salmon = OStatus::AtomSerializer.new.reject_follow_request_salmon(follow_request) reject_follow_request_salmon.nodes.find { |node| node.name == 'activity:object' } end end it 'appends id element with unique tag' do follow_request = Fabricate(:follow_request) time_before = Time.zone.now reject_follow_request_salmon = OStatus::AtomSerializer.new.reject_follow_request_salmon(follow_request) time_after = Time.zone.now expect(reject_follow_request_salmon.id.text).to( eq(OStatus::TagManager.instance.unique_tag(time_before.utc, follow_request.id, 'FollowRequest')) .or(OStatus::TagManager.instance.unique_tag(time_after.utc, follow_request.id, 'FollowRequest')) ) end it 'appends title element with description' do account = Fabricate(:account, domain: 'remote', username: 'account') target_account = Fabricate(:account, domain: nil, username: 'target_account') follow_request = Fabricate(:follow_request, account: account, target_account: target_account) reject_follow_request_salmon = OStatus::AtomSerializer.new.reject_follow_request_salmon(follow_request) expect(reject_follow_request_salmon.title.text).to eq 'target_account rejects follow request by account@remote' end it 'appends activity:object-type element with activity type' do follow_request = Fabricate(:follow_request) reject_follow_request_salmon = OStatus::AtomSerializer.new.reject_follow_request_salmon(follow_request) object_type = reject_follow_request_salmon.nodes.find { |node| node.name == 'activity:object-type' } expect(object_type.text).to eq OStatus::TagManager::TYPES[:activity] end it 'appends activity:verb element with authorize' do follow_request = Fabricate(:follow_request) reject_follow_request_salmon = OStatus::AtomSerializer.new.reject_follow_request_salmon(follow_request) verb = reject_follow_request_salmon.nodes.find { |node| node.name == 'activity:verb' } expect(verb.text).to eq OStatus::TagManager::VERBS[:reject] end it 'returns element whose rendered view deletes follow request when processed' do follow_request = Fabricate(:follow_request) reject_follow_request_salmon = OStatus::AtomSerializer.new.reject_follow_request_salmon(follow_request) xml = OStatus::AtomSerializer.render(reject_follow_request_salmon) envelope = OStatus2::Salmon.new.pack(xml, follow_request.target_account.keypair) ProcessInteractionService.new.call(envelope, follow_request.account) expect(follow_request.account.following?(follow_request.target_account)).to eq false expect { follow_request.reload }.to raise_error ActiveRecord::RecordNotFound end end describe '#object' do include_examples 'status attributes' do def serialize(status) OStatus::AtomSerializer.new.object(status) end end it 'returns activity:object element' do status = Fabricate(:status) object = OStatus::AtomSerializer.new.object(status) expect(object.name).to eq 'activity:object' end it 'appends id element with URL for status' do status = Fabricate(:status, created_at: '2000-01-01T00:00:00Z') object = OStatus::AtomSerializer.new.object(status) expect(object.id.text).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}" end it 'appends published element with created date' do status = Fabricate(:status, created_at: '2000-01-01T00:00:00Z') object = OStatus::AtomSerializer.new.object(status) expect(object.published.text).to eq '2000-01-01T00:00:00Z' end it 'appends updated element with updated date' do status = Fabricate(:status) status.updated_at = '2000-01-01T00:00:00Z' object = OStatus::AtomSerializer.new.object(status) expect(object.updated.text).to eq '2000-01-01T00:00:00Z' end it 'appends title element with title' do account = Fabricate(:account, username: 'username') status = Fabricate(:status, account: account) object = OStatus::AtomSerializer.new.object(status) expect(object.title.text).to eq 'New status by username' end it 'appends author element with account' do account = Fabricate(:account, username: 'username') status = Fabricate(:status, account: account) entry = OStatus::AtomSerializer.new.object(status) expect(entry.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/username' end it 'appends activity:object-type element with object type' do status = Fabricate(:status) entry = OStatus::AtomSerializer.new.object(status) object_type = entry.nodes.find { |node| node.name == 'activity:object-type' } expect(object_type.text).to eq OStatus::TagManager::TYPES[:note] end it 'appends activity:verb element with verb' do status = Fabricate(:status) entry = OStatus::AtomSerializer.new.object(status) object_type = entry.nodes.find { |node| node.name == 'activity:verb' } expect(object_type.text).to eq OStatus::TagManager::VERBS[:post] end it 'appends link element for an alternative' do account = Fabricate(:account, username: 'username') status = Fabricate(:status, account: account) entry = OStatus::AtomSerializer.new.object(status) link = entry.nodes.find { |node| node.name == 'link' && node[:rel] == 'alternate' && node[:type] == 'text/html' } expect(link[:type]).to eq 'text/html' expect(link[:href]).to eq "https://cb6e6126.ngrok.io/@username/#{status.id}" end it 'appends thr:in-reply-to element if it is a reply and thread is not nil' do account = Fabricate(:account, username: 'username') thread = Fabricate(:status, account: account, created_at: '2000-01-01T00:00:00Z') reply = Fabricate(:status, thread: thread) entry = OStatus::AtomSerializer.new.object(reply) in_reply_to = entry.nodes.find { |node| node.name == 'thr:in-reply-to' } expect(in_reply_to.ref).to eq "https://cb6e6126.ngrok.io/users/#{thread.account.to_param}/statuses/#{thread.id}" expect(in_reply_to.href).to eq "https://cb6e6126.ngrok.io/@username/#{thread.id}" end it 'does not append thr:in-reply-to element if thread is nil' do status = Fabricate(:status, thread: nil) entry = OStatus::AtomSerializer.new.object(status) entry.nodes.each { |node| expect(node.name).not_to eq 'thr:in-reply-to' } end it 'does not append ostatus:conversation element if conversation_id is nil' do status = Fabricate.build(:status, conversation_id: nil) status.save!(validate: false) entry = OStatus::AtomSerializer.new.object(status) entry.nodes.each { |node| expect(node.name).not_to eq 'ostatus:conversation' } end it 'appends ostatus:conversation element if conversation_id is not nil' do status = Fabricate(:status) status.conversation.update!(created_at: '2000-01-01T00:00:00Z') entry = OStatus::AtomSerializer.new.object(status) conversation = entry.nodes.find { |node| node.name == 'ostatus:conversation' } expect(conversation[:ref]).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{status.conversation.id}:objectType=Conversation" end end end ================================================ FILE: spec/lib/ostatus/tag_manager_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe OStatus::TagManager do describe '#unique_tag' do it 'returns a unique tag' do expect(OStatus::TagManager.instance.unique_tag(Time.utc(2000), 12, 'Status')).to eq 'tag:cb6e6126.ngrok.io,2000-01-01:objectId=12:objectType=Status' end end describe '#unique_tag_to_local_id' do it 'returns the ID part' do expect(OStatus::TagManager.instance.unique_tag_to_local_id('tag:cb6e6126.ngrok.io,2000-01-01:objectId=12:objectType=Status', 'Status')).to eql '12' end it 'returns nil if it is not local id' do expect(OStatus::TagManager.instance.unique_tag_to_local_id('tag:remote,2000-01-01:objectId=12:objectType=Status', 'Status')).to eq nil end it 'returns nil if it is not expected type' do expect(OStatus::TagManager.instance.unique_tag_to_local_id('tag:cb6e6126.ngrok.io,2000-01-01:objectId=12:objectType=Block', 'Status')).to eq nil end it 'returns nil if it does not have object ID' do expect(OStatus::TagManager.instance.unique_tag_to_local_id('tag:cb6e6126.ngrok.io,2000-01-01:objectType=Status', 'Status')).to eq nil end end describe '#local_id?' do it 'returns true for a local ID' do expect(OStatus::TagManager.instance.local_id?('tag:cb6e6126.ngrok.io;objectId=12:objectType=Status')).to be true end it 'returns false for a foreign ID' do expect(OStatus::TagManager.instance.local_id?('tag:foreign.tld;objectId=12:objectType=Status')).to be false end end describe '#uri_for' do subject { OStatus::TagManager.instance.uri_for(target) } context 'comment object' do let(:target) { Fabricate(:status, created_at: '2000-01-01T00:00:00Z', reply: true) } it 'returns the unique tag for status' do expect(target.object_type).to eq :comment is_expected.to eq target.uri end end context 'note object' do let(:target) { Fabricate(:status, created_at: '2000-01-01T00:00:00Z', reply: false, thread: nil) } it 'returns the unique tag for status' do expect(target.object_type).to eq :note is_expected.to eq target.uri end end context 'person object' do let(:target) { Fabricate(:account, username: 'alice') } it 'returns the URL for account' do expect(target.object_type).to eq :person is_expected.to eq 'https://cb6e6126.ngrok.io/users/alice' end end end end ================================================ FILE: spec/lib/proof_provider/keybase/verifier_spec.rb ================================================ require 'rails_helper' describe ProofProvider::Keybase::Verifier do let(:my_domain) { Rails.configuration.x.local_domain } let(:keybase_proof) do local_proof = AccountIdentityProof.new( provider: 'Keybase', provider_username: 'cryptoalice', token: '11111111111111111111111111' ) described_class.new('alice', 'cryptoalice', '11111111111111111111111111', my_domain) end let(:query_params) do "domain=#{my_domain}&kb_username=cryptoalice&sig_hash=11111111111111111111111111&username=alice" end describe '#valid?' do let(:base_url) { 'https://keybase.io/_/api/1.0/sig/proof_valid.json' } context 'when valid' do before do json_response_body = '{"status":{"code":0,"name":"OK"},"proof_valid":true}' stub_request(:get, "#{base_url}?#{query_params}").to_return(status: 200, body: json_response_body) end it 'calls out to keybase and returns true' do expect(keybase_proof.valid?).to eq true end end context 'when invalid' do before do json_response_body = '{"status":{"code":0,"name":"OK"},"proof_valid":false}' stub_request(:get, "#{base_url}?#{query_params}").to_return(status: 200, body: json_response_body) end it 'calls out to keybase and returns false' do expect(keybase_proof.valid?).to eq false end end context 'with an unexpected api response' do before do json_response_body = '{"status":{"code":100,"desc":"wrong size hex_id","fields":{"sig_hash":"wrong size hex_id"},"name":"INPUT_ERROR"}}' stub_request(:get, "#{base_url}?#{query_params}").to_return(status: 200, body: json_response_body) end it 'swallows the error and returns false' do expect(keybase_proof.valid?).to eq false end end end describe '#status' do let(:base_url) { 'https://keybase.io/_/api/1.0/sig/proof_live.json' } context 'with a normal response' do before do json_response_body = '{"status":{"code":0,"name":"OK"},"proof_live":false,"proof_valid":true}' stub_request(:get, "#{base_url}?#{query_params}").to_return(status: 200, body: json_response_body) end it 'calls out to keybase and returns the status fields as proof_valid and proof_live' do expect(keybase_proof.status).to include({ 'proof_valid' => true, 'proof_live' => false }) end end context 'with an unexpected keybase response' do before do json_response_body = '{"status":{"code":100,"desc":"missing non-optional field sig_hash","fields":{"sig_hash":"missing non-optional field sig_hash"},"name":"INPUT_ERROR"}}' stub_request(:get, "#{base_url}?#{query_params}").to_return(status: 200, body: json_response_body) end it 'raises a ProofProvider::Keybase::UnexpectedResponseError' do expect { keybase_proof.status }.to raise_error ProofProvider::Keybase::UnexpectedResponseError end end end end ================================================ FILE: spec/lib/request_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' require 'securerandom' describe Request do subject { Request.new(:get, 'http://example.com') } describe '#headers' do it 'returns user agent' do expect(subject.headers['User-Agent']).to be_present end it 'returns the date header' do expect(subject.headers['Date']).to be_present end it 'returns the host header' do expect(subject.headers['Host']).to be_present end it 'does not return virtual request-target header' do expect(subject.headers['(request-target)']).to be_nil end end describe '#on_behalf_of' do it 'when used, adds signature header' do subject.on_behalf_of(Fabricate(:account)) expect(subject.headers['Signature']).to be_present end end describe '#add_headers' do it 'adds headers to the request' do subject.add_headers('Test' => 'Foo') expect(subject.headers['Test']).to eq 'Foo' end end describe '#perform' do context 'with valid host' do before { stub_request(:get, 'http://example.com') } it 'executes a HTTP request' do expect { |block| subject.perform &block }.to yield_control expect(a_request(:get, 'http://example.com')).to have_been_made.once end it 'executes a HTTP request when the first address is private' do resolver = double allow(resolver).to receive(:getaddresses).with('example.com').and_return(%w(0.0.0.0 2001:4860:4860::8844)) allow(resolver).to receive(:timeouts=).and_return(nil) allow(Resolv::DNS).to receive(:open).and_yield(resolver) expect { |block| subject.perform &block }.to yield_control expect(a_request(:get, 'http://example.com')).to have_been_made.once end it 'sets headers' do expect { |block| subject.perform &block }.to yield_control expect(a_request(:get, 'http://example.com').with(headers: subject.headers)).to have_been_made end it 'closes underlaying connection' do expect_any_instance_of(HTTP::Client).to receive(:close) expect { |block| subject.perform &block }.to yield_control end it 'returns response which implements body_with_limit' do subject.perform do |response| expect(response).to respond_to :body_with_limit end end end context 'with private host' do around do |example| WebMock.disable! example.run WebMock.enable! end it 'raises Mastodon::ValidationError' do resolver = double allow(resolver).to receive(:getaddresses).with('example.com').and_return(%w(0.0.0.0 2001:db8::face)) allow(resolver).to receive(:timeouts=).and_return(nil) allow(Resolv::DNS).to receive(:open).and_yield(resolver) expect { subject.perform }.to raise_error Mastodon::ValidationError end end end describe "response's body_with_limit method" do it 'rejects body more than 1 megabyte by default' do stub_request(:any, 'http://example.com').to_return(body: SecureRandom.random_bytes(2.megabytes)) expect { subject.perform { |response| response.body_with_limit } }.to raise_error Mastodon::LengthValidationError end it 'accepts body less than 1 megabyte by default' do stub_request(:any, 'http://example.com').to_return(body: SecureRandom.random_bytes(2.kilobytes)) expect { subject.perform { |response| response.body_with_limit } }.not_to raise_error end it 'rejects body by given size' do stub_request(:any, 'http://example.com').to_return(body: SecureRandom.random_bytes(2.kilobytes)) expect { subject.perform { |response| response.body_with_limit(1.kilobyte) } }.to raise_error Mastodon::LengthValidationError end it 'rejects too large chunked body' do stub_request(:any, 'http://example.com').to_return(body: SecureRandom.random_bytes(2.megabytes), headers: { 'Transfer-Encoding' => 'chunked' }) expect { subject.perform { |response| response.body_with_limit } }.to raise_error Mastodon::LengthValidationError end it 'rejects too large monolithic body' do stub_request(:any, 'http://example.com').to_return(body: SecureRandom.random_bytes(2.megabytes), headers: { 'Content-Length' => 2.megabytes }) expect { subject.perform { |response| response.body_with_limit } }.to raise_error Mastodon::LengthValidationError end it 'uses binary encoding if Content-Type does not tell encoding' do stub_request(:any, 'http://example.com').to_return(body: '', headers: { 'Content-Type' => 'text/html' }) expect(subject.perform { |response| response.body_with_limit.encoding }).to eq Encoding::BINARY end it 'uses binary encoding if Content-Type tells unknown encoding' do stub_request(:any, 'http://example.com').to_return(body: '', headers: { 'Content-Type' => 'text/html; charset=unknown' }) expect(subject.perform { |response| response.body_with_limit.encoding }).to eq Encoding::BINARY end it 'uses encoding specified by Content-Type' do stub_request(:any, 'http://example.com').to_return(body: '', headers: { 'Content-Type' => 'text/html; charset=UTF-8' }) expect(subject.perform { |response| response.body_with_limit.encoding }).to eq Encoding::UTF_8 end end end ================================================ FILE: spec/lib/settings/extend_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' RSpec.describe Settings::Extend do class User include Settings::Extend end describe '#settings' do it 'sets @settings as an instance of Settings::ScopedSettings' do user = Fabricate(:user) expect(user.settings).to be_kind_of Settings::ScopedSettings end end end ================================================ FILE: spec/lib/settings/scoped_settings_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' RSpec.describe Settings::ScopedSettings do let(:object) { Fabricate(:user) } let(:scoped_setting) { described_class.new(object) } let(:val) { 'whatever' } let(:methods) { %i(auto_play_gif default_sensitive unfollow_modal boost_modal delete_modal reduce_motion system_font_ui noindex theme) } describe '.initialize' do it 'sets @object' do scoped_setting = described_class.new(object) expect(scoped_setting.instance_variable_get(:@object)).to be object end end describe '#method_missing' do it 'sets scoped_setting.method_name = val' do methods.each do |key| scoped_setting.send("#{key}=", val) expect(scoped_setting.send(key)).to eq val end end end describe '#[]= and #[]' do it 'sets [key] = val' do methods.each do |key| scoped_setting[key] = val expect(scoped_setting[key]).to eq val end end end end ================================================ FILE: spec/lib/status_filter_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe StatusFilter do describe '#filtered?' do let(:status) { Fabricate(:status) } context 'without an account' do subject { described_class.new(status, nil) } context 'when there are no connections' do it { is_expected.not_to be_filtered } end context 'when status account is silenced' do before do status.account.silence! end it { is_expected.to be_filtered } end context 'when status policy does not allow show' do before do expect_any_instance_of(StatusPolicy).to receive(:show?).and_return(false) end it { is_expected.to be_filtered } end end context 'with real account' do let(:account) { Fabricate(:account) } subject { described_class.new(status, account) } context 'when there are no connections' do it { is_expected.not_to be_filtered } end context 'when status account is blocked' do before do Fabricate(:block, account: account, target_account: status.account) end it { is_expected.to be_filtered } end context 'when status account domain is blocked' do before do status.account.update(domain: 'example.com') Fabricate(:account_domain_block, account: account, domain: status.account_domain) end it { is_expected.to be_filtered } end context 'when status account is muted' do before do Fabricate(:mute, account: account, target_account: status.account) end it { is_expected.to be_filtered } end context 'when status account is silenced' do before do status.account.silence! end it { is_expected.to be_filtered } end context 'when status policy does not allow show' do before do expect_any_instance_of(StatusPolicy).to receive(:show?).and_return(false) end it { is_expected.to be_filtered } end end end end ================================================ FILE: spec/lib/status_finder_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe StatusFinder do include RoutingHelper describe '#status' do subject { described_class.new(url) } context 'with a status url' do let(:status) { Fabricate(:status) } let(:url) { short_account_status_url(account_username: status.account.username, id: status.id) } it 'finds the stream entry' do expect(subject.status).to eq(status) end it 'raises an error if action is not :show' do recognized = Rails.application.routes.recognize_path(url) expect(recognized).to receive(:[]).with(:action).and_return(:create) expect(Rails.application.routes).to receive(:recognize_path).with(url).and_return(recognized) expect { subject.status }.to raise_error(ActiveRecord::RecordNotFound) end end context 'with a stream entry url' do let(:stream_entry) { Fabricate(:stream_entry) } let(:url) { account_stream_entry_url(stream_entry.account, stream_entry) } it 'finds the stream entry' do expect(subject.status).to eq(stream_entry.status) end end context 'with a remote url even if id exists on local' do let(:status) { Fabricate(:status) } let(:url) { "https://example.com/users/test/statuses/#{status.id}" } it 'raises an error' do expect { subject.status }.to raise_error(ActiveRecord::RecordNotFound) end end context 'with a plausible url' do let(:url) { 'https://example.com/users/test/updates/123/embed' } it 'raises an error' do expect { subject.status }.to raise_error(ActiveRecord::RecordNotFound) end end context 'with an unrecognized url' do let(:url) { 'https://example.com/about' } it 'raises an error' do expect { subject.status }.to raise_error(ActiveRecord::RecordNotFound) end end end end ================================================ FILE: spec/lib/tag_manager_spec.rb ================================================ require 'rails_helper' RSpec.describe TagManager do describe '#local_domain?' do # The following comparisons MUST be case-insensitive. around do |example| original_local_domain = Rails.configuration.x.local_domain Rails.configuration.x.local_domain = 'domain.test' example.run Rails.configuration.x.local_domain = original_local_domain end it 'returns true for nil' do expect(TagManager.instance.local_domain?(nil)).to eq true end it 'returns true if the slash-stripped string equals to local domain' do expect(TagManager.instance.local_domain?('DoMaIn.Test/')).to eq true end it 'returns false for irrelevant string' do expect(TagManager.instance.local_domain?('DoMaIn.Test!')).to eq false end end describe '#web_domain?' do # The following comparisons MUST be case-insensitive. around do |example| original_web_domain = Rails.configuration.x.web_domain Rails.configuration.x.web_domain = 'domain.test' example.run Rails.configuration.x.web_domain = original_web_domain end it 'returns true for nil' do expect(TagManager.instance.web_domain?(nil)).to eq true end it 'returns true if the slash-stripped string equals to web domain' do expect(TagManager.instance.web_domain?('DoMaIn.Test/')).to eq true end it 'returns false for string with irrelevant characters' do expect(TagManager.instance.web_domain?('DoMaIn.Test!')).to eq false end end describe '#normalize_domain' do it 'returns nil if the given parameter is nil' do expect(TagManager.instance.normalize_domain(nil)).to eq nil end it 'returns normalized domain' do expect(TagManager.instance.normalize_domain('DoMaIn.Test/')).to eq 'domain.test' end end describe '#local_url?' do around do |example| original_web_domain = Rails.configuration.x.web_domain example.run Rails.configuration.x.web_domain = original_web_domain end it 'returns true if the normalized string with port is local URL' do Rails.configuration.x.web_domain = 'domain.test:42' expect(TagManager.instance.local_url?('https://DoMaIn.Test:42/')).to eq true end it 'returns true if the normalized string without port is local URL' do Rails.configuration.x.web_domain = 'domain.test' expect(TagManager.instance.local_url?('https://DoMaIn.Test/')).to eq true end it 'returns false for string with irrelevant characters' do Rails.configuration.x.web_domain = 'domain.test' expect(TagManager.instance.local_url?('https://domainn.test/')).to eq false end end describe '#same_acct?' do # The following comparisons MUST be case-insensitive. it 'returns true if the needle has a correct username and domain for remote user' do expect(TagManager.instance.same_acct?('username@domain.test', 'UsErNaMe@DoMaIn.Test')).to eq true end it 'returns false if the needle is missing a domain for remote user' do expect(TagManager.instance.same_acct?('username@domain.test', 'UsErNaMe')).to eq false end it 'returns false if the needle has an incorrect domain for remote user' do expect(TagManager.instance.same_acct?('username@domain.test', 'UsErNaMe@incorrect.test')).to eq false end it 'returns false if the needle has an incorrect username for remote user' do expect(TagManager.instance.same_acct?('username@domain.test', 'incorrect@DoMaIn.test')).to eq false end it 'returns true if the needle has a correct username and domain for local user' do expect(TagManager.instance.same_acct?('username', 'UsErNaMe@Cb6E6126.nGrOk.Io')).to eq true end it 'returns true if the needle is missing a domain for local user' do expect(TagManager.instance.same_acct?('username', 'UsErNaMe')).to eq true end it 'returns false if the needle has an incorrect username for local user' do expect(TagManager.instance.same_acct?('username', 'UsErNaM@Cb6E6126.nGrOk.Io')).to eq false end it 'returns false if the needle has an incorrect domain for local user' do expect(TagManager.instance.same_acct?('username', 'incorrect@Cb6E6126.nGrOk.Io')).to eq false end end describe '#url_for' do let(:alice) { Fabricate(:account, username: 'alice') } subject { TagManager.instance.url_for(target) } context 'activity object' do let(:target) { Fabricate(:status, account: alice, reblog: Fabricate(:status)).stream_entry } it 'returns the unique tag for status' do expect(target.object_type).to eq :activity is_expected.to eq "https://cb6e6126.ngrok.io/@alice/#{target.id}" end end context 'comment object' do let(:target) { Fabricate(:status, account: alice, reply: true) } it 'returns the unique tag for status' do expect(target.object_type).to eq :comment is_expected.to eq "https://cb6e6126.ngrok.io/@alice/#{target.id}" end end context 'note object' do let(:target) { Fabricate(:status, account: alice, reply: false, thread: nil) } it 'returns the unique tag for status' do expect(target.object_type).to eq :note is_expected.to eq "https://cb6e6126.ngrok.io/@alice/#{target.id}" end end context 'person object' do let(:target) { alice } it 'returns the URL for account' do expect(target.object_type).to eq :person is_expected.to eq 'https://cb6e6126.ngrok.io/@alice' end end end end ================================================ FILE: spec/lib/user_settings_decorator_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe UserSettingsDecorator do describe 'update' do let(:user) { Fabricate(:user) } let(:settings) { described_class.new(user) } it 'updates the user settings value for email notifications' do values = { 'notification_emails' => { 'follow' => '1' } } settings.update(values) expect(user.settings['notification_emails']['follow']).to eq true end it 'updates the user settings value for interactions' do values = { 'interactions' => { 'must_be_follower' => '0' } } settings.update(values) expect(user.settings['interactions']['must_be_follower']).to eq false end it 'updates the user settings value for privacy' do values = { 'setting_default_privacy' => 'public' } settings.update(values) expect(user.settings['default_privacy']).to eq 'public' end it 'updates the user settings value for sensitive' do values = { 'setting_default_sensitive' => '1' } settings.update(values) expect(user.settings['default_sensitive']).to eq true end it 'updates the user settings value for unfollow modal' do values = { 'setting_unfollow_modal' => '0' } settings.update(values) expect(user.settings['unfollow_modal']).to eq false end it 'updates the user settings value for boost modal' do values = { 'setting_boost_modal' => '1' } settings.update(values) expect(user.settings['boost_modal']).to eq true end it 'updates the user settings value for delete toot modal' do values = { 'setting_delete_modal' => '0' } settings.update(values) expect(user.settings['delete_modal']).to eq false end it 'updates the user settings value for gif auto play' do values = { 'setting_auto_play_gif' => '0' } settings.update(values) expect(user.settings['auto_play_gif']).to eq false end it 'updates the user settings value for system font in UI' do values = { 'setting_system_font_ui' => '0' } settings.update(values) expect(user.settings['system_font_ui']).to eq false end it 'decoerces setting values before applying' do values = { 'setting_delete_modal' => 'false', 'setting_boost_modal' => 'true', } settings.update(values) expect(user.settings['delete_modal']).to eq false expect(user.settings['boost_modal']).to eq true end end end ================================================ FILE: spec/lib/webfinger_resource_spec.rb ================================================ require 'rails_helper' describe WebfingerResource do around do |example| before_local = Rails.configuration.x.local_domain before_web = Rails.configuration.x.web_domain example.run Rails.configuration.x.local_domain = before_local Rails.configuration.x.web_domain = before_web end describe '#username' do describe 'with a URL value' do it 'raises with a route whose controller is not AccountsController' do resource = 'https://example.com/users/alice/other' expect { WebfingerResource.new(resource).username }.to raise_error(ActiveRecord::RecordNotFound) end it 'raises with a route whose action is not show' do resource = 'https://example.com/users/alice' recognized = Rails.application.routes.recognize_path(resource) allow(recognized).to receive(:[]).with(:controller).and_return('accounts') allow(recognized).to receive(:[]).with(:username).and_return('alice') expect(recognized).to receive(:[]).with(:action).and_return('create') expect(Rails.application.routes).to receive(:recognize_path).with(resource).and_return(recognized).at_least(:once) expect { WebfingerResource.new(resource).username }.to raise_error(ActiveRecord::RecordNotFound) end it 'raises with a string that doesnt start with URL' do resource = 'website for http://example.com/users/alice/other' expect { WebfingerResource.new(resource).username }.to raise_error(ActiveRecord::RecordNotFound) end it 'finds the username in a valid https route' do resource = 'https://example.com/users/alice' result = WebfingerResource.new(resource).username expect(result).to eq 'alice' end it 'finds the username in a mixed case http route' do resource = 'HTTp://exAMPLEe.com/users/alice' result = WebfingerResource.new(resource).username expect(result).to eq 'alice' end it 'finds the username in a valid http route' do resource = 'http://example.com/users/alice' result = WebfingerResource.new(resource).username expect(result).to eq 'alice' end end describe 'with a username and hostname value' do it 'raises on a non-local domain' do resource = 'user@remote-host.com' expect { WebfingerResource.new(resource).username }.to raise_error(ActiveRecord::RecordNotFound) end it 'finds username for a local domain' do Rails.configuration.x.local_domain = 'example.com' resource = 'alice@example.com' result = WebfingerResource.new(resource).username expect(result).to eq 'alice' end it 'finds username for a web domain' do Rails.configuration.x.web_domain = 'example.com' resource = 'alice@example.com' result = WebfingerResource.new(resource).username expect(result).to eq 'alice' end end describe 'with an acct value' do it 'raises on a non-local domain' do resource = 'acct:user@remote-host.com' expect { WebfingerResource.new(resource).username }.to raise_error(ActiveRecord::RecordNotFound) end it 'raises on a nonsense domain' do resource = 'acct:user@remote-host@remote-hostess.remote.local@remote' expect { WebfingerResource.new(resource).username }.to raise_error(ActiveRecord::RecordNotFound) end it 'finds the username for a local account if the domain is the local one' do Rails.configuration.x.local_domain = 'example.com' resource = 'acct:alice@example.com' result = WebfingerResource.new(resource).username expect(result).to eq 'alice' end it 'finds the username for a local account if the domain is the Web one' do Rails.configuration.x.web_domain = 'example.com' resource = 'acct:alice@example.com' result = WebfingerResource.new(resource).username expect(result).to eq 'alice' end end end end ================================================ FILE: spec/mailers/admin_mailer_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' RSpec.describe AdminMailer, type: :mailer do describe '.new_report' do let(:sender) { Fabricate(:account, username: 'John', user: Fabricate(:user)) } let(:recipient) { Fabricate(:account, username: 'Mike', user: Fabricate(:user, locale: :en)) } let(:report) { Fabricate(:report, account: sender, target_account: recipient) } let(:mail) { described_class.new_report(recipient, report) } it 'renders the headers' do expect(mail.subject).to eq("New report for cb6e6126.ngrok.io (##{report.id})") expect(mail.to).to eq [recipient.user_email] expect(mail.from).to eq ['notifications@localhost'] end it 'renders the body' do expect(mail.body.encoded).to eq("Mike,\r\n\r\nJohn has reported Mike\r\n\r\nView: https://cb6e6126.ngrok.io/admin/reports/#{report.id}\r\n") end end end ================================================ FILE: spec/mailers/notification_mailer_spec.rb ================================================ require "rails_helper" RSpec.describe NotificationMailer, type: :mailer do let(:receiver) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } let(:sender) { Fabricate(:account, username: 'bob') } let(:foreign_status) { Fabricate(:status, account: sender, text: 'The body of the foreign status') } let(:own_status) { Fabricate(:status, account: receiver.account, text: 'The body of the own status') } shared_examples 'localized subject' do |*args, **kwrest| it 'renders subject localized for the locale of the receiver' do locale = %i(de en).sample receiver.update!(locale: locale) expect(mail.subject).to eq I18n.t(*args, kwrest.merge(locale: locale)) end it 'renders subject localized for the default locale if the locale of the receiver is unavailable' do receiver.update!(locale: nil) expect(mail.subject).to eq I18n.t(*args, kwrest.merge(locale: I18n.default_locale)) end end describe "mention" do let(:mention) { Mention.create!(account: receiver.account, status: foreign_status) } let(:mail) { NotificationMailer.mention(receiver.account, Notification.create!(account: receiver.account, activity: mention)) } include_examples 'localized subject', 'notification_mailer.mention.subject', name: 'bob' it "renders the headers" do expect(mail.subject).to eq("You were mentioned by bob") expect(mail.to).to eq([receiver.email]) end it "renders the body" do expect(mail.body.encoded).to match("You were mentioned by bob") expect(mail.body.encoded).to include 'The body of the foreign status' end end describe "follow" do let(:follow) { sender.follow!(receiver.account) } let(:mail) { NotificationMailer.follow(receiver.account, Notification.create!(account: receiver.account, activity: follow)) } include_examples 'localized subject', 'notification_mailer.follow.subject', name: 'bob' it "renders the headers" do expect(mail.subject).to eq("bob is now following you") expect(mail.to).to eq([receiver.email]) end it "renders the body" do expect(mail.body.encoded).to match("bob is now following you") end end describe "favourite" do let(:favourite) { Favourite.create!(account: sender, status: own_status) } let(:mail) { NotificationMailer.favourite(own_status.account, Notification.create!(account: receiver.account, activity: favourite)) } include_examples 'localized subject', 'notification_mailer.favourite.subject', name: 'bob' it "renders the headers" do expect(mail.subject).to eq("bob favourited your status") expect(mail.to).to eq([receiver.email]) end it "renders the body" do expect(mail.body.encoded).to match("Your status was favourited by bob") expect(mail.body.encoded).to include 'The body of the own status' end end describe "reblog" do let(:reblog) { Status.create!(account: sender, reblog: own_status) } let(:mail) { NotificationMailer.reblog(own_status.account, Notification.create!(account: receiver.account, activity: reblog)) } include_examples 'localized subject', 'notification_mailer.reblog.subject', name: 'bob' it "renders the headers" do expect(mail.subject).to eq("bob boosted your status") expect(mail.to).to eq([receiver.email]) end it "renders the body" do expect(mail.body.encoded).to match("Your status was boosted by bob") expect(mail.body.encoded).to include 'The body of the own status' end end describe 'follow_request' do let(:follow_request) { Fabricate(:follow_request, account: sender, target_account: receiver.account) } let(:mail) { NotificationMailer.follow_request(receiver.account, Notification.create!(account: receiver.account, activity: follow_request)) } include_examples 'localized subject', 'notification_mailer.follow_request.subject', name: 'bob' it 'renders the headers' do expect(mail.subject).to eq('Pending follower: bob') expect(mail.to).to eq([receiver.email]) end it 'renders the body' do expect(mail.body.encoded).to match("bob has requested to follow you") end end describe 'digest' do before do mention = Fabricate(:mention, account: receiver.account, status: foreign_status) Fabricate(:notification, account: receiver.account, activity: mention) sender.follow!(receiver.account) end context do let!(:mail) { NotificationMailer.digest(receiver.account, since: 5.days.ago) } include_examples 'localized subject', 'notification_mailer.digest.subject', count: 1, name: 'bob' it 'renders the headers' do expect(mail.subject).to match('notification since your last') expect(mail.to).to eq([receiver.email]) end it 'renders the body' do expect(mail.body.encoded).to match('brief summary') expect(mail.body.encoded).to include 'The body of the foreign status' expect(mail.body.encoded).to include sender.username end end it 'includes activities since the receiver last signed in' do receiver.update!(last_emailed_at: nil, current_sign_in_at: '2000-03-01T00:00:00Z') mail = NotificationMailer.digest(receiver.account) expect(mail.body.encoded).to include 'Mar 01, 2000, 00:00' end end end ================================================ FILE: spec/mailers/previews/admin_mailer_preview.rb ================================================ # Preview all emails at http://localhost:3000/rails/mailers/admin_mailer class AdminMailerPreview < ActionMailer::Preview # Preview this email at http://localhost:3000/rails/mailers/admin_mailer/new_pending_account def new_pending_account AdminMailer.new_pending_account(Account.first, User.pending.first) end end ================================================ FILE: spec/mailers/previews/notification_mailer_preview.rb ================================================ # Preview all emails at http://localhost:3000/rails/mailers/notification_mailer class NotificationMailerPreview < ActionMailer::Preview # Preview this email at http://localhost:3000/rails/mailers/notification_mailer/mention def mention m = Mention.last NotificationMailer.mention(m.account, Notification.find_by(activity: m)) end # Preview this email at http://localhost:3000/rails/mailers/notification_mailer/follow def follow f = Follow.last NotificationMailer.follow(f.target_account, Notification.find_by(activity: f)) end # Preview this email at http://localhost:3000/rails/mailers/notification_mailer/follow_request def follow_request f = Follow.last NotificationMailer.follow_request(f.target_account, Notification.find_by(activity: f)) end # Preview this email at http://localhost:3000/rails/mailers/notification_mailer/favourite def favourite f = Favourite.last NotificationMailer.favourite(f.status.account, Notification.find_by(activity: f)) end # Preview this email at http://localhost:3000/rails/mailers/notification_mailer/reblog def reblog r = Status.where.not(reblog_of_id: nil).first NotificationMailer.reblog(r.reblog.account, Notification.find_by(activity: r)) end # Preview this email at http://localhost:3000/rails/mailers/notification_mailer/digest def digest NotificationMailer.digest(Account.first, since: 90.days.ago) end end ================================================ FILE: spec/mailers/previews/user_mailer_preview.rb ================================================ # Preview all emails at http://localhost:3000/rails/mailers/user_mailer class UserMailerPreview < ActionMailer::Preview # Preview this email at http://localhost:3000/rails/mailers/user_mailer/confirmation_instructions def confirmation_instructions UserMailer.confirmation_instructions(User.first, 'spec') end # Preview this email at http://localhost:3000/rails/mailers/user_mailer/email_changed def email_changed user = User.first user.unconfirmed_email = 'foo@bar.com' UserMailer.email_changed(user) end # Preview this email at http://localhost:3000/rails/mailers/user_mailer/password_change def password_change UserMailer.password_change(User.first) end # Preview this email at http://localhost:3000/rails/mailers/user_mailer/reconfirmation_instructions def reconfirmation_instructions user = User.first user.unconfirmed_email = 'foo@bar.com' UserMailer.confirmation_instructions(user, 'spec') end # Preview this email at http://localhost:3000/rails/mailers/user_mailer/reset_password_instructions def reset_password_instructions UserMailer.reset_password_instructions(User.first, 'spec') end # Preview this email at http://localhost:3000/rails/mailers/user_mailer/welcome def welcome UserMailer.welcome(User.first) end # Preview this email at http://localhost:3000/rails/mailers/user_mailer/backup_ready def backup_ready UserMailer.backup_ready(User.first, Backup.first) end # Preview this email at http://localhost:3000/rails/mailers/user_mailer/warning def warning UserMailer.warning(User.first, AccountWarning.new(text: '', action: :silence)) end end ================================================ FILE: spec/mailers/user_mailer_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe UserMailer, type: :mailer do let(:receiver) { Fabricate(:user) } shared_examples 'localized subject' do |*args, **kwrest| it 'renders subject localized for the locale of the receiver' do locale = I18n.available_locales.sample receiver.update!(locale: locale) expect(mail.subject).to eq I18n.t(*args, kwrest.merge(locale: locale)) end it 'renders subject localized for the default locale if the locale of the receiver is unavailable' do receiver.update!(locale: nil) expect(mail.subject).to eq I18n.t(*args, kwrest.merge(locale: I18n.default_locale)) end end describe 'confirmation_instructions' do let(:mail) { UserMailer.confirmation_instructions(receiver, 'spec') } it 'renders confirmation instructions' do receiver.update!(locale: nil) expect(mail.body.encoded).to include I18n.t('devise.mailer.confirmation_instructions.title') expect(mail.body.encoded).to include 'spec' expect(mail.body.encoded).to include Rails.configuration.x.local_domain end include_examples 'localized subject', 'devise.mailer.confirmation_instructions.subject', instance: Rails.configuration.x.local_domain end describe 'reconfirmation_instructions' do let(:mail) { UserMailer.confirmation_instructions(receiver, 'spec') } it 'renders reconfirmation instructions' do receiver.update!(email: 'new-email@example.com', locale: nil) expect(mail.body.encoded).to include I18n.t('devise.mailer.reconfirmation_instructions.title') expect(mail.body.encoded).to include 'spec' expect(mail.body.encoded).to include Rails.configuration.x.local_domain expect(mail.subject).to eq I18n.t('devise.mailer.reconfirmation_instructions.subject', instance: Rails.configuration.x.local_domain, locale: I18n.default_locale) end end describe 'reset_password_instructions' do let(:mail) { UserMailer.reset_password_instructions(receiver, 'spec') } it 'renders reset password instructions' do receiver.update!(locale: nil) expect(mail.body.encoded).to include I18n.t('devise.mailer.reset_password_instructions.title') expect(mail.body.encoded).to include 'spec' end include_examples 'localized subject', 'devise.mailer.reset_password_instructions.subject' end describe 'password_change' do let(:mail) { UserMailer.password_change(receiver) } it 'renders password change notification' do receiver.update!(locale: nil) expect(mail.body.encoded).to include I18n.t('devise.mailer.password_change.title') end include_examples 'localized subject', 'devise.mailer.password_change.subject' end describe 'email_changed' do let(:mail) { UserMailer.email_changed(receiver) } it 'renders email change notification' do receiver.update!(locale: nil) expect(mail.body.encoded).to include I18n.t('devise.mailer.email_changed.title') end include_examples 'localized subject', 'devise.mailer.email_changed.subject' end end ================================================ FILE: spec/models/account_conversation_spec.rb ================================================ require 'rails_helper' RSpec.describe AccountConversation, type: :model do let!(:alice) { Fabricate(:account, username: 'alice') } let!(:bob) { Fabricate(:account, username: 'bob') } let!(:mark) { Fabricate(:account, username: 'mark') } describe '.add_status' do it 'creates new record when no others exist' do status = Fabricate(:status, account: alice, visibility: :direct) status.mentions.create(account: bob) conversation = AccountConversation.add_status(alice, status) expect(conversation.participant_accounts).to include(bob) expect(conversation.last_status).to eq status expect(conversation.status_ids).to eq [status.id] end it 'appends to old record when there is a match' do last_status = Fabricate(:status, account: alice, visibility: :direct) conversation = AccountConversation.create!(account: alice, conversation: last_status.conversation, participant_account_ids: [bob.id], status_ids: [last_status.id]) status = Fabricate(:status, account: bob, visibility: :direct, thread: last_status) status.mentions.create(account: alice) new_conversation = AccountConversation.add_status(alice, status) expect(new_conversation.id).to eq conversation.id expect(new_conversation.participant_accounts).to include(bob) expect(new_conversation.last_status).to eq status expect(new_conversation.status_ids).to eq [last_status.id, status.id] end it 'creates new record when new participants are added' do last_status = Fabricate(:status, account: alice, visibility: :direct) conversation = AccountConversation.create!(account: alice, conversation: last_status.conversation, participant_account_ids: [bob.id], status_ids: [last_status.id]) status = Fabricate(:status, account: bob, visibility: :direct, thread: last_status) status.mentions.create(account: alice) status.mentions.create(account: mark) new_conversation = AccountConversation.add_status(alice, status) expect(new_conversation.id).to_not eq conversation.id expect(new_conversation.participant_accounts).to include(bob, mark) expect(new_conversation.last_status).to eq status expect(new_conversation.status_ids).to eq [status.id] end end describe '.remove_status' do it 'updates last status to a previous value' do last_status = Fabricate(:status, account: alice, visibility: :direct) status = Fabricate(:status, account: alice, visibility: :direct) conversation = AccountConversation.create!(account: alice, conversation: last_status.conversation, participant_account_ids: [bob.id], status_ids: [status.id, last_status.id]) last_status.mentions.create(account: bob) last_status.destroy! conversation.reload expect(conversation.last_status).to eq status expect(conversation.status_ids).to eq [status.id] end it 'removes the record if no other statuses are referenced' do last_status = Fabricate(:status, account: alice, visibility: :direct) conversation = AccountConversation.create!(account: alice, conversation: last_status.conversation, participant_account_ids: [bob.id], status_ids: [last_status.id]) last_status.mentions.create(account: bob) last_status.destroy! expect(AccountConversation.where(id: conversation.id).count).to eq 0 end end end ================================================ FILE: spec/models/account_domain_block_spec.rb ================================================ require 'rails_helper' RSpec.describe AccountDomainBlock, type: :model do it 'removes blocking cache after creation' do account = Fabricate(:account) Rails.cache.write("exclude_domains_for:#{account.id}", 'a.domain.already.blocked') AccountDomainBlock.create!(account: account, domain: 'a.domain.blocked.later') expect(Rails.cache.exist?("exclude_domains_for:#{account.id}")).to eq false end it 'removes blocking cache after destruction' do account = Fabricate(:account) block = AccountDomainBlock.create!(account: account, domain: 'domain') Rails.cache.write("exclude_domains_for:#{account.id}", 'domain') block.destroy! expect(Rails.cache.exist?("exclude_domains_for:#{account.id}")).to eq false end end ================================================ FILE: spec/models/account_filter_spec.rb ================================================ require 'rails_helper' describe AccountFilter do describe 'with empty params' do it 'defaults to recent local not-suspended account list' do filter = described_class.new({}) expect(filter.results).to eq Account.local.recent.without_suspended end end describe 'with invalid params' do it 'raises with key error' do filter = described_class.new(wrong: true) expect { filter.results }.to raise_error(/wrong/) end end describe 'with valid params' do it 'combines filters on Account' do filter = described_class.new( by_domain: 'test.com', silenced: true, username: 'test', display_name: 'name', email: 'user@example.com', ) allow(Account).to receive(:where).and_return(Account.none) allow(Account).to receive(:silenced).and_return(Account.none) allow(Account).to receive(:matches_display_name).and_return(Account.none) allow(Account).to receive(:matches_username).and_return(Account.none) allow(User).to receive(:matches_email).and_return(User.none) filter.results expect(Account).to have_received(:where).with(domain: 'test.com') expect(Account).to have_received(:silenced) expect(Account).to have_received(:matches_username).with('test') expect(Account).to have_received(:matches_display_name).with('name') expect(User).to have_received(:matches_email).with('user@example.com') end describe 'that call account methods' do %i(local remote silenced suspended).each do |option| it "delegates the #{option} option" do allow(Account).to receive(option).and_return(Account.none) filter = described_class.new({ option => true }) filter.results expect(Account).to have_received(option).at_least(1) end end end end end ================================================ FILE: spec/models/account_moderation_note_spec.rb ================================================ require 'rails_helper' RSpec.describe AccountModerationNote, type: :model do end ================================================ FILE: spec/models/account_spec.rb ================================================ require 'rails_helper' RSpec.describe Account, type: :model do context do let(:bob) { Fabricate(:account, username: 'bob') } subject { Fabricate(:account) } describe '#follow!' do it 'creates a follow' do follow = subject.follow!(bob) expect(follow).to be_instance_of Follow expect(follow.account).to eq subject expect(follow.target_account).to eq bob end end describe '#unfollow!' do before do subject.follow!(bob) end it 'destroys a follow' do unfollow = subject.unfollow!(bob) expect(unfollow).to be_instance_of Follow expect(unfollow.account).to eq subject expect(unfollow.target_account).to eq bob expect(unfollow.destroyed?).to be true end end describe '#following?' do it 'returns true when the target is followed' do subject.follow!(bob) expect(subject.following?(bob)).to be true end it 'returns false if the target is not followed' do expect(subject.following?(bob)).to be false end end end describe '#local?' do it 'returns true when the account is local' do account = Fabricate(:account, domain: nil) expect(account.local?).to be true end it 'returns false when the account is on a different domain' do account = Fabricate(:account, domain: 'foreign.tld') expect(account.local?).to be false end end describe 'Local domain user methods' do around do |example| before = Rails.configuration.x.local_domain example.run Rails.configuration.x.local_domain = before end subject { Fabricate(:account, domain: nil, username: 'alice') } describe '#to_webfinger_s' do it 'returns a webfinger string for the account' do Rails.configuration.x.local_domain = 'example.com' expect(subject.to_webfinger_s).to eq 'acct:alice@example.com' end end describe '#local_username_and_domain' do it 'returns the username and local domain for the account' do Rails.configuration.x.local_domain = 'example.com' expect(subject.local_username_and_domain).to eq 'alice@example.com' end end end describe '#acct' do it 'returns username for local users' do account = Fabricate(:account, domain: nil, username: 'alice') expect(account.acct).to eql 'alice' end it 'returns username@domain for foreign users' do account = Fabricate(:account, domain: 'foreign.tld', username: 'alice') expect(account.acct).to eql 'alice@foreign.tld' end end describe '#save_with_optional_media!' do before do stub_request(:get, 'https://remote.test/valid_avatar').to_return(request_fixture('avatar.txt')) stub_request(:get, 'https://remote.test/invalid_avatar').to_return(request_fixture('feed.txt')) end let(:account) do Fabricate(:account, avatar_remote_url: 'https://remote.test/valid_avatar', header_remote_url: 'https://remote.test/valid_avatar') end let!(:expectation) { account.dup } context 'with valid properties' do before do account.save_with_optional_media! end it 'unchanges avatar, header, avatar_remote_url, and header_remote_url' do expect(account.avatar_remote_url).to eq expectation.avatar_remote_url expect(account.header_remote_url).to eq expectation.header_remote_url expect(account.avatar_file_name).to eq expectation.avatar_file_name expect(account.header_file_name).to eq expectation.header_file_name end end context 'with invalid properties' do before do account.avatar_remote_url = 'https://remote.test/invalid_avatar' account.save_with_optional_media! end it 'sets default avatar, header, avatar_remote_url, and header_remote_url' do expect(account.avatar_remote_url).to eq '' expect(account.header_remote_url).to eq '' expect(account.avatar_file_name).to eq nil expect(account.header_file_name).to eq nil end end end describe '#subscribed?' do it 'returns false when no subscription expiration information is present' do account = Fabricate(:account, subscription_expires_at: nil) expect(account.subscribed?).to be false end it 'returns true when subscription expiration has been set' do account = Fabricate(:account, subscription_expires_at: 30.days.from_now) expect(account.subscribed?).to be true end end describe '#possibly_stale?' do let(:account) { Fabricate(:account, last_webfingered_at: last_webfingered_at) } context 'last_webfingered_at is nil' do let(:last_webfingered_at) { nil } it 'returns true' do expect(account.possibly_stale?).to be true end end context 'last_webfingered_at is more than 24 hours before' do let(:last_webfingered_at) { 25.hours.ago } it 'returns true' do expect(account.possibly_stale?).to be true end end context 'last_webfingered_at is less than 24 hours before' do let(:last_webfingered_at) { 23.hours.ago } it 'returns false' do expect(account.possibly_stale?).to be false end end end describe '#refresh!' do let(:account) { Fabricate(:account, domain: domain) } let(:acct) { account.acct } context 'domain is nil' do let(:domain) { nil } it 'returns nil' do expect(account.refresh!).to be_nil end it 'calls not ResolveAccountService#call' do expect_any_instance_of(ResolveAccountService).not_to receive(:call).with(acct) account.refresh! end end context 'domain is present' do let(:domain) { 'example.com' } it 'calls ResolveAccountService#call' do expect_any_instance_of(ResolveAccountService).to receive(:call).with(acct).once account.refresh! end end end describe '#to_param' do it 'returns username' do account = Fabricate(:account, username: 'alice') expect(account.to_param).to eq 'alice' end end describe '#keypair' do it 'returns an RSA key pair' do account = Fabricate(:account) expect(account.keypair).to be_instance_of OpenSSL::PKey::RSA end end describe '#subscription' do it 'returns an OStatus subscription' do account = Fabricate(:account) expect(account.subscription('')).to be_instance_of OStatus2::Subscription end end describe '#object_type' do it 'is always a person' do account = Fabricate(:account) expect(account.object_type).to be :person end end describe '#favourited?' do let(:original_status) do author = Fabricate(:account, username: 'original') Fabricate(:status, account: author) end subject { Fabricate(:account) } context 'when the status is a reblog of another status' do let(:original_reblog) do author = Fabricate(:account, username: 'original_reblogger') Fabricate(:status, reblog: original_status, account: author) end it 'is is true when this account has favourited it' do Fabricate(:favourite, status: original_reblog, account: subject) expect(subject.favourited?(original_status)).to eq true end it 'is false when this account has not favourited it' do expect(subject.favourited?(original_status)).to eq false end end context 'when the status is an original status' do it 'is is true when this account has favourited it' do Fabricate(:favourite, status: original_status, account: subject) expect(subject.favourited?(original_status)).to eq true end it 'is false when this account has not favourited it' do expect(subject.favourited?(original_status)).to eq false end end end describe '#reblogged?' do let(:original_status) do author = Fabricate(:account, username: 'original') Fabricate(:status, account: author) end subject { Fabricate(:account) } context 'when the status is a reblog of another status' do let(:original_reblog) do author = Fabricate(:account, username: 'original_reblogger') Fabricate(:status, reblog: original_status, account: author) end it 'is true when this account has reblogged it' do Fabricate(:status, reblog: original_reblog, account: subject) expect(subject.reblogged?(original_reblog)).to eq true end it 'is false when this account has not reblogged it' do expect(subject.reblogged?(original_reblog)).to eq false end end context 'when the status is an original status' do it 'is true when this account has reblogged it' do Fabricate(:status, reblog: original_status, account: subject) expect(subject.reblogged?(original_status)).to eq true end it 'is false when this account has not reblogged it' do expect(subject.reblogged?(original_status)).to eq false end end end describe '#excluded_from_timeline_account_ids' do it 'includes account ids of blockings, blocked_bys and mutes' do account = Fabricate(:account) block = Fabricate(:block, account: account) mute = Fabricate(:mute, account: account) block_by = Fabricate(:block, target_account: account) results = account.excluded_from_timeline_account_ids expect(results.size).to eq 3 expect(results).to include(block.target_account.id) expect(results).to include(mute.target_account.id) expect(results).to include(block_by.account.id) end end describe '#excluded_from_timeline_domains' do it 'returns the domains blocked by the account' do account = Fabricate(:account) account.block_domain!('domain') expect(account.excluded_from_timeline_domains).to match_array ['domain'] end end describe '.search_for' do before do _missing = Fabricate( :account, display_name: "Missing", username: "missing", domain: "missing.com" ) end it 'accepts ?, \, : and space as delimiter' do match = Fabricate( :account, display_name: 'A & l & i & c & e', username: 'username', domain: 'example.com' ) results = Account.search_for('A?l\i:c e') expect(results).to eq [match] end it 'finds accounts with matching display_name' do match = Fabricate( :account, display_name: "Display Name", username: "username", domain: "example.com" ) results = Account.search_for("display") expect(results).to eq [match] end it 'finds accounts with matching username' do match = Fabricate( :account, display_name: "Display Name", username: "username", domain: "example.com" ) results = Account.search_for("username") expect(results).to eq [match] end it 'finds accounts with matching domain' do match = Fabricate( :account, display_name: "Display Name", username: "username", domain: "example.com" ) results = Account.search_for("example") expect(results).to eq [match] end it 'limits by 10 by default' do 11.times.each { Fabricate(:account, display_name: "Display Name") } results = Account.search_for("display") expect(results.size).to eq 10 end it 'accepts arbitrary limits' do 2.times.each { Fabricate(:account, display_name: "Display Name") } results = Account.search_for("display", 1) expect(results.size).to eq 1 end it 'ranks multiple matches higher' do matches = [ { username: "username", display_name: "username" }, { display_name: "Display Name", username: "username", domain: "example.com" }, ].map(&method(:Fabricate).curry(2).call(:account)) results = Account.search_for("username") expect(results).to eq matches end end describe '.advanced_search_for' do it 'accepts ?, \, : and space as delimiter' do account = Fabricate(:account) match = Fabricate( :account, display_name: 'A & l & i & c & e', username: 'username', domain: 'example.com' ) results = Account.advanced_search_for('A?l\i:c e', account) expect(results).to eq [match] end it 'limits by 10 by default' do 11.times { Fabricate(:account, display_name: "Display Name") } results = Account.search_for("display") expect(results.size).to eq 10 end it 'accepts arbitrary limits' do 2.times { Fabricate(:account, display_name: "Display Name") } results = Account.search_for("display", 1) expect(results.size).to eq 1 end it 'ranks followed accounts higher' do account = Fabricate(:account) match = Fabricate(:account, username: "Matching") followed_match = Fabricate(:account, username: "Matcher") Fabricate(:follow, account: account, target_account: followed_match) results = Account.advanced_search_for("match", account) expect(results).to eq [followed_match, match] expect(results.first.rank).to be > results.last.rank end end describe '.domains' do it 'returns domains' do Fabricate(:account, domain: 'domain') expect(Account.domains).to match_array(['domain']) end end describe '#statuses_count' do subject { Fabricate(:account) } it 'counts statuses' do Fabricate(:status, account: subject) Fabricate(:status, account: subject) expect(subject.statuses_count).to eq 2 end it 'does not count direct statuses' do Fabricate(:status, account: subject, visibility: :direct) expect(subject.statuses_count).to eq 0 end it 'is decremented when status is removed' do status = Fabricate(:status, account: subject) expect(subject.statuses_count).to eq 1 status.destroy expect(subject.statuses_count).to eq 0 end it 'is decremented when status is removed when account is not preloaded' do status = Fabricate(:status, account: subject) expect(subject.reload.statuses_count).to eq 1 clean_status = Status.find(status.id) expect(clean_status.association(:account).loaded?).to be false clean_status.destroy expect(subject.reload.statuses_count).to eq 0 end end describe '.following_map' do it 'returns an hash' do expect(Account.following_map([], 1)).to be_a Hash end end describe '.followed_by_map' do it 'returns an hash' do expect(Account.followed_by_map([], 1)).to be_a Hash end end describe '.blocking_map' do it 'returns an hash' do expect(Account.blocking_map([], 1)).to be_a Hash end end describe '.requested_map' do it 'returns an hash' do expect(Account.requested_map([], 1)).to be_a Hash end end describe 'MENTION_RE' do subject { Account::MENTION_RE } it 'matches usernames in the middle of a sentence' do expect(subject.match('Hello to @alice from me')[1]).to eq 'alice' end it 'matches usernames in the beginning of status' do expect(subject.match('@alice Hey how are you?')[1]).to eq 'alice' end it 'matches full usernames' do expect(subject.match('@alice@example.com')[1]).to eq 'alice@example.com' end it 'matches full usernames with a dot at the end' do expect(subject.match('Hello @alice@example.com.')[1]).to eq 'alice@example.com' end it 'matches dot-prepended usernames' do expect(subject.match('.@alice I want everybody to see this')[1]).to eq 'alice' end it 'does not match e-mails' do expect(subject.match('Drop me an e-mail at alice@example.com')).to be_nil end it 'does not match URLs' do expect(subject.match('Check this out https://medium.com/@alice/some-article#.abcdef123')).to be_nil end xit 'does not match URL querystring' do expect(subject.match('https://example.com/?x=@alice')).to be_nil end end describe 'validations' do it 'has a valid fabricator' do account = Fabricate.build(:account) account.valid? expect(account).to be_valid end it 'is invalid without a username' do account = Fabricate.build(:account, username: nil) account.valid? expect(account).to model_have_error_on_field(:username) end it 'squishes the username before validation' do account = Fabricate(:account, domain: nil, username: " \u3000bob \t \u00a0 \n ") expect(account.username).to eq 'bob' end context 'when is local' do it 'is invalid if the username is not unique in case-insensitive comparison among local accounts' do account_1 = Fabricate(:account, username: 'the_doctor') account_2 = Fabricate.build(:account, username: 'the_Doctor') account_2.valid? expect(account_2).to model_have_error_on_field(:username) end it 'is invalid if the username is reserved' do account = Fabricate.build(:account, username: 'support') account.valid? expect(account).to model_have_error_on_field(:username) end it 'is valid when username is reserved but record has already been created' do account = Fabricate.build(:account, username: 'support') account.save(validate: false) expect(account.valid?).to be true end it 'is invalid if the username doesn\'t only contains letters, numbers and underscores' do account = Fabricate.build(:account, username: 'the-doctor') account.valid? expect(account).to model_have_error_on_field(:username) end it 'is invalid if the username is longer then 30 characters' do account = Fabricate.build(:account, username: Faker::Lorem.characters(31)) account.valid? expect(account).to model_have_error_on_field(:username) end it 'is invalid if the display name is longer than 30 characters' do account = Fabricate.build(:account, display_name: Faker::Lorem.characters(31)) account.valid? expect(account).to model_have_error_on_field(:display_name) end it 'is invalid if the note is longer than 500 characters' do account = Fabricate.build(:account, note: Faker::Lorem.characters(501)) account.valid? expect(account).to model_have_error_on_field(:note) end end context 'when is remote' do it 'is invalid if the username is not unique in case-sensitive comparison among accounts in the same normalized domain' do Fabricate(:account, domain: 'にゃん', username: 'username') account = Fabricate.build(:account, domain: 'xn--r9j5b5b', username: 'username') account.valid? expect(account).to model_have_error_on_field(:username) end it 'is valid even if the username is unique only in case-sensitive comparison among accounts in the same normalized domain' do Fabricate(:account, domain: 'にゃん', username: 'username') account = Fabricate.build(:account, domain: 'xn--r9j5b5b', username: 'Username') account.valid? expect(account).not_to model_have_error_on_field(:username) end it 'is valid even if the username contains hyphens' do account = Fabricate.build(:account, domain: 'domain', username: 'the-doctor') account.valid? expect(account).to_not model_have_error_on_field(:username) end it 'is invalid if the username doesn\'t only contains letters, numbers, underscores and hyphens' do account = Fabricate.build(:account, domain: 'domain', username: 'the doctor') account.valid? expect(account).to model_have_error_on_field(:username) end it 'is valid even if the username is longer then 30 characters' do account = Fabricate.build(:account, domain: 'domain', username: Faker::Lorem.characters(31)) account.valid? expect(account).not_to model_have_error_on_field(:username) end it 'is valid even if the display name is longer than 30 characters' do account = Fabricate.build(:account, domain: 'domain', display_name: Faker::Lorem.characters(31)) account.valid? expect(account).not_to model_have_error_on_field(:display_name) end it 'is valid even if the note is longer than 500 characters' do account = Fabricate.build(:account, domain: 'domain', note: Faker::Lorem.characters(501)) account.valid? expect(account).not_to model_have_error_on_field(:note) end end end describe 'scopes' do describe 'alphabetic' do it 'sorts by alphabetic order of domain and username' do matches = [ { username: 'a', domain: 'a' }, { username: 'b', domain: 'a' }, { username: 'a', domain: 'b' }, { username: 'b', domain: 'b' }, ].map(&method(:Fabricate).curry(2).call(:account)) expect(Account.alphabetic).to eq matches end end describe 'matches_display_name' do it 'matches display name which starts with the given string' do match = Fabricate(:account, display_name: 'pattern and suffix') Fabricate(:account, display_name: 'prefix and pattern') expect(Account.matches_display_name('pattern')).to eq [match] end end describe 'matches_username' do it 'matches display name which starts with the given string' do match = Fabricate(:account, username: 'pattern_and_suffix') Fabricate(:account, username: 'prefix_and_pattern') expect(Account.matches_username('pattern')).to eq [match] end end describe 'expiring' do it 'returns remote accounts with followers whose subscription expiration date is past or not given' do local = Fabricate(:account, domain: nil) matches = [ { domain: 'remote', subscription_expires_at: '2000-01-01T00:00:00Z' }, ].map(&method(:Fabricate).curry(2).call(:account)) matches.each(&local.method(:follow!)) Fabricate(:account, domain: 'remote', subscription_expires_at: nil) local.follow!(Fabricate(:account, domain: 'remote', subscription_expires_at: '2000-01-03T00:00:00Z')) local.follow!(Fabricate(:account, domain: nil, subscription_expires_at: nil)) expect(Account.expiring('2000-01-02T00:00:00Z').recent).to eq matches.reverse end end describe 'remote' do it 'returns an array of accounts who have a domain' do account_1 = Fabricate(:account, domain: nil) account_2 = Fabricate(:account, domain: 'example.com') expect(Account.remote).to match_array([account_2]) end end describe 'by_domain_accounts' do it 'returns accounts grouped by domain sorted by accounts' do 2.times { Fabricate(:account, domain: 'example.com') } Fabricate(:account, domain: 'example2.com') results = Account.by_domain_accounts expect(results.length).to eq 2 expect(results.first.domain).to eq 'example.com' expect(results.first.accounts_count).to eq 2 expect(results.last.domain).to eq 'example2.com' expect(results.last.accounts_count).to eq 1 end end describe 'local' do it 'returns an array of accounts who do not have a domain' do account_1 = Fabricate(:account, domain: nil) account_2 = Fabricate(:account, domain: 'example.com') expect(Account.local).to match_array([account_1]) end end describe 'partitioned' do it 'returns a relation of accounts partitioned by domain' do matches = ['a', 'b', 'a', 'b'] matches.size.times.to_a.shuffle.each do |index| matches[index] = Fabricate(:account, domain: matches[index]) end expect(Account.partitioned).to match_array(matches) end end describe 'recent' do it 'returns a relation of accounts sorted by recent creation' do matches = 2.times.map { Fabricate(:account) } expect(Account.recent).to match_array(matches) end end describe 'silenced' do it 'returns an array of accounts who are silenced' do account_1 = Fabricate(:account, silenced: true) account_2 = Fabricate(:account, silenced: false) expect(Account.silenced).to match_array([account_1]) end end describe 'suspended' do it 'returns an array of accounts who are suspended' do account_1 = Fabricate(:account, suspended: true) account_2 = Fabricate(:account, suspended: false) expect(Account.suspended).to match_array([account_1]) end end end context 'when is local' do # Test disabled because test environment omits autogenerating keys for performance xit 'generates keys' do account = Account.create!(domain: nil, username: Faker::Internet.user_name(nil, ['_'])) expect(account.keypair.private?).to eq true end end context 'when is remote' do it 'does not generate keys' do key = OpenSSL::PKey::RSA.new(1024).public_key account = Account.create!(domain: 'remote', username: Faker::Internet.user_name(nil, ['_']), public_key: key.to_pem) expect(account.keypair.params).to eq key.params end it 'normalizes domain' do account = Account.create!(domain: 'にゃん', username: Faker::Internet.user_name(nil, ['_'])) expect(account.domain).to eq 'xn--r9j5b5b' end end include_examples 'AccountAvatar', :account end ================================================ FILE: spec/models/account_stat_spec.rb ================================================ require 'rails_helper' RSpec.describe AccountStat, type: :model do end ================================================ FILE: spec/models/account_tag_stat_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' RSpec.describe AccountTagStat, type: :model do key = 'accounts_count' let(:account_tag_stat) { Fabricate(:tag).account_tag_stat } describe '#increment_count!' do it 'calls #update' do args = { key => account_tag_stat.public_send(key) + 1 } expect(account_tag_stat).to receive(:update).with(args) account_tag_stat.increment_count!(key) end it 'increments value by 1' do expect do account_tag_stat.increment_count!(key) end.to change { account_tag_stat.accounts_count }.by(1) end end describe '#decrement_count!' do it 'calls #update' do args = { key => [account_tag_stat.public_send(key) - 1, 0].max } expect(account_tag_stat).to receive(:update).with(args) account_tag_stat.decrement_count!(key) end it 'decrements value by 1' do account_tag_stat.update(key => 1) expect do account_tag_stat.decrement_count!(key) end.to change { account_tag_stat.accounts_count }.by(-1) end end end ================================================ FILE: spec/models/admin/account_action_spec.rb ================================================ require 'rails_helper' RSpec.describe Admin::AccountAction, type: :model do let(:account_action) { described_class.new } describe '#save!' do subject { account_action.save! } let(:account) { Fabricate(:account, user: Fabricate(:user, admin: true)) } let(:target_account) { Fabricate(:account, user: Fabricate(:user)) } let(:type) { 'disable' } before do account_action.assign_attributes( type: type, current_account: account, target_account: target_account ) end context 'type is "disable"' do let(:type) { 'disable' } it 'disable user' do subject expect(target_account.user).to be_disabled end end context 'type is "silence"' do let(:type) { 'silence' } it 'silences account' do subject expect(target_account).to be_silenced end end context 'type is "suspend"' do let(:type) { 'suspend' } it 'suspends account' do subject expect(target_account).to be_suspended end it 'queues Admin::SuspensionWorker by 1' do Sidekiq::Testing.fake! do expect do subject end.to change { Admin::SuspensionWorker.jobs.size }.by 1 end end end it 'creates Admin::ActionLog' do expect do subject end.to change { Admin::ActionLog.count }.by 1 end it 'calls queue_email!' do expect(account_action).to receive(:queue_email!) subject end it 'calls process_reports!' do expect(account_action).to receive(:process_reports!) subject end end describe '#report' do subject { account_action.report } context 'report_id.present?' do before do account_action.report_id = Fabricate(:report).id end it 'returns Report' do expect(subject).to be_instance_of Report end end context '!report_id.present?' do it 'returns nil' do expect(subject).to be_nil end end end describe '#with_report?' do subject { account_action.with_report? } context '!report.nil?' do before do account_action.report_id = Fabricate(:report).id end it 'returns true' do expect(subject).to be true end end context '!(!report.nil?)' do it 'returns false' do expect(subject).to be false end end end describe '.types_for_account' do subject { described_class.types_for_account(account) } context 'account.local?' do let(:account) { Fabricate(:account, domain: nil) } it 'returns ["none", "disable", "silence", "suspend"]' do expect(subject).to eq %w(none disable silence suspend) end end context '!account.local?' do let(:account) { Fabricate(:account, domain: 'hoge.com') } it 'returns ["silence", "suspend"]' do expect(subject).to eq %w(silence suspend) end end end end ================================================ FILE: spec/models/admin/action_log_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' RSpec.describe Admin::ActionLog, type: :model do describe '#action' do it 'returns action' do action_log = described_class.new(action: 'hoge') expect(action_log.action).to be :hoge end end end ================================================ FILE: spec/models/backup_spec.rb ================================================ require 'rails_helper' RSpec.describe Backup, type: :model do end ================================================ FILE: spec/models/block_spec.rb ================================================ require 'rails_helper' RSpec.describe Block, type: :model do describe 'validations' do it 'has a valid fabricator' do block = Fabricate.build(:block) expect(block).to be_valid end it 'is invalid without an account' do block = Fabricate.build(:block, account: nil) block.valid? expect(block).to model_have_error_on_field(:account) end it 'is invalid without a target_account' do block = Fabricate.build(:block, target_account: nil) block.valid? expect(block).to model_have_error_on_field(:target_account) end end it 'removes blocking cache after creation' do account = Fabricate(:account) target_account = Fabricate(:account) Rails.cache.write("exclude_account_ids_for:#{account.id}", []) Rails.cache.write("exclude_account_ids_for:#{target_account.id}", []) Block.create!(account: account, target_account: target_account) expect(Rails.cache.exist?("exclude_account_ids_for:#{account.id}")).to eq false expect(Rails.cache.exist?("exclude_account_ids_for:#{target_account.id}")).to eq false end it 'removes blocking cache after destruction' do account = Fabricate(:account) target_account = Fabricate(:account) block = Block.create!(account: account, target_account: target_account) Rails.cache.write("exclude_account_ids_for:#{account.id}", [target_account.id]) Rails.cache.write("exclude_account_ids_for:#{target_account.id}", [account.id]) block.destroy! expect(Rails.cache.exist?("exclude_account_ids_for:#{account.id}")).to eq false expect(Rails.cache.exist?("exclude_account_ids_for:#{target_account.id}")).to eq false end end ================================================ FILE: spec/models/concerns/account_finder_concern_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe AccountFinderConcern do describe 'local finders' do before do @account = Fabricate(:account, username: 'Alice') end describe '.find_local' do it 'returns case-insensitive result' do expect(Account.find_local('alice')).to eq(@account) end it 'returns correctly cased result' do expect(Account.find_local('Alice')).to eq(@account) end it 'returns nil without a match' do expect(Account.find_local('a_ice')).to be_nil end it 'returns nil for regex style username value' do expect(Account.find_local('al%')).to be_nil end it 'returns nil for nil username value' do expect(Account.find_local(nil)).to be_nil end it 'returns nil for blank username value' do expect(Account.find_local('')).to be_nil end end describe '.find_local!' do it 'returns matching result' do expect(Account.find_local!('alice')).to eq(@account) end it 'raises on non-matching result' do expect { Account.find_local!('missing') }.to raise_error(ActiveRecord::RecordNotFound) end it 'raises with blank username' do expect { Account.find_local!('') }.to raise_error(ActiveRecord::RecordNotFound) end it 'raises with nil username' do expect { Account.find_local!(nil) }.to raise_error(ActiveRecord::RecordNotFound) end end end describe 'remote finders' do before do @account = Fabricate(:account, username: 'Alice', domain: 'mastodon.social') end describe '.find_remote' do it 'returns exact match result' do expect(Account.find_remote('alice', 'mastodon.social')).to eq(@account) end it 'returns case-insensitive result' do expect(Account.find_remote('ALICE', 'MASTODON.SOCIAL')).to eq(@account) end it 'returns nil when username does not match' do expect(Account.find_remote('a_ice', 'mastodon.social')).to be_nil end it 'returns nil when domain does not match' do expect(Account.find_remote('alice', 'm_stodon.social')).to be_nil end it 'returns nil for regex style domain value' do expect(Account.find_remote('alice', 'm%')).to be_nil end it 'returns nil for nil username value' do expect(Account.find_remote(nil, 'domain')).to be_nil end it 'returns nil for blank username value' do expect(Account.find_remote('', 'domain')).to be_nil end end describe '.find_remote!' do it 'returns matching result' do expect(Account.find_remote!('alice', 'mastodon.social')).to eq(@account) end it 'raises on non-matching result' do expect { Account.find_remote!('missing', 'mastodon.host') }.to raise_error(ActiveRecord::RecordNotFound) end it 'raises with blank username' do expect { Account.find_remote!('', '') }.to raise_error(ActiveRecord::RecordNotFound) end it 'raises with nil username' do expect { Account.find_remote!(nil, nil) }.to raise_error(ActiveRecord::RecordNotFound) end end end end ================================================ FILE: spec/models/concerns/account_interactions_spec.rb ================================================ require 'rails_helper' describe AccountInteractions do let(:account) { Fabricate(:account, username: 'account') } let(:account_id) { account.id } let(:account_ids) { [account_id] } let(:target_account) { Fabricate(:account, username: 'target') } let(:target_account_id) { target_account.id } let(:target_account_ids) { [target_account_id] } describe '.following_map' do subject { Account.following_map(target_account_ids, account_id) } context 'account with Follow' do it 'returns { target_account_id => true }' do Fabricate(:follow, account: account, target_account: target_account) is_expected.to eq(target_account_id => { reblogs: true }) end end context 'account without Follow' do it 'returns {}' do is_expected.to eq({}) end end end describe '.followed_by_map' do subject { Account.followed_by_map(target_account_ids, account_id) } context 'account with Follow' do it 'returns { target_account_id => true }' do Fabricate(:follow, account: target_account, target_account: account) is_expected.to eq(target_account_id => true) end end context 'account without Follow' do it 'returns {}' do is_expected.to eq({}) end end end describe '.blocking_map' do subject { Account.blocking_map(target_account_ids, account_id) } context 'account with Block' do it 'returns { target_account_id => true }' do Fabricate(:block, account: account, target_account: target_account) is_expected.to eq(target_account_id => true) end end context 'account without Block' do it 'returns {}' do is_expected.to eq({}) end end end describe '.muting_map' do subject { Account.muting_map(target_account_ids, account_id) } context 'account with Mute' do before do Fabricate(:mute, target_account: target_account, account: account, hide_notifications: hide) end context 'if Mute#hide_notifications?' do let(:hide) { true } it 'returns { target_account_id => { notifications: true } }' do is_expected.to eq(target_account_id => { notifications: true }) end end context 'unless Mute#hide_notifications?' do let(:hide) { false } it 'returns { target_account_id => { notifications: false } }' do is_expected.to eq(target_account_id => { notifications: false }) end end end context 'account without Mute' do it 'returns {}' do is_expected.to eq({}) end end end describe '#follow!' do it 'creates and returns Follow' do expect do expect(account.follow!(target_account)).to be_kind_of Follow end.to change { account.following.count }.by 1 end end describe '#block' do it 'creates and returns Block' do expect do expect(account.block!(target_account)).to be_kind_of Block end.to change { account.block_relationships.count }.by 1 end end describe '#mute!' do subject { account.mute!(target_account, notifications: arg_notifications) } context 'Mute does not exist yet' do context 'arg :notifications is nil' do let(:arg_notifications) { nil } it 'creates Mute, and returns Mute' do expect do expect(subject).to be_kind_of Mute end.to change { account.mute_relationships.count }.by 1 end end context 'arg :notifications is false' do let(:arg_notifications) { false } it 'creates Mute, and returns Mute' do expect do expect(subject).to be_kind_of Mute end.to change { account.mute_relationships.count }.by 1 end end context 'arg :notifications is true' do let(:arg_notifications) { true } it 'creates Mute, and returns Mute' do expect do expect(subject).to be_kind_of Mute end.to change { account.mute_relationships.count }.by 1 end end end context 'Mute already exists' do before do account.mute_relationships << mute end let(:mute) do Fabricate(:mute, account: account, target_account: target_account, hide_notifications: hide_notifications) end context 'mute.hide_notifications is true' do let(:hide_notifications) { true } context 'arg :notifications is nil' do let(:arg_notifications) { nil } it 'returns Mute without updating mute.hide_notifications' do expect do expect(subject).to be_kind_of Mute end.not_to change { mute.reload.hide_notifications? }.from(true) end end context 'arg :notifications is false' do let(:arg_notifications) { false } it 'returns Mute, and updates mute.hide_notifications false' do expect do expect(subject).to be_kind_of Mute end.to change { mute.reload.hide_notifications? }.from(true).to(false) end end context 'arg :notifications is true' do let(:arg_notifications) { true } it 'returns Mute without updating mute.hide_notifications' do expect do expect(subject).to be_kind_of Mute end.not_to change { mute.reload.hide_notifications? }.from(true) end end end context 'mute.hide_notifications is false' do let(:hide_notifications) { false } context 'arg :notifications is nil' do let(:arg_notifications) { nil } it 'returns Mute, and updates mute.hide_notifications true' do expect do expect(subject).to be_kind_of Mute end.to change { mute.reload.hide_notifications? }.from(false).to(true) end end context 'arg :notifications is false' do let(:arg_notifications) { false } it 'returns Mute without updating mute.hide_notifications' do expect do expect(subject).to be_kind_of Mute end.not_to change { mute.reload.hide_notifications? }.from(false) end end context 'arg :notifications is true' do let(:arg_notifications) { true } it 'returns Mute, and updates mute.hide_notifications true' do expect do expect(subject).to be_kind_of Mute end.to change { mute.reload.hide_notifications? }.from(false).to(true) end end end end end describe '#mute_conversation!' do let(:conversation) { Fabricate(:conversation) } subject { account.mute_conversation!(conversation) } it 'creates and returns ConversationMute' do expect do is_expected.to be_kind_of ConversationMute end.to change { account.conversation_mutes.count }.by 1 end end describe '#block_domain!' do let(:domain) { 'example.com' } subject { account.block_domain!(domain) } it 'creates and returns AccountDomainBlock' do expect do is_expected.to be_kind_of AccountDomainBlock end.to change { account.domain_blocks.count }.by 1 end end describe '#unfollow!' do subject { account.unfollow!(target_account) } context 'following target_account' do it 'returns destroyed Follow' do account.active_relationships.create(target_account: target_account) is_expected.to be_kind_of Follow expect(subject).to be_destroyed end end context 'not following target_account' do it 'returns nil' do is_expected.to be_nil end end end describe '#unblock!' do subject { account.unblock!(target_account) } context 'blocking target_account' do it 'returns destroyed Block' do account.block_relationships.create(target_account: target_account) is_expected.to be_kind_of Block expect(subject).to be_destroyed end end context 'not blocking target_account' do it 'returns nil' do is_expected.to be_nil end end end describe '#unmute!' do subject { account.unmute!(target_account) } context 'muting target_account' do it 'returns destroyed Mute' do account.mute_relationships.create(target_account: target_account) is_expected.to be_kind_of Mute expect(subject).to be_destroyed end end context 'not muting target_account' do it 'returns nil' do is_expected.to be_nil end end end describe '#unmute_conversation!' do let(:conversation) { Fabricate(:conversation) } subject { account.unmute_conversation!(conversation) } context 'muting the conversation' do it 'returns destroyed ConversationMute' do account.conversation_mutes.create(conversation: conversation) is_expected.to be_kind_of ConversationMute expect(subject).to be_destroyed end end context 'not muting the conversation' do it 'returns nil' do is_expected.to be nil end end end describe '#unblock_domain!' do let(:domain) { 'example.com' } subject { account.unblock_domain!(domain) } context 'blocking the domain' do it 'returns destroyed AccountDomainBlock' do account_domain_block = Fabricate(:account_domain_block, domain: domain) account.domain_blocks << account_domain_block is_expected.to be_kind_of AccountDomainBlock expect(subject).to be_destroyed end end context 'unblocking the domain' do it 'returns nil' do is_expected.to be_nil end end end describe '#following?' do subject { account.following?(target_account) } context 'following target_account' do it 'returns true' do account.active_relationships.create(target_account: target_account) is_expected.to be true end end context 'not following target_account' do it 'returns false' do is_expected.to be false end end end describe '#blocking?' do subject { account.blocking?(target_account) } context 'blocking target_account' do it 'returns true' do account.block_relationships.create(target_account: target_account) is_expected.to be true end end context 'not blocking target_account' do it 'returns false' do is_expected.to be false end end end describe '#domain_blocking?' do let(:domain) { 'example.com' } subject { account.domain_blocking?(domain) } context 'blocking the domain' do it' returns true' do account_domain_block = Fabricate(:account_domain_block, domain: domain) account.domain_blocks << account_domain_block is_expected.to be true end end context 'not blocking the domain' do it 'returns false' do is_expected.to be false end end end describe '#muting?' do subject { account.muting?(target_account) } context 'muting target_account' do it 'returns true' do mute = Fabricate(:mute, account: account, target_account: target_account) account.mute_relationships << mute is_expected.to be true end end context 'not muting target_account' do it 'returns false' do is_expected.to be false end end end describe '#muting_conversation?' do let(:conversation) { Fabricate(:conversation) } subject { account.muting_conversation?(conversation) } context 'muting the conversation' do it 'returns true' do account.conversation_mutes.create(conversation: conversation) is_expected.to be true end end context 'not muting the conversation' do it 'returns false' do is_expected.to be false end end end describe '#muting_notifications?' do before do mute = Fabricate(:mute, target_account: target_account, account: account, hide_notifications: hide) account.mute_relationships << mute end subject { account.muting_notifications?(target_account) } context 'muting notifications of target_account' do let(:hide) { true } it 'returns true' do is_expected.to be true end end context 'not muting notifications of target_account' do let(:hide) { false } it 'returns false' do is_expected.to be false end end end describe '#requested?' do subject { account.requested?(target_account) } context 'requested by target_account' do it 'returns true' do Fabricate(:follow_request, account: account, target_account: target_account) is_expected.to be true end end context 'not requested by target_account' do it 'returns false' do is_expected.to be false end end end describe '#favourited?' do let(:status) { Fabricate(:status, account: account, favourites: favourites) } subject { account.favourited?(status) } context 'favorited' do let(:favourites) { [Fabricate(:favourite, account: account)] } it 'returns true' do is_expected.to be true end end context 'not favorited' do let(:favourites) { [] } it 'returns false' do is_expected.to be false end end end describe '#reblogged?' do let(:status) { Fabricate(:status, account: account, reblogs: reblogs) } subject { account.reblogged?(status) } context 'reblogged' do let(:reblogs) { [Fabricate(:status, account: account)] } it 'returns true' do is_expected.to be true end end context 'not reblogged' do let(:reblogs) { [] } it 'returns false' do is_expected.to be false end end end describe '#pinned?' do let(:status) { Fabricate(:status, account: account) } subject { account.pinned?(status) } context 'pinned' do it 'returns true' do Fabricate(:status_pin, account: account, status: status) is_expected.to be true end end context 'not pinned' do it 'returns false' do is_expected.to be false end end end describe 'muting an account' do let(:me) { Fabricate(:account, username: 'Me') } let(:you) { Fabricate(:account, username: 'You') } context 'with the notifications option unspecified' do before do me.mute!(you) end it 'defaults to muting notifications' do expect(me.muting_notifications?(you)).to be true end end context 'with the notifications option set to false' do before do me.mute!(you, notifications: false) end it 'does not mute notifications' do expect(me.muting_notifications?(you)).to be false end end context 'with the notifications option set to true' do before do me.mute!(you, notifications: true) end it 'does mute notifications' do expect(me.muting_notifications?(you)).to be true end end end describe 'ignoring reblogs from an account' do before do @me = Fabricate(:account, username: 'Me') @you = Fabricate(:account, username: 'You') end context 'with the reblogs option unspecified' do before do @me.follow!(@you) end it 'defaults to showing reblogs' do expect(@me.muting_reblogs?(@you)).to be(false) end end context 'with the reblogs option set to false' do before do @me.follow!(@you, reblogs: false) end it 'does mute reblogs' do expect(@me.muting_reblogs?(@you)).to be(true) end end context 'with the reblogs option set to true' do before do @me.follow!(@you, reblogs: true) end it 'does not mute reblogs' do expect(@me.muting_reblogs?(@you)).to be(false) end end end end ================================================ FILE: spec/models/concerns/remotable_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' RSpec.describe Remotable do class Foo def initialize @attrs = {} end def [](arg) @attrs[arg] end def []=(arg1, arg2) @attrs[arg1] = arg2 end def hoge=(arg); end def hoge_file_name=(arg); end def has_attribute?(arg); end def self.attachment_definitions { hoge: nil } end end context 'Remotable module is included' do before do class Foo include Remotable remotable_attachment :hoge, 1.kilobyte end end let(:attribute_name) { "#{hoge}_remote_url".to_sym } let(:code) { 200 } let(:file) { 'filename="foo.txt"' } let(:foo) { Foo.new } let(:headers) { { 'content-disposition' => file } } let(:hoge) { :hoge } let(:url) { 'https://google.com' } let(:request) do stub_request(:get, url) .to_return(status: code, headers: headers) end it 'defines a method #hoge_remote_url=' do expect(foo).to respond_to(:hoge_remote_url=) end it 'defines a method #reset_hoge!' do expect(foo).to respond_to(:reset_hoge!) end describe '#hoge_remote_url' do before do request end it 'always returns arg' do [nil, '', [], {}].each do |arg| expect(foo.hoge_remote_url = arg).to be arg end end context 'Addressable::URI::InvalidURIError raised' do it 'makes no request' do allow(Addressable::URI).to receive_message_chain(:parse, :normalize) .with(url).with(no_args).and_raise(Addressable::URI::InvalidURIError) foo.hoge_remote_url = url expect(request).not_to have_been_requested end end context 'scheme is neither http nor https' do let(:url) { 'ftp://google.com' } it 'makes no request' do foo.hoge_remote_url = url expect(request).not_to have_been_requested end end context 'parsed_url.host is empty' do it 'makes no request' do parsed_url = double(scheme: 'https', host: double(blank?: true)) allow(Addressable::URI).to receive_message_chain(:parse, :normalize) .with(url).with(no_args).and_return(parsed_url) foo.hoge_remote_url = url expect(request).not_to have_been_requested end end context 'parsed_url.host is nil' do it 'makes no request' do parsed_url = Addressable::URI.parse('https:https://example.com/path/file.png') allow(Addressable::URI).to receive_message_chain(:parse, :normalize) .with(url).with(no_args).and_return(parsed_url) foo.hoge_remote_url = url expect(request).not_to have_been_requested end end context 'foo[attribute_name] == url' do it 'makes no request' do allow(foo).to receive(:[]).with(attribute_name).and_return(url) foo.hoge_remote_url = url expect(request).not_to have_been_requested end end context "scheme is https, parsed_url.host isn't empty, and foo[attribute_name] != url" do it 'makes a request' do foo.hoge_remote_url = url expect(request).to have_been_requested end context 'response.code != 200' do let(:code) { 500 } it 'calls not send' do expect(foo).not_to receive(:send).with("#{hoge}=", any_args) expect(foo).not_to receive(:send).with("#{hoge}_file_name=", any_args) foo.hoge_remote_url = url end end context 'response.code == 200' do let(:code) { 200 } context 'response contains headers["content-disposition"]' do let(:file) { 'filename="foo.txt"' } let(:headers) { { 'content-disposition' => file } } it 'calls send' do string_io = StringIO.new('') extname = '.txt' basename = '0123456789abcdef' allow(SecureRandom).to receive(:hex).and_return(basename) allow(StringIO).to receive(:new).with(anything).and_return(string_io) expect(foo).to receive(:send).with("#{hoge}=", string_io) expect(foo).to receive(:send).with("#{hoge}_file_name=", basename + extname) foo.hoge_remote_url = url end end context 'if has_attribute?' do it 'calls foo[attribute_name] = url' do allow(foo).to receive(:has_attribute?).with(attribute_name).and_return(true) expect(foo).to receive('[]=').with(attribute_name, url) foo.hoge_remote_url = url end end context 'unless has_attribute?' do it 'calls not foo[attribute_name] = url' do allow(foo).to receive(:has_attribute?) .with(attribute_name).and_return(false) expect(foo).not_to receive('[]=').with(attribute_name, url) foo.hoge_remote_url = url end end end context 'an error raised during the request' do let(:request) { stub_request(:get, url).to_raise(error_class) } error_classes = [ HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, Paperclip::Errors::NotIdentifiedByImageMagickError, Addressable::URI::InvalidURIError, ] error_classes.each do |error_class| let(:error_class) { error_class } it 'calls Rails.logger.debug' do expect(Rails.logger).to receive(:debug).with(/^Error fetching remote #{hoge}: /) foo.hoge_remote_url = url end end end end end describe '#reset_hoge!' do context 'if url.blank?' do it 'returns nil, without clearing foo[attribute_name] and calling #hoge_remote_url=' do url = nil expect(foo).not_to receive(:send).with(:hoge_remote_url=, url) foo[attribute_name] = url expect(foo.reset_hoge!).to be_nil expect(foo[attribute_name]).to be_nil end end context 'unless url.blank?' do it 'clears foo[attribute_name] and calls #hoge_remote_url=' do foo[attribute_name] = url expect(foo).to receive(:send).with(:hoge_remote_url=, url) foo.reset_hoge! expect(foo[attribute_name]).to be '' end end end end end ================================================ FILE: spec/models/concerns/status_threading_concern_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe StatusThreadingConcern do describe '#ancestors' do let!(:alice) { Fabricate(:account, username: 'alice') } let!(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com') } let!(:jeff) { Fabricate(:account, username: 'jeff') } let!(:status) { Fabricate(:status, account: alice) } let!(:reply1) { Fabricate(:status, thread: status, account: jeff) } let!(:reply2) { Fabricate(:status, thread: reply1, account: bob) } let!(:reply3) { Fabricate(:status, thread: reply2, account: alice) } let!(:viewer) { Fabricate(:account, username: 'viewer') } it 'returns conversation history' do expect(reply3.ancestors(4)).to include(status, reply1, reply2) end it 'does not return conversation history user is not allowed to see' do reply1.update(visibility: :private) status.update(visibility: :direct) expect(reply3.ancestors(4, viewer)).to_not include(reply1, status) end it 'does not return conversation history from blocked users' do viewer.block!(jeff) expect(reply3.ancestors(4, viewer)).to_not include(reply1) end it 'does not return conversation history from muted users' do viewer.mute!(jeff) expect(reply3.ancestors(4, viewer)).to_not include(reply1) end it 'does not return conversation history from silenced and not followed users' do jeff.silence! expect(reply3.ancestors(4, viewer)).to_not include(reply1) end it 'does not return conversation history from blocked domains' do viewer.block_domain!('example.com') expect(reply3.ancestors(4, viewer)).to_not include(reply2) end it 'ignores deleted records' do first_status = Fabricate(:status, account: bob) second_status = Fabricate(:status, thread: first_status, account: alice) # Create cache and delete cached record second_status.ancestors(4) first_status.destroy expect(second_status.ancestors(4)).to eq([]) end it 'can return more records than previously requested' do first_status = Fabricate(:status, account: bob) second_status = Fabricate(:status, thread: first_status, account: alice) third_status = Fabricate(:status, thread: second_status, account: alice) # Create cache second_status.ancestors(1) expect(third_status.ancestors(2)).to eq([first_status, second_status]) end it 'can return fewer records than previously requested' do first_status = Fabricate(:status, account: bob) second_status = Fabricate(:status, thread: first_status, account: alice) third_status = Fabricate(:status, thread: second_status, account: alice) # Create cache second_status.ancestors(2) expect(third_status.ancestors(1)).to eq([second_status]) end end describe '#descendants' do let!(:alice) { Fabricate(:account, username: 'alice') } let!(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com') } let!(:jeff) { Fabricate(:account, username: 'jeff') } let!(:status) { Fabricate(:status, account: alice) } let!(:reply1) { Fabricate(:status, thread: status, account: alice) } let!(:reply2) { Fabricate(:status, thread: status, account: bob) } let!(:reply3) { Fabricate(:status, thread: reply1, account: jeff) } let!(:viewer) { Fabricate(:account, username: 'viewer') } it 'returns replies' do expect(status.descendants(4)).to include(reply1, reply2, reply3) end it 'does not return replies user is not allowed to see' do reply1.update(visibility: :private) reply3.update(visibility: :direct) expect(status.descendants(4, viewer)).to_not include(reply1, reply3) end it 'does not return replies from blocked users' do viewer.block!(jeff) expect(status.descendants(4, viewer)).to_not include(reply3) end it 'does not return replies from muted users' do viewer.mute!(jeff) expect(status.descendants(4, viewer)).to_not include(reply3) end it 'does not return replies from silenced and not followed users' do jeff.silence! expect(status.descendants(4, viewer)).to_not include(reply3) end it 'does not return replies from blocked domains' do viewer.block_domain!('example.com') expect(status.descendants(4, viewer)).to_not include(reply2) end it 'promotes self-replies to the top while leaving the rest in order' do a = Fabricate(:status, account: alice) d = Fabricate(:status, account: jeff, thread: a) e = Fabricate(:status, account: bob, thread: d) c = Fabricate(:status, account: alice, thread: a) f = Fabricate(:status, account: bob, thread: c) expect(a.descendants(20)).to eq [c, d, e, f] end end end ================================================ FILE: spec/models/concerns/streamable_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' RSpec.describe Streamable do class Parent def title; end def target; end def thread; end def self.has_one(*); end def self.after_create; end end class Child < Parent include Streamable end child = Child.new describe '#title' do it 'calls Parent#title' do expect_any_instance_of(Parent).to receive(:title) child.title end end describe '#content' do it 'calls #title' do expect_any_instance_of(Parent).to receive(:title) child.content end end describe '#target' do it 'calls Parent#target' do expect_any_instance_of(Parent).to receive(:target) child.target end end describe '#object_type' do it 'returns :activity' do expect(child.object_type).to eq :activity end end describe '#thread' do it 'calls Parent#thread' do expect_any_instance_of(Parent).to receive(:thread) child.thread end end describe '#hidden?' do it 'returns false' do expect(child.hidden?).to be false end end end ================================================ FILE: spec/models/conversation_mute_spec.rb ================================================ require 'rails_helper' RSpec.describe ConversationMute, type: :model do end ================================================ FILE: spec/models/conversation_spec.rb ================================================ require 'rails_helper' RSpec.describe Conversation, type: :model do describe '#local?' do it 'returns true when URI is nil' do expect(Fabricate(:conversation).local?).to be true end it 'returns false when URI is not nil' do expect(Fabricate(:conversation, uri: 'abc').local?).to be false end end end ================================================ FILE: spec/models/custom_emoji_filter_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' RSpec.describe CustomEmojiFilter do describe '#results' do let!(:custom_emoji_0) { Fabricate(:custom_emoji, domain: 'a') } let!(:custom_emoji_1) { Fabricate(:custom_emoji, domain: 'b') } let!(:custom_emoji_2) { Fabricate(:custom_emoji, domain: nil, shortcode: 'hoge') } subject { described_class.new(params).results } context 'params have values' do context 'local' do let(:params) { { local: true } } it 'returns ActiveRecord::Relation' do expect(subject).to be_kind_of(ActiveRecord::Relation) expect(subject).to match_array([custom_emoji_2]) end end context 'remote' do let(:params) { { remote: true } } it 'returns ActiveRecord::Relation' do expect(subject).to be_kind_of(ActiveRecord::Relation) expect(subject).to match_array([custom_emoji_0, custom_emoji_1]) end end context 'by_domain' do let(:params) { { by_domain: 'a' } } it 'returns ActiveRecord::Relation' do expect(subject).to be_kind_of(ActiveRecord::Relation) expect(subject).to match_array([custom_emoji_0]) end end context 'shortcode' do let(:params) { { shortcode: 'hoge' } } it 'returns ActiveRecord::Relation' do expect(subject).to be_kind_of(ActiveRecord::Relation) expect(subject).to match_array([custom_emoji_2]) end end context 'else' do let(:params) { { else: 'else' } } it 'raises RuntimeError' do expect do subject end.to raise_error(RuntimeError, /Unknown filter: else/) end end end context 'params without value' do let(:params) { { hoge: nil } } it 'returns ActiveRecord::Relation' do expect(subject).to be_kind_of(ActiveRecord::Relation) expect(subject).to match_array([custom_emoji_0, custom_emoji_1, custom_emoji_2]) end end end end ================================================ FILE: spec/models/custom_emoji_spec.rb ================================================ require 'rails_helper' RSpec.describe CustomEmoji, type: :model do describe '#search' do let(:custom_emoji) { Fabricate(:custom_emoji, shortcode: shortcode) } subject { described_class.search(search_term) } context 'shortcode is exact' do let(:shortcode) { 'blobpats' } let(:search_term) { 'blobpats' } it 'finds emoji' do is_expected.to include(custom_emoji) end end context 'shortcode is partial' do let(:shortcode) { 'blobpats' } let(:search_term) { 'blob' } it 'finds emoji' do is_expected.to include(custom_emoji) end end end describe '#local?' do let(:custom_emoji) { Fabricate(:custom_emoji, domain: domain) } subject { custom_emoji.local? } context 'domain is nil' do let(:domain) { nil } it 'returns true' do is_expected.to be true end end context 'domain is present' do let(:domain) { 'example.com' } it 'returns false' do is_expected.to be false end end end describe '#object_type' do it 'returns :emoji' do custom_emoji = Fabricate(:custom_emoji) expect(custom_emoji.object_type).to be :emoji end end describe '.from_text' do let!(:emojo) { Fabricate(:custom_emoji) } subject { described_class.from_text(text, nil) } context 'with plain text' do let(:text) { 'Hello :coolcat:' } it 'returns records used via shortcodes in text' do is_expected.to include(emojo) end end context 'with html' do let(:text) { '

Hello :coolcat:

' } it 'returns records used via shortcodes in text' do is_expected.to include(emojo) end end end describe 'pre_validation' do let(:custom_emoji) { Fabricate(:custom_emoji, domain: 'wWw.MaStOdOn.CoM') } it 'should downcase' do custom_emoji.valid? expect(custom_emoji.domain).to eq('www.mastodon.com') end end end ================================================ FILE: spec/models/custom_filter_spec.rb ================================================ require 'rails_helper' RSpec.describe CustomFilter, type: :model do end ================================================ FILE: spec/models/domain_block_spec.rb ================================================ require 'rails_helper' RSpec.describe DomainBlock, type: :model do describe 'validations' do it 'has a valid fabricator' do domain_block = Fabricate.build(:domain_block) expect(domain_block).to be_valid end it 'is invalid without a domain' do domain_block = Fabricate.build(:domain_block, domain: nil) domain_block.valid? expect(domain_block).to model_have_error_on_field(:domain) end it 'is invalid if the same normalized domain already exists' do domain_block_1 = Fabricate(:domain_block, domain: 'にゃん') domain_block_2 = Fabricate.build(:domain_block, domain: 'xn--r9j5b5b') domain_block_2.valid? expect(domain_block_2).to model_have_error_on_field(:domain) end end describe 'blocked?' do it 'returns true if the domain is suspended' do Fabricate(:domain_block, domain: 'domain', severity: :suspend) expect(DomainBlock.blocked?('domain')).to eq true end it 'returns false even if the domain is silenced' do Fabricate(:domain_block, domain: 'domain', severity: :silence) expect(DomainBlock.blocked?('domain')).to eq false end it 'returns false if the domain is not suspended nor silenced' do expect(DomainBlock.blocked?('domain')).to eq false end end describe 'stricter_than?' do it 'returns true if the new block has suspend severity while the old has lower severity' do suspend = DomainBlock.new(domain: 'domain', severity: :suspend) silence = DomainBlock.new(domain: 'domain', severity: :silence) noop = DomainBlock.new(domain: 'domain', severity: :noop) expect(suspend.stricter_than?(silence)).to be true expect(suspend.stricter_than?(noop)).to be true end it 'returns false if the new block has lower severity than the old one' do suspend = DomainBlock.new(domain: 'domain', severity: :suspend) silence = DomainBlock.new(domain: 'domain', severity: :silence) noop = DomainBlock.new(domain: 'domain', severity: :noop) expect(silence.stricter_than?(suspend)).to be false expect(noop.stricter_than?(suspend)).to be false expect(noop.stricter_than?(silence)).to be false end it 'returns false if the new block does is less strict regarding reports' do older = DomainBlock.new(domain: 'domain', severity: :silence, reject_reports: true) newer = DomainBlock.new(domain: 'domain', severity: :silence, reject_reports: false) expect(newer.stricter_than?(older)).to be false end it 'returns false if the new block does is less strict regarding media' do older = DomainBlock.new(domain: 'domain', severity: :silence, reject_media: true) newer = DomainBlock.new(domain: 'domain', severity: :silence, reject_media: false) expect(newer.stricter_than?(older)).to be false end end end ================================================ FILE: spec/models/email_domain_block_spec.rb ================================================ require 'rails_helper' RSpec.describe EmailDomainBlock, type: :model do describe 'validations' do it 'has a valid fabricator' do email_domain_block = Fabricate.build(:email_domain_block) expect(email_domain_block).to be_valid end end describe 'block?' do it 'returns true if the domain is registed' do Fabricate(:email_domain_block, domain: 'example.com') expect(EmailDomainBlock.block?('nyarn@example.com')).to eq true end it 'returns true if the domain is not registed' do Fabricate(:email_domain_block, domain: 'example.com') expect(EmailDomainBlock.block?('nyarn@example.net')).to eq false end end end ================================================ FILE: spec/models/export_spec.rb ================================================ require 'rails_helper' describe Export do let(:account) { Fabricate(:account) } let(:target_accounts) do [ {}, { username: 'one', domain: 'local.host' } ].map(&method(:Fabricate).curry(2).call(:account)) end describe 'to_csv' do it 'returns a csv of the blocked accounts' do target_accounts.each(&account.method(:block!)) export = Export.new(account).to_blocked_accounts_csv results = export.strip.split expect(results.size).to eq 2 expect(results.first).to eq 'one@local.host' end it 'returns a csv of the muted accounts' do target_accounts.each(&account.method(:mute!)) export = Export.new(account).to_muted_accounts_csv results = export.strip.split("\n") expect(results.size).to eq 3 expect(results.first).to eq 'Account address,Hide notifications' expect(results.second).to eq 'one@local.host,true' end it 'returns a csv of the following accounts' do target_accounts.each(&account.method(:follow!)) export = Export.new(account).to_following_accounts_csv results = export.strip.split("\n") expect(results.size).to eq 3 expect(results.first).to eq 'Account address,Show boosts' expect(results.second).to eq 'one@local.host,true' end end describe 'total_storage' do it 'returns the total size of the media attachments' do media_attachment = Fabricate(:media_attachment, account: account) expect(Export.new(account).total_storage).to eq media_attachment.file_file_size || 0 end end describe 'total_follows' do it 'returns the total number of the followed accounts' do target_accounts.each(&account.method(:follow!)) expect(Export.new(account.reload).total_follows).to eq 2 end it 'returns the total number of the blocked accounts' do target_accounts.each(&account.method(:block!)) expect(Export.new(account.reload).total_blocks).to eq 2 end it 'returns the total number of the muted accounts' do target_accounts.each(&account.method(:mute!)) expect(Export.new(account.reload).total_mutes).to eq 2 end end end ================================================ FILE: spec/models/favourite_spec.rb ================================================ require 'rails_helper' RSpec.describe Favourite, type: :model do let(:account) { Fabricate(:account) } context 'when status is a reblog' do let(:reblog) { Fabricate(:status, reblog: nil) } let(:status) { Fabricate(:status, reblog: reblog) } it 'invalidates if the reblogged status is already a favourite' do Favourite.create!(account: account, status: reblog) expect(Favourite.new(account: account, status: status).valid?).to eq false end it 'replaces status with the reblogged one if it is a reblog' do favourite = Favourite.create!(account: account, status: status) expect(favourite.status).to eq reblog end end context 'when status is not a reblog' do let(:status) { Fabricate(:status, reblog: nil) } it 'saves with the specified status' do favourite = Favourite.create!(account: account, status: status) expect(favourite.status).to eq status end end end ================================================ FILE: spec/models/featured_tag_spec.rb ================================================ require 'rails_helper' RSpec.describe FeaturedTag, type: :model do end ================================================ FILE: spec/models/follow_request_spec.rb ================================================ require 'rails_helper' RSpec.describe FollowRequest, type: :model do describe '#authorize!' do let(:follow_request) { Fabricate(:follow_request, account: account, target_account: target_account) } let(:account) { Fabricate(:account) } let(:target_account) { Fabricate(:account) } it 'calls Account#follow!, MergeWorker.perform_async, and #destroy!' do expect(account).to receive(:follow!).with(target_account, reblogs: true, uri: follow_request.uri) expect(MergeWorker).to receive(:perform_async).with(target_account.id, account.id) expect(follow_request).to receive(:destroy!) follow_request.authorize! end it 'correctly passes show_reblogs when true' do follow_request = Fabricate.create(:follow_request, show_reblogs: true) follow_request.authorize! target = follow_request.target_account expect(follow_request.account.muting_reblogs?(target)).to be false end it 'correctly passes show_reblogs when false' do follow_request = Fabricate.create(:follow_request, show_reblogs: false) follow_request.authorize! target = follow_request.target_account expect(follow_request.account.muting_reblogs?(target)).to be true end end end ================================================ FILE: spec/models/follow_spec.rb ================================================ require 'rails_helper' RSpec.describe Follow, type: :model do let(:alice) { Fabricate(:account, username: 'alice') } let(:bob) { Fabricate(:account, username: 'bob') } describe 'validations' do subject { Follow.new(account: alice, target_account: bob) } it 'has a valid fabricator' do follow = Fabricate.build(:follow) expect(follow).to be_valid end it 'is invalid without an account' do follow = Fabricate.build(:follow, account: nil) follow.valid? expect(follow).to model_have_error_on_field(:account) end it 'is invalid without a target_account' do follow = Fabricate.build(:follow, target_account: nil) follow.valid? expect(follow).to model_have_error_on_field(:target_account) end it 'is invalid if account already follows too many people' do alice.update(following_count: FollowLimitValidator::LIMIT) expect(subject).to_not be_valid expect(subject).to model_have_error_on_field(:base) end it 'is valid if account is only on the brink of following too many people' do alice.update(following_count: FollowLimitValidator::LIMIT - 1) expect(subject).to be_valid expect(subject).to_not model_have_error_on_field(:base) end end describe 'recent' do it 'sorts so that more recent follows comes earlier' do follow0 = Follow.create!(account: alice, target_account: bob) follow1 = Follow.create!(account: bob, target_account: alice) a = Follow.recent.to_a expect(a.size).to eq 2 expect(a[0]).to eq follow1 expect(a[1]).to eq follow0 end end describe 'revoke_request!' do let(:follow) { Fabricate(:follow, account: account, target_account: target_account) } let(:account) { Fabricate(:account) } let(:target_account) { Fabricate(:account) } it 'revokes the follow relation' do follow.revoke_request! expect(account.following?(target_account)).to be false end it 'creates a follow request' do follow.revoke_request! expect(account.requested?(target_account)).to be true end end end ================================================ FILE: spec/models/form/status_batch_spec.rb ================================================ require 'rails_helper' describe Form::StatusBatch do let(:form) { Form::StatusBatch.new(action: action, status_ids: status_ids) } let(:status) { Fabricate(:status) } describe 'with nsfw action' do let(:status_ids) { [status.id, nonsensitive_status.id, sensitive_status.id] } let(:nonsensitive_status) { Fabricate(:status, sensitive: false) } let(:sensitive_status) { Fabricate(:status, sensitive: true) } let!(:shown_media_attachment) { Fabricate(:media_attachment, status: nonsensitive_status) } let!(:hidden_media_attachment) { Fabricate(:media_attachment, status: sensitive_status) } context 'nsfw_on' do let(:action) { 'nsfw_on' } it { expect(form.save).to be true } it { expect { form.save }.to change { nonsensitive_status.reload.sensitive }.from(false).to(true) } it { expect { form.save }.not_to change { sensitive_status.reload.sensitive } } it { expect { form.save }.not_to change { status.reload.sensitive } } end context 'nsfw_off' do let(:action) { 'nsfw_off' } it { expect(form.save).to be true } it { expect { form.save }.to change { sensitive_status.reload.sensitive }.from(true).to(false) } it { expect { form.save }.not_to change { nonsensitive_status.reload.sensitive } } it { expect { form.save }.not_to change { status.reload.sensitive } } end end describe 'with delete action' do let(:status_ids) { [status.id] } let(:action) { 'delete' } let!(:another_status) { Fabricate(:status) } before do allow(RemovalWorker).to receive(:perform_async) end it 'call RemovalWorker' do form.save expect(RemovalWorker).to have_received(:perform_async).with(status.id) end it 'do not call RemovalWorker' do form.save expect(RemovalWorker).not_to have_received(:perform_async).with(another_status.id) end end end ================================================ FILE: spec/models/home_feed_spec.rb ================================================ require 'rails_helper' RSpec.describe HomeFeed, type: :model do let(:account) { Fabricate(:account) } subject { described_class.new(account) } describe '#get' do before do Fabricate(:status, account: account, id: 1) Fabricate(:status, account: account, id: 2) Fabricate(:status, account: account, id: 3) Fabricate(:status, account: account, id: 10) end context 'when feed is generated' do before do Redis.current.zadd( FeedManager.instance.key(:home, account.id), [[4, 4], [3, 3], [2, 2], [1, 1]] ) end it 'gets statuses with ids in the range from redis' do results = subject.get(3) expect(results.map(&:id)).to eq [3, 2] expect(results.first.attributes.keys).to eq %w(id updated_at) end end context 'when feed is being generated' do before do Redis.current.set("account:#{account.id}:regeneration", true) end it 'gets statuses with ids in the range from database' do results = subject.get(3) expect(results.map(&:id)).to eq [10, 3, 2] expect(results.first.attributes.keys).to include('id', 'updated_at') end end end end ================================================ FILE: spec/models/identity_spec.rb ================================================ require 'rails_helper' RSpec.describe Identity, type: :model do describe '.find_for_oauth' do let(:auth) { Fabricate(:identity, user: Fabricate(:user)) } it 'calls .find_or_create_by' do expect(described_class).to receive(:find_or_create_by).with(uid: auth.uid, provider: auth.provider) described_class.find_for_oauth(auth) end it 'returns an instance of Identity' do expect(described_class.find_for_oauth(auth)).to be_instance_of Identity end end end ================================================ FILE: spec/models/import_spec.rb ================================================ require 'rails_helper' RSpec.describe Import, type: :model do let (:account) { Fabricate(:account) } let (:type) { 'following' } let (:data) { attachment_fixture('imports.txt') } describe 'validations' do it 'has a valid parameters' do import = Import.create(account: account, type: type, data: data) expect(import).to be_valid end it 'is invalid without an type' do import = Import.create(account: account, data: data) expect(import).to model_have_error_on_field(:type) end it 'is invalid without a data' do import = Import.create(account: account, type: type) expect(import).to model_have_error_on_field(:data) end end end ================================================ FILE: spec/models/invite_spec.rb ================================================ require 'rails_helper' RSpec.describe Invite, type: :model do describe '#valid_for_use?' do it 'returns true when there are no limitations' do invite = Invite.new(max_uses: nil, expires_at: nil) expect(invite.valid_for_use?).to be true end it 'returns true when not expired' do invite = Invite.new(max_uses: nil, expires_at: 1.hour.from_now) expect(invite.valid_for_use?).to be true end it 'returns false when expired' do invite = Invite.new(max_uses: nil, expires_at: 1.hour.ago) expect(invite.valid_for_use?).to be false end it 'returns true when uses still available' do invite = Invite.new(max_uses: 250, uses: 249, expires_at: nil) expect(invite.valid_for_use?).to be true end it 'returns false when maximum uses reached' do invite = Invite.new(max_uses: 250, uses: 250, expires_at: nil) expect(invite.valid_for_use?).to be false end end end ================================================ FILE: spec/models/list_account_spec.rb ================================================ require 'rails_helper' RSpec.describe ListAccount, type: :model do end ================================================ FILE: spec/models/list_spec.rb ================================================ require 'rails_helper' RSpec.describe List, type: :model do end ================================================ FILE: spec/models/media_attachment_spec.rb ================================================ require 'rails_helper' RSpec.describe MediaAttachment, type: :model do describe 'local?' do let(:media_attachment) { Fabricate(:media_attachment, remote_url: remote_url) } subject { media_attachment.local? } context 'remote_url is blank' do let(:remote_url) { '' } it 'returns true' do is_expected.to be true end end context 'remote_url is present' do let(:remote_url) { 'remote_url' } it 'returns false' do is_expected.to be false end end end describe 'needs_redownload?' do let(:media_attachment) { Fabricate(:media_attachment, remote_url: remote_url, file: file) } subject { media_attachment.needs_redownload? } context 'file is blank' do let(:file) { nil } context 'remote_url is blank' do let(:remote_url) { '' } it 'returns false' do is_expected.to be false end end context 'remote_url is present' do let(:remote_url) { 'remote_url' } it 'returns true' do is_expected.to be true end end end context 'file is present' do let(:file) { attachment_fixture('avatar.gif') } context 'remote_url is blank' do let(:remote_url) { '' } it 'returns false' do is_expected.to be false end end context 'remote_url is present' do let(:remote_url) { 'remote_url' } it 'returns true' do is_expected.to be false end end end end describe '#to_param' do let(:media_attachment) { Fabricate(:media_attachment) } let(:shortcode) { media_attachment.shortcode } it 'returns shortcode' do expect(media_attachment.to_param).to eq shortcode end end describe 'animated gif conversion' do let(:media) { MediaAttachment.create(account: Fabricate(:account), file: attachment_fixture('avatar.gif')) } it 'sets type to gifv' do expect(media.type).to eq 'gifv' end it 'converts original file to mp4' do expect(media.file_content_type).to eq 'video/mp4' end it 'sets meta' do expect(media.file.meta["original"]["width"]).to eq 128 expect(media.file.meta["original"]["height"]).to eq 128 end end describe 'non-animated gif non-conversion' do fixtures = [ { filename: 'attachment.gif', width: 600, height: 400, aspect: 1.5 }, { filename: 'mini-static.gif', width: 32, height: 32, aspect: 1.0 }, ] fixtures.each do |fixture| context fixture[:filename] do let(:media) { MediaAttachment.create(account: Fabricate(:account), file: attachment_fixture(fixture[:filename])) } it 'sets type to image' do expect(media.type).to eq 'image' end it 'leaves original file as-is' do expect(media.file_content_type).to eq 'image/gif' end it 'sets meta' do expect(media.file.meta["original"]["width"]).to eq fixture[:width] expect(media.file.meta["original"]["height"]).to eq fixture[:height] expect(media.file.meta["original"]["aspect"]).to eq fixture[:aspect] end end end end describe 'jpeg' do let(:media) { MediaAttachment.create(account: Fabricate(:account), file: attachment_fixture('attachment.jpg')) } it 'sets meta for different style' do expect(media.file.meta["original"]["width"]).to eq 600 expect(media.file.meta["original"]["height"]).to eq 400 expect(media.file.meta["original"]["aspect"]).to eq 1.5 expect(media.file.meta["small"]["width"]).to eq 490 expect(media.file.meta["small"]["height"]).to eq 327 expect(media.file.meta["small"]["aspect"]).to eq 490.0 / 327 end end describe 'descriptions for remote attachments' do it 'are cut off at 140 characters' do media = Fabricate(:media_attachment, description: 'foo' * 1000, remote_url: 'http://example.com/blah.jpg') expect(media.description.size).to be <= 420 end end end ================================================ FILE: spec/models/mention_spec.rb ================================================ require 'rails_helper' RSpec.describe Mention, type: :model do describe 'validations' do it 'has a valid fabricator' do mention = Fabricate.build(:mention) expect(mention).to be_valid end it 'is invalid without an account' do mention = Fabricate.build(:mention, account: nil) mention.valid? expect(mention).to model_have_error_on_field(:account) end it 'is invalid without a status' do mention = Fabricate.build(:mention, status: nil) mention.valid? expect(mention).to model_have_error_on_field(:status) end end end ================================================ FILE: spec/models/mute_spec.rb ================================================ require 'rails_helper' RSpec.describe Mute, type: :model do end ================================================ FILE: spec/models/notification_spec.rb ================================================ require 'rails_helper' RSpec.describe Notification, type: :model do describe '#target_status' do let(:notification) { Fabricate(:notification, activity: activity) } let(:status) { Fabricate(:status) } let(:reblog) { Fabricate(:status, reblog: status) } let(:favourite) { Fabricate(:favourite, status: status) } let(:mention) { Fabricate(:mention, status: status) } context 'activity is reblog' do let(:activity) { reblog } it 'returns status' do expect(notification.target_status).to eq status end end context 'activity is favourite' do let(:type) { :favourite } let(:activity) { favourite } it 'returns status' do expect(notification.target_status).to eq status end end context 'activity is mention' do let(:activity) { mention } it 'returns status' do expect(notification.target_status).to eq status end end end describe '#browserable?' do let(:notification) { Fabricate(:notification) } subject { notification.browserable? } context 'type is :follow_request' do before do allow(notification).to receive(:type).and_return(:follow_request) end it 'returns false' do is_expected.to be false end end context 'type is not :follow_request' do before do allow(notification).to receive(:type).and_return(:else) end it 'returns true' do is_expected.to be true end end end describe '#type' do it 'returns :reblog for a Status' do notification = Notification.new(activity: Status.new) expect(notification.type).to eq :reblog end it 'returns :mention for a Mention' do notification = Notification.new(activity: Mention.new) expect(notification.type).to eq :mention end it 'returns :favourite for a Favourite' do notification = Notification.new(activity: Favourite.new) expect(notification.type).to eq :favourite end it 'returns :follow for a Follow' do notification = Notification.new(activity: Follow.new) expect(notification.type).to eq :follow end end describe '.reload_stale_associations!' do context 'account_ids are empty' do let(:cached_items) { [] } subject { described_class.reload_stale_associations!(cached_items) } it 'returns nil' do is_expected.to be nil end end context 'account_ids are present' do before do allow(accounts_with_ids).to receive(:[]).with(stale_account1.id).and_return(account1) allow(accounts_with_ids).to receive(:[]).with(stale_account2.id).and_return(account2) allow(Account).to receive_message_chain(:where, :includes, :each_with_object).and_return(accounts_with_ids) end let(:cached_items) do [ Fabricate(:notification, activity: Fabricate(:status)), Fabricate(:notification, activity: Fabricate(:follow)), ] end let(:stale_account1) { cached_items[0].from_account } let(:stale_account2) { cached_items[1].from_account } let(:account1) { Fabricate(:account) } let(:account2) { Fabricate(:account) } let(:accounts_with_ids) { { account1.id => account1, account2.id => account2 } } it 'reloads associations' do expect(cached_items[0].from_account).to be stale_account1 expect(cached_items[1].from_account).to be stale_account2 described_class.reload_stale_associations!(cached_items) expect(cached_items[0].from_account).to be account1 expect(cached_items[1].from_account).to be account2 end end end end ================================================ FILE: spec/models/poll_spec.rb ================================================ require 'rails_helper' RSpec.describe Poll, type: :model do pending "add some examples to (or delete) #{__FILE__}" end ================================================ FILE: spec/models/poll_vote_spec.rb ================================================ require 'rails_helper' RSpec.describe PollVote, type: :model do pending "add some examples to (or delete) #{__FILE__}" end ================================================ FILE: spec/models/preview_card_spec.rb ================================================ require 'rails_helper' RSpec.describe PreviewCard, type: :model do end ================================================ FILE: spec/models/relay_spec.rb ================================================ require 'rails_helper' RSpec.describe Relay, type: :model do end ================================================ FILE: spec/models/remote_follow_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' RSpec.describe RemoteFollow do before do stub_request(:get, 'https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no').to_return(request_fixture('webfinger.txt')) end let(:attrs) { nil } let(:remote_follow) { described_class.new(attrs) } describe '.initialize' do subject { remote_follow.acct } context 'attrs with acct' do let(:attrs) { { acct: 'gargron@quitter.no' } } it 'returns acct' do is_expected.to eq 'gargron@quitter.no' end end context 'attrs without acct' do let(:attrs) { {} } it do is_expected.to be_nil end end end describe '#valid?' do subject { remote_follow.valid? } context 'attrs with acct' do let(:attrs) { { acct: 'gargron@quitter.no' } } it do is_expected.to be true end end context 'attrs without acct' do let(:attrs) { {} } it do is_expected.to be false end end end describe '#subscribe_address_for' do before do remote_follow.valid? end let(:attrs) { { acct: 'gargron@quitter.no' } } let(:account) { Fabricate(:account, username: 'alice') } subject { remote_follow.subscribe_address_for(account) } it 'returns subscribe address' do is_expected.to eq 'https://quitter.no/main/ostatussub?profile=alice%40cb6e6126.ngrok.io' end end end ================================================ FILE: spec/models/remote_profile_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' RSpec.describe RemoteProfile do let(:remote_profile) { RemoteProfile.new(body) } let(:body) do <<-XML John XML end describe '.initialize' do it 'calls Nokogiri::XML.parse' do expect(Nokogiri::XML).to receive(:parse).with(body, nil, 'utf-8') RemoteProfile.new(body) end it 'sets document' do remote_profile = RemoteProfile.new(body) expect(remote_profile).not_to be nil end end describe '#root' do let(:document) { remote_profile.document } it 'callse document.at_xpath' do expect(document).to receive(:at_xpath).with( '/atom:feed|/atom:entry', atom: OStatus::TagManager::XMLNS ) remote_profile.root end end describe '#author' do let(:root) { remote_profile.root } it 'calls root.at_xpath' do expect(root).to receive(:at_xpath).with( './atom:author|./dfrn:owner', atom: OStatus::TagManager::XMLNS, dfrn: OStatus::TagManager::DFRN_XMLNS ) remote_profile.author end end describe '#hub_link' do let(:root) { remote_profile.root } it 'calls #link_href_from_xml' do expect(remote_profile).to receive(:link_href_from_xml).with(root, 'hub') remote_profile.hub_link end end describe '#display_name' do let(:author) { remote_profile.author } it 'calls author.at_xpath.content' do expect(author).to receive_message_chain(:at_xpath, :content).with( './poco:displayName', poco: OStatus::TagManager::POCO_XMLNS ).with(no_args) remote_profile.display_name end end describe '#note' do let(:author) { remote_profile.author } it 'calls author.at_xpath.content' do expect(author).to receive_message_chain(:at_xpath, :content).with( './atom:summary|./poco:note', atom: OStatus::TagManager::XMLNS, poco: OStatus::TagManager::POCO_XMLNS ).with(no_args) remote_profile.note end end describe '#scope' do let(:author) { remote_profile.author } it 'calls author.at_xpath.content' do expect(author).to receive_message_chain(:at_xpath, :content).with( './mastodon:scope', mastodon: OStatus::TagManager::MTDN_XMLNS ).with(no_args) remote_profile.scope end end describe '#avatar' do let(:author) { remote_profile.author } it 'calls #link_href_from_xml' do expect(remote_profile).to receive(:link_href_from_xml).with(author, 'avatar') remote_profile.avatar end end describe '#header' do let(:author) { remote_profile.author } it 'calls #link_href_from_xml' do expect(remote_profile).to receive(:link_href_from_xml).with(author, 'header') remote_profile.header end end describe '#locked?' do before do allow(remote_profile).to receive(:scope).and_return(scope) end subject { remote_profile.locked? } context 'scope is private' do let(:scope) { 'private' } it 'returns true' do is_expected.to be true end end context 'scope is not private' do let(:scope) { 'public' } it 'returns false' do is_expected.to be false end end end end ================================================ FILE: spec/models/report_filter_spec.rb ================================================ require 'rails_helper' describe ReportFilter do describe 'with empty params' do it 'defaults to unresolved reports list' do filter = ReportFilter.new({}) expect(filter.results).to eq Report.unresolved end end describe 'with invalid params' do it 'raises with key error' do filter = ReportFilter.new(wrong: true) expect { filter.results }.to raise_error(/wrong/) end end describe 'with valid params' do it 'combines filters on Report' do filter = ReportFilter.new(account_id: '123', resolved: true, target_account_id: '456') allow(Report).to receive(:where).and_return(Report.none) allow(Report).to receive(:resolved).and_return(Report.none) filter.results expect(Report).to have_received(:where).with(account_id: '123') expect(Report).to have_received(:where).with(target_account_id: '456') expect(Report).to have_received(:resolved) end end end ================================================ FILE: spec/models/report_spec.rb ================================================ require 'rails_helper' describe Report do describe 'statuses' do it 'returns the statuses for the report' do status = Fabricate(:status) _other = Fabricate(:status) report = Fabricate(:report, status_ids: [status.id]) expect(report.statuses).to eq [status] end end describe 'media_attachments' do it 'returns media attachments from statuses' do status = Fabricate(:status) media_attachment = Fabricate(:media_attachment, status: status) _other_media_attachment = Fabricate(:media_attachment) report = Fabricate(:report, status_ids: [status.id]) expect(report.media_attachments).to eq [media_attachment] end end describe 'assign_to_self!' do subject { report.assigned_account_id } let(:report) { Fabricate(:report, assigned_account_id: original_account) } let(:original_account) { Fabricate(:account) } let(:current_account) { Fabricate(:account) } before do report.assign_to_self!(current_account) end it 'assigns to a given account' do is_expected.to eq current_account.id end end describe 'unassign!' do subject { report.assigned_account_id } let(:report) { Fabricate(:report, assigned_account_id: account.id) } let(:account) { Fabricate(:account) } before do report.unassign! end it 'unassigns' do is_expected.to be_nil end end describe 'resolve!' do subject(:report) { Fabricate(:report, action_taken: false, action_taken_by_account_id: nil) } let(:acting_account) { Fabricate(:account) } before do report.resolve!(acting_account) end it 'records action taken' do expect(report).to have_attributes(action_taken: true, action_taken_by_account_id: acting_account.id) end end describe 'unresolve!' do subject(:report) { Fabricate(:report, action_taken: true, action_taken_by_account_id: acting_account.id) } let(:acting_account) { Fabricate(:account) } before do report.unresolve! end it 'unresolves' do expect(report).to have_attributes(action_taken: false, action_taken_by_account_id: nil) end end describe 'unresolved?' do subject { report.unresolved? } let(:report) { Fabricate(:report, action_taken: action_taken) } context 'if action is taken' do let(:action_taken) { true } it { is_expected.to be false } end context 'if action not is taken' do let(:action_taken) { false } it { is_expected.to be true } end end describe 'history' do subject(:action_logs) { report.history } let(:report) { Fabricate(:report, target_account_id: target_account.id, status_ids: [status.id], created_at: 3.days.ago, updated_at: 1.day.ago) } let(:target_account) { Fabricate(:account) } let(:status) { Fabricate(:status) } before do Fabricate('Admin::ActionLog', target_type: 'Report', account_id: target_account.id, target_id: report.id, created_at: 2.days.ago) Fabricate('Admin::ActionLog', target_type: 'Account', account_id: target_account.id, target_id: report.target_account_id, created_at: 2.days.ago) Fabricate('Admin::ActionLog', target_type: 'Status', account_id: target_account.id, target_id: status.id, created_at: 2.days.ago) end it 'returns right logs' do expect(action_logs.count).to eq 3 end end describe 'validatiions' do it 'has a valid fabricator' do report = Fabricate(:report) report.valid? expect(report).to be_valid end it 'is invalid if comment is longer than 1000 characters' do report = Fabricate.build(:report, comment: Faker::Lorem.characters(1001)) report.valid? expect(report).to model_have_error_on_field(:comment) end end end ================================================ FILE: spec/models/scheduled_status_spec.rb ================================================ require 'rails_helper' RSpec.describe ScheduledStatus, type: :model do end ================================================ FILE: spec/models/session_activation_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' RSpec.describe SessionActivation, type: :model do describe '#detection' do let(:session_activation) { Fabricate(:session_activation, user_agent: 'Chrome/62.0.3202.89') } it 'sets a Browser instance as detection' do expect(session_activation.detection).to be_kind_of Browser::Chrome end end describe '#browser' do before do allow(session_activation).to receive(:detection).and_return(detection) end let(:detection) { double(id: 1) } let(:session_activation) { Fabricate(:session_activation) } it 'returns detection.id' do expect(session_activation.browser).to be 1 end end describe '#platform' do before do allow(session_activation).to receive(:detection).and_return(detection) end let(:session_activation) { Fabricate(:session_activation) } let(:detection) { double(platform: double(id: 1)) } it 'returns detection.platform.id' do expect(session_activation.platform).to be 1 end end describe '.active?' do subject { described_class.active?(id) } context 'id is absent' do let(:id) { nil } it 'returns nil' do is_expected.to be nil end end context 'id is present' do let(:id) { '1' } let!(:session_activation) { Fabricate(:session_activation, session_id: id) } context 'id exists as session_id' do it 'returns true' do is_expected.to be true end end context 'id does not exist as session_id' do before do session_activation.update!(session_id: '2') end it 'returns false' do is_expected.to be false end end end end describe '.activate' do let(:options) { { user: Fabricate(:user), session_id: '1' } } it 'calls create! and purge_old' do expect(described_class).to receive(:create!).with(options) expect(described_class).to receive(:purge_old) described_class.activate(options) end it 'returns an instance of SessionActivation' do expect(described_class.activate(options)).to be_kind_of SessionActivation end end describe '.deactivate' do context 'id is absent' do let(:id) { nil } it 'returns nil' do expect(described_class.deactivate(id)).to be nil end end context 'id exists' do let(:id) { '1' } it 'calls where.destroy_all' do expect(described_class).to receive_message_chain(:where, :destroy_all) .with(session_id: id).with(no_args) described_class.deactivate(id) end end end describe '.purge_old' do it 'calls order.offset.destroy_all' do expect(described_class).to receive_message_chain(:order, :offset, :destroy_all) .with('created_at desc').with(Rails.configuration.x.max_session_activations).with(no_args) described_class.purge_old end end describe '.exclusive' do let(:id) { '1' } it 'calls where.destroy_all' do expect(described_class).to receive_message_chain(:where, :destroy_all) .with('session_id != ?', id).with(no_args) described_class.exclusive(id) end end end ================================================ FILE: spec/models/setting_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' RSpec.describe Setting, type: :model do describe '#to_param' do let(:setting) { Fabricate(:setting, var: var) } let(:var) { 'var' } it 'returns setting.var' do expect(setting.to_param).to eq var end end describe '.[]' do before do allow(described_class).to receive(:rails_initialized?).and_return(rails_initialized) end let(:key) { 'key' } context 'rails_initialized? is falsey' do let(:rails_initialized) { false } it 'calls RailsSettings::Base#[]' do expect(RailsSettings::Base).to receive(:[]).with(key) described_class[key] end end context 'rails_initialized? is truthy' do before do allow(RailsSettings::Base).to receive(:cache_key).with(key, nil).and_return(cache_key) end let(:rails_initialized) { true } let(:cache_key) { 'cache-key' } let(:cache_value) { 'cache-value' } it 'calls not RailsSettings::Base#[]' do expect(RailsSettings::Base).not_to receive(:[]).with(key) described_class[key] end context 'Rails.cache does not exists' do before do allow(RailsSettings::Settings).to receive(:object).with(key).and_return(object) allow(described_class).to receive(:default_settings).and_return(default_settings) allow_any_instance_of(Settings::ScopedSettings).to receive(:thing_scoped).and_return(records) Rails.cache.delete(cache_key) end let(:object) { nil } let(:default_value) { 'default_value' } let(:default_settings) { { key => default_value } } let(:records) { [Fabricate(:setting, var: key, value: nil)] } it 'calls RailsSettings::Settings.object' do expect(RailsSettings::Settings).to receive(:object).with(key) described_class[key] end context 'RailsSettings::Settings.object returns truthy' do let(:object) { db_val } let(:db_val) { double(value: 'db_val') } context 'default_value is a Hash' do let(:default_value) { { default_value: 'default_value' } } it 'calls default_value.with_indifferent_access.merge!' do expect(default_value).to receive_message_chain(:with_indifferent_access, :merge!) .with(db_val.value) described_class[key] end end context 'default_value is not a Hash' do let(:default_value) { 'default_value' } it 'returns db_val.value' do expect(described_class[key]).to be db_val.value end end end context 'RailsSettings::Settings.object returns falsey' do let(:object) { nil } it 'returns default_settings[key]' do expect(described_class[key]).to be default_settings[key] end end end context 'Rails.cache exists' do before do Rails.cache.write(cache_key, cache_value) end it 'does not query the database' do expect do |callback| ActiveSupport::Notifications.subscribed callback, 'sql.active_record' do described_class[key] end end.not_to yield_control end it 'returns the cached value' do expect(described_class[key]).to eq cache_value end end end end describe '.all_as_records' do before do allow_any_instance_of(Settings::ScopedSettings).to receive(:thing_scoped).and_return(records) allow(described_class).to receive(:default_settings).and_return(default_settings) end let(:key) { 'key' } let(:default_value) { 'default_value' } let(:default_settings) { { key => default_value } } let(:original_setting) { Fabricate(:setting, var: key, value: nil) } let(:records) { [original_setting] } it 'returns a Hash' do expect(described_class.all_as_records).to be_kind_of Hash end context 'records includes Setting with var as the key' do let(:records) { [original_setting] } it 'includes the original Setting' do setting = described_class.all_as_records[key] expect(setting).to eq original_setting end end context 'records includes nothing' do let(:records) { [] } context 'default_value is not a Hash' do it 'includes Setting with value of default_value' do setting = described_class.all_as_records[key] expect(setting).to be_kind_of Setting expect(setting).to have_attributes(var: key) expect(setting).to have_attributes(value: 'default_value') end end context 'default_value is a Hash' do let(:default_value) { { 'foo' => 'fuga' } } it 'returns {}' do expect(described_class.all_as_records).to eq({}) end end end end describe '.default_settings' do before do allow(RailsSettings::Default).to receive(:enabled?).and_return(enabled) end subject { described_class.default_settings } context 'RailsSettings::Default.enabled? is false' do let(:enabled) { false } it 'returns {}' do is_expected.to eq({}) end end context 'RailsSettings::Settings.enabled? is true' do let(:enabled) { true } it 'returns instance of RailsSettings::Default' do is_expected.to be_kind_of RailsSettings::Default end end end end ================================================ FILE: spec/models/site_upload_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' RSpec.describe SiteUpload, type: :model do describe '#cache_key' do let(:site_upload) { SiteUpload.new(var: 'var') } it 'returns cache_key' do expect(site_upload.cache_key).to eq 'site_uploads/var' end end end ================================================ FILE: spec/models/status_pin_spec.rb ================================================ require 'rails_helper' RSpec.describe StatusPin, type: :model do describe 'validations' do it 'allows pins of own statuses' do account = Fabricate(:account) status = Fabricate(:status, account: account) expect(StatusPin.new(account: account, status: status).save).to be true end it 'does not allow pins of statuses by someone else' do account = Fabricate(:account) status = Fabricate(:status) expect(StatusPin.new(account: account, status: status).save).to be false end it 'does not allow pins of reblogs' do account = Fabricate(:account) status = Fabricate(:status, account: account) reblog = Fabricate(:status, reblog: status) expect(StatusPin.new(account: account, status: reblog).save).to be false end it 'does not allow pins of private statuses' do account = Fabricate(:account) status = Fabricate(:status, account: account, visibility: :private) expect(StatusPin.new(account: account, status: status).save).to be false end it 'does not allow pins of direct statuses' do account = Fabricate(:account) status = Fabricate(:status, account: account, visibility: :direct) expect(StatusPin.new(account: account, status: status).save).to be false end max_pins = 5 it 'does not allow pins above the max' do account = Fabricate(:account) status = [] (max_pins + 1).times do |i| status[i] = Fabricate(:status, account: account) end max_pins.times do |i| expect(StatusPin.new(account: account, status: status[i]).save).to be true end expect(StatusPin.new(account: account, status: status[max_pins]).save).to be false end it 'allows pins above the max for remote accounts' do account = Fabricate(:account, domain: 'remote.test', username: 'bob', url: 'https://remote.test/') status = [] (max_pins + 1).times do |i| status[i] = Fabricate(:status, account: account) end max_pins.times do |i| expect(StatusPin.new(account: account, status: status[i]).save).to be true end expect(StatusPin.new(account: account, status: status[max_pins]).save).to be true end end end ================================================ FILE: spec/models/status_spec.rb ================================================ require 'rails_helper' RSpec.describe Status, type: :model do let(:alice) { Fabricate(:account, username: 'alice') } let(:bob) { Fabricate(:account, username: 'bob') } let(:other) { Fabricate(:status, account: bob, text: 'Skulls for the skull god! The enemy\'s gates are sideways!') } subject { Fabricate(:status, account: alice) } describe '#local?' do it 'returns true when no remote URI is set' do expect(subject.local?).to be true end it 'returns false if a remote URI is set' do alice.update(domain: 'example.com') subject.save expect(subject.local?).to be false end it 'returns true if a URI is set and `local` is true' do subject.update(uri: 'example.com', local: true) expect(subject.local?).to be true end end describe '#reblog?' do it 'returns true when the status reblogs another status' do subject.reblog = other expect(subject.reblog?).to be true end it 'returns false if the status is self-contained' do expect(subject.reblog?).to be false end end describe '#reply?' do it 'returns true if the status references another' do subject.thread = other expect(subject.reply?).to be true end it 'returns false if the status is self-contained' do expect(subject.reply?).to be false end end describe '#verb' do context 'if destroyed?' do it 'returns :delete' do subject.destroy! expect(subject.verb).to be :delete end end context 'unless destroyed?' do context 'if reblog?' do it 'returns :share' do subject.reblog = other expect(subject.verb).to be :share end end context 'unless reblog?' do it 'returns :post' do subject.reblog = nil expect(subject.verb).to be :post end end end end describe '#object_type' do it 'is note when the status is self-contained' do expect(subject.object_type).to be :note end it 'is comment when the status replies to another' do subject.thread = other expect(subject.object_type).to be :comment end end describe '#title' do # rubocop:disable Style/InterpolationCheck let(:account) { subject.account } context 'if destroyed?' do it 'returns "#{account.acct} deleted status"' do subject.destroy! expect(subject.title).to eq "#{account.acct} deleted status" end end context 'unless destroyed?' do context 'if reblog?' do it 'returns "#{account.acct} shared a status by #{reblog.account.acct}"' do reblog = subject.reblog = other expect(subject.title).to eq "#{account.acct} shared a status by #{reblog.account.acct}" end end context 'unless reblog?' do it 'returns "New status by #{account.acct}"' do subject.reblog = nil expect(subject.title).to eq "New status by #{account.acct}" end end end end describe '#hidden?' do context 'if private_visibility?' do it 'returns true' do subject.visibility = :private expect(subject.hidden?).to be true end end context 'if direct_visibility?' do it 'returns true' do subject.visibility = :direct expect(subject.hidden?).to be true end end context 'if public_visibility?' do it 'returns false' do subject.visibility = :public expect(subject.hidden?).to be false end end context 'if unlisted_visibility?' do it 'returns false' do subject.visibility = :unlisted expect(subject.hidden?).to be false end end end describe '#content' do it 'returns the text of the status if it is not a reblog' do expect(subject.content).to eql subject.text end it 'returns the text of the reblogged status' do subject.reblog = other expect(subject.content).to eql other.text end end describe '#target' do it 'returns nil if the status is self-contained' do expect(subject.target).to be_nil end it 'returns nil if the status is a reply' do subject.thread = other expect(subject.target).to be_nil end it 'returns the reblogged status' do subject.reblog = other expect(subject.target).to eq other end end describe '#reblogs_count' do it 'is the number of reblogs' do Fabricate(:status, account: bob, reblog: subject) Fabricate(:status, account: alice, reblog: subject) expect(subject.reblogs_count).to eq 2 end it 'is decremented when reblog is removed' do reblog = Fabricate(:status, account: bob, reblog: subject) expect(subject.reblogs_count).to eq 1 reblog.destroy expect(subject.reblogs_count).to eq 0 end it 'does not fail when original is deleted before reblog' do reblog = Fabricate(:status, account: bob, reblog: subject) expect(subject.reblogs_count).to eq 1 expect { subject.destroy }.to_not raise_error expect(Status.find_by(id: reblog.id)).to be_nil end end describe '#replies_count' do it 'is the number of replies' do reply = Fabricate(:status, account: bob, thread: subject) expect(subject.replies_count).to eq 1 end it 'is decremented when reply is removed' do reply = Fabricate(:status, account: bob, thread: subject) expect(subject.replies_count).to eq 1 reply.destroy expect(subject.replies_count).to eq 0 end end describe '#favourites_count' do it 'is the number of favorites' do Fabricate(:favourite, account: bob, status: subject) Fabricate(:favourite, account: alice, status: subject) expect(subject.favourites_count).to eq 2 end it 'is decremented when favourite is removed' do favourite = Fabricate(:favourite, account: bob, status: subject) expect(subject.favourites_count).to eq 1 favourite.destroy expect(subject.favourites_count).to eq 0 end end describe '#proper' do it 'is itself for original statuses' do expect(subject.proper).to eq subject end it 'is the source status for reblogs' do subject.reblog = other expect(subject.proper).to eq other end end describe '.mutes_map' do let(:status) { Fabricate(:status) } let(:account) { Fabricate(:account) } subject { Status.mutes_map([status.conversation.id], account) } it 'returns a hash' do expect(subject).to be_a Hash end it 'contains true value' do account.mute_conversation!(status.conversation) expect(subject[status.conversation.id]).to be true end end describe '.favourites_map' do let(:status) { Fabricate(:status) } let(:account) { Fabricate(:account) } subject { Status.favourites_map([status], account) } it 'returns a hash' do expect(subject).to be_a Hash end it 'contains true value' do Fabricate(:favourite, status: status, account: account) expect(subject[status.id]).to be true end end describe '.reblogs_map' do let(:status) { Fabricate(:status) } let(:account) { Fabricate(:account) } subject { Status.reblogs_map([status], account) } it 'returns a hash' do expect(subject).to be_a Hash end it 'contains true value' do Fabricate(:status, account: account, reblog: status) expect(subject[status.id]).to be true end end describe '.in_chosen_languages' do context 'for accounts with language filters' do let(:user) { Fabricate(:user, chosen_languages: ['en']) } it 'does not include statuses in not in chosen languages' do status = Fabricate(:status, language: 'de') expect(Status.in_chosen_languages(user.account)).not_to include status end it 'includes status with unknown language' do status = Fabricate(:status, language: nil) expect(Status.in_chosen_languages(user.account)).to include status end end end describe '.as_home_timeline' do let(:account) { Fabricate(:account) } let(:followed) { Fabricate(:account) } let(:not_followed) { Fabricate(:account) } before do Fabricate(:follow, account: account, target_account: followed) @self_status = Fabricate(:status, account: account, visibility: :public) @self_direct_status = Fabricate(:status, account: account, visibility: :direct) @followed_status = Fabricate(:status, account: followed, visibility: :public) @followed_direct_status = Fabricate(:status, account: followed, visibility: :direct) @not_followed_status = Fabricate(:status, account: not_followed, visibility: :public) @results = Status.as_home_timeline(account) end it 'includes statuses from self' do expect(@results).to include(@self_status) end it 'does not include direct statuses from self' do expect(@results).to_not include(@self_direct_status) end it 'includes statuses from followed' do expect(@results).to include(@followed_status) end it 'does not include direct statuses mentioning recipient from followed' do Fabricate(:mention, account: account, status: @followed_direct_status) expect(@results).to_not include(@followed_direct_status) end it 'does not include direct statuses not mentioning recipient from followed' do expect(@results).not_to include(@followed_direct_status) end it 'does not include statuses from non-followed' do expect(@results).not_to include(@not_followed_status) end end describe '.as_direct_timeline' do let(:account) { Fabricate(:account) } let(:followed) { Fabricate(:account) } let(:not_followed) { Fabricate(:account) } before do Fabricate(:follow, account: account, target_account: followed) @self_public_status = Fabricate(:status, account: account, visibility: :public) @self_direct_status = Fabricate(:status, account: account, visibility: :direct) @followed_public_status = Fabricate(:status, account: followed, visibility: :public) @followed_direct_status = Fabricate(:status, account: followed, visibility: :direct) @not_followed_direct_status = Fabricate(:status, account: not_followed, visibility: :direct) @results = Status.as_direct_timeline(account) end it 'does not include public statuses from self' do expect(@results).to_not include(@self_public_status) end it 'includes direct statuses from self' do expect(@results).to include(@self_direct_status) end it 'does not include public statuses from followed' do expect(@results).to_not include(@followed_public_status) end it 'does not include direct statuses not mentioning recipient from followed' do expect(@results).to_not include(@followed_direct_status) end it 'does not include direct statuses not mentioning recipient from non-followed' do expect(@results).to_not include(@not_followed_direct_status) end it 'includes direct statuses mentioning recipient from followed' do Fabricate(:mention, account: account, status: @followed_direct_status) results2 = Status.as_direct_timeline(account) expect(results2).to include(@followed_direct_status) end it 'includes direct statuses mentioning recipient from non-followed' do Fabricate(:mention, account: account, status: @not_followed_direct_status) results2 = Status.as_direct_timeline(account) expect(results2).to include(@not_followed_direct_status) end end describe '.as_public_timeline' do it 'only includes statuses with public visibility' do public_status = Fabricate(:status, visibility: :public) private_status = Fabricate(:status, visibility: :private) results = Status.as_public_timeline expect(results).to include(public_status) expect(results).not_to include(private_status) end it 'does not include replies' do status = Fabricate(:status) reply = Fabricate(:status, in_reply_to_id: status.id) results = Status.as_public_timeline expect(results).to include(status) expect(results).not_to include(reply) end it 'does not include boosts' do status = Fabricate(:status) boost = Fabricate(:status, reblog_of_id: status.id) results = Status.as_public_timeline expect(results).to include(status) expect(results).not_to include(boost) end it 'filters out silenced accounts' do account = Fabricate(:account) silenced_account = Fabricate(:account, silenced: true) status = Fabricate(:status, account: account) silenced_status = Fabricate(:status, account: silenced_account) results = Status.as_public_timeline expect(results).to include(status) expect(results).not_to include(silenced_status) end context 'without local_only option' do let(:viewer) { nil } let!(:local_account) { Fabricate(:account, domain: nil) } let!(:remote_account) { Fabricate(:account, domain: 'test.com') } let!(:local_status) { Fabricate(:status, account: local_account) } let!(:remote_status) { Fabricate(:status, account: remote_account) } subject { Status.as_public_timeline(viewer, false) } context 'without a viewer' do let(:viewer) { nil } it 'includes remote instances statuses' do expect(subject).to include(remote_status) end it 'includes local statuses' do expect(subject).to include(local_status) end end context 'with a viewer' do let(:viewer) { Fabricate(:account, username: 'viewer') } it 'includes remote instances statuses' do expect(subject).to include(remote_status) end it 'includes local statuses' do expect(subject).to include(local_status) end end end context 'with a local_only option set' do let!(:local_account) { Fabricate(:account, domain: nil) } let!(:remote_account) { Fabricate(:account, domain: 'test.com') } let!(:local_status) { Fabricate(:status, account: local_account) } let!(:remote_status) { Fabricate(:status, account: remote_account) } subject { Status.as_public_timeline(viewer, true) } context 'without a viewer' do let(:viewer) { nil } it 'does not include remote instances statuses' do expect(subject).to include(local_status) expect(subject).not_to include(remote_status) end end context 'with a viewer' do let(:viewer) { Fabricate(:account, username: 'viewer') } it 'does not include remote instances statuses' do expect(subject).to include(local_status) expect(subject).not_to include(remote_status) end it 'is not affected by personal domain blocks' do viewer.block_domain!('test.com') expect(subject).to include(local_status) expect(subject).not_to include(remote_status) end end end describe 'with an account passed in' do before do @account = Fabricate(:account) end it 'excludes statuses from accounts blocked by the account' do blocked = Fabricate(:account) Fabricate(:block, account: @account, target_account: blocked) blocked_status = Fabricate(:status, account: blocked) results = Status.as_public_timeline(@account) expect(results).not_to include(blocked_status) end it 'excludes statuses from accounts who have blocked the account' do blocked = Fabricate(:account) Fabricate(:block, account: blocked, target_account: @account) blocked_status = Fabricate(:status, account: blocked) results = Status.as_public_timeline(@account) expect(results).not_to include(blocked_status) end it 'excludes statuses from accounts muted by the account' do muted = Fabricate(:account) Fabricate(:mute, account: @account, target_account: muted) muted_status = Fabricate(:status, account: muted) results = Status.as_public_timeline(@account) expect(results).not_to include(muted_status) end it 'excludes statuses from accounts from personally blocked domains' do blocked = Fabricate(:account, domain: 'example.com') @account.block_domain!(blocked.domain) blocked_status = Fabricate(:status, account: blocked) results = Status.as_public_timeline(@account) expect(results).not_to include(blocked_status) end context 'with language preferences' do it 'excludes statuses in languages not allowed by the account user' do user = Fabricate(:user, chosen_languages: [:en, :es]) @account.update(user: user) en_status = Fabricate(:status, language: 'en') es_status = Fabricate(:status, language: 'es') fr_status = Fabricate(:status, language: 'fr') results = Status.as_public_timeline(@account) expect(results).to include(en_status) expect(results).to include(es_status) expect(results).not_to include(fr_status) end it 'includes all languages when user does not have a setting' do user = Fabricate(:user, chosen_languages: nil) @account.update(user: user) en_status = Fabricate(:status, language: 'en') es_status = Fabricate(:status, language: 'es') results = Status.as_public_timeline(@account) expect(results).to include(en_status) expect(results).to include(es_status) end it 'includes all languages when account does not have a user' do expect(@account.user).to be_nil en_status = Fabricate(:status, language: 'en') es_status = Fabricate(:status, language: 'es') results = Status.as_public_timeline(@account) expect(results).to include(en_status) expect(results).to include(es_status) end end end end describe '.as_tag_timeline' do it 'includes statuses with a tag' do tag = Fabricate(:tag) status = Fabricate(:status, tags: [tag]) other = Fabricate(:status) results = Status.as_tag_timeline(tag) expect(results).to include(status) expect(results).not_to include(other) end it 'allows replies to be included' do original = Fabricate(:status) tag = Fabricate(:tag) status = Fabricate(:status, tags: [tag], in_reply_to_id: original.id) results = Status.as_tag_timeline(tag) expect(results).to include(status) end end describe '.permitted_for' do subject { described_class.permitted_for(target_account, account).pluck(:visibility) } let(:target_account) { alice } let(:account) { bob } let!(:public_status) { Fabricate(:status, account: target_account, visibility: 'public') } let!(:unlisted_status) { Fabricate(:status, account: target_account, visibility: 'unlisted') } let!(:private_status) { Fabricate(:status, account: target_account, visibility: 'private') } let!(:direct_status) do Fabricate(:status, account: target_account, visibility: 'direct').tap do |status| Fabricate(:mention, status: status, account: account) end end let!(:other_direct_status) do Fabricate(:status, account: target_account, visibility: 'direct').tap do |status| Fabricate(:mention, status: status) end end context 'given nil' do let(:account) { nil } let(:direct_status) { nil } it { is_expected.to eq(%w(unlisted public)) } end context 'given blocked account' do before do target_account.block!(account) end it { is_expected.to be_empty } end context 'given same account' do let(:account) { target_account } it { is_expected.to eq(%w(direct direct private unlisted public)) } end context 'given followed account' do before do account.follow!(target_account) end it { is_expected.to eq(%w(direct private unlisted public)) } end context 'given unfollowed account' do it { is_expected.to eq(%w(direct unlisted public)) } end end describe 'before_validation' do it 'sets account being replied to correctly over intermediary nodes' do first_status = Fabricate(:status, account: bob) intermediary = Fabricate(:status, thread: first_status, account: alice) final = Fabricate(:status, thread: intermediary, account: alice) expect(final.in_reply_to_account_id).to eq bob.id end it 'creates new conversation for stand-alone status' do expect(Status.create(account: alice, text: 'First').conversation_id).to_not be_nil end it 'keeps conversation of parent node' do parent = Fabricate(:status, text: 'First') expect(Status.create(account: alice, thread: parent, text: 'Response').conversation_id).to eq parent.conversation_id end it 'sets `local` to true for status by local account' do expect(Status.create(account: alice, text: 'foo').local).to be true end it 'sets `local` to false for status by remote account' do alice.update(domain: 'example.com') expect(Status.create(account: alice, text: 'foo').local).to be false end end describe 'validation' do it 'disallow empty uri for remote status' do alice.update(domain: 'example.com') status = Fabricate.build(:status, uri: '', account: alice) expect(status).to model_have_error_on_field(:uri) end end describe 'after_create' do it 'saves ActivityPub uri as uri for local status' do status = Status.create(account: alice, text: 'foo') status.reload expect(status.uri).to start_with('https://') end end end ================================================ FILE: spec/models/status_stat_spec.rb ================================================ require 'rails_helper' RSpec.describe StatusStat, type: :model do end ================================================ FILE: spec/models/stream_entry_spec.rb ================================================ require 'rails_helper' RSpec.describe StreamEntry, type: :model do let(:alice) { Fabricate(:account, username: 'alice') } let(:bob) { Fabricate(:account, username: 'bob') } let(:status) { Fabricate(:status, account: alice) } let(:reblog) { Fabricate(:status, account: bob, reblog: status) } let(:reply) { Fabricate(:status, account: bob, thread: status) } let(:stream_entry) { Fabricate(:stream_entry, activity: activity) } let(:activity) { reblog } describe '#object_type' do before do allow(stream_entry).to receive(:orphaned?).and_return(orphaned) allow(stream_entry).to receive(:targeted?).and_return(targeted) end subject { stream_entry.object_type } context 'orphaned? is true' do let(:orphaned) { true } let(:targeted) { false } it 'returns :activity' do is_expected.to be :activity end end context 'targeted? is true' do let(:orphaned) { false } let(:targeted) { true } it 'returns :activity' do is_expected.to be :activity end end context 'orphaned? and targeted? are false' do let(:orphaned) { false } let(:targeted) { false } context 'activity is reblog' do let(:activity) { reblog } it 'returns :note' do is_expected.to be :note end end context 'activity is reply' do let(:activity) { reply } it 'returns :comment' do is_expected.to be :comment end end end end describe '#verb' do before do allow(stream_entry).to receive(:orphaned?).and_return(orphaned) end subject { stream_entry.verb } context 'orphaned? is true' do let(:orphaned) { true } it 'returns :delete' do is_expected.to be :delete end end context 'orphaned? is false' do let(:orphaned) { false } context 'activity is reblog' do let(:activity) { reblog } it 'returns :share' do is_expected.to be :share end end context 'activity is reply' do let(:activity) { reply } it 'returns :post' do is_expected.to be :post end end end end describe '#mentions' do before do allow(stream_entry).to receive(:orphaned?).and_return(orphaned) end subject { stream_entry.mentions } context 'orphaned? is true' do let(:orphaned) { true } it 'returns []' do is_expected.to eq [] end end context 'orphaned? is false' do before do reblog.mentions << Fabricate(:mention, account: alice) reblog.mentions << Fabricate(:mention, account: bob) end let(:orphaned) { false } it 'returns [Account] includes alice and bob' do is_expected.to eq [alice, bob] end end end describe '#targeted?' do it 'returns true for a reblog' do expect(reblog.stream_entry.targeted?).to be true end it 'returns false otherwise' do expect(status.stream_entry.targeted?).to be false end end describe '#threaded?' do it 'returns true for a reply' do expect(reply.stream_entry.threaded?).to be true end it 'returns false otherwise' do expect(status.stream_entry.threaded?).to be false end end describe 'delegated methods' do context 'with a nil status' do subject { described_class.new(status: nil) } it 'returns nil for target' do expect(subject.target).to be_nil end it 'returns nil for title' do expect(subject.title).to be_nil end it 'returns nil for content' do expect(subject.content).to be_nil end it 'returns nil for thread' do expect(subject.thread).to be_nil end end context 'with a real status' do let(:original) { Fabricate(:status, text: 'Test status') } let(:status) { Fabricate(:status, reblog: original, thread: original) } subject { described_class.new(status: status) } it 'delegates target' do expect(status.target).not_to be_nil expect(subject.target).to eq(status.target) end it 'delegates title' do expect(status.title).not_to be_nil expect(subject.title).to eq(status.title) end it 'delegates content' do expect(status.content).not_to be_nil expect(subject.content).to eq(status.content) end it 'delegates thread' do expect(status.thread).not_to be_nil expect(subject.thread).to eq(status.thread) end end end end ================================================ FILE: spec/models/subscription_spec.rb ================================================ require 'rails_helper' RSpec.describe Subscription, type: :model do let(:alice) { Fabricate(:account, username: 'alice') } subject { Fabricate(:subscription, account: alice) } describe '#expired?' do it 'return true when expires_at is past' do subject.expires_at = 2.days.ago expect(subject.expired?).to be true end it 'return false when expires_at is future' do subject.expires_at = 2.days.from_now expect(subject.expired?).to be false end end describe 'lease_seconds' do it 'returns the time remaining until expiration' do datetime = 1.day.from_now subscription = Subscription.new(expires_at: datetime) travel_to(datetime - 12.hours) do expect(subscription.lease_seconds).to eq(12.hours) end end end describe 'lease_seconds=' do it 'sets expires_at to min expiration when small value is provided' do subscription = Subscription.new datetime = 1.day.from_now too_low = Subscription::MIN_EXPIRATION - 1000 travel_to(datetime) do subscription.lease_seconds = too_low end expected = datetime + Subscription::MIN_EXPIRATION.seconds expect(subscription.expires_at).to be_within(1.0).of(expected) end it 'sets expires_at to value when valid value is provided' do subscription = Subscription.new datetime = 1.day.from_now valid = Subscription::MIN_EXPIRATION + 1000 travel_to(datetime) do subscription.lease_seconds = valid end expected = datetime + valid.seconds expect(subscription.expires_at).to be_within(1.0).of(expected) end it 'sets expires_at to max expiration when large value is provided' do subscription = Subscription.new datetime = 1.day.from_now too_high = Subscription::MAX_EXPIRATION + 1000 travel_to(datetime) do subscription.lease_seconds = too_high end expected = datetime + Subscription::MAX_EXPIRATION.seconds expect(subscription.expires_at).to be_within(1.0).of(expected) end end end ================================================ FILE: spec/models/tag_spec.rb ================================================ require 'rails_helper' RSpec.describe Tag, type: :model do describe 'validations' do it 'invalid with #' do expect(Tag.new(name: '#hello_world')).to_not be_valid end it 'invalid with .' do expect(Tag.new(name: '.abcdef123')).to_not be_valid end it 'invalid with spaces' do expect(Tag.new(name: 'hello world')).to_not be_valid end it 'valid with aesthetic' do expect(Tag.new(name: 'aesthetic')).to be_valid end end describe 'HASHTAG_RE' do subject { Tag::HASHTAG_RE } it 'does not match URLs with anchors with non-hashtag characters' do expect(subject.match('Check this out https://medium.com/@alice/some-article#.abcdef123')).to be_nil end it 'does not match URLs with hashtag-like anchors' do expect(subject.match('https://en.wikipedia.org/wiki/Ghostbusters_(song)#Lawsuit')).to be_nil end it 'matches #aesthetic' do expect(subject.match('this is #aesthetic')).to_not be_nil end end describe '#to_param' do it 'returns name' do tag = Fabricate(:tag, name: 'foo') expect(tag.to_param).to eq 'foo' end end describe '.search_for' do it 'finds tag records with matching names' do tag = Fabricate(:tag, name: "match") _miss_tag = Fabricate(:tag, name: "miss") results = Tag.search_for("match") expect(results).to eq [tag] end it 'finds tag records in case insensitive' do tag = Fabricate(:tag, name: "MATCH") _miss_tag = Fabricate(:tag, name: "miss") results = Tag.search_for("match") expect(results).to eq [tag] end it 'finds the exact matching tag as the first item' do similar_tag = Fabricate(:tag, name: "matchlater") tag = Fabricate(:tag, name: "match") results = Tag.search_for("match") expect(results).to eq [tag, similar_tag] end end end ================================================ FILE: spec/models/user_invite_request_spec.rb ================================================ require 'rails_helper' RSpec.describe UserInviteRequest, type: :model do end ================================================ FILE: spec/models/user_spec.rb ================================================ require 'rails_helper' require 'devise_two_factor/spec_helpers' RSpec.describe User, type: :model do it_behaves_like 'two_factor_backupable' describe 'otp_secret' do it 'is encrypted with OTP_SECRET environment variable' do user = Fabricate(:user, encrypted_otp_secret: "Fttsy7QAa0edaDfdfSz094rRLAxc8cJweDQ4BsWH/zozcdVA8o9GLqcKhn2b\nGi/V\n", encrypted_otp_secret_iv: 'rys3THICkr60BoWC', encrypted_otp_secret_salt: '_LMkAGvdg7a+sDIKjI3mR2Q==') expect(user.otp_secret).to eq 'anotpsecretthatshouldbeencrypted' end end describe 'validations' do it 'is invalid without an account' do user = Fabricate.build(:user, account: nil) user.valid? expect(user).to model_have_error_on_field(:account) end it 'is invalid without a valid locale' do user = Fabricate.build(:user, locale: 'toto') user.valid? expect(user).to model_have_error_on_field(:locale) end it 'is invalid without a valid email' do user = Fabricate.build(:user, email: 'john@') user.valid? expect(user).to model_have_error_on_field(:email) end it 'is valid with an invalid e-mail that has already been saved' do user = Fabricate.build(:user, email: 'invalid-email') user.save(validate: false) expect(user.valid?).to be true end it 'cleans out empty string from languages' do user = Fabricate.build(:user, chosen_languages: ['']) user.valid? expect(user.chosen_languages).to eq nil end end describe 'scopes' do describe 'recent' do it 'returns an array of recent users ordered by id' do user_1 = Fabricate(:user) user_2 = Fabricate(:user) expect(User.recent).to eq [user_2, user_1] end end describe 'admins' do it 'returns an array of users who are admin' do user_1 = Fabricate(:user, admin: false) user_2 = Fabricate(:user, admin: true) expect(User.admins).to match_array([user_2]) end end describe 'confirmed' do it 'returns an array of users who are confirmed' do user_1 = Fabricate(:user, confirmed_at: nil) user_2 = Fabricate(:user, confirmed_at: Time.zone.now) expect(User.confirmed).to match_array([user_2]) end end describe 'inactive' do it 'returns a relation of inactive users' do specified = Fabricate(:user, current_sign_in_at: 15.days.ago) Fabricate(:user, current_sign_in_at: 6.days.ago) expect(User.inactive).to match_array([specified]) end end describe 'matches_email' do it 'returns a relation of users whose email starts with the given string' do specified = Fabricate(:user, email: 'specified@spec') Fabricate(:user, email: 'unspecified@spec') expect(User.matches_email('specified')).to match_array([specified]) end end end let(:account) { Fabricate(:account, username: 'alice') } let(:password) { 'abcd1234' } describe 'blacklist' do around(:each) do |example| old_blacklist = Rails.configuration.x.email_blacklist Rails.configuration.x.email_domains_blacklist = 'mvrht.com' example.run Rails.configuration.x.email_domains_blacklist = old_blacklist end it 'should allow a non-blacklisted user to be created' do user = User.new(email: 'foo@example.com', account: account, password: password, agreement: true) expect(user.valid?).to be_truthy end it 'should not allow a blacklisted user to be created' do user = User.new(email: 'foo@mvrht.com', account: account, password: password, agreement: true) expect(user.valid?).to be_falsey end it 'should not allow a subdomain blacklisted user to be created' do user = User.new(email: 'foo@mvrht.com.topdomain.tld', account: account, password: password, agreement: true) expect(user.valid?).to be_falsey end end describe '#confirmed?' do it 'returns true when a confirmed_at is set' do user = Fabricate.build(:user, confirmed_at: Time.now.utc) expect(user.confirmed?).to be true end it 'returns false if a confirmed_at is nil' do user = Fabricate.build(:user, confirmed_at: nil) expect(user.confirmed?).to be false end end describe '#confirm' do it 'sets email to unconfirmed_email' do user = Fabricate.build(:user, confirmed_at: Time.now.utc, unconfirmed_email: 'new-email@example.com') user.confirm expect(user.email).to eq 'new-email@example.com' end end describe '#disable_two_factor!' do it 'saves false for otp_required_for_login' do user = Fabricate.build(:user, otp_required_for_login: true) user.disable_two_factor! expect(user.reload.otp_required_for_login).to be false end it 'saves cleared otp_backup_codes' do user = Fabricate.build(:user, otp_backup_codes: %w(dummy dummy)) user.disable_two_factor! expect(user.reload.otp_backup_codes.empty?).to be true end end describe '#send_confirmation_instructions' do around do |example| queue_adapter = ActiveJob::Base.queue_adapter example.run ActiveJob::Base.queue_adapter = queue_adapter end it 'delivers confirmation instructions later' do user = Fabricate(:user) ActiveJob::Base.queue_adapter = :test expect { user.send_confirmation_instructions }.to have_enqueued_job(ActionMailer::DeliveryJob) end end describe 'settings' do it 'is instance of Settings::ScopedSettings' do user = Fabricate(:user) expect(user.settings).to be_kind_of Settings::ScopedSettings end end describe '#setting_default_privacy' do it 'returns default privacy setting if user has configured' do user = Fabricate(:user) user.settings[:default_privacy] = 'unlisted' expect(user.setting_default_privacy).to eq 'unlisted' end it "returns 'private' if user has not configured default privacy setting and account is locked" do user = Fabricate(:user, account: Fabricate(:account, locked: true)) expect(user.setting_default_privacy).to eq 'private' end it "returns 'public' if user has not configured default privacy setting and account is not locked" do user = Fabricate(:user, account: Fabricate(:account, locked: false)) expect(user.setting_default_privacy).to eq 'public' end end describe 'whitelist' do around(:each) do |example| old_whitelist = Rails.configuration.x.email_whitelist Rails.configuration.x.email_domains_whitelist = 'mastodon.space' example.run Rails.configuration.x.email_domains_whitelist = old_whitelist end it 'should not allow a user to be created unless they are whitelisted' do user = User.new(email: 'foo@example.com', account: account, password: password, agreement: true) expect(user.valid?).to be_falsey end it 'should allow a user to be created if they are whitelisted' do user = User.new(email: 'foo@mastodon.space', account: account, password: password, agreement: true) expect(user.valid?).to be_truthy end it 'should not allow a user with a whitelisted top domain as subdomain in their email address to be created' do user = User.new(email: 'foo@mastodon.space.userdomain.com', account: account, password: password, agreement: true) expect(user.valid?).to be_falsey end context do around do |example| old_blacklist = Rails.configuration.x.email_blacklist example.run Rails.configuration.x.email_domains_blacklist = old_blacklist end it 'should not allow a user to be created with a specific blacklisted subdomain even if the top domain is whitelisted' do Rails.configuration.x.email_domains_blacklist = 'blacklisted.mastodon.space' user = User.new(email: 'foo@blacklisted.mastodon.space', account: account, password: password) expect(user.valid?).to be_falsey end end end it_behaves_like 'Settings-extended' do def create! User.create!(account: Fabricate(:account), email: 'foo@mastodon.space', password: 'abcd1234', agreement: true) end def fabricate Fabricate(:user) end end describe 'token_for_app' do let(:user) { Fabricate(:user) } let(:app) { Fabricate(:application, owner: user) } it 'returns a token' do expect(user.token_for_app(app)).to be_a(Doorkeeper::AccessToken) end it 'persists a token' do t = user.token_for_app(app) expect(user.token_for_app(app)).to eql(t) end it 'is nil if user does not own app' do app.update!(owner: nil) expect(user.token_for_app(app)).to be_nil end end describe '#role' do it 'returns admin for admin' do user = User.new(admin: true) expect(user.role).to eq 'admin' end it 'returns moderator for moderator' do user = User.new(moderator: true) expect(user.role).to eq 'moderator' end it 'returns user otherwise' do user = User.new expect(user.role).to eq 'user' end end describe '#role?' do it 'returns false when invalid role requested' do user = User.new(admin: true) expect(user.role?('disabled')).to be false end it 'returns true when exact role match' do user = User.new mod = User.new(moderator: true) admin = User.new(admin: true) expect(user.role?('user')).to be true expect(mod.role?('moderator')).to be true expect(admin.role?('admin')).to be true end it 'returns true when role higher than needed' do mod = User.new(moderator: true) admin = User.new(admin: true) expect(mod.role?('user')).to be true expect(admin.role?('user')).to be true expect(admin.role?('moderator')).to be true end end describe '#disable!' do subject(:user) { Fabricate(:user, disabled: false, current_sign_in_at: current_sign_in_at, last_sign_in_at: nil) } let(:current_sign_in_at) { Time.zone.now } before do user.disable! end it 'disables user' do expect(user).to have_attributes(disabled: true, current_sign_in_at: nil, last_sign_in_at: current_sign_in_at) end end describe '#disable!' do subject(:user) { Fabricate(:user, disabled: false, current_sign_in_at: current_sign_in_at, last_sign_in_at: nil) } let(:current_sign_in_at) { Time.zone.now } before do user.disable! end it 'disables user' do expect(user).to have_attributes(disabled: true, current_sign_in_at: nil, last_sign_in_at: current_sign_in_at) end end describe '#enable!' do subject(:user) { Fabricate(:user, disabled: true) } before do user.enable! end it 'enables user' do expect(user).to have_attributes(disabled: false) end end describe '#confirm!' do subject(:user) { Fabricate(:user, confirmed_at: confirmed_at) } before do ActionMailer::Base.deliveries.clear user.confirm! end after { ActionMailer::Base.deliveries.clear } context 'when user is new' do let(:confirmed_at) { nil } it 'confirms user' do expect(user.confirmed_at).to be_present end it 'delivers mails' do expect(ActionMailer::Base.deliveries.count).to eq 2 end end context 'when user is not new' do let(:confirmed_at) { Time.zone.now } it 'confirms user' do expect(user.confirmed_at).to be_present end it 'does not deliver mail' do expect(ActionMailer::Base.deliveries.count).to eq 0 end end end describe '#promote!' do subject(:user) { Fabricate(:user, admin: is_admin, moderator: is_moderator) } before do user.promote! end context 'when user is an admin' do let(:is_admin) { true } context 'when user is a moderator' do let(:is_moderator) { true } it 'changes moderator filed false' do expect(user).to be_admin expect(user).not_to be_moderator end end context 'when user is not a moderator' do let(:is_moderator) { false } it 'does not change status' do expect(user).to be_admin expect(user).not_to be_moderator end end end context 'when user is not admin' do let(:is_admin) { false } context 'when user is a moderator' do let(:is_moderator) { true } it 'changes user into an admin' do expect(user).to be_admin expect(user).not_to be_moderator end end context 'when user is not a moderator' do let(:is_moderator) { false } it 'changes user into a moderator' do expect(user).not_to be_admin expect(user).to be_moderator end end end end describe '#demote!' do subject(:user) { Fabricate(:user, admin: admin, moderator: moderator) } before do user.demote! end context 'when user is an admin' do let(:admin) { true } context 'when user is a moderator' do let(:moderator) { true } it 'changes user into a moderator' do expect(user).not_to be_admin expect(user).to be_moderator end end context 'when user is not a moderator' do let(:moderator) { false } it 'changes user into a moderator' do expect(user).not_to be_admin expect(user).to be_moderator end end end context 'when user is not an admin' do let(:admin) { false } context 'when user is a moderator' do let(:moderator) { true } it 'changes user into a plain user' do expect(user).not_to be_admin expect(user).not_to be_moderator end end context 'when user is not a moderator' do let(:moderator) { false } it 'does not change any fields' do expect(user).not_to be_admin expect(user).not_to be_moderator end end end end describe '#active_for_authentication?' do subject { user.active_for_authentication? } let(:user) { Fabricate(:user, disabled: disabled, confirmed_at: confirmed_at) } context 'when user is disabled' do let(:disabled) { true } context 'when user is confirmed' do let(:confirmed_at) { Time.zone.now } it { is_expected.to be true } end context 'when user is not confirmed' do let(:confirmed_at) { nil } it { is_expected.to be false } end end context 'when user is not disabled' do let(:disabled) { false } context 'when user is confirmed' do let(:confirmed_at) { Time.zone.now } it { is_expected.to be true } end context 'when user is not confirmed' do let(:confirmed_at) { nil } it { is_expected.to be false } end end end end ================================================ FILE: spec/models/web/push_subscription_spec.rb ================================================ require 'rails_helper' RSpec.describe Web::PushSubscription, type: :model do let(:alerts) { { mention: true, reblog: false, follow: true, follow_request: false, favourite: true } } let(:push_subscription) { Web::PushSubscription.new(data: { alerts: alerts }) } describe '#pushable?' do it 'obeys alert settings' do expect(push_subscription.send(:pushable?, Notification.new(activity_type: 'Mention'))).to eq true expect(push_subscription.send(:pushable?, Notification.new(activity_type: 'Status'))).to eq false expect(push_subscription.send(:pushable?, Notification.new(activity_type: 'Follow'))).to eq true expect(push_subscription.send(:pushable?, Notification.new(activity_type: 'FollowRequest'))).to eq false expect(push_subscription.send(:pushable?, Notification.new(activity_type: 'Favourite'))).to eq true end end end ================================================ FILE: spec/models/web/setting_spec.rb ================================================ require 'rails_helper' RSpec.describe Web::Setting, type: :model do end ================================================ FILE: spec/policies/account_moderation_note_policy_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' require 'pundit/rspec' RSpec.describe AccountModerationNotePolicy do let(:subject) { described_class } let(:admin) { Fabricate(:user, admin: true).account } let(:john) { Fabricate(:user).account } permissions :create? do context 'staff' do it 'grants to create' do expect(subject).to permit(admin, AccountModerationNotePolicy) end end context 'not staff' do it 'denies to create' do expect(subject).to_not permit(john, AccountModerationNotePolicy) end end end permissions :destroy? do let(:account_moderation_note) do Fabricate(:account_moderation_note, account: john, target_account: Fabricate(:account)) end context 'admin' do it 'grants to destroy' do expect(subject).to permit(admin, AccountModerationNotePolicy) end end context 'owner' do it 'grants to destroy' do expect(subject).to permit(john, account_moderation_note) end end context 'neither admin nor owner' do let(:kevin) { Fabricate(:user).account } it 'denies to destroy' do expect(subject).to_not permit(kevin, account_moderation_note) end end end end ================================================ FILE: spec/policies/account_policy_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' require 'pundit/rspec' RSpec.describe AccountPolicy do let(:subject) { described_class } let(:admin) { Fabricate(:user, admin: true).account } let(:john) { Fabricate(:user).account } permissions :index?, :show?, :unsuspend?, :unsilence?, :remove_avatar?, :remove_header? do context 'staff' do it 'permits' do expect(subject).to permit(admin) end end context 'not staff' do it 'denies' do expect(subject).to_not permit(john) end end end permissions :redownload?, :subscribe?, :unsubscribe? do context 'admin' do it 'permits' do expect(subject).to permit(admin) end end context 'not admin' do it 'denies' do expect(subject).to_not permit(john) end end end permissions :suspend?, :silence? do let(:staff) { Fabricate(:user, admin: true).account } context 'staff' do context 'record is staff' do it 'denies' do expect(subject).to_not permit(admin, staff) end end context 'record is not staff' do it 'permits' do expect(subject).to permit(admin, john) end end end context 'not staff' do it 'denies' do expect(subject).to_not permit(john, Account) end end end permissions :memorialize? do let(:other_admin) { Fabricate(:user, admin: true).account } context 'admin' do context 'record is admin' do it 'denies' do expect(subject).to_not permit(admin, other_admin) end end context 'record is not admin' do it 'permits' do expect(subject).to permit(admin, john) end end end context 'not admin' do it 'denies' do expect(subject).to_not permit(john, Account) end end end end ================================================ FILE: spec/policies/backup_policy_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' require 'pundit/rspec' RSpec.describe BackupPolicy do let(:subject) { described_class } let(:john) { Fabricate(:user).account } permissions :create? do context 'not user_signed_in?' do it 'denies' do expect(subject).to_not permit(nil, Backup) end end context 'user_signed_in?' do context 'no backups' do it 'permits' do expect(subject).to permit(john, Backup) end end context 'backups are too old' do it 'permits' do travel(-8.days) do Fabricate(:backup, user: john.user) end expect(subject).to permit(john, Backup) end end context 'backups are newer' do it 'denies' do travel(-3.days) do Fabricate(:backup, user: john.user) end expect(subject).to_not permit(john, Backup) end end end end end ================================================ FILE: spec/policies/custom_emoji_policy_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' require 'pundit/rspec' RSpec.describe CustomEmojiPolicy do let(:subject) { described_class } let(:admin) { Fabricate(:user, admin: true).account } let(:john) { Fabricate(:user).account } permissions :index?, :enable?, :disable? do context 'staff' do it 'permits' do expect(subject).to permit(admin, CustomEmoji) end end context 'not staff' do it 'denies' do expect(subject).to_not permit(john, CustomEmoji) end end end permissions :create?, :update?, :copy?, :destroy? do context 'admin' do it 'permits' do expect(subject).to permit(admin, CustomEmoji) end end context 'not admin' do it 'denies' do expect(subject).to_not permit(john, CustomEmoji) end end end end ================================================ FILE: spec/policies/domain_block_policy_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' require 'pundit/rspec' RSpec.describe DomainBlockPolicy do let(:subject) { described_class } let(:admin) { Fabricate(:user, admin: true).account } let(:john) { Fabricate(:user).account } permissions :index?, :show?, :create?, :destroy? do context 'admin' do it 'permits' do expect(subject).to permit(admin, DomainBlock) end end context 'not admin' do it 'denies' do expect(subject).to_not permit(john, DomainBlock) end end end end ================================================ FILE: spec/policies/email_domain_block_policy_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' require 'pundit/rspec' RSpec.describe EmailDomainBlockPolicy do let(:subject) { described_class } let(:admin) { Fabricate(:user, admin: true).account } let(:john) { Fabricate(:user).account } permissions :index?, :create?, :destroy? do context 'admin' do it 'permits' do expect(subject).to permit(admin, EmailDomainBlock) end end context 'not admin' do it 'denies' do expect(subject).to_not permit(john, EmailDomainBlock) end end end end ================================================ FILE: spec/policies/instance_policy_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' require 'pundit/rspec' RSpec.describe InstancePolicy do let(:subject) { described_class } let(:admin) { Fabricate(:user, admin: true).account } let(:john) { Fabricate(:user).account } permissions :index? do context 'admin' do it 'permits' do expect(subject).to permit(admin, Instance) end end context 'not admin' do it 'denies' do expect(subject).to_not permit(john, Instance) end end end end ================================================ FILE: spec/policies/invite_policy_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' require 'pundit/rspec' RSpec.describe InvitePolicy do let(:subject) { described_class } let(:admin) { Fabricate(:user, admin: true).account } let(:john) { Fabricate(:user).account } permissions :index? do context 'staff?' do it 'permits' do expect(subject).to permit(admin, Invite) end end end permissions :create? do context 'min_required_role?' do it 'permits' do allow_any_instance_of(described_class).to receive(:min_required_role?) { true } expect(subject).to permit(john, Invite) end end context 'not min_required_role?' do it 'denies' do allow_any_instance_of(described_class).to receive(:min_required_role?) { false } expect(subject).to_not permit(john, Invite) end end end permissions :deactivate_all? do context 'admin?' do it 'permits' do expect(subject).to permit(admin, Invite) end end context 'not admin?' do it 'denies' do expect(subject).to_not permit(john, Invite) end end end permissions :destroy? do context 'owner?' do it 'permits' do expect(subject).to permit(john, Fabricate(:invite, user: john.user)) end end context 'not owner?' do context 'Setting.min_invite_role == "admin"' do before do Setting.min_invite_role = 'admin' end context 'admin?' do it 'permits' do expect(subject).to permit(admin, Fabricate(:invite)) end end context 'not admin?' do it 'denies' do expect(subject).to_not permit(john, Fabricate(:invite)) end end end context 'Setting.min_invite_role != "admin"' do before do Setting.min_invite_role = 'else' end context 'staff?' do it 'permits' do expect(subject).to permit(admin, Fabricate(:invite)) end end context 'not staff?' do it 'denies' do expect(subject).to_not permit(john, Fabricate(:invite)) end end end end end end ================================================ FILE: spec/policies/relay_policy_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' require 'pundit/rspec' RSpec.describe RelayPolicy do let(:subject) { described_class } let(:admin) { Fabricate(:user, admin: true).account } let(:john) { Fabricate(:user).account } permissions :update? do context 'admin?' do it 'permits' do expect(subject).to permit(admin, Relay) end end context '!admin?' do it 'denies' do expect(subject).to_not permit(john, Relay) end end end end ================================================ FILE: spec/policies/report_note_policy_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' require 'pundit/rspec' RSpec.describe ReportNotePolicy do let(:subject) { described_class } let(:admin) { Fabricate(:user, admin: true).account } let(:john) { Fabricate(:user).account } permissions :create? do context 'staff?' do it 'permits' do expect(subject).to permit(admin, ReportNote) end end context '!staff?' do it 'denies' do expect(subject).to_not permit(john, ReportNote) end end end permissions :destroy? do context 'admin?' do it 'permit' do expect(subject).to permit(admin, ReportNote) end end context 'admin?' do context 'owner?' do it 'permit' do report_note = Fabricate(:report_note, account: john) expect(subject).to permit(john, report_note) end end context '!owner?' do it 'denies' do report_note = Fabricate(:report_note) expect(subject).to_not permit(john, report_note) end end end end end ================================================ FILE: spec/policies/report_policy_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' require 'pundit/rspec' RSpec.describe ReportPolicy do let(:subject) { described_class } let(:admin) { Fabricate(:user, admin: true).account } let(:john) { Fabricate(:user).account } permissions :update?, :index?, :show? do context 'staff?' do it 'permits' do expect(subject).to permit(admin, Report) end end context '!staff?' do it 'denies' do expect(subject).to_not permit(john, Report) end end end end ================================================ FILE: spec/policies/settings_policy_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' require 'pundit/rspec' RSpec.describe SettingsPolicy do let(:subject) { described_class } let(:admin) { Fabricate(:user, admin: true).account } let(:john) { Fabricate(:user).account } permissions :update?, :show? do context 'admin?' do it 'permits' do expect(subject).to permit(admin, Settings) end end context '!admin?' do it 'denies' do expect(subject).to_not permit(john, Settings) end end end end ================================================ FILE: spec/policies/status_policy_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' require 'pundit/rspec' RSpec.describe StatusPolicy, type: :model do subject { described_class } let(:admin) { Fabricate(:user, admin: true) } let(:alice) { Fabricate(:account, username: 'alice') } let(:bob) { Fabricate(:account, username: 'bob') } let(:status) { Fabricate(:status, account: alice) } permissions :show?, :reblog? do it 'grants access when no viewer' do expect(subject).to permit(nil, status) end it 'denies access when viewer is blocked' do block = Fabricate(:block) status.visibility = :private status.account = block.target_account expect(subject).to_not permit(block.account, status) end end permissions :show? do it 'grants access when direct and account is viewer' do status.visibility = :direct expect(subject).to permit(status.account, status) end it 'grants access when direct and viewer is mentioned' do status.visibility = :direct status.mentions = [Fabricate(:mention, account: alice)] expect(subject).to permit(alice, status) end it 'denies access when direct and viewer is not mentioned' do viewer = Fabricate(:account) status.visibility = :direct expect(subject).to_not permit(viewer, status) end it 'grants access when private and account is viewer' do status.visibility = :private expect(subject).to permit(status.account, status) end it 'grants access when private and account is following viewer' do follow = Fabricate(:follow) status.visibility = :private status.account = follow.target_account expect(subject).to permit(follow.account, status) end it 'grants access when private and viewer is mentioned' do status.visibility = :private status.mentions = [Fabricate(:mention, account: alice)] expect(subject).to permit(alice, status) end it 'denies access when private and viewer is not mentioned or followed' do viewer = Fabricate(:account) status.visibility = :private expect(subject).to_not permit(viewer, status) end end permissions :reblog? do it 'denies access when private' do viewer = Fabricate(:account) status.visibility = :private expect(subject).to_not permit(viewer, status) end it 'denies access when direct' do viewer = Fabricate(:account) status.visibility = :direct expect(subject).to_not permit(viewer, status) end end permissions :destroy?, :unreblog? do it 'grants access when account is deleter' do expect(subject).to permit(status.account, status) end it 'grants access when account is admin' do expect(subject).to permit(admin.account, status) end it 'denies access when account is not deleter' do expect(subject).to_not permit(bob, status) end it 'denies access when no deleter' do expect(subject).to_not permit(nil, status) end end permissions :favourite? do it 'grants access when viewer is not blocked' do follow = Fabricate(:follow) status.account = follow.target_account expect(subject).to permit(follow.account, status) end it 'denies when viewer is blocked' do block = Fabricate(:block) status.account = block.target_account expect(subject).to_not permit(block.account, status) end end permissions :index?, :update? do it 'grants access if staff' do expect(subject).to permit(admin.account) end it 'denies access unless staff' do expect(subject).to_not permit(alice) end end end ================================================ FILE: spec/policies/subscription_policy_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' require 'pundit/rspec' RSpec.describe SubscriptionPolicy do let(:subject) { described_class } let(:admin) { Fabricate(:user, admin: true).account } let(:john) { Fabricate(:user).account } permissions :index? do context 'admin?' do it 'permits' do expect(subject).to permit(admin, Subscription) end end context '!admin?' do it 'denies' do expect(subject).to_not permit(john, Subscription) end end end end ================================================ FILE: spec/policies/tag_policy_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' require 'pundit/rspec' RSpec.describe TagPolicy do let(:subject) { described_class } let(:admin) { Fabricate(:user, admin: true).account } let(:john) { Fabricate(:user).account } permissions :index?, :hide?, :unhide? do context 'staff?' do it 'permits' do expect(subject).to permit(admin, Tag) end end context '!staff?' do it 'denies' do expect(subject).to_not permit(john, Tag) end end end end ================================================ FILE: spec/policies/user_policy_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' require 'pundit/rspec' RSpec.describe UserPolicy do let(:subject) { described_class } let(:admin) { Fabricate(:user, admin: true).account } let(:john) { Fabricate(:user).account } permissions :reset_password?, :change_email? do context 'staff?' do context '!record.staff?' do it 'permits' do expect(subject).to permit(admin, john.user) end end context 'record.staff?' do it 'denies' do expect(subject).to_not permit(admin, admin.user) end end end context '!staff?' do it 'denies' do expect(subject).to_not permit(john, User) end end end permissions :disable_2fa? do context 'admin?' do context '!record.staff?' do it 'permits' do expect(subject).to permit(admin, john.user) end end context 'record.staff?' do it 'denies' do expect(subject).to_not permit(admin, admin.user) end end end context '!admin?' do it 'denies' do expect(subject).to_not permit(john, User) end end end permissions :confirm? do context 'staff?' do context '!record.confirmed?' do it 'permits' do john.user.update(confirmed_at: nil) expect(subject).to permit(admin, john.user) end end context 'record.confirmed?' do it 'denies' do john.user.confirm! expect(subject).to_not permit(admin, john.user) end end end context '!staff?' do it 'denies' do expect(subject).to_not permit(john, User) end end end permissions :enable? do context 'staff?' do it 'permits' do expect(subject).to permit(admin, User) end end context '!staff?' do it 'denies' do expect(subject).to_not permit(john, User) end end end permissions :disable? do context 'staff?' do context '!record.admin?' do it 'permits' do expect(subject).to permit(admin, john.user) end end context 'record.admin?' do it 'denies' do expect(subject).to_not permit(admin, admin.user) end end end context '!staff?' do it 'denies' do expect(subject).to_not permit(john, User) end end end permissions :promote? do context 'admin?' do context 'promoteable?' do it 'permits' do expect(subject).to permit(admin, john.user) end end context '!promoteable?' do it 'denies' do expect(subject).to_not permit(admin, admin.user) end end end context '!admin?' do it 'denies' do expect(subject).to_not permit(john, User) end end end permissions :demote? do context 'admin?' do context '!record.admin?' do context 'demoteable?' do it 'permits' do john.user.update(moderator: true) expect(subject).to permit(admin, john.user) end end context '!demoteable?' do it 'denies' do expect(subject).to_not permit(admin, john.user) end end end context 'record.admin?' do it 'denies' do expect(subject).to_not permit(admin, admin.user) end end end context '!admin?' do it 'denies' do expect(subject).to_not permit(john, User) end end end end ================================================ FILE: spec/presenters/account_relationships_presenter_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' RSpec.describe AccountRelationshipsPresenter do describe '.initialize' do before do allow(Account).to receive(:following_map).with(account_ids, current_account_id).and_return(default_map) allow(Account).to receive(:followed_by_map).with(account_ids, current_account_id).and_return(default_map) allow(Account).to receive(:blocking_map).with(account_ids, current_account_id).and_return(default_map) allow(Account).to receive(:muting_map).with(account_ids, current_account_id).and_return(default_map) allow(Account).to receive(:requested_map).with(account_ids, current_account_id).and_return(default_map) allow(Account).to receive(:domain_blocking_map).with(account_ids, current_account_id).and_return(default_map) end let(:presenter) { AccountRelationshipsPresenter.new(account_ids, current_account_id, options) } let(:current_account_id) { Fabricate(:account).id } let(:account_ids) { [Fabricate(:account).id] } let(:default_map) { { 1 => true } } context 'options are not set' do let(:options) { {} } it 'sets default maps' do expect(presenter.following).to eq default_map expect(presenter.followed_by).to eq default_map expect(presenter.blocking).to eq default_map expect(presenter.muting).to eq default_map expect(presenter.requested).to eq default_map expect(presenter.domain_blocking).to eq default_map end end context 'options[:following_map] is set' do let(:options) { { following_map: { 2 => true } } } it 'sets @following merged with default_map and options[:following_map]' do expect(presenter.following).to eq default_map.merge(options[:following_map]) end end context 'options[:followed_by_map] is set' do let(:options) { { followed_by_map: { 3 => true } } } it 'sets @followed_by merged with default_map and options[:followed_by_map]' do expect(presenter.followed_by).to eq default_map.merge(options[:followed_by_map]) end end context 'options[:blocking_map] is set' do let(:options) { { blocking_map: { 4 => true } } } it 'sets @blocking merged with default_map and options[:blocking_map]' do expect(presenter.blocking).to eq default_map.merge(options[:blocking_map]) end end context 'options[:muting_map] is set' do let(:options) { { muting_map: { 5 => true } } } it 'sets @muting merged with default_map and options[:muting_map]' do expect(presenter.muting).to eq default_map.merge(options[:muting_map]) end end context 'options[:requested_map] is set' do let(:options) { { requested_map: { 6 => true } } } it 'sets @requested merged with default_map and options[:requested_map]' do expect(presenter.requested).to eq default_map.merge(options[:requested_map]) end end context 'options[:domain_blocking_map] is set' do let(:options) { { domain_blocking_map: { 7 => true } } } it 'sets @domain_blocking merged with default_map and options[:domain_blocking_map]' do expect(presenter.domain_blocking).to eq default_map.merge(options[:domain_blocking_map]) end end end end ================================================ FILE: spec/presenters/instance_presenter_spec.rb ================================================ require 'rails_helper' describe InstancePresenter do let(:instance_presenter) { InstancePresenter.new } context do around do |example| site_description = Setting.site_description example.run Setting.site_description = site_description end it "delegates site_description to Setting" do Setting.site_description = "Site desc" expect(instance_presenter.site_description).to eq "Site desc" end end context do around do |example| site_extended_description = Setting.site_extended_description example.run Setting.site_extended_description = site_extended_description end it "delegates site_extended_description to Setting" do Setting.site_extended_description = "Extended desc" expect(instance_presenter.site_extended_description).to eq "Extended desc" end end context do around do |example| site_contact_email = Setting.site_contact_email example.run Setting.site_contact_email = site_contact_email end it "delegates contact_email to Setting" do Setting.site_contact_email = "admin@example.com" expect(instance_presenter.site_contact_email).to eq "admin@example.com" end end describe "contact_account" do around do |example| site_contact_username = Setting.site_contact_username example.run Setting.site_contact_username = site_contact_username end it "returns the account for the site contact username" do Setting.site_contact_username = "aaa" account = Fabricate(:account, username: "aaa") expect(instance_presenter.contact_account).to eq(account) end end describe "user_count" do it "returns the number of site users" do Rails.cache.write 'user_count', 123 expect(instance_presenter.user_count).to eq(123) end end describe "status_count" do it "returns the number of local statuses" do Rails.cache.write 'local_status_count', 234 expect(instance_presenter.status_count).to eq(234) end end describe "domain_count" do it "returns the number of known domains" do Rails.cache.write 'distinct_domain_count', 345 expect(instance_presenter.domain_count).to eq(345) end end describe '#version_number' do it 'returns Florence::Version' do expect(instance_presenter.version_number).to be(Florence::Version) end end describe '#source_url' do it 'returns "https://github.com/florence-social/mastodon-fork"' do expect(instance_presenter.source_url).to eq('https://github.com/florence-social/mastodon-fork') end end describe '#thumbnail' do it 'returns SiteUpload' do thumbnail = Fabricate(:site_upload, var: 'thumbnail') expect(instance_presenter.thumbnail).to eq(thumbnail) end end describe '#hero' do it 'returns SiteUpload' do hero = Fabricate(:site_upload, var: 'hero') expect(instance_presenter.hero).to eq(hero) end end describe '#mascot' do it 'returns SiteUpload' do mascot = Fabricate(:site_upload, var: 'mascot') expect(instance_presenter.mascot).to eq(mascot) end end end ================================================ FILE: spec/rails_helper.rb ================================================ ENV['RAILS_ENV'] ||= 'test' require File.expand_path('../../config/environment', __FILE__) abort("The Rails environment is running in production mode!") if Rails.env.production? require 'spec_helper' require 'rspec/rails' require 'webmock/rspec' require 'paperclip/matchers' require 'capybara/rspec' Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } ActiveRecord::Migration.maintain_test_schema! WebMock.disable_net_connect! Redis.current = Redis::Namespace.new("mastodon_test#{ENV['TEST_ENV_NUMBER']}", redis: Redis.current) Sidekiq::Testing.inline! Sidekiq::Logging.logger = nil Devise::Test::ControllerHelpers.module_eval do alias_method :original_sign_in, :sign_in def sign_in(resource, _deprecated = nil, scope: nil) original_sign_in(resource, scope: scope) SessionActivation.deactivate warden.cookies.signed['_session_id'] warden.cookies.signed['_session_id'] = { value: resource.activate_session(warden.request), expires: 1.year.from_now, httponly: true, } end end RSpec.configure do |config| config.fixture_path = "#{::Rails.root}/spec/fixtures" config.use_transactional_fixtures = true config.order = 'random' config.infer_spec_type_from_file_location! config.filter_rails_from_backtrace! config.include Devise::Test::ControllerHelpers, type: :controller config.include Devise::Test::ControllerHelpers, type: :view config.include Paperclip::Shoulda::Matchers config.include ActiveSupport::Testing::TimeHelpers config.before :each, type: :feature do https = ENV['LOCAL_HTTPS'] == 'true' Capybara.app_host = "http#{https ? 's' : ''}://#{ENV.fetch('LOCAL_DOMAIN')}" end config.before :each, type: :controller do stub_jsonld_contexts! end config.before :each, type: :service do stub_jsonld_contexts! end config.after :each do Rails.cache.clear keys = Redis.current.keys Redis.current.del(keys) if keys.any? end end RSpec::Sidekiq.configure do |config| config.warn_when_jobs_not_processed_by_sidekiq = false end def request_fixture(name) File.read(Rails.root.join('spec', 'fixtures', 'requests', name)) end def attachment_fixture(name) File.open(Rails.root.join('spec', 'fixtures', 'files', name)) end def stub_jsonld_contexts! stub_request(:get, 'https://www.w3.org/ns/activitystreams').to_return(request_fixture('json-ld.activitystreams.txt')) stub_request(:get, 'https://w3id.org/identity/v1').to_return(request_fixture('json-ld.identity.txt')) stub_request(:get, 'https://w3id.org/security/v1').to_return(request_fixture('json-ld.security.txt')) end ================================================ FILE: spec/requests/account_show_page_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe 'The account show page' do it 'Has an h-feed with correct number of h-entry objects in it' do alice = Fabricate(:account, username: 'alice', display_name: 'Alice') _status = Fabricate(:status, account: alice, text: 'Hello World') _status2 = Fabricate(:status, account: alice, text: 'Hello World Again') _status3 = Fabricate(:status, account: alice, text: 'Are You Still There World?') get '/@alice' expect(h_feed_entries.size).to eq(3) end it 'has valid opengraph tags' do alice = Fabricate(:account, username: 'alice', display_name: 'Alice') _status = Fabricate(:status, account: alice, text: 'Hello World') get '/@alice' expect(head_meta_content('og:title')).to match alice.display_name expect(head_meta_content('og:type')).to eq 'profile' expect(head_meta_content('og:image')).to match '.+' expect(head_meta_content('og:url')).to match 'http://.+' end def head_meta_content(property) head_section.meta("[@property='#{property}']")[:content] end def head_section Nokogiri::Slop(response.body).html.head end def h_feed_entries Nokogiri::HTML(response.body).search('.h-feed .h-entry') end end ================================================ FILE: spec/requests/catch_all_route_request_spec.rb ================================================ require "rails_helper" describe "The catch all route" do describe "with a simple value" do it "returns a 404 page as html" do get "/test" expect(response.status).to eq 404 expect(response.content_type).to eq "text/html" end end describe "with an implied format" do it "returns a 404 page as html" do get "/test.test" expect(response.status).to eq 404 expect(response.content_type).to eq "text/html" end end end ================================================ FILE: spec/requests/host_meta_request_spec.rb ================================================ require "rails_helper" describe "The host_meta route" do describe "requested without accepts headers" do it "returns an xml response" do get host_meta_url expect(response).to have_http_status(200) expect(response.content_type).to eq "application/xrd+xml" end end end ================================================ FILE: spec/requests/link_headers_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe 'Link headers' do describe 'on the account show page' do let(:account) { Fabricate(:account, username: 'test') } before do get short_account_path(username: account) end it 'contains webfinger url in link header' do link_header = link_header_with_type('application/xrd+xml') expect(link_header.href).to match 'http://www.example.com/.well-known/webfinger?resource=acct%3Atest%40cb6e6126.ngrok.io' expect(link_header.attr_pairs.first).to eq %w(rel lrdd) end it 'contains atom url in link header' do link_header = link_header_with_type('application/atom+xml') expect(link_header.href).to eq 'http://www.example.com/users/test.atom' expect(link_header.attr_pairs.first).to eq %w(rel alternate) end def link_header_with_type(type) response.headers['Link'].links.find do |link| link.attr_pairs.any? { |pair| pair == ['type', type] } end end end end ================================================ FILE: spec/requests/localization_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe 'Localization' do after(:all) do I18n.locale = I18n.default_locale end it 'uses a specific region when provided' do headers = { 'Accept-Language' => 'zh-HK' } get "/about", headers: headers expect(response.body).to include( I18n.t('about.tagline', locale: 'zh-HK') ) end it 'falls back to a locale when region missing' do headers = { 'Accept-Language' => 'es-FAKE' } get "/about", headers: headers expect(response.body).to include( I18n.t('about.tagline', locale: 'es') ) end it 'falls back to english when locale is missing' do headers = { 'Accept-Language' => '12-FAKE' } get "/about", headers: headers expect(response.body).to include( I18n.t('about.tagline', locale: 'en') ) end end ================================================ FILE: spec/requests/webfinger_request_spec.rb ================================================ require 'rails_helper' describe 'The webfinger route' do let(:alice) { Fabricate(:account, username: 'alice') } describe 'requested with standard accepts headers' do it 'returns a json response' do get webfinger_url(resource: alice.to_webfinger_s) expect(response).to have_http_status(200) expect(response.content_type).to eq 'application/jrd+json' end end describe 'asking for xml format' do it 'returns an xml response for xml format' do get webfinger_url(resource: alice.to_webfinger_s, format: :xml) expect(response).to have_http_status(200) expect(response.content_type).to eq 'application/xrd+xml' end it 'returns an xml response for xml accept header' do headers = { 'HTTP_ACCEPT' => 'application/xrd+xml' } get webfinger_url(resource: alice.to_webfinger_s), headers: headers expect(response).to have_http_status(200) expect(response.content_type).to eq 'application/xrd+xml' end end describe 'asking for json format' do it 'returns a json response for json format' do get webfinger_url(resource: alice.to_webfinger_s, format: :json) expect(response).to have_http_status(200) expect(response.content_type).to eq 'application/jrd+json' end it 'returns a json response for json accept header' do headers = { 'HTTP_ACCEPT' => 'application/jrd+json' } get webfinger_url(resource: alice.to_webfinger_s), headers: headers expect(response).to have_http_status(200) expect(response.content_type).to eq 'application/jrd+json' end end end ================================================ FILE: spec/routing/accounts_routing_spec.rb ================================================ require 'rails_helper' describe 'Routes under accounts/' do describe 'the route for accounts who are followers of an account' do it 'routes to the followers action with the right username' do expect(get('/users/name/followers')). to route_to('follower_accounts#index', account_username: 'name') end end describe 'the route for accounts who are followed by an account' do it 'routes to the following action with the right username' do expect(get('/users/name/following')). to route_to('following_accounts#index', account_username: 'name') end end describe 'the route for following an account' do it 'routes to the follow create action with the right username' do expect(post('/users/name/follow')). to route_to('account_follow#create', account_username: 'name') end end describe 'the route for unfollowing an account' do it 'routes to the unfollow create action with the right username' do expect(post('/users/name/unfollow')). to route_to('account_unfollow#create', account_username: 'name') end end end ================================================ FILE: spec/routing/api_routing_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe 'API routes' do describe 'Credentials routes' do it 'routes to verify credentials' do expect(get('/api/v1/accounts/verify_credentials')). to route_to('api/v1/accounts/credentials#show') end it 'routes to update credentials' do expect(patch('/api/v1/accounts/update_credentials')). to route_to('api/v1/accounts/credentials#update') end end describe 'Account routes' do it 'routes to statuses' do expect(get('/api/v1/accounts/user/statuses')). to route_to('api/v1/accounts/statuses#index', account_id: 'user') end it 'routes to followers' do expect(get('/api/v1/accounts/user/followers')). to route_to('api/v1/accounts/follower_accounts#index', account_id: 'user') end it 'routes to following' do expect(get('/api/v1/accounts/user/following')). to route_to('api/v1/accounts/following_accounts#index', account_id: 'user') end it 'routes to search' do expect(get('/api/v1/accounts/search')). to route_to('api/v1/accounts/search#show') end it 'routes to relationships' do expect(get('/api/v1/accounts/relationships')). to route_to('api/v1/accounts/relationships#index') end end describe 'Statuses routes' do it 'routes reblogged_by' do expect(get('/api/v1/statuses/123/reblogged_by')). to route_to('api/v1/statuses/reblogged_by_accounts#index', status_id: '123') end it 'routes favourited_by' do expect(get('/api/v1/statuses/123/favourited_by')). to route_to('api/v1/statuses/favourited_by_accounts#index', status_id: '123') end it 'routes reblog' do expect(post('/api/v1/statuses/123/reblog')). to route_to('api/v1/statuses/reblogs#create', status_id: '123') end it 'routes unreblog' do expect(post('/api/v1/statuses/123/unreblog')). to route_to('api/v1/statuses/reblogs#destroy', status_id: '123') end it 'routes favourite' do expect(post('/api/v1/statuses/123/favourite')). to route_to('api/v1/statuses/favourites#create', status_id: '123') end it 'routes unfavourite' do expect(post('/api/v1/statuses/123/unfavourite')). to route_to('api/v1/statuses/favourites#destroy', status_id: '123') end it 'routes mute' do expect(post('/api/v1/statuses/123/mute')). to route_to('api/v1/statuses/mutes#create', status_id: '123') end it 'routes unmute' do expect(post('/api/v1/statuses/123/unmute')). to route_to('api/v1/statuses/mutes#destroy', status_id: '123') end end describe 'Timeline routes' do it 'routes to home timeline' do expect(get('/api/v1/timelines/home')). to route_to('api/v1/timelines/home#show') end it 'routes to public timeline' do expect(get('/api/v1/timelines/public')). to route_to('api/v1/timelines/public#show') end it 'routes to tag timeline' do expect(get('/api/v1/timelines/tag/test')). to route_to('api/v1/timelines/tag#show', id: 'test') end end end ================================================ FILE: spec/routing/well_known_routes_spec.rb ================================================ require 'rails_helper' describe 'the host-meta route' do it 'routes to correct place with xml format' do expect(get('/.well-known/host-meta')). to route_to('well_known/host_meta#show', format: 'xml') end end describe 'the webfinger route' do it 'routes to correct place with json format' do expect(get('/.well-known/webfinger')). to route_to('well_known/webfinger#show') end end ================================================ FILE: spec/serializers/activitypub/note_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe ActivityPub::NoteSerializer do let!(:account) { Fabricate(:account) } let!(:other) { Fabricate(:account) } let!(:parent) { Fabricate(:status, account: account, visibility: :public) } let!(:reply1) { Fabricate(:status, account: account, thread: parent, visibility: :public) } let!(:reply2) { Fabricate(:status, account: account, thread: parent, visibility: :public) } let!(:reply3) { Fabricate(:status, account: other, thread: parent, visibility: :public) } let!(:reply4) { Fabricate(:status, account: account, thread: parent, visibility: :public) } let!(:reply5) { Fabricate(:status, account: account, thread: parent, visibility: :direct) } before(:each) do @serialization = ActiveModelSerializers::SerializableResource.new(parent, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter) end subject { JSON.parse(@serialization.to_json) } it 'has a Note type' do expect(subject['type']).to eql('Note') end it 'has a replies collection' do expect(subject['replies']['type']).to eql('Collection') end it 'has a replies collection with a first Page' do expect(subject['replies']['first']['type']).to eql('CollectionPage') end it 'includes public self-replies in its replies collection' do expect(subject['replies']['first']['items']).to include(reply1.uri, reply2.uri, reply4.uri) end it 'does not include replies from others in its replies collection' do expect(subject['replies']['first']['items']).to_not include(reply3.uri) end it 'does not include replies with direct visibility in its replies collection' do expect(subject['replies']['first']['items']).to_not include(reply5.uri) end end ================================================ FILE: spec/services/account_search_service_spec.rb ================================================ require 'rails_helper' describe AccountSearchService, type: :service do describe '.call' do describe 'with a query to ignore' do it 'returns empty array for missing query' do results = subject.call('', nil, limit: 10) expect(results).to eq [] end it 'returns empty array for hashtag query' do results = subject.call('#tag', nil, limit: 10) expect(results).to eq [] end it 'returns empty array for limit zero' do Fabricate(:account, username: 'match') results = subject.call('match', nil, limit: 0) expect(results).to eq [] end end describe 'searching for a simple term that is not an exact match' do it 'does not return a nil entry in the array for the exact match' do match = Fabricate(:account, username: 'matchingusername') results = subject.call('match', nil, limit: 5) expect(results).to eq [match] end end describe 'searching local and remote users' do describe "when only '@'" do before do allow(Account).to receive(:find_local) allow(Account).to receive(:search_for) subject.call('@', nil, limit: 10) end it 'uses find_local with empty query to look for local accounts' do expect(Account).to have_received(:find_local).with('') end end describe 'when no domain' do before do allow(Account).to receive(:find_local) allow(Account).to receive(:search_for) subject.call('one', nil, limit: 10) end it 'uses find_local to look for local accounts' do expect(Account).to have_received(:find_local).with('one') end it 'uses search_for to find matches' do expect(Account).to have_received(:search_for).with('one', 10, 0) end end describe 'when there is a domain' do before do allow(Account).to receive(:find_remote) end it 'uses find_remote to look for remote accounts' do subject.call('two@example.com', nil, limit: 10) expect(Account).to have_received(:find_remote).with('two', 'example.com') end describe 'and there is no account provided' do it 'uses search_for to find matches' do allow(Account).to receive(:search_for) subject.call('two@example.com', nil, limit: 10, resolve: false) expect(Account).to have_received(:search_for).with('two example.com', 10, 0) end end describe 'and there is an account provided' do it 'uses advanced_search_for to find matches' do account = Fabricate(:account) allow(Account).to receive(:advanced_search_for) subject.call('two@example.com', account, limit: 10, resolve: false) expect(Account).to have_received(:advanced_search_for).with('two example.com', account, 10, nil, 0) end end end end describe 'with an exact match' do it 'returns exact match first, and does not return duplicates' do partial = Fabricate(:account, username: 'exactness') exact = Fabricate(:account, username: 'exact') results = subject.call('exact', nil, limit: 10) expect(results.size).to eq 2 expect(results).to eq [exact, partial] end end describe 'when there is a local domain' do around do |example| before = Rails.configuration.x.local_domain example.run Rails.configuration.x.local_domain = before end it 'returns exact match first' do remote = Fabricate(:account, username: 'a', domain: 'remote', display_name: 'e') remote_too = Fabricate(:account, username: 'b', domain: 'remote', display_name: 'e') exact = Fabricate(:account, username: 'e') Rails.configuration.x.local_domain = 'example.com' results = subject.call('e@example.com', nil, limit: 2) expect(results.size).to eq 2 expect(results).to eq([exact, remote]).or eq([exact, remote_too]) end end describe 'when there is a domain but no exact match' do it 'follows the remote account when resolve is true' do service = double(call: nil) allow(ResolveAccountService).to receive(:new).and_return(service) results = subject.call('newuser@remote.com', nil, limit: 10, resolve: true) expect(service).to have_received(:call).with('newuser@remote.com') end it 'does not follow the remote account when resolve is false' do service = double(call: nil) allow(ResolveAccountService).to receive(:new).and_return(service) results = subject.call('newuser@remote.com', nil, limit: 10, resolve: false) expect(service).not_to have_received(:call) end end describe 'should not include suspended accounts' do it 'returns the fuzzy match first, and does not return suspended exacts' do partial = Fabricate(:account, username: 'exactness') exact = Fabricate(:account, username: 'exact', suspended: true) results = subject.call('exact', nil, limit: 10) expect(results.size).to eq 1 expect(results).to eq [partial] end it "does not return suspended remote accounts" do remote = Fabricate(:account, username: 'a', domain: 'remote', display_name: 'e', suspended: true) results = subject.call('a@example.com', nil, limit: 2) expect(results.size).to eq 0 expect(results).to eq [] end end end end ================================================ FILE: spec/services/activitypub/fetch_remote_account_service_spec.rb ================================================ require 'rails_helper' RSpec.describe ActivityPub::FetchRemoteAccountService, type: :service do subject { ActivityPub::FetchRemoteAccountService.new } let!(:actor) do { '@context': 'https://www.w3.org/ns/activitystreams', id: 'https://example.com/alice', type: 'Person', preferredUsername: 'alice', name: 'Alice', summary: 'Foo bar', inbox: 'http://example.com/alice/inbox', } end describe '#call' do let(:account) { subject.call('https://example.com/alice', id: true) } shared_examples 'sets profile data' do it 'returns an account' do expect(account).to be_an Account end it 'sets display name' do expect(account.display_name).to eq 'Alice' end it 'sets note' do expect(account.note).to eq 'Foo bar' end it 'sets URL' do expect(account.url).to eq 'https://example.com/alice' end end context 'when the account does not have a inbox' do let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } } before do actor[:inbox] = nil stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor)) stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) end it 'fetches resource' do account expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once end it 'looks up webfinger' do account expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once end it 'returns nil' do expect(account).to be_nil end end context 'when URI and WebFinger share the same host' do let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } } before do stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor)) stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) end it 'fetches resource' do account expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once end it 'looks up webfinger' do account expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once end it 'sets username and domain from webfinger' do expect(account.username).to eq 'alice' expect(account.domain).to eq 'example.com' end include_examples 'sets profile data' end context 'when WebFinger presents different domain than URI' do let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/alice' }] } } before do stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor)) stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) end it 'fetches resource' do account expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once end it 'looks up webfinger' do account expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once end it 'looks up "redirected" webfinger' do account expect(a_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af')).to have_been_made.once end it 'sets username and domain from final webfinger' do expect(account.username).to eq 'alice' expect(account.domain).to eq 'iscool.af' end include_examples 'sets profile data' end context 'with wrong id' do it 'does not create account' do expect(subject.call('https://fake.address/@foo', prefetched_body: Oj.dump(actor))).to be_nil end end end end ================================================ FILE: spec/services/activitypub/fetch_remote_status_service_spec.rb ================================================ require 'rails_helper' RSpec.describe ActivityPub::FetchRemoteStatusService, type: :service do include ActionView::Helpers::TextHelper let(:sender) { Fabricate(:account) } let(:recipient) { Fabricate(:account) } let(:valid_domain) { Rails.configuration.x.local_domain } let(:note) do { '@context': 'https://www.w3.org/ns/activitystreams', id: "https://#{valid_domain}/@foo/1234", type: 'Note', content: 'Lorem ipsum', attributedTo: ActivityPub::TagManager.instance.uri_for(sender), } end subject { described_class.new } describe '#call' do before do sender.update(uri: ActivityPub::TagManager.instance.uri_for(sender)) stub_request(:head, 'https://example.com/watch?v=12345').to_return(status: 404, body: '') subject.call(object[:id], prefetched_body: Oj.dump(object)) end context 'with Note object' do let(:object) { note } it 'creates status' do status = sender.statuses.first expect(status).to_not be_nil expect(status.text).to eq 'Lorem ipsum' end end context 'with Video object' do let(:object) do { '@context': 'https://www.w3.org/ns/activitystreams', id: "https://#{valid_domain}/@foo/1234", type: 'Video', name: 'Nyan Cat 10 hours remix', attributedTo: ActivityPub::TagManager.instance.uri_for(sender), url: [ { type: 'Link', mimeType: 'application/x-bittorrent', href: "https://#{valid_domain}/12345.torrent", }, { type: 'Link', mimeType: 'text/html', href: "https://#{valid_domain}/watch?v=12345", }, ], } end it 'creates status' do status = sender.statuses.first expect(status).to_not be_nil expect(status.url).to eq "https://#{valid_domain}/watch?v=12345" expect(strip_tags(status.text)).to eq "Nyan Cat 10 hours remix https://#{valid_domain}/watch?v=12345" end end context 'with wrong id' do let(:note) do { '@context': 'https://www.w3.org/ns/activitystreams', id: "https://real.address/@foo/1234", type: 'Note', content: 'Lorem ipsum', attributedTo: ActivityPub::TagManager.instance.uri_for(sender), } end let(:object) do temp = note.dup temp[:id] = 'https://fake.address/@foo/5678' temp end it 'does not create status' do expect(sender.statuses.first).to be_nil end end end end ================================================ FILE: spec/services/activitypub/fetch_replies_service_spec.rb ================================================ require 'rails_helper' RSpec.describe ActivityPub::FetchRepliesService, type: :service do let(:actor) { Fabricate(:account, domain: 'example.com', uri: 'http://example.com/account') } let(:status) { Fabricate(:status, account: actor) } let(:collection_uri) { 'http://example.com/replies/1' } let(:items) do [ 'http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://other.com/other-reply-1', 'http://other.com/other-reply-2', 'http://other.com/other-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5', 'http://example.com/self-reply-6', ] end let(:payload) do { '@context': 'https://www.w3.org/ns/activitystreams', type: 'Collection', id: collection_uri, items: items, }.with_indifferent_access end subject { described_class.new } describe '#call' do context 'when the payload is a Collection with inlined replies' do context 'when passing the collection itself' do it 'spawns workers for up to 5 replies on the same server' do allow(FetchReplyWorker).to receive(:push_bulk) subject.call(status, payload) expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5']) end end context 'when passing the URL to the collection' do before do stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload)) end it 'spawns workers for up to 5 replies on the same server' do allow(FetchReplyWorker).to receive(:push_bulk) subject.call(status, collection_uri) expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5']) end end end context 'when the payload is an OrderedCollection with inlined replies' do let(:payload) do { '@context': 'https://www.w3.org/ns/activitystreams', type: 'OrderedCollection', id: collection_uri, orderedItems: items, }.with_indifferent_access end context 'when passing the collection itself' do it 'spawns workers for up to 5 replies on the same server' do allow(FetchReplyWorker).to receive(:push_bulk) subject.call(status, payload) expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5']) end end context 'when passing the URL to the collection' do before do stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload)) end it 'spawns workers for up to 5 replies on the same server' do allow(FetchReplyWorker).to receive(:push_bulk) subject.call(status, collection_uri) expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5']) end end end context 'when the payload is a paginated Collection with inlined replies' do let(:payload) do { '@context': 'https://www.w3.org/ns/activitystreams', type: 'Collection', id: collection_uri, first: { type: 'CollectionPage', partOf: collection_uri, items: items, } }.with_indifferent_access end context 'when passing the collection itself' do it 'spawns workers for up to 5 replies on the same server' do allow(FetchReplyWorker).to receive(:push_bulk) subject.call(status, payload) expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5']) end end context 'when passing the URL to the collection' do before do stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload)) end it 'spawns workers for up to 5 replies on the same server' do allow(FetchReplyWorker).to receive(:push_bulk) subject.call(status, collection_uri) expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5']) end end end end end ================================================ FILE: spec/services/activitypub/process_account_service_spec.rb ================================================ require 'rails_helper' RSpec.describe ActivityPub::ProcessAccountService, type: :service do subject { described_class.new } context 'property values' do let(:payload) do { id: 'https://foo.test', type: 'Actor', inbox: 'https://foo.test/inbox', attachment: [ { type: 'PropertyValue', name: 'Pronouns', value: 'They/them' }, { type: 'PropertyValue', name: 'Occupation', value: 'Unit test' }, ], }.with_indifferent_access end it 'parses out of attachment' do account = subject.call('alice', 'example.com', payload) expect(account.fields).to be_a Array expect(account.fields.size).to eq 2 expect(account.fields[0]).to be_a Account::Field expect(account.fields[0].name).to eq 'Pronouns' expect(account.fields[0].value).to eq 'They/them' expect(account.fields[1]).to be_a Account::Field expect(account.fields[1].name).to eq 'Occupation' expect(account.fields[1].value).to eq 'Unit test' end end context 'identity proofs' do let(:payload) do { id: 'https://foo.test', type: 'Actor', inbox: 'https://foo.test/inbox', attachment: [ { type: 'IdentityProof', name: 'Alice', signatureAlgorithm: 'keybase', signatureValue: 'a' * 66 }, ], }.with_indifferent_access end it 'parses out of attachment' do allow(ProofProvider::Keybase::Worker).to receive(:perform_async) account = subject.call('alice', 'example.com', payload) expect(account.identity_proofs.count).to eq 1 proof = account.identity_proofs.first expect(proof.provider).to eq 'keybase' expect(proof.provider_username).to eq 'Alice' expect(proof.token).to eq 'a' * 66 end it 'removes no longer present proofs' do allow(ProofProvider::Keybase::Worker).to receive(:perform_async) account = Fabricate(:account, username: 'alice', domain: 'example.com') old_proof = Fabricate(:account_identity_proof, account: account, provider: 'keybase', provider_username: 'Bob', token: 'b' * 66) subject.call('alice', 'example.com', payload) expect(account.identity_proofs.count).to eq 1 expect(account.identity_proofs.find_by(id: old_proof.id)).to be_nil end it 'queues a validity check on the proof' do allow(ProofProvider::Keybase::Worker).to receive(:perform_async) account = subject.call('alice', 'example.com', payload) expect(ProofProvider::Keybase::Worker).to have_received(:perform_async) end end end ================================================ FILE: spec/services/activitypub/process_collection_service_spec.rb ================================================ require 'rails_helper' RSpec.describe ActivityPub::ProcessCollectionService, type: :service do let(:actor) { Fabricate(:account, domain: 'example.com', uri: 'http://example.com/account') } let(:payload) do { '@context': 'https://www.w3.org/ns/activitystreams', id: 'foo', type: 'Create', actor: ActivityPub::TagManager.instance.uri_for(actor), object: { id: 'bar', type: 'Note', content: 'Lorem ipsum', }, } end let(:json) { Oj.dump(payload) } subject { described_class.new } describe '#call' do context 'when actor is the sender' context 'when actor differs from sender' do let(:forwarder) { Fabricate(:account, domain: 'example.com', uri: 'http://example.com/other_account') } it 'does not process payload if no signature exists' do expect_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_account!).and_return(nil) expect(ActivityPub::Activity).not_to receive(:factory) subject.call(json, forwarder) end it 'processes payload with actor if valid signature exists' do payload['signature'] = { 'type' => 'RsaSignature2017' } expect_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_account!).and_return(actor) expect(ActivityPub::Activity).to receive(:factory).with(instance_of(Hash), actor, instance_of(Hash)) subject.call(json, forwarder) end it 'does not process payload if invalid signature exists' do payload['signature'] = { 'type' => 'RsaSignature2017' } expect_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_account!).and_return(nil) expect(ActivityPub::Activity).not_to receive(:factory) subject.call(json, forwarder) end end end end ================================================ FILE: spec/services/after_block_domain_from_account_service_spec.rb ================================================ require 'rails_helper' RSpec.describe AfterBlockDomainFromAccountService, type: :service do let!(:wolf) { Fabricate(:account, username: 'wolf', domain: 'evil.org', inbox_url: 'https://evil.org/inbox', protocol: :activitypub) } let!(:alice) { Fabricate(:account, username: 'alice') } subject { AfterBlockDomainFromAccountService.new } before do stub_jsonld_contexts! allow(ActivityPub::DeliveryWorker).to receive(:perform_async) end it 'purge followers from blocked domain' do wolf.follow!(alice) subject.call(alice, 'evil.org') expect(wolf.following?(alice)).to be false end it 'sends Reject->Follow to followers from blocked domain' do wolf.follow!(alice) subject.call(alice, 'evil.org') expect(ActivityPub::DeliveryWorker).to have_received(:perform_async).once end end ================================================ FILE: spec/services/after_block_service_spec.rb ================================================ require 'rails_helper' RSpec.describe AfterBlockService, type: :service do subject do -> { described_class.new.call(account, target_account) } end let(:account) { Fabricate(:account) } let(:target_account) { Fabricate(:account) } describe 'home timeline' do let(:status) { Fabricate(:status, account: target_account) } let(:other_account_status) { Fabricate(:status) } let(:home_timeline_key) { FeedManager.instance.key(:home, account.id) } before do Redis.current.del(home_timeline_key) end it "clears account's statuses" do FeedManager.instance.push_to_home(account, status) FeedManager.instance.push_to_home(account, other_account_status) is_expected.to change { Redis.current.zrange(home_timeline_key, 0, -1) }.from([status.id.to_s, other_account_status.id.to_s]).to([other_account_status.id.to_s]) end end end ================================================ FILE: spec/services/app_sign_up_service_spec.rb ================================================ require 'rails_helper' RSpec.describe AppSignUpService, type: :service do let(:app) { Fabricate(:application, scopes: 'read write') } let(:good_params) { { username: 'alice', password: '12345678', email: 'good@email.com', agreement: true } } subject { described_class.new } describe '#call' do it 'returns nil when registrations are closed' do tmp = Setting.registrations_mode Setting.registrations_mode = 'none' expect(subject.call(app, good_params)).to be_nil Setting.registrations_mode = tmp end it 'raises an error when params are missing' do expect { subject.call(app, {}) }.to raise_error ActiveRecord::RecordInvalid end it 'creates an unconfirmed user with access token' do access_token = subject.call(app, good_params) expect(access_token).to_not be_nil user = User.find_by(id: access_token.resource_owner_id) expect(user).to_not be_nil expect(user.confirmed?).to be false end it 'creates access token with the app\'s scopes' do access_token = subject.call(app, good_params) expect(access_token).to_not be_nil expect(access_token.scopes.to_s).to eq 'read write' end it 'creates an account' do access_token = subject.call(app, good_params) expect(access_token).to_not be_nil user = User.find_by(id: access_token.resource_owner_id) expect(user).to_not be_nil expect(user.account).to_not be_nil end end end ================================================ FILE: spec/services/authorize_follow_service_spec.rb ================================================ require 'rails_helper' RSpec.describe AuthorizeFollowService, type: :service do let(:sender) { Fabricate(:account, username: 'alice') } subject { AuthorizeFollowService.new } describe 'local' do let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } before do FollowRequest.create(account: bob, target_account: sender) subject.call(bob, sender) end it 'removes follow request' do expect(bob.requested?(sender)).to be false end it 'creates follow relation' do expect(bob.following?(sender)).to be true end end describe 'remote OStatus' do let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account } before do FollowRequest.create(account: bob, target_account: sender) stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {}) subject.call(bob, sender) end it 'removes follow request' do expect(bob.requested?(sender)).to be false end it 'creates follow relation' do expect(bob.following?(sender)).to be true end it 'sends a follow request authorization salmon slap' do expect(a_request(:post, "http://salmon.example.com/").with { |req| xml = OStatus2::Salmon.new.unpack(req.body) xml.match(OStatus::TagManager::VERBS[:authorize]) }).to have_been_made.once end end describe 'remote ActivityPub' do let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox')).account } before do FollowRequest.create(account: bob, target_account: sender) stub_request(:post, bob.inbox_url).to_return(status: 200) subject.call(bob, sender) end it 'removes follow request' do expect(bob.requested?(sender)).to be false end it 'creates follow relation' do expect(bob.following?(sender)).to be true end it 'sends an accept activity' do expect(a_request(:post, bob.inbox_url)).to have_been_made.once end end end ================================================ FILE: spec/services/batched_remove_status_service_spec.rb ================================================ require 'rails_helper' RSpec.describe BatchedRemoveStatusService, type: :service do subject { BatchedRemoveStatusService.new } let!(:alice) { Fabricate(:account) } let!(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://example.com/salmon') } let!(:jeff) { Fabricate(:user).account } let!(:hank) { Fabricate(:account, username: 'hank', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') } let(:status1) { PostStatusService.new.call(alice, text: 'Hello @bob@example.com') } let(:status2) { PostStatusService.new.call(alice, text: 'Another status') } before do allow(Redis.current).to receive_messages(publish: nil) stub_request(:post, 'http://example.com/push').to_return(status: 200, body: '', headers: {}) stub_request(:post, 'http://example.com/salmon').to_return(status: 200, body: '', headers: {}) stub_request(:post, 'http://example.com/inbox').to_return(status: 200) Fabricate(:subscription, account: alice, callback_url: 'http://example.com/push', confirmed: true, expires_at: 30.days.from_now) jeff.user.update(current_sign_in_at: Time.zone.now) jeff.follow!(alice) hank.follow!(alice) status1 status2 subject.call([status1, status2]) end it 'removes statuses from author\'s home feed' do expect(HomeFeed.new(alice).get(10)).to_not include([status1.id, status2.id]) end it 'removes statuses from local follower\'s home feed' do expect(HomeFeed.new(jeff).get(10)).to_not include([status1.id, status2.id]) end it 'notifies streaming API of followers' do expect(Redis.current).to have_received(:publish).with("timeline:#{jeff.id}", any_args).at_least(:once) end it 'notifies streaming API of author' do expect(Redis.current).to have_received(:publish).with("timeline:#{alice.id}", any_args).at_least(:once) end it 'notifies streaming API of public timeline' do expect(Redis.current).to have_received(:publish).with('timeline:public', any_args).at_least(:once) end it 'sends PuSH update to PuSH subscribers' do expect(a_request(:post, 'http://example.com/push').with { |req| matches = req.body.match(OStatus::TagManager::VERBS[:delete]) }).to have_been_made.at_least_once end it 'sends Salmon slap to previously mentioned users' do expect(a_request(:post, "http://example.com/salmon").with { |req| xml = OStatus2::Salmon.new.unpack(req.body) xml.match(OStatus::TagManager::VERBS[:delete]) }).to have_been_made.once end it 'sends delete activity to followers' do expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.at_least_once end end ================================================ FILE: spec/services/block_domain_service_spec.rb ================================================ require 'rails_helper' RSpec.describe BlockDomainService, type: :service do let!(:bad_account) { Fabricate(:account, username: 'badguy666', domain: 'evil.org') } let!(:bad_status1) { Fabricate(:status, account: bad_account, text: 'You suck') } let!(:bad_status2) { Fabricate(:status, account: bad_account, text: 'Hahaha') } let!(:bad_attachment) { Fabricate(:media_attachment, account: bad_account, status: bad_status2, file: attachment_fixture('attachment.jpg')) } let!(:already_banned_account) { Fabricate(:account, username: 'badguy', domain: 'evil.org', suspended: true, silenced: true) } subject { BlockDomainService.new } describe 'for a suspension' do before do subject.call(DomainBlock.create!(domain: 'evil.org', severity: :suspend)) end it 'creates a domain block' do expect(DomainBlock.blocked?('evil.org')).to be true end it 'removes remote accounts from that domain' do expect(Account.find_remote('badguy666', 'evil.org').suspended?).to be true end it 'records suspension date appropriately' do expect(Account.find_remote('badguy666', 'evil.org').suspended_at).to eq DomainBlock.find_by(domain: 'evil.org').created_at end it 'keeps already-banned accounts banned' do expect(Account.find_remote('badguy', 'evil.org').suspended?).to be true end it 'does not overwrite suspension date of already-banned accounts' do expect(Account.find_remote('badguy', 'evil.org').suspended_at).to_not eq DomainBlock.find_by(domain: 'evil.org').created_at end it 'removes the remote accounts\'s statuses and media attachments' do expect { bad_status1.reload }.to raise_exception ActiveRecord::RecordNotFound expect { bad_status2.reload }.to raise_exception ActiveRecord::RecordNotFound expect { bad_attachment.reload }.to raise_exception ActiveRecord::RecordNotFound end end describe 'for a silence with reject media' do before do subject.call(DomainBlock.create!(domain: 'evil.org', severity: :silence, reject_media: true)) end it 'does not create a domain block' do expect(DomainBlock.blocked?('evil.org')).to be false end it 'silences remote accounts from that domain' do expect(Account.find_remote('badguy666', 'evil.org').silenced?).to be true end it 'records suspension date appropriately' do expect(Account.find_remote('badguy666', 'evil.org').silenced_at).to eq DomainBlock.find_by(domain: 'evil.org').created_at end it 'keeps already-banned accounts banned' do expect(Account.find_remote('badguy', 'evil.org').silenced?).to be true end it 'does not overwrite suspension date of already-banned accounts' do expect(Account.find_remote('badguy', 'evil.org').silenced_at).to_not eq DomainBlock.find_by(domain: 'evil.org').created_at end it 'leaves the domains status and attachements, but clears media' do expect { bad_status1.reload }.not_to raise_error expect { bad_status2.reload }.not_to raise_error expect { bad_attachment.reload }.not_to raise_error expect(bad_attachment.file.exists?).to be false end end end ================================================ FILE: spec/services/block_service_spec.rb ================================================ require 'rails_helper' RSpec.describe BlockService, type: :service do let(:sender) { Fabricate(:account, username: 'alice') } subject { BlockService.new } describe 'local' do let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } before do subject.call(sender, bob) end it 'creates a blocking relation' do expect(sender.blocking?(bob)).to be true end end describe 'remote OStatus' do let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account } before do stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {}) subject.call(sender, bob) end it 'creates a blocking relation' do expect(sender.blocking?(bob)).to be true end it 'sends a block salmon slap' do expect(a_request(:post, "http://salmon.example.com/").with { |req| xml = OStatus2::Salmon.new.unpack(req.body) xml.match(OStatus::TagManager::VERBS[:block]) }).to have_been_made.once end end describe 'remote ActivityPub' do let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox')).account } before do stub_request(:post, 'http://example.com/inbox').to_return(status: 200) subject.call(sender, bob) end it 'creates a blocking relation' do expect(sender.blocking?(bob)).to be true end it 'sends a block activity' do expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once end end end ================================================ FILE: spec/services/bootstrap_timeline_service_spec.rb ================================================ require 'rails_helper' RSpec.describe BootstrapTimelineService, type: :service do subject { described_class.new } describe '#call' do let(:source_account) { Fabricate(:account) } context 'when setting is empty' do let!(:admin) { Fabricate(:user, admin: true) } before do Setting.bootstrap_timeline_accounts = nil subject.call(source_account) end it 'follows admin accounts from account' do expect(source_account.following?(admin.account)).to be true end end context 'when setting is set' do let!(:alice) { Fabricate(:account, username: 'alice') } let!(:bob) { Fabricate(:account, username: 'bob') } before do Setting.bootstrap_timeline_accounts = 'alice, bob' subject.call(source_account) end it 'follows found accounts from account' do expect(source_account.following?(alice)).to be true expect(source_account.following?(bob)).to be true end end end end ================================================ FILE: spec/services/fan_out_on_write_service_spec.rb ================================================ require 'rails_helper' RSpec.describe FanOutOnWriteService, type: :service do let(:author) { Fabricate(:account, username: 'tom') } let(:status) { Fabricate(:status, text: 'Hello @alice #test', account: author) } let(:alice) { Fabricate(:user, account: Fabricate(:account, username: 'alice')).account } let(:follower) { Fabricate(:account, username: 'bob') } subject { FanOutOnWriteService.new } before do alice follower.follow!(author) ProcessMentionsService.new.call(status) ProcessHashtagsService.new.call(status) subject.call(status) end it 'delivers status to home timeline' do expect(HomeFeed.new(author).get(10).map(&:id)).to include status.id end it 'delivers status to local followers' do pending 'some sort of problem in test environment causes this to sometimes fail' expect(HomeFeed.new(follower).get(10).map(&:id)).to include status.id end it 'delivers status to hashtag' do expect(Tag.find_by!(name: 'test').statuses.pluck(:id)).to include status.id end it 'delivers status to public timeline' do expect(Status.as_public_timeline(alice).map(&:id)).to include status.id end end ================================================ FILE: spec/services/favourite_service_spec.rb ================================================ require 'rails_helper' RSpec.describe FavouriteService, type: :service do let(:sender) { Fabricate(:account, username: 'alice') } subject { FavouriteService.new } describe 'local' do let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } let(:status) { Fabricate(:status, account: bob) } before do subject.call(sender, status) end it 'creates a favourite' do expect(status.favourites.first).to_not be_nil end end describe 'remote OStatus' do let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', protocol: :ostatus, domain: 'example.com', salmon_url: 'http://salmon.example.com')).account } let(:status) { Fabricate(:status, account: bob, uri: 'tag:example.com:blahblah') } before do stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {}) subject.call(sender, status) end it 'creates a favourite' do expect(status.favourites.first).to_not be_nil end it 'sends a salmon slap' do expect(a_request(:post, "http://salmon.example.com/").with { |req| xml = OStatus2::Salmon.new.unpack(req.body) xml.match(OStatus::TagManager::VERBS[:favorite]) }).to have_been_made.once end end describe 'remote ActivityPub' do let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, protocol: :activitypub, username: 'bob', domain: 'example.com', inbox_url: 'http://example.com/inbox')).account } let(:status) { Fabricate(:status, account: bob) } before do stub_request(:post, "http://example.com/inbox").to_return(:status => 200, :body => "", :headers => {}) subject.call(sender, status) end it 'creates a favourite' do expect(status.favourites.first).to_not be_nil end it 'sends a like activity' do expect(a_request(:post, "http://example.com/inbox")).to have_been_made.once end end end ================================================ FILE: spec/services/fetch_atom_service_spec.rb ================================================ require 'rails_helper' RSpec.describe FetchAtomService, type: :service do describe '#call' do let(:url) { 'http://example.com' } subject { FetchAtomService.new.call(url) } context 'url is blank' do let(:url) { '' } it { is_expected.to be_nil } end context 'request failed' do before do WebMock.stub_request(:get, url).to_return(status: 500, body: '', headers: {}) end it { is_expected.to be_nil } end context 'raise OpenSSL::SSL::SSLError' do before do allow(Request).to receive_message_chain(:new, :add_headers, :perform).and_raise(OpenSSL::SSL::SSLError) end it 'output log and return nil' do expect_any_instance_of(ActiveSupport::Logger).to receive(:debug).with('SSL error: OpenSSL::SSL::SSLError') is_expected.to be_nil end end context 'raise HTTP::ConnectionError' do before do allow(Request).to receive_message_chain(:new, :add_headers, :perform).and_raise(HTTP::ConnectionError) end it 'output log and return nil' do expect_any_instance_of(ActiveSupport::Logger).to receive(:debug).with('HTTP ConnectionError: HTTP::ConnectionError') is_expected.to be_nil end end context 'response success' do let(:body) { '' } let(:headers) { { 'Content-Type' => content_type } } let(:json) { { id: 1, '@context': ActivityPub::TagManager::CONTEXT, type: 'Note', }.to_json } before do WebMock.stub_request(:get, url).to_return(status: 200, body: body, headers: headers) end context 'content type is application/atom+xml' do let(:content_type) { 'application/atom+xml' } it { is_expected.to eq [url, { :prefetched_body => "" }, :ostatus] } end context 'content_type is activity+json' do let(:content_type) { 'application/activity+json; charset=utf-8' } let(:body) { json } it { is_expected.to eq [1, { prefetched_body: body, id: true }, :activitypub] } end context 'content_type is ld+json with profile' do let(:content_type) { 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' } let(:body) { json } it { is_expected.to eq [1, { prefetched_body: body, id: true }, :activitypub] } end before do WebMock.stub_request(:get, url).to_return(status: 200, body: body, headers: headers) WebMock.stub_request(:get, 'http://example.com/foo').to_return(status: 200, body: json, headers: { 'Content-Type' => 'application/activity+json' }) end context 'has link header' do let(:headers) { { 'Link' => '; rel="alternate"; type="application/activity+json"', } } it { is_expected.to eq [1, { prefetched_body: json, id: true }, :activitypub] } end context 'content type is text/html' do let(:content_type) { 'text/html' } let(:body) { '' } it { is_expected.to eq [1, { prefetched_body: json, id: true }, :activitypub] } end end end end ================================================ FILE: spec/services/fetch_link_card_service_spec.rb ================================================ require 'rails_helper' RSpec.describe FetchLinkCardService, type: :service do subject { FetchLinkCardService.new } before do stub_request(:head, 'http://example.xn--fiqs8s/').to_return(status: 200, headers: { 'Content-Type' => 'text/html' }) stub_request(:get, 'http://example.xn--fiqs8s/').to_return(request_fixture('idn.txt')) stub_request(:head, 'http://example.com/sjis').to_return(status: 200, headers: { 'Content-Type' => 'text/html' }) stub_request(:get, 'http://example.com/sjis').to_return(request_fixture('sjis.txt')) stub_request(:head, 'http://example.com/sjis_with_wrong_charset').to_return(status: 200, headers: { 'Content-Type' => 'text/html' }) stub_request(:get, 'http://example.com/sjis_with_wrong_charset').to_return(request_fixture('sjis_with_wrong_charset.txt')) stub_request(:head, 'http://example.com/koi8-r').to_return(status: 200, headers: { 'Content-Type' => 'text/html' }) stub_request(:get, 'http://example.com/koi8-r').to_return(request_fixture('koi8-r.txt')) stub_request(:head, 'http://example.com/日本語').to_return(status: 200, headers: { 'Content-Type' => 'text/html' }) stub_request(:get, 'http://example.com/日本語').to_return(request_fixture('sjis.txt')) stub_request(:head, 'https://github.com/qbi/WannaCry').to_return(status: 404) stub_request(:head, 'http://example.com/test-').to_return(status: 200, headers: { 'Content-Type' => 'text/html' }) stub_request(:get, 'http://example.com/test-').to_return(request_fixture('idn.txt')) stub_request(:head, 'http://example.com/windows-1251').to_return(status: 200, headers: { 'Content-Type' => 'text/html' }) stub_request(:get, 'http://example.com/windows-1251').to_return(request_fixture('windows-1251.txt')) subject.call(status) end context 'in a local status' do context do let(:status) { Fabricate(:status, text: 'Check out http://example.中国') } it 'works with IDN URLs' do expect(a_request(:get, 'http://example.xn--fiqs8s/')).to have_been_made.at_least_once end end context do let(:status) { Fabricate(:status, text: 'Check out http://example.com/sjis') } it 'works with SJIS' do expect(a_request(:get, 'http://example.com/sjis')).to have_been_made.at_least_once expect(status.preview_cards.first.title).to eq("SJISのページ") end end context do let(:status) { Fabricate(:status, text: 'Check out http://example.com/sjis_with_wrong_charset') } it 'works with SJIS even with wrong charset header' do expect(a_request(:get, 'http://example.com/sjis_with_wrong_charset')).to have_been_made.at_least_once expect(status.preview_cards.first.title).to eq("SJISのページ") end end context do let(:status) { Fabricate(:status, text: 'Check out http://example.com/koi8-r') } it 'works with koi8-r' do expect(a_request(:get, 'http://example.com/koi8-r')).to have_been_made.at_least_once expect(status.preview_cards.first.title).to eq("Московя начинаетъ только въ XVI ст. привлекать внимане иностранцевъ.") end end context do let(:status) { Fabricate(:status, text: 'Check out http://example.com/windows-1251') } it 'works with windows-1251' do expect(a_request(:get, 'http://example.com/windows-1251')).to have_been_made.at_least_once expect(status.preview_cards.first.title).to eq('сэмпл текст') end end context do let(:status) { Fabricate(:status, text: 'テストhttp://example.com/日本語') } it 'works with Japanese path string' do expect(a_request(:get, 'http://example.com/日本語')).to have_been_made.at_least_once expect(status.preview_cards.first.title).to eq("SJISのページ") end end context do let(:status) { Fabricate(:status, text: 'test http://example.com/test-') } it 'works with a URL ending with a hyphen' do expect(a_request(:get, 'http://example.com/test-')).to have_been_made.at_least_once end end end context 'in a remote status' do let(:status) { Fabricate(:status, account: Fabricate(:account, domain: 'example.com'), text: 'Habt ihr ein paar gute Links zu #Wannacry herumfliegen? Ich will mal unter
https://github.com/qbi/WannaCry was sammeln. !security ') } it 'parses out URLs' do expect(a_request(:head, 'https://github.com/qbi/WannaCry')).to have_been_made.at_least_once end it 'ignores URLs to hashtags' do expect(a_request(:head, 'https://quitter.se/tag/wannacry')).to_not have_been_made end end end ================================================ FILE: spec/services/fetch_oembed_service_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe FetchOEmbedService, type: :service do subject { described_class.new } before do stub_request(:get, "https://host.test/provider.json").to_return(status: 404) stub_request(:get, "https://host.test/provider.xml").to_return(status: 404) stub_request(:get, "https://host.test/empty_provider.json").to_return(status: 200) end describe 'discover_provider' do context 'when status code is 200 and MIME type is text/html' do context 'Both of JSON and XML provider are discoverable' do before do stub_request(:get, 'https://host.test/oembed.html').to_return( status: 200, headers: { 'Content-Type': 'text/html' }, body: request_fixture('oembed_json_xml.html') ) end it 'returns new OEmbed::Provider for JSON provider if :format option is set to :json' do subject.call('https://host.test/oembed.html', format: :json) expect(subject.endpoint_url).to eq 'https://host.test/provider.json' expect(subject.format).to eq :json end it 'returns new OEmbed::Provider for XML provider if :format option is set to :xml' do subject.call('https://host.test/oembed.html', format: :xml) expect(subject.endpoint_url).to eq 'https://host.test/provider.xml' expect(subject.format).to eq :xml end end context 'JSON provider is discoverable while XML provider is not' do before do stub_request(:get, 'https://host.test/oembed.html').to_return( status: 200, headers: { 'Content-Type': 'text/html' }, body: request_fixture('oembed_json.html') ) end it 'returns new OEmbed::Provider for JSON provider' do subject.call('https://host.test/oembed.html') expect(subject.endpoint_url).to eq 'https://host.test/provider.json' expect(subject.format).to eq :json end end context 'XML provider is discoverable while JSON provider is not' do before do stub_request(:get, 'https://host.test/oembed.html').to_return( status: 200, headers: { 'Content-Type': 'text/html' }, body: request_fixture('oembed_xml.html') ) end it 'returns new OEmbed::Provider for XML provider' do subject.call('https://host.test/oembed.html') expect(subject.endpoint_url).to eq 'https://host.test/provider.xml' expect(subject.format).to eq :xml end end context 'Invalid XML provider is discoverable while JSON provider is not' do before do stub_request(:get, 'https://host.test/oembed.html').to_return( status: 200, headers: { 'Content-Type': 'text/html' }, body: request_fixture('oembed_invalid_xml.html') ) end it 'returns nil' do expect(subject.call('https://host.test/oembed.html')).to be_nil end end context 'Neither of JSON and XML provider is discoverable' do before do stub_request(:get, 'https://host.test/oembed.html').to_return( status: 200, headers: { 'Content-Type': 'text/html' }, body: request_fixture('oembed_undiscoverable.html') ) end it 'returns nil' do expect(subject.call('https://host.test/oembed.html')).to be_nil end end context 'Empty JSON provider is discoverable' do before do stub_request(:get, 'https://host.test/oembed.html').to_return( status: 200, headers: { 'Content-Type': 'text/html' }, body: request_fixture('oembed_json_empty.html') ) end it 'returns new OEmbed::Provider for JSON provider' do subject.call('https://host.test/oembed.html') expect(subject.endpoint_url).to eq 'https://host.test/empty_provider.json' expect(subject.format).to eq :json end end end context 'when status code is not 200' do before do stub_request(:get, 'https://host.test/oembed.html').to_return( status: 400, headers: { 'Content-Type': 'text/html' }, body: request_fixture('oembed_xml.html') ) end it 'returns nil' do expect(subject.call('https://host.test/oembed.html')).to be_nil end end context 'when MIME type is not text/html' do before do stub_request(:get, 'https://host.test/oembed.html').to_return( status: 200, body: request_fixture('oembed_xml.html') ) end it 'returns nil' do expect(subject.call('https://host.test/oembed.html')).to be_nil end end end end ================================================ FILE: spec/services/fetch_remote_account_service_spec.rb ================================================ require 'rails_helper' RSpec.describe FetchRemoteAccountService, type: :service do let(:url) { 'https://example.com/alice' } let(:prefetched_body) { nil } let(:protocol) { :ostatus } subject { FetchRemoteAccountService.new.call(url, prefetched_body, protocol) } let(:actor) do { '@context': 'https://www.w3.org/ns/activitystreams', id: 'https://example.com/alice', type: 'Person', preferredUsername: 'alice', name: 'Alice', summary: 'Foo bar', inbox: 'http://example.com/alice/inbox', } end let(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } } let(:xml) { File.read(Rails.root.join('spec', 'fixtures', 'xml', 'mastodon.atom')) } shared_examples 'return Account' do it { is_expected.to be_an Account } end context 'protocol is :activitypub' do let(:prefetched_body) { Oj.dump(actor) } let(:protocol) { :activitypub } before do stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) end include_examples 'return Account' end context 'protocol is :ostatus' do let(:prefetched_body) { xml } let(:protocol) { :ostatus } before do stub_request(:get, "https://kickass.zone/.well-known/webfinger?resource=acct:localhost@kickass.zone").to_return(request_fixture('webfinger-hacker3.txt')) stub_request(:get, "https://kickass.zone/api/statuses/user_timeline/7477.atom").to_return(request_fixture('feed.txt')) end include_examples 'return Account' it 'does not update account information if XML comes from an unverified domain' do feed_xml = <<-XML.squish http://activitystrea.ms/schema/1.0/person http://kickass.zone/users/localhost localhost localhost Villain!!! XML returned_account = described_class.new.call('https://real-fake-domains.com/alice', feed_xml, :ostatus) expect(returned_account.display_name).to_not eq 'Villain!!!' end end context 'when prefetched_body is nil' do context 'protocol is :activitypub' do before do stub_request(:get, url).to_return(status: 200, body: Oj.dump(actor), headers: { 'Content-Type' => 'application/activity+json' }) stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) end include_examples 'return Account' end context 'protocol is :ostatus' do before do stub_request(:get, url).to_return(status: 200, body: xml, headers: { 'Content-Type' => 'application/atom+xml' }) stub_request(:get, "https://kickass.zone/.well-known/webfinger?resource=acct:localhost@kickass.zone").to_return(request_fixture('webfinger-hacker3.txt')) stub_request(:get, "https://kickass.zone/api/statuses/user_timeline/7477.atom").to_return(request_fixture('feed.txt')) end include_examples 'return Account' end end end ================================================ FILE: spec/services/fetch_remote_status_service_spec.rb ================================================ require 'rails_helper' RSpec.describe FetchRemoteStatusService, type: :service do let(:account) { Fabricate(:account) } let(:prefetched_body) { nil } let(:valid_domain) { Rails.configuration.x.local_domain } let(:note) do { '@context': 'https://www.w3.org/ns/activitystreams', id: "https://#{valid_domain}/@foo/1234", type: 'Note', content: 'Lorem ipsum', attributedTo: ActivityPub::TagManager.instance.uri_for(account), } end context 'protocol is :activitypub' do subject { described_class.new.call(note[:id], prefetched_body, protocol) } let(:prefetched_body) { Oj.dump(note) } let(:protocol) { :activitypub } before do account.update(uri: ActivityPub::TagManager.instance.uri_for(account)) subject end it 'creates status' do status = account.statuses.first expect(status).to_not be_nil expect(status.text).to eq 'Lorem ipsum' end end context 'protocol is :ostatus' do subject { described_class.new } before do Fabricate(:account, username: 'tracer', domain: 'real.domain', remote_url: 'https://real.domain/users/tracer') end it 'does not create status with author at different domain' do status_body = <<-XML.squish tag:real.domain,2017-04-27:objectId=4487555:objectType=Status 2017-04-27T13:49:25Z 2017-04-27T13:49:25Z http://activitystrea.ms/schema/1.0/note http://activitystrea.ms/schema/1.0/post https://real.domain/users/tracer http://activitystrea.ms/schema/1.0/person https://real.domain/users/tracer tracer Overwatch rocks XML expect(subject.call('https://fake.domain/foo', status_body, :ostatus)).to be_nil end it 'does not create status with wrong id when id uses http format' do status_body = <<-XML.squish https://other-real.domain/statuses/123 2017-04-27T13:49:25Z 2017-04-27T13:49:25Z http://activitystrea.ms/schema/1.0/note http://activitystrea.ms/schema/1.0/post https://real.domain/users/tracer http://activitystrea.ms/schema/1.0/person https://real.domain/users/tracer tracer Overwatch rocks XML expect(subject.call('https://real.domain/statuses/456', status_body, :ostatus)).to be_nil end end end ================================================ FILE: spec/services/follow_service_spec.rb ================================================ require 'rails_helper' RSpec.describe FollowService, type: :service do let(:sender) { Fabricate(:account, username: 'alice') } subject { FollowService.new } context 'local account' do describe 'locked account' do let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, locked: true, username: 'bob')).account } before do subject.call(sender, bob.acct) end it 'creates a follow request with reblogs' do expect(FollowRequest.find_by(account: sender, target_account: bob, show_reblogs: true)).to_not be_nil end end describe 'locked account, no reblogs' do let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, locked: true, username: 'bob')).account } before do subject.call(sender, bob.acct, reblogs: false) end it 'creates a follow request without reblogs' do expect(FollowRequest.find_by(account: sender, target_account: bob, show_reblogs: false)).to_not be_nil end end describe 'unlocked account' do let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } before do subject.call(sender, bob.acct) end it 'creates a following relation with reblogs' do expect(sender.following?(bob)).to be true expect(sender.muting_reblogs?(bob)).to be false end end describe 'unlocked account, no reblogs' do let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } before do subject.call(sender, bob.acct, reblogs: false) end it 'creates a following relation without reblogs' do expect(sender.following?(bob)).to be true expect(sender.muting_reblogs?(bob)).to be true end end describe 'already followed account' do let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } before do sender.follow!(bob) subject.call(sender, bob.acct) end it 'keeps a following relation' do expect(sender.following?(bob)).to be true end end describe 'already followed account, turning reblogs off' do let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } before do sender.follow!(bob, reblogs: true) subject.call(sender, bob.acct, reblogs: false) end it 'disables reblogs' do expect(sender.muting_reblogs?(bob)).to be true end end describe 'already followed account, turning reblogs on' do let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } before do sender.follow!(bob, reblogs: false) subject.call(sender, bob.acct, reblogs: true) end it 'disables reblogs' do expect(sender.muting_reblogs?(bob)).to be false end end end context 'remote OStatus account' do describe 'locked account' do let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, protocol: :ostatus, locked: true, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account } before do stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {}) subject.call(sender, bob.acct) end it 'creates a follow request' do expect(FollowRequest.find_by(account: sender, target_account: bob)).to_not be_nil end it 'sends a follow request salmon slap' do expect(a_request(:post, "http://salmon.example.com/").with { |req| xml = OStatus2::Salmon.new.unpack(req.body) xml.match(OStatus::TagManager::VERBS[:request_friend]) }).to have_been_made.once end end describe 'unlocked account' do let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, protocol: :ostatus, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com', hub_url: 'http://hub.example.com')).account } before do stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {}) stub_request(:post, "http://hub.example.com/").to_return(status: 202) subject.call(sender, bob.acct) end it 'creates a following relation' do expect(sender.following?(bob)).to be true end it 'sends a follow salmon slap' do expect(a_request(:post, "http://salmon.example.com/").with { |req| xml = OStatus2::Salmon.new.unpack(req.body) xml.match(OStatus::TagManager::VERBS[:follow]) }).to have_been_made.once end it 'subscribes to PuSH' do expect(a_request(:post, "http://hub.example.com/")).to have_been_made.once end end describe 'already followed account' do let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, protocol: :ostatus, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com', hub_url: 'http://hub.example.com')).account } before do sender.follow!(bob) subject.call(sender, bob.acct) end it 'keeps a following relation' do expect(sender.following?(bob)).to be true end it 'does not send a follow salmon slap' do expect(a_request(:post, "http://salmon.example.com/")).not_to have_been_made end it 'does not subscribe to PuSH' do expect(a_request(:post, "http://hub.example.com/")).not_to have_been_made end end end context 'remote ActivityPub account' do let(:bob) { Fabricate(:user, account: Fabricate(:account, username: 'bob', domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox')).account } before do stub_request(:post, "http://example.com/inbox").to_return(:status => 200, :body => "", :headers => {}) subject.call(sender, bob.acct) end it 'creates follow request' do expect(FollowRequest.find_by(account: sender, target_account: bob)).to_not be_nil end it 'sends a follow activity to the inbox' do expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once end end end ================================================ FILE: spec/services/hashtag_query_service_spec.rb ================================================ require 'rails_helper' describe HashtagQueryService, type: :service do describe '.call' do let(:account) { Fabricate(:account) } let(:tag1) { Fabricate(:tag) } let(:tag2) { Fabricate(:tag) } let!(:status1) { Fabricate(:status, tags: [tag1]) } let!(:status2) { Fabricate(:status, tags: [tag2]) } let!(:both) { Fabricate(:status, tags: [tag1, tag2]) } it 'can add tags in "any" mode' do results = subject.call(tag1, { any: [tag2.name] }) expect(results).to include status1 expect(results).to include status2 expect(results).to include both end it 'can remove tags in "all" mode' do results = subject.call(tag1, { all: [tag2.name] }) expect(results).to_not include status1 expect(results).to_not include status2 expect(results).to include both end it 'can remove tags in "none" mode' do results = subject.call(tag1, { none: [tag2.name] }) expect(results).to include status1 expect(results).to_not include status2 expect(results).to_not include both end it 'ignores an invalid mode' do results = subject.call(tag1, { wark: [tag2.name] }) expect(results).to include status1 expect(results).to_not include status2 expect(results).to include both end it 'handles being passed non existant tag names' do results = subject.call(tag1, { any: ['wark'] }) expect(results).to include status1 expect(results).to_not include status2 expect(results).to include both end it 'can restrict to an account' do BlockService.new.call(account, status1.account) results = subject.call(tag1, { none: [tag2.name] }, account) expect(results).to_not include status1 end it 'can restrict to local' do status1.account.update(domain: 'example.com') status1.update(local: false, uri: 'example.com/toot') results = subject.call(tag1, { any: [tag2.name] }, nil, true) expect(results).to_not include status1 end end end ================================================ FILE: spec/services/import_service_spec.rb ================================================ require 'rails_helper' RSpec.describe ImportService, type: :service do let!(:account) { Fabricate(:account, locked: false) } let!(:bob) { Fabricate(:account, username: 'bob', locked: false) } let!(:eve) { Fabricate(:account, username: 'eve', domain: 'example.com', locked: false) } context 'import old-style list of muted users' do subject { ImportService.new } let(:csv) { attachment_fixture('mute-imports.txt') } describe 'when no accounts are muted' do let(:import) { Import.create(account: account, type: 'muting', data: csv) } it 'mutes the listed accounts, including notifications' do subject.call(import) expect(account.muting.count).to eq 2 expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true end end describe 'when some accounts are muted and overwrite is not set' do let(:import) { Import.create(account: account, type: 'muting', data: csv) } it 'mutes the listed accounts, including notifications' do account.mute!(bob, notifications: false) subject.call(import) expect(account.muting.count).to eq 2 expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true end end describe 'when some accounts are muted and overwrite is set' do let(:import) { Import.create(account: account, type: 'muting', data: csv, overwrite: true) } it 'mutes the listed accounts, including notifications' do account.mute!(bob, notifications: false) subject.call(import) expect(account.muting.count).to eq 2 expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true end end end context 'import new-style list of muted users' do subject { ImportService.new } let(:csv) { attachment_fixture('new-mute-imports.txt') } describe 'when no accounts are muted' do let(:import) { Import.create(account: account, type: 'muting', data: csv) } it 'mutes the listed accounts, respecting notifications' do subject.call(import) expect(account.muting.count).to eq 2 expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true expect(Mute.find_by(account: account, target_account: eve).hide_notifications).to be false end end describe 'when some accounts are muted and overwrite is not set' do let(:import) { Import.create(account: account, type: 'muting', data: csv) } it 'mutes the listed accounts, respecting notifications' do account.mute!(bob, notifications: true) subject.call(import) expect(account.muting.count).to eq 2 expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true expect(Mute.find_by(account: account, target_account: eve).hide_notifications).to be false end end describe 'when some accounts are muted and overwrite is set' do let(:import) { Import.create(account: account, type: 'muting', data: csv, overwrite: true) } it 'mutes the listed accounts, respecting notifications' do account.mute!(bob, notifications: true) subject.call(import) expect(account.muting.count).to eq 2 expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true expect(Mute.find_by(account: account, target_account: eve).hide_notifications).to be false end end end context 'import old-style list of followed users' do subject { ImportService.new } let(:csv) { attachment_fixture('mute-imports.txt') } before do allow(NotificationWorker).to receive(:perform_async) end describe 'when no accounts are followed' do let(:import) { Import.create(account: account, type: 'following', data: csv) } it 'follows the listed accounts, including boosts' do subject.call(import) expect(account.following.count).to eq 2 expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true end end describe 'when some accounts are already followed and overwrite is not set' do let(:import) { Import.create(account: account, type: 'following', data: csv) } it 'follows the listed accounts, including notifications' do account.follow!(bob, reblogs: false) subject.call(import) expect(account.following.count).to eq 2 expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true end end describe 'when some accounts are already followed and overwrite is set' do let(:import) { Import.create(account: account, type: 'following', data: csv, overwrite: true) } it 'mutes the listed accounts, including notifications' do account.follow!(bob, reblogs: false) subject.call(import) expect(account.following.count).to eq 2 expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true end end end context 'import new-style list of followed users' do subject { ImportService.new } let(:csv) { attachment_fixture('new-following-imports.txt') } before do allow(NotificationWorker).to receive(:perform_async) end describe 'when no accounts are followed' do let(:import) { Import.create(account: account, type: 'following', data: csv) } it 'follows the listed accounts, respecting boosts' do subject.call(import) expect(account.following.count).to eq 2 expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true expect(Follow.find_by(account: account, target_account: eve).show_reblogs).to be false end end describe 'when some accounts are already followed and overwrite is not set' do let(:import) { Import.create(account: account, type: 'following', data: csv) } it 'mutes the listed accounts, respecting notifications' do account.follow!(bob, reblogs: true) subject.call(import) expect(account.following.count).to eq 2 expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true expect(Follow.find_by(account: account, target_account: eve).show_reblogs).to be false end end describe 'when some accounts are already followed and overwrite is set' do let(:import) { Import.create(account: account, type: 'following', data: csv, overwrite: true) } it 'mutes the listed accounts, respecting notifications' do account.follow!(bob, reblogs: true) subject.call(import) expect(account.following.count).to eq 2 expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true expect(Follow.find_by(account: account, target_account: eve).show_reblogs).to be false end end end end ================================================ FILE: spec/services/mute_service_spec.rb ================================================ require 'rails_helper' RSpec.describe MuteService, type: :service do subject do -> { described_class.new.call(account, target_account) } end let(:account) { Fabricate(:account) } let(:target_account) { Fabricate(:account) } describe 'home timeline' do let(:status) { Fabricate(:status, account: target_account) } let(:other_account_status) { Fabricate(:status) } let(:home_timeline_key) { FeedManager.instance.key(:home, account.id) } before do Redis.current.del(home_timeline_key) end it "clears account's statuses" do FeedManager.instance.push_to_home(account, status) FeedManager.instance.push_to_home(account, other_account_status) is_expected.to change { Redis.current.zrange(home_timeline_key, 0, -1) }.from([status.id.to_s, other_account_status.id.to_s]).to([other_account_status.id.to_s]) end end it 'mutes account' do is_expected.to change { account.muting?(target_account) }.from(false).to(true) end context 'without specifying a notifications parameter' do it 'mutes notifications from the account' do is_expected.to change { account.muting_notifications?(target_account) }.from(false).to(true) end end context 'with a true notifications parameter' do subject do -> { described_class.new.call(account, target_account, notifications: true) } end it 'mutes notifications from the account' do is_expected.to change { account.muting_notifications?(target_account) }.from(false).to(true) end end context 'with a false notifications parameter' do subject do -> { described_class.new.call(account, target_account, notifications: false) } end it 'does not mute notifications from the account' do is_expected.to_not change { account.muting_notifications?(target_account) }.from(false) end end end ================================================ FILE: spec/services/notify_service_spec.rb ================================================ require 'rails_helper' RSpec.describe NotifyService, type: :service do subject do -> { described_class.new.call(recipient, activity) } end let(:user) { Fabricate(:user) } let(:recipient) { user.account } let(:sender) { Fabricate(:account, domain: 'example.com') } let(:activity) { Fabricate(:follow, account: sender, target_account: recipient) } it { is_expected.to change(Notification, :count).by(1) } it 'does not notify when sender is blocked' do recipient.block!(sender) is_expected.to_not change(Notification, :count) end it 'does not notify when sender is muted with hide_notifications' do recipient.mute!(sender, notifications: true) is_expected.to_not change(Notification, :count) end it 'does notify when sender is muted without hide_notifications' do recipient.mute!(sender, notifications: false) is_expected.to change(Notification, :count) end it 'does not notify when sender\'s domain is blocked' do recipient.block_domain!(sender.domain) is_expected.to_not change(Notification, :count) end it 'does still notify when sender\'s domain is blocked but sender is followed' do recipient.block_domain!(sender.domain) recipient.follow!(sender) is_expected.to change(Notification, :count) end it 'does not notify when sender is silenced and not followed' do sender.silence! is_expected.to_not change(Notification, :count) end it 'does not notify when recipient is suspended' do recipient.suspend! is_expected.to_not change(Notification, :count) end context 'for direct messages' do let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct)) } before do user.settings.interactions = user.settings.interactions.merge('must_be_following_dm' => enabled) end context 'if recipient is supposed to be following sender' do let(:enabled) { true } it 'does not notify' do is_expected.to_not change(Notification, :count) end context 'if the message chain initiated by recipient, but is not direct message' do let(:reply_to) { Fabricate(:status, account: recipient) } let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct, thread: reply_to)) } it 'does not notify' do is_expected.to_not change(Notification, :count) end end context 'if the message chain initiated by recipient and is direct message' do let(:reply_to) { Fabricate(:status, account: recipient, visibility: :direct) } let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct, thread: reply_to)) } it 'does notify' do is_expected.to change(Notification, :count) end end end context 'if recipient is NOT supposed to be following sender' do let(:enabled) { false } it 'does notify' do is_expected.to change(Notification, :count) end end end describe 'reblogs' do let(:status) { Fabricate(:status, account: Fabricate(:account)) } let(:activity) { Fabricate(:status, account: sender, reblog: status) } it 'shows reblogs by default' do recipient.follow!(sender) is_expected.to change(Notification, :count) end it 'shows reblogs when explicitly enabled' do recipient.follow!(sender, reblogs: true) is_expected.to change(Notification, :count) end it 'shows reblogs when disabled' do recipient.follow!(sender, reblogs: false) is_expected.to change(Notification, :count) end end context do let(:asshole) { Fabricate(:account, username: 'asshole') } let(:reply_to) { Fabricate(:status, account: asshole) } let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, thread: reply_to)) } it 'does not notify when conversation is muted' do recipient.mute_conversation!(activity.status.conversation) is_expected.to_not change(Notification, :count) end it 'does not notify when it is a reply to a blocked user' do recipient.block!(asshole) is_expected.to_not change(Notification, :count) end end context do let(:sender) { recipient } it 'does not notify when recipient is the sender' do is_expected.to_not change(Notification, :count) end end describe 'email' do before do ActionMailer::Base.deliveries.clear notification_emails = user.settings.notification_emails user.settings.notification_emails = notification_emails.merge('follow' => enabled) end context 'when email notification is enabled' do let(:enabled) { true } it 'sends email' do is_expected.to change(ActionMailer::Base.deliveries, :count).by(1) end end context 'when email notification is disabled' do let(:enabled) { false } it "doesn't send email" do is_expected.to_not change(ActionMailer::Base.deliveries, :count).from(0) end end end end ================================================ FILE: spec/services/post_status_service_spec.rb ================================================ require 'rails_helper' RSpec.describe PostStatusService, type: :service do subject { PostStatusService.new } it 'creates a new status' do account = Fabricate(:account) text = "test status update" status = subject.call(account, text: text) expect(status).to be_persisted expect(status.text).to eq text end it 'creates a new response status' do in_reply_to_status = Fabricate(:status) account = Fabricate(:account) text = "test status update" status = subject.call(account, text: text, thread: in_reply_to_status) expect(status).to be_persisted expect(status.text).to eq text expect(status.thread).to eq in_reply_to_status end it 'schedules a status' do account = Fabricate(:account) future = Time.now.utc + 2.hours status = subject.call(account, text: 'Hi future!', scheduled_at: future) expect(status).to be_a ScheduledStatus expect(status.scheduled_at).to eq future expect(status.params['text']).to eq 'Hi future!' end it 'does not immediately create a status when scheduling a status' do account = Fabricate(:account) media = Fabricate(:media_attachment) future = Time.now.utc + 2.hours status = subject.call(account, text: 'Hi future!', media_ids: [media.id], scheduled_at: future) expect(status).to be_a ScheduledStatus expect(status.scheduled_at).to eq future expect(status.params['text']).to eq 'Hi future!' expect(media.reload.status).to be_nil expect(Status.where(text: 'Hi future!').exists?).to be_falsey end it 'creates response to the original status of boost' do boosted_status = Fabricate(:status) in_reply_to_status = Fabricate(:status, reblog: boosted_status) account = Fabricate(:account) text = "test status update" status = subject.call(account, text: text, thread: in_reply_to_status) expect(status).to be_persisted expect(status.text).to eq text expect(status.thread).to eq boosted_status end it 'creates a sensitive status' do status = create_status_with_options(sensitive: true) expect(status).to be_persisted expect(status).to be_sensitive end it 'creates a status with spoiler text' do spoiler_text = "spoiler text" status = create_status_with_options(spoiler_text: spoiler_text) expect(status).to be_persisted expect(status.spoiler_text).to eq spoiler_text end it 'creates a status with empty default spoiler text' do status = create_status_with_options(spoiler_text: nil) expect(status).to be_persisted expect(status.spoiler_text).to eq '' end it 'creates a status with the given visibility' do status = create_status_with_options(visibility: :private) expect(status).to be_persisted expect(status.visibility).to eq "private" end it 'creates a status with limited visibility for silenced users' do status = subject.call(Fabricate(:account, silenced: true), text: 'test', visibility: :public) expect(status).to be_persisted expect(status.visibility).to eq "unlisted" end it 'creates a status for the given application' do application = Fabricate(:application) status = create_status_with_options(application: application) expect(status).to be_persisted expect(status.application).to eq application end it 'creates a status with a language set' do account = Fabricate(:account) text = 'This is an English text.' status = subject.call(account, text: text) expect(status.language).to eq 'en' end it 'processes mentions' do mention_service = double(:process_mentions_service) allow(mention_service).to receive(:call) allow(ProcessMentionsService).to receive(:new).and_return(mention_service) account = Fabricate(:account) status = subject.call(account, text: "test status update") expect(ProcessMentionsService).to have_received(:new) expect(mention_service).to have_received(:call).with(status) end it 'processes hashtags' do hashtags_service = double(:process_hashtags_service) allow(hashtags_service).to receive(:call) allow(ProcessHashtagsService).to receive(:new).and_return(hashtags_service) account = Fabricate(:account) status = subject.call(account, text: "test status update") expect(ProcessHashtagsService).to have_received(:new) expect(hashtags_service).to have_received(:call).with(status) end it 'gets distributed' do allow(DistributionWorker).to receive(:perform_async) allow(Pubsubhubbub::DistributionWorker).to receive(:perform_async) allow(ActivityPub::DistributionWorker).to receive(:perform_async) account = Fabricate(:account) status = subject.call(account, text: "test status update") expect(DistributionWorker).to have_received(:perform_async).with(status.id) expect(Pubsubhubbub::DistributionWorker).to have_received(:perform_async).with(status.stream_entry.id) expect(ActivityPub::DistributionWorker).to have_received(:perform_async).with(status.id) end it 'crawls links' do allow(LinkCrawlWorker).to receive(:perform_async) account = Fabricate(:account) status = subject.call(account, text: "test status update") expect(LinkCrawlWorker).to have_received(:perform_async).with(status.id) end it 'attaches the given media to the created status' do account = Fabricate(:account) media = Fabricate(:media_attachment, account: account) status = subject.call( account, text: "test status update", media_ids: [media.id], ) expect(media.reload.status).to eq status end it 'does not attach media from another account to the created status' do account = Fabricate(:account) media = Fabricate(:media_attachment, account: Fabricate(:account)) status = subject.call( account, text: "test status update", media_ids: [media.id], ) expect(media.reload.status).to eq nil end it 'does not allow attaching more than 4 files' do account = Fabricate(:account) expect do subject.call( account, text: "test status update", media_ids: [ Fabricate(:media_attachment, account: account), Fabricate(:media_attachment, account: account), Fabricate(:media_attachment, account: account), Fabricate(:media_attachment, account: account), Fabricate(:media_attachment, account: account), ].map(&:id), ) end.to raise_error( Mastodon::ValidationError, I18n.t('media_attachments.validations.too_many'), ) end it 'does not allow attaching both videos and images' do account = Fabricate(:account) expect do subject.call( account, text: "test status update", media_ids: [ Fabricate(:media_attachment, type: :video, account: account), Fabricate(:media_attachment, type: :image, account: account), ].map(&:id), ) end.to raise_error( Mastodon::ValidationError, I18n.t('media_attachments.validations.images_and_video'), ) end it 'returns existing status when used twice with idempotency key' do account = Fabricate(:account) status1 = subject.call(account, text: 'test', idempotency: 'meepmeep') status2 = subject.call(account, text: 'test', idempotency: 'meepmeep') expect(status2.id).to eq status1.id end def create_status_with_options(**options) subject.call(Fabricate(:account), options.merge(text: 'test')) end end ================================================ FILE: spec/services/precompute_feed_service_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' RSpec.describe PrecomputeFeedService, type: :service do subject { PrecomputeFeedService.new } describe 'call' do let(:account) { Fabricate(:account) } it 'fills a user timeline with statuses' do account = Fabricate(:account) status = Fabricate(:status, account: account) subject.call(account) expect(Redis.current.zscore(FeedManager.instance.key(:home, account.id), status.id)).to be_within(0.1).of(status.id.to_f) end it 'does not raise an error even if it could not find any status' do account = Fabricate(:account) subject.call(account) end it 'filters statuses' do account = Fabricate(:account) muted_account = Fabricate(:account) Fabricate(:mute, account: account, target_account: muted_account) reblog = Fabricate(:status, account: muted_account) status = Fabricate(:status, account: account, reblog: reblog) subject.call(account) expect(Redis.current.zscore(FeedManager.instance.key(:home, account.id), reblog.id)).to eq nil end end end ================================================ FILE: spec/services/process_feed_service_spec.rb ================================================ require 'rails_helper' RSpec.describe ProcessFeedService, type: :service do subject { ProcessFeedService.new } describe 'processing a feed' do let(:body) { File.read(Rails.root.join('spec', 'fixtures', 'xml', 'mastodon.atom')) } let(:account) { Fabricate(:account, username: 'localhost', domain: 'kickass.zone') } before do stub_request(:post, "https://pubsubhubbub.superfeedr.com/").to_return(:status => 200, :body => "", :headers => {}) stub_request(:head, "http://kickass.zone/media/2").to_return(:status => 404) stub_request(:head, "http://kickass.zone/media/3").to_return(:status => 404) stub_request(:get, "http://kickass.zone/system/accounts/avatars/000/000/001/large/eris.png").to_return(request_fixture('avatar.txt')) stub_request(:get, "http://kickass.zone/system/media_attachments/files/000/000/002/original/morpheus_linux.jpg?1476059910").to_return(request_fixture('attachment1.txt')) stub_request(:get, "http://kickass.zone/system/media_attachments/files/000/000/003/original/gizmo.jpg?1476060065").to_return(request_fixture('attachment2.txt')) end context 'when domain does not reject media' do before do subject.call(body, account) end it 'updates remote user\'s account information' do account.reload expect(account.display_name).to eq '::1' expect(account).to have_attached_file(:avatar) expect(account.avatar_file_name).not_to be_nil end it 'creates posts' do expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=1:objectType=Status')).to_not be_nil expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=2:objectType=Status')).to_not be_nil end it 'marks replies as replies' do status = Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=2:objectType=Status') expect(status.reply?).to be true end it 'sets account being replied to when possible' do status = Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=2:objectType=Status') expect(status.in_reply_to_account_id).to eq status.account_id end it 'ignores delete statuses unless they existed before' do expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=3:objectType=Status')).to be_nil expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=12:objectType=Status')).to be_nil end it 'does not create statuses for follows' do expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=1:objectType=Follow')).to be_nil expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=2:objectType=Follow')).to be_nil expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=4:objectType=Follow')).to be_nil expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=7:objectType=Follow')).to be_nil end it 'does not create statuses for favourites' do expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=2:objectType=Favourite')).to be_nil expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=3:objectType=Favourite')).to be_nil end it 'creates posts with media' do status = Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=14:objectType=Status') expect(status).to_not be_nil expect(status.media_attachments.first).to have_attached_file(:file) expect(status.media_attachments.first.image?).to be true expect(status.media_attachments.first.file_file_name).not_to be_nil end end context 'when domain is set to reject media' do let!(:domain_block) { Fabricate(:domain_block, domain: 'kickass.zone', reject_media: true) } before do subject.call(body, account) end it 'updates remote user\'s account information' do account.reload expect(account.display_name).to eq '::1' end it 'rejects remote user\'s avatar' do account.reload expect(account.display_name).to eq '::1' expect(account.avatar_file_name).to be_nil end it 'creates posts' do expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=1:objectType=Status')).to_not be_nil expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=2:objectType=Status')).to_not be_nil end it 'creates posts with remote-only media' do status = Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=14:objectType=Status') expect(status).to_not be_nil expect(status.media_attachments.first.file_file_name).to be_nil expect(status.media_attachments.first.unknown?).to be true end end end it 'does not accept tampered reblogs' do good_actor = Fabricate(:account, username: 'tracer', domain: 'overwatch.com') real_body = < tag:overwatch.com,2017-04-27:objectId=4467137:objectType=Status 2017-04-27T13:49:25Z 2017-04-27T13:49:25Z http://activitystrea.ms/schema/1.0/note http://activitystrea.ms/schema/1.0/post https://overwatch.com/users/tracer http://activitystrea.ms/schema/1.0/person https://overwatch.com/users/tracer tracer Overwatch rocks XML stub_request(:get, 'https://overwatch.com/users/tracer/updates/1').to_return(status: 200, body: real_body, headers: { 'Content-Type' => 'application/atom+xml' }) bad_actor = Fabricate(:account, username: 'sombra', domain: 'talon.xyz') body = < tag:talon.xyz,2017-04-27:objectId=4467137:objectType=Status 2017-04-27T13:49:25Z 2017-04-27T13:49:25Z https://talon.xyz/users/sombra http://activitystrea.ms/schema/1.0/person https://talon.xyz/users/sombra sombra http://activitystrea.ms/schema/1.0/activity http://activitystrea.ms/schema/1.0/share Overwatch SUCKS AHAHA tag:overwatch.com,2017-04-27:objectId=4467137:objectType=Status http://activitystrea.ms/schema/1.0/note http://activitystrea.ms/schema/1.0/post https://overwatch.com/users/tracer http://activitystrea.ms/schema/1.0/person https://overwatch.com/users/tracer tracer Overwatch SUCKS AHAHA XML created_statuses = subject.call(body, bad_actor) expect(created_statuses.first.reblog?).to be true expect(created_statuses.first.account_id).to eq bad_actor.id expect(created_statuses.first.reblog.account_id).to eq good_actor.id expect(created_statuses.first.reblog.text).to eq 'Overwatch rocks' end it 'ignores reblogs if it failed to retrieve reblogged statuses' do stub_request(:get, 'https://overwatch.com/users/tracer/updates/1').to_return(status: 404) actor = Fabricate(:account, username: 'tracer', domain: 'overwatch.com') body = < tag:overwatch.com,2017-04-27:objectId=4467137:objectType=Status 2017-04-27T13:49:25Z 2017-04-27T13:49:25Z https://overwatch.com/users/tracer http://activitystrea.ms/schema/1.0/person https://overwatch.com/users/tracer tracer http://activitystrea.ms/schema/1.0/activity http://activitystrea.ms/schema/1.0/share Overwatch rocks tag:overwatch.com,2017-04-27:objectId=4467137:objectType=Status http://activitystrea.ms/schema/1.0/note http://activitystrea.ms/schema/1.0/post https://overwatch.com/users/tracer http://activitystrea.ms/schema/1.0/person https://overwatch.com/users/tracer tracer Overwatch rocks XML created_statuses = subject.call(body, actor) expect(created_statuses).to eq [] end it 'ignores statuses with an out-of-order delete' do sender = Fabricate(:account, username: 'tracer', domain: 'overwatch.com') delete_body = < tag:overwatch.com,2017-04-27:objectId=4487555:objectType=Status 2017-04-27T13:49:25Z 2017-04-27T13:49:25Z http://activitystrea.ms/schema/1.0/note http://activitystrea.ms/schema/1.0/delete https://overwatch.com/users/tracer http://activitystrea.ms/schema/1.0/person https://overwatch.com/users/tracer tracer XML status_body = < tag:overwatch.com,2017-04-27:objectId=4487555:objectType=Status 2017-04-27T13:49:25Z 2017-04-27T13:49:25Z http://activitystrea.ms/schema/1.0/note http://activitystrea.ms/schema/1.0/post https://overwatch.com/users/tracer http://activitystrea.ms/schema/1.0/person https://overwatch.com/users/tracer tracer Overwatch rocks XML subject.call(delete_body, sender) created_statuses = subject.call(status_body, sender) expect(created_statuses).to be_empty end end ================================================ FILE: spec/services/process_interaction_service_spec.rb ================================================ require 'rails_helper' RSpec.describe ProcessInteractionService, type: :service do let(:receiver) { Fabricate(:user, email: 'alice@example.com', account: Fabricate(:account, username: 'alice')).account } let(:sender) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } let(:remote_sender) { Fabricate(:account, username: 'carol', domain: 'localdomain.com', uri: 'https://webdomain.com/users/carol') } subject { ProcessInteractionService.new } describe 'status delete slap' do let(:remote_status) { Fabricate(:status, account: remote_sender) } let(:envelope) { OStatus2::Salmon.new.pack(payload, sender.keypair) } let(:payload) { <<~XML carol@localdomain.com carol https://webdomain.com/users/carol #{remote_status.id} http://activitystrea.ms/schema/1.0/delete XML } before do receiver.update(locked: true) remote_sender.update(private_key: sender.private_key, public_key: remote_sender.public_key) end it 'deletes a record' do expect(RemovalWorker).to receive(:perform_async).with(remote_status.id) subject.call(envelope, receiver) end end describe 'follow request slap' do before do receiver.update(locked: true) payload = < bob https://cb6e6126.ngrok.io/users/bob someIdHere http://activitystrea.ms/schema/1.0/request-friend XML envelope = OStatus2::Salmon.new.pack(payload, sender.keypair) subject.call(envelope, receiver) end it 'creates a record' do expect(FollowRequest.find_by(account: sender, target_account: receiver)).to_not be_nil end end describe 'follow request slap from known remote user identified by email' do before do receiver.update(locked: true) # Copy already-generated key remote_sender.update(private_key: sender.private_key, public_key: remote_sender.public_key) payload = < carol@localdomain.com carol https://webdomain.com/users/carol someIdHere http://activitystrea.ms/schema/1.0/request-friend XML envelope = OStatus2::Salmon.new.pack(payload, remote_sender.keypair) subject.call(envelope, receiver) end it 'creates a record' do expect(FollowRequest.find_by(account: remote_sender, target_account: receiver)).to_not be_nil end end describe 'follow request authorization slap' do before do receiver.update(locked: true) FollowRequest.create(account: sender, target_account: receiver) payload = < alice https://cb6e6126.ngrok.io/users/alice someIdHere http://activitystrea.ms/schema/1.0/authorize XML envelope = OStatus2::Salmon.new.pack(payload, receiver.keypair) subject.call(envelope, sender) end it 'creates a follow relationship' do expect(Follow.find_by(account: sender, target_account: receiver)).to_not be_nil end it 'removes the follow request' do expect(FollowRequest.find_by(account: sender, target_account: receiver)).to be_nil end end describe 'follow request rejection slap' do before do receiver.update(locked: true) FollowRequest.create(account: sender, target_account: receiver) payload = < alice https://cb6e6126.ngrok.io/users/alice someIdHere http://activitystrea.ms/schema/1.0/reject XML envelope = OStatus2::Salmon.new.pack(payload, receiver.keypair) subject.call(envelope, sender) end it 'does not create a follow relationship' do expect(Follow.find_by(account: sender, target_account: receiver)).to be_nil end it 'removes the follow request' do expect(FollowRequest.find_by(account: sender, target_account: receiver)).to be_nil end end end ================================================ FILE: spec/services/process_mentions_service_spec.rb ================================================ require 'rails_helper' RSpec.describe ProcessMentionsService, type: :service do let(:account) { Fabricate(:account, username: 'alice') } let(:visibility) { :public } let(:status) { Fabricate(:status, account: account, text: "Hello @#{remote_user.acct}", visibility: visibility) } context 'OStatus with public toot' do let(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :ostatus, domain: 'example.com', salmon_url: 'http://salmon.example.com') } subject { ProcessMentionsService.new } before do stub_request(:post, remote_user.salmon_url) subject.call(status) end it 'creates a mention' do expect(remote_user.mentions.where(status: status).count).to eq 1 end it 'posts to remote user\'s Salmon end point' do expect(a_request(:post, remote_user.salmon_url)).to have_been_made.once end end context 'OStatus with private toot' do let(:visibility) { :private } let(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :ostatus, domain: 'example.com', salmon_url: 'http://salmon.example.com') } subject { ProcessMentionsService.new } before do stub_request(:post, remote_user.salmon_url) subject.call(status) end it 'does not create a mention' do expect(remote_user.mentions.where(status: status).count).to eq 0 end it 'does not post to remote user\'s Salmon end point' do expect(a_request(:post, remote_user.salmon_url)).to_not have_been_made end end context 'ActivityPub' do let(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') } subject { ProcessMentionsService.new } before do stub_request(:post, remote_user.inbox_url) subject.call(status) end it 'creates a mention' do expect(remote_user.mentions.where(status: status).count).to eq 1 end it 'sends activity to the inbox' do expect(a_request(:post, remote_user.inbox_url)).to have_been_made.once end end context 'Temporarily-unreachable ActivityPub user' do let(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox', last_webfingered_at: nil) } subject { ProcessMentionsService.new } before do stub_request(:get, "https://example.com/.well-known/host-meta").to_return(status: 404) stub_request(:get, "https://example.com/.well-known/webfinger?resource=acct:remote_user@example.com").to_return(status: 500) stub_request(:post, remote_user.inbox_url) subject.call(status) end it 'creates a mention' do expect(remote_user.mentions.where(status: status).count).to eq 1 end it 'sends activity to the inbox' do expect(a_request(:post, remote_user.inbox_url)).to have_been_made.once end end end ================================================ FILE: spec/services/pubsubhubbub/subscribe_service_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe Pubsubhubbub::SubscribeService, type: :service do describe '#call' do subject { described_class.new } let(:user_account) { Fabricate(:account) } context 'with a nil account' do it 'returns the invalid topic status results' do result = service_call(account: nil) expect(result).to eq invalid_topic_status end end context 'with an invalid callback url' do it 'returns invalid callback status when callback is blank' do result = service_call(callback: '') expect(result).to eq invalid_callback_status end it 'returns invalid callback status when callback is not a URI' do result = service_call(callback: 'invalid-hostname') expect(result).to eq invalid_callback_status end end context 'with a blocked domain in the callback' do it 'returns callback not allowed' do Fabricate(:domain_block, domain: 'test.host', severity: :suspend) result = service_call(callback: 'https://test.host/api') expect(result).to eq not_allowed_callback_status end end context 'with a valid account and callback' do it 'returns success status and confirms subscription' do allow(Pubsubhubbub::ConfirmationWorker).to receive(:perform_async).and_return(nil) subscription = Fabricate(:subscription, account: user_account) result = service_call(callback: subscription.callback_url) expect(result).to eq success_status expect(Pubsubhubbub::ConfirmationWorker).to have_received(:perform_async).with(subscription.id, 'subscribe', 'asdf', 3600) end end end def service_call(account: user_account, callback: 'https://callback.host', secret: 'asdf', lease_seconds: 3600) subject.call(account, callback, secret, lease_seconds) end def invalid_topic_status ['Invalid topic URL', 422] end def invalid_callback_status ['Invalid callback URL', 422] end def not_allowed_callback_status ['Callback URL not allowed', 403] end def success_status ['', 202] end end ================================================ FILE: spec/services/pubsubhubbub/unsubscribe_service_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe Pubsubhubbub::UnsubscribeService, type: :service do describe '#call' do subject { described_class.new } context 'with a nil account' do it 'returns an invalid topic status' do result = subject.call(nil, 'callback.host') expect(result).to eq invalid_topic_status end end context 'with a valid account' do let(:account) { Fabricate(:account) } it 'returns a valid topic status and does not run confirm when no subscription' do allow(Pubsubhubbub::ConfirmationWorker).to receive(:perform_async).and_return(nil) result = subject.call(account, 'callback.host') expect(result).to eq valid_topic_status expect(Pubsubhubbub::ConfirmationWorker).not_to have_received(:perform_async) end it 'returns a valid topic status and does run confirm when there is a subscription' do subscription = Fabricate(:subscription, account: account, callback_url: 'callback.host') allow(Pubsubhubbub::ConfirmationWorker).to receive(:perform_async).and_return(nil) result = subject.call(account, 'callback.host') expect(result).to eq valid_topic_status expect(Pubsubhubbub::ConfirmationWorker).to have_received(:perform_async).with(subscription.id, 'unsubscribe') end end def invalid_topic_status ['Invalid topic URL', 422] end def valid_topic_status ['', 202] end end end ================================================ FILE: spec/services/reblog_service_spec.rb ================================================ require 'rails_helper' RSpec.describe ReblogService, type: :service do let(:alice) { Fabricate(:account, username: 'alice') } context 'creates a reblog with appropriate visibility' do let(:visibility) { :public } let(:reblog_visibility) { :public } let(:status) { Fabricate(:status, account: alice, visibility: visibility) } subject { ReblogService.new } before do subject.call(alice, status, visibility: reblog_visibility) end describe 'boosting privately' do let(:reblog_visibility) { :private } it 'reblogs privately' do expect(status.reblogs.first.visibility).to eq 'private' end end describe 'public reblogs of private toots should remain private' do let(:visibility) { :private } let(:reblog_visibility) { :public } it 'reblogs privately' do expect(status.reblogs.first.visibility).to eq 'private' end end end context 'OStatus' do let(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com') } let(:status) { Fabricate(:status, account: bob, uri: 'tag:example.com;something:something') } subject { ReblogService.new } before do stub_request(:post, 'http://salmon.example.com') subject.call(alice, status) end it 'creates a reblog' do expect(status.reblogs.count).to eq 1 end it 'sends a Salmon slap for a remote reblog' do expect(a_request(:post, 'http://salmon.example.com')).to have_been_made end end context 'ActivityPub' do let(:bob) { Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') } let(:status) { Fabricate(:status, account: bob) } subject { ReblogService.new } before do stub_request(:post, bob.inbox_url) allow(ActivityPub::DistributionWorker).to receive(:perform_async) subject.call(alice, status) end it 'creates a reblog' do expect(status.reblogs.count).to eq 1 end describe 'after_create_commit :store_uri' do it 'keeps consistent reblog count' do expect(status.reblogs.count).to eq 1 end end it 'distributes to followers' do expect(ActivityPub::DistributionWorker).to have_received(:perform_async) end it 'sends an announce activity to the author' do expect(a_request(:post, bob.inbox_url)).to have_been_made.once end end end ================================================ FILE: spec/services/reject_follow_service_spec.rb ================================================ require 'rails_helper' RSpec.describe RejectFollowService, type: :service do let(:sender) { Fabricate(:account, username: 'alice') } subject { RejectFollowService.new } describe 'local' do let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } before do FollowRequest.create(account: bob, target_account: sender) subject.call(bob, sender) end it 'removes follow request' do expect(bob.requested?(sender)).to be false end it 'does not create follow relation' do expect(bob.following?(sender)).to be false end end describe 'remote OStatus' do let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account } before do FollowRequest.create(account: bob, target_account: sender) stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {}) subject.call(bob, sender) end it 'removes follow request' do expect(bob.requested?(sender)).to be false end it 'does not create follow relation' do expect(bob.following?(sender)).to be false end it 'sends a follow request rejection salmon slap' do expect(a_request(:post, "http://salmon.example.com/").with { |req| xml = OStatus2::Salmon.new.unpack(req.body) xml.match(OStatus::TagManager::VERBS[:reject]) }).to have_been_made.once end end describe 'remote ActivityPub' do let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox')).account } before do FollowRequest.create(account: bob, target_account: sender) stub_request(:post, bob.inbox_url).to_return(status: 200) subject.call(bob, sender) end it 'removes follow request' do expect(bob.requested?(sender)).to be false end it 'does not create follow relation' do expect(bob.following?(sender)).to be false end it 'sends a reject activity' do expect(a_request(:post, bob.inbox_url)).to have_been_made.once end end end ================================================ FILE: spec/services/remove_status_service_spec.rb ================================================ require 'rails_helper' RSpec.describe RemoveStatusService, type: :service do subject { RemoveStatusService.new } let!(:alice) { Fabricate(:account) } let!(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://example.com/salmon') } let!(:jeff) { Fabricate(:account) } let!(:hank) { Fabricate(:account, username: 'hank', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') } let!(:bill) { Fabricate(:account, username: 'bill', protocol: :activitypub, domain: 'example2.com', inbox_url: 'http://example2.com/inbox') } before do stub_request(:post, 'http://example.com/push').to_return(status: 200, body: '', headers: {}) stub_request(:post, 'http://example.com/salmon').to_return(status: 200, body: '', headers: {}) stub_request(:post, 'http://example.com/inbox').to_return(status: 200) stub_request(:post, 'http://example2.com/inbox').to_return(status: 200) Fabricate(:subscription, account: alice, callback_url: 'http://example.com/push', confirmed: true, expires_at: 30.days.from_now) jeff.follow!(alice) hank.follow!(alice) @status = PostStatusService.new.call(alice, text: 'Hello @bob@example.com') Fabricate(:status, account: bill, reblog: @status, uri: 'hoge') subject.call(@status) end it 'removes status from author\'s home feed' do expect(HomeFeed.new(alice).get(10)).to_not include(@status.id) end it 'removes status from local follower\'s home feed' do expect(HomeFeed.new(jeff).get(10)).to_not include(@status.id) end it 'sends PuSH update to PuSH subscribers' do expect(a_request(:post, 'http://example.com/push').with { |req| req.body.match(OStatus::TagManager::VERBS[:delete]) }).to have_been_made end it 'sends delete activity to followers' do expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.twice end it 'sends Salmon slap to previously mentioned users' do expect(a_request(:post, "http://example.com/salmon").with { |req| xml = OStatus2::Salmon.new.unpack(req.body) xml.match(OStatus::TagManager::VERBS[:delete]) }).to have_been_made.once end it 'sends delete activity to rebloggers' do expect(a_request(:post, 'http://example2.com/inbox')).to have_been_made end end ================================================ FILE: spec/services/report_service_spec.rb ================================================ require 'rails_helper' RSpec.describe ReportService, type: :service do subject { described_class.new } let(:source_account) { Fabricate(:user).account } context 'for a remote account' do let(:remote_account) { Fabricate(:account, domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox') } before do stub_request(:post, 'http://example.com/inbox').to_return(status: 200) end it 'sends ActivityPub payload when forward is true' do subject.call(source_account, remote_account, forward: true) expect(a_request(:post, 'http://example.com/inbox')).to have_been_made end it 'does not send anything when forward is false' do subject.call(source_account, remote_account, forward: false) expect(a_request(:post, 'http://example.com/inbox')).to_not have_been_made end it 'has an uri' do report = subject.call(source_account, remote_account, forward: true) expect(report.uri).to_not be_nil end end context 'when other reports already exist for the same target' do let!(:target_account) { Fabricate(:account) } let!(:other_report) { Fabricate(:report, target_account: target_account) } subject do -> { described_class.new.call(source_account, target_account) } end before do ActionMailer::Base.deliveries.clear source_account.user.settings.notification_emails['report'] = true end it 'does not send an e-mail' do is_expected.to_not change(ActionMailer::Base.deliveries, :count).from(0) end end end ================================================ FILE: spec/services/resolve_account_service_spec.rb ================================================ require 'rails_helper' RSpec.describe ResolveAccountService, type: :service do subject { described_class.new } before do stub_request(:get, "https://quitter.no/.well-known/host-meta").to_return(request_fixture('.host-meta.txt')) stub_request(:get, "https://example.com/.well-known/webfinger?resource=acct:catsrgr8@example.com").to_return(status: 404) stub_request(:get, "https://redirected.com/.well-known/host-meta").to_return(request_fixture('redirected.host-meta.txt')) stub_request(:get, "https://example.com/.well-known/host-meta").to_return(status: 404) stub_request(:get, "https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no").to_return(request_fixture('webfinger.txt')) stub_request(:get, "https://redirected.com/.well-known/webfinger?resource=acct:gargron@redirected.com").to_return(request_fixture('webfinger.txt')) stub_request(:get, "https://redirected.com/.well-known/webfinger?resource=acct:hacker1@redirected.com").to_return(request_fixture('webfinger-hacker1.txt')) stub_request(:get, "https://redirected.com/.well-known/webfinger?resource=acct:hacker2@redirected.com").to_return(request_fixture('webfinger-hacker2.txt')) stub_request(:get, "https://quitter.no/.well-known/webfinger?resource=acct:catsrgr8@quitter.no").to_return(status: 404) stub_request(:get, "https://quitter.no/api/statuses/user_timeline/7477.atom").to_return(request_fixture('feed.txt')) stub_request(:get, "https://quitter.no/avatar/7477-300-20160211190340.png").to_return(request_fixture('avatar.txt')) stub_request(:get, "https://localdomain.com/.well-known/host-meta").to_return(request_fixture('localdomain-hostmeta.txt')) stub_request(:get, "https://localdomain.com/.well-known/webfinger?resource=acct:foo@localdomain.com").to_return(status: 404) stub_request(:get, "https://webdomain.com/.well-known/webfinger?resource=acct:foo@localdomain.com").to_return(request_fixture('localdomain-webfinger.txt')) stub_request(:get, "https://webdomain.com/users/foo.atom").to_return(request_fixture('localdomain-feed.txt')) end it 'raises error if no such user can be resolved via webfinger' do expect(subject.call('catsrgr8@quitter.no')).to be_nil end it 'raises error if the domain does not have webfinger' do expect(subject.call('catsrgr8@example.com')).to be_nil end it 'prevents hijacking existing accounts' do account = subject.call('hacker1@redirected.com') expect(account.salmon_url).to_not eq 'https://hacker.com/main/salmon/user/7477' end it 'prevents hijacking inexisting accounts' do expect(subject.call('hacker2@redirected.com')).to be_nil end context 'with an OStatus account' do it 'returns an already existing remote account' do old_account = Fabricate(:account, username: 'gargron', domain: 'quitter.no') returned_account = subject.call('gargron@quitter.no') expect(old_account.id).to eq returned_account.id end it 'returns a new remote account' do account = subject.call('gargron@quitter.no') expect(account.username).to eq 'gargron' expect(account.domain).to eq 'quitter.no' expect(account.remote_url).to eq 'https://quitter.no/api/statuses/user_timeline/7477.atom' end it 'follows a legitimate account redirection' do account = subject.call('gargron@redirected.com') expect(account.username).to eq 'gargron' expect(account.domain).to eq 'quitter.no' expect(account.remote_url).to eq 'https://quitter.no/api/statuses/user_timeline/7477.atom' end it 'returns a new remote account' do account = subject.call('foo@localdomain.com') expect(account.username).to eq 'foo' expect(account.domain).to eq 'localdomain.com' expect(account.remote_url).to eq 'https://webdomain.com/users/foo.atom' end end context 'with an ActivityPub account' do before do stub_request(:get, "https://ap.example.com/.well-known/webfinger?resource=acct:foo@ap.example.com").to_return(request_fixture('activitypub-webfinger.txt')) stub_request(:get, "https://ap.example.com/users/foo").to_return(request_fixture('activitypub-actor.txt')) stub_request(:get, "https://ap.example.com/users/foo.atom").to_return(request_fixture('activitypub-feed.txt')) stub_request(:get, %r{https://ap.example.com/users/foo/\w+}).to_return(status: 404) end it 'fallback to OStatus if actor json could not be fetched' do stub_request(:get, "https://ap.example.com/users/foo").to_return(status: 404) account = subject.call('foo@ap.example.com') expect(account.ostatus?).to eq true expect(account.remote_url).to eq 'https://ap.example.com/users/foo.atom' end it 'fallback to OStatus if actor json did not have inbox_url' do stub_request(:get, "https://ap.example.com/users/foo").to_return(request_fixture('activitypub-actor-noinbox.txt')) account = subject.call('foo@ap.example.com') expect(account.ostatus?).to eq true expect(account.remote_url).to eq 'https://ap.example.com/users/foo.atom' end it 'returns new remote account' do account = subject.call('foo@ap.example.com') expect(account.activitypub?).to eq true expect(account.domain).to eq 'ap.example.com' expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox' end context 'with multiple types' do before do stub_request(:get, "https://ap.example.com/users/foo").to_return(request_fixture('activitypub-actor-individual.txt')) end it 'returns new remote account' do account = subject.call('foo@ap.example.com') expect(account.activitypub?).to eq true expect(account.domain).to eq 'ap.example.com' expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox' expect(account.actor_type).to eq 'Person' end end end it 'processes one remote account at a time using locks' do wait_for_start = true fail_occurred = false return_values = [] threads = Array.new(5) do Thread.new do true while wait_for_start begin return_values << described_class.new.call('foo@localdomain.com') rescue ActiveRecord::RecordNotUnique fail_occurred = true end end end wait_for_start = false threads.each(&:join) expect(fail_occurred).to be false expect(return_values).to_not include(nil) end end ================================================ FILE: spec/services/resolve_url_service_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe ResolveURLService, type: :service do subject { described_class.new } describe '#call' do it 'returns nil when there is no atom url' do url = 'http://example.com/missing-atom' service = double allow(FetchAtomService).to receive(:new).and_return service allow(service).to receive(:call).with(url).and_return(nil) result = subject.call(url) expect(result).to be_nil end it 'fetches remote accounts for feed types' do url = 'http://example.com/atom-feed' service = double allow(FetchAtomService).to receive(:new).and_return service feed_url = 'http://feed-url' feed_content = 'contents' allow(service).to receive(:call).with(url).and_return([feed_url, { prefetched_body: feed_content }]) account_service = double allow(FetchRemoteAccountService).to receive(:new).and_return(account_service) allow(account_service).to receive(:call) _result = subject.call(url) expect(account_service).to have_received(:call).with(feed_url, feed_content, nil) end it 'fetches remote statuses for entry types' do url = 'http://example.com/atom-entry' service = double allow(FetchAtomService).to receive(:new).and_return service feed_url = 'http://feed-url' feed_content = 'contents' allow(service).to receive(:call).with(url).and_return([feed_url, { prefetched_body: feed_content }]) account_service = double allow(FetchRemoteStatusService).to receive(:new).and_return(account_service) allow(account_service).to receive(:call) _result = subject.call(url) expect(account_service).to have_received(:call).with(feed_url, feed_content, nil) end end end ================================================ FILE: spec/services/search_service_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe SearchService, type: :service do subject { described_class.new } describe '#call' do describe 'with a blank query' do it 'returns empty results without searching' do allow(AccountSearchService).to receive(:new) allow(Tag).to receive(:search_for) results = subject.call('', nil, 10) expect(results).to eq(empty_results) expect(AccountSearchService).not_to have_received(:new) expect(Tag).not_to have_received(:search_for) end end describe 'with an url query' do before do @query = 'http://test.host/query' end context 'that does not find anything' do it 'returns the empty results' do service = double(call: nil) allow(ResolveURLService).to receive(:new).and_return(service) results = subject.call(@query, nil, 10) expect(service).to have_received(:call).with(@query, on_behalf_of: nil) expect(results).to eq empty_results end end context 'that finds an account' do it 'includes the account in the results' do account = Account.new service = double(call: account) allow(ResolveURLService).to receive(:new).and_return(service) results = subject.call(@query, nil, 10) expect(service).to have_received(:call).with(@query, on_behalf_of: nil) expect(results).to eq empty_results.merge(accounts: [account]) end end context 'that finds a status' do it 'includes the status in the results' do status = Status.new service = double(call: status) allow(ResolveURLService).to receive(:new).and_return(service) results = subject.call(@query, nil, 10) expect(service).to have_received(:call).with(@query, on_behalf_of: nil) expect(results).to eq empty_results.merge(statuses: [status]) end end end describe 'with a non-url query' do context 'that matches an account' do it 'includes the account in the results' do query = 'username' account = Account.new service = double(call: [account]) allow(AccountSearchService).to receive(:new).and_return(service) results = subject.call(query, nil, 10) expect(service).to have_received(:call).with(query, nil, limit: 10, offset: 0, resolve: false) expect(results).to eq empty_results.merge(accounts: [account]) end end context 'that matches a tag' do it 'includes the tag in the results' do query = '#tag' tag = Tag.new allow(Tag).to receive(:search_for).with('tag', 10, 0).and_return([tag]) results = subject.call(query, nil, 10) expect(Tag).to have_received(:search_for).with('tag', 10, 0) expect(results).to eq empty_results.merge(hashtags: [tag]) end it 'does not include tag when starts with @ character' do query = '@username' allow(Tag).to receive(:search_for) results = subject.call(query, nil, 10) expect(Tag).not_to have_received(:search_for) expect(results).to eq empty_results end end end end def empty_results { accounts: [], hashtags: [], statuses: [] } end end ================================================ FILE: spec/services/send_interaction_service_spec.rb ================================================ require 'rails_helper' RSpec.describe SendInteractionService, type: :service do subject { SendInteractionService.new } it 'sends an XML envelope to the Salmon end point of remote user' end ================================================ FILE: spec/services/subscribe_service_spec.rb ================================================ require 'rails_helper' RSpec.describe SubscribeService, type: :service do let(:account) { Fabricate(:account, username: 'bob', domain: 'example.com', hub_url: 'http://hub.example.com') } subject { SubscribeService.new } it 'sends subscription request to PuSH hub' do stub_request(:post, 'http://hub.example.com/').to_return(status: 202) subject.call(account) expect(a_request(:post, 'http://hub.example.com/')).to have_been_made.once end it 'generates and keeps PuSH secret on successful call' do stub_request(:post, 'http://hub.example.com/').to_return(status: 202) subject.call(account) expect(account.secret).to_not be_blank end it 'fails silently if PuSH hub forbids subscription' do stub_request(:post, 'http://hub.example.com/').to_return(status: 403) subject.call(account) end it 'fails silently if PuSH hub is not found' do stub_request(:post, 'http://hub.example.com/').to_return(status: 404) subject.call(account) end it 'fails loudly if there is a network error' do stub_request(:post, 'http://hub.example.com/').to_raise(HTTP::Error) expect { subject.call(account) }.to raise_error HTTP::Error end it 'fails loudly if PuSH hub is unavailable' do stub_request(:post, 'http://hub.example.com/').to_return(status: 503) expect { subject.call(account) }.to raise_error Mastodon::UnexpectedResponseError end it 'fails loudly if rate limited' do stub_request(:post, 'http://hub.example.com/').to_return(status: 429) expect { subject.call(account) }.to raise_error Mastodon::UnexpectedResponseError end end ================================================ FILE: spec/services/suspend_account_service_spec.rb ================================================ require 'rails_helper' RSpec.describe SuspendAccountService, type: :service do describe '#call on local account' do before do stub_request(:post, "https://alice.com/inbox").to_return(status: 201) stub_request(:post, "https://bob.com/inbox").to_return(status: 201) end subject do -> { described_class.new.call(account) } end let!(:account) { Fabricate(:account) } let!(:status) { Fabricate(:status, account: account) } let!(:media_attachment) { Fabricate(:media_attachment, account: account) } let!(:notification) { Fabricate(:notification, account: account) } let!(:favourite) { Fabricate(:favourite, account: account) } let!(:active_relationship) { Fabricate(:follow, account: account) } let!(:passive_relationship) { Fabricate(:follow, target_account: account) } let!(:subscription) { Fabricate(:subscription, account: account) } let!(:remote_alice) { Fabricate(:account, inbox_url: 'https://alice.com/inbox', protocol: :activitypub) } let!(:remote_bob) { Fabricate(:account, inbox_url: 'https://bob.com/inbox', protocol: :activitypub) } it 'deletes associated records' do is_expected.to change { [ account.statuses, account.media_attachments, account.stream_entries, account.notifications, account.favourites, account.active_relationships, account.passive_relationships, account.subscriptions ].map(&:count) }.from([1, 1, 1, 1, 1, 1, 1, 1]).to([0, 0, 0, 0, 0, 0, 0, 0]) end it 'sends a delete actor activity to all known inboxes' do subject.call expect(a_request(:post, "https://alice.com/inbox")).to have_been_made.once expect(a_request(:post, "https://bob.com/inbox")).to have_been_made.once end end describe '#call on remote account' do before do stub_request(:post, "https://alice.com/inbox").to_return(status: 201) stub_request(:post, "https://bob.com/inbox").to_return(status: 201) end subject do -> { described_class.new.call(remote_bob) } end let!(:account) { Fabricate(:account) } let!(:remote_alice) { Fabricate(:account, inbox_url: 'https://alice.com/inbox', protocol: :activitypub) } let!(:remote_bob) { Fabricate(:account, inbox_url: 'https://bob.com/inbox', protocol: :activitypub) } let!(:status) { Fabricate(:status, account: remote_bob) } let!(:media_attachment) { Fabricate(:media_attachment, account: remote_bob) } let!(:notification) { Fabricate(:notification, account: remote_bob) } let!(:favourite) { Fabricate(:favourite, account: remote_bob) } let!(:active_relationship) { Fabricate(:follow, account: remote_bob, target_account: account) } let!(:passive_relationship) { Fabricate(:follow, target_account: remote_bob) } let!(:subscription) { Fabricate(:subscription, account: remote_bob) } it 'deletes associated records' do is_expected.to change { [ remote_bob.statuses, remote_bob.media_attachments, remote_bob.stream_entries, remote_bob.notifications, remote_bob.favourites, remote_bob.active_relationships, remote_bob.passive_relationships, remote_bob.subscriptions ].map(&:count) }.from([1, 1, 1, 1, 1, 1, 1, 1]).to([0, 0, 0, 0, 0, 0, 0, 0]) end it 'sends a reject follow to follwer inboxes' do subject.call expect(a_request(:post, remote_bob.inbox_url)).to have_been_made.once end end end ================================================ FILE: spec/services/unblock_domain_service_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe UnblockDomainService, type: :service do subject { described_class.new } describe 'call' do before do @independently_suspended = Fabricate(:account, domain: 'example.com', suspended_at: 1.hour.ago) @independently_silenced = Fabricate(:account, domain: 'example.com', silenced_at: 1.hour.ago) @domain_block = Fabricate(:domain_block, domain: 'example.com') @silenced = Fabricate(:account, domain: 'example.com', silenced_at: @domain_block.created_at) @suspended = Fabricate(:account, domain: 'example.com', suspended_at: @domain_block.created_at) end it 'unsilences accounts and removes block' do @domain_block.update(severity: :silence) subject.call(@domain_block) expect_deleted_domain_block expect(@silenced.reload.silenced?).to be false expect(@suspended.reload.suspended?).to be true expect(@independently_suspended.reload.suspended?).to be true expect(@independently_silenced.reload.silenced?).to be true end it 'unsuspends accounts and removes block' do @domain_block.update(severity: :suspend) subject.call(@domain_block) expect_deleted_domain_block expect(@suspended.reload.suspended?).to be false expect(@silenced.reload.silenced?).to be true expect(@independently_suspended.reload.suspended?).to be true expect(@independently_silenced.reload.silenced?).to be true end end def expect_deleted_domain_block expect { @domain_block.reload }.to raise_error(ActiveRecord::RecordNotFound) end end ================================================ FILE: spec/services/unblock_service_spec.rb ================================================ require 'rails_helper' RSpec.describe UnblockService, type: :service do let(:sender) { Fabricate(:account, username: 'alice') } subject { UnblockService.new } describe 'local' do let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } before do sender.block!(bob) subject.call(sender, bob) end it 'destroys the blocking relation' do expect(sender.blocking?(bob)).to be false end end describe 'remote OStatus' do let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account } before do sender.block!(bob) stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {}) subject.call(sender, bob) end it 'destroys the blocking relation' do expect(sender.blocking?(bob)).to be false end it 'sends an unblock salmon slap' do expect(a_request(:post, "http://salmon.example.com/").with { |req| xml = OStatus2::Salmon.new.unpack(req.body) xml.match(OStatus::TagManager::VERBS[:unblock]) }).to have_been_made.once end end describe 'remote ActivityPub' do let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox')).account } before do sender.block!(bob) stub_request(:post, 'http://example.com/inbox').to_return(status: 200) subject.call(sender, bob) end it 'destroys the blocking relation' do expect(sender.blocking?(bob)).to be false end it 'sends an unblock activity' do expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once end end end ================================================ FILE: spec/services/unfollow_service_spec.rb ================================================ require 'rails_helper' RSpec.describe UnfollowService, type: :service do let(:sender) { Fabricate(:account, username: 'alice') } subject { UnfollowService.new } describe 'local' do let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } before do sender.follow!(bob) subject.call(sender, bob) end it 'destroys the following relation' do expect(sender.following?(bob)).to be false end end describe 'remote OStatus' do let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', protocol: :ostatus, domain: 'example.com', salmon_url: 'http://salmon.example.com')).account } before do sender.follow!(bob) stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {}) subject.call(sender, bob) end it 'destroys the following relation' do expect(sender.following?(bob)).to be false end it 'sends an unfollow salmon slap' do expect(a_request(:post, "http://salmon.example.com/").with { |req| xml = OStatus2::Salmon.new.unpack(req.body) xml.match(OStatus::TagManager::VERBS[:unfollow]) }).to have_been_made.once end end describe 'remote ActivityPub' do let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox')).account } before do sender.follow!(bob) stub_request(:post, 'http://example.com/inbox').to_return(status: 200) subject.call(sender, bob) end it 'destroys the following relation' do expect(sender.following?(bob)).to be false end it 'sends an unfollow activity' do expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once end end describe 'remote ActivityPub (reverse)' do let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox')).account } before do bob.follow!(sender) stub_request(:post, 'http://example.com/inbox').to_return(status: 200) subject.call(bob, sender) end it 'destroys the following relation' do expect(bob.following?(sender)).to be false end it 'sends a reject activity' do expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once end end end ================================================ FILE: spec/services/unmute_service_spec.rb ================================================ require 'rails_helper' RSpec.describe UnmuteService, type: :service do subject { UnmuteService.new } end ================================================ FILE: spec/services/unsubscribe_service_spec.rb ================================================ require 'rails_helper' RSpec.describe UnsubscribeService, type: :service do let(:account) { Fabricate(:account, username: 'bob', domain: 'example.com', hub_url: 'http://hub.example.com') } subject { UnsubscribeService.new } it 'removes the secret and resets expiration on account' do stub_request(:post, 'http://hub.example.com/').to_return(status: 204) subject.call(account) account.reload expect(account.secret).to be_blank expect(account.subscription_expires_at).to be_blank end it 'logs error on subscription failure' do logger = stub_logger stub_request(:post, 'http://hub.example.com/').to_return(status: 404) subject.call(account) expect(logger).to have_received(:debug).with(/unsubscribe for bob@example.com failed/) end it 'logs error on connection failure' do logger = stub_logger stub_request(:post, 'http://hub.example.com/').to_raise(HTTP::Error) subject.call(account) expect(logger).to have_received(:debug).with(/unsubscribe for bob@example.com failed/) end def stub_logger double(debug: nil).tap do |logger| allow(Rails).to receive(:logger).and_return(logger) end end end ================================================ FILE: spec/services/update_remote_profile_service_spec.rb ================================================ require 'rails_helper' RSpec.describe UpdateRemoteProfileService, type: :service do let(:xml) { File.read(Rails.root.join('spec', 'fixtures', 'push', 'feed.atom')) } subject { UpdateRemoteProfileService.new } before do stub_request(:get, 'https://quitter.no/avatar/7477-300-20160211190340.png').to_return(request_fixture('avatar.txt')) end context 'with updated details' do let(:remote_account) { Fabricate(:account, username: 'bob', domain: 'example.com') } before do subject.call(xml, remote_account) end it 'downloads new avatar' do expect(a_request(:get, 'https://quitter.no/avatar/7477-300-20160211190340.png')).to have_been_made end it 'sets the avatar remote url' do expect(remote_account.reload.avatar_remote_url).to eq 'https://quitter.no/avatar/7477-300-20160211190340.png' end it 'sets display name' do expect(remote_account.reload.display_name).to eq 'DIGITAL CAT' end it 'sets note' do expect(remote_account.reload.note).to eq 'Software engineer, free time musician and DIGITAL SPORTS enthusiast. Likes cats. Warning: May contain memes' end end context 'with unchanged details' do let(:remote_account) { Fabricate(:account, username: 'bob', domain: 'example.com', display_name: 'DIGITAL CAT', note: 'Software engineer, free time musician and DIGITAL SPORTS enthusiast. Likes cats. Warning: May contain memes', avatar_remote_url: 'https://quitter.no/avatar/7477-300-20160211190340.png') } before do subject.call(xml, remote_account) end it 'does not re-download avatar' do expect(a_request(:get, 'https://quitter.no/avatar/7477-300-20160211190340.png')).to have_been_made.once end it 'sets the avatar remote url' do expect(remote_account.reload.avatar_remote_url).to eq 'https://quitter.no/avatar/7477-300-20160211190340.png' end it 'sets display name' do expect(remote_account.reload.display_name).to eq 'DIGITAL CAT' end it 'sets note' do expect(remote_account.reload.note).to eq 'Software engineer, free time musician and DIGITAL SPORTS enthusiast. Likes cats. Warning: May contain memes' end end context 'with updated details from a domain set to reject media' do let(:remote_account) { Fabricate(:account, username: 'bob', domain: 'example.com') } let!(:domain_block) { Fabricate(:domain_block, domain: 'example.com', reject_media: true) } before do subject.call(xml, remote_account) end it 'does not the avatar remote url' do expect(remote_account.reload.avatar_remote_url).to be_nil end it 'sets display name' do expect(remote_account.reload.display_name).to eq 'DIGITAL CAT' end it 'sets note' do expect(remote_account.reload.note).to eq 'Software engineer, free time musician and DIGITAL SPORTS enthusiast. Likes cats. Warning: May contain memes' end it 'does not set store the avatar' do expect(remote_account.reload.avatar_file_name).to be_nil end end end ================================================ FILE: spec/services/verify_link_service_spec.rb ================================================ require 'rails_helper' RSpec.describe VerifyLinkService, type: :service do subject { described_class.new } context 'given a local account' do let(:account) { Fabricate(:account, username: 'alice') } let(:field) { Account::Field.new(account, 'name' => 'Website', 'value' => 'http://example.com') } before do stub_request(:head, 'https://redirect.me/abc').to_return(status: 301, headers: { 'Location' => ActivityPub::TagManager.instance.url_for(account) }) stub_request(:get, 'http://example.com').to_return(status: 200, body: html) subject.call(field) end context 'when a link contains an back' do let(:html) do <<-HTML Follow me on Mastodon HTML end it 'marks the field as verified' do expect(field.verified?).to be true end end context 'when a link contains an back' do let(:html) do <<-HTML Follow me on Mastodon HTML end it 'marks the field as verified' do expect(field.verified?).to be true end end context 'when a link contains a back' do let(:html) do <<-HTML HTML end it 'marks the field as verified' do expect(field.verified?).to be true end end context 'when a link goes through a redirect back' do let(:html) do <<-HTML HTML end it 'marks the field as verified' do expect(field.verified?).to be true end end context 'when a link does not contain a link back' do let(:html) { '' } it 'marks the field as verified' do expect(field.verified?).to be false end end end context 'given a remote account' do let(:account) { Fabricate(:account, username: 'alice', domain: 'example.com', url: 'https://profile.example.com/alice') } let(:field) { Account::Field.new(account, 'name' => 'Website', 'value' => 'example.com') } before do stub_request(:get, 'http://example.com').to_return(status: 200, body: html) subject.call(field) end context 'when a link contains an back' do let(:html) do <<-HTML Follow me on Mastodon HTML end it 'marks the field as verified' do expect(field.verified?).to be true end end end end ================================================ FILE: spec/spec_helper.rb ================================================ GC.disable if ENV['DISABLE_SIMPLECOV'] != 'true' require 'simplecov' SimpleCov.start 'rails' do add_group 'Services', 'app/services' add_group 'Presenters', 'app/presenters' add_group 'Validators', 'app/validators' end end gc_counter = -1 RSpec.configure do |config| config.expect_with :rspec do |expectations| expectations.include_chain_clauses_in_custom_matcher_descriptions = true end config.mock_with :rspec do |mocks| mocks.verify_partial_doubles = true config.around(:example, :without_verify_partial_doubles) do |example| mocks.verify_partial_doubles = false example.call mocks.verify_partial_doubles = true end end config.before :suite do Chewy.strategy(:bypass) end config.after :suite do gc_counter = 0 FileUtils.rm_rf(Dir["#{Rails.root}/spec/test_files/"]) end config.after :each do gc_counter += 1 if gc_counter > 19 GC.enable GC.start GC.disable gc_counter = 0 end end end def body_as_json json_str_to_hash(response.body) end def json_str_to_hash(str) JSON.parse(str, symbolize_names: true) end ================================================ FILE: spec/support/examples/lib/settings/scoped_settings.rb ================================================ # frozen_string_literal: true shared_examples 'ScopedSettings' do describe '[]' do it 'inherits default settings' do expect(Setting.boost_modal).to eq false expect(Setting.interactions['must_be_follower']).to eq false settings = create! expect(settings['boost_modal']).to eq false expect(settings['interactions']['must_be_follower']).to eq false end end describe 'all_as_records' do # expecting [] and []= works it 'returns records merged with default values except hashes' do expect(Setting.boost_modal).to eq false expect(Setting.delete_modal).to eq true settings = create! settings['boost_modal'] = true records = settings.all_as_records expect(records['boost_modal'].value).to eq true expect(records['delete_modal'].value).to eq true end end describe 'missing methods' do # expecting [] and []= works. it 'reads settings' do expect(Setting.boost_modal).to eq false settings = create! expect(settings.boost_modal).to eq false end it 'updates settings' do settings = fabricate settings.boost_modal = true expect(settings['boost_modal']).to eq true end end it 'can update settings with [] and can read with []=' do settings = fabricate settings['boost_modal'] = true settings['interactions'] = settings['interactions'].merge('must_be_follower' => true) Setting.save! expect(settings['boost_modal']).to eq true expect(settings['interactions']['must_be_follower']).to eq true Rails.cache.clear expect(settings['boost_modal']).to eq true expect(settings['interactions']['must_be_follower']).to eq true end xit 'does not mutate defaults via the cache' do fabricate['interactions']['must_be_follower'] = true # TODO # This mutates the global settings default such that future # instances will inherit the incorrect starting values expect(fabricate.settings['interactions']['must_be_follower']).to eq false end end ================================================ FILE: spec/support/examples/lib/settings/settings_extended.rb ================================================ # frozen_string_literal: true shared_examples 'Settings-extended' do describe 'settings' do def fabricate super.settings end def create! super.settings end it_behaves_like 'ScopedSettings' end end ================================================ FILE: spec/support/examples/models/concerns/account_avatar.rb ================================================ # frozen_string_literal: true shared_examples 'AccountAvatar' do |fabricator| describe 'static avatars' do describe 'when GIF' do it 'creates a png static style' do account = Fabricate(fabricator, avatar: attachment_fixture('avatar.gif')) expect(account.avatar_static_url).to_not eq account.avatar_original_url end end describe 'when non-GIF' do it 'does not create extra static style' do account = Fabricate(fabricator, avatar: attachment_fixture('attachment.jpg')) expect(account.avatar_static_url).to eq account.avatar_original_url end end end end ================================================ FILE: spec/support/matchers/model/model_have_error_on_field.rb ================================================ RSpec::Matchers.define :model_have_error_on_field do |expected| match do |record| if record.errors.empty? record.valid? end record.errors.has_key?(expected) end failure_message do |record| keys = record.errors.keys "expect record.errors(#{keys}) to include #{expected}" end end ================================================ FILE: spec/validators/blacklisted_email_validator_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' RSpec.describe BlacklistedEmailValidator, type: :validator do describe '#validate' do let(:user) { double(email: 'info@mail.com', errors: errors) } let(:errors) { double(add: nil) } before do allow(user).to receive(:valid_invitation?) { false } allow_any_instance_of(described_class).to receive(:blocked_email?) { blocked_email } described_class.new.validate(user) end context 'blocked_email?' do let(:blocked_email) { true } it 'calls errors.add' do expect(errors).to have_received(:add).with(:email, I18n.t('users.invalid_email')) end end context '!blocked_email?' do let(:blocked_email) { false } it 'not calls errors.add' do expect(errors).not_to have_received(:add).with(:email, I18n.t('users.invalid_email')) end end end end ================================================ FILE: spec/validators/disallowed_hashtags_validator_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' RSpec.describe DisallowedHashtagsValidator, type: :validator do describe '#validate' do before do allow_any_instance_of(described_class).to receive(:select_tags) { tags } described_class.new.validate(status) end let(:status) { double(errors: errors, local?: local, reblog?: reblog, text: '') } let(:errors) { double(add: nil) } context 'unless status.local? && !status.reblog?' do let(:local) { false } let(:reblog) { true } it 'not calls errors.add' do expect(errors).not_to have_received(:add).with(:text, any_args) end end context 'status.local? && !status.reblog?' do let(:local) { true } let(:reblog) { false } context 'tags.empty?' do let(:tags) { [] } it 'not calls errors.add' do expect(errors).not_to have_received(:add).with(:text, any_args) end end context '!tags.empty?' do let(:tags) { %w(a b c) } it 'calls errors.add' do expect(errors).to have_received(:add) .with(:text, I18n.t('statuses.disallowed_hashtags', tags: tags.join(', '), count: tags.size)) end end end end end ================================================ FILE: spec/validators/email_mx_validator_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe EmailMxValidator do describe '#validate' do let(:user) { double(email: 'foo@example.com', errors: double(add: nil)) } it 'adds an error if there are no DNS records for the e-mail domain' do resolver = double allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([]) allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([]) allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::AAAA).and_return([]) allow(resolver).to receive(:timeouts=).and_return(nil) allow(Resolv::DNS).to receive(:open).and_yield(resolver) subject.validate(user) expect(user.errors).to have_received(:add) end it 'adds an error if a MX record exists but does not lead to an IP' do resolver = double allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([double(exchange: 'mail.example.com')]) allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([]) allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::AAAA).and_return([]) allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::A).and_return([]) allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::AAAA).and_return([]) allow(resolver).to receive(:timeouts=).and_return(nil) allow(Resolv::DNS).to receive(:open).and_yield(resolver) subject.validate(user) expect(user.errors).to have_received(:add) end it 'adds an error if the A record is blacklisted' do EmailDomainBlock.create!(domain: '1.2.3.4') resolver = double allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([]) allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([double(address: '1.2.3.4')]) allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::AAAA).and_return([]) allow(resolver).to receive(:timeouts=).and_return(nil) allow(Resolv::DNS).to receive(:open).and_yield(resolver) subject.validate(user) expect(user.errors).to have_received(:add) end it 'adds an error if the AAAA record is blacklisted' do EmailDomainBlock.create!(domain: 'fd00::1') resolver = double allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([]) allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([]) allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::AAAA).and_return([double(address: 'fd00::1')]) allow(resolver).to receive(:timeouts=).and_return(nil) allow(Resolv::DNS).to receive(:open).and_yield(resolver) subject.validate(user) expect(user.errors).to have_received(:add) end it 'adds an error if the MX record is blacklisted' do EmailDomainBlock.create!(domain: '2.3.4.5') resolver = double allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([double(exchange: 'mail.example.com')]) allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([]) allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::AAAA).and_return([]) allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::A).and_return([double(address: '2.3.4.5')]) allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::AAAA).and_return([]) allow(resolver).to receive(:timeouts=).and_return(nil) allow(Resolv::DNS).to receive(:open).and_yield(resolver) subject.validate(user) expect(user.errors).to have_received(:add) end it 'adds an error if the MX IPv6 record is blacklisted' do EmailDomainBlock.create!(domain: 'fd00::2') resolver = double allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([double(exchange: 'mail.example.com')]) allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([]) allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::AAAA).and_return([]) allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::A).and_return([]) allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::AAAA).and_return([double(address: 'fd00::2')]) allow(resolver).to receive(:timeouts=).and_return(nil) allow(Resolv::DNS).to receive(:open).and_yield(resolver) subject.validate(user) expect(user.errors).to have_received(:add) end it 'adds an error if the MX hostname is blacklisted' do EmailDomainBlock.create!(domain: 'mail.example.com') resolver = double allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([double(exchange: 'mail.example.com')]) allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([]) allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::AAAA).and_return([]) allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::A).and_return([double(address: '2.3.4.5')]) allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::AAAA).and_return([double(address: 'fd00::2')]) allow(resolver).to receive(:timeouts=).and_return(nil) allow(Resolv::DNS).to receive(:open).and_yield(resolver) subject.validate(user) expect(user.errors).to have_received(:add) end end end ================================================ FILE: spec/validators/follow_limit_validator_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' RSpec.describe FollowLimitValidator, type: :validator do describe '#validate' do before do allow_any_instance_of(described_class).to receive(:limit_reached?).with(account) do limit_reached end described_class.new.validate(follow) end let(:follow) { double(account: account, errors: errors) } let(:errors) { double(add: nil) } let(:account) { double(nil?: _nil, local?: local, following_count: 0, followers_count: 0) } let(:_nil) { true } let(:local) { false } context 'follow.account.nil? || !follow.account.local?' do let(:_nil) { true } it 'not calls errors.add' do expect(errors).not_to have_received(:add).with(:base, any_args) end end context '!(follow.account.nil? || !follow.account.local?)' do let(:_nil) { false } let(:local) { true } context 'limit_reached?' do let(:limit_reached) { true } it 'calls errors.add' do expect(errors).to have_received(:add) .with(:base, I18n.t('users.follow_limit_reached', limit: FollowLimitValidator::LIMIT)) end end context '!limit_reached?' do let(:limit_reached) { false } it 'not calls errors.add' do expect(errors).not_to have_received(:add).with(:base, any_args) end end end end end ================================================ FILE: spec/validators/poll_validator_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' RSpec.describe PollValidator, type: :validator do describe '#validate' do before do validator.validate(poll) end let(:validator) { described_class.new } let(:poll) { double(options: options, expires_at: expires_at, errors: errors) } let(:errors) { double(add: nil) } let(:options) { %w(foo bar) } let(:expires_at) { 1.day.from_now } it 'have no errors' do expect(errors).not_to have_received(:add) end context 'expires just 5 min ago' do let(:expires_at) { 5.minutes.from_now } it 'not calls errors add' do expect(errors).not_to have_received(:add) end end end end ================================================ FILE: spec/validators/status_length_validator_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe StatusLengthValidator do describe '#validate' do it 'does not add errors onto remote statuses' do status = double(local?: false) subject.validate(status) expect(status).not_to receive(:errors) end it 'does not add errors onto local reblogs' do status = double(local?: false, reblog?: true) subject.validate(status) expect(status).not_to receive(:errors) end it 'adds an error when content warning is over 500 characters' do status = double(spoiler_text: 'a' * 520, text: '', errors: double(add: nil), local?: true, reblog?: false) subject.validate(status) expect(status.errors).to have_received(:add) end it 'adds an error when text is over 500 characters' do status = double(spoiler_text: '', text: 'a' * 520, errors: double(add: nil), local?: true, reblog?: false) subject.validate(status) expect(status.errors).to have_received(:add) end it 'adds an error when text and content warning are over 500 characters total' do status = double(spoiler_text: 'a' * 250, text: 'b' * 251, errors: double(add: nil), local?: true, reblog?: false) subject.validate(status) expect(status.errors).to have_received(:add) end it 'counts URLs as 23 characters flat' do text = ('a' * 476) + " http://#{'b' * 30}.com/example" status = double(spoiler_text: '', text: text, errors: double(add: nil), local?: true, reblog?: false) subject.validate(status) expect(status.errors).to_not have_received(:add) end it 'counts only the front part of remote usernames' do text = ('a' * 475) + " @alice@#{'b' * 30}.com" status = double(spoiler_text: '', text: text, errors: double(add: nil), local?: true, reblog?: false) subject.validate(status) expect(status.errors).to_not have_received(:add) end end end ================================================ FILE: spec/validators/status_pin_validator_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' RSpec.describe StatusPinValidator, type: :validator do describe '#validate' do before do subject.validate(pin) end let(:pin) { double(account: account, errors: errors, status: status, account_id: pin_account_id) } let(:status) { double(reblog?: reblog, account_id: status_account_id, visibility: visibility) } let(:account) { double(status_pins: status_pins, local?: local) } let(:status_pins) { double(count: count) } let(:errors) { double(add: nil) } let(:pin_account_id) { 1 } let(:status_account_id) { 1 } let(:visibility) { 'public' } let(:local) { false } let(:reblog) { false } let(:count) { 0 } context 'pin.status.reblog?' do let(:reblog) { true } it 'calls errors.add' do expect(errors).to have_received(:add).with(:base, I18n.t('statuses.pin_errors.reblog')) end end context 'pin.account_id != pin.status.account_id' do let(:pin_account_id) { 1 } let(:status_account_id) { 2 } it 'calls errors.add' do expect(errors).to have_received(:add).with(:base, I18n.t('statuses.pin_errors.ownership')) end end context 'unless %w(public unlisted).include?(pin.status.visibility)' do let(:visibility) { '' } it 'calls errors.add' do expect(errors).to have_received(:add).with(:base, I18n.t('statuses.pin_errors.private')) end end context 'pin.account.status_pins.count > 4 && pin.account.local?' do let(:count) { 5 } let(:local) { true } it 'calls errors.add' do expect(errors).to have_received(:add).with(:base, I18n.t('statuses.pin_errors.limit')) end end end end ================================================ FILE: spec/validators/unique_username_validator_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe UniqueUsernameValidator do describe '#validate' do it 'does not add errors if username is nil' do account = double(username: nil, persisted?: false, errors: double(add: nil)) subject.validate(account) expect(account.errors).to_not have_received(:add) end it 'does not add errors when existing one is subject itself' do account = Fabricate(:account, username: 'abcdef') expect(account).to be_valid end it 'adds an error when the username is already used with ignoring cases' do Fabricate(:account, username: 'ABCdef') account = double(username: 'abcDEF', persisted?: false, errors: double(add: nil)) subject.validate(account) expect(account.errors).to have_received(:add) end end end ================================================ FILE: spec/validators/unreserved_username_validator_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' RSpec.describe UnreservedUsernameValidator, type: :validator do describe '#validate' do before do allow(validator).to receive(:reserved_username?) { reserved_username } validator.validate(account) end let(:validator) { described_class.new } let(:account) { double(username: username, errors: errors) } let(:errors ) { double(add: nil) } context '@username.nil?' do let(:username) { nil } it 'not calls errors.add' do expect(errors).not_to have_received(:add).with(:username, any_args) end end context '!@username.nil?' do let(:username) { '' } context 'reserved_username?' do let(:reserved_username) { true } it 'calls erros.add' do expect(errors).to have_received(:add).with(:username, I18n.t('accounts.reserved_username')) end end context '!reserved_username?' do let(:reserved_username) { false } it 'not calls erros.add' do expect(errors).not_to have_received(:add).with(:username, any_args) end end end end end ================================================ FILE: spec/validators/url_validator_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' RSpec.describe UrlValidator, type: :validator do describe '#validate_each' do before do allow(validator).to receive(:compliant?).with(value) { compliant } validator.validate_each(record, attribute, value) end let(:validator) { described_class.new(attributes: [attribute]) } let(:record) { double(errors: errors) } let(:errors) { double(add: nil) } let(:value) { '' } let(:attribute) { :foo } context 'unless compliant?' do let(:compliant) { false } it 'calls errors.add' do expect(errors).to have_received(:add).with(attribute, I18n.t('applications.invalid_url')) end end context 'if compliant?' do let(:compliant) { true } it 'not calls errors.add' do expect(errors).not_to have_received(:add).with(attribute, any_args) end end end end ================================================ FILE: spec/views/about/show.html.haml_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe 'about/show.html.haml', without_verify_partial_doubles: true do before do allow(view).to receive(:site_hostname).and_return('example.com') allow(view).to receive(:site_title).and_return('example site') allow(view).to receive(:new_user).and_return(User.new) allow(view).to receive(:use_seamless_external_login?).and_return(false) end it 'has valid open graph tags' do instance_presenter = double( :instance_presenter, site_title: 'something', site_short_description: 'something', site_description: 'something', version_number: '1.0', source_url: 'https://github.com/tootsuite/mastodon', open_registrations: false, thumbnail: nil, hero: nil, mascot: nil, user_count: 420, status_count: 69, active_user_count: 420, contact_account: nil, sample_accounts: [] ) assign(:instance_presenter, instance_presenter) render header_tags = view.content_for(:header_tags) expect(header_tags).to match(%r{}) expect(header_tags).to match(%r{}) expect(header_tags).to match(%r{}) expect(header_tags).to match(%r{}) end end ================================================ FILE: spec/views/stream_entries/show.html.haml_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe 'stream_entries/show.html.haml', without_verify_partial_doubles: true do before do double(:api_oembed_url => '') double(:account_stream_entry_url => '') allow(view).to receive(:show_landing_strip?).and_return(true) allow(view).to receive(:site_title).and_return('example site') allow(view).to receive(:site_hostname).and_return('example.com') allow(view).to receive(:full_asset_url).and_return('//asset.host/image.svg') allow(view).to receive(:local_time) allow(view).to receive(:local_time_ago) allow(view).to receive(:current_account).and_return(nil) assign(:instance_presenter, InstancePresenter.new) end it 'has valid author h-card and basic data for a detailed_status' do alice = Fabricate(:account, username: 'alice', display_name: 'Alice') bob = Fabricate(:account, username: 'bob', display_name: 'Bob') status = Fabricate(:status, account: alice, text: 'Hello World') reply = Fabricate(:status, account: bob, thread: status, text: 'Hello Alice') assign(:status, status) assign(:stream_entry, status.stream_entry) assign(:account, alice) assign(:type, status.stream_entry.activity_type.downcase) assign(:descendant_threads, []) render mf2 = Microformats.parse(rendered) expect(mf2.entry.url.to_s).not_to be_empty expect(mf2.entry.author.name.to_s).to eq alice.display_name expect(mf2.entry.author.url.to_s).not_to be_empty end it 'has valid h-cites for p-in-reply-to and p-comment' do alice = Fabricate(:account, username: 'alice', display_name: 'Alice') bob = Fabricate(:account, username: 'bob', display_name: 'Bob') carl = Fabricate(:account, username: 'carl', display_name: 'Carl') status = Fabricate(:status, account: alice, text: 'Hello World') reply = Fabricate(:status, account: bob, thread: status, text: 'Hello Alice') comment = Fabricate(:status, account: carl, thread: reply, text: 'Hello Bob') assign(:status, reply) assign(:stream_entry, reply.stream_entry) assign(:account, alice) assign(:type, reply.stream_entry.activity_type.downcase) assign(:ancestors, reply.stream_entry.activity.ancestors(1, bob)) assign(:descendant_threads, [{ statuses: reply.stream_entry.activity.descendants(1) }]) render mf2 = Microformats.parse(rendered) expect(mf2.entry.url.to_s).not_to be_empty expect(mf2.entry.comment.url.to_s).not_to be_empty expect(mf2.entry.comment.author.name.to_s).to eq carl.display_name expect(mf2.entry.comment.author.url.to_s).not_to be_empty expect(mf2.entry.in_reply_to.url.to_s).not_to be_empty expect(mf2.entry.in_reply_to.author.name.to_s).to eq alice.display_name expect(mf2.entry.in_reply_to.author.url.to_s).not_to be_empty end it 'has valid opengraph tags' do alice = Fabricate(:account, username: 'alice', display_name: 'Alice') status = Fabricate(:status, account: alice, text: 'Hello World') assign(:status, status) assign(:stream_entry, status.stream_entry) assign(:account, alice) assign(:type, status.stream_entry.activity_type.downcase) assign(:descendant_threads, []) render header_tags = view.content_for(:header_tags) expect(header_tags).to match(%r{}) expect(header_tags).to match(%r{}) expect(header_tags).to match(%r{}) expect(header_tags).to match(%r{}) end end ================================================ FILE: spec/workers/activitypub/delivery_worker_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe ActivityPub::DeliveryWorker do subject { described_class.new } let(:sender) { Fabricate(:account) } let(:payload) { 'test' } describe 'perform' do it 'performs a request' do stub_request(:post, 'https://example.com/api').to_return(status: 200) subject.perform(payload, sender.id, 'https://example.com/api') expect(a_request(:post, 'https://example.com/api')).to have_been_made.once end it 'raises when request fails' do stub_request(:post, 'https://example.com/api').to_return(status: 500) expect { subject.perform(payload, sender.id, 'https://example.com/api') }.to raise_error Mastodon::UnexpectedResponseError end end end ================================================ FILE: spec/workers/activitypub/distribution_worker_spec.rb ================================================ require 'rails_helper' describe ActivityPub::DistributionWorker do subject { described_class.new } let(:status) { Fabricate(:status) } let(:follower) { Fabricate(:account, protocol: :activitypub, inbox_url: 'http://example.com') } describe '#perform' do before do allow(ActivityPub::DeliveryWorker).to receive(:push_bulk) follower.follow!(status.account) end context 'with public status' do before do status.update(visibility: :public) end it 'delivers to followers' do subject.perform(status.id) expect(ActivityPub::DeliveryWorker).to have_received(:push_bulk).with(['http://example.com']) end end context 'with private status' do before do status.update(visibility: :private) end it 'delivers to followers' do subject.perform(status.id) expect(ActivityPub::DeliveryWorker).to have_received(:push_bulk).with(['http://example.com']) end end context 'with direct status' do before do status.update(visibility: :direct) end it 'does nothing' do subject.perform(status.id) expect(ActivityPub::DeliveryWorker).to_not have_received(:push_bulk) end end end end ================================================ FILE: spec/workers/activitypub/fetch_replies_worker_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe ActivityPub::FetchRepliesWorker do subject { described_class.new } let(:account) { Fabricate(:account, uri: 'https://example.com/user/1') } let(:status) { Fabricate(:status, account: account) } let(:payload) do { '@context': 'https://www.w3.org/ns/activitystreams', id: 'https://example.com/statuses_replies/1', type: 'Collection', items: [], } end let(:json) { Oj.dump(payload) } describe 'perform' do it 'performs a request if the collection URI is from the same host' do stub_request(:get, 'https://example.com/statuses_replies/1').to_return(status: 200, body: json) subject.perform(status.id, 'https://example.com/statuses_replies/1') expect(a_request(:get, 'https://example.com/statuses_replies/1')).to have_been_made.once end it 'does not perform a request if the collection URI is from a different host' do stub_request(:get, 'https://other.com/statuses_replies/1').to_return(status: 200) subject.perform(status.id, 'https://other.com/statuses_replies/1') expect(a_request(:get, 'https://other.com/statuses_replies/1')).to_not have_been_made end it 'raises when request fails' do stub_request(:get, 'https://example.com/statuses_replies/1').to_return(status: 500) expect { subject.perform(status.id, 'https://example.com/statuses_replies/1') }.to raise_error Mastodon::UnexpectedResponseError end end end ================================================ FILE: spec/workers/activitypub/processing_worker_spec.rb ================================================ require 'rails_helper' describe ActivityPub::ProcessingWorker do subject { described_class.new } let(:account) { Fabricate(:account) } describe '#perform' do it 'delegates to ActivityPub::ProcessCollectionService' do allow(ActivityPub::ProcessCollectionService).to receive(:new).and_return(double(:service, call: nil)) subject.perform(account.id, '') expect(ActivityPub::ProcessCollectionService).to have_received(:new) end end end ================================================ FILE: spec/workers/activitypub/update_distribution_worker_spec.rb ================================================ require 'rails_helper' describe ActivityPub::UpdateDistributionWorker do subject { described_class.new } let(:account) { Fabricate(:account) } let(:follower) { Fabricate(:account, protocol: :activitypub, inbox_url: 'http://example.com') } describe '#perform' do before do allow(ActivityPub::DeliveryWorker).to receive(:push_bulk) follower.follow!(account) end it 'delivers to followers' do subject.perform(account.id) expect(ActivityPub::DeliveryWorker).to have_received(:push_bulk).with(['http://example.com']) end end end ================================================ FILE: spec/workers/after_remote_follow_request_worker_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe AfterRemoteFollowRequestWorker do subject { described_class.new } let(:follow_request) { Fabricate(:follow_request) } describe 'perform' do context 'when the follow_request does not exist' do it 'catches a raise and returns true' do allow(FollowService).to receive(:new) result = subject.perform('aaa') expect(result).to eq(true) expect(FollowService).not_to have_received(:new) end end context 'when the account cannot be updated' do it 'returns nil and does not call service when account is nil' do allow(FollowService).to receive(:new) service = double(call: nil) allow(FetchRemoteAccountService).to receive(:new).and_return(service) result = subject.perform(follow_request.id) expect(result).to be_nil expect(FollowService).not_to have_received(:new) end it 'returns nil and does not call service when account is locked' do allow(FollowService).to receive(:new) service = double(call: double(locked?: true)) allow(FetchRemoteAccountService).to receive(:new).and_return(service) result = subject.perform(follow_request.id) expect(result).to be_nil expect(FollowService).not_to have_received(:new) end end context 'when the account is updated' do it 'calls the follow service and destroys the follow' do follow_service = double(call: nil) allow(FollowService).to receive(:new).and_return(follow_service) account = Fabricate(:account, locked: false) service = double(call: account) allow(FetchRemoteAccountService).to receive(:new).and_return(service) result = subject.perform(follow_request.id) expect(result).to be_nil expect(follow_service).to have_received(:call).with(follow_request.account, account.acct) expect { follow_request.reload }.to raise_error(ActiveRecord::RecordNotFound) end end end end ================================================ FILE: spec/workers/after_remote_follow_worker_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe AfterRemoteFollowWorker do subject { described_class.new } let(:follow) { Fabricate(:follow) } describe 'perform' do context 'when the follow does not exist' do it 'catches a raise and returns true' do allow(FollowService).to receive(:new) result = subject.perform('aaa') expect(result).to eq(true) expect(FollowService).not_to have_received(:new) end end context 'when the account cannot be updated' do it 'returns nil and does not call service when account is nil' do allow(FollowService).to receive(:new) service = double(call: nil) allow(FetchRemoteAccountService).to receive(:new).and_return(service) result = subject.perform(follow.id) expect(result).to be_nil expect(FollowService).not_to have_received(:new) end it 'returns nil and does not call service when account is not locked' do allow(FollowService).to receive(:new) service = double(call: double(locked?: false)) allow(FetchRemoteAccountService).to receive(:new).and_return(service) result = subject.perform(follow.id) expect(result).to be_nil expect(FollowService).not_to have_received(:new) end end context 'when the account is updated' do it 'calls the follow service and destroys the follow' do follow_service = double(call: nil) allow(FollowService).to receive(:new).and_return(follow_service) account = Fabricate(:account, locked: true) service = double(call: account) allow(FetchRemoteAccountService).to receive(:new).and_return(service) result = subject.perform(follow.id) expect(result).to be_nil expect(follow_service).to have_received(:call).with(follow.account, account.acct) expect { follow.reload }.to raise_error(ActiveRecord::RecordNotFound) end end end end ================================================ FILE: spec/workers/digest_mailer_worker_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe DigestMailerWorker do describe 'perform' do let(:user) { Fabricate(:user, last_emailed_at: 3.days.ago) } context 'for a user who receives digests' do it 'sends the email' do service = double(deliver_now!: nil) allow(NotificationMailer).to receive(:digest).and_return(service) update_user_digest_setting(true) described_class.perform_async(user.id) expect(NotificationMailer).to have_received(:digest) expect(user.reload.last_emailed_at).to be_within(1).of(Time.now.utc) end end context 'for a user who does not receive digests' do it 'does not send the email' do allow(NotificationMailer).to receive(:digest) update_user_digest_setting(false) described_class.perform_async(user.id) expect(NotificationMailer).not_to have_received(:digest) expect(user.last_emailed_at).to be_within(1).of(3.days.ago) end end def update_user_digest_setting(value) user.settings['notification_emails'] = user.settings['notification_emails'].merge('digest' => value) end end end ================================================ FILE: spec/workers/domain_block_worker_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe DomainBlockWorker do subject { described_class.new } describe 'perform' do let(:domain_block) { Fabricate(:domain_block) } it 'returns true for non-existent domain block' do service = double(call: nil) allow(BlockDomainService).to receive(:new).and_return(service) result = subject.perform(domain_block.id) expect(result).to be_nil expect(service).to have_received(:call).with(domain_block) end it 'calls domain block service for relevant domain block' do result = subject.perform('aaa') expect(result).to eq(true) end end end ================================================ FILE: spec/workers/feed_insert_worker_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe FeedInsertWorker do subject { described_class.new } describe 'perform' do let(:follower) { Fabricate(:account) } let(:status) { Fabricate(:status) } context 'when there are no records' do it 'skips push with missing status' do instance = double(push_to_home: nil) allow(FeedManager).to receive(:instance).and_return(instance) result = subject.perform(nil, follower.id) expect(result).to eq true expect(instance).not_to have_received(:push_to_home) end it 'skips push with missing account' do instance = double(push_to_home: nil) allow(FeedManager).to receive(:instance).and_return(instance) result = subject.perform(status.id, nil) expect(result).to eq true expect(instance).not_to have_received(:push_to_home) end end context 'when there are real records' do it 'skips the push when there is a filter' do instance = double(push_to_home: nil, filter?: true) allow(FeedManager).to receive(:instance).and_return(instance) result = subject.perform(status.id, follower.id) expect(result).to be_nil expect(instance).not_to have_received(:push_to_home) end it 'pushes the status onto the home timeline without filter' do instance = double(push_to_home: nil, filter?: false) allow(FeedManager).to receive(:instance).and_return(instance) result = subject.perform(status.id, follower.id) expect(result).to be_nil expect(instance).to have_received(:push_to_home).with(follower, status) end end end end ================================================ FILE: spec/workers/publish_scheduled_status_worker_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe PublishScheduledStatusWorker do subject { described_class.new } let(:scheduled_status) { Fabricate(:scheduled_status, params: { text: 'Hello world, future!' }) } describe 'perform' do before do subject.perform(scheduled_status.id) end it 'creates a status' do expect(scheduled_status.account.statuses.first.text).to eq 'Hello world, future!' end it 'removes the scheduled status' do expect(ScheduledStatus.find_by(id: scheduled_status.id)).to be_nil end end end ================================================ FILE: spec/workers/pubsubhubbub/confirmation_worker_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe Pubsubhubbub::ConfirmationWorker do include RoutingHelper subject { described_class.new } let!(:alice) { Fabricate(:account, username: 'alice') } let!(:subscription) { Fabricate(:subscription, account: alice, callback_url: 'http://example.com/api', confirmed: false, expires_at: 3.days.from_now, secret: nil) } describe 'perform' do describe 'with subscribe mode' do it 'confirms and updates subscription when challenge matches' do stub_random_value stub_request(:get, url_for_mode('subscribe')) .with(headers: http_headers) .to_return(status: 200, body: challenge_value, headers: {}) seconds = 10.days.seconds.to_i subject.perform(subscription.id, 'subscribe', 'asdf', seconds) subscription.reload expect(subscription.secret).to eq 'asdf' expect(subscription.confirmed).to eq true expect(subscription.expires_at).to be_within(5).of(10.days.from_now) end it 'does not update subscription when challenge does not match' do stub_random_value stub_request(:get, url_for_mode('subscribe')) .with(headers: http_headers) .to_return(status: 200, body: 'wrong value', headers: {}) seconds = 10.days.seconds.to_i subject.perform(subscription.id, 'subscribe', 'asdf', seconds) subscription.reload expect(subscription.secret).to be_blank expect(subscription.confirmed).to eq false expect(subscription.expires_at).to be_within(5).of(3.days.from_now) end end describe 'with unsubscribe mode' do it 'confirms and destroys subscription when challenge matches' do stub_random_value stub_request(:get, url_for_mode('unsubscribe')) .with(headers: http_headers) .to_return(status: 200, body: challenge_value, headers: {}) seconds = 10.days.seconds.to_i subject.perform(subscription.id, 'unsubscribe', 'asdf', seconds) expect { subscription.reload }.to raise_error(ActiveRecord::RecordNotFound) end it 'does not destroy subscription when challenge does not match' do stub_random_value stub_request(:get, url_for_mode('unsubscribe')) .with(headers: http_headers) .to_return(status: 200, body: 'wrong value', headers: {}) seconds = 10.days.seconds.to_i subject.perform(subscription.id, 'unsubscribe', 'asdf', seconds) expect { subscription.reload }.not_to raise_error end end end def url_for_mode(mode) "http://example.com/api?hub.challenge=#{challenge_value}&hub.lease_seconds=863999&hub.mode=#{mode}&hub.topic=https://#{Rails.configuration.x.local_domain}/users/alice.atom" end def stub_random_value allow(SecureRandom).to receive(:hex).and_return(challenge_value) end def challenge_value '1a2s3d4f' end def http_headers { 'Connection' => 'close', 'Host' => 'example.com' } end end ================================================ FILE: spec/workers/pubsubhubbub/delivery_worker_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe Pubsubhubbub::DeliveryWorker do include RoutingHelper subject { described_class.new } let(:payload) { 'test' } describe 'perform' do it 'raises when subscription does not exist' do expect { subject.perform 123, payload }.to raise_error(ActiveRecord::RecordNotFound) end it 'does not attempt to deliver when domain blocked' do _domain_block = Fabricate(:domain_block, domain: 'example.com', severity: :suspend) subscription = Fabricate(:subscription, callback_url: 'https://example.com/api', last_successful_delivery_at: 2.days.ago) subject.perform(subscription.id, payload) expect(subscription.reload.last_successful_delivery_at).to be_within(2).of(2.days.ago) end it 'raises when request fails' do subscription = Fabricate(:subscription) stub_request_to_respond_with(subscription, 500) expect { subject.perform(subscription.id, payload) }.to raise_error Mastodon::UnexpectedResponseError end it 'updates subscriptions when delivery succeeds' do subscription = Fabricate(:subscription) stub_request_to_respond_with(subscription, 200) subject.perform(subscription.id, payload) expect(subscription.reload.last_successful_delivery_at).to be_within(2).of(Time.now.utc) end it 'updates subscription without a secret when delivery succeeds' do subscription = Fabricate(:subscription, secret: nil) stub_request_to_respond_with(subscription, 200) subject.perform(subscription.id, payload) expect(subscription.reload.last_successful_delivery_at).to be_within(2).of(Time.now.utc) end def stub_request_to_respond_with(subscription, code) stub_request(:post, 'http://example.com/callback') .with(body: payload, headers: expected_headers(subscription)) .to_return(status: code, body: '', headers: {}) end def expected_headers(subscription) { 'Connection' => 'close', 'Content-Type' => 'application/atom+xml', 'Host' => 'example.com', 'Link' => "; rel=\"hub\", ; rel=\"self\"", }.tap do |basic| known_digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha1'), subscription.secret.to_s, payload) basic.merge('X-Hub-Signature' => "sha1=#{known_digest}") if subscription.secret? end end end end ================================================ FILE: spec/workers/pubsubhubbub/distribution_worker_spec.rb ================================================ require 'rails_helper' describe Pubsubhubbub::DistributionWorker do subject { Pubsubhubbub::DistributionWorker.new } let!(:alice) { Fabricate(:account, username: 'alice') } let!(:bob) { Fabricate(:account, username: 'bob', domain: 'example2.com') } let!(:anonymous_subscription) { Fabricate(:subscription, account: alice, callback_url: 'http://example1.com', confirmed: true, lease_seconds: 3600) } let!(:subscription_with_follower) { Fabricate(:subscription, account: alice, callback_url: 'http://example2.com', confirmed: true, lease_seconds: 3600) } before do bob.follow!(alice) end describe 'with public status' do let(:status) { Fabricate(:status, account: alice, text: 'Hello', visibility: :public) } it 'delivers payload to all subscriptions' do allow(Pubsubhubbub::DeliveryWorker).to receive(:push_bulk) subject.perform(status.stream_entry.id) expect(Pubsubhubbub::DeliveryWorker).to have_received(:push_bulk).with([anonymous_subscription.id, subscription_with_follower.id]) end end context 'when OStatus privacy is not used' do describe 'with private status' do let(:status) { Fabricate(:status, account: alice, text: 'Hello', visibility: :private) } it 'does not deliver anything' do allow(Pubsubhubbub::DeliveryWorker).to receive(:push_bulk) subject.perform(status.stream_entry.id) expect(Pubsubhubbub::DeliveryWorker).to_not have_received(:push_bulk) end end describe 'with direct status' do let(:status) { Fabricate(:status, account: alice, text: 'Hello', visibility: :direct) } it 'does not deliver payload' do allow(Pubsubhubbub::DeliveryWorker).to receive(:push_bulk) subject.perform(status.stream_entry.id) expect(Pubsubhubbub::DeliveryWorker).to_not have_received(:push_bulk) end end end end ================================================ FILE: spec/workers/regeneration_worker_spec.rb ================================================ # frozen_string_literal: true require 'rails_helper' describe RegenerationWorker do subject { described_class.new } describe 'perform' do let(:account) { Fabricate(:account) } it 'calls the precompute feed service for the account' do service = double(call: nil) allow(PrecomputeFeedService).to receive(:new).and_return(service) result = subject.perform(account.id) expect(result).to be_nil expect(service).to have_received(:call).with(account) end it 'fails when account does not exist' do result = subject.perform('aaa') expect(result).to eq(true) end end end ================================================ FILE: spec/workers/scheduler/feed_cleanup_scheduler_spec.rb ================================================ require 'rails_helper' describe Scheduler::FeedCleanupScheduler do subject { described_class.new } let!(:active_user) { Fabricate(:user, current_sign_in_at: 2.days.ago) } let!(:inactive_user) { Fabricate(:user, current_sign_in_at: 22.days.ago) } it 'clears feeds of inactives' do Redis.current.zadd(feed_key_for(inactive_user), 1, 1) Redis.current.zadd(feed_key_for(active_user), 1, 1) Redis.current.zadd(feed_key_for(inactive_user, 'reblogs'), 2, 2) Redis.current.sadd(feed_key_for(inactive_user, 'reblogs:2'), 3) subject.perform expect(Redis.current.zcard(feed_key_for(inactive_user))).to eq 0 expect(Redis.current.zcard(feed_key_for(active_user))).to eq 1 expect(Redis.current.exists(feed_key_for(inactive_user, 'reblogs'))).to be false expect(Redis.current.exists(feed_key_for(inactive_user, 'reblogs:2'))).to be false end def feed_key_for(user, subtype = nil) FeedManager.instance.key(:home, user.account_id, subtype) end end ================================================ FILE: spec/workers/scheduler/media_cleanup_scheduler_spec.rb ================================================ require 'rails_helper' describe Scheduler::MediaCleanupScheduler do subject { described_class.new } let!(:old_media) { Fabricate(:media_attachment, account_id: nil, created_at: 10.days.ago) } let!(:new_media) { Fabricate(:media_attachment, account_id: nil, created_at: 1.hour.ago) } it 'removes old media records' do subject.perform expect { old_media.reload }.to raise_error(ActiveRecord::RecordNotFound) expect(new_media.reload).to be_persisted end end ================================================ FILE: spec/workers/scheduler/subscriptions_scheduler_spec.rb ================================================ require 'rails_helper' describe Scheduler::SubscriptionsScheduler do subject { Scheduler::SubscriptionsScheduler.new } let!(:expiring_account1) { Fabricate(:account, subscription_expires_at: 20.minutes.from_now, domain: 'example.com', followers_count: 1, hub_url: 'http://hub.example.com') } let!(:expiring_account2) { Fabricate(:account, subscription_expires_at: 4.hours.from_now, domain: 'example.org', followers_count: 1, hub_url: 'http://hub.example.org') } before do stub_request(:post, 'http://hub.example.com/').to_return(status: 202) stub_request(:post, 'http://hub.example.org/').to_return(status: 202) end it 're-subscribes for all expiring accounts' do subject.perform expect(a_request(:post, 'http://hub.example.com/')).to have_been_made.once expect(a_request(:post, 'http://hub.example.org/')).to have_been_made.once end end ================================================ FILE: streaming/index.js ================================================ const os = require('os'); const throng = require('throng'); const dotenv = require('dotenv'); const express = require('express'); const http = require('http'); const redis = require('redis'); const pg = require('pg'); const log = require('npmlog'); const url = require('url'); const { WebSocketServer } = require('@clusterws/cws'); const uuid = require('uuid'); const fs = require('fs'); const env = process.env.NODE_ENV || 'development'; dotenv.config({ path: env === 'production' ? '.env.production' : '.env', }); log.level = process.env.LOG_LEVEL || 'verbose'; const dbUrlToConfig = (dbUrl) => { if (!dbUrl) { return {}; } const params = url.parse(dbUrl, true); const config = {}; if (params.auth) { [config.user, config.password] = params.auth.split(':'); } if (params.hostname) { config.host = params.hostname; } if (params.port) { config.port = params.port; } if (params.pathname) { config.database = params.pathname.split('/')[1]; } const ssl = params.query && params.query.ssl; if (ssl && ssl === 'true' || ssl === '1') { config.ssl = true; } return config; }; const redisUrlToClient = (defaultConfig, redisUrl) => { const config = defaultConfig; if (!redisUrl) { return redis.createClient(config); } if (redisUrl.startsWith('unix://')) { return redis.createClient(redisUrl.slice(7), config); } return redis.createClient(Object.assign(config, { url: redisUrl, })); }; const numWorkers = +process.env.STREAMING_CLUSTER_NUM || (env === 'development' ? 1 : Math.max(os.cpus().length - 1, 1)); const startMaster = () => { if (!process.env.SOCKET && process.env.PORT && isNaN(+process.env.PORT)) { log.warn('UNIX domain socket is now supported by using SOCKET. Please migrate from PORT hack.'); } log.info(`Starting streaming API server master with ${numWorkers} workers`); }; const startWorker = (workerId) => { log.info(`Starting worker ${workerId}`); const pgConfigs = { development: { user: process.env.DB_USER || pg.defaults.user, password: process.env.DB_PASS || pg.defaults.password, database: process.env.DB_NAME || 'mastodon_development', host: process.env.DB_HOST || pg.defaults.host, port: process.env.DB_PORT || pg.defaults.port, max: 10, }, production: { user: process.env.DB_USER || 'mastodon', password: process.env.DB_PASS || '', database: process.env.DB_NAME || 'mastodon_production', host: process.env.DB_HOST || 'localhost', port: process.env.DB_PORT || 5432, max: 10, }, }; if (!!process.env.DB_SSLMODE && process.env.DB_SSLMODE !== 'disable') { pgConfigs.development.ssl = true; pgConfigs.production.ssl = true; } const app = express(); app.set('trusted proxy', process.env.TRUSTED_PROXY_IP || 'loopback,uniquelocal'); const pgPool = new pg.Pool(Object.assign(pgConfigs[env], dbUrlToConfig(process.env.DATABASE_URL))); const server = http.createServer(app); const redisNamespace = process.env.REDIS_NAMESPACE || null; const redisParams = { host: process.env.REDIS_HOST || '127.0.0.1', port: process.env.REDIS_PORT || 6379, db: process.env.REDIS_DB || 0, password: process.env.REDIS_PASSWORD, }; if (redisNamespace) { redisParams.namespace = redisNamespace; } const redisPrefix = redisNamespace ? `${redisNamespace}:` : ''; const redisSubscribeClient = redisUrlToClient(redisParams, process.env.REDIS_URL); const redisClient = redisUrlToClient(redisParams, process.env.REDIS_URL); const subs = {}; redisSubscribeClient.on('message', (channel, message) => { const callbacks = subs[channel]; log.silly(`New message on channel ${channel}`); if (!callbacks) { return; } callbacks.forEach(callback => callback(message)); }); const subscriptionHeartbeat = (channel) => { const interval = 6*60; const tellSubscribed = () => { redisClient.set(`${redisPrefix}subscribed:${channel}`, '1', 'EX', interval*3); }; tellSubscribed(); const heartbeat = setInterval(tellSubscribed, interval*1000); return () => { clearInterval(heartbeat); }; }; const subscribe = (channel, callback) => { log.silly(`Adding listener for ${channel}`); subs[channel] = subs[channel] || []; if (subs[channel].length === 0) { log.verbose(`Subscribe ${channel}`); redisSubscribeClient.subscribe(channel); } subs[channel].push(callback); }; const unsubscribe = (channel, callback) => { log.silly(`Removing listener for ${channel}`); subs[channel] = subs[channel].filter(item => item !== callback); if (subs[channel].length === 0) { log.verbose(`Unsubscribe ${channel}`); redisSubscribeClient.unsubscribe(channel); } }; const allowCrossDomain = (req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Headers', 'Authorization, Accept, Cache-Control'); res.header('Access-Control-Allow-Methods', 'GET, OPTIONS'); next(); }; const setRequestId = (req, res, next) => { req.requestId = uuid.v4(); res.header('X-Request-Id', req.requestId); next(); }; const setRemoteAddress = (req, res, next) => { req.remoteAddress = req.connection.remoteAddress; next(); }; const accountFromToken = (token, allowedScopes, req, next) => { pgPool.connect((err, client, done) => { if (err) { next(err); return; } client.query('SELECT oauth_access_tokens.resource_owner_id, users.account_id, users.chosen_languages, oauth_access_tokens.scopes FROM oauth_access_tokens INNER JOIN users ON oauth_access_tokens.resource_owner_id = users.id WHERE oauth_access_tokens.token = $1 AND oauth_access_tokens.revoked_at IS NULL LIMIT 1', [token], (err, result) => { done(); if (err) { next(err); return; } if (result.rows.length === 0) { err = new Error('Invalid access token'); err.statusCode = 401; next(err); return; } const scopes = result.rows[0].scopes.split(' '); if (allowedScopes.size > 0 && !scopes.some(scope => allowedScopes.includes(scope))) { err = new Error('Access token does not cover required scopes'); err.statusCode = 401; next(err); return; } req.accountId = result.rows[0].account_id; req.chosenLanguages = result.rows[0].chosen_languages; req.allowNotifications = scopes.some(scope => ['read', 'read:notifications'].includes(scope)); next(); }); }); }; const accountFromRequest = (req, next, required = true, allowedScopes = ['read']) => { const authorization = req.headers.authorization; const location = url.parse(req.url, true); const accessToken = location.query.access_token || req.headers['sec-websocket-protocol']; if (!authorization && !accessToken) { if (required) { const err = new Error('Missing access token'); err.statusCode = 401; next(err); return; } else { next(); return; } } const token = authorization ? authorization.replace(/^Bearer /, '') : accessToken; accountFromToken(token, allowedScopes, req, next); }; const PUBLIC_STREAMS = [ 'public', 'public:media', 'public:local', 'public:local:media', 'hashtag', 'hashtag:local', ]; const wsVerifyClient = (info, cb) => { const location = url.parse(info.req.url, true); const authRequired = !PUBLIC_STREAMS.some(stream => stream === location.query.stream); const allowedScopes = []; if (authRequired) { allowedScopes.push('read'); if (location.query.stream === 'user:notification') { allowedScopes.push('read:notifications'); } else { allowedScopes.push('read:statuses'); } } accountFromRequest(info.req, err => { if (!err) { cb(true, undefined, undefined); } else { log.error(info.req.requestId, err.toString()); cb(false, 401, 'Unauthorized'); } }, authRequired, allowedScopes); }; const PUBLIC_ENDPOINTS = [ '/api/v1/streaming/public', '/api/v1/streaming/public/local', '/api/v1/streaming/hashtag', '/api/v1/streaming/hashtag/local', ]; const authenticationMiddleware = (req, res, next) => { if (req.method === 'OPTIONS') { next(); return; } const authRequired = !PUBLIC_ENDPOINTS.some(endpoint => endpoint === req.path); const allowedScopes = []; if (authRequired) { allowedScopes.push('read'); if (req.path === '/api/v1/streaming/user/notification') { allowedScopes.push('read:notifications'); } else { allowedScopes.push('read:statuses'); } } accountFromRequest(req, next, authRequired, allowedScopes); }; const errorMiddleware = (err, req, res, {}) => { log.error(req.requestId, err.toString()); res.writeHead(err.statusCode || 500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: err.statusCode ? err.toString() : 'An unexpected error occurred' })); }; const placeholders = (arr, shift = 0) => arr.map((_, i) => `$${i + 1 + shift}`).join(', '); const authorizeListAccess = (id, req, next) => { pgPool.connect((err, client, done) => { if (err) { next(false); return; } client.query('SELECT id, account_id FROM lists WHERE id = $1 LIMIT 1', [id], (err, result) => { done(); if (err || result.rows.length === 0 || result.rows[0].account_id !== req.accountId) { next(false); return; } next(true); }); }); }; const streamFrom = (id, req, output, attachCloseHandler, needsFiltering = false, notificationOnly = false) => { const accountId = req.accountId || req.remoteAddress; const streamType = notificationOnly ? ' (notification)' : ''; log.verbose(req.requestId, `Starting stream from ${id} for ${accountId}${streamType}`); const listener = message => { const { event, payload, queued_at } = JSON.parse(message); const transmit = () => { const now = new Date().getTime(); const delta = now - queued_at; const encodedPayload = typeof payload === 'object' ? JSON.stringify(payload) : payload; log.silly(req.requestId, `Transmitting for ${accountId}: ${event} ${encodedPayload} Delay: ${delta}ms`); output(event, encodedPayload); }; if (notificationOnly && event !== 'notification') { return; } if (event === 'notification' && !req.allowNotifications) { return; } // Only messages that may require filtering are statuses, since notifications // are already personalized and deletes do not matter if (!needsFiltering || event !== 'update') { transmit(); return; } const unpackedPayload = payload; const targetAccountIds = [unpackedPayload.account.id].concat(unpackedPayload.mentions.map(item => item.id)); const accountDomain = unpackedPayload.account.acct.split('@')[1]; if (Array.isArray(req.chosenLanguages) && unpackedPayload.language !== null && req.chosenLanguages.indexOf(unpackedPayload.language) === -1) { log.silly(req.requestId, `Message ${unpackedPayload.id} filtered by language (${unpackedPayload.language})`); return; } // When the account is not logged in, it is not necessary to confirm the block or mute if (!req.accountId) { transmit(); return; } pgPool.connect((err, client, done) => { if (err) { log.error(err); return; } const queries = [ client.query(`SELECT 1 FROM blocks WHERE (account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 2)})) OR (account_id = $2 AND target_account_id = $1) UNION SELECT 1 FROM mutes WHERE account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 2)})`, [req.accountId, unpackedPayload.account.id].concat(targetAccountIds)), ]; if (accountDomain) { queries.push(client.query('SELECT 1 FROM account_domain_blocks WHERE account_id = $1 AND domain = $2', [req.accountId, accountDomain])); } Promise.all(queries).then(values => { done(); if (values[0].rows.length > 0 || (values.length > 1 && values[1].rows.length > 0)) { return; } transmit(); }).catch(err => { done(); log.error(err); }); }); }; subscribe(`${redisPrefix}${id}`, listener); attachCloseHandler(`${redisPrefix}${id}`, listener); }; // Setup stream output to HTTP const streamToHttp = (req, res) => { const accountId = req.accountId || req.remoteAddress; res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Transfer-Encoding', 'chunked'); const heartbeat = setInterval(() => res.write(':thump\n'), 15000); req.on('close', () => { log.verbose(req.requestId, `Ending stream for ${accountId}`); clearInterval(heartbeat); }); return (event, payload) => { res.write(`event: ${event}\n`); res.write(`data: ${payload}\n\n`); }; }; // Setup stream end for HTTP const streamHttpEnd = (req, closeHandler = false) => (id, listener) => { req.on('close', () => { unsubscribe(id, listener); if (closeHandler) { closeHandler(); } }); }; // Setup stream output to WebSockets const streamToWs = (req, ws) => (event, payload) => { if (ws.readyState !== ws.OPEN) { log.error(req.requestId, 'Tried writing to closed socket'); return; } ws.send(JSON.stringify({ event, payload })); }; // Setup stream end for WebSockets const streamWsEnd = (req, ws, closeHandler = false) => (id, listener) => { const accountId = req.accountId || req.remoteAddress; ws.on('close', () => { log.verbose(req.requestId, `Ending stream for ${accountId}`); unsubscribe(id, listener); if (closeHandler) { closeHandler(); } }); ws.on('error', () => { log.verbose(req.requestId, `Ending stream for ${accountId}`); unsubscribe(id, listener); if (closeHandler) { closeHandler(); } }); }; const httpNotFound = res => { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Not found' })); }; app.use(setRequestId); app.use(setRemoteAddress); app.use(allowCrossDomain); app.get('/api/v1/streaming/health', (req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('OK'); }); app.use(authenticationMiddleware); app.use(errorMiddleware); app.get('/api/v1/streaming/user', (req, res) => { const channel = `timeline:${req.accountId}`; streamFrom(channel, req, streamToHttp(req, res), streamHttpEnd(req, subscriptionHeartbeat(channel))); }); app.get('/api/v1/streaming/user/notification', (req, res) => { streamFrom(`timeline:${req.accountId}`, req, streamToHttp(req, res), streamHttpEnd(req), false, true); }); app.get('/api/v1/streaming/public', (req, res) => { const onlyMedia = req.query.only_media === '1' || req.query.only_media === 'true'; const channel = onlyMedia ? 'timeline:public:media' : 'timeline:public'; streamFrom(channel, req, streamToHttp(req, res), streamHttpEnd(req), true); }); app.get('/api/v1/streaming/public/local', (req, res) => { const onlyMedia = req.query.only_media === '1' || req.query.only_media === 'true'; const channel = onlyMedia ? 'timeline:public:local:media' : 'timeline:public:local'; streamFrom(channel, req, streamToHttp(req, res), streamHttpEnd(req), true); }); app.get('/api/v1/streaming/direct', (req, res) => { const channel = `timeline:direct:${req.accountId}`; streamFrom(channel, req, streamToHttp(req, res), streamHttpEnd(req, subscriptionHeartbeat(channel)), true); }); app.get('/api/v1/streaming/hashtag', (req, res) => { const { tag } = req.query; if (!tag || tag.length === 0) { httpNotFound(res); return; } streamFrom(`timeline:hashtag:${tag.toLowerCase()}`, req, streamToHttp(req, res), streamHttpEnd(req), true); }); app.get('/api/v1/streaming/hashtag/local', (req, res) => { const { tag } = req.query; if (!tag || tag.length === 0) { httpNotFound(res); return; } streamFrom(`timeline:hashtag:${tag.toLowerCase()}:local`, req, streamToHttp(req, res), streamHttpEnd(req), true); }); app.get('/api/v1/streaming/list', (req, res) => { const listId = req.query.list; authorizeListAccess(listId, req, authorized => { if (!authorized) { httpNotFound(res); return; } const channel = `timeline:list:${listId}`; streamFrom(channel, req, streamToHttp(req, res), streamHttpEnd(req, subscriptionHeartbeat(channel))); }); }); const wss = new WebSocketServer({ server, verifyClient: wsVerifyClient }); wss.on('connection', (ws, req) => { const location = url.parse(req.url, true); req.requestId = uuid.v4(); req.remoteAddress = ws._socket.remoteAddress; let channel; switch(location.query.stream) { case 'user': channel = `timeline:${req.accountId}`; streamFrom(channel, req, streamToWs(req, ws), streamWsEnd(req, ws, subscriptionHeartbeat(channel))); break; case 'user:notification': streamFrom(`timeline:${req.accountId}`, req, streamToWs(req, ws), streamWsEnd(req, ws), false, true); break; case 'public': streamFrom('timeline:public', req, streamToWs(req, ws), streamWsEnd(req, ws), true); break; case 'public:local': streamFrom('timeline:public:local', req, streamToWs(req, ws), streamWsEnd(req, ws), true); break; case 'public:media': streamFrom('timeline:public:media', req, streamToWs(req, ws), streamWsEnd(req, ws), true); break; case 'public:local:media': streamFrom('timeline:public:local:media', req, streamToWs(req, ws), streamWsEnd(req, ws), true); break; case 'direct': channel = `timeline:direct:${req.accountId}`; streamFrom(channel, req, streamToWs(req, ws), streamWsEnd(req, ws, subscriptionHeartbeat(channel)), true); break; case 'hashtag': if (!location.query.tag || location.query.tag.length === 0) { ws.close(); return; } streamFrom(`timeline:hashtag:${location.query.tag.toLowerCase()}`, req, streamToWs(req, ws), streamWsEnd(req, ws), true); break; case 'hashtag:local': if (!location.query.tag || location.query.tag.length === 0) { ws.close(); return; } streamFrom(`timeline:hashtag:${location.query.tag.toLowerCase()}:local`, req, streamToWs(req, ws), streamWsEnd(req, ws), true); break; case 'list': const listId = location.query.list; authorizeListAccess(listId, req, authorized => { if (!authorized) { ws.close(); return; } channel = `timeline:list:${listId}`; streamFrom(channel, req, streamToWs(req, ws), streamWsEnd(req, ws, subscriptionHeartbeat(channel))); }); break; default: ws.close(); } }); wss.startAutoPing(30000); attachServerWithConfig(server, address => { log.info(`Worker ${workerId} now listening on ${address}`); }); const onExit = () => { log.info(`Worker ${workerId} exiting, bye bye`); server.close(); process.exit(0); }; const onError = (err) => { log.error(err); server.close(); process.exit(0); }; process.on('SIGINT', onExit); process.on('SIGTERM', onExit); process.on('exit', onExit); process.on('uncaughtException', onError); }; const attachServerWithConfig = (server, onSuccess) => { if (process.env.SOCKET || process.env.PORT && isNaN(+process.env.PORT)) { server.listen(process.env.SOCKET || process.env.PORT, () => { if (onSuccess) { fs.chmodSync(server.address(), 0o666); onSuccess(server.address()); } }); } else { server.listen(+process.env.PORT || 4000, process.env.BIND || '0.0.0.0', () => { if (onSuccess) { onSuccess(`${server.address().address}:${server.address().port}`); } }); } }; const onPortAvailable = onSuccess => { const testServer = http.createServer(); testServer.once('error', err => { onSuccess(err); }); testServer.once('listening', () => { testServer.once('close', () => onSuccess()); testServer.close(); }); attachServerWithConfig(testServer); }; onPortAvailable(err => { if (err) { log.error('Could not start server, the port or socket is in use'); return; } throng({ workers: numWorkers, lifetime: Infinity, start: startWorker, master: startMaster, }); }); ================================================ FILE: vendor/.keep ================================================