[Add] Replyjumps
This commit is contained in:
parent
4caa6bb3e9
commit
29d46d1c95
12 changed files with 650 additions and 240 deletions
|
|
@ -8,6 +8,8 @@ pub enum StorageError {
|
|||
Pool(String),
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("message has reached the unique reaction limit")]
|
||||
ReactionLimitReached,
|
||||
#[error("{0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ use crate::storage_error::StorageError;
|
|||
use crate::util::db;
|
||||
use crate::util::sync::{self, EntityType, Operation};
|
||||
use iota_logger::log;
|
||||
use rusqlite::params;
|
||||
use rusqlite::{OptionalExtension, params};
|
||||
|
||||
pub const MAX_UNIQUE_REACTIONS_PER_MESSAGE: usize = 10;
|
||||
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
pub enum MessageState {
|
||||
|
|
@ -48,7 +50,16 @@ impl MessageState {
|
|||
pub struct StoredMessage {
|
||||
pub id: i64,
|
||||
pub external_user: i64,
|
||||
pub relay_signer_id: Option<i64>,
|
||||
pub relay_message_id: Option<String>,
|
||||
pub message_time: i64,
|
||||
pub authored_at: Option<i64>,
|
||||
pub origin_iota_received_at: Option<i64>,
|
||||
pub destination_iota_received_at: Option<i64>,
|
||||
pub client_received_at: Option<i64>,
|
||||
pub client_received_recorded_at: Option<i64>,
|
||||
pub read_at: Option<i64>,
|
||||
pub read_recorded_at: Option<i64>,
|
||||
pub content: String,
|
||||
pub edited: bool,
|
||||
pub sent_by_self: bool,
|
||||
|
|
@ -58,6 +69,22 @@ pub struct StoredMessage {
|
|||
pub reactions: Vec<StoredReaction>,
|
||||
}
|
||||
|
||||
pub struct NewMessage<'a> {
|
||||
pub relay_signer_id: i64,
|
||||
pub relay_message_id: &'a str,
|
||||
pub authored_at: i64,
|
||||
pub send_time: i64,
|
||||
pub storage_owner: i64,
|
||||
pub external_user: i64,
|
||||
pub sent_by_self: bool,
|
||||
pub content: &'a str,
|
||||
pub height: i64,
|
||||
pub reply_to: Option<i64>,
|
||||
pub origin_iota_received_at: Option<i64>,
|
||||
pub destination_iota_received_at: Option<i64>,
|
||||
pub initial_state: MessageState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct StoredReaction {
|
||||
pub reaction: String,
|
||||
|
|
@ -149,7 +176,7 @@ fn update_message_content(
|
|||
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.unwrap_or_default()
|
||||
.as_millis() as i64;
|
||||
|
||||
let tx = conn.unchecked_transaction()?;
|
||||
|
|
@ -351,8 +378,8 @@ pub fn add_reaction(
|
|||
user_id: i64,
|
||||
reaction: &str,
|
||||
) -> Result<(), StorageError> {
|
||||
db::with_db(|conn| {
|
||||
let msg_id: i64 = conn.query_row(
|
||||
db::with_immediate_transaction(|tx| {
|
||||
let msg_id: i64 = tx.query_row(
|
||||
r#"
|
||||
SELECT id FROM messages
|
||||
WHERE storage_owner = ?1 AND external_user = ?2 AND message_time = ?3
|
||||
|
|
@ -362,28 +389,46 @@ pub fn add_reaction(
|
|||
|row| row.get(0),
|
||||
)?;
|
||||
|
||||
let reaction_exists: bool = tx.query_row(
|
||||
"SELECT EXISTS(SELECT 1 FROM reactions WHERE message_id = ?1 AND reaction = ?2)",
|
||||
params![msg_id, reaction],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
if !reaction_exists {
|
||||
let unique_reactions: i64 = tx.query_row(
|
||||
"SELECT COUNT(DISTINCT reaction) FROM reactions WHERE message_id = ?1",
|
||||
[msg_id],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
if unique_reactions >= MAX_UNIQUE_REACTIONS_PER_MESSAGE as i64 {
|
||||
return Err(StorageError::ReactionLimitReached);
|
||||
}
|
||||
}
|
||||
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.unwrap_or_default()
|
||||
.as_millis() as i64;
|
||||
|
||||
let tx = conn.unchecked_transaction()?;
|
||||
tx.execute(
|
||||
let inserted = tx.execute(
|
||||
r#"
|
||||
INSERT OR IGNORE INTO reactions (message_id, user_id, reaction, created_at)
|
||||
VALUES (?1, ?2, ?3, ?4)
|
||||
"#,
|
||||
params![msg_id, user_id, reaction, now],
|
||||
)?;
|
||||
sync::record_event(
|
||||
&tx,
|
||||
storage_owner,
|
||||
EntityType::Message,
|
||||
msg_id,
|
||||
Operation::Upsert,
|
||||
)?;
|
||||
tx.commit()?;
|
||||
Ok(())
|
||||
if inserted > 0 {
|
||||
sync::record_event(
|
||||
tx,
|
||||
storage_owner,
|
||||
EntityType::Message,
|
||||
msg_id,
|
||||
Operation::Upsert,
|
||||
)?;
|
||||
Ok(())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -422,45 +467,46 @@ pub fn remove_reaction(
|
|||
})
|
||||
}
|
||||
|
||||
pub fn add_message(
|
||||
send_time: u128,
|
||||
storage_owner_is_sender: bool,
|
||||
storage_owner: i64,
|
||||
external_user: i64,
|
||||
message: &str,
|
||||
height: i64,
|
||||
reply_to: Option<i64>,
|
||||
) {
|
||||
let message_time = match i64::try_from(send_time) {
|
||||
Ok(v) => v,
|
||||
Err(_) => {
|
||||
log!("Failed to store message: send_time out of range for i64 ({send_time})");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = db::with_db(|conn| {
|
||||
pub fn add_message(message: NewMessage<'_>) -> Result<i64, StorageError> {
|
||||
let NewMessage {
|
||||
relay_signer_id,
|
||||
relay_message_id,
|
||||
authored_at,
|
||||
send_time,
|
||||
storage_owner,
|
||||
external_user,
|
||||
sent_by_self,
|
||||
content,
|
||||
height,
|
||||
reply_to,
|
||||
origin_iota_received_at,
|
||||
destination_iota_received_at,
|
||||
initial_state,
|
||||
} = message;
|
||||
let msg_id = db::with_db(|conn| {
|
||||
let tx = conn.unchecked_transaction()?;
|
||||
tx.execute(
|
||||
r#"
|
||||
INSERT INTO messages (
|
||||
storage_owner, external_user, message_time, content,
|
||||
sent_by_self, message_state, height, reply_to
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
|
||||
storage_owner, external_user, message_time, content, sent_by_self,
|
||||
message_state, height, reply_to, relay_signer_id, relay_message_id,
|
||||
authored_at, origin_iota_received_at, destination_iota_received_at
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)
|
||||
"#,
|
||||
params![
|
||||
storage_owner,
|
||||
external_user,
|
||||
message_time,
|
||||
message,
|
||||
if storage_owner_is_sender {
|
||||
1_i64
|
||||
} else {
|
||||
0_i64
|
||||
},
|
||||
MessageState::Sending.as_str(),
|
||||
send_time,
|
||||
content,
|
||||
i64::from(sent_by_self),
|
||||
initial_state.as_str(),
|
||||
height,
|
||||
reply_to,
|
||||
relay_signer_id,
|
||||
relay_message_id,
|
||||
authored_at,
|
||||
origin_iota_received_at,
|
||||
destination_iota_received_at,
|
||||
],
|
||||
)?;
|
||||
let msg_id = tx.last_insert_rowid();
|
||||
|
|
@ -472,15 +518,122 @@ pub fn add_message(
|
|||
Operation::Upsert,
|
||||
)?;
|
||||
tx.commit()?;
|
||||
Ok(())
|
||||
}) {
|
||||
log!("Failed to insert message into sqlite: {}", e);
|
||||
return;
|
||||
}
|
||||
Ok(msg_id)
|
||||
})?;
|
||||
|
||||
let mut contact = crate::users::contact::Contact::new(external_user);
|
||||
contact.set_last_message_at(message_time);
|
||||
contact.set_last_message_at(send_time);
|
||||
crate::util::chats_util::mod_user(storage_owner, &contact);
|
||||
Ok(msg_id)
|
||||
}
|
||||
|
||||
pub fn change_message_state_by_relay_id(
|
||||
storage_owner: i64,
|
||||
relay_signer_id: i64,
|
||||
relay_message_id: &str,
|
||||
new_state: MessageState,
|
||||
) -> Result<(), StorageError> {
|
||||
db::with_db(|conn| {
|
||||
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],
|
||||
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)),
|
||||
)
|
||||
.optional()?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
let state = MessageState::from_str(¤t).upgrade(new_state).as_str();
|
||||
tx.execute("UPDATE messages SET message_state = ?1 WHERE id = ?2", params![state, msg_id])?;
|
||||
sync::record_event(&tx, storage_owner, EntityType::Message, msg_id, Operation::Upsert)?;
|
||||
tx.commit()?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn record_message_receipt(
|
||||
storage_owner: i64,
|
||||
target_signer_id: i64,
|
||||
target_message_id: &str,
|
||||
receipt_signer_id: i64,
|
||||
receipt_message_id: &str,
|
||||
receipt_type: MessageState,
|
||||
event_at: i64,
|
||||
recorded_at: i64,
|
||||
) -> Result<(), StorageError> {
|
||||
let receipt_type = match receipt_type {
|
||||
MessageState::Received => "received",
|
||||
MessageState::Read => "read",
|
||||
_ => return Err(StorageError::Other("invalid message receipt state".into())),
|
||||
};
|
||||
db::with_db(|conn| {
|
||||
let tx = conn.unchecked_transaction()?;
|
||||
let Some((message_id, external_user)) = tx
|
||||
.query_row(
|
||||
"SELECT id, external_user 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],
|
||||
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)),
|
||||
)
|
||||
.optional()?
|
||||
else {
|
||||
return Err(StorageError::Other("message receipt target was not found".into()));
|
||||
};
|
||||
if external_user != receipt_signer_id {
|
||||
return Err(StorageError::Other("message receipt signer is not the chat partner".into()));
|
||||
}
|
||||
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],
|
||||
)?;
|
||||
let (state_column, recorded_column) = if receipt_type == "read" {
|
||||
("read_at", "read_recorded_at")
|
||||
} else {
|
||||
("client_received_at", "client_received_recorded_at")
|
||||
};
|
||||
let state = MessageState::from_str(
|
||||
&tx.query_row("SELECT message_state FROM messages WHERE id = ?1", [message_id], |row| row.get::<_, String>(0))?,
|
||||
)
|
||||
.upgrade(MessageState::from_str(receipt_type))
|
||||
.as_str()
|
||||
.to_string();
|
||||
tx.execute(
|
||||
&format!("UPDATE messages SET {state_column} = COALESCE({state_column}, ?1), {recorded_column} = COALESCE({recorded_column}, ?2), message_state = ?3 WHERE id = ?4"),
|
||||
params![event_at, recorded_at, state, message_id],
|
||||
)?;
|
||||
sync::record_event(&tx, storage_owner, EntityType::Message, message_id, Operation::Upsert)?;
|
||||
tx.commit()?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn record_destination_iota_received(
|
||||
storage_owner: i64,
|
||||
relay_signer_id: i64,
|
||||
relay_message_id: &str,
|
||||
accepted_at: i64,
|
||||
) -> Result<(), StorageError> {
|
||||
db::with_db(|conn| {
|
||||
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],
|
||||
|row| row.get::<_, i64>(0),
|
||||
)
|
||||
.optional()?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
tx.execute(
|
||||
"UPDATE messages SET destination_iota_received_at = COALESCE(destination_iota_received_at, ?1), message_state = CASE WHEN message_state = 'sending' THEN 'sent' ELSE message_state END WHERE id = ?2",
|
||||
params![accepted_at, message_id],
|
||||
)?;
|
||||
sync::record_event(&tx, storage_owner, EntityType::Message, message_id, Operation::Upsert)?;
|
||||
tx.commit()?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn change_message_state(
|
||||
|
|
@ -574,7 +727,15 @@ fn load_reactions(
|
|||
))
|
||||
}) {
|
||||
for row in rows.flatten() {
|
||||
map.entry(row.0).or_default().push(row.1);
|
||||
let reactions = map.entry(row.0).or_default();
|
||||
if reactions
|
||||
.iter()
|
||||
.any(|stored: &StoredReaction| stored.reaction == row.1.reaction)
|
||||
{
|
||||
reactions.push(row.1);
|
||||
} else if reactions.len() < MAX_UNIQUE_REACTIONS_PER_MESSAGE {
|
||||
reactions.push(row.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -594,7 +755,11 @@ pub fn get_messages(
|
|||
match db::with_db(|conn| {
|
||||
let mut stmt = conn.prepare(
|
||||
r#"
|
||||
SELECT id, message_time, content, sent_by_self, message_state, height, reply_to, edited_count
|
||||
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, content, sent_by_self, message_state, height,
|
||||
reply_to, edited_count
|
||||
FROM messages
|
||||
WHERE storage_owner = ?1 AND external_user = ?2 AND deleted_by_external = 0
|
||||
ORDER BY message_time DESC, id DESC
|
||||
|
|
@ -608,13 +773,22 @@ pub fn get_messages(
|
|||
Ok(StoredMessage {
|
||||
id: row.get(0)?,
|
||||
external_user,
|
||||
message_time: row.get(1)?,
|
||||
content: row.get(2)?,
|
||||
sent_by_self: row.get::<_, i64>(3)? != 0,
|
||||
message_state: row.get(4)?,
|
||||
height: row.get(5).unwrap_or(0),
|
||||
reply_to: row.get(6).ok().flatten(),
|
||||
edited: row.get::<_, i64>(7).unwrap_or(0) > 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)?,
|
||||
content: row.get(11)?,
|
||||
sent_by_self: row.get::<_, i64>(12)? != 0,
|
||||
message_state: row.get(13)?,
|
||||
height: row.get(14).unwrap_or(0),
|
||||
reply_to: row.get(15).ok().flatten(),
|
||||
edited: row.get::<_, i64>(16).unwrap_or(0) > 0,
|
||||
reactions: Vec::new(),
|
||||
})
|
||||
},
|
||||
|
|
@ -652,7 +826,10 @@ pub fn get_message(
|
|||
db::with_db(|conn| {
|
||||
let mut stmt = conn.prepare(
|
||||
r#"
|
||||
SELECT id, message_time, content, sent_by_self, message_state, height,
|
||||
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, content, sent_by_self, message_state, height,
|
||||
reply_to, edited_count, external_user
|
||||
FROM messages
|
||||
WHERE storage_owner = ?1
|
||||
|
|
@ -666,14 +843,23 @@ pub fn get_message(
|
|||
let rows = stmt.query_map(params![storage_owner, message_time, external_user], |row| {
|
||||
Ok(StoredMessage {
|
||||
id: row.get(0)?,
|
||||
message_time: row.get(1)?,
|
||||
content: row.get(2)?,
|
||||
sent_by_self: row.get::<_, i64>(3)? != 0,
|
||||
message_state: row.get(4)?,
|
||||
height: row.get(5).unwrap_or(0),
|
||||
reply_to: row.get(6).ok().flatten(),
|
||||
edited: row.get::<_, i64>(7).unwrap_or(0) > 0,
|
||||
external_user: row.get(8)?,
|
||||
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)?,
|
||||
content: row.get(11)?,
|
||||
sent_by_self: row.get::<_, i64>(12)? != 0,
|
||||
message_state: row.get(13)?,
|
||||
height: row.get(14).unwrap_or(0),
|
||||
reply_to: row.get(15).ok().flatten(),
|
||||
edited: row.get::<_, i64>(16).unwrap_or(0) > 0,
|
||||
external_user: row.get(17)?,
|
||||
reactions: Vec::new(),
|
||||
})
|
||||
})?;
|
||||
|
|
@ -700,6 +886,40 @@ pub fn get_message(
|
|||
})
|
||||
}
|
||||
|
||||
pub fn get_message_with_offset(
|
||||
storage_owner: i64,
|
||||
external_user: i64,
|
||||
message_time: i64,
|
||||
) -> Result<Option<(StoredMessage, i64)>, StorageError> {
|
||||
let Some(message) = get_message(storage_owner, message_time, Some(external_user))? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let offset = db::with_db(|conn| {
|
||||
conn.query_row(
|
||||
r#"
|
||||
SELECT COUNT(*)
|
||||
FROM messages
|
||||
WHERE storage_owner = ?1
|
||||
AND external_user = ?2
|
||||
AND deleted_by_external = 0
|
||||
AND (
|
||||
message_time > ?3
|
||||
OR (message_time = ?3 AND id > ?4)
|
||||
)
|
||||
"#,
|
||||
params![
|
||||
storage_owner,
|
||||
external_user,
|
||||
message.message_time,
|
||||
message.id
|
||||
],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.map_err(StorageError::from)
|
||||
})?;
|
||||
Ok(Some((message, offset)))
|
||||
}
|
||||
|
||||
pub fn get_messages_by_ids(storage_owner: i64, ids: &[i64]) -> Vec<StoredMessage> {
|
||||
if ids.is_empty() {
|
||||
return Vec::new();
|
||||
|
|
@ -708,19 +928,28 @@ pub fn get_messages_by_ids(storage_owner: i64, ids: &[i64]) -> Vec<StoredMessage
|
|||
// 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.
|
||||
match db::with_db(|conn| {
|
||||
let mut stmt = conn.prepare("SELECT id, message_time, content, sent_by_self, message_state, height, reply_to, edited_count, external_user FROM messages WHERE storage_owner = ?1 AND deleted_by_external = 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, content, sent_by_self, message_state, height, reply_to, edited_count, external_user FROM messages WHERE storage_owner = ?1 AND deleted_by_external = 0")?;
|
||||
let rows = stmt.query_map([storage_owner], |row| {
|
||||
let external_user: i64 = row.get(8)?;
|
||||
let external_user: i64 = row.get(17)?;
|
||||
Ok(StoredMessage {
|
||||
id: row.get(0)?,
|
||||
external_user,
|
||||
message_time: row.get(1)?,
|
||||
content: row.get(2)?,
|
||||
sent_by_self: row.get::<_, i64>(3)? != 0,
|
||||
message_state: row.get(4)?,
|
||||
height: row.get(5).unwrap_or(0),
|
||||
reply_to: row.get(6).ok().flatten(),
|
||||
edited: row.get::<_, i64>(7).unwrap_or(0) > 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)?,
|
||||
content: row.get(11)?,
|
||||
sent_by_self: row.get::<_, i64>(12)? != 0,
|
||||
message_state: row.get(13)?,
|
||||
height: row.get(14).unwrap_or(0),
|
||||
reply_to: row.get(15).ok().flatten(),
|
||||
edited: row.get::<_, i64>(16).unwrap_or(0) > 0,
|
||||
reactions: Vec::new(),
|
||||
})
|
||||
})?;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use once_cell::sync::Lazy;
|
||||
use r2d2::ManageConnection;
|
||||
use rusqlite::Connection;
|
||||
use rusqlite::{Connection, Transaction};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
|
@ -55,6 +55,17 @@ where
|
|||
f(&conn)
|
||||
}
|
||||
|
||||
pub fn with_immediate_transaction<T, F>(f: F) -> Result<T, StorageError>
|
||||
where
|
||||
F: FnOnce(&Transaction<'_>) -> Result<T, StorageError>,
|
||||
{
|
||||
let mut conn = POOL.get().map_err(|e| StorageError::Pool(e.to_string()))?;
|
||||
let tx = conn.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
|
||||
let value = f(&tx)?;
|
||||
tx.commit()?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
/* Verify the persistent database before the pool is initialized. A corrupt
|
||||
* database is moved aside rather than opened again, preserving material for
|
||||
* operator recovery while allowing the daemon to report the failed storage. */
|
||||
|
|
@ -388,6 +399,71 @@ fn run_migrations_on_connection(conn: &Connection) -> Result<(), StorageError> {
|
|||
)?;
|
||||
}
|
||||
|
||||
if current_version < 10 {
|
||||
conn.execute_batch(
|
||||
r#"
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_history
|
||||
ON messages (
|
||||
storage_owner,
|
||||
external_user,
|
||||
deleted_by_external,
|
||||
message_time DESC,
|
||||
id DESC
|
||||
);
|
||||
PRAGMA user_version = 10;
|
||||
"#,
|
||||
)?;
|
||||
}
|
||||
|
||||
if current_version < 11 {
|
||||
for (column, definition) in [
|
||||
("relay_signer_id", "relay_signer_id INTEGER"),
|
||||
("relay_message_id", "relay_message_id TEXT"),
|
||||
("authored_at", "authored_at INTEGER"),
|
||||
("origin_iota_received_at", "origin_iota_received_at INTEGER"),
|
||||
("destination_iota_received_at", "destination_iota_received_at INTEGER"),
|
||||
("client_received_at", "client_received_at INTEGER"),
|
||||
("client_received_recorded_at", "client_received_recorded_at INTEGER"),
|
||||
("read_at", "read_at INTEGER"),
|
||||
("read_recorded_at", "read_recorded_at INTEGER"),
|
||||
] {
|
||||
add_column_if_missing(conn, column, definition)?;
|
||||
}
|
||||
for (column, definition) in [
|
||||
("accepted_at", "accepted_at INTEGER"),
|
||||
("applied_at", "applied_at INTEGER"),
|
||||
("queued_at", "queued_at INTEGER"),
|
||||
("downstream_acked_at", "downstream_acked_at INTEGER"),
|
||||
("rejected_at", "rejected_at INTEGER"),
|
||||
] {
|
||||
add_table_column_if_missing(conn, "relay_inbox", column, definition)?;
|
||||
}
|
||||
conn.execute_batch(
|
||||
r#"
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_messages_relay_identity
|
||||
ON messages (storage_owner, relay_signer_id, relay_message_id)
|
||||
WHERE relay_message_id IS NOT NULL;
|
||||
CREATE TABLE IF NOT EXISTS message_receipts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
storage_owner INTEGER NOT NULL,
|
||||
target_signer_id INTEGER NOT NULL,
|
||||
target_message_id TEXT NOT NULL,
|
||||
receipt_signer_id INTEGER NOT NULL,
|
||||
receipt_message_id TEXT NOT NULL,
|
||||
receipt_type TEXT NOT NULL CHECK (receipt_type IN ('received', 'read')),
|
||||
event_at INTEGER NOT NULL,
|
||||
recorded_at INTEGER NOT NULL,
|
||||
UNIQUE(receipt_signer_id, receipt_message_id),
|
||||
UNIQUE(storage_owner, target_signer_id, target_message_id,
|
||||
receipt_signer_id, receipt_type)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_message_receipts_target
|
||||
ON message_receipts (storage_owner, target_signer_id, target_message_id);
|
||||
PRAGMA user_version = 11;
|
||||
"#,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -457,7 +533,7 @@ mod tests {
|
|||
run_migrations_on_connection(&conn)?;
|
||||
|
||||
let version: i64 = conn.pragma_query_value(None, "user_version", |row| row.get(0))?;
|
||||
assert_eq!(version, 9);
|
||||
assert_eq!(version, 11);
|
||||
for column in ["height", "reply_to", "edited_count", "deleted_by_external"] {
|
||||
let mut statement =
|
||||
conn.prepare("SELECT 1 FROM pragma_table_info('messages') WHERE name = ?1")?;
|
||||
|
|
@ -470,11 +546,13 @@ mod tests {
|
|||
#[test]
|
||||
fn migrates_version_five_once_and_is_idempotent() -> Result<(), StorageError> {
|
||||
let conn = Connection::open_in_memory()?;
|
||||
conn.execute_batch("PRAGMA user_version = 5;")?;
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE messages (id INTEGER PRIMARY KEY, storage_owner INTEGER NOT NULL, external_user INTEGER NOT NULL, message_time INTEGER NOT NULL, content TEXT NOT NULL, sent_by_self INTEGER NOT NULL, message_state TEXT NOT NULL, height INTEGER NOT NULL DEFAULT 0, reply_to INTEGER, edited_count INTEGER NOT NULL DEFAULT 0, deleted_by_external INTEGER NOT NULL DEFAULT 0); PRAGMA user_version = 5;",
|
||||
)?;
|
||||
run_migrations_on_connection(&conn)?;
|
||||
run_migrations_on_connection(&conn)?;
|
||||
let version: i64 = conn.pragma_query_value(None, "user_version", |row| row.get(0))?;
|
||||
assert_eq!(version, 9);
|
||||
assert_eq!(version, 11);
|
||||
for table in [
|
||||
"sync_heads",
|
||||
"sync_events",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ pub fn reserve(
|
|||
signer_id: u64,
|
||||
message_id: &str,
|
||||
created_at: u64,
|
||||
accepted_at: i64,
|
||||
destination_id: u64,
|
||||
frame: &[u8],
|
||||
frame_id: u32,
|
||||
|
|
@ -26,11 +27,12 @@ pub fn reserve(
|
|||
|
||||
db::with_db(|connection| {
|
||||
let inserted = connection.execute(
|
||||
"INSERT OR IGNORE INTO relay_inbox (signer_id, message_id, created_at, destination_id, frame, frame_id, type_map_version, state) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, 'received')",
|
||||
"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')",
|
||||
params![
|
||||
signer_id,
|
||||
message_id,
|
||||
created_at,
|
||||
accepted_at,
|
||||
destination_id,
|
||||
frame,
|
||||
i64::from(frame_id),
|
||||
|
|
@ -67,9 +69,13 @@ pub fn mark_delivered_for_frame(destination_id: u64, frame_id: u32) -> Result<()
|
|||
let destination_id = i64::try_from(destination_id)
|
||||
.map_err(|_| StorageError::Other("relay destination ID exceeds SQLite range".into()))?;
|
||||
db::with_db(|connection| {
|
||||
let delivered_at = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as i64;
|
||||
connection.execute(
|
||||
"UPDATE relay_inbox SET state = 'delivered' WHERE destination_id = ?1 AND frame_id = ?2",
|
||||
params![destination_id, i64::from(frame_id)],
|
||||
"UPDATE relay_inbox SET state = 'delivered', downstream_acked_at = COALESCE(downstream_acked_at, ?3) WHERE destination_id = ?1 AND frame_id = ?2",
|
||||
params![destination_id, i64::from(frame_id), delivered_at],
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
|
|
@ -84,24 +90,36 @@ pub fn mark_state(signer_id: u64, message_id: &str, state: &str) -> Result<(), S
|
|||
}
|
||||
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| {
|
||||
let column = match state {
|
||||
"applied" => "applied_at",
|
||||
"queued" => "queued_at",
|
||||
"delivered" => "downstream_acked_at",
|
||||
"rejected" => "rejected_at",
|
||||
"received" => "accepted_at",
|
||||
_ => return Err(StorageError::Other("invalid relay inbox state".into())),
|
||||
};
|
||||
connection.execute(
|
||||
"UPDATE relay_inbox SET state = ?3 WHERE signer_id = ?1 AND message_id = ?2",
|
||||
params![signer_id, message_id, state],
|
||||
&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],
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn prune_completed(before_created_at: i64) -> Result<(), StorageError> {
|
||||
pub fn prune_completed(before_terminal_at: i64) -> Result<(), StorageError> {
|
||||
db::with_db(|connection| {
|
||||
connection.execute(
|
||||
"DELETE FROM relay_inbox WHERE created_at < ?1 AND state IN ('delivered', 'rejected')",
|
||||
params![before_created_at],
|
||||
"DELETE FROM relay_inbox WHERE COALESCE(downstream_acked_at, rejected_at) < ?1 AND state IN ('delivered', 'rejected')",
|
||||
params![before_terminal_at],
|
||||
)?;
|
||||
connection.execute(
|
||||
"DELETE FROM relay_replay WHERE created_at < ?1",
|
||||
params![before_created_at],
|
||||
"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)",
|
||||
[],
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue