Skip to main content

nexus_tui_common/
lib.rs

1// common/src/lib.rs
2
3use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6pub mod config;
7pub use config::{ServerConfig, ClientConfig};
8
9// Simple color representation that works for both client and server
10#[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// For client compatibility with ratatui
36#[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()), // Default
50        }
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, // Default
77        }
78    }
79}
80
81// --- User & Role Management ---
82
83#[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/// Complete user profile with all fields (for profile editing/viewing)
110#[derive(Serialize, Deserialize, Debug, Clone)]
111pub struct UserProfile {
112    pub id: Uuid,
113    pub username: String,
114    pub hash: String, // Password hash (server-side only)
115    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/// Lightweight user info without profile images - use this for lists, mentions, etc.
127#[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, // Now UserStatus implements Copy, so no clone needed
156        }
157    }
158}
159
160// --- Data Structures ---
161
162#[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/// Lightweight forum structure for initial loading (no profile images)
171#[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/// Lightweight thread structure for initial loading (no profile images)
189#[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/// Lightweight post structure for initial loading (no profile images)
208#[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>, // base64
232    pub banner: Option<String>, // base64
233    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, // This is the author ID - frontend will look up user info
261    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, // This is the author ID - frontend will look up user info
269    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// --- Server Invites ---
295
296#[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// --- Network Protocol Definitions ---
316
317#[derive(Serialize, Deserialize, Debug)]
318pub enum ClientMessage {
319    // Auth
320    Register { username: String, password: String },
321    Login { username: String, password: String },
322    Logout,
323    // User
324    UpdatePassword(String),
325    UpdateColor(UserColor), // Changed from SerializableColor to UserColor
326    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    // Forums
336    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 }, // New: Reply to specific post
342    // Chat
343    SendDirectMessage { to: Uuid, content: String },
344    SendChannelMessage { channel_id: Uuid, content: String },
345    // Server invites
346    SendServerInvite { to_user_id: Uuid, server_id: Uuid },
347    RespondToServerInvite { invite_id: Uuid, accept: bool },
348    // New: Accept/decline invite from a specific user (for DM commands)
349    AcceptServerInviteFromUser { from_user_id: Uuid },
350    DeclineServerInviteFromUser { from_user_id: Uuid },
351    // Moderation
352    DeletePost(Uuid),
353    DeleteThread(Uuid),
354    // User management
355    GetUserList, // Request the list of connected users
356    GetProfile { user_id: Uuid },
357    GetServers, // Request all servers the user is a member of
358    // --- ENHANCED PAGINATION SUPPORT ---
359    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    // --- LEGACY COMPATIBILITY ---
372    GetChannelMessages { channel_id: Uuid, before: Option<i64> },
373    GetChannelUserList { channel_id: Uuid },
374    GetDMUserList, // Request list of users you have DMs with
375    GetDirectMessages { user_id: Uuid, before: Option<i64> }, // Fetch DMs with a user, paginated by timestamp
376    // --- NOTIFICATIONS ---
377    GetNotifications { before: Option<i64> },
378    MarkNotificationRead { notification_id: Uuid },
379    // --- CACHE MANAGEMENT ---
380    InvalidateImageCache { keys: Vec<String> },
381    GetCacheStats,
382    // Profile picture requests (for efficient loading)
383    GetUserAvatars { user_ids: Vec<Uuid> },
384}
385
386/// Pagination cursor for network protocol
387#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
388pub enum PaginationCursor {
389    Timestamp(i64),
390    Offset(usize),
391    Start,
392}
393
394/// Pagination direction for network protocol
395#[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    // Auth
404    AuthSuccess(User),
405    AuthFailure(String),
406    // General
407    Forums(Vec<Forum>),
408    ForumsLightweight(Vec<ForumLightweight>), // Lightweight version without profile images
409    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), // Message, is_error
414    // Server invites
415    ServerInviteReceived(ServerInvite),
416    ServerInviteResponse { invite_id: Uuid, accepted: bool, user: User },
417    // User management
418    UserList(Vec<User>), // List of connected users
419    UserJoined(User),    // A user joined
420    UserLeft(Uuid),      // A user left (by id)
421    Profile(UserProfile),
422    UserUpdated(User), // Broadcast when a user updates their profile
423    Servers(Vec<Server>), // List of servers and their channels
424    NewChannelMessage(ChannelMessage),
425    // --- ENHANCED PAGINATION RESPONSES ---
426    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    // --- LEGACY COMPATIBILITY ---
443    ChannelMessages { channel_id: Uuid, messages: Vec<ChannelMessage>, history_complete: bool },
444    ChannelUserList { channel_id: Uuid, users: Vec<User> },
445    DMUserList(Vec<User>), // List of users you have DMs with
446    DirectMessages { user_id: Uuid, messages: Vec<DirectMessage>, history_complete: bool },
447    // --- NOTIFICATIONS ---
448    Notifications { notifications: Vec<Notification>, history_complete: bool },
449    // --- CACHE MANAGEMENT ---
450    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>)> }, // user_id, profile_pic
458    // --- PERFORMANCE METRICS ---
459    PerformanceMetrics {
460        query_time_ms: u64,
461        cache_hit_rate: f64,
462        message_count: usize,
463    },
464}
465
466
467// Initial data creation
468pub 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, // Default to connected
477    };
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, // No parent post
495                    }],
496                },
497            ],
498        },
499    ]
500}