gateway

package module
v0.2.0-beta Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 10, 2023 License: MIT Imports: 17 Imported by: 0

README

Gateway

Code coverage PkgGoDev

A minimal implementation for the Discord gateway logic using the state pattern. The goal is to provide the Discord gateway behavior as a library to quickly build correct shard implementation. A functional shard implementation using github.com/gobwas/ws can be found at gatewayutil sub-package.

Design

A client is holds a state that affects how the next incoming message is processed. To begin with, the client is given a HelloState, which transitions into a ReadyState, which again transitions into a ConnectedState. Each state is named in accordance with each phase of the gateway connection setup guide, and are responsible for processing different Discord messages.

Different gateway client states

The different client methods takes the websocket connection as parameters in accordance with the io package (Reader, Writer), instead of writing an abstraction compliant wrapper of whatever websocket library you want to use.

A closed client is considered dead, and can not be used for future Discord events. A new client must be created. Specify the "dead client" as a parent allows the new client to potentially resume instead of creating a fresh session.

Live bot for testing

There is a bot running the gobwas code. Found in the cmd subdir. If you want to help out the "stress testing", you can add the bot here: https://discord.com/oauth2/authorize?scope=bot&client_id=792491747711123486&permissions=0

It only reads incoming events and waits to crash. Once any alerts such as warning, error, fatal, panic triggers; I get a notification so I can quickly patch the problem!

Support

  • operation codes
  • close codes
  • Intents
  • Events
  • JSON
  • ETF (see the encoding package)
  • Rate limit
    • Identify (local implementation)
    • Commands (local implementation)
  • Shard(s) manager
  • Buffer pool

Use the existing disgord channels for discussion

Discord Gophers Discord API

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrIdentifyRateLimited = fmt.Errorf("can't send identify command: %w", ErrRateLimited)
View Source
var ErrNotConnectedYet = errors.New("client is not in a connected state")
View Source
var ErrOutOfSync = errors.New("sequence number was out of sync")
View Source
var ErrRateLimited = errors.New("unable to send message to Discord due to hitting rate limited")
View Source
var ErrSequenceNumberSkipped = errors.New("the sequence number increased with more than 1, events lost")

Functions

This section is empty.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client provides a user target interface, for simplified Discord interaction.

Note: It's not suitable for internal processes/states.

func NewClient

func NewClient(options ...Option) (*Client, error)

func (*Client) Close

func (c *Client) Close(closeWriter io.Writer) error

func (*Client) ProcessNext

func (c *Client) ProcessNext(reader io.Reader, writer io.Writer) (*Payload, error)

ProcessNext processes the next Discord message and update state accordingly. On error, you are expected to call Client.Close to notify Discord about any issues accumulated in the Client.

func (*Client) ResumeURL

func (c *Client) ResumeURL() string

ResumeURL returns the URL to be used when dialing a new websocket connection. An empty string is returned when the shard can not be resumed, and you should instead use "Get Gateway Bot" endpoint to fetch the correct URL for connecting.

The client is assumed to have been correctly closed before calling this.

func (*Client) String

func (c *Client) String() string

func (*Client) Write

func (c *Client) Write(pipe io.Writer, evt event.Type, payload encoding.RawMessage) error

type ClosedState

type ClosedState struct {
}

func (*ClosedState) Process

func (st *ClosedState) Process(payload *Payload, _ io.Writer) error

func (*ClosedState) String

func (st *ClosedState) String() string

type ConnectedState

type ConnectedState struct {
	// contains filtered or unexported fields
}

ConnectedState handles any discord events after a successful gateway connection. The only possible state after this is the ClosedState or it's derivatives such as a resumable state.

See the Discord documentation for more information:

func (*ConnectedState) Process

func (st *ConnectedState) Process(payload *Payload, pipe io.Writer) error

func (*ConnectedState) String

func (st *ConnectedState) String() string

type DefaultHeartbeatHandler

