notes to self

This commit is contained in:
nnduc
2026-10-29 13:17:40 +07:00
parent 1ce93d883b
commit 5f37fcfe7a
4 changed files with 72 additions and 25 deletions
+68 -19
View File
@@ -785,11 +785,15 @@ pub async fn message_by_id(db: &Db, user: &str, id: &str) -> Result<Option<Messa
Ok(on!(db, p => 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<Option<Message>> {
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::<Vec<_>>().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<i64> {
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<Option<String>> {
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<Option<String>> {
@@ -1305,10 +1310,10 @@ pub async fn messages_search(
let words: Vec<String> = 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<i64> = 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<Vec<(i64, String, Option<String>, 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::<Vec<_>>(), ["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)");
+1 -2
View File
@@ -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");
+2 -2
View File
@@ -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 {
+1 -2
View File
@@ -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 {