Extract User::Activity concern (#35581)

This commit is contained in:
Matt Jankowski 2025-07-30 07:19:11 -04:00 committed by GitHub
parent 4042bc959b
commit 7e6b134222
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 79 additions and 48 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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