gateway

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 2, 2022 License: MIT Imports: 21 Imported by: 0

README

Code coverage PkgGoDev

Use the existing disgord channels for discussion

Discord Gophers Discord API

Features

  • Complete control of goroutines (if desired)
  • Specify intents or GuildEvents & DirectMessageEvents
    • When events are used; intents are derived and redundant events pruned as soon as they are identified
  • Receive Gateway events
  • Send Gateway commands
  • context support
  • Control over reconnect, disconnect, or behavior for handling discord errors

Design decisions

see DESIGN.md

Simple shard example

This code uses github.com/gobwas/ws, but you are free to use other websocket implementations as well. You just have to write your own Shard implementation and use GatewayState. See shard/shard.go for inspiration.

Here no handler is registered. Simply replace nil with a function pointer to read events (events with operation code 0).

Create a shard instance using the gatewayshard package:

package main

import (
   "context"
   "errors"
   "fmt"
   "github.com/discordpkg/gateway"
   "github.com/discordpkg/gateway/event"
   "github.com/discordpkg/gateway/intent"
   "github.com/discordpkg/gateway/log"
   "github.com/discordpkg/gateway/gatewayshard"
   "net"
   "os"
)

func main() {
   shard, err := gatewayshard.NewShard(0, os.Getenv("DISCORD_TOKEN"), nil,
      discordgateway.WithGuildEvents(event.All()...),
      discordgateway.WithDirectMessageEvents(intent.Events(intent.DirectMessageReactions)),
      discordgateway.WithIdentifyConnectionProperties(&discordgateway.IdentifyConnectionProperties{
         OS:      runtime.GOOS,
         Browser: "github.com/discordpkg/gateway v0",
         Device:  "tester",
      }),
   )
   if err != nil {
      log.Fatal(err)
   }

   dialUrl := "wss://gateway.discord.gg/?v=9&encoding=json"

You can then open a connection to discord and start listening for events. The event loop will continue to run until the connection is lost or a process failed (json unmarshal/marshal, websocket frame issue, etc.)

You can use the helper methods for the DiscordError to decide when to reconnect:

reconnectStage:
    if _, err := shard.Dial(context.Background(), dialUrl); err != nil {
        log.Fatal("failed to open websocket connection. ", err)
    }

   if err = shard.EventLoop(context.Background()); err != nil {
      reconnect := true

      var discordErr *discordgateway.DiscordError
      if errors.As(err, &discordErr) {
         reconnect = discordErr.CanReconnect()
      }

      if reconnect {
         logger.Infof("reconnecting: %s", discordErr.Error())
         if err := shard.PrepareForReconnect(); err != nil {
            logger.Fatal("failed to prepare for reconnect:", err)
         }
         goto reconnectStage
      }
   }
}

Or manually check the close code, operation code, or error:

reconnectStage:
   if _, err := shard.Dial(context.Background(), dialUrl); err != nil {
      log.Fatal("failed to open websocket connection. ", err)
   }

   if op, err := shard.EventLoop(context.Background()); err != nil {
      var discordErr *discordgateway.DiscordError
      if errors.As(err, &discordErr) {
         switch discordErr.CloseCode {
         case 1001, 4000: // will initiate a resume
            fallthrough
         case 4007, 4009: // will do a fresh identify
            if err := shard.PrepareForReconnect(); err != nil {
                logger.Fatal("failed to prepare for reconnect:", err)
            }
            goto reconnectStage
         case 4001, 4002, 4003, 4004, 4005, 4008, 4010, 4011, 4012, 4013, 4014:
         default:
            log.Error(fmt.Errorf("unhandled close error, with discord op code(%d): %d", op, discordErr.Code))
         }
      }
      if errors.Is(err, net.ErrClosed) {
         log.Debug("connection closed/lost .. will try to reconnect")

         if err := shard.PrepareForReconnect(); err != nil {
            logger.Fatal("failed to prepare for reconnect:", err)
         }
         goto reconnectStage
      }
   } else {
      if err := shard.PrepareForReconnect(); err != nil {
        logger.Fatal("failed to prepare for reconnect:", err)
      }
      goto reconnectStage
   }
}

Gateway command

To request guild members, update voice state or update presence, you can utilize Shard.Write or GatewayState.Write (same logic). The bytes argument should not contain the discord payload wrapper (operation code, event name, etc.), instead you write only the inner object and specify the relevant operation code.

Calling Write(..) before dial or instantiating a net.Conn object will cause the process to fail. You must be connected.


package main

import (
	"context"
	"fmt"
	"github.com/discordpkg/gateway"
	"github.com/discordpkg/gateway/event"
	"github.com/discordpkg/gateway/opcode"
	"github.com/discordpkg/gateway/command"
	"github.com/discordpkg/gateway/gatewayshard"
	"os"
)

func main() {
	shard, err := gatewayshard.NewShard(0, os.Getenv("DISCORD_TOKEN"), nil,
		discordgateway.WithIntents(intent.Guilds),
	)
	if err != nil {
		panic(err)
	}

	dialUrl := "wss://gateway.discord.gg/?v=9&encoding=json"
	if _, err := shard.Dial(context.Background(), dialUrl); err != nil {
       panic(fmt.Errorf("failed to open websocket connection. ", err))
	}

   // ...
   
	req := `{"guild_id":"23423","limit":0,"query":""}`
	if err := shard.Write(command.RequestGuildMembers, []byte(req)); err != nil {
       panic(fmt.Errorf("failed to request guild members", err))
    }
    
}

If you need to manually set the intent value for whatever reason, the ShardConfig exposes an "Intents" field. Note that intents will still be derived from DMEvents and GuildEvents and added to the final intents value used to identify.

