167 lines
4.5 KiB
Odin
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
|
|
}
|
|
})
|
|
}
|