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