Browse Source

Implement channel topics and fix join

master
Forest Belton 3 years ago
parent
commit
37d6b4fed0
9 changed files with 95 additions and 14 deletions
  1. +10
    -2
      paircd/channel.py
  2. +9
    -5
      paircd/handler/join.py
  3. +4
    -1
      paircd/handler/mode.py
  4. +4
    -1
      paircd/handler/nick.py
  5. +50
    -0
      paircd/handler/topic.py
  6. +6
    -1
      paircd/handler/who.py
  7. +2
    -0
      paircd/handlers.py
  8. +7
    -1
      paircd/reply.py
  9. +3
    -3
      paircd/server.py

+ 10
- 2
paircd/channel.py View File

@ -10,14 +10,14 @@ from paircd.message import Message
class Channel: class Channel:
name: str name: str
topic: 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) clients_by_nick: Dict[str, Client] = field(default_factory=dict)
modes_by_nick: Dict[str, Set[str]] = field(default_factory=lambda: defaultdict(set)) modes_by_nick: Dict[str, Set[str]] = field(default_factory=lambda: defaultdict(set))
def add_client(self, client: Client) -> None: def add_client(self, client: Client) -> None:
self.clients_by_nick[client.nickname] = client self.clients_by_nick[client.nickname] = client
if len(self.clients_by_nick) == 1: 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: def remove_client_by_nick(self, nick: str) -> None:
del self.clients_by_nick[nick] del self.clients_by_nick[nick]
@ -39,6 +39,14 @@ class Channel:
def get_user_modes_by_nick(self, nick: str) -> Set[str]: def get_user_modes_by_nick(self, nick: str) -> Set[str]:
return self.modes_by_nick[nick] 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: def rename_client(self, name: str, new_name: str) -> None:
if name not in self.clients_by_nick: if name not in self.clients_by_nick:
raise KeyError(f"No client on channel with nick {name}") raise KeyError(f"No client on channel with nick {name}")

+ 9
- 5
paircd/handler/join.py View File

@ -1,9 +1,9 @@
from paircd.reply import ( from paircd.reply import (
ERR_NOSUCHCHANNEL, ERR_NOSUCHCHANNEL,
ERR_NOTREGISTERED,
JOIN, JOIN,
RPL_ENDOFNAMES, RPL_ENDOFNAMES,
RPL_NAMREPLY, RPL_NAMREPLY,
RPL_NOTOPIC,
RPL_TOPIC, RPL_TOPIC,
) )
@ -19,11 +19,12 @@ class JoinHandler(CommandHandler):
async def handle(self, server: Server, client: Client, msg: Message) -> None: async def handle(self, server: Server, client: Client, msg: Message) -> None:
channel_name = msg.args[0] 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)) client.write_message(ERR_NOSUCHCHANNEL(client.nickname, channel_name))
return return
channel = server.get_channel_by_name(channel_name)
channel.add_client(client) channel.add_client(client)
client.channels.add(channel_name) client.channels.add(channel_name)
@ -31,15 +32,18 @@ class JoinHandler(CommandHandler):
channel.write_message(JOIN(channel_name, prefix=client.id())) channel.write_message(JOIN(channel_name, prefix=client.id()))
if channel.topic != "":
if len(channel.topic) > 0:
client.write_message( client.write_message(
RPL_TOPIC(client.nickname, channel_name, channel.topic) RPL_TOPIC(client.nickname, channel_name, channel.topic)
) )
else:
client.write_message(RPL_NOTOPIC(client.nickname, channel_name))
channel_members = [ 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() for member in channel.clients_by_nick.keys()
] ]
client.write_message( client.write_message(
# "=" means public channel (ref: https://modern.ircdocs.horse/#rplnamreply-353) # "=" means public channel (ref: https://modern.ircdocs.horse/#rplnamreply-353)
RPL_NAMREPLY(client.nickname, "=", channel_name, " ".join(channel_members)) RPL_NAMREPLY(client.nickname, "=", channel_name, " ".join(channel_members))

+ 4
- 1
paircd/handler/mode.py View File

@ -34,7 +34,10 @@ class ModeHandler(CommandHandler):
def handle_channel(self, server: Server, client: Client, msg: Message) -> None: def handle_channel(self, server: Server, client: Client, msg: Message) -> None:
name = msg.args[0] 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: if len(msg.args) == 2:
client.log("TODO: implement channel mode set") client.log("TODO: implement channel mode set")

+ 4
- 1
paircd/handler/nick.py View File

@ -34,7 +34,10 @@ class NickHandler(CommandHandler):
server.add_client(client) server.add_client(client)
for channel_name in client.channels: 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: if rename:
channel.rename_client(old_nick, nickname) channel.rename_client(old_nick, nickname)
channel.write_message(NICK(nickname, prefix=old_id)) channel.write_message(NICK(nickname, prefix=old_id))

+ 50
- 0
paircd/handler/topic.py View File

@ -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)

+ 6
- 1
paircd/handler/who.py View File

@ -24,7 +24,12 @@ class WhoHandler(CommandHandler):
self.who_end(client, name) self.who_end(client, name)
return 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(): for member in channel.clients_by_nick.values():
self.who_reply(client, channel, member) self.who_reply(client, channel, member)
self.who_end(client, name) self.who_end(client, name)

+ 2
- 0
paircd/handlers.py View File

@ -15,6 +15,7 @@ from paircd.handler.nick import NickHandler
from paircd.handler.notice import NoticeHandler from paircd.handler.notice import NoticeHandler
from paircd.handler.ping import PingHandler from paircd.handler.ping import PingHandler
from paircd.handler.privmsg import PrivmsgHandler from paircd.handler.privmsg import PrivmsgHandler
from paircd.handler.topic import TopicHandler
from paircd.handler.user import UserHandler from paircd.handler.user import UserHandler
from paircd.handler.who import WhoHandler from paircd.handler.who import WhoHandler
@ -26,6 +27,7 @@ HANDLER_CLASSES = [
NoticeHandler, NoticeHandler,
PingHandler, PingHandler,
PrivmsgHandler, PrivmsgHandler,
TopicHandler,
UserHandler, UserHandler,
WhoHandler, WhoHandler,
] ]

