Remove unwraps, revamp code

master
Manos Pitsidianakis 2022-09-24 15:30:31 +03:00
parent 6aa2f12e2c
commit 908493327f
9 changed files with 1259 additions and 1666 deletions

5
.gitignore vendored
View File

@ -1,2 +1,7 @@
/target /target
**/*.rs.bk **/*.rs.bk
.*.swp
*.swp
config.toml
issue-bot.log
sqlite3.db

2488
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -2,24 +2,23 @@
name = "issue-bot" name = "issue-bot"
version = "0.2.0" version = "0.2.0"
authors = ["Manos Pitsidianakis <el13635@mail.ntua.gr>"] authors = ["Manos Pitsidianakis <el13635@mail.ntua.gr>"]
edition = "2018" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
rusqlite = { version="0.20.0", features=["uuid",]} chrono = { version = "0.4.22" }
uuid = "*"
time = "*"
serde_json = "1.0.40"
serde = { version = "1.0.101", features = ["derive"]}
reqwest = "0.9.20"
toml = "0.5.3"
log = "0.4.11"
simplelog = "^0.8.0"
error-chain = "0.12.4" error-chain = "0.12.4"
log = "0.4.11"
reqwest = { version = "0.11", default-features = false, features = ["blocking", "native-tls", "json"] }
rusqlite = { version = "0.28", features = ["uuid", "chrono"] }
serde = { version = "1.0.101", features = ["derive"] }
serde_json = "1.0.40"
simplelog = "^0.8.0"
toml = "0.5.3"
uuid = "1.1.2"
[dependencies.melib] [dependencies.melib]
git = "https://git.meli.delivery/meli/meli" git = "https://git.meli.delivery/meli/meli"
version = "0.6.1" version = "0.7.2"
default-features = false default-features = false
features = [] features = []

View File

