lph-11/discord-interactions/main.odin
2025-03-13 18:14:21 +13:00

167 lines
4.5 KiB
Odin

package discord_interactions
import "core:crypto/ed25519"
import "core:encoding/hex"
import "core:encoding/json"
import "core:fmt"
import "core:io"
import "core:log"
import "core:net"
import "core:os"
import REST "../discord/rest"
import http "../odin-http"
import "../odin-http/client"
Interaction :: REST.Interaction
Payload :: REST.Payload
Webhook_Payload :: REST.Webhook_Payload
ReqResPair :: struct {
req: ^http.Request,
res: ^http.Response,
}
Config :: struct {
token: string,
port: int,
interaction_endpoint: string,
}
State :: struct {
config: Config,
application: REST.Application,
command_handlers: map[string]Command_Handler,
}
Command_Handler :: #type proc(interaction: Interaction) -> Payload
Error :: union {
os.Error,
json.Error,
json.Unmarshal_Error,
client.Error,
client.Body_Error,
}
DISCORD_API :: "https://discord.com/api/v10"
state: State
load_config :: proc(filename: string) -> Error {
config_bytes, config_read_err := os.read_entire_file_or_err("config.json")
if config_read_err != nil {
return config_read_err
}
json.unmarshal(config_bytes, &state.config) or_return
headers: http.Headers
http.headers_set(&headers, "Authorization", fmt.tprint("Bot", state.config.token))
request: client.Request
client.request_init(&request, .Get)
defer client.request_destroy(&request)
request.headers = headers
res, res_err := client.request(&request, DISCORD_API + "/applications/@me")
if res_err != nil {
return res_err
}
body, was_alloc, body_err := client.response_body(&res)
if body_err != nil {
return body_err
}
#partial switch v in body {
case client.Body_Plain:
json.unmarshal(transmute([]u8)v, &state.application) or_return
}
return nil
}
serve :: proc() -> net.Network_Error {
s: http.Server
http.server_shutdown_on_interrupt(&s)
router: http.Router
http.router_init(&router)
defer http.router_destroy(&router)
http.route_post(&router, state.config.interaction_endpoint, http.handler(interactions))
routed := http.router_handler(&router)
port := state.config.port == 0 ? 8080 : state.config.port
log.infof("Listening at http://{}:{}", net.address_to_string(net.IP4_Loopback), port)
http.listen_and_serve(&s, routed, net.Endpoint{address = net.IP4_Loopback, port = port}) or_return
return nil
}
register_command :: proc(command_name: string, handler: Command_Handler) {
state.command_handlers[command_name] = handler
}
@(private)
interactions :: proc(req: ^http.Request, res: ^http.Response) {
pair := new(ReqResPair)
pair.req = req
pair.res = res
http.headers_set_close(&res.headers)
http.body(req, -1, pair, proc(userdata: rawptr, body: http.Body, err: http.Body_Error) {
reqres := cast(^ReqResPair)userdata
req := reqres.req
res := reqres.res
interaction: Interaction
json.unmarshal(transmute([]u8)body, &interaction)
signature, signature_ok := hex.decode(
transmute([]u8)http.headers_get(req.headers, "X-Signature-Ed25519"),
)
if !signature_ok {
log.error("Failed to decode signature")
return
}
timestamp := http.headers_get(req.headers, "X-Signature-Timestamp")
ed25519_public_key: ed25519.Public_Key
public_key_bytes, public_key_ok := hex.decode(transmute([]u8)state.application.verify_key)
if !public_key_ok {
log.error("Failed to decode public key")
return
}
ed25519.public_key_set_bytes(&ed25519_public_key, public_key_bytes)
if !ed25519.verify(
&ed25519_public_key,
transmute([]u8)fmt.tprintf("{}{}", timestamp, body),
signature,
) {
http.respond_with_status(res, .Unauthorized)
return
}
switch interaction.type {
case 1:
if err := http.respond_json(res, Payload{type = 1}); err != nil {
log.error("Failed to marshal payload:", err)
}
return
case 2:
handler, found := state.command_handlers[interaction.data.name]
if found {
if err := http.respond_json(res, handler(interaction)); err != nil {
log.error("Failed to marshal payload:", err)
}
} else {
log.debug("Skipping unrecognized command:", interaction.data.name)
}
return
}
})
}