forked from jmug/stoatchat
inital webhook support
This commit is contained in:
16
crates/delta/src/routes/webhooks/mod.rs
Normal file
16
crates/delta/src/routes/webhooks/mod.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use rocket::Route;
|
||||
use rocket_okapi::okapi::openapi3::OpenApi;
|
||||
|
||||
mod webhook_delete;
|
||||
mod webhook_edit;
|
||||
mod webhook_execute;
|
||||
mod webhook_fetch;
|
||||
|
||||
pub fn routes() -> (Vec<Route>, OpenApi) {
|
||||
openapi_get_routes_spec![
|
||||
webhook_delete::req,
|
||||
webhook_edit::req,
|
||||
webhook_execute::req,
|
||||
webhook_fetch::req,
|
||||
]
|
||||
}
|
||||
18
crates/delta/src/routes/webhooks/webhook_delete.rs
Normal file
18
crates/delta/src/routes/webhooks/webhook_delete.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use revolt_quark::{Db, Ref, Result, EmptyResponse, Error};
|
||||
|
||||
/// # Deletes a webhook
|
||||
///
|
||||
/// deletes a webhook
|
||||
#[openapi(tag = "Webhooks")]
|
||||
#[delete("/<target>/<token>")]
|
||||
pub async fn req(db: &Db, target: Ref, token: String) -> Result<EmptyResponse> {
|
||||
let webhook = target.as_webhook(db).await?;
|
||||
|
||||
(webhook.token == token)
|
||||
.then_some(())
|
||||
.ok_or(Error::InvalidCredentials)?;
|
||||
|
||||
webhook.delete(db).await?;
|
||||
|
||||
Ok(EmptyResponse)
|
||||
}
|
||||
57
crates/delta/src/routes/webhooks/webhook_edit.rs
Normal file
57
crates/delta/src/routes/webhooks/webhook_edit.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
use revolt_quark::{Db, Ref, Result, Error, models::{webhook::{FieldsWebhook, Webhook, PartialWebhook}, File}};
|
||||
use serde::{Serialize, Deserialize};
|
||||
use validator::Validate;
|
||||
use rocket::serde::json::Json;
|
||||
|
||||
#[derive(Serialize, Deserialize, Validate, JsonSchema)]
|
||||
pub struct WebhookEditBody {
|
||||
#[validate(length(min = 1, max = 32))]
|
||||
name: Option<String>,
|
||||
|
||||
#[validate(length(min = 1, max = 128))]
|
||||
avatar: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
remove: Vec<FieldsWebhook>
|
||||
}
|
||||
|
||||
/// # Edits a webhook
|
||||
///
|
||||
/// edits a webhook
|
||||
#[openapi(tag = "Webhooks")]
|
||||
#[patch("/<target>/<token>", data="<data>")]
|
||||
pub async fn req(db: &Db, target: Ref, token: String, data: Json<WebhookEditBody>) -> Result<Json<Webhook>> {
|
||||
let data = data.into_inner();
|
||||
data.validate()
|
||||
.map_err(|error| Error::FailedValidation { error })?;
|
||||
|
||||
let mut webhook = target.as_webhook(db).await?;
|
||||
|
||||
(webhook.token == token)
|
||||
.then_some(())
|
||||
.ok_or(Error::InvalidCredentials)?;
|
||||
|
||||
if data.name.is_none()
|
||||
&& data.avatar.is_none()
|
||||
&& data.remove.is_empty()
|
||||
{
|
||||
return Ok(Json(webhook))
|
||||
};
|
||||
|
||||
let mut partial = PartialWebhook::default();
|
||||
|
||||
let WebhookEditBody { name, avatar, remove } = data;
|
||||
|
||||
if let Some(name) = name {
|
||||
partial.name = Some(name)
|
||||
}
|
||||
|
||||
if let Some(avatar) = avatar {
|
||||
let file = File::use_avatar(db, &avatar, &webhook.id).await?;
|
||||
partial.avatar = Some(file)
|
||||
}
|
||||
|
||||
webhook.update(db, partial, remove).await?;
|
||||
|
||||
Ok(Json(webhook))
|
||||
}
|
||||
138
crates/delta/src/routes/webhooks/webhook_execute.rs
Normal file
138
crates/delta/src/routes/webhooks/webhook_execute.rs
Normal file
@@ -0,0 +1,138 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use revolt_quark::{Db, Ref, Result, Error, models::{Message, message::Reply}, web::idempotency::IdempotencyKey, types::push::MessageAuthor};
|
||||
use rocket::serde::json::Json;
|
||||
use ulid::Ulid;
|
||||
use crate::routes::channels::message_send::{DataMessageSend, RE_MENTION};
|
||||
use validator::Validate;
|
||||
|
||||
/// # Executes a webhook
|
||||
///
|
||||
/// executes a webhook and sends a message
|
||||
#[openapi(tag = "Webhooks")]
|
||||
#[post("/<target>/<token>", data="<data>")]
|
||||
pub async fn req(db: &Db, target: Ref, token: String, data: Json<DataMessageSend>, mut idempotency: IdempotencyKey) -> Result<Json<Message>> {
|
||||
let data = data.into_inner();
|
||||
data.validate()
|
||||
.map_err(|error| Error::FailedValidation { error })?;
|
||||
|
||||
let webhook = target.as_webhook(db).await?;
|
||||
|
||||
(webhook.token == token)
|
||||
.then_some(())
|
||||
.ok_or(Error::InvalidCredentials)?;
|
||||
|
||||
Message::validate_sum(&data.content, &data.embeds)?;
|
||||
|
||||
idempotency.consume_nonce(data.nonce).await?;
|
||||
|
||||
let channel = Ref::from_unchecked(webhook.channel.clone()).as_channel(db).await?;
|
||||
|
||||
if (data.content.as_ref().map_or(true, |v| v.is_empty()))
|
||||
&& (data.attachments.as_ref().map_or(true, |v| v.is_empty()))
|
||||
&& (data.embeds.as_ref().map_or(true, |v| v.is_empty()))
|
||||
{
|
||||
return Err(Error::EmptyMessage);
|
||||
}
|
||||
|
||||
let message_id = Ulid::new().to_string();
|
||||
let mut message = Message {
|
||||
id: message_id.clone(),
|
||||
channel: channel.id().to_string(),
|
||||
webhook: Some(webhook.id.clone()),
|
||||
masquerade: data.masquerade,
|
||||
interactions: data.interactions.unwrap_or_default(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// 1. Parse mentions in message.
|
||||
let mut mentions = HashSet::new();
|
||||
if let Some(content) = &data.content {
|
||||
for capture in RE_MENTION.captures_iter(content) {
|
||||
if let Some(mention) = capture.get(1) {
|
||||
mentions.insert(mention.as_str().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Verify replies are valid.
|
||||
let mut replies = HashSet::new();
|
||||
if let Some(entries) = data.replies {
|
||||
if entries.len() > 5 {
|
||||
return Err(Error::TooManyReplies);
|
||||
}
|
||||
|
||||
for Reply { id, mention } in entries {
|
||||
let message = Ref::from_unchecked(id).as_message(db).await?;
|
||||
|
||||
if mention {
|
||||
mentions.insert(message.author_id().to_owned());
|
||||
}
|
||||
|
||||
replies.insert(message.id);
|
||||
}
|
||||
}
|
||||
|
||||
if !mentions.is_empty() {
|
||||
message.mentions.replace(mentions.into_iter().collect());
|
||||
}
|
||||
|
||||
if !replies.is_empty() {
|
||||
message
|
||||
.replies
|
||||
.replace(replies.into_iter().collect::<Vec<String>>());
|
||||
}
|
||||
|
||||
// 5. Process included embeds.
|
||||
let mut embeds = vec![];
|
||||
if let Some(sendable_embeds) = data.embeds {
|
||||
for sendable_embed in sendable_embeds {
|
||||
embeds.push(sendable_embed.into_embed(db, message_id.clone()).await?)
|
||||
}
|
||||
}
|
||||
|
||||
if !embeds.is_empty() {
|
||||
message.embeds.replace(embeds);
|
||||
}
|
||||
|
||||
// 6. Add attachments to message.
|
||||
let mut attachments = vec![];
|
||||
if let Some(ids) = &data.attachments {
|
||||
// ! FIXME: move this to app config
|
||||
if ids.len() > 5 {
|
||||
return Err(Error::TooManyAttachments);
|
||||
}
|
||||
|
||||
for attachment_id in ids {
|
||||
attachments.push(
|
||||
db.find_and_use_attachment(attachment_id, "attachments", "message", &message_id)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if !attachments.is_empty() {
|
||||
message.attachments.replace(attachments);
|
||||
}
|
||||
|
||||
// 7. Set content
|
||||
message.content = data.content;
|
||||
|
||||
// 8. Pass-through nonce value for clients
|
||||
message.nonce = Some(idempotency.into_key());
|
||||
|
||||
message.create(db, &channel, Some(MessageAuthor::Webhook(&webhook))).await?;
|
||||
|
||||
// Queue up a task for processing embeds
|
||||
if let Some(content) = &message.content {
|
||||
revolt_quark::tasks::process_embeds::queue(
|
||||
channel.id().to_string(),
|
||||
message.id.to_string(),
|
||||
content.clone(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(Json(message))
|
||||
|
||||
}
|
||||
17
crates/delta/src/routes/webhooks/webhook_fetch.rs
Normal file
17
crates/delta/src/routes/webhooks/webhook_fetch.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use revolt_quark::{Db, Ref, Result, Error, models::Webhook};
|
||||
use rocket::serde::json::Json;
|
||||
|
||||
/// # gets a webhook
|
||||
///
|
||||
/// gets a webhook
|
||||
#[openapi(tag = "Webhooks")]
|
||||
#[get("/<target>/<token>")]
|
||||
pub async fn req(db: &Db, target: Ref, token: String) -> Result<Json<Webhook>> {
|
||||
let webhook = target.as_webhook(db).await?;
|
||||
|
||||
(webhook.token == token)
|
||||
.then_some(())
|
||||
.ok_or(Error::InvalidCredentials)?;
|
||||
|
||||
Ok(Json(webhook))
|
||||
}
|
||||
Reference in New Issue
Block a user