bot.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  1. #!/usr/bin/env python
  2. #
  3. # v0.7.1
  4. # works with python 2.6.x and 2.7.x
  5. #
  6. import sys
  7. import socket
  8. import string
  9. import os
  10. import datetime
  11. import time
  12. import select
  13. import traceback
  14. import threading
  15. import botbrain
  16. from logger import Logger
  17. import db
  18. import confman
  19. from event import Event
  20. import db
  21. import util
  22. DEBUG = False
  23. class Bot(threading.Thread):
  24. def __init__(self, conf=None, network=None, d=None):
  25. threading.Thread.__init__(self)
  26. self.DEBUG = d
  27. self.brain = None
  28. self.network = network
  29. self.OFFLINE = False
  30. self.CONNECTED = False
  31. self.JOINED = False
  32. self.conf = conf
  33. self.db = db.DB()
  34. self.pid = os.getpid()
  35. self.logger = Logger()
  36. self.NICK = self.conf.getNick(self.network)
  37. self.logger.write(Logger.INFO, "\n", self.NICK)
  38. self.logger.write(Logger.INFO, " initializing bot, pid " + str(os.getpid()), self.NICK)
  39. # arbitrary key/value store for modules
  40. # they should be 'namespaced' like bot.mem_store.module_name
  41. self.mem_store = dict()
  42. self.CHANNELINIT = conf.getChannels(self.network)
  43. # this will be the socket
  44. self.s = None # each bot thread holds its own socket open to the network
  45. self.brain = botbrain.BotBrain(self.send, self)
  46. self.events_list = list()
  47. # define events here and add them to the events_list
  48. all_lines = Event("1__all_lines__")
  49. all_lines.define(".*")
  50. self.events_list.append(all_lines)
  51. implying = Event("__implying__")
  52. implying.define(">")
  53. #command = Event("__command__")
  54. # this is an example of passing in a regular expression to the event definition
  55. #command.define("fo.bar")
  56. lastfm = Event("__.lastfm__")
  57. lastfm.define(".lastfm")
  58. dance = Event("__.dance__")
  59. dance.define("\.dance")
  60. #unloads = Event("__module__")
  61. #unloads.define("^\.module")
  62. pimp = Event("__pimp__")
  63. pimp.define("\.pimp")
  64. bofh = Event("__.bofh__")
  65. bofh.define("\.bofh")
  66. #youtube = Event("__youtubes__")
  67. #youtube.define("youtube.com[\S]+")
  68. weather = Event("__.weather__")
  69. weather.define("\.weather")
  70. steam = Event("__.steam__")
  71. steam.define("\.steam")
  72. part = Event("__.part__")
  73. part.define("part")
  74. tell = Event("__privmsg__")
  75. tell.define("PRIVMSG")
  76. links = Event("__urls__")
  77. links.define("https?://*")
  78. # example
  79. # test = Event("__test__")
  80. # test.define(msg_definition="^\.test")
  81. # add your defined events here
  82. # tell your friends
  83. self.events_list.append(lastfm)
  84. self.events_list.append(dance)
  85. self.events_list.append(pimp)
  86. #self.events_list.append(youtube)
  87. self.events_list.append(bofh)
  88. self.events_list.append(weather)
  89. self.events_list.append(steam)
  90. self.events_list.append(part)
  91. self.events_list.append(tell)
  92. self.events_list.append(links)
  93. #self.events_list.append(unloads)
  94. # example
  95. # self.events_list.append(test)
  96. self.load_modules()
  97. self.logger.write(Logger.INFO, "bot initialized.", self.NICK)
  98. # conditionally subscribe to events list or add event to listing
  99. def register_event(self, event, module):
  100. for e in self.events_list:
  101. if e.definition == event.definition and e._type == event._type:
  102. # if our event is already in the listing, don't add it again, just have our module subscribe
  103. e.subscribe(module)
  104. return
  105. self.events_list.append(event)
  106. return
  107. # utility function for loading modules; can be called by modules themselves
  108. def load_modules(self, specific=None):
  109. nonspecific = False
  110. found = False
  111. self.loaded_modules = list()
  112. modules_dir_list = list()
  113. tmp_list = list()
  114. modules_path = 'modules'
  115. autoload_path = 'modules/autoloads'
  116. # this is magic.
  117. import inspect
  118. import os, imp, json
  119. dir_list = os.listdir(modules_path)
  120. mods = {}
  121. autoloads = {}
  122. #load autoloads if it exists
  123. if os.path.isfile(autoload_path):
  124. self.logger.write(Logger.INFO, "Found autoloads file", self.NICK)
  125. try:
  126. autoloads = json.load(open(autoload_path))
  127. #logging
  128. for k in autoloads.keys():
  129. self.logger.write(Logger.INFO, "Autoloads found for network " + k, self.NICK)
  130. except IOError:
  131. self.logger.write(Logger.ERROR, "Could not load autoloads file.",self.NICK)
  132. # create dictionary of things in the modules directory to load
  133. for fname in dir_list:
  134. name, ext = os.path.splitext(fname)
  135. if specific is None:
  136. nonspecific = True
  137. # ignore compiled python and __init__ files.
  138. #choose to either load all .py files or, available, just ones specified in autoloads
  139. if self.network not in autoloads.keys(): # if autoload does not specify for this network
  140. if ext == '.py' and not name == '__init__':
  141. f, filename, descr = imp.find_module(name, [modules_path])
  142. mods[name] = imp.load_module(name, f, filename, descr)
  143. self.logger.write(Logger.INFO, " loaded " + name + " for network " + self.network, self.NICK)
  144. else: # follow autoload's direction
  145. if ext == '.py' and not name == '__init__':
  146. if name == 'module':
  147. f, filename, descr = imp.find_module(name, [modules_path])
  148. mods[name] = imp.load_module(name, f, filename, descr)
  149. self.logger.write(Logger.INFO, " loaded " + name + " for network " + self.network, self.NICK)
  150. 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']):
  151. f, filename, descr = imp.find_module(name, [modules_path])
  152. mods[name] = imp.load_module(name, f, filename, descr)
  153. self.logger.write(Logger.INFO, " loaded " + name + " for network " + self.network, self.NICK)
  154. else:
  155. if name == specific: # we're reloading only one module
  156. if ext != '.pyc': # ignore compiled
  157. f, filename, descr = imp.find_module(name, [modules_path])
  158. mods[name] = imp.load_module(name, f, filename, descr)
  159. found = True
  160. for k,v in mods.iteritems():
  161. for name in dir(v):
  162. obj = getattr(mods[k], name) # get the object from the namespace of 'mods'
  163. try:
  164. if inspect.isclass(obj): # it's a class definition, initialize it
  165. a = obj(self.events_list, self.send, self, self.say) # now we're passing in a reference to the calling bot
  166. if a not in self.loaded_modules: # don't add in multiple copies
  167. self.loaded_modules.append(a)
  168. except TypeError:
  169. pass
  170. if nonspecific is True or found is True:
  171. return 0
  172. else: return 1
  173. # end magic.
  174. def send(self, message):
  175. if self.OFFLINE:
  176. self.debug_print(util.bcolors.YELLOW + " >> " + util.bcolors.ENDC + self.getName() + ": " + message.encode('utf-8', 'ignore'))
  177. else:
  178. if self.DEBUG is True:
  179. self.logger.write(Logger.INFO, "DEBUGGING OUTPUT", self.NICK)
  180. self.logger.write(Logger.INFO, str(datetime.datetime.now()) + ": " + self.getName() + ": " + message.encode('utf-8', 'ignore'), self.NICK)
  181. self.debug_print(util.bcolors.OKGREEN + ">> " + util.bcolors.ENDC + ": " + self.getName() + ": " + message.encode('utf-8', 'ignore'))
  182. self.s.send(message.encode('utf-8', 'ignore'))
  183. target = message.split()[1]
  184. if target.startswith("#"):
  185. self.processline(':' + self.conf.getNick(self.network) + '!~' + self.conf.getNick(self.network) + '@fakehost.here ' + message.rstrip())
  186. def pong(self, response):
  187. self.send('PONG ' + response + '\n')
  188. def processline(self, line):
  189. if self.DEBUG:
  190. import datetime
  191. self.debug_print(util.bcolors.OKBLUE + "<< " + util.bcolors.ENDC + ": " + self.getName() + ": " + line)
  192. message_number = line.split()[1]
  193. try:
  194. for e in self.events_list:
  195. if e.matches(line):
  196. e.notifySubscribers(line)
  197. # don't bother going any further if it's a PING/PONG request
  198. if line.startswith("PING"):
  199. ping_response_line = line.split(":", 1)
  200. self.pong(ping_response_line[1])
  201. # pings we respond to directly. everything else...
  202. else:
  203. # patch contributed by github.com/thekanbo
  204. if self.JOINED is False and (message_number == "376" or message_number == "422"):
  205. # wait until we receive end of MOTD before joining, or until the server tells us the MOTD doesn't exis
  206. self.chan_list = self.conf.getChannels(self.network)
  207. for c in self.chan_list:
  208. self.send('JOIN '+c+' \n')
  209. self.JOINED = True
  210. line_array = line.split()
  211. user_and_mask = line_array[0][1:]
  212. usr = user_and_mask.split("!")[0]
  213. channel = line_array[2]
  214. try:
  215. message = line.split(":",2)[2]
  216. self.brain.respond(usr, channel, message)
  217. except IndexError:
  218. try:
  219. message = line.split(":",2)[1]
  220. self.brain.respond(usr, channel, message)
  221. except IndexError:
  222. print "index out of range.", line
  223. except Exception:
  224. print "Unexpected error:", sys.exc_info()[0]
  225. traceback.print_exc(file=sys.stdout)
  226. def worker(self, mock=False):
  227. self.HOST = self.network
  228. self.NICK = self.conf.getNick(self.network)
  229. # we have to cast it to an int, otherwise the connection fails silently and the entire process dies
  230. self.PORT = int(self.conf.getPort(self.network))
  231. self.IDENT = 'mypy'
  232. self.REALNAME = 's1ash'
  233. self.OWNER = self.conf.getOwner(self.network)
  234. # connect to server
  235. self.s = socket.socket()
  236. while self.CONNECTED == False:
  237. try:
  238. # low level socket TCP/IP connection
  239. self.s.connect((self.HOST, self.PORT)) # force them into one argument
  240. self.CONNECTED = True
  241. self.logger.write(Logger.INFO, "Connected to " + self.network, self.NICK)
  242. if self.DEBUG:
  243. self.debug_print(util.bcolors.YELLOW + ">> " + util.bcolors.ENDC + "connected to " + self.network)
  244. except:
  245. self.debug_print(util.bcolors.FAIL + ">> " + util.bcolors.ENDC + "Could not connect! Retrying... ")
  246. time.sleep(1)
  247. self.worker()
  248. # core IRC protocol stuff
  249. self.s.send('NICK '+self.NICK+'\n')
  250. if self.DEBUG:
  251. self.debug_print(util.bcolors.YELLOW + ">> " + util.bcolors.ENDC + self.network + ': NICK ' + self.NICK + '\\n')
  252. self.s.send('USER '+self.IDENT+ ' 8 ' + ' bla : '+self.REALNAME+'\n') # yeah, don't delete this line
  253. if self.DEBUG:
  254. self.debug_print(util.bcolors.YELLOW + ">> " + util.bcolors.ENDC + self.network + ": USER " +self.IDENT+ ' 8 ' + ' bla : '+self.REALNAME+'\\n')
  255. time.sleep(3) # allow services to catch up
  256. self.s.send('PRIVMSG nickserv identify '+self.conf.getIRCPass(self.network)+'\n') # we're registered!
  257. if self.DEBUG:
  258. self.debug_print(util.bcolors.YELLOW + ">> " + util.bcolors.ENDC + self.network + ': PRIVMSG nickserv identify '+self.conf.getIRCPass(self.network)+'\\n')
  259. self.s.setblocking(1)
  260. read = ""
  261. timeout = 0
  262. # if we're only running a test of connecting, and don't want to loop forever
  263. if mock:
  264. return
  265. # infinite loop to keep parsing lines
  266. while True:
  267. try:
  268. timeout += 1
  269. # if we haven't received anything for 120 seconds
  270. time_since = self.conf.getTimeout(self.network)
  271. if timeout > time_since:
  272. if self.DEBUG:
  273. print "Disconnected! Retrying... "
  274. self.logger.write(Logger.CRITICAL, "Disconnected!", self.NICK)
  275. # so that we rejoin all our channels upon reconnecting to the server
  276. self.JOINED = False
  277. self.CONNECTED = False
  278. self.worker()
  279. time.sleep(1)
  280. # if self.CONNECTED == False:
  281. # self.connect()
  282. ready = select.select([self.s],[],[], 1)
  283. if ready[0]:
  284. try:
  285. read = read + self.s.recv(1024)
  286. except:
  287. if self.DEBUG:
  288. print "Disconnected! Retrying... "
  289. self.logger.write(Logger.CRITICAL, "Disconnected!", self.NICK)
  290. self.JOINED = False
  291. self.CONNECTED = False
  292. self.worker()
  293. lines = read.split('\n')
  294. # Important: all lines from irc are terminated with '\n'. lines.pop() will get you any "to be continued"
  295. # line that couldn't fit in the socket buffer. It is stored and tacked on to the start of the next recv.
  296. read = lines.pop()
  297. if len(lines) > 0:
  298. timeout = 0
  299. for line in lines:
  300. line = line.rstrip()
  301. self.processline(line)
  302. except KeyboardInterrupt:
  303. print "keyboard interrupt caught; exiting ..."
  304. raise
  305. # end worker
  306. def debug_print(self, line):
  307. print str(datetime.datetime.now()) + ": " + self.getName() + ": " + line
  308. def commands(*command_list):
  309. # stolen shamelessly from willie. damn, this is a good idea.
  310. """Decorator. Sets a command list for a callable.
  311. This decorator can be used to add multiple commands to one callable in a
  312. single line. The resulting match object will have the command as the first
  313. group, rest of the line, excluding leading whitespace, as the second group.
  314. Parameters 1 through 4, seperated by whitespace, will be groups 3-6.
  315. Args:
  316. command: A string, which can be a regular expression.
  317. Returns:
  318. A function with a new command appended to the commands
  319. attribute. If there is no commands attribute, it is added.
  320. Example:
  321. @command("hello"):
  322. If the command prefix is "\.", this would trigger on lines starting
  323. with ".hello".
  324. @commands('j', 'join')
  325. If the command prefix is "\.", this would trigger on lines starting
  326. with either ".j" or ".join".
  327. """
  328. def add_attribute(function):
  329. if not hasattr(function, "commands"):
  330. function.commands = []
  331. function.commands.extend(command_list)
  332. return function
  333. return add_attribute
  334. def depends(self, module_name):
  335. for m in self.loaded_modules:
  336. if m.__class__.__name__ == module_name:
  337. return m
  338. return None
  339. def run(self):
  340. self.worker()
  341. def say(self, channel, thing):
  342. self.brain.say(channel, thing)
  343. # end class Bot
  344. ## MAIN ## ACTUAL EXECUTION STARTS HERE
  345. if __name__ == "__main__":
  346. import bot
  347. DEBUG = False
  348. for i in sys.argv:
  349. if i == "-d":
  350. DEBUG = True
  351. # duuude this is so old.
  352. if os.name == "posix":
  353. botslist = list()
  354. if not DEBUG:
  355. pid = os.fork()
  356. if pid == 0: # child
  357. print "starting bot in the background, pid " + util.bcolors.GREEN + str(os.getpid()) + util.bcolors.ENDC
  358. if len(sys.argv) > 1:
  359. CONF = sys.argv[1]
  360. else: CONF = "~/.pybotrc"
  361. cm = confman.ConfManager(CONF)
  362. net_list = cm.getNetworks()
  363. i = 0
  364. if cm.getNumNets() > 1:
  365. for c in cm.getNetworks():
  366. b = bot.Bot(cm, net_list[i], DEBUG)
  367. b.start()
  368. i += 1
  369. else:
  370. b = bot.Bot(cm, net_list[0], DEBUG)
  371. b.start()
  372. elif pid > 0:
  373. sys.exit(0)
  374. else: # don't background
  375. print "starting bot in the background, pid " + util.bcolors.GREEN + str(os.getpid()) + util.bcolors.ENDC
  376. if len(sys.argv) > 1 and sys.argv[1] != "-d": # the conf file must be first argument
  377. CONF = sys.argv[1]
  378. try:
  379. f = open(CONF)
  380. except IOError:
  381. print "Could not open conf file " + sys.argv[1]
  382. sys.exit(1)
  383. else: CONF = "~/.pybotrc"
  384. cm = confman.ConfManager(CONF)
  385. net_list = cm.getNetworks()
  386. i = 0
  387. if cm.getNumNets() > 1:
  388. for c in cm.getNetworks():
  389. try:
  390. b = bot.Bot(cm, net_list[i], DEBUG)
  391. b.daemon = True
  392. b.start()
  393. botslist.append(b)
  394. i += 1
  395. while True: time.sleep(5)
  396. except (KeyboardInterrupt, SystemExit):
  397. print "keyboard interrupt caught; exiting..."
  398. sys.exit(0)
  399. else:
  400. try:
  401. b = bot.Bot(cm, net_list[0], DEBUG)
  402. b.daemon = True
  403. b.start()
  404. botslist.append(b)
  405. while True: time.sleep(5)
  406. except (KeyboardInterrupt, SystemExit):
  407. print "keyboard interrupt caught; exiting..."
  408. sys.exit(0)