3 from datetime
import datetime
, timedelta
4 from xml
.parsers
.expat
import ExpatError
7 from pseudoclient
.cmd_manager
import *
9 from internets_utils
import *
10 from api
.feed
import FeedError
11 from api
.idlerpg
import IrpgPlayer
12 from api
.quotes
import FmlException
13 from api
.weather
import WeatherException
14 from api
.steam
import SteamException
16 import pyva_net_rizon_acid_core_User
as User
18 RE_YT_PATTERN
= re
.compile(
19 "(?:www\\.|m\\.)?(?:(?:youtube\\.com/(?:watch)?(?:[?&][a-z]+=[a-z_]+)?(?:[?&]v=))"
20 "|(?:youtu\\.be\\/))([a-zA-Z0-9-_]+)")
23 def onPrivmsg_regex_youtube(self
, source
, target
, message
):
24 userinfo
= User
.findUser(source
)
25 myself
= User
.findUser(self
.nick
)
27 sender
= userinfo
['nick']
30 yt_links
= RE_YT_PATTERN
.findall(message
)
32 if yt_links
and self
.google
._check
_link
_eligibility
(channel
, yt_links
[0]):
34 video
= self
.google
.yt_video(yt_links
[0], userip
=userinfo
['ip'] if userinfo
['ip'] != '0' else None)
38 self
.msg(channel
, "@sep @bYouTube@b %(title)s @sep (%(duration)s) @sep @bViews@b %(views)s @sep "\
39 "@bRating@b @c3@b[+]@b %(liked)s likes @c4@b[-]@b %(disliked)s dislikes @sep" % {
40 'title': video
['title'],
41 'duration': '%s' % format_hms(video
['duration']),
42 'views': format_thousand(video
['view_count']),
43 'liked': format_thousand(video
['liked']) if video
['liked'] else 0,
44 'disliked': format_thousand(video
['disliked']) if video
['disliked'] else 0
50 def get_citystate_from_zipcode(self
, zipcode
):
51 """Return [city,state] for the given U.S. zip code (if database has been imported)"""
53 con
= core
.dbpool
.get_connection()
56 cursor
.execute("SELECT city, state FROM zipcode_citystate WHERE zipcode=%s", [int(zipcode
)])
57 city
, state
= cursor
.fetchone()
60 core
.dbpool
.put_connection(con
)
65 # Returns colour coded test of persona state in Steam.
67 def get_personastate_text(self
, state
):
70 return '@c14OFFLINE@c'
84 # user is looking to trade
85 return '@c5LOOKING TO TRADE@c'
87 # user is looking to play
88 return '@c10LOOKING TO PLAY@c'
91 return '@c14UNKNOWN@c'
93 def command_weather(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
94 arg
= self
.get_location(opts
, arg
, channel
, sender
)
99 location
= get_citystate_from_zipcode(self
, arg
)
101 self
.errormsg(channel
, 'zip code not recognised.')
103 city
, state
= location
104 location
= '{city}, {state}, USA'.format(city
=city
, state
=state
)
105 w_state
= state
+ u
', '
107 location
= arg
.strip()
110 w
= self
.weather
.get_conditions(location
)
111 except WeatherException
as exc
:
112 if exc
== 'this key is not valid':
113 self
.elog
.warning('WARNING: OpenWeatherMap API key is not correctly set (%s)' % exc
)
114 self
.errormsg(channel
, 'weather data is temporarily unavailable. Try again later')
116 self
.errormsg(channel
, exc
)
119 self
.errormsg(channel
, e
.msg
)
122 code
= get_tempcolor(w
['temp_c'])
124 self
.msg(channel
, format_weather(code
, u
"""@sep @b{w[city]}{w_state}{w[country]}@b @sep @bConditions@b {w[description]} @sep \
125 @bTemperature@b {tempcolor}{w[temp_c]}C / {w[temp_f]}F@o @sep \
126 @bPressure@b {w[pressure]}mb @sep @bHumidity@b {w[humidity]}% @sep \
127 @bRain@b {w[rain]} @sep \
128 Powered by OpenWeatherMap http://openweathermap.org/city/{w[id]} @sep""".format(w
=w
, tempcolor
=code
, w_state
=w_state
)))
131 def command_forecast(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
132 arg
= self
.get_location(opts
, arg
, channel
, sender
)
137 location
= get_citystate_from_zipcode(self
, arg
)
139 self
.errormsg(channel
, 'zip code not recognised.')
141 city
, state
= location
142 location
= '{city}, {state}, USA'.format(city
=city
, state
=state
)
143 w_state
= state
+ u
', '
145 location
= arg
.strip()
148 w
= self
.weather
.get_forecast(location
)
149 except WeatherException
as exc
:
150 if exc
== 'this key is not valid':
151 self
.elog
.warning('WARNING: OpenWeatherMap API key is not correctly set (%s)' % exc
)
152 self
.errormsg(channel
, 'weather data is temporarily unavailable. Try again later')
154 self
.errormsg(channel
, exc
)
157 self
.errormsg(channel
, e
.msg
)
160 fc
= ' @sep '.join([u
"""@b{day[name]}@b {day[description]} @c04{day[max_c]}C / {day[max_f]}F \
161 @c10{day[min_c]}C / {day[min_f]}F""".format(day
=day
) for day
in w
['days']])
163 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
))
166 def command_register_location(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
171 location
= get_citystate_from_zipcode(self
, arg
)
173 self
.errormsg(channel
, 'zip code not recognised.')
175 city
, state
= location
176 location
= '{city}, {state}, USA'.format(city
=city
, state
=state
)
177 w_state
= state
+ u
', '
179 location
= arg
.strip()
181 w
= self
.weather
.get_conditions(location
)
182 except WeatherException
as exc
:
183 if exc
== 'this key is not valid':
184 self
.elog
.warning('WARNING: OpenWeatherMap API key is not correctly set (%s)' % exc
)
185 self
.errormsg(channel
, 'weather data is temporarily unavailable. Try again later')
187 self
.errormsg(channel
, exc
)
190 self
.errormsg(channel
, e
.msg
)
193 loc_name
= u
'{w[city]}{w_state}{w[country]}'.format(w
=w
, w_state
=w_state
)
195 self
.users
.set(sender
, 'location', arg
)
196 self
.msg(channel
, u
'%s: registered location @b%s@b' % (sender
, loc_name
))
198 def command_bing_translate(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
199 sp
= arg
.split(' ', 2)
202 source
, target
, text
= sp
203 if source
.lower() not in self
.bing
.languages
or target
.lower() not in self
.bing
.languages
:
204 source
= self
.bing
.detect_language(arg
)
206 translation
= self
.bing
.translate(arg
)
208 source
= source
.lower()
209 target
= target
.lower()
210 translation
= self
.bing
.translate(text
, source
, target
)
212 source
= self
.bing
.detect_language(arg
)
214 translation
= self
.bing
.translate(arg
)
216 self
.elog
.warning('WARNING: Bing translate failed: %s' % e
)
217 self
.errormsg(channel
, e
.msg
)
220 self
.msg(channel
, '[t] [from %s] %s' % (source
, translation
))
222 def command_google_search(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
224 result
= self
.google
.search(arg
, userinfo
['ip'] if userinfo
['ip'] != '0' else None)
226 self
.errormsg(channel
, e
.msg
)
229 if result
['responseStatus'] == 403:
230 self
.elog
.warning('WARNING: Google Search failed: %s' % result
['responseDetails'] if 'responseDetails' in result
else 'unknown error')
231 self
.notice(sender
, 'Google Search is temporarily unavailable. Try again later.')
234 result
= result
['responseData']['results']
236 self
.msg(channel
, '[Google] No results found')
240 self
.msg(channel
, '[Google] @b%(title)s@b <@u%(url)s@u>' % {
241 'title': unescape(json
['titleNoFormatting']),
242 'url': json
['unescapedUrl']})
244 if json
['content'] != '':
245 self
.msg(channel
, '[Google] @bDescription@b: %s' % unescape(json
['content']).replace('<b>', '@b').replace('</b>', '@b'))
247 def command_google_image_search(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
249 result
= self
.google
.image_search(arg
, userinfo
['ip'] if userinfo
['ip'] != '0' else None)
251 self
.errormsg(channel
, e
.msg
)
254 if result
['responseStatus'] == 403:
255 self
.elog
.warning('WARNING: Google Image Search failed: %s' % result
['responseDetails'] if 'responseDetails' in result
else 'unknown error')
256 self
.notice(sender
, 'Google Search is temporarily unavailable. Try again later.')
259 result
= result
['responseData']['results']
261 self
.msg(channel
, '[Google Image] No results found')
265 self
.msg(channel
, '[Google Image] @b%(title)s@b <@u%(url)s@u>' % {
266 'title': unescape(json
['titleNoFormatting']),
267 'width': json
['width'],
268 'height': json
['height'],
269 'url': json
['unescapedUrl']})
271 self
.msg(channel
, '[Google Image] @bSize@b: %(width)sx%(height)spx%(desc)s' % {
273 'width': json
['width'],
274 'height': json
['height'],
275 'desc': (' - @bDescription@b: %s' % unescape(json
['content']).replace('<b>', '@b').replace('</b>', '@b')) if json
['content'] else ''})
278 def command_calc(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
279 try: # local calculation using PyParsing
280 result
= self
.nsp
.eval(arg
)
281 self
.msg(channel
, '[calc] {} = {}'.format(arg
, result
))
282 except: # just throw it at W|A, hopefully they can get it
284 result
= self
.wolfram
.alpha(arg
)
285 except FeedError
as e
:
286 self
.errormsg(channel
, e
.msg
)
290 self
.msg(channel
, '[W|A] Invalid input.')
292 lines
= len(result
[0].splitlines(True)) if result
[1] is None else len(result
[1].splitlines(True))
293 if lines
> self
.output_limit
:
294 self
.notice(sender
, u
'[W|A] {r[0]} = {r[1]}'.format(r
=result
))
296 self
.msg(channel
, u
'[W|A] {r[0]} = {r[1]}'.format(r
=result
))
298 def command_youtube_search(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
300 res
= self
.google
.yt_search(arg
, userip
=userinfo
['ip'] if userinfo
['ip'] != '0' else None)
302 self
.errormsg(channel
, e
.msg
)
306 self
.msg(channel
, '[YouTube] No results found')
309 self
.msg(channel
, """@sep @bYouTube@b %(title)s @sep @bURL@b %(url)s (%(duration)s) @sep @bViews@b %(views)s @sep \
310 @bRating@b @c3@b[+]@b %(liked)s likes @c4@b[-]@b %(disliked)s dislikes @sep""" % {
311 'title': res
['title'],
312 'url': 'http://www.youtube.com/watch?v=' + res
['id'],
313 'duration': '%s' % format_hms(res
['duration']),
314 'views': format_thousand(res
['view_count']),
315 'liked': format_thousand(res
['liked']) if res
['liked'] else 0,
316 'disliked': format_thousand(res
['disliked']) if res
['disliked'] else 0
319 def command_twitch(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
321 expr
, sep
, res_num
= arg
.partition('/')
325 if arg
.strip(): # search for a particular stream
326 [total
, streams
] = self
.twitch
.search_stream(arg
)
328 self
.errormsg(channel
, u
"No results found.")
331 s
= streams
[int(res_num
)-1]
332 self
.msg(channel
, (u
"@sep Game @b{s[game]}@b @sep has status @b{s[status]}@b on channel @u{s[chan]}@u, " +
333 u
"which has {s[views]} views and {s[followers]} followers @sep Link: {s[url]} @sep").format(s
=s
))
337 if i
> MAX_RESULTS
: break
338 self
.msg(channel
, (u
"@sep [{i}/{total}] Game @b{s[game]}@b @sep has status @b{s[status]}@b on channel @u{s[chan]}@u, " +
339 u
"which has {s[views]} views and {s[followers]} followers @sep Link: {s[url]} @sep").format(i
=i
, total
=total
, s
=s
))
341 self
.notice(sender
, "To view a particular result, type: @b.tw -s {} /@unumber@u@b".format(arg
.replace('+', ' ')))
342 else: # list top streams
343 streams
= self
.twitch
.get_streams()
345 s
= streams
[int(res_num
)-1]
346 self
.msg(channel
, (u
"@sep Game @b{s[game]}@b @sep has a stream created on {s[created_at]} that has " +
347 u
"{s[viewers]} viewers @sep Related channel: @b{s[channel]}@b @sep").format(s
=s
))
352 if i
> MAX_RESULTS
: break
353 self
.msg(channel
, (u
"@sep [{i}/{total}] Game @b{s[game]}@b @sep has a stream created on {s[created_at]} that has " +
354 u
"{s[viewers]} viewers @sep Related channel: @b{s[channel]}@b @sep").format(s
=s
, i
=i
, total
=total
))
356 self
.notice(sender
, u
"To view a particular result, type: @b.tw -s /@unumber@u@b")
358 elif 'channel' in opts
:
359 videos
= self
.twitch
.get_channel(arg
.strip())
362 v
= videos
[int(res_num
)-1]
363 self
.msg(channel
, u
"@sep @b{v[title]}@b @sep with {v[views]} views @sep Link: {v[url]} @sep".format(v
=v
))
365 self
.msg(channel
, "@sep Videos in channel @b{}@b @sep".format(arg
))
369 if i
> MAX_RESULTS
: break
370 self
.msg(channel
, "@sep [{i}/{total}] @b{v[title]}@b @sep with {v[views]} views @sep Link: {v[url]} @sep"
371 .format(v
=v
, i
=i
, total
=total
))
373 self
.notice(sender
, "To view a particular result, type: @b.tw -c {} /@unumber@u@b".format(arg
.replace('+', ' ')))
375 self
.errormsg(channel
, "Channel @b{}@b does not exit.".format(arg
))
378 games
= self
.twitch
.search_games(arg
)
380 self
.errormsg(channel
, "Game @b{}@b does not exit.".format(arg
))
383 g
= games
[int(res_num
)-1]
384 self
.msg(channel
, (u
"@sep @b{g[name]}@b @sep ranked @b{g[popularity]}@b in popularity @sep " +
385 u
"http://www.twitch.tv/search?query={g[name2]} @sep").format(g
=g
))
387 self
.msg(channel
, "Results for @b{}@b".format(arg
.replace('+', ' ')))
391 if i
> MAX_RESULTS
: break
392 self
.msg(channel
, (u
"@sep [{i}/{total}] @b{g[name]}@b @sep ranked @b{g[popularity]}@b in popularity @sep " +
393 u
"http://www.twitch.tv/search?query={g[name2]} @sep").format(i
=i
, g
=g
, total
=total
))
395 self
.notice(sender
, "To view a particular result, type: @b.tw -g {} /@unumber@u@b".format(arg
.replace('+', ' ')))
398 if arg
.strip(): # search for a particular team
399 t
= self
.twitch
.get_team(arg
)
401 self
.msg(channel
, (u
"@sep Team @u{t[name]}@u @sep called @b{t[display_name]}@b @sep was created " +
402 u
"on {t[created_at]} @sep Info: {t[info]} @sep").format(t
=t
))
404 self
.errormsg(channel
, "Team @b{}@b does not exist.".format(arg
))
405 else: # list top teams
406 teams
= self
.twitch
.get_teams()
408 self
.errormsg(channel
, u
"No teams exist.")
411 t
= teams
[int(res_num
)-1]
412 self
.msg(channel
, (u
"@sep Team @u{t[name]}@u @sep called @b{t[display_name]}@b @sep was created " +
413 u
"on {t[created_at]} @sep Info: {t[info]} @sep").format(t
=t
))
418 if i
> MAX_RESULTS
: break
419 self
.msg(channel
, (u
"@sep [{i}/{total}] Team @u{t[name]}@u @sep called @b{t[display_name]}@b @sep was created " +
420 u
"on {t[created_at]} @sep Info: {t[info]} @sep").format(t
=t
, i
=i
, total
=total
))
422 self
.notice(sender
, u
"To view a particular result, type: @b.tw -t /@unumber@u@b")
424 elif 'video' in opts
:
425 self
.msg(channel
, "@sep http://www.twitch.tv/search?query={} @sep".format(arg
.replace(' ', '+')))
428 self
.notice(sender
, u
"Type: @b.help twitch@b for the twitch.tv bot command syntax.")
430 self
.errormsg(channel
, u
"What's after the slash must be an integer number.")
432 self
.errormsg(channel
, u
"No result with that number exists.")
435 def command_dictionary(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
437 results
= self
.wordnik
.definition(arg
)
439 self
.errormsg(channel
, e
.msg
)
443 self
.msg(channel
, '[dictionary] Nothing found')
447 for n
, res
in enumerate(results
, 1):
448 self
.notice(sender
, u
'@sep [{num}/{tot}] @bDefinition@b {res.word} @sep {res.text} @sep'.format(
449 res
=res
, num
=n
, tot
=len(results
)))
450 elif 'number' in opts
:
451 if opts
['number'] - 1 < 0 or opts
['number'] - 1 > len(results
):
452 self
.errormsg(channel
, 'option -n out of range: only %d definitions found.' % len(results
))
455 result
= results
[opts
['number'] - 1]
456 self
.msg(channel
, u
'@sep [{num}/{tot}] @bDefinition@b {res.word} @sep {res.text} @sep'.format(
457 res
=result
, num
=opts
['number'], tot
=len(results
)))
459 for n
, res
in enumerate(results
, 1):
460 self
.msg(channel
, u
'@sep [{num}/{tot}] @bDefinition@b {res.word} @sep {res.text} @sep'.format(
461 res
=res
, num
=n
, tot
=len(results
)))
463 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
))
466 def command_urbandictionary(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
467 expr
, sep
, def_id
= arg
.partition('/')
469 res
= self
.urbandictionary
.get_definitions(expr
.strip())
471 self
.errormsg(channel
, e
.msg
)
472 self
.elog
.warning('feed error in .urbandictionary: %s' % e
)
475 if res
['result_type'] == 'no_results' or res
['result_type'] == 'fulltext':
476 self
.errormsg(channel
, 'no results found')
477 elif res
['result_type'] == 'exact':
482 self
.errormsg(channel
, 'invalid definition number')
485 entry
= res
['list'][def_id
- 1]
486 definition
= entry
['definition'].replace('\r\n', ' / ').replace('\n', ' / ')
487 example
= entry
['example'].replace('\r\n', ' / ').replace('\n', ' / ')
488 self
.msg(channel
, u
'@sep [{num}/{total}] {entry[word]} @sep {definition} @sep'.format(
490 total
= len(res
['list']),
492 definition
= definition
if len(definition
) < 200 else definition
[:200] + '...',
494 self
.msg(channel
, u
'@sep @bExample@b %s @sep' % (example
if len(example
) < 280 else example
[:280] + '...'))
496 self
.errormsg(channel
, 'invalid definition number')
498 self
.errormsg(channel
, 'definition id out of range: only %d definitions available' % len(res
['list']))
500 for num
, entry
in enumerate(res
['list'], 1):
502 self
.notice(sender
, u
'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']))
505 definition
= entry
['definition'].replace('\r\n', ' / ').replace('\n', ' / ')
506 self
.msg(channel
, u
'@sep [{num}/{total}] {entry[word]} @sep {definition} @sep'.format(
508 total
= len(res
['list']),
510 definition
= definition
if len(definition
) < 200 else definition
[:200] + '...',
513 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.')
514 self
.elog
.warning('unrecognized result type: %s' % res
['result_type'])
516 def command_imdb(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
518 reply
= self
.imdb
.get(arg
)
520 self
.errormsg(channel
, e
.msg
)
523 self
.errormsg(channel
, 'movie not found')
526 if reply
['Response'] != 'True':
527 self
.msg(channel
, '[imdb] Nothing found')
530 self
.msg(channel
, u
"""@sep @b{r[Title]}@b [{r[Year]}] Rated {r[Rated]} @sep @bRating@b {r[imdbRating]}/10, {r[imdbVotes]} votes @sep \
531 @bGenre@b {r[Genre]} @sep @bDirector@b {r[Director]} @sep @bActors@b {r[Actors]} @sep @bRuntime@b {r[Runtime]} @sep""".format(r
=reply
))
532 self
.msg(channel
, u
'@sep @bPlot@b {r[Plot]} @sep @uhttp://www.imdb.com/title/{r[imdbID]}/@u @sep'.format(r
=reply
))
535 # Registers the user's steam ID and links it to his/her nickname.
537 def command_register_steam(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
540 steam_data
= self
.steam
.find_user(arg
)
541 self
.notice(sender
, u
'Steam ID registered, current personaname: {name}'.format(name
= steam_data
['personaname']))
542 self
.users
.set(sender
, 'steamid', steam_data
['steamid'])
543 except SteamException
as exc
:
544 self
.notice(sender
, 'No user found')
547 # Shows user's online status and what game he/sh is playing.
548 # Game does not show when user's profile is set to private.
550 def command_steam(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
551 steamuser
= self
.get_steamid(opts
, arg
, channel
, sender
)
554 steam_data
= self
.steam
.get_status(steamuser
.steamid
)
556 if steam_data
['communityvisibilitystate'] == 1:
558 self
.notice(sender
, 'Profile is hidden. If you want to use this functionality, set your profile to Public.')
562 steam_games
= self
.steam
.get_games(steamuser
.steamid
)
565 playtime_forever_top
= 0
567 playtime_2weeks_top
= 0
569 message
= u
"""@sep @b{player}@b @sep""".format(player
= steam_data
['personaname'])
571 if steam_games
['game_count'] == 0:
572 # You know, because it's possible
573 message
+= u
""" Does not own any games. @sep"""
574 self
.msg(channel
, message
)
577 for item
in steam_games
['games']:
578 ptf
= item
['playtime_forever']
579 if ptf
> playtime_forever_top
:
580 game_forever
= item
['appid']
581 playtime_forever_top
= ptf
582 playtime_forever
+= ptf
585 ptw
= item
['playtime_2weeks']
586 if ptw
> playtime_2weeks_top
:
587 game_2weeks
= item
['appid']
588 playtime_2weeks_top
= ptw
589 playtime_2weeks
+= ptw
594 message
+= u
""" @bTotal games:@b {total} @sep @bTotal playtime:@b {ftime} hours @sep @bPlaytime last 2 weeks:@b {wtime} hours @sep""".format(
595 total
= steam_games
['game_count'],
596 ftime
= round(playtime_forever
/ 60, 0),
597 wtime
= round(playtime_2weeks
/ 60, 0))
598 if game_forever
!= "":
599 fgame
= self
.steam
.get_game_name(game_forever
)
600 message
+= u
""" @bMost played game:@b {fgame}, {ftime} hours @sep""".format(
602 ftime
= round(playtime_forever_top
/ 60, 0))
603 if game_2weeks
!= "":
604 wgame
= self
.steam
.get_game_name(game_2weeks
)
605 message
+= u
""" @bMost played last 2 weeks:@b {wgame}, {wtime} hours @sep""".format(
607 wtime
= round(playtime_2weeks
/ 60, 0))
608 self
.msg(channel
, message
)
611 message
= u
"""@sep @b{player}@b [{status}] @sep""".format(
612 player
= steam_data
['personaname'],
613 status
= get_personastate_text(self
, steam_data
['personastate']))
614 if steam_data
['personastate'] == 0 or steam_data
['personastate'] > 7:
615 # User is offline or unknown state
616 # NOTE: lastlogoff is actual logoff timestamp, not "appear offline" timestamp
617 latestdate
= get_timespan(datetime
.fromtimestamp(steam_data
['lastlogoff']))
618 message
+= u
""" @bLast seen@b {latestdate} ago @sep""".format(
619 latestdate
= latestdate
)
620 self
.msg(channel
, message
)
622 # user is online, busy, away, snooze, looking to trade or looking to play
623 if 'gameextrainfo' in steam_data
:
624 message
+= u
""" @bPlaying:@b {gamename} @sep""".format(
625 gamename
= steam_data
['gameextrainfo'])
626 if 'gameserverip' in steam_data
:
627 message
+= u
""" @bPlaying on server:@b {gameserver} @sep""".format(
628 gameserver
= steam_data
['gameserverip'])
630 # User is not playing a game.
631 message
+= u
""" Not playing anything right now @sep"""
633 self
.msg(channel
, message
)
635 def command_lastfm(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
637 user
= self
.lastfm
.get_user(arg
)
639 self
.errormsg(channel
, user
['message'])
641 latest
= self
.lastfm
.get_recent_tracks(arg
, 1)
642 except FeedError
as e
:
643 self
.errormsg(channel
, e
.msg
)
648 if 'realname' in user
:
650 userinfo
.append(user
['realname'])
652 userinfo
.append(user
['name'])
655 userinfo
.append(user
['age'])
656 if 'country' in user
:
658 userinfo
.append(user
['country'])
660 userinfo
= ' [%s]' % ', '.join(userinfo
)
662 if 'track' in latest
['recenttracks']:
663 if isinstance(latest
['recenttracks']['track'], list):
664 if latest
['recenttracks']['track']:
665 latest
= latest
['recenttracks']['track'][0]
669 latest
= latest
['recenttracks']['track']
671 latest
['@attr']['nowplaying']
672 latest_str
= u
' @bNow playing@b {latest[artist][#text]} - {latest[name]} @sep'.format(latest
=latest
)
675 latestdate
= get_timespan(datetime
.fromtimestamp(int(latest
['date']['uts'])))
676 latest_str
= u
' @bLatest track@b {latest[artist][#text]} - {latest[name]} ({latestdate} ago) @sep'.format(
677 latest
=latest
, latestdate
=latestdate
)
683 regdate
= datetime
.fromtimestamp(int(user
['registered']['unixtime'])).strftime('%Y-%m-%d %H:%M:%S')
685 self
.msg(channel
, u
'@sep @b{user[name]}@b{userinfo} @sep @bPlays@b {plays} since {regdate} @sep \
686 @bLink@b {user[url]} @sep{latest_track}'.format(
688 plays
= format_thousand(int(user
['playcount'])),
691 latest_track
= latest_str
))
693 def command_url_shorten(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
694 if not arg
.startswith('http://') and not arg
.startswith('https://'):
695 self
.errormsg(channel
, 'a valid URL must start with http:// or https://')
699 reply
= self
.urls
.shorten(arg
)
701 self
.errormsg(channel
, e
.msg
)
704 if reply
['status_code'] != 200:
705 self
.errormsg(channel
, 'an error occurred.')
706 self
.elog
.warning('[shorten] error: code %d, %s' % (reply
['status_code'], reply
['status_txt']))
708 self
.msg(channel
, '@sep @bShort URL@b %s @sep' % reply
['data']['url'])
710 def command_url_expand(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
711 if not arg
.startswith('http://') and not arg
.startswith('https://'):
712 self
.errormsg(channel
, 'a valid URL must start with http:// or https://')
716 reply
= self
.urls
.expand(arg
)
718 self
.errormsg(channel
, e
.msg
)
722 self
.errormsg(channel
, reply
['error'])
723 elif not reply
['long-url']:
724 self
.msg(channel
, '@sep @bLong URL@b URL does not redirect')
726 self
.msg(channel
, '@sep @bLong URL@b {reply[long-url]} @sep'.format(reply
=reply
))
728 def command_idlerpg(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
730 player
= IrpgPlayer(arg
)
732 self
.errormsg(channel
, e
.msg
)
736 self
.errormsg(channel
, 'player not found. @bNote@b: nicks are case sensitive.')
739 self
.msg(channel
, """@sep @b{player.name}@b [{status}] @sep @bLevel@b {player.level} {player.classe} @sep @bNext level@b \
740 {nextlevel} @sep @bIdled@b {idled_for} @sep @bAlignment@b {player.alignment} @sep""".format(
742 status
= '@c3ON@c' if player
.is_online
else '@c4OFF@c',
743 nextlevel
= timedelta(seconds
=player
.ttl
),
744 idled_for
= timedelta(seconds
=player
.idled_for
)))
746 def command_ipinfo(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
748 reply
= self
.ipinfo
.get_info(arg
)
750 self
.errormsg(channel
, e
.msg
)
753 self
.msg(channel
, """@sep @bIP/Host@b {arg} ({reply[ip_addr]}) @sep @bLocation@b {reply[city]}, {reply[region]}, \
754 {reply[country_name]} [{reply[country_code]}] @sep{map}""".format(
757 map = ' http://maps.google.com/maps?q=%s,%s @sep' % (reply
['latitude'], reply
['longitude']) if reply
['latitude'] and reply
['longitude'] else ''))
759 dice_regex
= re
.compile('^(?:(\d+)d)?(\d+)(?:([\+\-])(\d+))?$')
761 def command_dice(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
762 r
= dice_regex
.search(arg
)
764 self
.errormsg(channel
, 'invalid format')
767 num
, faces
, type, modifier
= r
.groups()
773 if num
< 1 or num
> 32 or faces
< 2 or faces
> 65536:
774 self
.errormsg(channel
, 'parameter out of range')
779 for n
in xrange(int(num
)):
780 randnum
= random
.randint(1, int(faces
))
782 results
.append(randnum
)
785 modifier
= int(modifier
)
787 max = num
* faces
- modifier
789 modifier
= int(modifier
)
791 max = num
* faces
+ modifier
795 self
.msg(channel
, '@sep @bTotal@b {total} / {max} [{percent}%] @sep @bResults@b {results} @sep'.format(
798 percent
= 100 * total
/ max if max != 0 else '9001',
799 results
= str(results
)))
801 def command_qdb(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
804 quote
= self
.quotes
.get_qdb_random()
808 quote
= self
.quotes
.get_qdb_id(quote_id
)
810 self
.errormsg(channel
, 'quote @b%d@b not found' % quote_id
)
813 self
.errormsg(channel
, 'invalid quote ID')
815 except ExpatError
: # qdb returns a malformed xml when the quote doesn't exist
816 self
.errormsg(channel
, 'quote @b%d@b not found' % quote_id
)
819 self
.errormsg(channel
, e
.msg
)
823 for line
in quote
['lines']:
824 self
.msg(channel
, u
'[qdb {id}] {line}'.format(id=id, line
=line
.replace('\n', '')))
826 def command_fml(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
829 quote
= self
.quotes
.get_fml()
833 quote
= self
.quotes
.get_fml(quote_id
)
835 self
.errormsg(channel
, 'quote @b%d@b not found' % quote_id
)
837 except (ValueError, IndexError):
838 self
.errormsg(channel
, 'invalid quote ID')
840 except (FeedError
, FmlException
) as e
:
841 self
.errormsg(channel
, e
.msg
)
842 self
.elog
.warning('WARNING: .fml error: %s' % e
.msg
)
845 self
.msg(channel
, u
'[fml #{quote[id]}] {quote[text]}'.format(quote
=quote
))
847 def command_internets_info(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
848 self
.notice(sender
, '@sep @bRizon Internets Bot@b @sep @bDevelopers@b martin <martin@rizon.net> @sep @bHelp/feedback@b %(channel)s @sep' % {
849 'channel' : '#internets'})
851 def command_internets_help(self
, manager
, opts
, arg
, channel
, sender
, userinfo
):
852 command
= arg
.lower()
855 message
= ['internets: .help internets - for internets commands']
856 elif command
== 'internets':
857 message
= manager
.get_help()
859 message
= manager
.get_help(command
)
862 message
= ['%s is not a valid command.' % arg
]
865 self
.notice(sender
, line
)
867 class UserCommandManager(CommandManager
):
868 def get_prefix(self
):
871 def get_commands(self
):
874 'calc': (command_calc
, ARG_YES
, 'Calculates an expression', [], 'expression'),
876 'dict': 'dictionary',
877 'dictionary': (command_dictionary
, ARG_YES
, 'Search for a dictionary definition', [
878 ('number', '-n', 'display the n-th result', {'type': '+integer'}
, ARG_YES
),
879 ('all', '-a', 'display all results (using /notice)', {'action': 'store_true'}
, ARG_YES
)], 'word'),
881 'u': 'urbandictionary',
882 'urbandictionary': (command_urbandictionary
, ARG_YES
, 'Search for a definition on Urban Dictionary', [], 'word'),
885 'google': (command_google_search
, ARG_YES
, 'Search for something on Google', [], 'google_search'),
887 'gi': 'google_image',
888 'google_image': (command_google_image_search
, ARG_YES
, 'Search for images via Google Image', [], 'google_image_search'),
891 'translate': (command_bing_translate
, ARG_YES
, 'Translate something from a language to another', [], 'from to text'),
894 'youtube': (command_youtube_search
, ARG_YES
, 'Search for something on YouTube', [], 'youtube_search'),
897 'weather': (command_weather
, ARG_OPT
, 'Displays current weather conditions for a location', [
898 ('nick', '-n', 'use the weather location linked to a nick', {'action': 'store_true'}
, ARG_YES
)]),
901 'forecast': (command_forecast
, ARG_OPT
, 'Displays 5-day forecast for a location', [
902 ('nick', '-n', 'use the weather location linked to a nick', {'action': 'store_true'}
, ARG_YES
)]),
904 'regloc': 'register_location',
905 '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'),
907 'imdb': (command_imdb
, ARG_YES
, 'Search for information on a movie on IMDB', [], 'movie_title'),
909 'lastfm': (command_lastfm
, ARG_YES
, 'Returns information on a Last.fm user', [], 'lastfm_user'),
911 'shorten': (command_url_shorten
, ARG_YES
, 'Shortens a URL using http://j.mp', [], 'long_url'),
913 'expand': (command_url_expand
, ARG_YES
, 'Expands a shortened URL using http://longurl.org', [], 'shortened_url'),
916 'idlerpg': (command_idlerpg
, ARG_YES
, 'Returns info on a player in Rizon IdleRPG (http://idlerpg.rizon.net/)', [], 'player_name'),
918 'ipinfo': (command_ipinfo
, ARG_YES
, 'Returns short info on a IP address/hostname', [], 'ip/host'),
921 'dice': (command_dice
, ARG_YES
, 'Rolls X N-sided dice with an optional modifier A (XdN+A format)', [], 'dice_notation'),
923 'qdb': (command_qdb
, ARG_OPT
, 'Displays a quote from qdb.us', []),
925 'fml': (command_fml
, ARG_OPT
, 'Displays a quote from http://www.fmylife.com', []),
927 'steam': (command_steam
, ARG_OPT
, 'Shows your steam information', [
928 ('nick', '-n', 'use the steamid linked to a nick.', {'action': 'store_true'}
, ARG_YES
),
929 ('games', '-g', 'shows the total games owned by nick and shows most played game.', {'action': 'store_true'}
, ARG_NO
)]),
931 'regsteam': 'register_steam',
932 'register_steam': (command_register_steam
, ARG_YES
, 'Registers your Steam user ID', [], 'steamid'),
935 'twitch': (command_twitch
, ARG_OPT
, 'Displays information about twitch.tv streams/games/channels/teams', [
936 ('stream', '-s', 'searches for a particular stream, or displays the top streams if no argument provided', {'action': 'store_true'}
, ARG_OPT
),
937 ('channel', '-c', 'lists the top videos of a particular channel on twitch.tv', {'action': 'store_true'}
, ARG_YES
),
938 ('game', '-g', 'searches for a game on twitch.tv', {'action': 'store_true'}
, ARG_YES
),
939 ('video', '-v', 'allows you to search the twitch.tv site for a video', {'action': 'store_true'}
, ARG_YES
),
940 ('team', '-t', 'displays the info of a particular stream on twitch.tv, or or displays the top teams if no argument provided', {'action': 'store_true'}
, ARG_OPT
)]),
942 'info': (command_internets_info
, ARG_NO|ARG_OFFLINE
, 'Displays version and author information', []),
943 'help': (command_internets_help
, ARG_OPT|ARG_OFFLINE
, 'Displays available commands and their usage', []),