discordgateway

package module
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Jan 7, 2022 License: MIT Imports: 22 Imported by: 1

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/andersfylling/discordgateway"
   "github.com/andersfylling/discordgateway/event"
   "github.com/andersfylling/discordgateway/intent"
   "github.com/andersfylling/discordgateway/log"
   "github.com/andersfylling/discordgateway/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/andersfylling/discordgateway 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/andersfylling/discordgateway"
	"github.com/andersfylling/discordgateway/event"
	"github.com/andersfylling/discordgateway/opcode"
	"github.com/andersfylling/discordgateway/command"
	"github.com/andersfylling/discordgateway/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 added in v0.2.0

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

func NewIdentifyRateLimiter added in v0.2.0

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

func NewRateLimiter added in v0.2.0

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

func ValidateDialURL added in v0.4.0

func ValidateDialURL(URLString string) (string, error)

Types

type DiscordError added in v0.5.0

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

func (DiscordError) CanReconnect added in v0.5.0

func (c DiscordError) CanReconnect() bool

func (*DiscordError) Error added in v0.5.0

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 added in v0.2.0

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 added in v0.5.0

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

func (*GatewayState) Close added in v0.2.0

func (gs *GatewayState) Close() error

func (*GatewayState) EventIsWhitelisted added in v0.5.0

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

func (*GatewayState) HaveIdentified added in v0.2.0

func (gs *GatewayState) HaveIdentified() bool

func (*GatewayState) HaveSessionID added in v0.2.0

func (gs *GatewayState) HaveSessionID() bool

func (*GatewayState) Heartbeat added in v0.2.0

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

Heartbeat Close method may be used if Write fails

func (*GatewayState) Identify added in v0.2.0

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

Identify Close method may be used if Write fails

func (*GatewayState) InvalidateSession added in v0.2.0

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

func (*GatewayState) ProcessCloseCode added in v0.5.0

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

ProcessCloseCode process close code sent by discord

func (*GatewayState) ProcessNextMessage added in v0.5.0

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

func (*GatewayState) ProcessPayload added in v0.5.0

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

func (*GatewayState) Read added in v0.2.0

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

func (*GatewayState) Resume added in v0.2.0

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

Resume Close method may be used if Write fails

func (*GatewayState) SessionID added in v0.5.0

func (gs *GatewayState) SessionID() string

func (*GatewayState) String added in v0.5.0

func (gs *GatewayState) String() string

func (*GatewayState) Write added in v0.2.0

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 added in v0.2.0

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

type Hello added in v0.5.0

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

type Identify added in v0.5.0

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 added in v0.5.0

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

type IdentifyRateLimiter added in v0.3.0

type IdentifyRateLimiter interface {
	Take(ShardID) bool
}

type Option added in v0.5.0

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 added in v0.5.0

func WithCommandRateLimiter(ratelimiter <-chan int) Option

func WithDirectMessageEvents added in v0.5.0

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

func WithGuildEvents added in v0.5.0

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

func WithIdentifyConnectionProperties added in v0.5.0

func WithIdentifyConnectionProperties(properties *IdentifyConnectionProperties) Option

func WithIdentifyRateLimiter added in v0.5.0

func WithIdentifyRateLimiter(ratelimiter IdentifyRateLimiter) Option

func WithIntents added in v0.5.0

func WithIntents(intents intent.Type) Option

func WithSequenceNumber added in v0.5.0

func WithSequenceNumber(seq int64) Option

func WithSessionID added in v0.5.0

func WithSessionID(id string) Option

func WithShardCount added in v0.5.0

func WithShardCount(count uint) Option

func WithShardID added in v0.5.0

func WithShardID(id ShardID) Option

type RawMessage

type RawMessage = json.RawMessage

type Ready added in v0.5.0

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

type Resume added in v0.5.0

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

type ShardID

type ShardID uint

func DeriveShardID added in v0.4.0

func DeriveShardID(snowflake uint64, totalNumberOfShards uint) ShardID

Directories

Path Synopsis
internal
voice

Jump to

Keyboard shortcuts

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