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] [flake8]
ignore = E203
max-line-length = 100 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 typing import Dict
from paircd.client import Client from paircd.client import Client
from paircd.message import Message
@dataclass @dataclass
@ -22,6 +23,9 @@ class Channel:
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)

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

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

+ 1
- 2
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
@ -29,4 +28,4 @@ class NickHandler(CommandHandler):
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
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 logging import WARN
from typing import Dict 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
@ -11,12 +11,14 @@ from paircd.handler.join import JoinHandler
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,
NickHandler, NickHandler,
PrivmsgHandler, PrivmsgHandler,
UserHandler, UserHandler,
WhoHandler,
] ]
CMD_HANDLERS: Dict[str, CommandHandler] = {} 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: 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) 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: 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]}"

+ 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: 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: def remove_client_by_name(self, name: str) -> None:

Loading…
Cancel
Save