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

     1  package voice
     2  
     3  import (
     4  	"context"
     5  	"sync"
     6  	"time"
     7  
     8  	"github.com/pkg/errors"
     9  
    10  	"github.com/diamondburned/arikawa/discord"
    11  	"github.com/diamondburned/arikawa/gateway"
    12  	"github.com/diamondburned/arikawa/internal/moreatomic"
    13  	"github.com/diamondburned/arikawa/session"
    14  	"github.com/diamondburned/arikawa/utils/wsutil"
    15  	"github.com/diamondburned/arikawa/voice/udp"
    16  	"github.com/diamondburned/arikawa/voice/voicegateway"
    17  )
    18  
    19  const Protocol = "xsalsa20_poly1305"
    20  
    21  var OpusSilence = [...]byte{0xF8, 0xFF, 0xFE}
    22  
    23  // WSTimeout is the duration to wait for a gateway operation including Session
    24  // to complete before erroring out. This only applies to functions that don't
    25  // take in a context already.
    26  var WSTimeout = 10 * time.Second
    27  
    28  type Session struct {
    29  	session *session.Session
    30  	state   voicegateway.State
    31  
    32  	ErrorLog func(err error)
    33  
    34  	// Filled by events.
    35  	// sessionID string
    36  	// token     string
    37  	// endpoint  string
    38  
    39  	// joining determines the behavior of incoming event callbacks (Update).
    40  	// If this is true, incoming events will just send into Updated channels. If
    41  	// false, events will trigger a reconnection.
    42  	joining  moreatomic.Bool
    43  	incoming chan struct{} // used only when joining == true
    44  
    45  	mut sync.RWMutex
    46  
    47  	// TODO: expose getters mutex-guarded.
    48  	gateway  *voicegateway.Gateway
    49  	voiceUDP *udp.Connection
    50  
    51  	muted    bool
    52  	deafened bool
    53  	speaking bool
    54  }
    55  
    56  func NewSession(ses *session.Session, userID discord.UserID) *Session {
    57  	return &Session{
    58  		session: ses,
    59  		state: voicegateway.State{
    60  			UserID: userID,
    61  		},
    62  		ErrorLog: func(err error) {},
    63  		incoming: make(chan struct{}, 2),
    64  	}
    65  }
    66  
    67  func (s *Session) UpdateServer(ev *gateway.VoiceServerUpdateEvent) {
    68  	if s.state.GuildID != ev.GuildID {
    69  		// Not our state.
    70  		return
    71  	}
    72  
    73  	// If this is true, then mutex is acquired already.
    74  	if s.joining.Get() {
    75  		s.state.Endpoint = ev.Endpoint
    76  		s.state.Token = ev.Token
    77  
    78  		s.incoming <- struct{}{}
    79  		return
    80  	}
    81  
    82  	// Reconnect.
    83  	s.mut.Lock()
    84  	defer s.mut.Unlock()
    85  
    86  	s.state.Endpoint = ev.Endpoint
    87  	s.state.Token = ev.Token
    88  
    89  	ctx, cancel := context.WithTimeout(context.Background(), WSTimeout)
    90  	defer cancel()
    91  
    92  	if err := s.reconnectCtx(ctx); err != nil {
    93  		s.ErrorLog(errors.Wrap(err, "failed to reconnect after voice server update"))
    94  	}
    95  }
    96  
    97  func (s *Session) UpdateState(ev *gateway.VoiceStateUpdateEvent) {
    98  	if s.state.UserID != ev.UserID {
    99  		// Not our state.
   100  		return
   101  	}
   102  
   103  	// If this is true, then mutex is acquired already.
   104  	if s.joining.Get() {
   105  		s.state.SessionID = ev.SessionID
   106  		s.state.ChannelID = ev.ChannelID
   107  
   108  		s.incoming <- struct{}{}
   109  		return
   110  	}
   111  }
   112  
   113  func (s *Session) JoinChannel(
   114  	gID discord.GuildID, cID discord.ChannelID, muted, deafened bool) error {
   115  
   116  	ctx, cancel := context.WithTimeout(context.Background(), WSTimeout)
   117  	defer cancel()
   118  
   119  	return s.JoinChannelCtx(ctx, gID, cID, muted, deafened)
   120  }
   121  
   122  func (s *Session) JoinChannelCtx(
   123  	ctx context.Context, gID discord.GuildID, cID discord.ChannelID, muted, deafened bool) error {
   124  
   125  	// Acquire the mutex during join, locking during IO as well.
   126  	s.mut.Lock()
   127  	defer s.mut.Unlock()
   128  
   129  	// Set that we're joining.
   130  	s.joining.Set(true)
   131  	defer s.joining.Set(false) // reset when done
   132  
   133  	// Ensure gateway and voiceUDP are already closed.
   134  	s.ensureClosed()
   135  
   136  	// Set the state.
   137  	s.state.ChannelID = cID
   138  	s.state.GuildID = gID
   139  
   140  	s.muted = muted
   141  	s.deafened = deafened
   142  	s.speaking = false
   143  
   144  	// Ensure that if `cID` is zero that it passes null to the update event.
   145  	channelID := discord.NullChannelID
   146  	if cID.IsValid() {
   147  		channelID = cID
   148  	}
   149  
   150  	// https://discordapp.com/developers/docs/topics/voice-connections#retrieving-voice-server-information
   151  	// Send a Voice State Update event to the gateway.
   152  	err := s.session.Gateway.UpdateVoiceStateCtx(ctx, gateway.UpdateVoiceStateData{
   153  		GuildID:   gID,
   154  		ChannelID: channelID,
   155  		SelfMute:  muted,
   156  		SelfDeaf:  deafened,
   157  	})
   158  	if err != nil {
   159  		return errors.Wrap(err, "failed to send Voice State Update event")
   160  	}
   161  
   162  	// Wait for 2 replies. The above command should reply with these 2 events.
   163  	if err := s.waitForIncoming(ctx, 2); err != nil {
   164  		return errors.Wrap(err, "failed to wait for needed gateway events")
   165  	}
   166  
   167  	// These 2 methods should've updated s.state before sending into these
   168  	// channels. Since s.state is already filled, we can go ahead and connect.
   169  
   170  	return s.reconnectCtx(ctx)
   171  }
   172  
   173  func (s *Session) waitForIncoming(ctx context.Context, n int) error {
   174  	for i := 0; i < n; i++ {
   175  		select {
   176  		case <-s.incoming:
   177  			continue
   178  		case <-ctx.Done():
   179  			return ctx.Err()
   180  		}
   181  	}
   182  
   183  	return nil
   184  }
   185  
   186  // reconnect uses the current state to reconnect to a new gateway and UDP
   187  // connection.
   188  func (s *Session) reconnectCtx(ctx context.Context) (err error) {
   189  	s.gateway = voicegateway.New(s.state)
   190  
   191  	// Open the voice gateway. The function will block until Ready is received.
   192  	if err := s.gateway.OpenCtx(ctx); err != nil {
   193  		return errors.Wrap(err, "failed to open voice gateway")
   194  	}
   195  
   196  	// Get the Ready event.
   197  	voiceReady := s.gateway.Ready()
   198  
   199  	// Prepare the UDP voice connection.
   200  	s.voiceUDP, err = udp.DialConnectionCtx(ctx, voiceReady.Addr(), voiceReady.SSRC)
   201  	if err != nil {
   202  		return errors.Wrap(err, "failed to open voice UDP connection")
   203  	}
   204  
   205  	// Get the session description from the voice gateway.
   206  	d, err := s.gateway.SessionDescriptionCtx(ctx, voicegateway.SelectProtocol{
   207  		Protocol: "udp",
   208  		Data: voicegateway.SelectProtocolData{
   209  			Address: s.voiceUDP.GatewayIP,
   210  			Port:    s.voiceUDP.GatewayPort,
   211  			Mode:    Protocol,
   212  		},
   213  	})
   214  	if err != nil {
   215  		return errors.Wrap(err, "failed to select protocol")
   216  	}
   217  
   218  	s.voiceUDP.UseSecret(d.SecretKey)
   219  
   220  	return nil
   221  }
   222  
   223  // Speaking tells Discord we're speaking. This calls
   224  // (*voicegateway.Gateway).Speaking().
   225  func (s *Session) Speaking(flag voicegateway.SpeakingFlag) error {
   226  	// TODO: maybe we don't need to mutex protect IO.
   227  	s.mut.RLock()
   228  	defer s.mut.RUnlock()
   229  
   230  	return s.gateway.Speaking(flag)
   231  }
   232  
   233  func (s *Session) StopSpeaking() error {
   234  	// Send 5 frames of silence.
   235  	for i := 0; i < 5; i++ {
   236  		if _, err := s.Write(OpusSilence[:]); err != nil {
   237  			return errors.Wrapf(err, "failed to send frame %d", i)
   238  		}
   239  	}
   240  	return nil
   241  }
   242  
   243  // UseContext tells the UDP voice connection to write with the given mutex.
   244  func (s *Session) UseContext(ctx context.Context) error {
   245  	s.mut.RLock()
   246  	defer s.mut.RUnlock()
   247  
   248  	if s.voiceUDP == nil {
   249  		return ErrCannotSend
   250  	}
   251  
   252  	return s.voiceUDP.UseContext(ctx)
   253  }
   254  
   255  // Write writes into the UDP voice connection WITHOUT a timeout.
   256  func (s *Session) Write(b []byte) (int, error) {
   257  	return s.WriteCtx(context.Background(), b)
   258  }
   259  
   260  // WriteCtx writes into the UDP voice connection with a context for timeout.
   261  func (s *Session) WriteCtx(ctx context.Context, b []byte) (int, error) {
   262  	s.mut.RLock()
   263  	defer s.mut.RUnlock()
   264  
   265  	if s.voiceUDP == nil {
   266  		return 0, ErrCannotSend
   267  	}
   268  
   269  	return s.voiceUDP.WriteCtx(ctx, b)
   270  }
   271  
   272  func (s *Session) Disconnect() error {
   273  	ctx, cancel := context.WithTimeout(context.Background(), WSTimeout)
   274  	defer cancel()
   275  
   276  	return s.DisconnectCtx(ctx)
   277  }
   278  
   279  func (s *Session) DisconnectCtx(ctx context.Context) error {
   280  	s.mut.Lock()
   281  	defer s.mut.Unlock()
   282  
   283  	// If we're already closed.
   284  	if s.gateway == nil && s.voiceUDP == nil {
   285  		return nil
   286  	}
   287  
   288  	// Notify Discord that we're leaving. This will send a
   289  	// VoiceStateUpdateEvent, in which our handler will promptly remove the
   290  	// session from the map.
   291  
   292  	err := s.session.Gateway.UpdateVoiceStateCtx(ctx, gateway.UpdateVoiceStateData{
   293  		GuildID:   s.state.GuildID,
   294  		ChannelID: discord.ChannelID(discord.NullSnowflake),
   295  		SelfMute:  true,
   296  		SelfDeaf:  true,
   297  	})
   298  
   299  	s.ensureClosed()
   300  	// wrap returns nil if err is nil
   301  	return errors.Wrap(err, "failed to update voice state")
   302  }
   303  
   304  // close ensures everything is closed. It does not acquire the mutex.
   305  func (s *Session) ensureClosed() {
   306  	// If we're already closed.
   307  	if s.gateway == nil && s.voiceUDP == nil {
   308  		return
   309  	}
   310  
   311  	// Disconnect the UDP connection.
   312  	if s.voiceUDP != nil {
   313  		s.voiceUDP.Close()
   314  		s.voiceUDP = nil
   315  	}
   316  
   317  	// Disconnect the voice gateway, ignoring the error.
   318  	if s.gateway != nil {
   319  		if err := s.gateway.Close(); err != nil {
   320  			wsutil.WSDebug("Uncaught voice gateway close error:", err)
   321  		}
   322  		s.gateway = nil
   323  	}
   324  }