From f0fff3eb1051ff77ec3f37aa75f8c56720b626a3 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 28 Sep 2018 02:23:45 +0200 Subject: Support min_id-based pagination in REST API (#8736) * Allow min_id pagination in Feed#get * Add min_id pagination to home and list timeline APIs * Add min_id pagination to account statuses, public and tag APIs * Remove unused stub in reports API * Use min_id pagination in notifications, favourites, and fix order * Fix HomeFeed#from_database not using paginate_by_id --- spec/controllers/api/v1/favourites_controller_spec.rb | 2 +- spec/controllers/api/v1/reports_controller_spec.rb | 10 ---------- 2 files changed, 1 insertion(+), 11 deletions(-) (limited to 'spec/controllers') diff --git a/spec/controllers/api/v1/favourites_controller_spec.rb b/spec/controllers/api/v1/favourites_controller_spec.rb index 2bdf927f2..231f76500 100644 --- a/spec/controllers/api/v1/favourites_controller_spec.rb +++ b/spec/controllers/api/v1/favourites_controller_spec.rb @@ -64,7 +64,7 @@ RSpec.describe Api::V1::FavouritesController, type: :controller do get :index, params: { limit: 1 } expect(response.headers['Link'].find_link(['rel', 'next']).href).to eq "http://test.host/api/v1/favourites?limit=1&max_id=#{favourite.id}" - expect(response.headers['Link'].find_link(['rel', 'prev']).href).to eq "http://test.host/api/v1/favourites?limit=1&since_id=#{favourite.id}" + expect(response.headers['Link'].find_link(['rel', 'prev']).href).to eq "http://test.host/api/v1/favourites?limit=1&min_id=#{favourite.id}" end it 'does not add pagination headers if not necessary' do diff --git a/spec/controllers/api/v1/reports_controller_spec.rb b/spec/controllers/api/v1/reports_controller_spec.rb index ac93998c6..a3596cf8a 100644 --- a/spec/controllers/api/v1/reports_controller_spec.rb +++ b/spec/controllers/api/v1/reports_controller_spec.rb @@ -12,16 +12,6 @@ RSpec.describe Api::V1::ReportsController, type: :controller do allow(controller).to receive(:doorkeeper_token) { token } end - describe 'GET #index' do - let(:scopes) { 'read:reports' } - - it 'returns http success' do - get :index - - expect(response).to have_http_status(200) - end - end - describe 'POST #create' do let(:scopes) { 'write:reports' } let!(:status) { Fabricate(:status) } -- cgit From 1f98eae1cf916a18007a26e1740b0e65aa7ff752 Mon Sep 17 00:00:00 2001 From: aus-social <42644106+aus-social@users.noreply.github.com> Date: Thu, 4 Oct 2018 20:36:53 +1000 Subject: Lint pass (#8876) --- config/boot.rb | 2 +- config/initializers/omniauth.rb | 3 +-- config/initializers/ostatus.rb | 2 +- config/initializers/twitter_regex.rb | 1 - config/initializers/vapid.rb | 1 - db/migrate/20160306172223_create_doorkeeper_tables.rb | 4 ++-- db/migrate/20161006213403_rails_settings_migration.rb | 2 +- .../20170606113804_change_tag_search_index_to_btree.rb | 1 - db/migrate/20170716191202_add_hide_notifications_to_mute.rb | 2 +- db/migrate/20170920032311_fix_reblogs_in_feeds.rb | 2 +- db/migrate/20171028221157_add_reblogs_to_follows.rb | 2 +- db/schema.rb | 1 - spec/controllers/accounts_controller_spec.rb | 4 ++-- spec/controllers/admin/accounts_controller_spec.rb | 1 - .../admin/email_domain_blocks_controller_spec.rb | 4 ++-- spec/controllers/admin/invites_controller_spec.rb | 2 +- spec/controllers/admin/report_notes_controller_spec.rb | 11 +++++------ spec/controllers/admin/reported_statuses_controller_spec.rb | 2 +- spec/controllers/api/v1/accounts_controller_spec.rb | 4 ++-- .../v1/statuses/favourited_by_accounts_controller_spec.rb | 1 - spec/controllers/api/v1/streaming_controller_spec.rb | 3 +-- spec/controllers/api/v1/timelines/home_controller_spec.rb | 2 +- spec/controllers/api/v1/timelines/public_controller_spec.rb | 2 +- spec/controllers/api/web/embeds_controller_spec.rb | 2 +- spec/controllers/application_controller_spec.rb | 4 ++-- spec/controllers/auth/confirmations_controller_spec.rb | 2 +- spec/controllers/invites_controller_spec.rb | 2 +- spec/controllers/remote_unfollows_controller_spec.rb | 6 +++--- spec/controllers/settings/applications_controller_spec.rb | 1 - spec/controllers/settings/migrations_controller_spec.rb | 2 -- .../confirmations_controller_spec.rb | 2 +- spec/fabricators/account_fabricator.rb | 2 +- spec/fabricators/list_fabricator.rb | 2 +- spec/fabricators/relay_fabricator.rb | 2 +- spec/fabricators/site_upload_fabricator.rb | 1 - spec/lib/feed_manager_spec.rb | 2 +- spec/lib/formatter_spec.rb | 13 ++++++------- spec/lib/language_detector_spec.rb | 2 +- spec/lib/ostatus/atom_serializer_spec.rb | 2 +- spec/lib/request_spec.rb | 2 +- spec/models/account_moderation_note_spec.rb | 1 - spec/models/admin/action_log_spec.rb | 1 - spec/models/backup_spec.rb | 1 - spec/models/conversation_mute_spec.rb | 1 - spec/models/custom_emoji_spec.rb | 2 +- spec/models/custom_filter_spec.rb | 1 - spec/models/list_account_spec.rb | 1 - spec/models/list_spec.rb | 1 - spec/models/media_attachment_spec.rb | 2 +- spec/models/mute_spec.rb | 1 - spec/models/preview_card_spec.rb | 1 - spec/models/remote_follow_spec.rb | 4 ++-- spec/models/web/setting_spec.rb | 1 - .../services/activitypub/process_collection_service_spec.rb | 4 ++-- spec/services/fetch_atom_service_spec.rb | 2 +- spec/views/stream_entries/show.html.haml_spec.rb | 2 +- 56 files changed, 55 insertions(+), 79 deletions(-) (limited to 'spec/controllers') diff --git a/config/boot.rb b/config/boot.rb index 593f575e9..beb45a5ee 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -1,6 +1,6 @@ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) -require 'bundler/setup' # Set up gems listed in the Gemfile. +require 'bundler/setup' # Set up gems listed in the Gemfile. require 'bootsnap' # Speed up boot time by caching expensive operations. Bootsnap.setup( diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb index 85fb81250..21b58eb5b 100644 --- a/config/initializers/omniauth.rb +++ b/config/initializers/omniauth.rb @@ -38,7 +38,7 @@ Devise.setup do |config| saml_options = options saml_options[:assertion_consumer_service_url] = ENV['SAML_ACS_URL'] if ENV['SAML_ACS_URL'] saml_options[:issuer] = ENV['SAML_ISSUER'] if ENV['SAML_ISSUER'] - saml_options[:idp_sso_target_url] = ENV['SAML_IDP_SSO_TARGET_URL'] if ENV['SAML_IDP_SSO_TARGET_URL'] + saml_options[:idp_sso_target_url] = ENV['SAML_IDP_SSO_TARGET_URL'] if ENV['SAML_IDP_SSO_TARGET_URL'] saml_options[:idp_sso_target_url_runtime_params] = ENV['SAML_IDP_SSO_TARGET_PARAMS'] if ENV['SAML_IDP_SSO_TARGET_PARAMS'] # FIXME: Should be parsable Hash saml_options[:idp_cert] = ENV['SAML_IDP_CERT'] if ENV['SAML_IDP_CERT'] saml_options[:idp_cert_fingerprint] = ENV['SAML_IDP_CERT_FINGERPRINT'] if ENV['SAML_IDP_CERT_FINGERPRINT'] @@ -62,5 +62,4 @@ Devise.setup do |config| saml_options[:uid_attribute] = ENV['SAML_UID_ATTRIBUTE'] if ENV['SAML_UID_ATTRIBUTE'] config.omniauth :saml, saml_options end - end diff --git a/config/initializers/ostatus.rb b/config/initializers/ostatus.rb index 5773b7290..757f1f735 100644 --- a/config/initializers/ostatus.rb +++ b/config/initializers/ostatus.rb @@ -7,7 +7,7 @@ web_host = ENV.fetch('WEB_DOMAIN') { host } alternate_domains = ENV.fetch('ALTERNATE_DOMAINS') { '' } Rails.application.configure do - https = Rails.env.production? || ENV['LOCAL_HTTPS'] == 'true' + https = Rails.env.production? || ENV['LOCAL_HTTPS'] == 'true' config.x.local_domain = host config.x.web_domain = web_host diff --git a/config/initializers/twitter_regex.rb b/config/initializers/twitter_regex.rb index 76b23f416..0e8f5bfeb 100644 --- a/config/initializers/twitter_regex.rb +++ b/config/initializers/twitter_regex.rb @@ -1,6 +1,5 @@ module Twitter class Regex - REGEXEN[:valid_general_url_path_chars] = /[^\p{White_Space}\(\)\?]/iou REGEXEN[:valid_url_path_ending_chars] = /[^\p{White_Space}\(\)\?!\*';:=\,\.\$%\[\]~&\|@]|(?:#{REGEXEN[:valid_url_balanced_parens]})/iou REGEXEN[:valid_url_balanced_parens] = / diff --git a/config/initializers/vapid.rb b/config/initializers/vapid.rb index 618f5a3fb..7dd870c8b 100644 --- a/config/initializers/vapid.rb +++ b/config/initializers/vapid.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true Rails.application.configure do - # You can generate the keys using the following command (first is the private key, second is the public one) # You should only generate this once per instance. If you later decide to change it, all push subscription will # be invalidated, requiring the users to access the website again to resubscribe. diff --git a/db/migrate/20160306172223_create_doorkeeper_tables.rb b/db/migrate/20160306172223_create_doorkeeper_tables.rb index 9e173a43f..462343e88 100644 --- a/db/migrate/20160306172223_create_doorkeeper_tables.rb +++ b/db/migrate/20160306172223_create_doorkeeper_tables.rb @@ -34,12 +34,12 @@ class CreateDoorkeeperTables < ActiveRecord::Migration[4.2] # https://github.com/doorkeeper-gem/doorkeeper/tree/v3.0.0.rc1#custom-access-token-generator # # t.text :token, null: false - t.string :token, null: false + t.string :token, null: false t.string :refresh_token t.integer :expires_in t.datetime :revoked_at - t.datetime :created_at, null: false + t.datetime :created_at, null: false t.string :scopes end diff --git a/db/migrate/20161006213403_rails_settings_migration.rb b/db/migrate/20161006213403_rails_settings_migration.rb index 3b2e637fc..42875d7cb 100644 --- a/db/migrate/20161006213403_rails_settings_migration.rb +++ b/db/migrate/20161006213403_rails_settings_migration.rb @@ -7,7 +7,7 @@ end class RailsSettingsMigration < MIGRATION_BASE_CLASS def self.up create_table :settings do |t| - t.string :var, :null => false + t.string :var, :null => false t.text :value t.references :target, :null => false, :polymorphic => true t.timestamps :null => true diff --git a/db/migrate/20170606113804_change_tag_search_index_to_btree.rb b/db/migrate/20170606113804_change_tag_search_index_to_btree.rb index 5248e1720..979df2e74 100644 --- a/db/migrate/20170606113804_change_tag_search_index_to_btree.rb +++ b/db/migrate/20170606113804_change_tag_search_index_to_btree.rb @@ -1,5 +1,4 @@ class ChangeTagSearchIndexToBtree < ActiveRecord::Migration[5.1] - def up remove_index :tags, name: :hashtag_search_index execute 'CREATE INDEX hashtag_search_index ON tags (name text_pattern_ops);' diff --git a/db/migrate/20170716191202_add_hide_notifications_to_mute.rb b/db/migrate/20170716191202_add_hide_notifications_to_mute.rb index 0410938c9..a498396b7 100644 --- a/db/migrate/20170716191202_add_hide_notifications_to_mute.rb +++ b/db/migrate/20170716191202_add_hide_notifications_to_mute.rb @@ -8,7 +8,7 @@ class AddHideNotificationsToMute < ActiveRecord::Migration[5.1] def up add_column_with_default :mutes, :hide_notifications, :boolean, default: true, allow_null: false end - + def down remove_column :mutes, :hide_notifications end diff --git a/db/migrate/20170920032311_fix_reblogs_in_feeds.rb b/db/migrate/20170920032311_fix_reblogs_in_feeds.rb index 439c5fca0..5654bf6f8 100644 --- a/db/migrate/20170920032311_fix_reblogs_in_feeds.rb +++ b/db/migrate/20170920032311_fix_reblogs_in_feeds.rb @@ -28,7 +28,7 @@ class FixReblogsInFeeds < ActiveRecord::Migration[5.1] -- So, first, we iterate over the user's feed to find any reblogs. local items = redis.call('zrange', timeline_key, 0, -1, 'withscores') - + for i = 1, #items, 2 do local reblogged_id = items[i] local reblogging_id = items[i + 1] diff --git a/db/migrate/20171028221157_add_reblogs_to_follows.rb b/db/migrate/20171028221157_add_reblogs_to_follows.rb index 4b5d5b7ff..3b2e46ed8 100644 --- a/db/migrate/20171028221157_add_reblogs_to_follows.rb +++ b/db/migrate/20171028221157_add_reblogs_to_follows.rb @@ -11,7 +11,7 @@ class AddReblogsToFollows < ActiveRecord::Migration[5.1] add_column_with_default :follow_requests, :show_reblogs, :boolean, default: true, allow_null: false end end - + def down remove_column :follows, :show_reblogs remove_column :follow_requests, :show_reblogs diff --git a/db/schema.rb b/db/schema.rb index f3b06f7c0..577296a6a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,6 @@ # It's strongly recommended that you check this file into your version control system. ActiveRecord::Schema.define(version: 2018_08_20_232245) do - # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" diff --git a/spec/controllers/accounts_controller_spec.rb b/spec/controllers/accounts_controller_spec.rb index 18c249c07..3ba5d8aec 100644 --- a/spec/controllers/accounts_controller_spec.rb +++ b/spec/controllers/accounts_controller_spec.rb @@ -3,8 +3,8 @@ require 'rails_helper' RSpec.describe AccountsController, type: :controller do render_views - let(:alice) { Fabricate(:account, username: 'alice') } - let(:eve) { Fabricate(:user) } + let(:alice) { Fabricate(:account, username: 'alice') } + let(:eve) { Fabricate(:user) } describe 'GET #show' do let!(:status1) { Status.create!(account: alice, text: 'Hello world') } diff --git a/spec/controllers/admin/accounts_controller_spec.rb b/spec/controllers/admin/accounts_controller_spec.rb index 197e019fe..fa2786c9b 100644 --- a/spec/controllers/admin/accounts_controller_spec.rb +++ b/spec/controllers/admin/accounts_controller_spec.rb @@ -75,7 +75,6 @@ RSpec.describe Admin::AccountsController, type: :controller do end end - describe 'POST #subscribe' do subject { post :subscribe, params: { id: account.id } } diff --git a/spec/controllers/admin/email_domain_blocks_controller_spec.rb b/spec/controllers/admin/email_domain_blocks_controller_spec.rb index 133d38ff1..52db56f4e 100644 --- a/spec/controllers/admin/email_domain_blocks_controller_spec.rb +++ b/spec/controllers/admin/email_domain_blocks_controller_spec.rb @@ -40,7 +40,7 @@ RSpec.describe Admin::EmailDomainBlocksController, type: :controller do describe 'POST #create' do it 'blocks the domain when succeeded to save' do - post :create, params: { email_domain_block: { domain: 'example.com'} } + post :create, params: { email_domain_block: { domain: 'example.com' } } expect(flash[:notice]).to eq I18n.t('admin.email_domain_blocks.created_msg') expect(response).to redirect_to(admin_email_domain_blocks_path) @@ -50,7 +50,7 @@ RSpec.describe Admin::EmailDomainBlocksController, type: :controller do describe 'DELETE #destroy' do it 'unblocks the domain' do email_domain_block = Fabricate(:email_domain_block) - delete :destroy, params: { id: email_domain_block.id } + delete :destroy, params: { id: email_domain_block.id } expect(flash[:notice]).to eq I18n.t('admin.email_domain_blocks.destroyed_msg') expect(response).to redirect_to(admin_email_domain_blocks_path) diff --git a/spec/controllers/admin/invites_controller_spec.rb b/spec/controllers/admin/invites_controller_spec.rb index e7d995411..34b51a6aa 100644 --- a/spec/controllers/admin/invites_controller_spec.rb +++ b/spec/controllers/admin/invites_controller_spec.rb @@ -24,7 +24,7 @@ describe Admin::InvitesController do subject { post :create, params: { invite: { max_uses: '10', expires_in: 1800 } } } it 'succeeds to create a invite' do - expect{ subject }.to change { Invite.count }.by(1) + expect { subject }.to change { Invite.count }.by(1) expect(subject).to redirect_to admin_invites_path expect(Invite.last).to have_attributes(user_id: user.id, max_uses: 10) end diff --git a/spec/controllers/admin/report_notes_controller_spec.rb b/spec/controllers/admin/report_notes_controller_spec.rb index 2c32303fb..ec5872c7d 100644 --- a/spec/controllers/admin/report_notes_controller_spec.rb +++ b/spec/controllers/admin/report_notes_controller_spec.rb @@ -15,7 +15,6 @@ describe Admin::ReportNotesController do let(:report) { Fabricate(:report, action_taken: action_taken, action_taken_by_account_id: account_id) } context 'when parameter is valid' do - context 'when report is unsolved' do let(:action_taken) { false } let(:account_id) { nil } @@ -24,7 +23,7 @@ describe Admin::ReportNotesController do let(:params) { { report_note: { content: 'test content', report_id: report.id }, create_and_resolve: nil } } it 'creates a report note and resolves report' do - expect{ subject }.to change{ ReportNote.count }.by(1) + expect { subject }.to change { ReportNote.count }.by(1) expect(report.reload).to be_action_taken expect(subject).to redirect_to admin_reports_path end @@ -34,7 +33,7 @@ describe Admin::ReportNotesController do let(:params) { { report_note: { content: 'test content', report_id: report.id } } } it 'creates a report note and does not resolve report' do - expect{ subject }.to change{ ReportNote.count }.by(1) + expect { subject }.to change { ReportNote.count }.by(1) expect(report.reload).not_to be_action_taken expect(subject).to redirect_to admin_report_path(report) end @@ -49,7 +48,7 @@ describe Admin::ReportNotesController do let(:params) { { report_note: { content: 'test content', report_id: report.id }, create_and_unresolve: nil } } it 'creates a report note and unresolves report' do - expect{ subject }.to change{ ReportNote.count }.by(1) + expect { subject }.to change { ReportNote.count }.by(1) expect(report.reload).not_to be_action_taken expect(subject).to redirect_to admin_report_path(report) end @@ -59,7 +58,7 @@ describe Admin::ReportNotesController do let(:params) { { report_note: { content: 'test content', report_id: report.id } } } it 'creates a report note and does not unresolve report' do - expect{ subject }.to change{ ReportNote.count }.by(1) + expect { subject }.to change { ReportNote.count }.by(1) expect(report.reload).to be_action_taken expect(subject).to redirect_to admin_report_path(report) end @@ -84,7 +83,7 @@ describe Admin::ReportNotesController do let!(:report_note) { Fabricate(:report_note) } it 'deletes note' do - expect{ subject }.to change{ ReportNote.count }.by(-1) + expect { subject }.to change { ReportNote.count }.by(-1) expect(subject).to redirect_to admin_report_path(report_note.report) end end diff --git a/spec/controllers/admin/reported_statuses_controller_spec.rb b/spec/controllers/admin/reported_statuses_controller_spec.rb index 7adbf36b9..c358506d6 100644 --- a/spec/controllers/admin/reported_statuses_controller_spec.rb +++ b/spec/controllers/admin/reported_statuses_controller_spec.rb @@ -13,7 +13,7 @@ describe Admin::ReportedStatusesController do describe 'POST #create' do subject do - -> { post :create, params: { :report_id => report, action => '', :form_status_batch => { status_ids: status_ids } } } + -> { post :create, params: { :report_id => report, action => '', :form_status_batch => { status_ids: status_ids } } } end let(:action) { 'nsfw_on' } diff --git a/spec/controllers/api/v1/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts_controller_spec.rb index 3e54e88a5..c506fb5f0 100644 --- a/spec/controllers/api/v1/accounts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts_controller_spec.rb @@ -154,7 +154,7 @@ RSpec.describe Api::V1::AccountsController, type: :controller do before do user.account.follow!(other_account) - post :mute, params: {id: other_account.id } + post :mute, params: { id: other_account.id } end it 'returns http success' do @@ -182,7 +182,7 @@ RSpec.describe Api::V1::AccountsController, type: :controller do before do user.account.follow!(other_account) - post :mute, params: {id: other_account.id, notifications: false } + post :mute, params: { id: other_account.id, notifications: false } end it 'returns http success' do diff --git a/spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb b/spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb index 23b5d7de9..40f75c700 100644 --- a/spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb +++ b/spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb @@ -25,7 +25,6 @@ RSpec.describe Api::V1::Statuses::FavouritedByAccountsController, type: :control expect(response.headers['Link'].links.size).to eq(2) end end - end context 'without an oauth token' do diff --git a/spec/controllers/api/v1/streaming_controller_spec.rb b/spec/controllers/api/v1/streaming_controller_spec.rb index daf2807e7..4ab409a54 100644 --- a/spec/controllers/api/v1/streaming_controller_spec.rb +++ b/spec/controllers/api/v1/streaming_controller_spec.rb @@ -31,7 +31,7 @@ describe Api::V1::StreamingController do describe 'GET #index' do it 'redirects to streaming host' do - get :index, params: {access_token: 'deadbeef', stream: 'public'} + get :index, params: { access_token: 'deadbeef', stream: 'public' } expect(response).to have_http_status(301) request_uri = URI.parse(request.url) redirect_to_uri = URI.parse(response.location) @@ -42,5 +42,4 @@ describe Api::V1::StreamingController do end end end - end diff --git a/spec/controllers/api/v1/timelines/home_controller_spec.rb b/spec/controllers/api/v1/timelines/home_controller_spec.rb index a667c33fa..63d624c35 100644 --- a/spec/controllers/api/v1/timelines/home_controller_spec.rb +++ b/spec/controllers/api/v1/timelines/home_controller_spec.rb @@ -5,7 +5,7 @@ require 'rails_helper' describe Api::V1::Timelines::HomeController do render_views - let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice'), current_sign_in_at: 1.day.ago) } + let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice'), current_sign_in_at: 1.day.ago) } before do allow(controller).to receive(:doorkeeper_token) { token } diff --git a/spec/controllers/api/v1/timelines/public_controller_spec.rb b/spec/controllers/api/v1/timelines/public_controller_spec.rb index 68d87bbcb..a0f778cdc 100644 --- a/spec/controllers/api/v1/timelines/public_controller_spec.rb +++ b/spec/controllers/api/v1/timelines/public_controller_spec.rb @@ -5,7 +5,7 @@ require 'rails_helper' describe Api::V1::Timelines::PublicController do render_views - let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } + let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } before do allow(controller).to receive(:doorkeeper_token) { token } diff --git a/spec/controllers/api/web/embeds_controller_spec.rb b/spec/controllers/api/web/embeds_controller_spec.rb index 6b7297189..a8fc1718f 100644 --- a/spec/controllers/api/web/embeds_controller_spec.rb +++ b/spec/controllers/api/web/embeds_controller_spec.rb @@ -14,7 +14,7 @@ describe Api::Web::EmbedsController do context 'when successfully finds status' do let(:status) { Fabricate(:status) } - let(:url) { "http://#{ Rails.configuration.x.web_domain }/@#{status.account.username}/#{status.id}" } + let(:url) { "http://#{Rails.configuration.x.web_domain}/@#{status.account.username}/#{status.id}" } it 'returns a right response' do expect(response).to have_http_status :ok diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index d158625e6..33cc71087 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -94,7 +94,7 @@ describe ApplicationController, type: :controller do describe 'helper_method :current_theme' do it 'returns "default" when theme wasn\'t changed in admin settings' do - allow(Setting).to receive(:default_settings).and_return({'theme' => 'default'}) + allow(Setting).to receive(:default_settings).and_return({ 'theme' => 'default' }) expect(controller.view_context.current_theme).to eq 'default' end @@ -197,7 +197,7 @@ describe ApplicationController, type: :controller do describe 'raise_not_found' do it 'raises error' do controller.params[:unmatched_route] = 'unmatched' - expect{ controller.raise_not_found }.to raise_error(ActionController::RoutingError, 'No route matches unmatched') + expect { controller.raise_not_found }.to raise_error(ActionController::RoutingError, 'No route matches unmatched') end end diff --git a/spec/controllers/auth/confirmations_controller_spec.rb b/spec/controllers/auth/confirmations_controller_spec.rb index 35eed4f51..e9a471fc5 100644 --- a/spec/controllers/auth/confirmations_controller_spec.rb +++ b/spec/controllers/auth/confirmations_controller_spec.rb @@ -67,7 +67,7 @@ describe Auth::ConfirmationsController, type: :controller do end describe 'PATCH #finish_signup' do - subject { patch :finish_signup, params: { user: { email: email }} } + subject { patch :finish_signup, params: { user: { email: email } } } let(:user) { Fabricate(:user) } before do diff --git a/spec/controllers/invites_controller_spec.rb b/spec/controllers/invites_controller_spec.rb index 9f5ab67c3..76e617e6b 100644 --- a/spec/controllers/invites_controller_spec.rb +++ b/spec/controllers/invites_controller_spec.rb @@ -43,7 +43,7 @@ describe InvitesController do let(:user) { Fabricate(:user, moderator: false, admin: true) } it 'succeeds to create a invite' do - expect{ subject }.to change { Invite.count }.by(1) + expect { subject }.to change { Invite.count }.by(1) expect(subject).to redirect_to invites_path expect(Invite.last).to have_attributes(user_id: user.id, max_uses: 10) end diff --git a/spec/controllers/remote_unfollows_controller_spec.rb b/spec/controllers/remote_unfollows_controller_spec.rb index 223ed64af..a1a55ede0 100644 --- a/spec/controllers/remote_unfollows_controller_spec.rb +++ b/spec/controllers/remote_unfollows_controller_spec.rb @@ -14,11 +14,11 @@ describe RemoteUnfollowsController do before do sign_in current_user current_account.follow!(remote_account) - stub_request(:post, 'http://example.com/inbox'){ { status: 200 } } + stub_request(:post, 'http://example.com/inbox') { { status: 200 } } end context 'when successfully unfollow remote account' do - let(:acct) {"acct:#{ remote_account.username }@#{ remote_account.domain }"} + let(:acct) { "acct:#{remote_account.username}@#{remote_account.domain}" } it do is_expected.to render_template :success @@ -27,7 +27,7 @@ describe RemoteUnfollowsController do end context 'when fails to unfollow remote account' do - let(:acct) {"acct:#{ remote_account.username + '_test' }@#{ remote_account.domain }"} + let(:acct) { "acct:#{remote_account.username + '_test'}@#{remote_account.domain}" } it do is_expected.to render_template :error diff --git a/spec/controllers/settings/applications_controller_spec.rb b/spec/controllers/settings/applications_controller_spec.rb index f87107695..fd4b10071 100644 --- a/spec/controllers/settings/applications_controller_spec.rb +++ b/spec/controllers/settings/applications_controller_spec.rb @@ -21,7 +21,6 @@ describe Settings::ApplicationsController do end end - describe 'GET #show' do it 'returns http success' do get :show, params: { id: app.id } diff --git a/spec/controllers/settings/migrations_controller_spec.rb b/spec/controllers/settings/migrations_controller_spec.rb index a621bcf1c..4d814a45e 100644 --- a/spec/controllers/settings/migrations_controller_spec.rb +++ b/spec/controllers/settings/migrations_controller_spec.rb @@ -10,7 +10,6 @@ describe Settings::MigrationsController do end describe 'GET #show' do - context 'when user is not sign in' do subject { get :show } @@ -45,7 +44,6 @@ describe Settings::MigrationsController do end describe 'PUT #update' do - context 'when user is not sign in' do subject { put :update } diff --git a/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb b/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb index 7612bf90e..478f24585 100644 --- a/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb +++ b/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb @@ -50,7 +50,7 @@ describe Settings::TwoFactorAuthentication::ConfirmationsController do describe 'when form_two_factor_confirmation parameter is not provided' do it 'raises ActionController::ParameterMissing' do - expect { post :create, params: { } }.to raise_error(ActionController::ParameterMissing) + expect { post :create, params: {} }.to raise_error(ActionController::ParameterMissing) end end diff --git a/spec/fabricators/account_fabricator.rb b/spec/fabricators/account_fabricator.rb index 7aa983f82..e092e6c09 100644 --- a/spec/fabricators/account_fabricator.rb +++ b/spec/fabricators/account_fabricator.rb @@ -6,5 +6,5 @@ Fabricator(:account) do username { sequence(:username) { |i| "#{Faker::Internet.user_name(nil, %w(_))}#{i}" } } last_webfingered_at { Time.now.utc } public_key { public_key } - private_key { private_key} + private_key { private_key } end diff --git a/spec/fabricators/list_fabricator.rb b/spec/fabricators/list_fabricator.rb index 2a61b317b..c3db690fa 100644 --- a/spec/fabricators/list_fabricator.rb +++ b/spec/fabricators/list_fabricator.rb @@ -1,4 +1,4 @@ Fabricator(:list) do account - title "MyString" + title "MyString" end diff --git a/spec/fabricators/relay_fabricator.rb b/spec/fabricators/relay_fabricator.rb index 3f8726f6b..488913f77 100644 --- a/spec/fabricators/relay_fabricator.rb +++ b/spec/fabricators/relay_fabricator.rb @@ -1,4 +1,4 @@ Fabricator(:relay) do inbox_url "https://example.com/inbox" - state :idle + state :idle end diff --git a/spec/fabricators/site_upload_fabricator.rb b/spec/fabricators/site_upload_fabricator.rb index 8f4e43ac9..4a171486f 100644 --- a/spec/fabricators/site_upload_fabricator.rb +++ b/spec/fabricators/site_upload_fabricator.rb @@ -1,3 +1,2 @@ Fabricator(:site_upload) do - end diff --git a/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb index 7535e144d..64e109aec 100644 --- a/spec/lib/feed_manager_spec.rb +++ b/spec/lib/feed_manager_spec.rb @@ -393,7 +393,7 @@ RSpec.describe FeedManager do end it 'sends push updates' do - status = Fabricate(:status) + status = Fabricate(:status) FeedManager.instance.push_to_home(receiver, status) diff --git a/spec/lib/formatter_spec.rb b/spec/lib/formatter_spec.rb index 0c2248cae..ec4a6493d 100644 --- a/spec/lib/formatter_spec.rb +++ b/spec/lib/formatter_spec.rb @@ -170,12 +170,11 @@ RSpec.describe Formatter do 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) } + let(:status) { Fabricate(:status, text: 'text', spoiler_text: 'Secret!', uri: nil) } it 'Returns the spoiler text' do is_expected.to eq 'Secret!' @@ -184,7 +183,7 @@ RSpec.describe Formatter do 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(: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 @@ -207,7 +206,7 @@ RSpec.describe Formatter do end context 'given a post containing plain text' do - let(:status) { Fabricate(:status, text: 'text', uri: nil) } + let(:status) { Fabricate(:status, text: 'text', uri: nil) } it 'paragraphizes the text' do is_expected.to eq '

text

' @@ -215,7 +214,7 @@ RSpec.describe Formatter do end context 'given a post containing line feeds' do - let(:status) { Fabricate(:status, text: "line\nfeed", uri: nil) } + let(:status) { Fabricate(:status, text: "line\nfeed", uri: nil) } it 'removes line feeds' do is_expected.not_to include "\n" @@ -367,7 +366,7 @@ RSpec.describe Formatter 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) } + 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

' @@ -375,7 +374,7 @@ RSpec.describe Formatter do end context 'given a post with remote status' do - let(:status) { Fabricate(:status, account: remote_account, text: '') } + let(:status) { Fabricate(:status, account: remote_account, text: '') } it 'returns tag-stripped text' do is_expected.to eq '' diff --git a/spec/lib/language_detector_spec.rb b/spec/lib/language_detector_spec.rb index d00d2a0e6..cdc51a656 100644 --- a/spec/lib/language_detector_spec.rb +++ b/spec/lib/language_detector_spec.rb @@ -90,7 +90,7 @@ describe LanguageDetector do end it 'uses nil when account is present but has no locale' do - result = described_class.instance.detect('', account_without_user_locale) + result = described_class.instance.detect('', account_without_user_locale) expect(result).to eq nil end diff --git a/spec/lib/ostatus/atom_serializer_spec.rb b/spec/lib/ostatus/atom_serializer_spec.rb index 0bd22880e..3bc4b7efb 100644 --- a/spec/lib/ostatus/atom_serializer_spec.rb +++ b/spec/lib/ostatus/atom_serializer_spec.rb @@ -880,7 +880,7 @@ RSpec.describe OStatus::AtomSerializer do ProcessInteractionService.new.call(envelope, block.target_account) - expect{ block.reload }.to raise_error ActiveRecord::RecordNotFound + expect { block.reload }.to raise_error ActiveRecord::RecordNotFound end end diff --git a/spec/lib/request_spec.rb b/spec/lib/request_spec.rb index 939ac006a..8cc5d90ce 100644 --- a/spec/lib/request_spec.rb +++ b/spec/lib/request_spec.rb @@ -84,7 +84,7 @@ describe Request do allow(Addrinfo).to receive(:foreach).with('example.com', nil, nil, :SOCK_STREAM) .and_yield(Addrinfo.new(["AF_INET", 0, "example.com", "0.0.0.0"], :PF_INET, :SOCK_STREAM)) .and_yield(Addrinfo.new(["AF_INET6", 0, "example.com", "2001:db8::face"], :PF_INET6, :SOCK_STREAM)) - expect{ subject.perform }.to raise_error Mastodon::ValidationError + expect { subject.perform }.to raise_error Mastodon::ValidationError end end end diff --git a/spec/models/account_moderation_note_spec.rb b/spec/models/account_moderation_note_spec.rb index 16983b2e3..69bd5500a 100644 --- a/spec/models/account_moderation_note_spec.rb +++ b/spec/models/account_moderation_note_spec.rb @@ -1,5 +1,4 @@ require 'rails_helper' RSpec.describe AccountModerationNote, type: :model do - end diff --git a/spec/models/admin/action_log_spec.rb b/spec/models/admin/action_log_spec.rb index 59206a36b..81d7e1be3 100644 --- a/spec/models/admin/action_log_spec.rb +++ b/spec/models/admin/action_log_spec.rb @@ -1,5 +1,4 @@ require 'rails_helper' RSpec.describe Admin::ActionLog, type: :model do - end diff --git a/spec/models/backup_spec.rb b/spec/models/backup_spec.rb index fabcdc845..45230986d 100644 --- a/spec/models/backup_spec.rb +++ b/spec/models/backup_spec.rb @@ -1,5 +1,4 @@ require 'rails_helper' RSpec.describe Backup, type: :model do - end diff --git a/spec/models/conversation_mute_spec.rb b/spec/models/conversation_mute_spec.rb index b602e80c1..3fc2915d4 100644 --- a/spec/models/conversation_mute_spec.rb +++ b/spec/models/conversation_mute_spec.rb @@ -1,5 +1,4 @@ require 'rails_helper' RSpec.describe ConversationMute, type: :model do - end diff --git a/spec/models/custom_emoji_spec.rb b/spec/models/custom_emoji_spec.rb index 87367df50..320a258d3 100644 --- a/spec/models/custom_emoji_spec.rb +++ b/spec/models/custom_emoji_spec.rb @@ -4,7 +4,7 @@ RSpec.describe CustomEmoji, type: :model do describe '#search' do let(:custom_emoji) { Fabricate(:custom_emoji, shortcode: shortcode) } - subject { described_class.search(search_term) } + subject { described_class.search(search_term) } context 'shortcode is exact' do let(:shortcode) { 'blobpats' } diff --git a/spec/models/custom_filter_spec.rb b/spec/models/custom_filter_spec.rb index 1024542e7..3943dd5f1 100644 --- a/spec/models/custom_filter_spec.rb +++ b/spec/models/custom_filter_spec.rb @@ -1,5 +1,4 @@ require 'rails_helper' RSpec.describe CustomFilter, type: :model do - end diff --git a/spec/models/list_account_spec.rb b/spec/models/list_account_spec.rb index a132e09b0..a0cf02efe 100644 --- a/spec/models/list_account_spec.rb +++ b/spec/models/list_account_spec.rb @@ -1,5 +1,4 @@ require 'rails_helper' RSpec.describe ListAccount, type: :model do - end diff --git a/spec/models/list_spec.rb b/spec/models/list_spec.rb index c302482b4..b780bb1de 100644 --- a/spec/models/list_spec.rb +++ b/spec/models/list_spec.rb @@ -1,5 +1,4 @@ require 'rails_helper' RSpec.describe List, type: :model do - end diff --git a/spec/models/media_attachment_spec.rb b/spec/models/media_attachment_spec.rb index cb1cee518..266cd4920 100644 --- a/spec/models/media_attachment_spec.rb +++ b/spec/models/media_attachment_spec.rb @@ -131,7 +131,7 @@ RSpec.describe MediaAttachment, type: :model do 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 + expect(media.file.meta["small"]["aspect"]).to eq 490.0 / 327 end end diff --git a/spec/models/mute_spec.rb b/spec/models/mute_spec.rb index 83ba793b2..38a87bdf4 100644 --- a/spec/models/mute_spec.rb +++ b/spec/models/mute_spec.rb @@ -1,5 +1,4 @@ require 'rails_helper' RSpec.describe Mute, type: :model do - end diff --git a/spec/models/preview_card_spec.rb b/spec/models/preview_card_spec.rb index 14ef23923..45233d1d4 100644 --- a/spec/models/preview_card_spec.rb +++ b/spec/models/preview_card_spec.rb @@ -1,5 +1,4 @@ require 'rails_helper' RSpec.describe PreviewCard, type: :model do - end diff --git a/spec/models/remote_follow_spec.rb b/spec/models/remote_follow_spec.rb index 72c580f9f..ed2667b28 100644 --- a/spec/models/remote_follow_spec.rb +++ b/spec/models/remote_follow_spec.rb @@ -34,7 +34,7 @@ RSpec.describe RemoteFollow do subject { remote_follow.valid? } context 'attrs with acct' do - let(:attrs) { { acct: 'gargron@quitter.no' }} + let(:attrs) { { acct: 'gargron@quitter.no' } } it do is_expected.to be true @@ -42,7 +42,7 @@ RSpec.describe RemoteFollow do end context 'attrs without acct' do - let(:attrs) { { } } + let(:attrs) { {} } it do is_expected.to be false diff --git a/spec/models/web/setting_spec.rb b/spec/models/web/setting_spec.rb index 90e7695aa..6657d4030 100644 --- a/spec/models/web/setting_spec.rb +++ b/spec/models/web/setting_spec.rb @@ -1,5 +1,4 @@ require 'rails_helper' RSpec.describe Web::Setting, type: :model do - end diff --git a/spec/services/activitypub/process_collection_service_spec.rb b/spec/services/activitypub/process_collection_service_spec.rb index e46f0ae45..bbe97d211 100644 --- a/spec/services/activitypub/process_collection_service_spec.rb +++ b/spec/services/activitypub/process_collection_service_spec.rb @@ -34,7 +34,7 @@ RSpec.describe ActivityPub::ProcessCollectionService, type: :service do end it 'processes payload with actor if valid signature exists' do - payload['signature'] = {'type' => 'RsaSignature2017'} + 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)) @@ -43,7 +43,7 @@ RSpec.describe ActivityPub::ProcessCollectionService, type: :service do end it 'does not process payload if invalid signature exists' do - payload['signature'] = {'type' => 'RsaSignature2017'} + payload['signature'] = { 'type' => 'RsaSignature2017' } expect_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_account!).and_return(nil) expect(ActivityPub::Activity).not_to receive(:factory) diff --git a/spec/services/fetch_atom_service_spec.rb b/spec/services/fetch_atom_service_spec.rb index bb233c12d..30e5b0935 100644 --- a/spec/services/fetch_atom_service_spec.rb +++ b/spec/services/fetch_atom_service_spec.rb @@ -57,7 +57,7 @@ RSpec.describe FetchAtomService, type: :service do context 'content type is application/atom+xml' do let(:content_type) { 'application/atom+xml' } - it { is_expected.to eq [url, {:prefetched_body=>""}, :ostatus] } + it { is_expected.to eq [url, { :prefetched_body => "" }, :ostatus] } end context 'content_type is json' do diff --git a/spec/views/stream_entries/show.html.haml_spec.rb b/spec/views/stream_entries/show.html.haml_spec.rb index d06bb7abb..e08419596 100644 --- a/spec/views/stream_entries/show.html.haml_spec.rb +++ b/spec/views/stream_entries/show.html.haml_spec.rb @@ -50,7 +50,7 @@ describe 'stream_entries/show.html.haml', without_verify_partial_doubles: true d 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)}]) + assign(:descendant_threads, [{ statuses: reply.stream_entry.activity.descendants(1) }]) render -- cgit From e645ae95610fcb69d15366bc32ab60014d2ebdcf Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 4 Oct 2018 16:05:38 +0200 Subject: Change admin accounts default sort to most recent (#8813) --- app/controllers/admin/accounts_controller.rb | 2 +- app/helpers/admin/filter_helper.rb | 2 +- app/models/account_filter.rb | 6 +++--- app/views/admin/accounts/index.html.haml | 4 ++-- spec/controllers/admin/accounts_controller_spec.rb | 4 ++-- spec/models/account_filter_spec.rb | 6 +++--- 6 files changed, 12 insertions(+), 12 deletions(-) (limited to 'spec/controllers') diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index e7ca6b907..5d57fe361 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -95,7 +95,7 @@ module Admin :remote, :by_domain, :silenced, - :recent, + :alphabetic, :suspended, :username, :display_name, diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb index 359c43d0e..60e5142e3 100644 --- a/app/helpers/admin/filter_helper.rb +++ b/app/helpers/admin/filter_helper.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Admin::FilterHelper - ACCOUNT_FILTERS = %i(local remote by_domain silenced suspended recent username display_name email ip staff).freeze + ACCOUNT_FILTERS = %i(local remote by_domain silenced suspended alphabetic username display_name email ip staff).freeze REPORT_FILTERS = %i(resolved account_id target_account_id).freeze INVITE_FILTER = %i(available expired).freeze CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze diff --git a/app/models/account_filter.rb b/app/models/account_filter.rb index dc7a03039..84364bf1b 100644 --- a/app/models/account_filter.rb +++ b/app/models/account_filter.rb @@ -8,7 +8,7 @@ class AccountFilter end def results - scope = Account.alphabetic + scope = Account.recent params.each do |key, value| scope.merge!(scope_for(key, value)) if value.present? @@ -29,8 +29,8 @@ class AccountFilter Account.where(domain: value) when 'silenced' Account.silenced - when 'recent' - Account.recent + when 'alphabetic' + Account.reorder(nil).alphabetic when 'suspended' Account.suspended when 'username' diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml index 6aa39a80a..4bee73adc 100644 --- a/app/views/admin/accounts/index.html.haml +++ b/app/views/admin/accounts/index.html.haml @@ -38,8 +38,8 @@ .filter-subset %strong= t('admin.accounts.order.title') %ul - %li= filter_link_to t('admin.accounts.order.alphabetic'), recent: nil - %li= filter_link_to t('admin.accounts.order.most_recent'), recent: '1' + %li= filter_link_to t('admin.accounts.order.most_recent'), alphabetic: nil + %li= filter_link_to t('admin.accounts.order.alphabetic'), alphabetic: '1' = form_tag admin_accounts_url, method: 'GET', class: 'simple_form' do .fields-group diff --git a/spec/controllers/admin/accounts_controller_spec.rb b/spec/controllers/admin/accounts_controller_spec.rb index fa2786c9b..ae9e058c8 100644 --- a/spec/controllers/admin/accounts_controller_spec.rb +++ b/spec/controllers/admin/accounts_controller_spec.rb @@ -25,7 +25,7 @@ RSpec.describe Admin::AccountsController, type: :controller do expect(h[:remote]).to eq '1' expect(h[:by_domain]).to eq 'domain' expect(h[:silenced]).to eq '1' - expect(h[:recent]).to eq '1' + expect(h[:alphabetic]).to eq '1' expect(h[:suspended]).to eq '1' expect(h[:username]).to eq 'username' expect(h[:display_name]).to eq 'display name' @@ -40,7 +40,7 @@ RSpec.describe Admin::AccountsController, type: :controller do remote: '1', by_domain: 'domain', silenced: '1', - recent: '1', + alphabetic: '1', suspended: '1', username: 'username', display_name: 'display name', diff --git a/spec/models/account_filter_spec.rb b/spec/models/account_filter_spec.rb index 8441939c5..0a0252642 100644 --- a/spec/models/account_filter_spec.rb +++ b/spec/models/account_filter_spec.rb @@ -2,10 +2,10 @@ require 'rails_helper' describe AccountFilter do describe 'with empty params' do - it 'defaults to alphabetic account list' do + it 'defaults to recent account list' do filter = described_class.new({}) - expect(filter.results).to eq Account.alphabetic + expect(filter.results).to eq Account.recent end end @@ -60,7 +60,7 @@ describe AccountFilter do end describe 'that call account methods' do - %i(local remote silenced recent suspended).each do |option| + %i(local remote silenced alphabetic 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 }) -- cgit From 0a4739c7324a96cee148373ccc7b57b9c7097b42 Mon Sep 17 00:00:00 2001 From: aus-social <42644106+aus-social@users.noreply.github.com> Date: Fri, 5 Oct 2018 01:38:04 +1000 Subject: lint pass 2 (#8878) * Code quality pass * Typofix * Update applications_controller_spec.rb * Update applications_controller_spec.rb --- config/environments/production.rb | 2 +- config/initializers/http_client_proxy.rb | 2 +- config/initializers/omniauth.rb | 2 +- config/initializers/open_uri_redirection.rb | 2 +- config/initializers/rack_attack.rb | 2 +- config/initializers/sidekiq.rb | 2 +- config/puma.rb | 2 +- .../20160314164231_add_owner_to_application.rb | 2 +- ...105224407_add_shortcode_to_media_attachments.rb | 4 +- spec/controllers/admin/statuses_controller_spec.rb | 2 +- .../concerns/export_controller_concern_spec.rb | 1 + .../settings/applications_controller_spec.rb | 64 +++++++++++----------- spec/models/account_spec.rb | 2 +- spec/models/user_spec.rb | 2 +- spec/views/stream_entries/show.html.haml_spec.rb | 2 +- 15 files changed, 47 insertions(+), 46 deletions(-) (limited to 'spec/controllers') diff --git a/config/environments/production.rb b/config/environments/production.rb index 30239671c..ed2d885b0 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -42,7 +42,7 @@ Rails.application.configure do config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX # Allow to specify public IP of reverse proxy if it's needed - config.action_dispatch.trusted_proxies = ENV['TRUSTED_PROXY_IP'].split.map { |item| IPAddr.new(item) } unless ENV['TRUSTED_PROXY_IP'].blank? + config.action_dispatch.trusted_proxies = ENV['TRUSTED_PROXY_IP'].split.map { |item| IPAddr.new(item) } if ENV['TRUSTED_PROXY_IP'].present? # Use the lowest log level to ensure availability of diagnostic information # when problems arise. diff --git a/config/initializers/http_client_proxy.rb b/config/initializers/http_client_proxy.rb index e607aff3c..9d7b16e69 100644 --- a/config/initializers/http_client_proxy.rb +++ b/config/initializers/http_client_proxy.rb @@ -6,7 +6,7 @@ Rails.application.configure do raise "No proxy host" unless proxy.host host = proxy.host - host = host[1...-1] if host[0] == '[' #for IPv6 address + host = host[1...-1] if host[0] == '[' # for IPv6 address config.x.http_client_proxy[:proxy] = { proxy_address: host, proxy_port: proxy.port, proxy_username: proxy.user, proxy_password: proxy.password }.compact end diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb index 21b58eb5b..254e751d4 100644 --- a/config/initializers/omniauth.rb +++ b/config/initializers/omniauth.rb @@ -3,7 +3,7 @@ Rails.application.config.middleware.use OmniAuth::Builder do end Devise.setup do |config| - # Devise omniauth strategies + # Devise omniauth strategies options = {} options[:redirect_at_sign_in] = ENV['OAUTH_REDIRECT_AT_SIGN_IN'] == 'true' diff --git a/config/initializers/open_uri_redirection.rb b/config/initializers/open_uri_redirection.rb index e24fdecab..ea2dcffea 100644 --- a/config/initializers/open_uri_redirection.rb +++ b/config/initializers/open_uri_redirection.rb @@ -1,7 +1,7 @@ require 'open-uri' module OpenURI - def OpenURI.redirectable?(uri1, uri2) # :nodoc: + def self.redirectable?(uri1, uri2) # :nodoc: uri1.scheme.downcase == uri2.scheme.downcase || (/\A(?:http|https|ftp)\z/i =~ uri1.scheme && /\A(?:http|https|ftp)\z/i =~ uri2.scheme) end diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb index 0ca0a7e7f..8756b8fbf 100644 --- a/config/initializers/rack_attack.rb +++ b/config/initializers/rack_attack.rb @@ -42,7 +42,7 @@ class Rack::Attack # (blocklist & throttles are skipped) Rack::Attack.safelist('allow from localhost') do |req| # Requests are allowed if the return value is truthy - '127.0.0.1' == req.ip || '::1' == req.ip + req.ip == '127.0.0.1' || req.ip == '::1' end throttle('throttle_authenticated_api', limit: 300, period: 5.minutes) do |req| diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index 05c804100..7f8a40d7b 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -19,4 +19,4 @@ Sidekiq.configure_client do |config| config.redis = redis_params end -Sidekiq::Logging.logger.level = ::Logger::const_get(ENV.fetch('RAILS_LOG_LEVEL', 'info').upcase.to_s) +Sidekiq::Logging.logger.level = ::Logger.const_get(ENV.fetch('RAILS_LOG_LEVEL', 'info').upcase.to_s) diff --git a/config/puma.rb b/config/puma.rb index 0397b8920..5ebf5ed19 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -1,7 +1,7 @@ threads_count = ENV.fetch('MAX_THREADS') { 5 }.to_i threads threads_count, threads_count -if ENV['SOCKET'] then +if ENV['SOCKET'] bind 'unix://' + ENV['SOCKET'] else port ENV.fetch('PORT') { 3000 } diff --git a/db/migrate/20160314164231_add_owner_to_application.rb b/db/migrate/20160314164231_add_owner_to_application.rb index 1919f09a1..553c18b5e 100644 --- a/db/migrate/20160314164231_add_owner_to_application.rb +++ b/db/migrate/20160314164231_add_owner_to_application.rb @@ -4,4 +4,4 @@ class AddOwnerToApplication < ActiveRecord::Migration[4.2] add_column :oauth_applications, :owner_type, :string, null: true add_index :oauth_applications, [:owner_id, :owner_type] end -end \ No newline at end of file +end diff --git a/db/migrate/20170105224407_add_shortcode_to_media_attachments.rb b/db/migrate/20170105224407_add_shortcode_to_media_attachments.rb index 2685ae150..fba46a4b6 100644 --- a/db/migrate/20170105224407_add_shortcode_to_media_attachments.rb +++ b/db/migrate/20170105224407_add_shortcode_to_media_attachments.rb @@ -8,7 +8,7 @@ class AddShortcodeToMediaAttachments < ActiveRecord::Migration[5.0] end def down - remove_index :media_attachments, :shortcode - remove_column :media_attachments, :shortcode + remove_index :media_attachments, :shortcode + remove_column :media_attachments, :shortcode end end diff --git a/spec/controllers/admin/statuses_controller_spec.rb b/spec/controllers/admin/statuses_controller_spec.rb index 6afcc1442..1a08c10b7 100644 --- a/spec/controllers/admin/statuses_controller_spec.rb +++ b/spec/controllers/admin/statuses_controller_spec.rb @@ -24,7 +24,7 @@ describe Admin::StatusesController do end it 'returns http success with media' do - get :index, params: { account_id: account.id , media: true } + get :index, params: { account_id: account.id, media: true } statuses = assigns(:statuses).to_a expect(statuses.size).to eq 1 diff --git a/spec/controllers/concerns/export_controller_concern_spec.rb b/spec/controllers/concerns/export_controller_concern_spec.rb index 6a13db69d..e5861c801 100644 --- a/spec/controllers/concerns/export_controller_concern_spec.rb +++ b/spec/controllers/concerns/export_controller_concern_spec.rb @@ -8,6 +8,7 @@ describe ApplicationController, type: :controller do def index send_export_file end + def export_data @export.account.username end diff --git a/spec/controllers/settings/applications_controller_spec.rb b/spec/controllers/settings/applications_controller_spec.rb index fd4b10071..29c278148 100644 --- a/spec/controllers/settings/applications_controller_spec.rb +++ b/spec/controllers/settings/applications_controller_spec.rb @@ -47,13 +47,13 @@ describe Settings::ApplicationsController do context 'success (passed scopes as a String)' do def call_create post :create, params: { - doorkeeper_application: { - name: 'My New App', - redirect_uri: 'urn:ietf:wg:oauth:2.0:oob', - website: 'http://google.com', - scopes: 'read write follow' - } - } + doorkeeper_application: { + name: 'My New App', + redirect_uri: 'urn:ietf:wg:oauth:2.0:oob', + website: 'http://google.com', + scopes: 'read write follow' + } + } response end @@ -69,13 +69,13 @@ describe Settings::ApplicationsController do context 'success (passed scopes as an Array)' do def call_create post :create, params: { - doorkeeper_application: { - name: 'My New App', - redirect_uri: 'urn:ietf:wg:oauth:2.0:oob', - website: 'http://google.com', - scopes: [ 'read', 'write', 'follow' ] - } - } + doorkeeper_application: { + name: 'My New App', + redirect_uri: 'urn:ietf:wg:oauth:2.0:oob', + website: 'http://google.com', + scopes: [ 'read', 'write', 'follow' ] + } + } response end @@ -91,13 +91,13 @@ describe Settings::ApplicationsController do context 'failure' do before do post :create, params: { - doorkeeper_application: { - name: '', - redirect_uri: '', - website: '', - scopes: [] - } - } + doorkeeper_application: { + name: '', + redirect_uri: '', + website: '', + scopes: [] + } + } end it 'returns http success' do @@ -120,9 +120,9 @@ describe Settings::ApplicationsController do def call_update patch :update, params: { - id: app.id, - doorkeeper_application: opts - } + id: app.id, + doorkeeper_application: opts + } response end @@ -139,14 +139,14 @@ describe Settings::ApplicationsController do context 'failure' do before do patch :update, params: { - id: app.id, - doorkeeper_application: { - name: '', - redirect_uri: '', - website: '', - scopes: [] - } - } + id: app.id, + doorkeeper_application: { + name: '', + redirect_uri: '', + website: '', + scopes: [] + } + } end it 'returns http success' do diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index ae19251ae..60d13d32e 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -275,7 +275,7 @@ RSpec.describe Account, type: :model do subject { Fabricate(:account) } - context 'when the status is a reblog of another status'do + 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) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 015e90edc..42198cb4d 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -254,7 +254,7 @@ RSpec.describe User, type: :model do it_behaves_like 'Settings-extended' do def create! - User.create!(account: Fabricate(:account), email: 'foo@mastodon.space', password: 'abcd1234' ) + User.create!(account: Fabricate(:account), email: 'foo@mastodon.space', password: 'abcd1234') end def fabricate diff --git a/spec/views/stream_entries/show.html.haml_spec.rb b/spec/views/stream_entries/show.html.haml_spec.rb index e08419596..93f0adb99 100644 --- a/spec/views/stream_entries/show.html.haml_spec.rb +++ b/spec/views/stream_entries/show.html.haml_spec.rb @@ -49,7 +49,7 @@ describe 'stream_entries/show.html.haml', without_verify_partial_doubles: true d 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(:ancestors, reply.stream_entry.activity.ancestors(1, bob)) assign(:descendant_threads, [{ statuses: reply.stream_entry.activity.descendants(1) }]) render -- cgit From 774ac473736cbf348827cf6d861e7fbbb72d7623 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 7 Oct 2018 23:44:58 +0200 Subject: Add conversations API (#8832) * Add conversations API * Add web UI for conversations * Add test for conversations API * Add tests for ConversationAccount * Improve web UI * Rename ConversationAccount to AccountConversation * Remove conversations on block and mute * Change last_status_id to be a denormalization of status_ids * Add optimistic locking --- app/controllers/api/v1/conversations_controller.rb | 55 ++++++++++ app/javascript/mastodon/actions/conversations.js | 59 +++++++++++ app/javascript/mastodon/actions/streaming.js | 4 + app/javascript/mastodon/actions/timelines.js | 1 - app/javascript/mastodon/components/display_name.js | 11 +- .../direct_timeline/components/conversation.js | 85 ++++++++++++++++ .../components/conversations_list.js | 68 +++++++++++++ .../containers/conversation_container.js | 15 +++ .../containers/conversations_list_container.js | 15 +++ .../mastodon/features/direct_timeline/index.js | 27 ++--- app/javascript/mastodon/reducers/conversations.js | 79 +++++++++++++++ app/javascript/mastodon/reducers/index.js | 2 + app/javascript/mastodon/reducers/notifications.js | 2 +- app/javascript/styles/mastodon/components.scss | 42 ++++++++ app/lib/inline_renderer.rb | 2 + app/models/account_conversation.rb | 111 +++++++++++++++++++++ app/models/status.rb | 13 +++ app/serializers/rest/conversation_serializer.rb | 7 ++ app/services/after_block_service.rb | 37 ++++++- app/services/fan_out_on_write_service.rb | 6 ++ app/services/mute_service.rb | 4 +- app/services/notify_service.rb | 22 ++-- app/workers/block_worker.rb | 5 +- app/workers/mute_worker.rb | 12 +++ app/workers/push_conversation_worker.rb | 15 +++ config/environments/development.rb | 2 +- config/routes.rb | 1 + .../20180929222014_create_account_conversations.rb | 14 +++ db/schema.rb | 17 +++- .../api/v1/conversations_controller_spec.rb | 37 +++++++ .../fabricators/conversation_account_fabricator.rb | 6 ++ spec/models/account_conversation_spec.rb | 72 +++++++++++++ streaming/index.js | 12 ++- 33 files changed, 816 insertions(+), 44 deletions(-) create mode 100644 app/controllers/api/v1/conversations_controller.rb create mode 100644 app/javascript/mastodon/actions/conversations.js create mode 100644 app/javascript/mastodon/features/direct_timeline/components/conversation.js create mode 100644 app/javascript/mastodon/features/direct_timeline/components/conversations_list.js create mode 100644 app/javascript/mastodon/features/direct_timeline/containers/conversation_container.js create mode 100644 app/javascript/mastodon/features/direct_timeline/containers/conversations_list_container.js create mode 100644 app/javascript/mastodon/reducers/conversations.js create mode 100644 app/models/account_conversation.rb create mode 100644 app/serializers/rest/conversation_serializer.rb create mode 100644 app/workers/mute_worker.rb create mode 100644 app/workers/push_conversation_worker.rb create mode 100644 db/migrate/20180929222014_create_account_conversations.rb create mode 100644 spec/controllers/api/v1/conversations_controller_spec.rb create mode 100644 spec/fabricators/conversation_account_fabricator.rb create mode 100644 spec/models/account_conversation_spec.rb (limited to 'spec/controllers') diff --git a/app/controllers/api/v1/conversations_controller.rb b/app/controllers/api/v1/conversations_controller.rb new file mode 100644 index 000000000..736cb21ca --- /dev/null +++ b/app/controllers/api/v1/conversations_controller.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +class Api::V1::ConversationsController < Api::BaseController + LIMIT = 20 + + before_action -> { doorkeeper_authorize! :read, :'read:statuses' } + before_action :require_user! + after_action :insert_pagination_headers + + respond_to :json + + def index + @conversations = paginated_conversations + render json: @conversations, each_serializer: REST::ConversationSerializer + end + + private + + def paginated_conversations + AccountConversation.where(account: current_account) + .paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def next_path + if records_continue? + api_v1_conversations_url pagination_params(max_id: pagination_max_id) + end + end + + def prev_path + unless @conversations.empty? + api_v1_conversations_url pagination_params(min_id: pagination_since_id) + end + end + + def pagination_max_id + @conversations.last.last_status_id + end + + def pagination_since_id + @conversations.first.last_status_id + end + + def records_continue? + @conversations.size == limit_param(LIMIT) + end + + def pagination_params(core_params) + params.slice(:limit).permit(:limit).merge(core_params) + end +end diff --git a/app/javascript/mastodon/actions/conversations.js b/app/javascript/mastodon/actions/conversations.js new file mode 100644 index 000000000..3840d23ca --- /dev/null +++ b/app/javascript/mastodon/actions/conversations.js @@ -0,0 +1,59 @@ +import api, { getLinks } from '../api'; +import { + importFetchedAccounts, + importFetchedStatuses, + importFetchedStatus, +} from './importer'; + +export const CONVERSATIONS_FETCH_REQUEST = 'CONVERSATIONS_FETCH_REQUEST'; +export const CONVERSATIONS_FETCH_SUCCESS = 'CONVERSATIONS_FETCH_SUCCESS'; +export const CONVERSATIONS_FETCH_FAIL = 'CONVERSATIONS_FETCH_FAIL'; +export const CONVERSATIONS_UPDATE = 'CONVERSATIONS_UPDATE'; + +export const expandConversations = ({ maxId } = {}) => (dispatch, getState) => { + dispatch(expandConversationsRequest()); + + const params = { max_id: maxId }; + + if (!maxId) { + params.since_id = getState().getIn(['conversations', 0, 'last_status']); + } + + api(getState).get('/api/v1/conversations', { params }) + .then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + + dispatch(importFetchedAccounts(response.data.reduce((aggr, item) => aggr.concat(item.accounts), []))); + dispatch(importFetchedStatuses(response.data.map(item => item.last_status).filter(x => !!x))); + dispatch(expandConversationsSuccess(response.data, next ? next.uri : null)); + }) + .catch(err => dispatch(expandConversationsFail(err))); +}; + +export const expandConversationsRequest = () => ({ + type: CONVERSATIONS_FETCH_REQUEST, +}); + +export const expandConversationsSuccess = (conversations, next) => ({ + type: CONVERSATIONS_FETCH_SUCCESS, + conversations, + next, +}); + +export const expandConversationsFail = error => ({ + type: CONVERSATIONS_FETCH_FAIL, + error, +}); + +export const updateConversations = conversation => dispatch => { + dispatch(importFetchedAccounts(conversation.accounts)); + + if (conversation.last_status) { + dispatch(importFetchedStatus(conversation.last_status)); + } + + dispatch({ + type: CONVERSATIONS_UPDATE, + conversation, + }); +}; diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js index 32fc67e67..8cf055540 100644 --- a/app/javascript/mastodon/actions/streaming.js +++ b/app/javascript/mastodon/actions/streaming.js @@ -6,6 +6,7 @@ import { disconnectTimeline, } from './timelines'; import { updateNotifications, expandNotifications } from './notifications'; +import { updateConversations } from './conversations'; import { fetchFilters } from './filters'; import { getLocale } from '../locales'; @@ -31,6 +32,9 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null) case 'notification': dispatch(updateNotifications(JSON.parse(data.payload), messages, locale)); break; + case 'conversation': + dispatch(updateConversations(JSON.parse(data.payload))); + break; case 'filters_changed': dispatch(fetchFilters()); break; diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index e8fd441e1..c4fc6448c 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -76,7 +76,6 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done); export const expandPublicTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`public${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { max_id: maxId, only_media: !!onlyMedia }, done); export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done); -export const expandDirectTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done); export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId }); export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true }); export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true }); diff --git a/app/javascript/mastodon/components/display_name.js b/app/javascript/mastodon/components/display_name.js index a1c56ae35..c3a9ab921 100644 --- a/app/javascript/mastodon/components/display_name.js +++ b/app/javascript/mastodon/components/display_name.js @@ -1,18 +1,25 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; export default class DisplayName extends React.PureComponent { static propTypes = { account: ImmutablePropTypes.map.isRequired, + withAcct: PropTypes.bool, + }; + + static defaultProps = { + withAcct: true, }; render () { - const displayNameHtml = { __html: this.props.account.get('display_name_html') }; + const { account, withAcct } = this.props; + const displayNameHtml = { __html: account.get('display_name_html') }; return ( - @{this.props.account.get('acct')} + {withAcct && @{account.get('acct')}} ); } diff --git a/app/javascript/mastodon/features/direct_timeline/components/conversation.js b/app/javascript/mastodon/features/direct_timeline/components/conversation.js new file mode 100644 index 000000000..f9a8d4f72 --- /dev/null +++ b/app/javascript/mastodon/features/direct_timeline/components/conversation.js @@ -0,0 +1,85 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import StatusContent from '../../../components/status_content'; +import RelativeTimestamp from '../../../components/relative_timestamp'; +import DisplayName from '../../../components/display_name'; +import Avatar from '../../../components/avatar'; +import AttachmentList from '../../../components/attachment_list'; +import { HotKeys } from 'react-hotkeys'; + +export default class Conversation extends ImmutablePureComponent { + + static contextTypes = { + router: PropTypes.object, + }; + + static propTypes = { + conversationId: PropTypes.string.isRequired, + accounts: ImmutablePropTypes.list.isRequired, + lastStatus: ImmutablePropTypes.map.isRequired, + onMoveUp: PropTypes.func, + onMoveDown: PropTypes.func, + }; + + handleClick = () => { + if (!this.context.router) { + return; + } + + const { lastStatus } = this.props; + this.context.router.history.push(`/statuses/${lastStatus.get('id')}`); + } + + handleHotkeyMoveUp = () => { + this.props.onMoveUp(this.props.conversationId); + } + + handleHotkeyMoveDown = () => { + this.props.onMoveDown(this.props.conversationId); + } + + render () { + const { accounts, lastStatus, lastAccount } = this.props; + + if (lastStatus === null) { + return null; + } + + const handlers = { + moveDown: this.handleHotkeyMoveDown, + moveUp: this.handleHotkeyMoveUp, + open: this.handleClick, + }; + + let media; + + if (lastStatus.get('media_attachments').size > 0) { + media = ; + } + + return ( + +
+
+
+
{accounts.map(account => )}
+
+ +
+ +
+ +
+
+ + + + {media} +
+
+ ); + } + +} diff --git a/app/javascript/mastodon/features/direct_timeline/components/conversations_list.js b/app/javascript/mastodon/features/direct_timeline/components/conversations_list.js new file mode 100644 index 000000000..4684548e0 --- /dev/null +++ b/app/javascript/mastodon/features/direct_timeline/components/conversations_list.js @@ -0,0 +1,68 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import ConversationContainer from '../containers/conversation_container'; +import ScrollableList from '../../../components/scrollable_list'; +import { debounce } from 'lodash'; + +export default class ConversationsList extends ImmutablePureComponent { + + static propTypes = { + conversationIds: ImmutablePropTypes.list.isRequired, + hasMore: PropTypes.bool, + isLoading: PropTypes.bool, + onLoadMore: PropTypes.func, + shouldUpdateScroll: PropTypes.func, + }; + + getCurrentIndex = id => this.props.conversationIds.indexOf(id) + + handleMoveUp = id => { + const elementIndex = this.getCurrentIndex(id) - 1; + this._selectChild(elementIndex); + } + + handleMoveDown = id => { + const elementIndex = this.getCurrentIndex(id) + 1; + this._selectChild(elementIndex); + } + + _selectChild (index) { + const element = this.node.node.querySelector(`article:nth-of-type(${index + 1}) .focusable`); + + if (element) { + element.focus(); + } + } + + setRef = c => { + this.node = c; + } + + handleLoadOlder = debounce(() => { + const last = this.props.conversationIds.last(); + + if (last) { + this.props.onLoadMore(last); + } + }, 300, { leading: true }) + + render () { + const { conversationIds, onLoadMore, ...other } = this.props; + + return ( + + {conversationIds.map(item => ( + + ))} + + ); + } + +} diff --git a/app/javascript/mastodon/features/direct_timeline/containers/conversation_container.js b/app/javascript/mastodon/features/direct_timeline/containers/conversation_container.js new file mode 100644 index 000000000..4166ee2ac --- /dev/null +++ b/app/javascript/mastodon/features/direct_timeline/containers/conversation_container.js @@ -0,0 +1,15 @@ +import { connect } from 'react-redux'; +import Conversation from '../components/conversation'; + +const mapStateToProps = (state, { conversationId }) => { + const conversation = state.getIn(['conversations', 'items']).find(x => x.get('id') === conversationId); + const lastStatus = state.getIn(['statuses', conversation.get('last_status')], null); + + return { + accounts: conversation.get('accounts').map(accountId => state.getIn(['accounts', accountId], null)), + lastStatus, + lastAccount: lastStatus === null ? null : state.getIn(['accounts', lastStatus.get('account')], null), + }; +}; + +export default connect(mapStateToProps)(Conversation); diff --git a/app/javascript/mastodon/features/direct_timeline/containers/conversations_list_container.js b/app/javascript/mastodon/features/direct_timeline/containers/conversations_list_container.js new file mode 100644 index 000000000..81ea812ad --- /dev/null +++ b/app/javascript/mastodon/features/direct_timeline/containers/conversations_list_container.js @@ -0,0 +1,15 @@ +import { connect } from 'react-redux'; +import ConversationsList from '../components/conversations_list'; +import { expandConversations } from '../../../actions/conversations'; + +const mapStateToProps = state => ({ + conversationIds: state.getIn(['conversations', 'items']).map(x => x.get('id')), + isLoading: state.getIn(['conversations', 'isLoading'], true), + hasMore: state.getIn(['conversations', 'hasMore'], false), +}); + +const mapDispatchToProps = dispatch => ({ + onLoadMore: maxId => dispatch(expandConversations({ maxId })), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(ConversationsList); diff --git a/app/javascript/mastodon/features/direct_timeline/index.js b/app/javascript/mastodon/features/direct_timeline/index.js index 3c7e2d007..4c8485690 100644 --- a/app/javascript/mastodon/features/direct_timeline/index.js +++ b/app/javascript/mastodon/features/direct_timeline/index.js @@ -1,23 +1,19 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; -import StatusListContainer from '../ui/containers/status_list_container'; import Column from '../../components/column'; import ColumnHeader from '../../components/column_header'; -import { expandDirectTimeline } from '../../actions/timelines'; +import { expandConversations } from '../../actions/conversations'; import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { defineMessages, injectIntl } from 'react-intl'; import { connectDirectStream } from '../../actions/streaming'; +import ConversationsListContainer from './containers/conversations_list_container'; const messages = defineMessages({ title: { id: 'column.direct', defaultMessage: 'Direct messages' }, }); -const mapStateToProps = state => ({ - hasUnread: state.getIn(['timelines', 'direct', 'unread']) > 0, -}); - -export default @connect(mapStateToProps) +export default @connect() @injectIntl class DirectTimeline extends React.PureComponent { @@ -52,7 +48,7 @@ class DirectTimeline extends React.PureComponent { componentDidMount () { const { dispatch } = this.props; - dispatch(expandDirectTimeline()); + dispatch(expandConversations()); this.disconnect = dispatch(connectDirectStream()); } @@ -68,11 +64,11 @@ class DirectTimeline extends React.PureComponent { } handleLoadMore = maxId => { - this.props.dispatch(expandDirectTimeline({ maxId })); + this.props.dispatch(expandConversations({ maxId })); } render () { - const { intl, shouldUpdateScroll, hasUnread, columnId, multiColumn } = this.props; + const { intl, hasUnread, columnId, multiColumn, shouldUpdateScroll } = this.props; const pinned = !!columnId; return ( @@ -88,14 +84,7 @@ class DirectTimeline extends React.PureComponent { multiColumn={multiColumn} /> - } - shouldUpdateScroll={shouldUpdateScroll} - /> + ); } diff --git a/app/javascript/mastodon/reducers/conversations.js b/app/javascript/mastodon/reducers/conversations.js new file mode 100644 index 000000000..f339abf56 --- /dev/null +++ b/app/javascript/mastodon/reducers/conversations.js @@ -0,0 +1,79 @@ +import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; +import { + CONVERSATIONS_FETCH_REQUEST, + CONVERSATIONS_FETCH_SUCCESS, + CONVERSATIONS_FETCH_FAIL, + CONVERSATIONS_UPDATE, +} from '../actions/conversations'; +import compareId from '../compare_id'; + +const initialState = ImmutableMap({ + items: ImmutableList(), + isLoading: false, + hasMore: true, +}); + +const conversationToMap = item => ImmutableMap({ + id: item.id, + accounts: ImmutableList(item.accounts.map(a => a.id)), + last_status: item.last_status.id, +}); + +const updateConversation = (state, item) => state.update('items', list => { + const index = list.findIndex(x => x.get('id') === item.id); + const newItem = conversationToMap(item); + + if (index === -1) { + return list.unshift(newItem); + } else { + return list.set(index, newItem); + } +}); + +const expandNormalizedConversations = (state, conversations, next) => { + let items = ImmutableList(conversations.map(conversationToMap)); + + return state.withMutations(mutable => { + if (!items.isEmpty()) { + mutable.update('items', list => { + list = list.map(oldItem => { + const newItemIndex = items.findIndex(x => x.get('id') === oldItem.get('id')); + + if (newItemIndex === -1) { + return oldItem; + } + + const newItem = items.get(newItemIndex); + items = items.delete(newItemIndex); + + return newItem; + }); + + list = list.concat(items); + + return list.sortBy(x => x.get('last_status'), (a, b) => compareId(a, b) * -1); + }); + } + + if (!next) { + mutable.set('hasMore', false); + } + + mutable.set('isLoading', false); + }); +}; + +export default function conversations(state = initialState, action) { + switch (action.type) { + case CONVERSATIONS_FETCH_REQUEST: + return state.set('isLoading', true); + case CONVERSATIONS_FETCH_FAIL: + return state.set('isLoading', false); + case CONVERSATIONS_FETCH_SUCCESS: + return expandNormalizedConversations(state, action.conversations, action.next); + case CONVERSATIONS_UPDATE: + return updateConversation(state, action.conversation); + default: + return state; + } +}; diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js index 4a981fada..d3b98d4f6 100644 --- a/app/javascript/mastodon/reducers/index.js +++ b/app/javascript/mastodon/reducers/index.js @@ -27,6 +27,7 @@ import custom_emojis from './custom_emojis'; import lists from './lists'; import listEditor from './list_editor'; import filters from './filters'; +import conversations from './conversations'; const reducers = { dropdown_menu, @@ -57,6 +58,7 @@ const reducers = { lists, listEditor, filters, + conversations, }; export default combineReducers(reducers); diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js index 0b29f19fa..d71ae00ae 100644 --- a/app/javascript/mastodon/reducers/notifications.js +++ b/app/javascript/mastodon/reducers/notifications.js @@ -69,7 +69,7 @@ const expandNormalizedNotifications = (state, notifications, next) => { } if (!next) { - mutable.set('hasMore', true); + mutable.set('hasMore', false); } mutable.set('isLoading', false); diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 90e2ed5a5..129bde856 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -825,6 +825,7 @@ &.status-direct { background: lighten($ui-base-color, 8%); + border-bottom-color: lighten($ui-base-color, 12%); } &.light { @@ -5496,3 +5497,44 @@ noscript { } } } + +.conversation { + padding: 14px 10px; + border-bottom: 1px solid lighten($ui-base-color, 8%); + cursor: pointer; + + &__header { + display: flex; + margin-bottom: 15px; + } + + &__avatars { + overflow: hidden; + flex: 1 1 auto; + + & > div { + display: flex; + flex-wrap: none; + width: 900px; + } + + .account__avatar { + margin-right: 10px; + } + } + + &__time { + flex: 0 0 auto; + font-size: 14px; + color: $darker-text-color; + text-align: right; + + .display-name { + color: $secondary-text-color; + } + } + + .attachment-list.compact { + margin-top: 15px; + } +} diff --git a/app/lib/inline_renderer.rb b/app/lib/inline_renderer.rb index 7cd9758ec..761a8822d 100644 --- a/app/lib/inline_renderer.rb +++ b/app/lib/inline_renderer.rb @@ -13,6 +13,8 @@ class InlineRenderer serializer = REST::StatusSerializer when :notification serializer = REST::NotificationSerializer + when :conversation + serializer = REST::ConversationSerializer else return end diff --git a/app/models/account_conversation.rb b/app/models/account_conversation.rb new file mode 100644 index 000000000..a7205ec1a --- /dev/null +++ b/app/models/account_conversation.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true +# == Schema Information +# +# Table name: account_conversations +# +# id :bigint(8) not null, primary key +# account_id :bigint(8) +# conversation_id :bigint(8) +# participant_account_ids :bigint(8) default([]), not null, is an Array +# status_ids :bigint(8) default([]), not null, is an Array +# last_status_id :bigint(8) +# lock_version :integer default(0), not null +# + +class AccountConversation < ApplicationRecord + after_commit :push_to_streaming_api + + belongs_to :account + belongs_to :conversation + belongs_to :last_status, class_name: 'Status' + + before_validation :set_last_status + + def participant_account_ids=(arr) + self[:participant_account_ids] = arr.sort + end + + def participant_accounts + if participant_account_ids.empty? + [account] + else + Account.where(id: participant_account_ids) + end + end + + class << self + def paginate_by_id(limit, options = {}) + if options[:min_id] + paginate_by_min_id(limit, options[:min_id]).reverse + else + paginate_by_max_id(limit, options[:max_id], options[:since_id]) + end + end + + def paginate_by_min_id(limit, min_id = nil) + query = order(arel_table[:last_status_id].asc).limit(limit) + query = query.where(arel_table[:last_status_id].gt(min_id)) if min_id.present? + query + end + + def paginate_by_max_id(limit, max_id = nil, since_id = nil) + query = order(arel_table[:last_status_id].desc).limit(limit) + query = query.where(arel_table[:last_status_id].lt(max_id)) if max_id.present? + query = query.where(arel_table[:last_status_id].gt(since_id)) if since_id.present? + query + end + + def add_status(recipient, status) + conversation = find_or_initialize_by(account: recipient, conversation_id: status.conversation_id, participant_account_ids: participants_from_status(recipient, status)) + conversation.status_ids << status.id + conversation.save + conversation + rescue ActiveRecord::StaleObjectError + retry + end + + def remove_status(recipient, status) + conversation = find_by(account: recipient, conversation_id: status.conversation_id, participant_account_ids: participants_from_status(recipient, status)) + + return if conversation.nil? + + conversation.status_ids.delete(status.id) + + if conversation.status_ids.empty? + conversation.destroy + else + conversation.save + end + + conversation + rescue ActiveRecord::StaleObjectError + retry + end + + private + + def participants_from_status(recipient, status) + ((status.mentions.pluck(:account_id) + [status.account_id]).uniq - [recipient.id]).sort + end + end + + private + + def set_last_status + self.status_ids = status_ids.sort + self.last_status_id = status_ids.last + end + + def push_to_streaming_api + return if destroyed? || !subscribed_to_timeline? + PushConversationWorker.perform_async(id) + end + + def subscribed_to_timeline? + Redis.current.exists("subscribed:#{streaming_channel}") + end + + def streaming_channel + "timeline:direct:#{account_id}" + end +end diff --git a/app/models/status.rb b/app/models/status.rb index f2b5cc6ce..f61bd0fee 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -24,6 +24,8 @@ # class Status < ApplicationRecord + before_destroy :unlink_from_conversations + include Paginable include Streamable include Cacheable @@ -473,4 +475,15 @@ class Status < ApplicationRecord reblog&.decrement_count!(:reblogs_count) if reblog? thread&.decrement_count!(:replies_count) if in_reply_to_id.present? && (public_visibility? || unlisted_visibility?) end + + def unlink_from_conversations + return unless direct_visibility? + + mentioned_accounts = mentions.includes(:account).map(&:account) + inbox_owners = mentioned_accounts.select(&:local?) + (account.local? ? [account] : []) + + inbox_owners.each do |inbox_owner| + AccountConversation.remove_status(inbox_owner, self) + end + end end diff --git a/app/serializers/rest/conversation_serializer.rb b/app/serializers/rest/conversation_serializer.rb new file mode 100644 index 000000000..08cea47d2 --- /dev/null +++ b/app/serializers/rest/conversation_serializer.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class REST::ConversationSerializer < ActiveModel::Serializer + attribute :id + has_many :participant_accounts, key: :accounts, serializer: REST::AccountSerializer + has_one :last_status, serializer: REST::StatusSerializer +end diff --git a/app/services/after_block_service.rb b/app/services/after_block_service.rb index a7dce08c7..706db0d63 100644 --- a/app/services/after_block_service.rb +++ b/app/services/after_block_service.rb @@ -2,16 +2,43 @@ class AfterBlockService < BaseService def call(account, target_account) - FeedManager.instance.clear_from_timeline(account, target_account) + clear_home_feed(account, target_account) clear_notifications(account, target_account) + clear_conversations(account, target_account) end private + def clear_home_feed(account, target_account) + FeedManager.instance.clear_from_timeline(account, target_account) + end + + def clear_conversations(account, target_account) + AccountConversation.where(account: account) + .where('? = ANY(participant_account_ids)', target_account.id) + .in_batches + .destroy_all + end + def clear_notifications(account, target_account) - Notification.where(account: account).joins(:follow).where(activity_type: 'Follow', follows: { account_id: target_account.id }).delete_all - Notification.where(account: account).joins(mention: :status).where(activity_type: 'Mention', statuses: { account_id: target_account.id }).delete_all - Notification.where(account: account).joins(:favourite).where(activity_type: 'Favourite', favourites: { account_id: target_account.id }).delete_all - Notification.where(account: account).joins(:status).where(activity_type: 'Status', statuses: { account_id: target_account.id }).delete_all + Notification.where(account: account) + .joins(:follow) + .where(activity_type: 'Follow', follows: { account_id: target_account.id }) + .delete_all + + Notification.where(account: account) + .joins(mention: :status) + .where(activity_type: 'Mention', statuses: { account_id: target_account.id }) + .delete_all + + Notification.where(account: account) + .joins(:favourite) + .where(activity_type: 'Favourite', favourites: { account_id: target_account.id }) + .delete_all + + Notification.where(account: account) + .joins(:status) + .where(activity_type: 'Status', statuses: { account_id: target_account.id }) + .delete_all end end diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index 5efd3edb2..ab520276b 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -13,6 +13,7 @@ class FanOutOnWriteService < BaseService if status.direct_visibility? deliver_to_mentioned_followers(status) deliver_to_direct_timelines(status) + deliver_to_own_conversation(status) else deliver_to_followers(status) deliver_to_lists(status) @@ -99,6 +100,11 @@ class FanOutOnWriteService < BaseService status.mentions.includes(:account).each do |mention| Redis.current.publish("timeline:direct:#{mention.account.id}", @payload) if mention.account.local? end + Redis.current.publish("timeline:direct:#{status.account.id}", @payload) if status.account.local? end + + def deliver_to_own_conversation(status) + AccountConversation.add_status(status.account, status) + end end diff --git a/app/services/mute_service.rb b/app/services/mute_service.rb index c6122a152..676804cb9 100644 --- a/app/services/mute_service.rb +++ b/app/services/mute_service.rb @@ -5,11 +5,13 @@ class MuteService < BaseService return if account.id == target_account.id mute = account.mute!(target_account, notifications: notifications) + if mute.hide_notifications? BlockWorker.perform_async(account.id, target_account.id) else - FeedManager.instance.clear_from_timeline(account, target_account) + MuteWorker.perform_async(account.id, target_account.id) end + mute end end diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb index 7d0dcc7ad..63bf8f17a 100644 --- a/app/services/notify_service.rb +++ b/app/services/notify_service.rb @@ -8,9 +8,10 @@ class NotifyService < BaseService return if recipient.user.nil? || blocked? - create_notification - push_notification if @notification.browserable? - send_email if email_enabled? + create_notification! + push_notification! if @notification.browserable? + push_to_conversation! if direct_message? + send_email! if email_enabled? rescue ActiveRecord::RecordInvalid return end @@ -100,18 +101,23 @@ class NotifyService < BaseService end end - def create_notification + def create_notification! @notification.save! end - def push_notification + def push_notification! return if @notification.activity.nil? Redis.current.publish("timeline:#{@recipient.id}", Oj.dump(event: :notification, payload: InlineRenderer.render(@notification, @recipient, :notification))) - send_push_notifications + send_push_notifications! end - def send_push_notifications + def push_to_conversation! + return if @notification.activity.nil? + AccountConversation.add_status(@recipient, @notification.target_status) + end + + def send_push_notifications! subscriptions_ids = ::Web::PushSubscription.where(user_id: @recipient.user.id) .select { |subscription| subscription.pushable?(@notification) } .map(&:id) @@ -121,7 +127,7 @@ class NotifyService < BaseService end end - def send_email + def send_email! return if @notification.activity.nil? NotificationMailer.public_send(@notification.type, @recipient, @notification).deliver_later(wait: 2.minutes) end diff --git a/app/workers/block_worker.rb b/app/workers/block_worker.rb index 0820490d3..25f5dd808 100644 --- a/app/workers/block_worker.rb +++ b/app/workers/block_worker.rb @@ -4,6 +4,9 @@ class BlockWorker include Sidekiq::Worker def perform(account_id, target_account_id) - AfterBlockService.new.call(Account.find(account_id), Account.find(target_account_id)) + AfterBlockService.new.call( + Account.find(account_id), + Account.find(target_account_id) + ) end end diff --git a/app/workers/mute_worker.rb b/app/workers/mute_worker.rb new file mode 100644 index 000000000..7bf0923a5 --- /dev/null +++ b/app/workers/mute_worker.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class MuteWorker + include Sidekiq::Worker + + def perform(account_id, target_account_id) + FeedManager.instance.clear_from_timeline( + Account.find(account_id), + Account.find(target_account_id) + ) + end +end diff --git a/app/workers/push_conversation_worker.rb b/app/workers/push_conversation_worker.rb new file mode 100644 index 000000000..16f538215 --- /dev/null +++ b/app/workers/push_conversation_worker.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class PushConversationWorker + include Sidekiq::Worker + + def perform(conversation_account_id) + conversation = AccountConversation.find(conversation_account_id) + message = InlineRenderer.render(conversation, conversation.account, :conversation) + timeline_id = "timeline:direct:#{conversation.account_id}" + + Redis.current.publish(timeline_id, Oj.dump(event: :conversation, payload: message, queued_at: (Time.now.to_f * 1000.0).to_i)) + rescue ActiveRecord::RecordNotFound + true + end +end diff --git a/config/environments/development.rb b/config/environments/development.rb index b6478f16e..0791b82ab 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -87,7 +87,7 @@ Rails.application.configure do config.x.otp_secret = ENV.fetch('OTP_SECRET', '1fc2b87989afa6351912abeebe31ffc5c476ead9bf8b3d74cbc4a302c7b69a45b40b1bbef3506ddad73e942e15ed5ca4b402bf9a66423626051104f4b5f05109') end -ActiveRecordQueryTrace.enabled = ENV.fetch('QUERY_TRACE_ENABLED') { false } +ActiveRecordQueryTrace.enabled = ENV['QUERY_TRACE_ENABLED'] == 'true' module PrivateAddressCheck def self.private_address?(*) diff --git a/config/routes.rb b/config/routes.rb index 50c1d57fc..a2468c9bd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -261,6 +261,7 @@ Rails.application.routes.draw do resources :streaming, only: [:index] resources :custom_emojis, only: [:index] resources :suggestions, only: [:index, :destroy] + resources :conversations, only: [:index] get '/search', to: 'search#index', as: :search diff --git a/db/migrate/20180929222014_create_account_conversations.rb b/db/migrate/20180929222014_create_account_conversations.rb new file mode 100644 index 000000000..53fa137e1 --- /dev/null +++ b/db/migrate/20180929222014_create_account_conversations.rb @@ -0,0 +1,14 @@ +class CreateAccountConversations < ActiveRecord::Migration[5.2] + def change + create_table :account_conversations do |t| + t.belongs_to :account, foreign_key: { on_delete: :cascade } + t.belongs_to :conversation, foreign_key: { on_delete: :cascade } + t.bigint :participant_account_ids, array: true, null: false, default: [] + t.bigint :status_ids, array: true, null: false, default: [] + t.bigint :last_status_id, null: true, default: nil + t.integer :lock_version, null: false, default: 0 + end + + add_index :account_conversations, [:account_id, :conversation_id, :participant_account_ids], unique: true, name: 'index_unique_conversations' + end +end diff --git a/db/schema.rb b/db/schema.rb index 577296a6a..1458bd70f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,10 +10,23 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2018_08_20_232245) do +ActiveRecord::Schema.define(version: 2018_09_29_222014) do + # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" + create_table "account_conversations", force: :cascade do |t| + t.bigint "account_id" + t.bigint "conversation_id" + t.bigint "participant_account_ids", default: [], null: false, array: true + t.bigint "status_ids", default: [], null: false, array: true + t.bigint "last_status_id" + t.integer "lock_version", default: 0, null: false + t.index ["account_id", "conversation_id", "participant_account_ids"], name: "index_unique_conversations", unique: true + t.index ["account_id"], name: "index_account_conversations_on_account_id" + t.index ["conversation_id"], name: "index_account_conversations_on_conversation_id" + end + create_table "account_domain_blocks", force: :cascade do |t| t.string "domain" t.datetime "created_at", null: false @@ -597,6 +610,8 @@ ActiveRecord::Schema.define(version: 2018_08_20_232245) do t.index ["user_id"], name: "index_web_settings_on_user_id", unique: true end + add_foreign_key "account_conversations", "accounts", on_delete: :cascade + add_foreign_key "account_conversations", "conversations", on_delete: :cascade add_foreign_key "account_domain_blocks", "accounts", name: "fk_206c6029bd", on_delete: :cascade add_foreign_key "account_moderation_notes", "accounts" add_foreign_key "account_moderation_notes", "accounts", column: "target_account_id" diff --git a/spec/controllers/api/v1/conversations_controller_spec.rb b/spec/controllers/api/v1/conversations_controller_spec.rb new file mode 100644 index 000000000..2e9525855 --- /dev/null +++ b/spec/controllers/api/v1/conversations_controller_spec.rb @@ -0,0 +1,37 @@ +require 'rails_helper' + +RSpec.describe Api::V1::ConversationsController, type: :controller do + render_views + + let!(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + let(:other) { Fabricate(:user, account: Fabricate(:account, username: 'bob')) } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + describe 'GET #index' do + let(:scopes) { 'read:statuses' } + + before do + PostStatusService.new.call(other.account, 'Hey @alice', nil, visibility: 'direct') + end + + it 'returns http success' do + get :index + expect(response).to have_http_status(200) + end + + it 'returns pagination headers' do + get :index, params: { limit: 1 } + expect(response.headers['Link'].links.size).to eq(2) + end + + it 'returns conversations' do + get :index + json = body_as_json + expect(json.size).to eq 1 + end + end +end diff --git a/spec/fabricators/conversation_account_fabricator.rb b/spec/fabricators/conversation_account_fabricator.rb new file mode 100644 index 000000000..f57ffd535 --- /dev/null +++ b/spec/fabricators/conversation_account_fabricator.rb @@ -0,0 +1,6 @@ +Fabricator(:conversation_account) do + account nil + conversation nil + participant_account_ids "" + last_status nil +end diff --git a/spec/models/account_conversation_spec.rb b/spec/models/account_conversation_spec.rb new file mode 100644 index 000000000..70a76281e --- /dev/null +++ b/spec/models/account_conversation_spec.rb @@ -0,0 +1,72 @@ +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 diff --git a/streaming/index.js b/streaming/index.js index 1c6004b77..debf7c8bf 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -485,7 +485,8 @@ const startWorker = (workerId) => { }); app.get('/api/v1/streaming/direct', (req, res) => { - streamFrom(`timeline:direct:${req.accountId}`, req, streamToHttp(req, res), streamHttpEnd(req), true); + 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) => { @@ -525,9 +526,11 @@ const startWorker = (workerId) => { ws.isAlive = true; }); + let channel; + switch(location.query.stream) { case 'user': - const channel = `timeline:${req.accountId}`; + channel = `timeline:${req.accountId}`; streamFrom(channel, req, streamToWs(req, ws), streamWsEnd(req, ws, subscriptionHeartbeat(channel))); break; case 'user:notification': @@ -546,7 +549,8 @@ const startWorker = (workerId) => { streamFrom('timeline:public:local:media', req, streamToWs(req, ws), streamWsEnd(req, ws), true); break; case 'direct': - streamFrom(`timeline:direct:${req.accountId}`, req, streamToWs(req, ws), streamWsEnd(req, ws), true); + channel = `timeline:direct:${req.accountId}`; + streamFrom(channel, req, streamToWs(req, ws), streamWsEnd(req, ws, subscriptionHeartbeat(channel)), true); break; case 'hashtag': streamFrom(`timeline:hashtag:${location.query.tag.toLowerCase()}`, req, streamToWs(req, ws), streamWsEnd(req, ws), true); @@ -563,7 +567,7 @@ const startWorker = (workerId) => { return; } - const channel = `timeline:list:${listId}`; + channel = `timeline:list:${listId}`; streamFrom(channel, req, streamToWs(req, ws), streamWsEnd(req, ws, subscriptionHeartbeat(channel))); }); break; -- cgit From f194857ac9eeb85f9b27c056c556038aee23cb43 Mon Sep 17 00:00:00 2001 From: ashleyhull-versent Date: Mon, 8 Oct 2018 13:50:11 +1100 Subject: rubocop issues - Cleaning up (#8912) * cleanup pass * undo mistakes * fixed. * revert --- app/models/concerns/omniauthable.rb | 4 ++-- config/initializers/open_uri_redirection.rb | 2 +- lib/mastodon/migration_helpers.rb | 10 ++++----- spec/controllers/api/salmon_controller_spec.rb | 4 ++-- .../api/subscriptions_controller_spec.rb | 2 +- spec/fabricators/user_fabricator.rb | 2 +- spec/features/log_in_spec.rb | 2 +- spec/lib/ostatus/atom_serializer_spec.rb | 24 +++++++++++----------- spec/models/status_spec.rb | 2 +- spec/models/user_spec.rb | 2 +- spec/rails_helper.rb | 4 ++-- .../services/batched_remove_status_service_spec.rb | 2 +- spec/services/fetch_remote_account_service_spec.rb | 2 +- spec/services/process_feed_service_spec.rb | 2 +- .../services/update_remote_profile_service_spec.rb | 2 +- 15 files changed, 33 insertions(+), 33 deletions(-) (limited to 'spec/controllers') diff --git a/app/models/concerns/omniauthable.rb b/app/models/concerns/omniauthable.rb index 50288e700..f263fe7af 100644 --- a/app/models/concerns/omniauthable.rb +++ b/app/models/concerns/omniauthable.rb @@ -26,7 +26,7 @@ module Omniauthable # to prevent the identity being locked with accidentally created accounts. # Note that this may leave zombie accounts (with no associated identity) which # can be cleaned up at a later date. - user = signed_in_resource ? signed_in_resource : identity.user + user = signed_in_resource || identity.user user = create_for_oauth(auth) if user.nil? if identity.user.nil? @@ -61,7 +61,7 @@ module Omniauthable display_name = auth.info.full_name || [auth.info.first_name, auth.info.last_name].join(' ') { - email: email ? email : "#{TEMP_EMAIL_PREFIX}-#{auth.uid}-#{auth.provider}.com", + email: email || "#{TEMP_EMAIL_PREFIX}-#{auth.uid}-#{auth.provider}.com", password: Devise.friendly_token[0, 20], account_attributes: { username: ensure_unique_username(auth.uid), diff --git a/config/initializers/open_uri_redirection.rb b/config/initializers/open_uri_redirection.rb index ea2dcffea..e9de85bdc 100644 --- a/config/initializers/open_uri_redirection.rb +++ b/config/initializers/open_uri_redirection.rb @@ -2,7 +2,7 @@ require 'open-uri' module OpenURI def self.redirectable?(uri1, uri2) # :nodoc: - uri1.scheme.downcase == uri2.scheme.downcase || + uri1.scheme.casecmp(uri2.scheme).zero? || (/\A(?:http|https|ftp)\z/i =~ uri1.scheme && /\A(?:http|https|ftp)\z/i =~ uri2.scheme) end end diff --git a/lib/mastodon/migration_helpers.rb b/lib/mastodon/migration_helpers.rb index 5c135685f..f5dc7e1c6 100644 --- a/lib/mastodon/migration_helpers.rb +++ b/lib/mastodon/migration_helpers.rb @@ -342,8 +342,8 @@ module Mastodon say "Migrating #{table_name}.#{column} (~#{total.to_i} rows)" - started_time = Time.now - last_time = Time.now + started_time = Time.zone.now + last_time = Time.zone.now migrated = 0 loop do stop_row = nil @@ -375,13 +375,13 @@ module Mastodon end migrated += batch_size - if Time.now - last_time > 1 + if Time.zone.now - last_time > 1 status = "Migrated #{migrated} rows" percentage = 100.0 * migrated / total status += " (~#{sprintf('%.2f', percentage)}%, " - remaining_time = (100.0 - percentage) * (Time.now - started_time) / percentage + remaining_time = (100.0 - percentage) * (Time.zone.now - started_time) / percentage status += "#{(remaining_time / 60).to_i}:" status += sprintf('%02d', remaining_time.to_i % 60) @@ -397,7 +397,7 @@ module Mastodon status += ')' say status, true - last_time = Time.now + last_time = Time.zone.now end # There are no more rows left to update. diff --git a/spec/controllers/api/salmon_controller_spec.rb b/spec/controllers/api/salmon_controller_spec.rb index 8ce4913a5..235a29af0 100644 --- a/spec/controllers/api/salmon_controller_spec.rb +++ b/spec/controllers/api/salmon_controller_spec.rb @@ -15,7 +15,7 @@ RSpec.describe Api::SalmonController, type: :controller do describe 'POST #update' do context 'with valid post data' do before do - post :update, params: { id: account.id }, body: File.read(File.join(Rails.root, 'spec', 'fixtures', 'salmon', 'mention.xml')) + post :update, params: { id: account.id }, body: File.read(Rails.root.join('spec', 'fixtures', 'salmon', 'mention.xml')) end it 'contains XML in the request body' do @@ -54,7 +54,7 @@ RSpec.describe Api::SalmonController, type: :controller do service = double(call: false) allow(VerifySalmonService).to receive(:new).and_return(service) - post :update, params: { id: account.id }, body: File.read(File.join(Rails.root, 'spec', 'fixtures', 'salmon', 'mention.xml')) + post :update, params: { id: account.id }, body: File.read(Rails.root.join('spec', 'fixtures', 'salmon', 'mention.xml')) end it 'returns http client error' do diff --git a/spec/controllers/api/subscriptions_controller_spec.rb b/spec/controllers/api/subscriptions_controller_spec.rb index b46971a54..7a4252fe6 100644 --- a/spec/controllers/api/subscriptions_controller_spec.rb +++ b/spec/controllers/api/subscriptions_controller_spec.rb @@ -33,7 +33,7 @@ RSpec.describe Api::SubscriptionsController, type: :controller do end describe 'POST #update' do - let(:feed) { File.read(File.join(Rails.root, 'spec', 'fixtures', 'push', 'feed.atom')) } + let(:feed) { File.read(Rails.root.join('spec', 'fixtures', 'push', 'feed.atom')) } before do stub_request(:post, "https://quitter.no/main/push/hub").to_return(:status => 200, :body => "", :headers => {}) diff --git a/spec/fabricators/user_fabricator.rb b/spec/fabricators/user_fabricator.rb index cf51fe81d..7dfbdb52d 100644 --- a/spec/fabricators/user_fabricator.rb +++ b/spec/fabricators/user_fabricator.rb @@ -2,5 +2,5 @@ Fabricator(:user) do account email { sequence(:email) { |i| "#{i}#{Faker::Internet.email}" } } password "123456789" - confirmed_at { Time.now } + confirmed_at { Time.zone.now } end diff --git a/spec/features/log_in_spec.rb b/spec/features/log_in_spec.rb index ed626d880..53a1f9b12 100644 --- a/spec/features/log_in_spec.rb +++ b/spec/features/log_in_spec.rb @@ -3,7 +3,7 @@ require "rails_helper" feature "Log in" do given(:email) { "test@examle.com" } given(:password) { "password" } - given(:confirmed_at) { Time.now } + given(:confirmed_at) { Time.zone.now } background do Fabricate(:user, email: email, password: password, confirmed_at: confirmed_at) diff --git a/spec/lib/ostatus/atom_serializer_spec.rb b/spec/lib/ostatus/atom_serializer_spec.rb index 3bc4b7efb..891871c1c 100644 --- a/spec/lib/ostatus/atom_serializer_spec.rb +++ b/spec/lib/ostatus/atom_serializer_spec.rb @@ -728,9 +728,9 @@ RSpec.describe OStatus::AtomSerializer do it 'appends id element with unique tag' do block = Fabricate(:block) - time_before = Time.now + time_before = Time.zone.now block_salmon = OStatus::AtomSerializer.new.block_salmon(block) - time_after = Time.now + time_after = Time.zone.now expect(block_salmon.id.text).to( eq(OStatus::TagManager.instance.unique_tag(time_before.utc, block.id, 'Block')) @@ -815,9 +815,9 @@ RSpec.describe OStatus::AtomSerializer do it 'appends id element with unique tag' do block = Fabricate(:block) - time_before = Time.now + time_before = Time.zone.now unblock_salmon = OStatus::AtomSerializer.new.unblock_salmon(block) - time_after = Time.now + time_after = Time.zone.now expect(unblock_salmon.id.text).to( eq(OStatus::TagManager.instance.unique_tag(time_before.utc, block.id, 'Block')) @@ -994,9 +994,9 @@ RSpec.describe OStatus::AtomSerializer do it 'appends id element with unique tag' do favourite = Fabricate(:favourite) - time_before = Time.now + time_before = Time.zone.now unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite) - time_after = Time.now + time_after = Time.zone.now expect(unfavourite_salmon.id.text).to( eq(OStatus::TagManager.instance.unique_tag(time_before.utc, favourite.id, 'Favourite')) @@ -1179,9 +1179,9 @@ RSpec.describe OStatus::AtomSerializer do follow = Fabricate(:follow) follow.destroy! - time_before = Time.now + time_before = Time.zone.now unfollow_salmon = OStatus::AtomSerializer.new.unfollow_salmon(follow) - time_after = Time.now + time_after = Time.zone.now expect(unfollow_salmon.id.text).to( eq(OStatus::TagManager.instance.unique_tag(time_before.utc, follow.id, 'Follow')) @@ -1327,9 +1327,9 @@ RSpec.describe OStatus::AtomSerializer do it 'appends id element with unique tag' do follow_request = Fabricate(:follow_request) - time_before = Time.now + time_before = Time.zone.now authorize_follow_request_salmon = OStatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request) - time_after = Time.now + 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')) @@ -1396,9 +1396,9 @@ RSpec.describe OStatus::AtomSerializer do it 'appends id element with unique tag' do follow_request = Fabricate(:follow_request) - time_before = Time.now + time_before = Time.zone.now reject_follow_request_salmon = OStatus::AtomSerializer.new.reject_follow_request_salmon(follow_request) - time_after = Time.now + 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')) diff --git a/spec/models/status_spec.rb b/spec/models/status_spec.rb index 9d8670129..e8cf18af9 100644 --- a/spec/models/status_spec.rb +++ b/spec/models/status_spec.rb @@ -154,7 +154,7 @@ RSpec.describe Status, type: :model do describe '#target' do it 'returns nil if the status is self-contained' do - expect(subject.target).to be_nil + expect(subject.target).to be_nil end it 'returns nil if the status is a reply' do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 42198cb4d..8c6778edc 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -67,7 +67,7 @@ RSpec.describe User, type: :model do 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.now) + user_2 = Fabricate(:user, confirmed_at: Time.zone.now) expect(User.confirmed).to match_array([user_2]) end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 79e80220c..1ded751ab 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -72,11 +72,11 @@ RSpec::Sidekiq.configure do |config| end def request_fixture(name) - File.read(File.join(Rails.root, 'spec', 'fixtures', 'requests', name)) + File.read(Rails.root.join('spec', 'fixtures', 'requests', name)) end def attachment_fixture(name) - File.open(File.join(Rails.root, 'spec', 'fixtures', 'files', name)) + File.open(Rails.root.join('spec', 'fixtures', 'files', name)) end def stub_jsonld_contexts! diff --git a/spec/services/batched_remove_status_service_spec.rb b/spec/services/batched_remove_status_service_spec.rb index 23c122e59..c66214555 100644 --- a/spec/services/batched_remove_status_service_spec.rb +++ b/spec/services/batched_remove_status_service_spec.rb @@ -19,7 +19,7 @@ RSpec.describe BatchedRemoveStatusService, type: :service do 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.now) + jeff.user.update(current_sign_in_at: Time.zone.now) jeff.follow!(alice) hank.follow!(alice) diff --git a/spec/services/fetch_remote_account_service_spec.rb b/spec/services/fetch_remote_account_service_spec.rb index 20dd505d0..3cd86708b 100644 --- a/spec/services/fetch_remote_account_service_spec.rb +++ b/spec/services/fetch_remote_account_service_spec.rb @@ -19,7 +19,7 @@ RSpec.describe FetchRemoteAccountService, type: :service do end let(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } } - let(:xml) { File.read(File.join(Rails.root, 'spec', 'fixtures', 'xml', 'mastodon.atom')) } + let(:xml) { File.read(Rails.root.join('spec', 'fixtures', 'xml', 'mastodon.atom')) } shared_examples 'return Account' do it { is_expected.to be_an Account } diff --git a/spec/services/process_feed_service_spec.rb b/spec/services/process_feed_service_spec.rb index 1f26660ed..9d3465f3f 100644 --- a/spec/services/process_feed_service_spec.rb +++ b/spec/services/process_feed_service_spec.rb @@ -4,7 +4,7 @@ RSpec.describe ProcessFeedService, type: :service do subject { ProcessFeedService.new } describe 'processing a feed' do - let(:body) { File.read(File.join(Rails.root, 'spec', 'fixtures', 'xml', 'mastodon.atom')) } + let(:body) { File.read(Rails.root.join('spec', 'fixtures', 'xml', 'mastodon.atom')) } let(:account) { Fabricate(:account, username: 'localhost', domain: 'kickass.zone') } before do diff --git a/spec/services/update_remote_profile_service_spec.rb b/spec/services/update_remote_profile_service_spec.rb index 7ac3a809a..f3ea70b80 100644 --- a/spec/services/update_remote_profile_service_spec.rb +++ b/spec/services/update_remote_profile_service_spec.rb @@ -1,7 +1,7 @@ require 'rails_helper' RSpec.describe UpdateRemoteProfileService, type: :service do - let(:xml) { File.read(File.join(Rails.root, 'spec', 'fixtures', 'push', 'feed.atom')) } + let(:xml) { File.read(Rails.root.join('spec', 'fixtures', 'push', 'feed.atom')) } subject { UpdateRemoteProfileService.new } -- cgit From fac529975b2b97e4c57393560d3359a98f7b1953 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 12 Oct 2018 00:15:55 +0200 Subject: Improve signature verification safeguards (#8959) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Downcase signed_headers string before building the signed string The HTTP Signatures draft does not mandate the “headers” field to be downcased, but mandates the header field names to be downcased in the signed string, which means that prior to this patch, Mastodon could fail to process signatures from some compliant clients. It also means that it would not actually check the Digest of non-compliant clients that wouldn't use a lowercased Digest field name. Thankfully, I don't know of any such client. * Revert "Remove dead code (#8919)" This reverts commit a00ce8c92c06f42109aad5cfe65d46862cf037bb. * Restore time window checking, change it to 12 hours By checking the Date header, we can prevent replaying old vulnerable signatures. The focus is to prevent replaying old vulnerable requests from software that has been fixed in the meantime, so a somewhat long window should be fine and accounts for timezone misconfiguration. * Escape users' URLs when formatting them Fixes possible HTML injection * Escape all string interpolations in Formatter class Slightly improve performance by reducing class allocations from repeated Formatter#encode calls * Fix code style issues --- app/controllers/concerns/signature_verification.rb | 18 +++++++++++++++- app/lib/formatter.rb | 16 +++++++++------ .../concerns/signature_verification_spec.rb | 24 ++++++++++++++++++++++ 3 files changed, 51 insertions(+), 7 deletions(-) (limited to 'spec/controllers') diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb index 5f95fa346..e5d5e2ca6 100644 --- a/app/controllers/concerns/signature_verification.rb +++ b/app/controllers/concerns/signature_verification.rb @@ -22,6 +22,12 @@ module SignatureVerification return end + if request.headers['Date'].present? && !matches_time_window? + @signature_verification_failure_reason = 'Signed request date outside acceptable time window' + @signed_request_account = nil + return + end + raw_signature = request.headers['Signature'] signature_params = {} @@ -76,7 +82,7 @@ module SignatureVerification def build_signed_string(signed_headers) signed_headers = 'date' if signed_headers.blank? - signed_headers.split(' ').map do |signed_header| + signed_headers.downcase.split(' ').map do |signed_header| if signed_header == Request::REQUEST_TARGET "#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}" elsif signed_header == 'digest' @@ -87,6 +93,16 @@ module SignatureVerification end.join("\n") end + def matches_time_window? + begin + time_sent = Time.httpdate(request.headers['Date']) + rescue ArgumentError + return false + end + + (Time.now.utc - time_sent).abs <= 12.hours + end + def body_digest "SHA-256=#{Digest::SHA256.base64digest(request_body)}" end diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index 8b694536c..35d5a09b7 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -90,8 +90,12 @@ class Formatter private + def html_entities + @html_entities ||= HTMLEntities.new + end + def encode(html) - HTMLEntities.new.encode(html) + html_entities.encode(html) end def encode_and_link_urls(html, accounts = nil, options = {}) @@ -143,7 +147,7 @@ class Formatter emoji = emoji_map[shortcode] if emoji - replacement = "\":#{shortcode}:\"" + replacement = "\":#{encode(shortcode)}:\"" before_html = shortname_start_index.positive? ? html[0..shortname_start_index - 1] : '' html = before_html + replacement + html[i + 1..-1] i += replacement.size - (shortcode.size + 2) - 1 @@ -212,7 +216,7 @@ class Formatter return link_to_account(acct) unless linkable_accounts account = linkable_accounts.find { |item| TagManager.instance.same_acct?(item.acct, acct) } - account ? mention_html(account) : "@#{acct}" + account ? mention_html(account) : "@#{encode(acct)}" end def link_to_account(acct) @@ -221,7 +225,7 @@ class Formatter domain = nil if TagManager.instance.local_domain?(domain) account = EntityCache.instance.mention(username, domain) - account ? mention_html(account) : "@#{acct}" + account ? mention_html(account) : "@#{encode(acct)}" end def link_to_hashtag(entity) @@ -239,10 +243,10 @@ class Formatter end def hashtag_html(tag) - "##{tag}" + "##{encode(tag)}" end def mention_html(account) - "@#{account.username}" + "@#{encode(account.username)}" end end diff --git a/spec/controllers/concerns/signature_verification_spec.rb b/spec/controllers/concerns/signature_verification_spec.rb index 3daf1fc4e..720690097 100644 --- a/spec/controllers/concerns/signature_verification_spec.rb +++ b/spec/controllers/concerns/signature_verification_spec.rb @@ -73,6 +73,30 @@ describe ApplicationController, type: :controller do end end + context 'with request older than a day' do + before do + get :success + + fake_request = Request.new(:get, request.url) + fake_request.add_headers({ 'Date' => 2.days.ago.utc.httpdate }) + fake_request.on_behalf_of(author) + + request.headers.merge!(fake_request.headers) + end + + describe '#signed_request?' do + it 'returns true' do + expect(controller.signed_request?).to be true + end + end + + describe '#signed_request_account' do + it 'returns nil' do + expect(controller.signed_request_account).to be_nil + end + end + end + context 'with body' do before do post :success, body: 'Hello world' -- cgit