1use crate::stored_event::{StoredEvent, event_kind};
4use rusqlite::OptionalExtension;
5use crate::crypto::maybe_encrypt;
6use crate::types::{Message, Attachment, Reaction};
7
8pub async fn save_event(event: &StoredEvent) -> Result<(), String> {
14 let conn = super::get_write_connection_guard_static()?;
15
16 let tags_json = serde_json::to_string(&event.tags)
17 .unwrap_or_else(|_| "[]".to_string());
18
19 let content = if event.kind == event_kind::CHAT_MESSAGE
21 || event.kind == event_kind::PRIVATE_DIRECT_MESSAGE
22 || event.kind == event_kind::MESSAGE_EDIT
23 {
24 maybe_encrypt(event.content.clone()).await
25 } else {
26 event.content.clone()
27 };
28
29 conn.execute(
33 r#"
34 INSERT INTO events (
35 id, kind, chat_id, user_id, content, tags, reference_id,
36 created_at, received_at, mine, pending, failed, wrapper_event_id, npub, preview_metadata
37 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)
38 ON CONFLICT(id) DO UPDATE SET
39 kind = excluded.kind, chat_id = excluded.chat_id, user_id = excluded.user_id,
40 content = excluded.content, tags = excluded.tags, reference_id = excluded.reference_id,
41 created_at = excluded.created_at, received_at = excluded.received_at,
42 mine = excluded.mine, pending = excluded.pending, failed = excluded.failed,
43 wrapper_event_id = COALESCE(excluded.wrapper_event_id, events.wrapper_event_id),
44 npub = excluded.npub, preview_metadata = excluded.preview_metadata
45 "#,
46 rusqlite::params![
47 event.id,
48 event.kind as i32,
49 event.chat_id,
50 event.user_id,
51 content,
52 tags_json,
53 event.reference_id,
54 event.created_at as i64,
55 event.received_at as i64,
56 event.mine as i32,
57 event.pending as i32,
58 event.failed as i32,
59 event.wrapper_event_id,
60 event.npub,
61 event.preview_metadata,
62 ],
63 ).map_err(|e| format!("Failed to save event: {}", e))?;
64
65 Ok(())
66}
67
68fn extract_bot_tags(tags: &[Vec<String>]) -> Vec<String> {
71 tags.iter()
72 .filter(|t| t.len() >= 2 && t[0] == "bot")
73 .map(|t| t[1].clone())
74 .collect()
75}
76
77fn extract_expiration_tag(tags: &[Vec<String>]) -> Option<u64> {
80 tags.iter()
81 .find(|t| t.len() >= 2 && t[0] == "expiration")
82 .and_then(|t| t[1].parse::<u64>().ok())
83}
84
85pub fn event_exists(event_id: &str) -> Result<bool, String> {
87 let conn = super::get_db_connection_guard_static()?;
88 conn.query_row(
89 "SELECT EXISTS(SELECT 1 FROM events WHERE id = ?1)",
90 rusqlite::params![event_id],
91 |row| row.get(0),
92 ).map_err(|e| format!("Failed to check event existence: {}", e))
93}
94
95pub async fn save_reaction_event(
97 reaction: &Reaction,
98 chat_id: i64,
99 user_id: Option<i64>,
100 mine: bool,
101 wrapper_event_id: Option<String>,
102) -> Result<(), String> {
103 let mut tags: Vec<Vec<String>> = vec![
107 vec!["e".to_string(), reaction.reference_id.clone()],
108 ];
109 if let Some(url) = &reaction.emoji_url {
110 if reaction.emoji.starts_with(':') && reaction.emoji.ends_with(':') && reaction.emoji.len() >= 3 {
111 let shortcode = &reaction.emoji[1..reaction.emoji.len() - 1];
112 if !shortcode.is_empty() && !url.is_empty() {
113 tags.push(vec!["emoji".to_string(), shortcode.to_string(), url.clone()]);
114 }
115 }
116 }
117 let event = StoredEvent {
118 id: reaction.id.clone(),
119 kind: event_kind::REACTION,
120 chat_id,
121 user_id,
122 content: reaction.emoji.clone(),
123 tags,
124 reference_id: Some(reaction.reference_id.clone()),
125 created_at: std::time::SystemTime::now()
126 .duration_since(std::time::UNIX_EPOCH)
127 .map(|d| d.as_secs()).unwrap_or(0),
128 received_at: std::time::SystemTime::now()
129 .duration_since(std::time::UNIX_EPOCH)
130 .map(|d| d.as_millis() as u64).unwrap_or(0),
131 mine,
132 pending: false,
133 failed: false,
134 wrapper_event_id,
135 npub: Some(reaction.author_id.clone()),
136 preview_metadata: None,
137 };
138 save_event(&event).await
139}
140
141pub async fn save_message(chat_id: &str, message: &Message) -> Result<(), String> {
150 let chat_int_id = super::id_cache::get_or_create_chat_id(chat_id)?;
151
152 let user_int_id = if let Some(ref npub_str) = message.npub {
153 super::id_cache::get_or_create_user_id(npub_str)?
154 } else {
155 None
156 };
157
158 let event = message_to_stored_event(message, chat_int_id, user_int_id);
159 save_event(&event).await?;
160
161 for reaction in &message.reactions {
163 if !event_exists(&reaction.id)? {
164 let user_id = super::id_cache::get_or_create_user_id(&reaction.author_id)?;
165 let is_mine = super::get_current_account()
166 .map(|npub| reaction.author_id == npub)
167 .unwrap_or(false);
168 save_reaction_event(reaction, chat_int_id, user_id, is_mine, None).await?;
169 }
170 }
171
172 Ok(())
173}
174
175fn message_to_stored_event(message: &Message, chat_id: i64, user_id: Option<i64>) -> StoredEvent {
177 let kind = if !message.attachments.is_empty() {
178 event_kind::FILE_ATTACHMENT
179 } else {
180 event_kind::PRIVATE_DIRECT_MESSAGE
181 };
182
183 let mut tags: Vec<Vec<String>> = Vec::new();
184
185 let ms = message.at % 1000;
187 if ms > 0 {
188 tags.push(vec!["ms".to_string(), ms.to_string()]);
189 }
190
191 if !message.replied_to.is_empty() {
193 tags.push(vec![
194 "e".to_string(),
195 message.replied_to.clone(),
196 "".to_string(),
197 "reply".to_string(),
198 ]);
199 }
200
201 if !message.attachments.is_empty() {
203 if let Ok(json) = serde_json::to_string(&message.attachments) {
204 tags.push(vec!["attachments".to_string(), json]);
205 }
206 }
207
208 for et in &message.emoji_tags {
211 tags.push(vec!["emoji".to_string(), et.shortcode.clone(), et.url.clone()]);
212 }
213
214 for npub in &message.addressed_bots {
217 tags.push(vec!["bot".to_string(), npub.clone()]);
218 }
219
220 if let Some(exp) = message.expiration {
223 tags.push(vec!["expiration".to_string(), exp.to_string()]);
224 }
225
226 let preview_metadata = message.preview_metadata.as_ref()
227 .and_then(|m| serde_json::to_string(m).ok());
228
229 StoredEvent {
230 id: message.id.clone(),
231 kind,
232 chat_id,
233 user_id,
234 content: message.content.clone(),
235 tags,
236 reference_id: None,
237 created_at: message.at / 1000,
238 received_at: std::time::SystemTime::now()
239 .duration_since(std::time::UNIX_EPOCH)
240 .map(|d| d.as_millis() as u64)
241 .unwrap_or(0),
242 mine: message.mine,
243 pending: message.pending,
244 failed: message.failed,
245 wrapper_event_id: message.wrapper_event_id.clone(),
246 npub: message.npub.clone(),
247 preview_metadata,
248 }
249}
250
251pub async fn save_pivx_payment_event(
253 conversation_id: &str,
254 mut event: StoredEvent,
255) -> Result<(), String> {
256 event.chat_id = super::id_cache::get_or_create_chat_id(conversation_id)?;
257 save_event(&event).await
258}
259
260pub async fn save_system_event_by_id(
263 event_id: &str,
264 conversation_id: &str,
265 event_type: crate::stored_event::SystemEventType,
266 member_npub: &str,
267 member_name: Option<&str>,
268) -> Result<bool, String> {
269 let now_secs = std::time::SystemTime::now()
270 .duration_since(std::time::UNIX_EPOCH)
271 .map(|d| d.as_secs()).unwrap_or(0);
272 save_system_event_at(event_id, conversation_id, event_type, member_npub, member_name, now_secs, None, None).await
273}
274
275pub async fn save_system_event_at(
279 event_id: &str,
280 conversation_id: &str,
281 event_type: crate::stored_event::SystemEventType,
282 member_npub: &str,
283 member_name: Option<&str>,
284 created_at_secs: u64,
285 invited_by: Option<&str>,
288 invited_label: Option<&str>,
289) -> Result<bool, String> {
290 let chat_id = super::id_cache::get_or_create_chat_id(conversation_id)?;
291
292 let now_secs = std::time::SystemTime::now()
293 .duration_since(std::time::UNIX_EPOCH)
294 .map(|d| d.as_secs()).unwrap_or(0);
295 let created_at = created_at_secs.min(now_secs);
297
298 let display_name = member_name.unwrap_or(member_npub);
299 let content = event_type.display_message(display_name);
300
301 let mut tags: Vec<Vec<String>> = vec![
302 vec!["d".to_string(), "system-event".to_string()],
303 vec!["event-type".to_string(), event_type.as_u8().to_string()],
304 vec!["member".to_string(), member_npub.to_string()],
305 ];
306 if let Some(by) = invited_by {
307 tags.push(vec!["invited-by".to_string(), by.to_string()]);
308 if let Some(l) = invited_label.filter(|l| !l.is_empty()) {
309 tags.push(vec!["invited-label".to_string(), l.to_string()]);
310 }
311 }
312 let tags_json = serde_json::to_string(&tags)
313 .map_err(|e| format!("Failed to serialize tags: {}", e))?;
314
315 let conn = super::get_write_connection_guard_static()?;
316 let rows = conn.execute(
317 r#"INSERT OR IGNORE INTO events (
318 id, kind, chat_id, user_id, content, tags, reference_id,
319 created_at, received_at, mine, pending, failed, wrapper_event_id, npub
320 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)"#,
321 rusqlite::params![
322 event_id,
323 event_kind::APPLICATION_SPECIFIC as i32,
324 chat_id, None::<i64>, content, tags_json, None::<String>,
325 created_at as i64, now_secs as i64,
326 0, 0, 0, None::<String>, member_npub,
327 ],
328 ).map_err(|e| format!("Failed to save system event: {}", e))?;
329
330 Ok(rows > 0)
331}
332
333pub async fn save_edit_event(
335 edit_id: &str,
336 message_id: &str,
337 new_content: &str,
338 emoji_tags: &[crate::types::EmojiTag],
339 chat_id: i64,
340 user_id: Option<i64>,
341 npub: &str,
342) -> Result<(), String> {
343 let now = std::time::SystemTime::now()
344 .duration_since(std::time::UNIX_EPOCH).unwrap();
345
346 let mut tags = vec![
349 vec!["e".to_string(), message_id.to_string(), "".to_string(), "edit".to_string()],
350 ];
351 for et in emoji_tags {
352 tags.push(vec!["emoji".to_string(), et.shortcode.clone(), et.url.clone()]);
353 }
354
355 let event = StoredEvent {
356 id: edit_id.to_string(),
357 kind: event_kind::MESSAGE_EDIT,
358 chat_id,
359 user_id,
360 content: new_content.to_string(),
361 tags,
362 reference_id: Some(message_id.to_string()),
363 created_at: now.as_secs(),
364 received_at: now.as_millis() as u64,
365 mine: true,
366 pending: false,
367 failed: false,
368 wrapper_event_id: None,
369 npub: Some(npub.to_string()),
370 preview_metadata: None,
371 };
372
373 save_event(&event).await
374}
375
376pub async fn delete_event(event_id: &str) -> Result<(), String> {
378 let conn = super::get_write_connection_guard_static()?;
379 if let Ok(Some((chat_row, at))) = conn.query_row(
383 "SELECT chat_id, created_at FROM events WHERE id = ?1",
384 rusqlite::params![event_id],
385 |r| Ok((r.get::<_, i64>(0)?, r.get::<_, i64>(1)?)),
386 ).optional() {
387 conn.execute(
388 "UPDATE chats SET last_read = COALESCE(( \
389 SELECT id FROM events WHERE chat_id = ?1 AND id != ?2 AND created_at <= ?3 \
390 ORDER BY created_at DESC, id DESC LIMIT 1), '') \
391 WHERE id = ?1 AND last_read = ?2",
392 rusqlite::params![chat_row, event_id, at],
393 ).map_err(|e| format!("read-marker retreat: {e}"))?;
394 }
395 conn.execute(
396 "DELETE FROM events WHERE id = ?1",
397 rusqlite::params![event_id],
398 ).map_err(|e| format!("Failed to delete event: {}", e))?;
399 Ok(())
400}
401
402pub fn event_author(event_id: &str) -> Result<Option<String>, String> {
405 let conn = match super::get_db_connection_guard_static() {
406 Ok(c) => c,
407 Err(_) => return Ok(None),
408 };
409 conn.query_row(
410 "SELECT npub FROM events WHERE id = ?1",
411 rusqlite::params![event_id],
412 |row| row.get::<_, Option<String>>(0),
413 )
414 .optional()
415 .map(|o| o.flatten())
416 .map_err(|e| format!("Failed to read event author: {}", e))
417}
418
419pub fn event_delete_context(event_id: &str) -> Result<Option<(String, bool, Option<String>)>, String> {
423 let conn = match super::get_db_connection_guard_static() {
424 Ok(c) => c,
425 Err(_) => return Ok(None),
426 };
427 conn.query_row(
428 "SELECT c.chat_identifier, e.mine, e.npub \
429 FROM events e JOIN chats c ON c.id = e.chat_id \
430 WHERE e.id = ?1",
431 rusqlite::params![event_id],
432 |row| {
433 Ok((
434 row.get::<_, String>(0)?,
435 row.get::<_, i32>(1)? != 0,
436 row.get::<_, Option<String>>(2)?,
437 ))
438 },
439 )
440 .optional()
441 .map_err(|e| format!("Failed to read event delete context: {}", e))
442}
443
444pub fn message_exists_in_db(message_id: &str) -> Result<bool, String> {
446 let conn = match super::get_db_connection_guard_static() {
447 Ok(c) => c,
448 Err(_) => return Ok(false),
449 };
450 conn.query_row(
451 "SELECT EXISTS(SELECT 1 FROM events WHERE id = ?1)",
452 rusqlite::params![message_id],
453 |row| row.get(0),
454 ).map_err(|e| format!("Failed to check event existence: {}", e))
455}
456
457pub fn wrapper_event_exists(wrapper_event_id: &str) -> Result<bool, String> {
459 let conn = match super::get_db_connection_guard_static() {
460 Ok(c) => c,
461 Err(_) => return Ok(false),
462 };
463 conn.query_row(
464 "SELECT EXISTS(SELECT 1 FROM events WHERE wrapper_event_id = ?1)",
465 rusqlite::params![wrapper_event_id],
466 |row| row.get(0),
467 ).map_err(|e| format!("Failed to check wrapper event existence: {}", e))
468}
469
470pub fn update_wrapper_event_id(event_id: &str, wrapper_event_id: &str) -> Result<bool, String> {
473 let conn = match super::get_write_connection_guard_static() {
474 Ok(c) => c,
475 Err(_) => return Ok(false),
476 };
477 let rows = conn.execute(
478 "UPDATE events SET wrapper_event_id = ?1 WHERE id = ?2 AND (wrapper_event_id IS NULL OR wrapper_event_id = '')",
479 rusqlite::params![wrapper_event_id, event_id],
480 ).map_err(|e| format!("Failed to update wrapper event ID: {}", e))?;
481 Ok(rows > 0)
482}
483
484pub fn get_chat_message_count(chat_id: i64) -> Result<usize, String> {
486 let conn = super::get_db_connection_guard_static()?;
487 let count: i64 = conn.query_row(
492 &format!(
493 "SELECT COUNT(*) FROM events WHERE chat_id = ?1 AND kind IN ({}, {}, {})",
494 event_kind::CHAT_MESSAGE, event_kind::PRIVATE_DIRECT_MESSAGE, event_kind::FILE_ATTACHMENT
495 ),
496 rusqlite::params![chat_id],
497 |row| row.get(0),
498 ).map_err(|e| format!("Failed to count messages: {}", e))?;
499 Ok(count as usize)
500}
501
502pub fn get_pivx_payments_for_chat(conversation_id: &str) -> Result<Vec<StoredEvent>, String> {
504 let conn = super::get_db_connection_guard_static()?;
505 let chat_id: i64 = conn.query_row(
506 "SELECT id FROM chats WHERE chat_identifier = ?1",
507 rusqlite::params![conversation_id], |row| row.get(0)
508 ).map_err(|_| "Chat not found")?;
509
510 let mut stmt = conn.prepare(
511 "SELECT id, kind, chat_id, user_id, content, tags, reference_id, \
512 created_at, received_at, mine, pending, failed, wrapper_event_id, npub \
513 FROM events WHERE chat_id = ?1 AND kind = ?2 ORDER BY created_at ASC, received_at ASC"
514 ).map_err(|e| format!("Failed to prepare: {}", e))?;
515
516 let rows = stmt.query_map(
517 rusqlite::params![chat_id, event_kind::APPLICATION_SPECIFIC as i32],
518 |row| {
519 let tags_json: String = row.get(5)?;
520 let tags: Vec<Vec<String>> = serde_json::from_str(&tags_json).unwrap_or_default();
521 Ok(StoredEvent {
522 id: row.get(0)?, kind: row.get::<_, i32>(1)? as u16,
523 chat_id: row.get(2)?, user_id: row.get(3)?, content: row.get(4)?,
524 tags, reference_id: row.get(6)?,
525 created_at: row.get::<_, i64>(7)? as u64, received_at: row.get::<_, i64>(8)? as u64,
526 mine: row.get::<_, i32>(9)? != 0, pending: row.get::<_, i32>(10)? != 0,
527 failed: row.get::<_, i32>(11)? != 0, wrapper_event_id: row.get(12)?,
528 npub: row.get(13)?, preview_metadata: None,
529 })
530 }
531 ).map_err(|e| format!("Failed to query: {}", e))?;
532
533 let mut payments = Vec::new();
534 for row in rows {
535 let event = row.map_err(|e| format!("Failed to read event: {}", e))?;
536 if event.tags.iter().any(|t| t.len() >= 2 && t[0] == "d" && t[1] == "pivx-payment") {
537 payments.push(event);
538 }
539 }
540 Ok(payments)
541}
542
543pub fn get_system_events_for_chat(conversation_id: &str) -> Result<Vec<StoredEvent>, String> {
545 let conn = super::get_db_connection_guard_static()?;
546 let chat_id: i64 = conn.query_row(
547 "SELECT id FROM chats WHERE chat_identifier = ?1",
548 rusqlite::params![conversation_id], |row| row.get(0)
549 ).map_err(|_| "Chat not found")?;
550
551 let mut stmt = conn.prepare(
552 "SELECT id, kind, chat_id, user_id, content, tags, reference_id, \
553 created_at, received_at, mine, pending, failed, wrapper_event_id, npub \
554 FROM events WHERE chat_id = ?1 AND kind = ?2 ORDER BY created_at ASC, received_at ASC"
555 ).map_err(|e| format!("Failed to prepare: {}", e))?;
556
557 let rows = stmt.query_map(
558 rusqlite::params![chat_id, event_kind::APPLICATION_SPECIFIC as i32],
559 |row| {
560 let tags_json: String = row.get(5)?;
561 let tags: Vec<Vec<String>> = serde_json::from_str(&tags_json).unwrap_or_default();
562 Ok(StoredEvent {
563 id: row.get(0)?, kind: row.get::<_, i32>(1)? as u16,
564 chat_id: row.get(2)?, user_id: row.get(3)?, content: row.get(4)?,
565 tags, reference_id: row.get(6)?,
566 created_at: row.get::<_, i64>(7)? as u64, received_at: row.get::<_, i64>(8)? as u64,
567 mine: row.get::<_, i32>(9)? != 0, pending: row.get::<_, i32>(10)? != 0,
568 failed: row.get::<_, i32>(11)? != 0, wrapper_event_id: row.get(12)?,
569 npub: row.get(13)?, preview_metadata: None,
570 })
571 }
572 ).map_err(|e| format!("Failed to query: {}", e))?;
573
574 let mut events = Vec::new();
575 for row in rows {
576 let event = row.map_err(|e| format!("Failed to read event: {}", e))?;
577 if event.tags.iter().any(|t| t.len() >= 2 && t[0] == "d" && t[1] == "system-event") {
578 events.push(event);
579 }
580 }
581 Ok(events)
582}
583
584fn parse_event_row(row: &rusqlite::Row) -> rusqlite::Result<StoredEvent> {
590 let tags_json: String = row.get(5)?;
591 let tags: Vec<Vec<String>> = serde_json::from_str(&tags_json).unwrap_or_default();
592
593 Ok(StoredEvent {
594 id: row.get(0)?,
595 kind: row.get::<_, i32>(1)? as u16,
596 chat_id: row.get(2)?,
597 user_id: row.get(3)?,
598 content: row.get(4)?,
599 tags,
600 reference_id: row.get(6)?,
601 created_at: row.get::<_, i64>(7)? as u64,
602 received_at: row.get::<_, i64>(8)? as u64,
603 mine: row.get::<_, i32>(9)? != 0,
604 pending: row.get::<_, i32>(10)? != 0,
605 failed: row.get::<_, i32>(11)? != 0,
606 wrapper_event_id: row.get(12)?,
607 npub: row.get(13)?,
608 preview_metadata: row.get(14)?,
609 })
610}
611
612pub async fn get_events(
615 chat_id: i64,
616 kinds: Option<&[u16]>,
617 limit: usize,
618 offset: usize,
619) -> Result<Vec<StoredEvent>, String> {
620 let events: Vec<StoredEvent> = {
621 let conn = super::get_db_connection_guard_static()?;
622
623 if let Some(k) = kinds {
624 let kind_placeholders: String = (0..k.len())
625 .map(|i| format!("?{}", i + 2))
626 .collect::<Vec<_>>()
627 .join(",");
628 let limit_param = k.len() + 2;
629 let offset_param = k.len() + 3;
630
631 let sql = format!(
632 "SELECT id, kind, chat_id, user_id, content, tags, reference_id, \
633 created_at, received_at, mine, pending, failed, wrapper_event_id, npub, preview_metadata \
634 FROM events WHERE chat_id = ?1 AND kind IN ({}) \
635 ORDER BY created_at DESC, received_at DESC \
636 LIMIT ?{} OFFSET ?{}",
637 kind_placeholders, limit_param, offset_param
638 );
639
640 let mut stmt = conn.prepare(&sql)
641 .map_err(|e| format!("Failed to prepare events query: {}", e))?;
642
643 match k.len() {
644 1 => {
645 let rows = stmt.query_map(
646 rusqlite::params![chat_id, k[0] as i32, limit as i64, offset as i64],
647 parse_event_row
648 ).map_err(|e| format!("Failed to query events: {}", e))?;
649 rows.filter_map(|r| r.ok()).collect()
650 },
651 2 => {
652 let rows = stmt.query_map(
653 rusqlite::params![chat_id, k[0] as i32, k[1] as i32, limit as i64, offset as i64],
654 parse_event_row
655 ).map_err(|e| format!("Failed to query events: {}", e))?;
656 rows.filter_map(|r| r.ok()).collect()
657 },
658 3 => {
659 let rows = stmt.query_map(
660 rusqlite::params![chat_id, k[0] as i32, k[1] as i32, k[2] as i32, limit as i64, offset as i64],
661 parse_event_row
662 ).map_err(|e| format!("Failed to query events: {}", e))?;
663 rows.filter_map(|r| r.ok()).collect()
664 },
665 _ => return Err("Unsupported number of kinds".to_string()),
666 }
667 } else {
668 let mut stmt = conn.prepare(
669 "SELECT id, kind, chat_id, user_id, content, tags, reference_id, \
670 created_at, received_at, mine, pending, failed, wrapper_event_id, npub, preview_metadata \
671 FROM events WHERE chat_id = ?1 \
672 ORDER BY created_at DESC, received_at DESC \
673 LIMIT ?2 OFFSET ?3"
674 ).map_err(|e| format!("Failed to prepare events query: {}", e))?;
675
676 let rows = stmt.query_map(
677 rusqlite::params![chat_id, limit as i64, offset as i64],
678 parse_event_row
679 ).map_err(|e| format!("Failed to query events: {}", e))?;
680 rows.filter_map(|r| r.ok()).collect()
681 }
682 };
683
684 let mut decrypted = Vec::with_capacity(events.len());
686 for mut event in events {
687 if event.kind == event_kind::CHAT_MESSAGE || event.kind == event_kind::PRIVATE_DIRECT_MESSAGE {
688 event.content = crate::crypto::maybe_decrypt(event.content).await
689 .unwrap_or_else(|_| "[Decryption failed]".to_string());
690 }
691 decrypted.push(event);
692 }
693
694 Ok(decrypted)
695}
696
697pub async fn get_related_events(
699 reference_ids: &[String],
700) -> Result<Vec<StoredEvent>, String> {
701 if reference_ids.is_empty() {
702 return Ok(Vec::new());
703 }
704
705 let conn = super::get_db_connection_guard_static()?;
706
707 let placeholders: String = reference_ids.iter().map(|_| "?").collect::<Vec<_>>().join(",");
708 let sql = format!(
709 "SELECT id, kind, chat_id, user_id, content, tags, reference_id, \
710 created_at, received_at, mine, pending, failed, wrapper_event_id, npub, preview_metadata \
711 FROM events WHERE reference_id IN ({}) \
712 ORDER BY created_at ASC, received_at ASC",
713 placeholders
714 );
715
716 let mut stmt = conn.prepare(&sql)
717 .map_err(|e| format!("Failed to prepare related events query: {}", e))?;
718
719 let params: Vec<&dyn rusqlite::ToSql> = reference_ids.iter()
720 .map(|s| s as &dyn rusqlite::ToSql)
721 .collect();
722
723 let events: Vec<StoredEvent> = stmt.query_map(params.as_slice(), parse_event_row)
724 .map_err(|e| format!("Failed to query related events: {}", e))?
725 .filter_map(|r| r.ok())
726 .collect();
727
728 Ok(events)
729}
730
731pub struct ReplyContext {
733 pub content: String,
734 pub npub: Option<String>,
735 pub has_attachment: bool,
736 pub extension: Option<String>,
739}
740
741pub async fn get_reply_contexts(
743 message_ids: &[String],
744) -> Result<std::collections::HashMap<String, ReplyContext>, String> {
745 use std::collections::HashMap;
746
747 if message_ids.is_empty() {
748 return Ok(HashMap::new());
749 }
750
751 let (events, edits): (Vec<(String, i32, String, Option<String>, Option<String>)>, Vec<(String, String)>) = {
752 let conn = super::get_db_connection_guard_static()?;
753
754 let placeholders: String = (0..message_ids.len())
755 .map(|i| format!("?{}", i + 1))
756 .collect::<Vec<_>>()
757 .join(",");
758
759 let sql = format!(
761 "SELECT id, kind, content, npub, tags FROM events WHERE id IN ({})",
762 placeholders
763 );
764 let mut stmt = conn.prepare(&sql)
765 .map_err(|e| format!("Failed to prepare reply context query: {}", e))?;
766
767 let params: Vec<&str> = message_ids.iter().map(|s| s.as_str()).collect();
768 let params_dyn: Vec<&dyn rusqlite::ToSql> = params.iter().map(|s| s as &dyn rusqlite::ToSql).collect();
769
770 let rows = stmt.query_map(params_dyn.as_slice(), |row| {
771 Ok((row.get::<_, String>(0)?, row.get::<_, i32>(1)?,
772 row.get::<_, String>(2)?, row.get::<_, Option<String>>(3)?,
773 row.get::<_, Option<String>>(4)?))
774 }).map_err(|e| format!("Failed to query reply contexts: {}", e))?;
775 let events_result: Vec<_> = rows.filter_map(|r| r.ok()).collect();
776 drop(stmt);
777
778 let edit_sql = format!(
780 "SELECT reference_id, content FROM events \
781 WHERE kind = {} AND reference_id IN ({}) \
782 ORDER BY created_at DESC, received_at DESC",
783 event_kind::MESSAGE_EDIT, placeholders
784 );
785 let mut edit_stmt = conn.prepare(&edit_sql)
786 .map_err(|e| format!("Failed to prepare edit query: {}", e))?;
787 let edit_rows = edit_stmt.query_map(params_dyn.as_slice(), |row| {
788 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
789 }).map_err(|e| format!("Failed to query edits: {}", e))?;
790 let edits_result: Vec<_> = edit_rows.filter_map(|r| r.ok()).collect();
791
792 (events_result, edits_result)
793 };
794
795 let mut latest_edits: HashMap<String, String> = HashMap::new();
797 for (ref_id, content) in edits {
798 latest_edits.entry(ref_id).or_insert(content);
799 }
800
801 let mut contexts = HashMap::new();
803 for (id, kind, original_content, npub, tags) in events {
804 let has_attachment = kind == event_kind::FILE_ATTACHMENT as i32;
805 let content_to_decrypt = latest_edits.get(&id).cloned().unwrap_or(original_content);
806
807 let decrypted_content = if kind == event_kind::CHAT_MESSAGE as i32
808 || kind == event_kind::PRIVATE_DIRECT_MESSAGE as i32
809 {
810 crate::crypto::maybe_decrypt(content_to_decrypt).await
811 .unwrap_or_else(|_| "[Decryption failed]".to_string())
812 } else {
813 String::new()
814 };
815
816 let extension = if has_attachment {
820 tags.as_deref()
821 .and_then(|t| serde_json::from_str::<Vec<Vec<String>>>(t).ok())
822 .and_then(|parsed| parsed.into_iter()
823 .find(|t| t.first().map(|k| k == "attachments").unwrap_or(false))
824 .and_then(|t| t.into_iter().nth(1)))
825 .and_then(|json| serde_json::from_str::<Vec<serde_json::Value>>(&json).ok())
826 .and_then(|atts| atts.into_iter().next())
827 .and_then(|a| a.get("extension").and_then(|e| e.as_str()).map(str::to_lowercase))
828 .filter(|e| !e.is_empty())
829 } else {
830 None
831 };
832
833 contexts.insert(id, ReplyContext { content: decrypted_content, npub, has_attachment, extension });
834 }
835
836 Ok(contexts)
837}
838
839pub async fn populate_reply_context(message: &mut Message) -> Result<(), String> {
842 if message.replied_to.is_empty() {
843 return Ok(());
844 }
845
846 let contexts = get_reply_contexts(&[message.replied_to.clone()]).await?;
847
848 if let Some(ctx) = contexts.get(&message.replied_to) {
849 message.replied_to_content = Some(ctx.content.clone());
850 message.replied_to_npub = ctx.npub.clone();
851 message.replied_to_has_attachment = Some(ctx.has_attachment);
852 message.replied_to_attachment_extension = ctx.extension.clone();
853 }
854
855 Ok(())
856}
857
858pub fn is_own_event(event_id: &str) -> bool {
862 let Ok(conn) = super::get_db_connection_guard_static() else {
863 return false;
864 };
865 conn.query_row(
866 "SELECT mine FROM events WHERE id = ?1",
867 [event_id],
868 |row| row.get::<_, i64>(0),
869 )
870 .map(|mine| mine == 1)
871 .unwrap_or(false)
872}
873
874fn extract_tag_from_json(tags_json: &str, key: &str) -> Option<String> {
880 if tags_json.len() <= 2 { return None; }
881 let pattern = format!("[\"{}\"", key);
882 if !tags_json.contains(&pattern) { return None; }
883 let tags: Vec<Vec<String>> = serde_json::from_str(tags_json).ok()?;
884 tags.into_iter()
885 .find(|tag| tag.first().map(|s| s.as_str()) == Some(key))
886 .and_then(|tag| tag.into_iter().nth(1))
887}
888
889
890fn normalize_reaction_author(author: String) -> String {
894 if author.len() == 64 && author.bytes().all(|b| b.is_ascii_hexdigit()) {
895 if let Ok(pk) = nostr_sdk::prelude::PublicKey::from_hex(&author) {
896 use nostr_sdk::prelude::ToBech32;
897 if let Ok(npub) = pk.to_bech32() {
898 return npub;
899 }
900 }
901 }
902 author
903}
904fn extract_reaction_emoji_url(tags: &[Vec<String>], content: &str) -> Option<String> {
910 if !content.starts_with(':') || !content.ends_with(':') || content.len() < 3 {
911 return None;
912 }
913 let sc = &content[1..content.len() - 1];
914 tags.iter().find_map(|t| {
915 if t.len() >= 3 && t[0] == "emoji" && t[1] == sc {
916 Some(t[2].clone())
917 } else {
918 None
919 }
920 })
921}
922
923fn extract_reply_tag_from_json(tags_json: &str) -> Option<String> {
925 if tags_json.len() <= 2 { return None; }
926 if !tags_json.contains("[\"e\"") { return None; }
927 let tags: Vec<Vec<String>> = serde_json::from_str(tags_json).ok()?;
928 tags.into_iter()
929 .find(|tag| {
930 tag.first().map(|s| s.as_str()) == Some("e")
931 && tag.get(3).map(|s| s.as_str()) == Some("reply")
932 })
933 .and_then(|tag| tag.into_iter().nth(1))
934}
935
936pub async fn get_message_views(
941 chat_id: i64,
942 limit: usize,
943 offset: usize,
944) -> Result<Vec<Message>, String> {
945 let message_kinds = [event_kind::CHAT_MESSAGE, event_kind::PRIVATE_DIRECT_MESSAGE, event_kind::FILE_ATTACHMENT];
947 let message_events = get_events(chat_id, Some(&message_kinds), limit, offset).await?;
948
949 compose_message_views(message_events).await
950}
951
952async fn compose_message_views(message_events: Vec<StoredEvent>) -> Result<Vec<Message>, String> {
957 use std::collections::HashMap;
958
959 if message_events.is_empty() {
960 return Ok(Vec::new());
961 }
962
963 let message_ids: Vec<String> = message_events.iter().map(|e| e.id.clone()).collect();
965 let related_events = get_related_events(&message_ids).await?;
966
967 let mut reactions_by_msg: HashMap<String, Vec<Reaction>> = HashMap::new();
968 let mut edits_by_msg: HashMap<String, Vec<(u64, String, Vec<crate::types::EmojiTag>)>> = HashMap::new();
969
970 for event in related_events {
971 if let Some(ref_id) = &event.reference_id {
972 match event.kind {
973 k if k == event_kind::REACTION => {
974 let emoji_url = extract_reaction_emoji_url(&event.tags, &event.content);
975 reactions_by_msg.entry(ref_id.clone()).or_default().push(Reaction {
976 id: event.id.clone(),
977 reference_id: ref_id.clone(),
978 author_id: normalize_reaction_author(event.npub.clone().unwrap_or_default()),
979 emoji: event.content.clone(),
980 emoji_url,
981 });
982 }
983 k if k == event_kind::MESSAGE_EDIT => {
984 let decrypted = crate::crypto::maybe_decrypt(event.content.clone()).await
985 .unwrap_or_else(|_| event.content.clone());
986 let edit_emoji = crate::types::EmojiTag::extract_from_stored(&event.tags);
987 edits_by_msg.entry(ref_id.clone()).or_default().push((event.created_at * 1000, decrypted, edit_emoji));
988 }
989 _ => {}
990 }
991 }
992 }
993
994 for edits in edits_by_msg.values_mut() {
995 edits.sort_by_key(|(ts, _, _)| *ts);
996 }
997
998 let mut attachments_by_msg: HashMap<String, Vec<Attachment>> = HashMap::new();
1000 let mut events_needing_legacy_lookup: Vec<String> = Vec::new();
1001
1002 for event in &message_events {
1003 if event.kind != event_kind::FILE_ATTACHMENT && event.kind != event_kind::CHAT_MESSAGE {
1004 continue;
1005 }
1006 if let Some(json) = event.get_tag("attachments") {
1007 if let Ok(atts) = serde_json::from_str::<Vec<Attachment>>(json) {
1008 if !atts.is_empty() {
1009 attachments_by_msg.insert(event.id.clone(), atts);
1010 continue;
1011 }
1012 }
1013 }
1014 if event.kind == event_kind::FILE_ATTACHMENT {
1015 events_needing_legacy_lookup.push(event.id.clone());
1016 }
1017 }
1018
1019 if !events_needing_legacy_lookup.is_empty() {
1021 if let Ok(conn) = super::get_db_connection_guard_static() {
1022 let has_messages: bool = conn.query_row(
1023 "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='messages'",
1024 [], |row| row.get::<_, i32>(0)
1025 ).map(|c| c > 0).unwrap_or(false);
1026
1027 if has_messages {
1028 for msg_id in &events_needing_legacy_lookup {
1029 if let Ok(json) = conn.query_row::<String, _, _>(
1030 "SELECT attachments FROM messages WHERE id = ?1",
1031 rusqlite::params![msg_id], |row| row.get(0),
1032 ) {
1033 if let Ok(atts) = serde_json::from_str::<Vec<Attachment>>(&json) {
1034 attachments_by_msg.insert(msg_id.to_string(), atts);
1035 }
1036 }
1037 }
1038 }
1039 }
1040 }
1041
1042 let mut messages = Vec::with_capacity(message_events.len());
1044 for event in message_events {
1045 let replied_to = event.get_reply_reference().unwrap_or("").to_string();
1046 let at = event.timestamp_ms();
1047 let reactions = reactions_by_msg.remove(&event.id).unwrap_or_default();
1048 let attachments = attachments_by_msg.remove(&event.id).unwrap_or_default();
1049
1050 let original_content = if event.kind == event_kind::FILE_ATTACHMENT {
1051 String::new()
1052 } else {
1053 event.content.clone()
1054 };
1055
1056 let original_emoji = crate::types::EmojiTag::extract_from_stored(&event.tags);
1059 let (content, edited, edit_history, emoji_tags) = if let Some(edits) = edits_by_msg.remove(&event.id) {
1060 let mut history = Vec::with_capacity(edits.len() + 1);
1061 history.push(crate::types::EditEntry { content: original_content.clone(), edited_at: at });
1062 for (ts, c, _) in &edits {
1063 history.push(crate::types::EditEntry { content: c.clone(), edited_at: *ts });
1064 }
1065 let (latest, latest_emoji) = edits.last()
1066 .map(|(_, c, e)| (c.clone(), e.clone()))
1067 .unwrap_or_else(|| (original_content.clone(), original_emoji.clone()));
1068 (latest, true, Some(history), latest_emoji)
1069 } else {
1070 (original_content, false, None, original_emoji)
1071 };
1072
1073 let preview_metadata = event.preview_metadata
1074 .and_then(|json| serde_json::from_str(&json).ok());
1075
1076 let addressed_bots = extract_bot_tags(&event.tags);
1077 let expiration = extract_expiration_tag(&event.tags);
1078 messages.push(Message {
1079 expiration,
1080 id: event.id, content, replied_to,
1081 replied_to_content: None, replied_to_npub: None, replied_to_has_attachment: None,
1082 replied_to_attachment_extension: None,
1083 preview_metadata, attachments, reactions, at,
1084 pending: event.pending, failed: event.failed, mine: event.mine,
1085 npub: event.npub, wrapper_event_id: event.wrapper_event_id,
1086 edited, edit_history,
1087 emoji_tags,
1088 addressed_bots,
1089 });
1090 }
1091
1092 let reply_ids: Vec<String> = messages.iter()
1094 .filter(|m| !m.replied_to.is_empty())
1095 .map(|m| m.replied_to.clone())
1096 .collect();
1097
1098 if !reply_ids.is_empty() {
1099 let contexts = get_reply_contexts(&reply_ids).await?;
1100 for msg in &mut messages {
1101 if let Some(ctx) = contexts.get(&msg.replied_to) {
1102 msg.replied_to_content = Some(ctx.content.clone());
1103 msg.replied_to_npub = ctx.npub.clone();
1104 msg.replied_to_has_attachment = Some(ctx.has_attachment);
1105 msg.replied_to_attachment_extension = ctx.extension.clone();
1106 }
1107 }
1108 }
1109
1110 Ok(messages)
1111}
1112
1113pub async fn get_messages_around(
1121 chat_id: i64,
1122 anchor_id: &str,
1123 before: usize,
1124 after: usize,
1125) -> Result<Vec<Message>, String> {
1126 let message_kinds = [event_kind::CHAT_MESSAGE, event_kind::PRIVATE_DIRECT_MESSAGE, event_kind::FILE_ATTACHMENT];
1127
1128 let message_events: Vec<StoredEvent> = {
1129 let conn = super::get_db_connection_guard_static()?;
1130
1131 let (anchor_at, anchor_rt, anchor_rowid): (i64, i64, i64) = conn.query_row(
1137 "SELECT created_at, received_at, rowid FROM events WHERE id = ?1",
1138 rusqlite::params![anchor_id],
1139 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
1140 ).map_err(|e| format!("Anchor message not found: {}", e))?;
1141
1142 let kind_placeholders: String = (0..message_kinds.len())
1144 .map(|i| format!("?{}", i + 2))
1145 .collect::<Vec<_>>()
1146 .join(",");
1147 let cols = "id, kind, chat_id, user_id, content, tags, reference_id, \
1148 created_at, received_at, mine, pending, failed, wrapper_event_id, npub, preview_metadata";
1149
1150 let older_sql = format!(
1152 "SELECT {} FROM events WHERE chat_id = ?1 AND kind IN ({}) \
1153 AND (created_at < ?5 OR (created_at = ?5 AND (received_at < ?6 \
1154 OR (received_at = ?6 AND rowid <= ?7)))) \
1155 ORDER BY created_at DESC, received_at DESC, rowid DESC LIMIT ?8",
1156 cols, kind_placeholders
1157 );
1158 let mut older_stmt = conn.prepare(&older_sql)
1159 .map_err(|e| format!("Failed to prepare older window query: {}", e))?;
1160 let older_rows = older_stmt.query_map(
1161 rusqlite::params![
1162 chat_id,
1163 message_kinds[0] as i32, message_kinds[1] as i32, message_kinds[2] as i32,
1164 anchor_at, anchor_rt, anchor_rowid, before as i64
1165 ],
1166 parse_event_row,
1167 ).map_err(|e| format!("Failed to query older window: {}", e))?;
1168 let mut older: Vec<StoredEvent> = older_rows.filter_map(|r| r.ok()).collect();
1169 older.reverse(); let newer_sql = format!(
1173 "SELECT {} FROM events WHERE chat_id = ?1 AND kind IN ({}) \
1174 AND (created_at > ?5 OR (created_at = ?5 AND (received_at > ?6 \
1175 OR (received_at = ?6 AND rowid > ?7)))) \
1176 ORDER BY created_at ASC, received_at ASC, rowid ASC LIMIT ?8",
1177 cols, kind_placeholders
1178 );
1179 let mut newer_stmt = conn.prepare(&newer_sql)
1180 .map_err(|e| format!("Failed to prepare newer window query: {}", e))?;
1181 let newer_rows = newer_stmt.query_map(
1182 rusqlite::params![
1183 chat_id,
1184 message_kinds[0] as i32, message_kinds[1] as i32, message_kinds[2] as i32,
1185 anchor_at, anchor_rt, anchor_rowid, after as i64
1186 ],
1187 parse_event_row,
1188 ).map_err(|e| format!("Failed to query newer window: {}", e))?;
1189 let newer: Vec<StoredEvent> = newer_rows.filter_map(|r| r.ok()).collect();
1190
1191 older.into_iter().chain(newer).collect()
1192 };
1193
1194 let mut decrypted = Vec::with_capacity(message_events.len());
1196 for mut event in message_events {
1197 if event.kind == event_kind::CHAT_MESSAGE || event.kind == event_kind::PRIVATE_DIRECT_MESSAGE {
1198 event.content = crate::crypto::maybe_decrypt(event.content).await
1199 .unwrap_or_else(|_| "[Decryption failed]".to_string());
1200 }
1201 decrypted.push(event);
1202 }
1203
1204 compose_message_views(decrypted).await
1205}
1206
1207pub async fn get_all_chats_last_messages() -> Result<std::collections::HashMap<String, Vec<Message>>, String> {
1210 use std::collections::HashMap;
1211
1212 let message_events: Vec<(String, StoredEvent, String)> = {
1214 let conn = super::get_db_connection_guard_static()?;
1215 let mut stmt = conn.prepare(
1216 "SELECT c.chat_identifier, \
1217 e.id, e.kind, e.chat_id, e.user_id, e.content, e.tags, e.reference_id, \
1218 e.created_at, e.received_at, e.mine, e.pending, e.failed, e.wrapper_event_id, e.npub, e.preview_metadata \
1219 FROM chats c JOIN events e ON e.rowid = ( \
1220 SELECT e2.rowid FROM events e2 WHERE e2.chat_id = c.id \
1221 AND e2.kind IN (?1, ?2, ?3) \
1222 ORDER BY e2.created_at DESC, e2.received_at DESC LIMIT 1) \
1223 WHERE c.chat_type != 1"
1224 ).map_err(|e| format!("Failed to prepare: {}", e))?;
1225
1226 let rows = stmt.query_map(
1227 rusqlite::params![
1228 event_kind::CHAT_MESSAGE as i32,
1229 event_kind::PRIVATE_DIRECT_MESSAGE as i32,
1230 event_kind::FILE_ATTACHMENT as i32
1231 ],
1232 |row| {
1233 let chat_id: String = row.get(0)?;
1234 let tags_json: String = row.get(6)?;
1235 let event = StoredEvent {
1236 id: row.get(1)?, kind: row.get::<_, i32>(2)? as u16,
1237 chat_id: row.get(3)?, user_id: row.get(4)?, content: row.get(5)?,
1238 tags: Vec::new(), reference_id: row.get(7)?,
1240 created_at: row.get::<_, i64>(8)? as u64, received_at: row.get::<_, i64>(9)? as u64,
1241 mine: row.get::<_, i32>(10)? != 0, pending: row.get::<_, i32>(11)? != 0,
1242 failed: row.get::<_, i32>(12)? != 0, wrapper_event_id: row.get(13)?,
1243 npub: row.get(14)?, preview_metadata: row.get(15)?,
1244 };
1245 Ok((chat_id, event, tags_json))
1246 }
1247 ).map_err(|e| format!("Failed to query: {}", e))?;
1248 rows.filter_map(|r| r.ok()).collect()
1249 };
1250
1251 if message_events.is_empty() {
1252 return Ok(HashMap::new());
1253 }
1254
1255 let message_ids: Vec<String> = message_events.iter().map(|(_, e, _)| e.id.clone()).collect();
1257 let related_events = get_related_events(&message_ids).await?;
1258
1259 let mut reactions_by_msg: HashMap<String, Vec<Reaction>> = HashMap::new();
1260 let mut edits_by_msg: HashMap<String, Vec<(u64, String, Vec<crate::types::EmojiTag>)>> = HashMap::new();
1261
1262 for event in related_events {
1263 if let Some(ref_id) = &event.reference_id {
1264 match event.kind {
1265 k if k == event_kind::REACTION => {
1266 let emoji_url = extract_reaction_emoji_url(&event.tags, &event.content);
1267 reactions_by_msg.entry(ref_id.clone()).or_default().push(Reaction {
1268 id: event.id.clone(), reference_id: ref_id.clone(),
1269 author_id: normalize_reaction_author(event.npub.clone().unwrap_or_default()),
1270 emoji: event.content.clone(),
1271 emoji_url,
1272 });
1273 }
1274 k if k == event_kind::MESSAGE_EDIT => {
1275 let decrypted = crate::crypto::maybe_decrypt(event.content.clone()).await
1276 .unwrap_or_else(|_| event.content.clone());
1277 let edit_emoji = crate::types::EmojiTag::extract_from_stored(&event.tags);
1278 edits_by_msg.entry(ref_id.clone()).or_default().push((event.created_at * 1000, decrypted, edit_emoji));
1279 }
1280 _ => {}
1281 }
1282 }
1283 }
1284 for edits in edits_by_msg.values_mut() {
1285 edits.sort_by_key(|(ts, _, _)| *ts);
1286 }
1287
1288 let mut attachments_by_msg: HashMap<String, Vec<Attachment>> = HashMap::new();
1290 for (_, event, tags_json) in &message_events {
1291 if event.kind != event_kind::FILE_ATTACHMENT && event.kind != event_kind::CHAT_MESSAGE {
1292 continue;
1293 }
1294 if let Some(val) = extract_tag_from_json(tags_json, "attachments") {
1295 if let Ok(atts) = serde_json::from_str::<Vec<Attachment>>(&val) {
1296 if !atts.is_empty() {
1297 attachments_by_msg.insert(event.id.clone(), atts);
1298 }
1299 }
1300 }
1301 }
1302
1303 let mut result: HashMap<String, Vec<Message>> = HashMap::new();
1305
1306 for (chat_identifier, event, tags_json) in message_events {
1307 let reactions = reactions_by_msg.remove(&event.id).unwrap_or_default();
1308 let attachments = attachments_by_msg.remove(&event.id).unwrap_or_default();
1309 let replied_to = extract_reply_tag_from_json(&tags_json).unwrap_or_default();
1310
1311 let original_content = if event.kind == event_kind::CHAT_MESSAGE
1313 || event.kind == event_kind::PRIVATE_DIRECT_MESSAGE
1314 {
1315 crate::crypto::maybe_decrypt(event.content.clone()).await
1316 .unwrap_or_else(|_| "[Decryption failed]".to_string())
1317 } else {
1318 String::new()
1319 };
1320
1321 let stored_tags = serde_json::from_str::<Vec<Vec<String>>>(&tags_json).unwrap_or_default();
1322 let original_emoji = crate::types::EmojiTag::extract_from_stored(&stored_tags);
1323 let addressed_bots = extract_bot_tags(&stored_tags);
1324 let expiration = extract_expiration_tag(&stored_tags);
1325 let (content, edited, edit_history, emoji_tags) = if let Some(edits) = edits_by_msg.remove(&event.id) {
1327 let (latest, latest_emoji) = edits.last()
1328 .map(|(_, c, e)| (c.clone(), e.clone()))
1329 .unwrap_or_else(|| (original_content.clone(), original_emoji.clone()));
1330 let history: Vec<crate::types::EditEntry> = std::iter::once(crate::types::EditEntry {
1331 content: original_content, edited_at: event.created_at * 1000,
1332 }).chain(edits.into_iter().map(|(ts, c, _)| crate::types::EditEntry { content: c, edited_at: ts }))
1333 .collect();
1334 (latest, true, Some(history), latest_emoji)
1335 } else {
1336 (original_content, false, None, original_emoji)
1337 };
1338
1339 let preview_metadata = event.preview_metadata
1340 .and_then(|json| serde_json::from_str(&json).ok());
1341
1342 result.entry(chat_identifier).or_default().push(Message {
1343 expiration,
1344 id: event.id, content, replied_to,
1345 replied_to_content: None, replied_to_npub: None, replied_to_has_attachment: None,
1346 replied_to_attachment_extension: None,
1347 preview_metadata, attachments, reactions, at: event.created_at * 1000,
1348 pending: event.pending, failed: event.failed, mine: event.mine,
1349 npub: event.npub, wrapper_event_id: event.wrapper_event_id,
1350 edited, edit_history,
1351 emoji_tags,
1352 addressed_bots,
1353 });
1354 }
1355
1356 Ok(result)
1357}
1358
1359pub async fn unread_counts() -> Result<std::collections::HashMap<String, u32>, String> {
1366 let conn = super::get_db_connection_guard_static()?;
1367 let mut stmt = conn
1371 .prepare(
1372 "SELECT c.chat_identifier, COUNT(*) AS unread \
1373 FROM events e JOIN chats c ON e.chat_id = c.id \
1374 WHERE e.kind IN (?1, ?2, ?3) AND e.mine = 0 \
1375 AND e.created_at > COALESCE(( \
1376 SELECT MAX(e2.created_at) FROM events e2 \
1377 WHERE e2.chat_id = c.id \
1378 AND ((e2.mine = 1 AND e2.kind IN (?1, ?2, ?3)) OR e2.id = c.last_read)), 0) \
1379 GROUP BY c.chat_identifier",
1380 )
1381 .map_err(|e| format!("prepare unread_counts: {e}"))?;
1382 let rows = stmt
1383 .query_map(
1384 rusqlite::params![
1385 event_kind::CHAT_MESSAGE as i32,
1386 event_kind::PRIVATE_DIRECT_MESSAGE as i32,
1387 event_kind::FILE_ATTACHMENT as i32
1388 ],
1389 |row| Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)? as u32)),
1390 )
1391 .map_err(|e| format!("query unread_counts: {e}"))?;
1392 let mut out = std::collections::HashMap::new();
1393 for r in rows.flatten() {
1394 out.insert(r.0, r.1);
1395 }
1396 Ok(out)
1397}
1398
1399#[derive(Debug, PartialEq)]
1403pub enum UnreadMark {
1404 NoOp,
1406 Clear,
1408 Anchor(String),
1410}
1411
1412pub async fn compute_unread_anchor(chat_identifier: &str) -> Result<UnreadMark, String> {
1416 let conn = super::get_db_connection_guard_static()?;
1417 let (k0, k1, k2) = (
1418 event_kind::CHAT_MESSAGE as i32,
1419 event_kind::PRIVATE_DIRECT_MESSAGE as i32,
1420 event_kind::FILE_ATTACHMENT as i32,
1421 );
1422 let (target_ts, newest_ts): (Option<i64>, Option<i64>) = conn
1424 .query_row(
1425 "SELECT MAX(CASE WHEN e.mine = 0 THEN e.created_at END), MAX(e.created_at) \
1426 FROM events e JOIN chats c ON e.chat_id = c.id \
1427 WHERE c.chat_identifier = ?1 AND e.kind IN (?2, ?3, ?4)",
1428 rusqlite::params![chat_identifier, k0, k1, k2],
1429 |row| Ok((row.get(0)?, row.get(1)?)),
1430 )
1431 .map_err(|e| format!("unread anchor target: {e}"))?;
1432
1433 let target_ts = match target_ts {
1434 Some(t) => t,
1435 None => return Ok(UnreadMark::NoOp), };
1437 if newest_ts.map_or(false, |n| n > target_ts) {
1438 return Ok(UnreadMark::NoOp); }
1440
1441 let anchor_id: Option<String> = conn
1442 .query_row(
1443 "SELECT e.id FROM events e JOIN chats c ON e.chat_id = c.id \
1444 WHERE c.chat_identifier = ?1 AND e.kind IN (?2, ?3, ?4) AND e.created_at < ?5 \
1445 ORDER BY e.created_at DESC LIMIT 1",
1446 rusqlite::params![chat_identifier, k0, k1, k2, target_ts],
1447 |row| row.get(0),
1448 )
1449 .optional()
1450 .map_err(|e| format!("unread anchor prev: {e}"))?;
1451
1452 Ok(match anchor_id {
1453 Some(id) => UnreadMark::Anchor(id),
1454 None => UnreadMark::Clear,
1455 })
1456}
1457
1458pub async fn save_chat_messages(chat_id: &str, messages: &[Message]) -> Result<(), String> {
1460 if messages.is_empty() {
1461 return Ok(());
1462 }
1463 for message in messages {
1464 if let Err(e) = save_message(chat_id, message).await {
1465 eprintln!("Failed to save message {}: {}", &message.id[..8.min(message.id.len())], e);
1466 }
1467 }
1468 Ok(())
1469}
1470
1471#[cfg(test)]
1472mod tests {
1473 use super::*;
1474 use crate::stored_event::SystemEventType;
1475
1476 static TEST_COUNTER: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(71000);
1477
1478 fn make_test_npub(n: u32) -> String {
1479 const BECH32: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
1480 let mut payload = vec![b'q'; 58];
1481 let mut x = n as u64;
1482 let mut i = 58;
1483 while x > 0 && i > 0 {
1484 i -= 1;
1485 payload[i] = BECH32[(x as usize) % 32];
1486 x /= 32;
1487 }
1488 format!("npub1{}", std::str::from_utf8(&payload).unwrap())
1489 }
1490
1491 fn init_test_db() -> (tempfile::TempDir, std::sync::MutexGuard<'static, ()>) {
1492 let guard = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
1493 crate::db::close_database();
1494 crate::db::clear_id_caches();
1497 let tmp = tempfile::tempdir().unwrap();
1498 let n = TEST_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1499 let account = make_test_npub(n);
1500 std::fs::create_dir_all(tmp.path().join(&account)).unwrap();
1501 crate::db::set_app_data_dir(tmp.path().to_path_buf());
1502 crate::db::set_current_account(account.clone()).unwrap();
1503 crate::db::init_database(&account).unwrap();
1504 (tmp, guard)
1505 }
1506
1507 fn now_secs() -> u64 {
1508 std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs()
1509 }
1510
1511 #[tokio::test]
1515 async fn system_event_stamps_authenticated_time_clamped_to_now() {
1516 let (_tmp, _guard) = init_test_db();
1517 let chat = "channel_ch2_timestamp";
1518 let before = now_secs();
1519 let past = before - 100_000;
1520
1521 save_system_event_at("ev_past", chat, SystemEventType::MemberJoined, "npubX", None, past, None, None).await.unwrap();
1522 save_system_event_at("ev_future", chat, SystemEventType::MemberJoined, "npubX", None, before + 100_000, None, None).await.unwrap();
1523 let after = now_secs();
1524
1525 let evs = get_system_events_for_chat(chat).unwrap();
1526 let past_ev = evs.iter().find(|e| e.id == "ev_past").expect("past event saved");
1527 assert_eq!(past_ev.created_at, past, "historical join keeps its real (authenticated) timestamp");
1528
1529 let fut_ev = evs.iter().find(|e| e.id == "ev_future").expect("future event saved");
1530 assert!(fut_ev.created_at >= before && fut_ev.created_at <= after,
1531 "future-dated event clamped to local now ({} not in {}..={})", fut_ev.created_at, before, after);
1532 }
1533
1534 #[tokio::test]
1537 async fn event_delete_context_resolves_from_db() {
1538 let (_tmp, _guard) = init_test_db();
1539 let chat = "npub1contactdc";
1540
1541 let mine_msg = Message { id: "dc_mine".into(), content: "x".into(), at: 1_000, mine: true, ..Default::default() };
1542 let theirs = Message {
1543 id: "dc_theirs".into(), content: "y".into(), at: 2_000, mine: false,
1544 npub: Some("npub1sender".to_string()),
1545 ..Default::default()
1546 };
1547 save_message(chat, &mine_msg).await.unwrap();
1548 save_message(chat, &theirs).await.unwrap();
1549
1550 let (chat_id, mine, _author) = event_delete_context("dc_mine").unwrap().expect("own row resolves");
1551 assert_eq!(chat_id, chat);
1552 assert!(mine);
1553
1554 let (chat_id, mine, author) = event_delete_context("dc_theirs").unwrap().expect("contact row resolves");
1555 assert_eq!(chat_id, chat);
1556 assert!(!mine);
1557 assert_eq!(author.as_deref(), Some("npub1sender"));
1558
1559 assert!(event_delete_context("dc_absent").unwrap().is_none(), "unknown id is None, not an error");
1560 }
1561
1562 #[tokio::test]
1565 async fn unread_counts_match_walk_back_semantics() {
1566 let (_tmp, _guard) = init_test_db();
1567 let chat = "npub1contactdm";
1568 let mk = |id: &str, secs: u64, mine: bool| Message {
1570 id: id.into(), content: "x".into(), at: secs * 1000, mine,
1571 npub: (!mine).then(|| "npub1sender".to_string()),
1572 ..Default::default()
1573 };
1574 let unread = || async { unread_counts().await.unwrap().get(chat).copied().unwrap_or(0) };
1575
1576 for i in 0..6u64 {
1578 save_message(chat, &mk(&format!("m{i}"), 1000 + i, false)).await.unwrap();
1579 }
1580 assert_eq!(unread().await, 6, "never-read backlog counts all 6");
1581
1582 save_message(chat, &mk("m6", 2000, false)).await.unwrap();
1584 save_message(chat, &mk("m7", 2001, false)).await.unwrap();
1585 assert_eq!(unread().await, 8, "6 backlog + 2 new = 8");
1586
1587 save_message(chat, &mk("mine", 2002, true)).await.unwrap();
1589 assert_eq!(unread().await, 0, "own message = read up to here");
1590
1591 save_message(chat, &mk("m8", 2003, false)).await.unwrap();
1593 assert_eq!(unread().await, 1, "one new after our send");
1594
1595 {
1597 let conn = crate::db::get_write_connection_guard_static().unwrap();
1598 conn.execute(
1599 "UPDATE chats SET last_read = ?1 WHERE chat_identifier = ?2",
1600 rusqlite::params!["m8", chat],
1601 ).unwrap();
1602 }
1603 assert_eq!(unread().await, 0, "last_read=m8 clears all");
1604 save_message(chat, &mk("m9", 2004, false)).await.unwrap();
1605 assert_eq!(unread().await, 1, "one arrival after last_read");
1606 }
1607
1608 #[tokio::test]
1612 async fn compute_unread_anchor_covers_the_cases() {
1613 let (_tmp, _guard) = init_test_db();
1614 let mk = |id: &str, secs: u64, mine: bool| Message {
1615 id: id.into(), content: "x".into(), at: secs * 1000, mine,
1616 npub: (!mine).then(|| "npub1sender".to_string()),
1617 ..Default::default()
1618 };
1619 let unread = |chat: &'static str| async move {
1620 unread_counts().await.unwrap().get(chat).copied().unwrap_or(0)
1621 };
1622 let set_lr = |chat: &str, lr: &str| {
1623 let conn = crate::db::get_write_connection_guard_static().unwrap();
1624 conn.execute("UPDATE chats SET last_read = ?1 WHERE chat_identifier = ?2",
1625 rusqlite::params![lr, chat]).unwrap();
1626 };
1627
1628 let a = "npub1anchorA";
1631 save_message(a, &mk("a_mine", 1000, true)).await.unwrap();
1632 for i in 0..8u64 { save_message(a, &mk(&format!("a{i}"), 2000 + i, false)).await.unwrap(); }
1633 assert_eq!(compute_unread_anchor(a).await.unwrap(), UnreadMark::Anchor("a6".into()));
1634 set_lr(a, "a6");
1635 assert_eq!(unread(a).await, 1, "A: newest contact message is the sole unread");
1636
1637 let b = "npub1anchorB";
1639 save_message(b, &mk("b0", 2000, false)).await.unwrap();
1640 save_message(b, &mk("b_mine", 2001, true)).await.unwrap();
1641 assert_eq!(compute_unread_anchor(b).await.unwrap(), UnreadMark::NoOp);
1642
1643 let c = "npub1anchorC";
1646 save_message(c, &mk("c0", 3000, false)).await.unwrap();
1647 save_message(c, &mk("c1", 3005, false)).await.unwrap();
1648 save_message(c, &mk("c2", 3005, false)).await.unwrap();
1649 assert_eq!(compute_unread_anchor(c).await.unwrap(), UnreadMark::Anchor("c0".into()));
1650 set_lr(c, "c0");
1651 assert_eq!(unread(c).await, 2, "C: same-second tail both count");
1652
1653 let d = "npub1anchorD";
1655 save_message(d, &mk("d0", 4000, false)).await.unwrap();
1656 assert_eq!(compute_unread_anchor(d).await.unwrap(), UnreadMark::Clear);
1657 set_lr(d, "");
1658 assert_eq!(unread(d).await, 1, "D: lone contact message surfaces");
1659
1660 let e = "npub1anchorE";
1662 save_message(e, &mk("e_mine", 5000, true)).await.unwrap();
1663 assert_eq!(compute_unread_anchor(e).await.unwrap(), UnreadMark::NoOp);
1664 }
1665
1666 #[tokio::test]
1670 async fn unread_clears_when_last_read_is_a_system_event() {
1671 let (_tmp, _guard) = init_test_db();
1672 let chat = "npub1sysevtdm";
1673 let mk = |id: &str, secs: u64| Message {
1674 id: id.into(), content: "x".into(), at: secs * 1000, mine: false,
1675 npub: Some("npub1sender".to_string()), ..Default::default()
1676 };
1677 let unread = || async { unread_counts().await.unwrap().get(chat).copied().unwrap_or(0) };
1678
1679 for i in 0..5u64 {
1682 save_message(chat, &mk(&format!("m{i}"), 1000 + i)).await.unwrap();
1683 }
1684 assert_eq!(unread().await, 5, "never-read backlog");
1685
1686 save_system_event_at("sysev", chat, SystemEventType::MemberJoined, "npubX", None, 2000, None, None).await.unwrap();
1688
1689 {
1691 let conn = crate::db::get_write_connection_guard_static().unwrap();
1692 conn.execute(
1693 "UPDATE chats SET last_read = ?1 WHERE chat_identifier = ?2",
1694 rusqlite::params!["sysev", chat],
1695 ).unwrap();
1696 }
1697 assert_eq!(unread().await, 0, "read marker on a system event still clears the badge");
1698 }
1699
1700 #[tokio::test]
1704 async fn deleting_a_message_adjusts_unread_without_wedging() {
1705 let (_tmp, _guard) = init_test_db();
1706 let chat = "npub1delunread";
1707 let mk = |id: &str, secs: u64| Message {
1708 id: id.into(), content: "x".into(), at: secs * 1000, mine: false,
1709 npub: Some("npub1sender".to_string()), ..Default::default()
1710 };
1711 let unread = || async { unread_counts().await.unwrap().get(chat).copied().unwrap_or(0) };
1712 let set_marker = |id: &str| {
1713 let conn = crate::db::get_write_connection_guard_static().unwrap();
1714 conn.execute("UPDATE chats SET last_read = ?1 WHERE chat_identifier = ?2",
1715 rusqlite::params![id, chat]).unwrap();
1716 };
1717 let marker = || -> String {
1718 let conn = crate::db::get_db_connection_guard_static().unwrap();
1719 conn.query_row("SELECT last_read FROM chats WHERE chat_identifier = ?1",
1720 rusqlite::params![chat], |r| r.get::<_, String>(0)).unwrap()
1721 };
1722
1723 for i in 0..6u64 { save_message(chat, &mk(&format!("m{i}"), 1000 + i)).await.unwrap(); }
1725 set_marker("m1");
1726 assert_eq!(unread().await, 4, "m2..m5 unread");
1727
1728 delete_event("m3").await.unwrap();
1730 assert_eq!(unread().await, 3, "one unread deleted → badge minus one");
1731 assert_eq!(marker(), "m1", "deleting an unread message leaves the marker alone");
1732
1733 delete_event("m1").await.unwrap();
1735 assert_eq!(marker(), "m0", "marker retreats to the newest survivor before it");
1736 assert_eq!(unread().await, 3, "retreat keeps the count, no collapse to 99+");
1737
1738 delete_event("m0").await.unwrap();
1740 assert_eq!(marker(), "", "no earlier survivor → marker clears");
1741 assert_eq!(unread().await, 3, "cleared marker counts only the true unread survivors");
1742 }
1743
1744 #[tokio::test]
1748 async fn edit_event_folds_into_history_on_reload() {
1749 let (_tmp, _guard) = init_test_db();
1750 let chat = "channel_edit_fold";
1751 save_message(chat, &Message {
1752 id: "orig1".into(), content: "original".into(), at: 5_000_000,
1753 npub: Some("npub1author".into()), ..Default::default()
1754 }).await.unwrap();
1755
1756 let cid = crate::db::id_cache::get_chat_id_by_identifier(chat).unwrap();
1757 let emoji = vec![crate::types::EmojiTag { shortcode: "wave".into(), url: "u/wave".into() }];
1758 save_edit_event("edit1", "orig1", "edited :wave:", &emoji, cid, None, "npub1author").await.unwrap();
1759
1760 let m = get_message_views(cid, 50, 0).await.unwrap()
1761 .into_iter().find(|m| m.id == "orig1").expect("message reloaded");
1762 assert!(m.edited, "folded edit sets the edited flag");
1763 let h = m.edit_history.as_ref().expect("history reconstructed from the edit event");
1764 assert_eq!(h.len(), 2, "original + one edit");
1765 assert_eq!(h[0].content, "original");
1766 assert_eq!(h[1].content, "edited :wave:");
1767 assert_eq!(m.content, "edited :wave:", "latest revision is the displayed content");
1768 assert_eq!(m.emoji_tags.len(), 1, "the edit's own emoji folds onto the message");
1769 assert_eq!(m.emoji_tags[0].shortcode, "wave");
1770 }
1771}