This commit is contained in:
retoor 2025-01-26 22:51:51 +01:00
parent 4c601e8333
commit 4ae846cf8b
6 changed files with 282 additions and 0 deletions

43
src/snek/service/chat.py Normal file
View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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);

22
src/snek/static/models.js Normal file
View File

@ -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
}

75
src/snek/view/rpc.py Normal file
View File

@ -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