]>
jfr.im git - erebus.git/blob - erebus.py
1a06ed3b5f6714b8136291465518e3f4613fa373
2 # vim: fileencoding=utf-8
4 # Erebus IRC bot - Author: John Runyon
7 from __future__
import print_function
9 import os
, sys
, select
, time
, traceback
, random
, gc
10 import bot
, config
, ctlmod
12 class Erebus(object): #singleton to pass around
21 exceptionhandlers
= [] # list of (Exception_class, handler_function) tuples
26 def __init__(self
, nick
, auth
=None):
31 self
.auth
= auth
.lower()
36 def bind_bot(self
, bot
):
37 return main
._BoundUser
(self
, bot
)
39 def msg(self
, *args
, **kwargs
):
40 main
.randbot().msg(self
, *args
, **kwargs
)
41 def slowmsg(self
, *args
, **kwargs
):
42 main
.randbot().slowmsg(self
, *args
, **kwargs
)
43 def fastmsg(self
, *args
, **kwargs
):
44 main
.randbot().fastmsg(self
, *args
, **kwargs
)
47 return self
.auth
is not None
49 def authed(self
, auth
):
50 if auth
== '0': self
.auth
= None
51 else: self
.auth
= auth
.lower()
58 c
= main
.query("SELECT level FROM users WHERE auth = %s", (self
.auth
,))
62 self
.glevel
= row
['level']
69 def setlevel(self
, level
, savetodb
=True):
72 c
= main
.query("REPLACE INTO users (auth, level) VALUES (%s, %s)", (self
.auth
, level
))
74 c
= main
.query("DELETE FROM users WHERE auth = %s", (self
.auth
,))
75 if c
== 0: # no rows affected
87 if chan
not in self
.chans
: self
.chans
.append(chan
)
90 self
.chans
.remove(chan
)
92 return len(self
.chans
) == 0
95 def nickchange(self
, newnick
):
98 def __str__(self
): return self
.nick
99 def __repr__(self
): return "<User %r (%d)>" % (self
.nick
, self
.glevel
)
101 class _BoundUser(object):
102 def __init__(self
, user
, bot
):
103 self
.__dict
__['_bound_user'] = user
104 self
.__dict
__['_bound_bot'] = bot
105 def __getattr__(self
, name
):
106 return getattr(self
._bound
_user
, name
)
107 def __setattr__(self
, name
, value
):
108 setattr(self
._bound
_user
, name
, value
)
109 def msg(self
, *args
, **kwargs
):
110 self
._bound
_bot
.msg(self
._bound
_user
, *args
, **kwargs
)
111 def slowmsg(self
, *args
, **kwargs
):
112 self
._bound
_bot
.slowmsg(self
._bound
_user
, *args
, **kwargs
)
113 def fastmsg(self
, *args
, **kwargs
):
114 self
._bound
_bot
.fastmsg(self
._bound
_user
, *args
, **kwargs
)
115 def __repr__(self
): return "<_BoundUser %r %r>" % (self
._bound
_user
, self
._bound
_bot
)
117 class Channel(object):
118 def __init__(self
, name
, bot
):
127 c
= main
.query("SELECT user, level FROM chusers WHERE chan = %s", (self
.name
,))
130 while row
is not None:
131 self
.levels
[row
['user']] = row
['level']
135 def msg(self
, *args
, **kwargs
):
136 self
.bot
.msg(self
, *args
, **kwargs
)
137 def slowmsg(self
, *args
, **kwargs
):
138 self
.bot
.slowmsg(self
, *args
, **kwargs
)
139 def fastmsg(self
, *args
, **kwargs
):
140 self
.bot
.fastmsg(self
, *args
, **kwargs
)
142 def levelof(self
, auth
):
146 if auth
in self
.levels
:
147 return self
.levels
[auth
]
151 def setlevel(self
, auth
, level
, savetodb
=True):
154 c
= main
.query("REPLACE INTO chusers (chan, user, level) VALUES (%s, %s, %s)", (self
.name
, auth
, level
))
156 self
.levels
[auth
] = level
161 self
.levels
[auth
] = level
164 def userjoin(self
, user
, level
=None):
165 if user
not in self
.users
: self
.users
.append(user
)
166 if level
== 'op' and user
not in self
.ops
: self
.ops
.append(user
)
167 if level
== 'voice' and user
not in self
.voices
: self
.voices
.append(user
)
168 def userpart(self
, user
):
169 if user
in self
.ops
: self
.ops
.remove(user
)
170 if user
in self
.voices
: self
.voices
.remove(user
)
171 if user
in self
.users
: self
.users
.remove(user
)
173 def userop(self
, user
):
174 if user
in self
.users
and user
not in self
.ops
: self
.ops
.append(user
)
175 def uservoice(self
, user
):
176 if user
in self
.users
and user
not in self
.voices
: self
.voices
.append(user
)
177 def userdeop(self
, user
):
178 if user
in self
.ops
: self
.ops
.remove(user
)
179 def userdevoice(self
, user
):
180 if user
in self
.voices
: self
.voices
.remove(user
)
182 def __str__(self
): return self
.name
183 def __repr__(self
): return "<Channel %r>" % (self
.name
)
185 def __init__(self
, cfg
):
187 self
.starttime
= time
.time()
189 self
.trigger
= cfg
.trigger
190 if os
.name
== "posix":
192 self
.po
= select
.poll()
193 else: # f.e. os.name == "nt" (Windows)
194 self
.potype
= "select"
197 def query(self
, *args
, **kwargs
):
198 if 'noretry' in kwargs
:
199 noretry
= kwargs
['noretry']
200 del kwargs
['noretry']
204 self
.log("[SQL]", "?", "query(%s, %s)" % (', '.join([repr(i
) for i
in args
]), ', '.join([str(key
)+"="+repr(kwargs
[key
]) for key
in kwargs
])))
206 curs
= self
.db
.cursor()
207 res
= curs
.execute(*args
, **kwargs
)
212 except db_api
.DataError
as e
:
213 self
.log("[SQL]", ".", "DB DataError: %r" % (e
))
215 except db_api
.Error
as e
:
216 self
.log("[SQL]", "!", "DB error! %r" % (e
))
219 return self
.query(*args
, noretry
=True, **kwargs
)
223 def querycb(self
, cb
, *args
, **kwargs
):
225 cb(self
.query(*args
, **kwargs
))
226 threading
.Thread(target
=run_query
).start()
228 def newbot(self
, nick
, user
, bind
, authname
, authpass
, server
, port
, realname
):
229 if bind
is None: bind
= ''
230 obj
= bot
.Bot(self
, nick
, user
, bind
, authname
, authpass
, server
, port
, realname
)
231 self
.bots
[nick
.lower()] = obj
233 def newfd(self
, obj
, fileno
):
234 self
.fds
[fileno
] = obj
235 if self
.potype
== "poll":
236 self
.po
.register(fileno
, select
.POLLIN
)
237 elif self
.potype
== "select":
238 self
.fdlist
.append(fileno
)
239 def delfd(self
, fileno
):
241 if self
.potype
== "poll":
242 self
.po
.unregister(fileno
)
243 elif self
.potype
== "select":
244 self
.fdlist
.remove(fileno
)
246 def bot(self
, name
): #get Bot() by name (nick)
247 return self
.bots
[name
.lower()]
248 def fd(self
, fileno
): #get Bot() by fd/fileno
249 return self
.fds
[fileno
]
250 def randbot(self
): #get Bot() randomly
251 return self
.bots
[random
.choice(list(self
.bots
.keys()))]
253 def user(self
, _nick
, send_who
=False, create
=True):
256 if send_who
and (nick
not in self
.users
or not self
.users
[nick
].isauthed()):
257 self
.randbot().conn
.send("WHO %s n%%ant,1" % (nick
))
259 if nick
in self
.users
:
260 return self
.users
[nick
]
262 user
= self
.User(_nick
)
263 self
.users
[nick
] = user
267 def channel(self
, name
): #get Channel() by name
268 if name
.lower() in self
.chans
:
269 return self
.chans
[name
.lower()]
273 def newchannel(self
, bot
, name
):
274 chan
= self
.Channel(name
.lower(), bot
)
275 self
.chans
[name
.lower()] = chan
280 if self
.potype
== "poll":
281 pollres
= self
.po
.poll(timeout_seconds
* 1000)
282 return [fd
for (fd
, ev
) in pollres
]
283 elif self
.potype
== "select":
284 return select
.select(self
.fdlist
, [], [], timeout_seconds
)[0]
286 def connectall(self
):
287 for bot
in self
.bots
.values():
288 if bot
.conn
.state
== 0:
291 def module(self
, name
):
292 return ctlmod
.modules
[name
]
294 def log(self
, source
, level
, message
):
295 print("%09.3f %s [%s] %s" % (time
.time() % 100000, source
, level
, message
))
297 def getuserbyauth(self
, auth
):
298 return [u
for u
in self
.users
.values() if u
.auth
== auth
.lower()]
301 """Get a DB object. The object must be returned to the pool after us, using returndb()."""
302 return self
.dbs
.pop()
304 def returndb(self
, db
):
308 def hook(self
, word
, handler
):
310 self
.msghandlers
[word
].append(handler
)
312 self
.msghandlers
[word
] = [handler
]
313 def unhook(self
, word
, handler
):
314 if word
in self
.msghandlers
and handler
in self
.msghandlers
[word
]:
315 self
.msghandlers
[word
].remove(handler
)
316 def hashook(self
, word
):
317 return word
in self
.msghandlers
and len(self
.msghandlers
[word
]) != 0
318 def gethook(self
, word
):
319 return self
.msghandlers
[word
]
321 def hooknum(self
, word
, handler
):
323 self
.numhandlers
[word
].append(handler
)
325 self
.numhandlers
[word
] = [handler
]
326 def unhooknum(self
, word
, handler
):
327 if word
in self
.numhandlers
and handler
in self
.numhandlers
[word
]:
328 self
.numhandlers
[word
].remove(handler
)
329 def hasnumhook(self
, word
):
330 return word
in self
.numhandlers
and len(self
.numhandlers
[word
]) != 0
331 def getnumhook(self
, word
):
332 return self
.numhandlers
[word
]
334 def hookchan(self
, chan
, handler
):
336 self
.chanhandlers
[chan
].append(handler
)
338 self
.chanhandlers
[chan
] = [handler
]
339 def unhookchan(self
, chan
, handler
):
340 if chan
in self
.chanhandlers
and handler
in self
.chanhandlers
[chan
]:
341 self
.chanhandlers
[chan
].remove(handler
)
342 def haschanhook(self
, chan
):
343 return chan
in self
.chanhandlers
and len(self
.chanhandlers
[chan
]) != 0
344 def getchanhook(self
, chan
):
345 return self
.chanhandlers
[chan
]
347 def hookexception(self
, exc
, handler
):
348 self
.exceptionhandlers
.append((exc
, handler
))
349 def unhookexception(self
, exc
, handler
):
350 self
.exceptionhandlers
.remove((exc
, handler
))
351 def hasexceptionhook(self
, exc
):
352 return any((True for x
,h
in self
.exceptionhandlers
if isinstance(exc
, x
)))
353 def getexceptionhook(self
, exc
):
354 return (h
for x
,h
in self
.exceptionhandlers
if isinstance(exc
, x
))
360 dbtype
= cfg
.get('erebus', 'dbtype', 'mysql')
361 if dbtype
== 'mysql':
364 main
.log('*', '!', 'Unknown dbtype in config: %s' % (dbtype
))
366 def _dbsetup_mysql():
368 import MySQLdb
as db_api
, MySQLdb
.cursors
369 for i
in range(cfg
.get('erebus', 'num_db_connections', 2)-1):
370 main
.dbs
.append(db_api
.connect(host
=cfg
.dbhost
, user
=cfg
.dbuser
, passwd
=cfg
.dbpass
, db
=cfg
.dbname
, cursorclass
=MySQLdb
.cursors
.DictCursor
))
371 main
.db
= db_api
.connect(host
=cfg
.dbhost
, user
=cfg
.dbuser
, passwd
=cfg
.dbpass
, db
=cfg
.dbname
, cursorclass
=MySQLdb
.cursors
.DictCursor
)
377 cfg
= config
.Config('bot.config')
379 if cfg
.getboolean('debug', 'gc'):
380 gc
.set_debug(gc
.DEBUG_LEAK
)
382 pidfile
= open(cfg
.pidfile
, 'w')
383 pidfile
.write(str(os
.getpid()))
389 autoloads
= [mod
for mod
, yes
in cfg
.items('autoloads') if int(yes
) == 1]
390 for mod
in autoloads
:
391 ctlmod
.load(main
, mod
)
393 c
= main
.query("SELECT nick, user, bind, authname, authpass FROM bots WHERE active = 1")
398 main
.newbot(row
['nick'], row
['user'], row
['bind'], row
['authname'], row
['authpass'], cfg
.host
, cfg
.port
, cfg
.realname
)
402 poready
= main
.poll()
403 for fileno
in poready
:
405 data
= main
.fd(fileno
).getdata()
407 main
.log('*', '!', 'Super-mega-emergency: getdata raised exception for socket %d' % (fileno
))
408 traceback
.print_exc()
411 main
.fd(fileno
).close()
414 if cfg
.getboolean('debug', 'io'):
415 main
.log(str(main
.fd(fileno
)), 'I', line
)
417 main
.fd(fileno
).parse(line
)
419 main
.log('*', '!', 'Super-mega-emergency: parse raised exception for socket %d data %r' % (fileno
, line
))
420 traceback
.print_exc()
421 if main
.mustquit
is not None:
422 main
.log('*', '!', 'Core exiting due to: %s' % (main
.mustquit
))
425 if __name__
== '__main__':
426 try: os
.rename('logfile', 'oldlogs/%s' % (time
.time()))
428 sys
.stdout
= open('logfile', 'w', 1)
429 sys
.stderr
= sys
.stdout