Browse Source

Beginning of the great refactor.

Douglas William Thrift 11 years ago
parent
commit
afe384d9ef
1 changed files with 247 additions and 107 deletions
  1. 247 107
      locationbot.py

+ 247 - 107
locationbot.py

@@ -5,12 +5,14 @@
 #
 # locationbot.py
 
+import apiclient.discovery
 from ConfigParser import NoOptionError, SafeConfigParser
 from datetime import datetime, timedelta
 import getpass
 import ircbot
 import irclib
-import oauth2 as oauth
+import oauth2client.client
+import oauth2client.tools
 import os
 import psycopg2
 import psycopg2.extensions
@@ -31,6 +33,223 @@ try:
 except ImportError:
 	import json
 
+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)}))
+
+	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 distance(value)
+		elif info in ('speed',):
+			return speed(value)
+		elif info in ('heading',):
+			return 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.values['location'], aux, self.url)
+
+	@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, meters_per_second_to_miles_per_hour(speed))
+
+	@classmethod
+	def heading(cls, heading):
+		if heading is not None:
+			return u'%.1f\xb0 (%s)' % (heading, heading_to_direction(heading))
+
+	@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'
+
 class LocationBot(ircbot.SingleServerIRCBot):
 	def __init__(self, bot = None):
 		self.__config = SafeConfigParser({'min_update_distance': "60"})
@@ -63,8 +282,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_client_id = self.__config.get(nick, 'latitude_client_id')
-			self.__latitude_consumer = oauth.Consumer(key = self.__latitude_client_id, secret = self.__config.get(nick, 'latitude_client_secret'))
+			#self.__latitude_client_id = self.__config.get(nick, 'latitude_client_id')
+			#self.__latitude_consumer = oauth.Consumer(key = self.__latitude_client_id, secret = self.__config.get(nick, 'latitude_client_secret'))
 			self.min_update_distance = self.__config.getint(nick, 'min_update_distance')
 
 		except NoOptionError, error:
@@ -78,13 +297,13 @@ class LocationBot(ircbot.SingleServerIRCBot):
 		ircbot.SingleServerIRCBot.__init__(self, servers, nick, 'Location Bot')
 
 		if bot is None:
-			self.__geocode_cache = {}
+			self.geocoder = Geocoder()
 			self.__locations = []
 			self.__logins = {}
 			self.__nick = None
 			self.__reloading = False
 		else:
-			self.__geocode_cache = bot.__geocode_cache
+			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
@@ -95,9 +314,6 @@ 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
@@ -136,83 +352,6 @@ 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), timeout = self.__timeout))
-		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 __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 __heading(self, heading):
 		if heading is not None:
 			return _encode(u'%.1f\xb0 (%s)' % (heading, _heading_to_direction(heading)))
@@ -272,7 +411,7 @@ class LocationBot(ircbot.SingleServerIRCBot):
 
 	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')
+			#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())
@@ -280,7 +419,7 @@ class LocationBot(ircbot.SingleServerIRCBot):
 			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.__geocode(False, coordinates = coordinates)[1]
+			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()
 
@@ -540,10 +679,10 @@ class LocationBot(ircbot.SingleServerIRCBot):
 							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',
-					}))
+					#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())
@@ -556,11 +695,11 @@ class LocationBot(ircbot.SingleServerIRCBot):
 						return invalid(value)
 
 					channels, old_location, granularity, token, secret = cursor.fetchone()
-					token = oauth.Token(token, secret)
+					#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')
+					#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:
@@ -606,7 +745,7 @@ class LocationBot(ircbot.SingleServerIRCBot):
 
 					location = None
 
-				geocode = self.__geocode(False, coordinates, location)
+				geocode = self.geocoder.geocode(False, coordinates, location)
 				new_location = geocode[1]
 
 				if variable == 'location':
@@ -616,11 +755,11 @@ class LocationBot(ircbot.SingleServerIRCBot):
 					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'})
+					#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())
@@ -853,10 +992,10 @@ class LocationBot(ircbot.SingleServerIRCBot):
 
 		latitude_thread.start()
 
-		geocode_cache_clean_thread = threading.Thread(None, self.__geocode_cache_clean)
-		geocode_cache_clean_thread.daemon = True
+		services = [self.geocoder]
 
-		geocode_cache_clean_thread.start()
+		for service in services:
+			service.start()
 
 		if not self.__reloading:
 			self._connect()
@@ -916,17 +1055,14 @@ class LocationBot(ircbot.SingleServerIRCBot):
 			self.ircobj.process_once(0.2)
 
 		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()
+		for service in services:
+			service.stop()
 
 		return not self.__quiting
 
@@ -956,6 +1092,8 @@ class _AddressMask(object):
 irclib.mask_matches = _AddressMask(reload(irclib).mask_matches)
 
 def _encode(string):
+	string = unicode(string)
+
 	try:
 		return string.encode('latin1')
 	except UnicodeEncodeError:
@@ -1044,3 +1182,5 @@ if __name__ == '__main__':
 		bot.disconnect()
 
 	os.unlink('locationbot.pid')
+
+# vim: noexpandtab tabstop=4