Add eval/exec to RPC

This commit is contained in:
dpedu 2015-08-08 15:04:45 -07:00
parent c2e2199b02
commit 5a511944bd
4 changed files with 605 additions and 510 deletions

1
.gitignore vendored
View File

@ -8,3 +8,4 @@ build
pyircbot.egg-info pyircbot.egg-info
dev dev
docs/builder/build.sh docs/builder/build.sh
examples/config.test.json

View File

@ -47,7 +47,7 @@ We can retrieve an arbitrary property from a module:
[True, {'apikey': 'deadbeefcafe', 'defaultUnit': 'f'}] [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 .. 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'}] [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: "<string>", line 1, in <module>
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 <module>
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 <module>
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 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 basic types can be passed over the RPC connection. Trying to access anything
extra results in an error: extra results in an error:

View File

@ -11,364 +11,377 @@ import asynchat
import asyncore import asyncore
import logging import logging
import traceback import traceback
import sys
from socket import SHUT_RDWR from socket import SHUT_RDWR
try: try:
from cStringIO import StringIO from cStringIO import StringIO
except: except:
from io import BytesIO as StringIO from io import BytesIO as StringIO
class IRCCore(asynchat.async_chat): class IRCCore(asynchat.async_chat):
def __init__(self): def __init__(self):
asynchat.async_chat.__init__(self) asynchat.async_chat.__init__(self)
self.connected=False self.connected=False
"""If we're connected or not""" """If we're connected or not"""
self.log = logging.getLogger('IRCCore') self.log = logging.getLogger('IRCCore')
"""Reference to logger object""" """Reference to logger object"""
self.buffer = StringIO() self.buffer = StringIO()
"""cStringIO used as a buffer""" """cStringIO used as a buffer"""
self.alive = True self.alive = True
"""True if we should try to stay connected""" """True if we should try to stay connected"""
self.server = None self.server = None
"""Server address""" """Server address"""
self.port = 0 self.port = 0
"""Server port""" """Server port"""
self.ipv6 = False self.ipv6 = False
"""Use IPv6?""" """Use IPv6?"""
# IRC Messages are terminated with \r\n # IRC Messages are terminated with \r\n
self.set_terminator(b"\r\n") self.set_terminator(b"\r\n")
# Set up hooks for modules # Set up hooks for modules
self.initHooks() self.initHooks()
# Map for asynchat # Map for asynchat
self.asynmap = {} self.asynmap = {}
def loop(self): def loop(self):
asyncore.loop(map=self.asynmap) asyncore.loop(map=self.asynmap)
def kill(self): def kill(self):
"""TODO close the socket""" """TODO close the socket"""
self.act_QUIT("Help! Another thread is killing me :(") self.act_QUIT("Help! Another thread is killing me :(")
" Net related code here on down " " Net related code here on down "
def getBuf(self): def getBuf(self):
"""Return the network buffer and clear it""" """Return the network buffer and clear it"""
self.buffer.seek(0) self.buffer.seek(0)
data = self.buffer.read() data = self.buffer.read()
self.buffer = StringIO() self.buffer = StringIO()
return data return data
def collect_incoming_data(self, data): def collect_incoming_data(self, data):
"""Recieve data from the IRC server, append it to the buffer """Recieve data from the IRC server, append it to the buffer
:param data: the data that was recieved :param data: the data that was recieved
:type data: str""" :type data: str"""
#self.log.debug("<< %(message)s", {"message":repr(data)}) #self.log.debug("<< %(message)s", {"message":repr(data)})
self.buffer.write(data) self.buffer.write(data)
def found_terminator(self): def found_terminator(self):
"""A complete command was pushed through, so clear the buffer and process it.""" """A complete command was pushed through, so clear the buffer and process it."""
line = None line = None
buf = self.getBuf() buf = self.getBuf()
try: try:
line = buf.decode("UTF-8") line = buf.decode("UTF-8")
except UnicodeDecodeError as ude: except UnicodeDecodeError as ude:
self.log.error("found_terminator(): could not decode input as UTF-8") 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(): data: %s" % line)
self.log.error("found_terminator(): repr(data): %s" % repr(line)) self.log.error("found_terminator(): repr(data): %s" % repr(line))
self.log.error("found_terminator(): error: %s" % str(ude)) self.log.error("found_terminator(): error: %s" % str(ude))
return return
self.process_data(line) self.process_data(line)
def handle_close(self): def handle_close(self):
"""Called when the socket is disconnected. Triggers the _DISCONNECT hook""" """Called when the socket is disconnected. Triggers the _DISCONNECT hook"""
self.log.debug("handle_close") self.log.debug("handle_close")
self.connected=False self.connected=False
self.close() self.close()
self.fire_hook("_DISCONNECT") self.fire_hook("_DISCONNECT")
def handle_error(self, *args, **kwargs): def handle_error(self, *args, **kwargs):
"""Called on fatal network errors.""" """Called on fatal network errors."""
self.log.error("Connection failed (handle_error)") self.log.error("Connection failed (handle_error)")
self.log.error(str(args)) self.log.error(str(args))
self.log.error(str(kwargs)) self.log.error(str(kwargs))
self.log(IRCCore.trace()); self.log(IRCCore.trace());
def _connect(self): def _connect(self):
"""Connect to IRC""" """Connect to IRC"""
self.log.debug("Connecting to %(server)s:%(port)i", {"server":self.server, "port":self.port}) self.log.debug("Connecting to %(server)s:%(port)i", {"server":self.server, "port":self.port})
socket_type = socket.AF_INET socket_type = socket.AF_INET
if self.ipv6: if self.ipv6:
self.log.info("IPv6 is enabled.") self.log.info("IPv6 is enabled.")
socket_type = socket.AF_INET6 socket_type = socket.AF_INET6
socketInfo = socket.getaddrinfo(self.server, self.port, socket_type) socketInfo = socket.getaddrinfo(self.server, self.port, socket_type)
self.create_socket(socket_type, socket.SOCK_STREAM) self.create_socket(socket_type, socket.SOCK_STREAM)
self.connect(socketInfo[0][4]) self.connect(socketInfo[0][4])
self.asynmap[self._fileno] = self # http://willpython.blogspot.com/2010/08/multiple-event-loops-with-asyncore-and.html self.asynmap[self._fileno] = self # http://willpython.blogspot.com/2010/08/multiple-event-loops-with-asyncore-and.html
def handle_connect(self): def handle_connect(self):
"""When asynchat indicates our socket is connected, fire the _CONNECT hook""" """When asynchat indicates our socket is connected, fire the _CONNECT hook"""
self.connected=True self.connected=True
self.log.debug("handle_connect: connected") self.log.debug("handle_connect: connected")
self.fire_hook("_CONNECT") self.fire_hook("_CONNECT")
self.log.debug("handle_connect: complete") self.log.debug("handle_connect: complete")
def sendRaw(self, text): def sendRaw(self, text):
"""Send a raw string to the IRC server """Send a raw string to the IRC server
:param text: the string to send :param text: the string to send
:type text: str""" :type text: str"""
if self.connected: if self.connected:
#self.log.debug(">> "+text) #self.log.debug(">> "+text)
self.send( (text+"\r\n").encode("UTF-8").decode().encode("UTF-8")) self.send( (text+"\r\n").encode("UTF-8").decode().encode("UTF-8"))
else: else:
self.log.warning("Send attempted while disconnected. >> "+text) self.log.warning("Send attempted while disconnected. >> "+text)
def process_data(self, data): def process_data(self, data):
"""Process one line of tet irc sent us """Process one line of tet irc sent us
:param data: the data to process :param data: the data to process
:type data: str""" :type data: str"""
if data.strip() == "": if data.strip() == "":
return return
prefix = None prefix = None
command = None command = None
args=[] args=[]
trailing=None trailing=None
if data[0]==":": if data[0]==":":
prefix=data.split(" ")[0][1:] prefix=data.split(" ")[0][1:]
data=data[data.find(" ")+1:] data=data[data.find(" ")+1:]
command = data.split(" ")[0] command = data.split(" ")[0]
data=data[data.find(" ")+1:] data=data[data.find(" ")+1:]
if(data[0]==":"): if(data[0]==":"):
# no args # no args
trailing = data[1:].strip() trailing = data[1:].strip()
else: else:
trailing = data[data.find(" :")+2:].strip() trailing = data[data.find(" :")+2:].strip()
data = data[:data.find(" :")] data = data[:data.find(" :")]
args = data.split(" ") args = data.split(" ")
for index,arg in enumerate(args): for index,arg in enumerate(args):
args[index]=arg.strip() args[index]=arg.strip()
if not command in self.hookcalls: if not command in self.hookcalls:
self.log.warning("Unknown command: cmd='%s' prefix='%s' args='%s' trailing='%s'" % (command, prefix, args, trailing)) self.log.warning("Unknown command: cmd='%s' prefix='%s' args='%s' trailing='%s'" % (command, prefix, args, trailing))
else: else:
self.fire_hook(command, args=args, prefix=prefix, trailing=trailing) self.fire_hook(command, args=args, prefix=prefix, trailing=trailing)
" Module related code " " Module related code "
def initHooks(self): def initHooks(self):
"""Defines hooks that modules can listen for events of""" """Defines hooks that modules can listen for events of"""
self.hooks = [ self.hooks = [
'_CONNECT', # Called when the bot connects to IRC on the socket level '_CONNECT', # Called when the bot connects to IRC on the socket level
'_DISCONNECT', # Called when the irc socket is forcibly closed '_DISCONNECT', # Called when the irc socket is forcibly closed
'NOTICE', # :irc.129irc.com NOTICE AUTH :*** Looking up your hostname... 'NOTICE', # :irc.129irc.com NOTICE AUTH :*** Looking up your hostname...
'MODE', # :CloneABCD MODE CloneABCD :+iwx 'MODE', # :CloneABCD MODE CloneABCD :+iwx
'PING', # PING :irc.129irc.com 'PING', # PING :irc.129irc.com
'JOIN', # :CloneA!dave@hidden-B4F6B1AA.rit.edu JOIN :#clonea 'JOIN', # :CloneA!dave@hidden-B4F6B1AA.rit.edu JOIN :#clonea
'QUIT', # :HCSMPBot!~HCSMPBot@108.170.48.18 QUIT :Quit: Disconnecting! 'QUIT', # :HCSMPBot!~HCSMPBot@108.170.48.18 QUIT :Quit: Disconnecting!
'NICK', # :foxiAway!foxi@irc.hcsmp.com NICK :foxi 'NICK', # :foxiAway!foxi@irc.hcsmp.com NICK :foxi
'PART', # :CloneA!dave@hidden-B4F6B1AA.rit.edu PART #clonea 'PART', # :CloneA!dave@hidden-B4F6B1AA.rit.edu PART #clonea
'PRIVMSG', # :CloneA!dave@hidden-B4F6B1AA.rit.edu PRIVMSG #clonea :aaa 'PRIVMSG', # :CloneA!dave@hidden-B4F6B1AA.rit.edu PRIVMSG #clonea :aaa
'KICK', # :xMopxShell!~rduser@host KICK #xMopx2 xBotxShellTest :xBotxShellTest 'KICK', # :xMopxShell!~rduser@host KICK #xMopx2 xBotxShellTest :xBotxShellTest
'INVITE', # :gmx!~gmxgeek@irc.hcsmp.com INVITE Tyrone :#hcsmp' '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 '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 '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 '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 '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 '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) '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 '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 '252', # :irc.129irc.com 252 CloneABCD 9 :operator(s) online
'254', # :irc.129irc.com 254 CloneABCD 6 :channels formed '254', # :irc.129irc.com 254 CloneABCD 6 :channels formed
'255', # :irc.129irc.com 255 CloneABCD :I have 42 clients and 1 servers '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 '265', # :irc.129irc.com 265 CloneABCD :Current Local Users: 42 Max: 47
'266', # :irc.129irc.com 266 CloneABCD :Current Global Users: 49 Max: 53 '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) '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 '333', # :chaos.esper.net 333 xBotxShellTest #xMopx2 xMopxShell!~rduser@108.170.60.242 1344370109
'353', # :irc.129irc.com 353 CloneABCD = #clonea :CloneABCD CloneABC '353', # :irc.129irc.com 353 CloneABCD = #clonea :CloneABCD CloneABC
'366', # :irc.129irc.com 366 CloneABCD #clonea :End of /NAMES list. '366', # :irc.129irc.com 366 CloneABCD #clonea :End of /NAMES list.
'372', # :chaos.esper.net 372 xBotxShell :motd text here '372', # :chaos.esper.net 372 xBotxShell :motd text here
'375', # :chaos.esper.net 375 xBotxShellTest :- chaos.esper.net Message of the Day - '375', # :chaos.esper.net 375 xBotxShellTest :- chaos.esper.net Message of the Day -
'376', # :chaos.esper.net 376 xBotxShell :End of /MOTD command. '376', # :chaos.esper.net 376 xBotxShell :End of /MOTD command.
'422', # :irc.129irc.com 422 CloneABCD :MOTD File is missing '422', # :irc.129irc.com 422 CloneABCD :MOTD File is missing
'433', # :nova.esper.net 433 * pyircbot3 :Nickname is already in use. '433', # :nova.esper.net 433 * pyircbot3 :Nickname is already in use.
] ]
" mapping of hooks to methods " " mapping of hooks to methods "
self.hookcalls = {} self.hookcalls = {}
for command in self.hooks: for command in self.hooks:
self.hookcalls[command]=[] self.hookcalls[command]=[]
def fire_hook(self, command, args=None, prefix=None, trailing=None): def fire_hook(self, command, args=None, prefix=None, trailing=None):
"""Run any listeners for a specific hook """Run any listeners for a specific hook
:param command: the hook to fire :param command: the hook to fire
:type command: str :type command: str
:param args: the list of arguments, if any, the command was passed :param args: the list of arguments, if any, the command was passed
:type args: list :type args: list
:param prefix: prefix of the sender of this command :param prefix: prefix of the sender of this command
:type prefix: str :type prefix: str
:param trailing: data payload of the command :param trailing: data payload of the command
:type trailing: str""" :type trailing: str"""
for hook in self.hookcalls[command]: for hook in self.hookcalls[command]:
try: try:
hook(args, prefix, trailing) hook(args, prefix, trailing)
except: except:
self.log.warning("Error processing hook: \n%s"% self.trace()) self.log.warning("Error processing hook: \n%s"% self.trace())
def addHook(self, command, method): def addHook(self, command, method):
"""**Internal.** Enable (connect) a single hook of a module """**Internal.** Enable (connect) a single hook of a module
:param command: command this hook will trigger on :param command: command this hook will trigger on
:type command: str :type command: str
:param method: callable method object to hook in :param method: callable method object to hook in
:type method: object""" :type method: object"""
" add a single hook " " add a single hook "
if command in self.hooks: if command in self.hooks:
self.hookcalls[command].append(method) self.hookcalls[command].append(method)
else: else:
self.log.warning("Invalid hook - %s" % command) self.log.warning("Invalid hook - %s" % command)
return False return False
def removeHook(self, command, method): def removeHook(self, command, method):
"""**Internal.** Disable (disconnect) a single hook of a module """**Internal.** Disable (disconnect) a single hook of a module
:param command: command this hook triggers on :param command: command this hook triggers on
:type command: str :type command: str
:param method: callable method that should be removed :param method: callable method that should be removed
:type method: object""" :type method: object"""
" remove a single hook " " remove a single hook "
if command in self.hooks: if command in self.hooks:
for hookedMethod in self.hookcalls[command]: for hookedMethod in self.hookcalls[command]:
if hookedMethod == method: if hookedMethod == method:
self.hookcalls[command].remove(hookedMethod) self.hookcalls[command].remove(hookedMethod)
else: else:
self.log.warning("Invalid hook - %s" % command) self.log.warning("Invalid hook - %s" % command)
return False return False
" Utility methods " " Utility methods "
@staticmethod @staticmethod
def decodePrefix(prefix): def decodePrefix(prefix):
"""Given a prefix like nick!username@hostname, return an object with these properties """Given a prefix like nick!username@hostname, return an object with these properties
:param prefix: the prefix to disassemble :param prefix: the prefix to disassemble
:type prefix: str :type prefix: str
:returns: object -- an UserPrefix object with the properties `nick`, `username`, `hostname` or a ServerPrefix object with the property `hostname`""" :returns: object -- an UserPrefix object with the properties `nick`, `username`, `hostname` or a ServerPrefix object with the property `hostname`"""
if "!" in prefix: if "!" in prefix:
ob = type('UserPrefix', (object,), {}) ob = type('UserPrefix', (object,), {})
ob.nick, prefix = prefix.split("!") ob.nick, prefix = prefix.split("!")
ob.username, ob.hostname = prefix.split("@") ob.username, ob.hostname = prefix.split("@")
return ob return ob
else: else:
ob = type('ServerPrefix', (object,), {}) ob = type('ServerPrefix', (object,), {})
ob.hostname = prefix ob.hostname = prefix
return ob return ob
@staticmethod @staticmethod
def trace(): def trace():
"""Return the stack trace of the bot as a string""" """Return the stack trace of the bot as a string"""
return traceback.format_exc() 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 " " Data Methods "
def get_nick(self): def get_nick(self):
"""Get the bot's current nick """Get the bot's current nick
:returns: str - the bot's current nickname""" :returns: str - the bot's current nickname"""
return self.nick return self.nick
" Action Methods " " Action Methods "
def act_PONG(self, data): def act_PONG(self, data):
"""Use the `/pong` command - respond to server pings """Use the `/pong` command - respond to server pings
:param data: the string or number the server sent with it's ping :param data: the string or number the server sent with it's ping
:type data: str""" :type data: str"""
self.sendRaw("PONG :%s" % data) self.sendRaw("PONG :%s" % data)
def act_USER(self, username, hostname, realname): def act_USER(self, username, hostname, realname):
"""Use the USER protocol command. Used during connection """Use the USER protocol command. Used during connection
:param username: the bot's username :param username: the bot's username
:type username: str :type username: str
:param hostname: the bot's hostname :param hostname: the bot's hostname
:type hostname: str :type hostname: str
:param realname: the bot's realname :param realname: the bot's realname
:type realname: str""" :type realname: str"""
self.sendRaw("USER %s %s %s :%s" % (username, hostname, self.server, realname)) self.sendRaw("USER %s %s %s :%s" % (username, hostname, self.server, realname))
def act_NICK(self, newNick): def act_NICK(self, newNick):
"""Use the `/nick` command """Use the `/nick` command
:param newNick: new nick for the bot :param newNick: new nick for the bot
:type newNick: str""" :type newNick: str"""
self.nick = newNick self.nick = newNick
self.sendRaw("NICK %s" % newNick) self.sendRaw("NICK %s" % newNick)
def act_JOIN(self, channel): def act_JOIN(self, channel):
"""Use the `/join` command """Use the `/join` command
:param channel: the channel to attempt to join :param channel: the channel to attempt to join
:type channel: str""" :type channel: str"""
self.sendRaw("JOIN %s"%channel) self.sendRaw("JOIN %s"%channel)
def act_PRIVMSG(self, towho, message): def act_PRIVMSG(self, towho, message):
"""Use the `/msg` command """Use the `/msg` command
:param towho: the target #channel or user's name :param towho: the target #channel or user's name
:type towho: str :type towho: str
:param message: the message to send :param message: the message to send
:type message: str""" :type message: str"""
self.sendRaw("PRIVMSG %s :%s"%(towho,message)) self.sendRaw("PRIVMSG %s :%s"%(towho,message))
def act_MODE(self, channel, mode, extra=None): def act_MODE(self, channel, mode, extra=None):
"""Use the `/mode` command """Use the `/mode` command
:param channel: the channel this mode is for :param channel: the channel this mode is for
:type channel: str :type channel: str
:param mode: the mode string. Example: +b :param mode: the mode string. Example: +b
:type mode: str :type mode: str
:param extra: additional argument if the mode needs it. Example: user@*!* :param extra: additional argument if the mode needs it. Example: user@*!*
:type extra: str""" :type extra: str"""
if extra != None: if extra != None:
self.sendRaw("MODE %s %s %s" % (channel,mode,extra)) self.sendRaw("MODE %s %s %s" % (channel,mode,extra))
else: else:
self.sendRaw("MODE %s %s" % (channel,mode)) self.sendRaw("MODE %s %s" % (channel,mode))
def act_ACTION(self, channel, action): def act_ACTION(self, channel, action):
"""Use the `/me <action>` command """Use the `/me <action>` command
:param channel: the channel name or target's name the message is sent to :param channel: the channel name or target's name the message is sent to
:type channel: str :type channel: str
:param action: the text to send :param action: the text to send
:type action: str""" :type action: str"""
self.sendRaw("PRIVMSG %s :\x01ACTION %s"%(channel,action)) self.sendRaw("PRIVMSG %s :\x01ACTION %s"%(channel,action))
def act_KICK(self, channel, who, comment=""): def act_KICK(self, channel, who, comment=""):
"""Use the `/kick <user> <message>` command """Use the `/kick <user> <message>` command
:param channel: the channel from which the user will be kicked :param channel: the channel from which the user will be kicked
:type channel: str :type channel: str
:param who: the nickname of the user to kick :param who: the nickname of the user to kick
:type action: str :type action: str
:param comment: the kick message :param comment: the kick message
:type comment: str""" :type comment: str"""
self.sendRaw("KICK %s %s :%s" % (channel, who, comment)) self.sendRaw("KICK %s %s :%s" % (channel, who, comment))
def act_QUIT(self, message): def act_QUIT(self, message):
"""Use the `/quit` command """Use the `/quit` command
:param message: quit message :param message: quit message
:type message: str""" :type message: str"""
self.sendRaw("QUIT :%s" % message) self.sendRaw("QUIT :%s" % message)

View File

@ -12,157 +12,173 @@ from pyircbot import jsonrpc
from threading import Thread from threading import Thread
class BotRPC(Thread): class BotRPC(Thread):
""":param main: A reference to the PyIRCBot instance this instance will control """:param main: A reference to the PyIRCBot instance this instance will control
:type main: PyIRCBot :type main: PyIRCBot
""" """
def __init__(self, main): def __init__(self, main):
Thread.__init__(self, daemon=True) Thread.__init__(self, daemon=True)
self.bot = main self.bot = main
self.log = logging.getLogger('RPC') self.log = logging.getLogger('RPC')
self.server = jsonrpc.Server( self.server = jsonrpc.Server(
jsonrpc.JsonRpc20(), jsonrpc.JsonRpc20(),
jsonrpc.TransportTcpIp( jsonrpc.TransportTcpIp(
addr=( addr=(
self.bot.botconfig["bot"]["rpcbind"], self.bot.botconfig["bot"]["rpcbind"],
self.bot.botconfig["bot"]["rpcport"] self.bot.botconfig["bot"]["rpcport"]
) )
) )
) )
self.server.register_function( self.importModule ) self.server.register_function( self.importModule )
self.server.register_function( self.deportModule ) self.server.register_function( self.deportModule )
self.server.register_function( self.loadModule ) self.server.register_function( self.loadModule )
self.server.register_function( self.unloadModule ) self.server.register_function( self.unloadModule )
self.server.register_function( self.reloadModule ) self.server.register_function( self.reloadModule )
self.server.register_function( self.redoModule ) self.server.register_function( self.redoModule )
self.server.register_function( self.getLoadedModules ) self.server.register_function( self.getLoadedModules )
self.server.register_function( self.pluginCommand ) self.server.register_function( self.pluginCommand )
self.server.register_function( self.setPluginVar ) self.server.register_function( self.setPluginVar )
self.server.register_function( self.getPluginVar ) self.server.register_function( self.getPluginVar )
self.server.register_function( self.quit ) self.server.register_function( self.quit )
self.server.register_function( self.eval )
self.server.register_function( self.exec )
self.start() self.start()
def run(self): def run(self):
"""Internal, starts the RPC server""" """Internal, starts the RPC server"""
self.server.serve() self.server.serve()
def importModule(self, moduleName): def importModule(self, moduleName):
"""Import a module """Import a module
:param moduleName: Name of the module to import :param moduleName: Name of the module to import
:type moduleName: str""" :type moduleName: str"""
self.log.info("RPC: calling importModule(%s)"%moduleName) self.log.info("RPC: calling importModule(%s)"%moduleName)
return self.bot.importmodule(moduleName) return self.bot.importmodule(moduleName)
def deportModule(self, moduleName): def deportModule(self, moduleName):
"""Remove a module's code from memory. If the module is loaded it will be unloaded silently. """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 :param moduleName: Name of the module to import
:type moduleName: str""" :type moduleName: str"""
self.log.info("RPC: calling deportModule(%s)"%moduleName) self.log.info("RPC: calling deportModule(%s)"%moduleName)
self.bot.deportmodule(moduleName) self.bot.deportmodule(moduleName)
def loadModule(self, moduleName): def loadModule(self, moduleName):
"""Activate a module. """Activate a module.
:param moduleName: Name of the module to activate :param moduleName: Name of the module to activate
:type moduleName: str""" :type moduleName: str"""
self.log.info("RPC: calling loadModule(%s)"%moduleName) self.log.info("RPC: calling loadModule(%s)"%moduleName)
return self.bot.loadmodule(moduleName) return self.bot.loadmodule(moduleName)
def unloadModule(self, moduleName): def unloadModule(self, moduleName):
"""Deactivate a module. """Deactivate a module.
:param moduleName: Name of the module to deactivate :param moduleName: Name of the module to deactivate
:type moduleName: str""" :type moduleName: str"""
self.log.info("RPC: calling unloadModule(%s)"%moduleName) self.log.info("RPC: calling unloadModule(%s)"%moduleName)
self.bot.unloadmodule(moduleName) self.bot.unloadmodule(moduleName)
def reloadModule(self, moduleName): def reloadModule(self, moduleName):
"""Deactivate and activate a module. """Deactivate and activate a module.
:param moduleName: Name of the target module :param moduleName: Name of the target module
:type moduleName: str""" :type moduleName: str"""
self.log.info("RPC: calling reloadModule(%s)"%moduleName) self.log.info("RPC: calling reloadModule(%s)"%moduleName)
self.bot.unloadmodule(moduleName) self.bot.unloadmodule(moduleName)
return self.bot.loadmodule(moduleName) return self.bot.loadmodule(moduleName)
def redoModule(self, moduleName): def redoModule(self, moduleName):
"""Reload a running module from disk """Reload a running module from disk
:param moduleName: Name of the target module :param moduleName: Name of the target module
:type moduleName: str""" :type moduleName: str"""
self.log.info("RPC: calling redoModule(%s)"%moduleName) self.log.info("RPC: calling redoModule(%s)"%moduleName)
return self.bot.redomodule(moduleName) return self.bot.redomodule(moduleName)
def getLoadedModules(self): def getLoadedModules(self):
"""Return a list of active modules """Return a list of active modules
:returns: list -- ['ModuleName1', 'ModuleName2']""" :returns: list -- ['ModuleName1', 'ModuleName2']"""
self.log.info("RPC: calling getLoadedModules()") self.log.info("RPC: calling getLoadedModules()")
return list(self.bot.moduleInstances.keys()) return list(self.bot.moduleInstances.keys())
def pluginCommand(self, moduleName, methodName, argList): def pluginCommand(self, moduleName, methodName, argList):
"""Run a method of an active module """Run a method of an active module
:param moduleName: Name of the target module :param moduleName: Name of the target module
:type moduleName: str :type moduleName: str
:param methodName: Name of the target method :param methodName: Name of the target method
:type methodName: str :type methodName: str
:param argList: List of positional arguments to call the method with :param argList: List of positional arguments to call the method with
:type argList: list :type argList: list
:returns: mixed -- Any basic type the target method may return""" :returns: mixed -- Any basic type the target method may return"""
plugin = self.bot.getmodulebyname(moduleName) plugin = self.bot.getmodulebyname(moduleName)
if not plugin: if not plugin:
return (False, "Plugin not found") return (False, "Plugin not found")
method = getattr(plugin, methodName) method = getattr(plugin, methodName)
if not method: if not method:
return (False, "Method not found") return (False, "Method not found")
self.log.info("RPC: calling %s.%s(%s)" % (moduleName, methodName, argList)) self.log.info("RPC: calling %s.%s(%s)" % (moduleName, methodName, argList))
return (True, method(*argList)) return (True, method(*argList))
def getPluginVar(self, moduleName, moduleVarName): def getPluginVar(self, moduleName, moduleVarName):
"""Extract a property from an active module and return it """Extract a property from an active module and return it
:param moduleName: Name of the target module :param moduleName: Name of the target module
:type moduleName: str :type moduleName: str
:param moduleVarName: Name of the target property :param moduleVarName: Name of the target property
:type moduleVarName: str :type moduleVarName: str
:returns: mixed -- Any basic type extracted from an active module""" :returns: mixed -- Any basic type extracted from an active module"""
plugin = self.bot.getmodulebyname(moduleName) plugin = self.bot.getmodulebyname(moduleName)
if moduleName == "_core": if moduleName == "_core":
plugin = self.bot plugin = self.bot
if not plugin: if not plugin:
return (False, "Plugin not found") return (False, "Plugin not found")
self.log.info("RPC: getting %s.%s" % (moduleName, moduleVarName)) self.log.info("RPC: getting %s.%s" % (moduleName, moduleVarName))
return (True, getattr(plugin, moduleVarName)) return (True, getattr(plugin, moduleVarName))
def setPluginVar(self, moduleName, moduleVarName, value): def setPluginVar(self, moduleName, moduleVarName, value):
"""Set a property of an active module """Set a property of an active module
:param moduleName: Name of the target module :param moduleName: Name of the target module
:type moduleName: str :type moduleName: str
:param moduleVarName: Name of the target property :param moduleVarName: Name of the target property
:type moduleVarName: str :type moduleVarName: str
:param value: Value the target property will be set to :param value: Value the target property will be set to
:type value: str""" :type value: str"""
plugin = self.bot.getmodulebyname(moduleName) plugin = self.bot.getmodulebyname(moduleName)
if moduleName == "_core": if moduleName == "_core":
plugin = self.bot plugin = self.bot
if not plugin: if not plugin:
return (False, "Plugin not found") return (False, "Plugin not found")
self.log.info("RPC: setting %s.%s = %s )" % (moduleName, moduleVarName, value)) self.log.info("RPC: setting %s.%s = %s )" % (moduleName, moduleVarName, value))
setattr(plugin, moduleVarName, value) setattr(plugin, moduleVarName, value)
return (True, "Var set") return (True, "Var set")
def quit(self, message): def eval(self, code):
"""Tell the bot to quit IRC and exit """Execute arbitrary python code on the bot
:param message: Quit message :param code: Python code to pass to eval
:type moduleName: str""" :type code: str"""
self.bot.act_QUIT(message) return (True, eval(code))
self.bot.kill()
return (True, "Shutdown ordered") 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")