package discord import ( "context" "errors" "fmt" "log" "math/rand" "net/http" "time" "github.com/gorilla/websocket" ) type Discord struct { token string conn *websocket.Conn rest REST } func NewClient(token string) *Discord { return &Discord{ token: token, conn: nil, rest: NewREST(token), } } func (d *Discord) Connect(ctx context.Context) error { gatewayURL, err := d.rest.Gateway() if err != nil { return err } connectHeader := http.Header{} d.conn, _, err = websocket.DefaultDialer.Dial(gatewayURL, connectHeader) if err != nil { return err } var helloMsg GatewayPayload[GatewayHelloMsg] if err = d.conn.ReadJSON(&helloMsg); err != nil { return err } if helloMsg.Op != GATEWAY_OP_HELLO { return fmt.Errorf("expected opcode %d, got %d", GATEWAY_OP_HELLO, helloMsg.Op) } go d.heartbeat(ctx, helloMsg.Data.HeartbeatInterval) if err = d.identify(); err != nil { return err } go d.listen(ctx) return nil } func (d *Discord) Disconnect() error { if d.conn == nil { return errors.New("not connected") } return d.conn.Close() } func (d *Discord) heartbeat(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) listen(ctx context.Context) { for { var msg GatewayPayload[any] if err := d.conn.ReadJSON(&msg); err != nil { log.Fatalf("error reading message: %s\n", err) } select { case <-ctx.Done(): return default: fmt.Printf("received message: %+v\n", msg) if msg.EventName == "MESSAGE_CREATE" { event := msg.Data.(map[string]interface{}) if event["content"] == "ping" { fmt.Println("got ping, sending pong...") if err := d.rest.SendMessage(Snowflake(event["channel_id"].(string)), "pong"); err != nil { log.Fatalf("error sending message: %s\n", err) } } } } } } func (d *Discord) identify() error { msg := GatewayPayload[GatewayIdentifyMsg]{ Op: GATEWAY_OP_IDENTIFY, Data: GatewayIdentifyMsg{ Token: d.token, Intents: 13991, 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 }