123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597 |
- #!/usr/bin/env python
- #
- # see version.py for version
- # works with python 2.6.x and 2.7.x
- #
- import sys
- import socket
- import string
- import os
- import datetime
- import time
- import select
- import traceback
- import threading
- import inspect
- import argparse
- import botbrain
- from logger import Logger
- import confman
- from event import Event
- import util
- DEBUG = False
- RETRY_COUNTER = 0
- class Bot(threading.Thread):
- """
- bot instance. one bot gets instantiated per network, as an entirely distinct, sandboxed thread.
- handles the core IRC protocol stuff, and passing lines to defined events, which dispatch to their subscribed modules.
- """
- def __init__(self, conf=None, network=None, d=None):
- threading.Thread.__init__(self)
- self.DEBUG = d
- self.brain = None
- self.network = network
- self.OFFLINE = False
- self.CONNECTED = False
- self.JOINED = False
- self.conf = conf
- self.pid = os.getpid()
- self.logger = Logger()
- # to be a dict of dicts
- self.command_function_map = dict()
-
- if self.conf.getDBType() == "sqlite":
- import lite
- self.db = lite.SqliteDB(self)
- else:
- import db
- self.db = db.DB(self)
- self.NICK = self.conf.getNick(self.network)
- self.logger.write(Logger.INFO, "\n", self.NICK)
- self.logger.write(Logger.INFO, " initializing bot, pid " + str(os.getpid()), self.NICK)
- # arbitrary key/value store for modules
- # they should be 'namespaced' like bot.mem_store.module_name
- self.mem_store = dict()
- self.CHANNELINIT = conf.getChannels(self.network)
- # this will be the socket
- self.s = None # each bot thread holds its own socket open to the network
- self.brain = botbrain.BotBrain(self.send, self)
- self.events_list = list()
- # define events here and add them to the events_list
- all_lines = Event("1__all_lines__")
- all_lines.define(".*")
- self.events_list.append(all_lines)
- implying = Event("__implying__")
- implying.define(">")
- #command = Event("__command__")
- # this is an example of passing in a regular expression to the event definition
- #command.define("fo.bar")
- lastfm = Event("__.lastfm__")
- lastfm.define(".lastfm")
- dance = Event("__.dance__")
- dance.define("\.dance")
- #unloads = Event("__module__")
- #unloads.define("^\.module")
- pimp = Event("__pimp__")
- pimp.define("\.pimp")
- bofh = Event("__.bofh__")
- bofh.define("\.bofh")
- #youtube = Event("__youtubes__")
- #youtube.define("youtube.com[\S]+")
- weather = Event("__.weather__")
- weather.define("\.weather")
- steam = Event("__.steam__")
- steam.define("\.steam")
- part = Event("__.part__")
- part.define("part")
- tell = Event("__privmsg__")
- tell.define("PRIVMSG")
- links = Event("__urls__")
- links.define("https?://*")
- # example
- # test = Event("__test__")
- # test.define(msg_definition="^\.test")
- # add your defined events here
- # tell your friends
- self.events_list.append(lastfm)
- self.events_list.append(dance)
- self.events_list.append(pimp)
- #self.events_list.append(youtube)
- self.events_list.append(bofh)
- self.events_list.append(weather)
- self.events_list.append(steam)
- self.events_list.append(part)
- self.events_list.append(tell)
- self.events_list.append(links)
- #self.events_list.append(unloads)
- # example
- # self.events_list.append(test)
- self.load_modules()
- self.logger.write(Logger.INFO, "bot initialized.", self.NICK)
- # conditionally subscribe to events list or add event to listing
- def register_event(self, event, module):
- """
- Allows for dynamic, asynchronous event creation. To be used by modules, mostly, to define their own events in their initialization.
- Prevents multiple of the same _type of event being registered.
- Args:
- event: an event object to be registered with the bot
- module: calling module; ensures the calling module can be subscribed to the event if it is not already.
- Returns:
- nothing.
- """
- for e in self.events_list:
- if e.definition == event.definition and e._type == event._type:
- # if our event is already in the listing, don't add it again, just have our module subscribe
- e.subscribe(module)
- return
- self.events_list.append(event)
- return
- def load_snippets(self):
- import imp
- snippets_path = self.modules_path + '/snippets'
- self.snippets_list = set()
- # load up snippets first
- for filename in os.listdir(snippets_path):
- name, ext = os.path.splitext(filename)
- try:
- if ext == ".py":
- # snippet is a module
- snippet = imp.load_source(name, snippets_path + '/' + filename)
- self.snippets_list.add(snippet)
- except Exception, e:
- print e
- print name, filename
- def set_snippets(self):
- """
- check each snippet for a function with a list of commands in it
- create a big ol list of dictionaries, commands mapping to the functions to call if the command is encountered
- """
- for obj in self.snippets_list:
- for k,v in inspect.getmembers(obj, inspect.isfunction):
- if inspect.isfunction(v) and hasattr(v, 'commands'):
- for c in v.commands:
- if not c in self.command_function_map:
- self.command_function_map[c] = dict()
- self.command_function_map[c] = v
- # utility function for loading modules; can be called by modules themselves
- def load_modules(self, specific=None):
- """
- Run through the ${bot_dir}/modules directory, dynamically instantiating each module as it goes.
- Args:
- specific: string name of module. if it is specified, the function attempts to load the named module.
- Returns:
- 1 if successful, 0 on failure. In keeping with the perverse reversal of UNIX programs and boolean values.
- """
- nonspecific = False
- found = False
- self.loaded_modules = list()
- modules_dir_list = list()
- tmp_list = list()
-
- self.modules_path = 'modules'
- modules_path = 'modules'
- self.autoload_path = 'modules/autoloads'
- autoload_path = 'modules/autoloads'
- # this is magic.
- import os, imp, json
- self.load_snippets()
- self.set_snippets()
- dir_list = os.listdir(modules_path)
- mods = {}
- autoloads = {}
- #load autoloads if it exists
- if os.path.isfile(autoload_path):
- self.logger.write(Logger.INFO, "Found autoloads file", self.NICK)
- try:
- autoloads = json.load(open(autoload_path))
- #logging
- for k in autoloads.keys():
- self.logger.write(Logger.INFO, "Autoloads found for network " + k, self.NICK)
- except IOError:
- self.logger.write(Logger.ERROR, "Could not load autoloads file.",self.NICK)
- # create dictionary of things in the modules directory to load
- for fname in dir_list:
- name, ext = os.path.splitext(fname)
- if specific is None:
- nonspecific = True
- # ignore compiled python and __init__ files.
- #choose to either load all .py files or, available, just ones specified in autoloads
- if self.network not in autoloads.keys(): # if autoload does not specify for this network
- if ext == '.py' and not name == '__init__':
- f, filename, descr = imp.find_module(name, [modules_path])
- mods[name] = imp.load_module(name, f, filename, descr)
- self.logger.write(Logger.INFO, " loaded " + name + " for network " + self.network, self.NICK)
- else: # follow autoload's direction
- if ext == '.py' and not name == '__init__':
- if name == 'module':
- f, filename, descr = imp.find_module(name, [modules_path])
- mods[name] = imp.load_module(name, f, filename, descr)
- self.logger.write(Logger.INFO, " loaded " + name + " for network " + self.network, self.NICK)
- elif ('include' in autoloads[self.network] and name in autoloads[self.network]['include']) or ('exclude' in autoloads[self.network] and name not in autoloads[self.network]['exclude']):
- f, filename, descr = imp.find_module(name, [modules_path])
- mods[name] = imp.load_module(name, f, filename, descr)
- self.logger.write(Logger.INFO, " loaded " + name + " for network " + self.network, self.NICK)
- else:
- if name == specific: # we're reloading only one module
- if ext != '.pyc': # ignore compiled
- f, filename, descr = imp.find_module(name, [modules_path])
- mods[name] = imp.load_module(name, f, filename, descr)
- found = True
- for k,v in mods.iteritems():
- for name in dir(v):
- obj = getattr(mods[k], name) # get the object from the namespace of 'mods'
- try:
- if inspect.isclass(obj): # it's a class definition, initialize it
- a = obj(self.events_list, self.send, self, self.say) # now we're passing in a reference to the calling bot
- if a not in self.loaded_modules: # don't add in multiple copies
- self.loaded_modules.append(a)
- except TypeError:
- pass
- if nonspecific is True or found is True:
- return 0
- else: return 1
- # end magic.
-
- def send(self, message):
- """
- Simply sends the specified message to the socket. Which should be our connected server.
- We parse our own lines, as well, in case we want an event triggered by something we say.
- If debug is True, we also print out a pretty thing to console.
- Args:
- message: string, sent directly and without manipulation (besides UTF-8ing it) to the server.
- """
- if self.OFFLINE:
- self.debug_print(util.bcolors.YELLOW + " >> " + util.bcolors.ENDC + self.getName() + ": " + message.encode('utf-8', 'ignore'))
- else:
- if self.DEBUG is True:
- self.logger.write(Logger.INFO, "DEBUGGING OUTPUT", self.NICK)
- self.logger.write(Logger.INFO, self.getName() + " " + message.encode('utf-8', 'ignore'), self.NICK)
- self.debug_print(util.bcolors.OKGREEN + ">> " + util.bcolors.ENDC + ": " + " " + message.encode('utf-8', 'ignore'))
- self.s.send(message.encode('utf-8', 'ignore'))
- target = message.split()[1]
- if target.startswith("#"):
- self.processline(':' + self.conf.getNick(self.network) + '!~' + self.conf.getNick(self.network) + '@fakehost.here ' + message.rstrip())
- def pong(self, response):
- """
- Keepalive heartbeat for IRC protocol. Until someone changes the IRC spec, don't modify this.
- """
- self.send('PONG ' + response + '\n')
- def processline(self, line):
- """
- Grab newline-delineated lines sent to us, and determine what to do with them.
- This function handles our initial low-level IRC stuff, as well; if we haven't joined, it waits for the MOTD message (or message indicating there isn't one) and then issues our own JOIN calls.
- Also immediately passes off PING messages to PONG.
- Args:
- line: string.
- """
- if self.DEBUG:
- import datetime
- if os.name == "posix": # because windows doesn't like the color codes.
- self.debug_print(util.bcolors.OKBLUE + "<< " + util.bcolors.ENDC + line)
- else:
- self.debug_print("<< " + ": " + line)
- message_number = line.split()[1]
- try:
- first_word = line.split(":", 2)[2].split()[0]
- channel = line.split()[2]
- except IndexError:
- pass
- else:
- if first_word in self.command_function_map:
- self.command_function_map[first_word](self, line, channel)
- try:
- for e in self.events_list:
- if e.matches(line):
- e.notifySubscribers(line)
- # don't bother going any further if it's a PING/PONG request
- if line.startswith("PING"):
- ping_response_line = line.split(":", 1)
- self.pong(ping_response_line[1])
- # pings we respond to directly. everything else...
- else:
- # patch contributed by github.com/thekanbo
- if self.JOINED is False and (message_number == "376" or message_number == "422"):
- # wait until we receive end of MOTD before joining, or until the server tells us the MOTD doesn't exis
- self.chan_list = self.conf.getChannels(self.network)
- for c in self.chan_list:
- self.send('JOIN '+c+' \n')
- self.JOINED = True
-
- line_array = line.split()
- user_and_mask = line_array[0][1:]
- usr = user_and_mask.split("!")[0]
- channel = line_array[2]
- try:
- message = line.split(":",2)[2]
- self.brain.respond(usr, channel, message)
- except IndexError:
- try:
- message = line.split(":",2)[1]
- self.brain.respond(usr, channel, message)
- except IndexError:
- print "index out of range.", line
-
- except Exception:
- print "Unexpected error:", sys.exc_info()[0]
- traceback.print_exc(file=sys.stdout)
-
- def worker(self, mock=False):
- """
- Open the socket, make the first incision^H^H connection and get us on the server.
- Handles keeping the connection alive; if we disconnect from the server, attempts to reconnect.
- Args:
- mock: boolean. If mock is true, don't loop forever -- mock is for testing.
- """
- self.HOST = self.network
- self.NICK = self.conf.getNick(self.network)
- # we have to cast it to an int, otherwise the connection fails silently and the entire process dies
- self.PORT = int(self.conf.getPort(self.network))
- self.IDENT = 'mypy'
- self.REALNAME = 's1ash'
- self.OWNER = self.conf.getOwner(self.network)
-
- # connect to server
- self.s = socket.socket()
- while self.CONNECTED == False:
- try:
- # low level socket TCP/IP connection
- self.s.connect((self.HOST, self.PORT)) # force them into one argument
- self.CONNECTED = True
- self.logger.write(Logger.INFO, "Connected to " + self.network, self.NICK)
- if self.DEBUG:
- self.debug_print(util.bcolors.YELLOW + ">> " + util.bcolors.ENDC + "connected to " + self.network)
- except:
- if self.DEBUG:
- self.debug_print(util.bcolors.FAIL + ">> " + util.bcolors.ENDC + "Could not connect! Retrying... ")
- self.logger.write(Logger.CRITICAL, "Could not connect! Retrying...")
- time.sleep(1)
- self.worker()
- time.sleep(1)
- # core IRC protocol stuff
- self.s.send('NICK '+self.NICK+'\n')
- if self.DEBUG:
- self.debug_print(util.bcolors.YELLOW + ">> " + util.bcolors.ENDC + self.network + ': NICK ' + self.NICK + '\\n')
- self.s.send('USER '+self.IDENT+ ' 8 ' + ' bla : '+self.REALNAME+'\n') # yeah, don't delete this line
- if self.DEBUG:
- self.debug_print(util.bcolors.YELLOW + ">> " + util.bcolors.ENDC + self.network + ": USER " +self.IDENT+ ' 8 ' + ' bla : '+self.REALNAME+'\\n')
- time.sleep(3) # allow services to catch up
- self.s.send('PRIVMSG nickserv identify '+self.conf.getIRCPass(self.network)+'\n') # we're registered!
- if self.DEBUG:
- self.debug_print(util.bcolors.YELLOW + ">> " + util.bcolors.ENDC + self.network + ': PRIVMSG nickserv identify '+self.conf.getIRCPass(self.network)+'\\n')
- self.s.setblocking(1)
-
- read = ""
-
- timeout = 0
- # does not require a definition -- it will be invoked specifically when the bot notices it has been disconnected
- disconnect_event = Event("__.disconnection__")
- # if we're only running a test of connecting, and don't want to loop forever
- if mock:
- return
- # infinite loop to keep parsing lines
- while True:
- try:
- timeout += 1
- # if we haven't received anything for 120 seconds
- time_since = self.conf.getTimeout(self.network)
- if timeout > time_since:
- if self.DEBUG:
- self.debug_print("Disconnected! Retrying... ")
- disconnect_event.notifySubscribers("null")
- self.logger.write(Logger.CRITICAL, "Disconnected!", self.NICK)
- # so that we rejoin all our channels upon reconnecting to the server
- self.JOINED = False
- self.CONNECTED = False
- global RETRY_COUNTER
- if RETRY_COUNTER > 10:
- self.debug_print("Failed to reconnect after 10 tries. Giving up...")
- sys.exit(1)
- RETRY_COUNTER+=1
- self.worker()
- time.sleep(1)
- # if self.CONNECTED == False:
- # self.connect()
- ready = select.select([self.s],[],[], 1)
- if ready[0]:
- try:
- read = read + self.s.recv(1024).decode('utf8', 'ignore')
- except UnicodeDecodeError, e:
- self.debug_print("Unicode decode error; " + e.__str__())
- self.debug_print("Offending recv: " + self.s.recv)
- pass
- except Exception, e:
- print e
- if self.DEBUG:
- self.debug_print("Disconnected! Retrying... ")
- disconnect_event.notifySubscribers("null")
- self.logger.write(Logger.CRITICAL, "Disconnected!", self.NICK)
- self.JOINED = False
- self.CONNECTED = False
- self.worker()
- lines = read.split('\n')
-
- # Important: all lines from irc are terminated with '\n'. lines.pop() will get you any "to be continued"
- # line that couldn't fit in the socket buffer. It is stored and tacked on to the start of the next recv.
- read = lines.pop()
- if len(lines) > 0:
- timeout = 0
- for line in lines:
- line = line.rstrip()
- self.processline(line)
- except KeyboardInterrupt:
- print "keyboard interrupt caught; exiting ..."
- raise
- # end worker
- def debug_print(self, line):
- """
- Prepends incoming lines with the current timestamp and the thread's name, then spits it to stdout.
- Warning: this is entirely asynchronous between threads. If you connect to multiple networks, they will interrupt each other between lines.
- Args:
- line: text.
- """
- print str(datetime.datetime.now()) + ": " + self.getName() + ": " + line.strip('\n').rstrip().lstrip()
-
- def run(self):
- """
- For implementing the parent threading.Thread class. Allows the thread the be initialized with our code.
- """
- self.worker()
- def say(self, channel, thing):
- """
- Speak, damn you!
- """
- self.brain.say(channel, thing)
- # end class Bot
-
- ## MAIN ## ACTUAL EXECUTION STARTS HERE
- if __name__ == "__main__":
- DEBUG = False
- import bot
- parser = argparse.ArgumentParser(description="a python irc bot that does stuff")
- parser.add_argument('config', nargs='?')
- parser.add_argument('-d', help='debug (foreground) mode', action='store_true')
- args = parser.parse_args()
- if args.d:
- DEBUG = True
- if args.config:
- config = args.config
- else:
- config = "~/.pybotrc"
- botslist = list()
- if not DEBUG and hasattr(os, 'fork'):
- pid = os.fork()
- if pid == 0: # child
- if os.name == "posix":
- print "starting bot in the background, pid " + util.bcolors.GREEN + str(os.getpid()) + util.bcolors.ENDC
- else:
- print "starting bot in the background, pid " + str(os.getpid())
- cm = confman.ConfManager(config)
- net_list = cm.getNetworks()
- for c in cm.getNetworks():
- b = bot.Bot(cm, c, DEBUG)
- b.start()
- elif pid > 0:
- sys.exit(0)
- else: # don't background; either we're in debug (foreground) mode, or on windows TODO
- if os.name == 'nt':
- print 'in debug mode; backgrounding currently unsupported on windows.'
- DEBUG = True
- print "starting bot, pid " + util.bcolors.GREEN + str(os.getpid()) + util.bcolors.ENDC
- try:
- f = open(os.path.expanduser(config))
- except IOError:
- print "Could not open conf file " + config
- sys.exit(1)
- cm = confman.ConfManager(config)
- net_list = cm.getNetworks()
- for c in cm.getNetworks():
- b = bot.Bot(cm, c, DEBUG)
- b.daemon = True
- b.start()
- botslist.append(b)
- try:
- while True:
- time.sleep(5)
- except (KeyboardInterrupt, SystemExit):
- l = Logger()
- l.write(Logger.INFO, "killed by ctrl+c or term signal")
- for b in botslist:
- b.s.send("QUIT :because I got killed\n")
- print
- print "keyboard interrupt caught; exiting"
- sys.exit(1)
-
|