Skip to content

Commit 7c6435b

Browse files
authored
0.6.4
0.6.4
2 parents 8e52b09 + 8888dc8 commit 7c6435b

12 files changed

Lines changed: 187 additions & 89 deletions

.sqlx/query-2c2b2e7ab1850b7c4bf786be23c54398267a395e700f4af69d5f396e208f07d5.json

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.sqlx/query-f79d485f20e22f0b3abf69d0f8bdf6e566cd81c6270a1d51988c19a4b205bdc7.json

Lines changed: 0 additions & 15 deletions
This file was deleted.

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "bazooka-bot"
3-
version = "0.6.3"
3+
version = "0.6.4"
44
edition = "2024"
55
publish = false
66

README.md

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ This is the rust replacement for the bot [originally](https://github.com/fone-gi
88

99
<!-- Leave completed items as a feature list / what is being considered for implementation -->
1010

11-
Note: Some checked off items my not be complete only started but at the time of writing (2024-04-07) all are completed.
11+
Note: Some checked off items my not be complete only started but at the time of writing (2025-08-13) all are completed.
1212
If they are not there will be TODO's in the code itself.
1313
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.
1414

@@ -53,12 +53,11 @@ It should be scheduled manually each season by an officer.
5353

5454
# Nice to have
5555

56-
- [ ] Add ability for owner to download the data files to facilitate testing before uploading a version
57-
- [ ] Setup deploy from CI - https://github.com/shuttle-hq/deploy-action
58-
- [ ] Setup test deployment on shuttle (idle time of about 15 minutes should be good)
59-
- [ ] [Naming](https://docs.shuttle.rs/configuration/project-name)
60-
- [ ] From release notes from 0.40.0
61-
- Added a --secrets arg to the run and deploy commands that allows you to use a different secrets file than the default
56+
- [x] Track how long the bot has been up or down
57+
- [ ] Keep history of downtime
58+
- [ ] Add ability for owner to download the data
59+
- [ ] Add ability for owner to replace the data (intended to be from something downloaded previously)
60+
- [x] Setup deploy from CI - https://github.com/shuttle-hq/deploy-action
6261
- [ ] Add message ID to the trace at ingress
6362
- [x] Sanitize input for markdown like `**` for example rn causes problems with bolding the ideas
6463
- [ ] 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

src/commands.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,13 @@ async fn call_to_parent_command(ctx: Context<'_>) -> anyhow::Result<()> {
4343

4444
pub fn commands_list() -> Vec<poise::Command<Data, anyhow::Error>> {
4545
vec![
46-
ping(),
47-
help(),
4846
general::version(),
49-
uptime(),
50-
unranked(),
51-
schedule(),
47+
help(),
48+
ping(),
5249
register(),
50+
schedule(),
51+
unranked(),
52+
uptime(),
5353
]
5454
}
5555

src/config.rs

Lines changed: 10 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::{collections::HashSet, fmt::Debug, time::Instant};
33
use anyhow::Context as _;
44
use poise::serenity_prelude::{ChannelId, GuildId, RoleId, UserId};
55
use shuttle_runtime::{SecretStore, tokio};
6-
use tracing::{error, info};
6+
use tracing::error;
77

88
use crate::secrets::KeyName;
99

@@ -72,33 +72,7 @@ impl SharedConfig {
7272
let key = key.to_string();
7373
let value = serde_json::to_string(value).context("failed to convert to json")?;
7474
tokio::spawn(async move {
75-
let query = sqlx::query!(
76-
"\
77-
INSERT INTO kv_store (id, content)
78-
VALUES ($1, $2)
79-
ON CONFLICT(id)
80-
DO UPDATE SET
81-
content = EXCLUDED.content;",
82-
key,
83-
value
84-
);
85-
match query.execute(&pool).await {
86-
Ok(query_result) => {
87-
if query_result.rows_affected() == 1 {
88-
info!("Save completed for key: {key}");
89-
} else {
90-
error!(
91-
?key,
92-
"Expected 1 row to be affected by save but got: {}",
93-
query_result.rows_affected()
94-
)
95-
}
96-
}
97-
Err(err_msg) => error!(
98-
?err_msg,
99-
"Failed to save content for key: {key} to kv store"
100-
),
101-
}
75+
crate::db::save_kv(&pool, &key, value).await;
10276
});
10377
Ok(())
10478
}
@@ -107,27 +81,17 @@ impl SharedConfig {
10781
&self,
10882
key: &str,
10983
) -> T {
110-
let record_opt = match sqlx::query!("SELECT content FROM kv_store where id = $1", key)
111-
.fetch_optional(&self.db_pool)
112-
.await
113-
{
114-
Ok(content) => content,
115-
Err(err_msg) => {
116-
error!(?err_msg, "Failed to get content for key: {key}");
117-
None
118-
}
84+
let Some(content) = crate::db::load_kv(&self.db_pool, key).await else {
85+
return T::default();
11986
};
120-
let record = match record_opt {
121-
Some(record) => record,
122-
None => {
123-
info!("No content found in DB for key: {key}");
124-
return T::default();
125-
}
126-
};
127-
match serde_json::from_str(&record.content) {
87+
match serde_json::from_str(&content) {
12888
Ok(x) => x,
12989
Err(err_msg) => {
130-
error!(?err_msg, ?record.content, "Failed to convert content extracted from the database");
90+
error!(
91+
?err_msg,
92+
?content,
93+
"Failed to convert content extracted from the database"
94+
);
13195
T::default()
13296
}
13397
}

src/db.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
use tracing::{debug, error, info};
2+
3+
pub async fn save_kv(pool: &sqlx::PgPool, key: &str, value: String) {
4+
let query = sqlx::query!(
5+
"\
6+
INSERT INTO kv_store (id, content)
7+
VALUES ($1, $2)
8+
ON CONFLICT(id)
9+
DO UPDATE SET
10+
content = EXCLUDED.content;",
11+
key,
12+
value
13+
);
14+
match query.execute(pool).await {
15+
Ok(query_result) => {
16+
if query_result.rows_affected() == 1 {
17+
debug!("Save completed for key: {key}");
18+
} else {
19+
error!(
20+
?key,
21+
"Expected 1 row to be affected by save but got: {}",
22+
query_result.rows_affected()
23+
)
24+
}
25+
}
26+
Err(err_msg) => error!(
27+
?err_msg,
28+
"Failed to save content for key: {key} to kv store"
29+
),
30+
}
31+
}
32+
33+
pub async fn load_kv(pool: &sqlx::PgPool, key: &str) -> Option<String> {
34+
match sqlx::query!("SELECT content FROM kv_store where id = $1", key)
35+
.fetch_optional(pool)
36+
.await
37+
{
38+
Ok(Some(record)) => Some(record.content),
39+
Ok(None) => {
40+
info!("No content found in DB for key: {key}");
41+
None
42+
}
43+
Err(err_msg) => {
44+
error!(?err_msg, "Failed to get content for key: {key}");
45+
None
46+
}
47+
}
48+
}

src/heartbeat.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use std::time::Duration;
2+
3+
use crate::{
4+
db::{load_kv, save_kv},
5+
model::schedule::UnixTimestamp,
6+
};
7+
use human_time::ToHumanTimeString;
8+
use tracing::{error, info};
9+
10+
const KEY: &str = "HEARTBEAT";
11+
12+
pub fn start_heartbeat(db_pool: sqlx::PgPool) {
13+
shuttle_runtime::tokio::spawn(async move {
14+
info!("Heartbeat started");
15+
loop {
16+
let timestamp = match UnixTimestamp::now() {
17+
Ok(x) => x,
18+
Err(err) => {
19+
error!(?err, "failed to get timestamp heartbeat stopping");
20+
break;
21+
}
22+
};
23+
save_kv(&db_pool, KEY, timestamp.to_db_fmt()).await;
24+
shuttle_runtime::tokio::time::sleep(std::time::Duration::from_secs(60)).await;
25+
}
26+
});
27+
}
28+
29+
pub async fn last_heartbeat_info(db_pool: sqlx::PgPool) -> String {
30+
match load_kv(&db_pool, KEY).await {
31+
Some(db_value) => match UnixTimestamp::from_db_fmt(&db_value) {
32+
Ok(last_heartbeat) => {
33+
let Ok(now) = UnixTimestamp::now() else {
34+
return format!(
35+
"Last Heartbeat: {last_heartbeat} but Failed to get current timestamp"
36+
);
37+
};
38+
let seconds_since_last_heartbeat = now.0 - last_heartbeat.0;
39+
if seconds_since_last_heartbeat < 0 {
40+
return format!(
41+
"Last heartbeat in the future?! Last heartbeat: {last_heartbeat}, Now: {now}"
42+
);
43+
}
44+
let Ok(seconds_since_last_heartbeat) = seconds_since_last_heartbeat.try_into()
45+
else {
46+
// Invalid u64
47+
return format!(
48+
"Invalid u64!!! Seconds since heartbeat: {seconds_since_last_heartbeat}, Last heartbeat: {last_heartbeat}, Now: {now}"
49+
);
50+
};
51+
let downtime = Duration::from_secs(seconds_since_last_heartbeat);
52+
format!(
53+
"Downtime: {}\nLast Heartbeat: {last_heartbeat}\nNow: {now}",
54+
downtime.to_human_time_string()
55+
)
56+
}
57+
Err(err) => {
58+
error!(?err);
59+
"Error Loading Last Heartbeat".to_string()
60+
}
61+
},
62+
None => "First run".to_string(),
63+
}
64+
}

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ pub use self::{
2121

2222
mod commands;
2323
mod config;
24+
mod db;
25+
pub mod heartbeat;
2426
mod model;
2527
mod secrets;
2628

0 commit comments

Comments
 (0)