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 }