diff --git a/app/models/concerns/user/activity.rb b/app/models/concerns/user/activity.rb new file mode 100644 index 00000000000..df2713415b3 --- /dev/null +++ b/app/models/concerns/user/activity.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module User::Activity + extend ActiveSupport::Concern + + # The home and list feeds will be stored for this amount of time, and status + # fan-out to followers will include only people active within this time frame. + # + # Lowering the duration may improve performance if many people sign up, but + # most will not check their feed every day. Raising the duration reduces the + # amount of background processing that happens when people become active. + ACTIVE_DURATION = ENV.fetch('USER_ACTIVE_DAYS', 7).to_i.days + + included do + scope :signed_in_recently, -> { where(current_sign_in_at: ACTIVE_DURATION.ago..) } + scope :not_signed_in_recently, -> { where(current_sign_in_at: ...ACTIVE_DURATION.ago) } + end + + def signed_in_recently? + current_sign_in_at.present? && current_sign_in_at >= ACTIVE_DURATION.ago + end + + private + + def inactive_since_duration? + last_sign_in_at < ACTIVE_DURATION.ago + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 669b51c0698..7da5876af19 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -58,21 +58,13 @@ class User < ApplicationRecord include LanguagesHelper include Redisable + include User::Activity include User::Confirmation include User::HasSettings include User::LdapAuthenticable include User::Omniauthable include User::PamAuthenticable - # The home and list feeds will be stored in Redis for this amount - # of time, and status fan-out to followers will include only people - # within this time frame. Lowering the duration may improve performance - # if lots of people sign up, but not a lot of them check their feed - # every day. Raising the duration reduces the amount of expensive - # RegenerationWorker jobs that need to be run when those people come - # to check their feed - ACTIVE_DURATION = ENV.fetch('USER_ACTIVE_DAYS', 7).to_i.days.freeze - devise :two_factor_authenticatable, otp_secret_length: 32 @@ -122,8 +114,6 @@ class User < ApplicationRecord scope :enabled, -> { where(disabled: false) } scope :disabled, -> { where(disabled: true) } scope :active, -> { confirmed.signed_in_recently.account_not_suspended } - scope :signed_in_recently, -> { where(current_sign_in_at: ACTIVE_DURATION.ago..) } - scope :not_signed_in_recently, -> { where(current_sign_in_at: ...ACTIVE_DURATION.ago) } scope :matches_email, ->(value) { where(arel_table[:email].matches("#{value}%")) } scope :matches_ip, ->(value) { left_joins(:ips).merge(IpBlock.contained_by(value)).group(users: [:id]) } @@ -179,10 +169,6 @@ class User < ApplicationRecord end end - def signed_in_recently? - current_sign_in_at.present? && current_sign_in_at >= ACTIVE_DURATION.ago - end - def invited? invite_id.present? end @@ -511,7 +497,7 @@ class User < ApplicationRecord return unless confirmed? ActivityTracker.record('activity:logins', id) - regenerate_feed! if needs_feed_update? + regenerate_feed! if inactive_since_duration? end def notify_staff_about_pending_account! @@ -530,10 +516,6 @@ class User < ApplicationRecord RegenerationWorker.perform_async(account_id) end - def needs_feed_update? - last_sign_in_at < ACTIVE_DURATION.ago - end - def validate_email_dns? email_changed? && !external? && !self.class.skip_mx_check? end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 438937b1001..32c9b7f9f8f 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -10,6 +10,7 @@ RSpec.describe User do let(:account) { Fabricate(:account, username: 'alice') } it_behaves_like 'two_factor_backupable' + it_behaves_like 'User::Activity' it_behaves_like 'User::Confirmation' describe 'otp_secret' do @@ -66,26 +67,6 @@ RSpec.describe User do end end - describe 'signed_in_recently' do - it 'returns a relation of users who have signed in during the recent period' do - recent_sign_in_user = Fabricate(:user, current_sign_in_at: within_duration_window_days.ago) - Fabricate(:user, current_sign_in_at: exceed_duration_window_days.ago) - - expect(described_class.signed_in_recently) - .to contain_exactly(recent_sign_in_user) - end - end - - describe 'not_signed_in_recently' do - it 'returns a relation of users who have not signed in during the recent period' do - no_recent_sign_in_user = Fabricate(:user, current_sign_in_at: exceed_duration_window_days.ago) - Fabricate(:user, current_sign_in_at: within_duration_window_days.ago) - - expect(described_class.not_signed_in_recently) - .to contain_exactly(no_recent_sign_in_user) - end - end - describe 'account_not_suspended' do it 'returns with linked accounts that are not suspended' do suspended_account = Fabricate(:account, suspended_at: 10.days.ago) @@ -120,14 +101,6 @@ RSpec.describe User do expect(described_class.matches_ip('2160:2160::/32')).to contain_exactly(user1) end end - - def exceed_duration_window_days - described_class::ACTIVE_DURATION + 2.days - end - - def within_duration_window_days - described_class::ACTIVE_DURATION - 2.days - end end describe 'email domains denylist integration' do diff --git a/spec/support/examples/models/concerns/user/activity.rb b/spec/support/examples/models/concerns/user/activity.rb new file mode 100644 index 00000000000..7e647b694a9 --- /dev/null +++ b/spec/support/examples/models/concerns/user/activity.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.shared_examples 'User::Activity' do + before { stub_const 'User::ACTIVE_DURATION', 7.days } + + describe 'Scopes' do + let!(:recent_sign_in_user) { Fabricate(:user, current_sign_in_at: 2.days.ago) } + let!(:no_recent_sign_in_user) { Fabricate(:user, current_sign_in_at: 10.days.ago) } + + describe '.signed_in_recently' do + it 'returns users who have signed in during the recent period' do + expect(described_class.signed_in_recently) + .to contain_exactly(recent_sign_in_user) + end + end + + describe '.not_signed_in_recently' do + it 'returns users who have not signed in during the recent period' do + expect(described_class.not_signed_in_recently) + .to contain_exactly(no_recent_sign_in_user) + end + end + end + + describe '#signed_in_recently?' do + subject { Fabricate.build :user, current_sign_in_at: } + + context 'when current_sign_in_at is nil' do + let(:current_sign_in_at) { nil } + + it { is_expected.to_not be_signed_in_recently } + end + + context 'when current_sign_in_at is before the threshold' do + let(:current_sign_in_at) { 10.days.ago } + + it { is_expected.to_not be_signed_in_recently } + end + + context 'when current_sign_in_at is after the threshold' do + let(:current_sign_in_at) { 2.days.ago } + + it { is_expected.to be_signed_in_recently } + end + end +end