Skip to content

Commit beba162

Browse files
committed
add: Intelligent Profile Syncing + Fawkes Badge Test + several bugfixes
1 parent 820a7dd commit beba162

File tree

8 files changed

+4423
-20
lines changed

8 files changed

+4423
-20
lines changed

src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ mdk-core = { git = "https://github.com/parres-hq/mdk", rev = "f46875ec6fbe1cd616
2828
mdk-sqlite-storage = { git = "https://github.com/parres-hq/mdk", rev = "f46875ec6fbe1cd616e9dfb4d2aa10f56044e58c" }
2929
mdk-storage-traits = { git = "https://github.com/parres-hq/mdk", rev = "f46875ec6fbe1cd616e9dfb4d2aa10f56044e58c" }
3030
bip39 = { version = "2.2.0", features = ["rand"] }
31-
tokio = { version = "1.48.0", features = ["sync"] }
31+
tokio = { version = "1.48.0", features = ["sync", "time"] }
3232
futures-util = "0.3.31"
3333
tauri = { version = "2.9.1", features = ["protocol-asset", "image-png"] }
3434
serde = { version = "1.0.228", features = ["derive"] }

src-tauri/src/lib.rs

Lines changed: 219 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ pub use message::{Message, Attachment, Reaction};
4747
mod profile;
4848
pub use profile::{Profile, Status};
4949

50+
mod profile_sync;
51+
5052
mod chat;
5153
pub use chat::{Chat, ChatType, ChatMetadata};
5254

@@ -3378,6 +3380,113 @@ async fn get_invited_users(npub: String) -> Result<u32, String> {
33783380

33793381
Ok(unique_acceptors.len() as u32)
33803382
}
3383+
3384+
// Guy Fawkes Day 2025 constants
3385+
// TESTING: Using today (Nov 4th, 2025) for testing (will change to Nov 5th for production)
3386+
const FAWKES_DAY_START: u64 = 1762214400; // 2025-11-04 00:00:00 UTC (TESTING - actual Nov 4, 2025)
3387+
const FAWKES_DAY_END: u64 = 1762300800; // 2025-11-05 00:00:00 UTC (TESTING - actual Nov 5, 2025)
3388+
// PRODUCTION VALUES (uncomment for release):
3389+
// const FAWKES_DAY_START: u64 = 1762300800; // 2025-11-05 00:00:00 UTC
3390+
// const FAWKES_DAY_END: u64 = 1762387200; // 2025-11-06 00:00:00 UTC
3391+
3392+
/// Helper function to check if a user has a valid Guy Fawkes Day badge
3393+
async fn has_fawkes_badge_internal(user_pubkey: PublicKey) -> Result<bool, String> {
3394+
let client = NOSTR_CLIENT.get().ok_or("Nostr client not initialized")?;
3395+
3396+
// Fetch the user's badge claim event
3397+
let filter = Filter::new()
3398+
.author(user_pubkey)
3399+
.kind(Kind::ApplicationSpecificData)
3400+
.custom_tag(SingleLetterTag::lowercase(Alphabet::D), "fawkes_2025")
3401+
.limit(10);
3402+
3403+
let events = client
3404+
.fetch_events_from(vec![TRUSTED_RELAY], filter, std::time::Duration::from_secs(10))
3405+
.await
3406+
.map_err(|e| e.to_string())?;
3407+
3408+
// Check if they have a valid badge claim
3409+
for event in events {
3410+
if event.content == "fawkes_badge_claimed" {
3411+
let timestamp = event.created_at.as_u64();
3412+
// Verify the timestamp is within the valid window
3413+
if timestamp >= FAWKES_DAY_START && timestamp < FAWKES_DAY_END {
3414+
return Ok(true);
3415+
}
3416+
}
3417+
}
3418+
3419+
Ok(false)
3420+
}
3421+
3422+
/// Claim the Guy Fawkes Day badge (5th November 2025)
3423+
/// This should be called on login to check if it's Guy Fawkes Day and claim the badge
3424+
#[tauri::command]
3425+
async fn claim_fawkes_badge() -> Result<bool, String> {
3426+
let client = NOSTR_CLIENT.get().ok_or("Nostr client not initialized")?;
3427+
3428+
// Get current timestamp
3429+
let now = std::time::SystemTime::now()
3430+
.duration_since(std::time::UNIX_EPOCH)
3431+
.map_err(|e| e.to_string())?
3432+
.as_secs();
3433+
3434+
println!("[FAWKES] Current timestamp: {}", now);
3435+
println!("[FAWKES] Valid window: {} - {}", FAWKES_DAY_START, FAWKES_DAY_END);
3436+
3437+
// Check if we're in the valid time window
3438+
if now < FAWKES_DAY_START || now >= FAWKES_DAY_END {
3439+
println!("[FAWKES] Outside valid time window, not claiming badge");
3440+
return Ok(false); // Not Guy Fawkes Day
3441+
}
3442+
3443+
println!("[FAWKES] Inside valid time window, proceeding with claim...");
3444+
3445+
// Get my public key
3446+
let signer = client.signer().await.map_err(|e| e.to_string())?;
3447+
let my_public_key = signer.get_public_key().await.map_err(|e| e.to_string())?;
3448+
3449+
// Check if we already have the badge using the helper function
3450+
if has_fawkes_badge_internal(my_public_key).await? {
3451+
return Ok(true); // Already claimed
3452+
}
3453+
3454+
// Create and publish the badge claim event
3455+
// TESTING: Add 5-minute expiration for testing (remove for production)
3456+
let expiry_time = Timestamp::from_secs(
3457+
std::time::SystemTime::now()
3458+
.duration_since(std::time::UNIX_EPOCH)
3459+
.unwrap()
3460+
.as_secs() + 300 // 5 minutes for testing
3461+
);
3462+
3463+
let event_builder = EventBuilder::new(Kind::ApplicationSpecificData, "fawkes_badge_claimed")
3464+
.tag(Tag::custom(TagKind::d(), vec!["fawkes_2025"]))
3465+
.tag(Tag::expiration(expiry_time)); // TESTING: Remove this line for production
3466+
3467+
// Build and sign the event
3468+
let event = client.sign_event_builder(event_builder).await.map_err(|e| e.to_string())?;
3469+
3470+
// Verify the timestamp is within the valid window (double-check after signing)
3471+
if event.created_at.as_u64() < FAWKES_DAY_START || event.created_at.as_u64() >= FAWKES_DAY_END {
3472+
return Err("Event timestamp is outside the valid Guy Fawkes Day window".to_string());
3473+
}
3474+
3475+
// Send to trusted relay
3476+
client.send_event_to([TRUSTED_RELAY], &event).await.map_err(|e| e.to_string())?;
3477+
3478+
Ok(true)
3479+
}
3480+
3481+
/// Check if a user has the Guy Fawkes Day badge
3482+
#[tauri::command]
3483+
async fn check_fawkes_badge(npub: String) -> Result<bool, String> {
3484+
// Convert npub to PublicKey
3485+
let user_pubkey = PublicKey::from_bech32(&npub).map_err(|e| e.to_string())?;
3486+
3487+
// Use the helper function
3488+
has_fawkes_badge_internal(user_pubkey).await
3489+
}
33813490
// MLS Tauri Commands
33823491

33833492

@@ -3758,18 +3867,24 @@ async fn invite_member_to_group(
37583867
.ok_or_else(|| format!("No device keypackages found for {}", member_npub))?;
37593868

37603869
// Run non-Send MLS engine work on a blocking thread
3870+
let group_id_clone = group_id.clone();
37613871
tokio::task::spawn_blocking(move || {
37623872
let handle = TAURI_APP.get().ok_or("App handle not initialized")?.clone();
37633873
let rt = tokio::runtime::Handle::current();
37643874
rt.block_on(async move {
37653875
let mls = MlsService::new_persistent(&handle).map_err(|e| e.to_string())?;
3766-
mls.add_member_device(&group_id, &member_npub, &device_id)
3876+
mls.add_member_device(&group_id_clone, &member_npub, &device_id)
37673877
.await
37683878
.map_err(|e| e.to_string())
37693879
})
37703880
})
37713881
.await
3772-
.map_err(|e| format!("Task join error: {}", e))?
3882+
.map_err(|e| format!("Task join error: {}", e))??;
3883+
3884+
// Sync participants array after adding member
3885+
sync_mls_group_participants(group_id).await?;
3886+
3887+
Ok(())
37733888
}
37743889

37753890
/// Remove a member device from an MLS group
@@ -3780,18 +3895,24 @@ async fn remove_mls_member_device(
37803895
device_id: String,
37813896
) -> Result<(), String> {
37823897
// Run non-Send MLS engine work on a blocking thread; drive async via current runtime
3898+
let group_id_clone = group_id.clone();
37833899
tokio::task::spawn_blocking(move || {
37843900
let handle = TAURI_APP.get().ok_or("App handle not initialized")?.clone();
37853901
let rt = tokio::runtime::Handle::current();
37863902
rt.block_on(async move {
37873903
let mls = MlsService::new_persistent(&handle).map_err(|e| e.to_string())?;
3788-
mls.remove_member_device(&group_id, &member_npub, &device_id)
3904+
mls.remove_member_device(&group_id_clone, &member_npub, &device_id)
37893905
.await
37903906
.map_err(|e| e.to_string())
37913907
})
37923908
})
37933909
.await
3794-
.map_err(|e| format!("Task join error: {}", e))?
3910+
.map_err(|e| format!("Task join error: {}", e))??;
3911+
3912+
// Sync participants array after removing member
3913+
sync_mls_group_participants(group_id).await?;
3914+
3915+
Ok(())
37953916
}
37963917

37973918
/// Sync MLS groups with the network
@@ -3849,6 +3970,11 @@ async fn sync_mls_groups_now(
38493970
eprintln!("[MLS] sync_group_since_cursor failed for {}: {}", gid, e);
38503971
}
38513972
}
3973+
3974+
// Sync participants array to ensure it matches actual group members
3975+
if let Err(e) = sync_mls_group_participants(gid.clone()).await {
3976+
eprintln!("[MLS] Failed to sync participants for group {}: {}", gid, e);
3977+
}
38523978
}
38533979

