]> jfr.im git - irc/rizon/acid.git/blob - pyva/pyva/src/main/python/internets/cmd_user.py
1270e39997f12124f3e83739758263ea9facb4e7
[irc/rizon/acid.git] / pyva / pyva / src / main / python / internets / cmd_user.py
1 import random
2 import re
3 from datetime import datetime, timedelta
4 from xml.parsers.expat import ExpatError
5
6 import core
7 from pseudoclient.cmd_manager import *
8 from utils 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
15
16 import pyva_net_rizon_acid_core_User as User
17
18 RE_YT_PATTERN = re.compile(
19 "(?:www\\.|m\\.)?(?:(?:youtube\\.com/(?:watch)?(?:[?&][a-z]+=[a-z_]+)?(?:[?&]v=))"
20 "|(?:youtu\\.be\\/))([a-zA-Z0-9-_]+)")
21
22
23 def onPrivmsg_regex_youtube(self, source, target, message):
24 userinfo = User.findUser(source)
25 myself = User.findUser(self.nick)
26
27 sender = userinfo['nick']
28 channel = target
29
30 yt_links = RE_YT_PATTERN.findall(message)
31
32 if yt_links and self.google._check_link_eligibility(channel, yt_links[0]):
33 try:
34 video = self.google.yt_video(yt_links[0], userip=userinfo['ip'] if userinfo['ip'] != '0' else None)
35 except:
36 # Silently die
37 return
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
45 })
46 else:
47 return True
48
49
50 def get_citystate_from_zipcode(self, zipcode):
51 """Return [city,state] for the given U.S. zip code (if database has been imported)"""
52 try:
53 con = core.dbpool.get_connection()
54 try:
55 cursor = con.cursor()
56 cursor.execute("SELECT city, state FROM zipcode_citystate WHERE zipcode=%s", [int(zipcode)])
57 city, state = cursor.fetchone()
58 return city, state
59 finally:
60 core.dbpool.put_connection(con)
61 except:
62 return None
63
64 ##
65 # Returns colour coded test of persona state in Steam.
66 ##
67 def get_personastate_text(self, state):
68 if state == 0:
69 # user is offline.
70 return '@c14OFFLINE@c'
71 elif state == 1:
72 # user is online
73 return '@c3ONLINE@c'
74 elif state == 2:
75 # user is busy
76 return '@c4BUSY@c'
77 elif state == 3:
78 # user is away
79 return '@c7AWAY@c'
80 elif state == 4:
81 # user is snooze
82 return '@c7SNOOZE@c'
83 elif state == 5:
84 # user is looking to trade
85 return '@c5LOOKING TO TRADE@c'
86 elif state == 6:
87 # user is looking to play
88 return '@c10LOOKING TO PLAY@c'
89 else:
90 # unknown status
91 return '@c14UNKNOWN@c'
92
93 def command_weather(self, manager, opts, arg, channel, sender, userinfo):
94 arg = self.get_location(opts, arg, channel, sender)
95 if not arg:
96 return
97 w_state = ''
98 if arg.isdigit():
99 location = get_citystate_from_zipcode(self, arg)
100 if location is None:
101 self.errormsg(channel, 'zip code not recognised.')
102 return False
103 city, state = location
104 location = '{city}, {state}, USA'.format(city=city, state=state)
105 w_state = state + u', '
106 else:
107 location = arg.strip()
108
109 try:
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')
115 else:
116 self.errormsg(channel, exc)
117 return
118 except FeedError, e:
119 self.errormsg(channel, e.msg)
120 return
121
122 code = get_tempcolor(w['temp_c'])
123
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)))
129
130
131 def command_forecast(self, manager, opts, arg, channel, sender, userinfo):
132 arg = self.get_location(opts, arg, channel, sender)
133 if not arg:
134 return
135 w_state = ''
136 if arg.isdigit():
137 location = get_citystate_from_zipcode(self, arg)
138 if location is None:
139 self.errormsg(channel, 'zip code not recognised.')
140 return False
141 city, state = location
142 location = '{city}, {state}, USA'.format(city=city, state=state)
143 w_state = state + u', '
144 else:
145 location = arg.strip()
146
147 try:
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')
153 else:
154 self.errormsg(channel, exc)
155 return
156 except FeedError, e:
157 self.errormsg(channel, e.msg)
158 return
159
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']])
162
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))
164
165
166 def command_register_location(self, manager, opts, arg, channel, sender, userinfo):
167 arg = arg.strip()
168 try:
169 w_state = ''
170 if arg.isdigit():
171 location = get_citystate_from_zipcode(self, arg)
172 if location is None:
173 self.errormsg(channel, 'zip code not recognised.')
174 return False
175 city, state = location
176 location = '{city}, {state}, USA'.format(city=city, state=state)
177 w_state = state + u', '
178 else:
179 location = arg.strip()
180
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')
186 else:
187 self.errormsg(channel, exc)
188 return
189 except FeedError, e:
190 self.errormsg(channel, e.msg)
191 return
192
193 loc_name = u'{w[city]}{w_state}{w[country]}'.format(w=w, w_state=w_state)
194
195 self.users.set(sender, 'location', arg)
196 self.msg(channel, u'%s: registered location @b%s@b' % (sender, loc_name))
197
198 def command_bing_translate(self, manager, opts, arg, channel, sender, userinfo):
199 sp = arg.split(' ', 2)
200 try:
201 if len(sp) > 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)
205 target = None
206 translation = self.bing.translate(arg)
207 else:
208 source = source.lower()
209 target = target.lower()
210 translation = self.bing.translate(text, source, target)
211 else:
212 source = self.bing.detect_language(arg)
213 target = None
214 translation = self.bing.translate(arg)
215 except FeedError, e:
216 self.elog.warning('WARNING: Bing translate failed: %s' % e)
217 self.errormsg(channel, e.msg)
218 return
219
220 self.msg(channel, '[t] [from %s] %s' % (source, translation))
221
222 def command_google_search(self, manager, opts, arg, channel, sender, userinfo):
223 try:
224 result = self.google.search(arg, userinfo['ip'] if userinfo['ip'] != '0' else None)
225 except FeedError, e:
226 self.errormsg(channel, e.msg)
227 return
228
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.')
232 return
233
234 result = result['responseData']['results']
235 if not result:
236 self.msg(channel, '[Google] No results found')
237 return
238
239 json = result[0]
240 self.msg(channel, '[Google] @b%(title)s@b <@u%(url)s@u>' % {
241 'title': unescape(json['titleNoFormatting']),
242 'url': json['unescapedUrl']})
243
244 if json['content'] != '':
245 self.msg(channel, '[Google] @bDescription@b: %s' % unescape(json['content']).replace('<b>', '@b').replace('</b>', '@b'))
246
247 def command_google_image_search(self, manager, opts, arg, channel, sender, userinfo):
248 try:
249 result = self.google.image_search(arg, userinfo['ip'] if userinfo['ip'] != '0' else None)
250 except FeedError, e:
251 self.errormsg(channel, e.msg)
252 return
253
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.')
257 return
258
259 result = result['responseData']['results']
260 if not result:
261 self.msg(channel, '[Google Image] No results found')
262 return
263
264 json = result[0]
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']})
270
271 self.msg(channel, '[Google Image] @bSize@b: %(width)sx%(height)spx%(desc)s' % {
272
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 ''})
276
277
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
283 try:
284 result = self.wolfram.alpha(arg)
285 except FeedError as e:
286 self.errormsg(channel, e.msg)
287 return
288
289 if result is None:
290 self.msg(channel, '[W|A] Invalid input.')
291 else:
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))
295 else:
296 self.msg(channel, u'[W|A] {r[0]} = {r[1]}'.format(r=result))
297
298 def command_youtube_search(self, manager, opts, arg, channel, sender, userinfo):
299 try:
300 res = self.google.yt_search(arg, userip=userinfo['ip'] if userinfo['ip'] != '0' else None)
301 except FeedError, e:
302 self.errormsg(channel, e.msg)
303 return
304
305 if not res:
306 self.msg(channel, '[YouTube] No results found')
307 return
308
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
317 })
318
319 def command_twitch(self, manager, opts, arg, channel, sender, userinfo):
320 MAX_RESULTS = 5
321 expr, sep, res_num = arg.partition('/')
322 arg = expr.strip()
323 try:
324 if 'stream' in opts:
325 if arg.strip(): # search for a particular stream
326 [total, streams] = self.twitch.search_stream(arg)
327 if not total:
328 self.errormsg(channel, u"No results found.")
329 return
330 if res_num:
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))
334 return
335 i = 1
336 for s in streams:
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))
340 i += 1
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()
344 if res_num:
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))
348 return
349 i = 1
350 total = len(streams)
351 for s in streams:
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))
355 i += 1
356 self.notice(sender, u"To view a particular result, type: @b.tw -s /@unumber@u@b")
357
358 elif 'channel' in opts:
359 videos = self.twitch.get_channel(arg.strip())
360 if videos:
361 if res_num:
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))
364 return
365 self.msg(channel, "@sep Videos in channel @b{}@b @sep".format(arg))
366 i = 1
367 total = len(videos)
368 for v in videos:
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))
372 i += 1
373 self.notice(sender, "To view a particular result, type: @b.tw -c {} /@unumber@u@b".format(arg.replace('+', ' ')))
374 else:
375 self.errormsg(channel, "Channel @b{}@b does not exit.".format(arg))
376
377 elif 'game' in opts:
378 games = self.twitch.search_games(arg)
379 if not games:
380 self.errormsg(channel, "Game @b{}@b does not exit.".format(arg))
381 return
382 if res_num:
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))
386 return
387 self.msg(channel, "Results for @b{}@b".format(arg.replace('+', ' ')))
388 i = 1
389 total = len(games)
390 for g in games:
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))
394 i += 1
395 self.notice(sender, "To view a particular result, type: @b.tw -g {} /@unumber@u@b".format(arg.replace('+', ' ')))
396
397 elif 'team' in opts:
398 if arg.strip(): # search for a particular team
399 t = self.twitch.get_team(arg)
400 if t:
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))
403 else:
404 self.errormsg(channel, "Team @b{}@b does not exist.".format(arg))
405 else: # list top teams
406 teams = self.twitch.get_teams()
407 if not teams:
408 self.errormsg(channel, u"No teams exist.")
409 return
410 if res_num:
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))
414 return
415 i = 1
416 total = len(teams)
417 for t in teams:
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))
421 i += 1
422 self.notice(sender, u"To view a particular result, type: @b.tw -t /@unumber@u@b")
423
424 elif 'video' in opts:
425 self.msg(channel, "@sep http://www.twitch.tv/search?query={} @sep".format(arg.replace(' ', '+')))
426
427 else:
428 self.notice(sender, u"Type: @b.help twitch@b for the twitch.tv bot command syntax.")
429 except ValueError:
430 self.errormsg(channel, u"What's after the slash must be an integer number.")
431 except IndexError:
432 self.errormsg(channel, u"No result with that number exists.")
433
434
435 def command_dictionary(self, manager, opts, arg, channel, sender, userinfo):
436 try:
437 results = self.wordnik.definition(arg)
438 except FeedError, e:
439 self.errormsg(channel, e.msg)
440 return
441
442 if not results:
443 self.msg(channel, '[dictionary] Nothing found')
444 return
445
446 if 'all' in opts:
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))
453 return
454
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)))
458 else:
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)))
462 if n == 4:
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))
464 break
465
466 def command_urbandictionary(self, manager, opts, arg, channel, sender, userinfo):
467 expr, sep, def_id = arg.partition('/')
468 try:
469 res = self.urbandictionary.get_definitions(expr.strip())
470 except FeedError, e:
471 self.errormsg(channel, e.msg)
472 self.elog.warning('feed error in .urbandictionary: %s' % e)
473 return
474
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':
478 if def_id:
479 try:
480 def_id = int(def_id)
481 if def_id < 1:
482 self.errormsg(channel, 'invalid definition number')
483 return
484
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(
489 num = def_id,
490 total = len(res['list']),
491 res = res,
492 definition = definition if len(definition) < 200 else definition[:200] + '...',
493 entry = entry))
494 self.msg(channel, u'@sep @bExample@b %s @sep' % (example if len(example) < 280 else example[:280] + '...'))
495 except ValueError:
496 self.errormsg(channel, 'invalid definition number')
497 except IndexError:
498 self.errormsg(channel, 'definition id out of range: only %d definitions available' % len(res['list']))
499 else:
500 for num, entry in enumerate(res['list'], 1):
501 if num == 4:
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']))
503 break
504
505 definition = entry['definition'].replace('\r\n', ' / ').replace('\n', ' / ')
506 self.msg(channel, u'@sep [{num}/{total}] {entry[word]} @sep {definition} @sep'.format(
507 num = num,
508 total = len(res['list']),
509 res = res,
510 definition = definition if len(definition) < 200 else definition[:200] + '...',
511 entry = entry))
512 else:
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'])
515
516 def command_imdb(self, manager, opts, arg, channel, sender, userinfo):
517 try:
518 reply = self.imdb.get(arg)
519 except FeedError, e:
520 self.errormsg(channel, e.msg)
521 return
522 except ValueError:
523 self.errormsg(channel, 'movie not found')
524 return
525
526 if reply['Response'] != 'True':
527 self.msg(channel, '[imdb] Nothing found')
528 return
529
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))
533
534 #
535 # Registers the user's steam ID and links it to his/her nickname.
536 #
537 def command_register_steam(self, manager, opts, arg, channel, sender, userinfo):
538 arg = arg.strip()
539 try:
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')
545
546 ##
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.
549 ##
550 def command_steam(self, manager, opts, arg, channel, sender, userinfo):
551 steamuser = self.get_steamid(opts, arg, channel, sender)
552 if not steamuser:
553 return
554 steam_data = self.steam.get_status(steamuser.steamid)
555
556 if steam_data['communityvisibilitystate'] == 1:
557 # Profile is hidden
558 self.notice(sender, 'Profile is hidden. If you want to use this functionality, set your profile to Public.')
559 return
560
561 if 'games' in opts:
562 steam_games = self.steam.get_games(steamuser.steamid)
563 playtime_forever = 0
564 playtime_2weeks = 0
565 playtime_forever_top = 0
566 game_forever = ""
567 playtime_2weeks_top = 0
568 game_2weeks = ""
569 message = u"""@sep @b{player}@b @sep""".format(player = steam_data['personaname'])
570
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)
575 return
576
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
583
584 try:
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
590 except Exception:
591 #just skip it
592 continue
593
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(
601 fgame = fgame,
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(
606 wgame = wgame,
607 wtime = round(playtime_2weeks / 60, 0))
608 self.msg(channel, message)
609 else:
610 # Prepare 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)
621 else:
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'])
629 else:
630 # User is not playing a game.
631 message += u""" Not playing anything right now @sep"""
632
633 self.msg(channel, message)
634
635 def command_lastfm(self, manager, opts, arg, channel, sender, userinfo):
636 try:
637 user = self.lastfm.get_user(arg)
638 if 'error' in user:
639 self.errormsg(channel, user['message'])
640 return
641 latest = self.lastfm.get_recent_tracks(arg, 1)
642 except FeedError as e:
643 self.errormsg(channel, e.msg)
644 return
645
646 user = user['user']
647 userinfo = []
648 if 'realname' in user:
649 if user['realname']:
650 userinfo.append(user['realname'])
651 else:
652 userinfo.append(user['name'])
653 if 'age' in user:
654 if user['age']:
655 userinfo.append(user['age'])
656 if 'country' in user:
657 if user['country']:
658 userinfo.append(user['country'])
659
660 userinfo = ' [%s]' % ', '.join(userinfo)
661
662 if 'track' in latest['recenttracks']:
663 if isinstance(latest['recenttracks']['track'], list):
664 if latest['recenttracks']['track']:
665 latest = latest['recenttracks']['track'][0]
666 else:
667 lastest = ''
668 else:
669 latest = latest['recenttracks']['track']
670 try:
671 latest['@attr']['nowplaying']
672 latest_str = u' @bNow playing@b {latest[artist][#text]} - {latest[name]} @sep'.format(latest=latest)
673 except KeyError:
674 if 'date' in 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)
678 else:
679 latest_str = ''
680 else:
681 latest_str = ''
682
683 regdate = datetime.fromtimestamp(int(user['registered']['unixtime'])).strftime('%Y-%m-%d %H:%M:%S')
684
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(
687 userinfo = userinfo,
688 plays = format_thousand(int(user['playcount'])),
689 regdate = regdate,
690 user = user,
691 latest_track = latest_str))
692
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://')
696 return
697
698 try:
699 reply = self.urls.shorten(arg)
700 except FeedError, e:
701 self.errormsg(channel, e.msg)
702 return
703
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']))
707 else:
708 self.msg(channel, '@sep @bShort URL@b %s @sep' % reply['data']['url'])
709
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://')
713 return
714
715 try:
716 reply = self.urls.expand(arg)
717 except FeedError, e:
718 self.errormsg(channel, e.msg)
719 return
720
721 if 'error' in reply:
722 self.errormsg(channel, reply['error'])
723 elif not reply['long-url']:
724 self.msg(channel, '@sep @bLong URL@b URL does not redirect')
725 else:
726 self.msg(channel, '@sep @bLong URL@b {reply[long-url]} @sep'.format(reply=reply))
727
728 def command_idlerpg(self, manager, opts, arg, channel, sender, userinfo):
729 try:
730 player = IrpgPlayer(arg)
731 except FeedError, e:
732 self.errormsg(channel, e.msg)
733 return
734
735 if not player.name:
736 self.errormsg(channel, 'player not found. @bNote@b: nicks are case sensitive.')
737 return
738
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(
741 player = player,
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)))
745
746 def command_ipinfo(self, manager, opts, arg, channel, sender, userinfo):
747 try:
748 reply = self.ipinfo.get_info(arg)
749 except FeedError, e:
750 self.errormsg(channel, e.msg)
751 return
752
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(
755 reply = reply,
756 arg = arg.lower(),
757 map = ' http://maps.google.com/maps?q=%s,%s @sep' % (reply['latitude'], reply['longitude']) if reply['latitude'] and reply['longitude'] else ''))
758
759 dice_regex = re.compile('^(?:(\d+)d)?(\d+)(?:([\+\-])(\d+))?$')
760
761 def command_dice(self, manager, opts, arg, channel, sender, userinfo):
762 r = dice_regex.search(arg)
763 if not r:
764 self.errormsg(channel, 'invalid format')
765 return
766
767 num, faces, type, modifier = r.groups()
768 if num:
769 num = int(num)
770 else:
771 num = 1
772 faces = int(faces)
773 if num < 1 or num > 32 or faces < 2 or faces > 65536:
774 self.errormsg(channel, 'parameter out of range')
775 return
776
777 total = 0
778 results = []
779 for n in xrange(int(num)):
780 randnum = random.randint(1, int(faces))
781 total += randnum
782 results.append(randnum)
783
784 if type == '-':
785 modifier = int(modifier)
786 total -= modifier
787 max = num * faces - modifier
788 elif type == '+':
789 modifier = int(modifier)
790 total += modifier
791 max = num * faces + modifier
792 else:
793 max = num * faces
794
795 self.msg(channel, '@sep @bTotal@b {total} / {max} [{percent}%] @sep @bResults@b {results} @sep'.format(
796 total = total,
797 max = max,
798 percent = 100 * total / max if max != 0 else '9001',
799 results = str(results)))
800
801 def command_qdb(self, manager, opts, arg, channel, sender, userinfo):
802 try:
803 if not arg:
804 quote = self.quotes.get_qdb_random()
805 else:
806 try:
807 quote_id = int(arg)
808 quote = self.quotes.get_qdb_id(quote_id)
809 if not quote:
810 self.errormsg(channel, 'quote @b%d@b not found' % quote_id)
811 return
812 except ValueError:
813 self.errormsg(channel, 'invalid quote ID')
814 return
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)
817 return
818 except FeedError, e:
819 self.errormsg(channel, e.msg)
820 return
821
822 id = quote['id']
823 for line in quote['lines']:
824 self.msg(channel, u'[qdb {id}] {line}'.format(id=id, line=line.replace('\n', '')))
825
826 def command_fml(self, manager, opts, arg, channel, sender, userinfo):
827 try:
828 if not arg:
829 quote = self.quotes.get_fml()
830 else:
831 try:
832 quote_id = int(arg)
833 quote = self.quotes.get_fml(quote_id)
834 if not quote:
835 self.errormsg(channel, 'quote @b%d@b not found' % quote_id)
836 return
837 except (ValueError, IndexError):
838 self.errormsg(channel, 'invalid quote ID')
839 return
840 except (FeedError, FmlException) as e:
841 self.errormsg(channel, e.msg)
842 self.elog.warning('WARNING: .fml error: %s' % e.msg)
843 return
844
845 self.msg(channel, u'[fml #{quote[id]}] {quote[text]}'.format(quote=quote))
846
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'})
850
851 def command_internets_help(self, manager, opts, arg, channel, sender, userinfo):
852 command = arg.lower()
853
854 if command == '':
855 message = ['internets: .help internets - for internets commands']
856 elif command == 'internets':
857 message = manager.get_help()
858 else:
859 message = manager.get_help(command)
860
861 if message == None:
862 message = ['%s is not a valid command.' % arg]
863
864 for line in message:
865 self.notice(sender, line)
866
867 class UserCommandManager(CommandManager):
868 def get_prefix(self):
869 return '.'
870
871 def get_commands(self):
872 return {
873 'cc': 'calc',
874 'calc': (command_calc, ARG_YES, 'Calculates an expression', [], 'expression'),
875
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'),
880
881 'u': 'urbandictionary',
882 'urbandictionary': (command_urbandictionary, ARG_YES, 'Search for a definition on Urban Dictionary', [], 'word'),
883
884 'g': 'google',
885 'google': (command_google_search, ARG_YES, 'Search for something on Google', [], 'google_search'),
886
887 'gi': 'google_image',
888 'google_image': (command_google_image_search, ARG_YES, 'Search for images via Google Image', [], 'google_image_search'),
889
890 't': 'translate',
891 'translate': (command_bing_translate, ARG_YES, 'Translate something from a language to another', [], 'from to text'),
892
893 'yt': 'youtube',
894 'youtube': (command_youtube_search, ARG_YES, 'Search for something on YouTube', [], 'youtube_search'),
895
896 'w': 'weather',
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)]),
899
900 'f': 'forecast',
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)]),
903
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'),
906
907 'imdb': (command_imdb, ARG_YES, 'Search for information on a movie on IMDB', [], 'movie_title'),
908
909 'lastfm': (command_lastfm, ARG_YES, 'Returns information on a Last.fm user', [], 'lastfm_user'),
910
911 'shorten': (command_url_shorten, ARG_YES, 'Shortens a URL using http://j.mp', [], 'long_url'),
912
913 'expand': (command_url_expand, ARG_YES, 'Expands a shortened URL using http://longurl.org', [], 'shortened_url'),
914
915 'irpg': 'idlerpg',
916 'idlerpg': (command_idlerpg, ARG_YES, 'Returns info on a player in Rizon IdleRPG (http://idlerpg.rizon.net/)', [], 'player_name'),
917
918 'ipinfo': (command_ipinfo, ARG_YES, 'Returns short info on a IP address/hostname', [], 'ip/host'),
919
920 'd': 'dice',
921 'dice': (command_dice, ARG_YES, 'Rolls X N-sided dice with an optional modifier A (XdN+A format)', [], 'dice_notation'),
922
923 'qdb': (command_qdb, ARG_OPT, 'Displays a quote from qdb.us', []),
924
925 'fml': (command_fml, ARG_OPT, 'Displays a quote from http://www.fmylife.com', []),
926
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)]),
930
931 'regsteam': 'register_steam',
932 'register_steam': (command_register_steam, ARG_YES, 'Registers your Steam user ID', [], 'steamid'),
933
934 'tw': 'twitch',
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)]),
941
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', []),
944 }