1// Discordgo - Discord bindings for Go 2// Available at https://github.com/bwmarrin/discordgo 3 4// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved. 5// Use of this source code is governed by a BSD-style 6// license that can be found in the LICENSE file. 7 8// This file contains low level functions for interacting with the Discord 9// data websocket interface. 10 11package discordgo 12 13import ( 14 "bytes" 15 "compress/zlib" 16 "encoding/json" 17 "errors" 18 "fmt" 19 "io" 20 "net/http" 21 "sync/atomic" 22 "time" 23 24 "github.com/gorilla/websocket" 25) 26 27// ErrWSAlreadyOpen is thrown when you attempt to open 28// a websocket that already is open. 29var ErrWSAlreadyOpen = errors.New("web socket already opened") 30 31// ErrWSNotFound is thrown when you attempt to use a websocket 32// that doesn't exist 33var ErrWSNotFound = errors.New("no websocket connection exists") 34 35// ErrWSShardBounds is thrown when you try to use a shard ID that is 36// less than the total shard count 37var ErrWSShardBounds = errors.New("ShardID must be less than ShardCount") 38 39type resumePacket struct { 40 Op int `json:"op"` 41 Data struct { 42 Token string `json:"token"` 43 SessionID string `json:"session_id"` 44 Sequence int64 `json:"seq"` 45 } `json:"d"` 46} 47 48// Open creates a websocket connection to Discord. 49// See: https://discord.com/developers/docs/topics/gateway#connecting 50func (s *Session) Open() error { 51 s.log(LogInformational, "called") 52 53 var err error 54 55 // Prevent Open or other major Session functions from 56 // being called while Open is still running. 57 s.Lock() 58 defer s.Unlock() 59 60 // If the websock is already open, bail out here. 61 if s.wsConn != nil { 62 return ErrWSAlreadyOpen 63 } 64 65 // Get the gateway to use for the Websocket connection 66 if s.gateway == "" { 67 s.gateway, err = s.Gateway() 68 if err != nil { 69 return err 70 } 71 72 // Add the version and encoding to the URL 73 s.gateway = s.gateway + "?v=" + APIVersion + "&encoding=json" 74 } 75 76 // Connect to the Gateway 77 s.log(LogInformational, "connecting to gateway %s", s.gateway) 78 header := http.Header{} 79 header.Add("accept-encoding", "zlib") 80 s.wsConn, _, err = websocket.DefaultDialer.Dial(s.gateway, header) 81 if err != nil { 82 s.log(LogError, "error connecting to gateway %s, %s", s.gateway, err) 83 s.gateway = "" // clear cached gateway 84 s.wsConn = nil // Just to be safe. 85 return err 86 } 87 88 s.wsConn.SetCloseHandler(func(code int, text string) error { 89 return nil 90 }) 91 92 defer func() { 93 // because of this, all code below must set err to the error 94 // when exiting with an error :) Maybe someone has a better 95 // way :) 96 if err != nil { 97 s.wsConn.Close() 98 s.wsConn = nil 99 } 100 }() 101 102 // The first response from Discord should be an Op 10 (Hello) Packet. 103 // When processed by onEvent the heartbeat goroutine will be started. 104 mt, m, err := s.wsConn.ReadMessage() 105 if err != nil { 106 return err 107 } 108 e, err := s.onEvent(mt, m) 109 if err != nil { 110 return err 111 } 112 if e.Operation != 10 { 113 err = fmt.Errorf("expecting Op 10, got Op %d instead", e.Operation) 114 return err 115 } 116 s.log(LogInformational, "Op 10 Hello Packet received from Discord") 117 s.LastHeartbeatAck = time.Now().UTC() 118 var h helloOp 119 if err = json.Unmarshal(e.RawData, &h); err != nil { 120 err = fmt.Errorf("error unmarshalling helloOp, %s", err) 121 return err 122 } 123 124 // Now we send either an Op 2 Identity if this is a brand new 125 // connection or Op 6 Resume if we are resuming an existing connection. 126 sequence := atomic.LoadInt64(s.sequence) 127 if s.sessionID == "" && sequence == 0 { 128 129 // Send Op 2 Identity Packet 130 err = s.identify() 131 if err != nil { 132 err = fmt.Errorf("error sending identify packet to gateway, %s, %s", s.gateway, err) 133 return err 134 } 135 136 } else { 137 138 // Send Op 6 Resume Packet 139 p := resumePacket{} 140 p.Op = 6 141 p.Data.Token = s.Token 142 p.Data.SessionID = s.sessionID 143 p.Data.Sequence = sequence 144 145 s.log(LogInformational, "sending resume packet to gateway") 146 s.wsMutex.Lock() 147 err = s.wsConn.WriteJSON(p) 148 s.wsMutex.Unlock() 149 if err != nil { 150 err = fmt.Errorf("error sending gateway resume packet, %s, %s", s.gateway, err) 151 return err 152 } 153 154 } 155 156 // A basic state is a hard requirement for Voice. 157 // We create it here so the below READY/RESUMED packet can populate 158 // the state :) 159 // XXX: Move to New() func? 160 if s.State == nil { 161 state := NewState() 162 state.TrackChannels = false 163 state.TrackEmojis = false 164 state.TrackMembers = false 165 state.TrackRoles = false 166 state.TrackVoice = false 167 s.State = state 168 } 169 170 // Now Discord should send us a READY or RESUMED packet. 171 mt, m, err = s.wsConn.ReadMessage() 172 if err != nil { 173 return err 174 } 175 e, err = s.onEvent(mt, m) 176 if err != nil { 177 return err 178 } 179 if e.Type != `READY` && e.Type != `RESUMED` { 180 // This is not fatal, but it does not follow their API documentation. 181 s.log(LogWarning, "Expected READY/RESUMED, instead got:\n%#v\n", e) 182 } 183 s.log(LogInformational, "First Packet:\n%#v\n", e) 184 185 s.log(LogInformational, "We are now connected to Discord, emitting connect event") 186 s.handleEvent(connectEventType, &Connect{}) 187 188 // A VoiceConnections map is a hard requirement for Voice. 189 // XXX: can this be moved to when opening a voice connection? 190 if s.VoiceConnections == nil { 191 s.log(LogInformational, "creating new VoiceConnections map") 192 s.VoiceConnections = make(map[string]*VoiceConnection) 193 } 194 195 // Create listening chan outside of listen, as it needs to happen inside the 196 // mutex lock and needs to exist before calling heartbeat and listen 197 // go rountines. 198 s.listening = make(chan interface{}) 199 200 // Start sending heartbeats and reading messages from Discord. 201 go s.heartbeat(s.wsConn, s.listening, h.HeartbeatInterval) 202 go s.listen(s.wsConn, s.listening) 203 204 s.log(LogInformational, "exiting") 205 return nil 206} 207 208// listen polls the websocket connection for events, it will stop when the 209// listening channel is closed, or an error occurs. 210func (s *Session) listen(wsConn *websocket.Conn, listening <-chan interface{}) { 211 212 s.log(LogInformational, "called") 213 214 for { 215 216 messageType, message, err := wsConn.ReadMessage() 217 218 if err != nil { 219 220 // Detect if we have been closed manually. If a Close() has already 221 // happened, the websocket we are listening on will be different to 222 // the current session. 223 s.RLock() 224 sameConnection := s.wsConn == wsConn 225 s.RUnlock() 226 227 if sameConnection { 228 229 s.log(LogWarning, "error reading from gateway %s websocket, %s", s.gateway, err) 230 // There has been an error reading, close the websocket so that 231 // OnDisconnect event is emitted. 232 err := s.Close() 233 if err != nil { 234 s.log(LogWarning, "error closing session connection, %s", err) 235 } 236 237 s.log(LogInformational, "calling reconnect() now") 238 s.reconnect() 239 } 240 241 return 242 } 243 244 select { 245 246 case <-listening: 247 return 248 249 default: 250 s.onEvent(messageType, message) 251 252 } 253 } 254} 255 256type heartbeatOp struct { 257 Op int `json:"op"` 258 Data int64 `json:"d"` 259} 260 261type helloOp struct { 262 HeartbeatInterval time.Duration `json:"heartbeat_interval"` 263} 264 265// FailedHeartbeatAcks is the Number of heartbeat intervals to wait until forcing a connection restart. 266const FailedHeartbeatAcks time.Duration = 5 * time.Millisecond 267 268// HeartbeatLatency returns the latency between heartbeat acknowledgement and heartbeat send. 269func (s *Session) HeartbeatLatency() time.Duration { 270 271 return s.LastHeartbeatAck.Sub(s.LastHeartbeatSent) 272 273} 274 275// heartbeat sends regular heartbeats to Discord so it knows the client 276// is still connected. If you do not send these heartbeats Discord will 277// disconnect the websocket connection after a few seconds. 278func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{}, heartbeatIntervalMsec time.Duration) { 279 280 s.log(LogInformational, "called") 281 282 if listening == nil || wsConn == nil { 283 return 284 } 285 286 var err error 287 ticker := time.NewTicker(heartbeatIntervalMsec * time.Millisecond) 288 defer ticker.Stop() 289 290 for { 291 s.RLock() 292 last := s.LastHeartbeatAck 293 s.RUnlock() 294 sequence := atomic.LoadInt64(s.sequence) 295 s.log(LogDebug, "sending gateway websocket heartbeat seq %d", sequence) 296 s.wsMutex.Lock() 297 s.LastHeartbeatSent = time.Now().UTC() 298 err = wsConn.WriteJSON(heartbeatOp{1, sequence}) 299 s.wsMutex.Unlock() 300 if err != nil || time.Now().UTC().Sub(last) > (heartbeatIntervalMsec*FailedHeartbeatAcks) { 301 if err != nil { 302 s.log(LogError, "error sending heartbeat to gateway %s, %s", s.gateway, err) 303 } else { 304 s.log(LogError, "haven't gotten a heartbeat ACK in %v, triggering a reconnection", time.Now().UTC().Sub(last)) 305 } 306 s.Close() 307 s.reconnect() 308 return 309 } 310 s.Lock() 311 s.DataReady = true 312 s.Unlock() 313 314 select { 315 case <-ticker.C: 316 // continue loop and send heartbeat 317 case <-listening: 318 return 319 } 320 } 321} 322 323// UpdateStatusData ia provided to UpdateStatusComplex() 324type UpdateStatusData struct { 325 IdleSince *int `json:"since"` 326 Activities []*Activity `json:"activities"` 327 AFK bool `json:"afk"` 328 Status string `json:"status"` 329} 330 331type updateStatusOp struct { 332 Op int `json:"op"` 333 Data UpdateStatusData `json:"d"` 334} 335 336func newUpdateStatusData(idle int, activityType ActivityType, name, url string) *UpdateStatusData { 337 usd := &UpdateStatusData{ 338 Status: "online", 339 } 340 341 if idle > 0 { 342 usd.IdleSince = &idle 343 } 344 345 if name != "" { 346 usd.Activities = []*Activity{{ 347 Name: name, 348 Type: activityType, 349 URL: url, 350 }} 351 } 352 353 return usd 354} 355 356// UpdateGameStatus is used to update the user's status. 357// If idle>0 then set status to idle. 358// If name!="" then set game. 359// if otherwise, set status to active, and no activity. 360func (s *Session) UpdateGameStatus(idle int, name string) (err error) { 361 return s.UpdateStatusComplex(*newUpdateStatusData(idle, ActivityTypeGame, name, "")) 362} 363 364// UpdateStreamingStatus is used to update the user's streaming status. 365// If idle>0 then set status to idle. 366// If name!="" then set game. 367// If name!="" and url!="" then set the status type to streaming with the URL set. 368// if otherwise, set status to active, and no game. 369func (s *Session) UpdateStreamingStatus(idle int, name string, url string) (err error) { 370 gameType := ActivityTypeGame 371 if url != "" { 372 gameType = ActivityTypeStreaming 373 } 374 return s.UpdateStatusComplex(*newUpdateStatusData(idle, gameType, name, url)) 375} 376 377// UpdateListeningStatus is used to set the user to "Listening to..." 378// If name!="" then set to what user is listening to 379// Else, set user to active and no activity. 380func (s *Session) UpdateListeningStatus(name string) (err error) { 381 return s.UpdateStatusComplex(*newUpdateStatusData(0, ActivityTypeListening, name, "")) 382} 383 384// UpdateStatusComplex allows for sending the raw status update data untouched by discordgo. 385func (s *Session) UpdateStatusComplex(usd UpdateStatusData) (err error) { 386 387 s.RLock() 388 defer s.RUnlock() 389 if s.wsConn == nil { 390 return ErrWSNotFound 391 } 392 393 s.wsMutex.Lock() 394 err = s.wsConn.WriteJSON(updateStatusOp{3, usd}) 395 s.wsMutex.Unlock() 396 397 return 398} 399 400type requestGuildMembersData struct { 401 GuildIDs []string `json:"guild_id"` 402 Query string `json:"query"` 403 Limit int `json:"limit"` 404 Presences bool `json:"presences"` 405} 406 407type requestGuildMembersOp struct { 408 Op int `json:"op"` 409 Data requestGuildMembersData `json:"d"` 410} 411 412// RequestGuildMembers requests guild members from the gateway 413// The gateway responds with GuildMembersChunk events 414// guildID : Single Guild ID to request members of 415// query : String that username starts with, leave empty to return all members 416// limit : Max number of items to return, or 0 to request all members matched 417// presences : Whether to request presences of guild members 418func (s *Session) RequestGuildMembers(guildID string, query string, limit int, presences bool) (err error) { 419 data := requestGuildMembersData{ 420 GuildIDs: []string{guildID}, 421 Query: query, 422 Limit: limit, 423 Presences: presences, 424 } 425 err = s.requestGuildMembers(data) 426 return 427} 428 429// RequestGuildMembersBatch requests guild members from the gateway 430// The gateway responds with GuildMembersChunk events 431// guildID : Slice of guild IDs to request members of 432// query : String that username starts with, leave empty to return all members 433// limit : Max number of items to return, or 0 to request all members matched 434// presences : Whether to request presences of guild members 435func (s *Session) RequestGuildMembersBatch(guildIDs []string, query string, limit int, presences bool) (err error) { 436 data := requestGuildMembersData{ 437 GuildIDs: guildIDs, 438 Query: query, 439 Limit: limit, 440 Presences: presences, 441 } 442 err = s.requestGuildMembers(data) 443 return 444} 445 446func (s *Session) requestGuildMembers(data requestGuildMembersData) (err error) { 447 s.log(LogInformational, "called") 448 449 s.RLock() 450 defer s.RUnlock() 451 if s.wsConn == nil { 452 return ErrWSNotFound 453 } 454 455 s.wsMutex.Lock() 456 err = s.wsConn.WriteJSON(requestGuildMembersOp{8, data}) 457 s.wsMutex.Unlock() 458 459 return 460} 461 462// onEvent is the "event handler" for all messages received on the 463// Discord Gateway API websocket connection. 464// 465// If you use the AddHandler() function to register a handler for a 466// specific event this function will pass the event along to that handler. 467// 468// If you use the AddHandler() function to register a handler for the 469// "OnEvent" event then all events will be passed to that handler. 470func (s *Session) onEvent(messageType int, message []byte) (*Event, error) { 471 472 var err error 473 var reader io.Reader 474 reader = bytes.NewBuffer(message) 475 476 // If this is a compressed message, uncompress it. 477 if messageType == websocket.BinaryMessage { 478 479 z, err2 := zlib.NewReader(reader) 480 if err2 != nil { 481 s.log(LogError, "error uncompressing websocket message, %s", err) 482 return nil, err2 483 } 484 485 defer func() { 486 err3 := z.Close() 487 if err3 != nil { 488 s.log(LogWarning, "error closing zlib, %s", err) 489 } 490 }() 491 492 reader = z 493 } 494 495 // Decode the event into an Event struct. 496 var e *Event 497 decoder := json.NewDecoder(reader) 498 if err = decoder.Decode(&e); err != nil { 499 s.log(LogError, "error decoding websocket message, %s", err) 500 return e, err 501 } 502 503 s.log(LogDebug, "Op: %d, Seq: %d, Type: %s, Data: %s\n\n", e.Operation, e.Sequence, e.Type, string(e.RawData)) 504 505 // Ping request. 506 // Must respond with a heartbeat packet within 5 seconds 507 if e.Operation == 1 { 508 s.log(LogInformational, "sending heartbeat in response to Op1") 509 s.wsMutex.Lock() 510 err = s.wsConn.WriteJSON(heartbeatOp{1, atomic.LoadInt64(s.sequence)}) 511 s.wsMutex.Unlock() 512 if err != nil { 513 s.log(LogError, "error sending heartbeat in response to Op1") 514 return e, err 515 } 516 517 return e, nil 518 } 519 520 // Reconnect 521 // Must immediately disconnect from gateway and reconnect to new gateway. 522 if e.Operation == 7 { 523 s.log(LogInformational, "Closing and reconnecting in response to Op7") 524 s.CloseWithCode(websocket.CloseServiceRestart) 525 s.reconnect() 526 return e, nil 527 } 528 529 // Invalid Session 530 // Must respond with a Identify packet. 531 if e.Operation == 9 { 532 533 s.log(LogInformational, "sending identify packet to gateway in response to Op9") 534 535 err = s.identify() 536 if err != nil { 537 s.log(LogWarning, "error sending gateway identify packet, %s, %s", s.gateway, err) 538 return e, err 539 } 540 541 return e, nil 542 } 543 544 if e.Operation == 10 { 545 // Op10 is handled by Open() 546 return e, nil 547 } 548 549 if e.Operation == 11 { 550 s.Lock() 551 s.LastHeartbeatAck = time.Now().UTC() 552 s.Unlock() 553 s.log(LogDebug, "got heartbeat ACK") 554 return e, nil 555 } 556 557 // Do not try to Dispatch a non-Dispatch Message 558 if e.Operation != 0 { 559 // But we probably should be doing something with them. 560 // TEMP 561 s.log(LogWarning, "unknown Op: %d, Seq: %d, Type: %s, Data: %s, message: %s", e.Operation, e.Sequence, e.Type, string(e.RawData), string(message)) 562 return e, nil 563 } 564 565 // Store the message sequence 566 atomic.StoreInt64(s.sequence, e.Sequence) 567 568 // Map event to registered event handlers and pass it along to any registered handlers. 569 if eh, ok := registeredInterfaceProviders[e.Type]; ok { 570 e.Struct = eh.New() 571 572 // Attempt to unmarshal our event. 573 if err = json.Unmarshal(e.RawData, e.Struct); err != nil { 574 s.log(LogError, "error unmarshalling %s event, %s", e.Type, err) 575 } 576 577 // Send event to any registered event handlers for it's type. 578 // Because the above doesn't cancel this, in case of an error 579 // the struct could be partially populated or at default values. 580 // However, most errors are due to a single field and I feel 581 // it's better to pass along what we received than nothing at all. 582 // TODO: Think about that decision :) 583 // Either way, READY events must fire, even with errors. 584 s.handleEvent(e.Type, e.Struct) 585 } else { 586 s.log(LogWarning, "unknown event: Op: %d, Seq: %d, Type: %s, Data: %s", e.Operation, e.Sequence, e.Type, string(e.RawData)) 587 } 588 589 // For legacy reasons, we send the raw event also, this could be useful for handling unknown events. 590 s.handleEvent(eventEventType, e) 591 592 return e, nil 593} 594 595// ------------------------------------------------------------------------------------------------ 596// Code related to voice connections that initiate over the data websocket 597// ------------------------------------------------------------------------------------------------ 598 599type voiceChannelJoinData struct { 600 GuildID *string `json:"guild_id"` 601 ChannelID *string `json:"channel_id"` 602 SelfMute bool `json:"self_mute"` 603 SelfDeaf bool `json:"self_deaf"` 604} 605 606type voiceChannelJoinOp struct { 607 Op int `json:"op"` 608 Data voiceChannelJoinData `json:"d"` 609} 610 611// ChannelVoiceJoin joins the session user to a voice channel. 612// 613// gID : Guild ID of the channel to join. 614// cID : Channel ID of the channel to join. 615// mute : If true, you will be set to muted upon joining. 616// deaf : If true, you will be set to deafened upon joining. 617func (s *Session) ChannelVoiceJoin(gID, cID string, mute, deaf bool) (voice *VoiceConnection, err error) { 618 619 s.log(LogInformational, "called") 620 621 s.RLock() 622 voice, _ = s.VoiceConnections[gID] 623 s.RUnlock() 624 625 if voice == nil { 626 voice = &VoiceConnection{} 627 s.Lock() 628 s.VoiceConnections[gID] = voice 629 s.Unlock() 630 } 631 632 voice.Lock() 633 voice.GuildID = gID 634 voice.ChannelID = cID 635 voice.deaf = deaf 636 voice.mute = mute 637 voice.session = s 638 voice.Unlock() 639 640 err = s.ChannelVoiceJoinManual(gID, cID, mute, deaf) 641 if err != nil { 642 return 643 } 644 645 // doesn't exactly work perfect yet.. TODO 646 err = voice.waitUntilConnected() 647 if err != nil { 648 s.log(LogWarning, "error waiting for voice to connect, %s", err) 649 voice.Close() 650 return 651 } 652 653 return 654} 655 656// ChannelVoiceJoinManual initiates a voice session to a voice channel, but does not complete it. 657// 658// This should only be used when the VoiceServerUpdate will be intercepted and used elsewhere. 659// 660// gID : Guild ID of the channel to join. 661// cID : Channel ID of the channel to join, leave empty to disconnect. 662// mute : If true, you will be set to muted upon joining. 663// deaf : If true, you will be set to deafened upon joining. 664func (s *Session) ChannelVoiceJoinManual(gID, cID string, mute, deaf bool) (err error) { 665 666 s.log(LogInformational, "called") 667 668 var channelID *string 669 if cID == "" { 670 channelID = nil 671 } else { 672 channelID = &cID 673 } 674 675 // Send the request to Discord that we want to join the voice channel 676 data := voiceChannelJoinOp{4, voiceChannelJoinData{&gID, channelID, mute, deaf}} 677 s.wsMutex.Lock() 678 err = s.wsConn.WriteJSON(data) 679 s.wsMutex.Unlock() 680 return 681} 682 683// onVoiceStateUpdate handles Voice State Update events on the data websocket. 684func (s *Session) onVoiceStateUpdate(st *VoiceStateUpdate) { 685 686 // If we don't have a connection for the channel, don't bother 687 if st.ChannelID == "" { 688 return 689 } 690 691 // Check if we have a voice connection to update 692 s.RLock() 693 voice, exists := s.VoiceConnections[st.GuildID] 694 s.RUnlock() 695 if !exists { 696 return 697 } 698 699 // We only care about events that are about us. 700 if s.State.User.ID != st.UserID { 701 return 702 } 703 704 // Store the SessionID for later use. 705 voice.Lock() 706 voice.UserID = st.UserID 707 voice.sessionID = st.SessionID 708 voice.ChannelID = st.ChannelID 709 voice.Unlock() 710} 711 712// onVoiceServerUpdate handles the Voice Server Update data websocket event. 713// 714// This is also fired if the Guild's voice region changes while connected 715// to a voice channel. In that case, need to re-establish connection to 716// the new region endpoint. 717func (s *Session) onVoiceServerUpdate(st *VoiceServerUpdate) { 718 719 s.log(LogInformational, "called") 720 721 s.RLock() 722 voice, exists := s.VoiceConnections[st.GuildID] 723 s.RUnlock() 724 725 // If no VoiceConnection exists, just skip this 726 if !exists { 727 return 728 } 729 730 // If currently connected to voice ws/udp, then disconnect. 731 // Has no effect if not connected. 732 voice.Close() 733 734 // Store values for later use 735 voice.Lock() 736 voice.token = st.Token 737 voice.endpoint = st.Endpoint 738 voice.GuildID = st.GuildID 739 voice.Unlock() 740 741 // Open a connection to the voice server 742 err := voice.open() 743 if err != nil { 744 s.log(LogError, "onVoiceServerUpdate voice.open, %s", err) 745 } 746} 747 748type identifyOp struct { 749 Op int `json:"op"` 750 Data Identify `json:"d"` 751} 752 753// identify sends the identify packet to the gateway 754func (s *Session) identify() error { 755 s.log(LogDebug, "called") 756 757 // TODO: This is a temporary block of code to help 758 // maintain backwards compatability 759 if s.Compress == false { 760 s.Identify.Compress = false 761 } 762 763 // TODO: This is a temporary block of code to help 764 // maintain backwards compatability 765 if s.Token != "" && s.Identify.Token == "" { 766 s.Identify.Token = s.Token 767 } 768 769 // TODO: Below block should be refactored so ShardID and ShardCount 770 // can be deprecated and their usage moved to the Session.Identify 771 // struct 772 if s.ShardCount > 1 { 773 774 if s.ShardID >= s.ShardCount { 775 return ErrWSShardBounds 776 } 777 778 s.Identify.Shard = &[2]int{s.ShardID, s.ShardCount} 779 } 780 781 // Send Identify packet to Discord 782 op := identifyOp{2, s.Identify} 783 s.log(LogDebug, "Identify Packet: \n%#v", op) 784 s.wsMutex.Lock() 785 err := s.wsConn.WriteJSON(op) 786 s.wsMutex.Unlock() 787 788 return err 789} 790 791func (s *Session) reconnect() { 792 793 s.log(LogInformational, "called") 794 795 var err error 796 797 if s.ShouldReconnectOnError { 798 799 wait := time.Duration(1) 800 801 for { 802 s.log(LogInformational, "trying to reconnect to gateway") 803 804 err = s.Open() 805 if err == nil { 806 s.log(LogInformational, "successfully reconnected to gateway") 807 808 // I'm not sure if this is actually needed. 809 // if the gw reconnect works properly, voice should stay alive 810 // However, there seems to be cases where something "weird" 811 // happens. So we're doing this for now just to improve 812 // stability in those edge cases. 813 s.RLock() 814 defer s.RUnlock() 815 for _, v := range s.VoiceConnections { 816 817 s.log(LogInformational, "reconnecting voice connection to guild %s", v.GuildID) 818 go v.reconnect() 819 820 // This is here just to prevent violently spamming the 821 // voice reconnects 822 time.Sleep(1 * time.Second) 823 824 } 825 return 826 } 827 828 // Certain race conditions can call reconnect() twice. If this happens, we 829 // just break out of the reconnect loop 830 if err == ErrWSAlreadyOpen { 831 s.log(LogInformational, "Websocket already exists, no need to reconnect") 832 return 833 } 834 835 s.log(LogError, "error reconnecting to gateway, %s", err) 836 837 <-time.After(wait * time.Second) 838 wait *= 2 839 if wait > 600 { 840 wait = 600 841 } 842 } 843 } 844} 845 846// Close closes a websocket and stops all listening/heartbeat goroutines. 847// TODO: Add support for Voice WS/UDP 848func (s *Session) Close() error { 849 return s.CloseWithCode(websocket.CloseNormalClosure) 850} 851 852// CloseWithCode closes a websocket using the provided closeCode and stops all 853// listening/heartbeat goroutines. 854// TODO: Add support for Voice WS/UDP connections 855func (s *Session) CloseWithCode(closeCode int) (err error) { 856 857 s.log(LogInformational, "called") 858 s.Lock() 859 860 s.DataReady = false 861 862 if s.listening != nil { 863 s.log(LogInformational, "closing listening channel") 864 close(s.listening) 865 s.listening = nil 866 } 867 868 // TODO: Close all active Voice Connections too 869 // this should force stop any reconnecting voice channels too 870 871 if s.wsConn != nil { 872 873 s.log(LogInformational, "sending close frame") 874 // To cleanly close a connection, a client should send a close 875 // frame and wait for the server to close the connection. 876 s.wsMutex.Lock() 877 err := s.wsConn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(closeCode, "")) 878 s.wsMutex.Unlock() 879 if err != nil { 880 s.log(LogInformational, "error closing websocket, %s", err) 881 } 882 883 // TODO: Wait for Discord to actually close the connection. 884 time.Sleep(1 * time.Second) 885 886 s.log(LogInformational, "closing gateway websocket") 887 err = s.wsConn.Close() 888 if err != nil { 889 s.log(LogInformational, "error closing websocket, %s", err) 890 } 891 892 s.wsConn = nil 893 } 894 895 s.Unlock() 896 897 s.log(LogInformational, "emit disconnect event") 898 s.handleEvent(disconnectEventType, &Disconnect{}) 899 900 return 901} 902