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