Esempio n. 1
0
File: core.py Progetto: vhata/ibid
class Complain(Processor):

    priority = 950
    processed = True
    event_types = (u'message', u'action', u'invite')

    complaints = DictOption('complaints', 'Complaint responses', {
        'nonsense': (
            u'Huh?', u'Sorry...',
            u'Excuse me?', u'*blink*', u'What?',
        ),
        'notauthed': (
            u"I'm not your bitch", u"Just do it yourself",
            u"I'm not going to listen to you", u"You're not the boss of me",
        ),
        'exception': (
            u"I'm not feeling too well", u"That didn't go down very well. Burp.",
            u"That didn't seem to agree with me",
        ),
        'network': (
            u'The tubes are clogged!', u"I can't reach that site",
            u"That site seems to be down",
        ),
    })

    @handler
    def complain(self, event):
        if 'complain' in event and not event.responses:
            event.addresponse(choice(self.complaints[event.complain]))
        elif event.processed:
            return
        else:
            event.addresponse(choice(self.complaints['nonsense']))
Esempio n. 2
0
class RMadison(Processor):
    usage = u"""what versions of <package> are in <distro>[/<version>]
    rmadison <package> [in <distro>[/<version>]]
    """
    features = ('rmadison', )
    rmadison_sources = DictOption(
        'rmadison_sources', "Rmadison service URLs", {
            'debian': 'http://qa.debian.org/madison.php',
            'bpo': 'http://www.backports.org/cgi-bin/madison.cgi',
            'debug': 'http://debug.debian.net/cgi-bin/madison.cgi',
            'ubuntu':
            'http://people.canonical.com/~ubuntu-archive/madison.cgi',
            'udd': 'http://qa.debian.org/cgi-bin/madison.cgi',
        })

    @match(r'^(?:what\s+)?versions?\s+of\s+(\S+)\s+(?:are\s+)?'
           r'in\s+(\S+?)(?:[\s/]+(\S+))?$')
    def english_rmadison(self, event, package, distro, release):
        self.rmadison(event, package, distro, release)

    @match(r'^rmadison\s+(\S+)(?:\s+in\s+(\S+?)(?:[\s/]+(\S+))?)?$')
    def rmadison(self, event, package, distro, release):
        distro = distro and distro.lower() or 'all'
        params = {
            'package': package.lower(),
            'text': 'on',
        }
        if release is not None:
            params['s'] = release.lower()
        if distro == 'all':
            params['table'] = 'all'
            distro = 'udd'
        if distro not in self.rmadison_sources:
            event.addresponse(
                "I'm sorry, but I don't have a madison source for %s", distro)
            return
        table = generic_webservice(self.rmadison_sources[distro], params)
        table = table.strip().splitlines()
        if table and table[0] == 'Traceback (most recent call last):':
            # Not very REST
            event.addresponse(u"Whoops, madison couldn't understand that: %s",
                              table[-1])
        versions = []
        for row in table:
            row = [x.strip() for x in row.split('|')]
            if versions and versions[-1][0] == row[1]:
                versions[-1].append(row[2])
            else:
                versions.append([row[1], row[2]])
        versions = human_join(u'%s (%s)' % (r[0], u', '.join(r[1:]))
                              for r in versions)
        if versions:
            event.addresponse(versions)
        else:
            event.addresponse(u"Sorry, I can't find a package called %s",
                              package.lower())
