Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8> {
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<i64> = 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}))
Expand Down
55 changes: 55 additions & 0 deletions src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
pub timestamp: i64,
pub direction: String, // "inbound" | "outbound"
}

/// Statistics from a prune operation.
#[derive(Debug, Clone)]
pub struct PruneStats {
Expand Down Expand Up @@ -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<i64>,
) -> Result<Vec<ConversationRow>> {
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::<std::result::Result<Vec<_>, _>>().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<u32> {
let jid = chat_jid.to_owned();
Expand Down