storkit: merge 396_story_whatsapp_bot_startup_announcement_after_restart
This commit is contained in:
@@ -379,6 +379,43 @@ async fn main() -> Result<(), std::io::Error> {
|
|||||||
let whatsapp_ctx_for_shutdown: Option<Arc<chat::transport::whatsapp::WhatsAppWebhookContext>> =
|
let whatsapp_ctx_for_shutdown: Option<Arc<chat::transport::whatsapp::WhatsAppWebhookContext>> =
|
||||||
whatsapp_ctx.clone();
|
whatsapp_ctx.clone();
|
||||||
|
|
||||||
|
// ── Startup announcements (WhatsApp & Slack) ──────────────────────────
|
||||||
|
//
|
||||||
|
// Send "{bot_name} is online." to all known contacts so users know the bot
|
||||||
|
// is ready. This mirrors the Matrix bot's startup announcement and fires
|
||||||
|
// on every fresh process start — including after a rebuild/re-exec.
|
||||||
|
//
|
||||||
|
// • WhatsApp: send to all phone numbers present in persisted history.
|
||||||
|
// • Slack: send to all configured channel IDs (channel_ids from bot.toml).
|
||||||
|
// • Matrix: handled by spawn_bot() below; no action needed here.
|
||||||
|
if let Some(ref ctx) = whatsapp_ctx {
|
||||||
|
let transport = Arc::clone(&ctx.transport);
|
||||||
|
let bot_name = ctx.bot_name.clone();
|
||||||
|
let history = Arc::clone(&ctx.history);
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let senders: Vec<String> = history.lock().await.keys().cloned().collect();
|
||||||
|
if senders.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let notifier =
|
||||||
|
crate::rebuild::BotShutdownNotifier::new(transport, senders, bot_name);
|
||||||
|
notifier.notify_startup().await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if let Some(ref ctx) = slack_ctx {
|
||||||
|
let transport = Arc::clone(&ctx.transport) as Arc<dyn crate::chat::ChatTransport>;
|
||||||
|
let bot_name = ctx.bot_name.clone();
|
||||||
|
let channels: Vec<String> = ctx.channel_ids.iter().cloned().collect();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if channels.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let notifier =
|
||||||
|
crate::rebuild::BotShutdownNotifier::new(transport, channels, bot_name);
|
||||||
|
notifier.notify_startup().await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Watch channel: signals the Matrix bot task to send a shutdown announcement.
|
// Watch channel: signals the Matrix bot task to send a shutdown announcement.
|
||||||
// `None` initial value means "server is running".
|
// `None` initial value means "server is running".
|
||||||
let (matrix_shutdown_tx, matrix_shutdown_rx) =
|
let (matrix_shutdown_tx, matrix_shutdown_rx) =
|
||||||
|
|||||||
@@ -43,6 +43,19 @@ impl BotShutdownNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send a startup announcement to all configured channels.
|
||||||
|
///
|
||||||
|
/// Called once per process start so users know the bot is online.
|
||||||
|
/// Errors are logged and ignored — startup is never blocked by a failed send.
|
||||||
|
pub async fn notify_startup(&self) {
|
||||||
|
let msg = format!("{} is online.", self.bot_name);
|
||||||
|
for channel in &self.channels {
|
||||||
|
if let Err(e) = self.transport.send_message(channel, &msg, &msg).await {
|
||||||
|
slog!("[startup] Failed to send startup message to {channel}: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Send a shutdown message to all configured channels.
|
/// Send a shutdown message to all configured channels.
|
||||||
///
|
///
|
||||||
/// Errors from individual sends are logged and ignored so that a single
|
/// Errors from individual sends are logged and ignored so that a single
|
||||||
@@ -345,4 +358,95 @@ mod tests {
|
|||||||
notifier.notify(ShutdownReason::Manual).await;
|
notifier.notify(ShutdownReason::Manual).await;
|
||||||
assert!(transport.messages().is_empty());
|
assert!(transport.messages().is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -- notify_startup -------------------------------------------------------
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn notify_startup_sends_online_message_to_all_channels() {
|
||||||
|
let transport = Arc::new(CapturingTransport::new());
|
||||||
|
let notifier = BotShutdownNotifier::new(
|
||||||
|
Arc::clone(&transport) as Arc<dyn ChatTransport>,
|
||||||
|
vec!["#channel1".to_string(), "#channel2".to_string()],
|
||||||
|
"Timmy".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
notifier.notify_startup().await;
|
||||||
|
|
||||||
|
let msgs = transport.messages();
|
||||||
|
assert_eq!(msgs.len(), 2);
|
||||||
|
assert_eq!(msgs[0].0, "#channel1");
|
||||||
|
assert_eq!(msgs[1].0, "#channel2");
|
||||||
|
assert!(
|
||||||
|
msgs[0].1.contains("online"),
|
||||||
|
"expected 'online' in startup message: {}",
|
||||||
|
msgs[0].1
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
msgs[0].1.contains("Timmy"),
|
||||||
|
"expected bot name in startup message: {}",
|
||||||
|
msgs[0].1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn notify_startup_message_uses_bot_name() {
|
||||||
|
let transport = Arc::new(CapturingTransport::new());
|
||||||
|
let notifier = BotShutdownNotifier::new(
|
||||||
|
Arc::clone(&transport) as Arc<dyn ChatTransport>,
|
||||||
|
vec!["#general".to_string()],
|
||||||
|
"HAL".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
notifier.notify_startup().await;
|
||||||
|
|
||||||
|
let msgs = transport.messages();
|
||||||
|
assert_eq!(msgs[0].1, "HAL is online.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn notify_startup_with_no_channels_is_noop() {
|
||||||
|
let transport = Arc::new(CapturingTransport::new());
|
||||||
|
let notifier = BotShutdownNotifier::new(
|
||||||
|
Arc::clone(&transport) as Arc<dyn ChatTransport>,
|
||||||
|
vec![],
|
||||||
|
"Timmy".to_string(),
|
||||||
|
);
|
||||||
|
notifier.notify_startup().await;
|
||||||
|
assert!(transport.messages().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn notify_startup_is_best_effort_failing_send_does_not_panic() {
|
||||||
|
let transport = Arc::new(CapturingTransport::failing());
|
||||||
|
let notifier = BotShutdownNotifier::new(
|
||||||
|
Arc::clone(&transport) as Arc<dyn ChatTransport>,
|
||||||
|
vec!["#channel".to_string()],
|
||||||
|
"Timmy".to_string(),
|
||||||
|
);
|
||||||
|
// Should complete without panicking.
|
||||||
|
notifier.notify_startup().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn notify_startup_message_differs_from_shutdown_message() {
|
||||||
|
let transport_start = Arc::new(CapturingTransport::new());
|
||||||
|
let notifier_start = BotShutdownNotifier::new(
|
||||||
|
Arc::clone(&transport_start) as Arc<dyn ChatTransport>,
|
||||||
|
vec!["C1".to_string()],
|
||||||
|
"Bot".to_string(),
|
||||||
|
);
|
||||||
|
notifier_start.notify_startup().await;
|
||||||
|
|
||||||
|
let transport_stop = Arc::new(CapturingTransport::new());
|
||||||
|
let notifier_stop = BotShutdownNotifier::new(
|
||||||
|
Arc::clone(&transport_stop) as Arc<dyn ChatTransport>,
|
||||||
|
vec!["C1".to_string()],
|
||||||
|
"Bot".to_string(),
|
||||||
|
);
|
||||||
|
notifier_stop.notify(ShutdownReason::Manual).await;
|
||||||
|
|
||||||
|
let startup_msg = &transport_start.messages()[0].1;
|
||||||
|
let shutdown_msg = &transport_stop.messages()[0].1;
|
||||||
|
assert_ne!(startup_msg, shutdown_msg, "startup and shutdown messages must differ");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user