diff --git a/examples/chat/assets/scripts.js b/examples/chat/assets/scripts.js
new file mode 100644
index 0000000..24210d3
--- /dev/null
+++ b/examples/chat/assets/scripts.js
@@ -0,0 +1,192 @@
+
+function get_room(name, token, on_history, on_message){
+
+ const ws = new WebSocket(`ws://localhost:3000?room=${name}&token=${token}`);
+ var status = 'pending';
+ const connection = new Promise((resolve)=>{
+ ws.onopen = function(){
+ status = 'connected';
+ resolve(true)
+ }
+ ws.onerror = function(){
+ status = 'error';
+ resolve(false)
+ }
+ });
+
+ ws.onclose = function(){
+ status = 'closed';
+ }
+ ws.onmessage = (event) => {
+ const message = JSON.parse(event.data);
+ if(message instanceof Array){
+ on_history(message)
+ }else{
+ on_message(message)
+ }
+ }
+
+ return {
+ send(message){
+ ws.send(JSON.stringify(message));
+ },
+ wait_connection(){
+ return connection;
+ },
+ is_connected(){
+ return status === 'connected';
+ },
+ close(){
+ ws.close()
+ }
+ }
+}
+
+function get_utc_date(){
+ return new Date().toLocaleTimeString('en-US', { hour12:false, hour: '2-digit', minute: '2-digit', timeZone: 'UTC', timeZoneName: 'short' })
+}
+
+function get_session() {
+ let name = "session=";
+ let decodedCookie = decodeURIComponent(document.cookie);
+ let ca = decodedCookie.split(';');
+ for (let i = 0; i < ca.length; i++) {
+ let c = ca[i];
+ while (c.charAt(0) == ' ') {
+ c = c.substring(1);
+ }
+ if (c.indexOf(name) == 0) {
+ return c.substring(name.length, c.length);
+ }
+ }
+ return "";
+}
+
+async function get_user() {
+ const session = get_session()
+ if (!session) return null;
+ try {
+ const response = await fetch('https://api.github.com/user', {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${session}`,
+ 'Accept': 'application/json'
+ }
+ })
+ return await response?.json()
+ } catch (err) {
+ console.error(err);
+ return null;
+ }
+}
+
+function request_login(){
+ return document.location.href = 'https://github.com/login/oauth/authorize?scope=user:email&client_id=9481803176fb73d616b7';
+}
+let current_room = null;
+let last_room_name = 'general';
+async function send_message(event){
+ if(event && event.key?.toLowerCase() !== 'enter') return;
+
+ const message = document.querySelector("#chat-message");
+ await current_room?.wait_connection();
+ if(current_room?.is_connected()){
+ current_room.send({
+ text: message.value,
+ datetime: get_utc_date()
+ })
+ //clean
+ message.value = ''
+ }else {
+ await open_room(last_room_name)
+ send_message();
+ }
+}
+
+async function open_room(name){
+ last_room_name = name;
+ const user = await get_user();
+ if(!user){
+ return request_login();
+ }
+ const chat = document.querySelector('.chat-messages');
+
+ const room = get_room(name, get_session(), (history)=> {
+ //clear
+ chat.innerHTML = '';
+ //add messages
+ for(let message of history){
+ chat.appendChild(format_message(message, user));
+ }
+ chat.scroll(0, chat.scrollHeight)
+ }, (message)=>{
+ //add message
+ chat.appendChild(format_message(message, user));
+
+ //trim size
+ while(chat.childNodes.length > 100){
+ chat.firstChild.remove();
+ }
+ chat.scroll(0, chat.scrollHeight)
+ });
+ await room.wait_connection()
+ current_room = room;
+}
+const markdown = new showdown.Converter({simpleLineBreaks: true,openLinksInNewWindow: true, emoji: true, ghMentions: true, tables: true, strikethrough: true, tasklists: true});
+
+function format_message(message, user){
+ const message_element = document.createElement("div");
+ if(message.login === user.login){
+ message.name = 'You';
+ message_element.classList.add('chat-message-right');
+ }else{
+ message_element.classList.add('chat-message-left');
+ }
+
+ message_element.classList.add('pb-4');
+
+ const header = document.createElement("div");
+ image = new Image(40, 40);
+ image.src = message.avatar_url;
+ image.classList.add('rounded-circle');
+ image.classList.add('mr-1');
+ image.alt = message.name;
+
+ const date = document.createElement("div");
+ date.classList.add('text-muted');
+ date.classList.add('small');
+ date.classList.add('text-nowrap');
+ date.classList.add('mt-2');
+ date.textContent = message.datetime;
+ header.addEventListener("click", ()=> {
+ window.open(message.html_url, '_blank').focus();
+ });
+ header.appendChild(image);
+ header.appendChild(date);
+
+ message_element.appendChild(header);
+
+ const body = document.createElement("div");
+ body.classList.add('flex-shrink-1');
+ body.classList.add('bg-light');
+ body.classList.add('rounded');
+ body.classList.add('py-2');
+ body.classList.add('px-3');
+ body.classList.add('mr-3');
+
+ const author = document.createElement("div")
+ author.classList.add('font-weight-bold');
+ author.classList.add('mb-1');
+ author.textContent = message.name;
+
+
+ body.appendChild(author);
+ const content = document.createElement("div");
+ content.innerHTML = markdown.makeHtml(message.text);
+ body.appendChild(content);
+
+ message_element.appendChild(body);
+
+ return message_element;
+}
diff --git a/examples/chat/assets/styles.css b/examples/chat/assets/styles.css
new file mode 100644
index 0000000..7ba9ea0
--- /dev/null
+++ b/examples/chat/assets/styles.css
@@ -0,0 +1,45 @@
+body{margin-top:20px;}
+
+.chat-online {
+ color: #34ce57
+}
+
+.chat-offline {
+ color: #e4606d
+}
+
+.chat-messages {
+ display: flex;
+ flex-direction: column;
+ max-height: 800px;
+ overflow-y: scroll
+}
+
+.chat-message-left,
+.chat-message-right {
+ display: flex;
+ flex-shrink: 0
+}
+
+.chat-message-left {
+ margin-right: auto
+}
+
+.chat-message-right {
+ flex-direction: row-reverse;
+ margin-left: auto
+}
+.py-3 {
+ padding-top: 1rem!important;
+ padding-bottom: 1rem!important;
+}
+.px-4 {
+ padding-right: 1.5rem!important;
+ padding-left: 1.5rem!important;
+}
+.flex-grow-0 {
+ flex-grow: 0!important;
+}
+.border-top {
+ border-top: 1px solid #dee2e6!important;
+}
\ No newline at end of file
diff --git a/examples/chat/index.html b/examples/chat/index.html
new file mode 100644
index 0000000..583fe48
--- /dev/null
+++ b/examples/chat/index.html
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+ socketify.py | chat
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Socketify.py Chat
+
+
+
+
+
+
+
+ Please select a chat room
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/chat/index.py b/examples/chat/index.py
new file mode 100644
index 0000000..01c3214
--- /dev/null
+++ b/examples/chat/index.py
@@ -0,0 +1,138 @@
+from socketify import App, sendfile, CompressOptions, OpCode
+import aiohttp
+import json
+from datetime import datetime, timedelta
+
+app = App()
+def get_welcome_message(room):
+ return {
+ "username": "@cirospaciari/socketify.py",
+ "html_url": "https://www.github.com/cirospaciari/socketify.py",
+ "avatar_url": "https://raw.githubusercontent.com/cirospaciari/socketify.py/main/misc/big_logo.png",
+ "datetime": "",
+ "name": "socketify.py",
+ "text": f"Welcome to chat room #{room} :heart: be nice! :tada:"
+}
+RoomsHistory = {
+ "general": [get_welcome_message("general")],
+ "open-source": [get_welcome_message("open-source")],
+ "reddit": [get_welcome_message("reddit")]
+}
+
+# send home page index.html
+async def home(res, req):
+ # Get Code for GitHub Auth
+ code = req.get_query("code")
+ if code is not None:
+ try:
+ # Get AccessToken for Auth
+ async with aiohttp.ClientSession() as session:
+ async with session.post(
+ "https://github.com/login/oauth/access_token",
+ data={
+ "client_id": "9481803176fb73d616b7",
+ "client_secret": "???",
+ "code": code,
+ },
+ headers={"Accept": "application/json"},
+ ) as response:
+ user = await response.json()
+ session = user.get("access_token", None)
+ res.set_cookie(
+ "session",
+ session,
+ {
+ "path": "/",
+ "httponly": False,
+ "expires": datetime.utcnow() + timedelta(minutes=30),
+ },
+ )
+ except Exception as error:
+ print(str(error)) # login fail
+
+ # sends the whole file with 304 and bytes range support
+ await sendfile(res, req, "./index.html")
+
+
+
+async def ws_upgrade(res, req, socket_context):
+ key = req.get_header("sec-websocket-key")
+ protocol = req.get_header("sec-websocket-protocol")
+ extensions = req.get_header("sec-websocket-extensions")
+ token = req.get_query("token")
+ room = req.get_query("room")
+ if RoomsHistory.get(room, None) is None:
+ return res.write_status(403).end("invalid room")
+
+ try:
+ async with aiohttp.ClientSession() as session:
+ async with session.get(
+ "https://api.github.com/user",
+ headers={
+ "Accept": "application/json",
+ "Content-Type": "application/json",
+ "Authorization": f"Bearer {token}",
+ },
+ ) as response:
+ user_data = await response.json()
+ if user_data.get('login', None) is not None:
+ user_data["room"] = room
+ res.upgrade(key, protocol, extensions, socket_context, user_data)
+ else:
+ res.write_status(403).end("auth fail")
+ except Exception as error:
+ print(str(error)) # auth error
+ res.write_status(403).end("auth fail")
+
+
+def ws_open(ws):
+ user_data = ws.get_user_data()
+ room = user_data.get("room", "general")
+ ws.subscribe(room)
+
+ history = RoomsHistory.get(room, [])
+ if history:
+ ws.send(history, OpCode.TEXT)
+
+def ws_message(ws, message, opcode):
+ try:
+ message_data = json.loads(message)
+ text = message_data.get('text', None)
+ if text and len(text) < 1024 and message_data.get('datetime', None):
+ user_data = ws.get_user_data()
+ room = user_data.get("room", "general")
+
+ message_data.update(user_data)
+ history = RoomsHistory.get(room, [])
+ if history:
+ history.append(message_data)
+ if len(history) > 100:
+ history = history[::100]
+
+ #broadcast
+ ws.send(message_data, OpCode.TEXT)
+ ws.publish(room, message_data, OpCode.TEXT)
+
+ except:
+ pass
+
+app.get("/", home)
+# serve all files in assets folder under /assets/* route
+app.static("/assets", "./assets")
+
+app.ws(
+ "/*",
+ {
+ "compression": CompressOptions.SHARED_COMPRESSOR,
+ "max_payload_length": 64 * 1024,
+ "idle_timeout": 60 * 30,
+ "open": ws_open,
+ "message": ws_message,
+ "upgrade": ws_upgrade
+ },
+)
+app.listen(
+ 3000,
+ lambda config: print("Listening on port http://localhost:%d now\n" % config.port),
+)
+app.run()
diff --git a/examples/router_and_basics.py b/examples/router_and_basics.py
index 678d36a..90ebc19 100644
--- a/examples/router_and_basics.py
+++ b/examples/router_and_basics.py
@@ -1,7 +1,6 @@
from socketify import App, AppOptions, AppListenOptions
import asyncio
-from datetime import datetime
-from datetime import timedelta
+from datetime import datetime, timedelta
app = App()
diff --git a/src/socketify/helpers.py b/src/socketify/helpers.py
index 0837a23..a68ae4a 100644
--- a/src/socketify/helpers.py
+++ b/src/socketify/helpers.py
@@ -97,18 +97,23 @@ def static_route(app, route, directory):
def route_handler(res, req):
url = req.get_url()
res.grab_aborted_handler()
- url = url[len(route) : :]
- if url.startswith("/"):
- url = url[1::]
- filename = path.join(path.realpath(directory), url)
+ url = url[len(route)::]
+ if url.endswith("/"):
+ if url.startswith("/"):
+ url = url[1:-1]
+ else:
+ url = url[:-1]
+ elif url.startswith("/"):
+ url = url[1:]
+ filename = path.join(path.realpath(directory), url)
if not in_directory(filename, directory):
res.write_status(404).end_without_body()
return
res.run_async(sendfile(res, req, filename))
- if route.startswith("/"):
- route = route[1::]
+ if route.endswith("/"):
+ route = route[:-1]
app.get("%s/*" % route, route_handler)
diff --git a/src/socketify/socketify.py b/src/socketify/socketify.py
index 5d92894..4ccc625 100644
--- a/src/socketify/socketify.py
+++ b/src/socketify/socketify.py
@@ -10,7 +10,7 @@ from os import path
import platform
import signal
from threading import Thread, local, Lock
-import time
+import uuid
from urllib.parse import parse_qs, quote_plus, unquote_plus
from .loop import Loop
@@ -775,7 +775,7 @@ class WebSocket:
topics = []
def copy_topics(topic):
- topics.append(value)
+ topics.append(topic)
self.for_each_topic(copy_topics)
return topics
@@ -1062,7 +1062,7 @@ class AppRequest:
url = self.get_url()
query = self.get_full_url()[len(url) :]
- if full_url.startswith("?"):
+ if query.startswith("?"):
query = query[1:]
self._query = parse_qs(query, encoding="utf-8")
return self._query
@@ -1612,7 +1612,7 @@ class AppResponse:
user_data_ptr = ffi.NULL
if not user_data is None:
_id = uuid.uuid4()
- user_data_ptr = (ffi.new_handle(user_data), _id)
+ user_data_ptr = ffi.new_handle((user_data, _id))
# keep alive data
SocketRefs[_id] = user_data_ptr