Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

138 lines
4.1KB

  1. import asyncio
  2. import collections
  3. import datetime
  4. import http
  5. import json
  6. import uuid
  7. from functools import partial
  8. from pathlib import Path
  9. import websockets
  10. rooms = collections.defaultdict(dict)
  11. class PicoProtocol(websockets.WebSocketServerProtocol):
  12. def serve_file(self, path):
  13. document = Path(__file__, '..', Path(path).name).resolve()
  14. if not document.is_file():
  15. document = Path(__file__, '..', 'pico.html').resolve()
  16. content_type = 'text/html; charset=utf-8'
  17. elif path.endswith('.js'):
  18. content_type = 'application/javascript; charset=utf-8'
  19. elif path.endswith('.css'):
  20. content_type = 'text/css; charset=utf-8'
  21. elif path.endswith('.svg'):
  22. content_type = 'image/svg+xml; charset=utf-8'
  23. else:
  24. content_type = 'text/plain; charset=utf-8'
  25. return (
  26. http.HTTPStatus.OK,
  27. [('Content-Type', content_type)],
  28. document.read_bytes(),
  29. )
  30. def random_redirect(self):
  31. new_path = str(uuid.uuid4()).split('-')[1]
  32. return (
  33. http.HTTPStatus.FOUND,
  34. [('Location', new_path)],
  35. b'',
  36. )
  37. async def process_request(self, path, request_headers):
  38. if path == '/':
  39. return self.random_redirect()
  40. if request_headers.get('Upgrade') != 'websocket':
  41. return self.serve_file(path)
  42. return await super().process_request(path, request_headers)
  43. async def send_json_many(targets, **data):
  44. for websocket in list(targets):
  45. await send_json(websocket, **data)
  46. async def send_json(websocket, **data):
  47. try:
  48. await websocket.send(json.dumps(data))
  49. except websockets.exceptions.ConnectionClosed:
  50. pass
  51. async def recv_json(websocket):
  52. try:
  53. return json.loads(await websocket.recv())
  54. except websockets.exceptions.ConnectionClosed:
  55. return {'kind': 'logout'}
  56. except json.decoder.JSONDecodeError:
  57. return {}
  58. async def handle(ws, path, server_name):
  59. room = rooms[path]
  60. usernames = room.keys()
  61. sockets = room.values()
  62. username = None
  63. while True:
  64. data = await recv_json(ws)
  65. ts = datetime.datetime.now().isoformat() + 'Z'
  66. emit = partial(send_json_many, kind=data['kind'], ts=ts)
  67. broadcast = partial(emit, targets=sockets)
  68. reply = partial(emit, targets=[ws])
  69. error = partial(reply, kind='error')
  70. if 'kind' not in data:
  71. await error(value='Message without kind is invalid')
  72. elif data['kind'] == 'login':
  73. username = data['value']
  74. if not username:
  75. await error(value='Username not allowed')
  76. break
  77. if username in usernames:
  78. await error(value='Username taken')
  79. break
  80. room[username] = ws
  81. online = list(usernames)
  82. await reply(kind='state', username=username)
  83. await broadcast(kind='state', online=online)
  84. elif username not in room:
  85. await error(value='Login required')
  86. break
  87. elif data['kind'] == 'logout':
  88. del room[username]
  89. online = list(usernames)
  90. await broadcast(kind='state', online=online)
  91. break
  92. else:
  93. value = data.get('value')
  94. if 'target' in data:
  95. recipients = {username, data['target']}
  96. targets = {v for k, v in room.items() if k in recipients}
  97. await broadcast(source=username, value=value, targets=targets)
  98. else:
  99. await broadcast(source=username, value=value)
  100. async def start_server(host, port, server_name):
  101. bound_handle = partial(handle, server_name=server_name)
  102. bound_serve = partial(websockets.serve, create_protocol=PicoProtocol)
  103. return await bound_serve(bound_handle, host, port)
  104. if __name__ == '__main__':
  105. host, port = 'localhost', 9753
  106. loop = asyncio.get_event_loop()
  107. loop.run_until_complete(start_server(host, port, 'PicoChat'))
  108. print(f'Running on {host}:{port}')
  109. loop.run_forever()