initial commit

Signed-off-by: Sienna Meridian Satterwhite <sienna@r3t.io>
This commit is contained in:
2025-11-15 23:42:12 +00:00
commit 2bad250a04
47 changed files with 14645 additions and 0 deletions

139
crates/lib/src/db.rs Normal file
View File

@@ -0,0 +1,139 @@
use crate::error::Result;
use crate::models::*;
use rusqlite::{Connection, OpenFlags, Row, params};
pub struct ChatDb {
conn: Connection,
}
impl ChatDb {
/// Open a connection to the chat database in read-only mode
pub fn open(path: &str) -> Result<Self> {
let conn = Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_ONLY)?;
Ok(Self { conn })
}
/// Get messages from the conversation with +31 6 39 13 29 13
///
/// Returns messages from January 1, 2024 to present from the conversation
/// with the specified Dutch phone number.
///
/// # Arguments
///
/// * `start_date` - Start date (defaults to January 1, 2024 if None)
/// * `end_date` - End date (defaults to current time if None)
pub fn get_our_messages(
&self,
start_date: Option<chrono::DateTime<chrono::Utc>>,
end_date: Option<chrono::DateTime<chrono::Utc>>,
) -> Result<Vec<Message>> {
use chrono::{TimeZone, Utc};
// Default date range: January 1, 2024 to now
let start =
start_date.unwrap_or_else(|| Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap());
let end = end_date.unwrap_or_else(|| Utc::now());
// Convert to Apple timestamps (nanoseconds since 2001-01-01)
let start_timestamp = datetime_to_apple_timestamp(start);
let end_timestamp = datetime_to_apple_timestamp(end);
// The phone number might be stored with or without spaces
let phone_with_spaces = "+31 6 39 13 29 13";
let phone_without_spaces = "+31639132913";
// Find the chat with this phone number (try both formats)
let chat = self
.get_chat_for_phone_number(phone_with_spaces)
.or_else(|_| self.get_chat_for_phone_number(phone_without_spaces))?;
// Get messages from this chat within the date range
let mut stmt = self.conn.prepare(
"SELECT m.ROWID, m.guid, m.text, m.service, m.handle_id, m.date, m.date_read, m.date_delivered,
m.is_from_me, m.is_read, m.is_delivered, m.is_sent, m.is_emote, m.is_audio_message,
m.cache_has_attachments, m.associated_message_guid, m.associated_message_type,
m.thread_originator_guid, m.reply_to_guid, m.is_spam
FROM message m
INNER JOIN chat_message_join cmj ON m.ROWID = cmj.message_id
WHERE cmj.chat_id = ?
AND m.date >= ?
AND m.date <= ?
ORDER BY m.date ASC"
)?;
let messages = stmt
.query_map(
params![chat.rowid, start_timestamp, end_timestamp],
map_message_row,
)?
.collect::<std::result::Result<Vec<_>, _>>()?;
Ok(messages)
}
/// Helper function to find the largest chat with a specific phone number
fn get_chat_for_phone_number(&self, phone_number: &str) -> Result<Chat> {
let mut stmt = self.conn.prepare(
"SELECT c.ROWID, c.guid, c.chat_identifier, c.service_name, c.display_name,
c.group_id, c.room_name, c.is_archived, c.is_filtered,
c.last_read_message_timestamp, COUNT(cmj.message_id) as msg_count
FROM chat c
INNER JOIN chat_handle_join chj ON c.ROWID = chj.chat_id
INNER JOIN handle h ON chj.handle_id = h.ROWID
INNER JOIN chat_message_join cmj ON c.ROWID = cmj.chat_id
WHERE h.id = ?
GROUP BY c.ROWID
ORDER BY msg_count DESC
LIMIT 1"
)?;
let chat = stmt.query_row(params![phone_number], |row| {
Ok(Chat {
rowid: row.get(0)?,
guid: row.get(1)?,
chat_identifier: row.get(2)?,
service_name: row.get(3)?,
display_name: row.get(4)?,
group_id: row.get(5)?,
room_name: row.get(6)?,
is_archived: row.get::<_, i64>(7)? != 0,
is_filtered: row.get::<_, i64>(8)? != 0,
last_read_message_timestamp: row.get::<_, Option<i64>>(9)?.map(apple_timestamp_to_datetime),
})
})?;
Ok(chat)
}
}
// Helper function to map database rows to structs
fn map_message_row(row: &Row) -> rusqlite::Result<Message> {
Ok(Message {
rowid: row.get(0)?,
guid: row.get(1)?,
text: row.get(2)?,
service: row.get(3)?,
handle_id: row.get(4)?,
date: row
.get::<_, Option<i64>>(5)?
.map(apple_timestamp_to_datetime),
date_read: row
.get::<_, Option<i64>>(6)?
.map(apple_timestamp_to_datetime),
date_delivered: row
.get::<_, Option<i64>>(7)?
.map(apple_timestamp_to_datetime),
is_from_me: row.get::<_, i64>(8)? != 0,
is_read: row.get::<_, i64>(9)? != 0,
is_delivered: row.get::<_, i64>(10)? != 0,
is_sent: row.get::<_, i64>(11)? != 0,
is_emote: row.get::<_, i64>(12)? != 0,
is_audio_message: row.get::<_, i64>(13)? != 0,
cache_has_attachments: row.get::<_, i64>(14)? != 0,
associated_message_guid: row.get(15)?,
associated_message_type: row.get(16)?,
thread_originator_guid: row.get(17)?,
reply_to_guid: row.get(18)?,
is_spam: row.get::<_, i64>(19)? != 0,
})
}

