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 } }) }