Browse Source

Using the Latitude API!

Douglas William Thrift 14 years ago
parent
commit
d7ab73f121
2 changed files with 236 additions and 91 deletions
  1. 223 89
      locationbot.py
  2. 13 2
      locationbot.sql

+ 223 - 89
locationbot.py

@@ -10,6 +10,7 @@ from datetime import datetime, timedelta
 import getpass
 import ircbot
 import irclib
+import oauth2 as oauth
 import os
 import psycopg2
 import psycopg2.extensions
@@ -19,6 +20,7 @@ import sys
 import time
 import threading
 import urllib, urllib2
+import urlparse
 import warnings
 
 try:
@@ -58,6 +60,8 @@ class LocationBot(ircbot.SingleServerIRCBot):
 			self.__admins = self.__config.get(nick, 'admins').split()
 			self.__channels = set(self.__config.get(nick, 'channels').split())
 			self.__dsn = self.__config.get(nick, 'dsn')
+			self.__latitude_key = self.__config.get(nick, 'latitude_key')
+			self.__latitude_consumer = oauth.Consumer(key = self.__latitude_key, secret = self.__config.get(nick, 'latitude_secret'))
 		except NoOptionError, error:
 			sys.exit(error)
 
@@ -68,16 +72,14 @@ class LocationBot(ircbot.SingleServerIRCBot):
 
 		ircbot.SingleServerIRCBot.__init__(self, servers, nick, 'Location Bot')
 
-		self.__latitude_timer_lock = threading.Lock()
-		self.__latitude_timer = None
-		self.__locations_lock = threading.Lock()
-
 		if bot is None:
+			self.__geocode_cache = {}
 			self.__locations = []
 			self.__logins = {}
 			self.__nick = None
 			self.__reloading = False
 		else:
+			self.__geocode_cache = bot.__geocode_cache
 			irclibobj = self.ircobj.connections[0].irclibobj
 			self.ircobj.connections[0] = bot.ircobj.connections[0]
 			self.ircobj.connections[0].irclibobj = irclibobj
@@ -88,9 +90,16 @@ class LocationBot(ircbot.SingleServerIRCBot):
 			self.__nick = bot.__nick
 			self.__reloading = True
 
+		self.__geocode_cache_lock = threading.Lock()
+		self.__geocode_cache_clean_timer_lock = threading.Lock()
+		self.__geocode_cache_clean_timer = None
+		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.__variables = frozenset(['nick', 'secret', 'masks', 'channels', 'timezone', 'location', 'coordinates', 'latitude'])
-		self.__geocode = self.__variables.intersection(['location', 'coordinates'])
+		self.__geocode_variables = self.__variables.intersection(['location', 'coordinates'])
 		self.__lists = self.__variables.intersection(['masks', 'channels'])
 		self.__unsetable = self.__variables.difference(['nick', 'secret'])
 
@@ -106,9 +115,6 @@ class LocationBot(ircbot.SingleServerIRCBot):
 
 		return _or(lambda channel: channel.has_user(nick), channels)
 
-	def __coordinates(self, coordinates):
-		return ' http://maps.google.com/maps?q=%f,%f' % coordinates if coordinates else ''
-
 	def __db(self):
 		db = psycopg2.connect(self.__dsn)
 
@@ -120,6 +126,84 @@ class LocationBot(ircbot.SingleServerIRCBot):
 
 		return db, db.cursor()
 
+	def __geocode(self, sensor, coordinates = None, location = None):
+		parameters = {'sensor': 'true' if sensor else 'false'}
+
+		with self.__geocode_cache_lock:
+			if coordinates is not None:
+				try:
+					return self.__geocode_cache[coordinates][0]
+				except KeyError:
+					parameters['latlng'] = '%f,%f' % coordinates
+			else:
+				try:
+					return self.__geocode_cache[coordinates][0]
+				except KeyError:
+					parameters['address'] = location
+
+		geocode = json.load(urllib2.urlopen('http://maps.google.com/maps/api/geocode/json?' + urllib.urlencode(parameters)))
+		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
+
+		if coordinates is not None:
+			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 __geocode_cache_clean(self):
+		now = datetime.utcnow()
+
+		try:
+			while now < self.__geocode_cache_clean_next:
+				time.sleep((self.__geocode_cache_clean_next - now).seconds)
+
+				now = datetime.utcnow()
+		except AttributeError:
+			self.__geocode_cache_length = timedelta(hours = 1)
+
+		self.__geocode_cache_clean_next = now.replace(minute = 2, second = 30, microsecond = 0) + self.__geocode_cache_length
+
+		with self.__geocode_cache_lock:
+			for location, (geocode, created) in self.__geocode_cache.items():
+				if now - created >= self.__geocode_cache_length:
+					del self.__geocode_cache[location]
+
+		with self.__geocode_cache_clean_timer_lock:
+			self.__geocode_cache_clean_timer = threading.Timer((self.__geocode_cache_clean_next - datetime.utcnow()).seconds, self.__latitude)
+
+			self.__geocode_cache_clean_timer.start()
+
 	def __help(self, connection, nick, admin, login, arguments):
 		command = irclib.irc_lower(arguments.split(None, 1)[0].lstrip('!')) if arguments else None
 		commands = {
@@ -173,12 +257,17 @@ class LocationBot(ircbot.SingleServerIRCBot):
 		self.__write()
 		connection.privmsg(nick, 'successfully joined channel ("%s")' % channel)
 
-	def __latitude(self, latitude = None):
-		if latitude is not None:
-			feature = json.load(urllib2.urlopen('http://www.google.com/latitude/apps/badge/api?' + urllib.urlencode({'user': latitude, 'type': 'json'})))['features'][0]
-			properties = feature['properties']
+	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)).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 properties['reverseGeocode'], tuple(reversed(feature['geometry']['coordinates'])), datetime.fromtimestamp(properties['timeStamp'], pytz.utc)
+			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.__geocode(False, coordinates = coordinates)[1]
 
 		now = datetime.utcnow()
 
@@ -195,25 +284,18 @@ class LocationBot(ircbot.SingleServerIRCBot):
 		try:
 			db, cursor = self.__db()
 
-			cursor.execute('select nick, channels, location, latitude from locationbot.nick where latitude is not null order by latitude')
+			cursor.execute('select nick, channels, location, latitude.granularity, token, latitude.secret from locationbot.nick join locationbot.latitude using (id)')
 
-			locations = {}
-
-			for nick, channels, old_location, latitude in cursor.fetchall():
-				new_location = locations.get(latitude)
-
-				if latitude not in locations:
-					try:
-						new_location, coordinates, updated = self.__latitude(latitude)
-						locations[latitude] = new_location
-					except Exception, error:
-						print error
-						continue
-
-					cursor.execute('update locationbot.nick set location = %s, coordinates = point %s, updated = %s where latitude = %s', (new_location, coordinates, updated, latitude))
-					db.commit()
+			for nick, channels, old_location, granularity, token, secret in cursor.fetchall():
+				try:
+					updated, coordinates, accuracy, speed, heading, altitude, altitude_accuracy, new_location = self.__latitude(granularity, token, secret)
+				except Exception, error:
+					print error
+					continue
 
-				self.__location(nick, channels, old_location, new_location, coordinates)
+				cursor.execute('update locationbot.nick set granularity = %s, location = %s, coordinates = point %s, updated = %s where nick = %s', (granularity, new_location, coordinates, updated, nick))
+				db.commit()
+				self.__location(nick, channels, granularity, old_location, new_location, coordinates)
 		except psycopg2.Error, error:
 			print error
 
@@ -222,11 +304,11 @@ class LocationBot(ircbot.SingleServerIRCBot):
 
 			self.__latitude_timer.start()
 
-	def __location(self, nick, channels, old_location, new_location, coordinates = None):
+	def __location(self, nick, channels, granularity, old_location, new_location, coordinates):
 		if channels and new_location and new_location != old_location:
 			with self.__locations_lock:
 				for channel in self.__channels.intersection(channels):
-					self.__locations.append((nick, channel, new_location, coordinates))
+					self.__locations.append((nick, channel, granularity, new_location, coordinates))
 
 	def __login(self, connection, nickmask, nick, arguments = ''):
 		login = nick
@@ -374,11 +456,19 @@ class LocationBot(ircbot.SingleServerIRCBot):
 		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 variable, variables)) + ' from locationbot.nick where nick = %s', (login,))
+			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 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, cursor.fetchone()):
+			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):
@@ -398,18 +488,56 @@ class LocationBot(ircbot.SingleServerIRCBot):
 						if not _mask.match(mask):
 							return invalid(mask, 'mask')
 			elif variable == 'latitude':