15
crates/lib/src/error.rs Normal file
View File

@@ -0,0 +1,15 @@
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ChatDbError {
#[error("Database error: {0}")]
Database(#[from] rusqlite::Error),
#[error("Not found: {0}")]
NotFound(String),
#[error("Invalid data: {0}")]
InvalidData(String),
}
pub type Result<T> = std::result::Result<T, ChatDbError>;

30
crates/lib/src/lib.rs Normal file
View File

@@ -0,0 +1,30 @@
//! Data access layer for iMessage chat.db
//!
//! This library provides a read-only interface to query messages from a specific conversation.
//!
//! # Safety
//!
//! All database connections are opened in read-only mode to prevent any
//! accidental modifications to your iMessage database.
//!
//! # Example
//!
//! ```no_run
//! use lib::ChatDb;
//!
//! let db = ChatDb::open("chat.db")?;
//!
//! // Get all messages from January 2024 to now
//! let messages = db.get_our_messages(None, None)?;
//! println!("Found {} messages", messages.len());
//! # Ok::<(), lib::ChatDbError>(())
//! ```
mod error;
mod models;
mod db;
pub mod sync;
pub use error::{ChatDbError, Result};
pub use models::{Message, Chat};
pub use db::ChatDb;

112
crates/lib/src/models.rs Normal file
View File

@@ -0,0 +1,112 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
/// Represents a message in the iMessage database
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
pub rowid: i64,
pub guid: String,
pub text: Option<String>,
pub service: Option<String>,
pub handle_id: i64,
pub date: Option<DateTime<Utc>>,
pub date_read: Option<DateTime<Utc>>,
pub date_delivered: Option<DateTime<Utc>>,
pub is_from_me: bool,
pub is_read: bool,
pub is_delivered: bool,
pub is_sent: bool,
pub is_emote: bool,
pub is_audio_message: bool,
pub cache_has_attachments: bool,
pub associated_message_guid: Option<String>,
pub associated_message_type: i64,
pub thread_originator_guid: Option<String>,
pub reply_to_guid: Option<String>,
pub is_spam: bool,
}
/// Represents a chat/conversation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Chat {
pub rowid: i64,
pub guid: String,
pub chat_identifier: Option<String>,
pub service_name: Option<String>,
pub display_name: Option<String>,
pub group_id: Option<String>,
pub room_name: Option<String>,
pub is_archived: bool,
pub is_filtered: bool,
pub last_read_message_timestamp: Option<DateTime<Utc>>,
}
/// Helper function to convert Apple's Cocoa timestamp (seconds since 2001-01-01) to DateTime
pub fn apple_timestamp_to_datetime(timestamp: i64) -> DateTime<Utc> {
// Apple's Cocoa timestamps are in nanoseconds since 2001-01-01 00:00:00 UTC
// Convert to Unix timestamp (seconds since 1970-01-01 00:00:00 UTC)
const APPLE_EPOCH_OFFSET: i64 = 978307200; // Seconds between 1970-01-01 and 2001-01-01
let seconds = timestamp / 1_000_000_000 + APPLE_EPOCH_OFFSET;
let nanos = (timestamp % 1_000_000_000) as u32;
DateTime::from_timestamp(seconds, nanos).unwrap_or_else(|| DateTime::from_timestamp(0, 0).unwrap())
}
/// Helper function to convert DateTime to Apple's Cocoa timestamp
pub fn datetime_to_apple_timestamp(dt: DateTime<Utc>) -> i64 {
const APPLE_EPOCH_OFFSET: i64 = 978307200;
let unix_timestamp = dt.timestamp();
let nanos = dt.timestamp_subsec_nanos() as i64;
(unix_timestamp - APPLE_EPOCH_OFFSET) * 1_000_000_000 + nanos
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::{Datelike, TimeZone, Timelike};
#[test]
fn test_apple_timestamp_to_datetime_zero() {
let dt = apple_timestamp_to_datetime(0);
assert_eq!(dt.year(), 2001);
assert_eq!(dt.month(), 1);
assert_eq!(dt.day(), 1);
assert_eq!(dt.hour(), 0);
assert_eq!(dt.minute(), 0);
assert_eq!(dt.second(), 0);
}
#[test]
fn test_apple_timestamp_to_datetime_known_value() {
let timestamp = 694224000000000000i64;
let dt = apple_timestamp_to_datetime(timestamp);
assert_eq!(dt.year(), 2023);
assert_eq!(dt.month(), 1);
assert_eq!(dt.day(), 1);
}
#[test]
fn test_apple_timestamp_roundtrip() {
let original = 694224000000000000i64;
let dt = apple_timestamp_to_datetime(original);
let converted_back = datetime_to_apple_timestamp(dt);
assert_eq!(original, converted_back);
}
#[test]
fn test_datetime_to_apple_timestamp_epoch() {
let dt = Utc.with_ymd_and_hms(2001, 1, 1, 0, 0, 0).unwrap();
let timestamp = datetime_to_apple_timestamp(dt);
assert_eq!(timestamp, 0);
}
#[test]
fn test_negative_apple_timestamp() {
let timestamp = -31536000000000000i64;
let dt = apple_timestamp_to_datetime(timestamp);
assert_eq!(dt.year(), 2000);
}
}

