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
 # locationbot.py
 
 
+import apiclient.discovery
 from ConfigParser import NoOptionError, SafeConfigParser
 from ConfigParser import NoOptionError, SafeConfigParser
 from datetime import datetime, timedelta
 from datetime import datetime, timedelta
 import getpass
 import getpass
 import ircbot
 import ircbot
 import irclib
 import irclib
-import oauth2 as oauth
+import oauth2client.client
+import oauth2client.tools
 import os
 import os
 import psycopg2
 import psycopg2
 import psycopg2.extensions
 import psycopg2.extensions
@@ -31,6 +33,223 @@ try:
 except ImportError:
 except ImportError:
 	import json
 	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):
 class LocationBot(ircbot.SingleServerIRCBot):
 	def __init__(self, bot = None):
 	def __init__(self, bot = None):
 		self.__config = SafeConfigParser({'min_update_distance': "60"})
 		self.__config = SafeConfigParser({'min_update_distance': "60"})
@@ -63,8 +282,8 @@ class LocationBot(ircbot.SingleServerIRCBot):
 			self.__admins = self.__config.get(nick, 'admins').split()
 			self.__admins = self.__config.get(nick, 'admins').split()
 			self.__channels = set(self.__config.get(nick, 'channels').split())
 			self.__channels = set(self.__config.get(nick, 'channels').split())
 			self.__dsn = self.__config.get(nick, 'dsn')
 			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')
 			self.min_update_distance = self.__config.getint(nick, 'min_update_distance')
 
 
 		except NoOptionError, error:
 		except NoOptionError, error:
@@ -78,13 +297,13 @@ class LocationBot(ircbot.SingleServerIRCBot):
 		ircbot.SingleServerIRCBot.__init__(self, servers, nick, 'Location Bot')
 		ircbot.SingleServerIRCBot.__init__(self, servers, nick, 'Location Bot')
 
 
 		if bot is None:
 		if bot is None:
-			self.__geocode_cache = {}
+			self.geocoder = Geocoder()
 			self.__locations = []
 			self.__locations = []
 			self.__logins = {}
 			self.__logins = {}
 			self.__nick = None
 			self.__nick = None
 			self.__reloading = False
 			self.__reloading = False
 		else:
 		else:
-			self.__geocode_cache = bot.__geocode_cache
+			self.geocoder = Geocoder(bot.geocoder.cache)
 			irclibobj = self.ircobj.connections[0].irclibobj
 			irclibobj = self.ircobj.connections[0].irclibobj
 			self.ircobj.connections[0] = bot.ircobj.connections[0]
 			self.ircobj.connections[0] = bot.ircobj.connections[0]
 			self.ircobj.connections[0].irclibobj = irclibobj
 			self.ircobj.connections[0].irclibobj = irclibobj
@@ -95,9 +314,6 @@ class LocationBot(ircbot.SingleServerIRCBot):
 			self.__nick = bot.__nick
 			self.__nick = bot.__nick
 			self.__reloading = True
 			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_granularities = frozenset(['city', 'best'])
 		self.__latitude_timer_lock = threading.Lock()
 		self.__latitude_timer_lock = threading.Lock()
 		self.__latitude_timer = None
 		self.__latitude_timer = None
@@ -136,83 +352,6 @@ class LocationBot(ircbot.SingleServerIRCBot):
 
 
 		return db, db.cursor()
 		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):
 	def __heading(self, heading):
 		if heading is not None:
 		if heading is not None:
 			return _encode(u'%.1f\xb0 (%s)' % (heading, _heading_to_direction(heading)))
 			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):
 	def __latitude(self, granularity = None, token = None, secret = None):
 		if granularity is not 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:
 			if int(response['status']) != 200:
 				raise Exception(content.strip())
 				raise Exception(content.strip())
@@ -280,7 +419,7 @@ class LocationBot(ircbot.SingleServerIRCBot):
 			data = json.loads(content)['data']
 			data = json.loads(content)['data']
 			coordinates = (data['latitude'], data['longitude'])
 			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()
 		now = datetime.utcnow()
 
 
@@ -540,10 +679,10 @@ class LocationBot(ircbot.SingleServerIRCBot):
 							return invalid(mask, 'mask')
 							return invalid(mask, 'mask')
 			elif variable == 'latitude':
 			elif variable == 'latitude':
 				if value in self.__latitude_granularities:
 				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:
 					if int(response['status']) != 200:
 						raise Exception(content.strip())
 						raise Exception(content.strip())
@@ -556,11 +695,11 @@ class LocationBot(ircbot.SingleServerIRCBot):
 						return invalid(value)
 						return invalid(value)
 
 
 					channels, old_location, granularity, token, secret = cursor.fetchone()
 					channels, old_location, granularity, token, secret = cursor.fetchone()
-					token = oauth.Token(token, secret)
+					#token = oauth.Token(token, secret)
 
 
 					token.set_verifier(value)
 					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'])
 					status = int(response['status'])
 
 
 					if status == 400:
 					if status == 400:
@@ -606,7 +745,7 @@ class LocationBot(ircbot.SingleServerIRCBot):
 
 
 					location = None
 					location = None
 
 
-				geocode = self.__geocode(False, coordinates, location)
+				geocode = self.geocoder.geocode(False, coordinates, location)
 				new_location = geocode[1]
 				new_location = geocode[1]
 
 
 				if variable == 'location':
 				if variable == 'location':
@@ -616,11 +755,11 @@ class LocationBot(ircbot.SingleServerIRCBot):
 					value = coordinates
 					value = coordinates
 
 
 				if authorized:
 				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:
 					if int(response['status']) != 200:
 						raise Exception(content.strip())
 						raise Exception(content.strip())
@@ -853,10 +992,10 @@ class LocationBot(ircbot.SingleServerIRCBot):
 
 
 		latitude_thread.start()
 		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:
 		if not self.__reloading:
 			self._connect()
 			self._connect()
@@ -916,17 +1055,14 @@ class LocationBot(ircbot.SingleServerIRCBot):
 			self.ircobj.process_once(0.2)
 			self.ircobj.process_once(0.2)
 
 
 		latitude_thread.join()
 		latitude_thread.join()
-		geocode_cache_clean_thread.join()
 
 
 		with self.__latitude_timer_lock:
 		with self.__latitude_timer_lock:
 			if self.__latitude_timer is not None:
 			if self.__latitude_timer is not None:
 				self.__latitude_timer.cancel()
 				self.__latitude_timer.cancel()
 				self.__latitude_timer.join()
 				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
 		return not self.__quiting
 
 
@@ -956,6 +1092,8 @@ class _AddressMask(object):
 irclib.mask_matches = _AddressMask(reload(irclib).mask_matches)
 irclib.mask_matches = _AddressMask(reload(irclib).mask_matches)
 
 
 def _encode(string):
 def _encode(string):
+	string = unicode(string)
+
 	try:
 	try:
 		return string.encode('latin1')
 		return string.encode('latin1')
 	except UnicodeEncodeError:
 	except UnicodeEncodeError:
@@ -1044,3 +1182,5 @@ if __name__ == '__main__':
 		bot.disconnect()
 		bot.disconnect()
 
 
 	os.unlink('locationbot.pid')
 	os.unlink('locationbot.pid')
+
+# vim: noexpandtab tabstop=4