38543980
Ok((total_processed, total_new))
@@ -4104,6 +4230,11 @@ async fn accept_mls_welcome(welcome_event_id_hex: String) -> Result<bool, String
41044230
}));
41054231
}
41064232

4233+
// Sync the participants array with actual group members from the engine
4234+
if let Err(e) = sync_mls_group_participants(nostr_group_id.clone()).await {
4235+
eprintln!("[MLS] Failed to sync participants after welcome accept: {}", e);
4236+
}
4237+
41074238
// Immediately prefetch recent MLS messages for this group so the chat list shows previews
41084239
// and ordering without requiring the user to open the chat. This loads a recent slice
41094240
// (48h window by default in sync_group_since_cursor) rather than full history.
@@ -4330,6 +4461,45 @@ struct GroupMembers {
43304461
admins: Vec<String>, // admin npubs
43314462
}
43324463

4464+
/// Sync the participants array for an MLS group chat with the actual members from the engine
4465+
/// This ensures chat.participants is always up-to-date
4466+
async fn sync_mls_group_participants(group_id: String) -> Result<(), String> {
4467+
// Get actual members from the engine
4468+
let group_members = get_mls_group_members(group_id.clone()).await?;
4469+
4470+
// Update the chat's participants array
4471+
let mut state = STATE.lock().await;
4472+
if let Some(chat) = state.get_chat_mut(&group_id) {
4473+
let old_count = chat.participants.len();
4474+
chat.participants = group_members.members.clone();
4475+
let new_count = chat.participants.len();
4476+
4477+
if old_count != new_count {
4478+
eprintln!(
4479+
"[MLS] Synced participants for group {}: {} -> {} members",
4480+
&group_id[..8],
4481+
old_count,
4482+
new_count
4483+
);
4484+
}
4485+
4486+
// Save updated chat to disk
4487+
let chat_clone = chat.clone();
4488+
drop(state);
4489+
4490+
if let Some(handle) = TAURI_APP.get() {
4491+
if let Err(e) = db_migration::save_chat(handle.clone(), &chat_clone).await {
4492+
eprintln!("[MLS] Failed to save chat after syncing participants: {}", e);
4493+
}
4494+
}
4495+
} else {
4496+
drop(state);
4497+
eprintln!("[MLS] Chat not found when syncing participants: {}", group_id);
4498+
}
4499+
4500+
Ok(())
4501+
}
4502+
43334503
/// Get members (npubs) of an MLS group from the persistent engine (on-demand)
43344504
#[tauri::command]
43354505
async fn get_mls_group_members(group_id: String) -> Result<GroupMembers, String> {
@@ -4493,6 +4663,38 @@ async fn refresh_keypackages_for_contact(
44934663
44944664
/// Remove orphaned MLS groups from metadata that are not in engine state
44954665
4666+
#[tauri::command]
4667+
async fn queue_profile_sync(npub: String, priority: String, force_refresh: bool) -> Result<(), String> {
4668+
let sync_priority = match priority.as_str() {
4669+
"critical" => profile_sync::SyncPriority::Critical,
4670+
"high" => profile_sync::SyncPriority::High,
4671+
"medium" => profile_sync::SyncPriority::Medium,
4672+
"low" => profile_sync::SyncPriority::Low,
4673+
_ => return Err(format!("Invalid priority: {}", priority)),
4674+
};
4675+
4676+
profile_sync::queue_profile_sync(npub, sync_priority, force_refresh).await;
4677+
Ok(())
4678+
}
4679+
4680+
#[tauri::command]
4681+
async fn queue_chat_profiles_sync(chat_id: String, is_opening: bool) -> Result<(), String> {
4682+
profile_sync::queue_chat_profiles(chat_id, is_opening).await;
4683+
Ok(())
4684+
}
4685+
4686+
#[tauri::command]
4687+
async fn refresh_profile_now(npub: String) -> Result<(), String> {
4688+
profile_sync::refresh_profile_now(npub).await;
4689+
Ok(())
4690+
}
4691+
4692+
#[tauri::command]
4693+
async fn sync_all_profiles() -> Result<(), String> {
4694+
profile_sync::sync_all_profiles().await;
4695+
Ok(())
4696+
}
4697+
44964698
#[cfg_attr(mobile, tauri::mobile_entry_point)]
44974699
pub fn run() {
44984700
#[cfg(target_os = "linux")]
@@ -4547,6 +4749,12 @@ pub fn run() {
45474749

45484750
// Set as our accessible static app handle
45494751
TAURI_APP.set(handle).unwrap();
4752+
4753+
// Start the profile sync background processor
4754+
tauri::async_runtime::spawn(async {
4755+
profile_sync::start_profile_sync_processor().await;
4756+
});
4757+
45504758
Ok(())
45514759
})
45524760
.invoke_handler(tauri::generate_handler![
@@ -4612,6 +4820,8 @@ pub fn run() {
46124820
get_or_create_invite_code,
46134821
accept_invite_code,
46144822
get_invited_users,
4823+
claim_fawkes_badge,
4824+
check_fawkes_badge,
46154825
db::get_invite_code,
46164826
db::set_invite_code,
46174827
export_keys,
@@ -4638,6 +4848,11 @@ pub fn run() {
46384848
leave_mls_group,
46394849
list_group_cursors,
46404850
refresh_keypackages_for_contact,
4851+
// Profile sync commands
4852+
queue_profile_sync,
4853+
queue_chat_profiles_sync,
4854+
refresh_profile_now,
4855+
sync_all_profiles,
46414856
#[cfg(all(not(target_os = "android"), feature = "whisper"))]
46424857
whisper::delete_whisper_model,
46434858
#[cfg(all(not(target_os = "android"), feature = "whisper"))]

0 commit comments

Comments
 (0)