From 5a511944bd408db6b87339b0a18ab4563aaeb3ed Mon Sep 17 00:00:00 2001 From: dpedu Date: Sat, 8 Aug 2015 15:04:45 -0700 Subject: [PATCH] Add eval/exec to RPC --- .gitignore | 1 + docs/rpc/_rpc.rst | 67 +++- pyircbot/irccore.py | 723 ++++++++++++++++++++++---------------------- pyircbot/rpc.py | 324 ++++++++++---------- 4 files changed, 605 insertions(+), 510 deletions(-) diff --git a/.gitignore b/.gitignore index 1cc53aa..1b5576d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ build pyircbot.egg-info dev docs/builder/build.sh +examples/config.test.json diff --git a/docs/rpc/_rpc.rst b/docs/rpc/_rpc.rst index 6adaec6..d4414cc 100644 --- a/docs/rpc/_rpc.rst +++ b/docs/rpc/_rpc.rst @@ -47,7 +47,7 @@ We can retrieve an arbitrary property from a module: [True, {'apikey': 'deadbeefcafe', 'defaultUnit': 'f'}] >>> -Or run a method in a module, passing args: +Run a method in a module, passing args: .. code-block:: python @@ -55,6 +55,71 @@ Or run a method in a module, passing args: [True, {'definition': "Loyal, unlike its predecessor", 'word': 'rhobot2', 'by': 'xMopxShell'}] >>> +Or simply pass a string to eval() or exec() to do anything. In this case, +retrieving a full stack trace of the bot, which is useful during module +development: + +.. code-block:: python + + >>> print( rpc.eval("self.bot.irc.trace()")[1] ) + + *** STACKTRACE - START *** + + # ThreadID: 140289192748800 + File: "/usr/lib/python3.4/threading.py", line 888, in _bootstrap + self._bootstrap_inner() + File: "/usr/lib/python3.4/threading.py", line 920, in _bootstrap_inner + self.run() + File: "/usr/lib/python3.4/threading.py", line 1184, in run + self.finished.wait(self.interval) + File: "/usr/lib/python3.4/threading.py", line 552, in wait + signaled = self._cond.wait(timeout) + File: "/usr/lib/python3.4/threading.py", line 293, in wait + gotit = waiter.acquire(True, timeout) + + # ThreadID: 140289297204992 + File: "/usr/lib/python3.4/threading.py", line 888, in _bootstrap + self._bootstrap_inner() + File: "/usr/lib/python3.4/threading.py", line 920, in _bootstrap_inner + self.run() + File: "/usr/local/lib/python3.4/dist-packages/pyircbot-4.0.0_r02-py3.4.egg/pyircbot/rpc.py", line 51, in run + self.server.serve() + File: "/usr/local/lib/python3.4/dist-packages/pyircbot-4.0.0_r02-py3.4.egg/pyircbot/jsonrpc.py", line 1110, in serve + self.__transport.serve( self.handle, n ) + File: "/usr/local/lib/python3.4/dist-packages/pyircbot-4.0.0_r02-py3.4.egg/pyircbot/jsonrpc.py", line 851, in serve + result = handler(data) + File: "/usr/local/lib/python3.4/dist-packages/pyircbot-4.0.0_r02-py3.4.egg/pyircbot/jsonrpc.py", line 1086, in handle + result = self.funcs[method]( *params ) + File: "/usr/local/lib/python3.4/dist-packages/pyircbot-4.0.0_r02-py3.4.egg/pyircbot/rpc.py", line 167, in eval + return (True, eval(code)) + File: "", line 1, in + File: "/usr/local/lib/python3.4/dist-packages/pyircbot-4.0.0_r02-py3.4.egg/pyircbot/irccore.py", line 288, in trace + for filename, lineno, name, line in traceback.extract_stack(stack): + + # ThreadID: 140289333405504 + File: "/usr/local/bin/pyircbot", line 5, in + pkg_resources.run_script('pyircbot==4.0.0-r02', 'pyircbot') + File: "/usr/lib/python3/dist-packages/pkg_resources.py", line 528, in run_script + self.require(requires)[0].run_script(script_name, ns) + File: "/usr/lib/python3/dist-packages/pkg_resources.py", line 1394, in run_script + execfile(script_filename, namespace, namespace) + File: "/usr/lib/python3/dist-packages/pkg_resources.py", line 55, in execfile + exec(compile(open(fn).read(), fn, 'exec'), globs, locs) + File: "/usr/local/lib/python3.4/dist-packages/pyircbot-4.0.0_r02-py3.4.egg/EGG-INFO/scripts/pyircbot", line 32, in + bot.loop() + File: "/usr/local/lib/python3.4/dist-packages/pyircbot-4.0.0_r02-py3.4.egg/pyircbot/pyircbot.py", line 68, in loop + self.irc.loop() + File: "/usr/local/lib/python3.4/dist-packages/pyircbot-4.0.0_r02-py3.4.egg/pyircbot/irccore.py", line 56, in loop + asyncore.loop(map=self.asynmap) + File: "/usr/lib/python3.4/asyncore.py", line 208, in loop + poll_fun(timeout, map) + File: "/usr/lib/python3.4/asyncore.py", line 145, in poll + r, w, e = select.select(r, w, e, timeout) + + *** STACKTRACE - END *** + + >>> + Careful, you can probably crash the bot by tweaking the wrong things. Only basic types can be passed over the RPC connection. Trying to access anything extra results in an error: diff --git a/pyircbot/irccore.py b/pyircbot/irccore.py index 8c6f148..c69ab9d 100644 --- a/pyircbot/irccore.py +++ b/pyircbot/irccore.py @@ -11,364 +11,377 @@ import asynchat import asyncore import logging import traceback +import sys from socket import SHUT_RDWR try: - from cStringIO import StringIO + from cStringIO import StringIO except: - from io import BytesIO as StringIO + from io import BytesIO as StringIO class IRCCore(asynchat.async_chat): - - def __init__(self): - asynchat.async_chat.__init__(self) - - self.connected=False - """If we're connected or not""" - - self.log = logging.getLogger('IRCCore') - """Reference to logger object""" - - self.buffer = StringIO() - """cStringIO used as a buffer""" - - self.alive = True - """True if we should try to stay connected""" - - self.server = None - """Server address""" - self.port = 0 - """Server port""" - self.ipv6 = False - """Use IPv6?""" - - # IRC Messages are terminated with \r\n - self.set_terminator(b"\r\n") - - # Set up hooks for modules - self.initHooks() - - # Map for asynchat - self.asynmap = {} - - def loop(self): - asyncore.loop(map=self.asynmap) - - def kill(self): - """TODO close the socket""" - self.act_QUIT("Help! Another thread is killing me :(") - - " Net related code here on down " - - def getBuf(self): - """Return the network buffer and clear it""" - self.buffer.seek(0) - data = self.buffer.read() - self.buffer = StringIO() - return data - - def collect_incoming_data(self, data): - """Recieve data from the IRC server, append it to the buffer - - :param data: the data that was recieved - :type data: str""" - #self.log.debug("<< %(message)s", {"message":repr(data)}) - self.buffer.write(data) - - def found_terminator(self): - """A complete command was pushed through, so clear the buffer and process it.""" - line = None - buf = self.getBuf() - try: - line = buf.decode("UTF-8") - except UnicodeDecodeError as ude: - self.log.error("found_terminator(): could not decode input as UTF-8") - self.log.error("found_terminator(): data: %s" % line) - self.log.error("found_terminator(): repr(data): %s" % repr(line)) - self.log.error("found_terminator(): error: %s" % str(ude)) - return - self.process_data(line) - - def handle_close(self): - """Called when the socket is disconnected. Triggers the _DISCONNECT hook""" - self.log.debug("handle_close") - self.connected=False - self.close() - self.fire_hook("_DISCONNECT") - - def handle_error(self, *args, **kwargs): - """Called on fatal network errors.""" - self.log.error("Connection failed (handle_error)") - self.log.error(str(args)) - self.log.error(str(kwargs)) - self.log(IRCCore.trace()); - - def _connect(self): - """Connect to IRC""" - self.log.debug("Connecting to %(server)s:%(port)i", {"server":self.server, "port":self.port}) - socket_type = socket.AF_INET - if self.ipv6: - self.log.info("IPv6 is enabled.") - socket_type = socket.AF_INET6 - socketInfo = socket.getaddrinfo(self.server, self.port, socket_type) - self.create_socket(socket_type, socket.SOCK_STREAM) - - self.connect(socketInfo[0][4]) - self.asynmap[self._fileno] = self # http://willpython.blogspot.com/2010/08/multiple-event-loops-with-asyncore-and.html - - def handle_connect(self): - """When asynchat indicates our socket is connected, fire the _CONNECT hook""" - self.connected=True - self.log.debug("handle_connect: connected") - self.fire_hook("_CONNECT") - self.log.debug("handle_connect: complete") - - def sendRaw(self, text): - """Send a raw string to the IRC server - - :param text: the string to send - :type text: str""" - if self.connected: - #self.log.debug(">> "+text) - self.send( (text+"\r\n").encode("UTF-8").decode().encode("UTF-8")) - else: - self.log.warning("Send attempted while disconnected. >> "+text) - - def process_data(self, data): - """Process one line of tet irc sent us - - :param data: the data to process - :type data: str""" - if data.strip() == "": - return - - prefix = None - command = None - args=[] - trailing=None - - if data[0]==":": - prefix=data.split(" ")[0][1:] - data=data[data.find(" ")+1:] - command = data.split(" ")[0] - data=data[data.find(" ")+1:] - if(data[0]==":"): - # no args - trailing = data[1:].strip() - else: - trailing = data[data.find(" :")+2:].strip() - data = data[:data.find(" :")] - args = data.split(" ") - for index,arg in enumerate(args): - args[index]=arg.strip() - if not command in self.hookcalls: - self.log.warning("Unknown command: cmd='%s' prefix='%s' args='%s' trailing='%s'" % (command, prefix, args, trailing)) - else: - self.fire_hook(command, args=args, prefix=prefix, trailing=trailing) - - - " Module related code " - def initHooks(self): - """Defines hooks that modules can listen for events of""" - self.hooks = [ - '_CONNECT', # Called when the bot connects to IRC on the socket level - '_DISCONNECT', # Called when the irc socket is forcibly closed - 'NOTICE', # :irc.129irc.com NOTICE AUTH :*** Looking up your hostname... - 'MODE', # :CloneABCD MODE CloneABCD :+iwx - 'PING', # PING :irc.129irc.com - 'JOIN', # :CloneA!dave@hidden-B4F6B1AA.rit.edu JOIN :#clonea - 'QUIT', # :HCSMPBot!~HCSMPBot@108.170.48.18 QUIT :Quit: Disconnecting! - 'NICK', # :foxiAway!foxi@irc.hcsmp.com NICK :foxi - 'PART', # :CloneA!dave@hidden-B4F6B1AA.rit.edu PART #clonea - 'PRIVMSG', # :CloneA!dave@hidden-B4F6B1AA.rit.edu PRIVMSG #clonea :aaa - 'KICK', # :xMopxShell!~rduser@host KICK #xMopx2 xBotxShellTest :xBotxShellTest - 'INVITE', # :gmx!~gmxgeek@irc.hcsmp.com INVITE Tyrone :#hcsmp' - '001', # :irc.129irc.com 001 CloneABCD :Welcome to the 129irc IRC Network CloneABCD!CloneABCD@djptwc-laptop1.rit.edu - '002', # :irc.129irc.com 002 CloneABCD :Your host is irc.129irc.com, running version Unreal3.2.8.1 - '003', # :irc.129irc.com 003 CloneABCD :This server was created Mon Jul 19 2010 at 03:12:01 EDT - '004', # :irc.129irc.com 004 CloneABCD irc.129irc.com Unreal3.2.8.1 iowghraAsORTVSxNCWqBzvdHtGp lvhopsmntikrRcaqOALQbSeIKVfMCuzNTGj - '005', # :irc.129irc.com 005 CloneABCD CMDS=KNOCK,MAP,DCCALLOW,USERIP UHNAMES NAMESX SAFELIST HCN MAXCHANNELS=10 CHANLIMIT=#:10 MAXLIST=b:60,e:60,I:60 NICKLEN=30 CHANNELLEN=32 TOPICLEN=307 KICKLEN=307 AWAYLEN=307 :are supported by this server - '250', # :chaos.esper.net 250 xBotxShellTest :Highest connection count: 1633 (1632 clients) (186588 connections received) - '251', # :irc.129irc.com 251 CloneABCD :There are 1 users and 48 invisible on 2 servers - '252', # :irc.129irc.com 252 CloneABCD 9 :operator(s) online - '254', # :irc.129irc.com 254 CloneABCD 6 :channels formed - '255', # :irc.129irc.com 255 CloneABCD :I have 42 clients and 1 servers - '265', # :irc.129irc.com 265 CloneABCD :Current Local Users: 42 Max: 47 - '266', # :irc.129irc.com 266 CloneABCD :Current Global Users: 49 Max: 53 - '332', # :chaos.esper.net 332 xBotxShellTest #xMopx2 :/ #XMOPX2 / https://code.google.com/p/pyircbot/ (Channel Topic) - '333', # :chaos.esper.net 333 xBotxShellTest #xMopx2 xMopxShell!~rduser@108.170.60.242 1344370109 - '353', # :irc.129irc.com 353 CloneABCD = #clonea :CloneABCD CloneABC - '366', # :irc.129irc.com 366 CloneABCD #clonea :End of /NAMES list. - '372', # :chaos.esper.net 372 xBotxShell :motd text here - '375', # :chaos.esper.net 375 xBotxShellTest :- chaos.esper.net Message of the Day - - '376', # :chaos.esper.net 376 xBotxShell :End of /MOTD command. - '422', # :irc.129irc.com 422 CloneABCD :MOTD File is missing - '433', # :nova.esper.net 433 * pyircbot3 :Nickname is already in use. - ] - " mapping of hooks to methods " - self.hookcalls = {} - for command in self.hooks: - self.hookcalls[command]=[] - - def fire_hook(self, command, args=None, prefix=None, trailing=None): - """Run any listeners for a specific hook - - :param command: the hook to fire - :type command: str - :param args: the list of arguments, if any, the command was passed - :type args: list - :param prefix: prefix of the sender of this command - :type prefix: str - :param trailing: data payload of the command - :type trailing: str""" - - for hook in self.hookcalls[command]: - try: - hook(args, prefix, trailing) - except: - self.log.warning("Error processing hook: \n%s"% self.trace()) - - def addHook(self, command, method): - """**Internal.** Enable (connect) a single hook of a module - - :param command: command this hook will trigger on - :type command: str - :param method: callable method object to hook in - :type method: object""" - " add a single hook " - if command in self.hooks: - self.hookcalls[command].append(method) - else: - self.log.warning("Invalid hook - %s" % command) - return False - - def removeHook(self, command, method): - """**Internal.** Disable (disconnect) a single hook of a module - - :param command: command this hook triggers on - :type command: str - :param method: callable method that should be removed - :type method: object""" - " remove a single hook " - if command in self.hooks: - for hookedMethod in self.hookcalls[command]: - if hookedMethod == method: - self.hookcalls[command].remove(hookedMethod) - else: - self.log.warning("Invalid hook - %s" % command) - return False - - " Utility methods " - @staticmethod - def decodePrefix(prefix): - """Given a prefix like nick!username@hostname, return an object with these properties - - :param prefix: the prefix to disassemble - :type prefix: str - :returns: object -- an UserPrefix object with the properties `nick`, `username`, `hostname` or a ServerPrefix object with the property `hostname`""" - if "!" in prefix: - ob = type('UserPrefix', (object,), {}) - ob.nick, prefix = prefix.split("!") - ob.username, ob.hostname = prefix.split("@") - return ob - else: - ob = type('ServerPrefix', (object,), {}) - ob.hostname = prefix - return ob - - @staticmethod - def trace(): - """Return the stack trace of the bot as a string""" - return traceback.format_exc() - - " Data Methods " - def get_nick(self): - """Get the bot's current nick - - :returns: str - the bot's current nickname""" - return self.nick - - " Action Methods " - def act_PONG(self, data): - """Use the `/pong` command - respond to server pings - - :param data: the string or number the server sent with it's ping - :type data: str""" - self.sendRaw("PONG :%s" % data) - - def act_USER(self, username, hostname, realname): - """Use the USER protocol command. Used during connection - - :param username: the bot's username - :type username: str - :param hostname: the bot's hostname - :type hostname: str - :param realname: the bot's realname - :type realname: str""" - self.sendRaw("USER %s %s %s :%s" % (username, hostname, self.server, realname)) - - def act_NICK(self, newNick): - """Use the `/nick` command - - :param newNick: new nick for the bot - :type newNick: str""" - self.nick = newNick - self.sendRaw("NICK %s" % newNick) - - def act_JOIN(self, channel): - """Use the `/join` command - - :param channel: the channel to attempt to join - :type channel: str""" - self.sendRaw("JOIN %s"%channel) - - def act_PRIVMSG(self, towho, message): - """Use the `/msg` command - - :param towho: the target #channel or user's name - :type towho: str - :param message: the message to send - :type message: str""" - self.sendRaw("PRIVMSG %s :%s"%(towho,message)) - - def act_MODE(self, channel, mode, extra=None): - """Use the `/mode` command - - :param channel: the channel this mode is for - :type channel: str - :param mode: the mode string. Example: +b - :type mode: str - :param extra: additional argument if the mode needs it. Example: user@*!* - :type extra: str""" - if extra != None: - self.sendRaw("MODE %s %s %s" % (channel,mode,extra)) - else: - self.sendRaw("MODE %s %s" % (channel,mode)) - - def act_ACTION(self, channel, action): - """Use the `/me ` command - - :param channel: the channel name or target's name the message is sent to - :type channel: str - :param action: the text to send - :type action: str""" - self.sendRaw("PRIVMSG %s :\x01ACTION %s"%(channel,action)) - - def act_KICK(self, channel, who, comment=""): - """Use the `/kick ` command - - :param channel: the channel from which the user will be kicked - :type channel: str - :param who: the nickname of the user to kick - :type action: str - :param comment: the kick message - :type comment: str""" - self.sendRaw("KICK %s %s :%s" % (channel, who, comment)) - - def act_QUIT(self, message): - """Use the `/quit` command - - :param message: quit message - :type message: str""" - self.sendRaw("QUIT :%s" % message) - + + def __init__(self): + asynchat.async_chat.__init__(self) + + self.connected=False + """If we're connected or not""" + + self.log = logging.getLogger('IRCCore') + """Reference to logger object""" + + self.buffer = StringIO() + """cStringIO used as a buffer""" + + self.alive = True + """True if we should try to stay connected""" + + self.server = None + """Server address""" + self.port = 0 + """Server port""" + self.ipv6 = False + """Use IPv6?""" + + # IRC Messages are terminated with \r\n + self.set_terminator(b"\r\n") + + # Set up hooks for modules + self.initHooks() + + # Map for asynchat + self.asynmap = {} + + def loop(self): + asyncore.loop(map=self.asynmap) + + def kill(self): + """TODO close the socket""" + self.act_QUIT("Help! Another thread is killing me :(") + + " Net related code here on down " + + def getBuf(self): + """Return the network buffer and clear it""" + self.buffer.seek(0) + data = self.buffer.read() + self.buffer = StringIO() + return data + + def collect_incoming_data(self, data): + """Recieve data from the IRC server, append it to the buffer + + :param data: the data that was recieved + :type data: str""" + #self.log.debug("<< %(message)s", {"message":repr(data)}) + self.buffer.write(data) + + def found_terminator(self): + """A complete command was pushed through, so clear the buffer and process it.""" + line = None + buf = self.getBuf() + try: + line = buf.decode("UTF-8") + except UnicodeDecodeError as ude: + self.log.error("found_terminator(): could not decode input as UTF-8") + self.log.error("found_terminator(): data: %s" % line) + self.log.error("found_terminator(): repr(data): %s" % repr(line)) + self.log.error("found_terminator(): error: %s" % str(ude)) + return + self.process_data(line) + + def handle_close(self): + """Called when the socket is disconnected. Triggers the _DISCONNECT hook""" + self.log.debug("handle_close") + self.connected=False + self.close() + self.fire_hook("_DISCONNECT") + + def handle_error(self, *args, **kwargs): + """Called on fatal network errors.""" + self.log.error("Connection failed (handle_error)") + self.log.error(str(args)) + self.log.error(str(kwargs)) + self.log(IRCCore.trace()); + + def _connect(self): + """Connect to IRC""" + self.log.debug("Connecting to %(server)s:%(port)i", {"server":self.server, "port":self.port}) + socket_type = socket.AF_INET + if self.ipv6: + self.log.info("IPv6 is enabled.") + socket_type = socket.AF_INET6 + socketInfo = socket.getaddrinfo(self.server, self.port, socket_type) + self.create_socket(socket_type, socket.SOCK_STREAM) + + self.connect(socketInfo[0][4]) + self.asynmap[self._fileno] = self # http://willpython.blogspot.com/2010/08/multiple-event-loops-with-asyncore-and.html + + def handle_connect(self): + """When asynchat indicates our socket is connected, fire the _CONNECT hook""" + self.connected=True + self.log.debug("handle_connect: connected") + self.fire_hook("_CONNECT") + self.log.debug("handle_connect: complete") + + def sendRaw(self, text): + """Send a raw string to the IRC server + + :param text: the string to send + :type text: str""" + if self.connected: + #self.log.debug(">> "+text) + self.send( (text+"\r\n").encode("UTF-8").decode().encode("UTF-8")) + else: + self.log.warning("Send attempted while disconnected. >> "+text) + + def process_data(self, data): + """Process one line of tet irc sent us + + :param data: the data to process + :type data: str""" + if data.strip() == "": + return + + prefix = None + command = None + args=[] + trailing=None + + if data[0]==":": + prefix=data.split(" ")[0][1:] + data=data[data.find(" ")+1:] + command = data.split(" ")[0] + data=data[data.find(" ")+1:] + if(data[0]==":"): + # no args + trailing = data[1:].strip() + else: + trailing = data[data.find(" :")+2:].strip() + data = data[:data.find(" :")] + args = data.split(" ") + for index,arg in enumerate(args): + args[index]=arg.strip() + if not command in self.hookcalls: + self.log.warning("Unknown command: cmd='%s' prefix='%s' args='%s' trailing='%s'" % (command, prefix, args, trailing)) + else: + self.fire_hook(command, args=args, prefix=prefix, trailing=trailing) + + + " Module related code " + def initHooks(self): + """Defines hooks that modules can listen for events of""" + self.hooks = [ + '_CONNECT', # Called when the bot connects to IRC on the socket level + '_DISCONNECT', # Called when the irc socket is forcibly closed + 'NOTICE', # :irc.129irc.com NOTICE AUTH :*** Looking up your hostname... + 'MODE', # :CloneABCD MODE CloneABCD :+iwx + 'PING', # PING :irc.129irc.com + 'JOIN', # :CloneA!dave@hidden-B4F6B1AA.rit.edu JOIN :#clonea + 'QUIT', # :HCSMPBot!~HCSMPBot@108.170.48.18 QUIT :Quit: Disconnecting! + 'NICK', # :foxiAway!foxi@irc.hcsmp.com NICK :foxi + 'PART', # :CloneA!dave@hidden-B4F6B1AA.rit.edu PART #clonea + 'PRIVMSG', # :CloneA!dave@hidden-B4F6B1AA.rit.edu PRIVMSG #clonea :aaa + 'KICK', # :xMopxShell!~rduser@host KICK #xMopx2 xBotxShellTest :xBotxShellTest + 'INVITE', # :gmx!~gmxgeek@irc.hcsmp.com INVITE Tyrone :#hcsmp' + '001', # :irc.129irc.com 001 CloneABCD :Welcome to the 129irc IRC Network CloneABCD!CloneABCD@djptwc-laptop1.rit.edu + '002', # :irc.129irc.com 002 CloneABCD :Your host is irc.129irc.com, running version Unreal3.2.8.1 + '003', # :irc.129irc.com 003 CloneABCD :This server was created Mon Jul 19 2010 at 03:12:01 EDT + '004', # :irc.129irc.com 004 CloneABCD irc.129irc.com Unreal3.2.8.1 iowghraAsORTVSxNCWqBzvdHtGp lvhopsmntikrRcaqOALQbSeIKVfMCuzNTGj + '005', # :irc.129irc.com 005 CloneABCD CMDS=KNOCK,MAP,DCCALLOW,USERIP UHNAMES NAMESX SAFELIST HCN MAXCHANNELS=10 CHANLIMIT=#:10 MAXLIST=b:60,e:60,I:60 NICKLEN=30 CHANNELLEN=32 TOPICLEN=307 KICKLEN=307 AWAYLEN=307 :are supported by this server + '250', # :chaos.esper.net 250 xBotxShellTest :Highest connection count: 1633 (1632 clients) (186588 connections received) + '251', # :irc.129irc.com 251 CloneABCD :There are 1 users and 48 invisible on 2 servers + '252', # :irc.129irc.com 252 CloneABCD 9 :operator(s) online + '254', # :irc.129irc.com 254 CloneABCD 6 :channels formed + '255', # :irc.129irc.com 255 CloneABCD :I have 42 clients and 1 servers + '265', # :irc.129irc.com 265 CloneABCD :Current Local Users: 42 Max: 47 + '266', # :irc.129irc.com 266 CloneABCD :Current Global Users: 49 Max: 53 + '332', # :chaos.esper.net 332 xBotxShellTest #xMopx2 :/ #XMOPX2 / https://code.google.com/p/pyircbot/ (Channel Topic) + '333', # :chaos.esper.net 333 xBotxShellTest #xMopx2 xMopxShell!~rduser@108.170.60.242 1344370109 + '353', # :irc.129irc.com 353 CloneABCD = #clonea :CloneABCD CloneABC + '366', # :irc.129irc.com 366 CloneABCD #clonea :End of /NAMES list. + '372', # :chaos.esper.net 372 xBotxShell :motd text here + '375', # :chaos.esper.net 375 xBotxShellTest :- chaos.esper.net Message of the Day - + '376', # :chaos.esper.net 376 xBotxShell :End of /MOTD command. + '422', # :irc.129irc.com 422 CloneABCD :MOTD File is missing + '433', # :nova.esper.net 433 * pyircbot3 :Nickname is already in use. + ] + " mapping of hooks to methods " + self.hookcalls = {} + for command in self.hooks: + self.hookcalls[command]=[] + + def fire_hook(self, command, args=None, prefix=None, trailing=None): + """Run any listeners for a specific hook + + :param command: the hook to fire + :type command: str + :param args: the list of arguments, if any, the command was passed + :type args: list + :param prefix: prefix of the sender of this command + :type prefix: str + :param trailing: data payload of the command + :type trailing: str""" + + for hook in self.hookcalls[command]: + try: + hook(args, prefix, trailing) + except: + self.log.warning("Error processing hook: \n%s"% self.trace()) + + def addHook(self, command, method): + """**Internal.** Enable (connect) a single hook of a module + + :param command: command this hook will trigger on + :type command: str + :param method: callable method object to hook in + :type method: object""" + " add a single hook " + if command in self.hooks: + self.hookcalls[command].append(method) + else: + self.log.warning("Invalid hook - %s" % command) + return False + + def removeHook(self, command, method): + """**Internal.** Disable (disconnect) a single hook of a module + + :param command: command this hook triggers on + :type command: str + :param method: callable method that should be removed + :type method: object""" + " remove a single hook " + if command in self.hooks: + for hookedMethod in self.hookcalls[command]: + if hookedMethod == method: + self.hookcalls[command].remove(hookedMethod) + else: + self.log.warning("Invalid hook - %s" % command) + return False + + " Utility methods " + @staticmethod + def decodePrefix(prefix): + """Given a prefix like nick!username@hostname, return an object with these properties + + :param prefix: the prefix to disassemble + :type prefix: str + :returns: object -- an UserPrefix object with the properties `nick`, `username`, `hostname` or a ServerPrefix object with the property `hostname`""" + if "!" in prefix: + ob = type('UserPrefix', (object,), {}) + ob.nick, prefix = prefix.split("!") + ob.username, ob.hostname = prefix.split("@") + return ob + else: + ob = type('ServerPrefix', (object,), {}) + ob.hostname = prefix + return ob + + @staticmethod + def trace(): + """Return the stack trace of the bot as a string""" + result = "" + result += "\n*** STACKTRACE - START ***\n" + code = [] + for threadId, stack in sys._current_frames().items(): + code.append("\n# ThreadID: %s" % threadId) + for filename, lineno, name, line in traceback.extract_stack(stack): + code.append('File: "%s", line %d, in %s' % (filename, lineno, name)) + if line: + code.append(" %s" % (line.strip())) + for line in code: + result += line + "\n" + result += "\n*** STACKTRACE - END ***\n" + return result + + " Data Methods " + def get_nick(self): + """Get the bot's current nick + + :returns: str - the bot's current nickname""" + return self.nick + + " Action Methods " + def act_PONG(self, data): + """Use the `/pong` command - respond to server pings + + :param data: the string or number the server sent with it's ping + :type data: str""" + self.sendRaw("PONG :%s" % data) + + def act_USER(self, username, hostname, realname): + """Use the USER protocol command. Used during connection + + :param username: the bot's username + :type username: str + :param hostname: the bot's hostname + :type hostname: str + :param realname: the bot's realname + :type realname: str""" + self.sendRaw("USER %s %s %s :%s" % (username, hostname, self.server, realname)) + + def act_NICK(self, newNick): + """Use the `/nick` command + + :param newNick: new nick for the bot + :type newNick: str""" + self.nick = newNick + self.sendRaw("NICK %s" % newNick) + + def act_JOIN(self, channel): + """Use the `/join` command + + :param channel: the channel to attempt to join + :type channel: str""" + self.sendRaw("JOIN %s"%channel) + + def act_PRIVMSG(self, towho, message): + """Use the `/msg` command + + :param towho: the target #channel or user's name + :type towho: str + :param message: the message to send + :type message: str""" + self.sendRaw("PRIVMSG %s :%s"%(towho,message)) + + def act_MODE(self, channel, mode, extra=None): + """Use the `/mode` command + + :param channel: the channel this mode is for + :type channel: str + :param mode: the mode string. Example: +b + :type mode: str + :param extra: additional argument if the mode needs it. Example: user@*!* + :type extra: str""" + if extra != None: + self.sendRaw("MODE %s %s %s" % (channel,mode,extra)) + else: + self.sendRaw("MODE %s %s" % (channel,mode)) + + def act_ACTION(self, channel, action): + """Use the `/me ` command + + :param channel: the channel name or target's name the message is sent to + :type channel: str + :param action: the text to send + :type action: str""" + self.sendRaw("PRIVMSG %s :\x01ACTION %s"%(channel,action)) + + def act_KICK(self, channel, who, comment=""): + """Use the `/kick ` command + + :param channel: the channel from which the user will be kicked + :type channel: str + :param who: the nickname of the user to kick + :type action: str + :param comment: the kick message + :type comment: str""" + self.sendRaw("KICK %s %s :%s" % (channel, who, comment)) + + def act_QUIT(self, message): + """Use the `/quit` command + + :param message: quit message + :type message: str""" + self.sendRaw("QUIT :%s" % message) + diff --git a/pyircbot/rpc.py b/pyircbot/rpc.py index 8004053..d572a65 100644 --- a/pyircbot/rpc.py +++ b/pyircbot/rpc.py @@ -12,157 +12,173 @@ from pyircbot import jsonrpc from threading import Thread class BotRPC(Thread): - """:param main: A reference to the PyIRCBot instance this instance will control - :type main: PyIRCBot - """ - - def __init__(self, main): - Thread.__init__(self, daemon=True) - self.bot = main - self.log = logging.getLogger('RPC') - self.server = jsonrpc.Server( - jsonrpc.JsonRpc20(), - jsonrpc.TransportTcpIp( - addr=( - self.bot.botconfig["bot"]["rpcbind"], - self.bot.botconfig["bot"]["rpcport"] - ) - ) - ) - - self.server.register_function( self.importModule ) - self.server.register_function( self.deportModule ) - self.server.register_function( self.loadModule ) - self.server.register_function( self.unloadModule ) - self.server.register_function( self.reloadModule ) - self.server.register_function( self.redoModule ) - self.server.register_function( self.getLoadedModules ) - self.server.register_function( self.pluginCommand ) - self.server.register_function( self.setPluginVar ) - self.server.register_function( self.getPluginVar ) - self.server.register_function( self.quit ) - - self.start() - - def run(self): - """Internal, starts the RPC server""" - self.server.serve() - - def importModule(self, moduleName): - """Import a module - - :param moduleName: Name of the module to import - :type moduleName: str""" - self.log.info("RPC: calling importModule(%s)"%moduleName) - return self.bot.importmodule(moduleName) - - def deportModule(self, moduleName): - """Remove a module's code from memory. If the module is loaded it will be unloaded silently. - - :param moduleName: Name of the module to import - :type moduleName: str""" - self.log.info("RPC: calling deportModule(%s)"%moduleName) - self.bot.deportmodule(moduleName) - - def loadModule(self, moduleName): - """Activate a module. - - :param moduleName: Name of the module to activate - :type moduleName: str""" - self.log.info("RPC: calling loadModule(%s)"%moduleName) - return self.bot.loadmodule(moduleName) - - def unloadModule(self, moduleName): - """Deactivate a module. - - :param moduleName: Name of the module to deactivate - :type moduleName: str""" - self.log.info("RPC: calling unloadModule(%s)"%moduleName) - self.bot.unloadmodule(moduleName) - - def reloadModule(self, moduleName): - """Deactivate and activate a module. - - :param moduleName: Name of the target module - :type moduleName: str""" - self.log.info("RPC: calling reloadModule(%s)"%moduleName) - self.bot.unloadmodule(moduleName) - return self.bot.loadmodule(moduleName) - - def redoModule(self, moduleName): - """Reload a running module from disk - - :param moduleName: Name of the target module - :type moduleName: str""" - self.log.info("RPC: calling redoModule(%s)"%moduleName) - return self.bot.redomodule(moduleName) - - def getLoadedModules(self): - """Return a list of active modules - - :returns: list -- ['ModuleName1', 'ModuleName2']""" - self.log.info("RPC: calling getLoadedModules()") - return list(self.bot.moduleInstances.keys()) - - def pluginCommand(self, moduleName, methodName, argList): - """Run a method of an active module - - :param moduleName: Name of the target module - :type moduleName: str - :param methodName: Name of the target method - :type methodName: str - :param argList: List of positional arguments to call the method with - :type argList: list - :returns: mixed -- Any basic type the target method may return""" - plugin = self.bot.getmodulebyname(moduleName) - if not plugin: - return (False, "Plugin not found") - method = getattr(plugin, methodName) - if not method: - return (False, "Method not found") - self.log.info("RPC: calling %s.%s(%s)" % (moduleName, methodName, argList)) - return (True, method(*argList)) - - def getPluginVar(self, moduleName, moduleVarName): - """Extract a property from an active module and return it - - :param moduleName: Name of the target module - :type moduleName: str - :param moduleVarName: Name of the target property - :type moduleVarName: str - :returns: mixed -- Any basic type extracted from an active module""" - plugin = self.bot.getmodulebyname(moduleName) - if moduleName == "_core": - plugin = self.bot - if not plugin: - return (False, "Plugin not found") - self.log.info("RPC: getting %s.%s" % (moduleName, moduleVarName)) - return (True, getattr(plugin, moduleVarName)) - - def setPluginVar(self, moduleName, moduleVarName, value): - """Set a property of an active module - - :param moduleName: Name of the target module - :type moduleName: str - :param moduleVarName: Name of the target property - :type moduleVarName: str - :param value: Value the target property will be set to - :type value: str""" - plugin = self.bot.getmodulebyname(moduleName) - if moduleName == "_core": - plugin = self.bot - if not plugin: - return (False, "Plugin not found") - self.log.info("RPC: setting %s.%s = %s )" % (moduleName, moduleVarName, value)) - setattr(plugin, moduleVarName, value) - return (True, "Var set") - - def quit(self, message): - """Tell the bot to quit IRC and exit - - :param message: Quit message - :type moduleName: str""" - self.bot.act_QUIT(message) - self.bot.kill() - return (True, "Shutdown ordered") - \ No newline at end of file + """:param main: A reference to the PyIRCBot instance this instance will control + :type main: PyIRCBot + """ + + def __init__(self, main): + Thread.__init__(self, daemon=True) + self.bot = main + self.log = logging.getLogger('RPC') + self.server = jsonrpc.Server( + jsonrpc.JsonRpc20(), + jsonrpc.TransportTcpIp( + addr=( + self.bot.botconfig["bot"]["rpcbind"], + self.bot.botconfig["bot"]["rpcport"] + ) + ) + ) + + self.server.register_function( self.importModule ) + self.server.register_function( self.deportModule ) + self.server.register_function( self.loadModule ) + self.server.register_function( self.unloadModule ) + self.server.register_function( self.reloadModule ) + self.server.register_function( self.redoModule ) + self.server.register_function( self.getLoadedModules ) + self.server.register_function( self.pluginCommand ) + self.server.register_function( self.setPluginVar ) + self.server.register_function( self.getPluginVar ) + self.server.register_function( self.quit ) + self.server.register_function( self.eval ) + self.server.register_function( self.exec ) + + self.start() + + def run(self): + """Internal, starts the RPC server""" + self.server.serve() + + def importModule(self, moduleName): + """Import a module + + :param moduleName: Name of the module to import + :type moduleName: str""" + self.log.info("RPC: calling importModule(%s)"%moduleName) + return self.bot.importmodule(moduleName) + + def deportModule(self, moduleName): + """Remove a module's code from memory. If the module is loaded it will be unloaded silently. + + :param moduleName: Name of the module to import + :type moduleName: str""" + self.log.info("RPC: calling deportModule(%s)"%moduleName) + self.bot.deportmodule(moduleName) + + def loadModule(self, moduleName): + """Activate a module. + + :param moduleName: Name of the module to activate + :type moduleName: str""" + self.log.info("RPC: calling loadModule(%s)"%moduleName) + return self.bot.loadmodule(moduleName) + + def unloadModule(self, moduleName): + """Deactivate a module. + + :param moduleName: Name of the module to deactivate + :type moduleName: str""" + self.log.info("RPC: calling unloadModule(%s)"%moduleName) + self.bot.unloadmodule(moduleName) + + def reloadModule(self, moduleName): + """Deactivate and activate a module. + + :param moduleName: Name of the target module + :type moduleName: str""" + self.log.info("RPC: calling reloadModule(%s)"%moduleName) + self.bot.unloadmodule(moduleName) + return self.bot.loadmodule(moduleName) + + def redoModule(self, moduleName): + """Reload a running module from disk + + :param moduleName: Name of the target module + :type moduleName: str""" + self.log.info("RPC: calling redoModule(%s)"%moduleName) + return self.bot.redomodule(moduleName) + + def getLoadedModules(self): + """Return a list of active modules + + :returns: list -- ['ModuleName1', 'ModuleName2']""" + self.log.info("RPC: calling getLoadedModules()") + return list(self.bot.moduleInstances.keys()) + + def pluginCommand(self, moduleName, methodName, argList): + """Run a method of an active module + + :param moduleName: Name of the target module + :type moduleName: str + :param methodName: Name of the target method + :type methodName: str + :param argList: List of positional arguments to call the method with + :type argList: list + :returns: mixed -- Any basic type the target method may return""" + plugin = self.bot.getmodulebyname(moduleName) + if not plugin: + return (False, "Plugin not found") + method = getattr(plugin, methodName) + if not method: + return (False, "Method not found") + self.log.info("RPC: calling %s.%s(%s)" % (moduleName, methodName, argList)) + return (True, method(*argList)) + + def getPluginVar(self, moduleName, moduleVarName): + """Extract a property from an active module and return it + + :param moduleName: Name of the target module + :type moduleName: str + :param moduleVarName: Name of the target property + :type moduleVarName: str + :returns: mixed -- Any basic type extracted from an active module""" + plugin = self.bot.getmodulebyname(moduleName) + if moduleName == "_core": + plugin = self.bot + if not plugin: + return (False, "Plugin not found") + self.log.info("RPC: getting %s.%s" % (moduleName, moduleVarName)) + return (True, getattr(plugin, moduleVarName)) + + def setPluginVar(self, moduleName, moduleVarName, value): + """Set a property of an active module + + :param moduleName: Name of the target module + :type moduleName: str + :param moduleVarName: Name of the target property + :type moduleVarName: str + :param value: Value the target property will be set to + :type value: str""" + plugin = self.bot.getmodulebyname(moduleName) + if moduleName == "_core": + plugin = self.bot + if not plugin: + return (False, "Plugin not found") + self.log.info("RPC: setting %s.%s = %s )" % (moduleName, moduleVarName, value)) + setattr(plugin, moduleVarName, value) + return (True, "Var set") + + def eval(self, code): + """Execute arbitrary python code on the bot + + :param code: Python code to pass to eval + :type code: str""" + return (True, eval(code)) + + def exec(self, code): + """Execute arbitrary python code on the bot + + :param code: Python code to pass to exec + :type code: str""" + return (True, exec(code)) + + def quit(self, message): + """Tell the bot to quit IRC and exit + + :param message: Quit message + :type moduleName: str""" + self.bot.act_QUIT(message) + self.bot.kill() + return (True, "Shutdown ordered") + \ No newline at end of file