+ 7
- 1
paircd/reply.py View File

@ -30,6 +30,7 @@ NICK = cmd_fn("NICK", "{0}")
NOTICE = cmd_fn("NOTICE", "{0} :{1}") NOTICE = cmd_fn("NOTICE", "{0} :{1}")
PONG = cmd_fn("PONG", ":{0}") PONG = cmd_fn("PONG", ":{0}")
PRIVMSG = cmd_fn("PRIVMSG", "{0} :{1}") PRIVMSG = cmd_fn("PRIVMSG", "{0} :{1}")
TOPIC = cmd_fn("TOPIC", "{0} :{1}")
# Error replies # Error replies
ERR_NOSUCHNICK = reply_fn(401, "{0} :No such nick/channel") 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_NOSUCHCHANNEL = reply_fn(403, "{0} :No such channel")
ERR_CANNOTSENDTOCHAN = reply_fn(404, "{0} :Cannot send to channel") ERR_CANNOTSENDTOCHAN = reply_fn(404, "{0} :Cannot send to channel")
ERR_TOOMANYCHANNELS = reply_fn(405, "{0} :You have joined too many channels") 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_NONICKNAMEGIVEN = reply_fn(431, ":No nickname given")
ERR_NICKNAMEINUSE = reply_fn(433, "{0} :Nickname is already in use") 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_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") ERR_USERSDONTMATCH = reply_fn(502, ":Cannot change mode for other users")
# Command responses # 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_ENDOFWHO = reply_fn(315, "{0} :End of /WHO list")
RPL_ENDOFWHOIS = reply_fn(318, "{0} :End of /WHOIS list") RPL_ENDOFWHOIS = reply_fn(318, "{0} :End of /WHOIS list")
RPL_CHANNELMODEIS = reply_fn(324, "{0} {1}{2}") 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_TOPIC = reply_fn(332, "{0} :{1}")
RPL_WHOREPLY = reply_fn(352, "{0} ~{1} {2} {3} {4} {5} :{6} {7}") RPL_WHOREPLY = reply_fn(352, "{0} ~{1} {2} {3} {4} {5} :{6} {7}")
RPL_NAMREPLY = reply_fn(353, "{0} {1} :{2}") RPL_NAMREPLY = reply_fn(353, "{0} {1} :{2}")

+ 3
- 3
paircd/server.py View File

@ -15,10 +15,10 @@ class Server:
def add_client(self, client: Client) -> None: def add_client(self, client: Client) -> None:
self.clients_by_nick[client.nickname] = client 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) 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]: def get_client_by_name(self, name: str) -> Optional[Client]:
return self.clients_by_nick.get(name) return self.clients_by_nick.get(name)

Loading…
Cancel
Save