diff options
Diffstat (limited to 'pkg/discord/gateway.go')
| -rw-r--r-- | pkg/discord/gateway.go | 174 |
1 files changed, 174 insertions, 0 deletions
diff --git a/pkg/discord/gateway.go b/pkg/discord/gateway.go new file mode 100644 index 0000000..cfabdc7 --- /dev/null +++ b/pkg/discord/gateway.go @@ -0,0 +1,174 @@ +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 +} + +func NewGateway(logger *zerolog.Logger, eventHandler EventHandler) *GatewayImpl { + return &GatewayImpl{ + ctx: nil, + logger: logger, + conn: nil, + eventHandler: eventHandler, + } +} + +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 := g.receive(&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 := g.receive(&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 := g.receive(&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) receive(res interface{}) error { + return g.conn.ReadJSON(res) +} + +func (g *GatewayImpl) send(payload interface{}) error { + return g.conn.WriteJSON(payload) +} |
