#!/usr/bin/python
+# vim: fileencoding=utf-8
# Erebus IRC bot - Author: John Runyon
# "Bot" and "BotConnection" classes (handling a specific "arm")
-#TODO: error checking
+import os, random, socket, struct, sys, threading, time, traceback, fcntl
+from collections import deque
-import socket, sys
+if sys.version_info.major < 3:
+ timerbase = threading._Timer
+ stringbase = basestring
+else:
+ timerbase = threading.Timer
+ stringbase = str
+
+class MyTimer(timerbase):
+ def __init__(self, *args, **kwargs):
+ timerbase.__init__(self, *args, **kwargs)
+ self.daemon = True
+
+if sys.version_info.major < 3:
+ stringbase = basestring
+else:
+ stringbase = str
#bots = {'erebus': bot.Bot(nick='Erebus', user='erebus', bind='', server='irc.quakenet.org', port=6667, realname='Erebus')}
class Bot(object):
- def __init__(self, parent, nick, user, bind, server, port, realname):
+ def __init__(self, parent, nick, user, bind, authname, authpass, server, port, realname):
+ self.maxlen = 510
+
self.parent = parent
self.nick = nick
+ self.permnick = nick
self.user = user
self.realname = realname
- self.chans = []
+ self.authname = authname
+ self.authpass = authpass
- chcurs = self.parent.db.cursor()
- chcurs.execute("SELECT chname FROM chans WHERE bot = %s AND active = 1", (self.nick,))
- chans = chcurs.fetchall()
- chcurs.close()
+ self.connecttime = 0 # time at which we received numeric 001
+ self.server = server # the address we try to (re-)connect to
+ self.port = port
+ self.servername = server # the name of the server we got connected to
- for chrow in chans:
- uscurs = self.parent.db.cursor()
- uscurs.execute("SELECT user, level FROM chusers WHERE chan = %s", (chrow['chname'],))
- usrow = uscurs.fetchone()
- levels = {}
- while usrow is not None:
- levels[usrow['user']] = -usrow['level']
- usrow = uscurs.fetchone()
- uscurs.close()
- self.chans.append(self.parent.newchannel(self, chrow['chname'], levels))
+ curs = self.parent.query("SELECT chname FROM chans WHERE bot = %s AND active = 1", (self.permnick,))
+ if curs:
+ chansres = curs.fetchall()
+ curs.close()
+ self.chans = [self.parent.newchannel(self, row['chname']) for row in chansres]
+ else:
+ self.chans = []
self.conn = BotConnection(self, bind, server, port)
+
+ self.lastreceived = time.time() #time we last received a line from the server
+ self.watchdog()
+
+ self.msgqueue = deque()
+ self.slowmsgqueue = deque()
+ self._makemsgtimer()
+ self._msgtimer.start()
+ self.joined_chans = False
+
+ def __del__(self):
+ try:
+ curs = self.parent.query("UPDATE bots SET connected = 0 WHERE nick = %s", (self.permnick,))
+ curs.close()
+ except: pass
+
+ def watchdog(self):
+ if time.time() > int(self.parent.cfg.get('watchdog', 'maxtime', default=300))+self.lastreceived:
+ self.parse("ERROR :Fake-error from watchdog timer.")
+ return
+ if self.conn.registered():
+ self.conn.send("PING :%s" % (time.time()))
+ self._checknick()
+ watchdogtimer = MyTimer(int(self.parent.cfg.get('watchdog', 'interval', default=30)), self.watchdog)
+ watchdogtimer.start()
+
+ def log(self, *args, **kwargs):
+ self.parent.log(self.nick, *args, **kwargs)
+
def connect(self):
+ self.log('!', "Connecting")
if self.conn.connect():
+ self.log('!', "Connected")
self.parent.newfd(self, self.conn.socket.fileno())
def getdata(self):
- return self.conn.read()
+ try:
+ recvd = self.conn.read()
+ self.lastreceived = time.time()
+ return recvd
+ except EOFError as e:
+ return [":%s ERROR :%s%r" % (self.nick, e.__class__.__name__, e.args)]
+
+ def _checknick(self): # check if we're using the right nick, try changing
+ if self.nick != self.permnick and self.conn.registered():
+ self.conn.send("NICK %s" % (self.permnick))
def parse(self, line):
pieces = line.split()
- if not self.conn.registered() and pieces[0] == "NOTICE":
- self.conn.register()
-
- elif pieces[1] == "001":
- self.conn.registered(True)
+ if pieces[0][0] == ":":
+ numeric = pieces[1]
+ else:
+ numeric = pieces[0]
+
+ # dispatch dict
+ dispatch = {
+ 'NOTICE': self._gotconnected,
+ '001': self._got001,
+ '004': self._got004,
+ '376': self._gotRegistered,
+ '422': self._gotRegistered,
+ 'PRIVMSG': self._gotprivmsg,
+ '353': self._got353, #NAMES
+ '354': self._got354, #WHO
+ '396': self._gotHiddenHost, # hidden host has been set
+ '433': self._got433, #nick in use
+ '437': self._got433, #nick protected
+ 'JOIN': self._gotjoin,
+ 'PART': self._gotpart,
+ 'KICK': self._gotkick,
+ 'QUIT': self._gotquit,
+ 'NICK': self._gotnick,
+ 'MODE': self._gotmode,
+ 'PING': self._gotping,
+ 'ERROR': self._goterror,
+ }
+
+ if self.parent.hasnumhook(numeric):
+ hooks = self.parent.getnumhook(numeric)
+ for callback in hooks:
+ try:
+ callback(self, line)
+ except Exception:
+ self._cbexception("numhook", line)
+
+ if numeric in dispatch:
+ dispatch[numeric](pieces)
+
+ def _gotconnected(self, pieces):
+ if not self.conn.registered():
+ self.conn.register()
+ def _gotping(self, pieces):
+ self.conn.send("PONG %s" % (pieces[1]))
+ self._checknick()
+ def _goterror(self, pieces):
+ # TODO: better handling, just reconnect that single bot
+ error = ' '.join(pieces)
+ try:
+ raise Exception(error)
+ except Exception as e:
+ self.parent.mustquit = e
+ try:
+ self.quit("Error detected: %s" % (error))
+ except: pass
+ try:
+ curs = self.parent.query("UPDATE bots SET connected = 0")
+ curs.close()
+ except: pass
+ self.log('!', 'Bot exiting due to: %s' % (error))
+ def _got001(self, pieces):
+ # We wait until the end of MOTD instead to consider ourselves registered, but consider uptime as of 001
+ self.connecttime = time.time()
+ def _got004(self, pieces):
+ self.servername = pieces[3]
+ def _gotRegistered(self, pieces):
+ self.conn.registered(True)
+
+ curs = self.parent.query("UPDATE bots SET connected = 1 WHERE nick = %s", (self.permnick,))
+ if curs: curs.close()
+
+ self.conn.send("MODE %s +x" % (pieces[2]))
+ if self.authname is not None and self.authpass is not None:
+ self.conn.send(self.parent.cfg.get('erebus', 'auth_command', "AUTH %s %s") % (self.authname, self.authpass))
+ if not self.parent.cfg.getboolean('erebus', 'wait_for_hidden_host'):
for c in self.chans:
self.join(c.name)
-
- elif pieces[1] == "PRIVMSG":
- nick = pieces[0].split('!')[0][1:]
- user = self.parent.user(nick)
- target = pieces[2]
- msg = ' '.join(pieces[3:])[1:]
- self.parsemsg(user, target, msg)
-
- elif pieces[0] == "PING":
- self.conn.send("PONG %s" % (pieces[1]))
-
- elif pieces[1] == "354": # WHOX
- qt = pieces[3]
- nick = pieces[4]
- auth = pieces[5]
- if auth != '0':
- self.parent.user(nick).authed(auth)
-
- elif pieces[1] == "JOIN":
- nick = pieces[0].split('!')[0][1:]
- chan = self.parent.channel(pieces[2])
-
- if nick == self.nick:
- self.conn.send("WHO %s %%ant,1" % (chan))
+ self.joined_chans = True
+ def _gotHiddenHost(self, pieces):
+ if not self.joined_chans and self.parent.cfg.getboolean('erebus', 'wait_for_hidden_host'):
+ for c in self.chans:
+ self.join(c.name)
+ self.joined_chans = True
+ def _gotprivmsg(self, pieces):
+ nick = pieces[0].split('!')[0][1:]
+ user = self.parent.user(nick)
+ target = pieces[2]
+ msg = ' '.join(pieces[3:])[1:]
+ self.parsemsg(user, target, msg)
+ def _got353(self, pieces):
+ prefixes = {'@': 'op', '+': 'voice'}
+ chan = self.parent.channel(pieces[4])
+ names = pieces[5:]
+ names[0] = names[0][1:] #remove colon
+ for n in names:
+ if n[0] in prefixes:
+ user = self.parent.user(n[1:])
+ chan.userjoin(user, prefixes[n[0]])
else:
- user = self.parent.user(nick, justjoined=True)
+ user = self.parent.user(n)
chan.userjoin(user)
- user.join(chan)
-
+ user.join(chan)
+ def _got354(self, pieces):
+ qt = int(pieces[3])
+ if qt < 3:
+ nick, auth = pieces[4:6]
+ chan = None
+ else:
+ chan, nick, auth = pieces[4:7]
+ chan = self.parent.channel(chan)
+ user = self.parent.user(nick)
+ user.authed(auth)
+
+ if chan is not None:
+ user.join(chan)
+ chan.userjoin(user)
+
+ if qt == 2: # triggered by !auth
+ if user.isauthed():
+ if user.glevel > 0:
+ self.msg(nick, "You are now known as #%s (access level: %s)" % (auth, user.glevel))
+ else:
+ self.msg(nick, "You are now known as #%s (not staff)" % (auth))
+ else:
+ self.msg(nick, "I tried, but you're not authed!")
+ def _got433(self, pieces):
+ if not self.conn.registered(): #we're trying to connect
+ newnick = "%s%d" % (self.nick, random.randint(111, 999))
+ self.conn.send("NICK %s" % (newnick))
+ self.nick = newnick
+ def _gotjoin(self, pieces):
+ nick = pieces[0].split('!')[0][1:]
+ chan = self.parent.channel(pieces[2])
+
+ if nick == self.nick:
+ self.conn.send("WHO %s c%%cant,3" % (chan))
+ else:
+ user = self.parent.user(nick, send_who=True)
+ chan.userjoin(user)
+ user.join(chan)
+ def _clientLeft(self, nick, chan):
+ if nick == self.nick:
+ for u in chan.users:
+ if u.nick != self.nick:
+ self._clientLeft(u.nick, chan)
+ if chan.deleting:
+ chan.bot.chans.remove(chan)
+ del self.parent.chans[chan.name.lower()]
+ del chan
+ else:
+ user = self.parent.user(nick)
+ gone = user.part(chan)
+ chan.userpart(user)
+ if gone:
+ user.quit()
+ del self.parent.users[nick.lower()]
+ def _gotpart(self, pieces):
+ nick = pieces[0].split('!')[0][1:]
+ chan = self.parent.channel(pieces[2])
+ self._clientLeft(nick, chan)
+ def _gotkick(self, pieces):
+ nick = pieces[3]
+ chan = self.parent.channel(pieces[2])
+ self._clientLeft(nick, chan)
+ def _gotquit(self, pieces):
+ nick = pieces[0].split('!')[0][1:]
+ if nick != self.nick:
+ for chan in self.parent.user(nick).chans:
+ chan.userpart(self.parent.user(nick))
+ self.parent.user(nick).quit()
+ del self.parent.users[nick.lower()]
+ def _gotnick(self, pieces):
+ oldnick = pieces[0].split('!')[0][1:]
+ newnick = pieces[2][1:]
+ if oldnick == self.nick:
+ self.nick = newnick
+ else:
+ if newnick.lower() != oldnick.lower():
+ self.parent.users[newnick.lower()] = self.parent.users[oldnick.lower()]
+ del self.parent.users[oldnick.lower()]
+ self.parent.users[newnick.lower()].nickchange(newnick)
+ def _gotmode(self, pieces):
+ source = pieces[0].split('!')[0][1:]
+ chan = pieces[2]
+ if not chan.startswith("#"): return
+ chan = self.parent.channel(pieces[2])
+ mode = pieces[3]
+ args = pieces[4:]
+
+ adding = True
+ for c in mode:
+ if c == '+':
+ adding = True
+ elif c == '-':
+ adding = False
+ elif c == 'o':
+ if adding:
+ chan.userop(self.parent.user(args.pop(0)))
+ else:
+ chan.userdeop(self.parent.user(args.pop(0)))
+ elif c == 'v':
+ if adding:
+ chan.uservoice(self.parent.user(args.pop(0)))
+ else:
+ chan.userdevoice(self.parent.user(args.pop(0)))
+ else:
+ pass # don't care about other modes
+
+ def _cbexception(self, source, *args, chained=False, **kwargs):
+ if not chained: # skip hooks if we were caused by a hook
+ exc = sys.exception()
+ if self.parent.hasexceptionhook(exc):
+ for callback in self.parent.getexceptionhook(exc):
+ try:
+ callback(self, exc, source, *args, **kwargs)
+ except Exception:
+ self._cbexception('exceptionhook', chained=True, module=callback.__module__, function=callback.__name__, underlying=(source, args, kwargs))
+ if self.parent.cfg.getboolean('debug', 'cbexc'):
+ self.conn.send("PRIVMSG %s :%09.3f \ 34\1f!!! CBEXC\1f\ 3 %s" % (self.parent.cfg.get('debug', 'owner'), time.time() % 100000, source))
+ traceback.print_exc(chain=not chained)
+ self.log('!', "CBEXC %s %r %r" % (source, args, kwargs))
+
+
def parsemsg(self, user, target, msg):
+ if user.glevel <= -2: return # short circuit if user is IGNORED
chan = None
- triggerused = msg[0] == self.parent.trigger
- if triggerused: msg = msg[1:]
+ chanparam = None # was the channel specified as part of the command?
+ if len(msg) == 0:
+ return
+
+ if target == self.nick and msg.startswith("\001"): #ctcp
+ msg = msg.strip("\001")
+ if msg:
+ pieces = msg.split()
+ if pieces[0] == "CLIENTINFO":
+ self.msg(user, "\001CLIENTINFO VERSION PING\001")
+ elif pieces[0] == "VERSION":
+ self.msg(user, "\001VERSION Erebus v%d.%d - http://jfr.im/git/erebus.git\001" % (self.parent.APIVERSION, self.parent.RELEASE))
+ elif pieces[0] == "PING":
+ if len(pieces) > 1:
+ self.msg(user, "\001PING %s\001" % (' '.join(pieces[1:])))
+ else:
+ self.msg(user, "\001PING\001")
+ return
+
+ triggerused = msg.startswith(self.parent.trigger)
+ if triggerused: msg = msg[len(self.parent.trigger):]
pieces = msg.split()
- if target == self.nick:
- if len(pieces) > 1:
- chanword = pieces[1]
- if chanword[0] == '#':
- chan = self.parent.channel(chanword)
- pieces.pop(1)
+ if len(pieces) == 0:
+ return
- else: # message was sent to a channel
- chan = self.parent.channel(target) #TODO check if bot's on channel --- in Erebus.channel() maybe?
- if msg[0] == '*': # message may be addressed to bot by "*BOTNICK" trigger?
- if pieces[0][1:].lower() == self.nick.lower():
+ if target != self.nick: # message was sent to a channel
+ try:
+ if pieces[0][:-1].lower() == self.nick.lower() and (pieces[0][-1] == ":" or pieces[0][-1] == ","):
pieces.pop(0) # command actually starts with next word
- msg = ' '.join(pieces) # command actually starts with next word
- elif not triggerused:
+ if len(pieces) == 0: # is there still anything left?
+ return
+ msg = ' '.join(pieces)
+ triggerused = True
+ except IndexError:
+ return # "message" is empty
+
+ if len(pieces) > 1:
+ chanword = pieces[1]
+ if chanword.startswith('#'):
+ chanparam = self.parent.channel(chanword)
+
+ if target != self.nick: # message was sent to a channel
+ chan = self.parent.channel(target)
+ if not triggerused:
+ if self.parent.haschanhook(target.lower()):
+ for callback in self.parent.getchanhook(target.lower()):
+ try:
+ cbret = callback(self, user, chan, *pieces)
+ if isinstance(cbret, stringbase):
+ self.reply(target, user, cbret)
+ except:
+ self.msg(user, "Command failed. Code: CBEXC%09.3f" % (time.time() % 100000))
+ self._cbexception("chanhook", user=user, target=target, msg=msg)
return # not to bot, don't process!
cmd = pieces[0].lower()
-
+ rancmd = False
if self.parent.hashook(cmd):
- callback = self.parent.gethook(cmd)
- if chan is None and callback.needchan:
- self.msg(user, "You need to specify a channel for that command.")
- return
- if user.glevel >= callback.reqglevel:
- #TODO TODO TODO check reqclevel
- cbret = callback(self, user, chan, target, *pieces[1:])
- if cbret is NotImplemented:
- self.msg(user, "Command not implemented.")
- return
-
- self.msg(user, "No such command, or you don't have access.")
-
- def msg(self, target, msg):
- if isinstance(target, self.parent.User): self.conn.send("NOTICE %s :%s" % (target.nick, msg))
- elif isinstance(target, self.parent.Channel): self.conn.send("PRIVMSG %s :%s" % (target.name, msg))
- elif isinstance(target, basestring):
- if target[0] == '#': self.conn.send("PRIVMSG %s :%s" % (target, msg))
- else: self.conn.send("NOTICE %s :%s" % (target, msg))
- else: raise TypeError('Bot.msg() "target" must be Erebus.User, Erebus.Channel, or string')
+ for callback in self.parent.gethook(cmd):
+ if chanparam is not None and (callback.needchan or callback.wantchan):
+ chan = chanparam
+ pieces.pop(1)
+ if chan is None and callback.needchan:
+ rancmd = True
+ self.msg(user, "You need to specify a channel for that command.")
+ elif user.glevel >= callback.reqglevel and (not callback.needchan or chan.levelof(user.auth) >= callback.reqclevel):
+ rancmd = True
+ try:
+ cbret = callback(self, user, chan, target, *pieces[1:])
+ if isinstance(cbret, stringbase):
+ self.reply(target, user, cbret)
+ except Exception:
+ self.msg(user, "Command failed. Code: CBEXC%09.3f" % (time.time() % 100000))
+ self._cbexception("hook", user=user, target=target, msg=msg)
+ except SystemExit as e:
+ self.parent.mustquit = e
+ try:
+ curs = self.parent.query("UPDATE bots SET connected = 0")
+ curs.close()
+ except: pass
+ raise e
+ else:
+ rancmd = True
+ self.msg(user, "I don't know that command.")
+ if not rancmd:
+ self.msg(user, "You don't have enough access to run that command.")
+
+ def __debug_nomsg(self, target, msg):
+ if self.parent.cfg.getboolean('debug', 'nomsg'):
+ 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))
+ self.log('!', "!!! NOMSG")
+# print "%09.3f %s [!] %s" % (time.time() % 100000, self.nick, "!!! NOMSG")
+ traceback.print_stack()
+
+
+ def reply(self, chan, user, msg):
+ if chan is not None and (isinstance(chan, self.parent.Channel) or (isinstance(chan, stringbase) and chan[0] == "#")):
+ self.msg(chan, "%s: %s" % (user, msg))
+ else:
+ self.msg(user, msg)
+
+ """
+ Does the work for msg/slowmsg/fastmsg. Uses the append_callback to append to the correct queue.
+
+ 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.
+ """
+ def _msg(self, target, msg, truncate, append_callback, msgtype):
+ if self.parent.cfg.getboolean('erebus', 'nofakelag'): append_callback = self.conn.send
+
+ cmd = self._formatmsg(target, msg, msgtype)
+ # The max length is much shorter than conn.maxlen (510) because of the length the server adds on about the source (us).
+ # If you know your hostmask, you can of course figure the exact length, but it's very difficult to reliably know your hostmask.
+ maxlen = self.maxmsglen()
+ if len(cmd) > maxlen:
+ if not truncate:
+ return False
+ else:
+ cmd = cmd[:maxlen]
+
+ if self.conn.exceeded or self.conn.bytessent+len(cmd) >= self.conn.recvq:
+ append_callback(cmd)
+ else:
+ self.conn.send(cmd)
+
+ self.conn.exceeded = True
+ return True
+
+ def msg(self, target, msg, truncate=False, *, msgtype=None):
+ """msgtype must be a valid IRC command, i.e. NOTICE or PRIVMSG; or leave as None to use default"""
+ return self._msg(target, msg, truncate, self.msgqueue.append, msgtype)
+
+ def slowmsg(self, target, msg, truncate=False, *, msgtype=None):
+ return self._msg(target, msg, truncate, self.slowmsgqueue.append, msgtype)
+
+ def fastmsg(self, target, msg, truncate=False, *, msgtype=None):
+ return self._msg(target, msg, truncate, self.conn.send, msgtype)
+
+ def _formatmsg(self, target, msg, msgtype):
+ if target is None or msg is None:
+ return self.__debug_nomsg(target, msg)
+
+ target = str(target)
+
+ if msgtype is not None: command = "%s %s :%s" % (msgtype, target, msg)
+ elif target.startswith('#'): command = "PRIVMSG %s :%s" % (target, msg)
+ else: command = "NOTICE %s :%s" % (target, msg)
+
+ return command
+
+ def _popmsg(self):
+ self._makemsgtimer()
+ self.conn.bytessent -= self.conn.recvq/3
+ if self.conn.bytessent < 0: self.conn.bytessent = 0
+ self.conn.exceeded = True
+
+ cmd = None
+ try:
+ cmd = self.msgqueue.popleft()
+ except IndexError:
+ try:
+ cmd = self.slowmsgqueue.popleft()
+ except IndexError:
+ pass
+
+ if cmd is not None:
+ if self.conn.bytessent+len(cmd) > self.conn.recvq: # If it's too long
+ if len(cmd) > self.conn.recvq: # Is the command itself somehow over max length???
+ self._msgtimer.start()
+ raise ValueError('Somehow a command that was too long made it into the message queue. Uhoh!', cmd)
+ # Discard the message.
+ self.msgqueue.appendleft(cmd) # Phew, we've just sent too much recently. Put it (back) on the (primary) queue.
+ else:
+ self.conn.send(cmd)
+
+ self._msgtimer.start()
+
+ def _makemsgtimer(self):
+ self._msgtimer = MyTimer(3, self._popmsg)
def join(self, chan):
self.conn.send("JOIN %s" % (chan))
def quit(self, reason="Shutdown"):
self.conn.send("QUIT :%s" % (reason))
+ def maxmsglen(self):
+ return (
+ self.maxlen
+ - 63 # max hostname len
+ - 11 # max ident len
+ - 3 # the symbols in :nick!user@host
+ - len(self.nick)
+ )
+
def __str__(self): return self.nick
def __repr__(self): return "<Bot %r>" % (self.nick)
class BotConnection(object):
- state = 0 # 0=disconnected, 1=registering, 2=connected
-
def __init__(self, parent, bind, server, port):
self.parent = parent
- self.buffer = ''
+ self.buffer = bytearray()
self.socket = None
self.bind = bind
self.server = server
self.port = int(port)
+ self.state = 0 # 0=disconnected, 1=registering, 2=connected
+
+ self.bytessent = 0
+ self.recvq = 510 # How much we can send per period
+ self.exceeded = False
+ self._nowrite = False
+
def connect(self):
- self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ if self.parent.parent.cfg.getboolean('erebus', 'tls'):
+ import ssl
+ undersocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ context = ssl.create_default_context()
+ self.socket = context.wrap_socket(undersocket, server_hostname=self.server)
+ else:
+ self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ 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.
+ self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', 0, 0))
+ self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
self.socket.bind((self.bind, 0))
+ self._write_oidentd()
self.socket.connect((self.server, self.port))
return True
def register(self):
if self.state == 0:
+ pss = self.parent.parent.cfg.get('erebus', 'pass')
+ if pss:
+ self.send("PASS %s" % (pss))
self.send("NICK %s" % (self.parent.nick))
self.send("USER %s 0 * :%s" % (self.parent.user, self.parent.realname))
self.state = 1
return True
def registered(self, done=False):
- if done: self.state = 2
+ if done:
+ self.state = 2
+ self._unwrite_oidentd()
return self.state == 2
- #TODO: rewrite send() to queue
def send(self, line):
- print self.parent.nick, '[O]', line
- self.write(line)
-
- def write(self, line):
- self.socket.sendall(line+"\r\n")
+ if not self._nowrite:
+ if self.parent.parent.cfg.getboolean('debug', 'io'):
+ self.parent.log('O', line)
+ self.bytessent += len(line)
+ try:
+ self._write(line)
+ except socket.error as e:
+ self._nowrite = True
+ self.parent._goterror(repr(e))
+ else:
+ if self.parent.parent.cfg.getboolean('debug', 'io'):
+ self.parent.log('X', line)
+
+ def _write(self, line):
+ self.socket.sendall(line.encode('utf-8', 'surrogateescape')+b"\r\n")
+
+ def _getsockerr(self):
+ try: # SO_ERROR might not exist on all platforms
+ return self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR)
+ except:
+ return None
def read(self):
- self.buffer += self.socket.recv(8192)
+ recvd = self.socket.recv(8192)
+ if recvd == b"":
+ raise EOFError("socket.recv returned empty", self.parent.nick, self._getsockerr())
+ self.buffer += recvd
lines = []
- while '\r\n' in self.buffer:
- pieces = self.buffer.split('\r\n', 1)
- print self.parent.nick, '[I]', pieces[0]
- lines.append(pieces[0])
+ while b"\r\n" in self.buffer:
+ pieces = self.buffer.split(b"\r\n", 1)
+ lines.append(pieces[0].decode('utf-8', 'backslashreplace'))
self.buffer = pieces[1]
return lines
- def __str__(self): return self.nick
+ def _format_oidentd(self):
+ ident = self.parent.user
+ fport = self.parent.port
+ from_ = self.bind
+ lport = self.socket.getsockname()[1]
+ if from_:
+ return 'fport %s from %s lport %s { reply "%s" }\n' % (fport, from_, lport, ident)
+ else:
+ return 'fport %s lport %s { reply "%s" }\n' % (fport, lport, ident)
+ def _write_oidentd(self):
+ path = self.parent.parent.cfg.get('erebus', 'oidentd_path')
+ if path is not None:
+ with open(path, 'a') as fh:
+ fcntl.lockf(fh, fcntl.LOCK_EX)
+ fh.write(self._format_oidentd())
+ fcntl.lockf(fh, fcntl.LOCK_UN)
+ def _unwrite_oidentd(self):
+ path = self.parent.parent.cfg.get('erebus', 'oidentd_path')
+ if path is not None:
+ with open(path, 'r+') as fh:
+ fcntl.lockf(fh, fcntl.LOCK_EX)
+ data = fh.read()
+ newdata = data.replace(self._format_oidentd(), '')
+ fh.seek(0)
+ fh.write(newdata)
+ fh.truncate()
+ fcntl.lockf(fh, fcntl.LOCK_UN)
+
+ def __str__(self): return self.parent.nick
def __repr__(self): return "<BotConnection %r (%r)>" % (self.socket.fileno(), self.parent.nick)