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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
//! Webhook operations for DiscordUser
use std::borrow::Cow;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use crate::{context::DiscordContext, error::Result, route::Route, types::*};
/// A standard sticker pack listed under `/sticker-packs`.
///
/// Discord groups its first-party "Standard" stickers into packs that any
/// Nitro user can ship. The structure mirrors the documented payload at
/// `developers/resources/sticker.mdx#sticker-pack-object`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StickerPack {
/// Snowflake ID of the sticker pack (string in JSON).
pub id: String,
/// Stickers contained in the pack.
#[serde(default)]
pub stickers: Vec<Sticker>,
/// Display name of the pack.
pub name: String,
/// SKU snowflake associated with the pack.
pub sku_id: String,
/// Sticker shown as the pack's cover, when set.
#[serde(default)]
pub cover_sticker_id: Option<String>,
/// Marketing description for the pack.
#[serde(default)]
pub description: String,
/// Asset hash for the banner image, when present.
#[serde(default)]
pub banner_asset_id: Option<String>,
}
/// Response shape for `GET /sticker-packs` per Discord documentation.
#[derive(Debug, Clone, Deserialize)]
struct StickerPacksResponse {
sticker_packs: Vec<StickerPack>,
}
/// Response shape for `GET /applications/{app_id}/emojis` per Discord
/// documentation. Application emoji listing is always wrapped in `items`.
#[derive(Debug, Clone, Deserialize)]
pub struct ApplicationEmojiList {
/// The emojis owned by the application.
pub items: Vec<Emoji>,
}
impl<T: DiscordContext + Send + Sync> WebhookOps for T {}
/// Extension trait providing webhook operations
#[allow(async_fn_in_trait)]
pub trait WebhookOps: DiscordContext {
/// Get all webhooks in a channel.
///
/// # Errors
/// Returns [`DiscordError::Http`] on HTTP failure.
///
/// # Permissions
/// Requires MANAGE_WEBHOOKS permission.
async fn get_channel_webhooks(&self, channel_id: &ChannelId) -> Result<Vec<Webhook>> {
self.http().get(Route::GetChannelWebhooks { channel_id: channel_id.get() }).await
}
/// Get all webhooks in a guild.
///
/// # Errors
/// Returns [`DiscordError::Http`] on HTTP failure.
///
/// # Permissions
/// Requires MANAGE_WEBHOOKS permission.
async fn get_guild_webhooks(&self, guild_id: &GuildId) -> Result<Vec<Webhook>> {
self.http().get(Route::GetGuildWebhooks { guild_id: guild_id.get() }).await
}
/// Create a webhook in a channel.
///
/// # Errors
/// Returns [`DiscordError::Http`] on HTTP failure.
///
/// # Permissions
/// Requires MANAGE_WEBHOOKS permission.
async fn create_webhook(&self, channel_id: &ChannelId, req: CreateWebhookRequest) -> Result<Webhook> {
self.http().post(Route::CreateWebhook { channel_id: channel_id.get() }, req).await
}
/// Get a webhook by ID (requires authentication).
///
/// # Errors
/// Returns [`DiscordError::Http`] on HTTP failure or if the webhook is not
/// found.
async fn get_webhook(&self, webhook_id: &WebhookId) -> Result<Webhook> {
self.http().get(Route::GetWebhook { webhook_id: webhook_id.get() }).await
}
/// Get a webhook using its ID and token (no authentication required).
///
/// # Errors
/// Returns [`DiscordError::Http`] on HTTP failure or if the webhook/token
/// pair is invalid.
async fn get_webhook_with_token(&self, webhook_id: &WebhookId, token: &str) -> Result<Webhook> {
self.http().get(Route::GetWebhookWithToken { webhook_id: webhook_id.get(), token }).await
}
/// Edit a webhook's name, avatar, or channel.
///
/// # Errors
/// Returns [`DiscordError::Http`] on HTTP failure.
///
/// # Permissions
/// Requires MANAGE_WEBHOOKS permission.
async fn edit_webhook(&self, webhook_id: &WebhookId, req: EditWebhookRequest) -> Result<Webhook> {
self.http().patch(Route::EditWebhook { webhook_id: webhook_id.get() }, req).await
}
/// Edit a webhook using its token (no authentication required).
///
/// Note: changing the channel is not supported via the token-authenticated
/// endpoint.
///
/// # Errors
/// Returns [`DiscordError::Http`] on HTTP failure.
async fn edit_webhook_with_token(&self, webhook_id: &WebhookId, token: &str, req: EditWebhookRequest) -> Result<Webhook> {
self.http().patch(Route::EditWebhookWithToken { webhook_id: webhook_id.get(), token }, req).await
}
/// Delete a webhook permanently.
///
/// # Errors
/// Returns [`DiscordError::Http`] on HTTP failure.
///
/// # Permissions
/// Requires MANAGE_WEBHOOKS permission.
async fn delete_webhook(&self, webhook_id: &WebhookId) -> Result<()> {
self.http().delete(Route::DeleteWebhook { webhook_id: webhook_id.get() }).await
}
/// Delete a webhook using its token (no authentication required).
///
/// # Errors
/// Returns [`DiscordError::Http`] on HTTP failure.
async fn delete_webhook_with_token(&self, webhook_id: &WebhookId, token: &str) -> Result<()> {
self.http().delete(Route::DeleteWebhookWithToken { webhook_id: webhook_id.get(), token }).await
}
/// Execute a webhook — sends a message via the webhook.
///
/// Discord returns 204 No Content by default. To receive the posted
/// `Message` back, append `?wait=true` — not yet implemented here.
async fn execute_webhook(&self, webhook_id: &WebhookId, token: &str, req: ExecuteWebhookRequest) -> Result<()> {
self.http().post_no_response(Route::ExecuteWebhook { webhook_id: webhook_id.get(), token }, req).await
}
/// Execute a webhook with a Slack-formatted payload.
///
/// Targets `POST /webhooks/{webhook.id}/{webhook.token}/slack`.
/// `body` should match the Slack incoming-webhook payload shape that
/// Discord parses on this endpoint. When `wait` is `true`, Discord
/// returns the created [`Message`]; otherwise it returns 204 and this
/// method yields `Ok(None)`.
///
/// # Errors
/// Returns [`DiscordError::Http`] on HTTP failure.
async fn execute_webhook_slack(&self, webhook_id: &WebhookId, token: &str, wait: bool, thread_id: Option<u64>, body: Value) -> Result<Option<Message>> {
// Inline the URL: stuff `slack` plus the query string into the
// existing `ExecuteWebhook` route's `token` field, which produces
// `webhooks/{id}/{token}` — yielding the full path we need.
let suffix: String = match thread_id {
Some(tid) => format!("{}/slack?wait={}&thread_id={}", token, wait, tid),
None => format!("{}/slack?wait={}", token, wait),
};
if wait {
let msg: Message = self.http().post(Route::ExecuteWebhook { webhook_id: webhook_id.get(), token: &suffix }, body).await?;
Ok(Some(msg))
} else {
self.http().post_no_response(Route::ExecuteWebhook { webhook_id: webhook_id.get(), token: &suffix }, body).await?;
Ok(None)
}
}
/// Execute a webhook with a GitHub-formatted payload.
///
/// Targets `POST /webhooks/{webhook.id}/{webhook.token}/github`.
/// `body` should match the GitHub webhook payload Discord understands
/// (e.g. push / pull_request events). Returns the created [`Message`]
/// when `wait` is `true`, otherwise `None`.
///
/// # Errors
/// Returns [`DiscordError::Http`] on HTTP failure.
async fn execute_webhook_github(&self, webhook_id: &WebhookId, token: &str, wait: bool, thread_id: Option<u64>, body: Value) -> Result<Option<Message>> {
let suffix: String = match thread_id {
Some(tid) => format!("{}/github?wait={}&thread_id={}", token, wait, tid),
None => format!("{}/github?wait={}", token, wait),
};
if wait {
let msg: Message = self.http().post(Route::ExecuteWebhook { webhook_id: webhook_id.get(), token: &suffix }, body).await?;
Ok(Some(msg))
} else {
self.http().post_no_response(Route::ExecuteWebhook { webhook_id: webhook_id.get(), token: &suffix }, body).await?;
Ok(None)
}
}
/// Get a previously-sent webhook message.
///
/// Targets `GET /webhooks/{webhook.id}/{webhook.token}/messages/{message.id}`.
///
/// # Errors
/// Returns [`DiscordError::Http`] on HTTP failure or if the message has
/// already been deleted.
async fn get_webhook_message(&self, webhook_id: &WebhookId, token: &str, message_id: &MessageId, thread_id: Option<u64>) -> Result<Message> {
// Compose the trailing path inline: `{token}/messages/{message_id}`
// (plus optional thread_id query) lands inside `ExecuteWebhook`'s
// token slot to produce `webhooks/{id}/{token}/messages/{mid}`.
let suffix: String = match thread_id {
Some(tid) => format!("{}/messages/{}?thread_id={}", token, message_id.get(), tid),
None => format!("{}/messages/{}", token, message_id.get()),
};
self.http().get(Route::GetWebhookWithToken { webhook_id: webhook_id.get(), token: &suffix }).await
}
/// Edit a previously-sent webhook message.
///
/// Targets `PATCH /webhooks/{webhook.id}/{webhook.token}/messages/{message.id}`.
/// The `body` mirrors the create-message payload (content / embeds /
/// allowed_mentions / components / attachments / flags).
///
/// # Errors
/// Returns [`DiscordError::Http`] on HTTP failure.
async fn edit_webhook_message(&self, webhook_id: &WebhookId, token: &str, message_id: &MessageId, thread_id: Option<u64>, body: Value) -> Result<Message> {
let suffix: String = match thread_id {
Some(tid) => format!("{}/messages/{}?thread_id={}", token, message_id.get(), tid),
None => format!("{}/messages/{}", token, message_id.get()),
};
self.http().patch(Route::EditWebhookWithToken { webhook_id: webhook_id.get(), token: &suffix }, body).await
}
/// Delete a previously-sent webhook message.
///
/// Targets `DELETE /webhooks/{webhook.id}/{webhook.token}/messages/{message.id}`.
///
/// # Errors
/// Returns [`DiscordError::Http`] on HTTP failure.
async fn delete_webhook_message(&self, webhook_id: &WebhookId, token: &str, message_id: &MessageId, thread_id: Option<u64>) -> Result<()> {
let suffix: String = match thread_id {
Some(tid) => format!("{}/messages/{}?thread_id={}", token, message_id.get(), tid),
None => format!("{}/messages/{}", token, message_id.get()),
};
self.http().delete(Route::DeleteWebhookWithToken { webhook_id: webhook_id.get(), token: &suffix }).await
}
/// List all emojis owned by an application.
///
/// Targets `GET /applications/{application.id}/emojis`. Discord wraps
/// the list in an `items` envelope which this method unwraps for
/// callers.
///
/// # Errors
/// Returns [`DiscordError::Http`] on HTTP failure.
async fn list_application_emojis(&self, application_id: &ApplicationId) -> Result<Vec<Emoji>> {
let resp: ApplicationEmojiList = self.http().get(Route::ApplicationEmojis { application_id: application_id.get() }).await?;
Ok(resp.items)
}
/// Get a single application-owned emoji.
///
/// Targets `GET /applications/{application.id}/emojis/{emoji.id}`.
///
/// # Errors
/// Returns [`DiscordError::Http`] on HTTP failure or if the emoji is
/// not owned by the application.
async fn get_application_emoji(&self, application_id: &ApplicationId, emoji_id: &EmojiId) -> Result<Emoji> {
self.http().get(Route::ApplicationEmoji { application_id: application_id.get(), emoji_id: emoji_id.get() }).await
}
/// Upload a new emoji owned by the application.
///
/// Targets `POST /applications/{application.id}/emojis`. The `image`
/// argument must be a [data URI](https://discord.com/developers/docs/reference#image-data)
/// such as `data:image/png;base64,...`.
///
/// # Errors
/// Returns [`DiscordError::Http`] on HTTP failure.
async fn create_application_emoji(&self, application_id: &ApplicationId, name: &str, image: &str) -> Result<Emoji> {
let body = json!({ "name": name, "image": image });
self.http().post(Route::ApplicationEmojis { application_id: application_id.get() }, body).await
}
/// Rename an application-owned emoji.
///
/// Targets `PATCH /applications/{application.id}/emojis/{emoji.id}`.
/// Only the `name` field may be modified at the application scope.
///
/// # Errors
/// Returns [`DiscordError::Http`] on HTTP failure.
async fn modify_application_emoji(&self, application_id: &ApplicationId, emoji_id: &EmojiId, name: &str) -> Result<Emoji> {
let body = json!({ "name": name });
self.http().patch(Route::ApplicationEmoji { application_id: application_id.get(), emoji_id: emoji_id.get() }, body).await
}
/// Delete an application-owned emoji.
///
/// Targets `DELETE /applications/{application.id}/emojis/{emoji.id}`.
/// Discord returns 204 No Content on success.
///
/// # Errors
/// Returns [`DiscordError::Http`] on HTTP failure.
async fn delete_application_emoji(&self, application_id: &ApplicationId, emoji_id: &EmojiId) -> Result<()> {
self.http().delete(Route::ApplicationEmoji { application_id: application_id.get(), emoji_id: emoji_id.get() }).await
}
/// Get a single sticker by ID, regardless of pack or guild.
///
/// Targets `GET /stickers/{sticker.id}`.
///
/// # Errors
/// Returns [`DiscordError::Http`] on HTTP failure or if the sticker
/// does not exist.
async fn get_sticker(&self, sticker_id: &StickerId) -> Result<Sticker> {
self.http().get(Route::Sticker { sticker_id: sticker_id.get() }).await
}
/// List the standard sticker packs available to all users.
///
/// Targets `GET /sticker-packs`. Discord wraps the response in
/// `{ "sticker_packs": [...] }`; this method unwraps and returns the
/// inner list.
///
/// # Errors
/// Returns [`DiscordError::Http`] on HTTP failure.
async fn list_sticker_packs(&self) -> Result<Vec<StickerPack>> {
let resp: StickerPacksResponse = self.http().get(Route::StickerPacks).await?;
Ok(resp.sticker_packs)
}
/// Get a single standard sticker pack by ID.
///
/// Targets `GET /sticker-packs/{pack.id}`.
///
/// # Errors
/// Returns [`DiscordError::Http`] on HTTP failure or if the pack does
/// not exist.
async fn get_sticker_pack(&self, pack_id: u64) -> Result<StickerPack> {
self.http().get(Route::StickerPack { pack_id }).await
}
/// Get an invite by its code.
///
/// Targets `GET /invites/{code}`. Optional flags expand the response:
/// - `with_counts` adds approximate member / presence counts.
/// - `with_expiration` includes the `expires_at` timestamp.
/// - `guild_scheduled_event_id` resolves a specific scheduled event
/// the invite is anchored to.
///
/// # Errors
/// Returns [`DiscordError::Http`] on HTTP failure or if the invite is
/// invalid / expired.
async fn get_invite(&self, code: &str, with_counts: Option<bool>, with_expiration: Option<bool>, guild_scheduled_event_id: Option<u64>) -> Result<crate::types::guild::Invite> {
self.http()
.get(Route::Invite {
code: Cow::Borrowed(code),
with_counts,
with_expiration,
guild_scheduled_event_id,
})
.await
}
}