Update live type.
This commit is contained in:
parent
af1cf4f5ae
commit
db6d6c0106
123
src/snek/balancer.py
Normal file
123
src/snek/balancer.py
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
|
||||||
|
class LoadBalancer:
|
||||||
|
def __init__(self, backend_ports):
|
||||||
|
self.backend_ports = backend_ports
|
||||||
|
self.backend_processes = []
|
||||||
|
self.client_counts = [0] * len(backend_ports)
|
||||||
|
self.lock = asyncio.Lock()
|
||||||
|
|
||||||
|
async def start_backend_servers(self,port,workers):
|
||||||
|
for x in range(workers):
|
||||||
|
port += 1
|
||||||
|
process = await asyncio.create_subprocess_exec(
|
||||||
|
sys.executable,
|
||||||
|
sys.argv[0],
|
||||||
|
'backend',
|
||||||
|
str(port),
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE
|
||||||
|
)
|
||||||
|
port += 1
|
||||||
|
self.backend_processes.append(process)
|
||||||
|
print(f"Started backend server on port {(port-1)/port} with PID {process.pid}")
|
||||||
|
|
||||||
|
async def handle_client(self, reader, writer):
|
||||||
|
async with self.lock:
|
||||||
|
min_clients = min(self.client_counts)
|
||||||
|
server_index = self.client_counts.index(min_clients)
|
||||||
|
self.client_counts[server_index] += 1
|
||||||
|
backend = ('127.0.0.1', self.backend_ports[server_index])
|
||||||
|
try:
|
||||||
|
backend_reader, backend_writer = await asyncio.open_connection(*backend)
|
||||||
|
|
||||||
|
async def forward(r, w):
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
data = await r.read(1024)
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
w.write(data)
|
||||||
|
await w.drain()
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
w.close()
|
||||||
|
|
||||||
|
task1 = asyncio.create_task(forward(reader, backend_writer))
|
||||||
|
task2 = asyncio.create_task(forward(backend_reader, writer))
|
||||||
|
await asyncio.gather(task1, task2)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
finally:
|
||||||
|
writer.close()
|
||||||
|
async with self.lock:
|
||||||
|
self.client_counts[server_index] -= 1
|
||||||
|
|
||||||
|
async def monitor(self):
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
print("Connected clients per server:")
|
||||||
|
for i, count in enumerate(self.client_counts):
|
||||||
|
print(f"Server {self.backend_ports[i]}: {count} clients")
|
||||||
|
|
||||||
|
async def start(self, host='0.0.0.0', port=8081,workers=5):
|
||||||
|
await self.start_backend_servers(port,workers)
|
||||||
|
server = await asyncio.start_server(self.handle_client, host, port)
|
||||||
|
monitor_task = asyncio.create_task(self.monitor())
|
||||||
|
|
||||||
|
# Handle shutdown gracefully
|
||||||
|
try:
|
||||||
|
async with server:
|
||||||
|
await server.serve_forever()
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
# Terminate backend processes
|
||||||
|
for process in self.backend_processes:
|
||||||
|
process.terminate()
|
||||||
|
await asyncio.gather(*(p.wait() for p in self.backend_processes))
|
||||||
|
print("Backend processes terminated.")
|
||||||
|
|
||||||
|
async def backend_echo_server(port):
|
||||||
|
async def handle_echo(reader, writer):
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
data = await reader.read(1024)
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
writer.write(data)
|
||||||
|
await writer.drain()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
writer.close()
|
||||||
|
|
||||||
|
server = await asyncio.start_server(handle_echo, '127.0.0.1', port)
|
||||||
|
print(f"Backend echo server running on port {port}")
|
||||||
|
await server.serve_forever()
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
backend_ports = [8001, 8003, 8005, 8006]
|
||||||
|
# Launch backend echo servers
|
||||||
|
# Wait a moment for servers to start
|
||||||
|
lb = LoadBalancer(backend_ports)
|
||||||
|
await lb.start()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
if sys.argv[1] == 'backend':
|
||||||
|
port = int(sys.argv[2])
|
||||||
|
from snek.app import Application
|
||||||
|
snek = Application(port=port)
|
||||||
|
web.run_app(snek, port=port, host='127.0.0.1')
|
||||||
|
elif sys.argv[1] == 'sync':
|
||||||
|
from snek.sync import app
|
||||||
|
web.run_app(snek, port=port, host='127.0.0.1')
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
asyncio.run(main())
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("Shutting down...")
|
||||||
|
|
@ -30,7 +30,7 @@ class ChannelMessageService(BaseService):
|
|||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print(ex, flush=True)
|
print(ex, flush=True)
|
||||||
|
|
||||||
if await self.save(model):
|
if await super().save(model):
|
||||||
return model
|
return model
|
||||||
raise Exception(f"Failed to create channel message: {model.errors}.")
|
raise Exception(f"Failed to create channel message: {model.errors}.")
|
||||||
|
|
||||||
@ -50,6 +50,12 @@ class ChannelMessageService(BaseService):
|
|||||||
"username": user["username"],
|
"username": user["username"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async def save(self, model):
|
||||||
|
context = model.record
|
||||||
|
template = self.app.jinja2_env.get_template("message.html")
|
||||||
|
model["html"] = template.render(**context)
|
||||||
|
return await super().save(model)
|
||||||
|
|
||||||
async def offset(self, channel_uid, page=0, timestamp=None, page_size=30):
|
async def offset(self, channel_uid, page=0, timestamp=None, page_size=30):
|
||||||
results = []
|
results = []
|
||||||
offset = page * page_size
|
offset = page * page_size
|
||||||
|
@ -36,4 +36,4 @@ class ChatService(BaseService):
|
|||||||
self.services.notification.create_channel_message(channel_message_uid)
|
self.services.notification.create_channel_message(channel_message_uid)
|
||||||
)
|
)
|
||||||
|
|
||||||
return True
|
return channel_message
|
||||||
|
@ -5,8 +5,64 @@
|
|||||||
// The code seems to rely on some external dependencies like 'models.Message', 'app', and 'Schedule'. These should be imported or defined elsewhere in your application.
|
// The code seems to rely on some external dependencies like 'models.Message', 'app', and 'Schedule'. These should be imported or defined elsewhere in your application.
|
||||||
|
|
||||||
// MIT License: This is free software. Permission is granted to use, copy, modify, and/or distribute this software for any purpose with or without fee. The software is provided "as is" without any warranty.
|
// MIT License: This is free software. Permission is granted to use, copy, modify, and/or distribute this software for any purpose with or without fee. The software is provided "as is" without any warranty.
|
||||||
|
import {app} from '../app.js'
|
||||||
|
class MessageList extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
app.ws.addEventListener("update_message_text",(data)=>{
|
||||||
|
this.updateMessageText(data.data.message_uid,data.data.text)
|
||||||
|
})
|
||||||
|
app.ws.addEventListener("set_typing",(data)=>{
|
||||||
|
this.triggerGlow(data.data.user_uid)
|
||||||
|
|
||||||
class MessageListElement extends HTMLElement {
|
})
|
||||||
|
|
||||||
|
this.items = [];
|
||||||
|
}
|
||||||
|
updateMessageText(uid,text){
|
||||||
|
const messageDiv = this.querySelector("div[data-uid=\""+uid+"\"]")
|
||||||
|
if(!messageDiv){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const textElement = messageDiv.querySelector(".text")
|
||||||
|
textElement.innerText = text
|
||||||
|
textElement.style.display = text == '' ? 'none' : 'block'
|
||||||
|
|
||||||
|
}
|
||||||
|
triggerGlow(uid) {
|
||||||
|
let lastElement = null;
|
||||||
|
this.querySelectorAll(".avatar").forEach((el)=>{
|
||||||
|
const div = el.closest('a');
|
||||||
|
if(el.href.indexOf(uid)!=-1){
|
||||||
|
lastElement = el
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
if(lastElement){
|
||||||
|
lastElement.classList.add("glow")
|
||||||
|
setTimeout(()=>{
|
||||||
|
lastElement.classList.remove("glow")
|
||||||
|
},1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
set data(items) {
|
||||||
|
this.items = items;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
this.innerHTML = '';
|
||||||
|
|
||||||
|
//this.insertAdjacentHTML("beforeend", html);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('message-list', MessageList);
|
||||||
|
|
||||||
|
class MessageListElementOLD extends HTMLElement {
|
||||||
static get observedAttributes() {
|
static get observedAttributes() {
|
||||||
return ["messages"];
|
return ["messages"];
|
||||||
}
|
}
|
||||||
@ -167,4 +223,4 @@ class MessageListElement extends HTMLElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('message-list', MessageListElement);
|
//customElements.define('message-list', MessageListElement);
|
||||||
|
1
src/snek/static/online-users.js
Normal file
1
src/snek/static/online-users.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
28
src/snek/static/user-list.css
Normal file
28
src/snek/static/user-list.css
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
.user-list__item {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.user-list__item-avatar {
|
||||||
|
margin-right: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.user-list__item-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.user-list__item-name {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.user-list__item-text {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
.user-list__item-time {
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: gray;
|
||||||
|
}
|
59
src/snek/static/user-list.js
Normal file
59
src/snek/static/user-list.js
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
class UserList extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.users = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
set data(userArray) {
|
||||||
|
this.users = userArray;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
formatRelativeTime(timestamp) {
|
||||||
|
const now = new Date();
|
||||||
|
const msgTime = new Date(timestamp);
|
||||||
|
const diffMs = now - msgTime;
|
||||||
|
const minutes = Math.floor(diffMs / 60000);
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
const days = Math.floor(hours / 24);
|
||||||
|
|
||||||
|
if (days > 0) {
|
||||||
|
return `${msgTime.getHours().toString().padStart(2, '0')}:${msgTime.getMinutes().toString().padStart(2, '0')}, ${days} day${days > 1 ? 's' : ''} ago`;
|
||||||
|
} else if (hours > 0) {
|
||||||
|
return `${msgTime.getHours().toString().padStart(2, '0')}:${msgTime.getMinutes().toString().padStart(2, '0')}, ${hours} hour${hours > 1 ? 's' : ''} ago`;
|
||||||
|
} else {
|
||||||
|
return `${msgTime.getHours().toString().padStart(2, '0')}:${msgTime.getMinutes().toString().padStart(2, '0')}, ${minutes} min ago`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
this.innerHTML = '';
|
||||||
|
|
||||||
|
this.users.forEach(user => {
|
||||||
|
const html = `
|
||||||
|
<div class="user-list__item"
|
||||||
|
data-uid="${user.uid}"
|
||||||
|
data-color="${user.color}"
|
||||||
|
data-user_nick="${user.nick}"
|
||||||
|
data-created_at="${user.created_at}"
|
||||||
|
data-user_uid="${user.user_uid}">
|
||||||
|
|
||||||
|
<a class="user-list__item-avatar" style="background-color: ${user.color}; color: black;" href="/user/${user.uid}.html">
|
||||||
|
<img width="40px" height="40px" src="/avatar/${user.uid}.svg" alt="${user.nick}">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="user-list__item-content">
|
||||||
|
<div class="user-list__item-name" style="color: ${user.color};">${user.nick}</div>
|
||||||
|
<div class="user-list__item-time" data-created_at="${user.last_ping}">
|
||||||
|
<a href="/user/${user.uid}.html">profile</a>
|
||||||
|
<a href="/channel/${user.uid}.html">dm</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
this.insertAdjacentHTML("beforeend", html);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('user-list', UserList);
|
135
src/snek/sync.py
Normal file
135
src/snek/sync.py
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class DatasetWebSocketView:
|
||||||
|
def __init__(self):
|
||||||
|
self.ws = None
|
||||||
|
self.db = dataset.connect('sqlite:///snek.db')
|
||||||
|
self.setattr(self, "db", self.get)
|
||||||
|
self.setattr(self, "db", self.set)
|
||||||
|
)
|
||||||
|
super()
|
||||||
|
|
||||||
|
def format_result(self, result):
|
||||||
|
|
||||||
|
try:
|
||||||
|
return dict(result)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
return [dict(row) for row in result]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def send_str(self, msg):
|
||||||
|
return await self.ws.send_str(msg)
|
||||||
|
|
||||||
|
def get(self, key):
|
||||||
|
returnl loads(dict(self.db['_kv'].get(key=key)['value']))
|
||||||
|
|
||||||
|
def set(self, key, value):
|
||||||
|
return self.db['_kv'].upsert({'key': key, 'value': json.dumps(value)}, ['key'])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async def handle(self, request):
|
||||||
|
ws = web.WebSocketResponse()
|
||||||
|
await ws.prepare(request)
|
||||||
|
self.ws = ws
|
||||||
|
|
||||||
|
async for msg in ws:
|
||||||
|
if msg.type == aiohttp.WSMsgType.TEXT:
|
||||||
|
try:
|
||||||
|
data = json.loads(msg.data)
|
||||||
|
call_uid = data.get("call_uid")
|
||||||
|
method = data.get("method")
|
||||||
|
table_name = data.get("table")
|
||||||
|
args = data.get("args", {})
|
||||||
|
kwargs = data.get("kwargs", {})
|
||||||
|
|
||||||
|
|
||||||
|
function = getattr(self.db, method, None)
|
||||||
|
if table_name:
|
||||||
|
function = getattr(self.db[table_name], method, None)
|
||||||
|
|
||||||
|
print(method, table_name, args, kwargs,flush=True)
|
||||||
|
|
||||||
|
if function:
|
||||||
|
response = {}
|
||||||
|
try:
|
||||||
|
result = function(*args, **kwargs)
|
||||||
|
print(result)
|
||||||
|
response['result'] = self.format_result(result)
|
||||||
|
response["call_uid"] = call_uid
|
||||||
|
response["success"] = True
|
||||||
|
except Exception as e:
|
||||||
|
response["call_uid"] = call_uid
|
||||||
|
response["success"] = False
|
||||||
|
response["error"] = str(e)
|
||||||
|
response["traceback"] = traceback.format_exc()
|
||||||
|
|
||||||
|
if call_uid:
|
||||||
|
await self.send_str(json.dumps(response,default=str))
|
||||||
|
else:
|
||||||
|
await self.send_str(json.dumps({"status": "error", "error":"Method not found.","call_uid": call_uid}))
|
||||||
|
except Exception as e:
|
||||||
|
await self.send_str(json.dumps({"success": False,"call_uid": call_uid, "error": str(e), "error": str(e), "traceback": traceback.format_exc()},default=str))
|
||||||
|
elif msg.type == aiohttp.WSMsgType.ERROR:
|
||||||
|
print('ws connection closed with exception %s' % ws.exception())
|
||||||
|
|
||||||
|
return ws
|
||||||
|
|
||||||
|
class BroadCastSocketView:
|
||||||
|
def __init__(self):
|
||||||
|
self.ws = None
|
||||||
|
super()
|
||||||
|
|
||||||
|
def format_result(self, result):
|
||||||
|
|
||||||
|
try:
|
||||||
|
return dict(result)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
return [dict(row) for row in result]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def send_str(self, msg):
|
||||||
|
return await self.ws.send_str(msg)
|
||||||
|
|
||||||
|
def get(self, key):
|
||||||
|
returnl loads(dict(self.db['_kv'].get(key=key)['value']))
|
||||||
|
|
||||||
|
def set(self, key, value):
|
||||||
|
return self.db['_kv'].upsert({'key': key, 'value': json.dumps(value)}, ['key'])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async def handle(self, request):
|
||||||
|
ws = web.WebSocketResponse()
|
||||||
|
await ws.prepare(request)
|
||||||
|
self.ws = ws
|
||||||
|
app = request.app
|
||||||
|
app['broadcast_clients'].append(ws)
|
||||||
|
|
||||||
|
async for msg in ws:
|
||||||
|
if msg.type == aiohttp.WSMsgType.TEXT:
|
||||||
|
print(msg.data)
|
||||||
|
for client in app['broadcast_clients'] if not client == ws:
|
||||||
|
await client.send_str(msg.data)
|
||||||
|
elif msg.type == aiohttp.WSMsgType.ERROR:
|
||||||
|
print('ws connection closed with exception %s' % ws.exception())
|
||||||
|
app['broadcast_clients'].remove(ws)
|
||||||
|
return ws
|
||||||
|
|
||||||
|
|
||||||
|
app = web.Application()
|
||||||
|
view = DatasetWebSocketView()
|
||||||
|
app['broadcast_clients'] = []
|
||||||
|
app.router.add_get('/db', view.handle)
|
||||||
|
app.router.add_get('/broadcast', sync_view.handle)
|
||||||
|
|
@ -16,6 +16,10 @@
|
|||||||
<script src="/html-frame.js" type="module"></script>
|
<script src="/html-frame.js" type="module"></script>
|
||||||
<script src="/app.js" type="module"></script>
|
<script src="/app.js" type="module"></script>
|
||||||
<script src="/file-manager.js" type="module"></script>
|
<script src="/file-manager.js" type="module"></script>
|
||||||
|
<script src="/user-list.js"></script>
|
||||||
|
<script src="/message-list.js" type="module"></script>
|
||||||
|
<link rel="stylesheet" href="/user-list.css">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/base.css">
|
<link rel="stylesheet" href="/base.css">
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
|
117
src/snek/templates/online.html
Normal file
117
src/snek/templates/online.html
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
<style>
|
||||||
|
/* Ensure the dialog appears centered when open as modal */
|
||||||
|
#online-users {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
background-color: #111; /* Deep black */
|
||||||
|
color: #f1f1f1;
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.8);
|
||||||
|
width: 90%;
|
||||||
|
max-width: 400px;
|
||||||
|
|
||||||
|
animation: fadeIn 0.3s ease-out, scaleIn 0.3s ease-out;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Backdrop styling */
|
||||||
|
#online-users::backdrop {
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Title and content */
|
||||||
|
#online-users .dialog-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#online-users .dialog-content {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #ccc;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button layout */
|
||||||
|
#online-users .dialog-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
#online-users .dialog-button {
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#online-users .dialog-button.primary {
|
||||||
|
background-color: #4f46e5;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#online-users .dialog-button.primary:hover {
|
||||||
|
background-color: #4338ca;
|
||||||
|
}
|
||||||
|
|
||||||
|
#online-users .dialog-button.secondary {
|
||||||
|
background-color: #333;
|
||||||
|
color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
#online-users .dialog-button.secondary:hover {
|
||||||
|
background-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scaleIn {
|
||||||
|
from { transform: scale(0.95) translate(-50%, -50%); opacity: 0; }
|
||||||
|
to { transform: scale(1) translate(-50%, -50%); opacity: 1; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
<dialog id="online-users">
|
||||||
|
<div class="dialog-backdrop">
|
||||||
|
<div class="dialog-box">
|
||||||
|
<div class="dialog-title"><h2>Currently online</h2></div>
|
||||||
|
<div class="dialog-content"><user-list></user-list></div>
|
||||||
|
<div class="dialog-actions">
|
||||||
|
<button class="dialog-button primary">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const onlineDialog = document.getElementById("online-users");
|
||||||
|
const dialogButton = onlineDialog.querySelector('.dialog-button.primary');
|
||||||
|
|
||||||
|
dialogButton.addEventListener('click', () => {
|
||||||
|
onlineDialog.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function showOnlineUsers() {
|
||||||
|
const users = await app.rpc.getOnlineUsers('{{ channel.uid.value }}');
|
||||||
|
onlineDialog.querySelector('user-list').data = users;
|
||||||
|
onlineDialog.showModal();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
@ -9,19 +9,19 @@
|
|||||||
|
|
||||||
|
|
||||||
<section class="chat-area">
|
<section class="chat-area">
|
||||||
<div class="chat-messages">
|
<message-list class="chat-messages">
|
||||||
{% for message in messages %}
|
{% for message in messages %}
|
||||||
{% autoescape false %}
|
{% autoescape false %}
|
||||||
{{ message.html }}
|
{{ message.html }}
|
||||||
{% endautoescape %}
|
{% endautoescape %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</message-list>
|
||||||
<div class="chat-input">
|
<div class="chat-input">
|
||||||
<textarea placeholder="Type a message..." rows="2"></textarea>
|
<textarea list="chat-input-autocomplete-items" placeholder="Type a message..." rows="2" autocomplete="on"></textarea>
|
||||||
<upload-button channel="{{ channel.uid.value }}"></upload-button>
|
<upload-button channel="{{ channel.uid.value }}"></upload-button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
{% include "online.html" %}
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { app } from "/app.js";
|
import { app } from "/app.js";
|
||||||
import { Schedule } from "/schedule.js";
|
import { Schedule } from "/schedule.js";
|
||||||
@ -30,18 +30,95 @@
|
|||||||
function getInputField(){
|
function getInputField(){
|
||||||
return document.querySelector("textarea")
|
return document.querySelector("textarea")
|
||||||
}
|
}
|
||||||
|
getInputField().autoComplete = {
|
||||||
|
"/online": () =>{
|
||||||
|
showOnlineUsers();
|
||||||
|
},
|
||||||
|
"/clear": () => {
|
||||||
|
document.querySelector(".chat-messages").innerHTML = '';
|
||||||
|
},
|
||||||
|
"/live": () =>{
|
||||||
|
getInputField().liveType = !getInputField().liveType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function initInputField(textBox) {
|
function initInputField(textBox) {
|
||||||
textBox.addEventListener('keydown', (e) => {
|
if(textBox.liveType == undefined){
|
||||||
|
textBox.liveType = false
|
||||||
|
}
|
||||||
|
textBox.addEventListener('keydown',async (e) => {
|
||||||
|
if(e.key === "ArrowUp"){
|
||||||
|
const value = findDivAboveText(e.target.value).querySelector('.text')
|
||||||
|
e.target.value = value.textContent
|
||||||
|
console.info("HIERR")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (e.key === "Tab") {
|
||||||
|
|
||||||
|
const message = e.target.value.trim();
|
||||||
|
if (!message) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let autoCompleteHandler = null;
|
||||||
|
Object.keys(e.target.autoComplete).forEach((key)=>{
|
||||||
|
if(key.startsWith(message)){
|
||||||
|
if(autoCompleteHandler){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
autoCompleteHandler = key
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if(autoCompleteHandler){
|
||||||
|
e.preventDefault();
|
||||||
|
e.target.value = autoCompleteHandler;
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const message = e.target.value.trim();
|
const message = e.target.value.trim();
|
||||||
if (message) {
|
if (!message) {
|
||||||
app.rpc.sendMessage(channelUid, message);
|
return
|
||||||
e.target.value = '';
|
|
||||||
}
|
}
|
||||||
|
let autoCompleteHandler = e.target.autoComplete[message]
|
||||||
|
if(autoCompleteHandler){
|
||||||
|
const value = message;
|
||||||
|
e.target.value = '';
|
||||||
|
autoCompleteHandler(value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.target.value = '';
|
||||||
|
if(textBox.liveType && textBox.lastMessageUid && textBox.lastMessageUid != '?'){
|
||||||
|
|
||||||
|
|
||||||
|
app.rpc.updateMessageText(textBox.lastMessageUid, message)
|
||||||
|
textBox.lastMessageUid = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageResponse = await app.rpc.sendMessage(channelUid, message);
|
||||||
|
|
||||||
}else{
|
}else{
|
||||||
|
if(textBox.liveType){
|
||||||
|
|
||||||
|
if(e.target.value[0] == "/"){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if(!textBox.lastMessageUid){
|
||||||
|
textBox.lastMessageUid = '?'
|
||||||
|
app.rpc.sendMessage(channelUid, e.target.value).then((messageResponse)=>{
|
||||||
|
textBox.lastMessageUid = messageResponse
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if(textBox.lastMessageUid == '?'){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
app.rpc.updateMessageText(textBox.lastMessageUid, e.target.value)
|
||||||
|
}
|
||||||
app.rpc.set_typing(channelUid)
|
app.rpc.set_typing(channelUid)
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
document.querySelector("upload-button").addEventListener("upload",function(e){
|
document.querySelector("upload-button").addEventListener("upload",function(e){
|
||||||
@ -81,28 +158,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function triggerGlow(uid) {
|
|
||||||
document.querySelectorAll(".avatar").forEach((el)=>{
|
|
||||||
const div = el.closest('a');
|
|
||||||
if(el.href.indexOf(uid)!=-1){
|
|
||||||
el.classList.add('glow')
|
|
||||||
let originalColor = el.style.backgroundColor
|
|
||||||
//console.error(originalColor)
|
|
||||||
//el.style.backgroundColor = 'black'
|
|
||||||
setTimeout(()=>{
|
|
||||||
// el.style.backgroundColor = originalColor
|
|
||||||
// console.error(el.style.backgroundColor)
|
|
||||||
el.classList.remove('glow')
|
|
||||||
},1200)
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
app.ws.addEventListener("set_typing",(data)=>{
|
|
||||||
triggerGlow(data.data.user_uid)
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
const chatInput = document.querySelector(".chat-area")
|
const chatInput = document.querySelector(".chat-area")
|
||||||
|
@ -16,6 +16,9 @@ from snek.system.model import now
|
|||||||
from snek.system.profiler import Profiler
|
from snek.system.profiler import Profiler
|
||||||
from snek.system.view import BaseView
|
from snek.system.view import BaseView
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class RPCView(BaseView):
|
class RPCView(BaseView):
|
||||||
|
|
||||||
@ -170,10 +173,33 @@ class RPCView(BaseView):
|
|||||||
)
|
)
|
||||||
return channels
|
return channels
|
||||||
|
|
||||||
|
async def update_message_text(self,message_uid, text):
|
||||||
|
self._require_login()
|
||||||
|
message = await self.services.channel_message.get(message_uid)
|
||||||
|
if message["user_uid"] != self.user_uid:
|
||||||
|
raise Exception("Not allowed")
|
||||||
|
await self.services.socket.broadcast(message["channel_uid"], {
|
||||||
|
"channel_uid": message["channel_uid"],
|
||||||
|
"event": "update_message_text",
|
||||||
|
"data": {
|
||||||
|
|
||||||
|
"message_uid": message_uid,
|
||||||
|
"text": text
|
||||||
|
}
|
||||||
|
})
|
||||||
|
message["message"] = text
|
||||||
|
if not text:
|
||||||
|
message['deleted_at'] = now()
|
||||||
|
else:
|
||||||
|
message['deleted_at'] = None
|
||||||
|
await self.services.channel_message.save(message)
|
||||||
|
return True
|
||||||
|
|
||||||
async def send_message(self, channel_uid, message):
|
async def send_message(self, channel_uid, message):
|
||||||
self._require_login()
|
self._require_login()
|
||||||
await self.services.chat.send(self.user_uid, channel_uid, message)
|
message = await self.services.chat.send(self.user_uid, channel_uid, message)
|
||||||
return True
|
|
||||||
|
return message["uid"]
|
||||||
|
|
||||||
async def echo(self, *args):
|
async def echo(self, *args):
|
||||||
self._require_login()
|
self._require_login()
|
||||||
@ -243,12 +269,14 @@ class RPCView(BaseView):
|
|||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
result = {"exception": str(ex), "traceback": traceback.format_exc()}
|
result = {"exception": str(ex), "traceback": traceback.format_exc()}
|
||||||
success = False
|
success = False
|
||||||
|
logger.exception(ex)
|
||||||
if result != "noresponse":
|
if result != "noresponse":
|
||||||
await self._send_json(
|
await self._send_json(
|
||||||
{"callId": call_id, "success": success, "data": result}
|
{"callId": call_id, "success": success, "data": result}
|
||||||
)
|
)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print(str(ex), flush=True)
|
print(str(ex), flush=True)
|
||||||
|
logger.exception(ex)
|
||||||
await self._send_json(
|
await self._send_json(
|
||||||
{"callId": call_id, "success": False, "data": str(ex)}
|
{"callId": call_id, "success": False, "data": str(ex)}
|
||||||
)
|
)
|
||||||
@ -259,15 +287,15 @@ class RPCView(BaseView):
|
|||||||
async def get_online_users(self, channel_uid):
|
async def get_online_users(self, channel_uid):
|
||||||
self._require_login()
|
self._require_login()
|
||||||
|
|
||||||
return [
|
results = [
|
||||||
{
|
record.record async for record in self.services.channel.get_online_users(channel_uid)
|
||||||
"uid": record["uid"],
|
|
||||||
"username": record["username"],
|
|
||||||
"nick": record["nick"],
|
|
||||||
"last_ping": record["last_ping"],
|
|
||||||
}
|
|
||||||
async for record in self.services.channel.get_online_users(channel_uid)
|
|
||||||
]
|
]
|
||||||
|
for result in results:
|
||||||
|
del result['email']
|
||||||
|
del result['password']
|
||||||
|
del result['deleted_at']
|
||||||
|
del result['updated_at']
|
||||||
|
return results
|
||||||
|
|
||||||
async def echo(self, obj):
|
async def echo(self, obj):
|
||||||
await self.ws.send_json(obj)
|
await self.ws.send_json(obj)
|
||||||
@ -314,6 +342,7 @@ class RPCView(BaseView):
|
|||||||
await rpc(msg.json())
|
await rpc(msg.json())
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("Deleting socket", ex, flush=True)
|
print("Deleting socket", ex, flush=True)
|
||||||
|
logger.exception(ex)
|
||||||
await self.services.socket.delete(ws)
|
await self.services.socket.delete(ws)
|
||||||
break
|
break
|
||||||
elif msg.type == web.WSMsgType.ERROR:
|
elif msg.type == web.WSMsgType.ERROR:
|
||||||
|
Loading…
Reference in New Issue
Block a user