IRC parsing, tokenization, and state handling in C#
at main 1195 lines 34 kB view raw
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}