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
Unified 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] [flake8]
ignore = E203
max-line-length = 100 max-line-length = 100

+ 13
- 3
paircd/channel.py View File

@ -1,8 +1,9 @@
from asyncio import Queue from asyncio import Queue
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Dict
from typing import Dict, List
from paircd.client import Client from paircd.client import Client
from paircd.message import Message
@dataclass @dataclass
@ -14,11 +15,20 @@ class Channel:
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 remove_client_by_nick(self, nick: str) -> None:
del self.clients_by_nick[nick]
async def process(self) -> None: async def process(self) -> None:
while True: while True:
msg = await self.msg_queue.get() msg = await self.msg_queue.get()
for client in self.clients_by_nick.values(): for client in self.clients_by_nick.values():
# Don't broadcast client's messages back to themselves # 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 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 asyncio import StreamReader, StreamWriter, Queue
from dataclasses import dataclass, field from dataclasses import dataclass, field
from logging import log, INFO
from paircd.message import Message
from typing import Set from typing import Set
@ -18,10 +20,18 @@ class Client:
channels: Set[str] = field(default_factory=set) channels: Set[str] = field(default_factory=set)
def id(self) -> str: 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: async def write_forever(self) -> None:
while True: while True:
msg = await self.msg_queue.get() msg = await self.msg_queue.get()
self.writer.write(msg) self.writer.write(msg)
await self.writer.drain() 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 abc import abstractmethod, ABC
from typing import Optional
from paircd.client import Client from paircd.client import Client
from paircd.message import Message from paircd.message import Message
@ -7,9 +8,9 @@ from paircd.server import Server
class CommandHandler(ABC): class CommandHandler(ABC):
cmd: str 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.cmd = cmd
self.argc = argc self.argc = argc

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

@ -2,7 +2,6 @@ import logging
from paircd.client import Client from paircd.client import Client
from paircd.command_handler import CommandHandler from paircd.command_handler import CommandHandler
from paircd.log import log_client
from paircd.message import Message from paircd.message import Message
from paircd.server import Server from paircd.server import Server
@ -13,20 +12,20 @@ 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:
if not client.registered: if not client.registered:
log_client(client, "join: not registered", level=logging.WARN)
client.log("join: not registered", level=logging.WARN)
return return
channel_name = msg.args[0] channel_name = msg.args[0]
if not channel_name.startswith("#"): 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 return
channel = server.get_channel_by_name(channel_name) 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)
log_client(client, f"joined {channel_name}")
client.log(f"joined {channel_name}")
await channel.msg_queue.put( 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.client import Client
from paircd.command_handler import CommandHandler from paircd.command_handler import CommandHandler
from paircd.log import log_client
from paircd.message import Message from paircd.message import Message
from paircd.server import Server from paircd.server import Server
@ -12,13 +11,21 @@ class NickHandler(CommandHandler):
async def handle(self, server: Server, client: Client, msg: Message) -> None: async def handle(self, server: Server, client: Client, msg: Message) -> None:
nickname = msg.args[0] nickname = msg.args[0]
# Remove stale references if they exist
if client.nickname: 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 client.nickname = nickname
# Add references for client
server.add_client(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: if client.username and client.realname:
client.registered = True 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.client import Client
from paircd.command_handler import CommandHandler from paircd.command_handler import CommandHandler
from paircd.log import log_client
from paircd.message import Message from paircd.message import Message
from paircd.server import Server from paircd.server import Server
@ -14,19 +13,16 @@ class PrivmsgHandler(CommandHandler):
async def handle(self, server: Server, client: Client, msg: Message) -> None: async def handle(self, server: Server, client: Client, msg: Message) -> None:
recipient = msg.args[0] recipient = msg.args[0]
raw_msg = msg.args[1] 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(): for name, other_client in server.clients_by_nick.items():
if name == recipient: if name == recipient:
other_client.msg_queue.put_nowait(out)
other_client.write_message(out)
return return
for name, channel in server.channels_by_name.items(): for name, channel in server.channels_by_name.items():
if name == recipient: if name == recipient:
channel.msg_queue.put_nowait(out)
channel.write_message(out)
return 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.client import Client
from paircd.command_handler import CommandHandler from paircd.command_handler import CommandHandler
from paircd.log import log_client
from paircd.message import Message from paircd.message import Message
from paircd.server import Server from paircd.server import Server
@ -13,7 +12,7 @@ class UserHandler(CommandHandler):
async def handle(self, server: Server, client: Client, msg: Message) -> None: async def handle(self, server: Server, client: Client, msg: Message) -> None:
if client.registered: if client.registered:
log_client(client, "USER issued after registration", level=logging.WARN)
client.log("USER issued after registration", level=logging.WARN)
return return
client.username = msg.args[0] client.username = msg.args[0]
@ -21,4 +20,4 @@ class UserHandler(CommandHandler):
if client.nickname: if client.nickname:
client.registered = True 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.client import Client
from paircd.command_handler import CommandHandler from paircd.command_handler import CommandHandler
from paircd.log import log_client
from paircd.message import Message from paircd.message import Message
from paircd.server import Server from paircd.server import Server
from paircd.handler.join import JoinHandler from paircd.handler.join import JoinHandler
from paircd.handler.mode import ModeHandler
from paircd.handler.nick import NickHandler from paircd.handler.nick import NickHandler
from paircd.handler.privmsg import PrivmsgHandler from paircd.handler.privmsg import PrivmsgHandler
from paircd.handler.user import UserHandler from paircd.handler.user import UserHandler
from paircd.handler.who import WhoHandler
HANDLER_CLASSES = [ HANDLER_CLASSES = [
JoinHandler, JoinHandler,
ModeHandler,
NickHandler, NickHandler,
PrivmsgHandler, PrivmsgHandler,
UserHandler, UserHandler,
WhoHandler,
] ]
CMD_HANDLERS: Dict[str, CommandHandler] = {} 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: async def handle_cmd(server: Server, client: Client, msg: Message) -> None:
if msg.cmd not in CMD_HANDLERS: 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 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, reuse_port=True,
) )
logger.info(f"Listening on {bind_addr}:{port}...")
logger.info(f"Listening on {bind_addr}:{port}")
async with server: async with server:
await server.serve_forever() 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: if len(tokens) == 0:
raise ParsingError("Message has no command") 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: if cmd in EXPECTED_ARG_COUNT and EXPECTED_ARG_COUNT[cmd] != len(tokens) - 1:
raise ParsingError( raise ParsingError(
f"{cmd} had {len(tokens)-1} arguments, expected {EXPECTED_ARG_COUNT[cmd]}" 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: def get_channel_by_name(self, name: str) -> Channel:
if name not in self.channels_by_name: if name not in self.channels_by_name:
self.channels_by_name[name] = Channel(name=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] return self.channels_by_name[name]
def remove_client_by_name(self, name: str) -> None:
del self.clients_by_nick[name]

Loading…
Cancel
Save