]>
Commit | Line | Data |
---|---|---|
685e346e A |
1 | import random |
2 | import re | |
3 | from datetime import datetime, timedelta | |
4 | from xml.parsers.expat import ExpatError | |
5 | ||
b4079de9 | 6 | import core |
685e346e A |
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 | |
9c9f0247 | 14 | from api.steam import SteamException |
685e346e | 15 | |
adadb95c M |
16 | import pyva_net_rizon_acid_core_User as User |
17 | ||
62c723d6 DO |
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 | ||
adadb95c M |
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 | ||
62c723d6 | 30 | yt_links = RE_YT_PATTERN.findall(message) |
adadb95c M |
31 | |
32 | if yt_links and self.google._check_link_eligibility(channel, yt_links[0]): | |
adadb95c M |
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']), | |
88d49644 D |
43 | 'liked': format_thousand(video['liked']), |
44 | 'disliked': format_thousand(video['disliked']) | |
adadb95c M |
45 | }) |
46 | else: | |
47 | return True | |
48 | ||
49 | ||
685e346e A |
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: | |
b4079de9 A |
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) | |
685e346e A |
61 | except: |
62 | return None | |
63 | ||
9c9f0247 M |
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' | |
685e346e A |
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: | |
e5bad2fb | 224 | result = self.google.search(arg, userinfo['ip'] if userinfo['ip'] != '0' else None) |
685e346e A |
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>' % { | |
cb6afe7c | 241 | 'title': unescape(json['titleNoFormatting']), |
685e346e | 242 | 'url': json['unescapedUrl']}) |
6b005024 KB |
243 | |
244 | if json['content'] != '': | |
245 | self.msg(channel, '[Google] @bDescription@b: %s' % unescape(json['content']).replace('<b>', '@b').replace('</b>', '@b')) | |
685e346e | 246 | |
cb6afe7c M |
247 | def command_google_image_search(self, manager, opts, arg, channel, sender, userinfo): |
248 | try: | |
e5bad2fb | 249 | result = self.google.image_search(arg, userinfo['ip'] if userinfo['ip'] != '0' else None) |
cb6afe7c M |
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 | ||
685e346e A |
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: | |
3d4ef992 A |
292 | resultLines = result[0].splitlines(True) if result[1] is None else result[1].splitlines(True) |
293 | lines = len(resultLines) # number of lines | |
294 | maxLineLength = max([len(x) for x in resultLines]) # max length of lines | |
295 | ||
4c4628e3 D |
296 | tosend = u'[W|A] {r[0]}'.format(r=result) # default to always show the first part |
297 | if result[1] is not None: | |
298 | tosend += u' = {r[1]}'.format(r=result) | |
299 | ||
3d4ef992 | 300 | if lines > self.output_limit or maxLineLength > self.max_line_length: |
4c4628e3 | 301 | self.notice(sender, tosend) |
d0baaf8a | 302 | else: |
4c4628e3 | 303 | self.msg(channel, tosend) |
685e346e A |
304 | |
305 | def command_youtube_search(self, manager, opts, arg, channel, sender, userinfo): | |
306 | try: | |
e5bad2fb | 307 | res = self.google.yt_search(arg, userip=userinfo['ip'] if userinfo['ip'] != '0' else None) |
685e346e A |
308 | except FeedError, e: |
309 | self.errormsg(channel, e.msg) | |
310 | return | |
311 | ||
312 | if not res: | |
313 | self.msg(channel, '[YouTube] No results found') | |
314 | return | |
315 | ||
316 | self.msg(channel, """@sep @bYouTube@b %(title)s @sep @bURL@b %(url)s (%(duration)s) @sep @bViews@b %(views)s @sep \ | |
c7a15283 | 317 | @bRating@b @c3@b[+]@b %(liked)s likes @c4@b[-]@b %(disliked)s dislikes @sep""" % { |
685e346e A |
318 | 'title': res['title'], |
319 | 'url': 'http://www.youtube.com/watch?v=' + res['id'], | |
320 | 'duration': '%s' % format_hms(res['duration']), | |
321 | 'views': format_thousand(res['view_count']), | |
685e346e A |
322 | 'liked': format_thousand(res['liked']) if res['liked'] else 0, |
323 | 'disliked': format_thousand(res['disliked']) if res['disliked'] else 0 | |
324 | }) | |
325 | ||
f0ac597d | 326 | def command_twitch(self, manager, opts, arg, channel, sender, userinfo): |
327 | MAX_RESULTS = 5 | |
328 | expr, sep, res_num = arg.partition('/') | |
329 | arg = expr.strip() | |
330 | try: | |
331 | if 'stream' in opts: | |
332 | if arg.strip(): # search for a particular stream | |
333 | [total, streams] = self.twitch.search_stream(arg) | |
334 | if not total: | |
335 | self.errormsg(channel, u"No results found.") | |
336 | return | |
337 | if res_num: | |
338 | s = streams[int(res_num)-1] | |
339 | self.msg(channel, (u"@sep Game @b{s[game]}@b @sep has status @b{s[status]}@b on channel @u{s[chan]}@u, " + | |
340 | u"which has {s[views]} views and {s[followers]} followers @sep Link: {s[url]} @sep").format(s=s)) | |
341 | return | |
342 | i = 1 | |
343 | for s in streams: | |
344 | if i > MAX_RESULTS: break | |
345 | 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, " + | |
346 | u"which has {s[views]} views and {s[followers]} followers @sep Link: {s[url]} @sep").format(i=i, total=total, s=s)) | |
347 | i += 1 | |
348 | self.notice(sender, "To view a particular result, type: @b.tw -s {} /@unumber@u@b".format(arg.replace('+', ' '))) | |
349 | else: # list top streams | |
350 | streams = self.twitch.get_streams() | |
351 | if res_num: | |
352 | s = streams[int(res_num)-1] | |
353 | self.msg(channel, (u"@sep 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)) | |
355 | return | |
356 | i = 1 | |
357 | total = len(streams) | |
358 | for s in streams: | |
359 | if i > MAX_RESULTS: break | |
360 | self.msg(channel, (u"@sep [{i}/{total}] Game @b{s[game]}@b @sep has a stream created on {s[created_at]} that has " + | |
361 | u"{s[viewers]} viewers @sep Related channel: @b{s[channel]}@b @sep").format(s=s, i=i, total=total)) | |
362 | i += 1 | |
363 | self.notice(sender, u"To view a particular result, type: @b.tw -s /@unumber@u@b") | |
364 | ||
365 | elif 'channel' in opts: | |
366 | videos = self.twitch.get_channel(arg.strip()) | |
367 | if videos: | |
368 | if res_num: | |
369 | v = videos[int(res_num)-1] | |
370 | self.msg(channel, u"@sep @b{v[title]}@b @sep with {v[views]} views @sep Link: {v[url]} @sep".format(v=v)) | |
371 | return | |
372 | self.msg(channel, "@sep Videos in channel @b{}@b @sep".format(arg)) | |
373 | i = 1 | |
374 | total = len(videos) | |
375 | for v in videos: | |
376 | if i > MAX_RESULTS: break | |
377 | self.msg(channel, "@sep [{i}/{total}] @b{v[title]}@b @sep with {v[views]} views @sep Link: {v[url]} @sep" | |
378 | .format(v=v, i=i, total=total)) | |
379 | i += 1 | |
380 | self.notice(sender, "To view a particular result, type: @b.tw -c {} /@unumber@u@b".format(arg.replace('+', ' '))) | |
381 | else: | |
382 | self.errormsg(channel, "Channel @b{}@b does not exit.".format(arg)) | |
383 | ||
384 | elif 'game' in opts: | |
385 | games = self.twitch.search_games(arg) | |
386 | if not games: | |
387 | self.errormsg(channel, "Game @b{}@b does not exit.".format(arg)) | |
388 | return | |
389 | if res_num: | |
390 | g = games[int(res_num)-1] | |
391 | self.msg(channel, (u"@sep @b{g[name]}@b @sep ranked @b{g[popularity]}@b in popularity @sep " + | |
392 | u"http://www.twitch.tv/search?query={g[name2]} @sep").format(g=g)) | |
393 | return | |
394 | self.msg(channel, "Results for @b{}@b".format(arg.replace('+', ' '))) | |
395 | i = 1 | |
396 | total = len(games) | |
397 | for g in games: | |
398 | if i > MAX_RESULTS: break | |
399 | self.msg(channel, (u"@sep [{i}/{total}] @b{g[name]}@b @sep ranked @b{g[popularity]}@b in popularity @sep " + | |
400 | u"http://www.twitch.tv/search?query={g[name2]} @sep").format(i=i, g=g, total=total)) | |
401 | i += 1 | |
402 | self.notice(sender, "To view a particular result, type: @b.tw -g {} /@unumber@u@b".format(arg.replace('+', ' '))) | |
403 | ||
404 | elif 'team' in opts: | |
405 | if arg.strip(): # search for a particular team | |
406 | t = self.twitch.get_team(arg) | |
407 | if t: | |
408 | self.msg(channel, (u"@sep Team @u{t[name]}@u @sep called @b{t[display_name]}@b @sep was created " + | |
409 | u"on {t[created_at]} @sep Info: {t[info]} @sep").format(t=t)) | |
410 | else: | |
411 | self.errormsg(channel, "Team @b{}@b does not exist.".format(arg)) | |
412 | else: # list top teams | |
413 | teams = self.twitch.get_teams() | |
414 | if not teams: | |
415 | self.errormsg(channel, u"No teams exist.") | |
416 | return | |
417 | if res_num: | |
418 | t = teams[int(res_num)-1] | |
419 | self.msg(channel, (u"@sep 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)) | |
421 | return | |
422 | i = 1 | |
423 | total = len(teams) | |
424 | for t in teams: | |
425 | if i > MAX_RESULTS: break | |
426 | self.msg(channel, (u"@sep [{i}/{total}] Team @u{t[name]}@u @sep called @b{t[display_name]}@b @sep was created " + | |
427 | u"on {t[created_at]} @sep Info: {t[info]} @sep").format(t=t, i=i, total=total)) | |
428 | i += 1 | |
429 | self.notice(sender, u"To view a particular result, type: @b.tw -t /@unumber@u@b") | |
430 | ||
431 | elif 'video' in opts: | |
432 | self.msg(channel, "@sep http://www.twitch.tv/search?query={} @sep".format(arg.replace(' ', '+'))) | |
433 | ||
434 | else: | |
435 | self.notice(sender, u"Type: @b.help twitch@b for the twitch.tv bot command syntax.") | |
436 | except ValueError: | |
437 | self.errormsg(channel, u"What's after the slash must be an integer number.") | |
438 | except IndexError: | |
439 | self.errormsg(channel, u"No result with that number exists.") | |
440 | ||
441 | ||
685e346e A |
442 | def command_dictionary(self, manager, opts, arg, channel, sender, userinfo): |
443 | try: | |
444 | results = self.wordnik.definition(arg) | |
445 | except FeedError, e: | |
446 | self.errormsg(channel, e.msg) | |
447 | return | |
448 | ||
449 | if not results: | |
450 | self.msg(channel, '[dictionary] Nothing found') | |
451 | return | |
452 | ||
453 | if 'all' in opts: | |
454 | for n, res in enumerate(results, 1): | |
455 | self.notice(sender, u'@sep [{num}/{tot}] @bDefinition@b {res.word} @sep {res.text} @sep'.format( | |
456 | res=res, num=n, tot=len(results))) | |
457 | elif 'number' in opts: | |
458 | if opts['number'] - 1 < 0 or opts['number'] - 1 > len(results): | |
459 | self.errormsg(channel, 'option -n out of range: only %d definitions found.' % len(results)) | |
460 | return | |
461 | ||
462 | result = results[opts['number'] - 1] | |
463 | self.msg(channel, u'@sep [{num}/{tot}] @bDefinition@b {res.word} @sep {res.text} @sep'.format( | |
464 | res=result, num=opts['number'], tot=len(results))) | |
465 | else: | |
466 | for n, res in enumerate(results, 1): | |
467 | self.msg(channel, u'@sep [{num}/{tot}] @bDefinition@b {res.word} @sep {res.text} @sep'.format( | |
468 | res=res, num=n, tot=len(results))) | |
469 | if n == 4: | |
470 | 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)) | |
471 | break | |
472 | ||
473 | def command_urbandictionary(self, manager, opts, arg, channel, sender, userinfo): | |
474 | expr, sep, def_id = arg.partition('/') | |
475 | try: | |
476 | res = self.urbandictionary.get_definitions(expr.strip()) | |
477 | except FeedError, e: | |
478 | self.errormsg(channel, e.msg) | |
479 | self.elog.warning('feed error in .urbandictionary: %s' % e) | |
480 | return | |
481 | ||
482 | if res['result_type'] == 'no_results' or res['result_type'] == 'fulltext': | |
483 | self.errormsg(channel, 'no results found') | |
484 | elif res['result_type'] == 'exact': | |
485 | if def_id: | |
486 | try: | |
487 | def_id = int(def_id) | |
488 | if def_id < 1: | |
489 | self.errormsg(channel, 'invalid definition number') | |
490 | return | |
491 | ||
492 | entry = res['list'][def_id - 1] | |
493 | definition = entry['definition'].replace('\r\n', ' / ').replace('\n', ' / ') | |
494 | example = entry['example'].replace('\r\n', ' / ').replace('\n', ' / ') | |
495 | self.msg(channel, u'@sep [{num}/{total}] {entry[word]} @sep {definition} @sep'.format( | |
496 | num = def_id, | |
497 | total = len(res['list']), | |
498 | res = res, | |
499 | definition = definition if len(definition) < 200 else definition[:200] + '...', | |
500 | entry = entry)) | |
501 | self.msg(channel, u'@sep @bExample@b %s @sep' % (example if len(example) < 280 else example[:280] + '...')) | |
502 | except ValueError: | |
503 | self.errormsg(channel, 'invalid definition number') | |
504 | except IndexError: | |
505 | self.errormsg(channel, 'definition id out of range: only %d definitions available' % len(res['list'])) | |
506 | else: | |
507 | for num, entry in enumerate(res['list'], 1): | |
508 | if num == 4: | |
7f571da5 | 509 | 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'])) |
685e346e A |
510 | break |
511 | ||
512 | definition = entry['definition'].replace('\r\n', ' / ').replace('\n', ' / ') | |
513 | self.msg(channel, u'@sep [{num}/{total}] {entry[word]} @sep {definition} @sep'.format( | |
514 | num = num, | |
515 | total = len(res['list']), | |
516 | res = res, | |
517 | definition = definition if len(definition) < 200 else definition[:200] + '...', | |
518 | entry = entry)) | |
519 | else: | |
520 | 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.') | |
521 | self.elog.warning('unrecognized result type: %s' % res['result_type']) | |
522 | ||
523 | def command_imdb(self, manager, opts, arg, channel, sender, userinfo): | |
524 | try: | |
525 | reply = self.imdb.get(arg) | |
526 | except FeedError, e: | |
527 | self.errormsg(channel, e.msg) | |
528 | return | |
529 | except ValueError: | |
530 | self.errormsg(channel, 'movie not found') | |
531 | return | |
532 | ||
533 | if reply['Response'] != 'True': | |
534 | self.msg(channel, '[imdb] Nothing found') | |
535 | return | |
536 | ||
537 | self.msg(channel, u"""@sep @b{r[Title]}@b [{r[Year]}] Rated {r[Rated]} @sep @bRating@b {r[imdbRating]}/10, {r[imdbVotes]} votes @sep \ | |
538 | @bGenre@b {r[Genre]} @sep @bDirector@b {r[Director]} @sep @bActors@b {r[Actors]} @sep @bRuntime@b {r[Runtime]} @sep""".format(r=reply)) | |
539 | self.msg(channel, u'@sep @bPlot@b {r[Plot]} @sep @uhttp://www.imdb.com/title/{r[imdbID]}/@u @sep'.format(r=reply)) | |
540 | ||
9c9f0247 M |
541 | # |
542 | # Registers the user's steam ID and links it to his/her nickname. | |
543 | # | |
544 | def command_register_steam(self, manager, opts, arg, channel, sender, userinfo): | |
545 | arg = arg.strip() | |
546 | try: | |
547 | steam_data = self.steam.find_user(arg) | |
548 | self.notice(sender, u'Steam ID registered, current personaname: {name}'.format(name = steam_data['personaname'])) | |
549 | self.users.set(sender, 'steamid', steam_data['steamid']) | |
550 | except SteamException as exc: | |
551 | self.notice(sender, 'No user found') | |
552 | ||
553 | ## | |
554 | # Shows user's online status and what game he/sh is playing. | |
555 | # Game does not show when user's profile is set to private. | |
556 | ## | |
557 | def command_steam(self, manager, opts, arg, channel, sender, userinfo): | |
558 | steamuser = self.get_steamid(opts, arg, channel, sender) | |
559 | if not steamuser: | |
560 | return | |
561 | steam_data = self.steam.get_status(steamuser.steamid) | |
562 | ||
563 | if steam_data['communityvisibilitystate'] == 1: | |
564 | # Profile is hidden | |
565 | self.notice(sender, 'Profile is hidden. If you want to use this functionality, set your profile to Public.') | |
566 | return | |
567 | ||
568 | if 'games' in opts: | |
569 | steam_games = self.steam.get_games(steamuser.steamid) | |
570 | playtime_forever = 0 | |
571 | playtime_2weeks = 0 | |
572 | playtime_forever_top = 0 | |
573 | game_forever = "" | |
574 | playtime_2weeks_top = 0 | |
575 | game_2weeks = "" | |
576 | message = u"""@sep @b{player}@b @sep""".format(player = steam_data['personaname']) | |
577 | ||
578 | if steam_games['game_count'] == 0: | |
579 | # You know, because it's possible | |
580 | message += u""" Does not own any games. @sep""" | |
581 | self.msg(channel, message) | |
582 | return | |
583 | ||
584 | for item in steam_games['games']: | |
585 | ptf = item['playtime_forever'] | |
586 | if ptf > playtime_forever_top: | |
587 | game_forever = item['appid'] | |
588 | playtime_forever_top = ptf | |
589 | playtime_forever += ptf | |
590 | ||
591 | try: | |
592 | ptw = item['playtime_2weeks'] | |
593 | if ptw > playtime_2weeks_top: | |
594 | game_2weeks = item['appid'] | |
595 | playtime_2weeks_top = ptw | |
596 | playtime_2weeks += ptw | |
597 | except Exception: | |
598 | #just skip it | |
599 | continue | |
600 | ||
601 | message += u""" @bTotal games:@b {total} @sep @bTotal playtime:@b {ftime} hours @sep @bPlaytime last 2 weeks:@b {wtime} hours @sep""".format( | |
602 | total = steam_games['game_count'], | |
603 | ftime = round(playtime_forever / 60, 0), | |
604 | wtime = round(playtime_2weeks / 60, 0)) | |
605 | if game_forever != "": | |
606 | fgame = self.steam.get_game_name(game_forever) | |
607 | message += u""" @bMost played game:@b {fgame}, {ftime} hours @sep""".format( | |
608 | fgame = fgame, | |
609 | ftime = round(playtime_forever_top / 60, 0)) | |
610 | if game_2weeks != "": | |
611 | wgame = self.steam.get_game_name(game_2weeks) | |
612 | message += u""" @bMost played last 2 weeks:@b {wgame}, {wtime} hours @sep""".format( | |
613 | wgame = wgame, | |
614 | wtime = round(playtime_2weeks / 60, 0)) | |
615 | self.msg(channel, message) | |
616 | else: | |
617 | # Prepare message | |
618 | message = u"""@sep @b{player}@b [{status}] @sep""".format( | |
619 | player = steam_data['personaname'], | |
620 | status = get_personastate_text(self, steam_data['personastate'])) | |
621 | if steam_data['personastate'] == 0 or steam_data['personastate'] > 7: | |
622 | # User is offline or unknown state | |
623 | # NOTE: lastlogoff is actual logoff timestamp, not "appear offline" timestamp | |
624 | latestdate = get_timespan(datetime.fromtimestamp(steam_data['lastlogoff'])) | |
625 | message += u""" @bLast seen@b {latestdate} ago @sep""".format( | |
626 | latestdate = latestdate) | |
627 | self.msg(channel, message) | |
628 | else: | |
629 | # user is online, busy, away, snooze, looking to trade or looking to play | |
c800a608 | 630 | if 'gameid' in steam_data: |
9c9f0247 | 631 | message += u""" @bPlaying:@b {gamename} @sep""".format( |
c800a608 | 632 | gamename = self.steam.get_game_name(steam_data['gameid'])) |
9c9f0247 M |
633 | if 'gameserverip' in steam_data: |
634 | message += u""" @bPlaying on server:@b {gameserver} @sep""".format( | |
635 | gameserver = steam_data['gameserverip']) | |
636 | else: | |
637 | # User is not playing a game. | |
638 | message += u""" Not playing anything right now @sep""" | |
639 | ||
640 | self.msg(channel, message) | |
641 | ||
685e346e A |
642 | def command_lastfm(self, manager, opts, arg, channel, sender, userinfo): |
643 | try: | |
644 | user = self.lastfm.get_user(arg) | |
645 | if 'error' in user: | |
646 | self.errormsg(channel, user['message']) | |
647 | return | |
648 | latest = self.lastfm.get_recent_tracks(arg, 1) | |
649 | except FeedError as e: | |
650 | self.errormsg(channel, e.msg) | |
651 | return | |
652 | ||
653 | user = user['user'] | |
654 | userinfo = [] | |
1486e205 | 655 | if 'realname' in user: |
656 | if user['realname']: | |
657 | userinfo.append(user['realname']) | |
685e346e | 658 | else: |
1486e205 | 659 | userinfo.append(user['name']) |
660 | if 'age' in user: | |
661 | if user['age']: | |
662 | userinfo.append(user['age']) | |
663 | if 'country' in user: | |
664 | if user['country']: | |
665 | userinfo.append(user['country']) | |
666 | ||
667 | userinfo = ' [%s]' % ', '.join(userinfo) | |
685e346e A |
668 | |
669 | if 'track' in latest['recenttracks']: | |
670 | if isinstance(latest['recenttracks']['track'], list): | |
1486e205 | 671 | if latest['recenttracks']['track']: |
672 | latest = latest['recenttracks']['track'][0] | |
673 | else: | |
674 | lastest = '' | |
685e346e A |
675 | else: |
676 | latest = latest['recenttracks']['track'] | |
677 | try: | |
678 | latest['@attr']['nowplaying'] | |
679 | latest_str = u' @bNow playing@b {latest[artist][#text]} - {latest[name]} @sep'.format(latest=latest) | |
680 | except KeyError: | |
1486e205 | 681 | if 'date' in latest: |
682 | latestdate = get_timespan(datetime.fromtimestamp(int(latest['date']['uts']))) | |
683 | latest_str = u' @bLatest track@b {latest[artist][#text]} - {latest[name]} ({latestdate} ago) @sep'.format( | |
684 | latest=latest, latestdate=latestdate) | |
685 | else: | |
686 | latest_str = '' | |
685e346e A |
687 | else: |
688 | latest_str = '' | |
689 | ||
1486e205 | 690 | regdate = datetime.fromtimestamp(int(user['registered']['unixtime'])).strftime('%Y-%m-%d %H:%M:%S') |
691 | ||
685e346e A |
692 | self.msg(channel, u'@sep @b{user[name]}@b{userinfo} @sep @bPlays@b {plays} since {regdate} @sep \ |
693 | @bLink@b {user[url]} @sep{latest_track}'.format( | |
694 | userinfo = userinfo, | |
695 | plays = format_thousand(int(user['playcount'])), | |
1486e205 | 696 | regdate = regdate, |
685e346e A |
697 | user = user, |
698 | latest_track = latest_str)) | |
699 | ||
700 | def command_url_shorten(self, manager, opts, arg, channel, sender, userinfo): | |
701 | if not arg.startswith('http://') and not arg.startswith('https://'): | |
702 | self.errormsg(channel, 'a valid URL must start with http:// or https://') | |
703 | return | |
704 | ||
705 | try: | |
706 | reply = self.urls.shorten(arg) | |
707 | except FeedError, e: | |
708 | self.errormsg(channel, e.msg) | |
709 | return | |
710 | ||
711 | if reply['status_code'] != 200: | |
712 | self.errormsg(channel, 'an error occurred.') | |
713 | self.elog.warning('[shorten] error: code %d, %s' % (reply['status_code'], reply['status_txt'])) | |
714 | else: | |
715 | self.msg(channel, '@sep @bShort URL@b %s @sep' % reply['data']['url']) | |
716 | ||
717 | def command_url_expand(self, manager, opts, arg, channel, sender, userinfo): | |
718 | if not arg.startswith('http://') and not arg.startswith('https://'): | |
719 | self.errormsg(channel, 'a valid URL must start with http:// or https://') | |
720 | return | |
721 | ||
722 | try: | |
723 | reply = self.urls.expand(arg) | |
724 | except FeedError, e: | |
725 | self.errormsg(channel, e.msg) | |
726 | return | |
727 | ||
728 | if 'error' in reply: | |
729 | self.errormsg(channel, reply['error']) | |
e559b64e A |
730 | elif not reply['long-url']: |
731 | self.msg(channel, '@sep @bLong URL@b URL does not redirect') | |
685e346e | 732 | else: |
e559b64e | 733 | self.msg(channel, '@sep @bLong URL@b {reply[long-url]} @sep'.format(reply=reply)) |
685e346e A |
734 | |
735 | def command_idlerpg(self, manager, opts, arg, channel, sender, userinfo): | |
736 | try: | |
737 | player = IrpgPlayer(arg) | |
738 | except FeedError, e: | |
739 | self.errormsg(channel, e.msg) | |
740 | return | |
741 | ||
742 | if not player.name: | |
743 | self.errormsg(channel, 'player not found. @bNote@b: nicks are case sensitive.') | |
744 | return | |
745 | ||
746 | self.msg(channel, """@sep @b{player.name}@b [{status}] @sep @bLevel@b {player.level} {player.classe} @sep @bNext level@b \ | |
747 | {nextlevel} @sep @bIdled@b {idled_for} @sep @bAlignment@b {player.alignment} @sep""".format( | |
748 | player = player, | |
749 | status = '@c3ON@c' if player.is_online else '@c4OFF@c', | |
750 | nextlevel = timedelta(seconds=player.ttl), | |
751 | idled_for = timedelta(seconds=player.idled_for))) | |
752 | ||
753 | def command_ipinfo(self, manager, opts, arg, channel, sender, userinfo): | |
754 | try: | |
755 | reply = self.ipinfo.get_info(arg) | |
756 | except FeedError, e: | |
757 | self.errormsg(channel, e.msg) | |
758 | return | |
759 | ||
760 | self.msg(channel, """@sep @bIP/Host@b {arg} ({reply[ip_addr]}) @sep @bLocation@b {reply[city]}, {reply[region]}, \ | |
761 | {reply[country_name]} [{reply[country_code]}] @sep{map}""".format( | |
762 | reply = reply, | |
763 | arg = arg.lower(), | |
764 | map = ' http://maps.google.com/maps?q=%s,%s @sep' % (reply['latitude'], reply['longitude']) if reply['latitude'] and reply['longitude'] else '')) | |
765 | ||
766 | dice_regex = re.compile('^(?:(\d+)d)?(\d+)(?:([\+\-])(\d+))?$') | |
9c9f0247 | 767 | |
685e346e A |
768 | def command_dice(self, manager, opts, arg, channel, sender, userinfo): |
769 | r = dice_regex.search(arg) | |
770 | if not r: | |
771 | self.errormsg(channel, 'invalid format') | |
772 | return | |
773 | ||
774 | num, faces, type, modifier = r.groups() | |
775 | if num: | |
776 | num = int(num) | |
777 | else: | |
778 | num = 1 | |
779 | faces = int(faces) | |
780 | if num < 1 or num > 32 or faces < 2 or faces > 65536: | |
781 | self.errormsg(channel, 'parameter out of range') | |
782 | return | |
783 | ||
784 | total = 0 | |
785 | results = [] | |
786 | for n in xrange(int(num)): | |
787 | randnum = random.randint(1, int(faces)) | |
788 | total += randnum | |
789 | results.append(randnum) | |
790 | ||
791 | if type == '-': | |
792 | modifier = int(modifier) | |
793 | total -= modifier | |
794 | max = num * faces - modifier | |
795 | elif type == '+': | |
796 | modifier = int(modifier) | |
797 | total += modifier | |
798 | max = num * faces + modifier | |
799 | else: | |
800 | max = num * faces | |
801 | ||
802 | self.msg(channel, '@sep @bTotal@b {total} / {max} [{percent}%] @sep @bResults@b {results} @sep'.format( | |
803 | total = total, | |
804 | max = max, | |
805 | percent = 100 * total / max if max != 0 else '9001', | |
806 | results = str(results))) | |
807 | ||
808 | def command_qdb(self, manager, opts, arg, channel, sender, userinfo): | |
809 | try: | |
810 | if not arg: | |
811 | quote = self.quotes.get_qdb_random() | |
812 | else: | |
813 | try: | |
814 | quote_id = int(arg) | |
815 | quote = self.quotes.get_qdb_id(quote_id) | |
816 | if not quote: | |
817 | self.errormsg(channel, 'quote @b%d@b not found' % quote_id) | |
818 | return | |
819 | except ValueError: | |
820 | self.errormsg(channel, 'invalid quote ID') | |
821 | return | |
822 | except ExpatError: # qdb returns a malformed xml when the quote doesn't exist | |
823 | self.errormsg(channel, 'quote @b%d@b not found' % quote_id) | |
824 | return | |
825 | except FeedError, e: | |
826 | self.errormsg(channel, e.msg) | |
827 | return | |
828 | ||
829 | id = quote['id'] | |
830 | for line in quote['lines']: | |
831 | self.msg(channel, u'[qdb {id}] {line}'.format(id=id, line=line.replace('\n', ''))) | |
832 | ||
833 | def command_fml(self, manager, opts, arg, channel, sender, userinfo): | |
834 | try: | |
835 | if not arg: | |
836 | quote = self.quotes.get_fml() | |
837 | else: | |
838 | try: | |
839 | quote_id = int(arg) | |
840 | quote = self.quotes.get_fml(quote_id) | |
841 | if not quote: | |
842 | self.errormsg(channel, 'quote @b%d@b not found' % quote_id) | |
843 | return | |
844 | except (ValueError, IndexError): | |
845 | self.errormsg(channel, 'invalid quote ID') | |
846 | return | |
847 | except (FeedError, FmlException) as e: | |
848 | self.errormsg(channel, e.msg) | |
849 | self.elog.warning('WARNING: .fml error: %s' % e.msg) | |
850 | return | |
851 | ||
852 | self.msg(channel, u'[fml #{quote[id]}] {quote[text]}'.format(quote=quote)) | |
853 | ||
854 | def command_internets_info(self, manager, opts, arg, channel, sender, userinfo): | |
855 | self.notice(sender, '@sep @bRizon Internets Bot@b @sep @bDevelopers@b martin <martin@rizon.net> @sep @bHelp/feedback@b %(channel)s @sep' % { | |
856 | 'channel' : '#internets'}) | |
857 | ||
858 | def command_internets_help(self, manager, opts, arg, channel, sender, userinfo): | |
859 | command = arg.lower() | |
860 | ||
861 | if command == '': | |
862 | message = ['internets: .help internets - for internets commands'] | |
863 | elif command == 'internets': | |
864 | message = manager.get_help() | |
865 | else: | |
866 | message = manager.get_help(command) | |
867 | ||
868 | if message == None: | |
869 | message = ['%s is not a valid command.' % arg] | |
870 | ||
871 | for line in message: | |
872 | self.notice(sender, line) | |
873 | ||
874 | class UserCommandManager(CommandManager): | |
875 | def get_prefix(self): | |
876 | return '.' | |
877 | ||
878 | def get_commands(self): | |
879 | return { | |
880 | 'cc': 'calc', | |
881 | 'calc': (command_calc, ARG_YES, 'Calculates an expression', [], 'expression'), | |
882 | ||
883 | 'dict': 'dictionary', | |
884 | 'dictionary': (command_dictionary, ARG_YES, 'Search for a dictionary definition', [ | |
885 | ('number', '-n', 'display the n-th result', {'type': '+integer'}, ARG_YES), | |
886 | ('all', '-a', 'display all results (using /notice)', {'action': 'store_true'}, ARG_YES)], 'word'), | |
887 | ||
888 | 'u': 'urbandictionary', | |
889 | 'urbandictionary': (command_urbandictionary, ARG_YES, 'Search for a definition on Urban Dictionary', [], 'word'), | |
890 | ||
1d0bd48a D |
891 | # 'g': 'google', |
892 | # 'google': (command_google_search, ARG_YES, 'Search for something on Google', [], 'google_search'), | |
685e346e | 893 | |
1d0bd48a D |
894 | # 'gi': 'google_image', |
895 | # 'google_image': (command_google_image_search, ARG_YES, 'Search for images via Google Image', [], 'google_image_search'), | |
cb6afe7c | 896 | |
685e346e A |
897 | 't': 'translate', |
898 | 'translate': (command_bing_translate, ARG_YES, 'Translate something from a language to another', [], 'from to text'), | |
899 | ||
900 | 'yt': 'youtube', | |
901 | 'youtube': (command_youtube_search, ARG_YES, 'Search for something on YouTube', [], 'youtube_search'), | |
902 | ||
903 | 'w': 'weather', | |
904 | 'weather': (command_weather, ARG_OPT, 'Displays current weather conditions for a location', [ | |
905 | ('nick', '-n', 'use the weather location linked to a nick', {'action': 'store_true'}, ARG_YES)]), | |
906 | ||
907 | 'f': 'forecast', | |
908 | 'forecast': (command_forecast, ARG_OPT, 'Displays 5-day forecast for a location', [ | |
909 | ('nick', '-n', 'use the weather location linked to a nick', {'action': 'store_true'}, ARG_YES)]), | |
910 | ||
911 | 'regloc': 'register_location', | |
912 | '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'), | |
913 | ||
914 | 'imdb': (command_imdb, ARG_YES, 'Search for information on a movie on IMDB', [], 'movie_title'), | |
915 | ||
916 | 'lastfm': (command_lastfm, ARG_YES, 'Returns information on a Last.fm user', [], 'lastfm_user'), | |
917 | ||
918 | 'shorten': (command_url_shorten, ARG_YES, 'Shortens a URL using http://j.mp', [], 'long_url'), | |
919 | ||
1d0bd48a | 920 | 'unshorten': 'expand', |
685e346e A |
921 | 'expand': (command_url_expand, ARG_YES, 'Expands a shortened URL using http://longurl.org', [], 'shortened_url'), |
922 | ||
923 | 'irpg': 'idlerpg', | |
924 | 'idlerpg': (command_idlerpg, ARG_YES, 'Returns info on a player in Rizon IdleRPG (http://idlerpg.rizon.net/)', [], 'player_name'), | |
925 | ||
926 | 'ipinfo': (command_ipinfo, ARG_YES, 'Returns short info on a IP address/hostname', [], 'ip/host'), | |
927 | ||
928 | 'd': 'dice', | |
929 | 'dice': (command_dice, ARG_YES, 'Rolls X N-sided dice with an optional modifier A (XdN+A format)', [], 'dice_notation'), | |
930 | ||
931 | 'qdb': (command_qdb, ARG_OPT, 'Displays a quote from qdb.us', []), | |
932 | ||
933 | 'fml': (command_fml, ARG_OPT, 'Displays a quote from http://www.fmylife.com', []), | |
934 | ||
9c9f0247 M |
935 | 'steam': (command_steam, ARG_OPT, 'Shows your steam information', [ |
936 | ('nick', '-n', 'use the steamid linked to a nick.', {'action': 'store_true'}, ARG_YES), | |
937 | ('games', '-g', 'shows the total games owned by nick and shows most played game.', {'action': 'store_true'}, ARG_NO)]), | |
938 | ||
939 | 'regsteam': 'register_steam', | |
940 | 'register_steam': (command_register_steam, ARG_YES, 'Registers your Steam user ID', [], 'steamid'), | |
f0ac597d | 941 | |
942 | 'tw': 'twitch', | |
943 | 'twitch': (command_twitch, ARG_OPT, 'Displays information about twitch.tv streams/games/channels/teams', [ | |
944 | ('stream', '-s', 'searches for a particular stream, or displays the top streams if no argument provided', {'action': 'store_true'}, ARG_OPT), | |
945 | ('channel', '-c', 'lists the top videos of a particular channel on twitch.tv', {'action': 'store_true'}, ARG_YES), | |
946 | ('game', '-g', 'searches for a game on twitch.tv', {'action': 'store_true'}, ARG_YES), | |
947 | ('video', '-v', 'allows you to search the twitch.tv site for a video', {'action': 'store_true'}, ARG_YES), | |
948 | ('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)]), | |
9c9f0247 | 949 | |
685e346e A |
950 | 'info': (command_internets_info, ARG_NO|ARG_OFFLINE, 'Displays version and author information', []), |
951 | 'help': (command_internets_help, ARG_OPT|ARG_OFFLINE, 'Displays available commands and their usage', []), | |
952 | } |