IRC parsing, tokenization, and state handling in C#
1using System.Globalization;
2using IRCTokens;
3// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
4// ReSharper disable MemberCanBePrivate.Global
5// ReSharper disable UnusedAutoPropertyAccessor.Global
6// ReSharper disable CommentTypo
7// ReSharper disable IdentifierTypo
8
9namespace IRCStates;
10
11public class Server
12{
13 public const string WhoType = "525"; // randomly generated
14 private readonly StatefulDecoder _decoder;
15
16 private readonly Dictionary<string, string> _tempCaps;
17
18 public Server(string name)
19 {
20 Name = name;
21 Registered = false;
22 Modes = [];
23 Motd = [];
24 _decoder = new StatefulDecoder();
25 Users = [];
26 Channels = [];
27 ISupport = new ISupport();
28 HasCap = false;
29 _tempCaps = [];
30 AvailableCaps = [];
31 AgreedCaps = [];
32 }
33
34 public string Name { get; set; }
35 public string NickName { get; set; }
36 public string NickNameLower { get; set; }
37 public string UserName { get; set; }
38 public string HostName { get; set; }
39 public string RealName { get; set; }
40 public string Account { get; set; }
41 public string Away { get; set; }
42
43 public bool Registered { get; set; }
44 public List<string> Modes { get; set; }
45 public List<string> Motd { get; set; }
46 public Dictionary<string, User> Users { get; set; }
47 public Dictionary<string, Channel> Channels { get; set; }
48 public Dictionary<string, string> AvailableCaps { get; set; }
49 public List<string> AgreedCaps { get; set; }
50
51 // ReSharper disable once InconsistentNaming
52 public ISupport ISupport { get; set; }
53 public bool HasCap { get; set; }
54
55 public override string ToString()
56 {
57 return $"Server(name={Name})";
58 }
59
60 /// <summary>
61 /// Use <see cref="ISupport"/>'s case mapping to convert to lowercase
62 /// </summary>
63 /// <param name="str"></param>
64 /// <returns></returns>
65 public string CaseFold(string str)
66 {
67 return Casemap.CaseFold(ISupport.CaseMapping, str);
68 }
69
70 /// <summary>
71 /// Is the current nickname this client?
72 /// </summary>
73 /// <param name="nickname"></param>
74 /// <returns></returns>
75 private bool IsMe(string nickname)
76 {
77 return CaseFold(nickname) == NickNameLower;
78 }
79
80 /// <summary>
81 /// Check for a user - not case-sensitive
82 /// </summary>
83 /// <param name="nickname"></param>
84 /// <returns></returns>
85 private bool HasUser(string nickname)
86 {
87 return Users.ContainsKey(CaseFold(nickname));
88 }
89
90 /// <summary>
91 /// Get existing user by case-insensitive nickname
92 /// </summary>
93 /// <param name="nickname"></param>
94 /// <returns></returns>
95 private User GetUser(string nickname)
96 {
97 return HasUser(nickname) ? Users[CaseFold(nickname)] : null;
98 }
99
100 /// <summary>
101 /// Create and add user
102 /// </summary>
103 /// <param name="nickname"></param>
104 /// <returns></returns>
105 private User AddUser(string nickname)
106 {
107 var user = CreateUser(nickname);
108 Users[CaseFold(nickname)] = user;
109 return user;
110 }
111
112 /// <summary>
113 /// Build a new <see cref="User"/> and update correct case-mapped nick
114 /// </summary>
115 /// <param name="nickname"></param>
116 /// <returns></returns>
117 private User CreateUser(string nickname)
118 {
119 var user = new User();
120 user.SetNickName(nickname, CaseFold(nickname));
121 return user;
122 }
123
124 /// <summary>
125 /// Is the channel a valid ISupport type?
126 /// </summary>
127 /// <param name="target"></param>
128 /// <returns></returns>
129 private bool IsChannel(string target)
130 {
131 return !string.IsNullOrEmpty(target) &&
132 ISupport.ChanTypes.Contains(target[0].ToString(CultureInfo.InvariantCulture));
133 }
134
135 /// <summary>
136 /// Is the channel known to this client?
137 /// </summary>
138 /// <param name="name"></param>
139 /// <returns></returns>
140 public bool HasChannel(string name)
141 {
142 return IsChannel(name) && Channels.ContainsKey(CaseFold(name));
143 }
144
145 /// <summary>
146 /// Get the channel if it's known to us
147 /// </summary>
148 /// <param name="name"></param>
149 /// <returns></returns>
150 private Channel GetChannel(string name)
151 {
152 return HasChannel(name) ? Channels[CaseFold(name)] : null;
153 }
154
155 /// <summary>
156 /// Add a <see cref="User"/> to a <see cref="Channel"/>
157 /// </summary>
158 /// <param name="channel"></param>
159 /// <param name="user"></param>
160 /// <returns>the <see cref="ChannelUser"/> that was added</returns>
161 private ChannelUser UserJoin(Channel channel, User user)
162 {
163 var channelUser = new ChannelUser();
164 user.Channels.Add(CaseFold(channel.Name));
165 channel.Users[user.NickNameLower] = channelUser;
166 return channelUser;
167 }
168
169 /// <summary>
170 /// Set own <see cref="NickName"/>, <see cref="UserName"/>, and <see cref="HostName"/>
171 /// from a given <see cref="Hostmask"/>
172 /// </summary>
173 /// <param name="hostmask"></param>
174 private void SelfHostmask(Hostmask hostmask)
175 {
176 NickName = hostmask.NickName;
177 if (hostmask.UserName != null) UserName = hostmask.UserName;
178 if (hostmask.HostName != null) HostName = hostmask.HostName;
179 }
180
181 private void SelfHostmask(string raw)
182 {
183 SelfHostmask(new Hostmask(raw));
184 }
185
186 /// <summary>
187 /// Remove a user from a channel. Used to handle PART and KICK
188 /// </summary>
189 /// <param name="line"></param>
190 /// <param name="nickName"></param>
191 /// <param name="channelName"></param>
192 /// <param name="reasonIndex"></param>
193 /// <returns></returns>
194 private (Emit, User) UserPart(Line line, string nickName, string channelName, int reasonIndex)
195 {
196 var emit = new Emit();
197 var channelLower = CaseFold(channelName);
198 if (line.Params.Count >= reasonIndex + 1) emit.Text = line.Params[reasonIndex];
199
200 User user = null;
201 if (HasChannel(channelName))
202 {
203 var channel = GetChannel(channelName);
204 emit.Channel = channel;
205 var nickLower = CaseFold(nickName);
206 if (HasUser(nickLower))
207 {
208 user = Users[nickLower];
209 user.Channels.Remove(channelLower);
210 channel.Users.Remove(nickLower);
211 if (!user.Channels.Any()) Users.Remove(nickLower);
212 }
213
214 if (IsMe(nickName))
215 {
216 Channels.Remove(channelLower);
217 foreach (var userToRemove in channel.Users.Keys.Select(u => Users[u]))
218 {
219 userToRemove.Channels.Remove(channelLower);
220 if (!userToRemove.Channels.Any()) Users.Remove(userToRemove.NickNameLower);
221 }
222 }
223 }
224
225 return (emit, user);
226 }
227
228 /// <summary>
229 /// Update modes on a <see cref="Channel"/> given modes and parameters
230 /// </summary>
231 /// <param name="channel"></param>
232 /// <param name="modes"></param>
233 /// <param name="parameters"></param>
234 private void SetChannelModes(Channel channel, IEnumerable<(bool, string)> modes, IList<string> parameters)
235 {
236 foreach (var (add, c) in modes)
237 {
238 var listMode = ISupport.ChanModes.ListModes.Contains(c);
239 if (ISupport.Prefix.Modes.Contains(c))
240 {
241 var nicknameLower = CaseFold(parameters.First());
242 parameters.RemoveAt(0);
243 if (!HasUser(nicknameLower)) continue;
244
245 var channelUser = channel.Users[nicknameLower];
246 if (add)
247 {
248 if (!channelUser.Modes.Contains(c)) channelUser.Modes.Add(c);
249 }
250 else if (channelUser.Modes.Contains(c))
251 {
252 channelUser.Modes.Remove(c);
253 }
254 }
255 else if (add && (listMode ||
256 ISupport.ChanModes.SettingBModes.Contains(c) ||
257 ISupport.ChanModes.SettingCModes.Contains(c)))
258 {
259 channel.AddMode(c, parameters.First(), listMode);
260 parameters.RemoveAt(0);
261 }
262 else if (!add && (listMode || ISupport.ChanModes.SettingBModes.Contains(c)))
263 {
264 channel.RemoveMode(c, parameters.First());
265 parameters.RemoveAt(0);
266 }
267 else if (add)
268 {
269 channel.AddMode(c, null, false);
270 }
271 else
272 {
273 channel.RemoveMode(c, null);
274 }
275 }
276 }
277
278 /// <summary>
279 /// Handle incoming bytes
280 /// </summary>
281 /// <param name="data"></param>
282 /// <param name="length"></param>
283 /// <returns>parsed lines and emits</returns>
284 /// <exception cref="ServerDisconnectedException"></exception>
285 public IEnumerable<(Line, Emit)> Receive(byte[] data, int length)
286 {
287 if (data == null) return null;
288
289 var lines = _decoder.Push(data, length);
290 if (lines == null) throw new ServerDisconnectedException();
291
292 return lines.Select(l => (l, Parse(l)));
293 }
294
295 /// <summary>
296 /// Delegate a <see cref="Line"/> to the correct handler
297 /// </summary>
298 /// <param name="line"></param>
299 /// <returns></returns>
300 public Emit Parse(Line line)
301 {
302 if (line == null) return null;
303
304 var emit = line.Command switch
305 {
306 Numeric.RPL_WELCOME => HandleWelcome(line),
307 Numeric.RPL_ISUPPORT => HandleISupport(line),
308 Numeric.RPL_MOTDSTART => HandleMotd(line),
309 Numeric.RPL_MOTD => HandleMotd(line),
310 Commands.Nick => HandleNick(line),
311 Commands.Join => HandleJoin(line),
312 Commands.Part => HandlePart(line),
313 Commands.Kick => HandleKick(line),
314 Commands.Quit => HandleQuit(line),
315 Commands.Error => HandleError(line),
316 Numeric.RPL_NAMREPLY => HandleNames(line),
317 Numeric.RPL_CREATIONTIME => HandleCreationTime(line),
318 Commands.Topic => HandleTopic(line),
319 Numeric.RPL_TOPIC => HandleTopicNumeric(line),
320 Numeric.RPL_TOPICWHOTIME => HandleTopicTime(line),
321 Commands.Mode => HandleMode(line),
322 Numeric.RPL_CHANNELMODEIS => HandleChannelModeIs(line),
323 Numeric.RPL_UMODEIS => HandleUModeIs(line),
324 Commands.Privmsg => HandleMessage(line),
325 Commands.Notice => HandleMessage(line),
326 Commands.Tagmsg => HandleMessage(line),
327 Numeric.RPL_VISIBLEHOST => HandleVisibleHost(line),
328 Numeric.RPL_WHOREPLY => HandleWhoReply(line),
329 Numeric.RPL_WHOSPCRPL => HandleWhox(line),
330 Numeric.RPL_WHOISUSER => HandleWhoIsUser(line),
331 Commands.Chghost => HandleChghost(line),
332 Commands.Setname => HandleSetname(line),
333 Commands.Away => HandleAway(line),
334 Commands.Account => HandleAccount(line),
335 Commands.Cap => HandleCap(line),
336 Numeric.RPL_LOGGEDIN => HandleLoggedIn(line),
337 Numeric.RPL_LOGGEDOUT => HandleLoggedOut(line),
338 _ => null
339 };
340
341 if (emit != null)
342 emit.Command = line.Command;
343 else
344 emit = new Emit();
345
346 return emit;
347 }
348
349 /// <summary>
350 /// Handles SETNAME command
351 /// </summary>
352 /// <param name="line"></param>
353 /// <returns></returns>
354 private Emit HandleSetname(Line line)
355 {
356 var emit = new Emit();
357 var realname = line.Params[0];
358 var nicknameLower = CaseFold(line.Hostmask.NickName);
359
360 if (IsMe(nicknameLower))
361 {
362 emit.Self = true;
363 RealName = realname;
364 }
365
366 if (Users.TryGetValue(nicknameLower, out var user))
367 {
368 emit.User = user;
369 user.RealName = realname;
370 }
371
372 return emit;
373 }
374
375 /// <summary>
376 /// Handles AWAY command
377 /// </summary>
378 /// <param name="line"></param>
379 /// <returns></returns>
380 private Emit HandleAway(Line line)
381 {
382 var emit = new Emit();
383 var away = line.Params.FirstOrDefault();
384 var nicknameLower = CaseFold(line.Hostmask.NickName);
385
386 if (IsMe(nicknameLower))
387 {
388 emit.Self = true;
389 Away = away;
390 }
391
392 if (Users.TryGetValue(nicknameLower, out var user))
393 {
394 emit.User = user;
395 user.Away = away;
396 }
397
398 return emit;
399 }
400
401 /// <summary>
402 /// Handles ACCOUNT command
403 /// </summary>
404 /// <param name="line"></param>
405 /// <returns></returns>
406 private Emit HandleAccount(Line line)
407 {
408 var emit = new Emit();
409 var account = line.Params[0].Trim('*');
410 var nicknameLower = CaseFold(line.Hostmask.NickName);
411
412 if (IsMe(nicknameLower))
413 {
414 emit.Self = true;
415 Account = account;
416 }
417
418 if (Users.TryGetValue(nicknameLower, out var user))
419 {
420 emit.User = user;
421 user.Account = account;
422 }
423
424 return emit;
425 }
426
427 /// <summary>
428 /// Handles CAP command
429 /// </summary>
430 /// <param name="line"></param>
431 /// <returns></returns>
432 private Emit HandleCap(Line line)
433 {
434 HasCap = true;
435 var subcommand = line.Params[1].ToUpperInvariant();
436 var multiline = line.Params[2] == "*";
437 var caps = line.Params[multiline ? 3 : 2];
438
439 var tokens = new Dictionary<string, string>();
440 var tokensStr = new List<string>();
441 foreach (var cap in caps.Split([' '], StringSplitOptions.RemoveEmptyEntries))
442 {
443 tokensStr.Add(cap);
444 var kv = cap.Split(['='], 2);
445 tokens[kv[0]] = kv.Length > 1 ? kv[1] : string.Empty;
446 }
447
448 var emit = new Emit { Subcommand = subcommand, Finished = !multiline, Tokens = tokensStr };
449
450 switch (subcommand)
451 {
452 case "LS":
453 _tempCaps.UpdateWith(tokens);
454 if (!multiline)
455 {
456 AvailableCaps.UpdateWith(_tempCaps);
457 _tempCaps.Clear();
458 }
459
460 break;
461 case "NEW":
462 AvailableCaps.UpdateWith(tokens);
463 break;
464 case "DEL":
465 foreach (var key in tokens.Keys.Where(key => AvailableCaps.ContainsKey(key)))
466 {
467 AvailableCaps.Remove(key);
468 if (AgreedCaps.Contains(key)) AgreedCaps.Remove(key);
469 }
470
471 break;
472 case "ACK":
473 foreach (var key in tokens.Keys)
474 if (key.StartsWith("-"))
475 {
476 var k = key.Substring(1);
477 if (AgreedCaps.Contains(k)) AgreedCaps.Remove(k);
478 }
479 else if (!AgreedCaps.Contains(key) && AvailableCaps.ContainsKey(key))
480 {
481 AgreedCaps.Add(key);
482 }
483
484 break;
485 }
486
487 return emit;
488 }
489
490 /// <summary>
491 /// Handles RPL_LOGGEDIN numeric
492 /// </summary>
493 /// <param name="line"></param>
494 /// <returns></returns>
495 private Emit HandleLoggedIn(Line line)
496 {
497 SelfHostmask(new Hostmask(line.Params[1]));
498 Account = line.Params[2];
499 return new Emit();
500 }
501
502 /// <summary>
503 /// Handles CHGHOST command
504 /// </summary>
505 /// <param name="line"></param>
506 /// <returns></returns>
507 private Emit HandleChghost(Line line)
508 {
509 var emit = new Emit();
510 var username = line.Params[0];
511 var hostname = line.Params[1];
512 var nicknameLower = CaseFold(line.Hostmask.NickName);
513
514 if (IsMe(nicknameLower))
515 {
516 emit.Self = true;
517 UserName = username;
518 HostName = hostname;
519 }
520
521 if (Users.TryGetValue(nicknameLower, out var user))
522 {
523 emit.User = user;
524 user.UserName = username;
525 user.HostName = hostname;
526 }
527
528 return emit;
529 }
530
531 /// <summary>
532 /// Handles RPL_WHOISUSER numeric
533 /// </summary>
534 /// <param name="line"></param>
535 /// <returns></returns>
536 private Emit HandleWhoIsUser(Line line)
537 {
538 var emit = new Emit();
539 var nickname = line.Params[1];
540 var username = line.Params[2];
541 var hostname = line.Params[3];
542 var realname = line.Params[5];
543
544 if (IsMe(nickname))
545 {
546 emit.Self = true;
547 UserName = username;
548 HostName = hostname;
549 RealName = realname;
550 }
551
552 if (HasUser(nickname))
553 {
554 var user = Users[CaseFold(nickname)];
555 emit.User = user;
556 user.UserName = username;
557 user.HostName = hostname;
558 user.RealName = realname;
559 }
560
561 return emit;
562 }
563
564 /// <summary>
565 /// Handles RPL_WHOSPCRPL numeric
566 /// </summary>
567 /// <param name="line"></param>
568 /// <returns></returns>
569 private Emit HandleWhox(Line line)
570 {
571 var emit = new Emit();
572 if (line.Params[1] == WhoType && line.Params.Count == 8)
573 {
574 var nickname = line.Params[5];
575 var username = line.Params[2];
576 var hostname = line.Params[4];
577 var realname = line.Params[7];
578 var account = line.Params[6] == "0" ? null : line.Params[6];
579
580 if (IsMe(nickname))
581 {
582 emit.Self = true;
583 UserName = username;
584 HostName = hostname;
585 RealName = realname;
586 Account = account;
587 }
588
589 if (HasUser(nickname))
590 {
591 var user = Users[CaseFold(nickname)];
592 emit.User = user;
593 user.UserName = username;
594 user.HostName = hostname;
595 user.RealName = realname;
596 user.Account = account;
597 }
598 }
599
600 return emit;
601 }
602
603 /// <summary>
604 /// Handles RPL_WHOREPLY numeric
605 /// </summary>
606 /// <param name="line"></param>
607 /// <returns></returns>
608 private Emit HandleWhoReply(Line line)
609 {
610 var emit = new Emit {Target = line.Params[1]};
611 var nickname = line.Params[5];
612 var username = line.Params[2];
613 var hostname = line.Params[3];
614 var realname = line.Params[7].Split([' '], 2)[1];
615
616 if (IsMe(nickname))
617 {
618 emit.Self = true;
619 UserName = username;
620 HostName = hostname;
621 RealName = realname;
622 }
623
624 if (HasUser(nickname))
625 {
626 var user = Users[CaseFold(nickname)];
627 emit.User = user;
628 user.UserName = username;
629 user.HostName = hostname;
630 user.RealName = realname;
631 }
632
633 return emit;
634 }
635
636 /// <summary>
637 /// Handles RPL_VISIBLEHOST numeric
638 /// </summary>
639 /// <param name="line"></param>
640 /// <returns></returns>
641 private Emit HandleVisibleHost(Line line)
642 {
643 var split = line.Params[1].Split(['@'], 2);
644 switch (split.Length)
645 {
646 case 1:
647 HostName = split[0];
648 break;
649 case 2:
650 HostName = split[1];
651 UserName = split[0];
652 break;
653 }
654
655 return new Emit();
656 }
657
658 /// <summary>
659 /// Handles PRIVMSG, NOTICE, and TAGMSG commands
660 /// </summary>
661 /// <param name="line"></param>
662 /// <returns></returns>
663 private Emit HandleMessage(Line line)
664 {
665 var emit = new Emit();
666 var message = line.Params.Count > 1 ? line.Params[1] : null;
667 if (message != null) emit.Text = message;
668
669 var nick = CaseFold(line.Hostmask.NickName);
670 if (IsMe(nick))
671 {
672 emit.SelfSource = true;
673 SelfHostmask(line.Hostmask);
674 }
675
676 var user = GetUser(nick) ?? AddUser(nick);
677 emit.User = user;
678
679 if (line.Hostmask.UserName != null) user.UserName = line.Hostmask.UserName;
680 if (line.Hostmask.HostName != null) user.HostName = line.Hostmask.HostName;
681
682 var target = line.Params[0];
683 var statusMsg = new List<string>();
684 while (target.Length > 0)
685 {
686 var t = target[0].ToString(CultureInfo.InvariantCulture);
687 if (ISupport.StatusMsg.Contains(t))
688 {
689 statusMsg.Add(t);
690 target = target.Substring(1);
691 }
692 else
693 {
694 break;
695 }
696 }
697
698 emit.Target = line.Params[0];
699
700 if (IsChannel(target) && HasChannel(target))
701 {
702 emit.Channel = GetChannel(target);
703 }
704 else if (IsMe(target))
705 {
706 emit.SelfTarget = true;
707 }
708
709 return emit;
710 }
711
712 /// <summary>
713 /// Handles RPL_UMODEIS numeric
714 /// </summary>
715 /// <param name="line"></param>
716 /// <returns></returns>
717 private Emit HandleUModeIs(Line line)
718 {
719 foreach (var c in line.Params[1]
720 .TrimStart('+')
721 .Select(m => m.ToString(CultureInfo.InvariantCulture))
722 .Where(m => !Modes.Contains(m)))
723 Modes.Add(c);
724
725 return new Emit();
726 }
727
728 /// <summary>
729 /// Handles RPL_CHANNELMODEIS numeric
730 /// </summary>
731 /// <param name="line"></param>
732 /// <returns></returns>
733 private Emit HandleChannelModeIs(Line line)
734 {
735 var emit = new Emit();
736 if (HasChannel(line.Params[1]))
737 {
738 var channel = GetChannel(line.Params[1]);
739 emit.Channel = channel;
740 var modes = line.Params[2]
741 .TrimStart('+')
742 .Select(p => (true, p.ToString(CultureInfo.InvariantCulture)));
743 var parameters = line.Params.Skip(3).ToList();
744 SetChannelModes(channel, modes, parameters);
745 }
746
747 return emit;
748 }
749
750 /// <summary>
751 /// Handles MODE command
752 /// </summary>
753 /// <param name="line"></param>
754 /// <returns></returns>
755 private Emit HandleMode(Line line)
756 {
757 var emit = new Emit();
758 var target = line.Params[0];
759 var modeString = line.Params[1];
760 var parameters = line.Params.Skip(2).ToList();
761
762 var modifier = '+';
763 var modes = new List<(bool, string)>();
764 var tokens = new List<string>();
765
766 foreach (var c in modeString)
767 if (new[] {'+', '-'}.Contains(c))
768 {
769 modifier = c;
770 }
771 else
772 {
773 modes.Add((modifier == '+', c.ToString(CultureInfo.InvariantCulture)));
774 tokens.Add($"{modifier}{c}");
775 }
776
777 emit.Tokens = tokens;
778
779 if (IsMe(target))
780 {
781 emit.SelfTarget = true;
782 foreach (var (add, c) in modes)
783 if (add && !Modes.Contains(c))
784 Modes.Add(c);
785 else if (Modes.Contains(c)) Modes.Remove(c);
786 }
787 else if (HasChannel(target))
788 {
789 var channel = GetChannel(CaseFold(target));
790 emit.Channel = channel;
791 SetChannelModes(channel, modes, parameters);
792 }
793
794 return emit;
795 }
796
797 /// <summary>
798 /// Handles RPL_TOPICWHOTIME numeric
799 /// </summary>
800 /// <param name="line"></param>
801 /// <returns></returns>
802 private Emit HandleTopicTime(Line line)
803 {
804 var emit = new Emit();
805 if (HasChannel(line.Params[1]))
806 {
807 var channel = GetChannel(line.Params[1]);
808 emit.Channel = channel;
809 channel.TopicSetter = line.Params[2];
810 channel.TopicTime = DateTimeOffset
811 .FromUnixTimeSeconds(int.Parse(line.Params[3], CultureInfo.InvariantCulture)).DateTime;
812 }
813
814 return emit;
815 }
816
817 /// <summary>
818 /// Handles RPL_TOPIC numeric
819 /// </summary>
820 /// <param name="line"></param>
821 /// <returns></returns>
822 private Emit HandleTopicNumeric(Line line)
823 {
824 var emit = new Emit();
825 if (HasChannel(line.Params[1]))
826 {
827 var channel = GetChannel(line.Params[1]);
828 emit.Channel = channel;
829 channel.Topic = line.Params[2];
830 }
831
832 return emit;
833 }
834
835 /// <summary>
836 /// Handles TOPIC command
837 /// </summary>
838 /// <param name="line"></param>
839 /// <returns></returns>
840 private Emit HandleTopic(Line line)
841 {
842 var emit = new Emit();
843 if (HasChannel(line.Params[0]))
844 {
845 var channel = GetChannel(line.Params[0]);
846 emit.Channel = channel;
847 channel.Topic = line.Params[1];
848 channel.TopicSetter = line.Hostmask.ToString();
849 channel.TopicTime = DateTime.UtcNow;
850 }
851
852 return emit;
853 }
854
855 /// <summary>
856 /// Handles RPL_CREATIONTIME numeric
857 /// </summary>
858 /// <param name="line"></param>
859 /// <returns></returns>
860 private Emit HandleCreationTime(Line line)
861 {
862 var emit = new Emit();
863 if (HasChannel(line.Params[1]))
864 {
865 var channel = GetChannel(line.Params[1]);
866 emit.Channel = channel;
867 channel.Created = DateTimeOffset
868 .FromUnixTimeSeconds(int.Parse(line.Params[2], CultureInfo.InvariantCulture)).DateTime;
869 }
870
871 return emit;
872 }
873
874 /// <summary>
875 /// Handles RPL_NAMREPLY numeric
876 /// </summary>
877 /// <param name="line"></param>
878 /// <returns></returns>
879 private Emit HandleNames(Line line)
880 {
881 var emit = new Emit();
882 if (!HasChannel(line.Params[2])) return emit;
883
884 var channel = GetChannel(line.Params[2]);
885 emit.Channel = channel;
886 var nicknames = line.Params[3].Split([' '], StringSplitOptions.RemoveEmptyEntries);
887 var users = new List<User>();
888 emit.Users = users;
889
890 foreach (var nick in nicknames)
891 {
892 var modes = "";
893 foreach (var c in nick)
894 {
895 var mode = ISupport.Prefix.FromPrefix(c);
896 if (mode != null)
897 modes += mode;
898 else
899 break;
900 }
901
902 var hostmask = new Hostmask(nick.Substring(modes.Length));
903 var user = GetUser(hostmask.NickName) ?? AddUser(hostmask.NickName);
904
905 users.Add(user);
906 var channelUser = UserJoin(channel, user);
907
908 if (hostmask.UserName != null) user.UserName = hostmask.UserName;
909 if (hostmask.HostName != null) user.HostName = hostmask.HostName;
910
911 if (IsMe(hostmask.NickName)) SelfHostmask(hostmask);
912
913 foreach (var mode in modes.Select(c => c.ToString(CultureInfo.InvariantCulture)))
914 if (!channelUser.Modes.Contains(mode))
915 channelUser.Modes.Add(mode);
916 }
917
918 return emit;
919 }
920
921 /// <summary>
922 /// Handles ERROR command
923 /// </summary>
924 /// <param name="line"></param>
925 /// <returns></returns>
926 private Emit HandleError(Line line)
927 {
928 Users.Clear();
929 Channels.Clear();
930 return new Emit();
931 }
932
933 /// <summary>
934 /// Handles QUIT command
935 /// </summary>
936 /// <param name="line"></param>
937 /// <returns></returns>
938 private Emit HandleQuit(Line line)
939 {
940 var emit = new Emit();
941 var nick = line.Hostmask.NickName;
942 if (line.Params.Any()) emit.Text = line.Params[0];
943
944 if (IsMe(nick) || line.Source == null)
945 {
946 emit.Self = true;
947 Users.Clear();
948 Channels.Clear();
949 }
950 else if (HasUser(nick))
951 {
952 var user = GetUser(nick);
953 Users.Remove(user.NickNameLower);
954 emit.User = user;
955 foreach (var channel in user.Channels.Select(c => Channels[c]))
956 channel.Users.Remove(user.NickNameLower);
957 }
958
959 return emit;
960 }
961
962 /// <summary>
963 /// Handles RPL_LOGGEDOUT numeric
964 /// </summary>
965 /// <param name="line"></param>
966 /// <returns></returns>
967 private Emit HandleLoggedOut(Line line)
968 {
969 Account = null;
970 SelfHostmask(line.Params[1]);
971 return new Emit();
972 }
973
974 /// <summary>
975 /// Handles KICK command
976 /// </summary>
977 /// <param name="line"></param>
978 /// <returns></returns>
979 private Emit HandleKick(Line line)
980 {
981 var (emit, kicked) = UserPart(line, line.Params[1], line.Params[0], 2);
982 if (kicked != null)
983 {
984 emit.UserTarget = kicked;
985 if (IsMe(kicked.NickName)) emit.Self = true;
986
987 var kicker = line.Hostmask.NickName;
988 if (IsMe(kicker)) emit.SelfSource = true;
989
990 emit.UserSource = GetUser(kicker) ?? CreateUser(kicker);
991 }
992
993 return emit;
994 }
995
996 /// <summary>
997 /// Handles PART command
998 /// </summary>
999 /// <param name="line"></param>
1000 /// <returns></returns>
1001 private Emit HandlePart(Line line)
1002 {
1003 var (emit, user) = UserPart(line, line.Hostmask.NickName, line.Params[0], 1);
1004 if (user != null)
1005 {
1006 emit.User = user;
1007 emit.Self = IsMe(user.NickName);
1008 }
1009
1010 return emit;
1011 }
1012
1013 /// <summary>
1014 /// Handles JOIN command
1015 /// </summary>
1016 /// <param name="line"></param>
1017 /// <returns></returns>
1018 private Emit HandleJoin(Line line)
1019 {
1020 var extended = line.Params.Count == 3;
1021 var account = extended ? line.Params[1].Trim('*') : null;
1022 var realname = extended ? line.Params[2] : null;
1023 var emit = new Emit();
1024
1025 var channelName = line.Params[0];
1026 var nick = line.Hostmask.NickName;
1027
1028 // handle own join
1029 if (IsMe(nick))
1030 {
1031 emit.Self = true;
1032 if (!HasChannel(channelName))
1033 {
1034 var channel = new Channel();
1035 channel.SetName(channelName, CaseFold(channelName));
1036 Channels[CaseFold(channelName)] = channel;
1037 }
1038
1039 SelfHostmask(line.Hostmask);
1040 if (extended)
1041 {
1042 Account = account;
1043 RealName = realname;
1044 }
1045 }
1046
1047 if (HasChannel(channelName))
1048 {
1049 var channel = GetChannel(channelName);
1050 emit.Channel = channel;
1051
1052 if (!HasUser(nick)) AddUser(nick);
1053
1054 var user = GetUser(nick);
1055 emit.User = user;
1056 if (line.Hostmask.UserName != null) user.UserName = line.Hostmask.UserName;
1057 if (line.Hostmask.HostName != null) user.HostName = line.Hostmask.HostName;
1058 if (extended)
1059 {
1060 user.Account = account;
1061 user.RealName = realname;
1062 }
1063
1064 UserJoin(channel, user);
1065 }
1066
1067 return emit;
1068 }
1069
1070 /// <summary>
1071 /// Handles NICK command
1072 /// </summary>
1073 /// <param name="line"></param>
1074 /// <returns></returns>
1075 private Emit HandleNick(Line line)
1076 {
1077 var newNick = line.Params[0];
1078 var oldNick = line.Hostmask.NickName;
1079
1080 var emit = new Emit();
1081
1082 if (HasUser(oldNick))
1083 {
1084 var user = GetUser(oldNick);
1085 var oldNickLower = user.NickNameLower;
1086 var newNickLower = CaseFold(newNick);
1087
1088 emit.User = user;
1089 Users.Remove(oldNickLower);
1090 Users[newNickLower] = user;
1091 user.SetNickName(newNick, newNickLower);
1092
1093 foreach (var channelLower in user.Channels)
1094 {
1095 var channel = GetChannel(channelLower);
1096 var channelUser = channel.Users[oldNickLower];
1097 channel.Users.Remove(oldNickLower);
1098 channel.Users[newNickLower] = channelUser;
1099 }
1100 }
1101
1102 if (IsMe(oldNick))
1103 {
1104 emit.Self = true;
1105 NickName = newNick;
1106 NickNameLower = CaseFold(newNick);
1107 }
1108
1109 return emit;
1110 }
1111
1112 /// <summary>
1113 /// Handles RPL_MOTDSTART and RPL_MOTD numerics
1114 /// </summary>
1115 /// <param name="line"></param>
1116 /// <returns></returns>
1117 private Emit HandleMotd(Line line)
1118 {
1119 if (line.Command == Numeric.RPL_MOTDSTART) Motd.Clear();
1120
1121 var emit = new Emit { Text = line.Params[1] };
1122 Motd.Add(line.Params[1]);
1123 return emit;
1124 }
1125
1126 /// <summary>
1127 /// Handles RPL_ISUPPORT numeric
1128 /// </summary>
1129 /// <param name="line"></param>
1130 /// <returns></returns>
1131 private Emit HandleISupport(Line line)
1132 {
1133 ISupport = new ISupport();
1134 ISupport.Parse(line.Params);
1135 return new Emit();
1136 }
1137
1138 /// <summary>
1139 /// Handles RPL_WELCOME numeric
1140 /// </summary>
1141 /// <param name="line"></param>
1142 /// <returns></returns>
1143 private Emit HandleWelcome(Line line)
1144 {
1145 NickName = line.Params[0];
1146 NickNameLower = CaseFold(line.Params[0]);
1147 Registered = true;
1148 return new Emit();
1149 }
1150}