class Bash(Processor): usage = u'bash[.org] [(random|<number>)]' features = ('bash', ) public_browse = BoolOption('public_browse', 'Allow random quotes in public', True) @match(r'^bash(?:\.org)?(?:\s+#?(random|\d+))?$') def bash(self, event, id): id = id is None and u'random' or id.lower() if id == u'random' and event.public and not self.public_browse: event.addresponse(u'Sorry, not in public. PM me') return soup = get_html_parse_tree('http://bash.org/?%s' % id) number = u"".join(soup.find('p', 'quote').find('b').contents) output = [u'%s:' % number] body = soup.find('p', 'qt') if not body: event.addresponse( u"There's no such quote, but if you keep talking like that maybe there will be" ) else: for line in body.contents: line = unicode(line).strip() if line != u'<br />': output.append(line) event.addresponse(u'\n'.join(output), conflate=False)
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 IMDB(Processor): usage = u'imdb [search] [character|company|episode|movie|person] <terms> [#<index>]' features = ('imdb', ) access_system = Option("accesssystem", "Method of querying IMDB", "http") adult_search = BoolOption("adultsearch", "Include adult films in search results", True) name_keys = { "character": "long imdb name", "company": "long imdb name", "episode": "long imdb title", "movie": "long imdb title", "person": "name", } def setup(self): if IMDb is None: raise Exception("IMDbPY not installed") self.imdb = IMDb(accessSystem=self.access_system, adultSearch=int(self.adult_search)) @match( r'^imdb(?:\s+search)?(?:\s+(character|company|episode|movie|person))?\s+(.+?)(?:\s+#(\d+))?$' ) def search(self, event, search_type, terms, index): if search_type is None: search_type = "movie" if index is not None: index = int(index) - 1 result = None try: if terms.isdigit(): result = getattr(self.imdb, "get_" + search_type)(terms) else: results = getattr(self.imdb, "search_" + search_type)(terms) if len(results) == 1: index = 0 if index is not None: result = results[index] self.imdb.update(result) except IMDbDataAccessError, e: event.addresponse(u"IMDb doesn't like me today. It said '%s'", e[0]["errmsg"]) raise except IMDbError, e: event.addresponse( u'IMDb must be having a bad day (or you are asking it silly things)' ) raise
class SourceFactory(IbidSourceFactory): auth = ('implicit', ) supports = ('action', 'multiline', 'topic') subdomain = Option('subdomain', 'Campfire subdomain') secure = BoolOption('secure', 'Use https (paid accounts only)', False) token = Option('token', 'Campfire token') rooms = ListOption('rooms', 'Rooms to join', []) keepalive_timeout = IntOption( 'keepalive_timeout', 'Stream keepalive timeout. ' 'Campfire sends a keepalive every <5 seconds', 30) def __init__(self, name): super(SourceFactory, self).__init__(name) self.log = logging.getLogger('source.%s' % self.name) self.client = CampfireBot(self) def setServiceParent(self, service): self.client.connect() def disconnect(self): self.client.disconnect() return True def url(self): protocol = self.secure and 'https' or 'http' return '%s://%s.campfirenow.com/' % (protocol, self.subdomain) def send(self, response): return self.client.send(response) def join(self, room_name): return self.client.join(room_name) def leave(self, room_name): return self.client.leave(room_name) def truncation_point(self, response, event=None): return None
class 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 MyLifeIsAverage(Processor): usage = (u"mlia [(<number> | random | recent | today | yesterday | " u"this week | this month | this year )]") features = ('mlia', ) public_browse = BoolOption('public_browse', 'Allow random quotes in public', True) random_pool = [] pages = 1 def find_stories(self, url): if isinstance(url, basestring): tree = get_html_parse_tree(url, treetype='etree') else: tree = url stories = [ div for div in tree.findall('.//div') if div.get(u'class') == u'story s' ] for story in stories: body = story.findtext('div').strip() id = story.findtext('.//a') if isinstance(id, basestring) and id[1:].isdigit(): id = int(id[1:]) yield id, body @match(r'^mlia(?:\s+this)?' r'(?:\s+(\d+|random|recent|today|yesterday|week|month|year))?$') def mlia(self, event, query): query = query is None and u'random' or query.lower() if query == u'random' and event.public and not self.public_browse: event.addresponse(u'Sorry, not in public. PM me') return url = 'http://mylifeisaverage.com/' if query == u'random' or query is None: if not self.random_pool: purl = url + str(randint(1, self.pages)) tree = get_html_parse_tree(purl, treetype='etree') self.random_pool = list(self.find_stories(tree)) shuffle(self.random_pool) pagination = [ ul for ul in tree.findall('.//ul') if ul.get(u'class') == u'pages' ][0] self.pages = int([ li for li in pagination.findall('li') if li.get(u'class') == u'last' ][0].find(u'a').get(u'href')) story = self.random_pool.pop() else: try: if query.isdigit(): surl = url + '/s/' + query else: surl = url + '/best/' + query story = self.find_stories(surl).next() except StopIteration: event.addresponse(u'No such quote') return id, body = story url += 's/%i' % id event.addresponse(u'%(body)s\n- %(url)s', { 'url': url, 'body': body, }) @match(r'^(?:http://)?(?:www\.)?mylifeisaverage\.com' r'/s/(\d+)$') def mlia_url(self, event, id): self.mlia(event, id)
class TextsFromLastNight(Processor): usage = u"""tfln [(random|<number>)] tfln (worst|best) [(today|this week|this month)]""" features = ('tfln', ) public_browse = BoolOption('public_browse', 'Allow random quotes in public', True) random_pool = [] def get_tfln(self, section): tree = get_html_parse_tree('http://textsfromlastnight.com/%s' % section, treetype='etree') ul = [x for x in tree.findall('.//ul') if x.get('id') == 'texts-list'][0] id_re = re.compile('^/Text-Replies-(\d+)\.html$') for li in ul.findall('li'): id = 0 message = '' div = [x for x in li.findall('div') if x.get('class') == 'text'][0] for a in div.findall('.//a'): href = a.get('href') if href.startswith('/Texts-From-Areacode-'): message += u'\n' + a.text elif href.startswith('/Text-Replies-'): id = int(id_re.match(href).group(1)) message += a.text yield id, message.strip() @match(r'^tfln' r'(?:\s+(random|worst|best|\d+))?' r'(?:this\s+)?(?:\s+(today|week|month))?$') def tfln(self, event, number, timeframe=None): number = number is None and u'random' or number.lower() if number == u'random' and not timeframe \ and event.public and not self.public_browse: event.addresponse(u'Sorry, not in public. PM me') return if number in (u'worst', u'best'): number = u'Texts-From-%s-Nights' % number.title() if timeframe: number += u'-' + timeframe.title() number += u'.html' elif number.isdigit(): number = 'Text-Replies-%s.html' % number if number == u'random': if not self.random_pool: self.random_pool = [ message for message in self.get_tfln( u'Random-Texts-From-Last-Night.html') ] shuffle(self.random_pool) message = self.random_pool.pop() else: try: message = self.get_tfln(number).next() except StopIteration: event.addresponse(u'No such quote') return id, body = message event.addresponse( u'%(body)s\n' u'- http://textsfromlastnight.com/Text-Replies-%(id)i.html', { 'id': id, 'body': body, }, conflate=False) @match(r'^(?:http://)?(?:www\.)?textsfromlastnight\.com/' r'Text-Replies-(\d+).html$') def tfln_url(self, event, id): self.tfln(event, id)
class FMyLife(Processor): usage = u'fml (<number> | [random] | flop | top | last | love | money | kids | work | health | sex | miscellaneous )' features = ('fml', ) api_url = Option('fml_api_url', 'FML API URL base', 'http://api.betacie.com/') # The Ibid API Key, registered by Stefano Rivera: api_key = Option('fml_api_key', 'FML API Key', '4b39a7fcaf01c') fml_lang = Option('fml_lang', 'FML Lanugage', 'en') public_browse = BoolOption('public_browse', 'Allow random quotes in public', True) failure_messages = ( u'Today, I tried to get a quote for %(nick)s but failed. FML', u'Today, FML is down. FML', u"Sorry, it's broken, the FML admins must having a really bad day", ) def remote_get(self, id): url = urljoin( self.api_url, 'view/%s?%s' % (id.isalnum() and id + '/nocomment' or quote(id), urlencode({ 'language': self.fml_lang, 'key': self.api_key }))) f = urlopen(url) try: tree = ElementTree.parse(f) except SyntaxError: class_, e, tb = exc_info() new_exc = FMLException(u'XML Parsing Error: %s' % unicode(e)) raise new_exc.__class__, new_exc, tb if tree.find('.//error'): raise FMLException(tree.findtext('.//error')) item = tree.find('.//item') if item: url = u"http://www.fmylife.com/%s/%s" % ( item.findtext('category'), item.get('id'), ) text = item.find('text').text return u'%s\n- %s' % (text, url) @match( r'^(?:fml\s+|http://www\.fmylife\.com/\S+/)(\d+|random|flop|top|last|love|money|kids|work|health|sex|miscellaneous)$' ) def fml(self, event, id): try: body = self.remote_get(id) except (FMLException, HTTPError, BadStatusLine): event.addresponse(choice(self.failure_messages) % event.sender) return if body: event.addresponse(body) elif id.isdigit(): event.addresponse(u'No such quote') else: event.addresponse(choice(self.failure_messages) % event.sender) @match(r'^fml$') def fml_default(self, event): if not event.public or self.public_browse: self.fml(event, 'random') else: event.addresponse(u'Sorry, not in public. PM me')
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 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 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 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 Subversion(Processor, RPC): usage = u"""(last commit|commit <revno>) [to <repo>] [full] (svnrepos|svnrepositories) """ feature = ('svn', ) autoload = False permission = u'svn' repositories = DictOption('repositories', 'Dict of repositories names and URLs') svn_command = Option('svn_command', 'Path to svn executable', 'svn') svn_timeout = FloatOption('svn_timeout', 'Maximum svn execution time (sec)', 15.0) multiline = BoolOption('multiline', 'Output multi-line (Jabber, Campfire)', False) def __init__(self, name): self.log = logging.getLogger('plugins.svn') Processor.__init__(self, name) RPC.__init__(self) def setup(self): self.branches = {} for name, repository in self.repositories.items(): reponame = name.lower() if pysvn: self.branches[reponame] = PySVNBranch( reponame, repository['url'], username=repository['username'], password=repository['password'], multiline=self.multiline) else: self.branches[reponame] = CommandLineBranch( reponame, repository['url'], username=repository['username'], password=repository['password'], svn_command=self.svn_command, svn_timeout=self.svn_timeout, multiline=self.multiline) @match(r'^svn ?(?:repos|repositories)$') @authorise() 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.strip(), 'source': repo['source'], 'target': repo['channel'], }) return True @match( r'^(?:last\s+)?commit(?:\s+(\d+))?(?:(?:\s+to)?\s+(\S+?))?(\s+full)?$') @authorise() def commit(self, event, revno, repository, full): if repository == "full": repository = None full = True if full: full = True revno = revno and int(revno) or None commits = self.get_commits(repository, revno, full=full) if commits: for commit in commits: if commit: event.addresponse(commit.strip()) 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: (repository, branch) = self.branches.items()[0] if not start: start = HEAD_REVISION if not end: end = None commits = branch.get_commits(start, end_revision=end, full=full) return commits
class Tickets(Processor, RPC): usage = u"""ticket <number> (open|my|<who>'s) tickets""" feature = ('trac',) autoload = 'trac' in ibid.databases url = Option('url', 'URL of Trac instance') source = Option('source', 'Source to send commit notifications to') channel = Option('channel', 'Channel to send commit notifications to') announce_changes = BoolOption('announce_changes', u'Announce changes to tickets', True) def __init__(self, name): Processor.__init__(self, name) RPC.__init__(self) self.log = logging.getLogger('plugins.trac') def get_ticket(self, id): session = ibid.databases.trac() ticket = session.query(Ticket).get(id) session.close() return ticket def remote_ticket_created(self, id): ticket = self.get_ticket(id) if not ticket: raise Exception(u"No such ticket") message = u'New %s in %s reported by %s: "%s" %sticket/%s' % (ticket.type, ticket.component, ticket.reporter, ticket.summary, self.url, ticket.id) ibid.dispatcher.send({'reply': message, 'source': self.source, 'target': self.channel}) self.log.info(u'Ticket %s created', id) return True def remote_ticket_changed(self, id, comment, author, old_values): if not self.announce_changes: return False ticket = self.get_ticket(id) if not ticket: raise Exception(u'No such ticket') changes = [] for field, old in old_values.items(): if hasattr(ticket, field): changes.append(u'%s: %s' % (field, getattr(ticket, field))) if comment: changes.append(u'comment: "%s"' % comment) message = u'Ticket %s (%s %s %s in %s for %s) modified by %s. %s' % (id, ticket.status, ticket.priority, ticket.type, ticket.component, ticket.milestone, author, u', '.join(changes)) ibid.dispatcher.send({'reply': message, 'source': self.source, 'target': self.channel}) self.log.info(u'Ticket %s modified', id) return True @match(r'^ticket\s+(\d+)$') def get(self, event, number): ticket = self.get_ticket(int(number)) if ticket: event.addresponse(u'Ticket %(id)s (%(status)s %(priority)s %(type)s in %(component)s for %(milestone)s) ' u'reported %(ago)s ago assigned to %(owner)s: "%(summary)s" %(url)sticket/%(id)s', { 'id': ticket.id, 'status': ticket.status, 'priority': ticket.priority, 'type': ticket.type, 'component': ticket.component, 'milestone': ticket.milestone, 'ago': ago(datetime.now() - datetime.fromtimestamp(ticket.time), 2), 'owner': ticket.owner, 'summary': ticket.summary, 'url': self.url, }) else: event.addresponse(u"No such ticket") @match(r"^(?:(my|\S+?(?:'s))\s+)?(?:(open|closed|new|assigned)\s+)?tickets(?:\s+for\s+(.+?))?$") def handle_list(self, event, owner, status, milestone): session = ibid.databases.trac() status = status or 'open' if status.lower() == 'open': statuses = (u'new', u'assigned', u'reopened') else: statuses = (status.lower(),) query = session.query(Ticket).filter(Ticket.status.in_(statuses)) if owner: if owner.lower() == 'my': owner = event.sender['nick'] else: owner = owner.lower().replace("'s", '') query = query.filter(func.lower(Ticket.owner)==(owner.lower())) if milestone: query = query.filter_by(milestone=milestone) tickets = query.order_by(Ticket.id).all() if len(tickets) > 0: event.addresponse(u', '.join(['%s (%s): "%s"' % (ticket.id, ticket.owner, ticket.summary) for ticket in tickets])) else: event.addresponse(u"No tickets found") session.close()
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
method.im_func.failing = False except: if not method.failing: self.__log.exception(u'Periodic method failing: %s', 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'.+',