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