Plugin development: Creating plugins — Plugin API 1 — Plugin API 2 Main development: Core
Plugins: calc — papertrading — trivia
forkb0t.py
#!/usr/bin/env python # -*- coding: utf-8 -*- ### # forkb0t.py # Main forkb0t code ### # Copyright (C) 2008-2010, Ken Rushia (krushia), forkb0t@kenrushia.com # Copyright (C) 2008, Kenneth Prugh (Ken69267), ken69267@gentoo.org # Inspired by http://www.oreilly.com/pub/h/1968#code # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation under version 2 of the license. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the # Free Software Foundation, Inc., # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. ### import sys # The following line prevents python from making most *.pyc sys.dont_write_bytecode = False import socket #import string import threading import time #import traceback #import subprocess #from optparse import OptionParser import ConfigParser #import gc import Queue #import asyncore import plugger from ZODB import FileStorage, DB storage = FileStorage.FileStorage('test-filestorage.fs') db = DB(storage) # Below was used for debug logs up to 8 # gc.set_debug(gc.DEBUG_LEAK) #knownCommands = ['JOIN', 'KICK', 'LINKS', 'NAMES', 'NICK', 'NOTICE', 'PART', 'PING', 'PONG', 'PRIVMSG', 'QUIT', 'STATS'. 'VERSION', 'WHOIS'] ircQueue = Queue.Queue() #outQueue = Queue.Queue() class doIRC(threading.Thread): def __init__(self, name): self.reinit = False self.commander = '#b0tcage' self.skynet = 'freenode' self.ninja = plugger.pluggerThread() threading.Thread.__init__(self, name=name) def america(self, fuck_yeah, commander, skynet): if fuck_yeah != self.name: self.reinit = True self.commander = commander self.skynet = skynet def sendRaw(self, name, stuff): for taco in fls: if taco.name == name: for line in stuff.splitlines(True): taco.outQueue.put(line) break # Dedenting this costs 1 day of debugging def run(self): while 1: Options, name, msg = ircQueue.get() # NOTE: This msgtext is broken on IPv6 and should not be used # Reload codes really needs to be cleaned up # Better way would be to have plugger pick up !reload and signal main msgtext = msg.partition(':')[2].partition(':')[2] if self.reinit or msgtext.strip() in ["!reload", "+!reload"]: try: reload(plugger) self.ninja = plugger.pluggerThread() if not self.reinit: self.sendRaw(name, "PRIVMSG " + Options['debugchannel'] + " :["+self.name+"] Plugger reloaded. I am commanding other threads to reload now.\r\n") for noodle in fts: noodle.america(self.name, Options['debugchannel'], name) else: self.sendRaw(self.skynet, "PRIVMSG " + self.commander + " :["+self.name+"] Plugger reloaded.\r\n") self.reinit = False except: self.sendRaw(name, "PRIVMSG " + Options['debugchannel'] + " :["+self.name+"] Plugger reload FAILED\r\n") try: self.ninja.run(msg, Options, name, plugger.pluggar('plugins.conf'), zomgthefiles, db) #not a real thread... except: self.sendRaw(name, "PRIVMSG " + Options['debugchannel'] + " :["+self.name+"] Core caught an unhandled exception in plugger. I'd tell you more, but some asshole decided to delete backtrace code from core.\r\n") if self.ninja.out.strip(): self.sendRaw(self.ninja.network, self.ninja.out) ircQueue.task_done() class linkIRC(threading.Thread): def __init__(self, name, args): self.Options = args self.outQueue = Queue.Queue() #self.socket = socket.socket() self.logfile = open(name+'.log.txt', 'a', 0) self.logQueue = Queue.Queue() self.suicide = False self.reconnect = False threading.Thread.__init__(self, name=name) def doConnect(self): attempts = 1 while 1: try: self.socket = socket.socket() self.socket.connect((self.Options['host'], int(self.Options['port']))) break except socket.error: if attempts > 3: return False time.sleep(3) attempts += 1 #TODO: Insert PASS here self.outQueue.put("NICK %s\r\n" % self.Options['nick']) # Note the USER below is RFC2812 and varies significantly from original codebase self.outQueue.put("USER %s 0 * :%s\r\n" % (self.Options['nick'], self.Options['ident'])) #TODO: Check for RPL_WELCOME here self.outQueue.put("PRIVMSG NickServ :identify %s\r\n" % self.Options['password']) if self.Options['capab'].strip(): for i in self.Options['capab'].split(): self.outQueue.put('CAPAB ' + i + '\r\n') if self.Options['cap'].strip(): for i in self.Options['cap'].split(): self.outQueue.put('CAP REQ :' + i + '\r\n') for i in self.Options['channels'].split(): self.outQueue.put("JOIN %s\r\n" % i) #self.socket.settimeout(0.0) return True def run(self): while 1: if not self.doConnect(): # connect puked return inHandler = threading.Thread(target=self.doRead, args=(ircQueue,)) inHandler.start() outHandler = threading.Thread(target=self.doWrite, args=(self.outQueue,)) outHandler.start() while 1: try: logline = self.logQueue.get(block=True, timeout=10) except: if not inHandler.is_alive() and not outHandler.is_alive(): if self.suicide: # shutdown return elif self.reconnect: # reconnect self.reconnect = False break else: continue else: continue self.logfile.write(logline) self.logQueue.task_done() def doRead(self, iq): rbuffer = '' while 1: try: poppy = self.socket.recv(4096) except: if not self.suicide: self.reconnect = True self.log('SOCKET READ ERROR, RESTARTING\r\n', 2) self.socket.close() return if not poppy: continue rbuffer+=poppy if "\r\n" in rbuffer: temp=rbuffer.split("\r\n") if rbuffer.endswith("\r\n"): rbuffer="" # clear buffer else: rbuffer=temp.pop() # last element prolly incomplete... keep in buffer for msg in temp: if msg.strip(): self.log(msg+'\r\n', 0) iq.put((self.Options, self.name, msg)) def doWrite(self, oq): sbuffer = '' while 1: try: sbuffer = oq.get(block=True, timeout=10) except Queue.Empty: if self.reconnect: return continue # in-band signaling ftw :P if sbuffer == 'DIAF\r\n': self.suicide = True self.log('Suiciding cuz pluggar (we hope) said to\r\n', 2) oq.task_done() self.socket.close() return if sbuffer == 'PHOENIX\r\n': self.reconnect = True self.log('Restarting cuz pluggar (we hope) said to\r\n', 2) oq.task_done() self.socket.close() return try: self.socket.sendall(sbuffer) self.log(sbuffer, 1) oq.task_done() except: self.reconnect = True self.log('SOCKET WRITE ERROR, RESTARTING\r\n', 2) oq.task_done() self.socket.close() return def log(self, text, prefix): # 0 = in # 1 = out # 2 = error/info prefixes = [' ', ' >>>', ' ***'] self.logQueue.put(str(int(time.time()*1000.0))+prefixes[prefix]+text) class linkDCC(threading.Thread): def __init__(self, name, args): self.Options = args self.outQueue = Queue.Queue() #self.socket = socket.socket() self.logfile = open(name+'.log.txt', 'a', 0) self.logQueue = Queue.Queue() self.suicide = False threading.Thread.__init__(self, name=name) def doConnect(self): try: self.socket = socket.socket() self.socket.connect((self.Options['host'], int(self.Options['port']))) except socket.error: return False return True def run(self): while 1: if not self.doConnect(): # connect puked return inHandler = threading.Thread(target=self.doRead, args=(ircQueue,)) inHandler.start() outHandler = threading.Thread(target=self.doWrite, args=(self.outQueue,)) outHandler.start() while 1: try: logline = self.logQueue.get(block=True, timeout=10) except: if not inHandler.is_alive() and not outHandler.is_alive(): return continue self.logfile.write(logline) self.logQueue.task_done() def doRead(self, iq): rbuffer = '' while 1: if self.suicide: return try: poppy = self.socket.recv(4096) except: self.suicide = True self.log('SOCKET READ ERROR. CLOSING.\r\n', 2) return if not poppy: continue rbuffer+=poppy # Some DCC specs, in particular WHITEBOARD, allow either # "\r\n" or "\n" as line termination. if "\n" in rbuffer: temp=rbuffer.splitlines() if rbuffer.endswith("\n"): rbuffer="" # clear buffer else: # NOTE: Next line could cut off "\r", but shouldn't matter. rbuffer=temp.pop() # last element prolly incomplete... keep in buffer for msg in temp: if msg.strip(): self.log(msg+'\r\n', 0) iq.put((self.Options, self.name, msg)) def doWrite(self, oq): sbuffer = '' while 1: if self.suicide: return try: sbuffer = oq.get(block=True, timeout=10) except Queue.Empty: continue try: self.socket.sendall(sbuffer) self.log(sbuffer, 1) oq.task_done() except: self.suicide = True self.log('SOCKET WRITE ERROR. CLOSING.\r\n', 2) oq.task_done() return def log(self, text, prefix): # 0 = in # 1 = out # 2 = error/info prefixes = [' ', ' >>>', ' ***'] self.logQueue.put(str(int(time.time()*1000.0))+prefixes[prefix]+text) netconf = ConfigParser.SafeConfigParser() netconf.read('networks.conf') fls = [] # fork links fts = [] # fork threads zomgthefiles = threading.Lock() # lock for all plugin file access def connectNewNet(t, host, port): pass # Each network section in networks.conf results in two threads produced here. # 1. fl - forklink (link*) thread that handles sockets. This produces 2 subthreads. # 2. ft - forkthread (do*) thread that acts as a worker for n in netconf.sections(): # TODO: Make sense of this block... can it be simplified? nettable = {} All = netconf.items(n) for v in All: nettable[v[0]] = v[1] if nettable['type'] == 'irc': # Spawn a linkIRC object, which handles the connection to a network fl = linkIRC(name=n, args=nettable) fl.start() fls.append(fl) # Spawn a doIRC (or forkthread) object, which handles plugin processing # These threads are NOT linked to any specific forklink (is a worker pool) for ftid in range(2): # spawn 2 workers for every network ft = doIRC(name="irc thread %s"%ftid) ft.start() fts.append(ft) # main program sits here and exits after each forklink dies for fl in fls: fl.join()
plugger.py
#!/usr/bin/env python # -*- coding: utf-8 -*- ### # plugger.py # Part of forkb0t ### # Copyright (C) 2008-2010, Ken Rushia (krushia), forkb0t@kenrushia.com # Copyright (C) 2008, Kenneth Prugh (Ken69267), ken69267@gentoo.org # Inspired by http://www.oreilly.com/pub/h/1968#code # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation under version 2 of the license. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the # Free Software Foundation, Inc., # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. ### import sys import trace import traceback import subprocess import time import ConfigParser import collections import linecache import textwrap import os import itertools #Example plugin import if one was to put it up here # from plugins.gigablast import gigablast # TODO: # Make namedtuple usage cleaner (always use names rather than positional assignment, and try to remove redundantcy) # Add defaults and make all options optional # Sanity checks on data # Is there any reason for using namedtuple, besides converting to py types? class pluggar: def __init__(self, configfile): plugconf = ConfigParser.SafeConfigParser({'shblacklist': 'False', 'shgreylist': 'True', 'prefixnick': 'True', 'input': 'terms', 'debug': 'False', 'output': 'terms', 'api': '1', 'type': 'python', 'load': '', 'run': ''}) plugconf.read(configfile) self.plugwords = {} self.functable = {} self.plugalways = [] FuncOptionTuple = collections.namedtuple('FuncOptionTuple', 'shblacklist shgreylist prefixnick input debug output api type load run') for i in plugconf.sections(): for n in plugconf.get(i,'keywords').split(): if n is not '*': self.plugwords[n] = i else: self.plugalways.append(i) self.functable[i] = FuncOptionTuple(shblacklist=plugconf.getboolean(i,'shblacklist'),shgreylist=plugconf.getboolean(i,'shgreylist'),prefixnick=plugconf.getboolean(i,'prefixnick'),input=plugconf.get(i,'input'),debug=plugconf.getboolean(i,'debug'),output=plugconf.get(i,'output'),api=plugconf.getint(i,'api'),type=plugconf.get(i,'type'),load=plugconf.get(i,'load'),run=plugconf.get(i,'run')) class pluggerThread: def __init__(self): self.online = [] # not correct... should be in network data CAPAB_IDENTIFY_MSG = False self.identified = False self.out = '' def run(self, lmsg, net, name, pconf, zomgthefiles, db): self.out = '' self.pconf = pconf self.zomgthefiles = zomgthefiles self.Options = net self.name = name # will be a network from networks.conf, like "freenode" self.network = name #default to output same network self.msg = self.decode(lmsg) # Optimization globals... use instead of functions if possible self.msgtype, self.chan, self.nick, self.msgtext = self.findAll(self.msg) CAPAB_IDENTIFY_MSG = True # temporary, till we get data stuff if self.msgtype == '290': if "IDENTIFY-MSG" in self.msg: CAPAB_IDENTIFY_MSG = True self.identified = False # reset here every scan if CAPAB_IDENTIFY_MSG: if self.msgtype in ['PRIVMSG', 'NOTICE']: if self.msgtext[:1] in ["+", "-"]: if self.msgtext[:1] == "+": self.identified = True self.msgtext = self.msgtext[1:] # Somewhat out of place but critical... play pingpong with ircd if self.msgtype == 'PING': self.sendRaw("PONG %s\r\n" %self.msg.partition(':')[2]) # HAX - 2nd half of !names... read response fron nickserv # 353 RPL_NAMREPLY - defined in RFC1459 and RFC2812 # msgtext is same in both definitions, but header varies elif self.msgtype == '353': self.online = [] for name in self.msgtext.split(): if name.startswith('@') or name.startswith('+'): name = name[1:] if name not in self.online: self.online.append(name) # When someone leaves the channel, remove from online list elif self.msgtype in ['PART', 'QUIT']: if self.nick in self.online: self.online.remove(self.nick) elif self.msgtype == 'PRIVMSG': # If someone talks, they must be online. Add them to online list if missing. if self.nick not in self.online: self.online.append(self.nick) ######################################## # BEGIN PRIVMSG INTERNAL COMMAND CHECK # ######################################## # CTCP responses if self.msgtext.startswith("\001"): # chan == nick makes sure we do not send replies to channel # CTCP spammers. if "\001ACTION" not in self.msgtext and self.chan == self.nick: if not self.msgtext.endswith("\001"): self.spam("WARNING: CTCP from "+self.nick+"possibly malformed (doesn't end with \\001) - parsing anyway") if self.msgtext.startswith("\001CLIENTINFO"): self.sendRaw("NOTICE " + self.nick + " :\001CLIENTINFO ACTION CLIENTINFO PING TIME URL USERINFO VERSION\001\r\n") elif self.msgtext.startswith("\001PING"): self.sendRaw("NOTICE " + self.nick + " :" + self.msgtext + "\r\n") elif self.msgtext.startswith("\001TIME"): # Returned in RFC 2822 format per http://www.invlogic.com/irc/ctcp.html # well.. technically it calls for RFC 822, which differs by using 2-digit year # however, it seems most IRC clients use 4-digit, so we should be safe # ... # On the other hand, irssi and Konversation return a different format, # http://www.irchelp.org/irchelp/rfc/ctcpspec.html but sans timezone! self.sendRaw("NOTICE " + self.nick + " :\001TIME " +time.strftime("%a, %d %b %Y %H:%M:%S %z")+"\001\r\n") elif self.msgtext.startswith("\001URL"): self.sendRaw("NOTICE " + self.nick + " :\001URL http://www.gentoo-pr0n.org/forkb0t:forkb0t\001\r\n") elif self.msgtext.startswith("\001USERINFO"): self.sendRaw("NOTICE " + self.nick + " :\001USERINFO A friendly b0t based in the #gentoo-pr0n channel on Freenode. Owner is krushia on IRC, who can also be contacted at forkb0t@kenrushia.com\001\r\n") elif self.msgtext.startswith("\001VERSION"): # Note we go by http://www.invlogic.com/irc/ctcp.html # somewhat different than http://www.irchelp.org/irchelp/rfc/ctcpspec.html # also we don't quote self.sendRaw("NOTICE " + self.nick + " :\001VERSION forkb0t 0.1 - by krushia\001\r\n") elif self.msgtext.startswith("\001DCC CHAT"): a, b, c = self.msgtext.partition('CHAT')[2].strip().split() self.spam("GOT CTCP DCC CHAT from "+self.nick+". protocol: "+a+" ip: "+self.unDccIP(int(b))+" port: "+c) else: self.spam("Unknown CTCP command... "+self.msgtext.partition("\001")[2].partition("\001")[0]) # HAX - !rawforksay command... should be converted to plugin elif self.msgtext.startswith("!rawforksay"): try: if self.identified and self.nick == self.Options['master']: terms = self.msg.partition('!rawforksay ')[2] self.sendRaw(terms+'\r\n') except: self.spam("rawforksay failure", True) # HAX - !rawforksay command... should be converted to plugin elif self.msgtext.startswith("!netstat"): try: if self.identified and self.nick == self.Options['master']: self.network = self.msg.partition('!netstat ')[2] self.sendRaw('LINKS\r\n') return except: self.spam("netstat failure", True) # !forkhelp can't be a plugin because plugins can't access the plugin list elif self.msgtext.startswith("!forkhelp"): if self.msgtext.strip() != '!forkhelp': try: temp = self.msgtext.split()[1] if temp in self.pconf.plugwords: self.msgtext=str(temp)+' --help' else: self.say(self.chan, '''You're a dumbass''') except: pass else: try: self.say(self.chan, '''I know the following commands. To get help for a specific command, type "!command --help"''') plugwords = [] for i in self.pconf.plugwords: plugwords.append(i) plugwords.sort() self.say(self.chan, ' '.join(plugwords)) #for i in plugwords: # helptext = helptext + i + " " #helptext = helptext + """\nThere are some cute operators:\n"!command >>> user" produces "user: output"\n"!command >>>> channel" pipes output to channel\n"!command1 <!command2" executes command2 and passes output as arguments to command1\nI also know some things that aren't plugins yet. These include !forkhelp, !rawforksay, and !netstat""" self.say(self.chan, "More information online at http://www.gentoo-pr0n.org/forkb0t:forkb0t") except: self.spam("forkhelp failed", True) # !forkdev replug reloads the specified plugin, or all plugins # iirc, the return is to kill a possible doomsday recursion scenario elif self.msgtext.startswith("!forkdev replug"): output = self.replug(self.msgtext.partition('!forkdev replug')[2].strip()) self.say(self.chan, output) return # This line is what starts the plugin parsing everytime pandas talk if self.msgtype == 'PRIVMSG': # BUG: forkb0tswdfkjhnshwidtvst@#$*&%@(#$5fmf234c2#& !wat if self.msgtext.startswith(self.Options['nick']): # the try... except was added cuz ed found a way to crash with: # :quiznilo!~CC@unaffiliated/ed-209 PRIVMSG #gentoo-pr0n :+forkb0t: try: haxmsgtext = self.msgtext.split(None, 1)[1] except: haxmsgtext = self.msgtext else: haxmsgtext = self.msgtext # The if statement below was added so all whitespace # msgtext doesn't implode on split if haxmsgtext.strip() != '': for keyword in self.pconf.plugwords: if haxmsgtext.split()[0] == keyword: self.doTopPlug(pconf.plugwords[keyword], self.msg, self.msgtext, keyword) for plugin in self.pconf.plugalways: self.doTopPlug(plugin, self.msg, self.msgtext, "*") else: # NOTE: In future, add support for non PM regex keywords for plugin in self.pconf.plugalways: if self.pconf.functable[plugin].input == "raw": self.doTopPlug(plugin, self.msg, self.msgtext, "*") # I don't think this does much. Only needed if there are massively naughty plugins. def decode(self, bytes): if bytes is None: return bytes try: text = bytes.decode('utf-8','replace') if text != bytes: try: deedeedeecode = self.loadPlugin('encoding') self.spam("WARNING: decode() did some magic with utf-8. The original bytes are monkeyshit.... " + deedeedeecode(bytes)) except: self.spam("I failed at catching fail", True, True) except: try: text = bytes.decode('iso-8859-1') except: try: text = bytes.decode('cp1252') except: #self.spam("EPIC WARNING: decode() is useless", True) text = bytes return text # This defines the debug trace spam that forkb0t hurls def formatExceptionInfo(self, maxTBlevel=5): cla, exc, trbk = sys.exc_info() excName = cla.__name__ try: excArgs = str(exc) except: try: excArgs = exc.__dict__["args"] except KeyError: excArgs = "<no args>" excTb = traceback.format_tb(trbk, maxTBlevel) return (excName, excArgs, excTb) # Reloads plugin modules def replug(self, plugin=''): try: self.pconf = pluggar('plugins.conf') plugin1count = 0 plugin2count = 0 errors = 0 if plugin == '': repluglist = self.pconf.functable else: repluglist = [str(plugin)] for pluginText in repluglist: if self.pconf.functable[pluginText].api == 1: plugin1count += 1 # To update line numbers linecache.checkcache("plugins/"+pluginText+"/"+pluginText+".py") if self.loadPlugin(pluginText, doreload=True) is None: errors += 1 else: plugin2count += 1 command = self.pconf.functable[pluginText].load if command == '': if self.pconf.functable[plugin].type == 'python': continue else: self.spam('Pluggar failed to reload plugin ' + pluginText + ' because the load setting in ' + pluginText + '.conf appears to be empty. If this is intentional, please inform krushia') errors += 1 continue loadproc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) pipe, err = loadproc.communicate() if loadproc.returncode != 0: self.spam("Pluggar failed to reload plugin " + pluginText + " with load command " + command) self.spam("Returncode " + str(loadproc.returncode) + " -- stderr follows") for el in str(err).splitlines(): self.spam(el) errors += 1 linecache.checkcache() if errors == 0: return "Successfully replugged " + str(plugin2count) + " API v2, and " + str(plugin1count) + " legacy API v1 plugin(s)" else: return "Replug attempt had " + str(errors) + " error(s). Details in " + self.Options['debugchannel'] except: self.spam("Horrible replug() failure, with passed plugin=" + str(plugin), True, True) return "EPIC FAIL" def loadPlugin(self, plugin, function='', doreload=False): if function == '': function = plugin try: # Will return plugins.foo, where foo is a package # On filesystem, this is a directory ./plugins/foo opackage = __import__("plugins." + plugin, globals(), locals(), [plugin], -1) except: self.spam("Pluggar fails at finding package for plugin " + plugin, True) return None if doreload: try: reload(opackage) except: self.spam("Pluggar failed to reload package for plugin " + plugin, True) return None try: # Will return plugins.foo.foo, where foo is a module # On filesystem, this is a file ./plugins/foo/foo.py omodule = getattr(opackage, plugin) except: self.spam("Pluggar fails at finding module for plugin " + plugin, True) return None if doreload: try: reload(omodule) except: self.spam("Pluggar failed to reload module for plugin " + plugin, True) return None try: # Will return plugins.foo.foo.bar, where bar is a function # On filesystem, this is the function bar() in ./plugins/foo/foo.py ofunction = getattr(omodule, function) return ofunction except: self.spam("Pluggar fails at finding function named " + function + " in module for plugin " + plugin, True) return None # New complete message parser # Replaces findType(), findChannel(), and findNick() def findAll(self, msg): headerdict = {} if msg.startswith(':'): splitmsg = msg.split(None, 2) prefix = splitmsg[0][1:] if '!' in prefix: nickname, temp = prefix.split('!') user, host = temp.split('@') rnick = nickname # remove when forky gets smart elif '@' in prefix: nickname, host = prefix.split('@') rnick = nickname # remove when forky gets smart else: servername = prefix rnick = servername # remove when forky gets smart splitmsg.pop(0) else: splitmsg = msg.split(None, 1) rnick = 'dumb0t' # remove when forky gets smart command = splitmsg[0] rmsgtext = splitmsg[1] # remove when forky gets smart if ':' in splitmsg[1]: a, b, c = splitmsg[1].partition(':') if a == '': params = [c] else: params = a.split() if c != '': params.append(c) else: params = splitmsg[1].split() # A note on the naming of paramdict entries... # While writing this function, I had the RFC open and was making # sure that it was followed 100%. To this end, I decided to use the # exact naming from the RFC for parameters to commands. # ... it seemed like a good idea at first ... # However, it turns out that the RFC was written by those who aren't # gifted in the art of technical writing, as you can plainly see in # the code below. There is an annoying lack of consistency in format # of names. Most astounding are "text" for PRIVMSG and # "text to be sent" for NOTICE. Also confusion of "user" and "nickname" paramdict = {} # Not implemented: # OPER, SERVICE, SQUIT, NAMES, LIST, if command == 'PASS': # Not from server paramdict['password'] = params[0] elif command == 'NICK': paramdict['nickname'] = params[0] elif command == 'USER': # Not from server paramdict['user'] = params[0] paramdict['mode'] = params[1] paramdict['unused'] = params[2] paramdict['realname'] = params[3] elif command == 'MODE': pass #if params[0] == nickname: # Not from server # paramdict['nickname'] = params[0] # rest are a list of mode changes #if params[0] != nickname: # paramdict['channel'] = params[0] # rest is a bunch of stuff to uberparse elif command == 'QUIT': pass #paramdict['quit_message'] = params[0] # Note that quit message is optional # insert netsplit checks here elif command == 'JOIN': # Note we don't check lists, since RFC says servers should not return them paramdict['channel'] = params[0] elif command == 'PART': paramdict['channel'] = params[0] #paramdict['part_message'] = params[1] # Note that part message is optional elif command == 'TOPIC': paramdict['channel'] = params[0] paramdict['topic'] = params[1] elif command == 'INVITE': paramdict['nickname'] = params[0] paramdict['channel'] = params[1] elif command == 'KICK': paramdict['channel'] = params[0] paramdict['user'] = params[1] #paramdict['comment'] = params[2] # comment is optional elif command == 'PRIVMSG': paramdict['msgtarget'] = params[0] paramdict['text_to_send'] = params[1] rmsgtext = paramdict['text_to_send'] # remove when forky gets smart elif command == 'NOTICE': paramdict['msgtarget'] = params[0] paramdict['text'] = params[1] rmsgtext = paramdict['text'] # remove when forky gets smart elif command == 'PING': paramdict['server1'] = params[0] # we don't check for server2 elif command == 'ERROR': #should only get when connection is terminated paramdict['error_message'] = params[0] try: # remove when forky gets smart rchan = paramdict['channel'] # remove when forky gets smart except: # remove when forky gets smart try: # remove when forky gets smart rchan = paramdict['msgtarget'] # remove when forky gets smart if self.Options['nick'] in rchan: # remove when forky gets smart rchan = rnick # remove when forky gets smart except: # remove when forky gets smart rchan = self.Options['debugchannel'] # remove when forky gets smart return command, rchan, rnick, rmsgtext # Print debuggin information for fails. The name makes sense if you monitor #b0tcage # NOTE: Might not need all the str() - they are there from transition from %s # NOTE: say() uses string.encode('utf-8') on output, but spam() doesn't... BUG? def spam(self, text, trace=False, fulltrace=False): self.sendRaw("PRIVMSG " + self.Options['debugchannel'] + " :" + str(text) + "\r\n") if trace: try: e, a, t = self.formatExceptionInfo() self.sendRaw("PRIVMSG "+self.Options['debugchannel']+" : "+ str(e) +" --- "+str(a)+"\r\n") for line in t: # NOTE: line isn't really line, it is a bt point. Needs work. self.sendRaw("PRIVMSG " + self.Options['debugchannel'] + " : " + str(line).splitlines()[0] + "\r\n") if ( not fulltrace ) and ( "forkb0t" not in line ): # stop after the backtrace leaves our source time.sleep(3) # should remove... proper socket level flood control instead return except: self.sendRaw("PRIVMSG "+self.Options['debugchannel']+" : FAILED TO GET TRACEBACK\r\n") # Create a raw IRC PRIVMSG line that sends text to destination # TODO: Properly handle text with multiple lines def say(self, destination, text, prefix=""): for line in text.splitlines(): if not line: continue command = "PRIVMSG" lprefix = prefix ldestination = destination # ACTION method is depreciated # Make sure pandas don't use it in new plugins! if line.startswith("ACTION"): self.spam("WARNING: Depreciated ACTION used in plugin output") lprefix = "" lbody = "\001ACTION" + line.partition('ACTION')[2] + "\001" elif line.startswith("/me"): lprefix = "" lbody = "\001ACTION" + line.partition('/me')[2] + "\001" elif line.startswith("/notice"): command = "NOTICE" lprefix = "" ldestination = line.split()[1] lbody = line.partition(ldestination)[2].strip() elif line.startswith("/ctcp"): lprefix = "" ldestination = line.split()[1] lbody = "\001" + line.partition(ldestination)[2].strip() + "\001" elif line.startswith("/nctcp"): command = "NOTICE" lprefix = "" ldestination = line.split()[1] lbody = "\001" + line.partition(ldestination)[2].strip() + "\001" # note, /msg code should be a function? elif line.startswith("/msg"): prefix = "" lprefix = "" destination = line.split()[1] ldestination = destination lbody = line.partition(ldestination)[2].strip() elif line.startswith("/macro"): continue elif line.startswith("/core"): if 'replug' in line.partition('/core')[2]: if len(line.split()) >= 3: lbody = self.replug(line.split()[2]) else: lbody = self.replug('') else: self.spam('A plugin specified invalid /core call: ' + line.partition('/core')[2]) return elif line.startswith("/say"): lbody = line.partition('/say')[2].lstrip() else: lbody = line if "" != lprefix != ldestination and ldestination.startswith("#"): lprefix += ": " else: lprefix = "" # The True added at end cuz too lazy to add superuser bypass here if ldestination in self.online or ldestination in self.Options['channels'].split() or True: a = command + " " + ldestination + " :" + lprefix for z in textwrap.wrap(lbody,512-(len(a)+3+36)): tosend = a + z + "\r\n" self.sendRaw(tosend.encode('utf-8')) else: self.spam('A plugin tried to /msg invalid target: ' + ldestination) return def sendRaw(self, msg): self.out+=msg # TODO: I have no idea where this was headed... def report(self, msg): rtime = str(int(time.time()+1000.0)) self.spam('Autogenerating bug report...') import sys rfile = open('report_'+rtime+'.txt', 'a', 0) rfile.write('Report\n') rfile.write('\n\nLoaded Modules\n') for i in sys.modules: rfile.write('%s\n'%i) # Ugly shell exploit checker. Really needs to be abstracted better. def checkHaxors(self, lmsgtext, black, grey): if black: for i in ["$", "`", "|", ";", "&", "~", "<<", ">>", '\'\'', '\"', '\\']: if i in lmsgtext: return "This command doesn't allow " + i + " for safety reasons." if grey: for i in [">", "<", "&", "%", "*", "/"]: if i in lmsgtext: return "This command is configured for stringent safety checks. The character(s) " + i + " are not allowed." return False def doTopPlug(self, plugin, lmsg, lmsgtext, keyword): exportTo = self.chan if self.pconf.functable[plugin].prefixnick: writeTo = self.nick else: writeTo = "" superuser = False master = False if self.identified: if self.nick in self.Options['superusers'].split(): superuser = True if self.nick == self.Options['master']: master = True # 1 ONE-TIME PARSE PER MSG # Parse redirection operators if keyword is not "*": if '>>>>>' in lmsgtext: self.network = lmsg.partition('>>>>>')[2].strip() lmsg = lmsg.partition('>>>>>')[0].strip() lmsgtext = lmsgtext.partition('>>>>>')[0].strip() if '>>>>' in lmsgtext: # note that the validation here is redundant... see self.say() if lmsg.partition('>>>>')[2].strip() in self.online or lmsg.partition('>>>>')[2].strip() in self.Options['channels'].split() or superuser: exportTo = lmsg.partition('>>>>')[2].strip() lmsg = lmsg.partition('>>>>')[0].strip() lmsgtext = lmsgtext.partition('>>>>')[0].strip() writeTo = "" else: self.say(self.chan, '''I either don't know or am not allowed to redirect to ''' + lmsg.partition('>>>>')[2].strip()) self.spam('Failed an exportTo') return if '>>>' in lmsgtext: writeTo = lmsg.partition('>>>')[2].strip() lmsg = lmsg.partition('>>>')[0].strip() lmsgtext = lmsgtext.partition('>>>')[0].strip() # 2 INSERT PIPING AND COMMAND SUBSTITUTION HERE # 3 CODE RUNS FOR EVERY COMMAND (MAKE IT HAPPEN) # Set plugin environment variables # NOTE: This is code prevents us from threading msg parsing self.zomgthefiles.acquire() penv = ConfigParser.SafeConfigParser() penv.read('plugin.env') if not penv.has_section('msg'): penv.add_section('msg') penv.set('msg', 'network', self.name.encode('unicode_escape')) penv.set('msg', 'nick', self.nick.encode('unicode_escape')) penv.set('msg', 'channel', self.chan.encode('unicode_escape')) penv.set('msg', 'keyword', keyword.encode('unicode_escape')) penv.set('msg', 'plugin', plugin.encode('unicode_escape')) penv.set('msg', 'identified', str(self.identified).encode('unicode_escape')) # Note that following line was redundant with doPlugKeyword if keyword is not "*": # Changed from lstrip to partition, old code on next line # lmsgtext = lmsgtext.lstrip(keyword).strip() lmsgtext = lmsgtext.partition(keyword)[2].strip() # BUG: Usually get exceptions here when % in values try: penv.set('msg', 'terms', lmsgtext.encode('unicode_escape')) except: penv.set('msg', 'terms', ' '.encode('unicode_escape')) try: penv.set('msg', 'raw', lmsg.encode('unicode_escape')) except: penv.set('msg', 'raw', ' '.encode('unicode_escape')) penv.set('msg', 'superuser', str(superuser).encode('unicode_escape')) penv.set('msg', 'master', str(master).encode('unicode_escape')) with open('plugin.env', 'wb') as configfile: penv.write(configfile) # Run plugin output = self.doPlugKeyword(plugin, lmsg, lmsgtext, keyword) self.zomgthefiles.release() # 3.5 (?) PARSE OF OUTPUT FROM EACH COMMAND # 4 ONE-TIME PARSE OF FINAL OUTPUT # NOTE: Need to put a flood detector here if output is None: return # Apply personal preferences (noprefixnick) if writeTo == self.nick: parsedmyconf = ConfigParser.SafeConfigParser() parsedmyconf.read('parsed.my.conf') if parsedmyconf.has_section(self.nick): if parsedmyconf.has_option(self.nick, 'noprefixnick'): if parsedmyconf.getboolean(self.nick, 'noprefixnick'): writeTo = "" self.say(exportTo, output, writeTo) # This huge mutha is waht runs a plugin. It keeps getting bigger. WTF. def doPlugKeyword(self, plugin, raw, lmsgtext, keyword): grey = self.pconf.functable[plugin].shgreylist black = self.pconf.functable[plugin].shblacklist if self.pconf.functable[plugin].api == 2: grey = True black = True haxresult = self.checkHaxors(lmsgtext, black, grey) if haxresult: if keyword is not "*": return haxresult return try: if self.pconf.functable[plugin].api == 1: if self.pconf.functable[plugin].input == "raw": return self.runPlugin(plugin, raw, keyword, self.pconf.functable[plugin].debug) else: return self.runPlugin(plugin, lmsgtext, keyword, self.pconf.functable[plugin].debug) else: return self.runPlugin2(plugin, lmsgtext, keyword, self.pconf.functable[plugin].debug) except: # NOTE: This exception catches problems in runPlugin, NOT plugins themselves if keyword is not "*": self.say(self.chan, "I accidentally teh " + keyword + ". Details in " + self.Options['debugchannel']) self.spam("Failed doPlugKeyword for " + keyword + " (plugin " + plugin + ")", True) def runPlugin2(self, plugin, text, keyword, debug): command = self.pconf.functable[plugin].run if command == '': if self.pconf.functable[plugin].type == 'python': command = "python -B /home/forkb0t/forkb0t/plugins/" + plugin + "/" + plugin + ".py '" + text + "'" else: return 'Accident (FIXME - runPlugin2)' if plugin == 'dst': command += " '" + self.nick + "' '" + text + "'" plugproc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) pipe, err = plugproc.communicate() if plugproc.returncode != 0: self.say(self.chan, "I accidentally teh %s (plugin %s). Details in %s" %(keyword, plugin, self.Options['debugchannel'])) if debug: self.spam("Failed to run plugin %s (keyword %s) *EXTENDED DEBUGGING ENABLED*" %(plugin, keyword), True, True) else: self.spam("Failed to run plugin %s (keyword %s)" %(plugin, keyword), True) self.spam("Returncode " + str(plugproc.returncode) + " -- stderr follows", False) for el in str(err).splitlines(): self.spam(el, False) funcout = '' # to avoid second accident at next return return pipe def runPlugin(self, plugin, text, keyword, debug): functext = plugin if keyword is not "*": # NOTE: Plugin help() is going to be removed in the future if text in ['-h', '-?', '--help']: functext = 'help' text = keyword func = self.loadPlugin(plugin, function=functext) if func is None: self.spam("Used bailout clause in runPlugin, functext=" + str(functext)) return if debug: command = "bash forkdev-debug-setup.sh " + plugin pipe = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True).communicate()[0] tf = open(plugin+'.trace.txt', 'w', 0) sys.stdout = tf tracer = trace.Trace(ignoredirs=[sys.prefix, sys.exec_prefix], timing=True) try: if debug: funcout = tracer.runfunc(func, text) else: funcout = func(text) except: if keyword is not "*": self.say(self.chan, "I accidentally teh %s (plugin %s). Details in %s" %(keyword, plugin, self.Options['debugchannel'])) if debug: self.spam("Failed to run plugin %s (keyword %s) *EXTENDED DEBUGGING ENABLED*" %(plugin, keyword), True, True) else: self.spam("Failed to run plugin %s (keyword %s)" %(plugin, keyword), True) funcout = '' # to avoid second accident at next return if debug: sys.stdout = sys.__stdout__ command = "bash forkdev-debug-postrun.sh " + plugin pipe = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True).communicate()[0] return self.decode(funcout) # Function borrowed from supybot ircutils.py # commit 6135a88741fcafa49bb2bd768cfc971cd7d58b5e def dccIP(self, ip): """Converts an IP string to the DCC integer form.""" i = 0 x = 256**3 for quad in ip.split('.'): i += int(quad)*x x /= 256 return i # Function borrowed from supybot ircutils.py # commit 6135a88741fcafa49bb2bd768cfc971cd7d58b5e def unDccIP(self, i): """Takes an integer DCC IP and return a normal string IP.""" L = [] while len(L) < 4: L.append(i % 256) i /= 256 L.reverse() return '.'.join(itertools.imap(str, L))
Back to top