Esempio n. 3
0
class Twitter(Processor):
    usage = u"""latest (tweet|identica) from <name>
    (tweet|identica) <number>"""

    features = ('microblog', )

    default = {
        'twitter': {
            'endpoint': 'http://twitter.com/',
            'api': 'twitter',
            'name': 'tweet',
            'user': '******'
        },
        'tweet': {
            'endpoint': 'http://twitter.com/',
            'api': 'twitter',
            'name': 'tweet',
            'user': '******'
        },
        'identica': {
            'endpoint': 'http://identi.ca/api/',
            'api': 'laconica',
            'name': 'dent',
            'user': '******'
        },
        'identi.ca': {
            'endpoint': 'http://identi.ca/api/',
            'api': 'laconica',
            'name': 'dent',
            'user': '******'
        },
        'dent': {
            'endpoint': 'http://identi.ca/api/',
            'api': 'laconica',
            'name': 'dent',
            'user': '******'
        },
    }
    services = DictOption('services', 'Micro blogging services', default)

    class NoTweetsException(Exception):
        pass

    def setup(self):
        self.update.im_func.pattern = re.compile(
            r'^(%s)\s+(\d+)$' % '|'.join(self.services.keys()), re.I)
        self.latest.im_func.pattern = re.compile(
            r'^(?:latest|last)\s+(%s)\s+(?:update\s+)?(?:(?:by|from|for)\s+)?@?(\S+)$'
            % '|'.join(self.services.keys()), re.I)

    def remote_update(self, service, id):
        status = json_webservice('%sstatuses/show/%s.json' %
                                 (service['endpoint'], id))

        return {
            'screen_name': status['user']['screen_name'],
            'text': decode_htmlentities(status['text'])
        }

    def remote_latest(self, service, user):
        if service['api'] == 'twitter':
            # Twitter ommits retweets in the JSON and XML results:
            statuses = generic_webservice(
                '%sstatuses/user_timeline/%s.atom' %
                (service['endpoint'], user.encode('utf-8')), {'count': 1})
            tree = ElementTree.fromstring(statuses)
            latest = tree.find('{http://www.w3.org/2005/Atom}entry')
            if latest is None:
                raise self.NoTweetsException(user)
            return {
                'text':
                latest.findtext('{http://www.w3.org/2005/Atom}content').split(
                    ': ', 1)[1],
                'ago':
                ago(datetime.utcnow() - parse_timestamp(
                    latest.findtext('{http://www.w3.org/2005/Atom}published'))
                    ),
                'url': [
                    x for x in latest.getiterator(
                        '{http://www.w3.org/2005/Atom}link')
                    if x.get('type') == 'text/html'
                ][0].get('href'),
            }
        elif service['api'] == 'laconica':
            statuses = json_webservice(
                '%sstatuses/user_timeline/%s.json' %
                (service['endpoint'], user.encode('utf-8')), {'count': 1})
            if not statuses:
                raise self.NoTweetsException(user)
            latest = statuses[0]
            url = '%s/notice/%i' % (service['endpoint'].split(
                '/api/', 1)[0], latest['id'])

        return {
            'text': decode_htmlentities(latest['text']),
            'ago':
            ago(datetime.utcnow() - parse_timestamp(latest['created_at'])),
            'url': url,
        }

    @handler
    def update(self, event, service_name, id):
        service = self.services[service_name.lower()]
        try:
            event.addresponse(u'%(screen_name)s: "%(text)s"',
                              self.remote_update(service, int(id)))
        except HTTPError, e:
            if e.code in (401, 403):
                event.addresponse(u'That %s is private', service['name'])
            elif e.code == 404:
                event.addresponse(u'No such %s', service['name'])
            else:
                log.debug(u'%s raised %s', service['name'], unicode(e))
                event.addresponse(u'I can only see the Fail Whale')
