Skip to content

Commit a3bde3c

Browse files
committed
chore: add initial discord bot
1 parent 4224895 commit a3bde3c

File tree

8 files changed

+227
-2
lines changed

8 files changed

+227
-2
lines changed

api/Cargo.lock

Lines changed: 121 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,8 @@ twilight-model = "0.16.0"
1818
aws-sdk-dynamodb = "1.89.0"
1919
aws-config = "1.8.5"
2020
reqwest = { version = "0.12.23", features = ["json", "native-tls-vendored"] }
21-
chrono = "0.4.41"
21+
chrono = "0.4.41"
22+
ed25519-dalek = "2.2.0"
23+
once_cell = "1.21.3"
24+
hex = "0.4.3"
25+
http-body-util = "0.1.3"

api/src/interactions/mod.rs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
use crate::AppState;
2+
use axum::extract::{Request, State};
3+
use axum::middleware::Next;
4+
use axum::response::Response;
5+
use axum::routing::post;
6+
use axum::{middleware, Json};
7+
use ed25519_dalek::{Verifier, VerifyingKey, PUBLIC_KEY_LENGTH};
8+
use hex::FromHex;
9+
use http::StatusCode;
10+
use http_body_util::BodyExt;
11+
use once_cell::sync::Lazy;
12+
use serde_json::{json, Value};
13+
use twilight_model::application::interaction::{Interaction, InteractionType};
14+
use twilight_model::http::interaction::{InteractionResponse, InteractionResponseType};
15+
16+
static PUB_KEY: Lazy<VerifyingKey> = Lazy::new(|| {
17+
VerifyingKey::from_bytes(&<[u8; PUBLIC_KEY_LENGTH] as FromHex>::from_hex("DISCORD_PUBLIC_KEY").unwrap())
18+
.unwrap()
19+
});
20+
21+
pub fn router() -> axum::Router<AppState> {
22+
axum::Router::new()
23+
.route("/", post(post_interactions))
24+
.layer(middleware::from_fn(pubkey_middleware))
25+
}
26+
27+
pub async fn pubkey_middleware(
28+
request: Request,
29+
next: Next,
30+
) -> Result<Response, StatusCode> {
31+
let timestamp = if let Some(ts) = request.headers().get("x-signature-timestamp") {
32+
ts.to_owned()
33+
} else {
34+
return Err(StatusCode::BAD_REQUEST);
35+
};
36+
// Extract the signature to check against.
37+
let signature = if let Some(hex_sig) = request
38+
.headers()
39+
.get("x-signature-ed25519")
40+
.and_then(|v| v.to_str().ok())
41+
{
42+
hex_sig.parse().unwrap()
43+
} else {
44+
return Err(StatusCode::BAD_REQUEST);
45+
};
46+
47+
let (parts, body) = request.into_parts();
48+
let body = body
49+
.collect()
50+
.await
51+
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
52+
.to_bytes();
53+
54+
if PUB_KEY
55+
.verify(
56+
[timestamp.as_bytes(), &body].concat().as_ref(),
57+
&signature,
58+
)
59+
.is_err()
60+
{
61+
return Err(StatusCode::UNAUTHORIZED);
62+
}
63+
let new_request = Request::from_parts(parts, axum::body::Body::from(body));
64+
Ok(next.run(new_request).await)
65+
}
66+
67+
async fn post_interactions(
68+
State(_app_state): State<AppState>,
69+
Json(interaction): Json<Interaction>,
70+
) -> Result<Json<Value>, StatusCode> {
71+
match interaction.kind {
72+
InteractionType::Ping => {
73+
Ok(Json(json!(InteractionResponse {kind: InteractionResponseType::Pong, data: None})))
74+
}
75+
InteractionType::ApplicationCommand => {
76+
Err(StatusCode::NOT_IMPLEMENTED)
77+
}
78+
InteractionType::MessageComponent => {
79+
Err(StatusCode::NOT_IMPLEMENTED)
80+
}
81+
InteractionType::ApplicationCommandAutocomplete => {
82+
Err(StatusCode::NOT_IMPLEMENTED)
83+
}
84+
InteractionType::ModalSubmit => {
85+
Err(StatusCode::NOT_IMPLEMENTED)
86+
}
87+
_ => Err(StatusCode::BAD_REQUEST)
88+
}
89+
}

api/src/main.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ mod auth;
1313
mod dynamo;
1414
mod users;
1515
mod guilds;
16-
16+
mod interactions;
1717

1818
#[derive(Clone)]
1919
pub struct AppState {
@@ -73,6 +73,7 @@ async fn main() -> Result<(), Error> {
7373
.nest("/guilds", guilds::router())
7474
.layer(CorsLayer::permissive())
7575
.route_layer(middleware::from_fn(auth::auth_middleware))
76+
.nest("/interactions", interactions::router())
7677
.with_state(app_state)
7778
.route("/health", get(health_check))
7879
.route("/bot", get(get_bot_redirect));

infra/main.tf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ module "lambda" {
2929
source = "./modules/compute/lambda"
3030
deployment_env = var.deployment_env
3131
discord_bot_token = var.discord_bot_token
32+
discord_public_key = var.discord_public_key
3233
}
3334

3435
module "s3" {

infra/modules/compute/lambda/main.tf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ resource "aws_lambda_function" "lambda_function" {
7777
API_GATEWAY_BASE_PATH = "/default"
7878
DEPLOYMENT_ENV = var.deployment_env
7979
DISCORD_BOT_TOKEN = var.discord_bot_token
80+
DISCORD_PUBLIC_KEY = var.discord_public_key
8081
}
8182
}
8283
}

infra/modules/compute/lambda/variables.tf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,7 @@ variable "deployment_env" {
55
variable "discord_bot_token" {
66
type = string
77
}
8+
9+
variable "discord_public_key" {
10+
type = string
11+
}

infra/variables.tf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,7 @@ variable "root_domain_name" {
1010
variable "discord_bot_token" {
1111
type = string
1212
}
13+
14+
variable "discord_public_key" {
15+
type = string
16+
}

0 commit comments

Comments
 (0)