Remove unwraps, revamp code
parent
6aa2f12e2c
commit
908493327f
|
@ -1,2 +1,7 @@
|
||||||
/target
|
/target
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
|
.*.swp
|
||||||
|
*.swp
|
||||||
|
config.toml
|
||||||
|
issue-bot.log
|
||||||
|
sqlite3.db
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
23
Cargo.toml
23
Cargo.toml
|
@ -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 = []
|
||||||
|
|
87
src/api.rs
87
src/api.rs
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
24
src/conf.rs
24
src/conf.rs
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
181
src/cron.rs
181
src/cron.rs
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
95
src/main.rs
95
src/main.rs
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue