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