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