Пример #1
0
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)
Пример #2
0
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
Пример #3
0
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
Пример #4
0
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
Пример #5
0
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
Пример #6
0
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:
Пример #7
0
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)
Пример #8
0
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)
Пример #9
0
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')
Пример #10
0
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
Пример #11
0
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)
Пример #12
0
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__
Пример #13
0
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)
Пример #14
0
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
Пример #15
0
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()
Пример #16
0
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
Пример #17
0
                            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'.+',