1# -*- coding: utf-8 -*- 2 3""" 4Internet Relay Chat (IRC) protocol client library. 5 6This library is intended to encapsulate the IRC protocol in Python. 7It provides an event-driven IRC client framework. It has 8a fairly thorough support for the basic IRC protocol, CTCP, and DCC chat. 9 10To best understand how to make an IRC client, the reader more 11or less must understand the IRC specifications. They are available 12here: [IRC specifications]. 13 14The main features of the IRC client framework are: 15 16 * Abstraction of the IRC protocol. 17 * Handles multiple simultaneous IRC server connections. 18 * Handles server PONGing transparently. 19 * Messages to the IRC server are done by calling methods on an IRC 20 connection object. 21 * Messages from an IRC server triggers events, which can be caught 22 by event handlers. 23 * Reading from and writing to IRC server sockets are normally done 24 by an internal select() loop, but the select()ing may be done by 25 an external main loop. 26 * Functions can be registered to execute at specified times by the 27 event-loop. 28 * Decodes CTCP tagging correctly (hopefully); I haven't seen any 29 other IRC client implementation that handles the CTCP 30 specification subtilties. 31 * A kind of simple, single-server, object-oriented IRC client class 32 that dispatches events to instance methods is included. 33 34Current limitations: 35 36 * Data is not written asynchronously to the server, i.e. the write() 37 may block if the TCP buffers are stuffed. 38 * DCC file transfers are not supported. 39 * RFCs 2810, 2811, 2812, and 2813 have not been considered. 40 41Notes: 42 * connection.quit() only sends QUIT to the server. 43 * ERROR from the server triggers the error event and the disconnect event. 44 * dropping of the connection triggers the disconnect event. 45 46 47.. [IRC specifications] http://www.irchelp.org/irchelp/rfc/ 48""" 49 50from __future__ import absolute_import, division 51 52import bisect 53import re 54import select 55import socket 56import time 57import struct 58import logging 59import threading 60import abc 61import collections 62import functools 63import itertools 64import contextlib 65 66import six 67from jaraco.itertools import always_iterable 68from jaraco.functools import Throttler 69 70try: 71 import pkg_resources 72except ImportError: 73 pass 74 75from . import connection 76from . import events 77from . import functools as irc_functools 78from . import buffer 79from . import schedule 80from . import features 81from . import ctcp 82from . import message 83 84log = logging.getLogger(__name__) 85 86# set the version tuple 87try: 88 VERSION_STRING = pkg_resources.require('irc')[0].version 89 VERSION = tuple(int(res) for res in re.findall('\d+', VERSION_STRING)) 90except Exception: 91 VERSION_STRING = 'unknown' 92 VERSION = () 93 94 95class IRCError(Exception): 96 "An IRC exception" 97 98class InvalidCharacters(ValueError): 99 "Invalid characters were encountered in the message" 100 101class MessageTooLong(ValueError): 102 "Message is too long" 103 104class PrioritizedHandler( 105 collections.namedtuple('Base', ('priority', 'callback'))): 106 def __lt__(self, other): 107 "when sorting prioritized handlers, only use the priority" 108 return self.priority < other.priority 109 110class Reactor(object): 111 """ 112 Processes events from one or more IRC server connections. 113 114 This class implements a reactor in the style of the `reactor pattern 115 <http://en.wikipedia.org/wiki/Reactor_pattern>`_. 116 117 When a Reactor object has been instantiated, it can be used to create 118 Connection objects that represent the IRC connections. The 119 responsibility of the reactor object is to provide an event-driven 120 framework for the connections and to keep the connections alive. 121 It runs a select loop to poll each connection's TCP socket and 122 hands over the sockets with incoming data for processing by the 123 corresponding connection. 124 125 The methods of most interest for an IRC client writer are server, 126 add_global_handler, remove_global_handler, execute_at, 127 execute_delayed, execute_every, process_once, and process_forever. 128 129 This is functionally an event-loop which can either use it's own 130 internal polling loop, or tie into an external event-loop, by 131 having the external event-system periodically call `process_once` 132 on the instantiated reactor class. This will allow the reactor 133 to process any queued data and/or events. 134 135 Calling `process_forever` will hand off execution to the reactor's 136 internal event-loop, which will not return for the life of the 137 reactor. 138 139 Here is an example: 140 141 client = irc.client.Reactor() 142 server = client.server() 143 server.connect("irc.some.where", 6667, "my_nickname") 144 server.privmsg("a_nickname", "Hi there!") 145 client.process_forever() 146 147 This will connect to the IRC server irc.some.where on port 6667 148 using the nickname my_nickname and send the message "Hi there!" 149 to the nickname a_nickname. 150 151 The methods of this class are thread-safe; accesses to and modifications 152 of its internal lists of connections, handlers, and delayed commands 153 are guarded by a mutex. 154 """ 155 156 def __do_nothing(*args, **kwargs): 157 pass 158 159 def __init__(self, on_connect=__do_nothing, on_disconnect=__do_nothing, 160 on_schedule=__do_nothing): 161 """Constructor for Reactor objects. 162 163 on_connect: optional callback invoked when a new connection 164 is made. 165 166 on_disconnect: optional callback invoked when a socket is 167 disconnected. 168 169 on_schedule: optional callback, usually supplied by an external 170 event loop, to indicate in float seconds that the client needs to 171 process events that many seconds in the future. An external event 172 loop will implement this callback to schedule a call to 173 process_timeout. 174 175 The three arguments mainly exist to be able to use an external 176 main loop (for example Tkinter's or PyGTK's main app loop) 177 instead of calling the process_forever method. 178 179 An alternative is to just call ServerConnection.process_once() 180 once in a while. 181 """ 182 183 self._on_connect = on_connect 184 self._on_disconnect = on_disconnect 185 self._on_schedule = on_schedule 186 187 self.connections = [] 188 self.handlers = {} 189 self.delayed_commands = [] # list of DelayedCommands 190 # Modifications to these shared lists and dict need to be thread-safe 191 self.mutex = threading.RLock() 192 193 self.add_global_handler("ping", _ping_ponger, -42) 194 195 def server(self): 196 """Creates and returns a ServerConnection object.""" 197 198 c = ServerConnection(self) 199 with self.mutex: 200 self.connections.append(c) 201 return c 202 203 def process_data(self, sockets): 204 """Called when there is more data to read on connection sockets. 205 206 Arguments: 207 208 sockets -- A list of socket objects. 209 210 See documentation for Reactor.__init__. 211 """ 212 with self.mutex: 213 log.log(logging.DEBUG-2, "process_data()") 214 for s, c in itertools.product(sockets, self.connections): 215 if s == c.socket: 216 c.process_data() 217 218 def process_timeout(self): 219 """Called when a timeout notification is due. 220 221 See documentation for Reactor.__init__. 222 """ 223 with self.mutex: 224 while self.delayed_commands: 225 command = self.delayed_commands[0] 226 if not command.due(): 227 break 228 command.function() 229 if isinstance(command, schedule.PeriodicCommand): 230 self._schedule_command(command.next()) 231 del self.delayed_commands[0] 232 233 @property 234 def sockets(self): 235 with self.mutex: 236 return [ 237 conn.socket 238 for conn in self.connections 239 if conn is not None 240 and conn.socket is not None 241 ] 242 243 def process_once(self, timeout=0): 244 """Process data from connections once. 245 246 Arguments: 247 248 timeout -- How long the select() call should wait if no 249 data is available. 250 251 This method should be called periodically to check and process 252 incoming data, if there are any. If that seems boring, look 253 at the process_forever method. 254 """ 255 log.log(logging.DEBUG-2, "process_once()") 256 sockets = self.sockets 257 if sockets: 258 (i, o, e) = select.select(sockets, [], [], timeout) 259 self.process_data(i) 260 else: 261 time.sleep(timeout) 262 self.process_timeout() 263 264 def process_forever(self, timeout=0.2): 265 """Run an infinite loop, processing data from connections. 266 267 This method repeatedly calls process_once. 268 269 Arguments: 270 271 timeout -- Parameter to pass to process_once. 272 """ 273 # This loop should specifically *not* be mutex-locked. 274 # Otherwise no other thread would ever be able to change 275 # the shared state of a Reactor object running this function. 276 log.debug("process_forever(timeout=%s)", timeout) 277 while 1: 278 self.process_once(timeout) 279 280 def disconnect_all(self, message=""): 281 """Disconnects all connections.""" 282 with self.mutex: 283 for c in self.connections: 284 c.disconnect(message) 285 286 def add_global_handler(self, event, handler, priority=0): 287 """Adds a global handler function for a specific event type. 288 289 Arguments: 290 291 event -- Event type (a string). Check the values of 292 numeric_events for possible event types. 293 294 handler -- Callback function taking 'connection' and 'event' 295 parameters. 296 297 priority -- A number (the lower number, the higher priority). 298 299 The handler function is called whenever the specified event is 300 triggered in any of the connections. See documentation for 301 the Event class. 302 303 The handler functions are called in priority order (lowest 304 number is highest priority). If a handler function returns 305 "NO MORE", no more handlers will be called. 306 """ 307 handler = PrioritizedHandler(priority, handler) 308 with self.mutex: 309 event_handlers = self.handlers.setdefault(event, []) 310 bisect.insort(event_handlers, handler) 311 312 def remove_global_handler(self, event, handler): 313 """Removes a global handler function. 314 315 Arguments: 316 317 event -- Event type (a string). 318 handler -- Callback function. 319 320 Returns 1 on success, otherwise 0. 321 """ 322 with self.mutex: 323 if not event in self.handlers: 324 return 0 325 for h in self.handlers[event]: 326 if handler == h.callback: 327 self.handlers[event].remove(h) 328 return 1 329 330 def execute_at(self, at, function, arguments=()): 331 """Execute a function at a specified time. 332 333 Arguments: 334 335 at -- Execute at this time (a standard Unix timestamp). 336 function -- Function to call. 337 arguments -- Arguments to give the function. 338 """ 339 function = functools.partial(function, *arguments) 340 command = schedule.DelayedCommand.at_time(at, function) 341 self._schedule_command(command) 342 343 def execute_delayed(self, delay, function, arguments=()): 344 """ 345 Execute a function after a specified time. 346 347 delay -- How many seconds to wait. 348 function -- Function to call. 349 arguments -- Arguments to give the function. 350 """ 351 function = functools.partial(function, *arguments) 352 command = schedule.DelayedCommand.after(delay, function) 353 self._schedule_command(command) 354 355 def execute_every(self, period, function, arguments=()): 356 """ 357 Execute a function every 'period' seconds. 358 359 period -- How often to run (always waits this long for first). 360 function -- Function to call. 361 arguments -- Arguments to give the function. 362 """ 363 function = functools.partial(function, *arguments) 364 command = schedule.PeriodicCommand.after(period, function) 365 self._schedule_command(command) 366 367 def _schedule_command(self, command): 368 with self.mutex: 369 bisect.insort(self.delayed_commands, command) 370 self._on_schedule(command.delay.total_seconds()) 371 372 def dcc(self, dcctype="chat"): 373 """Creates and returns a DCCConnection object. 374 375 Arguments: 376 377 dcctype -- "chat" for DCC CHAT connections or "raw" for 378 DCC SEND (or other DCC types). If "chat", 379 incoming data will be split in newline-separated 380 chunks. If "raw", incoming data is not touched. 381 """ 382 with self.mutex: 383 c = DCCConnection(self, dcctype) 384 self.connections.append(c) 385 return c 386 387 def _handle_event(self, connection, event): 388 """ 389 Handle an Event event incoming on ServerConnection connection. 390 """ 391 with self.mutex: 392 h = self.handlers 393 matching_handlers = sorted( 394 h.get("all_events", []) + 395 h.get(event.type, []) 396 ) 397 for handler in matching_handlers: 398 result = handler.callback(connection, event) 399 if result == "NO MORE": 400 return 401 402 def _remove_connection(self, connection): 403 """[Internal]""" 404 with self.mutex: 405 self.connections.remove(connection) 406 self._on_disconnect(connection.socket) 407 408_cmd_pat = "^(@(?P<tags>[^ ]*) )?(:(?P<prefix>[^ ]+) +)?(?P<command>[^ ]+)( *(?P<argument> .+))?" 409_rfc_1459_command_regexp = re.compile(_cmd_pat) 410 411 412@six.add_metaclass(abc.ABCMeta) 413class Connection(object): 414 """ 415 Base class for IRC connections. 416 """ 417 418 @abc.abstractproperty 419 def socket(self): 420 "The socket for this connection" 421 422 def __init__(self, reactor): 423 self.reactor = reactor 424 425 ############################## 426 ### Convenience wrappers. 427 428 def execute_at(self, at, function, arguments=()): 429 self.reactor.execute_at(at, function, arguments) 430 431 def execute_delayed(self, delay, function, arguments=()): 432 self.reactor.execute_delayed(delay, function, arguments) 433 434 def execute_every(self, period, function, arguments=()): 435 self.reactor.execute_every(period, function, arguments) 436 437class ServerConnectionError(IRCError): 438 pass 439 440class ServerNotConnectedError(ServerConnectionError): 441 pass 442 443 444class ServerConnection(Connection): 445 """ 446 An IRC server connection. 447 448 ServerConnection objects are instantiated by calling the server 449 method on a Reactor object. 450 """ 451 452 buffer_class = buffer.DecodingLineBuffer 453 socket = None 454 455 def __init__(self, reactor): 456 super(ServerConnection, self).__init__(reactor) 457 self.connected = False 458 self.features = features.FeatureSet() 459 460 # save the method args to allow for easier reconnection. 461 @irc_functools.save_method_args 462 def connect(self, server, port, nickname, password=None, username=None, 463 ircname=None, connect_factory=connection.Factory()): 464 """Connect/reconnect to a server. 465 466 Arguments: 467 468 * server - Server name 469 * port - Port number 470 * nickname - The nickname 471 * password - Password (if any) 472 * username - The username 473 * ircname - The IRC name ("realname") 474 * server_address - The remote host/port of the server 475 * connect_factory - A callable that takes the server address and 476 returns a connection (with a socket interface) 477 478 This function can be called to reconnect a closed connection. 479 480 Returns the ServerConnection object. 481 """ 482 log.debug("connect(server=%r, port=%r, nickname=%r, ...)", server, 483 port, nickname) 484 485 if self.connected: 486 self.disconnect("Changing servers") 487 488 self.buffer = self.buffer_class() 489 self.handlers = {} 490 self.real_server_name = "" 491 self.real_nickname = nickname 492 self.server = server 493 self.port = port 494 self.server_address = (server, port) 495 self.nickname = nickname 496 self.username = username or nickname 497 self.ircname = ircname or nickname 498 self.password = password 499 self.connect_factory = connect_factory 500 try: 501 self.socket = self.connect_factory(self.server_address) 502 except socket.error as ex: 503 raise ServerConnectionError("Couldn't connect to socket: %s" % ex) 504 self.connected = True 505 self.reactor._on_connect(self.socket) 506 507 # Log on... 508 if self.password: 509 self.pass_(self.password) 510 self.nick(self.nickname) 511 self.user(self.username, self.ircname) 512 return self 513 514 def reconnect(self): 515 """ 516 Reconnect with the last arguments passed to self.connect() 517 """ 518 self.connect(*self._saved_connect.args, **self._saved_connect.kwargs) 519 520 def close(self): 521 """Close the connection. 522 523 This method closes the connection permanently; after it has 524 been called, the object is unusable. 525 """ 526 # Without this thread lock, there is a window during which 527 # select() can find a closed socket, leading to an EBADF error. 528 with self.reactor.mutex: 529 self.disconnect("Closing object") 530 self.reactor._remove_connection(self) 531 532 def get_server_name(self): 533 """Get the (real) server name. 534 535 This method returns the (real) server name, or, more 536 specifically, what the server calls itself. 537 """ 538 return self.real_server_name or "" 539 540 def get_nickname(self): 541 """Get the (real) nick name. 542 543 This method returns the (real) nickname. The library keeps 544 track of nick changes, so it might not be the nick name that 545 was passed to the connect() method. 546 """ 547 return self.real_nickname 548 549 @contextlib.contextmanager 550 def as_nick(self, name): 551 """ 552 Set the nick for the duration of the context. 553 """ 554 orig = self.get_nickname() 555 self.nick(name) 556 try: 557 yield orig 558 finally: 559 self.nick(orig) 560 561 def process_data(self): 562 "read and process input from self.socket" 563 564 try: 565 reader = getattr(self.socket, 'read', self.socket.recv) 566 new_data = reader(2 ** 14) 567 except socket.error: 568 # The server hung up. 569 self.disconnect("Connection reset by peer") 570 return 571 if not new_data: 572 # Read nothing: connection must be down. 573 self.disconnect("Connection reset by peer") 574 return 575 576 self.buffer.feed(new_data) 577 578 # process each non-empty line after logging all lines 579 for line in self.buffer: 580 log.debug("FROM SERVER: %s", line) 581 if not line: continue 582 self._process_line(line) 583 584 def _process_line(self, line): 585 event = Event("all_raw_messages", self.get_server_name(), None, 586 [line]) 587 self._handle_event(event) 588 589 grp = _rfc_1459_command_regexp.match(line).group 590 591 source = NickMask.from_group(grp("prefix")) 592 command = self._command_from_group(grp("command")) 593 arguments = message.Arguments.from_group(grp('argument')) 594 tags = message.Tag.from_group(grp('tags')) 595 596 if source and not self.real_server_name: 597 self.real_server_name = source 598 599 if command == "nick": 600 if source.nick == self.real_nickname: 601 self.real_nickname = arguments[0] 602 elif command == "welcome": 603 # Record the nickname in case the client changed nick 604 # in a nicknameinuse callback. 605 self.real_nickname = arguments[0] 606 elif command == "featurelist": 607 self.features.load(arguments) 608 609 handler = ( 610 self._handle_message 611 if command in ["privmsg", "notice"] 612 else self._handle_other 613 ) 614 handler(arguments, command, source, tags) 615 616 def _handle_message(self, arguments, command, source, tags): 617 target, msg = arguments[:2] 618 messages = ctcp.dequote(msg) 619 if command == "privmsg": 620 if is_channel(target): 621 command = "pubmsg" 622 else: 623 if is_channel(target): 624 command = "pubnotice" 625 else: 626 command = "privnotice" 627 for m in messages: 628 if isinstance(m, tuple): 629 if command in ["privmsg", "pubmsg"]: 630 command = "ctcp" 631 else: 632 command = "ctcpreply" 633 634 m = list(m) 635 log.debug("command: %s, source: %s, target: %s, " 636 "arguments: %s, tags: %s", command, source, target, m, tags) 637 event = Event(command, source, target, m, tags) 638 self._handle_event(event) 639 if command == "ctcp" and m[0] == "ACTION": 640 event = Event("action", source, target, m[1:], tags) 641 self._handle_event(event) 642 else: 643 log.debug("command: %s, source: %s, target: %s, " 644 "arguments: %s, tags: %s", command, source, target, [m], tags) 645 event = Event(command, source, target, [m], tags) 646 self._handle_event(event) 647 648 def _handle_other(self, arguments, command, source, tags): 649 target = None 650 if command == "quit": 651 arguments = [arguments[0]] 652 elif command == "ping": 653 target = arguments[0] 654 else: 655 target = arguments[0] if arguments else None 656 arguments = arguments[1:] 657 if command == "mode": 658 if not is_channel(target): 659 command = "umode" 660 log.debug("command: %s, source: %s, target: %s, " 661 "arguments: %s, tags: %s", command, source, target, arguments, tags) 662 event = Event(command, source, target, arguments, tags) 663 self._handle_event(event) 664 665 @staticmethod 666 def _command_from_group(group): 667 command = group.lower() 668 # Translate numerics into more readable strings. 669 return events.numeric.get(command, command) 670 671 def _handle_event(self, event): 672 """[Internal]""" 673 self.reactor._handle_event(self, event) 674 if event.type in self.handlers: 675 for fn in self.handlers[event.type]: 676 fn(self, event) 677 678 def is_connected(self): 679 """Return connection status. 680 681 Returns true if connected, otherwise false. 682 """ 683 return self.connected 684 685 def add_global_handler(self, *args): 686 """Add global handler. 687 688 See documentation for IRC.add_global_handler. 689 """ 690 self.reactor.add_global_handler(*args) 691 692 def remove_global_handler(self, *args): 693 """Remove global handler. 694 695 See documentation for IRC.remove_global_handler. 696 """ 697 self.reactor.remove_global_handler(*args) 698 699 def action(self, target, action): 700 """Send a CTCP ACTION command.""" 701 self.ctcp("ACTION", target, action) 702 703 def admin(self, server=""): 704 """Send an ADMIN command.""" 705 self.send_raw(" ".join(["ADMIN", server]).strip()) 706 707 def cap(self, subcommand, *args): 708 """ 709 Send a CAP command according to `the spec 710 <http://ircv3.atheme.org/specification/capability-negotiation-3.1>`_. 711 712 Arguments: 713 714 subcommand -- LS, LIST, REQ, ACK, CLEAR, END 715 args -- capabilities, if required for given subcommand 716 717 Example: 718 719 .cap('LS') 720 .cap('REQ', 'multi-prefix', 'sasl') 721 .cap('END') 722 """ 723 cap_subcommands = set('LS LIST REQ ACK NAK CLEAR END'.split()) 724 client_subcommands = set(cap_subcommands) - set('NAK') 725 assert subcommand in client_subcommands, "invalid subcommand" 726 727 def _multi_parameter(args): 728 """ 729 According to the spec:: 730 731 If more than one capability is named, the RFC1459 designated 732 sentinel (:) for a multi-parameter argument must be present. 733 734 It's not obvious where the sentinel should be present or if it 735 must be omitted for a single parameter, so follow convention and 736 only include the sentinel prefixed to the first parameter if more 737 than one parameter is present. 738 """ 739 if len(args) > 1: 740 return (':' + args[0],) + args[1:] 741 return args 742 743 args = _multi_parameter(args) 744 self.send_raw(' '.join(('CAP', subcommand) + args)) 745 746 def ctcp(self, ctcptype, target, parameter=""): 747 """Send a CTCP command.""" 748 ctcptype = ctcptype.upper() 749 tmpl = ( 750 "\001{ctcptype} {parameter}\001" if parameter else 751 "\001{ctcptype}\001" 752 ) 753 self.privmsg(target, tmpl.format(**vars())) 754 755 def ctcp_reply(self, target, parameter): 756 """Send a CTCP REPLY command.""" 757 self.notice(target, "\001%s\001" % parameter) 758 759 def disconnect(self, message=""): 760 """Hang up the connection. 761 762 Arguments: 763 764 message -- Quit message. 765 """ 766 if not self.connected: 767 return 768 769 self.connected = 0 770 771 self.quit(message) 772 773 try: 774 self.socket.shutdown(socket.SHUT_WR) 775 self.socket.close() 776 except socket.error: 777 pass 778 del self.socket 779 self._handle_event(Event("disconnect", self.server, "", [message])) 780 781 def globops(self, text): 782 """Send a GLOBOPS command.""" 783 self.send_raw("GLOBOPS :" + text) 784 785 def info(self, server=""): 786 """Send an INFO command.""" 787 self.send_raw(" ".join(["INFO", server]).strip()) 788 789 def invite(self, nick, channel): 790 """Send an INVITE command.""" 791 self.send_raw(" ".join(["INVITE", nick, channel]).strip()) 792 793 def ison(self, nicks): 794 """Send an ISON command. 795 796 Arguments: 797 798 nicks -- List of nicks. 799 """ 800 self.send_raw("ISON " + " ".join(nicks)) 801 802 def join(self, channel, key=""): 803 """Send a JOIN command.""" 804 self.send_raw("JOIN %s%s" % (channel, (key and (" " + key)))) 805 806 def kick(self, channel, nick, comment=""): 807 """Send a KICK command.""" 808 tmpl = "KICK {channel} {nick}" 809 if comment: 810 tmpl += " :{comment}" 811 self.send_raw(tmpl.format(**vars())) 812 813 def links(self, remote_server="", server_mask=""): 814 """Send a LINKS command.""" 815 command = "LINKS" 816 if remote_server: 817 command = command + " " + remote_server 818 if server_mask: 819 command = command + " " + server_mask 820 self.send_raw(command) 821 822 def list(self, channels=None, server=""): 823 """Send a LIST command.""" 824 command = "LIST" 825 channels = ",".join(always_iterable(channels)) 826 if channels: 827 command += ' ' + channels 828 if server: 829 command = command + " " + server 830 self.send_raw(command) 831 832 def lusers(self, server=""): 833 """Send a LUSERS command.""" 834 self.send_raw("LUSERS" + (server and (" " + server))) 835 836 def mode(self, target, command): 837 """Send a MODE command.""" 838 self.send_raw("MODE %s %s" % (target, command)) 839 840 def motd(self, server=""): 841 """Send an MOTD command.""" 842 self.send_raw("MOTD" + (server and (" " + server))) 843 844 def names(self, channels=None): 845 """Send a NAMES command.""" 846 tmpl = "NAMES {channels}" if channels else "NAMES" 847 channels = ','.join(always_iterable(channels)) 848 self.send_raw(tmpl.format(channels=channels)) 849 850 def nick(self, newnick): 851 """Send a NICK command.""" 852 self.send_raw("NICK " + newnick) 853 854 def notice(self, target, text): 855 """Send a NOTICE command.""" 856 # Should limit len(text) here! 857 self.send_raw("NOTICE %s :%s" % (target, text)) 858 859 def oper(self, nick, password): 860 """Send an OPER command.""" 861 self.send_raw("OPER %s %s" % (nick, password)) 862 863 def part(self, channels, message=""): 864 """Send a PART command.""" 865 channels = always_iterable(channels) 866 cmd_parts = [ 867 'PART', 868 ','.join(channels), 869 ] 870 if message: cmd_parts.append(message) 871 self.send_raw(' '.join(cmd_parts)) 872 873 def pass_(self, password): 874 """Send a PASS command.""" 875 self.send_raw("PASS " + password) 876 877 def ping(self, target, target2=""): 878 """Send a PING command.""" 879 self.send_raw("PING %s%s" % (target, target2 and (" " + target2))) 880 881 def pong(self, target, target2=""): 882 """Send a PONG command.""" 883 self.send_raw("PONG %s%s" % (target, target2 and (" " + target2))) 884 885 def privmsg(self, target, text): 886 """Send a PRIVMSG command.""" 887 self.send_raw("PRIVMSG %s :%s" % (target, text)) 888 889 def privmsg_many(self, targets, text): 890 """Send a PRIVMSG command to multiple targets.""" 891 target = ','.join(targets) 892 return self.privmsg(target, text) 893 894 def quit(self, message=""): 895 """Send a QUIT command.""" 896 # Note that many IRC servers don't use your QUIT message 897 # unless you've been connected for at least 5 minutes! 898 self.send_raw("QUIT" + (message and (" :" + message))) 899 900 def send_raw(self, string): 901 """Send raw string to the server. 902 903 The string will be padded with appropriate CR LF. 904 """ 905 # The string should not contain any carriage return other than the 906 # one added here. 907 if '\n' in string: 908 raise InvalidCharacters( 909 "Carriage returns not allowed in privmsg(text)") 910 bytes = string.encode('utf-8') + b'\r\n' 911 # According to the RFC http://tools.ietf.org/html/rfc2812#page-6, 912 # clients should not transmit more than 512 bytes. 913 if len(bytes) > 512: 914 raise MessageTooLong( 915 "Messages limited to 512 bytes including CR/LF") 916 if self.socket is None: 917 raise ServerNotConnectedError("Not connected.") 918 sender = getattr(self.socket, 'write', self.socket.send) 919 try: 920 sender(bytes) 921 log.debug("TO SERVER: %s", string) 922 except socket.error: 923 # Ouch! 924 self.disconnect("Connection reset by peer.") 925 926 def squit(self, server, comment=""): 927 """Send an SQUIT command.""" 928 self.send_raw("SQUIT %s%s" % (server, comment and (" :" + comment))) 929 930 def stats(self, statstype, server=""): 931 """Send a STATS command.""" 932 self.send_raw("STATS %s%s" % (statstype, server and (" " + server))) 933 934 def time(self, server=""): 935 """Send a TIME command.""" 936 self.send_raw("TIME" + (server and (" " + server))) 937 938 def topic(self, channel, new_topic=None): 939 """Send a TOPIC command.""" 940 if new_topic is None: 941 self.send_raw("TOPIC " + channel) 942 else: 943 self.send_raw("TOPIC %s :%s" % (channel, new_topic)) 944 945 def trace(self, target=""): 946 """Send a TRACE command.""" 947 self.send_raw("TRACE" + (target and (" " + target))) 948 949 def user(self, username, realname): 950 """Send a USER command.""" 951 self.send_raw("USER %s 0 * :%s" % (username, realname)) 952 953 def userhost(self, nicks): 954 """Send a USERHOST command.""" 955 self.send_raw("USERHOST " + ",".join(nicks)) 956 957 def users(self, server=""): 958 """Send a USERS command.""" 959 self.send_raw("USERS" + (server and (" " + server))) 960 961 def version(self, server=""): 962 """Send a VERSION command.""" 963 self.send_raw("VERSION" + (server and (" " + server))) 964 965 def wallops(self, text): 966 """Send a WALLOPS command.""" 967 self.send_raw("WALLOPS :" + text) 968 969 def who(self, target="", op=""): 970 """Send a WHO command.""" 971 self.send_raw("WHO%s%s" % (target and (" " + target), op and (" o"))) 972 973 def whois(self, targets): 974 """Send a WHOIS command.""" 975 self.send_raw("WHOIS " + ",".join(always_iterable(targets))) 976 977 def whowas(self, nick, max="", server=""): 978 """Send a WHOWAS command.""" 979 self.send_raw("WHOWAS %s%s%s" % (nick, 980 max and (" " + max), 981 server and (" " + server))) 982 983 def set_rate_limit(self, frequency): 984 """ 985 Set a `frequency` limit (messages per second) for this connection. 986 Any attempts to send faster than this rate will block. 987 """ 988 self.send_raw = Throttler(self.send_raw, frequency) 989 990 def set_keepalive(self, interval): 991 """ 992 Set a keepalive to occur every ``interval`` on this connection. 993 """ 994 pinger = functools.partial(self.ping, 'keep-alive') 995 self.reactor.execute_every(period=interval, function=pinger) 996 997 998class DCCConnectionError(IRCError): 999 pass 1000 1001 1002class DCCConnection(Connection): 1003 """ 1004 A DCC (Direct Client Connection). 1005 1006 DCCConnection objects are instantiated by calling the dcc 1007 method on a Reactor object. 1008 """ 1009 socket = None 1010 1011 def __init__(self, reactor, dcctype): 1012 super(DCCConnection, self).__init__(reactor) 1013 self.connected = 0 1014 self.passive = 0 1015 self.dcctype = dcctype 1016 self.peeraddress = None 1017 self.peerport = None 1018 1019 def connect(self, address, port): 1020 """Connect/reconnect to a DCC peer. 1021 1022 Arguments: 1023 address -- Host/IP address of the peer. 1024 1025 port -- The port number to connect to. 1026 1027 Returns the DCCConnection object. 1028 """ 1029 self.peeraddress = socket.gethostbyname(address) 1030 self.peerport = port 1031 self.buffer = buffer.LineBuffer() 1032 self.handlers = {} 1033 self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 1034 self.passive = 0 1035 try: 1036 self.socket.connect((self.peeraddress, self.peerport)) 1037 except socket.error as x: 1038 raise DCCConnectionError("Couldn't connect to socket: %s" % x) 1039 self.connected = 1 1040 self.reactor._on_connect(self.socket) 1041 return self 1042 1043 def listen(self): 1044 """Wait for a connection/reconnection from a DCC peer. 1045 1046 Returns the DCCConnection object. 1047 1048 The local IP address and port are available as 1049 self.localaddress and self.localport. After connection from a 1050 peer, the peer address and port are available as 1051 self.peeraddress and self.peerport. 1052 """ 1053 self.buffer = buffer.LineBuffer() 1054 self.handlers = {} 1055 self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 1056 self.passive = 1 1057 try: 1058 self.socket.bind((socket.gethostbyname(socket.gethostname()), 0)) 1059 self.localaddress, self.localport = self.socket.getsockname() 1060 self.socket.listen(10) 1061 except socket.error as x: 1062 raise DCCConnectionError("Couldn't bind socket: %s" % x) 1063 return self 1064 1065 def disconnect(self, message=""): 1066 """Hang up the connection and close the object. 1067 1068 Arguments: 1069 1070 message -- Quit message. 1071 """ 1072 if not self.connected: 1073 return 1074 1075 self.connected = 0 1076 try: 1077 self.socket.shutdown(socket.SHUT_WR) 1078 self.socket.close() 1079 except socket.error: 1080 pass 1081 del self.socket 1082 self.reactor._handle_event( 1083 self, 1084 Event("dcc_disconnect", self.peeraddress, "", [message])) 1085 self.reactor._remove_connection(self) 1086 1087 def process_data(self): 1088 """[Internal]""" 1089 1090 if self.passive and not self.connected: 1091 conn, (self.peeraddress, self.peerport) = self.socket.accept() 1092 self.socket.close() 1093 self.socket = conn 1094 self.connected = 1 1095 log.debug("DCC connection from %s:%d", self.peeraddress, 1096 self.peerport) 1097 self.reactor._handle_event( 1098 self, 1099 Event("dcc_connect", self.peeraddress, None, None)) 1100 return 1101 1102 try: 1103 new_data = self.socket.recv(2 ** 14) 1104 except socket.error: 1105 # The server hung up. 1106 self.disconnect("Connection reset by peer") 1107 return 1108 if not new_data: 1109 # Read nothing: connection must be down. 1110 self.disconnect("Connection reset by peer") 1111 return 1112 1113 if self.dcctype == "chat": 1114 self.buffer.feed(new_data) 1115 1116 chunks = list(self.buffer) 1117 1118 if len(self.buffer) > 2 ** 14: 1119 # Bad peer! Naughty peer! 1120 log.info("Received >16k from a peer without a newline; " 1121 "disconnecting.") 1122 self.disconnect() 1123 return 1124 else: 1125 chunks = [new_data] 1126 1127 command = "dccmsg" 1128 prefix = self.peeraddress 1129 target = None 1130 for chunk in chunks: 1131 log.debug("FROM PEER: %s", chunk) 1132 arguments = [chunk] 1133 log.debug("command: %s, source: %s, target: %s, arguments: %s", 1134 command, prefix, target, arguments) 1135 event = Event(command, prefix, target, arguments) 1136 self.reactor._handle_event(self, event) 1137 1138 def privmsg(self, text): 1139 """ 1140 Send text to DCC peer. 1141 1142 The text will be padded with a newline if it's a DCC CHAT session. 1143 """ 1144 if self.dcctype == 'chat': 1145 text += '\n' 1146 bytes = text.encode('utf-8') 1147 return self.send_bytes(bytes) 1148 1149 def send_bytes(self, bytes): 1150 """ 1151 Send data to DCC peer. 1152 """ 1153 try: 1154 self.socket.send(bytes) 1155 log.debug("TO PEER: %r\n", bytes) 1156 except socket.error: 1157 self.disconnect("Connection reset by peer.") 1158 1159 1160class SimpleIRCClient(object): 1161 """A simple single-server IRC client class. 1162 1163 This is an example of an object-oriented wrapper of the IRC 1164 framework. A real IRC client can be made by subclassing this 1165 class and adding appropriate methods. 1166 1167 The method on_join will be called when a "join" event is created 1168 (which is done when the server sends a JOIN messsage/command), 1169 on_privmsg will be called for "privmsg" events, and so on. The 1170 handler methods get two arguments: the connection object (same as 1171 self.connection) and the event object. 1172 1173 Functionally, any of the event names in `events.py` my be subscribed 1174 to by prefixing them with `on_`, and creating a function of that 1175 name in the child-class of `SimpleIRCClient`. When the event of 1176 `event_name` is received, the appropriately named method will be 1177 called (if it exists) by runtime class introspection. 1178 1179 See `_dispatcher()`, which takes the event name, postpends it to 1180 `on_`, and then attemps to look up the class member function by 1181 name and call it. 1182 1183 Instance attributes that can be used by sub classes: 1184 1185 reactor -- The Reactor instance. 1186 1187 connection -- The ServerConnection instance. 1188 1189 dcc_connections -- A list of DCCConnection instances. 1190 """ 1191 reactor_class = Reactor 1192 1193 def __init__(self): 1194 self.reactor = self.reactor_class() 1195 self.connection = self.reactor.server() 1196 self.dcc_connections = [] 1197 self.reactor.add_global_handler("all_events", self._dispatcher, -10) 1198 self.reactor.add_global_handler("dcc_disconnect", 1199 self._dcc_disconnect, -10) 1200 1201 def _dispatcher(self, connection, event): 1202 """ 1203 Dispatch events to on_<event.type> method, if present. 1204 """ 1205 log.debug("_dispatcher: %s", event.type) 1206 1207 do_nothing = lambda c, e: None 1208 method = getattr(self, "on_" + event.type, do_nothing) 1209 method(connection, event) 1210 1211 def _dcc_disconnect(self, c, e): 1212 self.dcc_connections.remove(c) 1213 1214 def connect(self, *args, **kwargs): 1215 """Connect using the underlying connection""" 1216 self.connection.connect(*args, **kwargs) 1217 1218 def dcc_connect(self, address, port, dcctype="chat"): 1219 """Connect to a DCC peer. 1220 1221 Arguments: 1222 1223 address -- IP address of the peer. 1224 1225 port -- Port to connect to. 1226 1227 Returns a DCCConnection instance. 1228 """ 1229 dcc = self.reactor.dcc(dcctype) 1230 self.dcc_connections.append(dcc) 1231 dcc.connect(address, port) 1232 return dcc 1233 1234 def dcc_listen(self, dcctype="chat"): 1235 """Listen for connections from a DCC peer. 1236 1237 Returns a DCCConnection instance. 1238 """ 1239 dcc = self.reactor.dcc(dcctype) 1240 self.dcc_connections.append(dcc) 1241 dcc.listen() 1242 return dcc 1243 1244 def start(self): 1245 """Start the IRC client.""" 1246 self.reactor.process_forever() 1247 1248 1249class Event(object): 1250 "An IRC event." 1251 def __init__(self, type, source, target, arguments=None, tags=None): 1252 """ 1253 Initialize an Event. 1254 1255 Arguments: 1256 1257 type -- A string describing the event. 1258 1259 source -- The originator of the event (a nick mask or a server). 1260 1261 target -- The target of the event (a nick or a channel). 1262 1263 arguments -- Any event-specific arguments. 1264 """ 1265 self.type = type 1266 self.source = source 1267 self.target = target 1268 if arguments is None: 1269 arguments = [] 1270 self.arguments = arguments 1271 if tags is None: 1272 tags = [] 1273 self.tags = tags 1274 1275def is_channel(string): 1276 """Check if a string is a channel name. 1277 1278 Returns true if the argument is a channel name, otherwise false. 1279 """ 1280 return string and string[0] in "#&+!" 1281 1282def ip_numstr_to_quad(num): 1283 """ 1284 Convert an IP number as an integer given in ASCII 1285 representation to an IP address string. 1286 1287 >>> ip_numstr_to_quad('3232235521') 1288 '192.168.0.1' 1289 >>> ip_numstr_to_quad(3232235521) 1290 '192.168.0.1' 1291 """ 1292 n = int(num) 1293 packed = struct.pack('>L', n) 1294 bytes = struct.unpack('BBBB', packed) 1295 return ".".join(map(str, bytes)) 1296 1297def ip_quad_to_numstr(quad): 1298 """ 1299 Convert an IP address string (e.g. '192.168.0.1') to an IP 1300 number as a base-10 integer given in ASCII representation. 1301 1302 >>> ip_quad_to_numstr('192.168.0.1') 1303 '3232235521' 1304 """ 1305 bytes = map(int, quad.split(".")) 1306 packed = struct.pack('BBBB', *bytes) 1307 return str(struct.unpack('>L', packed)[0]) 1308 1309class NickMask(six.text_type): 1310 """ 1311 A nickmask (the source of an Event) 1312 1313 >>> nm = NickMask('pinky!username@example.com') 1314 >>> print(nm.nick) 1315 pinky 1316 1317 >>> print(nm.host) 1318 example.com 1319 1320 >>> print(nm.user) 1321 username 1322 1323 >>> isinstance(nm, six.text_type) 1324 True 1325 1326 >>> nm = 'красный!red@yahoo.ru' 1327 >>> if not six.PY3: nm = nm.decode('utf-8') 1328 >>> nm = NickMask(nm) 1329 1330 >>> isinstance(nm.nick, six.text_type) 1331 True 1332 1333 Some messages omit the userhost. In that case, None is returned. 1334 1335 >>> nm = NickMask('irc.server.net') 1336 >>> print(nm.nick) 1337 irc.server.net 1338 >>> nm.userhost 1339 >>> nm.host 1340 >>> nm.user 1341 """ 1342 @classmethod 1343 def from_params(cls, nick, user, host): 1344 return cls('{nick}!{user}@{host}'.format(**vars())) 1345 1346 @property 1347 def nick(self): 1348 nick, sep, userhost = self.partition("!") 1349 return nick 1350 1351 @property 1352 def userhost(self): 1353 nick, sep, userhost = self.partition("!") 1354 return userhost or None 1355 1356 @property 1357 def host(self): 1358 nick, sep, userhost = self.partition("!") 1359 user, sep, host = userhost.partition('@') 1360 return host or None 1361 1362 @property 1363 def user(self): 1364 nick, sep, userhost = self.partition("!") 1365 user, sep, host = userhost.partition('@') 1366 return user or None 1367 1368 @classmethod 1369 def from_group(cls, group): 1370 return cls(group) if group else None 1371 1372 1373def _ping_ponger(connection, event): 1374 "A global handler for the 'ping' event" 1375 connection.pong(event.target) 1376