Esempio n. 4
0
class Bazaar(Processor, RPC):
    usage = u"""(last commit|commit <revno>) [to <repo>] [full]
    repositories"""
    features = ('bzr', )
    autoload = False

    repositories = DictOption('repositories',
                              'Dict of repositories names and URLs')
    interval = IntOption('interval',
                         'Interval inbetween checks for new revisions', 300)

    def __init__(self, name):
        self.log = logging.getLogger('plugins.bzr')
        Processor.__init__(self, name)
        RPC.__init__(self)

    def setup(self):
        self.branches = {}
        must_monitor = False
        for name, repository in self.repositories.items():
            try:
                self.branches[name.lower()] = Branch.open(repository['url'])
            except NotBranchError:
                self.log.error(u'%s is not a branch', repository)
                continue
            if repository.get('poll', 'False').lower() in ('yes', 'true'):
                must_monitor = True
        self.check.im_func.disabled = not must_monitor
        if must_monitor:
            self.seen_revisions = {}

    @match(r'^(?:repos|repositories)$')
    def handle_repositories(self, event):
        repositories = self.branches.keys()
        if repositories:
            event.addresponse(u'I know about: %s',
                              human_join(sorted(repositories)))
        else:
            event.addresponse(u"I don't know about any repositories")

    def remote_committed(self, repository, start, end=None):
        commits = self.get_commits(repository, start, end)
        repo = self.repositories[repository]
        for commit in commits:
            ibid.dispatcher.send({
                'reply': commit,
                'source': repo['source'],
                'target': repo['channel'],
            })

        return True

    @match(
        r'^(?:last\s+)?commit(?:\s+(\d+))?(?:(?:\s+to)?\s+(\S+?))?(\s+full)?$')
    def commit(self, event, revno, repository, full):

        revno = revno and int(revno) or None
        commits = self.get_commits(repository, revno, full=full)

        output = u''
        for commit in commits:
            if commit:
                output += commit.strip()
        event.addresponse(output, conflate=False)

    def get_commits(self, repository, start, end=None, full=None):
        branch = None
        if repository:
            repository = repository.lower()
            if repository not in self.branches:
                return None
            branch = self.branches[repository]

        if not branch:
            if len(self.branches) == 1:
                (repository, branch) = self.branches.items()[0]
            else:
                (repository,
                 branch) = sorted(self.branches.iteritems(),
                                  reverse=True,
                                  key=lambda (k, v): v.repository.get_revision(
                                      v.last_revision_info()[1]).timestamp)[0]

        if not start:
            start = branch.revision_id_to_revno(branch.last_revision())

        f = StringIO()
        log.show_log(branch,
                     LogFormatter(f, repository, branch, full),
                     start_revision=start,
                     end_revision=end or start)
        f.seek(0)
        commits = f.readlines()
        commits.reverse()
        return commits

    @handler
    def launchpad(self, event):
        if ibid.sources[event.source].type != 'smtp' \
                or 'X-Launchpad-Branch' not in event.headers:
            return

        event.processed = True

        if 'X-Launchpad-Branch' not in event.headers or 'X-Launchpad-Branch-Revision-Number' not in event.headers:
            return

        for name, repository in self.repositories.iteritems():
            if (event.headers['X-Launchpad-Branch'] == repository.get(
                    'lp_branch', None)):
                self.remote_committed(
                    name,
                    int(event.headers['X-Launchpad-Branch-Revision-Number']))

    @periodic(config_key='interval')
    def check(self, event):
        for name, repo in self.repositories.iteritems():
            if repo.get('poll', 'False').lower() not in ('yes', 'true'):
                continue
            branch = self.branches[name]
            lastrev = branch.last_revision()
            if name not in self.seen_revisions:
                self.seen_revisions[name] = lastrev
                continue
            if lastrev == self.seen_revisions[name]:
                continue

            try:
                commits = self.get_commits(name, None, False)
            except RevisionNotPresent:
                self.log.debug(
                    u"Got a RevisionNotPresent, hoping it won't be there next time..."
                )
                continue
            self.seen_revisions[name] = lastrev

            if commits:
                event.addresponse(unicode(commits[0].strip()),
                                  source=repo['source'],
                                  target=repo['channel'],
                                  address=False)
Esempio n. 5
0
class HTTP(Processor):
    usage = u"""(get|head) <url>
    is <domain> (up|down)
    tell me when <domain|url> is up"""
    features = ('http',)
    priority = -10

    max_size = IntOption('max_size', u'Only request this many bytes', 2048)
    timeout = IntOption('timeout',
            u'Timeout for HTTP connections in seconds', 15)
    sites = DictOption('sites', u'Mapping of site names to domains', {})
    redirect_limit = IntOption('redirect_limit',
            u'Maximum number of http redirects to follow', 5)
    whensitup_delay = IntOption('whensitup_delay',
            u'Initial delay between whensitup attempts in seconds', 60)
    whensitup_factor = FloatOption('whensitup_factor',
            u'Factor to mutliply subsequent delays by for whensitup', 1.03)
    whensitup_maxdelay = IntOption('whensitup_maxdelay',
            u'Maximum delay between whensitup attempts in seconds', 30*60)
    whensitup_maxperiod = FloatOption('whensitup_maxperiod',
            u'Maximum period after which to stop checking the url '
            u'for whensitup in hours', 72)

    def _get_header(self, headers, name):
        for header in headers:
            if header[0] == name:
                return header[1]
        return None

    @match(r'^(get|head)\s+(\S+)$')
    def get(self, event, action, url):
        try:
            status, reason, data, headers = self._request(self._makeurl(url),
                                                          action.upper())
            reply = u'%s %s' % (status, reason)

            hops = 0
            while 300 <= status < 400 and self._get_header(headers, 'location'):
                location = self._get_header(headers, 'location')
                if not location:
                    break
                status, reason, data, headers = self._request(location, 'GET')
                if hops >= self.redirect_limit:
                    reply += u' to %s' % location
                    break
                hops += 1
                reply += u' to %(location)s, which gets a ' \
                         u'%(status)d %(reason)s' % {
                    u'location': location,
                    u'status': status,
                    u'reason': reason,
                }

            if action.upper() == 'GET':
                got_title = False
                content_type = self._get_header(headers, 'content-type')
                if content_type and (content_type.startswith('text/html') or
                        content_type.startswith('application/xhtml+xml')):
                    match = re.search(r'<title>(.*)<\/title>', data,
                                      re.I | re.DOTALL)
                    if match:
                        got_title = True
                        reply += u' "%s"' % match.groups()[0].strip()

                if not got_title and content_type:
                    reply += u' ' + content_type

            event.addresponse(reply)

        except HTTPException, e:
            event.addresponse(unicode(e))
