Skip to content

Instantly share code, notes, and snippets.

@Jeffrey04
Created October 11, 2024 10:46
Show Gist options
  • Save Jeffrey04/63d0e675bd001634b083d7ad444d3052 to your computer and use it in GitHub Desktop.
Save Jeffrey04/63d0e675bd001634b083d7ad444d3052 to your computer and use it in GitHub Desktop.
ASGI - Websocket Chatroom
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
crossorigin="anonymous"
/>
<title>Test Chatroom</title>
</head>
<body>
<div class="container">
<div class="d-flex flex-column vh-100">
<h1 class="flex-shrink-0">Test chatroom</h1>
<div id="messages" class="flex-grow-1 overflow-y-scroll"></div>
<form id="send" class="flex-shrink-0">
<input
class="form-control form-control-lg"
type="text"
aria-label=".form-control-lg example"
/>
</form>
</div>
</div>
<script src=" https://cdn.jsdelivr.net/npm/[email protected]/chance.min.js "></script>
<script
type="text/javascript"
src="https://code.jquery.com/jquery-3.7.1.js"
></script>
<script src=" https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js "></script>
<script
src="https://cdn.jsdelivr.net/npm/@popperjs/[email protected]/dist/umd/popper.min.js"
integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r"
crossorigin="anonymous"
></script>
<script
src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js"
integrity="sha384-0pUGZvbkm6XF6gxjEnlmuGrJXVbNuzT9qBBavbLwCsOGabYfZo0T0to5eqruptLy"
crossorigin="anonymous"
></script>
<script id="message" type="text/template">
<div>
<p><strong><%= sender %>:</strong> <%= message %></p>
</div>
</script>
<script async type="text/javascript">
document.addEventListener("DOMContentLoaded", () => {
const ws = new WebSocket("/chat");
const sender = chance.name();
ws.addEventListener("message", (e) => {
document
.getElementById("messages")
.dispatchEvent(
new CustomEvent("update", { detail: JSON.parse(e.data) })
);
});
const elem = document.querySelector("#send input");
elem.setAttribute("placeholder", sender.concat(":"));
elem.focus();
document.getElementById("send").addEventListener("submit", (e) => {
e.preventDefault();
ws.send(
JSON.stringify({
sender,
message: $("input", e.target).val(),
})
);
e.target.querySelector("input").value = "";
});
document.getElementById("messages").addEventListener("update", (e) => {
const elem = new DOMParser().parseFromString(
_.template(document.getElementById("message").innerHTML)({
sender: e.detail.sender,
message: e.detail.message,
}),
"text/html"
).body.firstChild;
e.target.appendChild(elem);
e.target.lastElementChild.scrollIntoView(false);
jQuery(elem).hide().fadeIn();
});
});
</script>
</body>
</html>
import asyncio
import asyncio.selector_events
import contextlib
import json
import logging
import multiprocessing
import multiprocessing.managers
import random
import string
from functools import partial
from pathlib import Path
import uvicorn
from structlog import get_logger
queue = multiprocessing.Manager().Queue()
subscribers = {}
HTTP_REQUEST = "http.request"
HTTP_DISCONNECT = "http.disconnect"
WEBSOCKET_CONNECT = "websocket.connect"
WEBSOCKET_ACCEPT = "websocket.accept"
WEBSOCKET_RECEIVE = "websocket.receive"
WEBSOCKET_SEND = "websocket.send"
WEBSOCKET_DISCONNECT = "websocket.disconnect"
logger = get_logger()
logging.basicConfig()
def get_path(scope):
return scope["path"].lstrip(scope["root_path"])
async def application(scope, receive, send):
subscriber = "".join(random.choices(string.ascii_letters, k=7))
while event := await receive():
logger.info("Incoming event", **event, path=get_path(scope))
if event["type"] == HTTP_REQUEST and get_path(scope) == "/":
await index(send)
elif event["type"] == WEBSOCKET_CONNECT and get_path(scope) == "/chat":
subscribers[subscriber] = send
await accept(send)
elif event["type"] == WEBSOCKET_RECEIVE and get_path(scope) == "/chat":
asyncio.create_task(asyncio.to_thread(partial(queue.put, event["text"])))
elif event["type"] == HTTP_DISCONNECT:
break
elif event["type"] == WEBSOCKET_DISCONNECT:
del subscribers[subscriber]
break
async def consume():
while message := await asyncio.to_thread(partial(queue.get)):
for send in subscribers.values():
asyncio.create_task(
send(
{
"type": WEBSOCKET_SEND,
"text": message,
}
)
)
async def accept(send):
await send({"type": WEBSOCKET_ACCEPT})
await send(
{
"type": WEBSOCKET_SEND,
"text": json.dumps({"sender": "server", "message": "You are connected"}),
}
)
async def index(send):
with open(Path(__file__).parent / "index.html", "rb") as template:
await send(
{
"type": "http.response.start",
"status": 200,
}
)
await send({"type": "http.response.body", "body": template.read()})
async def main():
asyncio.create_task(consume())
server = uvicorn.Server(
uvicorn.Config(
app=application,
workers=4,
host="0.0.0.0",
port=8080,
)
)
await server.serve()
if __name__ == "__main__":
asyncio.run(main())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment