Merge lp:~vhata/ibid/plugin-reorg-399667 into lp:~ibid-core/ibid/old-trunk-1.6

Proposed by Jonathan Hitchcock
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
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.

To post a comment you must log in.
Revision history for this message
Jonathan Hitchcock (vhata) wrote : Posted in a previous version of this proposal

- 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

review: Needs Fixing
Revision history for this message
Jonathan Hitchcock (vhata) wrote : Posted in a previous version of this proposal

I think this is ready...

review: Abstain
Revision history for this message
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/ibid-setup", line 22, in <module>
    from ibid.plugins.auth import hash
ImportError: No module named auth

review: Needs Fixing
Revision history for this message
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__(module)
  File "./ibid/plugins/timezone.py", line 26, in <module>
    help['timezone'] = "Converts times between timezones."
TypeError: '_Helper' object does not support item assignment

Missing a "help = {}"

Revision history for this message
Stefano Rivera (stefanor) wrote : Posted in a previous version of this proposal

scripts/ibid_import looks like it needs import fixes

> ibid/plugins/conversions.py
> 13:log = logging.getLogger('conversions')
make that plugins.conversions

Revision history for this message
Stefano Rivera (stefanor) wrote : Posted in a previous version of this proposal

OK With me. Re-propose for Michael?

review: Approve
Revision history for this message
Stefano Rivera (stefanor) : Posted in a previous version of this proposal
review: Approve
Revision history for this message
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...

Revision history for this message
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/languages ?

Revision history for this message
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://holst.cs.uct.ac.za/~stefanor/tibid-logs/meetings/atrum-%23ibid-2010-01-18-10-05-18.html

lp:~vhata/ibid/plugin-reorg-399667 updated
871. By Jonathan Hitchcock

config plugin is now part of admin

Revision history for this message
Stefano Rivera (stefanor) :
review: Approve
Revision history for this message
Michael Gorven (mgorven) wrote :

 review approve
 status approved

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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."
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches