Browse Source

Implement channel topics and fix join

master
Forest Belton 2 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:
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}")

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

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

+ 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:
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")

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

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

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

+ 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.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,
]

+ 7
- 1
paircd/reply.py View File

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

+ 3
- 3
paircd/server.py View File

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

Loading…
Cancel
Save