class DrawImage(Processor): usage = u'draw <url> [in colour] [width <width>] [height <height>]' features = ('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(iri_to_uri(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 Retrieve(Processor): usage = u"""latest [ <count> ] articles from <name> [ starting at <number> ] article ( <number> | /<pattern>/ ) from <name>""" feature = ('feeds', ) interval = IntOption('interval', 'Feed Poll interval (in seconds)', 300) @match(r'^(?:latest|last)\s+(?:(\d+)\s+)?articles\s+from\s+(.+?)' r'(?:\s+start(?:ing)?\s+(?:at\s+|from\s+)?(\d+))?$') def list(self, event, number, name, start): number = number and int(number) or 10 start = start and int(start) or 0 feed = event.session.query(Feed).filter_by(name=name).first() if not feed: event.addresponse(u"I don't know about the %s feed", name) return feed.update() if not feed.entries: event.addresponse(u"I can't find any articles in that feed") return articles = feed.entries[start:number + start] articles = [ u'%s: "%s"' % (feed.entries.index(entry) + 1, html2text_file(entry.title, None).strip()) for entry in articles ] event.addresponse(u', '.join(articles)) @match(r'^article\s+(?:(\d+)|/(.+?)/)\s+from\s+(.+?)$') def article(self, event, number, pattern, name): feed = event.session.query(Feed).filter_by(name=name).first() if not feed: event.addresponse(u"I don't know about the %s feed", name) return feed.update() if not feed.entries: event.addresponse(u"I can't access that feed") return article = None if number: if int(number) > len(feed.entries) or 1 > int(number): event.addresponse(u"That's old news dude") return article = feed.entries[int(number) - 1] else: pattern = re.compile(pattern, re.I) for entry in feed.entries: if pattern.search(entry.title): article = entry break if not article: event.addresponse(u'Are you making up news again?') return if 'summary' in article: summary = html2text_file(article.summary, None) else: if article.content[0].type in \ ('application/xhtml+xml', 'text/html'): summary = html2text_file(article.content[0].value, None) else: summary = article.content[0].value event.addresponse( u'"%(title)s" %(link)s : %(summary)s', { 'title': html2text_file(article.title, None).strip(), 'link': article.link, 'summary': summary, }) last_seen = {} @periodic(config_key='interval') def poll(self, event): feeds = event.session.query(Feed) \ .filter(Feed.source != None) \ .filter(Feed.target != None).all() for feed in feeds: try: feed.update(max_age=self.interval) except Exception, e: if isinstance(e, URLError): log.warning( u'Exception "%s" occured while polling ' u'feed %s from %s', e, feed, feed.url) else: log.exception( u'Exception "%s" occured while polling ' u'feed %s from %s', e, feed, feed.url) continue if not feed.entries: continue if feed.name not in self.last_seen: seen = {} for entry in feed.entries: id = entry.get('id', entry.title) seen[id] = entry.updated_parsed self.last_seen[feed.name] = seen continue old_seen = self.last_seen[feed.name] seen = {} for entry in reversed(feed.entries): id = entry.get('id', entry.title) seen[id] = entry.updated_parsed if entry.updated_parsed != old_seen.get(id): event.addresponse( u"%(status)s item in %(feed)s: %(title)s", { 'status': id in old_seen and u'Updated' or u'New', 'feed': feed.name, 'title': entry.title, }, source=feed.source, target=feed.target, adress=False) self.last_seen[feed.name] = seen
class LogWatch(Processor): interval = IntOption('interval', 'Update Poll interval (in seconds)', 1) files = {} def incoming(self, target, filename, group, data): reply = u" == %(filename)s (%(group)s) == \n%(data)s" % { u'group': group, u'filename': filename, u'data': data.strip() } ibid.dispatcher.send({ 'reply': reply, 'source': 'jabber', 'target': target, }) def setup(self): super(LogWatch, self).setup() # Start tailing all the setup alerts immediatly for section, group in groups.iteritems(): for target in group['alerts']: for filename in group['files']: self.add_tail(target, filename, filename) @periodic(config_key='interval', initial_delay=15) def update(self, event): for filename, obj in self.files.iteritems(): # Try call the callback if we can data = obj['fileobj'].read() if data: for (target, group) in obj['events']: self.incoming(target, filename, group, data) # Try get the latest file stats try: nstat = os.stat(filename) except: nstat = obj['fstat'] # Check if the file changed, if so start from beginning again if nstat[stat.ST_DEV] != obj['fstat'][stat.ST_DEV] or nstat[ stat.ST_INO] != obj['fstat'][stat.ST_INO]: obj['fileobj'] = open(filename) obj['fstat'] = os.fstat(obj['fileobj'].fileno()) def add_tail(self, target, filename, name): if not filename in self.files: fileobj = open(filename) fileobj.seek(0, 2) self.files[filename] = { 'fileobj': fileobj, 'fstat': os.fstat(fileobj.fileno()), 'events': [] } for event in self.files[filename]['events']: if event[0] == target: return False self.files[filename]['events'].append((target, name)) return True def remove_tail(self, target, filename): if not filename in self.files: return self.files[filename]['events'] = [ x for x in self.files[filename]['events'] if x[0] != target ] @match(r'^tail ([/^ ]+|"/[^"]*")$') def tail_file(self, event, filename): # Strip off quotes if needed if filename[0] == filename[-1] == '"': filename = filename[1:-1] if not os.path.exists(filename): event.addresponse(u"Sorry, I could not find %(filename)s", {u'filename': filename}) else: event.addresponse(u"Aiight, I'm watching %(filename)s for you.", {u'filename': filename}) self.add_tail(event['sender']['id'], filename, 'manual') @match(r'^tail ([a-z-]+)$') def tail_group(self, event, group): if group in groups: for filename in groups[group]['files']: self.add_tail(event['sender']['id'], filename, group) event.addresponse(u"Aiight, I'm watching %(group)s for you.", {u'group': group}) else: event.addresponse(u"Sorry, I could not find the group %(group)s", {u'group': group}) @match(r'^untail ([a-z-]+)$') def untail_group(self, event, group): if group in groups: for filename in groups[group]['files']: self.remove_tail(event['sender']['id'], filename) event.addresponse(u"Aiight, You're no longer tailing %(group)s.", {u'group': group}) else: event.addresponse(u"Sorry, I could not find the group %(group)s", {u'group': group})
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 Bazaar(Processor, RPC): usage = u"""(last commit|commit <revno>) [to <repo>] [full] repositories""" features = ('bzr', ) autoload = False repositories = DictOption('repositories', 'Dict of repositories names and URLs') interval = IntOption('interval', 'Interval inbetween checks for new revisions', 300) def __init__(self, name): self.log = logging.getLogger('plugins.bzr') Processor.__init__(self, name) RPC.__init__(self) def setup(self): self.branches = {} must_monitor = False for name, repository in self.repositories.items(): try: self.branches[name.lower()] = Branch.open(repository['url']) except NotBranchError: self.log.error(u'%s is not a branch', repository) continue if repository.get('poll', 'False').lower() in ('yes', 'true'): must_monitor = True self.check.im_func.disabled = not must_monitor if must_monitor: self.seen_revisions = {} @match(r'^(?:repos|repositories)$') def handle_repositories(self, event): repositories = self.branches.keys() if repositories: event.addresponse(u'I know about: %s', human_join(sorted(repositories))) else: event.addresponse(u"I don't know about any repositories") def remote_committed(self, repository, start, end=None): commits = self.get_commits(repository, start, end) repo = self.repositories[repository] for commit in commits: ibid.dispatcher.send({ 'reply': commit, 'source': repo['source'], 'target': repo['channel'], }) return True @match( r'^(?:last\s+)?commit(?:\s+(\d+))?(?:(?:\s+to)?\s+(\S+?))?(\s+full)?$') def commit(self, event, revno, repository, full): revno = revno and int(revno) or None commits = self.get_commits(repository, revno, full=full) output = u'' for commit in commits: if commit: output += commit.strip() event.addresponse(output, conflate=False) def get_commits(self, repository, start, end=None, full=None): branch = None if repository: repository = repository.lower() if repository not in self.branches: return None branch = self.branches[repository] if not branch: if len(self.branches) == 1: (repository, branch) = self.branches.items()[0] else: (repository, branch) = sorted(self.branches.iteritems(), reverse=True, key=lambda (k, v): v.repository.get_revision( v.last_revision_info()[1]).timestamp)[0] if not start: start = branch.revision_id_to_revno(branch.last_revision()) f = StringIO() log.show_log(branch, LogFormatter(f, repository, branch, full), start_revision=start, end_revision=end or start) f.seek(0) commits = f.readlines() commits.reverse() return commits @handler def launchpad(self, event): if ibid.sources[event.source].type != 'smtp' \ or 'X-Launchpad-Branch' not in event.headers: return event.processed = True if 'X-Launchpad-Branch' not in event.headers or 'X-Launchpad-Branch-Revision-Number' not in event.headers: return for name, repository in self.repositories.iteritems(): if (event.headers['X-Launchpad-Branch'] == repository.get( 'lp_branch', None)): self.remote_committed( name, int(event.headers['X-Launchpad-Branch-Revision-Number'])) @periodic(config_key='interval') def check(self, event): for name, repo in self.repositories.iteritems(): if repo.get('poll', 'False').lower() not in ('yes', 'true'): continue branch = self.branches[name] lastrev = branch.last_revision() if name not in self.seen_revisions: self.seen_revisions[name] = lastrev continue if lastrev == self.seen_revisions[name]: continue try: commits = self.get_commits(name, None, False) except RevisionNotPresent: self.log.debug( u"Got a RevisionNotPresent, hoping it won't be there next time..." ) continue self.seen_revisions[name] = lastrev if commits: event.addresponse(unicode(commits[0].strip()), source=repo['source'], target=repo['channel'], address=False)
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 HTTP(Processor): usage = u"""(get|head) <url> is <domain> (up|down) tell me when <domain|url> is up""" features = ('http',) priority = -10 max_size = IntOption('max_size', u'Only request this many bytes', 2048) timeout = IntOption('timeout', u'Timeout for HTTP connections in seconds', 15) sites = DictOption('sites', u'Mapping of site names to domains', {}) redirect_limit = IntOption('redirect_limit', u'Maximum number of http redirects to follow', 5) whensitup_delay = IntOption('whensitup_delay', u'Initial delay between whensitup attempts in seconds', 60) whensitup_factor = FloatOption('whensitup_factor', u'Factor to mutliply subsequent delays by for whensitup', 1.03) whensitup_maxdelay = IntOption('whensitup_maxdelay', u'Maximum delay between whensitup attempts in seconds', 30*60) whensitup_maxperiod = FloatOption('whensitup_maxperiod', u'Maximum period after which to stop checking the url ' u'for whensitup in hours', 72) def _get_header(self, headers, name): for header in headers: if header[0] == name: return header[1] return None @match(r'^(get|head)\s+(\S+)$') def get(self, event, action, url): try: status, reason, data, headers = self._request(self._makeurl(url), action.upper()) reply = u'%s %s' % (status, reason) hops = 0 while 300 <= status < 400 and self._get_header(headers, 'location'): location = self._get_header(headers, 'location') if not location: break status, reason, data, headers = self._request(location, 'GET') if hops >= self.redirect_limit: reply += u' to %s' % location break hops += 1 reply += u' to %(location)s, which gets a ' \ u'%(status)d %(reason)s' % { u'location': location, u'status': status, u'reason': reason, } if action.upper() == 'GET': got_title = False content_type = self._get_header(headers, 'content-type') if content_type and (content_type.startswith('text/html') or content_type.startswith('application/xhtml+xml')): match = re.search(r'<title>(.*)<\/title>', data, re.I | re.DOTALL) if match: got_title = True reply += u' "%s"' % match.groups()[0].strip() if not got_title and content_type: reply += u' ' + content_type event.addresponse(reply) except HTTPException, e: event.addresponse(unicode(e))
class ICECast(Processor): usage = u"what's playing [on <stream>]?" feature = ('icecast', ) interval = IntOption('interval', 'Interval between checking for song changes', 60) streams = DictOption( 'streams', 'Dictionary of Stream names to base URL (include trailing /)', {}) last_checked = None last_songs = {} def scrape_status(self, stream): tree = get_html_parse_tree(self.streams[stream]['url'] + 'status.xsl', treetype='etree') main_table = tree.findall('.//table')[2] status = {} for row in main_table.findall('.//tr'): key, value = [x.text for x in row.findall('td')] status[key[:-1]] = value return status @match(r'^what(?:\'|\s+i)s\s+playing(?:\s+on\s+(.+))?$') def playing(self, event, stream): if not event.get('addressed', False): return if len(self.streams) == 0: event.addresponse(u"Sorry, I don't know about any streams") return elif stream is None and len(self.streams) == 1: stream = self.streams.keys()[0] elif stream is not None and stream not in self.streams: for name in self.streams.iterkeys(): if name.lower() == stream.lower(): stream = name break else: stream = None if stream is None: event.addresponse( u'Sorry, I only know about the following streams, ' u'please choose one: %s', human_join(self.streams.keys())) return try: status = self.scrape_status(stream) event.addresponse( u'Currently Playing on %(stream)s: ' u'%(song)s - %(description)s (Listeners: %(listeners)s)', { 'stream': stream, 'song': status['Current Song'], 'description': status['Stream Description'], 'listeners': status['Current Listeners'], }) except HTTPError: event.addresponse( u'The stream must be down, back to the MP3 collection for you') @periodic(config_key='interval') def check(self, event): for name, stream in self.streams.iteritems(): if 'source' in stream and 'channel' in stream: log.debug(u'Probing %s', name) status = self.scrape_status(name) if self.last_songs.get(name, '') != status['Current Song']: self.last_songs[name] = status['Current Song'] event.addresponse( u'Now Playing on %(stream)s: ' u'%(song)s - %(description)s ' u'(Listeners: %(listeners)s)', { 'stream': name, 'song': status['Current Song'], 'description': status['Stream Description'], 'listeners': status['Current Listeners'], }, source=stream['source'], target=stream['channel'], topic=(stream.get('topic', 'False').lower() in ('yes', 'true')), address=False, )
class FlightSearch(Processor): usage = u"""airport [in] <name|location|code> [<cheapest|quickest>] flight from <departure> to <destination> from <depart_date> [anytime|morning|afternoon|evening|<time>] to <return_date> [anytime|morning|afternoon|evening|<time>]""" features = ('flight',) airports_url = u'http://openflights.svn.sourceforge.net/viewvc/openflights/openflights/data/airports.dat' max_results = IntOption('max_results', 'Maximum number of results to list', 5) airports = {} def read_airport_data(self): # File is listed as ISO 8859-1 (Latin-1) encoded on # http://openflights.org/data.html, but from decoding it appears to # actually be UTF8 filename = cacheable_download(self.airports_url, u'flight/airports.dat') reader = csv.reader(open(filename), delimiter=',', quotechar='"') for row in reader: self.airports[int(row[0])] = [unicode(r, u'utf-8') for r in row[1:]] def _airport_search(self, query, search_loc = True): if not self.airports: self.read_airport_data() if search_loc: ids = self._airport_search(query, False) if len(ids) == 1: return ids query = [q for q in query.lower().split()] else: query = [query.lower()] ids = [] for id, airport in self.airports.items(): if search_loc: data = (u' '.join(c.lower() for c in airport[:5])).split() elif len(query[0]) == 3: data = [airport[3].lower()] else: # assume length 4 (won't break if not) data = [airport[4].lower()] if len(filter(lambda q: q in data, query)) == len(query): ids.append(id) return ids def repr_airport(self, id): airport = self.airports[id] code = u'' if airport[3] or airport[4]: code = u' (%s)' % u'/'.join(filter(lambda c: c, airport[3:5])) return u'%s%s' % (airport[0], code) @match(r'^airports?\s+((?:in|for)\s+)?(.+)$') def airport_search(self, event, search_loc, query): search_loc = search_loc is not None if not search_loc and not 3 <= len(query) <= 4: event.addresponse(u'Airport code must be 3 or 4 characters') return ids = self._airport_search(query, search_loc) if len(ids) == 0: event.addresponse(u"Sorry, I don't know that airport") elif len(ids) == 1: id = ids[0] airport = self.airports[id] code = u'unknown code' if airport[3] and airport[4]: code = u'codes %s and %s' % (airport[3], airport[4]) elif airport[3]: code = u'code %s' % airport[3] elif airport[4]: code = u'code %s' % airport[4] event.addresponse(u'%(airport)s in %(city)s, %(country)s has %(code)s', { u'airport': airport[0], u'city': airport[1], u'country': airport[2], u'code': code, }) else: event.addresponse(u'Found the following airports: %s', human_join(self.repr_airport(id) for id in ids)[:480]) def _flight_search(self, event, dpt, to, dep_date, ret_date): airport_dpt = self._airport_search(dpt) airport_to = self._airport_search(to) if len(airport_dpt) == 0: event.addresponse(u"Sorry, I don't know the airport you want to leave from") return if len(airport_to) == 0: event.addresponse(u"Sorry, I don't know the airport you want to fly to") return if len(airport_dpt) > 1: event.addresponse(u'The following airports match the departure: %s', human_join(self.repr_airport(id) for id in airport_dpt)[:480]) return if len(airport_to) > 1: event.addresponse(u'The following airports match the destination: %s', human_join(self.repr_airport(id) for id in airport_to)[:480]) return dpt = airport_dpt[0] to = airport_to[0] def to_travelocity_date(date): date = date.lower() time = None for period in [u'anytime', u'morning', u'afternoon', u'evening']: if period in date: time = period.title() date = date.replace(period, u'') break try: date = parse(date) except ValueError: raise FlightException(u"Sorry, I can't understand the date %s" % date) if time is None: if date.hour == 0 and date.minute == 0: time = u'Anytime' else: time = date.strftime('%I:00') if time[0] == u'0': time = time[1:] if date.hour < 12: time += u'am' else: time += u'pm' date = date.strftime('%m/%d/%Y') return (date, time) (dep_date, dep_time) = to_travelocity_date(dep_date) (ret_date, ret_time) = to_travelocity_date(ret_date) params = {} params[u'leavingFrom'] = self.airports[dpt][3] params[u'goingTo'] = self.airports[to][3] params[u'leavingDate'] = dep_date params[u'dateLeavingTime'] = dep_time params[u'returningDate'] = ret_date params[u'dateReturningTime'] = ret_time etree = get_html_parse_tree('http://travel.travelocity.com/flights/InitialSearch.do', data=urlencode(params), treetype='etree') while True: script = [script for script in etree.getiterator(u'script')][1] matches = script.text and re.search(r'var finurl = "(.*)"', script.text) if matches: url = u'http://travel.travelocity.com/flights/%s' % matches.group(1) etree = get_html_parse_tree(url, treetype=u'etree') else: break # Handle error div = [d for d in etree.getiterator(u'div') if d.get(u'class') == u'e_content'] if len(div): error = div[0].find(u'h3').text raise FlightException(error) departing_flights = self._parse_travelocity(etree) return_url = None table = [t for t in etree.getiterator(u'table') if t.get(u'id') == u'tfGrid'][0] for tr in table.getiterator(u'tr'): for td in tr.getiterator(u'td'): if td.get(u'class').strip() in [u'tfPrice', u'tfPriceOrButton']: onclick = td.find(u'div/button').get(u'onclick') match = re.search(r"location.href='\.\./flights/(.+)'", onclick) url_page = match.group(1) match = re.search(r'^(.*?)[^/]*$', url) url_base = match.group(1) return_url = url_base + url_page etree = get_html_parse_tree(return_url, treetype=u'etree') returning_flights = self._parse_travelocity(etree) return (departing_flights, returning_flights, url) def _parse_travelocity(self, etree): flights = [] table = [t for t in etree.getiterator(u'table') if t.get(u'id') == u'tfGrid'][0] trs = [t for t in table.getiterator(u'tr')] tr_index = 1 while tr_index < len(trs): tds = [] while True: new_tds = [t for t in trs[tr_index].getiterator(u'td')] tds.extend(new_tds) tr_index += 1 if len(filter(lambda t: t.get(u'class').strip() == u'tfAirlineSeatsMR', new_tds)): break flight = Flight() for td in tds: if td.get(u'class').strip() == u'tfAirline': anchor = td.find(u'a') if anchor is not None: airline = anchor.text.strip() else: airline = td.text.split(u'\n')[0].strip() flight.flight.append(u'%s %s' % (airline, td.findtext(u'div').strip())) if td.get(u'class').strip() == u'tfDepart' and td.text: flight.depart_time = td.text.split(u'\n')[0].strip() flight.depart_ap = u'%s %s' % (td.findtext(u'div').strip(), td.findtext(u'div/span').strip()) if td.get(u'class').strip() == u'tfArrive' and td.text: flight.arrive_time = td.text.split(u'\n')[0].strip() span = td.find(u'span') if span is not None and span.get(u'class').strip() == u'tfNextDayDate': flight.arrive_time = u'%s %s' % (flight.arrive_time, span.text.strip()[2:]) span = [s for s in td.find(u'div').getiterator(u'span')][1] flight.arrive_ap = u'%s %s' % (td.findtext(u'div').strip(), span.text.strip()) else: flight.arrive_ap = u'%s %s' % (td.findtext(u'div').strip(), td.findtext(u'div/span').strip()) if td.get(u'class').strip() == u'tfTime' and td.text: flight.duration = td.text.strip() flight.stops = td.findtext(u'span/a').strip() if td.get(u'class').strip() in [u'tfPrice', u'tfPriceOr'] and td.text: flight.price = td.text.strip() flight.flight = human_join(flight.flight) flights.append(flight) return flights @match(r'^(?:(cheapest|quickest)\s+)?flights?\s+from\s+(.+)\s+to\s+(.+)\s+from\s+(%s)\s+to\s+(%s)$' % (DATE, DATE), simple=False) def flight_search(self, event, priority, dpt, to, dep_date, ret_date): try: flights = self._flight_search(event, dpt, to, dep_date, ret_date) except FlightException, e: event.addresponse(unicode(e)) return if flights is None: return if len(flights[0]) == 0: event.addresponse(u'No matching departure flights found') return if len(flights[1]) == 0: event.addresponse(u'No matching return flights found') return cmp = None if priority is not None: priority = priority.lower() if priority == u'cheapest': cmp = lambda a, b: a.int_price() < b.int_price() elif priority == u'quickest': cmp = lambda a, b: a.int_duration() < b.int_duration() if cmp: # select best flight based on priority for i in xrange(2): flights[i].sort(cmp=cmp) del flights[i][1:] response = [] for i, flight_type in zip(xrange(2), [u'Departing', u'Returning']): if len(flights[i]) > 1: response.append(u'%s flights:' % flight_type) for flight in flights[i][:self.max_results]: leading = u'' if len(flights[i]) == 1: leading = u'%s flight: ' % flight_type response.append(u'%(leading)s%(flight)s departing %(depart_time)s from %(depart_airport)s, arriving %(arrive_time)s at %(arrive_airport)s (flight time %(duration)s, %(stops)s) costs %(price)s per person' % { 'leading': leading, 'flight': flight.flight, 'depart_time': flight.depart_time, 'depart_airport': flight.depart_ap, 'arrive_time': flight.arrive_time, 'arrive_airport': flight.arrive_ap, 'duration': flight.duration, 'stops': flight.stops, 'price': flight.price or 'unknown' }) response.append(u'Full results: %s' % flights[2]) event.addresponse(u'\n'.join(response), conflate=False)
class Poll(Processor): usage = u""" [secret] poll on <topic> [until <time>] vote <option> [or <option>]... vote (<id> | <option>) [on <topic>] end poll """ features = ('poll', ) permission = u'chairmeeting' polls = {} date_utc = BoolOption('date_utc', u'Interpret poll end times as UTC', False) poll_time = IntOption('poll_time', u'Default poll length', 5 * 60) @authorise(fallthrough=False) @match(r'^(secret\s+)?(?:poll|ballot)\s+on\s+(.+?)\s+' r'(?:until\s+(.+?)\s+)?vote\s+(.+\sor\s.+)$') def start_poll(self, event, secret, topic, end, options): if not event.public: event.addresponse(u'Sorry, must be done in public') return if (event.source, event.channel) in self.polls: event.addresponse(u'Sorry, poll on %s in progress.', self.polls[(event.source, event.channel)].topic) return class PollContainer(object): pass poll = PollContainer() self.polls[(event.source, event.channel)] = poll poll.secret = secret is not None if end is None: poll.end = event.time + timedelta(seconds=self.poll_time) else: poll.end = parse(end) if poll.end.tzinfo is None and not self.date_utc: poll.end = poll.end.replace(tzinfo=tzlocal()) if poll.end.tzinfo is not None: poll.end = poll.end.astimezone(tzutc()).replace(tzinfo=None) if poll.end < event.time: event.addresponse(u"I can't end a poll in the past") return poll.topic = topic poll.options = re.split(r'\s+or\s+', options) poll.lower_options = [o.lower() for o in poll.options] poll.votes = {} event.addresponse( u'You heard that, voting has begun. ' u'The polls close at %(end)s. ' u'%(private)s' u'The Options Are:', { 'private': poll.secret and u'You may vote in public or private. ' or u'', 'end': format_date(poll.end), }, address=False) for i, o in enumerate(poll.options): event.addresponse(u'%(id)i: %(option)s', { 'id': i + 1, 'option': o, }, address=False) delay = poll.end - event.time poll.delayed_call = ibid.dispatcher.call_later( delay.days * 86400 + delay.seconds, self.end_poll, event) def locate_poll(self, event, selection, topic): "Attempt to find which poll the user is voting in" if event.public: if (event.source, event.channel) in self.polls: return self.polls[(event.source, event.channel)] else: if topic: polls = [ p for p in self.polls.iteritems() if p.topic.lower() == topic.lower() ] if len(polls) == 1: return polls[0] polls = [ self.polls[p] for p in self.polls.iterkeys() if p[0] == event.source ] if len(polls) == 1: return polls[0] elif len(polls) > 1: if not selection.isdigit(): possibles = [ p for p in polls if selection.lower() in p.lower_options ] if len(possibles) == 1: return possibles[0] event.addresponse( u'Sorry, I have more than one poll open. ' u'Please say "vote %s on <topic>"', selection) return event.addresponse(u'Sorry, no poll in progress') @match(r'^vote\s+(?:for\s+)?(.+?)(?:\s+on\s+(.+))?$') def vote(self, event, selection, topic): poll = self.locate_poll(event, selection, topic) log.debug(u'Poll: %s', repr(poll)) if poll is None: return if selection.isdigit() and int(selection) > 0 \ and int(selection) <= len(poll.options): selection = int(selection) - 1 else: try: selection = poll.lower_options.index(selection) except ValueError: event.addresponse( u"Sorry, I don't know of such an option for %s", poll.topic) return poll.votes[event.identity] = selection if not event.public: event.addresponse( u'Your vote on %(topic)s has been registered as %(option)s', { 'topic': poll.topic, 'option': poll.options[selection], }) else: event.processed = True @match(r'^end\s+poll$') @authorise() def end_poll(self, event): if not event.public: event.addresponse(u'Sorry, must be done in public') return if (event.source, event.channel) not in self.polls: event.addresponse(u'Sorry, no poll in progress.') return poll = self.polls.pop((event.source, event.channel)) if poll.delayed_call.active(): poll.delayed_call.cancel() votes = [[poll.options[i], 0] for i in range(len(poll.options))] for vote in poll.votes.itervalues(): votes[vote][1] += 1 votes.sort(reverse=True, key=lambda x: x[1]) event.addresponse(u'The polls are closed. Totals:', address=False) position = (1, votes[0][1]) for o, v in votes: if v < position[1]: position = (position[0] + 1, v) event.addresponse(u'%(position)i: %(option)s - %(votes)i %(word)s', { 'position': position[0], 'option': o, 'votes': v, 'word': plural(v, u'vote', u'votes'), }, address=False)
class WriteFiglet(Processor): usage = u"""figlet <text> [in <font>] list figlet fonts [from <index>]""" features = ('figlet', ) max_width = IntOption('max_width', 'Maximum width for ascii output', 60) preferred_fonts = ListOption('preferred_fonts', 'List of default figlet fonts', ('slant', 'letter')) def setup(self): self.fonts = None def _find_fonts(self): if self.fonts is None: self.fonts = Figlet().getFonts() @match(r'^list\s+figlet\s+fonts(?:\s+from\s+(\d+))?$') def list_fonts(self, event, index): self._find_fonts() if index is None: index = 0 index = int(index) if index >= len(self.fonts): event.addresponse(u'I wish I had that many fonts installed') return event.addresponse(unicode(', '.join(self.fonts[int(index):]))) @match(r'^figlet\s+(.+?)(\s+in\s+(\S+))?$', 'deaddressed') def write(self, event, text, font_phrase, font): self._find_fonts() if font is not None and font not in self.fonts: text = '%s%s' % (text, font_phrase) font = None if font is None: if self.fonts: for font in self.preferred_fonts: if font in self.fonts: break else: font = choice(self.fonts) else: event.addresponse(u"I'm afraid I have no fonts available") return self._write(event, text, font) def _render(self, text, font): figlet = Figlet(font=font) return figlet.renderText(text) def _write(self, event, text, font): rendered = self._render(text, font).split('\n') while rendered and rendered[0].strip() == '': del rendered[0] while rendered and rendered[-1].strip() == '': del rendered[-1] if rendered and len(rendered[0]) > self.max_width: event.addresponse( u"Sorry that's too long, nobody will be able to read it") return event.addresponse(unicode('\n'.join(rendered), 'utf-8'), address=False, conflate=False)
class WerewolfGame(Processor): usage = u""" start a game of werewolf join ( kill | see | eat ) <villager> vote for <villager> """ feature = ('werewolf', ) state = None player_limit = IntOption('min_players', 'The minimum number of players', 5) start_delay = IntOption('start_delay', 'How long to wait before starting, in seconds', 60) day_length = IntOption('day_length', 'Length of day / night, in seconds', 60) addressed = BoolOption('addressed', 'Messages must be addressed to bot', True) players_per_wolf = IntOption('players_per_wolf', 'Number of players to each wolf/seer', 4) seer_delay = IntOption( 'seer_delay', 'Number of players between extra wolf and extra seer', 4) event_types = (u'message', u'action') @match(r'^(?:start|play|begin)s?\b.*werewolf$') def prestart(self, event): """Initiate a game. This is the state from initiation to start of game. Next state is start. """ if self.state: log.debug(u'Not starting game: already in state %s.', self.state_name()) return if not event.public: log.debug(u'Event is not public.') event.addresponse(u'You must start the game in public.') return self.state = self.prestart self.channel = event.channel log.debug(u'Starting game.') werewolf_games.append(self) starter = event.sender['nick'] self.players = set((starter, )) event.addresponse( u'You have started a game of Werewolf. ' u'Everybody has %i seconds to join the game.', self.start_delay) self.timed_goto(event, self.start_delay, self.start) @match(r'^joins?\b') def join(self, event): if self.state != self.prestart: log.debug(u'Not joining: already in state %s.', self.state_name()) return if event.sender['nick'] not in self.players: self.players.add(event.sender['nick']) event.addresponse(u'%(player)s has joined (%(num)i players).', { 'num': len(self.players), 'player': event.sender['nick'] }, target=self.channel, address=False) else: event.addresponse(u'You have already joined the game.') def start(self, event): """Start game. Players are assigned their roles. The next state is night. """ self.state = self.start if len(self.players) < self.player_limit: event.addresponse(u'Not enough players. Try again later.') self.state = None return event.addresponse( u'%i players joined. Please wait while I assign roles.', len(self.players)) self.players = list(self.players) shuffle(self.players) nwolves = max(1, len(self.players) // self.players_per_wolf) nseers = max(1, (len(self.players) - self.seer_delay) // self.players_per_wolf) self.wolves = set(self.players[:nwolves]) self.seers = set(self.players[nwolves:nwolves + nseers]) self.roles = dict((player, 'villager') for player in self.players) del self.players for player in self.wolves: self.roles[player] = 'wolf' for player in self.seers: self.roles[player] = 'seer' for player, role in self.roles.iteritems(): event.addresponse(u'%(name)s, you are a %(role)s.', { 'name': player, 'role': role, }, target=player, address=False) if nwolves > 1 and nseers > 1: event.addresponse( u'This game has %(seers)i seers and %(wolves)i wolves.', { 'seers': nseers, 'wolves': nwolves, }) elif nwolves > 1: event.addresponse(u'This game has %i wolves.', nwolves) elif nseers > 1: event.addresponse(u'This game has %i seers.', nseers) self.timed_goto(event, 10, self.night) def night(self, event): """Start of night. Tell seer and werewolf to act. This state lasts for the whole night. The next state is dawn. """ self.state = self.night event.addresponse( u'Night falls... most villagers are sleeping, ' u'but outside, something stirs.\n' + plural(len(self.wolves), u'Werewolf, you may kill somebody.', u'Werewolves, you may kill somebody.') + '\n' + plural(len( self.seers), u"Seer, you may discover somebody's true form.", u"Seers, you may discover somebody's true form."), conflate=False) self.say_survivors(event) self.wolf_targets = {} self.seer_targets = {} self.timed_goto(event, self.day_length, self.dawn) @match(r'^(?:kill|see|eat)s?\s+(\S+)$') def kill_see(self, event, target_nick): """Kill or see a player. Only works for seers and wolves. """ if (self.state != self.night or event.public or event.sender['nick'] not in self.roles): return sender = event.sender['nick'] target = self.identify(target_nick) if target is None: event.addresponse(u'%s is not playing.', target_nick) elif self.roles[sender] == 'wolf': event.addresponse(u'You have chosen %s for your feast tonight.', target_nick) self.wolf_targets[sender] = target elif self.roles[sender] == 'seer': event.addresponse(u"You will discover %s's role at dawn tomorrow.", target_nick) self.seer_targets[sender] = target def dawn(self, event): """Start of day. During this state, villagers discover what happened overnight and discuss who to lynch. The next state is noon. """ self.state = self.dawn eaten = frozenset(self.wolf_targets.itervalues()) if eaten: victim = choice(list(eaten)) event.addresponse( u'The village awakes to find that werewolves have ' u'devoured %(nick)s the %(role)s in the night.', { 'nick': victim, 'role': self.roles[victim], }) self.death(victim) else: event.addresponse(u'The werewolves were abroad last night.') self.wolf_targets = {} for seer in self.seers: target = self.seer_targets.get(seer) if target is not None: # seer saw somebody if target in self.roles: # that somebody is alive msg = u'%(nick)s is a %(role)s' % { 'nick': target, 'role': self.roles[target], } else: msg = u'The wolves also had %s in mind last night.' \ % target event.addresponse(msg, target=seer) self.seer_targets = {} if not self.endgame(event): event.addresponse( u'Villagers, you have %i seconds ' u'to discuss suspicions and cast accusations.', self.day_length) self.say_survivors(event) self.timed_goto(event, self.day_length, self.noon) def noon(self, event): """Start of voting. Next state is dusk. """ self.state = self.noon event.addresponse( u'Villagers, you have %i seconds to cast ' u'your vote to lynch somebody.', self.day_length) self.votes = {} self.timed_goto(event, self.day_length, self.dusk) @match(r'^(?:lynch(?:es)?|votes?)\s+(?:for|against)\s+(\S+)$') def vote(self, event, target_nick): """Vote to lynch a player.""" if (self.state != self.noon or event.sender['nick'] not in self.roles): return target = self.identify(target_nick) if target is None: event.addresponse(u'%s is not playing.', target_nick) else: self.votes[event.sender['nick']] = target event.addresponse(u'%(voter)s voted for %(target)s.', { 'target': target, 'voter': event.sender['nick'], }, target=self.channel, address=False) def dusk(self, event): """Counting of votes and lynching. Next state is night. """ self.state = self.dusk vote_counts = defaultdict(int) for vote in self.votes.values(): vote_counts[vote] += 1 self.votes = {} victims = [] victim_votes = 0 for player, votes in vote_counts.iteritems(): if votes > victim_votes: victims = [player] victim_votes = votes elif votes == victim_votes: victims.append(player) if victims: if len(victims) > 1: event.addresponse(u'The votes are tied. Picking randomly...') victim = choice(victims) event.addresponse( u'The ballots are in, ' u'and %(nick)s the %(role)s has been lynched.', { 'nick': victim, 'role': self.roles[victim], }) self.death(victim) else: event.addresponse(u'Nobody voted.') if not self.endgame(event): self.timed_goto(event, 10, self.night) def say_survivors(self, event): """Name surviving players.""" event.addresponse(u'The surviving villagers are: %s.', human_join(self.roles)) def identify(self, nick): """Find the identity (correctly-capitalised nick) of a player. Returns None if nick is not playing. """ for player in self.roles.iterkeys(): if player.lower() == nick.lower(): return player return None def death(self, player): """Remove player from game.""" if self.state == self.prestart: self.players.remove(player) elif self.state is not None: del self.roles[player] for role in (self.wolves, self.seers): try: role.remove(player) except KeyError: pass def endgame(self, event): """Check if the game is over. If the game is over, announce the winners and return True. Otherwise return False. """ if 2 * len(self.wolves) >= len(self.roles): # werewolves win event.addresponse( u'The werewolves devour the remaining ' u'villagers and win. OM NOM NOM.\n' u'The winning werewolves were: %s', human_join(self.wolves), conflate=False) elif not self.wolves: # villagers win event.addresponse( u'The villagers have defeated the werewolves. ' u'Vigilantism FTW.\n' u'The surviving villagers were: %s', human_join(self.roles), conflate=False) else: return False self.state = None werewolf_games.remove(self) return True def timed_goto(self, event, delay, target): """Like call_later, but does nothing if state has changed.""" from_state = self.state log.debug(u'Going from state %s to %s in %i seconds.', self.state_name(), target.__name__, delay) def goto(evt): """Change state if it hasn't already changed.""" if self.state == from_state: target(evt) ibid.dispatcher.call_later(delay, goto, event) def rename(self, oldnick, newnick): """Rename a player.""" for playerset in ('players', 'wolves', 'seers'): if hasattr(self, playerset): playerset = getattr(self, playerset) if oldnick in playerset: playerset.remove(oldnick) playerset.add(newnick) if hasattr(self, 'roles') and oldnick in self.roles: self.roles[newnick] = self.roles[oldnick] del self.roles[oldnick] def state_change(self, event): if self.state is None: return if not hasattr(event, 'state'): return if event.state != 'online': nick = event.sender['nick'] if hasattr(event, 'othername'): self.rename(event.othername, nick) elif ((self.state == self.prestart and nick in self.players) or nick in self.roles): event.addresponse(u'%s has fled the game in terror.', nick, target=self.channel, address=False) self.death(nick) def state_name(self): "Return a printable version of the current state" if self.state is None: return 'stopped' return self.state.__name__
class DuelInitiate(Processor): usage = u""" I challenge <user> to a duel [over <something>] I demand satisfaction from <user> [over <something>] I throw the gauntlet down at <user>'s feet [over <something>] """ feature = ('duel', ) accept_timeout = FloatOption('accept_timeout', 'How long do we wait for acceptance?', 60.0) start_delay = IntOption( 'start_delay', 'Time between acceptance and start of duel (rounded down to the highest minute)', 30) timeout = FloatOption('timeout', 'How long is a duel on for', 15.0) happy_endings = ListOption('happy_endings', 'Both survive', ( u'walk off into the sunset', u'go for a beer', u'call it quits', )) class Duel(object): def stop(self): for callback in ('cancel', 'start', 'timeout'): callback += '_callback' if hasattr(self, callback) and getattr(self, callback).active(): getattr(self, callback).cancel() def shutdown(self): for duel in duels: duel.stop() @match( r'^(?:I\s+)throw\s+(?:down\s+(?:the|my)\s+gauntlet|(?:the|my)\s+gauntlet\s+down)\s+' r'at\s+(\S+?)(?:\'s\s+feet)?(?:\s+(?:over|because|for)\s+.+)?$') def initiate_gauntlet(self, event, recipient): self.initiate(event, recipient) @match( r'^(?:I\s+)?demand\s+satisfaction\s+from\s+(\S+)(?:\s+(?:over|because|for)\s+.+)?$' ) def initiate_satisfaction(self, event, recipient): self.initiate(event, recipient) @match( r'^(?:I\s+)?challenge\s+(\S+)(?:\s+to\s+a\s+duel)?(?:\s+(?:over|because|for)\s+.+)?$' ) def initiate(self, event, recipient): if not event.public: event.addresponse( choice(( u"All duels must take place in public places, by decree of the bot", u"How do you expect to fight %(recipient)s, when he is not present?", u"Your challenge must be made in public, Sir Knight", )), {'recipient': recipient}) return if (event.source, event.channel) in duels: event.addresponse( choice(( u"We already have a war in here. Take your fight outside", u"Isn't one fight enough? You may wait your turn", ))) return aggressor = event.sender['nick'] if recipient.lower() == aggressor.lower(): # Yes I know schizophrenia isn't the same as DID, but this sounds better :P event.addresponse( choice(( u"Are you schizophrenic?", u"Um, How exactly do you plan on fighting yourself?", ))) return if recipient.lower() in [ name.lower() for name in ibid.config.plugins['core']['names'] ]: event.addresponse( choice(( u"I'm a peaceful bot", u"The ref can't take part in the battle", u"You just want me to die. No way", ))) return duel = self.Duel() duels[(event.source, event.channel)] = duel duel.hp = { aggressor.lower(): 100.0, recipient.lower(): 100.0, } duel.names = { aggressor.lower(): aggressor, recipient.lower(): recipient, } duel.drawn = { aggressor.lower(): False, recipient.lower(): False, } duel.started = False duel.confirmed = False duel.aggressor = event.sender['nick'].lower() duel.recipient = recipient.lower() duel.cancel_callback = ibid.dispatcher.call_later( self.accept_timeout, self.cancel, event) event.addresponse(u'%(recipient)s: ' + choice(( u"The gauntlet has been thrown at your feet. Do you accept?", u"You have been challenged. Do you accept?", u"%(aggressor)s wishes to meet you at dawn on the field of honour. Do you accept?", )), { 'recipient': recipient, 'aggressor': event.sender['nick'], }, address=False) def cancel(self, event): duel = duels[(event.source, event.channel)] del duels[(event.source, event.channel)] event.addresponse( choice(( u"%(recipient)s appears to have fled the country during the night", u"%(recipient)s refuses to meet your challenge and accepts dishonour", u"Your challenge was not met. I suggest anger management counselling", )), { 'recipient': duel.names[duel.recipient], }) @match( r'^.*\b(?:ok|yes|I\s+do|sure|accept|hit\s+me|bite\s+me|i\'m\s+game|bring\s+it|yebo)\b.*$' ) def confirm(self, event): if (event.source, event.channel) not in duels: return duel = duels[(event.source, event.channel)] confirmer = event.sender['nick'].lower() if confirmer not in duel.names or duel.confirmed or confirmer != duel.recipient: return # Correct capitalisation duel.names[confirmer] = event.sender['nick'] duel.confirmed = True duel.cancel_callback.cancel() starttime = event.time + timedelta(seconds=self.start_delay + ((30 - event.time.second) % 30)) starttime = starttime.replace(microsecond=0) delay = starttime - event.time delay = delay.seconds + (delay.microseconds / 10.**6) duel.start_callback = ibid.dispatcher.call_later( delay, self.start, event) event.addresponse( u"%(aggressor)s, %(recipient)s: " u"The duel shall begin on the stroke of %(starttime)s (in %(delay)s seconds). " + choice(( u"You may clean your pistols.", u"Prepare yourselves.", u"Get ready", )), { 'aggressor': duel.names[duel.aggressor], 'recipient': duel.names[duel.recipient], 'starttime': format_date(starttime, 'time'), 'delay': (starttime - event.time).seconds, }, address=False) def start(self, event): duel = duels[(event.source, event.channel)] duel.started = True duel.timeout_callback = ibid.dispatcher.call_later( self.timeout, self.end, event) event.addresponse(u'%s, %s: %s' % tuple(duel.names.values() + [ choice((u'aaaand ... go!', u'5 ... 4 ... 3 ... 2 ... 1 ... fire!', u'match on!', u'ready ... aim ... fire!')) ]), address=False) def end(self, event): duel = duels[(event.source, event.channel)] del duels[(event.source, event.channel)] winner, loser = duel.names.keys() if duel.hp[winner] < duel.hp[loser]: winner, loser = loser, winner if duel.hp[loser] == 100.0: message = u"DRAW: %(winner)s and %(loser)s shake hands and %(ending)s" elif duel.hp[winner] < 50.0: message = u"DRAW: %(winner)s and %(loser)s bleed to death together" elif duel.hp[loser] < 50.0: message = u"VICTORY: %(loser)s bleeds to death" elif duel.hp[winner] < 100.0: message = u"DRAW: %(winner)s and %(loser)s hobble off together. Satisfaction is obtained" else: message = u"VICTORY: %(loser)s hobbles off while %(winner)s looks victorious" event.addresponse(message, { 'loser': duel.names[loser], 'winner': duel.names[winner], 'ending': choice(self.happy_endings), }, address=False)
class WriteFiglet(Processor): usage = u"""figlet <text> [in <font>] list figlet fonts [from <index>]""" feature = ('figlet',) max_width = IntOption('max_width', 'Maximum width for ascii output', 60) fonts_ = Option('fonts', 'Directory or Zip file containing figlet fonts', '/usr/share/figlet') preferred_fonts = ListOption('preferred_fonts', 'List of default figlet fonts', ('slant', 'letter')) def setup(self): self.fonts = None def _find_fonts(self): if self.fonts is not None: return if os.path.isdir(self.fonts_): self.fontstore = 'dir' fonts = listdir(self.fonts_) else: self.fontstore = 'zip' zip = ZipFile(self.fonts_) fonts = zip.namelist() fonts = fnmatch.filter(fonts, '*.[tf]lf') self.fonts = {} for font in fonts: font = os.path.splitext(font)[0] # Not all fonts are compatible with pyfiglet # (e.g. TLFs with colour): try: self._render('test', font) except FontNotFound: continue name = os.path.split(font)[-1] self.fonts[name] = font @match(r'^list\s+figlet\s+fonts(?:\s+from\s+(\d+))?$') def list_fonts(self, event, index): self._find_fonts() if index is None: index = 0 index = int(index) if index >= len(self.fonts): event.addresponse(u'I wish I had that many fonts installed') return event.addresponse(unicode(', '.join(self.fonts.keys()[int(index):]))) @match(r'^figlet\s+(.+?)(\s+in\s+(\S+))?$', 'deaddressed') def write(self, event, text, font_phrase, font): self._find_fonts() if font is not None and font not in self.fonts: text = '%s%s' % (text, font_phrase) font = None if font is None: if self.fonts: for font in self.preferred_fonts: if font in self.fonts: break else: font = choice(self.fonts.keys()) else: event.addresponse(u"I'm afraid I have no fonts available") return self._write(event, text, font) def _render(self, text, font): if self.fontstore == 'dir': figlet = Figlet(font=font, dir=self.fonts_) else: figlet = Figlet(font=font, zipfile=self.fonts_) return figlet.renderText(text) def _write(self, event, text, font): rendered = self._render(text, font).split('\n') while rendered and rendered[0].strip() == '': del rendered[0] while rendered and rendered[-1].strip() == '': del rendered[-1] if rendered and len(rendered[0]) > self.max_width: event.addresponse( u"Sorry that's too long, nobody will be able to read it") return event.addresponse(unicode('\n'.join(rendered), 'utf-8'), address=False, conflate=False)
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:
name) method.im_func.failing = True else: self.__log.debug(u'Still failing: %s', name) finally: method.lock.release() # This is a bit yucky, but necessary since ibid.config imports Processor from ibid.config import BoolOption, IntOption options = { 'addressed': BoolOption('addressed', u'Only process events if bot was addressed'), 'processed': BoolOption('processed', u"Process events even if they've already been processed"), 'priority': IntOption('priority', u'Processor priority'), } def handler(function): "Wrapper: Handle all events" function.handler = True function.message_version = 'clean' return function def _match_sub_selectors(regex): selector_patterns = { 'alpha' : r'[a-zA-Z]+', 'any' : r'.+', 'chunk' : r'\S+', 'digits' : r'\d+', 'number' : r'\d*\.?\d+',
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 Search(Processor): usage = u'search [for] [<limit>] [(facts|values) [containing]] (<pattern>|/<pattern>/[r]) [from <start>]' features = ('factoid', ) limit = IntOption('search_limit', u'Maximum number of results to return', 30) default = IntOption('search_default', u'Default number of results to return', 10) regex_re = re.compile(r'^/(.*)/(r?)$') @match( r'^search\s+(?:for\s+)?(?:(\d+)\s+)?(?:(facts?|values?)\s+)?(?:containing\s+)?(.+?)(?:\s+from)?(?:\s+(\d+))?\s*$', version='deaddressed') def search(self, event, limit, search_type, pattern, start): limit = limit and min(int(limit), self.limit) or self.default start = start and max(int(start) - 1, 0) or 0 search_type = search_type and search_type.lower() or u"" origpattern = pattern m = self.regex_re.match(pattern) is_regex = False if m: pattern = m.group(1) is_regex = bool(m.group(2)) # Hack: We replace $arg with _%, but this won't match a partial # "$arg" string if is_regex: filter_op = get_regexp_op(event.session) name_pattern = pattern.replace(r'\$arg', '_%') else: filter_op = lambda x, y: x.like(y, escape='#') pattern = '%%%s%%' % escape_like_re.sub(r'#\1', pattern) name_pattern = pattern.replace('$arg', '#_#%') query = event.session.query(Factoid)\ .join(Factoid.names).add_entity(FactoidName)\ .join(Factoid.values) if search_type.startswith('fact'): query = query.filter(filter_op(FactoidName.name, name_pattern)) elif search_type.startswith('value'): query = query.filter(filter_op(FactoidValue.value, pattern)) else: query = query.filter( or_(filter_op(FactoidName.name, name_pattern), filter_op(FactoidValue.value, pattern))) # Pre-evalute the iterable or the if statement will be True in SQLAlchemy 0.4. LP: #383286 matches = [match for match in query.all()] bounded_matches = matches[start:start + limit] if bounded_matches: event.addresponse(u'; '.join( u'%s [%s]' % (fname.name, len(factoid.values)) for factoid, fname in bounded_matches)) elif len(matches): event.addresponse( u"I could only find %(number)d things that matched '%(pattern)s'", { u'number': len(matches), u'pattern': origpattern, }) else: event.addresponse(u"I couldn't find anything that matched '%s'" % origpattern)
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 Insult(Processor): usage = u"""(flame | insult) <person> (swear | cuss | explete) [at <person>]""" features = ('insult', ) adjectives = ListOption('adjectives', 'List of adjectives', ( u'acidic', u'antique', u'artless', u'base-court', u'bat-fowling', u'bawdy', u'beef-witted', u'beetle-headed', u'beslubbering', u'boil-brained', u'bootless', u'churlish', u'clapper-clawed', u'clay-brained', u'clouted', u'cockered', u'common-kissing', u'contemptible', u'coughed-up', u'craven', u'crook-pated', u'culturally-unsound', u'currish', u'dankish', u'decayed', u'despicable', u'dismal-dreaming', u'dissembling', u'dizzy-eyed', u'doghearted', u'dread-bolted', u'droning', u'earth-vexing', u'egg-sucking', u'elf-skinned', u'errant', u'evil', u'fat-kidneyed', u'fawning', u'fen-sucked', u'fermented', u'festering', u'flap-mouthed', u'fly-bitten', u'fobbing', u'folly-fallen', u'fool-born', u'foul', u'frothy', u'froward', u'full-gorged', u'fulminating', u'gleeking', u'goatish', u'gorbellied', u'guts-griping', u'hacked-up', u'halfbaked', u'half-faced', u'hasty-witted', u'headless', u'hedge-born', u'hell-hated', u'horn-beat', u'hugger-muggered', u'humid', u'idle-headed', u'ill-borne', u'ill-breeding', u'ill-nurtured', u'imp-bladdereddle-headed', u'impertinent', u'impure', u'industrial', u'inept', u'infected', u'infectious', u'inferior', u'it-fowling', u'jarring', u'knotty-pated', u'left-over', u'lewd-minded', u'loggerheaded', u'low-quality', u'lumpish', u'malodorous', u'malt-wormy', u'mammering', u'mangled', u'measled', u'mewling', u'milk-livered', u'motley-mind', u'motley-minded', u'off-color', u'onion-eyed', u'paunchy', u'penguin-molesting', u'petrified', u'pickled', u'pignutted', u'plume-plucked', u'pointy-nosed', u'porous', u'pottle-deep', u'pox-marked', u'pribbling', u'puking', u'puny', u'railing', u'rank', u'reeky', u'reeling-ripe', u'roguish', u'rough-hewn', u'rude-growing', u'rude-snouted', u'rump-fed', u'ruttish', u'salty', u'saucy', u'saucyspleened', u'sausage-snorfling', u'shard-borne', u'sheep-biting', u'spam-sucking', u'spleeny', u'spongy', u'spur-galled', u'squishy', u'surly', u'swag-bellied', u'tardy-gaited', u'tastless', u'tempestuous', u'tepid', u'thick', u'tickle-brained', u'toad-spotted', u'tofu-nibbling', u'tottering', u'uninspiring', u'unintelligent', u'unmuzzled', u'unoriginal', u'urchin-snouted', u'vain', u'vapid', u'vassal-willed', u'venomed', u'villainous', u'warped', u'wayward', u'weasel-smelling', u'weather-bitten', u'weedy', u'wretched', u'yeasty', )) collections = ListOption('collections', 'List of collective nouns', ( u'accumulation', u'ass-full', u'assload', u'bag', u'bucket', u'coagulation', u'enema-bucketful', u'gob', u'half-mouthful', u'heap', u'mass', u'mound', u'ooze', u'petrification', u'pile', u'plate', u'puddle', u'quart', u'stack', u'thimbleful', u'tongueful', )) nouns = ListOption('nouns', u'List of singular nouns', ( u'apple-john', u'baggage', u'barnacle', u'bladder', u'boar-pig', u'bugbear', u'bum-bailey', u'canker-blossom', u'clack-dish', u'clotpole', u'coxcomb', u'codpiece', u'death-token', u'dewberry', u'flap-dragon', u'flax-wench', u'flirt-gill', u'foot-licker', u'fustilarian', u'giglet', u'gudgeon', u'haggard', u'harpy', u'hedge-pig', u'horn-beast', u'hugger-mugger', u'jolthead', u'lewdster', u'lout', u'maggot-pie', u'malt-worm', u'mammet', u'measle', u'minnow', u'miscreant', u'moldwarp', u'mumble-news', u'nut-hook', u'pigeon-egg', u'pignut', u'puttock', u'pumpion', u'ratsbane', u'scut', u'skainsmate', u'strumpet', u'varlet', u'vassal', u'whey-face', u'wagtail', )) plnouns = ListOption('plnouns', u'List of plural nouns', ( u'anal warts', u'armadillo snouts', u'bat toenails', u'bug spit', u'buzzard gizzards', u'cat bladders', u'cat hair', u'cat-hair-balls', u'chicken piss', u'cold sores', u'craptacular carpet droppings', u'dog balls', u'dog vomit', u'dung', u'eel ooze', u'entrails', u"fat-woman's stomach-bile", u'fish heads', u'guano', u'gunk', u'jizzum', u'pods', u'pond scum', u'poop', u'poopy', u'pus', u'rat-farts', u'rat retch', u'red dye number-9', u'seagull puke', u'slurpee-backwash', u'snake assholes', u'snake bait', u'snake snot', u'squirrel guts', u'Stimpy-drool', u'Sun IPC manuals', u'toxic waste', u'urine samples', u'waffle-house grits', u'yoo-hoo', )) @match(r'^(?:insult|flame)\s+(.+)$') def insult(self, event, insultee): articleadj = choice(self.adjectives) articleadj = (articleadj[0] in u'aehiou' and u'an ' or u'a ') + articleadj event.addresponse(choice(( u'%(insultee)s, thou %(adj1)s, %(adj2)s %(noun)s', u'%(insultee)s is nothing but %(articleadj)s %(collection)s of %(adj1)s %(plnoun)s', )), { 'insultee': insultee, 'adj1': choice(self.adjectives), 'adj2': choice(self.adjectives), 'articleadj': articleadj, 'collection': choice(self.collections), 'noun': choice(self.nouns), 'plnoun': choice(self.plnouns), }, address=False) loneadjectives = ListOption('loneadjectives', 'List of stand-alone adjectives for swearing', ( 'bloody', 'damn', 'f*****g', 's******g', 'sodding', 'crapping', 'wanking', 'buggering', )) swearadjectives = ListOption( 'swearadjectives', 'List of adjectives to be combined with swearnouns', ( 'reaming', 'lapping', 'eating', 'sucking', 'vokken', 'kak', 'donder', 'bliksem', 'f*****g', 's******g', 'sodding', 'crapping', 'wanking', 'buggering', )) swearnouns = ListOption( 'swearnouns', 'List of nounes to be comined with swearadjectives', ( 'shit', 'c**t', 'hell', 'mother', 'god', 'maggot', 'father', 'crap', 'ball', 'w***e', 'goat', 'dick', 'c**k', 'pile', 'bugger', 'poes', 'hoer', 'kakrooker', 'ma', 'pa', 'naiier', 'kak', 'bliksem', 'vokker', 'kakrooker', )) swearlength = IntOption('swearlength', 'Number of expletives to swear with', 15) @match(r'^(?:swear|cuss|explete)(?:\s+at\s+(?:the\s+)?(.*))?$') def swear(self, event, insultee): swearage = [] for i in range(self.swearlength): if random() > 0.7: swearage.append(choice(self.loneadjectives)) else: swearage.append( choice(self.swearnouns) + choice(self.swearadjectives)) if insultee is not None: swearage.append(insultee) else: swearage.append(choice(self.swearnouns)) event.addresponse(u' '.join(swearage) + u'!', address=False)
class Retrieve(Processor): usage = u"""latest [ <count> ] ( articles | headlines ) from <name> [ starting at <number> ] article ( <number> | /<pattern>/ ) from <name>""" features = ('feeds',) interval = IntOption('interval', 'Feed Poll interval (in seconds)', 300) max_interval = IntOption('max_interval', 'Maximum feed poll interval for broken feeds (in seconds)', 86400) backoff_ratio = FloatOption('backoff', 'The slowdown ratio to back off from broken feeds', 2.0) @match(r'^(?:latest|last)\s+(?:(\d+)\s+)?(article|headline)(s)?\s+from\s+(.+?)' r'(?:\s+start(?:ing)?\s+(?:at\s+|from\s+)?(\d+))?$') def list(self, event, number, full, plurality, name, start): full = full == 'article' if number: number = int(number) elif not plurality: number = 1 else: number = 10 start = start and int(start) or 0 feed = event.session.query(Feed).filter_by(name=name).first() if not feed: event.addresponse(u"I don't know about the %s feed", name) return feed.update() if not feed.entries: event.addresponse(u"I can't find any articles in that feed") return articles = feed.entries[start:number+start] entries = [] for article in articles: if full: if 'summary' in article: summary = html2text_file(article.summary, None) else: if article.content[0].type in \ ('application/xhtml+xml', 'text/html'): summary = html2text_file(article.content[0].value, None) else: summary = article.content[0].value entries.append(u'%(number)s: "%(title)s"%(link)s : %(summary)s' % { 'number': articles.index(article) + 1, 'title': html2text_file(article.title, None).strip(), 'link': get_link(article), 'summary': summary, }) else: entries.append(u'%s: "%s"' % (feed.entries.index(article) + 1, html2text_file(article.title, None).strip())) event.addresponse(u', '.join(entries)) @match(r'^article\s+(?:(\d+)|/(.+?)/)\s+from\s+(.+?)$') def article(self, event, number, pattern, name): feed = event.session.query(Feed).filter_by(name=name).first() if not feed: event.addresponse(u"I don't know about the %s feed", name) return feed.update() if not feed.entries: event.addresponse(u"I can't find any articles in that feed") return article = None if number: if int(number) > len(feed.entries) or 1 > int(number): event.addresponse(u"That's old news dude") return article = feed.entries[int(number) - 1] else: pattern = re.compile(pattern, re.I) for entry in feed.entries: if pattern.search(entry.title): article = entry break if not article: event.addresponse(u'Are you making up news again?') return if 'summary' in article: summary = html2text_file(article.summary, None) else: if article.content[0].type in \ ('application/xhtml+xml', 'text/html'): summary = html2text_file(article.content[0].value, None) else: summary = article.content[0].value event.addresponse(u'"%(title)s"%(link)s : %(summary)s', { 'title': html2text_file(article.title, None).strip(), 'link': get_link(article), 'summary': summary, }) last_seen = {} @periodic(config_key='interval') def poll(self, event): feeds = event.session.query(Feed) \ .filter(Feed.source != None) \ .filter(Feed.target != None).all() for feed in feeds: broken_lock.acquire() try: if feed.name in broken_feeds: last_exc, interval, time_since_fetch = broken_feeds[feed.name] time_since_fetch += self.interval if time_since_fetch < interval: broken_feeds[feed.name] = \ last_exc, interval, time_since_fetch continue else: last_exc = None interval = time_since_fetch = self.interval try: feed.update(max_age=time_since_fetch) except Exception, e: if type(e) != type(last_exc): if isinstance(e, URLError): log.warning(u'Exception "%s" occured while polling ' u'feed %s from %s', e, feed, feed.url) else: log.exception(u'Exception "%s" occured while polling ' u'feed %s from %s', e, feed, feed.url) broken_feeds[feed.name] = e, self.backoff(interval), 0 continue else: if feed.name in broken_feeds: del broken_feeds[feed.name] finally:
class ExchangeMessage(Processor): usage = u"""(have|take) <object> what are you carrying? who gave you <object>? give <person> <object>""" features = ('bucket', ) bucket_size = IntOption('bucket_size', "The maximum number of objects in the bucket", 5) @match(r'(?:have|take) ' + object_pat) def have(self, event, determiner, object): if determiner in ('his', 'her', 'their', 'its'): event.addresponse("I don't know whose %s you're talking about", object) else: return exchange(event, determiner, object, self.bucket_size) @match(r'(?:what (?:are|do) (?:yo)?u )?(?:carrying|have)') def query_carrying(self, event): items = Item.carried_items(event.session).all() if items: event.addresponse(u"I'm carrying %s", human_join(map(unicode, items))) else: event.addresponse(u"I'm not carrying anything") def find_items(self, session, determiner, object): """Find items matching (determiner, object). Return a tuple (kind, items). If determiner is a genitive and there are matching objects with the correct owner, return them with kind='owned'; if there are no matching objects, find unowned objects and return with kind='unowned'. If the determiner is *not* genitive, ignore determiners and set kind='all'.""" all_items = Item.carried_items(session) \ .filter_by(description=object) if "'" in determiner: items = all_items.filter_by(determiner=determiner).all() if items: return ('owned', items) else: # find unowned items clause = or_(not_(Item.determiner.contains(u"'")), Item.determiner == None) return ('unowned', all_items.filter(clause).all()) else: return ('all', all_items.all()) @match(r'give {chunk} ' + object_pat) def give(self, event, receiver, determiner, object): if determiner is None: determiner = '' who = event.sender['nick'] if determiner.lower() == 'our': if who[-1] in 'sS': determiner = who + "'" else: determiner = who + "'s" yours = 'your' elif determiner.lower() == 'my': determiner = who + "'s" yours = 'your' elif determiner.lower() in ('his', 'her', 'their'): yours = determiner.lower() determiner = receiver + "'s" else: yours = False if receiver.lower() == 'me': receiver = who kind, items = self.find_items(event.session, determiner, object) if items: item = choice(items) item.carried = False event.session.save_or_update(item) if kind == 'owned' and yours and yours != 'your': item.determiner = yours event.addresponse(u'hands %(receiver)s %(item)s ', { 'receiver': receiver, 'item': item }, action=True) else: if yours: object = yours + u' ' + object elif determiner: object = determiner + u' ' + object event.addresponse( choice((u"There's nothing like that in my bucket.", u"I don't have %s" % object))) @match(r'(?:who gave (?:yo)?u|where did (?:yo)?u get) ' + object_pat) def query_giver(self, event, determiner, object): if determiner is None: determiner = '' who = event.sender['nick'] if determiner.lower() == 'our': if who[-1] in 'sS': determiner = who + "'" else: determiner = who + "'s" yours = True elif determiner.lower() == 'my': determiner = who + "'s" yours = True else: yours = False kind, items = self.find_items(event.session, determiner, object) if items: explanation = u'' if kind == 'unowned': explanation = plural(len(items), u". I didn't realise it was ", u". I didn't realise they were ") if yours: explanation += u"yours" else: explanation += determiner explanation += u"." yours = False event.addresponse(u'I got ' + human_join( u'%(item)s from %(giver)s' % { 'item': [item, u'your ' + item.description][yours], 'giver': identity_name(event, item.giver) } for item in items) + explanation) return else: if yours: object = u'your ' + object elif determiner: object = determiner + u' ' + object event.addresponse( choice((u"There's nothing like that in my bucket.", u"I don't have %s" % object)))
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 RFCLookup(Processor): usage = u"""rfc <number> rfc [for] <search terms> rfc [for] /regex/""" features = ('rfc', ) indexurl = Option('index_url', "A HTTP url for the RFC Index file", "http://www.rfc-editor.org/rfc/rfc-index.txt") cachetime = IntOption("cachetime", "Time to cache RFC index for", cachetime) indexfile = None last_checked = 0 def _update_list(self): if not self.indexfile or time.time( ) - self.last_checked > self.cachetime: self.indexfile = cacheable_download(self.indexurl, "rfc/rfc-index.txt") self.last_checked = time.time() class RFC(object): special_authors = ( "Ed\.", "Eds\.", "RFC Editor", "IAP", "et al\.", "IAB", "IAB and IESG", "Internet Architecture Board", "Defense Advanced Research Projects Agency", "Internet Activities Board", "Gateway Algorithms and Data Structures Task Force", "International Organization for Standardization", "IAB Advisory Committee", "Federal Networking Council", "Internet Engineering Steering Group", "The Internet Society", "Sun Microsystems", "KOI8-U Working Group", "ISOC Board of Trustees", "Internet Assigned Numbers Authority \(IANA\)", "The North American Directory Forum", "Vietnamese Standardization Working Group", "ESnet Site Coordinating Comittee \(ESCC\)", "Energy Sciences Network \(ESnet\)", "North American Directory Forum", "Stanford Research Institute", "National Research Council", "Information Sciences Institute University of Southern California", "Bolt Beranek and Newman Laboratories", "International Telegraph and Telephone Consultative Committee of the International Telecommunication Union", "National Bureau of Standards", "Network Technical Advisory Group", "National Science Foundation", "End-to-End Services Task Force", "NetBIOS Working Group in the Defense Advanced Research Projects Agency", "ESCC X.500/X.400 Task Force", ) # She's pretty, isn't she? # Beginners guide: # First line is title, initials # Second is middle names, surnames, and suffixes # Third is date and extensions record_re = re.compile( r"^(.+?)\. ((?:(?:[A-Z]{1,2}|[A-Z]\.-?[A-Z]|[A-Z]-[A-Z]|[A-Z]\([A-Z]\)|[A-Z][a-z]+)\.{0,2}" r"(?: (?:[Vv]an|[Dd]e[nr]?|[Ll][ae]|El|Del|Dos|da))* ?[a-zA-Z\-']+(?:[\.,]? (?:\d+(?:rd|nd|st|th)|Jr|I+)\.?)?|%s)" r"(?:, ?)?)+\. ([A-Z][a-z]{2,8}(?: \d{1,2})? \d{4})\. \((.+)\)$" % "|".join(special_authors)) def __init__(self, number, record): self.number = number self.record = unicode(record, encoding="ASCII") self.issued = not self.record == "Not Issued." self.summary = self.record def parse(self): if self.issued: m = self.record_re.match(self.record) if not m: log.warning("CAN'T DECODE RFC: " + self.record) else: self.title, self.authors, self.date, extensions = m.groups( ) extensions = extensions.split(') (') self.formats = [] self.status = None self.also = None self.obsoleted = self.obsoletes = None self.updated = self.updates = None self.online = True for ex in extensions: if ex.startswith("Format:"): self.formats = [ fmt.strip() for fmt in ex.split(":", 1)[1].split(",") ] elif ex.startswith("Status:"): self.status = ex.split(":", 1)[1].strip() elif ex == "Not online": self.online = False else: values = [ fmt.strip() for fmt in ex.split(" ", 1)[1].split(",") ] values = [ val[:3] == "RFC" and val[3:] or val for val in values ] if ex.startswith("Also"): self.also = values elif ex.startswith("Obsoleted by"): self.obsoleted = values elif ex.startswith("Obsoletes"): self.obsoletes = values elif ex.startswith("Updated by"): self.updated = values elif ex.startswith("Updates"): self.updates = values else: log.warning("CAN'T DECODE RFC: " + self.record) extensions = [ ":" in ex and ex.split(":", 1) or ex.split(" ", 1) for ex in extensions if ":" in ex ] extensions = dict([(name.strip().upper(), values.strip()) for name, values in extensions]) self.extensions = extensions self.summary = u"%s. %s." % (self.title, self.date) if self.status: self.summary += u" " + self.status if self.obsoleted: self.summary += u" Obsoleted by " + u", ".join( self.obsoleted) def _parse_rfcs(self): self._update_list() f = file(self.indexfile, "rU") lines = f.readlines() f.close() breaks = 0 strip = -1 for lineno, line in enumerate(lines): if line.startswith(20 * "~"): breaks += 1 elif breaks == 2 and line.startswith("000"): strip = lineno break lines = lines[strip:] rfcs = {} buf = "" # So there's nothing left in buf: lines.append("") for line in lines: line = line.strip() if line: buf += " " + line elif buf: number, desc = buf.strip().split(None, 1) number = int(number) rfcs[number] = self.RFC(number, desc) buf = "" return rfcs @match(r'^rfc\s+#?(\d+)$') def lookup(self, event, number): rfcs = self._parse_rfcs() number = int(number) if number in rfcs: event.addresponse( u"%(record)s http://www.rfc-editor.org/rfc/rfc%(number)i.txt", { 'record': rfcs[number].record, 'number': number, }) else: event.addresponse(u"Sorry, no such RFC") @match(r'^rfc\s+(?:for\s+)?(.+)$') def search(self, event, terms): # If it's an RFC number, lookup() will catch it if terms.isdigit(): return rfcs = self._parse_rfcs() # Search engines: pool = rfcs.itervalues() if len(terms) > 2 and terms[0] == terms[-1] == "/": try: term_re = re.compile(terms[1:-1], re.I) except re.error: event.addresponse(u"Couldn't search. Invalid regex: %s", re.message) return pool = [rfc for rfc in pool if term_re.search(rfc.record)] else: terms = set(terms.split()) for term in terms: pool = [ rfc for rfc in pool if term.lower() in rfc.record.lower() ] # Newer RFCs matter more: pool.reverse() if pool: results = [] for result in pool[:5]: result.parse() results.append("%04i: %s" % (result.number, result.summary)) event.addresponse( u'Found %(found)i matching RFCs. Listing %(listing)i: %(results)s', { 'found': len(pool), 'listing': min(len(pool), 5), 'results': u', '.join(results), }) else: event.addresponse(u"Sorry, can't find anything")
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 = 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 Set(Processor): usage = u'<subject>(++|--|==| ftw| ftl) [[reason]]' features = ('karma', ) # Clashes with morse & math priority = 510 permission = u'karma' increase = ListOption('increase', 'Suffixes which indicate increased karma', ('++', ' ftw')) decrease = ListOption('decrease', 'Suffixes which indicate decreased karma', ('--', ' ftl')) neutral = ListOption('neutral', 'Suffixes which indicate neutral karma', ('==', )) reply = BoolOption('reply', 'Acknowledge karma changes', False) public = BoolOption('public', 'Only allow karma changes in public', True) ignore = ListOption('ignore', 'Karma subjects to silently ignore', ()) importance = IntOption( 'importance', 'Threshold for number of changes after' " which a karma won't be forgotten", 0) def setup(self): # When not addressed, match karma changes in any text if self.addressed: matchpat = r'^(.+?)\s*(%s)\s*(?:[[{(]+\s*(.+?)\s*[\]})]+)?$' else: matchpat = r'(\S*\w\S*)(%s)(?:$|[\s,;\.\?!])' self.increase_reg = self.regex_tokens(self.increase) self.decrease_reg = self.regex_tokens(self.decrease) self.neutral_reg = self.regex_tokens(self.neutral) self.set.im_func.pattern = re.compile( matchpat % '|'.join(self.increase_reg + self.decrease_reg + self.neutral_reg), re.I | re.UNICODE | re.DOTALL) def regex_tokens(self, tokens): """ Turn configured tokens into regex versions """ return [re.escape(t).replace(r'\ ', r'\s+') for t in tokens] def match_operators(self, roperators, adjust): return any(re.match(r, adjust) for r in roperators) @handler @authorise(fallthrough=False) def set(self, event, subject, adjust, reason=None): if reason is None: reason = event['message']['clean'] if self.public and not event.public: event.addresponse(u'Karma must be done in public') return if subject.lower() in self.ignore: return karma = event.session.query(Karma).filter_by(subject=subject).first() if not karma: karma = Karma(subject) if self.match_operators(self.increase_reg, adjust.lower()): if subject.lower() == event.sender['nick'].lower(): event.addresponse(u"You can't karma yourself!") return karma.changes += 1 karma.value += 1 change = u'Increased' elif self.match_operators(self.decrease_reg, adjust.lower()): karma.changes += 1 karma.value -= 1 change = u'Decreased' else: karma.changes += 2 change = u'Increased and decreased' if karma.value == 0 and karma.changes <= self.importance: change = u'Forgotten (unimportant)' event.session.delete(karma) else: event.session.save_or_update(karma) event.session.commit() log.info(u"%s karma for '%s' by %s/%s (%s) because: %s", change, subject, event.account, event.identity, event.sender['connection'], reason) if self.reply: event.addresponse( u'%(subject)s now has %(value)s %(points)s of karma', { 'subject': subject, 'value': karma.value, 'points': plural(karma.value, "point", "points"), }) else: event.processed = True
class Set(Processor): usage = u'<subject> (++|--|==|ftw|ftl) [[reason]]' feature = ('karma', ) # Clashes with morse & math priority = 510 permission = u'karma' increase = ListOption('increase', 'Suffixes which indicate increased karma', ('++', 'ftw')) decrease = ListOption('decrease', 'Suffixes which indicate decreased karma', ('--', 'ftl')) neutral = ListOption('neutral', 'Suffixes which indicate neutral karma', ('==', )) reply = BoolOption('reply', 'Acknowledge karma changes', False) public = BoolOption('public', 'Only allow karma changes in public', True) ignore = ListOption('ignore', 'Karma subjects to silently ignore', ()) importance = IntOption( 'importance', 'Threshold for number of changes after' " which a karma won't be forgotten", 0) def setup(self): self.set.im_func.pattern = re.compile( r'^(.+?)\s*(%s)\s*(?:[[{(]+\s*(.+?)\s*[\]})]+)?$' % '|'.join( re.escape(token) for token in self.increase + self.decrease + self.neutral), re.I) @handler @authorise(fallthrough=False) def set(self, event, subject, adjust, reason): if self.public and not event.public: event.addresponse(u'Karma must be done in public') return if subject.lower() in self.ignore: return karma = event.session.query(Karma).filter_by(subject=subject).first() if not karma: karma = Karma(subject) if adjust.lower() in self.increase: if subject.lower() == event.sender['nick'].lower(): event.addresponse(u"You can't karma yourself!") return karma.changes += 1 karma.value += 1 change = u'Increased' elif adjust.lower() in self.decrease: karma.changes += 1 karma.value -= 1 change = u'Decreased' else: karma.changes += 2 change = u'Increased and decreased' if karma.value == 0 and karma.changes <= self.importance: change = u'Forgotten (unimportant)' event.session.delete(karma) else: event.session.save_or_update(karma) event.session.commit() log.info(u"%s karma for '%s' by %s/%s (%s) because: %s", change, subject, event.account, event.identity, event.sender['connection'], reason) if self.reply: event.addresponse(True) else: event.processed = True