#!/usr/bin/env python # Location Bot # # Douglas Thrift # # locationbot.py import apiclient.discovery from ConfigParser import NoOptionError, SafeConfigParser from datetime import datetime, timedelta import functools import getpass import inspect import ircbot import irclib import oauth2client.client import oauth2client.tools import os import psycopg2 import psycopg2.extensions import pytz import re import socket import sys import time import threading import traceback import urllib, urllib2 import urlparse import warnings import math try: import simplejson as json except ImportError: import json def shortcutting_or(function, values): if values: for value in values: if function(value): return True return False def address_mask(function): @functools.wraps(function) def address_mask(nick, mask): if not function(nick, mask): nick, address = nick.split('@', 1) try: host = socket.gethostbyaddr(address)[0] except socket.herror: traceback.print_exc() else: if host != address: return function(nick + '@' + host, mask) return False return True return address_mask def encode(function): @functools.wraps(function) def encode(self, target, text): return function(self, target, text.encode('utf8')) return encode irclib.mask_matches = address_mask(reload(irclib).mask_matches) irclib.ServerConnection.notice = encode(irclib.ServerConnection.notice) irclib.ServerConnection.privmsg = encode(irclib.ServerConnection.privmsg) irclib.ServerConnection.privmsg_many = encode(irclib.ServerConnection.privmsg_many) class ThreadTimerServiceMixin(object): def start(self): self.thread.daemon = True self.timer = None self.timer_lock = threading.Lock() self.thread.start() def stop(self): self.thread.join() with self.timer_lock: if self.timer is not None: self.timer.cancel() self.timer.join() class Geocoder(ThreadTimerServiceMixin): CACHE_DURATION = timedelta(hours = 1) def __init__(self, cache = None): if cache is None: self.cache = {} else: self.cache = cache self.cache_lock = threading.Lock() self.thread = threading.Thread(target = self.cache_clean) def geocode(self, sensor, coordinates = None, location = None): parameters = {'sensor': 'true' if sensor else 'false'} with self.cache_lock: if coordinates is not None: try: return self.cache[coordinates][0] except KeyError: parameters['latlng'] = '%f,%f' % coordinates else: try: return self.cache[coordinates][0] except KeyError: parameters['address'] = location geocode = json.load(urllib2.urlopen('http://maps.google.com/maps/api/geocode/json?' + urllib.urlencode(parameters), timeout = 5)) status = geocode['status'] if status != 'OK': if coordinates is not None and status == 'ZERO_RESULTS': return self.geocode(sensor, location = parameters['latlng']) else: raise Exception(status) results = geocode['results'] def _result(): _location = result['geometry']['location'] geocode = (_location['lat'], _location['lng']), result['formatted_address'] with self.__geocode_cache_lock: self.__geocode_cache[coordinates if coordinates is not None else location] = (geocode, datetime.utcnow()) return geocode types = frozenset([ 'country', 'administrative_area_level_1', 'administrative_area_level_2', 'administrative_area_level_3', 'colloquial_area', 'locality', 'sublocality', 'neighborhood', ]) for result in results: if not types.isdisjoint(result['types']): return _result() result = results[0] return _result() def cache_clean(self): now = datetime.utcnow() try: while now < self.cache_clean_next: time.sleep((self.cache_clean_next - now).seconds) now = datetime.utcnow() except AttributeError: pass self.cache_clean_next = now.replace(minute = 2, second = 30, microsecond = 0) + self.CACHE_DURATION with self.cache_lock: for location, (geocode, created) in self.cache.items(): if now - created >= self.CACHE_DURATION: del self.cache[location] with self.timer_lock: self.timer = threading.Timer((self.cache_clean_next - datetime.utcnow()).seconds, self.cache_clean) self.timer.start() class Location(object): VALUES = ('nick', 'granularity', 'location', 'coordinates', 'accuracy', 'speed', 'heading', 'altitude', 'altitude_accuracy', 'updated') def __init__(self, **values): if 'row' in values: self.values = dict(zip(VALUES, values[row])) else: self.values = values @property def url(self): if self.granularity == 'best': location = '%f,%f' % self.values['coordinates'] else: location = self.values['location'] return 'http://maps.google.com/maps?' + re.sub('%(2[89cC])', lambda match: chr(int(match.group(1), 16)), urllib.urlencode({'q': '%s (%s)' % (location, self.nick)})) @property def _accuracy(self): return self.values.get('accuracy') @property def _latitude(self): return self.values['coordinates'][0] @property def _longitude(self): return self.values['coordinates'][1] def has_changed_from(self, previous): if self.location != previous.location: return True distance = self - previous if distance < 60: return False if self._accuracy is not None and previous._accuracy is not None: if distance > (self._accuracy + previous._accuracy) / 2: return True if distance > min(self._accuracy, previous._accuracy) and self._accuracy < previous._accuracy: return True return False def __get_attr__(self, name): if name not in VALUES: raise AttributeError("'Location' object has no attribute '%s'" % name) value = self.values.get(name) if info in ('accuracy', 'altitude', 'altitude_accuracy'): return self.distance(value) elif info in ('speed',): return self.speed(value) elif info in ('heading',): return self.heading(value) else: return unicode(value) def __unicode__(self): try: venue = ' at %s' % self.values['venue'] except KeyError: venue = '' aux = [] for name in ('accuracy', 'speed', 'heading', 'altitude', 'altitude_accuracy'): if name in self.values: aux.append('%s: %s' % (name.replace('_', ' '), getattr(self, name))) if aux: aux = ' [%s]' % ', '.join(aux) else: aux = '' return '%s is%s in %s%s %s' % (self.nick, venue, self.location, aux, self.url) def __sub__(self, other): x = (math.cos(self.degrees_to_radians(self._latitude)) + math.cos(self.degrees_to_radians(other._latitude))) * self.longitude_to_nautical_miles(self._longitude - other._longitude) / 2 y = self.latitude_to_nautical_miles(self._latitude - other._latitude) return self.nautical_miles_to_meters(abs(x) ** 2 + abs(y) ** 2) @classmethod def distance(cls, distance): if distance is not None: return '%.1f m (%.1f ft)' % (distance, cls.meters_to_feet(distance)) @classmethod def speed(cls, speed): if speed is not None: return '%.1f m/s (%.1f mph)' % (speed, cls.meters_per_second_to_miles_per_hour(speed)) @classmethod def heading(cls, heading): if heading is not None: return u'%.1f\xb0 (%s)' % (heading, cls.heading_to_direction(heading)) @staticmethod def degrees_to_radians(degrees): return degrees * math.pi / 180 @staticmethod def latitude_to_nautical_miles(latitude): return latitude * 60.00721 @staticmethod def longitude_to_nautical_miles(longitude): return longitude * 60.10793 @staticmethod def nautical_miles_to_meters(nautical_miles): return nauticla_miles * 1852 @staticmethod def meters_to_feet(meters): return meters * 3.2808399 @staticmethod def meters_per_second_to_miles_per_hour(meters_per_second): return meters_per_second * 2.23693629 @staticmethod def heading_to_direction(heading): heading %= 360 if 348.75 < heading or heading <= 11.25: return 'N' elif 11.25 < heading <= 33.75: return 'NNE' elif 33.75 < heading <= 56.25: return 'NE' elif 56.25 < heading <= 78.75: return 'ENE' elif 78.75 < heading <= 101.25: return 'E' elif 101.25 < heading <= 123.75: return 'ESE' elif 123.75 < heading <= 146.25: return 'SE' elif 146.25 < heading <= 168.75: return 'SSE' elif 168.75 < heading <= 191.25: return 'S' elif 191.25 < heading <= 213.75: return 'SSW' elif 213.75 < heading <= 236.25: return 'SW' elif 236.25 < heading <= 258.75: return 'WSW' elif 258.75 < heading <= 281.25: return 'W' elif 281.25 < heading <= 303.75: return 'WNW' elif 303.75 < heading <= 326.25: return 'NW' else: return 'NNW' LOGIN = 0b01 LOGOUT = 0b10 ALWAYS = 0b11 CHANNEL = 0b01 PRIVATE = 0b10 BOTH = 0b11 class Command(object): def __init__(self, admin, state, access, arguments, description, function): self.admin = admin self.state = state self.access = access self.arguments = arguments self.description = description self.function = function @property def is_admin(self): return self.admin @property def is_login(self): return bool(self.state & LOGIN) @property def is_logout(self): return bool(self.state & LOGOUT) @property def is_channel(self): return bool(self.access & CHANNEL) @property def is_private(self): return bool(self.access & PRIVATE) def __call__(self, *args, **kwargs): self.function(*args, **kwargs) def command(admin, state, access, arguments, description): def command(function): cls = inspect.getouterframes(inspect.currentframe())[1][0].f_locals cls.setdefault('commands', {})[function.func_name] = Command(admin, state, access, arguments, description, function) return function return command class LocationBot(ircbot.SingleServerIRCBot): def __init__(self, bot = None): self.__config = SafeConfigParser() self.__config.read('locationbot.ini') try: nick = self.__config.sections()[0] except IndexError: sys.exit('No nick configured') servers = [] try: for server in self.__config.get(nick, 'servers').split(): server = server.split(':', 2) if len(server) == 1: servers.append((server[0], 6667)) else: host = server[0] port = int(server[1]) ssl = server[1].startswith('+') if len(server) == 3: servers.append((host, port, ssl, server[2])) else: servers.append((host, port, ssl)) self.__admins = self.__config.get(nick, 'admins').split() self.__channels = set(self.__config.get(nick, 'channels').split()) self.__dsn = self.__config.get(nick, 'dsn') except NoOptionError, error: sys.exit(error) try: self.__hostname = self.__config.get(nick, 'hostname') except NoOptionError: self.__hostname = '' ircbot.SingleServerIRCBot.__init__(self, servers, nick, 'Location Bot') if bot is None: self.geocoder = Geocoder() self.__locations = [] self.__logins = {} self.__nick = None self.__reloading = False else: self.geocoder = Geocoder(bot.geocoder.cache) irclibobj = self.ircobj.connections[0].irclibobj self.ircobj.connections[0] = bot.ircobj.connections[0] self.ircobj.connections[0].irclibobj = irclibobj self.channels = bot.channels self.connection = bot.connection self.__locations = bot.__locations self.__logins = bot.__logins self.__nick = bot.__nick self.__reloading = True self.__latitude_granularities = frozenset(['city', 'best']) self.__latitude_timer_lock = threading.Lock() self.__latitude_timer = None self.__locations_lock = threading.Lock() self.__quiting = False self.__timeout = 5 self.__variables = frozenset(['nick', 'secret', 'masks', 'channels', 'timezone', 'location', 'coordinates', 'latitude']) self.__geocode_variables = self.__variables.intersection(['location', 'coordinates']) self.__lists = self.__variables.intersection(['masks', 'channels']) self.__unsetable = self.__variables.difference(['nick', 'secret']) def __admin(self, nickmask): return shortcutting_or(lambda admin: irclib.mask_matches(nickmask, admin), self.__admins) def __channel(self, nick, exclude = None): if exclude is not None: exclude = irclib.irc_lower(exclude) channels = map(lambda channel: channel[1], filter(lambda channel: irclib.irc_lower(channel[0]) == exclude, self.channels)) else: channels = self.channels.values() return shortcutting_or(lambda channel: channel.has_user(nick), channels) def __db(self): db = psycopg2.connect(self.__dsn) def point(value, cursor): if value is not None: return tuple(map(lambda a: float(a), re.match(r'^\(([^)]+),([^)]+)\)$', value).groups())) psycopg2.extensions.register_type(psycopg2.extensions.new_type((600,), 'point', point), db) return db, db.cursor() @command(False, ALWAYS, PRIVATE, '[command]', 'show this help message') def help(self, connection, nick, **kwargs): arguments = kwargs.get('arguments') command = irclib.irc_lower(arguments.split(None, 1)[0].lstrip('!')) if arguments else None connection.privmsg(nick, '\x02command arguments description\x0f') def help(name, command): connection.privmsg(nick, '%-11s %-23s %s' % (name, command.arguments, command.description)) if command in self.commands: help(command, self.commands[command]) else: for name, command in sorted(self.commands.iteritems()): help(name, command) @command(False, ALWAYS, BOTH, '[nick]', 'show where everybody or a nick is') def status(self, connection, nick, **kwargs): pass @command(False, LOGOUT, PRIVATE, '[nick] [secret]', 'log in as nick with secret or using masks') def login(self, connection, nick, **kwargs): pass @command(False, LOGOUT, PRIVATE, '[nick] [secret', 'register as nick with secret') def register(self, connection, nick, **kwargs): pass @command(False, LOGIN, PRIVATE, '', 'log out as nick') def logout(self, connection, nick, **kwargs): pass @command(False, LOGIN, PRIVATE, '[variable [value]]', 'display or set variables') def set(self, connection, nick, **kwargs): pass @command(False, LOGIN, PRIVATE, 'variable', 'unset a variable') def unset(self, connection, nick, **kwargs): pass @command(True, ALWAYS, PRIVATE, 'channel', 'join a channel') def join(self, connection, nick, **kwargs): pass @command(True, ALWAYS, PRIVATE, 'channel [message]', 'part from a channel') def part(self, connection, nick, **kwargs): pass @command(True, ALWAYS, PRIVATE, '[message]', 'quit and do not come back') def quit(self, connection, nick, **kwargs): pass @command(True, ALWAYS, PRIVATE, '', 'reload with more up to date code') def reload(self, connection, nick, **kwargs): pass @command(True, ALWAYS, PRIVATE, '', 'quit and join running more up to date code') def restart(self, connection, nick, **kwargs): pass @command(True, ALWAYS, PRIVATE, 'nick|channel message', 'say message to nick or channel') def say(self, connection, nick, **kwargs): pass @command(True, ALWAYS, PRIVATE, '', 'show who is logged in') def who(self, connection, nick, **kwargs): pass def __help(self, connection, nick, admin, login, arguments): self.help(connection, nick, admin = admin, login = login, arguments = arguments) def __join(self, connection, nick, arguments): try: channel = arguments.split(None, 1)[0] except IndexError: return self.__help(connection, nick, True, False, 'join') connection.join(channel) self.__channels.add(channel) self.__config.set(self._nickname, 'channels', ' '.join(self.__channels)) self.__write() connection.privmsg(nick, 'successfully joined channel ("%s")' % channel) def __latitude(self, granularity = None, token = None, secret = None): if granularity is not None: #response, content = oauth.Client(self.__latitude_consumer, oauth.Token(token, secret), timeout = self.__timeout).request('https://www.googleapis.com/latitude/v1/currentLocation?' + urllib.urlencode({'granularity': granularity}), 'GET') if int(response['status']) != 200: raise Exception(content.strip()) data = json.loads(content)['data'] coordinates = (data['latitude'], data['longitude']) return datetime.fromtimestamp(int(data['timestampMs']) / 1e3, pytz.utc), coordinates, data.get('accuracy'), data.get('speed'), data.get('heading'), data.get('altitude'), data.get('altitudeAccuracy'), self.geocoder.geocode(False, coordinates = coordinates)[1] now = datetime.utcnow() try: while now < self.__latitude_next: time.sleep((self.__latitude_next - now).seconds) now = datetime.utcnow() except AttributeError: pass self.__latitude_next = now.replace(minute = now.minute - now.minute % 5, second = 0, microsecond = 0) + timedelta(minutes = 5) try: db, cursor = self.__db() cursor.execute('select nick, channels, latitude.granularity, location, coordinates, accuracy, speed, heading, altitude, altitude_accuracy, updated, token, latitude.secret from locationbot.nick join locationbot.latitude using (id)') for nick, channels, granularity, old_location, old_coordinates, old_accuracy, old_speed, old_heading, old_altitude, old_altitude_accuracy, old_updated, token, secret in cursor.fetchall(): try: updated, coordinates, accuracy, speed, heading, altitude, altitude_accuracy, location = self.__latitude(granularity, token, secret) except KeyError, error: print nick, error continue except Exception, error: traceback.print_exc() continue cursor.execute('update locationbot.nick set granularity = %s, location = %s, coordinates = point %s, accuracy = %s, speed = %s, heading = %s, altitude = %s, altitude_accuracy = %s, updated = %s where nick = %s', (granularity, location, coordinates, accuracy, speed, heading, altitude, altitude_accuracy, updated, nick)) db.commit() self.__location(nick, channels, granularity, old_location, location, old_coordinates, coordinates, old_accuracy, accuracy, old_speed, speed, old_heading, heading, old_altitude, altitude, old_altitude_accuracy, altitude_accuracy, old_updated, updated) except psycopg2.Error, error: traceback.print_exc() with self.__latitude_timer_lock: self.__latitude_timer = threading.Timer((self.__latitude_next - datetime.utcnow()).seconds, self.__latitude) self.__latitude_timer.start() def __login(self, connection, nickmask, nick, arguments = ''): login = nick if connection is not None: arguments = arguments.split(None, 1) if len(arguments) == 2: login, secret = arguments elif len(arguments) == 1: secret = arguments[0] else: secret = None else: secret = None if nick in self.__logins: login = self.__logins[nick][0] if connection is not None: return connection.privmsg(nick, 'already logged in as "%s"' % login) return login db, cursor = self.__db() def success(): connection.privmsg(nick, 'successfully logged in as "%s"' % login) if secret is not None: cursor.execute('select true from locationbot.nick where nick = %s and secret = md5(%s)', (login, secret)) if cursor.rowcount == 1: self.__logins[nick] = (login, nickmask) return success() cursor.execute('select nick, masks from locationbot.nick where nick in (%s, %s)', (login, secret if len(arguments) != 2 else None)) for login, masks in cursor.fetchall(): if shortcutting_or(lambda mask: irclib.mask_matches(nickmask, mask), masks): self.__logins[nick] = (login, nickmask) return success() if connection else login if connection is not None: return connection.privmsg(nick, 'failed to log in as "%s"' % login) def __logout(self, connection, nick): connection.privmsg(nick, 'logged out as "%s"' % self.__logins.pop(nick)[0]) def __part(self, connection, nick, arguments): arguments = arguments.split(None, 1) if len(arguments) == 2: channel, message = arguments message = ':' + message elif len(arguments) == 1: channel = arguments[0] message = '' else: return self.__help(connection, nick, True, False, 'part') if channel in self.__channels: connection.part(channel, message) self.__channels.remove(channel) self.__config.set(self._nickname, 'channels', ' '.join(self.__channels)) self.__write() connection.privmsg(nick, 'successfully parted channel ("%s")' % channel) else: connection.privmsg(nick, 'not in channel ("%s")' % channel) def __quit(self, connection, nick, arguments): self.__reloading = True self.__quiting = True connection.privmsg(nick, 'quiting') self.disconnect(arguments) def __register(self, connection, nick, arguments): arguments = arguments.split(None, 1) if len(arguments) == 2: login, secret = arguments elif len(arguments) == 1: login = nick secret = arguments[0] else: return self.__help(connection, nick, False, False, 'register') db, cursor = self.__db() try: cursor.execute('insert into locationbot.nick (nick, secret) values (%s, md5(%s))', (login, secret)) db.commit() except psycopg2.IntegrityError: return connection.privmsg(nick, 'nick ("%s") is already registered' % login) connection.privmsg(nick, 'nick ("%s") sucessfully registered' % login) def __reload(self, connection, nick): self.__nick = nick self.__reloading = True connection.privmsg(nick, 'reloading') def __restart(self, connection): connection.disconnect('restarting') os.execvp(sys.argv[0], sys.argv) def __say(self, connection, nick, arguments): try: nick_channel, message = arguments.split(None, 1) except ValueError: return self.__help(connection, nick, True, False, 'say') if irclib.is_channel(nick_channel): if nick_channel not in self.channels: return connection.privmsg(nick, 'not in channel ("%s")' % nick_channel) elif not self.__channel(nick_channel): return connection.privmsg(nick, 'nick ("%s") not in channel(s)' % nick_channel) elif nick_channel == connection.get_nickname(): return connection.privmsg(nick, 'nice try') connection.privmsg(nick_channel, message) connection.privmsg(nick, 'successfully sent message ("%s") to nick/channel ("%s")' % (message, nick_channel)) def __set(self, connection, nickmask, nick, login, arguments): arguments = arguments.split(None, 1) if len(arguments) == 2: variable, value = arguments elif len(arguments) == 1: variable = arguments[0] value = None else: variable = None value = None if variable is not None and variable not in self.__variables: return self.__unknown_variable(connection, nick, variable) db, cursor = self.__db() if value is None: variables = sorted(self.__variables) if variable is None else [variable] cursor.execute('select ' + ', '.join(map(lambda variable: "'%s'" % ('*' * 8) if variable == 'secret' else 'latitude.granularity, latitude.authorized' if variable == 'latitude' else variable, variables)) + ' from locationbot.nick left join locationbot.latitude using (id) where nick = %s', (login,)) values = list(cursor.fetchone()) try: index = variables.index('latitude') values[index:index + 2] = ['%s (%s)' % (values[index], 'authorized' if values[index + 1] else 'unauthorized')] except ValueError: pass connection.privmsg(nick, '\x02variable value\x0f') for variable, value in zip(variables, values): connection.privmsg(nick, '%-11s %s' % (variable, ' '.join(value) if isinstance(value, list) else '%f,%f' % value if isinstance(value, tuple) else value)) else: def invalid(value, variable = variable): connection.privmsg(nick, 'invalid %s ("%s")' % (variable, value)) if variable in self.__lists: value = value.split() if variable == 'channels': for channel in value: if not irclib.is_channel(channel) or channel not in self.__channels: return invalid(channel, 'channel') elif variable == 'masks': _mask = re.compile('^.+!.+@.+$') for mask in value: if not _mask.match(mask): return invalid(mask, 'mask') elif variable == 'latitude': if value in self.__latitude_granularities: #response, content = oauth.Client(self.__latitude_consumer, timeout = self.__timeout).request('https://www.google.com/accounts/OAuthGetRequestToken', 'POST', urllib.urlencode({ # 'scope': 'https://www.googleapis.com/auth/latitude', # 'oauth_callback': 'oob', #})) if int(response['status']) != 200: raise Exception(content.strip()) authorized = False else: cursor.execute('select channels, location, latitude.granularity, token, latitude.secret from locationbot.nick join locationbot.latitude using (id) where nick = %s and authorized = false', (login,)) if cursor.rowcount == 0: return invalid(value) channels, old_location, granularity, token, secret = cursor.fetchone() #token = oauth.Token(token, secret) token.set_verifier(value) #response, content = oauth.Client(self.__latitude_consumer, token, timeout = self.__timeout).request('https://www.google.com/accounts/OAuthGetAccessToken', 'GET') status = int(response['status']) if status == 400: return invalid(value) elif status != 200: raise Exception(content.strip()) authorized = True data = dict(urlparse.parse_qsl(content)) token = data['oauth_token'] secret = data['oauth_token_secret'] if not authorized: connection.privmsg(nick, 'go to https://www.google.com/latitude/apps/OAuthAuthorizeToken?' + urllib.urlencode({ 'domain': self.__latitude_client_id, 'granularity': value, 'oauth_token': token, })) elif variable in self.__geocode_variables: cursor.execute('select channels, location, latitude.granularity, token, latitude.secret, authorized from locationbot.nick left join locationbot.latitude using (id) where nick = %s', (login,)) channels, old_location, granularity, token, secret, authorized = cursor.fetchone() if variable == 'location': coordinates = None granularity = 'city' location = value else: coordinates = value.split(None, 1) if len(coordinates) == 1: coordinates = coordinates[0].split(',', 1) try: coordinates = tuple(map(lambda a: float(a), coordinates)) except ValueError: return invalid(value) for coordinate in coordinates: if not -180.0 <= coordinate <= 180.0: return invalid(value) location = None geocode = self.geocoder.geocode(False, coordinates, location) new_location = geocode[1] if variable == 'location': coordinates = geocode[0] value = new_location else: value = coordinates if authorized: #response, content = oauth.Client(self.__latitude_consumer, oauth.Token(token, secret), timeout = self.__timeout).request('https://www.googleapis.com/latitude/v1/currentLocation?' + urllib.urlencode({'granularity': granularity}), 'POST', json.dumps({'data': { # 'kind': 'latitude#location', # 'latitude': coordinates[0], # 'longitude': coordinates[1], #}}), {'Content-Type': 'application/json'}) if int(response['status']) != 200: raise Exception(content.strip()) accuracy = speed = heading = altitude = altitude_accuracy = None elif variable == 'nick': _nick = value.split(None, 1) if len(_nick) != 1: return invalid(value) elif variable == 'timezone': if value not in pytz.all_timezones_set: return invalid(value) try: if variable in self.__geocode_variables: cursor.execute('update locationbot.nick set granularity = %s, location = %s, coordinates = point %s, accuracy = %s, speed = %s, heading = %s, altitude = %s, altitude_accuracy = %s, updated = now() where nick = %s', (granularity, new_location, coordinates, accuracy, speed, heading, altitude, altitude_accuracy, login)) elif variable == 'latitude': if authorized: cursor.execute('update locationbot.latitude set token = %s, secret = %s, authorized = %s from locationbot.nick where latitude.id = nick.id and nick = %s', (token, secret, authorized, login)) else: cursor.execute('delete from locationbot.latitude using locationbot.nick where latitude.id = nick.id and nick = %s', (login,)) cursor.execute('insert into locationbot.latitude (id, granularity, token, secret, authorized) select id, %s, %s, %s, %s from locationbot.nick where nick = %s', (value, token, secret, authorized, login)) else: cursor.execute('update locationbot.nick set ' + variable + ' = ' + ('md5(%s)' if variable == 'secret' else '%s') + ' where nick = %s', (value, login)) db.commit() except psycopg2.IntegrityError: if variable == 'nick': return connection.privmsg(nick, 'nick ("%s") is already registered' % value) raise connection.privmsg(nick, 'variable ("%s") successfully set to value ("%s")' % (variable, ' '.join(value) if isinstance(value, list) else '%f,%f' % value if isinstance(value, tuple) else value)) if variable == 'nick': self.__logins[nick] = (value, nickmask) elif variable in self.__geocode_variables or variable == 'latitude' and authorized: if variable == 'latitude': updated, coordinates, accuracy, speed, heading, altitude, altitude_accuracy, new_location = self.__latitude(granularity, token, secret) cursor.execute('update locationbot.nick set granularity = %s, location = %s, coordinates = point %s, accuracy = %s, speed = %s, heading = %s, altitude = %s, altitude_accuracy = %s, updated = %s where nick = %s', (granularity, new_location, coordinates, accuracy, speed, heading, altitude, altitude_accuracy, updated, login)) self.__location(login, channels, granularity, old_location, new_location, None, coordinates, None, accuracy, None, speed, None, heading, None, altitude, None, altitude_accuracy, None, updated) def __status(self, connection, nick, login, arguments): _nick = arguments.split(None, 1)[0] if arguments else None db, cursor = self.__db() cursor.execute('select nick, granularity, location, coordinates, accuracy, speed, heading, altitude, altitude_accuracy, updated from locationbot.nick where ' + ('nick = %s and ' if _nick is not None else '') + 'location is not null order by updated desc', (_nick,)) if cursor.rowcount == 0: return connection.privmsg(nick, 'no location information for ' + ('"%s"' % _nick if _nick is not None else 'anybody')) locations = cursor.fetchall() if login is not None: cursor.execute('select timezone from locationbot.nick where nick = %s and timezone is not null', (login,)) timezone = pytz.timezone(cursor.fetchone()[0]) if cursor.rowcount == 1 else pytz.utc else: timezone = pytz.utc connection.privmsg(nick, '\x02%-24s%-36s%-24s%-24s%-16s%-24s%-24s%-24s%s\x0f' % ('nick', 'location', 'accuracy', 'speed', 'heading', 'altitude', 'altitude accuracy', 'when', 'map')) for _nick, granularity, location, coordinates, accuracy, speed, heading, altitude, altitude_accuracy, updated in locations: connection.privmsg(nick, '%-23s %-35s %-23s %-23s %-15s %-23s %-23s %-23s %s' % (_nick, location, self.__distance(accuracy), self.__speed(speed), self.__heading(heading), self.__distance(altitude), self.__distance(altitude_accuracy), timezone.normalize(updated.astimezone(timezone)).strftime('%Y-%m-%d %H:%M %Z'), self.__url(_nick, granularity, location, coordinates))) def __unknown(self, connection, nick, command): connection.privmsg(nick, 'unknown command ("%s"); try "help"' % command) def __unknown_variable(self, connection, nick, variable): connection.privmsg(nick, 'unknown variable ("%s")' % variable) def __unset(self, connection, nick, login, arguments): try: variable = irclib.irc_lower(arguments.split(None, 1)[0]) except IndexError: return self.__help(connection, nick, False, login, 'unset') if variable not in self.__unsetable: if variable in self.__variables: return connection.privmsg(nick, 'variable ("%s") is not unsetable' % variable) return self.__unknown_variable(connection, nick, variable) db, cursor = self.__db() cursor.execute('update locationbot.nick set ' + ('location = null, coordinates = null, updated = null' if variable in self.__geocode_variables else variable + ' = null') + ' where nick = %s and ' + variable + ' is not null', (login,)) db.commit() connection.privmsg(nick, 'variable ("%s") %s unset' % (variable, 'successfuly' if cursor.rowcount == 1 else 'already')) def __url(self, nick, granularity, location, coordinates): if granularity == 'best': location = '%f,%f' % coordinates return 'http://maps.google.com/maps?' + re.sub('%(2[cC])', lambda match: chr(int(match.group(1), 16)), urllib.urlencode({'q': '%s (%s)' % (location, nick)})) def __who(self, connection, nick): if self.__logins: connection.privmsg(nick, '\x02login nick nick mask\x0f') for login in sorted(self.__logins.values()): connection.privmsg(nick, '%-23s %s' % login) else: connection.privmsg(nick, 'nobody logged in') def __write(self): with open('locationbot.ini', 'w') as config: self.__config.write(config) def _connect(self): if len(self.server_list[0]) != 2: ssl = self.server_list[0][2] else: ssl = False if len(self.server_list[0]) == 4: password = self.server_list[0][3] else: password = None try: with warnings.catch_warnings(): warnings.filterwarnings('ignore', r'socket\.ssl\(\) is deprecated\. Use ssl\.wrap_socket\(\) instead\.', DeprecationWarning) self.connect(self.server_list[0][0], self.server_list[0][1], self._nickname, password, getpass.getuser(), self._realname, localaddress = self.__hostname, ssl = ssl) except irclib.ServerConnectionError: pass def disconnect(self, message = 'oh no!'): ircbot.SingleServerIRCBot.disconnect(self, message) def error(self, error): traceback.print_exc() self.connection.privmsg(self.__nick, 'an error occured') def get_version(self): return 'locationbot ' + sys.platform def on_kick(self, connection, event): nick = event.arguments()[0] if not self.__channel(nick, event.target()): self.__logins.pop(nick, None) def on_nick(self, connection, event): nickmask = event.source() login = self.__logins.pop(irclib.nm_to_n(nickmask), (None,))[0] if login is not None: nick = event.target() self.__logins[nick] = (login, nick + '!' + irclib.nm_to_uh(nickmask)) def on_nicknameinuse(self, connection, event): connection.nick(connection.get_nickname() + '_') def on_part(self, connection, event): nick = irclib.nm_to_n(event.source()) if not self.__channel(nick, event.target()): self.__logins.pop(nick, None) def on_privmsg(self, connection, event): nickmask = event.source() nick = irclib.nm_to_n(nickmask) admin = self.__admin(nickmask) if not admin and not self.__channel(nick): return try: login = self.__login(None, nickmask, nick) try: command, arguments = event.arguments()[0].split(None, 1) except ValueError: command = event.arguments()[0].strip() arguments = '' command = irclib.irc_lower(command.lstrip('!')) if command == 'help': self.__help(connection, nick, admin, login, arguments) elif command == 'login': self.__login(connection, nickmask, nick, arguments) elif command == 'status': self.__status(connection, nick, login, arguments) elif not login and command == 'register': self.__register(connection, nick, arguments) elif login and command == 'logout': self.__logout(connection, nick) elif login and command == 'set': self.__set(connection, nickmask, nick, login, arguments) elif login and command == 'unset': self.__unset(connection, nick, login, arguments) elif admin and command == 'join': self.__join(connection, nick, arguments) elif admin and command == 'part': self.__part(connection, nick, arguments) elif admin and command == 'quit': self.__quit(connection, nick, arguments) elif admin and command == 'reload': self.__reload(connection, nick) elif admin and command == 'restart': self.__restart(connection) elif admin and command == 'say': self.__say(connection, nick, arguments) elif admin and command == 'who': self.__who(connection, nick) else: self.__unknown(connection, nick, command) except Exception, error: traceback.print_exc() connection.privmsg(nick, 'an error occurred') def on_quit(self, connection, event): self.__logins.pop(irclib.nm_to_n(event.source()), None) def on_welcome(self, connection, event): for channel in self.__channels: connection.join(channel) def start(self): latitude_thread = threading.Thread(None, self.__latitude) latitude_thread.daemon = True latitude_thread.start() services = [self.geocoder] for service in services: service.start() if not self.__reloading: self._connect() else: self.__reloading = False for channel in self.__channels.symmetric_difference(self.channels.keys()): if channel in self.__channels: self.connection.join(channel) else: self.connection.part(channel) ping_next = datetime.utcnow() + timedelta(minutes = 1) while not self.__reloading: try: now = datetime.utcnow() if now >= ping_next: self.connection.ping(self.connection.server) ping_next = now + timedelta(minutes = 1) if self.__locations_lock.acquire(False): if self.__locations and self.__channels.issubset(self.channels.keys()): for nick, channel, granularity, location, coordinates, accuracy, speed, heading, altitude, altitude_accuracy in self.__locations: aux = [] if accuracy is not None: aux.append('accuracy: ' + self.__distance(accuracy)) if speed is not None: aux.append('speed: ' + self.__speed(speed)) if heading is not None: aux.append(u'heading: ' + self.__heading(heading)) if altitude is not None: aux.append('altitude: ' + self.__distance(altitude)) if altitude_accuracy is not None: aux.append('altitude accuracy: ' + self.__distance(altitude_accuracy)) if aux: aux = ' [%s]' % ', '.join(aux) else: aux = '' self.connection.notice(channel, '%s is in %s%s %s' % (nick, location, aux, self.__url(nick, granularity, location, coordinates))) self.__locations = [] self.__locations_lock.release() except irclib.ServerNotConnectedError: self.jump_server() self.ircobj.process_once(0.2) latitude_thread.join() with self.__latitude_timer_lock: if self.__latitude_timer is not None: self.__latitude_timer.cancel() self.__latitude_timer.join() for service in services: service.stop() return not self.__quiting def success(self): self.connection.privmsg(self.__nick, 'successfully reloaded') if __name__ == '__main__': os.chdir(os.path.abspath(os.path.dirname(__file__))) pid = os.fork() if pid != 0: with open('locationbot.pid', 'w') as _file: _file.write('%u\n' % pid) sys.exit(0) sys.stdin = open('/dev/null') sys.stdout = open('locationbot.log', 'a', 1) sys.stderr = sys.stdout import locationbot bot = locationbot.LocationBot() try: while bot.start(): try: bot = reload(locationbot).LocationBot(bot) except (ImportError, SyntaxError), error: bot.error(error) else: bot.success() except KeyboardInterrupt: bot.disconnect() os.unlink('locationbot.pid') # vim: noexpandtab tabstop=4