diff --git a/src/db.rs b/src/db.rs index 7253996..c18ff27 100644 --- a/src/db.rs +++ b/src/db.rs @@ -785,11 +785,15 @@ pub async fn message_by_id(db: &Db, user: &str, id: &str) -> Result sqlx::query_as::<_, Message>(sql).bind(user).bind(id).fetch_optional(p).await?)) } +/// Messages exchanged with `jid` (bound three times). With our own JID, only +/// the notes sent to ourself, not everything we sent or received. +const WITH_JID: &str = "(jidfrom = ? OR jidto = ?) AND (jidfrom = jidto OR ? != user_id)"; + /// Resolve a message referenced by another one (reply, reaction, retraction, receipt) /// from the conversation `jid`, by its message id, origin id or stanza id. pub async fn message_resolve_reference(db: &Db, user: &str, jid: &str, reference: &str) -> Result> { let sql = db.sql(&format!( - "SELECT {MESSAGE_COLUMNS} FROM messages WHERE user_id = ? AND (jidfrom = ? OR jidto = ?) + "SELECT {MESSAGE_COLUMNS} FROM messages WHERE user_id = ? AND {WITH_JID} AND (messageid = ? OR originid = ? OR stanzaid = ? OR id = ?) ORDER BY mid DESC LIMIT 1" )); Ok(on!(db, p => { @@ -797,6 +801,7 @@ pub async fn message_resolve_reference(db: &Db, user: &str, jid: &str, reference .bind(user) .bind(jid) .bind(jid) + .bind(jid) .bind(reference) .bind(reference) .bind(reference) @@ -903,7 +908,7 @@ pub async fn messages_for( let types = if muc { MESSAGE_TYPE_MUC } else { MESSAGE_TYPE }; let types_sql = types.iter().map(|t| format!("'{t}'")).collect::>().join(","); let mut sql = format!( - "SELECT {MESSAGE_COLUMNS} FROM messages WHERE user_id = ? AND (jidfrom = ? OR jidto = ?) AND type IN ({types_sql})" + "SELECT {MESSAGE_COLUMNS} FROM messages WHERE user_id = ? AND {WITH_JID} AND type IN ({types_sql})" ); if muc { sql.push_str(" AND subject IS NULL"); @@ -919,7 +924,7 @@ pub async fn messages_for( let sql = db.sql(&sql); Ok(on!(db, p => { - let mut q = sqlx::query_as::<_, Message>(sql).bind(user).bind(jid).bind(jid); + let mut q = sqlx::query_as::<_, Message>(sql).bind(user).bind(jid).bind(jid).bind(jid); if let Some(b) = before { q = q.bind(b); } @@ -931,8 +936,8 @@ pub async fn messages_for( } pub async fn messages_count(db: &Db, user: &str, jid: &str) -> Result { - let sql = db.sql("SELECT COUNT(*) FROM messages WHERE user_id = ? AND (jidfrom = ? OR jidto = ?)"); - Ok(on!(db, p => sqlx::query_scalar(sql).bind(user).bind(jid).bind(jid).fetch_one(p).await?)) + let sql = db.sql(&format!("SELECT COUNT(*) FROM messages WHERE user_id = ? AND {WITH_JID}")); + Ok(on!(db, p => sqlx::query_scalar(sql).bind(user).bind(jid).bind(jid).bind(jid).fetch_one(p).await?)) } /// Clear the local history of a conversation. The archive is marked as fully @@ -942,11 +947,11 @@ pub async fn messages_clear(db: &Db, user: &str, jid: &str, archive_key: &str) - on!(db, p => { let mut tx = p.begin().await?; for sql in [ - "DELETE FROM reactions WHERE message_mid IN (SELECT mid FROM messages WHERE user_id = ? AND (jidfrom = ? OR jidto = ?))", - "DELETE FROM message_files WHERE message_mid IN (SELECT mid FROM messages WHERE user_id = ? AND (jidfrom = ? OR jidto = ?))", - "DELETE FROM messages WHERE user_id = ? AND (jidfrom = ? OR jidto = ?)", + format!("DELETE FROM reactions WHERE message_mid IN (SELECT mid FROM messages WHERE user_id = ? AND {WITH_JID})"), + format!("DELETE FROM message_files WHERE message_mid IN (SELECT mid FROM messages WHERE user_id = ? AND {WITH_JID})"), + format!("DELETE FROM messages WHERE user_id = ? AND {WITH_JID}"), ] { - sqlx::query(db.sql(sql)).bind(user).bind(jid).bind(jid).execute(&mut *tx).await?; + sqlx::query(db.sql(&sql)).bind(user).bind(jid).bind(jid).bind(jid).execute(&mut *tx).await?; } sqlx::query(db.sql( "INSERT INTO mam_earliests (user_id, jid, earliest) VALUES (?, ?, NULL) @@ -1194,8 +1199,8 @@ pub async fn mam_earliest_set(db: &Db, user: &str, jid: &str, earliest: Option<& } pub async fn earliest_message_date(db: &Db, user: &str, jid: &str) -> Result> { - let sql = db.sql("SELECT MIN(published) FROM messages WHERE user_id = ? AND (jidfrom = ? OR jidto = ?)"); - Ok(on!(db, p => sqlx::query_scalar(sql).bind(user).bind(jid).bind(jid).fetch_one(p).await?)) + let sql = db.sql(&format!("SELECT MIN(published) FROM messages WHERE user_id = ? AND {WITH_JID}")); + Ok(on!(db, p => sqlx::query_scalar(sql).bind(user).bind(jid).bind(jid).bind(jid).fetch_one(p).await?)) } pub async fn latest_message_date(db: &Db, user: &str) -> Result> { @@ -1305,10 +1310,10 @@ pub async fn messages_search( let words: Vec = crate::utils::search_normalize(keywords).split_whitespace().take(8).map(String::from).collect(); let sql = db.sql(&format!( - "SELECT mid, body FROM messages WHERE user_id = ? AND (jidfrom = ? OR jidto = ?) AND type IN ({types_sql}) + "SELECT mid, body FROM messages WHERE user_id = ? AND {WITH_JID} AND type IN ({types_sql}) AND encrypted = FALSE AND retracted = FALSE AND body IS NOT NULL AND {NOT_REPLACED} ORDER BY published DESC" )); - let rows: Vec<(i64, String)> = on!(db, p => sqlx::query_as(sql).bind(user).bind(jid).bind(jid).fetch_all(p).await?); + let rows: Vec<(i64, String)> = on!(db, p => sqlx::query_as(sql).bind(user).bind(jid).bind(jid).bind(jid).fetch_all(p).await?); let mut mids: Vec = rows .into_iter() .filter(|(_, body)| { @@ -1325,11 +1330,11 @@ pub async fn messages_search( let placeholders = vec!["?"; mids.len()].join(","); let sql = db.sql(&format!( - "SELECT {MESSAGE_COLUMNS} FROM messages WHERE user_id = ? AND (jidfrom = ? OR jidto = ?) AND type IN ({types_sql}) + "SELECT {MESSAGE_COLUMNS} FROM messages WHERE user_id = ? AND {WITH_JID} AND type IN ({types_sql}) AND retracted = FALSE AND mid IN ({placeholders}) ORDER BY published DESC LIMIT ?" )); Ok(on!(db, p => { - let mut q = sqlx::query_as::<_, Message>(sql).bind(user).bind(jid).bind(jid); + let mut q = sqlx::query_as::<_, Message>(sql).bind(user).bind(jid).bind(jid).bind(jid); for mid in &mids { q = q.bind(mid); } @@ -1349,10 +1354,10 @@ const NOT_REPLACED: &str = "NOT EXISTS (SELECT 1 FROM messages r WHERE r.user_id pub async fn encrypted_index(db: &Db, user: &str, jid: &str, muc: bool) -> Result, String)>> { let types_sql = conversation_types(muc); let sql = db.sql(&format!( - "SELECT mid, id, messageid, published FROM messages WHERE user_id = ? AND (jidfrom = ? OR jidto = ?) + "SELECT mid, id, messageid, published FROM messages WHERE user_id = ? AND {WITH_JID} AND type IN ({types_sql}) AND encrypted = TRUE AND retracted = FALSE AND {NOT_REPLACED} ORDER BY published DESC LIMIT 5000" )); - Ok(on!(db, p => sqlx::query_as(sql).bind(user).bind(jid).bind(jid).fetch_all(p).await?)) + Ok(on!(db, p => sqlx::query_as(sql).bind(user).bind(jid).bind(jid).bind(jid).fetch_all(p).await?)) } #[derive(Debug, Clone, FromRow, Serialize)] @@ -1372,10 +1377,10 @@ pub async fn conversation_files(db: &Db, user: &str, jid: &str, muc: bool, limit let types_sql = conversation_types(muc); let sql = db.sql(&format!( "SELECT m.mid, f.name, f.type, f.size, f.url, m.published FROM message_files f JOIN messages m ON m.mid = f.message_mid - WHERE m.user_id = ? AND (m.jidfrom = ? OR m.jidto = ?) AND m.type IN ({types_sql}) AND m.retracted = FALSE + WHERE m.user_id = ? AND (m.jidfrom = ? OR m.jidto = ?) AND (m.jidfrom = m.jidto OR ? != m.user_id) AND m.type IN ({types_sql}) AND m.retracted = FALSE ORDER BY m.published DESC LIMIT ?" )); - Ok(on!(db, p => sqlx::query_as(sql).bind(user).bind(jid).bind(jid).bind(limit).fetch_all(p).await?)) + Ok(on!(db, p => sqlx::query_as(sql).bind(user).bind(jid).bind(jid).bind(jid).bind(limit).fetch_all(p).await?)) } /// Forget everything stored for an account @@ -1488,6 +1493,50 @@ pub mod tests { db.close().await; } + #[tokio::test] + async fn notes_to_self_sqlite() { + let (db, path) = sqlite_test_db("notes").await; + notes_to_self(&db).await; + db.close().await; + remove_sqlite(&path); + } + + #[tokio::test] + async fn notes_to_self_postgres() { + let Some(url) = postgres_test_url("notes").await else { return }; + let db = connect(&url).await.unwrap(); + notes_to_self(&db).await; + db.close().await; + } + + /// The conversation with our own JID only holds the notes, not every message + async fn notes_to_self(db: &Db) { + let (me, bob) = ("me@example.org", "bob@example.org"); + let m = Message { + user_id: me.into(), + id: "n1".into(), + jidfrom: bob.into(), + jidto: me.into(), + type_: "chat".into(), + body: Some("hi".into()), + published: "2026-01-01 10:00:00".into(), + ..Default::default() + }; + message_save(db, &m).await.unwrap(); + message_save(db, &Message { id: "n2".into(), jidfrom: me.into(), jidto: bob.into(), ..m.clone() }).await.unwrap(); + message_save(db, &Message { id: "n3".into(), jidfrom: me.into(), jidto: me.into(), body: Some("note".into()), ..m.clone() }) + .await + .unwrap(); + + assert_eq!(messages_count(db, me, bob).await.unwrap(), 2); + let notes = messages_for(db, me, me, false, None, None, 10).await.unwrap(); + assert_eq!(notes.iter().map(|n| n.id.as_str()).collect::>(), ["n3"]); + + messages_clear(db, me, me, me).await.unwrap(); + assert_eq!(messages_count(db, me, me).await.unwrap(), 0); + assert_eq!(messages_count(db, me, bob).await.unwrap(), 2); + } + #[test] fn postgres_placeholders() { assert_eq!(to_postgres("SELECT a FROM t WHERE b = ? AND c LIKE 'x?%' AND d IN (?, ?)"), "SELECT a FROM t WHERE b = $1 AND c LIKE 'x?%' AND d IN ($2, $3)"); diff --git a/src/widgets/chat.rs b/src/widgets/chat.rs index 7dd7fcb..97df6d6 100644 --- a/src/widgets/chat.rs +++ b/src/widgets/chat.rs @@ -230,8 +230,7 @@ async fn prepare_empty(ctx: &Ctx) -> String { /// Open a 1:1 discussion (or the empty view) pub async fn get(ctx: &Ctx, jid: Option<&str>, light: bool) { - let me = ctx.jid(); - match jid.filter(|j| !j.is_empty() && *j != me) { + match jid.filter(|j| !j.is_empty()) { None => { rpc!(ctx, "MovimTpl.hidePanel"); rpc!(ctx, "Notif.current", "chat"); diff --git a/src/widgets/chats.rs b/src/widgets/chats.rs index aa6ca8b..d06aa3c 100644 --- a/src/widgets/chats.rs +++ b/src/widgets/chats.rs @@ -73,7 +73,7 @@ pub async fn prepare_chats(ctx: &Ctx) -> String { let chats = db::open_chats(ctx.db(), &me).await.unwrap_or_default(); let mut html = String::new(); for jid in chats.iter().rev() { - if utils::validate_jid(jid) && *jid != me { + if utils::validate_jid(jid) { html.push_str(&prepare_chat(ctx, jid).await); } } @@ -168,7 +168,7 @@ pub async fn refresh_item(ctx: &Ctx, jid: &str) { pub async fn open(ctx: &Ctx, jid: &str, and_show: bool, history: bool) { let me = ctx.jid(); - if !utils::validate_jid(jid) || jid == me { + if !utils::validate_jid(jid) { return; } if history { diff --git a/src/widgets/search.rs b/src/widgets/search.rs index f9c4519..5a5cef6 100644 --- a/src/widgets/search.rs +++ b/src/widgets/search.rs @@ -23,8 +23,7 @@ pub async fn call(ctx: &Ctx, widget: &str, func: &str, p: &P<'_>) -> bool { ("Search", "ajaxSearch") => { let key = p.str(0).replace("xmpp:", "").trim().to_string(); let key = key.split('?').next().unwrap_or("").to_string(); - let me = ctx.jid(); - let html = if utils::validate_bare_jid(&key) && key.contains('@') && key != me { + let html = if utils::validate_bare_jid(&key) && key.contains('@') { let picture = ctx.placeholder_url(&key); ctx.render("Search/_search_results.html", json!({ "jid": key, "picture": picture })) } else {