mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
Strangler-fig migration of 15 WebSocket handlers from V1 (Hub method, *Client) to V2 (pure functions: Command, ClientInfo, deps -> Result). V2 handlers are testable without a running Hub and produce declarative Result values that the dispatch loop applies. New abstractions: - Command interface + typed constructors with input validation - 7 Event routing interfaces (Channel, ExcludeSender, SequencedDM, UserTargeted, BroadcastAll, VoiceChannel, VoiceChannelGuarded) - Per-domain deps structs (PingDeps, ChatDeps, PresenceDeps, ReactionDeps, VoiceDeps) with interface-based DI - EmitEvents router matching events to delivery mechanisms - DispatchV2 with panic recovery and runtime.Stack logging Security hardening: - Pre-sanitize byte length guard before bluemonday (DoS prevention) - GetRoleForUser single-JOIN query avoids password hash on hot path - channel_id positivity enforced in all command constructors - Log injection prevention: msgType/reqID capped to 64 chars - Nil KeyHolder dep returns ErrCodeInternal (not silent bypass) - VoiceChannelGuardedEvent atomic check-and-send under h.mu.RLock V1-only (complex state/mutex requirements): voice_join, voice_leave. All tests pass with -race. No CI regressions expected.
295 lines
11 KiB
Go
295 lines
11 KiB
Go
package ws
|
|
|
|
// ClientError represents an error to send back to the requesting client.
|
|
// It implements the error interface so it can be used as Result.Error.
|
|
type ClientError struct {
|
|
Code string
|
|
Message string
|
|
}
|
|
|
|
func (e ClientError) Error() string { return e.Code + ": " + e.Message }
|
|
|
|
// Result is returned by V2 handlers. It describes the outcome of processing
|
|
// a Command: zero or more Events to emit, an optional error, and an optional
|
|
// Reply (ACK) to send back to the sender.
|
|
type Result struct {
|
|
// Events to route to other clients via EmitEvents.
|
|
Events []Event
|
|
// Error, if non-nil, is sent to the client. Use ClientError for
|
|
// user-facing errors; other error types are treated as internal.
|
|
Error error
|
|
// Reply is an optional raw JSON ACK sent only to the sender
|
|
// (e.g. chat_send_ok with the new message ID).
|
|
Reply []byte
|
|
// SetChannelID, if non-nil, updates the client's focused channel.
|
|
// Used by channel_focus to mutate client state from a V2 handler.
|
|
SetChannelID *int64
|
|
// SetE2EEPubKey, if non-nil, stores the ECDH public key on the client.
|
|
// Used by voice_e2ee_announce to persist the key for later retrieval.
|
|
SetE2EEPubKey *string
|
|
// SetVoiceJoinToken, if non-nil, caches the voice join token on the client.
|
|
// Used by voice_token_refresh when falling back to the DB for the token.
|
|
SetVoiceJoinToken *string
|
|
}
|
|
|
|
// Event is the base interface for all server-to-client events.
|
|
type Event interface {
|
|
// EventType returns the outbound message type constant (e.g. MsgTypeChatMessage).
|
|
EventType() string
|
|
}
|
|
|
|
// ── Routing interfaces ──────────────────────────────────────────────────────
|
|
// EmitEvents will type-switch on these interfaces to decide how to deliver
|
|
// each Event. The check order matters: SequencedDMEvent MUST be checked
|
|
// before ChannelEvent because DM events implement both.
|
|
|
|
// ChannelEvent routes to Hub.BroadcastToChannel (sequenced, replayable).
|
|
type ChannelEvent interface {
|
|
Event
|
|
ChannelID() int64
|
|
Payload() []byte
|
|
}
|
|
|
|
// ExcludeSenderEvent routes to Hub.broadcastExclude (ephemeral, not replayed).
|
|
// Used for typing indicators in non-DM channels.
|
|
type ExcludeSenderEvent interface {
|
|
Event
|
|
ChannelID() int64
|
|
ExcludeUserID() int64
|
|
Payload() []byte
|
|
}
|
|
|
|
// SequencedDMEvent routes to Hub.sendSequencedToUsers (sequenced, replayable).
|
|
// Used for chat messages, edits, deletes, and reactions in DM channels.
|
|
type SequencedDMEvent interface {
|
|
Event
|
|
ChannelID() int64
|
|
ParticipantIDs() []int64
|
|
Payload() []byte
|
|
}
|
|
|
|
// UserTargetedEvent routes to Hub.SendToUser (direct delivery to one user).
|
|
type UserTargetedEvent interface {
|
|
Event
|
|
TargetUserID() int64
|
|
Payload() []byte
|
|
}
|
|
|
|
// BroadcastAllEvent routes to Hub.BroadcastToAll (channelID=0, all clients).
|
|
type BroadcastAllEvent interface {
|
|
Event
|
|
Payload() []byte
|
|
}
|
|
|
|
// VoiceChannelEvent routes to Hub.sendToVoiceChannelExcept (ephemeral,
|
|
// targets voice channel participants excluding sender).
|
|
type VoiceChannelEvent interface {
|
|
Event
|
|
VoiceChannelID() int64
|
|
ExcludeUserID() int64
|
|
Payload() []byte
|
|
}
|
|
|
|
// VoiceChannelGuardedEvent routes to Hub.sendToUserIfInVoiceChannel —
|
|
// atomic check-and-send that verifies the target is still in the expected
|
|
// voice channel before delivering the message, all under a single h.mu.RLock.
|
|
// Used by voice_e2ee_offer to prevent TOCTOU races with concurrent voice_leave.
|
|
type VoiceChannelGuardedEvent interface {
|
|
Event
|
|
VoiceChannelID() int64
|
|
TargetUserID() int64
|
|
Payload() []byte
|
|
}
|
|
|
|
// ── Concrete event structs ──────────────────────────────────────────────────
|
|
|
|
// MessageSentChannelEvent is a chat message broadcast to a non-DM channel.
|
|
type MessageSentChannelEvent struct {
|
|
channelID int64
|
|
payload []byte
|
|
}
|
|
|
|
func (e MessageSentChannelEvent) EventType() string { return MsgTypeChatMessage }
|
|
func (e MessageSentChannelEvent) ChannelID() int64 { return e.channelID }
|
|
func (e MessageSentChannelEvent) Payload() []byte { return e.payload }
|
|
|
|
// MessageSentDMEvent is a chat message broadcast to a DM channel's participants.
|
|
type MessageSentDMEvent struct {
|
|
channelID int64
|
|
participantIDs []int64
|
|
payload []byte
|
|
}
|
|
|
|
func (e MessageSentDMEvent) EventType() string { return MsgTypeChatMessage }
|
|
func (e MessageSentDMEvent) ChannelID() int64 { return e.channelID }
|
|
func (e MessageSentDMEvent) ParticipantIDs() []int64 {
|
|
dst := make([]int64, len(e.participantIDs))
|
|
copy(dst, e.participantIDs)
|
|
return dst
|
|
}
|
|
func (e MessageSentDMEvent) Payload() []byte { return e.payload }
|
|
|
|
// MessageEditedChannelEvent is a chat_edited broadcast to a non-DM channel.
|
|
type MessageEditedChannelEvent struct {
|
|
channelID int64
|
|
payload []byte
|
|
}
|
|
|
|
func (e MessageEditedChannelEvent) EventType() string { return MsgTypeChatEdited }
|
|
func (e MessageEditedChannelEvent) ChannelID() int64 { return e.channelID }
|
|
func (e MessageEditedChannelEvent) Payload() []byte { return e.payload }
|
|
|
|
// MessageEditedDMEvent is a chat_edited broadcast to DM participants.
|
|
type MessageEditedDMEvent struct {
|
|
channelID int64
|
|
participantIDs []int64
|
|
payload []byte
|
|
}
|
|
|
|
func (e MessageEditedDMEvent) EventType() string { return MsgTypeChatEdited }
|
|
func (e MessageEditedDMEvent) ChannelID() int64 { return e.channelID }
|
|
func (e MessageEditedDMEvent) ParticipantIDs() []int64 {
|
|
dst := make([]int64, len(e.participantIDs))
|
|
copy(dst, e.participantIDs)
|
|
return dst
|
|
}
|
|
func (e MessageEditedDMEvent) Payload() []byte { return e.payload }
|
|
|
|
// MessageDeletedChannelEvent is a chat_deleted broadcast to a non-DM channel.
|
|
type MessageDeletedChannelEvent struct {
|
|
channelID int64
|
|
payload []byte
|
|
}
|
|
|
|
func (e MessageDeletedChannelEvent) EventType() string { return MsgTypeChatDeleted }
|
|
func (e MessageDeletedChannelEvent) ChannelID() int64 { return e.channelID }
|
|
func (e MessageDeletedChannelEvent) Payload() []byte { return e.payload }
|
|
|
|
// MessageDeletedDMEvent is a chat_deleted broadcast to DM participants.
|
|
type MessageDeletedDMEvent struct {
|
|
channelID int64
|
|
participantIDs []int64
|
|
payload []byte
|
|
}
|
|
|
|
func (e MessageDeletedDMEvent) EventType() string { return MsgTypeChatDeleted }
|
|
func (e MessageDeletedDMEvent) ChannelID() int64 { return e.channelID }
|
|
func (e MessageDeletedDMEvent) ParticipantIDs() []int64 {
|
|
dst := make([]int64, len(e.participantIDs))
|
|
copy(dst, e.participantIDs)
|
|
return dst
|
|
}
|
|
func (e MessageDeletedDMEvent) Payload() []byte { return e.payload }
|
|
|
|
// TypingChannelEvent is a typing indicator broadcast to a channel, excluding sender.
|
|
type TypingChannelEvent struct {
|
|
channelID int64
|
|
excludeUserID int64
|
|
payload []byte
|
|
}
|
|
|
|
func (e TypingChannelEvent) EventType() string { return MsgTypeTyping }
|
|
func (e TypingChannelEvent) ChannelID() int64 { return e.channelID }
|
|
func (e TypingChannelEvent) ExcludeUserID() int64 { return e.excludeUserID }
|
|
func (e TypingChannelEvent) Payload() []byte { return e.payload }
|
|
|
|
// TypingDMEvent is a typing indicator sent to DM participants, excluding sender.
|
|
// It uses UserTargetedEvent routing because DM typing excludes the sender and
|
|
// is delivered directly to each other participant.
|
|
type TypingDMEvent struct {
|
|
targetUserID int64
|
|
payload []byte
|
|
}
|
|
|
|
func (e TypingDMEvent) EventType() string { return MsgTypeTyping }
|
|
func (e TypingDMEvent) TargetUserID() int64 { return e.targetUserID }
|
|
func (e TypingDMEvent) Payload() []byte { return e.payload }
|
|
|
|
// PresenceEvent is a presence update broadcast to all connected clients.
|
|
type PresenceEvent struct {
|
|
payload []byte
|
|
}
|
|
|
|
func (e PresenceEvent) EventType() string { return MsgTypePresence }
|
|
func (e PresenceEvent) Payload() []byte { return e.payload }
|
|
|
|
// ReactionChannelEvent is a reaction update broadcast to a non-DM channel.
|
|
type ReactionChannelEvent struct {
|
|
channelID int64
|
|
payload []byte
|
|
}
|
|
|
|
func (e ReactionChannelEvent) EventType() string { return MsgTypeReactionUpdate }
|
|
func (e ReactionChannelEvent) ChannelID() int64 { return e.channelID }
|
|
func (e ReactionChannelEvent) Payload() []byte { return e.payload }
|
|
|
|
// ReactionDMEvent is a reaction update broadcast to DM participants.
|
|
type ReactionDMEvent struct {
|
|
channelID int64
|
|
participantIDs []int64
|
|
payload []byte
|
|
}
|
|
|
|
func (e ReactionDMEvent) EventType() string { return MsgTypeReactionUpdate }
|
|
func (e ReactionDMEvent) ChannelID() int64 { return e.channelID }
|
|
func (e ReactionDMEvent) ParticipantIDs() []int64 {
|
|
dst := make([]int64, len(e.participantIDs))
|
|
copy(dst, e.participantIDs)
|
|
return dst
|
|
}
|
|
func (e ReactionDMEvent) Payload() []byte { return e.payload }
|
|
|
|
// VoiceStateEvent is a voice state broadcast to all connected clients.
|
|
type VoiceStateEvent struct {
|
|
payload []byte
|
|
}
|
|
|
|
func (e VoiceStateEvent) EventType() string { return MsgTypeVoiceState }
|
|
func (e VoiceStateEvent) Payload() []byte { return e.payload }
|
|
|
|
// VoiceLeaveEvent is a voice_leave broadcast to all connected clients.
|
|
// NOTE: Currently unused by V2 handlers — voice_leave remains V1 and emits
|
|
// via h.BroadcastToAll directly. Retained as forward-compatible scaffolding.
|
|
type VoiceLeaveEvent struct {
|
|
payload []byte
|
|
}
|
|
|
|
func (e VoiceLeaveEvent) EventType() string { return MsgTypeVoiceLeaveBC }
|
|
func (e VoiceLeaveEvent) Payload() []byte { return e.payload }
|
|
|
|
// VoiceE2EEAnnounceEvent relays an ECDH public key to other voice channel participants.
|
|
type VoiceE2EEAnnounceEvent struct {
|
|
voiceChannelID int64
|
|
excludeUserID int64
|
|
payload []byte
|
|
}
|
|
|
|
func (e VoiceE2EEAnnounceEvent) EventType() string { return MsgTypeVoiceE2EEAnnounceBC }
|
|
func (e VoiceE2EEAnnounceEvent) VoiceChannelID() int64 { return e.voiceChannelID }
|
|
func (e VoiceE2EEAnnounceEvent) ExcludeUserID() int64 { return e.excludeUserID }
|
|
func (e VoiceE2EEAnnounceEvent) Payload() []byte { return e.payload }
|
|
|
|
// VoiceE2EEOfferGuardedEvent relays an encrypted room key to a specific user,
|
|
// using atomic check-and-send to verify the target is still in the same voice
|
|
// channel. Satisfies VoiceChannelGuardedEvent.
|
|
type VoiceE2EEOfferGuardedEvent struct {
|
|
voiceChannelID int64
|
|
targetUserID int64
|
|
payload []byte
|
|
}
|
|
|
|
func (e VoiceE2EEOfferGuardedEvent) EventType() string { return MsgTypeVoiceE2EEOfferRelay }
|
|
func (e VoiceE2EEOfferGuardedEvent) VoiceChannelID() int64 { return e.voiceChannelID }
|
|
func (e VoiceE2EEOfferGuardedEvent) TargetUserID() int64 { return e.targetUserID }
|
|
func (e VoiceE2EEOfferGuardedEvent) Payload() []byte { return e.payload }
|
|
|
|
// DMChannelOpenEvent sends a dm_channel_open notification to a specific user.
|
|
type DMChannelOpenEvent struct {
|
|
targetUserID int64
|
|
payload []byte
|
|
}
|
|
|
|
func (e DMChannelOpenEvent) EventType() string { return MsgTypeDMChannelOpen }
|
|
func (e DMChannelOpenEvent) TargetUserID() int64 { return e.targetUserID }
|
|
func (e DMChannelOpenEvent) Payload() []byte { return e.payload }
|