A bot that allows users to post issues on a Gitea instance without registering simply by acting as a medium.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

316 lines
12 KiB

2 years ago
  1. use melib::Envelope;
  2. use rusqlite::types::ToSql;
  3. use rusqlite::{Connection, NO_PARAMS};
  4. use std::io::{stdin, Read};
  5. use time::Timespec;
  6. use uuid::Uuid;
  7. mod error;
  8. use error::*;
  9. mod api;
  10. mod conf;
  11. use conf::*;
  12. mod cron;
  13. mod templates;
  14. type Password = Uuid;
  15. static PASSWORD_COMMANDS: &'static [&'static str] = &["reply", "unsubscribe", "subscribe", "close"];
  16. use melib::{email::parser, Address};
  17. #[derive(Debug)]
  18. pub struct Issue {
  19. id: i64,
  20. submitter: Address,
  21. password: Password,
  22. time_created: Timespec,
  23. anonymous: bool,
  24. subscribed: bool,
  25. title: String,
  26. last_update: String,
  27. }
  28. fn new_address(s: &str) -> Address {
  29. parser::address(s.as_bytes()).to_full_result().unwrap()
  30. }
  31. pub fn send_mail(d: melib::email::Draft, conf: &Config) {
  32. use std::io::Write;
  33. use std::process::Stdio;
  34. let parts = conf.mailer.split_whitespace().collect::<Vec<&str>>();
  35. let (cmd, args) = (parts[0], &parts[1..]);
  36. let mut mailer = std::process::Command::new(cmd)
  37. .args(args)
  38. .stdin(Stdio::piped())
  39. .stdout(Stdio::inherit())
  40. .spawn()
  41. .expect("Failed to start mailer command");
  42. {
  43. let stdin = mailer.stdin.as_mut().expect("failed to open stdin");
  44. let draft = d.finalise().unwrap();
  45. stdin
  46. .write_all(draft.as_bytes())
  47. .expect("Failed to write to stdin");
  48. }
  49. let output = mailer.wait().expect("Failed to wait on mailer");
  50. if !output.success() {
  51. // TODO: commit to database queue
  52. eprintln!("mailer fail");
  53. std::process::exit(1);
  54. }
  55. }
  56. fn main() -> std::result::Result<(), std::io::Error> {
  57. let mut file = std::fs::File::open("./config.toml")?;
  58. let args = std::env::args().skip(1).collect::<Vec<String>>();
  59. let perform_cron: bool;
  60. if args.len() > 1 {
  61. eprintln!("Too many arguments.");
  62. std::process::exit(1);
  63. } else if args == &["cron"] {
  64. perform_cron = true;
  65. } else if args.is_empty() {
  66. perform_cron = false;
  67. } else {
  68. eprintln!("Usage: issue_bot [cron]");
  69. std::process::exit(1);
  70. }
  71. let mut contents = String::new();
  72. file.read_to_string(&mut contents)?;
  73. let conf: Config = toml::from_str(&contents).unwrap();
  74. /* - read mail from stdin
  75. * - decide which case this mail falls to
  76. * a) error/junk
  77. * reply with error
  78. * b) new issue
  79. * - assign random id to issue
  80. * - reply to sender with id
  81. * - save id to sqlite3
  82. * - post issue
  83. * c) reply
  84. * d) close
  85. *
  86. *
  87. */
  88. let db_path = "./sqlite3.db";
  89. let conn = Connection::open(db_path).unwrap();
  90. conn.execute(
  91. "CREATE TABLE IF NOT EXISTS issue (
  92. id INTEGER PRIMARY KEY,
  93. submitter TEXT NOT NULL,
  94. password BLOB,
  95. time_created TEXT NOT NULL,
  96. anonymous BOOLEAN,
  97. subscribed BOOLEAN,
  98. title TEXT NOT NULL,
  99. last_update TEXT
  100. )",
  101. NO_PARAMS,
  102. )
  103. .unwrap();
  104. if perform_cron {
  105. cron::check(conn, conf);
  106. return Ok(());
  107. }
  108. let mut new_message_raw = String::new();
  109. stdin().lock().read_to_string(&mut new_message_raw).unwrap();
  110. let envelope = Envelope::from_bytes(new_message_raw.as_bytes(), None);
  111. if let Ok(envelope) = envelope {
  112. let mut reply = melib::Draft::new_reply(&envelope, &[]);
  113. reply.headers_mut().insert(
  114. "From".to_string(),
  115. format!(
  116. "{local_part}@{domain}",
  117. local_part = &conf.local_part,
  118. domain = &conf.domain
  119. ),
  120. );
  121. let tags: Vec<String> = envelope.to()[0].get_tags('+');
  122. match tags.as_slice() {
  123. s if s.is_empty() || s == &["anonymous"] => {
  124. /* Assign new issue */
  125. let subject = envelope.subject().to_string();
  126. let body = envelope.body_bytes(new_message_raw.as_bytes()).text();
  127. let from = envelope.from()[0].clone();
  128. let mut reply = melib::Draft::new_reply(&envelope, &[]);
  129. let anonymous = !tags.is_empty();
  130. reply.headers_mut().insert(
  131. "From".to_string(),
  132. format!(
  133. "{local_part}@{domain}",
  134. local_part = &conf.local_part,
  135. domain = &conf.domain
  136. ),
  137. );
  138. match api::new_issue(&conn, subject.clone(), body, anonymous, from, &conf) {
  139. Ok((password, issue_id)) => {
  140. reply.headers_mut().insert(
  141. "Subject".to_string(),
  142. format!(
  143. "[{tag}] Issue `{}` successfully created",
  144. &subject,
  145. tag = &conf.tag
  146. ),
  147. );
  148. reply.set_body(templates::new_issue_success(
  149. subject, password, issue_id, &conf,
  150. ));
  151. send_mail(reply, &conf);
  152. }
  153. Err(e) => {
  154. reply.headers_mut().insert(
  155. "Subject".to_string(),
  156. format!(
  157. "[{tag}] Issue `{}` could not be created",
  158. &subject,
  159. tag = &conf.tag
  160. ),
  161. );
  162. reply.set_body(templates::new_issue_failure(e, &conf));
  163. send_mail(reply, &conf);
  164. }
  165. }
  166. }
  167. &[ref p, ref cmd]
  168. if Password::parse_str(p).is_ok() && PASSWORD_COMMANDS.contains(&cmd.as_str()) =>
  169. {
  170. let p = Password::parse_str(p).unwrap();
  171. match cmd.as_str() {
  172. "reply" => {
  173. let body = envelope.body_bytes(new_message_raw.as_bytes()).text();
  174. let from = envelope.from()[0].clone();
  175. match api::new_reply(&conn, body, p, from, &conf) {
  176. Ok((title, issue_id, is_subscribed)) => {
  177. reply.headers_mut().insert(
  178. "Subject".to_string(),
  179. format!(
  180. "[{tag}] Your reply on issue `{}` has been posted",
  181. &title,
  182. tag = &conf.tag,
  183. ),
  184. );
  185. reply.set_body(templates::new_reply_success(
  186. title,
  187. p,
  188. issue_id,
  189. is_subscribed,
  190. &conf,
  191. ));
  192. send_mail(reply, &conf);
  193. }
  194. Err(e) => {
  195. reply.headers_mut().insert(
  196. "Subject".to_string(),
  197. format!(
  198. "[{tag}] Your reply could not be created",
  199. tag = &conf.tag,
  200. ),
  201. );
  202. reply.set_body(templates::new_reply_failure(e, &conf));
  203. send_mail(reply, &conf);
  204. }
  205. }
  206. }
  207. "close" => match api::close(&conn, p, &conf) {
  208. Ok((title, issue_id, _)) => {
  209. reply.headers_mut().insert(
  210. "Subject".to_string(),
  211. format!(
  212. "[{tag}] issue `{}` has been closed",
  213. &title,
  214. tag = &conf.tag
  215. ),
  216. );
  217. reply.set_body(templates::close_success(title, issue_id, &conf));
  218. send_mail(reply, &conf);
  219. }
  220. Err(e) => {
  221. reply.headers_mut().insert(
  222. "Subject".to_string(),
  223. format!("[{tag}] issue could not be closed", tag = &conf.tag,),
  224. );
  225. reply.set_body(templates::close_failure(e, &conf));
  226. send_mail(reply, &conf);
  227. }
  228. },
  229. "unsubscribe" => match api::change_subscription(&conn, p, false) {
  230. Ok((title, issue_id, _)) => {
  231. reply.headers_mut().insert(
  232. "Subject".to_string(),
  233. format!(
  234. "[{tag}] subscription removal to `{}` successful",
  235. &title,
  236. tag = &conf.tag
  237. ),
  238. );
  239. reply.set_body(templates::change_subscription_success(
  240. title, p, issue_id, false, &conf,
  241. ));
  242. send_mail(reply, &conf);
  243. }
  244. Err(e) => {
  245. eprintln!("error: {}", e.to_string());
  246. reply.headers_mut().insert(
  247. "Subject".to_string(),
  248. format!("[{tag}] could not unsubscribe", tag = &conf.tag,),
  249. );
  250. reply.set_body(templates::change_subscription_failure(false, &conf));
  251. send_mail(reply, &conf);
  252. }
  253. },
  254. "subscribe" => match api::change_subscription(&conn, p, true) {
  255. Ok((title, issue_id, _)) => {
  256. reply.headers_mut().insert(
  257. "Subject".to_string(),
  258. format!(
  259. "[{tag}] subscription to `{}` successful",
  260. &title,
  261. tag = &conf.tag
  262. ),
  263. );
  264. reply.set_body(templates::change_subscription_success(
  265. title, p, issue_id, true, &conf,
  266. ));
  267. send_mail(reply, &conf);
  268. }
  269. Err(e) => {
  270. eprintln!("error: {}", e.to_string());
  271. reply.headers_mut().insert(
  272. "Subject".to_string(),
  273. format!("[{tag}] could not subscribe", tag = &conf.tag,),
  274. );
  275. reply.set_body(templates::change_subscription_failure(true, &conf));
  276. send_mail(reply, &conf);
  277. }
  278. },
  279. other => {
  280. reply.headers_mut().insert(
  281. "Subject".to_string(),
  282. format!("[{tag}] invalid action: `{}`", &other, tag = &conf.tag),
  283. );
  284. reply.set_body(templates::invalid_request(&conf));
  285. send_mail(reply, &conf);
  286. }
  287. }
  288. }
  289. other => {
  290. reply.headers_mut().insert(
  291. "Subject".to_string(),
  292. format!("[{tag}] invalid request", tag = &conf.tag),
  293. );
  294. reply.set_body(templates::invalid_request(&conf));
  295. send_mail(reply, &conf);
  296. println!("error: {:?}", other);
  297. }
  298. }
  299. }
  300. Ok(())
  301. }