コード例 #1
0
ファイル: communication.py プロジェクト: opcm/aca
    class v3(Base):
        MAX_CORE_CHANNELS = 16

        def _init_channels(self):
            self.control_channel = Channel(index=0, log=self._log)

            self.cpu_data_channels = ChannelList(length=self.MAX_CORE_CHANNELS,
                                                 log=self._log)
            self.module_data_channel = Channel(index=self.MAX_CORE_CHANNELS,
                                               log=self._log)
            self.uncore_data_channel = Channel(index=self.MAX_CORE_CHANNELS +
                                               1,
                                               log=self._log)

            self.data_channels = ChannelList(length=self.MAX_CORE_CHANNELS + 2,
                                             log=self._log)
            self.data_channels.includes(*self.cpu_data_channels.reserved)
            self.data_channels.includes(self.module_data_channel)
            self.data_channels.includes(self.uncore_data_channel)

        def close(self):
            self._log.debug('COMMUNICATION CHANNELS - Closing')
            for channel in self.data_channels:
                channel.close()
            self.control_channel.close()
            self._init_channels()
コード例 #2
0
ファイル: communication.py プロジェクト: opcm/aca
        def _init_channels(self):
            self.control_channel = Channel(index=0, log=self._log)

            self.cpu_data_channels = ChannelList(length=self.MAX_CORE_CHANNELS,
                                                 log=self._log)

            self.module_data_channel = None
            self.uncore_data_channel = None

            self.data_channels = ChannelList(length=self.MAX_CORE_CHANNELS + 2,
                                             log=self._log)
            self.data_channels.includes(*self.cpu_data_channels.reserved)
コード例 #3
0
        def parse(html):
            CSS = "select[name=channels] optgroup[label!=Alphabetisch] option"
            channels = ChannelList()

            for elem in html.cssselect(CSS):
                values = elem.attrib["value"].split("]")[1].split(",")
                texts = elem.text.split(",")
                assert len(values) == len(texts)

                for i in xrange(len(values)):
                    channels.append(
                        Channel(values[i].strip(), texts[i].strip()))

            return channels
コード例 #4
0
    def __init__(self,
                 nick,
                 realname,
                 channels,
                 admins,
                 _ssl=False,
                 reconnect=False,
                 password=False,
                 oper_user=False,
                 oper_pass=False):
        self.users = UserList()
        self.user = User(nick=nick, real=realname)
        self.users.append(self.user)
        self.channels = ChannelList(self.users)

        self._nick = nick
        self._real = realname

        self.logger = logging.getLogger('teslabot.irc')
        self._init_channels = channels
        self._ssl = _ssl
        self._reconnect = reconnect
        self._reconnect_time = 0
        self._password = password
        self._buffer = ''
        self._ipaddr = None

        self._oper = False
        self._oper_user = oper_user
        self._oper_pass = oper_pass

        self._msg_time = []
        self._msg_last_id = 0
        self._max_mps = 5
        self._throttle = False

        self._ping = 0
        self._stimeout_count = 0
        self._SOCKET_TIMEOUT = 5
        self._PING_TIMEOUT = 5
        self._PING_FREQUENCY = 12

        self._last_msg = 0

        self.alive = 1

        # Define users with administrator privileges
        for admin in admins:
            user = User(src=admin, admin=True)
            self.users.append(user)
コード例 #5
0
ファイル: irc.py プロジェクト: Anachronos/teslabot
    def __init__(self, nick, realname, channels, admins, _ssl = False, reconnect = False,
                  password = False, oper_user = False, oper_pass = False):
        self.users = UserList()
        self.user = User(nick=nick, real=realname)
        self.users.append(self.user)
        self.channels = ChannelList(self.users)

        self._nick = nick
        self._real = realname
        
        self.logger = logging.getLogger('teslabot.irc')
        self._init_channels = channels
        self._ssl = _ssl
        self._reconnect = reconnect
        self._reconnect_time = 0
        self._password = password
        self._buffer = ''
        self._ipaddr = None
        
        self._oper = False
        self._oper_user = oper_user
        self._oper_pass = oper_pass
        
        self._msg_time = []
        self._msg_last_id = 0
        self._max_mps = 5
        self._throttle = False

        self._ping = 0
        self._stimeout_count = 0
        self._SOCKET_TIMEOUT = 5
        self._PING_TIMEOUT = 5
        self._PING_FREQUENCY = 12
        
        self._last_msg = 0

        self.alive = 1
        
        # Define users with administrator privileges
        for admin in admins:
            user = User(src=admin, admin=True)
            self.users.append(user)
コード例 #6
0
ファイル: irc.py プロジェクト: Anachronos/teslabot
class IRC(object):
    """
    IRC protocol interface.
    
    Attributes:
        user: A User object to store the bot's user details
        logger: A Logger object
        channels: A ChannelList object
        admins: A list of User objects
        ssl: A boolean to enable or disable SSL wrapper
        
        _init_channels: A list of channels that is joined when the client is connected
        _reconnect: Whether or not to reconnect when socket connection is lost
        _password: The connection password (if any)
        _buffer: A socket buffer string
        
        _oper: Boolean indicating whether or not the server accepts the client as an IRCOP
        _oper_user: A username string argument for the OPER command
        _oper_pass: A password string argument for the OPER command
        
        _ping: UNIX time of last ping request
        _stimeout_count: A counter to keep track of the number of timeouts
        _SOCKET_TIMEOUT: The number of seconds before the socket times out
        _PING_TIMEOUT: The number of seconds after which a PING message is considered timed out
        _PING_FREQUENCY: The number of seconds (in multiples of _SOCKET_TIMEOUT) that must elapse
            before a PING message is sent to the server.
        
        _last_msg: UNIX time of latest received message
    """
    def __init__(self, nick, realname, channels, admins, _ssl = False, reconnect = False,
                  password = False, oper_user = False, oper_pass = False):
        self.users = UserList()
        self.user = User(nick=nick, real=realname)
        self.users.append(self.user)
        self.channels = ChannelList(self.users)

        self._nick = nick
        self._real = realname
        
        self.logger = logging.getLogger('teslabot.irc')
        self._init_channels = channels
        self._ssl = _ssl
        self._reconnect = reconnect
        self._reconnect_time = 0
        self._password = password
        self._buffer = ''
        self._ipaddr = None
        
        self._oper = False
        self._oper_user = oper_user
        self._oper_pass = oper_pass
        
        self._msg_time = []
        self._msg_last_id = 0
        self._max_mps = 5
        self._throttle = False

        self._ping = 0
        self._stimeout_count = 0
        self._SOCKET_TIMEOUT = 5
        self._PING_TIMEOUT = 5
        self._PING_FREQUENCY = 12
        
        self._last_msg = 0

        self.alive = 1
        
        # Define users with administrator privileges
        for admin in admins:
            user = User(src=admin, admin=True)
            self.users.append(user)

    def run(self):
        """
        Keeps the the connection alive by continuously calling _recv().
        It will attempt to reconnect if connection is lost and reconnect is enabled.
        """
        while self.alive:
            try:
                self._recv()
            except socket.error as e:
                self.reconnect()

    def _get_headers(self):
        return ':' + self.user.nick + '!' + self.user.real + '@' + self.user.host

    def join(self, chan):
        """Accepts channel name string."""
        self.send('JOIN :{0}'.format(chan))
        
    @property
    def nick(self):
        return self.user.nick

    @nick.setter
    def nick(self, value):
        self.send('NICK {0}'.format(value))
        self.user.nick = value
        
    def notice(self, msg, nick):
        """Accepts a string or a list of messages."""
        if type(msg) == list:
            for line in msg:
                if len(line) > 0:
                    self.send(u'NOTICE {0} :{1}'.format(nick, line))
                    self.logger.info('>{0}< {1}'.format(nick, line))
        else:
            msg = msg.split('\r\n')
            for line in msg:
                maxlen = 512 - len(self._get_headers()) - len(nick) - 12
                
                if len(line) < maxlen:
                    self.send('NOTICE {0} :{1}'.format(nick, line))
                    self.logger.info('>{0}< {1}'.format(nick, line))
                else:
                    self.send('NOTICE {0} :{1}'.format(nick, line[:maxlen-1]))
                    self.logger.info('>{0}< {1}'.format(nick, line[:maxlen-1]))
                    self.notice(line, line[maxlen-1:])
            
    def mode(self, target, modes = False, args = False):
        """Sends a MODE command.
        Args:
            target: A channel or nick string
            modes: A string of flags
            args: An argument string
        """
        if not modes:
            self.send('MODE {0}'.format(target))
        elif modes and not args:
            self.send('MODE {0} {1}'.format(target, modes))
        elif modes and args:
            self.send('MODE {0} {1} {2}'.format(target, modes, args))
            
    def kick(self, nick, chan, reason = ''):
        self.send(u'KICK {0} {1} :{2}'.format(chan, nick, reason))
            
    def _get_avg_mps(self):
        """Returns the average rate of sent messages per second."""
        x1 = self._msg_last_id - self._max_mps
        t1 = self._msg_time[self._msg_last_id - self._max_mps]
        x2 = self._msg_last_id
        t2 = self._msg_time[self._msg_last_id]
        
        avg_mps = (x2 - x1) / (t2 - t1)
        
        return avg_mps
            
    def _is_throttling(self):
        """Returns true if the rate of messages per second has exceeded the limit.
        
        Formula:
            (x2 - x1)/(t2 - t1) = mps (average messages per second)
            where (x1, t1) and (x2, t2) represent amount of x messages covered in y seconds.
            
            The distance between (x1, t1) and (x2, t2) is determined by _max_mps (5 by default).
            
            If the average mps exceeds _max_mps, throttling occurs. When throttling, messages are
            sent every 1 second. When the average mps is equal to or less than 1, throttling stops.
            
        Data structure:
            _msg_time: a list of UNIX time values, whose index represents the number of messages.
            Although in practice, the difference between each index is the only relevant fact.
            As a result, we "pop" values from the beginning of the list that are no longer needed.
        """
        self._msg_time.append(time.time())
        throttle = False
        
        if self._throttle:    
            avg_mps = self._get_avg_mps()
            
            if self._msg_last_id % self._max_mps == 0:
                # In order to prevent the list from growing big, we drop values we no longer need.
                self._msg_time.pop(0)
                self._msg_last_id -= 1
            
            if avg_mps <= 1:
                self.logger.warning('Throttling disabled.')
                self._throttle = False
            else:
                self.logger.warning('Throttling.')
                throttle = True
        
        elif len(self._msg_time) > self._max_mps:
            avg_mps = self._get_avg_mps()
            
            # In order to prevent the list from growing large, we drop values we no longer need.
            if self._msg_last_id % self._max_mps == 0:
                self._msg_time.pop(0)
                self._msg_last_id -= 1
            
            if avg_mps >= self._max_mps:
                self.logger.warning('Throttling enabled.')
                self._throttle = throttle = True
            else:
                throttle = False
                
        self._msg_last_id += 1
        return throttle

    def send(self, msg, silent = False):
        """msg should be 512 bytes or less"""
        
        if self._is_throttling():
            time.sleep(1)
            
        # Encode to bytes -- assuming it's a utf-8 string
        msg = msg.encode('utf-8')
        
        self.sock.send(msg + '\r\n')
        if not silent:
            self.logger.debug('{0}'.format(msg))

    def leave(self, chan, reason = 'Leaving'):
        self.send('PART {0} :{1}'.format(chan, reason))

    def say(self, msg, dst):
        msg = msg.split('\r\n')

        for line in msg:
            maxlen = 512 - len(self._get_headers()) - len(dst) - 12
            
            if len(line) < maxlen:
                self.send(u'PRIVMSG {0} :{1}'.format(dst, line))
                self.logger.info(u'[{0}] <{1}> {2}'.format(dst, self.user.nick, line))
            else:
                self.send(u'PRIVMSG ' + dst + ' :' + line[:maxlen-1])
                self.logger.info(u'[{0}] <{1}> {2}'.format(dst, self.user.nick, line[:maxlen-1]))
                self.say(line[maxlen-1:], dst)
                
    def names(self, chan):
        self.send('NAMES {0}'.format(chan))

    def action(self, msg, dst):
        self.send('PRIVMSG ' + dst + ' :\x01ACTION ' + msg + '\x01')

    def whois(self, nick):
        self.send('WHOIS {0}'.format(nick))
        
    def oper(self, username = False, password = False):
        if not username:
            username = self._oper_user
        if not password:
            password = self._oper_pass
        self.send('OPER {0} {1}'.format(username, password), True)

    def is_chan(self, chan):
        """Returns true if a given string is a valid channel name."""
        if chan[:1] in ('&', '#', '+', '!'):
            return True
        return False

    def _on_privmsg(self, user, args):
        """Calls the appropriate handler for a given PRIVMSG type.
        Either channel, private, or CTCP message.
        
        Args:
            user: A User class instance
            args: A string of arguments
        """
        dst, msg  = args.split(' ', 1)
        
        # CTCP query/reply
        if msg[1] == '\x01' and msg[-1] == '\x01':
            ctcp_msg = msg[2:-1]
            if len(ctcp_msg.split()) > 1:
                cmd, subargs = ctcp_msg.split(' ', 1)
            else:
                cmd = ctcp_msg
                subargs = None

            self.on_ctcp(user, cmd, subargs)
        
        # Channel message
        if self.is_chan(dst[:1]):
            self.on_channel_message(user, self.channels.get(dst), msg[1:])

        # Private query
        elif dst.lower() == self.user.nick.lower():
            self.on_private_message(user, msg[1:])

        else:
            self.logger.warning("Unrecognized PRIVMSG format.")

    def quit(self, msg = 'Quitting', force = None):
        try:
            if force:
                sys.exit()
            
            self.send('QUIT :{0}'.format(msg))
            self.sock.close()
            self.alive = 0
        finally:
            self.logger.info('Disconnected from [{0}].'.format(self.user.host))

    def ping(self):
        self.send('PING {0}'.format(self._host))
        self._ping = time.time()

    def _set_hostname(self, hostname):
        self.user.host = hostname
        
    def _recv_timeout(self):
        self._stimeout_count += 1
        if self._stimeout_count >= self._PING_FREQUENCY and not self._ping:
            self._stimeout_count = 0
            self.ping()
        elif self._ping:
            diff = time.time() - self._ping
            if diff >= self._PING_TIMEOUT:
                self.logger.info('Disconnected due to timeout.')
                self.reconnect()
                
    def _recv(self, timeout = False):
        """Processes messages received from the IRC server and calls the
        appropriate handlers."""
        buffer = ''
        self._last_msg = time.time()
        
        try:
            buffer = self._buffer + self.sock.recv(512)
        except (socket.error, ssl.SSLError) as e:
            if e.message == 'The read operation timed out'\
                    or e.message == 'timed out':
               self._recv_timeout()
               return
        except socket.timeout:
            self._recv_timeout()
            return
        
        self._buffer = ''

        # Server has closed the socket connection
        if len(buffer) == 0:
            self.logger.debug('Server has closed socket connection.')
            raise socket.error
        
        data = buffer.split('\r\n')
        
        # If not empty, this is part of a new message. Add it to the buffer.
        self._buffer += data.pop()

        # Server didn't send CRLF
        if not len(data):
            return

        # Remove empty strings due to an \r\n in the beginning 
        if data[0] == '':
            data.pop(0)

        for msg in data:
            self.logger.debug('{0}'.format(msg))
            # Assuming it's UTF-8 encoded. If not, ignore errors.
            self._parse_message(msg.decode('utf-8', 'ignore'))

    def _parse_message(self, msg):
        """Parses a given IRC message."""
        if msg[:4] == 'PING':
            self.send('PONG {0}'.format(msg[5:]))
            return

        elif msg[:5] == 'ERROR':
            self.quit(force=True)
        
        src, cmd, args = msg.split(' ', 2)
        user = self.users.get(src[1:])
        
        if cmd == 'PONG' and self._ping:
            diff = time.time() - self._ping
            self._ping = 0
            self.logger.debug('PONG acknowledged ({0}s).'.format(diff))

        elif cmd == 'PRIVMSG':
            self._on_privmsg(user, args)

        elif cmd == 'MODE':
            args = args.split(' ', 2)
            subargs = False

            if self.is_chan(args[0]):
                if len(args) == 3:
                    subargs = args.pop().split()

                chan, modes = args
                channel = self.channels.get(chan)
                self.on_channel_mode(user, channel, modes, subargs)
            
        elif cmd == 'NOTICE':
            self.on_notice(user, args)

        elif cmd == 'TOPIC':
            chan = args.split(' ', 1)[0]
            topic = args.split(' ', 1)[1][1:]

            self.on_channel_topic(user, self.channels[chan], topic)

        elif cmd == 'JOIN':
            if args[0] != ':':
                chan = args
            else:
                chan = args[1:]
            channel = self.channels.add(chan)
            self.on_channel_join(user, channel)
            
        elif cmd == 'KICK':
            chan, target, reason = args.split(' ', 2)
            channel = self.channels[chan]
            target = self.users.get(target)
            
            self.on_channel_kick(user, channel, target, reason[1:])
            
        elif cmd == 'NICK':
            self.users.get(user=user).nick = args

        elif cmd == 'PART':
            subargs = args.split()
            
            if len(subargs) == 2:
                chan, reason = subargs
                self.on_channel_part(user, self.channels[chan], reason[1:])
            else:
                chan = args.split()[0]
                self.on_channel_part(user, self.channels[chan])

        elif cmd == RPL_WELCOME:
            self.on_connect()
            
        elif cmd == RPL_YOUREOPER:
            self._oper = True

        elif cmd == RPL_HOSTHIDDEN:
            self._set_hostname(args)

        elif cmd in (RPL_WHOISUSER, RPL_WHOISIDLE, RPL_WHOISACTUALLY,
            RPL_WHOISHOST):
            self._on_whois(cmd, args)

        elif cmd == ERR_NICKNAMEINUSE:
            self.on_nickinuse()
        
        elif cmd in (RPL_TOPIC, RPL_TOPICWHOTIME, RPL_NAMREPLY):
            self.channels.on_reply(cmd, args)
            
        elif cmd in (RPL_MOTDSTART, RPL_MOTD, RPL_ENDOFMOTD):
            self.on_motd(args)
            
    def on_motd(self, msg):
        if not __debug__:
            self.logger.info(msg.split(':', 1)[1])
            
    def _on_whois(self, cmd, args):
        if cmd == RPL_WHOISUSER:
            if args.split(' ')[1] == self.user.nick:   # Self-WHOIS
                self._set_hostname(args.split(' ')[3])    # Get the hostname
                self.logger.debug('Setting hostname to [{0}].'.format(self.user.host))
            else:
                pass
        elif cmd == RPL_WHOISIDLE:
            args = args.split()
            
            user = self.users.get(args[1])
            user.idle = int(args[2])
            user.signon = int(args[3])

        elif cmd == RPL_WHOISACTUALLY:
            self._ipaddr = args.split()[-1][1:-1]

        elif cmd == RPL_WHOISHOST:
            self._ipaddr = args.split()[-1]

    def ipaddr(self):
        return self._ipaddr

    def connect(self, host, port):
        """Attempts to establish a socket connection with a given IRC server."""
        self._host = host
        self._port = port
        
        try:
            self.logger.info('Connecting to {0}:{1}.'.format(host, port))
            
            self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            if self._ssl:
                self.sock = ssl.wrap_socket(self.sock, cert_reqs=ssl.CERT_NONE)
            self.sock.settimeout(self._SOCKET_TIMEOUT)
            self.sock.connect((host, port))
            self.alive = 1
            
            if self._password:
                self.send('PASS {0}'.format(self._password))
        
            self.send('NICK {0}'.format(self._nick))
            self.send('USER {0} 0 * :{0}'.format(self._real))

        except socket.error as e:
            self.logger.critical('Failed to connect to {0}:{1}.'.format(host, port))
            
    def reconnect(self):
        if self._reconnect:
            # Wait 15 seconds before reconnecting again
            tdiff = time.time() - self._reconnect_time
            if self._reconnect_time and tdiff < 15:
                self.logger.info('Attempting to reconnect in 15 seconds...')
                time.sleep(15)
                
            self.logger.debug('Reconnecting.')
            self.connect(self._host, self._port)
            self._reconnect_time = time.time()
        else:
            self.logger.info('Reconnection disabled.')
            
    def on_connect(self):
        self.whois(self.user.nick)
        if self._oper_user and self._oper_pass:
            self.oper()
            
        for channel in self._init_channels:
            self.join(channel)

    def on_channel_mode(self, user, channel, modes, args = False):
        """Called when a channel mode is changed.

        TODO: Recognize channel modes that aren't specific to users.

        Args:
            args: A list of arguments
        """
        for i, mode in enumerate(modes[1:]):
            if mode in ('a', 'q', 'o', 'h', 'v'):
                target = self.users.get(args[i])
                if modes[0] == '+':
                    target.modes.add(channel.name, mode)
                else:
                    target.modes.remove(channel.name, mode)

    def on_channel_message(self, user, channel, msg):
        raise NotImplementedError

    def on_channel_join(self, user, channel):
        """on_channel_join is called when a user (including the bot) joins a channel.
        
        Args:
            channel: A Channel instance
            user: A user instance
        """
        # Requests channel modes
        self.mode(channel.name)

    def on_channel_part(self, user, channel, reason):
        """on_channel_part is called when a user (including the bot) leaves the channel."""
        user.modes.remove(channel.name, -1)
        if user.nick == self.user.nick:
            self.channels.remove(channel)
        else:
            channel.remove(user)
            
    def on_channel_kick(self, user, channel, target, reason):
        raise NotImplementedError

    def on_channel_topic(self, user, channel, topic):
        raise NotImplementedError

    def on_private_message(self, user, msg):
        raise NotImplementedError
    
    def on_notice(self, user, msg):
        if not __debug__:
            self.logger.info('-{0}- {1}'.format(user.nick, msg))

    def on_nickinuse(self):
        raise NotImplementedError

    def ctcp(self, msg, nick):
        self.send(u'PRIVMSG {2} :{0}{1}{0}'.format('\x01', msg, nick))

    def on_ctcp(self, user, cmd, args):
        global __version__

        if cmd == 'VERSION':
            self.notice('VERSION Teslabot {0}'.format(__version__), user.nick)
