Merge lp:~vhata/ibid/plugin-reorg-399667 into lp:~ibid-core/ibid/old-trunk-1.6
- plugin-reorg-399667
- Merge into old-trunk-1.6
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Michael Gorven | ||||
Approved revision: | 871 | ||||
Merged at revision: | 847 | ||||
Proposed branch: | lp:~vhata/ibid/plugin-reorg-399667 | ||||
Merge into: | lp:~ibid-core/ibid/old-trunk-1.6 | ||||
Diff against target: |
6138 lines (+2807/-2825) 38 files modified
ibid/plugins/admin.py (+138/-1) ibid/plugins/auth.py (+0/-162) ibid/plugins/basic.py (+0/-69) ibid/plugins/calc.py (+15/-198) ibid/plugins/config.py (+0/-79) ibid/plugins/conversions.py (+412/-0) ibid/plugins/core.py (+0/-1) ibid/plugins/dbus.py (+2/-1) ibid/plugins/factoid.py (+7/-7) ibid/plugins/film.py (+212/-0) ibid/plugins/fun.py (+237/-0) ibid/plugins/geography.py (+352/-0) ibid/plugins/google.py (+7/-174) ibid/plugins/http.py (+0/-59) ibid/plugins/identity.py (+153/-1) ibid/plugins/imdb.py (+0/-144) ibid/plugins/info.py (+0/-110) ibid/plugins/languages.py (+147/-4) ibid/plugins/lookup.py (+0/-1019) ibid/plugins/lotto.py (+52/-0) ibid/plugins/memory.py (+0/-1) ibid/plugins/misc.py (+0/-236) ibid/plugins/morse.py (+0/-79) ibid/plugins/network.py (+90/-1) ibid/plugins/oeis.py (+74/-0) ibid/plugins/quotes.py (+419/-0) ibid/plugins/social.py (+120/-0) ibid/plugins/sources.py (+0/-58) ibid/plugins/strings.py (+104/-0) ibid/plugins/sysadmin.py (+41/-0) ibid/plugins/tools.py (+0/-263) ibid/plugins/urlgrab.py (+164/-0) ibid/plugins/urlinfo.py (+35/-153) ibid/test/plugins/test_url.py (+2/-2) ibid/utils/html.py (+21/-0) scripts/ibid-plugin (+1/-1) scripts/ibid-setup (+1/-1) scripts/ibid_import (+1/-1) |
||||
To merge this branch: | bzr merge lp:~vhata/ibid/plugin-reorg-399667 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Michael Gorven | Approve | ||
Stefano Rivera | Approve | ||
Jonathan Hitchcock | Pending | ||
Review via email: mp+17610@code.launchpad.net |
This proposal supersedes a proposal from 2010-01-17.
Commit message
Description of the change
Jonathan Hitchcock (vhata) wrote : Posted in a previous version of this proposal | # |
Jonathan Hitchcock (vhata) wrote : Posted in a previous version of this proposal | # |
I think this is ready...
Stefano Rivera (stefanor) wrote : Posted in a previous version of this proposal | # |
$ bzr springclean
(where springclean == clean-tree --ignored --detritus)
$ scripts/ibid-setup
Traceback (most recent call last):
File "scripts/
from ibid.plugins.auth import hash
ImportError: No module named auth
Stefano Rivera (stefanor) wrote : Posted in a previous version of this proposal | # |
Traceback (most recent call last):
File "./ibid/core.py", line 208, in load_processor
__import_
File "./ibid/
help[
TypeError: '_Helper' object does not support item assignment
Missing a "help = {}"
Stefano Rivera (stefanor) wrote : Posted in a previous version of this proposal | # |
scripts/ibid_import looks like it needs import fixes
> ibid/plugins/
> 13:log = logging.
make that plugins.conversions
Stefano Rivera (stefanor) wrote : Posted in a previous version of this proposal | # |
OK With me. Re-propose for Michael?
Stefano Rivera (stefanor) : Posted in a previous version of this proposal | # |
Michael Gorven (mgorven) wrote : Posted in a previous version of this proposal | # |
lotto into games?
morse into strings?
rfc into sysadmin?
timezone into conversions?
weather seems a bit lonely...
Michael Gorven (mgorven) wrote : Posted in a previous version of this proposal | # |
config into admin?
dict seems a bit lonely. translate and dict into lang/langtools/
Stefano Rivera (stefanor) wrote : Posted in a previous version of this proposal | # |
[10:05:18] STARTED (tumbleweed)
[10:05:50] AGREED: morse -> strings (tumbleweed)
[10:19:41] AGREED: config -> admin (tumbleweed)
[10:21:18] AGREED: dict + translate -> languages (tumbleweed)
[10:49:12] AGREED: timezone -> conversions (tumbleweed)
[11:41:23] AGREED: weather, timezone, distance -> geography (tumbleweed)
[11:41:25] ENDED (tumbleweed)
http://
- 871. By Jonathan Hitchcock
-
config plugin is now part of admin
Stefano Rivera (stefanor) : | # |
Michael Gorven (mgorven) wrote : | # |
review approve
status approved
Preview Diff
1 | === modified file 'ibid/plugins/admin.py' |
2 | --- ibid/plugins/admin.py 2009-10-20 15:45:46 +0000 |
3 | +++ ibid/plugins/admin.py 2010-01-18 18:54:16 +0000 |
4 | @@ -1,8 +1,14 @@ |
5 | +from os.path import join |
6 | from twisted.internet import reactor |
7 | +import logging |
8 | |
9 | import ibid |
10 | from ibid.utils import human_join |
11 | -from ibid.plugins import Processor, match, authorise |
12 | +from ibid.config import FileConfig |
13 | +from ibid.plugins import Processor, match, authorise, auth_responses |
14 | +from ibid.utils import ibid_version |
15 | + |
16 | +log = logging.getLogger('plugins.admin') |
17 | |
18 | help = {} |
19 | |
20 | @@ -70,4 +76,135 @@ |
21 | def die(self, event): |
22 | reactor.stop() |
23 | |
24 | +help['sources'] = u'Controls and lists the configured sources.' |
25 | +class Admin(Processor): |
26 | + u"""(connect|disconnect) (to|from) <source> |
27 | + load <source> source""" |
28 | + feature = 'sources' |
29 | + |
30 | + permission = u'sources' |
31 | + |
32 | + @match(r'^connect\s+(?:to\s+)?(\S+)$') |
33 | + @authorise() |
34 | + def connect(self, event, source): |
35 | + if source not in ibid.sources: |
36 | + event.addresponse(u"I don't have a source called %s", source) |
37 | + elif ibid.sources[source].connect(): |
38 | + event.addresponse(u'Connecting to %s', source) |
39 | + else: |
40 | + event.addresponse(u"I couldn't connect to %s", source) |
41 | + |
42 | + @match(r'^disconnect\s+(?:from\s+)?(\S+)$') |
43 | + @authorise() |
44 | + def disconnect(self, event, source): |
45 | + if source not in ibid.sources: |
46 | + event.addresponse(u"I am not connected to %s", source) |
47 | + elif ibid.sources[source].disconnect(): |
48 | + event.addresponse(u'Disconnecting from %s', source) |
49 | + else: |
50 | + event.addresponse(u"I couldn't disconnect from %s", source) |
51 | + |
52 | + @match(r'^(?:re)?load\s+(\S+)\s+source$') |
53 | + @authorise() |
54 | + def load(self, event, source): |
55 | + if ibid.reloader.load_source(source, ibid.service): |
56 | + event.addresponse(u"%s source loaded", source) |
57 | + else: |
58 | + event.addresponse(u"Couldn't load %s source", source) |
59 | + |
60 | +class Info(Processor): |
61 | + u"""(sources|list configured sources)""" |
62 | + feature = 'sources' |
63 | + |
64 | + @match(r'^sources$') |
65 | + def list(self, event): |
66 | + sources = [] |
67 | + for name, source in ibid.sources.items(): |
68 | + url = source.url() |
69 | + sources.append(url and u'%s (%s)' % (name, url) or name) |
70 | + event.addresponse(u'Sources: %s', human_join(sorted(sources)) or u'none') |
71 | + |
72 | + @match(r'^list\s+configured\s+sources$') |
73 | + def listall(self, event): |
74 | + event.addresponse(u'Configured sources: %s', human_join(sorted(ibid.config.sources.keys())) or u'none') |
75 | + |
76 | +help['version'] = u"Show the Ibid version currently running" |
77 | +class Version(Processor): |
78 | + u"""version""" |
79 | + feature = 'version' |
80 | + |
81 | + @match(r'^version$') |
82 | + def show_version(self, event): |
83 | + if ibid_version(): |
84 | + event.addresponse(u'I am version %s', ibid_version()) |
85 | + else: |
86 | + event.addresponse(u"I don't know what version I am :-(") |
87 | + |
88 | +help['config'] = u'Gets and sets configuration settings, and rereads the configuration file.' |
89 | +class Config(Processor): |
90 | + u"""reread config |
91 | + set config <name> to <value> |
92 | + get config <name>""" |
93 | + feature = 'config' |
94 | + |
95 | + priority = -10 |
96 | + permission = u'config' |
97 | + |
98 | + @match(r'^reread\s+config$') |
99 | + @authorise() |
100 | + def reload(self, event): |
101 | + ibid.config.reload() |
102 | + ibid.config.merge(FileConfig(join(ibid.options['base'], 'local.ini'))) |
103 | + ibid.reloader.reload_config() |
104 | + ibid.auth.drop_caches() |
105 | + event.addresponse(True) |
106 | + log.info(u'Reread configuration file') |
107 | + |
108 | + @match(r'^(?:set\s+config|config\s+set)\s+(\S+?)(?:\s+to\s+|\s*=\s*)(\S.*?)$') |
109 | + @authorise() |
110 | + def set(self, event, key, value): |
111 | + config = ibid.config |
112 | + for part in key.split('.')[:-1]: |
113 | + if isinstance(config, dict): |
114 | + if part not in config: |
115 | + config[part] = {} |
116 | + else: |
117 | + event.addresponse(u'No such option') |
118 | + return |
119 | + |
120 | + config = config[part] |
121 | + |
122 | + part = key.split('.')[-1] |
123 | + if not isinstance(config, dict): |
124 | + event.addresponse(u'No such option') |
125 | + return |
126 | + if ',' in value: |
127 | + config[part] = [part.strip() for part in value.split(',')] |
128 | + else: |
129 | + config[part] = value |
130 | + |
131 | + ibid.config.write() |
132 | + ibid.reloader.reload_config() |
133 | + log.info(u"Set %s to %s", key, value) |
134 | + |
135 | + event.addresponse(True) |
136 | + |
137 | + @match(r'^(?:get\s+config|config\s+get)\s+(\S+?)$') |
138 | + def get(self, event, key): |
139 | + if 'password' in key.lower() and not auth_responses(event, u'config'): |
140 | + return |
141 | + |
142 | + config = ibid.config |
143 | + for part in key.split('.'): |
144 | + if not isinstance(config, dict) or part not in config: |
145 | + event.addresponse(u'No such option') |
146 | + return |
147 | + config = config[part] |
148 | + if isinstance(config, list): |
149 | + event.addresponse(u', '.join(config)) |
150 | + elif isinstance(config, dict): |
151 | + event.addresponse(u'Keys: ' + human_join(config.keys())) |
152 | + else: |
153 | + event.addresponse(unicode(config)) |
154 | + |
155 | # vi: set et sta sw=4 ts=4: |
156 | |
157 | === removed file 'ibid/plugins/auth.py' |
158 | --- ibid/plugins/auth.py 2009-12-20 20:17:17 +0000 |
159 | +++ ibid/plugins/auth.py 1970-01-01 00:00:00 +0000 |
160 | @@ -1,162 +0,0 @@ |
161 | -import logging |
162 | - |
163 | -import ibid |
164 | -from ibid.db.models import Credential, Permission, Account |
165 | -from ibid.plugins import Processor, match, auth_responses, authorise |
166 | -from ibid.auth import hash |
167 | -from ibid.utils import human_join |
168 | - |
169 | -help = {} |
170 | - |
171 | -log = logging.getLogger('plugins.auth') |
172 | - |
173 | -actions = {'revoke': 'Revoked', 'grant': 'Granted', 'remove': 'Removed'} |
174 | - |
175 | -help['auth'] = u'Adds and removes authentication credentials and permissions' |
176 | -class AddAuth(Processor): |
177 | - u"""authenticate <account> [on source] using <method> [<credential>]""" |
178 | - feature = 'auth' |
179 | - |
180 | - @match(r'^authenticate\s+(.+?)(?:\s+on\s+(.+))?\s+using\s+(\S+)\s+(.+)$') |
181 | - def handler(self, event, user, source, method, credential): |
182 | - |
183 | - if user.lower() == 'me': |
184 | - if not event.account: |
185 | - event.addresponse(u"I don't know who you are") |
186 | - return |
187 | - if not ibid.auth.authenticate(event): |
188 | - event.complain = 'notauthed' |
189 | - return |
190 | - account = event.session.query(Account).get(event.account) |
191 | - |
192 | - else: |
193 | - if not auth_responses(event, 'admin'): |
194 | - return |
195 | - account = event.session.query(Account).filter_by(username=user).first() |
196 | - if not account: |
197 | - event.addresponse(u"I don't know who %s is", user) |
198 | - return |
199 | - |
200 | - if source: |
201 | - if source not in ibid.sources: |
202 | - event.addresponse(u"I am not connected to %s", source) |
203 | - return |
204 | - source = ibid.sources[source].name |
205 | - |
206 | - if method.lower() == 'password': |
207 | - password = hash(credential) |
208 | - event.message['clean'] = event.message['clean'][:-len(credential)] + password |
209 | - event.message['raw'] = event.message['raw'][:event.message['raw'].rfind(credential)] \ |
210 | - + password + event.message['raw'][event.message['raw'].rfind(credential)+len(credential):] |
211 | - credential = password |
212 | - |
213 | - credential = Credential(method, credential, source, account.id) |
214 | - event.session.save_or_update(credential) |
215 | - event.session.commit() |
216 | - log.info(u"Added %s credential %s for account %s (%s) on %s by account %s", |
217 | - method, credential.credential, account.id, account.username, source, event.account) |
218 | - |
219 | - event.addresponse(True) |
220 | - |
221 | -permission_values = {'no': '-', 'yes': '+', 'auth': ''} |
222 | -class Permissions(Processor): |
223 | - u"""(grant|revoke|remove) <permission> (to|from|on) <username> [when authed] |
224 | - permissions [for <username>] |
225 | - list permissions""" |
226 | - feature = 'auth' |
227 | - |
228 | - permission = u'admin' |
229 | - |
230 | - @match(r'^(grant|revoke|remove)\s+(.+?)(?:\s+permission)?\s+(?:to|from|on)\s+(.+?)(\s+(?:with|when|if)\s+(?:auth|authed|authenticated))?$') |
231 | - @authorise() |
232 | - def grant(self, event, action, name, username, auth): |
233 | - |
234 | - account = event.session.query(Account).filter_by(username=username).first() |
235 | - if not account: |
236 | - event.addresponse(u"I don't know who %s is", username) |
237 | - return |
238 | - |
239 | - permission = event.session.query(Permission) \ |
240 | - .filter_by(account_id=account.id, name=name).first() |
241 | - if action.lower() == 'remove': |
242 | - if permission: |
243 | - event.session.delete(permission) |
244 | - else: |
245 | - event.addresponse(u"%s doesn't have that permission anyway", username) |
246 | - return |
247 | - |
248 | - else: |
249 | - if not permission: |
250 | - permission = Permission(name) |
251 | - account.permissions.append(permission) |
252 | - |
253 | - if action.lower() == 'revoke': |
254 | - value = 'no' |
255 | - elif auth: |
256 | - value = 'auth' |
257 | - else: |
258 | - value = 'yes' |
259 | - |
260 | - if permission.value == value: |
261 | - event.addresponse(u'%(permission)s permission for %(user)s is already %(value)s', { |
262 | - 'permission': name, |
263 | - 'user': username, |
264 | - 'value': value, |
265 | - }) |
266 | - return |
267 | - |
268 | - permission.value = value |
269 | - event.session.save_or_update(permission) |
270 | - |
271 | - event.session.commit() |
272 | - ibid.auth.drop_caches() |
273 | - log.info(u"%s %s permission for account %s (%s) by account %s", |
274 | - actions[action.lower()], name, account.id, account.username, event.account) |
275 | - |
276 | - event.addresponse(True) |
277 | - |
278 | - @match(r'^permissions(?:\s+for\s+(\S+))?$') |
279 | - def list(self, event, username): |
280 | - if not username: |
281 | - if not event.account: |
282 | - event.addresponse(u"I don't know who you are") |
283 | - return |
284 | - account = event.session.query(Account).get(event.account) |
285 | - else: |
286 | - if not auth_responses(event, u'accounts'): |
287 | - return |
288 | - account = event.session.query(Account) \ |
289 | - .filter_by(username=username).first() |
290 | - if not account: |
291 | - event.addresponse(u"I don't know who %s is", username) |
292 | - return |
293 | - |
294 | - permissions = sorted(u'%s%s' % (permission_values[perm.value], perm.name) for perm in account.permissions) |
295 | - event.addresponse(u'Permissions: %s', human_join(permissions) or u'none') |
296 | - |
297 | - @match(r'^list\s+permissions$') |
298 | - def list_permissions(self, event): |
299 | - permissions = [] |
300 | - for processor in ibid.processors: |
301 | - if hasattr(processor, 'permission') and getattr(processor, 'permission') not in permissions: |
302 | - permissions.append(getattr(processor, 'permission')) |
303 | - if hasattr(processor, 'permissions'): |
304 | - for permission in getattr(processor, 'permissions'): |
305 | - if permission not in permissions: |
306 | - permissions.append(permission) |
307 | - |
308 | - event.addresponse(u'Permissions: %s', human_join(sorted(permissions)) or u'none') |
309 | - |
310 | -class Auth(Processor): |
311 | - u"""auth <credential>""" |
312 | - feature = 'auth' |
313 | - |
314 | - @match(r'^auth(?:\s+(.+))?$') |
315 | - def handler(self, event, password): |
316 | - result = ibid.auth.authenticate(event, password) |
317 | - if result: |
318 | - event.addresponse(u'You are authenticated') |
319 | - else: |
320 | - event.addresponse(u'Authentication failed') |
321 | - |
322 | -# vi: set et sta sw=4 ts=4: |
323 | |
324 | === removed file 'ibid/plugins/basic.py' |
325 | --- ibid/plugins/basic.py 2010-01-02 11:26:31 +0000 |
326 | +++ ibid/plugins/basic.py 1970-01-01 00:00:00 +0000 |
327 | @@ -1,69 +0,0 @@ |
328 | -from random import choice |
329 | -import re |
330 | - |
331 | -import ibid |
332 | -from ibid.plugins import Processor, match, handler, authorise |
333 | - |
334 | -help = {} |
335 | - |
336 | -help['saydo'] = u'Says or does stuff in a channel.' |
337 | -class SayDo(Processor): |
338 | - u"""(say|do) in <channel> [on <source>] <text>""" |
339 | - feature = 'saydo' |
340 | - |
341 | - permission = u'saydo' |
342 | - |
343 | - @match(r'^(say|do)\s+(?:in|to)\s+(\S+)\s+(?:on\s+(\S+)\s+)?(.*)$', 'deaddressed') |
344 | - @authorise() |
345 | - def saydo(self, event, action, channel, source, what): |
346 | - event.addresponse(what, address=False, target=channel, source=source or event.source, |
347 | - action=(action.lower() == u"do")) |
348 | - |
349 | -help['redirect'] = u'Redirects the response to a command to a different channel.' |
350 | -class RedirectCommand(Processor): |
351 | - u"""redirect [to] <channel> [on <source>] <command>""" |
352 | - feature = 'redirect' |
353 | - |
354 | - priority = -1200 |
355 | - permission = u'saydo' |
356 | - |
357 | - @match(r'^redirect\s+(?:to\s+)?(\S+)\s+(?:on\s+(\S+)\s+)?(.+)$') |
358 | - @authorise() |
359 | - def redirect(self, event, channel, source, command): |
360 | - if source: |
361 | - if source.lower() not in ibid.sources: |
362 | - event.addresponse(u'No such source: %s', source) |
363 | - return |
364 | - event.redirect_source = source |
365 | - event.redirect_target = channel |
366 | - event.message['clean'] = command |
367 | - |
368 | -class Redirect(Processor): |
369 | - feature = 'redirect' |
370 | - |
371 | - processed = True |
372 | - priority = 940 |
373 | - |
374 | - @handler |
375 | - def redirect(self, event): |
376 | - if 'redirect_target' in event: |
377 | - responses = [] |
378 | - for response in event.responses: |
379 | - response['target'] = event.redirect_target |
380 | - if 'redirect_source' in event: |
381 | - response['source'] = event.redirect_source |
382 | - responses.append(response) |
383 | - event.responses = responses |
384 | - |
385 | -help['choose'] = u'Choose one of the given options.' |
386 | -class Choose(Processor): |
387 | - u"""choose <choice> or <choice>...""" |
388 | - feature = 'choose' |
389 | - |
390 | - choose_re = re.compile(r'(?:\s*,\s*(?:or\s+)?)|(?:\s+or\s+)', re.I) |
391 | - |
392 | - @match(r'^(?:choose|choice|pick)\s+(.+)$') |
393 | - def choose(self, event, choices): |
394 | - event.addresponse(u'I choose %s', choice(self.choose_re.split(choices))) |
395 | - |
396 | -# vi: set et sta sw=4 ts=4: |
397 | |
398 | === renamed file 'ibid/plugins/maths.py' => 'ibid/plugins/calc.py' |
399 | --- ibid/plugins/maths.py 2010-01-12 10:54:47 +0000 |
400 | +++ ibid/plugins/calc.py 2010-01-18 18:54:16 +0000 |
401 | @@ -1,16 +1,15 @@ |
402 | from __future__ import division |
403 | +from random import random, randint |
404 | |
405 | import logging |
406 | from os import kill |
407 | -import re |
408 | from signal import SIGTERM |
409 | from subprocess import Popen, PIPE |
410 | from time import time, sleep |
411 | |
412 | -import ibid |
413 | from ibid.compat import all, factorial |
414 | from ibid.config import Option, FloatOption |
415 | -from ibid.plugins import Processor, match, handler |
416 | +from ibid.plugins import Processor, match |
417 | from ibid.utils import file_in_path, unicode_output |
418 | |
419 | try: |
420 | @@ -27,7 +26,7 @@ |
421 | futures = ('division',) |
422 | |
423 | help = {} |
424 | -log = logging.getLogger('maths') |
425 | +log = logging.getLogger('calc') |
426 | |
427 | help['bc'] = u'Calculate mathematical expressions using bc' |
428 | class BC(Processor): |
429 | @@ -191,200 +190,18 @@ |
430 | if isinstance(result, (int, long, float, complex)): |
431 | event.addresponse(unicode(result)) |
432 | |
433 | -help['base'] = u'Convert numbers between bases (radixes)' |
434 | -class BaseConvert(Processor): |
435 | - u"""[convert] <number> [from base <number>] to base <number> |
436 | - [convert] ascii <text> to base <number> |
437 | - [convert] <sequence> from base <number> to ascii""" |
438 | - |
439 | - feature = "base" |
440 | - |
441 | - abbr_named_bases = { |
442 | - "hex": 16, |
443 | - "dec": 10, |
444 | - "oct": 8, |
445 | - "bin": 2, |
446 | - } |
447 | - |
448 | - base_names = { |
449 | - 2: "binary", |
450 | - 3: "ternary", |
451 | - 4: "quaternary", |
452 | - 6: "senary", |
453 | - 8: "octal", |
454 | - 9: "nonary", |
455 | - 10: "decimal", |
456 | - 12: "duodecimal", |
457 | - 16: "hexadecimal", |
458 | - 20: "vigesimal", |
459 | - 30: "trigesimal", |
460 | - 32: "duotrigesimal", |
461 | - 36: "hexatridecimal", |
462 | - } |
463 | - base_values = {} |
464 | - for value, name in base_names.iteritems(): |
465 | - base_values[name] = value |
466 | - |
467 | - numerals = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+/" |
468 | - values = {} |
469 | - for value, numeral in enumerate(numerals): |
470 | - values[numeral] = value |
471 | - |
472 | - def _in_base(self, num, base): |
473 | - "Recursive base-display formatter" |
474 | - if num == 0: |
475 | - return "0" |
476 | - return self._in_base(num // base, base).lstrip("0") + self.numerals[num % base] |
477 | - |
478 | - def _from_base(self, num, base): |
479 | - "Return a base-n number in decimal. Needed as int(x, n) only works for n<=36" |
480 | - |
481 | - if base <= 36: |
482 | - num = num.upper() |
483 | - |
484 | - decimal = 0 |
485 | - for digit in num: |
486 | - decimal *= base |
487 | - if self.values[digit] >= base: |
488 | - raise ValueError("'%s' is not a valid digit in %s" % (digit, self._base_name(base))) |
489 | - decimal += self.values[digit] |
490 | - |
491 | - return decimal |
492 | - |
493 | - def _parse_base(self, base): |
494 | - "Parse a base in the form of 'hex' or 'base 13' or None" |
495 | - |
496 | - if base is None: |
497 | - base = 10 |
498 | - elif len(base) == 3 and base in self.abbr_named_bases: |
499 | - base = self.abbr_named_bases[base] |
500 | - elif base in self.base_values: |
501 | - base = self.base_values[base] |
502 | - elif base.startswith(u"base"): |
503 | - base = int(base.split()[-1]) |
504 | +help['random'] = u'Generates random numbers.' |
505 | +class Random(Processor): |
506 | + u"""random [ <max> | <min> <max> ]""" |
507 | + feature = 'random' |
508 | + |
509 | + @match('^rand(?:om)?(?:\s+(\d+)(?:\s+(\d+))?)?$') |
510 | + def random(self, event, begin, end): |
511 | + if not begin and not end: |
512 | + event.addresponse(u'I always liked %f', random()) |
513 | else: |
514 | - # The above should be the only cases allowed by the regex |
515 | - # This exception indicates programmer error: |
516 | - raise ValueError("Unparsable base: " + base) |
517 | - |
518 | - return base |
519 | - |
520 | - def _base_name(self, base): |
521 | - "Shows off the bot's smartypants heritage by naming bases" |
522 | - |
523 | - base_name = u"base %i" % base |
524 | - |
525 | - if base in self.base_names: |
526 | - base_name = self.base_names[base] |
527 | - |
528 | - return base_name |
529 | - |
530 | - def setup(self): |
531 | - bases = [] |
532 | - for number, name in self.base_names.iteritems(): |
533 | - if name[:3] in self.abbr_named_bases and self.abbr_named_bases[name[:3]] == number: |
534 | - bases.append(r"%s(?:%s)?" % (name[:3], name[3:])) |
535 | - else: |
536 | - bases.append(name) |
537 | - bases = "|".join(bases) |
538 | - |
539 | - self.base_conversion.im_func.pattern = re.compile( |
540 | - r"^(?:convert\s+)?([0-9a-zA-Z+/]+)\s+(?:(?:(?:from|in)\s+)?(base\s+\d+|%s)\s+)?(?:in|to|into)\s+(base\s+\d+|%s)\s*$" |
541 | - % (bases, bases), re.I) |
542 | - |
543 | - self.ascii_decode.im_func.pattern = re.compile( |
544 | - r"^(?:convert\s+)?ascii\s+(.+?)(?:(?:\s+(?:in|to|into))?\s+(base\s+\d+|%s))?$" % bases, re.I) |
545 | - |
546 | - self.ascii_encode.im_func.pattern = re.compile( |
547 | - r"^(?:convert\s+)?([0-9a-zA-Z+/\s]+?)(?:\s+(?:(?:from|in)\s+)?(base\s+\d+|%s))?\s+(?:in|to|into)\s+ascii$" % bases, re.I) |
548 | - |
549 | - @handler |
550 | - def base_conversion(self, event, number, base_from, base_to): |
551 | - "Arbitrary (2 <= base <= 64) numeric base conversions." |
552 | - |
553 | - base_from = self._parse_base(base_from) |
554 | - base_to = self._parse_base(base_to) |
555 | - |
556 | - if min(base_from, base_to) < 2 or max(base_from, base_to) > 64: |
557 | - event.addresponse(u'Sorry, valid bases are between 2 and 64, inclusive') |
558 | - return |
559 | - |
560 | - try: |
561 | - number = self._from_base(number, base_from) |
562 | - except ValueError, e: |
563 | - event.addresponse(unicode(e)) |
564 | - return |
565 | - |
566 | - event.addresponse(u'That is %(result)s in %(base)s', { |
567 | - 'result': self._in_base(number, base_to), |
568 | - 'base': self._base_name(base_to), |
569 | - }) |
570 | - |
571 | - @handler |
572 | - def ascii_decode(self, event, text, base_to): |
573 | - "Display the values of each character in an ASCII string" |
574 | - |
575 | - base_to = self._parse_base(base_to) |
576 | - |
577 | - if len(text) > 2 and text[0] == text[-1] and text[0] in ("'", '"'): |
578 | - text = text[1:-1] |
579 | - |
580 | - output = u"" |
581 | - for char in text: |
582 | - code_point = ord(char) |
583 | - if code_point > 255: |
584 | - output += u'U%s ' % self._in_base(code_point, base_to) |
585 | - else: |
586 | - output += self._in_base(code_point, base_to) + u" " |
587 | - |
588 | - output = output.strip() |
589 | - |
590 | - event.addresponse(u'That is %(result)s in %(base)s', { |
591 | - 'result': output, |
592 | - 'base': self._base_name(base_to), |
593 | - }) |
594 | - |
595 | - if base_to == 64 and [True for plugin in ibid.processors if getattr(plugin, 'feature', None) == 'base64']: |
596 | - event.addresponse(u'If you want a base64 encoding, use the "base64" feature') |
597 | - |
598 | - @handler |
599 | - def ascii_encode(self, event, source, base_from): |
600 | - |
601 | - base_from = self._parse_base(base_from) |
602 | - |
603 | - output = u"" |
604 | - buf = u"" |
605 | - |
606 | - def process_buf(buf): |
607 | - char = self._from_base(buf, base_from) |
608 | - if char > 127: |
609 | - raise ValueError(u"I only deal with the first page of ASCII (i.e. under 127). %i is invalid." % char) |
610 | - elif char < 32: |
611 | - return u" %s " % "NUL SOH STX EOT ENQ ACK BEL BS HT LF VT FF SO SI DLE DC1 DC2 DC2 DC4 NAK SYN ETB CAN EM SUB ESC FS GS RS US".split()[char] |
612 | - elif char == 127: |
613 | - return u" DEL " |
614 | - return unichr(char) |
615 | - |
616 | - try: |
617 | - for char in source: |
618 | - if char == u" ": |
619 | - if len(buf) > 0: |
620 | - output += process_buf(buf) |
621 | - buf = u"" |
622 | - else: |
623 | - buf += char |
624 | - if (len(buf) == 2 and base_from == 16) or (len(buf) == 3 and base_from == 8) or (len(buf) == 8 and base_from == 2): |
625 | - output += process_buf(buf) |
626 | - buf = u"" |
627 | - |
628 | - if len(buf) > 0: |
629 | - output += process_buf(buf) |
630 | - except ValueError, e: |
631 | - event.addresponse(unicode(e)) |
632 | - return |
633 | - |
634 | - event.addresponse(u'That is "%s"', output) |
635 | - if base_from == 64 and [True for plugin in ibid.processors if getattr(plugin, 'feature', None) == 'base64']: |
636 | - event.addresponse(u'If you want a base64 encoding, use the "base64" feature') |
637 | + begin = int(begin) |
638 | + end = end and int(end) or 0 |
639 | + event.addresponse(u'I always liked %i', randint(min(begin,end), max(begin,end))) |
640 | |
641 | # vi: set et sta sw=4 ts=4: |
642 | |
643 | === removed file 'ibid/plugins/config.py' |
644 | --- ibid/plugins/config.py 2009-12-31 17:27:23 +0000 |
645 | +++ ibid/plugins/config.py 1970-01-01 00:00:00 +0000 |
646 | @@ -1,79 +0,0 @@ |
647 | -from os.path import join |
648 | -import logging |
649 | - |
650 | -import ibid |
651 | -from ibid.config import FileConfig |
652 | -from ibid.utils import human_join |
653 | -from ibid.plugins import Processor, match, authorise, auth_responses |
654 | - |
655 | -help = {'config': u'Gets and sets configuration settings, and rereads the configuration file.'} |
656 | - |
657 | -log = logging.getLogger('plugins.config') |
658 | - |
659 | -class Config(Processor): |
660 | - u"""reread config |
661 | - set config <name> to <value> |
662 | - get config <name>""" |
663 | - feature = 'config' |
664 | - |
665 | - priority = -10 |
666 | - permission = u'config' |
667 | - |
668 | - @match(r'^reread\s+config$') |
669 | - @authorise() |
670 | - def reload(self, event): |
671 | - ibid.config.reload() |
672 | - ibid.config.merge(FileConfig(join(ibid.options['base'], 'local.ini'))) |
673 | - ibid.reloader.reload_config() |
674 | - ibid.auth.drop_caches() |
675 | - event.addresponse(True) |
676 | - log.info(u'Reread configuration file') |
677 | - |
678 | - @match(r'^(?:set\s+config|config\s+set)\s+(\S+?)(?:\s+to\s+|\s*=\s*)(\S.*?)$') |
679 | - @authorise() |
680 | - def set(self, event, key, value): |
681 | - config = ibid.config |
682 | - for part in key.split('.')[:-1]: |
683 | - if isinstance(config, dict): |
684 | - if part not in config: |
685 | - config[part] = {} |
686 | - else: |
687 | - event.addresponse(u'No such option') |
688 | - return |
689 | - |
690 | - config = config[part] |
691 | - |
692 | - part = key.split('.')[-1] |
693 | - if not isinstance(config, dict): |
694 | - event.addresponse(u'No such option') |
695 | - return |
696 | - if ',' in value: |
697 | - config[part] = [part.strip() for part in value.split(',')] |
698 | - else: |
699 | - config[part] = value |
700 | - |
701 | - ibid.config.write() |
702 | - ibid.reloader.reload_config() |
703 | - log.info(u"Set %s to %s", key, value) |
704 | - |
705 | - event.addresponse(True) |
706 | - |
707 | - @match(r'^(?:get\s+config|config\s+get)\s+(\S+?)$') |
708 | - def get(self, event, key): |
709 | - if 'password' in key.lower() and not auth_responses(event, u'config'): |
710 | - return |
711 | - |
712 | - config = ibid.config |
713 | - for part in key.split('.'): |
714 | - if not isinstance(config, dict) or part not in config: |
715 | - event.addresponse(u'No such option') |
716 | - return |
717 | - config = config[part] |
718 | - if isinstance(config, list): |
719 | - event.addresponse(u', '.join(config)) |
720 | - elif isinstance(config, dict): |
721 | - event.addresponse(u'Keys: ' + human_join(config.keys())) |
722 | - else: |
723 | - event.addresponse(unicode(config)) |
724 | - |
725 | -# vi: set et sta sw=4 ts=4: |
726 | |
727 | === added file 'ibid/plugins/conversions.py' |
728 | --- ibid/plugins/conversions.py 1970-01-01 00:00:00 +0000 |
729 | +++ ibid/plugins/conversions.py 2010-01-18 18:54:16 +0000 |
730 | @@ -0,0 +1,412 @@ |
731 | +from subprocess import Popen, PIPE |
732 | +from urllib import urlencode |
733 | +import logging |
734 | +import re |
735 | + |
736 | +import ibid |
737 | +from ibid.plugins import Processor, handler, match |
738 | +from ibid.config import Option |
739 | +from ibid.utils import file_in_path, unicode_output, human_join |
740 | +from ibid.utils.html import get_country_codes, get_html_parse_tree |
741 | + |
742 | +help = {} |
743 | +log = logging.getLogger('plugins.conversions') |
744 | + |
745 | +help['base'] = u'Convert numbers between bases (radixes)' |
746 | +class BaseConvert(Processor): |
747 | + u"""[convert] <number> [from base <number>] to base <number> |
748 | + [convert] ascii <text> to base <number> |
749 | + [convert] <sequence> from base <number> to ascii""" |
750 | + |
751 | + feature = "base" |
752 | + |
753 | + abbr_named_bases = { |
754 | + "hex": 16, |
755 | + "dec": 10, |
756 | + "oct": 8, |
757 | + "bin": 2, |
758 | + } |
759 | + |
760 | + base_names = { |
761 | + 2: "binary", |
762 | + 3: "ternary", |
763 | + 4: "quaternary", |
764 | + 6: "senary", |
765 | + 8: "octal", |
766 | + 9: "nonary", |
767 | + 10: "decimal", |
768 | + 12: "duodecimal", |
769 | + 16: "hexadecimal", |
770 | + 20: "vigesimal", |
771 | + 30: "trigesimal", |
772 | + 32: "duotrigesimal", |
773 | + 36: "hexatridecimal", |
774 | + } |
775 | + base_values = {} |
776 | + for value, name in base_names.iteritems(): |
777 | + base_values[name] = value |
778 | + |
779 | + numerals = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+/" |
780 | + values = {} |
781 | + for value, numeral in enumerate(numerals): |
782 | + values[numeral] = value |
783 | + |
784 | + def _in_base(self, num, base): |
785 | + "Recursive base-display formatter" |
786 | + if num == 0: |
787 | + return "0" |
788 | + return self._in_base(num // base, base).lstrip("0") + self.numerals[num % base] |
789 | + |
790 | + def _from_base(self, num, base): |
791 | + "Return a base-n number in decimal. Needed as int(x, n) only works for n<=36" |
792 | + |
793 | + if base <= 36: |
794 | + num = num.upper() |
795 | + |
796 | + decimal = 0 |
797 | + for digit in num: |
798 | + decimal *= base |
799 | + if self.values[digit] >= base: |
800 | + raise ValueError("'%s' is not a valid digit in %s" % (digit, self._base_name(base))) |
801 | + decimal += self.values[digit] |
802 | + |
803 | + return decimal |
804 | + |
805 | + def _parse_base(self, base): |
806 | + "Parse a base in the form of 'hex' or 'base 13' or None" |
807 | + |
808 | + if base is None: |
809 | + base = 10 |
810 | + elif len(base) == 3 and base in self.abbr_named_bases: |
811 | + base = self.abbr_named_bases[base] |
812 | + elif base in self.base_values: |
813 | + base = self.base_values[base] |
814 | + elif base.startswith(u"base"): |
815 | + base = int(base.split()[-1]) |
816 | + else: |
817 | + # The above should be the only cases allowed by the regex |
818 | + # This exception indicates programmer error: |
819 | + raise ValueError("Unparsable base: " + base) |
820 | + |
821 | + return base |
822 | + |
823 | + def _base_name(self, base): |
824 | + "Shows off the bot's smartypants heritage by naming bases" |
825 | + |
826 | + base_name = u"base %i" % base |
827 | + |
828 | + if base in self.base_names: |
829 | + base_name = self.base_names[base] |
830 | + |
831 | + return base_name |
832 | + |
833 | + def setup(self): |
834 | + bases = [] |
835 | + for number, name in self.base_names.iteritems(): |
836 | + if name[:3] in self.abbr_named_bases and self.abbr_named_bases[name[:3]] == number: |
837 | + bases.append(r"%s(?:%s)?" % (name[:3], name[3:])) |
838 | + else: |
839 | + bases.append(name) |
840 | + bases = "|".join(bases) |
841 | + |
842 | + self.base_conversion.im_func.pattern = re.compile( |
843 | + r"^(?:convert\s+)?([0-9a-zA-Z+/]+)\s+(?:(?:(?:from|in)\s+)?(base\s+\d+|%s)\s+)?(?:in|to|into)\s+(base\s+\d+|%s)\s*$" |
844 | + % (bases, bases), re.I) |
845 | + |
846 | + self.ascii_decode.im_func.pattern = re.compile( |
847 | + r"^(?:convert\s+)?ascii\s+(.+?)(?:(?:\s+(?:in|to|into))?\s+(base\s+\d+|%s))?$" % bases, re.I) |
848 | + |
849 | + self.ascii_encode.im_func.pattern = re.compile( |
850 | + r"^(?:convert\s+)?([0-9a-zA-Z+/\s]+?)(?:\s+(?:(?:from|in)\s+)?(base\s+\d+|%s))?\s+(?:in|to|into)\s+ascii$" % bases, re.I) |
851 | + |
852 | + @handler |
853 | + def base_conversion(self, event, number, base_from, base_to): |
854 | + "Arbitrary (2 <= base <= 64) numeric base conversions." |
855 | + |
856 | + base_from = self._parse_base(base_from) |
857 | + base_to = self._parse_base(base_to) |
858 | + |
859 | + if min(base_from, base_to) < 2 or max(base_from, base_to) > 64: |
860 | + event.addresponse(u'Sorry, valid bases are between 2 and 64, inclusive') |
861 | + return |
862 | + |
863 | + try: |
864 | + number = self._from_base(number, base_from) |
865 | + except ValueError, e: |
866 | + event.addresponse(unicode(e)) |
867 | + return |
868 | + |
869 | + event.addresponse(u'That is %(result)s in %(base)s', { |
870 | + 'result': self._in_base(number, base_to), |
871 | + 'base': self._base_name(base_to), |
872 | + }) |
873 | + |
874 | + @handler |
875 | + def ascii_decode(self, event, text, base_to): |
876 | + "Display the values of each character in an ASCII string" |
877 | + |
878 | + base_to = self._parse_base(base_to) |
879 | + |
880 | + if len(text) > 2 and text[0] == text[-1] and text[0] in ("'", '"'): |
881 | + text = text[1:-1] |
882 | + |
883 | + output = u"" |
884 | + for char in text: |
885 | + code_point = ord(char) |
886 | + if code_point > 255: |
887 | + output += u'U%s ' % self._in_base(code_point, base_to) |
888 | + else: |
889 | + output += self._in_base(code_point, base_to) + u" " |
890 | + |
891 | + output = output.strip() |
892 | + |
893 | + event.addresponse(u'That is %(result)s in %(base)s', { |
894 | + 'result': output, |
895 | + 'base': self._base_name(base_to), |
896 | + }) |
897 | + |
898 | + if base_to == 64 and [True for plugin in ibid.processors if getattr(plugin, 'feature', None) == 'base64']: |
899 | + event.addresponse(u'If you want a base64 encoding, use the "base64" feature') |
900 | + |
901 | + @handler |
902 | + def ascii_encode(self, event, source, base_from): |
903 | + |
904 | + base_from = self._parse_base(base_from) |
905 | + |
906 | + output = u"" |
907 | + buf = u"" |
908 | + |
909 | + def process_buf(buf): |
910 | + char = self._from_base(buf, base_from) |
911 | + if char > 127: |
912 | + raise ValueError(u"I only deal with the first page of ASCII (i.e. under 127). %i is invalid." % char) |
913 | + elif char < 32: |
914 | + return u" %s " % "NUL SOH STX EOT ENQ ACK BEL BS HT LF VT FF SO SI DLE DC1 DC2 DC2 DC4 NAK SYN ETB CAN EM SUB ESC FS GS RS US".split()[char] |
915 | + elif char == 127: |
916 | + return u" DEL " |
917 | + return unichr(char) |
918 | + |
919 | + try: |
920 | + for char in source: |
921 | + if char == u" ": |
922 | + if len(buf) > 0: |
923 | + output += process_buf(buf) |
924 | + buf = u"" |
925 | + else: |
926 | + buf += char |
927 | + if (len(buf) == 2 and base_from == 16) or (len(buf) == 3 and base_from == 8) or (len(buf) == 8 and base_from == 2): |
928 | + output += process_buf(buf) |
929 | + buf = u"" |
930 | + |
931 | + if len(buf) > 0: |
932 | + output += process_buf(buf) |
933 | + except ValueError, e: |
934 | + event.addresponse(unicode(e)) |
935 | + return |
936 | + |
937 | + event.addresponse(u'That is "%s"', output) |
938 | + if base_from == 64 and [True for plugin in ibid.processors if getattr(plugin, 'feature', None) == 'base64']: |
939 | + event.addresponse(u'If you want a base64 encoding, use the "base64" feature') |
940 | + |
941 | +help['units'] = 'Converts values between various units.' |
942 | +class Units(Processor): |
943 | + u"""convert [<value>] <unit> to <unit>""" |
944 | + feature = 'units' |
945 | + priority = 10 |
946 | + |
947 | + units = Option('units', 'Path to units executable', 'units') |
948 | + |
949 | + temp_scale_names = { |
950 | + 'fahrenheit': 'tempF', |
951 | + 'f': 'tempF', |
952 | + 'celsius': 'tempC', |
953 | + 'celcius': 'tempC', |
954 | + 'c': 'tempC', |
955 | + 'kelvin': 'tempK', |
956 | + 'k': 'tempK', |
957 | + 'rankine': 'tempR', |
958 | + 'r': 'tempR', |
959 | + } |
960 | + |
961 | + temp_function_names = set(temp_scale_names.values()) |
962 | + |
963 | + def setup(self): |
964 | + if not file_in_path(self.units): |
965 | + raise Exception("Cannot locate units executable") |
966 | + |
967 | + def format_temperature(self, unit): |
968 | + "Return the unit, and convert to 'tempX' format if a known temperature scale" |
969 | + |
970 | + lunit = unit.lower() |
971 | + if lunit in self.temp_scale_names: |
972 | + unit = self.temp_scale_names[lunit] |
973 | + elif lunit.startswith("deg") and " " in lunit and lunit.split(None, 1)[1] in self.temp_scale_names: |
974 | + unit = self.temp_scale_names[lunit.split(None, 1)[1]] |
975 | + return unit |
976 | + |
977 | + @match(r'^convert\s+(-?[0-9.]+)?\s*(.+)\s+(?:in)?to\s+(.+)$') |
978 | + def convert(self, event, value, frm, to): |
979 | + |
980 | + # We have to special-case temperatures because GNU units uses function notation |
981 | + # for direct temperature conversions |
982 | + if self.format_temperature(frm) in self.temp_function_names \ |
983 | + and self.format_temperature(to) in self.temp_function_names: |
984 | + frm = self.format_temperature(frm) |
985 | + to = self.format_temperature(to) |
986 | + |
987 | + if value is not None: |
988 | + if frm in self.temp_function_names: |
989 | + frm = "%s(%s)" % (frm, value) |
990 | + else: |
991 | + frm = '%s %s' % (value, frm) |
992 | + |
993 | + units = Popen([self.units, '--verbose', '--', frm, to], stdout=PIPE, stderr=PIPE) |
994 | + output, error = units.communicate() |
995 | + code = units.wait() |
996 | + |
997 | + output = unicode_output(output) |
998 | + result = output.splitlines()[0].strip() |
999 | + |
1000 | + if code == 0: |
1001 | + event.addresponse(result) |
1002 | + elif code == 1: |
1003 | + if result == "conformability error": |
1004 | + event.addresponse(u"I don't think %(from)s can be converted to %(to)s", { |
1005 | + 'from': frm, |
1006 | + 'to': to, |
1007 | + }) |
1008 | + elif result.startswith("conformability error"): |
1009 | + event.addresponse(u"I don't think %(from)s can be converted to %(to)s: %(error)s", { |
1010 | + 'from': frm, |
1011 | + 'to': to, |
1012 | + 'error': result.split(":", 1)[1], |
1013 | + }) |
1014 | + else: |
1015 | + event.addresponse(u"I can't do that: %s", result) |
1016 | + |
1017 | +help['currency'] = u'Converts amounts between currencies.' |
1018 | +class Currency(Processor): |
1019 | + u"""exchange <amount> <currency> for <currency> |
1020 | + currencies for <country>""" |
1021 | + |
1022 | + feature = "currency" |
1023 | + |
1024 | + headers = {'User-Agent': 'Mozilla/5.0', 'Referer': 'http://www.xe.com/'} |
1025 | + currencies = {} |
1026 | + country_codes = {} |
1027 | + |
1028 | + def _load_currencies(self): |
1029 | + etree = get_html_parse_tree('http://www.xe.com/iso4217.php', headers=self.headers, treetype='etree') |
1030 | + |
1031 | + tbl_main = [x for x in etree.getiterator('table') if x.get('class') == 'tbl_main'][0] |
1032 | + |
1033 | + self.currencies = {} |
1034 | + for tbl_sub in tbl_main.getiterator('table'): |
1035 | + if tbl_sub.get('class') == 'tbl_sub': |
1036 | + for tr in tbl_sub.getiterator('tr'): |
1037 | + code, place = [x.text for x in tr.getchildren()] |
1038 | + name = u'' |
1039 | + if not place: |
1040 | + place = u'' |
1041 | + if u',' in place[1:-1]: |
1042 | + place, name = place.split(u',', 1) |
1043 | + place = place.strip() |
1044 | + if code in self.currencies: |
1045 | + currency = self.currencies[code] |
1046 | + # Are we using another country's currency? |
1047 | + if place != u'' and name != u'' and (currency[1] == u'' or currency[1].rsplit(None, 1)[0] in place |
1048 | + or (u'(also called' in currency[1] and currency[1].split(u'(', 1)[0].rsplit(None, 1)[0] in place)): |
1049 | + currency[0].insert(0, place) |
1050 | + currency[1] = name.strip() |
1051 | + else: |
1052 | + currency[0].append(place) |
1053 | + else: |
1054 | + self.currencies[code] = [[place], name.strip()] |
1055 | + |
1056 | + # Special cases for shared currencies: |
1057 | + self.currencies['EUR'][0].insert(0, u'Euro Member Countries') |
1058 | + self.currencies['XOF'][0].insert(0, u'Communaut\xe9 Financi\xe8re Africaine') |
1059 | + self.currencies['XOF'][1] = u'Francs' |
1060 | + |
1061 | + strip_currency_re = re.compile(r'^[\.\s]*([\w\s]+?)s?$', re.UNICODE) |
1062 | + |
1063 | + def _resolve_currency(self, name, rough=True): |
1064 | + "Return the canonical name for a currency" |
1065 | + |
1066 | + if name.upper() in self.currencies: |
1067 | + return name.upper() |
1068 | + |
1069 | + m = self.strip_currency_re.match(name) |
1070 | + |
1071 | + if m is None: |
1072 | + return False |
1073 | + |
1074 | + name = m.group(1).lower() |
1075 | + |
1076 | + # TLD -> country name |
1077 | + if rough and len(name) == 2 and name.upper() in self.country_codes: |
1078 | + name = self.country_codes[name.upper()].lower() |
1079 | + |
1080 | + # Currency Name |
1081 | + if name == u'dollar': |
1082 | + return "USD" |
1083 | + |
1084 | + name_re = re.compile(r'^(.+\s+)?\(?%ss?\)?(\s+.+)?$' % name, re.I | re.UNICODE) |
1085 | + for code, (places, currency) in self.currencies.iteritems(): |
1086 | + if name_re.match(currency) or [True for place in places if name_re.match(place)]: |
1087 | + return code |
1088 | + |
1089 | + return False |
1090 | + |
1091 | + @match(r'^(exchange|convert)\s+([0-9.]+)\s+(.+)\s+(?:for|to|into)\s+(.+)$') |
1092 | + def exchange(self, event, command, amount, frm, to): |
1093 | + if not self.currencies: |
1094 | + self._load_currencies() |
1095 | + |
1096 | + if not self.country_codes: |
1097 | + self.country_codes = get_country_codes() |
1098 | + |
1099 | + rough = command.lower() == 'exchange' |
1100 | + |
1101 | + canonical_frm = self._resolve_currency(frm, rough) |
1102 | + canonical_to = self._resolve_currency(to, rough) |
1103 | + if not canonical_frm or not canonical_to: |
1104 | + if rough: |
1105 | + event.addresponse(u"Sorry, I don't know about a currency for %s", (not canonical_frm and frm or to)) |
1106 | + return |
1107 | + |
1108 | + data = {'Amount': amount, 'From': canonical_frm, 'To': canonical_to} |
1109 | + etree = get_html_parse_tree('http://www.xe.com/ucc/convert.cgi', urlencode(data), self.headers, 'etree') |
1110 | + |
1111 | + result = [tag.text for tag in etree.getiterator('h2')] |
1112 | + if result: |
1113 | + event.addresponse(u'%(fresult)s (%(fcountry)s %(fcurrency)s) = %(tresult)s (%(tcountry)s %(tcurrency)s)', { |
1114 | + 'fresult': result[0], |
1115 | + 'tresult': result[2], |
1116 | + 'fcountry': self.currencies[canonical_frm][0][0], |
1117 | + 'fcurrency': self.currencies[canonical_frm][1], |
1118 | + 'tcountry': self.currencies[canonical_to][0][0], |
1119 | + 'tcurrency': self.currencies[canonical_to][1], |
1120 | + }) |
1121 | + else: |
1122 | + event.addresponse(u"The bureau de change appears to be closed for lunch") |
1123 | + |
1124 | + @match(r'^(?:currency|currencies)\s+for\s+(?:the\s+)?(.+)$') |
1125 | + def currency(self, event, place): |
1126 | + if not self.currencies: |
1127 | + self._load_currencies() |
1128 | + |
1129 | + search = re.compile(place, re.I) |
1130 | + results = [] |
1131 | + for code, (places, name) in self.currencies.iteritems(): |
1132 | + for place in places: |
1133 | + if search.search(place): |
1134 | + results.append(u'%s uses %s (%s)' % (place, name, code)) |
1135 | + break |
1136 | + |
1137 | + if results: |
1138 | + event.addresponse(human_join(results)) |
1139 | + else: |
1140 | + event.addresponse(u'No currencies found') |
1141 | + |
1142 | +# vi: set et sta sw=4 ts=4: |
1143 | |
1144 | === modified file 'ibid/plugins/core.py' |
1145 | --- ibid/plugins/core.py 2010-01-14 22:12:39 +0000 |
1146 | +++ ibid/plugins/core.py 2010-01-18 18:54:16 +0000 |
1147 | @@ -4,7 +4,6 @@ |
1148 | import logging |
1149 | |
1150 | import ibid |
1151 | -from ibid.compat import any |
1152 | from ibid.config import IntOption, ListOption, DictOption |
1153 | from ibid.plugins import Processor, handler |
1154 | from ibid.plugins.identity import identify |
1155 | |
1156 | === modified file 'ibid/plugins/dbus.py' |
1157 | --- ibid/plugins/dbus.py 2009-07-09 14:45:08 +0000 |
1158 | +++ ibid/plugins/dbus.py 2010-01-18 18:54:16 +0000 |
1159 | @@ -1,4 +1,5 @@ |
1160 | import sys |
1161 | +import re |
1162 | from traceback import print_exc |
1163 | |
1164 | import ibid |
1165 | @@ -8,7 +9,7 @@ |
1166 | autoload = False |
1167 | |
1168 | def __init__(self, name): |
1169 | - Module.__init__(self, name) |
1170 | + Processor.__init__(self, name) |
1171 | if 'dbus' not in sys.modules: |
1172 | raise Exception('dbus library not loaded') |
1173 | |
1174 | |
1175 | === modified file 'ibid/plugins/factoid.py' |
1176 | --- ibid/plugins/factoid.py 2009-12-30 22:01:38 +0000 |
1177 | +++ ibid/plugins/factoid.py 2010-01-18 18:54:16 +0000 |
1178 | @@ -15,7 +15,7 @@ |
1179 | from ibid.plugins.identity import get_identities |
1180 | from ibid.utils import format_date |
1181 | |
1182 | -help = {'factoids': u'Factoids are arbitrary pieces of information stored by a key. ' |
1183 | +help = {'factoid': u'Factoids are arbitrary pieces of information stored by a key. ' |
1184 | u'Factoids beginning with a command such as "<action>" or "<reply>" will supress the "name verb value" output. ' |
1185 | u"Search and replace functions won't use real regexs unless appended with the 'r' flag."} |
1186 | |
1187 | @@ -257,7 +257,7 @@ |
1188 | |
1189 | class Utils(Processor): |
1190 | u"""literal <name> [( #<from number> | /<pattern>/[r] )]""" |
1191 | - feature = 'factoids' |
1192 | + feature = 'factoid' |
1193 | |
1194 | @match(r'^literal\s+(.+?)(?:\s+#(\d+)|\s+(?:/(.+?)/(r?)))?$') |
1195 | def literal(self, event, name, number, pattern, is_regex): |
1196 | @@ -272,7 +272,7 @@ |
1197 | class Forget(Processor): |
1198 | u"""forget <name> [( #<number> | /<pattern>/[r] )] |
1199 | <name> is the same as <other name>""" |
1200 | - feature = 'factoids' |
1201 | + feature = 'factoid' |
1202 | |
1203 | priority = 10 |
1204 | permission = u'factoid' |
1205 | @@ -368,7 +368,7 @@ |
1206 | |
1207 | class Search(Processor): |
1208 | u"""search [for] [<limit>] [(facts|values) [containing]] (<pattern>|/<pattern>/[r]) [from <start>]""" |
1209 | - feature = 'factoids' |
1210 | + feature = 'factoid' |
1211 | |
1212 | limit = IntOption('search_limit', u'Maximum number of results to return', 30) |
1213 | default = IntOption('search_default', u'Default number of results to return', 10) |
1214 | @@ -443,7 +443,7 @@ |
1215 | |
1216 | class Get(Processor, RPC): |
1217 | u"""<factoid> [( #<number> | /<pattern>/[r] )]""" |
1218 | - feature = 'factoids' |
1219 | + feature = 'factoid' |
1220 | |
1221 | priority = 200 |
1222 | |
1223 | @@ -497,7 +497,7 @@ |
1224 | <name> (<verb>|=<verb>=) [also] <value> |
1225 | last set factoid |
1226 | """ |
1227 | - feature = 'factoids' |
1228 | + feature = 'factoid' |
1229 | |
1230 | interrogatives = ListOption('interrogatives', 'Question words to strip', default_interrogatives) |
1231 | verbs = ListOption('verbs', 'Verbs that split name from value', default_verbs) |
1232 | @@ -575,7 +575,7 @@ |
1233 | class Modify(Processor): |
1234 | u"""<name> [( #<number> | /<pattern>/[r] )] += <suffix> |
1235 | <name> [( #<number> | /<pattern>/[r] )] ~= ( s/<regex>/<replacement>/[g][i][r] | y/<source>/<dest>/ )""" |
1236 | - feature = 'factoids' |
1237 | + feature = 'factoid' |
1238 | |
1239 | permission = u'factoid' |
1240 | permissions = (u'factoidadmin',) |
1241 | |
1242 | === added file 'ibid/plugins/film.py' |
1243 | --- ibid/plugins/film.py 1970-01-01 00:00:00 +0000 |
1244 | +++ ibid/plugins/film.py 2010-01-18 18:54:16 +0000 |
1245 | @@ -0,0 +1,212 @@ |
1246 | +from urllib2 import urlopen |
1247 | +from urllib import urlencode |
1248 | +from time import strptime, strftime |
1249 | +import logging |
1250 | +try: |
1251 | + from imdb import IMDb, IMDbDataAccessError, IMDbError |
1252 | +except ImportError: |
1253 | + IMDb = IMDbDataAccessError = IMDbError = None |
1254 | + |
1255 | +from ibid.compat import defaultdict |
1256 | +from ibid.plugins import Processor, match |
1257 | +from ibid.utils import human_join |
1258 | +from ibid.config import Option, BoolOption |
1259 | + |
1260 | +log = logging.getLogger('plugins.film') |
1261 | + |
1262 | +help = {} |
1263 | + |
1264 | +help['tvshow'] = u'Retrieves TV show information from tvrage.com.' |
1265 | +class TVShow(Processor): |
1266 | + u"""tvshow <show>""" |
1267 | + |
1268 | + feature = 'tvshow' |
1269 | + |
1270 | + def remote_tvrage(self, show): |
1271 | + info_url = 'http://services.tvrage.com/tools/quickinfo.php?%s' |
1272 | + |
1273 | + info = urlopen(info_url % urlencode({'show': show.encode('utf-8')})) |
1274 | + |
1275 | + info = info.read() |
1276 | + info = info.decode('utf-8') |
1277 | + if info.startswith('No Show Results Were Found'): |
1278 | + return |
1279 | + info = info[5:].splitlines() |
1280 | + show_info = [i.split('@', 1) for i in info] |
1281 | + show_dict = dict(show_info) |
1282 | + |
1283 | + #check if there are actual airdates for Latest and Next Episode. None for Next |
1284 | + #Episode does not neccesarily mean it is nor airing, just the date is unconfirmed. |
1285 | + show_dict = defaultdict(lambda: 'None', show_info) |
1286 | + |
1287 | + for field in ('Latest Episode', 'Next Episode'): |
1288 | + if field in show_dict: |
1289 | + ep, name, date = show_dict[field].split('^', 2) |
1290 | + count = date.count('/') |
1291 | + format_from = { |
1292 | + 0: '%Y', |
1293 | + 1: '%b/%Y', |
1294 | + 2: '%b/%d/%Y' |
1295 | + }[count] |
1296 | + format_to = ' '.join(('%d', '%B', '%Y')[-1 - count:]) |
1297 | + date = strftime(format_to, strptime(date, format_from)) |
1298 | + show_dict[field] = u'%s - "%s" - %s' % (ep, name, date) |
1299 | + |
1300 | + if 'Genres' in show_dict: |
1301 | + show_dict['Genres'] = human_join(show_dict['Genres'].split(' | ')) |
1302 | + |
1303 | + return show_dict |
1304 | + |
1305 | + @match(r'^tv\s*show\s+(.+)$') |
1306 | + def tvshow(self, event, show): |
1307 | + retr_info = self.remote_tvrage(show) |
1308 | + |
1309 | + message = u'Show: %(Show Name)s. Premiered: %(Premiered)s. ' \ |
1310 | + u'Latest Episode: %(Latest Episode)s. Next Episode: %(Next Episode)s. ' \ |
1311 | + u'Airtime: %(Airtime)s on %(Network)s. Genres: %(Genres)s. ' \ |
1312 | + u'Status: %(Status)s. %(Show URL)s' |
1313 | + |
1314 | + if not retr_info: |
1315 | + event.addresponse(u"I can't find anything out about '%s'", show) |
1316 | + return |
1317 | + |
1318 | + event.addresponse(message, retr_info) |
1319 | + |
1320 | +# Uses the IMDbPY package in http mode against imdb.com |
1321 | +# This isn't strictly legal: http://www.imdb.com/help/show_leaf?usedatasoftware |
1322 | +# |
1323 | +# Note that it will return porn movies by default. |
1324 | +help['imdb'] = u'Looks up movies on IMDB.com.' |
1325 | +class IMDB(Processor): |
1326 | + u"imdb [search] [character|company|episode|movie|person] <terms> [#<index>]" |
1327 | + feature = 'imdb' |
1328 | + |
1329 | + access_system = Option("accesssystem", "Method of querying IMDB", "http") |
1330 | + adult_search = BoolOption("adultsearch", "Include adult films in search results", True) |
1331 | + |
1332 | + name_keys = { |
1333 | + "character": "long imdb name", |
1334 | + "company": "long imdb name", |
1335 | + "episode": "long imdb title", |
1336 | + "movie": "long imdb title", |
1337 | + "person": "name", |
1338 | + } |
1339 | + |
1340 | + def setup(self): |
1341 | + if IMDb is None: |
1342 | + raise Exception("IMDbPY not installed") |
1343 | + self.imdb = IMDb(accessSystem=self.access_system, adultSearch=int(self.adult_search)) |
1344 | + |
1345 | + @match(r'^imdb(?:\s+search)?(?:\s+(character|company|episode|movie|person))?\s+(.+?)(?:\s+#(\d+))?$') |
1346 | + def search(self, event, search_type, terms, index): |
1347 | + if search_type is None: |
1348 | + search_type = "movie" |
1349 | + if index is not None: |
1350 | + index = int(index) - 1 |
1351 | + |
1352 | + result = None |
1353 | + try: |
1354 | + if terms.isdigit(): |
1355 | + result = getattr(self.imdb, "get_" + search_type)(terms) |
1356 | + else: |
1357 | + results = getattr(self.imdb, "search_" + search_type)(terms) |
1358 | + |
1359 | + if len(results) == 1: |
1360 | + index = 0 |
1361 | + |
1362 | + if index is not None: |
1363 | + result = results[index] |
1364 | + self.imdb.update(result) |
1365 | + |
1366 | + except IMDbDataAccessError, e: |
1367 | + event.addresponse(u"IMDb doesn't like me today. It said '%s'", e[0]["errmsg"]) |
1368 | + raise |
1369 | + |
1370 | + except IMDbError, e: |
1371 | + event.addresponse(u'IMDb must be having a bad day (or you are asking it silly things)') |
1372 | + raise |
1373 | + |
1374 | + if result is not None: |
1375 | + event.addresponse(u'Found %s', getattr(self, 'display_' + search_type)(result)) |
1376 | + return |
1377 | + |
1378 | + if len(results) == 0: |
1379 | + event.addresponse(u"Sorry, couldn't find that") |
1380 | + else: |
1381 | + results = [x[self.name_keys[search_type]] for x in results] |
1382 | + results = enumerate(results) |
1383 | + results = [u"%i: %s" % (x[0] + 1, x[1]) for x in results] |
1384 | + event.addresponse(u'Found %(greaterthan)s%(num)i matches: %(results)s', { |
1385 | + 'greaterthan': (u'', u'>')[len(results) == 20], |
1386 | + 'num': len(results), |
1387 | + 'results': u', '.join(results), |
1388 | + }) |
1389 | + |
1390 | + def display_character(self, character): |
1391 | + desc = u"%s: %s." % (character.characterID, character["long imdb name"]) |
1392 | + filmography = character.get("filmography", ()) |
1393 | + if len(filmography): |
1394 | + more = (u"", u" etc")[len(filmography) > 5] |
1395 | + desc += u" Appeared in %s%s." % (human_join(x["long imdb title"] for x in filmography[:5]), more) |
1396 | + if character.has_key("introduction"): |
1397 | + desc += u" Bio: %s" % character["introduction"] |
1398 | + return desc |
1399 | + |
1400 | + def display_company(self, company): |
1401 | + desc = "%s: %s" % (company.companyID, company["long imdb name"]) |
1402 | + for key, title in ( |
1403 | + (u"production companies", u"Produced"), |
1404 | + (u"distributors", u"Distributed"), |
1405 | + (u"miscellaneous companies", u"Was involved in")): |
1406 | + if len(company.get(key, ())) > 0: |
1407 | + more = (u"", u" etc.")[len(company[key]) > 3] |
1408 | + desc += u" %s %s%s" % (title, human_join(x["long imdb title"] for x in company[key][:3]), more) |
1409 | + return desc |
1410 | + |
1411 | + def display_episode(self, episode): |
1412 | + desc = u"%s: %s s%02ie%02i %s(%s)." % ( |
1413 | + episode.movieID, episode["series title"], episode["season"], |
1414 | + episode["episode"], episode["title"], episode["year"]) |
1415 | + if len(episode.get("director", ())) > 0: |
1416 | + desc += u" Dir: %s." % (human_join(x["name"] for x in episode["director"])) |
1417 | + if len(episode.get("cast", ())) > 0: |
1418 | + desc += u" Starring: %s." % (human_join(x["name"] for x in episode["cast"][:3])) |
1419 | + if episode.has_key("rating"): |
1420 | + desc += u" Rated: %.1f " % episode["rating"] |
1421 | + desc += human_join(episode.get("genres", ())) |
1422 | + desc += u" Plot: %s" % episode.get("plot outline", u"Unknown") |
1423 | + return desc |
1424 | + |
1425 | + def display_movie(self, movie): |
1426 | + desc = u"%s: %s." % (movie.movieID, movie["long imdb title"]) |
1427 | + if len(movie.get("director", ())) > 0: |
1428 | + desc += u" Dir: %s." % (human_join(x["name"] for x in movie["director"])) |
1429 | + if len(movie.get("cast", ())) > 0: |
1430 | + desc += u" Starring: %s." % (human_join(x["name"] for x in movie["cast"][:3])) |
1431 | + if movie.has_key("rating"): |
1432 | + desc += u" Rated: %.1f " % movie["rating"] |
1433 | + desc += human_join(movie.get("genres", ())) |
1434 | + desc += u" Plot: %s" % movie.get("plot outline", u"Unknown") |
1435 | + return desc |
1436 | + |
1437 | + def display_person(self, person): |
1438 | + desc = u"%s: %s. %s." % (person.personID, person["name"], |
1439 | + human_join(role.title() for role in ( |
1440 | + u"actor", u"animation department", u"art department", |
1441 | + u"art director", u"assistant director", u"camera department", |
1442 | + u"casting department", u"casting director", u"cinematographer", |
1443 | + u"composer", u"costume department", u"costume designer", |
1444 | + u"director", u"editorial department", u"editor", |
1445 | + u"make up department", u"music department", u"producer", |
1446 | + u"production designer", u"set decorator", u"sound department", |
1447 | + u"speccial effects department", u"stunts", u"transport department", |
1448 | + u"visual effects department", u"writer", u"miscellaneous crew" |
1449 | + ) if person.has_key(role))) |
1450 | + if person.has_key("mini biography"): |
1451 | + desc += u" " + u" ".join(person["mini biography"]) |
1452 | + else: |
1453 | + if person.has_key("birth name") or person.has_key("birth date"): |
1454 | + desc += u" Born %s." % u", ".join(person[attr] for attr in ("birth name", "birth date") if person.has_key(attr)) |
1455 | + return desc |
1456 | + |
1457 | +# vi: set et sta sw=4 ts=4: |
1458 | |
1459 | === added file 'ibid/plugins/fun.py' |
1460 | --- ibid/plugins/fun.py 1970-01-01 00:00:00 +0000 |
1461 | +++ ibid/plugins/fun.py 2010-01-18 18:54:16 +0000 |
1462 | @@ -0,0 +1,237 @@ |
1463 | +from unicodedata import normalize |
1464 | +from random import choice, random |
1465 | +import re |
1466 | + |
1467 | +from nickometer import nickometer |
1468 | + |
1469 | +import ibid |
1470 | +from ibid.plugins import Processor, match |
1471 | +from ibid.config import IntOption, ListOption |
1472 | +from ibid.utils import human_join |
1473 | + |
1474 | +help = {} |
1475 | + |
1476 | +help['nickometer'] = u'Calculates how lame a nick is.' |
1477 | +class Nickometer(Processor): |
1478 | + u"""nickometer [<nick>] [with reasons]""" |
1479 | + feature = 'nickometer' |
1480 | + |
1481 | + @match(r'^(?:nick|lame)-?o-?meter(?:(?:\s+for)?\s+(.+?))?(\s+with\s+reasons)?$') |
1482 | + def handle_nickometer(self, event, nick, wreasons): |
1483 | + nick = nick or event.sender['nick'] |
1484 | + if u'\ufffd' in nick: |
1485 | + score, reasons = 100., ((u'Not UTF-8 clean', u'infinite'),) |
1486 | + else: |
1487 | + score, reasons = nickometer(normalize('NFKD', nick).encode('ascii', 'ignore')) |
1488 | + |
1489 | + event.addresponse(u'%(nick)s is %(score)s%% lame', { |
1490 | + 'nick': nick, |
1491 | + 'score': score, |
1492 | + }) |
1493 | + if wreasons: |
1494 | + if not reasons: |
1495 | + reasons = ((u'A good, traditional nick', 0),) |
1496 | + event.addresponse(u'Because: %s', u', '.join(['%s (%s)' % reason for reason in reasons])) |
1497 | + |
1498 | +help['choose'] = u'Choose one of the given options.' |
1499 | +class Choose(Processor): |
1500 | + u"""choose <choice> or <choice>...""" |
1501 | + feature = 'choose' |
1502 | + |
1503 | + choose_re = re.compile(r'(?:\s*,\s*(?:or\s+)?)|(?:\s+or\s+)', re.I) |
1504 | + |
1505 | + @match(r'^(?:choose|choice|pick)\s+(.+)$') |
1506 | + def choose(self, event, choices): |
1507 | + event.addresponse(u'I choose %s', choice(self.choose_re.split(choices))) |
1508 | + |
1509 | +help['coffee'] = u"Times coffee brewing and reserves cups for people" |
1510 | +class Coffee(Processor): |
1511 | + u"""coffee (on|please)""" |
1512 | + feature = 'coffee' |
1513 | + |
1514 | + pots = {} |
1515 | + |
1516 | + time = IntOption('coffee_time', u'Brewing time in seconds', 240) |
1517 | + cups = IntOption('coffee_cups', u'Maximum number of cups', 4) |
1518 | + |
1519 | + def coffee_announce(self, event): |
1520 | + event.addresponse(u"Coffee's ready for %s!", |
1521 | + human_join(self.pots[(event.source, event.channel)])) |
1522 | + del self.pots[(event.source, event.channel)] |
1523 | + |
1524 | + @match(r'^coffee\s+on$') |
1525 | + def coffee_on(self, event): |
1526 | + if (event.source, event.channel) in self.pots: |
1527 | + if len(self.pots[(event.source, event.channel)]) >= self.cups: |
1528 | + event.addresponse(u"There's already a pot on, and it's all reserved") |
1529 | + elif event.sender['nick'] in self.pots[(event.source, event.channel)]: |
1530 | + event.addresponse(u"You already have a pot on the go") |
1531 | + else: |
1532 | + event.addresponse(u"There's already a pot on. If you ask nicely, maybe you can have a cup") |
1533 | + return |
1534 | + |
1535 | + self.pots[(event.source, event.channel)] = [event.sender['nick']] |
1536 | + ibid.dispatcher.call_later(self.time, self.coffee_announce, event) |
1537 | + |
1538 | + event.addresponse(choice(( |
1539 | + u'puts the kettle on', |
1540 | + u'starts grinding coffee', |
1541 | + u'flips the salt-timer', |
1542 | + u'washes some mugs', |
1543 | + )), action=True) |
1544 | + |
1545 | + @match('^coffee\s+(?:please|pls)$') |
1546 | + def coffee_accept(self, event): |
1547 | + if (event.source, event.channel) not in self.pots: |
1548 | + event.addresponse(u"There isn't a pot on") |
1549 | + |
1550 | + elif len(self.pots[(event.source, event.channel)]) >= self.cups: |
1551 | + event.addresponse(u"Sorry, there aren't any more cups left") |
1552 | + |
1553 | + elif event.sender['nick'] in self.pots[(event.source, event.channel)]: |
1554 | + event.addresponse(u"Now now, we don't want anyone getting caffeine overdoses") |
1555 | + |
1556 | + else: |
1557 | + self.pots[(event.source, event.channel)].append(event.sender['nick']) |
1558 | + event.addresponse(True) |
1559 | + |
1560 | +help['insult'] = u"Slings verbal abuse at someone" |
1561 | +class Insult(Processor): |
1562 | + u""" |
1563 | + (flame | insult) <person> |
1564 | + (swear | cuss | explete) [at <person>] |
1565 | + """ |
1566 | + feature = 'insult' |
1567 | + |
1568 | + adjectives = ListOption('adjectives', 'List of adjectives', ( |
1569 | + u'acidic', u'antique', u'artless', u'base-court', u'bat-fowling', |
1570 | + u'bawdy', u'beef-witted', u'beetle-headed', u'beslubbering', |
1571 | + u'boil-brained', u'bootless', u'churlish', u'clapper-clawed', |
1572 | + u'clay-brained', u'clouted', u'cockered', u'common-kissing', |
1573 | + u'contemptible', u'coughed-up', u'craven', u'crook-pated', |
1574 | + u'culturally-unsound', u'currish', u'dankish', u'decayed', |
1575 | + u'despicable', u'dismal-dreaming', u'dissembling', u'dizzy-eyed', |
1576 | + u'doghearted', u'dread-bolted', u'droning', u'earth-vexing', |
1577 | + u'egg-sucking', u'elf-skinned', u'errant', u'evil', u'fat-kidneyed', |
1578 | + u'fawning', u'fen-sucked', u'fermented', u'festering', u'flap-mouthed', |
1579 | + u'fly-bitten', u'fobbing', u'folly-fallen', u'fool-born', u'foul', |
1580 | + u'frothy', u'froward', u'full-gorged', u'fulminating', u'gleeking', |
1581 | + u'goatish', u'gorbellied', u'guts-griping', u'hacked-up', u'halfbaked', |
1582 | + u'half-faced', u'hasty-witted', u'headless', u'hedge-born', |
1583 | + u'hell-hated', u'horn-beat', u'hugger-muggered', u'humid', |
1584 | + u'idle-headed', u'ill-borne', u'ill-breeding', u'ill-nurtured', |
1585 | + u'imp-bladdereddle-headed', u'impertinent', u'impure', u'industrial', |
1586 | + u'inept', u'infected', u'infectious', u'inferior', u'it-fowling', |
1587 | + u'jarring', u'knotty-pated', u'left-over', u'lewd-minded', |
1588 | + u'loggerheaded', u'low-quality', u'lumpish', u'malodorous', |
1589 | + u'malt-wormy', u'mammering', u'mangled', u'measled', u'mewling', |
1590 | + u'milk-livered', u'motley-mind', u'motley-minded', u'off-color', |
1591 | + u'onion-eyed', u'paunchy', u'penguin-molesting', u'petrified', |
1592 | + u'pickled', u'pignutted', u'plume-plucked', u'pointy-nosed', u'porous', |
1593 | + u'pottle-deep', u'pox-marked', u'pribbling', u'puking', u'puny', |
1594 | + u'railing', u'rank', u'reeky', u'reeling-ripe', u'roguish', |
1595 | + u'rough-hewn', u'rude-growing', u'rude-snouted', u'rump-fed', |
1596 | + u'ruttish', u'salty', u'saucy', u'saucyspleened', u'sausage-snorfling', |
1597 | + u'shard-borne', u'sheep-biting', u'spam-sucking', u'spleeny', |
1598 | + u'spongy', u'spur-galled', u'squishy', u'surly', u'swag-bellied', |
1599 | + u'tardy-gaited', u'tastless', u'tempestuous', u'tepid', u'thick', |
1600 | + u'tickle-brained', u'toad-spotted', u'tofu-nibbling', u'tottering', |
1601 | + u'uninspiring', u'unintelligent', u'unmuzzled', u'unoriginal', |
1602 | + u'urchin-snouted', u'vain', u'vapid', u'vassal-willed', u'venomed', |
1603 | + u'villainous', u'warped', u'wayward', u'weasel-smelling', |
1604 | + u'weather-bitten', u'weedy', u'wretched', u'yeasty', |
1605 | + )) |
1606 | + |
1607 | + collections = ListOption('collections', 'List of collective nouns', ( |
1608 | + u'accumulation', u'ass-full', u'assload', u'bag', u'bucket', |
1609 | + u'coagulation', u'enema-bucketful', u'gob', u'half-mouthful', u'heap', |
1610 | + u'mass', u'mound', u'ooze', u'petrification', u'pile', u'plate', |
1611 | + u'puddle', u'quart', u'stack', u'thimbleful', u'tongueful', |
1612 | + )) |
1613 | + |
1614 | + nouns = ListOption('nouns', u'List of singular nouns', ( |
1615 | + u'apple-john', u'baggage', u'barnacle', u'bladder', u'boar-pig', |
1616 | + u'bugbear', u'bum-bailey', u'canker-blossom', u'clack-dish', |
1617 | + u'clotpole', u'coxcomb', u'codpiece', u'death-token', u'dewberry', |
1618 | + u'flap-dragon', u'flax-wench', u'flirt-gill', u'foot-licker', |
1619 | + u'fustilarian', u'giglet', u'gudgeon', u'haggard', u'harpy', |
1620 | + u'hedge-pig', u'horn-beast', u'hugger-mugger', u'jolthead', |
1621 | + u'lewdster', u'lout', u'maggot-pie', u'malt-worm', u'mammet', |
1622 | + u'measle', u'minnow', u'miscreant', u'moldwarp', u'mumble-news', |
1623 | + u'nut-hook', u'pigeon-egg', u'pignut', u'puttock', u'pumpion', |
1624 | + u'ratsbane', u'scut', u'skainsmate', u'strumpet', u'varlet', u'vassal', |
1625 | + u'whey-face', u'wagtail', |
1626 | + )) |
1627 | + |
1628 | + plnouns = ListOption('plnouns', u'List of plural nouns', ( |
1629 | + u'anal warts', u'armadillo snouts', u'bat toenails', u'bug spit', |
1630 | + u'buzzard gizzards', u'cat bladders', u'cat hair', u'cat-hair-balls', |
1631 | + u'chicken piss', u'cold sores', u'craptacular carpet droppings', |
1632 | + u'dog balls', u'dog vomit', u'dung', u'eel ooze', u'entrails', |
1633 | + u"fat-woman's stomach-bile", u'fish heads', u'guano', u'gunk', |
1634 | + u'jizzum', u'pods', u'pond scum', u'poop', u'poopy', u'pus', |
1635 | + u'rat-farts', u'rat retch', u'red dye number-9', u'seagull puke', |
1636 | + u'slurpee-backwash', u'snake assholes', u'snake bait', u'snake snot', |
1637 | + u'squirrel guts', u'Stimpy-drool', u'Sun IPC manuals', u'toxic waste', |
1638 | + u'urine samples', u'waffle-house grits', u'yoo-hoo', |
1639 | + )) |
1640 | + |
1641 | + @match(r'^(?:insult|flame)\s+(.+)$') |
1642 | + def insult(self, event, insultee): |
1643 | + articleadj = choice(self.adjectives) |
1644 | + articleadj = (articleadj[0] in u'aehiou' and u'an ' or u'a ') + articleadj |
1645 | + |
1646 | + event.addresponse(choice(( |
1647 | + u'%(insultee)s, thou %(adj1)s, %(adj2)s %(noun)s', |
1648 | + u'%(insultee)s is nothing but %(articleadj)s %(collection)s of %(adj1)s %(plnoun)s', |
1649 | + )), { |
1650 | + 'insultee': insultee, |
1651 | + 'adj1': choice(self.adjectives), |
1652 | + 'adj2': choice(self.adjectives), |
1653 | + 'articleadj': articleadj, |
1654 | + 'collection': choice(self.collections), |
1655 | + 'noun': choice(self.nouns), |
1656 | + 'plnoun': choice(self.plnouns), |
1657 | + }, address=False) |
1658 | + |
1659 | + loneadjectives = ListOption('loneadjectives', |
1660 | + 'List of stand-alone adjectives for swearing', ( |
1661 | + 'bloody', 'damn', 'fucking', 'shitting', 'sodding', 'crapping', |
1662 | + 'wanking', 'buggering', |
1663 | + )) |
1664 | + |
1665 | + swearadjectives = ListOption('swearadjectives', |
1666 | + 'List of adjectives to be combined with swearnouns', ( |
1667 | + 'reaming', 'lapping', 'eating', 'sucking', 'vokken', 'kak', |
1668 | + 'donder', 'bliksem', 'fucking', 'shitting', 'sodding', 'crapping', |
1669 | + 'wanking', 'buggering', |
1670 | + )) |
1671 | + |
1672 | + swearnouns = ListOption('swearnouns', |
1673 | + 'List of nounes to be comined with swearadjectives', ( |
1674 | + 'shit', 'cunt', 'hell', 'mother', 'god', 'maggot', 'father', 'crap', |
1675 | + 'ball', 'whore', 'goat', 'dick', 'cock', 'pile', 'bugger', 'poes', |
1676 | + 'hoer', 'kakrooker', 'ma', 'pa', 'naiier', 'kak', 'bliksem', |
1677 | + 'vokker', 'kakrooker', |
1678 | + )) |
1679 | + |
1680 | + swearlength = IntOption('swearlength', 'Number of expletives to swear with', |
1681 | + 15) |
1682 | + |
1683 | + @match(r'^(?:swear|cuss|explete)(?:\s+at\s+(?:the\s+)?(.*))?$') |
1684 | + def swear(self, event, insultee): |
1685 | + swearage = [] |
1686 | + for i in range(self.swearlength): |
1687 | + if random() > 0.7: |
1688 | + swearage.append(choice(self.loneadjectives)) |
1689 | + else: |
1690 | + swearage.append(choice(self.swearnouns) |
1691 | + + choice(self.swearadjectives)) |
1692 | + if insultee is not None: |
1693 | + swearage.append(insultee) |
1694 | + else: |
1695 | + swearage.append(choice(self.swearnouns)) |
1696 | + |
1697 | + event.addresponse(u' '.join(swearage) + u'!', address=False) |
1698 | + |
1699 | +# vi: set et sta sw=4 ts=4: |
1700 | |
1701 | === added file 'ibid/plugins/geography.py' |
1702 | --- ibid/plugins/geography.py 1970-01-01 00:00:00 +0000 |
1703 | +++ ibid/plugins/geography.py 2010-01-18 18:54:16 +0000 |
1704 | @@ -0,0 +1,352 @@ |
1705 | +from math import acos, sin, cos, radians |
1706 | +from urllib import quote |
1707 | +from urlparse import urljoin |
1708 | +import re |
1709 | +import logging |
1710 | +from os.path import exists, join |
1711 | +from datetime import datetime |
1712 | +from os import walk |
1713 | +from dateutil.parser import parse |
1714 | +from dateutil.tz import gettz, tzlocal, tzoffset |
1715 | + |
1716 | +from ibid.plugins import Processor, match |
1717 | +from ibid.utils import json_webservice, human_join, format_date |
1718 | +from ibid.utils.html import get_html_parse_tree |
1719 | +from ibid.config import Option, DictOption |
1720 | +from ibid.compat import defaultdict |
1721 | + |
1722 | +log = logging.getLogger('plugins.geography') |
1723 | + |
1724 | +help = {} |
1725 | + |
1726 | +help['distance'] = u"Returns the distance between two places" |
1727 | +class Distance(Processor): |
1728 | + u"""distance [in <unit>] between <source> and <destination> |
1729 | + place search for <placename>""" |
1730 | + |
1731 | + # For Mathematics, see: |
1732 | + # http://www.mathforum.com/library/drmath/view/51711.html |
1733 | + # http://mathworld.wolfram.com/GreatCircle.html |
1734 | + |
1735 | + feature = 'distance' |
1736 | + |
1737 | + default_unit_names = { |
1738 | + 'km': "kilometres", |
1739 | + 'mi': "miles", |
1740 | + 'nm': "nautical miles"} |
1741 | + default_radius_values = { |
1742 | + 'km': 6378, |
1743 | + 'mi': 3963.1, |
1744 | + 'nm': 3443.9} |
1745 | + |
1746 | + unit_names = DictOption('unit_names', 'Names of units in which to specify distances', default_unit_names) |
1747 | + radius_values = DictOption('radius_values', 'Radius of the earth in the units in which to specify distances', default_radius_values) |
1748 | + |
1749 | + def get_place_data(self, place, num): |
1750 | + return json_webservice('http://ws.geonames.org/searchJSON', {'q': place, 'maxRows': num}) |
1751 | + |
1752 | + def get_place(self, place): |
1753 | + js = self.get_place_data(place, 1) |
1754 | + if js['totalResultsCount'] == 0: |
1755 | + return None |
1756 | + info = js['geonames'][0] |
1757 | + return {'name': "%s, %s, %s" % (info['name'], info['adminName1'], info['countryName']), |
1758 | + 'lng': radians(info['lng']), |
1759 | + 'lat': radians(info['lat'])} |
1760 | + |
1761 | + @match(r'^(?:(?:search\s+for\s+place)|(?:place\s+search\s+for)|(?:places\s+for))\s+(\S.+?)\s*$') |
1762 | + def placesearch(self, event, place): |
1763 | + js = self.get_place_data(place, 10) |
1764 | + if js['totalResultsCount'] == 0: |
1765 | + event.addresponse(u"I don't know of anywhere even remotely like '%s'", place) |
1766 | + else: |
1767 | + event.addresponse(u"I can find: %s", |
1768 | + (human_join([u"%s, %s, %s" % (p['name'], p['adminName1'], p['countryName']) |
1769 | + for p in js['geonames'][:10]], |
1770 | + separator=u';'))) |
1771 | + |
1772 | + @match(r'^(?:how\s*far|distance)(?:\s+in\s+(\S+))?\s+' |
1773 | + r'(?:(between)|from)' # Between ... and ... | from ... to ... |
1774 | + r'\s+(\S.+?)\s+(?(2)and|to)\s+(\S.+?)\s*$') |
1775 | + def distance(self, event, unit, ignore, src, dst): |
1776 | + unit_names = self.unit_names |
1777 | + if unit and unit not in self.unit_names: |
1778 | + event.addresponse(u"I don't know the unit '%(badunit)s'. I know about: %(knownunits)s", { |
1779 | + 'badunit': unit, |
1780 | + 'knownunits': |
1781 | + human_join(u"%s (%s)" % (unit, self.unit_names[unit]) |
1782 | + for unit in self.unit_names), |
1783 | + }) |
1784 | + return |
1785 | + if unit: |
1786 | + unit_names = [unit] |
1787 | + |
1788 | + srcp, dstp = self.get_place(src), self.get_place(dst) |
1789 | + if not srcp or not dstp: |
1790 | + event.addresponse(u"I don't know of anywhere called %s", |
1791 | + (u" or ".join("'%s'" % place[0] |
1792 | + for place in ((src, srcp), (dst, dstp)) if not place[1]))) |
1793 | + return |
1794 | + |
1795 | + dist = acos(cos(srcp['lng']) * cos(dstp['lng']) * cos(srcp['lat']) * cos(dstp['lat']) + |
1796 | + cos(srcp['lat']) * sin(srcp['lng']) * cos(dstp['lat']) * sin(dstp['lng']) + |
1797 | + sin(srcp['lat'])*sin(dstp['lat'])) |
1798 | + |
1799 | + event.addresponse(u"Approximate distance, as the bot flies, between %(srcname)s and %(dstname)s is: %(distance)s", { |
1800 | + 'srcname': srcp['name'], |
1801 | + 'dstname': dstp['name'], |
1802 | + 'distance': human_join([ |
1803 | + u"%.02f %s" % (self.radius_values[unit]*dist, self.unit_names[unit]) |
1804 | + for unit in unit_names], |
1805 | + conjunction=u'or'), |
1806 | + }) |
1807 | + |
1808 | +help['weather'] = u'Retrieves current weather and forecasts for cities.' |
1809 | +class Weather(Processor): |
1810 | + u"""weather in <city> |
1811 | + forecast for <city>""" |
1812 | + |
1813 | + feature = "weather" |
1814 | + |
1815 | + defaults = { 'ct': 'Cape Town, South Africa', |
1816 | + 'jhb': 'Johannesburg, South Africa', |
1817 | + 'joburg': 'Johannesburg, South Africa', |
1818 | + } |
1819 | + places = DictOption('places', 'Alternate names for places', defaults) |
1820 | + labels = ('temp', 'humidity', 'dew', 'wind', 'pressure', 'conditions', 'visibility', 'uv', 'clouds', 'ymin', 'ymax', 'ycool', 'sunrise', 'sunset', 'moonrise', 'moonset', 'moonphase', 'metar') |
1821 | + whitespace = re.compile('\s+') |
1822 | + |
1823 | + class WeatherException(Exception): |
1824 | + pass |
1825 | + |
1826 | + class TooManyPlacesException(WeatherException): |
1827 | + pass |
1828 | + |
1829 | + def _text(self, string): |
1830 | + if not isinstance(string, basestring): |
1831 | + string = ''.join(string.findAll(text=True)) |
1832 | + return self.whitespace.sub(' ', string).strip() |
1833 | + |
1834 | + def _get_page(self, place): |
1835 | + if place.lower() in self.places: |
1836 | + place = self.places[place.lower()] |
1837 | + |
1838 | + soup = get_html_parse_tree('http://m.wund.com/cgi-bin/findweather/getForecast?brand=mobile_metric&query=' + quote(place)) |
1839 | + |
1840 | + if soup.body.center and soup.body.center.b.string == 'Search not found:': |
1841 | + raise Weather.WeatherException(u'City not found') |
1842 | + |
1843 | + if soup.table.tr.th and soup.table.tr.th.string == 'Place: Temperature': |
1844 | + places = [] |
1845 | + for td in soup.table.findAll('td'): |
1846 | + places.append(td.find('a', href=re.compile('.*html$')).string) |
1847 | + |
1848 | + # Cities with more than one airport give duplicate entries. We can take the first |
1849 | + if len([x for x in places if x == places[0]]) == len(places): |
1850 | + url = urljoin('http://m.wund.com/cgi-bin/findweather/getForecast', |
1851 | + soup.table.find('td').find('a', href=re.compile('.*html$'))['href']) |
1852 | + soup = get_html_parse_tree(url) |
1853 | + else: |
1854 | + raise Weather.TooManyPlacesException(places) |
1855 | + |
1856 | + return soup |
1857 | + |
1858 | + def remote_weather(self, place): |
1859 | + soup = self._get_page(place) |
1860 | + tds = [x.table for x in soup.findAll('table') if x.table][0].findAll('td') |
1861 | + |
1862 | + # HACK: Some cities include a windchill row, but others don't |
1863 | + if len(tds) == 39: |
1864 | + del tds[3] |
1865 | + del tds[4] |
1866 | + |
1867 | + values = {'place': tds[0].findAll('b')[1].string, 'time': tds[0].findAll('b')[0].string} |
1868 | + for index, td in enumerate(tds[2::2]): |
1869 | + values[self.labels[index]] = self._text(td) |
1870 | + |
1871 | + return values |
1872 | + |
1873 | + def remote_forecast(self, place): |
1874 | + soup = self._get_page(place) |
1875 | + forecasts = [] |
1876 | + table = [table for table in soup.findAll('table') if table.findAll('td', align='left')][0] |
1877 | + |
1878 | + for td in table.findAll('td', align='left'): |
1879 | + day = td.b.string |
1880 | + forecast = u' '.join([self._text(line) for line in td.contents[2:]]) |
1881 | + forecasts.append(u'%s: %s' % (day, self._text(forecast))) |
1882 | + |
1883 | + return forecasts |
1884 | + |
1885 | + @match(r'^weather\s+(?:(?:for|at|in)\s+)?(.+)$') |
1886 | + def weather(self, event, place): |
1887 | + try: |
1888 | + values = self.remote_weather(place) |
1889 | + event.addresponse(u'In %(place)s at %(time)s: %(temp)s; Humidity: %(humidity)s; Wind: %(wind)s; Conditions: %(conditions)s; Sunrise/set: %(sunrise)s/%(sunset)s; Moonrise/set: %(moonrise)s/%(moonset)s', values) |
1890 | + except Weather.TooManyPlacesException, e: |
1891 | + event.addresponse(u'Too many places match %(place)s: %(exception)s', { |
1892 | + 'place': place, |
1893 | + 'exception': human_join(e.args[0], separator=u';'), |
1894 | + }) |
1895 | + except Weather.WeatherException, e: |
1896 | + event.addresponse(unicode(e)) |
1897 | + |
1898 | + @match(r'^forecast\s+(?:for\s+)?(.+)$') |
1899 | + def forecast(self, event, place): |
1900 | + try: |
1901 | + event.addresponse(u', '.join(self.remote_forecast(place))) |
1902 | + except Weather.TooManyPlacesException, e: |
1903 | + event.addresponse(u'Too many places match %(place)s: %(exception)s', { |
1904 | + 'place': place, |
1905 | + 'exception': human_join(e.args[0], separator=u';'), |
1906 | + }) |
1907 | + except Weather.WeatherException, e: |
1908 | + event.addresponse(unicode(e)) |
1909 | + |
1910 | +class TimezoneException(Exception): |
1911 | + pass |
1912 | + |
1913 | +MONTH_SHORT = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec') |
1914 | +MONTH_LONG = ('January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December') |
1915 | +OTHER_STUFF = ('am', 'pm', 'st', 'nd', 'rd', 'th') |
1916 | + |
1917 | +CUSTOM_ZONES = { |
1918 | + 'PST': 'US/Pacific', |
1919 | + 'MST': 'US/Mountain', |
1920 | + 'CST': 'US/Central', |
1921 | + 'EST': 'US/Eastern', |
1922 | +} |
1923 | + |
1924 | +help['timezone'] = "Converts times between timezones." |
1925 | +class TimeZone(Processor): |
1926 | + u"""when is <time> <place|timezone> in <place|timezone> |
1927 | + time in <place|timezone>""" |
1928 | + feature = 'timezone' |
1929 | + |
1930 | + zoneinfo = Option('zoneinfo', 'Timezone info directory', '/usr/share/zoneinfo') |
1931 | + custom_zones = DictOption('timezones', 'Custom timezone names', CUSTOM_ZONES) |
1932 | + |
1933 | + countries = {} |
1934 | + timezones = {} |
1935 | + lowerzones = {} |
1936 | + |
1937 | + def setup(self): |
1938 | + iso3166 = join(self.zoneinfo, 'iso3166.tab') |
1939 | + if exists(iso3166): |
1940 | + self.countries = {} |
1941 | + for line in open(iso3166).readlines(): |
1942 | + if not line.startswith('#'): |
1943 | + code, name = line.strip().split('\t') |
1944 | + self.countries[code] = name |
1945 | + |
1946 | + zones = join(self.zoneinfo, 'zone.tab') |
1947 | + if exists(zones): |
1948 | + self.timezones = defaultdict(list) |
1949 | + for line in open(zones).readlines(): |
1950 | + if not line.startswith('#'): |
1951 | + code, coordinates, zone = line.strip().split('\t', 2) |
1952 | + if '\t' in zone: |
1953 | + zone, comment = zone.split('\t') |
1954 | + self.timezones[code].append(zone) |
1955 | + |
1956 | + lowerzones = {} |
1957 | + for path, directories, filenames in walk(self.zoneinfo): |
1958 | + if path.replace(self.zoneinfo, '').lstrip('/').split('/')[0] not in ('posix', 'right'): |
1959 | + for filename in filenames: |
1960 | + name = join(path, filename).replace(self.zoneinfo, '').lstrip('/') |
1961 | + self.lowerzones[name.lower().replace('etc/', '')] = name |
1962 | + |
1963 | + def _find_timezone(self, string): |
1964 | + for name, zonename in self.custom_zones.items(): |
1965 | + if string.lower() == name.lower(): |
1966 | + return gettz(zonename) |
1967 | + |
1968 | + zone = gettz(string) |
1969 | + if zone: |
1970 | + return zone |
1971 | + |
1972 | + zone = gettz(string.upper()) |
1973 | + if zone: |
1974 | + return zone |
1975 | + |
1976 | + if string.lower() in self.lowerzones: |
1977 | + return gettz(self.lowerzones[string.lower()]) |
1978 | + |
1979 | + ccode = None |
1980 | + for code, name in self.countries.items(): |
1981 | + if name.lower() == string.lower(): |
1982 | + ccode = code |
1983 | + if not ccode: |
1984 | + if string.replace('.', '').upper() in self.timezones: |
1985 | + ccode = string.replace('.', '').upper() |
1986 | + |
1987 | + if ccode: |
1988 | + if len(self.timezones[ccode]) == 1: |
1989 | + return gettz(self.timezones[ccode][0]) |
1990 | + else: |
1991 | + raise TimezoneException(u'%s has multiple timezones: %s' % (self.countries[ccode], human_join(self.timezones[ccode]))) |
1992 | + |
1993 | + possibles = [] |
1994 | + for zones in self.timezones.values(): |
1995 | + for name in zones: |
1996 | + if string.replace(' ', '_').lower() in [part.lower() for part in name.split('/')]: |
1997 | + possibles.append(name) |
1998 | + |
1999 | + if len(possibles) == 1: |
2000 | + return gettz(possibles[0]) |
2001 | + elif len(possibles) > 1: |
2002 | + raise TimezoneException(u'Multiple timezones found: %s' % (human_join(possibles))) |
2003 | + |
2004 | + zone = self._geonames_lookup(string) |
2005 | + if zone: |
2006 | + return zone |
2007 | + |
2008 | + raise TimezoneException(u"I don't know about the %s timezone" % (string,)) |
2009 | + |
2010 | + def _geonames_lookup(self, place): |
2011 | + search = json_webservice('http://ws.geonames.org/searchJSON', {'q': place, 'maxRows': 1}) |
2012 | + if search['totalResultsCount'] == 0: |
2013 | + return None |
2014 | + |
2015 | + city = search['geonames'][0] |
2016 | + timezone = json_webservice('http://ws.geonames.org/timezoneJSON', {'lat': city['lat'], 'lng': city['lng']}) |
2017 | + |
2018 | + if 'timezoneId' in timezone: |
2019 | + return gettz(timezone['timezoneId']) |
2020 | + |
2021 | + if 'rawOffset' in timezone: |
2022 | + offset = timezone['rawOffset'] |
2023 | + return tzoffset('UTC%s%s' % (offset>=0 and '+' or '', offset), offset*3600) |
2024 | + |
2025 | + @match(r'^when\s+is\s+((?:[0-9.:/hT -]|%s)+)(?:\s+in)?(?:\s+(.+))?\s+in\s+(.+)$' % '|'.join(MONTH_SHORT+MONTH_LONG+OTHER_STUFF)) |
2026 | + def convert(self, event, time, from_, to): |
2027 | + try: |
2028 | + source = time and parse(time) or datetime.now() |
2029 | + except ValueError: |
2030 | + event.addresponse(u"That's not a real time") |
2031 | + return |
2032 | + |
2033 | + try: |
2034 | + if from_: |
2035 | + from_zone = self._find_timezone(from_) |
2036 | + else: |
2037 | + from_zone = tzlocal() |
2038 | + |
2039 | + to_zone = self._find_timezone(to) |
2040 | + except TimezoneException, e: |
2041 | + event.addresponse(unicode(e)) |
2042 | + return |
2043 | + |
2044 | + source = source.replace(tzinfo=from_zone) |
2045 | + result = source.astimezone(to_zone) |
2046 | + |
2047 | + event.addresponse(time and u'%(source)s is %(destination)s' or 'It is %(destination)s', { |
2048 | + 'source': format_date(source, tolocaltime=False), |
2049 | + 'destination': format_date(result, tolocaltime=False), |
2050 | + }) |
2051 | + |
2052 | + @match(r"^(?:(?:what(?:'?s|\s+is)\s+the\s+)?time\s+in|what\s+time\s+is\s+it\s+in)\s+(.+)$") |
2053 | + def time(self, event, place): |
2054 | + self.convert(event, None, None, place) |
2055 | + |
2056 | +# vi: set et sta sw=4 ts=4: |
2057 | |
2058 | === modified file 'ibid/plugins/google.py' |
2059 | --- ibid/plugins/google.py 2010-01-01 12:16:12 +0000 |
2060 | +++ ibid/plugins/google.py 2010-01-18 18:54:16 +0000 |
2061 | @@ -1,18 +1,16 @@ |
2062 | -import codecs |
2063 | from httplib import BadStatusLine |
2064 | -import re |
2065 | from urllib import quote |
2066 | -from urllib2 import build_opener, urlopen, HTTPCookieProcessor, Request |
2067 | +from urllib2 import urlopen, Request |
2068 | |
2069 | from BeautifulSoup import BeautifulSoup |
2070 | |
2071 | from ibid.plugins import Processor, match |
2072 | -from ibid.config import Option, IntOption |
2073 | -from ibid.utils import decode_htmlentities, json_webservice, cacheable_download |
2074 | -from ibid.utils import human_join |
2075 | -from ibid.utils.html import get_html_parse_tree |
2076 | - |
2077 | -help = {'google': u'Retrieves results from Google and Google Calculator.'} |
2078 | +from ibid.config import Option |
2079 | +from ibid.utils import decode_htmlentities, json_webservice |
2080 | + |
2081 | +help = {} |
2082 | + |
2083 | +help['google'] = u'Retrieves results from Google and Google Calculator.' |
2084 | |
2085 | default_user_agent = 'Mozilla/5.0' |
2086 | default_referer = "http://ibid.omnia.za.net/" |
2087 | @@ -125,169 +123,4 @@ |
2088 | else: |
2089 | event.addresponse(u'Are you making up words again?') |
2090 | |
2091 | -class UnknownLanguageException (Exception): pass |
2092 | -class TranslationException (Exception): pass |
2093 | - |
2094 | -help['translate'] = u'''Translates a phrase using Google Translate.''' |
2095 | -class Translate(Processor): |
2096 | - u"""translate <phrase> [from <language>] [to <language>] |
2097 | - translation chain <phrase> [from <language>] [to <language>]""" |
2098 | - |
2099 | - feature = 'translate' |
2100 | - |
2101 | - api_key = Option('api_key', 'Your Google API Key (optional)', None) |
2102 | - referer = Option('referer', 'The referer string to use (API searches)', default_referer) |
2103 | - dest_lang = Option('dest_lang', 'Destination language when none is specified', 'english') |
2104 | - |
2105 | - chain_length = IntOption('chain_length', 'Maximum length of translation chains', 10) |
2106 | - |
2107 | - lang_names = {'afrikaans':'af', 'albanian':'sq', 'arabic':'ar', |
2108 | - 'belarusian':'be', 'bulgarian':'bg', 'catalan':'ca', |
2109 | - 'chinese':'zh', 'chinese simplified':'zh-cn', |
2110 | - 'chinese traditional':'zh-tw', 'croatian':'hr', 'czech':'cs', |
2111 | - 'danish':'da', 'dutch':'nl', 'english':'en', 'estonian':'et', |
2112 | - 'filipino':'tl', 'finnish':'fi', 'french':'fr', |
2113 | - 'galacian':'gl', 'german':'de', 'greek':'el', 'hebrew':'iw', |
2114 | - 'hindi':'hi', 'hungarian':'hu', 'icelandic':'is', |
2115 | - 'indonesian':'id', 'irish':'ga', 'italian':'it', |
2116 | - 'japanese':'ja', 'korean': 'ko', 'latvian':'lv', |
2117 | - 'lithuanian':'lt', 'macedonian':'mk', 'malay':'ms', |
2118 | - 'maltese':'mt', 'norwegian':'no', 'persian':'fa', |
2119 | - 'polish':'pl', 'portuguese':'pt', 'romanian':'ro', |
2120 | - 'russian': 'ru', 'serbian':'sr', 'slovak':'sk', |
2121 | - 'slovenian':'sl', 'spanish':'es', 'swahili':'sw', |
2122 | - 'swedish':'sv', 'thai':'th', 'turkish':'tr', 'ukrainian':'uk', |
2123 | - 'uzbek': 'uz', 'vietnamese':'vi', 'welsh':'cy', |
2124 | - 'yiddish':'yi'} |
2125 | - |
2126 | - alt_lang_names = {'simplified':'zh-CN', 'simplified chinese':'zh-CN', |
2127 | - 'traditional':'zh-TW', 'traditional chinese':'zh-TW', |
2128 | - 'bokmal':'no', 'norwegian bokmal':'no', |
2129 | - u'bokm\N{LATIN SMALL LETTER A WITH RING ABOVE}l':'no', |
2130 | - u'norwegian bokm\N{LATIN SMALL LETTER A WITH RING ABOVE}l': |
2131 | - 'no', |
2132 | - 'farsi':'fa'} |
2133 | - |
2134 | - LANG_REGEX = '|'.join(lang_names.keys() + lang_names.values() + |
2135 | - alt_lang_names.keys()) |
2136 | - |
2137 | - @match(r'^(?:translation\s*)?languages$') |
2138 | - def languages (self, event): |
2139 | - event.addresponse(human_join(sorted(self.lang_names.keys()))) |
2140 | - |
2141 | - @match(r'^translate\s+(.*?)(?:\s+from\s+(' + LANG_REGEX + r'))?' |
2142 | - r'(?:\s+(?:in)?to\s+(' + LANG_REGEX + r'))?$') |
2143 | - def translate (self, event, text, src_lang, dest_lang): |
2144 | - dest_lang = self.language_code(dest_lang or self.dest_lang) |
2145 | - src_lang = self.language_code(src_lang or '') |
2146 | - |
2147 | - try: |
2148 | - translated = self._translate(event, text, src_lang, dest_lang)[0] |
2149 | - event.addresponse(translated) |
2150 | - except TranslationException, e: |
2151 | - event.addresponse(u"I couldn't translate that: %s.", unicode(e)) |
2152 | - |
2153 | - @match(r'^translation[-\s]*(?:chain|party)\s+(.*?)' |
2154 | - r'(?:\s+from\s+(' + LANG_REGEX + r'))?' |
2155 | - r'(?:\s+(?:in)?to\s+(' + LANG_REGEX + r'))?$') |
2156 | - def translation_chain (self, event, phrase, src_lang, dest_lang): |
2157 | - if self.chain_length < 1: |
2158 | - event.addresponse(u"I'm not allowed to play translation games.") |
2159 | - try: |
2160 | - dest_lang = self.language_code(dest_lang or self.dest_lang) |
2161 | - src_lang = self.language_code(src_lang or '') |
2162 | - |
2163 | - chain = [phrase] |
2164 | - for i in range(self.chain_length): |
2165 | - phrase, src_lang = self._translate(event, phrase, |
2166 | - src_lang, dest_lang) |
2167 | - src_lang, dest_lang = dest_lang, src_lang |
2168 | - chain.append(phrase) |
2169 | - if phrase in chain[:-1]: |
2170 | - break |
2171 | - |
2172 | - event.addresponse(u'\n'.join(chain[1:]), conflate=False) |
2173 | - |
2174 | - except TranslationException, e: |
2175 | - event.addresponse(u"I couldn't translate that: %s.", unicode(e)) |
2176 | - |
2177 | - def _translate (self, event, phrase, src_lang, dest_lang): |
2178 | - params = { |
2179 | - 'v': '1.0', |
2180 | - 'q': phrase, |
2181 | - 'langpair': src_lang + '|' + dest_lang, |
2182 | - } |
2183 | - if self.api_key: |
2184 | - params['key'] = self.api_key |
2185 | - |
2186 | - headers = {'referer': self.referer} |
2187 | - |
2188 | - response = json_webservice( |
2189 | - 'http://ajax.googleapis.com/ajax/services/language/translate', |
2190 | - params, headers) |
2191 | - |
2192 | - if response['responseStatus'] == 200: |
2193 | - translated = unicode(decode_htmlentities( |
2194 | - response['responseData']['translatedText'])) |
2195 | - return (translated, src_lang or |
2196 | - response['responseData']['detectedSourceLanguage']) |
2197 | - else: |
2198 | - errors = { |
2199 | - 'invalid translation language pair': |
2200 | - u"I don't know that language", |
2201 | - 'invalid text': |
2202 | - u"there's not much to go on", |
2203 | - 'could not reliably detect source language': |
2204 | - u"I'm not sure what language that was", |
2205 | - } |
2206 | - |
2207 | - msg = errors.get(response['responseDetails'], |
2208 | - response['responseDetails']) |
2209 | - |
2210 | - raise TranslationException(msg) |
2211 | - |
2212 | - def language_code (self, name): |
2213 | - """Convert a name to a language code.""" |
2214 | - |
2215 | - name = name.lower() |
2216 | - |
2217 | - if name == '': |
2218 | - return name |
2219 | - |
2220 | - try: |
2221 | - return self.lang_names.get(name) or self.alt_lang_names[name] |
2222 | - except KeyError: |
2223 | - m = re.match('^([a-z]{2,3})(?:-[a-z]{2})?$', name) |
2224 | - if m and m.group(1) in self.lang_names.values(): |
2225 | - return name |
2226 | - else: |
2227 | - raise UnknownLanguageException |
2228 | - |
2229 | -# This Plugin uses code from youtube-dl |
2230 | -# Copyright (c) 2006-2008 Ricardo Garcia Gonzalez |
2231 | -# Released under MIT Licence |
2232 | - |
2233 | -help['youtube'] = u'Determine the title and a download URL for a Youtube Video' |
2234 | -class Youtube(Processor): |
2235 | - u'<Youtube URL>' |
2236 | - |
2237 | - feature = 'youtube' |
2238 | - |
2239 | - @match(r'^(?:youtube(?:\.com)?\s+)?' |
2240 | - r'(?:http://)?(?:\w+\.)?youtube\.com/' |
2241 | - r'(?:v/|(?:watch(?:\.php)?)?\?(?:.+&)?v=)' |
2242 | - r'([0-9A-Za-z_-]+)(?(1)[&/].*)?$') |
2243 | - def youtube(self, event, id): |
2244 | - url = 'http://www.youtube.com/watch?v=' + id |
2245 | - opener = build_opener(HTTPCookieProcessor()) |
2246 | - opener.addheaders = [('User-Agent', default_user_agent)] |
2247 | - video_webpage = opener.open(url).read() |
2248 | - title = re.search(r'<title>\s*YouTube\s+-\s+([^<]*)</title>', |
2249 | - video_webpage, re.M | re.I | re.DOTALL).group(1).strip() |
2250 | - t = re.search(r', "t": "([^"]+)"', video_webpage).group(1) |
2251 | - event.addresponse(u'%(title)s: %(url)s', { |
2252 | - 'title': title, |
2253 | - 'url': 'http://www.youtube.com/get_video?video_id=%s&t=%s' % (id, t), |
2254 | - }) |
2255 | - |
2256 | # vi: set et sta sw=4 ts=4: |
2257 | |
2258 | === removed file 'ibid/plugins/http.py' |
2259 | --- ibid/plugins/http.py 2009-07-13 15:19:39 +0000 |
2260 | +++ ibid/plugins/http.py 1970-01-01 00:00:00 +0000 |
2261 | @@ -1,59 +0,0 @@ |
2262 | -from httplib import HTTPConnection, HTTPSConnection |
2263 | -from urllib import getproxies_environment |
2264 | -from urlparse import urlparse |
2265 | -import re |
2266 | - |
2267 | -from ibid.plugins import Processor, match |
2268 | -from ibid.config import IntOption |
2269 | - |
2270 | -help = {} |
2271 | - |
2272 | -title = re.compile(r'<title>(.*)<\/title>', re.I+re.S) |
2273 | - |
2274 | -help['get'] = u'Retrieves a URL and returns the HTTP status and optionally the HTML title.' |
2275 | -class HTTP(Processor): |
2276 | - u"""(get|head) <url>""" |
2277 | - feature = 'get' |
2278 | - |
2279 | - max_size = IntOption('max_size', 'Only request this many bytes', 500) |
2280 | - |
2281 | - @match(r'^(get|head)\s+(\S+\.\S+)$') |
2282 | - def handler(self, event, action, url): |
2283 | - if not url.lower().startswith("http://") and not url.lower().startswith("https://"): |
2284 | - url = "http://" + url |
2285 | - if url.count("/") < 3: |
2286 | - url += "/" |
2287 | - |
2288 | - action = action.upper() |
2289 | - |
2290 | - scheme, host = urlparse(url)[:2] |
2291 | - scheme = scheme.lower() |
2292 | - proxies = getproxies_environment() |
2293 | - if scheme in proxies: |
2294 | - scheme, host = urlparse(proxies[scheme])[:2] |
2295 | - scheme = scheme.lower() |
2296 | - |
2297 | - if scheme == "https": |
2298 | - conn = HTTPSConnection(host) |
2299 | - else: |
2300 | - conn = HTTPConnection(host) |
2301 | - |
2302 | - headers={} |
2303 | - if action == 'GET': |
2304 | - headers['Range'] = 'bytes=0-%s' % self.max_size |
2305 | - conn.request(action.upper(), url, headers=headers) |
2306 | - |
2307 | - response = conn.getresponse() |
2308 | - reply = u'%s %s' % (response.status, response.reason) |
2309 | - |
2310 | - data = response.read() |
2311 | - conn.close() |
2312 | - |
2313 | - if action == 'GET': |
2314 | - match = title.search(data) |
2315 | - if match: |
2316 | - reply += u' "%s"' % match.groups()[0].strip() |
2317 | - |
2318 | - event.addresponse(reply) |
2319 | - |
2320 | -# vi: set et sta sw=4 ts=4: |
2321 | |
2322 | === modified file 'ibid/plugins/identity.py' |
2323 | --- ibid/plugins/identity.py 2010-01-15 12:42:48 +0000 |
2324 | +++ ibid/plugins/identity.py 2010-01-18 18:54:16 +0000 |
2325 | @@ -4,10 +4,12 @@ |
2326 | |
2327 | import ibid |
2328 | from ibid.config import Option |
2329 | +from ibid.compat import any |
2330 | from ibid.db import eagerload, IntegrityError, and_, or_ |
2331 | -from ibid.db.models import Account, Identity, Attribute |
2332 | +from ibid.db.models import Account, Identity, Attribute, Credential, Permission |
2333 | from ibid.plugins import Processor, match, handler, auth_responses, authorise |
2334 | from ibid.utils import human_join |
2335 | +from ibid.auth import hash |
2336 | |
2337 | help = {} |
2338 | identify_cache = {} |
2339 | @@ -488,4 +490,154 @@ |
2340 | .filter_by(source=source, identity=id).first() |
2341 | return identity and identity.id |
2342 | |
2343 | +actions = {'revoke': 'Revoked', 'grant': 'Granted', 'remove': 'Removed'} |
2344 | + |
2345 | +help['auth'] = u'Adds and removes authentication credentials and permissions' |
2346 | +class AddAuth(Processor): |
2347 | + u"""authenticate <account> [on source] using <method> [<credential>]""" |
2348 | + feature = 'auth' |
2349 | + |
2350 | + @match(r'^authenticate\s+(.+?)(?:\s+on\s+(.+))?\s+using\s+(\S+)\s+(.+)$') |
2351 | + def handler(self, event, user, source, method, credential): |
2352 | + |
2353 | + if user.lower() == 'me': |
2354 | + if not event.account: |
2355 | + event.addresponse(u"I don't know who you are") |
2356 | + return |
2357 | + if not ibid.auth.authenticate(event): |
2358 | + event.complain = 'notauthed' |
2359 | + return |
2360 | + account = event.session.query(Account).get(event.account) |
2361 | + |
2362 | + else: |
2363 | + if not auth_responses(event, 'admin'): |
2364 | + return |
2365 | + account = event.session.query(Account).filter_by(username=user).first() |
2366 | + if not account: |
2367 | + event.addresponse(u"I don't know who %s is", user) |
2368 | + return |
2369 | + |
2370 | + if source: |
2371 | + if source not in ibid.sources: |
2372 | + event.addresponse(u"I am not connected to %s", source) |
2373 | + return |
2374 | + source = ibid.sources[source].name |
2375 | + |
2376 | + if method.lower() == 'password': |
2377 | + password = hash(credential) |
2378 | + event.message['clean'] = event.message['clean'][:-len(credential)] + password |
2379 | + event.message['raw'] = event.message['raw'][:event.message['raw'].rfind(credential)] \ |
2380 | + + password + event.message['raw'][event.message['raw'].rfind(credential)+len(credential):] |
2381 | + credential = password |
2382 | + |
2383 | + credential = Credential(method, credential, source, account.id) |
2384 | + event.session.save_or_update(credential) |
2385 | + event.session.commit() |
2386 | + log.info(u"Added %s credential %s for account %s (%s) on %s by account %s", |
2387 | + method, credential.credential, account.id, account.username, source, event.account) |
2388 | + |
2389 | + event.addresponse(True) |
2390 | + |
2391 | +permission_values = {'no': '-', 'yes': '+', 'auth': ''} |
2392 | +class Permissions(Processor): |
2393 | + u"""(grant|revoke|remove) <permission> (to|from|on) <username> [when authed] |
2394 | + permissions [for <username>] |
2395 | + list permissions""" |
2396 | + feature = 'auth' |
2397 | + |
2398 | + permission = u'admin' |
2399 | + |
2400 | + @match(r'^(grant|revoke|remove)\s+(.+?)(?:\s+permission)?\s+(?:to|from|on)\s+(.+?)(\s+(?:with|when|if)\s+(?:auth|authed|authenticated))?$') |
2401 | + @authorise() |
2402 | + def grant(self, event, action, name, username, auth): |
2403 | + |
2404 | + account = event.session.query(Account).filter_by(username=username).first() |
2405 | + if not account: |
2406 | + event.addresponse(u"I don't know who %s is", username) |
2407 | + return |
2408 | + |
2409 | + permission = event.session.query(Permission) \ |
2410 | + .filter_by(account_id=account.id, name=name).first() |
2411 | + if action.lower() == 'remove': |
2412 | + if permission: |
2413 | + event.session.delete(permission) |
2414 | + else: |
2415 | + event.addresponse(u"%s doesn't have that permission anyway", username) |
2416 | + return |
2417 | + |
2418 | + else: |
2419 | + if not permission: |
2420 | + permission = Permission(name) |
2421 | + account.permissions.append(permission) |
2422 | + |
2423 | + if action.lower() == 'revoke': |
2424 | + value = 'no' |
2425 | + elif auth: |
2426 | + value = 'auth' |
2427 | + else: |
2428 | + value = 'yes' |
2429 | + |
2430 | + if permission.value == value: |
2431 | + event.addresponse(u'%(permission)s permission for %(user)s is already %(value)s', { |
2432 | + 'permission': name, |
2433 | + 'user': username, |
2434 | + 'value': value, |
2435 | + }) |
2436 | + return |
2437 | + |
2438 | + permission.value = value |
2439 | + event.session.save_or_update(permission) |
2440 | + |
2441 | + event.session.commit() |
2442 | + ibid.auth.drop_caches() |
2443 | + log.info(u"%s %s permission for account %s (%s) by account %s", |
2444 | + actions[action.lower()], name, account.id, account.username, event.account) |
2445 | + |
2446 | + event.addresponse(True) |
2447 | + |
2448 | + @match(r'^permissions(?:\s+for\s+(\S+))?$') |
2449 | + def list(self, event, username): |
2450 | + if not username: |
2451 | + if not event.account: |
2452 | + event.addresponse(u"I don't know who you are") |
2453 | + return |
2454 | + account = event.session.query(Account).get(event.account) |
2455 | + else: |
2456 | + if not auth_responses(event, u'accounts'): |
2457 | + return |
2458 | + account = event.session.query(Account) \ |
2459 | + .filter_by(username=username).first() |
2460 | + if not account: |
2461 | + event.addresponse(u"I don't know who %s is", username) |
2462 | + return |
2463 | + |
2464 | + permissions = sorted(u'%s%s' % (permission_values[perm.value], perm.name) for perm in account.permissions) |
2465 | + event.addresponse(u'Permissions: %s', human_join(permissions) or u'none') |
2466 | + |
2467 | + @match(r'^list\s+permissions$') |
2468 | + def list_permissions(self, event): |
2469 | + permissions = [] |
2470 | + for processor in ibid.processors: |
2471 | + if hasattr(processor, 'permission') and getattr(processor, 'permission') not in permissions: |
2472 | + permissions.append(getattr(processor, 'permission')) |
2473 | + if hasattr(processor, 'permissions'): |
2474 | + for permission in getattr(processor, 'permissions'): |
2475 | + if permission not in permissions: |
2476 | + permissions.append(permission) |
2477 | + |
2478 | + event.addresponse(u'Permissions: %s', human_join(sorted(permissions)) or u'none') |
2479 | + |
2480 | +class Auth(Processor): |
2481 | + u"""auth <credential>""" |
2482 | + feature = 'auth' |
2483 | + |
2484 | + @match(r'^auth(?:\s+(.+))?$') |
2485 | + def handler(self, event, password): |
2486 | + result = ibid.auth.authenticate(event, password) |
2487 | + if result: |
2488 | + event.addresponse(u'You are authenticated') |
2489 | + else: |
2490 | + event.addresponse(u'Authentication failed') |
2491 | + |
2492 | + |
2493 | # vi: set et sta sw=4 ts=4: |
2494 | |
2495 | === removed file 'ibid/plugins/imdb.py' |
2496 | --- ibid/plugins/imdb.py 2009-07-27 14:00:45 +0000 |
2497 | +++ ibid/plugins/imdb.py 1970-01-01 00:00:00 +0000 |
2498 | @@ -1,144 +0,0 @@ |
2499 | -# Uses the IMDbPY package in http mode against imdb.com |
2500 | -# This isn't strictly legal: http://www.imdb.com/help/show_leaf?usedatasoftware |
2501 | -# |
2502 | -# Note that it will return porn movies by default. |
2503 | - |
2504 | -from .. imdb import IMDb, IMDbDataAccessError, IMDbError |
2505 | - |
2506 | -from ibid.plugins import Processor, match |
2507 | -from ibid.config import Option, BoolOption |
2508 | -from ibid.utils import human_join |
2509 | - |
2510 | -help = {'imdb': u'Looks up movies on IMDB.com.'} |
2511 | - |
2512 | -class IMDB(Processor): |
2513 | - u"imdb [search] [character|company|episode|movie|person] <terms> [#<index>]" |
2514 | - feature = 'imdb' |
2515 | - |
2516 | - access_system = Option("accesssystem", "Method of querying IMDB", "http") |
2517 | - adult_search = BoolOption("adultsearch", "Include adult films in search results", True) |
2518 | - |
2519 | - name_keys = { |
2520 | - "character": "long imdb name", |
2521 | - "company": "long imdb name", |
2522 | - "episode": "long imdb title", |
2523 | - "movie": "long imdb title", |
2524 | - "person": "name", |
2525 | - } |
2526 | - |
2527 | - def setup(self): |
2528 | - self.imdb = IMDb(accessSystem=self.access_system, adultSearch=int(self.adult_search)) |
2529 | - |
2530 | - @match(r'^imdb(?:\s+search)?(?:\s+(character|company|episode|movie|person))?\s+(.+?)(?:\s+#(\d+))?$') |
2531 | - def search(self, event, search_type, terms, index): |
2532 | - if search_type is None: |
2533 | - search_type = "movie" |
2534 | - if index is not None: |
2535 | - index = int(index) - 1 |
2536 | - |
2537 | - result = None |
2538 | - try: |
2539 | - if terms.isdigit(): |
2540 | - result = getattr(self.imdb, "get_" + search_type)(terms) |
2541 | - else: |
2542 | - results = getattr(self.imdb, "search_" + search_type)(terms) |
2543 | - |
2544 | - if len(results) == 1: |
2545 | - index = 0 |
2546 | - |
2547 | - if index is not None: |
2548 | - result = results[index] |
2549 | - self.imdb.update(result) |
2550 | - |
2551 | - except IMDbDataAccessError, e: |
2552 | - event.addresponse(u"IMDb doesn't like me today. It said '%s'", e[0]["errmsg"]) |
2553 | - raise |
2554 | - |
2555 | - except IMDbError, e: |
2556 | - event.addresponse(u'IMDb must be having a bad day (or you are asking it silly things)') |
2557 | - raise |
2558 | - |
2559 | - if result is not None: |
2560 | - event.addresponse(u'Found %s', getattr(self, 'display_' + search_type)(result)) |
2561 | - return |
2562 | - |
2563 | - if len(results) == 0: |
2564 | - event.addresponse(u"Sorry, couldn't find that") |
2565 | - else: |
2566 | - results = [x[self.name_keys[search_type]] for x in results] |
2567 | - results = enumerate(results) |
2568 | - results = [u"%i: %s" % (x[0] + 1, x[1]) for x in results] |
2569 | - event.addresponse(u'Found %(greaterthan)s%(num)i matches: %(results)s', { |
2570 | - 'greaterthan': (u'', u'>')[len(results) == 20], |
2571 | - 'num': len(results), |
2572 | - 'results': u', '.join(results), |
2573 | - }) |
2574 | - |
2575 | - def display_character(self, character): |
2576 | - desc = u"%s: %s." % (character.characterID, character["long imdb name"]) |
2577 | - filmography = character.get("filmography", ()) |
2578 | - if len(filmography): |
2579 | - more = (u"", u" etc")[len(filmography) > 5] |
2580 | - desc += u" Appeared in %s%s." % (human_join(x["long imdb title"] for x in filmography[:5]), more) |
2581 | - if character.has_key("introduction"): |
2582 | - desc += u" Bio: %s" % character["introduction"] |
2583 | - return desc |
2584 | - |
2585 | - def display_company(self, company): |
2586 | - desc = "%s: %s" % (company.companyID, company["long imdb name"]) |
2587 | - for key, title in ( |
2588 | - (u"production companies", u"Produced"), |
2589 | - (u"distributors", u"Distributed"), |
2590 | - (u"miscellaneous companies", u"Was involved in")): |
2591 | - if len(company.get(key, ())) > 0: |
2592 | - more = (u"", u" etc.")[len(company[key]) > 3] |
2593 | - desc += u" %s %s%s" % (title, human_join(x["long imdb title"] for x in company[key][:3]), more) |
2594 | - return desc |
2595 | - |
2596 | - def display_episode(self, episode): |
2597 | - desc = u"%s: %s s%02ie%02i %s(%s)." % ( |
2598 | - episode.movieID, episode["series title"], episode["season"], |
2599 | - episode["episode"], episode["title"], episode["year"]) |
2600 | - if len(episode.get("director", ())) > 0: |
2601 | - desc += u" Dir: %s." % (human_join(x["name"] for x in episode["director"])) |
2602 | - if len(episode.get("cast", ())) > 0: |
2603 | - desc += u" Starring: %s." % (human_join(x["name"] for x in episode["cast"][:3])) |
2604 | - if episode.has_key("rating"): |
2605 | - desc += u" Rated: %.1f " % episode["rating"] |
2606 | - desc += human_join(episode.get("genres", ())) |
2607 | - desc += u" Plot: %s" % episode.get("plot outline", u"Unknown") |
2608 | - return desc |
2609 | - |
2610 | - def display_movie(self, movie): |
2611 | - desc = u"%s: %s." % (movie.movieID, movie["long imdb title"]) |
2612 | - if len(movie.get("director", ())) > 0: |
2613 | - desc += u" Dir: %s." % (human_join(x["name"] for x in movie["director"])) |
2614 | - if len(movie.get("cast", ())) > 0: |
2615 | - desc += u" Starring: %s." % (human_join(x["name"] for x in movie["cast"][:3])) |
2616 | - if movie.has_key("rating"): |
2617 | - desc += u" Rated: %.1f " % movie["rating"] |
2618 | - desc += human_join(movie.get("genres", ())) |
2619 | - desc += u" Plot: %s" % movie.get("plot outline", u"Unknown") |
2620 | - return desc |
2621 | - |
2622 | - def display_person(self, person): |
2623 | - desc = u"%s: %s. %s." % (person.personID, person["name"], |
2624 | - human_join(role.title() for role in ( |
2625 | - u"actor", u"animation department", u"art department", |
2626 | - u"art director", u"assistant director", u"camera department", |
2627 | - u"casting department", u"casting director", u"cinematographer", |
2628 | - u"composer", u"costume department", u"costume designer", |
2629 | - u"director", u"editorial department", u"editor", |
2630 | - u"make up department", u"music department", u"producer", |
2631 | - u"production designer", u"set decorator", u"sound department", |
2632 | - u"speccial effects department", u"stunts", u"transport department", |
2633 | - u"visual effects department", u"writer", u"miscellaneous crew" |
2634 | - ) if person.has_key(role))) |
2635 | - if person.has_key("mini biography"): |
2636 | - desc += u" " + u" ".join(person["mini biography"]) |
2637 | - else: |
2638 | - if person.has_key("birth name") or person.has_key("birth date"): |
2639 | - desc += u" Born %s." % u", ".join(person[attr] for attr in ("birth name", "birth date") if person.has_key(attr)) |
2640 | - return desc |
2641 | - |
2642 | -# vi: set et sta sw=4 ts=4: |
2643 | |
2644 | === removed file 'ibid/plugins/info.py' |
2645 | --- ibid/plugins/info.py 2009-12-30 14:08:03 +0000 |
2646 | +++ ibid/plugins/info.py 1970-01-01 00:00:00 +0000 |
2647 | @@ -1,110 +0,0 @@ |
2648 | -from subprocess import Popen, PIPE |
2649 | -import os |
2650 | -from unicodedata import normalize |
2651 | - |
2652 | -from nickometer import nickometer |
2653 | - |
2654 | -from ibid.plugins import Processor, match, RPC |
2655 | -from ibid.config import Option |
2656 | -from ibid.utils import file_in_path, unicode_output |
2657 | - |
2658 | -help = {} |
2659 | - |
2660 | -help['fortune'] = u'Returns a random fortune.' |
2661 | -class Fortune(Processor, RPC): |
2662 | - u"""fortune""" |
2663 | - feature = 'fortune' |
2664 | - |
2665 | - fortune = Option('fortune', 'Path of the fortune executable', 'fortune') |
2666 | - |
2667 | - def __init__(self, name): |
2668 | - super(Fortune, self).__init__(name) |
2669 | - RPC.__init__(self) |
2670 | - |
2671 | - def setup(self): |
2672 | - if not file_in_path(self.fortune): |
2673 | - raise Exception("Cannot locate fortune executable") |
2674 | - |
2675 | - @match(r'^fortune$') |
2676 | - def handler(self, event): |
2677 | - fortune = self.remote_fortune() |
2678 | - if fortune: |
2679 | - event.addresponse(fortune) |
2680 | - else: |
2681 | - event.addresponse(u"Couldn't execute fortune") |
2682 | - |
2683 | - def remote_fortune(self): |
2684 | - fortune = Popen(self.fortune, stdout=PIPE, stderr=PIPE) |
2685 | - output, error = fortune.communicate() |
2686 | - code = fortune.wait() |
2687 | - |
2688 | - output = unicode_output(output.strip(), 'replace') |
2689 | - |
2690 | - if code == 0: |
2691 | - return output |
2692 | - else: |
2693 | - return None |
2694 | - |
2695 | -help['nickometer'] = u'Calculates how lame a nick is.' |
2696 | -class Nickometer(Processor): |
2697 | - u"""nickometer [<nick>] [with reasons]""" |
2698 | - feature = 'nickometer' |
2699 | - |
2700 | - @match(r'^(?:nick|lame)-?o-?meter(?:(?:\s+for)?\s+(.+?))?(\s+with\s+reasons)?$') |
2701 | - def handle_nickometer(self, event, nick, wreasons): |
2702 | - nick = nick or event.sender['nick'] |
2703 | - if u'\ufffd' in nick: |
2704 | - score, reasons = 100., ((u'Not UTF-8 clean', u'infinite'),) |
2705 | - else: |
2706 | - score, reasons = nickometer(normalize('NFKD', nick).encode('ascii', 'ignore')) |
2707 | - |
2708 | - event.addresponse(u'%(nick)s is %(score)s%% lame', { |
2709 | - 'nick': nick, |
2710 | - 'score': score, |
2711 | - }) |
2712 | - if wreasons: |
2713 | - if not reasons: |
2714 | - reasons = ((u'A good, traditional nick', 0),) |
2715 | - event.addresponse(u'Because: %s', u', '.join(['%s (%s)' % reason for reason in reasons])) |
2716 | - |
2717 | -help['man'] = u'Retrieves information from manpages.' |
2718 | -class Man(Processor): |
2719 | - u"""man [<section>] <page>""" |
2720 | - feature = 'man' |
2721 | - |
2722 | - man = Option('man', 'Path of the man executable', 'man') |
2723 | - |
2724 | - def setup(self): |
2725 | - if not file_in_path(self.man): |
2726 | - raise Exception("Cannot locate man executable") |
2727 | - |
2728 | - @match(r'^man\s+(?:(\d)\s+)?(\S+)$') |
2729 | - def handle_man(self, event, section, page): |
2730 | - command = [self.man, page] |
2731 | - if section: |
2732 | - command.insert(1, section) |
2733 | - |
2734 | - if page.strip().startswith("-"): |
2735 | - event.addresponse(False) |
2736 | - return |
2737 | - |
2738 | - env = os.environ.copy() |
2739 | - env["COLUMNS"] = "500" |
2740 | - |
2741 | - man = Popen(command, stdout=PIPE, stderr=PIPE, env=env) |
2742 | - output, error = man.communicate() |
2743 | - code = man.wait() |
2744 | - |
2745 | - if code != 0: |
2746 | - event.addresponse(u'Manpage not found') |
2747 | - else: |
2748 | - output = unicode_output(output.strip(), errors="replace") |
2749 | - output = output.splitlines() |
2750 | - index = output.index('NAME') |
2751 | - if index: |
2752 | - event.addresponse(output[index+1].strip()) |
2753 | - index = output.index('SYNOPSIS') |
2754 | - if index: |
2755 | - event.addresponse(output[index+1].strip()) |
2756 | - |
2757 | -# vi: set et sta sw=4 ts=4: |
2758 | |
2759 | === renamed file 'ibid/plugins/dict.py' => 'ibid/plugins/languages.py' |
2760 | --- ibid/plugins/dict.py 2009-10-25 16:29:12 +0000 |
2761 | +++ ibid/plugins/languages.py 2010-01-18 18:54:16 +0000 |
2762 | @@ -1,13 +1,15 @@ |
2763 | from random import choice |
2764 | +import re |
2765 | |
2766 | from dictclient import Connection |
2767 | |
2768 | from ibid.plugins import Processor, match |
2769 | from ibid.config import Option, IntOption |
2770 | -from ibid.utils import human_join |
2771 | - |
2772 | -help = {'dict': u'Defines words and checks spellings.'} |
2773 | - |
2774 | +from ibid.utils import decode_htmlentities, json_webservice, human_join |
2775 | + |
2776 | +help = {} |
2777 | + |
2778 | +help['dict'] = u'Defines words and checks spellings.' |
2779 | class Dict(Processor): |
2780 | u"""spell <word> [using <strategy>] |
2781 | define <word> [using <dictionary>] |
2782 | @@ -116,4 +118,145 @@ |
2783 | else: |
2784 | event.addresponse(u"I don't have that strategy") |
2785 | |
2786 | +default_user_agent = 'Mozilla/5.0' |
2787 | +default_referer = "http://ibid.omnia.za.net/" |
2788 | + |
2789 | +class UnknownLanguageException (Exception): pass |
2790 | +class TranslationException (Exception): pass |
2791 | + |
2792 | +help['translate'] = u'''Translates a phrase using Google Translate.''' |
2793 | +class Translate(Processor): |
2794 | + u"""translate <phrase> [from <language>] [to <language>] |
2795 | + translation chain <phrase> [from <language>] [to <language>]""" |
2796 | + |
2797 | + feature = 'translate' |
2798 | + |
2799 | + api_key = Option('api_key', 'Your Google API Key (optional)', None) |
2800 | + referer = Option('referer', 'The referer string to use (API searches)', default_referer) |
2801 | + dest_lang = Option('dest_lang', 'Destination language when none is specified', 'english') |
2802 | + |
2803 | + chain_length = IntOption('chain_length', 'Maximum length of translation chains', 10) |
2804 | + |
2805 | + lang_names = {'afrikaans':'af', 'albanian':'sq', 'arabic':'ar', |
2806 | + 'belarusian':'be', 'bulgarian':'bg', 'catalan':'ca', |
2807 | + 'chinese':'zh', 'chinese simplified':'zh-cn', |
2808 | + 'chinese traditional':'zh-tw', 'croatian':'hr', 'czech':'cs', |
2809 | + 'danish':'da', 'dutch':'nl', 'english':'en', 'estonian':'et', |
2810 | + 'filipino':'tl', 'finnish':'fi', 'french':'fr', |
2811 | + 'galacian':'gl', 'german':'de', 'greek':'el', 'hebrew':'iw', |
2812 | + 'hindi':'hi', 'hungarian':'hu', 'icelandic':'is', |
2813 | + 'indonesian':'id', 'irish':'ga', 'italian':'it', |
2814 | + 'japanese':'ja', 'korean': 'ko', 'latvian':'lv', |
2815 | + 'lithuanian':'lt', 'macedonian':'mk', 'malay':'ms', |
2816 | + 'maltese':'mt', 'norwegian':'no', 'persian':'fa', |
2817 | + 'polish':'pl', 'portuguese':'pt', 'romanian':'ro', |
2818 | + 'russian': 'ru', 'serbian':'sr', 'slovak':'sk', |
2819 | + 'slovenian':'sl', 'spanish':'es', 'swahili':'sw', |
2820 | + 'swedish':'sv', 'thai':'th', 'turkish':'tr', 'ukrainian':'uk', |
2821 | + 'uzbek': 'uz', 'vietnamese':'vi', 'welsh':'cy', |
2822 | + 'yiddish':'yi'} |
2823 | + |
2824 | + alt_lang_names = {'simplified':'zh-CN', 'simplified chinese':'zh-CN', |
2825 | + 'traditional':'zh-TW', 'traditional chinese':'zh-TW', |
2826 | + 'bokmal':'no', 'norwegian bokmal':'no', |
2827 | + u'bokm\N{LATIN SMALL LETTER A WITH RING ABOVE}l':'no', |
2828 | + u'norwegian bokm\N{LATIN SMALL LETTER A WITH RING ABOVE}l': |
2829 | + 'no', |
2830 | + 'farsi':'fa'} |
2831 | + |
2832 | + LANG_REGEX = '|'.join(lang_names.keys() + lang_names.values() + |
2833 | + alt_lang_names.keys()) |
2834 | + |
2835 | + @match(r'^(?:translation\s*)?languages$') |
2836 | + def languages (self, event): |
2837 | + event.addresponse(human_join(sorted(self.lang_names.keys()))) |
2838 | + |
2839 | + @match(r'^translate\s+(.*?)(?:\s+from\s+(' + LANG_REGEX + r'))?' |
2840 | + r'(?:\s+(?:in)?to\s+(' + LANG_REGEX + r'))?$') |
2841 | + def translate (self, event, text, src_lang, dest_lang): |
2842 | + dest_lang = self.language_code(dest_lang or self.dest_lang) |
2843 | + src_lang = self.language_code(src_lang or '') |
2844 | + |
2845 | + try: |
2846 | + translated = self._translate(event, text, src_lang, dest_lang)[0] |
2847 | + event.addresponse(translated) |
2848 | + except TranslationException, e: |
2849 | + event.addresponse(u"I couldn't translate that: %s.", unicode(e)) |
2850 | + |
2851 | + @match(r'^translation[-\s]*(?:chain|party)\s+(.*?)' |
2852 | + r'(?:\s+from\s+(' + LANG_REGEX + r'))?' |
2853 | + r'(?:\s+(?:in)?to\s+(' + LANG_REGEX + r'))?$') |
2854 | + def translation_chain (self, event, phrase, src_lang, dest_lang): |
2855 | + if self.chain_length < 1: |
2856 | + event.addresponse(u"I'm not allowed to play translation games.") |
2857 | + try: |
2858 | + dest_lang = self.language_code(dest_lang or self.dest_lang) |
2859 | + src_lang = self.language_code(src_lang or '') |
2860 | + |
2861 | + chain = [phrase] |
2862 | + for i in range(self.chain_length): |
2863 | + phrase, src_lang = self._translate(event, phrase, |
2864 | + src_lang, dest_lang) |
2865 | + src_lang, dest_lang = dest_lang, src_lang |
2866 | + chain.append(phrase) |
2867 | + if phrase in chain[:-1]: |
2868 | + break |
2869 | + |
2870 | + event.addresponse(u'\n'.join(chain[1:]), conflate=False) |
2871 | + |
2872 | + except TranslationException, e: |
2873 | + event.addresponse(u"I couldn't translate that: %s.", unicode(e)) |
2874 | + |
2875 | + def _translate (self, event, phrase, src_lang, dest_lang): |
2876 | + params = { |
2877 | + 'v': '1.0', |
2878 | + 'q': phrase, |
2879 | + 'langpair': src_lang + '|' + dest_lang, |
2880 | + } |
2881 | + if self.api_key: |
2882 | + params['key'] = self.api_key |
2883 | + |
2884 | + headers = {'referer': self.referer} |
2885 | + |
2886 | + response = json_webservice( |
2887 | + 'http://ajax.googleapis.com/ajax/services/language/translate', |
2888 | + params, headers) |
2889 | + |
2890 | + if response['responseStatus'] == 200: |
2891 | + translated = unicode(decode_htmlentities( |
2892 | + response['responseData']['translatedText'])) |
2893 | + return (translated, src_lang or |
2894 | + response['responseData']['detectedSourceLanguage']) |
2895 | + else: |
2896 | + errors = { |
2897 | + 'invalid translation language pair': |
2898 | + u"I don't know that language", |
2899 | + 'invalid text': |
2900 | + u"there's not much to go on", |
2901 | + 'could not reliably detect source language': |
2902 | + u"I'm not sure what language that was", |
2903 | + } |
2904 | + |
2905 | + msg = errors.get(response['responseDetails'], |
2906 | + response['responseDetails']) |
2907 | + |
2908 | + raise TranslationException(msg) |
2909 | + |
2910 | + def language_code (self, name): |
2911 | + """Convert a name to a language code.""" |
2912 | + |
2913 | + name = name.lower() |
2914 | + |
2915 | + if name == '': |
2916 | + return name |
2917 | + |
2918 | + try: |
2919 | + return self.lang_names.get(name) or self.alt_lang_names[name] |
2920 | + except KeyError: |
2921 | + m = re.match('^([a-z]{2,3})(?:-[a-z]{2})?$', name) |
2922 | + if m and m.group(1) in self.lang_names.values(): |
2923 | + return name |
2924 | + else: |
2925 | + raise UnknownLanguageException |
2926 | + |
2927 | # vi: set et sta sw=4 ts=4: |
2928 | |
2929 | === removed file 'ibid/plugins/lookup.py' |
2930 | --- ibid/plugins/lookup.py 2009-12-30 14:08:03 +0000 |
2931 | +++ ibid/plugins/lookup.py 1970-01-01 00:00:00 +0000 |
2932 | @@ -1,1019 +0,0 @@ |
2933 | -from urllib2 import urlopen, HTTPError |
2934 | -from urllib import urlencode, quote |
2935 | -from httplib import BadStatusLine |
2936 | -from urlparse import urljoin |
2937 | -from time import time, strptime, strftime |
2938 | -from datetime import datetime |
2939 | -from random import choice, shuffle, randint |
2940 | -from math import acos, sin, cos, radians |
2941 | -import re |
2942 | -from sys import exc_info |
2943 | -import logging |
2944 | - |
2945 | -import feedparser |
2946 | - |
2947 | -from ibid.compat import defaultdict, dt_strptime, ElementTree |
2948 | -from ibid.config import Option, BoolOption, DictOption |
2949 | -from ibid.plugins import Processor, match, handler |
2950 | -from ibid.utils import ago, decode_htmlentities, cacheable_download, \ |
2951 | - json_webservice, human_join, plural |
2952 | -from ibid.utils.html import get_html_parse_tree |
2953 | - |
2954 | -log = logging.getLogger('plugins.lookup') |
2955 | - |
2956 | -help = {} |
2957 | - |
2958 | -def get_country_codes(): |
2959 | - # The XML download doesn't include things like UK, so we consume this steaming pile of crud instead |
2960 | - filename = cacheable_download('http://www.iso.org/iso/country_codes/iso_3166_code_lists/iso-3166-1_decoding_table.htm', 'lookup/iso-3166-1_decoding_table.htm') |
2961 | - etree = get_html_parse_tree('file://' + filename, treetype='etree') |
2962 | - table = [x for x in etree.getiterator('table')][2] |
2963 | - |
2964 | - countries = {} |
2965 | - for tr in table.getiterator('tr'): |
2966 | - abbr = [x.text for x in tr.getiterator('div')][0] |
2967 | - eng_name = [x.text for x in tr.getchildren()][1] |
2968 | - |
2969 | - if eng_name and eng_name.strip(): |
2970 | - # Cleanup: |
2971 | - if u',' in eng_name: |
2972 | - eng_name = u' '.join(reversed(eng_name.split(',', 1))) |
2973 | - eng_name = u' '.join(eng_name.split()) |
2974 | - |
2975 | - countries[abbr.upper()] = eng_name.title() |
2976 | - |
2977 | - return countries |
2978 | - |
2979 | -help['bash'] = u'Retrieve quotes from bash.org.' |
2980 | -class Bash(Processor): |
2981 | - u"bash[.org] [(random|<number>)]" |
2982 | - |
2983 | - feature = 'bash' |
2984 | - |
2985 | - public_browse = BoolOption('public_browse', 'Allow random quotes in public', True) |
2986 | - |
2987 | - @match(r'^bash(?:\.org)?(?:\s+(random|\d+))?$') |
2988 | - def bash(self, event, id): |
2989 | - id = id is None and u'random' or id.lower() |
2990 | - |
2991 | - if id == u'random' and event.public and not self.public_browse: |
2992 | - event.addresponse(u'Sorry, not in public. PM me') |
2993 | - return |
2994 | - |
2995 | - soup = get_html_parse_tree('http://bash.org/?%s' % id) |
2996 | - |
2997 | - number = u"".join(soup.find('p', 'quote').find('b').contents) |
2998 | - output = [u'%s:' % number] |
2999 | - |
3000 | - body = soup.find('p', 'qt') |
3001 | - if not body: |
3002 | - event.addresponse(u"There's no such quote, but if you keep talking like that maybe there will be") |
3003 | - else: |
3004 | - for line in body.contents: |
3005 | - line = unicode(line).strip() |
3006 | - if line != u'<br />': |
3007 | - output.append(line) |
3008 | - event.addresponse(u'\n'.join(output), conflate=False) |
3009 | - |
3010 | -help['lastfm'] = u'Lists the tracks last listened to by the specified user.' |
3011 | -class LastFm(Processor): |
3012 | - u"last.fm for <username>" |
3013 | - |
3014 | - feature = "lastfm" |
3015 | - |
3016 | - @match(r'^last\.?fm\s+for\s+(\S+?)\s*$') |
3017 | - def listsongs(self, event, username): |
3018 | - songs = feedparser.parse('http://ws.audioscrobbler.com/1.0/user/%s/recenttracks.rss?%s' % (username, time())) |
3019 | - if songs['bozo']: |
3020 | - event.addresponse(u'No such user') |
3021 | - else: |
3022 | - event.addresponse(u', '.join(u'%s (%s ago)' % ( |
3023 | - e.title, |
3024 | - ago(event.time - dt_strptime(e.updated, '%a, %d %b %Y %H:%M:%S +0000'), 1) |
3025 | - ) for e in songs['entries'])) |
3026 | - |
3027 | -help['lotto'] = u"Gets the latest lotto results from the South African National Lottery." |
3028 | -class Lotto(Processor): |
3029 | - u"""lotto""" |
3030 | - |
3031 | - feature = 'lotto' |
3032 | - |
3033 | - za_url = 'http://www.nationallottery.co.za/' |
3034 | - za_re = re.compile(r'images/(?:power_)?balls/(?:ball|power)_(\d+).gif') |
3035 | - |
3036 | - @match(r'^lotto(\s+for\s+south\s+africa)?$') |
3037 | - def za(self, event, za): |
3038 | - try: |
3039 | - f = urlopen(self.za_url) |
3040 | - except Exception, e: |
3041 | - event.addresponse(u'Something went wrong getting to the Lotto site') |
3042 | - return |
3043 | - |
3044 | - s = "".join(f) |
3045 | - f.close() |
3046 | - |
3047 | - balls = self.za_re.findall(s) |
3048 | - |
3049 | - if len(balls) != 20: |
3050 | - event.addresponse(u'I expected to get %(expected)s balls, but found %(found)s. They were: %(balls)s', { |
3051 | - 'expected': 20, |
3052 | - 'found': len(balls), |
3053 | - 'balls': u', '.join(balls), |
3054 | - }) |
3055 | - return |
3056 | - |
3057 | - event.addresponse(u'Latest lotto results for South Africa, ' |
3058 | - u'Lotto: %(lottoballs)s (Bonus: %(lottobonus)s), ' |
3059 | - u'Lotto Plus: %(plusballs)s (Bonus: %(plusbonus)s), ' |
3060 | - u'PowerBall: %(powerballs)s PB: %(powerball)s', { |
3061 | - 'lottoballs': u' '.join(balls[:6]), |
3062 | - 'lottobonus': balls[6], |
3063 | - 'plusballs': u' '.join(balls[7:13]), |
3064 | - 'plusbonus': balls[13], |
3065 | - 'powerballs': u' '.join(balls[14:19]), |
3066 | - 'powerball': balls[19], |
3067 | - }) |
3068 | - |
3069 | -help['fml'] = u'Retrieves quotes from fmylife.com.' |
3070 | -class FMLException(Exception): |
3071 | - pass |
3072 | - |
3073 | -class FMyLife(Processor): |
3074 | - u"""fml (<number> | [random] | flop | top | last | love | money | kids | work | health | sex | miscellaneous )""" |
3075 | - |
3076 | - feature = "fml" |
3077 | - |
3078 | - api_url = Option('fml_api_url', 'FML API URL base', 'http://api.betacie.com/') |
3079 | - # The Ibid API Key, registered by Stefano Rivera: |
3080 | - api_key = Option('fml_api_key', 'FML API Key', '4b39a7fcaf01c') |
3081 | - fml_lang = Option('fml_lang', 'FML Lanugage', 'en') |
3082 | - |
3083 | - public_browse = BoolOption('public_browse', 'Allow random quotes in public', True) |
3084 | - |
3085 | - failure_messages = ( |
3086 | - u'Today, I tried to get a quote for %(nick)s but failed. FML', |
3087 | - u'Today, FML is down. FML', |
3088 | - u"Sorry, it's broken, the FML admins must having a really bad day", |
3089 | - ) |
3090 | - |
3091 | - def remote_get(self, id): |
3092 | - url = urljoin(self.api_url, 'view/%s?%s' % ( |
3093 | - id.isalnum() and id + '/nocomment' or quote(id), |
3094 | - urlencode({'language': self.fml_lang, 'key': self.api_key})) |
3095 | - ) |
3096 | - f = urlopen(url) |
3097 | - try: |
3098 | - tree = ElementTree.parse(f) |
3099 | - except SyntaxError: |
3100 | - class_, e, tb = exc_info() |
3101 | - new_exc = FMLException(u'XML Parsing Error: %s' % unicode(e)) |
3102 | - raise new_exc.__class__, new_exc, tb |
3103 | - |
3104 | - if tree.find('.//error'): |
3105 | - raise FMLException(tree.findtext('.//error')) |
3106 | - |
3107 | - item = tree.find('.//item') |
3108 | - if item: |
3109 | - url = u"http://www.fmylife.com/%s/%s" % ( |
3110 | - item.findtext('category'), |
3111 | - item.get('id'), |
3112 | - ) |
3113 | - text = item.find('text').text |
3114 | - return u'%s\n- %s' % (text, url) |
3115 | - |
3116 | - @match(r'^(?:fml\s+|http://www\.fmylife\.com/\S+/)(\d+|random|flop|top|last|love|money|kids|work|health|sex|miscellaneous)$') |
3117 | - def fml(self, event, id): |
3118 | - try: |
3119 | - body = self.remote_get(id) |
3120 | - except (FMLException, HTTPError, BadStatusLine): |
3121 | - event.addresponse(choice(self.failure_messages) % event.sender) |
3122 | - return |
3123 | - |
3124 | - if body: |
3125 | - event.addresponse(body) |
3126 | - elif id.isdigit(): |
3127 | - event.addresponse(u'No such quote') |
3128 | - else: |
3129 | - event.addresponse(choice(self.failure_messages) % event.sender) |
3130 | - |
3131 | - @match(r'^fml$') |
3132 | - def fml_default(self, event): |
3133 | - if not event.public or self.public_browse: |
3134 | - self.fml(event, 'random') |
3135 | - else: |
3136 | - event.addresponse(u'Sorry, not in public. PM me') |
3137 | - |
3138 | -help["tfln"] = u"Looks up quotes from textsfromlastnight.com" |
3139 | -class TextsFromLastNight(Processor): |
3140 | - u"""tfln [(random|<number>)] |
3141 | - tfln (worst|best) [(today|this week|this month)]""" |
3142 | - |
3143 | - feature = 'tfln' |
3144 | - |
3145 | - public_browse = BoolOption('public_browse', 'Allow random quotes in public', True) |
3146 | - |
3147 | - random_pool = [] |
3148 | - |
3149 | - def get_tfln(self, section): |
3150 | - tree = get_html_parse_tree('http://textsfromlastnight.com/%s/' % section.lower()) |
3151 | - for div in tree.findAll('div', attrs={'class': 'post_wrap'}): |
3152 | - id = int(div.get('id').split('_', 1)[1]) |
3153 | - message = [] |
3154 | - line = '' |
3155 | - for a in div.findAll('div', attrs={'class': 'post_content'})[0].findAll('a'): |
3156 | - if a['href'].startswith('/areacode/'): |
3157 | - line = u'%s: ' % a.contents[0] |
3158 | - else: |
3159 | - message.append(line + a.contents[0]) |
3160 | - yield id, message |
3161 | - |
3162 | - @match(r'^tfln' |
3163 | - r'(?:\s+(random|worst|best|\d+))?' |
3164 | - r'(?:this\s+)?(?:\s+(today|week|month))?$') |
3165 | - def tfln(self, event, number, timeframe=None): |
3166 | - number = number is None and u'random' or number.lower() |
3167 | - |
3168 | - if number == u'random' and not timeframe \ |
3169 | - and event.public and not self.public_browse: |
3170 | - event.addresponse(u'Sorry, not in public. PM me') |
3171 | - return |
3172 | - |
3173 | - if number in (u'worst', u'best'): |
3174 | - number += u'-nights' |
3175 | - if timeframe.lower() in (u'week', u'month'): |
3176 | - number += u'this-' + timeframe.lower() |
3177 | - elif number.isdigit(): |
3178 | - number = 'view/%s' % number |
3179 | - |
3180 | - if number == u'random': |
3181 | - if not self.random_pool: |
3182 | - self.random_pool = [message for message in self.get_tfln(number)] |
3183 | - shuffle(self.random_pool) |
3184 | - |
3185 | - message = self.random_pool.pop() |
3186 | - else: |
3187 | - try: |
3188 | - message = self.get_tfln(number).next() |
3189 | - except StopIteration: |
3190 | - event.addresponse(u'No such quote') |
3191 | - return |
3192 | - |
3193 | - id, body = message |
3194 | - if len(body) > 1: |
3195 | - for line in body: |
3196 | - event.addresponse(line) |
3197 | - event.addresponse(u'- http://textsfromlastnight.com/view/%i', id) |
3198 | - else: |
3199 | - event.addresponse(u'%(body)s\n- http://textsfromlastnight.com/view/%(id)i', { |
3200 | - 'id': id, |
3201 | - 'body': body[0], |
3202 | - }) |
3203 | - |
3204 | - @match(r'^(?:http://)?(?:www\.)?textsfromlastnight\.com/view/(\d+)$') |
3205 | - def tfln_url(self, event, id): |
3206 | - self.tfln(event, id) |
3207 | - |
3208 | -help["mlia"] = u"Looks up quotes from MyLifeIsAverage.com and MyLifeIsG.com" |
3209 | -class MyLifeIsAverage(Processor): |
3210 | - u"""mlia [(<number> | random | recent | today | yesterday | this week | this month | this year )] |
3211 | - mlig [(<number> | random | recent | today | yesterday | this week | this month | this year )]""" |
3212 | - |
3213 | - feature = 'mlia' |
3214 | - |
3215 | - public_browse = BoolOption('public_browse', |
3216 | - 'Allow random quotes in public', True) |
3217 | - |
3218 | - random_pool = {} |
3219 | - pages = {} |
3220 | - |
3221 | - def find_stories(self, url, site='mlia'): |
3222 | - if isinstance(url, basestring): |
3223 | - tree = get_html_parse_tree(url, treetype='etree') |
3224 | - else: |
3225 | - tree = url |
3226 | - |
3227 | - stories = [div for div in tree.findall('.//div') |
3228 | - if div.get(u'class') in |
3229 | - (u'story s', # mlia |
3230 | - u'stories', u'stories-wide')] # mlig |
3231 | - |
3232 | - for story in stories: |
3233 | - if site == 'mlia': |
3234 | - body = story.findtext('div').strip() |
3235 | - else: |
3236 | - body = story.findtext('div/span/span').strip() |
3237 | - id = story.findtext('.//a') |
3238 | - if isinstance(id, basestring) and id[1:].isdigit(): |
3239 | - id = int(id[1:]) |
3240 | - yield id, body |
3241 | - |
3242 | - @match(r'^(mli[ag])(?:\s+this)?' |
3243 | - r'(?:\s+(\d+|random|recent|today|yesterday|week|month|year))?$') |
3244 | - def mlia(self, event, site, query): |
3245 | - query = query is None and u'random' or query.lower() |
3246 | - |
3247 | - if query == u'random' and event.public and not self.public_browse: |
3248 | - event.addresponse(u'Sorry, not in public. PM me') |
3249 | - return |
3250 | - |
3251 | - site = site.lower() |
3252 | - url = { |
3253 | - 'mlia': 'http://mylifeisaverage.com/', |
3254 | - 'mlig': 'http://mylifeisg.com/', |
3255 | - }[site] |
3256 | - |
3257 | - if query == u'random' or query is None: |
3258 | - if not self.random_pool.get(site): |
3259 | - if site == 'mlia': |
3260 | - purl = url + str(randint(1, self.pages.get(site, 1))) |
3261 | - else: |
3262 | - purl = url + 'index.php?' + urlencode({ |
3263 | - 'page': randint(1, self.pages.get(site, 1)) |
3264 | - }) |
3265 | - tree = get_html_parse_tree(purl, treetype='etree') |
3266 | - self.random_pool[site] = [story for story |
3267 | - in self.find_stories(tree, site=site)] |
3268 | - shuffle(self.random_pool[site]) |
3269 | - |
3270 | - if site == 'mlia': |
3271 | - pagination = [ul for ul in tree.findall('.//ul') |
3272 | - if ul.get(u'class') == u'pages'][0] |
3273 | - self.pages[site] = int( |
3274 | - [li for li in pagination.findall('li') |
3275 | - if li.get(u'class') == u'last'][0] |
3276 | - .find(u'a').get(u'href')) |
3277 | - else: |
3278 | - pagination = [div for div in tree.findall('.//div') |
3279 | - if div.get(u'class') == u'pagination'][0] |
3280 | - self.pages[site] = sorted(int(a.text) for a |
3281 | - in pagination.findall('.//a') |
3282 | - if a.text.isdigit())[-1] |
3283 | - |
3284 | - story = self.random_pool[site].pop() |
3285 | - |
3286 | - else: |
3287 | - try: |
3288 | - if site == 'mlia': |
3289 | - if query.isdigit(): |
3290 | - surl = url + '/s/' + query |
3291 | - else: |
3292 | - surl = url + '/best/' + query |
3293 | - else: |
3294 | - if query.isdigit(): |
3295 | - surl = url + 'story.php?' + urlencode({'id': query}) |
3296 | - else: |
3297 | - surl = url + 'index.php?' + urlencode({'part': query}) |
3298 | - |
3299 | - story = self.find_stories(surl, site=site).next() |
3300 | - |
3301 | - except StopIteration: |
3302 | - event.addresponse(u'No such quote') |
3303 | - return |
3304 | - |
3305 | - id, body = story |
3306 | - if site == 'mlia': |
3307 | - url += 's/%i' % id |
3308 | - else: |
3309 | - url += 'story.php?id=%i' % id |
3310 | - event.addresponse(u'%(body)s\n- %(url)s', { |
3311 | - 'url': url, |
3312 | - 'body': body, |
3313 | - }) |
3314 | - |
3315 | - @match(r'^(?:http://)?(?:www\.)?mylifeis(average|g)\.com' |
3316 | - r'/story\.php\?id=(\d+)$') |
3317 | - def mlia_url(self, event, site, id): |
3318 | - self.mlia(event, 'mli' + site[0].lower(), id) |
3319 | - |
3320 | -help["microblog"] = u"Looks up messages on microblogging services like twitter and identica." |
3321 | -class Twitter(Processor): |
3322 | - u"""latest (tweet|identica) from <name> |
3323 | - (tweet|identica) <number>""" |
3324 | - |
3325 | - feature = "microblog" |
3326 | - |
3327 | - default = { |
3328 | - 'twitter': {'endpoint': 'http://twitter.com/', 'api': 'twitter', 'name': 'tweet', 'user': 'twit'}, |
3329 | - 'tweet': {'endpoint': 'http://twitter.com/', 'api': 'twitter', 'name': 'tweet', 'user': 'twit'}, |
3330 | - 'identica': {'endpoint': 'http://identi.ca/api/', 'api': 'laconica', 'name': 'dent', 'user': 'denter'}, |
3331 | - 'identi.ca': {'endpoint': 'http://identi.ca/api/', 'api': 'laconica', 'name': 'dent', 'user': 'denter'}, |
3332 | - 'dent': {'endpoint': 'http://identi.ca/api/', 'api': 'laconica', 'name': 'dent', 'user': 'denter'}, |
3333 | - } |
3334 | - services = DictOption('services', 'Micro blogging services', default) |
3335 | - |
3336 | - class NoSuchUserException(Exception): |
3337 | - pass |
3338 | - |
3339 | - def setup(self): |
3340 | - self.update.im_func.pattern = re.compile(r'^(%s)\s+(\d+)$' % '|'.join(self.services.keys()), re.I) |
3341 | - self.latest.im_func.pattern = re.compile(r'^(?:latest|last)\s+(%s)\s+(?:update\s+)?(?:(?:by|from|for)\s+)?@?(\S+)$' |
3342 | - % '|'.join(self.services.keys()), re.I) |
3343 | - |
3344 | - def remote_update(self, service, id): |
3345 | - status = json_webservice('%sstatuses/show/%s.json' % (service['endpoint'], id)) |
3346 | - |
3347 | - return {'screen_name': status['user']['screen_name'], 'text': decode_htmlentities(status['text'])} |
3348 | - |
3349 | - def remote_latest(self, service, user): |
3350 | - statuses = json_webservice( |
3351 | - '%sstatuses/user_timeline/%s.json' % (service['endpoint'], user.encode('utf-8')), |
3352 | - {'count': 1}) |
3353 | - |
3354 | - if not statuses: |
3355 | - raise self.NoSuchUserException(user) |
3356 | - |
3357 | - latest = statuses[0] |
3358 | - |
3359 | - if service['api'] == 'twitter': |
3360 | - url = '%s%s/status/%i' % (service['endpoint'], latest['user']['screen_name'], latest['id']) |
3361 | - elif service['api'] == 'laconica': |
3362 | - url = '%s/notice/%i' % (service['endpoint'].split('/api/', 1)[0], latest['id']) |
3363 | - |
3364 | - return { |
3365 | - 'text': decode_htmlentities(latest['text']), |
3366 | - 'ago': ago(datetime.utcnow() - dt_strptime(latest['created_at'], '%a %b %d %H:%M:%S +0000 %Y')), |
3367 | - 'url': url, |
3368 | - } |
3369 | - |
3370 | - @handler |
3371 | - def update(self, event, service_name, id): |
3372 | - service = self.services[service_name.lower()] |
3373 | - try: |
3374 | - event.addresponse(u'%(screen_name)s: "%(text)s"', self.remote_update(service, int(id))) |
3375 | - except HTTPError, e: |
3376 | - if e.code in (401, 403): |
3377 | - event.addresponse(u'That %s is private', service['name']) |
3378 | - elif e.code == 404: |
3379 | - event.addresponse(u'No such %s', service['name']) |
3380 | - else: |
3381 | - event.addresponse(u'I can only see the Fail Whale') |
3382 | - |
3383 | - @handler |
3384 | - def latest(self, event, service_name, user): |
3385 | - service = self.services[service_name.lower()] |
3386 | - try: |
3387 | - event.addresponse(u'"%(text)s" %(ago)s ago, %(url)s', self.remote_latest(service, user)) |
3388 | - except HTTPError, e: |
3389 | - if e.code in (401, 403): |
3390 | - event.addresponse(u"Sorry, %s's feed is private", user) |
3391 | - elif e.code == 404: |
3392 | - event.addresponse(u'No such %s', service['user']) |
3393 | - else: |
3394 | - event.addresponse(u'I can only see the Fail Whale') |
3395 | - except self.NoSuchUserException, e: |
3396 | - event.addresponse(u'No such %s', service['user']) |
3397 | - |
3398 | - @match(r'^https?://(?:www\.)?twitter\.com/[^/ ]+/statuse?s?/(\d+)$') |
3399 | - def twitter(self, event, id): |
3400 | - self.update(event, u'twitter', id) |
3401 | - |
3402 | - @match(r'^https?://(?:www\.)?identi.ca/notice/(\d+)$') |
3403 | - def identica(self, event, id): |
3404 | - self.update(event, u'identica', id) |
3405 | - |
3406 | -help['currency'] = u'Converts amounts between currencies.' |
3407 | -class Currency(Processor): |
3408 | - u"""exchange <amount> <currency> for <currency> |
3409 | - currencies for <country>""" |
3410 | - |
3411 | - feature = "currency" |
3412 | - |
3413 | - headers = {'User-Agent': 'Mozilla/5.0', 'Referer': 'http://www.xe.com/'} |
3414 | - currencies = {} |
3415 | - country_codes = {} |
3416 | - |
3417 | - def _load_currencies(self): |
3418 | - etree = get_html_parse_tree('http://www.xe.com/iso4217.php', headers=self.headers, treetype='etree') |
3419 | - |
3420 | - tbl_main = [x for x in etree.getiterator('table') if x.get('class') == 'tbl_main'][0] |
3421 | - |
3422 | - self.currencies = {} |
3423 | - for tbl_sub in tbl_main.getiterator('table'): |
3424 | - if tbl_sub.get('class') == 'tbl_sub': |
3425 | - for tr in tbl_sub.getiterator('tr'): |
3426 | - code, place = [x.text for x in tr.getchildren()] |
3427 | - name = u'' |
3428 | - if not place: |
3429 | - place = u'' |
3430 | - if u',' in place[1:-1]: |
3431 | - place, name = place.split(u',', 1) |
3432 | - place = place.strip() |
3433 | - if code in self.currencies: |
3434 | - currency = self.currencies[code] |
3435 | - # Are we using another country's currency? |
3436 | - if place != u'' and name != u'' and (currency[1] == u'' or currency[1].rsplit(None, 1)[0] in place |
3437 | - or (u'(also called' in currency[1] and currency[1].split(u'(', 1)[0].rsplit(None, 1)[0] in place)): |
3438 | - currency[0].insert(0, place) |
3439 | - currency[1] = name.strip() |
3440 | - else: |
3441 | - currency[0].append(place) |
3442 | - else: |
3443 | - self.currencies[code] = [[place], name.strip()] |
3444 | - |
3445 | - # Special cases for shared currencies: |
3446 | - self.currencies['EUR'][0].insert(0, u'Euro Member Countries') |
3447 | - self.currencies['XOF'][0].insert(0, u'Communaut\xe9 Financi\xe8re Africaine') |
3448 | - self.currencies['XOF'][1] = u'Francs' |
3449 | - |
3450 | - strip_currency_re = re.compile(r'^[\.\s]*([\w\s]+?)s?$', re.UNICODE) |
3451 | - |
3452 | - def _resolve_currency(self, name, rough=True): |
3453 | - "Return the canonical name for a currency" |
3454 | - |
3455 | - if name.upper() in self.currencies: |
3456 | - return name.upper() |
3457 | - |
3458 | - m = self.strip_currency_re.match(name) |
3459 | - |
3460 | - if m is None: |
3461 | - return False |
3462 | - |
3463 | - name = m.group(1).lower() |
3464 | - |
3465 | - # TLD -> country name |
3466 | - if rough and len(name) == 2 and name.upper() in self.country_codes: |
3467 | - name = self.country_codes[name.upper()].lower() |
3468 | - |
3469 | - # Currency Name |
3470 | - if name == u'dollar': |
3471 | - return "USD" |
3472 | - |
3473 | - name_re = re.compile(r'^(.+\s+)?\(?%ss?\)?(\s+.+)?$' % name, re.I | re.UNICODE) |
3474 | - for code, (places, currency) in self.currencies.iteritems(): |
3475 | - if name_re.match(currency) or [True for place in places if name_re.match(place)]: |
3476 | - return code |
3477 | - |
3478 | - return False |
3479 | - |
3480 | - @match(r'^(exchange|convert)\s+([0-9.]+)\s+(.+)\s+(?:for|to|into)\s+(.+)$') |
3481 | - def exchange(self, event, command, amount, frm, to): |
3482 | - if not self.currencies: |
3483 | - self._load_currencies() |
3484 | - |
3485 | - if not self.country_codes: |
3486 | - self.country_codes = get_country_codes() |
3487 | - |
3488 | - rough = command.lower() == 'exchange' |
3489 | - |
3490 | - canonical_frm = self._resolve_currency(frm, rough) |
3491 | - canonical_to = self._resolve_currency(to, rough) |
3492 | - if not canonical_frm or not canonical_to: |
3493 | - if rough: |
3494 | - event.addresponse(u"Sorry, I don't know about a currency for %s", (not canonical_frm and frm or to)) |
3495 | - return |
3496 | - |
3497 | - data = {'Amount': amount, 'From': canonical_frm, 'To': canonical_to} |
3498 | - etree = get_html_parse_tree('http://www.xe.com/ucc/convert.cgi', urlencode(data), self.headers, 'etree') |
3499 | - |
3500 | - result = [tag.text for tag in etree.getiterator('h2')] |
3501 | - if result: |
3502 | - event.addresponse(u'%(fresult)s (%(fcountry)s %(fcurrency)s) = %(tresult)s (%(tcountry)s %(tcurrency)s)', { |
3503 | - 'fresult': result[0], |
3504 | - 'tresult': result[2], |
3505 | - 'fcountry': self.currencies[canonical_frm][0][0], |
3506 | - 'fcurrency': self.currencies[canonical_frm][1], |
3507 | - 'tcountry': self.currencies[canonical_to][0][0], |
3508 | - 'tcurrency': self.currencies[canonical_to][1], |
3509 | - }) |
3510 | - else: |
3511 | - event.addresponse(u"The bureau de change appears to be closed for lunch") |
3512 | - |
3513 | - @match(r'^(?:currency|currencies)\s+for\s+(?:the\s+)?(.+)$') |
3514 | - def currency(self, event, place): |
3515 | - if not self.currencies: |
3516 | - self._load_currencies() |
3517 | - |
3518 | - search = re.compile(place, re.I) |
3519 | - results = [] |
3520 | - for code, (places, name) in self.currencies.iteritems(): |
3521 | - for place in places: |
3522 | - if search.search(place): |
3523 | - results.append(u'%s uses %s (%s)' % (place, name, code)) |
3524 | - break |
3525 | - |
3526 | - if results: |
3527 | - event.addresponse(human_join(results)) |
3528 | - else: |
3529 | - event.addresponse(u'No currencies found') |
3530 | - |
3531 | -help['tld'] = u"Resolve country TLDs (ISO 3166)" |
3532 | -class TLD(Processor): |
3533 | - u""".<tld> |
3534 | - tld for <country>""" |
3535 | - feature = 'tld' |
3536 | - |
3537 | - country_codes = {} |
3538 | - |
3539 | - @match(r'^\.([a-zA-Z]{2})$') |
3540 | - def tld_to_country(self, event, tld): |
3541 | - if not self.country_codes: |
3542 | - self.country_codes = get_country_codes() |
3543 | - |
3544 | - tld = tld.upper() |
3545 | - |
3546 | - if tld in self.country_codes: |
3547 | - event.addresponse(u'%(tld)s is the TLD for %(country)s', { |
3548 | - 'tld': tld, |
3549 | - 'country': self.country_codes[tld], |
3550 | - }) |
3551 | - else: |
3552 | - event.addresponse(u"ISO doesn't know about any such TLD") |
3553 | - |
3554 | - @match(r'^tld\s+for\s+(.+)$') |
3555 | - def country_to_tld(self, event, location): |
3556 | - if not self.country_codes: |
3557 | - self.country_codes = get_country_codes() |
3558 | - |
3559 | - for tld, country in self.country_codes.iteritems(): |
3560 | - if location.lower() in country.lower(): |
3561 | - event.addresponse(u'%(tld)s is the TLD for %(country)s', { |
3562 | - 'tld': tld, |
3563 | - 'country': country, |
3564 | - }) |
3565 | - return |
3566 | - |
3567 | - event.addresponse(u"ISO doesn't know about any TLD for %s", location) |
3568 | - |
3569 | -help['weather'] = u'Retrieves current weather and forecasts for cities.' |
3570 | -class Weather(Processor): |
3571 | - u"""weather in <city> |
3572 | - forecast for <city>""" |
3573 | - |
3574 | - feature = "weather" |
3575 | - |
3576 | - defaults = { 'ct': 'Cape Town, South Africa', |
3577 | - 'jhb': 'Johannesburg, South Africa', |
3578 | - 'joburg': 'Johannesburg, South Africa', |
3579 | - } |
3580 | - places = DictOption('places', 'Alternate names for places', defaults) |
3581 | - labels = ('temp', 'humidity', 'dew', 'wind', 'pressure', 'conditions', 'visibility', 'uv', 'clouds', 'ymin', 'ymax', 'ycool', 'sunrise', 'sunset', 'moonrise', 'moonset', 'moonphase', 'metar') |
3582 | - whitespace = re.compile('\s+') |
3583 | - |
3584 | - class WeatherException(Exception): |
3585 | - pass |
3586 | - |
3587 | - class TooManyPlacesException(WeatherException): |
3588 | - pass |
3589 | - |
3590 | - def _text(self, string): |
3591 | - if not isinstance(string, basestring): |
3592 | - string = ''.join(string.findAll(text=True)) |
3593 | - return self.whitespace.sub(' ', string).strip() |
3594 | - |
3595 | - def _get_page(self, place): |
3596 | - if place.lower() in self.places: |
3597 | - place = self.places[place.lower()] |
3598 | - |
3599 | - soup = get_html_parse_tree('http://m.wund.com/cgi-bin/findweather/getForecast?brand=mobile_metric&query=' + quote(place)) |
3600 | - |
3601 | - if soup.body.center and soup.body.center.b.string == 'Search not found:': |
3602 | - raise Weather.WeatherException(u'City not found') |
3603 | - |
3604 | - if soup.table.tr.th and soup.table.tr.th.string == 'Place: Temperature': |
3605 | - places = [] |
3606 | - for td in soup.table.findAll('td'): |
3607 | - places.append(td.find('a', href=re.compile('.*html$')).string) |
3608 | - |
3609 | - # Cities with more than one airport give duplicate entries. We can take the first |
3610 | - if len([x for x in places if x == places[0]]) == len(places): |
3611 | - url = urljoin('http://m.wund.com/cgi-bin/findweather/getForecast', |
3612 | - soup.table.find('td').find('a', href=re.compile('.*html$'))['href']) |
3613 | - soup = get_html_parse_tree(url) |
3614 | - else: |
3615 | - raise Weather.TooManyPlacesException(places) |
3616 | - |
3617 | - return soup |
3618 | - |
3619 | - def remote_weather(self, place): |
3620 | - soup = self._get_page(place) |
3621 | - tds = [x.table for x in soup.findAll('table') if x.table][0].findAll('td') |
3622 | - |
3623 | - # HACK: Some cities include a windchill row, but others don't |
3624 | - if len(tds) == 39: |
3625 | - del tds[3] |
3626 | - del tds[4] |
3627 | - |
3628 | - values = {'place': tds[0].findAll('b')[1].string, 'time': tds[0].findAll('b')[0].string} |
3629 | - for index, td in enumerate(tds[2::2]): |
3630 | - values[self.labels[index]] = self._text(td) |
3631 | - |
3632 | - return values |
3633 | - |
3634 | - def remote_forecast(self, place): |
3635 | - soup = self._get_page(place) |
3636 | - forecasts = [] |
3637 | - table = [table for table in soup.findAll('table') if table.findAll('td', align='left')][0] |
3638 | - |
3639 | - for td in table.findAll('td', align='left'): |
3640 | - day = td.b.string |
3641 | - forecast = u' '.join([self._text(line) for line in td.contents[2:]]) |
3642 | - forecasts.append(u'%s: %s' % (day, self._text(forecast))) |
3643 | - |
3644 | - return forecasts |
3645 | - |
3646 | - @match(r'^weather\s+(?:(?:for|at|in)\s+)?(.+)$') |
3647 | - def weather(self, event, place): |
3648 | - try: |
3649 | - values = self.remote_weather(place) |
3650 | - event.addresponse(u'In %(place)s at %(time)s: %(temp)s; Humidity: %(humidity)s; Wind: %(wind)s; Conditions: %(conditions)s; Sunrise/set: %(sunrise)s/%(sunset)s; Moonrise/set: %(moonrise)s/%(moonset)s', values) |
3651 | - except Weather.TooManyPlacesException, e: |
3652 | - event.addresponse(u'Too many places match %(place)s: %(exception)s', { |
3653 | - 'place': place, |
3654 | - 'exception': human_join(e.args[0], separator=u';'), |
3655 | - }) |
3656 | - except Weather.WeatherException, e: |
3657 | - event.addresponse(unicode(e)) |
3658 | - |
3659 | - @match(r'^forecast\s+(?:for\s+)?(.+)$') |
3660 | - def forecast(self, event, place): |
3661 | - try: |
3662 | - event.addresponse(u', '.join(self.remote_forecast(place))) |
3663 | - except Weather.TooManyPlacesException, e: |
3664 | - event.addresponse(u'Too many places match %(place)s: %(exception)s', { |
3665 | - 'place': place, |
3666 | - 'exception': human_join(e.args[0], separator=u';'), |
3667 | - }) |
3668 | - except Weather.WeatherException, e: |
3669 | - event.addresponse(unicode(e)) |
3670 | - |
3671 | -help['distance'] = u"Returns the distance between two places" |
3672 | -class Distance(Processor): |
3673 | - u"""distance [in <unit>] between <source> and <destination> |
3674 | - place search for <placename>""" |
3675 | - |
3676 | - # For Mathematics, see: |
3677 | - # http://www.mathforum.com/library/drmath/view/51711.html |
3678 | - # http://mathworld.wolfram.com/GreatCircle.html |
3679 | - |
3680 | - feature = 'distance' |
3681 | - |
3682 | - default_unit_names = { |
3683 | - 'km': "kilometres", |
3684 | - 'mi': "miles", |
3685 | - 'nm': "nautical miles"} |
3686 | - default_radius_values = { |
3687 | - 'km': 6378, |
3688 | - 'mi': 3963.1, |
3689 | - 'nm': 3443.9} |
3690 | - |
3691 | - unit_names = DictOption('unit_names', 'Names of units in which to specify distances', default_unit_names) |
3692 | - radius_values = DictOption('radius_values', 'Radius of the earth in the units in which to specify distances', default_radius_values) |
3693 | - |
3694 | - def get_place_data(self, place, num): |
3695 | - return json_webservice('http://ws.geonames.org/searchJSON', {'q': place, 'maxRows': num}) |
3696 | - |
3697 | - def get_place(self, place): |
3698 | - js = self.get_place_data(place, 1) |
3699 | - if js['totalResultsCount'] == 0: |
3700 | - return None |
3701 | - info = js['geonames'][0] |
3702 | - return {'name': "%s, %s, %s" % (info['name'], info['adminName1'], info['countryName']), |
3703 | - 'lng': radians(info['lng']), |
3704 | - 'lat': radians(info['lat'])} |
3705 | - |
3706 | - @match(r'^(?:(?:search\s+for\s+place)|(?:place\s+search\s+for)|(?:places\s+for))\s+(\S.+?)\s*$') |
3707 | - def placesearch(self, event, place): |
3708 | - js = self.get_place_data(place, 10) |
3709 | - if js['totalResultsCount'] == 0: |
3710 | - event.addresponse(u"I don't know of anywhere even remotely like '%s'", place) |
3711 | - else: |
3712 | - event.addresponse(u"I can find: %s", |
3713 | - (human_join([u"%s, %s, %s" % (p['name'], p['adminName1'], p['countryName']) |
3714 | - for p in js['geonames'][:10]], |
3715 | - separator=u';'))) |
3716 | - |
3717 | - @match(r'^(?:how\s*far|distance)(?:\s+in\s+(\S+))?\s+' |
3718 | - r'(?:(between)|from)' # Between ... and ... | from ... to ... |
3719 | - r'\s+(\S.+?)\s+(?(2)and|to)\s+(\S.+?)\s*$') |
3720 | - def distance(self, event, unit, ignore, src, dst): |
3721 | - unit_names = self.unit_names |
3722 | - if unit and unit not in self.unit_names: |
3723 | - event.addresponse(u"I don't know the unit '%(badunit)s'. I know about: %(knownunits)s", { |
3724 | - 'badunit': unit, |
3725 | - 'knownunits': |
3726 | - human_join(u"%s (%s)" % (unit, self.unit_names[unit]) |
3727 | - for unit in self.unit_names), |
3728 | - }) |
3729 | - return |
3730 | - if unit: |
3731 | - unit_names = [unit] |
3732 | - |
3733 | - srcp, dstp = self.get_place(src), self.get_place(dst) |
3734 | - if not srcp or not dstp: |
3735 | - event.addresponse(u"I don't know of anywhere called %s", |
3736 | - (u" or ".join("'%s'" % place[0] |
3737 | - for place in ((src, srcp), (dst, dstp)) if not place[1]))) |
3738 | - return |
3739 | - |
3740 | - dist = acos(cos(srcp['lng']) * cos(dstp['lng']) * cos(srcp['lat']) * cos(dstp['lat']) + |
3741 | - cos(srcp['lat']) * sin(srcp['lng']) * cos(dstp['lat']) * sin(dstp['lng']) + |
3742 | - sin(srcp['lat'])*sin(dstp['lat'])) |
3743 | - |
3744 | - event.addresponse(u"Approximate distance, as the bot flies, between %(srcname)s and %(dstname)s is: %(distance)s", { |
3745 | - 'srcname': srcp['name'], |
3746 | - 'dstname': dstp['name'], |
3747 | - 'distance': human_join([ |
3748 | - u"%.02f %s" % (self.radius_values[unit]*dist, self.unit_names[unit]) |
3749 | - for unit in unit_names], |
3750 | - conjunction=u'or'), |
3751 | - }) |
3752 | - |
3753 | -help['tvshow'] = u'Retrieves TV show information from tvrage.com.' |
3754 | -class TVShow(Processor): |
3755 | - u"""tvshow <show>""" |
3756 | - |
3757 | - feature = 'tvshow' |
3758 | - |
3759 | - def remote_tvrage(self, show): |
3760 | - info_url = 'http://services.tvrage.com/tools/quickinfo.php?%s' |
3761 | - |
3762 | - info = urlopen(info_url % urlencode({'show': show.encode('utf-8')})) |
3763 | - |
3764 | - info = info.read() |
3765 | - info = info.decode('utf-8') |
3766 | - if info.startswith('No Show Results Were Found'): |
3767 | - return |
3768 | - info = info[5:].splitlines() |
3769 | - show_info = [i.split('@', 1) for i in info] |
3770 | - show_dict = dict(show_info) |
3771 | - |
3772 | - #check if there are actual airdates for Latest and Next Episode. None for Next |
3773 | - #Episode does not neccesarily mean it is nor airing, just the date is unconfirmed. |
3774 | - show_dict = defaultdict(lambda: 'None', show_info) |
3775 | - |
3776 | - for field in ('Latest Episode', 'Next Episode'): |
3777 | - if field in show_dict: |
3778 | - ep, name, date = show_dict[field].split('^', 2) |
3779 | - count = date.count('/') |
3780 | - format_from = { |
3781 | - 0: '%Y', |
3782 | - 1: '%b/%Y', |
3783 | - 2: '%b/%d/%Y' |
3784 | - }[count] |
3785 | - format_to = ' '.join(('%d', '%B', '%Y')[-1 - count:]) |
3786 | - date = strftime(format_to, strptime(date, format_from)) |
3787 | - show_dict[field] = u'%s - "%s" - %s' % (ep, name, date) |
3788 | - |
3789 | - if 'Genres' in show_dict: |
3790 | - show_dict['Genres'] = human_join(show_dict['Genres'].split(' | ')) |
3791 | - |
3792 | - return show_dict |
3793 | - |
3794 | - @match(r'^tv\s*show\s+(.+)$') |
3795 | - def tvshow(self, event, show): |
3796 | - retr_info = self.remote_tvrage(show) |
3797 | - |
3798 | - message = u'Show: %(Show Name)s. Premiered: %(Premiered)s. ' \ |
3799 | - u'Latest Episode: %(Latest Episode)s. Next Episode: %(Next Episode)s. ' \ |
3800 | - u'Airtime: %(Airtime)s on %(Network)s. Genres: %(Genres)s. ' \ |
3801 | - u'Status: %(Status)s. %(Show URL)s' |
3802 | - |
3803 | - if not retr_info: |
3804 | - event.addresponse(u"I can't find anything out about '%s'", show) |
3805 | - return |
3806 | - |
3807 | - event.addresponse(message, retr_info) |
3808 | - |
3809 | -help['bible'] = u'Retrieves Bible verses' |
3810 | -class Bible(Processor): |
3811 | - u"""bible <passages> [in <version>] |
3812 | - <book> <verses> [in <version>]""" |
3813 | - |
3814 | - feature = 'bible' |
3815 | - # http://labs.bible.org/api/ is an alternative |
3816 | - # Their feature set is a little different, but they should be fairly |
3817 | - # compatible |
3818 | - api_url = Option('bible_api_url', 'Bible API URL base', |
3819 | - 'http://api.preachingcentral.com/bible.php') |
3820 | - |
3821 | - psalm_pat = re.compile(r'\bpsalm\b', re.IGNORECASE) |
3822 | - |
3823 | - # The API doesn't seem to work with the apocrypha, even when looking in |
3824 | - # versions that include it |
3825 | - books = '|'.join(['Genesis', 'Exodus', 'Leviticus', 'Numbers', 'Deuteronomy', |
3826 | - 'Joshua', 'Judges', 'Ruth', '(?:1|2|I|II) Samuel', '(?:1|2|I|II) Kings', |
3827 | - '(?:1|2|I|II) Chronicles', 'Ezra', 'Nehemiah', 'Esther', 'Job', 'Psalms?', |
3828 | - 'Proverbs', 'Ecclesiastes', 'Song(?: of (?:Songs|Solomon)?)?', |
3829 | - 'Canticles', 'Isaiah', 'Jeremiah', 'Lamentations', |
3830 | - 'Ezekiel', 'Daniel', 'Hosea', 'Joel', 'Amos', 'Obadiah', 'Jonah', 'Micah', |
3831 | - 'Nahum', 'Habakkuk', 'Zephaniah', 'Haggai', 'Zechariah', 'Malachi', |
3832 | - 'Matthew', 'Mark', 'Luke', 'John', 'Acts', 'Romans', |
3833 | - '(?:1|2|I|II) Corinthians', 'Galatians', 'Ephesians', 'Philippians', |
3834 | - 'Colossians', '(?:1|2|I|II) Thessalonians', '(?:1|2|I|II) Timothy', |
3835 | - 'Titus', 'Philemon', 'Hebrews', 'James', '(?:1|2|I|II) Peter', |
3836 | - '(?:1|2|3|I|II|III) John', 'Jude', |
3837 | - 'Revelations?(?: of (?:St.|Saint) John)?']).replace(' ', '\s*') |
3838 | - |
3839 | - @match(r'^bible\s+(.*?)(?:\s+(?:in|from)\s+(.*))?$') |
3840 | - def bible(self, event, passage, version=None): |
3841 | - passage = self.psalm_pat.sub('psalms', passage) |
3842 | - |
3843 | - params = {'passage': passage.encode('utf-8'), |
3844 | - 'type': 'xml', |
3845 | - 'formatting': 'plain'} |
3846 | - if version: |
3847 | - params['version'] = version.lower().encode('utf-8') |
3848 | - |
3849 | - f = urlopen(self.api_url + '?' + urlencode(params)) |
3850 | - tree = ElementTree.parse(f) |
3851 | - |
3852 | - message = self.formatPassage(tree) |
3853 | - if message: |
3854 | - event.addresponse(message) |
3855 | - errors = list(tree.findall('.//error')) |
3856 | - if errors: |
3857 | - event.addresponse('There were errors: %s.', '. '.join(err.text for err in errors)) |
3858 | - elif not message: |
3859 | - event.addresponse("I couldn't find that passage.") |
3860 | - |
3861 | - # Allow queries which are quite definitely bible references to omit "bible". |
3862 | - # Specifically, they must start with the name of a book and be followed only |
3863 | - # by book names, chapters and verses. |
3864 | - @match(r'^((?:(?:' + books + ')(?:\d|[-:,]|\s)*)+?)(?:\s+(?:in|from)\s+(.*))?$') |
3865 | - def bookbible(self, *args): |
3866 | - self.bible(*args) |
3867 | - |
3868 | - def formatPassage(self, xml): |
3869 | - message = [] |
3870 | - oldref = (None, None, None) |
3871 | - for item in xml.findall('.//item'): |
3872 | - ref, text = self.verseInfo(item) |
3873 | - if oldref[0] != ref[0]: |
3874 | - message.append(u'(%s %s:%s)' % ref) |
3875 | - elif oldref[1] != ref[1]: |
3876 | - message.append(u'(%s:%s)' % ref[1:]) |
3877 | - else: |
3878 | - message.append(u'%s' % ref[2]) |
3879 | - oldref = ref |
3880 | - |
3881 | - message.append(text) |
3882 | - |
3883 | - return u' '.join(message) |
3884 | - |
3885 | - def verseInfo(self, xml): |
3886 | - book, chapter, verse, text = map(xml.findtext, |
3887 | - ('bookname', 'chapter', 'verse', 'text')) |
3888 | - return ((book, chapter, verse), text) |
3889 | - |
3890 | -class Sequence(object): |
3891 | - def __init__ (self, lines): |
3892 | - cmds = defaultdict(list) |
3893 | - for line in lines: |
3894 | - line = line.lstrip()[:-1] |
3895 | - if not line: |
3896 | - break |
3897 | - |
3898 | - line_m = re.match(r'%([A-Z]) (A\d+)(?: (.*))?$', line) |
3899 | - if line_m: |
3900 | - cmd, self.catalog_num, info = line_m.groups() |
3901 | - cmds[cmd].append(info) |
3902 | - else: |
3903 | - cmds[cmd][-1] += line |
3904 | - |
3905 | - # %V, %W and %X give signed values if the sequence is signed. |
3906 | - # Otherwise, only %S, %T and %U are given. |
3907 | - self.values = (''.join(cmds['V'] + cmds['W'] + cmds['X']) or |
3908 | - ''.join(cmds['S'] + cmds['T'] + cmds['U'])) |
3909 | - |
3910 | - self.name = ''.join(cmds['N']) |
3911 | - |
3912 | - def url (self): |
3913 | - return 'http://www.research.att.com/~njas/sequences/' + self.catalog_num |
3914 | - |
3915 | -help['oeis'] = 'Query the Online Encyclopedia of Integer Sequences' |
3916 | -class OEIS(Processor): |
3917 | - u"""oeis (A<OEIS number>|M<EIS number>|N<HIS number>) |
3918 | - oeis <term>[, ...]""" |
3919 | - |
3920 | - feature = 'oeis' |
3921 | - |
3922 | - @match(r'^oeis\s+([AMN]\d+|-?\d(?:\d|-|,|\s)*)$') |
3923 | - def oeis (self, event, query): |
3924 | - query = re.sub(r'(,|\s)+', ',', query) |
3925 | - f = urlopen('http://www.research.att.com/~njas/sequences/?n=1&fmt=3&q=' |
3926 | - + query) |
3927 | - |
3928 | - f.next() # the first line is uninteresting |
3929 | - results_m = re.search(r'(\d+) results found', f.next()) |
3930 | - if results_m: |
3931 | - f.next() |
3932 | - sequence = Sequence(f) |
3933 | - event.addresponse(u'%(name)s - %(url)s - %(values)s', |
3934 | - {'name': sequence.name, |
3935 | - 'url': sequence.url(), |
3936 | - 'values': sequence.values}) |
3937 | - |
3938 | - results = int(results_m.group(1)) |
3939 | - if results > 1: |
3940 | - event.addresponse(u'There %(was)s %(count)d more %(results)s. ' |
3941 | - u'See %(url)s%(query)s for more.', |
3942 | - {'was': plural(results-1, 'was', 'were'), |
3943 | - 'count': results-1, |
3944 | - 'results': plural(results-1, 'result', 'results'), |
3945 | - 'url': |
3946 | - 'http://www.research.att.com/~njas/sequences/?fmt=1&q=', |
3947 | - 'query': query}) |
3948 | - else: |
3949 | - event.addresponse(u"I couldn't find that sequence.") |
3950 | - |
3951 | -# vi: set et sta sw=4 ts=4: |
3952 | |
3953 | === added file 'ibid/plugins/lotto.py' |
3954 | --- ibid/plugins/lotto.py 1970-01-01 00:00:00 +0000 |
3955 | +++ ibid/plugins/lotto.py 2010-01-18 18:54:16 +0000 |
3956 | @@ -0,0 +1,52 @@ |
3957 | +import re |
3958 | +from urllib2 import urlopen |
3959 | +import logging |
3960 | + |
3961 | +from ibid.plugins import Processor, match |
3962 | + |
3963 | +log = logging.getLogger('plugins.lotto') |
3964 | +help = {} |
3965 | + |
3966 | +help['lotto'] = u"Gets the latest lotto results from the South African National Lottery." |
3967 | +class Lotto(Processor): |
3968 | + u"""lotto""" |
3969 | + |
3970 | + feature = 'lotto' |
3971 | + |
3972 | + za_url = 'http://www.nationallottery.co.za/' |
3973 | + za_re = re.compile(r'images/(?:power_)?balls/(?:ball|power)_(\d+).gif') |
3974 | + |
3975 | + @match(r'^lotto(\s+for\s+south\s+africa)?$') |
3976 | + def za(self, event, za): |
3977 | + try: |
3978 | + f = urlopen(self.za_url) |
3979 | + except Exception, e: |
3980 | + event.addresponse(u'Something went wrong getting to the Lotto site') |
3981 | + return |
3982 | + |
3983 | + s = "".join(f) |
3984 | + f.close() |
3985 | + |
3986 | + balls = self.za_re.findall(s) |
3987 | + |
3988 | + if len(balls) != 20: |
3989 | + event.addresponse(u'I expected to get %(expected)s balls, but found %(found)s. They were: %(balls)s', { |
3990 | + 'expected': 20, |
3991 | + 'found': len(balls), |
3992 | + 'balls': u', '.join(balls), |
3993 | + }) |
3994 | + return |
3995 | + |
3996 | + event.addresponse(u'Latest lotto results for South Africa, ' |
3997 | + u'Lotto: %(lottoballs)s (Bonus: %(lottobonus)s), ' |
3998 | + u'Lotto Plus: %(plusballs)s (Bonus: %(plusbonus)s), ' |
3999 | + u'PowerBall: %(powerballs)s PB: %(powerball)s', { |
4000 | + 'lottoballs': u' '.join(balls[:6]), |
4001 | + 'lottobonus': balls[6], |
4002 | + 'plusballs': u' '.join(balls[7:13]), |
4003 | + 'plusbonus': balls[13], |
4004 | + 'powerballs': u' '.join(balls[14:19]), |
4005 | + 'powerball': balls[19], |
4006 | + }) |
4007 | + |
4008 | +# vi: set et sta sw=4 ts=4: |
4009 | |
4010 | === modified file 'ibid/plugins/memory.py' |
4011 | --- ibid/plugins/memory.py 2009-10-19 15:07:40 +0000 |
4012 | +++ ibid/plugins/memory.py 2010-01-18 18:54:16 +0000 |
4013 | @@ -3,7 +3,6 @@ |
4014 | import gc |
4015 | import gzip |
4016 | import os |
4017 | -import os.path |
4018 | |
4019 | import objgraph |
4020 | |
4021 | |
4022 | === removed file 'ibid/plugins/misc.py' |
4023 | --- ibid/plugins/misc.py 2009-12-20 18:47:12 +0000 |
4024 | +++ ibid/plugins/misc.py 1970-01-01 00:00:00 +0000 |
4025 | @@ -1,236 +0,0 @@ |
4026 | -import logging |
4027 | -from random import choice, random |
4028 | - |
4029 | -import ibid |
4030 | -from ibid.plugins import Processor, match |
4031 | -from ibid.config import IntOption, ListOption |
4032 | -from ibid.utils import ibid_version, human_join |
4033 | - |
4034 | -help = {} |
4035 | -log = logging.getLogger('plugins.misc') |
4036 | - |
4037 | -help['coffee'] = u"Times coffee brewing and reserves cups for people" |
4038 | -class Coffee(Processor): |
4039 | - u"""coffee (on|please)""" |
4040 | - feature = 'coffee' |
4041 | - |
4042 | - pots = {} |
4043 | - |
4044 | - time = IntOption('coffee_time', u'Brewing time in seconds', 240) |
4045 | - cups = IntOption('coffee_cups', u'Maximum number of cups', 4) |
4046 | - |
4047 | - def coffee_announce(self, event): |
4048 | - event.addresponse(u"Coffee's ready for %s!", |
4049 | - human_join(self.pots[(event.source, event.channel)])) |
4050 | - del self.pots[(event.source, event.channel)] |
4051 | - |
4052 | - @match(r'^coffee\s+on$') |
4053 | - def coffee_on(self, event): |
4054 | - if (event.source, event.channel) in self.pots: |
4055 | - if len(self.pots[(event.source, event.channel)]) >= self.cups: |
4056 | - event.addresponse(u"There's already a pot on, and it's all reserved") |
4057 | - elif event.sender['nick'] in self.pots[(event.source, event.channel)]: |
4058 | - event.addresponse(u"You already have a pot on the go") |
4059 | - else: |
4060 | - event.addresponse(u"There's already a pot on. If you ask nicely, maybe you can have a cup") |
4061 | - return |
4062 | - |
4063 | - self.pots[(event.source, event.channel)] = [event.sender['nick']] |
4064 | - ibid.dispatcher.call_later(self.time, self.coffee_announce, event) |
4065 | - |
4066 | - event.addresponse(choice(( |
4067 | - u'puts the kettle on', |
4068 | - u'starts grinding coffee', |
4069 | - u'flips the salt-timer', |
4070 | - u'washes some mugs', |
4071 | - )), action=True) |
4072 | - |
4073 | - @match('^coffee\s+(?:please|pls)$') |
4074 | - def coffee_accept(self, event): |
4075 | - if (event.source, event.channel) not in self.pots: |
4076 | - event.addresponse(u"There isn't a pot on") |
4077 | - |
4078 | - elif len(self.pots[(event.source, event.channel)]) >= self.cups: |
4079 | - event.addresponse(u"Sorry, there aren't any more cups left") |
4080 | - |
4081 | - elif event.sender['nick'] in self.pots[(event.source, event.channel)]: |
4082 | - event.addresponse(u"Now now, we don't want anyone getting caffeine overdoses") |
4083 | - |
4084 | - else: |
4085 | - self.pots[(event.source, event.channel)].append(event.sender['nick']) |
4086 | - event.addresponse(True) |
4087 | - |
4088 | -help['version'] = u"Show the Ibid version currently running" |
4089 | -class Version(Processor): |
4090 | - u"""version""" |
4091 | - feature = 'version' |
4092 | - |
4093 | - @match(r'^version$') |
4094 | - def show_version(self, event): |
4095 | - if ibid_version(): |
4096 | - event.addresponse(u'I am version %s', ibid_version()) |
4097 | - else: |
4098 | - event.addresponse(u"I don't know what version I am :-(") |
4099 | - |
4100 | -help['dvorak'] = u"Makes text typed on a QWERTY keyboard as if it was Dvorak work, and vice-versa" |
4101 | -class Dvorak(Processor): |
4102 | - u"""(aoeu|asdf) <text>""" |
4103 | - feature = 'dvorak' |
4104 | - |
4105 | - # List of characters on each keyboard layout |
4106 | - dvormap = u"""',.pyfgcrl/=aoeuidhtns-;qjkxbmwvz"<>PYFGCRL?+AOEUIDHTNS_:QJKXBMWVZ[]{}|""" |
4107 | - qwermap = u"""qwertyuiop[]asdfghjkl;'zxcvbnm,./QWERTYUIOP{}ASDFGHJKL:"ZXCVBNM<>?-=_+|""" |
4108 | - |
4109 | - # Typed by a QWERTY typist on a Dvorak-mapped keyboard |
4110 | - typed_on_dvorak = dict(zip(map(ord, dvormap), qwermap)) |
4111 | - # Typed by a Dvorak typist on a QWERTY-mapped keyboard |
4112 | - typed_on_qwerty = dict(zip(map(ord, qwermap), dvormap)) |
4113 | - |
4114 | - @match(r'^(?:asdf|dvorak)\s+(.+)$') |
4115 | - def convert_from_qwerty(self, event, text): |
4116 | - event.addresponse(text.translate(self.typed_on_qwerty)) |
4117 | - |
4118 | - @match(r'^(?:aoeu|qwerty)\s+(.+)$') |
4119 | - def convert_from_dvorak(self, event, text): |
4120 | - event.addresponse(text.translate(self.typed_on_dvorak)) |
4121 | - |
4122 | -help['insult'] = u"Slings verbal abuse at someone" |
4123 | -class Insult(Processor): |
4124 | - u""" |
4125 | - (flame | insult) <person> |
4126 | - (swear | cuss | explete) [at <person>] |
4127 | - """ |
4128 | - feature = 'insult' |
4129 | - |
4130 | - adjectives = ListOption('adjectives', 'List of adjectives', ( |
4131 | - u'acidic', u'antique', u'artless', u'base-court', u'bat-fowling', |
4132 | - u'bawdy', u'beef-witted', u'beetle-headed', u'beslubbering', |
4133 | - u'boil-brained', u'bootless', u'churlish', u'clapper-clawed', |
4134 | - u'clay-brained', u'clouted', u'cockered', u'common-kissing', |
4135 | - u'contemptible', u'coughed-up', u'craven', u'crook-pated', |
4136 | - u'culturally-unsound', u'currish', u'dankish', u'decayed', |
4137 | - u'despicable', u'dismal-dreaming', u'dissembling', u'dizzy-eyed', |
4138 | - u'doghearted', u'dread-bolted', u'droning', u'earth-vexing', |
4139 | - u'egg-sucking', u'elf-skinned', u'errant', u'evil', u'fat-kidneyed', |
4140 | - u'fawning', u'fen-sucked', u'fermented', u'festering', u'flap-mouthed', |
4141 | - u'fly-bitten', u'fobbing', u'folly-fallen', u'fool-born', u'foul', |
4142 | - u'frothy', u'froward', u'full-gorged', u'fulminating', u'gleeking', |
4143 | - u'goatish', u'gorbellied', u'guts-griping', u'hacked-up', u'halfbaked', |
4144 | - u'half-faced', u'hasty-witted', u'headless', u'hedge-born', |
4145 | - u'hell-hated', u'horn-beat', u'hugger-muggered', u'humid', |
4146 | - u'idle-headed', u'ill-borne', u'ill-breeding', u'ill-nurtured', |
4147 | - u'imp-bladdereddle-headed', u'impertinent', u'impure', u'industrial', |
4148 | - u'inept', u'infected', u'infectious', u'inferior', u'it-fowling', |
4149 | - u'jarring', u'knotty-pated', u'left-over', u'lewd-minded', |
4150 | - u'loggerheaded', u'low-quality', u'lumpish', u'malodorous', |
4151 | - u'malt-wormy', u'mammering', u'mangled', u'measled', u'mewling', |
4152 | - u'milk-livered', u'motley-mind', u'motley-minded', u'off-color', |
4153 | - u'onion-eyed', u'paunchy', u'penguin-molesting', u'petrified', |
4154 | - u'pickled', u'pignutted', u'plume-plucked', u'pointy-nosed', u'porous', |
4155 | - u'pottle-deep', u'pox-marked', u'pribbling', u'puking', u'puny', |
4156 | - u'railing', u'rank', u'reeky', u'reeling-ripe', u'roguish', |
4157 | - u'rough-hewn', u'rude-growing', u'rude-snouted', u'rump-fed', |
4158 | - u'ruttish', u'salty', u'saucy', u'saucyspleened', u'sausage-snorfling', |
4159 | - u'shard-borne', u'sheep-biting', u'spam-sucking', u'spleeny', |
4160 | - u'spongy', u'spur-galled', u'squishy', u'surly', u'swag-bellied', |
4161 | - u'tardy-gaited', u'tastless', u'tempestuous', u'tepid', u'thick', |
4162 | - u'tickle-brained', u'toad-spotted', u'tofu-nibbling', u'tottering', |
4163 | - u'uninspiring', u'unintelligent', u'unmuzzled', u'unoriginal', |
4164 | - u'urchin-snouted', u'vain', u'vapid', u'vassal-willed', u'venomed', |
4165 | - u'villainous', u'warped', u'wayward', u'weasel-smelling', |
4166 | - u'weather-bitten', u'weedy', u'wretched', u'yeasty', |
4167 | - )) |
4168 | - |
4169 | - collections = ListOption('collections', 'List of collective nouns', ( |
4170 | - u'accumulation', u'ass-full', u'assload', u'bag', u'bucket', |
4171 | - u'coagulation', u'enema-bucketful', u'gob', u'half-mouthful', u'heap', |
4172 | - u'mass', u'mound', u'ooze', u'petrification', u'pile', u'plate', |
4173 | - u'puddle', u'quart', u'stack', u'thimbleful', u'tongueful', |
4174 | - )) |
4175 | - |
4176 | - nouns = ListOption('nouns', u'List of singular nouns', ( |
4177 | - u'apple-john', u'baggage', u'barnacle', u'bladder', u'boar-pig', |
4178 | - u'bugbear', u'bum-bailey', u'canker-blossom', u'clack-dish', |
4179 | - u'clotpole', u'coxcomb', u'codpiece', u'death-token', u'dewberry', |
4180 | - u'flap-dragon', u'flax-wench', u'flirt-gill', u'foot-licker', |
4181 | - u'fustilarian', u'giglet', u'gudgeon', u'haggard', u'harpy', |
4182 | - u'hedge-pig', u'horn-beast', u'hugger-mugger', u'jolthead', |
4183 | - u'lewdster', u'lout', u'maggot-pie', u'malt-worm', u'mammet', |
4184 | - u'measle', u'minnow', u'miscreant', u'moldwarp', u'mumble-news', |
4185 | - u'nut-hook', u'pigeon-egg', u'pignut', u'puttock', u'pumpion', |
4186 | - u'ratsbane', u'scut', u'skainsmate', u'strumpet', u'varlet', u'vassal', |
4187 | - u'whey-face', u'wagtail', |
4188 | - )) |
4189 | - |
4190 | - plnouns = ListOption('plnouns', u'List of plural nouns', ( |
4191 | - u'anal warts', u'armadillo snouts', u'bat toenails', u'bug spit', |
4192 | - u'buzzard gizzards', u'cat bladders', u'cat hair', u'cat-hair-balls', |
4193 | - u'chicken piss', u'cold sores', u'craptacular carpet droppings', |
4194 | - u'dog balls', u'dog vomit', u'dung', u'eel ooze', u'entrails', |
4195 | - u"fat-woman's stomach-bile", u'fish heads', u'guano', u'gunk', |
4196 | - u'jizzum', u'pods', u'pond scum', u'poop', u'poopy', u'pus', |
4197 | - u'rat-farts', u'rat retch', u'red dye number-9', u'seagull puke', |
4198 | - u'slurpee-backwash', u'snake assholes', u'snake bait', u'snake snot', |
4199 | - u'squirrel guts', u'Stimpy-drool', u'Sun IPC manuals', u'toxic waste', |
4200 | - u'urine samples', u'waffle-house grits', u'yoo-hoo', |
4201 | - )) |
4202 | - |
4203 | - @match(r'^(?:insult|flame)\s+(.+)$') |
4204 | - def insult(self, event, insultee): |
4205 | - articleadj = choice(self.adjectives) |
4206 | - articleadj = (articleadj[0] in u'aehiou' and u'an ' or u'a ') + articleadj |
4207 | - |
4208 | - event.addresponse(choice(( |
4209 | - u'%(insultee)s, thou %(adj1)s, %(adj2)s %(noun)s', |
4210 | - u'%(insultee)s is nothing but %(articleadj)s %(collection)s of %(adj1)s %(plnoun)s', |
4211 | - )), { |
4212 | - 'insultee': insultee, |
4213 | - 'adj1': choice(self.adjectives), |
4214 | - 'adj2': choice(self.adjectives), |
4215 | - 'articleadj': articleadj, |
4216 | - 'collection': choice(self.collections), |
4217 | - 'noun': choice(self.nouns), |
4218 | - 'plnoun': choice(self.plnouns), |
4219 | - }, address=False) |
4220 | - |
4221 | - loneadjectives = ListOption('loneadjectives', |
4222 | - 'List of stand-alone adjectives for swearing', ( |
4223 | - 'bloody', 'damn', 'fucking', 'shitting', 'sodding', 'crapping', |
4224 | - 'wanking', 'buggering', |
4225 | - )) |
4226 | - |
4227 | - swearadjectives = ListOption('swearadjectives', |
4228 | - 'List of adjectives to be combined with swearnouns', ( |
4229 | - 'reaming', 'lapping', 'eating', 'sucking', 'vokken', 'kak', |
4230 | - 'donder', 'bliksem', 'fucking', 'shitting', 'sodding', 'crapping', |
4231 | - 'wanking', 'buggering', |
4232 | - )) |
4233 | - |
4234 | - swearnouns = ListOption('swearnouns', |
4235 | - 'List of nounes to be comined with swearadjectives', ( |
4236 | - 'shit', 'cunt', 'hell', 'mother', 'god', 'maggot', 'father', 'crap', |
4237 | - 'ball', 'whore', 'goat', 'dick', 'cock', 'pile', 'bugger', 'poes', |
4238 | - 'hoer', 'kakrooker', 'ma', 'pa', 'naiier', 'kak', 'bliksem', |
4239 | - 'vokker', 'kakrooker', |
4240 | - )) |
4241 | - |
4242 | - swearlength = IntOption('swearlength', 'Number of expletives to swear with', |
4243 | - 15) |
4244 | - |
4245 | - @match(r'^(?:swear|cuss|explete)(?:\s+at\s+(?:the\s+)?(.*))?$') |
4246 | - def swear(self, event, insultee): |
4247 | - swearage = [] |
4248 | - for i in range(self.swearlength): |
4249 | - if random() > 0.7: |
4250 | - swearage.append(choice(self.loneadjectives)) |
4251 | - else: |
4252 | - swearage.append(choice(self.swearnouns) |
4253 | - + choice(self.swearadjectives)) |
4254 | - if insultee is not None: |
4255 | - swearage.append(insultee) |
4256 | - else: |
4257 | - swearage.append(choice(self.swearnouns)) |
4258 | - |
4259 | - event.addresponse(u' '.join(swearage) + u'!', address=False) |
4260 | - |
4261 | -# vi: set et sta sw=4 ts=4: |
4262 | |
4263 | === removed file 'ibid/plugins/morse.py' |
4264 | --- ibid/plugins/morse.py 2009-10-16 16:31:34 +0000 |
4265 | +++ ibid/plugins/morse.py 1970-01-01 00:00:00 +0000 |
4266 | @@ -1,79 +0,0 @@ |
4267 | -from ibid.plugins import Processor, match |
4268 | - |
4269 | -help = {} |
4270 | - |
4271 | -help["morse"] = u"Translates messages into and out of morse code." |
4272 | - |
4273 | -class Morse(Processor): |
4274 | - u"""morse (text|morsecode)""" |
4275 | - feature = 'morse' |
4276 | - |
4277 | - _table = { |
4278 | - 'A': ".-", |
4279 | - 'B': "-...", |
4280 | - 'C': "-.-.", |
4281 | - 'D': "-..", |
4282 | - 'E': ".", |
4283 | - 'F': "..-.", |
4284 | - 'G': "--.", |
4285 | - 'H': "....", |
4286 | - 'I': "..", |
4287 | - 'J': ".---", |
4288 | - 'K': "-.-", |
4289 | - 'L': ".-..", |
4290 | - 'M': "--", |
4291 | - 'N': "-.", |
4292 | - 'O': "---", |
4293 | - 'P': ".--.", |
4294 | - 'Q': "--.-", |
4295 | - 'R': ".-.", |
4296 | - 'S': "...", |
4297 | - 'T': "-", |
4298 | - 'U': "..-", |
4299 | - 'V': "...-", |
4300 | - 'W': ".--", |
4301 | - 'X': "-..-", |
4302 | - 'Y': "-.--", |
4303 | - 'Z': "--..", |
4304 | - '0': "-----", |
4305 | - '1': ".----", |
4306 | - '2': "..---", |
4307 | - '3': "...--", |
4308 | - '4': "....-", |
4309 | - '5': ".....", |
4310 | - '6': "-....", |
4311 | - '7': "--...", |
4312 | - '8': "---..", |
4313 | - '9': "----.", |
4314 | - ' ': " ", |
4315 | - '.': ".-.-.-", |
4316 | - ',': "--..--", |
4317 | - '?': "..--..", |
4318 | - ':': "---...", |
4319 | - ';': "-.-.-.", |
4320 | - '-': "-....-", |
4321 | - '_': "..--.-", |
4322 | - '"': ".-..-.", |
4323 | - "'": ".----.", |
4324 | - '/': "-..-.", |
4325 | - '(': "-.--.", |
4326 | - ')': "-.--.-", |
4327 | - '=': "-...-", |
4328 | - } |
4329 | - _rtable = dict((v, k) for k, v in _table.items()) |
4330 | - |
4331 | - def _text2morse(self, text): |
4332 | - return u" ".join(self._table.get(c.upper(), c) for c in text) |
4333 | - |
4334 | - def _morse2text(self, morse): |
4335 | - toks = morse.split(u' ') |
4336 | - return u"".join(self._rtable.get(t, u' ') for t in toks) |
4337 | - |
4338 | - @match(r'^morse\s*(.*)$', 'deaddressed') |
4339 | - def morse(self, event, message): |
4340 | - if not (set(message) - set(u'-./ \t\n')): |
4341 | - event.addresponse(u'Decodes as %s', self._morse2text(message)) |
4342 | - else: |
4343 | - event.addresponse(u'Encodes as %s', self._text2morse(message)) |
4344 | - |
4345 | -# vi: set et sta sw=4 ts=4: |
4346 | |
4347 | === modified file 'ibid/plugins/network.py' |
4348 | --- ibid/plugins/network.py 2009-12-30 14:08:03 +0000 |
4349 | +++ ibid/plugins/network.py 2010-01-18 18:54:16 +0000 |
4350 | @@ -3,13 +3,18 @@ |
4351 | |
4352 | from dns.resolver import Resolver, NoAnswer, NXDOMAIN |
4353 | from dns.reversename import from_address |
4354 | +from httplib import HTTPConnection, HTTPSConnection |
4355 | +from urllib import getproxies_environment |
4356 | +from urlparse import urlparse |
4357 | |
4358 | from ibid.plugins import Processor, match |
4359 | -from ibid.config import Option |
4360 | +from ibid.config import Option, IntOption |
4361 | from ibid.utils import file_in_path, unicode_output, human_join |
4362 | +from ibid.utils.html import get_country_codes |
4363 | |
4364 | help = {} |
4365 | ipaddr = re.compile('\d+\.\d+\.\d+\.\d+') |
4366 | +title = re.compile(r'<title>(.*)<\/title>', re.I+re.S) |
4367 | |
4368 | help['dns'] = u'Performs DNS lookups' |
4369 | class DNS(Processor): |
4370 | @@ -166,4 +171,88 @@ |
4371 | error = unicode_output(error.strip()) |
4372 | event.addresponse(error.replace(u'\n', u' ')) |
4373 | |
4374 | +help['get'] = u'Retrieves a URL and returns the HTTP status and optionally the HTML title.' |
4375 | +class HTTP(Processor): |
4376 | + u"""(get|head) <url>""" |
4377 | + feature = 'get' |
4378 | + |
4379 | + max_size = IntOption('max_size', 'Only request this many bytes', 500) |
4380 | + |
4381 | + @match(r'^(get|head)\s+(\S+\.\S+)$') |
4382 | + def handler(self, event, action, url): |
4383 | + if not url.lower().startswith("http://") and not url.lower().startswith("https://"): |
4384 | + url = "http://" + url |
4385 | + if url.count("/") < 3: |
4386 | + url += "/" |
4387 | + |
4388 | + action = action.upper() |
4389 | + |
4390 | + scheme, host = urlparse(url)[:2] |
4391 | + scheme = scheme.lower() |
4392 | + proxies = getproxies_environment() |
4393 | + if scheme in proxies: |
4394 | + scheme, host = urlparse(proxies[scheme])[:2] |
4395 | + scheme = scheme.lower() |
4396 | + |
4397 | + if scheme == "https": |
4398 | + conn = HTTPSConnection(host) |
4399 | + else: |
4400 | + conn = HTTPConnection(host) |
4401 | + |
4402 | + headers={} |
4403 | + if action == 'GET': |
4404 | + headers['Range'] = 'bytes=0-%s' % self.max_size |
4405 | + conn.request(action.upper(), url, headers=headers) |
4406 | + |
4407 | + response = conn.getresponse() |
4408 | + reply = u'%s %s' % (response.status, response.reason) |
4409 | + |
4410 | + data = response.read() |
4411 | + conn.close() |
4412 | + |
4413 | + if action == 'GET': |
4414 | + match = title.search(data) |
4415 | + if match: |
4416 | + reply += u' "%s"' % match.groups()[0].strip() |
4417 | + |
4418 | + event.addresponse(reply) |
4419 | + |
4420 | +help['tld'] = u"Resolve country TLDs (ISO 3166)" |
4421 | +class TLD(Processor): |
4422 | + u""".<tld> |
4423 | + tld for <country>""" |
4424 | + feature = 'tld' |
4425 | + |
4426 | + country_codes = {} |
4427 | + |
4428 | + @match(r'^\.([a-zA-Z]{2})$') |
4429 | + def tld_to_country(self, event, tld): |
4430 | + if not self.country_codes: |
4431 | + self.country_codes = get_country_codes() |
4432 | + |
4433 | + tld = tld.upper() |
4434 | + |
4435 | + if tld in self.country_codes: |
4436 | + event.addresponse(u'%(tld)s is the TLD for %(country)s', { |
4437 | + 'tld': tld, |
4438 | + 'country': self.country_codes[tld], |
4439 | + }) |
4440 | + else: |
4441 | + event.addresponse(u"ISO doesn't know about any such TLD") |
4442 | + |
4443 | + @match(r'^tld\s+for\s+(.+)$') |
4444 | + def country_to_tld(self, event, location): |
4445 | + if not self.country_codes: |
4446 | + self.country_codes = get_country_codes() |
4447 | + |
4448 | + for tld, country in self.country_codes.iteritems(): |
4449 | + if location.lower() in country.lower(): |
4450 | + event.addresponse(u'%(tld)s is the TLD for %(country)s', { |
4451 | + 'tld': tld, |
4452 | + 'country': country, |
4453 | + }) |
4454 | + return |
4455 | + |
4456 | + event.addresponse(u"ISO doesn't know about any TLD for %s", location) |
4457 | + |
4458 | # vi: set et sta sw=4 ts=4: |
4459 | |
4460 | === added file 'ibid/plugins/oeis.py' |
4461 | --- ibid/plugins/oeis.py 1970-01-01 00:00:00 +0000 |
4462 | +++ ibid/plugins/oeis.py 2010-01-18 18:54:16 +0000 |
4463 | @@ -0,0 +1,74 @@ |
4464 | +from urllib2 import urlopen |
4465 | +import re |
4466 | +import logging |
4467 | + |
4468 | +from ibid.compat import defaultdict |
4469 | +from ibid.plugins import Processor, match |
4470 | +from ibid.utils import plural |
4471 | + |
4472 | +log = logging.getLogger('plugins.oeis') |
4473 | + |
4474 | +help = {} |
4475 | + |
4476 | +help['oeis'] = 'Query the Online Encyclopedia of Integer Sequences' |
4477 | +class OEIS(Processor): |
4478 | + u"""oeis (A<OEIS number>|M<EIS number>|N<HIS number>) |
4479 | + oeis <term>[, ...]""" |
4480 | + |
4481 | + feature = 'oeis' |
4482 | + |
4483 | + @match(r'^oeis\s+([AMN]\d+|-?\d(?:\d|-|,|\s)*)$') |
4484 | + def oeis (self, event, query): |
4485 | + query = re.sub(r'(,|\s)+', ',', query) |
4486 | + f = urlopen('http://www.research.att.com/~njas/sequences/?n=1&fmt=3&q=' |
4487 | + + query) |
4488 | + |
4489 | + f.next() # the first line is uninteresting |
4490 | + results_m = re.search(r'(\d+) results found', f.next()) |
4491 | + if results_m: |
4492 | + f.next() |
4493 | + sequence = Sequence(f) |
4494 | + event.addresponse(u'%(name)s - %(url)s - %(values)s', |
4495 | + {'name': sequence.name, |
4496 | + 'url': sequence.url(), |
4497 | + 'values': sequence.values}) |
4498 | + |
4499 | + results = int(results_m.group(1)) |
4500 | + if results > 1: |
4501 | + event.addresponse(u'There %(was)s %(count)d more %(results)s. ' |
4502 | + u'See %(url)s%(query)s for more.', |
4503 | + {'was': plural(results-1, 'was', 'were'), |
4504 | + 'count': results-1, |
4505 | + 'results': plural(results-1, 'result', 'results'), |
4506 | + 'url': |
4507 | + 'http://www.research.att.com/~njas/sequences/?fmt=1&q=', |
4508 | + 'query': query}) |
4509 | + else: |
4510 | + event.addresponse(u"I couldn't find that sequence.") |
4511 | + |
4512 | +class Sequence(object): |
4513 | + def __init__ (self, lines): |
4514 | + cmds = defaultdict(list) |
4515 | + for line in lines: |
4516 | + line = line.lstrip()[:-1] |
4517 | + if not line: |
4518 | + break |
4519 | + |
4520 | + line_m = re.match(r'%([A-Z]) (A\d+)(?: (.*))?$', line) |
4521 | + if line_m: |
4522 | + cmd, self.catalog_num, info = line_m.groups() |
4523 | + cmds[cmd].append(info) |
4524 | + else: |
4525 | + cmds[cmd][-1] += line |
4526 | + |
4527 | + # %V, %W and %X give signed values if the sequence is signed. |
4528 | + # Otherwise, only %S, %T and %U are given. |
4529 | + self.values = (''.join(cmds['V'] + cmds['W'] + cmds['X']) or |
4530 | + ''.join(cmds['S'] + cmds['T'] + cmds['U'])) |
4531 | + |
4532 | + self.name = ''.join(cmds['N']) |
4533 | + |
4534 | + def url (self): |
4535 | + return 'http://www.research.att.com/~njas/sequences/' + self.catalog_num |
4536 | + |
4537 | +# vi: set et sta sw=4 ts=4: |
4538 | |
4539 | === added file 'ibid/plugins/quotes.py' |
4540 | --- ibid/plugins/quotes.py 1970-01-01 00:00:00 +0000 |
4541 | +++ ibid/plugins/quotes.py 2010-01-18 18:54:16 +0000 |
4542 | @@ -0,0 +1,419 @@ |
4543 | +from urllib2 import urlopen, HTTPError |
4544 | +from urllib import urlencode, quote |
4545 | +from httplib import BadStatusLine |
4546 | +from urlparse import urljoin |
4547 | +from random import choice, shuffle, randint |
4548 | +from sys import exc_info |
4549 | +from subprocess import Popen, PIPE |
4550 | +import logging |
4551 | +import re |
4552 | + |
4553 | +from ibid.compat import ElementTree |
4554 | +from ibid.config import Option, BoolOption |
4555 | +from ibid.plugins import Processor, match, RPC |
4556 | +from ibid.utils.html import get_html_parse_tree |
4557 | +from ibid.utils import file_in_path, unicode_output |
4558 | + |
4559 | +log = logging.getLogger('plugins.quotes') |
4560 | + |
4561 | +help = {} |
4562 | + |
4563 | +help['fortune'] = u'Returns a random fortune.' |
4564 | +class Fortune(Processor, RPC): |
4565 | + u"""fortune""" |
4566 | + feature = 'fortune' |
4567 | + |
4568 | + fortune = Option('fortune', 'Path of the fortune executable', 'fortune') |
4569 | + |
4570 | + def __init__(self, name): |
4571 | + super(Fortune, self).__init__(name) |
4572 | + RPC.__init__(self) |
4573 | + |
4574 | + def setup(self): |
4575 | + if not file_in_path(self.fortune): |
4576 | + raise Exception("Cannot locate fortune executable") |
4577 | + |
4578 | + @match(r'^fortune$') |
4579 | + def handler(self, event): |
4580 | + fortune = self.remote_fortune() |
4581 | + if fortune: |
4582 | + event.addresponse(fortune) |
4583 | + else: |
4584 | + event.addresponse(u"Couldn't execute fortune") |
4585 | + |
4586 | + def remote_fortune(self): |
4587 | + fortune = Popen(self.fortune, stdout=PIPE, stderr=PIPE) |
4588 | + output, error = fortune.communicate() |
4589 | + code = fortune.wait() |
4590 | + |
4591 | + output = unicode_output(output.strip(), 'replace') |
4592 | + |
4593 | + if code == 0: |
4594 | + return output |
4595 | + else: |
4596 | + return None |
4597 | + |
4598 | +help['bash'] = u'Retrieve quotes from bash.org.' |
4599 | +class Bash(Processor): |
4600 | + u"bash[.org] [(random|<number>)]" |
4601 | + |
4602 | + feature = 'bash' |
4603 | + |
4604 | + public_browse = BoolOption('public_browse', 'Allow random quotes in public', True) |
4605 | + |
4606 | + @match(r'^bash(?:\.org)?(?:\s+(random|\d+))?$') |
4607 | + def bash(self, event, id): |
4608 | + id = id is None and u'random' or id.lower() |
4609 | + |
4610 | + if id == u'random' and event.public and not self.public_browse: |
4611 | + event.addresponse(u'Sorry, not in public. PM me') |
4612 | + return |
4613 | + |
4614 | + soup = get_html_parse_tree('http://bash.org/?%s' % id) |
4615 | + |
4616 | + number = u"".join(soup.find('p', 'quote').find('b').contents) |
4617 | + output = [u'%s:' % number] |
4618 | + |
4619 | + body = soup.find('p', 'qt') |
4620 | + if not body: |
4621 | + event.addresponse(u"There's no such quote, but if you keep talking like that maybe there will be") |
4622 | + else: |
4623 | + for line in body.contents: |
4624 | + line = unicode(line).strip() |
4625 | + if line != u'<br />': |
4626 | + output.append(line) |
4627 | + event.addresponse(u'\n'.join(output), conflate=False) |
4628 | + |
4629 | +help['fml'] = u'Retrieves quotes from fmylife.com.' |
4630 | +class FMLException(Exception): |
4631 | + pass |
4632 | + |
4633 | +class FMyLife(Processor): |
4634 | + u"""fml (<number> | [random] | flop | top | last | love | money | kids | work | health | sex | miscellaneous )""" |
4635 | + |
4636 | + feature = "fml" |
4637 | + |
4638 | + api_url = Option('fml_api_url', 'FML API URL base', 'http://api.betacie.com/') |
4639 | + # The Ibid API Key, registered by Stefano Rivera: |
4640 | + api_key = Option('fml_api_key', 'FML API Key', '4b39a7fcaf01c') |
4641 | + fml_lang = Option('fml_lang', 'FML Lanugage', 'en') |
4642 | + |
4643 | + public_browse = BoolOption('public_browse', 'Allow random quotes in public', True) |
4644 | + |
4645 | + failure_messages = ( |
4646 | + u'Today, I tried to get a quote for %(nick)s but failed. FML', |
4647 | + u'Today, FML is down. FML', |
4648 | + u"Sorry, it's broken, the FML admins must having a really bad day", |
4649 | + ) |
4650 | + |
4651 | + def remote_get(self, id): |
4652 | + url = urljoin(self.api_url, 'view/%s?%s' % ( |
4653 | + id.isalnum() and id + '/nocomment' or quote(id), |
4654 | + urlencode({'language': self.fml_lang, 'key': self.api_key})) |
4655 | + ) |
4656 | + f = urlopen(url) |
4657 | + try: |
4658 | + tree = ElementTree.parse(f) |
4659 | + except SyntaxError: |
4660 | + class_, e, tb = exc_info() |
4661 | + new_exc = FMLException(u'XML Parsing Error: %s' % unicode(e)) |
4662 | + raise new_exc.__class__, new_exc, tb |
4663 | + |
4664 | + if tree.find('.//error'): |
4665 | + raise FMLException(tree.findtext('.//error')) |
4666 | + |
4667 | + item = tree.find('.//item') |
4668 | + if item: |
4669 | + url = u"http://www.fmylife.com/%s/%s" % ( |
4670 | + item.findtext('category'), |
4671 | + item.get('id'), |
4672 | + ) |
4673 | + text = item.find('text').text |
4674 | + return u'%s\n- %s' % (text, url) |
4675 | + |
4676 | + @match(r'^(?:fml\s+|http://www\.fmylife\.com/\S+/)(\d+|random|flop|top|last|love|money|kids|work|health|sex|miscellaneous)$') |
4677 | + def fml(self, event, id): |
4678 | + try: |
4679 | + body = self.remote_get(id) |
4680 | + except (FMLException, HTTPError, BadStatusLine): |
4681 | + event.addresponse(choice(self.failure_messages) % event.sender) |
4682 | + return |
4683 | + |
4684 | + if body: |
4685 | + event.addresponse(body) |
4686 | + elif id.isdigit(): |
4687 | + event.addresponse(u'No such quote') |
4688 | + else: |
4689 | + event.addresponse(choice(self.failure_messages) % event.sender) |
4690 | + |
4691 | + @match(r'^fml$') |
4692 | + def fml_default(self, event): |
4693 | + if not event.public or self.public_browse: |
4694 | + self.fml(event, 'random') |
4695 | + else: |
4696 | + event.addresponse(u'Sorry, not in public. PM me') |
4697 | + |
4698 | +help["tfln"] = u"Looks up quotes from textsfromlastnight.com" |
4699 | +class TextsFromLastNight(Processor): |
4700 | + u"""tfln [(random|<number>)] |
4701 | + tfln (worst|best) [(today|this week|this month)]""" |
4702 | + |
4703 | + feature = 'tfln' |
4704 | + |
4705 | + public_browse = BoolOption('public_browse', 'Allow random quotes in public', True) |
4706 | + |
4707 | + random_pool = [] |
4708 | + |
4709 | + def get_tfln(self, section): |
4710 | + tree = get_html_parse_tree('http://textsfromlastnight.com/%s/' % section.lower()) |
4711 | + for div in tree.findAll('div', attrs={'class': 'post_wrap'}): |
4712 | + id = int(div.get('id').split('_', 1)[1]) |
4713 | + message = [] |
4714 | + line = '' |
4715 | + for a in div.findAll('div', attrs={'class': 'post_content'})[0].findAll('a'): |
4716 | + if a['href'].startswith('/areacode/'): |
4717 | + line = u'%s: ' % a.contents[0] |
4718 | + else: |
4719 | + message.append(line + a.contents[0]) |
4720 | + yield id, message |
4721 | + |
4722 | + @match(r'^tfln' |
4723 | + r'(?:\s+(random|worst|best|\d+))?' |
4724 | + r'(?:this\s+)?(?:\s+(today|week|month))?$') |
4725 | + def tfln(self, event, number, timeframe=None): |
4726 | + number = number is None and u'random' or number.lower() |
4727 | + |
4728 | + if number == u'random' and not timeframe \ |
4729 | + and event.public and not self.public_browse: |
4730 | + event.addresponse(u'Sorry, not in public. PM me') |
4731 | + return |
4732 | + |
4733 | + if number in (u'worst', u'best'): |
4734 | + number += u'-nights' |
4735 | + if timeframe.lower() in (u'week', u'month'): |
4736 | + number += u'this-' + timeframe.lower() |
4737 | + elif number.isdigit(): |
4738 | + number = 'view/%s' % number |
4739 | + |
4740 | + if number == u'random': |
4741 | + if not self.random_pool: |
4742 | + self.random_pool = [message for message in self.get_tfln(number)] |
4743 | + shuffle(self.random_pool) |
4744 | + |
4745 | + message = self.random_pool.pop() |
4746 | + else: |
4747 | + try: |
4748 | + message = self.get_tfln(number).next() |
4749 | + except StopIteration: |
4750 | + event.addresponse(u'No such quote') |
4751 | + return |
4752 | + |
4753 | + id, body = message |
4754 | + if len(body) > 1: |
4755 | + for line in body: |
4756 | + event.addresponse(line) |
4757 | + event.addresponse(u'- http://textsfromlastnight.com/view/%i', id) |
4758 | + else: |
4759 | + event.addresponse(u'%(body)s\n- http://textsfromlastnight.com/view/%(id)i', { |
4760 | + 'id': id, |
4761 | + 'body': body[0], |
4762 | + }) |
4763 | + |
4764 | + @match(r'^(?:http://)?(?:www\.)?textsfromlastnight\.com/view/(\d+)$') |
4765 | + def tfln_url(self, event, id): |
4766 | + self.tfln(event, id) |
4767 | + |
4768 | +help["mlia"] = u"Looks up quotes from MyLifeIsAverage.com and MyLifeIsG.com" |
4769 | +class MyLifeIsAverage(Processor): |
4770 | + u"""mlia [(<number> | random | recent | today | yesterday | this week | this month | this year )] |
4771 | + mlig [(<number> | random | recent | today | yesterday | this week | this month | this year )]""" |
4772 | + |
4773 | + feature = 'mlia' |
4774 | + |
4775 | + public_browse = BoolOption('public_browse', |
4776 | + 'Allow random quotes in public', True) |
4777 | + |
4778 | + random_pool = {} |
4779 | + pages = {} |
4780 | + |
4781 | + def find_stories(self, url, site='mlia'): |
4782 | + if isinstance(url, basestring): |
4783 | + tree = get_html_parse_tree(url, treetype='etree') |
4784 | + else: |
4785 | + tree = url |
4786 | + |
4787 | + stories = [div for div in tree.findall('.//div') |
4788 | + if div.get(u'class') in |
4789 | + (u'story s', # mlia |
4790 | + u'stories', u'stories-wide')] # mlig |
4791 | + |
4792 | + for story in stories: |
4793 | + if site == 'mlia': |
4794 | + body = story.findtext('div').strip() |
4795 | + else: |
4796 | + body = story.findtext('div/span/span').strip() |
4797 | + id = story.findtext('.//a') |
4798 | + if isinstance(id, basestring) and id[1:].isdigit(): |
4799 | + id = int(id[1:]) |
4800 | + yield id, body |
4801 | + |
4802 | + @match(r'^(mli[ag])(?:\s+this)?' |
4803 | + r'(?:\s+(\d+|random|recent|today|yesterday|week|month|year))?$') |
4804 | + def mlia(self, event, site, query): |
4805 | + query = query is None and u'random' or query.lower() |
4806 | + |
4807 | + if query == u'random' and event.public and not self.public_browse: |
4808 | + event.addresponse(u'Sorry, not in public. PM me') |
4809 | + return |
4810 | + |
4811 | + site = site.lower() |
4812 | + url = { |
4813 | + 'mlia': 'http://mylifeisaverage.com/', |
4814 | + 'mlig': 'http://mylifeisg.com/', |
4815 | + }[site] |
4816 | + |
4817 | + if query == u'random' or query is None: |
4818 | + if not self.random_pool.get(site): |
4819 | + if site == 'mlia': |
4820 | + purl = url + str(randint(1, self.pages.get(site, 1))) |
4821 | + else: |
4822 | + purl = url + 'index.php?' + urlencode({ |
4823 | + 'page': randint(1, self.pages.get(site, 1)) |
4824 | + }) |
4825 | + tree = get_html_parse_tree(purl, treetype='etree') |
4826 | + self.random_pool[site] = [story for story |
4827 | + in self.find_stories(tree, site=site)] |
4828 | + shuffle(self.random_pool[site]) |
4829 | + |
4830 | + if site == 'mlia': |
4831 | + pagination = [ul for ul in tree.findall('.//ul') |
4832 | + if ul.get(u'class') == u'pages'][0] |
4833 | + self.pages[site] = int( |
4834 | + [li for li in pagination.findall('li') |
4835 | + if li.get(u'class') == u'last'][0] |
4836 | + .find(u'a').get(u'href')) |
4837 | + else: |
4838 | + pagination = [div for div in tree.findall('.//div') |
4839 | + if div.get(u'class') == u'pagination'][0] |
4840 | + self.pages[site] = sorted(int(a.text) for a |
4841 | + in pagination.findall('.//a') |
4842 | + if a.text.isdigit())[-1] |
4843 | + |
4844 | + story = self.random_pool[site].pop() |
4845 | + |
4846 | + else: |
4847 | + try: |
4848 | + if site == 'mlia': |
4849 | + if query.isdigit(): |
4850 | + surl = url + '/s/' + query |
4851 | + else: |
4852 | + surl = url + '/best/' + query |
4853 | + else: |
4854 | + if query.isdigit(): |
4855 | + surl = url + 'story.php?' + urlencode({'id': query}) |
4856 | + else: |
4857 | + surl = url + 'index.php?' + urlencode({'part': query}) |
4858 | + |
4859 | + story = self.find_stories(surl, site=site).next() |
4860 | + |
4861 | + except StopIteration: |
4862 | + event.addresponse(u'No such quote') |
4863 | + return |
4864 | + |
4865 | + id, body = story |
4866 | + if site == 'mlia': |
4867 | + url += 's/%i' % id |
4868 | + else: |
4869 | + url += 'story.php?id=%i' % id |
4870 | + event.addresponse(u'%(body)s\n- %(url)s', { |
4871 | + 'url': url, |
4872 | + 'body': body, |
4873 | + }) |
4874 | + |
4875 | + @match(r'^(?:http://)?(?:www\.)?mylifeis(average|g)\.com' |
4876 | + r'/story\.php\?id=(\d+)$') |
4877 | + def mlia_url(self, event, site, id): |
4878 | + self.mlia(event, 'mli' + site[0].lower(), id) |
4879 | + |
4880 | +help['bible'] = u'Retrieves Bible verses' |
4881 | +class Bible(Processor): |
4882 | + u"""bible <passages> [in <version>] |
4883 | + <book> <verses> [in <version>]""" |
4884 | + |
4885 | + feature = 'bible' |
4886 | + # http://labs.bible.org/api/ is an alternative |
4887 | + # Their feature set is a little different, but they should be fairly |
4888 | + # compatible |
4889 | + api_url = Option('bible_api_url', 'Bible API URL base', |
4890 | + 'http://api.preachingcentral.com/bible.php') |
4891 | + |
4892 | + psalm_pat = re.compile(r'\bpsalm\b', re.IGNORECASE) |
4893 | + |
4894 | + # The API doesn't seem to work with the apocrypha, even when looking in |
4895 | + # versions that include it |
4896 | + books = '|'.join(['Genesis', 'Exodus', 'Leviticus', 'Numbers', 'Deuteronomy', |
4897 | + 'Joshua', 'Judges', 'Ruth', '(?:1|2|I|II) Samuel', '(?:1|2|I|II) Kings', |
4898 | + '(?:1|2|I|II) Chronicles', 'Ezra', 'Nehemiah', 'Esther', 'Job', 'Psalms?', |
4899 | + 'Proverbs', 'Ecclesiastes', 'Song(?: of (?:Songs|Solomon)?)?', |
4900 | + 'Canticles', 'Isaiah', 'Jeremiah', 'Lamentations', |
4901 | + 'Ezekiel', 'Daniel', 'Hosea', 'Joel', 'Amos', 'Obadiah', 'Jonah', 'Micah', |
4902 | + 'Nahum', 'Habakkuk', 'Zephaniah', 'Haggai', 'Zechariah', 'Malachi', |
4903 | + 'Matthew', 'Mark', 'Luke', 'John', 'Acts', 'Romans', |
4904 | + '(?:1|2|I|II) Corinthians', 'Galatians', 'Ephesians', 'Philippians', |
4905 | + 'Colossians', '(?:1|2|I|II) Thessalonians', '(?:1|2|I|II) Timothy', |
4906 | + 'Titus', 'Philemon', 'Hebrews', 'James', '(?:1|2|I|II) Peter', |
4907 | + '(?:1|2|3|I|II|III) John', 'Jude', |
4908 | + 'Revelations?(?: of (?:St.|Saint) John)?']).replace(' ', '\s*') |
4909 | + |
4910 | + @match(r'^bible\s+(.*?)(?:\s+(?:in|from)\s+(.*))?$') |
4911 | + def bible(self, event, passage, version=None): |
4912 | + passage = self.psalm_pat.sub('psalms', passage) |
4913 | + |
4914 | + params = {'passage': passage.encode('utf-8'), |
4915 | + 'type': 'xml', |
4916 | + 'formatting': 'plain'} |
4917 | + if version: |
4918 | + params['version'] = version.lower().encode('utf-8') |
4919 | + |
4920 | + f = urlopen(self.api_url + '?' + urlencode(params)) |
4921 | + tree = ElementTree.parse(f) |
4922 | + |
4923 | + message = self.formatPassage(tree) |
4924 | + if message: |
4925 | + event.addresponse(message) |
4926 | + errors = list(tree.findall('.//error')) |
4927 | + if errors: |
4928 | + event.addresponse('There were errors: %s.', '. '.join(err.text for err in errors)) |
4929 | + elif not message: |
4930 | + event.addresponse("I couldn't find that passage.") |
4931 | + |
4932 | + # Allow queries which are quite definitely bible references to omit "bible". |
4933 | + # Specifically, they must start with the name of a book and be followed only |
4934 | + # by book names, chapters and verses. |
4935 | + @match(r'^((?:(?:' + books + ')(?:\d|[-:,]|\s)*)+?)(?:\s+(?:in|from)\s+(.*))?$') |
4936 | + def bookbible(self, *args): |
4937 | + self.bible(*args) |
4938 | + |
4939 | + def formatPassage(self, xml): |
4940 | + message = [] |
4941 | + oldref = (None, None, None) |
4942 | + for item in xml.findall('.//item'): |
4943 | + ref, text = self.verseInfo(item) |
4944 | + if oldref[0] != ref[0]: |
4945 | + message.append(u'(%s %s:%s)' % ref) |
4946 | + elif oldref[1] != ref[1]: |
4947 | + message.append(u'(%s:%s)' % ref[1:]) |
4948 | + else: |
4949 | + message.append(u'%s' % ref[2]) |
4950 | + oldref = ref |
4951 | + |
4952 | + message.append(text) |
4953 | + |
4954 | + return u' '.join(message) |
4955 | + |
4956 | + def verseInfo(self, xml): |
4957 | + book, chapter, verse, text = map(xml.findtext, |
4958 | + ('bookname', 'chapter', 'verse', 'text')) |
4959 | + return ((book, chapter, verse), text) |
4960 | + |
4961 | +# vi: set et sta sw=4 ts=4: |
4962 | |
4963 | === added file 'ibid/plugins/social.py' |
4964 | --- ibid/plugins/social.py 1970-01-01 00:00:00 +0000 |
4965 | +++ ibid/plugins/social.py 2010-01-18 18:54:16 +0000 |
4966 | @@ -0,0 +1,120 @@ |
4967 | +from urllib2 import HTTPError |
4968 | +from time import time |
4969 | +from datetime import datetime |
4970 | +import re |
4971 | +import logging |
4972 | + |
4973 | +import feedparser |
4974 | + |
4975 | +from ibid.compat import dt_strptime |
4976 | +from ibid.config import DictOption |
4977 | +from ibid.plugins import Processor, match, handler |
4978 | +from ibid.utils import ago, decode_htmlentities, json_webservice |
4979 | + |
4980 | +log = logging.getLogger('plugins.social') |
4981 | +help = {} |
4982 | + |
4983 | +help['lastfm'] = u'Lists the tracks last listened to by the specified user.' |
4984 | +class LastFm(Processor): |
4985 | + u"last.fm for <username>" |
4986 | + |
4987 | + feature = "lastfm" |
4988 | + |
4989 | + @match(r'^last\.?fm\s+for\s+(\S+?)\s*$') |
4990 | + def listsongs(self, event, username): |
4991 | + songs = feedparser.parse('http://ws.audioscrobbler.com/1.0/user/%s/recenttracks.rss?%s' % (username, time())) |
4992 | + if songs['bozo']: |
4993 | + event.addresponse(u'No such user') |
4994 | + else: |
4995 | + event.addresponse(u', '.join(u'%s (%s ago)' % ( |
4996 | + e.title, |
4997 | + ago(event.time - dt_strptime(e.updated, '%a, %d %b %Y %H:%M:%S +0000'), 1) |
4998 | + ) for e in songs['entries'])) |
4999 | + |
5000 | +help["microblog"] = u"Looks up messages on microblogging services like twitter and identica." |
- url needs renaming
- svn,bzr needs marco's thoughts
- crypto needs renaming
- factoid's plurality needs fixing: what does this involve
- lookup isn't even half done