diff --git a/crates/delta/src/main.rs b/crates/delta/src/main.rs index 4a7ab96d..5bb58261 100644 --- a/crates/delta/src/main.rs +++ b/crates/delta/src/main.rs @@ -14,6 +14,7 @@ use async_std::channel::unbounded; use revolt_quark::events::client::EventV1; use revolt_quark::rauth::{RAuth, RAuthEvent}; use revolt_quark::DatabaseInfo; +use rocket::data::ToByteUnit; #[launch] async fn rocket() -> _ { @@ -70,4 +71,9 @@ async fn rocket() -> _ { .manage(cors.clone()) .attach(revolt_quark::web::ratelimiter::RatelimitFairing) .attach(cors) + .configure(rocket::Config { + limits: rocket::data::Limits::default() + .limit("string", 5.megabytes()), + ..Default::default() + }) } diff --git a/crates/delta/src/routes/webhooks/mod.rs b/crates/delta/src/routes/webhooks/mod.rs index 4b730326..6fdc732f 100644 --- a/crates/delta/src/routes/webhooks/mod.rs +++ b/crates/delta/src/routes/webhooks/mod.rs @@ -1,5 +1,5 @@ use rocket::Route; -use rocket_okapi::okapi::openapi3::OpenApi; +use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi; mod webhook_delete; mod webhook_edit; diff --git a/crates/delta/src/routes/webhooks/webhook_execute_github.rs b/crates/delta/src/routes/webhooks/webhook_execute_github.rs index 3a55c5f3..6238d2e3 100644 --- a/crates/delta/src/routes/webhooks/webhook_execute_github.rs +++ b/crates/delta/src/routes/webhooks/webhook_execute_github.rs @@ -1,7 +1,7 @@ -use revolt_quark::{Db, Ref, Result, Error, models::{Webhook, Message, message::SendableEmbed}, types::push::MessageAuthor}; +use revolt_quark::{Db, Ref, Result, Error, models::{Message, message::SendableEmbed}, types::push::MessageAuthor}; use rocket::{Request, request::FromRequest, http::Status}; -use rocket_okapi::{request::{OpenApiFromRequest, RequestHeaderInput}, okapi::openapi3::{Parameter, ParameterValue, MediaType}, gen::OpenApiGenerator}; -use schemars::schema::{SchemaObject, StringValidation}; +use revolt_rocket_okapi::{request::{OpenApiFromRequest, RequestHeaderInput}, revolt_okapi::openapi3::{Parameter, ParameterValue, MediaType}, gen::OpenApiGenerator}; +use schemars::schema::SchemaObject; use serde::{Deserialize, Serialize}; use serde_json::Value; use ulid::Ulid; @@ -139,8 +139,8 @@ pub struct GithubRepository { archived: Option, disabled: Option, visibility: Option, - pushed_at: Option, - created_at: Option, + pushed_at: Option, + created_at: Value, updated_at: Option, permissions: Option, role_name: Option, @@ -181,9 +181,252 @@ pub struct GithubCommit { tree_id: String, url: String } +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +pub struct GithubReactions { + #[serde(rename="+1")] + plus_one: u32, + #[serde(rename="-1")] + minus_one: u32, + confused: u32, + eyes: u32, + heart: u32, + hooray: u32, + laugh: u32, + rocket: u32, + total_count: u32, + url: String +} #[derive(Serialize, Deserialize, Debug, JsonSchema)] -#[serde(rename_all = "lowercase", tag = "type")] +pub struct GithubComment { + author_association: String, + body: String, + commit_id: String, + created_at: Value, + html_url: String, + id: u32, + line: Option, + node_id: String, + path: Option, + position: Option, + reactions: Option, + updated_at: String, + url: String, + user: GithubUser +} + +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +pub struct GithubDiscussionComment { + #[serde(flatten)] + comment: GithubComment, + child_comment_count: u32, + parent_id: Option, + repository_url: String +} + +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +pub struct GithubDiscussionCategory { + id: u32, + node_id: String, + repository_id: u32, + emoji: String, + name: String, + description: String, + created_at: Value, + updated_at: String, + slug: String, + is_answerable: bool +} + +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +pub struct GithubDiscussion { + repository_url: String, + category: GithubDiscussionCategory, + answer_html_url: Option, + answer_chosen_at: Value, // ?? + answer_chosen_by: Value, // ?? + html_url: String, + id: u32, + node_id: String, + number: u32, + title: String, + user: GithubUser, + state: String, + locked: bool, + comments: u32, + created_at: Value, + updated_at: String, + author_association: String, + active_lock_reason: Option, + body: String, + reactions: GithubReactions, + timeline_url: String +} + +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "snake_case", tag = "action")] +#[allow(clippy::large_enum_variant)] +pub enum GithubDiscussionEvent { + Created { + discussion: GithubDiscussion + }, + Answered { + discussion: GithubDiscussion, + answer: GithubDiscussionComment + } +} + +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "snake_case", tag = "action")] +#[allow(clippy::large_enum_variant)] +pub enum DiscussionCommentEvent { + Created { + comment: GithubDiscussionComment, + discussion: GithubDiscussion + }, + Deleted {}, + Edited {} +} + +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +pub struct GithubMilestone { + url: String, + html_url: String, + labels_url: String, + id: u32, + node_id: String, + number: u32, + state: String, + title: String, + description: String, + creator: Option, + open_issues: u32, + closed_issues: u32, + created_at: Value, + updated_at: String, + closed_at: Option, + due_on: Option +} + +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +pub struct GithubLinkedPullRequest { + merged_at: Option, + diff_url: Option, + html_url: Option, + patch_url: Option, + url: Option, +} +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +pub struct GithubAppPermissions { + issues: Option, + checks: Option, + metadata: Option, + contents: Option, + deployments: Option +} + + +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +pub struct GithubApp { + id: u32, + slug: String, + node_is: String, + owner: Option, + name: String, + description: Option, + external_url: String, + html_url: String, + created_at: Value, + updated_at: String, + permissions: GithubAppPermissions, + events: Vec, + installations_count: Option, + client_id: Option, + client_secret: Option, + webhook_secret: Option, + pem: Option +} + +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +pub struct GithubIssue { + id: u32, + node_id: String, + url: String, + repository_url: String, + labels_url: String, + comments_url: String, + events_url: String, + html_url: String, + number: u32, + state: String, + state_reason: Option, + user: Option, + labels: Vec, + assignee: Option, + assignees: Option>, + milestone: Option, + locked: bool, + active_lock_reason: Option, + comments: u32, + pull_request: Option, + closed_at: Option, + created_at: Value, + updated_at: String, + draft: Option, + closed_by: Option, + body_html: Option, + body_text: Option, + timeline_url: Option, + repository: Option, + performed_via_github_app: Option, + author_accociation: String, + reactions: Option, + title: String +} + +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "snake_case", tag = "action")] +#[allow(clippy::large_enum_variant)] +pub enum IssueCommentEvent { + Created { + comment: GithubComment, + issue: GithubIssue + }, + Deleted {}, + Edited {} +} + +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "snake_case", tag = "action")] +#[allow(clippy::large_enum_variant)] +pub enum IssuesEvent { + Assigned {}, + Closed { + issue: GithubIssue + }, + Deleted {}, + Demilestoned {}, + Edited {}, + Labeled {}, + Locked {}, + Milestoned {}, + Opened { + issue: GithubIssue + }, + Pinned {}, + Reopened { + issue: GithubIssue + }, + Transferred {}, + Unassigned {}, + Unlabeled {}, + Unlocked {}, + Unpinned {} +} + +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "snake_case", tag = "type")] +#[allow(clippy::large_enum_variant)] pub enum BaseEvent { Star { starred_at: Option, @@ -201,7 +444,29 @@ pub enum BaseEvent { head_commit: Option, pusher: CommitAuthor, r#ref: String - } + }, + CommitComment { + comment: GithubComment + }, + Create { + master_branch: String, + pusher_type: String, + r#ref: String, + ref_type: String + }, + Delete { + pusher_type: String, + r#ref: String, + ref_type: String + }, + Discussion(GithubDiscussionEvent), + DiscussionComment(DiscussionCommentEvent), + Fork { + forkee: GithubRepository + }, + IssueComment(IssueCommentEvent), + Issues(IssuesEvent) + } #[derive(Serialize, Deserialize, Debug, JsonSchema)] @@ -238,11 +503,11 @@ impl<'r> OpenApiFromRequest<'r> for EventHeader<'r> { _gen: &mut OpenApiGenerator, _name: String, _required: bool, - ) -> rocket_okapi::Result { + ) -> revolt_rocket_okapi::Result { let mut content = schemars::Map::new(); content.insert("X-Github-Event".to_string(), MediaType { schema: Some(SchemaObject { - string: Some(Box::new(StringValidation::default())), + string: Some(Box::default()), ..Default::default() }), example: None, @@ -266,13 +531,21 @@ impl<'r> OpenApiFromRequest<'r> for EventHeader<'r> { } } -const GREEN: &'static str = "#0f890f"; -const RED: &'static str = "#e73e3e"; -const GREY: &'static str = "#202224"; -const BLUE: &'static str = "#279adc"; -const ORANGE: &'static str = "#d76a34"; -const LIGHT_ORANGE: &'static str = "#d9916d"; -const WHITE: &'static str = "#c3e1c3"; +const GREEN: &str = "#0f890f"; +const RED: &str = "#e73e3e"; +const GREY: &str = "#202224"; +const BLUE: &str = "#279adc"; +const ORANGE: &str = "#d76a34"; +const LIGHT_ORANGE: &str = "#d9916d"; +const WHITE: &str = "#c3e1c3"; + +fn shorten_text(text: &str, length: usize) -> String { + if text.len() < length { + text.to_string() + } else { + format!("{}...", &text[0..length]) + } +} /// # executes a webhook specific to github /// @@ -304,22 +577,22 @@ pub async fn req(db: &Db, target: Ref, token: String, event: EventHeader<'_>, da let sendable_embed = match event.event { BaseEvent::Star { .. } => { - if !(event.action.as_deref() == Some("created")) { return Ok(()) }; + if event.action.as_deref() != Some("created") { return Ok(()) }; SendableEmbed { title: Some(event.sender.login), - description: Some(format!("[[{}] New star added]({})", event.repository.full_name, event.repository.html_url)), + description: Some(format!("#### [[{}] New star added]({})", event.repository.full_name, event.repository.html_url)), colour: Some(GREY.to_string()), icon_url: Some(event.sender.avatar_url), url: Some(event.sender.html_url), ..Default::default() } }, - BaseEvent::Push { after, base_ref, before, commits, compare, created, deleted, forced, head_commit, pusher, r#ref } => { - let branch = r#ref.split('/').nth(2).unwrap(); + BaseEvent::Push { after, commits, compare, forced, r#ref, .. } => { + let Some(branch) = r#ref.split('/').nth(2) else { return Ok(()) }; if forced { - let description = format!("[{}] Branch {} was force-pushed to {}\n[compare changes]({})", event.repository.full_name, branch, &after[0..=7], compare); + let description = format!("#### [{}] Branch {} was force-pushed to {}\n[compare changes]({})", event.repository.full_name, branch, &after[0..=7], compare); SendableEmbed { icon_url: Some(event.sender.avatar_url), @@ -333,22 +606,151 @@ pub async fn req(db: &Db, target: Ref, token: String, event: EventHeader<'_>, da let title = format!("[[{}:{}] {} new commit]({})", event.repository.full_name, branch, commits.len(), compare); let commit_description = commits .into_iter() - .map(|commit| format!("[`{}`]({}) {} - {}", &commit.id[0..=7], commit.url, commit.message, commit.author.name)) + .map(|commit| format!("[`{}`]({}) {} - {}", &commit.id[0..=7], commit.url, shorten_text(&commit.message, 50), commit.author.name)) .collect::>() .join("\n"); SendableEmbed { title: Some(event.sender.login), - description: Some(format!("{title}\n{commit_description}")), + description: Some(format!("#### {title}\n{commit_description}")), colour: Some(BLUE.to_string()), icon_url: Some(event.sender.avatar_url), url: Some(event.sender.html_url), ..Default::default() } } - } - _ => { + }, + BaseEvent::CommitComment { comment } => { + SendableEmbed { + icon_url: Some(event.sender.avatar_url), + url: Some(event.sender.html_url), + title: Some(event.sender.login), + description: Some(format!("#### [[{}] New comment on commit `{}`]({})\n{}", event.repository.full_name, &comment.commit_id[0..=7], comment.html_url, shorten_text(&comment.body, 450))), + colour: Some(GREY.to_string()), + ..Default::default() + } + }, + BaseEvent::Ping { } => { return Ok(()) + }, + BaseEvent::Create { r#ref, ref_type, .. } => { + SendableEmbed { + icon_url: Some(event.sender.avatar_url), + url: Some(event.sender.html_url), + title: Some(event.sender.login), + description: Some(format!("#### [{}] New {} created: {}", event.repository.full_name, ref_type, r#ref)), + colour: Some(GREY.to_string()), + ..Default::default() + } + }, + BaseEvent::Delete { r#ref, ref_type, .. } => { + SendableEmbed { + icon_url: Some(event.sender.avatar_url), + url: Some(event.sender.html_url), + title: Some(event.sender.login), + description: Some(format!("#### [{}] {} deleted: {}", event.repository.full_name, ref_type, r#ref)), + colour: Some(GREY.to_string()), + ..Default::default() + } + }, + BaseEvent::Discussion(discussion_event) => { + match discussion_event { + GithubDiscussionEvent::Created { discussion } => { + SendableEmbed { + icon_url: Some(event.sender.avatar_url), + url: Some(event.sender.html_url), + title: Some(event.sender.login), + description: Some(format!("#### [{}] New discussion #{}: {}\n{}", event.repository.full_name, discussion.number, discussion.title, shorten_text(&discussion.body, 450))), + colour: Some(LIGHT_ORANGE.to_string()), + ..Default::default() + } + }, + GithubDiscussionEvent::Answered { discussion, answer } => { + SendableEmbed { + icon_url: Some(answer.comment.user.avatar_url), + url: Some(answer.comment.user.html_url), + title: Some(answer.comment.user.login), + description: Some(format!("#### [{}] discussion #{} marked answered: {}\n{}", event.repository.full_name, discussion.number, discussion.title, shorten_text(&answer.comment.body, 450))), + colour: Some(LIGHT_ORANGE.to_string()), + ..Default::default() + } + }, + } + }, + BaseEvent::DiscussionComment(comment_event) => { + match comment_event { + DiscussionCommentEvent::Created { comment, discussion } => { + SendableEmbed { + icon_url: Some(comment.comment.user.avatar_url), + url: Some(comment.comment.user.html_url), + title: Some(comment.comment.user.login), + description: Some(format!("[{}] New comment on discussion #{}: {}\n{}", event.repository.full_name, discussion.number, discussion.title, shorten_text(&comment.comment.body, 450))), + colour: Some(LIGHT_ORANGE.to_string()), + ..Default::default() + } + }, + _ => { return Ok(()) } + } + }, + BaseEvent::Fork { forkee } => { + SendableEmbed { + icon_url: Some(event.sender.avatar_url), + url: Some(event.sender.html_url), + title: Some(event.sender.login), + description: Some(format!("#### [[{}] Fork created: {}]({})", event.repository.full_name, forkee.full_name, forkee.html_url)), + colour: Some(GREY.to_string()), + ..Default::default() + } + }, + BaseEvent::Issues(issue_event) => { + match issue_event { + IssuesEvent::Closed { issue } => { + SendableEmbed { + icon_url: Some(event.sender.avatar_url), + url: Some(event.sender.html_url), + title: Some(event.sender.login), + description: Some(format!("#### [[{}] Issue Closed #{}: {}]({})", event.repository.full_name, issue.number, issue.title, issue.html_url)), + colour: Some(GREY.to_string()), + ..Default::default() + } + }, + IssuesEvent::Opened { issue } => { + SendableEmbed { + icon_url: Some(event.sender.avatar_url), + url: Some(event.sender.html_url), + title: Some(event.sender.login), + description: Some(format!("#### [[{}] Issue Opened #{}: {}]({})\n{}", event.repository.full_name, issue.number, issue.title, issue.html_url, issue.body_text.unwrap_or_default())), + colour: Some(ORANGE.to_string()), + ..Default::default() + } + }, + IssuesEvent::Reopened { issue } => { + SendableEmbed { + icon_url: Some(event.sender.avatar_url), + url: Some(event.sender.html_url), + title: Some(event.sender.login), + description: Some(format!("#### [[{}] Issue Reopened #{}: {}]({})", event.repository.full_name, issue.number, issue.html_url, issue.title)), + colour: Some(GREEN.to_string()), + ..Default::default() + } + }, + _ => { return Ok(()) } + } + }, + BaseEvent::IssueComment(comment_event) => { + match comment_event { + IssueCommentEvent::Created { comment, issue } => { + SendableEmbed { + icon_url: Some(event.sender.avatar_url), + url: Some(event.sender.html_url), + title: Some(event.sender.login), + description: Some(format!("#### [[{}] New comment on issue #{}: {}]({})\n{}", event.repository.full_name, issue.number, issue.title, issue.html_url, comment.body)), + colour: Some(LIGHT_ORANGE.to_string()), + ..Default::default() + } + }, + _ => { return Ok(()) } + } } };