]>
jfr.im git - erebus.git/blob - erebus.py
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
, modlib
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
_u ser
, name
)
107 def __setattr__ ( self
, name
, value
):
108 setattr ( self
._ bound
_u ser
, name
, value
)
109 def msg ( self
, * args
, ** kwargs
):
110 self
._ bound
_ bot
. msg ( self
._ bound
_u ser
, * args
, ** kwargs
)
111 def slowmsg ( self
, * args
, ** kwargs
):
112 self
._ bound
_ bot
. slowmsg ( self
._ bound
_u ser
, * args
, ** kwargs
)
113 def fastmsg ( self
, * args
, ** kwargs
):
114 self
._ bound
_ bot
. fastmsg ( self
._ bound
_u ser
, * args
, ** kwargs
)
115 def __repr__ ( self
): return "<_BoundUser %r %r >" % ( self
._ bound
_u ser
, self
._ bound
_ bot
)
117 class Channel ( object ):
118 def __init__ ( self
, name
, bot
):
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
129 c
= main
. query ( "SELECT user, level FROM chusers WHERE chan = %s " , ( self
. name
,))
132 while row
is not None :
133 self
. levels
[ row
[ 'user' ]] = row
[ 'level' ]
137 def msg ( self
, * args
, ** kwargs
):
138 self
. bot
. msg ( self
, * args
, ** kwargs
)
139 def slowmsg ( self
, * args
, ** kwargs
):
140 self
. bot
. slowmsg ( self
, * args
, ** kwargs
)
141 def fastmsg ( self
, * args
, ** kwargs
):
142 self
. bot
. fastmsg ( self
, * args
, ** kwargs
)
144 def levelof ( self
, auth
):
148 if auth
in self
. levels
:
149 return self
. levels
[ auth
]
153 def setlevel ( self
, auth
, level
, savetodb
= True ):
156 c
= main
. query ( "REPLACE INTO chusers (chan, user, level) VALUES ( %s , %s , %s )" , ( self
. name
, auth
, level
))
158 self
. levels
[ auth
] = level
163 self
. levels
[ auth
] = level
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
)
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
)
184 def __str__ ( self
): return self
. name
185 def __repr__ ( self
): return "<Channel %r >" % ( self
. name
)
187 def __init__ ( self
, cfg
):
189 self
. starttime
= time
. time ()
191 self
. trigger
= cfg
. trigger
192 if os
. name
== "posix" :
194 self
. po
= select
. poll ()
195 else : # f.e. os.name == "nt" (Windows)
196 self
. potype
= "select"
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.
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
211 log_noretry
= ', noretry=True'
212 self
. log ( "[SQL]" , "?" , "query( %r , %r%s )" % ( sql
, parameters
, log_noretry
))
215 curs
= self
. db
. cursor ()
216 res
= curs
. execute ( sql
, parameters
)
221 except db_api
. DataError
as e
:
222 self
. log ( "[SQL]" , "." , "DB DataError: %r " % ( e
))
224 except db_api
. Error
as e
:
225 self
. log ( "[SQL]" , "!" , "DB error! %r " % ( e
))
228 return self
. query ( sql
, parameters
, noretry
= True )
232 def querycb ( self
, cb
, * args
, ** kwargs
):
233 # TODO this should either get thrown out with getdb()/returndb(), or else be adjusted to make use of it.
235 cb ( self
. query (* args
, ** kwargs
))
236 threading
. Thread ( target
= run_query
). start ()
238 def newbot ( self
, nick
, user
, bind
, authname
, authpass
, server
, port
, realname
):
239 if bind
is None : bind
= ''
240 obj
= bot
. Bot ( self
, nick
, user
, bind
, authname
, authpass
, server
, port
, realname
)
241 self
. bots
[ nick
. lower ()] = obj
243 def newfd ( self
, obj
, fileno
):
244 if not isinstance ( obj
, modlib
. Socketlike
):
245 raise Exception ( 'Attempted to hook a socket without a class to process data' )
246 self
. fds
[ fileno
] = obj
247 if self
. potype
== "poll" :
248 self
. po
. register ( fileno
, select
. POLLIN
)
249 elif self
. potype
== "select" :
250 self
. fdlist
. append ( fileno
)
251 def delfd ( self
, fileno
):
253 if self
. potype
== "poll" :
254 self
. po
. unregister ( fileno
)
255 elif self
. potype
== "select" :
256 self
. fdlist
. remove ( fileno
)
258 def bot ( self
, name
): #get Bot() by name (nick)
259 return self
. bots
[ name
. lower ()]
260 def fd ( self
, fileno
): #get Bot() by fd/fileno
261 return self
. fds
[ fileno
]
262 def randbot ( self
): #get Bot() randomly
263 return self
. bots
[ random
. choice ( list ( self
. bots
. keys ()))]
265 def user ( self
, _nick
, send_who
= False , create
= True ):
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
))
271 if nick
in self
. users
:
272 return self
. users
[ nick
]
274 user
= self
. User ( _nick
)
275 self
. users
[ nick
] = user
279 def channel ( self
, name
): #get Channel() by name
280 if name
. lower () in self
. chans
:
281 return self
. chans
[ name
. lower ()]
285 def newchannel ( self
, bot
, name
):
286 chan
= self
. Channel ( name
. lower (), bot
)
287 self
. chans
[ name
. lower ()] = chan
292 if self
. potype
== "poll" :
293 pollres
= self
. po
. poll ( timeout_seconds
* 1000 )
294 return [ fd
for ( fd
, ev
) in pollres
]
295 elif self
. potype
== "select" :
296 return select
. select ( self
. fdlist
, [], [], timeout_seconds
)[ 0 ]
298 def connectall ( self
):
299 for bot
in self
. bots
. values ():
300 if bot
. conn
. state
== 0 :
303 def module ( self
, name
):
304 return ctlmod
. modules
[ name
]
306 def log ( self
, source
, level
, message
):
307 print ( " %0 9.3f %s [ %s ] %s " % ( time
. time () % 100000 , source
, level
, message
))
309 def getuserbyauth ( self
, auth
):
310 return [ u
for u
in self
. users
. values () if u
. auth
== auth
. lower ()]
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."""
316 return self
. dbs
. pop ()
318 def returndb ( self
, db
):
322 def hook ( self
, word
, handler
):
324 self
. msghandlers
[ word
]. append ( handler
)
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
)
330 def hashook ( self
, word
):
331 return word
in self
. msghandlers
and len ( self
. msghandlers
[ word
]) != 0
332 def gethook ( self
, word
):
333 return self
. msghandlers
[ word
]
335 def hooknum ( self
, word
, handler
):
337 self
. numhandlers
[ word
]. append ( handler
)
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
]
348 def hookchan ( self
, chan
, handler
):
350 self
. chanhandlers
[ chan
]. append ( handler
)
352 self
. chanhandlers
[ chan
] = [ handler
]
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
]
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
))
374 dbtype
= cfg
. get ( 'erebus' , 'dbtype' , 'mysql' )
375 if dbtype
== 'mysql' :
377 elif dbtype
== 'sqlite' :
380 main
. log ( '*' , '!' , 'Unknown dbtype in config: %s ' % ( dbtype
))
382 def _dbsetup_mysql ():
384 import MySQLdb
as db_api
, MySQLdb
. cursors
385 for i
in range ( cfg
. get ( 'erebus' , 'num_db_connections' , 2 )- 1 ):
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
)
389 def _dbsetup_sqlite ():
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
)
401 cfg
= config
. Config ( 'bot.config' )
403 if cfg
. getboolean ( 'debug' , 'gc' ):
404 gc
. set_debug ( gc
. DEBUG_LEAK
)
406 pidfile
= open ( cfg
. pidfile
, 'w' )
407 pidfile
. write ( str ( os
. getpid ()))
413 autoloads
= [ mod
for mod
, yes
in cfg
. items ( 'autoloads' ) if int ( yes
) == 1 ]
414 for mod
in autoloads
:
415 ctlmod
. load ( main
, mod
)
417 c
= main
. query ( "SELECT nick, user, bind, authname, authpass FROM bots WHERE active = 1" )
422 main
. newbot ( row
[ 'nick' ], row
[ 'user' ], row
[ 'bind' ], row
[ 'authname' ], row
[ 'authpass' ], cfg
. host
, cfg
. port
, cfg
. realname
)
426 poready
= main
. poll ()
427 for fileno
in poready
:
429 data
= main
. fd ( fileno
). getdata ()
431 main
. log ( '*' , '!' , 'Error receiving data: getdata raised exception for socket %d , closing' % ( fileno
))
432 traceback
. print_exc ()
435 main
. fd ( fileno
). close ()
438 if cfg
. getboolean ( 'debug' , 'io' ):
439 main
. log ( str ( main
. fd ( fileno
)), 'I' , line
)
441 main
. fd ( fileno
). parse ( line
)
443 main
. log ( '*' , '!' , 'Error receiving data: parse raised exception for socket %d data %r , ignoring' % ( fileno
, line
))
444 traceback
. print_exc ()
445 if main
. mustquit
is not None :
446 main
. log ( '*' , '!' , 'Core exiting due to: %s ' % ( main
. mustquit
))
449 if __name__
== '__main__' :
450 try : os
. rename ( 'logfile' , 'oldlogs/ %s ' % ( time
. time ()))
452 sys
. stdout
= open ( 'logfile' , 'w' , 1 )
453 sys
. stderr
= sys
. stdout