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