1 from __future__
import with_statement
4 from twisted
.python
import log
6 from datetime
import datetime
7 import random
, time
, re
, urllib
9 from contextlib
import contextmanager
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
+ ':'])
23 commands
= Commands(bot
, user
, channel
)
27 filters
= Filters(bot
, user
, channel
)
30 def prefixMatch(string
, prefixes
):
31 for prefix
in prefixes
:
32 if string
.startswith(prefix
):
33 return string
[len(prefix
):]
38 def probability_of_one_by(n
):
39 random
.seed(time
.time())
40 yield 0 == random
.choice(range(n
))
43 from BeautifulSoup
import BeautifulSoup
45 req
= urllib2
.Request(url
, None, {'User-agent': 'mozilla'})
46 contents
= urllib2
.urlopen(req
).read()
47 return BeautifulSoup(contents
)
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
__
59 def limitFreq(secs
, msg
=None):
61 Limit the frequency of execution of the function to avoid IRC spamming
65 def proxy(self
, arg
=None):
67 if lastmsg
[0] is not None:
68 diff
= now
- lastmsg
[0]
69 if diff
.seconds
< secs
:
77 return decorate(proxy
, func
)
83 Only admin listed in `nicks` can run this command. Silently fail otherwise.
86 def proxy(self
, *args
, **kwargs
):
87 if self
.user
in nicks
:
88 return func(self
, *args
, **kwargs
)
90 log
.msg('ADMIN: %s not authorized to run command %s' % (self
.user
, func
.__name
__))
92 return decorate(proxy
, func
)
95 class BotActions(object):
97 def __init__(self
, 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
):
111 # XXX: this does not search for a word, but substring!
117 class Commands(BotActions
):
119 def __init__(self
, bot
, user
, channel
):
120 BotActions
.__init
__(self
, bot
)
122 self
.channel
= channel
125 parts
= rest
.split(None, 1)
129 cmdFunc
= getattr(self
, 'cmd_%s' % command
)
130 except AttributeError:
131 log
.msg('no such command: %s', command
)
135 def cmd_ping(self
, args
=None):
137 self
.toUser(args
and 'PONG %s' % args
or 'PONG')
141 def cmd_reload(self
, args
=None):
142 "reload plugins.py (self)"
149 self
.toUser('reloaded')
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"
163 from math
import * # make math functions available to `eval`
166 except SyntaxError, e
:
167 self
.toUser('wrong syntax')
171 self
.toChannel(result
)
174 def cmd_google(self
, search_terms
=None):
175 "search the internet"
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
)
185 def cmd_source(self
, args
=None):
186 "how to access lusty's source code"
189 def cmd_tell(self
, args
=None):
191 user
, message
= args
.split(None, 1)
196 def cmd_join(self
, channel
=None):
198 self
.bot
.join(channel
)
201 def cmd_leave(self
, channel
=None):
203 self
.bot
.leave(channel
)
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
)
213 "Iterate over all commands (minus their aliases)"
215 for attr
in dir(self
):
216 if attr
.startswith('cmd_'):
217 func
= getattr(self
, attr
)
218 name
= attr
[len('cmd_'):]
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
):
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
)
237 self
.channel
= channel
240 for attr
in dir(self
):
241 if attr
.startswith('filter_'):
242 filter = getattr(self
, attr
)
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
:
250 self
.toChannel(random
.choice(with_responses
))
252 def filter_obscene(self
, msg
):
253 words
= ['fuck', 'f**k', 'bitch', 'cunt', 'tits']
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']
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')
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.",
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
:
305 with
probability_of_one_by(2) as do2
:
309 'looks around nervously',
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
)
322 self
.toChannel('Title: %s' % urlTitle(match
.group(1)))