1#[cfg(feature = "serde")]
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
14pub enum Command {
15 Nick(String),
18 User { username: String, realname: String },
20 Pass(String),
22 Quit(Option<String>),
24 Ping(String),
26 Pong(String),
28
29 Join(Vec<String>, Vec<String>), Part(Vec<String>, Option<String>), Topic { channel: String, topic: Option<String> },
36 Names(Vec<String>),
38 List(Option<Vec<String>>),
40
41 Privmsg { target: String, message: String },
44 Notice { target: String, message: String },
46
47 Who(Option<String>),
50 Whois(Vec<String>),
52 Whowas(String, Option<i32>),
54 Query(String),
56
57 Kick { channel: String, user: String, reason: Option<String> },
60 Mode { target: String, modes: Option<String>, params: Vec<String> },
62 Invite { nick: String, channel: String },
64
65 Motd(Option<String>),
68 Version(Option<String>),
70 Stats(Option<String>, Option<String>),
72 Time(Option<String>),
74 Info(Option<String>),
76
77 Cap { subcommand: String, params: Vec<String> },
80 Authenticate(String),
82 Account(String),
84 Monitor { subcommand: String, targets: Vec<String> },
86 Metadata { target: String, subcommand: String, params: Vec<String> },
88 TagMsg { target: String },
90 Batch { reference: String, batch_type: Option<String>, params: Vec<String> },
92
93 Redact { target: String, msgid: String, reason: Option<String> },
96 MarkRead { target: String, timestamp: Option<String> },
98 SetName { realname: String },
100 ChatHistory { subcommand: String, target: String, params: Vec<String> },
102
103 Oper { name: String, password: String },
106 Kill { nick: String, reason: String },
108 Rehash,
110 Restart,
112 Die,
114
115 CtcpRequest { target: String, command: String, params: String },
118 CtcpResponse { target: String, command: String, params: String },
120
121 Unknown(String, Vec<String>),
124}
125
126impl Command {
127 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 "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 "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 "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 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", Command::CtcpResponse { .. } => "NOTICE", Command::Unknown(cmd, _) => cmd,
482 }
483 }
484
485 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 pub fn is_message_command(&self) -> bool {
500 matches!(self, Command::Privmsg { .. } | Command::Notice { .. })
501 }
502
503 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}