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:
 | 
			
		||||
            print(ex, flush=True)
 | 
			
		||||
 | 
			
		||||
        if await self.save(model):
 | 
			
		||||
        if await super().save(model):
 | 
			
		||||
            return model
 | 
			
		||||
        raise Exception(f"Failed to create channel message: {model.errors}.")
 | 
			
		||||
 | 
			
		||||
@ -50,6 +50,12 @@ class ChannelMessageService(BaseService):
 | 
			
		||||
            "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):
 | 
			
		||||
        results = []
 | 
			
		||||
        offset = page * page_size
 | 
			
		||||
 | 
			
		||||
@ -36,4 +36,4 @@ class ChatService(BaseService):
 | 
			
		||||
            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.
 | 
			
		||||
 | 
			
		||||
// 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() {
 | 
			
		||||
        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="/app.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"
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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">
 | 
			
		||||
    <div class="chat-messages">
 | 
			
		||||
    <message-list class="chat-messages">
 | 
			
		||||
    {% for message in messages %}
 | 
			
		||||
        {% autoescape false %}
 | 
			
		||||
        {{ message.html }}
 | 
			
		||||
        {% endautoescape %}
 | 
			
		||||
    {% endfor %}
 | 
			
		||||
    </div>
 | 
			
		||||
    </message-list>
 | 
			
		||||
    <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>
 | 
			
		||||
    </div>
 | 
			
		||||
</section>
 | 
			
		||||
 | 
			
		||||
{% include "online.html" %}
 | 
			
		||||
<script type="module">
 | 
			
		||||
    import { app } from "/app.js";
 | 
			
		||||
    import { Schedule } from "/schedule.js";
 | 
			
		||||
@ -30,18 +30,95 @@
 | 
			
		||||
    function getInputField(){
 | 
			
		||||
        return document.querySelector("textarea")
 | 
			
		||||
    }
 | 
			
		||||
    getInputField().autoComplete = {
 | 
			
		||||
        "/online": () =>{
 | 
			
		||||
            showOnlineUsers();
 | 
			
		||||
        },
 | 
			
		||||
        "/clear": () => {
 | 
			
		||||
            document.querySelector(".chat-messages").innerHTML = '';
 | 
			
		||||
        },
 | 
			
		||||
        "/live": () =>{
 | 
			
		||||
            getInputField().liveType = !getInputField().liveType
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    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) {
 | 
			
		||||
                e.preventDefault();
 | 
			
		||||
                const message = e.target.value.trim();
 | 
			
		||||
                if (message) {
 | 
			
		||||
                    app.rpc.sendMessage(channelUid, message);
 | 
			
		||||
                    e.target.value = '';
 | 
			
		||||
                if (!message) {
 | 
			
		||||
                    return
 | 
			
		||||
                }
 | 
			
		||||
                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{
 | 
			
		||||
		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)
 | 
			
		||||
        
 | 
			
		||||
	    }
 | 
			
		||||
        });
 | 
			
		||||
        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")
 | 
			
		||||
 | 
			
		||||
@ -16,6 +16,9 @@ from snek.system.model import now
 | 
			
		||||
from snek.system.profiler import Profiler
 | 
			
		||||
from snek.system.view import BaseView
 | 
			
		||||
 | 
			
		||||
import logging 
 | 
			
		||||
 | 
			
		||||
logger = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
class RPCView(BaseView):
 | 
			
		||||
 | 
			
		||||
@ -170,10 +173,33 @@ class RPCView(BaseView):
 | 
			
		||||
                )
 | 
			
		||||
            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):
 | 
			
		||||
            self._require_login()
 | 
			
		||||
            await self.services.chat.send(self.user_uid, channel_uid, message)
 | 
			
		||||
            return True
 | 
			
		||||
            message = await self.services.chat.send(self.user_uid, channel_uid, message)
 | 
			
		||||
            
 | 
			
		||||
            return message["uid"]
 | 
			
		||||
 | 
			
		||||
        async def echo(self, *args):
 | 
			
		||||
            self._require_login()
 | 
			
		||||
@ -243,12 +269,14 @@ class RPCView(BaseView):
 | 
			
		||||
                except Exception as ex:
 | 
			
		||||
                    result = {"exception": str(ex), "traceback": traceback.format_exc()}
 | 
			
		||||
                    success = False
 | 
			
		||||
                    logger.exception(ex)
 | 
			
		||||
                if result != "noresponse":
 | 
			
		||||
                    await self._send_json(
 | 
			
		||||
                        {"callId": call_id, "success": success, "data": result}
 | 
			
		||||
                    )
 | 
			
		||||
            except Exception as ex:
 | 
			
		||||
                print(str(ex), flush=True)
 | 
			
		||||
                logger.exception(ex)
 | 
			
		||||
                await self._send_json(
 | 
			
		||||
                    {"callId": call_id, "success": False, "data": str(ex)}
 | 
			
		||||
                )
 | 
			
		||||
@ -259,15 +287,15 @@ class RPCView(BaseView):
 | 
			
		||||
        async def get_online_users(self, channel_uid):
 | 
			
		||||
            self._require_login()
 | 
			
		||||
 | 
			
		||||
            return [
 | 
			
		||||
                {
 | 
			
		||||
                    "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)
 | 
			
		||||
            results = [
 | 
			
		||||
                    record.record 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):
 | 
			
		||||
            await self.ws.send_json(obj)
 | 
			
		||||
@ -314,6 +342,7 @@ class RPCView(BaseView):
 | 
			
		||||
                        await rpc(msg.json())
 | 
			
		||||
                except Exception as ex:
 | 
			
		||||
                    print("Deleting socket", ex, flush=True)
 | 
			
		||||
                    logger.exception(ex)
 | 
			
		||||
                    await self.services.socket.delete(ws)
 | 
			
		||||
                    break
 | 
			
		||||
            elif msg.type == web.WSMsgType.ERROR:
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user