use revolt_quark::{Db, Ref, Result, Error, models::{Webhook, 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 serde::{Deserialize, Serialize}; use serde_json::Value; use ulid::Ulid; #[derive(Serialize, Deserialize, Debug, JsonSchema)] pub struct GithubUser { name: Option, email: Option, login: String, id: u32, node_id: String, avatar_url: String, gravatar_id: String, url: String, html_url: String, followers_url: String, following_url: String, gists_url: String, starred_url: String, subscriptions_url: String, organizations_url: String, repos_url: String, events_url: String, received_events_url: String, } #[derive(Serialize, Deserialize, Debug, JsonSchema)] pub struct GithubRepositorySecurityAndAnalysisStatus { status: String } #[derive(Serialize, Deserialize, Debug, JsonSchema)] pub struct GithubRepositorySecurityAndAnalysis { advanced_security: GithubRepositorySecurityAndAnalysisStatus, secret_scanning: GithubRepositorySecurityAndAnalysisStatus, secret_scanning_push_protection: GithubRepositorySecurityAndAnalysisStatus } #[derive(Serialize, Deserialize, Debug, JsonSchema)] pub struct GithubRepositoryLicense { key: Option, name: Option, spdx_id: Option, url: Option, node_id: Option, } #[derive(Serialize, Deserialize, Debug, JsonSchema)] pub struct GithubRepositoryCodeOfConduct { key: String, name: String, url: String, body: Option, html_url: Option } #[derive(Serialize, Deserialize, Debug, JsonSchema)] pub struct GithubRepositoryPermissions { admin: Option, maintain: Option, push: Option, triage: Option, pull: Option } #[derive(Serialize, Deserialize, Debug, JsonSchema)] pub struct GithubRepository { id: u32, node_id: String, name: String, full_name: String, owner: GithubUser, private: bool, html_url: String, description: Option, fork: bool, url: String, archive_url: String, assignees_url: String, blobs_url: String, branches_url: String, collaborators_url: String, comments_url: String, commits_url: String, compare_url: String, contents_url: String, contributors_url: String, deployments_url: String, downloads_url: String, events_url: String, forks_url: String, git_commits_url: String, git_refs_url: String, git_tags_url: String, git_url: Option, issue_comment_url: String, issue_events_url: String, issues_url: String, keys_url: String, labels_url: String, languages_url: String, merges_url: String, milestones_url: String, notifications_url: String, pulls_url: String, releases_url: String, ssh_url: String, stargazers_url: String, statuses_url: String, subscribers_url: String, subscription_url: String, tags_url: String, teams_url: String, trees_url: String, clone_url: Option, mirror_url: Option, hooks_url: String, svn_url: Option, homepage: Option, language: Option, forks_count: Option, stargazers_count: Option, watchers_count: Option, size: Option, default_branch: Option, open_issues_count: Option, is_template: Option, topics: Option>, has_issues: Option, has_projects: Option, has_wiki: Option, has_pages: Option, has_downloads: Option, has_discussions: Option, archived: Option, disabled: Option, visibility: Option, pushed_at: Option, created_at: Option, updated_at: Option, permissions: Option, role_name: Option, temp_clone_token: Option, delete_branch_on_merge: Option, subscribers_count: Option, network_count: Option, code_of_conduct: Option, license: Option, forks: Option, open_issues: Option, watchers: Option, allow_forking: Option, web_commit_signoff_required: Option, security_and_analysis: Option } #[derive(Serialize, Deserialize, Debug, JsonSchema)] pub struct CommitAuthor { date: Option, email: Option, name: String, username: Option } #[derive(Serialize, Deserialize, Debug, JsonSchema)] pub struct GithubCommit { added: Option>, author: CommitAuthor, committer: CommitAuthor, distinct: bool, id: String, message: String, modified: Option>, removed: Option>, timestamp: String, tree_id: String, url: String } #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "lowercase", tag = "type")] pub enum BaseEvent { Star { starred_at: Option, }, Ping {}, Push { after: String, base_ref: Option, before: String, commits: Vec, compare: String, created: bool, deleted: bool, forced: bool, head_commit: Option, pusher: CommitAuthor, r#ref: String } } #[derive(Serialize, Deserialize, Debug, JsonSchema)] pub struct Event { action: Option, sender: GithubUser, repository: GithubRepository, organization: Option, installation: Option, #[serde(flatten)] event: BaseEvent } #[derive(Debug, JsonSchema)] pub struct EventHeader<'r>(pub &'r str); #[async_trait] impl<'r> FromRequest<'r> for EventHeader<'r> { type Error = Error; async fn from_request(request: &'r Request<'_>) -> rocket::request::Outcome { let headers = request.headers(); let Some(event) = headers.get_one("X-GitHub-Event") else { return rocket::request::Outcome::Failure((Status::BadRequest, Error::InvalidOperation)) }; rocket::request::Outcome::Success(Self(event)) } } impl<'r> OpenApiFromRequest<'r> for EventHeader<'r> { fn from_request_input( _gen: &mut OpenApiGenerator, _name: String, _required: bool, ) -> 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())), ..Default::default() }), example: None, examples: None, encoding: schemars::Map::new(), extensions: schemars::Map::new(), }); Ok(RequestHeaderInput::Parameter( Parameter { name: "X-Github-Event".to_string(), location: "header".to_string(), required: true, description: Some("The name of the github event".to_string()), deprecated: false, allow_empty_value: false, value: ParameterValue::Content { content }, extensions: schemars::Map::new() } )) } } 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"; /// # executes a webhook specific to github /// /// executes a webhook specific to github and sends a message containg the relavent info about the event #[openapi(tag = "Webhooks")] #[post("///github", data="")] pub async fn req(db: &Db, target: Ref, token: String, event: EventHeader<'_>, data: String) -> Result<()> { let webhook = target.as_webhook(db).await?; (webhook.token == token) .then_some(()) .ok_or(Error::InvalidCredentials)?; let channel = db.fetch_channel(&webhook.channel).await?; let body = format!(r#"{}, "type": "{}"}}"#, &data[0..data.len() - 1], &event.0); log::info!("{body}"); let event = match serde_json::from_str::(&body) { Ok(event) => event, Err(err) => { log::error!("{err:?}"); return Err(Error::InvalidOperation); } }; log::info!("{event:?}"); let sendable_embed = match event.event { BaseEvent::Star { .. } => { 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)), 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(); if forced { 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), title: Some(event.sender.login), description: Some(description), url: Some(event.sender.html_url), colour: Some(RED.to_string()), ..Default::default() } } else { 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)) .collect::>() .join("\n"); SendableEmbed { title: Some(event.sender.login), 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() } } } _ => { return Ok(()) } }; let message_id = Ulid::new().to_string(); let embed = sendable_embed.into_embed(db, message_id.clone()).await?; let mut message = Message { id: message_id, channel: webhook.channel.clone(), embeds: Some(vec![embed]), webhook: Some(webhook.id.clone()), ..Default::default() }; message.create(db, &channel, Some(MessageAuthor::Webhook(&webhook))).await }