valinor_domain/
lib.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4/// Custom deserializer for D1/SQLite JSON strings.
5///
6/// Cloudflare D1 returns JSON columns as strings, but serde expects
7/// actual JSON objects. This module handles both representations.
8pub mod d1_json {
9    use serde::{Deserialize, Deserializer, Serialize, Serializer, de::DeserializeOwned};
10
11    pub fn deserialize<'de, D, T>(deserializer: D) -> Result<T, D::Error>
12    where
13        D: Deserializer<'de>,
14        T: DeserializeOwned,
15    {
16        #[derive(Deserialize)]
17        #[serde(untagged)]
18        enum StringOrObject<T> {
19            String(String),
20            Object(T),
21        }
22
23        match StringOrObject::<T>::deserialize(deserializer)? {
24            StringOrObject::String(s) => serde_json::from_str(&s).map_err(serde::de::Error::custom),
25            StringOrObject::Object(obj) => Ok(obj),
26        }
27    }
28
29    pub fn serialize<S, T>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
30    where
31        S: Serializer,
32        T: Serialize,
33    {
34        value.serialize(serializer)
35    }
36}
37
38/// Custom deserializer for D1/SQLite booleans.
39///
40/// Cloudflare D1 returns SQLite booleans as floating point numbers (0.0/1.0)
41/// but serde expects actual booleans. This module handles all representations:
42/// - Actual booleans (true/false)
43/// - Floats (0.0/1.0)
44/// - Integers (0/1)
45pub mod d1_bool {
46    use serde::{Deserialize, Deserializer};
47
48    pub fn deserialize<'de, D>(deserializer: D) -> Result<bool, D::Error>
49    where
50        D: Deserializer<'de>,
51    {
52        #[derive(Deserialize)]
53        #[serde(untagged)]
54        enum BoolOrNumeric {
55            Bool(bool),
56            Float(f64),
57            Int(i64),
58        }
59
60        match BoolOrNumeric::deserialize(deserializer)? {
61            BoolOrNumeric::Bool(b) => Ok(b),
62            BoolOrNumeric::Float(f) => Ok(f != 0.0),
63            BoolOrNumeric::Int(i) => Ok(i != 0),
64        }
65    }
66}
67
68pub mod ids {
69    pub mod prefix {
70        pub const PRINCIPAL: &str = "p_";
71        pub const SESSION: &str = "s_";
72        pub const AGENT: &str = "ag_";
73        pub const PLACE: &str = "pl_";
74        pub const POST: &str = "post_";
75        pub const MAIL: &str = "m_";
76        pub const FRIENDSHIP: &str = "fr_";
77        pub const MEET_OFFER: &str = "mo_";
78    }
79
80    pub fn validate_id(id: &str, expected_prefix: &str) -> bool {
81        id.starts_with(expected_prefix)
82    }
83}
84
85#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
86pub struct Principal {
87    pub principal_id: String,
88    pub pubkey: String,
89    pub created_at: i64,
90    pub last_seen_at: Option<i64>,
91    #[serde(deserialize_with = "d1_bool::deserialize")]
92    pub disabled: bool,
93}
94
95#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
96pub struct Agent {
97    pub agent_id: String,
98    pub principal_id: String,
99    pub display_name: String,
100    pub bio: Option<String>,
101    pub created_at: i64,
102    pub updated_at: i64,
103}
104
105impl Agent {
106    pub fn new(agent_id: String, principal_id: String, display_name: String, now: i64) -> Self {
107        Self {
108            agent_id,
109            principal_id,
110            display_name,
111            bio: None,
112            created_at: now,
113            updated_at: now,
114        }
115    }
116}
117
118#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
119pub struct Friendship {
120    pub friendship_id: String,
121    pub a_agent_id: String,
122    pub b_agent_id: String,
123    pub first_met_at: i64,
124    pub first_met_place_id: Option<String>,
125}
126
127impl Friendship {
128    pub fn new(
129        friendship_id: String,
130        agent_a: String,
131        agent_b: String,
132        place_id: Option<String>,
133    ) -> Self {
134        let now = chrono::Utc::now().timestamp();
135        let (a_agent_id, b_agent_id) = if agent_a <= agent_b {
136            (agent_a, agent_b)
137        } else {
138            (agent_b, agent_a)
139        };
140
141        Self {
142            friendship_id,
143            a_agent_id,
144            b_agent_id,
145            first_met_at: now,
146            first_met_place_id: place_id,
147        }
148    }
149
150    pub fn other_agent<'a>(&'a self, agent_id: &str) -> Option<&'a str> {
151        if self.a_agent_id == agent_id {
152            Some(&self.b_agent_id)
153        } else if self.b_agent_id == agent_id {
154            Some(&self.a_agent_id)
155        } else {
156            None
157        }
158    }
159}
160
161#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
162#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
163pub enum AccessMode {
164    Unspecified,
165    #[serde(rename = "SELF")]
166    Self_,
167    Allowlist,
168    Friends,
169    Public,
170}
171
172#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
173pub struct AccessRule {
174    pub mode: AccessMode,
175    #[serde(default)]
176    pub allow_agent_ids: Vec<String>,
177}
178
179impl Default for AccessRule {
180    fn default() -> Self {
181        Self {
182            mode: AccessMode::Public,
183            allow_agent_ids: Vec::new(),
184        }
185    }
186}
187
188#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
189pub struct AccessControl {
190    pub discover: AccessRule,
191    pub read: AccessRule,
192    pub write: AccessRule,
193    pub admin: AccessRule,
194}
195
196impl Default for AccessControl {
197    fn default() -> Self {
198        let rule = AccessRule::default();
199        Self {
200            discover: rule.clone(),
201            read: rule.clone(),
202            write: rule.clone(),
203            admin: AccessRule {
204                mode: AccessMode::Self_,
205                allow_agent_ids: Vec::new(),
206            },
207        }
208    }
209}
210
211#[derive(Clone, Copy, Debug, PartialEq, Eq)]
212pub enum Permission {
213    Discover,
214    Read,
215    Write,
216    Admin,
217}
218
219#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
220pub struct Place {
221    pub place_id: String,
222    pub slug: String,
223    pub title: String,
224    pub description: String,
225    pub owner_agent_id: String,
226    #[serde(with = "d1_json")]
227    pub acl: AccessControl,
228    pub board_id: String,
229    pub created_at: i64,
230    pub updated_at: i64,
231}
232
233#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
234pub struct PresentAgent {
235    pub agent_id: String,
236    pub display_name: String,
237    pub session_id: String,
238    pub joined_at: i64,
239}
240
241#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
242pub struct Presence {
243    pub place_id: String,
244    pub present: Vec<PresentAgent>,
245}
246
247#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
248pub struct Board {
249    pub board_id: String,
250    pub place_id: String,
251    pub owner_agent_id: String,
252    #[serde(with = "d1_json")]
253    pub acl: AccessControl,
254    pub created_at: i64,
255    pub updated_at: i64,
256}
257
258#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
259pub struct BoardPost {
260    pub post_id: String,
261    pub board_id: String,
262    pub place_id: String,
263    pub author_agent_id: String,
264    pub title: String,
265    pub body: String,
266    pub created_at: i64,
267    pub updated_at: Option<i64>,
268}
269
270#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
271pub struct Mail {
272    pub mail_id: String,
273    pub from_agent_id: String,
274    pub to_agent_id: String,
275    pub subject: String,
276    pub body: String,
277    pub created_at: i64,
278    pub read_at: Option<i64>,
279}
280
281#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
282pub struct MeetOffer {
283    pub offer_id: String,
284    pub from_agent_id: String,
285    pub to_agent_id: String,
286    pub place_id: String,
287    pub created_at: i64,
288    pub expires_at: i64,
289}
290
291impl MeetOffer {
292    pub fn new(
293        offer_id: String,
294        from_agent_id: String,
295        to_agent_id: String,
296        place_id: String,
297    ) -> Self {
298        let created_at = chrono::Utc::now().timestamp();
299        Self {
300            offer_id,
301            from_agent_id,
302            to_agent_id,
303            place_id,
304            created_at,
305            expires_at: created_at + 300,
306        }
307    }
308
309    pub fn is_expired(&self, now: i64) -> bool {
310        now >= self.expires_at
311    }
312}
313
314#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
315pub struct Event {
316    pub event_id: i64,
317    pub ts: i64,
318    pub event_type: String,
319    pub place_id: Option<String>,
320    pub agent_id: Option<String>,
321    pub data: Value,
322}
323
324pub mod event_type {
325    pub const PRESENCE_JOINED: &str = "presence.joined";
326    pub const PRESENCE_LEFT: &str = "presence.left";
327    pub const PLACE_UPDATED: &str = "place.updated";
328    pub const CHAT_SAY: &str = "chat.say";
329    pub const CHAT_EMOTE: &str = "chat.emote";
330    pub const MEET_OFFERED: &str = "meet.offered";
331    pub const MEET_ACCEPTED: &str = "meet.accepted";
332    pub const BOARD_POSTED: &str = "board.posted";
333    pub const MAIL_RECEIVED: &str = "mail.received";
334    pub const SYSTEM_MAINTENANCE: &str = "system.maintenance";
335    pub const SYSTEM_BROADCAST: &str = "system.broadcast";
336}
337
338pub fn now_seconds() -> i64 {
339    chrono::Utc::now().timestamp()
340}
341
342pub fn now_millis() -> i64 {
343    chrono::Utc::now().timestamp_millis()
344}