165
crates/lib/src/sync.rs Normal file
View File

@@ -0,0 +1,165 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::ops::{Deref, DerefMut};
// Re-export the macros
pub use sync_macros::{synced, Synced};
// Re-export common CRDT types from the crdts library
pub use crdts::{
ctx::ReadCtx,
lwwreg::LWWReg,
map::Map,
orswot::Orswot,
CmRDT, CvRDT,
};
pub type NodeId = String;
/// Transparent wrapper for synced values
///
/// This wraps any value with LWW semantics but allows you to use it like a normal value
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyncedValue<T: Clone> {
value: T,
timestamp: DateTime<Utc>,
node_id: NodeId,
}
impl<T: Clone> SyncedValue<T> {
pub fn new(value: T, node_id: NodeId) -> Self {
Self {
value,
timestamp: Utc::now(),
node_id,
}
}
pub fn get(&self) -> &T {
&self.value
}
pub fn set(&mut self, value: T, node_id: NodeId) {
self.value = value;
self.timestamp = Utc::now();
self.node_id = node_id;
}
pub fn apply_lww(&mut self, value: T, timestamp: DateTime<Utc>, node_id: NodeId) {
if timestamp > self.timestamp || (timestamp == self.timestamp && node_id > self.node_id) {
self.value = value;
self.timestamp = timestamp;
self.node_id = node_id;
}
}
pub fn merge(&mut self, other: &Self) {
self.apply_lww(other.value.clone(), other.timestamp, other.node_id.clone());
}
}
// Allow transparent access to the inner value
impl<T: Clone> Deref for SyncedValue<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.value
}
}
impl<T: Clone> DerefMut for SyncedValue<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.value
}
}
/// Wrapper for a sync message that goes over gossip
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyncMessage<T> {
/// Unique message ID
pub message_id: String,
/// Node that sent this
pub node_id: NodeId,
/// When it was sent
pub timestamp: DateTime<Utc>,
/// The actual sync operation
pub operation: T,
}
impl<T: Serialize> SyncMessage<T> {
pub fn new(node_id: NodeId, operation: T) -> Self {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let seq = COUNTER.fetch_add(1, Ordering::SeqCst);
Self {
message_id: format!("{}-{}-{}", node_id, Utc::now().timestamp_millis(), seq),
node_id,
timestamp: Utc::now(),
operation,
}
}
pub fn to_bytes(&self) -> anyhow::Result<Vec<u8>> {
Ok(serde_json::to_vec(self)?)
}
}
impl<T: for<'de> Deserialize<'de>> SyncMessage<T> {
pub fn from_bytes(bytes: &[u8]) -> anyhow::Result<Self> {
Ok(serde_json::from_slice(bytes)?)
}
}
/// Helper trait for types that can be synced
pub trait Syncable: Sized {
type Operation: Serialize + for<'de> Deserialize<'de> + Clone;
/// Apply a sync operation to this value
fn apply_sync_op(&mut self, op: &Self::Operation);
/// Get the node ID for this instance
fn node_id(&self) -> &NodeId;
/// Create a sync message for an operation
fn create_sync_message(&self, op: Self::Operation) -> SyncMessage<Self::Operation> {
SyncMessage::new(self.node_id().clone(), op)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_synced_value() {
let mut val = SyncedValue::new(42, "node1".to_string());
assert_eq!(*val.get(), 42);
val.set(100, "node1".to_string());
assert_eq!(*val.get(), 100);
// Test LWW semantics
let old_time = Utc::now() - chrono::Duration::seconds(10);
val.apply_lww(50, old_time, "node2".to_string());
assert_eq!(*val.get(), 100); // Should not update with older timestamp
}
#[test]
fn test_sync_message() {
#[derive(Debug, Clone, Serialize, Deserialize)]
struct TestOp {
value: i32,
}
let op = TestOp { value: 42 };
let msg = SyncMessage::new("node1".to_string(), op);
let bytes = msg.to_bytes().unwrap();
let decoded = SyncMessage::<TestOp>::from_bytes(&bytes).unwrap();
assert_eq!(decoded.node_id, "node1");
assert_eq!(decoded.operation.value, 42);
}
}