#!/usr/bin/env python from twisted.words.protocols import irc from twisted.internet import reactor, protocol from twisted.python import log from twisted.protocols.basic import LineReceiver from twisted import version as twisted_version import os, re, platform, sys, traceback from ConfigParser import ConfigParser import login config = ConfigParser() def load_config(): print 'Loading config...' config.read([os.path.expanduser('~/.fot')]) print 'Done!' load_config() modules = {} def load_modules(): print 'Loading modules...' for s in config.sections(): if not s.startswith('module/'): continue m = s[7:] if not config.has_option(s, 'load') or config.getboolean(s, 'load'): try: if modules.has_key(m): reload(modules[m]) else: modules[m] = __import__('modules.' + m, globals(), locals(), ['Module']) mod = modules[m] mod.config = config print '%s by %s' % (mod.info['title'], mod.info['author']) del mod except: print 'Failed to load', m raise else: print 'Not loading', m print 'Done!' load_modules() bots = [] def refresh_modules(): print 'Re-applying modules to bots...' for bot in bots: bot.apply_modules() print 'Done!' class Bot(irc.IRCClient): def __init__(self): self.versionName = 'fot' self.versionNum = 'Python/%s %s' % (platform.python_version(), str(twisted_version)) self.versionEnv = '%s %s %s %s' % (platform.system(), platform.release(), platform.version(), platform.machine()) self.sourceURL = 'http://cgit.jvnv.net/fot/' bots.append(self) self.modules = {} self.keywords = {} self.msg_callbacks = [] self.module_channels = None def __repr__(self): return '' % (self.nickname, self.factory.server) def register_keyword(self, kw, mod): if kw in self.keywords and self.keywords[kw] != mod: raise Exception('Keyword "%s" already in use' % kw) self.keywords[kw] = mod def register_callback(self, mod): if not mod in self.msg_callbacks: self.msg_callbacks.append(mod) def apply_modules(self): # Call stop() for each module if neccessary (can't rely on __del__ here) for m in self.modules.itervalues(): if hasattr(m, 'stop'): m.stop() self.modules = {} self.keywords = {} self.msg_callbacks = [] server = self.factory.server.split('/', 1)[1] channel_list = config.get(self.factory.server, 'channels').split() self.module_channels = dict([(x, []) for x in channel_list]) for m in modules.keys(): if config.has_option('module/' + m, 'filter_channel'): mod_re = re.compile(config.get('module/' + m, 'filter_channel')) else: mod_re = None mod = None # the module instance for channel in channel_list: if not mod_re or mod_re.match('%s/%s' % (server, channel)) != None: if not mod: mod = modules[m].Module(self) self.module_channels[channel].append(mod) if mod: self.modules[m] = mod def connectionMade(self): self.apply_modules() self.nickname = config.get(self.factory.server, 'nickname') irc.IRCClient.connectionMade(self) def set_usermodes(self): mode = config.get(self.factory.server, 'mode') if config.has_option(self.factory.server, 'mode') else None if not mode: return from StringIO import StringIO s = StringIO(mode) mode = 0 add = set() remove = set() c = s.read(1) while c: if c == '+': mode = 1 c = s.read(1) elif c == '-': mode = -1 c = s.read(1) if mode > 0: add.add(c) elif mode < 0: remove.add(c) else: raise RuntimeError('No mode set!') c = s.read(1) intersect = add.intersection(remove) if intersect: print 'Modes conflict:', intersect add.difference_update(intersect) remove.difference_update(intersect) if add: self.mode(self.nickname, True, ''.join(add)) if remove: self.mode(self.nickname, False, ''.join(remove)) def signedOn(self): self.set_usermodes() for chan in config.get(self.factory.server, 'channels').split(' '): self.join(chan) def privmsg(self, nick, channel, msg): priv = channel == self.nickname # filter out server stuff if not priv and not channel[:1] in irc.CHANNEL_PREFIXES: return for mod in self.msg_callbacks: if priv or mod in self.module_channels[channel]: # This is a somewhat hackish way to get the module name. mod_name = mod.__module__.split('.')[-1] if config.has_option('module/' + mod_name, 'filter_nick'): filter_re = re.compile(config.get('module/' + mod_name, 'filter_nick')) if not filter_re.match(nick): continue try: mod.privmsg(nick, channel, msg) except: traceback.print_exc() msg = msg.split(None, 1) if not len(msg): return elif len(msg) == 1: kw, msg = msg[0], '' else: kw, msg = msg if kw in self.keywords: mod = self.keywords[kw] if priv or mod in self.module_channels[channel]: try: mod.keyword(nick, channel, kw, msg) except: traceback.print_exc() def kickedFrom(self, channel, kicker, message): self.join(channel) def connectionLost(self, reason): for m in self.modules.itervalues(): if hasattr(m, 'stop'): m.stop() self.modules = {} self.keywords = {} self.msg_callbacks = [] irc.IRCClient.connectionLost(self, reason) del bots[bots.index(self)] def reconnect(self): self.quit('reconnecting') class BotFactory(protocol.ReconnectingClientFactory): protocol = Bot def __init__(self, server, nickname): self.server = server self.nickname = nickname self.maxDelay = 120 def buildProtocol(self, addr): self.resetDelay() return protocol.ReconnectingClientFactory.buildProtocol(self, addr) def startedConnecting(self, connector): print 'Connecting to', connector.host def clientConnectionFailed(self, connector, reason): print 'Connection failed:', reason.getErrorMessage() protocol.ReconnectingClientFactory.clientConnectionFailed(self, connector, reason) def clientConnectionLost(self, connector, reason): print 'Connection lost:', reason.getErrorMessage() protocol.ReconnectingClientFactory.clientConnectionLost(self, connector, reason) def start_server(server): if not config.has_option(server,'host') or not config.has_option(server, 'port') \ or not config.has_option(server, 'channels') or config.has_option(server, 'disabled'): return print '%s' % server factory = BotFactory(server, config.get(server, 'nickname')) reactor.connectTCP(config.get(server, 'host'), config.getint(server, 'port'), factory, bindAddress = (config.get(server, 'bind'), 0), timeout = 60) print 'Starting per-network instances...' for server in (server for server in config.sections() if server.startswith('server/')): start_server(server) loginfactory = login.getManholeFactory(globals(), os.path.expanduser('~/.fot.users')) reactor.listenTCP(3333, loginfactory) reactor.run()