neat hack: probability_of_one_by
[lusty.git] / plugins.py
blob0582c1f1b301e9cf0f3f5a7bc6482b5d5c60acee
1 from __future__ import with_statement
3 import twisted
4 from twisted.python import log
6 from datetime import datetime
7 import random, time, re, urllib
9 from contextlib import contextmanager
12 PREFIX = ','
13 SOURCE = 'http://repo.or.cz/w/lusty.git'
15 def handleMsg(bot, user, channel, msg):
16 ignoreChannels = [bot.nickname, 'AUTH']
17 if channel not in ignoreChannels:
18 log.msg('handleMsg> %s/%s: %s' % (user, channel, msg))
20 rest = prefixMatch(msg, [PREFIX, bot.nickname + ':'])
21 if rest is not None:
22 # Run COMMAND
23 commands = Commands(bot, user, channel)
24 commands.run(rest)
26 # Run FILTERS
27 filters = Filters(bot, user, channel)
28 filters.run(msg)
30 def prefixMatch(string, prefixes):
31 for prefix in prefixes:
32 if string.startswith(prefix):
33 return string[len(prefix):]
34 else:
35 return None
37 @contextmanager
38 def probability_of_one_by(n):
39 random.seed(time.time())
40 yield 0 == random.choice(range(n))
42 def soupify(url):
43 from BeautifulSoup import BeautifulSoup
44 import urllib2
45 req = urllib2.Request(url, None, {'User-agent': 'mozilla'})
46 contents = urllib2.urlopen(req).read()
47 return BeautifulSoup(contents)
49 def urlTitle(url):
50 soup = soupify(url)
51 return soup.title.contents[0].encode('utf-8').strip()
53 def decorate(proxy, original):
54 # as decorator 'overwrites' namespace with these proxy
55 # functions, we need to make the introspecting code happy.
56 proxy.__doc__ = original.__doc__
57 return proxy
59 def limitFreq(secs, msg=None):
60 """
61 Limit the frequency of execution of the function to avoid IRC spamming
62 """
63 lastmsg = [None]
64 def decorator(func):
65 def proxy(self, arg=None):
66 now = datetime.now()
67 if lastmsg[0] is not None:
68 diff = now - lastmsg[0]
69 if diff.seconds < secs:
70 log.msg('too quick!')
71 if msg:
72 self.toUser(msg)
73 return
75 func(self, arg)
76 lastmsg[0] = now
77 return decorate(proxy, func)
79 return decorator
81 def admin(nicks):
82 """
83 Only admin listed in `nicks` can run this command. Silently fail otherwise.
84 """
85 def decorator(func):
86 def proxy(self, *args, **kwargs):
87 if self.user in nicks:
88 return func(self, *args, **kwargs)
89 else:
90 log.msg('ADMIN: %s not authorized to run command %s' % (self.user, func.__name__))
92 return decorate(proxy, func)
93 return decorator
95 class BotActions(object):
97 def __init__(self, bot):
98 self.bot = bot
100 def toChannel(self, msg):
101 self.bot.msg(self.channel, msg)
103 def toUser(self, msg):
104 self.toChannel('%s: %s' % (self.user, msg))
106 def doAction(self, action):
107 self.bot.me(self.channel, action)
109 def grep(self, msg, words):
110 for word in words:
111 # XXX: this does not search for a word, but substring!
112 if word in msg:
113 return True
114 return False
117 class Commands(BotActions):
119 def __init__(self, bot, user, channel):
120 BotActions.__init__(self, bot)
121 self.user = user
122 self.channel = channel
124 def run(self, rest):
125 parts = rest.split(None, 1)
126 command = parts[0]
127 args = parts[1:]
128 try:
129 cmdFunc = getattr(self, 'cmd_%s' % command)
130 except AttributeError:
131 log.msg('no such command: %s', command)
132 else:
133 cmdFunc(*args)
135 def cmd_ping(self, args=None):
136 "respond to PING"
137 self.toUser(args and 'PONG %s' % args or 'PONG')
138 cmd_PING = cmd_ping
140 @admin(['srid'])
141 def cmd_reload(self, args=None):
142 "reload plugins.py (self)"
143 import plugins
144 try:
145 reload(plugins)
146 except Exception, e:
147 self.toUser(e)
148 else:
149 self.toUser('reloaded')
150 cmd_r = cmd_reload
152 def cmd_help(self, args=None):
153 ignore = ('help', 'PING')
154 for name, func, docstring in self:
155 if name not in ignore and docstring is not None:
156 self.toChannel('%10s - %s' % (name, docstring))
158 # more useful commands
160 def cmd_eval(self, args=None):
161 "evaluate a python expression"
162 if args:
163 from math import * # make math functions available to `eval`
164 try:
165 result = eval(args)
166 except SyntaxError, e:
167 self.toUser('wrong syntax')
168 except Exception, e:
169 self.toUser(e)
170 else:
171 self.toChannel(result)
172 cmd_e = cmd_eval
174 def cmd_google(self, search_terms=None):
175 "search the internet"
176 if search_terms:
177 soup = soupify('http://www.google.com/search?' + \
178 urllib.urlencode({'q': search_terms}))
179 firstLink = soup.find('a', attrs={'class': 'l'})
180 firstLink = firstLink['href'].encode('utf-8').strip()
181 log.msg('google:', firstLink)
182 self.toChannel(firstLink)
183 cmd_g = cmd_google
185 def cmd_source(self, args=None):
186 "how to access lusty's source code"
187 self.toUser(SOURCE)
189 def cmd_tell(self, args=None):
190 if args:
191 user, message = args.split(None, 1)
192 # XXX: add to db
193 self.toUser('ok')
195 @admin(['srid'])
196 def cmd_join(self, channel=None):
197 if channel:
198 self.bot.join(channel)
200 @admin(['srid'])
201 def cmd_leave(self, channel=None):
202 if channel:
203 self.bot.leave(channel)
205 @admin(['srid'])
206 def cmd_do(self, args):
207 # perform an action in a channel
208 # > ,do #foo is bored
209 channel, action = args.split(None, 1)
210 self.bot.me(channel, action)
212 def __iter__(self):
213 "Iterate over all commands (minus their aliases)"
214 cmdMap = {}
215 for attr in dir(self):
216 if attr.startswith('cmd_'):
217 func = getattr(self, attr)
218 name = attr[len('cmd_'):]
219 if func in cmdMap:
220 # find and overwrite the alias.
221 # the name of an alias is shorter than the name
222 # of the original version.
223 if len(cmdMap[func]) < len(name):
224 cmdMap[func] = name
225 else:
226 cmdMap[func] = name
228 for func, name in cmdMap.items():
229 yield name, func, func.__doc__
232 class Filters(BotActions):
234 def __init__(self, bot, user, channel):
235 BotActions.__init__(self, bot)
236 self.user = user
237 self.channel = channel
239 def run(self, msg):
240 for attr in dir(self):
241 if attr.startswith('filter_'):
242 filter = getattr(self, attr)
243 filter(msg)
245 def respondWisely(self, msg, for_words, with_responses, one_for_every=1):
246 random.seed(time.time())
247 if self.grep(msg, for_words):
248 with probability_of_one_by(one_for_every) as do:
249 if do:
250 self.toChannel(random.choice(with_responses))
252 def filter_obscene(self, msg):
253 words = ['fuck', 'f**k', 'bitch', 'cunt', 'tits']
254 quotes = [
255 'The rate at which a person can mature is directly proportional to the embarrassment he can tolerate.',
256 'To make mistakes is human; to stumble is commonplace; to be able to laugh at yourself is maturity.',
257 'It is the province of knowledge to speak and it is the privilege of wisdom to listen.',
258 'Some folks are wise and some otherwise.',
259 'The wise man doesn\'t give the right answers, he poses the right questions.',
260 'There are many who know many things, yet are lacking in wisdom.',
261 'Wise men hear and see as little children do.',
262 'Speech both conceals and reveals the thoughts of men',
263 'Few people know how to take a walk. The qualifications are endurance, plain clothes, old shoes, an eye for nature, good humor, vast curiosity, good speech, good silence and nothing too much.',
264 'The trouble with her is that she lacks the power of conversation but not the power of speech.',
265 'Speak when you are angry - and you\'ll make the best speech you\'ll ever regret.',
266 'Speech is human, silence is divine, yet also brutish and dead: therefore we must learn both arts.',
267 'We must have reasons for speech but we need none for silence',
268 'It usually takes more than three weeks to prepare a good impromptu speech.',
269 'Men use thought only as authority for their injustice, and employ speech only to conceal their thoughts',
270 'Much talking is the cause of danger. Silence is the means of avoiding misfortune. The talkative parrot is shut up in a cage. Other birds, without speech, fly freely about.',
271 'Silence is also speech',
272 'Speak properly, and in as few words as you can, but always plainly; for the end of speech is not ostentation, but to be understood.',
275 self.respondWisely(msg, words, quotes)
277 def filter_hatred(self, msg):
278 words = ['hate', 'suck', 'bad']
279 quotes = [
280 'The first reaction to truth is hatred',
281 'Hatred does not cease by hatred, but only by love; this is the eternal rule.',
282 'Hatred is settled anger',
283 'I love humanity but I hate people.',
284 'Let them hate, so long as they fear.',
285 'What we need is hatred. From it our ideas are born.',
288 self.respondWisely(msg, words, quotes, 3)
290 def filter_m8ball(self, msg):
291 # This list is taken from fsbot@emacs (type ',dl m8ball')
292 answers = [
293 "Yes", "No", "Definitely", "Of course not!", "Highly likely.",
294 "Ask yourself, do you really want to know?",
295 "I'm telling you, you don't want to know.",
296 "mu!"
299 self.respondWisely(msg, ['??'], answers)
301 def filter_random(self, msg):
302 random.seed(time.time())
303 with probability_of_one_by(100) as do:
304 if do:
305 with probability_of_one_by(2) as do2:
306 if do2:
307 self.doAction(
308 random.choice([
309 'looks around nervously',
311 else:
312 self.toChannel(
313 random.choice([
314 'Let\'s fight a land war in space.'
317 _filter_links_url = re.compile('.*(https?://[^ ]*).*')
318 def filter_links(self, msg):
319 "Show <title> in channel"
320 match = self._filter_links_url.match(msg)
321 if match:
322 self.toChannel('Title: %s' % urlTitle(match.group(1)))