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
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
app = App()
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue