hmn/src/discord/rest.go

183 lines
4.5 KiB
Go

package discord
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httputil"
"git.handmade.network/hmn/hmn/src/config"
"git.handmade.network/hmn/hmn/src/logging"
"git.handmade.network/hmn/hmn/src/oops"
)
const (
BotName = "HandmadeNetwork"
BaseUrl = "https://discord.com/api/v9"
UserAgentURL = "https://handmade.network/"
UserAgentVersion = "1.0"
)
var UserAgent = fmt.Sprintf("%s (%s, %s)", BotName, UserAgentURL, UserAgentVersion)
var httpClient = &http.Client{}
func makeRequest(ctx context.Context, method string, path string, body []byte) *http.Request {
var bodyReader io.Reader
if body != nil {
bodyReader = bytes.NewBuffer(body)
}
req, err := http.NewRequestWithContext(ctx, method, fmt.Sprintf("%s%s", BaseUrl, path), bodyReader)
if err != nil {
panic(err)
}
req.Header.Add("Authorization", fmt.Sprintf("Bot %s", config.Config.Discord.BotToken))
req.Header.Add("User-Agent", UserAgent)
return req
}
type GetGatewayBotResponse struct {
URL string `json:"url"`
// We don't care about shards or session limit stuff; we will never hit those limits
}
func GetGatewayBot(ctx context.Context) (*GetGatewayBotResponse, error) {
const name = "Get Gateway Bot"
res, err := doWithRateLimiting(ctx, name, func(ctx context.Context) *http.Request {
return makeRequest(ctx, http.MethodGet, "/gateway/bot", nil)
})
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != 200 {
logErrorResponse(ctx, name, res, "")
return nil, oops.New(nil, "received error from Discord")
}
body, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
var result GetGatewayBotResponse
err = json.Unmarshal(body, &result)
if err != nil {
return nil, oops.New(err, "failed to unmarshal Discord response")
}
return &result, nil
}
type CreateMessageRequest struct {
Content string `json:"content"`
}
func CreateMessage(ctx context.Context, channelID string, payloadJSON string) (*Message, error) {
const name = "Create Message"
path := fmt.Sprintf("/channels/%s/messages", channelID)
res, err := doWithRateLimiting(ctx, name, func(ctx context.Context) *http.Request {
req := makeRequest(ctx, http.MethodPost, path, []byte(payloadJSON))
req.Header.Add("Content-Type", "application/json")
return req
})
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode >= 400 {
logErrorResponse(ctx, name, res, "")
return nil, oops.New(nil, "received error from Discord")
}
// Maybe in the future we could more nicely handle errors like "bad channel",
// but honestly what are the odds that we mess that up...
bodyBytes, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
var msg Message
err = json.Unmarshal(bodyBytes, &msg)
if err != nil {
return nil, oops.New(err, "failed to unmarshal Discord message")
}
return &msg, nil
}
func DeleteMessage(ctx context.Context, channelID string, messageID string) error {
const name = "Delete Message"
path := fmt.Sprintf("/channels/%s/messages/%s", channelID, messageID)
res, err := doWithRateLimiting(ctx, name, func(ctx context.Context) *http.Request {
return makeRequest(ctx, http.MethodDelete, path, nil)
})
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
logErrorResponse(ctx, name, res, "")
return oops.New(nil, "got unexpected status code when deleting message")
}
return nil
}
func CreateDM(ctx context.Context, recipientID string) (*Channel, error) {
const name = "Create DM"
path := "/users/@me/channels"
body := []byte(fmt.Sprintf(`{"recipient_id":"%s"}`, recipientID))
res, err := doWithRateLimiting(ctx, name, func(ctx context.Context) *http.Request {
req := makeRequest(ctx, http.MethodPost, path, body)
req.Header.Add("Content-Type", "application/json")
return req
})
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode >= 400 {
logErrorResponse(ctx, name, res, "")
return nil, oops.New(nil, "received error from Discord")
}
bodyBytes, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
var channel Channel
err = json.Unmarshal(bodyBytes, &channel)
if err != nil {
return nil, oops.New(err, "failed to unmarshal Discord channel")
}
return &channel, nil
}
func logErrorResponse(ctx context.Context, name string, res *http.Response, msg string) {
dump, err := httputil.DumpResponse(res, true)
if err != nil {
panic(err)
}
logging.ExtractLogger(ctx).Error().Str("name", name).Msg(msg)
fmt.Println(string(dump))
}