package discord import ( "context" "encoding/json" "errors" "fmt" "jinx/pkg/libs/cancellablewebsocket" "net/http" "github.com/gorilla/websocket" "github.com/rs/zerolog" ) type Gateway interface { Start(ctx context.Context, url string) error Close() error Hello() (GatewayHelloEvent, error) Identify(token string) error Listen() Heartbeat() error } var _ Gateway = &GatewayImpl{} type GatewayImpl struct { ctx context.Context logger *zerolog.Logger conn *cancellablewebsocket.CancellableWebSocket eventHandler EventHandler lastSeq uint64 } func NewGateway(logger *zerolog.Logger, eventHandler EventHandler) *GatewayImpl { return &GatewayImpl{ ctx: nil, logger: logger, conn: nil, eventHandler: eventHandler, lastSeq: 0, } } func (g *GatewayImpl) Start(ctx context.Context, url string) error { connectHeader := http.Header{} conn, err := cancellablewebsocket.Dial(websocket.DefaultDialer, ctx, url, connectHeader) if err != nil { return err } g.conn = conn g.ctx = ctx return nil } func (g *GatewayImpl) Close() error { return g.conn.Close() } func (g *GatewayImpl) Hello() (GatewayHelloEvent, error) { var msg GatewayPayload[GatewayHelloEvent] if err := receive(g, &msg); err != nil { return GatewayHelloEvent{}, err } if msg.Op != GATEWAY_OP_HELLO { return GatewayHelloEvent{}, fmt.Errorf("expected opcode %d, got %d", GATEWAY_OP_HELLO, msg.Op) } return msg.Data, nil } func (g *GatewayImpl) Identify(token string) error { msg := GatewayPayload[GatewayIdentifyMsg]{ Op: GATEWAY_OP_IDENTIFY, Data: GatewayIdentifyMsg{ Token: token, Intents: 13991, Properties: GatewayIdentifyProperties{ OS: "linux", Browser: "jinx", Device: "jinx", }, }, Sequence: 0, } if err := g.send(msg); err != nil { return err } var res GatewayPayload[GatewayReadyEvent] if err := receive(g, &res); err != nil { return err } g.logger.Debug().Msgf("identify response payload: %+v", 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 (g *GatewayImpl) Listen() { for { var msg GatewayPayload[json.RawMessage] if err := receive(g, &msg); err != nil { g.logger.Error().Err(err).Msgf("error reading message") continue } select { case <-g.ctx.Done(): return default: g.onEvent(msg) } } } func (g *GatewayImpl) Heartbeat() error { msg := GatewayPayload[any]{ Op: GATEWAY_OP_HEARTBEAT, } return g.send(msg) } func (g *GatewayImpl) onEvent(msg GatewayPayload[json.RawMessage]) error { switch msg.Op { case GATEWAY_OP_HEARTBEAT_ACK: g.logger.Debug().Msg("received heartbeat ack.") case GATEWAY_OP_HEARTBEAT: return errors.New("on demand heartbeat not implemented") case GATEWAY_OP_DISPATCH: return g.onDispatch(msg.EventName, msg.Data) default: g.logger.Warn().Msgf("received unknown opcode: %d", msg.Op) } return nil } func (g *GatewayImpl) onDispatch(eventName string, body json.RawMessage) error { switch eventName { case "MESSAGE_CREATE": var payload GatewayMessageCreateEvent if err := json.Unmarshal(body, &payload); err != nil { return err } g.eventHandler.Fire(DISCORD_EVENT_MESSAGE, payload) default: g.logger.Warn().Msgf("received unknown event: %s", eventName) } return nil } func (g *GatewayImpl) send(payload any) error { return g.conn.WriteJSON(payload) } func receive[D any](g *GatewayImpl, res *GatewayPayload[D]) error { err := g.conn.ReadJSON(&res) if err != nil { return err } if res.Sequence != 0 { g.lastSeq = res.Sequence } return nil }