Esempio n. 6
0
class ICECast(Processor):
    usage = u"what's playing [on <stream>]?"
    feature = ('icecast', )

    interval = IntOption('interval',
                         'Interval between checking for song changes', 60)
    streams = DictOption(
        'streams',
        'Dictionary of Stream names to base URL (include trailing /)', {})

    last_checked = None
    last_songs = {}

    def scrape_status(self, stream):
        tree = get_html_parse_tree(self.streams[stream]['url'] + 'status.xsl',
                                   treetype='etree')
        main_table = tree.findall('.//table')[2]
        status = {}
        for row in main_table.findall('.//tr'):
            key, value = [x.text for x in row.findall('td')]
            status[key[:-1]] = value
        return status

    @match(r'^what(?:\'|\s+i)s\s+playing(?:\s+on\s+(.+))?$')
    def playing(self, event, stream):
        if not event.get('addressed', False):
            return

        if len(self.streams) == 0:
            event.addresponse(u"Sorry, I don't know about any streams")
            return
        elif stream is None and len(self.streams) == 1:
            stream = self.streams.keys()[0]
        elif stream is not None and stream not in self.streams:
            for name in self.streams.iterkeys():
                if name.lower() == stream.lower():
                    stream = name
                    break
            else:
                stream = None
        if stream is None:
            event.addresponse(
                u'Sorry, I only know about the following streams, '
                u'please choose one: %s', human_join(self.streams.keys()))
            return

        try:
            status = self.scrape_status(stream)
            event.addresponse(
                u'Currently Playing on %(stream)s: '
                u'%(song)s - %(description)s (Listeners: %(listeners)s)', {
                    'stream': stream,
                    'song': status['Current Song'],
                    'description': status['Stream Description'],
                    'listeners': status['Current Listeners'],
                })
        except HTTPError:
            event.addresponse(
                u'The stream must be down, back to the MP3 collection for you')

    @periodic(config_key='interval')
    def check(self, event):
        for name, stream in self.streams.iteritems():
            if 'source' in stream and 'channel' in stream:
                log.debug(u'Probing %s', name)
                status = self.scrape_status(name)
                if self.last_songs.get(name, '') != status['Current Song']:
                    self.last_songs[name] = status['Current Song']
                    event.addresponse(
                        u'Now Playing on %(stream)s: '
                        u'%(song)s - %(description)s '
                        u'(Listeners: %(listeners)s)',
                        {
                            'stream': name,
                            'song': status['Current Song'],
                            'description': status['Stream Description'],
                            'listeners': status['Current Listeners'],
                        },
                        source=stream['source'],
                        target=stream['channel'],
                        topic=(stream.get('topic', 'False').lower()
                               in ('yes', 'true')),
                        address=False,
                    )
