From 24f175df385466e04ef21d153713d2ecf3a9733b Mon Sep 17 00:00:00 2001 From: Mel Date: Fri, 8 Apr 2022 12:54:09 +0200 Subject: Subdivide discord into packages --- pkg/discord/discord.go | 22 ++-- pkg/discord/entities.go | 23 ---- pkg/discord/entities/entities.go | 23 ++++ pkg/discord/event_handler.go | 36 ------- pkg/discord/events/event_handler.go | 29 ++++++ pkg/discord/events/events.go | 14 +++ pkg/discord/gateway.go | 201 ----------------------------------- pkg/discord/gateway/gateway.go | 202 ++++++++++++++++++++++++++++++++++++ pkg/discord/gateway/heartbeat.go | 71 +++++++++++++ pkg/discord/gateway/payloads.go | 54 ++++++++++ pkg/discord/heartbeat.go | 71 ------------- pkg/discord/payloads.go | 50 --------- pkg/discord/rest.go | 111 -------------------- pkg/discord/rest/rest.go | 112 ++++++++++++++++++++ 14 files changed, 518 insertions(+), 501 deletions(-) delete mode 100644 pkg/discord/entities.go create mode 100644 pkg/discord/entities/entities.go delete mode 100644 pkg/discord/event_handler.go create mode 100644 pkg/discord/events/event_handler.go create mode 100644 pkg/discord/events/events.go delete mode 100644 pkg/discord/gateway.go create mode 100644 pkg/discord/gateway/gateway.go create mode 100644 pkg/discord/gateway/heartbeat.go create mode 100644 pkg/discord/gateway/payloads.go delete mode 100644 pkg/discord/heartbeat.go delete mode 100644 pkg/discord/payloads.go delete mode 100644 pkg/discord/rest.go create mode 100644 pkg/discord/rest/rest.go (limited to 'pkg/discord') diff --git a/pkg/discord/discord.go b/pkg/discord/discord.go index a75e8e7..0030971 100644 --- a/pkg/discord/discord.go +++ b/pkg/discord/discord.go @@ -2,6 +2,10 @@ package discord import ( "context" + "jinx/pkg/discord/entities" + "jinx/pkg/discord/events" + "jinx/pkg/discord/gateway" + "jinx/pkg/discord/rest" "github.com/rs/zerolog" ) @@ -9,17 +13,17 @@ import ( type Discord struct { token string logger *zerolog.Logger - gateway Gateway - eventHandler EventHandler - rest REST + gateway gateway.Gateway + eventHandler events.EventHandler + rest rest.REST } func NewClient(token string, logger *zerolog.Logger) *Discord { token = "Bot " + token - eventHandler := NewEventHandler() - rest := NewREST(token) - gateway := NewGateway(logger, eventHandler) + eventHandler := events.NewEventHandler() + rest := rest.New(token) + gateway := gateway.New(logger, eventHandler) return &Discord{ token: token, @@ -42,7 +46,7 @@ func (d *Discord) Connect(ctx context.Context) error { } // We are ready! - d.eventHandler.Fire(DISCORD_EVENT_READY, nil) + d.eventHandler.Fire(events.READY, nil) return nil } @@ -51,10 +55,10 @@ func (d *Discord) Disconnect() error { return d.gateway.Close() } -func (d *Discord) SendMessage(channelID Snowflake, content string) error { +func (d *Discord) SendMessage(channelID entities.Snowflake, content string) error { return d.rest.SendMessage(channelID, content) } -func (d *Discord) On(eventName DiscordEvent, handler func(payload any)) { +func (d *Discord) On(eventName events.Event, handler func(payload any)) { d.eventHandler.Add(eventName, handler) } diff --git a/pkg/discord/entities.go b/pkg/discord/entities.go deleted file mode 100644 index 8a62fc6..0000000 --- a/pkg/discord/entities.go +++ /dev/null @@ -1,23 +0,0 @@ -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"` -} - -type Message struct { - ID Snowflake `json:"id"` - ChannelID Snowflake `json:"channel_id"` - GuildID Snowflake `json:"guild_id"` - Author User `json:"author"` - Content string `json:"content"` -} diff --git a/pkg/discord/entities/entities.go b/pkg/discord/entities/entities.go new file mode 100644 index 0000000..268a74c --- /dev/null +++ b/pkg/discord/entities/entities.go @@ -0,0 +1,23 @@ +package entities + +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"` +} + +type Message struct { + ID Snowflake `json:"id"` + ChannelID Snowflake `json:"channel_id"` + GuildID Snowflake `json:"guild_id"` + Author User `json:"author"` + Content string `json:"content"` +} diff --git a/pkg/discord/event_handler.go b/pkg/discord/event_handler.go deleted file mode 100644 index 6f3ded5..0000000 --- a/pkg/discord/event_handler.go +++ /dev/null @@ -1,36 +0,0 @@ -package discord - -type DiscordEvent uint8 - -const ( - DISCORD_EVENT_READY DiscordEvent = iota - DISCORD_EVENT_MESSAGE -) - -type EventHandler interface { - Add(event DiscordEvent, handler func(payload any)) - - Fire(event DiscordEvent, payload any) -} - -var _ EventHandler = &EventHandlerImpl{} - -type EventHandlerImpl struct { - handlers map[DiscordEvent]func(payload any) -} - -func NewEventHandler() *EventHandlerImpl { - return &EventHandlerImpl{ - handlers: make(map[DiscordEvent]func(payload any)), - } -} - -func (h *EventHandlerImpl) Add(event DiscordEvent, handler func(payload any)) { - h.handlers[event] = handler -} - -func (h *EventHandlerImpl) Fire(event DiscordEvent, payload any) { - if handler, ok := h.handlers[event]; ok { - handler(payload) - } -} diff --git a/pkg/discord/events/event_handler.go b/pkg/discord/events/event_handler.go new file mode 100644 index 0000000..5a1a118 --- /dev/null +++ b/pkg/discord/events/event_handler.go @@ -0,0 +1,29 @@ +package events + +type EventHandler interface { + Add(event Event, handler func(payload any)) + + Fire(event Event, payload any) +} + +var _ EventHandler = &EventHandlerImpl{} + +type EventHandlerImpl struct { + handlers map[Event]func(payload any) +} + +func NewEventHandler() *EventHandlerImpl { + return &EventHandlerImpl{ + handlers: make(map[Event]func(payload any)), + } +} + +func (h *EventHandlerImpl) Add(event Event, handler func(payload any)) { + h.handlers[event] = handler +} + +func (h *EventHandlerImpl) Fire(event Event, payload any) { + if handler, ok := h.handlers[event]; ok { + handler(payload) + } +} diff --git a/pkg/discord/events/events.go b/pkg/discord/events/events.go new file mode 100644 index 0000000..b1730f0 --- /dev/null +++ b/pkg/discord/events/events.go @@ -0,0 +1,14 @@ +package events + +import "jinx/pkg/discord/entities" + +type Event uint8 + +const ( + READY Event = iota + MESSAGE +) + +type Ready struct{} + +type Message entities.Message diff --git a/pkg/discord/gateway.go b/pkg/discord/gateway.go deleted file mode 100644 index 32a1e99..0000000 --- a/pkg/discord/gateway.go +++ /dev/null @@ -1,201 +0,0 @@ -package discord - -import ( - "context" - "encoding/json" - "fmt" - "jinx/pkg/libs/cancellablewebsocket" - "net/http" - - "github.com/gorilla/websocket" - "github.com/rs/zerolog" -) - -type Gateway interface { - Start(ctx context.Context, url string, token string) error - Close() error - - Heartbeat() error -} - -var _ Gateway = &GatewayImpl{} - -type GatewayImpl struct { - ctx context.Context - logger *zerolog.Logger - conn *cancellablewebsocket.CancellableWebSocket - heartbeat Heartbeat - eventHandler EventHandler - lastSeq uint64 -} - -func NewGateway(logger *zerolog.Logger, eventHandler EventHandler) *GatewayImpl { - gateway := &GatewayImpl{ - ctx: nil, - logger: logger, - conn: nil, - eventHandler: eventHandler, - heartbeat: nil, - lastSeq: 0, - } - - // Cycle dependency, is this the best way to do this? - heartbeat := NewHeartbeat(logger, gateway) - gateway.heartbeat = heartbeat - - return gateway -} - -func (g *GatewayImpl) Start(ctx context.Context, url string, token 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 - - hello, err := g.hello() - if err != nil { - return err - } - - g.heartbeat.Start(ctx, hello.HeartbeatInterval) - - if err = g.identify(token); err != nil { - return err - } - - go g.listen() - - return nil -} - -func (g *GatewayImpl) Close() error { - return g.conn.Close() -} - -func (g *GatewayImpl) Heartbeat() error { - msg := GatewayPayload[uint64]{ - Op: GATEWAY_OP_HEARTBEAT, - Data: g.lastSeq, - } - - return g.send(msg) -} - -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) onEvent(msg GatewayPayload[json.RawMessage]) error { - switch msg.Op { - case GATEWAY_OP_HEARTBEAT_ACK: - g.heartbeat.Ack() - case GATEWAY_OP_HEARTBEAT: - g.heartbeat.ForceHeartbeat() - 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 -} diff --git a/pkg/discord/gateway/gateway.go b/pkg/discord/gateway/gateway.go new file mode 100644 index 0000000..18cf708 --- /dev/null +++ b/pkg/discord/gateway/gateway.go @@ -0,0 +1,202 @@ +package gateway + +import ( + "context" + "encoding/json" + "fmt" + "jinx/pkg/discord/events" + "jinx/pkg/libs/cancellablewebsocket" + "net/http" + + "github.com/gorilla/websocket" + "github.com/rs/zerolog" +) + +type Gateway interface { + Start(ctx context.Context, url string, token string) error + Close() error + + Heartbeat() error +} + +var _ Gateway = &GatewayImpl{} + +type GatewayImpl struct { + ctx context.Context + logger *zerolog.Logger + conn *cancellablewebsocket.CancellableWebSocket + heartbeat Heartbeat + eventHandler events.EventHandler + lastSeq uint64 +} + +func New(logger *zerolog.Logger, eventHandler events.EventHandler) *GatewayImpl { + gateway := &GatewayImpl{ + ctx: nil, + logger: logger, + conn: nil, + eventHandler: eventHandler, + heartbeat: nil, + lastSeq: 0, + } + + // Cycle dependency, is this the best way to do this? + heartbeat := NewHeartbeat(logger, gateway) + gateway.heartbeat = heartbeat + + return gateway +} + +func (g *GatewayImpl) Start(ctx context.Context, url string, token 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 + + hello, err := g.hello() + if err != nil { + return err + } + + g.heartbeat.Start(ctx, hello.HeartbeatInterval) + + if err = g.identify(token); err != nil { + return err + } + + go g.listen() + + return nil +} + +func (g *GatewayImpl) Close() error { + return g.conn.Close() +} + +func (g *GatewayImpl) Heartbeat() error { + msg := Payload[uint64]{ + Op: OP_HEARTBEAT, + Data: g.lastSeq, + } + + return g.send(msg) +} + +func (g *GatewayImpl) hello() (HelloEvent, error) { + var msg Payload[HelloEvent] + if err := receive(g, &msg); err != nil { + return HelloEvent{}, err + } + + if msg.Op != OP_HELLO { + return HelloEvent{}, fmt.Errorf("expected opcode %d, got %d", OP_HELLO, msg.Op) + } + + return msg.Data, nil +} + +func (g *GatewayImpl) identify(token string) error { + msg := Payload[IdentifyCmd]{ + Op: OP_IDENTIFY, + Data: IdentifyCmd{ + Token: token, + Intents: 13991, + Properties: IdentifyProperties{ + OS: "linux", + Browser: "jinx", + Device: "jinx", + }, + }, + Sequence: 0, + } + + if err := g.send(msg); err != nil { + return err + } + + var res Payload[ReadyEvent] + if err := receive(g, &res); err != nil { + return err + } + + g.logger.Debug().Msgf("identify response payload: %+v", res) + + if res.Op != OP_DISPATCH { + return fmt.Errorf("expected opcode %d, got %d", 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 Payload[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) onEvent(msg Payload[json.RawMessage]) error { + switch msg.Op { + case OP_HEARTBEAT_ACK: + g.heartbeat.Ack() + case OP_HEARTBEAT: + g.heartbeat.ForceHeartbeat() + case 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 MessageCreateEvent + if err := json.Unmarshal(body, &payload); err != nil { + return err + } + + g.eventHandler.Fire(events.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 *Payload[D]) error { + err := g.conn.ReadJSON(&res) + if err != nil { + return err + } + + if res.Sequence != 0 { + g.lastSeq = res.Sequence + } + + return nil +} diff --git a/pkg/discord/gateway/heartbeat.go b/pkg/discord/gateway/heartbeat.go new file mode 100644 index 0000000..6cefb21 --- /dev/null +++ b/pkg/discord/gateway/heartbeat.go @@ -0,0 +1,71 @@ +package gateway + +import ( + "context" + "math/rand" + "time" + + "github.com/rs/zerolog" +) + +type Heartbeat interface { + Start(ctx context.Context, interval uint64) + + ForceHeartbeat() + Ack() +} + +var _ Heartbeat = &HeartbeatImpl{} + +type HeartbeatImpl struct { + ctx context.Context + logger *zerolog.Logger + gateway Gateway +} + +func NewHeartbeat(logger *zerolog.Logger, gateway Gateway) *HeartbeatImpl { + return &HeartbeatImpl{ + ctx: nil, + logger: logger, + gateway: gateway, + } +} + +func (h *HeartbeatImpl) Start(ctx context.Context, interval uint64) { + h.ctx = ctx + go h.heartbeatRoutine(interval) +} + +func (h *HeartbeatImpl) ForceHeartbeat() { + h.gateway.Heartbeat() +} + +func (h *HeartbeatImpl) Ack() { + // What do we do here? + h.logger.Debug().Msg("received heartbeat ack") +} + +func (h *HeartbeatImpl) heartbeatRoutine(interval uint64) { + h.logger.Debug().Msgf("beating heart every %dms", interval) + + // 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 { + h.logger.Debug().Msg("sending heartbeat") + if err := h.gateway.Heartbeat(); err != nil { + h.logger.Error().Err(err).Msg("failed to send heartbeat") + } + + select { + case <-h.ctx.Done(): + return + case <-ticker.C: + continue + } + } +} diff --git a/pkg/discord/gateway/payloads.go b/pkg/discord/gateway/payloads.go new file mode 100644 index 0000000..8da894f --- /dev/null +++ b/pkg/discord/gateway/payloads.go @@ -0,0 +1,54 @@ +package gateway + +import ( + "jinx/pkg/discord/entities" +) + +type GatewayOp uint8 + +const ( + OP_DISPATCH GatewayOp = 0 + OP_HEARTBEAT GatewayOp = 1 + OP_IDENTIFY GatewayOp = 2 + OP_PRESENCE_UPDATE GatewayOp = 3 + OP_VOICE_STATE_UPDATE GatewayOp = 4 + OP_RESUME GatewayOp = 6 + OP_RECONNECT GatewayOp = 7 + OP_REQUEST_GUILD_MEMBERS GatewayOp = 8 + OP_INVALID_SESSION GatewayOp = 9 + OP_HELLO GatewayOp = 10 + OP_HEARTBEAT_ACK GatewayOp = 11 +) + +type Payload[D any] struct { + Op GatewayOp `json:"op"` + Data D `json:"d,omitempty"` + Sequence uint64 `json:"s,omitempty"` + EventName string `json:"t,omitempty"` +} + +type IdentifyCmd struct { + Token string `json:"token"` + Intents uint64 `json:"intents"` + Properties IdentifyProperties `json:"properties"` +} + +type HelloEvent struct { + HeartbeatInterval uint64 `json:"heartbeat_interval"` +} + +type ReadyEvent struct { + Version uint64 `json:"v"` + User entities.User `json:"user"` + Guilds []entities.Guild `json:"guilds"` + SessionID string `json:"session_id"` + Shard []int `json:"shard"` +} + +type MessageCreateEvent entities.Message + +type IdentifyProperties struct { + OS string `json:"$os"` + Browser string `json:"$browser"` + Device string `json:"$device"` +} diff --git a/pkg/discord/heartbeat.go b/pkg/discord/heartbeat.go deleted file mode 100644 index 5c4a955..0000000 --- a/pkg/discord/heartbeat.go +++ /dev/null @@ -1,71 +0,0 @@ -package discord - -import ( - "context" - "math/rand" - "time" - - "github.com/rs/zerolog" -) - -type Heartbeat interface { - Start(ctx context.Context, interval uint64) - - ForceHeartbeat() - Ack() -} - -var _ Heartbeat = &HeartbeatImpl{} - -type HeartbeatImpl struct { - ctx context.Context - logger *zerolog.Logger - gateway Gateway -} - -func NewHeartbeat(logger *zerolog.Logger, gateway Gateway) *HeartbeatImpl { - return &HeartbeatImpl{ - ctx: nil, - logger: logger, - gateway: gateway, - } -} - -func (h *HeartbeatImpl) Start(ctx context.Context, interval uint64) { - h.ctx = ctx - go h.heartbeatRoutine(interval) -} - -func (h *HeartbeatImpl) ForceHeartbeat() { - h.gateway.Heartbeat() -} - -func (h *HeartbeatImpl) Ack() { - // What do we do here? - h.logger.Debug().Msg("received heartbeat ack") -} - -func (h *HeartbeatImpl) heartbeatRoutine(interval uint64) { - h.logger.Debug().Msgf("beating heart every %dms", interval) - - // 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 { - h.logger.Debug().Msg("sending heartbeat") - if err := h.gateway.Heartbeat(); err != nil { - h.logger.Error().Err(err).Msg("failed to send heartbeat") - } - - select { - case <-h.ctx.Done(): - return - case <-ticker.C: - continue - } - } -} diff --git a/pkg/discord/payloads.go b/pkg/discord/payloads.go deleted file mode 100644 index 81c6c68..0000000 --- a/pkg/discord/payloads.go +++ /dev/null @@ -1,50 +0,0 @@ -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 GatewayHelloEvent struct { - HeartbeatInterval uint64 `json:"heartbeat_interval"` -} - -type GatewayReadyEvent struct { - Version uint64 `json:"v"` - User User `json:"user"` - Guilds []Guild `json:"guilds"` - SessionID string `json:"session_id"` - Shard []int `json:"shard"` -} - -type GatewayMessageCreateEvent Message - -type GatewayIdentifyProperties struct { - OS string `json:"$os"` - Browser string `json:"$browser"` - Device string `json:"$device"` -} diff --git a/pkg/discord/rest.go b/pkg/discord/rest.go deleted file mode 100644 index 438d8d1..0000000 --- a/pkg/discord/rest.go +++ /dev/null @@ -1,111 +0,0 @@ -package discord - -import ( - "bytes" - "encoding/json" - "errors" - "net/http" - "time" -) - -const DISCORD_URl = "https://discord.com/api/v9/" -const USER_AGENT = "DiscordBot (https://jinx.rnrd.eu/, v0.0.0) Jinx" - -type REST interface { - Gateway() (string, error) - - SendMessage(channelID Snowflake, content string) error -} - -var _ REST = &RESTImpl{} - -type RESTImpl struct { - token string - client *http.Client -} - -func NewREST(token string) *RESTImpl { - return &RESTImpl{ - token: token, - client: &http.Client{ - Timeout: time.Second * 5, - }, - } -} - -func (r *RESTImpl) Gateway() (string, error) { - type gatewayResponse struct { - URL string `json:"url"` - } - - res, err := request[gatewayResponse](r, "GET", url("gateway"), nil) - if err != nil { - return "", err - } - - return res.URL + "?v=9&encoding=json", nil -} - -func (r *RESTImpl) SendMessage(channelID Snowflake, content string) error { - msg := struct { - Content string `json:"content"` - }{ - Content: content, - } - - _, err := request[any](r, "POST", url("channels/"+string(channelID)+"/messages"), msg) - if err != nil { - return err - } - - return nil -} - -func request[D any](r *RESTImpl, method string, url string, data any) (*D, error) { - var raw []byte - if data != nil { - var err error - raw, err = json.Marshal(data) - if err != nil { - return nil, err - } - } - - req, err := http.NewRequest(method, url, bytes.NewBuffer(raw)) - if err != nil { - return nil, err - } - - req.Header.Set("Authorization", r.token) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("User-Agent", USER_AGENT) - - resp, err := r.client.Do(req) - if err != nil { - return nil, err - } - - defer resp.Body.Close() - - switch resp.StatusCode { - case 200: - default: - return nil, errors.New("unexpected status code: " + resp.Status) - } - - var buf bytes.Buffer - if _, err = buf.ReadFrom(resp.Body); err != nil { - return nil, err - } - - var res D - if err = json.Unmarshal(buf.Bytes(), &res); err != nil { - return nil, err - } - - return &res, nil -} - -func url(path string) string { - return DISCORD_URl + path -} diff --git a/pkg/discord/rest/rest.go b/pkg/discord/rest/rest.go new file mode 100644 index 0000000..5f5bb78 --- /dev/null +++ b/pkg/discord/rest/rest.go @@ -0,0 +1,112 @@ +package rest + +import ( + "bytes" + "encoding/json" + "errors" + "jinx/pkg/discord/entities" + "net/http" + "time" +) + +const DISCORD_URl = "https://discord.com/api/v9/" +const USER_AGENT = "DiscordBot (https://jinx.rnrd.eu/, v0.0.0) Jinx" + +type REST interface { + Gateway() (string, error) + + SendMessage(channelID entities.Snowflake, content string) error +} + +var _ REST = &RESTImpl{} + +type RESTImpl struct { + token string + client *http.Client +} + +func New(token string) *RESTImpl { + return &RESTImpl{ + token: token, + client: &http.Client{ + Timeout: time.Second * 5, + }, + } +} + +func (r *RESTImpl) Gateway() (string, error) { + type gatewayResponse struct { + URL string `json:"url"` + } + + res, err := request[gatewayResponse](r, "GET", url("gateway"), nil) + if err != nil { + return "", err + } + + return res.URL + "?v=9&encoding=json", nil +} + +func (r *RESTImpl) SendMessage(channelID entities.Snowflake, content string) error { + msg := struct { + Content string `json:"content"` + }{ + Content: content, + } + + _, err := request[any](r, "POST", url("channels/"+string(channelID)+"/messages"), msg) + if err != nil { + return err + } + + return nil +} + +func request[D any](r *RESTImpl, method string, url string, data any) (*D, error) { + var raw []byte + if data != nil { + var err error + raw, err = json.Marshal(data) + if err != nil { + return nil, err + } + } + + req, err := http.NewRequest(method, url, bytes.NewBuffer(raw)) + if err != nil { + return nil, err + } + + req.Header.Set("Authorization", r.token) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("User-Agent", USER_AGENT) + + resp, err := r.client.Do(req) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + switch resp.StatusCode { + case 200: + default: + return nil, errors.New("unexpected status code: " + resp.Status) + } + + var buf bytes.Buffer + if _, err = buf.ReadFrom(resp.Body); err != nil { + return nil, err + } + + var res D + if err = json.Unmarshal(buf.Bytes(), &res); err != nil { + return nil, err + } + + return &res, nil +} + +func url(path string) string { + return DISCORD_URl + path +} -- cgit 1.4.1