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