Esempio n. 7
0
class Distance(Processor):
    usage = u"""distance [in <unit>] between <source> and <destination>
    place search for <place>
    coordinates for <place>"""

    # For Mathematics, see:
    # http://www.mathforum.com/library/drmath/view/51711.html
    # http://mathworld.wolfram.com/GreatCircle.html

    features = ('distance', 'coordinates')

    default_unit_names = {
            'km': "kilometres",
            'mi': "miles",
            'nm': "nautical miles"}
    default_radius_values = {
            'km': 6378,
            'mi': 3963.1,
            'nm': 3443.9}

    unit_names = DictOption('unit_names', 'Names of units in which to specify distances', default_unit_names)
    radius_values = DictOption('radius_values', 'Radius of the earth in the units in which to specify distances', default_radius_values)

    def get_place_data(self, place, num):
        return json_webservice('http://ws.geonames.org/searchJSON', {'q': place, 'maxRows': num, 'username': '******'})

    def get_place(self, place):
        js = self.get_place_data(place, 1)
        if js['totalResultsCount'] == 0:
            return None
        info = js['geonames'][0]
        return {'name': self.format_name(info),
                'lng': radians(info['lng']),
                'lat': radians(info['lat'])}

    def format_name(self, info):
        parts = info['name'], info['adminName1'], info['countryName']
        parts = filter(None, parts)
        uniq_parts = [parts[0]]
        for part in parts[1:]:
            if part != uniq_parts[-1]:
                uniq_parts.append(part)
        return ', '.join(uniq_parts)

    @match(r'^(?:(?:search\s+for\s+place)|(?:place\s+search\s+for)|(?:places\s+for))\s+(\S.+?)\s*$')
    def placesearch(self, event, place):
        js = self.get_place_data(place, 10)
        if js['totalResultsCount'] == 0:
            event.addresponse(u"I don't know of anywhere even remotely like '%s'", place)
        else:
            event.addresponse(u"I can find: %s",
                    (human_join([u"%s, %s, %s" % (p['name'], p['adminName1'], p['countryName'])
                        for p in js['geonames'][:10]],
                        separator=u';')))

    @match(r'^(?:how\s*far|distance)(?:\s+in\s+(\S+))?\s+'
            r'(?:(between)|from)' # Between ... and ... | from ... to ...
            r'\s+(\S.+?)\s+(?(2)and|to)\s+(\S.+?)\s*$')
    def distance(self, event, unit, ignore, src, dst):
        unit_names = self.unit_names
        if unit and unit not in self.unit_names:
            event.addresponse(u"I don't know the unit '%(badunit)s'. I know about: %(knownunits)s", {
                'badunit': unit,
                'knownunits':
                    human_join(u"%s (%s)" % (unit, self.unit_names[unit])
                        for unit in self.unit_names),
            })
            return
        if unit:
            unit_names = [unit]

        srcp, dstp = self.get_place(src), self.get_place(dst)
        if not srcp or not dstp:
            event.addresponse(u"I don't know of anywhere called %s",
                    (u" or ".join("'%s'" % place[0]
                        for place in ((src, srcp), (dst, dstp)) if not place[1])))
            return

        dist = acos(cos(srcp['lng']) * cos(dstp['lng']) * cos(srcp['lat']) * cos(dstp['lat']) +
                    cos(srcp['lat']) * sin(srcp['lng']) * cos(dstp['lat']) * sin(dstp['lng']) +
                    sin(srcp['lat'])*sin(dstp['lat']))

        event.addresponse(u"Approximate distance, as the bot flies, between %(srcname)s and %(dstname)s is: %(distance)s", {
            'srcname': srcp['name'],
            'dstname': dstp['name'],
            'distance': human_join([
                u"%.02f %s" % (self.radius_values[unit]*dist, self.unit_names[unit])
                for unit in unit_names],
                conjunction=u'or'),
        })

    def degrees_minutes_seconds(self, degrees, kind):
        degs = int(degrees)
        minutes = abs(degrees - degs)*60
        mins = int(minutes)
        secs = int((minutes-mins)*60)

        dirn = ''
        if kind == 'lat':
            if degs > 0:
                dirn = ' N'
            elif degs < 0:
                dirn = ' S'
        else:
            if degs > 0:
                dirn = ' E'
            elif degs < 0:
                dirn = ' W'
        degs = abs(degs)
        return u'%i° %iʹ %iʺ%s' % (degs, mins, secs, dirn)

    @match(r"coord(?:inate)?s (?:for|of|to) (.*)")
    def coordinates(self, event, place):
        place_data = self.get_place(place)
        if not place_data:
            event.addresponse("I've never heard of %s", place)
            return

        lat_deg = degrees(place_data['lat'])
        lng_deg = degrees(place_data['lng'])
        place_data.update({
            'lat_deg': lat_deg,
            'lng_deg': lng_deg,
            'lat_dms': self.degrees_minutes_seconds(lat_deg, 'lat'),
            'lng_dms': self.degrees_minutes_seconds(lng_deg, 'lng'),
            })

        latitudes = [('North Pole', 90, 'back of beyond'),
                     ('Arctic Circle', 66+33/60+39/3600,
                        'Arctic'),
                     ('Tropic of Cancer', 23+26/30+21/3600,
                        'north temperate zone'),
                     ('Equator', 0,
                        'northern tropics'),
                     ('Tropic of Capricorn', -(23+26/30+21/3600),
                        'southern tropics'),
                     ('Antarctic Circle', -(66+33/60+39/3600),
                        'south temperate zone'),
                     ('South Pole', -90,
                        'Antarctic'),
                    ]
        for name, lat, zone in latitudes:
            if abs(lat-lat_deg) <= 1/60:
                if name.endswith('Pole'):
                    place_data['lat_desc'] = 'at the ' + name
                else:
                    place_data['lat_desc'] = 'on the ' + name
                break
            elif abs(lat-lat_deg) <= 2:
                place_data['lat_desc'] = 'near the ' + name
                break
        else:
            for (name1, lat1, _), (name2, lat2, zone) in zip(latitudes, latitudes[1:]):
                if lat1 > lat_deg > lat2:
                    place_data['lat_desc'] = 'in the ' + zone
                    break
            else:
                place_data['lat_desc'] = 'beyond the fields we know'

        place_data['tz'] = round(lng_deg/15)

        event.addresponse("%(name)s is at %(lat_dms)s, %(lng_dms)s "
                          u"(%(lat_deg)0.4f°, %(lng_deg)0.4f°). "
                          "That's in nautical time zone GMT%(tz)+i, "
                          "%(lat_desc)s.",
                          place_data)
