首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >命令行IRC客户端

命令行IRC客户端
EN

Code Review用户
提问于 2016-01-14 17:00:23
回答 1查看 1.9K关注 0票数 11

我很久以前就用Python制作了这个IRC客户端,并认为我会为Python3.5重新访问它(我想玩异步游戏)。在开始之前,我想对此进行一次全面的设计审查。

已知的问题,尽管可以随意评论它们:

  • 我在一节课上做的太多了
  • 有相当一部分重复,可能会被删除。

我不认为有任何明显的but,但我从来没有一个严格的测试套件(读为:从来没有测试套件,我在发现单元测试之前写过这个),所以我很可能遗漏了一些东西。

我也不是IRC规范方面的专家--这主要是谷歌搜索和试错。如果我误解了/误用了/错过了什么,也请对此发表评论。

代码语言:javascript
复制
from __future__ import absolute_import, print_function, unicode_literals

from functools import partial
from multiprocessing import dummy
import datetime
import enum
import logging
import select
import socket 
import time
import threading


now = datetime.datetime.now()
logging.basicConfig(
    filename=''.join(["Logs/", str(datetime.datetime.now()), ".log"]),
    level=logging.INFO
)

ErrorCodes = enum(
    'ErrorCodes', 
    'UNKNOWN_HOST UNKNOWN_FAILURE UNKNOWN_CHANNEL UNKOWN_USER '
    'MESSAGE_SENT PONG_SUCCESS SERVER_CONNECTED HOSTNAME_NOT_RESOLVED '
    'SERVER_DISCONNECTED CHANNEL_JOINED CHANNEL_NAME_MALFORMED CHANNEL_LEFT '
)


