test
[lusty.git] / plugins.py
blob6625d15253f4d4f90d1493246834e3a187844c6f
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 if word in msg:
102 return True
103 return False
106 class Commands(BotActions):
108 def __init__(self, bot, user, channel):
109 BotActions.__init__(self, bot)
110 self.user = user
111 self.channel = channel
113 def run(self, rest):
114 parts = rest.split(None, 1)
115 command = parts[0]
116 args = parts[1:]
117 try:
118 cmdFunc = getattr(self, 'cmd_%s' % command)
119 except AttributeError:
120 log.msg('no such command: %s', command)
121 else:
122 cmdFunc(*args)
124 def cmd_ping(self, args=None):
125 "respond to PING"
126 self.toUser(args and 'PONG %s' % args or 'PONG')
127 cmd_PING = cmd_ping
129 @admin(['srid'])
130 def cmd_reload(self, args=None):
131 "reload plugins.py (self)"
132 import plugins
133 try:
134 reload(plugins)
135 except Exception, e:
136 self.toUser(e)
137 else:
138 self.toUser('reloaded')
139 cmd_r = cmd_reload
141 @admin(['srid'])
142 @limitFreq(15)
143 def cmd_help(self, args=None):
144 ignore = ('help', 'PING')
145 for name, func, docstring in self:
146 if name not in ignore and docstring is not None:
147 self.toChannel('%10s - %s' % (name, docstring))
149 # more useful commands
151 def cmd_eval(self, args=None):
152 "evaluate a python expression"
153 if args:
154 from math import * # make math functions available to `eval`
155 try:
156 result = eval(args)
157 except SyntaxError, e:
158 self.toUser('wrong syntax')
159 except Exception, e:
160 self.toUser(e)
161 else:
162 self.toChannel(result)
163 cmd_e = cmd_eval
165 def cmd_google(self, search_terms=None):
166 "search the internet"
167 if search_terms:
168 soup = soupify('http://www.google.com/search?' + urllib.urlencode({'q': search_terms}))
169 firstLink = soup.find('a', attrs={'class': 'l'})['href'].encode('utf-8').strip()
170 log.msg('google:', firstLink)
171 self.toChannel(firstLink)
172 cmd_g = cmd_google
174 def cmd_tell(self, args=None):
175 if args:
176 user, message = args.split(None, 1)
177 # XXX: add to db
178 self.toUser('ok')
180 @admin(['srid'])
181 def cmd_join(self, channel=None):
182 if channel:
183 self.bot.join(channel)
185 @admin(['srid'])
186 def cmd_leave(self, channel=None):
187 if channel:
188 self.bot.leave(channel)
190 @admin(['srid'])
191 def cmd_do(self, args):
192 # perform an action in a channel
193 # > ,do #foo is bored
194 channel, action = args.split(None, 1)
195 self.bot.me(channel, action)
197 def __iter__(self):
198 "Iterate over all commands (minus their aliases)"
199 cmdMap = {}
200 for attr in dir(self):
201 if attr.startswith('cmd_'):
202 func = getattr(self, attr)
203 name = attr[len('cmd_'):]
204 if func in cmdMap:
205 # find and overwrite the alias.
206 # the name of an alias is shorter than the name of the original version.
207 if len(cmdMap[func]) < len(name):
208 cmdMap[func] = name
209 else:
210 cmdMap[func] = name
212 for func, name in cmdMap.items():
213 yield name, func, func.__doc__
216 class Filters(BotActions):
218 def __init__(self, bot, user, channel):
219 BotActions.__init__(self, bot)
220 self.user = user
221 self.channel = channel
223 def run(self, msg):
224 for attr in dir(self):
225 if attr.startswith('filter_'):
226 filter = getattr(self, attr)
227 filter(msg)
229 def respondWisely(self, msg, for_words, with_responses, one_for_every=1):
230 random.seed(time.time())
231 if self.grep(msg, for_words):
232 if random.choice(range(one_for_every)) == 0:
233 self.toChannel(random.choice(with_responses))
235 def filter_obscene(self, msg):
236 words = ['fuck', 'f**k', 'bitch', 'cunt', 'tits']
237 quotes = [
238 'The rate at which a person can mature is directly proportional to the embarrassment he can tolerate.',
239 'To make mistakes is human; to stumble is commonplace; to be able to laugh at yourself is maturity.',
240 'It is the province of knowledge to speak and it is the privilege of wisdom to listen.',
241 'Some folks are wise and some otherwise.',
242 'The wise man doesn\'t give the right answers, he poses the right questions.',
243 'There are many who know many things, yet are lacking in wisdom.',
244 'Wise men hear and see as little children do.',
245 'Speech both conceals and reveals the thoughts of men',
246 '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.',
247 'The trouble with her is that she lacks the power of conversation but not the power of speech.',
248 'Speak when you are angry - and you\'ll make the best speech you\'ll ever regret.',
249 'Speech is human, silence is divine, yet also brutish and dead: therefore we must learn both arts.',
250 'We must have reasons for speech but we need none for silence',
251 'It usually takes more than three weeks to prepare a good impromptu speech.',
252 'Men use thought only as authority for their injustice, and employ speech only to conceal their thoughts',
253 '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.',
254 'Silence is also speech',
255 '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.',
258 self.respondWisely(msg, words, quotes)
260 def filter_hatred(self, msg):
261 words = ['hate', 'suck', 'bad']
262 quotes = [
263 'The first reaction to truth is hatred',
264 'Hatred does not cease by hatred, but only by love; this is the eternal rule.',
265 'Hatred is settled anger',
266 'I love humanity but I hate people.',
267 'Let them hate, so long as they fear.',
268 'What we need is hatred. From it our ideas are born.',
271 self.respondWisely(msg, words, quotes, 3)
273 def filter_m8ball(self, msg):
274 # This list is taken from fsbot@emacs (type ',dl m8ball')
275 answers = [
276 "Yes", "No", "Definitely", "Of course not!", "Highly likely.",
277 "Ask yourself, do you really want to know?",
278 "I'm telling you, you don't want to know.",
279 "mu!"
282 self.respondWisely(msg, ['??'], answers)
284 def filter_random(self, msg):
285 random.seed(time.time())
286 if random.choice(range(100)) == 0:
287 self.doAction(
288 random.choice([
289 'looks around nervously',
292 _filter_links_url = re.compile('.*(https?://[^ ]*).*')
293 def filter_links(self, msg):
294 "Show <title> in channel"
295 match = self._filter_links_url.match(msg)
296 if match:
297 self.toChannel('Title: %s' % urlTitle(match.group(1)))