locationbot.py 38 KB

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