Merge lp:~ibid-core/ibid/feature-discovery-399667 into lp:~ibid-core/ibid/old-trunk-1.6

Proposed by Stefano Rivera on 2010-02-20
Status: Merged
Approved by: Jonathan Hitchcock on 2010-03-02
Approved revision: 921
Merged at revision: 907
Proposed branch: lp:~ibid-core/ibid/feature-discovery-399667
Merge into: lp:~ibid-core/ibid/old-trunk-1.6
Diff against target: 3065 lines (+1243/-454)
44 files modified
ibid/__init__.py (+1/-0)
ibid/lib/stemmer.py (+367/-0)
ibid/plugins/__init__.py (+24/-0)
ibid/plugins/admin.py (+42/-23)
ibid/plugins/ascii.py (+14/-6)
ibid/plugins/buildbot.py (+6/-4)
ibid/plugins/bzr.py (+6/-3)
ibid/plugins/calc.py (+22/-10)
ibid/plugins/codecontest.py (+11/-3)
ibid/plugins/conversions.py (+25/-13)
ibid/plugins/eval.py (+10/-7)
ibid/plugins/factoid.py (+21/-18)
ibid/plugins/feeds.py (+8/-5)
ibid/plugins/film.py (+13/-7)
ibid/plugins/fun.py (+26/-16)
ibid/plugins/games.py (+22/-19)
ibid/plugins/gameservers.py (+8/-6)
ibid/plugins/geography.py (+25/-13)
ibid/plugins/google.py (+8/-7)
ibid/plugins/help.py (+218/-85)
ibid/plugins/icecast.py (+6/-5)
ibid/plugins/identity.py (+32/-21)
ibid/plugins/karma.py (+10/-7)
ibid/plugins/languages.py (+14/-8)
ibid/plugins/lotto.py (+7/-4)
ibid/plugins/meetings.py (+14/-8)
ibid/plugins/memo.py (+10/-7)
ibid/plugins/memory.py (+7/-6)
ibid/plugins/network.py (+50/-25)
ibid/plugins/oeis.py (+6/-5)
ibid/plugins/quotes.py (+37/-19)
ibid/plugins/rfc.py (+6/-3)
ibid/plugins/seen.py (+7/-4)
ibid/plugins/social.py (+14/-7)
ibid/plugins/sources.py (+23/-13)
ibid/plugins/strings.py (+38/-19)
ibid/plugins/svn.py (+6/-3)
ibid/plugins/sysadmin.py (+25/-13)
ibid/plugins/test.py (+24/-14)
ibid/plugins/trac.py (+6/-3)
ibid/plugins/urlgrab.py (+5/-4)
ibid/plugins/urlinfo.py (+15/-8)
scripts/ibid-plugin (+3/-3)
setup.py (+1/-0)
To merge this branch: bzr merge lp:~ibid-core/ibid/feature-discovery-399667
Reviewer Review Type Date Requested Status
Keegan Carruthers-Smith Abstain on 2010-03-02
Jonathan Hitchcock Approve on 2010-03-02
marcog (community) Approve on 2010-03-02
Michael Gorven 2010-02-20 Approve on 2010-02-23
Review via email: mp+19785@code.launchpad.net
To post a comment you must log in.
Stefano Rivera (stefanor) wrote :

Preliminary review to get you all thinking, testing, and hacking on this.
I'm mostly interested in the help module's interface.

TODOs:
* Do a pass through all hte processors and assign multiple features where appropriate.
* docstring to "usage" attribute conversion.

900. By Stefano Rivera on 2010-02-20

Relocate usage with long descriptions

901. By Stefano Rivera on 2010-02-20

Switch from dostrings -> usage attributes

902. By Stefano Rivera on 2010-02-21

Merge from trunk

903. By Stefano Rivera on 2010-02-21

Inspect isn't needed any more

904. By Stefano Rivera on 2010-02-21

Apply spit-and-polish to help system

Stefano Rivera (stefanor) wrote :

Next round of review, please. helpbid in #ibid

905. By Stefano Rivera on 2010-02-21

Update usage string

906. By Stefano Rivera on 2010-02-21

Satisfy marcog with a silly-person catcher

907. By Stefano Rivera on 2010-02-21

Spelling

Stefano Rivera (stefanor) wrote :

Observed silly use:
21:22 <&Siggi> tibid: can you do Markov?
21:22 < tibid> Siggi: I'll remember that
21:23 <&Siggi> tibid: can you dance?
21:23 < tibid> Siggi: Excuse me?

Michael Gorven (mgorven) :
review: Approve
908. By Stefano Rivera on 2010-02-24

Merge from trunk

909. By Stefano Rivera on 2010-02-24

Merge from trunk

Jonathan Hitchcock (vhata) wrote :

14:58 <Vhata> tibid: how do I use message?
14:58 <tibid> Vhata: Please be more specific. I don't know if you mean memo or memo

review: Needs Fixing
910. By Stefano Rivera on 2010-02-24

Use set for usage search results

Stefano Rivera (stefanor) wrote :

> 14:58 <Vhata> tibid: how do I use message?
> 14:58 <tibid> Vhata: Please be more specific. I don't know if you mean memo or
> memo

Fixed in r910

> 23:24 <casted> features
> 23:24 <tibid> I can: look things up, remember things, deliver messages, play
> games, make decisions, keep an eye on things, browse the
> internet, perform conversions, perform calculations, do silly
> fun stuff, help you with system administration, monitor software
> develpment and do south african stuff
> 23:24 <tibid> Ask me "what can you ..." for more details

s/develpment/development/

Also for "monitor software development" it is non-obvious you have to go "what
can you development". I was trying "what can you monitor" and "what can you
monitor software development". Maybe the second approach should be a fallback.

> 23:28 <tibid> I can look things up with: google, help, unicode, tinyurl,
> weather, imdb, aptitude, seen, factoid, flight, lotto, fml,
> dict, dns, lastfm, translate, oeis, tld, mac, mlia, bash, man,
> apt-file, distance, bible, fortune, microblog, youtube, rfc,
> tvshow, feeds and ports

It would be useful if this was shown in sorted order.

Just played with the feature for a little bit, will review code tomorrow. But for now this is all I can pick up.

review: Needs Fixing
911. By Stefano Rivera on 2010-02-24

Typo

912. By Stefano Rivera on 2010-02-24

Add first ellipsis to "what ... can you ..."

Stefano Rivera (stefanor) wrote :

> s/develpment/development/
r911

> Also for "monitor software development" it is non-obvious you have to go "what
> can you development". I was trying "what can you monitor" and "what can you
> monitor software development". Maybe the second approach should be a fallback.

There's an alternate syntax that helps phrase those questions. See r912

> It would be useful if this was shown in sorted order.

r913

913. By Stefano Rivera on 2010-02-24

Sort features

> 13:15 <tibid> Ask me "what ... can you ..." for more details

What is the purpose of the first ...?

Stefano Rivera (stefanor) wrote :

> What is the purpose of the first ...?

To help the user form valid sentences. It is used in search.

marcog (marco-gallotta) wrote :

We use ellipses here, but the usage strings use <foo> notation. Consistency?

Query: what silly fun stuff do you do?
Response: Got it

Perhaps that syntax should work too?

Query: how do i use rot
Response: I'm afraid I don't know what you are asking about. Ask "what can you do" to browse my features

Would be nice if it suggested "Did you mean rot13"?

Haven't looked much at the code yet though.

review: Needs Fixing
Stefano Rivera (stefanor) wrote :

> We use ellipses here, but the usage strings use <foo> notation. Consistency?

Yeah, not ideal but <brackets> don't seem to work there grammatically.

> Query: what silly fun stuff do you do?
> Response: Got it

There are a hell of a lot of those type of things. I'd say let's ignore them for 0.1.

> Query: how do i use rot
> Response: I'm afraid I don't know what you are asking about. Ask "what can you
> do" to browse my features
>
> Would be nice if it suggested "Did you mean rot13"?

That is possible via substring searches. Don't know how vital it is.

marcog (marco-gallotta) wrote :

> > We use ellipses here, but the usage strings use <foo> notation. Consistency?
>
> Yeah, not ideal but <brackets> don't seem to work there grammatically.

Ok, I'll grudgingly let that slip for now. However, I do feel the inconsistency is quite poor long-term.

> > Query: what silly fun stuff do you do?
> > Response: Got it
>
> There are a hell of a lot of those type of things. I'd say let's ignore them
> for 0.1.

Fair enough.

> > Query: how do i use rot
> > Response: I'm afraid I don't know what you are asking about. Ask "what can
> you
> > do" to browse my features
> >
> > Would be nice if it suggested "Did you mean rot13"?
>
> That is possible via substring searches. Don't know how vital it is.

I think at the very least, we should look for the same feature in plural/singular form (i.e. drop/add an 's'). This is the source of the most failed lookups.

914. By Stefano Rivera on 2010-03-01

Merge from trunk

915. By Stefano Rivera on 2010-03-01

Use a Porter stemmer in help searches. Include a pure-Python implementation until PyStemmer is available in Debian

Stefano Rivera (stefanor) wrote :

> I think at the very least, we should look for the same feature in
> plural/singular form (i.e. drop/add an 's'). This is the source of the most
> failed lookups.

r915

916. By Stefano Rivera on 2010-03-01

Reword usage text, indent usage lines

marcog (marco-gallotta) :
review: Approve
917. By Stefano Rivera on 2010-03-02

Use noun phrases for category descriptions

918. By Stefano Rivera on 2010-03-02

Update ellipsis cather

919. By Stefano Rivera on 2010-03-02

Remove syntactic characters when generating usage keywords

Jonathan Hitchcock (vhata) wrote :

Line 394: "adminstrative functions" - typo

Line 2406: "ategories" - typo

review: Needs Fixing
920. By Stefano Rivera on 2010-03-02

Don't treat keys in 'features' and 'categories' as being correct, they are stemmed

Stefano Rivera (stefanor) wrote :

> Line 394: "adminstrative functions" - typo
>
> Line 2406: "ategories" - typo

r921

921. By Stefano Rivera on 2010-03-02

Typos spotted by Vhata

Jonathan Hitchcock (vhata) wrote :

Proper categorisation -> Phase 2