Esempio n. 8
0
class TimeZone(Processor):
    usage = u"""when is <time> <place|timezone> in <place|timezone>
    time in <place|timezone>"""
    features = ('timezone',)

    zoneinfo = Option('zoneinfo', 'Timezone info directory', '/usr/share/zoneinfo')
    custom_zones = DictOption('timezones', 'Custom timezone names', CUSTOM_ZONES)

    countries = {}
    timezones = {}
    lowerzones = {}

    def setup(self):
        iso3166 = join(self.zoneinfo, 'iso3166.tab')
        if exists(iso3166):
            self.countries = {}
            for line in open(iso3166).readlines():
                if not line.startswith('#'):
                    code, name = line.strip().split('\t')
                    self.countries[code] = name

        zones = join(self.zoneinfo, 'zone.tab')
        if exists(zones):
            self.timezones = defaultdict(list)
            for line in open(zones).readlines():
                if not line.startswith('#'):
                    code, coordinates, zone = line.strip().split('\t', 2)
                    if '\t' in zone:
                        zone, comment = zone.split('\t')
                    self.timezones[code].append(zone)

        self.lowerzones = {}
        for path, directories, filenames in walk(self.zoneinfo):
            if path.replace(self.zoneinfo, '').lstrip('/').split('/')[0] not in ('posix', 'right'):
                for filename in filenames:
                    name = join(path, filename).replace(self.zoneinfo, '').lstrip('/')
                    self.lowerzones[name.lower().replace('etc/', '')] = name

    def _find_timezone(self, string):
        for name, zonename in self.custom_zones.items():
            if string.lower() == name.lower():
                return gettz(zonename)

        zone = gettz(string)
        if zone:
            return zone

        zone = gettz(string.upper())
        if zone:
            return zone

        if string.lower() in self.lowerzones:
            return gettz(self.lowerzones[string.lower()])

        ccode = None
        for code, name in self.countries.items():
            if name.lower() == string.lower():
                ccode = code
        if not ccode:
            if string.replace('.', '').upper() in self.timezones:
                ccode = string.replace('.', '').upper()

        if ccode:
            if len(self.timezones[ccode]) == 1:
                return gettz(self.timezones[ccode][0])
            else:
                raise TimezoneException(u'%s has multiple timezones: %s' % (self.countries[ccode], human_join(self.timezones[ccode])))

        possibles = []
        for zones in self.timezones.values():
            for name in zones:
                if string.replace(' ', '_').lower() in [part.lower() for part in name.split('/')]:
                    possibles.append(name)

        if len(possibles) == 1:
            return gettz(possibles[0])
        elif len(possibles) > 1:
            raise TimezoneException(u'Multiple timezones found: %s' % (human_join(possibles)))

        zone = self._geonames_lookup(string)
        if zone:
            return zone

        raise TimezoneException(u"I don't know about the %s timezone" % (string,))

    def _geonames_lookup(self, place):
        search = json_webservice('http://ws.geonames.org/searchJSON', {'q': place, 'maxRows': 1, 'username': '******'})
        if search['totalResultsCount'] == 0:
            return None

        city = search['geonames'][0]
        timezone = json_webservice('http://ws.geonames.org/timezoneJSON', {'lat': city['lat'], 'lng': city['lng'], 'username': '******'})

        if 'timezoneId' in timezone:
            return gettz(timezone['timezoneId'])

        if 'rawOffset' in timezone:
            offset = timezone['rawOffset']
            return tzoffset('UTC%s%s' % (offset>=0 and '+' or '', offset), offset*3600)

    @match(r'^when\s+is\s+((?:[0-9.:/hT -]|%s)+)(?:\s+in)?(?:\s+(.+))?\s+in\s+(.+)$' % '|'.join(MONTH_SHORT+MONTH_LONG+OTHER_STUFF), simple=False)
    def convert(self, event, time, from_, to):
        try:
            source = time and parse(time) or datetime.now()
        except ValueError:
            event.addresponse(u"That's not a real time")
            return

        try:
            if from_:
                from_zone = self._find_timezone(from_)
            else:
                from_zone = tzlocal()

            to_zone = self._find_timezone(to)
        except TimezoneException, e:
            event.addresponse(unicode(e))
            return

        source = source.replace(tzinfo=from_zone)
        result = source.astimezone(to_zone)

        event.addresponse(time and u'%(source)s is %(destination)s' or 'It is %(destination)s', {
            'source': format_date(source, tolocaltime=False),
            'destination': format_date(result, tolocaltime=False),
        })
