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']))
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())
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')
class Bazaar(Processor, RPC): usage = u"""(last commit|commit <revno>) [to <repo>] [full] repositories""" features = ('bzr', ) autoload = False repositories = DictOption('repositories', 'Dict of repositories names and URLs') interval = IntOption('interval', 'Interval inbetween checks for new revisions', 300) def __init__(self, name): self.log = logging.getLogger('plugins.bzr') Processor.__init__(self, name) RPC.__init__(self) def setup(self): self.branches = {} must_monitor = False for name, repository in self.repositories.items(): try: self.branches[name.lower()] = Branch.open(repository['url']) except NotBranchError: self.log.error(u'%s is not a branch', repository) continue if repository.get('poll', 'False').lower() in ('yes', 'true'): must_monitor = True self.check.im_func.disabled = not must_monitor if must_monitor: self.seen_revisions = {} @match(r'^(?:repos|repositories)$') def handle_repositories(self, event): repositories = self.branches.keys() if repositories: event.addresponse(u'I know about: %s', human_join(sorted(repositories))) else: event.addresponse(u"I don't know about any repositories") def remote_committed(self, repository, start, end=None): commits = self.get_commits(repository, start, end) repo = self.repositories[repository] for commit in commits: ibid.dispatcher.send({ 'reply': commit, 'source': repo['source'], 'target': repo['channel'], }) return True @match( r'^(?:last\s+)?commit(?:\s+(\d+))?(?:(?:\s+to)?\s+(\S+?))?(\s+full)?$') def commit(self, event, revno, repository, full): revno = revno and int(revno) or None commits = self.get_commits(repository, revno, full=full) output = u'' for commit in commits: if commit: output += commit.strip() event.addresponse(output, conflate=False) def get_commits(self, repository, start, end=None, full=None): branch = None if repository: repository = repository.lower() if repository not in self.branches: return None branch = self.branches[repository] if not branch: if len(self.branches) == 1: (repository, branch) = self.branches.items()[0] else: (repository, branch) = sorted(self.branches.iteritems(), reverse=True, key=lambda (k, v): v.repository.get_revision( v.last_revision_info()[1]).timestamp)[0] if not start: start = branch.revision_id_to_revno(branch.last_revision()) f = StringIO() log.show_log(branch, LogFormatter(f, repository, branch, full), start_revision=start, end_revision=end or start) f.seek(0) commits = f.readlines() commits.reverse() return commits @handler def launchpad(self, event): if ibid.sources[event.source].type != 'smtp' \ or 'X-Launchpad-Branch' not in event.headers: return event.processed = True if 'X-Launchpad-Branch' not in event.headers or 'X-Launchpad-Branch-Revision-Number' not in event.headers: return for name, repository in self.repositories.iteritems(): if (event.headers['X-Launchpad-Branch'] == repository.get( 'lp_branch', None)): self.remote_committed( name, int(event.headers['X-Launchpad-Branch-Revision-Number'])) @periodic(config_key='interval') def check(self, event): for name, repo in self.repositories.iteritems(): if repo.get('poll', 'False').lower() not in ('yes', 'true'): continue branch = self.branches[name] lastrev = branch.last_revision() if name not in self.seen_revisions: self.seen_revisions[name] = lastrev continue if lastrev == self.seen_revisions[name]: continue try: commits = self.get_commits(name, None, False) except RevisionNotPresent: self.log.debug( u"Got a RevisionNotPresent, hoping it won't be there next time..." ) continue self.seen_revisions[name] = lastrev if commits: event.addresponse(unicode(commits[0].strip()), source=repo['source'], target=repo['channel'], address=False)
class HTTP(Processor): usage = u"""(get|head) <url> is <domain> (up|down) tell me when <domain|url> is up""" features = ('http',) priority = -10 max_size = IntOption('max_size', u'Only request this many bytes', 2048) timeout = IntOption('timeout', u'Timeout for HTTP connections in seconds', 15) sites = DictOption('sites', u'Mapping of site names to domains', {}) redirect_limit = IntOption('redirect_limit', u'Maximum number of http redirects to follow', 5) whensitup_delay = IntOption('whensitup_delay', u'Initial delay between whensitup attempts in seconds', 60) whensitup_factor = FloatOption('whensitup_factor', u'Factor to mutliply subsequent delays by for whensitup', 1.03) whensitup_maxdelay = IntOption('whensitup_maxdelay', u'Maximum delay between whensitup attempts in seconds', 30*60) whensitup_maxperiod = FloatOption('whensitup_maxperiod', u'Maximum period after which to stop checking the url ' u'for whensitup in hours', 72) def _get_header(self, headers, name): for header in headers: if header[0] == name: return header[1] return None @match(r'^(get|head)\s+(\S+)$') def get(self, event, action, url): try: status, reason, data, headers = self._request(self._makeurl(url), action.upper()) reply = u'%s %s' % (status, reason) hops = 0 while 300 <= status < 400 and self._get_header(headers, 'location'): location = self._get_header(headers, 'location') if not location: break status, reason, data, headers = self._request(location, 'GET') if hops >= self.redirect_limit: reply += u' to %s' % location break hops += 1 reply += u' to %(location)s, which gets a ' \ u'%(status)d %(reason)s' % { u'location': location, u'status': status, u'reason': reason, } if action.upper() == 'GET': got_title = False content_type = self._get_header(headers, 'content-type') if content_type and (content_type.startswith('text/html') or content_type.startswith('application/xhtml+xml')): match = re.search(r'<title>(.*)<\/title>', data, re.I | re.DOTALL) if match: got_title = True reply += u' "%s"' % match.groups()[0].strip() if not got_title and content_type: reply += u' ' + content_type event.addresponse(reply) except HTTPException, e: event.addresponse(unicode(e))
class ICECast(Processor): usage = u"what's playing [on <stream>]?" feature = ('icecast', ) interval = IntOption('interval', 'Interval between checking for song changes', 60) streams = DictOption( 'streams', 'Dictionary of Stream names to base URL (include trailing /)', {}) last_checked = None last_songs = {} def scrape_status(self, stream): tree = get_html_parse_tree(self.streams[stream]['url'] + 'status.xsl', treetype='etree') main_table = tree.findall('.//table')[2] status = {} for row in main_table.findall('.//tr'): key, value = [x.text for x in row.findall('td')] status[key[:-1]] = value return status @match(r'^what(?:\'|\s+i)s\s+playing(?:\s+on\s+(.+))?$') def playing(self, event, stream): if not event.get('addressed', False): return if len(self.streams) == 0: event.addresponse(u"Sorry, I don't know about any streams") return elif stream is None and len(self.streams) == 1: stream = self.streams.keys()[0] elif stream is not None and stream not in self.streams: for name in self.streams.iterkeys(): if name.lower() == stream.lower(): stream = name break else: stream = None if stream is None: event.addresponse( u'Sorry, I only know about the following streams, ' u'please choose one: %s', human_join(self.streams.keys())) return try: status = self.scrape_status(stream) event.addresponse( u'Currently Playing on %(stream)s: ' u'%(song)s - %(description)s (Listeners: %(listeners)s)', { 'stream': stream, 'song': status['Current Song'], 'description': status['Stream Description'], 'listeners': status['Current Listeners'], }) except HTTPError: event.addresponse( u'The stream must be down, back to the MP3 collection for you') @periodic(config_key='interval') def check(self, event): for name, stream in self.streams.iteritems(): if 'source' in stream and 'channel' in stream: log.debug(u'Probing %s', name) status = self.scrape_status(name) if self.last_songs.get(name, '') != status['Current Song']: self.last_songs[name] = status['Current Song'] event.addresponse( u'Now Playing on %(stream)s: ' u'%(song)s - %(description)s ' u'(Listeners: %(listeners)s)', { 'stream': name, 'song': status['Current Song'], 'description': status['Stream Description'], 'listeners': status['Current Listeners'], }, source=stream['source'], target=stream['channel'], topic=(stream.get('topic', 'False').lower() in ('yes', 'true')), address=False, )
class 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)
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), })
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))
class DuelDraw(Processor): usage = u"""draw [my <weapon>] bam|pew|bang|kapow|pewpew|holyhandgrenadeofantioch""" feature = ('duel', ) # Parameters for Processor: event_types = (u'message', u'action') addressed = BoolOption('addressed', 'Must the bot be addressed?', True) # Game configurables: weapons = DictOption( 'weapons', 'Weapons that can be used: name: (chance, damage)', { u'bam': (0.75, 50), u'pew': (0.75, 50), u'fire': (0.75, 70), u'fires': (0.75, 70), u'bang': (0.75, 70), u'kapow': (0.75, 90), u'pewpew': (0.75, 110), u'holyhandgrenadeofantioch': (1.0, 200), }) extremities = ListOption('extremities', u'Extremities that can be hit', ( u'toe', u'foot', u'leg', u'thigh', u'finger', u'hand', u'arm', u'elbow', u'shoulder', u'ear', u'nose', u'stomach', )) vitals = ListOption('vitals', 'Vital parts of the body that can be hit', ( u'head', u'groin', u'chest', u'heart', u'neck', )) draw_required = BoolOption('draw_required', 'Must you draw your weapon before firing?', True) extratime = FloatOption( 'extratime', 'How much more time to grant after every shot fired?', 1.0) @match(r'^draws?(?:\s+h(?:is|er)\s+.*|\s+my\s+.*)?$') def draw(self, event): if (event.source, event.channel) not in duels: if event.get('addressed', False): event.addresponse( choice(( u"We do not permit drawn weapons here", u"You may only draw a weapon on the field of honour", ))) return duel = duels[(event.source, event.channel)] shooter = event.sender['nick'] if shooter.lower() not in duel.names: event.addresponse( choice(( u"Spectators are not permitted to draw weapons", u"Do you think you are %(fighter)s?", )), {'fighter': choice(duel.names.values())}) return if not duel.started: event.addresponse( choice(( u"Now now, not so fast!", u"Did I say go yet?", u"Put that AWAY!", ))) return duel.drawn[shooter.lower()] = True event.addresponse(True) def setup(self): self.fire.im_func.pattern = re.compile( r'^(%s)(?:[\s,.!:;].*)?$' % '|'.join(self.weapons.keys()), re.I | re.DOTALL) @handler def fire(self, event, weapon): shooter = event.sender['nick'].lower() if (event.source, event.channel) not in duels: return duel = duels[(event.source, event.channel)] if shooter not in duel.names: event.addresponse( choice(( u"You aren't in a war", u'You are a non-combatant', u'You are a spectator', ))) return enemy = set(duel.names.keys()) enemy.remove(shooter) enemy = enemy.pop() if self.draw_required and not duel.drawn[shooter]: recipient = shooter else: recipient = enemy if not duel.started or not duel.confirmed: if self.draw_required: message = choice(( u"%(shooter)s tried to escape his duel by shooting himself in the foot. The duel has been cancelled, but his honour is forfeit", u"%(shooter)s shot himself while preparing for his duel. The funeral will be held on the weekend", )) elif not duel.started: message = choice(( u"FOUL! %(shooter)s fired before my mark. Just as well you didn't hit anything. I refuse to referee under these conditions", u"FOUL! %(shooter)s injures %(enemy)s before the match started and is marched away in handcuffs", u"FOUL! %(shooter)s killed %(enemy)s before the match started and was shot by the referee before he could hurt anyone else", )) else: message = choice(( u"FOUL! The duel is not yet confirmed. %(shooter)s is marched away in handcuffs", u"FOUL! Arrest %(shooter)s! Firing a weapon within city limits is not permitted", )) event.addresponse(message, { 'shooter': duel.names[shooter], 'enemy': duel.names[enemy], }, address=False) del duels[(event.source, event.channel)] duel.stop() return chance, power = self.weapons[weapon.lower()] if random() < chance: damage = max(gauss(power, power / 2.0), 0) duel.hp[recipient] -= damage if duel.hp[recipient] <= 0.0: del duels[(event.source, event.channel)] duel.stop() else: duel.timeout_callback.delay(self.extratime) params = { 'shooter': duel.names[shooter], 'enemy': duel.names[enemy], 'part': u'foot', } if shooter == recipient: message = u"TRAGEDY: %(shooter)s shoots before drawing his weapon. " if damage > 100.0: message += choice(( u"The explosion killed him", u"There was little left of him", )) elif duel.hp[recipient] <= 0.0: message += choice(( u"Combined with his other injuries, he didn't stand a chance", u"He died during field surgery", )) else: message += choice(( u"Luckily, it was only a flesh wound", u"He narrowly missed his femoral artery", )) elif damage > 100.0: message = u'VICTORY: ' + choice(( u'%(shooter)s blows %(enemy)s away', u'%(shooter)s destroys %(enemy)s', )) elif duel.hp[enemy] <= 0.0: message = u'VICTORY: ' + choice(( u'%(shooter)s kills %(enemy)s with a shot to the %(part)s', u'%(shooter)s shoots %(enemy)s killing him with a fatal shot to the %(part)s', )) params['part'] = choice(self.vitals) else: message = choice(( u'%(shooter)s hits %(enemy)s in the %(part)s, wounding him', u'%(shooter)s shoots %(enemy)s in the %(part)s, but %(enemy)s can still fight', )) params['part'] = choice(self.extremities) event.addresponse(message, params, address=False) elif shooter == recipient: event.addresponse(choice(( u"%s forget to draw his weapon. Luckily he missed his foot", u"%s fires a holstered weapon. Luckily it only put a hole in his jacket", u"%s won't win at this rate. He forgot to draw before firing. He missed himself too", )), duel.names[shooter], address=False) else: event.addresponse(choice((u'%s misses', u'%s aims wide', u'%s is useless with a weapon')), duel.names[shooter], address=False)
class 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'), })
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