review: Approve
review: Abstain

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'ibid/__init__.py'
2--- ibid/__init__.py 2010-02-16 09:41:11 +0000
3+++ ibid/__init__.py 2010-03-02 19:03:13 +0000
4@@ -83,6 +83,7 @@
5 config = {}
6 dispatcher = None
7 processors = []
8+categories = {}
9 reloader = None
10 databases = {}
11 auth = None
12
13=== added file 'ibid/lib/stemmer.py'
14--- ibid/lib/stemmer.py 1970-01-01 00:00:00 +0000
15+++ ibid/lib/stemmer.py 2010-03-02 19:03:13 +0000
16@@ -0,0 +1,367 @@
17+#!/usr/bin/env python
18+
19+"""Porter Stemming Algorithm
20+This is the Porter stemming algorithm, ported to Python from the
21+version coded up in ANSI C by the author. It may be be regarded
22+as canonical, in that it follows the algorithm presented in
23+
24+Porter, 1980, An algorithm for suffix stripping, Program, Vol. 14,
25+no. 3, pp 130-137,
26+
27+only differing from it at the points maked --DEPARTURE-- below.
28+
29+See also http://www.tartarus.org/~martin/PorterStemmer
30+
31+The algorithm as described in the paper could be exactly replicated
32+by adjusting the points of DEPARTURE, but this is barely necessary,
33+because (a) the points of DEPARTURE are definitely improvements, and
34+(b) no encoding of the Porter stemmer I have seen is anything like
35+as exact as this version, even with the points of DEPARTURE!
36+
37+Vivake Gupta (v@nano.com)
38+
39+Release 1: January 2001
40+
41+Further adjustments by Santiago Bruno (bananabruno@gmail.com)
42+to allow word input not restricted to one word per line, leading
43+to:
44+
45+release 2: July 2008
46+"""
47+
48+import sys
49+
50+class PorterStemmer:
51+
52+ def __init__(self):
53+ """The main part of the stemming algorithm starts here.
54+ b is a buffer holding a word to be stemmed. The letters are in b[k0],
55+ b[k0+1] ... ending at b[k]. In fact k0 = 0 in this demo program. k is
56+ readjusted downwards as the stemming progresses. Zero termination is
57+ not in fact used in the algorithm.
58+
59+ Note that only lower case sequences are stemmed. Forcing to lower case
60+ should be done before stem(...) is called.
61+ """
62+
63+ self.b = "" # buffer for word to be stemmed
64+ self.k = 0
65+ self.k0 = 0
66+ self.j = 0 # j is a general offset into the string
67+
68+ def cons(self, i):
69+ """cons(i) is TRUE <=> b[i] is a consonant."""
70+ if self.b[i] == 'a' or self.b[i] == 'e' or self.b[i] == 'i' or self.b[i] == 'o' or self.b[i] == 'u':
71+ return 0
72+ if self.b[i] == 'y':
73+ if i == self.k0:
74+ return 1
75+ else:
76+ return (not self.cons(i - 1))
77+ return 1
78+
79+ def m(self):
80+ """m() measures the number of consonant sequences between k0 and j.
81+ if c is a consonant sequence and v a vowel sequence, and <..>
82+ indicates arbitrary presence,
83+
84+ <c><v> gives 0
85+ <c>vc<v> gives 1
86+ <c>vcvc<v> gives 2
87+ <c>vcvcvc<v> gives 3
88+ ....
89+ """
90+ n = 0
91+ i = self.k0
92+ while 1:
93+ if i > self.j:
94+ return n
95+ if not self.cons(i):
96+ break
97+ i = i + 1
98+ i = i + 1
99+ while 1:
100+ while 1:
101+ if i > self.j:
102+ return n
103+ if self.cons(i):
104+ break
105+ i = i + 1
106+ i = i + 1
107+ n = n + 1
108+ while 1:
109+ if i > self.j:
110+ return n
111+ if not self.cons(i):
112+ break
113+ i = i + 1
114+ i = i + 1
115+
116+ def vowelinstem(self):
117+ """vowelinstem() is TRUE <=> k0,...j contains a vowel"""
118+ for i in range(self.k0, self.j + 1):
119+ if not self.cons(i):
120+ return 1
121+ return 0
122+
123+ def doublec(self, j):
124+ """doublec(j) is TRUE <=> j,(j-1) contain a double consonant."""
125+ if j < (self.k0 + 1):
126+ return 0
127+ if (self.b[j] != self.b[j-1]):
128+ return 0
129+ return self.cons(j)
130+
131+ def cvc(self, i):
132+ """cvc(i) is TRUE <=> i-2,i-1,i has the form consonant - vowel - consonant
133+ and also if the second c is not w,x or y. this is used when trying to
134+ restore an e at the end of a short e.g.
135+
136+ cav(e), lov(e), hop(e), crim(e), but
137+ snow, box, tray.
138+ """
139+ if i < (self.k0 + 2) or not self.cons(i) or self.cons(i-1) or not self.cons(i-2):
140+ return 0
141+ ch = self.b[i]
142+ if ch == 'w' or ch == 'x' or ch == 'y':
143+ return 0
144+ return 1
145+
146+ def ends(self, s):
147+ """ends(s) is TRUE <=> k0,...k ends with the string s."""
148+ length = len(s)
149+ if s[length - 1] != self.b[self.k]: # tiny speed-up
150+ return 0
151+ if length > (self.k - self.k0 + 1):
152+ return 0
153+ if self.b[self.k-length+1:self.k+1] != s:
154+ return 0
155+ self.j = self.k - length
156+ return 1
157+
158+ def setto(self, s):
159+ """setto(s) sets (j+1),...k to the characters in the string s, readjusting k."""
160+ length = len(s)
161+ self.b = self.b[:self.j+1] + s + self.b[self.j+length+1:]
162+ self.k = self.j + length
163+
164+ def r(self, s):
165+ """r(s) is used further down."""
166+ if self.m() > 0:
167+ self.setto(s)
168+
169+ def step1ab(self):
170+ """step1ab() gets rid of plurals and -ed or -ing. e.g.
171+
172+ caresses -> caress
173+ ponies -> poni
174+ ties -> ti
175+ caress -> caress
176+ cats -> cat
177+
178+ feed -> feed
179+ agreed -> agree
180+ disabled -> disable
181+
182+ matting -> mat
183+ mating -> mate
184+ meeting -> meet
185+ milling -> mill
186+ messing -> mess
187+
188+ meetings -> meet
189+ """
190+ if self.b[self.k] == 's':
191+ if self.ends("sses"):
192+ self.k = self.k - 2
193+ elif self.ends("ies"):
194+ self.setto("i")
195+ elif self.b[self.k - 1] != 's':
196+ self.k = self.k - 1
197+ if self.ends("eed"):
198+ if self.m() > 0:
199+ self.k = self.k - 1
200+ elif (self.ends("ed") or self.ends("ing")) and self.vowelinstem():
201+ self.k = self.j
202+ if self.ends("at"): self.setto("ate")
203+ elif self.ends("bl"): self.setto("ble")
204+ elif self.ends("iz"): self.setto("ize")
205+ elif self.doublec(self.k):
206+ self.k = self.k - 1
207+ ch = self.b[self.k]
208+ if ch == 'l' or ch == 's' or ch == 'z':
209+ self.k = self.k + 1
210+ elif (self.m() == 1 and self.cvc(self.k)):
211+ self.setto("e")
212+
213+ def step1c(self):
214+ """step1c() turns terminal y to i when there is another vowel in the stem."""
215+ if (self.ends("y") and self.vowelinstem()):
216+ self.b = self.b[:self.k] + 'i' + self.b[self.k+1:]
217+
218+ def step2(self):
219+ """step2() maps double suffices to single ones.
220+ so -ization ( = -ize plus -ation) maps to -ize etc. note that the
221+ string before the suffix must give m() > 0.
222+ """
223+ if self.b[self.k - 1] == 'a':
224+ if self.ends("ational"): self.r("ate")
225+ elif self.ends("tional"): self.r("tion")
226+ elif self.b[self.k - 1] == 'c':
227+ if self.ends("enci"): self.r("ence")
228+ elif self.ends("anci"): self.r("ance")
229+ elif self.b[self.k - 1] == 'e':
230+ if self.ends("izer"): self.r("ize")
231+ elif self.b[self.k - 1] == 'l':
232+ if self.ends("bli"): self.r("ble") # --DEPARTURE--
233+ # To match the published algorithm, replace this phrase with
234+ # if self.ends("abli"): self.r("able")
235+ elif self.ends("alli"): self.r("al")
236+ elif self.ends("entli"): self.r("ent")
237+ elif self.ends("eli"): self.r("e")
238+ elif self.ends("ousli"): self.r("ous")
239+ elif self.b[self.k - 1] == 'o':
240+ if self.ends("ization"): self.r("ize")
241+ elif self.ends("ation"): self.r("ate")
242+ elif self.ends("ator"): self.r("ate")
243+ elif self.b[self.k - 1] == 's':
244+ if self.ends("alism"): self.r("al")
245+ elif self.ends("iveness"): self.r("ive")
246+ elif self.ends("fulness"): self.r("ful")
247+ elif self.ends("ousness"): self.r("ous")
248+ elif self.b[self.k - 1] == 't':
249+ if self.ends("aliti"): self.r("al")
250+ elif self.ends("iviti"): self.r("ive")
251+ elif self.ends("biliti"): self.r("ble")
252+ elif self.b[self.k - 1] == 'g': # --DEPARTURE--
253+ if self.ends("logi"): self.r("log")
254+ # To match the published algorithm, delete this phrase
255+
256+ def step3(self):
257+ """step3() dels with -ic-, -full, -ness etc. similar strategy to step2."""
258+ if self.b[self.k] == 'e':
259+ if self.ends("icate"): self.r("ic")
260+ elif self.ends("ative"): self.r("")
261+ elif self.ends("alize"): self.r("al")
262+ elif self.b[self.k] == 'i':
263+ if self.ends("iciti"): self.r("ic")
264+ elif self.b[self.k] == 'l':
265+ if self.ends("ical"): self.r("ic")
266+ elif self.ends("ful"): self.r("")
267+ elif self.b[self.k] == 's':
268+ if self.ends("ness"): self.r("")
269+
270+ def step4(self):
271+ """step4() takes off -ant, -ence etc., in context <c>vcvc<v>."""
272+ if self.b[self.k - 1] == 'a':
273+ if self.ends("al"): pass
274+ else: return
275+ elif self.b[self.k - 1] == 'c':
276+ if self.ends("ance"): pass
277+ elif self.ends("ence"): pass
278+ else: return
279+ elif self.b[self.k - 1] == 'e':
280+ if self.ends("er"): pass
281+ else: return
282+ elif self.b[self.k - 1] == 'i':
283+ if self.ends("ic"): pass
284+ else: return
285+ elif self.b[self.k - 1] == 'l':
286+ if self.ends("able"): pass
287+ elif self.ends("ible"): pass
288+ else: return
289+ elif self.b[self.k - 1] == 'n':
290+ if self.ends("ant"): pass
291+ elif self.ends("ement"): pass
292+ elif self.ends("ment"): pass
293+ elif self.ends("ent"): pass
294+ else: return
295+ elif self.b[self.k - 1] == 'o':
296+ if self.ends("ion") and (self.b[self.j] == 's' or self.b[self.j] == 't'): pass
297+ elif self.ends("ou"): pass
298+ # takes care of -ous
299+ else: return
300+ elif self.b[self.k - 1] == 's':
301+ if self.ends("ism"): pass
302+ else: return
303+ elif self.b[self.k - 1] == 't':
304+ if self.ends("ate"): pass
305+ elif self.ends("iti"): pass
306+ else: return
307+ elif self.b[self.k - 1] == 'u':
308+ if self.ends("ous"): pass
309+ else: return
310+ elif self.b[self.k - 1] == 'v':
311+ if self.ends("ive"): pass
312+ else: return
313+ elif self.b[self.k - 1] == 'z':
314+ if self.ends("ize"): pass
315+ else: return
316+ else:
317+ return
318+ if self.m() > 1:
319+ self.k = self.j
320+
321+ def step5(self):
322+ """step5() removes a final -e if m() > 1, and changes -ll to -l if
323+ m() > 1.
324+ """
325+ self.j = self.k
326+ if self.b[self.k] == 'e':
327+ a = self.m()
328+ if a > 1 or (a == 1 and not self.cvc(self.k-1)):
329+ self.k = self.k - 1
330+ if self.b[self.k] == 'l' and self.doublec(self.k) and self.m() > 1:
331+ self.k = self.k -1
332+
333+ def stem(self, p, i, j):
334+ """In stem(p,i,j), p is a char pointer, and the string to be stemmed
335+ is from p[i] to p[j] inclusive. Typically i is zero and j is the
336+ offset to the last character of a string, (p[j+1] == '\0'). The
337+ stemmer adjusts the characters p[i] ... p[j] and returns the new
338+ end-point of the string, k. Stemming never increases word length, so
339+ i <= k <= j. To turn the stemmer into a module, declare 'stem' as
340+ extern, and delete the remainder of this file.
341+ """
342+ # copy the parameters into statics
343+ self.b = p
344+ self.k = j
345+ self.k0 = i
346+ if self.k <= self.k0 + 1:
347+ return self.b # --DEPARTURE--
348+
349+ # With this line, strings of length 1 or 2 don't go through the
350+ # stemming process, although no mention is made of this in the
351+ # published algorithm. Remove the line to match the published
352+ # algorithm.
353+
354+ self.step1ab()
355+ self.step1c()
356+ self.step2()
357+ self.step3()
358+ self.step4()
359+ self.step5()
360+ return self.b[self.k0:self.k+1]
361+
362+
363+if __name__ == '__main__':
364+ p = PorterStemmer()
365+ if len(sys.argv) > 1:
366+ for f in sys.argv[1:]:
367+ infile = open(f, 'r')
368+ while 1:
369+ output = ''
370+ word = ''
371+ line = infile.readline()
372+ if line == '':
373+ break
374+ for c in line:
375+ if c.isalpha():
376+ word += c.lower()
377+ else:
378+ if word:
379+ output += p.stem(word, 0,len(word)-1)
380+ word = ''
381+ output += c.lower()
382+ print output,
383+ infile.close()
384
385=== modified file 'ibid/plugins/__init__.py'
386--- ibid/plugins/__init__.py 2010-02-23 11:45:13 +0000
387+++ ibid/plugins/__init__.py 2010-03-02 19:03:13 +0000
388@@ -27,6 +27,30 @@
389
390 __path__ = pluginPackagePaths(__name__) + __path__
391
392+for cat, desc, weight in (
393+ ('account', u'bot accounts and permissions', None),
394+ ('admin', u'administrative functions', None),
395+ ('calculate', u'calculations', 0),
396+ ('convert', u'conversions', 0),
397+ ('debug', u'debugging me', None),
398+ ('decide', u'decisions', -2),
399+ ('development', u'software development', 10),
400+ ('fun', u'silly fun stuff', 0),
401+ ('game', u'games', -2),
402+ ('lookup', u'looking things up', -10),
403+ ('monitor', u'monitoring things', -2),
404+ ('remember', u'remembering things', -5),
405+ ('web', u'browsing the Internet', 0),
406+ ('message', u'delivering messages', -5),
407+ ('south africa', u'South African stuff', 10),
408+ ('sysadmin', u'System Administration', 5),
409+ ):
410+ if cat not in ibid.categories:
411+ ibid.categories[cat] = {
412+ 'description': desc,
413+ 'weight': weight,
414+ }
415+
416 class Processor(object):
417 """Base class for Ibid plugins.
418 Processors receive events and (optionally) do things with them.
419
420=== modified file 'ibid/plugins/admin.py'
421--- ibid/plugins/admin.py 2010-01-18 23:20:33 +0000
422+++ ibid/plugins/admin.py 2010-03-02 19:03:13 +0000
423@@ -13,12 +13,15 @@
424
425 log = logging.getLogger('plugins.admin')
426
427-help = {}
428+features = {}
429
430-help['plugins'] = u'Lists, loads and unloads plugins.'
431+features['plugins'] = {
432+ 'description': u'Lists, loads and unloads plugins.',
433+ 'categories': ('admin',),
434+}
435 class ListPLugins(Processor):
436- u"""list plugins"""
437- feature = 'plugins'
438+ usage = u'list plugins'
439+ feature = ('plugins',)
440
441 @match(r'^lsmod|list\s+plugins$')
442 def handler(self, event):
443@@ -29,10 +32,13 @@
444
445 event.addresponse(u'Plugins: %s', human_join(sorted(plugins)) or u'none')
446
447-help['core'] = u'Reloads core modules.'
448+features['core'] = {
449+ 'description': u'Reloads core modules.',
450+ 'categories': ('admin',),
451+}
452 class ReloadCoreModules(Processor):
453- u"""reload (reloader|dispatcher|databases|auth)"""
454- feature = 'core'
455+ usage = u'reload (reloader|dispatcher|databases|auth)'
456+ feature = ('core',)
457
458 priority = -5
459 permission = u'core'
460@@ -49,8 +55,8 @@
461 event.addresponse(result and u'%s reloaded' or u"Couldn't reload %s", module)
462
463 class LoadModules(Processor):
464- u"""(load|unload|reload) <plugin|processor>"""
465- feature = 'plugins'
466+ usage = u'(load|unload|reload) <plugin|processor>'
467+ feature = ('plugins',)
468
469 permission = u'plugins'
470
471@@ -67,10 +73,13 @@
472 result = ibid.reloader.unload_processor(plugin)
473 event.addresponse(result and u'%s unloaded' or u"Couldn't unload %s", plugin)
474
475-help['die'] = u'Terminates the bot'
476+features['die'] = {
477+ 'description': u'Terminates the bot',
478+ 'categories': ('admin',),
479+}
480 class Die(Processor):
481- u"""die"""
482- feature = 'die'
483+ usage = u'die'
484+ feature = ('die',)
485
486 permission = u'admin'
487
488@@ -79,11 +88,14 @@
489 def die(self, event):
490 reactor.stop()
491
492-help['sources'] = u'Controls and lists the configured sources.'
493+features['sources'] = {
494+ 'description': u'Controls and lists the configured sources.',
495+ 'categories': ('admin',),
496+}
497 class Admin(Processor):
498- u"""(connect|disconnect) (to|from) <source>
499+ usage = u"""(connect|disconnect) (to|from) <source>
500 load <source> source"""
501- feature = 'sources'
502+ feature = ('sources',)
503
504 permission = u'sources'
505
506@@ -116,8 +128,8 @@
507 event.addresponse(u"Couldn't load %s source", source)
508
509 class Info(Processor):
510- u"""(sources|list configured sources)"""
511- feature = 'sources'
512+ usage = u'(sources|list configured sources)'
513+ feature = ('sources',)
514
515 @match(r'^sources$')
516 def list(self, event):
517@@ -131,10 +143,13 @@
518 def listall(self, event):
519 event.addresponse(u'Configured sources: %s', human_join(sorted(ibid.config.sources.keys())) or u'none')
520
521-help['version'] = u"Show the Ibid version currently running"
522+features['version'] = {
523+ 'description': u'Show the Ibid version currently running',
524+ 'categories': ('admin',),
525+}
526 class Version(Processor):
527- u"""version"""
528- feature = 'version'
529+ usage = u'version'
530+ feature = ('version',)
531
532 @match(r'^version$')
533 def show_version(self, event):
534@@ -143,12 +158,16 @@
535 else:
536 event.addresponse(u"I don't know what version I am :-(")
537
538-help['config'] = u'Gets and sets configuration settings, and rereads the configuration file.'
539+features['config'] = {
540+ 'description': u'Gets and sets configuration settings, and rereads the '
541+ u'configuration file.',
542+ 'categories': ('admin',),
543+}
544 class Config(Processor):
545- u"""reread config
546+ usage = u"""reread config
547 set config <name> to <value>
548 get config <name>"""
549- feature = 'config'
550+ feature = ('config',)
551
552 priority = -10
553 permission = u'config'
554
555=== modified file 'ibid/plugins/ascii.py'
556--- ibid/plugins/ascii.py 2010-01-24 18:38:02 +0000
557+++ ibid/plugins/ascii.py 2010-03-02 19:03:13 +0000
558@@ -27,11 +27,15 @@
559 * pyfiglet (http://sourceforge.net/projects/pyfiglet/)
560 """
561
562-help = { 'draw' : u'Retrieve images from the web and render them in ascii-art.',
563- 'figlet': u'Render text in ascii-art using figlet.' }
564+features = {}
565+
566+features['draw-aa'] = {
567+ 'description': u'Retrieve images from the web and render them in ASCII-art.',
568+ 'categories': ('fun', 'web',),
569+}
570 class DrawImage(Processor):
571- u"""draw <url> [in colour] [width <width>] [height <height>]"""
572- feature = 'draw'
573+ usage = u'draw <url> [in colour] [width <width>] [height <height>]'
574+ feature = ('draw-aa',)
575
576 max_filesize = IntOption('max_filesize', 'Only request this many KiB', 200)
577 def_height = IntOption('def_height', 'Default height for libaa output', 10)
578@@ -142,10 +146,14 @@
579 else:
580 event.addresponse(u"Sorry, that doesn't look like an image")
581
582+features['figlet'] = {
583+ 'description': u'Render text in ASCII-art using figlet.',
584+ 'categories': ('fun',),
585+}
586 class WriteFiglet(Processor):
587- u"""figlet <text> [in <font>]
588+ usage = u"""figlet <text> [in <font>]
589 list figlet fonts [from <index>]"""
590- feature = 'figlet'
591+ feature = ('figlet',)
592
593 max_width = IntOption('max_width', 'Maximum width for ascii output', 60)
594 fonts_ = Option('fonts', 'Directory or Zip file containing figlet fonts',
595
596=== modified file 'ibid/plugins/buildbot.py'
597--- ibid/plugins/buildbot.py 2010-01-18 23:20:33 +0000
598+++ ibid/plugins/buildbot.py 2010-03-02 19:03:13 +0000
599@@ -9,11 +9,13 @@
600 from ibid.plugins import Processor, match, RPC
601 from ibid.config import Option, IntOption
602
603-help = {'buildbot': u'Displays buildbot build results and triggers builds.'}
604-
605+features = {'buildbot': {
606+ 'description': u'Displays buildbot build results and triggers builds.',
607+ 'categories': ('development',),
608+}}
609 class BuildBot(Processor, RPC):
610- u"""rebuild <branch> [ (revision|r) <number> ]"""
611- feature = 'buildbot'
612+ usage = u'rebuild <branch> [ (revision|r) <number> ]'
613+ feature = ('buildbot',)
614 autoload = False
615
616 server = Option('server', 'Buildbot server hostname', 'localhost')
617
618=== modified file 'ibid/plugins/bzr.py'
619--- ibid/plugins/bzr.py 2010-02-20 07:38:38 +0000
620+++ ibid/plugins/bzr.py 2010-03-02 19:03:13 +0000
621@@ -14,7 +14,10 @@
622 from ibid.config import DictOption, IntOption
623 from ibid.utils import ago, format_date, human_join
624
625-help = {'bzr': u'Retrieves commit logs from a Bazaar repository.'}
626+features = {'bzr': {
627+ 'description': u'Retrieves commit logs from a Bazaar repository.',
628+ 'categories': ('development',),
629+}}
630
631 class LogFormatter(log.LogFormatter):
632
633@@ -57,9 +60,9 @@
634 self.to_file.write(commit)
635
636 class Bazaar(Processor, RPC):
637- u"""(last commit|commit <revno>) [to <repo>] [full]
638+ usage = u"""(last commit|commit <revno>) [to <repo>] [full]
639 repositories"""
640- feature = 'bzr'
641+ feature = ('bzr',)
642 autoload = False
643
644 repositories = DictOption('repositories', 'Dict of repositories names and URLs')
645
646=== modified file 'ibid/plugins/calc.py'
647--- ibid/plugins/calc.py 2010-02-24 12:47:25 +0000
648+++ ibid/plugins/calc.py 2010-03-02 19:03:13 +0000
649@@ -27,14 +27,17 @@
650 class FD_ExpressionCodeGenerator(pycodegen.ExpressionCodeGenerator):
651 futures = ('division',)
652
653-help = {}
654+features = {}
655 log = logging.getLogger('calc')
656
657-help['bc'] = u'Calculate mathematical expressions using bc'
658+features['bc'] = {
659+ 'description': u'Calculate mathematical expressions using bc',
660+ 'categories': ('calculate',),
661+}
662 class BC(Processor):
663- u"""bc <expression>"""
664+ usage = u'bc <expression>'
665
666- feature = 'bc'
667+ feature = ('bc',)
668
669 bc = Option('bc', 'Path to bc executable', 'bc')
670 bc_timeout = FloatOption('bc_timeout', 'Maximum BC execution time (sec)', 2.0)
671@@ -78,7 +81,11 @@
672 error = unicode_output(error.strip())
673 raise Exception("BC Error: %s" % error)
674
675-help['calc'] = u'Returns the anwser to mathematical expressions. Uses Python syntax and semantics (i.e. radians)'
676+features['calc'] = {
677+ 'description': u'Returns the anwser to mathematical expressions. '
678+ u'Uses Python syntax and semantics (i.e. radians)',
679+ 'categories': ('calculate',),
680+}
681 class LimitException(Exception):
682 pass
683
684@@ -133,8 +140,8 @@
685 raise AccessException
686
687 class Calc(Processor):
688- u"""[calc] <expression>"""
689- feature = 'calc'
690+ usage = u'<expression>'
691+ feature = ('calc',)
692
693 priority = 500
694
695@@ -191,16 +198,21 @@
696
697
698 class ExplicitCalc(Calc):
699+ usage = u'calc <expression>'
700 priority = 0
701
702 @match(r'^calc(?:ulate)?\s+(.+)$')
703 def calculate(self, event, expression):
704 super(ExplicitCalc, self).calculate(event, expression)
705
706-help['random'] = u'Generates random numbers.'
707+
708+features['random'] = {
709+ 'description': u'Generates random numbers.',
710+ 'categories': ('calculate', 'fun',),
711+}
712 class Random(Processor):
713- u"""random [ <max> | <min> <max> ]"""
714- feature = 'random'
715+ usage = u'random [ <max> | <min> <max> ]'
716+ feature = ('random',)
717
718 @match('^rand(?:om)?(?:\s+(\d+)(?:\s+(\d+))?)?$')
719 def random(self, event, begin, end):
720
721=== modified file 'ibid/plugins/codecontest.py'
722--- ibid/plugins/codecontest.py 2010-01-25 18:31:04 +0000
723+++ ibid/plugins/codecontest.py 2010-03-02 19:03:13 +0000
724@@ -13,7 +13,15 @@
725 from ibid.utils import cacheable_download
726 from ibid.utils.html import get_html_parse_tree
727
728-help = {u'usaco': u'Query USACO sections, divisions and more. Since this info is private, users are required to provide their USACO password when linking their USACO account to their ibid account and only linked accounts can be queried. Your password is used only to confirm that the account is yours and is discarded immediately.'}
729+features = {'usaco': {
730+ 'description': u'Query USACO sections, divisions and more. Since this info '
731+ u'is private, users are required to provide their USACO '
732+ u'password when linking their USACO account to their ibid '
733+ u'account and only linked accounts can be queried. Your '
734+ u'password is used only to confirm that the account is '
735+ u'yours and is discarded immediately.',
736+ 'categories': ('monitor',),
737+}}
738
739 class UsacoException(Exception):
740 def __init__(self, msg):
741@@ -23,14 +31,14 @@
742 return unicode(self.msg)
743
744 class Usaco(Processor):
745- """usaco <section|division> for <user>
746+ usage = u"""usaco <section|division> for <user>
747 usaco <contest> results [for <name|user>]
748 (i am|<user> is) <usaco_username> on usaco [password <usaco_password>]"""
749
750 admin_user = Option('admin_user', 'Admin user on USACO', None)
751 admin_password = Option('admin_password', 'Admin password on USACO', None)
752
753- feature = 'usaco'
754+ feature = ('usaco',)
755 # Clashes with identity, so lower our priority since if we match, then
756 # this is the better match
757 priority = -20
758
759=== modified file 'ibid/plugins/conversions.py'
760--- ibid/plugins/conversions.py 2010-02-06 14:26:08 +0000
761+++ ibid/plugins/conversions.py 2010-03-02 19:03:13 +0000
762@@ -13,16 +13,19 @@
763 from ibid.utils import file_in_path, unicode_output, human_join
764 from ibid.utils.html import get_country_codes, get_html_parse_tree
765
766-help = {}
767+features = {}
768 log = logging.getLogger('plugins.conversions')
769
770-help['base'] = u'Convert numbers between bases (radixes)'
771+features['base'] = {
772+ 'description': u'Convert numbers between bases (radixes)',
773+ 'categories': ('calculate', 'convert',),
774+}
775 class BaseConvert(Processor):
776- u"""[convert] <number> [from base <number>] to base <number>
777+ usage = u"""[convert] <number> [from base <number>] to base <number>
778 [convert] ascii <text> to base <number>
779 [convert] <sequence> from base <number> to ascii"""
780
781- feature = "base"
782+ feature = ('base',)
783
784 abbr_named_bases = {
785 "hex": 16,
786@@ -212,10 +215,13 @@
787 if base_from == 64 and [True for plugin in ibid.processors if getattr(plugin, 'feature', None) == 'base64']:
788 event.addresponse(u'If you want a base64 encoding, use the "base64" feature')
789
790-help['units'] = 'Converts values between various units.'
791+features['units'] = {
792+ 'description': 'Converts values between various units.',
793+ 'categories': ('convert',),
794+}
795 class Units(Processor):
796- u"""convert [<value>] <unit> to <unit>"""
797- feature = 'units'
798+ usage = u'convert [<value>] <unit> to <unit>'
799+ feature = ('units',)
800 priority = 10
801
802 units = Option('units', 'Path to units executable', 'units')
803@@ -288,12 +294,15 @@
804 else:
805 event.addresponse(u"I can't do that: %s", result)
806
807-help['currency'] = u'Converts amounts between currencies.'
808+features['currency'] = {
809+ 'description': u'Converts amounts between currencies.',
810+ 'categories': ('convert',),
811+}
812 class Currency(Processor):
813- u"""exchange <amount> <currency> for <currency>
814+ usage = u"""exchange <amount> <currency> for <currency>
815 currencies for <country>"""
816
817- feature = "currency"
818+ feature = ('currency',)
819
820 headers = {'User-Agent': 'Mozilla/5.0', 'Referer': 'http://www.xe.com/'}
821 currencies = {}
822@@ -415,12 +424,15 @@
823
824 class UnassignedCharacter(Exception): pass
825
826-help['unicode'] = """Look up characters in the Unicode database."""
827+features['unicode'] = {
828+ 'description': u'Look up characters in the Unicode database.',
829+ 'categories': ('lookup', 'convert',),
830+}
831 class UnicodeData(Processor):
832- """U+<hex code>
833+ usage = u"""U+<hex code>
834 unicode (<character>|<character name>|<decimal code>|0x<hex code>)"""
835
836- feature = 'unicode'
837+ feature = ('unicode',)
838
839 bidis = {'AL': u'right-to-left Arabic', 'AN': u'Arabic number',
840 'B': u'paragraph separator', 'BN': u'boundary neutral',
841
842=== modified file 'ibid/plugins/eval.py'
843--- ibid/plugins/eval.py 2010-01-18 23:20:33 +0000
844+++ ibid/plugins/eval.py 2010-03-02 19:03:13 +0000
845@@ -13,11 +13,14 @@
846
847 from ibid.plugins import Processor, match, authorise
848
849-help = {'eval': u'Evaluates Python, Perl and Lua code.'}
850+features = {'eval': {
851+ 'description': u'Evaluates Python, Perl and Lua code.',
852+ 'categories': ('debug',),
853+}}
854
855 class Python(Processor):
856- u"""py <code>"""
857- feature = 'eval'
858+ usage = u'py <code>'
859+ feature = ('eval',)
860
861 permission = u'eval'
862
863@@ -36,8 +39,8 @@
864 event.addresponse(repr(result))
865
866 class Perl(Processor):
867- u"""pl <code>"""
868- feature = 'eval'
869+ usage = u'pl <code>'
870+ feature = ('eval',)
871
872 permission = u'eval'
873
874@@ -52,8 +55,8 @@
875 event.addresponse(repr(result))
876
877 class Lua(Processor):
878- u"""lua <code>"""
879- feature = 'eval'
880+ usage = u'lua <code>'
881+ feature = ('eval',)
882
883 permission = u'eval'
884
885
886=== modified file 'ibid/plugins/factoid.py'
887--- ibid/plugins/factoid.py 2010-02-21 19:50:14 +0000
888+++ ibid/plugins/factoid.py 2010-03-02 19:03:13 +0000
889@@ -18,9 +18,14 @@
890 from ibid.plugins.identity import get_identities
891 from ibid.utils import format_date
892
893-help = {'factoid': u'Factoids are arbitrary pieces of information stored by a key. '
894- u'Factoids beginning with a command such as "<action>" or "<reply>" will supress the "name verb value" output. '
895- u"Search and replace functions won't use real regexs unless appended with the 'r' flag."}
896+features = {'factoid': {
897+ 'description': u'Factoids are arbitrary pieces of information stored by a '
898+ u'key. Factoids beginning with a command such as "<action>" '
899+ u'or "<reply>" will supress the "name verb value" output. '
900+ u"Search and replace functions won't use real regexs unless "
901+ u"appended with the 'r' flag.",
902+ 'categories': ('lookup', 'remember',),
903+}}
904
905 log = logging.getLogger('plugins.factoid')
906
907@@ -261,8 +266,8 @@
908 return []
909
910 class Utils(Processor):
911- u"""literal <name> [( #<from number> | /<pattern>/[r] )]"""
912- feature = 'factoid'
913+ usage = u'literal <name> [( #<from number> | /<pattern>/[r] )]'
914+ feature = ('factoid',)
915
916 @match(r'^literal\s+(.+?)(?:\s+#(\d+)|\s+(?:/(.+?)/(r?)))?$')
917 def literal(self, event, name, number, pattern, is_regex):
918@@ -275,9 +280,9 @@
919 for index, (factoid, name, value) in enumerate(factoids)))
920
921 class Forget(Processor):
922- u"""forget <name> [( #<number> | /<pattern>/[r] )]
923+ usage = u"""forget <name> [( #<number> | /<pattern>/[r] )]
924 <name> is the same as <other name>"""
925- feature = 'factoid'
926+ feature = ('factoid',)
927
928 priority = 10
929 permission = u'factoid'
930@@ -372,8 +377,8 @@
931 event.addresponse(u"I don't know about %s", source)
932
933 class Search(Processor):
934- u"""search [for] [<limit>] [(facts|values) [containing]] (<pattern>|/<pattern>/[r]) [from <start>]"""
935- feature = 'factoid'
936+ usage = u'search [for] [<limit>] [(facts|values) [containing]] (<pattern>|/<pattern>/[r]) [from <start>]'
937+ feature = ('factoid',)
938
939 limit = IntOption('search_limit', u'Maximum number of results to return', 30)
940 default = IntOption('search_default', u'Default number of results to return', 10)
941@@ -447,8 +452,8 @@
942 return message
943
944 class Get(Processor, RPC):
945- u"""<factoid> [( #<number> | /<pattern>/[r] )]"""
946- feature = 'factoid'
947+ usage = u'<factoid> [( #<number> | /<pattern>/[r] )]'
948+ feature = ('factoid',)
949
950 priority = 200
951
952@@ -498,11 +503,9 @@
953 return reply
954
955 class Set(Processor):
956- u"""
957- <name> (<verb>|=<verb>=) [also] <value>
958- last set factoid
959- """
960- feature = 'factoid'
961+ usage = u"""<name> (<verb>|=<verb>=) [also] <value>
962+ last set factoid"""
963+ feature = ('factoid',)
964
965 interrogatives = ListOption('interrogatives', 'Question words to strip', default_interrogatives)
966 verbs = ListOption('verbs', 'Verbs that split name from value', default_verbs)
967@@ -578,9 +581,9 @@
968 event.addresponse(u'It was: %s', self.last_set_factoid)
969
970 class Modify(Processor):
971- u"""<name> [( #<number> | /<pattern>/[r] )] += <suffix>
972+ usage = u"""<name> [( #<number> | /<pattern>/[r] )] += <suffix>
973 <name> [( #<number> | /<pattern>/[r] )] ~= ( s/<regex>/<replacement>/[g][i][r] | y/<source>/<dest>/ )"""
974- feature = 'factoid'
975+ feature = ('factoid',)
976
977 permission = u'factoid'
978 permissions = (u'factoidadmin',)
979
980=== modified file 'ibid/plugins/feeds.py'
981--- ibid/plugins/feeds.py 2010-01-18 23:20:33 +0000
982+++ ibid/plugins/feeds.py 2010-03-02 19:03:13 +0000
983@@ -16,7 +16,10 @@
984 from ibid.utils import cacheable_download, human_join
985 from ibid.utils.html import get_html_parse_tree
986
987-help = {'feeds': u'Displays articles from RSS and Atom feeds'}
988+features = {'feeds': {
989+ 'description': u'Displays articles from RSS and Atom feeds',
990+ 'categories': ('lookup', 'web',),
991+}}
992
993 log = logging.getLogger('plugins.feeds')
994
995@@ -93,14 +96,14 @@
996 return self.name
997
998 class Manage(Processor):
999- u"""
1000+ usage = u"""
1001 add feed <url> as <name>
1002 remove <name> feed
1003 list feeds
1004 poll <name> feed notify <channel> on <source>
1005 stop polling <name> feed
1006 """
1007- feature = 'feeds'
1008+ feature = ('feeds',)
1009
1010 permission = u'feeds'
1011
1012@@ -203,9 +206,9 @@
1013 event.addresponse(True)
1014
1015 class Retrieve(Processor):
1016- u"""latest [ <count> ] articles from <name> [ starting at <number> ]
1017+ usage = u"""latest [ <count> ] articles from <name> [ starting at <number> ]
1018 article ( <number> | /<pattern>/ ) from <name>"""
1019- feature = 'feeds'
1020+ feature = ('feeds',)
1021
1022 interval = IntOption('interval', 'Feed Poll interval (in seconds)', 300)
1023
1024
1025=== modified file 'ibid/plugins/film.py'
1026--- ibid/plugins/film.py 2010-01-18 23:20:33 +0000
1027+++ ibid/plugins/film.py 2010-03-02 19:03:13 +0000
1028@@ -17,13 +17,16 @@
1029
1030 log = logging.getLogger('plugins.film')
1031
1032-help = {}
1033+features = {}
1034
1035-help['tvshow'] = u'Retrieves TV show information from tvrage.com.'
1036+features['tvshow'] = {
1037+ 'description': u'Retrieves TV show information from tvrage.com.',
1038+ 'categories': ('lookup', 'web',),
1039+}
1040 class TVShow(Processor):
1041- u"""tvshow <show>"""
1042+ usage = u'tvshow <show>'
1043
1044- feature = 'tvshow'
1045+ feature = ('tvshow',)
1046
1047 def remote_tvrage(self, show):
1048 info_url = 'http://services.tvrage.com/tools/quickinfo.php?%s'
1049@@ -79,10 +82,13 @@
1050 # This isn't strictly legal: http://www.imdb.com/help/show_leaf?usedatasoftware
1051 #
1052 # Note that it will return porn movies by default.
1053-help['imdb'] = u'Looks up movies on IMDB.com.'
1054+features['imdb'] = {
1055+ 'description': u'Looks up movies on IMDB.com.',
1056+ 'categories': ('lookup', 'web',),
1057+}
1058 class IMDB(Processor):
1059- u"imdb [search] [character|company|episode|movie|person] <terms> [#<index>]"
1060- feature = 'imdb'
1061+ usage = u'imdb [search] [character|company|episode|movie|person] <terms> [#<index>]'
1062+ feature = ('imdb',)
1063
1064 access_system = Option("accesssystem", "Method of querying IMDB", "http")
1065 adult_search = BoolOption("adultsearch", "Include adult films in search results", True)
1066
1067=== modified file 'ibid/plugins/fun.py'
1068--- ibid/plugins/fun.py 2010-01-18 23:20:33 +0000
1069+++ ibid/plugins/fun.py 2010-03-02 19:03:13 +0000
1070@@ -12,12 +12,15 @@
1071 from ibid.config import IntOption, ListOption
1072 from ibid.utils import human_join
1073
1074-help = {}
1075+features = {}
1076
1077-help['nickometer'] = u'Calculates how lame a nick is.'
1078+features['nickometer'] = {
1079+ 'description': u'Calculates how lame a nick is.',
1080+ 'categories': ('fun', 'calculate',),
1081+}
1082 class Nickometer(Processor):
1083- u"""nickometer [<nick>] [with reasons]"""
1084- feature = 'nickometer'
1085+ usage = u'nickometer [<nick>] [with reasons]'
1086+ feature = ('nickometer',)
1087
1088 @match(r'^(?:nick|lame)-?o-?meter(?:(?:\s+for)?\s+(.+?))?(\s+with\s+reasons)?$')
1089 def handle_nickometer(self, event, nick, wreasons):
1090@@ -36,10 +39,13 @@
1091 reasons = ((u'A good, traditional nick', 0),)
1092 event.addresponse(u'Because: %s', u', '.join(['%s (%s)' % reason for reason in reasons]))
1093
1094-help['choose'] = u'Choose one of the given options.'
1095+features['choose'] = {
1096+ 'description': u'Choose one of the given options.',
1097+ 'categories': ('fun', 'decide',),
1098+}
1099 class Choose(Processor):
1100- u"""choose <choice> or <choice>..."""
1101- feature = 'choose'
1102+ usage = u'choose <choice> or <choice>...'
1103+ feature = ('choose',)
1104
1105 choose_re = re.compile(r'(?:\s*,\s*(?:or\s+)?)|(?:\s+or\s+)', re.I)
1106
1107@@ -47,10 +53,13 @@
1108 def choose(self, event, choices):
1109 event.addresponse(u'I choose %s', choice(self.choose_re.split(choices)))
1110
1111-help['coffee'] = u"Times coffee brewing and reserves cups for people"
1112+features['coffee'] = {
1113+ 'description': u'Times coffee brewing and reserves cups for people',
1114+ 'categories': ('fun', 'monitor',),
1115+}
1116 class Coffee(Processor):
1117- u"""coffee (on|please)"""
1118- feature = 'coffee'
1119+ usage = u'coffee (on|please)'
1120+ feature = ('coffee',)
1121
1122 pots = {}
1123
1124@@ -98,13 +107,14 @@
1125 self.pots[(event.source, event.channel)].append(event.sender['nick'])
1126 event.addresponse(True)
1127
1128-help['insult'] = u"Slings verbal abuse at someone"
1129+features['insult'] = {
1130+ 'description': u'Slings verbal abuse at someone',
1131+ 'categories': ('fun',),
1132+}
1133 class Insult(Processor):
1134- u"""
1135- (flame | insult) <person>
1136- (swear | cuss | explete) [at <person>]
1137- """
1138- feature = 'insult'
1139+ usage = u"""(flame | insult) <person>
1140+ (swear | cuss | explete) [at <person>]"""
1141+ feature = ('insult',)
1142
1143 adjectives = ListOption('adjectives', 'List of adjectives', (
1144 u'acidic', u'antique', u'artless', u'base-court', u'bat-fowling',
1145
1146=== modified file 'ibid/plugins/games.py'
1147--- ibid/plugins/games.py 2010-01-18 23:20:33 +0000
1148+++ ibid/plugins/games.py 2010-03-02 19:03:13 +0000
1149@@ -12,20 +12,23 @@
1150 from ibid.plugins import Processor, match, handler
1151 from ibid.utils import format_date, human_join, plural
1152
1153-help = {}
1154+features = {}
1155 log = logging.getLogger('plugins.games')
1156
1157 duels = {}
1158
1159-help['duel'] = u"Duel at dawn, between channel members"
1160+features['duel'] = {
1161+ 'description': u'Duel at dawn, between channel members',
1162+ 'categories': ('fun', 'game',),
1163+}
1164 class DuelInitiate(Processor):
1165- u"""
1166+ usage = u"""
1167 I challenge <user> to a duel [over <something>]
1168 I demand satisfaction from <user> [over <something>]
1169 I throw the gauntlet down at <user>'s feet [over <something>]
1170 """
1171
1172- feature = 'duel'
1173+ feature = ('duel',)
1174
1175 accept_timeout = FloatOption('accept_timeout', 'How long do we wait for acceptance?', 60.0)
1176 start_delay = IntOption('start_delay', 'Time between acceptance and start of duel (rounded down to the highest minute)', 30)
1177@@ -213,12 +216,10 @@
1178 }, address=False)
1179
1180 class DuelDraw(Processor):
1181- u"""
1182- draw [my <weapon>]
1183- bam|pew|bang|kapow|pewpew|holyhandgrenadeofantioch
1184- """
1185+ usage = u"""draw [my <weapon>]
1186+ bam|pew|bang|kapow|pewpew|holyhandgrenadeofantioch"""
1187
1188- feature = 'duel'
1189+ feature = ('duel',)
1190
1191 # Parameters for Processor:
1192 event_types = (u'message', u'action')
1193@@ -401,7 +402,7 @@
1194 )), duel.names[shooter], address=False)
1195
1196 class DuelFlee(Processor):
1197- feature = 'duel'
1198+ feature = ('duel',)
1199 addressed = False
1200 event_types = (u'state',)
1201
1202@@ -445,20 +446,22 @@
1203
1204 werewolf_games = []
1205
1206-help['werewolf'] = (u'Play the werewolf game. '
1207- u'Channel becomes a village containing a werewolf, seer and villagers. '
1208- u'Every night, the werewolf can kill a villager, and the seer can test '
1209- u'a villager for werewolf symptoms. '
1210- u'Villagers then vote to lynch a wolf during the day.')
1211+features['werewolf'] = {
1212+ 'description': u'Play the werewolf game. Channel becomes a village '
1213+ u'containing a werewolf, seer and villagers. Every night, '
1214+ u'the werewolf can kill a villager, and the seer can test '
1215+ u'a villager for werewolf symptoms. Villagers then vote to '
1216+ u'lynch a wolf during the day.',
1217+ 'categories': ('fun', 'game',),
1218+}
1219 class WerewolfGame(Processor):
1220- u"""
1221+ usage = u"""
1222 start a game of werewolf
1223 join
1224 ( kill | see | eat ) <villager>
1225 vote for <villager>
1226 """
1227-
1228- feature = 'werewolf'
1229+ feature = ('werewolf',)
1230 state = None
1231
1232 player_limit = IntOption('min_players', 'The minimum number of players', 5)
1233@@ -845,7 +848,7 @@
1234 return self.state.__name__
1235
1236 class WerewolfState(Processor):
1237- feature = 'werewolf'
1238+ feature = ('werewolf',)
1239 event_types = (u'state',)
1240
1241 @handler
1242
1243=== modified file 'ibid/plugins/gameservers.py'
1244--- ibid/plugins/gameservers.py 2010-02-25 20:58:58 +0000
1245+++ ibid/plugins/gameservers.py 2010-03-02 19:03:13 +0000
1246@@ -9,12 +9,14 @@
1247 from ibid.config import Option, IntOption
1248 from ibid.utils import human_join
1249
1250-help = {}
1251-help['gameservers'] = u'Lists the users on Game servers'
1252+features = {'gameservers': {
1253+ 'description': u'Lists the users on Game servers',
1254+ 'categories': ('lookup',),
1255+}}
1256
1257 class Bnet(Processor):
1258- u'dota players | who is playing dota'
1259- feature = 'gameservers'
1260+ usage = u'dota players | who is playing dota'
1261+ feature = ('gameservers',)
1262 autoload = False
1263
1264 bnet_host = Option('bnet_host', 'Bnet server hostname / IP', '127.0.0.1')
1265@@ -51,8 +53,8 @@
1266 event.addresponse(u'Nobody. Everyone must have a lives...')
1267
1268 class CounterStrike(Processor):
1269- u'cs players | who is playing cs'
1270- feature = 'gameservers'
1271+ usage = u'cs players | who is playing cs'
1272+ feature = ('gameservers',)
1273 autoload = False
1274
1275 cs_host = Option('cs_host', 'CS server hostname / IP', '127.0.0.1')
1276
1277=== modified file 'ibid/plugins/geography.py'
1278--- ibid/plugins/geography.py 2010-02-20 11:32:43 +0000
1279+++ ibid/plugins/geography.py 2010-03-02 19:03:13 +0000
1280@@ -23,18 +23,21 @@
1281
1282 log = logging.getLogger('plugins.geography')
1283
1284-help = {}
1285+features = {}
1286
1287-help['distance'] = u"Returns the distance between two places"
1288+features['distance'] = {
1289+ 'description': u'Returns the distance between two places',
1290+ 'categories': ('lookup', 'calculate',),
1291+}
1292 class Distance(Processor):
1293- u"""distance [in <unit>] between <source> and <destination>
1294+ usage = u"""distance [in <unit>] between <source> and <destination>
1295 place search for <placename>"""
1296
1297 # For Mathematics, see:
1298 # http://www.mathforum.com/library/drmath/view/51711.html
1299 # http://mathworld.wolfram.com/GreatCircle.html
1300
1301- feature = 'distance'
1302+ feature = ('distance',)
1303
1304 default_unit_names = {
1305 'km': "kilometres",
1306@@ -107,12 +110,15 @@
1307 conjunction=u'or'),
1308 })
1309
1310-help['weather'] = u'Retrieves current weather and forecasts for cities.'
1311+features['weather'] = {
1312+ 'description': u'Retrieves current weather and forecasts for cities.',
1313+ 'categories': ('lookup', 'web',),
1314+}
1315 class Weather(Processor):
1316- u"""weather in <city>
1317+ usage = u"""weather in <city>
1318 forecast for <city>"""
1319
1320- feature = "weather"
1321+ feature = ('weather',)
1322
1323 defaults = { 'ct': 'Cape Town, South Africa',
1324 'jhb': 'Johannesburg, South Africa',
1325@@ -223,11 +229,14 @@
1326 'EST': 'US/Eastern',
1327 }
1328
1329-help['timezone'] = "Converts times between timezones."
1330+features['timezone'] = {
1331+ 'description': 'Converts times between timezones.',
1332+ 'categories': ('convert',),
1333+}
1334 class TimeZone(Processor):
1335- u"""when is <time> <place|timezone> in <place|timezone>
1336+ usage = u"""when is <time> <place|timezone> in <place|timezone>
1337 time in <place|timezone>"""
1338- feature = 'timezone'
1339+ feature = ('timezone',)
1340
1341 zoneinfo = Option('zoneinfo', 'Timezone info directory', '/usr/share/zoneinfo')
1342 custom_zones = DictOption('timezones', 'Custom timezone names', CUSTOM_ZONES)
1343@@ -355,7 +364,10 @@
1344 def time(self, event, place):
1345 self.convert(event, None, None, place)
1346
1347-help['flight'] = u'Search for flights on travelocity'
1348+features['flight'] = {
1349+ 'description': u'Search for flights on travelocity',
1350+ 'categories': ('lookup', 'web',),
1351+}
1352 class Flight:
1353 def __init__(self):
1354 self.flight, self.depart_time, self.depart_ap, self.arrive_time, \
1355@@ -387,10 +399,10 @@
1356 pass
1357
1358 class FlightSearch(Processor):
1359- """airport [in] <name|location|code>
1360+ usage = u"""airport [in] <name|location|code>
1361 [<cheapest|quickest>] flight from <departure> to <destination> from <depart_date> [anytime|morning|afternoon|evening|<time>] to <return_date> [anytime|morning|afternoon|evening|<time>]"""
1362
1363- feature = 'flight'
1364+ feature = ('flight',)
1365
1366 airports_url = u'http://openflights.svn.sourceforge.net/viewvc/openflights/openflights/data/airports.dat'
1367 max_results = IntOption('max_results', 'Maximum number of results to list', 5)
1368
1369=== modified file 'ibid/plugins/google.py'
1370--- ibid/plugins/google.py 2010-01-24 20:50:59 +0000
1371+++ ibid/plugins/google.py 2010-03-02 19:03:13 +0000
1372@@ -11,18 +11,19 @@
1373 from ibid.config import Option
1374 from ibid.utils import decode_htmlentities, json_webservice
1375
1376-help = {}
1377-
1378-help['google'] = u'Retrieves results from Google and Google Calculator.'
1379+features = {'google': {
1380+ 'description': u'Retrieves results from Google and Google Calculator.',
1381+ 'categories': ('lookup', 'web',),
1382+}}
1383
1384 default_user_agent = 'Mozilla/5.0'
1385 default_referer = "http://ibid.omnia.za.net/"
1386
1387 class GoogleAPISearch(Processor):
1388- u"""google [for] <term>
1389+ usage = u"""google [for] <term>
1390 googlefight [for] <term> and <term>"""
1391
1392- feature = 'google'
1393+ feature = ('google',)
1394
1395 api_key = Option('api_key', 'Your Google API Key (optional)', None)
1396 referer = Option('referer', 'The referer string to use (API searches)', default_referer)
1397@@ -85,11 +86,11 @@
1398 # features.
1399 # Dear Google: We promise we don't bite.
1400 class GoogleScrapeSearch(Processor):
1401- u"""gcalc <expression>
1402+ usage = u"""gcalc <expression>
1403 gdefine <term>
1404 google.<TLD> [for] <terms>"""
1405
1406- feature = 'google'
1407+ feature = ('google',)
1408
1409 user_agent = Option('user_agent', 'HTTP user agent to present to Google (for non-API searches)', default_user_agent)
1410 google_scrape_url = "http://www.google.com/search?q=%s"
1411
1412=== modified file 'ibid/plugins/help.py'
1413--- ibid/plugins/help.py 2010-01-18 23:20:33 +0000
1414+++ ibid/plugins/help.py 2010-03-02 19:03:13 +0000
1415@@ -1,103 +1,236 @@
1416 # Copyright (c) 2008-2010, Michael Gorven, Stefano Rivera, Max Rabkin
1417 # Released under terms of the MIT/X/Expat Licence. See COPYING for details.
1418
1419-import inspect
1420+from copy import copy
1421+import re
1422 import sys
1423
1424+try:
1425+ from Stemmer import Stemmer
1426+except ImportError:
1427+ from stemmer import PorterStemmer
1428+ class Stemmer(PorterStemmer):
1429+ def __init__(self, language):
1430+ PorterStemmer.__init__(self)
1431+ def stemWord(self, word):
1432+ return PorterStemmer.stem(self, word, 0, len(word) - 1)
1433+
1434 import ibid
1435 from ibid.plugins import Processor, match
1436 from ibid.utils import human_join
1437
1438-help = {'help': u'Provides help and usage information about plugins.'}
1439+features = {'help': {
1440+ 'description': u'Provides help and usage information about plugins.',
1441+ 'categories': ('admin', 'lookup',),
1442+}}
1443
1444 class Help(Processor):
1445- u"""features [for <word>]
1446- help [<feature>]
1447- usage <feature>"""
1448- feature = 'help'
1449-
1450- @match(r'^help$')
1451- def intro(self, event):
1452- event.addresponse(u'Use "features" to get a list of available features. '
1453- u'"help <feature>" will give a description of the feature, and "usage <feature>" will describe how to use it.')
1454-
1455- @match(r'^features$')
1456- def features(self, event):
1457- features = []
1458-
1459- for processor in ibid.processors:
1460- module = eval(processor.__module__)
1461- if hasattr(module, 'help'):
1462- for feature in module.help.keys():
1463- if feature not in features:
1464- features.append(feature)
1465-
1466- event.addresponse(u'Features: %s', human_join(sorted(features)) or u'none')
1467-
1468- @match(r'^help\s+(.+)$')
1469- def help(self, event, feature):
1470- feature = feature.lower()
1471-
1472- for processor in ibid.processors:
1473- module = eval(processor.__module__)
1474- if hasattr(module, 'help') and feature in module.help:
1475- event.addresponse(module.help[feature])
1476- return
1477-
1478- event.addresponse(u"I can't help you with %s", feature)
1479-
1480- @match(r'^(?:usage|how\s+do\s+I\s+use)\s+(.+)$')
1481- def usage(self, event, feature):
1482- feature = feature.lower()
1483-
1484- output = []
1485- for processor in ibid.processors:
1486- for name, klass in inspect.getmembers(processor, inspect.isclass):
1487- if hasattr(klass, 'feature') and klass.feature == feature and klass.__doc__:
1488- for line in klass.__doc__.strip().splitlines():
1489- output.append(line.strip())
1490-
1491- if len(output) == 1:
1492- event.addresponse(u'Usage: %s', output[0])
1493- elif len(output) > 1:
1494- event.addresponse(
1495- u"You can use %(feature)s in the following ways:\n%(usage)s", {
1496- 'feature': feature,
1497- 'usage': u'\n'.join(output)
1498- }, conflate=False)
1499- else:
1500- event.addresponse(u"I don't know how to use %s either", feature)
1501-
1502- @match(r'^features\s+(?:for|with)\s+(.*)$')
1503- def search (self, event, phrase):
1504- features = map(unicode, self._search(phrase))
1505- features.sort()
1506- if len(features) == 0:
1507- event.addresponse(u"I couldn't find that feature.")
1508- elif len(features) == 1:
1509- event.addresponse(
1510- u'The "%s" feature might be what you\'re looking for.',
1511- features[0])
1512- else:
1513- event.addresponse(u"Are you looking for %s?",
1514- human_join(features, conjunction='or'))
1515-
1516- def _search (self, phrase):
1517- phrase = phrase.lower()
1518- matches = set()
1519+ usage = u"""
1520+ what can you do|help
1521+ help me with <category>
1522+ how do I use <feature>
1523+ help <(category|feature)>
1524+ """
1525+ feature = ('help',)
1526+ stemmer = Stemmer('english')
1527+
1528+ def _get_features(self):
1529+ """Walk the loaded processors and build dicts of categories and
1530+ features in use. Dicts are cross-referenced by string.
1531+ """
1532+ categories = {}
1533+ for k, v in ibid.categories.iteritems():
1534+ v = copy(v)
1535+ v.update({
1536+ 'name': k,
1537+ 'features': set(),
1538+ })
1539+ categories[k] = v
1540+
1541+ features = {}
1542 processor_modules = set()
1543 for processor in ibid.processors:
1544- if (hasattr(processor, 'feature') and processor.__doc__ and
1545- phrase in processor.__doc__.lower()):
1546- matches.add(processor.feature)
1547+ for feature in getattr(processor, 'feature', []):
1548+ if feature not in features:
1549+ features[feature] = {
1550+ 'name': feature,
1551+ 'description': None,
1552+ 'categories': set(),
1553+ 'processors': set(),
1554+ 'usage': [],
1555+ }
1556+ features[feature]['processors'].add(processor)
1557+ if hasattr(processor, 'usage'):
1558+ features[feature]['usage'] += [line.strip()
1559+ for line in processor.usage.split('\n')
1560+ if line.strip()]
1561 processor_modules.add(sys.modules[processor.__module__])
1562
1563 for module in processor_modules:
1564- if hasattr(module, 'help'):
1565- for feature, help in module.help.iteritems():
1566- if phrase in feature or phrase in help.lower():
1567- matches.add(feature)
1568-
1569- return matches
1570+ for feature, meta in getattr(module, 'features', {}).iteritems():
1571+ if feature not in features:
1572+ continue
1573+ if meta.get('description'):
1574+ features[feature]['description'] = meta['description']
1575+ for category in meta.get('categories', []):
1576+ features[feature]['categories'].add(category)
1577+ categories[category]['features'].add(feature)
1578+
1579+ categories = dict((k, v) for k, v in categories.iteritems()
1580+ if v['features'])
1581+
1582+ usere = re.compile(r'[\s()[\]<>|]+')
1583+ for name, feat in features.iteritems():
1584+ feat['usage_keywords'] = frozenset(
1585+ self.stemmer.stemWord(word.strip())
1586+ for word in usere.split(u' '.join(feat['usage']))
1587+ if word.strip())
1588+ for name, cat in categories.iteritems():
1589+ cat['description_keywords'] = frozenset(self.stemmer.stemWord(word)
1590+ for word in cat['description'].lower().split())
1591+ for name in features.keys():
1592+ st_name = self.stemmer.stemWord(name)
1593+ features[st_name] = features[name]
1594+ if st_name != name:
1595+ del features[name]
1596+ for name in categories.keys():
1597+ st_name = self.stemmer.stemWord(name)
1598+ categories[st_name] = categories[name]
1599+ if st_name != name:
1600+ del categories[name]
1601+
1602+ return categories, features
1603+
1604+ def _describe_category(self, event, category):
1605+ """Respond with the help information for a category"""
1606+ event.addresponse(u'I use the following features for %(description)s: '
1607+ u'%(features)s\n'
1608+ u'Ask me "how do I use ..." for more details.',
1609+ {
1610+ 'description': category['description'].lower(),
1611+ 'features': human_join(sorted(category['features'])),
1612+ }, conflate=False)
1613+
1614+ def _describe_feature(self, event, feature):
1615+ """Respond with the help information for a feature"""
1616+ output = []
1617+ desc = feature['description']
1618+ if desc is None:
1619+ output.append(u'You can use it like this:')
1620+ elif len(desc) > 100:
1621+ output.append(desc)
1622+ output.append(u'You can use it like this:')
1623+ elif desc.endswith('.'):
1624+ output.append(desc + u' You can use it like this:')
1625+ else:
1626+ output.append(desc + u'. You can use it like this:')
1627+
1628+ for line in feature['usage']:
1629+ output.append(u' ' + line)
1630+
1631+ event.addresponse(u'\n'.join(output), conflate=False)
1632+
1633+ def _usage_search(self, event, terms, features):
1634+ terms = frozenset(self.stemmer.stemWord(term) for term in terms)
1635+ results = set()
1636+ for name, feat in features.iteritems():
1637+ if terms.issubset(feat['usage_keywords']):
1638+ results.add(name)
1639+ results = sorted(results)
1640+ if len(results) == 1:
1641+ self._describe_feature(event, features[results[0]])
1642+ elif len(results) > 1:
1643+ event.addresponse(
1644+ u"Please be more specific. I don't know if you mean %s",
1645+ human_join((features[result]['name'] for result in results),
1646+ conjunction=u'or'))
1647+ else:
1648+ event.addresponse(
1649+ u"I'm afraid I don't know what you are asking about. "
1650+ u'Ask "what can you do" to browse my features')
1651+
1652+ @match(r'^(?:help|features|what\s+(?:can|do)\s+you\s+do)$')
1653+ def intro(self, event):
1654+ categories, features = self._get_features()
1655+ categories = filter(lambda c: c['weight'] is not None,
1656+ categories.itervalues())
1657+ categories = sorted(categories, key=lambda c: c['weight'])
1658+ event.addresponse(u'I can help you with: %s.\n'
1659+ u'Ask me "help me with ..." for more details.',
1660+ human_join(c['description'].lower() for c in categories),
1661+ conflate=False)
1662+
1663+ @match(r'^help\s+(?:me\s+)?with\s+(.+)$')
1664+ def describe_category(self, event, terms):
1665+ categories, features = self._get_features()
1666+ termset = frozenset(self.stemmer.stemWord(term)
1667+ for term in terms.lower().split())
1668+
1669+ if len(termset) == 1:
1670+ term = list(termset)[0]
1671+ exact = [c for c in categories.itervalues() if c['name'] == term]
1672+ if exact:
1673+ self._describe_category(event, exact[0])
1674+ return
1675+
1676+ results = []
1677+ for name, cat in categories.iteritems():
1678+ if termset.issubset(cat['description_keywords']):
1679+ results.append(name)
1680+
1681+ if len(results) == 0:
1682+ for name, cat in categories.iteritems():
1683+ if terms.lower() in cat['description'].lower():
1684+ results.append(name)
1685+
1686+ results.sort()
1687+ if len(results) == 1:
1688+ self._describe_category(event, categories[results[0]])
1689+ return
1690+ elif len(results) > 1:
1691+ event.addresponse(
1692+ u"Please be more specific, I don't know if you mean %s.",
1693+ human_join(('%s (%s)'
1694+ % (categories[r]['description'].lower(), r)
1695+ for r in results),
1696+ conjunction=u'or'))
1697+ return
1698+
1699+ event.addresponse(u"I'm afraid I don't know what you are asking about"
1700+ u'Ask "what can you do" to browse my features')
1701+
1702+ @match(r'^(?:help|usage|modinfo)\s+(\S+)$')
1703+ def quick_help(self, event, terms):
1704+ categories, features = self._get_features()
1705+ terms = frozenset(terms.lower().split())
1706+ if len(terms) == 1:
1707+ term = list(terms)[0]
1708+ exact = [c for c in categories.itervalues() if c['name'] == term]
1709+ if exact:
1710+ self._describe_category(event, exact[0])
1711+ return
1712+ exact = [f for f in features.itervalues() if f['name'] == term]
1713+ if exact:
1714+ self._describe_feature(event, exact[0])
1715+ return
1716+
1717+ self._usage_search(event, terms, features)
1718+
1719+ @match(r'^how\s+do\s+I(?:\s+use)?\s+(.+)$')
1720+ def describe_feature(self, event, feature):
1721+ categories, features = self._get_features()
1722+
1723+ feature = feature.lower()
1724+ exact = [f for f in features.itervalues() if f['name'] == feature]
1725+ if exact:
1726+ self._describe_feature(event, exact[0])
1727+ else:
1728+ self._usage_search(event, frozenset(feature.split()), features)
1729+
1730+ @match(r'^\s*(?:help\s+me\s+with|how\s+do\s+I(?:\s+use)?)\s+\.\.\.\s*$',
1731+ version='deaddressed')
1732+ def silly_people(self, event):
1733+ event.addresponse(
1734+ u'You must replace the ellipsis with the thing you are after')
1735
1736 # vi: set et sta sw=4 ts=4:
1737
1738=== modified file 'ibid/plugins/icecast.py'
1739--- ibid/plugins/icecast.py 2010-01-30 09:38:13 +0000
1740+++ ibid/plugins/icecast.py 2010-03-02 19:03:13 +0000
1741@@ -11,12 +11,13 @@
1742
1743 log = logging.getLogger('plugins.icecast')
1744
1745-help = {'icecast': u'Follows an ICECast stream'}
1746+features = {'icecast': {
1747+ 'description': u'Follows an ICECast stream',
1748+ 'categories': ('monitor',),
1749+}}
1750 class ICECast(Processor):
1751- u"""
1752- what's playing [on <stream>]?
1753- """
1754- feature = 'icecast'
1755+ usage = u"what's playing [on <stream>]?"
1756+ feature = ('icecast',)
1757
1758 interval = IntOption('interval',
1759 'Interval between checking for song changes', 60)
1760
1761=== modified file 'ibid/plugins/identity.py'
1762--- ibid/plugins/identity.py 2010-01-18 23:20:33 +0000
1763+++ ibid/plugins/identity.py 2010-03-02 19:03:13 +0000
1764@@ -14,18 +14,22 @@
1765 from ibid.utils import human_join
1766 from ibid.auth import hash
1767
1768-help = {}
1769+features = {}
1770 identify_cache = {}
1771
1772 log = logging.getLogger('plugins.identity')
1773
1774-help['accounts'] = u'An account represents a person. ' \
1775- 'An account has one or more identities, which is a user on a specific source.'
1776+features['accounts'] = {
1777+ 'description': u'Manage users accounts with the bot. An account represents '
1778+ u'a person. An account has one or more identities, which is '
1779+ u'a user on a specific source.',
1780+ 'categories': ('admin', 'account',),
1781+}
1782 class Accounts(Processor):
1783- u"""create account [<name>]
1784+ usage = u"""create account [<name>]
1785 delete (my account|account <name>)
1786 rename (my account|account <name>) to <name>"""
1787- feature = 'accounts'
1788+ feature = ('accounts',)
1789
1790 @match(r'^create\s+account(?:\s+(.+))?$')
1791 def new_account(self, event, username):
1792@@ -140,9 +144,9 @@
1793 chars = [x for x in string.letters + string.digits if x not in '01lOIB86G']
1794
1795 class Identities(Processor):
1796- u"""(I am|<username> is) <identity> on <source>
1797+ usage = u"""(I am|<username> is) <identity> on <source>
1798 remove identity <identity> on <source> [from <username>]"""
1799- feature = 'accounts'
1800+ feature = ('accounts',)
1801 priority = -10
1802
1803 def __init__(self, name):
1804@@ -314,8 +318,8 @@
1805 account.username, event.account, event.identity, event.sender['connection'])
1806
1807 class Attributes(Processor):
1808- u"""set (my|<account>) <name> to <value>"""
1809- feature = 'accounts'
1810+ usage = u'set (my|<account>) <name> to <value>'
1811+ feature = ('accounts',)
1812
1813 @match(r"^set\s+(my|.+?)(?:\'s)?\s+(.+)\s+to\s+(.+)$")
1814 def attribute(self, event, username, name, value):
1815@@ -348,8 +352,8 @@
1816 event.identity, event.sender['connection'])
1817
1818 class Describe(Processor):
1819- u"""who (am I|is <username>)"""
1820- feature = "accounts"
1821+ usage = u'who (am I|is <username>)'
1822+ feature = ('accounts',)
1823
1824 @match(r'^who\s+(?:is|am)\s+(I|.+?)$')
1825 def describe(self, event, username):
1826@@ -374,10 +378,13 @@
1827 'identities': human_join(u'%s on %s' % (identity.identity, identity.source) for identity in account.identities),
1828 })
1829
1830-help['summon'] = u"Get the attention of a person via different source"
1831+features['summon'] = {
1832+ 'description': u'Get the attention of a person via different source',
1833+ 'categories': ('message',),
1834+}
1835 class Summon(Processor):
1836- u"summon <person> [via <source>]"
1837- feature = 'summon'
1838+ usage = u'summon <person> [via <source>]'
1839+ feature = ('summon',)
1840 permission = u'summon'
1841
1842 default_source = Option('default_source',
1843@@ -495,10 +502,14 @@
1844
1845 actions = {'revoke': 'Revoked', 'grant': 'Granted', 'remove': 'Removed'}
1846
1847-help['auth'] = u'Adds and removes authentication credentials and permissions'
1848+features['auth'] = {
1849+ 'description': u'Adds and removes authentication credentials and '
1850+ u'permissions',
1851+ 'categories': ('admin', 'account',),
1852+}
1853 class AddAuth(Processor):
1854- u"""authenticate <account> [on source] using <method> [<credential>]"""
1855- feature = 'auth'
1856+ usage = u'authenticate <account> [on source] using <method> [<credential>]'
1857+ feature = ('auth',)
1858
1859 @match(r'^authenticate\s+(.+?)(?:\s+on\s+(.+))?\s+using\s+(\S+)\s+(.+)$')
1860 def handler(self, event, user, source, method, credential):
1861@@ -543,10 +554,10 @@
1862
1863 permission_values = {'no': '-', 'yes': '+', 'auth': ''}
1864 class Permissions(Processor):
1865- u"""(grant|revoke|remove) <permission> (to|from|on) <username> [when authed]
1866+ usage = u"""(grant|revoke|remove) <permission> (to|from|on) <username> [when authed]
1867 permissions [for <username>]
1868 list permissions"""
1869- feature = 'auth'
1870+ feature = ('auth',)
1871
1872 permission = u'admin'
1873
1874@@ -631,8 +642,8 @@
1875 event.addresponse(u'Permissions: %s', human_join(sorted(permissions)) or u'none')
1876
1877 class Auth(Processor):
1878- u"""auth <credential>"""
1879- feature = 'auth'
1880+ usage = u'auth <credential>'
1881+ feature = ('auth',)
1882
1883 @match(r'^auth(?:\s+(.+))?$')
1884 def handler(self, event, password):
1885
1886=== modified file 'ibid/plugins/karma.py'
1887--- ibid/plugins/karma.py 2010-01-18 23:20:33 +0000
1888+++ ibid/plugins/karma.py 2010-03-02 19:03:13 +0000
1889@@ -10,7 +10,10 @@
1890 VersionedSchema
1891 from ibid.plugins import Processor, match, handler, authorise
1892
1893-help = {'karma': u'Keeps track of karma for people and things.'}
1894+features = {'karma': {
1895+ 'description': u'Keeps track of karma for people and things.',
1896+ 'categories': ('remember',),
1897+}}
1898
1899 log = logging.getLogger('plugins.karma')
1900
1901@@ -47,8 +50,8 @@
1902 self.time = datetime.utcnow()
1903
1904 class Set(Processor):
1905- u"""<subject> (++|--|==|ftw|ftl) [[reason]]"""
1906- feature = 'karma'
1907+ usage = u'<subject> (++|--|==|ftw|ftl) [[reason]]'
1908+ feature = ('karma',)
1909
1910 # Clashes with morse & math
1911 priority = 510
1912@@ -121,9 +124,9 @@
1913 event.processed = True
1914
1915 class Get(Processor):
1916- u"""karma for <subject>
1917+ usage = u"""karma for <subject>
1918 [reverse] karmaladder"""
1919- feature = 'karma'
1920+ feature = ('karma',)
1921
1922 @match(r'^karma\s+(?:for\s+)?(.+)$')
1923 def handle_karma(self, event, subject):
1924@@ -149,8 +152,8 @@
1925 event.addresponse(u"I don't really care about anything")
1926
1927 class Forget(Processor):
1928- u"""forget karma for <subject> [[reason]]"""
1929- feature = 'karma'
1930+ usage = u'forget karma for <subject> [[reason]]'
1931+ feature = ('karma',)
1932
1933 # Clashes with factoid
1934 priority = -10
1935
1936=== modified file 'ibid/plugins/languages.py'
1937--- ibid/plugins/languages.py 2010-03-01 12:13:12 +0000
1938+++ ibid/plugins/languages.py 2010-03-02 19:03:14 +0000
1939@@ -13,15 +13,18 @@
1940 from ibid.utils import decode_htmlentities, json_webservice, human_join, \
1941 is_url, url_to_bytestring
1942
1943-help = {}
1944+features = {}
1945
1946-help['dict'] = u'Defines words and checks spellings.'
1947+features['dict'] = {
1948+ 'description': u'Defines words and checks spellings.',
1949+ 'categories': ('lookup',),
1950+}
1951 class Dict(Processor):
1952- u"""spell <word> [using <strategy>]
1953+ usage = u"""spell <word> [using <strategy>]
1954 define <word> [using <dictionary>]
1955 (dictionaries|strategies)
1956 (dictionary|strategy) <name>"""
1957- feature = 'dict'
1958+ feature = ('dict',)
1959
1960 server = Option('server', 'Dictionary server hostname', 'localhost')
1961 port = IntOption('port', 'Dictionary server port number', 2628)
1962@@ -130,12 +133,15 @@
1963 class UnknownLanguageException (Exception): pass
1964 class TranslationException (Exception): pass
1965
1966-help['translate'] = u'''Translates a phrase using Google Translate.'''
1967+features['translate'] = {
1968+ 'description': u'Translates a phrase using Google Translate.',
1969+ 'categories': ('lookup', 'convert', 'web',),
1970+}
1971 class Translate(Processor):
1972- u"""translate (<phrase>|<url>) [from <language>] [to <language>]
1973- translation chain <phrase> [from <language>] [to <language>]"""
1974+ usage = u"""translate (<phrase>|<url>) [from <language>] [to <language>]
1975+ translation chain <phrase> [from <language>] [to <language>]"""
1976
1977- feature = 'translate'
1978+ feature = ('translate',)
1979
1980 api_key = Option('api_key', 'Your Google API Key (optional)', None)
1981 referer = Option('referer', 'The referer string to use (API searches)', default_referer)
1982
1983=== modified file 'ibid/plugins/lotto.py'
1984--- ibid/plugins/lotto.py 2010-02-20 07:38:16 +0000
1985+++ ibid/plugins/lotto.py 2010-03-02 19:03:14 +0000
1986@@ -8,13 +8,16 @@
1987 from ibid.plugins import Processor, match
1988
1989 log = logging.getLogger('plugins.lotto')
1990-help = {}
1991
1992-help['lotto'] = u"Gets the latest lotto results from the South African National Lottery."
1993+features = {'lotto': {
1994+ 'description': u'Gets the latest lotto results from the South African '
1995+ u'National Lottery.',
1996+ 'categories': ('lookup', 'south africa', 'web',),
1997+}}
1998 class Lotto(Processor):
1999- u"""lotto"""
2000+ usage = u'lotto'
2001
2002- feature = 'lotto'
2003+ feature = ('lotto',)
2004
2005 za_url = 'http://www.nationallottery.co.za/'
2006 za_re = re.compile(r'images/(?:power_)?balls/(?:ball|power)_(\d+).gif')
2007
2008=== modified file 'ibid/plugins/meetings.py'
2009--- ibid/plugins/meetings.py 2010-02-16 08:06:35 +0000
2010+++ ibid/plugins/meetings.py 2010-03-02 19:03:14 +0000
2011@@ -19,14 +19,17 @@
2012 from ibid.source.http import templates
2013 from ibid.utils import format_date, plural
2014
2015-help = {}
2016+features = {}
2017 log = logging.getLogger('plugins.meetings')
2018
2019 meetings = {}
2020
2021-help['meeting'] = u'Take minutes of an IRC Meeting'
2022+features['meeting'] = {
2023+ 'description': u'Take minutes of an IRC Meeting',
2024+ 'categories': ('remember', 'monitor',),
2025+}
2026 class Meeting(Processor):
2027- u"""
2028+ usage = u"""
2029 (start | end) meeting [about <title>]
2030 I am <True Name>
2031 topic <topic>
2032@@ -34,7 +37,7 @@
2033 minutes so far
2034 meeting title is <title>
2035 """
2036- feature = 'meeting'
2037+ feature = ('meeting',)
2038 permission = u'chairmeeting'
2039
2040 formats = Option('formats', u'Formats to log to. '
2041@@ -241,7 +244,7 @@
2042 addressed = False
2043 processed = True
2044 priority = 1900
2045- feature = 'meeting'
2046+ feature = ('meeting',)
2047
2048 def process(self, event):
2049 if 'channel' in event and 'source' in event \
2050@@ -270,14 +273,17 @@
2051 'time': event.time,
2052 })
2053
2054-help['poll'] = u'Does a quick poll of channel members'
2055+features['poll'] = {
2056+ 'description': u'Does a quick poll of channel members',
2057+ 'categories': ('decide',),
2058+}
2059 class Poll(Processor):
2060- u"""
2061+ usage = u"""
2062 [secret] poll on <topic> [until <time>] vote <option> [or <option>]...
2063 vote (<id> | <option>) [on <topic>]
2064 end poll
2065 """
2066- feature = 'poll'
2067+ feature = ('poll',)
2068 permission = u'chairmeeting'
2069
2070 polls = {}
2071
2072=== modified file 'ibid/plugins/memo.py'
2073--- ibid/plugins/memo.py 2010-01-18 23:20:33 +0000
2074+++ ibid/plugins/memo.py 2010-03-02 19:03:14 +0000
2075@@ -15,7 +15,10 @@
2076 from ibid.plugins.identity import get_identities
2077 from ibid.utils import ago, format_date
2078
2079-help = {'memo': u'Keeps messages for people.'}
2080+features = {'memo': {
2081+ 'description': u'Keeps messages for people.',
2082+ 'categories': ('remember', 'message',),
2083+}}
2084
2085 nomemos_cache = set()
2086 notified_overlimit_cache = set()
2087@@ -60,9 +63,9 @@
2088 backref='recipient')
2089
2090 class Tell(Processor):
2091- u"""(tell|pm|privmsg|msg|ask) <person> [on <source>] <message>
2092+ usage = u"""(tell|pm|privmsg|msg|ask) <person> [on <source>] <message>
2093 forget my (first|last|<n>th) message for <person> [on <source>]"""
2094- feature = 'memo'
2095+ feature = ('memo',)
2096
2097 permission = u'sendmemo'
2098 permissions = (u'recvmemo',)
2099@@ -225,7 +228,7 @@
2100 .order_by(Memo.time.asc()).all()
2101
2102 class Deliver(Processor):
2103- feature = 'memo'
2104+ feature = ('memo',)
2105
2106 addressed = False
2107 processed = True
2108@@ -286,7 +289,7 @@
2109 nomemos_cache.add(event.identity)
2110
2111 class Notify(Processor):
2112- feature = 'memo'
2113+ feature = ('memo',)
2114
2115 event_types = (u'state',)
2116 addressed = False
2117@@ -319,10 +322,10 @@
2118 nomemos_cache.add(event.identity)
2119
2120 class Messages(Processor):
2121- u"""my messages
2122+ usage = u"""my messages
2123 message <number>
2124 my messages for <person> [on <source>]"""
2125- feature = 'memo'
2126+ feature = ('memo',)
2127
2128 @match(r'^my\s+messages$')
2129 def messages(self, event):
2130
2131=== modified file 'ibid/plugins/memory.py'
2132--- ibid/plugins/memory.py 2010-02-20 07:37:33 +0000
2133+++ ibid/plugins/memory.py 2010-03-02 19:03:14 +0000
2134@@ -14,9 +14,10 @@
2135 from ibid.config import Option, IntOption
2136 from ibid.plugins import Processor, match
2137
2138-help = {}
2139-
2140-help['memory'] = u'Debugging module that keeps track of memory usage'
2141+features = {'memory': {
2142+ 'description': u'Debugging module that keeps track of memory usage',
2143+ 'categories': ('debug',),
2144+}}
2145
2146 def get_memusage():
2147 status = file('/proc/%i/status' % os.getpid(), 'r').readlines()
2148@@ -25,7 +26,7 @@
2149
2150 class MemoryLog(Processor):
2151
2152- feature = 'memory'
2153+ feature = ('memory',)
2154 autoload = False
2155
2156 mem_filename = Option('mem_filename', 'Memory log filename', 'logs/memory.log')
2157@@ -96,9 +97,9 @@
2158 self.obj_file.flush()
2159
2160 class MemoryInfo(Processor):
2161- u"memory usage"
2162+ usage = u'memory usage'
2163
2164- feature = 'memory'
2165+ feature = ('memory',)
2166
2167 @match('^memory\s+usage$')
2168 def memory_usage(self, event):
2169
2170=== modified file 'ibid/plugins/network.py'
2171--- ibid/plugins/network.py 2010-02-22 08:58:15 +0000
2172+++ ibid/plugins/network.py 2010-03-02 19:03:14 +0000
2173@@ -25,13 +25,16 @@
2174 url_to_bytestring, get_process_output
2175 from ibid.utils.html import get_country_codes
2176
2177-help = {}
2178+features = {}
2179
2180-help['dns'] = u'Performs DNS lookups'
2181+features['dns'] = {
2182+ 'description': u'Performs DNS lookups',
2183+ 'categories': ('lookup', 'sysadmin',),
2184+}
2185 class DNS(Processor):
2186- u"""dns [<record type>] [for] <host> [from <nameserver>]"""
2187+ usage = u'dns [<record type>] [for] <host> [from <nameserver>]'
2188
2189- feature = 'dns'
2190+ feature = ('dns',)
2191
2192 def setup(self):
2193 if Resolver is None:
2194@@ -74,10 +77,13 @@
2195
2196 event.addresponse(u'Records: %s', human_join(responses))
2197
2198-help['ping'] = u'ICMP pings the specified host.'
2199+features['ping'] = {
2200+ 'description': u'ICMP pings the specified host.',
2201+ 'categories': ('sysadmin', 'monitor',),
2202+}
2203 class Ping(Processor):
2204- u"""ping <host>"""
2205- feature = 'ping'
2206+ usage = u'ping <host>'
2207+ feature = ('ping',)
2208
2209 ping = Option('ping', 'Path to ping executable', 'ping')
2210
2211@@ -104,10 +110,13 @@
2212 .replace(u'ping:', u'', 1).strip()
2213 event.addresponse(u'Error: %s', error)
2214
2215-help['tracepath'] = u'Traces the path to the given host.'
2216+features['tracepath'] = {
2217+ 'description': u'Traces the path to the given host.',
2218+ 'categories': ('sysadmin',),
2219+}
2220 class Tracepath(Processor):
2221- u"""tracepath <host>"""
2222- feature = 'tracepath'
2223+ usage = u'tracepath <host>'
2224+ feature = ('tracepath',)
2225
2226 tracepath = Option('tracepath', 'Path to tracepath executable', 'tracepath')
2227
2228@@ -129,11 +138,14 @@
2229 error = unicode_output(error.strip())
2230 event.addresponse(u'Error: %s', error.replace(u'\n', u' '))
2231
2232-help['ipcalc'] = u'IP address calculator'
2233+features['ipcalc'] = {
2234+ 'description': u'IP address calculator',
2235+ 'categories': ('sysadmin', 'calculate',),
2236+}
2237 class IPCalc(Processor):
2238- u"""ipcalc <network>/<subnet>
2239+ usage = u"""ipcalc <network>/<subnet>
2240 ipcalc <address> - <address>"""
2241- feature = 'ipcalc'
2242+ feature = ('ipcalc',)
2243
2244 ipcalc = Option('ipcalc', 'Path to ipcalc executable', 'ipcalc')
2245
2246@@ -202,12 +214,15 @@
2247 class HTTPException(Exception):
2248 pass
2249
2250-help['http'] = u'Tests if an HTTP site is up and retrieves HTTP URLs.'
2251+features['http'] = {
2252+ 'description': u'Tests if an HTTP site is up and retrieves HTTP URLs.',
2253+ 'categories': ('monitor', 'sysadmin',),
2254+}
2255 class HTTP(Processor):
2256- u"""(get|head) <url>
2257+ usage = u"""(get|head) <url>
2258 is <domain> (up|down)
2259 tell me when <domain|url> is up"""
2260- feature = 'http'
2261+ feature = ('http',)
2262 priority = -10
2263
2264 max_size = IntOption('max_size', u'Only request this many bytes', 500)
2265@@ -396,11 +411,14 @@
2266 return response.status, response.reason, data.decode(charset), \
2267 response.getheaders()
2268
2269-help['tld'] = u"Resolve country TLDs (ISO 3166)"
2270+features['tld'] = {
2271+ 'description': u'Resolve country TLDs (ISO 3166)',
2272+ 'categories': ('lookup', 'sysadmin',),
2273+}
2274 class TLD(Processor):
2275- u""".<tld>
2276+ usage = u""".<tld>
2277 tld for <country>"""
2278- feature = 'tld'
2279+ feature = ('tld',)
2280
2281 country_codes = {}
2282
2283@@ -434,11 +452,14 @@
2284
2285 event.addresponse(u"ISO doesn't know about any TLD for %s", location)
2286
2287-help['ports'] = u'Looks up port numbers for protocols'
2288+features['ports'] = {
2289+ 'description': u'Looks up port numbers for protocols',
2290+ 'categories': ('lookup', 'sysadmin',),
2291+}
2292 class Ports(Processor):
2293- u"""port for <protocol>
2294+ usage = u"""port for <protocol>
2295 (tcp|udp) port <number>"""
2296- feature = 'ports'
2297+ feature = ('ports',)
2298 priority = 10
2299
2300 services = Option('services', 'Path to services file', '/etc/services')
2301@@ -502,12 +523,16 @@
2302 else:
2303 event.addresponse(u"I don't know about any protocols using that port")
2304
2305-help['nmap'] = u'Finds open network ports on a host or scans a subnet for active hosts.'
2306+features['nmap'] = {
2307+ 'description': u'Finds open network ports on a host or scans a subnet for '
2308+ u'active hosts.',
2309+ 'categories': ('sysadmin',),
2310+}
2311 class Nmap(Processor):
2312- """port scan <hostname>
2313+ usage = u"""port scan <hostname>
2314 net scan <network>/<prefix>"""
2315
2316- feature = 'nmap'
2317+ feature = ('nmap',)
2318 permission = 'nmap'
2319 min_prefix = IntOption('min_prefix', 'Minimum network prefix that may be scanned', 24)
2320
2321
2322=== modified file 'ibid/plugins/oeis.py'
2323--- ibid/plugins/oeis.py 2010-01-30 09:38:13 +0000
2324+++ ibid/plugins/oeis.py 2010-03-02 19:03:14 +0000
2325@@ -11,14 +11,15 @@
2326
2327 log = logging.getLogger('plugins.oeis')
2328
2329-help = {}
2330-
2331-help['oeis'] = 'Query the Online Encyclopedia of Integer Sequences'
2332+features = {'oeis': {
2333+ 'description': 'Query the Online Encyclopedia of Integer Sequences',
2334+ 'categories': ('lookup', 'web', 'calculate',),
2335+}}
2336 class OEIS(Processor):
2337- u"""oeis (A<OEIS number>|M<EIS number>|N<HIS number>)
2338+ usage = u"""oeis (A<OEIS number>|M<EIS number>|N<HIS number>)
2339 oeis <term>[, ...]"""
2340
2341- feature = 'oeis'
2342+ feature = ('oeis',)
2343
2344 @match(r'^oeis\s+([AMN]\d+|-?\d(?:\d|-|,|\s)*)$')
2345 def oeis (self, event, query):
2346
2347=== modified file 'ibid/plugins/quotes.py'
2348--- ibid/plugins/quotes.py 2010-01-30 09:38:13 +0000
2349+++ ibid/plugins/quotes.py 2010-03-02 19:03:14 +0000
2350@@ -19,12 +19,15 @@
2351
2352 log = logging.getLogger('plugins.quotes')
2353
2354-help = {}
2355+features = {}
2356
2357-help['fortune'] = u'Returns a random fortune.'
2358+features['fortune'] = {
2359+ 'description': u'Returns a random fortune.',
2360+ 'categories': ('fun', 'lookup',),
2361+}
2362 class Fortune(Processor, RPC):
2363- u"""fortune"""
2364- feature = 'fortune'
2365+ usage = u'fortune'
2366+ feature = ('fortune',)
2367
2368 fortune = Option('fortune', 'Path of the fortune executable', 'fortune')
2369
2370@@ -56,11 +59,14 @@
2371 else:
2372 return None
2373
2374-help['bash'] = u'Retrieve quotes from bash.org.'
2375+features['bash'] = {
2376+ 'description': u'Retrieve quotes from bash.org.',
2377+ 'categories': ('fun', 'lookup', 'web',),
2378+}
2379 class Bash(Processor):
2380- u"bash[.org] [(random|<number>)]"
2381+ usage = u'bash[.org] [(random|<number>)]'
2382
2383- feature = 'bash'
2384+ feature = ('bash',)
2385
2386 public_browse = BoolOption('public_browse', 'Allow random quotes in public', True)
2387
2388@@ -87,14 +93,17 @@
2389 output.append(line)
2390 event.addresponse(u'\n'.join(output), conflate=False)
2391
2392-help['fml'] = u'Retrieves quotes from fmylife.com.'
2393+features['fml'] = {
2394+ 'description': u'Retrieves quotes from fmylife.com.',
2395+ 'categories': ('fun', 'lookup', 'web',),
2396+}
2397 class FMLException(Exception):
2398 pass
2399
2400 class FMyLife(Processor):
2401- u"""fml (<number> | [random] | flop | top | last | love | money | kids | work | health | sex | miscellaneous )"""
2402+ usage = u'fml (<number> | [random] | flop | top | last | love | money | kids | work | health | sex | miscellaneous )'
2403
2404- feature = "fml"
2405+ feature = ('fml',)
2406
2407 api_url = Option('fml_api_url', 'FML API URL base', 'http://api.betacie.com/')
2408 # The Ibid API Key, registered by Stefano Rivera:
2409@@ -156,12 +165,15 @@
2410 else:
2411 event.addresponse(u'Sorry, not in public. PM me')
2412
2413-help["tfln"] = u"Looks up quotes from textsfromlastnight.com"
2414+features['tfln'] = {
2415+ 'description': u'Looks up quotes from textsfromlastnight.com',
2416+ 'categories': ('fun', 'lookup', 'web',),
2417+}
2418 class TextsFromLastNight(Processor):
2419- u"""tfln [(random|<number>)]
2420+ usage = u"""tfln [(random|<number>)]
2421 tfln (worst|best) [(today|this week|this month)]"""
2422
2423- feature = 'tfln'
2424+ feature = ('tfln',)
2425
2426 public_browse = BoolOption('public_browse', 'Allow random quotes in public', True)
2427
2428@@ -226,12 +238,15 @@
2429 def tfln_url(self, event, id):
2430 self.tfln(event, id)
2431
2432-help["mlia"] = u"Looks up quotes from MyLifeIsAverage.com and MyLifeIsG.com"
2433+features['mlia'] = {
2434+ 'description': u'Looks up quotes from MyLifeIsAverage.com and MyLifeIsG.com',
2435+ 'categories': ('fun', 'lookup', 'web',),
2436+}
2437 class MyLifeIsAverage(Processor):
2438- u"""mlia [(<number> | random | recent | today | yesterday | this week | this month | this year )]
2439+ usage = u"""mlia [(<number> | random | recent | today | yesterday | this week | this month | this year )]
2440 mlig [(<number> | random | recent | today | yesterday | this week | this month | this year )]"""
2441
2442- feature = 'mlia'
2443+ feature = ('mlia',)
2444
2445 public_browse = BoolOption('public_browse',
2446 'Allow random quotes in public', True)
2447@@ -338,12 +353,15 @@
2448 def mlia_url(self, event, site, id):
2449 self.mlia(event, 'mli' + site[0].lower(), id)
2450
2451-help['bible'] = u'Retrieves Bible verses'
2452+features['bible'] = {
2453+ 'description': u'Retrieves Bible verses',
2454+ 'categories': ('lookup', 'web',),
2455+}
2456 class Bible(Processor):
2457- u"""bible <passages> [in <version>]
2458+ usage = u"""bible <passages> [in <version>]
2459 <book> <verses> [in <version>]"""
2460
2461- feature = 'bible'
2462+ feature = ('bible',)
2463 # http://labs.bible.org/api/ is an alternative
2464 # Their feature set is a little different, but they should be fairly
2465 # compatible
2466
2467=== modified file 'ibid/plugins/rfc.py'
2468--- ibid/plugins/rfc.py 2010-01-18 23:20:33 +0000
2469+++ ibid/plugins/rfc.py 2010-03-02 19:03:14 +0000
2470@@ -9,16 +9,19 @@
2471 from ibid.plugins import Processor, match
2472 from ibid.utils import cacheable_download
2473
2474-help = {"rfc": u"Looks up RFCs by number or title."}
2475+features = {'rfc': {
2476+ 'description': u'Looks up RFCs by number or title.',
2477+ 'categories': ('lookup', 'web', 'development',),
2478+}}
2479
2480 cachetime = 60*60
2481 log = logging.getLogger("plugin.rfc")
2482
2483 class RFCLookup(Processor):
2484- u"""rfc <number>
2485+ usage = u"""rfc <number>
2486 rfc [for] <search terms>
2487 rfc [for] /regex/"""
2488- feature = "rfc"
2489+ feature = ('rfc',)
2490
2491 indexurl = Option('index_url', "A HTTP url for the RFC Index file", "http://www.rfc-editor.org/rfc/rfc-index.txt")
2492 cachetime = IntOption("cachetime", "Time to cache RFC index for", cachetime)
2493
2494=== modified file 'ibid/plugins/seen.py'
2495--- ibid/plugins/seen.py 2010-01-18 23:20:33 +0000
2496+++ ibid/plugins/seen.py 2010-03-02 19:03:14 +0000
2497@@ -13,7 +13,10 @@
2498
2499 log = logging.getLogger('plugins.seen')
2500
2501-help = {'seen': u'Records when people were last seen.'}
2502+features = {'seen': {
2503+ 'description': u'Records when people were last seen.',
2504+ 'categories': ('remember', 'lookup',),
2505+}}
2506
2507 class Sighting(Base):
2508 __table__ = Table('seen', Base.metadata,
2509@@ -60,7 +63,7 @@
2510 self.type, self.identity_id, self.channel, self.time, self.value)
2511
2512 class See(Processor):
2513- feature = 'seen'
2514+ feature = ('seen',)
2515
2516 priority = 1500
2517 event_types = (u'message', u'state')
2518@@ -94,8 +97,8 @@
2519 event.sender['id'], event.source)
2520
2521 class Seen(Processor):
2522- u"""seen <who>"""
2523- feature = 'seen'
2524+ usage = u'seen <who>'
2525+ feature = ('seen',)
2526
2527 @match(r'^(?:have\s+you\s+)?seen\s+(\S+)(?:\s+on\s+(\S+))?$')
2528 def handler(self, event, who, source):
2529
2530=== modified file 'ibid/plugins/social.py'
2531--- ibid/plugins/social.py 2010-02-25 20:59:02 +0000
2532+++ ibid/plugins/social.py 2010-03-02 19:03:14 +0000
2533@@ -15,13 +15,16 @@
2534 from ibid.utils import ago, decode_htmlentities, json_webservice
2535
2536 log = logging.getLogger('plugins.social')
2537-help = {}
2538+features = {}
2539
2540-help['lastfm'] = u'Lists the tracks last listened to by the specified user.'
2541+features['lastfm'] = {
2542+ 'description': u'Lists the tracks last listened to by the specified user.',
2543+ 'categories': ('lookup', 'web',),
2544+}
2545 class LastFm(Processor):
2546- u"last.fm for <username>"
2547+ usage = u'last.fm for <username>'
2548
2549- feature = "lastfm"
2550+ feature = ('lastfm',)
2551
2552 @match(r'^last\.?fm\s+for\s+(\S+?)\s*$')
2553 def listsongs(self, event, username):
2554@@ -34,12 +37,16 @@
2555 ago(event.time - dt_strptime(e.updated, '%a, %d %b %Y %H:%M:%S +0000'), 1)
2556 ) for e in songs['entries']))
2557
2558-help["microblog"] = u"Looks up messages on microblogging services like twitter and identica."
2559+features['microblog'] = {
2560+ 'description': u'Looks up messages on microblogging services like twitter '
2561+ u'and identica.',
2562+ 'categories': ('lookup', 'web',),
2563+}
2564 class Twitter(Processor):
2565- u"""latest (tweet|identica) from <name>
2566+ usage = u"""latest (tweet|identica) from <name>
2567 (tweet|identica) <number>"""
2568
2569- feature = "microblog"
2570+ feature = ('microblog',)
2571
2572 default = {
2573 'twitter': {'endpoint': 'http://twitter.com/', 'api': 'twitter', 'name': 'tweet', 'user': 'twit'},
2574
2575=== modified file 'ibid/plugins/sources.py'
2576--- ibid/plugins/sources.py 2010-01-18 23:20:33 +0000
2577+++ ibid/plugins/sources.py 2010-03-02 19:03:14 +0000
2578@@ -11,15 +11,18 @@
2579
2580 log = logging.getLogger('plugins.sources')
2581
2582-help = {}
2583-
2584-
2585-help["actions"] = u"Provides commands for joining/parting channels on IRC and Jabber, and changing the bot's nick"
2586+features = {}
2587+
2588+features['actions'] = {
2589+ 'description': u"Provides commands for joining/parting channels on IRC and "
2590+ u"Jabber, and changing the bot's nick",
2591+ 'categories': ('admin',),
2592+}
2593
2594 class Actions(Processor):
2595- u"""(join|part|leave) [<channel> [on <source>]]
2596+ usage = u"""(join|part|leave) [<channel> [on <source>]]
2597 change nick to <nick> [on <source>]"""
2598- feature = 'actions'
2599+ feature = ('actions',)
2600
2601 permission = 'sources'
2602
2603@@ -98,10 +101,13 @@
2604 if self.is_nickserv(event):
2605 log.info(u'Authenticated with NickServ')
2606
2607-help['saydo'] = u'Says or does stuff in a channel.'
2608+features['saydo'] = {
2609+ 'description': u'Says or does stuff in a channel.',
2610+ 'categories': ('admin', 'fun',),
2611+}
2612 class SayDo(Processor):
2613- u"""(say|do) in <channel> [on <source>] <text>"""
2614- feature = 'saydo'
2615+ usage = u'(say|do) in <channel> [on <source>] <text>'
2616+ feature = ('saydo',)
2617
2618 permission = u'saydo'
2619
2620@@ -111,10 +117,14 @@
2621 event.addresponse(what, address=False, target=channel, source=source or event.source,
2622 action=(action.lower() == u"do"))
2623
2624-help['redirect'] = u'Redirects the response to a command to a different channel.'
2625+features['redirect'] = {
2626+ 'description': u'Redirects the response to a command to a different '
2627+ u'channel.',
2628+ 'categories': ('admin', 'fun',),
2629+}
2630 class RedirectCommand(Processor):
2631- u"""redirect [to] <channel> [on <source>] <command>"""
2632- feature = 'redirect'
2633+ usage = u'redirect [to] <channel> [on <source>] <command>'
2634+ feature = ('redirect',)
2635
2636 priority = -1200
2637 permission = u'saydo'
2638@@ -131,7 +141,7 @@
2639 event.message['clean'] = command
2640
2641 class Redirect(Processor):
2642- feature = 'redirect'
2643+ feature = ('redirect',)
2644
2645 processed = True
2646 priority = 940
2647
2648=== modified file 'ibid/plugins/strings.py'
2649--- ibid/plugins/strings.py 2010-02-25 20:43:01 +0000
2650+++ ibid/plugins/strings.py 2010-03-02 19:03:14 +0000
2651@@ -9,13 +9,16 @@
2652 from ibid.compat import hashlib
2653 from ibid.plugins import Processor, match, authorise
2654
2655-help = {}
2656+features = {}
2657
2658-help['hash'] = u'Calculates numerous cryptographic hash functions.'
2659+features['hash'] = {
2660+ 'description': u'Calculates numerous cryptographic hash functions.',
2661+ 'categories': ('calculate',),
2662+}
2663 class Hash(Processor):
2664- u"""(md5|sha1|sha224|sha256|sha384|sha512) <string>
2665+ usage = u"""(md5|sha1|sha224|sha256|sha384|sha512) <string>
2666 crypt <string> <salt>"""
2667- feature = 'hash'
2668+ feature = ('hash',)
2669
2670 @match(r'^(md5|sha1|sha224|sha256|sha384|sha512)(?:sum)?\s+(.+?)$')
2671 def hash(self, event, hash, string):
2672@@ -26,10 +29,13 @@
2673 def handle_crypt(self, event, string, salt):
2674 event.addresponse(unicode(crypt(string.encode('utf-8'), salt.encode('utf-8'))))
2675
2676-help['base64'] = u'Encodes and decodes base 16, 32 and 64. Assumes UTF-8.'
2677+features['base64'] = {
2678+ 'description': u'Encodes and decodes base 16, 32 and 64. Assumes UTF-8.',
2679+ 'categories': ('calculate', 'convert', 'development',),
2680+}
2681 class Base64(Processor):
2682- u"""base(16|32|64) (encode|decode) <string>"""
2683- feature = 'base64'
2684+ usage = u'base(16|32|64) (encode|decode) <string>'
2685+ feature = ('base64',)
2686
2687 @match(r'^b(?:ase)?(16|32|64)\s*(enc|dec)(?:ode)?\s+(.+?)$')
2688 def base64(self, event, base, operation, string):
2689@@ -47,20 +53,26 @@
2690 else:
2691 event.addresponse(unicode(func(string.encode('utf-8'))))
2692
2693-help['rot13'] = u'Transforms a string with ROT13.'
2694+features['rot13'] = {
2695+ 'description': u'Transforms a string with ROT13.',
2696+ 'categories': ('convert', 'fun',),
2697+}
2698 class Rot13(Processor):
2699- u"""rot13 <string>"""
2700- feature = 'rot13'
2701+ usage = u'rot13 <string>'
2702+ feature = ('rot13',)
2703
2704 @match(r'^rot13\s+(.+)$')
2705 def rot13(self, event, string):
2706 repl = lambda x: x.group(0).encode('rot13')
2707 event.addresponse(re.sub('[a-zA-Z]+', repl, string))
2708
2709-help['dvorak'] = u"Makes text typed on a QWERTY keyboard as if it was Dvorak work, and vice-versa"
2710+features['dvorak'] = {
2711+ 'description': u'Makes text typed on a QWERTY keyboard as if it was Dvorak work, and vice-versa',
2712+ 'categories': ('convert', 'fun',),
2713+}
2714 class Dvorak(Processor):
2715- u"""(aoeu|asdf) <text>"""
2716- feature = 'dvorak'
2717+ usage = u"""(aoeu|asdf) <text>"""
2718+ feature = ('dvorak',)
2719
2720 # List of characters on each keyboard layout
2721 dvormap = u"""',.pyfgcrl/=aoeuidhtns-;qjkxbmwvz"<>PYFGCRL?+AOEUIDHTNS_:QJKXBMWVZ[]{}|"""
2722@@ -79,10 +91,14 @@
2723 def convert_from_dvorak(self, event, text):
2724 event.addresponse(text.translate(self.typed_on_dvorak))
2725
2726-help['retest'] = u'Checks whether a regular expression matches a given string.'
2727+features['retest'] = {
2728+ 'description': u'Checks whether a regular expression matches a given '
2729+ u'string.',
2730+ 'categories': ('development',),
2731+}
2732 class ReTest(Processor):
2733- u"""does <pattern> match <string>"""
2734- feature = 'retest'
2735+ usage = u'does <pattern> match <string>'
2736+ feature = ('retest',)
2737 permission = 'regex'
2738
2739 @match('^does\s+(.+?)\s+match\s+(.+?)$')
2740@@ -90,10 +106,13 @@
2741 def retest(self, event, regex, string):
2742 event.addresponse(re.search(regex, string) and u'Yes' or u'No')
2743
2744-help["morse"] = u"Translates messages into and out of morse code."
2745+features['morse'] = {
2746+ 'description': u'Translates messages into and out of morse code.',
2747+ 'categories': ('convert', 'fun',),
2748+}
2749 class Morse(Processor):
2750- u"""morse (text|morsecode)"""
2751- feature = 'morse'
2752+ usage = u'morse (text|morsecode)'
2753+ feature = ('morse',)
2754
2755 _table = {
2756 'A': ".-",
2757
2758=== modified file 'ibid/plugins/svn.py'
2759--- ibid/plugins/svn.py 2010-02-20 07:37:19 +0000
2760+++ ibid/plugins/svn.py 2010-03-02 19:03:14 +0000
2761@@ -40,7 +40,10 @@
2762 from ibid.config import DictOption, FloatOption, Option, BoolOption
2763 from ibid.utils import ago, format_date, human_join
2764
2765-help = {'svn': u'Retrieves commit logs from a Subversion repository.'}
2766+features = {'svn': {
2767+ 'description': u'Retrieves commit logs from a Subversion repository.',
2768+ 'categories': ('development', 'lookup',),
2769+}}
2770
2771 HEAD_REVISION = object()
2772
2773@@ -387,10 +390,10 @@
2774 pass
2775
2776 class Subversion(Processor, RPC):
2777- u"""(last commit|commit <revno>) [to <repo>] [full]
2778+ usage = u"""(last commit|commit <revno>) [to <repo>] [full]
2779 (svnrepos|svnrepositories)
2780 """
2781- feature = 'svn'
2782+ feature = ('svn',)
2783 autoload = False
2784
2785 permission = u'svn'
2786
2787=== modified file 'ibid/plugins/sysadmin.py'
2788--- ibid/plugins/sysadmin.py 2010-02-13 12:08:22 +0000
2789+++ ibid/plugins/sysadmin.py 2010-03-02 19:03:14 +0000
2790@@ -9,12 +9,15 @@
2791 from ibid.config import Option
2792 from ibid.utils import file_in_path, unicode_output, human_join, cacheable_download
2793
2794-help = {}
2795+features = {}
2796
2797-help['aptitude'] = u'Searches for packages'
2798+features['aptitude'] = {
2799+ 'description': u'Searches for packages',
2800+ 'categories': ('sysadmin', 'lookup',),
2801+}
2802 class Aptitude(Processor):
2803- u"""apt (search|show) <term>"""
2804- feature = 'aptitude'
2805+ usage = u'apt (search|show) <term>'
2806+ feature = ('aptitude',)
2807
2808 aptitude = Option('aptitude', 'Path to aptitude executable', 'aptitude')
2809
2810@@ -106,10 +109,13 @@
2811 error = error[3:]
2812 event.addresponse(u"Couldn't find package: %s", error)
2813
2814-help['apt-file'] = u'Searches for packages containing the specified file'
2815+features['apt-file'] = {
2816+ 'description': u'Searches for packages containing the specified file',
2817+ 'categories': ('sysadmin', 'lookup',),
2818+}
2819 class AptFile(Processor):
2820- u"""apt-file [search] <term>"""
2821- feature = 'apt-file'
2822+ usage = u'apt-file [search] <term>'
2823+ feature = ('apt-file',)
2824
2825 aptfile = Option('apt-file', 'Path to apt-file executable', 'apt-file')
2826
2827@@ -142,10 +148,13 @@
2828 event.addresponse(u'Search error')
2829 raise Exception("apt-file: %s" % error)
2830
2831-help['man'] = u'Retrieves information from manpages.'
2832+features['man'] = {
2833+ 'description': u'Retrieves information from manpages.',
2834+ 'categories': ('sysadmin', 'lookup',),
2835+}
2836 class Man(Processor):
2837- u"""man [<section>] <page>"""
2838- feature = 'man'
2839+ usage = u'man [<section>] <page>'
2840+ feature = ('man',)
2841
2842 man = Option('man', 'Path of the man executable', 'man')
2843
2844@@ -182,10 +191,13 @@
2845 if index:
2846 event.addresponse(output[index+1].strip())
2847
2848-help ['mac'] = u'Finds the organization owning the specific MAC address.'
2849+features['mac'] = {
2850+ 'description': u'Finds the organization owning the specific MAC address.',
2851+ 'categories': ('sysadmin', 'lookup',),
2852+}
2853 class Mac(Processor):
2854- u"""mac <address>"""
2855- feature = 'mac'
2856+ usage = u'mac <address>'
2857+ feature = ('mac',)
2858
2859 @match(r'^((?:mac|oui|ether(?:net)?(?:\s*code)?)\s+)?((?:(?:[0-9a-f]{2}(?(1)[:-]?|:))){2,5}[0-9a-f]{2})$')
2860 def lookup_mac(self, event, _, mac):
2861
2862=== modified file 'ibid/plugins/test.py'
2863--- ibid/plugins/test.py 2010-01-18 23:20:33 +0000
2864+++ ibid/plugins/test.py 2010-03-02 19:03:14 +0000
2865@@ -4,40 +4,50 @@
2866 from time import sleep
2867
2868 import ibid
2869-from ibid.plugins import Processor, match
2870-
2871-class Delay(Processor):
2872+from ibid.plugins import Processor, match, authorise
2873+
2874+features = {'test': {
2875+ 'description': u'Test functions for use in bot development',
2876+ 'categories': ('debug',),
2877+}}
2878+
2879+class Tests(Processor):
2880+ usage = u"""delay <seconds>
2881+ authorise <permission>
2882+ email <address>
2883+ raise exception
2884+ topic <topic>
2885+ """
2886+ feature = ('test',)
2887+ permission = u'debug'
2888
2889 @match(r'^delay\s+(\d+\.?\d*)$')
2890- def handler(self, event, delay):
2891+ @authorise()
2892+ def sleep(self, event, delay):
2893 sleep(float(delay))
2894 event.addresponse(True)
2895
2896-class Authorise(Processor):
2897-
2898 @match(r'^authorise\s+(\S+)$')
2899- def handler(self, event, permission):
2900+ def is_authorised(self, event, permission):
2901 if ibid.auth.authorise(event, permission):
2902 event.addresponse(u'Yes')
2903 else:
2904 event.addresponse(u'No')
2905
2906-class Email(Processor):
2907-
2908 @match(r'^email\s+(.+)$')
2909+ @authorise()
2910 def email(self, event, address):
2911 event.addresponse(u'Test message', source='email', target=unicode(address))
2912 event.addresponse(u"I've emailed %s", address)
2913
2914-class Except(Processor):
2915-
2916 @match(r'^raise\s+exception$')
2917- def handler(self, event):
2918+ @authorise()
2919+ def throw_up(self, event):
2920 raise Exception("Ow, that hurt.")
2921
2922-class Topic(Processor):
2923 @match(r'^topic\s+(.+)$')
2924- def handler(self, event, topic):
2925+ @authorise()
2926+ def topic(self, event, topic):
2927 event.addresponse(topic, topic=True, address=False)
2928
2929 # vi: set et sta sw=4 ts=4:
2930
2931=== modified file 'ibid/plugins/trac.py'
2932--- ibid/plugins/trac.py 2010-01-18 23:20:33 +0000
2933+++ ibid/plugins/trac.py 2010-03-02 19:03:14 +0000
2934@@ -13,7 +13,10 @@
2935 from ibid.config import Option, BoolOption
2936 from ibid.utils import ago
2937
2938-help = {'trac': u'Retrieves tickets from a Trac database.'}
2939+features = {'trac': {
2940+ 'description': u'Retrieves tickets from a Trac database.',
2941+ 'categories': ('development', 'lookup',),
2942+}}
2943
2944 class Ticket(object):
2945 pass
2946@@ -25,9 +28,9 @@
2947 mapper(Ticket, ticket_table)
2948
2949 class Tickets(Processor, RPC):
2950- u"""ticket <number>
2951+ usage = u"""ticket <number>
2952 (open|my|<who>'s) tickets"""
2953- feature = 'trac'
2954+ feature = ('trac',)
2955 autoload = 'trac' in ibid.databases
2956
2957 url = Option('url', 'URL of Trac instance')
2958
2959=== modified file 'ibid/plugins/urlgrab.py'
2960--- ibid/plugins/urlgrab.py 2010-02-24 09:52:01 +0000
2961+++ ibid/plugins/urlgrab.py 2010-03-02 19:03:14 +0000
2962@@ -16,12 +16,13 @@
2963 from ibid.utils import url_regex
2964 from ibid.utils.html import get_html_parse_tree
2965
2966-help = {}
2967-
2968 log = logging.getLogger('plugins.urlgrab')
2969
2970-help['url'] = u'Captures URLs seen in channel to database and/or to ' \
2971- u'delicious/faves'
2972+features = {'urlgrab': {
2973+ 'description': u'Captures URLs seen in channel to database and/or to '
2974+ u'delicious/faves',
2975+ 'categories': ('remember', 'web',),
2976+}}
2977
2978 class URL(Base):
2979 __table__ = Table('urls', Base.metadata,
2980
2981=== modified file 'ibid/plugins/urlinfo.py'
2982--- ibid/plugins/urlinfo.py 2010-01-25 09:23:49 +0000
2983+++ ibid/plugins/urlinfo.py 2010-03-02 19:03:14 +0000
2984@@ -16,13 +16,17 @@
2985 default_user_agent = 'Mozilla/5.0'
2986 default_referer = "http://ibid.omnia.za.net/"
2987
2988-help = {}
2989+features = {}
2990
2991 log = logging.getLogger('plugins.url')
2992
2993+features['tinyurl'] = {
2994+ 'description': u'Shorten and lengthen URLs',
2995+ 'categories': ('lookup', 'web',),
2996+}
2997 class Shorten(Processor):
2998- u"""shorten <url>"""
2999- feature = 'url'
3000+ usage = u'shorten <url>'
3001+ feature = ('tinyurl',)
3002
3003 @match(r'^shorten\s+(\S+\.\S+)$')
3004 def shorten(self, event, url):
3005@@ -38,9 +42,9 @@
3006 return None
3007
3008 class Lengthen(Processor):
3009- u"""<url>
3010+ usage = u"""<url>
3011 expand <url>"""
3012- feature = 'url'
3013+ feature = ('tinyurl',)
3014
3015 services = ListOption('services', 'List of URL prefixes of URL shortening services', (
3016 'http://is.gd/', 'http://tinyurl.com/', 'http://ff.im/',
3017@@ -70,11 +74,14 @@
3018
3019 event.addresponse(u"No redirect")
3020
3021-help['youtube'] = u'Determine the title and a download URL for a Youtube Video'
3022+features['youtube'] = {
3023+ 'description': u'Determine the title and a download URL for a Youtube Video',
3024+ 'categories': ('lookup', 'web',),
3025+}
3026 class Youtube(Processor):
3027- u'<Youtube URL>'
3028+ usage = u'<Youtube URL>'
3029
3030- feature = 'youtube'
3031+ feature = ('youtube',)
3032
3033 @match(r'^(?:youtube(?:\.com)?\s+)?'
3034 r'(?:http://)?(?:\w+\.)?youtube\.com/'
3035
3036=== modified file 'scripts/ibid-plugin'
3037--- scripts/ibid-plugin 2010-02-21 08:08:24 +0000
3038+++ scripts/ibid-plugin 2010-03-02 19:03:14 +0000
3039@@ -157,11 +157,11 @@
3040 if isinstance(response, basestring):
3041 print 'Response: %s' % response
3042 elif response.get('action', False):
3043- print 'Action: %s' % response['reply']
3044+ print 'Action: %s' % response.get('reply')
3045 elif response.get('notice', False):
3046- print 'Notice: %s' % response['reply']
3047+ print 'Notice: %s' % response.get('reply')
3048 else:
3049- print 'Response: %s' % response['reply']
3050+ print 'Response: %s' % response.get('reply')
3051
3052 event.session.close()
3053
3054
3055=== modified file 'setup.py'
3056--- setup.py 2010-02-05 15:35:26 +0000
3057+++ setup.py 2010-03-02 19:03:13 +0000
3058@@ -16,6 +16,7 @@
3059 'jinja',
3060 'pyopenssl',
3061 'pysqlite',
3062+ 'PyStemmer',
3063 'python-dateutil',
3064 'SOAPpy',
3065 'SQLAlchemy>=0.5', # Works with >=0.4.6 except on OS X

Subscribers

People subscribed via source and target branches