-				try:
-					value = int(re.sub(r'^(?:http://(?:www\.)?google\.com/latitude/apps/badge/api\?user=)?([0-9]+)(?:&type=.*)?$', r'\1', value))
-				except ValueError:
-					return invalid(value)
-			elif variable in self.__geocode:
+				if value in self.__latitude_granularities:
+					response, content = oauth.Client(self.__latitude_consumer).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).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_key,
+						'granularity': value,
+						'oauth_token': token,
+					}))
+			elif variable in self.__geocode_variables:
 				cursor.execute('select channels, location from locationbot.nick where nick = %s', (login,))
 
 				channels, old_location = cursor.fetchone()
-				parameters = {'sensor': 'false'}
 
 				if variable == 'location':
-					parameters['address'] = value
+					coordinates = None
+					granularity = 'city'
+					location = value
 				else:
 					coordinates = value.split(None, 1)
 
@@ -421,42 +549,20 @@ class LocationBot(ircbot.SingleServerIRCBot):
 					except ValueError:
 						return invalid(value)
 
-					parameters['latlng'] = '%f,%f' % coordinates
+					for coordinate in coordinates:
+						if not -180.0 <= coordinate <= 180.0:
+							return invalid(value)
 
-				geocode = json.load(urllib2.urlopen('http://maps.google.com/maps/api/geocode/json?' + urllib.urlencode(parameters)))
-				status = geocode['status']
+					granularity = 'best'
+					location = None
 
-				if status != 'OK':
-					if status == 'ZERO_RESULTS':
-						return invalid(value)
-					else:
-						raise Exception(status)
-
-				results = geocode['results']
-				result = results[0]
-				new_location = result['formatted_address']
+				geocode = self.__geocode(False, coordinates, location)
+				new_location = geocode[1]
 
 				if variable == 'location':
-					location = result['geometry']['location']
-					coordinates = (location['lat'], location['lng'])
+					coordinates = geocode[0]
 					value = new_location
 				else:
-					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']):
-							new_location = result['formatted_address']
-							break
-
 					value = coordinates
 			elif variable == 'nick':
 				_nick = value.split(None, 1)
@@ -468,7 +574,17 @@ class LocationBot(ircbot.SingleServerIRCBot):
 					return invalid(value)
 
 			try:
