1use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6pub mod config;
7pub use config::{ServerConfig, ClientConfig};
8
9#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
11pub struct UserColor(pub String);
12
13impl UserColor {
14 pub fn new(color: impl Into<String>) -> Self {
15 Self(color.into())
16 }
17
18 pub fn as_str(&self) -> &str {
19 &self.0
20 }
21}
22
23impl From<String> for UserColor {
24 fn from(s: String) -> Self {
25 Self(s)
26 }
27}
28
29impl From<&str> for UserColor {
30 fn from(s: &str) -> Self {
31 Self(s.to_string())
32 }
33}
34
35#[cfg(feature = "ratatui")]
37impl From<ratatui::style::Color> for UserColor {
38 fn from(color: ratatui::style::Color) -> Self {
39 match color {
40 ratatui::style::Color::Rgb(r, g, b) => Self(format!("#{:02X}{:02X}{:02X}", r, g, b)),
41 ratatui::style::Color::Red => Self("Red".to_string()),
42 ratatui::style::Color::Green => Self("Green".to_string()),
43 ratatui::style::Color::Blue => Self("Blue".to_string()),
44 ratatui::style::Color::Yellow => Self("Yellow".to_string()),
45 ratatui::style::Color::Cyan => Self("Cyan".to_string()),
46 ratatui::style::Color::Magenta => Self("Magenta".to_string()),
47 ratatui::style::Color::White => Self("White".to_string()),
48 ratatui::style::Color::Black => Self("Black".to_string()),
49 _ => Self("Cyan".to_string()), }
51 }
52}
53
54#[cfg(feature = "ratatui")]
55impl From<UserColor> for ratatui::style::Color {
56 fn from(user_color: UserColor) -> ratatui::style::Color {
57 if user_color.0.starts_with('#') && user_color.0.len() == 7 {
58 if let (Ok(r), Ok(g), Ok(b)) = (
59 u8::from_str_radix(&user_color.0[1..3], 16),
60 u8::from_str_radix(&user_color.0[3..5], 16),
61 u8::from_str_radix(&user_color.0[5..7], 16),
62 ) {
63 return ratatui::style::Color::Rgb(r, g, b);
64 }
65 }
66
67 match user_color.0.as_str() {
68 "Red" => ratatui::style::Color::Red,
69 "Green" => ratatui::style::Color::Green,
70 "Blue" => ratatui::style::Color::Blue,
71 "Yellow" => ratatui::style::Color::Yellow,
72 "Cyan" => ratatui::style::Color::Cyan,
73 "Magenta" => ratatui::style::Color::Magenta,
74 "White" => ratatui::style::Color::White,
75 "Black" => ratatui::style::Color::Black,
76 _ => ratatui::style::Color::Cyan, }
78 }
79}
80
81#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
84pub enum UserRole {
85 User,
86 Moderator,
87 Admin,
88}
89
90#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
91pub enum UserStatus {
92 Connected,
93 Away,
94 Busy,
95 Offline,
96}
97
98#[derive(Serialize, Deserialize, Debug, Clone)]
99pub struct User {
100 pub id: Uuid,
101 pub username: String,
102 pub color: UserColor,
103 pub role: UserRole,
104 pub profile_pic: Option<String>,
105 pub cover_banner: Option<String>,
106 pub status: UserStatus,
107}
108
109#[derive(Serialize, Deserialize, Debug, Clone)]
111pub struct UserProfile {
112 pub id: Uuid,
113 pub username: String,
114 pub hash: String, pub color: UserColor,
116 pub role: UserRole,
117 pub bio: Option<String>,
118 pub url1: Option<String>,
119 pub url2: Option<String>,
120 pub url3: Option<String>,
121 pub location: Option<String>,
122 pub profile_pic: Option<String>,
123 pub cover_banner: Option<String>,
124}
125
126#[derive(Serialize, Deserialize, Debug, Clone)]
128pub struct UserInfo {
129 pub id: Uuid,
130 pub username: String,
131 pub color: UserColor,
132 pub role: UserRole,
133 pub status: UserStatus,
134}
135
136impl From<User> for UserInfo {
137 fn from(user: User) -> Self {
138 Self {
139 id: user.id,
140 username: user.username,
141 color: user.color,
142 role: user.role,
143 status: user.status,
144 }
145 }
146}
147
148impl From<&User> for UserInfo {
149 fn from(user: &User) -> Self {
150 Self {
151 id: user.id,
152 username: user.username.clone(),
153 color: user.color.clone(),
154 role: user.role,
155 status: user.status, }
157 }
158}
159
160#[derive(Debug, Serialize, Deserialize, Clone)]
163pub struct Forum {
164 pub id: Uuid,
165 pub name: String,
166 pub description: String,
167 pub threads: Vec<Thread>,
168}
169
170#[derive(Debug, Serialize, Deserialize, Clone)]
172pub struct ForumLightweight {
173 pub id: Uuid,
174 pub name: String,
175 pub description: String,
176 pub threads: Vec<ThreadLightweight>,
177}
178
179#[derive(Debug, Serialize, Deserialize, Clone)]
180pub struct Thread {
181 pub id: Uuid,
182 pub title: String,
183 pub author: User,
184 pub posts: Vec<Post>,
185 pub timestamp: i64,
186}
187
188#[derive(Debug, Serialize, Deserialize, Clone)]
190pub struct ThreadLightweight {
191 pub id: Uuid,
192 pub title: String,
193 pub author: UserInfo,
194 pub posts: Vec<PostLightweight>,
195 pub timestamp: i64,
196}
197
198#[derive(Debug, Serialize, Deserialize, Clone)]
199pub struct Post {
200 pub id: Uuid,
201 pub author: User,
202 pub content: String,
203 pub timestamp: i64,
204 pub reply_to: Option<Uuid>,
205}
206
207#[derive(Debug, Serialize, Deserialize, Clone)]
209pub struct PostLightweight {
210 pub id: Uuid,
211 pub author: UserInfo,
212 pub content: String,
213 pub timestamp: i64,
214 pub reply_to: Option<Uuid>,
215}
216
217#[derive(Debug, Serialize, Deserialize, Clone)]
218pub struct ChatMessage {
219 pub author: String,
220 pub content: String,
221 pub color: UserColor,
222}
223
224#[derive(Debug, Serialize, Deserialize, Clone)]
225pub struct Server {
226 pub id: Uuid,
227 pub name: String,
228 pub description: String,
229 pub public: bool,
230 pub invite_code: Option<String>,
231 pub icon: Option<String>, pub banner: Option<String>, pub owner: Uuid,
234 pub mods: Vec<Uuid>,
235 pub userlist: Vec<Uuid>,
236 pub channels: Vec<Channel>,
237}
238
239#[derive(Debug, Serialize, Deserialize, Clone)]
240pub struct Channel {
241 pub id: Uuid,
242 pub server_id: Uuid,
243 pub name: String,
244 pub description: String,
245 pub permissions: ChannelPermissions,
246 pub userlist: Vec<Uuid>,
247 pub messages: Vec<ChannelMessage>,
248}
249
250#[derive(Debug, Serialize, Deserialize, Clone)]
251pub struct ChannelPermissions {
252 pub can_read: Vec<Uuid>,
253 pub can_write: Vec<Uuid>,
254}
255
256#[derive(Debug, Serialize, Deserialize, Clone)]
257pub struct ChannelMessage {
258 pub id: Uuid,
259 pub channel_id: Uuid,
260 pub sent_by: Uuid, pub timestamp: i64,
262 pub content: String,
263}
264
265#[derive(Debug, Serialize, Deserialize, Clone)]
266pub struct DirectMessage {
267 pub id: Uuid,
268 pub from: Uuid, pub to: Uuid,
270 pub timestamp: i64,
271 pub content: String,
272}
273
274#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
275pub enum NotificationType {
276 ThreadReply,
277 DM,
278 Announcement,
279 Mention,
280 Other(String),
281}
282
283#[derive(Debug, Serialize, Deserialize, Clone)]
284pub struct Notification {
285 pub id: Uuid,
286 pub user_id: Uuid,
287 pub notif_type: NotificationType,
288 pub related_id: Uuid,
289 pub created_at: i64,
290 pub read: bool,
291 pub extra: Option<String>,
292}
293
294#[derive(Debug, Serialize, Deserialize, Clone)]
297pub struct ServerInvite {
298 pub id: Uuid,
299 pub from_user: User,
300 pub to_user_id: Uuid,
301 pub server: Server,
302 pub timestamp: i64,
303 pub status: ServerInviteStatus,
304}
305
306#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
307pub enum ServerInviteStatus {
308 Pending,
309 Accepted,
310 Declined,
311 Expired,
312}
313
314
315#[derive(Serialize, Deserialize, Debug)]
318pub enum ClientMessage {
319 Register { username: String, password: String },
321 Login { username: String, password: String },
322 Logout,
323 UpdatePassword(String),
325 UpdateColor(UserColor), UpdateProfile {
327 bio: Option<String>,
328 url1: Option<String>,
329 url2: Option<String>,
330 url3: Option<String>,
331 location: Option<String>,
332 profile_pic: Option<String>,
333 cover_banner: Option<String>,
334 },
335 GetForums,
337 CreateForum { name: String, description: String },
338 DeleteForum { forum_id: Uuid },
339 CreateThread { forum_id: Uuid, title: String, content: String },
340 CreatePost { thread_id: Uuid, content: String },
341 CreatePostReply { thread_id: Uuid, content: String, reply_to: Uuid }, SendDirectMessage { to: Uuid, content: String },
344 SendChannelMessage { channel_id: Uuid, content: String },
345 SendServerInvite { to_user_id: Uuid, server_id: Uuid },
347 RespondToServerInvite { invite_id: Uuid, accept: bool },
348 AcceptServerInviteFromUser { from_user_id: Uuid },
350 DeclineServerInviteFromUser { from_user_id: Uuid },
351 DeletePost(Uuid),
353 DeleteThread(Uuid),
354 GetUserList, GetProfile { user_id: Uuid },
357 GetServers, GetChannelMessagesPaginated {
360 channel_id: Uuid,
361 cursor: PaginationCursor,
362 limit: Option<usize>,
363 direction: PaginationDirection,
364 },
365 GetDirectMessagesPaginated {
366 user_id: Uuid,
367 cursor: PaginationCursor,
368 limit: Option<usize>,
369 direction: PaginationDirection,
370 },
371 GetChannelMessages { channel_id: Uuid, before: Option<i64> },
373 GetChannelUserList { channel_id: Uuid },
374 GetDMUserList, GetDirectMessages { user_id: Uuid, before: Option<i64> }, GetNotifications { before: Option<i64> },
378 MarkNotificationRead { notification_id: Uuid },
379 InvalidateImageCache { keys: Vec<String> },
381 GetCacheStats,
382 GetUserAvatars { user_ids: Vec<Uuid> },
384}
385
386#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
388pub enum PaginationCursor {
389 Timestamp(i64),
390 Offset(usize),
391 Start,
392}
393
394#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
396pub enum PaginationDirection {
397 Forward,
398 Backward,
399}
400
401#[derive(Serialize, Deserialize, Debug, Clone)]
402pub enum ServerMessage {
403 AuthSuccess(User),
405 AuthFailure(String),
406 Forums(Vec<Forum>),
408 ForumsLightweight(Vec<ForumLightweight>), NewChatMessage(ChatMessage),
410 DirectMessage(DirectMessage),
411 MentionNotification { from: User, content: String },
412 ForumReplyNotification { thread_id: Uuid, from_username: String, message: String, from_user_profile_pic: Option<String> },
413 Notification(String, bool), ServerInviteReceived(ServerInvite),
416 ServerInviteResponse { invite_id: Uuid, accepted: bool, user: User },
417 UserList(Vec<User>), UserJoined(User), UserLeft(Uuid), Profile(UserProfile),
422 UserUpdated(User), Servers(Vec<Server>), NewChannelMessage(ChannelMessage),
425 ChannelMessagesPaginated {
427 channel_id: Uuid,
428 messages: Vec<ChannelMessage>,
429 has_more: bool,
430 next_cursor: Option<PaginationCursor>,
431 prev_cursor: Option<PaginationCursor>,
432 total_count: Option<usize>,
433 },
434 DirectMessagesPaginated {
435 user_id: Uuid,
436 messages: Vec<DirectMessage>,
437 has_more: bool,
438 next_cursor: Option<PaginationCursor>,
439 prev_cursor: Option<PaginationCursor>,
440 total_count: Option<usize>,
441 },
442 ChannelMessages { channel_id: Uuid, messages: Vec<ChannelMessage>, history_complete: bool },
444 ChannelUserList { channel_id: Uuid, users: Vec<User> },
445 DMUserList(Vec<User>), DirectMessages { user_id: Uuid, messages: Vec<DirectMessage>, history_complete: bool },
447 Notifications { notifications: Vec<Notification>, history_complete: bool },
449 CacheStats {
451 total_entries: usize,
452 total_size_mb: f64,
453 hit_ratio: f64,
454 expired_entries: usize,
455 },
456 ImageCacheInvalidated { keys: Vec<String> },
457 UserAvatars { avatars: Vec<(Uuid, Option<String>)> }, PerformanceMetrics {
460 query_time_ms: u64,
461 cache_hit_rate: f64,
462 message_count: usize,
463 },
464}
465
466
467pub fn create_initial_forums() -> Vec<Forum> {
469 let system_user = User {
470 id: Uuid::new_v4(),
471 username: "system".to_string(),
472 color: UserColor::new("Red"),
473 role: UserRole::Admin,
474 profile_pic: Some("system.png".to_string()),
475 cover_banner: Some("system_banner.png".to_string()),
476 status: UserStatus::Connected, };
478 vec![
479 Forum {
480 id: Uuid::new_v4(),
481 name: "Decompiling Corporate ICE".to_string(),
482 description: "Tips and tricks for getting past the big boys' security.".to_string(),
483 threads: vec![
484 Thread {
485 id: Uuid::new_v4(),
486 title: "Militech's 'Aegis' Firewall - Any exploits?".to_string(),
487 author: system_user.clone(),
488 timestamp: 1633072800,
489 posts: vec![ Post {
490 id: Uuid::new_v4(),
491 author: system_user.clone(),
492 content: "I've been probing their new Aegis system. It's tough.".to_string(),
493 timestamp: 1633072800,
494 reply_to: None, }],
496 },
497 ],
498 },
499 ]
500}