diff --git a/README.md b/README.md index bec260a..5c117b0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # arikawa [![Godoc Reference](https://img.shields.io/badge/godoc-reference-blue?style=flat-square )](https://godoc.org/github.com/diamondburned/arikawa) -[![ Examples](https://img.shields.io/badge/Example-__example%2F-blueviolet?style=flat-square)]() +[![ Examples](https://img.shields.io/badge/Example-__example%2F-blueviolet?style=flat-square)](https://github.com/diamondburned/arikawa/tree/master/_example) [![ Discord nixhub](https://img.shields.io/badge/Discord-nixhub-7289da?style=flat-square )](https://discord.gg/kF9mYBV ) [![ Hime Arikawa](https://img.shields.io/badge/Hime-Arikawa-ea75a2?style=flat-square )](https://hime-goto.fandom.com/wiki/Hime_Arikawa ) diff --git a/_example/simple/main.go b/_example/simple/main.go new file mode 100644 index 0000000..4869f19 --- /dev/null +++ b/_example/simple/main.go @@ -0,0 +1,45 @@ +package main + +import ( + "log" + "os" + + "github.com/diamondburned/arikawa/gateway" + "github.com/diamondburned/arikawa/session" +) + +// To run, do `BOT_TOKEN="TOKEN HERE" go run .` + +func main() { + var token = os.Getenv("BOT_TOKEN") + if token == "" { + log.Fatalln("No $BOT_TOKEN given.") + } + + s, err := session.New("Bot " + token) + if err != nil { + log.Fatalln("Session failed:", err) + } + + s.AddHandler(func(c *gateway.MessageCreateEvent) { + log.Println(c.Author.Username, "sent", c.Content) + }) + + if err := s.Open(); err != nil { + log.Fatalln("Failed to connect:", err) + } + + defer s.Close() + + u, err := s.Me() + if err != nil { + log.Fatalln("Failed to get myself:", err) + } + + log.Println("Started as", u.Username) + + // Wait is optional. + if err := s.Wait(); err != nil { + log.Fatalln("Fatal error:", err) + } +} diff --git a/discord/snowflake.go b/discord/snowflake.go index 5765719..24cc689 100644 --- a/discord/snowflake.go +++ b/discord/snowflake.go @@ -1,32 +1,47 @@ package discord import ( - "bytes" "strconv" + "strings" "time" ) const DiscordEpoch = 1420070400000 * int64(time.Millisecond) -type Snowflake uint64 +type Snowflake int64 func NewSnowflake(t time.Time) Snowflake { return Snowflake(TimeToDiscordEpoch(t) << 22) } +const Me = Snowflake(-1) + func (s *Snowflake) UnmarshalJSON(v []byte) error { - v = bytes.Trim(v, `"`) - u, err := strconv.ParseUint(string(v), 10, 64) + id := strings.Trim(string(v), `"`) + if id == "null" { + return nil + } + + i, err := strconv.ParseInt(id, 10, 64) if err != nil { return err } - *s = Snowflake(u) + *s = Snowflake(i) return nil } func (s *Snowflake) MarshalJSON() ([]byte, error) { - return []byte(`"` + strconv.FormatUint(uint64(*s), 10) + `"`), nil + var id string + + switch i := int64(*s); i { + case -1: // @me + id = "@me" + default: + id = strconv.FormatInt(i, 10) + } + + return []byte(`"` + id + `"`), nil } func (s Snowflake) String() string { diff --git a/discord/time.go b/discord/time.go index bc0a366..30fca0b 100644 --- a/discord/time.go +++ b/discord/time.go @@ -2,6 +2,7 @@ package discord import ( "encoding/json" + "strings" "time" ) @@ -15,7 +16,12 @@ var ( ) func (t *Timestamp) UnmarshalJSON(v []byte) error { - r, err := time.Parse(TimestampFormat, string(v)) + str := strings.Trim(string(v), `"`) + if str == "null" { + return nil + } + + r, err := time.Parse(TimestampFormat, str) if err != nil { return err } diff --git a/gateway/gateway.go b/gateway/gateway.go index 60b476c..42f6cc6 100644 --- a/gateway/gateway.go +++ b/gateway/gateway.go @@ -40,6 +40,9 @@ var ( WSRetries = uint(5) // WSError is the default error handler WSError = func(err error) {} + // WSExtraReadTimeout is the duration to be added to Hello, as a read + // timeout for the websocket. + WSExtraReadTimeout = time.Second ) var ( @@ -138,7 +141,7 @@ func NewGatewayWithDriver(token string, driver json.Driver) (*Gateway, error) { g.WS = ws // Try and dial it - return g, g.connect() + return g, nil } // Close closes the underlying Websocket connection. @@ -158,7 +161,53 @@ func (g *Gateway) Reconnect() error { // Close, but we don't care about the error (I think) g.Close() // Actually a reconnect at this point. - return g.connect() + return g.Open() +} + +func (g *Gateway) Open() error { + // Reconnect timeout + ctx, cancel := context.WithTimeout(context.Background(), g.WSTimeout) + defer cancel() + + var Lerr error + + for i := uint(0); i < g.WSRetries; i++ { + // Check if context is expired + if err := ctx.Err(); err != nil { + // Don't bother if it's expired + return err + } + + // Reconnect to the Gateway + if err := g.WS.Redial(ctx); err != nil { + // Save the error, retry again + Lerr = errors.Wrap(err, "Failed to reconnect") + continue + } + + // Try to resume the connection + if err := g.Start(); err != nil { + // If the connection is rate limited (documented behavior): + // https://discordapp.com/developers/docs/topics/gateway#rate-limiting + if err == ErrInvalidSession { + continue // retry + } + + // Else, fatal + return errors.Wrap(err, "Failed to start gateway") + } + + // Started successfully, return + return nil + } + + // Check if any earlier errors are fatal + if Lerr != nil { + return Lerr + } + + // We tried. + return ErrWSMaxTries } // Start authenticates with the websocket, or resume from a dead Websocket @@ -254,49 +303,3 @@ func (g *Gateway) Send(code OPCode, v interface{}) error { return g.WS.Send(ctx, b) } - -func (g *Gateway) connect() error { - // Reconnect timeout - ctx, cancel := context.WithTimeout(context.Background(), g.WSTimeout) - defer cancel() - - var Lerr error - - for i := uint(0); i < g.WSRetries; i++ { - // Check if context is expired - if err := ctx.Err(); err != nil { - // Don't bother if it's expired - return err - } - - // Reconnect to the Gateway - if err := g.WS.Redial(ctx); err != nil { - // Save the error, retry again - Lerr = errors.Wrap(err, "Failed to reconnect") - continue - } - - // Try to resume the connection - if err := g.Start(); err != nil { - // If the connection is rate limited (documented behavior): - // https://discordapp.com/developers/docs/topics/gateway#rate-limiting - if err == ErrInvalidSession { - continue // retry - } - - // Else, fatal - return errors.Wrap(err, "Failed to start gateway") - } - - // Started successfully, return - return nil - } - - // Check if any earlier errors are fatal - if Lerr != nil { - return Lerr - } - - // We tried. - return ErrWSMaxTries -} diff --git a/internal/wsutil/conn.go b/internal/wsutil/conn.go index 10f03b5..16b514e 100644 --- a/internal/wsutil/conn.go +++ b/internal/wsutil/conn.go @@ -5,7 +5,6 @@ import ( "context" "io/ioutil" "net/http" - "time" "github.com/diamondburned/arikawa/internal/json" "github.com/pkg/errors" @@ -43,8 +42,6 @@ type Conn struct { *websocket.Conn json.Driver - ReadTimeout time.Duration // DefaultTimeout - events chan Event } @@ -52,9 +49,8 @@ var _ Connection = (*Conn)(nil) func NewConn(driver json.Driver) *Conn { return &Conn{ - Driver: driver, - ReadTimeout: DefaultTimeout, - events: make(chan Event, WSBuffer), + Driver: driver, + events: make(chan Event, WSBuffer), } } @@ -81,11 +77,7 @@ func (c *Conn) Listen() <-chan Event { func (c *Conn) readLoop(ch chan Event) { for { - ctx, cancel := context.WithTimeout( - context.Background(), c.ReadTimeout) - defer cancel() - - b, err := c.readAll(ctx) + b, err := c.readAll(context.Background()) if err != nil { // Check if the error is a fatal one if code := websocket.CloseStatus(err); code > -1 { diff --git a/session/session.go b/session/session.go index d0f18c6..66e2c7f 100644 --- a/session/session.go +++ b/session/session.go @@ -69,7 +69,7 @@ func NewWithGateway(gw *gateway.Gateway) *Session { } func (s *Session) Open() error { - if err := s.gateway.Start(); err != nil { + if err := s.gateway.Open(); err != nil { return errors.Wrap(err, "Failed to start gateway") } @@ -91,6 +91,10 @@ func (s *Session) startHandler(stop <-chan struct{}) { } } +func (s *Session) Wait() error { + return s.gateway.Wait() +} + func (s *Session) Close() error { // Stop the event handler if s.hstop != nil { diff --git a/state/state.go b/state/state.go index 2d1afc6..1f42ec9 100644 --- a/state/state.go +++ b/state/state.go @@ -56,6 +56,7 @@ func (s *State) hookSession() error { } switch ev := iface.(type) { + case *gateway.ReadyEvent: case *gateway.MessageCreateEvent: _ = ev panic("IMPLEMENT ME")