package rpcfetch import ( "encoding/json" "errors" "fmt" ) var ( ErrNonce = errors.New("nonce mismatch") ) const ( // Dispatch indicates an event was dispatched. Dispatch = iota // Heartbeat is fired periodically by the client to keep the connection alive. Heartbeat // Identify starts a new session during the initial handshake. Identify // PresenceUpdate update the client's presence. PresenceUpdate // VoiceStateUpdate is used to join/leave or move between voice channels. VoiceStateUpdate // Resume a previous session that was disconnected. Resume // Reconnect indicates you should attempt to reconnect and resume immediately. Reconnect // RequestGuildMembers requests information about offline guild members in a large guild. RequestGuildMembers // InvalidSession indicates that the session has been invalidated. You should reconnect and identify/resume accordingly. InvalidSession // Hello is sent immediately after connecting, contains the heartbeat_interval to use. Hello // HeartbeatACK is sent in response to receiving a heartbeat to acknowledge that it has been received. HeartbeatACK ) // nonceEventCommandTracer exposes tracing of the Command, Event and nonce field to the generic validateRaw function type nonceEventCommandTracer interface { traceEvent(event string) *string traceCommand(command string) *string nonce(n string) bool } type payload struct { // Command is the payload command. // https://discord.com/developers/docs/topics/rpc#commands-and-events-rpc-commands Command string `json:"cmd"` // Nonce is a unique string used once for replies from the server, present in responses to commands (not subscribed) Nonce string `json:"nonce,omitempty"` // Event is the subscription event, present in subscribed events, errors and (un)subscribing events // https://discord.com/developers/docs/topics/rpc#commands-and-events-rpc-events Event string `json:"evt,omitempty"` } func (p payload) traceEvent(event string) *string { if event == p.Event { return nil } return &p.Event } func (p payload) traceCommand(command string) *string { if command == p.Command { return nil } return &p.Command } func (p payload) nonce(n string) bool { return p.Nonce == n } type Command[A any] struct { // Arguments are command arguments, present in commands sent to the server Arguments A `json:"args,omitempty"` payload } type Response[D any] struct { // Data is the event data, present in responses from the server Data D `json:"data,omitempty"` payload } type Config struct { APIEndpoint string `json:"api_endpoint"` CDNHost string `json:"cdn_host"` Environment string `json:"environment"` } type User struct { Avatar string `json:"avatar"` Discriminator string `json:"discriminator"` Flags int `json:"flags"` ID string `json:"id"` PremiumType int `json:"premium_type"` Username string `json:"username"` } type ReadyData struct { Config Config `json:"config"` User User `json:"user"` Version int `json:"v"` } func (d *Client) activate() error { // this function will be used in every method requiring an active client // therefore it silently succeeds on an already active client if d.active { return nil } if !d.dialed { if err := d.dial(); err != nil { return err } } // do Handshake if _, resp, err := validateRaw[Response[ReadyData]](Heartbeat, "READY", "DISPATCH", "initial-ready")( d, Dispatch, struct { Version int `json:"v"` ID string `json:"client_id"` }{1, d.id}); err != nil { return err } else { d.config = &resp.Data.Config d.user = &resp.Data.User d.active = true } return nil } func validateRaw[T nonceEventCommandTracer](opcode uint32, event, command, nonce string) func( d *Client, opcode uint32, payload any) (uint32, T, error) { return func(d *Client, opcodeP uint32, p any) (uint32, T, error) { opcodeR, resp, err := Raw[T](d, opcodeP, p) eventR := resp.traceEvent(event) commandR := resp.traceCommand(command) if err == nil { switch { case opcodeR != opcode: // the socket is still open however // as far as I'm aware this state is not recoverable // therefore we close the connection and advise a retry if opcodeR == Identify { // clean up as much as possible _ = d.Close() // advise retry err = ErrAgain } else { debugResponse(resp) err = fmt.Errorf("received unexpected opcode %d", opcodeR) } case !resp.nonce(nonce): err = ErrNonce case eventR != nil: debugResponse(resp) err = fmt.Errorf("received unexpected event %s", *eventR) case commandR != nil: debugResponse(resp) err = fmt.Errorf("received unexpected command %s", *commandR) } } return opcodeR, resp, err } } func debugResponse(resp any) { if d, err := json.Marshal(resp); err != nil { fmt.Printf("error dumping response: %s\n", err) } else { fmt.Println(string(d)) } }