コード例 #7
0
class IRC(object):
    """
    IRC protocol interface.
    
    Attributes:
        user: A User object to store the bot's user details
        logger: A Logger object
        channels: A ChannelList object
        admins: A list of User objects
        ssl: A boolean to enable or disable SSL wrapper
        
        _init_channels: A list of channels that is joined when the client is connected
        _reconnect: Whether or not to reconnect when socket connection is lost
        _password: The connection password (if any)
        _buffer: A socket buffer string
        
        _oper: Boolean indicating whether or not the server accepts the client as an IRCOP
        _oper_user: A username string argument for the OPER command
        _oper_pass: A password string argument for the OPER command
        
        _ping: UNIX time of last ping request
        _stimeout_count: A counter to keep track of the number of timeouts
        _SOCKET_TIMEOUT: The number of seconds before the socket times out
        _PING_TIMEOUT: The number of seconds after which a PING message is considered timed out
        _PING_FREQUENCY: The number of seconds (in multiples of _SOCKET_TIMEOUT) that must elapse
            before a PING message is sent to the server.
        
        _last_msg: UNIX time of latest received message
    """
    def __init__(self,
                 nick,
                 realname,
                 channels,
                 admins,
                 _ssl=False,
                 reconnect=False,
                 password=False,
                 oper_user=False,
                 oper_pass=False):
        self.users = UserList()
        self.user = User(nick=nick, real=realname)
        self.users.append(self.user)
        self.channels = ChannelList(self.users)

        self._nick = nick
        self._real = realname

        self.logger = logging.getLogger('teslabot.irc')
        self._init_channels = channels
        self._ssl = _ssl
        self._reconnect = reconnect
        self._reconnect_time = 0
        self._password = password
        self._buffer = ''
        self._ipaddr = None

        self._oper = False
        self._oper_user = oper_user
        self._oper_pass = oper_pass

        self._msg_time = []
        self._msg_last_id = 0
        self._max_mps = 5
        self._throttle = False

        self._ping = 0
        self._stimeout_count = 0
        self._SOCKET_TIMEOUT = 5
        self._PING_TIMEOUT = 5
        self._PING_FREQUENCY = 12

        self._last_msg = 0

        self.alive = 1

        # Define users with administrator privileges
        for admin in admins:
            user = User(src=admin, admin=True)
            self.users.append(user)

    def run(self):
        """
        Keeps the the connection alive by continuously calling _recv().
        It will attempt to reconnect if connection is lost and reconnect is enabled.
        """
        while self.alive:
            try:
                self._recv()
            except socket.error as e:
                self.reconnect()

    def _get_headers(self):
        return ':' + self.user.nick + '!' + self.user.real + '@' + self.user.host

    def join(self, chan):
        """Accepts channel name string."""
        self.send('JOIN :{0}'.format(chan))

    @property
    def nick(self):
        return self.user.nick

    @nick.setter
    def nick(self, value):
        self.send('NICK {0}'.format(value))
        self.user.nick = value

    def notice(self, msg, nick):
        """Accepts a string or a list of messages."""
        if type(msg) == list:
            for line in msg:
                if len(line) > 0:
                    self.send(u'NOTICE {0} :{1}'.format(nick, line))
                    self.logger.info('>{0}< {1}'.format(nick, line))
        else:
            msg = msg.split('\r\n')
            for line in msg:
                maxlen = 512 - len(self._get_headers()) - len(nick) - 12

                if len(line) < maxlen:
                    self.send('NOTICE {0} :{1}'.format(nick, line))
                    self.logger.info('>{0}< {1}'.format(nick, line))
                else:
                    self.send('NOTICE {0} :{1}'.format(nick,
                                                       line[:maxlen - 1]))
                    self.logger.info('>{0}< {1}'.format(
                        nick, line[:maxlen - 1]))
                    self.notice(line, line[maxlen - 1:])

    def mode(self, target, modes=False, args=False):
        """Sends a MODE command.
        Args:
            target: A channel or nick string
            modes: A string of flags
            args: An argument string
        """
        if not modes:
            self.send('MODE {0}'.format(target))
        elif modes and not args:
            self.send('MODE {0} {1}'.format(target, modes))
        elif modes and args:
            self.send('MODE {0} {1} {2}'.format(target, modes, args))

    def kick(self, nick, chan, reason=''):
        self.send(u'KICK {0} {1} :{2}'.format(chan, nick, reason))

    def _get_avg_mps(self):
        """Returns the average rate of sent messages per second."""
        x1 = self._msg_last_id - self._max_mps
        t1 = self._msg_time[self._msg_last_id - self._max_mps]
        x2 = self._msg_last_id
        t2 = self._msg_time[self._msg_last_id]

        avg_mps = (x2 - x1) / (t2 - t1)

        return avg_mps

    def _is_throttling(self):
        """Returns true if the rate of messages per second has exceeded the limit.
        
        Formula:
            (x2 - x1)/(t2 - t1) = mps (average messages per second)
            where (x1, t1) and (x2, t2) represent amount of x messages covered in y seconds.
            
            The distance between (x1, t1) and (x2, t2) is determined by _max_mps (5 by default).
            
            If the average mps exceeds _max_mps, throttling occurs. When throttling, messages are
            sent every 1 second. When the average mps is equal to or less than 1, throttling stops.
            
        Data structure:
            _msg_time: a list of UNIX time values, whose index represents the number of messages.
            Although in practice, the difference between each index is the only relevant fact.
            As a result, we "pop" values from the beginning of the list that are no longer needed.
        """
        self._msg_time.append(time.time())
        throttle = False

        if self._throttle:
            avg_mps = self._get_avg_mps()

            if self._msg_last_id % self._max_mps == 0:
                # In order to prevent the list from growing big, we drop values we no longer need.
                self._msg_time.pop(0)
                self._msg_last_id -= 1

            if avg_mps <= 1:
                self.logger.warning('Throttling disabled.')
                self._throttle = False
            else:
                self.logger.warning('Throttling.')
                throttle = True

        elif len(self._msg_time) > self._max_mps:
            avg_mps = self._get_avg_mps()

            # In order to prevent the list from growing large, we drop values we no longer need.
            if self._msg_last_id % self._max_mps == 0:
                self._msg_time.pop(0)
                self._msg_last_id -= 1

            if avg_mps >= self._max_mps:
                self.logger.warning('Throttling enabled.')
                self._throttle = throttle = True
            else:
                throttle = False

        self._msg_last_id += 1
        return throttle

    def send(self, msg, silent=False):
        """msg should be 512 bytes or less"""

        if self._is_throttling():
            time.sleep(1)

        # Encode to bytes -- assuming it's a utf-8 string
        msg = msg.encode('utf-8')

        self.sock.send(msg + '\r\n')
        if not silent:
            self.logger.debug('{0}'.format(msg))

    def leave(self, chan, reason='Leaving'):
        self.send('PART {0} :{1}'.format(chan, reason))

    def say(self, msg, dst):
        msg = msg.split('\r\n')

        for line in msg:
            maxlen = 512 - len(self._get_headers()) - len(dst) - 12

            if len(line) < maxlen:
                self.send(u'PRIVMSG {0} :{1}'.format(dst, line))
                self.logger.info(u'[{0}] <{1}> {2}'.format(
                    dst, self.user.nick, line))
            else:
                self.send(u'PRIVMSG ' + dst + ' :' + line[:maxlen - 1])
                self.logger.info(u'[{0}] <{1}> {2}'.format(
                    dst, self.user.nick, line[:maxlen - 1]))
                self.say(line[maxlen - 1:], dst)

    def names(self, chan):
        self.send('NAMES {0}'.format(chan))

    def action(self, msg, dst):
        self.send('PRIVMSG ' + dst + ' :\x01ACTION ' + msg + '\x01')

    def whois(self, nick):
        self.send('WHOIS {0}'.format(nick))

    def oper(self, username=False, password=False):
        if not username:
            username = self._oper_user
        if not password:
            password = self._oper_pass
        self.send('OPER {0} {1}'.format(username, password), True)

    def is_chan(self, chan):
        """Returns true if a given string is a valid channel name."""
        if chan[:1] in ('&', '#', '+', '!'):
            return True
        return False

    def _on_privmsg(self, user, args):
        """Calls the appropriate handler for a given PRIVMSG type.
        Either channel, private, or CTCP message.
        
        Args:
            user: A User class instance
            args: A string of arguments
        """
        dst, msg = args.split(' ', 1)

        # CTCP query/reply
        if msg[1] == '\x01' and msg[-1] == '\x01':
            ctcp_msg = msg[2:-1]
            if len(ctcp_msg.split()) > 1:
                cmd, subargs = ctcp_msg.split(' ', 1)
            else:
                cmd = ctcp_msg
                subargs = None

            self.on_ctcp(user, cmd, subargs)

        # Channel message
        if self.is_chan(dst[:1]):
            self.on_channel_message(user, self.channels.get(dst), msg[1:])

        # Private query
        elif dst.lower() == self.user.nick.lower():
            self.on_private_message(user, msg[1:])

        else:
            self.logger.warning("Unrecognized PRIVMSG format.")

    def quit(self, msg='Quitting', force=None):
        try:
            if force:
                sys.exit()

            self.send('QUIT :{0}'.format(msg))
            self.sock.close()
            self.alive = 0
        finally:
            self.logger.info('Disconnected from [{0}].'.format(self.user.host))

    def ping(self):
        self.send('PING {0}'.format(self._host))
        self._ping = time.time()

    def _set_hostname(self, hostname):
        self.user.host = hostname

    def _recv_timeout(self):
        self._stimeout_count += 1
        if self._stimeout_count >= self._PING_FREQUENCY and not self._ping:
            self._stimeout_count = 0
            self.ping()
        elif self._ping:
            diff = time.time() - self._ping
            if diff >= self._PING_TIMEOUT:
                self.logger.info('Disconnected due to timeout.')
                self.reconnect()

    def _recv(self, timeout=False):
        """Processes messages received from the IRC server and calls the
        appropriate handlers."""
        buffer = ''
        self._last_msg = time.time()

        try:
            buffer = self._buffer + self.sock.recv(512)
        except (socket.error, ssl.SSLError) as e:
            if e.message == 'The read operation timed out'\
                    or e.message == 'timed out':
                self._recv_timeout()
                return
        except socket.timeout:
            self._recv_timeout()
            return

        self._buffer = ''

        # Server has closed the socket connection
        if len(buffer) == 0:
            self.logger.debug('Server has closed socket connection.')
            raise socket.error

        data = buffer.split('\r\n')

        # If not empty, this is part of a new message. Add it to the buffer.
        self._buffer += data.pop()

        # Server didn't send CRLF
        if not len(data):
            return

        # Remove empty strings due to an \r\n in the beginning
        if data[0] == '':
            data.pop(0)

        for msg in data:
            self.logger.debug('{0}'.format(msg))
            # Assuming it's UTF-8 encoded. If not, ignore errors.
            self._parse_message(msg.decode('utf-8', 'ignore'))

    def _parse_message(self, msg):
        """Parses a given IRC message."""
        if msg[:4] == 'PING':
            self.send('PONG {0}'.format(msg[5:]))
            return

        elif msg[:5] == 'ERROR':
            self.quit(force=True)

        src, cmd, args = msg.split(' ', 2)
        user = self.users.get(src[1:])

        if cmd == 'PONG' and self._ping:
            diff = time.time() - self._ping
            self._ping = 0
            self.logger.debug('PONG acknowledged ({0}s).'.format(diff))

        elif cmd == 'PRIVMSG':
            self._on_privmsg(user, args)

        elif cmd == 'MODE':
            args = args.split(' ', 2)
            subargs = False

            if self.is_chan(args[0]):
                if len(args) == 3:
                    subargs = args.pop().split()

                chan, modes = args
                channel = self.channels.get(chan)
                self.on_channel_mode(user, channel, modes, subargs)

        elif cmd == 'NOTICE':
            self.on_notice(user, args)

        elif cmd == 'TOPIC':
            chan = args.split(' ', 1)[0]
            topic = args.split(' ', 1)[1][1:]

            self.on_channel_topic(user, self.channels[chan], topic)

        elif cmd == 'JOIN':
            if args[0] != ':':
                chan = args
            else:
                chan = args[1:]
            channel = self.channels.add(chan)
            self.on_channel_join(user, channel)

        elif cmd == 'KICK':
            chan, target, reason = args.split(' ', 2)
            channel = self.channels[chan]
            target = self.users.get(target)

            self.on_channel_kick(user, channel, target, reason[1:])

        elif cmd == 'NICK':
            self.users.get(user=user).nick = args

        elif cmd == 'PART':
            subargs = args.split()

            if len(subargs) == 2:
                chan, reason = subargs
                self.on_channel_part(user, self.channels[chan], reason[1:])
            else:
                chan = args.split()[0]
                self.on_channel_part(user, self.channels[chan])

        elif cmd == RPL_WELCOME:
            self.on_connect()

        elif cmd == RPL_YOUREOPER:
            self._oper = True

        elif cmd == RPL_HOSTHIDDEN:
            self._set_hostname(args)

        elif cmd in (RPL_WHOISUSER, RPL_WHOISIDLE, RPL_WHOISACTUALLY,
                     RPL_WHOISHOST):
            self._on_whois(cmd, args)

        elif cmd == ERR_NICKNAMEINUSE:
            self.on_nickinuse()

        elif cmd in (RPL_TOPIC, RPL_TOPICWHOTIME, RPL_NAMREPLY):
            self.channels.on_reply(cmd, args)

        elif cmd in (RPL_MOTDSTART, RPL_MOTD, RPL_ENDOFMOTD):
            self.on_motd(args)

    def on_motd(self, msg):
        if not __debug__:
            self.logger.info(msg.split(':', 1)[1])

    def _on_whois(self, cmd, args):
        if cmd == RPL_WHOISUSER:
            if args.split(' ')[1] == self.user.nick:  # Self-WHOIS
                self._set_hostname(args.split(' ')[3])  # Get the hostname
                self.logger.debug('Setting hostname to [{0}].'.format(
                    self.user.host))
            else:
                pass
        elif cmd == RPL_WHOISIDLE:
            args = args.split()

            user = self.users.get(args[1])
            user.idle = int(args[2])
            user.signon = int(args[3])

        elif cmd == RPL_WHOISACTUALLY:
            self._ipaddr = args.split()[-1][1:-1]

        elif cmd == RPL_WHOISHOST:
            self._ipaddr = args.split()[-1]

    def ipaddr(self):
        return self._ipaddr

    def connect(self, host, port):
        """Attempts to establish a socket connection with a given IRC server."""
        self._host = host
        self._port = port

        try:
            self.logger.info('Connecting to {0}:{1}.'.format(host, port))

            self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            if self._ssl:
                self.sock = ssl.wrap_socket(self.sock, cert_reqs=ssl.CERT_NONE)
            self.sock.settimeout(self._SOCKET_TIMEOUT)
            self.sock.connect((host, port))
            self.alive = 1

            if self._password:
                self.send('PASS {0}'.format(self._password))

            self.send('NICK {0}'.format(self._nick))
            self.send('USER {0} 0 * :{0}'.format(self._real))

        except socket.error as e:
            self.logger.critical('Failed to connect to {0}:{1}.'.format(
                host, port))

    def reconnect(self):
        if self._reconnect:
            # Wait 15 seconds before reconnecting again
            tdiff = time.time() - self._reconnect_time
            if self._reconnect_time and tdiff < 15:
                self.logger.info('Attempting to reconnect in 15 seconds...')
                time.sleep(15)

            self.logger.debug('Reconnecting.')
            self.connect(self._host, self._port)
            self._reconnect_time = time.time()
        else:
            self.logger.info('Reconnection disabled.')

    def on_connect(self):
        self.whois(self.user.nick)
        if self._oper_user and self._oper_pass:
            self.oper()

        for channel in self._init_channels:
            self.join(channel)

    def on_channel_mode(self, user, channel, modes, args=False):
        """Called when a channel mode is changed.

        TODO: Recognize channel modes that aren't specific to users.

        Args:
            args: A list of arguments
        """
        for i, mode in enumerate(modes[1:]):
            if mode in ('a', 'q', 'o', 'h', 'v'):
                target = self.users.get(args[i])
                if modes[0] == '+':
                    target.modes.add(channel.name, mode)
                else:
                    target.modes.remove(channel.name, mode)

    def on_channel_message(self, user, channel, msg):
        raise NotImplementedError

    def on_channel_join(self, user, channel):
        """on_channel_join is called when a user (including the bot) joins a channel.
        
        Args:
            channel: A Channel instance
            user: A user instance
        """
        # Requests channel modes
        self.mode(channel.name)

    def on_channel_part(self, user, channel, reason):
        """on_channel_part is called when a user (including the bot) leaves the channel."""
        user.modes.remove(channel.name, -1)
        if user.nick == self.user.nick:
            self.channels.remove(channel)
        else:
            channel.remove(user)

    def on_channel_kick(self, user, channel, target, reason):
        raise NotImplementedError

    def on_channel_topic(self, user, channel, topic):
        raise NotImplementedError

    def on_private_message(self, user, msg):
        raise NotImplementedError

    def on_notice(self, user, msg):
        if not __debug__:
            self.logger.info('-{0}- {1}'.format(user.nick, msg))

    def on_nickinuse(self):
        raise NotImplementedError

    def ctcp(self, msg, nick):
        self.send(u'PRIVMSG {2} :{0}{1}{0}'.format('\x01', msg, nick))

    def on_ctcp(self, user, cmd, args):
        global __version__

        if cmd == 'VERSION':
            self.notice('VERSION Teslabot {0}'.format(__version__), user.nick)