4 Commits

Author SHA1 Message Date
  Forest Belton 4fc22a1722 Implement stubbed MODE handler 2 years ago
  Forest Belton 49278f9432 Fix format of WHO reply 2 years ago
  Forest Belton c8805d0a65 Implement WHO command 2 years ago
  Forest Belton 8824a465ce Update channel indexes when user changes nickname 2 years ago
16 changed files with 168 additions and 39 deletions
Split View
  1. +1
    -0
      .flake8
  2. +13
    -3
      paircd/channel.py
  3. +11
    -1
      paircd/client.py
  4. +3
    -2
      paircd/command_handler.py
  5. +4
    -5
      paircd/handler/join.py
  6. +27
    -0
      paircd/handler/mode.py
  7. +11
    -4
      paircd/handler/nick.py
  8. +4
    -8
      paircd/handler/privmsg.py
  9. +2
    -3
      paircd/handler/user.py
  10. +48
    -0
      paircd/handler/who.py
  11. +15
    -3
      paircd/handlers.py
  12. +0
    -7
      paircd/log.py
  13. +1
    -1
      paircd/main.py
  14. +1
    -1
      paircd/message.py
  15. +23
    -0
      paircd/reply.py
  16. +4
    -1
      paircd/server.py

+ 1
- 0
.flake8 View File

@ -1,2 +1,3 @@
[flake8]
ignore = E203
max-line-length = 100

+ 13
- 3
paircd/channel.py View File

@ -1,8 +1,9 @@
from asyncio import Queue
from dataclasses import dataclass, field
from typing import Dict
from typing import Dict, List
from paircd.client import Client
from paircd.message import Message
@dataclass
@ -14,11 +15,20 @@ class Channel:
def add_client(self, client: Client) -> None:
self.clients_by_nick[client.nickname] = client
def remove_client_by_nick(self, nick: str) -> None:
del self.clients_by_nick[nick]
async def process(self) -> None:
while True:
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)
def get_modes(self) -> List[str]:
return []

+ 11
- 1
paircd/client.py View File

@ -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
@ -18,10 +20,18 @@ class Client:
channels: Set[str] = field(default_factory=set)
def id(self) -> str:
return f"{self.nickname}!{self.username}@{self.hostname}"
nickname = self.nickname or "<unknown>"
username = self.username or "<unknown>"
return f"{nickname}!{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())

+ 3
- 2
paircd/command_handler.py View File

@ -1,4 +1,5 @@
from abc import abstractmethod, ABC
from typing import Optional
from paircd.client import Client
from paircd.message import Message
@ -7,9 +8,9 @@ from paircd.server import Server
class CommandHandler(ABC):
cmd: str
argc: int
argc: Optional[int]
def __init__(self, cmd: str, argc: int) -> None:
def __init__(self, cmd: str, argc: Optional[int] = None) -> None:
self.cmd = cmd
self.argc = argc

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

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

+ 27
- 0
paircd/handler/mode.py View File

@ -0,0 +1,27 @@
import logging
from paircd.reply import RPL_CHANNELMODEIS
from paircd.client import Client
from paircd.command_handler import CommandHandler
from paircd.message import Message
from paircd.server import Server
class ModeHandler(CommandHandler):
def __init__(self) -> None:
super().__init__("MODE")
async def handle(self, server: Server, client: Client, msg: Message) -> None:
name = msg.args[0]
if not name.startswith("#"):
client.log("TODO: implement user mode queries", level=logging.WARN)
return
channel = server.get_channel_by_name(name)
modes = "".join(channel.get_modes())
if modes != "":
modes = f"+{modes}"
client.log("TODO: implement channel modes", level=logging.WARN)
msg = RPL_CHANNELMODEIS(client.nickname, name, modes, "")
client.write_message(msg)

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

@ -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
@ -12,13 +11,21 @@ class NickHandler(CommandHandler):
async def handle(self, server: Server, client: Client, msg: Message) -> None:
nickname = msg.args[0]
# Remove stale references if they exist
if client.nickname:
del server.clients_by_nick[client.nickname]
# TODO: Update all channel references
server.remove_client_by_name(client.nickname)
for channel_name in client.channels:
channel = server.get_channel_by_name(channel_name)
channel.remove_client_by_nick(client.nickname)
client.nickname = nickname
# Add references for client
server.add_client(client)
for channel_name in client.channels:
channel = server.get_channel_by_name(channel_name)
channel.remove_client_by_nick(client.nickname)
if client.username and client.realname:
client.registered = True
log_client(client, "registered")
client.log("registered")

+ 4
- 8
paircd/handler/privmsg.py View File

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

+ 2
- 3
paircd/handler/user.py View File

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

+ 48
- 0
paircd/handler/who.py View File

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

+ 15
- 3
paircd/handlers.py View File

@ -3,20 +3,23 @@ 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
from paircd.handler.join import JoinHandler
from paircd.handler.mode import ModeHandler
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,
ModeHandler,
NickHandler,
PrivmsgHandler,
UserHandler,
WhoHandler,
]
CMD_HANDLERS: Dict[str, CommandHandler] = {}
@ -30,6 +33,15 @@ 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)
handler = CMD_HANDLERS[msg.cmd]
if handler.argc is not None and handler.argc != len(msg.args):
client.log(
"got {len(msg.args)} arguments for {msg.cmd}, expected {handler.argc}",
level=WARN,
)
return
await handler.handle(server, client, msg)

+ 0
- 7
paircd/log.py View File

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

+ 1
- 1
paircd/main.py View File

@ -45,7 +45,7 @@ async def main() -> None:
reuse_port=True,
)
logger.info(f"Listening on {bind_addr}:{port}...")
logger.info(f"Listening on {bind_addr}:{port}")
async with server:
await server.serve_forever()

+ 1
- 1
paircd/message.py View File

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

+ 23
- 0
paircd/reply.py View File

@ -0,0 +1,23 @@
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_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_WHOREPLY = reply_fn(352, "{0} {1} {2} {3} {4} {5} :{6} {7}")

+ 4
- 1
paircd/server.py View File

@ -17,5 +17,8 @@ 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:
del self.clients_by_nick[name]

Loading…
Cancel
Save