locationbot.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  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 os
  13. import psycopg2
  14. import pytz
  15. import sys
  16. import time
  17. import threading
  18. import urllib, urllib2
  19. import warnings
  20. try:
  21. import simplejson as json
  22. except ImportError:
  23. import json
  24. class LocationBot(ircbot.SingleServerIRCBot):
  25. def __init__(self, bot = None):
  26. config = SafeConfigParser({'hostname': ''})
  27. config.read('locationbot.ini')
  28. try:
  29. nick = config.sections()[0]
  30. except IndexError:
  31. sys.exit('No nick configured')
  32. servers = []
  33. try:
  34. for server in config.get(nick, 'servers').split():
  35. server = server.split(':', 2)
  36. if len(server) == 1:
  37. servers.append((server[0], 6667))
  38. else:
  39. host = server[0]
  40. port = int(server[1])
  41. ssl = server[1].startswith('+')
  42. if len(server) == 3:
  43. servers.append((host, port, ssl, server[2]))
  44. else:
  45. servers.append((host, port, ssl))
  46. self.__admins = config.get(nick, 'admins').split()
  47. self.__channels = config.get(nick, 'channels').split()
  48. self.__dsn = config.get(nick, 'dsn')
  49. self.__hostname = config.get(nick, 'hostname')
  50. except NoOptionError, error:
  51. sys.exit(error)
  52. ircbot.SingleServerIRCBot.__init__(self, servers, nick, 'Location Bot')
  53. self.__latitude_timer_lock = threading.Lock()
  54. self.__latitude_timer = None
  55. self.__locations_lock = threading.Lock()
  56. if bot is None:
  57. self.__locations = []
  58. self.__logins = {}
  59. self.__reloading = False
  60. else:
  61. irclibobj = self.ircobj.connections[0].irclibobj
  62. self.ircobj.connections[0] = bot.ircobj.connections[0]
  63. self.ircobj.connections[0].irclibobj = irclibobj
  64. self.channels = bot.channels
  65. self.connection = bot.connection
  66. self.__locations = bot.__locations
  67. self.__logins = bot.__logins
  68. self.__reloading = True
  69. self.__variables = frozenset(['nick', 'secret', 'masks', 'channels', 'timezone', 'location', 'latitude'])
  70. self.__unsetable = self.__variables.difference(['nick', 'secret'])
  71. def __admin(self, nickmask):
  72. return _or(lambda admin: irclib.mask_matches(nickmask, admin), self.__admins)
  73. def __db(self):
  74. db = psycopg2.connect(self.__dsn)
  75. return db, db.cursor()
  76. def __channel(self, nick, exclude = None):
  77. if exclude is not None:
  78. exclude = irclib.irc_lower(exclude)
  79. channels = map(lambda channel: channel[1], filter(lambda channel: irclib.irc_lower(channel[0]) == exclude, self.channels))
  80. else:
  81. channels = self.channels.values()
  82. return _or(lambda channel: channel.has_user(nick), channels)
  83. def __help(self, connection, nick, admin, login, arguments):
  84. command = irclib.irc_lower(arguments.split(None, 1)[0].lstrip('!')) if arguments else None
  85. commands = {
  86. 'help': ('[command]', 'show this help message'),
  87. 'status': ('[nick]', 'show where everybody or a nick is'),
  88. }
  89. if not login:
  90. commands.update({
  91. 'login': ('[nick] [secret]', 'log in as nick with secret or using masks'),
  92. 'register': ('[nick] secret', 'register as nick with secret'),
  93. })
  94. else:
  95. commands.update({
  96. 'logout': ('', 'log out as nick'),
  97. 'set': ('[variable [value]]', 'display or set variables'),
  98. 'unset': ('variable', 'unset a variable'),
  99. })
  100. if admin:
  101. commands.update({
  102. 'reload': ('', 'reload with more up to date code'),
  103. 'restart': ('', 'quit and join running more up to date code'),
  104. 'say': ('nick|channel message', 'say message to nick or channel'),
  105. 'who': ('', 'show who is logged in'),
  106. })
  107. connection.privmsg(nick, 'Command Arguments Description')
  108. def help(command, arguments, description):
  109. connection.privmsg(nick, '%-11s %-23s %s' % (command, arguments, description))
  110. if command in commands:
  111. help(command, *commands[command])
  112. else:
  113. for command, (arguments, description) in sorted(commands.iteritems()):
  114. help(command, arguments, description)
  115. def __latitude(self):
  116. now = datetime.utcnow()
  117. try:
  118. while now < self.__latitude_next:
  119. time.sleep((self.__latitude_next - now).seconds)
  120. now = datetime.utcnow()
  121. except AttributeError:
  122. pass
  123. self.__latitude_next = now.replace(minute = now.minute - now.minute % 5, second = 0, microsecond = 0) + timedelta(minutes = 5)
  124. try:
  125. db, cursor = self.__db()
  126. cursor.execute('select nick, channels, location, latitude from locationbot.nick where latitude is not null order by latitude')
  127. locations = {}
  128. for nick, channels, old_location, latitude in cursor.fetchall():
  129. new_location = locations.get(latitude)
  130. if latitude not in locations:
  131. url = 'http://www.google.com/latitude/apps/badge/api?' + urllib.urlencode({'user': latitude, 'type': 'json'})
  132. try:
  133. response = urllib2.urlopen(url)
  134. except urllib2.URLError, error:
  135. print error
  136. continue
  137. try:
  138. geo = json.load(response)
  139. except (TypeError, ValueError), error:
  140. print error
  141. continue
  142. try:
  143. properties = geo['features'][0]['properties']
  144. new_location = properties['reverseGeocode']
  145. locations[latitude] = new_location
  146. updated = datetime.fromtimestamp(properties['timeStamp'], pytz.utc)
  147. except (IndexError, KeyError), error:
  148. print error
  149. continue
  150. cursor.execute('update locationbot.nick set location = %s, updated = %s where latitude = %s', (new_location, updated, latitude))
  151. db.commit()
  152. if channels and new_location and new_location != old_location:
  153. with self.__locations_lock:
  154. for channel in frozenset(channels).intersection(self.__channels):
  155. self.__locations.append((nick, channel, locations[latitude]))
  156. except psycopg2.Error, error:
  157. print error
  158. with self.__latitude_timer_lock:
  159. self.__latitude_timer = threading.Timer((self.__latitude_next - datetime.utcnow()).seconds, self.__latitude)
  160. self.__latitude_timer.start()
  161. def __login(self, connection, nickmask, nick, arguments = ''):
  162. login = nick
  163. if connection is not None:
  164. arguments = arguments.split(None, 1)
  165. if len(arguments) == 2:
  166. login, secret = arguments
  167. elif len(arguments) == 1:
  168. secret = arguments[0]
  169. else:
  170. secret = None
  171. else:
  172. secret = None
  173. if nick in self.__logins:
  174. login = self.__logins[nick][0]
  175. if connection is not None:
  176. return connection.privmsg(nick, 'already logged in as "%s"' % login)
  177. return login
  178. db, cursor = self.__db()
  179. def success():
  180. connection.privmsg(nick, 'successfully logged in as "%s"' % login)
  181. if secret is not None:
  182. cursor.execute('select true from locationbot.nick where nick = %s and secret = md5(%s)', (login, secret))
  183. if cursor.rowcount == 1:
  184. self.__logins[nick] = (login, nickmask)
  185. return success()
  186. cursor.execute('select nick, masks from locationbot.nick where nick in (%s, %s)', (login, secret if len(arguments) != 2 else None))
  187. for login, masks in cursor.fetchall():
  188. if _or(lambda mask: irclib.mask_matches(nickmask, mask), masks):
  189. self.__logins[nick] = (login, nickmask)
  190. return success() if connection else login
  191. if connection is not None:
  192. return connection.privmsg(nick, 'failed to log in as "%s"' % login)
  193. def __logout(self, connection, nick):
  194. connection.privmsg(nick, 'logged out as "%s"' % self.__logins.pop(nick)[0])
  195. def __register(self, connection, nick, arguments):
  196. arguments = arguments.split(None, 1)
  197. if len(arguments) == 2:
  198. login, secret = arguments
  199. elif len(arguments) == 1:
  200. login = nick
  201. secret = arguments[0]
  202. else:
  203. return self.__help(connection, nick, False, False, 'register')
  204. db, cursor = self.__db()
  205. try:
  206. cursor.execute('insert into locationbot.nick (nick, secret) values (%s, md5(%s))', (login, secret))
  207. db.commit()
  208. except psycopg2.IntegrityError:
  209. return connection.privmsg(nick, 'nick ("%s") is already registered' % login)
  210. connection.privmsg(nick, 'nick ("%s") sucessfully registered' % login)
  211. def __reload(self, connection, nick):
  212. self.__reloading = True
  213. connection.privmsg(nick, 'reloading')
  214. def __restart(self, connection):
  215. connection.disconnect('restarting')
  216. os.execvp(sys.argv[0], sys.argv)
  217. def __say(self, connection, nick, arguments):
  218. try:
  219. nick_channel, message = arguments.split(None, 1)
  220. except ValueError:
  221. return self.__help(connection, nick, True, False, 'say')
  222. if irclib.is_channel(nick_channel):
  223. if nick_channel not in self.channels:
  224. return connection.privmsg(nick, 'not in channel ("%s")' % nick_channel)
  225. elif not self.__channel(nick_channel):
  226. return connection.privmsg(nick, 'nick ("%s") not in channel(s)' % nick_channel)
  227. elif nick_channel == connection.get_nickname():
  228. return connection.privmsg(nick, 'nice try')
  229. connection.privmsg(nick_channel, message)
  230. def __status(self, connection, nick, login, arguments):
  231. _nick = arguments.split(None, 1)[0] if arguments else None
  232. db, cursor = self.__db()
  233. cursor.execute('select nick, location, updated from locationbot.nick where ' + ('nick = %s and ' if _nick is not None else '') + 'location is not null order by updated desc', (_nick,))
  234. if cursor.rowcount == 0:
  235. return connection.privmsg(nick, 'no location information for ' + ('"%s"' % _nick if _nick is not None else 'anybody'))
  236. locations = cursor.fetchall()
  237. if login is not None:
  238. cursor.execute('select timezone from locationbot.nick where nick = %s and timezone is not null', (login,))
  239. timezone = pytz.timezone(cursor.fetchone()[0]) if cursor.rowcount == 1 else pytz.utc
  240. else:
  241. timezone = pytz.utc
  242. connection.privmsg(nick, 'Nick Location When')
  243. for _nick, location, updated in locations:
  244. connection.privmsg(nick, '%-23s %-36s %s' % (_nick, location, timezone.normalize(updated.astimezone(timezone)).strftime('%Y-%m-%d %H:%M %Z')))
  245. def __unknown(self, connection, nick, command):
  246. connection.privmsg(nick, 'unknown command ("%s"); try "help"' % command)
  247. def __unknown_variable(self, connection, nick, variable):
  248. connection.privmsg(nick, 'unknown variable ("%s")' % variable)
  249. def __unset(self, connection, nick, login, arguments):
  250. try:
  251. variable = irclib.irc_lower(arguments.split(None, 1)[0])
  252. except IndexError:
  253. return self.__help(connection, nick, False, login, 'unset')
  254. if variable not in self.__unsetable:
  255. if variable in self.__variables:
  256. return connection.privmsg(nick, 'variable ("%s") is not unsetable' % variable)
  257. return self.__unknown_variable(connection, nick, variable)
  258. db, cursor = self.__db()
  259. cursor.execute('update locationbot.nick set ' + variable + ' = null' + (', updated = null' if variable == 'location' else '') + ' where nick = %s and ' + variable + ' is not null', (login,))
  260. db.commit()
  261. connection.privmsg(nick, 'variable ("' + variable + '") ' + ('' if cursor.rowcount == 1 else 'already ') + 'unset')
  262. def __who(self, connection, nick):
  263. connection.privmsg(nick, 'Login Nick Nick Mask')
  264. for login in sorted(self.__logins.values()):
  265. connection.privmsg(nick, '%-23s %s' % login)
  266. def _connect(self):
  267. if len(self.server_list[0]) != 2:
  268. ssl = self.server_list[0][2]
  269. else:
  270. ssl = False
  271. if len(self.server_list[0]) == 4:
  272. password = self.server_list[0][3]
  273. else:
  274. password = None
  275. try:
  276. with warnings.catch_warnings():
  277. warnings.filterwarnings('ignore', r'socket\.ssl\(\) is deprecated\. Use ssl\.wrap_socket\(\) instead\.', DeprecationWarning)
  278. self.connect(self.server_list[0][0], self.server_list[0][1], self._nickname, password, getpass.getuser(), self._realname, localaddress = self.__hostname, ssl = ssl)
  279. except irclib.ServerConnectionError:
  280. pass
  281. def get_version(self):
  282. return 'locationbot ' + sys.platform
  283. def on_kick(self, connection, event):
  284. nick = event.arguments()[0]
  285. if not self.__channel(nick, event.target()):
  286. self.__logins.pop(nick, None)
  287. def on_nick(self, connection, event):
  288. nickmask = event.source()
  289. login = self.__logins.pop(irclib.nm_to_n(nickmask), (None,))[0]
  290. if login is not None:
  291. nick = event.target()
  292. self.__logins[nick] = (login, nick + '!' + nm_to_uh(nickmask))
  293. def on_nicknameinuse(self, connection, event):
  294. connection.nick(connection.get_nickname() + '_')
  295. def on_part(self, connection, event):
  296. nick = irclib.nm_to_n(event.source())
  297. if not self.__channel(nick, event.target()):
  298. self.__logins.pop(nick, None)
  299. def on_privmsg(self, connection, event):
  300. nickmask = event.source()
  301. nick = irclib.nm_to_n(nickmask)
  302. admin = self.__admin(nickmask)
  303. if not admin and not self.__channel(nick):
  304. return
  305. try:
  306. login = self.__login(None, nickmask, nick)
  307. try:
  308. command, arguments = event.arguments()[0].split(None, 1)
  309. except ValueError:
  310. command = event.arguments()[0].strip()
  311. arguments = ''
  312. command = irclib.irc_lower(command.lstrip('!'))
  313. if command == 'help':
  314. self.__help(connection, nick, admin, login, arguments)
  315. elif command == 'login':
  316. self.__login(connection, nickmask, nick, arguments)
  317. elif command == 'status':
  318. self.__status(connection, nick, login, arguments)
  319. elif not login and command == 'register':
  320. self.__register(connection, nick, arguments)
  321. elif login and command == 'logout':
  322. self.__logout(connection, nick)
  323. elif login and command == 'unset':
  324. self.__unset(connection, nick, login, arguments)
  325. elif admin and command == 'reload':
  326. self.__reload(connection, nick)
  327. elif admin and command == 'restart':
  328. self.__restart(connection)
  329. elif admin and command == 'say':
  330. self.__say(connection, nick, arguments)
  331. elif admin and command == 'who':
  332. self.__who(connection, nick)
  333. else:
  334. self.__unknown(connection, nick, command)
  335. except psycopg2.Error, error:
  336. print error
  337. connection.privmsg(nick, 'an error occurred')
  338. def on_quit(self, connection, event):
  339. self.__logins.pop(irclib.nm_to_n(event.source()), None)
  340. def on_welcome(self, connection, event):
  341. for channel in self.__channels:
  342. connection.join(channel)
  343. def start(self):
  344. self.__latitude_thread = threading.Thread(None, self.__latitude)
  345. self.__latitude_thread.daemon = True
  346. self.__latitude_thread.start()
  347. if not self.__reloading:
  348. self._connect()
  349. else:
  350. self.__reloading = False
  351. while not self.__reloading:
  352. if self.__locations_lock.acquire(False):
  353. if self.__locations and sorted(self.__channels) == sorted(self.channels.keys()):
  354. for nick, channel, location in self.__locations:
  355. self.connection.notice(channel, '%s is in %s' % (nick, location))
  356. self.__locations = []
  357. self.__locations_lock.release()
  358. self.ircobj.process_once(0.2)
  359. self.__latitude_thread.join()
  360. with self.__latitude_timer_lock:
  361. if self.__latitude_timer is not None:
  362. self.__latitude_timer.cancel()
  363. self.__latitude_timer.join()
  364. def _or(function, values):
  365. return reduce(lambda a, b: a or b, map(function, values) if values else [False])
  366. if __name__ == '__main__':
  367. import locationbot
  368. bot = None
  369. try:
  370. while True:
  371. bot = reload(locationbot).LocationBot(bot)
  372. bot.start()
  373. except KeyboardInterrupt:
  374. bot.connection.disconnect('oh no!')