From 11107f5155216ac33796b34f65b4ec920d730bf2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?To=CF=80?= <git@topi.wtf>
Date: Wed, 4 Oct 2023 10:45:09 +0200
Subject: [PATCH 1/7] make Gateway.Open wait until ready event is received

---
 gateway/gateway_impl.go | 56 ++++++++++++++++++++++++++++++++++-------
 1 file changed, 47 insertions(+), 9 deletions(-)

diff --git a/gateway/gateway_impl.go b/gateway/gateway_impl.go
index dc0ccbb4..644dfbce 100644
--- a/gateway/gateway_impl.go
+++ b/gateway/gateway_impl.go
@@ -80,8 +80,8 @@ func (g *gatewayImpl) open(ctx context.Context) error {
 	g.config.Logger.Debug("opening gateway connection")
 
 	g.connMu.Lock()
-	defer g.connMu.Unlock()
 	if g.conn != nil {
+		g.connMu.Unlock()
 		return discord.ErrGatewayAlreadyConnected
 	}
 	g.status = StatusConnecting
@@ -107,6 +107,7 @@ func (g *gatewayImpl) open(ctx context.Context) error {
 		}
 
 		g.config.Logger.Error("error connecting to the gateway", slog.Any("err", err), slog.String("url", gatewayURL), slog.String("body", body))
+		g.connMu.Unlock()
 		return err
 	}
 
@@ -115,13 +116,27 @@ func (g *gatewayImpl) open(ctx context.Context) error {
 	})
 
 	g.conn = conn
+	g.connMu.Unlock()
 
 	// reset rate limiter when connecting
 	g.config.RateLimiter.Reset()
 
 	g.status = StatusWaitingForHello
 
-	go g.listen(conn)
+	readyChan := make(chan error)
+	go g.listen(conn, readyChan)
+
+	select {
+	case <-ctx.Done():
+		closeCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+		defer cancel()
+		g.Close(closeCtx)
+		return ctx.Err()
+	case err = <-readyChan:
+		if err != nil {
+			return fmt.Errorf("failed to open gateway connection: %w", err)
+		}
+	}
 
 	return nil
 }
