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