bug fixes and chat example

pull/39/head
Ciro 2022-11-17 11:03:38 -03:00
rodzic 021dda7d2a
commit 0bcee69d21
7 zmienionych plików z 473 dodań i 12 usunięć

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -0,0 +1,82 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<!-- This file has been downloaded from bootdey.com @bootdey on twitter -->
<!-- All snippets are MIT license http://bootdey.com/license -->
<title>socketify.py | chat</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://code.jquery.com/jquery-1.10.2.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="/assets/styles.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/2.1.0/showdown.min.js"></script>
<script defer src="/assets/scripts.js"></script>
</head>
<body>
<main class="content">
<div class="container p-0">
<h1 class="h3 mb-3"> Socketify.py Chat</h1>
<div class="card">
<div class="row g-0">
<div class="col-12 col-lg-5 col-xl-3 border-right">
<div class="px-4 d-md-block">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<br/>
Chat Rooms
</div>
</div>
</div>
<a href="#" class="list-group-item list-group-item-action border-0">
<div class="d-flex align-items-start">
<div class="flex-grow-1 ml-3" onclick="open_room('general')">
#general
<div class="small"><span class="fas fa-circle chat-online"></span> Be nice with everyone</div>
</div>
</div>
</a>
<a href="#" class="list-group-item list-group-item-action border-0">
<div class="d-flex align-items-start">
<div class="flex-grow-1 ml-3" onclick="open_room('open-source')">
#open-source
<div class="small"><span class="fas fa-circle chat-online"></span> Talk about open source</div>
</div>
</div>
</a>
<a href="#" class="list-group-item list-group-item-action border-0">
<div class="d-flex align-items-start">
<div class="flex-grow-1 ml-3" onclick="open_room('reddit')">
#reddit
<div class="small"><span class="fas fa-circle chat-online"></span> Tell about your reddit experience</div>
</div>
</div>
</a>
<hr class="d-block d-lg-none mt-1 mb-0">
</div>
<div class="col-12 col-lg-7 col-xl-9">
<div class="position-relative">
<div class="chat-messages p-4">
Please select a chat room
</div>
</div>
<div class="flex-grow-0 py-3 px-4 border-top">
<div class="input-group">
<input maxlength="1024" id="chat-message" type="text" class="form-control" placeholder="Type your message, you can use markdown ;)" onkeyup="send_message(event)">
<button class="btn btn-primary" onclick="send_message()">Send</button>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
</body>
</html>

Wyświetl plik

@ -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()

Wyświetl plik

@ -1,7 +1,6 @@
from socketify import App, AppOptions, AppListenOptions from socketify import App, AppOptions, AppListenOptions
import asyncio import asyncio
from datetime import datetime from datetime import datetime, timedelta
from datetime import timedelta
app = App() app = App()

Wyświetl plik

@ -98,17 +98,22 @@ def static_route(app, route, directory):
url = req.get_url() url = req.get_url()
res.grab_aborted_handler() res.grab_aborted_handler()
url = url[len(route)::] url = url[len(route)::]
if url.endswith("/"):
if url.startswith("/"): if url.startswith("/"):
url = url[1::] url = url[1:-1]
filename = path.join(path.realpath(directory), url) else:
url = url[:-1]
elif url.startswith("/"):
url = url[1:]
filename = path.join(path.realpath(directory), url)
if not in_directory(filename, directory): if not in_directory(filename, directory):
res.write_status(404).end_without_body() res.write_status(404).end_without_body()
return return
res.run_async(sendfile(res, req, filename)) res.run_async(sendfile(res, req, filename))
if route.startswith("/"): if route.endswith("/"):
route = route[1::] route = route[:-1]
app.get("%s/*" % route, route_handler) app.get("%s/*" % route, route_handler)

Wyświetl plik

@ -10,7 +10,7 @@ from os import path
import platform import platform
import signal import signal
from threading import Thread, local, Lock from threading import Thread, local, Lock
import time import uuid
from urllib.parse import parse_qs, quote_plus, unquote_plus from urllib.parse import parse_qs, quote_plus, unquote_plus
from .loop import Loop from .loop import Loop
@ -775,7 +775,7 @@ class WebSocket:
topics = [] topics = []
def copy_topics(topic): def copy_topics(topic):
topics.append(value) topics.append(topic)
self.for_each_topic(copy_topics) self.for_each_topic(copy_topics)
return topics return topics
@ -1062,7 +1062,7 @@ class AppRequest:
url = self.get_url() url = self.get_url()
query = self.get_full_url()[len(url) :] query = self.get_full_url()[len(url) :]
if full_url.startswith("?"): if query.startswith("?"):
query = query[1:] query = query[1:]
self._query = parse_qs(query, encoding="utf-8") self._query = parse_qs(query, encoding="utf-8")
return self._query return self._query
@ -1612,7 +1612,7 @@ class AppResponse:
user_data_ptr = ffi.NULL user_data_ptr = ffi.NULL
if not user_data is None: if not user_data is None:
_id = uuid.uuid4() _id = uuid.uuid4()
user_data_ptr = (ffi.new_handle(user_data), _id) user_data_ptr = ffi.new_handle((user_data, _id))
# keep alive data # keep alive data
SocketRefs[_id] = user_data_ptr SocketRefs[_id] = user_data_ptr