forked from twilight-rs/twilight
-
Notifications
You must be signed in to change notification settings - Fork 0
/
model-webhook-slash.rs
186 lines (164 loc) · 6.09 KB
/
model-webhook-slash.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
use ed25519_dalek::{Verifier, VerifyingKey, PUBLIC_KEY_LENGTH};
use hex::FromHex;
use hyper::{
header::CONTENT_TYPE,
http::StatusCode,
service::{make_service_fn, service_fn},
Body, Method, Request, Response, Server,
};
use once_cell::sync::Lazy;
use std::future::Future;
use twilight_model::{
application::interaction::{
application_command::CommandData, Interaction, InteractionData, InteractionType,
},
http::interaction::{InteractionResponse, InteractionResponseData, InteractionResponseType},
};
/// Public key given from Discord.
static PUB_KEY: Lazy<VerifyingKey> = Lazy::new(|| {
VerifyingKey::from_bytes(&<[u8; PUBLIC_KEY_LENGTH] as FromHex>::from_hex("PUBLIC_KEY").unwrap())
.unwrap()
});
/// Main request handler which will handle checking the signature.
///
/// Responses are made by giving a function that takes a Interaction and returns
/// a InteractionResponse or a error.
async fn interaction_handler<F>(
req: Request<Body>,
f: impl Fn(Box<CommandData>) -> F,
) -> anyhow::Result<Response<Body>>
where
F: Future<Output = anyhow::Result<InteractionResponse>>,
{
// Check that the method used is a POST, all other methods are not allowed.
if req.method() != Method::POST {
return Ok(Response::builder()
.status(StatusCode::METHOD_NOT_ALLOWED)
.body(Body::empty())?);
}
// Check if the path the request is sent to is the root of the domain.
//
// This filter is for the purposes of this example. The user may filter by
// any path they choose.
if req.uri().path() != "/" {
return Ok(Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Body::empty())?);
}
// Extract the timestamp header for use later to check the signature.
let timestamp = if let Some(ts) = req.headers().get("x-signature-timestamp") {
ts.to_owned()
} else {
return Ok(Response::builder()
.status(StatusCode::BAD_REQUEST)
.body(Body::empty())?);
};
// Extract the signature to check against.
let signature = if let Some(hex_sig) = req
.headers()
.get("x-signature-ed25519")
.and_then(|v| v.to_str().ok())
{
hex_sig.parse().unwrap()
} else {
return Ok(Response::builder()
.status(StatusCode::BAD_REQUEST)
.body(Body::empty())?);
};
// Fetch the whole body of the request as that is needed to check the
// signature against.
let whole_body = hyper::body::to_bytes(req).await?;
// Check if the signature matches and else return a error response.
if PUB_KEY
.verify(
[timestamp.as_bytes(), &whole_body].concat().as_ref(),
&signature,
)
.is_err()
{
return Ok(Response::builder()
.status(StatusCode::UNAUTHORIZED)
.body(Body::empty())?);
}
// Deserialize the body into a interaction.
let interaction = serde_json::from_slice::<Interaction>(&whole_body)?;
match interaction.kind {
// Return a Pong if a Ping is received.
InteractionType::Ping => {
let response = InteractionResponse {
kind: InteractionResponseType::Pong,
data: None,
};
let json = serde_json::to_vec(&response)?;
Ok(Response::builder()
.status(StatusCode::OK)
.header(CONTENT_TYPE, "application/json")
.body(json.into())?)
}
// Respond to a slash command.
InteractionType::ApplicationCommand => {
// Run the handler to gain a response.
let data = match interaction.data {
Some(InteractionData::ApplicationCommand(data)) => Some(data),
_ => None,
}
.expect("`InteractionType::ApplicationCommand` has data");
let response = f(data).await?;
// Serialize the response and return it back to Discord.
let json = serde_json::to_vec(&response)?;
Ok(Response::builder()
.header(CONTENT_TYPE, "application/json")
.status(StatusCode::OK)
.body(json.into())?)
}
// Unhandled interaction types.
_ => Ok(Response::builder()
.status(StatusCode::BAD_REQUEST)
.body(Body::empty())?),
}
}
/// Interaction handler that matches on the name of the interaction that
/// have been dispatched from Discord.
async fn handler(data: Box<CommandData>) -> anyhow::Result<InteractionResponse> {
match data.name.as_ref() {
"vroom" => vroom(data).await,
"debug" => debug(data).await,
_ => debug(data).await,
}
}
/// Example of a handler that returns the formatted version of the interaction.
async fn debug(data: Box<CommandData>) -> anyhow::Result<InteractionResponse> {
Ok(InteractionResponse {
kind: InteractionResponseType::ChannelMessageWithSource,
data: Some(InteractionResponseData {
content: Some(format!("```rust\n{data:?}\n```")),
..Default::default()
}),
})
}
/// Example of interaction that responds with a message saying "Vroom vroom".
async fn vroom(_: Box<CommandData>) -> anyhow::Result<InteractionResponse> {
Ok(InteractionResponse {
kind: InteractionResponseType::ChannelMessageWithSource,
data: Some(InteractionResponseData {
content: Some("Vroom vroom".to_owned()),
..Default::default()
}),
})
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Initialize the tracing subscriber.
tracing_subscriber::fmt::init();
// Local address to bind the service to.
let addr = "127.0.0.1:3030".parse().unwrap();
// Make the interaction handler into a service function.
let interaction_service = make_service_fn(|_| async {
Ok::<_, anyhow::Error>(service_fn(|req| interaction_handler(req, handler)))
});
// Construct the server and serve the interaction service.
let server = Server::bind(&addr).serve(interaction_service);
// Start the server.
server.await?;
Ok(())
}