From bb718d51e0425472e07bec24bca92679c10ab4e5 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Sat, 24 Sep 2022 17:18:50 +0300 Subject: [PATCH] Read config/db paths from env, plus add docs --- README.md | 86 ++++++++++++------------------------------ docs/POSTFIX.md | 73 +++++++++++++++++++++++++++++++++++ docs/SCHEDULING.md | 34 +++++++++++++++++ docs/issue-bot.service | 14 +++++++ docs/issue-bot.timer | 29 ++++++++++++++ src/cron.rs | 16 ++++---- src/main.rs | 18 +++++---- 7 files changed, 193 insertions(+), 77 deletions(-) create mode 100644 docs/POSTFIX.md create mode 100644 docs/SCHEDULING.md create mode 100644 docs/issue-bot.service create mode 100644 docs/issue-bot.timer diff --git a/README.md b/README.md index f8d1d80..8661d68 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,26 @@ A bot to handle bug reports through mail, for gitea's issue tracker. +Expects configuration file path in environment variable `ISSUE_BOT_CONFIG`. If +it's not defined, default is `./config.toml` (current working directory). + +Expects database file path in environment variable `ISSUE_BOT_DB`. If it's not +defined, default is `./sqlite3.db` (current working directory). + +``` +issue-bot +``` + +by default expects to read a valid [RFC5322](https://www.rfc-editor.org/rfc/rfc5322) e-mail in valid utf-8 or valid ascii. + + +``` +issue-bot cron +``` + +Checks if there are new comments or other updates in the issues, and sends +emails to anyone subscribed. An example systemd service and timer file is provided in `docs/`. + ## Problem Users have to register to your gitea instance to file bugs. This is a deterrent. A mailing list requires less effort, but lacks bridging with an issue tracker. @@ -18,9 +38,9 @@ Spam? ## Configuration -The bot looks for a `config.toml` file in the same directory as the binary. The config needs the following values: +The config file must be valid TOML and needs the following values: -```text +```toml # the tag that prefixes email subjects eg "[issue-bot-issues] blah blah" tag = "issue-bot-issues" # the auth_token for the gitea's instance API @@ -38,68 +58,12 @@ bot_username = "issue_bot" mailer = "/usr/sbin/sendmail -t" ``` +Optionally, you can set `dry_run = true` to avoid any email/db update being performed in order to debug what would happen if you ran the `cron` command. + Setup your mail server to deliver mail with destination `{local_part}+tags@{domain}` to this binary. Simply call the binary and write the email in UTF-8 in the binary's standard input. -On postfix this can be done by creating a transport map and a pipe. A transport map is a file that tells postfix to send mails send to `{local_part}` to a specific program. The pipe will be this program. +For postfix setup see `docs/POSTFIX.md`. -Open `master.cf` and paste this line at the bottom: - -```text -issue_bot unix - n n - - pipe - user=issuebot directory=/path/to/binarydir/ argv=/path/to/binary -``` - -an example: - - -```text -issue_bot unix - n n - - pipe - user=issuebot directory=/home/issuebot/ argv=/home/issuebot/issue-bot -``` - -Then create your transport map: - -```text -{local_part}@{domain} issue_bot: -``` - -Notice the colon at the end. This means that it refers to a transfer, not an address. Save the file somewhere (eg `/etc/postfix/issue_transport`) and make it readable by postfix. Issue `postmap /etcpostfix/issue_transport`. Finally add the entry `hash:/etc/postfix/issue_transport` in your `transport_maps` and `local_recipient_maps` keys in `main.cf`. `postfix reload` to load the configuration changes. - -You will also need the following setting to allow tags in your recipient addresses: - -```text -recipient_delimiter = + -``` - -Setup a periodic check in your preferred task scheduler to run `issue_bot_bin cron` in order to fetch replies to issues. On systemd this can be done with timers. - - -### Troubleshooting -If you your email stops working or postfix doesn't pass mail to the bot, make sure you're not using a non-default setup like virtual mailboxes. In that case you have to add the transport along with the transports of your setup, whatever that be. - -If the e-mail gets to the binary and nothing happens, make sure: - -- the binary is executable and readable by the pipe's user -- the configuration file is in the same directory as the binary -- that in `master.cf` there are no `flags=` in the transport entry. The mail must be piped unaltered. -- your auth token works. You can check yourself by issuing requests to your API via cURL. There are examples here: https://docs.gitea.io/en-us/api-usage/ - -If commands (using +reply, +close etc) don't work, make sure you have added `recipient_delimiter = +` in your `main.cf` file. - -The bot's state is saved in a sqlite3 database in the same directory as the binary. You can view its data by using the `sqlite3` cli tool: - -```shell -root# sqlite3 /home/issuebot/sqlite3.db -SQLite version ****** ********** ******** -Enter ".help" for usage hints. -sqlite> .tables -issue -sqlite> select * from issue; -1|Name |1F:|2019-09-29T12:20:21.658495173Z|0|1|issue title|"2019-09-29T15:20:21+03:00" -2|Name |{^D0u|2019-09-29T12:23:48.291970808Z|0|1|issue title#2|"2019-09-29T15:23:48+03:00" -3|Name |Gd)i]|2019-09-29T12:24:31.414792595Z|0|1|issue title again|"2019-09-29T15:26:53+03:00" -4|Name |$3fBוv|2019-09-29T12:28:21.187425505Z|1|1|many issues|"2019-09-29T15:28:21+03:00" -``` ## Demo My email: diff --git a/docs/POSTFIX.md b/docs/POSTFIX.md new file mode 100644 index 0000000..a810d04 --- /dev/null +++ b/docs/POSTFIX.md @@ -0,0 +1,73 @@ +# issue-bot with postfix + +Setup your mail server to deliver mail with destination `{local_part}+tags@{domain}` to this binary. Simply call the binary and write the email in UTF-8 in the binary's standard input. + +On postfix this can be done by creating a transport map and a pipe. A transport map is a file that tells postfix to send mails send to `{local_part}` to a specific program. The pipe will be this program. + +**BEWARE**: If `issue-bot` needs to read its configuration file and database file paths from environment variables, create a wrapper script and call that from postfix instead of going through the complicated trouble of setting up the exported environment (see postfix manual pages `master(t)` and `pipe(8)`) + +```shell +/bin/sh + +export ISSUE_BOT_CONFIG=_ +export ISSUE_BOT_DB=_ +/path/to/issue-bot +``` + +Open `master.cf` and paste this line at the bottom: + +```text +issue_bot unix - n n - - pipe + user=issuebot directory=/path/to/binarydir/ argv=/path/to/binary +``` + +an example: + +```text +issue_bot unix - n n - - pipe + user=issuebot directory=/home/issuebot/ argv=/home/issuebot/issue-bot +``` + +Then create your transport map: + +```text +{local_part}@{domain} issue_bot: +``` + +Notice the colon at the end. This means that it refers to a transfer, not an address. Save the file somewhere (eg `/etc/postfix/issue_transport`) and make it readable by postfix. Issue `postmap /etcpostfix/issue_transport`. Finally add the entry `hash:/etc/postfix/issue_transport` in your `transport_maps` and `local_recipient_maps` keys in `main.cf`. `postfix reload` to load the configuration changes. + +You will also need the following setting to allow tags in your recipient addresses: + +```text +recipient_delimiter = + +``` + +Setup a periodic check in your preferred task scheduler to run `issue_bot_bin cron` in order to fetch replies to issues. On systemd this can be done with timers. + + +### Troubleshooting +If you your email stops working or postfix doesn't pass mail to the bot, make sure you're not using a non-default setup like virtual mailboxes. In that case you have to add the transport along with the transports of your setup, whatever that be. + +If the e-mail gets to the binary and nothing happens, make sure: + +- the binary is executable and readable by the pipe's user +- the configuration file is in the same directory as the binary +- that in `master.cf` there are no `flags=` in the transport entry. The mail must be piped unaltered. +- your auth token works. You can check yourself by issuing requests to your API via cURL. There are examples here: https://docs.gitea.io/en-us/api-usage/ + +If commands (using +reply, +close etc) don't work, make sure you have added `recipient_delimiter = +` in your `main.cf` file. + +The bot's state is saved in a sqlite3 database in the same directory as the binary. You can view its data by using the `sqlite3` cli tool: + +```shell +root# sqlite3 /home/issuebot/sqlite3.db +SQLite version ****** ********** ******** +Enter ".help" for usage hints. +sqlite> .tables +issue +sqlite> select * from issue; +1|Name |1F:|2019-09-29T12:20:21.658495173Z|0|1|issue title|"2019-09-29T15:20:21+03:00" +2|Name |{^D0u|2019-09-29T12:23:48.291970808Z|0|1|issue title#2|"2019-09-29T15:23:48+03:00" +3|Name |Gd)i]|2019-09-29T12:24:31.414792595Z|0|1|issue title again|"2019-09-29T15:26:53+03:00" +4|Name |$3fBוv|2019-09-29T12:28:21.187425505Z|1|1|many issues|"2019-09-29T15:28:21+03:00" +``` diff --git a/docs/SCHEDULING.md b/docs/SCHEDULING.md new file mode 100644 index 0000000..02c1cb8 --- /dev/null +++ b/docs/SCHEDULING.md @@ -0,0 +1,34 @@ +# issue-bot scheduled jobs + +You can set up scheduled jobs by configuring `crontab` to run `issue-bot cron` whenever you want. For the more complicated but more reliable systemd setup, two example files are included in this directory: a service unit file and a timer unit file. The service unit executes once, and the timer unit is responsible for calling the service at the intervals you set. + +Copy the example files somewhere else and edit them with your own values. + +You can put `dry_run = true` in the config file to check it works without making changes or sending any mail. Also, backup your database if needed. + +```shell +systemctl --user enable issue-bot.service +``` + +You can do a test run with + +```shell +systemctl --user start issue-bot.service +``` + +Now you enable/activate the timer. + +```shell +systemctl --user enable issue-bot.timer +``` + +```shell +systemctl --user start issue-bot.timer +``` + + +Monitor the service status: + +```shell +systemctl --user status issue-bot +``` diff --git a/docs/issue-bot.service b/docs/issue-bot.service new file mode 100644 index 0000000..70774b5 --- /dev/null +++ b/docs/issue-bot.service @@ -0,0 +1,14 @@ +[Unit] +Description=issue-bot cron +RefuseManualStart=no # Allow manual starts +RefuseManualStop=no # Allow manual stops + +[Service] +Type=simple +ExecStart=/path/to/issue-bot cron +Environment=ISSUE_BOT_CONFIG=/a/b/c/d.toml +Environment=ISSUE_BOT_DB=/a/b/c/sqlite3.db + + +[Install] +WantedBy=default.target diff --git a/docs/issue-bot.timer b/docs/issue-bot.timer new file mode 100644 index 0000000..fa97eb5 --- /dev/null +++ b/docs/issue-bot.timer @@ -0,0 +1,29 @@ +[Unit] +Description=issue-bot crons +RefuseManualStart=no # Allow manual starts +RefuseManualStop=no # Allow manual stops + +[Timer] +#Execute job if it missed a run due to machine being off +Persistent=true +#Run 120 seconds after boot for the first time +OnBootSec=120 +#Run every 5 minutes thereafter +OnUnitActiveSec=300 +#File describing job to execute +Unit=issue-bot.service + + +## more complicated examples: +# # run on the minute of every minute every hour of every day +# OnCalendar=*-*-* *:*:00 +# # run on the hour of every hour of every day +# OnCalendar=*-*-* *:00:00 +# # run every day +# OnCalendar=*-*-* 00:00:00 +# # run 11:12:13 of the first or fifth day of any month of the year +# # 2012, but only if that day is a Thursday or Friday +# OnCalendar=Thu,Fri 2012-*-1,5 11:12:13 + +[Install] +WantedBy=timers.target diff --git a/src/cron.rs b/src/cron.rs index 0c222fa..3182b1d 100644 --- a/src/cron.rs +++ b/src/cron.rs @@ -122,15 +122,13 @@ pub fn check(conn: Connection, conf: Configuration) -> Result<()> { 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 - ); - } + 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::>>()?; Ok(()) } diff --git a/src/main.rs b/src/main.rs index de1aae9..d9c84e0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -300,7 +300,9 @@ fn run_request(conn: Connection, conf: Configuration) -> Result<()> { } fn run_app() -> Result<()> { - let mut file = std::fs::File::open("./config.toml")?; + let conf_path = + std::env::var("ISSUE_BOT_CONFIG").unwrap_or_else(|_| "./config.toml".to_string()); + let mut file = std::fs::File::open(&conf_path)?; let args = std::env::args().skip(1).collect::>(); let perform_cron: bool; if args.len() > 1 { @@ -339,11 +341,11 @@ fn run_app() -> Result<()> { * * */ - let db_path = "./sqlite3.db"; - let conn = Connection::open(db_path)?; + let db_path = std::env::var("ISSUE_BOT_DB").unwrap_or_else(|_| "./sqlite3.db".to_string()); + let conn = Connection::open(&db_path)?; - conn.execute( - "CREATE TABLE IF NOT EXISTS issue ( + conn.execute_batch( + r##"CREATE TABLE IF NOT EXISTS issue ( id INTEGER PRIMARY KEY, submitter TEXT NOT NULL, password BLOB, @@ -352,8 +354,10 @@ fn run_app() -> Result<()> { subscribed BOOLEAN, title TEXT NOT NULL, last_update TEXT - )", - [], + ); + + UPDATE issue SET last_update = replace(last_update, '"', ''); + "##, )?; if perform_cron {