feat: add user presence notifications with debounced departures
This commit is contained in:
parent
e798fd50a8
commit
c23ce6085a
@ -3,6 +3,14 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Version 1.3.0 - 2025-12-17
|
||||||
|
|
||||||
|
Users now receive notifications when other users join or depart the application. Departure notifications are debounced to reduce the frequency of rapid successive alerts.
|
||||||
|
|
||||||
|
**Changes:** 5 files, 418 lines
|
||||||
|
**Languages:** HTML (1 lines), JavaScript (259 lines), Python (158 lines)
|
||||||
|
|
||||||
## Version 1.2.0 - 2025-12-17
|
## Version 1.2.0 - 2025-12-17
|
||||||
|
|
||||||
Removes Umami analytics integration, eliminating user tracking functionality. Developers must handle analytics separately if needed.
|
Removes Umami analytics integration, eliminating user tracking functionality. Developers must handle analytics separately if needed.
|
||||||
|
|||||||
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "Snek"
|
name = "Snek"
|
||||||
version = "1.2.0"
|
version = "1.3.0"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
#license = { file = "LICENSE", content-type="text/markdown" }
|
#license = { file = "LICENSE", content-type="text/markdown" }
|
||||||
description = "Snek Chat Application by Molodetz"
|
description = "Snek Chat Application by Molodetz"
|
||||||
|
|||||||
@ -8,6 +8,8 @@ from snek.system.service import BaseService
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
from snek.system.model import now
|
from snek.system.model import now
|
||||||
|
|
||||||
|
PRESENCE_DEBOUNCE_SECONDS = 3.0
|
||||||
|
|
||||||
|
|
||||||
class SocketService(BaseService):
|
class SocketService(BaseService):
|
||||||
|
|
||||||
@ -16,6 +18,8 @@ class SocketService(BaseService):
|
|||||||
self.ws = ws
|
self.ws = ws
|
||||||
self.is_connected = True
|
self.is_connected = True
|
||||||
self.user = user
|
self.user = user
|
||||||
|
self.user_uid = user["uid"] if user else None
|
||||||
|
self.user_color = user["color"] if user else None
|
||||||
|
|
||||||
async def send_json(self, data):
|
async def send_json(self, data):
|
||||||
if not self.is_connected:
|
if not self.is_connected:
|
||||||
@ -30,7 +34,10 @@ class SocketService(BaseService):
|
|||||||
if not self.is_connected:
|
if not self.is_connected:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
await self.ws.close()
|
try:
|
||||||
|
await self.ws.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
self.is_connected = False
|
self.is_connected = False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@ -41,6 +48,7 @@ class SocketService(BaseService):
|
|||||||
self.users = {}
|
self.users = {}
|
||||||
self.subscriptions = {}
|
self.subscriptions = {}
|
||||||
self.last_update = str(datetime.now())
|
self.last_update = str(datetime.now())
|
||||||
|
self._departure_tasks = {}
|
||||||
|
|
||||||
async def user_availability_service(self):
|
async def user_availability_service(self):
|
||||||
logger.info("User availability update service started.")
|
logger.info("User availability update service started.")
|
||||||
@ -70,15 +78,34 @@ class SocketService(BaseService):
|
|||||||
await asyncio.sleep(60)
|
await asyncio.sleep(60)
|
||||||
|
|
||||||
async def add(self, ws, user_uid):
|
async def add(self, ws, user_uid):
|
||||||
s = self.Socket(ws, await self.app.services.user.get(uid=user_uid))
|
if not user_uid:
|
||||||
|
return None
|
||||||
|
user = await self.app.services.user.get(uid=user_uid)
|
||||||
|
if not user:
|
||||||
|
return None
|
||||||
|
s = self.Socket(ws, user)
|
||||||
self.sockets.add(s)
|
self.sockets.add(s)
|
||||||
s.user["last_ping"] = now()
|
s.user["last_ping"] = now()
|
||||||
await self.app.services.user.save(s.user)
|
await self.app.services.user.save(s.user)
|
||||||
logger.info(f"Added socket for user {s.user['username']}")
|
logger.info(f"Added socket for user {s.user['username']}")
|
||||||
|
|
||||||
|
is_first_connection = False
|
||||||
|
if user_uid in self._departure_tasks:
|
||||||
|
self._departure_tasks[user_uid].cancel()
|
||||||
|
del self._departure_tasks[user_uid]
|
||||||
|
|
||||||
if not self.users.get(user_uid):
|
if not self.users.get(user_uid):
|
||||||
self.users[user_uid] = set()
|
self.users[user_uid] = set()
|
||||||
|
is_first_connection = True
|
||||||
|
elif len(self.users[user_uid]) == 0:
|
||||||
|
is_first_connection = True
|
||||||
self.users[user_uid].add(s)
|
self.users[user_uid].add(s)
|
||||||
|
|
||||||
|
if is_first_connection:
|
||||||
|
await self._broadcast_presence("arrived", user_uid, s.user.get("nick") or s.user.get("username"), s.user_color)
|
||||||
|
|
||||||
|
return s
|
||||||
|
|
||||||
async def subscribe(self, ws, channel_uid, user_uid):
|
async def subscribe(self, ws, channel_uid, user_uid):
|
||||||
if channel_uid not in self.subscriptions:
|
if channel_uid not in self.subscriptions:
|
||||||
self.subscriptions[channel_uid] = set()
|
self.subscriptions[channel_uid] = set()
|
||||||
@ -110,5 +137,58 @@ class SocketService(BaseService):
|
|||||||
async def delete(self, ws):
|
async def delete(self, ws):
|
||||||
for s in [sock for sock in self.sockets if sock.ws == ws]:
|
for s in [sock for sock in self.sockets if sock.ws == ws]:
|
||||||
await s.close()
|
await s.close()
|
||||||
logger.info(f"Removed socket for user {s.user['username']}")
|
user_uid = s.user_uid
|
||||||
self.sockets.remove(s)
|
user_nick = s.user.get("nick") or s.user.get("username") if s.user else None
|
||||||
|
user_color = s.user_color
|
||||||
|
logger.info(f"Removed socket for user {s.user['username'] if s.user else 'unknown'}")
|
||||||
|
self.sockets.discard(s)
|
||||||
|
|
||||||
|
if user_uid:
|
||||||
|
if user_uid in self.users:
|
||||||
|
self.users[user_uid].discard(s)
|
||||||
|
if len(self.users[user_uid]) == 0:
|
||||||
|
await self._schedule_departure(user_uid, user_nick, user_color)
|
||||||
|
|
||||||
|
async def _schedule_departure(self, user_uid, user_nick, user_color):
|
||||||
|
if user_uid in self._departure_tasks:
|
||||||
|
return
|
||||||
|
|
||||||
|
async def delayed_departure():
|
||||||
|
try:
|
||||||
|
await asyncio.sleep(PRESENCE_DEBOUNCE_SECONDS)
|
||||||
|
if user_uid in self.users and len(self.users[user_uid]) == 0:
|
||||||
|
await self._broadcast_presence("departed", user_uid, user_nick, user_color)
|
||||||
|
if user_uid in self._departure_tasks:
|
||||||
|
del self._departure_tasks[user_uid]
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
except Exception as ex:
|
||||||
|
logger.warning(f"Error in departure broadcast: {ex}")
|
||||||
|
|
||||||
|
self._departure_tasks[user_uid] = asyncio.create_task(delayed_departure())
|
||||||
|
|
||||||
|
async def _broadcast_presence(self, event_type, user_uid, user_nick, user_color):
|
||||||
|
if not user_uid or not user_nick:
|
||||||
|
return
|
||||||
|
message = {
|
||||||
|
"event": "user_presence",
|
||||||
|
"data": {
|
||||||
|
"type": event_type,
|
||||||
|
"user_uid": user_uid,
|
||||||
|
"user_nick": user_nick,
|
||||||
|
"user_color": user_color,
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sent_count = 0
|
||||||
|
for s in list(self.sockets):
|
||||||
|
if not s.is_connected:
|
||||||
|
continue
|
||||||
|
if s.user_uid == user_uid:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
if await s.send_json(message):
|
||||||
|
sent_count += 1
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
logger.info(f"Broadcast presence '{event_type}' for {user_nick} to {sent_count} users")
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import { Schedule } from "./schedule.js";
|
|||||||
import { EventHandler } from "./event-handler.js";
|
import { EventHandler } from "./event-handler.js";
|
||||||
import { Socket } from "./socket.js";
|
import { Socket } from "./socket.js";
|
||||||
import { Njet } from "./njet.js";
|
import { Njet } from "./njet.js";
|
||||||
|
import { PresenceNotification } from "./presence-notification.js";
|
||||||
export class RESTClient {
|
export class RESTClient {
|
||||||
debug = false;
|
debug = false;
|
||||||
|
|
||||||
@ -160,7 +161,8 @@ export class App extends EventHandler {
|
|||||||
typeLock = null;
|
typeLock = null;
|
||||||
typeListener = null;
|
typeListener = null;
|
||||||
typeEventChannelUid = null;
|
typeEventChannelUid = null;
|
||||||
_debug = false
|
_debug = false;
|
||||||
|
presenceNotification = null;
|
||||||
async set_typing(channel_uid) {
|
async set_typing(channel_uid) {
|
||||||
this.typeEventChannel_uid = channel_uid;
|
this.typeEventChannel_uid = channel_uid;
|
||||||
}
|
}
|
||||||
@ -192,6 +194,7 @@ export class App extends EventHandler {
|
|||||||
this.ws = new Socket();
|
this.ws = new Socket();
|
||||||
this.rpc = this.ws.client;
|
this.rpc = this.ws.client;
|
||||||
this.audio = new NotificationAudio(500);
|
this.audio = new NotificationAudio(500);
|
||||||
|
this.presenceNotification = new PresenceNotification(this.ws);
|
||||||
this.is_pinging = false;
|
this.is_pinging = false;
|
||||||
this.ping_interval = setInterval(() => {
|
this.ping_interval = setInterval(() => {
|
||||||
this.ping("active");
|
this.ping("active");
|
||||||
|
|||||||
254
src/snek/static/presence-notification.js
Normal file
254
src/snek/static/presence-notification.js
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
// retoor <retoor@molodetz.nl>
|
||||||
|
|
||||||
|
import { EventHandler } from "./event-handler.js";
|
||||||
|
|
||||||
|
class PresenceNotification extends EventHandler {
|
||||||
|
constructor(socket) {
|
||||||
|
super();
|
||||||
|
this._socket = socket;
|
||||||
|
this._container = null;
|
||||||
|
this._maxNotifications = 4;
|
||||||
|
this._displayDuration = 3500;
|
||||||
|
this._fadeDuration = 500;
|
||||||
|
this._queue = [];
|
||||||
|
this._activeCount = 0;
|
||||||
|
this._initialized = false;
|
||||||
|
|
||||||
|
this._init();
|
||||||
|
}
|
||||||
|
|
||||||
|
_init() {
|
||||||
|
if (this._initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", () => this._setup());
|
||||||
|
} else {
|
||||||
|
this._setup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_setup() {
|
||||||
|
if (this._initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._initialized = true;
|
||||||
|
this._createContainer();
|
||||||
|
this._bindEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
_createContainer() {
|
||||||
|
if (this._container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._container = document.createElement("div");
|
||||||
|
this._container.className = "presence-notification-container";
|
||||||
|
this._container.setAttribute("aria-live", "polite");
|
||||||
|
this._container.setAttribute("aria-atomic", "false");
|
||||||
|
|
||||||
|
const style = document.createElement("style");
|
||||||
|
style.textContent = `
|
||||||
|
.presence-notification-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 60px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 10000;
|
||||||
|
pointer-events: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
max-width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presence-toast {
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: inherit;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(100%);
|
||||||
|
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||||
|
pointer-events: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presence-toast.visible {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.presence-toast.fade-out {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.presence-toast.arrived {
|
||||||
|
background: rgba(46, 125, 50, 0.95);
|
||||||
|
color: #fff;
|
||||||
|
border-left: 3px solid #81c784;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presence-toast.departed {
|
||||||
|
background: rgba(66, 66, 66, 0.95);
|
||||||
|
color: #bbb;
|
||||||
|
border-left: 3px solid #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presence-toast-icon {
|
||||||
|
font-size: 11px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presence-toast-text {
|
||||||
|
flex: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presence-toast-nick {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.head.appendChild(style);
|
||||||
|
document.body.appendChild(this._container);
|
||||||
|
}
|
||||||
|
|
||||||
|
_bindEvents() {
|
||||||
|
if (!this._socket) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._socket.addEventListener("user_presence", (data) => {
|
||||||
|
this._handlePresenceEvent(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_handlePresenceEvent(data) {
|
||||||
|
if (!data || !data.type || !data.user_nick) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = data.type;
|
||||||
|
const nick = data.user_nick;
|
||||||
|
const color = data.user_color;
|
||||||
|
|
||||||
|
if (type !== "arrived" && type !== "departed") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._queueNotification(type, nick, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
_queueNotification(type, nick, color) {
|
||||||
|
if (this._activeCount >= this._maxNotifications) {
|
||||||
|
this._queue.push({ type, nick, color });
|
||||||
|
if (this._queue.length > this._maxNotifications * 2) {
|
||||||
|
this._queue.shift();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._showNotification(type, nick, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
_showNotification(type, nick, color) {
|
||||||
|
if (!this._container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._activeCount++;
|
||||||
|
|
||||||
|
const toast = document.createElement("div");
|
||||||
|
toast.className = `presence-toast ${type}`;
|
||||||
|
toast.setAttribute("role", "status");
|
||||||
|
|
||||||
|
const icon = document.createElement("span");
|
||||||
|
icon.className = "presence-toast-icon";
|
||||||
|
icon.textContent = type === "arrived" ? "●" : "○";
|
||||||
|
if (color) {
|
||||||
|
icon.style.color = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = document.createElement("span");
|
||||||
|
text.className = "presence-toast-text";
|
||||||
|
|
||||||
|
const nickSpan = document.createElement("span");
|
||||||
|
nickSpan.className = "presence-toast-nick";
|
||||||
|
nickSpan.textContent = nick;
|
||||||
|
if (color) {
|
||||||
|
nickSpan.style.color = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
const action = document.createTextNode(type === "arrived" ? " arrived" : " departed");
|
||||||
|
|
||||||
|
text.appendChild(nickSpan);
|
||||||
|
text.appendChild(action);
|
||||||
|
|
||||||
|
toast.appendChild(icon);
|
||||||
|
toast.appendChild(text);
|
||||||
|
|
||||||
|
this._container.appendChild(toast);
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
toast.classList.add("visible");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this._removeNotification(toast);
|
||||||
|
}, this._displayDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
_removeNotification(toast) {
|
||||||
|
if (!toast || !toast.parentNode) {
|
||||||
|
this._activeCount = Math.max(0, this._activeCount - 1);
|
||||||
|
this._processQueue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.classList.remove("visible");
|
||||||
|
toast.classList.add("fade-out");
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (toast.parentNode) {
|
||||||
|
toast.parentNode.removeChild(toast);
|
||||||
|
}
|
||||||
|
this._activeCount = Math.max(0, this._activeCount - 1);
|
||||||
|
this._processQueue();
|
||||||
|
}, this._fadeDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
_processQueue() {
|
||||||
|
if (this._queue.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._activeCount >= this._maxNotifications) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = this._queue.shift();
|
||||||
|
if (next) {
|
||||||
|
this._showNotification(next.type, next.nick, next.color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
if (this._container && this._container.parentNode) {
|
||||||
|
this._container.parentNode.removeChild(this._container);
|
||||||
|
}
|
||||||
|
this._container = null;
|
||||||
|
this._queue = [];
|
||||||
|
this._activeCount = 0;
|
||||||
|
this._initialized = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { PresenceNotification };
|
||||||
@ -25,6 +25,7 @@
|
|||||||
<script src="/message-list.js" type="module"></script>
|
<script src="/message-list.js" type="module"></script>
|
||||||
<script src="/chat-input.js" type="module"></script>
|
<script src="/chat-input.js" type="module"></script>
|
||||||
<script src="/container.js" type="module"></script>
|
<script src="/container.js" type="module"></script>
|
||||||
|
<script src="/presence-notification.js" type="module"></script>
|
||||||
<script src="/dumb-term.js" type="module"></script>
|
<script src="/dumb-term.js" type="module"></script>
|
||||||
<link rel="stylesheet" href="/sandbox.css">
|
<link rel="stylesheet" href="/sandbox.css">
|
||||||
<link rel="stylesheet" href="/user-list.css">
|
<link rel="stylesheet" href="/user-list.css">
|
||||||
|
|||||||
@ -627,40 +627,44 @@ class RPCView(BaseView):
|
|||||||
|
|
||||||
ws = web.WebSocketResponse()
|
ws = web.WebSocketResponse()
|
||||||
await ws.prepare(self.request)
|
await ws.prepare(self.request)
|
||||||
if self.request.session.get("logged_in"):
|
user_uid = self.request.session.get("uid") if self.request.session.get("logged_in") else None
|
||||||
await self.services.socket.add(ws, self.request.session.get("uid"))
|
|
||||||
async for subscription in self.services.channel_member.find(
|
|
||||||
user_uid=self.request.session.get("uid"),
|
|
||||||
deleted_at=None,
|
|
||||||
is_banned=False,
|
|
||||||
):
|
|
||||||
await self.services.socket.subscribe(
|
|
||||||
ws, subscription["channel_uid"], self.request.session.get("uid")
|
|
||||||
)
|
|
||||||
if not scheduled and self.request.app.uptime_seconds < 5:
|
|
||||||
await schedule(
|
|
||||||
self.request.session.get("uid"),
|
|
||||||
0,
|
|
||||||
{"event": "refresh", "data": {"message": "Finishing deployment"}},
|
|
||||||
)
|
|
||||||
await schedule(
|
|
||||||
self.request.session.get("uid"),
|
|
||||||
15,
|
|
||||||
{"event": "deployed", "data": {"uptime": self.request.app.uptime}},
|
|
||||||
)
|
|
||||||
|
|
||||||
rpc = RPCView.RPCApi(self, ws)
|
try:
|
||||||
async for msg in ws:
|
if user_uid:
|
||||||
if msg.type == web.WSMsgType.TEXT:
|
await self.services.socket.add(ws, user_uid)
|
||||||
try:
|
async for subscription in self.services.channel_member.find(
|
||||||
await rpc(msg.json())
|
user_uid=user_uid,
|
||||||
except Exception as ex:
|
deleted_at=None,
|
||||||
print("XXXXXXXXXX Deleting socket", ex, flush=True)
|
is_banned=False,
|
||||||
logger.exception(ex)
|
):
|
||||||
await self.services.socket.delete(ws)
|
await self.services.socket.subscribe(
|
||||||
|
ws, subscription["channel_uid"], user_uid
|
||||||
|
)
|
||||||
|
if not scheduled and self.request.app.uptime_seconds < 5:
|
||||||
|
await schedule(
|
||||||
|
user_uid,
|
||||||
|
0,
|
||||||
|
{"event": "refresh", "data": {"message": "Finishing deployment"}},
|
||||||
|
)
|
||||||
|
await schedule(
|
||||||
|
user_uid,
|
||||||
|
15,
|
||||||
|
{"event": "deployed", "data": {"uptime": self.request.app.uptime}},
|
||||||
|
)
|
||||||
|
|
||||||
|
rpc = RPCView.RPCApi(self, ws)
|
||||||
|
async for msg in ws:
|
||||||
|
if msg.type == web.WSMsgType.TEXT:
|
||||||
|
try:
|
||||||
|
await rpc(msg.json())
|
||||||
|
except Exception as ex:
|
||||||
|
logger.exception(ex)
|
||||||
|
break
|
||||||
|
elif msg.type == web.WSMsgType.ERROR:
|
||||||
break
|
break
|
||||||
elif msg.type == web.WSMsgType.ERROR:
|
elif msg.type == web.WSMsgType.CLOSE:
|
||||||
pass
|
break
|
||||||
elif msg.type == web.WSMsgType.CLOSE:
|
finally:
|
||||||
pass
|
await self.services.socket.delete(ws)
|
||||||
|
|
||||||
return ws
|
return ws
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user