From c8805d0a6526c57491598c223088d7fc4414e6ba Mon Sep 17 00:00:00 2001 From: Forest Belton <65484+forestbelton@users.noreply.github.com> Date: Tue, 22 Jun 2021 01:07:25 -0400 Subject: [PATCH] Implement WHO command --- .flake8 | 1 + paircd/channel.py | 8 +++++-- paircd/client.py | 8 +++++++ paircd/handler/join.py | 9 ++++---- paircd/handler/nick.py | 3 +-- paircd/handler/privmsg.py | 12 ++++------ paircd/handler/user.py | 5 ++-- paircd/handler/who.py | 48 +++++++++++++++++++++++++++++++++++++++ paircd/handlers.py | 6 +++-- paircd/log.py | 7 ------ paircd/message.py | 2 +- paircd/reply.py | 22 ++++++++++++++++++ paircd/server.py | 2 +- 13 files changed, 102 insertions(+), 31 deletions(-) create mode 100644 paircd/handler/who.py delete mode 100644 paircd/log.py create mode 100644 paircd/reply.py diff --git a/.flake8 b/.flake8 index 7da1f96..24b63ca 100644 --- a/.flake8 +++ b/.flake8 @@ -1,2 +1,3 @@ [flake8] +ignore = E203 max-line-length = 100 diff --git a/paircd/channel.py b/paircd/channel.py index c6dfec8..5be7818 100644 --- a/paircd/channel.py +++ b/paircd/channel.py @@ -3,6 +3,7 @@ from dataclasses import dataclass, field from typing import Dict from paircd.client import Client +from paircd.message import Message @dataclass @@ -22,6 +23,9 @@ class Channel: msg = await self.msg_queue.get() for client in self.clients_by_nick.values(): # Don't broadcast client's messages back to themselves - if msg.startswith(f":{client.id()} PRIVMSG".encode("utf-8")): + if msg.cmd == "PRIVMSG" and msg.prefix == client.id(): continue - client.msg_queue.put_nowait(msg) + client.write_message(msg) + + def write_message(self, msg: Message) -> None: + self.msg_queue.put_nowait(msg) diff --git a/paircd/client.py b/paircd/client.py index b8536eb..4df17d9 100644 --- a/paircd/client.py +++ b/paircd/client.py @@ -1,5 +1,7 @@ from asyncio import StreamReader, StreamWriter, Queue from dataclasses import dataclass, field +from logging import log, INFO +from paircd.message import Message from typing import Set @@ -20,8 +22,14 @@ class Client: def id(self) -> str: return f"{self.nickname}!{self.username}@{self.hostname}" + def log(self, msg: str, level: int = INFO) -> None: + log(level, f"{self.hostname} ({self.id()}) {msg}") + async def write_forever(self) -> None: while True: msg = await self.msg_queue.get() self.writer.write(msg) await self.writer.drain() + + def write_message(self, message: Message) -> None: + self.msg_queue.put_nowait(message.encode()) diff --git a/paircd/handler/join.py b/paircd/handler/join.py index 36693d4..9432a62 100644 --- a/paircd/handler/join.py +++ b/paircd/handler/join.py @@ -2,7 +2,6 @@ import logging from paircd.client import Client from paircd.command_handler import CommandHandler -from paircd.log import log_client from paircd.message import Message from paircd.server import Server @@ -13,20 +12,20 @@ class JoinHandler(CommandHandler): async def handle(self, server: Server, client: Client, msg: Message) -> None: if not client.registered: - log_client(client, "join: not registered", level=logging.WARN) + client.log("join: not registered", level=logging.WARN) return channel_name = msg.args[0] if not channel_name.startswith("#"): - log_client(client, "tried to join invalid channel", level=logging.WARN) + client.log("tried to join invalid channel", level=logging.WARN) return channel = server.get_channel_by_name(channel_name) channel.add_client(client) client.channels.add(channel_name) - log_client(client, f"joined {channel_name}") + client.log(f"joined {channel_name}") await channel.msg_queue.put( - Message(cmd="JOIN", args=[channel_name], prefix=client.id()).encode() + Message(cmd="JOIN", args=[channel_name], prefix=client.id()) ) diff --git a/paircd/handler/nick.py b/paircd/handler/nick.py index 65525d2..65d7e5b 100644 --- a/paircd/handler/nick.py +++ b/paircd/handler/nick.py @@ -1,6 +1,5 @@ from paircd.client import Client from paircd.command_handler import CommandHandler -from paircd.log import log_client from paircd.message import Message from paircd.server import Server @@ -29,4 +28,4 @@ class NickHandler(CommandHandler): if client.username and client.realname: client.registered = True - log_client(client, "registered") + client.log("registered") diff --git a/paircd/handler/privmsg.py b/paircd/handler/privmsg.py index 26eb847..9736570 100644 --- a/paircd/handler/privmsg.py +++ b/paircd/handler/privmsg.py @@ -2,7 +2,6 @@ import logging from paircd.client import Client from paircd.command_handler import CommandHandler -from paircd.log import log_client from paircd.message import Message from paircd.server import Server @@ -14,19 +13,16 @@ class PrivmsgHandler(CommandHandler): async def handle(self, server: Server, client: Client, msg: Message) -> None: recipient = msg.args[0] raw_msg = msg.args[1] - - out = Message( - "PRIVMSG", [recipient, f":{raw_msg}"], prefix=client.id() - ).encode() + out = Message("PRIVMSG", [recipient, f":{raw_msg}"], prefix=client.id()) for name, other_client in server.clients_by_nick.items(): if name == recipient: - other_client.msg_queue.put_nowait(out) + other_client.write_message(out) return for name, channel in server.channels_by_name.items(): if name == recipient: - channel.msg_queue.put_nowait(out) + channel.write_message(out) return - log_client(client, "unknown recipient", level=logging.WARN) + client.log("unknown recipient", level=logging.WARN) diff --git a/paircd/handler/user.py b/paircd/handler/user.py index 4c6a311..4134890 100644 --- a/paircd/handler/user.py +++ b/paircd/handler/user.py @@ -2,7 +2,6 @@ import logging from paircd.client import Client from paircd.command_handler import CommandHandler -from paircd.log import log_client from paircd.message import Message from paircd.server import Server @@ -13,7 +12,7 @@ class UserHandler(CommandHandler): async def handle(self, server: Server, client: Client, msg: Message) -> None: if client.registered: - log_client(client, "USER issued after registration", level=logging.WARN) + client.log("USER issued after registration", level=logging.WARN) return client.username = msg.args[0] @@ -21,4 +20,4 @@ class UserHandler(CommandHandler): if client.nickname: client.registered = True - log_client(client, "registered") + client.log("registered") diff --git a/paircd/handler/who.py b/paircd/handler/who.py new file mode 100644 index 0000000..ca03c82 --- /dev/null +++ b/paircd/handler/who.py @@ -0,0 +1,48 @@ +import logging +from paircd.reply import RPL_ENDOFWHO, RPL_WHOREPLY + +from paircd.client import Client +from paircd.command_handler import CommandHandler +from paircd.message import Message +from paircd.server import Server + + +class WhoHandler(CommandHandler): + def __init__(self) -> None: + super().__init__("WHO", 1) + + async def handle(self, server: Server, client: Client, msg: Message) -> None: + name = msg.args[0] + if not name.startswith("#"): + client.log("used WHO on invalid channel", level=logging.WARN) + self.who_end(client, name) + return + + if name not in client.channels: + client.log("client used WHO outside of channel", level=logging.WARN) + self.who_end(client, name) + return + + channel = server.get_channel_by_name(name) + for member in channel.clients_by_nick.values(): + self.who_reply(client, name, member) + self.who_end(client, name) + + def who_reply(self, client: Client, channel_name: str, member: Client) -> None: + msg = RPL_WHOREPLY( + client.nickname, + channel_name, + member.username, + member.hostname, + "0", # server + member.nickname, + # TODO: Implement + "H", # H = here, G = gone + "0", # hop count + f":{member.realname}", + ) + client.write_message(msg) + + def who_end(self, client: Client, channel_name: str) -> None: + msg = RPL_ENDOFWHO(client.nickname, channel_name) + client.write_message(msg) diff --git a/paircd/handlers.py b/paircd/handlers.py index 3023af3..8482317 100644 --- a/paircd/handlers.py +++ b/paircd/handlers.py @@ -1,9 +1,9 @@ from logging import WARN + from typing import Dict from paircd.client import Client from paircd.command_handler import CommandHandler -from paircd.log import log_client from paircd.message import Message from paircd.server import Server @@ -11,12 +11,14 @@ from paircd.handler.join import JoinHandler from paircd.handler.nick import NickHandler from paircd.handler.privmsg import PrivmsgHandler from paircd.handler.user import UserHandler +from paircd.handler.who import WhoHandler HANDLER_CLASSES = [ JoinHandler, NickHandler, PrivmsgHandler, UserHandler, + WhoHandler, ] CMD_HANDLERS: Dict[str, CommandHandler] = {} @@ -30,6 +32,6 @@ def register_cmd_handlers() -> None: async def handle_cmd(server: Server, client: Client, msg: Message) -> None: if msg.cmd not in CMD_HANDLERS: - log_client(client, f"used unknown command {msg.cmd}", level=WARN) + client.log(f"used unknown command {msg.cmd}", level=WARN) return await CMD_HANDLERS[msg.cmd].handle(server, client, msg) diff --git a/paircd/log.py b/paircd/log.py deleted file mode 100644 index 57b2013..0000000 --- a/paircd/log.py +++ /dev/null @@ -1,7 +0,0 @@ -import logging - -from paircd.client import Client - - -def log_client(client: Client, msg: str, level: int = logging.INFO) -> None: - logging.log(level, f"{client.hostname} ({client.id()}) {msg}") diff --git a/paircd/message.py b/paircd/message.py index c53aa82..f2264ed 100644 --- a/paircd/message.py +++ b/paircd/message.py @@ -55,7 +55,7 @@ def parse_message(raw: bytes) -> Message: if len(tokens) == 0: raise ParsingError("Message has no command") - cmd = tokens[0] + cmd = tokens[0].upper() if cmd in EXPECTED_ARG_COUNT and EXPECTED_ARG_COUNT[cmd] != len(tokens) - 1: raise ParsingError( f"{cmd} had {len(tokens)-1} arguments, expected {EXPECTED_ARG_COUNT[cmd]}" diff --git a/paircd/reply.py b/paircd/reply.py new file mode 100644 index 0000000..11fa60e --- /dev/null +++ b/paircd/reply.py @@ -0,0 +1,22 @@ +from typing import Any, Callable, List + +from paircd.message import Message + + +def reply_fn(cmd: int, tmpl: str) -> Callable: + def fn(target: str, *args: List[Any]) -> Message: + msg = f"{target} {tmpl.format(*args)}" + return Message(cmd=str(cmd), args=[msg]) + + return fn + + +# Error replies +ERR_NOSUCHNICK = reply_fn(401, "{0} :No such nick/channel") +ERR_NOSUCHSERVER = reply_fn(402, "{0}: No such server") + +# Command responses +RPL_WHOISUSER = reply_fn(311, "{0} {1} {2} * :{3}") +RPL_ENDOFWHOIS = reply_fn(318, "{0} :End of /WHOIS list") +RPL_WHOREPLY = reply_fn(352, "{0} {1} {2} {3} {3} {4} {5} :{6} {7}") +RPL_ENDOFWHO = reply_fn(315, "{0} :End of /WHO list") diff --git a/paircd/server.py b/paircd/server.py index 04ee0fd..9c7679d 100644 --- a/paircd/server.py +++ b/paircd/server.py @@ -17,7 +17,7 @@ class Server: def get_channel_by_name(self, name: str) -> Channel: if name not in self.channels_by_name: self.channels_by_name[name] = Channel(name=name) - create_task(self.channels_by_name[name].process()) + create_task(self.channels_by_name[name].process()) return self.channels_by_name[name] def remove_client_by_name(self, name: str) -> None: