move sqlite queries
Some checks failed
Flutter analyze & test / flutter_analyze_and_test (push) Has been cancelled

This commit is contained in:
otsmr 2026-08-28 23:22:21 +02:00
parent 218985a3ff
commit 3498040aeb
10 changed files with 232 additions and 159 deletions

View file

@ -26,12 +26,12 @@ static ALREADY_QUEUED_RECEIPTS: LazyLock<std::sync::Mutex<HashMap<String, std::t
LazyLock::new(|| std::sync::Mutex::new(HashMap::new())); LazyLock::new(|| std::sync::Mutex::new(HashMap::new()));
pub(crate) async fn queue_encrypted_content( pub(crate) async fn queue_encrypted_content(
transaction: &mut Transaction<'_, Sqlite>, t: &mut Transaction<'_, Sqlite>,
target_user_id: i64, target_user_id: i64,
content: proto::EncryptedContent, content: proto::EncryptedContent,
contact_will_send_receipt: bool, contact_will_send_receipt: bool,
) -> Result<String> { ) -> Result<String> {
let Ok(Some(contact)) = Contact::get_contact_by_id(transaction, target_user_id).await else { let Some(contact) = Contact::get_contact_by_id(t, target_user_id).await? else {
return Err(twonly_error!("missing contact")); return Err(twonly_error!("missing contact"));
}; };
@ -48,9 +48,10 @@ pub(crate) async fn queue_encrypted_content(
.encode_to_vec(); .encode_to_vec();
let receipt_id = new_uuid_v4(); let receipt_id = new_uuid_v4();
NewReceipt::new(&receipt_id, target_user_id, &message) NewReceipt::new(&receipt_id, target_user_id, &message)
.contact_will_send_receipt(contact_will_send_receipt) .contact_will_send_receipt(contact_will_send_receipt)
.insert(transaction) .insert(t)
.await?; .await?;
Ok(receipt_id) Ok(receipt_id)
@ -58,21 +59,22 @@ pub(crate) async fn queue_encrypted_content(
pub(crate) async fn process_encrypted_or_queue_error( pub(crate) async fn process_encrypted_or_queue_error(
ctx: &Arc<Context>, ctx: &Arc<Context>,
tr: &mut Transaction<'_, Sqlite>, t: &mut Transaction<'_, Sqlite>,
from_user_id: i64, from_user_id: i64,
receipt_id: &str, receipt_id: &str,
content: proto::EncryptedContent, content: proto::EncryptedContent,
) -> Result<Option<String>> { ) -> Result<Option<String>> {
let group_id = content.group_id.clone(); let group_id = content.group_id.clone();
match handle_encrypted(ctx, tr, from_user_id, receipt_id, content).await { match handle_encrypted(ctx, t, from_user_id, receipt_id, content).await {
Ok(()) => Ok(None), Ok(()) => Ok(None),
Err(error) => { Err(error) => {
let description = error.to_string(); let description = error.to_string();
if description.contains("group join arrived before") { if description.contains("group join arrived before") {
queue_retry_control(tr, from_user_id, receipt_id).await?; queue_retry_control(t, from_user_id, receipt_id).await?;
return Ok(Some(receipt_id.to_owned())); return Ok(Some(receipt_id.to_owned()));
} }
let error_type = if description.contains("not a member") { let error_type = if description.contains("not a member") {
Some(Type::GroupNotFoundOrNotAMember) Some(Type::GroupNotFoundOrNotAMember)
} else if description.contains("not implemented in Rust") { } else if description.contains("not implemented in Rust") {
@ -80,9 +82,11 @@ pub(crate) async fn process_encrypted_or_queue_error(
} else { } else {
None None
}; };
let Some(error_type) = error_type else { let Some(error_type) = error_type else {
return Err(error); return Err(error);
}; };
let outgoing_receipt_id = new_uuid_v4(); let outgoing_receipt_id = new_uuid_v4();
let response_content = proto::EncryptedContent { let response_content = proto::EncryptedContent {
group_id, group_id,
@ -92,6 +96,7 @@ pub(crate) async fn process_encrypted_or_queue_error(
}), }),
..Default::default() ..Default::default()
}; };
let response = proto::Message { let response = proto::Message {
r#type: proto::message::Type::Ciphertext as i32, r#type: proto::message::Type::Ciphertext as i32,
receipt_id: String::new(), receipt_id: String::new(),
@ -99,6 +104,7 @@ pub(crate) async fn process_encrypted_or_queue_error(
plaintext_content: None, plaintext_content: None,
} }
.encode_to_vec(); .encode_to_vec();
sqlx::query!( sqlx::query!(
r#" r#"
INSERT INTO receipts(receipt_id, contact_id, message, contact_will_sends_receipt) INSERT INTO receipts(receipt_id, contact_id, message, contact_will_sends_receipt)
@ -108,15 +114,16 @@ pub(crate) async fn process_encrypted_or_queue_error(
from_user_id, from_user_id,
response, response,
) )
.execute(&mut **tr) .execute(&mut **t)
.await?; .await?;
Ok(Some(outgoing_receipt_id)) Ok(Some(outgoing_receipt_id))
} }
} }
} }
async fn queue_retry_control( async fn queue_retry_control(
transaction: &mut Transaction<'_, Sqlite>, t: &mut Transaction<'_, Sqlite>,
from_user_id: i64, from_user_id: i64,
receipt_id: &str, receipt_id: &str,
) -> Result<()> { ) -> Result<()> {
@ -130,6 +137,7 @@ async fn queue_retry_control(
}), }),
} }
.encode_to_vec(); .encode_to_vec();
sqlx::query!( sqlx::query!(
r#" r#"
INSERT OR REPLACE INTO receipts(receipt_id, contact_id, message, contact_will_sends_receipt) INSERT OR REPLACE INTO receipts(receipt_id, contact_id, message, contact_will_sends_receipt)
@ -139,8 +147,9 @@ async fn queue_retry_control(
from_user_id, from_user_id,
response, response,
) )
.execute(&mut **transaction) .execute(&mut **t)
.await?; .await?;
Ok(()) Ok(())
} }
@ -175,6 +184,7 @@ pub(crate) async fn ensure_contact_exists(ctx: &Arc<Context>, from_user_id: i64)
.map(String::from_utf8) .map(String::from_utf8)
.transpose()? .transpose()?
.unwrap_or_else(|| "[Unknown]".into()); .unwrap_or_else(|| "[Unknown]".into());
let signal_version = if user.pqc_bundle.is_some() { let signal_version = if user.pqc_bundle.is_some() {
"v2" "v2"
} else { } else {
@ -215,7 +225,7 @@ pub(crate) async fn ensure_contact_exists(ctx: &Arc<Context>, from_user_id: i64)
} }
pub(crate) async fn queue_sender_delivery_receipt( pub(crate) async fn queue_sender_delivery_receipt(
transaction: &mut Transaction<'_, Sqlite>, t: &mut Transaction<'_, Sqlite>,
from_user_id: i64, from_user_id: i64,
receipt_id: &str, receipt_id: &str,
) -> Result<()> { ) -> Result<()> {
@ -226,6 +236,7 @@ pub(crate) async fn queue_sender_delivery_receipt(
plaintext_content: None, plaintext_content: None,
} }
.encode_to_vec(); .encode_to_vec();
sqlx::query!( sqlx::query!(
r#" r#"
INSERT OR IGNORE INTO receipts( INSERT OR IGNORE INTO receipts(
@ -236,8 +247,9 @@ pub(crate) async fn queue_sender_delivery_receipt(
from_user_id, from_user_id,
response, response,
) )
.execute(&mut **transaction) .execute(&mut **t)
.await?; .await?;
Ok(()) Ok(())
} }
@ -275,9 +287,11 @@ async fn encrypt_v2_with_session_recovery(
contact_id, contact_id,
"Signal session missing; rebuilding it from the server prekey bundle" "Signal session missing; rebuilding it from the server prekey bundle"
); );
ContactService::new(ctx) ContactService::new(ctx)
.establish_signal_session(contact_id) .establish_signal_session(contact_id)
.await?; .await?;
encrypt(plaintext).await encrypt(plaintext).await
} }
result => result, result => result,