type DefaultHeartbeatHandler struct {
	TextWriter io.Writer

	// ConnectionCloser assumes that by closing the connection we can trigger an interrupt signal for whatever process
	// that is busy reading/waiting for the next websocket frame/message. After an interrupt you are expected to call
	// Client.Close - allowing the client to properly update its internal state. This allows the client to be operated
	// be a single process, avoiding the need of locking complexity (see gatewayutil/shard.go for an example).
	//
	// If this doesn't achieve what you need/want, then implement you own version using the HeartbeatHandler interface.
	ConnectionCloser io.Closer
	// contains filtered or unexported fields
}

func (*DefaultHeartbeatHandler) Configure

func (p *DefaultHeartbeatHandler) Configure(ctx *StateCtx, interval time.Duration)

func (*DefaultHeartbeatHandler) Run

func (p *DefaultHeartbeatHandler) Run()

type DiscordError

type DiscordError struct {
	CloseCode closecode.Type
	OpCode    opcode.Type
	Reason    string
}

func (DiscordError) CanReconnect

func (c DiscordError) CanReconnect() bool

func (*DiscordError) Error

func (c *DiscordError) Error() string

type Handler

type Handler func(shardID ShardID, evt event.Type, data encoding.RawMessage)

type HeartbeatHandler

type HeartbeatHandler interface {
	Configure(ctx *StateCtx, interval time.Duration)
	Run()
}

type Hello

type Hello struct {
	HeartbeatIntervalMilli int64 `json:"heartbeat_interval"`
}

type HelloState

type HelloState struct {
	Identity *Identify
	// contains filtered or unexported fields
}

HelloState is one of several initial state for the client. It's responsibility are as follows

  1. Process incoming Hello event
  2. Initiate a heartbeat process
  3. Send Identify message
  4. Transition to the ReadyState

This state is responsible for handling the Hello phase of the gateway connection. See the Discord documentation for more information:

func (*HelloState) Process

func (st *HelloState) Process(payload *Payload, pipe io.Writer) error

func (*HelloState) String

func (st *HelloState) String() string

type Identify

type Identify struct {
	BotToken       string      `json:"token"`
	Properties     interface{} `json:"properties"`
	Compress       bool        `json:"compress,omitempty"`
	LargeThreshold uint8       `json:"large_threshold,omitempty"`
	Shard          [2]int      `json:"shard"`
	Presence       interface{} `json:"presence"`
	Intents        intent.Type `json:"intents"`
}

type IdentifyConnectionProperties

type IdentifyConnectionProperties struct {
	OS      string `json:"os"`
	Browser string `json:"browser"`
	Device  string `json:"device"`
}

type Logger

type Logger interface {
	// Debug low level insight in system behavior to assist diagnostic.
	Debug(format string, args ...interface{})

	// Info general information that might be interesting
	Info(format string, args ...interface{})

	// Warn creeping technical debt, such as dependency updates will cause the system to not compile/break.
	Warn(format string, args ...interface{})

	// Error recoverable events/issues that does not cause a system shutdown, but is also crucial and needs to be
	// dealt with quickly.
	Error(format string, args ...interface{})

	// Panic identifies system crashing/breaking issues that forces the application to shut down or completely stop
	Panic(format string, args ...interface{})
}

Logger for logging different situations

type Option

type Option func(client *Client) error

Option for initializing a new gateway client. An option must be deterministic regardless of when or how many times it is executed.

func WithBotToken

func WithBotToken(token string) Option

func WithCommandRateLimiter

func WithCommandRateLimiter(ratelimiter RateLimiter) Option

func WithDirectMessageEvents

func WithDirectMessageEvents(events ...event.Type) Option

func WithEventHandler

func WithEventHandler(handler Handler) Option

WithEventHandler provides a callback that is triggered on incoming events. Note that the allowlist will filter out events you have not requested.

Warning: this function call is blocking. You should not run heavy logic in the handler, preferably just forward it to a processing component. An example usage would be to send it to a buffered worker channel.

func WithExistingSession

func WithExistingSession(deadClient *Client) Option

func WithGuildEvents

func WithGuildEvents(events ...event.Type) Option

func WithHeartbeatHandler

func WithHeartbeatHandler(handler HeartbeatHandler) Option

WithHeartbeatHandler allows overwriting default heartbeat behavior. Basic behavior is achieved with the DefaultHeartbeatHandler:

 NewClient(
 	WithHeartbeatHandler(&DefaultHeartbeatHandler{
			TextWriter:
		})
 )

func WithIdentifyConnectionProperties

func WithIdentifyConnectionProperties(properties *IdentifyConnectionProperties) Option

func WithIdentifyRateLimiter

func WithIdentifyRateLimiter(ratelimiter RateLimiter) Option

func WithIntents

func WithIntents(intents intent.Type) Option

func WithLogger

func WithLogger(logger Logger) Option

func WithShardInfo

func WithShardInfo(id ShardID, count int) Option

type Payload

type Payload struct {
	Op        opcode.Type         `json:"op"`
	Data      encoding.RawMessage `json:"d"`
	Seq       int64               `json:"s,omitempty"`
	EventName event.Type          `json:"t,omitempty"`

	// CloseCode is a special case for this library.
	// You can specify an io.Reader which produces relevant closecode data
	// for correct handling of close frames
	// TODO: improve documentation
	CloseCode closecode.Type `json:"closecode,omitempty"`
}

func (Payload) String

func (p Payload) String() string

type RateLimiter

type RateLimiter interface {
	Try(ShardID) (bool, time.Duration)
}

type Ready

type Ready struct {
	SessionID        string `json:"session_id"`
	ResumeGatewayURL string `json:"resume_gateway_url"`
}

type ReadyState

type ReadyState struct {
	// contains filtered or unexported fields
}

ReadyState is responsibile for the Ready phase of the gateway connection. It's responsibilities are:

  1. Process incoming Ready event
  2. Cache relevant Discord session data
  3. Transition to the ConnectedState

See the Discord documentation for more information:

func (*ReadyState) Process

func (st *ReadyState) Process(payload *Payload, _ io.Writer) error

func (*ReadyState) String

func (st *ReadyState) String() string

type ResumableClosedState

type ResumableClosedState struct {
	// contains filtered or unexported fields
}

func (*ResumableClosedState) Process

func (st *ResumableClosedState) Process(payload *Payload, _ io.Writer) error

func (*ResumableClosedState) String

func (st *ResumableClosedState) String() string

type Resume

type Resume struct {
	BotToken       string `json:"token"`
	SessionID      string `json:"session_id"`
	SequenceNumber int64  `json:"seq"`
}

type ResumeState

type ResumeState struct {
	// contains filtered or unexported fields
}

ResumeState wraps a ConnectedState until a Resumed event is received from Discord...

func (*ResumeState) Process

func (st *ResumeState) Process(payload *Payload, pipe io.Writer) error

func (*ResumeState) String

func (st *ResumeState) String() string

type ShardID

type ShardID uint

type State

type State interface {
	fmt.Stringer
	Process(payload *Payload, pipe io.Writer) error
}

type StateCloser

type StateCloser interface {
	State
	Close(closeWriter io.Writer) error
}

StateCloser any state implementing a Close method may overwrite the default behavior of StateCtx.Close

type StateCtx

type StateCtx struct {
	SessionID        string
	ResumeGatewayURL string
	// contains filtered or unexported fields
}

func (*StateCtx) Close

func (ctx *StateCtx) Close(closeWriter io.Writer) error

func (*StateCtx) CloseCodeHandler

func (ctx *StateCtx) CloseCodeHandler(payload *Payload) error

func (*StateCtx) Process

func (ctx *StateCtx) Process(payload *Payload, pipe io.Writer) error

func (*StateCtx) SessionIssueHandler

func (ctx *StateCtx) SessionIssueHandler(payload *Payload) error

func (*StateCtx) SetState

func (ctx *StateCtx) SetState(state State)

func (*StateCtx) String

func (ctx *StateCtx) String() string

func (*StateCtx) Write

func (ctx *StateCtx) Write(pipe io.Writer, evt event.Type, payload encoding.RawMessage) error

func (*StateCtx) WriteNormalClose

func (ctx *StateCtx) WriteNormalClose(pipe io.Writer) error

func (*StateCtx) WriteRestartClose

func (ctx *StateCtx) WriteRestartClose(pipe io.Writer) error

Directories

Path Synopsis
log
internal
generate/events command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL