[Fix] Connection Management
This commit is contained in:
parent
9e9e3597da
commit
3f2ac18333
122 changed files with 19970 additions and 5263 deletions
877
iota-storage/src/identity.rs
Normal file
877
iota-storage/src/identity.rs
Normal file
|
|
@ -0,0 +1,877 @@
|
|||
use iota_identity::{
|
||||
AuthorityId, AuthorityKind, IdentityError, LocalDescriptorPublisher, LocalUserDescriptor,
|
||||
LocalUserId, LocalUserStore, PrincipalDescriptor, PrincipalHandle, PrincipalHome, PrincipalId,
|
||||
PrincipalStore, PublicKeyBundle, ResolvedPrincipal, SignedPrincipalDescriptor,
|
||||
};
|
||||
use iota_util::crypto_helper::{public_key_bundle_from_base64, public_key_bundle_to_base64};
|
||||
use rusqlite::{OptionalExtension, params};
|
||||
use std::sync::Arc;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use crate::users::user_manager;
|
||||
use crate::util::db;
|
||||
|
||||
const LOCAL_DESCRIPTOR_LIFETIME_MILLIS: i64 = 30 * 24 * 60 * 60 * 1_000;
|
||||
const LOCAL_DESCRIPTOR_RENEWAL_MILLIS: i64 = 7 * 24 * 60 * 60 * 1_000;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SqliteLocalUserStore;
|
||||
|
||||
impl LocalUserStore for SqliteLocalUserStore {
|
||||
fn get_local_user(
|
||||
&self,
|
||||
id: LocalUserId,
|
||||
) -> Result<Option<LocalUserDescriptor>, IdentityError> {
|
||||
user_manager::get_user(id.0)
|
||||
.map_err(storage_error)
|
||||
.map(|user| user.map(local_descriptor))
|
||||
}
|
||||
|
||||
fn get_local_user_by_username(
|
||||
&self,
|
||||
username: &str,
|
||||
) -> Result<Option<LocalUserDescriptor>, IdentityError> {
|
||||
user_manager::get_user_by_username(username)
|
||||
.map_err(storage_error)
|
||||
.map(|user| user.map(local_descriptor))
|
||||
}
|
||||
|
||||
fn is_hosted_here(&self, id: LocalUserId) -> Result<bool, IdentityError> {
|
||||
self.get_local_user(id).map(|user| user.is_some())
|
||||
}
|
||||
|
||||
fn local_user_for_principal(
|
||||
&self,
|
||||
principal: PrincipalHandle,
|
||||
) -> Result<Option<LocalUserId>, IdentityError> {
|
||||
db::with_db(|connection| {
|
||||
connection
|
||||
.query_row(
|
||||
"SELECT local_user_id FROM hosted_principals WHERE principal_handle = ?1",
|
||||
[principal.0],
|
||||
|row| row.get::<_, i64>(0).map(LocalUserId),
|
||||
)
|
||||
.optional()
|
||||
.map_err(Into::into)
|
||||
})
|
||||
.map_err(storage_error)
|
||||
}
|
||||
|
||||
fn principal_for_local_user(
|
||||
&self,
|
||||
user: LocalUserId,
|
||||
) -> Result<Option<PrincipalHandle>, IdentityError> {
|
||||
SqlitePrincipalStore.principal_for_local_user(user)
|
||||
}
|
||||
}
|
||||
|
||||
fn local_descriptor(user: crate::users::user_profile::UserProfile) -> LocalUserDescriptor {
|
||||
LocalUserDescriptor {
|
||||
id: LocalUserId(user.user_id),
|
||||
username: user.username,
|
||||
display_name: user.display_name,
|
||||
public_key: user.public_key,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SqlitePrincipalStore;
|
||||
|
||||
impl SqlitePrincipalStore {
|
||||
pub fn principal_for_local_user(
|
||||
&self,
|
||||
user: LocalUserId,
|
||||
) -> Result<Option<PrincipalHandle>, IdentityError> {
|
||||
db::with_db(|connection| {
|
||||
connection
|
||||
.query_row(
|
||||
"SELECT principal_handle FROM hosted_principals WHERE local_user_id = ?1",
|
||||
[user.0],
|
||||
|row| row.get::<_, i64>(0).map(PrincipalHandle),
|
||||
)
|
||||
.optional()
|
||||
.map_err(Into::into)
|
||||
})
|
||||
.map_err(storage_error)
|
||||
}
|
||||
|
||||
pub fn migrate_legacy_omega_authority(
|
||||
&self,
|
||||
authority: &AuthorityId,
|
||||
) -> Result<(), IdentityError> {
|
||||
db::with_immediate_transaction(|transaction| {
|
||||
transaction.execute(
|
||||
"INSERT INTO identity_configuration (singleton, omega_authority_id) VALUES (1, ?1) ON CONFLICT(singleton) DO UPDATE SET omega_authority_id = excluded.omega_authority_id",
|
||||
[authority.as_str()],
|
||||
)?;
|
||||
let mut statement = transaction.prepare(
|
||||
"SELECT principal_pk, remote_user_id FROM principals WHERE authority_id = 'omega:central'",
|
||||
)?;
|
||||
let legacy = statement
|
||||
.query_map([], |row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)))?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
drop(statement);
|
||||
for (legacy_handle, user_id) in legacy {
|
||||
let current_handle = transaction
|
||||
.query_row(
|
||||
"SELECT principal_pk FROM principals WHERE authority_id = ?1 AND remote_user_id = ?2",
|
||||
params![authority.as_str(), user_id],
|
||||
|row| row.get::<_, i64>(0),
|
||||
)
|
||||
.optional()?;
|
||||
if let Some(current_handle) = current_handle {
|
||||
transaction.execute(
|
||||
"INSERT OR IGNORE INTO principal_keys (principal_pk, public_key, valid_from, valid_until, source_revision) SELECT ?1, public_key, valid_from, valid_until, source_revision FROM principal_keys WHERE principal_pk = ?2",
|
||||
params![current_handle, legacy_handle],
|
||||
)?;
|
||||
for (table, column) in [
|
||||
("contacts", "principal_handle"),
|
||||
("messages", "external_principal"),
|
||||
("relay_replay", "signer_principal"),
|
||||
("relay_inbox", "signer_principal"),
|
||||
("pending_relays", "destination_principal"),
|
||||
("blocked_users", "blocked_principal"),
|
||||
] {
|
||||
transaction.execute(
|
||||
&format!("UPDATE {table} SET {column} = ?1 WHERE {column} = ?2"),
|
||||
params![current_handle, legacy_handle],
|
||||
)?;
|
||||
}
|
||||
transaction.execute(
|
||||
"DELETE FROM principals WHERE principal_pk = ?1",
|
||||
[legacy_handle],
|
||||
)?;
|
||||
} else {
|
||||
transaction.execute(
|
||||
"UPDATE principals SET authority_id = ?1 WHERE principal_pk = ?2",
|
||||
params![authority.as_str(), legacy_handle],
|
||||
)?;
|
||||
}
|
||||
}
|
||||
transaction.execute_batch(
|
||||
"CREATE TEMP TABLE IF NOT EXISTS observed_omega_users (user_id INTEGER PRIMARY KEY); DELETE FROM observed_omega_users; INSERT OR IGNORE INTO observed_omega_users SELECT user_id FROM users WHERE user_id > 0; INSERT OR IGNORE INTO observed_omega_users SELECT user_id FROM contacts WHERE user_id > 0; INSERT OR IGNORE INTO observed_omega_users SELECT external_user FROM messages WHERE external_user > 0; INSERT OR IGNORE INTO observed_omega_users SELECT signer_id FROM relay_replay WHERE signer_id > 0; INSERT OR IGNORE INTO observed_omega_users SELECT signer_id FROM relay_inbox WHERE signer_id > 0; INSERT OR IGNORE INTO observed_omega_users SELECT destination_id FROM relay_inbox WHERE destination_id > 0; INSERT OR IGNORE INTO observed_omega_users SELECT relay_signer_id FROM pending_relays WHERE relay_signer_id > 0; INSERT OR IGNORE INTO observed_omega_users SELECT relay_destination_user_id FROM pending_relays WHERE relay_destination_user_id > 0; INSERT OR IGNORE INTO observed_omega_users SELECT blocked_user_id FROM blocked_users WHERE blocked_user_id > 0;"
|
||||
)?;
|
||||
transaction.execute(
|
||||
"INSERT OR IGNORE INTO principals (authority_kind, authority_id, remote_user_id, descriptor_revision, last_resolved_at) SELECT 'omega', ?1, user_id, 0, 0 FROM observed_omega_users",
|
||||
[authority.as_str()],
|
||||
)?;
|
||||
for (table, principal_column, user_column) in [
|
||||
("contacts", "principal_handle", "user_id"),
|
||||
("messages", "external_principal", "external_user"),
|
||||
("relay_replay", "signer_principal", "signer_id"),
|
||||
("relay_inbox", "signer_principal", "signer_id"),
|
||||
("pending_relays", "destination_principal", "relay_destination_user_id"),
|
||||
("blocked_users", "blocked_principal", "blocked_user_id"),
|
||||
] {
|
||||
transaction.execute(
|
||||
&format!("UPDATE {table} SET {principal_column} = (SELECT principal_pk FROM principals WHERE authority_id = ?1 AND remote_user_id = {table}.{user_column}) WHERE {principal_column} IS NULL AND {user_column} IS NOT NULL"),
|
||||
[authority.as_str()],
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.map_err(storage_error)
|
||||
}
|
||||
|
||||
pub fn ensure_local_principal(
|
||||
&self,
|
||||
authority: &AuthorityId,
|
||||
authority_kind: AuthorityKind,
|
||||
user: &LocalUserDescriptor,
|
||||
home: PrincipalHome,
|
||||
resolved_at: i64,
|
||||
) -> Result<PrincipalHandle, IdentityError> {
|
||||
let user_id = u64::try_from(user.id.0)
|
||||
.map_err(|_| IdentityError::InvalidDescriptor("negative local user ID".into()))?;
|
||||
let key = public_key_bundle_from_base64(&user.public_key)
|
||||
.ok_or_else(|| IdentityError::InvalidDescriptor("stored user key is invalid".into()))?;
|
||||
let encoded_home = encode_home(&home);
|
||||
let current = db::with_db(|connection| {
|
||||
connection
|
||||
.query_row(
|
||||
r#"SELECT p.authority_kind, p.username, p.display_name, p.home,
|
||||
p.descriptor_revision, p.descriptor_valid_until,
|
||||
COALESCE(p.descriptor_issued_at, p.last_resolved_at), p.retired_at,
|
||||
(SELECT public_key FROM principal_keys
|
||||
WHERE principal_pk = p.principal_pk
|
||||
AND (valid_until IS NULL OR valid_until > ?3)
|
||||
ORDER BY source_revision DESC LIMIT 1)
|
||||
FROM principals p
|
||||
WHERE p.authority_id = ?1 AND p.remote_user_id = ?2"#,
|
||||
params![authority.as_str(), user.id.0, resolved_at],
|
||||
|row| {
|
||||
Ok((
|
||||
row.get::<_, String>(0)?,
|
||||
row.get::<_, Option<String>>(1)?,
|
||||
row.get::<_, Option<String>>(2)?,
|
||||
row.get::<_, Option<String>>(3)?,
|
||||
row.get::<_, i64>(4)?,
|
||||
row.get::<_, Option<i64>>(5)?,
|
||||
row.get::<_, i64>(6)?,
|
||||
row.get::<_, Option<i64>>(7)?,
|
||||
row.get::<_, Option<String>>(8)?,
|
||||
))
|
||||
},
|
||||
)
|
||||
.optional()
|
||||
.map_err(Into::into)
|
||||
})
|
||||
.map_err(storage_error)?;
|
||||
if current.as_ref().is_some_and(|current| current.7.is_some()) {
|
||||
return Err(IdentityError::InvalidDescriptor(
|
||||
"retired local principal cannot be published".into(),
|
||||
));
|
||||
}
|
||||
let signed_fields_match = current.as_ref().is_some_and(|current| {
|
||||
current.0 == authority_kind.as_str()
|
||||
&& current.1.as_deref() == Some(user.username.as_str())
|
||||
&& current.2.as_ref() == user.display_name.as_ref()
|
||||
&& current.3.as_ref() == encoded_home.as_ref()
|
||||
&& current.8.as_deref() == Some(user.public_key.as_str())
|
||||
});
|
||||
let current_valid_until = current.as_ref().and_then(|current| current.5);
|
||||
let renew_validity = current_valid_until
|
||||
.is_none_or(|valid_until| valid_until <= resolved_at + LOCAL_DESCRIPTOR_RENEWAL_MILLIS);
|
||||
let revision = current.as_ref().map_or(0, |current| {
|
||||
if signed_fields_match && !renew_validity {
|
||||
current.4
|
||||
} else {
|
||||
current.4.saturating_add(1)
|
||||
}
|
||||
});
|
||||
let issued_at = current.as_ref().map_or(resolved_at, |current| {
|
||||
if signed_fields_match && !renew_validity {
|
||||
current.6
|
||||
} else {
|
||||
resolved_at
|
||||
}
|
||||
});
|
||||
let valid_until = if signed_fields_match && !renew_validity {
|
||||
current_valid_until
|
||||
} else {
|
||||
Some(resolved_at.saturating_add(LOCAL_DESCRIPTOR_LIFETIME_MILLIS))
|
||||
};
|
||||
let descriptor = iota_identity::VerifiedPrincipalDescriptor::from_trusted_authority(
|
||||
PrincipalDescriptor {
|
||||
principal: PrincipalId {
|
||||
authority: authority.clone(),
|
||||
user_id,
|
||||
},
|
||||
authority_kind,
|
||||
username: Some(user.username.clone()),
|
||||
display_name: user.display_name.clone(),
|
||||
public_keys: vec![key],
|
||||
home,
|
||||
revision,
|
||||
valid_until,
|
||||
issued_at,
|
||||
},
|
||||
)?;
|
||||
let handle = self.upsert_remote_descriptor(&descriptor)?;
|
||||
db::with_db(|connection| {
|
||||
connection.execute(
|
||||
"INSERT INTO hosted_principals (local_user_id, principal_handle) VALUES (?1, ?2) ON CONFLICT(local_user_id) DO UPDATE SET principal_handle = excluded.principal_handle",
|
||||
params![user.id.0, handle.0],
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
.map_err(storage_error)?;
|
||||
Ok(handle)
|
||||
}
|
||||
|
||||
pub fn local_principal_descriptor(
|
||||
&self,
|
||||
user: LocalUserId,
|
||||
) -> Result<PrincipalDescriptor, IdentityError> {
|
||||
db::with_db(|connection| {
|
||||
let row = connection
|
||||
.query_row(
|
||||
r#"SELECT p.authority_kind, p.authority_id, p.remote_user_id,
|
||||
p.username, p.display_name, p.home,
|
||||
p.descriptor_revision, p.descriptor_valid_until,
|
||||
COALESCE(p.descriptor_issued_at, p.last_resolved_at)
|
||||
FROM hosted_principals h
|
||||
JOIN principals p ON p.principal_pk = h.principal_handle
|
||||
WHERE h.local_user_id = ?1 AND p.retired_at IS NULL"#,
|
||||
[user.0],
|
||||
|row| {
|
||||
Ok((
|
||||
row.get::<_, String>(0)?,
|
||||
row.get::<_, String>(1)?,
|
||||
row.get::<_, i64>(2)?,
|
||||
row.get::<_, Option<String>>(3)?,
|
||||
row.get::<_, Option<String>>(4)?,
|
||||
row.get::<_, Option<String>>(5)?,
|
||||
row.get::<_, i64>(6)?,
|
||||
row.get::<_, Option<i64>>(7)?,
|
||||
row.get::<_, i64>(8)?,
|
||||
))
|
||||
},
|
||||
)
|
||||
.optional()?;
|
||||
let Some((kind, authority, remote_user_id, username, display_name, home, revision, valid_until, issued_at)) = row else {
|
||||
return Ok(None);
|
||||
};
|
||||
let principal_pk = connection.query_row(
|
||||
"SELECT principal_handle FROM hosted_principals WHERE local_user_id = ?1",
|
||||
[user.0],
|
||||
|row| row.get::<_, i64>(0),
|
||||
)?;
|
||||
let mut statement = connection.prepare(
|
||||
"SELECT public_key FROM principal_keys WHERE principal_pk = ?1 AND valid_from <= ?2 AND (valid_until IS NULL OR valid_until > ?2) ORDER BY source_revision DESC",
|
||||
)?;
|
||||
let encoded = statement
|
||||
.query_map(params![principal_pk, issued_at], |row| row.get::<_, String>(0))?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
Ok(Some((kind, authority, remote_user_id, username, display_name, home, revision, valid_until, issued_at, encoded)))
|
||||
})
|
||||
.map_err(storage_error)?
|
||||
.ok_or(IdentityError::NotFound)
|
||||
.and_then(|(kind, authority, remote_user_id, username, display_name, home, revision, valid_until, issued_at, encoded)| {
|
||||
let authority_kind = match kind.as_str() {
|
||||
"iota" => AuthorityKind::Iota,
|
||||
"omega" => AuthorityKind::Omega,
|
||||
_ => return Err(IdentityError::InvalidDescriptor("stored authority kind is invalid".into())),
|
||||
};
|
||||
let public_keys = encoded
|
||||
.into_iter()
|
||||
.map(|key| public_key_bundle_from_base64(&key).ok_or_else(|| IdentityError::InvalidDescriptor("stored principal key is invalid".into())))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
Ok(PrincipalDescriptor {
|
||||
principal: PrincipalId {
|
||||
authority: AuthorityId::new(authority)?,
|
||||
user_id: u64::try_from(remote_user_id).map_err(|_| IdentityError::InvalidDescriptor("stored principal user ID is negative".into()))?,
|
||||
},
|
||||
authority_kind,
|
||||
username,
|
||||
display_name,
|
||||
public_keys,
|
||||
home: decode_home(home.as_deref()).map_err(storage_error)?,
|
||||
revision,
|
||||
valid_until,
|
||||
issued_at,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/* Retire hosted identity after its data is purged. Keeping descriptor
|
||||
* revision state rejects descriptors issued before account removal. */
|
||||
pub fn retire_local_principal(
|
||||
&self,
|
||||
user: LocalUserId,
|
||||
retired_at: i64,
|
||||
) -> Result<(), IdentityError> {
|
||||
db::with_immediate_transaction(|transaction| {
|
||||
let principal = transaction
|
||||
.query_row(
|
||||
"SELECT principal_handle FROM hosted_principals WHERE local_user_id = ?1",
|
||||
[user.0],
|
||||
|row| row.get::<_, i64>(0),
|
||||
)
|
||||
.optional()?
|
||||
.ok_or_else(|| {
|
||||
crate::storage_error::StorageError::Other(
|
||||
"hosted principal was not found".into(),
|
||||
)
|
||||
})?;
|
||||
transaction.execute(
|
||||
"UPDATE principals SET descriptor_revision = descriptor_revision + 1, descriptor_valid_until = ?2, last_resolved_at = ?2, retired_at = ?2 WHERE principal_pk = ?1",
|
||||
params![principal, retired_at],
|
||||
)?;
|
||||
transaction.execute(
|
||||
"UPDATE principal_keys SET valid_until = MIN(COALESCE(valid_until, ?2), ?2) WHERE principal_pk = ?1",
|
||||
params![principal, retired_at],
|
||||
)?;
|
||||
transaction.execute(
|
||||
"DELETE FROM hosted_principals WHERE local_user_id = ?1",
|
||||
[user.0],
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
.map_err(storage_error)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SqliteLocalDescriptorPublisher {
|
||||
identity: iota_identity::LocalNodeIdentity,
|
||||
}
|
||||
|
||||
impl SqliteLocalDescriptorPublisher {
|
||||
pub fn new(identity: iota_identity::LocalNodeIdentity) -> Self {
|
||||
Self { identity }
|
||||
}
|
||||
}
|
||||
|
||||
impl LocalDescriptorPublisher for SqliteLocalDescriptorPublisher {
|
||||
fn principal_descriptor(
|
||||
&self,
|
||||
user: LocalUserId,
|
||||
) -> Result<SignedPrincipalDescriptor, IdentityError> {
|
||||
let local_user = SqliteLocalUserStore
|
||||
.get_local_user(user)?
|
||||
.ok_or(IdentityError::NotFound)?;
|
||||
SqlitePrincipalStore.ensure_local_principal(
|
||||
self.identity.authority_id(),
|
||||
AuthorityKind::Iota,
|
||||
&local_user,
|
||||
PrincipalHome::Iota(self.identity.node_id().clone()),
|
||||
now_millis(),
|
||||
)?;
|
||||
SignedPrincipalDescriptor::sign(
|
||||
SqlitePrincipalStore.local_principal_descriptor(user)?,
|
||||
&self.identity.keyring(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LocalIdentityResolver {
|
||||
authority: AuthorityId,
|
||||
authority_kind: AuthorityKind,
|
||||
home: PrincipalHome,
|
||||
local_users: Arc<dyn LocalUserStore>,
|
||||
principals: Arc<SqlitePrincipalStore>,
|
||||
}
|
||||
|
||||
impl LocalIdentityResolver {
|
||||
pub fn new(
|
||||
authority: AuthorityId,
|
||||
authority_kind: AuthorityKind,
|
||||
home: PrincipalHome,
|
||||
local_users: Arc<dyn LocalUserStore>,
|
||||
principals: Arc<SqlitePrincipalStore>,
|
||||
) -> Self {
|
||||
Self {
|
||||
authority,
|
||||
authority_kind,
|
||||
home,
|
||||
local_users,
|
||||
principals,
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_local(&self, user: LocalUserDescriptor) -> Result<ResolvedPrincipal, IdentityError> {
|
||||
let handle = self.principals.ensure_local_principal(
|
||||
&self.authority,
|
||||
self.authority_kind,
|
||||
&user,
|
||||
self.home.clone(),
|
||||
now_millis(),
|
||||
)?;
|
||||
self.principals
|
||||
.get_principal(handle)?
|
||||
.ok_or(IdentityError::NotFound)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl iota_identity::IdentityResolver for LocalIdentityResolver {
|
||||
async fn resolve_address(
|
||||
&self,
|
||||
address: &iota_identity::UserAddress,
|
||||
_: &iota_identity::ResolutionContext,
|
||||
) -> Result<ResolvedPrincipal, IdentityError> {
|
||||
if let Some(address_authority) = &address.authority {
|
||||
let matches_home = matches!(
|
||||
&self.home,
|
||||
PrincipalHome::Omega(locator) if locator == address_authority
|
||||
);
|
||||
if address_authority.as_str() != self.authority.as_str() && !matches_home {
|
||||
return Err(IdentityError::NotFound);
|
||||
}
|
||||
}
|
||||
let user = match &address.selector {
|
||||
iota_identity::UserSelector::UserId(user_id) => {
|
||||
let user_id = i64::try_from(*user_id).map_err(|_| {
|
||||
IdentityError::InvalidIdentifier("user ID exceeds local storage range".into())
|
||||
})?;
|
||||
self.local_users.get_local_user(LocalUserId(user_id))?
|
||||
}
|
||||
iota_identity::UserSelector::Username(username) => {
|
||||
self.local_users.get_local_user_by_username(username)?
|
||||
}
|
||||
}
|
||||
.ok_or(IdentityError::NotFound)?;
|
||||
let resolved = self.resolve_local(user)?;
|
||||
verify_pin(address.public_key_pin.as_ref(), &resolved.public_keys)?;
|
||||
Ok(resolved)
|
||||
}
|
||||
|
||||
async fn resolve_principal(
|
||||
&self,
|
||||
principal: &PrincipalId,
|
||||
) -> Result<ResolvedPrincipal, IdentityError> {
|
||||
if principal.authority != self.authority {
|
||||
return Err(IdentityError::NotFound);
|
||||
}
|
||||
let user_id = i64::try_from(principal.user_id).map_err(|_| {
|
||||
IdentityError::InvalidIdentifier("user ID exceeds local storage range".into())
|
||||
})?;
|
||||
let user = self
|
||||
.local_users
|
||||
.get_local_user(LocalUserId(user_id))?
|
||||
.ok_or(IdentityError::NotFound)?;
|
||||
self.resolve_local(user)
|
||||
}
|
||||
|
||||
async fn signing_keys(
|
||||
&self,
|
||||
principal: &PrincipalId,
|
||||
_: &iota_identity::ResolutionContext,
|
||||
) -> Result<Vec<PublicKeyBundle>, IdentityError> {
|
||||
self.resolve_principal(principal)
|
||||
.await
|
||||
.map(|resolved| resolved.public_keys)
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_pin(
|
||||
pin: Option<&PublicKeyBundle>,
|
||||
keys: &[PublicKeyBundle],
|
||||
) -> Result<(), IdentityError> {
|
||||
let Some(pin) = pin else {
|
||||
return Ok(());
|
||||
};
|
||||
let pin = pin
|
||||
.try_as_bytes()
|
||||
.map_err(|error| IdentityError::InvalidDescriptor(error.to_string()))?;
|
||||
let matches = keys.iter().any(|key| {
|
||||
key.try_as_bytes()
|
||||
.map(|candidate| candidate == pin)
|
||||
.unwrap_or(false)
|
||||
});
|
||||
if matches {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(IdentityError::KeyPinMismatch)
|
||||
}
|
||||
}
|
||||
|
||||
fn now_millis() -> i64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis()
|
||||
.try_into()
|
||||
.unwrap_or(i64::MAX)
|
||||
}
|
||||
|
||||
impl PrincipalStore for SqlitePrincipalStore {
|
||||
fn get_principal(
|
||||
&self,
|
||||
handle: PrincipalHandle,
|
||||
) -> Result<Option<ResolvedPrincipal>, IdentityError> {
|
||||
db::with_db(|connection| load_principal(connection, "p.principal_pk = ?1", handle.0))
|
||||
.map_err(storage_error)
|
||||
}
|
||||
|
||||
fn get_by_canonical_id(
|
||||
&self,
|
||||
principal: &PrincipalId,
|
||||
) -> Result<Option<ResolvedPrincipal>, IdentityError> {
|
||||
let remote_user_id = i64::try_from(principal.user_id).map_err(|_| {
|
||||
IdentityError::InvalidIdentifier("principal user ID exceeds storage range".into())
|
||||
})?;
|
||||
db::with_db(|connection| {
|
||||
let handle = connection
|
||||
.query_row(
|
||||
"SELECT principal_pk FROM principals WHERE authority_id = ?1 AND remote_user_id = ?2",
|
||||
params![principal.authority.as_str(), remote_user_id],
|
||||
|row| row.get::<_, i64>(0),
|
||||
)
|
||||
.optional()?;
|
||||
match handle {
|
||||
Some(handle) => load_principal(connection, "p.principal_pk = ?1", handle),
|
||||
None => Ok(None),
|
||||
}
|
||||
})
|
||||
.map_err(storage_error)
|
||||
}
|
||||
|
||||
fn get_by_username(
|
||||
&self,
|
||||
authority: &AuthorityId,
|
||||
username: &str,
|
||||
) -> Result<Option<ResolvedPrincipal>, IdentityError> {
|
||||
db::with_db(|connection| {
|
||||
let handle = connection
|
||||
.query_row(
|
||||
"SELECT principal_pk FROM principals WHERE authority_id = ?1 AND username = ?2 AND retired_at IS NULL",
|
||||
params![authority.as_str(), username],
|
||||
|row| row.get::<_, i64>(0),
|
||||
)
|
||||
.optional()?;
|
||||
match handle {
|
||||
Some(handle) => load_principal(connection, "p.principal_pk = ?1", handle),
|
||||
None => Ok(None),
|
||||
}
|
||||
})
|
||||
.map_err(storage_error)
|
||||
}
|
||||
|
||||
fn upsert_remote_descriptor(
|
||||
&self,
|
||||
descriptor: &iota_identity::VerifiedPrincipalDescriptor,
|
||||
) -> Result<PrincipalHandle, IdentityError> {
|
||||
let resolved_at = descriptor.resolved_at();
|
||||
let descriptor = descriptor.descriptor();
|
||||
if descriptor.public_keys.is_empty() {
|
||||
return Err(IdentityError::InvalidDescriptor(
|
||||
"principal descriptor contains no signing keys".into(),
|
||||
));
|
||||
}
|
||||
if descriptor.revision < 0 {
|
||||
return Err(IdentityError::InvalidDescriptor(
|
||||
"principal descriptor revision is negative".into(),
|
||||
));
|
||||
}
|
||||
if descriptor
|
||||
.valid_until
|
||||
.is_some_and(|valid_until| valid_until <= resolved_at)
|
||||
{
|
||||
return Err(IdentityError::InvalidDescriptor(
|
||||
"principal descriptor is already expired".into(),
|
||||
));
|
||||
}
|
||||
let remote_user_id = i64::try_from(descriptor.principal.user_id).map_err(|_| {
|
||||
IdentityError::InvalidIdentifier("principal user ID exceeds storage range".into())
|
||||
})?;
|
||||
let home = encode_home(&descriptor.home);
|
||||
db::with_immediate_transaction(|transaction| {
|
||||
let current = transaction
|
||||
.query_row(
|
||||
"SELECT principal_pk, descriptor_revision, last_resolved_at, retired_at, authority_kind, username, display_name, home, descriptor_valid_until, descriptor_issued_at FROM principals WHERE authority_id = ?1 AND remote_user_id = ?2",
|
||||
params![descriptor.principal.authority.as_str(), remote_user_id],
|
||||
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?, row.get::<_, i64>(2)?, row.get::<_, Option<i64>>(3)?, row.get::<_, String>(4)?, row.get::<_, Option<String>>(5)?, row.get::<_, Option<String>>(6)?, row.get::<_, Option<String>>(7)?, row.get::<_, Option<i64>>(8)?, row.get::<_, Option<i64>>(9)?)),
|
||||
)
|
||||
.optional()?;
|
||||
if let Some((handle, revision, current_resolved_at, retired_at, authority_kind, username, display_name, current_home, valid_until, issued_at)) = ¤t {
|
||||
if retired_at.is_some()
|
||||
|| *revision > descriptor.revision
|
||||
|| (*revision == descriptor.revision && *current_resolved_at > resolved_at)
|
||||
{
|
||||
return Err(crate::storage_error::StorageError::Other(
|
||||
"stale principal descriptor update was rejected".into(),
|
||||
));
|
||||
}
|
||||
if *revision == descriptor.revision {
|
||||
let mut stored_keys = transaction
|
||||
.prepare("SELECT public_key FROM principal_keys WHERE principal_pk = ?1 AND source_revision = ?2 ORDER BY public_key")?
|
||||
.query_map(params![handle, revision], |row| row.get::<_, String>(0))?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let mut descriptor_keys = descriptor
|
||||
.public_keys
|
||||
.iter()
|
||||
.map(public_key_bundle_to_base64)
|
||||
.collect::<Vec<_>>();
|
||||
stored_keys.sort();
|
||||
descriptor_keys.sort();
|
||||
if authority_kind != descriptor.authority_kind.as_str()
|
||||
|| username != &descriptor.username
|
||||
|| display_name != &descriptor.display_name
|
||||
|| current_home != &home
|
||||
|| valid_until != &descriptor.valid_until
|
||||
|| issued_at.is_some_and(|issued_at| issued_at != descriptor.issued_at)
|
||||
|| stored_keys != descriptor_keys
|
||||
{
|
||||
return Err(crate::storage_error::StorageError::Other(
|
||||
"conflicting principal descriptor revision was rejected".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
transaction.execute(
|
||||
r#"INSERT INTO principals (
|
||||
authority_kind, authority_id, remote_user_id, username, display_name,
|
||||
home, descriptor_revision, descriptor_valid_until, descriptor_issued_at,
|
||||
last_resolved_at
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)
|
||||
ON CONFLICT(authority_id, remote_user_id) DO UPDATE SET
|
||||
authority_kind = excluded.authority_kind,
|
||||
username = excluded.username,
|
||||
display_name = excluded.display_name,
|
||||
home = excluded.home,
|
||||
descriptor_revision = excluded.descriptor_revision,
|
||||
descriptor_valid_until = excluded.descriptor_valid_until,
|
||||
descriptor_issued_at = excluded.descriptor_issued_at,
|
||||
last_resolved_at = excluded.last_resolved_at"#,
|
||||
params![
|
||||
descriptor.authority_kind.as_str(),
|
||||
descriptor.principal.authority.as_str(),
|
||||
remote_user_id,
|
||||
descriptor.username,
|
||||
descriptor.display_name,
|
||||
home,
|
||||
descriptor.revision,
|
||||
descriptor.valid_until,
|
||||
descriptor.issued_at,
|
||||
resolved_at,
|
||||
],
|
||||
)?;
|
||||
let handle = transaction.query_row(
|
||||
"SELECT principal_pk FROM principals WHERE authority_id = ?1 AND remote_user_id = ?2",
|
||||
params![descriptor.principal.authority.as_str(), remote_user_id],
|
||||
|row| row.get::<_, i64>(0),
|
||||
)?;
|
||||
transaction.execute(
|
||||
"UPDATE principal_keys SET valid_until = MIN(COALESCE(valid_until, ?2), ?2) WHERE principal_pk = ?1",
|
||||
params![handle, resolved_at],
|
||||
)?;
|
||||
for key in &descriptor.public_keys {
|
||||
let encoded = public_key_bundle_to_base64(key);
|
||||
if encoded.is_empty() {
|
||||
return Err(crate::storage_error::StorageError::Other(
|
||||
"principal signing key could not be encoded".into(),
|
||||
));
|
||||
}
|
||||
transaction.execute(
|
||||
r#"INSERT INTO principal_keys (
|
||||
principal_pk, public_key, valid_from, valid_until, source_revision
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5)
|
||||
ON CONFLICT(principal_pk, public_key) DO UPDATE SET
|
||||
valid_until = excluded.valid_until,
|
||||
source_revision = excluded.source_revision"#,
|
||||
params![
|
||||
handle,
|
||||
encoded,
|
||||
resolved_at,
|
||||
descriptor.valid_until,
|
||||
descriptor.revision,
|
||||
],
|
||||
)?;
|
||||
}
|
||||
Ok(PrincipalHandle(handle))
|
||||
})
|
||||
.map_err(storage_error)
|
||||
}
|
||||
|
||||
fn signing_keys(&self, principal: &PrincipalId) -> Result<Vec<PublicKeyBundle>, IdentityError> {
|
||||
let resolved = self
|
||||
.get_by_canonical_id(principal)?
|
||||
.ok_or(IdentityError::NotFound)?;
|
||||
if !resolved.is_valid_at(now_millis()) {
|
||||
return Err(IdentityError::Unavailable(
|
||||
"cached principal descriptor is expired".into(),
|
||||
));
|
||||
}
|
||||
if resolved.public_keys.is_empty() {
|
||||
return Err(IdentityError::InvalidDescriptor(
|
||||
"principal has no current signing keys".into(),
|
||||
));
|
||||
}
|
||||
Ok(resolved.public_keys)
|
||||
}
|
||||
}
|
||||
|
||||
fn load_principal(
|
||||
connection: &rusqlite::Connection,
|
||||
predicate: &str,
|
||||
value: i64,
|
||||
) -> Result<Option<ResolvedPrincipal>, crate::storage_error::StorageError> {
|
||||
let query = format!(
|
||||
"SELECT p.principal_pk, p.authority_id, p.remote_user_id, p.username, p.home, p.descriptor_revision, p.descriptor_valid_until, p.last_resolved_at FROM principals p WHERE {predicate}"
|
||||
);
|
||||
let row = connection
|
||||
.query_row(&query, [value], |row| {
|
||||
Ok((
|
||||
row.get::<_, i64>(0)?,
|
||||
row.get::<_, String>(1)?,
|
||||
row.get::<_, i64>(2)?,
|
||||
row.get::<_, Option<String>>(3)?,
|
||||
row.get::<_, Option<String>>(4)?,
|
||||
row.get::<_, i64>(5)?,
|
||||
row.get::<_, Option<i64>>(6)?,
|
||||
row.get::<_, i64>(7)?,
|
||||
))
|
||||
})
|
||||
.optional()?;
|
||||
let Some((
|
||||
handle,
|
||||
authority,
|
||||
remote_user_id,
|
||||
username,
|
||||
home,
|
||||
descriptor_revision,
|
||||
valid_until,
|
||||
resolved_at,
|
||||
)) = row
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let mut statement = connection.prepare(
|
||||
"SELECT public_key FROM principal_keys WHERE principal_pk = ?1 AND valid_from <= ?2 AND (valid_until IS NULL OR valid_until > ?2) ORDER BY source_revision DESC, valid_from DESC",
|
||||
)?;
|
||||
let encoded = statement
|
||||
.query_map(params![handle, now_millis()], |row| row.get::<_, String>(0))?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let mut public_keys = Vec::with_capacity(encoded.len());
|
||||
for value in encoded {
|
||||
let key = public_key_bundle_from_base64(&value).ok_or_else(|| {
|
||||
crate::storage_error::StorageError::Other("stored principal key is invalid".into())
|
||||
})?;
|
||||
public_keys.push(key);
|
||||
}
|
||||
let authority = AuthorityId::new(authority)
|
||||
.map_err(|error| crate::storage_error::StorageError::Other(error.to_string()))?;
|
||||
let user_id = u64::try_from(remote_user_id).map_err(|_| {
|
||||
crate::storage_error::StorageError::Other("stored principal user ID is negative".into())
|
||||
})?;
|
||||
Ok(Some(ResolvedPrincipal {
|
||||
principal: PrincipalId { authority, user_id },
|
||||
handle: PrincipalHandle(handle),
|
||||
username,
|
||||
public_keys,
|
||||
home: decode_home(home.as_deref())?,
|
||||
descriptor_revision,
|
||||
valid_until,
|
||||
resolved_at,
|
||||
}))
|
||||
}
|
||||
|
||||
fn encode_home(home: &PrincipalHome) -> Option<String> {
|
||||
match home {
|
||||
PrincipalHome::Iota(id) => Some(format!("iota:{}", id.as_str())),
|
||||
PrincipalHome::LegacyOmegaIota { omega, iota_id } => {
|
||||
Some(format!("legacy_omega_iota:{}:{iota_id}", omega.as_str()))
|
||||
}
|
||||
PrincipalHome::Omega(locator) => Some(format!("omega:{}", locator.as_str())),
|
||||
PrincipalHome::Unknown => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_home(home: Option<&str>) -> Result<PrincipalHome, crate::storage_error::StorageError> {
|
||||
let Some(home) = home else {
|
||||
return Ok(PrincipalHome::Unknown);
|
||||
};
|
||||
if let Some(id) = home.strip_prefix("iota:") {
|
||||
return iota_identity::IotaNodeId::new(id)
|
||||
.map(PrincipalHome::Iota)
|
||||
.map_err(|error| crate::storage_error::StorageError::Other(error.to_string()));
|
||||
}
|
||||
if let Some(value) = home.strip_prefix("legacy_omega_iota:") {
|
||||
let (omega, iota_id) = value.rsplit_once(':').ok_or_else(|| {
|
||||
crate::storage_error::StorageError::Other(
|
||||
"stored legacy Omega Iota home is invalid".into(),
|
||||
)
|
||||
})?;
|
||||
let omega = AuthorityId::new(omega)
|
||||
.map_err(|error| crate::storage_error::StorageError::Other(error.to_string()))?;
|
||||
let iota_id = iota_id.parse::<u64>().map_err(|_| {
|
||||
crate::storage_error::StorageError::Other(
|
||||
"stored legacy Omega Iota ID is invalid".into(),
|
||||
)
|
||||
})?;
|
||||
return Ok(PrincipalHome::LegacyOmegaIota { omega, iota_id });
|
||||
}
|
||||
if let Some(locator) = home.strip_prefix("omega:") {
|
||||
return iota_identity::AuthorityLocator::new(locator)
|
||||
.map(PrincipalHome::Omega)
|
||||
.map_err(|error| crate::storage_error::StorageError::Other(error.to_string()));
|
||||
}
|
||||
Err(crate::storage_error::StorageError::Other(
|
||||
"stored principal home is invalid".into(),
|
||||
))
|
||||
}
|
||||
|
||||
fn storage_error(error: impl std::fmt::Display) -> IdentityError {
|
||||
IdentityError::Storage(error.to_string())
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
pub mod identity;
|
||||
pub mod node_directory;
|
||||
pub mod storage_error;
|
||||
pub mod users;
|
||||
pub mod util;
|
||||
|
|
|
|||
420
iota-storage/src/node_directory.rs
Normal file
420
iota-storage/src/node_directory.rs
Normal file
|
|
@ -0,0 +1,420 @@
|
|||
use async_trait::async_trait;
|
||||
use iota_identity::{
|
||||
AuthorityLocator, IdentityError, IotaNodeId, LocalNodeIdentity, NodeDescriptor,
|
||||
NodeIdentityResolver, PublicKeyBundle, ResolvedNodeIdentity, SignedNodeDescriptor,
|
||||
VerifiedNodeDescriptor,
|
||||
};
|
||||
use iota_util::crypto_helper::{public_key_bundle_from_base64, public_key_bundle_to_base64};
|
||||
use rusqlite::{OptionalExtension, params};
|
||||
|
||||
use crate::util::db;
|
||||
|
||||
const DESCRIPTOR_LIFETIME_MILLIS: i64 = 30 * 24 * 60 * 60 * 1_000;
|
||||
const DESCRIPTOR_RENEWAL_MILLIS: i64 = 7 * 24 * 60 * 60 * 1_000;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SqliteNodeDirectory;
|
||||
|
||||
impl SqliteNodeDirectory {
|
||||
pub fn key_for_authentication_hint(
|
||||
&self,
|
||||
hint: u64,
|
||||
) -> Result<Option<PublicKeyBundle>, IdentityError> {
|
||||
db::with_db(|connection| {
|
||||
let mut statement = connection.prepare("SELECT node_id FROM node_directory")?;
|
||||
let nodes = statement
|
||||
.query_map([], |row| row.get::<_, String>(0))?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
for node in nodes {
|
||||
let node = IotaNodeId::new(node).map_err(|error| {
|
||||
crate::storage_error::StorageError::Other(error.to_string())
|
||||
})?;
|
||||
if node.mtp_authentication_hint() != hint {
|
||||
continue;
|
||||
}
|
||||
let key = connection
|
||||
.query_row(
|
||||
"SELECT public_key FROM node_keys WHERE node_id = ?1 AND (valid_until IS NULL OR valid_until > ?2) ORDER BY source_revision DESC LIMIT 1",
|
||||
params![node.as_str(), now_millis()],
|
||||
|row| row.get::<_, String>(0),
|
||||
)
|
||||
.optional()?;
|
||||
return Ok(key);
|
||||
}
|
||||
Ok(None)
|
||||
})
|
||||
.map_err(storage_error)?
|
||||
.map(|key| {
|
||||
public_key_bundle_from_base64(&key).ok_or_else(|| {
|
||||
IdentityError::InvalidDescriptor("stored node key is invalid".into())
|
||||
})
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
pub fn ensure_local_descriptor(
|
||||
&self,
|
||||
identity: &LocalNodeIdentity,
|
||||
direct_endpoints: Vec<AuthorityLocator>,
|
||||
relay_hints: Vec<AuthorityLocator>,
|
||||
now: i64,
|
||||
) -> Result<SignedNodeDescriptor, IdentityError> {
|
||||
let current = self.load(identity.node_id())?;
|
||||
let fields_match = current.as_ref().is_some_and(|current| {
|
||||
current.direct_endpoints == direct_endpoints && current.relay_hints == relay_hints
|
||||
});
|
||||
let renew = current
|
||||
.as_ref()
|
||||
.and_then(|current| current.valid_until)
|
||||
.is_none_or(|valid_until| valid_until <= now + DESCRIPTOR_RENEWAL_MILLIS);
|
||||
let revision = current.as_ref().map_or(0, |current| {
|
||||
if fields_match && !renew {
|
||||
current.descriptor_revision
|
||||
} else {
|
||||
current.descriptor_revision.saturating_add(1)
|
||||
}
|
||||
});
|
||||
let valid_until = if fields_match && !renew {
|
||||
current.and_then(|current| current.valid_until)
|
||||
} else {
|
||||
Some(now.saturating_add(DESCRIPTOR_LIFETIME_MILLIS))
|
||||
};
|
||||
let descriptor = NodeDescriptor {
|
||||
node: identity.node_id().clone(),
|
||||
public_keys: identity.public_keys(),
|
||||
direct_endpoints,
|
||||
relay_hints,
|
||||
revision,
|
||||
valid_until,
|
||||
};
|
||||
let signed = SignedNodeDescriptor::sign(descriptor, identity)?;
|
||||
let verified = signed
|
||||
.clone()
|
||||
.verify(current_revision(&self.load(identity.node_id())?), now)?;
|
||||
self.upsert(&verified)?;
|
||||
Ok(signed)
|
||||
}
|
||||
|
||||
pub fn upsert(&self, verified: &VerifiedNodeDescriptor) -> Result<(), IdentityError> {
|
||||
let descriptor = verified.descriptor();
|
||||
let direct_endpoints = serde_json::to_string(&descriptor.direct_endpoints)
|
||||
.map_err(|error| IdentityError::Storage(error.to_string()))?;
|
||||
let relay_hints = serde_json::to_string(&descriptor.relay_hints)
|
||||
.map_err(|error| IdentityError::Storage(error.to_string()))?;
|
||||
let public_key = public_key_bundle_to_base64(&descriptor.public_keys);
|
||||
if public_key.is_empty() {
|
||||
return Err(IdentityError::InvalidDescriptor(
|
||||
"node public key could not be encoded".into(),
|
||||
));
|
||||
}
|
||||
db::with_immediate_transaction(|transaction| {
|
||||
let current = transaction
|
||||
.query_row(
|
||||
"SELECT n.descriptor_revision, n.last_resolved_at, n.descriptor_valid_until, n.direct_endpoints, n.relay_hints, (SELECT public_key FROM node_keys WHERE node_id = n.node_id AND source_revision = n.descriptor_revision LIMIT 1) FROM node_directory n WHERE n.node_id = ?1",
|
||||
[descriptor.node.as_str()],
|
||||
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?, row.get::<_, Option<i64>>(2)?, row.get::<_, String>(3)?, row.get::<_, String>(4)?, row.get::<_, Option<String>>(5)?)),
|
||||
)
|
||||
.optional()?;
|
||||
if let Some((revision, resolved_at, valid_until, current_endpoints, current_relays, current_key)) = current {
|
||||
if revision > descriptor.revision
|
||||
|| (revision == descriptor.revision && resolved_at > verified.resolved_at())
|
||||
{
|
||||
return Err(crate::storage_error::StorageError::Other(
|
||||
"stale node descriptor update was rejected".into(),
|
||||
));
|
||||
}
|
||||
if revision == descriptor.revision
|
||||
&& (valid_until != descriptor.valid_until
|
||||
|| current_endpoints != direct_endpoints
|
||||
|| current_relays != relay_hints
|
||||
|| current_key.as_deref() != Some(public_key.as_str()))
|
||||
{
|
||||
return Err(crate::storage_error::StorageError::Other(
|
||||
"conflicting node descriptor revision was rejected".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
transaction.execute(
|
||||
r#"INSERT INTO node_directory (
|
||||
node_id, descriptor_revision, descriptor_valid_until,
|
||||
direct_endpoints, relay_hints, last_resolved_at
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6)
|
||||
ON CONFLICT(node_id) DO UPDATE SET
|
||||
descriptor_revision = excluded.descriptor_revision,
|
||||
descriptor_valid_until = excluded.descriptor_valid_until,
|
||||
direct_endpoints = excluded.direct_endpoints,
|
||||
relay_hints = excluded.relay_hints,
|
||||
last_resolved_at = excluded.last_resolved_at"#,
|
||||
params![
|
||||
descriptor.node.as_str(),
|
||||
descriptor.revision,
|
||||
descriptor.valid_until,
|
||||
direct_endpoints,
|
||||
relay_hints,
|
||||
verified.resolved_at(),
|
||||
],
|
||||
)?;
|
||||
transaction.execute(
|
||||
"UPDATE node_keys SET valid_until = MIN(COALESCE(valid_until, ?2), ?2) WHERE node_id = ?1",
|
||||
params![descriptor.node.as_str(), verified.resolved_at()],
|
||||
)?;
|
||||
transaction.execute(
|
||||
r#"INSERT INTO node_keys (
|
||||
node_id, public_key, valid_from, valid_until, source_revision
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5)
|
||||
ON CONFLICT(node_id, public_key) DO UPDATE SET
|
||||
valid_until = excluded.valid_until,
|
||||
source_revision = excluded.source_revision"#,
|
||||
params![
|
||||
descriptor.node.as_str(),
|
||||
public_key,
|
||||
verified.resolved_at(),
|
||||
descriptor.valid_until,
|
||||
descriptor.revision,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
.map_err(storage_error)
|
||||
}
|
||||
|
||||
pub fn pin_endpoint_record(
|
||||
&self,
|
||||
endpoint: &AuthorityLocator,
|
||||
node: &IotaNodeId,
|
||||
now: i64,
|
||||
) -> Result<(), IdentityError> {
|
||||
db::with_db(|connection| {
|
||||
let current = connection
|
||||
.query_row(
|
||||
"SELECT node_id FROM node_endpoint_pins WHERE endpoint = ?1",
|
||||
[endpoint.as_str()],
|
||||
|row| row.get::<_, String>(0),
|
||||
)
|
||||
.optional()?;
|
||||
if current.as_deref().is_some_and(|current| current != node.as_str()) {
|
||||
return Err(crate::storage_error::StorageError::Other(
|
||||
"endpoint node identity changed from its TOFU pin".into(),
|
||||
));
|
||||
}
|
||||
connection.execute(
|
||||
"INSERT OR IGNORE INTO node_endpoint_pins (endpoint, node_id, pinned_at) VALUES (?1, ?2, ?3)",
|
||||
params![endpoint.as_str(), node.as_str(), now],
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
.map_err(storage_error)
|
||||
}
|
||||
|
||||
pub fn pinned_node_record(
|
||||
&self,
|
||||
endpoint: &AuthorityLocator,
|
||||
) -> Result<Option<IotaNodeId>, IdentityError> {
|
||||
db::with_db(|connection| {
|
||||
connection
|
||||
.query_row(
|
||||
"SELECT node_id FROM node_endpoint_pins WHERE endpoint = ?1",
|
||||
[endpoint.as_str()],
|
||||
|row| row.get::<_, String>(0),
|
||||
)
|
||||
.optional()
|
||||
.map_err(Into::into)
|
||||
})
|
||||
.map_err(storage_error)?
|
||||
.map(IotaNodeId::new)
|
||||
.transpose()
|
||||
}
|
||||
|
||||
pub fn pin_endpoint_certificate_record(
|
||||
&self,
|
||||
endpoint: &AuthorityLocator,
|
||||
certificate: &[u8],
|
||||
) -> Result<(), IdentityError> {
|
||||
if certificate.is_empty() {
|
||||
return Err(IdentityError::InvalidDescriptor(
|
||||
"endpoint TLS certificate is empty".into(),
|
||||
));
|
||||
}
|
||||
db::with_db(|connection| {
|
||||
let current = connection
|
||||
.query_row(
|
||||
"SELECT tls_certificate FROM node_endpoint_pins WHERE endpoint = ?1",
|
||||
[endpoint.as_str()],
|
||||
|row| row.get::<_, Option<Vec<u8>>>(0),
|
||||
)
|
||||
.optional()?
|
||||
.ok_or_else(|| {
|
||||
crate::storage_error::StorageError::Other(
|
||||
"endpoint must be pinned before its TLS certificate".into(),
|
||||
)
|
||||
})?;
|
||||
if current
|
||||
.as_deref()
|
||||
.is_some_and(|current| current != certificate)
|
||||
{
|
||||
return Err(crate::storage_error::StorageError::Other(
|
||||
"endpoint TLS certificate changed from its TOFU pin".into(),
|
||||
));
|
||||
}
|
||||
connection.execute(
|
||||
"UPDATE node_endpoint_pins SET tls_certificate = COALESCE(tls_certificate, ?2) WHERE endpoint = ?1",
|
||||
params![endpoint.as_str(), certificate],
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
.map_err(storage_error)
|
||||
}
|
||||
|
||||
pub fn pinned_endpoint_certificate_record(
|
||||
&self,
|
||||
endpoint: &AuthorityLocator,
|
||||
) -> Result<Option<Vec<u8>>, IdentityError> {
|
||||
db::with_db(|connection| {
|
||||
connection
|
||||
.query_row(
|
||||
"SELECT tls_certificate FROM node_endpoint_pins WHERE endpoint = ?1",
|
||||
[endpoint.as_str()],
|
||||
|row| row.get::<_, Option<Vec<u8>>>(0),
|
||||
)
|
||||
.optional()
|
||||
.map(|certificate| certificate.flatten())
|
||||
.map_err(Into::into)
|
||||
})
|
||||
.map_err(storage_error)
|
||||
}
|
||||
|
||||
pub fn record_successful_endpoint(
|
||||
&self,
|
||||
node: &IotaNodeId,
|
||||
endpoint: &AuthorityLocator,
|
||||
) -> Result<(), IdentityError> {
|
||||
db::with_db(|connection| {
|
||||
connection.execute(
|
||||
"UPDATE node_directory SET last_successful_endpoint = ?2 WHERE node_id = ?1",
|
||||
params![node.as_str(), endpoint.as_str()],
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
.map_err(storage_error)
|
||||
}
|
||||
|
||||
fn load(&self, node: &IotaNodeId) -> Result<Option<ResolvedNodeIdentity>, IdentityError> {
|
||||
db::with_db(|connection| {
|
||||
let row = connection
|
||||
.query_row(
|
||||
"SELECT descriptor_revision, descriptor_valid_until, direct_endpoints, relay_hints, last_successful_endpoint, last_resolved_at FROM node_directory WHERE node_id = ?1",
|
||||
[node.as_str()],
|
||||
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, Option<i64>>(1)?, row.get::<_, String>(2)?, row.get::<_, String>(3)?, row.get::<_, Option<String>>(4)?, row.get::<_, i64>(5)?)),
|
||||
)
|
||||
.optional()?;
|
||||
let Some((descriptor_revision, valid_until, direct_endpoints, relay_hints, last_successful_endpoint, resolved_at)) = row else {
|
||||
return Ok(None);
|
||||
};
|
||||
let mut statement = connection.prepare(
|
||||
"SELECT public_key FROM node_keys WHERE node_id = ?1 AND valid_from <= ?2 AND (valid_until IS NULL OR valid_until > ?2) ORDER BY source_revision DESC",
|
||||
)?;
|
||||
let encoded = statement
|
||||
.query_map(params![node.as_str(), now_millis()], |row| row.get::<_, String>(0))?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
Ok(Some((descriptor_revision, valid_until, direct_endpoints, relay_hints, last_successful_endpoint, resolved_at, encoded)))
|
||||
})
|
||||
.map_err(storage_error)?
|
||||
.map(|(descriptor_revision, valid_until, direct_endpoints, relay_hints, last_successful_endpoint, resolved_at, encoded)| {
|
||||
Ok(ResolvedNodeIdentity {
|
||||
node: node.clone(),
|
||||
public_keys: encoded
|
||||
.into_iter()
|
||||
.map(|key| public_key_bundle_from_base64(&key).ok_or_else(|| IdentityError::InvalidDescriptor("stored node key is invalid".into())))
|
||||
.collect::<Result<Vec<PublicKeyBundle>, _>>()?,
|
||||
descriptor_revision,
|
||||
valid_until,
|
||||
direct_endpoints: serde_json::from_str(&direct_endpoints).map_err(|error| IdentityError::Storage(error.to_string()))?,
|
||||
relay_hints: serde_json::from_str(&relay_hints).map_err(|error| IdentityError::Storage(error.to_string()))?,
|
||||
last_successful_endpoint: last_successful_endpoint.map(AuthorityLocator::new).transpose()?,
|
||||
resolved_at,
|
||||
})
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl NodeIdentityResolver for SqliteNodeDirectory {
|
||||
async fn resolve_node(&self, node: &IotaNodeId) -> Result<ResolvedNodeIdentity, IdentityError> {
|
||||
let resolved = self.load(node)?.ok_or(IdentityError::NotFound)?;
|
||||
if resolved
|
||||
.valid_until
|
||||
.is_some_and(|valid_until| valid_until <= now_millis())
|
||||
{
|
||||
return Err(IdentityError::Unavailable(
|
||||
"cached node descriptor is expired".into(),
|
||||
));
|
||||
}
|
||||
Ok(resolved)
|
||||
}
|
||||
}
|
||||
|
||||
impl iota_identity::NodeDirectory for SqliteNodeDirectory {
|
||||
fn upsert_node_descriptor(
|
||||
&self,
|
||||
descriptor: &VerifiedNodeDescriptor,
|
||||
) -> Result<(), IdentityError> {
|
||||
self.upsert(descriptor)
|
||||
}
|
||||
|
||||
fn pin_endpoint(
|
||||
&self,
|
||||
endpoint: &AuthorityLocator,
|
||||
node: &IotaNodeId,
|
||||
now: i64,
|
||||
) -> Result<(), IdentityError> {
|
||||
self.pin_endpoint_record(endpoint, node, now)
|
||||
}
|
||||
|
||||
fn pinned_node(
|
||||
&self,
|
||||
endpoint: &AuthorityLocator,
|
||||
) -> Result<Option<IotaNodeId>, IdentityError> {
|
||||
self.pinned_node_record(endpoint)
|
||||
}
|
||||
|
||||
fn pin_endpoint_certificate(
|
||||
&self,
|
||||
endpoint: &AuthorityLocator,
|
||||
certificate: &[u8],
|
||||
) -> Result<(), IdentityError> {
|
||||
self.pin_endpoint_certificate_record(endpoint, certificate)
|
||||
}
|
||||
|
||||
fn pinned_endpoint_certificate(
|
||||
&self,
|
||||
endpoint: &AuthorityLocator,
|
||||
) -> Result<Option<Vec<u8>>, IdentityError> {
|
||||
self.pinned_endpoint_certificate_record(endpoint)
|
||||
}
|
||||
|
||||
fn record_successful_endpoint(
|
||||
&self,
|
||||
node: &IotaNodeId,
|
||||
endpoint: &AuthorityLocator,
|
||||
) -> Result<(), IdentityError> {
|
||||
SqliteNodeDirectory::record_successful_endpoint(self, node, endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
fn current_revision(node: &Option<ResolvedNodeIdentity>) -> Option<i64> {
|
||||
node.as_ref().map(|node| node.descriptor_revision)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
fn storage_error(error: impl std::fmt::Display) -> IdentityError {
|
||||
IdentityError::Storage(error.to_string())
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
#[derive(Debug, Clone)]
|
||||
pub struct Contact {
|
||||
pub user_id: i64,
|
||||
pub principal: Option<iota_identity::PrincipalHandle>,
|
||||
pub user_name: Option<String>,
|
||||
pub created_at: i64,
|
||||
pub last_message_at: Option<i64>,
|
||||
|
|
@ -12,6 +13,7 @@ impl Default for Contact {
|
|||
fn default() -> Self {
|
||||
Contact {
|
||||
user_id: 0,
|
||||
principal: None,
|
||||
user_name: None,
|
||||
created_at: 0,
|
||||
last_message_at: None,
|
||||
|
|
@ -28,7 +30,8 @@ impl Contact {
|
|||
.unwrap_or_default()
|
||||
.as_millis() as i64;
|
||||
Contact {
|
||||
user_id: user_id,
|
||||
user_id,
|
||||
principal: None,
|
||||
user_name: None,
|
||||
created_at,
|
||||
last_message_at: None,
|
||||
|
|
@ -36,6 +39,13 @@ impl Contact {
|
|||
notifications_read_at: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn for_principal(user_id: i64, principal: iota_identity::PrincipalHandle) -> Self {
|
||||
Self {
|
||||
principal: Some(principal),
|
||||
..Self::new(user_id)
|
||||
}
|
||||
}
|
||||
pub fn set_last_message_at(&mut self, p0: i64) {
|
||||
self.last_message_at = Option::from(p0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,31 @@ pub fn try_add_user(user: UserProfile) -> Result<(), crate::storage_error::Stora
|
|||
try_add_user_with_credential_origin(user, CredentialOrigin::Local)
|
||||
}
|
||||
|
||||
/* Reserve a local identity before account creation. The counter never moves
|
||||
* backward, so a removed account cannot be assigned its old canonical ID. */
|
||||
pub fn allocate_local_user_id(maximum: i64) -> Result<i64, crate::storage_error::StorageError> {
|
||||
db::with_immediate_transaction(|tx| {
|
||||
let next = tx.query_row(
|
||||
"SELECT next_user_id FROM local_identity_allocator WHERE singleton = 1",
|
||||
[],
|
||||
|row| row.get::<_, i64>(0),
|
||||
)?;
|
||||
if next < 1 || next > maximum {
|
||||
return Err(crate::storage_error::StorageError::Other(
|
||||
"local user ID space is exhausted".into(),
|
||||
));
|
||||
}
|
||||
let following = next.checked_add(1).ok_or_else(|| {
|
||||
crate::storage_error::StorageError::Other("local user ID space is exhausted".into())
|
||||
})?;
|
||||
tx.execute(
|
||||
"UPDATE local_identity_allocator SET next_user_id = ?1 WHERE singleton = 1",
|
||||
params![following],
|
||||
)?;
|
||||
Ok(next)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn try_add_user_with_credential_origin(
|
||||
user: UserProfile,
|
||||
credential_origin: CredentialOrigin,
|
||||
|
|
@ -457,15 +482,15 @@ fn purge_database_rows(user_id: i64) -> Result<(), crate::storage_error::Storage
|
|||
params![user_id],
|
||||
)?;
|
||||
tx.execute(
|
||||
"DELETE FROM pending_relays WHERE relay_signer_id = ?1 OR relay_destination_user_id = ?1",
|
||||
"DELETE FROM pending_relays WHERE relay_signer_id = ?1 OR relay_destination_user_id = ?1 OR signer_principal = (SELECT principal_handle FROM hosted_principals WHERE local_user_id = ?1) OR destination_principal = (SELECT principal_handle FROM hosted_principals WHERE local_user_id = ?1)",
|
||||
params![user_id],
|
||||
)?;
|
||||
tx.execute(
|
||||
"DELETE FROM relay_replay WHERE EXISTS (SELECT 1 FROM relay_inbox WHERE relay_inbox.signer_id = relay_replay.signer_id AND relay_inbox.message_id = relay_replay.message_id AND (relay_inbox.signer_id = ?1 OR relay_inbox.destination_id = ?1))",
|
||||
"DELETE FROM relay_replay WHERE EXISTS (SELECT 1 FROM relay_inbox WHERE relay_inbox.signer_principal = relay_replay.signer_principal AND relay_inbox.message_id = relay_replay.message_id AND (relay_inbox.signer_principal = (SELECT principal_handle FROM hosted_principals WHERE local_user_id = ?1) OR relay_inbox.destination_id = ?1))",
|
||||
params![user_id],
|
||||
)?;
|
||||
tx.execute(
|
||||
"DELETE FROM relay_inbox WHERE signer_id = ?1 OR destination_id = ?1",
|
||||
"DELETE FROM relay_inbox WHERE signer_principal = (SELECT principal_handle FROM hosted_principals WHERE local_user_id = ?1) OR destination_id = ?1",
|
||||
params![user_id],
|
||||
)?;
|
||||
tx.commit()?;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)?,
|
||||
|
|
|
|||
|
|
@ -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)?,
|
||||
})
|
||||
},
|
||||
)?))
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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(())
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue