]> jfr.im git - erebus.git/blame - bot.py
urls - allow port in url
[erebus.git] / bot.py
CommitLineData
b25d4368 1#!/usr/bin/python
4477123d 2# vim: fileencoding=utf-8
b25d4368 3
931c88a4 4# Erebus IRC bot - Author: John Runyon
5# "Bot" and "BotConnection" classes (handling a specific "arm")
6
39fd397a 7import os, random, socket, struct, sys, threading, time, traceback, fcntl
e64ac4a0 8from collections import deque
b25d4368 9
a28e2ae9 10if sys.version_info.major < 3:
11 timerbase = threading._Timer
d6052ebf 12 stringbase = basestring
a28e2ae9 13else:
14 timerbase = threading.Timer
d6052ebf 15 stringbase = str
f89262c4 16
a28e2ae9 17class MyTimer(timerbase):
2ffa3996 18 def __init__(self, *args, **kwargs):
a28e2ae9 19 timerbase.__init__(self, *args, **kwargs)
2ffa3996 20 self.daemon = True
21
d6052ebf 22if sys.version_info.major < 3:
23 stringbase = basestring
24else:
25 stringbase = str
2ffa3996 26
b25d4368 27#bots = {'erebus': bot.Bot(nick='Erebus', user='erebus', bind='', server='irc.quakenet.org', port=6667, realname='Erebus')}
28class Bot(object):
0af282c6 29 def __init__(self, parent, nick, user, bind, authname, authpass, server, port, realname):
a9a00d34
JR
30 self.maxlen = 510
31
b25d4368 32 self.parent = parent
33 self.nick = nick
0784e720 34 self.permnick = nick
a12f7519 35 self.user = user
36 self.realname = realname
5477b368 37
0af282c6 38 self.authname = authname
39 self.authpass = authpass
40
1e76e96c
JR
41 self.connecttime = 0 # time at which we received numeric 001
42 self.server = server # the address we try to (re-)connect to
43 self.port = port
44 self.servername = server # the name of the server we got connected to
45
50331d1a 46 curs = self.parent.query("SELECT chname FROM chans WHERE bot = %s AND active = 1", (self.permnick,))
2729abc8 47 if curs:
4fa1118b 48 chansres = curs.fetchall()
49 curs.close()
50 self.chans = [self.parent.newchannel(self, row['chname']) for row in chansres]
6de27fd4 51 else:
52 self.chans = []
b25d4368 53
a12f7519 54 self.conn = BotConnection(self, bind, server, port)
e64ac4a0 55
2ffa3996 56 self.lastreceived = time.time() #time we last received a line from the server
d6c6516c 57 self.watchdog()
2ffa3996 58
e64ac4a0 59 self.msgqueue = deque()
2bb267e0 60 self.slowmsgqueue = deque()
82025c0a
JR
61 self._makemsgtimer()
62 self._msgtimer.start()
04fe7fd8 63 self.joined_chans = False
e64ac4a0 64
e40e5b39 65 def __del__(self):
2729abc8 66 try:
50331d1a 67 curs = self.parent.query("UPDATE bots SET connected = 0 WHERE nick = %s", (self.permnick,))
2729abc8 68 curs.close()
69 except: pass
e40e5b39 70
2ffa3996 71 def watchdog(self):
b8150e3d 72 if time.time() > int(self.parent.cfg.get('watchdog', 'maxtime', default=300))+self.lastreceived:
2ffa3996 73 self.parse("ERROR :Fake-error from watchdog timer.")
a9c33762 74 return
b8150e3d
JR
75 if self.conn.registered():
76 self.conn.send("PING :%s" % (time.time()))
a46d97d3 77 self._checknick()
2a44c0cd
JR
78 watchdogtimer = MyTimer(int(self.parent.cfg.get('watchdog', 'interval', default=30)), self.watchdog)
79 watchdogtimer.start()
e64ac4a0 80
a8553c45 81 def log(self, *args, **kwargs):
82 self.parent.log(self.nick, *args, **kwargs)
83
b25d4368 84 def connect(self):
d3531ad2 85 self.log('!', "Connecting")
b2a896c8 86 if self.conn.connect():
b736b336 87 self.log('!', "Connected")
49a455aa 88 self.parent.newfd(self, self.conn.socket.fileno())
89
b25d4368 90 def getdata(self):
b8150e3d
JR
91 try:
92 recvd = self.conn.read()
93 self.lastreceived = time.time()
94 return recvd
95 except EOFError as e:
b736b336 96 return [":%s ERROR :%s%r" % (self.nick, e.__class__.__name__, e.args)]
a4eacae2 97
0784e720 98 def _checknick(self): # check if we're using the right nick, try changing
99 if self.nick != self.permnick and self.conn.registered():
100 self.conn.send("NICK %s" % (self.permnick))
101
b25d4368 102 def parse(self, line):
103 pieces = line.split()
a4eacae2 104
a99afee6
JR
105 if pieces[0][0] == ":":
106 numeric = pieces[1]
107 else:
108 numeric = pieces[0]
109
a38e8be0 110 # dispatch dict
b8150e3d 111 dispatch = {
c22ee2ba 112 'NOTICE': self._gotconnected,
28d06664 113 '001': self._got001,
1e76e96c 114 '004': self._got004,
0784e720 115 '376': self._gotRegistered,
116 '422': self._gotRegistered,
28d06664 117 'PRIVMSG': self._gotprivmsg,
84b7c247 118 '353': self._got353, #NAMES
119 '354': self._got354, #WHO
1aba32fb 120 '396': self._gotHiddenHost, # hidden host has been set
0784e720 121 '433': self._got433, #nick in use
c3c8dcf7 122 '437': self._got433, #nick protected
28d06664 123 'JOIN': self._gotjoin,
124 'PART': self._gotpart,
6de27fd4 125 'KICK': self._gotkick,
28d06664 126 'QUIT': self._gotquit,
127 'NICK': self._gotnick,
128 'MODE': self._gotmode,
a99afee6
JR
129 'PING': self._gotping,
130 'ERROR': self._goterror,
28d06664 131 }
d1ea2946 132
a99afee6
JR
133 if self.parent.hasnumhook(numeric):
134 hooks = self.parent.getnumhook(numeric)
e4a4c762 135 for callback in hooks:
a38e8be0 136 try:
137 callback(self, line)
138 except Exception:
e8885384 139 self._cbexception("numhook", line)
e4a4c762 140
a99afee6
JR
141 if numeric in dispatch:
142 dispatch[numeric](pieces)
28d06664 143
0784e720 144 def _gotconnected(self, pieces):
28d06664 145 if not self.conn.registered():
146 self.conn.register()
147 def _gotping(self, pieces):
148 self.conn.send("PONG %s" % (pieces[1]))
0784e720 149 self._checknick()
f5f2b592 150 def _goterror(self, pieces):
666366fd 151 # TODO: better handling, just reconnect that single bot
dc0f891b 152 error = ' '.join(pieces)
2729abc8 153 try:
dc0f891b
JR
154 raise Exception(error)
155 except Exception as e:
156 self.parent.mustquit = e
157 try:
158 self.quit("Error detected: %s" % (error))
666366fd 159 except: pass
160 try:
2729abc8 161 curs = self.parent.query("UPDATE bots SET connected = 0")
162 curs.close()
2ffa3996 163 except: pass
dc0f891b 164 self.log('!', 'Bot exiting due to: %s' % (error))
28d06664 165 def _got001(self, pieces):
1e76e96c
JR
166 # We wait until the end of MOTD instead to consider ourselves registered, but consider uptime as of 001
167 self.connecttime = time.time()
168 def _got004(self, pieces):
169 self.servername = pieces[3]
0784e720 170 def _gotRegistered(self, pieces):
28d06664 171 self.conn.registered(True)
e40e5b39 172
50331d1a 173 curs = self.parent.query("UPDATE bots SET connected = 1 WHERE nick = %s", (self.permnick,))
2729abc8 174 if curs: curs.close()
e40e5b39 175
28d06664 176 self.conn.send("MODE %s +x" % (pieces[2]))
177 if self.authname is not None and self.authpass is not None:
04fe7fd8 178 self.conn.send(self.parent.cfg.get('erebus', 'auth_command', "AUTH %s %s") % (self.authname, self.authpass))
1aba32fb
JR
179 if not self.parent.cfg.getboolean('erebus', 'wait_for_hidden_host'):
180 for c in self.chans:
181 self.join(c.name)
04fe7fd8 182 self.joined_chans = True
1aba32fb 183 def _gotHiddenHost(self, pieces):
04fe7fd8 184 if not self.joined_chans and self.parent.cfg.getboolean('erebus', 'wait_for_hidden_host'):
1aba32fb
JR
185 for c in self.chans:
186 self.join(c.name)
04fe7fd8 187 self.joined_chans = True
28d06664 188 def _gotprivmsg(self, pieces):
189 nick = pieces[0].split('!')[0][1:]
190 user = self.parent.user(nick)
191 target = pieces[2]
192 msg = ' '.join(pieces[3:])[1:]
193 self.parsemsg(user, target, msg)
84b7c247 194 def _got353(self, pieces):
2591a1c8 195 prefixes = {'@': 'op', '+': 'voice'}
84b7c247 196 chan = self.parent.channel(pieces[4])
197 names = pieces[5:]
198 names[0] = names[0][1:] #remove colon
199 for n in names:
2591a1c8 200 if n[0] in prefixes:
201 user = self.parent.user(n[1:])
202 chan.userjoin(user, prefixes[n[0]])
84b7c247 203 else:
2591a1c8 204 user = self.parent.user(n)
84b7c247 205 chan.userjoin(user)
206 user.join(chan)
28d06664 207 def _got354(self, pieces):
14011220 208 qt = int(pieces[3])
209 if qt < 3:
210 nick, auth = pieces[4:6]
211 chan = None
212 else:
213 chan, nick, auth = pieces[4:7]
214 chan = self.parent.channel(chan)
e40e5b39 215 user = self.parent.user(nick)
216 user.authed(auth)
14011220 217
218 if chan is not None:
219 user.join(chan)
220 chan.userjoin(user)
221
222 if qt == 2: # triggered by !auth
e40e5b39 223 if user.isauthed():
224 if user.glevel > 0:
225 self.msg(nick, "You are now known as #%s (access level: %s)" % (auth, user.glevel))
226 else:
227 self.msg(nick, "You are now known as #%s (not staff)" % (auth))
228 else:
229 self.msg(nick, "I tried, but you're not authed!")
0784e720 230 def _got433(self, pieces):
231 if not self.conn.registered(): #we're trying to connect
71ef8273 232 newnick = "%s%d" % (self.nick, random.randint(111, 999))
0784e720 233 self.conn.send("NICK %s" % (newnick))
234 self.nick = newnick
28d06664 235 def _gotjoin(self, pieces):
236 nick = pieces[0].split('!')[0][1:]
237 chan = self.parent.channel(pieces[2])
238
239 if nick == self.nick:
14011220 240 self.conn.send("WHO %s c%%cant,3" % (chan))
28d06664 241 else:
f6386fa7 242 user = self.parent.user(nick, send_who=True)
28d06664 243 chan.userjoin(user)
244 user.join(chan)
6de27fd4 245 def _clientLeft(self, nick, chan):
f6386fa7
JR
246 if nick == self.nick:
247 for u in chan.users:
248 if u.nick != self.nick:
249 self._clientLeft(u.nick, chan)
79519d5a
JR
250 if chan.deleting:
251 chan.bot.chans.remove(chan)
252 del self.parent.chans[chan.name.lower()]
253 del chan
f6386fa7
JR
254 else:
255 user = self.parent.user(nick)
256 gone = user.part(chan)
257 chan.userpart(user)
14011220 258 if gone:
f6386fa7 259 user.quit()
14011220 260 del self.parent.users[nick.lower()]
6de27fd4 261 def _gotpart(self, pieces):
262 nick = pieces[0].split('!')[0][1:]
263 chan = self.parent.channel(pieces[2])
264 self._clientLeft(nick, chan)
265 def _gotkick(self, pieces):
266 nick = pieces[3]
267 chan = self.parent.channel(pieces[2])
268 self._clientLeft(nick, chan)
28d06664 269 def _gotquit(self, pieces):
270 nick = pieces[0].split('!')[0][1:]
271 if nick != self.nick:
14011220 272 for chan in self.parent.user(nick).chans:
273 chan.userpart(self.parent.user(nick))
28d06664 274 self.parent.user(nick).quit()
275 del self.parent.users[nick.lower()]
276 def _gotnick(self, pieces):
277 oldnick = pieces[0].split('!')[0][1:]
278 newnick = pieces[2][1:]
f89262c4 279 if oldnick == self.nick:
280 self.nick = newnick
281 else:
282 if newnick.lower() != oldnick.lower():
283 self.parent.users[newnick.lower()] = self.parent.users[oldnick.lower()]
284 del self.parent.users[oldnick.lower()]
285 self.parent.users[newnick.lower()].nickchange(newnick)
84b7c247 286 def _gotmode(self, pieces):
287 source = pieces[0].split('!')[0][1:]
fe73f782 288 chan = pieces[2]
289 if not chan.startswith("#"): return
84b7c247 290 chan = self.parent.channel(pieces[2])
291 mode = pieces[3]
292 args = pieces[4:]
293
294 adding = True
295 for c in mode:
296 if c == '+':
297 adding = True
298 elif c == '-':
299 adding = False
300 elif c == 'o':
301 if adding:
302 chan.userop(self.parent.user(args.pop(0)))
303 else:
304 chan.userdeop(self.parent.user(args.pop(0)))
305 elif c == 'v':
306 if adding:
307 chan.uservoice(self.parent.user(args.pop(0)))
308 else:
309 chan.userdevoice(self.parent.user(args.pop(0)))
310 else:
311 pass # don't care about other modes
b6212f14 312
e8885384
JR
313 def _cbexception(self, source, *args, chained=False, **kwargs):
314 if not chained: # skip hooks if we were caused by a hook
315 exc = sys.exception()
316 if self.parent.hasexceptionhook(exc):
317 for callback in self.parent.getexceptionhook(exc):
318 try:
319 callback(self, exc, source, *args, **kwargs)
320 except Exception:
321 self._cbexception('exceptionhook', chained=True, module=callback.__module__, function=callback.__name__, underlying=(source, args, kwargs))
6b6f9624 322 if self.parent.cfg.getboolean('debug', 'cbexc'):
f59f8c9b 323 self.conn.send("PRIVMSG %s :%09.3f \ 34\1f!!! CBEXC\1f\ 3 %s" % (self.parent.cfg.get('debug', 'owner'), time.time() % 100000, source))
e8885384 324 traceback.print_exc(chain=not chained)
a8553c45 325 self.log('!', "CBEXC %s %r %r" % (source, args, kwargs))
3d724d3a 326
327
839d2b35 328 def parsemsg(self, user, target, msg):
83c2f201 329 if user.glevel <= -2: return # short circuit if user is IGNORED
839d2b35 330 chan = None
6de27fd4 331 chanparam = None # was the channel specified as part of the command?
877cd61d 332 if len(msg) == 0:
333 return
334
8545c1b1
JR
335 if target == self.nick and msg.startswith("\001"): #ctcp
336 msg = msg.strip("\001")
337 if msg:
338 pieces = msg.split()
339 if pieces[0] == "CLIENTINFO":
340 self.msg(user, "\001CLIENTINFO VERSION PING\001")
341 elif pieces[0] == "VERSION":
342 self.msg(user, "\001VERSION Erebus v%d.%d - http://jfr.im/git/erebus.git\001" % (self.parent.APIVERSION, self.parent.RELEASE))
343 elif pieces[0] == "PING":
344 if len(pieces) > 1:
345 self.msg(user, "\001PING %s\001" % (' '.join(pieces[1:])))
346 else:
347 self.msg(user, "\001PING\001")
348 return
6de27fd4 349
10b86b56 350 triggerused = msg.startswith(self.parent.trigger)
351 if triggerused: msg = msg[len(self.parent.trigger):]
352 pieces = msg.split()
353
3296dba1 354 if len(pieces) == 0:
355 return
356
6de27fd4 357 if target != self.nick: # message was sent to a channel
90b64dc0 358 try:
3296dba1 359 if pieces[0][:-1].lower() == self.nick.lower() and (pieces[0][-1] == ":" or pieces[0][-1] == ","):
360 pieces.pop(0) # command actually starts with next word
361 if len(pieces) == 0: # is there still anything left?
362 return
363 msg = ' '.join(pieces)
364 triggerused = True
90b64dc0 365 except IndexError:
a76c4bd8 366 return # "message" is empty
839d2b35 367
827ec8f0 368 if len(pieces) > 1:
369 chanword = pieces[1]
370 if chanword.startswith('#'):
371 chanparam = self.parent.channel(chanword)
372
373 if target != self.nick: # message was sent to a channel
374 chan = self.parent.channel(target)
375 if not triggerused:
376 if self.parent.haschanhook(target.lower()):
377 for callback in self.parent.getchanhook(target.lower()):
378 try:
379 cbret = callback(self, user, chan, *pieces)
f89262c4 380 if isinstance(cbret, stringbase):
381 self.reply(target, user, cbret)
827ec8f0 382 except:
383 self.msg(user, "Command failed. Code: CBEXC%09.3f" % (time.time() % 100000))
e8885384 384 self._cbexception("chanhook", user=user, target=target, msg=msg)
827ec8f0 385 return # not to bot, don't process!
386
db50981b 387 cmd = pieces[0].lower()
6de27fd4 388 rancmd = False
db50981b 389 if self.parent.hashook(cmd):
e4a4c762 390 for callback in self.parent.gethook(cmd):
827ec8f0 391 if chanparam is not None and (callback.needchan or callback.wantchan):
6de27fd4 392 chan = chanparam
393 pieces.pop(1)
e4a4c762 394 if chan is None and callback.needchan:
6de27fd4 395 rancmd = True
e4a4c762 396 self.msg(user, "You need to specify a channel for that command.")
586997a7 397 elif user.glevel >= callback.reqglevel and (not callback.needchan or chan.levelof(user.auth) >= callback.reqclevel):
6de27fd4 398 rancmd = True
3d724d3a 399 try:
400 cbret = callback(self, user, chan, target, *pieces[1:])
f89262c4 401 if isinstance(cbret, stringbase):
f2b6e85c 402 self.reply(target, user, cbret)
3d724d3a 403 except Exception:
404 self.msg(user, "Command failed. Code: CBEXC%09.3f" % (time.time() % 100000))
e8885384 405 self._cbexception("hook", user=user, target=target, msg=msg)
e40e5b39 406 except SystemExit as e:
2a44c0cd 407 self.parent.mustquit = e
2729abc8 408 try:
409 curs = self.parent.query("UPDATE bots SET connected = 0")
410 curs.close()
411 except: pass
e40e5b39 412 raise e
32b160dc 413 else:
6de27fd4 414 rancmd = True
32b160dc 415 self.msg(user, "I don't know that command.")
6de27fd4 416 if not rancmd:
417 self.msg(user, "You don't have enough access to run that command.")
3d724d3a 418
419 def __debug_nomsg(self, target, msg):
6b6f9624 420 if self.parent.cfg.getboolean('debug', 'nomsg'):
f59f8c9b 421 self.conn.send("PRIVMSG %s :%09.3f \ 34\1f!!! NOMSG\1f\ 3 %r, %r" % (self.parent.cfg.get('debug', 'owner'), time.time() % 100000, target, msg))
a8553c45 422 self.log('!', "!!! NOMSG")
423# print "%09.3f %s [!] %s" % (time.time() % 100000, self.nick, "!!! NOMSG")
e8885384 424 traceback.print_stack()
49a455aa 425
d6052ebf 426
427 def reply(self, chan, user, msg):
f2b6e85c 428 if chan is not None and (isinstance(chan, self.parent.Channel) or (isinstance(chan, stringbase) and chan[0] == "#")):
d6052ebf 429 self.msg(chan, "%s: %s" % (user, msg))
430 else:
431 self.msg(user, msg)
432
6f6d04b5
JR
433 """
434 Does the work for msg/slowmsg/fastmsg. Uses the append_callback to append to the correct queue.
435
436 In the case of fastmsg, self.conn.exceeded may be True, however, in this case append_callback=self.conn.send, so it will still be sent immediately.
437 """
e669bde0 438 def _msg(self, target, msg, truncate, append_callback, msgtype):
a9ba1edc 439 if self.parent.cfg.getboolean('erebus', 'nofakelag'): append_callback = self.conn.send
6f6d04b5 440
e669bde0 441 cmd = self._formatmsg(target, msg, msgtype)
a9a00d34 442 # The max length is much shorter than conn.maxlen (510) because of the length the server adds on about the source (us).
992876fb 443 # If you know your hostmask, you can of course figure the exact length, but it's very difficult to reliably know your hostmask.
a9a00d34 444 maxlen = self.maxmsglen()
992876fb 445 if len(cmd) > maxlen:
4d925ae3 446 if not truncate:
447 return False
448 else:
992876fb 449 cmd = cmd[:maxlen]
6f6d04b5 450
c6e6807f 451 if self.conn.exceeded or self.conn.bytessent+len(cmd) >= self.conn.recvq:
6f6d04b5 452 append_callback(cmd)
c6e6807f 453 else:
454 self.conn.send(cmd)
6f6d04b5 455
c6e6807f 456 self.conn.exceeded = True
656dc5a4 457 return True
e64ac4a0 458
e669bde0
JR
459 def msg(self, target, msg, truncate=False, *, msgtype=None):
460 """msgtype must be a valid IRC command, i.e. NOTICE or PRIVMSG; or leave as None to use default"""
461 return self._msg(target, msg, truncate, self.msgqueue.append, msgtype)
6f6d04b5 462
e669bde0
JR
463 def slowmsg(self, target, msg, truncate=False, *, msgtype=None):
464 return self._msg(target, msg, truncate, self.slowmsgqueue.append, msgtype)
2bb267e0 465
e669bde0
JR
466 def fastmsg(self, target, msg, truncate=False, *, msgtype=None):
467 return self._msg(target, msg, truncate, self.conn.send, msgtype)
c6e6807f 468
e669bde0 469 def _formatmsg(self, target, msg, msgtype):
3d724d3a 470 if target is None or msg is None:
28d06664 471 return self.__debug_nomsg(target, msg)
3d724d3a 472
6681579e 473 target = str(target)
e64ac4a0 474
e669bde0
JR
475 if msgtype is not None: command = "%s %s :%s" % (msgtype, target, msg)
476 elif target.startswith('#'): command = "PRIVMSG %s :%s" % (target, msg)
e64ac4a0 477 else: command = "NOTICE %s :%s" % (target, msg)
478
c6e6807f 479 return command
e64ac4a0 480
481 def _popmsg(self):
82025c0a 482 self._makemsgtimer()
c6e6807f 483 self.conn.bytessent -= self.conn.recvq/3
484 if self.conn.bytessent < 0: self.conn.bytessent = 0
82025c0a 485 self.conn.exceeded = True
e64ac4a0 486
82025c0a 487 cmd = None
e64ac4a0 488 try:
c6e6807f 489 cmd = self.msgqueue.popleft()
2bb267e0 490 except IndexError:
491 try:
c6e6807f 492 cmd = self.slowmsgqueue.popleft()
2bb267e0 493 except IndexError:
494 pass
e64ac4a0 495
82025c0a
JR
496 if cmd is not None:
497 if self.conn.bytessent+len(cmd) > self.conn.recvq: # If it's too long
498 if len(cmd) > self.conn.recvq: # Is the command itself somehow over max length???
499 self._msgtimer.start()
500 raise ValueError('Somehow a command that was too long made it into the message queue. Uhoh!', cmd)
501 # Discard the message.
502 self.msgqueue.appendleft(cmd) # Phew, we've just sent too much recently. Put it (back) on the (primary) queue.
503 else:
504 self.conn.send(cmd)
505
506 self._msgtimer.start()
507
508 def _makemsgtimer(self):
509 self._msgtimer = MyTimer(3, self._popmsg)
a4eacae2 510
49a455aa 511 def join(self, chan):
512 self.conn.send("JOIN %s" % (chan))
a4eacae2 513
49a455aa 514 def part(self, chan):
515 self.conn.send("PART %s" % (chan))
a4eacae2 516
49a455aa 517 def quit(self, reason="Shutdown"):
518 self.conn.send("QUIT :%s" % (reason))
b25d4368 519
a9a00d34
JR
520 def maxmsglen(self):
521 return (
522 self.maxlen
523 - 63 # max hostname len
524 - 11 # max ident len
525 - 3 # the symbols in :nick!user@host
526 - len(self.nick)
527 )
528
a12f7519 529 def __str__(self): return self.nick
530 def __repr__(self): return "<Bot %r>" % (self.nick)
531
b25d4368 532class BotConnection(object):
a12f7519 533 def __init__(self, parent, bind, server, port):
b25d4368 534 self.parent = parent
49601f9d 535 self.buffer = bytearray()
b25d4368 536 self.socket = None
537
b25d4368 538 self.bind = bind
539 self.server = server
540 self.port = int(port)
b25d4368 541
7631844f 542 self.state = 0 # 0=disconnected, 1=registering, 2=connected
543
c6e6807f 544 self.bytessent = 0
a9a00d34 545 self.recvq = 510 # How much we can send per period
c6e6807f 546 self.exceeded = False
f2b6e85c 547 self._nowrite = False
c6e6807f 548
b25d4368 549 def connect(self):
ab9f6124 550 if self.parent.parent.cfg.getboolean('erebus', 'tls'):
4c10a7da
JR
551 import ssl
552 undersocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
553 context = ssl.create_default_context()
ab9f6124 554 self.socket = context.wrap_socket(undersocket, server_hostname=self.server)
4c10a7da
JR
555 else:
556 self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
68dff4aa
JR
557 self.socket.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) # Does Python make SOL_TCP portable? Who knows, it's not documented, and it appears to come from the _socket C lib.
558 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', 0, 0))
559 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
b25d4368 560 self.socket.bind((self.bind, 0))
39fd397a 561 self._write_oidentd()
b25d4368 562 self.socket.connect((self.server, self.port))
d1ea2946 563 return True
564 def register(self):
565 if self.state == 0:
a9c33762
JR
566 pss = self.parent.parent.cfg.get('erebus', 'pass')
567 if pss:
568 self.send("PASS %s" % (pss))
d1ea2946 569 self.send("NICK %s" % (self.parent.nick))
570 self.send("USER %s 0 * :%s" % (self.parent.user, self.parent.realname))
571 self.state = 1
49a455aa 572 return True
b25d4368 573
574 def registered(self, done=False):
39fd397a
JR
575 if done:
576 self.state = 2
577 self._unwrite_oidentd()
b25d4368 578 return self.state == 2
579
b25d4368 580 def send(self, line):
f2b6e85c 581 if not self._nowrite:
582 if self.parent.parent.cfg.getboolean('debug', 'io'):
583 self.parent.log('O', line)
584 self.bytessent += len(line)
585 try:
586 self._write(line)
587 except socket.error as e:
588 self._nowrite = True
589 self.parent._goterror(repr(e))
590 else:
591 if self.parent.parent.cfg.getboolean('debug', 'io'):
592 self.parent.log('X', line)
a4eacae2 593
7631844f 594 def _write(self, line):
c9c79ad5 595 self.socket.sendall(line.encode('utf-8', 'surrogateescape')+b"\r\n")
a4eacae2 596
b736b336
JR
597 def _getsockerr(self):
598 try: # SO_ERROR might not exist on all platforms
599 return self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR)
600 except:
601 return None
602
b25d4368 603 def read(self):
b8150e3d 604 recvd = self.socket.recv(8192)
b736b336
JR
605 if recvd == b"":
606 raise EOFError("socket.recv returned empty", self.parent.nick, self._getsockerr())
b8150e3d 607 self.buffer += recvd
b25d4368 608 lines = []
a4eacae2 609
a28e2ae9 610 while b"\r\n" in self.buffer:
611 pieces = self.buffer.split(b"\r\n", 1)
612 lines.append(pieces[0].decode('utf-8', 'backslashreplace'))
b25d4368 613 self.buffer = pieces[1]
a4eacae2 614
b25d4368 615 return lines
a12f7519 616
39fd397a
JR
617 def _format_oidentd(self):
618 ident = self.parent.user
619 fport = self.parent.port
620 from_ = self.bind
621 lport = self.socket.getsockname()[1]
622 if from_:
623 return 'fport %s from %s lport %s { reply "%s" }\n' % (fport, from_, lport, ident)
624 else:
625 return 'fport %s lport %s { reply "%s" }\n' % (fport, lport, ident)
626 def _write_oidentd(self):
627 path = self.parent.parent.cfg.get('erebus', 'oidentd_path')
628 if path is not None:
629 with open(path, 'a') as fh:
630 fcntl.lockf(fh, fcntl.LOCK_EX)
631 fh.write(self._format_oidentd())
632 fcntl.lockf(fh, fcntl.LOCK_UN)
633 def _unwrite_oidentd(self):
634 path = self.parent.parent.cfg.get('erebus', 'oidentd_path')
635 if path is not None:
636 with open(path, 'r+') as fh:
637 fcntl.lockf(fh, fcntl.LOCK_EX)
638 data = fh.read()
639 newdata = data.replace(self._format_oidentd(), '')
640 fh.seek(0)
641 fh.write(newdata)
642 fh.truncate()
643 fcntl.lockf(fh, fcntl.LOCK_UN)
644
28d06664 645 def __str__(self): return self.parent.nick
a12f7519 646 def __repr__(self): return "<BotConnection %r (%r)>" % (self.socket.fileno(), self.parent.nick)