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"
" Data Methods " code = []
def get_nick(self): for threadId, stack in sys._current_frames().items():
"""Get the bot's current nick code.append("\n# ThreadID: %s" % threadId)
for filename, lineno, name, line in traceback.extract_stack(stack):
:returns: str - the bot's current nickname""" code.append('File: "%s", line %d, in %s' % (filename, lineno, name))
return self.nick if line:
code.append(" %s" % (line.strip()))
" Action Methods " for line in code:
def act_PONG(self, data): result += line + "\n"
"""Use the `/pong` command - respond to server pings result += "\n*** STACKTRACE - END ***\n"
return result
:param data: the string or number the server sent with it's ping
:type data: str""" " Data Methods "
self.sendRaw("PONG :%s" % data) def get_nick(self):
"""Get the bot's current nick
def act_USER(self, username, hostname, realname):
"""Use the USER protocol command. Used during connection :returns: str - the bot's current nickname"""
return self.nick
:param username: the bot's username
:type username: str " Action Methods "
:param hostname: the bot's hostname def act_PONG(self, data):
:type hostname: str """Use the `/pong` command - respond to server pings
:param realname: the bot's realname
:type realname: str""" :param data: the string or number the server sent with it's ping
self.sendRaw("USER %s %s %s :%s" % (username, hostname, self.server, realname)) :type data: str"""
self.sendRaw("PONG :%s" % data)
def act_NICK(self, newNick):
"""Use the `/nick` command def act_USER(self, username, hostname, realname):
"""Use the USER protocol command. Used during connection
:param newNick: new nick for the bot
:type newNick: str""" :param username: the bot's username
self.nick = newNick :type username: str
self.sendRaw("NICK %s" % newNick) :param hostname: the bot's hostname
:type hostname: str
def act_JOIN(self, channel): :param realname: the bot's realname
"""Use the `/join` command :type realname: str"""
self.sendRaw("USER %s %s %s :%s" % (username, hostname, self.server, realname))
:param channel: the channel to attempt to join
:type channel: str""" def act_NICK(self, newNick):
self.sendRaw("JOIN %s"%channel) """Use the `/nick` command
def act_PRIVMSG(self, towho, message): :param newNick: new nick for the bot
"""Use the `/msg` command :type newNick: str"""
self.nick = newNick
:param towho: the target #channel or user's name self.sendRaw("NICK %s" % newNick)
:type towho: str
:param message: the message to send def act_JOIN(self, channel):
:type message: str""" """Use the `/join` command
self.sendRaw("PRIVMSG %s :%s"%(towho,message))
:param channel: the channel to attempt to join
def act_MODE(self, channel, mode, extra=None): :type channel: str"""
"""Use the `/mode` command self.sendRaw("JOIN %s"%channel)
:param channel: the channel this mode is for def act_PRIVMSG(self, towho, message):
:type channel: str """Use the `/msg` command
:param mode: the mode string. Example: +b
:type mode: str :param towho: the target #channel or user's name
:param extra: additional argument if the mode needs it. Example: user@*!* :type towho: str
:type extra: str""" :param message: the message to send
if extra != None: :type message: str"""
self.sendRaw("MODE %s %s %s" % (channel,mode,extra)) self.sendRaw("PRIVMSG %s :%s"%(towho,message))
else:
self.sendRaw("MODE %s %s" % (channel,mode)) def act_MODE(self, channel, mode, extra=None):
"""Use the `/mode` command
def act_ACTION(self, channel, action):
"""Use the `/me <action>` command :param channel: the channel this mode is for
:type channel: str
:param channel: the channel name or target's name the message is sent to :param mode: the mode string. Example: +b
:type channel: str :type mode: str
:param action: the text to send :param extra: additional argument if the mode needs it. Example: user@*!*
:type action: str""" :type extra: str"""
self.sendRaw("PRIVMSG %s :\x01ACTION %s"%(channel,action)) if extra != None:
self.sendRaw("MODE %s %s %s" % (channel,mode,extra))
def act_KICK(self, channel, who, comment=""): else:
"""Use the `/kick <user> <message>` command self.sendRaw("MODE %s %s" % (channel,mode))
:param channel: the channel from which the user will be kicked def act_ACTION(self, channel, action):
:type channel: str """Use the `/me <action>` command
:param who: the nickname of the user to kick
:type action: str :param channel: the channel name or target's name the message is sent to
:param comment: the kick message :type channel: str
:type comment: str""" :param action: the text to send
self.sendRaw("KICK %s %s :%s" % (channel, who, comment)) :type action: str"""
self.sendRaw("PRIVMSG %s :\x01ACTION %s"%(channel,action))
def act_QUIT(self, message):
"""Use the `/quit` command def act_KICK(self, channel, who, comment=""):
"""Use the `/kick <user> <message>` command
:param message: quit message
:type message: str""" :param channel: the channel from which the user will be kicked
self.sendRaw("QUIT :%s" % message) :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)

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