2026-04-05 19:03:22 +02:00
package ws
import (
"encoding/json"
"fmt"
2026-07-20 11:41:58 +02:00
"strings"
2026-04-05 19:03:22 +02:00
)
// Command is the minimal interface for all client-to-server commands.
type Command interface {
// Type returns the message type constant (e.g. MsgTypeChatSend).
Type () string
// UserID returns the authenticated user who sent this command.
UserID () int64
}
// ChannelScoped is an optional interface for commands targeting a channel.
type ChannelScoped interface {
ChannelID () int64
}
// ── Concrete command structs ────────────────────────────────────────────────
// PingCmd represents a client ping (heartbeat).
type PingCmd struct {
userID int64
}
func ( c PingCmd ) Type () string { return MsgTypePing }
func ( c PingCmd ) UserID () int64 { return c . userID }
// ChatSendCmd represents a chat_send message.
type ChatSendCmd struct {
userID int64
reqID string
channelID int64
content string
replyTo * int64
attachments [] string
}
func ( c ChatSendCmd ) Type () string { return MsgTypeChatSend }
func ( c ChatSendCmd ) UserID () int64 { return c . userID }
func ( c ChatSendCmd ) ChannelID () int64 { return c . channelID }
func ( c ChatSendCmd ) ReqID () string { return c . reqID }
func ( c ChatSendCmd ) Content () string { return c . content }
func ( c ChatSendCmd ) ReplyTo () * int64 { return c . replyTo }
func ( c ChatSendCmd ) Attachments () [] string {
dst := make ([] string , len ( c . attachments ))
copy ( dst , c . attachments )
return dst
}
// ChatEditCmd represents a chat_edit message.
type ChatEditCmd struct {
userID int64
reqID string
messageID int64
content string
}
func ( c ChatEditCmd ) Type () string { return MsgTypeChatEdit }
func ( c ChatEditCmd ) UserID () int64 { return c . userID }
func ( c ChatEditCmd ) ReqID () string { return c . reqID }
func ( c ChatEditCmd ) MessageID () int64 { return c . messageID }
func ( c ChatEditCmd ) Content () string { return c . content }
// ChatDeleteCmd represents a chat_delete message.
type ChatDeleteCmd struct {
userID int64
reqID string
messageID int64
}
func ( c ChatDeleteCmd ) Type () string { return MsgTypeChatDelete }
func ( c ChatDeleteCmd ) UserID () int64 { return c . userID }
func ( c ChatDeleteCmd ) ReqID () string { return c . reqID }
func ( c ChatDeleteCmd ) MessageID () int64 { return c . messageID }
// TypingStartCmd represents a typing_start message.
type TypingStartCmd struct {
userID int64
channelID int64
}
func ( c TypingStartCmd ) Type () string { return MsgTypeTypingStart }
func ( c TypingStartCmd ) UserID () int64 { return c . userID }
func ( c TypingStartCmd ) ChannelID () int64 { return c . channelID }
// PresenceUpdateCmd represents a presence_update message.
type PresenceUpdateCmd struct {
userID int64
status string
2026-08-01 22:06:14 +02:00
// customStatus is nil when the payload carried no custom_status field at
// all, which means "leave the stored text alone". A present-but-empty
// string clears it. The distinction matters because the auto-idle timer
// sends a bare status flip several times an hour and must not wipe the
// text the user typed.
customStatus * string
2026-04-05 19:03:22 +02:00
}
2026-08-01 22:06:14 +02:00
func ( c PresenceUpdateCmd ) Type () string { return MsgTypePresenceUpdate }
func ( c PresenceUpdateCmd ) UserID () int64 { return c . userID }
func ( c PresenceUpdateCmd ) Status () string { return c . status }
func ( c PresenceUpdateCmd ) CustomStatus () * string { return c . customStatus }
2026-04-05 19:03:22 +02:00
// ChannelFocusCmd represents a channel_focus message.
type ChannelFocusCmd struct {
userID int64
channelID int64
}
func ( c ChannelFocusCmd ) Type () string { return MsgTypeChannelFocus }
func ( c ChannelFocusCmd ) UserID () int64 { return c . userID }
func ( c ChannelFocusCmd ) ChannelID () int64 { return c . channelID }
2026-08-01 22:06:14 +02:00
// MarkReadCmd represents a mark_read message: advance the caller's read state
// for a channel without making it their focused channel. channel_focus already
// marks read, but it also rebinds the connection's focused channel, so it is
// the wrong tool for "mark that other channel read from its context menu".
type MarkReadCmd struct {
userID int64
channelID int64
}
func ( c MarkReadCmd ) Type () string { return MsgTypeMarkRead }
func ( c MarkReadCmd ) UserID () int64 { return c . userID }
func ( c MarkReadCmd ) ChannelID () int64 { return c . channelID }
2026-04-05 19:03:22 +02:00
// ReactionAddCmd represents a reaction_add message.
type ReactionAddCmd struct {
userID int64
messageID int64
emoji string
}
func ( c ReactionAddCmd ) Type () string { return MsgTypeReactionAdd }
func ( c ReactionAddCmd ) UserID () int64 { return c . userID }
func ( c ReactionAddCmd ) MessageID () int64 { return c . messageID }
func ( c ReactionAddCmd ) Emoji () string { return c . emoji }
// ReactionRemoveCmd represents a reaction_remove message.
type ReactionRemoveCmd struct {
userID int64
messageID int64
emoji string
}
func ( c ReactionRemoveCmd ) Type () string { return MsgTypeReactionRemove }
func ( c ReactionRemoveCmd ) UserID () int64 { return c . userID }
func ( c ReactionRemoveCmd ) MessageID () int64 { return c . messageID }
func ( c ReactionRemoveCmd ) Emoji () string { return c . emoji }
// VoiceJoinCmd represents a voice_join message.
type VoiceJoinCmd struct {
userID int64
channelID int64
}
func ( c VoiceJoinCmd ) Type () string { return MsgTypeVoiceJoin }
func ( c VoiceJoinCmd ) UserID () int64 { return c . userID }
func ( c VoiceJoinCmd ) ChannelID () int64 { return c . channelID }
// VoiceLeaveCmd represents a voice_leave message.
type VoiceLeaveCmd struct {
userID int64
}
func ( c VoiceLeaveCmd ) Type () string { return MsgTypeVoiceLeave }
func ( c VoiceLeaveCmd ) UserID () int64 { return c . userID }
// VoiceTokenRefreshCmd represents a voice_token_refresh message.
type VoiceTokenRefreshCmd struct {
userID int64
}
func ( c VoiceTokenRefreshCmd ) Type () string { return MsgTypeVoiceTokenRefresh }
func ( c VoiceTokenRefreshCmd ) UserID () int64 { return c . userID }
// VoiceMuteCmd represents a voice_mute message.
type VoiceMuteCmd struct {
userID int64
muted bool
}
func ( c VoiceMuteCmd ) Type () string { return MsgTypeVoiceMute }
func ( c VoiceMuteCmd ) UserID () int64 { return c . userID }
func ( c VoiceMuteCmd ) Muted () bool { return c . muted }
// VoiceDeafenCmd represents a voice_deafen message.
type VoiceDeafenCmd struct {
userID int64
deafened bool
}
func ( c VoiceDeafenCmd ) Type () string { return MsgTypeVoiceDeafen }
func ( c VoiceDeafenCmd ) UserID () int64 { return c . userID }
func ( c VoiceDeafenCmd ) Deafened () bool { return c . deafened }
// VoiceCameraCmd represents a voice_camera message.
type VoiceCameraCmd struct {
userID int64
enabled bool
}
func ( c VoiceCameraCmd ) Type () string { return MsgTypeVoiceCamera }
func ( c VoiceCameraCmd ) UserID () int64 { return c . userID }
func ( c VoiceCameraCmd ) Enabled () bool { return c . enabled }
// VoiceScreenshareCmd represents a voice_screenshare message.
type VoiceScreenshareCmd struct {
userID int64
enabled bool
}
func ( c VoiceScreenshareCmd ) Type () string { return MsgTypeVoiceScreenshare }
func ( c VoiceScreenshareCmd ) UserID () int64 { return c . userID }
func ( c VoiceScreenshareCmd ) Enabled () bool { return c . enabled }
2026-08-01 22:06:14 +02:00
// VoiceModMuteCmd represents a voice_mod_mute message: a moderator setting
// another user's server mute. channelID is the voice channel the moderator
// believes the target is in — the handler refuses when it disagrees, so a
// stale sidebar cannot mute someone who has since moved.
type VoiceModMuteCmd struct {
userID int64
channelID int64
targetID int64
muted bool
}
func ( c VoiceModMuteCmd ) Type () string { return MsgTypeVoiceModMute }
func ( c VoiceModMuteCmd ) UserID () int64 { return c . userID }
func ( c VoiceModMuteCmd ) ChannelID () int64 { return c . channelID }
func ( c VoiceModMuteCmd ) TargetID () int64 { return c . targetID }
func ( c VoiceModMuteCmd ) Muted () bool { return c . muted }
// VoiceModDeafenCmd represents a voice_mod_deafen message. See VoiceModMuteCmd
// for the channelID contract.
type VoiceModDeafenCmd struct {
userID int64
channelID int64
targetID int64
deafened bool
}
func ( c VoiceModDeafenCmd ) Type () string { return MsgTypeVoiceModDeafen }
func ( c VoiceModDeafenCmd ) UserID () int64 { return c . userID }
func ( c VoiceModDeafenCmd ) ChannelID () int64 { return c . channelID }
func ( c VoiceModDeafenCmd ) TargetID () int64 { return c . targetID }
func ( c VoiceModDeafenCmd ) Deafened () bool { return c . deafened }
// VoiceModMoveCmd represents a voice_mod_move message: a moderator moving a
// user to another voice channel.
type VoiceModMoveCmd struct {
userID int64
targetID int64
toChannelID int64
}
func ( c VoiceModMoveCmd ) Type () string { return MsgTypeVoiceModMove }
func ( c VoiceModMoveCmd ) UserID () int64 { return c . userID }
func ( c VoiceModMoveCmd ) TargetID () int64 { return c . targetID }
func ( c VoiceModMoveCmd ) ToChannelID () int64 { return c . toChannelID }
// VoiceModKickCmd represents a voice_mod_kick message: a moderator
// disconnecting a user from voice.
type VoiceModKickCmd struct {
userID int64
targetID int64
}
func ( c VoiceModKickCmd ) Type () string { return MsgTypeVoiceModKick }
func ( c VoiceModKickCmd ) UserID () int64 { return c . userID }
func ( c VoiceModKickCmd ) TargetID () int64 { return c . targetID }
2026-04-05 19:03:22 +02:00
// VoiceE2EEAnnounceCmd represents a voice_e2ee_announce message.
2026-07-23 21:07:09 +02:00
// signature is the ECDSA identity-key signature over the ephemeral public key
// (F3 TOFU); optional at the protocol level — legacy clients omit it and the
// receiving client enforces the fail-closed posture.
2026-04-05 19:03:22 +02:00
type VoiceE2EEAnnounceCmd struct {
userID int64
publicKey string
2026-07-23 21:07:09 +02:00
signature string
2026-04-05 19:03:22 +02:00
}
func ( c VoiceE2EEAnnounceCmd ) Type () string { return MsgTypeVoiceE2EEAnnounce }
func ( c VoiceE2EEAnnounceCmd ) UserID () int64 { return c . userID }
func ( c VoiceE2EEAnnounceCmd ) PublicKey () string { return c . publicKey }
2026-07-23 21:07:09 +02:00
func ( c VoiceE2EEAnnounceCmd ) Signature () string { return c . signature }
2026-04-05 19:03:22 +02:00
2026-07-20 11:41:58 +02:00
// ChatCommandCmd represents a chat_command (plugin slash command) message.
type ChatCommandCmd struct {
userID int64
reqID string
channelID int64
command string // trimmed, including leading slash, e.g. "/hello"
args [] string
}
func ( c ChatCommandCmd ) Type () string { return MsgTypeChatCommand }
func ( c ChatCommandCmd ) UserID () int64 { return c . userID }
func ( c ChatCommandCmd ) ChannelID () int64 { return c . channelID }
func ( c ChatCommandCmd ) ReqID () string { return c . reqID }
func ( c ChatCommandCmd ) Command () string { return c . command }
func ( c ChatCommandCmd ) Args () [] string {
dst := make ([] string , len ( c . args ))
copy ( dst , c . args )
return dst
}
2026-04-05 19:03:22 +02:00
// VoiceE2EEOfferCmd represents a voice_e2ee_offer message.
type VoiceE2EEOfferCmd struct {
userID int64
targetUserID int64
encryptedKey string
iv string
}
func ( c VoiceE2EEOfferCmd ) Type () string { return MsgTypeVoiceE2EEOffer }
func ( c VoiceE2EEOfferCmd ) UserID () int64 { return c . userID }
func ( c VoiceE2EEOfferCmd ) TargetUserID () int64 { return c . targetUserID }
func ( c VoiceE2EEOfferCmd ) EncryptedKey () string { return c . encryptedKey }
func ( c VoiceE2EEOfferCmd ) IV () string { return c . iv }
2026-08-01 22:06:14 +02:00
// CallRingCmd represents a call_ring message: "start ringing the other people
// in this DM". It carries only the channel — who is calling is the
// authenticated sender, and there is no call id because there is no call
// record (see registerCallHandlers).
type CallRingCmd struct {
userID int64
channelID int64
}
func ( c CallRingCmd ) Type () string { return MsgTypeCallRing }
func ( c CallRingCmd ) UserID () int64 { return c . userID }
func ( c CallRingCmd ) ChannelID () int64 { return c . channelID }
// CallDeclineCmd represents a call_decline message.
type CallDeclineCmd struct {
userID int64
channelID int64
}
func ( c CallDeclineCmd ) Type () string { return MsgTypeCallDecline }
func ( c CallDeclineCmd ) UserID () int64 { return c . userID }
func ( c CallDeclineCmd ) ChannelID () int64 { return c . channelID }
2026-04-05 19:03:22 +02:00
// ── Command constructors ────────────────────────────────────────────────────
2026-08-01 22:06:14 +02:00
// parseModTarget parses the (channel, target user) pair every voice moderation
// payload carries. Both must be positive: a non-positive id can only come from
// a malformed client, and rejecting at parse time keeps the handlers free of
// id-shape checks.
func parseModTarget ( channelID , userID json . Number ) ( int64 , int64 , error ) {
chID , err := channelID . Int64 ()
if err != nil {
return 0 , 0 , fmt . Errorf ( "channel_id must be integer: %w" , err )
}
if chID <= 0 {
return 0 , 0 , fmt . Errorf ( "channel_id must be positive" )
}
targetID , err := userID . Int64 ()
if err != nil {
return 0 , 0 , fmt . Errorf ( "user_id must be integer: %w" , err )
}
if targetID <= 0 {
return 0 , 0 , fmt . Errorf ( "user_id must be positive" )
}
return chID , targetID , nil
}
// parseCallChannelID parses the lone channel_id both call signalling payloads
// carry. Shared so call_ring and call_decline cannot drift on what counts as a
// valid channel reference.
func parseCallChannelID ( msgType string , raw json . RawMessage ) ( int64 , error ) {
var p struct {
ChannelID json . Number `json:"channel_id"`
}
if err := json . Unmarshal ( raw , & p ); err != nil {
return 0 , fmt . Errorf ( "invalid %s payload: %w" , msgType , err )
}
chID , err := p . ChannelID . Int64 ()
if err != nil {
return 0 , fmt . Errorf ( "channel_id must be integer: %w" , err )
}
if chID <= 0 {
return 0 , fmt . Errorf ( "channel_id must be positive" )
}
return chID , nil
}
2026-04-05 19:03:22 +02:00
// commandConstructors maps message types to functions that parse payloads
// into typed Commands. The userID and reqID come from the envelope and
// authenticated client; raw is the JSON payload body.
// Unexported to prevent accidental mutation; use getCommandConstructor for lookups.
var commandConstructors = map [ string ] func ( userID int64 , reqID string , raw json . RawMessage ) ( Command , error ){
2026-04-07 10:10:38 +02:00
MsgTypePing : func ( userID int64 , _ string , _ json . RawMessage ) ( Command , error ) { //nolint:unparam // error always nil; signature dictated by map type
2026-04-05 19:03:22 +02:00
return PingCmd { userID : userID }, nil
},
MsgTypeChatSend : func ( userID int64 , reqID string , raw json . RawMessage ) ( Command , error ) {
var p struct {
ChannelID json . Number `json:"channel_id"`
Content string `json:"content"`
ReplyTo * int64 `json:"reply_to"`
Attachments [] string `json:"attachments"`
}
if err := json . Unmarshal ( raw , & p ); err != nil {
return nil , fmt . Errorf ( "invalid chat_send payload: %w" , err )
}
chID , err := p . ChannelID . Int64 ()
if err != nil {
return nil , fmt . Errorf ( "channel_id must be integer: %w" , err )
}
if len ( p . Attachments ) > 10 {
return nil , fmt . Errorf ( "too many attachments (max 10)" )
}
2026-08-01 22:06:14 +02:00
// These are upload IDs (UUIDs from POST /api/v1/uploads), NOT URLs: the
// client renders an attachment by resolving its id to /api/v1/files/{id},
// and SendMessage links each id through LinkAttachmentsToMessage, which
// keeps only ids that name a real upload owned by the sender and still
// unlinked. A javascript:/data: string is therefore never stored or
// echoed — it simply matches no row and is dropped. A scheme check would
// be wrong here (it would reject the legitimate UUID ids); the only
// bound that applies is length.
for i , id := range p . Attachments {
if len ( id ) > 2048 {
return nil , fmt . Errorf ( "attachment[%d] id too long (max 2048)" , i )
2026-04-05 19:03:22 +02:00
}
}
attachments := make ([] string , len ( p . Attachments ))
copy ( attachments , p . Attachments )
return ChatSendCmd {
userID : userID ,
reqID : reqID ,
channelID : chID ,
content : p . Content ,
replyTo : p . ReplyTo ,
attachments : attachments ,
}, nil
},
MsgTypeChatEdit : func ( userID int64 , reqID string , raw json . RawMessage ) ( Command , error ) {
var p struct {
MessageID json . Number `json:"message_id"`
Content string `json:"content"`
}
if err := json . Unmarshal ( raw , & p ); err != nil {
return nil , fmt . Errorf ( "invalid chat_edit payload: %w" , err )
}
msgID , err := p . MessageID . Int64 ()
if err != nil {
return nil , fmt . Errorf ( "message_id must be integer: %w" , err )
}
return ChatEditCmd {
userID : userID ,
reqID : reqID ,
messageID : msgID ,
content : p . Content ,
}, nil
},
MsgTypeChatDelete : func ( userID int64 , reqID string , raw json . RawMessage ) ( Command , error ) {
var p struct {
MessageID json . Number `json:"message_id"`
}
if err := json . Unmarshal ( raw , & p ); err != nil {
return nil , fmt . Errorf ( "invalid chat_delete payload: %w" , err )
}
msgID , err := p . MessageID . Int64 ()
if err != nil {
return nil , fmt . Errorf ( "message_id must be integer: %w" , err )
}
return ChatDeleteCmd {
userID : userID ,
reqID : reqID ,
messageID : msgID ,
}, nil
},
MsgTypeTypingStart : func ( userID int64 , _ string , raw json . RawMessage ) ( Command , error ) {
var p struct {
ChannelID json . Number `json:"channel_id"`
}
if err := json . Unmarshal ( raw , & p ); err != nil {
return nil , fmt . Errorf ( "invalid typing_start payload: %w" , err )
}
chID , err := p . ChannelID . Int64 ()
if err != nil {
return nil , fmt . Errorf ( "channel_id must be integer: %w" , err )
}
if chID <= 0 {
return nil , fmt . Errorf ( "channel_id must be positive" )
}
return TypingStartCmd { userID : userID , channelID : chID }, nil
},
MsgTypePresenceUpdate : func ( userID int64 , _ string , raw json . RawMessage ) ( Command , error ) {
var p struct {
2026-08-01 22:06:14 +02:00
Status string `json:"status"`
CustomStatus * string `json:"custom_status"`
2026-04-05 19:03:22 +02:00
}
if err := json . Unmarshal ( raw , & p ); err != nil {
return nil , fmt . Errorf ( "invalid presence_update payload: %w" , err )
}
2026-08-01 22:06:14 +02:00
return PresenceUpdateCmd { userID : userID , status : p . Status , customStatus : p . CustomStatus }, nil
2026-04-05 19:03:22 +02:00
},
MsgTypeChannelFocus : func ( userID int64 , _ string , raw json . RawMessage ) ( Command , error ) {
var p struct {
ChannelID json . Number `json:"channel_id"`
}
if err := json . Unmarshal ( raw , & p ); err != nil {
return nil , fmt . Errorf ( "invalid channel_focus payload: %w" , err )
}
chID , err := p . ChannelID . Int64 ()
if err != nil {
return nil , fmt . Errorf ( "channel_id must be integer: %w" , err )
}
if chID <= 0 {
return nil , fmt . Errorf ( "channel_id must be positive" )
}
return ChannelFocusCmd { userID : userID , channelID : chID }, nil
},
2026-08-01 22:06:14 +02:00
MsgTypeMarkRead : func ( userID int64 , _ string , raw json . RawMessage ) ( Command , error ) {
var p struct {
ChannelID json . Number `json:"channel_id"`
}
if err := json . Unmarshal ( raw , & p ); err != nil {
return nil , fmt . Errorf ( "invalid mark_read payload: %w" , err )
}
chID , err := p . ChannelID . Int64 ()
if err != nil {
return nil , fmt . Errorf ( "channel_id must be integer: %w" , err )
}
if chID <= 0 {
return nil , fmt . Errorf ( "channel_id must be positive" )
}
return MarkReadCmd { userID : userID , channelID : chID }, nil
},
MsgTypeCallRing : func ( userID int64 , _ string , raw json . RawMessage ) ( Command , error ) {
chID , err := parseCallChannelID ( MsgTypeCallRing , raw )
if err != nil {
return nil , err
}
return CallRingCmd { userID : userID , channelID : chID }, nil
},
MsgTypeCallDecline : func ( userID int64 , _ string , raw json . RawMessage ) ( Command , error ) {
chID , err := parseCallChannelID ( MsgTypeCallDecline , raw )
if err != nil {
return nil , err
}
return CallDeclineCmd { userID : userID , channelID : chID }, nil
},
2026-04-05 19:03:22 +02:00
MsgTypeReactionAdd : func ( userID int64 , _ string , raw json . RawMessage ) ( Command , error ) {
var p struct {
MessageID json . Number `json:"message_id"`
Emoji string `json:"emoji"`
}
if err := json . Unmarshal ( raw , & p ); err != nil {
return nil , fmt . Errorf ( "invalid reaction_add payload: %w" , err )
}
msgID , err := p . MessageID . Int64 ()
if err != nil {
return nil , fmt . Errorf ( "message_id must be integer: %w" , err )
}
return ReactionAddCmd { userID : userID , messageID : msgID , emoji : p . Emoji }, nil
},
MsgTypeReactionRemove : func ( userID int64 , _ string , raw json . RawMessage ) ( Command , error ) {
var p struct {
MessageID json . Number `json:"message_id"`
Emoji string `json:"emoji"`
}
if err := json . Unmarshal ( raw , & p ); err != nil {
return nil , fmt . Errorf ( "invalid reaction_remove payload: %w" , err )
}
msgID , err := p . MessageID . Int64 ()
if err != nil {
return nil , fmt . Errorf ( "message_id must be integer: %w" , err )
}
return ReactionRemoveCmd { userID : userID , messageID : msgID , emoji : p . Emoji }, nil
},
MsgTypeVoiceJoin : func ( userID int64 , _ string , raw json . RawMessage ) ( Command , error ) {
var p struct {
ChannelID json . Number `json:"channel_id"`
}
if err := json . Unmarshal ( raw , & p ); err != nil {
return nil , fmt . Errorf ( "invalid voice_join payload: %w" , err )
}
chID , err := p . ChannelID . Int64 ()
if err != nil {
return nil , fmt . Errorf ( "channel_id must be integer: %w" , err )
}
if chID <= 0 {
return nil , fmt . Errorf ( "channel_id must be positive" )
}
return VoiceJoinCmd { userID : userID , channelID : chID }, nil
},
2026-04-07 10:10:38 +02:00
MsgTypeVoiceLeave : func ( userID int64 , _ string , _ json . RawMessage ) ( Command , error ) { //nolint:unparam // error always nil; signature dictated by map type
2026-04-05 19:03:22 +02:00
return VoiceLeaveCmd { userID : userID }, nil
},
2026-04-07 10:10:38 +02:00
MsgTypeVoiceTokenRefresh : func ( userID int64 , _ string , _ json . RawMessage ) ( Command , error ) { //nolint:unparam // error always nil; signature dictated by map type
2026-04-05 19:03:22 +02:00
return VoiceTokenRefreshCmd { userID : userID }, nil
},
MsgTypeVoiceMute : func ( userID int64 , _ string , raw json . RawMessage ) ( Command , error ) {
var p struct {
Muted bool `json:"muted"`
}
if err := json . Unmarshal ( raw , & p ); err != nil {
return nil , fmt . Errorf ( "invalid voice_mute payload: %w" , err )
}
return VoiceMuteCmd { userID : userID , muted : p . Muted }, nil
},
MsgTypeVoiceDeafen : func ( userID int64 , _ string , raw json . RawMessage ) ( Command , error ) {
var p struct {
Deafened bool `json:"deafened"`
}
if err := json . Unmarshal ( raw , & p ); err != nil {
return nil , fmt . Errorf ( "invalid voice_deafen payload: %w" , err )
}
return VoiceDeafenCmd { userID : userID , deafened : p . Deafened }, nil
},
MsgTypeVoiceCamera : func ( userID int64 , _ string , raw json . RawMessage ) ( Command , error ) {
var p struct {
Enabled bool `json:"enabled"`
}
if err := json . Unmarshal ( raw , & p ); err != nil {
return nil , fmt . Errorf ( "invalid voice_camera payload: %w" , err )
}
return VoiceCameraCmd { userID : userID , enabled : p . Enabled }, nil
},
MsgTypeVoiceScreenshare : func ( userID int64 , _ string , raw json . RawMessage ) ( Command , error ) {
var p struct {
Enabled bool `json:"enabled"`
}
if err := json . Unmarshal ( raw , & p ); err != nil {
return nil , fmt . Errorf ( "invalid voice_screenshare payload: %w" , err )
}
return VoiceScreenshareCmd { userID : userID , enabled : p . Enabled }, nil
},
2026-08-01 22:06:14 +02:00
MsgTypeVoiceModMute : func ( userID int64 , _ string , raw json . RawMessage ) ( Command , error ) {
var p struct {
ChannelID json . Number `json:"channel_id"`
UserID json . Number `json:"user_id"`
Muted bool `json:"muted"`
}
if err := json . Unmarshal ( raw , & p ); err != nil {
return nil , fmt . Errorf ( "invalid voice_mod_mute payload: %w" , err )
}
chID , targetID , err := parseModTarget ( p . ChannelID , p . UserID )
if err != nil {
return nil , err
}
return VoiceModMuteCmd { userID : userID , channelID : chID , targetID : targetID , muted : p . Muted }, nil
},
MsgTypeVoiceModDeafen : func ( userID int64 , _ string , raw json . RawMessage ) ( Command , error ) {
var p struct {
ChannelID json . Number `json:"channel_id"`
UserID json . Number `json:"user_id"`
Deafened bool `json:"deafened"`
}
if err := json . Unmarshal ( raw , & p ); err != nil {
return nil , fmt . Errorf ( "invalid voice_mod_deafen payload: %w" , err )
}
chID , targetID , err := parseModTarget ( p . ChannelID , p . UserID )
if err != nil {
return nil , err
}
return VoiceModDeafenCmd { userID : userID , channelID : chID , targetID : targetID , deafened : p . Deafened }, nil
},
MsgTypeVoiceModMove : func ( userID int64 , _ string , raw json . RawMessage ) ( Command , error ) {
var p struct {
UserID json . Number `json:"user_id"`
ToChannelID json . Number `json:"to_channel_id"`
}
if err := json . Unmarshal ( raw , & p ); err != nil {
return nil , fmt . Errorf ( "invalid voice_mod_move payload: %w" , err )
}
toChID , targetID , err := parseModTarget ( p . ToChannelID , p . UserID )
if err != nil {
return nil , err
}
return VoiceModMoveCmd { userID : userID , targetID : targetID , toChannelID : toChID }, nil
},
MsgTypeVoiceModKick : func ( userID int64 , _ string , raw json . RawMessage ) ( Command , error ) {
var p struct {
UserID json . Number `json:"user_id"`
}
if err := json . Unmarshal ( raw , & p ); err != nil {
return nil , fmt . Errorf ( "invalid voice_mod_kick payload: %w" , err )
}
targetID , err := p . UserID . Int64 ()
if err != nil {
return nil , fmt . Errorf ( "user_id must be integer: %w" , err )
}
if targetID <= 0 {
return nil , fmt . Errorf ( "user_id must be positive" )
}
return VoiceModKickCmd { userID : userID , targetID : targetID }, nil
},
2026-04-05 19:03:22 +02:00
MsgTypeVoiceE2EEAnnounce : func ( userID int64 , _ string , raw json . RawMessage ) ( Command , error ) {
var p struct {
PublicKey string `json:"public_key"`
2026-07-23 21:07:09 +02:00
Signature string `json:"signature"`
2026-04-05 19:03:22 +02:00
}
if err := json . Unmarshal ( raw , & p ); err != nil {
return nil , fmt . Errorf ( "invalid voice_e2ee_announce payload: %w" , err )
}
2026-07-23 21:07:09 +02:00
return VoiceE2EEAnnounceCmd { userID : userID , publicKey : p . PublicKey , signature : p . Signature }, nil
2026-04-05 19:03:22 +02:00
},
2026-07-20 11:41:58 +02:00
MsgTypeChatCommand : func ( userID int64 , reqID string , raw json . RawMessage ) ( Command , error ) {
var p struct {
ChannelID int64 `json:"channel_id"`
Command string `json:"command"`
Args [] string `json:"args"`
}
if err := json . Unmarshal ( raw , & p ); err != nil {
return nil , fmt . Errorf ( "invalid chat_command payload: %w" , err )
}
cmd := strings . TrimSpace ( p . Command )
if cmd == "" {
return nil , fmt . Errorf ( "command must not be empty" )
}
// Guard against a client flooding the plugin allocate/dispatch ABI.
if len ( p . Args ) > maxCommandArgs {
return nil , fmt . Errorf ( "too many command arguments (max %d)" , maxCommandArgs )
}
args := make ([] string , len ( p . Args ))
copy ( args , p . Args )
return ChatCommandCmd {
userID : userID ,
reqID : reqID ,
channelID : p . ChannelID ,
command : cmd ,
args : args ,
}, nil
},
2026-04-05 19:03:22 +02:00
MsgTypeVoiceE2EEOffer : func ( userID int64 , _ string , raw json . RawMessage ) ( Command , error ) {
var p struct {
TargetUserID int64 `json:"target_user_id"`
EncryptedKey string `json:"encrypted_key"`
IV string `json:"iv"`
}
if err := json . Unmarshal ( raw , & p ); err != nil {
return nil , fmt . Errorf ( "invalid voice_e2ee_offer payload: %w" , err )
}
return VoiceE2EEOfferCmd {
userID : userID ,
targetUserID : p . TargetUserID ,
encryptedKey : p . EncryptedKey ,
iv : p . IV ,
}, nil
},
}
// getCommandConstructor returns the constructor for a message type, if registered.
func getCommandConstructor ( msgType string ) ( func ( int64 , string , json . RawMessage ) ( Command , error ), bool ) {
ctor , ok := commandConstructors [ msgType ]
return ctor , ok
}