@ -16,8 +16,22 @@
use super::*; use super::*;
static ISSUES_BASE_URL: &'static str = "{base_url}/api/v1/repos/{repo}/issues"; #[macro_export]
static ISSUES_COMMENTS_URL: &'static str = "{base_url}/api/v1/repos/{repo}/issues/{index}/comments"; macro_rules! gitea_api_mismatch {
($map:ident[$value:literal].$conv_method:ident()) => {{
$map[$value].$conv_method().ok_or_else(|| {
log::error!(
"issue API response missing valid {} field: {:?}",
$value,
$map
);
Error::new("Gitea API response or API version not matching what was expected.")
})?
}};
}
static ISSUES_BASE_URL: &str = "{base_url}/api/v1/repos/{repo}/issues";
static ISSUES_COMMENTS_URL: &str = "{base_url}/api/v1/repos/{repo}/issues/{index}/comments";
use serde::Serialize; use serde::Serialize;
@ -51,7 +65,7 @@ pub fn new_issue(
), ),
..CreateIssueOption::default() ..CreateIssueOption::default()
}; };
let client = reqwest::Client::new(); let client = reqwest::blocking::Client::new();
let res = client let res = client
.post( .post(
&ISSUES_BASE_URL &ISSUES_BASE_URL
@ -60,21 +74,19 @@ pub fn new_issue(
) )
.header("Authorization", format!("token {}", &conf.auth_token)) .header("Authorization", format!("token {}", &conf.auth_token))
.json(&issue) .json(&issue)
.send() .send()?
.unwrap() .text()?;
.text()
.unwrap();
let map: serde_json::map::Map<String, serde_json::Value> = serde_json::from_str(&res).unwrap(); let map: serde_json::map::Map<String, serde_json::Value> = serde_json::from_str(&res)?;
let issue = Issue { let issue = Issue {
id: map["number"].as_i64().unwrap(), id: gitea_api_mismatch!(map["number"].as_i64()),
submitter, submitter,
password: Uuid::new_v4(), password: Uuid::new_v4(),
time_created: time::get_time(), time_created: chrono::Local::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
anonymous, anonymous,
subscribed: true, subscribed: true,
title: issue.title, title: issue.title,
last_update: map["created_at"].to_string(), last_update: gitea_api_mismatch!(map["created_at"].as_str()).to_string(),
}; };
conn.execute( conn.execute(
"INSERT INTO issue (id, submitter, password, time_created, anonymous, subscribed, title, last_update) "INSERT INTO issue (id, submitter, password, time_created, anonymous, subscribed, title, last_update)
@ -89,8 +101,7 @@ pub fn new_issue(
&issue.title, &issue.title,
&issue.last_update, &issue.last_update,
], ],
) )?;
.unwrap();
Ok((issue.password, issue.id)) Ok((issue.password, issue.id))
} }
@ -109,15 +120,14 @@ pub fn new_reply(
let mut stmt = let mut stmt =
conn.prepare("SELECT id, title, subscribed, anonymous FROM issue WHERE password = ?")?; conn.prepare("SELECT id, title, subscribed, anonymous FROM issue WHERE password = ?")?;
let mut results = stmt let mut results = stmt
.query_map(&[password.as_bytes().to_vec()], |row| { .query_map([password.as_bytes().to_vec()], |row| {
Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)) Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?))
})? })?
.map(|r| r.unwrap()) .collect::<std::result::Result<Vec<(i64, String, bool, bool)>, _>>()?;
.collect::<Vec<(i64, String, bool, bool)>>();
if results.is_empty() { if results.is_empty() {
return Err(Error::new("Not found".to_string())); return Err(Error::new("Not found".to_string()));
} }
let client = reqwest::Client::new(); let client = reqwest::blocking::Client::new();
let response = client let response = client
.post( .post(
&ISSUES_COMMENTS_URL &ISSUES_COMMENTS_URL
@ -145,8 +155,8 @@ pub fn new_reply(
eprintln!( eprintln!(
"New reply could not be created: {:?}\npassword: {}\nsubmitter: {}\nbody: {}", "New reply could not be created: {:?}\npassword: {}\nsubmitter: {}\nbody: {}",
response.status(), response.status(),
password.to_string(), password,
submitter.to_string(), submitter,
body body
); );
Err(Error::new( Err(Error::new(
@ -167,15 +177,14 @@ pub fn close(
) -> Result<(String, i64, bool)> { ) -> Result<(String, i64, bool)> {
let mut stmt = conn.prepare("SELECT id, title, subscribed FROM issue WHERE password = ?")?; let mut stmt = conn.prepare("SELECT id, title, subscribed FROM issue WHERE password = ?")?;
let mut results = stmt let mut results = stmt
.query_map(&[password.as_bytes().to_vec()], |row| { .query_map([password.as_bytes().to_vec()], |row| {
Ok((row.get(0)?, row.get(1)?, row.get(2)?)) Ok((row.get(0)?, row.get(1)?, row.get(2)?))
})? })?
.map(|r| r.unwrap()) .collect::<std::result::Result<Vec<(i64, String, bool)>, _>>()?;
.collect::<Vec<(i64, String, bool)>>();
if results.is_empty() { if results.is_empty() {
return Err(Error::new("Not found".to_string())); return Err(Error::new("Not found".to_string()));
} }
let client = reqwest::Client::new(); let client = reqwest::blocking::Client::new();
let res = client let res = client
.patch(&format!( .patch(&format!(
"{}/{}", "{}/{}",
@ -191,7 +200,7 @@ pub fn close(
.send()? .send()?
.text()?; .text()?;
let map: serde_json::map::Map<String, serde_json::Value> = serde_json::from_str(&res).unwrap(); let map: serde_json::map::Map<String, serde_json::Value> = serde_json::from_str(&res)?;
if map["state"] == "closed" { if map["state"] == "closed" {
let (issue_id, title, is_subscribed) = results.remove(0); let (issue_id, title, is_subscribed) = results.remove(0);
Ok((title, issue_id, is_subscribed)) Ok((title, issue_id, is_subscribed))
@ -209,12 +218,11 @@ pub fn change_subscription(
new_val: bool, new_val: bool,
) -> Result<(String, i64, bool)> { ) -> Result<(String, i64, bool)> {
let mut stmt = conn.prepare("SELECT id, title, subscribed FROM issue WHERE password = ?")?; let mut stmt = conn.prepare("SELECT id, title, subscribed FROM issue WHERE password = ?")?;
let mut results = stmt let mut results: Vec<(i64, String, bool)> = stmt
.query_map(&[password.as_bytes().to_vec()], |row| { .query_map([password.as_bytes().to_vec()], |row| {
Ok((row.get(0)?, row.get(1)?, row.get(2)?)) Ok((row.get(0)?, row.get(1)?, row.get(2)?))
})? })?
.map(|r| r.unwrap()) .collect::<std::result::Result<Vec<(i64, String, bool)>, _>>()?;
.collect::<Vec<(i64, String, bool)>>();
if results.is_empty() { if results.is_empty() {
return Err(Error::new("Issue not found".to_string())); return Err(Error::new("Issue not found".to_string()));
} }
@ -234,10 +242,10 @@ pub fn change_subscription(
let mut stmt = let mut stmt =
conn.prepare("UPDATE issue SET subscribed = (:subscribed) WHERE password = (:password)")?; conn.prepare("UPDATE issue SET subscribed = (:subscribed) WHERE password = (:password)")?;
assert_eq!( assert_eq!(
stmt.execute_named(&[ stmt.execute(rusqlite::named_params! {
(":subscribed", &new_val), ":subscribed": &new_val,
(":password", &password.as_bytes().to_vec()) ":password": &password.as_bytes().to_vec()
])?, })?,
1 1
); );
Ok((title, issue_id, is_subscribed)) Ok((title, issue_id, is_subscribed))
@ -247,8 +255,8 @@ pub fn comments(
id: i64, id: i64,
since: &str, since: &str,
conf: &Configuration, conf: &Configuration,
) -> Vec<serde_json::map::Map<String, serde_json::Value>> { ) -> Result<Vec<serde_json::map::Map<String, serde_json::Value>>> {
let client = reqwest::Client::new(); let client = reqwest::blocking::Client::new();
let result = client let result = client
.get( .get(
&ISSUES_COMMENTS_URL &ISSUES_COMMENTS_URL
@ -256,12 +264,9 @@ pub fn comments(
.replace("{repo}", &conf.repo) .replace("{repo}", &conf.repo)
.replace("{index}", &id.to_string()), .replace("{index}", &id.to_string()),
) )
.header("Authorization", format!("token {}", &conf.auth_token))
.query(&[("since", since)]) .query(&[("since", since)])
.send() .send()?
.unwrap() .text()?;
.text() let result: Vec<_> = serde_json::from_str(&result)?;
.unwrap(); Ok(result)
let result: Vec<_> = serde_json::from_str(&result).unwrap();
result
} }

View File

@ -17,25 +17,29 @@
use serde::Deserialize; use serde::Deserialize;
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct Configuration { pub struct Configuration {
/** eg. meli-issues becomes [meli-issues] **/ /// eg. meli-issues becomes [meli-issues]
pub tag: String, pub tag: String,
/** your bot's authentication token from Gitea's Swagger **/ /// your bot's authentication token from Gitea's Swagger
pub auth_token: String, pub auth_token: String,
/** eg. for issues@meli.delivery the local part is issues **/ /// eg. for issues@meli.delivery the local part is issues
pub local_part: String, pub local_part: String,
/** eg. for issues@meli.delivery the domain is meli.delivery **/ /// eg. for issues@meli.delivery the domain is meli.delivery
pub domain: String, pub domain: String,
/** eg. "https://git.meli.delivery" **/ /// eg. "https://git.meli.delivery"
pub base_url: String, pub base_url: String,
/** eg. "meli/meli" **/ /// eg. "meli/meli"
pub repo: String, pub repo: String,
/** The bot's name that will be displayed in signatures of sent replies **/ /// The bot's name that will be displayed in signatures of sent replies
pub bot_name: String, pub bot_name: String,
/** The bot's login username **/ /// The bot's login username
pub bot_username: String, pub bot_username: String,
/** the command to pipe an email to **/ /// the command to pipe an email to
pub mailer: String, pub mailer: String,
/** file to write logs **/ /// file to write logs
pub log_file: String, pub log_file: String,
/// don't actually email anything
#[serde(default)]
pub dry_run: bool,
} }

View File

@ -17,93 +17,120 @@
use super::*; use super::*;
use melib::email::address::Address; use melib::email::address::Address;
pub fn check(conn: Connection, conf: Configuration) -> Result<()> { pub fn check_issue(conn: &Connection, conf: &Configuration, issue: Issue) -> Result<bool> {
let mut stmt = conn.prepare("SELECT * FROM issue")?; let mut update = false;
let mut results = stmt let mut comments = api::comments(issue.id, &issue.last_update, conf)?;
.query_map(NO_PARAMS, |row| { let mut new_value = issue.last_update.clone();
let submitter: String = row.get(1)?; for c in &comments {
let password: Vec<u8> = row.get(2)?; _ = gitea_api_mismatch!(c["created_at"].as_str());
let last_update: Option<String> = row.get(7)?; }
Ok(Issue { comments.retain(|c| {
id: row.get(0)?, // Unwrap is safe since we checked above in the forloop
submitter: Address::new(None, submitter.as_str().to_string()), let created_at = c["created_at"].as_str().unwrap().to_string();
password: Password::from_slice(password.as_slice()).unwrap(), if created_at > issue.last_update {
time_created: row.get(3)?, if created_at > new_value {
anonymous: row.get(4)?, new_value = created_at;
subscribed: row.get(5)?, update = true;
title: row.get(6)?,
last_update: last_update.unwrap_or(String::new()),
})
})?
.collect::<std::result::Result<Vec<Issue>, _>>()?;
for issue in &mut results {
let mut update = false;
let mut comments = api::comments(issue.id, &issue.last_update, &conf);
let mut new_value = issue.last_update.clone();
comments.retain(|c| {
let created_at = c["created_at"].to_string();
if created_at > issue.last_update {
if created_at > new_value {
new_value = created_at;
update = true;
}
true
} else {
false
} }
}); true
if update { } else {
let mut stmt = false
conn.prepare("UPDATE issue SET last_update = (:last_update) WHERE id = (:id)")?; }
assert_eq!( });
stmt.execute_named(&[(":last_update", &new_value), (":id", &issue.id),])?, if update {
1 if issue.subscribed {
); let comments = comments
if issue.subscribed { .into_iter()
let comments = comments .map(|c| {
.into_iter() let u = &c["user"];
.map(|c| { Ok(
if c["user"]["login"].as_str().unwrap() == &conf.bot_username { if gitea_api_mismatch!(u["login"].as_str()) == conf.bot_username {
c["body"].as_str().unwrap().to_string() gitea_api_mismatch!(c["body"].as_str()).to_string()
} else { } else {
format!( format!(
"User {} replied:\n\n{}", "User {} replied:\n\n{}",
c["user"]["login"], c["user"]["login"],
c["body"].as_str().unwrap() gitea_api_mismatch!(c["body"].as_str())
) )
} },
})
.collect::<Vec<String>>();
let mut notice = melib::Draft::default();
notice.headers_mut().insert(
HeaderName::new_unchecked("From"),
Address::new(
None,
format!(
"{local_part}@{domain}",
local_part = &conf.local_part,
domain = &conf.domain
),
) )
.to_string(), })
); .collect::<Result<Vec<String>>>()?;
notice.headers_mut().insert( let mut notice = melib::Draft::default();
HeaderName::new_unchecked("Subject"), notice.headers_mut().insert(
HeaderName::new_unchecked("From"),
Address::new(
None,
format!( format!(
"[{tag}] new replies in issue `{title}`", "{local_part}@{domain}",
tag = &conf.tag, local_part = &conf.local_part,
title = &issue.title domain = &conf.domain
) ),
.to_string(), )
); .to_string(),
notice );
.headers_mut() notice.headers_mut().insert(
.insert(HeaderName::new_unchecked("To"), issue.submitter.to_string()); HeaderName::new_unchecked("Subject"),
format!(
"[{tag}] new replies in issue `{title}`",
tag = &conf.tag,
title = &issue.title
),
);
notice
.headers_mut()
.insert(HeaderName::new_unchecked("To"), issue.submitter.to_string());
notice.set_body(templates::reply_update(&issue, &conf, comments)); notice.set_body(templates::reply_update(&issue, conf, comments));
send_mail(notice, &conf); send_mail(notice, conf)?;
} }
if !conf.dry_run {
let mut stmt =
conn.prepare("UPDATE issue SET last_update = (:last_update) WHERE id = (:id)")?;
assert_eq!(
stmt.execute(
rusqlite::named_params! {":last_update": &new_value, ":id": &issue.id}
)?,
1
);
} }
} }
Ok(update)
}
pub fn check(conn: Connection, conf: Configuration) -> Result<()> {
let mut stmt = conn.prepare("SELECT * FROM issue")?;
let results = stmt
.query_map([], |row| {
let submitter: String = row.get(1)?;
let password: uuid::Uuid = row.get(2)?;
let last_update: Option<String> = row.get(7)?;
Ok(Issue {
id: row.get(0)?,
submitter: Address::new(None, submitter.as_str().to_string()),
password,
time_created: row.get(3)?,
anonymous: row.get(4)?,
subscribed: row.get(5)?,
title: row.get(6)?,
last_update: last_update.unwrap_or_default(),
})
})?
.collect::<std::result::Result<Vec<Issue>, _>>()?;
let mut errors: Vec<Result<bool>> = vec![];
for issue in results {
errors.push(check_issue(&conn, &conf, issue));
}
if errors.iter().any(|r| matches!(r, Ok(true))) {
let successes_count = errors.iter().filter(|r| matches!(r, Ok(true))).count();
let error_count = errors.iter().filter(|r| r.is_err()).count();
log::info!(
"Cron run with {} updates and {} errors.",
successes_count,
error_count
);
}
_ = errors.into_iter().collect::<Result<Vec<bool>>>()?;
Ok(()) Ok(())
} }

View File

@ -22,6 +22,10 @@ error_chain! {
Unicode(std::str::Utf8Error); Unicode(std::str::Utf8Error);
UnicodeS(std::string::FromUtf8Error); UnicodeS(std::string::FromUtf8Error);
Email(melib::error::MeliError); Email(melib::error::MeliError);
Api(serde_json::Error);
Conf(toml::de::Error);
Logger(log::SetLoggerError);
Password(uuid::Error);
} }
} }

View File

@ -24,11 +24,10 @@ use log::{error, info, trace};
use melib::email::headers::HeaderName; use melib::email::headers::HeaderName;
use melib::{Address, Envelope}; use melib::{Address, Envelope};
use rusqlite::types::ToSql; use rusqlite::types::ToSql;
use rusqlite::{Connection, NO_PARAMS}; use rusqlite::Connection;
use simplelog::*; use simplelog::*;
use std::fs::File; use std::fs::File;
use std::io::{stdin, Read}; use std::io::{stdin, Read};
use time::Timespec;
use uuid::Uuid; use uuid::Uuid;
mod error; mod error;
@ -40,48 +39,51 @@ mod cron;
mod templates; mod templates;
type Password = Uuid; type Password = Uuid;
static PASSWORD_COMMANDS: &'static [&'static str] = &["reply", "unsubscribe", "subscribe", "close"]; static PASSWORD_COMMANDS: &[&str] = &["reply", "unsubscribe", "subscribe", "close"];
#[derive(Debug)] #[derive(Debug)]
pub struct Issue { pub struct Issue {
id: i64, id: i64,
submitter: Address, submitter: Address,
password: Password, password: Password,
time_created: Timespec, time_created: String, // chrono::naive::NaiveDateTime,
anonymous: bool, anonymous: bool,
subscribed: bool, subscribed: bool,
title: String, title: String,
last_update: String, last_update: String, // chrono::DateTime<chrono::FixedOffset>,
} }
pub fn send_mail(d: melib::email::Draft, conf: &Configuration) { pub fn send_mail(d: melib::email::Draft, conf: &Configuration) -> Result<()> {
use std::io::Write; use std::io::Write;
use std::process::Stdio; use std::process::Stdio;
let parts = conf.mailer.split_whitespace().collect::<Vec<&str>>(); let parts = conf.mailer.split_whitespace().collect::<Vec<&str>>();
let (cmd, args) = (parts[0], &parts[1..]); let (cmd, args) = (parts[0], &parts[1..]);
if conf.dry_run {
eprintln!("DRY_RUN: NOT sending to the following email:\n{:?}\n", &d);
return Ok(());
}
let mut mailer = std::process::Command::new(cmd) let mut mailer = std::process::Command::new(cmd)
.args(args) .args(args)
.stdin(Stdio::piped()) .stdin(Stdio::piped())
.stdout(Stdio::inherit()) .stdout(Stdio::piped())
.spawn() .stderr(Stdio::piped())
.expect("Failed to start mailer command"); .spawn()?;
{ {
let stdin = mailer.stdin.as_mut().expect("failed to open stdin"); let stdin = mailer.stdin.as_mut().expect("failed to open stdin");
let draft = d.finalise().unwrap(); let draft = d.finalise()?;
stdin stdin.write_all(draft.as_bytes())?;
.write_all(draft.as_bytes())
.expect("Failed to write to stdin");
} }
let output = mailer.wait().expect("Failed to wait on mailer"); let output = mailer.wait()?;
if !output.success() { if !output.success() {
// TODO: commit to database queue // TODO: commit to database queue
error!("mailer fail"); error!("mailer fail");
eprintln!("mailer fail"); eprintln!("mailer fail");
std::process::exit(1); return Err(Error::new(format!("Mailer failed. {:?}", output)));
} }
Ok(())
} }
fn run_app(conn: Connection, conf: Configuration) -> Result<()> { fn run_request(conn: Connection, conf: Configuration) -> Result<()> {
let mut new_message_raw = vec![]; let mut new_message_raw = vec![];
stdin().lock().read_to_end(&mut new_message_raw)?; stdin().lock().read_to_end(&mut new_message_raw)?;
@ -103,7 +105,7 @@ fn run_app(conn: Connection, conf: Configuration) -> Result<()> {
let tags: Vec<String> = envelope.to()[0].get_tags('+'); let tags: Vec<String> = envelope.to()[0].get_tags('+');
match tags.as_slice() { match tags.as_slice() {
s if s.is_empty() || s == &["anonymous"] => { s if s.is_empty() || s == ["anonymous"] => {
/* Assign new issue */ /* Assign new issue */
let subject = envelope.subject().to_string(); let subject = envelope.subject().to_string();
let body = envelope.body_bytes(new_message_raw.as_slice()).text(); let body = envelope.body_bytes(new_message_raw.as_slice()).text();
@ -133,7 +135,7 @@ fn run_app(conn: Connection, conf: Configuration) -> Result<()> {
reply.set_body(templates::new_issue_success( reply.set_body(templates::new_issue_success(
subject, password, issue_id, &conf, subject, password, issue_id, &conf,
)); ));
send_mail(reply, &conf); send_mail(reply, &conf)?;
} }
Err(err) => { Err(err) => {
error!("Issue {} could not be created {}.", &subject, &err); error!("Issue {} could not be created {}.", &subject, &err);
@ -146,7 +148,7 @@ fn run_app(conn: Connection, conf: Configuration) -> Result<()> {
), ),
); );
reply.set_body(templates::new_issue_failure(err, &conf)); reply.set_body(templates::new_issue_failure(err, &conf));
send_mail(reply, &conf); send_mail(reply, &conf)?;
} }
} }
} }
@ -154,7 +156,7 @@ fn run_app(conn: Connection, conf: Configuration) -> Result<()> {
if Password::parse_str(p).is_ok() && PASSWORD_COMMANDS.contains(&cmd.as_str()) => if Password::parse_str(p).is_ok() && PASSWORD_COMMANDS.contains(&cmd.as_str()) =>
{ {
trace!("Got command {} from {}", cmd.as_str(), &envelope.from()[0]); trace!("Got command {} from {}", cmd.as_str(), &envelope.from()[0]);
let p = Password::parse_str(p).unwrap(); let p = Password::parse_str(p)?;
match cmd.as_str() { match cmd.as_str() {
"reply" => { "reply" => {
info!( info!(
@ -182,7 +184,7 @@ fn run_app(conn: Connection, conf: Configuration) -> Result<()> {
is_subscribed, is_subscribed,
&conf, &conf,
)); ));
send_mail(reply, &conf); send_mail(reply, &conf)?;
} }
Err(err) => { Err(err) => {
error!( error!(
@ -197,7 +199,7 @@ fn run_app(conn: Connection, conf: Configuration) -> Result<()> {
), ),
); );
reply.set_body(templates::new_reply_failure(err, &conf)); reply.set_body(templates::new_reply_failure(err, &conf));
send_mail(reply, &conf); send_mail(reply, &conf)?;
} }
} }
} }
@ -212,7 +214,7 @@ fn run_app(conn: Connection, conf: Configuration) -> Result<()> {
), ),
); );
reply.set_body(templates::close_success(title, issue_id, &conf)); reply.set_body(templates::close_success(title, issue_id, &conf));
send_mail(reply, &conf); send_mail(reply, &conf)?;
} }
Err(e) => { Err(e) => {
reply.headers_mut().insert( reply.headers_mut().insert(
@ -220,7 +222,7 @@ fn run_app(conn: Connection, conf: Configuration) -> Result<()> {
format!("[{tag}] issue could not be closed", tag = &conf.tag,), format!("[{tag}] issue could not be closed", tag = &conf.tag,),
); );
reply.set_body(templates::close_failure(e, &conf)); reply.set_body(templates::close_failure(e, &conf));
send_mail(reply, &conf); send_mail(reply, &conf)?;
} }
}, },
"unsubscribe" => match api::change_subscription(&conn, p, false) { "unsubscribe" => match api::change_subscription(&conn, p, false) {
@ -236,7 +238,7 @@ fn run_app(conn: Connection, conf: Configuration) -> Result<()> {
reply.set_body(templates::change_subscription_success( reply.set_body(templates::change_subscription_success(
title, p, issue_id, false, &conf, title, p, issue_id, false, &conf,
)); ));
send_mail(reply, &conf); send_mail(reply, &conf)?;
} }
Err(e) => { Err(e) => {
error!("unsubscribe error: {}", e.to_string()); error!("unsubscribe error: {}", e.to_string());
@ -245,7 +247,7 @@ fn run_app(conn: Connection, conf: Configuration) -> Result<()> {
format!("[{tag}] could not unsubscribe", tag = &conf.tag,), format!("[{tag}] could not unsubscribe", tag = &conf.tag,),
); );
reply.set_body(templates::change_subscription_failure(false, &conf)); reply.set_body(templates::change_subscription_failure(false, &conf));
send_mail(reply, &conf); send_mail(reply, &conf)?;
} }
}, },
"subscribe" => match api::change_subscription(&conn, p, true) { "subscribe" => match api::change_subscription(&conn, p, true) {
@ -261,7 +263,7 @@ fn run_app(conn: Connection, conf: Configuration) -> Result<()> {
reply.set_body(templates::change_subscription_success( reply.set_body(templates::change_subscription_success(
title, p, issue_id, true, &conf, title, p, issue_id, true, &conf,
)); ));
send_mail(reply, &conf); send_mail(reply, &conf)?;
} }
Err(e) => { Err(e) => {
error!("subscribe error: {}", e.to_string()); error!("subscribe error: {}", e.to_string());
@ -270,7 +272,7 @@ fn run_app(conn: Connection, conf: Configuration) -> Result<()> {
format!("[{tag}] could not subscribe", tag = &conf.tag,), format!("[{tag}] could not subscribe", tag = &conf.tag,),
); );
reply.set_body(templates::change_subscription_failure(true, &conf)); reply.set_body(templates::change_subscription_failure(true, &conf));
send_mail(reply, &conf); send_mail(reply, &conf)?;
} }
}, },
@ -280,7 +282,7 @@ fn run_app(conn: Connection, conf: Configuration) -> Result<()> {
format!("[{tag}] invalid action: `{}`", &other, tag = &conf.tag), format!("[{tag}] invalid action: `{}`", &other, tag = &conf.tag),
); );
reply.set_body(templates::invalid_request(&conf)); reply.set_body(templates::invalid_request(&conf));
send_mail(reply, &conf); send_mail(reply, &conf)?;
} }
} }
} }
@ -290,41 +292,38 @@ fn run_app(conn: Connection, conf: Configuration) -> Result<()> {
format!("[{tag}] invalid request", tag = &conf.tag), format!("[{tag}] invalid request", tag = &conf.tag),
); );
reply.set_body(templates::invalid_request(&conf)); reply.set_body(templates::invalid_request(&conf));
send_mail(reply, &conf); send_mail(reply, &conf)?;
error!("invalid request: {:?}", other); error!("invalid request: {:?}", other);
} }
} }
Ok(()) Ok(())
} }
fn main() -> Result<()> { fn run_app() -> Result<()> {
let mut file = std::fs::File::open("./config.toml")?; let mut file = std::fs::File::open("./config.toml")?;
let args = std::env::args().skip(1).collect::<Vec<String>>(); let args = std::env::args().skip(1).collect::<Vec<String>>();
let perform_cron: bool; let perform_cron: bool;
if args.len() > 1 { if args.len() > 1 {
eprintln!("Too many arguments."); return Err(Error::new("Too many arguments."));
std::process::exit(1); } else if args == ["cron"] {
} else if args == &["cron"] {
perform_cron = true; perform_cron = true;
} else if args.is_empty() { } else if args.is_empty() {
perform_cron = false; perform_cron = false;
} else { } else {
eprintln!("Usage: issue_bot [cron]"); return Err(Error::new("Usage: issue_bot [cron]"));
std::process::exit(1);
} }
let mut contents = String::new(); let mut contents = String::new();
file.read_to_string(&mut contents)?; file.read_to_string(&mut contents)?;
let conf: Configuration = toml::from_str(&contents).unwrap(); let conf: Configuration = toml::from_str(&contents)?;
CombinedLogger::init(vec![ CombinedLogger::init(vec![
TermLogger::new(LevelFilter::Error, Config::default(), TerminalMode::Mixed), TermLogger::new(LevelFilter::Error, Config::default(), TerminalMode::Mixed),
WriteLogger::new( WriteLogger::new(
LevelFilter::Trace, LevelFilter::Trace,
Config::default(), Config::default(),
File::create(&conf.log_file).unwrap(), File::create(&conf.log_file)?,
), ),
]) ])?;
.unwrap();
/* - read mail from stdin /* - read mail from stdin
* - decide which case this mail falls to * - decide which case this mail falls to
@ -341,7 +340,7 @@ fn main() -> Result<()> {
* *
*/ */
let db_path = "./sqlite3.db"; let db_path = "./sqlite3.db";
let conn = Connection::open(db_path).unwrap(); let conn = Connection::open(db_path)?;
conn.execute( conn.execute(
"CREATE TABLE IF NOT EXISTS issue ( "CREATE TABLE IF NOT EXISTS issue (
@ -354,9 +353,8 @@ fn main() -> Result<()> {
title TEXT NOT NULL, title TEXT NOT NULL,
last_update TEXT last_update TEXT
)", )",
NO_PARAMS, [],
) )?;
.unwrap();
if perform_cron { if perform_cron {
info!("Performing cron duties."); info!("Performing cron duties.");
@ -367,9 +365,16 @@ fn main() -> Result<()> {
return Ok(()); return Ok(());
} }
if let Err(err) = run_app(conn, conf) { if let Err(err) = run_request(conn, conf) {
error!("Encountered an error: {}", &err); error!("Encountered an error: {}", &err);
return Err(err); return Err(err);
} }
Ok(()) Ok(())
} }
fn main() {
if let Err(err) = run_app() {
eprintln!("{}", err);
std::process::exit(1);
}
}

View File

@ -16,14 +16,14 @@
use super::*; use super::*;
static BASE_ISSUE_URL: &'static str = "{base_url}/{repo}/issues"; static BASE_ISSUE_URL: &str = "{base_url}/{repo}/issues";
pub fn new_issue_failure(e: Error, conf: &Configuration) -> String { pub fn new_issue_failure(e: Error, conf: &Configuration) -> String {
format!("Hello, format!("Hello,
Unfortunately we were not able to create your issue. The reason was: `{}`. Please contact the repository's owners for assistance. Unfortunately we were not able to create your issue. The reason was: `{}`. Please contact the repository's owners for assistance.
This is an automated email from {bot_name} <{local_part}+help@{domain}>", e.to_string(), local_part = &conf.local_part, domain = &conf.domain, bot_name = &conf.bot_name) This is an automated email from {bot_name} <{local_part}+help@{domain}>", e, local_part = &conf.local_part, domain = &conf.domain, bot_name = &conf.bot_name)
} }
pub fn new_issue_success( pub fn new_issue_success(
@ -46,7 +46,7 @@ To close the issue, send an email to {local_part}+{password}+close@{domain}.
Please keep this email in order to be able to keep in touch with your issue. Please keep this email in order to be able to keep in touch with your issue.
This is an automated email from {bot_name} <{local_part}+help@{domain}>", title = title, password = password.to_string(), issue_id = issue_id, url = BASE_ISSUE_URL.replace("{base_url}", &conf.base_url).replace("{repo}", &conf.repo), local_part = &conf.local_part, domain = &conf.domain, bot_name = &conf.bot_name) This is an automated email from {bot_name} <{local_part}+help@{domain}>", title = title, password = password, issue_id = issue_id, url = BASE_ISSUE_URL.replace("{base_url}", &conf.base_url).replace("{repo}", &conf.repo), local_part = &conf.local_part, domain = &conf.domain, bot_name = &conf.bot_name)
} }
pub fn new_reply_failure(e: Error, conf: &Configuration) -> String { pub fn new_reply_failure(e: Error, conf: &Configuration) -> String {
@ -54,7 +54,7 @@ pub fn new_reply_failure(e: Error, conf: &Configuration) -> String {
Unfortunately we were not able to post your reply. The reason was: `{}`. Please contact the repository's owners for assistance. Unfortunately we were not able to post your reply. The reason was: `{}`. Please contact the repository's owners for assistance.
This is an automated email from {bot_name} <{local_part}+help@{domain}>", e.to_string(), local_part = &conf.local_part, domain = &conf.domain, bot_name = &conf.bot_name) This is an automated email from {bot_name} <{local_part}+help@{domain}>", e, local_part = &conf.local_part, domain = &conf.domain, bot_name = &conf.bot_name)
} }
pub fn new_reply_success( pub fn new_reply_success(
@ -79,7 +79,7 @@ To close the issue, send an email to {local_part}+{password}+close@{domain}.
Please keep this email in order to be able to keep in touch with your issue. Please keep this email in order to be able to keep in touch with your issue.
This is an automated email from {bot_name} <{local_part}+help@{domain}>", title = title, password = password.to_string(), issue_id = issue_id, url = BASE_ISSUE_URL.replace("{base_url}", &conf.base_url).replace("{repo}", &conf.repo), local_part = &conf.local_part, domain = &conf.domain, bot_name = &conf.bot_name) This is an automated email from {bot_name} <{local_part}+help@{domain}>", title = title, password = password, issue_id = issue_id, url = BASE_ISSUE_URL.replace("{base_url}", &conf.base_url).replace("{repo}", &conf.repo), local_part = &conf.local_part, domain = &conf.domain, bot_name = &conf.bot_name)
} else { } else {
format!("Hello, format!("Hello,
@ -95,7 +95,7 @@ To close the issue, send an email to {local_part}+{password}+close@{domain}.
Please keep this email in order to be able to keep in touch with your issue. Please keep this email in order to be able to keep in touch with your issue.
This is an automated email from {bot_name} <{local_part}+help@{domain}>", title = title, password = password.to_string(), issue_id = issue_id, url = BASE_ISSUE_URL.replace("{base_url}", &conf.base_url).replace("{repo}", &conf.repo), local_part = &conf.local_part, domain = &conf.domain, bot_name = &conf.bot_name) This is an automated email from {bot_name} <{local_part}+help@{domain}>", title = title, password = password, issue_id = issue_id, url = BASE_ISSUE_URL.replace("{base_url}", &conf.base_url).replace("{repo}", &conf.repo), local_part = &conf.local_part, domain = &conf.domain, bot_name = &conf.bot_name)
} }
} }
@ -124,7 +124,7 @@ pub fn close_failure(e: Error, conf: &Configuration) -> String {
Unfortunately we were not able to close this issue. The reason was: `{}`. Please contact the repository's owners for assistance. Unfortunately we were not able to close this issue. The reason was: `{}`. Please contact the repository's owners for assistance.
This is an automated email from {bot_name} <{local_part}+help@{domain}>", e.to_string(), local_part = &conf.local_part, domain = &conf.domain, bot_name = &conf.bot_name) This is an automated email from {bot_name} <{local_part}+help@{domain}>", e, local_part = &conf.local_part, domain = &conf.domain, bot_name = &conf.bot_name)
} }
pub fn invalid_request(conf: &Configuration) -> String { pub fn invalid_request(conf: &Configuration) -> String {
@ -170,7 +170,7 @@ To close the issue, send an email to {local_part}+{password}+close@{domain}.
Please keep this email in order to be able to keep in touch with your issue. Please keep this email in order to be able to keep in touch with your issue.
This is an automated email from {bot_name} <{local_part}+help@{domain}>", title = title, password = password.to_string(), issue_id = issue_id, url = BASE_ISSUE_URL.replace("{base_url}", &conf.base_url).replace("{repo}", &conf.repo), local_part = &conf.local_part, domain = &conf.domain, bot_name = &conf.bot_name, not = if is_subscribed { "" }else {"not "}, un = if is_subscribed { "un" } else { "" } ) This is an automated email from {bot_name} <{local_part}+help@{domain}>", title = title, password = password, issue_id = issue_id, url = BASE_ISSUE_URL.replace("{base_url}", &conf.base_url).replace("{repo}", &conf.repo), local_part = &conf.local_part, domain = &conf.domain, bot_name = &conf.bot_name, not = if is_subscribed { "" }else {"not "}, un = if is_subscribed { "un" } else { "" } )
} }
pub fn change_subscription_failure(is_subscribed: bool, conf: &Configuration) -> String { pub fn change_subscription_failure(is_subscribed: bool, conf: &Configuration) -> String {
@ -188,7 +188,7 @@ This is an automated email from {bot_name} <{local_part}+help@{domain}>",
} }
pub fn reply_update(issue: &Issue, conf: &Configuration, comments: Vec<String>) -> String { pub fn reply_update(issue: &Issue, conf: &Configuration, comments: Vec<String>) -> String {
assert!(comments.len() > 0); assert!(!comments.is_empty());
format!( format!(
"Hello, "Hello,