Skip to main content

legion_protocol/
command.rs

1//! IRC command parsing and representation
2//!
3//! This module provides types and functionality for working with IRC commands,
4//! including both standard IRC commands and IRCv3 extensions.
5
6// use std::str::FromStr; // Not currently used
7
8#[cfg(feature = "serde")]
9use serde::{Deserialize, Serialize};
10
11/// Represents various IRC commands with their parameters
12#[derive(Debug, Clone, PartialEq, Eq)]
13#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
14pub enum Command {
15    // Connection registration
16    /// NICK command - set nickname
17    Nick(String),
18    /// USER command - user registration
19    User { username: String, realname: String },
20    /// PASS command - connection password
21    Pass(String),
22    /// QUIT command - disconnect
23    Quit(Option<String>),
24    /// PING command - server ping
25    Ping(String),
26    /// PONG command - ping response
27    Pong(String),
28    
29    // Channel operations
30    /// JOIN command - join channels
31    Join(Vec<String>, Vec<String>), // channels, keys
32    /// PART command - leave channels
33    Part(Vec<String>, Option<String>), // channels, message
34    /// TOPIC command - get/set channel topic
35    Topic { channel: String, topic: Option<String> },
36    /// NAMES command - list channel members
37    Names(Vec<String>),
38    /// LIST command - list channels
39    List(Option<Vec<String>>),
40    
41    // Messaging
42    /// PRIVMSG command - send message
43    Privmsg { target: String, message: String },
44    /// NOTICE command - send notice
45    Notice { target: String, message: String },
46    
47    // User queries
48    /// WHO command - query user information
49    Who(Option<String>),
50    /// WHOIS command - detailed user information
51    Whois(Vec<String>),
52    /// WHOWAS command - historical user information
53    Whowas(String, Option<i32>),
54    /// QUERY command - open private message window
55    Query(String),
56    
57    // Channel management
58    /// KICK command - remove user from channel
59    Kick { channel: String, user: String, reason: Option<String> },
60    /// MODE command - change modes
61    Mode { target: String, modes: Option<String>, params: Vec<String> },
62    /// INVITE command - invite user to channel
63    Invite { nick: String, channel: String },
64    
65    // Server queries
66    /// MOTD command - message of the day
67    Motd(Option<String>),
68    /// VERSION command - server version
69    Version(Option<String>),
70    /// STATS command - server statistics
71    Stats(Option<String>, Option<String>),
72    /// TIME command - server time
73    Time(Option<String>),
74    /// INFO command - server information
75    Info(Option<String>),
76    
77    // IRCv3 commands
78    /// CAP command - capability negotiation
79    Cap { subcommand: String, params: Vec<String> },
80    /// AUTHENTICATE command - SASL authentication
81    Authenticate(String),
82    /// ACCOUNT command - account notification
83    Account(String),
84    /// MONITOR command - nickname monitoring
85    Monitor { subcommand: String, targets: Vec<String> },
86    /// METADATA command - user metadata
87    Metadata { target: String, subcommand: String, params: Vec<String> },
88    /// TAGMSG command - tag-only message
89    TagMsg { target: String },
90    /// BATCH command - message batching
91    Batch { reference: String, batch_type: Option<String>, params: Vec<String> },
92    
93    // 2024 Bleeding-edge IRCv3 commands
94    /// REDACT command - message redaction
95    Redact { target: String, msgid: String, reason: Option<String> },
96    /// MARKREAD command - mark messages as read
97    MarkRead { target: String, timestamp: Option<String> },
98    /// SETNAME command - change real name
99    SetName { realname: String },
100    /// CHATHISTORY command - request chat history
101    ChatHistory { subcommand: String, target: String, params: Vec<String> },
102    
103    // Operator commands
104    /// OPER command - gain operator privileges
105    Oper { name: String, password: String },
106    /// KILL command - forcibly disconnect user
107    Kill { nick: String, reason: String },
108    /// REHASH command - reload server configuration
109    Rehash,
110    /// RESTART command - restart server
111    Restart,
112    /// DIE command - shutdown server
113    Die,
114    
115    // CTCP commands
116    /// CTCP request
117    CtcpRequest { target: String, command: String, params: String },
118    /// CTCP response
119    CtcpResponse { target: String, command: String, params: String },
120    
121    // Fallback for unknown commands
122    /// Unknown command
123    Unknown(String, Vec<String>),
124}
125
126impl Command {
127    /// Parse a command from its string representation and parameters
128    pub fn parse(command: &str, params: Vec<String>) -> Self {
129        match command.to_uppercase().as_str() {
130            "NICK" => {
131                if let Some(nick) = params.first() {
132                    Command::Nick(nick.clone())
133                } else {
134                    Command::Unknown(command.to_string(), params)
135                }
136            }
137            "USER" => {
138                if params.len() >= 4 {
139                    Command::User {
140                        username: params[0].clone(),
141                        realname: params[3].clone(),
142                    }
143                } else {
144                    Command::Unknown(command.to_string(), params)
145                }
146            }
147            "PASS" => {
148                if let Some(pass) = params.first() {
149                    Command::Pass(pass.clone())
150                } else {
151                    Command::Unknown(command.to_string(), params)
152                }
153            }
154            "QUIT" => Command::Quit(params.first().cloned()),
155            "PING" => {
156                if let Some(token) = params.first() {
157                    Command::Ping(token.clone())
158                } else {
159                    Command::Unknown(command.to_string(), params)
160                }
161            }
162            "PONG" => {
163                if let Some(token) = params.first() {
164                    Command::Pong(token.clone())
165                } else {
166                    Command::Unknown(command.to_string(), params)
167                }
168            }
169            "JOIN" => {
170                if let Some(channels) = params.first() {
171                    let channels: Vec<String> = channels.split(',').map(|s| s.to_string()).collect();
172                    let keys: Vec<String> = params.get(1)
173                        .map(|k| k.split(',').map(|s| s.to_string()).collect())
174                        .unwrap_or_default();
175                    Command::Join(channels, keys)
176                } else {
177                    Command::Unknown(command.to_string(), params)
178                }
179            }
180            "PART" => {
181                if let Some(channels) = params.first() {
182                    let channels: Vec<String> = channels.split(',').map(|s| s.to_string()).collect();
183                    let message = params.get(1).cloned();
184                    Command::Part(channels, message)
185                } else {
186                    Command::Unknown(command.to_string(), params)
187                }
188            }
189            "TOPIC" => {
190                if let Some(channel) = params.first() {
191                    Command::Topic {
192                        channel: channel.clone(),
193                        topic: params.get(1).cloned(),
194                    }
195                } else {
196                    Command::Unknown(command.to_string(), params)
197                }
198            }
199            "NAMES" => {
200                if let Some(channels) = params.first() {
201                    let channels: Vec<String> = channels.split(',').map(|s| s.to_string()).collect();
202                    Command::Names(channels)
203                } else {
204                    Command::Names(Vec::new())
205                }
206            }
207            "LIST" => {
208                if let Some(channels) = params.first() {
209                    let channels: Vec<String> = channels.split(',').map(|s| s.to_string()).collect();
210                    Command::List(Some(channels))
211                } else {
212                    Command::List(None)
213                }
214            }
215            "PRIVMSG" => {
216                if params.len() >= 2 {
217                    Command::Privmsg {
218                        target: params[0].clone(),
219                        message: params[1].clone(),
220                    }
221                } else {
222                    Command::Unknown(command.to_string(), params)
223                }
224            }
225            "NOTICE" => {
226                if params.len() >= 2 {
227                    Command::Notice {
228                        target: params[0].clone(),
229                        message: params[1].clone(),
230                    }
231                } else {
232                    Command::Unknown(command.to_string(), params)
233                }
234            }
235            "WHO" => Command::Who(params.first().cloned()),
236            "WHOIS" => {
237                if !params.is_empty() {
238                    Command::Whois(params)
239                } else {
240                    Command::Unknown(command.to_string(), params)
241                }
242            }
243            "WHOWAS" => {
244                if let Some(nick) = params.first() {
245                    let count = params.get(1).and_then(|s| s.parse().ok());
246                    Command::Whowas(nick.clone(), count)
247                } else {
248                    Command::Unknown(command.to_string(), params)
249                }
250            }
251            "QUERY" => {
252                if let Some(target) = params.first() {
253                    Command::Query(target.clone())
254                } else {
255                    Command::Unknown(command.to_string(), params)
256                }
257            }
258            "KICK" => {
259                if params.len() >= 2 {
260                    Command::Kick {
261                        channel: params[0].clone(),
262                        user: params[1].clone(),
263                        reason: params.get(2).cloned(),
264                    }
265                } else {
266                    Command::Unknown(command.to_string(), params)
267                }
268            }
269            "MODE" => {
270                if let Some(target) = params.first() {
271                    Command::Mode {
272                        target: target.clone(),
273                        modes: params.get(1).cloned(),
274                        params: params[2..].to_vec(),
275                    }
276                } else {
277                    Command::Unknown(command.to_string(), params)
278                }
279            }
280            "INVITE" => {
281                if params.len() >= 2 {
282                    Command::Invite {
283                        nick: params[0].clone(),
284                        channel: params[1].clone(),
285                    }
286                } else {
287                    Command::Unknown(command.to_string(), params)
288                }
289            }
290            "MOTD" => Command::Motd(params.first().cloned()),
291            "VERSION" => Command::Version(params.first().cloned()),
292            "STATS" => Command::Stats(params.first().cloned(), params.get(1).cloned()),
293            "TIME" => Command::Time(params.first().cloned()),
294            "INFO" => Command::Info(params.first().cloned()),
295            
296            // IRCv3 commands
297            "CAP" => {
298                if let Some(subcommand) = params.first() {
299                    Command::Cap {
300                        subcommand: subcommand.clone(),
301                        params: params[1..].to_vec(),
302                    }
303                } else {
304                    Command::Unknown(command.to_string(), params)
305                }
306            }
307            "AUTHENTICATE" => {
308                if let Some(data) = params.first() {
309                    Command::Authenticate(data.clone())
310                } else {
311                    Command::Unknown(command.to_string(), params)
312                }
313            }
314            "ACCOUNT" => {
315                if let Some(account) = params.first() {
316                    Command::Account(account.clone())
317                } else {
318                    Command::Unknown(command.to_string(), params)
319                }
320            }
321            "MONITOR" => {
322                if let Some(subcommand) = params.first() {
323                    Command::Monitor {
324                        subcommand: subcommand.clone(),
325                        targets: params[1..].to_vec(),
326                    }
327                } else {
328                    Command::Unknown(command.to_string(), params)
329                }
330            }
331            "METADATA" => {
332                if params.len() >= 2 {
333                    Command::Metadata {
334                        target: params[0].clone(),
335                        subcommand: params[1].clone(),
336                        params: params[2..].to_vec(),
337                    }
338                } else {
339                    Command::Unknown(command.to_string(), params)
340                }
341            }
342            "TAGMSG" => {
343                if let Some(target) = params.first() {
344                    Command::TagMsg {
345                        target: target.clone(),
346                    }
347                } else {
348                    Command::Unknown(command.to_string(), params)
349                }
350            }
351            "BATCH" => {
352                if let Some(reference) = params.first() {
353                    Command::Batch {
354                        reference: reference.clone(),
355                        batch_type: params.get(1).cloned(),
356                        params: params[2..].to_vec(),
357                    }
358                } else {
359                    Command::Unknown(command.to_string(), params)
360                }
361            }
362            
363            // 2024 Bleeding-edge IRCv3 commands
364            "REDACT" => {
365                if params.len() >= 2 {
366                    Command::Redact {
367                        target: params[0].clone(),
368                        msgid: params[1].clone(),
369                        reason: params.get(2).cloned(),
370                    }
371                } else {
372                    Command::Unknown(command.to_string(), params)
373                }
374            }
375            "MARKREAD" => {
376                if !params.is_empty() {
377                    Command::MarkRead {
378                        target: params[0].clone(),
379                        timestamp: params.get(1).cloned(),
380                    }
381                } else {
382                    Command::Unknown(command.to_string(), params)
383                }
384            }
385            "SETNAME" => {
386                if let Some(realname) = params.first() {
387                    Command::SetName {
388                        realname: realname.clone(),
389                    }
390                } else {
391                    Command::Unknown(command.to_string(), params)
392                }
393            }
394            "CHATHISTORY" => {
395                if params.len() >= 2 {
396                    Command::ChatHistory {
397                        subcommand: params[0].clone(),
398                        target: params[1].clone(),
399                        params: params[2..].to_vec(),
400                    }
401                } else {
402                    Command::Unknown(command.to_string(), params)
403                }
404            }
405            
406            // Operator commands
407            "OPER" => {
408                if params.len() >= 2 {
409                    Command::Oper {
410                        name: params[0].clone(),
411                        password: params[1].clone(),
412                    }
413                } else {
414                    Command::Unknown(command.to_string(), params)
415                }
416            }
417            "KILL" => {
418                if params.len() >= 2 {
419                    Command::Kill {
420                        nick: params[0].clone(),
421                        reason: params[1].clone(),
422                    }
423                } else {
424                    Command::Unknown(command.to_string(), params)
425                }
426            }
427            "REHASH" => Command::Rehash,
428            "RESTART" => Command::Restart,
429            "DIE" => Command::Die,
430            
431            _ => Command::Unknown(command.to_string(), params),
432        }
433    }
434
435    /// Get the command name as a string
436    pub fn command_name(&self) -> &str {
437        match self {
438            Command::Nick(_) => "NICK",
439            Command::User { .. } => "USER",
440            Command::Pass(_) => "PASS",
441            Command::Quit(_) => "QUIT",
442            Command::Ping(_) => "PING",
443            Command::Pong(_) => "PONG",
444            Command::Join(_, _) => "JOIN",
445            Command::Part(_, _) => "PART",
446            Command::Topic { .. } => "TOPIC",
447            Command::Names(_) => "NAMES",
448            Command::List(_) => "LIST",
449            Command::Privmsg { .. } => "PRIVMSG",
450            Command::Notice { .. } => "NOTICE",
451            Command::Who(_) => "WHO",
452            Command::Whois(_) => "WHOIS",
453            Command::Whowas(_, _) => "WHOWAS",
454            Command::Query(_) => "QUERY",
455            Command::Kick { .. } => "KICK",
456            Command::Mode { .. } => "MODE",
457            Command::Invite { .. } => "INVITE",
458            Command::Motd(_) => "MOTD",
459            Command::Version(_) => "VERSION",
460            Command::Stats(_, _) => "STATS",
461            Command::Time(_) => "TIME",
462            Command::Info(_) => "INFO",
463            Command::Cap { .. } => "CAP",
464            Command::Authenticate(_) => "AUTHENTICATE",
465            Command::Account(_) => "ACCOUNT",
466            Command::Monitor { .. } => "MONITOR",
467            Command::Metadata { .. } => "METADATA",
468            Command::TagMsg { .. } => "TAGMSG",
469            Command::Batch { .. } => "BATCH",
470            Command::Redact { .. } => "REDACT",
471            Command::MarkRead { .. } => "MARKREAD",
472            Command::SetName { .. } => "SETNAME",
473            Command::ChatHistory { .. } => "CHATHISTORY",
474            Command::Oper { .. } => "OPER",
475            Command::Kill { .. } => "KILL",
476            Command::Rehash => "REHASH",
477            Command::Restart => "RESTART",
478            Command::Die => "DIE",
479            Command::CtcpRequest { .. } => "PRIVMSG", // CTCP is sent via PRIVMSG
480            Command::CtcpResponse { .. } => "NOTICE", // CTCP response via NOTICE
481            Command::Unknown(cmd, _) => cmd,
482        }
483    }
484
485    /// Check if this is a channel-related command
486    pub fn is_channel_command(&self) -> bool {
487        match self {
488            Command::Join(_, _) |
489            Command::Part(_, _) |
490            Command::Topic { .. } |
491            Command::Names(_) |
492            Command::Kick { .. } => true,
493            Command::Mode { target, .. } => target.starts_with('#') || target.starts_with('&'),
494            _ => false,
495        }
496    }
497
498    /// Check if this is a messaging command
499    pub fn is_message_command(&self) -> bool {
500        matches!(self, Command::Privmsg { .. } | Command::Notice { .. })
501    }
502
503    /// Check if this is an IRCv3 command
504    pub fn is_ircv3_command(&self) -> bool {
505        matches!(self,
506            Command::Cap { .. } |
507            Command::Authenticate(_) |
508            Command::Account(_) |
509            Command::Monitor { .. } |
510            Command::Metadata { .. } |
511            Command::TagMsg { .. } |
512            Command::Batch { .. } |
513            Command::Redact { .. } |
514            Command::MarkRead { .. } |
515            Command::SetName { .. } |
516            Command::ChatHistory { .. }
517        )
518    }
519}
520
521#[cfg(test)]
522mod tests {
523    use super::*;
524
525    #[test]
526    fn test_basic_command_parsing() {
527        let cmd = Command::parse("PRIVMSG", vec!["#channel".to_string(), "Hello world".to_string()]);
528        match cmd {
529            Command::Privmsg { target, message } => {
530                assert_eq!(target, "#channel");
531                assert_eq!(message, "Hello world");
532            }
533            _ => panic!("Expected Privmsg command"),
534        }
535    }
536
537    #[test]
538    fn test_join_command_parsing() {
539        let cmd = Command::parse("JOIN", vec!["#chan1,#chan2".to_string(), "key1,key2".to_string()]);
540        match cmd {
541            Command::Join(channels, keys) => {
542                assert_eq!(channels, vec!["#chan1", "#chan2"]);
543                assert_eq!(keys, vec!["key1", "key2"]);
544            }
545            _ => panic!("Expected Join command"),
546        }
547    }
548
549    #[test]
550    fn test_cap_command_parsing() {
551        let cmd = Command::parse("CAP", vec!["LS".to_string(), "302".to_string()]);
552        match cmd {
553            Command::Cap { subcommand, params } => {
554                assert_eq!(subcommand, "LS");
555                assert_eq!(params, vec!["302"]);
556            }
557            _ => panic!("Expected Cap command"),
558        }
559    }
560
561    #[test]
562    fn test_command_name() {
563        let cmd = Command::Privmsg { target: "#test".to_string(), message: "hello".to_string() };
564        assert_eq!(cmd.command_name(), "PRIVMSG");
565    }
566
567    #[test]
568    fn test_command_categories() {
569        let privmsg = Command::Privmsg { target: "#test".to_string(), message: "hello".to_string() };
570        let join = Command::Join(vec!["#test".to_string()], vec![]);
571        let cap = Command::Cap { subcommand: "LS".to_string(), params: vec![] };
572
573        assert!(privmsg.is_message_command());
574        assert!(join.is_channel_command());
575        assert!(cap.is_ircv3_command());
576    }
577
578    #[test]
579    fn test_unknown_command() {
580        let cmd = Command::parse("UNKNOWN", vec!["param1".to_string()]);
581        match cmd {
582            Command::Unknown(name, params) => {
583                assert_eq!(name, "UNKNOWN");
584                assert_eq!(params, vec!["param1"]);
585            }
586            _ => panic!("Expected Unknown command"),
587        }
588    }
589}