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
import asyncio
from datetime import datetime
from datetime import timedelta
from datetime import datetime, timedelta
app = App()

Wyświetl plik

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

Wyświetl plik

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