1# -*- test-case-name: twisted.words.test.test_irc -*- 2# Copyright (c) Twisted Matrix Laboratories. 3# See LICENSE for details. 4 5""" 6Internet Relay Chat protocol for client and server. 7 8Future Plans 9============ 10 11The way the IRCClient class works here encourages people to implement 12IRC clients by subclassing the ephemeral protocol class, and it tends 13to end up with way more state than it should for an object which will 14be destroyed as soon as the TCP transport drops. Someone oughta do 15something about that, ya know? 16 17The DCC support needs to have more hooks for the client for it to be 18able to ask the user things like "Do you want to accept this session?" 19and "Transfer #2 is 67% done." and otherwise manage the DCC sessions. 20 21Test coverage needs to be better. 22 23@var MAX_COMMAND_LENGTH: The maximum length of a command, as defined by RFC 24 2812 section 2.3. 25 26@var attributes: Singleton instance of L{_CharacterAttributes}, used for 27 constructing formatted text information. 28 29@author: Kevin Turner 30 31@see: RFC 1459: Internet Relay Chat Protocol 32@see: RFC 2812: Internet Relay Chat: Client Protocol 33@see: U{The Client-To-Client-Protocol 34<http://www.irchelp.org/irchelp/rfc/ctcpspec.html>} 35""" 36 37import errno 38import operator 39import os 40import random 41import re 42import shlex 43import socket 44import stat 45import string 46import struct 47import sys 48import textwrap 49import time 50import traceback 51from functools import reduce 52from os import path 53from typing import Optional 54 55from twisted.internet import protocol, reactor, task 56from twisted.persisted import styles 57from twisted.protocols import basic 58from twisted.python import _textattributes, log, reflect 59 60NUL = chr(0) 61CR = chr(0o15) 62NL = chr(0o12) 63LF = NL 64SPC = chr(0o40) 65 66# This includes the CRLF terminator characters. 67MAX_COMMAND_LENGTH = 512 68 69CHANNEL_PREFIXES = "&#!+" 70 71 72class IRCBadMessage(Exception): 73 pass 74 75 76class IRCPasswordMismatch(Exception): 77 pass 78 79 80class IRCBadModes(ValueError): 81 """ 82 A malformed mode was encountered while attempting to parse a mode string. 83 """ 84 85 86def parsemsg(s): 87 """ 88 Breaks a message from an IRC server into its prefix, command, and 89 arguments. 90 91 @param s: The message to break. 92 @type s: L{bytes} 93 94 @return: A tuple of (prefix, command, args). 95 @rtype: L{tuple} 96 """ 97 prefix = "" 98 trailing = [] 99 if not s: 100 raise IRCBadMessage("Empty line.") 101 if s[0:1] == ":": 102 prefix, s = s[1:].split(" ", 1) 103 if s.find(" :") != -1: 104 s, trailing = s.split(" :", 1) 105 args = s.split() 106 args.append(trailing) 107 else: 108 args = s.split() 109 command = args.pop(0) 110 return prefix, command, args 111 112 113def split(str, length=80): 114 """ 115 Split a string into multiple lines. 116 117 Whitespace near C{str[length]} will be preferred as a breaking point. 118 C{"\\n"} will also be used as a breaking point. 119 120 @param str: The string to split. 121 @type str: C{str} 122 123 @param length: The maximum length which will be allowed for any string in 124 the result. 125 @type length: C{int} 126 127 @return: C{list} of C{str} 128 """ 129 return [chunk for line in str.split("\n") for chunk in textwrap.wrap(line, length)] 130 131 132def _intOrDefault(value, default=None): 133 """ 134 Convert a value to an integer if possible. 135 136 @rtype: C{int} or type of L{default} 137 @return: An integer when C{value} can be converted to an integer, 138 otherwise return C{default} 139 """ 140 if value: 141 try: 142 return int(value) 143 except (TypeError, ValueError): 144 pass 145 return default 146 147 148class UnhandledCommand(RuntimeError): 149 """ 150 A command dispatcher could not locate an appropriate command handler. 151 """ 152 153 154class _CommandDispatcherMixin: 155 """ 156 Dispatch commands to handlers based on their name. 157 158 Command handler names should be of the form C{prefix_commandName}, 159 where C{prefix} is the value specified by L{prefix}, and must 160 accept the parameters as given to L{dispatch}. 161 162 Attempting to mix this in more than once for a single class will cause 163 strange behaviour, due to L{prefix} being overwritten. 164 165 @type prefix: C{str} 166 @ivar prefix: Command handler prefix, used to locate handler attributes 167 """ 168 169 prefix: Optional[str] = None 170 171 def dispatch(self, commandName, *args): 172 """ 173 Perform actual command dispatch. 174 """ 175 176 def _getMethodName(command): 177 return f"{self.prefix}_{command}" 178 179 def _getMethod(name): 180 return getattr(self, _getMethodName(name), None) 181 182 method = _getMethod(commandName) 183 if method is not None: 184 return method(*args) 185 186 method = _getMethod("unknown") 187 if method is None: 188 raise UnhandledCommand( 189 f"No handler for {_getMethodName(commandName)!r} could be found" 190 ) 191 return method(commandName, *args) 192 193 194def parseModes(modes, params, paramModes=("", "")): 195 """ 196 Parse an IRC mode string. 197 198 The mode string is parsed into two lists of mode changes (added and 199 removed), with each mode change represented as C{(mode, param)} where mode 200 is the mode character, and param is the parameter passed for that mode, or 201 L{None} if no parameter is required. 202 203 @type modes: C{str} 204 @param modes: Modes string to parse. 205 206 @type params: C{list} 207 @param params: Parameters specified along with L{modes}. 208 209 @type paramModes: C{(str, str)} 210 @param paramModes: A pair of strings (C{(add, remove)}) that indicate which modes take 211 parameters when added or removed. 212 213 @returns: Two lists of mode changes, one for modes added and the other for 214 modes removed respectively, mode changes in each list are represented as 215 C{(mode, param)}. 216 """ 217 if len(modes) == 0: 218 raise IRCBadModes("Empty mode string") 219 220 if modes[0] not in "+-": 221 raise IRCBadModes(f"Malformed modes string: {modes!r}") 222 223 changes = ([], []) 224 225 direction = None 226 count = -1 227 for ch in modes: 228 if ch in "+-": 229 if count == 0: 230 raise IRCBadModes(f"Empty mode sequence: {modes!r}") 231 direction = "+-".index(ch) 232 count = 0 233 else: 234 param = None 235 if ch in paramModes[direction]: 236 try: 237 param = params.pop(0) 238 except IndexError: 239 raise IRCBadModes(f"Not enough parameters: {ch!r}") 240 changes[direction].append((ch, param)) 241 count += 1 242 243 if len(params) > 0: 244 raise IRCBadModes(f"Too many parameters: {modes!r} {params!r}") 245 246 if count == 0: 247 raise IRCBadModes(f"Empty mode sequence: {modes!r}") 248 249 return changes 250 251 252class IRC(protocol.Protocol): 253 """ 254 Internet Relay Chat server protocol. 255 """ 256 257 buffer = "" 258 hostname = None 259 260 encoding: Optional[str] = None 261 262 def connectionMade(self): 263 self.channels = [] 264 if self.hostname is None: 265 self.hostname = socket.getfqdn() 266 267 def sendLine(self, line): 268 line = line + CR + LF 269 if isinstance(line, str): 270 useEncoding = self.encoding if self.encoding else "utf-8" 271 line = line.encode(useEncoding) 272 self.transport.write(line) 273 274 def sendMessage(self, command, *parameter_list, **prefix): 275 """ 276 Send a line formatted as an IRC message. 277 278 First argument is the command, all subsequent arguments are parameters 279 to that command. If a prefix is desired, it may be specified with the 280 keyword argument 'prefix'. 281 282 The L{sendCommand} method is generally preferred over this one. 283 Notably, this method does not support sending message tags, while the 284 L{sendCommand} method does. 285 """ 286 if not command: 287 raise ValueError("IRC message requires a command.") 288 289 if " " in command or command[0] == ":": 290 # Not the ONLY way to screw up, but provides a little 291 # sanity checking to catch likely dumb mistakes. 292 raise ValueError( 293 "Somebody screwed up, 'cuz this doesn't" 294 " look like a command to me: %s" % command 295 ) 296 297 line = " ".join([command] + list(parameter_list)) 298 if "prefix" in prefix: 299 line = ":{} {}".format(prefix["prefix"], line) 300 self.sendLine(line) 301 302 if len(parameter_list) > 15: 303 log.msg( 304 "Message has %d parameters (RFC allows 15):\n%s" 305 % (len(parameter_list), line) 306 ) 307 308 def sendCommand(self, command, parameters, prefix=None, tags=None): 309 """ 310 Send to the remote peer a line formatted as an IRC message. 311 312 @param command: The command or numeric to send. 313 @type command: L{unicode} 314 315 @param parameters: The parameters to send with the command. 316 @type parameters: A L{tuple} or L{list} of L{unicode} parameters 317 318 @param prefix: The prefix to send with the command. If not 319 given, no prefix is sent. 320 @type prefix: L{unicode} 321 322 @param tags: A dict of message tags. If not given, no message 323 tags are sent. The dict key should be the name of the tag 324 to send as a string; the value should be the unescaped value 325 to send with the tag, or either None or "" if no value is to 326 be sent with the tag. 327 @type tags: L{dict} of tags (L{unicode}) => values (L{unicode}) 328 @see: U{https://ircv3.net/specs/core/message-tags-3.2.html} 329 """ 330 if not command: 331 raise ValueError("IRC message requires a command.") 332 333 if " " in command or command[0] == ":": 334 # Not the ONLY way to screw up, but provides a little 335 # sanity checking to catch likely dumb mistakes. 336 raise ValueError(f'Invalid command: "{command}"') 337 338 if tags is None: 339 tags = {} 340 341 line = " ".join([command] + list(parameters)) 342 if prefix: 343 line = f":{prefix} {line}" 344 if tags: 345 tagStr = self._stringTags(tags) 346 line = f"@{tagStr} {line}" 347 self.sendLine(line) 348 349 if len(parameters) > 15: 350 log.msg( 351 "Message has %d parameters (RFC allows 15):\n%s" 352 % (len(parameters), line) 353 ) 354 355 def _stringTags(self, tags): 356 """ 357 Converts a tag dictionary to a string. 358 359 @param tags: The tag dict passed to sendMsg. 360 361 @rtype: L{unicode} 362 @return: IRCv3-format tag string 363 """ 364 self._validateTags(tags) 365 tagStrings = [] 366 for tag, value in tags.items(): 367 if value: 368 tagStrings.append(f"{tag}={self._escapeTagValue(value)}") 369 else: 370 tagStrings.append(tag) 371 return ";".join(tagStrings) 372 373 def _validateTags(self, tags): 374 """ 375 Checks the tag dict for errors and raises L{ValueError} if an 376 error is found. 377 378 @param tags: The tag dict passed to sendMsg. 379 """ 380 for tag, value in tags.items(): 381 if not tag: 382 raise ValueError("A tag name is required.") 383 for char in tag: 384 if not char.isalnum() and char not in ("-", "/", "."): 385 raise ValueError("Tag contains invalid characters.") 386 387 def _escapeTagValue(self, value): 388 """ 389 Escape the given tag value according to U{escaping rules in IRCv3 390 <https://ircv3.net/specs/core/message-tags-3.2.html>}. 391 392 @param value: The string value to escape. 393 @type value: L{str} 394 395 @return: The escaped string for sending as a message value 396 @rtype: L{str} 397 """ 398 return ( 399 value.replace("\\", "\\\\") 400 .replace(";", "\\:") 401 .replace(" ", "\\s") 402 .replace("\r", "\\r") 403 .replace("\n", "\\n") 404 ) 405 406 def dataReceived(self, data): 407 """ 408 This hack is to support mIRC, which sends LF only, even though the RFC 409 says CRLF. (Also, the flexibility of LineReceiver to turn "line mode" 410 on and off was not required.) 411 """ 412 if isinstance(data, bytes): 413 data = data.decode("utf-8") 414 lines = (self.buffer + data).split(LF) 415 # Put the (possibly empty) element after the last LF back in the 416 # buffer 417 self.buffer = lines.pop() 418 419 for line in lines: 420 if len(line) <= 2: 421 # This is a blank line, at best. 422 continue 423 if line[-1] == CR: 424 line = line[:-1] 425 prefix, command, params = parsemsg(line) 426 # mIRC is a big pile of doo-doo 427 command = command.upper() 428 # DEBUG: log.msg( "%s %s %s" % (prefix, command, params)) 429 430 self.handleCommand(command, prefix, params) 431 432 def handleCommand(self, command, prefix, params): 433 """ 434 Determine the function to call for the given command and call it with 435 the given arguments. 436 437 @param command: The IRC command to determine the function for. 438 @type command: L{bytes} 439 440 @param prefix: The prefix of the IRC message (as returned by 441 L{parsemsg}). 442 @type prefix: L{bytes} 443 444 @param params: A list of parameters to call the function with. 445 @type params: L{list} 446 """ 447 method = getattr(self, "irc_%s" % command, None) 448 try: 449 if method is not None: 450 method(prefix, params) 451 else: 452 self.irc_unknown(prefix, command, params) 453 except BaseException: 454 log.deferr() 455 456 def irc_unknown(self, prefix, command, params): 457 """ 458 Called by L{handleCommand} on a command that doesn't have a defined 459 handler. Subclasses should override this method. 460 """ 461 raise NotImplementedError(command, prefix, params) 462 463 # Helper methods 464 def privmsg(self, sender, recip, message): 465 """ 466 Send a message to a channel or user 467 468 @type sender: C{str} or C{unicode} 469 @param sender: Who is sending this message. Should be of the form 470 username!ident@hostmask (unless you know better!). 471 472 @type recip: C{str} or C{unicode} 473 @param recip: The recipient of this message. If a channel, it must 474 start with a channel prefix. 475 476 @type message: C{str} or C{unicode} 477 @param message: The message being sent. 478 """ 479 self.sendCommand("PRIVMSG", (recip, f":{lowQuote(message)}"), sender) 480 481 def notice(self, sender, recip, message): 482 """ 483 Send a "notice" to a channel or user. 484 485 Notices differ from privmsgs in that the RFC claims they are different. 486 Robots are supposed to send notices and not respond to them. Clients 487 typically display notices differently from privmsgs. 488 489 @type sender: C{str} or C{unicode} 490 @param sender: Who is sending this message. Should be of the form 491 username!ident@hostmask (unless you know better!). 492 493 @type recip: C{str} or C{unicode} 494 @param recip: The recipient of this message. If a channel, it must 495 start with a channel prefix. 496 497 @type message: C{str} or C{unicode} 498 @param message: The message being sent. 499 """ 500 self.sendCommand("NOTICE", (recip, f":{message}"), sender) 501 502 def action(self, sender, recip, message): 503 """ 504 Send an action to a channel or user. 505 506 @type sender: C{str} or C{unicode} 507 @param sender: Who is sending this message. Should be of the form 508 username!ident@hostmask (unless you know better!). 509 510 @type recip: C{str} or C{unicode} 511 @param recip: The recipient of this message. If a channel, it must 512 start with a channel prefix. 513 514 @type message: C{str} or C{unicode} 515 @param message: The action being sent. 516 """ 517 self.sendLine(f":{sender} ACTION {recip} :{message}") 518 519 def topic(self, user, channel, topic, author=None): 520 """ 521 Send the topic to a user. 522 523 @type user: C{str} or C{unicode} 524 @param user: The user receiving the topic. Only their nickname, not 525 the full hostmask. 526 527 @type channel: C{str} or C{unicode} 528 @param channel: The channel for which this is the topic. 529 530 @type topic: C{str} or C{unicode} or L{None} 531 @param topic: The topic string, unquoted, or None if there is no topic. 532 533 @type author: C{str} or C{unicode} 534 @param author: If the topic is being changed, the full username and 535 hostmask of the person changing it. 536 """ 537 if author is None: 538 if topic is None: 539 self.sendLine( 540 ":%s %s %s %s :%s" 541 % (self.hostname, RPL_NOTOPIC, user, channel, "No topic is set.") 542 ) 543 else: 544 self.sendLine( 545 ":%s %s %s %s :%s" 546 % (self.hostname, RPL_TOPIC, user, channel, lowQuote(topic)) 547 ) 548 else: 549 self.sendLine(f":{author} TOPIC {channel} :{lowQuote(topic)}") 550 551 def topicAuthor(self, user, channel, author, date): 552 """ 553 Send the author of and time at which a topic was set for the given 554 channel. 555 556 This sends a 333 reply message, which is not part of the IRC RFC. 557 558 @type user: C{str} or C{unicode} 559 @param user: The user receiving the topic. Only their nickname, not 560 the full hostmask. 561 562 @type channel: C{str} or C{unicode} 563 @param channel: The channel for which this information is relevant. 564 565 @type author: C{str} or C{unicode} 566 @param author: The nickname (without hostmask) of the user who last set 567 the topic. 568 569 @type date: C{int} 570 @param date: A POSIX timestamp (number of seconds since the epoch) at 571 which the topic was last set. 572 """ 573 self.sendLine( 574 ":%s %d %s %s %s %d" % (self.hostname, 333, user, channel, author, date) 575 ) 576 577 def names(self, user, channel, names): 578 """ 579 Send the names of a channel's participants to a user. 580 581 @type user: C{str} or C{unicode} 582 @param user: The user receiving the name list. Only their nickname, 583 not the full hostmask. 584 585 @type channel: C{str} or C{unicode} 586 @param channel: The channel for which this is the namelist. 587 588 @type names: C{list} of C{str} or C{unicode} 589 @param names: The names to send. 590 """ 591 # XXX If unicode is given, these limits are not quite correct 592 prefixLength = len(channel) + len(user) + 10 593 namesLength = 512 - prefixLength 594 595 L = [] 596 count = 0 597 for n in names: 598 if count + len(n) + 1 > namesLength: 599 self.sendLine( 600 ":%s %s %s = %s :%s" 601 % (self.hostname, RPL_NAMREPLY, user, channel, " ".join(L)) 602 ) 603 L = [n] 604 count = len(n) 605 else: 606 L.append(n) 607 count += len(n) + 1 608 if L: 609 self.sendLine( 610 ":%s %s %s = %s :%s" 611 % (self.hostname, RPL_NAMREPLY, user, channel, " ".join(L)) 612 ) 613 self.sendLine( 614 ":%s %s %s %s :End of /NAMES list" 615 % (self.hostname, RPL_ENDOFNAMES, user, channel) 616 ) 617 618 def who(self, user, channel, memberInfo): 619 """ 620 Send a list of users participating in a channel. 621 622 @type user: C{str} or C{unicode} 623 @param user: The user receiving this member information. Only their 624 nickname, not the full hostmask. 625 626 @type channel: C{str} or C{unicode} 627 @param channel: The channel for which this is the member information. 628 629 @type memberInfo: C{list} of C{tuples} 630 @param memberInfo: For each member of the given channel, a 7-tuple 631 containing their username, their hostmask, the server to which they 632 are connected, their nickname, the letter "H" or "G" (standing for 633 "Here" or "Gone"), the hopcount from C{user} to this member, and 634 this member's real name. 635 """ 636 for info in memberInfo: 637 (username, hostmask, server, nickname, flag, hops, realName) = info 638 assert flag in ("H", "G") 639 self.sendLine( 640 ":%s %s %s %s %s %s %s %s %s :%d %s" 641 % ( 642 self.hostname, 643 RPL_WHOREPLY, 644 user, 645 channel, 646 username, 647 hostmask, 648 server, 649 nickname, 650 flag, 651 hops, 652 realName, 653 ) 654 ) 655 656 self.sendLine( 657 ":%s %s %s %s :End of /WHO list." 658 % (self.hostname, RPL_ENDOFWHO, user, channel) 659 ) 660 661 def whois( 662 self, 663 user, 664 nick, 665 username, 666 hostname, 667 realName, 668 server, 669 serverInfo, 670 oper, 671 idle, 672 signOn, 673 channels, 674 ): 675 """ 676 Send information about the state of a particular user. 677 678 @type user: C{str} or C{unicode} 679 @param user: The user receiving this information. Only their nickname, 680 not the full hostmask. 681 682 @type nick: C{str} or C{unicode} 683 @param nick: The nickname of the user this information describes. 684 685 @type username: C{str} or C{unicode} 686 @param username: The user's username (eg, ident response) 687 688 @type hostname: C{str} 689 @param hostname: The user's hostmask 690 691 @type realName: C{str} or C{unicode} 692 @param realName: The user's real name 693 694 @type server: C{str} or C{unicode} 695 @param server: The name of the server to which the user is connected 696 697 @type serverInfo: C{str} or C{unicode} 698 @param serverInfo: A descriptive string about that server 699 700 @type oper: C{bool} 701 @param oper: Indicates whether the user is an IRC operator 702 703 @type idle: C{int} 704 @param idle: The number of seconds since the user last sent a message 705 706 @type signOn: C{int} 707 @param signOn: A POSIX timestamp (number of seconds since the epoch) 708 indicating the time the user signed on 709 710 @type channels: C{list} of C{str} or C{unicode} 711 @param channels: A list of the channels which the user is participating in 712 """ 713 self.sendLine( 714 ":%s %s %s %s %s %s * :%s" 715 % (self.hostname, RPL_WHOISUSER, user, nick, username, hostname, realName) 716 ) 717 self.sendLine( 718 ":%s %s %s %s %s :%s" 719 % (self.hostname, RPL_WHOISSERVER, user, nick, server, serverInfo) 720 ) 721 if oper: 722 self.sendLine( 723 ":%s %s %s %s :is an IRC operator" 724 % (self.hostname, RPL_WHOISOPERATOR, user, nick) 725 ) 726 self.sendLine( 727 ":%s %s %s %s %d %d :seconds idle, signon time" 728 % (self.hostname, RPL_WHOISIDLE, user, nick, idle, signOn) 729 ) 730 self.sendLine( 731 ":%s %s %s %s :%s" 732 % (self.hostname, RPL_WHOISCHANNELS, user, nick, " ".join(channels)) 733 ) 734 self.sendLine( 735 ":%s %s %s %s :End of WHOIS list." 736 % (self.hostname, RPL_ENDOFWHOIS, user, nick) 737 ) 738 739 def join(self, who, where): 740 """ 741 Send a join message. 742 743 @type who: C{str} or C{unicode} 744 @param who: The name of the user joining. Should be of the form 745 username!ident@hostmask (unless you know better!). 746 747 @type where: C{str} or C{unicode} 748 @param where: The channel the user is joining. 749 """ 750 self.sendLine(f":{who} JOIN {where}") 751 752 def part(self, who, where, reason=None): 753 """ 754 Send a part message. 755 756 @type who: C{str} or C{unicode} 757 @param who: The name of the user joining. Should be of the form 758 username!ident@hostmask (unless you know better!). 759 760 @type where: C{str} or C{unicode} 761 @param where: The channel the user is joining. 762 763 @type reason: C{str} or C{unicode} 764 @param reason: A string describing the misery which caused this poor 765 soul to depart. 766 """ 767 if reason: 768 self.sendLine(f":{who} PART {where} :{reason}") 769 else: 770 self.sendLine(f":{who} PART {where}") 771 772 def channelMode(self, user, channel, mode, *args): 773 """ 774 Send information about the mode of a channel. 775 776 @type user: C{str} or C{unicode} 777 @param user: The user receiving the name list. Only their nickname, 778 not the full hostmask. 779 780 @type channel: C{str} or C{unicode} 781 @param channel: The channel for which this is the namelist. 782 783 @type mode: C{str} 784 @param mode: A string describing this channel's modes. 785 786 @param args: Any additional arguments required by the modes. 787 """ 788 self.sendLine( 789 ":%s %s %s %s %s %s" 790 % (self.hostname, RPL_CHANNELMODEIS, user, channel, mode, " ".join(args)) 791 ) 792 793 794class ServerSupportedFeatures(_CommandDispatcherMixin): 795 """ 796 Handle ISUPPORT messages. 797 798 Feature names match those in the ISUPPORT RFC draft identically. 799 800 Information regarding the specifics of ISUPPORT was gleaned from 801 <http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt>. 802 """ 803 804 prefix = "isupport" 805 806 def __init__(self): 807 self._features = { 808 "CHANNELLEN": 200, 809 "CHANTYPES": tuple("#&"), 810 "MODES": 3, 811 "NICKLEN": 9, 812 "PREFIX": self._parsePrefixParam("(ovh)@+%"), 813 # The ISUPPORT draft explicitly says that there is no default for 814 # CHANMODES, but we're defaulting it here to handle the case where 815 # the IRC server doesn't send us any ISUPPORT information, since 816 # IRCClient.getChannelModeParams relies on this value. 817 "CHANMODES": self._parseChanModesParam(["b", "", "lk", ""]), 818 } 819 820 @classmethod 821 def _splitParamArgs(cls, params, valueProcessor=None): 822 """ 823 Split ISUPPORT parameter arguments. 824 825 Values can optionally be processed by C{valueProcessor}. 826 827 For example:: 828 829 >>> ServerSupportedFeatures._splitParamArgs(['A:1', 'B:2']) 830 (('A', '1'), ('B', '2')) 831 832 @type params: C{iterable} of C{str} 833 834 @type valueProcessor: C{callable} taking {str} 835 @param valueProcessor: Callable to process argument values, or L{None} 836 to perform no processing 837 838 @rtype: C{list} of C{(str, object)} 839 @return: Sequence of C{(name, processedValue)} 840 """ 841 if valueProcessor is None: 842 valueProcessor = lambda x: x 843 844 def _parse(): 845 for param in params: 846 if ":" not in param: 847 param += ":" 848 a, b = param.split(":", 1) 849 yield a, valueProcessor(b) 850 851 return list(_parse()) 852 853 @classmethod 854 def _unescapeParamValue(cls, value): 855 """ 856 Unescape an ISUPPORT parameter. 857 858 The only form of supported escape is C{\\xHH}, where HH must be a valid 859 2-digit hexadecimal number. 860 861 @rtype: C{str} 862 """ 863 864 def _unescape(): 865 parts = value.split("\\x") 866 # The first part can never be preceded by the escape. 867 yield parts.pop(0) 868 for s in parts: 869 octet, rest = s[:2], s[2:] 870 try: 871 octet = int(octet, 16) 872 except ValueError: 873 raise ValueError(f"Invalid hex octet: {octet!r}") 874 yield chr(octet) + rest 875 876 if "\\x" not in value: 877 return value 878 return "".join(_unescape()) 879 880 @classmethod 881 def _splitParam(cls, param): 882 """ 883 Split an ISUPPORT parameter. 884 885 @type param: C{str} 886 887 @rtype: C{(str, list)} 888 @return: C{(key, arguments)} 889 """ 890 if "=" not in param: 891 param += "=" 892 key, value = param.split("=", 1) 893 return key, [cls._unescapeParamValue(v) for v in value.split(",")] 894 895 @classmethod 896 def _parsePrefixParam(cls, prefix): 897 """ 898 Parse the ISUPPORT "PREFIX" parameter. 899 900 The order in which the parameter arguments appear is significant, the 901 earlier a mode appears the more privileges it gives. 902 903 @rtype: C{dict} mapping C{str} to C{(str, int)} 904 @return: A dictionary mapping a mode character to a two-tuple of 905 C({symbol, priority)}, the lower a priority (the lowest being 906 C{0}) the more privileges it gives 907 """ 908 if not prefix: 909 return None 910 if prefix[0] != "(" and ")" not in prefix: 911 raise ValueError("Malformed PREFIX parameter") 912 modes, symbols = prefix.split(")", 1) 913 symbols = zip(symbols, range(len(symbols))) 914 modes = modes[1:] 915 return dict(zip(modes, symbols)) 916 917 @classmethod 918 def _parseChanModesParam(self, params): 919 """ 920 Parse the ISUPPORT "CHANMODES" parameter. 921 922 See L{isupport_CHANMODES} for a detailed explanation of this parameter. 923 """ 924 names = ("addressModes", "param", "setParam", "noParam") 925 if len(params) > len(names): 926 raise ValueError( 927 "Expecting a maximum of %d channel mode parameters, got %d" 928 % (len(names), len(params)) 929 ) 930 items = map(lambda key, value: (key, value or ""), names, params) 931 return dict(items) 932 933 def getFeature(self, feature, default=None): 934 """ 935 Get a server supported feature's value. 936 937 A feature with the value L{None} is equivalent to the feature being 938 unsupported. 939 940 @type feature: C{str} 941 @param feature: Feature name 942 943 @type default: C{object} 944 @param default: The value to default to, assuming that C{feature} 945 is not supported 946 947 @return: Feature value 948 """ 949 return self._features.get(feature, default) 950 951 def hasFeature(self, feature): 952 """ 953 Determine whether a feature is supported or not. 954 955 @rtype: C{bool} 956 """ 957 return self.getFeature(feature) is not None 958 959 def parse(self, params): 960 """ 961 Parse ISUPPORT parameters. 962 963 If an unknown parameter is encountered, it is simply added to the 964 dictionary, keyed by its name, as a tuple of the parameters provided. 965 966 @type params: C{iterable} of C{str} 967 @param params: Iterable of ISUPPORT parameters to parse 968 """ 969 for param in params: 970 key, value = self._splitParam(param) 971 if key.startswith("-"): 972 self._features.pop(key[1:], None) 973 else: 974 self._features[key] = self.dispatch(key, value) 975 976 def isupport_unknown(self, command, params): 977 """ 978 Unknown ISUPPORT parameter. 979 """ 980 return tuple(params) 981 982 def isupport_CHANLIMIT(self, params): 983 """ 984 The maximum number of each channel type a user may join. 985 """ 986 return self._splitParamArgs(params, _intOrDefault) 987 988 def isupport_CHANMODES(self, params): 989 """ 990 Available channel modes. 991 992 There are 4 categories of channel mode:: 993 994 addressModes - Modes that add or remove an address to or from a 995 list, these modes always take a parameter. 996 997 param - Modes that change a setting on a channel, these modes 998 always take a parameter. 999 1000 setParam - Modes that change a setting on a channel, these modes 1001 only take a parameter when being set. 1002 1003 noParam - Modes that change a setting on a channel, these modes 1004 never take a parameter. 1005 """ 1006 try: 1007 return self._parseChanModesParam(params) 1008 except ValueError: 1009 return self.getFeature("CHANMODES") 1010 1011 def isupport_CHANNELLEN(self, params): 1012 """ 1013 Maximum length of a channel name a client may create. 1014 """ 1015 return _intOrDefault(params[0], self.getFeature("CHANNELLEN")) 1016 1017 def isupport_CHANTYPES(self, params): 1018 """ 1019 Valid channel prefixes. 1020 """ 1021 return tuple(params[0]) 1022 1023 def isupport_EXCEPTS(self, params): 1024 """ 1025 Mode character for "ban exceptions". 1026 1027 The presence of this parameter indicates that the server supports 1028 this functionality. 1029 """ 1030 return params[0] or "e" 1031 1032 def isupport_IDCHAN(self, params): 1033 """ 1034 Safe channel identifiers. 1035 1036 The presence of this parameter indicates that the server supports 1037 this functionality. 1038 """ 1039 return self._splitParamArgs(params) 1040 1041 def isupport_INVEX(self, params): 1042 """ 1043 Mode character for "invite exceptions". 1044 1045 The presence of this parameter indicates that the server supports 1046 this functionality. 1047 """ 1048 return params[0] or "I" 1049 1050 def isupport_KICKLEN(self, params): 1051 """ 1052 Maximum length of a kick message a client may provide. 1053 """ 1054 return _intOrDefault(params[0]) 1055 1056 def isupport_MAXLIST(self, params): 1057 """ 1058 Maximum number of "list modes" a client may set on a channel at once. 1059 1060 List modes are identified by the "addressModes" key in CHANMODES. 1061 """ 1062 return self._splitParamArgs(params, _intOrDefault) 1063 1064 def isupport_MODES(self, params): 1065 """ 1066 Maximum number of modes accepting parameters that may be sent, by a 1067 client, in a single MODE command. 1068 """ 1069 return _intOrDefault(params[0]) 1070 1071 def isupport_NETWORK(self, params): 1072 """ 1073 IRC network name. 1074 """ 1075 return params[0] 1076 1077 def isupport_NICKLEN(self, params): 1078 """ 1079 Maximum length of a nickname the client may use. 1080 """ 1081 return _intOrDefault(params[0], self.getFeature("NICKLEN")) 1082 1083 def isupport_PREFIX(self, params): 1084 """ 1085 Mapping of channel modes that clients may have to status flags. 1086 """ 1087 try: 1088 return self._parsePrefixParam(params[0]) 1089 except ValueError: 1090 return self.getFeature("PREFIX") 1091 1092 def isupport_SAFELIST(self, params): 1093 """ 1094 Flag indicating that a client may request a LIST without being 1095 disconnected due to the large amount of data generated. 1096 """ 1097 return True 1098 1099 def isupport_STATUSMSG(self, params): 1100 """ 1101 The server supports sending messages to only to clients on a channel 1102 with a specific status. 1103 """ 1104 return params[0] 1105 1106 def isupport_TARGMAX(self, params): 1107 """ 1108 Maximum number of targets allowable for commands that accept multiple 1109 targets. 1110 """ 1111 return dict(self._splitParamArgs(params, _intOrDefault)) 1112 1113 def isupport_TOPICLEN(self, params): 1114 """ 1115 Maximum length of a topic that may be set. 1116 """ 1117 return _intOrDefault(params[0]) 1118 1119 1120class IRCClient(basic.LineReceiver): 1121 """ 1122 Internet Relay Chat client protocol, with sprinkles. 1123 1124 In addition to providing an interface for an IRC client protocol, 1125 this class also contains reasonable implementations of many common 1126 CTCP methods. 1127 1128 TODO 1129 ==== 1130 - Limit the length of messages sent (because the IRC server probably 1131 does). 1132 - Add flood protection/rate limiting for my CTCP replies. 1133 - NickServ cooperation. (a mix-in?) 1134 1135 @ivar nickname: Nickname the client will use. 1136 @ivar password: Password used to log on to the server. May be L{None}. 1137 @ivar realname: Supplied to the server during login as the "Real name" 1138 or "ircname". May be L{None}. 1139 @ivar username: Supplied to the server during login as the "User name". 1140 May be L{None} 1141 1142 @ivar userinfo: Sent in reply to a C{USERINFO} CTCP query. If L{None}, no 1143 USERINFO reply will be sent. 1144 "This is used to transmit a string which is settable by 1145 the user (and never should be set by the client)." 1146 @ivar fingerReply: Sent in reply to a C{FINGER} CTCP query. If L{None}, no 1147 FINGER reply will be sent. 1148 @type fingerReply: Callable or String 1149 1150 @ivar versionName: CTCP VERSION reply, client name. If L{None}, no VERSION 1151 reply will be sent. 1152 @type versionName: C{str}, or None. 1153 @ivar versionNum: CTCP VERSION reply, client version. 1154 @type versionNum: C{str}, or None. 1155 @ivar versionEnv: CTCP VERSION reply, environment the client is running in. 1156 @type versionEnv: C{str}, or None. 1157 1158 @ivar sourceURL: CTCP SOURCE reply, a URL where the source code of this 1159 client may be found. If L{None}, no SOURCE reply will be sent. 1160 1161 @ivar lineRate: Minimum delay between lines sent to the server. If 1162 L{None}, no delay will be imposed. 1163 @type lineRate: Number of Seconds. 1164 1165 @ivar motd: Either L{None} or, between receipt of I{RPL_MOTDSTART} and 1166 I{RPL_ENDOFMOTD}, a L{list} of L{str}, each of which is the content 1167 of an I{RPL_MOTD} message. 1168 1169 @ivar erroneousNickFallback: Default nickname assigned when an unregistered 1170 client triggers an C{ERR_ERRONEUSNICKNAME} while trying to register 1171 with an illegal nickname. 1172 @type erroneousNickFallback: C{str} 1173 1174 @ivar _registered: Whether or not the user is registered. It becomes True 1175 once a welcome has been received from the server. 1176 @type _registered: C{bool} 1177 1178 @ivar _attemptedNick: The nickname that will try to get registered. It may 1179 change if it is illegal or already taken. L{nickname} becomes the 1180 L{_attemptedNick} that is successfully registered. 1181 @type _attemptedNick: C{str} 1182 1183 @type supported: L{ServerSupportedFeatures} 1184 @ivar supported: Available ISUPPORT features on the server 1185 1186 @type hostname: C{str} 1187 @ivar hostname: Host name of the IRC server the client is connected to. 1188 Initially the host name is L{None} and later is set to the host name 1189 from which the I{RPL_WELCOME} message is received. 1190 1191 @type _heartbeat: L{task.LoopingCall} 1192 @ivar _heartbeat: Looping call to perform the keepalive by calling 1193 L{IRCClient._sendHeartbeat} every L{heartbeatInterval} seconds, or 1194 L{None} if there is no heartbeat. 1195 1196 @type heartbeatInterval: C{float} 1197 @ivar heartbeatInterval: Interval, in seconds, to send I{PING} messages to 1198 the server as a form of keepalive, defaults to 120 seconds. Use L{None} 1199 to disable the heartbeat. 1200 """ 1201 1202 hostname = None 1203 motd = None 1204 nickname = "irc" 1205 password = None 1206 realname = None 1207 username = None 1208 ### Responses to various CTCP queries. 1209 1210 userinfo = None 1211 # fingerReply is a callable returning a string, or a str()able object. 1212 fingerReply = None 1213 versionName = None 1214 versionNum = None 1215 versionEnv = None 1216 1217 sourceURL = "http://twistedmatrix.com/downloads/" 1218 1219 dcc_destdir = "." 1220 dcc_sessions = None 1221 1222 # If this is false, no attempt will be made to identify 1223 # ourself to the server. 1224 performLogin = 1 1225 1226 lineRate = None 1227 _queue = None 1228 _queueEmptying = None 1229 1230 delimiter = b"\n" # b'\r\n' will also work (see dataReceived) 1231 1232 __pychecker__ = "unusednames=params,prefix,channel" 1233 1234 _registered = False 1235 _attemptedNick = "" 1236 erroneousNickFallback = "defaultnick" 1237 1238 _heartbeat = None 1239 heartbeatInterval = 120 1240 1241 def _reallySendLine(self, line): 1242 quoteLine = lowQuote(line) 1243 if isinstance(quoteLine, str): 1244 quoteLine = quoteLine.encode("utf-8") 1245 quoteLine += b"\r" 1246 return basic.LineReceiver.sendLine(self, quoteLine) 1247 1248 def sendLine(self, line): 1249 if self.lineRate is None: 1250 self._reallySendLine(line) 1251 else: 1252 self._queue.append(line) 1253 if not self._queueEmptying: 1254 self._sendLine() 1255 1256 def _sendLine(self): 1257 if self._queue: 1258 self._reallySendLine(self._queue.pop(0)) 1259 self._queueEmptying = reactor.callLater(self.lineRate, self._sendLine) 1260 else: 1261 self._queueEmptying = None 1262 1263 def connectionLost(self, reason): 1264 basic.LineReceiver.connectionLost(self, reason) 1265 self.stopHeartbeat() 1266 1267 def _createHeartbeat(self): 1268 """ 1269 Create the heartbeat L{LoopingCall}. 1270 """ 1271 return task.LoopingCall(self._sendHeartbeat) 1272 1273 def _sendHeartbeat(self): 1274 """ 1275 Send a I{PING} message to the IRC server as a form of keepalive. 1276 """ 1277 self.sendLine("PING " + self.hostname) 1278 1279 def stopHeartbeat(self): 1280 """ 1281 Stop sending I{PING} messages to keep the connection to the server 1282 alive. 1283 1284 @since: 11.1 1285 """ 1286 if self._heartbeat is not None: 1287 self._heartbeat.stop() 1288 self._heartbeat = None 1289 1290 def startHeartbeat(self): 1291 """ 1292 Start sending I{PING} messages every L{IRCClient.heartbeatInterval} 1293 seconds to keep the connection to the server alive during periods of no 1294 activity. 1295 1296 @since: 11.1 1297 """ 1298 self.stopHeartbeat() 1299 if self.heartbeatInterval is None: 1300 return 1301 self._heartbeat = self._createHeartbeat() 1302 self._heartbeat.start(self.heartbeatInterval, now=False) 1303 1304 ### Interface level client->user output methods 1305 ### 1306 ### You'll want to override these. 1307 1308 ### Methods relating to the server itself 1309 1310 def created(self, when): 1311 """ 1312 Called with creation date information about the server, usually at logon. 1313 1314 @type when: C{str} 1315 @param when: A string describing when the server was created, probably. 1316 """ 1317 1318 def yourHost(self, info): 1319 """ 1320 Called with daemon information about the server, usually at logon. 1321 1322 @type info: C{str} 1323 @param info: A string describing what software the server is running, probably. 1324 """ 1325 1326 def myInfo(self, servername, version, umodes, cmodes): 1327 """ 1328 Called with information about the server, usually at logon. 1329 1330 @type servername: C{str} 1331 @param servername: The hostname of this server. 1332 1333 @type version: C{str} 1334 @param version: A description of what software this server runs. 1335 1336 @type umodes: C{str} 1337 @param umodes: All the available user modes. 1338 1339 @type cmodes: C{str} 1340 @param cmodes: All the available channel modes. 1341 """ 1342 1343 def luserClient(self, info): 1344 """ 1345 Called with information about the number of connections, usually at logon. 1346 1347 @type info: C{str} 1348 @param info: A description of the number of clients and servers 1349 connected to the network, probably. 1350 """ 1351 1352 def bounce(self, info): 1353 """ 1354 Called with information about where the client should reconnect. 1355 1356 @type info: C{str} 1357 @param info: A plaintext description of the address that should be 1358 connected to. 1359 """ 1360 1361 def isupport(self, options): 1362 """ 1363 Called with various information about what the server supports. 1364 1365 @type options: C{list} of C{str} 1366 @param options: Descriptions of features or limits of the server, possibly 1367 in the form "NAME=VALUE". 1368 """ 1369 1370 def luserChannels(self, channels): 1371 """ 1372 Called with the number of channels existent on the server. 1373 1374 @type channels: C{int} 1375 """ 1376 1377 def luserOp(self, ops): 1378 """ 1379 Called with the number of ops logged on to the server. 1380 1381 @type ops: C{int} 1382 """ 1383 1384 def luserMe(self, info): 1385 """ 1386 Called with information about the server connected to. 1387 1388 @type info: C{str} 1389 @param info: A plaintext string describing the number of users and servers 1390 connected to this server. 1391 """ 1392 1393 ### Methods involving me directly 1394 1395 def privmsg(self, user, channel, message): 1396 """ 1397 Called when I have a message from a user to me or a channel. 1398 """ 1399 pass 1400 1401 def joined(self, channel): 1402 """ 1403 Called when I finish joining a channel. 1404 1405 channel has the starting character (C{'#'}, C{'&'}, C{'!'}, or C{'+'}) 1406 intact. 1407 """ 1408 1409 def left(self, channel): 1410 """ 1411 Called when I have left a channel. 1412 1413 channel has the starting character (C{'#'}, C{'&'}, C{'!'}, or C{'+'}) 1414 intact. 1415 """ 1416 1417 def noticed(self, user, channel, message): 1418 """ 1419 Called when I have a notice from a user to me or a channel. 1420 1421 If the client makes any automated replies, it must not do so in 1422 response to a NOTICE message, per the RFC:: 1423 1424 The difference between NOTICE and PRIVMSG is that 1425 automatic replies MUST NEVER be sent in response to a 1426 NOTICE message. [...] The object of this rule is to avoid 1427 loops between clients automatically sending something in 1428 response to something it received. 1429 """ 1430 1431 def modeChanged(self, user, channel, set, modes, args): 1432 """ 1433 Called when users or channel's modes are changed. 1434 1435 @type user: C{str} 1436 @param user: The user and hostmask which instigated this change. 1437 1438 @type channel: C{str} 1439 @param channel: The channel where the modes are changed. If args is 1440 empty the channel for which the modes are changing. If the changes are 1441 at server level it could be equal to C{user}. 1442 1443 @type set: C{bool} or C{int} 1444 @param set: True if the mode(s) is being added, False if it is being 1445 removed. If some modes are added and others removed at the same time 1446 this function will be called twice, the first time with all the added 1447 modes, the second with the removed ones. (To change this behaviour 1448 override the irc_MODE method) 1449 1450 @type modes: C{str} 1451 @param modes: The mode or modes which are being changed. 1452 1453 @type args: C{tuple} 1454 @param args: Any additional information required for the mode 1455 change. 1456 """ 1457 1458 def pong(self, user, secs): 1459 """ 1460 Called with the results of a CTCP PING query. 1461 """ 1462 pass 1463 1464 def signedOn(self): 1465 """ 1466 Called after successfully signing on to the server. 1467 """ 1468 pass 1469 1470 def kickedFrom(self, channel, kicker, message): 1471 """ 1472 Called when I am kicked from a channel. 1473 """ 1474 pass 1475 1476 def nickChanged(self, nick): 1477 """ 1478 Called when my nick has been changed. 1479 """ 1480 self.nickname = nick 1481 1482 ### Things I observe other people doing in a channel. 1483 1484 def userJoined(self, user, channel): 1485 """ 1486 Called when I see another user joining a channel. 1487 """ 1488 pass 1489 1490 def userLeft(self, user, channel): 1491 """ 1492 Called when I see another user leaving a channel. 1493 """ 1494 pass 1495 1496 def userQuit(self, user, quitMessage): 1497 """ 1498 Called when I see another user disconnect from the network. 1499 """ 1500 pass 1501 1502 def userKicked(self, kickee, channel, kicker, message): 1503 """ 1504 Called when I observe someone else being kicked from a channel. 1505 """ 1506 pass 1507 1508 def action(self, user, channel, data): 1509 """ 1510 Called when I see a user perform an ACTION on a channel. 1511 """ 1512 pass 1513 1514 def topicUpdated(self, user, channel, newTopic): 1515 """ 1516 In channel, user changed the topic to newTopic. 1517 1518 Also called when first joining a channel. 1519 """ 1520 pass 1521 1522 def userRenamed(self, oldname, newname): 1523 """ 1524 A user changed their name from oldname to newname. 1525 """ 1526 pass 1527 1528 ### Information from the server. 1529 1530 def receivedMOTD(self, motd): 1531 """ 1532 I received a message-of-the-day banner from the server. 1533 1534 motd is a list of strings, where each string was sent as a separate 1535 message from the server. To display, you might want to use:: 1536 1537 '\\n'.join(motd) 1538 1539 to get a nicely formatted string. 1540 """ 1541 pass 1542 1543 ### user input commands, client->server 1544 ### Your client will want to invoke these. 1545 1546 def join(self, channel, key=None): 1547 """ 1548 Join a channel. 1549 1550 @type channel: C{str} 1551 @param channel: The name of the channel to join. If it has no prefix, 1552 C{'#'} will be prepended to it. 1553 @type key: C{str} 1554 @param key: If specified, the key used to join the channel. 1555 """ 1556 if channel[0] not in CHANNEL_PREFIXES: 1557 channel = "#" + channel 1558 if key: 1559 self.sendLine(f"JOIN {channel} {key}") 1560 else: 1561 self.sendLine(f"JOIN {channel}") 1562 1563 def leave(self, channel, reason=None): 1564 """ 1565 Leave a channel. 1566 1567 @type channel: C{str} 1568 @param channel: The name of the channel to leave. If it has no prefix, 1569 C{'#'} will be prepended to it. 1570 @type reason: C{str} 1571 @param reason: If given, the reason for leaving. 1572 """ 1573 if channel[0] not in CHANNEL_PREFIXES: 1574 channel = "#" + channel 1575 if reason: 1576 self.sendLine(f"PART {channel} :{reason}") 1577 else: 1578 self.sendLine(f"PART {channel}") 1579 1580 def kick(self, channel, user, reason=None): 1581 """ 1582 Attempt to kick a user from a channel. 1583 1584 @type channel: C{str} 1585 @param channel: The name of the channel to kick the user from. If it has 1586 no prefix, C{'#'} will be prepended to it. 1587 @type user: C{str} 1588 @param user: The nick of the user to kick. 1589 @type reason: C{str} 1590 @param reason: If given, the reason for kicking the user. 1591 """ 1592 if channel[0] not in CHANNEL_PREFIXES: 1593 channel = "#" + channel 1594 if reason: 1595 self.sendLine(f"KICK {channel} {user} :{reason}") 1596 else: 1597 self.sendLine(f"KICK {channel} {user}") 1598 1599 part = leave 1600 1601 def invite(self, user, channel): 1602 """ 1603 Attempt to invite user to channel 1604 1605 @type user: C{str} 1606 @param user: The user to invite 1607 @type channel: C{str} 1608 @param channel: The channel to invite the user too 1609 1610 @since: 11.0 1611 """ 1612 if channel[0] not in CHANNEL_PREFIXES: 1613 channel = "#" + channel 1614 self.sendLine(f"INVITE {user} {channel}") 1615 1616 def topic(self, channel, topic=None): 1617 """ 1618 Attempt to set the topic of the given channel, or ask what it is. 1619 1620 If topic is None, then I sent a topic query instead of trying to set the 1621 topic. The server should respond with a TOPIC message containing the 1622 current topic of the given channel. 1623 1624 @type channel: C{str} 1625 @param channel: The name of the channel to change the topic on. If it 1626 has no prefix, C{'#'} will be prepended to it. 1627 @type topic: C{str} 1628 @param topic: If specified, what to set the topic to. 1629 """ 1630 # << TOPIC #xtestx :fff 1631 if channel[0] not in CHANNEL_PREFIXES: 1632 channel = "#" + channel 1633 if topic != None: 1634 self.sendLine(f"TOPIC {channel} :{topic}") 1635 else: 1636 self.sendLine(f"TOPIC {channel}") 1637 1638 def mode(self, chan, set, modes, limit=None, user=None, mask=None): 1639 """ 1640 Change the modes on a user or channel. 1641 1642 The C{limit}, C{user}, and C{mask} parameters are mutually exclusive. 1643 1644 @type chan: C{str} 1645 @param chan: The name of the channel to operate on. 1646 @type set: C{bool} 1647 @param set: True to give the user or channel permissions and False to 1648 remove them. 1649 @type modes: C{str} 1650 @param modes: The mode flags to set on the user or channel. 1651 @type limit: C{int} 1652 @param limit: In conjunction with the C{'l'} mode flag, limits the 1653 number of users on the channel. 1654 @type user: C{str} 1655 @param user: The user to change the mode on. 1656 @type mask: C{str} 1657 @param mask: In conjunction with the C{'b'} mode flag, sets a mask of 1658 users to be banned from the channel. 1659 """ 1660 if set: 1661 line = f"MODE {chan} +{modes}" 1662 else: 1663 line = f"MODE {chan} -{modes}" 1664 if limit is not None: 1665 line = "%s %d" % (line, limit) 1666 elif user is not None: 1667 line = f"{line} {user}" 1668 elif mask is not None: 1669 line = f"{line} {mask}" 1670 self.sendLine(line) 1671 1672 def say(self, channel, message, length=None): 1673 """ 1674 Send a message to a channel 1675 1676 @type channel: C{str} 1677 @param channel: The channel to say the message on. If it has no prefix, 1678 C{'#'} will be prepended to it. 1679 @type message: C{str} 1680 @param message: The message to say. 1681 @type length: C{int} 1682 @param length: The maximum number of octets to send at a time. This has 1683 the effect of turning a single call to C{msg()} into multiple 1684 commands to the server. This is useful when long messages may be 1685 sent that would otherwise cause the server to kick us off or 1686 silently truncate the text we are sending. If None is passed, the 1687 entire message is always send in one command. 1688 """ 1689 if channel[0] not in CHANNEL_PREFIXES: 1690 channel = "#" + channel 1691 self.msg(channel, message, length) 1692 1693 def _safeMaximumLineLength(self, command): 1694 """ 1695 Estimate a safe maximum line length for the given command. 1696 1697 This is done by assuming the maximum values for nickname length, 1698 realname and hostname combined with the command that needs to be sent 1699 and some guessing. A theoretical maximum value is used because it is 1700 possible that our nickname, username or hostname changes (on the server 1701 side) while the length is still being calculated. 1702 """ 1703 # :nickname!realname@hostname COMMAND ... 1704 theoretical = ":{}!{}@{} {}".format( 1705 "a" * self.supported.getFeature("NICKLEN"), 1706 # This value is based on observation. 1707 "b" * 10, 1708 # See <http://tools.ietf.org/html/rfc2812#section-2.3.1>. 1709 "c" * 63, 1710 command, 1711 ) 1712 # Fingers crossed. 1713 fudge = 10 1714 return MAX_COMMAND_LENGTH - len(theoretical) - fudge 1715 1716 def msg(self, user, message, length=None): 1717 """ 1718 Send a message to a user or channel. 1719 1720 The message will be split into multiple commands to the server if: 1721 - The message contains any newline characters 1722 - Any span between newline characters is longer than the given 1723 line-length. 1724 1725 @param user: Username or channel name to which to direct the 1726 message. 1727 @type user: C{str} 1728 1729 @param message: Text to send. 1730 @type message: C{str} 1731 1732 @param length: Maximum number of octets to send in a single 1733 command, including the IRC protocol framing. If L{None} is given 1734 then L{IRCClient._safeMaximumLineLength} is used to determine a 1735 value. 1736 @type length: C{int} 1737 """ 1738 fmt = f"PRIVMSG {user} :" 1739 1740 if length is None: 1741 length = self._safeMaximumLineLength(fmt) 1742 1743 # Account for the line terminator. 1744 minimumLength = len(fmt) + 2 1745 if length <= minimumLength: 1746 raise ValueError( 1747 "Maximum length must exceed %d for message " 1748 "to %s" % (minimumLength, user) 1749 ) 1750 for line in split(message, length - minimumLength): 1751 self.sendLine(fmt + line) 1752 1753 def notice(self, user, message): 1754 """ 1755 Send a notice to a user. 1756 1757 Notices are like normal message, but should never get automated 1758 replies. 1759 1760 @type user: C{str} 1761 @param user: The user to send a notice to. 1762 @type message: C{str} 1763 @param message: The contents of the notice to send. 1764 """ 1765 self.sendLine(f"NOTICE {user} :{message}") 1766 1767 def away(self, message=""): 1768 """ 1769 Mark this client as away. 1770 1771 @type message: C{str} 1772 @param message: If specified, the away message. 1773 """ 1774 self.sendLine("AWAY :%s" % message) 1775 1776 def back(self): 1777 """ 1778 Clear the away status. 1779 """ 1780 # An empty away marks us as back 1781 self.away() 1782 1783 def whois(self, nickname, server=None): 1784 """ 1785 Retrieve user information about the given nickname. 1786 1787 @type nickname: C{str} 1788 @param nickname: The nickname about which to retrieve information. 1789 1790 @since: 8.2 1791 """ 1792 if server is None: 1793 self.sendLine("WHOIS " + nickname) 1794 else: 1795 self.sendLine(f"WHOIS {server} {nickname}") 1796 1797 def register(self, nickname, hostname="foo", servername="bar"): 1798 """ 1799 Login to the server. 1800 1801 @type nickname: C{str} 1802 @param nickname: The nickname to register. 1803 @type hostname: C{str} 1804 @param hostname: If specified, the hostname to logon as. 1805 @type servername: C{str} 1806 @param servername: If specified, the servername to logon as. 1807 """ 1808 if self.password is not None: 1809 self.sendLine("PASS %s" % self.password) 1810 self.setNick(nickname) 1811 if self.username is None: 1812 self.username = nickname 1813 self.sendLine( 1814 "USER {} {} {} :{}".format( 1815 self.username, hostname, servername, self.realname 1816 ) 1817 ) 1818 1819 def setNick(self, nickname): 1820 """ 1821 Set this client's nickname. 1822 1823 @type nickname: C{str} 1824 @param nickname: The nickname to change to. 1825 """ 1826 self._attemptedNick = nickname 1827 self.sendLine("NICK %s" % nickname) 1828 1829 def quit(self, message=""): 1830 """ 1831 Disconnect from the server 1832 1833 @type message: C{str} 1834 1835 @param message: If specified, the message to give when quitting the 1836 server. 1837 """ 1838 self.sendLine("QUIT :%s" % message) 1839 1840 ### user input commands, client->client 1841 1842 def describe(self, channel, action): 1843 """ 1844 Strike a pose. 1845 1846 @type channel: C{str} 1847 @param channel: The name of the channel to have an action on. If it 1848 has no prefix, it is sent to the user of that name. 1849 @type action: C{str} 1850 @param action: The action to preform. 1851 @since: 9.0 1852 """ 1853 self.ctcpMakeQuery(channel, [("ACTION", action)]) 1854 1855 _pings = None 1856 _MAX_PINGRING = 12 1857 1858 def ping(self, user, text=None): 1859 """ 1860 Measure round-trip delay to another IRC client. 1861 """ 1862 if self._pings is None: 1863 self._pings = {} 1864 1865 if text is None: 1866 chars = string.ascii_letters + string.digits + string.punctuation 1867 key = "".join([random.choice(chars) for i in range(12)]) 1868 else: 1869 key = str(text) 1870 self._pings[(user, key)] = time.time() 1871 self.ctcpMakeQuery(user, [("PING", key)]) 1872 1873 if len(self._pings) > self._MAX_PINGRING: 1874 # Remove some of the oldest entries. 1875 byValue = [(v, k) for (k, v) in self._pings.items()] 1876 byValue.sort() 1877 excess = len(self._pings) - self._MAX_PINGRING 1878 for i in range(excess): 1879 del self._pings[byValue[i][1]] 1880 1881 def dccSend(self, user, file): 1882 """ 1883 This is supposed to send a user a file directly. This generally 1884 doesn't work on any client, and this method is included only for 1885 backwards compatibility and completeness. 1886 1887 @param user: C{str} representing the user 1888 @param file: an open file (unknown, since this is not implemented) 1889 """ 1890 raise NotImplementedError( 1891 "XXX!!! Help! I need to bind a socket, have it listen, and tell me its address. " 1892 "(and stop accepting once we've made a single connection.)" 1893 ) 1894 1895 def dccResume(self, user, fileName, port, resumePos): 1896 """ 1897 Send a DCC RESUME request to another user. 1898 """ 1899 self.ctcpMakeQuery(user, [("DCC", ["RESUME", fileName, port, resumePos])]) 1900 1901 def dccAcceptResume(self, user, fileName, port, resumePos): 1902 """ 1903 Send a DCC ACCEPT response to clients who have requested a resume. 1904 """ 1905 self.ctcpMakeQuery(user, [("DCC", ["ACCEPT", fileName, port, resumePos])]) 1906 1907 ### server->client messages 1908 ### You might want to fiddle with these, 1909 ### but it is safe to leave them alone. 1910 1911 def irc_ERR_NICKNAMEINUSE(self, prefix, params): 1912 """ 1913 Called when we try to register or change to a nickname that is already 1914 taken. 1915 """ 1916 self._attemptedNick = self.alterCollidedNick(self._attemptedNick) 1917 self.setNick(self._attemptedNick) 1918 1919 def alterCollidedNick(self, nickname): 1920 """ 1921 Generate an altered version of a nickname that caused a collision in an 1922 effort to create an unused related name for subsequent registration. 1923 1924 @param nickname: The nickname a user is attempting to register. 1925 @type nickname: C{str} 1926 1927 @returns: A string that is in some way different from the nickname. 1928 @rtype: C{str} 1929 """ 1930 return nickname + "_" 1931 1932 def irc_ERR_ERRONEUSNICKNAME(self, prefix, params): 1933 """ 1934 Called when we try to register or change to an illegal nickname. 1935 1936 The server should send this reply when the nickname contains any 1937 disallowed characters. The bot will stall, waiting for RPL_WELCOME, if 1938 we don't handle this during sign-on. 1939 1940 @note: The method uses the spelling I{erroneus}, as it appears in 1941 the RFC, section 6.1. 1942 """ 1943 if not self._registered: 1944 self.setNick(self.erroneousNickFallback) 1945 1946 def irc_ERR_PASSWDMISMATCH(self, prefix, params): 1947 """ 1948 Called when the login was incorrect. 1949 """ 1950 raise IRCPasswordMismatch("Password Incorrect.") 1951 1952 def irc_RPL_WELCOME(self, prefix, params): 1953 """ 1954 Called when we have received the welcome from the server. 1955 """ 1956 self.hostname = prefix 1957 self._registered = True 1958 self.nickname = self._attemptedNick 1959 self.signedOn() 1960 self.startHeartbeat() 1961 1962 def irc_JOIN(self, prefix, params): 1963 """ 1964 Called when a user joins a channel. 1965 """ 1966 nick = prefix.split("!")[0] 1967 channel = params[-1] 1968 if nick == self.nickname: 1969 self.joined(channel) 1970 else: 1971 self.userJoined(nick, channel) 1972 1973 def irc_PART(self, prefix, params): 1974 """ 1975 Called when a user leaves a channel. 1976 """ 1977 nick = prefix.split("!")[0] 1978 channel = params[0] 1979 if nick == self.nickname: 1980 self.left(channel) 1981 else: 1982 self.userLeft(nick, channel) 1983 1984 def irc_QUIT(self, prefix, params): 1985 """ 1986 Called when a user has quit. 1987 """ 1988 nick = prefix.split("!")[0] 1989 self.userQuit(nick, params[0]) 1990 1991 def irc_MODE(self, user, params): 1992 """ 1993 Parse a server mode change message. 1994 """ 1995 channel, modes, args = params[0], params[1], params[2:] 1996 1997 if modes[0] not in "-+": 1998 modes = "+" + modes 1999 2000 if channel == self.nickname: 2001 # This is a mode change to our individual user, not a channel mode 2002 # that involves us. 2003 paramModes = self.getUserModeParams() 2004 else: 2005 paramModes = self.getChannelModeParams() 2006 2007 try: 2008 added, removed = parseModes(modes, args, paramModes) 2009 except IRCBadModes: 2010 log.err( 2011 None, 2012 "An error occurred while parsing the following " 2013 "MODE message: MODE %s" % (" ".join(params),), 2014 ) 2015 else: 2016 if added: 2017 modes, params = zip(*added) 2018 self.modeChanged(user, channel, True, "".join(modes), params) 2019 2020 if removed: 2021 modes, params = zip(*removed) 2022 self.modeChanged(user, channel, False, "".join(modes), params) 2023 2024 def irc_PING(self, prefix, params): 2025 """ 2026 Called when some has pinged us. 2027 """ 2028 self.sendLine("PONG %s" % params[-1]) 2029 2030 def irc_PRIVMSG(self, prefix, params): 2031 """ 2032 Called when we get a message. 2033 """ 2034 user = prefix 2035 channel = params[0] 2036 message = params[-1] 2037 2038 if not message: 2039 # Don't raise an exception if we get blank message. 2040 return 2041 2042 if message[0] == X_DELIM: 2043 m = ctcpExtract(message) 2044 if m["extended"]: 2045 self.ctcpQuery(user, channel, m["extended"]) 2046 2047 if not m["normal"]: 2048 return 2049 2050 message = " ".join(m["normal"]) 2051 2052 self.privmsg(user, channel, message) 2053 2054 def irc_NOTICE(self, prefix, params): 2055 """ 2056 Called when a user gets a notice. 2057 """ 2058 user = prefix 2059 channel = params[0] 2060 message = params[-1] 2061 2062 if message[0] == X_DELIM: 2063 m = ctcpExtract(message) 2064 if m["extended"]: 2065 self.ctcpReply(user, channel, m["extended"]) 2066 2067 if not m["normal"]: 2068 return 2069 2070 message = " ".join(m["normal"]) 2071 2072 self.noticed(user, channel, message) 2073 2074 def irc_NICK(self, prefix, params): 2075 """ 2076 Called when a user changes their nickname. 2077 """ 2078 nick = prefix.split("!", 1)[0] 2079 if nick == self.nickname: 2080 self.nickChanged(params[0]) 2081 else: 2082 self.userRenamed(nick, params[0]) 2083 2084 def irc_KICK(self, prefix, params): 2085 """ 2086 Called when a user is kicked from a channel. 2087 """ 2088 kicker = prefix.split("!")[0] 2089 channel = params[0] 2090 kicked = params[1] 2091 message = params[-1] 2092 if kicked.lower() == self.nickname.lower(): 2093 # Yikes! 2094 self.kickedFrom(channel, kicker, message) 2095 else: 2096 self.userKicked(kicked, channel, kicker, message) 2097 2098 def irc_TOPIC(self, prefix, params): 2099 """ 2100 Someone in the channel set the topic. 2101 """ 2102 user = prefix.split("!")[0] 2103 channel = params[0] 2104 newtopic = params[1] 2105 self.topicUpdated(user, channel, newtopic) 2106 2107 def irc_RPL_TOPIC(self, prefix, params): 2108 """ 2109 Called when the topic for a channel is initially reported or when it 2110 subsequently changes. 2111 """ 2112 user = prefix.split("!")[0] 2113 channel = params[1] 2114 newtopic = params[2] 2115 self.topicUpdated(user, channel, newtopic) 2116 2117 def irc_RPL_NOTOPIC(self, prefix, params): 2118 user = prefix.split("!")[0] 2119 channel = params[1] 2120 newtopic = "" 2121 self.topicUpdated(user, channel, newtopic) 2122 2123 def irc_RPL_MOTDSTART(self, prefix, params): 2124 if params[-1].startswith("- "): 2125 params[-1] = params[-1][2:] 2126 self.motd = [params[-1]] 2127 2128 def irc_RPL_MOTD(self, prefix, params): 2129 if params[-1].startswith("- "): 2130 params[-1] = params[-1][2:] 2131 if self.motd is None: 2132 self.motd = [] 2133 self.motd.append(params[-1]) 2134 2135 def irc_RPL_ENDOFMOTD(self, prefix, params): 2136 """ 2137 I{RPL_ENDOFMOTD} indicates the end of the message of the day 2138 messages. Deliver the accumulated lines to C{receivedMOTD}. 2139 """ 2140 motd = self.motd 2141 self.motd = None 2142 self.receivedMOTD(motd) 2143 2144 def irc_RPL_CREATED(self, prefix, params): 2145 self.created(params[1]) 2146 2147 def irc_RPL_YOURHOST(self, prefix, params): 2148 self.yourHost(params[1]) 2149 2150 def irc_RPL_MYINFO(self, prefix, params): 2151 info = params[1].split(None, 3) 2152 while len(info) < 4: 2153 info.append(None) 2154 self.myInfo(*info) 2155 2156 def irc_RPL_BOUNCE(self, prefix, params): 2157 self.bounce(params[1]) 2158 2159 def irc_RPL_ISUPPORT(self, prefix, params): 2160 args = params[1:-1] 2161 # Several ISUPPORT messages, in no particular order, may be sent 2162 # to the client at any given point in time (usually only on connect, 2163 # though.) For this reason, ServerSupportedFeatures.parse is intended 2164 # to mutate the supported feature list. 2165 self.supported.parse(args) 2166 self.isupport(args) 2167 2168 def irc_RPL_LUSERCLIENT(self, prefix, params): 2169 self.luserClient(params[1]) 2170 2171 def irc_RPL_LUSEROP(self, prefix, params): 2172 try: 2173 self.luserOp(int(params[1])) 2174 except ValueError: 2175 pass 2176 2177 def irc_RPL_LUSERCHANNELS(self, prefix, params): 2178 try: 2179 self.luserChannels(int(params[1])) 2180 except ValueError: 2181 pass 2182 2183 def irc_RPL_LUSERME(self, prefix, params): 2184 self.luserMe(params[1]) 2185 2186 def irc_unknown(self, prefix, command, params): 2187 pass 2188 2189 ### Receiving a CTCP query from another party 2190 ### It is safe to leave these alone. 2191 2192 def ctcpQuery(self, user, channel, messages): 2193 """ 2194 Dispatch method for any CTCP queries received. 2195 2196 Duplicated CTCP queries are ignored and no dispatch is 2197 made. Unrecognized CTCP queries invoke L{IRCClient.ctcpUnknownQuery}. 2198 """ 2199 seen = set() 2200 for tag, data in messages: 2201 method = getattr(self, "ctcpQuery_%s" % tag, None) 2202 if tag not in seen: 2203 if method is not None: 2204 method(user, channel, data) 2205 else: 2206 self.ctcpUnknownQuery(user, channel, tag, data) 2207 seen.add(tag) 2208 2209 def ctcpUnknownQuery(self, user, channel, tag, data): 2210 """ 2211 Fallback handler for unrecognized CTCP queries. 2212 2213 No CTCP I{ERRMSG} reply is made to remove a potential denial of service 2214 avenue. 2215 """ 2216 log.msg(f"Unknown CTCP query from {user!r}: {tag!r} {data!r}") 2217 2218 def ctcpQuery_ACTION(self, user, channel, data): 2219 self.action(user, channel, data) 2220 2221 def ctcpQuery_PING(self, user, channel, data): 2222 nick = user.split("!")[0] 2223 self.ctcpMakeReply(nick, [("PING", data)]) 2224 2225 def ctcpQuery_FINGER(self, user, channel, data): 2226 if data is not None: 2227 self.quirkyMessage(f"Why did {user} send '{data}' with a FINGER query?") 2228 if not self.fingerReply: 2229 return 2230 2231 if callable(self.fingerReply): 2232 reply = self.fingerReply() 2233 else: 2234 reply = str(self.fingerReply) 2235 2236 nick = user.split("!")[0] 2237 self.ctcpMakeReply(nick, [("FINGER", reply)]) 2238 2239 def ctcpQuery_VERSION(self, user, channel, data): 2240 if data is not None: 2241 self.quirkyMessage(f"Why did {user} send '{data}' with a VERSION query?") 2242 2243 if self.versionName: 2244 nick = user.split("!")[0] 2245 self.ctcpMakeReply( 2246 nick, 2247 [ 2248 ( 2249 "VERSION", 2250 "%s:%s:%s" 2251 % ( 2252 self.versionName, 2253 self.versionNum or "", 2254 self.versionEnv or "", 2255 ), 2256 ) 2257 ], 2258 ) 2259 2260 def ctcpQuery_SOURCE(self, user, channel, data): 2261 if data is not None: 2262 self.quirkyMessage(f"Why did {user} send '{data}' with a SOURCE query?") 2263 if self.sourceURL: 2264 nick = user.split("!")[0] 2265 # The CTCP document (Zeuge, Rollo, Mesander 1994) says that SOURCE 2266 # replies should be responded to with the location of an anonymous 2267 # FTP server in host:directory:file format. I'm taking the liberty 2268 # of bringing it into the 21st century by sending a URL instead. 2269 self.ctcpMakeReply(nick, [("SOURCE", self.sourceURL), ("SOURCE", None)]) 2270 2271 def ctcpQuery_USERINFO(self, user, channel, data): 2272 if data is not None: 2273 self.quirkyMessage(f"Why did {user} send '{data}' with a USERINFO query?") 2274 if self.userinfo: 2275 nick = user.split("!")[0] 2276 self.ctcpMakeReply(nick, [("USERINFO", self.userinfo)]) 2277 2278 def ctcpQuery_CLIENTINFO(self, user, channel, data): 2279 """ 2280 A master index of what CTCP tags this client knows. 2281 2282 If no arguments are provided, respond with a list of known tags, sorted 2283 in alphabetical order. 2284 If an argument is provided, provide human-readable help on 2285 the usage of that tag. 2286 """ 2287 nick = user.split("!")[0] 2288 if not data: 2289 # XXX: prefixedMethodNames gets methods from my *class*, 2290 # but it's entirely possible that this *instance* has more 2291 # methods. 2292 names = sorted(reflect.prefixedMethodNames(self.__class__, "ctcpQuery_")) 2293 2294 self.ctcpMakeReply(nick, [("CLIENTINFO", " ".join(names))]) 2295 else: 2296 args = data.split() 2297 method = getattr(self, f"ctcpQuery_{args[0]}", None) 2298 if not method: 2299 self.ctcpMakeReply( 2300 nick, 2301 [ 2302 ( 2303 "ERRMSG", 2304 "CLIENTINFO %s :" "Unknown query '%s'" % (data, args[0]), 2305 ) 2306 ], 2307 ) 2308 return 2309 doc = getattr(method, "__doc__", "") 2310 self.ctcpMakeReply(nick, [("CLIENTINFO", doc)]) 2311 2312 def ctcpQuery_ERRMSG(self, user, channel, data): 2313 # Yeah, this seems strange, but that's what the spec says to do 2314 # when faced with an ERRMSG query (not a reply). 2315 nick = user.split("!")[0] 2316 self.ctcpMakeReply(nick, [("ERRMSG", "%s :No error has occurred." % data)]) 2317 2318 def ctcpQuery_TIME(self, user, channel, data): 2319 if data is not None: 2320 self.quirkyMessage(f"Why did {user} send '{data}' with a TIME query?") 2321 nick = user.split("!")[0] 2322 self.ctcpMakeReply( 2323 nick, [("TIME", ":%s" % time.asctime(time.localtime(time.time())))] 2324 ) 2325 2326 def ctcpQuery_DCC(self, user, channel, data): 2327 """ 2328 Initiate a Direct Client Connection 2329 2330 @param user: The hostmask of the user/client. 2331 @type user: L{bytes} 2332 2333 @param channel: The name of the IRC channel. 2334 @type channel: L{bytes} 2335 2336 @param data: The DCC request message. 2337 @type data: L{bytes} 2338 """ 2339 2340 if not data: 2341 return 2342 dcctype = data.split(None, 1)[0].upper() 2343 handler = getattr(self, "dcc_" + dcctype, None) 2344 if handler: 2345 if self.dcc_sessions is None: 2346 self.dcc_sessions = [] 2347 data = data[len(dcctype) + 1 :] 2348 handler(user, channel, data) 2349 else: 2350 nick = user.split("!")[0] 2351 self.ctcpMakeReply( 2352 nick, 2353 [("ERRMSG", f"DCC {data} :Unknown DCC type '{dcctype}'")], 2354 ) 2355 self.quirkyMessage(f"{user} offered unknown DCC type {dcctype}") 2356 2357 def dcc_SEND(self, user, channel, data): 2358 # Use shlex.split for those who send files with spaces in the names. 2359 data = shlex.split(data) 2360 if len(data) < 3: 2361 raise IRCBadMessage(f"malformed DCC SEND request: {data!r}") 2362 2363 (filename, address, port) = data[:3] 2364 2365 address = dccParseAddress(address) 2366 try: 2367 port = int(port) 2368 except ValueError: 2369 raise IRCBadMessage(f"Indecipherable port {port!r}") 2370 2371 size = -1 2372 if len(data) >= 4: 2373 try: 2374 size = int(data[3]) 2375 except ValueError: 2376 pass 2377 2378 # XXX Should we bother passing this data? 2379 self.dccDoSend(user, address, port, filename, size, data) 2380 2381 def dcc_ACCEPT(self, user, channel, data): 2382 data = shlex.split(data) 2383 if len(data) < 3: 2384 raise IRCBadMessage(f"malformed DCC SEND ACCEPT request: {data!r}") 2385 (filename, port, resumePos) = data[:3] 2386 try: 2387 port = int(port) 2388 resumePos = int(resumePos) 2389 except ValueError: 2390 return 2391 2392 self.dccDoAcceptResume(user, filename, port, resumePos) 2393 2394 def dcc_RESUME(self, user, channel, data): 2395 data = shlex.split(data) 2396 if len(data) < 3: 2397 raise IRCBadMessage(f"malformed DCC SEND RESUME request: {data!r}") 2398 (filename, port, resumePos) = data[:3] 2399 try: 2400 port = int(port) 2401 resumePos = int(resumePos) 2402 except ValueError: 2403 return 2404 2405 self.dccDoResume(user, filename, port, resumePos) 2406 2407 def dcc_CHAT(self, user, channel, data): 2408 data = shlex.split(data) 2409 if len(data) < 3: 2410 raise IRCBadMessage(f"malformed DCC CHAT request: {data!r}") 2411 2412 (filename, address, port) = data[:3] 2413 2414 address = dccParseAddress(address) 2415 try: 2416 port = int(port) 2417 except ValueError: 2418 raise IRCBadMessage(f"Indecipherable port {port!r}") 2419 2420 self.dccDoChat(user, channel, address, port, data) 2421 2422 ### The dccDo methods are the slightly higher-level siblings of 2423 ### common dcc_ methods; the arguments have been parsed for them. 2424 2425 def dccDoSend(self, user, address, port, fileName, size, data): 2426 """ 2427 Called when I receive a DCC SEND offer from a client. 2428 2429 By default, I do nothing here. 2430 2431 @param user: The hostmask of the requesting user. 2432 @type user: L{bytes} 2433 2434 @param address: The IP address of the requesting user. 2435 @type address: L{bytes} 2436 2437 @param port: An integer representing the port of the requesting user. 2438 @type port: L{int} 2439 2440 @param fileName: The name of the file to be transferred. 2441 @type fileName: L{bytes} 2442 2443 @param size: The size of the file to be transferred, which may be C{-1} 2444 if the size of the file was not specified in the DCC SEND request. 2445 @type size: L{int} 2446 2447 @param data: A 3-list of [fileName, address, port]. 2448 @type data: L{list} 2449 """ 2450 2451 def dccDoResume(self, user, file, port, resumePos): 2452 """ 2453 Called when a client is trying to resume an offered file via DCC send. 2454 It should be either replied to with a DCC ACCEPT or ignored (default). 2455 2456 @param user: The hostmask of the user who wants to resume the transfer 2457 of a file previously offered via DCC send. 2458 @type user: L{bytes} 2459 2460 @param file: The name of the file to resume the transfer of. 2461 @type file: L{bytes} 2462 2463 @param port: An integer representing the port of the requesting user. 2464 @type port: L{int} 2465 2466 @param resumePos: The position in the file from where the transfer 2467 should resume. 2468 @type resumePos: L{int} 2469 """ 2470 pass 2471 2472 def dccDoAcceptResume(self, user, file, port, resumePos): 2473 """ 2474 Called when a client has verified and accepted a DCC resume request 2475 made by us. By default it will do nothing. 2476 2477 @param user: The hostmask of the user who has accepted the DCC resume 2478 request. 2479 @type user: L{bytes} 2480 2481 @param file: The name of the file to resume the transfer of. 2482 @type file: L{bytes} 2483 2484 @param port: An integer representing the port of the accepting user. 2485 @type port: L{int} 2486 2487 @param resumePos: The position in the file from where the transfer 2488 should resume. 2489 @type resumePos: L{int} 2490 """ 2491 pass 2492 2493 def dccDoChat(self, user, channel, address, port, data): 2494 pass 2495 # factory = DccChatFactory(self, queryData=(user, channel, data)) 2496 # reactor.connectTCP(address, port, factory) 2497 # self.dcc_sessions.append(factory) 2498 2499 # def ctcpQuery_SED(self, user, data): 2500 # """Simple Encryption Doodoo 2501 # 2502 # Feel free to implement this, but no specification is available. 2503 # """ 2504 # raise NotImplementedError 2505 2506 def ctcpMakeReply(self, user, messages): 2507 """ 2508 Send one or more C{extended messages} as a CTCP reply. 2509 2510 @type messages: a list of extended messages. An extended 2511 message is a (tag, data) tuple, where 'data' may be L{None}. 2512 """ 2513 self.notice(user, ctcpStringify(messages)) 2514 2515 ### client CTCP query commands 2516 2517 def ctcpMakeQuery(self, user, messages): 2518 """ 2519 Send one or more C{extended messages} as a CTCP query. 2520 2521 @type messages: a list of extended messages. An extended 2522 message is a (tag, data) tuple, where 'data' may be L{None}. 2523 """ 2524 self.msg(user, ctcpStringify(messages)) 2525 2526 ### Receiving a response to a CTCP query (presumably to one we made) 2527 ### You may want to add methods here, or override UnknownReply. 2528 2529 def ctcpReply(self, user, channel, messages): 2530 """ 2531 Dispatch method for any CTCP replies received. 2532 """ 2533 for m in messages: 2534 method = getattr(self, "ctcpReply_%s" % m[0], None) 2535 if method: 2536 method(user, channel, m[1]) 2537 else: 2538 self.ctcpUnknownReply(user, channel, m[0], m[1]) 2539 2540 def ctcpReply_PING(self, user, channel, data): 2541 nick = user.split("!", 1)[0] 2542 if (not self._pings) or ((nick, data) not in self._pings): 2543 raise IRCBadMessage(f"Bogus PING response from {user}: {data}") 2544 2545 t0 = self._pings[(nick, data)] 2546 self.pong(user, time.time() - t0) 2547 2548 def ctcpUnknownReply(self, user, channel, tag, data): 2549 """ 2550 Called when a fitting ctcpReply_ method is not found. 2551 2552 @param user: The hostmask of the user. 2553 @type user: L{bytes} 2554 2555 @param channel: The name of the IRC channel. 2556 @type channel: L{bytes} 2557 2558 @param tag: The CTCP request tag for which no fitting method is found. 2559 @type tag: L{bytes} 2560 2561 @param data: The CTCP message. 2562 @type data: L{bytes} 2563 """ 2564 # FIXME:7560: 2565 # Add code for handling arbitrary queries and not treat them as 2566 # anomalies. 2567 2568 log.msg(f"Unknown CTCP reply from {user}: {tag} {data}\n") 2569 2570 ### Error handlers 2571 ### You may override these with something more appropriate to your UI. 2572 2573 def badMessage(self, line, excType, excValue, tb): 2574 """ 2575 When I get a message that's so broken I can't use it. 2576 2577 @param line: The indecipherable message. 2578 @type line: L{bytes} 2579 2580 @param excType: The exception type of the exception raised by the 2581 message. 2582 @type excType: L{type} 2583 2584 @param excValue: The exception parameter of excType or its associated 2585 value(the second argument to C{raise}). 2586 @type excValue: L{BaseException} 2587 2588 @param tb: The Traceback as a traceback object. 2589 @type tb: L{traceback} 2590 """ 2591 log.msg(line) 2592 log.msg("".join(traceback.format_exception(excType, excValue, tb))) 2593 2594 def quirkyMessage(self, s): 2595 """ 2596 This is called when I receive a message which is peculiar, but not 2597 wholly indecipherable. 2598 2599 @param s: The peculiar message. 2600 @type s: L{bytes} 2601 """ 2602 log.msg(s + "\n") 2603 2604 ### Protocol methods 2605 2606 def connectionMade(self): 2607 self.supported = ServerSupportedFeatures() 2608 self._queue = [] 2609 if self.performLogin: 2610 self.register(self.nickname) 2611 2612 def dataReceived(self, data): 2613 if isinstance(data, str): 2614 data = data.encode("utf-8") 2615 data = data.replace(b"\r", b"") 2616 basic.LineReceiver.dataReceived(self, data) 2617 2618 def lineReceived(self, line): 2619 if bytes != str and isinstance(line, bytes): 2620 # decode bytes from transport to unicode 2621 line = line.decode("utf-8") 2622 2623 line = lowDequote(line) 2624 try: 2625 prefix, command, params = parsemsg(line) 2626 if command in numeric_to_symbolic: 2627 command = numeric_to_symbolic[command] 2628 self.handleCommand(command, prefix, params) 2629 except IRCBadMessage: 2630 self.badMessage(line, *sys.exc_info()) 2631 2632 def getUserModeParams(self): 2633 """ 2634 Get user modes that require parameters for correct parsing. 2635 2636 @rtype: C{[str, str]} 2637 @return: C{[add, remove]} 2638 """ 2639 return ["", ""] 2640 2641 def getChannelModeParams(self): 2642 """ 2643 Get channel modes that require parameters for correct parsing. 2644 2645 @rtype: C{[str, str]} 2646 @return: C{[add, remove]} 2647 """ 2648 # PREFIX modes are treated as "type B" CHANMODES, they always take 2649 # parameter. 2650 params = ["", ""] 2651 prefixes = self.supported.getFeature("PREFIX", {}) 2652 params[0] = params[1] = "".join(prefixes.keys()) 2653 2654 chanmodes = self.supported.getFeature("CHANMODES") 2655 if chanmodes is not None: 2656 params[0] += chanmodes.get("addressModes", "") 2657 params[0] += chanmodes.get("param", "") 2658 params[1] = params[0] 2659 params[0] += chanmodes.get("setParam", "") 2660 return params 2661 2662 def handleCommand(self, command, prefix, params): 2663 """ 2664 Determine the function to call for the given command and call it with 2665 the given arguments. 2666 2667 @param command: The IRC command to determine the function for. 2668 @type command: L{bytes} 2669 2670 @param prefix: The prefix of the IRC message (as returned by 2671 L{parsemsg}). 2672 @type prefix: L{bytes} 2673 2674 @param params: A list of parameters to call the function with. 2675 @type params: L{list} 2676 """ 2677 method = getattr(self, "irc_%s" % command, None) 2678 try: 2679 if method is not None: 2680 method(prefix, params) 2681 else: 2682 self.irc_unknown(prefix, command, params) 2683 except BaseException: 2684 log.deferr() 2685 2686 def __getstate__(self): 2687 dct = self.__dict__.copy() 2688 dct["dcc_sessions"] = None 2689 dct["_pings"] = None 2690 return dct 2691 2692 2693def dccParseAddress(address): 2694 if "." in address: 2695 pass 2696 else: 2697 try: 2698 address = int(address) 2699 except ValueError: 2700 raise IRCBadMessage(f"Indecipherable address {address!r}") 2701 else: 2702 address = ( 2703 (address >> 24) & 0xFF, 2704 (address >> 16) & 0xFF, 2705 (address >> 8) & 0xFF, 2706 address & 0xFF, 2707 ) 2708 address = ".".join(map(str, address)) 2709 return address 2710 2711 2712class DccFileReceiveBasic(protocol.Protocol, styles.Ephemeral): 2713 """ 2714 Bare protocol to receive a Direct Client Connection SEND stream. 2715 2716 This does enough to keep the other guy talking, but you'll want to extend 2717 my dataReceived method to *do* something with the data I get. 2718 2719 @ivar bytesReceived: An integer representing the number of bytes of data 2720 received. 2721 @type bytesReceived: L{int} 2722 """ 2723 2724 bytesReceived = 0 2725 2726 def __init__(self, resumeOffset=0): 2727 """ 2728 @param resumeOffset: An integer representing the amount of bytes from 2729 where the transfer of data should be resumed. 2730 @type resumeOffset: L{int} 2731 """ 2732 self.bytesReceived = resumeOffset 2733 self.resume = resumeOffset != 0 2734 2735 def dataReceived(self, data): 2736 """ 2737 See: L{protocol.Protocol.dataReceived} 2738 2739 Warning: This just acknowledges to the remote host that the data has 2740 been received; it doesn't I{do} anything with the data, so you'll want 2741 to override this. 2742 """ 2743 self.bytesReceived = self.bytesReceived + len(data) 2744 self.transport.write(struct.pack("!i", self.bytesReceived)) 2745 2746 2747class DccSendProtocol(protocol.Protocol, styles.Ephemeral): 2748 """ 2749 Protocol for an outgoing Direct Client Connection SEND. 2750 2751 @ivar blocksize: An integer representing the size of an individual block of 2752 data. 2753 @type blocksize: L{int} 2754 2755 @ivar file: The file to be sent. This can be either a file object or 2756 simply the name of the file. 2757 @type file: L{file} or L{bytes} 2758 2759 @ivar bytesSent: An integer representing the number of bytes sent. 2760 @type bytesSent: L{int} 2761 2762 @ivar completed: An integer representing whether the transfer has been 2763 completed or not. 2764 @type completed: L{int} 2765 2766 @ivar connected: An integer representing whether the connection has been 2767 established or not. 2768 @type connected: L{int} 2769 """ 2770 2771 blocksize = 1024 2772 file = None 2773 bytesSent = 0 2774 completed = 0 2775 connected = 0 2776 2777 def __init__(self, file): 2778 if type(file) is str: 2779 self.file = open(file) 2780 2781 def connectionMade(self): 2782 self.connected = 1 2783 self.sendBlock() 2784 2785 def dataReceived(self, data): 2786 # XXX: Do we need to check to see if len(data) != fmtsize? 2787 2788 bytesShesGot = struct.unpack("!I", data) 2789 if bytesShesGot < self.bytesSent: 2790 # Wait for her. 2791 # XXX? Add some checks to see if we've stalled out? 2792 return 2793 elif bytesShesGot > self.bytesSent: 2794 # self.transport.log("DCC SEND %s: She says she has %d bytes " 2795 # "but I've only sent %d. I'm stopping " 2796 # "this screwy transfer." 2797 # % (self.file, 2798 # bytesShesGot, self.bytesSent)) 2799 self.transport.loseConnection() 2800 return 2801 2802 self.sendBlock() 2803 2804 def sendBlock(self): 2805 block = self.file.read(self.blocksize) 2806 if block: 2807 self.transport.write(block) 2808 self.bytesSent = self.bytesSent + len(block) 2809 else: 2810 # Nothing more to send, transfer complete. 2811 self.transport.loseConnection() 2812 self.completed = 1 2813 2814 def connectionLost(self, reason): 2815 self.connected = 0 2816 if hasattr(self.file, "close"): 2817 self.file.close() 2818 2819 2820class DccSendFactory(protocol.Factory): 2821 protocol = DccSendProtocol # type: ignore[assignment] 2822 2823 def __init__(self, file): 2824 self.file = file 2825 2826 def buildProtocol(self, connection): 2827 p = self.protocol(self.file) 2828 p.factory = self 2829 return p 2830 2831 2832def fileSize(file): 2833 """ 2834 I'll try my damndest to determine the size of this file object. 2835 2836 @param file: The file object to determine the size of. 2837 @type file: L{io.IOBase} 2838 2839 @rtype: L{int} or L{None} 2840 @return: The size of the file object as an integer if it can be determined, 2841 otherwise return L{None}. 2842 """ 2843 size = None 2844 if hasattr(file, "fileno"): 2845 fileno = file.fileno() 2846 try: 2847 stat_ = os.fstat(fileno) 2848 size = stat_[stat.ST_SIZE] 2849 except BaseException: 2850 pass 2851 else: 2852 return size 2853 2854 if hasattr(file, "name") and path.exists(file.name): 2855 try: 2856 size = path.getsize(file.name) 2857 except BaseException: 2858 pass 2859 else: 2860 return size 2861 2862 if hasattr(file, "seek") and hasattr(file, "tell"): 2863 try: 2864 try: 2865 file.seek(0, 2) 2866 size = file.tell() 2867 finally: 2868 file.seek(0, 0) 2869 except BaseException: 2870 pass 2871 else: 2872 return size 2873 2874 return size 2875 2876 2877class DccChat(basic.LineReceiver, styles.Ephemeral): 2878 """ 2879 Direct Client Connection protocol type CHAT. 2880 2881 DCC CHAT is really just your run o' the mill basic.LineReceiver 2882 protocol. This class only varies from that slightly, accepting 2883 either LF or CR LF for a line delimeter for incoming messages 2884 while always using CR LF for outgoing. 2885 2886 The lineReceived method implemented here uses the DCC connection's 2887 'client' attribute (provided upon construction) to deliver incoming 2888 lines from the DCC chat via IRCClient's normal privmsg interface. 2889 That's something of a spoof, which you may well want to override. 2890 """ 2891 2892 queryData = None 2893 delimiter = CR.encode("ascii") + NL.encode("ascii") 2894 client = None 2895 remoteParty = None 2896 buffer = b"" 2897 2898 def __init__(self, client, queryData=None): 2899 """ 2900 Initialize a new DCC CHAT session. 2901 2902 queryData is a 3-tuple of 2903 (fromUser, targetUserOrChannel, data) 2904 as received by the CTCP query. 2905 2906 (To be honest, fromUser is the only thing that's currently 2907 used here. targetUserOrChannel is potentially useful, while 2908 the 'data' argument is solely for informational purposes.) 2909 """ 2910 self.client = client 2911 if queryData: 2912 self.queryData = queryData 2913 self.remoteParty = self.queryData[0] 2914 2915 def dataReceived(self, data): 2916 self.buffer = self.buffer + data 2917 lines = self.buffer.split(LF) 2918 # Put the (possibly empty) element after the last LF back in the 2919 # buffer 2920 self.buffer = lines.pop() 2921 2922 for line in lines: 2923 if line[-1] == CR: 2924 line = line[:-1] 2925 self.lineReceived(line) 2926 2927 def lineReceived(self, line): 2928 log.msg(f"DCC CHAT<{self.remoteParty}> {line}") 2929 self.client.privmsg(self.remoteParty, self.client.nickname, line) 2930 2931 2932class DccChatFactory(protocol.ClientFactory): 2933 protocol = DccChat # type: ignore[assignment] 2934 noisy = False 2935 2936 def __init__(self, client, queryData): 2937 self.client = client 2938 self.queryData = queryData 2939 2940 def buildProtocol(self, addr): 2941 p = self.protocol(client=self.client, queryData=self.queryData) 2942 p.factory = self 2943 return p 2944 2945 def clientConnectionFailed(self, unused_connector, unused_reason): 2946 self.client.dcc_sessions.remove(self) 2947 2948 def clientConnectionLost(self, unused_connector, unused_reason): 2949 self.client.dcc_sessions.remove(self) 2950 2951 2952def dccDescribe(data): 2953 """ 2954 Given the data chunk from a DCC query, return a descriptive string. 2955 2956 @param data: The data from a DCC query. 2957 @type data: L{bytes} 2958 2959 @rtype: L{bytes} 2960 @return: A descriptive string. 2961 """ 2962 2963 orig_data = data 2964 data = data.split() 2965 if len(data) < 4: 2966 return orig_data 2967 2968 (dcctype, arg, address, port) = data[:4] 2969 2970 if "." in address: 2971 pass 2972 else: 2973 try: 2974 address = int(address) 2975 except ValueError: 2976 pass 2977 else: 2978 address = ( 2979 (address >> 24) & 0xFF, 2980 (address >> 16) & 0xFF, 2981 (address >> 8) & 0xFF, 2982 address & 0xFF, 2983 ) 2984 address = ".".join(map(str, address)) 2985 2986 if dcctype == "SEND": 2987 filename = arg 2988 2989 size_txt = "" 2990 if len(data) >= 5: 2991 try: 2992 size = int(data[4]) 2993 size_txt = " of size %d bytes" % (size,) 2994 except ValueError: 2995 pass 2996 2997 dcc_text = "SEND for file '{}'{} at host {}, port {}".format( 2998 filename, 2999 size_txt, 3000 address, 3001 port, 3002 ) 3003 elif dcctype == "CHAT": 3004 dcc_text = f"CHAT for host {address}, port {port}" 3005 else: 3006 dcc_text = orig_data 3007 3008 return dcc_text 3009 3010 3011class DccFileReceive(DccFileReceiveBasic): 3012 """ 3013 Higher-level coverage for getting a file from DCC SEND. 3014 3015 I allow you to change the file's name and destination directory. I won't 3016 overwrite an existing file unless I've been told it's okay to do so. If 3017 passed the resumeOffset keyword argument I will attempt to resume the file 3018 from that amount of bytes. 3019 3020 XXX: I need to let the client know when I am finished. 3021 XXX: I need to decide how to keep a progress indicator updated. 3022 XXX: Client needs a way to tell me "Do not finish until I say so." 3023 XXX: I need to make sure the client understands if the file cannot be written. 3024 3025 @ivar filename: The name of the file to get. 3026 @type filename: L{bytes} 3027 3028 @ivar fileSize: The size of the file to get, which has a default value of 3029 C{-1} if the size of the file was not specified in the DCC SEND 3030 request. 3031 @type fileSize: L{int} 3032 3033 @ivar destDir: The destination directory for the file to be received. 3034 @type destDir: L{bytes} 3035 3036 @ivar overwrite: An integer representing whether an existing file should be 3037 overwritten or not. This initially is an L{int} but can be modified to 3038 be a L{bool} using the L{set_overwrite} method. 3039 @type overwrite: L{int} or L{bool} 3040 3041 @ivar queryData: queryData is a 3-tuple of (user, channel, data). 3042 @type queryData: L{tuple} 3043 3044 @ivar fromUser: This is the hostmask of the requesting user and is found at 3045 index 0 of L{queryData}. 3046 @type fromUser: L{bytes} 3047 """ 3048 3049 filename = "dcc" 3050 fileSize = -1 3051 destDir = "." 3052 overwrite = 0 3053 fromUser: Optional[bytes] = None 3054 queryData = None 3055 3056 def __init__( 3057 self, filename, fileSize=-1, queryData=None, destDir=".", resumeOffset=0 3058 ): 3059 DccFileReceiveBasic.__init__(self, resumeOffset=resumeOffset) 3060 self.filename = filename 3061 self.destDir = destDir 3062 self.fileSize = fileSize 3063 self._resumeOffset = resumeOffset 3064 3065 if queryData: 3066 self.queryData = queryData 3067 self.fromUser = self.queryData[0] 3068 3069 def set_directory(self, directory): 3070 """ 3071 Set the directory where the downloaded file will be placed. 3072 3073 May raise OSError if the supplied directory path is not suitable. 3074 3075 @param directory: The directory where the file to be received will be 3076 placed. 3077 @type directory: L{bytes} 3078 """ 3079 if not path.exists(directory): 3080 raise OSError(errno.ENOENT, "You see no directory there.", directory) 3081 if not path.isdir(directory): 3082 raise OSError( 3083 errno.ENOTDIR, 3084 "You cannot put a file into " "something which is not a directory.", 3085 directory, 3086 ) 3087 if not os.access(directory, os.X_OK | os.W_OK): 3088 raise OSError( 3089 errno.EACCES, "This directory is too hard to write in to.", directory 3090 ) 3091 self.destDir = directory 3092 3093 def set_filename(self, filename): 3094 """ 3095 Change the name of the file being transferred. 3096 3097 This replaces the file name provided by the sender. 3098 3099 @param filename: The new name for the file. 3100 @type filename: L{bytes} 3101 """ 3102 self.filename = filename 3103 3104 def set_overwrite(self, boolean): 3105 """ 3106 May I overwrite existing files? 3107 3108 @param boolean: A boolean value representing whether existing files 3109 should be overwritten or not. 3110 @type boolean: L{bool} 3111 """ 3112 self.overwrite = boolean 3113 3114 # Protocol-level methods. 3115 3116 def connectionMade(self): 3117 dst = path.abspath(path.join(self.destDir, self.filename)) 3118 exists = path.exists(dst) 3119 if self.resume and exists: 3120 # I have been told I want to resume, and a file already 3121 # exists - Here we go 3122 self.file = open(dst, "rb+") 3123 self.file.seek(self._resumeOffset) 3124 self.file.truncate() 3125 log.msg( 3126 "Attempting to resume %s - starting from %d bytes" 3127 % (self.file, self.file.tell()) 3128 ) 3129 elif self.resume and not exists: 3130 raise OSError( 3131 errno.ENOENT, 3132 "You cannot resume writing to a file " "that does not exist!", 3133 dst, 3134 ) 3135 elif self.overwrite or not exists: 3136 self.file = open(dst, "wb") 3137 else: 3138 raise OSError( 3139 errno.EEXIST, 3140 "There's a file in the way. " "Perhaps that's why you cannot open it.", 3141 dst, 3142 ) 3143 3144 def dataReceived(self, data): 3145 self.file.write(data) 3146 DccFileReceiveBasic.dataReceived(self, data) 3147 3148 # XXX: update a progress indicator here? 3149 3150 def connectionLost(self, reason): 3151 """ 3152 When the connection is lost, I close the file. 3153 3154 @param reason: The reason why the connection was lost. 3155 @type reason: L{Failure} 3156 """ 3157 self.connected = 0 3158 logmsg = f"{self} closed." 3159 if self.fileSize > 0: 3160 logmsg = "%s %d/%d bytes received" % ( 3161 logmsg, 3162 self.bytesReceived, 3163 self.fileSize, 3164 ) 3165 if self.bytesReceived == self.fileSize: 3166 pass # Hooray! 3167 elif self.bytesReceived < self.fileSize: 3168 logmsg = "%s (Warning: %d bytes short)" % ( 3169 logmsg, 3170 self.fileSize - self.bytesReceived, 3171 ) 3172 else: 3173 logmsg = f"{logmsg} (file larger than expected)" 3174 else: 3175 logmsg = "%s %d bytes received" % (logmsg, self.bytesReceived) 3176 3177 if hasattr(self, "file"): 3178 logmsg = f"{logmsg} and written to {self.file.name}.\n" 3179 if hasattr(self.file, "close"): 3180 self.file.close() 3181 3182 # self.transport.log(logmsg) 3183 3184 def __str__(self) -> str: 3185 if not self.connected: 3186 return f"<Unconnected DccFileReceive object at {id(self):x}>" 3187 transport = self.transport 3188 assert transport is not None 3189 from_ = str(transport.getPeer()) 3190 if self.fromUser is not None: 3191 from_ = f"{self.fromUser!r} ({from_})" 3192 3193 s = f"DCC transfer of '{self.filename}' from {from_}" 3194 return s 3195 3196 def __repr__(self) -> str: 3197 s = f"<{self.__class__} at {id(self):x}: GET {self.filename}>" 3198 return s 3199 3200 3201_OFF = "\x0f" 3202_BOLD = "\x02" 3203_COLOR = "\x03" 3204_REVERSE_VIDEO = "\x16" 3205_UNDERLINE = "\x1f" 3206 3207# Mapping of IRC color names to their color values. 3208_IRC_COLORS = dict( 3209 zip( 3210 [ 3211 "white", 3212 "black", 3213 "blue", 3214 "green", 3215 "lightRed", 3216 "red", 3217 "magenta", 3218 "orange", 3219 "yellow", 3220 "lightGreen", 3221 "cyan", 3222 "lightCyan", 3223 "lightBlue", 3224 "lightMagenta", 3225 "gray", 3226 "lightGray", 3227 ], 3228 range(16), 3229 ) 3230) 3231 3232# Mapping of IRC color values to their color names. 3233_IRC_COLOR_NAMES = {code: name for name, code in _IRC_COLORS.items()} 3234 3235 3236class _CharacterAttributes(_textattributes.CharacterAttributesMixin): 3237 """ 3238 Factory for character attributes, including foreground and background color 3239 and non-color attributes such as bold, reverse video and underline. 3240 3241 Character attributes are applied to actual text by using object 3242 indexing-syntax (C{obj['abc']}) after accessing a factory attribute, for 3243 example:: 3244 3245 attributes.bold['Some text'] 3246 3247 These can be nested to mix attributes:: 3248 3249 attributes.bold[attributes.underline['Some text']] 3250 3251 And multiple values can be passed:: 3252 3253 attributes.normal[attributes.bold['Some'], ' text'] 3254 3255 Non-color attributes can be accessed by attribute name, available 3256 attributes are: 3257 3258 - bold 3259 - reverseVideo 3260 - underline 3261 3262 Available colors are: 3263 3264 0. white 3265 1. black 3266 2. blue 3267 3. green 3268 4. light red 3269 5. red 3270 6. magenta 3271 7. orange 3272 8. yellow 3273 9. light green 3274 10. cyan 3275 11. light cyan 3276 12. light blue 3277 13. light magenta 3278 14. gray 3279 15. light gray 3280 3281 @ivar fg: Foreground colors accessed by attribute name, see above 3282 for possible names. 3283 3284 @ivar bg: Background colors accessed by attribute name, see above 3285 for possible names. 3286 3287 @since: 13.1 3288 """ 3289 3290 fg = _textattributes._ColorAttribute( 3291 _textattributes._ForegroundColorAttr, _IRC_COLORS 3292 ) 3293 bg = _textattributes._ColorAttribute( 3294 _textattributes._BackgroundColorAttr, _IRC_COLORS 3295 ) 3296 3297 attrs = {"bold": _BOLD, "reverseVideo": _REVERSE_VIDEO, "underline": _UNDERLINE} 3298 3299 3300attributes = _CharacterAttributes() 3301 3302 3303class _FormattingState(_textattributes._FormattingStateMixin): 3304 """ 3305 Formatting state/attributes of a single character. 3306 3307 Attributes include: 3308 - Formatting nullifier 3309 - Bold 3310 - Underline 3311 - Reverse video 3312 - Foreground color 3313 - Background color 3314 3315 @since: 13.1 3316 """ 3317 3318 compareAttributes = ( 3319 "off", 3320 "bold", 3321 "underline", 3322 "reverseVideo", 3323 "foreground", 3324 "background", 3325 ) 3326 3327 def __init__( 3328 self, 3329 off=False, 3330 bold=False, 3331 underline=False, 3332 reverseVideo=False, 3333 foreground=None, 3334 background=None, 3335 ): 3336 self.off = off 3337 self.bold = bold 3338 self.underline = underline 3339 self.reverseVideo = reverseVideo 3340 self.foreground = foreground 3341 self.background = background 3342 3343 def toMIRCControlCodes(self): 3344 """ 3345 Emit a mIRC control sequence that will set up all the attributes this 3346 formatting state has set. 3347 3348 @return: A string containing mIRC control sequences that mimic this 3349 formatting state. 3350 """ 3351 attrs = [] 3352 if self.bold: 3353 attrs.append(_BOLD) 3354 if self.underline: 3355 attrs.append(_UNDERLINE) 3356 if self.reverseVideo: 3357 attrs.append(_REVERSE_VIDEO) 3358 if self.foreground is not None or self.background is not None: 3359 c = "" 3360 if self.foreground is not None: 3361 c += "%02d" % (self.foreground,) 3362 if self.background is not None: 3363 c += ",%02d" % (self.background,) 3364 attrs.append(_COLOR + c) 3365 return _OFF + "".join(map(str, attrs)) 3366 3367 3368def _foldr(f, z, xs): 3369 """ 3370 Apply a function of two arguments cumulatively to the items of 3371 a sequence, from right to left, so as to reduce the sequence to 3372 a single value. 3373 3374 @type f: C{callable} taking 2 arguments 3375 3376 @param z: Initial value. 3377 3378 @param xs: Sequence to reduce. 3379 3380 @return: Single value resulting from reducing C{xs}. 3381 """ 3382 return reduce(lambda x, y: f(y, x), reversed(xs), z) 3383 3384 3385class _FormattingParser(_CommandDispatcherMixin): 3386 """ 3387 A finite-state machine that parses formatted IRC text. 3388 3389 Currently handled formatting includes: bold, reverse, underline, 3390 mIRC color codes and the ability to remove all current formatting. 3391 3392 @see: U{http://www.mirc.co.uk/help/color.txt} 3393 3394 @type _formatCodes: C{dict} mapping C{str} to C{str} 3395 @cvar _formatCodes: Mapping of format code values to names. 3396 3397 @type state: C{str} 3398 @ivar state: Current state of the finite-state machine. 3399 3400 @type _buffer: C{str} 3401 @ivar _buffer: Buffer, containing the text content, of the formatting 3402 sequence currently being parsed, the buffer is used as the content for 3403 L{_attrs} before being added to L{_result} and emptied upon calling 3404 L{emit}. 3405 3406 @type _attrs: C{set} 3407 @ivar _attrs: Set of the applicable formatting states (bold, underline, 3408 etc.) for the current L{_buffer}, these are applied to L{_buffer} when 3409 calling L{emit}. 3410 3411 @type foreground: L{_ForegroundColorAttr} 3412 @ivar foreground: Current foreground color attribute, or L{None}. 3413 3414 @type background: L{_BackgroundColorAttr} 3415 @ivar background: Current background color attribute, or L{None}. 3416 3417 @ivar _result: Current parse result. 3418 """ 3419 3420 prefix = "state" 3421 3422 _formatCodes = { 3423 _OFF: "off", 3424 _BOLD: "bold", 3425 _COLOR: "color", 3426 _REVERSE_VIDEO: "reverseVideo", 3427 _UNDERLINE: "underline", 3428 } 3429 3430 def __init__(self): 3431 self.state = "TEXT" 3432 self._buffer = "" 3433 self._attrs = set() 3434 self._result = None 3435 self.foreground = None 3436 self.background = None 3437 3438 def process(self, ch): 3439 """ 3440 Handle input. 3441 3442 @type ch: C{str} 3443 @param ch: A single character of input to process 3444 """ 3445 self.dispatch(self.state, ch) 3446 3447 def complete(self): 3448 """ 3449 Flush the current buffer and return the final parsed result. 3450 3451 @return: Structured text and attributes. 3452 """ 3453 self.emit() 3454 if self._result is None: 3455 self._result = attributes.normal 3456 return self._result 3457 3458 def emit(self): 3459 """ 3460 Add the currently parsed input to the result. 3461 """ 3462 if self._buffer: 3463 attrs = [getattr(attributes, name) for name in self._attrs] 3464 attrs.extend(filter(None, [self.foreground, self.background])) 3465 if not attrs: 3466 attrs.append(attributes.normal) 3467 attrs.append(self._buffer) 3468 3469 attr = _foldr(operator.getitem, attrs.pop(), attrs) 3470 if self._result is None: 3471 self._result = attr 3472 else: 3473 self._result[attr] 3474 self._buffer = "" 3475 3476 def state_TEXT(self, ch): 3477 """ 3478 Handle the "text" state. 3479 3480 Along with regular text, single token formatting codes are handled 3481 in this state too. 3482 3483 @param ch: The character being processed. 3484 """ 3485 formatName = self._formatCodes.get(ch) 3486 if formatName == "color": 3487 self.emit() 3488 self.state = "COLOR_FOREGROUND" 3489 else: 3490 if formatName is None: 3491 self._buffer += ch 3492 else: 3493 self.emit() 3494 if formatName == "off": 3495 self._attrs = set() 3496 self.foreground = self.background = None 3497 else: 3498 self._attrs.symmetric_difference_update([formatName]) 3499 3500 def state_COLOR_FOREGROUND(self, ch): 3501 """ 3502 Handle the foreground color state. 3503 3504 Foreground colors can consist of up to two digits and may optionally 3505 end in a I{,}. Any non-digit or non-comma characters are treated as 3506 invalid input and result in the state being reset to "text". 3507 3508 @param ch: The character being processed. 3509 """ 3510 # Color codes may only be a maximum of two characters. 3511 if ch.isdigit() and len(self._buffer) < 2: 3512 self._buffer += ch 3513 else: 3514 if self._buffer: 3515 # Wrap around for color numbers higher than we support, like 3516 # most other IRC clients. 3517 col = int(self._buffer) % len(_IRC_COLORS) 3518 self.foreground = getattr(attributes.fg, _IRC_COLOR_NAMES[col]) 3519 else: 3520 # If there were no digits, then this has been an empty color 3521 # code and we can reset the color state. 3522 self.foreground = self.background = None 3523 3524 if ch == "," and self._buffer: 3525 # If there's a comma and it's not the first thing, move on to 3526 # the background state. 3527 self._buffer = "" 3528 self.state = "COLOR_BACKGROUND" 3529 else: 3530 # Otherwise, this is a bogus color code, fall back to text. 3531 self._buffer = "" 3532 self.state = "TEXT" 3533 self.emit() 3534 self.process(ch) 3535 3536 def state_COLOR_BACKGROUND(self, ch): 3537 """ 3538 Handle the background color state. 3539 3540 Background colors can consist of up to two digits and must occur after 3541 a foreground color and must be preceded by a I{,}. Any non-digit 3542 character is treated as invalid input and results in the state being 3543 set to "text". 3544 3545 @param ch: The character being processed. 3546 """ 3547 # Color codes may only be a maximum of two characters. 3548 if ch.isdigit() and len(self._buffer) < 2: 3549 self._buffer += ch 3550 else: 3551 if self._buffer: 3552 # Wrap around for color numbers higher than we support, like 3553 # most other IRC clients. 3554 col = int(self._buffer) % len(_IRC_COLORS) 3555 self.background = getattr(attributes.bg, _IRC_COLOR_NAMES[col]) 3556 self._buffer = "" 3557 3558 self.emit() 3559 self.state = "TEXT" 3560 self.process(ch) 3561 3562 3563def parseFormattedText(text): 3564 """ 3565 Parse text containing IRC formatting codes into structured information. 3566 3567 Color codes are mapped from 0 to 15 and wrap around if greater than 15. 3568 3569 @type text: C{str} 3570 @param text: Formatted text to parse. 3571 3572 @return: Structured text and attributes. 3573 3574 @since: 13.1 3575 """ 3576 state = _FormattingParser() 3577 for ch in text: 3578 state.process(ch) 3579 return state.complete() 3580 3581 3582def assembleFormattedText(formatted): 3583 """ 3584 Assemble formatted text from structured information. 3585 3586 Currently handled formatting includes: bold, reverse, underline, 3587 mIRC color codes and the ability to remove all current formatting. 3588 3589 It is worth noting that assembled text will always begin with the control 3590 code to disable other attributes for the sake of correctness. 3591 3592 For example:: 3593 3594 from twisted.words.protocols.irc import attributes as A 3595 assembleFormattedText( 3596 A.normal[A.bold['Time: '], A.fg.lightRed['Now!']]) 3597 3598 Would produce "Time: " in bold formatting, followed by "Now!" with a 3599 foreground color of light red and without any additional formatting. 3600 3601 Available attributes are: 3602 - bold 3603 - reverseVideo 3604 - underline 3605 3606 Available colors are: 3607 0. white 3608 1. black 3609 2. blue 3610 3. green 3611 4. light red 3612 5. red 3613 6. magenta 3614 7. orange 3615 8. yellow 3616 9. light green 3617 10. cyan 3618 11. light cyan 3619 12. light blue 3620 13. light magenta 3621 14. gray 3622 15. light gray 3623 3624 @see: U{http://www.mirc.co.uk/help/color.txt} 3625 3626 @param formatted: Structured text and attributes. 3627 3628 @rtype: C{str} 3629 @return: String containing mIRC control sequences that mimic those 3630 specified by I{formatted}. 3631 3632 @since: 13.1 3633 """ 3634 return _textattributes.flatten(formatted, _FormattingState(), "toMIRCControlCodes") 3635 3636 3637def stripFormatting(text): 3638 """ 3639 Remove all formatting codes from C{text}, leaving only the text. 3640 3641 @type text: C{str} 3642 @param text: Formatted text to parse. 3643 3644 @rtype: C{str} 3645 @return: Plain text without any control sequences. 3646 3647 @since: 13.1 3648 """ 3649 formatted = parseFormattedText(text) 3650 return _textattributes.flatten(formatted, _textattributes.DefaultFormattingState()) 3651 3652 3653# CTCP constants and helper functions 3654 3655X_DELIM = chr(0o01) 3656 3657 3658def ctcpExtract(message): 3659 """ 3660 Extract CTCP data from a string. 3661 3662 @return: A C{dict} containing two keys: 3663 - C{'extended'}: A list of CTCP (tag, data) tuples. 3664 - C{'normal'}: A list of strings which were not inside a CTCP delimiter. 3665 """ 3666 extended_messages = [] 3667 normal_messages = [] 3668 retval = {"extended": extended_messages, "normal": normal_messages} 3669 3670 messages = message.split(X_DELIM) 3671 odd = 0 3672 3673 # X1 extended data X2 nomal data X3 extended data X4 normal... 3674 while messages: 3675 if odd: 3676 extended_messages.append(messages.pop(0)) 3677 else: 3678 normal_messages.append(messages.pop(0)) 3679 odd = not odd 3680 3681 extended_messages[:] = list(filter(None, extended_messages)) 3682 normal_messages[:] = list(filter(None, normal_messages)) 3683 3684 extended_messages[:] = list(map(ctcpDequote, extended_messages)) 3685 for i in range(len(extended_messages)): 3686 m = extended_messages[i].split(SPC, 1) 3687 tag = m[0] 3688 if len(m) > 1: 3689 data = m[1] 3690 else: 3691 data = None 3692 3693 extended_messages[i] = (tag, data) 3694 3695 return retval 3696 3697 3698# CTCP escaping 3699 3700M_QUOTE = chr(0o20) 3701 3702mQuoteTable = { 3703 NUL: M_QUOTE + "0", 3704 NL: M_QUOTE + "n", 3705 CR: M_QUOTE + "r", 3706 M_QUOTE: M_QUOTE + M_QUOTE, 3707} 3708 3709mDequoteTable = {} 3710for k, v in mQuoteTable.items(): 3711 mDequoteTable[v[-1]] = k 3712del k, v 3713 3714mEscape_re = re.compile(f"{re.escape(M_QUOTE)}.", re.DOTALL) 3715 3716 3717def lowQuote(s): 3718 for c in (M_QUOTE, NUL, NL, CR): 3719 s = s.replace(c, mQuoteTable[c]) 3720 return s 3721 3722 3723def lowDequote(s): 3724 def sub(matchobj, mDequoteTable=mDequoteTable): 3725 s = matchobj.group()[1] 3726 try: 3727 s = mDequoteTable[s] 3728 except KeyError: 3729 s = s 3730 return s 3731 3732 return mEscape_re.sub(sub, s) 3733 3734 3735X_QUOTE = "\\" 3736 3737xQuoteTable = {X_DELIM: X_QUOTE + "a", X_QUOTE: X_QUOTE + X_QUOTE} 3738 3739xDequoteTable = {} 3740 3741for k, v in xQuoteTable.items(): 3742 xDequoteTable[v[-1]] = k 3743 3744xEscape_re = re.compile(f"{re.escape(X_QUOTE)}.", re.DOTALL) 3745 3746 3747def ctcpQuote(s): 3748 for c in (X_QUOTE, X_DELIM): 3749 s = s.replace(c, xQuoteTable[c]) 3750 return s 3751 3752 3753def ctcpDequote(s): 3754 def sub(matchobj, xDequoteTable=xDequoteTable): 3755 s = matchobj.group()[1] 3756 try: 3757 s = xDequoteTable[s] 3758 except KeyError: 3759 s = s 3760 return s 3761 3762 return xEscape_re.sub(sub, s) 3763 3764 3765def ctcpStringify(messages): 3766 """ 3767 @type messages: a list of extended messages. An extended 3768 message is a (tag, data) tuple, where 'data' may be L{None}, a 3769 string, or a list of strings to be joined with whitespace. 3770 3771 @returns: String 3772 """ 3773 coded_messages = [] 3774 for (tag, data) in messages: 3775 if data: 3776 if not isinstance(data, str): 3777 try: 3778 # data as list-of-strings 3779 data = " ".join(map(str, data)) 3780 except TypeError: 3781 # No? Then use it's %s representation. 3782 pass 3783 m = f"{tag} {data}" 3784 else: 3785 m = str(tag) 3786 m = ctcpQuote(m) 3787 m = f"{X_DELIM}{m}{X_DELIM}" 3788 coded_messages.append(m) 3789 3790 line = "".join(coded_messages) 3791 return line 3792 3793 3794# Constants (from RFC 2812) 3795RPL_WELCOME = "001" 3796RPL_YOURHOST = "002" 3797RPL_CREATED = "003" 3798RPL_MYINFO = "004" 3799RPL_ISUPPORT = "005" 3800RPL_BOUNCE = "010" 3801RPL_USERHOST = "302" 3802RPL_ISON = "303" 3803RPL_AWAY = "301" 3804RPL_UNAWAY = "305" 3805RPL_NOWAWAY = "306" 3806RPL_WHOISUSER = "311" 3807RPL_WHOISSERVER = "312" 3808RPL_WHOISOPERATOR = "313" 3809RPL_WHOISIDLE = "317" 3810RPL_ENDOFWHOIS = "318" 3811RPL_WHOISCHANNELS = "319" 3812RPL_WHOWASUSER = "314" 3813RPL_ENDOFWHOWAS = "369" 3814RPL_LISTSTART = "321" 3815RPL_LIST = "322" 3816RPL_LISTEND = "323" 3817RPL_UNIQOPIS = "325" 3818RPL_CHANNELMODEIS = "324" 3819RPL_NOTOPIC = "331" 3820RPL_TOPIC = "332" 3821RPL_INVITING = "341" 3822RPL_SUMMONING = "342" 3823RPL_INVITELIST = "346" 3824RPL_ENDOFINVITELIST = "347" 3825RPL_EXCEPTLIST = "348" 3826RPL_ENDOFEXCEPTLIST = "349" 3827RPL_VERSION = "351" 3828RPL_WHOREPLY = "352" 3829RPL_ENDOFWHO = "315" 3830RPL_NAMREPLY = "353" 3831RPL_ENDOFNAMES = "366" 3832RPL_LINKS = "364" 3833RPL_ENDOFLINKS = "365" 3834RPL_BANLIST = "367" 3835RPL_ENDOFBANLIST = "368" 3836RPL_INFO = "371" 3837RPL_ENDOFINFO = "374" 3838RPL_MOTDSTART = "375" 3839RPL_MOTD = "372" 3840RPL_ENDOFMOTD = "376" 3841RPL_YOUREOPER = "381" 3842RPL_REHASHING = "382" 3843RPL_YOURESERVICE = "383" 3844RPL_TIME = "391" 3845RPL_USERSSTART = "392" 3846RPL_USERS = "393" 3847RPL_ENDOFUSERS = "394" 3848RPL_NOUSERS = "395" 3849RPL_TRACELINK = "200" 3850RPL_TRACECONNECTING = "201" 3851RPL_TRACEHANDSHAKE = "202" 3852RPL_TRACEUNKNOWN = "203" 3853RPL_TRACEOPERATOR = "204" 3854RPL_TRACEUSER = "205" 3855RPL_TRACESERVER = "206" 3856RPL_TRACESERVICE = "207" 3857RPL_TRACENEWTYPE = "208" 3858RPL_TRACECLASS = "209" 3859RPL_TRACERECONNECT = "210" 3860RPL_TRACELOG = "261" 3861RPL_TRACEEND = "262" 3862RPL_STATSLINKINFO = "211" 3863RPL_STATSCOMMANDS = "212" 3864RPL_ENDOFSTATS = "219" 3865RPL_STATSUPTIME = "242" 3866RPL_STATSOLINE = "243" 3867RPL_UMODEIS = "221" 3868RPL_SERVLIST = "234" 3869RPL_SERVLISTEND = "235" 3870RPL_LUSERCLIENT = "251" 3871RPL_LUSEROP = "252" 3872RPL_LUSERUNKNOWN = "253" 3873RPL_LUSERCHANNELS = "254" 3874RPL_LUSERME = "255" 3875RPL_ADMINME = "256" 3876RPL_ADMINLOC1 = "257" 3877RPL_ADMINLOC2 = "258" 3878RPL_ADMINEMAIL = "259" 3879RPL_TRYAGAIN = "263" 3880ERR_NOSUCHNICK = "401" 3881ERR_NOSUCHSERVER = "402" 3882ERR_NOSUCHCHANNEL = "403" 3883ERR_CANNOTSENDTOCHAN = "404" 3884ERR_TOOMANYCHANNELS = "405" 3885ERR_WASNOSUCHNICK = "406" 3886ERR_TOOMANYTARGETS = "407" 3887ERR_NOSUCHSERVICE = "408" 3888ERR_NOORIGIN = "409" 3889ERR_NORECIPIENT = "411" 3890ERR_NOTEXTTOSEND = "412" 3891ERR_NOTOPLEVEL = "413" 3892ERR_WILDTOPLEVEL = "414" 3893ERR_BADMASK = "415" 3894# Defined in errata. 3895# https://www.rfc-editor.org/errata_search.php?rfc=2812&eid=2822 3896ERR_TOOMANYMATCHES = "416" 3897ERR_UNKNOWNCOMMAND = "421" 3898ERR_NOMOTD = "422" 3899ERR_NOADMININFO = "423" 3900ERR_FILEERROR = "424" 3901ERR_NONICKNAMEGIVEN = "431" 3902ERR_ERRONEUSNICKNAME = "432" 3903ERR_NICKNAMEINUSE = "433" 3904ERR_NICKCOLLISION = "436" 3905ERR_UNAVAILRESOURCE = "437" 3906ERR_USERNOTINCHANNEL = "441" 3907ERR_NOTONCHANNEL = "442" 3908ERR_USERONCHANNEL = "443" 3909ERR_NOLOGIN = "444" 3910ERR_SUMMONDISABLED = "445" 3911ERR_USERSDISABLED = "446" 3912ERR_NOTREGISTERED = "451" 3913ERR_NEEDMOREPARAMS = "461" 3914ERR_ALREADYREGISTRED = "462" 3915ERR_NOPERMFORHOST = "463" 3916ERR_PASSWDMISMATCH = "464" 3917ERR_YOUREBANNEDCREEP = "465" 3918ERR_YOUWILLBEBANNED = "466" 3919ERR_KEYSET = "467" 3920ERR_CHANNELISFULL = "471" 3921ERR_UNKNOWNMODE = "472" 3922ERR_INVITEONLYCHAN = "473" 3923ERR_BANNEDFROMCHAN = "474" 3924ERR_BADCHANNELKEY = "475" 3925ERR_BADCHANMASK = "476" 3926ERR_NOCHANMODES = "477" 3927ERR_BANLISTFULL = "478" 3928ERR_NOPRIVILEGES = "481" 3929ERR_CHANOPRIVSNEEDED = "482" 3930ERR_CANTKILLSERVER = "483" 3931ERR_RESTRICTED = "484" 3932ERR_UNIQOPPRIVSNEEDED = "485" 3933ERR_NOOPERHOST = "491" 3934ERR_NOSERVICEHOST = "492" 3935ERR_UMODEUNKNOWNFLAG = "501" 3936ERR_USERSDONTMATCH = "502" 3937 3938# And hey, as long as the strings are already intern'd... 3939symbolic_to_numeric = { 3940 "RPL_WELCOME": "001", 3941 "RPL_YOURHOST": "002", 3942 "RPL_CREATED": "003", 3943 "RPL_MYINFO": "004", 3944 "RPL_ISUPPORT": "005", 3945 "RPL_BOUNCE": "010", 3946 "RPL_USERHOST": "302", 3947 "RPL_ISON": "303", 3948 "RPL_AWAY": "301", 3949 "RPL_UNAWAY": "305", 3950 "RPL_NOWAWAY": "306", 3951 "RPL_WHOISUSER": "311", 3952 "RPL_WHOISSERVER": "312", 3953 "RPL_WHOISOPERATOR": "313", 3954 "RPL_WHOISIDLE": "317", 3955 "RPL_ENDOFWHOIS": "318", 3956 "RPL_WHOISCHANNELS": "319", 3957 "RPL_WHOWASUSER": "314", 3958 "RPL_ENDOFWHOWAS": "369", 3959 "RPL_LISTSTART": "321", 3960 "RPL_LIST": "322", 3961 "RPL_LISTEND": "323", 3962 "RPL_UNIQOPIS": "325", 3963 "RPL_CHANNELMODEIS": "324", 3964 "RPL_NOTOPIC": "331", 3965 "RPL_TOPIC": "332", 3966 "RPL_INVITING": "341", 3967 "RPL_SUMMONING": "342", 3968 "RPL_INVITELIST": "346", 3969 "RPL_ENDOFINVITELIST": "347", 3970 "RPL_EXCEPTLIST": "348", 3971 "RPL_ENDOFEXCEPTLIST": "349", 3972 "RPL_VERSION": "351", 3973 "RPL_WHOREPLY": "352", 3974 "RPL_ENDOFWHO": "315", 3975 "RPL_NAMREPLY": "353", 3976 "RPL_ENDOFNAMES": "366", 3977 "RPL_LINKS": "364", 3978 "RPL_ENDOFLINKS": "365", 3979 "RPL_BANLIST": "367", 3980 "RPL_ENDOFBANLIST": "368", 3981 "RPL_INFO": "371", 3982 "RPL_ENDOFINFO": "374", 3983 "RPL_MOTDSTART": "375", 3984 "RPL_MOTD": "372", 3985 "RPL_ENDOFMOTD": "376", 3986 "RPL_YOUREOPER": "381", 3987 "RPL_REHASHING": "382", 3988 "RPL_YOURESERVICE": "383", 3989 "RPL_TIME": "391", 3990 "RPL_USERSSTART": "392", 3991 "RPL_USERS": "393", 3992 "RPL_ENDOFUSERS": "394", 3993 "RPL_NOUSERS": "395", 3994 "RPL_TRACELINK": "200", 3995 "RPL_TRACECONNECTING": "201", 3996 "RPL_TRACEHANDSHAKE": "202", 3997 "RPL_TRACEUNKNOWN": "203", 3998 "RPL_TRACEOPERATOR": "204", 3999 "RPL_TRACEUSER": "205", 4000 "RPL_TRACESERVER": "206", 4001 "RPL_TRACESERVICE": "207", 4002 "RPL_TRACENEWTYPE": "208", 4003 "RPL_TRACECLASS": "209", 4004 "RPL_TRACERECONNECT": "210", 4005 "RPL_TRACELOG": "261", 4006 "RPL_TRACEEND": "262", 4007 "RPL_STATSLINKINFO": "211", 4008 "RPL_STATSCOMMANDS": "212", 4009 "RPL_ENDOFSTATS": "219", 4010 "RPL_STATSUPTIME": "242", 4011 "RPL_STATSOLINE": "243", 4012 "RPL_UMODEIS": "221", 4013 "RPL_SERVLIST": "234", 4014 "RPL_SERVLISTEND": "235", 4015 "RPL_LUSERCLIENT": "251", 4016 "RPL_LUSEROP": "252", 4017 "RPL_LUSERUNKNOWN": "253", 4018 "RPL_LUSERCHANNELS": "254", 4019 "RPL_LUSERME": "255", 4020 "RPL_ADMINME": "256", 4021 "RPL_ADMINLOC1": "257", 4022 "RPL_ADMINLOC2": "258", 4023 "RPL_ADMINEMAIL": "259", 4024 "RPL_TRYAGAIN": "263", 4025 "ERR_NOSUCHNICK": "401", 4026 "ERR_NOSUCHSERVER": "402", 4027 "ERR_NOSUCHCHANNEL": "403", 4028 "ERR_CANNOTSENDTOCHAN": "404", 4029 "ERR_TOOMANYCHANNELS": "405", 4030 "ERR_WASNOSUCHNICK": "406", 4031 "ERR_TOOMANYTARGETS": "407", 4032 "ERR_NOSUCHSERVICE": "408", 4033 "ERR_NOORIGIN": "409", 4034 "ERR_NORECIPIENT": "411", 4035 "ERR_NOTEXTTOSEND": "412", 4036 "ERR_NOTOPLEVEL": "413", 4037 "ERR_WILDTOPLEVEL": "414", 4038 "ERR_BADMASK": "415", 4039 "ERR_TOOMANYMATCHES": "416", 4040 "ERR_UNKNOWNCOMMAND": "421", 4041 "ERR_NOMOTD": "422", 4042 "ERR_NOADMININFO": "423", 4043 "ERR_FILEERROR": "424", 4044 "ERR_NONICKNAMEGIVEN": "431", 4045 "ERR_ERRONEUSNICKNAME": "432", 4046 "ERR_NICKNAMEINUSE": "433", 4047 "ERR_NICKCOLLISION": "436", 4048 "ERR_UNAVAILRESOURCE": "437", 4049 "ERR_USERNOTINCHANNEL": "441", 4050 "ERR_NOTONCHANNEL": "442", 4051 "ERR_USERONCHANNEL": "443", 4052 "ERR_NOLOGIN": "444", 4053 "ERR_SUMMONDISABLED": "445", 4054 "ERR_USERSDISABLED": "446", 4055 "ERR_NOTREGISTERED": "451", 4056 "ERR_NEEDMOREPARAMS": "461", 4057 "ERR_ALREADYREGISTRED": "462", 4058 "ERR_NOPERMFORHOST": "463", 4059 "ERR_PASSWDMISMATCH": "464", 4060 "ERR_YOUREBANNEDCREEP": "465", 4061 "ERR_YOUWILLBEBANNED": "466", 4062 "ERR_KEYSET": "467", 4063 "ERR_CHANNELISFULL": "471", 4064 "ERR_UNKNOWNMODE": "472", 4065 "ERR_INVITEONLYCHAN": "473", 4066 "ERR_BANNEDFROMCHAN": "474", 4067 "ERR_BADCHANNELKEY": "475", 4068 "ERR_BADCHANMASK": "476", 4069 "ERR_NOCHANMODES": "477", 4070 "ERR_BANLISTFULL": "478", 4071 "ERR_NOPRIVILEGES": "481", 4072 "ERR_CHANOPRIVSNEEDED": "482", 4073 "ERR_CANTKILLSERVER": "483", 4074 "ERR_RESTRICTED": "484", 4075 "ERR_UNIQOPPRIVSNEEDED": "485", 4076 "ERR_NOOPERHOST": "491", 4077 "ERR_NOSERVICEHOST": "492", 4078 "ERR_UMODEUNKNOWNFLAG": "501", 4079 "ERR_USERSDONTMATCH": "502", 4080} 4081 4082numeric_to_symbolic = {} 4083for k, v in symbolic_to_numeric.items(): 4084 numeric_to_symbolic[v] = k 4085