-				cursor.execute('update locationbot.nick set ' + ('location = %s, coordinates = point %s, updated = now()' if variable in self.__geocode else variable + ' = ' + ('md5(%s)' if variable == 'secret' else '%s')) + ' where nick = %s', (new_location, coordinates, login) if variable in self.__geocode else (value, login))
+				if variable in self.__geocode_variables:
+					cursor.execute('update locationbot.nick set granularity = %s, location = %s, coordinates = point %s, updated = now() where nick = %s', (granularity, new_location, coordinates, 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':
@@ -480,16 +596,13 @@ class LocationBot(ircbot.SingleServerIRCBot):
 
 			if variable == 'nick':
 				self.__logins[nick] = (value, nickmask)
-			elif variable in self.__geocode or variable == 'latitude':
+			elif variable in self.__geocode_variables or variable == 'latitude' and authorized:
 				if variable == 'latitude':
-					cursor.execute('select channels, location from locationbot.nick where nick = %s', (login,))
-
-					channels, old_location = cursor.fetchone()
-					new_location, coordinates, updated = self.__latitude(value)
+					updated, coordinates, accuracy, speed, heading, altitude, altitude_accuracy, new_location = self.__latitude(granularity, token, secret)
 
-					cursor.execute('update locationbot.nick set location = %s, coordinates = point %s, updated = %s where nick = %s', (new_location, coordinates, updated, login))
+					cursor.execute('update locationbot.nick set granularity = %s, location = %s, coordinates = point %s, updated = %s where nick = %s', (granularity, new_location, coordinates, updated, login))
 
-				self.__location(login, channels, old_location, new_location, coordinates)
+				self.__location(login, channels, granularity, old_location, new_location, coordinates)
 
 	def __status(self, connection, nick, login, arguments):
 		_nick = arguments.split(None, 1)[0] if arguments else None
@@ -509,10 +622,10 @@ class LocationBot(ircbot.SingleServerIRCBot):
 		else:
 			timezone = pytz.utc
 
-		connection.privmsg(nick, '\x02nick                    location                            when                     map\x0f')
+		connection.privmsg(nick, '\x02nick                    location                            when                    map\x0f')
 
 		for _nick, location, coordinates, updated in locations:
-			connection.privmsg(nick, '%-23s %-35s %-23s %s' % (_nick, location, timezone.normalize(updated.astimezone(timezone)).strftime('%Y-%m-%d %H:%M %Z'), self.__coordinates(coordinates)))
+			connection.privmsg(nick, '%-23s %-35s %-23s %s' % (_nick, location, timezone.normalize(updated.astimezone(timezone)).strftime('%Y-%m-%d %H:%M %Z'), self.__url(_nick, 'best', location, coordinates)))
 
 	def __unknown(self, connection, nick, command):
 		connection.privmsg(nick, 'unknown command ("%s"); try "help"' % command)
@@ -534,10 +647,16 @@ class LocationBot(ircbot.SingleServerIRCBot):
 
 		db, cursor = self.__db()
 
-		cursor.execute('update locationbot.nick set ' + ('location = null, coordinates = null, updated = null' if variable in self.__geocode else variable + ' = null') + ' where nick = %s and ' + variable + ' is not null', (login,))
+		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?' + urllib.urlencode({'q': '%s (%s)' % (location, nick)})
+
 	def __who(self, connection, nick):
 		if self.__logins:
 			connection.privmsg(nick, '\x02login nick              nick mask\x0f')
@@ -665,10 +784,15 @@ class LocationBot(ircbot.SingleServerIRCBot):
 			connection.join(channel)
 
 	def start(self):
-		self.__latitude_thread = threading.Thread(None, self.__latitude)
-		self.__latitude_thread.daemon = True
+		latitude_thread = threading.Thread(None, self.__latitude)
+		latitude_thread.daemon = True
 
-		self.__latitude_thread.start()
+		latitude_thread.start()
+
+		geocode_cache_clean_thread = threading.Thread(None, self.__geocode_cache_clean)
+		geocode_cache_clean_thread.daemon = True
+
+		geocode_cache_clean_thread.start()
 
 		if not self.__reloading:
 			self._connect()
@@ -684,8 +808,8 @@ class LocationBot(ircbot.SingleServerIRCBot):
 		while not self.__reloading:
 			if self.__locations_lock.acquire(False):
 				if self.__locations and self.__channels.issubset(self.channels.keys()):
-					for nick, channel, location, coordinates in self.__locations:
-						self.connection.notice(channel, '%s is in %s' % (nick, location + self.__coordinates(coordinates)))
+					for nick, channel, granularity, location, coordinates in self.__locations:
+						self.connection.notice(channel, '%s is in %s %s' % (nick, location, self.__url(nick, granularity, location, coordinates)))
 
 					self.__locations = []
 
@@ -693,20 +817,30 @@ class LocationBot(ircbot.SingleServerIRCBot):
 
 			self.ircobj.process_once(0.2)
 
-		self.__latitude_thread.join()
+		latitude_thread.join()
+		geocode_cache_clean_thread.join()
 
 		with self.__latitude_timer_lock:
 			if self.__latitude_timer is not None:
 				self.__latitude_timer.cancel()
 				self.__latitude_timer.join()
 
+		with self.__geocode_cache_clean_timer_lock:
+			if self.__geocode_cache_clean_timer is not None:
+				self.__geocode_cache_clean_timer.cancel()
+				self.__geocode_cache_clean_timer.join()
+
 		return not self.__quiting
 
 	def success(self):
 		self.connection.privmsg(self.__nick, 'successfully reloaded')
 
 def _or(function, values):
-	return reduce(lambda a, b: a or b, map(function, values) if values else [False])
+	for value in values:
+		if function(value):
+			return True
+
+	return False
 
 if __name__ == '__main__':
 	os.chdir(os.path.abspath(os.path.dirname(__file__)))

+ 13 - 2
locationbot.sql

@@ -6,6 +6,8 @@
 
 create schema locationbot;
 
+create type locationbot.granularity as enum ('city', 'best');
+
 create table locationbot.nick (
 	id serial primary key,
 	nick text not null unique,
@@ -13,12 +15,21 @@ create table locationbot.nick (
 	masks text[],
 	channels text[],
 	timezone text,
+	granularity locationbot.granularity,
 	location text,
 	coordinates point,
-	updated timestamp with time zone,
-	latitude bigint
+	updated timestamp with time zone
+);
+
+create table locationbot.latitude (
+	id integer primary key references locationbot.nick on delete cascade,
+	granularity locationbot.granularity not null,
+	token text not null,
+	secret text not null,
+	authorized boolean not null
 );
 
 create index nick_location_index on locationbot.nick (location);
 create index nick_updated_index on locationbot.nick (updated);
 create index nick_latitude_index on locationbot.nick (latitude);
+create index latitude_authorized on locationbot.latitude (authorized);