github.com/diamondburned/arikawa@v1.3.14/voice/voicegateway/gateway.go (about)

     1  //
     2  // For the brave souls who get this far: You are the chosen ones,
     3  // the valiant knights of programming who toil away, without rest,
     4  // fixing our most awful code.  To you, true saviors, kings of men,
     5  // I say this: never gonna give you up, never gonna let you down,
     6  // never gonna run around and desert you.  Never gonna make you cry,
     7  // never gonna say goodbye.  Never gonna tell a lie and hurt you.
     8  //
     9  
    10  package voicegateway
    11  
    12  import (
    13  	"context"
    14  	"strings"
    15  	"sync"
    16  	"time"
    17  
    18  	"github.com/pkg/errors"
    19  
    20  	"github.com/diamondburned/arikawa/discord"
    21  	"github.com/diamondburned/arikawa/internal/moreatomic"
    22  	"github.com/diamondburned/arikawa/utils/json"
    23  	"github.com/diamondburned/arikawa/utils/wsutil"
    24  )
    25  
    26  const (
    27  	// Version represents the current version of the Discord Gateway Gateway this package uses.
    28  	Version = "4"
    29  )
    30  
    31  var (
    32  	ErrNoSessionID = errors.New("no sessionID was received")
    33  	ErrNoEndpoint  = errors.New("no endpoint was received")
    34  )
    35  
    36  // State contains state information of a voice gateway.
    37  type State struct {
    38  	GuildID   discord.GuildID
    39  	ChannelID discord.ChannelID
    40  	UserID    discord.UserID
    41  
    42  	SessionID string
    43  	Token     string
    44  	Endpoint  string
    45  }
    46  
    47  // Gateway represents a Discord Gateway Gateway connection.
    48  type Gateway struct {
    49  	state State // constant
    50  
    51  	mutex sync.RWMutex
    52  	ready ReadyEvent
    53  
    54  	WS *wsutil.Websocket
    55  
    56  	Timeout   time.Duration
    57  	reconnect moreatomic.Bool
    58  
    59  	EventLoop wsutil.PacemakerLoop
    60  
    61  	// ErrorLog will be called when an error occurs (defaults to log.Println)
    62  	ErrorLog func(err error)
    63  	// AfterClose is called after each close. Error can be non-nil, as this is
    64  	// called even when the Gateway is gracefully closed. It's used mainly for
    65  	// reconnections or any type of connection interruptions. (defaults to noop)
    66  	AfterClose func(err error)
    67  
    68  	// Filled by methods, internal use
    69  	waitGroup *sync.WaitGroup
    70  }
    71  
    72  func New(state State) *Gateway {
    73  	// https://discordapp.com/developers/docs/topics/voice-connections#establishing-a-voice-websocket-connection
    74  	var endpoint = "wss://" + strings.TrimSuffix(state.Endpoint, ":80") + "/?v=" + Version
    75  
    76  	return &Gateway{
    77  		state:      state,
    78  		WS:         wsutil.New(endpoint),
    79  		Timeout:    wsutil.WSTimeout,
    80  		ErrorLog:   wsutil.WSError,
    81  		AfterClose: func(error) {},
    82  	}
    83  }
    84  
    85  // TODO: get rid of
    86  func (c *Gateway) Ready() ReadyEvent {
    87  	c.mutex.RLock()
    88  	defer c.mutex.RUnlock()
    89  
    90  	return c.ready
    91  }
    92  
    93  // OpenCtx shouldn't be used, but JoinServer instead.
    94  func (c *Gateway) OpenCtx(ctx context.Context) error {
    95  	if c.state.Endpoint == "" {
    96  		return errors.New("missing endpoint in state")
    97  	}
    98  
    99  	// https://discordapp.com/developers/docs/topics/voice-connections#establishing-a-voice-websocket-connection
   100  	var endpoint = "wss://" + strings.TrimSuffix(c.state.Endpoint, ":80") + "/?v=" + Version
   101  
   102  	wsutil.WSDebug("VoiceGateway: Connecting to voice endpoint (endpoint=" + endpoint + ")")
   103  
   104  	// Create a new context with a timeout for the connection.
   105  	ctx, cancel := context.WithTimeout(ctx, c.Timeout)
   106  	defer cancel()
   107  
   108  	// Connect to the Gateway Gateway.
   109  	if err := c.WS.Dial(ctx); err != nil {
   110  		return errors.Wrap(err, "failed to connect to voice gateway")
   111  	}
   112  
   113  	wsutil.WSDebug("VoiceGateway: Trying to start...")
   114  
   115  	// Try to start or resume the connection.
   116  	if err := c.start(ctx); err != nil {
   117  		return err
   118  	}
   119  
   120  	return nil
   121  }
   122  
   123  // Start .
   124  func (c *Gateway) start(ctx context.Context) error {
   125  	if err := c.__start(ctx); err != nil {
   126  		wsutil.WSDebug("VoiceGateway: Start failed: ", err)
   127  
   128  		// Close can be called with the mutex still acquired here, as the
   129  		// pacemaker hasn't started yet.
   130  		if err := c.Close(); err != nil {
   131  			wsutil.WSDebug("VoiceGateway: Failed to close after start fail: ", err)
   132  		}
   133  		return err
   134  	}
   135  
   136  	return nil
   137  }
   138  
   139  // this function blocks until READY.
   140  func (c *Gateway) __start(ctx context.Context) error {
   141  	// Make a new WaitGroup for use in background loops:
   142  	c.waitGroup = new(sync.WaitGroup)
   143  
   144  	ch := c.WS.Listen()
   145  
   146  	// Wait for hello.
   147  	wsutil.WSDebug("VoiceGateway: Waiting for Hello..")
   148  
   149  	var hello *HelloEvent
   150  	// Wait for the Hello event; return if it times out.
   151  	select {
   152  	case e, ok := <-ch:
   153  		if !ok {
   154  			return errors.New("unexpected ws close while waiting for Hello")
   155  		}
   156  		if _, err := wsutil.AssertEvent(e, HelloOP, &hello); err != nil {
   157  			return errors.Wrap(err, "error at Hello")
   158  		}
   159  	case <-ctx.Done():
   160  		return errors.Wrap(ctx.Err(), "failed to wait for Hello event")
   161  	}
   162  
   163  	wsutil.WSDebug("VoiceGateway: Received Hello")
   164  
   165  	// https://discordapp.com/developers/docs/topics/voice-connections#establishing-a-voice-websocket-connection
   166  	// Turns out Hello is sent right away on connection start.
   167  	if !c.reconnect.Get() {
   168  		if err := c.IdentifyCtx(ctx); err != nil {
   169  			return errors.Wrap(err, "failed to identify")
   170  		}
   171  	} else {
   172  		if err := c.ResumeCtx(ctx); err != nil {
   173  			return errors.Wrap(err, "failed to resume")
   174  		}
   175  	}
   176  	// This bool is because we should only try and Resume once.
   177  	c.reconnect.Set(false)
   178  
   179  	// Wait for either Ready or Resumed.
   180  	err := wsutil.WaitForEvent(ctx, c, ch, func(op *wsutil.OP) bool {
   181  		return op.Code == ReadyOP || op.Code == ResumedOP
   182  	})
   183  	if err != nil {
   184  		return errors.Wrap(err, "failed to wait for Ready or Resumed")
   185  	}
   186  
   187  	// Start the event handler, which also handles the pacemaker death signal.
   188  	c.waitGroup.Add(1)
   189  
   190  	c.EventLoop.RunAsync(hello.HeartbeatInterval.Duration(), ch, c, func(err error) {
   191  		c.waitGroup.Done() // mark so Close() can exit.
   192  		wsutil.WSDebug("VoiceGateway: Event loop stopped.")
   193  
   194  		if err != nil {
   195  			c.ErrorLog(err)
   196  
   197  			if err := c.Reconnect(); err != nil {
   198  				c.ErrorLog(errors.Wrap(err, "failed to reconnect voice"))
   199  			}
   200  
   201  			// Reconnect should spawn another eventLoop in its Start function.
   202  		}
   203  	})
   204  
   205  	wsutil.WSDebug("VoiceGateway: Started successfully.")
   206  
   207  	return nil
   208  }
   209  
   210  // Close closes the underlying Websocket connection.
   211  func (g *Gateway) Close() error {
   212  	wsutil.WSDebug("VoiceGateway: Trying to close. Pacemaker check skipped.")
   213  
   214  	wsutil.WSDebug("VoiceGateway: Closing the Websocket...")
   215  	err := g.WS.Close()
   216  
   217  	if errors.Is(err, wsutil.ErrWebsocketClosed) {
   218  		wsutil.WSDebug("VoiceGateway: Websocket already closed.")
   219  		return nil
   220  	}
   221  
   222  	wsutil.WSDebug("VoiceGateway: Websocket closed; error:", err)
   223  
   224  	wsutil.WSDebug("VoiceGateway: Waiting for the Pacemaker loop to exit.")
   225  	g.waitGroup.Wait()
   226  	wsutil.WSDebug("VoiceGateway: Pacemaker loop exited.")
   227  
   228  	g.AfterClose(err)
   229  	wsutil.WSDebug("VoiceGateway: AfterClose callback finished.")
   230  
   231  	return err
   232  }
   233  
   234  func (c *Gateway) Reconnect() error {
   235  	return c.ReconnectCtx(context.Background())
   236  }
   237  
   238  func (c *Gateway) ReconnectCtx(ctx context.Context) error {
   239  	wsutil.WSDebug("VoiceGateway: Reconnecting...")
   240  
   241  	// TODO: implement a reconnect loop
   242  
   243  	// Guarantee the gateway is already closed. Ignore its error, as we're
   244  	// redialing anyway.
   245  	c.Close()
   246  
   247  	c.reconnect.Set(true)
   248  
   249  	// Condition: err == ErrInvalidSession:
   250  	// If the connection is rate limited (documented behavior):
   251  	// https://discordapp.com/developers/docs/topics/gateway#rate-limiting
   252  
   253  	if err := c.OpenCtx(ctx); err != nil {
   254  		return errors.Wrap(err, "failed to reopen gateway")
   255  	}
   256  
   257  	wsutil.WSDebug("VoiceGateway: Reconnected successfully.")
   258  
   259  	return nil
   260  }
   261  
   262  func (c *Gateway) SessionDescriptionCtx(
   263  	ctx context.Context, sp SelectProtocol) (*SessionDescriptionEvent, error) {
   264  
   265  	// Add the handler first.
   266  	ch, cancel := c.EventLoop.Extras.Add(func(op *wsutil.OP) bool {
   267  		return op.Code == SessionDescriptionOP
   268  	})
   269  	defer cancel()
   270  
   271  	if err := c.SelectProtocolCtx(ctx, sp); err != nil {
   272  		return nil, err
   273  	}
   274  
   275  	var sesdesc *SessionDescriptionEvent
   276  
   277  	// Wait for SessionDescriptionOP packet.
   278  	select {
   279  	case e, ok := <-ch:
   280  		if !ok {
   281  			return nil, errors.New("unexpected close waiting for session description")
   282  		}
   283  		if err := e.UnmarshalData(&sesdesc); err != nil {
   284  			return nil, errors.Wrap(err, "failed to unmarshal session description")
   285  		}
   286  	case <-ctx.Done():
   287  		return nil, errors.Wrap(ctx.Err(), "failed to wait for session description")
   288  	}
   289  
   290  	return sesdesc, nil
   291  }
   292  
   293  // Send sends a payload to the Gateway with the default timeout.
   294  func (c *Gateway) Send(code OPCode, v interface{}) error {
   295  	ctx, cancel := context.WithTimeout(context.Background(), c.Timeout)
   296  	defer cancel()
   297  
   298  	return c.SendCtx(ctx, code, v)
   299  }
   300  
   301  func (c *Gateway) SendCtx(ctx context.Context, code OPCode, v interface{}) error {
   302  	var op = wsutil.OP{
   303  		Code: code,
   304  	}
   305  
   306  	if v != nil {
   307  		b, err := json.Marshal(v)
   308  		if err != nil {
   309  			return errors.Wrap(err, "failed to encode v")
   310  		}
   311  
   312  		op.Data = b
   313  	}
   314  
   315  	b, err := json.Marshal(op)
   316  	if err != nil {
   317  		return errors.Wrap(err, "failed to encode payload")
   318  	}
   319  
   320  	// WS should already be thread-safe.
   321  	return c.WS.SendCtx(ctx, b)
   322  }