1
0
Fork 0
mirror of https://github.com/diamondburned/arikawa.git synced 2024-11-01 04:24:19 +00:00
arikawa/session/session.go

182 lines
4.9 KiB
Go
Raw Normal View History

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 (
"context"
"github.com/pkg/errors"
2021-06-02 02:53:19 +00:00
"github.com/diamondburned/arikawa/v3/api"
"github.com/diamondburned/arikawa/v3/gateway"
2021-06-10 23:48:32 +00:00
"github.com/diamondburned/arikawa/v3/gateway/shard"
2021-06-02 02:53:19 +00:00
"github.com/diamondburned/arikawa/v3/internal/handleloop"
"github.com/diamondburned/arikawa/v3/utils/handler"
2020-01-15 04:43:34 +00:00
)
var ErrMFA = errors.New("account has 2FA enabled")
// Closed is an event that's sent to Session's command handler. This works by
// using (*Gateway).AfterClose. If the user sets this callback, no Closed events
// would be sent.
//
// Usage
//
// ses.AddHandler(func(*session.Closed) {})
//
type Closed struct {
Error error
}
2021-06-10 23:48:32 +00:00
// NewShardFunc creates a shard constructor for a session.
// Accessing any shard and adding a handler will add a handler for all shards.
func NewShardFunc(f func(m *shard.Manager, s *Session)) shard.NewShardFunc {
return func(m *shard.Manager, id *gateway.Identifier) (shard.Shard, error) {
s := NewCustomShard(m, id)
if f != nil {
f(m, s)
}
return s, nil
}
}
// NewCustomShard creates a new session from the given shard manager and other
// parameters.
func NewCustomShard(m *shard.Manager, id *gateway.Identifier) *Session {
return NewCustomSession(
shard.NewGatewayShard(m, id),
api.NewClient(id.Token),
handler.New(),
)
}
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
2021-06-10 23:48:32 +00:00
*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
// internal state to not be copied around.
looper *handleloop.Loop
}
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 {
return nil, errors.Wrap(err, "failed to connect to Gateway")
2020-01-15 04:43:34 +00:00
}
return NewWithGateway(g), nil
2020-01-15 04:43:34 +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")
}
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")
}
return New(l.Token)
}
2021-06-10 23:48:32 +00:00
// NewWithGateway creates a new Session with the given Gateway.
2020-01-17 02:08:03 +00:00
func NewWithGateway(gw *gateway.Gateway) *Session {
2021-06-10 23:48:32 +00:00
return NewCustomSession(gw, api.NewClient(gw.Identifier.Token), handler.New())
}
2021-06-10 23:48:32 +00:00
// NewCustomSession constructs a bare Session from the given parameters.
func NewCustomSession(gw *gateway.Gateway, cl *api.Client, h *handler.Handler) *Session {
2020-02-16 05:29:25 +00:00
return &Session{
Gateway: gw,
2021-06-10 23:48:32 +00:00
Client: cl,
Handler: h,
looper: handleloop.NewLoop(h),
2020-01-17 02:08:03 +00:00
}
}
2021-06-10 23:48:32 +00:00
func (s *Session) Open(ctx context.Context) error {
// Start the handler beforehand so no events are missed.
s.looper.Start(s.Gateway.Events)
2020-01-17 02:08:03 +00:00
// Set the AfterClose's handler.
s.Gateway.AfterClose = func(err error) {
s.Handler.Call(&Closed{
Error: err,
})
}
2021-06-10 23:48:32 +00:00
if err := s.Gateway.Open(ctx); err != nil {
2020-05-16 21:14:49 +00:00
return errors.Wrap(err, "failed to start gateway")
}
2020-01-17 02:08:03 +00:00
return nil
}
// 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
}
// Close closes the underlying Websocket connection, invalidating the session
// ID.
//
// It will send a closing frame before ending the connection, closing it
// gracefully. This will cause the bot to appear as offline instantly.
2020-01-17 02:08:03 +00:00
func (s *Session) Close() error {
// Stop the event handler
s.looper.Stop()
return s.Gateway.Close()
2021-01-30 07:25:15 +00:00
}
// Pause pauses the Gateway connection, by ending the connection without
// sending a closing frame. This allows the connection to be resumed at a later
// point.
func (s *Session) Pause() error {
2020-01-17 02:08:03 +00:00
// Stop the event handler
s.looper.Stop()
return s.Gateway.Pause()
2020-01-15 04:43:34 +00:00
}