kopia lustrzana https://github.com/cirospaciari/socketify.py
bug fixes and chat example
rodzic
021dda7d2a
commit
0bcee69d21
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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>
|
|
@ -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()
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Ładowanie…
Reference in New Issue