From f8421e4577cb66e870416132c34296a5658b2148 Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Wed, 13 Aug 2025 21:40:18 -0400 Subject: [PATCH 01/18] chore: dev version bump --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5be59cf..0ba1a0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -129,7 +129,7 @@ checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "bazooka-bot" -version = "0.6.3" +version = "0.6.4-dev" dependencies = [ "anyhow", "human-time", diff --git a/Cargo.toml b/Cargo.toml index a408e7b..72ff122 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bazooka-bot" -version = "0.6.3" +version = "0.6.4-dev" edition = "2024" publish = false From b9f8ecdd062d7d25814032593f1b72b274a95813 Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Wed, 13 Aug 2025 21:52:18 -0400 Subject: [PATCH 02/18] docs: update objectives --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 02f285a..3760883 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,8 @@ It should be scheduled manually each season by an officer. # Nice to have -- [ ] Add ability for owner to download the data files to facilitate testing before uploading a version +- [ ] Add ability for owner to download the data +- [ ] Add ability for owner to replace the data (intended to be from something downloaded previously) - [ ] Setup deploy from CI - https://github.com/shuttle-hq/deploy-action - [ ] Setup test deployment on shuttle (idle time of about 15 minutes should be good) - [ ] [Naming](https://docs.shuttle.rs/configuration/project-name) From 00830cab8880cd344230a376fad1280fec4d8584 Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Wed, 13 Aug 2025 21:52:37 -0400 Subject: [PATCH 03/18] docs: check of completed task --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3760883..668eb0e 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ It should be scheduled manually each season by an officer. - [ ] Add ability for owner to download the data - [ ] Add ability for owner to replace the data (intended to be from something downloaded previously) -- [ ] Setup deploy from CI - https://github.com/shuttle-hq/deploy-action +- [x] Setup deploy from CI - https://github.com/shuttle-hq/deploy-action - [ ] Setup test deployment on shuttle (idle time of about 15 minutes should be good) - [ ] [Naming](https://docs.shuttle.rs/configuration/project-name) - [ ] From release notes from 0.40.0 From de3a6495ec1b865647e24e109ad9e87dc01f76a0 Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Wed, 13 Aug 2025 21:53:20 -0400 Subject: [PATCH 04/18] docs: remove objective because only 1 allowed Shuttle now only allows one project on the free tier --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 668eb0e..0a0d2e0 100644 --- a/README.md +++ b/README.md @@ -56,10 +56,6 @@ It should be scheduled manually each season by an officer. - [ ] Add ability for owner to download the data - [ ] Add ability for owner to replace the data (intended to be from something downloaded previously) - [x] Setup deploy from CI - https://github.com/shuttle-hq/deploy-action -- [ ] Setup test deployment on shuttle (idle time of about 15 minutes should be good) - - [ ] [Naming](https://docs.shuttle.rs/configuration/project-name) - - [ ] From release notes from 0.40.0 - - Added a --secrets arg to the run and deploy commands that allows you to use a different secrets file than the default - [ ] Add message ID to the trace at ingress - [x] Sanitize input for markdown like `**` for example rn causes problems with bolding the ideas - [ ] Add web page (with info and to wake up bot, would help for test bot that would go to sleep) https://docs.shuttle.rs/templates/tutorials/custom-service From 8dc947d59ee93917d89898b75d0b78a309dfbbeb Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Wed, 13 Aug 2025 22:01:11 -0400 Subject: [PATCH 05/18] docs: update completed date in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0a0d2e0..2bbabbb 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ This is the rust replacement for the bot [originally](https://github.com/fone-gi -Note: Some checked off items my not be complete only started but at the time of writing (2024-04-07) all are completed. +Note: Some checked off items my not be complete only started but at the time of writing (2025-08-13) all are completed. If they are not there will be TODO's in the code itself. The check off only denotes the start of the feature as then the TODOs are moved into the code as maintaining two places is not sustainable. From 0959323fa1d8ccedc832131517208d316af9df3b Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Wed, 13 Aug 2025 22:04:10 -0400 Subject: [PATCH 06/18] style: sort commands alphabetically --- src/commands.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index be38fad..186b16e 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -43,13 +43,13 @@ async fn call_to_parent_command(ctx: Context<'_>) -> anyhow::Result<()> { pub fn commands_list() -> Vec> { vec![ - ping(), - help(), general::version(), - uptime(), - unranked(), - schedule(), + help(), + ping(), register(), + schedule(), + unranked(), + uptime(), ] } From 01b1d3ad720f063f0e11560b475a882f861fe389 Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Wed, 13 Aug 2025 22:47:57 -0400 Subject: [PATCH 07/18] feat: create stub for heartbeat --- README.md | 2 ++ src/heartbeat.rs | 17 +++++++++++++++++ src/lib.rs | 1 + src/main.rs | 15 +++++++++++---- 4 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 src/heartbeat.rs diff --git a/README.md b/README.md index 2bbabbb..5e50b65 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,8 @@ It should be scheduled manually each season by an officer. # Nice to have +- [x] Track how long the bot has been up or down +- [ ] Keep history of downtime - [ ] Add ability for owner to download the data - [ ] Add ability for owner to replace the data (intended to be from something downloaded previously) - [x] Setup deploy from CI - https://github.com/shuttle-hq/deploy-action diff --git a/src/heartbeat.rs b/src/heartbeat.rs new file mode 100644 index 0000000..206a2b2 --- /dev/null +++ b/src/heartbeat.rs @@ -0,0 +1,17 @@ +use tracing::info; + +pub fn start_heartbeat(db_pool: sqlx::PgPool) { + shuttle_runtime::tokio::spawn(async move { + info!("Heartbeat started"); + loop { + // TODO 1: Save time of last heartbeat to DB + println!("beat"); + shuttle_runtime::tokio::time::sleep(std::time::Duration::from_secs(6)).await; + } + }); +} + +pub fn time_since_last_heartbeat(db_pool: sqlx::PgPool) -> String { + // TODO 1: Get last heartbeat from DB + "First run".to_string() +} diff --git a/src/lib.rs b/src/lib.rs index 71f40ea..20ef31d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,6 +21,7 @@ pub use self::{ mod commands; mod config; +pub mod heartbeat; mod model; mod secrets; diff --git a/src/main.rs b/src/main.rs index f9df4ad..af9c0b5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,7 @@ use anyhow::Context as _; -use bazooka_bot::{Data, SharedConfig, StartupConfig, commands_list, get_secret_discord_token}; +use bazooka_bot::{ + Data, SharedConfig, StartupConfig, commands_list, get_secret_discord_token, heartbeat, +}; use poise::serenity_prelude::{ClientBuilder, GatewayIntents}; use secrecy::ExposeSecret; use shuttle_runtime::SecretStore; @@ -40,8 +42,8 @@ async fn main( let discord_token = get_secret_discord_token(&secret_store)?; let startup_config = StartupConfig::try_new(&secret_store).context("failed to create setup config")?; - let shared_config = - SharedConfig::try_new(&secret_store, db_pool).context("failed to created shared_config")?; + let shared_config = SharedConfig::try_new(&secret_store, db_pool.clone()) + .context("failed to created shared_config")?; let framework = poise::Framework::builder() .options(poise::FrameworkOptions { @@ -81,7 +83,11 @@ async fn main( } else { error!("Development run detected but no guild ID found so slash commands NOT registered"); } - let connect_msg = format!("{} is connected! Version: {}", ready.user.name, version!()); + let connect_msg = format!( + "{} is connected! Version: {}, Downtime: {}", + ready.user.name, version!(), + heartbeat::time_since_last_heartbeat(db_pool.clone()), + ); info!("{connect_msg}"); if let Some(channel) = shared_config.channel_bot_status{ channel.say(ctx, connect_msg).await?; @@ -89,6 +95,7 @@ async fn main( warn!("Not sending connection notification because channel_bot_status not set"); } let data = Data::new(shared_config, ctx.clone()).await; + heartbeat::start_heartbeat(db_pool); info!("END OF SETUP CLOSURE"); Ok(data) }) From e0ba32c9a2ce0ba0e1b714c90818f2bac83fb62d Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Wed, 13 Aug 2025 23:26:31 -0400 Subject: [PATCH 08/18] refactor: extract db save function --- src/config.rs | 28 +--------------------------- src/db.rs | 31 +++++++++++++++++++++++++++++++ src/lib.rs | 1 + 3 files changed, 33 insertions(+), 27 deletions(-) create mode 100644 src/db.rs diff --git a/src/config.rs b/src/config.rs index 7858283..caa1ecd 100644 --- a/src/config.rs +++ b/src/config.rs @@ -72,33 +72,7 @@ impl SharedConfig { let key = key.to_string(); let value = serde_json::to_string(value).context("failed to convert to json")?; tokio::spawn(async move { - let query = sqlx::query!( - "\ - INSERT INTO kv_store (id, content) - VALUES ($1, $2) - ON CONFLICT(id) - DO UPDATE SET - content = EXCLUDED.content;", - key, - value - ); - match query.execute(&pool).await { - Ok(query_result) => { - if query_result.rows_affected() == 1 { - info!("Save completed for key: {key}"); - } else { - error!( - ?key, - "Expected 1 row to be affected by save but got: {}", - query_result.rows_affected() - ) - } - } - Err(err_msg) => error!( - ?err_msg, - "Failed to save content for key: {key} to kv store" - ), - } + crate::db::save_kv(&pool, key, value).await; }); Ok(()) } diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..437fcaa --- /dev/null +++ b/src/db.rs @@ -0,0 +1,31 @@ +use tracing::{error, info}; + +pub async fn save_kv(pool: &sqlx::PgPool, key: String, value: String) { + let query = sqlx::query!( + "\ +INSERT INTO kv_store (id, content) + VALUES ($1, $2) + ON CONFLICT(id) + DO UPDATE SET + content = EXCLUDED.content;", + key, + value + ); + match query.execute(pool).await { + Ok(query_result) => { + if query_result.rows_affected() == 1 { + info!("Save completed for key: {key}"); + } else { + error!( + ?key, + "Expected 1 row to be affected by save but got: {}", + query_result.rows_affected() + ) + } + } + Err(err_msg) => error!( + ?err_msg, + "Failed to save content for key: {key} to kv store" + ), + } +} diff --git a/src/lib.rs b/src/lib.rs index 20ef31d..f7a2bf2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,6 +21,7 @@ pub use self::{ mod commands; mod config; +mod db; pub mod heartbeat; mod model; mod secrets; From 45bea649339c6b68118f7f4a923e7300a1f44599 Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Wed, 13 Aug 2025 23:41:21 -0400 Subject: [PATCH 09/18] refactor: extract db load function --- src/config.rs | 28 +++++++++------------------- src/db.rs | 17 +++++++++++++++++ 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/src/config.rs b/src/config.rs index caa1ecd..1650549 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,7 +3,7 @@ use std::{collections::HashSet, fmt::Debug, time::Instant}; use anyhow::Context as _; use poise::serenity_prelude::{ChannelId, GuildId, RoleId, UserId}; use shuttle_runtime::{SecretStore, tokio}; -use tracing::{error, info}; +use tracing::error; use crate::secrets::KeyName; @@ -81,27 +81,17 @@ impl SharedConfig { &self, key: &str, ) -> T { - let record_opt = match sqlx::query!("SELECT content FROM kv_store where id = $1", key) - .fetch_optional(&self.db_pool) - .await - { - Ok(content) => content, - Err(err_msg) => { - error!(?err_msg, "Failed to get content for key: {key}"); - None - } - }; - let record = match record_opt { - Some(record) => record, - None => { - info!("No content found in DB for key: {key}"); - return T::default(); - } + let Some(content) = crate::db::load_kv(&self.db_pool, key).await else { + return T::default(); }; - match serde_json::from_str(&record.content) { + match serde_json::from_str(&content) { Ok(x) => x, Err(err_msg) => { - error!(?err_msg, ?record.content, "Failed to convert content extracted from the database"); + error!( + ?err_msg, + ?content, + "Failed to convert content extracted from the database" + ); T::default() } } diff --git a/src/db.rs b/src/db.rs index 437fcaa..eb487cb 100644 --- a/src/db.rs +++ b/src/db.rs @@ -29,3 +29,20 @@ INSERT INTO kv_store (id, content) ), } } + +pub async fn load_kv(pool: &sqlx::PgPool, key: &str) -> Option { + match sqlx::query!("SELECT content FROM kv_store where id = $1", key) + .fetch_optional(pool) + .await + { + Ok(Some(record)) => Some(record.content), + Ok(None) => { + info!("No content found in DB for key: {key}"); + None + } + Err(err_msg) => { + error!(?err_msg, "Failed to get content for key: {key}"); + None + } + } +} From bdfc4f9daa7bb7e52c028dddf3cfa3a90b64d254 Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Wed, 13 Aug 2025 23:43:23 -0400 Subject: [PATCH 10/18] chore: update prepared sql statements --- ...e23c54398267a395e700f4af69d5f396e208f07d5.json | 15 +++++++++++++++ ...0f8bdf6e566cd81c6270a1d51988c19a4b205bdc7.json | 15 --------------- 2 files changed, 15 insertions(+), 15 deletions(-) create mode 100644 .sqlx/query-2c2b2e7ab1850b7c4bf786be23c54398267a395e700f4af69d5f396e208f07d5.json delete mode 100644 .sqlx/query-f79d485f20e22f0b3abf69d0f8bdf6e566cd81c6270a1d51988c19a4b205bdc7.json diff --git a/.sqlx/query-2c2b2e7ab1850b7c4bf786be23c54398267a395e700f4af69d5f396e208f07d5.json b/.sqlx/query-2c2b2e7ab1850b7c4bf786be23c54398267a395e700f4af69d5f396e208f07d5.json new file mode 100644 index 0000000..20eeafa --- /dev/null +++ b/.sqlx/query-2c2b2e7ab1850b7c4bf786be23c54398267a395e700f4af69d5f396e208f07d5.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO kv_store (id, content)\n VALUES ($1, $2)\n ON CONFLICT(id)\n DO UPDATE SET\n content = EXCLUDED.content;", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "2c2b2e7ab1850b7c4bf786be23c54398267a395e700f4af69d5f396e208f07d5" +} diff --git a/.sqlx/query-f79d485f20e22f0b3abf69d0f8bdf6e566cd81c6270a1d51988c19a4b205bdc7.json b/.sqlx/query-f79d485f20e22f0b3abf69d0f8bdf6e566cd81c6270a1d51988c19a4b205bdc7.json deleted file mode 100644 index a605f79..0000000 --- a/.sqlx/query-f79d485f20e22f0b3abf69d0f8bdf6e566cd81c6270a1d51988c19a4b205bdc7.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO kv_store (id, content)\n VALUES ($1, $2)\n ON CONFLICT(id)\n DO UPDATE SET\n content = EXCLUDED.content;", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [] - }, - "hash": "f79d485f20e22f0b3abf69d0f8bdf6e566cd81c6270a1d51988c19a4b205bdc7" -} From ae2dc6630ab3e316593a77a0a8819fff5e42873d Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Wed, 13 Aug 2025 23:47:25 -0400 Subject: [PATCH 11/18] feat: change display to support more info --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index af9c0b5..c1529cf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -84,7 +84,7 @@ async fn main( error!("Development run detected but no guild ID found so slash commands NOT registered"); } let connect_msg = format!( - "{} is connected! Version: {}, Downtime: {}", + "{} is connected! Version: {} [{}]", ready.user.name, version!(), heartbeat::time_since_last_heartbeat(db_pool.clone()), ); From 26b9cd43a4eefbfa187f2a52e1790b9101e1156e Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Wed, 13 Aug 2025 23:48:40 -0400 Subject: [PATCH 12/18] feat: remove unnecessary logging --- src/model/schedule.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/model/schedule.rs b/src/model/schedule.rs index b7d51fd..b16e370 100644 --- a/src/model/schedule.rs +++ b/src/model/schedule.rs @@ -106,7 +106,6 @@ impl ScheduledTask { let seconds_since_epoch: i32 = seconds_since_epoch .try_into() .context("failed to convert system time as seconds since epoch into i32")?; - info!(seconds_since_epoch); let timestamp_now = UnixTimestamp::new(seconds_since_epoch); info!("timestamp_now={timestamp_now:?}"); let seconds_to_desired = self.desired_execution_timestamp.0 - timestamp_now.0; From f620954bd931bf9dbff24030a4f5dcdca0911541 Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Wed, 13 Aug 2025 23:51:13 -0400 Subject: [PATCH 13/18] feat: create a now function on timestamp --- src/model/schedule.rs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/model/schedule.rs b/src/model/schedule.rs index b16e370..ee441d7 100644 --- a/src/model/schedule.rs +++ b/src/model/schedule.rs @@ -18,6 +18,17 @@ impl UnixTimestamp { pub fn new(value: i32) -> Self { Self(value) } + + pub fn now() -> anyhow::Result { + let seconds_since_epoch = UNIX_EPOCH + .elapsed() + .context("failed to get timestamp. System date before Unix Epoch?")? + .as_secs(); + let seconds_since_epoch: i32 = seconds_since_epoch + .try_into() + .context("failed to convert system time as seconds since epoch into i32")?; + Ok(Self(seconds_since_epoch)) + } } impl Display for UnixTimestamp { @@ -99,14 +110,7 @@ impl ScheduledTask { self.task.is_none(), "task should have been aborted already if it existed" ); - let seconds_since_epoch = UNIX_EPOCH - .elapsed() - .context("failed to get timestamp. System date before Unix Epoch?")? - .as_secs(); - let seconds_since_epoch: i32 = seconds_since_epoch - .try_into() - .context("failed to convert system time as seconds since epoch into i32")?; - let timestamp_now = UnixTimestamp::new(seconds_since_epoch); + let timestamp_now = UnixTimestamp::now()?; info!("timestamp_now={timestamp_now:?}"); let seconds_to_desired = self.desired_execution_timestamp.0 - timestamp_now.0; info!(seconds_to_desired); From 03ed3f26366ce8b61ce8949e6ba241d57de66029 Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Wed, 13 Aug 2025 23:59:24 -0400 Subject: [PATCH 14/18] refactor: take slice instead --- src/config.rs | 2 +- src/db.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config.rs b/src/config.rs index 1650549..40155f5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -72,7 +72,7 @@ impl SharedConfig { let key = key.to_string(); let value = serde_json::to_string(value).context("failed to convert to json")?; tokio::spawn(async move { - crate::db::save_kv(&pool, key, value).await; + crate::db::save_kv(&pool, &key, value).await; }); Ok(()) } diff --git a/src/db.rs b/src/db.rs index eb487cb..b7e8f5f 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,6 +1,6 @@ use tracing::{error, info}; -pub async fn save_kv(pool: &sqlx::PgPool, key: String, value: String) { +pub async fn save_kv(pool: &sqlx::PgPool, key: &str, value: String) { let query = sqlx::query!( "\ INSERT INTO kv_store (id, content) From 0911e143b8326dac4652280d1005c23365693623 Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Thu, 14 Aug 2025 00:02:24 -0400 Subject: [PATCH 15/18] feat: lower log level of save due to freq use --- src/db.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/db.rs b/src/db.rs index b7e8f5f..efa17f4 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,4 +1,4 @@ -use tracing::{error, info}; +use tracing::{debug, error, info}; pub async fn save_kv(pool: &sqlx::PgPool, key: &str, value: String) { let query = sqlx::query!( @@ -14,7 +14,7 @@ INSERT INTO kv_store (id, content) match query.execute(pool).await { Ok(query_result) => { if query_result.rows_affected() == 1 { - info!("Save completed for key: {key}"); + debug!("Save completed for key: {key}"); } else { error!( ?key, From ac317ec091c452c1d67fb08607a78036dd40b85a Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Thu, 14 Aug 2025 00:07:00 -0400 Subject: [PATCH 16/18] feat: save heartbeat to db --- src/heartbeat.rs | 17 +++++++++++++---- src/model/schedule.rs | 11 +++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/heartbeat.rs b/src/heartbeat.rs index 206a2b2..3ff37bc 100644 --- a/src/heartbeat.rs +++ b/src/heartbeat.rs @@ -1,12 +1,21 @@ -use tracing::info; +use crate::{db::save_kv, model::schedule::UnixTimestamp}; +use tracing::{error, info}; + +const KEY: &str = "HEARTBEAT"; pub fn start_heartbeat(db_pool: sqlx::PgPool) { shuttle_runtime::tokio::spawn(async move { info!("Heartbeat started"); loop { - // TODO 1: Save time of last heartbeat to DB - println!("beat"); - shuttle_runtime::tokio::time::sleep(std::time::Duration::from_secs(6)).await; + let timestamp = match UnixTimestamp::now() { + Ok(x) => x, + Err(err) => { + error!(?err, "failed to get timestamp heartbeat stopping"); + break; + } + }; + save_kv(&db_pool, KEY, timestamp.to_db_fmt()).await; + shuttle_runtime::tokio::time::sleep(std::time::Duration::from_secs(60)).await; } }); } diff --git a/src/model/schedule.rs b/src/model/schedule.rs index ee441d7..be8903a 100644 --- a/src/model/schedule.rs +++ b/src/model/schedule.rs @@ -29,6 +29,17 @@ impl UnixTimestamp { .context("failed to convert system time as seconds since epoch into i32")?; Ok(Self(seconds_since_epoch)) } + + pub fn to_db_fmt(self) -> String { + self.0.to_string() + } + + pub fn from_db_fmt(value: &str) -> anyhow::Result { + let value = value + .parse::() + .with_context(|| format!("failed to convert {value} into i32 for timestamp"))?; + Ok(Self(value)) + } } impl Display for UnixTimestamp { From b4a20fc3bd8d2b0b684143ca3bfa3d895cde4eba Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Thu, 14 Aug 2025 00:29:00 -0400 Subject: [PATCH 17/18] feat: add output of downtime info --- src/heartbeat.rs | 46 +++++++++++++++++++++++++++++++++++++++---- src/main.rs | 4 ++-- src/model/schedule.rs | 2 +- 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/src/heartbeat.rs b/src/heartbeat.rs index 3ff37bc..5796ec0 100644 --- a/src/heartbeat.rs +++ b/src/heartbeat.rs @@ -1,4 +1,10 @@ -use crate::{db::save_kv, model::schedule::UnixTimestamp}; +use std::time::Duration; + +use crate::{ + db::{load_kv, save_kv}, + model::schedule::UnixTimestamp, +}; +use human_time::ToHumanTimeString; use tracing::{error, info}; const KEY: &str = "HEARTBEAT"; @@ -20,7 +26,39 @@ pub fn start_heartbeat(db_pool: sqlx::PgPool) { }); } -pub fn time_since_last_heartbeat(db_pool: sqlx::PgPool) -> String { - // TODO 1: Get last heartbeat from DB - "First run".to_string() +pub async fn last_heartbeat_info(db_pool: sqlx::PgPool) -> String { + match load_kv(&db_pool, KEY).await { + Some(db_value) => match UnixTimestamp::from_db_fmt(&db_value) { + Ok(last_heartbeat) => { + let Ok(now) = UnixTimestamp::now() else { + return format!( + "Last Heartbeat: {last_heartbeat} but Failed to get current timestamp" + ); + }; + let seconds_since_last_heartbeat = now.0 - last_heartbeat.0; + if seconds_since_last_heartbeat < 0 { + return format!( + "Last heartbeat in the future?! Last heartbeat: {last_heartbeat}, Now: {now}" + ); + } + let Ok(seconds_since_last_heartbeat) = seconds_since_last_heartbeat.try_into() + else { + // Invalid u64 + return format!( + "Invalid u64!!! Seconds since heartbeat: {seconds_since_last_heartbeat}, Last heartbeat: {last_heartbeat}, Now: {now}" + ); + }; + let downtime = Duration::from_secs(seconds_since_last_heartbeat); + format!( + "Downtime: {}\nLast Heartbeat: {last_heartbeat}\nNow: {now}", + downtime.to_human_time_string() + ) + } + Err(err) => { + error!(?err); + "Error Loading Last Heartbeat".to_string() + } + }, + None => "First run".to_string(), + } } diff --git a/src/main.rs b/src/main.rs index c1529cf..de3609a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -84,9 +84,9 @@ async fn main( error!("Development run detected but no guild ID found so slash commands NOT registered"); } let connect_msg = format!( - "{} is connected! Version: {} [{}]", + "{} is connected! Version: {}\n{}", ready.user.name, version!(), - heartbeat::time_since_last_heartbeat(db_pool.clone()), + heartbeat::last_heartbeat_info(db_pool.clone()).await, ); info!("{connect_msg}"); if let Some(channel) = shared_config.channel_bot_status{ diff --git a/src/model/schedule.rs b/src/model/schedule.rs index be8903a..9bc164d 100644 --- a/src/model/schedule.rs +++ b/src/model/schedule.rs @@ -13,7 +13,7 @@ pub mod protected_ops; pub type ScheduledTaskId = OneBasedId; #[derive(Debug, serde::Serialize, serde::Deserialize, Default, Clone, Copy)] -pub struct UnixTimestamp(i32); +pub struct UnixTimestamp(pub i32); impl UnixTimestamp { pub fn new(value: i32) -> Self { Self(value) From 8888dc8047409f22696a065523a2c6671cdfc02b Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Thu, 14 Aug 2025 00:29:37 -0400 Subject: [PATCH 18/18] chore: version bump --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0ba1a0f..3990e03 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -129,7 +129,7 @@ checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "bazooka-bot" -version = "0.6.4-dev" +version = "0.6.4" dependencies = [ "anyhow", "human-time", diff --git a/Cargo.toml b/Cargo.toml index 72ff122..29c09b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bazooka-bot" -version = "0.6.4-dev" +version = "0.6.4" edition = "2024" publish = false