Files
stoatchat/crates/delta/src/routes/webhooks/webhook_execute_github.rs
2026-03-27 17:29:37 -07:00

1108 lines
33 KiB
Rust

use revolt_database::{util::reference::Reference, Database, Message, AMQP};
use revolt_models::v0::{MessageAuthor, SendableEmbed, Webhook};
use revolt_result::{create_error, Error, Result};
use revolt_rocket_okapi::{
gen::OpenApiGenerator,
request::{OpenApiFromRequest, RequestHeaderInput},
revolt_okapi::openapi3::{MediaType, Parameter, ParameterValue},
};
use rocket::{http::Status, request::FromRequest, Request, State};
use schemars::schema::SchemaObject;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use ulid::Ulid;
use validator::Validate;
#[derive(Serialize, Deserialize, Debug, JsonSchema)]
pub struct GithubUser {
name: Option<String>,
email: Option<String>,
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<String>,
name: Option<String>,
spdx_id: Option<String>,
url: Option<String>,
node_id: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, JsonSchema)]
pub struct GithubRepositoryCodeOfConduct {
key: String,
name: String,
url: String,
body: Option<String>,
html_url: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, JsonSchema)]
pub struct GithubRepositoryPermissions {
admin: Option<bool>,
maintain: Option<bool>,
push: Option<bool>,
triage: Option<bool>,
pull: Option<bool>,
}
#[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<String>,
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<String>,
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<String>,
mirror_url: Option<String>,
hooks_url: String,
svn_url: Option<String>,
homepage: Option<String>,
language: Option<String>,
forks_count: Option<u32>,
stargazers_count: Option<u32>,
watchers_count: Option<u32>,
size: Option<u64>,
default_branch: Option<String>,
open_issues_count: Option<u32>,
is_template: Option<bool>,
topics: Option<Vec<String>>,
has_issues: Option<bool>,
has_projects: Option<bool>,
has_wiki: Option<bool>,
has_pages: Option<bool>,
has_downloads: Option<bool>,
has_discussions: Option<bool>,
archived: Option<bool>,
disabled: Option<bool>,
visibility: Option<String>,
pushed_at: Option<Value>,
created_at: Value,
updated_at: Option<Value>,
permissions: Option<GithubRepositoryPermissions>,
role_name: Option<String>,
temp_clone_token: Option<String>,
delete_branch_on_merge: Option<bool>,
subscribers_count: Option<u32>,
network_count: Option<u32>,
code_of_conduct: Option<GithubRepositoryCodeOfConduct>,
license: Option<GithubRepositoryLicense>,
forks: Option<u32>,
open_issues: Option<u32>,
watchers: Option<u32>,
allow_forking: Option<bool>,
web_commit_signoff_required: Option<bool>,
security_and_analysis: Option<GithubRepositorySecurityAndAnalysis>,
}
#[derive(Serialize, Deserialize, Debug, JsonSchema)]
pub struct CommitAuthor {
date: Option<String>,
email: Option<String>,
name: String,
username: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, JsonSchema)]
pub struct GithubCommit {
added: Option<Vec<String>>,
author: CommitAuthor,
committer: CommitAuthor,
distinct: bool,
id: String,
message: String,
modified: Option<Vec<String>>,
removed: Option<Vec<String>>,
timestamp: String,
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)]
pub struct GithubComment {
author_association: String,
body: String,
commit_id: Option<String>,
created_at: Value,
html_url: String,
id: u32,
line: Option<u32>,
node_id: String,
path: Option<String>,
position: Option<u32>,
reactions: Option<GithubReactions>,
updated_at: Value,
user: GithubUser,
}
#[derive(Serialize, Deserialize, Debug, JsonSchema)]
pub struct GithubDiscussionComment {
#[serde(flatten)]
comment: GithubComment,
child_comment_count: u32,
parent_id: Option<u32>,
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: Value,
slug: String,
is_answerable: bool,
}
#[derive(Serialize, Deserialize, Debug, JsonSchema)]
pub struct GithubDiscussion {
repository_url: String,
category: GithubDiscussionCategory,
answer_html_url: Option<String>,
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: Value,
author_association: String,
active_lock_reason: Option<String>,
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<GithubUser>,
open_issues: u32,
closed_issues: u32,
created_at: Value,
updated_at: Value,
closed_at: Option<Value>,
due_on: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, JsonSchema)]
pub struct GithubLinkedPullRequest {
merged_at: Option<Value>,
diff_url: Option<String>,
html_url: Option<String>,
patch_url: Option<String>,
url: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, JsonSchema)]
pub struct GithubAppPermissions {
issues: Option<String>,
checks: Option<String>,
metadata: Option<String>,
contents: Option<String>,
deployments: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, JsonSchema)]
pub struct GithubApp {
id: u32,
slug: String,
node_is: String,
owner: Option<GithubUser>,
name: String,
description: Option<String>,
external_url: String,
html_url: String,
created_at: Value,
updated_at: Value,
permissions: GithubAppPermissions,
events: Vec<String>,
installations_count: Option<u32>,
client_id: Option<String>,
client_secret: Option<String>,
webhook_secret: Option<String>,
pem: Option<String>,
}
#[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<String>,
user: Option<GithubUser>,
labels: Vec<String>,
assignee: Option<GithubUser>,
assignees: Vec<GithubUser>,
milestone: Option<GithubMilestone>,
locked: bool,
active_lock_reason: Option<String>,
comments: u32,
pull_request: Option<GithubLinkedPullRequest>,
closed_at: Option<Value>,
created_at: Value,
updated_at: Value,
draft: Option<bool>,
closed_by: Option<GithubUser>,
body: Option<String>,
timeline_url: Option<String>,
repository: Option<GithubRepository>,
performed_via_github_app: Option<GithubApp>,
author_association: String,
reactions: Option<GithubReactions>,
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)]
pub struct StarEvent {
starred_at: Option<Value>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct PushEvent {
after: String,
base_ref: Option<String>,
before: String,
commits: Vec<GithubCommit>,
compare: String,
created: bool,
deleted: bool,
forced: bool,
head_commit: Option<GithubCommit>,
pusher: CommitAuthor,
r#ref: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct CommitCommentEvent {
comment: GithubComment,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct CreateEvent {
master_branch: String,
pusher_type: String,
r#ref: String,
ref_type: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct DeleteEvent {
pusher_type: String,
r#ref: String,
ref_type: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ForkEvent {
forkee: GithubRepository,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GithubTeam {
id: u32,
node_id: String,
url: String,
hmtl_url: String,
name: String,
slug: String,
description: Option<String>,
privacy: String,
permission: String,
members_url: String,
repositories_url: String,
parent: Option<Box<GithubTeam>>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GithubHead {
label: String,
r#ref: String,
repo: GithubRepository,
sha: String,
user: Option<GithubUser>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GithubHref {
href: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GithubLinks {
#[serde(rename = "self")]
_self: GithubHref,
html: GithubHref,
comments: GithubHref,
commits: GithubHref,
statuses: GithubHref,
issue: GithubHref,
review_comments: GithubHref,
review_comment: GithubHref,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GithubAutoMerge {
enabled_by: GithubUser,
merge_method: String,
commit_title: String,
commit_message: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GithubPullRequest {
url: String,
id: u32,
node_id: String,
html_url: String,
diff_url: String,
patch_url: String,
issue_url: String,
number: u32,
state: String,
locked: bool,
title: String,
user: GithubUser,
body: Option<String>,
created_at: Value,
updated_at: Value,
closed_at: Option<Value>,
merged_at: Option<Value>,
merge_commit_sha: Option<String>,
assignee: Option<GithubUser>,
assignees: Vec<GithubUser>,
requested_reviewers: Vec<GithubUser>,
requested_teams: Vec<GithubTeam>,
head: GithubHead,
base: GithubHead,
_links: GithubLinks,
author_association: String,
auto_merge: Option<GithubAutoMerge>,
draft: bool,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "snake_case", tag = "action")]
pub enum PullRequestEvent {
Assigned {},
AutoMergeDisabled {},
AutoMergeEnabled {},
Closed {
number: u32,
pull_request: GithubPullRequest,
},
ConvertedToDraft {},
Demilestoned {},
Dequeued {},
Edited {},
Enqueued {},
Labeled {},
Locked {},
Milestoned {},
Opened {
number: u32,
pull_request: GithubPullRequest,
},
ReadyForReview {},
Reopened {
number: u32,
pull_request: GithubPullRequest,
},
ReviewRequestRemoved {},
ReviewRequest {},
Synchronized {},
Unassigned {},
Unlabeled {},
Unlocked {},
}
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum BaseEvent {
#[allow(dead_code)]
Star(StarEvent),
Ping,
Push(PushEvent),
CommitComment(CommitCommentEvent),
Create(CreateEvent),
Delete(DeleteEvent),
Discussion(GithubDiscussionEvent),
DiscussionComment(DiscussionCommentEvent),
Fork(ForkEvent),
IssueComment(IssueCommentEvent),
Issues(IssuesEvent),
PullRequest(PullRequestEvent),
}
#[derive(Serialize, Deserialize, Debug)]
pub struct _Event {
action: Option<String>,
sender: GithubUser,
repository: GithubRepository,
}
#[derive(Debug)]
pub struct Event {
event: BaseEvent,
action: Option<String>,
sender: GithubUser,
repository: GithubRepository,
}
#[derive(Debug, JsonSchema)]
pub struct EventHeader<'r>(pub &'r str);
impl std::ops::Deref for EventHeader<'_> {
type Target = str;
fn deref(&self) -> &Self::Target {
self.0
}
}
#[async_trait]
impl<'r> FromRequest<'r> for EventHeader<'r> {
type Error = Error;
async fn from_request(request: &'r Request<'_>) -> rocket::request::Outcome<Self, Self::Error> {
let headers = request.headers();
let Some(event) = headers.get_one("X-GitHub-Event") else {
return rocket::request::Outcome::Error((
Status::BadRequest,
create_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,
) -> revolt_rocket_okapi::Result<RequestHeaderInput> {
let mut content = schemars::Map::new();
content.insert(
"X-Github-Event".to_string(),
MediaType {
schema: Some(SchemaObject {
string: Some(Box::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: &str = "#0f890f";
const RED: &str = "#e73e3e";
const GREY: &str = "#202224";
const BLUE: &str = "#279adc";
const ORANGE: &str = "#d76a34";
const LIGHT_ORANGE: &str = "#d9916d";
// for future use
// const WHITE: &str = "#c3e1c3";
fn shorten_single_line_text(text: &str, length: usize) -> String {
if text.contains('\n') {
let text = text.split('\n').next().unwrap();
format!("{}...", &text[..text.len().min(length) - 3])
} else if text.len() > length {
format!("{}...", &text[..length - 3])
} else {
text.to_string()
}
}
fn shorten_text(text: &str, length: usize) -> String {
if text.len() >= length {
format!("{}...", &text[..length - 3])
} else {
text.to_string()
}
}
fn safe_from_str<T: for<'de> Deserialize<'de>>(data: &str) -> Result<T> {
match serde_json::from_str(data) {
Ok(output) => Ok(output),
Err(err) => {
revolt_config::capture_internal_error!(err);
Err(create_error!(InvalidOperation))
}
}
}
// Because github gives the event name in the header, this requires us to manually parse the enum, i did try to manually edit the json to add the tag into it,
// however it seems serde messes up with #[serde(flattern)] and enum handling.
// If someone finds a better solution to this please make a PR.
fn convert_event(data: &str, event_name: &str) -> Result<Event> {
let event = safe_from_str(data)?;
let base_event = match event_name {
"star" => BaseEvent::Star(safe_from_str(data)?),
"ping" => BaseEvent::Ping,
"push" => BaseEvent::Push(safe_from_str(data)?),
"commit_comment" => BaseEvent::CommitComment(safe_from_str(data)?),
"create" => BaseEvent::Create(safe_from_str(data)?),
"delete" => BaseEvent::Delete(safe_from_str(data)?),
"discussion" => BaseEvent::Discussion(safe_from_str(data)?),
"discussion_comment" => BaseEvent::DiscussionComment(safe_from_str(data)?),
"fork" => BaseEvent::Fork(safe_from_str(data)?),
"issue_comment" => BaseEvent::IssueComment(safe_from_str(data)?),
"issues" => BaseEvent::Issues(safe_from_str(data)?),
"pull_request" => BaseEvent::PullRequest(safe_from_str(data)?),
_ => return Err(create_error!(InvalidOperation)),
};
let _Event {
action,
sender,
repository,
} = event;
Ok(Event {
action,
sender,
repository,
event: base_event,
})
}
/// # Executes a webhook specific to github
///
/// Executes a webhook specific to github and sends a message containing the relevant info about the event
#[openapi(tag = "Webhooks")]
#[post("/<webhook_id>/<token>/github", data = "<data>")]
pub async fn webhook_execute_github(
db: &State<Database>,
amqp: &State<AMQP>,
webhook_id: Reference<'_>,
token: &str,
event: EventHeader<'_>,
data: String,
) -> Result<()> {
let webhook = webhook_id.as_webhook(db).await?;
webhook.assert_token(token)?;
let channel = db.fetch_channel(&webhook.channel_id).await?;
let event = convert_event(&data, &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(PushEvent {
after,
commits,
compare,
forced,
r#ref,
..
}) => {
let branch = r#ref.split('/').skip(2).collect::<Vec<_>>().join("/");
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 = shorten_text(
&commits
.into_iter()
.map(|commit| {
format!(
"[`{}`]({}) {} - {}",
&commit.id[0..=7],
commit.url,
shorten_single_line_text(&commit.message, 50),
commit.author.name
)
})
.collect::<Vec<String>>()
.join("\n"),
1000,
);
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()
}
}
}
BaseEvent::CommitComment(CommitCommentEvent { comment }) => {
let commit_id = match comment.commit_id {
Some(id) => id[0..=7].to_string(),
None => "".to_string(),
};
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,
commit_id,
comment.html_url,
shorten_text(&comment.body, 450)
)),
colour: Some(GREY.to_string()),
..Default::default()
}
}
BaseEvent::Ping => return Ok(()),
BaseEvent::Create(CreateEvent {
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(DeleteEvent {
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,
discussion.html_url,
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,
answer.comment.html_url,
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,
comment.comment.html_url,
shorten_text(&comment.comment.body, 450)
)),
colour: Some(LIGHT_ORANGE.to_string()),
..Default::default()
},
_ => return Ok(()),
},
BaseEvent::Fork(ForkEvent { 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,
shorten_text(&issue.body.unwrap_or_default(), 450)
)),
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,
comment.html_url,
shorten_text(&comment.body, 450)
)),
colour: Some(LIGHT_ORANGE.to_string()),
..Default::default()
},
_ => return Ok(()),
},
BaseEvent::PullRequest(pull_request_event) => match pull_request_event {
PullRequestEvent::Closed {
number,
pull_request,
} => SendableEmbed {
icon_url: Some(event.sender.avatar_url),
url: Some(event.sender.html_url),
title: Some(event.sender.login),
description: Some(format!(
"#### [[{}] Pull Request Closed #{}: {}]({})",
event.repository.full_name, number, pull_request.title, pull_request.html_url
)),
colour: Some(GREY.to_string()),
..Default::default()
},
PullRequestEvent::Opened {
number,
pull_request,
} => SendableEmbed {
icon_url: Some(event.sender.avatar_url),
url: Some(event.sender.html_url),
title: Some(event.sender.login),
description: Some(format!(
"#### [[{}] Pull Request Opened #{}: {}]({})\n{}",
event.repository.full_name,
number,
pull_request.title,
pull_request.html_url,
shorten_text(&pull_request.body.unwrap_or_default(), 450)
)),
colour: Some(ORANGE.to_string()),
..Default::default()
},
PullRequestEvent::Reopened {
number,
pull_request,
} => SendableEmbed {
icon_url: Some(event.sender.avatar_url),
url: Some(event.sender.html_url),
title: Some(event.sender.login),
description: Some(format!(
"#### [[{}] Pull Request Reopened #{}: {}]({})",
event.repository.full_name, number, pull_request.html_url, pull_request.title
)),
colour: Some(GREEN.to_string()),
..Default::default()
},
_ => return Ok(()),
},
};
sendable_embed.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let message_id = Ulid::new().to_string();
let mut message = Message {
id: message_id,
author: webhook.id.clone(),
channel: webhook.channel_id.clone(),
webhook: Some(std::convert::Into::<Webhook>::into(webhook.clone()).into()),
..Default::default()
};
#[allow(clippy::disallowed_methods)]
message.attach_sendable_embed(db, sendable_embed).await?;
message
.send(
db,
Some(amqp),
MessageAuthor::Webhook(&webhook.into()),
None,
None,
&channel,
false,
)
.await
}