class SourceFactory(client.DeferredClientFactory, protocol.ReconnectingClientFactory, IbidSourceFactory): auth = ('implicit',) supports = ('multiline',) jid_str = Option('jid', 'Jabber ID') server = Option('server', 'Server hostname (defaults to SRV lookup, ' 'falling back to JID domain)') port = IntOption('port', 'Server port number (defaults to SRV lookup, ' 'falling back to 5222/5223') ssl = BoolOption('ssl', 'Use SSL instead of automatic TLS') password = Option('password', 'Jabber password') nick = Option('nick', 'Nick for chatrooms', ibid.config['botname']) rooms = ListOption('rooms', 'Chatrooms to autojoin', []) accept_domains = ListOption('accept_domains', 'Only accept messages from these domains', []) max_public_message_length = IntOption('max_public_message_length', 'Maximum length of public messages', 512) def __init__(self, name): IbidSourceFactory.__init__(self, name) self.log = logging.getLogger('source.%s' % name) client.DeferredClientFactory.__init__(self, JID(self.jid_str), self.password) bot = JabberBot() self.addHandler(bot) bot.setHandlerParent(self) def setServiceParent(self, service): c = IbidXMPPClientConnector(reactor, self.authenticator.jid.host, self, self.server, self.port, self.ssl) c.connect() def connect(self): return self.setServiceParent(None) def disconnect(self): self.stopTrying() self.stopFactory() self.proto.xmlstream.transport.loseConnection() return True def join(self, room): return self.proto.join(room) def leave(self, room): return self.proto.leave(room) def url(self): return u'xmpp://%s' % (self.jid_str,) def logging_name(self, identity): return identity.split('/')[0] def truncation_point(self, response, event=None): if response.get('target', None) in self.proto.rooms: return self.max_public_message_length return None
class SourceFactory(IbidSourceFactory): auth = ('implicit', ) supports = ('action', 'topic') server = Option('server', 'Server hostname') port = IntOption('port', 'Server port number', 706) nick = Option('nick', 'Nick', ibid.config['botname']) channels = ListOption('channels', 'Channels to autojoin', []) realname = Option('realname', 'Real Name', ibid.config['botname']) public_key = Option('public_key', 'Filename of public key', 'silc.pub') private_key = Option('private_key', 'Filename of private key', 'silc.prv') max_public_message_length = IntOption('max_public_message_length', 'Maximum length of public messages', 512) def __init__(self, name): IbidSourceFactory.__init__(self, name) self.log = logging.getLogger('source.%s' % self.name) pub = join(ibid.options['base'], self.public_key) prv = join(ibid.options['base'], self.private_key) if not exists(pub) and not exists(prv): keys = create_key_pair(pub, prv, passphrase='') else: keys = load_key_pair(pub, prv, passphrase='') self.client = SilcBot(keys, self.nick, self.nick, self.realname, self) def run_one(self): self.client.run_one() def setServiceParent(self, service): self.s = internet.TimerService(0.2, self.run_one) if service is None: self.s.startService() else: self.s.setServiceParent(service) def disconnect(self): self.client.disconnect() return True def url(self): return u'silc://%s@%s:%s' % (self.nick, self.server, self.port) def logging_name(self, identity): return self.client.logging_name(identity) def truncation_point(self, response, event=None): if response.get('target', None) in self.client.channels: return self.max_public_message_length return None
class Ping(Processor): usage = u'ping <host>' features = ('ping',) ping = Option('ping', 'Path to ping executable', 'ping') def setup(self): if not file_in_path(self.ping): raise Exception("Cannot locate ping executable") @match(r'^ping\s+(\S+)$') def handle_ping(self, event, host): if host.strip().startswith("-"): event.addresponse(False) return ping = Popen([self.ping, '-q', '-c5', host], stdout=PIPE, stderr=PIPE) output, error = ping.communicate() ping.wait() if not error: output = unicode_output(output) output = u' '.join(output.splitlines()[-2:]) event.addresponse(output) else: error = unicode_output(error).replace(u'\n', u' ') \ .replace(u'ping:', u'', 1).strip() event.addresponse(u'Error: %s', error)
class Fortune(Processor, RPC): usage = u'fortune' features = ('fortune', ) fortune = Option('fortune', 'Path of the fortune executable', 'fortune') def __init__(self, name): super(Fortune, self).__init__(name) RPC.__init__(self) def setup(self): if not file_in_path(self.fortune): raise Exception("Cannot locate fortune executable") @match(r'^fortune$') def handler(self, event): fortune = self.remote_fortune() if fortune: event.addresponse(fortune) else: event.addresponse(u"Couldn't execute fortune") def remote_fortune(self): fortune = Popen(self.fortune, stdout=PIPE, stderr=PIPE) output, error = fortune.communicate() code = fortune.wait() output = unicode_output(output.strip(), 'replace') if code == 0: return output else: return None
class GoogleScrapeSearch(Processor): usage = u"""gcalc <expression> gdefine <term>""" features = ('google', ) user_agent = Option( 'user_agent', 'HTTP user agent to present to Google (for non-API searches)', default_user_agent) def _google_scrape_search(self, query, country=None): params = {'q': query.encode('utf-8')} if country: params['cr'] = u'country' + country.upper() return get_html_parse_tree('http://www.google.com/search?' + urlencode(params), headers={'user-agent': self.user_agent}, treetype='etree') @match(r'^gcalc\s+(.+)$') def calc(self, event, expression): tree = self._google_scrape_search(expression) nodes = [ node for node in tree.findall('.//h2') if node.get('class') == 'r' ] if len(nodes) == 1: # ElementTree doesn't support inline tags: # May return ASCII unless an encoding is specified. # "utf8" will result in an xml header node = ElementTree.tostring(nodes[0], encoding='utf-8') node = node.decode('utf-8') node = re.sub(r'<sup>(.*?)</sup>', lambda x: u'^' + x.group(1), node) node = re.sub(r'<.*?>', '', node) node = re.sub(r'(\d)\s+(\d)', lambda x: x.group(1) + x.group(2), node) node = decode_htmlentities(node) node = re.sub(r'\s+', ' ', node) event.addresponse(node) else: event.addresponse( u"%s, Google wasn't interested in calculating that", choice(('Sorry', 'Whoops'))) @match(r'^gdefine\s+(.+)$') def define(self, event, term): tree = self._google_scrape_search("define:%s" % term) definitions = [] for li in tree.findall('.//li'): if li.text: definitions.append(li.text) if definitions: event.addresponse(u' :: '.join(definitions)) else: event.addresponse(u'Are you making up words again?')
class AptFile(Processor): usage = u'apt-file [search] <term>' feature = ('apt-file',) aptfile = Option('apt-file', 'Path to apt-file executable', 'apt-file') def setup(self): if not file_in_path(self.aptfile): raise Exception("Cannot locate apt-file executable") @match(r'^apt-?file\s+(?:search\s+)?(.+)$') def search(self, event, term): apt = Popen([self.aptfile, 'search', term], stdout=PIPE, stderr=PIPE) output, error = apt.communicate() code = apt.wait() if code == 0: if output: output = unicode_output(output.strip()) output = [line.split(u':')[0] for line in output.splitlines()] packages = sorted(set(output)) event.addresponse(u'Found %(num)i packages: %(names)s', { 'num': len(packages), 'names': human_join(packages), }) else: event.addresponse(u'No packages found') else: error = unicode_output(error.strip()) if u"The cache directory is empty." in error: event.addresponse(u'Search error: apt-file cache empty') else: event.addresponse(u'Search error') raise Exception("apt-file: %s" % error)
class DrawImage(Processor): usage = u'draw <url> [in colour] [width <width>] [height <height>]' feature = ('draw-aa',) max_filesize = IntOption('max_filesize', 'Only request this many KiB', 200) def_height = IntOption('def_height', 'Default height for libaa output', 10) max_width = IntOption('max_width', 'Maximum width for ascii output', 60) max_height = IntOption('max_height', 'Maximum height for ascii output', 15) font_width = IntOption('font_width', 'Font width assumed for output', 6) font_height = IntOption('font_height', 'Font height assumed for output', 10) img2txt_bin = Option('img2txt_bin', 'libcaca img2txt binary to use', 'img2txt') def setup(self): if not file_in_path(self.img2txt_bin): raise Exception('Cannot locate img2txt executable') @match(r'^draw\s+(\S+\.\S+)(\s+in\s+colou?r)?(?:\s+w(?:idth)?\s+(\d+))?(?:\s+h(?:eight)\s+(\d+))?$') def draw(self, event, url, colour, width, height): if not urlparse(url).netloc: url = 'http://' + url if urlparse(url).scheme == 'file': event.addresponse(u'Are you trying to haxor me?') return if not urlparse(url).path: url += '/' try: f = urlopen(url_to_bytestring(url)) except HTTPError, e: event.addresponse(u'Sorry, error fetching URL: %s', BaseHTTPRequestHandler.responses[e.code][0]) return except URLError: event.addresponse(u'Sorry, error fetching URL') return
class CounterStrike(Processor): usage = u'cs players | who is playing cs' feature = ('gameservers', ) autoload = False cs_host = Option('cs_host', 'CS server hostname / IP', '127.0.0.1') cs_port = IntOption('cs_port', 'CS server port', 27015) @match( r'^(?:(?:cs|counter[\s-]*strike)\s+players|who(?:\'s|\s+is)\s+(?:playing|on)\s+(?:cs|counter[\s-]*strike))$' ) def cs_players(self, event): server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) server.sendto('\xFF\xFF\xFF\xFFdetails', (self.cs_host, self.cs_port)) server.settimeout(5) data = server.recv(16384) assert data.startswith('\xFF\xFF\xFF\xFFm') data = data[5:] address, hostname, map, mod, modname, details = data.split('\x00', 5) details = details[:5] # We don't care about the rest clientcount, clientmax, protocol, type, os = struct.unpack( '<3Bcc', details) if clientcount == 0: event.addresponse(u'Nobody. Everyone must have lives...') return server.sendto('\xFF\xFF\xFF\xFFplayers', (self.cs_host, self.cs_port)) data = server.recv(16384) assert data.startswith('\xFF\xFF\xFF\xFF') data = data[6:] players = [] while data: player = {} data = data[1:] player['nickname'], data = data.split('\x00', 1) player['fragtotal'] = struct.unpack('<i', data[:4])[0] data = data[8:] players.append(player) players.sort(key=lambda x: x['fragtotal'], reverse=True) event.addresponse( u'There are %(clients)i/%(clientmax)s players playing %(map)s: %(players)s', { 'clients': clientcount, 'clientmax': clientmax, 'map': map, 'players': human_join(u'%s (%i)' % (p['nickname'], p['fragtotal']) for p in players), })
class Bnet(Processor): usage = u'dota players | who is playing dota' feature = ('gameservers', ) autoload = False bnet_host = Option('bnet_host', 'Bnet server hostname / IP', '127.0.0.1') bnet_port = IntOption('bnet_port', 'Bnet server port', 6112) bnet_user = Option('bnet_user', 'Bnet username', 'guest') bnet_pass = Option('bnet_pass', 'Bnet password', 'guest') def bnet_players(self, gametype): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((self.bnet_host, self.bnet_port)) s.settimeout(5) s.send('\03%s\n%s\n/con\n/quit\n' % (self.bnet_user, self.bnet_pass)) out = "" while (True): line = s.recv(1024) if line == "": break out += line s.close() player_re = re.compile( r'^1018 INFO "\s+bnet\s+%s\s+"(\S+?)"?\s+\d+\s+' % gametype) users = [ player_re.match(line).group(1) for line in out.splitlines() if player_re.match(line) ] users.sort() return users @match( r'^(?:dota\s+players|who(?:\'s|\s+is)\s+(?:playing\s+dota|on\s+bnet))$' ) def dota_players(self, event): users = self.bnet_players('W3XP') if users: event.addresponse(u'The battlefield contains %s', human_join(users)) else: event.addresponse(u'Nobody. Everyone must have a lives...')
class IMDB(Processor): usage = u'imdb [search] [character|company|episode|movie|person] <terms> [#<index>]' features = ('imdb', ) access_system = Option("accesssystem", "Method of querying IMDB", "http") adult_search = BoolOption("adultsearch", "Include adult films in search results", True) name_keys = { "character": "long imdb name", "company": "long imdb name", "episode": "long imdb title", "movie": "long imdb title", "person": "name", } def setup(self): if IMDb is None: raise Exception("IMDbPY not installed") self.imdb = IMDb(accessSystem=self.access_system, adultSearch=int(self.adult_search)) @match( r'^imdb(?:\s+search)?(?:\s+(character|company|episode|movie|person))?\s+(.+?)(?:\s+#(\d+))?$' ) def search(self, event, search_type, terms, index): if search_type is None: search_type = "movie" if index is not None: index = int(index) - 1 result = None try: if terms.isdigit(): result = getattr(self.imdb, "get_" + search_type)(terms) else: results = getattr(self.imdb, "search_" + search_type)(terms) if len(results) == 1: index = 0 if index is not None: result = results[index] self.imdb.update(result) except IMDbDataAccessError, e: event.addresponse(u"IMDb doesn't like me today. It said '%s'", e[0]["errmsg"]) raise except IMDbError, e: event.addresponse( u'IMDb must be having a bad day (or you are asking it silly things)' ) raise
class SourceFactory(IbidSourceFactory): auth = ('implicit', ) supports = ('action', 'multiline', 'topic') subdomain = Option('subdomain', 'Campfire subdomain') secure = BoolOption('secure', 'Use https (paid accounts only)', False) token = Option('token', 'Campfire token') rooms = ListOption('rooms', 'Rooms to join', []) keepalive_timeout = IntOption( 'keepalive_timeout', 'Stream keepalive timeout. ' 'Campfire sends a keepalive every <5 seconds', 30) def __init__(self, name): super(SourceFactory, self).__init__(name) self.log = logging.getLogger('source.%s' % self.name) self.client = CampfireBot(self) def setServiceParent(self, service): self.client.connect() def disconnect(self): self.client.disconnect() return True def url(self): protocol = self.secure and 'https' or 'http' return '%s://%s.campfirenow.com/' % (protocol, self.subdomain) def send(self, response): return self.client.send(response) def join(self, room_name): return self.client.join(room_name) def leave(self, room_name): return self.client.leave(room_name) def truncation_point(self, response, event=None): return None
class AptFile(Processor): usage = u'apt-file [search] <term> [on <distribution>[/<architecture>]]' features = ('apt-file',) distro = Option('distro', 'Default distribution to search', 'sid') arch = Option('arch', 'Default distribution to search', 'i386') @match(r'^apt-?file\s+(?:search\s+)?(\S+)' r'(?:\s+[oi]n\s+([a-z]+?)(?:[/-]([a-z0-9]+))?)?$') def search(self, event, term, distro, arch): distro = distro and distro.lower() or self.distro arch = arch and arch.lower() or self.arch distro = distro + u'-' + arch if distro == u'all-all': distro = u'all' result = json_webservice( u'http://dde.debian.net/dde/q/aptfile/byfile/%s/%s' % (distro, quote(term)), {'t': 'json'}) result = result['r'] if result: if isinstance(result[0], list): bypkg = map(lambda x: (x[-1], u'/'.join(x[:-1])), result) numpackages = len(bypkg) packages = defaultdict(list) for p, arch in bypkg: packages[p].append(arch) packages = map(lambda i: u'%s [%s]' % (i[0], u', '.join(i[1])), packages.iteritems()) else: numpackages = len(result) packages = result event.addresponse(u'Found %(num)i packages: %(names)s', { 'num': numpackages, 'names': human_join(packages), }) else: event.addresponse(u'No packages found')
class SourceFactory(ShellFactory, IbidSourceFactory): port = IntOption('port', 'Port number to listen on', 9898) username = Option('username', 'Login Username', 'admin') password = Option('password', 'Login Password', 'admin') def __init__(self, name): ShellFactory.__init__(self) IbidSourceFactory.__init__(self, name) self.name = name def setServiceParent(self, service=None): if service: self.listener = internet.TCPServer(self.port, self).setServiceParent(service) return self.listener else: self.listener = reactor.listenTCP(self.port, self) def connect(self): return self.setServiceParent(None) def disconnect(self): self.listener.stopListening() return True
class BC(Processor): usage = u'bc <expression>' features = ('bc', ) bc = Option('bc', 'Path to bc executable', 'bc') bc_timeout = FloatOption('bc_timeout', 'Maximum BC execution time (sec)', 2.0) def setup(self): if not file_in_path(self.bc): raise Exception("Cannot locate bc executable") @match(r'^bc\s+(.+)$') def calculate(self, event, expression): bc = Popen([self.bc, '-l'], stdin=PIPE, stdout=PIPE, stderr=PIPE) start_time = time() bc.stdin.write(expression.encode('utf-8') + '\n') bc.stdin.close() while bc.poll() is None and time() - start_time < self.bc_timeout: sleep(0.1) if bc.poll() is None: kill(bc.pid, SIGTERM) event.addresponse(u'Sorry, that took too long. I stopped waiting') return output = bc.stdout.read() error = bc.stderr.read() code = bc.wait() if code == 0: if output: output = unicode_output(output.strip()) output = output.replace('\\\n', '') event.addresponse(output) else: error = unicode_output(error.strip()) error = error.split(":", 1)[1].strip() error = error[0].lower() + error[1:].split('\n')[0] event.addresponse(u"I'm sorry, I couldn't deal with the %s", error) else: event.addresponse(u"Error running bc") error = unicode_output(error.strip()) raise Exception("BC Error: %s" % error)
class StaticFactoid(Processor): priority = 900 extras = Option('static', 'List of static factoids using regexes', {}) def setup(self): self.factoids = static_default.copy() self.factoids.update(self.extras) @handler def static(self, event): for factoid in self.factoids.values(): for match in factoid['matches']: if re.search(match, event.message['stripped'], re.I|re.DOTALL): event.addresponse(_interpolate(choice(factoid['responses']), event), address=False) return
class Man(Processor): usage = u'man [<section>] <page>' features = ('man',) man = Option('man', 'Path of the man executable', 'man') def setup(self): if not file_in_path(self.man): raise Exception("Cannot locate man executable") @match(r'^man\s+(?:(\d)\s+)?(\S+)$') def handle_man(self, event, section, page): command = [self.man, page] if section: command.insert(1, section) if page.strip().startswith("-"): event.addresponse(False) return env = os.environ.copy() env["COLUMNS"] = "500" man = Popen(command, stdout=PIPE, stderr=PIPE, env=env) output, error = man.communicate() code = man.wait() if code != 0: event.addresponse(u'Manpage not found') else: output = unicode_output(output.strip(), errors="replace") output = output.splitlines() index = output.index('NAME') if index: event.addresponse(output[index+1].strip()) index = output.index('SYNOPSIS') if index: event.addresponse(output[index+1].strip())
class SourceFactory(IbidSourceFactory): port = IntOption('port', 'Port number to listen on', 8080) myurl = Option('url', 'URL to advertise') def __init__(self, name): IbidSourceFactory.__init__(self, name) root = Plugin(name) root.putChild('', Index(name)) root.putChild('message', Message(name)) root.putChild('static', static.File(locate_resource('ibid', 'static'))) root.putChild('RPC2', XMLRPC()) root.putChild('SOAP', SOAP()) self.site = server.Site(root) def setServiceParent(self, service): if service: return internet.TCPServer(self.port, self.site).setServiceParent(service) else: reactor.listenTCP(self.port, self.site) def url(self): return self.myurl
class Tracepath(Processor): usage = u'tracepath <host>' features = ('tracepath',) tracepath = Option('tracepath', 'Path to tracepath executable', 'tracepath') def setup(self): if not file_in_path(self.tracepath): raise Exception("Cannot locate tracepath executable") @match(r'^tracepath\s+(\S+)$') def handle_tracepath(self, event, host): tracepath = Popen([self.tracepath, host], stdout=PIPE, stderr=PIPE) output, error = tracepath.communicate() code = tracepath.wait() if code == 0: output = unicode_output(output) event.addresponse(output, conflate=False) else: error = unicode_output(error.strip()) event.addresponse(u'Error: %s', error.replace(u'\n', u' '))
class Units(Processor): usage = u'convert [<value>] <unit> to <unit>' features = ('units', ) priority = 10 units = Option('units', 'Path to units executable', 'units') temp_scale_names = { 'fahrenheit': 'tempF', 'f': 'tempF', 'celsius': 'tempC', 'celcius': 'tempC', 'c': 'tempC', 'kelvin': 'tempK', 'k': 'tempK', 'rankine': 'tempR', 'r': 'tempR', } temp_function_names = set(temp_scale_names.values()) def setup(self): if not file_in_path(self.units): raise Exception("Cannot locate units executable") def format_temperature(self, unit): "Return the unit, and convert to 'tempX' format if a known temperature scale" lunit = unit.lower() if lunit in self.temp_scale_names: unit = self.temp_scale_names[lunit] elif lunit.startswith("deg") and " " in lunit and lunit.split( None, 1)[1] in self.temp_scale_names: unit = self.temp_scale_names[lunit.split(None, 1)[1]] return unit @match(r'^convert\s+(-?[0-9.]+)?\s*(.+)\s+(?:in)?to\s+(.+)$') def convert(self, event, value, frm, to): # We have to special-case temperatures because GNU units uses function notation # for direct temperature conversions if self.format_temperature(frm) in self.temp_function_names \ and self.format_temperature(to) in self.temp_function_names: frm = self.format_temperature(frm) to = self.format_temperature(to) if value is not None: if frm in self.temp_function_names: frm = "%s(%s)" % (frm, value) else: frm = '%s %s' % (value, frm) units = Popen([self.units, '--verbose', '--', frm, to], stdout=PIPE, stderr=PIPE) output, error = units.communicate() code = units.wait() output = unicode_output(output) result = output.splitlines()[0].strip() if code == 0: event.addresponse(result) elif code == 1: if result == "conformability error": event.addresponse( u"I don't think %(from)s can be converted to %(to)s", { 'from': frm, 'to': to, }) elif result.startswith("conformability error"): event.addresponse( u"I don't think %(from)s can be converted to %(to)s: %(error)s", { 'from': frm, 'to': to, 'error': result.split(":", 1)[1], }) else: event.addresponse(u"I can't do that: %s", result)
class BuildBot(Processor, RPC): usage = u'rebuild <branch> [ (revision|r) <number> ]' feature = ('buildbot', ) autoload = False server = Option('server', 'Buildbot server hostname', 'localhost') status_port = IntOption('status_port', 'Buildbot server port number', 9988) change_port = IntOption('change_port', 'Buildbot server port number', 9989) source = Option('source', 'Source to send commit notifications to') channel = Option('channel', 'Channel to send commit notifications to') def __init__(self, name): Processor.__init__(self, name) RPC.__init__(self) def setup(self): self.status = pb.PBClientFactory() reactor.connectTCP(self.server, self.status_port, self.status) d = self.status.login( credentials.UsernamePassword('statusClient', 'clientpw')) d.addCallback(self.store_root, 'status') d.addCallback( lambda root: root.callRemote('subscribe', 'builds', 0, self)) d.addErrback(self.exception) self.change = pb.PBClientFactory() reactor.connectTCP(self.server, self.change_port, self.change) d = self.change.login( credentials.UsernamePassword('change', 'changepw')) d.addCallback(self.store_root, 'change') d.addErrback(self.exception) def remote_built(self, branch, revision, person, result): reply = u"Build %s of %s triggered by %s: %s" % (revision, branch, person, result) ibid.dispatcher.send({ 'reply': reply, 'source': self.source, 'target': self.channel }) return True @match(r'^(?:re)?build\s+(.+?)(?:\s+(?:revision|r)?\s*(\d+))?$') def build(self, event, branch, revision): change = { 'who': str(event.sender['nick']), 'branch': str(branch), 'files': [None], 'revision': revision and str(revision) or '-1', 'comments': 'Rebuild', } d = self.change_root.callRemote('addChange', change) d.addCallback(self.respond, event, True) d.addErrback(self.respond, event, False) event.processed = True def respond(self, rpc_response, event, result): ibid.dispatcher.send({ 'reply': result and 'Okay' or u"buildbot doesn't want to build :-(", 'source': event.source, 'target': event.channel }) def store_root(self, root, type): setattr(self, '%s_root' % type, root) return root def exception(self, exception): print exception raise exception def remote_buildsetSubmitted(self, buildset): pass def remote_builderAdded(self, builderName, builder): pass def remote_builderChangedState(self, builderName, state, foo): pass def remote_buildStarted(self, builderName, build): print "Build %s started on %s" % (builderName, build) def remote_buildETAUpdate(self, build, ETA): pass def remote_stepStarted(self, build, step): pass def remote_stepTextChanged(self, build, step, text): pass def remote_stepText2Changed(self, build, step, text2): pass def remote_stepETAUpdate(self, build, step, ETA, expectations): pass def remote_logStarted(self, build, step, log): pass def remote_logChunk(self, build, step, log, channel, text): pass def remote_logFinished(self, build, step, log): pass def remote_stepFinished(self, build, step, results): pass def remote_buildFinished(self, builderName, build, results): print "Build %s finished on %s" % (builderName, build) def remote_builderRemoved(self, builderName): pass
class SourceFactory(protocol.ReconnectingClientFactory, IbidSourceFactory): protocol = DCBot supports = ['multiline', 'topic'] auth = ('op', ) port = IntOption('port', 'Server port number', 411) server = Option('server', 'Server hostname') nick = Option('nick', 'DC nick', ibid.config['botname']) password = Option('password', 'Password', None) interest = Option('interest', 'User Description', '') speed = Option('speed', 'Bandwidth', '1kbps') email = Option('email', 'eMail Address', 'http://ibid.omnia.za.net/') sharesize = IntOption('sharesize', 'DC Share Size (bytes)', 0) slots = IntOption('slots', 'DC Open Slots', 0) action_prefix = Option('action_prefix', 'Command for actions (i.e. +me)', None) banned_prefixes = Option('banned_prefixes', 'Prefixes not allowed in bot responses, i.e. !', '') max_message_length = IntOption('max_message_length', 'Maximum length of messages', 490) ping_interval = FloatOption('ping_interval', 'Seconds idle before sending a PING', 60) pong_timeout = FloatOption('pong_timeout', 'Seconds to wait for PONG', 300) # ReconnectingClient uses this: maxDelay = IntOption('max_delay', 'Max seconds to wait inbetween reconnects', 900) factor = FloatOption('delay_factor', 'Factor to multiply delay inbetween reconnects by', 2) def __init__(self, name): IbidSourceFactory.__init__(self, name) self.log = logging.getLogger('source.%s' % self.name) self._auth = {} def setup(self): if self.action_prefix is None and 'action' in self.supports: self.supports.remove('action') if self.action_prefix is not None and 'action' not in self.supports: self.supports.append('action') def setServiceParent(self, service): if service: internet.TCPClient(self.server, self.port, self).setServiceParent(service) else: reactor.connectTCP(self.server, self.port, self) def connect(self): return self.setServiceParent(None) def disconnect(self): self.stopTrying() self.stopFactory() if hasattr(self, 'proto'): self.proto.transport.loseConnection() return True def truncation_point(self, response, event=None): return self.max_message_length def _dc_auth_callback(self, nick, result): self._auth[nick] = result def auth_op(self, event, credential): nick = event.sender['nick'] if nick in self.proto.hub_users and self.proto.hub_users[nick].op in ( True, False): return self.proto.hub_users[nick].op reactor.callFromThread(self.proto.authenticate, nick, self._dc_auth_callback) for i in xrange(150): if nick in self._auth: break sleep(0.1) if nick in self._auth: result = self._auth[nick] del self._auth[nick] return result def url(self): return u'dc://%s@%s:%s' % (self.nick, self.server, self.port)
class SourceFactory(IbidSourceFactory, smtp.SMTPFactory): supports = ('multiline',) port = IntOption('port', 'Port number to listen on', 10025) address = Option('address', 'Email address to accept messages for and send from', 'ibid@localhost') accept = ListOption('accept', 'Email addresses to accept messages for', []) relayhost = Option('relayhost', 'SMTP server to relay outgoing messages to') def __init__(self, name): IbidSourceFactory.__init__(self, name) self.log = logging.getLogger('source.%s' % name) self.delivery = IbidDelivery(self) def buildProtocol(self, addr): p = smtp.SMTPFactory.buildProtocol(self, addr) p.delivery = self.delivery return p def setServiceParent(self, service): self.service = service if service: internet.TCPServer(self.port, self).setServiceParent(service) else: reactor.listenTCP(self.port, self) def url(self): return u'mailto:%s' % (self.address,) def respond(self, event): messages = {} for response in event.responses: if response['target'] not in messages: messages[response['target']] = response else: messages[response['target']]['reply'] += '\n' + response['reply'] for message in messages.values(): if 'subject' not in message: message['Subject'] = 'Re: ' + event['subject'] if 'message-id' in event.headers: response['In-Reply-To'] = event.headers['message-id'] if 'references' in event.headers: response['References'] = '%(references)s %(message-id)s' % event.headers elif 'in-reply-to' in event.headers: response['References'] = '%(in-reply-to)s %(message-id)s' % event.headers else: response['References'] = '%(message-id)s' % event.headers self.send(message) def send(self, response): message = response['reply'] response['To'] = response['target'] response['Date'] = smtp.rfc822date() if 'Subject' not in response: response['Subject'] = 'Message from %s' % ibid.config['botname'] response['Content-Type'] = 'text/plain; charset=utf-8' del response['target'] del response['source'] del response['reply'] body = '' for header, value in response.items(): body += '%s: %s\n' % (header, value) body += '\n' body += message port = ':' in self.relayhost and int(self.relayhost.split(':')[1]) or 25 smtp.sendmail(self.relayhost.split(':')[0], self.address, response['To'], body.encode('utf-8'), port=port) self.log.debug(u"Sent email to %s: %s", response['To'], response['Subject'])
class SourceFactory(protocol.ReconnectingClientFactory, IbidSourceFactory): protocol = Ircbot auth = ('hostmask', 'nickserv') supports = ('action', 'notice', 'topic', 'channel key') port = IntOption('port', 'Server port number', 6667) ssl = BoolOption('ssl', 'Use SSL', False) server = Option('server', 'Server hostname') nick = Option('nick', 'IRC nick', ibid.config['botname']) realname = Option('realname', 'Full Name', ibid.config['botname']) password = Option('password', 'Connection password', None) username = Option('username', 'Local username', None) modes = Option('modes', 'User modes to set') channels = ListOption('channels', 'Channels to autojoin', []) ping_interval = FloatOption('ping_interval', 'Seconds idle before sending a PING', 60) pong_timeout = FloatOption('pong_timeout', 'Seconds to wait for PONG', 300) # ReconnectingClient uses this: maxDelay = IntOption('max_delay', 'Max seconds to wait inbetween reconnects', 900) factor = FloatOption('delay_factor', 'Factor to multiply delay inbetween reconnects by', 2) def __init__(self, name): IbidSourceFactory.__init__(self, name) self._auth = {} self._auth_ticket = 0 self._auth_ticket_lock = Lock() self.log = logging.getLogger('source.%s' % self.name) def setServiceParent(self, service): if self.ssl: sslctx = ssl.ClientContextFactory() if service: internet.SSLClient(self.server, self.port, self, sslctx).setServiceParent(service) else: reactor.connectSSL(self.server, self.port, self, sslctx) else: if service: internet.TCPClient(self.server, self.port, self).setServiceParent(service) else: reactor.connectTCP(self.server, self.port, self) def connect(self): return self.setServiceParent(None) def disconnect(self): self.stopTrying() self.stopFactory() if hasattr(self, 'proto'): self.proto.transport.loseConnection() return True def join(self, channel, key=None): return self.proto.join(channel, key) def leave(self, channel): return self.proto.leave(channel) def change_nick(self, nick): return self.proto.setNick(nick.encode('utf-8')) def send(self, response): return self.proto.send(response) def logging_name(self, identity): if identity is None: return u'' return identity.split(u'!')[0] def truncation_point(self, response, event=None): target = response['target'].split('!')[0] raw_target = target.encode('utf-8') if hasattr(self.proto, 'hostmask'): hostmask_len = len(self.proto.hostmask) else: hostmask_len = 50 # max = 512 - len(':' + hostmask + ' ' + command + ' ' + target + ' :\r\n') cmds = { 'notice': len('NOTICE'), 'topic': len('TOPIC'), 'action': len('PRIVMSG\001ACTION \001') } for cmd, command_len in cmds.items(): if response.get(cmd, False): break else: command_len = len('PRIVMSG') return 505 - command_len - len(raw_target) - hostmask_len def url(self): return u'irc://%s@%s:%s' % (self.nick, self.server, self.port) def auth_hostmask(self, event, credential=None): for credential in event.session.query(Credential) \ .filter_by(method=u'hostmask', account_id=event.account) \ .filter(or_(Credential.source == event.source, Credential.source == None)) \ .all(): if fnmatch(event.sender['connection'], credential.credential): return True def auth_nickserv(self, event, credential): self._auth_ticket_lock.acquire() self._auth_ticket += 1 ticket = self._auth_ticket self._auth_ticket_lock.release() def callback(result): self._auth[ticket] = result reactor.callFromThread(self.proto.authenticate, event.sender['nick'], callback) # We block in the plugin thread for up to this long, waiting for # NickServ to reply wait = 15 for i in xrange(wait * 10): if ticket in self._auth: break sleep(0.1) if ticket in self._auth: result = self._auth[ticket] del self._auth[ticket] return result
class Translate(Processor): usage = u"""translate (<phrase>|<url>) [from <language>] [to <language>] translation chain <phrase> [from <language>] [to <language>]""" feature = ('translate',) api_key = Option('api_key', 'Your Google API Key (optional)', None) referer = Option('referer', 'The referer string to use (API searches)', default_referer) dest_lang = Option('dest_lang', 'Destination language when none is specified', 'english') chain_length = IntOption('chain_length', 'Maximum length of translation chains', 10) lang_names = {'afrikaans':'af', 'albanian':'sq', 'arabic':'ar', 'belarusian':'be', 'bulgarian':'bg', 'catalan':'ca', 'chinese':'zh', 'chinese simplified':'zh-cn', 'chinese traditional':'zh-tw', 'croatian':'hr', 'czech':'cs', 'danish':'da', 'dutch':'nl', 'english':'en', 'estonian':'et', 'filipino':'tl', 'finnish':'fi', 'french':'fr', 'galacian':'gl', 'german':'de', 'greek':'el', 'hebrew':'iw', 'hindi':'hi', 'hungarian':'hu', 'icelandic':'is', 'indonesian':'id', 'irish':'ga', 'italian':'it', 'japanese':'ja', 'korean': 'ko', 'latvian':'lv', 'lithuanian':'lt', 'macedonian':'mk', 'malay':'ms', 'maltese':'mt', 'norwegian':'no', 'persian':'fa', 'polish':'pl', 'portuguese':'pt', 'romanian':'ro', 'russian': 'ru', 'serbian':'sr', 'slovak':'sk', 'slovenian':'sl', 'spanish':'es', 'swahili':'sw', 'swedish':'sv', 'thai':'th', 'turkish':'tr', 'ukrainian':'uk', 'uzbek': 'uz', 'vietnamese':'vi', 'welsh':'cy', 'yiddish': 'yi', 'haitian creole': 'ht'} alt_lang_names = {'simplified':'zh-CN', 'simplified chinese':'zh-CN', 'traditional':'zh-TW', 'traditional chinese':'zh-TW', 'bokmal':'no', 'norwegian bokmal':'no', u'bokm\N{LATIN SMALL LETTER A WITH RING ABOVE}l':'no', u'norwegian bokm\N{LATIN SMALL LETTER A WITH RING ABOVE}l': 'no', 'farsi': 'fa', 'haitian': 'ht', 'kreyol': 'ht'} LANG_REGEX = '|'.join(lang_names.keys() + lang_names.values() + alt_lang_names.keys()) @match(r'^(?:translation\s*)?languages$') def languages (self, event): event.addresponse(human_join(sorted(self.lang_names.keys()))) @match(r'^translate\s+(.*?)(?:\s+from\s+(' + LANG_REGEX + r'))?' r'(?:\s+(?:in)?to\s+(' + LANG_REGEX + r'))?$') def translate (self, event, text, src_lang, dest_lang): dest_lang = self.language_code(dest_lang or self.dest_lang) src_lang = self.language_code(src_lang or '') if is_url(text): if urlparse(text).scheme in ('', 'http'): url = url_to_bytestring(text) query = {'sl': src_lang, 'tl': dest_lang, 'u': url} event.addresponse(u'http://translate.google.com/translate?' + urlencode(query)) else: event.addresponse(u'I can only translate HTTP pages') return try: translated = self._translate(event, text, src_lang, dest_lang)[0] event.addresponse(translated) except TranslationException, e: event.addresponse(u"I couldn't translate that: %s.", unicode(e))
class Summon(Processor): usage = u'summon <person> [via <source>]' features = ('summon', ) permission = u'summon' default_source = Option('default_source', u'Default source to summon people via', u'jabber') @authorise(fallthrough=False) @match(r'^summon\s+(\S+)(?:\s+(?:via|on|using)\s+(\S+))?$') def summon(self, event, who, source): if not source: source = self.default_source if source.lower() not in ibid.sources: event.addresponse(u"I'm afraid that I'm not connected to %s", source) return account = event.session.query(Account) \ .options(eagerload('identities')) \ .join('identities') \ .filter( or_( and_( Identity.identity == who, Identity.source == event.source, ), Account.username == who, )) \ .first() if account: for other_identity in [ id for id in account.identities if id.source.lower() == source.lower() ]: if any(True for channel in ibid.channels[ other_identity.source].itervalues() if other_identity.id in channel): event.addresponse( u'Your presence has been requested by ' u'%(who)s in %(channel)s on %(source)s.', { 'who': event.sender['nick'], 'channel': (not event.public) and u'private' or event.channel, 'source': event.source, }, target=other_identity.identity, source=other_identity.source, address=False) event.addresponse(True) else: event.addresponse( u"Sorry %s doesn't appear to be available right now.", who) return event.addresponse( u"Sorry, I don't know how to find %(who)s on %(source)s. " u'%(who)s must first link an identity on %(source)s.', { 'who': who, 'source': source, }) return
class Ports(Processor): usage = u"""port for <protocol> (tcp|udp) port <number>""" features = ('ports',) priority = 10 services = Option('services', 'Path to services file', '/etc/services') nmapservices = Option('nmap-services', "Path to Nmap's services file", '/usr/share/nmap/nmap-services') protocols = {} ports = {} def setup(self): self.filename = None if exists(self.nmapservices): self.filename = self.nmapservices self.nmap = True elif exists(self.services): self.filename = self.services self.nmap = False if not self.filename: raise Exception(u"Services file doesn't exist") def _load_services(self): if self.protocols: return self.protocols = defaultdict(list) self.ports = defaultdict(list) f = open(self.filename) for line in f.readlines(): parts = line.split() if parts and not parts[0].startswith('#') and parts[0] != 'unknown': number, transport = parts[1].split('/') port = '%s (%s)' % (number, transport.upper()) self.protocols[parts[0].lower()].append(port) self.ports[parts[1]].append(parts[0]) if not self.nmap: for proto in parts[2:]: if proto.startswith('#'): break self.protocols[proto.lower()].append(port) @match(r'(?:{proto:any} )?ports?(?: numbers?)?(?(1)| for {proto:any})') def portfor(self, event, proto): self._load_services() protocol = proto.lower() if protocol in self.protocols: event.addresponse(human_join(self.protocols[protocol])) else: event.addresponse(u"I don't know about that protocol") @match(r'^(?:(udp|tcp|sctp)\s+)?port\s+(\d+)$') def port(self, event, transport, number): self._load_services() results = [] if transport: results.extend(self.ports.get('%s/%s' % (number, transport.lower()), [])) else: for transport in ('tcp', 'udp', 'sctp'): results.extend('%s (%s)' % (protocol, transport.upper()) for protocol in self.ports.get('%s/%s' % (number, transport.lower()), [])) if results: event.addresponse(human_join(results)) else: event.addresponse(u"I don't know about any protocols using that port")
class IPCalc(Processor): usage = u"""ipcalc <network>/<subnet> ipcalc <address> - <address>""" features = ('ipcalc',) ipcalc = Option('ipcalc', 'Path to ipcalc executable', 'ipcalc') def setup(self): if not file_in_path(self.ipcalc): raise Exception("Cannot locate ipcalc executable") def call_ipcalc(self, parameters): ipcalc = Popen([self.ipcalc, '-n', '-b'] + parameters, stdout=PIPE, stderr=PIPE) output, error = ipcalc.communicate() code = ipcalc.wait() output = unicode_output(output) return (code, output, error) @match(r'^ipcalc\s+((?:\d{1,3}\.){3}\d{1,3}|(?:0x)?[0-9A-F]{8})' r'(?:(?:/|\s+)((?:\d{1,3}\.){3}\d{1,3}|\d{1,2}))?$') def ipcalc_netmask(self, event, address, netmask): address = address if netmask: address += u'/' + netmask code, output, error = self.call_ipcalc([address]) if code == 0: if output.startswith(u'INVALID ADDRESS'): event.addresponse(u"That's an invalid address. " u"Try something like 192.168.1.0/24") else: response = {} for line in output.splitlines(): if ":" in line: name, value = [x.strip() for x in line.split(u':', 1)] name = name.lower() if name == "netmask": value, response['cidr'] = value.split(' = ') elif name == "hosts/net": value, response['class'] = value.split(None, 1) response[name] = value event.addresponse(u'Host: %(address)s/%(netmask)s (/%(cidr)s) ' u'Wildcard: %(wildcard)s | ' u'Network: %(network)s (%(hostmin)s - %(hostmax)s) ' u'Broadcast: %(broadcast)s Hosts: %(hosts/net)s %(class)s', response) else: error = unicode_output(error.strip()) event.addresponse(error.replace(u'\n', u' ')) @match(r'^ipcalc\s+((?:\d{1,3}\.){3}\d{1,3}|(?:0x)?[0-9A-F]{8})\s*-\s*' r'((?:\d{1,3}\.){3}\d{1,3}|(?:0x)?[0-9A-F]{8})$') def ipcalc_deggregate(self, event, frm, to): code, output, error = self.call_ipcalc([frm, '-', to]) if code == 0: if output.startswith(u'INVALID ADDRESS'): event.addresponse(u"That's an invalid address. " u"Try something like 192.168.1.0") else: event.addresponse(u'Deaggregates to: %s', human_join(output.splitlines()[1:])) else: error = unicode_output(error.strip()) event.addresponse(error.replace(u'\n', u' '))
class MemoryLog(Processor): feature = ('memory', ) autoload = False mem_filename = Option('mem_filename', 'Memory log filename', 'logs/memory.log') mem_interval = IntOption('mem_interval', 'Interval between memory stat logging', 0) obj_filename = Option('obj_filename', 'Object Statistics log filename', 'logs/objstats.log') obj_interval = IntOption('obj_interval', 'Interval between logging object statistics', 0) def setup(self): fns = [] if self.mem_interval: fns.append(self.mem_filename) if self.obj_interval: fns.append(self.obj_filename) for filename in fns: if os.path.isfile(filename + '.10.gz'): os.remove(filename + '.10.gz') for i in range(9, 0, -1): if os.path.isfile('%s.%i.gz' % (filename, i)): os.rename('%s.%i.gz' % (filename, i), '%s.%i.gz' % (filename, i + 1)) if os.path.isfile(filename): o = gzip.open(filename + '.1.gz', 'wb') i = open(filename, 'rb') o.write(i.read()) o.close() i.close() stat = os.stat(filename) os.utime(filename + '.1.gz', (stat.st_atime, stat.st_mtime)) if self.mem_interval: self.mem_file = file(self.mem_filename, 'w+') self.mem_file.write('Ibid Memory Log v2: %s\n' % ibid.config['botname']) self.mem_csv = csv.writer(self.mem_file) self.mem_last = datetime.utcnow() if self.obj_interval: self.obj_file = file(self.obj_filename, 'w+') self.obj_file.write('Ibid Object Log v1: %s\n' % ibid.config['botname']) self.obj_last = datetime.utcnow() def process(self, event): if self.mem_interval and event.time - self.mem_last >= \ timedelta(seconds=self.mem_interval): self.mem_log() self.mem_last = event.time if self.obj_interval and event.time - self.obj_last >= \ timedelta(seconds=self.obj_interval): self.obj_log() self.obj_last = event.time def mem_log(self): status = get_memusage() gc.collect() self.mem_csv.writerow(( datetime.utcnow().isoformat(), len(gc.get_objects()), status['VmSize'], status['VmRSS'], )) self.mem_file.flush() def obj_log(self): self.obj_file.write( '%s %s\n' % (datetime.utcnow().isoformat(), json.dumps(objgraph.typestats()))) self.obj_file.flush()
class Dict(Processor): usage = u"""spell <word> [using <strategy>] define <word> [using <dictionary>] (dictionaries|strategies) (dictionary|strategy) <name>""" feature = ('dict',) server = Option('server', 'Dictionary server hostname', 'localhost') port = IntOption('port', 'Dictionary server port number', 2628) @staticmethod def reduce_suggestions(suggestions): "Remove duplicate suggestions and suffixes" output = [] for s in suggestions: s = s.getword() if not s.startswith('-') and s not in output: output.append(s) return output @match(r'^(?:define|dict)\s+(.+?)(?:\s+using\s+(.+))?$') def define(self, event, word, dictionary): connection = Connection(self.server, self.port) dictionary = dictionary is None and '*' or dictionary.lower() dictionaries = connection.getdbdescs().keys() if dictionary != '*' and dictionary not in dictionaries: event.addresponse( u"I'm afraid I don't have a dictionary of that name. I know about: %s", human_join(sorted(dictionaries))) return definitions = connection.define(dictionary, word.encode('utf-8')) if definitions: event.addresponse(u', '.join(d.getdefstr() for d in definitions)) else: suggestions = connection.match(dictionary, 'lev', word.encode('utf-8')) if suggestions: event.addresponse( u"I don't know about %(word)s. Maybe you meant %(suggestions)s?", { 'word': word, 'suggestions': human_join( self.reduce_suggestions(suggestions), conjunction=u'or'), }) else: event.addresponse(u"I don't have a definition for that. Is it even a word?") @match(r'^spell\s+(.+?)(?:\s+using\s+(.+))?$') def handle_spell(self, event, word, strategy): connection = Connection(self.server, self.port) word = word.encode('utf-8') strategies = connection.getstratdescs().keys() if connection.match('*', 'exact', word): event.addresponse(choice(( u'That seems correct. Carry on', u'Looks good to me', u"Yup, that's a word all right", u'Yes, you *can* spell', ))) return strategy = strategy is None and 'lev' or strategy.lower() if strategy not in strategies: event.addresponse( u"I'm afraid I don't know about such a strategy. I know about: %s", human_join(sorted(strategies))) suggestions = connection.match('*', strategy, word) if suggestions: event.addresponse(u'Suggestions: %s', human_join( self.reduce_suggestions(suggestions), conjunction=u'or')) else: event.addresponse(u"That doesn't seem correct, but I can't find anything to suggest") @match(r'^dictionaries$') def handle_dictionaries(self, event): connection = Connection(self.server, self.port) dictionaries = connection.getdbdescs() event.addresponse(u'My Dictionaries: %s', human_join(sorted(dictionaries.keys()))) @match(r'^strater?gies$') def handle_strategies(self, event): connection = Connection(self.server, self.port) strategies = connection.getstratdescs() event.addresponse(u'My Strategies: %s', human_join(sorted(strategies.keys()))) @match(r'^dictionary\s+(.+?)$') def handle_dictionary(self, event, dictionary): connection = Connection(self.server, self.port) dictionaries = connection.getdbdescs() dictionary = dictionary.lower() if dictionary in dictionaries: event.addresponse(unicode(dictionaries[dictionary])) else: event.addresponse(u"I don't have that dictionary") @match(r'^strater?gy\s+(.+?)$') def handle_strategy(self, event, strategy): connection = Connection(self.server, self.port) strategies = connection.getstratdescs() strategy = strategy.lower() if strategy in strategies: event.addresponse(unicode(strategies[strategy])) else: event.addresponse(u"I don't have that strategy")
class Log(Processor): addressed = False processed = True event_types = (u'message', u'state', u'action', u'notice') priority = 1900 log = Option('log', 'Log file to log messages to. Can contain substitutions: source, channel, year, month, day', 'logs/%(year)d/%(month)02d/%(source)s/%(channel)s.log') timestamp_format = Option('timestamp_format', 'Format to substitute %(timestamp)s with', '%Y-%m-%d %H:%M:%S%z') date_utc = BoolOption('date_utc', 'Log with UTC timestamps', False) message_format = Option('message_format', 'Format string for messages', u'%(timestamp)s <%(sender_nick)s> %(message)s') action_format = Option('action_format', 'Format string for actions', u'%(timestamp)s * %(sender_nick)s %(message)s') notice_format = Option('notice_format', 'Format string for notices', u'%(timestamp)s -%(sender_nick)s- %(message)s') presence_format = Option('presence_format', 'Format string for presence events', u'%(timestamp)s %(sender_nick)s (%(sender_connection)s) is now %(state)s') rename_format = Option('rename_format', 'Format string for rename events', u'%(timestamp)s %(sender_nick)s (%(sender_connection)s) has renamed to %(new_nick)s') public_logs = ListOption('public_logs', u'List of source:channel globs for channels which should have public logs', []) public_mode = Option('public_mode', u'File Permissions mode for public channels, in octal', '644') private_mode = Option('private_mode', u'File Permissions mode for private chats, in octal', '640') dir_mode = Option('dir_mode', u'Directory Permissions mode, in octal', '755') fd_cache = IntOption('fd_cache', 'Number of log files to keep open.', 5) lock = Lock() logs = WeakValueDictionary() # Ensures that recently used FDs are still available in logs: recent_logs = [] def setup(self): sources = list(set(ibid.config.sources.keys()) | set(ibid.sources.keys())) for glob in self.public_logs: if u':' not in glob: log.warning(u"public_logs configuration values must follow the " u"format source:channel. \"%s\" doesn't contain a " u"colon.", glob) continue source_glob = glob.split(u':', 1)[0] if not fnmatch.filter(sources, source_glob): log.warning(u'public_logs includes "%s", but there is no ' u'configured source matching "%s"', glob, source_glob) def get_logfile(self, event): self.lock.acquire() try: when = event.time if not self.date_utc: when = when.replace(tzinfo=tzutc()).astimezone(tzlocal()) if event.channel is not None: channel = ibid.sources[event.source].logging_name(event.channel) else: channel = ibid.sources[event.source].logging_name(event.sender['id']) filename = self.log % { 'source': event.source.replace('/', '-'), 'channel': channel.replace('/', '-'), 'year': when.year, 'month': when.month, 'day': when.day, 'hour': when.hour, 'minute': when.minute, 'second': when.second, } filename = join(ibid.options['base'], expanduser(filename)) log = self.logs.get(filename, None) if log is None: try: makedirs(dirname(filename), int(self.dir_mode, 8)) except OSError, e: if e.errno != EEXIST: raise e log = open(filename, 'a') self.logs[filename] = log for glob in self.public_logs: if u':' not in glob: continue source_glob, channel_glob = glob.split(u':', 1) if (fnmatch.fnmatch(event.source, source_glob) and fnmatch.fnmatch(channel, channel_glob)): chmod(filename, int(self.public_mode, 8)) break else: chmod(filename, int(self.private_mode, 8)) else: