linux-india madness
[lusty.git] / plugins.py
blob758de852d86a6a7dfdd0759d9d070c0f8203c435
1 import twisted
2 from twisted.python import log
4 from datetime import datetime
5 import random, time, re, urllib
7 PREFIX = ','
10 def handleMsg(bot, user, channel, msg):
11 ignoreChannels = [bot.nickname, 'AUTH']
12 if channel not in ignoreChannels:
13 log.msg('handleMsg> %s/%s: %s' % (user, channel, msg))
15 rest = prefixMatch(msg, [PREFIX, bot.nickname + ':'])
16 if rest is not None:
17 # Run COMMAND
18 commands = Commands(bot, user, channel)
19 commands.run(rest)
21 # Run FILTERS
22 filters = Filters(bot, user, channel)
23 filters.run(msg)
25 def prefixMatch(string, prefixes):
26 for prefix in prefixes:
27 if string.startswith(prefix):
28 return string[len(prefix):]
29 else:
30 return None
32 def soupify(url):
33 from BeautifulSoup import BeautifulSoup
34 import urllib2
35 req = urllib2.Request(url, None, {'User-agent': 'mozilla'})
36 contents = urllib2.urlopen(req).read()
37 return BeautifulSoup(contents)
39 def urlTitle(url):
40 soup = soupify(url)
41 return soup.title.contents[0].encode('utf-8').strip()
43 def decorate(proxy, original):
44 # as decorator 'overwrites' namespace with these proxy
45 # functions, we need to make the introspecting code happy.
46 proxy.__doc__ = original.__doc__
47 return proxy
49 def limitFreq(secs, msg=None):
50 """
51 Limit the frequency of execution of the function to avoid IRC spamming
52 """
53 lastmsg = [None]
54 def decorator(func):
55 def proxy(self, arg=None):
56 now = datetime.now()
57 if lastmsg[0] is not None:
58 diff = now - lastmsg[0]
59 if diff.seconds < secs:
60 log.msg('too quick!')
61 if msg:
62 self.toUser(msg)
63 return
65 func(self, arg)
66 lastmsg[0] = now
67 return decorate(proxy, func)
69 return decorator
71 def admin(nicks):
72 """
73 Only admin listed in `nicks` can run this command. Silently fail otherwise.
74 """
75 def decorator(func):
76 def proxy(self, *args, **kwargs):
77 if self.user in nicks:
78 return func(self, *args, **kwargs)
79 else:
80 log.msg('ADMIN: %s not authorized to run command %s' % (self.user, func.__name__))
82 return decorate(proxy, func)
83 return decorator
85 class BotActions(object):
87 def __init__(self, bot):
88 self.bot = bot
90 def toChannel(self, msg):
91 self.bot.msg(self.channel, msg)
93 def toUser(self, msg):
94 self.toChannel('%s: %s' % (self.user, msg))
96 def doAction(self, action):
97 self.bot.me(self.channel, action)
99 def grep(self, msg, words):
100 for word in words:
101 # XXX: this does not search for a word, but substring!
102 if word in msg:
103 return True
104 return False
107 class Commands(BotActions):
109 def __init__(self, bot, user, channel):
110 BotActions.__init__(self, bot)
111 self.user = user
112 self.channel = channel
114 def run(self, rest):
115 parts = rest.split(None, 1)
116 command = parts[0]
117 args = parts[1:]
118 try:
119 cmdFunc = getattr(self, 'cmd_%s' % command)
120 except AttributeError:
121 log.msg('no such command: %s', command)
122 else:
123 cmdFunc(*args)
125 def cmd_ping(self, args=None):
126 "respond to PING"
127 self.toUser(args and 'PONG %s' % args or 'PONG')
128 cmd_PING = cmd_ping
130 @admin(['srid'])
131 def cmd_reload(self, args=None):
132 "reload plugins.py (self)"
133 import plugins
134 try:
135 reload(plugins)
136 except Exception, e:
137 self.toUser(e)
138 else:
139 self.toUser('reloaded')
140 cmd_r = cmd_reload
142 @admin(['srid'])
143 @limitFreq(15)
144 def cmd_help(self, args=None):
145 ignore = ('help', 'PING')
146 for name, func, docstring in self:
147 if name not in ignore and docstring is not None:
148 self.toChannel('%10s - %s' % (name, docstring))
150 # more useful commands
152 def cmd_eval(self, args=None):
153 "evaluate a python expression"
154 if args:
155 from math import * # make math functions available to `eval`
156 try:
157 result = eval(args)
158 except SyntaxError, e:
159 self.toUser('wrong syntax')
160 except Exception, e:
161 self.toUser(e)
162 else:
163 self.toChannel(result)
164 cmd_e = cmd_eval
166 @limitFreq(60*2)
167 def cmd_google(self, search_terms=None):
168 "search the internet"
169 if search_terms:
170 soup = soupify('http://www.google.com/search?' + urllib.urlencode({'q': search_terms}))
171 firstLink = soup.find('a', attrs={'class': 'l'})['href'].encode('utf-8').strip()
172 log.msg('google:', firstLink)
173 self.toChannel(firstLink)
174 cmd_g = cmd_google
176 def cmd_tell(self, args=None):
177 if args:
178 user, message = args.split(None, 1)
179 # XXX: add to db
180 self.toUser('ok')
182 @admin(['srid'])
183 def cmd_join(self, channel=None):
184 if channel:
185 self.bot.join(channel)
187 @admin(['srid'])
188 def cmd_leave(self, channel=None):
189 if channel:
190 self.bot.leave(channel)
192 @admin(['srid'])
193 def cmd_do(self, args):
194 # perform an action in a channel
195 # > ,do #foo is bored
196 channel, action = args.split(None, 1)
197 self.bot.me(channel, action)
199 def __iter__(self):
200 "Iterate over all commands (minus their aliases)"
201 cmdMap = {}
202 for attr in dir(self):
203 if attr.startswith('cmd_'):
204 func = getattr(self, attr)
205 name = attr[len('cmd_'):]
206 if func in cmdMap:
207 # find and overwrite the alias.
208 # the name of an alias is shorter than the name of the original version.
209 if len(cmdMap[func]) < len(name):
210 cmdMap[func] = name
211 else:
212 cmdMap[func] = name
214 for func, name in cmdMap.items():
215 yield name, func, func.__doc__
218 class Filters(BotActions):
220 def __init__(self, bot, user, channel):
221 BotActions.__init__(self, bot)
222 self.user = user
223 self.channel = channel
225 def run(self, msg):
226 for attr in dir(self):
227 if attr.startswith('filter_'):
228 filter = getattr(self, attr)
229 filter(msg)
231 def respondWisely(self, msg, for_words, with_responses, one_for_every=1):
232 random.seed(time.time())
233 if self.grep(msg, for_words):
234 if random.choice(range(one_for_every)) == 0:
235 self.toChannel(random.choice(with_responses))
237 def filter_obscene(self, msg):
238 words = ['fuck', 'f**k', 'bitch', 'cunt', 'tits']
239 quotes = [
240 'The rate at which a person can mature is directly proportional to the embarrassment he can tolerate.',
241 'To make mistakes is human; to stumble is commonplace; to be able to laugh at yourself is maturity.',
242 'It is the province of knowledge to speak and it is the privilege of wisdom to listen.',
243 'Some folks are wise and some otherwise.',
244 'The wise man doesn\'t give the right answers, he poses the right questions.',
245 'There are many who know many things, yet are lacking in wisdom.',
246 'Wise men hear and see as little children do.',
247 'Speech both conceals and reveals the thoughts of men',
248 '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.',
249 'The trouble with her is that she lacks the power of conversation but not the power of speech.',
250 'Speak when you are angry - and you\'ll make the best speech you\'ll ever regret.',
251 'Speech is human, silence is divine, yet also brutish and dead: therefore we must learn both arts.',
252 'We must have reasons for speech but we need none for silence',
253 'It usually takes more than three weeks to prepare a good impromptu speech.',
254 'Men use thought only as authority for their injustice, and employ speech only to conceal their thoughts',
255 '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.',
256 'Silence is also speech',
257 '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.',
260 self.respondWisely(msg, words, quotes)
262 def filter_hatred(self, msg):
263 words = ['hate', 'suck', 'bad']
264 quotes = [
265 'The first reaction to truth is hatred',
266 'Hatred does not cease by hatred, but only by love; this is the eternal rule.',
267 'Hatred is settled anger',
268 'I love humanity but I hate people.',
269 'Let them hate, so long as they fear.',
270 'What we need is hatred. From it our ideas are born.',
273 self.respondWisely(msg, words, quotes, 3)
275 def filter_m8ball(self, msg):
276 # This list is taken from fsbot@emacs (type ',dl m8ball')
277 answers = [
278 "Yes", "No", "Definitely", "Of course not!", "Highly likely.",
279 "Ask yourself, do you really want to know?",
280 "I'm telling you, you don't want to know.",
281 "mu!"
284 self.respondWisely(msg, ['??'], answers)
286 def filter_random(self, msg):
287 random.seed(time.time())
288 if random.choice(range(100)) == 0:
289 self.doAction(
290 random.choice([
291 'looks around nervously',
294 _filter_links_url = re.compile('.*(https?://[^ ]*).*')
295 @limitFreq(60*2)
296 def filter_links(self, msg):
297 "Show <title> in channel"
298 match = self._filter_links_url.match(msg)
299 if match:
300 self.toChannel('Title: %s' % urlTitle(match.group(1)))