View file

@ -68,7 +68,7 @@ pub(crate) async fn handle_encrypted(
} }
if let Some(update) = content.message_update { if let Some(update) = content.message_update {
return text_message::handle_message_update(t, from_user_id, update).await; return text_message::handle_message_update(ctx, t, from_user_id, update).await;
} }
if let Some(update) = content.media_update { if let Some(update) = content.media_update {

View file

@ -63,7 +63,7 @@ pub(crate) async fn handle_passwordless_recovery(
} }
pub(crate) async fn perform_heartbeat(ctx: &Arc<Context>) -> Result<()> { pub(crate) async fn perform_heartbeat(ctx: &Arc<Context>) -> Result<()> {
let now = chrono::Utc::now(); let now = crate::utils::current_time().with_timezone(&chrono::Utc);
let base_config = UserConfig::load_required_from(ctx)?; let base_config = UserConfig::load_required_from(ctx)?;
let mut config = base_config.clone(); let mut config = base_config.clone();
@ -164,11 +164,15 @@ pub(crate) async fn perform_heartbeat(ctx: &Arc<Context>) -> Result<()> {
} }
if config != base_config { if config != base_config {
UserConfig::update_json( let updated_recovery = config.password_less_recovery.as_ref();
ctx, let last_server_heartbeat = updated_recovery.and_then(|r| r.last_server_heartbeat);
&serde_json::to_string(&base_config)?, let last_contact_heartbeat = updated_recovery.and_then(|r| r.last_contact_heartbeat);
&serde_json::to_string(&config)?, let config = UserConfig::update(ctx, |current| {
)?; if let Some(recovery) = current.password_less_recovery.as_mut() {
recovery.last_server_heartbeat = last_server_heartbeat;
recovery.last_contact_heartbeat = last_contact_heartbeat;
}
})?;
if let Ok(callbacks) = crate::bridge::callbacks::get_callbacks() { if let Ok(callbacks) = crate::bridge::callbacks::get_callbacks() {
(callbacks.api.user_config_changed)(config).await; (callbacks.api.user_config_changed)(config).await;
} }

View file

@ -4,9 +4,11 @@
*/ */
use crate::api::proto::client::encrypted_content; use crate::api::proto::client::encrypted_content;
use crate::context::Context;
use crate::database::app::tables::{Group, Message, MessageType, NewMessage}; use crate::database::app::tables::{Group, Message, MessageType, NewMessage};
use crate::error::Result; use crate::error::Result;
use crate::utils::milliseconds_to_seconds; use crate::services::mediafiles::MediaFileService;
use crate::utils::{current_time, milliseconds_to_seconds};
use encrypted_content::message_update::Type; use encrypted_content::message_update::Type;
use sqlx::{Sqlite, Transaction}; use sqlx::{Sqlite, Transaction};
@ -28,7 +30,7 @@ pub(crate) async fn handle_text_message(
.sender_id(from_user_id) .sender_id(from_user_id)
.content(&message.text) .content(&message.text)
.maybe_quotes_message_id(message.quote_message_id.as_deref()) .maybe_quotes_message_id(message.quote_message_id.as_deref())
.ack_by_server(chrono::Utc::now().timestamp()) .ack_by_server(current_time().timestamp())
.build() .build()
.insert(t) .insert(t)
.await?; .await?;
@ -39,6 +41,7 @@ pub(crate) async fn handle_text_message(
} }
pub(crate) async fn handle_message_update( pub(crate) async fn handle_message_update(
ctx: &std::sync::Arc<Context>,
t: &mut Transaction<'_, Sqlite>, t: &mut Transaction<'_, Sqlite>,
from_user_id: i64, from_user_id: i64,
update: encrypted_content::MessageUpdate, update: encrypted_content::MessageUpdate,
@ -51,146 +54,32 @@ pub(crate) async fn handle_message_update(
match update_type { match update_type {
Type::Opened => { Type::Opened => {
for message_id in update.multiple_target_message_ids { for message_id in update.multiple_target_message_ids {
let action_at = sqlx::query_scalar::<_, i64>( Message::record_opened(t, &message_id, from_user_id, timestamp).await?;
"SELECT MAX(created_at, ?) FROM messages WHERE message_id = ?",
)
.bind(timestamp)
.bind(&message_id)
.fetch_optional(&mut **t)
.await?;
let Some(action_at) = action_at else { continue };
sqlx::query!(
r#"
INSERT INTO message_actions(message_id, contact_id, type, action_at)
VALUES (?, ?, 'openedAt', ?)
ON CONFLICT(message_id, contact_id, type)
DO UPDATE SET action_at = excluded.action_at
"#,
message_id,
from_user_id,
action_at,
)
.execute(&mut **t)
.await?;
sqlx::query!(
r#"UPDATE messages SET opened_at = ?, opened_by_all = CASE WHEN NOT EXISTS(
SELECT 1 FROM group_members gm
WHERE gm.group_id = messages.group_id AND NOT EXISTS(
SELECT 1 FROM message_actions ma
WHERE ma.message_id = messages.message_id
AND ma.contact_id = gm.contact_id AND ma.type = 'openedAt'
)
) THEN ? ELSE NULL END
WHERE message_id = ?"#,
action_at,
action_at,
message_id,
)
.execute(&mut **t)
.await?;
} }
} }
Type::Delete => { Type::Delete => {
let media_id = sqlx::query_scalar!( let deleted_media = Message::delete_from_sender(
"SELECT media_id FROM messages WHERE message_id = ? AND sender_id = ?", t,
update.sender_message_id, update.sender_message_id.as_deref(),
from_user_id, from_user_id,
)
.fetch_optional(&mut **t)
.await?
.flatten();
sqlx::query!(
"DELETE FROM message_histories WHERE message_id = ?",
update.sender_message_id
)
.execute(&mut **t)
.await?;
sqlx::query!(
"DELETE FROM receipts WHERE message_id = ?",
update.sender_message_id
)
.execute(&mut **t)
.await?;
sqlx::query!(
r#"
UPDATE messages
SET is_deleted_from_sender = 1, content = NULL, media_id = NULL, modified_at = ?
WHERE message_id = ? AND sender_id = ?
"#,
timestamp, timestamp,
update.sender_message_id,
from_user_id,
) )
.execute(&mut **t)
.await?; .await?;
if let Some(media_id) = media_id { if let Some(media) = deleted_media {
let references = sqlx::query_scalar!( MediaFileService::new(ctx)
"SELECT COUNT(*) FROM messages WHERE media_id = ?", .remove_files_if_deleted(t, &media.media_id, &media.media_type)
media_id,
)
.fetch_one(&mut **t)
.await?; .await?;
if references == 0 {
let media_type = sqlx::query_scalar!(
"SELECT type FROM media_files WHERE media_id = ?",
media_id,
)
.fetch_optional(&mut **t)
.await?;
sqlx::query!("DELETE FROM media_files WHERE media_id = ?", media_id)
.execute(&mut **t)
.await?;
if let Some(media_type) = media_type {
let ctx = crate::context::Context::get_static()?;
let ctx = ctx.clone();
tokio::spawn(async move {
// Let the surrounding message transaction commit before
// applying its corresponding filesystem side effect.
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
if let Err(error) =
crate::services::mediafiles::MediaFileService::new(&ctx)
.remove_files_if_deleted(&media_id, &media_type)
.await
{
tracing::warn!(media_id, %error, "could not remove media files");
}
});
}
}
} }
} }
Type::EditText => { Type::EditText => {
sqlx::query!( Message::edit_text(
r#"INSERT INTO message_histories(message_id, content, created_at) t,
SELECT message_id, content, ? FROM messages update.sender_message_id.as_deref(),
WHERE message_id = ? AND sender_id = ? AND content IS NOT NULL"#,
timestamp,
update.sender_message_id,
from_user_id, from_user_id,
) update.text.as_deref(),
.execute(&mut **t)
.await?;
sqlx::query!(
r#"
UPDATE messages
SET content = ?, modified_at = ?
WHERE message_id = ? AND sender_id = ? AND content IS NOT NULL
"#,
update.text,
timestamp, timestamp,
update.sender_message_id,
from_user_id,
) )
.execute(&mut **t)
.await?; .await?;
} }
} }

View file

@ -9,6 +9,11 @@ use crate::error::{Result, TwonlyError};
pub struct Message; pub struct Message;
pub struct DeletedMessageMedia {
pub media_id: String,
pub media_type: String,
}
pub enum MessageType<'a> { pub enum MessageType<'a> {
Text, Text,
Media, Media,
@ -37,6 +42,142 @@ impl<'a> NewMessage<'a> {
} }
impl Message { impl Message {
pub async fn record_opened(
t: &mut Transaction<'_, Sqlite>,
message_id: &str,
contact_id: i64,
timestamp: i64,
) -> Result<()> {
let action_at = sqlx::query_scalar::<_, i64>(
"SELECT MAX(created_at, ?) FROM messages WHERE message_id = ?",
)
.bind(timestamp)
.bind(message_id)
.fetch_optional(&mut **t)
.await?;
let Some(action_at) = action_at else {
return Ok(());
};
sqlx::query!(
r#"INSERT INTO message_actions(message_id, contact_id, type, action_at)
VALUES (?, ?, 'openedAt', ?)
ON CONFLICT(message_id, contact_id, type)
DO UPDATE SET action_at = excluded.action_at"#,
message_id,
contact_id,
action_at,
)
.execute(&mut **t)
.await?;
sqlx::query!(
r#"UPDATE messages SET opened_at = ?, opened_by_all = CASE WHEN NOT EXISTS(
SELECT 1 FROM group_members gm
WHERE gm.group_id = messages.group_id AND NOT EXISTS(
SELECT 1 FROM message_actions ma
WHERE ma.message_id = messages.message_id
AND ma.contact_id = gm.contact_id AND ma.type = 'openedAt'
)
) THEN ? ELSE NULL END
WHERE message_id = ?"#,
action_at,
action_at,
message_id,
)
.execute(&mut **t)
.await?;
Ok(())
}
pub async fn delete_from_sender(
t: &mut Transaction<'_, Sqlite>,
message_id: Option<&str>,
sender_id: i64,
timestamp: i64,
) -> Result<Option<DeletedMessageMedia>> {
let media_id = sqlx::query_scalar!(
"SELECT media_id FROM messages WHERE message_id = ? AND sender_id = ?",
message_id,
sender_id,
)
.fetch_optional(&mut **t)
.await?
.flatten();
sqlx::query!(
"DELETE FROM message_histories WHERE message_id = ?",
message_id
)
.execute(&mut **t)
.await?;
sqlx::query!("DELETE FROM receipts WHERE message_id = ?", message_id)
.execute(&mut **t)
.await?;
sqlx::query!(
r#"UPDATE messages
SET is_deleted_from_sender = 1, content = NULL, media_id = NULL, modified_at = ?
WHERE message_id = ? AND sender_id = ?"#,
timestamp,
message_id,
sender_id,
)
.execute(&mut **t)
.await?;
let Some(media_id) = media_id else {
return Ok(None);
};
let references =
sqlx::query_scalar!("SELECT COUNT(*) FROM messages WHERE media_id = ?", media_id,)
.fetch_one(&mut **t)
.await?;
if references != 0 {
return Ok(None);
}
let media_type =
sqlx::query_scalar!("SELECT type FROM media_files WHERE media_id = ?", media_id,)
.fetch_optional(&mut **t)
.await?;
sqlx::query!("DELETE FROM media_files WHERE media_id = ?", media_id)
.execute(&mut **t)
.await?;
Ok(media_type.map(|media_type| DeletedMessageMedia {
media_id,
media_type,
}))
}
pub async fn edit_text(
t: &mut Transaction<'_, Sqlite>,
message_id: Option<&str>,
sender_id: i64,
text: Option<&str>,
timestamp: i64,
) -> Result<()> {
sqlx::query!(
r#"INSERT INTO message_histories(message_id, content, created_at)
SELECT message_id, content, ? FROM messages
WHERE message_id = ? AND sender_id = ? AND content IS NOT NULL"#,
timestamp,
message_id,
sender_id,
)
.execute(&mut **t)
.await?;
sqlx::query!(
r#"UPDATE messages SET content = ?, modified_at = ?
WHERE message_id = ? AND sender_id = ? AND content IS NOT NULL"#,
text,
timestamp,
message_id,
sender_id,
)
.execute(&mut **t)
.await?;
Ok(())
}
pub async fn insert(tr: &mut Transaction<'_, Sqlite>, message: NewMessage<'_>) -> Result<()> { pub async fn insert(tr: &mut Transaction<'_, Sqlite>, message: NewMessage<'_>) -> Result<()> {
sqlx::query!( sqlx::query!(
r#" r#"

View file

@ -125,6 +125,8 @@ pub enum TwonlyError {
InvalidParams(#[from] InvalidParams), InvalidParams(#[from] InvalidParams),
#[error("{0}")] #[error("{0}")]
InvalidOutputLen(#[from] InvalidOutputLen), InvalidOutputLen(#[from] InvalidOutputLen),
#[error("{0}")]
InvalidDigestLength(#[from] sha2::digest::InvalidLength),
#[error("AES-GCM error")] #[error("AES-GCM error")]
AesGcm, AesGcm,

View file

@ -5,12 +5,14 @@
use crate::api::messages::outgoing::send_c2c_message_to_contact; use crate::api::messages::outgoing::send_c2c_message_to_contact;
use crate::api::proto::client::{self as proto, encrypted_content}; use crate::api::proto::client::{self as proto, encrypted_content};
use crate::api::proto::server_to_client;
use crate::api::Server; use crate::api::Server;
use crate::bridge::api::ServerResult; use crate::bridge::api::ServerResult;
use crate::context::Context; use crate::context::Context;
use crate::database::app::tables::{Contact, Group, UpdateContact}; use crate::database::app::tables::{Contact, Group, UpdateContact};
use crate::error::{Result, TwonlyError}; use crate::error::{Result, TwonlyError};
use crate::signal::engine::FrbPreKeyBundle; use crate::signal::engine::FrbPreKeyBundle;
use crate::user_config::UserConfig;
use prost::Message as _; use prost::Message as _;
use std::io::Write as _; use std::io::Write as _;
use std::sync::Arc; use std::sync::Arc;
@ -73,7 +75,7 @@ impl ContactService {
async fn process_user_prekey_bundle( async fn process_user_prekey_bundle(
&self, &self,
user: &crate::api::proto::server_to_client::response::UserData, user: &server_to_client::response::UserData,
) -> Result<()> { ) -> Result<()> {
let missing = TwonlyError::ApiResponseMissingField; let missing = TwonlyError::ApiResponseMissingField;
let pqc_bundle = user.pqc_bundle.as_ref().ok_or(missing("pqc_bundle"))?; let pqc_bundle = user.pqc_bundle.as_ref().ok_or(missing("pqc_bundle"))?;
@ -81,6 +83,7 @@ impl ContactService {
.public_identity_key .public_identity_key
.clone() .clone()
.ok_or(missing("public_identity_key"))?; .ok_or(missing("public_identity_key"))?;
let registration_id = user.registration_id.ok_or(missing("registration_id"))?; let registration_id = user.registration_id.ok_or(missing("registration_id"))?;
let (pre_key_id, pre_key_public, kyber_pre_key_id, kyber_pre_key_public, kyber_signature) = let (pre_key_id, pre_key_public, kyber_pre_key_id, kyber_pre_key_public, kyber_signature) =
@ -136,6 +139,7 @@ impl ContactService {
let contact = Contact::get_contact_by_id(&mut transaction, contact_id) let contact = Contact::get_contact_by_id(&mut transaction, contact_id)
.await? .await?
.ok_or_else(|| TwonlyError::Generic("contact request does not exist".into()))?; .ok_or_else(|| TwonlyError::Generic("contact request does not exist".into()))?;
if contact.requested == 0 { if contact.requested == 0 {
return Err(TwonlyError::Generic( return Err(TwonlyError::Generic(
"contact has no pending request".into(), "contact has no pending request".into(),
@ -150,6 +154,7 @@ impl ContactService {
.build() .build()
.update(&mut transaction) .update(&mut transaction)
.await?; .await?;
Group::create_direct_chat(&self.ctx, &mut transaction, contact).await?; Group::create_direct_chat(&self.ctx, &mut transaction, contact).await?;
transaction.commit().await?; transaction.commit().await?;
database.notify_committed(["contacts", "groups"]); database.notify_committed(["contacts", "groups"]);
@ -198,7 +203,7 @@ impl ContactService {
} }
pub async fn send_profile(&self, contact_id: i64) -> Result<()> { pub async fn send_profile(&self, contact_id: i64) -> Result<()> {
let config = crate::user_config::UserConfig::load_required_from(&self.ctx)?; let config = UserConfig::load_required_from(&self.ctx)?;
let avatar_svg_compressed = config let avatar_svg_compressed = config
.avatar_svg .avatar_svg

View file

@ -51,13 +51,16 @@ impl GroupService {
let Some(best_friend) = groups.iter().max_by_key(|group| group.total_media_counter) else { let Some(best_friend) = groups.iter().max_by_key(|group| group.total_media_counter) else {
return Ok(()); return Ok(());
}; };
let best_friend_id = best_friend.group_id.clone(); let best_friend_id = best_friend.group_id.clone();
let now = current_time().timestamp(); let now = current_time().timestamp();
let start_today = now - now.rem_euclid(86_400); let start_today = now - now.rem_euclid(86_400);
for group in groups { for group in groups {
let Some(changed) = group.last_flame_counter_change else { let Some(changed) = group.last_flame_counter_change else {
continue; continue;
}; };
if changed < start_today if changed < start_today
|| group || group
.last_flame_sync .last_flame_sync
@ -65,9 +68,11 @@ impl GroupService {
{ {
continue; continue;
} }
if group.flame_counter <= 2 && group.group_id != best_friend_id { if group.flame_counter <= 2 && group.group_id != best_friend_id {
continue; continue;
} }
MessageService::new(&self.ctx) MessageService::new(&self.ctx)
.send_to_group( .send_to_group(
group.group_id.clone(), group.group_id.clone(),
@ -85,6 +90,7 @@ impl GroupService {
false, false,
) )
.await?; .await?;
Group::set_last_flame_sync(&db.pool, &group.group_id, now).await?; Group::set_last_flame_sync(&db.pool, &group.group_id, now).await?;
} }
db.notify_committed(["groups"]); db.notify_committed(["groups"]);

View file

@ -3,6 +3,7 @@
* *
*/ */
use crate::api::messages::outgoing::send_c2c_message_to_contact;
use crate::api::proto::client::{encrypted_content, EncryptedContent}; use crate::api::proto::client::{encrypted_content, EncryptedContent};
use crate::bridge::api::RustApi; use crate::bridge::api::RustApi;
use crate::context::Context; use crate::context::Context;
@ -11,7 +12,7 @@ use chacha20poly1305::aead::{AeadInPlace, KeyInit};
use chacha20poly1305::{ChaCha20Poly1305, Nonce, Tag}; use chacha20poly1305::{ChaCha20Poly1305, Nonce, Tag};
use prost::Message as _; use prost::Message as _;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use sqlx::FromRow; use sqlx::{FromRow, Sqlite, Transaction};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
@ -240,26 +241,28 @@ impl MediaFileService {
let mac = media.encryption_mac.as_deref().ok_or_else(|| { let mac = media.encryption_mac.as_deref().ok_or_else(|| {
TwonlyError::Generic(format!("media {} has no encryption MAC", media.media_id)) TwonlyError::Generic(format!("media {} has no encryption MAC", media.media_id))
})?; })?;
let cipher = ChaCha20Poly1305::new_from_slice(key)
.map_err(|_| TwonlyError::Generic("invalid media encryption key".into()))?; let cipher = ChaCha20Poly1305::new_from_slice(key)?;
if nonce.len() != 12 { if nonce.len() != 12 {
return Err(TwonlyError::Generic( return Err(TwonlyError::Generic(
"invalid media encryption nonce".into(), "invalid media encryption nonce".into(),
)); ));
} }
if mac.len() != 16 { if mac.len() != 16 {
return Err(TwonlyError::Generic("invalid media encryption MAC".into())); return Err(TwonlyError::Generic("invalid media encryption MAC".into()));
} }
let nonce = Nonce::from_slice(nonce); let nonce = Nonce::from_slice(nonce);
let tag = Tag::from_slice(mac); let tag = Tag::from_slice(mac);
let mut bytes = std::fs::read(encrypted_path)?; let mut bytes = std::fs::read(encrypted_path)?;
cipher cipher.decrypt_in_place_detached(nonce, b"", &mut bytes, tag)?;
.decrypt_in_place_detached(nonce, b"", &mut bytes, tag)
.map_err(|_| TwonlyError::Generic("media authentication failed".into()))?;
let temp_path = self.temp_path(&media.media_id, &media.media_type); let temp_path = self.temp_path(&media.media_id, &media.media_type);
Self::ensure_parent(&temp_path)?; Self::ensure_parent(&temp_path)?;
std::fs::write(&temp_path, &bytes)?; std::fs::write(&temp_path, &bytes)?;
let hash = Sha256::digest(&bytes).to_vec(); let hash = Sha256::digest(&bytes).to_vec();
// Keep the file update and state transition ordered: ready is only // Keep the file update and state transition ordered: ready is only
@ -273,6 +276,7 @@ impl MediaFileService {
) )
.execute(&database.pool) .execute(&database.pool)
.await?; .await?;
std::fs::remove_file(encrypted_path)?; std::fs::remove_file(encrypted_path)?;
Ok(()) Ok(())
} }
@ -285,6 +289,7 @@ impl MediaFileService {
) )
.execute(&database.pool) .execute(&database.pool)
.await?; .await?;
let targets = sqlx::query_as::<_, ReuploadTarget>( let targets = sqlx::query_as::<_, ReuploadTarget>(
r#"SELECT message_id, sender_id FROM messages r#"SELECT message_id, sender_id FROM messages
WHERE media_id = ? AND opened_at IS NULL AND sender_id IS NOT NULL"#, WHERE media_id = ? AND opened_at IS NULL AND sender_id IS NOT NULL"#,
@ -292,6 +297,7 @@ impl MediaFileService {
.bind(media_id) .bind(media_id)
.fetch_all(&database.pool) .fetch_all(&database.pool)
.await?; .await?;
database.notify_committed(["media_files"]); database.notify_committed(["media_files"]);
drop(database); drop(database);
@ -303,7 +309,8 @@ impl MediaFileService {
}), }),
..Default::default() ..Default::default()
}; };
crate::api::messages::outgoing::send_c2c_message_to_contact()
send_c2c_message_to_contact()
.ctx(&self.ctx) .ctx(&self.ctx)
.contact_id(target.sender_id) .contact_id(target.sender_id)
.encrypted_content(content.encode_to_vec()) .encrypted_content(content.encode_to_vec())
@ -324,15 +331,20 @@ impl MediaFileService {
Ok(()) Ok(())
} }
pub async fn remove_files_if_deleted(&self, media_id: &str, media_type: &str) -> Result<()> { pub async fn remove_files_if_deleted(
let database = self.ctx.get_app_database().await; &self,
t: &mut Transaction<'_, Sqlite>,
media_id: &str,
media_type: &str,
) -> Result<()> {
let still_exists = sqlx::query_scalar!( let still_exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM media_files WHERE media_id = ?)", "SELECT EXISTS(SELECT 1 FROM media_files WHERE media_id = ?)",
media_id, media_id,
) )
.fetch_one(&database.pool) .fetch_one(&mut **t)
.await? .await?
!= 0; != 0;
if !still_exists { if !still_exists {
self.remove_files(media_id, media_type)?; self.remove_files(media_id, media_type)?;
} }

View file

@ -375,7 +375,7 @@ impl UserConfig {
/// Atomically updates the latest persisted configuration with a typed Rust /// Atomically updates the latest persisted configuration with a typed Rust
/// mutation. Unlike `update_json`, this does not need a caller snapshot: /// mutation. Unlike `update_json`, this does not need a caller snapshot:
/// loading, mutation, and saving all happen while holding the write lock. /// loading, mutation, and saving all happen while holding the write lock.
pub(crate) fn update(context: &Context, mutate: impl FnOnce(&mut Self)) -> Result<()> { pub(crate) fn update(context: &Context, mutate: impl FnOnce(&mut Self)) -> Result<Self> {
let _guard = config_lock() let _guard = config_lock()
.write() .write()
.map_err(|_| twonly_error!("user configuration lock was poisoned"))?; .map_err(|_| twonly_error!("user configuration lock was poisoned"))?;
@ -383,7 +383,7 @@ impl UserConfig {
.ok_or_else(|| twonly_error!("user configuration is unavailable"))?; .ok_or_else(|| twonly_error!("user configuration is unavailable"))?;
mutate(&mut config); mutate(&mut config);
Self::save_unlocked(context, &config)?; Self::save_unlocked(context, &config)?;
Ok(()) Ok(config)
} }
/// Applies only fields changed relative to the caller's original snapshot. /// Applies only fields changed relative to the caller's original snapshot.