class IrcMember(object):
    """Represents an individual who uses the IRC client.

    Only stores non-sensitive information.

    Attributes
    ----------
    nickname : str
        User nickname.
    real_name : str
        User's "real" name.
    ident : str
        User's id
    servers: dict
        Server name to socket mapping for connected servers.
    server_channels: dict
        Server name to a list of channel names.
    server_data: dict
        Server name to dict of user information if it differs from the
        default values.
    lock: threading.Lock
        Socket lock.
    replies: dict
        Pending replies.
    DEFAULT_PORT: int
        The default port to use.
    """

    DEFAULT_PORT = 6667

    def __init__(self, nick, **kwargs):
        """Creates a new member.

        Parameters
        ----------
        nick : str
            The user's nickname.
        real : str, optional
            The user's "real" name, defaults to the nickname.
        ident : str, optional
            The user's id, defaults to the nickname.
        """

        self.nickname = nick
        self.real_name = nick
        self.ident = nick

        for key, value in kwargs.iteritems():
            self.__dict__[key] = value

        self.servers = {}
        self.server_channels = {}
        self.server_data = {}

        self.lock = threading.Lock()
        self.replies = {}

    def send_server_message(self, hostname, message): 
        """Send a message to a server.

        Parameters
        ----------
        hostname : str
            Name of the server to send to.
        message : str
            Message to send.
        """

        if hostname not in self.servers:
            logging.warning("No such server {}".format(hostname))
            logging.warning("Failed to send message {}".format(message))
            return ErrorCodes.UNKNOWN_HOST

        sock = self.servers[hostname]
        try:
            sock.send("{} \r\n".format(message.rstrip()))
        except socket.error as e:
            logging.exception(e)
            logging.warning("Failed to send message {}".format(message))
            return ErrorCodes.UNKNOWN_FAILURE
        else:
            return ErrorCodes.MESSAGE_SENT

    def send_channel_message(self, hostname, chan_name, message):
        """Sends a message to a channel.

        Parameters
        ----------
        hostname : str
            Name of the server to send to
        chan_name : str
            Name of the channel
        message : str
            Message to send
        """

        if hostname not in self.servers:
            logging.warning("Not connected to server {}".format(hostname))
            logging.warning("Failed to send message {}".format(message))
            return ErrorCodes.UNKNOWN_HOST
        elif chan_name not in self.server_channels[hostname]:
            logging.warning("Not in channel {}".format(chan_name))
            logging.warning("Failed to send message {}".format(message))
            return ErrorCodes.UNKNOWN_CHANNEL
        else:
            return self.send_private_message(
                hostname, chan_name, message, channel=True
            )

    def send_private_message(self, hostname, name, message, channel=False):
        """Sends a private message.

        Parameters
        ----------
        hostname: str
            Name of the server to send to
        name: str
            Name of the user or channel to send to
        message: str
            Message to send
        channel: bool, optional
            Whether or not this is a channel message
        """

        if hostname not in self.servers:
            logging.warning("No such server {}".format(hostname))
            logging.warning("Failed to send message {}".format(message))
            return ErrorCodes.UNKNOWN_HOST

        if not (channel or self.user_exists(name)):
            return ErrorCodes.UNKNOWN_USER

        message = "PRIVMSG {}: {}".format(username, message.rstrip())
        return self.send_server_message(hostname, message)

    def user_exists(self, username):
        """Validate a user exists.

        Parameters
        ----------
        username: str
            Name of the user.
        """

        ## TODO: implement this
        return True

    def ping_pong(self, sock, data):
        """Pong a server.

        Parameters
        ----------
        sock : socket.socket
            Socket to pong on.
        data : str
            Data to send in pong.
        """

        try:
            sock.send("PONG {}\r\n".format(data))
        except socket.error as e:
            logging.exception(e)
            logging.warn("Couldn't pong the server")
            return ErrorCodes.UNKNOWN_FAILURE
        else:
            return ErrorCodes.PONG_SUCCESS

    def join_server(self, hostname, port=None, **kwargs):
        """Join a server.

        Parameters
        ----------
        hostname: str
            Name of the server to join.
        port: int, optional
            Port to connect on - defaults to `IrcMember.DEFAULT_PORT`.
        nickname: str, optional
            Nickname to use on this server
        real_name: str, optional
            'Real' name ot use on this server
        ident: str, optional
            Identity to use on this server.
        """

        if port is None:
            port = IrcMember.DEFAULT_PORT

        if hostname in self.servers:
            logging.warn("Already connected to {}".format(hostname))
            return ErrorCodes.SERVER_CONNECTED

        nick = self.nickname
        ident = self.ident
        realname = self.real_name

        ## Checking if the data for this server is different from the defaults
        if kwargs:
            self.serv_to_data[hostname] = {}
            for key, value in kwargs.items():
                if key in ['nickname', 'real_name', 'ident']:
                    self.server_data[hostname][key] = value
                    locals()[key] = value
                else:
                    logging.info(
                        "key-value pair {}: {} unusued".format(key, value)
                    )
            if not self.server_data[hostname]:
                del self.server_data[hostname]

        try:
            ip = socket.gethostbyname(hostname)
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            sock.connect((ip, port))
            self.servers[hostname] = sock
            self.serv_to_chan[hostname] = []
            sock.settimeout(2)
            self.send_server_message(
                hostname, "NICK {}\r\n".format(nick))
            self.send_server_message(hostname, 
                "USER {} {} bla: {}\r\n".format(nick, ident, realname))

        except socket.gaierror as e:
            logging.exception(e)
            return ErrorCodes.HOSTNAME_NOT_RESOLVED

        except socket.error as e:
            logging.exception(e)
            if port != IrcMember.DEFAULT_PORT:
                logging.warning(
                    "Consider using port %s (the defacto IRC port) not of %s"
                        % (IrcMember.DEFAULT_PORT, port)
                )
            return ErrorCodes.UNKNOWN_FAILURE

        else:
            logging.info("Connected to {} on {}".format(hostname, port))
            return ErrorCodes.SERVER_CONNECTED

    def leave_server(self, hostname):
        """Leave a server.

        Parameters
        ----------
        hostname: str
            Server to disconnect from.
        """

        if hostname not in self.servers:
            logging.warning("Not connected to {}".format(hostname))
            return ErrorCodes.SERVER_DISCONNECTED

        try:
            self.send_server_message(hostname, "QUIT\r\n")
            self.servers[hostname].close()        
        except socket.error as e:
            logging.exception(e)
            logging.warning("Failed to leave server {}".format(hostname))
            return 1            
        else:
            for attr_name in ['servers', 'server_channels', 'server_data']:
                try:
                    del getattr(self, attr_name)[hostname]
                except KeyError:
                    # This host doesn't have any data about that
                    pass
            logging.info("Left server {}".format(hostname))
            return ErrorCodes.SERVER_DISCONNECTED

    def join_channel(self, hostname, channel_name):
        """Join a channel.

        Parameters
        ----------
        hostname: str
            Server the channel is on.
        channel_name: str
            Name of the channel.
        """

        if channel_name in self.serv_to_chan[hostname]:
            logging.warning(
                "Already connected to {} on {}".format(hostname, channel_name))
            return ErrorCodes.CHANNEL_JOINED

        if chan_name.startswith("#"):
            try:
                self.send_server_message(
                    hostname, "JOIN {}\r\n".format(chan_name))
            except socket.error as e:
                logging.exception(e)
                logging.warning("Failed to connect to {}".format(chan_name))
                return ErrorCodes.UNKNOWN_FAILURE
            else:
                self.serv_to_chan[hostname].append(chan_name)
                logging.info("Connected to {}".format(chan_name))
                return ErrorCodes.CHANNEL_JOINED
        else:
            logging.warning("Channel names should look like #<channel_name>")
            return ErrorCodes.CHANNEL_NAME_MALFORMED

    def leave_channel(self, hostname, chan_name):
        """Leave a channel.

        Parameters
        ----------
        hostname: str
            Server the channel is on.
        channel_name: str
            Name of the channel.
        """

        if hostname not in self.servers:
            logging.warning("No such server {}".format(hostname))
            return ErrorCodes.UNKNOWN_HOST
        elif chan_name not in self.serv_to_chan[hostname]:
            logging.warning("No such channel {}".format(chan_name))
            return ErrorCodes.CHANNEL_LEFT
        else:
            try:
                self.send_server_message(
                    hostname, "PART {}\r\n".format(chan_name)
                )
            except socket.error as e:
                logging.exception(e)
                logging.warning("Failed to leave {}".format(chan_name))
                return ErrorCodes.UNKNOWN_FAILURE
            else:
                self.serv_to_chan[hostname].remove(chan_name)
                logging.info("Left channel {}".format(chan_name))
            return ErrorCodes.CHANNEL_LEFT

    def receive_all_messages(self, buff_size=4096):
        """Display all messages waiting to be received.

        Parameters
        ----------
        buff_size: int, optional
            How large of a buffer to receive with.
        """

        ready, _, _ = select.select(self.servers.values(), [], [], 5)

        if ready:
            for i in range(len(ready)):
                for host, sock in self.servers.iteritems():
                    if sock == ready[i]:
                        ready[i] = host
            try:
                pool = dummy.Pool()
                pool.map(partial(self.receive_message,
                                 buff_size=buff_size),
                         (tuple(ready),))

                with self.lock:
                    replies, self.replies = self.replies, {}

                for server, reply in replies.iteritems():
                    print("{} :\n\n".format(server))
                    for message in reply:
                        print(" {}".format(message))
            except socket.error as e:
                logging.exception(e)
                logging.warning("Failed to get messages")
                return ErrorCodes.UNKNOWN_FAILURE

        return ErrorCodes.MESSAGES_RECEIVED

    def receive_message(self, hostname, buff_size=4096):
        """Receive a message from a single server.

        Parameters
        ----------
        hostname: tuple
            Server to receive from.
        buff_size: int, optional
            How large of a buffer to receive with.

        Notes
        -----
        Has already checked that there is a message waiting.
        """

        hostname = hostname[0]
        reply = []
        sock = self.servers[hostname]

        while True:
            try:
                readbuffer = sock.recv(buff_size)
                if not readbuffer: 
                    break

                temp = readbuffer.split("\n")
                readbuffer = temp.pop()

                for line in temp:
                    line = line.rstrip().split()
                    if (line[0] == "PING"):
                        self.ping_pong(sock, line[1])
                    else:
                        line = " ".join(line)
                        reply.append(line)
            except socket.error:
                break

        with self.lock:
            try:
                if reply not in self.replies[hostname]: 
                    self.replies[hostname] += reply
            except KeyError:
                self.replies[hostname] = reply

    def __del__(self):
        for host, sock in self.servers.items():
            self.leave_server(host)


