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, 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: Value, 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)] 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, created_at: Value, html_url: String, id: u32, line: Option, node_id: String, path: Option, position: Option, reactions: Option, updated_at: Value, 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: Value, 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: Value, 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: Value, 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: Value, 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: Vec, milestone: Option, locked: bool, active_lock_reason: Option, comments: u32, pull_request: Option, closed_at: Option, created_at: Value, updated_at: Value, draft: Option, closed_by: Option, body: Option, timeline_url: Option, repository: Option, performed_via_github_app: Option, author_association: 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)] pub struct StarEvent { starred_at: Option, } #[derive(Serialize, Deserialize, Debug)] pub struct PushEvent { 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)] 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, privacy: String, permission: String, members_url: String, repositories_url: String, parent: Option>, } #[derive(Serialize, Deserialize, Debug)] pub struct GithubHead { label: String, r#ref: String, repo: GithubRepository, sha: String, user: Option, } #[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, created_at: Value, updated_at: Value, closed_at: Option, merged_at: Option, merge_commit_sha: Option, assignee: Option, assignees: Vec, requested_reviewers: Vec, requested_teams: Vec, head: GithubHead, base: GithubHead, _links: GithubLinks, author_association: String, auto_merge: Option, 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, sender: GithubUser, repository: GithubRepository, } #[derive(Debug)] pub struct Event { event: BaseEvent, action: Option, 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 { 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 { 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_text(text: &str, length: usize) -> String { if text.len() < length { text.to_string() } else { format!("{}...", &text[0..length]) } } fn safe_from_str Deserialize<'de>>(data: &str) -> Result { match serde_json::from_str(data) { Ok(output) => Ok(output), Err(err) => { log::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 { 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("///github", data = "")] pub async fn webhook_execute_github( db: &State, amqp: &State, webhook_id: Reference<'_>, token: String, 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::>().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_text(&commit.message, 50), commit.author.name ) }) .collect::>() .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, 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(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, issue.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::::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 }