From 9e6906c00c49186189d211dc96e132d85e7ff641 Mon Sep 17 00:00:00 2001 From: iximeow Date: Thu, 13 Jul 2023 00:51:51 -0700 Subject: reorganize the whole thing into crates/ packages --- ci-lib-native/src/notifier.rs | 181 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 ci-lib-native/src/notifier.rs (limited to 'ci-lib-native/src/notifier.rs') diff --git a/ci-lib-native/src/notifier.rs b/ci-lib-native/src/notifier.rs new file mode 100644 index 0000000..dd4a35c --- /dev/null +++ b/ci-lib-native/src/notifier.rs @@ -0,0 +1,181 @@ +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use axum::http::StatusCode; +use lettre::transport::smtp::authentication::{Credentials, Mechanism}; +use lettre::{Message, Transport}; +use lettre::transport::smtp::extension::ClientId; +use lettre::transport::smtp::client::{SmtpConnection, TlsParametersBuilder}; +use std::time::Duration; +use std::path::Path; + +use ci_lib_core::dbctx::DbCtx; + +pub struct RemoteNotifier { + pub remote_path: String, + pub notifier: NotifierConfig, +} + +#[derive(Serialize, Deserialize)] +#[serde(untagged)] +pub enum NotifierConfig { + GitHub { + token: String, + }, + Email { + username: String, + password: String, + mailserver: String, + from: String, + to: String, + } +} + +impl NotifierConfig { + pub fn github_from_file>(path: P) -> Result { + let path = path.as_ref(); + let bytes = std::fs::read(path) + .map_err(|e| format!("can't read notifier config at {}: {:?}", path.display(), e))?; + let config = serde_json::from_slice(&bytes) + .map_err(|e| format!("can't deserialize notifier config at {}: {:?}", path.display(), e))?; + + if matches!(config, NotifierConfig::GitHub { .. }) { + Ok(config) + } else { + Err(format!("config at {} doesn't look like a github config (but was otherwise valid?)", path.display())) + } + } + + pub fn email_from_file>(path: P) -> Result { + let path = path.as_ref(); + let bytes = std::fs::read(path) + .map_err(|e| format!("can't read notifier config at {}: {:?}", path.display(), e))?; + let config = serde_json::from_slice(&bytes) + .map_err(|e| format!("can't deserialize notifier config at {}: {:?}", path.display(), e))?; + + if matches!(config, NotifierConfig::Email { .. }) { + Ok(config) + } else { + Err(format!("config at {} doesn't look like an email config (but was otherwise valid?)", path.display())) + } + } +} + +impl RemoteNotifier { + pub async fn tell_pending_job(&self, ctx: &Arc, repo_id: u64, sha: &str, job_id: u64) -> Result<(), String> { + self.tell_job_status( + ctx, + repo_id, sha, job_id, + "pending", "build is queued", &format!("https://{}/{}/{}", "ci.butactuallyin.space", &self.remote_path, sha) + ).await + } + + pub async fn tell_complete_job(&self, ctx: &Arc, repo_id: u64, sha: &str, job_id: u64, desc: Result) -> Result<(), String> { + match desc { + Ok(status) => { + self.tell_job_status( + ctx, + repo_id, sha, job_id, + "success", &status, &format!("https://{}/{}/{}", "ci.butactuallyin.space", &self.remote_path, sha) + ).await + }, + Err(status) => { + self.tell_job_status( + ctx, + repo_id, sha, job_id, + "failure", &status, &format!("https://{}/{}/{}", "ci.butactuallyin.space", &self.remote_path, sha) + ).await + } + } + } + + pub async fn tell_job_status(&self, ctx: &Arc, repo_id: u64, sha: &str, job_id: u64, state: &str, desc: &str, target_url: &str) -> Result<(), String> { + match &self.notifier { + NotifierConfig::GitHub { token } => { + let status_info = serde_json::json!({ + "state": state, + "description": desc, + "target_url": target_url, + "context": "actuallyinspace runner", + }); + + // TODO: should pool (probably in ctx?) to have an upper bound in concurrent + // connections. + let client = reqwest::Client::new(); + let req = client.post(&format!("https://api.github.com/repos/{}/statuses/{}", &self.remote_path, sha)) + .body(serde_json::to_string(&status_info).expect("can stringify json")) + .header("content-type", "application/json") + .header("user-agent", "iximeow") + .header("authorization", format!("Bearer {}", token)) + .header("accept", "application/vnd.github+json"); + eprintln!("sending {:?}", req); + eprintln!(" body: {}", serde_json::to_string(&status_info).expect("can stringify json")); + let res = req + .send() + .await; + + match res { + Ok(res) => { + if res.status() == StatusCode::OK || res.status() == StatusCode::CREATED{ + Ok(()) + } else { + Err(format!("bad response: {}, response data: {:?}", res.status().as_u16(), res)) + } + } + Err(e) => { + Err(format!("failure sending request: {:?}", e)) + } + } + } + NotifierConfig::Email { username, password, mailserver, from, to } => { + eprintln!("[.] emailing {} for job {} via {}", state, &self.remote_path, mailserver); + + let subject = format!("{}: job for {}", state, &self.remote_path); + + let body = format!("{}", subject); + + // TODO: when ci.butactuallyin.space has valid certs again, ... fix this. + let tls = TlsParametersBuilder::new(mailserver.to_string()) + .dangerous_accept_invalid_certs(true) + .build() + .unwrap(); + + let mut mailer = SmtpConnection::connect( + mailserver, + Some(Duration::from_millis(5000)), + &ClientId::Domain("ci.butactuallyin.space".to_string()), + None, + None, + ).unwrap(); + + mailer.starttls( + &tls, + &ClientId::Domain("ci.butactuallyin.space".to_string()), + ).unwrap(); + + let resp = mailer.auth( + &[Mechanism::Plain, Mechanism::Login], + &Credentials::new(username.to_owned(), password.to_owned()) + ).unwrap(); + assert!(resp.is_positive()); + + let email = Message::builder() + .from(from.parse().unwrap()) + .to(to.parse().unwrap()) + .subject(&subject) + .body(body) + .unwrap(); + + match mailer.send(email.envelope(), &email.formatted()) { + Ok(_) => { + eprintln!("[+] notified {}@{}", username, mailserver); + Ok(()) + } + Err(e) => { + eprintln!("[-] could not send email: {:?}", e); + Err(e.to_string()) + } + } + } + } + } +} -- cgit v1.1