Browse Source

Implement WHO command

master
Forest Belton 2 years ago
parent
commit
c8805d0a65
13 changed files with 102 additions and 31 deletions
  1. +1
    -0
      .flake8
  2. +6
    -2
      paircd/channel.py
  3. +8
    -0
      paircd/client.py
  4. +4
    -5
      paircd/handler/join.py
  5. +1
    -2
      paircd/handler/nick.py
  6. +4
    -8
      paircd/handler/privmsg.py
  7. +2
    -3
      paircd/handler/user.py
  8. +48
    -0
      paircd/handler/who.py
  9. +4
    -2
      paircd/handlers.py
  10. +0
    -7
      paircd/log.py
  11. +1
    -1
      paircd/message.py
  12. +22
    -0
      paircd/reply.py
  13. +1
    -1
      paircd/server.py

+ 1
- 0
.flake8 View File

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

+ 6
- 2
paircd/channel.py View File

@ -3,6 +3,7 @@ from dataclasses import dataclass, field
from typing import Dict
from paircd.client import Client
from paircd.message import Message
@dataclass
@ -22,6 +23,9 @@ class Channel:
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)

+ 8
- 0
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
@ -20,8 +22,14 @@ class Client:
def id(self) -> str:
return f"{self.nickname}!{self.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())

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

+ 1
- 2
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
@ -29,4 +28,4 @@ class NickHandler(CommandHandler):
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
f":{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)

+ 4
- 2
paircd/handlers.py View File

@ -1,9 +1,9 @@
from logging import WARN
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
@ -11,12 +11,14 @@ from paircd.handler.join import JoinHandler
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,
NickHandler,
PrivmsgHandler,
UserHandler,
WhoHandler,
]
CMD_HANDLERS: Dict[str, CommandHandler] = {}
@ -30,6 +32,6 @@ 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)

+ 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/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]}"

+ 22
- 0
paircd/reply.py View File

@ -0,0 +1,22 @@
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_ENDOFWHOIS = reply_fn(318, "{0} :End of /WHOIS list")
RPL_WHOREPLY = reply_fn(352, "{0} {1} {2} {3} {3} {4} {5} :{6} {7}")
RPL_ENDOFWHO = reply_fn(315, "{0} :End of /WHO list")

+ 1
- 1
paircd/server.py View File

@ -17,7 +17,7 @@ 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:

Loading…
Cancel
Save