]>
jfr.im git - erebus.git/blob - modlib.py
1 # Erebus IRC bot - Author: John Runyon
2 # vim: fileencoding=utf-8
3 # module helper functions, see modules/modtest.py for usage
4 # This file is released into the public domain; see http://unlicense.org/
8 from functools
import wraps
10 if sys
.version_info
.major
< 3:
11 stringbase
= basestring
15 """Used to return an error to the bot core."""
17 def __init__(self
, desc
):
19 def __nonzero__(self
):
20 return False #object will test to False
21 __bool__
= __nonzero__
#py3 compat
23 return '<modlib.error %r>' % self
.errormsg
25 return str(self
.errormsg
)
28 # default (global) access levels
34 AUTHED
= 0 # Users which have are known to be authed
35 ANYONE
= -1 # non-authed users have glevel set to -1
36 IGNORED
= -2 # If the user is IGNORED, no hooks or chanhooks will fire for their messages. numhooks can still be fired.
48 # (channel) access levels
54 PUBLIC
= 0 # Anyone (use glevel to control whether auth is needed)
55 BANNED
= -1 # The default reqclevel is PUBLIC, so any commands which needchan will be ignored from BANNED users unless the command reqclevel=-1
57 clevs
= [None, 'Friend', 'Voice', 'Op', 'Master', 'Owner', 'Banned']
60 WRONGARGS
= "Wrong number of arguments."
62 def __init__(self
, name
):
63 self
.hooks
= {} # {command:handler}
64 self
.chanhooks
= {} # {channel:handler}
65 self
.exceptionhooks
= [] # [(exception,handler)]
66 self
.numhooks
= {} # {numeric:handler}
67 self
.sockhooks
= [] # [(af,ty,address,handler_class)]
68 self
.sockets
= [] # [(sock,obj)]
72 self
.name
= (name
.split("."))[-1]
74 def modstart(self
, parent
):
75 #modstart can return a few things...
76 # None: unspecified success
77 # False: unspecified error
78 # modlib.error (or anything else False-y): specified error
79 # True: unspecified success
80 # non-empty string (or anything else True-y): specified success
81 #"specified" values will be printed. unspecified values will result in "OK" or "failed"
83 for cmd
, func
in self
.hooks
.items():
84 parent
.hook(cmd
, func
)
85 parent
.hook("%s.%s" % (self
.name
, cmd
), func
)
86 for chan
, func
in self
.chanhooks
.items():
87 parent
.hookchan(chan
, func
)
88 for exc
, func
in self
.exceptionhooks
:
89 parent
.hookexception(exc
, func
)
90 for num
, func
in self
.numhooks
.items():
91 parent
.hooknum(num
, func
)
92 for hookdata
in self
.sockhooks
:
93 self
._create
_socket
(*hookdata
)
95 for func
, args
, kwargs
in self
.helps
:
97 self
.mod('help').reghelp(func
, *args
, **kwargs
)
101 def modstop(self
, parent
):
102 for cmd
, func
in self
.hooks
.items():
103 parent
.unhook(cmd
, func
)
104 parent
.unhook("%s.%s" % (self
.name
, cmd
), func
)
105 for chan
, func
in self
.chanhooks
.items():
106 parent
.unhookchan(chan
, func
)
107 for exc
, func
in self
.exceptionhooks
:
108 parent
.unhookexception(exc
, func
)
109 for num
, func
in self
.numhooks
.items():
110 parent
.unhooknum(num
, func
)
111 for sock
, obj
in self
.sockets
:
112 self
._destroy
_socket
(sock
, obj
)
114 for func
, args
, kwargs
in self
.helps
:
116 self
.mod('help').dereghelp(func
, *args
, **kwargs
)
121 def hook(self
, cmd
=None, needchan
=True, glevel
=ANYONE
, clevel
=PUBLIC
, wantchan
=None):
122 if wantchan
is None: wantchan
= needchan
123 _cmd
= cmd
#save this since it gets wiped out...
125 cmd
= _cmd
#...and restore it
127 cmd
= func
.__name
__ # default to function name
128 if isinstance(cmd
, stringbase
):
131 if clevel
> self
.PUBLIC
and not needchan
:
132 raise Exception('clevel must be left at default if needchan is False')
134 func
.needchan
= needchan
135 func
.wantchan
= wantchan
136 func
.reqglevel
= glevel
137 func
.reqclevel
= clevel
139 func
.module
= func
.__module
__.split('.')[1]
143 if self
.parent
is not None:
144 self
.parent
.hook(c
, func
)
145 self
.parent
.hook("%s.%s" % (self
.name
, c
), func
)
149 def hookchan(self
, chan
, glevel
=ANYONE
, clevel
=PUBLIC
):
151 self
.chanhooks
[chan
] = func
152 if self
.parent
is not None:
153 self
.parent
.hookchan(chan
, func
)
157 def hookexception(self
, exc
):
159 self
.exceptionhooks
.append((exc
, func
))
160 if self
.parent
is not None:
161 self
.parent
.hookexception(exc
, func
)
165 def hooknum(self
, num
):
167 self
.numhooks
[str(num
)] = func
168 if self
.parent
is not None:
169 self
.parent
.hooknum(str(num
), func
)
173 def bind(self
, bindto
, data
=None):
174 """Used as a decorator on a class which implements getdata and parse methods.
175 See modules/sockets.py for an example.
181 raise Exception('bindto must have a value')
183 return self
._hooksocket
(socket
.AF_UNIX
, socket
.SOCK_STREAM
, bindto
)
184 if len(bindto
) > 5 and bindto
[0:5] == 'unix:':
185 return self
._hooksocket
(socket
.AF_UNIX
, socket
.SOCK_STREAM
, bindto
[5:])
187 ty
= socket
.SOCK_STREAM
189 if len(bindto
) > 4 and bindto
[0:4] == 'udp:':
190 ty
= socket
.SOCK_DGRAM
192 if len(bindto
) > 4 and bindto
[0:4] == 'tcp:':
195 pieces
= bindto
.rsplit(':', 1)
199 return self
._hooksocket
(af
, ty
, (host
, port
), data
)
201 def bind_tcp(self
, host
, port
, data
=None):
202 return self
._hooksocket
(socket
.AF_INET
, socket
.SOCK_STREAM
, (host
, port
), data
)
203 def bind_udp(self
, host
, port
, data
=None):
204 return self
._hooksocket
(socket
.AF_INET
, socket
.SOCK_DGRAM
, (host
, port
), data
)
205 def bind_unix(self
, path
, data
=None):
206 return self
._hooksocket
(socket
.AF_UNIX
, socket
.SOCK_STREAM
, path
, data
)
207 def _hooksocket(self
, af
, ty
, address
, data
):
209 if not (hasattr(cls
, 'getdata') and callable(cls
.getdata
)):
210 # Check early that the object implements getdata.
211 # If getdata ever returns a non-empty list, then a parse method must also exist, but we don't check that.
212 raise Exception('Attempted to hook a socket without a class to process data')
213 self
.sockhooks
.append((af
, ty
, address
, cls
, data
))
214 if self
.parent
is not None:
215 self
._create
_socket
(af
, ty
, address
, cls
, data
)
218 def _create_socket(self
, af
, ty
, address
, cls
, data
):
219 ty
= ty | socket
.SOCK_NONBLOCK
220 sock
= socket
.socket(af
, ty
)
221 sock
.setsockopt(socket
.SOL_SOCKET
, socket
.SO_REUSEADDR
, 1)
223 obj
= _ListenSocket(self
, sock
, cls
, data
)
224 self
.sockets
.append((sock
,obj
))
226 self
.parent
.newfd(obj
, sock
.fileno())
227 self
.parent
.log(repr(obj
), '?', 'Socket ready to accept new connections (%r, %r, %r, %r)' % (af
, ty
, address
, cls
))
228 def _destroy_socket(self
, sock
, obj
):
231 def mod(self
, modname
):
232 if self
.parent
is not None:
233 return self
.parent
.module(modname
)
235 return error('unknown parent')
237 def argsEQ(self
, num
):
240 def checkargs(bot
, user
, chan
, realtarget
, *args
):
242 return func(bot
, user
, chan
, realtarget
, *args
)
244 bot
.msg(user
, self
.WRONGARGS
)
248 def argsGE(self
, num
):
251 def checkargs(bot
, user
, chan
, realtarget
, *args
):
253 return func(bot
, user
, chan
, realtarget
, *args
)
255 bot
.msg(user
, self
.WRONGARGS
)
259 def help(self
, *args
, **kwargs
):
260 """help(syntax, shorthelp, longhelp?, more lines longhelp?, cmd=...?)
262 help("<user> <pass>", "login")
263 ^ Help will only be one line. Command name determined based on function name.
264 help("<user> <level>", "add a user", cmd=("adduser", "useradd"))
265 ^ Help will be listed under ADDUSER; USERADD will say "alias for adduser"
266 help(None, "do stuff", "This command is really complicated.")
267 ^ Command takes no args. Short description (in overall HELP listing) is "do stuff".
268 Long description (HELP <command>) will say "<command> - do stuff", newline, "This command is really complicated."
271 if self
.parent
is not None:
273 self
.mod('help').reghelp(func
, *args
, **kwargs
)
276 self
.helps
.append((func
, args
, kwargs
))
280 class _ListenSocket(object):
281 def __init__(self
, lib
, sock
, cls
, data
):
288 def _make_closer(self
, obj
, client
):
290 self
.lib
.parent
.log(repr(self
), '?', 'Closing child socket #%d' % (client
.fileno()))
293 except AttributeError:
295 self
.lib
.parent
.delfd(client
.fileno())
296 client
.shutdown(socket
.SHUT_RDWR
)
298 self
.clients
.remove((client
,obj
))
302 client
, addr
= self
.sock
.accept()
303 obj
= self
.cls(client
, self
.data
)
304 obj
.close
= self
._make
_closer
(obj
, client
)
305 self
.lib
.parent
.log(repr(self
), '?', 'New connection #%d from %s' % (client
.fileno(), addr
))
306 self
.clients
.append((client
,obj
))
307 self
.lib
.parent
.newfd(obj
, client
.fileno())
311 self
.lib
.parent
.log(repr(self
), '?', 'Socket closing')
312 if self
.sock
.fileno() != -1:
313 self
.lib
.parent
.delfd(self
.sock
.fileno())
314 self
.sock
.shutdown(socket
.SHUT_RDWR
)
316 for client
, obj
in self
.clients
:
319 def __repr__(self
): return '<_ListenSocket #%d>' % (self
.sock
.fileno())