package discord import ( "bytes" "context" "encoding/json" "errors" "fmt" "log" "math/rand" "net/http" "time" "github.com/gorilla/websocket" ) const DISCORD_URl = "https://discord.com/api/v9/" 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(ctx context.Context) error { gatewayURL, err := d.getGateway() if err != nil { return err } fmt.Printf("gateway: %s\n", gatewayURL) 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 } 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 } 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) getGateway() (string, error) { url := DISCORD_URl + "gateway" req, err := http.NewRequest("GET", url, nil) if err != nil { return "", err } req.Header.Set("Authorization", d.Token) req.Header.Set("Content-Type", "application/json") req.Header.Set("User-Agent", USER_AGENT) resp, err := http.DefaultClient.Do(req) if err != nil { return "", err } defer resp.Body.Close() var buf bytes.Buffer _, err = buf.ReadFrom(resp.Body) if err != nil { return "", err } switch resp.StatusCode { case 200: default: return "", errors.New("gateway response status code: " + resp.Status) } body := struct { URL string `json:"url"` }{} err = json.Unmarshal(buf.Bytes(), &body) if err != nil { return "", err } 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) 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.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 } func (d *Discord) sendMessage(channelID Snowflake, content string) error { url := DISCORD_URl + "channels/" + string(channelID) + "/messages" msg := struct { Content string `json:"content"` }{ Content: content, } raw, err := json.Marshal(msg) if err != nil { return err } req, err := http.NewRequest("POST", url, bytes.NewBuffer(raw)) if err != nil { return err } req.Header.Set("Authorization", "Bot "+d.Token) req.Header.Set("Content-Type", "application/json") req.Header.Set("User-Agent", USER_AGENT) res, err := http.DefaultClient.Do(req) if err != nil { return err } switch res.StatusCode { case 200: default: return errors.New("unexpected status code after sending message: " + res.Status) } return nil }