story-kit: merge 316_refactor_abstract_bot_transport_layer_for_multi_platform_support

This commit is contained in:
Dave
2026-03-19 20:43:38 +00:00
parent 3b9cea22bb
commit 7eb9686bfb
10 changed files with 510 additions and 163 deletions

117
server/src/whatsapp.rs Normal file
View File

@@ -0,0 +1,117 @@
//! WhatsApp stub implementation of [`ChatTransport`].
//!
//! This is a placeholder transport that logs operations and returns stub
//! values. It exists to prove the transport abstraction works with a
//! second platform and will be replaced with a real WhatsApp Business API
//! integration in the future.
use async_trait::async_trait;
use crate::slog;
use crate::transport::{ChatTransport, MessageId};
/// Stub WhatsApp transport.
///
/// All methods log the operation and return success with placeholder values.
/// Message editing is not supported by WhatsApp — `edit_message` sends a
/// new message instead (TODO: implement via WhatsApp Business API).
pub struct WhatsAppTransport {
/// Counter for generating unique stub message IDs.
next_id: std::sync::atomic::AtomicU64,
}
impl WhatsAppTransport {
pub fn new() -> Self {
Self {
next_id: std::sync::atomic::AtomicU64::new(1),
}
}
fn next_message_id(&self) -> String {
let id = self
.next_id
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
format!("whatsapp-stub-{id}")
}
}
impl Default for WhatsAppTransport {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl ChatTransport for WhatsAppTransport {
async fn send_message(
&self,
room_id: &str,
plain: &str,
_html: &str,
) -> Result<MessageId, String> {
// TODO: Send via WhatsApp Business API
let msg_id = self.next_message_id();
slog!(
"[whatsapp-stub] send_message to {room_id}: {plain:.80} (id={msg_id})"
);
Ok(msg_id)
}
async fn edit_message(
&self,
room_id: &str,
original_message_id: &str,
plain: &str,
html: &str,
) -> Result<(), String> {
// WhatsApp does not support message editing.
// Send a new message instead.
slog!(
"[whatsapp-stub] edit_message (original={original_message_id}) — \
WhatsApp does not support edits, sending new message"
);
self.send_message(room_id, plain, html).await.map(|_| ())
}
async fn send_typing(&self, room_id: &str, typing: bool) -> Result<(), String> {
// TODO: Send typing indicator via WhatsApp Business API
slog!("[whatsapp-stub] send_typing to {room_id}: typing={typing}");
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn send_message_returns_unique_ids() {
let transport = WhatsAppTransport::new();
let id1 = transport
.send_message("room1", "hello", "<p>hello</p>")
.await
.unwrap();
let id2 = transport
.send_message("room1", "world", "<p>world</p>")
.await
.unwrap();
assert_ne!(id1, id2, "each message should get a unique ID");
assert!(id1.starts_with("whatsapp-stub-"));
}
#[tokio::test]
async fn edit_message_succeeds() {
let transport = WhatsAppTransport::new();
let result = transport
.edit_message("room1", "msg-1", "updated", "<p>updated</p>")
.await;
assert!(result.is_ok(), "edit should succeed (sends new message)");
}
#[tokio::test]
async fn send_typing_succeeds() {
let transport = WhatsAppTransport::new();
assert!(transport.send_typing("room1", true).await.is_ok());
assert!(transport.send_typing("room1", false).await.is_ok());
}
}