Files
once-campfire/app/models/room.rb
T
Jeremy Daer 5c5c82b27a Scope room lookup to the type each controller administers
Rooms::DirectsController relaxes ensure_can_administer to true, because every
participant in a direct room may administer it. set_room was inherited unscoped,
though, so that relaxation applied to any room the caller was merely a member of:
DELETE /rooms/directs/<id> destroyed open and closed rooms and all their messages.

The same unscoped lookup let a direct room be loaded by the opens and closeds
controllers, where force_room_type promoted it. Promoting a DM to open grants every
user on the account membership and republishes the whole conversation, including the
other participant's messages; converting it to closed lets the initiator revise who
is in it and lock the other participant out.

Each controller now narrows room_scope to the types it may act on. Opens and closeds
keep reach into each other, since converting between them is a feature. Neither can
reach a direct room, and directs can only reach directs.

Room also refuses to change type away from Rooms::Direct, so the invariant holds for
any future caller of becomes! rather than only these two controllers.
2026-08-03 14:55:04 -07:00

87 lines
2.2 KiB
Ruby

class Room < ApplicationRecord
has_many :memberships, dependent: :delete_all do
def grant_to(users)
room = proxy_association.owner
Membership.insert_all(Array(users).collect { |user| { room_id: room.id, user_id: user.id, involvement: room.default_involvement } })
end
def revoke_from(users)
destroy_by user: users
end
def revise(granted: [], revoked: [])
transaction do
grant_to(granted) if granted.present?
revoke_from(revoked) if revoked.present?
end
end
end
has_many :users, through: :memberships
has_many :messages, dependent: :destroy
belongs_to :creator, class_name: "User", default: -> { Current.user }
validate :direct_rooms_keep_their_type, on: :update
scope :opens, -> { where(type: "Rooms::Open") }
scope :closeds, -> { where(type: "Rooms::Closed") }
scope :directs, -> { where(type: "Rooms::Direct") }
scope :without_directs, -> { where.not(type: "Rooms::Direct") }
scope :ordered, -> { order("LOWER(name)") }
class << self
def create_for(attributes, users:)
transaction do
create!(attributes).tap do |room|
room.memberships.grant_to users
end
end
end
def original
order(:created_at).first
end
end
def receive(message)
unread_memberships(message)
push_later(message)
end
def open?
is_a?(Rooms::Open)
end
def closed?
is_a?(Rooms::Closed)
end
def direct?
is_a?(Rooms::Direct)
end
def default_involvement
"mentions"
end
private
# Open and closed rooms convert into each other freely. A direct room can't become
# either: its participants agreed to a private conversation, not to one whose
# audience someone else gets to widen afterwards.
def direct_rooms_keep_their_type
if type_changed? && type_was == "Rooms::Direct"
errors.add :type, "can't be changed for a direct room"
end
end
def unread_memberships(message)
memberships.visible.disconnected.where.not(user: message.creator).update_all(unread_at: message.created_at, updated_at: Time.current)
end
def push_later(message)
Room::PushMessageJob.perform_later(self, message)
end
end