|
@@ -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
|