python ircd using asyncio
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

92 lines
2.8 KiB

  1. from asyncio import StreamReader, StreamWriter, TimeoutError, Queue
  2. from asyncio.tasks import wait_for
  3. from dataclasses import dataclass, field
  4. from datetime import datetime
  5. from logging import log, INFO
  6. from typing import Any, Set
  7. from paircd.reply import RPL_CREATED, RPL_MYINFO, RPL_WELCOME, RPL_YOURHOST
  8. from paircd.message import Message
  9. @dataclass
  10. class Client:
  11. hostname: str
  12. reader: StreamReader
  13. writer: StreamWriter
  14. msg_queue: Queue = field(default_factory=Queue)
  15. nickname: str = ""
  16. username: str = ""
  17. realname: str = ""
  18. registered: bool = False
  19. away: str = ""
  20. closed = False
  21. modes: Set[str] = field(default_factory=set)
  22. channels: Set[str] = field(default_factory=set)
  23. def id(self) -> str:
  24. nickname = self.nickname or "<unknown>"
  25. username = self.username or "<unknown>"
  26. return f"{nickname}!{username}@{self.hostname}"
  27. def log(self, msg: str, level: int = INFO) -> None:
  28. log(level, f"{self.hostname} ({self.id()}) {msg}")
  29. async def write_until_closed(self, server: Any) -> None:
  30. while not self.closed:
  31. msg = None
  32. try:
  33. msg = await wait_for(self.msg_queue.get(), timeout=1.0)
  34. except TimeoutError:
  35. pass
  36. if msg is not None:
  37. self.writer.write(msg)
  38. try:
  39. await self.writer.drain()
  40. except ConnectionResetError:
  41. await server.disconnect_client(
  42. self.nickname, "Connection reset by peer"
  43. )
  44. async def close(self) -> None:
  45. if self.closed:
  46. return
  47. self.closed = True
  48. if not self.writer.is_closing():
  49. self.writer.close()
  50. await self.writer.wait_closed()
  51. def write_message(self, message: Message) -> None:
  52. self.msg_queue.put_nowait(message.encode())
  53. def register(self) -> None:
  54. if self.registered:
  55. return
  56. if not (self.nickname and self.username and self.realname):
  57. return
  58. self.registered = True
  59. self.log("registered")
  60. self.write_message(RPL_WELCOME(self.nickname, self.id()))
  61. self.write_message(RPL_YOURHOST(self.nickname, "localhost", "paircd-0.0.1"))
  62. # TODO: Pull timestamp from server instance
  63. self.write_message(RPL_CREATED(self.nickname, datetime.utcnow()))
  64. # TODO: Display list of supported user & channel modes
  65. self.write_message(
  66. RPL_MYINFO(self.nickname, "localhost", "paircd-0.0.1", "", "")
  67. )
  68. def add_mode(self, mode: str) -> None:
  69. self.modes.add(mode)
  70. def get_mode_settings(self) -> str:
  71. return f"+{''.join(sorted(self.modes))}"
  72. def remove_mode(self, mode: str) -> None:
  73. self.modes.remove(mode)