class Address(Processor): priority = 1600 processed = True addressed = False event_types = ('message', 'action', 'notice', 'state') acknowledgements = ListOption( 'acknowledgements', 'Responses for positive acknowledgements', (u'Okay', u'Sure', u'Done', u'Righto', u'Alrighty', u'Yessir')) refusals = ListOption( 'refusals', 'Responses for negative acknowledgements', (u'No', u"I won't", u"Shan't", u"I'm sorry, but I can't do that")) @handler def address(self, event): for response in event.responses: if isinstance(response['reply'], bool): if response: response['reply'] = choice(self.acknowledgements) else: response['reply'] = choice(self.refusals) if (response.get('address', False) and not response.get('action', False) and not response.get('notice', False) and event.public): response['reply'] = ('%s: %s' % (event.sender['nick'], response['reply']))
class SourceFactory(client.DeferredClientFactory, protocol.ReconnectingClientFactory, IbidSourceFactory): auth = ('implicit',) supports = ('multiline',) jid_str = Option('jid', 'Jabber ID') server = Option('server', 'Server hostname (defaults to SRV lookup, ' 'falling back to JID domain)') port = IntOption('port', 'Server port number (defaults to SRV lookup, ' 'falling back to 5222/5223') ssl = BoolOption('ssl', 'Use SSL instead of automatic TLS') password = Option('password', 'Jabber password') nick = Option('nick', 'Nick for chatrooms', ibid.config['botname']) rooms = ListOption('rooms', 'Chatrooms to autojoin', []) accept_domains = ListOption('accept_domains', 'Only accept messages from these domains', []) max_public_message_length = IntOption('max_public_message_length', 'Maximum length of public messages', 512) def __init__(self, name): IbidSourceFactory.__init__(self, name) self.log = logging.getLogger('source.%s' % name) client.DeferredClientFactory.__init__(self, JID(self.jid_str), self.password) bot = JabberBot() self.addHandler(bot) bot.setHandlerParent(self) def setServiceParent(self, service): c = IbidXMPPClientConnector(reactor, self.authenticator.jid.host, self, self.server, self.port, self.ssl) c.connect() def connect(self): return self.setServiceParent(None) def disconnect(self): self.stopTrying() self.stopFactory() self.proto.xmlstream.transport.loseConnection() return True def join(self, room): return self.proto.join(room) def leave(self, room): return self.proto.leave(room) def url(self): return u'xmpp://%s' % (self.jid_str,) def logging_name(self, identity): return identity.split('/')[0] def truncation_point(self, response, event=None): if response.get('target', None) in self.proto.rooms: return self.max_public_message_length return None
class Get(Processor, RPC): usage = u'<factoid> [( #<number> | /<pattern>/[r] )]' features = ('factoid', ) priority = 200 interrogatives = ListOption('interrogatives', 'Question words to strip', default_interrogatives) verbs = ListOption('verbs', 'Verbs that split name from value', default_verbs) def __init__(self, name): super(Get, self).__init__(name) RPC.__init__(self) def setup(self): self.get.im_func.pattern = re.compile( r'^(?:(?:%s)\s+(?:(?:%s)\s+)?)?(.+?)(?:\s+#(\d+))?(?:\s+/(.+?)/(r?))?$' % ('|'.join(self.interrogatives), '|'.join(self.verbs)), re.I) @handler def get(self, event, name, number, pattern, is_regex): response = self.remote_get(name, number, pattern, is_regex, event) if response: event.addresponse(response) def remote_get(self, name, number=None, pattern=None, is_regex=None, event={}): factoid = get_factoid(event.session, name, number, pattern, is_regex) if factoid: (factoid, fname, fvalue) = factoid reply = fvalue.value oname = fname.name pattern = re.escape(fname.name).replace(r'\$arg', '(.*)') args = re.match(pattern, name, re.I | re.U).groups() for i, capture in enumerate(args): reply = reply.replace('$%s' % (i + 1), capture) oname = oname.replace('$arg', capture, 1) reply = _interpolate(reply, event) (reply, count) = action_re.subn('', reply) if count: return {'action': True, 'reply': reply} (reply, count) = reply_re.subn('', reply) if count: return {'address': False, 'reply': reply} reply = u'%s %s' % (oname, reply) return reply
class Addressed(Processor): priority = -1500 addressed = False names = ListOption('names', 'Names to respond to', [ibid.config['botname']]) verbs = ListOption( 'verbs', u'Verbs to ignore', ('is', 'has', 'was', 'might', 'may', 'would', 'will', "isn't", "hasn't", "wasn't", "wouldn't", "won't", 'can', "can't", 'did', "didn't", 'said', 'says', 'should', "shouldn't", 'does', "doesn't")) def setup(self): names = '|'.join(re.escape(x) for x in self.names) verbs = '|'.join(re.escape(x) for x in self.verbs) self.patterns = [ re.compile( r'^\s*(?P<nick>%s)' % names + r'(?:\s*[:;.?>!,-]+\s+|\s+|\s*[,:]\s*)(?P<body>.*)', re.I | re.DOTALL), # "hello there, bot"-style addressing. But we want to be sure that # there wasn't normal addressing too: re.compile( r'^(?:\S+:.*|(?P<body>.*),\s*(?P<nick>%s))[\s?!.]*$' % names, re.I | re.DOTALL) ] self.verb_pattern = re.compile(r'^(?:%s)\s+(?:%s)\s+' % (names, verbs), re.I | re.DOTALL) @handler def handle_addressed(self, event): if 'addressed' not in event: event.addressed = False if self.verb_pattern.match(event.message['stripped']): return for pattern in self.patterns: matches = pattern.search(event.message['stripped']) if matches and matches.group('nick'): new_message = matches.group('body') event.addressed = matches.group('nick') event.message['clean'] = new_message event.message['deaddressed'] = \ pattern.search(event.message['raw']).group('body')
class Lengthen(Processor): usage = u"""<url> expand <url>""" feature = ('tinyurl', ) services = ListOption('services', 'List of URL prefixes of URL shortening services', ( 'http://is.gd/', 'http://tinyurl.com/', 'http://ff.im/', 'http://shorl.com/', 'http://icanhaz.com/', 'http://url.omnia.za.net/', 'http://snipurl.com/', 'http://tr.im/', 'http://snipr.com/', 'http://bit.ly/', 'http://cli.gs/', 'http://zi.ma/', 'http://twurl.nl/', 'http://xrl.us/', 'http://lnk.in/', 'http://url.ie/', 'http://ne1.net/', 'http://turo.us/', 'http://301url.com/', 'http://u.nu/', 'http://twi.la/', 'http://ow.ly/', 'http://su.pr/', 'http://tiny.cc/', 'http://ur1.ca/', )) def setup(self): self.lengthen.im_func.pattern = re.compile( r'^(?:((?:%s)\S+)|(?:lengthen\s+|expand\s+)(http://\S+))$' % '|'.join([re.escape(service) for service in self.services]), re.I | re.DOTALL) @handler def lengthen(self, event, url1, url2): url = url1 or url2 opener = build_opener(NullRedirect()) try: f = opener.open(url) f.close() except HTTPError, e: if e.code in (301, 302, 303, 307): event.addresponse(u'That expands to: %s', e.hdrs['location']) return raise event.addresponse(u"No redirect")
class Ignore(Processor): priority = -1500 addressed = False event_types = (u'message', u'action', u'notice') nicks = ListOption('ignore', 'List of nicks to ignore', []) @handler def handle_ignore(self, event): for who in self.nicks: if event.sender['nick'] == who: event.processed = True
class SourceFactory(IbidSourceFactory): auth = ('implicit', ) supports = ('action', 'topic') server = Option('server', 'Server hostname') port = IntOption('port', 'Server port number', 706) nick = Option('nick', 'Nick', ibid.config['botname']) channels = ListOption('channels', 'Channels to autojoin', []) realname = Option('realname', 'Real Name', ibid.config['botname']) public_key = Option('public_key', 'Filename of public key', 'silc.pub') private_key = Option('private_key', 'Filename of private key', 'silc.prv') max_public_message_length = IntOption('max_public_message_length', 'Maximum length of public messages', 512) def __init__(self, name): IbidSourceFactory.__init__(self, name) self.log = logging.getLogger('source.%s' % self.name) pub = join(ibid.options['base'], self.public_key) prv = join(ibid.options['base'], self.private_key) if not exists(pub) and not exists(prv): keys = create_key_pair(pub, prv, passphrase='') else: keys = load_key_pair(pub, prv, passphrase='') self.client = SilcBot(keys, self.nick, self.nick, self.realname, self) def run_one(self): self.client.run_one() def setServiceParent(self, service): self.s = internet.TimerService(0.2, self.run_one) if service is None: self.s.startService() else: self.s.setServiceParent(service) def disconnect(self): self.client.disconnect() return True def url(self): return u'silc://%s@%s:%s' % (self.nick, self.server, self.port) def logging_name(self, identity): return self.client.logging_name(identity) def truncation_point(self, response, event=None): if response.get('target', None) in self.client.channels: return self.max_public_message_length return None
class SourceFactory(IbidSourceFactory): auth = ('implicit', ) supports = ('action', 'multiline', 'topic') subdomain = Option('subdomain', 'Campfire subdomain') secure = BoolOption('secure', 'Use https (paid accounts only)', False) token = Option('token', 'Campfire token') rooms = ListOption('rooms', 'Rooms to join', []) keepalive_timeout = IntOption( 'keepalive_timeout', 'Stream keepalive timeout. ' 'Campfire sends a keepalive every <5 seconds', 30) def __init__(self, name): super(SourceFactory, self).__init__(name) self.log = logging.getLogger('source.%s' % self.name) self.client = CampfireBot(self) def setServiceParent(self, service): self.client.connect() def disconnect(self): self.client.disconnect() return True def url(self): protocol = self.secure and 'https' or 'http' return '%s://%s.campfirenow.com/' % (protocol, self.subdomain) def send(self, response): return self.client.send(response) def join(self, room_name): return self.client.join(room_name) def leave(self, room_name): return self.client.leave(room_name) def truncation_point(self, response, event=None): return None
class 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 SourceFactory(IbidSourceFactory, smtp.SMTPFactory): supports = ('multiline',) port = IntOption('port', 'Port number to listen on', 10025) address = Option('address', 'Email address to accept messages for and send from', 'ibid@localhost') accept = ListOption('accept', 'Email addresses to accept messages for', []) relayhost = Option('relayhost', 'SMTP server to relay outgoing messages to') def __init__(self, name): IbidSourceFactory.__init__(self, name) self.log = logging.getLogger('source.%s' % name) self.delivery = IbidDelivery(self) def buildProtocol(self, addr): p = smtp.SMTPFactory.buildProtocol(self, addr) p.delivery = self.delivery return p def setServiceParent(self, service): self.service = service if service: internet.TCPServer(self.port, self).setServiceParent(service) else: reactor.listenTCP(self.port, self) def url(self): return u'mailto:%s' % (self.address,) def respond(self, event): messages = {} for response in event.responses: if response['target'] not in messages: messages[response['target']] = response else: messages[response['target']]['reply'] += '\n' + response['reply'] for message in messages.values(): if 'subject' not in message: message['Subject'] = 'Re: ' + event['subject'] if 'message-id' in event.headers: response['In-Reply-To'] = event.headers['message-id'] if 'references' in event.headers: response['References'] = '%(references)s %(message-id)s' % event.headers elif 'in-reply-to' in event.headers: response['References'] = '%(in-reply-to)s %(message-id)s' % event.headers else: response['References'] = '%(message-id)s' % event.headers self.send(message) def send(self, response): message = response['reply'] response['To'] = response['target'] response['Date'] = smtp.rfc822date() if 'Subject' not in response: response['Subject'] = 'Message from %s' % ibid.config['botname'] response['Content-Type'] = 'text/plain; charset=utf-8' del response['target'] del response['source'] del response['reply'] body = '' for header, value in response.items(): body += '%s: %s\n' % (header, value) body += '\n' body += message port = ':' in self.relayhost and int(self.relayhost.split(':')[1]) or 25 smtp.sendmail(self.relayhost.split(':')[0], self.address, response['To'], body.encode('utf-8'), port=port) self.log.debug(u"Sent email to %s: %s", response['To'], response['Subject'])
class SourceFactory(protocol.ReconnectingClientFactory, IbidSourceFactory): protocol = Ircbot auth = ('hostmask', 'nickserv') supports = ('action', 'notice', 'topic', 'channel key') port = IntOption('port', 'Server port number', 6667) ssl = BoolOption('ssl', 'Use SSL', False) server = Option('server', 'Server hostname') nick = Option('nick', 'IRC nick', ibid.config['botname']) realname = Option('realname', 'Full Name', ibid.config['botname']) password = Option('password', 'Connection password', None) username = Option('username', 'Local username', None) modes = Option('modes', 'User modes to set') channels = ListOption('channels', 'Channels to autojoin', []) ping_interval = FloatOption('ping_interval', 'Seconds idle before sending a PING', 60) pong_timeout = FloatOption('pong_timeout', 'Seconds to wait for PONG', 300) # ReconnectingClient uses this: maxDelay = IntOption('max_delay', 'Max seconds to wait inbetween reconnects', 900) factor = FloatOption('delay_factor', 'Factor to multiply delay inbetween reconnects by', 2) def __init__(self, name): IbidSourceFactory.__init__(self, name) self._auth = {} self._auth_ticket = 0 self._auth_ticket_lock = Lock() self.log = logging.getLogger('source.%s' % self.name) def setServiceParent(self, service): if self.ssl: sslctx = ssl.ClientContextFactory() if service: internet.SSLClient(self.server, self.port, self, sslctx).setServiceParent(service) else: reactor.connectSSL(self.server, self.port, self, sslctx) else: if service: internet.TCPClient(self.server, self.port, self).setServiceParent(service) else: reactor.connectTCP(self.server, self.port, self) def connect(self): return self.setServiceParent(None) def disconnect(self): self.stopTrying() self.stopFactory() if hasattr(self, 'proto'): self.proto.transport.loseConnection() return True def join(self, channel, key=None): return self.proto.join(channel, key) def leave(self, channel): return self.proto.leave(channel) def change_nick(self, nick): return self.proto.setNick(nick.encode('utf-8')) def send(self, response): return self.proto.send(response) def logging_name(self, identity): if identity is None: return u'' return identity.split(u'!')[0] def truncation_point(self, response, event=None): target = response['target'].split('!')[0] raw_target = target.encode('utf-8') if hasattr(self.proto, 'hostmask'): hostmask_len = len(self.proto.hostmask) else: hostmask_len = 50 # max = 512 - len(':' + hostmask + ' ' + command + ' ' + target + ' :\r\n') cmds = { 'notice': len('NOTICE'), 'topic': len('TOPIC'), 'action': len('PRIVMSG\001ACTION \001') } for cmd, command_len in cmds.items(): if response.get(cmd, False): break else: command_len = len('PRIVMSG') return 505 - command_len - len(raw_target) - hostmask_len def url(self): return u'irc://%s@%s:%s' % (self.nick, self.server, self.port) def auth_hostmask(self, event, credential=None): for credential in event.session.query(Credential) \ .filter_by(method=u'hostmask', account_id=event.account) \ .filter(or_(Credential.source == event.source, Credential.source == None)) \ .all(): if fnmatch(event.sender['connection'], credential.credential): return True def auth_nickserv(self, event, credential): self._auth_ticket_lock.acquire() self._auth_ticket += 1 ticket = self._auth_ticket self._auth_ticket_lock.release() def callback(result): self._auth[ticket] = result reactor.callFromThread(self.proto.authenticate, event.sender['nick'], callback) # We block in the plugin thread for up to this long, waiting for # NickServ to reply wait = 15 for i in xrange(wait * 10): if ticket in self._auth: break sleep(0.1) if ticket in self._auth: result = self._auth[ticket] del self._auth[ticket] return result
class 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:
class Set(Processor): usage = u"""<name> (<verb>|=<verb>=) [also] <value> last set factoid""" features = ('factoid', ) interrogatives = ListOption('interrogatives', 'Question words to strip', default_interrogatives) verbs = ListOption('verbs', 'Verbs that split name from value', default_verbs) priority = 800 permission = u'factoid' last_set_factoid = None def setup(self): self.set_factoid.im_func.pattern = re.compile( r'^(no[,.: ]\s*)?(.+?)\s+(also\s+)?(?:=(\S+)=)?(?(4)|(%s))(\s+also)?\s+((?(3).+|(?!.*=\S+=).+))$' % '|'.join(self.verbs), re.I) self.set_factoid.im_func.message_version = 'deaddressed' @handler @authorise(fallthrough=False) def set_factoid(self, event, correction, name, addition1, verb1, verb2, addition2, value): verb = verb1 or verb2 addition = addition1 or addition2 name = strip_name(name) if name == u'': event.addresponse(u"Sorry, I'm not interested in empty factoids") return if name.lower() in self.interrogatives: event.addresponse( choice(( u"I'm afraid I have no idea", u"Not a clue, sorry", u"Erk, dunno", ))) return factoid = event.session.query(Factoid).join(Factoid.names)\ .filter(FactoidName.name==escape_name(name)).first() if factoid: if correction: identities = get_identities(event) if not auth_responses(event, u'factoidadmin') and len( filter(lambda x: x.identity_id not in identities, factoid.values)) > 0: return for fvalue in factoid.values: event.session.delete(fvalue) elif not addition: event.addresponse(u'I already know stuff about %s', name) return else: factoid = Factoid() fname = FactoidName(unicode(name), event.identity) factoid.names.append(fname) event.session.add(factoid) event.session.flush() log.info(u"Creating factoid %s with name '%s' by %s", factoid.id, fname.name, event.identity) if not reply_re.match(value) and not action_re.match(value): value = '%s %s' % (verb, value) fvalue = FactoidValue(unicode(value), event.identity) factoid.values.append(fvalue) event.session.add(factoid) event.session.commit() self.last_set_factoid = factoid.names[0].name log.info(u"Added value '%s' to factoid %s (%s) by %s/%s (%s)", fvalue.value, factoid.id, factoid.names[0].name, event.account, event.identity, event.sender['connection']) event.addresponse( choice(( u'If you say so', u'One learns a new thing every day', u"I'll remember that", u'Got it', ))) @match(r'^(?:last\s+set\s+factoid|what\s+did\s+\S+\s+just\s+set)$') def last_set(self, event): if self.last_set_factoid is None: event.addresponse(u'Sorry, nobody has taught me anything recently') else: event.addresponse(u'It was: %s', self.last_set_factoid)
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 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 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
class DuelDraw(Processor): usage = u"""draw [my <weapon>] bam|pew|bang|kapow|pewpew|holyhandgrenadeofantioch""" feature = ('duel', ) # Parameters for Processor: event_types = (u'message', u'action') addressed = BoolOption('addressed', 'Must the bot be addressed?', True) # Game configurables: weapons = DictOption( 'weapons', 'Weapons that can be used: name: (chance, damage)', { u'bam': (0.75, 50), u'pew': (0.75, 50), u'fire': (0.75, 70), u'fires': (0.75, 70), u'bang': (0.75, 70), u'kapow': (0.75, 90), u'pewpew': (0.75, 110), u'holyhandgrenadeofantioch': (1.0, 200), }) extremities = ListOption('extremities', u'Extremities that can be hit', ( u'toe', u'foot', u'leg', u'thigh', u'finger', u'hand', u'arm', u'elbow', u'shoulder', u'ear', u'nose', u'stomach', )) vitals = ListOption('vitals', 'Vital parts of the body that can be hit', ( u'head', u'groin', u'chest', u'heart', u'neck', )) draw_required = BoolOption('draw_required', 'Must you draw your weapon before firing?', True) extratime = FloatOption( 'extratime', 'How much more time to grant after every shot fired?', 1.0) @match(r'^draws?(?:\s+h(?:is|er)\s+.*|\s+my\s+.*)?$') def draw(self, event): if (event.source, event.channel) not in duels: if event.get('addressed', False): event.addresponse( choice(( u"We do not permit drawn weapons here", u"You may only draw a weapon on the field of honour", ))) return duel = duels[(event.source, event.channel)] shooter = event.sender['nick'] if shooter.lower() not in duel.names: event.addresponse( choice(( u"Spectators are not permitted to draw weapons", u"Do you think you are %(fighter)s?", )), {'fighter': choice(duel.names.values())}) return if not duel.started: event.addresponse( choice(( u"Now now, not so fast!", u"Did I say go yet?", u"Put that AWAY!", ))) return duel.drawn[shooter.lower()] = True event.addresponse(True) def setup(self): self.fire.im_func.pattern = re.compile( r'^(%s)(?:[\s,.!:;].*)?$' % '|'.join(self.weapons.keys()), re.I | re.DOTALL) @handler def fire(self, event, weapon): shooter = event.sender['nick'].lower() if (event.source, event.channel) not in duels: return duel = duels[(event.source, event.channel)] if shooter not in duel.names: event.addresponse( choice(( u"You aren't in a war", u'You are a non-combatant', u'You are a spectator', ))) return enemy = set(duel.names.keys()) enemy.remove(shooter) enemy = enemy.pop() if self.draw_required and not duel.drawn[shooter]: recipient = shooter else: recipient = enemy if not duel.started or not duel.confirmed: if self.draw_required: message = choice(( u"%(shooter)s tried to escape his duel by shooting himself in the foot. The duel has been cancelled, but his honour is forfeit", u"%(shooter)s shot himself while preparing for his duel. The funeral will be held on the weekend", )) elif not duel.started: message = choice(( u"FOUL! %(shooter)s fired before my mark. Just as well you didn't hit anything. I refuse to referee under these conditions", u"FOUL! %(shooter)s injures %(enemy)s before the match started and is marched away in handcuffs", u"FOUL! %(shooter)s killed %(enemy)s before the match started and was shot by the referee before he could hurt anyone else", )) else: message = choice(( u"FOUL! The duel is not yet confirmed. %(shooter)s is marched away in handcuffs", u"FOUL! Arrest %(shooter)s! Firing a weapon within city limits is not permitted", )) event.addresponse(message, { 'shooter': duel.names[shooter], 'enemy': duel.names[enemy], }, address=False) del duels[(event.source, event.channel)] duel.stop() return chance, power = self.weapons[weapon.lower()] if random() < chance: damage = max(gauss(power, power / 2.0), 0) duel.hp[recipient] -= damage if duel.hp[recipient] <= 0.0: del duels[(event.source, event.channel)] duel.stop() else: duel.timeout_callback.delay(self.extratime) params = { 'shooter': duel.names[shooter], 'enemy': duel.names[enemy], 'part': u'foot', } if shooter == recipient: message = u"TRAGEDY: %(shooter)s shoots before drawing his weapon. " if damage > 100.0: message += choice(( u"The explosion killed him", u"There was little left of him", )) elif duel.hp[recipient] <= 0.0: message += choice(( u"Combined with his other injuries, he didn't stand a chance", u"He died during field surgery", )) else: message += choice(( u"Luckily, it was only a flesh wound", u"He narrowly missed his femoral artery", )) elif damage > 100.0: message = u'VICTORY: ' + choice(( u'%(shooter)s blows %(enemy)s away', u'%(shooter)s destroys %(enemy)s', )) elif duel.hp[enemy] <= 0.0: message = u'VICTORY: ' + choice(( u'%(shooter)s kills %(enemy)s with a shot to the %(part)s', u'%(shooter)s shoots %(enemy)s killing him with a fatal shot to the %(part)s', )) params['part'] = choice(self.vitals) else: message = choice(( u'%(shooter)s hits %(enemy)s in the %(part)s, wounding him', u'%(shooter)s shoots %(enemy)s in the %(part)s, but %(enemy)s can still fight', )) params['part'] = choice(self.extremities) event.addresponse(message, params, address=False) elif shooter == recipient: event.addresponse(choice(( u"%s forget to draw his weapon. Luckily he missed his foot", u"%s fires a holstered weapon. Luckily it only put a hole in his jacket", u"%s won't win at this rate. He forgot to draw before firing. He missed himself too", )), duel.names[shooter], address=False) else: event.addresponse(choice((u'%s misses', u'%s aims wide', u'%s is useless with a weapon')), duel.names[shooter], address=False)
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>]""" 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)