Files
once-campfire/app/models/user.rb
T
Marcello Costagliola 462eff10df Reset former members' connections and grant open rooms in one statement
Deleting the memberships with delete_all skips Membership's after_destroy_commit, which resets a member's
connections when one membership is revoked. Until the job ran, a member who had the room open kept its
streams, and a message that still landed in the room (a request already past the membership check, a bot's
reply) reached them. The request now reads the members' ids in the transaction that deletes their
memberships, and the job resets their connections before it destroys the messages. It costs a Redis round
trip per member, about 1.5 s for 10,000, so it's done in the job rather than in the request.

User#grant_membership_to_open_rooms read the open rooms and inserted in a separate statement, so a user
created while a room was being closed could read it as open and be granted it after the close. It's now a
single insert ... select, which SQLite runs under the write lock, skipping duplicates as insert_all did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bj8KnxpTf9sj2Ysa8aLAVa
2026-10-06 14:45:18 +02:00

75 lines
2.3 KiB
Ruby
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
class User < ApplicationRecord
include Avatar, Bannable, Bot, Mentionable, Role, Transferable
has_many :memberships, dependent: :delete_all
has_many :rooms, through: :memberships
has_many :reachable_messages, through: :rooms, source: :messages
has_many :messages, dependent: :destroy, foreign_key: :creator_id
has_many :push_subscriptions, class_name: "Push::Subscription", dependent: :delete_all
has_many :boosts, dependent: :destroy, foreign_key: :booster_id
has_many :searches, dependent: :delete_all
has_many :sessions, dependent: :destroy
has_many :bans, dependent: :destroy
enum :status, %i[ active deactivated banned ], default: :active
has_secure_password validations: false
after_create_commit :grant_membership_to_open_rooms
scope :ordered, -> { order("LOWER(name)") }
scope :filtered_by, ->(query) { where("name like ?", "%#{query}%") }
def initials
name.scan(/\b\w/).join
end
def title
[ name, bio ].compact_blank.join(" – ")
end
def deactivate
transaction do
close_remote_connections
memberships.without_direct_rooms.delete_all
push_subscriptions.delete_all
searches.delete_all
sessions.delete_all
update! status: :deactivated, email_address: deactived_email_address
end
end
def reset_remote_connections
close_remote_connections reconnect: true
end
private
# One statement, which SQLite runs under the write lock, so no room is closed between reading the open ones and
# inserting. A room that Room#destroy_later closes is either closed before, and skipped, or after, and takes this
# membership away with the others. A room that granted itself to every user when it was created is skipped too,
# as insert_all did.
def grant_membership_to_open_rooms
now = Time.current
Membership.connection.execute Membership.sanitize_sql([ <<~SQL, id, now, now ])
insert into memberships (room_id, user_id, created_at, updated_at)
select id, ?, ?, ? from rooms where type = 'Rooms::Open'
on conflict do nothing
SQL
end
def deactived_email_address
email_address&.gsub(/@/, "-deactivated-#{SecureRandom.uuid}@")
end
def close_remote_connections(reconnect: false)
ActionCable.server.remote_connections.where(current_user: self).disconnect reconnect: reconnect
end
end