if __name__ == "__main__":
    NICK = raw_input("Please enter your nickname ")
    HOST = raw_input("Please enter your desired server ")
    CHAN = raw_input("Please enter your desired channel ")

    me = IRC_member(NICK)
    me.join_server(HOST, nickname='test', ident='test', real_name='test')
    time.sleep(1)
    me.receive_all_messages()
    me.join_channel(HOST, CHAN)
    time.sleep(1)
    me.receive_all_messages()
    i = 0

    while i < 100:
        start = time.time()
        msg = raw_input("Would you like to say something? ")
        if msg == 'n': 
            break
        if msg.rstrip():
            me.send_channel_message(HOST, CHAN, msg)
        me.receive_all_messages()
        end = time.time()
        if (end-start) < 5:
            time.sleep(int(5-(end-start)))
        i += 1
EN

回答 1

Code Review用户

发布于 2016-01-14 17:24:52

send_server_message中,您应该使用EAFP (请求宽恕比允许容易)。您测试了if hostname not in self.servers,但通常情况下不会是这样吗?您应该使用try except,并在罕见的情况下捕获KeyError

代码语言:javascript
复制
    try:
        sock = self.servers[hostname]
    except KeyError:
        logging.warning("No such server {}".format(hostname))
        logging.warning("Failed to send message {}".format(message))
        return ErrorCodes.UNKNOWN_HOST

此外,在语义上,我不喜欢从return内部的else,只要有一个块之外的return,因为没有其他的方式,它将达到这一点。

将其作为get_hostvalidate_host函数也可能是值得的。您反复使用类似的模式。但令人困惑的是,你似乎并不总是把同样的错误还给你?只有一个函数可以防止重复的代码,并使错误更加清楚。

我也认为返回MESSAGE_SENT作为一个明显的错误代码是有问题的。这肯定是个成功的结果?对于成功的结果,可以有一个单独的枚举,或者将其重命名为类似于ResultCodes的内容,这一点可能会更加清楚。

票数 6
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/116793

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档