1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4pub 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
38pub 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}