Esempio n. 9
0
class Weather(Processor):
    usage = u"""weather in <city>
    forecast for <city>"""

    features = ('weather',)

    defaults = {    'ct': 'Cape Town, South Africa',
                    'jhb': 'Johannesburg, South Africa',
                    'joburg': 'Johannesburg, South Africa',
               }
    places = DictOption('places', 'Alternate names for places', defaults)
    labels = {'temperature': 'temp',
              'humidity': 'humidity',
              'dew point': 'dew',
              'wind': 'wind',
              'pressure': 'pressure',
              'conditions': 'conditions',
              'visibility': 'visibility',
              'uv': 'uv',
              'clouds': 'clouds',
              "yesterday's minimum": 'ymin',
              "yesterday's maximum": 'ymax',
              "yesterday's cooling degree days": 'ycool',
              'sunrise': 'sunrise',
              'sunset': 'sunset',
              'moon rise': 'moonrise',
              'moon set': 'moonset',
              'moon phase': 'moonphase',
              'raw metar': 'metar',
             }

    class WeatherException(Exception):
        pass

    class TooManyPlacesException(WeatherException):
        pass

    def _text(self, string):
        if not isinstance(string, basestring):
            string = ''.join(string.findAll(text=True))
        return re.sub('\s+', ' ', string).strip()

    def _get_page(self, place):
        if place.lower() in self.places:
            place = self.places[place.lower()]

        soup = get_html_parse_tree('http://m.wund.com/cgi-bin/findweather/getForecast?brand=mobile_metric&query=' + quote(place))

        if soup.body.center and soup.body.center.b.string == 'Search not found:':
            raise Weather.WeatherException(u'City not found')

        if soup.table.tr.th and soup.table.tr.th.string == 'Place: Temperature':
            places = []
            for td in soup.table.findAll('td'):
                places.append(td.find('a', href=re.compile('.*html$')).string)

            # Cities with more than one airport give duplicate entries. We can take the first
            if len([x for x in places if x == places[0]]) == len(places):
                url = urljoin('http://m.wund.com/cgi-bin/findweather/getForecast',
                        soup.table.find('td').find('a', href=re.compile('.*html$'))['href'])
                soup = get_html_parse_tree(url)
            else:
                raise Weather.TooManyPlacesException(places)

        return soup

    def remote_weather(self, place):
        soup = self._get_page(place)
        tds = soup.findAll('td')

        values = {}
        for index, td in enumerate(tds):
            text = self._text(td).lower()
            if text.startswith('updated:'):
                values['place'] = td.findAll('b')[1].string
                values['time'] = td.findAll('b')[0].string
            if text in self.labels:
                values[self.labels[text]] = self._text(tds[index+1])

        return values

    def remote_forecast(self, place):
        soup = self._get_page(place)
        forecasts = []
        table = [table for table in soup.findAll('table') if table.findAll('td', align='left')][0]

        for td in table.findAll('td', align='left'):
            day = td.b.string
            forecast = u' '.join([self._text(line) for line in td.contents[2:]])
            forecasts.append(u'%s: %s' % (day, self._text(forecast)))

        return forecasts

    @match(r'^weather (?:(?:for|at|in) )?(.+)$')
    def weather(self, event, place):
        # The regex also matches "weather forecast..." which forecast should
        # process. So ignore it when this happens
        if place.lower().startswith('forecast'):
            return

        try:
            values = self.remote_weather(place)
            event.addresponse(u'In %(place)s at %(time)s: %(temp)s; Humidity: %(humidity)s; Wind: %(wind)s; Conditions: %(conditions)s; Sunrise/set: %(sunrise)s/%(sunset)s; Moonrise/set: %(moonrise)s/%(moonset)s', values)
        except Weather.TooManyPlacesException, e:
            event.addresponse(u'Too many places match %(place)s: %(exception)s', {
                'place': place,
                'exception': human_join(e.args[0], separator=u';'),
            })
        except Weather.WeatherException, e:
            event.addresponse(unicode(e))
