aboutsummaryrefslogtreecommitdiff
path: root/pkg/discord
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/discord')
-rw-r--r--pkg/discord/discord.go106
-rw-r--r--pkg/discord/entities.go15
-rw-r--r--pkg/discord/payloads.go47
3 files changed, 157 insertions, 11 deletions
diff --git a/pkg/discord/discord.go b/pkg/discord/discord.go
index 13de020..1fefe6c 100644
--- a/pkg/discord/discord.go
+++ b/pkg/discord/discord.go
@@ -2,10 +2,14 @@ package discord
import (
"bytes"
+ "context"
"encoding/json"
"errors"
"fmt"
+ "log"
+ "math/rand"
"net/http"
+ "time"
"github.com/gorilla/websocket"
)
@@ -15,41 +19,59 @@ const USER_AGENT = "DiscordBot (https://jinx.rnrd.eu/, v0.0.0) Jinx"
type Discord struct {
Token string
+ Conn *websocket.Conn
}
func NewClient(token string) *Discord {
return &Discord{
Token: token,
+ Conn: nil,
}
}
-func (d *Discord) Connect() error {
- gatewayURL, err := d.gateway()
+func (d *Discord) Connect(ctx context.Context) error {
+ gatewayURL, err := d.getGateway()
if err != nil {
return err
}
- fmt.Printf("Gateway: %s\n", gatewayURL)
+ fmt.Printf("gateway: %s\n", gatewayURL)
connectHeader := http.Header{}
- conn, _, err := websocket.DefaultDialer.Dial(gatewayURL, connectHeader)
+ d.Conn, _, err = websocket.DefaultDialer.Dial(gatewayURL, connectHeader)
if err != nil {
return err
}
- defer conn.Close()
-
- messageType, message, err := conn.ReadMessage()
- if err != nil {
+ var helloMsg GatewayPayload[GatewayHelloMsg]
+ if err = d.Conn.ReadJSON(&helloMsg); err != nil {
return err
}
- fmt.Printf("Type: %d, Message: %s\n", messageType, message)
+ fmt.Printf("connection response Payload: %+v\n", helloMsg)
+
+ if helloMsg.Op != GATEWAY_OP_HELLO {
+ return fmt.Errorf("expected opcode %d, got %d", GATEWAY_OP_HELLO, helloMsg.Op)
+ }
+
+ go d.startHeartbeat(ctx, helloMsg.Data.HeartbeatInterval)
+
+ if err = d.identify(); err != nil {
+ return err
+ }
return nil
}
-func (d *Discord) gateway() (string, error) {
+func (d *Discord) Disconnect() error {
+ if d.Conn == nil {
+ return errors.New("not connected")
+ }
+
+ return d.Conn.Close()
+}
+
+func (d *Discord) getGateway() (string, error) {
url := DISCORD_URl + "gateway"
req, err := http.NewRequest("GET", url, nil)
@@ -77,7 +99,7 @@ func (d *Discord) gateway() (string, error) {
switch resp.StatusCode {
case 200:
default:
- return "", errors.New("Gateway response status code: " + resp.Status)
+ return "", errors.New("gateway response status code: " + resp.Status)
}
body := struct {
@@ -92,3 +114,65 @@ func (d *Discord) gateway() (string, error) {
url = body.URL + "?v=9&encoding=json"
return url, nil
}
+
+func (d *Discord) startHeartbeat(ctx context.Context, interval uint64) {
+ // REF: heartbeat_interval * jitter
+ jitter := rand.Intn(int(interval))
+ time.Sleep(time.Duration(jitter) * time.Millisecond)
+
+ ticker := time.NewTicker(time.Duration(interval) * time.Millisecond)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case <-ticker.C:
+ fmt.Println("sending heartbeat.")
+
+ msg := GatewayPayload[any]{
+ Op: GATEWAY_OP_HEARTBEAT,
+ }
+
+ if err := d.Conn.WriteJSON(msg); err != nil {
+ log.Fatalf("error sending heartbeat: %s\n", err)
+ }
+ }
+ }
+}
+
+func (d *Discord) identify() error {
+ msg := GatewayPayload[GatewayIdentifyMsg]{
+ Op: GATEWAY_OP_IDENTIFY,
+ Data: GatewayIdentifyMsg{
+ Token: d.Token,
+ Properties: GatewayIdentifyProperties{
+ OS: "linux",
+ Browser: "jinx",
+ Device: "jinx",
+ },
+ },
+ Sequence: 0,
+ }
+
+ if err := d.Conn.WriteJSON(msg); err != nil {
+ return err
+ }
+
+ var res GatewayPayload[GatewayReadyMsg]
+ if err := d.Conn.ReadJSON(&res); err != nil {
+ return err
+ }
+
+ fmt.Printf("identify response payload: %+v\n", res)
+
+ if res.Op != GATEWAY_OP_DISPATCH {
+ return fmt.Errorf("expected opcode %d, got %d", GATEWAY_OP_DISPATCH, res.Op)
+ }
+
+ if res.EventName != "READY" {
+ return fmt.Errorf("expected event name %s, got %s", "READY", res.EventName)
+ }
+
+ return nil
+}
diff --git a/pkg/discord/entities.go b/pkg/discord/entities.go
new file mode 100644
index 0000000..85965bf
--- /dev/null
+++ b/pkg/discord/entities.go
@@ -0,0 +1,15 @@
+package discord
+
+type Snowflake string
+
+type User struct {
+ ID Snowflake `json:"id"`
+ Username string `json:"username"`
+ Discriminator string `json:"discriminator"`
+}
+
+type Guild struct {
+ ID Snowflake `json:"id"`
+ Name string `json:"name"`
+ Unavailable bool `json:"unavailable"`
+}
diff --git a/pkg/discord/payloads.go b/pkg/discord/payloads.go
new file mode 100644
index 0000000..d7ab6d4
--- /dev/null
+++ b/pkg/discord/payloads.go
@@ -0,0 +1,47 @@
+package discord
+
+type GatewayOp uint8
+
+const (
+ GATEWAY_OP_DISPATCH GatewayOp = 0
+ GATEWAY_OP_HEARTBEAT GatewayOp = 1
+ GATEWAY_OP_IDENTIFY GatewayOp = 2
+ GATEWAY_OP_PRESENCE_UPDATE GatewayOp = 3
+ GATEWAY_OP_VOICE_STATE_UPDATE GatewayOp = 4
+ GATEWAY_OP_RESUME GatewayOp = 6
+ GATEWAY_OP_RECONNECT GatewayOp = 7
+ GATEWAY_OP_REQUEST_GUILD_MEMBERS GatewayOp = 8
+ GATEWAY_OP_INVALID_SESSION GatewayOp = 9
+ GATEWAY_OP_HELLO GatewayOp = 10
+ GATEWAY_OP_HEARTBEAT_ACK GatewayOp = 11
+)
+
+type GatewayPayload[D any] struct {
+ Op GatewayOp `json:"op"`
+ Data D `json:"d,omitempty"`
+ Sequence uint64 `json:"s,omitempty"`
+ EventName string `json:"t,omitempty"`
+}
+
+type GatewayIdentifyMsg struct {
+ Token string `json:"token"`
+ Intents uint64 `json:"intents"`
+ Properties GatewayIdentifyProperties `json:"properties"`
+}
+type GatewayHelloMsg struct {
+ HeartbeatInterval uint64 `json:"heartbeat_interval"`
+}
+
+type GatewayReadyMsg struct {
+ Version uint64 `json:"v"`
+ User User `json:"user"`
+ Guilds []Guild `json:"guilds"`
+ SessionID string `json:"session_id"`
+ Shard []int `json:"shard"`
+}
+
+type GatewayIdentifyProperties struct {
+ OS string `json:"$os"`
+ Browser string `json:"$browser"`
+ Device string `json:"$device"`
+}