1
0
Fork 0
mirror of https://github.com/diamondburned/arikawa.git synced 2024-12-04 20:54:43 +00:00
arikawa/gateway/identify.go
diamondburned 54cadd2f45 gateway: Refactor for a better concurrent API
This commit refactors the whole package gateway as well as utils/ws
(formerly utils/wsutil) and voice/voicegateway. The new refactor
utilizes a design pattern involving a concurrent loop and an arriving
event channel.

An additional change was made to the way gateway events are typed.
Before, pretty much any type will satisfy a gateway event type, since
the actual type was just interface{}. The new refactor defines a
concrete interface that events can implement:

    type Event interface {
        Op() OpCode
        EventType() EventType
    }

Using this interface, the user can easily add custom gateway events
independently of the library without relying on string maps. This adds a
lot of type safety into the library and makes type-switching on Event
types much more reasonable.

Gateway error callbacks are also almost entirely removed in favor of
custom gateway events. A catch-all can easily be added like this:

    s.AddHandler(func(err error) {
        log.Println("gateway error:, err")
    })
2021-12-14 13:49:34 -08:00

223 lines
6.9 KiB
Go

package gateway
import (
"context"
"runtime"
"strings"
"time"
"github.com/diamondburned/arikawa/v3/api"
"github.com/diamondburned/arikawa/v3/discord"
"github.com/diamondburned/arikawa/v3/utils/json/option"
"github.com/pkg/errors"
"golang.org/x/time/rate"
)
// DefaultPresence is used as the default presence when initializing a new
// Gateway.
var DefaultPresence *UpdatePresenceCommand
// Identifier is a wrapper around IdentifyCommand to add in appropriate rate
// limiters.
type Identifier struct {
IdentifyCommand
IdentifyShortLimit *rate.Limiter `json:"-"` // optional
IdentifyGlobalLimit *rate.Limiter `json:"-"` // optional
}
// DefaultIdentifier creates a new default Identifier
func DefaultIdentifier(token string) Identifier {
return NewIdentifier(DefaultIdentifyCommand(token))
}
// NewIdentifier creates a new identifier with the given IdentifyCommand and
// default rate limiters.
func NewIdentifier(data IdentifyCommand) Identifier {
return Identifier{
IdentifyCommand: data,
IdentifyShortLimit: rate.NewLimiter(rate.Every(5*time.Second), 1),
IdentifyGlobalLimit: rate.NewLimiter(rate.Every(24*time.Hour), 1000),
}
}
// Wait waits for the rate limiters to pass. If a limiter is nil, then it will
// not be used to wait. This is useful
func (id *Identifier) Wait(ctx context.Context) error {
if id.IdentifyShortLimit != nil {
if err := id.IdentifyShortLimit.Wait(ctx); err != nil {
return errors.Wrap(err, "can't wait for short limit")
}
}
if id.IdentifyGlobalLimit != nil {
if err := id.IdentifyGlobalLimit.Wait(ctx); err != nil {
return errors.Wrap(err, "can't wait for global limit")
}
}
return nil
}
// QueryGateway queries the gateway for the URL and updates the Identifier with
// the appropriate information.
func (id *Identifier) QueryGateway(ctx context.Context) (gatewayURL string, err error) {
var botData *api.BotData
if strings.HasPrefix(id.Token, "Bot ") {
botData, err = BotURL(ctx, id.Token)
if err != nil {
return "", errors.Wrap(err, "failed to get bot data")
}
gatewayURL = botData.URL
} else {
gatewayURL, err = URL(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to get gateway endpoint")
}
}
// Use the supplied connect rate limit, if any.
if botData != nil && botData.StartLimit != nil {
resetAt := time.Now().Add(botData.StartLimit.ResetAfter.Duration())
limiter := id.IdentifyGlobalLimit
// Update the burst to be the current given time and reset it back to
// the default when the given time is reached.
limiter.SetBurst(botData.StartLimit.Remaining)
limiter.SetBurstAt(resetAt, botData.StartLimit.Total)
// Update the maximum number of identify requests allowed per 5s.
id.IdentifyShortLimit.SetBurst(botData.StartLimit.MaxConcurrency)
}
return
}
// DefaultIdentity is used as the default identity when initializing a new
// Gateway.
var DefaultIdentity = IdentifyProperties{
OS: runtime.GOOS,
Browser: "Arikawa",
Device: "Arikawa",
}
// IdentifyCommand is a command for Op 2. It is the struct for a data that's
// sent over in an Identify command.
type IdentifyCommand struct {
Token string `json:"token"`
Properties IdentifyProperties `json:"properties"`
Compress bool `json:"compress,omitempty"` // true
LargeThreshold uint `json:"large_threshold,omitempty"` // 50
Shard *Shard `json:"shard,omitempty"` // [ shard_id, num_shards ]
Presence *UpdatePresenceCommand `json:"presence,omitempty"`
// ClientState is the client state for a user's accuont. Bot accounts should
// NOT touch this field.
ClientState *ClientState `json:"client_state,omitempty"`
// Capabilities defines the client's capabilities when connecting to the
// gateway with a user account. Bot accounts should NOT touch this field.
// The official client sets this at 125 at the time of this commit.
Capabilities int `json:"capabilities,omitempty"`
// Intents specifies which groups of events the gateway
// connection will receive.
//
// For user accounts, it must be nil.
//
// For bot accounts, it must not be nil, and
// Gateway.AddIntents(0) can be used if you want to
// specify no intents.
Intents option.Uint `json:"intents"`
}
// DefaultIdentifyCommand creates a default IdentifyCommand with the given token.
func DefaultIdentifyCommand(token string) IdentifyCommand {
return IdentifyCommand{
Token: token,
Properties: DefaultIdentity,
Presence: DefaultPresence,
Compress: true,
LargeThreshold: 50,
}
}
// SetShard is a helper function to set the shard configuration inside
// IdentifyCommand.
func (i *IdentifyCommand) SetShard(id, num int) {
if i.Shard == nil {
i.Shard = new(Shard)
}
i.Shard[0], i.Shard[1] = id, num
}
// AddIntents adds gateway intents into the identify data.
func (i *IdentifyCommand) AddIntents(intents Intents) {
if i.Intents == nil {
i.Intents = option.NewUint(uint(intents))
} else {
*i.Intents |= uint(intents)
}
}
// HasIntents reports if the Gateway has the passed Intents.
//
// If no intents are set, e.g. if using a user account, HasIntents will always
// return true.
func (i *IdentifyCommand) HasIntents(intents Intents) bool {
if i.Intents == nil {
return true
}
return Intents(*i.Intents).Has(intents)
}
type IdentifyProperties struct {
// Required
OS string `json:"os"` // GOOS
Browser string `json:"browser"` // Arikawa
Device string `json:"device"` // Arikawa
// Optional
BrowserUserAgent string `json:"browser_user_agent,omitempty"`
BrowserVersion string `json:"browser_version,omitempty"`
OSVersion string `json:"os_version,omitempty"`
Referrer string `json:"referrer,omitempty"`
ReferringDomain string `json:"referring_domain,omitempty"`
}
// Shard is a type for two numbers that represent the Bot's shard configuration.
// The first number is the shard's ID, which could be obtained through the
// ShardID method. The second number is the total number of shards, which could
// be obtained through the NumShards method.
type Shard [2]int
// DefaultShard returns the default shard configuration of 1 shard total, in
// which the current shard ID is 0.
var DefaultShard = &Shard{0, 1}
// ShardID returns the current shard's ID. It uses the first number.
func (s Shard) ShardID() int {
return s[0]
}
// NumShards returns the total number of shards. It uses the second number.
func (s Shard) NumShards() int {
return s[1]
}
// ClientState describes the undocumented client_state field in the Identify
// command. Little is known about this type.
type ClientState struct {
GuildHashes map[discord.GuildID]interface{} `json:"guild_hashes"` // {}
HighestLastMessageID discord.MessageID `json:"highest_last_message_id"` // "0"
ReadStateVersion int `json:"read_state_version"` // 0
UserGuildSettingsVersion int `json:"user_guild_settings_version"` // -1
UserSettingsVersion int `json:"user_settings_version"` // -1
}