From b8de7f4e16574511eff8e001f66ae9df4d830d82 Mon Sep 17 00:00:00 2001 From: LimeNade Date: Sun, 12 Oct 2025 20:27:22 +0200 Subject: [PATCH 1/2] feat(graph & bg) Added custom user range from the nightscout status + range indicators on the graph. Also removed the debugging text below the graph stickers. --- src/commands/bg.rs | 42 +++++++--- src/commands/graph.rs | 12 +++ src/commands/update_message.rs | 6 ++ src/utils/graph/drawing.rs | 7 +- src/utils/graph/helpers.rs | 33 ++++++++ src/utils/graph/mod.rs | 53 ++++++++++++- src/utils/graph/stickers.rs | 49 ++++++------ src/utils/graph/types.rs | 6 +- src/utils/nightscout.rs | 136 ++++++++++++++++++++++++++++++++- 9 files changed, 298 insertions(+), 46 deletions(-) diff --git a/src/commands/bg.rs b/src/commands/bg.rs index 3c72b98..646d297 100644 --- a/src/commands/bg.rs +++ b/src/commands/bg.rs @@ -102,6 +102,12 @@ pub async fn run( } }; + let status = handler + .nightscout_client + .get_status(base_url, token) + .await + .ok(); + let profile = match handler.nightscout_client.get_profile(base_url, token).await { Ok(profile) => profile, Err(e) => { @@ -126,7 +132,15 @@ pub async fn run( .get(default_profile_name) .context("Default profile not found")?; + let thresholds = status + .as_ref() + .and_then(|s| s.settings.as_ref()) + .and_then(|settings| settings.thresholds.as_ref()); + let user_timezone = &profile_store.timezone; + let target_low_mg = profile_store.get_target_low_mg(thresholds); + let target_high_mg = profile_store.get_target_high_mg(thresholds); + let entry_time = entry.millis_to_user_timezone(user_timezone); let now = chrono::Utc::now() .with_timezone(&chrono_tz::Tz::from_str(user_timezone).unwrap_or(chrono_tz::UTC)); @@ -140,9 +154,9 @@ pub async fn run( format!("{} days ago", duration.num_days()) }; - let color = if entry.sgv > 180.0 { + let color = if entry.sgv > target_high_mg { Colour::from_rgb(227, 177, 11) - } else if entry.sgv < 70.0 { + } else if entry.sgv < target_low_mg { Colour::from_rgb(235, 47, 47) } else { Colour::from_rgb(87, 189, 79) @@ -154,13 +168,23 @@ pub async fn run( .and_then(|u| u.avatar_url()) .unwrap_or_default(); - let title = format!( - "{}'s Nightscout data", - target_user - .as_ref() - .map(|u| u.display_name()) - .unwrap_or_else(|| "User") - ); + let custom_title = status + .as_ref() + .and_then(|s| s.settings.as_ref()) + .and_then(|settings| settings.custom_title.as_deref()) + .filter(|title| *title != "Nightscout"); + + let title = if let Some(custom) = custom_title { + custom.to_string() + } else { + format!( + "{}'s Nightscout data", + target_user + .as_ref() + .map(|u| u.display_name()) + .unwrap_or_else(|| "User") + ) + }; let icon_bytes = std::fs::read("assets/images/nightscout_icon.png")?; let icon_attachment = CreateAttachment::bytes(icon_bytes, "nightscout_icon.png"); diff --git a/src/commands/graph.rs b/src/commands/graph.rs index 6daa5b0..1976842 100644 --- a/src/commands/graph.rs +++ b/src/commands/graph.rs @@ -115,6 +115,12 @@ pub async fn run( } }; + let status = handler + .nightscout_client + .get_status(base_url, token) + .await + .ok(); + let now = chrono::Utc::now(); let hours_ago = now - chrono::Duration::hours(hours); let start_time = hours_ago.to_rfc3339(); @@ -132,6 +138,11 @@ pub async fn run( } }; + let thresholds = status + .as_ref() + .and_then(|s| s.settings.as_ref()) + .and_then(|settings| settings.thresholds.as_ref()); + let buffer = draw_graph( &entries, &treatments, @@ -141,6 +152,7 @@ pub async fn run( handler, hours as u16, None, + thresholds, ) .await?; diff --git a/src/commands/update_message.rs b/src/commands/update_message.rs index 05a461f..1b45ff1 100644 --- a/src/commands/update_message.rs +++ b/src/commands/update_message.rs @@ -9,9 +9,15 @@ pub fn create_update_embed(version: &str) -> CreateEmbed { "• Added contextual stickers. When adding a new sticker it will prompt you to categorize it. The sticker will now generate Depending your blood glucose value!", "• Updated the `/stickers` commmand to work with contextual stickers", "• Added `/set-token`, `/set-nightscout-url`, `/get-nightscout-url` and `/set-visibility` commands to avoid having to run `/setup` each time to change their values.", + "• MBG (meter blood glucose) entries are now displayed as fingerprick readings on graphs", + "• Target high/low ranges are now dynamically fetched from your Nightscout profile instead of being hardcoded", + "• Added faint striped horizontal lines at your target high/low ranges on graphs for better visibility", + "• The `/bg` command now uses your Nightscout custom title from status settings if configured", "", "**Fixes:**", "• Fixed issue where missing data on the edges of the graph would collapse the graph instead of showing the gap", + "• Fixed MBG entries not being fetched from the API", + "• Fixed duplicate detection treating MBG and SGV entries the same way", ], _ => vec![ "**What's New:**", diff --git a/src/utils/graph/drawing.rs b/src/utils/graph/drawing.rs index 8bbb259..3c2ffd3 100644 --- a/src/utils/graph/drawing.rs +++ b/src/utils/graph/drawing.rs @@ -215,6 +215,7 @@ pub fn draw_glucose_reading( } /// Draw glucose data points on the graph +#[allow(clippy::too_many_arguments)] pub fn draw_glucose_points( img: &mut RgbaImage, entries: &[Entry], @@ -223,12 +224,14 @@ pub fn draw_glucose_points( high_col: Rgba, low_col: Rgba, axis_col: Rgba, + target_high: f32, + target_low: f32, ) { for (i, e) in entries.iter().enumerate() { let (x, y) = points_px[i]; - let color = if e.sgv > 180.0 { + let color = if e.sgv > target_high { high_col - } else if e.sgv < 70.0 { + } else if e.sgv < target_low { low_col } else { axis_col diff --git a/src/utils/graph/helpers.rs b/src/utils/graph/helpers.rs index c0c46dc..b8e298b 100644 --- a/src/utils/graph/helpers.rs +++ b/src/utils/graph/helpers.rs @@ -56,3 +56,36 @@ pub fn draw_dashed_vertical_line( drawing_dash = !drawing_dash; } } + +/// Draw a dashed horizontal line on the image +pub fn draw_dashed_horizontal_line( + img: &mut RgbaImage, + y: f32, + x_start: f32, + x_end: f32, + color: image::Rgba, + dash_length: i32, + gap_length: i32, +) { + let y = y.round() as i32; + let x_start = x_start.round() as i32; + let x_end = x_end.round() as i32; + + let mut x = x_start; + let mut drawing_dash = true; + + while x < x_end { + if drawing_dash { + let dash_end = (x + dash_length).min(x_end); + for px in x..dash_end { + if px >= 0 && px < img.width() as i32 && y >= 0 && y < img.height() as i32 { + img.put_pixel(px as u32, y as u32, color); + } + } + x += dash_length; + } else { + x += gap_length; + } + drawing_dash = !drawing_dash; + } +} diff --git a/src/utils/graph/mod.rs b/src/utils/graph/mod.rs index e39c163..473a5dc 100644 --- a/src/utils/graph/mod.rs +++ b/src/utils/graph/mod.rs @@ -6,7 +6,7 @@ mod types; use drawing::{ draw_carbs_treatment, draw_glucose_points, draw_glucose_reading, draw_insulin_treatment, }; -use helpers::draw_dashed_vertical_line; +use helpers::{draw_dashed_horizontal_line, draw_dashed_vertical_line}; use stickers::{ StickerConfig, draw_sticker, filter_ranges_by_duration, find_sticker_position, identify_status_ranges, select_stickers_to_place, @@ -35,6 +35,7 @@ pub async fn draw_graph( handler: &Handler, hours: u16, save_path: Option<&str>, + status_thresholds: Option<&super::nightscout::StatusThresholds>, ) -> Result> { tracing::info!( "[GRAPH] Starting graph generation for {} hours of data", @@ -64,6 +65,14 @@ pub async fn draw_graph( let user_timezone = &profile_store.timezone; tracing::info!("[GRAPH] Using timezone: {}", user_timezone); + let target_low_mg = profile_store.get_target_low_mg(status_thresholds); + let target_high_mg = profile_store.get_target_high_mg(status_thresholds); + tracing::info!( + "[GRAPH] Using target ranges: {:.1} - {:.1} mg/dL", + target_low_mg, + target_high_mg + ); + let nightscout_client = crate::utils::nightscout::Nightscout::new(); let entries = match nightscout_client.filter_and_clean_entries(entries, hours, user_timezone) { Ok(filtered) => filtered, @@ -315,6 +324,34 @@ pub async fn draw_graph( } } + let target_high_y = project_y(target_high_mg); + if target_high_y >= inner_plot_top && target_high_y <= inner_plot_bottom { + let faint_orange = Rgba([255u8, 159u8, 10u8, 80u8]); + draw_dashed_horizontal_line( + &mut img, + target_high_y, + inner_plot_left, + inner_plot_right, + faint_orange, + 10, + 5, + ); + } + + let target_low_y = project_y(target_low_mg); + if target_low_y >= inner_plot_top && target_low_y <= inner_plot_bottom { + let faint_red = Rgba([255u8, 69u8, 58u8, 80u8]); + draw_dashed_horizontal_line( + &mut img, + target_low_y, + inner_plot_left, + inner_plot_right, + faint_red, + 10, + 5, + ); + } + let user_tz: Tz = user_timezone.parse().unwrap_or(chrono_tz::UTC); let now = Utc::now().with_timezone(&user_tz); @@ -520,7 +557,8 @@ pub async fn draw_graph( tracing::info!("[GRAPH] Drawing contextual stickers"); - let status_ranges = identify_status_ranges(&entries, user_timezone); + let status_ranges = + identify_status_ranges(&entries, user_timezone, target_low_mg, target_high_mg); let status_ranges = filter_ranges_by_duration(status_ranges, &entries, user_timezone); let mut treatment_positions: Vec<(f32, f32)> = Vec::new(); @@ -595,7 +633,6 @@ pub async fn draw_graph( inner_plot_top, inner_plot_bottom, handler, - bright, ) .await { @@ -712,7 +749,15 @@ pub async fn draw_graph( } draw_glucose_points( - &mut img, &entries, &points_px, svg_radius, high_col, low_col, axis_col, + &mut img, + &entries, + &points_px, + svg_radius, + high_col, + low_col, + axis_col, + target_high_mg, + target_low_mg, ); let mbg_count = entries.iter().filter(|e| e.has_mbg()).count(); diff --git a/src/utils/graph/stickers.rs b/src/utils/graph/stickers.rs index b2d0da7..8ed73d9 100644 --- a/src/utils/graph/stickers.rs +++ b/src/utils/graph/stickers.rs @@ -1,7 +1,5 @@ -use ab_glyph::PxScale; use anyhow::Result; use image::{Rgba, RgbaImage}; -use imageproc::drawing::draw_text_mut; use super::helpers::download_sticker_image; use super::types::GlucoseStatus; @@ -35,6 +33,8 @@ impl Default for StickerConfig { pub fn identify_status_ranges( entries: &[Entry], _user_timezone: &str, + target_low: f32, + target_high: f32, ) -> Vec<(GlucoseStatus, usize, usize)> { let mut status_ranges: Vec<(GlucoseStatus, usize, usize)> = Vec::new(); @@ -42,11 +42,11 @@ pub fn identify_status_ranges( return status_ranges; } - let mut current_status = GlucoseStatus::from_sgv(entries[0].sgv); + let mut current_status = GlucoseStatus::from_sgv(entries[0].sgv, target_low, target_high); let mut range_start = 0; for (i, entry) in entries.iter().enumerate().skip(1) { - let status = GlucoseStatus::from_sgv(entry.sgv); + let status = GlucoseStatus::from_sgv(entry.sgv, target_low, target_high); if status != current_status { status_ranges.push((current_status, range_start, i - 1)); current_status = status; @@ -295,8 +295,7 @@ pub async fn draw_sticker( inner_plot_right: f32, inner_plot_top: f32, inner_plot_bottom: f32, - handler: &Handler, - bright: Rgba, + _handler: &Handler, ) -> Result<()> { let inner_plot_w = inner_plot_right - inner_plot_left; let inner_plot_h = inner_plot_bottom - inner_plot_top; @@ -377,25 +376,25 @@ pub async fn draw_sticker( sticker_y ); - let debug_label = match sticker.category { - StickerCategory::Low => "low_sticker", - StickerCategory::InRange => "inrange_sticker", - StickerCategory::High => "high_sticker", - StickerCategory::Any => "any_sticker", - }; - - let label_y = (sticker_y + new_h as i32 / 2 + 50).min(img.height() as i32 - 20); - let label_x = (sticker_x - 60).max(10); - - draw_text_mut( - img, - bright, - label_x, - label_y, - PxScale::from(32.0), - &handler.font, - debug_label, - ); + // let debug_label = match sticker.category { + // StickerCategory::Low => "low_sticker", + // StickerCategory::InRange => "inrange_sticker", + // StickerCategory::High => "high_sticker", + // StickerCategory::Any => "any_sticker", + // }; + + // let label_y = (sticker_y + new_h as i32 / 2 + 50).min(img.height() as i32 - 20); + // let label_x = (sticker_x - 60).max(10); + + // draw_text_mut( + // img, + // bright, + // label_x, + // label_y, + // PxScale::from(32.0), + // &handler.font, + // debug_label, + // ); Ok(()) } diff --git a/src/utils/graph/types.rs b/src/utils/graph/types.rs index da2dc45..7373df2 100644 --- a/src/utils/graph/types.rs +++ b/src/utils/graph/types.rs @@ -15,10 +15,10 @@ pub enum GlucoseStatus { } impl GlucoseStatus { - pub fn from_sgv(sgv: f32) -> Self { - if sgv < 70.0 { + pub fn from_sgv(sgv: f32, target_low: f32, target_high: f32) -> Self { + if sgv < target_low { Self::Low - } else if sgv > 180.0 { + } else if sgv > target_high { Self::High } else { Self::InRange diff --git a/src/utils/nightscout.rs b/src/utils/nightscout.rs index 355d07b..4ee69ab 100644 --- a/src/utils/nightscout.rs +++ b/src/utils/nightscout.rs @@ -418,9 +418,10 @@ impl Entry { /// Check if this entry has a meter blood glucose (finger stick) reading pub fn has_mbg(&self) -> bool { if let Some(entry_type) = &self.entry_type - && entry_type == "mbg" { - return self.mbg.is_some() && self.mbg.unwrap_or(0.0) > 0.0; - } + && entry_type == "mbg" + { + return self.mbg.is_some() && self.mbg.unwrap_or(0.0) > 0.0; + } self.mbg.is_some() && self.mbg.unwrap_or(0.0) > 0.0 } } @@ -479,11 +480,25 @@ impl Treatment { } } +#[derive(Deserialize, Debug, Clone)] +pub struct TargetRange { + #[allow(dead_code)] + pub time: String, + pub value: f32, + #[allow(dead_code)] + #[serde(rename = "timeAsSeconds")] + pub time_as_seconds: u32, +} + #[derive(Deserialize, Debug, Clone)] pub struct ProfileStore { pub timezone: String, #[serde(default)] pub units: Option, + #[serde(default)] + pub target_low: Option>, + #[serde(default)] + pub target_high: Option>, } #[derive(Deserialize, Debug, Clone)] @@ -493,6 +508,76 @@ pub struct Profile { pub store: std::collections::HashMap, } +impl ProfileStore { + pub fn get_target_low(&self, status_thresholds: Option<&StatusThresholds>) -> f32 { + self.target_low + .as_ref() + .and_then(|ranges| ranges.first()) + .map(|range| range.value) + .or_else(|| status_thresholds.map(|thresholds| thresholds.bg_target_bottom as f32)) + .unwrap_or(70.0) + } + + pub fn get_target_high(&self, status_thresholds: Option<&StatusThresholds>) -> f32 { + self.target_high + .as_ref() + .and_then(|ranges| ranges.first()) + .map(|range| range.value) + .or_else(|| status_thresholds.map(|thresholds| thresholds.bg_target_top as f32)) + .unwrap_or(180.0) + } + + pub fn get_target_low_mg(&self, status_thresholds: Option<&StatusThresholds>) -> f32 { + let low = self.get_target_low(status_thresholds); + if self.units.as_deref() == Some("mmol") { + low * 18.0 + } else { + low + } + } + + pub fn get_target_high_mg(&self, status_thresholds: Option<&StatusThresholds>) -> f32 { + let high = self.get_target_high(status_thresholds); + if self.units.as_deref() == Some("mmol") { + high * 18.0 + } else { + high + } + } +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct StatusThresholds { + #[allow(dead_code)] + #[serde(rename = "bgHigh")] + pub bg_high: u16, + #[serde(rename = "bgTargetTop")] + pub bg_target_top: u16, + #[serde(rename = "bgTargetBottom")] + pub bg_target_bottom: u16, + #[allow(dead_code)] + #[serde(rename = "bgLow")] + pub bg_low: u16, +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct StatusSettings { + #[serde(default)] + pub custom_title: Option, + #[serde(default)] + pub thresholds: Option, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Status { + #[allow(dead_code)] + pub name: String, + #[serde(default)] + pub settings: Option, +} + #[derive(Deserialize, Debug, Clone)] #[allow(dead_code)] pub struct DeviceStatus { @@ -1244,4 +1329,49 @@ impl Nightscout { } } } + + pub async fn get_status( + &self, + base_url: &str, + token: Option<&str>, + ) -> Result { + tracing::debug!("[API] Fetching status from URL: '{}'", base_url); + + let base = Self::parse_base_url(base_url)?; + let url = base.join("api/v1/status.json")?; + tracing::debug!("[API] Status API URL: {}", url); + + let mut req = self.http_client.get(url.clone()); + + let auth_method = token.map(AuthMethod::from_token); + if let Some(auth) = auth_method { + req = auth.apply_to_request(req); + tracing::debug!("[OK] Applied {} authentication", auth.description()); + } + + tracing::debug!("[HTTP] Sending status request..."); + let res = match req.send().await { + Ok(response) => { + tracing::debug!("[HTTP] Received status response"); + response + } + Err(e) => return Err(Self::handle_connection_error(e, &url)), + }; + + let res = match res.error_for_status() { + Ok(response) => { + tracing::info!("[HTTP] Status response status: {}", response.status()); + response + } + Err(e) => { + tracing::error!("[ERROR] Status request returned error status: {}", e); + return Err(NightscoutError::Network(e)); + } + }; + + let status: Status = res.json().await?; + tracing::info!("[STATUS] Successfully retrieved status"); + + Ok(status) + } } From 98541957c809393dac20b76112de719d081f2aef Mon Sep 17 00:00:00 2001 From: LimeNade Date: Sun, 12 Oct 2025 21:07:16 +0200 Subject: [PATCH 2/2] feat(bg): finger prick The `/bg` command now displays fingerprick values from the past 30 minutes (from MBG entries or BG Check treatments) in both mg/dL and mmol/L Also some version updating thingies :) --- src/bot/version_checker.rs | 77 +++++++++++++++++++++++++--- src/commands/bg.rs | 91 ++++++++++++++++++++++++++++++++++ src/commands/update_message.rs | 89 +++++++++++++++++++++------------ 3 files changed, 217 insertions(+), 40 deletions(-) diff --git a/src/bot/version_checker.rs b/src/bot/version_checker.rs index d5178c4..31d6010 100644 --- a/src/bot/version_checker.rs +++ b/src/bot/version_checker.rs @@ -12,16 +12,42 @@ pub async fn check_and_notify_version_update( let current_version = dotenvy::var("BOT_VERSION").unwrap_or_else(|_| "0.1.1".to_string()); let user_id = command.user.id.get(); + tracing::info!( + "[VERSION] Checking version for user {}. Current version: {}", + user_id, + current_version + ); + match handler.database.get_user_last_seen_version(user_id).await { Ok(last_seen_version) => { + tracing::info!( + "[VERSION] User {} last seen version: {}", + user_id, + last_seen_version + ); + if last_seen_version != current_version { + tracing::info!( + "[VERSION] Version mismatch. Sending update notification to user {}", + user_id + ); + let embed = commands::update_message::create_update_embed(¤t_version); let response = CreateInteractionResponseFollowup::new() .embed(embed) .ephemeral(true); if let Err(e) = command.create_followup(&context.http, response).await { - tracing::warn!("[VERSION] Failed to send update notification: {}", e); + tracing::warn!( + "[VERSION] Failed to send update notification to user {}: {}", + user_id, + e + ); + } else { + tracing::info!( + "[VERSION] Successfully sent update notification to user {}", + user_id + ); } if let Err(e) = handler @@ -32,20 +58,57 @@ pub async fn check_and_notify_version_update( tracing::error!("[VERSION] Failed to update last seen version: {}", e); } else { tracing::info!( - "[VERSION] User {} notified of update from {} to {}", + "[VERSION] User {} version updated from {} to {}", user_id, last_seen_version, current_version ); } + } else { + tracing::debug!( + "[VERSION] User {} already on current version {}", + user_id, + current_version + ); } } - Err(e) => { - tracing::debug!( - "[VERSION] Could not get last seen version for user {}: {}", - user_id, - e + Err(_) => { + tracing::info!( + "[VERSION] No last seen version for user {}. Sending initial notification", + user_id ); + + let embed = commands::update_message::create_update_embed(¤t_version); + let response = CreateInteractionResponseFollowup::new() + .embed(embed) + .ephemeral(true); + + if let Err(e) = command.create_followup(&context.http, response).await { + tracing::warn!( + "[VERSION] Failed to send initial version notification to user {}: {}", + user_id, + e + ); + } else { + tracing::info!( + "[VERSION] Successfully sent initial notification to user {}", + user_id + ); + } + + if let Err(e) = handler + .database + .update_user_last_seen_version(user_id, ¤t_version) + .await + { + tracing::error!("[VERSION] Failed to set initial version: {}", e); + } else { + tracing::info!( + "[VERSION] User {} initial version set to {}", + user_id, + current_version + ); + } } } diff --git a/src/commands/bg.rs b/src/commands/bg.rs index 646d297..9015f19 100644 --- a/src/commands/bg.rs +++ b/src/commands/bg.rs @@ -126,6 +126,23 @@ pub async fn run( .ok() .flatten(); + let now_utc = chrono::Utc::now(); + let thirty_min_ago = now_utc - chrono::Duration::minutes(30); + let start_time = thirty_min_ago.to_rfc3339(); + let end_time = now_utc.to_rfc3339(); + + let recent_entries = handler + .nightscout_client + .get_entries_for_hours(base_url, 1, token) + .await + .unwrap_or_default(); + + let recent_treatments = handler + .nightscout_client + .fetch_treatments_between(base_url, &start_time, &end_time, token) + .await + .unwrap_or_default(); + let default_profile_name = &profile.default_profile; let profile_store = profile .store @@ -243,6 +260,80 @@ pub async fn run( } } + let mut fingerprick_value: Option<(f32, u64)> = None; + let thirty_min_ago_millis = thirty_min_ago.timestamp_millis() as u64; + + for entry in recent_entries.iter() { + if entry.has_mbg() + && let Some(entry_time_millis) = entry.date + && entry_time_millis >= thirty_min_ago_millis + && let Some(mbg) = entry.mbg + { + fingerprick_value = Some((mbg, entry_time_millis)); + break; + } + } + + if fingerprick_value.is_none() { + for treatment in recent_treatments.iter() { + if treatment.event_type.as_deref() == Some("BG Check") + && let Some(glucose_str) = &treatment.glucose + && let Ok(glucose) = glucose_str.parse::() + { + let treatment_time_millis = if let Some(time) = treatment.date.or(treatment.mills) { + time + } else if let Some(created_at) = &treatment.created_at { + if let Ok(parsed_time) = chrono::DateTime::parse_from_rfc3339(created_at) { + parsed_time.timestamp_millis() as u64 + } else { + continue; + } + } else { + continue; + }; + + if treatment_time_millis >= thirty_min_ago_millis { + fingerprick_value = Some((glucose, treatment_time_millis)); + break; + } + } + } + } + + if let Some((fp_value, fp_timestamp)) = fingerprick_value { + let fp_mmol = fp_value / 18.0; + + let now_millis = now_utc.timestamp_millis() as u64; + + tracing::info!("[BG] Fingerprick timestamp: {}", fp_timestamp); + tracing::info!("[BG] Now timestamp: {}", now_millis); + + let timestamp_millis = if fp_timestamp < 10000000000 { + tracing::info!("[BG] Converting seconds to milliseconds"); + fp_timestamp * 1000 + } else { + tracing::info!("[BG] Timestamp already in milliseconds"); + fp_timestamp + }; + + tracing::info!("[BG] Normalized timestamp: {}", timestamp_millis); + + let diff_millis = now_millis.saturating_sub(timestamp_millis); + tracing::info!("[BG] Difference in milliseconds: {}", diff_millis); + + let fp_age_minutes = diff_millis / 1000 / 60; + tracing::info!("[BG] Age in minutes: {}", fp_age_minutes); + + embed = embed.field( + "Fingerprick", + format!( + "{:.0} mg/dL ({:.1} mmol/L)\n-# {} min ago", + fp_value, fp_mmol, fp_age_minutes + ), + false, + ); + } + embed = embed.footer( CreateEmbedFooter::new(format!("measured • {time_ago}")) .icon_url("attachment://nightscout_icon.png"), diff --git a/src/commands/update_message.rs b/src/commands/update_message.rs index 1b45ff1..7d255d8 100644 --- a/src/commands/update_message.rs +++ b/src/commands/update_message.rs @@ -1,38 +1,61 @@ use serenity::all::{Colour, CreateEmbed, CreateEmbedFooter}; pub fn create_update_embed(version: &str) -> CreateEmbed { - let changelog = match version { - "0.2.0" => vec![ - "**What's new:**", - "• **Doubled** the graph resolution allowing for noticeably bigger and clearer resulting images", - "• Added a warning in the `/bg` command if the data is older than 15 min", - "• Added contextual stickers. When adding a new sticker it will prompt you to categorize it. The sticker will now generate Depending your blood glucose value!", - "• Updated the `/stickers` commmand to work with contextual stickers", - "• Added `/set-token`, `/set-nightscout-url`, `/get-nightscout-url` and `/set-visibility` commands to avoid having to run `/setup` each time to change their values.", - "• MBG (meter blood glucose) entries are now displayed as fingerprick readings on graphs", - "• Target high/low ranges are now dynamically fetched from your Nightscout profile instead of being hardcoded", - "• Added faint striped horizontal lines at your target high/low ranges on graphs for better visibility", - "• The `/bg` command now uses your Nightscout custom title from status settings if configured", - "", - "**Fixes:**", - "• Fixed issue where missing data on the edges of the graph would collapse the graph instead of showing the gap", - "• Fixed MBG entries not being fetched from the API", - "• Fixed duplicate detection treating MBG and SGV entries the same way", - ], - _ => vec![ - "**What's New:**", - "• Bug fixes and performance improvements", - "• Enhanced stability", - ], - }; + match version { + "0.2.0" => { + let whats_new = [ + "• **Doubled** graph resolution for bigger and clearer images", + "• Added warning in `/bg` if data is older than 15 min", + "• Added contextual stickers that generate based on your blood glucose value", + "• Updated `/stickers` command to work with contextual stickers", + "• Added `/set-token`, `/set-nightscout-url`, `/get-nightscout-url` and `/set-visibility` commands", + "• MBG (meter blood glucose) entries now displayed as fingerprick readings on graphs", + "• Target ranges now dynamically fetched from your Nightscout profile", + "• Added faint striped lines at target high/low ranges on graphs", + "• `/bg` now uses custom title from Nightscout status settings", + "• `/bg` displays fingerprick values from past 30 min in both mg/dL and mmol/L", + ]; - CreateEmbed::new() - .title(format!("🎉 Beetroot has been updated to v{} | Enhancements Update", version)) - .description("Here's what's new in this update:") - .color(Colour::DARK_GREEN) - .field("Changelog", changelog.join("\n"), false) - .field("For more info","For additional information, please check out the official repository: https://github.com/ItsLimeNade/Beetroot/releases", false) - .footer(CreateEmbedFooter::new( - "Thank you for using Beetroot! Use /help to see all available commands.", - )) + let fixes = [ + "• Fixed missing data on graph edges collapsing the graph", + "• Fixed MBG entries not being fetched from the API", + "• Fixed duplicate detection treating MBG and SGV entries the same", + ]; + + CreateEmbed::new() + .title(format!( + "🎉 Beetroot has been updated to v{} | Enhancements Update", + version + )) + .description("Here's what's new in this update:") + .color(Colour::DARK_GREEN) + .field("What's New", whats_new.join("\n"), false) + .field("Fixes", fixes.join("\n"), false) + .field( + "For more info", + "Check out: https://github.com/ItsLimeNade/Beetroot/releases", + false, + ) + .footer(CreateEmbedFooter::new( + "Thank you for using Beetroot! Use /help to see all available commands.", + )) + } + _ => CreateEmbed::new() + .title(format!("🎉 Beetroot has been updated to v{}", version)) + .description("Here's what's new in this update:") + .color(Colour::DARK_GREEN) + .field( + "What's New", + "• Bug fixes and performance improvements\n• Enhanced stability", + false, + ) + .field( + "For more info", + "Check out: https://github.com/ItsLimeNade/Beetroot/releases", + false, + ) + .footer(CreateEmbedFooter::new( + "Thank you for using Beetroot! Use /help to see all available commands.", + )), + } }