]> jfr.im git - erebus.git/blame - erebus.py
admin_config - add !getconfig, remove some unused functions
[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
5b8f6176 9import os, sys, select, time, traceback, random, gc
56580e4e 10import bot, config, ctlmod, modlib
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
79519d5a
JR
127 self.deleting = False # if true, the bot will remove cached records of this channel when the bot sees that it has left the channel
128
2729abc8 129 c = main.query("SELECT user, level FROM chusers WHERE chan = %s", (self.name,))
130 if c:
586997a7 131 row = c.fetchone()
4fa1118b 132 while row is not None:
133 self.levels[row['user']] = row['level']
134 row = c.fetchone()
586997a7 135
136
fd52fb16 137 def msg(self, *args, **kwargs):
e64ac4a0 138 self.bot.msg(self, *args, **kwargs)
2bb267e0 139 def slowmsg(self, *args, **kwargs):
140 self.bot.slowmsg(self, *args, **kwargs)
e64ac4a0 141 def fastmsg(self, *args, **kwargs):
142 self.bot.fastmsg(self, *args, **kwargs)
fd52fb16 143
586997a7 144 def levelof(self, auth):
a9ce8d6a 145 if auth is None:
146 return 0
586997a7 147 auth = auth.lower()
148 if auth in self.levels:
149 return self.levels[auth]
150 else:
151 return 0
152
153 def setlevel(self, auth, level, savetodb=True):
154 auth = auth.lower()
155 if savetodb:
2729abc8 156 c = main.query("REPLACE INTO chusers (chan, user, level) VALUES (%s, %s, %s)", (self.name, auth, level))
157 if c:
4fa1118b 158 self.levels[auth] = level
159 return True
160 else:
161 return False
25bf8fc5
JR
162 else:
163 self.levels[auth] = level
164 return True
586997a7 165
49a455aa 166 def userjoin(self, user, level=None):
167 if user not in self.users: self.users.append(user)
168 if level == 'op' and user not in self.ops: self.ops.append(user)
169 if level == 'voice' and user not in self.voices: self.voices.append(user)
170 def userpart(self, user):
171 if user in self.ops: self.ops.remove(user)
172 if user in self.voices: self.voices.remove(user)
173 if user in self.users: self.users.remove(user)
a4eacae2 174
49a455aa 175 def userop(self, user):
176 if user in self.users and user not in self.ops: self.ops.append(user)
177 def uservoice(self, user):
178 if user in self.users and user not in self.voices: self.voices.append(user)
179 def userdeop(self, user):
180 if user in self.ops: self.ops.remove(user)
181 def userdevoice(self, user):
182 if user in self.voices: self.voices.remove(user)
183
184 def __str__(self): return self.name
185 def __repr__(self): return "<Channel %r>" % (self.name)
186
c0eee1b4 187 def __init__(self, cfg):
2a44c0cd 188 self.mustquit = None
fc16e064 189 self.starttime = time.time()
c0eee1b4 190 self.cfg = cfg
191 self.trigger = cfg.trigger
fd96a423 192 if os.name == "posix":
193 self.potype = "poll"
194 self.po = select.poll()
195 else: # f.e. os.name == "nt" (Windows)
196 self.potype = "select"
197 self.fdlist = []
49a455aa 198
6b4ba0b6
JR
199 def query(self, sql, parameters=[], noretry=False):
200 # Callers use %s-style (paramstyle='format') placeholders in queries.
201 # There's no provision for a literal '%s' present inside the query; stuff it in a parameter instead.
202 if db_api.paramstyle == 'format' or db_api.paramstyle == 'pyformat': # mysql, postgresql
203 # psycopg actually asks for a mapping with %(name)s style (pyformat) but it will accept %s style.
204 pass
205 elif db_api.paramstyle == 'qmark': # sqlite doesn't like %s style.
206 parameters = [str(p) for p in parameters]
207 sql = sql.replace('%s', '?') # hope that wasn't literal, oopsie
208
209 log_noretry = ''
210 if noretry:
211 log_noretry = ', noretry=True'
212 self.log("[SQL]", "?", "query(%r, %r%s)" % (sql, parameters, log_noretry))
2729abc8 213
2729abc8 214 try:
215 curs = self.db.cursor()
6b4ba0b6 216 res = curs.execute(sql, parameters)
2729abc8 217 if res:
218 return curs
219 else:
220 return res
5b8f6176
JR
221 except db_api.DataError as e:
222 self.log("[SQL]", ".", "DB DataError: %r" % (e))
2c58b913 223 return False
5b8f6176
JR
224 except db_api.Error as e:
225 self.log("[SQL]", "!", "DB error! %r" % (e))
c728e51c 226 if not noretry:
2729abc8 227 dbsetup()
6b4ba0b6 228 return self.query(sql, parameters, noretry=True)
2729abc8 229 else:
230 raise e
231
c728e51c 232 def querycb(self, cb, *args, **kwargs):
6b4ba0b6 233 # TODO this should either get thrown out with getdb()/returndb(), or else be adjusted to make use of it.
c728e51c 234 def run_query():
235 cb(self.query(*args, **kwargs))
236 threading.Thread(target=run_query).start()
237
0af282c6 238 def newbot(self, nick, user, bind, authname, authpass, server, port, realname):
49a455aa 239 if bind is None: bind = ''
0af282c6 240 obj = bot.Bot(self, nick, user, bind, authname, authpass, server, port, realname)
49a455aa 241 self.bots[nick.lower()] = obj
a4eacae2 242
49a455aa 243 def newfd(self, obj, fileno):
56580e4e
JR
244 if not isinstance(obj, modlib.Socketlike):
245 raise Exception('Attempted to hook a socket without a class to process data')
49a455aa 246 self.fds[fileno] = obj
fd96a423 247 if self.potype == "poll":
248 self.po.register(fileno, select.POLLIN)
249 elif self.potype == "select":
250 self.fdlist.append(fileno)
9d44d267
JR
251 def delfd(self, fileno):
252 del self.fds[fileno]
253 if self.potype == "poll":
254 self.po.unregister(fileno)
255 elif self.potype == "select":
256 self.fdlist.remove(fileno)
a4eacae2 257
43b98e4e 258 def bot(self, name): #get Bot() by name (nick)
49a455aa 259 return self.bots[name.lower()]
43b98e4e 260 def fd(self, fileno): #get Bot() by fd/fileno
49a455aa 261 return self.fds[fileno]
8af0407d 262 def randbot(self): #get Bot() randomly
71ef8273 263 return self.bots[random.choice(list(self.bots.keys()))]
49a455aa 264
f6386fa7 265 def user(self, _nick, send_who=False, create=True):
c695f740 266 nick = _nick.lower()
f6386fa7
JR
267
268 if send_who and (nick not in self.users or not self.users[nick].isauthed()):
269 self.randbot().conn.send("WHO %s n%%ant,1" % (nick))
270
b2a896c8 271 if nick in self.users:
272 return self.users[nick]
3d724d3a 273 elif create:
c695f740 274 user = self.User(_nick)
b2a896c8 275 self.users[nick] = user
276 return user
3d724d3a 277 else:
278 return None
5477b368 279 def channel(self, name): #get Channel() by name
280 if name.lower() in self.chans:
281 return self.chans[name.lower()]
282 else:
283 return None
284
586997a7 285 def newchannel(self, bot, name):
286 chan = self.Channel(name.lower(), bot)
5477b368 287 self.chans[name.lower()] = chan
288 return chan
49a455aa 289
290 def poll(self):
2a44c0cd 291 timeout_seconds = 30
fd96a423 292 if self.potype == "poll":
2a44c0cd
JR
293 pollres = self.po.poll(timeout_seconds * 1000)
294 return [fd for (fd, ev) in pollres]
fd96a423 295 elif self.potype == "select":
2a44c0cd 296 return select.select(self.fdlist, [], [], timeout_seconds)[0]
49a455aa 297
298 def connectall(self):
a28e2ae9 299 for bot in self.bots.values():
49a455aa 300 if bot.conn.state == 0:
301 bot.connect()
302
fadbf980 303 def module(self, name):
304 return ctlmod.modules[name]
305
a8553c45 306 def log(self, source, level, message):
a28e2ae9 307 print("%09.3f %s [%s] %s" % (time.time() % 100000, source, level, message))
a8553c45 308
f560eb44 309 def getuserbyauth(self, auth):
a28e2ae9 310 return [u for u in self.users.values() if u.auth == auth.lower()]
f560eb44 311
bffe0139 312 def getdb(self):
6b4ba0b6
JR
313 """Get a DB object. The object must be returned to the pool after us, using returndb(). This is intended for use from child threads.
314 It should probably be treated as deprecated though. Where possible new modules should avoid using threads.
315 In the future, timers will be provided (manipulating the timeout_seconds of the poll() method), and that should mostly be used in place of threading."""
bffe0139 316 return self.dbs.pop()
317
318 def returndb(self, db):
319 self.dbs.append(db)
320
49a455aa 321 #bind functions
db50981b 322 def hook(self, word, handler):
e4a4c762 323 try:
324 self.msghandlers[word].append(handler)
325 except:
326 self.msghandlers[word] = [handler]
327 def unhook(self, word, handler):
328 if word in self.msghandlers and handler in self.msghandlers[word]:
329 self.msghandlers[word].remove(handler)
db50981b 330 def hashook(self, word):
e4a4c762 331 return word in self.msghandlers and len(self.msghandlers[word]) != 0
db50981b 332 def gethook(self, word):
333 return self.msghandlers[word]
b25d4368 334
e4a4c762 335 def hooknum(self, word, handler):
336 try:
337 self.numhandlers[word].append(handler)
338 except:
339 self.numhandlers[word] = [handler]
340 def unhooknum(self, word, handler):
341 if word in self.numhandlers and handler in self.numhandlers[word]:
342 self.numhandlers[word].remove(handler)
343 def hasnumhook(self, word):
344 return word in self.numhandlers and len(self.numhandlers[word]) != 0
345 def getnumhook(self, word):
346 return self.numhandlers[word]
347
2a1a69a6 348 def hookchan(self, chan, handler):
349 try:
9557ee54 350 self.chanhandlers[chan].append(handler)
2a1a69a6 351 except:
9557ee54 352 self.chanhandlers[chan] = [handler]
2a1a69a6 353 def unhookchan(self, chan, handler):
354 if chan in self.chanhandlers and handler in self.chanhandlers[chan]:
355 self.chanhandlers[chan].remove(handler)
356 def haschanhook(self, chan):
357 return chan in self.chanhandlers and len(self.chanhandlers[chan]) != 0
358 def getchanhook(self, chan):
359 return self.chanhandlers[chan]
586997a7 360
e8885384
JR
361 def hookexception(self, exc, handler):
362 self.exceptionhandlers.append((exc, handler))
363 def unhookexception(self, exc, handler):
364 self.exceptionhandlers.remove((exc, handler))
365 def hasexceptionhook(self, exc):
366 return any((True for x,h in self.exceptionhandlers if isinstance(exc, x)))
367 def getexceptionhook(self, exc):
368 return (h for x,h in self.exceptionhandlers if isinstance(exc, x))
369
586997a7 370
de89db13 371def dbsetup():
4fa1118b 372 main.db = None
bffe0139 373 main.dbs = []
5b8f6176
JR
374 dbtype = cfg.get('erebus', 'dbtype', 'mysql')
375 if dbtype == 'mysql':
376 _dbsetup_mysql()
6b4ba0b6
JR
377 elif dbtype == 'sqlite':
378 _dbsetup_sqlite()
5b8f6176
JR
379 else:
380 main.log('*', '!', 'Unknown dbtype in config: %s' % (dbtype))
381
382def _dbsetup_mysql():
383 global db_api
384 import MySQLdb as db_api, MySQLdb.cursors
bffe0139 385 for i in range(cfg.get('erebus', 'num_db_connections', 2)-1):
5b8f6176
JR
386 main.dbs.append(db_api.connect(host=cfg.dbhost, user=cfg.dbuser, passwd=cfg.dbpass, db=cfg.dbname, cursorclass=MySQLdb.cursors.DictCursor))
387 main.db = db_api.connect(host=cfg.dbhost, user=cfg.dbuser, passwd=cfg.dbpass, db=cfg.dbname, cursorclass=MySQLdb.cursors.DictCursor)
388
6b4ba0b6
JR
389def _dbsetup_sqlite():
390 global db_api
391 import sqlite3 as db_api
392 for i in range(cfg.get('erebus', 'num_db_connections', 2)):
393 main.db = db_api.connect(cfg.dbhost)
394 main.db.row_factory = db_api.Row
395 main.db.isolation_level = None
396 main.dbs.append(main.db)
586997a7 397
b25d4368 398def setup():
db50981b 399 global cfg, main
400
48479459 401 cfg = config.Config('bot.config')
e64ac4a0 402
dcc5bde3 403 if cfg.getboolean('debug', 'gc'):
2ffef3ff 404 gc.set_debug(gc.DEBUG_LEAK)
405
e64ac4a0 406 pidfile = open(cfg.pidfile, 'w')
407 pidfile.write(str(os.getpid()))
408 pidfile.close()
409
c0eee1b4 410 main = Erebus(cfg)
bffe0139 411 dbsetup()
db50981b 412
413 autoloads = [mod for mod, yes in cfg.items('autoloads') if int(yes) == 1]
414 for mod in autoloads:
b9c6eb1d 415 ctlmod.load(main, mod)
db50981b 416
2729abc8 417 c = main.query("SELECT nick, user, bind, authname, authpass FROM bots WHERE active = 1")
418 if c:
4fa1118b 419 rows = c.fetchall()
420 c.close()
421 for row in rows:
0af282c6 422 main.newbot(row['nick'], row['user'], row['bind'], row['authname'], row['authpass'], cfg.host, cfg.port, cfg.realname)
a12f7519 423 main.connectall()
b25d4368 424
425def loop():
49a455aa 426 poready = main.poll()
fd96a423 427 for fileno in poready:
9d44d267
JR
428 try:
429 data = main.fd(fileno).getdata()
430 except:
0ef0d38b 431 main.log('*', '!', 'Error receiving data: getdata raised exception for socket %d, closing' % (fileno))
9d44d267
JR
432 traceback.print_exc()
433 data = None
434 if data is None:
435 main.fd(fileno).close()
436 else:
437 for line in data:
4aa86bbb
JR
438 if cfg.getboolean('debug', 'io'):
439 main.log(str(main.fd(fileno)), 'I', line)
9d44d267
JR
440 try:
441 main.fd(fileno).parse(line)
442 except:
0ef0d38b 443 main.log('*', '!', 'Error receiving data: parse raised exception for socket %d data %r, ignoring' % (fileno, line))
9d44d267 444 traceback.print_exc()
2a44c0cd 445 if main.mustquit is not None:
dc0f891b 446 main.log('*', '!', 'Core exiting due to: %s' % (main.mustquit))
2a44c0cd 447 raise main.mustquit
b25d4368 448
449if __name__ == '__main__':
963f2522 450 try: os.rename('logfile', 'oldlogs/%s' % (time.time()))
24b74bb3 451 except: pass
3d724d3a 452 sys.stdout = open('logfile', 'w', 1)
24b74bb3 453 sys.stderr = sys.stdout
b25d4368 454 setup()
49a455aa 455 while True: loop()