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"
|
2021-09-28 20:19:04 +00:00
|
|
|
"sync"
|
2020-10-17 10:21:07 +00:00
|
|
|
|
2020-06-06 20:47:15 +00:00
|
|
|
"github.com/pkg/errors"
|
|
|
|
|
2021-06-02 02:53:19 +00:00
|
|
|
"github.com/diamondburned/arikawa/v3/api"
|
|
|
|
"github.com/diamondburned/arikawa/v3/gateway"
|
|
|
|
"github.com/diamondburned/arikawa/v3/utils/handler"
|
2021-09-28 20:19:04 +00:00
|
|
|
"github.com/diamondburned/arikawa/v3/utils/json/option"
|
|
|
|
"github.com/diamondburned/arikawa/v3/utils/ws/ophandler"
|
2020-01-15 04:43:34 +00:00
|
|
|
)
|
|
|
|
|
2021-09-28 20:19:04 +00:00
|
|
|
// ErrMFA is returned if the account requires a 2FA code to log in.
|
2020-06-06 20:47:15 +00:00
|
|
|
var ErrMFA = errors.New("account has 2FA enabled")
|
|
|
|
|
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-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.
|
2021-09-28 20:19:04 +00:00
|
|
|
state *sessionState
|
2020-11-14 23:30:18 +00:00
|
|
|
}
|
|
|
|
|
2021-09-28 20:19:04 +00:00
|
|
|
type sessionState struct {
|
|
|
|
sync.Mutex
|
|
|
|
id gateway.Identifier
|
|
|
|
gateway *gateway.Gateway
|
|
|
|
|
|
|
|
ctx context.Context
|
|
|
|
cancel context.CancelFunc
|
|
|
|
doneCh <-chan struct{}
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewWithIntents is similar to New but adds the given intents in during
|
|
|
|
// construction.
|
|
|
|
func NewWithIntents(token string, intents ...gateway.Intents) *Session {
|
|
|
|
var allIntent gateway.Intents
|
|
|
|
for _, intent := range intents {
|
|
|
|
allIntent |= intent
|
2020-07-11 19:50:32 +00:00
|
|
|
}
|
|
|
|
|
2021-09-28 20:19:04 +00:00
|
|
|
id := gateway.DefaultIdentifier(token)
|
|
|
|
id.Intents = option.NewUint(uint(allIntent))
|
|
|
|
|
|
|
|
return NewWithIdentifier(id)
|
2020-07-11 19:50:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// New creates a new session from a given token. Most bots should be using
|
|
|
|
// NewWithIntents instead.
|
2021-09-28 20:19:04 +00:00
|
|
|
func New(token string) *Session {
|
|
|
|
return NewWithIdentifier(gateway.DefaultIdentifier(token))
|
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.
|
2021-09-28 20:19:04 +00:00
|
|
|
func Login(ctx context.Context, email, password, mfa string) (*Session, error) {
|
2020-01-19 21:54:16 +00:00
|
|
|
// Make a scratch HTTP client without a token
|
2021-09-28 20:19:04 +00:00
|
|
|
client := api.NewClient("").WithContext(ctx)
|
2020-01-19 21:54:16 +00:00
|
|
|
|
|
|
|
// 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.
|
2021-09-28 20:19:04 +00:00
|
|
|
return New(l.Token), nil
|
2020-01-19 21:54:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
}
|
|
|
|
|
2021-09-28 20:19:04 +00:00
|
|
|
return New(l.Token), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewWithIdentifier creates a bare Session with the given identifier.
|
|
|
|
func NewWithIdentifier(id gateway.Identifier) *Session {
|
|
|
|
return NewCustom(id, api.NewClient(id.Token), handler.New())
|
2020-01-19 21:54:16 +00:00
|
|
|
}
|
|
|
|
|
2021-09-28 20:19:04 +00:00
|
|
|
// NewWithGateway constructs a bare Session from the given UNOPENED gateway.
|
|
|
|
func NewWithGateway(g *gateway.Gateway, h *handler.Handler) *Session {
|
|
|
|
state := g.State()
|
|
|
|
return &Session{
|
|
|
|
Client: api.NewClient(state.Identifier.Token),
|
|
|
|
Handler: h,
|
|
|
|
state: &sessionState{
|
|
|
|
gateway: g,
|
|
|
|
id: state.Identifier,
|
|
|
|
},
|
|
|
|
}
|
2021-06-10 23:48:32 +00:00
|
|
|
}
|
2020-11-17 19:09:51 +00:00
|
|
|
|
2021-09-28 20:19:04 +00:00
|
|
|
// NewCustom constructs a bare Session from the given parameters.
|
|
|
|
func NewCustom(id gateway.Identifier, cl *api.Client, h *handler.Handler) *Session {
|
2020-02-16 05:29:25 +00:00
|
|
|
return &Session{
|
2021-06-10 23:48:32 +00:00
|
|
|
Client: cl,
|
|
|
|
Handler: h,
|
2021-09-28 20:19:04 +00:00
|
|
|
state: &sessionState{id: id},
|
2020-01-17 02:08:03 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-28 20:19:04 +00:00
|
|
|
// AddIntents adds the given intents into the gateway. Calling it after Open has
|
|
|
|
// already been called will result in a panic.
|
|
|
|
func (s *Session) AddIntents(intents gateway.Intents) {
|
|
|
|
s.state.Lock()
|
|
|
|
|
|
|
|
s.state.id.AddIntents(intents)
|
|
|
|
|
|
|
|
if s.state.gateway != nil {
|
|
|
|
s.state.gateway.AddIntents(intents)
|
|
|
|
}
|
|
|
|
|
|
|
|
s.state.Unlock()
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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 (s *Session) HasIntents(intents gateway.Intents) bool {
|
|
|
|
return s.state.id.HasIntents(intents)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Gateway returns the current session's gateway. If Open has never been called
|
|
|
|
// or Session was never constructed with a gateway, then nil is returned.
|
|
|
|
func (s *Session) Gateway() *gateway.Gateway {
|
|
|
|
s.state.Lock()
|
|
|
|
g := s.state.gateway
|
|
|
|
s.state.Unlock()
|
|
|
|
return g
|
|
|
|
}
|
|
|
|
|
|
|
|
// Open opens the Discord gateway and its handler, then waits until either the
|
|
|
|
// Ready or Resumed event gets through.
|
2021-06-10 23:48:32 +00:00
|
|
|
func (s *Session) Open(ctx context.Context) error {
|
2021-09-28 20:19:04 +00:00
|
|
|
evCh := make(chan interface{})
|
|
|
|
|
|
|
|
s.state.Lock()
|
|
|
|
defer s.state.Unlock()
|
|
|
|
|
|
|
|
if s.state.cancel != nil {
|
|
|
|
if err := s.close(ctx); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-04-07 02:36:06 +00:00
|
|
|
}
|
|
|
|
|
2021-09-28 20:19:04 +00:00
|
|
|
if s.state.gateway == nil {
|
|
|
|
g, err := gateway.NewWithIdentifier(ctx, s.state.id)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
s.state.gateway = g
|
2020-04-06 20:26:00 +00:00
|
|
|
}
|
|
|
|
|
2021-09-28 20:19:04 +00:00
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
s.state.ctx = ctx
|
|
|
|
s.state.cancel = cancel
|
|
|
|
|
|
|
|
// TODO: change this to AddSyncHandler.
|
|
|
|
rm := s.AddHandler(evCh)
|
|
|
|
defer rm()
|
|
|
|
|
|
|
|
opCh := s.state.gateway.Connect(s.state.ctx)
|
|
|
|
s.state.doneCh = ophandler.Loop(opCh, s.Handler)
|
|
|
|
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
s.close(ctx)
|
|
|
|
return ctx.Err()
|
|
|
|
|
|
|
|
case <-s.state.doneCh:
|
|
|
|
// Event loop died.
|
|
|
|
return s.state.gateway.LastError()
|
|
|
|
|
|
|
|
case ev := <-evCh:
|
|
|
|
switch ev.(type) {
|
|
|
|
case *gateway.ReadyEvent, *gateway.ResumedEvent:
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-01-17 02:08:03 +00:00
|
|
|
}
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2021-05-29 20:32:58 +00:00
|
|
|
// Close closes the underlying Websocket connection, invalidating the session
|
2021-09-28 20:19:04 +00:00
|
|
|
// ID. It will send a closing frame before ending the connection, closing it
|
|
|
|
// gracefully. This will cause the bot to appear as offline instantly. To
|
|
|
|
// prevent this behavior, change Gateway.AlwaysCloseGracefully.
|
2020-01-17 02:08:03 +00:00
|
|
|
func (s *Session) Close() error {
|
2021-09-28 20:19:04 +00:00
|
|
|
s.state.Lock()
|
|
|
|
defer s.state.Unlock()
|
|
|
|
|
|
|
|
return s.close(context.Background())
|
2021-01-30 07:25:15 +00:00
|
|
|
}
|
|
|
|
|
2021-09-28 20:19:04 +00:00
|
|
|
func (s *Session) close(ctx context.Context) error {
|
|
|
|
if s.state.cancel == nil {
|
|
|
|
return errors.New("Session is already closed")
|
|
|
|
}
|
|
|
|
|
|
|
|
s.state.cancel()
|
|
|
|
s.state.cancel = nil
|
|
|
|
s.state.ctx = nil
|
|
|
|
|
|
|
|
// Wait until we've successfully disconnected.
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
return errors.Wrap(ctx.Err(), "cannot wait for gateway exit")
|
|
|
|
case <-s.state.doneCh:
|
|
|
|
// ok
|
|
|
|
}
|
|
|
|
|
|
|
|
s.state.doneCh = nil
|
|
|
|
|
|
|
|
return s.state.gateway.LastError()
|
2020-01-15 04:43:34 +00:00
|
|
|
}
|