diff --git a/src/api.rs b/src/api.rs index 8758611..4c2af59 100644 --- a/src/api.rs +++ b/src/api.rs @@ -884,10 +884,13 @@ fn mime_for_doc(path: &std::path::Path) -> &'static str { // --- History & Search --- async fn handle_history(bridge: &WhatsAppBridge, req: &HttpRequest) -> Vec { - let jid = req.query_get("jid"); + let jid = match req.query_get("jid") { + Some(j) if !j.is_empty() => j, + _ => return json_err(400, "query parameter 'jid' is required"), + }; let limit: i64 = req.query_get("limit").and_then(|v| v.parse().ok()).unwrap_or(50).max(1).min(200); let before: Option = req.query_get("before").and_then(|v| v.parse().ok()); - match bridge.store().search_inbound(jid, None, limit, before).await { + match bridge.store().search_unified(&jid, limit, before).await { Ok(rows) => { let count = rows.len(); json_ok(json!({"messages": rows, "count": count})) diff --git a/src/storage.rs b/src/storage.rs index c5e349d..a7a03c6 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -254,6 +254,18 @@ pub struct InboundRow { pub timestamp: i64, } +/// A row from the unified conversation history (inbound + outbound). +#[derive(Debug, Clone, serde::Serialize)] +pub struct ConversationRow { + pub chat_jid: String, + pub sender_jid: String, + pub message_id: String, + pub content_kind: String, + pub body_text: Option, + pub timestamp: i64, + pub direction: String, // "inbound" | "outbound" +} + /// Statistics from a prune operation. #[derive(Debug, Clone)] pub struct PruneStats { @@ -612,6 +624,49 @@ impl Store { .await } + /// Unified conversation history: inbound messages + sent outbound messages, + /// merged and sorted by timestamp. Outbound rows use the JID as sender_jid + /// with direction="outbound"; inbound rows use direction="inbound". + pub async fn search_unified( + &self, + chat_jid: &str, + limit: i64, + before_ts: Option, + ) -> Result> { + let jid = chat_jid.to_owned(); + let before = before_ts.unwrap_or(i64::MAX); + self.run(move |c| { + let sql = " + SELECT chat_jid, sender_jid, message_id, content_kind, body_text, timestamp, 'inbound' AS direction + FROM inbound_messages + WHERE chat_jid = ?1 AND timestamp < ?2 + UNION ALL + SELECT jid AS chat_jid, jid AS sender_jid, COALESCE(wa_message_id, CAST(id AS TEXT)) AS message_id, + op_kind AS content_kind, + json_extract(payload_json, '$.text') AS body_text, + created_at AS timestamp, 'outbound' AS direction + FROM outbound_queue + WHERE jid = ?1 AND status = 'sent' AND created_at < ?2 + ORDER BY timestamp DESC + LIMIT ?3 + "; + let mut stmt = c.prepare(sql).map_err(db_err)?; + let rows = stmt.query_map(params![jid, before, limit], |row| { + Ok(ConversationRow { + chat_jid: row.get(0)?, + sender_jid: row.get(1)?, + message_id: row.get(2)?, + content_kind: row.get(3)?, + body_text: row.get(4)?, + timestamp: row.get(5)?, + direction: row.get(6)?, + }) + }).map_err(db_err)?; + rows.collect::, _>>().map_err(db_err) + }) + .await + } + /// Delete all inbound messages for a chat (e.g. when the chat is deleted). pub async fn delete_inbound_chat(&self, chat_jid: &str) -> Result { let jid = chat_jid.to_owned();