limit limitFreq to per channel
[lusty.git] / plugins.py
blob1f3874af4e064b92e2de00a3eb9afb2a96132130
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 = {}
64 def decorator(func):
65 def proxy(self, arg=None):
66 now = datetime.now()
67 last = lastmsg.get(self.channel, None)
68 if last is not None:
69 diff = now - last
70 if diff.seconds < secs:
71 log.msg('too quick!')
72 if msg:
73 self.toUser(msg)
74 return
76 func(self, arg)
77 lastmsg[self.channel] = now
78 return decorate(proxy, func)
80 return decorator
82 def admin(nicks):
83 """
84 Only admin listed in `nicks` can run this command. Silently fail otherwise.
85 """
86 def decorator(func):
87 def proxy(self, *args, **kwargs):
88 if self.user in nicks:
89 return func(self, *args, **kwargs)
90 else:
91 log.msg('ADMIN: %s not authorized to run command %s' % (self.user, func.__name__))
93 return decorate(proxy, func)
94 return decorator
96 class BotActions(object):
98 def __init__(self, bot):
99 self.bot = bot
101 def toChannel(self, msg):
102 self.bot.msg(self.channel, msg)
104 def toUser(self, msg):
105 self.toChannel('%s: %s' % (self.user, msg))
107 def doAction(self, action):
108 self.bot.me(self.channel, action)
110 def grep(self, msg, words):
111 for word in words:
112 # XXX: this does not search for a word, but substring!
113 if word in msg:
114 return True
115 return False
118 class Commands(BotActions):
120 def __init__(self, bot, user, channel):
121 BotActions.__init__(self, bot)
122 self.user = user
123 self.channel = channel
125 def run(self, rest):
126 parts = rest.split(None, 1)
127 command = parts[0]
128 args = parts[1:]
129 try:
130 cmdFunc = getattr(self, 'cmd_%s' % command)
131 except AttributeError:
132 log.msg('no such command: %s', command)
133 else:
134 cmdFunc(*args)
136 def cmd_ping(self, args=None):
137 "respond to PING"
138 self.toUser(args and 'PONG %s' % args or 'PONG')
139 cmd_PING = cmd_ping
141 @admin(['srid'])
142 def cmd_reload(self, args=None):
143 "reload plugins.py (self)"
144 import plugins
145 try:
146 reload(plugins)
147 except Exception, e:
148 self.toUser(e)
149 else:
150 self.toUser('reloaded')
151 cmd_r = cmd_reload
153 def cmd_help(self, args=None):
154 ignore = ('help', 'PING')
155 for name, func, docstring in self:
156 if name not in ignore and docstring is not None:
157 self.toChannel('%10s - %s' % (name, docstring))
159 # more useful commands
161 def cmd_eval(self, args=None):
162 "evaluate a python expression"
163 if args:
164 from math import * # make math functions available to `eval`
165 try:
166 result = eval(args)
167 except SyntaxError, e:
168 self.toUser('wrong syntax')
169 except Exception, e:
170 self.toUser(e)
171 else:
172 self.toChannel(result)
173 cmd_e = cmd_eval
175 def cmd_google(self, search_terms=None):
176 "search the internet"
177 if search_terms:
178 soup = soupify('http://www.google.com/search?' + \
179 urllib.urlencode({'q': search_terms}))
180 firstLink = soup.find('a', attrs={'class': 'l'})
181 firstLink = firstLink['href'].encode('utf-8').strip()
182 log.msg('google:', firstLink)
183 self.toChannel(firstLink)
184 cmd_g = cmd_google
186 def cmd_source(self, args=None):
187 "how to access lusty's source code"
188 self.toChannel(SOURCE)
190 def cmd_tell(self, args=None):
191 if args:
192 user, message = args.split(None, 1)
193 # XXX: add to db
194 self.toUser('ok')
196 @admin(['srid'])
197 def cmd_join(self, channel=None):
198 if channel:
199 self.bot.join(channel)
201 @admin(['srid'])
202 def cmd_leave(self, channel=None):
203 if channel:
204 self.bot.leave(channel)
206 @admin(['srid'])
207 def cmd_do(self, args):
208 # perform an action in a channel
209 # > ,do #foo is bored
210 channel, action = args.split(None, 1)
211 self.bot.me(channel, action)
213 def __iter__(self):
214 "Iterate over all commands (minus their aliases)"
215 cmdMap = {}
216 for attr in dir(self):
217 if attr.startswith('cmd_'):
218 func = getattr(self, attr)
219 name = attr[len('cmd_'):]
220 if func in cmdMap:
221 # find and overwrite the alias.
222 # the name of an alias is shorter than the name
223 # of the original version.
224 if len(cmdMap[func]) < len(name):
225 cmdMap[func] = name
226 else:
227 cmdMap[func] = name
229 for func, name in cmdMap.items():
230 yield name, func, func.__doc__
233 class Filters(BotActions):
235 def __init__(self, bot, user, channel):
236 BotActions.__init__(self, bot)
237 self.user = user
238 self.channel = channel
240 def run(self, msg):
241 for attr in dir(self):
242 if attr.startswith('filter_'):
243 filter = getattr(self, attr)
244 filter(msg)
246 def respondWisely(self, msg, for_words, with_responses, one_for_every=1):
247 random.seed(time.time())
248 if self.grep(msg, for_words):
249 with probability_of_one_by(one_for_every) as do:
250 if do:
251 self.toChannel(random.choice(with_responses))
253 def filter_obscene(self, msg):
254 words = ['fuck', 'f**k', 'bitch', 'cunt', 'tits']
255 quotes = [
256 'The rate at which a person can mature is directly proportional to the embarrassment he can tolerate.',
257 'To make mistakes is human; to stumble is commonplace; to be able to laugh at yourself is maturity.',
258 'It is the province of knowledge to speak and it is the privilege of wisdom to listen.',
259 'Some folks are wise and some otherwise.',
260 'The wise man doesn\'t give the right answers, he poses the right questions.',
261 'There are many who know many things, yet are lacking in wisdom.',
262 'Wise men hear and see as little children do.',
263 'Speech both conceals and reveals the thoughts of men',
264 '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.',
265 'The trouble with her is that she lacks the power of conversation but not the power of speech.',
266 'Speak when you are angry - and you\'ll make the best speech you\'ll ever regret.',
267 'Speech is human, silence is divine, yet also brutish and dead: therefore we must learn both arts.',
268 'We must have reasons for speech but we need none for silence',
269 'It usually takes more than three weeks to prepare a good impromptu speech.',
270 'Men use thought only as authority for their injustice, and employ speech only to conceal their thoughts',
271 '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.',
272 'Silence is also speech',
273 '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.',
276 self.respondWisely(msg, words, quotes)
278 def filter_hatred(self, msg):
279 words = ['hate', 'suck', 'bad']
280 quotes = [
281 'The first reaction to truth is hatred',
282 'Hatred does not cease by hatred, but only by love; this is the eternal rule.',
283 'Hatred is settled anger',
284 'I love humanity but I hate people.',
285 'Let them hate, so long as they fear.',
286 'What we need is hatred. From it our ideas are born.',
289 self.respondWisely(msg, words, quotes, 3)
291 def filter_m8ball(self, msg):
292 # This list is taken from fsbot@emacs (type ',dl m8ball')
293 answers = [
294 "Yes", "No", "Definitely", "Of course not!", "Highly likely.",
295 "Ask yourself, do you really want to know?",
296 "I'm telling you, you don't want to know.",
297 "mu!"
300 self.respondWisely(msg, ['??'], answers)
302 def filter_random(self, msg):
303 random.seed(time.time())
304 with probability_of_one_by(100) as do:
305 if do:
306 with probability_of_one_by(2) as do2:
307 if do2:
308 self.doAction(
309 random.choice([
310 'looks around nervously',
312 else:
313 self.toChannel(
314 random.choice([
315 'Let\'s fight a land war in space.'
318 _filter_links_url = re.compile('.*(https?://[^ ]*).*')
319 def filter_links(self, msg):
320 "Show <title> in channel"
321 match = self._filter_links_url.match(msg)
322 if match:
323 self.toChannel('Title: %s' % urlTitle(match.group(1)))