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 }