Esempio n. 10
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)
Esempio n. 11
0
class Distance(Processor):
    usage = u"""distance [in <unit>] between <source> and <destination>
    place search for <placename>"""

    # For Mathematics, see:
    # http://www.mathforum.com/library/drmath/view/51711.html
    # http://mathworld.wolfram.com/GreatCircle.html

    feature = ('distance',)

    default_unit_names = {
            'km': "kilometres",
            'mi': "miles",
            'nm': "nautical miles"}
    default_radius_values = {
            'km': 6378,
            'mi': 3963.1,
            'nm': 3443.9}

    unit_names = DictOption('unit_names', 'Names of units in which to specify distances', default_unit_names)
    radius_values = DictOption('radius_values', 'Radius of the earth in the units in which to specify distances', default_radius_values)

    def get_place_data(self, place, num):
        return json_webservice('http://ws.geonames.org/searchJSON', {'q': place, 'maxRows': num, 'username': '******'})

    def get_place(self, place):
        js = self.get_place_data(place, 1)
        if js['totalResultsCount'] == 0:
            return None
        info = js['geonames'][0]
        return {'name': "%s, %s, %s" % (info['name'], info['adminName1'], info['countryName']),
                'lng': radians(info['lng']),
                'lat': radians(info['lat'])}

    @match(r'^(?:(?:search\s+for\s+place)|(?:place\s+search\s+for)|(?:places\s+for))\s+(\S.+?)\s*$')
    def placesearch(self, event, place):
        js = self.get_place_data(place, 10)
        if js['totalResultsCount'] == 0:
            event.addresponse(u"I don't know of anywhere even remotely like '%s'", place)
        else:
            event.addresponse(u"I can find: %s",
                    (human_join([u"%s, %s, %s" % (p['name'], p['adminName1'], p['countryName'])
                        for p in js['geonames'][:10]],
                        separator=u';')))

    @match(r'^(?:how\s*far|distance)(?:\s+in\s+(\S+))?\s+'
            r'(?:(between)|from)' # Between ... and ... | from ... to ...
            r'\s+(\S.+?)\s+(?(2)and|to)\s+(\S.+?)\s*$')
    def distance(self, event, unit, ignore, src, dst):
        unit_names = self.unit_names
        if unit and unit not in self.unit_names:
            event.addresponse(u"I don't know the unit '%(badunit)s'. I know about: %(knownunits)s", {
                'badunit': unit,
                'knownunits':
                    human_join(u"%s (%s)" % (unit, self.unit_names[unit])
                        for unit in self.unit_names),
            })
            return
        if unit:
            unit_names = [unit]

        srcp, dstp = self.get_place(src), self.get_place(dst)
        if not srcp or not dstp:
            event.addresponse(u"I don't know of anywhere called %s",
                    (u" or ".join("'%s'" % place[0]
                        for place in ((src, srcp), (dst, dstp)) if not place[1])))
            return

        dist = acos(cos(srcp['lng']) * cos(dstp['lng']) * cos(srcp['lat']) * cos(dstp['lat']) +
                    cos(srcp['lat']) * sin(srcp['lng']) * cos(dstp['lat']) * sin(dstp['lng']) +
                    sin(srcp['lat'])*sin(dstp['lat']))

        event.addresponse(u"Approximate distance, as the bot flies, between %(srcname)s and %(dstname)s is: %(distance)s", {
            'srcname': srcp['name'],
            'dstname': dstp['name'],
            'distance': human_join([
                u"%.02f %s" % (self.radius_values[unit]*dist, self.unit_names[unit])
                for unit in unit_names],
                conjunction=u'or'),
        })
Esempio n. 12
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