diff --git a/src/snek/service/chat.py b/src/snek/service/chat.py new file mode 100644 index 0000000..3dd67e3 --- /dev/null +++ b/src/snek/service/chat.py @@ -0,0 +1,43 @@ + + + +from snek.system.service import BaseService + + +class ChatService(BaseService): + + async def send(self,user_uid, channel_uid, message): + channel_message = await self.services.channel_message.create( + user_uid, + channel_uid, + message + ) + channel_message_uid = channel_message["uid"] + + user = await self.services.user.get(uid=user_uid) + async for channel_member in self.services.channel_member.find( + channel_uid=channel_message["channel_uid"], + is_banned=False, + is_muted=False, + deleted_at=None, + ): + model = await self.new() + model["object_uid"] = channel_message_uid + model["object_type"] = "channel_message" + model["user_uid"] = channel_member["user_uid"] + model["message"] = ( + f"New message from {user['nick']} in {channel_member['label']}." + ) + if not await self.services.channel_member.save(model): + raise Exception(f"Failed to create notification: {model.errors}.") + + sent_to_count = await self.services.socket.broadcast(channel_uid, dict( + message=message, + user_uid=user_uid, + channel_uid=channel_uid, + created_at=channel_message["created_at"], + updated_at=None, + uid=channel_message['uid'], + user_nick=user['nick'] + )) + return sent_to_count \ No newline at end of file diff --git a/src/snek/service/socket.py b/src/snek/service/socket.py new file mode 100644 index 0000000..eb4695c --- /dev/null +++ b/src/snek/service/socket.py @@ -0,0 +1,33 @@ + + + +from snek.system.service import BaseService + + +class SocketService(BaseService): + + def __init__(self, app): + super().__init__(app) + self.sockets = set() + self.subscriptions = {} + + async def add(self, ws): + self.sockets.add(ws) + + async def subscribe(self, ws, channel_uid): + if not channel_uid in self.subscriptions: + self.subscriptions[channel_uid] = set() + self.subscriptions[channel_uid].add(ws) + + async def broadcast(self, channel_uid, message): + print("BROADCAT!",message) + count = 0 + for ws in self.subscriptions.get(channel_uid,[]): + await ws.send_json(message) + count += 1 + return count + async def delete(self, ws): + try: + self.sockets.remove(ws) + except IndexError: + pass \ No newline at end of file diff --git a/src/snek/static/message-list-manager.js b/src/snek/static/message-list-manager.js new file mode 100644 index 0000000..69a3f87 --- /dev/null +++ b/src/snek/static/message-list-manager.js @@ -0,0 +1,23 @@ + + +class MessageListManagerElement extends HTMLElement { + constructor() { + super() + this.attachShadow({mode:'open'}) + this.container = document.createElement("div") + this.shadowRoot.appendChild(this.container) + } + + async connectedCallback() { + let channels = await app.rpc.getChannels() + const me = this + channels.forEach(channel=>{ + const messageList = document.createElement("message-list") + messageList.setAttribute("channel",channel.uid) + me.container.appendChild(messageList) + }) + } + +} + +customElements.define("message-list-manager",MessageListManagerElement) \ No newline at end of file diff --git a/src/snek/static/message-list.js b/src/snek/static/message-list.js new file mode 100644 index 0000000..c1d7b3d --- /dev/null +++ b/src/snek/static/message-list.js @@ -0,0 +1,86 @@ + + +class MessageListElement extends HTMLElement { + + static get observedAttributes() { + return ["messages"]; + } + messages = [] + room = null + url = null + container = null + constructor() { + super() + this.attachShadow({ mode: 'open' }); + this.component = document.createElement('div') + this.shadowRoot.appendChild(this.component ) + } + createElement(message){ + const element = document.createElement("div") + + element.classList.add("message") + const avatar = document.createElement("div") + avatar.classList.add("avatar") + avatar.innerText = message.user_nick[0] + const messageContent = document.createElement("div") + messageContent.classList.add("message-content") + const author = document.createElement("div") + author.classList.add("author") + author.textContent = message.user_nick + const text = document.createElement("div") + text.classList.add("text") + text.textContent = message.message + const time = document.createElement("div") + time.classList.add("time") + time.textContent = message.created_at + messageContent.appendChild(author) + messageContent.appendChild(text) + messageContent.appendChild(time) + element.appendChild(avatar) + element.appendChild(messageContent) + + + + message.element = element + + return element + } + addMessage(message){ + + const obj = new models.Message( + message.uid, + message.channel_uid, + message.user_uid, + message.user_nick, + message.message, + message.created_at, + message.updated_at + ) + const element = this.createElement(obj) + this.messages.push(obj) + this.container.appendChild(element) + return obj + } + connectedCallback() { + const link = document.createElement('link') + link.rel = 'stylesheet' + link.href = '/base.css' + this.component.appendChild(link) + this.container = document.createElement('div') + this.container.classList.add("chat-messages") + this.component.appendChild(this.container) + + this.messages = [] + this.channel_uid = this.getAttribute("channel") + const me = this + app.addEventListener(this.channel_uid, (data) => { + console.info("WIIIIIIIIIIIIIIIIIIIIIIII") + me.addMessage(data) + }) + this.dispatchEvent(new CustomEvent("rendered", {detail:this,bubbles:true})) + + + } +} + +customElements.define('message-list', MessageListElement); \ No newline at end of file diff --git a/src/snek/static/models.js b/src/snek/static/models.js new file mode 100644 index 0000000..6589279 --- /dev/null +++ b/src/snek/static/models.js @@ -0,0 +1,22 @@ +class MessageModel { + message = null + user_uid = null + channel_uid = null + created_at = null + updated_at = null + element = null + constructor(uid, channel_uid,user_uid,user_nick, message,created_at, updated_at){ + this.uid = uid + this.message = message + this.user_uid = user_uid + this.user_nick = user_nick + this.channel_uid = channel_uid + this.created_at = created_at + this.updated_at = updated_at + } +} + +const models = { + Message: MessageModel + +} \ No newline at end of file diff --git a/src/snek/view/rpc.py b/src/snek/view/rpc.py new file mode 100644 index 0000000..b3611ec --- /dev/null +++ b/src/snek/view/rpc.py @@ -0,0 +1,75 @@ +from aiohttp import web +from snek.system.view import BaseView + + +class RPCView(BaseView): + + login_required = True + + class RPCApi: + def __init__(self,view, ws): + self.view = view + self.app = self.view.app + self.services = self.app.services + self.user_uid = self.view.session.get("uid") + self.ws = ws + + + async def get_channels(self): + channels = [] + async for subscription in self.services.channel_member.find(user_uid=self.user_uid,is_banned=False): + channels.append(dict( + name=subscription["label"], + uid=subscription["channel_uid"], + is_moderator=subscription["is_moderator"], + is_read_only=subscription["is_read_only"] + )) + return channels + + async def send_message(self, room, message): + await self.services.chat.send(self.user_uid,room,message) + return True + + + async def echo(self,*args): + return args + + + + + + async def __call__(self, data): + call_id = data.get("callId") + method_name = data.get("method") + args = data.get("args") + if hasattr(super(),method_name) or not hasattr(self,method_name): + return await self.ws.send_json({"callId":call_id,"data":"Not allowed"}) + + method = getattr(self,method_name.replace(".","_"),None) + result = await method(*args) + await self.ws.send_json({"callId":call_id,"data":result}) + + + async def call_ping(self,callId,*args): + return {"pong": args} + + + async def get(self): + + + ws = web.WebSocketResponse() + await ws.prepare(self.request) + await self.services.socket.add(ws) + async for subscription in self.services.channel_member.find(user_uid=self.session.get("uid"),deleted_at=None,is_banned=False): + await self.services.socket.subscribe(ws,subscription["channel_uid"]) + print("Subscribed for: ", subscription["label"],flush=True) + rpc = RPCView.RPCApi(self,ws) + async for msg in ws: + if msg.type == web.WSMsgType.TEXT: + await rpc(msg.json()) + elif msg.type == web.WSMsgType.ERROR: + print(f"WebSocket exception {ws.exception()}") + + await self.services.socket.delete(ws) + print("WebSocket connection closed") + return ws \ No newline at end of file