Skip to content
Newer
Older
100644 372 lines (320 sloc) 15.2 KB
30cddb2 initial commit
Michael Yanovich authored Dec 11, 2009
1 #!/usr/bin/env python
2 """
d93f349 @myano standardized the copyright notice across all files
authored Jan 29, 2013
3 bot.py - jenni IRC Bot
4 Copyright 2009-2013, Michael Yanovich (yanovich.net)
5 Copyright 2008-2013, Sean B. Palmer (inamidst.com)
30cddb2 initial commit
Michael Yanovich authored Dec 12, 2009
6 Licensed under the Eiffel Forum License 2.
7
4dae575 @myano SECURITY: Strip "\x01" out of commands being sent to the server. Only…
authored Oct 7, 2012
8 More info:
d93f349 @myano standardized the copyright notice across all files
authored Jan 29, 2013
9 * jenni: https://github.com/myano/jenni/
4dae575 @myano SECURITY: Strip "\x01" out of commands being sent to the server. Only…
authored Oct 7, 2012
10 * Phenny: http://inamidst.com/phenny/
30cddb2 initial commit
Michael Yanovich authored Dec 12, 2009
11 """
beb4c61 @myano updated jenni with sbp's latest patch to fix an explot via .py
authored Sep 5, 2011
12
5f6fd69 @myano added global rate limit module
authored Jun 18, 2012
13 import time, sys, os, re, threading, imp
30cddb2 initial commit
Michael Yanovich authored Dec 12, 2009
14 import irc
15
beb4c61 @myano updated jenni with sbp's latest patch to fix an explot via .py
authored Sep 6, 2011
16 home = os.getcwd()
30cddb2 initial commit
Michael Yanovich authored Dec 12, 2009
17
6986adf @myano fixed spacing on core files
authored Jun 28, 2011
18 def decode(bytes):
19 try: text = bytes.decode('utf-8')
20 except UnicodeDecodeError:
21 try: text = bytes.decode('iso-8859-1')
22 except UnicodeDecodeError:
23 text = bytes.decode('cp1252')
24 return text
25
26 class Jenni(irc.Bot):
27 def __init__(self, config):
624211d @myano core updates (made IPv6 optional)
authored Oct 21, 2014
28 lc_pm = None
3b25876 @myano added ability for jenni to log PMs to a channel definied in the config.
authored Jun 19, 2012
29 if hasattr(config, "logchan_pm"): lc_pm = config.logchan_pm
624211d @myano core updates (made IPv6 optional)
authored Oct 21, 2014
30 logging = False
4dae575 @myano SECURITY: Strip "\x01" out of commands being sent to the server. Only…
authored Oct 7, 2012
31 if hasattr(config, "logging"): logging = config.logging
624211d @myano core updates (made IPv6 optional)
authored Oct 21, 2014
32 ipv6 = False
33 if hasattr(config, 'ipv6'): ipv6 = config.ipv6
8ac3f61 @vista- proper indentation
vista- authored Feb 11, 2015
34 serverpass = None
35 if hasattr(config, 'serverpass'): serverpass = config.serverpass
77b1d7a @solidgoldbomb auth: allow config of independent user and nick parameters for auth
solidgoldbomb authored May 24, 2015
36 user = None
37 if hasattr(config, 'user'): user = config.user
38 args = (config.nick, config.name, config.channels, user, serverpass, lc_pm, logging, ipv6)
2e49668 @myano cleaned up some code and added more verbose output for weather lookup
authored Jun 25, 2013
39 ## next, try putting a try/except around the following line
6986adf @myano fixed spacing on core files
authored Jun 29, 2011
40 irc.Bot.__init__(self, *args)
41 self.config = config
42 self.doc = {}
43 self.stats = {}
5f6fd69 @myano added global rate limit module
authored Jun 19, 2012
44 self.times = {}
624211d @myano core updates (made IPv6 optional)
authored Oct 21, 2014
45 self.excludes = {}
644ccfc @myano added self.excludes, removed this previously thinking it wasn't needed.
authored Aug 13, 2012
46 if hasattr(config, 'excludes'):
47 self.excludes = config.excludes
6986adf @myano fixed spacing on core files
authored Jun 29, 2011
48 self.setup()
49
50 def setup(self):
51 self.variables = {}
52
53 filenames = []
54
0bd45e6 revamped module loading, addresses #202 issue
Your Name authored Aug 10, 2015
55 # Default module folder + extra folders
56 module_folders = [os.path.join(home, 'modules')]
57 module_folders.extend(getattr(self.config, 'extra', []))
7f863bf @myano removed unnecessary whitespace
authored Nov 6, 2015
58
0bd45e6 revamped module loading, addresses #202 issue
Your Name authored Aug 10, 2015
59 excluded = getattr(self.config, 'exclude', [])
60 enabled = getattr(self.config, 'enable', [])
61
62 for folder in module_folders:
63 if os.path.isfile(folder):
64 filenames.append(folder)
65 elif os.path.isdir(folder):
66 for fn in os.listdir(folder):
67 if fn.endswith('.py') and not fn.startswith('_'):
68 name = os.path.basename(fn)[:-3]
69 # If whitelist is present only include whitelisted
70 # Never include blacklisted items
71 if name in enabled or not enabled and name not in excluded:
72 filenames.append(os.path.join(folder, fn))
6986adf @myano fixed spacing on core files
authored Jun 29, 2011
73
74 modules = []
75 for filename in filenames:
76 name = os.path.basename(filename)[:-3]
beb4c61 @myano updated jenni with sbp's latest patch to fix an explot via .py
authored Sep 6, 2011
77 # if name in sys.modules:
78 # del sys.modules[name]
6986adf @myano fixed spacing on core files
authored Jun 29, 2011
79 try: module = imp.load_source(name, filename)
80 except Exception, e:
81 print >> sys.stderr, "Error loading %s: %s (in bot.py)" % (name, e)
82 else:
83 if hasattr(module, 'setup'):
84 module.setup(self)
85 self.register(vars(module))
86 modules.append(name)
87
88 if modules:
0ec9f1d @kaneda [abjonnes-sorted_modules] Sorting modules
kaneda authored Apr 5, 2015
89 print >> sys.stderr, 'Registered modules:', ', '.join(sorted(modules))
90 else:
91 print >> sys.stderr, "Warning: Couldn't find any modules"
6986adf @myano fixed spacing on core files
authored Jun 29, 2011
92
93 self.bind_commands()
94
95 def register(self, variables):
96 # This is used by reload.py, hence it being methodised
97 for name, obj in variables.iteritems():
98 if hasattr(obj, 'commands') or hasattr(obj, 'rule'):
99 self.variables[name] = obj
100
101 def bind_commands(self):
102 self.commands = {'high': {}, 'medium': {}, 'low': {}}
103
b7c7fe4 @myano fixed loading problem of find.py
authored Jun 29, 2011
104 def bind(self, priority, regexp, func):
6986adf @myano fixed spacing on core files
authored Jun 29, 2011
105 # register documentation
106 if not hasattr(func, 'name'):
107 func.name = func.__name__
108 if func.__doc__:
109 if hasattr(func, 'example'):
110 example = func.example
111 example = example.replace('$nickname', self.nick)
112 else: example = None
113 self.doc[func.name] = (func.__doc__, example)
114 self.commands[priority].setdefault(regexp, []).append(func)
0ec9f1d @kaneda [abjonnes-sorted_modules] Sorting modules
kaneda authored Apr 5, 2015
115 regexp = re.sub('\x01|\x02', '', regexp.pattern)
116 return (func.__module__, func.__name__, regexp, priority)
6986adf @myano fixed spacing on core files
authored Jun 29, 2011
117
118 def sub(pattern, self=self):
119 # These replacements have significant order
beb4c61 @myano updated jenni with sbp's latest patch to fix an explot via .py
authored Sep 6, 2011
120 pattern = pattern.replace('$nickname', re.escape(self.nick))
121 return pattern.replace('$nick', r'%s[,:] +' % re.escape(self.nick))
6986adf @myano fixed spacing on core files
authored Jun 29, 2011
122
0ec9f1d @kaneda [abjonnes-sorted_modules] Sorting modules
kaneda authored Apr 5, 2015
123 bound_funcs = []
6986adf @myano fixed spacing on core files
authored Jun 29, 2011
124 for name, func in self.variables.iteritems():
125 # print name, func
126 if not hasattr(func, 'priority'):
127 func.priority = 'medium'
128
129 if not hasattr(func, 'thread'):
130 func.thread = True
131
132 if not hasattr(func, 'event'):
133 func.event = 'PRIVMSG'
a081bb3 @myano fixed critical bug that would prevent jenni from starting
authored Mar 18, 2014
134 else:
135 if func.event:
136 func.event = func.event.upper()
137 else:
138 continue
6986adf @myano fixed spacing on core files
authored Jun 29, 2011
139
5f6fd69 @myano added global rate limit module
authored Jun 19, 2012
140 if not hasattr(func, 'rate'):
0f93e0b @myano re-did rate-limit default so all modules don't need to be programmed
authored Jun 22, 2012
141 if hasattr(func, 'commands'):
3dee418 @myano reduced global rate limit to 3 seconds for nondefined functions
authored Jul 12, 2013
142 func.rate = 3
0f93e0b @myano re-did rate-limit default so all modules don't need to be programmed
authored Jun 22, 2012
143 else:
3dee418 @myano reduced global rate limit to 3 seconds for nondefined functions
authored Jul 12, 2013
144 func.rate = -1
5f6fd69 @myano added global rate limit module
authored Jun 19, 2012
145
6986adf @myano fixed spacing on core files
authored Jun 29, 2011
146 if hasattr(func, 'rule'):
147 if isinstance(func.rule, str):
148 pattern = sub(func.rule)
149 regexp = re.compile(pattern)
0ec9f1d @kaneda [abjonnes-sorted_modules] Sorting modules
kaneda authored Apr 5, 2015
150 bound_funcs.append(bind(self, func.priority, regexp, func))
6986adf @myano fixed spacing on core files
authored Jun 29, 2011
151
152 if isinstance(func.rule, tuple):
153 # 1) e.g. ('$nick', '(.*)')
154 if len(func.rule) == 2 and isinstance(func.rule[0], str):
155 prefix, pattern = func.rule
156 prefix = sub(prefix)
157 regexp = re.compile(prefix + pattern)
0ec9f1d @kaneda [abjonnes-sorted_modules] Sorting modules
kaneda authored Apr 5, 2015
158 bound_funcs.append(bind(self, func.priority, regexp, func))
6986adf @myano fixed spacing on core files
authored Jun 29, 2011
159
160 # 2) e.g. (['p', 'q'], '(.*)')
161 elif len(func.rule) == 2 and isinstance(func.rule[0], list):
162 prefix = self.config.prefix
163 commands, pattern = func.rule
164 for command in commands:
47dab40 @myano make commands/triggers case insensitive
authored Dec 19, 2013
165 command = r'(?i)(%s)\b(?: +(?:%s))?' % (command, pattern)
6986adf @myano fixed spacing on core files
authored Jun 29, 2011
166 regexp = re.compile(prefix + command)
0ec9f1d @kaneda [abjonnes-sorted_modules] Sorting modules
kaneda authored Apr 5, 2015
167 bound_funcs.append(bind(self, func.priority, regexp, func))
6986adf @myano fixed spacing on core files
authored Jun 29, 2011
168
169 # 3) e.g. ('$nick', ['p', 'q'], '(.*)')
170 elif len(func.rule) == 3:
171 prefix, commands, pattern = func.rule
172 prefix = sub(prefix)
173 for command in commands:
47dab40 @myano make commands/triggers case insensitive
authored Dec 19, 2013
174 command = r'(?i)(%s) +' % command
6986adf @myano fixed spacing on core files
authored Jun 29, 2011
175 regexp = re.compile(prefix + command + pattern)
0ec9f1d @kaneda [abjonnes-sorted_modules] Sorting modules
kaneda authored Apr 5, 2015
176 bound_funcs.append(bind(self, func.priority, regexp, func))
6986adf @myano fixed spacing on core files
authored Jun 29, 2011
177
178 if hasattr(func, 'commands'):
179 for command in func.commands:
47dab40 @myano make commands/triggers case insensitive
authored Dec 19, 2013
180 template = r'(?i)^%s(%s)(?: +(.*))?$'
6986adf @myano fixed spacing on core files
authored Jun 29, 2011
181 pattern = template % (self.config.prefix, command)
182 regexp = re.compile(pattern)
0ec9f1d @kaneda [abjonnes-sorted_modules] Sorting modules
kaneda authored Apr 5, 2015
183 bound_funcs.append(bind(self, func.priority, regexp, func))
184
185 max_pattern_width = max(len(f[2]) for f in bound_funcs)
186 for module, name, regexp, priority in sorted(bound_funcs):
187 encoded_regex = regexp.encode('utf-8').ljust(max_pattern_width)
188 print ('{0} | {1}.{2}, {3} priority'.format(encoded_regex, module, name, priority))
6986adf @myano fixed spacing on core files
authored Jun 29, 2011
189
190 def wrapped(self, origin, text, match):
191 class JenniWrapper(object):
192 def __init__(self, jenni):
432eebe @solidgoldbomb bot: hide the wrapped class in JenniWrapper class
solidgoldbomb authored Jun 3, 2015
193 self._bot = jenni
6986adf @myano fixed spacing on core files
authored Jun 29, 2011
194
195 def __getattr__(self, attr):
196 sender = origin.sender or text
197 if attr == 'reply':
198 return (lambda msg:
432eebe @solidgoldbomb bot: hide the wrapped class in JenniWrapper class
solidgoldbomb authored Jun 3, 2015
199 self._bot.msg(sender, origin.nick + ': ' + msg))
6986adf @myano fixed spacing on core files
authored Jun 29, 2011
200 elif attr == 'say':
432eebe @solidgoldbomb bot: hide the wrapped class in JenniWrapper class
solidgoldbomb authored Jun 3, 2015
201 return lambda msg: self._bot.msg(sender, msg)
9d2d3c7 @solidgoldbomb bot: warn about deprecated usage of jenni.bot.foo syntax
solidgoldbomb authored Jun 3, 2015
202 elif attr == 'bot':
203 # Allow deprecated usage of jenni.bot.foo but print a warning to the console
204 print "Warning: Direct access to jenni.bot.foo is deprecated. Please use jenni.foo instead."
205 import traceback
206 traceback.print_stack()
207 # Let this keep working by passing it transparently to _bot
208 return self._bot
432eebe @solidgoldbomb bot: hide the wrapped class in JenniWrapper class
solidgoldbomb authored Jun 3, 2015
209 return getattr(self._bot, attr)
6986adf @myano fixed spacing on core files
authored Jun 29, 2011
210
105e3f4 @solidgoldbomb bot: pass setattr calls through to wrapper class transparently
solidgoldbomb authored May 31, 2015
211 def __setattr__(self, attr, value):
212 if attr in ('_bot',):
213 # Explicitly allow the wrapped class to be set during __init__()
214 return super(JenniWrapper, self).__setattr__(attr, value)
215 else:
216 # All other attributes will be set on the wrapped class transparently
217 return setattr(self._bot, attr, value)
218
6986adf @myano fixed spacing on core files
authored Jun 29, 2011
219 return JenniWrapper(self)
220
221 def input(self, origin, text, bytes, match, event, args):
222 class CommandInput(unicode):
223 def __new__(cls, text, origin, bytes, match, event, args):
224 s = unicode.__new__(cls, text)
225 s.sender = origin.sender
226 s.nick = origin.nick
227 s.event = event
228 s.bytes = bytes
229 s.match = match
230 s.group = match.group
231 s.groups = match.groups
8325385 @myano cleanup commit
authored Jun 6, 2014
232 s.ident = origin.user
233 s.raw = origin
6986adf @myano fixed spacing on core files
authored Jun 29, 2011
234 s.args = args
c7006bb @kaneda [dev-ban-words] Add modes and ban words
kaneda authored Feb 8, 2015
235 s.mode = origin.mode
236 s.mode_target = origin.mode_target
237 s.names = origin.names
238 s.full_ident = origin.full_ident
6986adf @myano fixed spacing on core files
authored Jun 29, 2011
239 s.admin = origin.nick in self.config.admins
b1741b7 @myano updated jenni so one can add regular expressions for hostmasks to the
authored Mar 30, 2012
240 if s.admin == False:
241 for each_admin in self.config.admins:
242 re_admin = re.compile(each_admin)
243 if re_admin.findall(origin.host):
244 s.admin = True
7ed5cac @myano fixed bug where owner wasn't being nadded to the list of admins
authored Mar 30, 2012
245 elif '@' in each_admin:
246 temp = each_admin.split('@')
247 re_host = re.compile(temp[1])
248 if re_host.findall(origin.host):
249 s.admin = True
901b431 @myano added ability to declare nick + host for owner
authored Mar 30, 2012
250 s.owner = origin.nick + '@' + origin.host == self.config.owner
251 if s.owner == False: s.owner = origin.nick == self.config.owner
ddbc830 @myano added ability for modules to now see the host of the user running the
authored Mar 30, 2012
252 s.host = origin.host
6986adf @myano fixed spacing on core files
authored Jun 29, 2011
253 return s
254
255 return CommandInput(text, origin, bytes, match, event, args)
256
257 def call(self, func, origin, jenni, input):
5f6fd69 @myano added global rate limit module
authored Jun 19, 2012
258 nick = (input.nick).lower()
624211d @myano core updates (made IPv6 optional)
authored Oct 21, 2014
259
260 ## rate limiting
5f6fd69 @myano added global rate limit module
authored Jun 19, 2012
261 if nick in self.times:
262 if func in self.times[nick]:
263 if not input.admin:
624211d @myano core updates (made IPv6 optional)
authored Oct 21, 2014
264 ## admins are not rate limited
5f6fd69 @myano added global rate limit module
authored Jun 19, 2012
265 if time.time() - self.times[nick][func] < func.rate:
266 self.times[nick][func] = time.time()
267 return
624211d @myano core updates (made IPv6 optional)
authored Oct 21, 2014
268 else:
269 self.times[nick] = dict()
270
5f6fd69 @myano added global rate limit module
authored Jun 19, 2012
271 self.times[nick][func] = time.time()
624211d @myano core updates (made IPv6 optional)
authored Oct 21, 2014
272
5f6fd69 @myano added global rate limit module
authored Jun 19, 2012
273 try:
5fae2f9 @myano added the ability to have jenni blacklist modules for different
authored Aug 13, 2012
274 if hasattr(self, 'excludes'):
9a7d0d5 @myano make it check for incase-sensitively for channel names
authored Apr 29, 2015
275 if (input.sender).lower() in self.excludes:
276 if '!' in self.excludes[(input.sender).lower()]:
5fae2f9 @myano added the ability to have jenni blacklist modules for different
authored Aug 13, 2012
277 # block all function calls for this channel
278 return
279 fname = func.func_code.co_filename.split('/')[-1].split('.')[0]
9a7d0d5 @myano make it check for incase-sensitively for channel names
authored Apr 29, 2015
280 if fname in self.excludes[(input.sender).lower()]:
5fae2f9 @myano added the ability to have jenni blacklist modules for different
authored Aug 13, 2012
281 # block function call if channel is blacklisted
282 return
283 except Exception, e:
284 print "Error attempting to block:", str(func.name)
285 self.error(origin)
286
287 try:
5f6fd69 @myano added global rate limit module
authored Jun 19, 2012
288 func(jenni, input)
6986adf @myano fixed spacing on core files
authored Jun 29, 2011
289 except Exception, e:
290 self.error(origin)
291
292 def limit(self, origin, func):
293 if origin.sender and origin.sender.startswith('#'):
294 if hasattr(self.config, 'limit'):
295 limits = self.config.limit.get(origin.sender)
296 if limits and (func.__module__ not in limits):
297 return True
298 return False
299
300 def dispatch(self, origin, args):
301 bytes, event, args = args[0], args[1], args[2:]
302 text = decode(bytes)
beb4c61 @myano updated jenni with sbp's latest patch to fix an explot via .py
authored Sep 6, 2011
303
6986adf @myano fixed spacing on core files
authored Jun 29, 2011
304 for priority in ('high', 'medium', 'low'):
305 items = self.commands[priority].items()
306 for regexp, funcs in items:
307 for func in funcs:
308 if event != func.event: continue
309
310 match = regexp.match(text)
311 if match:
312 if self.limit(origin, func): continue
beb4c61 @myano updated jenni with sbp's latest patch to fix an explot via .py
authored Sep 6, 2011
313
6986adf @myano fixed spacing on core files
authored Jun 29, 2011
314 jenni = self.wrapped(origin, text, match)
315 input = self.input(origin, text, bytes, match, event, args)
beb4c61 @myano updated jenni with sbp's latest patch to fix an explot via .py
authored Sep 6, 2011
316
5f6fd69 @myano added global rate limit module
authored Jun 19, 2012
317 nick = (input.nick).lower()
318
9ea07c2 @myano changed some logic flow in bot.py to prevent the bot from crashing if a
authored Aug 16, 2012
319 # blocking ability
e8cd873 @myano added ability to have jenni block on nick and hostmask; also updated tld
authored Dec 28, 2011
320 if os.path.isfile("blocks"):
321 g = open("blocks", "r")
322 contents = g.readlines()
323 g.close()
324
cafe4e6 @myano updated code to allow for 0 entries for both hostmasks and nicks; this
authored Jan 6, 2012
325 try: bad_masks = contents[0].split(',')
326 except: bad_masks = ['']
327
328 try: bad_nicks = contents[1].split(',')
329 except: bad_nicks = ['']
e8cd873 @myano added ability to have jenni block on nick and hostmask; also updated tld
authored Dec 28, 2011
330
9ea07c2 @myano changed some logic flow in bot.py to prevent the bot from crashing if a
authored Aug 16, 2012
331 # check for blocked hostmasks
e8cd873 @myano added ability to have jenni block on nick and hostmask; also updated tld
authored Dec 28, 2011
332 if len(bad_masks) > 0:
9ea07c2 @myano changed some logic flow in bot.py to prevent the bot from crashing if a
authored Aug 16, 2012
333 host = origin.host
334 host = host.lower()
e8cd873 @myano added ability to have jenni block on nick and hostmask; also updated tld
authored Dec 28, 2011
335 for hostmask in bad_masks:
9ea07c2 @myano changed some logic flow in bot.py to prevent the bot from crashing if a
authored Aug 16, 2012
336 hostmask = hostmask.replace("\n", "").strip()
c43dc58 @myano added code top revent jenni from ignoring the server
authored Dec 28, 2011
337 if len(hostmask) < 1: continue
9ea07c2 @myano changed some logic flow in bot.py to prevent the bot from crashing if a
authored Aug 16, 2012
338 try:
339 re_temp = re.compile(hostmask)
340 if re_temp.findall(host):
341 return
342 except:
343 if hostmask in host:
344 return
345 # check for blocked nicks
e8cd873 @myano added ability to have jenni block on nick and hostmask; also updated tld
authored Dec 28, 2011
346 if len(bad_nicks) > 0:
347 for nick in bad_nicks:
9ea07c2 @myano changed some logic flow in bot.py to prevent the bot from crashing if a
authored Aug 16, 2012
348 nick = nick.replace("\n", "").strip()
1233507 @myano updated bug where last entry wasn't being used
authored Dec 28, 2011
349 if len(nick) < 1: continue
9ea07c2 @myano changed some logic flow in bot.py to prevent the bot from crashing if a
authored Aug 16, 2012
350 try:
351 re_temp = re.compile(nick)
352 if re_temp.findall(input.nick):
353 return
354 except:
355 if nick in input.nick:
356 return
5f6fd69 @myano added global rate limit module
authored Jun 19, 2012
357 # stats
6986adf @myano fixed spacing on core files
authored Jun 29, 2011
358 if func.thread:
359 targs = (func, origin, jenni, input)
360 t = threading.Thread(target=self.call, args=targs)
361 t.start()
362 else: self.call(func, origin, jenni, input)
363
364 for source in [origin.sender, origin.nick]:
365 try: self.stats[(func.name, source)] += 1
366 except KeyError:
367 self.stats[(func.name, source)] = 1
368
369 if __name__ == '__main__':
370 print __doc__
0ec9f1d @kaneda [abjonnes-sorted_modules] Sorting modules
kaneda authored Apr 5, 2015
371
Something went wrong with that request. Please try again.