2020-01-17 05:17:46 +00:00
|
|
|
// Package session abstracts around the REST API and the Gateway, managing both
|
|
|
|
// at once. It offers a handler interface similar to that in discordgo for
|
|
|
|
// Gateway events.
|
2020-01-15 04:43:34 +00:00
|
|
|
package session
|
|
|
|
|
|
|
|
import (
|
2020-11-14 23:30:18 +00:00
|
|
|
"context"
|
2020-10-17 10:21:07 +00:00
|
|
|
"sync"
|
|
|
|
|
2020-06-06 20:47:15 +00:00
|
|
|
"github.com/pkg/errors"
|
|
|
|
|
2020-10-28 22:39:59 +00:00
|
|
|
"github.com/diamondburned/arikawa/v2/api"
|
|
|
|
"github.com/diamondburned/arikawa/v2/gateway"
|
|
|
|
"github.com/diamondburned/arikawa/v2/utils/handler"
|
2020-01-15 04:43:34 +00:00
|
|
|
)
|
|
|
|
|
2020-06-06 20:47:15 +00:00
|
|
|
var ErrMFA = errors.New("account has 2FA enabled")
|
|
|
|
|
2020-04-07 02:36:06 +00:00
|
|
|
// Closed is an event that's sent to Session's command handler. This works by
|
2020-06-06 20:47:15 +00:00
|
|
|
// using (*Gateway).AfterClose. If the user sets this callback, no Closed events
|
2020-04-07 02:36:06 +00:00
|
|
|
// would be sent.
|
|
|
|
//
|
|
|
|
// Usage
|
|
|
|
//
|
|
|
|
// ses.AddHandler(func(*session.Closed) {})
|
|
|
|
//
|
|
|
|
type Closed struct {
|
|
|
|
Error error
|
|
|
|
}
|
|
|
|
|
2020-01-17 05:23:56 +00:00
|
|
|
// Session manages both the API and Gateway. As such, Session inherits all of
|
|
|
|
// API's methods, as well has the Handler used for Gateway.
|
2020-01-15 04:43:34 +00:00
|
|
|
type Session struct {
|
2020-01-17 02:08:03 +00:00
|
|
|
*api.Client
|
2020-01-19 21:54:16 +00:00
|
|
|
Gateway *gateway.Gateway
|
2020-01-15 04:43:34 +00:00
|
|
|
|
2020-01-17 05:23:56 +00:00
|
|
|
// Command handler with inherited methods.
|
2020-01-17 05:17:46 +00:00
|
|
|
*handler.Handler
|
2020-01-17 05:23:56 +00:00
|
|
|
|
2020-11-14 23:30:18 +00:00
|
|
|
// internal state to not be copied around.
|
|
|
|
*sessionState
|
|
|
|
}
|
2020-01-19 21:54:16 +00:00
|
|
|
|
2020-11-14 23:30:18 +00:00
|
|
|
// sessionState contains fields crucial for controlling the state of session. It
|
|
|
|
// should not be copied around.
|
|
|
|
type sessionState struct {
|
2020-01-17 05:17:46 +00:00
|
|
|
hstop chan struct{}
|
2020-10-17 10:21:07 +00:00
|
|
|
wstop sync.Once
|
2020-01-15 04:43:34 +00:00
|
|
|
}
|
|
|
|
|
2020-11-14 23:30:18 +00:00
|
|
|
func (state *sessionState) Reset() {
|
|
|
|
state.hstop = make(chan struct{})
|
|
|
|
state.wstop = sync.Once{}
|
|
|
|
}
|
|
|
|
|
2020-07-11 19:50:32 +00:00
|
|
|
func NewWithIntents(token string, intents ...gateway.Intents) (*Session, error) {
|
|
|
|
g, err := gateway.NewGatewayWithIntents(token, intents...)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "failed to connect to Gateway")
|
|
|
|
}
|
|
|
|
|
|
|
|
return NewWithGateway(g), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// New creates a new session from a given token. Most bots should be using
|
|
|
|
// NewWithIntents instead.
|
2020-01-15 04:43:34 +00:00
|
|
|
func New(token string) (*Session, error) {
|
2020-04-19 21:53:53 +00:00
|
|
|
// Create a gateway
|
2020-01-17 02:08:03 +00:00
|
|
|
g, err := gateway.NewGateway(token)
|
2020-01-15 04:43:34 +00:00
|
|
|
if err != nil {
|
2020-06-29 18:00:07 +00:00
|
|
|
return nil, errors.Wrap(err, "failed to connect to Gateway")
|
2020-01-15 04:43:34 +00:00
|
|
|
}
|
|
|
|
|
2020-07-11 19:50:32 +00:00
|
|
|
return NewWithGateway(g), nil
|
2020-01-15 04:43:34 +00:00
|
|
|
}
|
|
|
|
|
2020-01-19 21:54:16 +00:00
|
|
|
// Login tries to log in as a normal user account; MFA is optional.
|
|
|
|
func Login(email, password, mfa string) (*Session, error) {
|
|
|
|
// Make a scratch HTTP client without a token
|
|
|
|
client := api.NewClient("")
|
|
|
|
|
|
|
|
// Try to login without TOTP
|
|
|
|
l, err := client.Login(email, password)
|
|
|
|
if err != nil {
|
2020-05-16 21:14:49 +00:00
|
|
|
return nil, errors.Wrap(err, "failed to login")
|
2020-01-19 21:54:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if l.Token != "" && !l.MFA {
|
|
|
|
// We got the token, return with a new Session.
|
|
|
|
return New(l.Token)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Discord requests MFA, so we need the MFA token.
|
|
|
|
if mfa == "" {
|
|
|
|
return nil, ErrMFA
|
|
|
|
}
|
|
|
|
|
|
|
|
// Retry logging in with a 2FA token
|
|
|
|
l, err = client.TOTP(mfa, l.Ticket)
|
|
|
|
if err != nil {
|
2020-05-16 21:14:49 +00:00
|
|
|
return nil, errors.Wrap(err, "failed to login with 2FA")
|
2020-01-19 21:54:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return New(l.Token)
|
|
|
|
}
|
|
|
|
|
2020-01-17 02:08:03 +00:00
|
|
|
func NewWithGateway(gw *gateway.Gateway) *Session {
|
2020-02-16 05:29:25 +00:00
|
|
|
return &Session{
|
|
|
|
Gateway: gw,
|
2020-01-17 02:08:03 +00:00
|
|
|
// Nab off gateway's token
|
2020-11-14 23:30:18 +00:00
|
|
|
Client: api.NewClient(gw.Identifier.Token),
|
|
|
|
Handler: handler.New(),
|
|
|
|
sessionState: &sessionState{},
|
2020-01-17 02:08:03 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Session) Open() error {
|
2020-04-06 20:26:00 +00:00
|
|
|
// Start the handler beforehand so no events are missed.
|
2020-11-14 23:30:18 +00:00
|
|
|
s.sessionState.Reset()
|
2020-10-17 10:21:07 +00:00
|
|
|
go s.startHandler()
|
2020-01-17 02:08:03 +00:00
|
|
|
|
2020-04-07 02:36:06 +00:00
|
|
|
// Set the AfterClose's handler.
|
|
|
|
s.Gateway.AfterClose = func(err error) {
|
|
|
|
s.Handler.Call(&Closed{
|
|
|
|
Error: err,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-04-06 20:26:00 +00:00
|
|
|
if err := s.Gateway.Open(); err != nil {
|
2020-05-16 21:14:49 +00:00
|
|
|
return errors.Wrap(err, "failed to start gateway")
|
2020-04-06 20:26:00 +00:00
|
|
|
}
|
|
|
|
|
2020-01-17 02:08:03 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-11-14 23:30:18 +00:00
|
|
|
// WithContext returns a shallow copy of Session with the context replaced in
|
|
|
|
// the API client. All methods called on the returned Session will use this
|
|
|
|
// given context.
|
|
|
|
//
|
|
|
|
// This method is thread-safe only after Open and before Close are called. Open
|
|
|
|
// and Close should not be called on the returned Session.
|
|
|
|
func (s *Session) WithContext(ctx context.Context) *Session {
|
|
|
|
cpy := *s
|
|
|
|
cpy.Client = s.Client.WithContext(ctx)
|
|
|
|
return &cpy
|
|
|
|
}
|
|
|
|
|
2020-10-17 10:21:07 +00:00
|
|
|
func (s *Session) startHandler() {
|
2020-01-17 02:08:03 +00:00
|
|
|
for {
|
|
|
|
select {
|
2020-10-17 10:21:07 +00:00
|
|
|
case <-s.hstop:
|
2020-01-17 02:08:03 +00:00
|
|
|
return
|
2020-01-19 21:54:16 +00:00
|
|
|
case ev := <-s.Gateway.Events:
|
2020-06-06 20:47:15 +00:00
|
|
|
s.Call(ev)
|
2020-01-15 04:43:34 +00:00
|
|
|
}
|
|
|
|
}
|
2020-01-17 02:08:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Session) Close() error {
|
|
|
|
// Stop the event handler
|
2020-11-14 23:30:18 +00:00
|
|
|
s.wstop.Do(func() { close(s.hstop) })
|
2020-01-17 02:08:03 +00:00
|
|
|
// Close the websocket
|
2020-01-19 21:54:16 +00:00
|
|
|
return s.Gateway.Close()
|
2020-01-15 04:43:34 +00:00
|
|
|
}
|