3 from datetime
import datetime
, timedelta
4 from xml
.parsers
.expat
import ExpatError
6 from pseudoclient
.cmd_manager
import *
8 from internets_utils
import *
9 from api
.feed
import FeedError
10 from api
.idlerpg
import IrpgPlayer
11 from api
.quotes
import FmlException
12 from api
.weather
import WeatherException
15 def get_citystate_from_zipcode(self
, zipcode
):
16 """Return [city,state] for the given U.S. zip code (if database has been imported)"""
18 self
.dbp
.execute("SELECT city, state FROM zipcode_citystate WHERE zipcode=%s", [int(zipcode
)])
19 city
, state
= self
.dbp
.fetchone()
25 def command_weather(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
26 arg
= self
.get_location(opts
, arg
, channel
, sender
)
31 location
= get_citystate_from_zipcode(self
, arg
)
33 self
.errormsg(channel
, 'zip code not recognised.')
35 city
, state
= location
36 location
= '{city}, {state}, USA'.format(city
=city
, state
=state
)
37 w_state
= state
+ u
', '
39 location
= arg
.strip()
42 w
= self
.weather
.get_conditions(location
)
43 except WeatherException
as exc
:
44 if exc
== 'this key is not valid':
45 self
.elog
.warning('WARNING: OpenWeatherMap API key is not correctly set (%s)' % exc
)
46 self
.errormsg(channel
, 'weather data is temporarily unavailable. Try again later')
48 self
.errormsg(channel
, exc
)
51 self
.errormsg(channel
, e
.msg
)
54 code
= get_tempcolor(w
['temp_c'])
56 self
.msg(channel
, format_weather(code
, u
"""@sep @b{w[city]}{w_state}{w[country]}@b @sep @bConditions@b {w[description]} @sep \
57 @bTemperature@b {tempcolor}{w[temp_c]}C / {w[temp_f]}F@o @sep \
58 @bPressure@b {w[pressure]}mb @sep @bHumidity@b {w[humidity]}% @sep \
59 @bRain@b {w[rain]} @sep \
60 Powered by OpenWeatherMap http://openweathermap.org/city/{w[id]} @sep""".format(w
=w
, tempcolor
=code
, w_state
=w_state
)))
63 def command_forecast(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
64 arg
= self
.get_location(opts
, arg
, channel
, sender
)
69 location
= get_citystate_from_zipcode(self
, arg
)
71 self
.errormsg(channel
, 'zip code not recognised.')
73 city
, state
= location
74 location
= '{city}, {state}, USA'.format(city
=city
, state
=state
)
75 w_state
= state
+ u
', '
77 location
= arg
.strip()
80 w
= self
.weather
.get_forecast(location
)
81 except WeatherException
as exc
:
82 if exc
== 'this key is not valid':
83 self
.elog
.warning('WARNING: OpenWeatherMap API key is not correctly set (%s)' % exc
)
84 self
.errormsg(channel
, 'weather data is temporarily unavailable. Try again later')
86 self
.errormsg(channel
, exc
)
89 self
.errormsg(channel
, e
.msg
)
92 fc
= ' @sep '.join([u
"""@b{day[name]}@b {day[description]} @c04{day[max_c]}C / {day[max_f]}F \
93 @c10{day[min_c]}C / {day[min_f]}F""".format(day
=day
) for day
in w
['days']])
95 self
.msg(channel
, u
'@sep @b{w[city]}{w_state}{w[country]}@b @sep {fc} @sep'.format(w
=w
, fc
=fc
, w_state
=w_state
))
98 def command_register_location(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
103 location
= get_citystate_from_zipcode(self
, arg
)
105 self
.errormsg(channel
, 'zip code not recognised.')
107 city
, state
= location
108 location
= '{city}, {state}, USA'.format(city
=city
, state
=state
)
109 w_state
= state
+ u
', '
111 location
= arg
.strip()
113 w
= self
.weather
.get_conditions(location
)
114 except WeatherException
as exc
:
115 if exc
== 'this key is not valid':
116 self
.elog
.warning('WARNING: OpenWeatherMap API key is not correctly set (%s)' % exc
)
117 self
.errormsg(channel
, 'weather data is temporarily unavailable. Try again later')
119 self
.errormsg(channel
, exc
)
122 self
.errormsg(channel
, e
.msg
)
125 loc_name
= u
'{w[city]}{w_state}{w[country]}'.format(w
=w
, w_state
=w_state
)
127 self
.users
.set(sender
, 'location', arg
)
128 self
.msg(channel
, u
'%s: registered location @b%s@b' % (sender
, loc_name
))
130 def command_bing_translate(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
131 sp
= arg
.split(' ', 2)
134 source
, target
, text
= sp
135 if source
.lower() not in self
.bing
.languages
or target
.lower() not in self
.bing
.languages
:
136 source
= self
.bing
.detect_language(arg
)
138 translation
= self
.bing
.translate(arg
)
140 source
= source
.lower()
141 target
= target
.lower()
142 translation
= self
.bing
.translate(text
, source
, target
)
144 source
= self
.bing
.detect_language(arg
)
146 translation
= self
.bing
.translate(arg
)
148 self
.elog
.warning('WARNING: Bing translate failed: %s' % e
)
149 self
.errormsg(channel
, e
.msg
)
152 self
.msg(channel
, '[t] [from %s] %s' % (source
, translation
))
154 def command_google_search(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
156 result
= self
.google
.search(arg
, userinfo
['ip'] if userinfo
['ip'] != '0' else '255.255.255.255')
158 self
.errormsg(channel
, e
.msg
)
161 if result
['responseStatus'] == 403:
162 self
.elog
.warning('WARNING: Google Search failed: %s' % result
['responseDetails'] if 'responseDetails' in result
else 'unknown error')
163 self
.notice(sender
, 'Google Search is temporarily unavailable. Try again later.')
166 result
= result
['responseData']['results']
168 self
.msg(channel
, '[Google] No results found')
172 self
.msg(channel
, '[Google] @b%(title)s@b <@u%(url)s@u>' % {
173 'title': unescape(json
['titleNoFormatting']),
174 'url': json
['unescapedUrl']})
175 self
.msg(channel
, '[Google] @bDescription@b: %s' % unescape(json
['content']).replace('<b>', '@b').replace('</b>', '@b'))
177 def command_calc(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
178 try: # local calculation using PyParsing
179 result
= self
.nsp
.eval(arg
)
180 self
.msg(channel
, '[calc] {} = {}'.format(arg
, result
))
181 except: # just throw it at W|A, hopefully they can get it
183 result
= self
.wolfram
.alpha(arg
)
184 except FeedError
as e
:
185 self
.errormsg(channel
, e
.msg
)
189 self
.msg(channel
, '[W|A] Invalid input.')
191 self
.msg(channel
, u
'[W|A] {r[0]} = {r[1]}'.format(r
=result
))
193 def command_youtube_search(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
195 res
= self
.google
.yt_search(arg
)
197 self
.errormsg(channel
, e
.msg
)
201 self
.msg(channel
, '[YouTube] No results found')
204 self
.msg(channel
, """@sep @bYouTube@b %(title)s @sep @bURL@b %(url)s (%(duration)s) @sep @bViews@b %(views)s @sep \
205 @bRating@b %(rating)s/5 - %(votes)s votes @c3@b[+]@b %(liked)s likes @c4@b[-]@b %(disliked)s dislikes @sep""" % {
206 'title': res
['title'],
207 'url': 'http://www.youtube.com/watch?v=' + res
['id'],
208 'duration': '%s' % format_hms(res
['duration']),
209 'views': format_thousand(res
['view_count']),
210 'rating': round(res
['rating'], 2) if res
['rating'] else 0,
211 'votes': format_thousand(res
['rate_count']) if res
['rate_count'] else 0,
212 'liked': format_thousand(res
['liked']) if res
['liked'] else 0,
213 'disliked': format_thousand(res
['disliked']) if res
['disliked'] else 0
216 def command_dictionary(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
218 results
= self
.wordnik
.definition(arg
)
220 self
.errormsg(channel
, e
.msg
)
224 self
.msg(channel
, '[dictionary] Nothing found')
228 for n
, res
in enumerate(results
, 1):
229 self
.notice(sender
, u
'@sep [{num}/{tot}] @bDefinition@b {res.word} @sep {res.text} @sep'.format(
230 res
=res
, num
=n
, tot
=len(results
)))
231 elif 'number' in opts
:
232 if opts
['number'] - 1 < 0 or opts
['number'] - 1 > len(results
):
233 self
.errormsg(channel
, 'option -n out of range: only %d definitions found.' % len(results
))
236 result
= results
[opts
['number'] - 1]
237 self
.msg(channel
, u
'@sep [{num}/{tot}] @bDefinition@b {res.word} @sep {res.text} @sep'.format(
238 res
=result
, num
=opts
['number'], tot
=len(results
)))
240 for n
, res
in enumerate(results
, 1):
241 self
.msg(channel
, u
'@sep [{num}/{tot}] @bDefinition@b {res.word} @sep {res.text} @sep'.format(
242 res
=res
, num
=n
, tot
=len(results
)))
244 self
.notice(sender
, 'To view all definitions: .dict {res.word} -a. To view the n-th definition: .dict {res.word} -n <number>'.format(res
=res
))
247 def command_urbandictionary(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
248 expr
, sep
, def_id
= arg
.partition('/')
250 res
= self
.urbandictionary
.get_definitions(expr
.strip())
252 self
.errormsg(channel
, e
.msg
)
253 self
.elog
.warning('feed error in .urbandictionary: %s' % e
)
256 if res
['result_type'] == 'no_results' or res
['result_type'] == 'fulltext':
257 self
.errormsg(channel
, 'no results found')
258 elif res
['result_type'] == 'exact':
263 self
.errormsg(channel
, 'invalid definition number')
266 entry
= res
['list'][def_id
- 1]
267 definition
= entry
['definition'].replace('\r\n', ' / ').replace('\n', ' / ')
268 example
= entry
['example'].replace('\r\n', ' / ').replace('\n', ' / ')
269 self
.msg(channel
, u
'@sep [{num}/{total}] {entry[word]} @sep {definition} @sep'.format(
271 total
= len(res
['list']),
273 definition
= definition
if len(definition
) < 200 else definition
[:200] + '...',
275 self
.msg(channel
, u
'@sep @bExample@b %s @sep' % (example
if len(example
) < 280 else example
[:280] + '...'))
277 self
.errormsg(channel
, 'invalid definition number')
279 self
.errormsg(channel
, 'definition id out of range: only %d definitions available' % len(res
['list']))
281 for num
, entry
in enumerate(res
['list'], 1):
283 self
.notice(sender
, 'To view a single definition with a related example, type: @b.u %s /def_number@b. For more definitions, visit: %s' % (expr
, res
['list'][0]['permalink']))
286 definition
= entry
['definition'].replace('\r\n', ' / ').replace('\n', ' / ')
287 self
.msg(channel
, u
'@sep [{num}/{total}] {entry[word]} @sep {definition} @sep'.format(
289 total
= len(res
['list']),
291 definition
= definition
if len(definition
) < 200 else definition
[:200] + '...',
294 self
.msg(channel
, 'An exception occurred and has been reported to the developers. If this error persists please do not use the faulty command until it has been fixed.')
295 self
.elog
.warning('unrecognized result type: %s' % res
['result_type'])
297 def command_imdb(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
299 reply
= self
.imdb
.get(arg
)
301 self
.errormsg(channel
, e
.msg
)
304 self
.errormsg(channel
, 'movie not found')
307 if reply
['Response'] != 'True':
308 self
.msg(channel
, '[imdb] Nothing found')
311 self
.msg(channel
, u
"""@sep @b{r[Title]}@b [{r[Year]}] Rated {r[Rated]} @sep @bRating@b {r[imdbRating]}/10, {r[imdbVotes]} votes @sep \
312 @bGenre@b {r[Genre]} @sep @bDirector@b {r[Director]} @sep @bActors@b {r[Actors]} @sep @bRuntime@b {r[Runtime]} @sep""".format(r
=reply
))
313 self
.msg(channel
, u
'@sep @bPlot@b {r[Plot]} @sep @uhttp://www.imdb.com/title/{r[imdbID]}/@u @sep'.format(r
=reply
))
315 def command_lastfm(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
317 user
= self
.lastfm
.get_user(arg
)
319 self
.errormsg(channel
, user
['message'])
321 latest
= self
.lastfm
.get_recent_tracks(arg
, 1)
322 except FeedError
as e
:
323 self
.errormsg(channel
, e
.msg
)
329 userinfo
.append(user
['realname'])
331 userinfo
.append(user
['age'])
333 userinfo
.append(user
['country'])
336 userinfo
= ' [%s]' % ', '.join(userinfo
)
340 if 'track' in latest
['recenttracks']:
341 if isinstance(latest
['recenttracks']['track'], list):
342 latest
= latest
['recenttracks']['track'][0]
344 latest
= latest
['recenttracks']['track']
346 latest
['@attr']['nowplaying']
347 latest_str
= u
' @bNow playing@b {latest[artist][#text]} - {latest[name]} @sep'.format(latest
=latest
)
349 latestdate
= get_timespan(datetime
.fromtimestamp(int(latest
['date']['uts'])))
350 latest_str
= u
' @bLatest track@b {latest[artist][#text]} - {latest[name]} ({latestdate} ago) @sep'.format(
351 latest
=latest
, latestdate
=latestdate
)
355 self
.msg(channel
, u
'@sep @b{user[name]}@b{userinfo} @sep @bPlays@b {plays} since {regdate} @sep \
356 @bLink@b {user[url]} @sep{latest_track}'.format(
358 plays
= format_thousand(int(user
['playcount'])),
359 regdate
= user
['registered']['#text'][:10],
361 latest_track
= latest_str
))
363 def command_url_shorten(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
364 if not arg
.startswith('http://') and not arg
.startswith('https://'):
365 self
.errormsg(channel
, 'a valid URL must start with http:// or https://')
369 reply
= self
.urls
.shorten(arg
)
371 self
.errormsg(channel
, e
.msg
)
374 if reply
['status_code'] != 200:
375 self
.errormsg(channel
, 'an error occurred.')
376 self
.elog
.warning('[shorten] error: code %d, %s' % (reply
['status_code'], reply
['status_txt']))
378 self
.msg(channel
, '@sep @bShort URL@b %s @sep' % reply
['data']['url'])
380 def command_url_expand(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
381 if not arg
.startswith('http://') and not arg
.startswith('https://'):
382 self
.errormsg(channel
, 'a valid URL must start with http:// or https://')
386 reply
= self
.urls
.expand(arg
)
388 self
.errormsg(channel
, e
.msg
)
392 self
.errormsg(channel
, reply
['error'])
394 self
.msg(channel
, '@sep @bLong URL@b {reply[long-url]} @sep @bContent-type@b {reply[content-type]} @sep'.format(reply
=reply
))
396 def command_idlerpg(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
398 player
= IrpgPlayer(arg
)
400 self
.errormsg(channel
, e
.msg
)
404 self
.errormsg(channel
, 'player not found. @bNote@b: nicks are case sensitive.')
407 self
.msg(channel
, """@sep @b{player.name}@b [{status}] @sep @bLevel@b {player.level} {player.classe} @sep @bNext level@b \
408 {nextlevel} @sep @bIdled@b {idled_for} @sep @bAlignment@b {player.alignment} @sep""".format(
410 status
= '@c3ON@c' if player
.is_online
else '@c4OFF@c',
411 nextlevel
= timedelta(seconds
=player
.ttl
),
412 idled_for
= timedelta(seconds
=player
.idled_for
)))
414 def command_ipinfo(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
416 reply
= self
.ipinfo
.get_info(arg
)
418 self
.errormsg(channel
, e
.msg
)
421 self
.msg(channel
, """@sep @bIP/Host@b {arg} ({reply[ip_addr]}) @sep @bLocation@b {reply[city]}, {reply[region]}, \
422 {reply[country_name]} [{reply[country_code]}] @sep{map}""".format(
425 map = ' http://maps.google.com/maps?q=%s,%s @sep' % (reply
['latitude'], reply
['longitude']) if reply
['latitude'] and reply
['longitude'] else ''))
427 dice_regex
= re
.compile('^(?:(\d+)d)?(\d+)(?:([\+\-])(\d+))?$')
428 def command_dice(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
429 r
= dice_regex
.search(arg
)
431 self
.errormsg(channel
, 'invalid format')
434 num
, faces
, type, modifier
= r
.groups()
440 if num
< 1 or num
> 32 or faces
< 2 or faces
> 65536:
441 self
.errormsg(channel
, 'parameter out of range')
446 for n
in xrange(int(num
)):
447 randnum
= random
.randint(1, int(faces
))
449 results
.append(randnum
)
452 modifier
= int(modifier
)
454 max = num
* faces
- modifier
456 modifier
= int(modifier
)
458 max = num
* faces
+ modifier
462 self
.msg(channel
, '@sep @bTotal@b {total} / {max} [{percent}%] @sep @bResults@b {results} @sep'.format(
465 percent
= 100 * total
/ max if max != 0 else '9001',
466 results
= str(results
)))
468 def command_qdb(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
471 quote
= self
.quotes
.get_qdb_random()
475 quote
= self
.quotes
.get_qdb_id(quote_id
)
477 self
.errormsg(channel
, 'quote @b%d@b not found' % quote_id
)
480 self
.errormsg(channel
, 'invalid quote ID')
482 except ExpatError
: # qdb returns a malformed xml when the quote doesn't exist
483 self
.errormsg(channel
, 'quote @b%d@b not found' % quote_id
)
486 self
.errormsg(channel
, e
.msg
)
490 for line
in quote
['lines']:
491 self
.msg(channel
, u
'[qdb {id}] {line}'.format(id=id, line
=line
.replace('\n', '')))
493 def command_fml(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
496 quote
= self
.quotes
.get_fml()
500 quote
= self
.quotes
.get_fml(quote_id
)
502 self
.errormsg(channel
, 'quote @b%d@b not found' % quote_id
)
504 except (ValueError, IndexError):
505 self
.errormsg(channel
, 'invalid quote ID')
507 except (FeedError
, FmlException
) as e
:
508 self
.errormsg(channel
, e
.msg
)
509 self
.elog
.warning('WARNING: .fml error: %s' % e
.msg
)
512 self
.msg(channel
, u
'[fml #{quote[id]}] {quote[text]}'.format(quote
=quote
))
514 def command_internets_info(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
515 self
.notice(sender
, '@sep @bRizon Internets Bot@b @sep @bDevelopers@b martin <martin@rizon.net> @sep @bHelp/feedback@b %(channel)s @sep' % {
516 'channel' : '#internets'})
518 def command_internets_help(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
519 command
= arg
.lower()
522 message
= ['internets: .help internets - for internets commands']
523 elif command
== 'internets':
524 message
= manager
.get_help()
526 message
= manager
.get_help(command
)
529 message
= ['%s is not a valid command.' % arg
]
532 self
.notice(sender
, line
)
534 class UserCommandManager(CommandManager
):
535 def get_prefix(self
):
538 def get_commands(self
):
541 'calc': (command_calc
, ARG_YES
, 'Calculates an expression', [], 'expression'),
543 'dict': 'dictionary',
544 'dictionary': (command_dictionary
, ARG_YES
, 'Search for a dictionary definition', [
545 ('number', '-n', 'display the n-th result', {'type': '+integer'}
, ARG_YES
),
546 ('all', '-a', 'display all results (using /notice)', {'action': 'store_true'}
, ARG_YES
)], 'word'),
548 'u': 'urbandictionary',
549 'urbandictionary': (command_urbandictionary
, ARG_YES
, 'Search for a definition on Urban Dictionary', [], 'word'),
552 'google': (command_google_search
, ARG_YES
, 'Search for something on Google', [], 'google_search'),
555 'translate': (command_bing_translate
, ARG_YES
, 'Translate something from a language to another', [], 'from to text'),
558 'youtube': (command_youtube_search
, ARG_YES
, 'Search for something on YouTube', [], 'youtube_search'),
561 'weather': (command_weather
, ARG_OPT
, 'Displays current weather conditions for a location', [
562 ('nick', '-n', 'use the weather location linked to a nick', {'action': 'store_true'}
, ARG_YES
)]),
565 'forecast': (command_forecast
, ARG_OPT
, 'Displays 5-day forecast for a location', [
566 ('nick', '-n', 'use the weather location linked to a nick', {'action': 'store_true'}
, ARG_YES
)]),
568 'regloc': 'register_location',
569 'register_location': (command_register_location
, ARG_YES
, 'Links a location to your nick that will be used as default location in .w and .f', [], 'location'),
571 'imdb': (command_imdb
, ARG_YES
, 'Search for information on a movie on IMDB', [], 'movie_title'),
573 'lastfm': (command_lastfm
, ARG_YES
, 'Returns information on a Last.fm user', [], 'lastfm_user'),
575 'shorten': (command_url_shorten
, ARG_YES
, 'Shortens a URL using http://j.mp', [], 'long_url'),
577 'expand': (command_url_expand
, ARG_YES
, 'Expands a shortened URL using http://longurl.org', [], 'shortened_url'),
580 'idlerpg': (command_idlerpg
, ARG_YES
, 'Returns info on a player in Rizon IdleRPG (http://idlerpg.rizon.net/)', [], 'player_name'),
582 'ipinfo': (command_ipinfo
, ARG_YES
, 'Returns short info on a IP address/hostname', [], 'ip/host'),
585 'dice': (command_dice
, ARG_YES
, 'Rolls X N-sided dice with an optional modifier A (XdN+A format)', [], 'dice_notation'),
587 'qdb': (command_qdb
, ARG_OPT
, 'Displays a quote from qdb.us', []),
589 'fml': (command_fml
, ARG_OPT
, 'Displays a quote from http://www.fmylife.com', []),
591 'info': (command_internets_info
, ARG_NO|ARG_OFFLINE
, 'Displays version and author information', []),
592 'help': (command_internets_help
, ARG_OPT|ARG_OFFLINE
, 'Displays available commands and their usage', []),