├── lib └── geventirc │ ├── __init__.py │ ├── tests │ └── test_ctpc.py │ ├── replycode.py │ ├── handlers.py │ ├── irc.py │ └── message.py ├── setup.py ├── LICENSE.txt └── README.rst /lib/geventirc/__init__.py: -------------------------------------------------------------------------------- 1 | from geventirc.irc import Client 2 | 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup(name="geventirc", 4 | version="0.1dev", 5 | author="Antonin Amand (@gwik)", 6 | author_email="antonin.amand@gmail.com", 7 | description="gevent based irc client", 8 | namespace_packages=['geventirc'], 9 | package_dir = {'':'lib'}, 10 | packages=find_packages('lib'), 11 | zip_safe=False, 12 | install_requires=[ 13 | 'distribute', 14 | 'gevent', 15 | ], 16 | ) 17 | 18 | -------------------------------------------------------------------------------- /lib/geventirc/tests/test_ctpc.py: -------------------------------------------------------------------------------- 1 | from geventirc import message 2 | 3 | 4 | def test_low_level_quoting(): 5 | data = "some mess\r\0age with\nspecial\0charaters" 6 | encoded = message.low_level_quote(data) 7 | assert encoded == 'some mess\x10r\x100age with\x10nspecial\x100charaters' 8 | assert data == message.low_level_dequote(data) 9 | 10 | def test_ctcp_quoting(): 11 | data = "some mess\r\0age with\nspeci:al\0charaters" 12 | encoded = message.low_level_quote(data) 13 | encoded = message.ctcp_quote(encoded) 14 | print repr(encoded) 15 | assert encoded == 'some mess\x10r\x100age with\x10nspeci :al\x100charaters' 16 | 17 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2011 by Antonin Amand 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | geventirc 3 | ========= 4 | 5 | Introduction 6 | ============ 7 | 8 | `geventirc` is a simple irc client library using gevent:: 9 | 10 | from geventirc import Client 11 | 12 | nick = 'geventircbot' 13 | irc = Client('irc.freenode.net', nick, port=6667) 14 | irc.start() 15 | irc.join() 16 | 17 | 18 | Handlers 19 | ======== 20 | 21 | `handlers` react to messages from server, they can be any python 22 | callable:: 23 | 24 | 25 | from gevenirc import Client 26 | from gevenirc import message 27 | from gevenirc import handlers 28 | 29 | def join_and_say_hello_handler(client, msg): 30 | client.send_message(message.Join('#gevent')) 31 | client.msg('#gevent', 'Hello #gevent guys!') 32 | 33 | 34 | class ReplyWhenQuoted(object): 35 | """ Reply when someone quotes me. 36 | """ 37 | 38 | commands = ['PRIVMSG'] 39 | 40 | def __init__(self, reply): 41 | self.reply = reply 42 | 43 | def __call__(self, client, msg): 44 | channel, content = msg.params 45 | if client.nick in content: 46 | client.msg(self.reply) 47 | 48 | 49 | def print_handler(self, client, msg): 50 | """ Print every message from server to standard output. 51 | """ 52 | print msg.encode()[-2] 53 | 54 | nick = 'geventircbot' 55 | nickserv_handler = handlers.NickServHandler(nick, 'somepassword') 56 | 57 | irc = Client('irc.freenode.net', nick, port=6667) 58 | # this handler doesn't provide a commands attribute nor specify at 59 | # registration so it receives everything. 60 | irc.add_handler(print_handler) 61 | # 001 code means that you have successfully connected and can 62 | # join channels. 63 | irc.add_handler(join_and_say_hello_handler, '001') 64 | # nickserv_handler has a commands attribute which tell which 65 | # commands it's reacts to. 66 | irc.add_handler(nickserv_handler) 67 | irc.add_handler(ReplyWhenQuoted("I'm just a bot")) 68 | irc.start() 69 | irc.join() # join means join the current greenlet, not join irc channel 70 | 71 | 72 | Contact & Help 73 | ============== 74 | 75 | - twitter: @gwik 76 | - email: antonin.amand@gmail.com 77 | - Join #gevent on freenode and talk to gwik. 78 | 79 | License 80 | ======= 81 | 82 | MIT, see LICENSE.txt 83 | -------------------------------------------------------------------------------- /lib/geventirc/replycode.py: -------------------------------------------------------------------------------- 1 | ERR_NOSUCHNICK = 401 2 | ERR_NOSUCHSERVER = 402 3 | ERR_NOSUCHCHANNEL = 403 4 | ERR_CANNOTSENDTOCHAN = 404 5 | ERR_TOOMANYCHANNELS = 405 6 | ERR_WASNOSUCHNICK = 406 7 | ERR_TOOMANYTARGETS = 407 8 | ERR_NOORIGIN = 409 9 | ERR_NORECIPIENT = 411 10 | ERR_NOTEXTTOSEND = 412 11 | ERR_NOTOPLEVEL = 413 12 | ERR_WILDTOPLEVEL = 414 13 | ERR_UNKNOWNCOMMAND = 421 14 | ERR_NOMOTD = 422 15 | ERR_NOADMININFO = 423 16 | ERR_FILEERROR = 424 17 | ERR_NONICKNAMEGIVEN = 431 18 | ERR_ERRONEUSNICKNAME = 432 19 | ERR_NICKNAMEINUSE = 433 20 | ERR_NICKCOLLISION = 436 21 | ERR_USERNOTINCHANNEL = 441 22 | ERR_NOTONCHANNEL = 442 23 | ERR_USERONCHANNEL = 443 24 | ERR_NOLOGIN = 444 25 | ERR_SUMMONDISABLED = 445 26 | ERR_USERSDISABLED = 446 27 | ERR_NOTREGISTERED = 451 28 | ERR_NEEDMOREPARAMS = 461 29 | ERR_ALREADYREGISTRED = 462 30 | ERR_NOPERMFORHOST = 463 31 | ERR_PASSWDMISMATCH = 464 32 | ERR_YOUREBANNEDCREEP = 465 33 | ERR_KEYSET = 467 34 | ERR_CHANNELISFULL = 471 35 | ERR_UNKNOWNMODE = 472 36 | ERR_INVITEONLYCHAN = 473 37 | ERR_BANNEDFROMCHAN = 474 38 | ERR_BADCHANNELKEY = 475 39 | ERR_NOPRIVILEGES = 481 40 | ERR_CHANOPRIVSNEEDED = 482 41 | ERR_CANTKILLSERVER = 483 42 | ERR_NOOPERHOST = 491 43 | ERR_UMODEUNKNOWNFLAG = 501 44 | ERR_USERSDONTMATCH = 502 45 | 46 | RPL_NONE = 300 47 | RPL_AWAY = 301 48 | RPL_USERHOST = 302 49 | RPL_ISON = 303 50 | RPL_UNAWAY = 305 51 | RPL_NOWAWAY = 306 52 | RPL_WHOISUSER = 311 53 | RPL_WHOISSERVER = 312 54 | RPL_WHOISOPERATOR = 313 55 | RPL_WHOISIDLE = 317 56 | RPL_ENDOFWHOIS = 318 57 | RPL_WHOISCHANNELS = 319 58 | RPL_WHOWASUSER = 314 59 | RPL_ENDOFWHOWAS = 369 60 | RPL_LISTSTART = 321 61 | RPL_LIST = 322 62 | RPL_LISTEND = 323 63 | RPL_CHANNELMODEIS = 324 64 | RPL_NOTOPIC = 331 65 | RPL_TOPIC = 332 66 | RPL_INVITING = 341 67 | RPL_SUMMONING = 342 68 | RPL_VERSION = 351 69 | RPL_WHOREPLY = 352 70 | RPL_ENDOFWHO = 315 71 | RPL_NAMREPLY = 353 72 | RPL_ENDOFNAMES = 366 73 | RPL_LINKS = 364 74 | RPL_ENDOFLINKS = 365 75 | RPL_BANLIST = 367 76 | RPL_ENDOFBANLIST = 368 77 | RPL_INFO = 371 78 | RPL_ENDOFINFO = 374 79 | RPL_MOTDSTART = 375 80 | RPL_MOTD = 372 81 | RPL_ENDOFMOTD = 376 82 | RPL_YOUREOPER = 381 83 | RPL_REHASHING = 382 84 | RPL_TIME = 391 85 | RPL_USERSSTART = 392 86 | RPL_USERS = 393 87 | RPL_ENDOFUSERS = 394 88 | RPL_NOUSERS = 395 89 | RPL_TRACELINK = 200 90 | RPL_TRACECONNECTING = 201 91 | RPL_TRACEHANDSHAKE = 202 92 | RPL_TRACEUNKNOWN = 203 93 | RPL_TRACEOPERATOR = 204 94 | RPL_TRACEUSER = 205 95 | RPL_TRACESERVER = 206 96 | RPL_TRACENEWTYPE = 208 97 | RPL_TRACELOG = 261 98 | RPL_STATSLINKINFO = 211 99 | RPL_STATSCOMMANDS = 212 100 | RPL_STATSCLINE = 213 101 | RPL_STATSNLINE = 214 102 | RPL_STATSILINE = 215 103 | RPL_STATSKLINE = 216 104 | RPL_STATSYLINE = 218 105 | RPL_ENDOFSTATS = 219 106 | RPL_STATSLLINE = 241 107 | RPL_STATSUPTIME = 242 108 | RPL_STATSOLINE = 243 109 | RPL_STATSHLINE = 244 110 | RPL_UMODEIS = 221 111 | RPL_LUSERCLIENT = 251 112 | RPL_LUSEROP = 252 113 | RPL_LUSERUNKNOWN = 253 114 | RPL_LUSERCHANNELS = 254 115 | RPL_LUSERME = 255 116 | RPL_ADMINME = 256 117 | RPL_ADMINLOC1 = 257 118 | RPL_ADMINLOC2 = 258 119 | RPL_ADMINEMAIL = 259 120 | 121 | -------------------------------------------------------------------------------- /lib/geventirc/handlers.py: -------------------------------------------------------------------------------- 1 | import gevent 2 | from geventirc import message 3 | from geventirc import replycode 4 | 5 | 6 | def ping_handler(client, msg): 7 | client.send_message(message.Pong()) 8 | 9 | def print_handler(client, msg): 10 | print msg.encode()[:-2] 11 | 12 | 13 | class JoinHandler(object): 14 | 15 | commands = ['001'] 16 | 17 | def __init__(self, channel): 18 | self.channel = channel 19 | 20 | def __call__(self, client, msg): 21 | client.send_message(message.Join(self.channel)) 22 | 23 | 24 | def nick_in_user_handler(self, client, msg): 25 | client.nick = msg.params[1] + '_' 26 | client.send_message(message.Nick(client.nick)) 27 | 28 | 29 | class NickServHandler(object): 30 | 31 | commands = ['001', 32 | replycode.ERR_NICKNAMEINUSE, 33 | replycode.ERR_NICKCOLLISION] 34 | 35 | def __init__(self, nick, password): 36 | self.nick = nick 37 | self.current_nick = None 38 | self.password = password 39 | 40 | def __call__(self, client, msg): 41 | if msg.command == str(replycode.ERR_NICKNAMEINUSE) or \ 42 | msg.command == str(replycode.ERR_NICKCOLLISION): 43 | nick = msg.params[1] 44 | self.current_nick = nick + '_' 45 | client.send_message(message.Nick(self.current_nick)) 46 | return 47 | if msg.command == '001': 48 | client.send_message(message.Nick(self.nick)) 49 | self.current_nick = self.nick 50 | msg = message.PrivMsg('nickserv', 'identify ' + self.password) 51 | client.send_message(msg) 52 | 53 | 54 | class ReplyWhenQuoted(object): 55 | 56 | commands = ['PRIVMSG'] 57 | 58 | def __init__(self, reply): 59 | self.reply = reply 60 | 61 | def __call__(self, client, msg): 62 | channel, content = msg.params[0], " ".join(msg.params[1:]) 63 | if client.nick in content: 64 | # check if this is a direct message 65 | if channel != client.nick: 66 | client.msg(channel, self.reply) 67 | 68 | 69 | class ReplyToDirectMessage(object): 70 | 71 | commands = ['PRIVMSG'] 72 | 73 | def __init__(self, reply): 74 | self.reply = reply 75 | 76 | def __call__(self, client, msg): 77 | channel = msg.params[0] 78 | if client.nick == channel: 79 | nick, user_agent, host = msg.prefix_parts 80 | if nick is not None: 81 | client.msg(nick, self.reply) 82 | 83 | 84 | class PeriodicMessage(object): 85 | """ Send a message every interval or `wait` 86 | 87 | !!! gevent 1.0 only !!! 88 | """ 89 | 90 | commands = ['001'] 91 | 92 | def __init__(self, channel, msg='hello', wait=1.0): 93 | self.channel = channel 94 | self.msg = 'hello' 95 | self.wait = wait 96 | 97 | def start(self, client, msg): 98 | self.client = client 99 | self._schedule() 100 | 101 | def _schedule(self): 102 | timer = gevent.get_hub().loop.timer(self.wait) 103 | timer.start(self.__call__) 104 | 105 | def run(self): 106 | self.client.msg(self.channel, self.msg) 107 | 108 | def __call__(self): 109 | gevent.spawn(self.run) 110 | self._schedule() 111 | 112 | 113 | -------------------------------------------------------------------------------- /lib/geventirc/irc.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import logging 4 | 5 | import gevent.queue 6 | import gevent.pool 7 | from gevent import socket 8 | 9 | from geventirc import message 10 | from geventirc import replycode 11 | from geventirc import handlers 12 | 13 | IRC_PORT = 194 14 | IRCS_PORT = 994 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | 19 | class Client(object): 20 | 21 | def __init__(self, hostname, nick, port=IRC_PORT, 22 | local_hostname=None, server_name=None, real_name=None): 23 | self.hostname = hostname 24 | self.port = port 25 | self.nick = nick 26 | self._socket = None 27 | self.real_name = real_name or nick 28 | self.local_hostname = local_hostname or socket.gethostname() 29 | self.server_name = server_name or 'gevent-irc' 30 | self._recv_queue = gevent.queue.Queue() 31 | self._send_queue = gevent.queue.Queue() 32 | self._group = gevent.pool.Group() 33 | self._handlers = {} 34 | self._global_handlers = set([]) 35 | 36 | def add_handler(self, to_call, *commands): 37 | if not commands: 38 | if hasattr(to_call, 'commands'): 39 | commands = to_call.commands 40 | else: 41 | self._global_handlers.add(to_call) 42 | return 43 | 44 | for command in commands: 45 | command = str(command).upper() 46 | if self._handlers.has_key(command): 47 | self._handlers[command].add(to_call) 48 | continue 49 | self._handlers[command] = set([to_call]) 50 | 51 | def _handle(self, msg): 52 | handlers = self._global_handlers | self._handlers.get(msg.command, set()) 53 | if handlers is not None: 54 | for handler in handlers: 55 | self._group.spawn(handler, self, msg) 56 | 57 | def send_message(self, msg): 58 | self._send_queue.put(msg.encode()) 59 | 60 | def start(self): 61 | address = None 62 | try: 63 | address = (socket.gethostbyname(self.hostname), self.port) 64 | except socket.gaierror: 65 | logger.error('hostname not found') 66 | raise 67 | 68 | logger.info('connecting to %r...' % (address,)) 69 | self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 70 | self._socket.connect(address) 71 | self._group.spawn(self._send_loop) 72 | self._group.spawn(self._process_loop) 73 | self._group.spawn(self._recv_loop) 74 | # give control back to the hub 75 | gevent.sleep(0) 76 | 77 | def _recv_loop(self): 78 | buf = '' 79 | while True: 80 | data = self._socket.recv(512) 81 | buf += data 82 | pos = buf.find("\r\n") 83 | while pos >= 0: 84 | line = buf[0:pos] 85 | self._recv_queue.put(line) 86 | buf = buf[pos + 2:] 87 | pos = buf.find("\r\n") 88 | 89 | def _send_loop(self): 90 | while True: 91 | command = self._send_queue.get() 92 | self._socket.sendall(command.encode()) 93 | 94 | def _process_loop(self): 95 | self.send_message(message.Nick(self.nick)) 96 | self.send_message( 97 | message.User( 98 | self.nick, 99 | self.local_hostname, 100 | self.server_name, 101 | self.real_name)) 102 | while True: 103 | data = self._recv_queue.get() 104 | msg = message.CTCPMessage.decode(data) 105 | self._handle(msg) 106 | 107 | def stop(self): 108 | self._group.kill() 109 | if self._socket is not None: 110 | self._socket.close() 111 | self._socket = None 112 | 113 | def join(self): 114 | self._group.join() 115 | 116 | def msg(self, to, content): 117 | self.send_message(message.PrivMsg(to, content)) 118 | 119 | def quit(self, msg=None): 120 | self.send_message(message.Quit(msg)) 121 | self.stop() 122 | 123 | 124 | if __name__ == '__main__': 125 | 126 | class MeHandler(object): 127 | commands = ['PRIVMSG'] 128 | 129 | def __call__(self, client, msg): 130 | if client.nick == msg.params[0]: 131 | nick, _, _ = msg.prefix_parts 132 | client.send_message( 133 | message.Me(nick, "do nothing it's just a bot")) 134 | 135 | nick = 'geventbot' 136 | client = Client('irc.freenode.net', nick, port=6667) 137 | client.add_handler(handlers.ping_handler, 'PING') 138 | client.add_handler(handlers.JoinHandler('#flood!')) 139 | # client.add_handler(hello.start, '001') 140 | client.add_handler(handlers.ReplyWhenQuoted("I'm just a bot")) 141 | client.add_handler(handlers.print_handler) 142 | client.add_handler(handlers.nick_in_user_handler, replycode.ERR_NICKNAMEINUSE) 143 | client.add_handler(handlers.ReplyToDirectMessage("I'm just a bot")) 144 | client.add_handler(MeHandler()) 145 | client.start() 146 | client.join() 147 | 148 | 149 | -------------------------------------------------------------------------------- /lib/geventirc/message.py: -------------------------------------------------------------------------------- 1 | DELIM = chr(040) 2 | INVALID_CHARS = ["\r", "\n", "\0"] 3 | CR = "\r" 4 | NL = "\n" 5 | NUL = chr(0) 6 | 7 | 8 | class ProtocolViolationError(StandardError): 9 | pass 10 | 11 | def is_valid_param(param): 12 | for invalid in INVALID_CHARS: 13 | if invalid in param: 14 | return False 15 | return True 16 | 17 | def irc_split(data): 18 | prefix = '' 19 | buf = data 20 | trailing = None 21 | command = None 22 | 23 | if buf.startswith(':'): 24 | try: 25 | prefix, buf = buf[1:].split(DELIM, 1) 26 | except ValueError: 27 | pass 28 | try: 29 | command, buf = buf.split(DELIM, 1) 30 | except ValueError: 31 | raise ProtocolViolationError('no command received: %r' % buf) 32 | try: 33 | buf, trailing = buf.split(DELIM + ':', 1) 34 | except ValueError: 35 | pass 36 | params = buf.split(DELIM) 37 | if trailing is not None: 38 | params.append(trailing) 39 | return prefix, command, params 40 | 41 | def irc_unsplit(prefix, command, params): 42 | buf = '' 43 | if prefix is not None: 44 | buf += prefix + DELIM 45 | buf += command + DELIM 46 | if params is None: 47 | pass 48 | elif isinstance(params, basestring): 49 | assert not params.startswith(':'), 'params must not start with :' 50 | buf += ":" + params 51 | else: 52 | if params: 53 | rparams, trailing = params[:-1], params[-1] 54 | if rparams: 55 | buf += DELIM.join(rparams) + DELIM 56 | if trailing: 57 | buf += ":" + trailing 58 | return buf 59 | 60 | 61 | class Message(object): 62 | 63 | @classmethod 64 | def decode(cls, data): 65 | prefix, command, params = irc_split(data) 66 | return cls(command, params, prefix=prefix) 67 | 68 | def __init__(self, command, params, prefix=None): 69 | assert command, 'command is mandatory' 70 | self.prefix = prefix 71 | self.command = command 72 | self.params = params 73 | 74 | @property 75 | def prefix_parts(self): 76 | """ return tuple(, , ) 77 | """ 78 | server_name = None 79 | user = None 80 | host = None 81 | pos = self.prefix.find('!') 82 | if pos >= 0: 83 | server_name, userhost = self.prefix[:pos], self.prefix[pos+1:] 84 | pos = self.prefix.find('@') 85 | if pos >= 0: 86 | user, host = userhost[:pos], userhost[pos+1:] 87 | else: 88 | host = userhost 89 | else: 90 | server_name = self.prefix 91 | return server_name, user, host 92 | 93 | def encode(self): 94 | return irc_unsplit(self.prefix, self.command, self.params) + "\r\n" 95 | 96 | 97 | class Command(Message): 98 | 99 | def __init__(self, params, command=None, prefix=None): 100 | if command is None: 101 | command = self.__class__.__name__.upper() 102 | super(Command, self).__init__(command, params, prefix=prefix) 103 | 104 | 105 | class Nick(Command): 106 | 107 | def __init__(self, nickname, hopcount=None, prefix=None): 108 | params = [nickname] 109 | if hopcount is not None: 110 | assert isinstance(hopcount, int), 'hopcount should be int if not none' 111 | params.append(str(hopcount)) 112 | super(Nick, self).__init__(params, prefix=prefix) 113 | 114 | 115 | class User(Command): 116 | 117 | def __init__(self, username, hostname, servername, realname, prefix=None): 118 | params = [username, hostname, servername, realname] 119 | super(User, self).__init__(params, prefix=prefix) 120 | 121 | 122 | class Quit(Command): 123 | 124 | def __init__(self, msg, prefix=None): 125 | params = None 126 | if msg is not None: 127 | params = msg 128 | super(Quit, self).__init__(params, prefix=prefix) 129 | 130 | 131 | class Join(Command): 132 | 133 | def __init__(self, channels, prefix=None): 134 | params = [] 135 | if isinstance(channels, basestring): 136 | if channels.startswith('#'): 137 | params = channels 138 | else: 139 | params = "#" + channels 140 | else: 141 | chans = [] 142 | keys = [] 143 | for channel, key in channels: 144 | if key is None: 145 | chans.append('#' + channel) 146 | else: 147 | chans.append('&' + channel) 148 | keys.append(key) 149 | params = [",".join(chans), ",".join(keys)] 150 | 151 | assert params, 'invalid channel ' + channels 152 | super(Join, self).__init__(params, prefix=prefix) 153 | 154 | 155 | class PrivMsg(Command): 156 | 157 | def __init__(self, to, msg, prefix=None): 158 | super(PrivMsg, self).__init__([to, msg], prefix=prefix) 159 | 160 | 161 | class Pong(Command): 162 | def __init__(self, prefix=None): 163 | super(Pong, self).__init__(None, prefix=prefix) 164 | 165 | 166 | X_DELIM = chr(001) 167 | X_QUOTE = chr(134) 168 | M_QUOTE = chr(020) 169 | 170 | _low_level_quote_table = { 171 | NUL: M_QUOTE + '0', 172 | NL: M_QUOTE + 'n', 173 | CR: M_QUOTE + 'r', 174 | M_QUOTE: M_QUOTE * 2 175 | } 176 | 177 | _low_level_dequote_table = {} 178 | for k, v in _low_level_quote_table.items(): 179 | _low_level_dequote_table[v] = k 180 | 181 | _ctcp_quote_table = { 182 | X_DELIM: X_QUOTE + 'a', 183 | X_QUOTE: X_QUOTE * 2 184 | } 185 | 186 | _ctcp_dequote_table = {} 187 | for k, v in _ctcp_quote_table.items(): 188 | _ctcp_dequote_table[v] = k 189 | 190 | def _quote(string, table): 191 | cursor = 0 192 | buf = '' 193 | for pos, char in enumerate(string): 194 | if pos is 0: 195 | continue 196 | if char in table: 197 | buf += string[cursor:pos] + table[char] 198 | cursor = pos + 1 199 | buf += string[cursor:] 200 | return buf 201 | 202 | def _dequote(string, table): 203 | cursor = 0 204 | buf = '' 205 | last_char = '' 206 | for pos, char in enumerate(string): 207 | if pos is 0: 208 | last_char = char 209 | continue 210 | if last_char + char in table: 211 | buf += string[cursor:pos] + table[char] 212 | cursor = pos + 1 213 | last_char = char 214 | 215 | buf += string[cursor:] 216 | return buf 217 | 218 | def low_level_quote(string): 219 | return _quote(string, _low_level_quote_table) 220 | 221 | def low_level_dequote(string): 222 | return _dequote(string, _low_level_dequote_table) 223 | 224 | def ctcp_quote(string): 225 | return _quote(string, _ctcp_quote_table) 226 | 227 | def ctcp_dequote(string): 228 | return _dequote(string, _ctcp_dequote_table) 229 | 230 | 231 | class CTCPMessage(Message): 232 | 233 | def __init__(self, command, params, ctcp_params, prefix=None): 234 | super(CTCPMessage, self).__init__(command, params, prefix=prefix) 235 | self.ctcp_params = ctcp_params 236 | 237 | @classmethod 238 | def decode(cls, data): 239 | prefix, command, params = irc_split(data) 240 | extended_messages = [] 241 | normal_messages = [] 242 | if params: 243 | params = DELIM.join(params) 244 | decoded = low_level_dequote(params) 245 | messages = decoded.split(X_DELIM) 246 | messages.reverse() 247 | 248 | odd = False 249 | extended_messages = [] 250 | normal_messages = [] 251 | 252 | while messages: 253 | message = messages.pop() 254 | if odd: 255 | if message: 256 | ctcp_decoded = ctcp_dequote(message) 257 | split = ctcp_decoded.split(DELIM, 1) 258 | tag = split[0] 259 | data = None 260 | if len(split) > 1: 261 | data = split[1] 262 | extended_messages.append((tag, data)) 263 | else: 264 | if message: 265 | normal_messages += filter(None, message.split(DELIM)) 266 | odd = not odd 267 | 268 | return cls(command, normal_messages, extended_messages, prefix=prefix) 269 | 270 | def encode(self): 271 | ctcp_buf = '' 272 | for tag, data in self.ctcp_params: 273 | if data: 274 | if not isinstance(data, basestring): 275 | data = DELIM.join(map(str, data)) 276 | m = tag + DELIM + data 277 | else: 278 | m = str(tag) 279 | ctcp_buf += X_DELIM + ctcp_quote(m) + X_DELIM 280 | 281 | return irc_unsplit( 282 | self.prefix, self.command, self.params + 283 | [low_level_quote(ctcp_buf)]) + "\r\n" 284 | 285 | 286 | class Me(CTCPMessage): 287 | 288 | def __init__(self, to, action, prefix=None): 289 | super(Me, self).__init__( 290 | 'PRIVMSG', [to], [('ACTION', action)], prefix=prefix) 291 | --------------------------------------------------------------------------------