mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
Applied from a deadcode (RTA from mains, all build tags) sweep with
per-symbol adversarial verification:
Deleted (nothing but their own self-tests used them):
- admin.Handler (deprecated since Phase 6; production mounts NewHandler)
plus its two self-tests
- ws.Hub.broadcastVoiceStateUpdate + wrapper + two self-tests (pre-V2
leftover; the live voice_state path is the hub voice routines)
- ws.VoiceLeaveEvent + methods ('retained as scaffolding', never
constructed in production; MsgTypeVoiceLeaveBC stays — live via the
leave routine)
- ws.parseIdentity (production calls parseParticipantIdentity directly;
ParseIdentityForTest now exercises the real parser)
- telemetry.Float64 (String/Int64 are used; the float case is covered by
the otel-tagged internal test, re-addable when a caller appears)
Moved into export_test.go so they leave the production binary (all
callers are same-package tests): the eight ws test-client constructors
and voice/E2EE setters from ws/client.go, admin.SetBackupBaseDir
(new admin/export_test.go), api.SecurityHeaders (test-only wrapper;
production uses SecurityHeadersWithTLS — docs/api.md updated to the
real name). Client.getVoiceJoinToken/setVoiceChID inlined into their
existing ForTest wrappers; TestSetVoiceChID_* self-tests deleted.
Kept after verification: updater.SetBaseURL (11 cross-package test call
sites) and telemetry.resetAppMetricsForInit (live under -tags otel —
untagged deadcode false positive).
Full gate green: gofmt/vet, 4 build-tag variants, full suite, deadlock,
race.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
309 lines
12 KiB
Go
309 lines
12 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
|
|
// JoinVoice, if true, triggers the hub's voice-join routine after the handler
|
|
// returns. voice_join's effect is a large, hub-coupled sequence (DB
|
|
// persistence, LiveKit token, existing-state fan-out, key-holder election,
|
|
// topic subscription) that also invokes the leave routine on a channel
|
|
// switch, so the applier runs handleVoiceJoin (re-parsing the envelope
|
|
// payload it already validated) rather than re-expressing it as pure events.
|
|
JoinVoice bool
|
|
// LeaveVoice, if true, triggers the hub's voice-leave routine after the
|
|
// handler returns. handleVoiceLeave stays hub-internal because disconnect and
|
|
// channel-switch cleanup call it un-throttled; only the message dispatch
|
|
// moved to V2 (which does the rate-limit before setting this flag).
|
|
LeaveVoice bool
|
|
}
|
|
|
|
// 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 }
|
|
|
|
// PluginBroadcastEvent is a plugin slash-command result broadcast to a channel
|
|
// (sequenced, replayable). Emitted by the chat_command handler after the
|
|
// invoking user's post permission is verified.
|
|
type PluginBroadcastEvent struct {
|
|
channelID int64
|
|
payload []byte
|
|
}
|
|
|
|
func (e PluginBroadcastEvent) EventType() string { return "plugin_broadcast" }
|
|
func (e PluginBroadcastEvent) ChannelID() int64 { return e.channelID }
|
|
func (e PluginBroadcastEvent) 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 }
|