locationbot.py 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186
  1. #!/usr/bin/env python
  2. # Location Bot
  3. #
  4. # Douglas Thrift
  5. #
  6. # locationbot.py
  7. import apiclient.discovery
  8. from ConfigParser import NoOptionError, SafeConfigParser
  9. from datetime import datetime, timedelta
  10. import getpass
  11. import ircbot
  12. import irclib
  13. import oauth2client.client
  14. import oauth2client.tools
  15. import os
  16. import psycopg2
  17. import psycopg2.extensions
  18. import pytz
  19. import re
  20. import socket
  21. import sys
  22. import time
  23. import threading
  24. import traceback
  25. import urllib, urllib2
  26. import urlparse
  27. import warnings
  28. import math
  29. try:
  30. import simplejson as json
  31. except ImportError:
  32. import json
  33. class ThreadTimerServiceMixin(object):
  34. def start(self):
  35. self.thread.daemon = True
  36. self.timer = None
  37. self.timer_lock = threading.Lock()
  38. self.thread.start()
  39. def stop(self):
  40. self.thread.join()
  41. with self.timer_lock:
  42. if self.timer is not None:
  43. self.timer.cancel()
  44. self.timer.join()
  45. class Geocoder(ThreadTimerServiceMixin):
  46. CACHE_DURATION = timedelta(hours = 1)
  47. def __init__(self, cache = None):
  48. if cache is None:
  49. self.cache = {}
  50. else:
  51. self.cache = cache
  52. self.cache_lock = threading.Lock()
  53. self.thread = threading.Thread(target = self.cache_clean)
  54. def geocode(self, sensor, coordinates = None, location = None):
  55. parameters = {'sensor': 'true' if sensor else 'false'}
  56. with self.cache_lock:
  57. if coordinates is not None:
  58. try:
  59. return self.cache[coordinates][0]
  60. except KeyError:
  61. parameters['latlng'] = '%f,%f' % coordinates
  62. else:
  63. try:
  64. return self.cache[coordinates][0]
  65. except KeyError:
  66. parameters['address'] = location
  67. geocode = json.load(urllib2.urlopen('http://maps.google.com/maps/api/geocode/json?' + urllib.urlencode(parameters), timeout = 5))
  68. status = geocode['status']
  69. if status != 'OK':
  70. if coordinates is not None and status == 'ZERO_RESULTS':
  71. return self.geocode(sensor, location = parameters['latlng'])
  72. else:
  73. raise Exception(status)
  74. results = geocode['results']
  75. def _result():
  76. _location = result['geometry']['location']
  77. geocode = (_location['lat'], _location['lng']), result['formatted_address']
  78. with self.__geocode_cache_lock:
  79. self.__geocode_cache[coordinates if coordinates is not None else location] = (geocode, datetime.utcnow())
  80. return geocode
  81. types = frozenset([
  82. 'country',
  83. 'administrative_area_level_1',
  84. 'administrative_area_level_2',
  85. 'administrative_area_level_3',
  86. 'colloquial_area',
  87. 'locality',
  88. 'sublocality',
  89. 'neighborhood',
  90. ])
  91. for result in results:
  92. if not types.isdisjoint(result['types']):
  93. return _result()
  94. result = results[0]
  95. return _result()
  96. def cache_clean(self):
  97. now = datetime.utcnow()
  98. try:
  99. while now < self.cache_clean_next:
  100. time.sleep((self.cache_clean_next - now).seconds)
  101. now = datetime.utcnow()
  102. except AttributeError:
  103. pass
  104. self.cache_clean_next = now.replace(minute = 2, second = 30, microsecond = 0) + self.CACHE_DURATION
  105. with self.cache_lock:
  106. for location, (geocode, created) in self.cache.items():
  107. if now - created >= self.CACHE_DURATION:
  108. del self.cache[location]
  109. with self.timer_lock:
  110. self.timer = threading.Timer((self.cache_clean_next - datetime.utcnow()).seconds, self.cache_clean)
  111. self.timer.start()
  112. class Location(object):
  113. VALUES = ('nick', 'granularity', 'location', 'coordinates', 'accuracy', 'speed', 'heading', 'altitude', 'altitude_accuracy', 'updated')
  114. def __init__(self, **values):
  115. if 'row' in values:
  116. self.values = dict(zip(VALUES, values[row]))
  117. else:
  118. self.values = values
  119. @property
  120. def url(self):
  121. if self.granularity == 'best':
  122. location = '%f,%f' % self.values['coordinates']
  123. else:
  124. location = self.values['location']
  125. 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)}))
  126. def __get_attr__(self, name):
  127. if name not in VALUES:
  128. raise AttributeError("'Location' object has no attribute '%s'" % name)
  129. value = self.values.get(name)
  130. if info in ('accuracy', 'altitude', 'altitude_accuracy'):
  131. return distance(value)
  132. elif info in ('speed',):
  133. return speed(value)
  134. elif info in ('heading',):
  135. return heading(value)
  136. else:
  137. return unicode(value)
  138. def __unicode__(self):
  139. try:
  140. venue = ' at %s' % self.values['venue']
  141. except KeyError:
  142. venue = ''
  143. aux = []
  144. for name in ('accuracy', 'speed', 'heading', 'altitude', 'altitude_accuracy'):
  145. if name in self.values:
  146. aux.append('%s: %s' % (name.replace('_', ' '), getattr(self, name)))
  147. if aux:
  148. aux = ' [%s]' % ', '.join(aux)
  149. else:
  150. aux = ''
  151. return '%s is%s in %s%s %s' % (self.nick, venue, self.values['location'], aux, self.url)
  152. @classmethod
  153. def distance(cls, distance):
  154. if distance is not None:
  155. return '%.1f m (%.1f ft)' % (distance, cls.meters_to_feet(distance))
  156. @classmethod
  157. def speed(cls, speed):
  158. if speed is not None:
  159. return '%.1f m/s (%.1f mph)' % (speed, meters_per_second_to_miles_per_hour(speed))
  160. @classmethod
  161. def heading(cls, heading):
  162. if heading is not None:
  163. return u'%.1f\xb0 (%s)' % (heading, heading_to_direction(heading))
  164. @staticmethod
  165. def meters_to_feet(meters):
  166. return meters * 3.2808399
  167. @staticmethod
  168. def meters_per_second_to_miles_per_hour(meters_per_second):
  169. return meters_per_second * 2.23693629
  170. @staticmethod
  171. def heading_to_direction(heading):
  172. heading %= 360
  173. if 348.75 < heading or heading <= 11.25:
  174. return 'N'
  175. elif 11.25 < heading <= 33.75:
  176. return 'NNE'
  177. elif 33.75 < heading <= 56.25:
  178. return 'NE'
  179. elif 56.25 < heading <= 78.75:
  180. return 'ENE'
  181. elif 78.75 < heading <= 101.25:
  182. return 'E'
  183. elif 101.25 < heading <= 123.75:
  184. return 'ESE'
  185. elif 123.75 < heading <= 146.25:
  186. return 'SE'
  187. elif 146.25 < heading <= 168.75:
  188. return 'SSE'
  189. elif 168.75 < heading <= 191.25:
  190. return 'S'
  191. elif 191.25 < heading <= 213.75:
  192. return 'SSW'
  193. elif 213.75 < heading <= 236.25:
  194. return 'SW'
  195. elif 236.25 < heading <= 258.75:
  196. return 'WSW'
  197. elif 258.75 < heading <= 281.25:
  198. return 'W'
  199. elif 281.25 < heading <= 303.75:
  200. return 'WNW'
  201. elif 303.75 < heading <= 326.25:
  202. return 'NW'
  203. else:
  204. return 'NNW'
  205. class LocationBot(ircbot.SingleServerIRCBot):
  206. def __init__(self, bot = None):
  207. self.__config = SafeConfigParser({'min_update_distance': "60"})
  208. self.__config.read('locationbot.ini')
  209. try:
  210. nick = self.__config.sections()[0]
  211. except IndexError:
  212. sys.exit('No nick configured')
  213. servers = []
  214. try:
  215. for server in self.__config.get(nick, 'servers').split():
  216. server = server.split(':', 2)
  217. if len(server) == 1:
  218. servers.append((server[0], 6667))
  219. else:
  220. host = server[0]
  221. port = int(server[1])
  222. ssl = server[1].startswith('+')
  223. if len(server) == 3:
  224. servers.append((host, port, ssl, server[2]))
  225. else:
  226. servers.append((host, port, ssl))
  227. self.__admins = self.__config.get(nick, 'admins').split()
  228. self.__channels = set(self.__config.get(nick, 'channels').split())
  229. self.__dsn = self.__config.get(nick, 'dsn')
  230. #self.__latitude_client_id = self.__config.get(nick, 'latitude_client_id')
  231. #self.__latitude_consumer = oauth.Consumer(key = self.__latitude_client_id, secret = self.__config.get(nick, 'latitude_client_secret'))
  232. self.min_update_distance = self.__config.getint(nick, 'min_update_distance')
  233. except NoOptionError, error:
  234. sys.exit(error)
  235. try:
  236. self.__hostname = self.__config.get(nick, 'hostname')
  237. except NoOptionError:
  238. self.__hostname = ''
  239. ircbot.SingleServerIRCBot.__init__(self, servers, nick, 'Location Bot')
  240. if bot is None:
  241. self.geocoder = Geocoder()
  242. self.__locations = []
  243. self.__logins = {}
  244. self.__nick = None
  245. self.__reloading = False
  246. else:
  247. self.geocoder = Geocoder(bot.geocoder.cache)
  248. irclibobj = self.ircobj.connections[0].irclibobj
  249. self.ircobj.connections[0] = bot.ircobj.connections[0]
  250. self.ircobj.connections[0].irclibobj = irclibobj
  251. self.channels = bot.channels
  252. self.connection = bot.connection
  253. self.__locations = bot.__locations
  254. self.__logins = bot.__logins
  255. self.__nick = bot.__nick
  256. self.__reloading = True
  257. self.__latitude_granularities = frozenset(['city', 'best'])
  258. self.__latitude_timer_lock = threading.Lock()
  259. self.__latitude_timer = None
  260. self.__locations_lock = threading.Lock()
  261. self.__quiting = False
  262. self.__timeout = 5
  263. self.__variables = frozenset(['nick', 'secret', 'masks', 'channels', 'timezone', 'location', 'coordinates', 'latitude'])
  264. self.__geocode_variables = self.__variables.intersection(['location', 'coordinates'])
  265. self.__lists = self.__variables.intersection(['masks', 'channels'])
  266. self.__unsetable = self.__variables.difference(['nick', 'secret'])
  267. def __admin(self, nickmask):
  268. return _or(lambda admin: irclib.mask_matches(nickmask, admin), self.__admins)
  269. def __channel(self, nick, exclude = None):
  270. if exclude is not None:
  271. exclude = irclib.irc_lower(exclude)
  272. channels = map(lambda channel: channel[1], filter(lambda channel: irclib.irc_lower(channel[0]) == exclude, self.channels))
  273. else:
  274. channels = self.channels.values()
  275. return _or(lambda channel: channel.has_user(nick), channels)
  276. def __distance(self, distance):
  277. if distance is not None:
  278. return '%.1f m (%.1f ft)' % (distance, _meters_to_feet(distance))
  279. def __db(self):
  280. db = psycopg2.connect(self.__dsn)
  281. def point(value, cursor):
  282. if value is not None:
  283. return tuple(map(lambda a: float(a), re.match(r'^\(([^)]+),([^)]+)\)$', value).groups()))
  284. psycopg2.extensions.register_type(psycopg2.extensions.new_type((600,), 'point', point), db)
  285. return db, db.cursor()
  286. def __heading(self, heading):
  287. if heading is not None:
  288. return _encode(u'%.1f\xb0 (%s)' % (heading, _heading_to_direction(heading)))
  289. def __help(self, connection, nick, admin, login, arguments):
  290. command = irclib.irc_lower(arguments.split(None, 1)[0].lstrip('!')) if arguments else None
  291. commands = {
  292. 'help': ('[command]', 'show this help message'),
  293. 'status': ('[nick]', 'show where everybody or a nick is'),
  294. }
  295. if not login:
  296. commands.update({
  297. 'login': ('[nick] [secret]', 'log in as nick with secret or using masks'),
  298. 'register': ('[nick] secret', 'register as nick with secret'),
  299. })
  300. else:
  301. commands.update({
  302. 'logout': ('', 'log out as nick'),
  303. 'set': ('[variable [value]]', 'display or set variables'),
  304. 'unset': ('variable', 'unset a variable'),
  305. })
  306. if admin:
  307. commands.update({
  308. 'join': ('channel', 'join a channel'),
  309. 'part': ('channel [message]', 'part from a channel'),
  310. 'quit': ('[message]', 'quit and do not come back'),
  311. 'reload': ('', 'reload with more up to date code'),
  312. 'restart': ('', 'quit and join running more up to date code'),
  313. 'say': ('nick|channel message', 'say message to nick or channel'),
  314. 'who': ('', 'show who is logged in'),
  315. })
  316. connection.privmsg(nick, '\x02command arguments description\x0f')
  317. def help(command, arguments, description):
  318. connection.privmsg(nick, '%-11s %-23s %s' % (command, arguments, description))
  319. if command in commands:
  320. help(command, *commands[command])
  321. else:
  322. for command, (arguments, description) in sorted(commands.iteritems()):
  323. help(command, arguments, description)
  324. def __join(self, connection, nick, arguments):
  325. try:
  326. channel = arguments.split(None, 1)[0]
  327. except IndexError:
  328. return self.__help(connection, nick, True, False, 'join')
  329. connection.join(channel)
  330. self.__channels.add(channel)
  331. self.__config.set(self._nickname, 'channels', ' '.join(self.__channels))
  332. self.__write()
  333. connection.privmsg(nick, 'successfully joined channel ("%s")' % channel)
  334. def __latitude(self, granularity = None, token = None, secret = None):
  335. if granularity is not None:
  336. #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')
  337. if int(response['status']) != 200:
  338. raise Exception(content.strip())
  339. data = json.loads(content)['data']
  340. coordinates = (data['latitude'], data['longitude'])
  341. 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]
  342. now = datetime.utcnow()
  343. try:
  344. while now < self.__latitude_next:
  345. time.sleep((self.__latitude_next - now).seconds)
  346. now = datetime.utcnow()
  347. except AttributeError:
  348. pass
  349. self.__latitude_next = now.replace(minute = now.minute - now.minute % 5, second = 0, microsecond = 0) + timedelta(minutes = 5)
  350. try:
  351. db, cursor = self.__db()
  352. 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)')
  353. 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():
  354. try:
  355. updated, coordinates, accuracy, speed, heading, altitude, altitude_accuracy, location = self.__latitude(granularity, token, secret)
  356. except KeyError, error:
  357. print nick, error
  358. continue
  359. except Exception, error:
  360. traceback.print_exc()
  361. continue
  362. 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))
  363. db.commit()
  364. 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)
  365. except psycopg2.Error, error:
  366. traceback.print_exc()
  367. with self.__latitude_timer_lock:
  368. self.__latitude_timer = threading.Timer((self.__latitude_next - datetime.utcnow()).seconds, self.__latitude)
  369. self.__latitude_timer.start()
  370. def __location(self, nick, channels, granularity, old_location, new_location, old_coordinates, new_coordinates, old_accuracy, new_accuracy, old_speed, new_speed, old_heading, new_heading, old_altitude, new_altitude, old_altitude_accuracy, new_altitude_accuracy, old_updated, new_updated):
  371. def calc_distance(lat1, lon1, lat2, lon2): # returns meters
  372. nauticalMilePerLat = 60.00721
  373. nauticalMilePerLongitude = 60.10793
  374. rad = math.pi / 180.0
  375. milesPerNauticalMile = 1.15078
  376. metersPerNauticalMile = 1852
  377. yDistance = (lat2 - lat1) * nauticalMilePerLat
  378. xDistance = (math.cos(lat1 * rad) + math.cos(lat2 * rad)) * \
  379. (lon2 - lon1) * (nauticalMilePerLongitude / 2)
  380. distance = math.sqrt( yDistance**2 + xDistance**2 )
  381. return distance * metersPerNauticalMile
  382. def make_update_noise():
  383. with self.__locations_lock:
  384. for channel in self.__channels.intersection(channels):
  385. self.__locations.append((nick, channel, granularity, new_location, new_coordinates, new_accuracy, new_speed, new_heading, new_altitude, new_altitude_accuracy))
  386. if channels:
  387. if new_location != old_location: # old method
  388. return make_update_noise()
  389. distance = calc_distance(old_coordinates[0], old_coordinates[1],
  390. new_coordinates[0], new_coordinates[1])
  391. if distance < self.min_update_distance:
  392. return
  393. if old_accuracy is not None and new_accuracy is not None:
  394. avg_radius = (old_accuracy + new_accuracy) / 2
  395. if distance > avg_radius:
  396. return make_update_noise()
  397. min_radius = min(old_accuracy, new_accuracy)
  398. if distance > min_radius and new_accuracy < old_accuracy:
  399. return make_update_noise()
  400. def __login(self, connection, nickmask, nick, arguments = ''):
  401. login = nick
  402. if connection is not None:
  403. arguments = arguments.split(None, 1)
  404. if len(arguments) == 2:
  405. login, secret = arguments
  406. elif len(arguments) == 1:
  407. secret = arguments[0]
  408. else:
  409. secret = None
  410. else:
  411. secret = None
  412. if nick in self.__logins:
  413. login = self.__logins[nick][0]
  414. if connection is not None:
  415. return connection.privmsg(nick, 'already logged in as "%s"' % login)
  416. return login
  417. db, cursor = self.__db()
  418. def success():
  419. connection.privmsg(nick, 'successfully logged in as "%s"' % login)
  420. if secret is not None:
  421. cursor.execute('select true from locationbot.nick where nick = %s and secret = md5(%s)', (login, secret))
  422. if cursor.rowcount == 1:
  423. self.__logins[nick] = (login, nickmask)
  424. return success()
  425. cursor.execute('select nick, masks from locationbot.nick where nick in (%s, %s)', (login, secret if len(arguments) != 2 else None))
  426. for login, masks in cursor.fetchall():
  427. if _or(lambda mask: irclib.mask_matches(nickmask, mask), masks):
  428. self.__logins[nick] = (login, nickmask)
  429. return success() if connection else login
  430. if connection is not None:
  431. return connection.privmsg(nick, 'failed to log in as "%s"' % login)
  432. def __logout(self, connection, nick):
  433. connection.privmsg(nick, 'logged out as "%s"' % self.__logins.pop(nick)[0])
  434. def __part(self, connection, nick, arguments):
  435. arguments = arguments.split(None, 1)
  436. if len(arguments) == 2:
  437. channel, message = arguments
  438. message = ':' + message
  439. elif len(arguments) == 1:
  440. channel = arguments[0]
  441. message = ''
  442. else:
  443. return self.__help(connection, nick, True, False, 'part')
  444. if channel in self.__channels:
  445. connection.part(channel, message)
  446. self.__channels.remove(channel)
  447. self.__config.set(self._nickname, 'channels', ' '.join(self.__channels))
  448. self.__write()
  449. connection.privmsg(nick, 'successfully parted channel ("%s")' % channel)
  450. else:
  451. connection.privmsg(nick, 'not in channel ("%s")' % channel)
  452. def __quit(self, connection, nick, arguments):
  453. self.__reloading = True
  454. self.__quiting = True
  455. connection.privmsg(nick, 'quiting')
  456. self.disconnect(arguments)
  457. def __register(self, connection, nick, arguments):
  458. arguments = arguments.split(None, 1)
  459. if len(arguments) == 2:
  460. login, secret = arguments
  461. elif len(arguments) == 1:
  462. login = nick
  463. secret = arguments[0]
  464. else:
  465. return self.__help(connection, nick, False, False, 'register')
  466. db, cursor = self.__db()
  467. try:
  468. cursor.execute('insert into locationbot.nick (nick, secret) values (%s, md5(%s))', (login, secret))
  469. db.commit()
  470. except psycopg2.IntegrityError:
  471. return connection.privmsg(nick, 'nick ("%s") is already registered' % login)
  472. connection.privmsg(nick, 'nick ("%s") sucessfully registered' % login)
  473. def __reload(self, connection, nick):
  474. self.__nick = nick
  475. self.__reloading = True
  476. connection.privmsg(nick, 'reloading')
  477. def __restart(self, connection):
  478. connection.disconnect('restarting')
  479. os.execvp(sys.argv[0], sys.argv)
  480. def __say(self, connection, nick, arguments):
  481. try:
  482. nick_channel, message = arguments.split(None, 1)
  483. except ValueError:
  484. return self.__help(connection, nick, True, False, 'say')
  485. if irclib.is_channel(nick_channel):
  486. if nick_channel not in self.channels:
  487. return connection.privmsg(nick, 'not in channel ("%s")' % nick_channel)
  488. elif not self.__channel(nick_channel):
  489. return connection.privmsg(nick, 'nick ("%s") not in channel(s)' % nick_channel)
  490. elif nick_channel == connection.get_nickname():
  491. return connection.privmsg(nick, 'nice try')
  492. connection.privmsg(nick_channel, message)
  493. connection.privmsg(nick, 'successfully sent message ("%s") to nick/channel ("%s")' % (message, nick_channel))
  494. def __set(self, connection, nickmask, nick, login, arguments):
  495. arguments = arguments.split(None, 1)
  496. if len(arguments) == 2:
  497. variable, value = arguments
  498. elif len(arguments) == 1:
  499. variable = arguments[0]
  500. value = None
  501. else:
  502. variable = None
  503. value = None
  504. if variable is not None and variable not in self.__variables:
  505. return self.__unknown_variable(connection, nick, variable)
  506. db, cursor = self.__db()
  507. if value is None:
  508. variables = sorted(self.__variables) if variable is None else [variable]
  509. 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,))
  510. values = list(cursor.fetchone())
  511. try:
  512. index = variables.index('latitude')
  513. values[index:index + 2] = ['%s (%s)' % (values[index], 'authorized' if values[index + 1] else 'unauthorized')]
  514. except ValueError:
  515. pass
  516. connection.privmsg(nick, '\x02variable value\x0f')
  517. for variable, value in zip(variables, values):
  518. connection.privmsg(nick, '%-11s %s' % (variable, ' '.join(value) if isinstance(value, list) else '%f,%f' % value if isinstance(value, tuple) else value))
  519. else:
  520. def invalid(value, variable = variable):
  521. connection.privmsg(nick, 'invalid %s ("%s")' % (variable, value))
  522. if variable in self.__lists:
  523. value = value.split()
  524. if variable == 'channels':
  525. for channel in value:
  526. if not irclib.is_channel(channel) or channel not in self.__channels:
  527. return invalid(channel, 'channel')
  528. elif variable == 'masks':
  529. _mask = re.compile('^.+!.+@.+$')
  530. for mask in value:
  531. if not _mask.match(mask):
  532. return invalid(mask, 'mask')
  533. elif variable == 'latitude':
  534. if value in self.__latitude_granularities:
  535. #response, content = oauth.Client(self.__latitude_consumer, timeout = self.__timeout).request('https://www.google.com/accounts/OAuthGetRequestToken', 'POST', urllib.urlencode({
  536. # 'scope': 'https://www.googleapis.com/auth/latitude',
  537. # 'oauth_callback': 'oob',
  538. #}))
  539. if int(response['status']) != 200:
  540. raise Exception(content.strip())
  541. authorized = False
  542. else:
  543. 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,))
  544. if cursor.rowcount == 0:
  545. return invalid(value)
  546. channels, old_location, granularity, token, secret = cursor.fetchone()
  547. #token = oauth.Token(token, secret)
  548. token.set_verifier(value)
  549. #response, content = oauth.Client(self.__latitude_consumer, token, timeout = self.__timeout).request('https://www.google.com/accounts/OAuthGetAccessToken', 'GET')
  550. status = int(response['status'])
  551. if status == 400:
  552. return invalid(value)
  553. elif status != 200:
  554. raise Exception(content.strip())
  555. authorized = True
  556. data = dict(urlparse.parse_qsl(content))
  557. token = data['oauth_token']
  558. secret = data['oauth_token_secret']
  559. if not authorized:
  560. connection.privmsg(nick, 'go to https://www.google.com/latitude/apps/OAuthAuthorizeToken?' + urllib.urlencode({
  561. 'domain': self.__latitude_client_id,
  562. 'granularity': value,
  563. 'oauth_token': token,
  564. }))
  565. elif variable in self.__geocode_variables:
  566. cursor.execute('select channels, location, latitude.granularity, token, latitude.secret, authorized from locationbot.nick left join locationbot.latitude using (id) where nick = %s', (login,))
  567. channels, old_location, granularity, token, secret, authorized = cursor.fetchone()
  568. if variable == 'location':
  569. coordinates = None
  570. granularity = 'city'
  571. location = value
  572. else:
  573. coordinates = value.split(None, 1)
  574. if len(coordinates) == 1:
  575. coordinates = coordinates[0].split(',', 1)
  576. try:
  577. coordinates = tuple(map(lambda a: float(a), coordinates))
  578. except ValueError:
  579. return invalid(value)
  580. for coordinate in coordinates:
  581. if not -180.0 <= coordinate <= 180.0:
  582. return invalid(value)
  583. location = None
  584. geocode = self.geocoder.geocode(False, coordinates, location)
  585. new_location = geocode[1]
  586. if variable == 'location':
  587. coordinates = geocode[0]
  588. value = new_location
  589. else:
  590. value = coordinates
  591. if authorized:
  592. #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': {
  593. # 'kind': 'latitude#location',
  594. # 'latitude': coordinates[0],
  595. # 'longitude': coordinates[1],
  596. #}}), {'Content-Type': 'application/json'})
  597. if int(response['status']) != 200:
  598. raise Exception(content.strip())
  599. accuracy = speed = heading = altitude = altitude_accuracy = None
  600. elif variable == 'nick':
  601. _nick = value.split(None, 1)
  602. if len(_nick) != 1:
  603. return invalid(value)
  604. elif variable == 'timezone':
  605. if value not in pytz.all_timezones_set:
  606. return invalid(value)
  607. try:
  608. if variable in self.__geocode_variables:
  609. 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))
  610. elif variable == 'latitude':
  611. if authorized:
  612. 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))
  613. else:
  614. cursor.execute('delete from locationbot.latitude using locationbot.nick where latitude.id = nick.id and nick = %s', (login,))
  615. 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))
  616. else:
  617. cursor.execute('update locationbot.nick set ' + variable + ' = ' + ('md5(%s)' if variable == 'secret' else '%s') + ' where nick = %s', (value, login))
  618. db.commit()
  619. except psycopg2.IntegrityError:
  620. if variable == 'nick':
  621. return connection.privmsg(nick, 'nick ("%s") is already registered' % value)
  622. raise
  623. 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))
  624. if variable == 'nick':
  625. self.__logins[nick] = (value, nickmask)
  626. elif variable in self.__geocode_variables or variable == 'latitude' and authorized:
  627. if variable == 'latitude':
  628. updated, coordinates, accuracy, speed, heading, altitude, altitude_accuracy, new_location = self.__latitude(granularity, token, secret)
  629. 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))
  630. self.__location(login, channels, granularity, old_location, new_location, None, coordinates, None, accuracy, None, speed, None, heading, None, altitude, None, altitude_accuracy, None, updated)
  631. def __speed(self, speed):
  632. if speed is not None:
  633. return '%.1f m/s (%.1f mph)' % (speed, _meters_per_second_to_miles_per_hour(speed))
  634. def __status(self, connection, nick, login, arguments):
  635. _nick = arguments.split(None, 1)[0] if arguments else None
  636. db, cursor = self.__db()
  637. 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,))
  638. if cursor.rowcount == 0:
  639. return connection.privmsg(nick, 'no location information for ' + ('"%s"' % _nick if _nick is not None else 'anybody'))
  640. locations = cursor.fetchall()
  641. if login is not None:
  642. cursor.execute('select timezone from locationbot.nick where nick = %s and timezone is not null', (login,))
  643. timezone = pytz.timezone(cursor.fetchone()[0]) if cursor.rowcount == 1 else pytz.utc
  644. else:
  645. timezone = pytz.utc
  646. connection.privmsg(nick, '\x02%-24s%-36s%-24s%-24s%-16s%-24s%-24s%-24s%s\x0f' % ('nick', 'location', 'accuracy', 'speed', 'heading', 'altitude', 'altitude accuracy', 'when', 'map'))
  647. for _nick, granularity, location, coordinates, accuracy, speed, heading, altitude, altitude_accuracy, updated in locations:
  648. 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)))
  649. def __unknown(self, connection, nick, command):
  650. connection.privmsg(nick, 'unknown command ("%s"); try "help"' % command)
  651. def __unknown_variable(self, connection, nick, variable):
  652. connection.privmsg(nick, 'unknown variable ("%s")' % variable)
  653. def __unset(self, connection, nick, login, arguments):
  654. try:
  655. variable = irclib.irc_lower(arguments.split(None, 1)[0])
  656. except IndexError:
  657. return self.__help(connection, nick, False, login, 'unset')
  658. if variable not in self.__unsetable:
  659. if variable in self.__variables:
  660. return connection.privmsg(nick, 'variable ("%s") is not unsetable' % variable)
  661. return self.__unknown_variable(connection, nick, variable)
  662. db, cursor = self.__db()
  663. 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,))
  664. db.commit()
  665. connection.privmsg(nick, 'variable ("%s") %s unset' % (variable, 'successfuly' if cursor.rowcount == 1 else 'already'))
  666. def __url(self, nick, granularity, location, coordinates):
  667. if granularity == 'best':
  668. location = '%f,%f' % coordinates
  669. 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)}))
  670. def __who(self, connection, nick):
  671. if self.__logins:
  672. connection.privmsg(nick, '\x02login nick nick mask\x0f')
  673. for login in sorted(self.__logins.values()):
  674. connection.privmsg(nick, '%-23s %s' % login)
  675. else:
  676. connection.privmsg(nick, 'nobody logged in')
  677. def __write(self):
  678. with open('locationbot.ini', 'w') as config:
  679. self.__config.write(config)
  680. def _connect(self):
  681. if len(self.server_list[0]) != 2:
  682. ssl = self.server_list[0][2]
  683. else:
  684. ssl = False
  685. if len(self.server_list[0]) == 4:
  686. password = self.server_list[0][3]
  687. else:
  688. password = None
  689. try:
  690. with warnings.catch_warnings():
  691. warnings.filterwarnings('ignore', r'socket\.ssl\(\) is deprecated\. Use ssl\.wrap_socket\(\) instead\.', DeprecationWarning)
  692. self.connect(self.server_list[0][0], self.server_list[0][1], self._nickname, password, getpass.getuser(), self._realname, localaddress = self.__hostname, ssl = ssl)
  693. except irclib.ServerConnectionError:
  694. pass
  695. def disconnect(self, message = 'oh no!'):
  696. ircbot.SingleServerIRCBot.disconnect(self, message)
  697. def error(self, error):
  698. traceback.print_exc()
  699. self.connection.privmsg(self.__nick, 'an error occured')
  700. def get_version(self):
  701. return 'locationbot ' + sys.platform
  702. def on_kick(self, connection, event):
  703. nick = event.arguments()[0]
  704. if not self.__channel(nick, event.target()):
  705. self.__logins.pop(nick, None)
  706. def on_nick(self, connection, event):
  707. nickmask = event.source()
  708. login = self.__logins.pop(irclib.nm_to_n(nickmask), (None,))[0]
  709. if login is not None:
  710. nick = event.target()
  711. self.__logins[nick] = (login, nick + '!' + irclib.nm_to_uh(nickmask))
  712. def on_nicknameinuse(self, connection, event):
  713. connection.nick(connection.get_nickname() + '_')
  714. def on_part(self, connection, event):
  715. nick = irclib.nm_to_n(event.source())
  716. if not self.__channel(nick, event.target()):
  717. self.__logins.pop(nick, None)
  718. def on_privmsg(self, connection, event):
  719. nickmask = event.source()
  720. nick = irclib.nm_to_n(nickmask)
  721. admin = self.__admin(nickmask)
  722. if not admin and not self.__channel(nick):
  723. return
  724. try:
  725. login = self.__login(None, nickmask, nick)
  726. try:
  727. command, arguments = event.arguments()[0].split(None, 1)
  728. except ValueError:
  729. command = event.arguments()[0].strip()
  730. arguments = ''
  731. command = irclib.irc_lower(command.lstrip('!'))
  732. if command == 'help':
  733. self.__help(connection, nick, admin, login, arguments)
  734. elif command == 'login':
  735. self.__login(connection, nickmask, nick, arguments)
  736. elif command == 'status':
  737. self.__status(connection, nick, login, arguments)
  738. elif not login and command == 'register':
  739. self.__register(connection, nick, arguments)
  740. elif login and command == 'logout':
  741. self.__logout(connection, nick)
  742. elif login and command == 'set':
  743. self.__set(connection, nickmask, nick, login, arguments)
  744. elif login and command == 'unset':
  745. self.__unset(connection, nick, login, arguments)
  746. elif admin and command == 'join':
  747. self.__join(connection, nick, arguments)
  748. elif admin and command == 'part':
  749. self.__part(connection, nick, arguments)
  750. elif admin and command == 'quit':
  751. self.__quit(connection, nick, arguments)
  752. elif admin and command == 'reload':
  753. self.__reload(connection, nick)
  754. elif admin and command == 'restart':
  755. self.__restart(connection)
  756. elif admin and command == 'say':
  757. self.__say(connection, nick, arguments)
  758. elif admin and command == 'who':
  759. self.__who(connection, nick)
  760. else:
  761. self.__unknown(connection, nick, command)
  762. except Exception, error:
  763. traceback.print_exc()
  764. connection.privmsg(nick, 'an error occurred')
  765. def on_quit(self, connection, event):
  766. self.__logins.pop(irclib.nm_to_n(event.source()), None)
  767. def on_welcome(self, connection, event):
  768. for channel in self.__channels:
  769. connection.join(channel)
  770. def start(self):
  771. latitude_thread = threading.Thread(None, self.__latitude)
  772. latitude_thread.daemon = True
  773. latitude_thread.start()
  774. services = [self.geocoder]
  775. for service in services:
  776. service.start()
  777. if not self.__reloading:
  778. self._connect()
  779. else:
  780. self.__reloading = False
  781. for channel in self.__channels.symmetric_difference(self.channels.keys()):
  782. if channel in self.__channels:
  783. self.connection.join(channel)
  784. else:
  785. self.connection.part(channel)
  786. ping_next = datetime.utcnow() + timedelta(minutes = 1)
  787. while not self.__reloading:
  788. try:
  789. now = datetime.utcnow()
  790. if now >= ping_next:
  791. self.connection.ping(self.connection.server)
  792. ping_next = now + timedelta(minutes = 1)
  793. if self.__locations_lock.acquire(False):
  794. if self.__locations and self.__channels.issubset(self.channels.keys()):
  795. for nick, channel, granularity, location, coordinates, accuracy, speed, heading, altitude, altitude_accuracy in self.__locations:
  796. aux = []
  797. if accuracy is not None:
  798. aux.append('accuracy: ' + self.__distance(accuracy))
  799. if speed is not None:
  800. aux.append('speed: ' + self.__speed(speed))
  801. if heading is not None:
  802. aux.append(u'heading: ' + self.__heading(heading))
  803. if altitude is not None:
  804. aux.append('altitude: ' + self.__distance(altitude))
  805. if altitude_accuracy is not None:
  806. aux.append('altitude accuracy: ' + self.__distance(altitude_accuracy))
  807. if aux:
  808. aux = ' [%s]' % ', '.join(aux)
  809. else:
  810. aux = ''
  811. self.connection.notice(channel, '%s is in %s%s %s' % (nick, _encode(location), aux, self.__url(nick, granularity, location, coordinates)))
  812. self.__locations = []
  813. self.__locations_lock.release()
  814. except irclib.ServerNotConnectedError:
  815. self.jump_server()
  816. self.ircobj.process_once(0.2)
  817. latitude_thread.join()
  818. with self.__latitude_timer_lock:
  819. if self.__latitude_timer is not None:
  820. self.__latitude_timer.cancel()
  821. self.__latitude_timer.join()
  822. for service in services:
  823. service.stop()
  824. return not self.__quiting
  825. def success(self):
  826. self.connection.privmsg(self.__nick, 'successfully reloaded')
  827. class _AddressMask(object):
  828. def __init__(self, function):
  829. self.function = function
  830. def __call__(self, nick, mask):
  831. if not self.function(nick, mask):
  832. nick, address = nick.split('@', 1)
  833. try:
  834. host = socket.gethostbyaddr(address)[0]
  835. except socket.herror:
  836. pass
  837. else:
  838. if host != address:
  839. return self.function(nick + '@' + host, mask)
  840. return False
  841. return True
  842. irclib.mask_matches = _AddressMask(reload(irclib).mask_matches)
  843. def _encode(string):
  844. string = unicode(string)
  845. try:
  846. return string.encode('latin1')
  847. except UnicodeEncodeError:
  848. return string.encode('utf8')
  849. def _meters_per_second_to_miles_per_hour(meters_per_second):
  850. return meters_per_second * 2.23693629
  851. def _meters_to_feet(meters):
  852. return meters * 3.2808399
  853. def _heading_to_direction(heading):
  854. heading %= 360
  855. if 348.75 < heading or heading <= 11.25:
  856. return 'N'
  857. elif 11.25 < heading <= 33.75:
  858. return 'NNE'
  859. elif 33.75 < heading <= 56.25:
  860. return 'NE'
  861. elif 56.25 < heading <= 78.75:
  862. return 'ENE'
  863. elif 78.75 < heading <= 101.25:
  864. return 'E'
  865. elif 101.25 < heading <= 123.75:
  866. return 'ESE'
  867. elif 123.75 < heading <= 146.25:
  868. return 'SE'
  869. elif 146.25 < heading <= 168.75:
  870. return 'SSE'
  871. elif 168.75 < heading <= 191.25:
  872. return 'S'
  873. elif 191.25 < heading <= 213.75:
  874. return 'SSW'
  875. elif 213.75 < heading <= 236.25:
  876. return 'SW'
  877. elif 236.25 < heading <= 258.75:
  878. return 'WSW'
  879. elif 258.75 < heading <= 281.25:
  880. return 'W'
  881. elif 281.25 < heading <= 303.75:
  882. return 'WNW'
  883. elif 303.75 < heading <= 326.25:
  884. return 'NW'
  885. else:
  886. return 'NNW'
  887. def _or(function, values):
  888. if values:
  889. for value in values:
  890. if function(value):
  891. return True
  892. return False
  893. if __name__ == '__main__':
  894. os.chdir(os.path.abspath(os.path.dirname(__file__)))
  895. pid = os.fork()
  896. if pid != 0:
  897. with open('locationbot.pid', 'w') as _file:
  898. _file.write('%u\n' % pid)
  899. sys.exit(0)
  900. sys.stdin = open('/dev/null')
  901. sys.stdout = open('locationbot.log', 'a', 1)
  902. sys.stderr = sys.stdout
  903. import locationbot
  904. bot = locationbot.LocationBot()
  905. try:
  906. while bot.start():
  907. import locationbot
  908. try:
  909. bot = reload(locationbot).LocationBot(bot)
  910. except (ImportError, SyntaxError), error:
  911. bot.error(error)
  912. else:
  913. bot.success()
  914. except KeyboardInterrupt:
  915. bot.disconnect()
  916. os.unlink('locationbot.pid')
  917. # vim: noexpandtab tabstop=4