From 37d6b4fed049caf62335f09db6fd44651a1e9d7d Mon Sep 17 00:00:00 2001 From: Forest Belton <65484+forestbelton@users.noreply.github.com> Date: Tue, 22 Jun 2021 19:11:37 -0400 Subject: [PATCH] Implement channel topics and fix join --- paircd/channel.py | 12 ++++++++-- paircd/handler/join.py | 14 +++++++----- paircd/handler/mode.py | 5 ++++- paircd/handler/nick.py | 5 ++++- paircd/handler/topic.py | 50 +++++++++++++++++++++++++++++++++++++++++ paircd/handler/who.py | 7 +++++- paircd/handlers.py | 2 ++ paircd/reply.py | 8 ++++++- paircd/server.py | 6 ++--- 9 files changed, 95 insertions(+), 14 deletions(-) create mode 100644 paircd/handler/topic.py diff --git a/paircd/channel.py b/paircd/channel.py index aeb9008..f636126 100644 --- a/paircd/channel.py +++ b/paircd/channel.py @@ -10,14 +10,14 @@ from paircd.message import Message class Channel: name: str topic: str = "" - modes: Set[str] = field(default_factory=set) + modes: Set[str] = field(default_factory=lambda: {"t"}) clients_by_nick: Dict[str, Client] = field(default_factory=dict) modes_by_nick: Dict[str, Set[str]] = field(default_factory=lambda: defaultdict(set)) def add_client(self, client: Client) -> None: self.clients_by_nick[client.nickname] = client if len(self.clients_by_nick) == 1: - self.modes_by_nick[client.nickname].add("@") + self.modes_by_nick[client.nickname].add("o") def remove_client_by_nick(self, nick: str) -> None: del self.clients_by_nick[nick] @@ -39,6 +39,14 @@ class Channel: def get_user_modes_by_nick(self, nick: str) -> Set[str]: return self.modes_by_nick[nick] + def get_channel_name(self, nick: str) -> str: + modes = self.get_user_modes_by_nick(nick) + if "o" in modes: + return f"@{nick}" + if "v" in modes: + return f"+{nick}" + return nick + def rename_client(self, name: str, new_name: str) -> None: if name not in self.clients_by_nick: raise KeyError(f"No client on channel with nick {name}") diff --git a/paircd/handler/join.py b/paircd/handler/join.py index ef9e66f..f3f9dab 100644 --- a/paircd/handler/join.py +++ b/paircd/handler/join.py @@ -1,9 +1,9 @@ from paircd.reply import ( ERR_NOSUCHCHANNEL, - ERR_NOTREGISTERED, JOIN, RPL_ENDOFNAMES, RPL_NAMREPLY, + RPL_NOTOPIC, RPL_TOPIC, ) @@ -19,11 +19,12 @@ class JoinHandler(CommandHandler): async def handle(self, server: Server, client: Client, msg: Message) -> None: channel_name = msg.args[0] - if not channel_name.startswith("#"): + + channel = server.get_channel_by_name(channel_name) + if channel is None: client.write_message(ERR_NOSUCHCHANNEL(client.nickname, channel_name)) return - channel = server.get_channel_by_name(channel_name) channel.add_client(client) client.channels.add(channel_name) @@ -31,15 +32,18 @@ class JoinHandler(CommandHandler): channel.write_message(JOIN(channel_name, prefix=client.id())) - if channel.topic != "": + if len(channel.topic) > 0: client.write_message( RPL_TOPIC(client.nickname, channel_name, channel.topic) ) + else: + client.write_message(RPL_NOTOPIC(client.nickname, channel_name)) channel_members = [ - f"{''.join(channel.get_user_modes_by_nick(member))}{member}" + channel.get_channel_name(member) for member in channel.clients_by_nick.keys() ] + client.write_message( # "=" means public channel (ref: https://modern.ircdocs.horse/#rplnamreply-353) RPL_NAMREPLY(client.nickname, "=", channel_name, " ".join(channel_members)) diff --git a/paircd/handler/mode.py b/paircd/handler/mode.py index 962c539..6eabb9f 100644 --- a/paircd/handler/mode.py +++ b/paircd/handler/mode.py @@ -34,7 +34,10 @@ class ModeHandler(CommandHandler): def handle_channel(self, server: Server, client: Client, msg: Message) -> None: name = msg.args[0] - channel = server.get_channel_by_name(name) + + channel = server.get_channel_by_name(name, create=False) + if channel is None: + return if len(msg.args) == 2: client.log("TODO: implement channel mode set") diff --git a/paircd/handler/nick.py b/paircd/handler/nick.py index cd0352d..5fef1b1 100644 --- a/paircd/handler/nick.py +++ b/paircd/handler/nick.py @@ -34,7 +34,10 @@ class NickHandler(CommandHandler): server.add_client(client) for channel_name in client.channels: - channel = server.get_channel_by_name(channel_name) + channel = server.get_channel_by_name(channel_name, create=False) + if channel is None: + continue + if rename: channel.rename_client(old_nick, nickname) channel.write_message(NICK(nickname, prefix=old_id)) diff --git a/paircd/handler/topic.py b/paircd/handler/topic.py new file mode 100644 index 0000000..a81d5be --- /dev/null +++ b/paircd/handler/topic.py @@ -0,0 +1,50 @@ +from paircd.client import Client +from paircd.command_handler import CommandHandler +from paircd.message import Message +from paircd.reply import ( + ERR_CHANOPRIVSNEEDED, + ERR_NEEDMOREPARAMS, + ERR_NOTONCHANNEL, + RPL_NOTOPIC, + RPL_TOPIC, + TOPIC, +) +from paircd.server import Server + + +class TopicHandler(CommandHandler): + def __init__(self) -> None: + super().__init__("TOPIC") + + async def handle(self, server: Server, client: Client, msg: Message) -> None: + if len(msg.args) == 0: + client.write_message(ERR_NEEDMOREPARAMS(client.nickname, "TOPIC")) + return + + channel_name = msg.args[0] + channel = server.get_channel_by_name(channel_name, create=False) + if channel is None or client.nickname not in channel.clients_by_nick: + client.write_message(ERR_NOTONCHANNEL(client.nickname, channel_name)) + return + + topic_set = False + if len(msg.args) == 2: + user_modes = channel.get_user_modes_by_nick(client.nickname) + if "t" in channel.get_modes() and "o" not in user_modes: + client.write_message( + ERR_CHANOPRIVSNEEDED(client.nickname, channel_name) + ) + return + channel.topic = msg.args[1] + topic_set = True + + if topic_set: + client.log(f"updated topic for {channel_name}") + channel.write_message( + TOPIC(channel_name, channel.topic, prefix=client.id()) + ) + else: + topic_msg = RPL_TOPIC(client.nickname, channel_name, channel.topic) + if len(channel.topic) == 0: + topic_msg = RPL_NOTOPIC(client.nickname, channel_name) + client.write_message(topic_msg) diff --git a/paircd/handler/who.py b/paircd/handler/who.py index 5352f01..23ca588 100644 --- a/paircd/handler/who.py +++ b/paircd/handler/who.py @@ -24,7 +24,12 @@ class WhoHandler(CommandHandler): self.who_end(client, name) return - channel = server.get_channel_by_name(name) + channel = server.get_channel_by_name(name, create=False) + if channel is None: + client.log("client used WHO outside of channel", level=logging.WARN) + self.who_end(client, name) + return + for member in channel.clients_by_nick.values(): self.who_reply(client, channel, member) self.who_end(client, name) diff --git a/paircd/handlers.py b/paircd/handlers.py index cf634c8..c75194b 100644 --- a/paircd/handlers.py +++ b/paircd/handlers.py @@ -15,6 +15,7 @@ from paircd.handler.nick import NickHandler from paircd.handler.notice import NoticeHandler from paircd.handler.ping import PingHandler from paircd.handler.privmsg import PrivmsgHandler +from paircd.handler.topic import TopicHandler from paircd.handler.user import UserHandler from paircd.handler.who import WhoHandler @@ -26,6 +27,7 @@ HANDLER_CLASSES = [ NoticeHandler, PingHandler, PrivmsgHandler, + TopicHandler, UserHandler, WhoHandler, ] diff --git a/paircd/reply.py b/paircd/reply.py index a116221..74e54bf 100644 --- a/paircd/reply.py +++ b/paircd/reply.py @@ -30,6 +30,7 @@ NICK = cmd_fn("NICK", "{0}") NOTICE = cmd_fn("NOTICE", "{0} :{1}") PONG = cmd_fn("PONG", ":{0}") PRIVMSG = cmd_fn("PRIVMSG", "{0} :{1}") +TOPIC = cmd_fn("TOPIC", "{0} :{1}") # Error replies ERR_NOSUCHNICK = reply_fn(401, "{0} :No such nick/channel") @@ -37,10 +38,14 @@ ERR_NOSUCHSERVER = reply_fn(402, "{0} :No such server") ERR_NOSUCHCHANNEL = reply_fn(403, "{0} :No such channel") ERR_CANNOTSENDTOCHAN = reply_fn(404, "{0} :Cannot send to channel") ERR_TOOMANYCHANNELS = reply_fn(405, "{0} :You have joined too many channels") +ERR_NOTEXTTOSEND = reply_fn(412, ":No text to send") ERR_NONICKNAMEGIVEN = reply_fn(431, ":No nickname given") ERR_NICKNAMEINUSE = reply_fn(433, "{0} :Nickname is already in use") -ERR_NOTEXTTOSEND = reply_fn(412, ":No text to send") +ERR_NOTONCHANNEL = reply_fn(442, "{0} :You're not on that channel") ERR_NOTREGISTERED = reply_fn(451, ":You have not registered") +ERR_NEEDMOREPARAMS = reply_fn(461, "{0} :Not enough parameters") +ERR_ALREADYREGISTRED = reply_fn(462, ":Unauthorized command (already registered)") +ERR_CHANOPRIVSNEEDED = reply_fn(482, "{0} :You're not channel operator") ERR_USERSDONTMATCH = reply_fn(502, ":Cannot change mode for other users") # Command responses @@ -56,6 +61,7 @@ RPL_WHOISUSER = reply_fn(311, "{0} {1} {2} * :{3}") RPL_ENDOFWHO = reply_fn(315, "{0} :End of /WHO list") RPL_ENDOFWHOIS = reply_fn(318, "{0} :End of /WHOIS list") RPL_CHANNELMODEIS = reply_fn(324, "{0} {1}{2}") +RPL_NOTOPIC = reply_fn(331, "{0} :No topic is set") RPL_TOPIC = reply_fn(332, "{0} :{1}") RPL_WHOREPLY = reply_fn(352, "{0} ~{1} {2} {3} {4} {5} :{6} {7}") RPL_NAMREPLY = reply_fn(353, "{0} {1} :{2}") diff --git a/paircd/server.py b/paircd/server.py index f20b97f..0e77304 100644 --- a/paircd/server.py +++ b/paircd/server.py @@ -15,10 +15,10 @@ class Server: def add_client(self, client: Client) -> None: self.clients_by_nick[client.nickname] = client - def get_channel_by_name(self, name: str) -> Channel: - if name not in self.channels_by_name: + def get_channel_by_name(self, name: str, create: bool = True) -> Optional[Channel]: + if name not in self.channels_by_name and create: self.channels_by_name[name] = Channel(name=name) - return self.channels_by_name[name] + return self.channels_by_name.get(name) def get_client_by_name(self, name: str) -> Optional[Client]: return self.clients_by_nick.get(name)