[Fix] Connection Management

This commit is contained in:
Alex Emmet 2026-09-13 20:58:41 +02:00
commit 3f2ac18333
No known key found for this signature in database
122 changed files with 19970 additions and 5263 deletions

View file

@ -6,35 +6,48 @@ use rusqlite::{OptionalExtension, params};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BlockMutation {
pub blocked_user_id: i64,
pub blocked_principal: iota_identity::PrincipalHandle,
pub revision: i64,
pub deleted: bool,
}
fn validate(user_id: i64, blocked_user_id: i64) -> Result<(), StorageError> {
if user_id <= 0 || blocked_user_id <= 0 || user_id == blocked_user_id {
fn validate(
user_id: i64,
blocked_principal: iota_identity::PrincipalHandle,
) -> Result<(), StorageError> {
if user_id <= 0 || blocked_principal.0 <= 0 {
Err(StorageError::Other("invalid block relationship".into()))
} else {
Ok(())
}
}
pub fn block(user_id: i64, blocked_user_id: i64) -> Result<BlockMutation, StorageError> {
validate(user_id, blocked_user_id)?;
pub fn block(
user_id: i64,
blocked_principal: iota_identity::PrincipalHandle,
) -> Result<BlockMutation, StorageError> {
validate(user_id, blocked_principal)?;
db::with_immediate_transaction(|tx| {
let prior = tx.query_row("SELECT id, revision FROM blocked_users WHERE user_id = ?1 AND blocked_user_id = ?2", params![user_id, blocked_user_id], |r| Ok((r.get::<_, i64>(0)?, r.get::<_, i64>(1)?))).optional()?;
let blocked_user_id = tx.query_row(
"SELECT remote_user_id FROM principals WHERE principal_pk = ?1",
[blocked_principal.0],
|row| row.get::<_, i64>(0),
)?;
let prior = tx.query_row("SELECT id, revision FROM blocked_users WHERE user_id = ?1 AND blocked_principal = ?2", params![user_id, blocked_principal.0], |r| Ok((r.get::<_, i64>(0)?, r.get::<_, i64>(1)?))).optional()?;
if let Some((_, revision)) = prior {
return Ok(BlockMutation {
blocked_user_id,
blocked_principal,
revision,
deleted: false,
});
}
let created_at = sync::now_millis();
tx.execute("INSERT INTO blocked_users (user_id, blocked_user_id, revision, created_at) VALUES (?1, ?2, 0, ?3)", params![user_id, blocked_user_id, created_at])?;
tx.execute("INSERT INTO blocked_users (user_id, blocked_user_id, blocked_principal, revision, created_at) VALUES (?1, ?2, ?3, 0, ?4)", params![user_id, blocked_user_id, blocked_principal.0, created_at])?;
let id = tx.last_insert_rowid();
let revision = sync::record_event(
tx,
user_id,
sync::EntityType::BlockedUser,
id,
blocked_principal.0,
sync::Operation::Upsert,
)?;
tx.execute(
@ -43,41 +56,48 @@ pub fn block(user_id: i64, blocked_user_id: i64) -> Result<BlockMutation, Storag
)?;
Ok(BlockMutation {
blocked_user_id,
blocked_principal,
revision,
deleted: false,
})
})
}
pub fn unblock(user_id: i64, blocked_user_id: i64) -> Result<Option<BlockMutation>, StorageError> {
validate(user_id, blocked_user_id)?;
pub fn unblock(
user_id: i64,
blocked_principal: iota_identity::PrincipalHandle,
) -> Result<Option<BlockMutation>, StorageError> {
validate(user_id, blocked_principal)?;
db::with_immediate_transaction(|tx| {
let id = tx
let row = tx
.query_row(
"SELECT id FROM blocked_users WHERE user_id = ?1 AND blocked_user_id = ?2",
params![user_id, blocked_user_id],
|r| r.get(0),
"SELECT id, blocked_user_id FROM blocked_users WHERE user_id = ?1 AND blocked_principal = ?2",
params![user_id, blocked_principal.0],
|r| Ok((r.get::<_, i64>(0)?, r.get::<_, i64>(1)?)),
)
.optional()?;
let Some(id) = id else {
let Some((id, blocked_user_id)) = row else {
return Ok(None);
};
let revision = sync::record_event(
tx,
user_id,
sync::EntityType::BlockedUser,
id,
blocked_principal.0,
sync::Operation::Delete,
)?;
tx.execute("DELETE FROM blocked_users WHERE id = ?1", [id])?;
Ok(Some(BlockMutation {
blocked_user_id,
blocked_principal,
revision,
deleted: true,
}))
})
}
pub fn is_blocked(user_id: i64, candidate_sender_id: i64) -> Result<bool, StorageError> {
validate(user_id, candidate_sender_id)?;
if user_id <= 0 || candidate_sender_id <= 0 || user_id == candidate_sender_id {
return Err(StorageError::Other("invalid block relationship".into()));
}
db::with_db(|conn| {
Ok(conn
.query_row(
@ -89,16 +109,42 @@ pub fn is_blocked(user_id: i64, candidate_sender_id: i64) -> Result<bool, Storag
.is_some())
})
}
pub fn list(user_id: i64) -> Result<Vec<i64>, StorageError> {
pub fn is_principal_blocked(
user_id: i64,
candidate: iota_identity::PrincipalHandle,
) -> Result<bool, StorageError> {
if user_id <= 0 || candidate.0 <= 0 {
return Err(StorageError::Other("invalid block relationship".into()));
}
db::with_db(|conn| {
Ok(conn
.query_row(
"SELECT 1 FROM blocked_users WHERE user_id = ?1 AND blocked_principal = ?2",
params![user_id, candidate.0],
|_| Ok(()),
)
.optional()?
.is_some())
})
}
pub fn list(user_id: i64) -> Result<Vec<BlockMutation>, StorageError> {
if user_id <= 0 {
return Err(StorageError::Other("invalid block owner".into()));
}
db::with_db(|conn| {
let mut stmt = conn.prepare(
"SELECT blocked_user_id FROM blocked_users WHERE user_id = ?1 ORDER BY blocked_user_id",
"SELECT blocked_user_id, blocked_principal, revision FROM blocked_users WHERE user_id = ?1 ORDER BY blocked_principal",
)?;
stmt.query_map([user_id], |r| r.get(0))?
.collect::<Result<Vec<_>, _>>()
.map_err(StorageError::from)
stmt.query_map([user_id], |r| {
Ok(BlockMutation {
blocked_user_id: r.get(0)?,
blocked_principal: iota_identity::PrincipalHandle(r.get(1)?),
revision: r.get(2)?,
deleted: false,
})
})?
.collect::<Result<Vec<_>, _>>()
.map_err(StorageError::from)
})
}

View file

@ -50,7 +50,9 @@ impl MessageState {
pub struct StoredMessage {
pub id: i64,
pub external_user: i64,
pub external_principal: Option<iota_identity::PrincipalHandle>,
pub relay_signer_id: Option<i64>,
pub relay_signer_principal: Option<iota_identity::PrincipalHandle>,
pub relay_message_id: Option<String>,
pub message_time: i64,
pub authored_at: Option<i64>,
@ -74,11 +76,13 @@ pub struct StoredMessage {
pub struct NewMessage<'a> {
pub relay_signer_id: i64,
pub relay_signer_principal: iota_identity::PrincipalHandle,
pub relay_message_id: &'a str,
pub authored_at: i64,
pub send_time: i64,
pub storage_owner: i64,
pub external_user: i64,
pub external_principal: iota_identity::PrincipalHandle,
pub sent_by_self: bool,
pub content: &'a str,
pub height: i64,
@ -109,6 +113,26 @@ pub fn edit_message(
update_message_content(
storage_owner,
external_user,
None,
message_time,
editor_id,
new_content,
true,
)
}
pub fn edit_message_for_principal(
storage_owner: i64,
external_user: i64,
external_principal: iota_identity::PrincipalHandle,
message_time: i64,
editor_id: i64,
new_content: &str,
) -> Result<(), StorageError> {
update_message_content(
storage_owner,
external_user,
Some(external_principal),
message_time,
editor_id,
new_content,
@ -133,6 +157,31 @@ pub fn apply_remote_edit(
update_message_content(
storage_owner,
external_user,
None,
message_time,
editor_id,
new_content,
false,
)
}
pub fn apply_remote_edit_for_principal(
storage_owner: i64,
external_user: i64,
external_principal: iota_identity::PrincipalHandle,
message_time: i64,
editor_id: i64,
new_content: &str,
) -> Result<(), StorageError> {
if editor_id != external_user {
return Err(StorageError::Other(
"Remote editor does not match chat partner".into(),
));
}
update_message_content(
storage_owner,
external_user,
Some(external_principal),
message_time,
editor_id,
new_content,
@ -143,6 +192,7 @@ pub fn apply_remote_edit(
fn update_message_content(
storage_owner: i64,
external_user: i64,
external_principal: Option<iota_identity::PrincipalHandle>,
message_time: i64,
editor_id: i64,
new_content: &str,
@ -153,10 +203,20 @@ fn update_message_content(
r#"
SELECT id, content, sent_by_self, history_deleted
FROM messages
WHERE storage_owner = ?1 AND external_user = ?2 AND message_time = ?3
WHERE storage_owner = ?1
AND message_time = ?3
AND (
(?4 IS NULL AND external_user = ?2)
OR external_principal = ?4
)
ORDER BY id DESC LIMIT 1
"#,
params![storage_owner, external_user, message_time],
params![
storage_owner,
external_user,
message_time,
external_principal.map(|principal| principal.0)
],
|row| {
Ok((
row.get::<_, i64>(0)?,
@ -221,16 +281,35 @@ pub fn hard_delete_message(
storage_owner: i64,
external_user: i64,
message_time: i64,
) -> Result<(), StorageError> {
hard_delete_message_with_principal(storage_owner, external_user, None, message_time)
}
fn hard_delete_message_with_principal(
storage_owner: i64,
external_user: i64,
external_principal: Option<iota_identity::PrincipalHandle>,
message_time: i64,
) -> Result<(), StorageError> {
db::with_db(|conn| {
let msg_id = conn
.query_row(
r#"
SELECT id, history_deleted FROM messages
WHERE storage_owner = ?1 AND external_user = ?2 AND message_time = ?3
WHERE storage_owner = ?1
AND message_time = ?3
AND (
(?4 IS NULL AND external_user = ?2)
OR external_principal = ?4
)
ORDER BY id DESC LIMIT 1
"#,
params![storage_owner, external_user, message_time],
params![
storage_owner,
external_user,
message_time,
external_principal.map(|principal| principal.0)
],
|row| row.get::<_, i64>(0),
)
.optional()?;
@ -277,14 +356,14 @@ pub fn remove_message_history_in_tx(
storage_owner: i64,
message_id: i64,
) -> Result<(), StorageError> {
let message: Option<(i64, i64)> = tx
let message: Option<(i64, Option<i64>, i64)> = tx
.query_row(
"SELECT external_user, history_deleted FROM messages WHERE id = ?1 AND storage_owner = ?2",
"SELECT external_user, external_principal, history_deleted FROM messages WHERE id = ?1 AND storage_owner = ?2",
params![message_id, storage_owner],
|row| Ok((row.get(0)?, row.get(1)?)),
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
)
.optional()?;
let Some((external_user, history_deleted)) = message else {
let Some((external_user, external_principal, history_deleted)) = message else {
return Ok(());
};
if history_deleted != 0 {
@ -295,7 +374,7 @@ pub fn remove_message_history_in_tx(
[message_id],
)?;
tx.execute("DELETE FROM reactions WHERE message_id = ?1", [message_id])?;
tx.execute("DELETE FROM message_receipts WHERE storage_owner = ?1 AND EXISTS (SELECT 1 FROM messages WHERE id = ?2 AND relay_signer_id = message_receipts.target_signer_id AND relay_message_id = message_receipts.target_message_id)", params![storage_owner, message_id])?;
tx.execute("DELETE FROM message_receipts WHERE storage_owner = ?1 AND EXISTS (SELECT 1 FROM messages WHERE id = ?2 AND relay_signer_principal = message_receipts.target_signer_principal AND relay_message_id = message_receipts.target_message_id)", params![storage_owner, message_id])?;
tx.execute("UPDATE messages SET content = '', history_deleted = 1, history_deleted_at = ?2, expires_at = NULL, client_received_at = NULL, client_received_recorded_at = NULL, read_at = NULL, read_recorded_at = NULL WHERE id = ?1", params![message_id, sync::now_millis()])?;
sync::record_event(
tx,
@ -304,7 +383,12 @@ pub fn remove_message_history_in_tx(
message_id,
Operation::Delete,
)?;
update_contact_last_message_in_tx(tx, storage_owner, external_user)?;
update_contact_last_message_in_tx(
tx,
storage_owner,
external_user,
external_principal.map(iota_identity::PrincipalHandle),
)?;
Ok(())
}
@ -312,8 +396,12 @@ fn update_contact_last_message_in_tx(
tx: &Transaction<'_>,
storage_owner: i64,
external_user: i64,
external_principal: Option<iota_identity::PrincipalHandle>,
) -> Result<(), StorageError> {
tx.execute("UPDATE contacts SET last_message_at = (SELECT MAX(COALESCE(destination_iota_received_at, origin_iota_received_at, authored_at, message_time)) FROM messages WHERE storage_owner = ?1 AND external_user = ?2 AND deleted_by_external = 0 AND history_deleted = 0) WHERE storage_owner = ?1 AND user_id = ?2", params![storage_owner, external_user])?;
tx.execute(
"UPDATE contacts SET last_message_at = (SELECT MAX(COALESCE(destination_iota_received_at, origin_iota_received_at, authored_at, message_time)) FROM messages WHERE storage_owner = ?1 AND ((?3 IS NULL AND external_user = ?2) OR external_principal = ?3) AND deleted_by_external = 0 AND history_deleted = 0) WHERE storage_owner = ?1 AND ((?3 IS NULL AND user_id = ?2) OR principal_handle = ?3)",
params![storage_owner, external_user, external_principal.map(|principal| principal.0)],
)?;
Ok(())
}
@ -322,15 +410,17 @@ pub fn purge_message_in_tx(
storage_owner: i64,
message_id: i64,
) -> Result<(), StorageError> {
let (external_user, relay_signer_id, relay_message_id) = tx.query_row(
"SELECT external_user, relay_signer_id, relay_message_id FROM messages WHERE id = ?1 AND storage_owner = ?2",
let (external_user, external_principal, relay_signer_principal, relay_message_id) = tx.query_row(
"SELECT external_user, external_principal, relay_signer_principal, relay_message_id FROM messages WHERE id = ?1 AND storage_owner = ?2",
params![message_id, storage_owner],
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, Option<i64>>(1)?, row.get::<_, Option<String>>(2)?)),
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, Option<i64>>(1)?, row.get::<_, Option<i64>>(2)?, row.get::<_, Option<String>>(3)?)),
)?;
if let (Some(relay_signer_id), Some(relay_message_id)) = (relay_signer_id, relay_message_id) {
if let (Some(relay_signer_principal), Some(relay_message_id)) =
(relay_signer_principal, relay_message_id)
{
tx.execute(
"DELETE FROM message_receipts WHERE storage_owner = ?1 AND target_signer_id = ?2 AND target_message_id = ?3",
params![storage_owner, relay_signer_id, relay_message_id],
"DELETE FROM message_receipts WHERE storage_owner = ?1 AND target_signer_principal = ?2 AND target_message_id = ?3",
params![storage_owner, relay_signer_principal, relay_message_id],
)?;
}
tx.execute(
@ -346,7 +436,12 @@ pub fn purge_message_in_tx(
message_id,
Operation::Delete,
)?;
update_contact_last_message_in_tx(tx, storage_owner, external_user)?;
update_contact_last_message_in_tx(
tx,
storage_owner,
external_user,
external_principal.map(iota_identity::PrincipalHandle),
)?;
Ok(())
}
@ -356,8 +451,50 @@ pub fn delete_message(
external_user: i64,
message_time: i64,
) -> Result<(), StorageError> {
match ensure_message_direction(storage_owner, external_user, message_time, true) {
Ok(()) => hard_delete_message(storage_owner, external_user, message_time),
delete_message_with_principal(storage_owner, external_user, None, message_time, true)
}
pub fn delete_message_for_principal(
storage_owner: i64,
external_user: i64,
external_principal: iota_identity::PrincipalHandle,
message_time: i64,
) -> Result<(), StorageError> {
delete_message_with_principal(
storage_owner,
external_user,
Some(external_principal),
message_time,
true,
)
}
fn delete_message_with_principal(
storage_owner: i64,
external_user: i64,
external_principal: Option<iota_identity::PrincipalHandle>,
message_time: i64,
sent_by_self: bool,
) -> Result<(), StorageError> {
match ensure_message_direction(
storage_owner,
external_user,
external_principal,
message_time,
sent_by_self,
) {
Ok(()) if sent_by_self => hard_delete_message_with_principal(
storage_owner,
external_user,
external_principal,
message_time,
),
Ok(()) => flag_deleted_by_external_with_principal(
storage_owner,
external_user,
external_principal,
message_time,
),
Err(StorageError::Db(rusqlite::Error::QueryReturnedNoRows)) => Ok(()),
Err(error) => Err(error),
}
@ -375,16 +512,34 @@ pub fn apply_remote_delete(
"Remote sender does not match chat partner".into(),
));
}
match ensure_message_direction(storage_owner, external_user, message_time, false) {
Ok(()) => flag_deleted_by_external(storage_owner, external_user, message_time),
Err(StorageError::Db(rusqlite::Error::QueryReturnedNoRows)) => Ok(()),
Err(error) => Err(error),
delete_message_with_principal(storage_owner, external_user, None, message_time, false)
}
pub fn apply_remote_delete_for_principal(
storage_owner: i64,
external_user: i64,
external_principal: iota_identity::PrincipalHandle,
message_time: i64,
sender_id: i64,
) -> Result<(), StorageError> {
if sender_id != external_user {
return Err(StorageError::Other(
"Remote sender does not match chat partner".into(),
));
}
delete_message_with_principal(
storage_owner,
external_user,
Some(external_principal),
message_time,
false,
)
}
fn ensure_message_direction(
storage_owner: i64,
external_user: i64,
external_principal: Option<iota_identity::PrincipalHandle>,
message_time: i64,
expected_sent_by_self: bool,
) -> Result<(), StorageError> {
@ -392,10 +547,20 @@ fn ensure_message_direction(
let sent_by_self: i64 = conn.query_row(
r#"
SELECT sent_by_self FROM messages
WHERE storage_owner = ?1 AND external_user = ?2 AND message_time = ?3
WHERE storage_owner = ?1
AND message_time = ?3
AND (
(?4 IS NULL AND external_user = ?2)
OR external_principal = ?4
)
ORDER BY id DESC LIMIT 1
"#,
params![storage_owner, external_user, message_time],
params![
storage_owner,
external_user,
message_time,
external_principal.map(|principal| principal.0)
],
|row| row.get(0),
)?;
if (sent_by_self != 0) != expected_sent_by_self {
@ -415,6 +580,15 @@ pub fn flag_deleted_by_external(
storage_owner: i64,
external_user: i64,
message_time: i64,
) -> Result<(), StorageError> {
flag_deleted_by_external_with_principal(storage_owner, external_user, None, message_time)
}
fn flag_deleted_by_external_with_principal(
storage_owner: i64,
external_user: i64,
external_principal: Option<iota_identity::PrincipalHandle>,
message_time: i64,
) -> Result<(), StorageError> {
db::with_db(|conn| {
let tx = conn.unchecked_transaction()?;
@ -422,14 +596,28 @@ pub fn flag_deleted_by_external(
r#"
UPDATE messages
SET deleted_by_external = 1
WHERE storage_owner = ?1 AND external_user = ?2 AND message_time = ?3
WHERE storage_owner = ?1
AND message_time = ?3
AND (
(?4 IS NULL AND external_user = ?2)
OR external_principal = ?4
)
"#,
params![storage_owner, external_user, message_time],
params![
storage_owner,
external_user,
message_time,
external_principal.map(|principal| principal.0)
],
)?;
if affected == 0 {
return Err(StorageError::Other("Message not found".into()));
}
let msg_id: i64 = tx.query_row("SELECT id FROM messages WHERE storage_owner = ?1 AND external_user = ?2 AND message_time = ?3 ORDER BY id DESC LIMIT 1", params![storage_owner, external_user, message_time], |r| r.get(0))?;
let msg_id: i64 = tx.query_row(
"SELECT id FROM messages WHERE storage_owner = ?1 AND message_time = ?3 AND ((?4 IS NULL AND external_user = ?2) OR external_principal = ?4) ORDER BY id DESC LIMIT 1",
params![storage_owner, external_user, message_time, external_principal.map(|principal| principal.0)],
|r| r.get(0),
)?;
sync::record_event(
&tx,
storage_owner,
@ -437,7 +625,7 @@ pub fn flag_deleted_by_external(
msg_id,
Operation::Delete,
)?;
update_contact_last_message_in_tx(&tx, storage_owner, external_user)?;
update_contact_last_message_in_tx(&tx, storage_owner, external_user, external_principal)?;
tx.commit()?;
Ok(())
})
@ -490,15 +678,65 @@ pub fn add_reaction(
message_time: i64,
user_id: i64,
reaction: &str,
) -> Result<(), StorageError> {
add_reaction_with_principal(
storage_owner,
external_user,
None,
message_time,
user_id,
None,
reaction,
)
}
pub fn add_reaction_for_principal(
storage_owner: i64,
external_user: i64,
external_principal: iota_identity::PrincipalHandle,
message_time: i64,
user_id: i64,
user_principal: iota_identity::PrincipalHandle,
reaction: &str,
) -> Result<(), StorageError> {
add_reaction_with_principal(
storage_owner,
external_user,
Some(external_principal),
message_time,
user_id,
Some(user_principal),
reaction,
)
}
fn add_reaction_with_principal(
storage_owner: i64,
external_user: i64,
external_principal: Option<iota_identity::PrincipalHandle>,
message_time: i64,
user_id: i64,
user_principal: Option<iota_identity::PrincipalHandle>,
reaction: &str,
) -> Result<(), StorageError> {
db::with_immediate_transaction(|tx| {
let (msg_id, history_deleted): (i64, i64) = tx.query_row(
r#"
SELECT id, history_deleted FROM messages
WHERE storage_owner = ?1 AND external_user = ?2 AND message_time = ?3
WHERE storage_owner = ?1
AND message_time = ?3
AND (
(?4 IS NULL AND external_user = ?2)
OR external_principal = ?4
)
ORDER BY id DESC LIMIT 1
"#,
params![storage_owner, external_user, message_time],
params![
storage_owner,
external_user,
message_time,
external_principal.map(|principal| principal.0)
],
|row| Ok((row.get(0)?, row.get(1)?)),
)?;
if history_deleted != 0 {
@ -528,10 +766,16 @@ pub fn add_reaction(
let inserted = tx.execute(
r#"
INSERT OR IGNORE INTO reactions (message_id, user_id, reaction, created_at)
VALUES (?1, ?2, ?3, ?4)
INSERT OR IGNORE INTO reactions (message_id, user_id, user_principal, reaction, created_at)
VALUES (?1, ?2, ?3, ?4, ?5)
"#,
params![msg_id, user_id, reaction, now],
params![
msg_id,
user_id,
user_principal.map(|principal| principal.0),
reaction,
now
],
)?;
if inserted > 0 {
sync::record_event(
@ -554,15 +798,65 @@ pub fn remove_reaction(
message_time: i64,
user_id: i64,
reaction: &str,
) -> Result<(), StorageError> {
remove_reaction_with_principal(
storage_owner,
external_user,
None,
message_time,
user_id,
None,
reaction,
)
}
pub fn remove_reaction_for_principal(
storage_owner: i64,
external_user: i64,
external_principal: iota_identity::PrincipalHandle,
message_time: i64,
user_id: i64,
user_principal: iota_identity::PrincipalHandle,
reaction: &str,
) -> Result<(), StorageError> {
remove_reaction_with_principal(
storage_owner,
external_user,
Some(external_principal),
message_time,
user_id,
Some(user_principal),
reaction,
)
}
fn remove_reaction_with_principal(
storage_owner: i64,
external_user: i64,
external_principal: Option<iota_identity::PrincipalHandle>,
message_time: i64,
user_id: i64,
user_principal: Option<iota_identity::PrincipalHandle>,
reaction: &str,
) -> Result<(), StorageError> {
db::with_db(|conn| {
let (msg_id, history_deleted): (i64, i64) = conn.query_row(
r#"
SELECT id, history_deleted FROM messages
WHERE storage_owner = ?1 AND external_user = ?2 AND message_time = ?3
WHERE storage_owner = ?1
AND message_time = ?3
AND (
(?4 IS NULL AND external_user = ?2)
OR external_principal = ?4
)
ORDER BY id DESC LIMIT 1
"#,
params![storage_owner, external_user, message_time],
params![
storage_owner,
external_user,
message_time,
external_principal.map(|principal| principal.0)
],
|row| Ok((row.get(0)?, row.get(1)?)),
)?;
if history_deleted != 0 {
@ -571,8 +865,13 @@ pub fn remove_reaction(
let tx = conn.unchecked_transaction()?;
tx.execute(
"DELETE FROM reactions WHERE message_id = ?1 AND user_id = ?2 AND reaction = ?3",
params![msg_id, user_id, reaction],
"DELETE FROM reactions WHERE message_id = ?1 AND ((?3 IS NULL AND user_id = ?2) OR user_principal = ?3) AND reaction = ?4",
params![
msg_id,
user_id,
user_principal.map(|principal| principal.0),
reaction
],
)?;
sync::record_event(
&tx,
@ -589,11 +888,13 @@ pub fn remove_reaction(
pub fn add_message(message: NewMessage<'_>) -> Result<i64, StorageError> {
let NewMessage {
relay_signer_id,
relay_signer_principal,
relay_message_id,
authored_at,
send_time,
storage_owner,
external_user,
external_principal,
sent_by_self,
content,
height,
@ -620,8 +921,9 @@ pub fn add_message(message: NewMessage<'_>) -> Result<i64, StorageError> {
INSERT INTO messages (
storage_owner, external_user, message_time, content, sent_by_self,
message_state, height, key_version, reply_to, relay_signer_id, relay_message_id,
authored_at, origin_iota_received_at, destination_iota_received_at, stored_at, expires_at
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16)
authored_at, origin_iota_received_at, destination_iota_received_at, stored_at,
expires_at, external_principal, relay_signer_principal
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18)
"#,
params![
storage_owner,
@ -640,6 +942,8 @@ pub fn add_message(message: NewMessage<'_>) -> Result<i64, StorageError> {
destination_iota_received_at,
stored_at,
expires_at,
external_principal.0,
relay_signer_principal.0,
],
)?;
let msg_id = tx.last_insert_rowid();
@ -650,7 +954,8 @@ pub fn add_message(message: NewMessage<'_>) -> Result<i64, StorageError> {
msg_id,
Operation::Upsert,
)?;
let mut contact = crate::users::contact::Contact::new(external_user);
let mut contact =
crate::users::contact::Contact::for_principal(external_user, external_principal);
contact.set_last_message_at(
destination_iota_received_at
.or(origin_iota_received_at)
@ -659,8 +964,8 @@ pub fn add_message(message: NewMessage<'_>) -> Result<i64, StorageError> {
crate::util::chats_util::upsert_contact(&tx, storage_owner, &contact)?;
if !sent_by_self {
tx.execute(
"UPDATE contacts SET notifications = CASE WHEN notifications < 9223372036854775807 THEN notifications + 1 ELSE notifications END WHERE storage_owner = ?1 AND user_id = ?2",
params![storage_owner, external_user],
"UPDATE contacts SET notifications = CASE WHEN notifications < 9223372036854775807 THEN notifications + 1 ELSE notifications END WHERE storage_owner = ?1 AND principal_handle = ?2",
params![storage_owner, external_principal.0],
)?;
}
tx.commit()?;
@ -670,7 +975,8 @@ pub fn add_message(message: NewMessage<'_>) -> Result<i64, StorageError> {
pub fn change_message_state_by_relay_id(
storage_owner: i64,
relay_signer_id: i64,
_relay_signer_id: i64,
relay_signer_principal: iota_identity::PrincipalHandle,
relay_message_id: &str,
new_state: MessageState,
) -> Result<(), StorageError> {
@ -678,8 +984,8 @@ pub fn change_message_state_by_relay_id(
let tx = conn.unchecked_transaction()?;
let Some((msg_id, current)) = tx
.query_row(
"SELECT id, message_state FROM messages WHERE storage_owner = ?1 AND relay_signer_id = ?2 AND relay_message_id = ?3",
params![storage_owner, relay_signer_id, relay_message_id],
"SELECT id, message_state FROM messages WHERE storage_owner = ?1 AND relay_signer_principal = ?2 AND relay_message_id = ?3",
params![storage_owner, relay_signer_principal.0, relay_message_id],
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)),
)
.optional()?
@ -706,8 +1012,10 @@ pub fn change_message_state_by_relay_id(
pub fn record_message_receipt(
storage_owner: i64,
target_signer_id: i64,
target_signer_principal: iota_identity::PrincipalHandle,
target_message_id: &str,
receipt_signer_id: i64,
receipt_signer_principal: iota_identity::PrincipalHandle,
receipt_message_id: &str,
receipt_type: MessageState,
event_at: i64,
@ -720,17 +1028,17 @@ pub fn record_message_receipt(
};
db::with_db(|conn| {
let tx = conn.unchecked_transaction()?;
let Some((message_id, external_user, authored_at, history_deleted)) = tx
let Some((message_id, external_principal, authored_at, history_deleted)) = tx
.query_row(
"SELECT id, external_user, authored_at, history_deleted FROM messages WHERE storage_owner = ?1 AND relay_signer_id = ?2 AND relay_message_id = ?3",
params![storage_owner, target_signer_id, target_message_id],
"SELECT id, external_principal, authored_at, history_deleted FROM messages WHERE storage_owner = ?1 AND relay_signer_principal = ?2 AND relay_message_id = ?3",
params![storage_owner, target_signer_principal.0, target_message_id],
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?, row.get::<_, Option<i64>>(2)?, row.get::<_, i64>(3)?)),
)
.optional()?
else {
return Err(StorageError::Other("message receipt target was not found".into()));
};
if external_user != receipt_signer_id {
if external_principal != receipt_signer_principal.0 {
return Err(StorageError::Other(
"message receipt signer is not the chat partner".into(),
));
@ -746,8 +1054,8 @@ pub fn record_message_receipt(
return Ok(());
}
tx.execute(
"INSERT OR IGNORE INTO message_receipts (storage_owner, target_signer_id, target_message_id, receipt_signer_id, receipt_message_id, receipt_type, event_at, recorded_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
params![storage_owner, target_signer_id, target_message_id, receipt_signer_id, receipt_message_id, receipt_type, event_at, recorded_at],
"INSERT OR IGNORE INTO message_receipts (storage_owner, target_signer_id, target_signer_principal, target_message_id, receipt_signer_id, receipt_signer_principal, receipt_message_id, receipt_type, event_at, recorded_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
params![storage_owner, target_signer_id, target_signer_principal.0, target_message_id, receipt_signer_id, receipt_signer_principal.0, receipt_message_id, receipt_type, event_at, recorded_at],
)?;
let (state_column, recorded_column) = if receipt_type == "read" {
("read_at", "read_recorded_at")
@ -780,7 +1088,7 @@ pub fn record_message_receipt(
pub fn record_destination_iota_received(
storage_owner: i64,
relay_signer_id: i64,
relay_signer_principal: iota_identity::PrincipalHandle,
relay_message_id: &str,
accepted_at: i64,
) -> Result<(), StorageError> {
@ -788,8 +1096,8 @@ pub fn record_destination_iota_received(
let tx = conn.unchecked_transaction()?;
let Some(message_id) = tx
.query_row(
"SELECT id FROM messages WHERE storage_owner = ?1 AND relay_signer_id = ?2 AND relay_message_id = ?3",
params![storage_owner, relay_signer_id, relay_message_id],
"SELECT id FROM messages WHERE storage_owner = ?1 AND relay_signer_principal = ?2 AND relay_message_id = ?3",
params![storage_owner, relay_signer_principal.0, relay_message_id],
|row| row.get::<_, i64>(0),
)
.optional()?
@ -814,7 +1122,7 @@ pub fn record_destination_iota_received(
pub fn record_delivery_failure(
storage_owner: i64,
relay_signer_id: i64,
relay_signer_principal: iota_identity::PrincipalHandle,
relay_message_id: &str,
failure: &str,
failed_at: i64,
@ -823,8 +1131,8 @@ pub fn record_delivery_failure(
let tx = conn.unchecked_transaction()?;
let Some(message_id) = tx
.query_row(
"SELECT id FROM messages WHERE storage_owner = ?1 AND relay_signer_id = ?2 AND relay_message_id = ?3",
params![storage_owner, relay_signer_id, relay_message_id],
"SELECT id FROM messages WHERE storage_owner = ?1 AND relay_signer_principal = ?2 AND relay_message_id = ?3",
params![storage_owner, relay_signer_principal.0, relay_message_id],
|row| row.get::<_, i64>(0),
)
.optional()?
@ -957,6 +1265,20 @@ pub fn get_messages(
external_user: i64,
loaded_messages: i64,
amount: i64,
) -> Result<Vec<StoredMessage>, StorageError> {
let Some(principal) = crate::util::chats_util::get_user(storage_owner, external_user)?
.and_then(|contact| contact.principal)
else {
return Ok(Vec::new());
};
get_messages_for_principal(storage_owner, principal, loaded_messages, amount)
}
pub fn get_messages_for_principal(
storage_owner: i64,
external_principal: iota_identity::PrincipalHandle,
loaded_messages: i64,
amount: i64,
) -> Result<Vec<StoredMessage>, StorageError> {
if amount <= 0 || loaded_messages < 0 {
return Ok(Vec::new());
@ -969,21 +1291,25 @@ pub fn get_messages(
origin_iota_received_at, destination_iota_received_at,
client_received_at, client_received_recorded_at, read_at,
read_recorded_at, delivery_failed_at, delivery_failure, content, sent_by_self, message_state, height, key_version,
reply_to, edited_count
reply_to, edited_count, external_user, relay_signer_principal
FROM messages
WHERE storage_owner = ?1 AND external_user = ?2 AND deleted_by_external = 0 AND history_deleted = 0
WHERE storage_owner = ?1 AND external_principal = ?2 AND deleted_by_external = 0 AND history_deleted = 0
ORDER BY COALESCE(destination_iota_received_at, origin_iota_received_at, authored_at, id) DESC, id DESC
LIMIT ?3 OFFSET ?4
"#,
)?;
let rows = stmt.query_map(
params![storage_owner, external_user, amount, loaded_messages],
params![storage_owner, external_principal.0, amount, loaded_messages],
|row| {
Ok(StoredMessage {
id: row.get(0)?,
external_user,
external_user: row.get(20)?,
external_principal: Some(external_principal),
relay_signer_id: row.get(1)?,
relay_signer_principal: row
.get::<_, Option<i64>>(21)?
.map(iota_identity::PrincipalHandle),
relay_message_id: row.get(2)?,
message_time: row.get(3)?,
authored_at: row.get(4)?,
@ -1026,6 +1352,7 @@ pub fn get_message(
storage_owner: i64,
message_time: i64,
external_user: Option<i64>,
external_principal: Option<iota_identity::PrincipalHandle>,
) -> Result<Option<StoredMessage>, StorageError> {
db::with_db(|conn| {
let mut stmt = conn.prepare(
@ -1034,43 +1361,62 @@ pub fn get_message(
origin_iota_received_at, destination_iota_received_at,
client_received_at, client_received_recorded_at, read_at,
read_recorded_at, delivery_failed_at, delivery_failure, content, sent_by_self, message_state, height, key_version,
reply_to, edited_count, external_user
reply_to, edited_count, external_user, external_principal,
relay_signer_principal
FROM messages
WHERE storage_owner = ?1
AND message_time = ?2
AND deleted_by_external = 0
AND history_deleted = 0
AND (?3 IS NULL OR external_user = ?3)
AND (
?3 IS NULL
OR (?4 IS NULL AND external_user = ?3)
OR external_principal = ?4
)
ORDER BY id DESC
"#,
)?;
let rows = stmt.query_map(params![storage_owner, message_time, external_user], |row| {
Ok(StoredMessage {
id: row.get(0)?,
relay_signer_id: row.get(1)?,
relay_message_id: row.get(2)?,
message_time: row.get(3)?,
authored_at: row.get(4)?,
origin_iota_received_at: row.get(5)?,
destination_iota_received_at: row.get(6)?,
client_received_at: row.get(7)?,
client_received_recorded_at: row.get(8)?,
read_at: row.get(9)?,
read_recorded_at: row.get(10)?,
delivery_failed_at: row.get(11)?,
delivery_failure: row.get(12)?,
content: row.get(13)?,
sent_by_self: row.get::<_, i64>(14)? != 0,
message_state: row.get(15)?,
height: row.get(16)?,
key_version: row.get(17)?,
reply_to: row.get(18)?,
edited: row.get::<_, i64>(19)? > 0,
external_user: row.get(20)?,
reactions: Vec::new(),
})
})?;
let rows = stmt.query_map(
params![
storage_owner,
message_time,
external_user,
external_principal.map(|principal| principal.0)
],
|row| {
Ok(StoredMessage {
id: row.get(0)?,
relay_signer_id: row.get(1)?,
relay_signer_principal: row
.get::<_, Option<i64>>(22)?
.map(iota_identity::PrincipalHandle),
relay_message_id: row.get(2)?,
message_time: row.get(3)?,
authored_at: row.get(4)?,
origin_iota_received_at: row.get(5)?,
destination_iota_received_at: row.get(6)?,
client_received_at: row.get(7)?,
client_received_recorded_at: row.get(8)?,
read_at: row.get(9)?,
read_recorded_at: row.get(10)?,
delivery_failed_at: row.get(11)?,
delivery_failure: row.get(12)?,
content: row.get(13)?,
sent_by_self: row.get::<_, i64>(14)? != 0,
message_state: row.get(15)?,
height: row.get(16)?,
key_version: row.get(17)?,
reply_to: row.get(18)?,
edited: row.get::<_, i64>(19)? > 0,
external_user: row.get(20)?,
external_principal: row
.get::<_, Option<i64>>(21)?
.map(iota_identity::PrincipalHandle),
reactions: Vec::new(),
})
},
)?;
let messages: Vec<StoredMessage> = rows.collect::<Result<_, _>>()?;
if messages.is_empty() {
@ -1079,7 +1425,7 @@ pub fn get_message(
if external_user.is_none()
&& messages
.iter()
.map(|message| message.external_user)
.filter_map(|message| message.external_principal)
.collect::<std::collections::HashSet<_>>()
.len()
> 1
@ -1099,7 +1445,18 @@ pub fn get_message_with_offset(
external_user: i64,
message_time: i64,
) -> Result<Option<(StoredMessage, i64)>, StorageError> {
let Some(message) = get_message(storage_owner, message_time, Some(external_user))? else {
let Some(external_principal) = crate::util::chats_util::get_user(storage_owner, external_user)?
.and_then(|contact| contact.principal)
else {
return Ok(None);
};
let Some(message) = get_message(
storage_owner,
message_time,
Some(external_user),
Some(external_principal),
)?
else {
return Ok(None);
};
let offset = db::with_db(|conn| {
@ -1108,7 +1465,7 @@ pub fn get_message_with_offset(
SELECT COUNT(*)
FROM messages
WHERE storage_owner = ?1
AND external_user = ?2
AND external_principal = ?2
AND deleted_by_external = 0
AND history_deleted = 0
AND (
@ -1120,7 +1477,7 @@ pub fn get_message_with_offset(
"#,
params![
storage_owner,
external_user,
external_principal.0,
message.destination_iota_received_at.or(message.origin_iota_received_at).or(message.authored_at),
message.id
],
@ -1142,13 +1499,19 @@ pub fn get_messages_by_ids(
// A journal id uniquely identifies a row. Load all messages for this owner and retain only
// those ids; this keeps reaction hydration identical to normal message loading.
db::with_db(|conn| {
let mut stmt = conn.prepare("SELECT id, relay_signer_id, relay_message_id, message_time, authored_at, origin_iota_received_at, destination_iota_received_at, client_received_at, client_received_recorded_at, read_at, read_recorded_at, delivery_failed_at, delivery_failure, content, sent_by_self, message_state, height, key_version, reply_to, edited_count, external_user FROM messages WHERE storage_owner = ?1 AND deleted_by_external = 0 AND history_deleted = 0")?;
let mut stmt = conn.prepare("SELECT id, relay_signer_id, relay_message_id, message_time, authored_at, origin_iota_received_at, destination_iota_received_at, client_received_at, client_received_recorded_at, read_at, read_recorded_at, delivery_failed_at, delivery_failure, content, sent_by_self, message_state, height, key_version, reply_to, edited_count, external_user, external_principal, relay_signer_principal FROM messages WHERE storage_owner = ?1 AND deleted_by_external = 0 AND history_deleted = 0")?;
let rows = stmt.query_map([storage_owner], |row| {
let external_user: i64 = row.get(20)?;
Ok(StoredMessage {
id: row.get(0)?,
external_user,
external_principal: row
.get::<_, Option<i64>>(21)?
.map(iota_identity::PrincipalHandle),
relay_signer_id: row.get(1)?,
relay_signer_principal: row
.get::<_, Option<i64>>(22)?
.map(iota_identity::PrincipalHandle),
relay_message_id: row.get(2)?,
message_time: row.get(3)?,
authored_at: row.get(4)?,

View file

@ -9,14 +9,31 @@ pub(crate) fn upsert_contact(
storage_owner: i64,
contact: &Contact,
) -> Result<(), StorageError> {
let principal = match contact.principal {
Some(principal) => principal,
None => iota_identity::PrincipalHandle(tx.query_row(
r#"
SELECT p.principal_pk
FROM principals p
WHERE p.authority_id = COALESCE(
(SELECT omega_authority_id FROM identity_configuration WHERE singleton = 1),
'omega:central'
)
AND p.remote_user_id = ?1
"#,
[contact.user_id],
|row| row.get(0),
)?),
};
tx.execute(
r#"
INSERT INTO contacts (
storage_owner, user_id, user_name, created_at, last_message_at,
storage_owner, user_id, principal_handle, user_name, created_at, last_message_at,
notifications, notifications_read_at
)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
ON CONFLICT(storage_owner, user_id) DO UPDATE SET
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
ON CONFLICT(storage_owner, principal_handle) DO UPDATE SET
user_id = excluded.user_id,
user_name = COALESCE(excluded.user_name, contacts.user_name),
created_at = MIN(contacts.created_at, excluded.created_at),
last_message_at = CASE
@ -28,6 +45,7 @@ pub(crate) fn upsert_contact(
params![
storage_owner,
contact.user_id,
principal.0,
contact.user_name,
contact.created_at,
contact.last_message_at,
@ -39,63 +57,112 @@ pub(crate) fn upsert_contact(
tx,
storage_owner,
EntityType::Contact,
contact.user_id,
principal.0,
Operation::Upsert,
)?;
Ok(())
}
pub fn has_user(storage_owner: i64, user_id: i64) -> Result<bool, StorageError> {
pub fn has_principal(
storage_owner: i64,
principal: iota_identity::PrincipalHandle,
) -> Result<bool, StorageError> {
db::with_db(|conn| {
Ok(conn.query_row(
"SELECT EXISTS(SELECT 1 FROM contacts WHERE storage_owner = ?1 AND user_id = ?2)",
params![storage_owner, user_id],
"SELECT EXISTS(SELECT 1 FROM contacts WHERE storage_owner = ?1 AND principal_handle = ?2)",
params![storage_owner, principal.0],
|row| row.get(0),
)?)
})
}
pub fn has_user(storage_owner: i64, user_id: i64) -> Result<bool, StorageError> {
get_user(storage_owner, user_id).map(|contact| contact.is_some())
}
pub fn mod_user(storage_owner: i64, contact: &Contact) -> Result<(), StorageError> {
db::with_immediate_transaction(|tx| upsert_contact(tx, storage_owner, contact))
}
pub fn get_users_by_ids(storage_owner: i64, ids: &[i64]) -> Result<Vec<Contact>, StorageError> {
if ids.is_empty() {
pub fn get_users_by_principals(
storage_owner: i64,
principals: &[iota_identity::PrincipalHandle],
) -> Result<Vec<Contact>, StorageError> {
if principals.is_empty() {
return Ok(Vec::new());
}
let wanted: std::collections::HashSet<i64> = ids.iter().copied().collect();
let wanted: std::collections::HashSet<iota_identity::PrincipalHandle> =
principals.iter().copied().collect();
Ok(get_users(storage_owner)?
.into_iter()
.filter(|contact| wanted.contains(&contact.user_id))
.filter(|contact| {
contact
.principal
.is_some_and(|principal| wanted.contains(&principal))
})
.collect())
}
pub fn get_user(storage_owner: i64, user_id: i64) -> Result<Option<Contact>, StorageError> {
db::with_db(|conn| {
match conn.query_row(
let mut statement = conn.prepare(
r#"
SELECT user_id, user_name, created_at, last_message_at,
SELECT user_id, principal_handle, user_name, created_at, last_message_at,
notifications, notifications_read_at
FROM contacts
WHERE storage_owner = ?1 AND user_id = ?2
LIMIT 1
LIMIT 2
"#,
params![storage_owner, user_id],
|r| {
)?;
let contacts = statement
.query_map(params![storage_owner, user_id], |r| {
Ok(Contact {
user_id: r.get(0)?,
user_name: r.get(1)?,
created_at: r.get(2)?,
last_message_at: r.get(3)?,
notifications: r.get(4)?,
notifications_read_at: r.get(5)?,
principal: r
.get::<_, Option<i64>>(1)?
.map(iota_identity::PrincipalHandle),
user_name: r.get(2)?,
created_at: r.get(3)?,
last_message_at: r.get(4)?,
notifications: r.get(5)?,
notifications_read_at: r.get(6)?,
})
})?
.collect::<Result<Vec<_>, _>>()?;
match contacts.as_slice() {
[contact] => Ok(Some(contact.clone())),
_ => Ok(None),
}
})
}
pub fn get_user_by_principal(
storage_owner: i64,
principal: iota_identity::PrincipalHandle,
) -> Result<Option<Contact>, StorageError> {
db::with_db(|conn| {
conn.query_row(
r#"
SELECT user_id, principal_handle, user_name, created_at, last_message_at,
notifications, notifications_read_at
FROM contacts
WHERE storage_owner = ?1 AND principal_handle = ?2
"#,
params![storage_owner, principal.0],
|row| {
Ok(Contact {
user_id: row.get(0)?,
principal: Some(iota_identity::PrincipalHandle(row.get(1)?)),
user_name: row.get(2)?,
created_at: row.get(3)?,
last_message_at: row.get(4)?,
notifications: row.get(5)?,
notifications_read_at: row.get(6)?,
})
},
) {
Ok(c) => Ok(Some(c)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e.into()),
}
)
.optional()
.map_err(Into::into)
})
}
@ -103,7 +170,7 @@ pub fn get_users(storage_owner: i64) -> Result<Vec<Contact>, StorageError> {
db::with_db(|conn| {
let mut stmt = conn.prepare(
r#"
SELECT user_id, user_name, created_at, last_message_at,
SELECT user_id, principal_handle, user_name, created_at, last_message_at,
notifications, notifications_read_at
FROM contacts
WHERE storage_owner = ?1
@ -117,11 +184,14 @@ pub fn get_users(storage_owner: i64) -> Result<Vec<Contact>, StorageError> {
let rows = stmt.query_map(params![storage_owner], |r| {
Ok(Contact {
user_id: r.get(0)?,
user_name: r.get(1)?,
created_at: r.get(2)?,
last_message_at: r.get(3)?,
notifications: r.get(4)?,
notifications_read_at: r.get(5)?,
principal: r
.get::<_, Option<i64>>(1)?
.map(iota_identity::PrincipalHandle),
user_name: r.get(2)?,
created_at: r.get(3)?,
last_message_at: r.get(4)?,
notifications: r.get(5)?,
notifications_read_at: r.get(6)?,
})
})?;
@ -135,14 +205,14 @@ pub fn get_users(storage_owner: i64) -> Result<Vec<Contact>, StorageError> {
pub fn read_notifications(
storage_owner: i64,
user_id: i64,
principal: iota_identity::PrincipalHandle,
through: i64,
) -> Result<Option<Contact>, StorageError> {
db::with_immediate_transaction(|tx| {
let Some(current_read_at) = tx
.query_row(
"SELECT notifications_read_at FROM contacts WHERE storage_owner = ?1 AND user_id = ?2",
params![storage_owner, user_id],
"SELECT notifications_read_at FROM contacts WHERE storage_owner = ?1 AND principal_handle = ?2",
params![storage_owner, principal.0],
|row| row.get::<_, i64>(0),
)
.optional()?
@ -155,43 +225,46 @@ pub fn read_notifications(
SELECT COUNT(*)
FROM messages
WHERE storage_owner = ?1
AND external_user = ?2
AND external_principal = ?2
AND sent_by_self = 0
AND deleted_by_external = 0
AND history_deleted = 0
AND COALESCE(destination_iota_received_at, stored_at, authored_at, message_time) > ?3
"#,
params![storage_owner, user_id, read_at],
params![storage_owner, principal.0, read_at],
|row| row.get::<_, i64>(0),
)?;
tx.execute(
"UPDATE contacts SET notifications = ?3, notifications_read_at = ?4 WHERE storage_owner = ?1 AND user_id = ?2",
params![storage_owner, user_id, notifications, read_at],
"UPDATE contacts SET notifications = ?3, notifications_read_at = ?4 WHERE storage_owner = ?1 AND principal_handle = ?2",
params![storage_owner, principal.0, notifications, read_at],
)?;
sync::record_event(
tx,
storage_owner,
EntityType::Contact,
user_id,
principal.0,
Operation::Upsert,
)?;
Ok(Some(tx.query_row(
r#"
SELECT user_id, user_name, created_at, last_message_at,
SELECT user_id, principal_handle, user_name, created_at, last_message_at,
notifications, notifications_read_at
FROM contacts
WHERE storage_owner = ?1 AND user_id = ?2
WHERE storage_owner = ?1 AND principal_handle = ?2
"#,
params![storage_owner, user_id],
params![storage_owner, principal.0],
|row| {
Ok(Contact {
user_id: row.get(0)?,
user_name: row.get(1)?,
created_at: row.get(2)?,
last_message_at: row.get(3)?,
notifications: row.get(4)?,
notifications_read_at: row.get(5)?,
principal: row
.get::<_, Option<i64>>(1)?
.map(iota_identity::PrincipalHandle),
user_name: row.get(2)?,
created_at: row.get(3)?,
last_message_at: row.get(4)?,
notifications: row.get(5)?,
notifications_read_at: row.get(6)?,
})
},
)?))

View file

@ -25,8 +25,8 @@ pub fn acknowledge_client_delivery(
== message_storage_policy::MessageHistoryMode::DeleteAfterClientDelivery
{
let message_id = tx.query_row(
"SELECT id FROM messages WHERE storage_owner = ?1 AND relay_signer_id = ?2 AND relay_message_id = ?3",
rusqlite::params![destination_id, relay.signer_id, relay.message_id],
"SELECT id FROM messages WHERE storage_owner = ?1 AND relay_signer_principal = ?2 AND relay_message_id = ?3",
rusqlite::params![destination_id, relay.signer.0, relay.message_id],
|row| row.get::<_, i64>(0),
).optional()?;
if let Some(message_id) = message_id {

View file

@ -30,6 +30,16 @@ pub enum ConfigError {
#[source]
source: std::net::AddrParseError,
},
#[error("invalid federation endpoint {endpoint:?}: {source}")]
InvalidFederationEndpoint {
endpoint: String,
#[source]
source: iota_identity::IdentityError,
},
#[error("invalid relay router public key: {0}")]
InvalidRelayRouterKey(String),
#[error("relay router certificate path must not be empty")]
MissingRelayRouterCertificate,
#[error("max_ipc_clients must be greater than zero")]
InvalidMaxIpcClients,
}
@ -43,6 +53,8 @@ pub struct IotaConfig {
pub port: u16,
#[serde(default)]
pub web: WebSettings,
#[serde(default)]
pub relay_routers: Vec<RelayRouterSettings>,
#[serde(skip_serializing_if = "Option::is_none")]
pub omikron_host: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
@ -61,6 +73,14 @@ pub struct IotaConfig {
pub max_ipc_clients: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RelayRouterSettings {
pub endpoint: String,
pub public_key: String,
pub certificate: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WebMode {
@ -89,6 +109,10 @@ pub struct WebSettings {
pub key: Option<String>,
#[serde(default)]
pub required: bool,
#[serde(default)]
pub direct_endpoints: Vec<String>,
#[serde(default)]
pub relay_hints: Vec<String>,
}
fn default_web_bind() -> String {
"127.0.0.1".into()
@ -106,6 +130,8 @@ impl Default for WebSettings {
certificate: None,
key: None,
required: false,
direct_endpoints: Vec::new(),
relay_hints: Vec::new(),
}
}
}
@ -128,6 +154,7 @@ impl Default for IotaConfig {
iota_id: None,
port: default_port(),
web: WebSettings::default(),
relay_routers: Vec::new(),
omikron_host: None,
omikron_port: None,
omikron_id: None,
@ -187,6 +214,32 @@ pub fn validate_config(config: &IotaConfig) -> Result<(), ConfigError> {
if config.max_ipc_clients == 0 {
return Err(ConfigError::InvalidMaxIpcClients);
}
for endpoint in config
.web
.direct_endpoints
.iter()
.chain(&config.web.relay_hints)
{
iota_identity::AuthorityLocator::new(endpoint.clone()).map_err(|source| {
ConfigError::InvalidFederationEndpoint {
endpoint: endpoint.clone(),
source,
}
})?;
}
for router in &config.relay_routers {
iota_identity::AuthorityLocator::new(router.endpoint.clone()).map_err(|source| {
ConfigError::InvalidFederationEndpoint {
endpoint: router.endpoint.clone(),
source,
}
})?;
iota_identity::PublicKeyBundle::from_base64(&router.public_key)
.map_err(|error| ConfigError::InvalidRelayRouterKey(error.to_string()))?;
if router.certificate.trim().is_empty() {
return Err(ConfigError::MissingRelayRouterCertificate);
}
}
Ok(())
}
@ -301,7 +354,7 @@ pub fn configure_config_path(path: PathBuf) {
#[cfg(test)]
mod tests {
use super::{ConfigError, IotaConfig, parse_config, validate_config};
use super::{ConfigError, IotaConfig, RelayRouterSettings, parse_config, validate_config};
use std::path::Path;
#[test]
@ -339,4 +392,34 @@ mod tests {
Err(ConfigError::InvalidMaxIpcClients)
));
}
#[test]
fn relay_router_requires_valid_explicit_trust_pins() {
let mut config = IotaConfig::default();
config.relay_routers.push(RelayRouterSettings {
endpoint: "router.example:1984".into(),
public_key: "invalid".into(),
certificate: "router.pem".into(),
});
assert!(matches!(
validate_config(&config),
Err(ConfigError::InvalidRelayRouterKey(_))
));
}
#[test]
fn relay_router_configuration_parses_with_all_trust_material() {
let public_key = mtp::crypto::Keyring::generate()
.public_key_bundle()
.try_to_base64()
.unwrap();
let config = parse_config(
Path::new("config.yaml"),
&format!(
"relay_routers:\n - endpoint: router.example:1984\n public_key: {public_key}\n certificate: router.pem\n"
),
)
.unwrap();
assert_eq!(config.relay_routers.len(), 1);
}
}

File diff suppressed because it is too large Load diff

View file

@ -4,65 +4,85 @@ use crate::util::{db, sync};
use rusqlite::{OptionalExtension, params};
pub fn acknowledge_iota_delivery(
destination_iota: u64,
frame_id: u32,
signer_id: i64,
signer_principal: iota_identity::PrincipalHandle,
_legacy_signer_id: Option<i64>,
relay_message_id: &str,
destination_accepted_at: i64,
) -> Result<(), StorageError> {
let destination_iota = i64::try_from(destination_iota)
.map_err(|_| StorageError::Other("relay destination ID exceeds SQLite range".into()))?;
db::with_immediate_transaction(|tx| {
let pending_id = tx.query_row("SELECT id FROM pending_relays WHERE destination_id = ?1 AND target_kind = 1 AND frame_id = ?2", params![destination_iota, i64::from(frame_id)], |row| row.get::<_, i64>(0)).optional()?;
let pending_id = tx
.query_row(
"SELECT id FROM pending_relays WHERE signer_principal = ?1 AND frame_id = ?2",
params![signer_principal.0, i64::from(frame_id)],
|row| row.get::<_, i64>(0),
)
.optional()?;
let Some(pending_id) = pending_id else {
return Ok(());
};
let message_id = tx.query_row("SELECT id FROM messages WHERE storage_owner = ?1 AND relay_signer_id = ?1 AND relay_message_id = ?2", params![signer_id, relay_message_id], |row| row.get::<_, i64>(0)).optional()?;
if let Some(message_id) = message_id {
let message = tx
.query_row(
"SELECT id, storage_owner FROM messages WHERE relay_signer_principal = ?1 AND relay_message_id = ?2",
params![signer_principal.0, relay_message_id],
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)),
)
.optional()?;
if let Some((message_id, storage_owner)) = message {
tx.execute("UPDATE messages SET destination_iota_received_at = COALESCE(destination_iota_received_at, ?1), delivery_failed_at = NULL, delivery_failure = NULL, message_state = CASE WHEN message_state = 'sending' THEN 'sent' ELSE message_state END WHERE id = ?2", params![destination_accepted_at, message_id])?;
sync::record_event(
tx,
signer_id,
storage_owner,
sync::EntityType::Message,
message_id,
sync::Operation::Upsert,
)?;
}
tx.execute("UPDATE relay_inbox SET state = 'delivered', downstream_acked_at = COALESCE(downstream_acked_at, ?3) WHERE signer_id = ?1 AND message_id = ?2", params![signer_id, relay_message_id, sync::now_millis()])?;
tx.execute("UPDATE relay_inbox SET state = 'delivered', downstream_acked_at = COALESCE(downstream_acked_at, ?3) WHERE signer_principal = ?1 AND message_id = ?2", params![signer_principal.0, relay_message_id, sync::now_millis()])?;
tx.execute("DELETE FROM pending_relays WHERE id = ?1", [pending_id])?;
Ok(())
})
}
pub fn reject_iota_delivery(
destination_iota: u64,
frame_id: u32,
signer_id: i64,
signer_principal: iota_identity::PrincipalHandle,
_legacy_signer_id: Option<i64>,
relay_message_id: &str,
failure: &str,
) -> Result<(), StorageError> {
let destination_iota = i64::try_from(destination_iota)
.map_err(|_| StorageError::Other("relay destination ID exceeds SQLite range".into()))?;
db::with_immediate_transaction(|tx| {
let pending_id = tx.query_row("SELECT id FROM pending_relays WHERE destination_id = ?1 AND target_kind = 1 AND frame_id = ?2", params![destination_iota, i64::from(frame_id)], |row| row.get::<_, i64>(0)).optional()?;
let pending_id = tx
.query_row(
"SELECT id FROM pending_relays WHERE signer_principal = ?1 AND frame_id = ?2",
params![signer_principal.0, i64::from(frame_id)],
|row| row.get::<_, i64>(0),
)
.optional()?;
let Some(pending_id) = pending_id else {
return Ok(());
};
let message_id = tx.query_row("SELECT id FROM messages WHERE storage_owner = ?1 AND relay_signer_id = ?1 AND relay_message_id = ?2", params![signer_id, relay_message_id], |row| row.get::<_, i64>(0)).optional()?;
if let Some(message_id) = message_id {
let message = tx
.query_row(
"SELECT id, storage_owner FROM messages WHERE relay_signer_principal = ?1 AND relay_message_id = ?2",
params![signer_principal.0, relay_message_id],
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)),
)
.optional()?;
if let Some((message_id, storage_owner)) = message {
tx.execute(
"UPDATE messages SET delivery_failed_at = ?1, delivery_failure = ?2 WHERE id = ?3",
params![sync::now_millis(), failure, message_id],
)?;
sync::record_event(
tx,
signer_id,
storage_owner,
sync::EntityType::Message,
message_id,
sync::Operation::Upsert,
)?;
}
tx.execute("UPDATE relay_inbox SET state = 'rejected', rejected_at = COALESCE(rejected_at, ?3) WHERE signer_id = ?1 AND message_id = ?2", params![signer_id, relay_message_id, sync::now_millis()])?;
tx.execute("UPDATE relay_inbox SET state = 'rejected', rejected_at = COALESCE(rejected_at, ?3) WHERE signer_principal = ?1 AND message_id = ?2", params![signer_principal.0, relay_message_id, sync::now_millis()])?;
tx.execute("DELETE FROM pending_relays WHERE id = ?1", [pending_id])?;
Ok(())
})

View file

@ -25,6 +25,68 @@ pub struct ChatSecretQuery {
pub version: Option<i64>,
}
pub fn principal_chat_id(
first: iota_identity::PrincipalHandle,
second: iota_identity::PrincipalHandle,
) -> Option<String> {
if first.0 <= 0 || second.0 <= 0 || first == second {
return None;
}
let (first, second) = if first.0 < second.0 {
(first.0, second.0)
} else {
(second.0, first.0)
};
Some(format!("principal:{first}:{second}"))
}
pub fn principal_secret_id(chat_id: &str) -> String {
format!("chat:{chat_id}:main")
}
pub fn migrate_chat_secret_namespace(
user_id: &str,
legacy_chat_id: &str,
legacy_secret_id: &str,
principal_chat_id: &str,
principal_secret_id: &str,
) -> Result<(), StorageError> {
if [
user_id,
legacy_chat_id,
legacy_secret_id,
principal_chat_id,
principal_secret_id,
]
.iter()
.any(|value| value.is_empty())
{
return Err("chat secret namespace is incomplete".into());
}
db::with_conn(&E2EE_DB, |conn| {
conn.execute(
r#"
INSERT OR IGNORE INTO chat_secrets (
user_id, chat_id, secret_id, version, encrypted_secret,
kem_ciphertext, wrapping_scheme, created_at, updated_at
)
SELECT user_id, ?4, ?5, version, encrypted_secret,
kem_ciphertext, wrapping_scheme, created_at, updated_at
FROM chat_secrets
WHERE user_id = ?1 AND chat_id = ?2 AND secret_id = ?3
"#,
params![
user_id,
legacy_chat_id,
legacy_secret_id,
principal_chat_id,
principal_secret_id
],
)?;
Ok(())
})
}
static E2EE_DB: LazyLock<Arc<Mutex<rusqlite::Connection>>> = LazyLock::new(|| {
let database = db::create_shared_connection(
"e2ee",
@ -186,3 +248,34 @@ fn chat_secret_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<StoredChatS
updated_at: row.get(8)?,
})
}
#[cfg(test)]
mod tests {
use super::{principal_chat_id, principal_secret_id};
use iota_identity::PrincipalHandle;
#[test]
fn chat_secret_namespace_uses_principals() {
let first = principal_chat_id(PrincipalHandle(3), PrincipalHandle(9));
let second = principal_chat_id(PrincipalHandle(9), PrincipalHandle(3));
assert_eq!(first, Some("principal:3:9".to_string()));
assert_eq!(first, second);
assert_eq!(
principal_secret_id("principal:3:9"),
"chat:principal:3:9:main"
);
}
#[test]
fn equal_or_invalid_principals_have_no_chat_namespace() {
assert_eq!(
principal_chat_id(PrincipalHandle(7), PrincipalHandle(7)),
None
);
assert_eq!(
principal_chat_id(PrincipalHandle(0), PrincipalHandle(7)),
None
);
}
}

View file

@ -1,11 +1,10 @@
/* Accepts an outgoing relay only when its retry record and retention state commit together. */
use crate::storage_error::StorageError;
use crate::util::{chat_files, db, message_storage_policy, relay_queue, relay_replay};
use iota_util::route_target::RouteTarget;
use rusqlite::OptionalExtension;
pub struct OutgoingRelay<'a> {
pub target: RouteTarget,
pub target: relay_queue::RelayTarget,
pub identity: &'a relay_queue::RelayIdentity,
pub frame: &'a [u8],
pub created_at: i64,
@ -24,22 +23,30 @@ pub fn commit_outgoing_relay(relay: OutgoingRelay<'_>) -> Result<(), StorageErro
relay.frame_id,
relay.type_map_version,
)?;
if message_storage_policy::get_in_tx(tx, relay.identity.signer_id)?.history_mode
let signer_id = tx
.query_row(
"SELECT local_user_id FROM hosted_principals WHERE principal_handle = ?1",
[relay.identity.signer.0],
|row| row.get::<_, i64>(0),
)
.optional()?
.ok_or_else(|| StorageError::Other("outgoing signer is not hosted locally".into()))?;
if message_storage_policy::get_in_tx(tx, signer_id)?.history_mode
== message_storage_policy::MessageHistoryMode::DeleteAfterClientDelivery
{
let message_id = tx.query_row("SELECT id FROM messages WHERE storage_owner = ?1 AND relay_signer_id = ?1 AND relay_message_id = ?2", rusqlite::params![relay.identity.signer_id, relay.identity.message_id], |row| row.get::<_, i64>(0)).optional()?;
let message_id = tx.query_row("SELECT id FROM messages WHERE storage_owner = ?1 AND relay_signer_principal = ?2 AND relay_message_id = ?3", rusqlite::params![signer_id, relay.identity.signer.0, relay.identity.message_id], |row| row.get::<_, i64>(0)).optional()?;
if let Some(message_id) = message_id {
chat_files::remove_message_history_in_tx(tx, relay.identity.signer_id, message_id)?;
chat_files::remove_message_history_in_tx(tx, signer_id, message_id)?;
}
}
relay_replay::mark_queued_in_tx(tx, relay.identity.signer_id, &relay.identity.message_id)?;
relay_replay::mark_queued_in_tx(tx, relay.identity.signer, &relay.identity.message_id)?;
Ok(())
})
}
pub fn apply_outgoing_history_policy(
storage_owner: i64,
relay_signer_id: i64,
relay_signer_principal: iota_identity::PrincipalHandle,
relay_message_id: &str,
) -> Result<(), StorageError> {
db::with_immediate_transaction(|tx| {
@ -49,8 +56,8 @@ pub fn apply_outgoing_history_policy(
return Ok(());
}
let message_id = tx.query_row(
"SELECT id FROM messages WHERE storage_owner = ?1 AND relay_signer_id = ?2 AND relay_message_id = ?3",
rusqlite::params![storage_owner, relay_signer_id, relay_message_id],
"SELECT id FROM messages WHERE storage_owner = ?1 AND relay_signer_principal = ?2 AND relay_message_id = ?3",
rusqlite::params![storage_owner, relay_signer_principal.0, relay_message_id],
|row| row.get::<_, i64>(0),
).optional()?;
if let Some(message_id) = message_id {

View file

@ -1,21 +1,44 @@
use crate::storage_error::StorageError;
use crate::util::db;
use iota_identity::{AuthorityId, IotaNodeId, PrincipalHandle};
use iota_util::route_target::RouteTarget;
use rusqlite::{OptionalExtension, Transaction, params};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PendingRelay {
pub id: i64,
pub target: RouteTarget,
pub target: RelayTarget,
pub frame: Vec<u8>,
pub created_at: i64,
pub frame_id: u32,
pub type_map_version: String,
pub relay_signer_id: Option<i64>,
pub relay_destination_user_id: Option<i64>,
pub signer_principal: Option<PrincipalHandle>,
pub destination_principal: Option<PrincipalHandle>,
pub relay_message_id: Option<String>,
pub delivery_state: PendingRelayDeliveryState,
pub last_error: Option<String>,
pub attempt_count: i64,
pub last_attempt_at: Option<i64>,
pub next_attempt_at: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RelayTarget {
User(u64),
Iota(IotaNodeId),
LegacyOmegaIota { omega: AuthorityId, iota_id: u64 },
}
impl RelayTarget {
pub fn legacy_wire_target(&self) -> Option<RouteTarget> {
match self {
Self::User(user_id) => Some(RouteTarget::User(*user_id)),
Self::LegacyOmegaIota { iota_id, .. } => Some(RouteTarget::Iota(*iota_id)),
Self::Iota(_) => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
@ -38,23 +61,25 @@ impl PendingRelayDeliveryState {
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RelayIdentity {
pub signer_id: i64,
pub destination_user_id: i64,
pub signer: PrincipalHandle,
pub recipient: PrincipalHandle,
pub message_id: String,
pub legacy_signer_id: Option<i64>,
pub legacy_recipient_id: Option<i64>,
}
pub fn enqueue(
target: RouteTarget,
target: RelayTarget,
relay: &RelayIdentity,
frame: &[u8],
created_at: i64,
frame_id: u32,
type_map_version: &str,
) -> Result<(), StorageError> {
if let RouteTarget::User(destination) = target {
if let RelayTarget::User(destination) = target {
let destination = i64::try_from(destination)
.map_err(|_| StorageError::Other("relay destination ID exceeds SQLite range".into()))?;
if destination != relay.destination_user_id {
if Some(destination) != relay.legacy_recipient_id {
return Err(StorageError::Other(
"user relay target does not match final recipient".into(),
));
@ -75,31 +100,49 @@ pub fn enqueue(
pub fn enqueue_in_tx(
tx: &Transaction<'_>,
target: RouteTarget,
target: RelayTarget,
relay: &RelayIdentity,
frame: &[u8],
created_at: i64,
frame_id: u32,
type_map_version: &str,
) -> Result<(), StorageError> {
let destination_id = i64::try_from(target.id())
.map_err(|_| StorageError::Other("relay destination ID exceeds SQLite range".into()))?;
let target_kind = match target {
RouteTarget::User(_) => 0_i64,
RouteTarget::Iota(_) => 1_i64,
let (destination_id, target_kind, destination_node_id, destination_authority_id) = match &target
{
RelayTarget::User(user_id) => (
i64::try_from(*user_id).map_err(|_| {
StorageError::Other("relay destination ID exceeds SQLite range".into())
})?,
0_i64,
None,
None,
),
RelayTarget::LegacyOmegaIota { omega, iota_id } => (
i64::try_from(*iota_id).map_err(|_| {
StorageError::Other("relay destination ID exceeds SQLite range".into())
})?,
1_i64,
None,
Some(omega.as_str()),
),
RelayTarget::Iota(node) => (0, 2_i64, Some(node.as_str()), None),
};
tx.execute(
"INSERT OR IGNORE INTO pending_relays (destination_id, target_kind, relay_signer_id, relay_destination_user_id, relay_message_id, frame, created_at, frame_id, type_map_version) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
"INSERT OR IGNORE INTO pending_relays (destination_id, target_kind, relay_signer_id, relay_destination_user_id, relay_message_id, frame, created_at, frame_id, type_map_version, signer_principal, destination_principal, destination_node_id, destination_authority_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)",
params![
destination_id,
target_kind,
relay.signer_id,
relay.destination_user_id,
relay.legacy_signer_id,
relay.legacy_recipient_id,
relay.message_id,
frame,
created_at,
i64::from(frame_id),
type_map_version
type_map_version,
relay.signer.0,
relay.recipient.0,
destination_node_id,
destination_authority_id,
],
)?;
Ok(())
@ -108,7 +151,7 @@ pub fn enqueue_in_tx(
pub fn list(limit: i64) -> Result<Vec<PendingRelay>, StorageError> {
db::with_db(|connection| {
let mut statement = connection.prepare(
"SELECT id, destination_id, target_kind, frame, created_at, frame_id, type_map_version, relay_signer_id, relay_destination_user_id, relay_message_id, delivery_state, last_error FROM pending_relays ORDER BY id LIMIT ?1",
"SELECT id, destination_id, target_kind, frame, created_at, frame_id, type_map_version, relay_signer_id, relay_destination_user_id, relay_message_id, delivery_state, last_error, destination_principal, signer_principal, destination_node_id, destination_authority_id, attempt_count, last_attempt_at, next_attempt_at FROM pending_relays ORDER BY id LIMIT ?1",
)?;
let rows = statement.query_map(params![limit.clamp(1, 500)], |row| {
let destination_id = row.get::<_, i64>(1)?;
@ -121,8 +164,26 @@ pub fn list(limit: i64) -> Result<Vec<PendingRelay>, StorageError> {
)
})?;
let target = match target_kind {
0 => RouteTarget::User(destination_id),
1 => RouteTarget::Iota(destination_id),
0 => RelayTarget::User(destination_id),
1 => RelayTarget::LegacyOmegaIota {
omega: AuthorityId::new(row.get::<_, String>(15)?).map_err(|error| {
rusqlite::Error::FromSqlConversionFailure(
15,
rusqlite::types::Type::Text,
Box::new(error),
)
})?,
iota_id: destination_id,
},
2 => RelayTarget::Iota(IotaNodeId::new(row.get::<_, String>(14)?).map_err(
|error| {
rusqlite::Error::FromSqlConversionFailure(
14,
rusqlite::types::Type::Text,
Box::new(error),
)
},
)?),
_ => {
return Err(rusqlite::Error::FromSqlConversionFailure(
2,
@ -146,9 +207,16 @@ pub fn list(limit: i64) -> Result<Vec<PendingRelay>, StorageError> {
type_map_version: row.get(6)?,
relay_signer_id: row.get(7)?,
relay_destination_user_id: row.get(8)?,
signer_principal: row.get::<_, Option<i64>>(13)?.map(PrincipalHandle),
destination_principal: row
.get::<_, Option<i64>>(12)?
.map(iota_identity::PrincipalHandle),
relay_message_id: row.get(9)?,
delivery_state: PendingRelayDeliveryState::from_db(row.get(10)?)?,
last_error: row.get(11)?,
attempt_count: row.get(16)?,
last_attempt_at: row.get(17)?,
next_attempt_at: row.get(18)?,
})
})?;
rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
@ -160,9 +228,12 @@ pub fn list(limit: i64) -> Result<Vec<PendingRelay>, StorageError> {
pub fn list_active(limit: i64) -> Result<Vec<PendingRelay>, StorageError> {
db::with_db(|connection| {
let mut statement = connection.prepare(
"SELECT p.id, p.destination_id, p.target_kind, p.frame, p.created_at, p.frame_id, p.type_map_version, p.relay_signer_id, p.relay_destination_user_id, p.relay_message_id, p.delivery_state, p.last_error FROM pending_relays p WHERE p.delivery_state = 'ready' AND ((p.target_kind = 0 AND EXISTS (SELECT 1 FROM users u WHERE u.user_id = p.destination_id)) OR (p.target_kind = 1 AND EXISTS (SELECT 1 FROM users u WHERE u.user_id = p.relay_signer_id))) ORDER BY p.id LIMIT ?1",
"SELECT p.id, p.destination_id, p.target_kind, p.frame, p.created_at, p.frame_id, p.type_map_version, p.relay_signer_id, p.relay_destination_user_id, p.relay_message_id, p.delivery_state, p.last_error, p.destination_principal, p.signer_principal, p.destination_node_id, p.destination_authority_id, p.attempt_count, p.last_attempt_at, p.next_attempt_at FROM pending_relays p WHERE p.delivery_state = 'ready' AND p.next_attempt_at <= ?1 AND ((p.target_kind = 0 AND EXISTS (SELECT 1 FROM users u WHERE u.user_id = p.destination_id)) OR (p.target_kind IN (1, 2) AND p.signer_principal IS NOT NULL)) ORDER BY p.id LIMIT ?2",
)?;
let rows = statement.query_map(
params![now_millis(), limit.clamp(1, 500)],
pending_relay_from_row,
)?;
let rows = statement.query_map(params![limit.clamp(1, 500)], pending_relay_from_row)?;
rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
})
}
@ -176,7 +247,7 @@ pub fn list_without_relay_identity_after(
limit: i64,
) -> Result<Vec<PendingRelay>, StorageError> {
db::with_db(|connection| {
let mut statement = connection.prepare("SELECT id, destination_id, target_kind, frame, created_at, frame_id, type_map_version, relay_signer_id, relay_destination_user_id, relay_message_id, delivery_state, last_error FROM pending_relays WHERE id > ?1 AND (relay_signer_id IS NULL OR relay_destination_user_id IS NULL OR relay_message_id IS NULL) ORDER BY id LIMIT ?2")?;
let mut statement = connection.prepare("SELECT id, destination_id, target_kind, frame, created_at, frame_id, type_map_version, relay_signer_id, relay_destination_user_id, relay_message_id, delivery_state, last_error, destination_principal, signer_principal, destination_node_id, destination_authority_id, attempt_count, last_attempt_at, next_attempt_at FROM pending_relays WHERE id > ?1 AND (signer_principal IS NULL OR destination_principal IS NULL OR relay_message_id IS NULL) ORDER BY id LIMIT ?2")?;
let rows = statement.query_map(
params![after_id, limit.clamp(1, 500)],
pending_relay_from_row,
@ -189,8 +260,16 @@ fn pending_relay_from_row(row: &rusqlite::Row<'_>) -> Result<PendingRelay, rusql
let destination_id = u64::try_from(row.get::<_, i64>(1)?)
.map_err(|_| rusqlite::Error::IntegralValueOutOfRange(1, 0))?;
let target = match row.get::<_, i64>(2)? {
0 => RouteTarget::User(destination_id),
1 => RouteTarget::Iota(destination_id),
0 => RelayTarget::User(destination_id),
1 => RelayTarget::LegacyOmegaIota {
omega: AuthorityId::new(row.get::<_, String>(15)?)
.map_err(|_| rusqlite::Error::InvalidQuery)?,
iota_id: destination_id,
},
2 => RelayTarget::Iota(
IotaNodeId::new(row.get::<_, String>(14)?)
.map_err(|_| rusqlite::Error::InvalidQuery)?,
),
_ => return Err(rusqlite::Error::IntegralValueOutOfRange(2, 0)),
};
Ok(PendingRelay {
@ -203,12 +282,55 @@ fn pending_relay_from_row(row: &rusqlite::Row<'_>) -> Result<PendingRelay, rusql
type_map_version: row.get(6)?,
relay_signer_id: row.get(7)?,
relay_destination_user_id: row.get(8)?,
signer_principal: row.get::<_, Option<i64>>(13)?.map(PrincipalHandle),
destination_principal: row
.get::<_, Option<i64>>(12)?
.map(iota_identity::PrincipalHandle),
relay_message_id: row.get(9)?,
delivery_state: PendingRelayDeliveryState::from_db(row.get(10)?)?,
last_error: row.get(11)?,
attempt_count: row.get(16)?,
last_attempt_at: row.get(17)?,
next_attempt_at: row.get(18)?,
})
}
pub fn record_retry(id: i64, now: i64, error: &str) -> Result<(), StorageError> {
db::with_db(|connection| {
let attempts = connection.query_row(
"SELECT attempt_count FROM pending_relays WHERE id = ?1",
[id],
|row| row.get::<_, i64>(0),
)?;
let exponent = u32::try_from(attempts.clamp(0, 6)).unwrap_or(6);
let delay = 5_000_i64.saturating_mul(1_i64 << exponent).min(300_000);
connection.execute(
"UPDATE pending_relays SET attempt_count = attempt_count + 1, last_attempt_at = ?2, next_attempt_at = ?3, last_error = ?4 WHERE id = ?1",
params![id, now, now.saturating_add(delay), error],
)?;
Ok(())
})
}
pub fn quarantine_for_frame(frame_id: u32, error: &str) -> Result<(), StorageError> {
db::with_db(|connection| {
connection.execute(
"UPDATE pending_relays SET delivery_state = 'quarantined', last_error = ?2 WHERE frame_id = ?1",
params![i64::from(frame_id), error],
)?;
Ok(())
})
}
fn now_millis() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis()
.try_into()
.unwrap_or(i64::MAX)
}
pub fn mark_client_offline(destination_user_id: i64, frame_id: u32) -> Result<(), StorageError> {
db::with_immediate_transaction(|tx| {
tx.execute("UPDATE pending_relays SET delivery_state = 'waiting_client', last_error = 'client_offline' WHERE target_kind = 0 AND destination_id = ?1 AND frame_id = ?2", params![destination_user_id, i64::from(frame_id)])?;
@ -258,7 +380,7 @@ pub fn set_relay_identity(id: i64, relay: &RelayIdentity) -> Result<(), StorageE
[id],
|row| row.get::<_, i64>(0),
)?;
if destination_id != relay.destination_user_id {
if Some(destination_id) != relay.legacy_recipient_id {
connection.execute(
"UPDATE pending_relays SET delivery_state = 'quarantined', last_error = 'target_recipient_mismatch' WHERE id = ?1",
[id],
@ -266,7 +388,7 @@ pub fn set_relay_identity(id: i64, relay: &RelayIdentity) -> Result<(), StorageE
return Ok(());
}
}
connection.execute("UPDATE pending_relays SET relay_signer_id = ?2, relay_destination_user_id = ?3, relay_message_id = ?4 WHERE id = ?1", params![id, relay.signer_id, relay.destination_user_id, relay.message_id])?;
connection.execute("UPDATE pending_relays SET relay_signer_id = ?2, relay_destination_user_id = ?3, relay_message_id = ?4, signer_principal = ?5, destination_principal = ?6 WHERE id = ?1", params![id, relay.legacy_signer_id, relay.legacy_recipient_id, relay.message_id, relay.signer.0, relay.recipient.0])?;
Ok(())
})
}
@ -274,7 +396,7 @@ pub fn set_relay_identity(id: i64, relay: &RelayIdentity) -> Result<(), StorageE
pub fn has_unclassified_relays() -> Result<bool, StorageError> {
db::with_db(|connection| {
connection
.query_row("SELECT EXISTS(SELECT 1 FROM pending_relays WHERE relay_signer_id IS NULL OR relay_destination_user_id IS NULL OR relay_message_id IS NULL)", [], |row| row.get(0))
.query_row("SELECT EXISTS(SELECT 1 FROM pending_relays WHERE signer_principal IS NULL OR destination_principal IS NULL OR relay_message_id IS NULL)", [], |row| row.get(0))
.map_err(Into::into)
})
}
@ -285,14 +407,24 @@ pub fn find_user_delivery_in_tx(
frame_id: u32,
) -> Result<Option<(i64, RelayIdentity)>, StorageError> {
tx.query_row(
"SELECT id, relay_signer_id, relay_destination_user_id, relay_message_id FROM pending_relays WHERE destination_id = ?1 AND target_kind = 0 AND frame_id = ?2",
"SELECT id, relay_signer_id, relay_destination_user_id, relay_message_id, signer_principal, destination_principal FROM pending_relays WHERE destination_id = ?1 AND target_kind = 0 AND frame_id = ?2",
params![destination_id, i64::from(frame_id)],
|row| {
let signer_id = row.get::<_, Option<i64>>(1)?;
let destination_user_id = row.get::<_, Option<i64>>(2)?;
let message_id = row.get::<_, Option<String>>(3)?;
match (signer_id, destination_user_id, message_id) {
(Some(signer_id), Some(destination_user_id), Some(message_id)) => Ok((row.get(0)?, RelayIdentity { signer_id, destination_user_id, message_id })),
(Some(signer_id), Some(destination_user_id), Some(message_id)) => {
let signer = row.get::<_, Option<i64>>(4)?.ok_or(rusqlite::Error::InvalidQuery)?;
let recipient = row.get::<_, Option<i64>>(5)?.ok_or(rusqlite::Error::InvalidQuery)?;
Ok((row.get(0)?, RelayIdentity {
signer: PrincipalHandle(signer),
recipient: PrincipalHandle(recipient),
message_id,
legacy_signer_id: Some(signer_id),
legacy_recipient_id: Some(destination_user_id),
}))
}
_ => Err(rusqlite::Error::InvalidQuery),
}
},
@ -310,14 +442,14 @@ pub fn acknowledge_in_tx(tx: &Transaction<'_>, id: i64) -> Result<(), StorageErr
pub fn reject_outgoing_relay(
destination_iota: u64,
frame_id: u32,
signer_id: i64,
signer_principal: PrincipalHandle,
relay_message_id: &str,
) -> Result<(), StorageError> {
let destination_iota = i64::try_from(destination_iota)
.map_err(|_| StorageError::Other("relay destination ID exceeds SQLite range".into()))?;
db::with_immediate_transaction(|tx| {
tx.execute("DELETE FROM pending_relays WHERE destination_id = ?1 AND target_kind = 1 AND frame_id = ?2", params![destination_iota, i64::from(frame_id)])?;
tx.execute("UPDATE relay_inbox SET state = 'rejected', rejected_at = COALESCE(rejected_at, ?3) WHERE signer_id = ?1 AND message_id = ?2", params![signer_id, relay_message_id, crate::util::sync::now_millis()])?;
tx.execute("UPDATE relay_inbox SET state = 'rejected', rejected_at = COALESCE(rejected_at, ?3) WHERE signer_principal = ?1 AND message_id = ?2", params![signer_principal.0, relay_message_id, crate::util::sync::now_millis()])?;
Ok(())
})
}

View file

@ -15,16 +15,30 @@ pub enum RelayReservation {
Existing { state: String, frame_matches: bool },
}
pub fn reserve(
signer_id: u64,
message_id: &str,
created_at: u64,
accepted_at: i64,
destination_id: u64,
frame: &[u8],
frame_id: u32,
type_map_version: &str,
) -> Result<RelayReservation, StorageError> {
pub struct RelayReservationRequest<'a> {
pub signer_principal: iota_identity::PrincipalHandle,
pub signer_id: u64,
pub message_id: &'a str,
pub created_at: u64,
pub accepted_at: i64,
pub destination_id: u64,
pub frame: &'a [u8],
pub frame_id: u32,
pub type_map_version: &'a str,
}
pub fn reserve(request: RelayReservationRequest<'_>) -> Result<RelayReservation, StorageError> {
let RelayReservationRequest {
signer_principal,
signer_id,
message_id,
created_at,
accepted_at,
destination_id,
frame,
frame_id,
type_map_version,
} = request;
let signer_id = i64::try_from(signer_id)
.map_err(|_| StorageError::Other("relay signer ID exceeds SQLite range".into()))?;
let created_at = i64::try_from(created_at)
@ -34,8 +48,9 @@ pub fn reserve(
db::with_db(|connection| {
let inserted = connection.execute(
"INSERT OR IGNORE INTO relay_inbox (signer_id, message_id, created_at, accepted_at, destination_id, frame, frame_id, type_map_version, state) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, 'received')",
"INSERT OR IGNORE INTO relay_inbox (signer_principal, signer_id, message_id, created_at, accepted_at, destination_id, frame, frame_id, type_map_version, state) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, 'received')",
params![
signer_principal.0,
signer_id,
message_id,
created_at,
@ -52,8 +67,8 @@ pub fn reserve(
let (state, existing_destination_id, existing_frame, existing_type_map_version):
(String, i64, Vec<u8>, String) = connection.query_row(
"SELECT state, destination_id, frame, type_map_version FROM relay_inbox WHERE signer_id = ?1 AND message_id = ?2",
params![signer_id, message_id],
"SELECT state, destination_id, frame, type_map_version FROM relay_inbox WHERE signer_principal = ?1 AND message_id = ?2",
params![signer_principal.0, message_id],
|row| {
Ok((
row.get(0)?,
@ -119,52 +134,67 @@ pub fn mark_delivered_for_frame_in_tx(
}
fn mark_transition(
signer_id: u64,
signer_principal: iota_identity::PrincipalHandle,
message_id: &str,
state: &str,
column: &str,
) -> Result<(), StorageError> {
let signer_id = i64::try_from(signer_id)
.map_err(|_| StorageError::Other("relay signer ID exceeds SQLite range".into()))?;
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64;
db::with_db(|connection| {
connection.execute(
&format!("UPDATE relay_inbox SET state = ?3, {column} = COALESCE({column}, ?4) WHERE signer_id = ?1 AND message_id = ?2"),
params![signer_id, message_id, state, timestamp],
&format!("UPDATE relay_inbox SET state = ?3, {column} = COALESCE({column}, ?4) WHERE signer_principal = ?1 AND message_id = ?2"),
params![signer_principal.0, message_id, state, timestamp],
)?;
Ok(())
})
}
pub fn mark_applied(signer_id: u64, message_id: &str) -> Result<(), StorageError> {
mark_transition(signer_id, message_id, "applied", "applied_at")
pub fn mark_applied(
signer_principal: iota_identity::PrincipalHandle,
message_id: &str,
) -> Result<(), StorageError> {
mark_transition(signer_principal, message_id, "applied", "applied_at")
}
pub fn mark_queued(signer_id: u64, message_id: &str) -> Result<(), StorageError> {
mark_transition(signer_id, message_id, "queued", "queued_at")
pub fn mark_queued(
signer_principal: iota_identity::PrincipalHandle,
message_id: &str,
) -> Result<(), StorageError> {
mark_transition(signer_principal, message_id, "queued", "queued_at")
}
pub fn mark_queued_in_tx(
tx: &rusqlite::Transaction<'_>,
signer_id: i64,
signer_principal: iota_identity::PrincipalHandle,
message_id: &str,
) -> Result<(), StorageError> {
tx.execute(
"UPDATE relay_inbox SET state = 'queued', queued_at = COALESCE(queued_at, ?3) WHERE signer_id = ?1 AND message_id = ?2",
params![signer_id, message_id, crate::util::sync::now_millis()],
"UPDATE relay_inbox SET state = 'queued', queued_at = COALESCE(queued_at, ?3) WHERE signer_principal = ?1 AND message_id = ?2",
params![signer_principal.0, message_id, crate::util::sync::now_millis()],
)?;
Ok(())
}
pub fn mark_downstream_acked(signer_id: u64, message_id: &str) -> Result<(), StorageError> {
mark_transition(signer_id, message_id, "delivered", "downstream_acked_at")
pub fn mark_downstream_acked(
signer_principal: iota_identity::PrincipalHandle,
message_id: &str,
) -> Result<(), StorageError> {
mark_transition(
signer_principal,
message_id,
"delivered",
"downstream_acked_at",
)
}
pub fn mark_rejected(signer_id: u64, message_id: &str) -> Result<(), StorageError> {
mark_transition(signer_id, message_id, "rejected", "rejected_at")
pub fn mark_rejected(
signer_principal: iota_identity::PrincipalHandle,
message_id: &str,
) -> Result<(), StorageError> {
mark_transition(signer_principal, message_id, "rejected", "rejected_at")
}
pub fn prune_completed(before_terminal_at: i64) -> Result<(), StorageError> {
@ -174,14 +204,19 @@ pub fn prune_completed(before_terminal_at: i64) -> Result<(), StorageError> {
params![before_terminal_at],
)?;
connection.execute(
"DELETE FROM relay_replay WHERE NOT EXISTS (SELECT 1 FROM relay_inbox WHERE relay_inbox.signer_id = relay_replay.signer_id AND relay_inbox.message_id = relay_replay.message_id)",
"DELETE FROM relay_replay WHERE NOT EXISTS (SELECT 1 FROM relay_inbox WHERE relay_inbox.signer_principal = relay_replay.signer_principal AND relay_inbox.message_id = relay_replay.message_id)",
[],
)?;
Ok(())
})
}
pub fn accept(signer_id: u64, message_id: &str, created_at: u64) -> Result<bool, StorageError> {
pub fn accept(
signer_principal: iota_identity::PrincipalHandle,
signer_id: u64,
message_id: &str,
created_at: u64,
) -> Result<bool, StorageError> {
let signer_id = i64::try_from(signer_id)
.map_err(|_| StorageError::Other("relay signer ID exceeds SQLite range".into()))?;
let created_at = i64::try_from(created_at)
@ -189,8 +224,8 @@ pub fn accept(signer_id: u64, message_id: &str, created_at: u64) -> Result<bool,
db::with_db(|connection| {
let inserted = connection.execute(
"INSERT OR IGNORE INTO relay_replay (signer_id, message_id, created_at) VALUES (?1, ?2, ?3)",
params![signer_id, message_id, created_at],
"INSERT OR IGNORE INTO relay_replay (signer_principal, signer_id, message_id, created_at) VALUES (?1, ?2, ?3, ?4)",
params![signer_principal.0, signer_id, message_id, created_at],
)?;
Ok(inserted == 1)
})
@ -201,26 +236,29 @@ mod tests {
use rusqlite::{Connection, params};
#[test]
fn replay_identity_uses_signer_and_message_id() -> Result<(), rusqlite::Error> {
fn replay_identity_uses_principal_and_message_id() -> Result<(), rusqlite::Error> {
let connection = Connection::open_in_memory()?;
connection.execute_batch(
"CREATE TABLE relay_replay (signer_id INTEGER NOT NULL, message_id TEXT NOT NULL, created_at INTEGER NOT NULL, PRIMARY KEY (signer_id, message_id));",
"CREATE TABLE relay_replay (signer_principal INTEGER NOT NULL, signer_id INTEGER NOT NULL, message_id TEXT NOT NULL, created_at INTEGER NOT NULL, PRIMARY KEY (signer_principal, message_id));",
)?;
let first = connection.execute(
"INSERT OR IGNORE INTO relay_replay (signer_id, message_id, created_at) VALUES (?1, ?2, ?3)",
params![7_i64, "message", 1_i64],
"INSERT OR IGNORE INTO relay_replay (signer_principal, signer_id, message_id, created_at) VALUES (?1, ?2, ?3, ?4)",
params![70_i64, 7_i64, "message", 1_i64],
)?;
let duplicate = connection.execute(
"INSERT OR IGNORE INTO relay_replay (signer_id, message_id, created_at) VALUES (?1, ?2, ?3)",
params![7_i64, "message", 2_i64],
"INSERT OR IGNORE INTO relay_replay (signer_principal, signer_id, message_id, created_at) VALUES (?1, ?2, ?3, ?4)",
params![70_i64, 7_i64, "message", 2_i64],
)?;
let other_signer = connection.execute(
"INSERT OR IGNORE INTO relay_replay (signer_id, message_id, created_at) VALUES (?1, ?2, ?3)",
params![8_i64, "message", 2_i64],
let other_authority_same_numeric_signer = connection.execute(
"INSERT OR IGNORE INTO relay_replay (signer_principal, signer_id, message_id, created_at) VALUES (?1, ?2, ?3, ?4)",
params![71_i64, 7_i64, "message", 2_i64],
)?;
assert_eq!((first, duplicate, other_signer), (1, 0, 1));
assert_eq!(
(first, duplicate, other_authority_same_numeric_signer),
(1, 0, 1)
);
Ok(())
}
}