locationbot.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887
  1. #!/usr/bin/env python
  2. # Location Bot
  3. #
  4. # Douglas Thrift
  5. #
  6. # $Id$
  7. from ConfigParser import NoOptionError, SafeConfigParser
  8. from datetime import datetime, timedelta
  9. import getpass
  10. import ircbot
  11. import irclib
  12. import oauth2 as oauth
  13. import os
  14. import psycopg2
  15. import psycopg2.extensions
  16. import pytz
  17. import re
  18. import sys
  19. import time
  20. import threading
  21. import urllib, urllib2
  22. import urlparse
  23. import warnings
  24. try:
  25. import simplejson as json
  26. except ImportError:
  27. import json
  28. class LocationBot(ircbot.SingleServerIRCBot):
  29. def __init__(self, bot = None):
  30. self.__config = SafeConfigParser()
  31. self.__config.read('locationbot.ini')
  32. try:
  33. nick = self.__config.sections()[0]
  34. except IndexError:
  35. sys.exit('No nick configured')
  36. servers = []
  37. try:
  38. for server in self.__config.get(nick, 'servers').split():
  39. server = server.split(':', 2)
  40. if len(server) == 1:
  41. servers.append((server[0], 6667))
  42. else:
  43. host = server[0]
  44. port = int(server[1])
  45. ssl = server[1].startswith('+')
  46. if len(server) == 3:
  47. servers.append((host, port, ssl, server[2]))
  48. else:
  49. servers.append((host, port, ssl))
  50. self.__admins = self.__config.get(nick, 'admins').split()
  51. self.__channels = set(self.__config.get(nick, 'channels').split())
  52. self.__dsn = self.__config.get(nick, 'dsn')
  53. self.__latitude_key = self.__config.get(nick, 'latitude_key')
  54. self.__latitude_consumer = oauth.Consumer(key = self.__latitude_key, secret = self.__config.get(nick, 'latitude_secret'))
  55. except NoOptionError, error:
  56. sys.exit(error)
  57. try:
  58. self.__hostname = self.__config.get(nick, 'hostname')
  59. except NoOptionError:
  60. self.__hostname = ''
  61. ircbot.SingleServerIRCBot.__init__(self, servers, nick, 'Location Bot')
  62. if bot is None:
  63. self.__geocode_cache = {}
  64. self.__locations = []
  65. self.__logins = {}
  66. self.__nick = None
  67. self.__reloading = False
  68. else:
  69. self.__geocode_cache = bot.__geocode_cache
  70. irclibobj = self.ircobj.connections[0].irclibobj
  71. self.ircobj.connections[0] = bot.ircobj.connections[0]
  72. self.ircobj.connections[0].irclibobj = irclibobj
  73. self.channels = bot.channels
  74. self.connection = bot.connection
  75. self.__locations = bot.__locations
  76. self.__logins = bot.__logins
  77. self.__nick = bot.__nick
  78. self.__reloading = True
  79. self.__geocode_cache_lock = threading.Lock()
  80. self.__geocode_cache_clean_timer_lock = threading.Lock()
  81. self.__geocode_cache_clean_timer = None
  82. self.__latitude_granularities = frozenset(['city', 'best'])
  83. self.__latitude_timer_lock = threading.Lock()
  84. self.__latitude_timer = None
  85. self.__locations_lock = threading.Lock()
  86. self.__quiting = False
  87. self.__timeout = 5
  88. self.__variables = frozenset(['nick', 'secret', 'masks', 'channels', 'timezone', 'location', 'coordinates', 'latitude'])
  89. self.__geocode_variables = self.__variables.intersection(['location', 'coordinates'])
  90. self.__lists = self.__variables.intersection(['masks', 'channels'])
  91. self.__unsetable = self.__variables.difference(['nick', 'secret'])
  92. def __admin(self, nickmask):
  93. return _or(lambda admin: irclib.mask_matches(nickmask, admin), self.__admins)
  94. def __channel(self, nick, exclude = None):
  95. if exclude is not None:
  96. exclude = irclib.irc_lower(exclude)
  97. channels = map(lambda channel: channel[1], filter(lambda channel: irclib.irc_lower(channel[0]) == exclude, self.channels))
  98. else:
  99. channels = self.channels.values()
  100. return _or(lambda channel: channel.has_user(nick), channels)
  101. def __db(self):
  102. db = psycopg2.connect(self.__dsn)
  103. def point(value, cursor):
  104. if value is not None:
  105. return tuple(map(lambda a: float(a), re.match(r'^\(([^)]+),([^)]+)\)$', value).groups()))
  106. psycopg2.extensions.register_type(psycopg2.extensions.new_type((600,), 'point', point), db)
  107. return db, db.cursor()
  108. def __geocode(self, sensor, coordinates = None, location = None):
  109. parameters = {'sensor': 'true' if sensor else 'false'}
  110. with self.__geocode_cache_lock:
  111. if coordinates is not None:
  112. try:
  113. return self.__geocode_cache[coordinates][0]
  114. except KeyError:
  115. parameters['latlng'] = '%f,%f' % coordinates
  116. else:
  117. try:
  118. return self.__geocode_cache[coordinates][0]
  119. except KeyError:
  120. parameters['address'] = location
  121. geocode = json.load(urllib2.urlopen('http://maps.google.com/maps/api/geocode/json?' + urllib.urlencode(parameters), timeout = self.__timeout))
  122. status = geocode['status']
  123. if status != 'OK':
  124. if coordinates is not None and status == 'ZERO_RESULTS':
  125. return self.__geocode(sensor, location = parameters['latlng'])
  126. else:
  127. raise Exception(status)
  128. results = geocode['results']
  129. def _result():
  130. _location = result['geometry']['location']
  131. geocode = (_location['lat'], _location['lng']), result['formatted_address']
  132. with self.__geocode_cache_lock:
  133. self.__geocode_cache[coordinates if coordinates is not None else location] = (geocode, datetime.utcnow())
  134. return geocode
  135. if coordinates is not None:
  136. types = frozenset([
  137. 'country',
  138. 'administrative_area_level_1',
  139. 'administrative_area_level_2',
  140. 'administrative_area_level_3',
  141. 'colloquial_area',
  142. 'locality',
  143. 'sublocality',
  144. 'neighborhood',
  145. ])
  146. for result in results:
  147. if not types.isdisjoint(result['types']):
  148. return _result()
  149. result = results[0]
  150. return _result()
  151. def __geocode_cache_clean(self):
  152. now = datetime.utcnow()
  153. try:
  154. while now < self.__geocode_cache_clean_next:
  155. time.sleep((self.__geocode_cache_clean_next - now).seconds)
  156. now = datetime.utcnow()
  157. except AttributeError:
  158. self.__geocode_cache_length = timedelta(hours = 1)
  159. self.__geocode_cache_clean_next = now.replace(minute = 2, second = 30, microsecond = 0) + self.__geocode_cache_length
  160. with self.__geocode_cache_lock:
  161. for location, (geocode, created) in self.__geocode_cache.items():
  162. if now - created >= self.__geocode_cache_length:
  163. del self.__geocode_cache[location]
  164. with self.__geocode_cache_clean_timer_lock:
  165. self.__geocode_cache_clean_timer = threading.Timer((self.__geocode_cache_clean_next - datetime.utcnow()).seconds, self.__latitude)
  166. self.__geocode_cache_clean_timer.start()
  167. def __help(self, connection, nick, admin, login, arguments):
  168. command = irclib.irc_lower(arguments.split(None, 1)[0].lstrip('!')) if arguments else None
  169. commands = {
  170. 'help': ('[command]', 'show this help message'),
  171. 'status': ('[nick]', 'show where everybody or a nick is'),
  172. }
  173. if not login:
  174. commands.update({
  175. 'login': ('[nick] [secret]', 'log in as nick with secret or using masks'),
  176. 'register': ('[nick] secret', 'register as nick with secret'),
  177. })
  178. else:
  179. commands.update({
  180. 'logout': ('', 'log out as nick'),
  181. 'set': ('[variable [value]]', 'display or set variables'),
  182. 'unset': ('variable', 'unset a variable'),
  183. })
  184. if admin:
  185. commands.update({
  186. 'join': ('channel', 'join a channel'),
  187. 'part': ('channel [message]', 'part from a channel'),
  188. 'quit': ('[message]', 'quit and do not come back'),
  189. 'reload': ('', 'reload with more up to date code'),
  190. 'restart': ('', 'quit and join running more up to date code'),
  191. 'say': ('nick|channel message', 'say message to nick or channel'),
  192. 'who': ('', 'show who is logged in'),
  193. })
  194. connection.privmsg(nick, '\x02command arguments description\x0f')
  195. def help(command, arguments, description):
  196. connection.privmsg(nick, '%-11s %-23s %s' % (command, arguments, description))
  197. if command in commands:
  198. help(command, *commands[command])
  199. else:
  200. for command, (arguments, description) in sorted(commands.iteritems()):
  201. help(command, arguments, description)
  202. def __join(self, connection, nick, arguments):
  203. try:
  204. channel = arguments.split(None, 1)[0]
  205. except IndexError:
  206. return self.__help(connection, nick, True, False, 'join')
  207. connection.join(channel)
  208. self.__channels.add(channel)
  209. self.__config.set(self._nickname, 'channels', ' '.join(self.__channels))
  210. self.__write()
  211. connection.privmsg(nick, 'successfully joined channel ("%s")' % channel)
  212. def __latitude(self, granularity = None, token = None, secret = None):
  213. if granularity is not None:
  214. 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')
  215. if int(response['status']) != 200:
  216. raise Exception(content.strip())
  217. data = json.loads(content)['data']
  218. coordinates = (data['latitude'], data['longitude'])
  219. 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]
  220. now = datetime.utcnow()
  221. try:
  222. while now < self.__latitude_next:
  223. time.sleep((self.__latitude_next - now).seconds)
  224. now = datetime.utcnow()
  225. except AttributeError:
  226. pass
  227. self.__latitude_next = now.replace(minute = now.minute - now.minute % 5, second = 0, microsecond = 0) + timedelta(minutes = 5)
  228. try:
  229. db, cursor = self.__db()
  230. cursor.execute('select nick, channels, location, latitude.granularity, token, latitude.secret from locationbot.nick join locationbot.latitude using (id)')
  231. for nick, channels, old_location, granularity, token, secret in cursor.fetchall():
  232. try:
  233. updated, coordinates, accuracy, speed, heading, altitude, altitude_accuracy, new_location = self.__latitude(granularity, token, secret)
  234. except Exception, error:
  235. print error
  236. continue
  237. cursor.execute('update locationbot.nick set granularity = %s, location = %s, coordinates = point %s, updated = %s where nick = %s', (granularity, new_location, coordinates, updated, nick))
  238. db.commit()
  239. self.__location(nick, channels, granularity, old_location, new_location, coordinates)
  240. except psycopg2.Error, error:
  241. print error
  242. with self.__latitude_timer_lock:
  243. self.__latitude_timer = threading.Timer((self.__latitude_next - datetime.utcnow()).seconds, self.__latitude)
  244. self.__latitude_timer.start()
  245. def __location(self, nick, channels, granularity, old_location, new_location, coordinates):
  246. if channels and new_location and new_location != old_location:
  247. with self.__locations_lock:
  248. for channel in self.__channels.intersection(channels):
  249. self.__locations.append((nick, channel, granularity, new_location, coordinates))
  250. def __login(self, connection, nickmask, nick, arguments = ''):
  251. login = nick
  252. if connection is not None:
  253. arguments = arguments.split(None, 1)
  254. if len(arguments) == 2:
  255. login, secret = arguments
  256. elif len(arguments) == 1:
  257. secret = arguments[0]
  258. else:
  259. secret = None
  260. else:
  261. secret = None
  262. if nick in self.__logins:
  263. login = self.__logins[nick][0]
  264. if connection is not None:
  265. return connection.privmsg(nick, 'already logged in as "%s"' % login)
  266. return login
  267. db, cursor = self.__db()
  268. def success():
  269. connection.privmsg(nick, 'successfully logged in as "%s"' % login)
  270. if secret is not None:
  271. cursor.execute('select true from locationbot.nick where nick = %s and secret = md5(%s)', (login, secret))
  272. if cursor.rowcount == 1:
  273. self.__logins[nick] = (login, nickmask)
  274. return success()
  275. cursor.execute('select nick, masks from locationbot.nick where nick in (%s, %s)', (login, secret if len(arguments) != 2 else None))
  276. for login, masks in cursor.fetchall():
  277. if _or(lambda mask: irclib.mask_matches(nickmask, mask), masks):
  278. self.__logins[nick] = (login, nickmask)
  279. return success() if connection else login
  280. if connection is not None:
  281. return connection.privmsg(nick, 'failed to log in as "%s"' % login)
  282. def __logout(self, connection, nick):
  283. connection.privmsg(nick, 'logged out as "%s"' % self.__logins.pop(nick)[0])
  284. def __part(self, connection, nick, arguments):
  285. arguments = arguments.split(None, 1)
  286. if len(arguments) == 2:
  287. channel, message = arguments
  288. message = ':' + message
  289. elif len(arguments) == 1:
  290. channel = arguments[0]
  291. message = ''
  292. else:
  293. return self.__help(connection, nick, True, False, 'part')
  294. if channel in self.__channels:
  295. connection.part(channel, message)
  296. self.__channels.remove(channel)
  297. self.__config.set(self._nickname, 'channels', ' '.join(self.__channels))
  298. self.__write()
  299. connection.privmsg(nick, 'successfully parted channel ("%s")' % channel)
  300. else:
  301. connection.privmsg(nick, 'not in channel ("%s")' % channel)
  302. def __quit(self, connection, nick, arguments):
  303. self.__reloading = True
  304. self.__quiting = True
  305. connection.privmsg(nick, 'quiting')
  306. self.disconnect(arguments)
  307. def __register(self, connection, nick, arguments):
  308. arguments = arguments.split(None, 1)
  309. if len(arguments) == 2:
  310. login, secret = arguments
  311. elif len(arguments) == 1:
  312. login = nick
  313. secret = arguments[0]
  314. else:
  315. return self.__help(connection, nick, False, False, 'register')
  316. db, cursor = self.__db()
  317. try:
  318. cursor.execute('insert into locationbot.nick (nick, secret) values (%s, md5(%s))', (login, secret))
  319. db.commit()
  320. except psycopg2.IntegrityError:
  321. return connection.privmsg(nick, 'nick ("%s") is already registered' % login)
  322. connection.privmsg(nick, 'nick ("%s") sucessfully registered' % login)
  323. def __reload(self, connection, nick):
  324. self.__nick = nick
  325. self.__reloading = True
  326. connection.privmsg(nick, 'reloading')
  327. def __restart(self, connection):
  328. connection.disconnect('restarting')
  329. os.execvp(sys.argv[0], sys.argv)
  330. def __say(self, connection, nick, arguments):
  331. try:
  332. nick_channel, message = arguments.split(None, 1)
  333. except ValueError:
  334. return self.__help(connection, nick, True, False, 'say')
  335. if irclib.is_channel(nick_channel):
  336. if nick_channel not in self.channels:
  337. return connection.privmsg(nick, 'not in channel ("%s")' % nick_channel)
  338. elif not self.__channel(nick_channel):
  339. return connection.privmsg(nick, 'nick ("%s") not in channel(s)' % nick_channel)
  340. elif nick_channel == connection.get_nickname():
  341. return connection.privmsg(nick, 'nice try')
  342. connection.privmsg(nick_channel, message)
  343. connection.privmsg(nick, 'successfully sent message ("%s") to nick/channel ("%s")' % (message, nick_channel))
  344. def __set(self, connection, nickmask, nick, login, arguments):
  345. arguments = arguments.split(None, 1)
  346. if len(arguments) == 2:
  347. variable, value = arguments
  348. elif len(arguments) == 1:
  349. variable = arguments[0]
  350. value = None
  351. else:
  352. variable = None
  353. value = None
  354. if variable is not None and variable not in self.__variables:
  355. return self.__unknown_variable(connection, nick, variable)
  356. db, cursor = self.__db()
  357. if value is None:
  358. variables = sorted(self.__variables) if variable is None else [variable]
  359. cursor.execute('select ' + ', '.join(map(lambda variable: "'%s'" % ('*' * 8) if variable == 'secret' else 'latitude.granularity, latitude.authorized' if variable == 'latitude' else variable, variables)) + ' from locationbot.nick join locationbot.latitude using (id) where nick = %s', (login,))
  360. values = list(cursor.fetchone())
  361. try:
  362. index = variables.index('latitude')
  363. values[index:index + 2] = ['%s (%s)' % (values[index], 'authorized' if values[index + 1] else 'unauthorized')]
  364. except ValueError:
  365. pass
  366. connection.privmsg(nick, '\x02variable value\x0f')
  367. for variable, value in zip(variables, values):
  368. connection.privmsg(nick, '%-11s %s' % (variable, ' '.join(value) if isinstance(value, list) else '%f,%f' % value if isinstance(value, tuple) else value))
  369. else:
  370. def invalid(value, variable = variable):
  371. connection.privmsg(nick, 'invalid %s ("%s")' % (variable, value))
  372. if variable in self.__lists:
  373. value = value.split()
  374. if variable == 'channels':
  375. for channel in value:
  376. if not irclib.is_channel(channel) or channel not in self.__channels:
  377. return invalid(channel, 'channel')
  378. elif variable == 'masks':
  379. _mask = re.compile('^.+!.+@.+$')
  380. for mask in value:
  381. if not _mask.match(mask):
  382. return invalid(mask, 'mask')
  383. elif variable == 'latitude':
  384. if value in self.__latitude_granularities:
  385. response, content = oauth.Client(self.__latitude_consumer, timeout = self.__timeout).request('https://www.google.com/accounts/OAuthGetRequestToken', 'POST', urllib.urlencode({
  386. 'scope': 'https://www.googleapis.com/auth/latitude',
  387. 'oauth_callback': 'oob',
  388. }))
  389. if int(response['status']) != 200:
  390. raise Exception(content.strip())
  391. authorized = False
  392. else:
  393. 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,))
  394. if cursor.rowcount == 0:
  395. return invalid(value)
  396. channels, old_location, granularity, token, secret = cursor.fetchone()
  397. token = oauth.Token(token, secret)
  398. token.set_verifier(value)
  399. response, content = oauth.Client(self.__latitude_consumer, token, timeout = self.__timeout).request('https://www.google.com/accounts/OAuthGetAccessToken', 'GET')
  400. status = int(response['status'])
  401. if status == 400:
  402. return invalid(value)
  403. elif status != 200:
  404. raise Exception(content.strip())
  405. authorized = True
  406. data = dict(urlparse.parse_qsl(content))
  407. token = data['oauth_token']
  408. secret = data['oauth_token_secret']
  409. if not authorized:
  410. connection.privmsg(nick, 'go to https://www.google.com/latitude/apps/OAuthAuthorizeToken?' + urllib.urlencode({
  411. 'domain': self.__latitude_key,
  412. 'granularity': value,
  413. 'oauth_token': token,
  414. }))
  415. elif variable in self.__geocode_variables:
  416. cursor.execute('select channels, location, latitude.granularity, token, latitude.secret, authorized from locationbot.nick left join locationbot.latitude using (id) where nick = %s', (login,))
  417. channels, old_location, granularity, token, secret, authorized = cursor.fetchone()
  418. if variable == 'location':
  419. coordinates = None
  420. granularity = 'city'
  421. location = value
  422. else:
  423. coordinates = value.split(None, 1)
  424. if len(coordinates) == 1:
  425. coordinates = coordinates[0].split(',', 1)
  426. try:
  427. coordinates = tuple(map(lambda a: float(a), coordinates))
  428. except ValueError:
  429. return invalid(value)
  430. for coordinate in coordinates:
  431. if not -180.0 <= coordinate <= 180.0:
  432. return invalid(value)
  433. location = None
  434. geocode = self.__geocode(False, coordinates, location)
  435. new_location = geocode[1]
  436. if variable == 'location':
  437. coordinates = geocode[0]
  438. value = new_location
  439. else:
  440. value = coordinates
  441. if authorized:
  442. 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': {
  443. 'kind': 'latitude#location',
  444. 'latitude': coordinates[0],
  445. 'longitude': coordinates[1],
  446. }}), {'Content-Type': 'application/json'})
  447. if int(response['status']) != 200:
  448. raise Exception(content.strip())
  449. elif variable == 'nick':
  450. _nick = value.split(None, 1)
  451. if len(_nick) != 1:
  452. return invalid(value)
  453. elif variable == 'timezone':
  454. if value not in pytz.all_timezones_set:
  455. return invalid(value)
  456. try:
  457. if variable in self.__geocode_variables:
  458. cursor.execute('update locationbot.nick set granularity = %s, location = %s, coordinates = point %s, updated = now() where nick = %s', (granularity, new_location, coordinates, login))
  459. elif variable == 'latitude':
  460. if authorized:
  461. 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))
  462. else:
  463. cursor.execute('delete from locationbot.latitude using locationbot.nick where latitude.id = nick.id and nick = %s', (login,))
  464. 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))
  465. else:
  466. cursor.execute('update locationbot.nick set ' + variable + ' = ' + ('md5(%s)' if variable == 'secret' else '%s') + ' where nick = %s', (value, login))
  467. db.commit()
  468. except psycopg2.IntegrityError:
  469. if variable == 'nick':
  470. return connection.privmsg(nick, 'nick ("%s") is already registered' % value)
  471. raise
  472. 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))
  473. if variable == 'nick':
  474. self.__logins[nick] = (value, nickmask)
  475. elif variable in self.__geocode_variables or variable == 'latitude' and authorized:
  476. if variable == 'latitude':
  477. updated, coordinates, accuracy, speed, heading, altitude, altitude_accuracy, new_location = self.__latitude(granularity, token, secret)
  478. cursor.execute('update locationbot.nick set granularity = %s, location = %s, coordinates = point %s, updated = %s where nick = %s', (granularity, new_location, coordinates, updated, login))
  479. self.__location(login, channels, granularity, old_location, new_location, coordinates)
  480. def __status(self, connection, nick, login, arguments):
  481. _nick = arguments.split(None, 1)[0] if arguments else None
  482. db, cursor = self.__db()
  483. cursor.execute('select nick, location, coordinates, updated from locationbot.nick where ' + ('nick = %s and ' if _nick is not None else '') + 'location is not null order by updated desc', (_nick,))
  484. if cursor.rowcount == 0:
  485. return connection.privmsg(nick, 'no location information for ' + ('"%s"' % _nick if _nick is not None else 'anybody'))
  486. locations = cursor.fetchall()
  487. if login is not None:
  488. cursor.execute('select timezone from locationbot.nick where nick = %s and timezone is not null', (login,))
  489. timezone = pytz.timezone(cursor.fetchone()[0]) if cursor.rowcount == 1 else pytz.utc
  490. else:
  491. timezone = pytz.utc
  492. connection.privmsg(nick, '\x02nick location when map\x0f')
  493. for _nick, location, coordinates, updated in locations:
  494. connection.privmsg(nick, '%-23s %-35s %-23s %s' % (_nick, location, timezone.normalize(updated.astimezone(timezone)).strftime('%Y-%m-%d %H:%M %Z'), self.__url(_nick, 'best', location, coordinates)))
  495. def __unknown(self, connection, nick, command):
  496. connection.privmsg(nick, 'unknown command ("%s"); try "help"' % command)
  497. def __unknown_variable(self, connection, nick, variable):
  498. connection.privmsg(nick, 'unknown variable ("%s")' % variable)
  499. def __unset(self, connection, nick, login, arguments):
  500. try:
  501. variable = irclib.irc_lower(arguments.split(None, 1)[0])
  502. except IndexError:
  503. return self.__help(connection, nick, False, login, 'unset')
  504. if variable not in self.__unsetable:
  505. if variable in self.__variables:
  506. return connection.privmsg(nick, 'variable ("%s") is not unsetable' % variable)
  507. return self.__unknown_variable(connection, nick, variable)
  508. db, cursor = self.__db()
  509. 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,))
  510. db.commit()
  511. connection.privmsg(nick, 'variable ("%s") %s unset' % (variable, 'successfuly' if cursor.rowcount == 1 else 'already'))
  512. def __url(self, nick, granularity, location, coordinates):
  513. if granularity == 'best':
  514. location = '%f,%f' % coordinates
  515. return 'http://maps.google.com/maps?' + urllib.urlencode({'q': '%s (%s)' % (location, nick)})
  516. def __who(self, connection, nick):
  517. if self.__logins:
  518. connection.privmsg(nick, '\x02login nick nick mask\x0f')
  519. for login in sorted(self.__logins.values()):
  520. connection.privmsg(nick, '%-23s %s' % login)
  521. else:
  522. connection.privmsg(nick, 'nobody logged in')
  523. def __write(self):
  524. with open('locationbot.ini', 'w') as config:
  525. self.__config.write(config)
  526. def _connect(self):
  527. if len(self.server_list[0]) != 2:
  528. ssl = self.server_list[0][2]
  529. else:
  530. ssl = False
  531. if len(self.server_list[0]) == 4:
  532. password = self.server_list[0][3]
  533. else:
  534. password = None
  535. try:
  536. with warnings.catch_warnings():
  537. warnings.filterwarnings('ignore', r'socket\.ssl\(\) is deprecated\. Use ssl\.wrap_socket\(\) instead\.', DeprecationWarning)
  538. self.connect(self.server_list[0][0], self.server_list[0][1], self._nickname, password, getpass.getuser(), self._realname, localaddress = self.__hostname, ssl = ssl)
  539. except irclib.ServerConnectionError:
  540. pass
  541. def disconnect(self, message = 'oh no!'):
  542. ircbot.SingleServerIRCBot.disconnect(self, message)
  543. def error(self, error):
  544. print error
  545. self.connection.privmsg(self.__nick, 'an error occured')
  546. def get_version(self):
  547. return 'locationbot ' + sys.platform
  548. def on_kick(self, connection, event):
  549. nick = event.arguments()[0]
  550. if not self.__channel(nick, event.target()):
  551. self.__logins.pop(nick, None)
  552. def on_nick(self, connection, event):
  553. nickmask = event.source()
  554. login = self.__logins.pop(irclib.nm_to_n(nickmask), (None,))[0]
  555. if login is not None:
  556. nick = event.target()
  557. self.__logins[nick] = (login, nick + '!' + nm_to_uh(nickmask))
  558. def on_nicknameinuse(self, connection, event):
  559. connection.nick(connection.get_nickname() + '_')
  560. def on_part(self, connection, event):
  561. nick = irclib.nm_to_n(event.source())
  562. if not self.__channel(nick, event.target()):
  563. self.__logins.pop(nick, None)
  564. def on_privmsg(self, connection, event):
  565. nickmask = event.source()
  566. nick = irclib.nm_to_n(nickmask)
  567. admin = self.__admin(nickmask)
  568. if not admin and not self.__channel(nick):
  569. return
  570. try:
  571. login = self.__login(None, nickmask, nick)
  572. try:
  573. command, arguments = event.arguments()[0].split(None, 1)
  574. except ValueError:
  575. command = event.arguments()[0].strip()
  576. arguments = ''
  577. command = irclib.irc_lower(command.lstrip('!'))
  578. if command == 'help':
  579. self.__help(connection, nick, admin, login, arguments)
  580. elif command == 'login':
  581. self.__login(connection, nickmask, nick, arguments)
  582. elif command == 'status':
  583. self.__status(connection, nick, login, arguments)
  584. elif not login and command == 'register':
  585. self.__register(connection, nick, arguments)
  586. elif login and command == 'logout':
  587. self.__logout(connection, nick)
  588. elif login and command == 'set':
  589. self.__set(connection, nickmask, nick, login, arguments)
  590. elif login and command == 'unset':
  591. self.__unset(connection, nick, login, arguments)
  592. elif admin and command == 'join':
  593. self.__join(connection, nick, arguments)
  594. elif admin and command == 'part':
  595. self.__part(connection, nick, arguments)
  596. elif admin and command == 'quit':
  597. self.__quit(connection, nick, arguments)
  598. elif admin and command == 'reload':
  599. self.__reload(connection, nick)
  600. elif admin and command == 'restart':
  601. self.__restart(connection)
  602. elif admin and command == 'say':
  603. self.__say(connection, nick, arguments)
  604. elif admin and command == 'who':
  605. self.__who(connection, nick)
  606. else:
  607. self.__unknown(connection, nick, command)
  608. except Exception, error:
  609. print error
  610. connection.privmsg(nick, 'an error occurred')
  611. def on_quit(self, connection, event):
  612. self.__logins.pop(irclib.nm_to_n(event.source()), None)
  613. def on_welcome(self, connection, event):
  614. for channel in self.__channels:
  615. connection.join(channel)
  616. def start(self):
  617. latitude_thread = threading.Thread(None, self.__latitude)
  618. latitude_thread.daemon = True
  619. latitude_thread.start()
  620. geocode_cache_clean_thread = threading.Thread(None, self.__geocode_cache_clean)
  621. geocode_cache_clean_thread.daemon = True
  622. geocode_cache_clean_thread.start()
  623. if not self.__reloading:
  624. self._connect()
  625. else:
  626. self.__reloading = False
  627. for channel in self.__channels.symmetric_difference(self.channels.keys()):
  628. if channel in self.__channels:
  629. self.connection.join(channel)
  630. else:
  631. self.connection.part(channel)
  632. while not self.__reloading:
  633. if self.__locations_lock.acquire(False):
  634. if self.__locations and self.__channels.issubset(self.channels.keys()):
  635. for nick, channel, granularity, location, coordinates in self.__locations:
  636. self.connection.notice(channel, '%s is in %s %s' % (nick, location, self.__url(nick, granularity, location, coordinates)))
  637. self.__locations = []
  638. self.__locations_lock.release()
  639. self.ircobj.process_once(0.2)
  640. latitude_thread.join()
  641. geocode_cache_clean_thread.join()
  642. with self.__latitude_timer_lock:
  643. if self.__latitude_timer is not None:
  644. self.__latitude_timer.cancel()
  645. self.__latitude_timer.join()
  646. with self.__geocode_cache_clean_timer_lock:
  647. if self.__geocode_cache_clean_timer is not None:
  648. self.__geocode_cache_clean_timer.cancel()
  649. self.__geocode_cache_clean_timer.join()
  650. return not self.__quiting
  651. def success(self):
  652. self.connection.privmsg(self.__nick, 'successfully reloaded')
  653. def _or(function, values):
  654. for value in values:
  655. if function(value):
  656. return True
  657. return False
  658. if __name__ == '__main__':
  659. os.chdir(os.path.abspath(os.path.dirname(__file__)))
  660. pid = os.fork()
  661. if pid != 0:
  662. with open('locationbot.pid', 'w') as _file:
  663. _file.write('%u\n' % pid)
  664. sys.exit(0)
  665. sys.stdin = open('/dev/null')
  666. sys.stdout = open('locationbot.log', 'a', 1)
  667. sys.stderr = sys.stdout
  668. import locationbot
  669. bot = locationbot.LocationBot()
  670. try:
  671. while bot.start():
  672. import locationbot
  673. try:
  674. bot = reload(locationbot).LocationBot(bot)
  675. except (ImportError, SyntaxError), error:
  676. bot.error(error)
  677. else:
  678. bot.success()
  679. except KeyboardInterrupt:
  680. bot.disconnect()
  681. os.unlink('locationbot.pid')