about summary refs log tree commit diff
path: root/pkg
diff options
context:
space:
mode:
authorMel <einebeere@gmail.com>2022-04-04 00:05:12 +0200
committerMel <einebeere@gmail.com>2022-04-04 00:05:12 +0200
commit4cbe353d824d5064ee6f72f4693ee3c759b50e0a (patch)
tree9e0d47d6098e87b913a44348cf3d9811d992f190 /pkg
parentdfeef2aad6fb25eadc13c12e1caea7160bfe6c3e (diff)
downloadjinx-4cbe353d824d5064ee6f72f4693ee3c759b50e0a.tar.zst
jinx-4cbe353d824d5064ee6f72f4693ee3c759b50e0a.zip
Successful Identify
Diffstat (limited to 'pkg')
-rw-r--r--pkg/bot/bot.go11
-rw-r--r--pkg/discord/discord.go106
-rw-r--r--pkg/discord/entities.go15
-rw-r--r--pkg/discord/payloads.go47
4 files changed, 166 insertions, 13 deletions
diff --git a/pkg/bot/bot.go b/pkg/bot/bot.go
index 264e328..a0c0808 100644
--- a/pkg/bot/bot.go
+++ b/pkg/bot/bot.go
@@ -1,6 +1,7 @@
 package bot
 
 import (
+	"context"
 	"fmt"
 	"jinx/pkg/discord"
 )
@@ -12,11 +13,17 @@ func Start() error {
 
 	client := discord.NewClient(TOKEN)
 
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
 	fmt.Println("connecting..")
+	if err := client.Connect(ctx); err != nil {
+		return err
+	}
 
-	err := client.Connect()
+	fmt.Println("connected..")
 
-	if err != nil {
+	if err := client.Disconnect(); err != nil {
 		return err
 	}
 
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"`
+}