@@ -216,6 +231,13 @@ func (g *gatewayImpl) reconnectTry(ctx context.Context, try int) error {
 	}
 
 	if err := g.open(ctx); err != nil {
+		var closeError *websocket.CloseError
+		if errors.As(err, &closeError) {
+			closeCode := CloseEventCodeByCode(closeError.Code)
+			if !closeCode.Reconnect {
+				return err
+			}
+		}
 		if errors.Is(err, discord.ErrGatewayAlreadyConnected) {
 			return err
 		}
@@ -271,7 +293,7 @@ func (g *gatewayImpl) sendHeartbeat() {
 	g.lastHeartbeatSent = time.Now().UTC()
 }
 
-func (g *gatewayImpl) identify() {
+func (g *gatewayImpl) identify() error {
 	g.status = StatusIdentifying
 	g.config.Logger.Debug("sending Identify command")
 
@@ -292,12 +314,13 @@ func (g *gatewayImpl) identify() {
 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 	defer cancel()
 	if err := g.Send(ctx, OpcodeIdentify, identify); err != nil {
-		g.config.Logger.Error("error sending Identify command", slog.Any("err", err))
+		return err
 	}
 	g.status = StatusWaitingForReady
+	return nil
 }
 
-func (g *gatewayImpl) resume() {
+func (g *gatewayImpl) resume() error {
 	g.status = StatusResuming
 	resume := MessageDataResume{
 		Token:     g.token,
@@ -309,16 +332,22 @@ func (g *gatewayImpl) resume() {
 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 	defer cancel()
 	if err := g.Send(ctx, OpcodeResume, resume); err != nil {
-		g.config.Logger.Error("error sending resume command", slog.Any("err", err))
+		return err
 	}
+	return nil
 }
 
-func (g *gatewayImpl) listen(conn *websocket.Conn) {
+func (g *gatewayImpl) listen(conn *websocket.Conn, readyChan chan<- error) {
 	defer g.config.Logger.Debug("exiting listen goroutine")
 loop:
 	for {
 		mt, r, err := conn.NextReader()
 		if err != nil {
+			if g.status != StatusReady {
+				readyChan <- err
+				close(readyChan)
+				break loop
+			}
 			g.connMu.Lock()
 			sameConnection := g.conn == conn
 			g.connMu.Unlock()
@@ -383,9 +412,14 @@ loop:
 			go g.heartbeat()
 
 			if g.config.LastSequenceReceived == nil || g.config.SessionID == nil {
-				g.identify()
+				err = g.identify()
 			} else {
-				g.resume()
+				err = g.resume()
+			}
+			if err != nil {
+				readyChan <- err
+				close(readyChan)
+				return
 			}
 
 		case OpcodeDispatch:
@@ -419,6 +453,10 @@ loop:
 				continue
 			}
 			g.eventHandlerFunc(message.T, message.S, g.config.ShardID, eventData)
+			if _, ok = eventData.(EventReady); ok {
+				readyChan <- nil
+				close(readyChan)
+			}
 
 		case OpcodeHeartbeat:
 			g.sendHeartbeat()

From 130fd344dce5948ec6cd2d6f00189c4f86f47275 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?To=CF=80?= <git@topi.wtf>
Date: Wed, 4 Oct 2023 12:03:38 +0200
Subject: [PATCH 2/7] make sure gateway is closed on ready error

---
 gateway/gateway_impl.go | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/gateway/gateway_impl.go b/gateway/gateway_impl.go
index 644dfbce..f8fa62b3 100644
--- a/gateway/gateway_impl.go
+++ b/gateway/gateway_impl.go
@@ -134,6 +134,9 @@ func (g *gatewayImpl) open(ctx context.Context) error {
 		return ctx.Err()
 	case err = <-readyChan:
 		if err != nil {
+			closeCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+			defer cancel()
+			g.Close(closeCtx)
 			return fmt.Errorf("failed to open gateway connection: %w", err)
 		}
 	}

From cc1355f8d71f7933fba152707be3aebc31e73a5d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?To=CF=80?= <git@topi.wtf>
Date: Tue, 10 Oct 2023 01:43:34 +0200
Subject: [PATCH 3/7] handle resume with rady chan

---
 gateway/gateway_impl.go | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/gateway/gateway_impl.go b/gateway/gateway_impl.go
index f8fa62b3..809a6be3 100644
--- a/gateway/gateway_impl.go
+++ b/gateway/gateway_impl.go
@@ -425,6 +425,12 @@ loop:
 				return
 			}
 
+		case OpcodeResume:
+			g.config.Logger.Debug(g.formatLogs("resume successful"))
+			g.status = StatusReady
+			readyChan <- nil
+			close(readyChan)
+
 		case OpcodeDispatch:
 			// set last sequence received
 			g.config.LastSequenceReceived = &message.S

From 76f1e9bf4f5df88f111f1ccd4f50463ca273e607 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?To=CF=80?= <git@topi.wtf>
Date: Tue, 10 Oct 2023 20:40:42 +0200
Subject: [PATCH 4/7] actually handle the resumed event

---
 gateway/gateway_events.go   |  6 ++++++
 gateway/gateway_impl.go     | 12 ++++++------
 gateway/gateway_messages.go |  2 +-
 3 files changed, 13 insertions(+), 7 deletions(-)

diff --git a/gateway/gateway_events.go b/gateway/gateway_events.go
index b033e018..e4657e09 100644
--- a/gateway/gateway_events.go
+++ b/gateway/gateway_events.go
@@ -43,6 +43,12 @@ type EventReady struct {
 func (EventReady) messageData() {}
 func (EventReady) eventData()   {}
 
+// EventResumed is the event sent by discord when you successfully resume
+type EventResumed struct{}
+
+func (EventResumed) messageData() {}
+func (EventResumed) eventData()   {}
+
 type EventApplicationCommandPermissionsUpdate struct {
 	discord.ApplicationCommandPermissions
 }
diff --git a/gateway/gateway_impl.go b/gateway/gateway_impl.go
index 809a6be3..8591457a 100644
--- a/gateway/gateway_impl.go
+++ b/gateway/gateway_impl.go
@@ -425,12 +425,6 @@ loop:
 				return
 			}
 
-		case OpcodeResume:
-			g.config.Logger.Debug(g.formatLogs("resume successful"))
-			g.status = StatusReady
-			readyChan <- nil
-			close(readyChan)
-
 		case OpcodeDispatch:
 			// set last sequence received
 			g.config.LastSequenceReceived = &message.S
@@ -463,6 +457,12 @@ loop:
 			}
 			g.eventHandlerFunc(message.T, message.S, g.config.ShardID, eventData)
 			if _, ok = eventData.(EventReady); ok {
+				g.config.Logger.Debug(g.formatLogs("ready successful"))
+				readyChan <- nil
+				close(readyChan)
+			} else if _, ok = eventData.(EventResumed); ok {
+				g.config.Logger.Debug(g.formatLogs("resume successful"))
+				g.status = StatusReady
 				readyChan <- nil
 				close(readyChan)
 			}
diff --git a/gateway/gateway_messages.go b/gateway/gateway_messages.go
index 8b538071..c44936ec 100644
--- a/gateway/gateway_messages.go
+++ b/gateway/gateway_messages.go
@@ -119,7 +119,7 @@ func UnmarshalEventData(data []byte, eventType EventType) (EventData, error) {
 		eventData = d
 
 	case EventTypeResumed:
-		// no data
+		eventData = EventResumed{}
 
 	case EventTypeApplicationCommandPermissionsUpdate:
 		var d EventApplicationCommandPermissionsUpdate

From 775dce0b2bf0190292d0ecc8d1927b5445098aba Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?To=CF=80?= <git@topi.wtf>
Date: Fri, 10 Nov 2023 22:39:36 +0100
Subject: [PATCH 5/7] migrate to slog & go 1.21 (#294)

---
 .github/workflows/lint.yml | 38 ++++++++++++++++++++++++++++++++++++++
 gateway/gateway_impl.go    |  4 ++--
 2 files changed, 40 insertions(+), 2 deletions(-)
 create mode 100644 .github/workflows/lint.yml

diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 00000000..c20a0c05
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,38 @@
+name: Go
+
+on:
+    push:
+    pull_request_target:
+
+jobs:
+    gobuild:
+        runs-on: ubuntu-latest
+        steps:
+            -   uses: actions/setup-go@v3
+                with:
+                    go-version: 1.21
+            -   uses: actions/checkout@v3
+            -   name: go build
+                run: go build -v ./...
+
+    gotest:
+        runs-on: ubuntu-latest
+        steps:
+            -   uses: actions/setup-go@v3
+                with:
+                    go-version: 1.21
+            -   uses: actions/checkout@v3
+            -   name: go build
+                run: go test -v ./...
+
+    golangci:
+        runs-on: ubuntu-latest
+        steps:
+            -   uses: actions/setup-go@v3
+                with:
+                    go-version: 1.21
+            -   uses: actions/checkout@v3
+            -   name: golangci-lint
+                uses: golangci/golangci-lint-action@v3
+                with:
+                    version: latest
diff --git a/gateway/gateway_impl.go b/gateway/gateway_impl.go
index 8591457a..e7fec0bb 100644
--- a/gateway/gateway_impl.go
+++ b/gateway/gateway_impl.go
@@ -457,11 +457,11 @@ loop:
 			}
 			g.eventHandlerFunc(message.T, message.S, g.config.ShardID, eventData)
 			if _, ok = eventData.(EventReady); ok {
-				g.config.Logger.Debug(g.formatLogs("ready successful"))
+				g.config.Logger.Debug("ready successful")
 				readyChan <- nil
 				close(readyChan)
 			} else if _, ok = eventData.(EventResumed); ok {
-				g.config.Logger.Debug(g.formatLogs("resume successful"))
+				g.config.Logger.Debug("resume successful")
 				g.status = StatusReady
 				readyChan <- nil
 				close(readyChan)

From e7eed7aa2c24ff751765a82ee6cb62dccc7954ae Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?To=CF=80?= <git@topi.wtf>
Date: Wed, 4 Oct 2023 10:45:09 +0200
Subject: [PATCH 6/7] make Gateway.Open wait until ready event is received

---
 gateway/gateway_impl.go | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/gateway/gateway_impl.go b/gateway/gateway_impl.go
index e7fec0bb..73205cf0 100644
--- a/gateway/gateway_impl.go
+++ b/gateway/gateway_impl.go
@@ -466,6 +466,10 @@ loop:
 				readyChan <- nil
 				close(readyChan)
 			}
+			if _, ok = eventData.(EventReady); ok {
+				readyChan <- nil
+				close(readyChan)
+			}
 
 		case OpcodeHeartbeat:
 			g.sendHeartbeat()

From 82b4fa8068d989fc70bb433dad75b6ef927d02bc Mon Sep 17 00:00:00 2001
From: apricotbucket28 <agustin.nicolas.marcos@outlook.com>
Date: Sun, 16 Feb 2025 13:31:48 -0300
Subject: [PATCH 7/7] handle ready and resume before dispatching

---
 .github/workflows/lint.yml | 38 --------------------------------------
 gateway/gateway_impl.go    | 24 ++++++++----------------
 2 files changed, 8 insertions(+), 54 deletions(-)
 delete mode 100644 .github/workflows/lint.yml

diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
deleted file mode 100644
index c20a0c05..00000000
--- a/.github/workflows/lint.yml
+++ /dev/null
@@ -1,38 +0,0 @@
-name: Go
-
-on:
-    push:
-    pull_request_target:
-
-jobs:
-    gobuild:
-        runs-on: ubuntu-latest
-        steps:
-            -   uses: actions/setup-go@v3
-                with:
-                    go-version: 1.21
-            -   uses: actions/checkout@v3
-            -   name: go build
-                run: go build -v ./...
-
-    gotest:
-        runs-on: ubuntu-latest
-        steps:
-            -   uses: actions/setup-go@v3
-                with:
-                    go-version: 1.21
-            -   uses: actions/checkout@v3
-            -   name: go build
-                run: go test -v ./...
-
-    golangci:
-        runs-on: ubuntu-latest
-        steps:
-            -   uses: actions/setup-go@v3
-                with:
-                    go-version: 1.21
-            -   uses: actions/checkout@v3
-            -   name: golangci-lint
-                uses: golangci/golangci-lint-action@v3
-                with:
-                    version: latest
diff --git a/gateway/gateway_impl.go b/gateway/gateway_impl.go
index 73205cf0..0eef0caa 100644
--- a/gateway/gateway_impl.go
+++ b/gateway/gateway_impl.go
@@ -435,12 +435,18 @@ loop:
 				continue
 			}
 
-			// get session id here
 			if readyEvent, ok := eventData.(EventReady); ok {
 				g.config.SessionID = &readyEvent.SessionID
 				g.config.ResumeURL = &readyEvent.ResumeGatewayURL
-				g.status = StatusReady
 				g.config.Logger.Debug("ready message received")
+				g.status = StatusReady
+				readyChan <- nil
+				close(readyChan)
+			} else if _, ok = eventData.(EventResumed); ok {
+				g.config.Logger.Debug("resume message received")
+				g.status = StatusReady
+				readyChan <- nil
+				close(readyChan)
 			}
 
 			// push message to the command manager
@@ -456,20 +462,6 @@ loop:
 				continue
 			}
 			g.eventHandlerFunc(message.T, message.S, g.config.ShardID, eventData)
-			if _, ok = eventData.(EventReady); ok {
-				g.config.Logger.Debug("ready successful")
-				readyChan <- nil
-				close(readyChan)
-			} else if _, ok = eventData.(EventResumed); ok {
-				g.config.Logger.Debug("resume successful")
-				g.status = StatusReady
-				readyChan <- nil
-				close(readyChan)
-			}
-			if _, ok = eventData.(EventReady); ok {
-				readyChan <- nil
-				close(readyChan)
-			}
 
 		case OpcodeHeartbeat:
 			g.sendHeartbeat()