]>
Commit | Line | Data |
---|---|---|
b069ba10 JR |
1 | #!/usr/bin/python |
2 | ||
3 | ### | |
4 | ### Ophion Bot | |
5 | ### v1.00 | |
6 | ### Copyright 2012 John Runyon | |
bd933211 | 7 | ### <https://github.com/Mustis/Ophion> |
b069ba10 JR |
8 | ### |
9 | ||
10 | ## CONFIG | |
11 | dbhost = 'localhost' | |
12 | dbuser = 'bot' | |
13 | dbpass = 'roboticism' | |
14 | dbname = 'bot' | |
550b7560 | 15 | rootdir = '/home/bots/' |
b069ba10 JR |
16 | logfile = rootdir+'/output.log' |
17 | excfile = rootdir+'/exception.log' | |
18 | ||
550b7560 JR |
19 | oidfile = '/home/bots/.oidentd.conf' |
20 | identprefix = 'ophion' # ident will be <identprefix><bot ID#> | |
b069ba10 JR |
21 | |
22 | import socket, select, sys, os, signal, time | |
23 | from threading import * | |
24 | from traceback import print_exc | |
25 | ||
26 | import MySQLdb, MySQLdb.cursors | |
27 | ||
28 | from classes import * | |
29 | from util import * | |
30 | ||
31 | cache = Cache() | |
32 | ||
33 | def quit(reason, restart=False): | |
34 | cache.quitting = True | |
35 | cache.mainBot.cmsg('fatal', "Shutting down because: %s" % (reason)) | |
36 | time.sleep(1) | |
37 | for bot in cache.bots.values(): | |
38 | bot.disconnect() | |
39 | for tkey in cache.timers.keys(): | |
40 | cache.timers[tkey].cancel() | |
41 | del cache.timers[tkey] | |
42 | for modf in cache.modules: | |
43 | unloadmod(modf, cache, True) | |
44 | ||
45 | if restart: | |
46 | os.execv(sys.argv[0], sys.argv) | |
47 | sys.exit(0) | |
48 | def sigHandle(signum, stack): | |
49 | quit("Signal: %d" % (signum)) | |
50 | def online(bid): | |
51 | return (bid in cache.bots and cache.bots[bid].online) | |
52 | def toofew(bot, nick): # like: if len(params) < NEEDED: return toofew(bot, nick) | |
53 | bot.msg(nick, "Too few parameters for that command.") | |
54 | return False | |
55 | ||
56 | def sendRaws(): | |
57 | for bot in cache.bots.values(): | |
58 | bot.sendRaws() | |
59 | if not cache.quitting: | |
60 | t = Timer(1, sendRaws) | |
61 | t.daemon = True | |
62 | if 'raws' in cache.timers: del cache.timers['raws'] | |
63 | cache.timers['raws'] = t | |
64 | t.start() | |
65 | ||
66 | def connectBots(): | |
67 | curs = cache.dbc.cursor() | |
68 | curs.execute("SELECT 1") | |
69 | curs.close() | |
70 | ||
71 | connected = 0 | |
72 | for bot in cache.bots.values(): | |
73 | if not bot.online: | |
74 | ident = "%s%d" % (identprefix, bot.id) | |
75 | oid = open(oidfile, 'w') | |
76 | oid.write("global {\n\treply \"%s\"\n}" % ident) | |
77 | oid.close() | |
78 | if bot.connect(ident): | |
79 | bot.cmsg('info', "Connected") | |
80 | connected += 1 | |
81 | if connected == 2: | |
82 | break | |
83 | ||
84 | if not cache.quitting: | |
85 | if connected < 2: interval = 300 | |
86 | else: interval = 60 | |
87 | t = Timer(interval, connectBots) | |
88 | t.daemon = True | |
89 | if 'conn' in cache.timers: del cache.timers['conn'] | |
90 | cache.timers['conn'] = t | |
91 | t.start() | |
92 | ||
93 | return connected | |
94 | def makeBots(): | |
95 | curs = cache.dbc.cursor() | |
96 | curs.execute("SELECT id FROM bots") | |
97 | row = curs.fetchone() | |
98 | while row is not None: | |
99 | bot = Bot(row['id']) | |
100 | if row['id'] == 0: | |
101 | cache.mainBot = bot | |
102 | bot.mainbot = True | |
103 | cache.bots[row['id']] = bot | |
104 | row = curs.fetchone() | |
105 | ||
106 | def fillCache(): | |
107 | curs = cache.dbc.cursor() | |
108 | curs.execute("SELECT username, level FROM admins") | |
109 | row = curs.fetchone() | |
110 | while row is not None: | |
111 | cache.admins[row['username']] = row['level'] | |
112 | row = curs.fetchone() | |
113 | curs.execute("SELECT chname FROM chans ORDER BY id LIMIT 2") | |
114 | rows = curs.fetchall() | |
115 | cache.ctrl = rows[0]['chname'] | |
116 | cache.home = rows[1]['chname'] | |
117 | curs.close() | |
118 | makeBots() | |
119 | ||
120 | def parseCmd(bot, line, linepieces, msg, iscm): | |
121 | hostmask = linepieces[0][1:] | |
122 | fromnick = hostmask.split('!')[0] | |
123 | target = linepieces[2] | |
124 | pieces = msg.split() | |
125 | cmd = pieces[0].upper() | |
126 | if len(pieces) == 1: params = '' | |
127 | else: params = ' '.join(pieces[1:]) | |
128 | ||
129 | ret = "" | |
130 | ||
131 | if cmd not in cache.cmds: | |
132 | noaccess(bot, fromnick) | |
133 | return ret | |
134 | else: ci = cache.cmds[cmd] | |
135 | ||
136 | if not ci['isadmin']: | |
137 | if fromnick not in cache.users: | |
138 | cache.users[fromnick] = User(fromnick) | |
139 | auth = cache.users[fromnick].auth | |
140 | if not iscm: | |
141 | if ci['reqchan']: | |
142 | target = pieces.pop(1).lower() | |
143 | params = ' '.join(pieces[1:]) | |
144 | else: | |
145 | target = fromnick | |
146 | if target not in cache.chans: | |
147 | bot.msg(fromnick, "No such channel.") | |
148 | return ret | |
149 | chan = cache.chans[target] | |
150 | if auth not in chan.alist or chan.alist[auth] < ci['level']: | |
151 | noaccess(bot, fromnick, True) | |
152 | return ret | |
153 | elif len(pieces)-1 < cache.cmds[cmd]['params']: | |
154 | toofew(bot, fromnick) | |
155 | return ret | |
156 | else: | |
157 | try: ret = cache.cmds[cmd]['func'](fromnick, target, params, chan.bot, cache) | |
158 | except Exception as e: | |
159 | print_exc(None, cache.excfile) | |
160 | bot.msg(fromnick, "An error occurred, sorry! Try again later.") | |
161 | cache.mainBot.cmsg('warn', "EXCEPTION! %r Caused by <%s> %s" % (e, fromnick, line)) | |
162 | ||
163 | else: # admin/global command | |
164 | if fromnick in cache.users: glblevel = cache.users[fromnick].access | |
165 | else: glblevel = 0 | |
166 | if glblevel < ci['level']: | |
167 | noaccess(bot, fromnick) | |
168 | elif len(pieces)-1 < cache.cmds[cmd]['params']: | |
169 | toofew(bot, fromnick) | |
170 | else: | |
171 | try: ret = cache.cmds[cmd]['func'](fromnick, target, params, bot, cache) | |
172 | except Exception as e: | |
173 | print_exc(None, cache.excfile) | |
174 | bot.msg(fromnick, "An error occurred, sorry! Try again later.") | |
175 | cache.mainBot.cmsg('warn', "EXCEPTION! %r Caused by <%s> %s" % (e, fromnick, line)) | |
176 | return ret | |
177 | ||
178 | def _parse(bot, line): | |
179 | pieces = line.split() | |
180 | if pieces[1] == "PRIVMSG": | |
181 | target = pieces[2] | |
182 | msg = ' '.join(pieces[3:]).lstrip(':') | |
183 | if msg[0] == cache.trigger: | |
184 | return parseCmd(bot, line, pieces, msg[1:], True) | |
185 | elif target == bot.nick: | |
186 | return parseCmd(bot, line, pieces, msg, False) | |
187 | elif pieces[1] == "JOIN": | |
188 | nick = pieces[0][1:].split('!')[0] | |
189 | host = pieces[0][1:].split('@')[1] | |
190 | chname = pieces[2].lower() | |
191 | if chname in cache.chans: | |
192 | bot.joined(chname, nick) | |
193 | elif pieces[1] == "PART": | |
194 | nick = pieces[0][1:].split('!')[0] | |
195 | chname = pieces[2].lower() | |
196 | if chname in cache.chans: | |
197 | bot.parted(chname, nick) | |
198 | elif pieces[1] == "QUIT": | |
199 | nick = pieces[0][1:].split('!')[0] | |
200 | bot.quit(nick) | |
201 | elif pieces[1] == "NICK": | |
202 | fromnick = pieces[0][1:].split('!')[0] | |
203 | tonick = pieces[2][1:] | |
204 | cache.users[tonick] = cache.users[fromnick] | |
205 | del cache.users[fromnick] | |
206 | cache.users[tonick].nick = tonick | |
207 | elif pieces[0] == "PING": | |
208 | bot.rawnow("PONG %s" % (pieces[1])) | |
209 | elif pieces[0] == "ERROR": | |
210 | try: bot.disconnect() | |
211 | except: pass | |
212 | elif pieces[1] in cache.nums: | |
213 | for fn in cache.nums[pieces[1]]: | |
214 | try: fn(line, bot) | |
215 | except Exception as e: | |
216 | print_exc(None, cache.excfile) | |
217 | bot.msg(fromnick, "An error occurred, sorry! Try again later.") | |
218 | cache.mainBot.cmsg('warn', "EXCEPTION! %r Caused by <%s> %s" % (e, fromnick, line)) | |
219 | ||
220 | def parse(bot): | |
221 | line = bot.get().strip() | |
222 | return _parse(bot, line) | |
223 | ||
224 | ||
225 | def loop(): | |
226 | while True: | |
227 | ready = cache.poll.poll() | |
228 | for fde in ready: | |
229 | ret = "" | |
230 | if fde[0] == sys.stdin.fileno(): | |
231 | line = sys.stdin.readline().strip() | |
232 | pieces = line.split(None, 2) | |
233 | if len(pieces) == 3 and pieces[1].isdigit(): | |
234 | mode = pieces[0] | |
235 | bid = int(pieces[1]) | |
236 | line = pieces[2] | |
237 | if mode.upper() == "IN": ret = _parse(cache.bots[bid], line) | |
238 | elif mode.upper() == "OUT": cache.bots[bid].rawnow(line) | |
239 | else: print "ERROR! <'IN'|'OUT'>:<bid>:<line>" | |
240 | else: print "ERROR! <'IN'|'OUT'>:<bid>:<line>" | |
241 | elif fde[0] in cache.botsByFD: # it's a bot | |
242 | bot = cache.botsByFD[fde[0]] | |
243 | if fde[1] & select.POLLERR or fde[1] & select.POLLHUP or fde[1] & select.POLLNVAL: | |
244 | bot.disconnect() | |
245 | else: | |
246 | ret = parse(bot) | |
247 | if ret == "QUIT": break | |
248 | else: continue | |
249 | break # if the "for" was broken, break the while as well. | |
250 | ||
251 | ||
252 | signal.signal(signal.SIGHUP, signal.SIG_IGN) | |
253 | signal.signal(signal.SIGINT, sigHandle) | |
254 | ||
255 | cache.excfile = open(excfile, 'a') | |
256 | ||
257 | sys.path.append(rootdir+'/modules') | |
258 | sys.path.append(rootdir+'/modules/autoload') | |
259 | for modf in os.listdir(rootdir+'/modules/autoload'): | |
260 | if modf[-3:] == ".py": | |
261 | loadmod(modf[:-3], cache) | |
262 | ||
263 | cache.dbc = MySQLdb.connect(host=dbhost, user=dbuser, passwd=dbpass, db=dbname, cursorclass=MySQLdb.cursors.DictCursor) | |
264 | fillCache() | |
265 | ||
266 | cache.poll = select.poll() | |
267 | ||
268 | cache.poll.register(sys.stdin.fileno(), select.POLLIN) | |
269 | ||
270 | t = Timer(1, sendRaws) | |
271 | t.daemon = True | |
272 | cache.timers['raws'] = t | |
273 | t.start() | |
274 | ||
275 | connectBots() | |
276 | t = Timer(60, connectBots) | |
277 | t.daemon = True | |
278 | cache.timers['conn'] = t | |
279 | t.start() | |
280 | ||
281 | loop() |