2013-12-28 09:58:20 -08:00
|
|
|
#!/usr/bin/env python
|
|
|
|
import socket
|
|
|
|
import asynchat
|
|
|
|
import logging
|
|
|
|
import traceback
|
|
|
|
import time
|
|
|
|
import sys
|
|
|
|
from socket import SHUT_RDWR
|
|
|
|
from core.rpc import BotRPC
|
|
|
|
|
|
|
|
try:
|
|
|
|
from cStringIO import StringIO
|
|
|
|
except:
|
|
|
|
from io import BytesIO as StringIO
|
|
|
|
|
|
|
|
class PyIRCBot(asynchat.async_chat):
|
|
|
|
def __init__(self, coreconfig, botconfig):
|
|
|
|
asynchat.async_chat.__init__(self)
|
|
|
|
" logging "
|
|
|
|
self.log = logging.getLogger('PyIRCBot')
|
|
|
|
" config "
|
|
|
|
self.coreconfig = coreconfig
|
|
|
|
self.botconfig = botconfig
|
|
|
|
" rpc "
|
|
|
|
self.rpc = BotRPC(self)
|
|
|
|
|
|
|
|
" stringio object as buffer "
|
|
|
|
self.buffer = StringIO()
|
|
|
|
" line terminator "
|
|
|
|
self.set_terminator(b"\r\n")
|
|
|
|
|
|
|
|
" Setup hooks for modules "
|
|
|
|
self.initHooks()
|
|
|
|
" Load modules "
|
|
|
|
self.initModules()
|
|
|
|
|
|
|
|
self._connect()
|
|
|
|
self.connected=False
|
|
|
|
|
|
|
|
def kill(self):
|
|
|
|
" Close RPC Socket "
|
|
|
|
#try:
|
|
|
|
# self.rpc.server._Server__transport.shutdown(SHUT_RDWR)
|
|
|
|
#except Exception as e:
|
|
|
|
# self.log.error(str(e))
|
|
|
|
try:
|
|
|
|
self.rpc.server._Server__transport.close()
|
|
|
|
except Exception as e:
|
|
|
|
self.log.error(str(e))
|
|
|
|
|
|
|
|
" Kill RPC thread "
|
|
|
|
self.rpc._stop()
|
|
|
|
|
|
|
|
" Close all modules "
|
|
|
|
self.closeAllModules()
|
|
|
|
|
|
|
|
" Net related code "
|
|
|
|
|
|
|
|
def getBuf(self):
|
|
|
|
" return buffer and clear "
|
|
|
|
self.buffer.seek(0)
|
|
|
|
data = self.buffer.read()
|
|
|
|
self.buffer = StringIO()
|
|
|
|
return data
|
|
|
|
|
|
|
|
def collect_incoming_data(self, data):
|
|
|
|
" Recieve data from stream, add to buffer "
|
|
|
|
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."
|
2014-01-07 10:17:05 -08:00
|
|
|
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)
|
2013-12-28 09:58:20 -08:00
|
|
|
|
|
|
|
def handle_close(self):
|
|
|
|
" called on socket shutdown "
|
|
|
|
self.log.debug("handle_close")
|
|
|
|
self.connected=False
|
|
|
|
self.close()
|
|
|
|
|
|
|
|
self.log.warning("Connection was lost.")
|
|
|
|
|
|
|
|
#self.log.warning("Connection was lost. Reconnecting in 5 seconds.")
|
|
|
|
#time.sleep(5)
|
|
|
|
#self._connect()
|
|
|
|
|
|
|
|
def handle_error(self, *args, **kwargs):
|
|
|
|
raise
|
|
|
|
|
|
|
|
def _connect(self):
|
|
|
|
self.log.debug("Connecting to %(server)s:%(port)i", {"server":self.botconfig["connection"]["server"], "port":self.botconfig["connection"]["port"]})
|
|
|
|
socket_type = socket.AF_INET
|
|
|
|
if self.botconfig["connection"]["ipv6"]:
|
|
|
|
self.log.info("IPv6 is enabled.")
|
|
|
|
socket_type = socket.AF_INET6
|
|
|
|
socketInfo = socket.getaddrinfo(self.botconfig["connection"]["server"], self.botconfig["connection"]["port"], socket_type)
|
|
|
|
self.create_socket(socket_type, socket.SOCK_STREAM)
|
|
|
|
if "bindaddr" in self.botconfig["connection"]:
|
|
|
|
self.bind((self.botconfig["connection"]["bindaddr"], 0))
|
|
|
|
self.connect(socketInfo[0][4])
|
|
|
|
|
|
|
|
def handle_connect(self):
|
|
|
|
" Called when the first packets come through, so we ident here "
|
|
|
|
self.connected=True
|
|
|
|
self.log.debug("handle_connect: setting USER and NICK")
|
|
|
|
self.fire_hook("_CONNECT")
|
|
|
|
self.log.debug("handle_connect: complete")
|
|
|
|
|
|
|
|
def sendRaw(self, text):
|
|
|
|
if self.connected:
|
|
|
|
self.log.debug(">> "+text)
|
|
|
|
self.send( (text+"\r\n").encode("ascii"))
|
|
|
|
else:
|
|
|
|
self.log.warning("Send attempted while disconnected. >> "+text)
|
|
|
|
|
|
|
|
def process_data(self, data):
|
|
|
|
" called per line of irc sent through "
|
|
|
|
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):
|
|
|
|
self.hooks = [
|
|
|
|
'_CONNECT', # Called when the bot connects to IRC on the socket level
|
|
|
|
'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):
|
|
|
|
for hook in self.hookcalls[command]:
|
|
|
|
try:
|
|
|
|
hook(args, prefix, trailing)
|
|
|
|
except:
|
|
|
|
self.log.warning("Error processing hook: \n%s"% self.trace())
|
|
|
|
|
|
|
|
def initModules(self):
|
|
|
|
" load modules specified in config "
|
|
|
|
" storage of imported modules "
|
|
|
|
self.modules = {}
|
|
|
|
" instances of modules "
|
|
|
|
self.moduleInstances = {}
|
|
|
|
" append module location to path "
|
|
|
|
sys.path.append(self.coreconfig["moduledir"])
|
|
|
|
" append bot directory to path "
|
|
|
|
sys.path.append(self.coreconfig["botdir"]+"core/")
|
|
|
|
|
|
|
|
for modulename in self.botconfig["modules"]:
|
|
|
|
self.loadmodule(modulename)
|
|
|
|
|
|
|
|
def importmodule(self, name):
|
|
|
|
" import a module by name "
|
|
|
|
" check if already exists "
|
|
|
|
if not name in self.modules:
|
|
|
|
" attempt to load "
|
|
|
|
try:
|
|
|
|
moduleref = __import__(name)
|
|
|
|
self.modules[name]=moduleref
|
|
|
|
return (True, None)
|
|
|
|
except Exception as e:
|
|
|
|
" on failure (usually syntax error in Module code) print an error "
|
|
|
|
self.log.error("Module %s failed to load: " % name)
|
|
|
|
self.log.error("Module load failure reason: " + str(e))
|
|
|
|
return (False, str(e))
|
|
|
|
else:
|
|
|
|
self.log.warning("Module %s already imported" % name)
|
|
|
|
return (False, "Module already imported")
|
|
|
|
|
|
|
|
def deportmodule(self, name):
|
|
|
|
" remove a module from memory by name "
|
|
|
|
" unload if necessary "
|
|
|
|
if name in self.moduleInstances:
|
|
|
|
self.unloadmodule(name)
|
|
|
|
" delete all references to the module"
|
|
|
|
if name in self.modules:
|
|
|
|
item = self.modules[name]
|
|
|
|
del self.modules[name]
|
|
|
|
del item
|
|
|
|
" delete copy that python stores in sys.modules "
|
|
|
|
if name in sys.modules:
|
|
|
|
del sys.modules[name]
|
|
|
|
|
|
|
|
def loadmodule(self, name):
|
|
|
|
" load a module and activate it "
|
|
|
|
" check if already loaded "
|
|
|
|
if name in self.moduleInstances:
|
|
|
|
self.log.warning( "Module %s already loaded" % name )
|
|
|
|
return False
|
|
|
|
" check if needs to be imported, and verify it was "
|
|
|
|
if not name in self.modules:
|
|
|
|
importResult = self.importmodule(name)
|
|
|
|
if not importResult[0]:
|
|
|
|
return importResult
|
|
|
|
" init the module "
|
|
|
|
self.moduleInstances[name] = getattr(self.modules[name], name)(self, name)
|
|
|
|
" load hooks "
|
|
|
|
self.loadModuleHooks(self.moduleInstances[name])
|
|
|
|
|
|
|
|
def unloadmodule(self, name):
|
|
|
|
" unload a module by name "
|
|
|
|
if name in self.moduleInstances:
|
|
|
|
" notify the module of disabling "
|
|
|
|
self.moduleInstances[name].ondisable()
|
|
|
|
" unload all hooks "
|
|
|
|
self.unloadModuleHooks(self.moduleInstances[name])
|
|
|
|
" remove the instance "
|
|
|
|
item = self.moduleInstances.pop(name)
|
|
|
|
" delete the instance"
|
|
|
|
del item
|
|
|
|
self.log.info( "Module %s unloaded" % name )
|
|
|
|
return (True, None)
|
|
|
|
else:
|
|
|
|
self.log.info("Module %s not loaded" % name)
|
|
|
|
return (False, "Module not loaded")
|
|
|
|
|
|
|
|
def reloadmodule(self, name):
|
|
|
|
" unload then load a module by name "
|
|
|
|
" make sure it's imporeted"
|
|
|
|
if name in self.modules:
|
|
|
|
" remember if it was loaded before"
|
|
|
|
loadedbefore = name in self.moduleInstances
|
|
|
|
self.log.info("Reloading %s" % self.modules[name])
|
|
|
|
" unload "
|
|
|
|
self.unloadmodule(name)
|
|
|
|
" load "
|
|
|
|
if loadedbefore:
|
|
|
|
self.loadmodule(name)
|
|
|
|
return (True, None)
|
|
|
|
return (False, "Module is not loaded")
|
|
|
|
|
|
|
|
def redomodule(self, name):
|
|
|
|
" reload a modules code from disk "
|
|
|
|
" remember if it was loaded before "
|
|
|
|
loadedbefore = name in self.moduleInstances
|
|
|
|
" unload/deport "
|
|
|
|
self.deportmodule(name)
|
|
|
|
" import "
|
|
|
|
importResult = self.importmodule(name)
|
|
|
|
if not importResult[0]:
|
|
|
|
return importResult
|
|
|
|
" load "
|
|
|
|
if loadedbefore:
|
|
|
|
self.loadmodule(name)
|
|
|
|
return (True, None)
|
|
|
|
|
|
|
|
def loadModuleHooks(self, module):
|
|
|
|
" activate a module's hooks "
|
|
|
|
for hook in module.hooks:
|
|
|
|
self.addHook(hook.hook, hook.method)
|
|
|
|
|
|
|
|
def unloadModuleHooks(self, module):
|
|
|
|
" remove a modules hooks "
|
|
|
|
for hook in module.hooks:
|
|
|
|
self.removeHook(hook.hook, hook.method)
|
|
|
|
|
|
|
|
def addHook(self, command, method):
|
|
|
|
" 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):
|
|
|
|
" 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
|
|
|
|
|
|
|
|
def getmodulebyname(self, name):
|
|
|
|
" return a module specified by the name "
|
|
|
|
if not name in self.moduleInstances:
|
|
|
|
return None
|
|
|
|
return self.moduleInstances[name]
|
|
|
|
|
|
|
|
def getmodulesbyservice(self, service):
|
|
|
|
" get a list of modules that provide the specified service "
|
|
|
|
validModules = []
|
|
|
|
for module in self.moduleInstances:
|
|
|
|
if service in self.moduleInstances[module].services:
|
|
|
|
validModules.append(self.moduleInstances[module])
|
|
|
|
return validModules
|
|
|
|
|
|
|
|
def getBestModuleForService(self, service):
|
|
|
|
m = self.getmodulesbyservice(service)
|
|
|
|
if len(m)>0:
|
|
|
|
return m[0]
|
|
|
|
return None
|
|
|
|
|
|
|
|
def closeAllModules(self):
|
|
|
|
" Deport all modules (for shutdown). Modules are unloaded in the opposite order listed in the config. "
|
|
|
|
loaded = list(self.moduleInstances.keys())
|
|
|
|
loadOrder = self.botconfig["modules"]
|
|
|
|
loadOrder.reverse()
|
|
|
|
for key in loadOrder:
|
|
|
|
if key in loaded:
|
|
|
|
loaded.remove(key)
|
|
|
|
self.deportmodule(key)
|
|
|
|
for key in loaded:
|
|
|
|
self.deportmodule(key)
|
|
|
|
|
|
|
|
" Filesystem Methods "
|
|
|
|
def getDataPath(self, moduleName):
|
|
|
|
return "%s/data/%s/" % (self.botconfig["bot"]["datadir"], moduleName)
|
|
|
|
|
|
|
|
def getConfigPath(self, moduleName):
|
|
|
|
return "%s/config/%s.yml" % (self.botconfig["bot"]["datadir"], moduleName)
|
|
|
|
|
|
|
|
" Utility methods "
|
|
|
|
@staticmethod
|
|
|
|
def decodePrefix(prefix):
|
|
|
|
" Returns an object with nick, username, hostname attributes"
|
|
|
|
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 traceback.format_exc()
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def messageHasCommand(command, message, requireArgs=False):
|
|
|
|
# Check if the message at least starts with the command
|
|
|
|
messageBeginning = message[0:len(command)]
|
|
|
|
if messageBeginning!=command:
|
|
|
|
return False
|
|
|
|
# Make sure it's not a subset of a longer command (ie .meme being set off by .memes)
|
|
|
|
subsetCheck = message[len(command):len(command)+1]
|
|
|
|
if subsetCheck!=" " and subsetCheck!="":
|
|
|
|
return False
|
|
|
|
|
|
|
|
# We've got the command! Do we need args?
|
|
|
|
argsStart = len(command)
|
|
|
|
args = ""
|
|
|
|
if argsStart > 0:
|
|
|
|
args = message[argsStart+1:]
|
|
|
|
|
|
|
|
if requireArgs and args.strip() == '':
|
|
|
|
return False
|
|
|
|
|
|
|
|
# Verified! Return the set.
|
|
|
|
ob = type('ParsedCommand', (object,), {})
|
|
|
|
ob.command = command
|
|
|
|
ob.args = [] if args=="" else args.split(" ")
|
|
|
|
ob.args_str = args
|
|
|
|
ob.message = message
|
|
|
|
return ob
|
|
|
|
# return (True, command, args, message)
|
|
|
|
|
|
|
|
|
|
|
|
" Data Methods "
|
|
|
|
def get_nick(self):
|
|
|
|
return self.config["nick"]
|
|
|
|
|
|
|
|
|
|
|
|
" Action Methods "
|
|
|
|
def act_PONG(self, data):
|
|
|
|
self.sendRaw("PONG :%s" % data)
|
|
|
|
|
|
|
|
def act_USER(self, username, hostname, realname):
|
|
|
|
self.sendRaw("USER %s %s %s :%s" % (username, hostname, self.botconfig["connection"]["server"], realname))
|
|
|
|
|
|
|
|
def act_NICK(self, newNick):
|
|
|
|
self.sendRaw("NICK %s" % newNick)
|
|
|
|
|
|
|
|
def act_JOIN(self, channel):
|
|
|
|
self.sendRaw("JOIN %s"%channel)
|
|
|
|
|
|
|
|
def act_PRIVMSG(self, towho, message):
|
|
|
|
self.sendRaw("PRIVMSG %s :%s"%(towho,message))
|
|
|
|
|
|
|
|
def act_MODE(self, channel, mode, extra=None):
|
|
|
|
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):
|
|
|
|
self.sendRaw("PRIVMSG %s :\x01ACTION %s"%(channel,action))
|
|
|
|
|
|
|
|
def act_KICK(self, channel, who, comment):
|
|
|
|
self.sendRaw("KICK %s %s :%s" % (channel, who, comment))
|
|
|
|
|