]> jfr.im git - erebus.git/blame - erebus.py
update comments
[erebus.git] / erebus.py
CommitLineData
b25d4368 1#!/usr/bin/python
4477123d 2# vim: fileencoding=utf-8
b25d4368 3
931c88a4 4# Erebus IRC bot - Author: John Runyon
5# main startup code
6
a28e2ae9 7from __future__ import print_function
8
9d44d267 9import os, sys, select, MySQLdb, MySQLdb.cursors, time, traceback, random, gc
db50981b 10import bot, config, ctlmod
b25d4368 11
a8553c45 12class Erebus(object): #singleton to pass around
134c1193 13 APIVERSION = 0
a76c4bd8 14 RELEASE = 0
15
49a455aa 16 bots = {}
17 fds = {}
e4a4c762 18 numhandlers = {}
49a455aa 19 msghandlers = {}
9557ee54 20 chanhandlers = {}
e8885384 21 exceptionhandlers = [] # list of (Exception_class, handler_function) tuples
b2a896c8 22 users = {}
23 chans = {}
49a455aa 24
25 class User(object):
49a455aa 26 def __init__(self, nick, auth=None):
27 self.nick = nick
25bf8fc5
JR
28 if auth is None:
29 self.auth = None
30 else:
31 self.auth = auth.lower()
676b2a85 32 self.checklevel()
a4eacae2 33
5477b368 34 self.chans = []
35
5f5d669f
JR
36 def bind_bot(self, bot):
37 return main._BoundUser(self, bot)
38
e80bf7de 39 def msg(self, *args, **kwargs):
e64ac4a0 40 main.randbot().msg(self, *args, **kwargs)
2bb267e0 41 def slowmsg(self, *args, **kwargs):
42 main.randbot().slowmsg(self, *args, **kwargs)
e64ac4a0 43 def fastmsg(self, *args, **kwargs):
44 main.randbot().fastmsg(self, *args, **kwargs)
e80bf7de 45
b2a896c8 46 def isauthed(self):
47 return self.auth is not None
48
49a455aa 49 def authed(self, auth):
de89db13 50 if auth == '0': self.auth = None
51 else: self.auth = auth.lower()
49a455aa 52 self.checklevel()
a4eacae2 53
676b2a85 54 def checklevel(self):
55 if self.auth is None:
839d2b35 56 self.glevel = -1
676b2a85 57 else:
2729abc8 58 c = main.query("SELECT level FROM users WHERE auth = %s", (self.auth,))
59 if c:
4fa1118b 60 row = c.fetchone()
61 if row is not None:
62 self.glevel = row['level']
63 else:
64 self.glevel = 0
676b2a85 65 else:
839d2b35 66 self.glevel = 0
67 return self.glevel
43b98e4e 68
25bf8fc5
JR
69 def setlevel(self, level, savetodb=True):
70 if savetodb:
71 if level != 0:
72 c = main.query("REPLACE INTO users (auth, level) VALUES (%s, %s)", (self.auth, level))
73 else:
74 c = main.query("DELETE FROM users WHERE auth = %s", (self.auth,))
75 if c == 0: # no rows affected
76 c = True # is fine
77 if c:
78 self.glevel = level
79 return True
80 else:
81 return False
82 else:
83 self.glevel = level
84 return True
85
5477b368 86 def join(self, chan):
84b7c247 87 if chan not in self.chans: self.chans.append(chan)
5477b368 88 def part(self, chan):
3d724d3a 89 try:
90 self.chans.remove(chan)
91 except: pass
d53d073b 92 return len(self.chans) == 0
c695f740 93 def quit(self):
d53d073b 94 pass
124f114c 95 def nickchange(self, newnick):
e80bf7de 96 self.nick = newnick
5477b368 97
49a455aa 98 def __str__(self): return self.nick
71ef8273 99 def __repr__(self): return "<User %r (%d)>" % (self.nick, self.glevel)
43b98e4e 100
5f5d669f
JR
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)
116
49a455aa 117 class Channel(object):
586997a7 118 def __init__(self, name, bot):
49a455aa 119 self.name = name
5477b368 120 self.bot = bot
586997a7 121 self.levels = {}
5477b368 122
123 self.users = []
124 self.voices = []
125 self.ops = []
a4eacae2 126
2729abc8 127 c = main.query("SELECT user, level FROM chusers WHERE chan = %s", (self.name,))
128 if c:
586997a7 129 row = c.fetchone()
4fa1118b 130 while row is not None:
131 self.levels[row['user']] = row['level']
132 row = c.fetchone()
586997a7 133
134
fd52fb16 135 def msg(self, *args, **kwargs):
e64ac4a0 136 self.bot.msg(self, *args, **kwargs)
2bb267e0 137 def slowmsg(self, *args, **kwargs):
138 self.bot.slowmsg(self, *args, **kwargs)
e64ac4a0 139 def fastmsg(self, *args, **kwargs):
140 self.bot.fastmsg(self, *args, **kwargs)
fd52fb16 141
586997a7 142 def levelof(self, auth):
a9ce8d6a 143 if auth is None:
144 return 0
586997a7 145 auth = auth.lower()
146 if auth in self.levels:
147 return self.levels[auth]
148 else:
149 return 0
150
151 def setlevel(self, auth, level, savetodb=True):
152 auth = auth.lower()
153 if savetodb:
2729abc8 154 c = main.query("REPLACE INTO chusers (chan, user, level) VALUES (%s, %s, %s)", (self.name, auth, level))
155 if c:
4fa1118b 156 self.levels[auth] = level
157 return True
158 else:
159 return False
25bf8fc5
JR
160 else:
161 self.levels[auth] = level
162 return True
586997a7 163
49a455aa 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)
a4eacae2 172
49a455aa 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)
181
182 def __str__(self): return self.name
183 def __repr__(self): return "<Channel %r>" % (self.name)
184
c0eee1b4 185 def __init__(self, cfg):
2a44c0cd 186 self.mustquit = None
fc16e064 187 self.starttime = time.time()
c0eee1b4 188 self.cfg = cfg
189 self.trigger = cfg.trigger
fd96a423 190 if os.name == "posix":
191 self.potype = "poll"
192 self.po = select.poll()
193 else: # f.e. os.name == "nt" (Windows)
194 self.potype = "select"
195 self.fdlist = []
49a455aa 196
2729abc8 197 def query(self, *args, **kwargs):
c728e51c 198 if 'noretry' in kwargs:
199 noretry = kwargs['noretry']
200 del kwargs['noretry']
2729abc8 201 else:
c728e51c 202 noretry = False
2729abc8 203
204 self.log("[SQL]", "?", "query(%s, %s)" % (', '.join([repr(i) for i in args]), ', '.join([str(key)+"="+repr(kwargs[key]) for key in kwargs])))
205 try:
206 curs = self.db.cursor()
207 res = curs.execute(*args, **kwargs)
208 if res:
209 return curs
210 else:
211 return res
2c58b913
JR
212 except MySQLdb.DataError as e:
213 self.log("[SQL]", ".", "MySQL DataError: %r" % (e))
214 return False
2729abc8 215 except MySQLdb.MySQLError as e:
216 self.log("[SQL]", "!", "MySQL error! %r" % (e))
c728e51c 217 if not noretry:
2729abc8 218 dbsetup()
c728e51c 219 return self.query(*args, noretry=True, **kwargs)
2729abc8 220 else:
221 raise e
222
c728e51c 223 def querycb(self, cb, *args, **kwargs):
224 def run_query():
225 cb(self.query(*args, **kwargs))
226 threading.Thread(target=run_query).start()
227
0af282c6 228 def newbot(self, nick, user, bind, authname, authpass, server, port, realname):
49a455aa 229 if bind is None: bind = ''
0af282c6 230 obj = bot.Bot(self, nick, user, bind, authname, authpass, server, port, realname)
49a455aa 231 self.bots[nick.lower()] = obj
a4eacae2 232
49a455aa 233 def newfd(self, obj, fileno):
49a455aa 234 self.fds[fileno] = obj
fd96a423 235 if self.potype == "poll":
236 self.po.register(fileno, select.POLLIN)
237 elif self.potype == "select":
238 self.fdlist.append(fileno)
9d44d267
JR
239 def delfd(self, fileno):
240 del self.fds[fileno]
241 if self.potype == "poll":
242 self.po.unregister(fileno)
243 elif self.potype == "select":
244 self.fdlist.remove(fileno)
a4eacae2 245
43b98e4e 246 def bot(self, name): #get Bot() by name (nick)
49a455aa 247 return self.bots[name.lower()]
43b98e4e 248 def fd(self, fileno): #get Bot() by fd/fileno
49a455aa 249 return self.fds[fileno]
8af0407d 250 def randbot(self): #get Bot() randomly
71ef8273 251 return self.bots[random.choice(list(self.bots.keys()))]
49a455aa 252
f6386fa7 253 def user(self, _nick, send_who=False, create=True):
c695f740 254 nick = _nick.lower()
f6386fa7
JR
255
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))
258
b2a896c8 259 if nick in self.users:
260 return self.users[nick]
3d724d3a 261 elif create:
c695f740 262 user = self.User(_nick)
b2a896c8 263 self.users[nick] = user
264 return user
3d724d3a 265 else:
266 return None
5477b368 267 def channel(self, name): #get Channel() by name
268 if name.lower() in self.chans:
269 return self.chans[name.lower()]
270 else:
271 return None
272
586997a7 273 def newchannel(self, bot, name):
274 chan = self.Channel(name.lower(), bot)
5477b368 275 self.chans[name.lower()] = chan
276 return chan
49a455aa 277
278 def poll(self):
2a44c0cd 279 timeout_seconds = 30
fd96a423 280 if self.potype == "poll":
2a44c0cd
JR
281 pollres = self.po.poll(timeout_seconds * 1000)
282 return [fd for (fd, ev) in pollres]
fd96a423 283 elif self.potype == "select":
2a44c0cd 284 return select.select(self.fdlist, [], [], timeout_seconds)[0]
49a455aa 285
286 def connectall(self):
a28e2ae9 287 for bot in self.bots.values():
49a455aa 288 if bot.conn.state == 0:
289 bot.connect()
290
fadbf980 291 def module(self, name):
292 return ctlmod.modules[name]
293
a8553c45 294 def log(self, source, level, message):
a28e2ae9 295 print("%09.3f %s [%s] %s" % (time.time() % 100000, source, level, message))
a8553c45 296
f560eb44 297 def getuserbyauth(self, auth):
a28e2ae9 298 return [u for u in self.users.values() if u.auth == auth.lower()]
f560eb44 299
bffe0139 300 def getdb(self):
301 """Get a DB object. The object must be returned to the pool after us, using returndb()."""
302 return self.dbs.pop()
303
304 def returndb(self, db):
305 self.dbs.append(db)
306
49a455aa 307 #bind functions
db50981b 308 def hook(self, word, handler):
e4a4c762 309 try:
310 self.msghandlers[word].append(handler)
311 except:
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)
db50981b 316 def hashook(self, word):
e4a4c762 317 return word in self.msghandlers and len(self.msghandlers[word]) != 0
db50981b 318 def gethook(self, word):
319 return self.msghandlers[word]
b25d4368 320
e4a4c762 321 def hooknum(self, word, handler):
322 try:
323 self.numhandlers[word].append(handler)
324 except:
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]
333
2a1a69a6 334 def hookchan(self, chan, handler):
335 try:
9557ee54 336 self.chanhandlers[chan].append(handler)
2a1a69a6 337 except:
9557ee54 338 self.chanhandlers[chan] = [handler]
2a1a69a6 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]
586997a7 346
e8885384
JR
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))
355
586997a7 356
de89db13 357def dbsetup():
4fa1118b 358 main.db = None
bffe0139 359 main.dbs = []
360 for i in range(cfg.get('erebus', 'num_db_connections', 2)-1):
361 main.dbs.append(MySQLdb.connect(host=cfg.dbhost, user=cfg.dbuser, passwd=cfg.dbpass, db=cfg.dbname, cursorclass=MySQLdb.cursors.DictCursor))
2729abc8 362 main.db = MySQLdb.connect(host=cfg.dbhost, user=cfg.dbuser, passwd=cfg.dbpass, db=cfg.dbname, cursorclass=MySQLdb.cursors.DictCursor)
586997a7 363
b25d4368 364def setup():
db50981b 365 global cfg, main
366
48479459 367 cfg = config.Config('bot.config')
e64ac4a0 368
dcc5bde3 369 if cfg.getboolean('debug', 'gc'):
2ffef3ff 370 gc.set_debug(gc.DEBUG_LEAK)
371
e64ac4a0 372 pidfile = open(cfg.pidfile, 'w')
373 pidfile.write(str(os.getpid()))
374 pidfile.close()
375
c0eee1b4 376 main = Erebus(cfg)
bffe0139 377 dbsetup()
db50981b 378
379 autoloads = [mod for mod, yes in cfg.items('autoloads') if int(yes) == 1]
380 for mod in autoloads:
b9c6eb1d 381 ctlmod.load(main, mod)
db50981b 382
2729abc8 383 c = main.query("SELECT nick, user, bind, authname, authpass FROM bots WHERE active = 1")
384 if c:
4fa1118b 385 rows = c.fetchall()
386 c.close()
387 for row in rows:
0af282c6 388 main.newbot(row['nick'], row['user'], row['bind'], row['authname'], row['authpass'], cfg.host, cfg.port, cfg.realname)
a12f7519 389 main.connectall()
b25d4368 390
391def loop():
49a455aa 392 poready = main.poll()
fd96a423 393 for fileno in poready:
9d44d267
JR
394 try:
395 data = main.fd(fileno).getdata()
396 except:
397 main.log('*', '!', 'Super-mega-emergency: getdata raised exception for socket %d' % (fileno))
398 traceback.print_exc()
399 data = None
400 if data is None:
401 main.fd(fileno).close()
402 else:
403 for line in data:
4aa86bbb
JR
404 if cfg.getboolean('debug', 'io'):
405 main.log(str(main.fd(fileno)), 'I', line)
9d44d267
JR
406 try:
407 main.fd(fileno).parse(line)
408 except:
409 main.log('*', '!', 'Super-mega-emergency: parse raised exception for socket %d data %r' % (fileno, line))
410 traceback.print_exc()
2a44c0cd 411 if main.mustquit is not None:
dc0f891b 412 main.log('*', '!', 'Core exiting due to: %s' % (main.mustquit))
2a44c0cd 413 raise main.mustquit
b25d4368 414
415if __name__ == '__main__':
963f2522 416 try: os.rename('logfile', 'oldlogs/%s' % (time.time()))
24b74bb3 417 except: pass
3d724d3a 418 sys.stdout = open('logfile', 'w', 1)
24b74bb3 419 sys.stderr = sys.stdout
b25d4368 420 setup()
49a455aa 421 while True: loop()