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 }