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