Identify rate limit

When you have multiple shards, you must inject a rate limiter for identify. The CommandRateLimitChan is optional in either case. When no rate limiter for identifies are injected, one is created with the standard 1 identify per 5 second.

See the IdentifyRateLimiter interface for minimum implementation.

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

  • Voice
    • operation codes
    • close codes
  • Gateway
    • operation codes
    • close codes
    • Intents
    • Events
    • Commands
    • JSON
    • ETF
    • Rate limit
      • Identify
      • Commands
  • Shard(s) manager
  • Buffer pool

Documentation

Index

Constants

View Source
const (
	NormalCloseCode  uint16 = 1000
	RestartCloseCode uint16 = 1012
)

Variables

View Source
var ErrIncompleteDialURL = errors.New("incomplete url is missing one or many of: 'version', 'encoding', 'scheme'")
View Source
var ErrSequenceNumberSkipped = errors.New("the sequence number increased with more than 1, events lost")
View Source
var ErrURLScheme = errors.New("url scheme was not websocket (ws nor wss)")
View Source
var ErrUnsupportedAPICodec = fmt.Errorf("only %+v is supported", supportedAPICodes)
View Source
var ErrUnsupportedAPIVersion = fmt.Errorf("only discord api version %+v is supported", supportedAPIVersions)

Functions

func NewCommandRateLimiter

func NewCommandRateLimiter() (<-chan int, io.Closer)

func NewIdentifyRateLimiter

func NewIdentifyRateLimiter() (<-chan int, io.Closer)

func NewRateLimiter

func NewRateLimiter(burstCapacity int, burstDuration time.Duration) (<-chan int, io.Closer)

func ValidateDialURL

func ValidateDialURL(URLString string) (string, error)

Types

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 GatewayPayload

type GatewayPayload struct {
	Op        opcode.Type     `json:"op"`
	Data      json.RawMessage `json:"d"`
	Seq       int64           `json:"s,omitempty"`
	EventName event.Type      `json:"t,omitempty"`
	Outdated  bool            `json:"-"`
}

type GatewayState

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

GatewayState should be discarded after the connection has closed. reconnect must create a new shard instance.

func NewGatewayState

func NewGatewayState(botToken string, options ...Option) (*GatewayState, error)

func (*GatewayState) Close

func (gs *GatewayState) Close() error

func (*GatewayState) EventIsWhitelisted

func (gs *GatewayState) EventIsWhitelisted(evt event.Type) bool

func (*GatewayState) HaveIdentified

func (gs *GatewayState) HaveIdentified() bool

func (*GatewayState) HaveSessionID

func (gs *GatewayState) HaveSessionID() bool

func (*GatewayState) Heartbeat

func (gs *GatewayState) Heartbeat(client io.Writer) error

Heartbeat Close method may be used if Write fails

func (*GatewayState) Identify

func (gs *GatewayState) Identify(client io.Writer) error

Identify Close method may be used if Write fails

func (*GatewayState) InvalidateSession

func (gs *GatewayState) InvalidateSession(closeWriter io.Writer)

func (*GatewayState) ProcessCloseCode

func (gs *GatewayState) ProcessCloseCode(code closecode.Type, reason string, closeWriter io.Writer) error

ProcessCloseCode process close code sent by discord

func (*GatewayState) ProcessNextMessage

func (gs *GatewayState) ProcessNextMessage(pipe io.Reader, textWriter, closeWriter io.Writer) (payload *GatewayPayload, redundant bool, err error)

func (*GatewayState) ProcessPayload

func (gs *GatewayState) ProcessPayload(payload *GatewayPayload, textWriter, closeWriter io.Writer) (redundant bool, err error)

func (*GatewayState) Read

func (gs *GatewayState) Read(client io.Reader) (*GatewayPayload, int, error)

func (*GatewayState) Resume

func (gs *GatewayState) Resume(client io.Writer) error

Resume Close method may be used if Write fails

func (*GatewayState) SessionID

func (gs *GatewayState) SessionID() string

func (*GatewayState) String

func (gs *GatewayState) String() string

func (*GatewayState) Write

func (gs *GatewayState) Write(client io.Writer, opc command.Type, payload json.RawMessage) (err error)

type Handler

type Handler func(ShardID, event.Type, RawMessage)

type HandlerStruct

type HandlerStruct struct {
	ShardID
	Name event.Type
	Data RawMessage
}

type Hello

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

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]uint     `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 IdentifyRateLimiter

type IdentifyRateLimiter interface {
	Take(ShardID) bool
}

type Option

type Option func(st *GatewayState) error

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

func WithCommandRateLimiter

func WithCommandRateLimiter(ratelimiter <-chan int) Option

func WithDirectMessageEvents

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

func WithGuildEvents

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

func WithIdentifyConnectionProperties

func WithIdentifyConnectionProperties(properties *IdentifyConnectionProperties) Option

func WithIdentifyRateLimiter

func WithIdentifyRateLimiter(ratelimiter IdentifyRateLimiter) Option

func WithIntents

func WithIntents(intents intent.Type) Option

func WithSequenceNumber

func WithSequenceNumber(seq int64) Option

func WithSessionID

func WithSessionID(id string) Option

func WithShardCount

func WithShardCount(count uint) Option

func WithShardID

func WithShardID(id ShardID) Option

type RawMessage

type RawMessage = json.RawMessage

type Ready

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

type Resume

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

type ShardID

type ShardID uint

func DeriveShardID

func DeriveShardID(snowflake uint64, totalNumberOfShards uint) ShardID

Directories

Path Synopsis
internal
log

Jump to

Keyboard shortcuts

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