33192
crates/lib/lonni_messages.csv
Normal file
33192
crates/lib/lonni_messages.csv
Normal file
File diff suppressed because one or more lines are too long
245
crates/lib/scripts/export_messages.rs
Executable file
245
crates/lib/scripts/export_messages.rs
Executable file
@@ -0,0 +1,245 @@
|
||||
#!/usr/bin/env -S cargo +nightly -Zscript
|
||||
---
|
||||
[dependencies]
|
||||
rusqlite = { version = "0.37.0", features = ["bundled"] }
|
||||
csv = "1.3"
|
||||
chrono = "0.4"
|
||||
plist = "1.8"
|
||||
ns-keyed-archive = "0.1.4"
|
||||
anyhow = "1.0"
|
||||
---
|
||||
|
||||
use rusqlite::{Connection, OpenFlags};
|
||||
use std::fs::File;
|
||||
use csv::Writer;
|
||||
use chrono::{DateTime, Utc};
|
||||
use anyhow::Result;
|
||||
use ns_keyed_archive::decode::from_bytes as decode_keyed_archive;
|
||||
|
||||
const PHONE_NUMBER: &str = "+31639132913";
|
||||
const COCOA_EPOCH_OFFSET: i64 = 978307200;
|
||||
|
||||
fn cocoa_timestamp_to_datetime(timestamp: i64) -> String {
|
||||
if timestamp == 0 {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let seconds_since_2001 = timestamp / 1_000_000_000;
|
||||
let nanoseconds = (timestamp % 1_000_000_000) as u32;
|
||||
let unix_timestamp = COCOA_EPOCH_OFFSET + seconds_since_2001;
|
||||
|
||||
DateTime::from_timestamp(unix_timestamp, nanoseconds)
|
||||
.map(|dt: DateTime<Utc>| dt.to_rfc3339())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn extract_text_from_attributed_body(attributed_body: &[u8]) -> String {
|
||||
if attributed_body.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
// Try to parse as NSKeyedArchiver using the specialized crate
|
||||
match decode_keyed_archive(attributed_body) {
|
||||
Ok(value) => {
|
||||
// Try to extract the string value from the decoded archive
|
||||
if let Some(s) = extract_string_from_value(&value) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// If ns-keyed-archive fails, try regular plist parsing
|
||||
if let Ok(value) = plist::from_bytes::<plist::Value>(attributed_body) {
|
||||
if let Some(dict) = value.as_dictionary() {
|
||||
if let Some(objects) = dict.get("$objects").and_then(|v| v.as_array()) {
|
||||
for obj in objects {
|
||||
if let Some(s) = obj.as_string() {
|
||||
if !s.is_empty()
|
||||
&& s != "$null"
|
||||
&& !s.starts_with("NS")
|
||||
&& !s.starts_with("__k")
|
||||
{
|
||||
return s.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Last resort: simple string extraction
|
||||
return extract_text_fallback(attributed_body);
|
||||
}
|
||||
}
|
||||
|
||||
String::new()
|
||||
}
|
||||
|
||||
fn extract_string_from_value(value: &plist::Value) -> Option<String> {
|
||||
match value {
|
||||
plist::Value::String(s) => Some(s.clone()),
|
||||
plist::Value::Dictionary(dict) => {
|
||||
// Look for common NSAttributedString keys
|
||||
for key in &["NSString", "NS.string", "string"] {
|
||||
if let Some(val) = dict.get(*key) {
|
||||
if let Some(s) = extract_string_from_value(val) {
|
||||
return Some(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
plist::Value::Array(arr) => {
|
||||
// Find first non-empty string in array
|
||||
for item in arr {
|
||||
if let Some(s) = extract_string_from_value(item) {
|
||||
if !s.is_empty() && !s.starts_with("NS") && !s.starts_with("__k") {
|
||||
return Some(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_text_fallback(attributed_body: &[u8]) -> String {
|
||||
// Simple fallback: extract printable ASCII strings
|
||||
let mut current_str = String::new();
|
||||
let mut best_string = String::new();
|
||||
|
||||
for &byte in attributed_body {
|
||||
if (32..127).contains(&byte) {
|
||||
current_str.push(byte as char);
|
||||
} else {
|
||||
if current_str.len() > best_string.len()
|
||||
&& !current_str.starts_with("NS")
|
||||
&& !current_str.starts_with("__k")
|
||||
&& current_str != "streamtyped"
|
||||
&& current_str != "NSDictionary"
|
||||
{
|
||||
best_string = current_str.clone();
|
||||
}
|
||||
current_str.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Check final string
|
||||
if current_str.len() > best_string.len() {
|
||||
best_string = current_str;
|
||||
}
|
||||
|
||||
// Clean up common artifacts
|
||||
best_string = best_string.trim_start_matches(|c: char| {
|
||||
c == '+' && best_string.len() > 2
|
||||
}).trim().to_string();
|
||||
|
||||
best_string
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let home = std::env::var("HOME")?;
|
||||
let chat_db_path = format!("{}/Library/Messages/chat.db", home);
|
||||
let conn = Connection::open_with_flags(&chat_db_path, OpenFlags::SQLITE_OPEN_READ_ONLY)?;
|
||||
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT
|
||||
m.ROWID,
|
||||
m.text,
|
||||
m.attributedBody,
|
||||
m.date,
|
||||
m.date_read,
|
||||
m.date_delivered,
|
||||
m.is_from_me,
|
||||
m.is_read,
|
||||
COALESCE(h.id, 'unknown') as handle_id,
|
||||
c.chat_identifier,
|
||||
m.service
|
||||
FROM message m
|
||||
LEFT JOIN handle h ON m.handle_id = h.ROWID
|
||||
LEFT JOIN chat_message_join cmj ON m.ROWID = cmj.message_id
|
||||
LEFT JOIN chat c ON cmj.chat_id = c.ROWID
|
||||
WHERE h.id = ?1 OR c.chat_identifier = ?1
|
||||
ORDER BY m.date ASC",
|
||||
)?;
|
||||
|
||||
let messages = stmt.query_map([PHONE_NUMBER], |row| {
|
||||
Ok((
|
||||
row.get::<_, i64>(0)?, // ROWID
|
||||
row.get::<_, Option<String>>(1)?, // text
|
||||
row.get::<_, Option<Vec<u8>>>(2)?, // attributedBody
|
||||
row.get::<_, i64>(3)?, // date
|
||||
row.get::<_, Option<i64>>(4)?, // date_read
|
||||
row.get::<_, Option<i64>>(5)?, // date_delivered
|
||||
row.get::<_, i32>(6)?, // is_from_me
|
||||
row.get::<_, i32>(7)?, // is_read
|
||||
row.get::<_, String>(8)?, // handle_id
|
||||
row.get::<_, Option<String>>(9)?, // chat_identifier
|
||||
row.get::<_, Option<String>>(10)?, // service
|
||||
))
|
||||
})?;
|
||||
|
||||
let file = File::create("lonni_messages.csv")?;
|
||||
let mut wtr = Writer::from_writer(file);
|
||||
|
||||
wtr.write_record(&[
|
||||
"id",
|
||||
"date",
|
||||
"date_read",
|
||||
"date_delivered",
|
||||
"is_from_me",
|
||||
"is_read",
|
||||
"handle",
|
||||
"chat_identifier",
|
||||
"service",
|
||||
"text",
|
||||
])?;
|
||||
|
||||
let mut count = 0;
|
||||
for message in messages {
|
||||
let (
|
||||
rowid,
|
||||
text,
|
||||
attributed_body,
|
||||
date,
|
||||
date_read,
|
||||
date_delivered,
|
||||
is_from_me,
|
||||
is_read,
|
||||
handle_id,
|
||||
chat_identifier,
|
||||
service,
|
||||
) = message?;
|
||||
|
||||
// Extract text from attributedBody if text field is empty
|
||||
let message_text = text.unwrap_or_else(|| {
|
||||
attributed_body
|
||||
.as_ref()
|
||||
.map(|body| extract_text_from_attributed_body(body))
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
wtr.write_record(&[
|
||||
rowid.to_string(),
|
||||
cocoa_timestamp_to_datetime(date),
|
||||
date_read.map(cocoa_timestamp_to_datetime).unwrap_or_default(),
|
||||
date_delivered.map(cocoa_timestamp_to_datetime).unwrap_or_default(),
|
||||
is_from_me.to_string(),
|
||||
is_read.to_string(),
|
||||
handle_id,
|
||||
chat_identifier.unwrap_or_default(),
|
||||
service.unwrap_or_default(),
|
||||
message_text,
|
||||
])?;
|
||||
|
||||
count += 1;
|
||||
if count % 1000 == 0 {
|
||||
println!("Exported {} messages...", count);
|
||||
}
|
||||
}
|
||||
|
||||
wtr.flush()?;
|
||||
println!("Successfully exported {} messages to lonni_messages.csv", count);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -46,6 +46,7 @@ mod operations;
|
||||
mod orset;
|
||||
mod plugin;
|
||||
mod rga;
|
||||
mod sync_component;
|
||||
mod tombstones;
|
||||
mod vector_clock;
|
||||
|
||||
@@ -67,5 +68,6 @@ pub use operations::*;
|
||||
pub use orset::*;
|
||||
pub use plugin::*;
|
||||
pub use rga::*;
|
||||
pub use sync_component::*;
|
||||
pub use tombstones::*;
|
||||
pub use vector_clock::*;
|
||||
|
||||
160
crates/lib/src/networking/sync_component.rs
Normal file
160
crates/lib/src/networking/sync_component.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
//! Sync Component trait and supporting types for RFC 0003
|
||||
//!
|
||||
//! This module defines the core trait that all synced components implement,
|
||||
//! along with the types used for strategy selection and merge decisions.
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
/// Sync strategy enum - determines how conflicts are resolved
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SyncStrategy {
|
||||
/// Last-Write-Wins: Newer timestamp wins, node ID tiebreaker for concurrent
|
||||
LastWriteWins,
|
||||
/// OR-Set: Observed-Remove Set for collections
|
||||
Set,
|
||||
/// Sequence: RGA (Replicated Growable Array) for ordered lists
|
||||
Sequence,
|
||||
/// Custom: User-defined conflict resolution
|
||||
Custom,
|
||||
}
|
||||
|
||||
/// Result of comparing vector clocks
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ClockComparison {
|
||||
/// Remote vector clock is strictly newer
|
||||
RemoteNewer,
|
||||
/// Local vector clock is strictly newer
|
||||
LocalNewer,
|
||||
/// Concurrent (neither is newer)
|
||||
Concurrent,
|
||||
}
|
||||
|
||||
/// Decision made during component merge operation
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ComponentMergeDecision {
|
||||
/// Kept local value
|
||||
KeptLocal,
|
||||
/// Took remote value
|
||||
TookRemote,
|
||||
/// Merged both (for CRDTs)
|
||||
Merged,
|
||||
}
|
||||
|
||||
/// Core trait for synced components
|
||||
///
|
||||
/// This trait is automatically implemented by the `#[derive(Synced)]` macro.
|
||||
/// All synced components must implement this trait.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use bevy::prelude::*;
|
||||
/// use lib::networking::{SyncComponent, SyncStrategy, ClockComparison, ComponentMergeDecision};
|
||||
///
|
||||
/// // Example showing what the trait looks like - normally generated by #[derive(Synced)]
|
||||
/// #[derive(Component, Reflect, Clone, serde::Serialize, serde::Deserialize)]
|
||||
/// struct Health(f32);
|
||||
///
|
||||
/// // The SyncComponent trait defines these methods that the macro generates
|
||||
/// // You can serialize and deserialize components for sync
|
||||
/// ```
|
||||
pub trait SyncComponent: Component + Reflect + Sized {
|
||||
/// Schema version for this component
|
||||
const VERSION: u32;
|
||||
|
||||
/// Sync strategy for conflict resolution
|
||||
const STRATEGY: SyncStrategy;
|
||||
|
||||
/// Serialize this component to bytes
|
||||
///
|
||||
/// Uses bincode for efficient binary serialization.
|
||||
fn serialize_sync(&self) -> anyhow::Result<Vec<u8>>;
|
||||
|
||||
/// Deserialize this component from bytes
|
||||
///
|
||||
/// Uses bincode to deserialize from the format created by `serialize_sync`.
|
||||
fn deserialize_sync(data: &[u8]) -> anyhow::Result<Self>;
|
||||
|
||||
/// Merge remote state with local state
|
||||
///
|
||||
/// The merge logic is strategy-specific:
|
||||
/// - **LWW**: Takes newer value based on vector clock, uses tiebreaker for concurrent
|
||||
/// - **Set**: Merges both sets (OR-Set semantics)
|
||||
/// - **Sequence**: Merges sequences preserving order (RGA semantics)
|
||||
/// - **Custom**: Calls user-defined ConflictResolver
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `remote` - The remote state to merge
|
||||
/// * `clock_cmp` - Result of comparing local and remote vector clocks
|
||||
///
|
||||
/// # Returns
|
||||
/// Decision about what happened during the merge
|
||||
fn merge(&mut self, remote: Self, clock_cmp: ClockComparison) -> ComponentMergeDecision;
|
||||
}
|
||||
|
||||
/// Marker component for entities that should be synced
|
||||
///
|
||||
/// Add this to any entity with synced components to enable automatic
|
||||
/// change detection and synchronization.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use bevy::prelude::*;
|
||||
/// use lib::networking::Synced;
|
||||
/// use sync_macros::Synced as SyncedDerive;
|
||||
///
|
||||
/// #[derive(Component, Reflect, Clone, serde::Serialize, serde::Deserialize)]
|
||||
/// #[derive(SyncedDerive)]
|
||||
/// #[sync(version = 1, strategy = "LastWriteWins")]
|
||||
/// struct Health(f32);
|
||||
///
|
||||
/// #[derive(Component, Reflect, Clone, serde::Serialize, serde::Deserialize)]
|
||||
/// #[derive(SyncedDerive)]
|
||||
/// #[sync(version = 1, strategy = "LastWriteWins")]
|
||||
/// struct Position { x: f32, y: f32 }
|
||||
///
|
||||
/// let mut world = World::new();
|
||||
/// world.spawn((
|
||||
/// Health(100.0),
|
||||
/// Position { x: 0.0, y: 0.0 },
|
||||
/// Synced, // Marker enables sync
|
||||
/// ));
|
||||
/// ```
|
||||
#[derive(Component, Reflect, Default, Clone, Copy)]
|
||||
#[reflect(Component)]
|
||||
pub struct Synced;
|
||||
|
||||
/// Diagnostic component for debugging sync issues
|
||||
///
|
||||
/// Add this to an entity to get detailed diagnostic output about
|
||||
/// its sync status.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use bevy::prelude::*;
|
||||
/// use lib::networking::DiagnoseSync;
|
||||
///
|
||||
/// let mut world = World::new();
|
||||
/// let entity = world.spawn_empty().id();
|
||||
/// world.entity_mut(entity).insert(DiagnoseSync);
|
||||
/// // A diagnostic system will check this entity and log sync status
|
||||
/// ```
|
||||
#[derive(Component, Reflect, Default)]
|
||||
#[reflect(Component)]
|
||||
pub struct DiagnoseSync;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn strategy_enum_works() {
|
||||
assert_eq!(SyncStrategy::LastWriteWins, SyncStrategy::LastWriteWins);
|
||||
assert_ne!(SyncStrategy::LastWriteWins, SyncStrategy::Set);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clock_comparison_works() {
|
||||
assert_eq!(ClockComparison::RemoteNewer, ClockComparison::RemoteNewer);
|
||||
assert_ne!(ClockComparison::RemoteNewer, ClockComparison::LocalNewer);
|
||||
}
|
||||
}
|
||||
@@ -20,11 +20,8 @@ use serde::{
|
||||
Deserialize,
|
||||
Serialize,
|
||||
};
|
||||
// Re-export the macros
|
||||
pub use sync_macros::{
|
||||
Synced,
|
||||
synced,
|
||||
};
|
||||
// Re-export the Synced derive macro
|
||||
pub use sync_macros::Synced;
|
||||
|
||||
pub type NodeId = String;
|
||||
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use iroh::{
|
||||
Endpoint,
|
||||
protocol::{
|
||||
AcceptError,
|
||||
ProtocolHandler,
|
||||
Router,
|
||||
},
|
||||
};
|
||||
use lib::sync::{
|
||||
SyncMessage,
|
||||
Syncable,
|
||||
synced,
|
||||
};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
/// Test configuration that can be synced
|
||||
#[synced]
|
||||
struct TestConfig {
|
||||
value: i32,
|
||||
name: String,
|
||||
|
||||
#[sync(skip)]
|
||||
node_id: String,
|
||||
}
|
||||
|
||||
/// ALPN identifier for our sync protocol
|
||||
const SYNC_ALPN: &[u8] = b"/lonni/sync/1";
|
||||
|
||||
/// Protocol handler for receiving sync messages
|
||||
#[derive(Debug, Clone)]
|
||||
struct SyncProtocol {
|
||||
config: Arc<Mutex<TestConfig>>,
|
||||
}
|
||||
|
||||
impl ProtocolHandler for SyncProtocol {
|
||||
async fn accept(&self, connection: iroh::endpoint::Connection) -> Result<(), AcceptError> {
|
||||
println!("Accepting connection from: {}", connection.remote_id());
|
||||
|
||||
// Accept the bidirectional stream
|
||||
let (mut send, mut recv) = connection
|
||||
.accept_bi()
|
||||
.await
|
||||
.map_err(AcceptError::from_err)?;
|
||||
|
||||
println!("Stream accepted, reading message...");
|
||||
|
||||
// Read the sync message
|
||||
let bytes = recv
|
||||
.read_to_end(1024 * 1024)
|
||||
.await
|
||||
.map_err(AcceptError::from_err)?;
|
||||
|
||||
println!("Received {} bytes", bytes.len());
|
||||
|
||||
// Deserialize and apply
|
||||
let msg = SyncMessage::<TestConfigOp>::from_bytes(&bytes).map_err(|e| {
|
||||
AcceptError::from_err(std::io::Error::new(std::io::ErrorKind::InvalidData, e))
|
||||
})?;
|
||||
|
||||
println!("Applying operation from node: {}", msg.node_id);
|
||||
|
||||
let mut config = self.config.lock().await;
|
||||
config.apply_op(&msg.operation);
|
||||
|
||||
println!("Operation applied successfully");
|
||||
|
||||
// Close the stream
|
||||
send.finish().map_err(AcceptError::from_err)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_sync_between_two_nodes() -> Result<()> {
|
||||
println!("\n=== Testing Sync Between Two Nodes ===\n");
|
||||
|
||||
// Create two endpoints
|
||||
let node1 = Endpoint::builder().bind().await?;
|
||||
let node2 = Endpoint::builder().bind().await?;
|
||||
|
||||
let node1_addr = node1.addr();
|
||||
let node2_addr = node2.addr();
|
||||
|
||||
let node1_id = node1_addr.id.to_string();
|
||||
let node2_id = node2_addr.id.to_string();
|
||||
|
||||
println!("Node 1: {}", node1_id);
|
||||
println!("Node 2: {}", node2_id);
|
||||
|
||||
// Create synced configs on both nodes
|
||||
let mut config1 = TestConfig::new(42, "initial".to_string(), node1_id.clone());
|
||||
|
||||
let config2 = TestConfig::new(42, "initial".to_string(), node2_id.clone());
|
||||
let config2_shared = Arc::new(Mutex::new(config2));
|
||||
|
||||
println!("\nInitial state:");
|
||||
println!(
|
||||
" Node 1: value={}, name={}",
|
||||
config1.value(),
|
||||
config1.name()
|
||||
);
|
||||
{
|
||||
let config2 = config2_shared.lock().await;
|
||||
println!(
|
||||
" Node 2: value={}, name={}",
|
||||
config2.value(),
|
||||
config2.name()
|
||||
);
|
||||
}
|
||||
|
||||
// Set up router on node2 to accept incoming connections
|
||||
println!("\nSetting up node2 router...");
|
||||
let protocol = SyncProtocol {
|
||||
config: config2_shared.clone(),
|
||||
};
|
||||
let router = Router::builder(node2).accept(SYNC_ALPN, protocol).spawn();
|
||||
|
||||
router.endpoint().online().await;
|
||||
println!("✓ Node2 router ready");
|
||||
|
||||
// Node 1 changes the value
|
||||
println!("\nNode 1 changing value to 100...");
|
||||
let op = config1.set_value(100);
|
||||
|
||||
// Serialize the operation
|
||||
let sync_msg = SyncMessage::new(node1_id.clone(), op);
|
||||
let bytes = sync_msg.to_bytes()?;
|
||||
println!("Serialized to {} bytes", bytes.len());
|
||||
|
||||
// Establish QUIC connection from node1 to node2
|
||||
println!("\nEstablishing QUIC connection...");
|
||||
let conn = node1.connect(node2_addr.clone(), SYNC_ALPN).await?;
|
||||
println!("✓ Connection established");
|
||||
|
||||
// Open a bidirectional stream
|
||||
let (mut send, _recv) = conn.open_bi().await?;
|
||||
|
||||
// Send the sync message
|
||||
println!("Sending sync message...");
|
||||
send.write_all(&bytes).await?;
|
||||
send.finish()?;
|
||||
println!("✓ Message sent");
|
||||
|
||||
// Wait a bit for the message to be processed
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
|
||||
// Verify both configs have the same value
|
||||
println!("\nFinal state:");
|
||||
println!(
|
||||
" Node 1: value={}, name={}",
|
||||
config1.value(),
|
||||
config1.name()
|
||||
);
|
||||
{
|
||||
let config2 = config2_shared.lock().await;
|
||||
println!(
|
||||
" Node 2: value={}, name={}",
|
||||
config2.value(),
|
||||
config2.name()
|
||||
);
|
||||
|
||||
assert_eq!(*config1.value(), 100);
|
||||
assert_eq!(*config2.value(), 100);
|
||||
assert_eq!(config1.name(), "initial");
|
||||
assert_eq!(config2.name(), "initial");
|
||||
}
|
||||
|
||||
println!("\n✓ Sync successful!");
|
||||
|
||||
// Cleanup
|
||||
router.shutdown().await?;
|
||||
node1.close().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user