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.

105 lines
3.3 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 QUIT, 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 self.quit(server, "Connection reset by peer")
  42. async def quit(self, server: Any, msg: str) -> None:
  43. if self.closed:
  44. return
  45. quit_msg = QUIT(msg, prefix=self.id())
  46. for client in server.clients_by_nick.values():
  47. client.write_message(quit_msg)
  48. for channel_name in self.channels:
  49. channel = server.get_channel_by_name(channel_name, create=False)
  50. if channel is None:
  51. continue
  52. channel.remove_client_by_nick(self.nickname)
  53. server.remove_client_by_name(self.nickname)
  54. await self.close()
  55. async def close(self) -> None:
  56. if self.closed:
  57. return
  58. self.closed = True
  59. if not self.writer.is_closing():
  60. self.writer.close()
  61. await self.writer.wait_closed()
  62. def write_message(self, message: Message) -> None:
  63. self.msg_queue.put_nowait(message.encode())
  64. def register(self) -> None:
  65. if self.registered:
  66. return
  67. if not (self.nickname and self.username and self.realname):
  68. return
  69. self.registered = True
  70. self.log("registered")
  71. self.write_message(RPL_WELCOME(self.nickname, self.id()))
  72. self.write_message(RPL_YOURHOST(self.nickname, "localhost", "paircd-0.0.1"))
  73. # TODO: Pull timestamp from server instance
  74. self.write_message(RPL_CREATED(self.nickname, datetime.utcnow()))
  75. # TODO: Display list of supported user & channel modes
  76. self.write_message(
  77. RPL_MYINFO(self.nickname, "localhost", "paircd-0.0.1", "", "")
  78. )
  79. def add_mode(self, mode: str) -> None:
  80. self.modes.add(mode)
  81. def get_mode_settings(self) -> str:
  82. return f"+{''.join(sorted(self.modes))}"
  83. def remove_mode(self, mode: str) -> None:
  84. self.modes.remove(mode)